Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { globSync } from 'glob';
const LAMBDA_FUNCTIONS_DIR = './src/lambda-functions-layer';
const LAMBDA_FUNCTION_TIMEOUT = 10;
const LAYER_DIR = './node_modules/@sentry/aws-serverless/';
export const SAM_PORT = 3001;
export const SAM_PORT = Number(process.env.SAM_PORT) || 7120;

/** Match SAM / Docker to this machine so Apple Silicon does not mix arm64 images with an x86_64 template default. */
function samLambdaArchitecture(): 'arm64' | 'x86_64' {
Expand Down Expand Up @@ -91,7 +91,9 @@ export class LocalLambdaStack extends Stack {
}
}

static async waitForStack(timeout = 60000, port = SAM_PORT) {
// Generous timeout: with `--warm-containers EAGER`, SAM boots every function container
// before the endpoint responds, which can take well over a minute on slow CI runners.
static async waitForStack(timeout = 180000, port = SAM_PORT) {
const startTime = Date.now();
const maxWaitTime = timeout;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export const test = base.extend<{ testEnvironment: LocalLambdaStack; lambdaClien
'local',
'start-lambda',
'--debug',
'--port',
String(SAM_PORT),
'--template',
SAM_TEMPLATE_FILE,
'--warm-containers',
Expand Down Expand Up @@ -79,7 +81,8 @@ export const test = base.extend<{ testEnvironment: LocalLambdaStack; lambdaClien
removeDockerNetwork();
}
},
{ scope: 'worker', auto: true },
// Own timeout so slow SAM stack startup does not eat into the first test's budget.
{ scope: 'worker', auto: true, timeout: 240_000 },
],
lambdaClient: async ({}, use) => {
const lambdaClient = new LambdaClient({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { execFileSync } from 'node:child_process';

const LAMBDA_FUNCTIONS_DIR = './src/lambda-functions-npm';
const LAMBDA_FUNCTION_TIMEOUT = 10;
export const SAM_PORT = 3001;
export const SAM_PORT = Number(process.env.SAM_PORT) || 7120;

/** Match SAM / Docker to this machine so Apple Silicon does not mix arm64 images with an x86_64 template default. */
function samLambdaArchitecture(): 'arm64' | 'x86_64' {
Expand Down Expand Up @@ -123,7 +123,9 @@ export class LocalLambdaStack extends Stack {
}
}

static async waitForStack(timeout = 60000, port = SAM_PORT) {
// Generous timeout: with `--warm-containers EAGER`, SAM boots every function container
// before the endpoint responds, which can take well over a minute on slow CI runners.
static async waitForStack(timeout = 180000, port = SAM_PORT) {
const startTime = Date.now();
const maxWaitTime = timeout;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export const test = base.extend<{ testEnvironment: LocalLambdaStack; lambdaClien
'local',
'start-lambda',
'--debug',
'--port',
String(SAM_PORT),
'--template',
SAM_TEMPLATE_FILE,
'--warm-containers',
Expand Down Expand Up @@ -79,7 +81,8 @@ export const test = base.extend<{ testEnvironment: LocalLambdaStack; lambdaClien
removeDockerNetwork();
}
},
{ scope: 'worker', auto: true },
// Own timeout so slow SAM stack startup does not eat into the first test's budget.
{ scope: 'worker', auto: true, timeout: 240_000 },
],
lambdaClient: async ({}, use) => {
const lambdaClient = new LambdaClient({
Expand Down
5 changes: 5 additions & 0 deletions packages/aws-serverless/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
"default": "./build/npm/esm/awslambda-auto.js"
}
},
"./run-lambda-handler": {
"default": "./build/npm/run-lambda-handler.mjs"
},
"./dist/awslambda-auto": {
"//": "@deprecated Use `./awslambda-auto` instead",
"require": {
Expand Down Expand Up @@ -118,6 +121,8 @@
"outputs": [
"{projectRoot}/build/npm/esm",
"{projectRoot}/build/npm/cjs",
"{projectRoot}/build/npm/run-lambda-handler.mjs",
"{projectRoot}/build/npm/run-lambda-handler.mjs.map",
"{projectRoot}/build/lambda-extension"
],
"cache": true
Expand Down
25 changes: 25 additions & 0 deletions packages/aws-serverless/rollup.npm.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,31 @@
import { makeBaseNPMConfig, makeNPMConfigVariants, makeOtelLoaders } from '@sentry-internal/rollup-utils';

// The handler shim (loaded by the AWS Lambda runtime via the redirected `_HANDLER`) is
// built as a standalone, ESM-only bundle: it uses top-level await to load the user's
// handler module, which cannot be expressed in the CJS variant. Relative imports are
// bundled into the file; bare imports (`@sentry/*`, node builtins) stay external and
// resolve against the installed package at runtime.
function makeHandlerShimConfig() {
const baseConfig = makeBaseNPMConfig({
entrypoints: ['src/run-lambda-handler.ts'],
// Top-level await requires es2022.
esbuild: { target: 'es2022' },
});

return {
...baseConfig,
output: {
...baseConfig.output,
dir: undefined,
file: 'build/npm/run-lambda-handler.mjs',
format: 'esm',
preserveModules: false,
},
};
}

export default [
makeHandlerShimConfig(),
...makeNPMConfigVariants(
makeBaseNPMConfig({
// TODO: `awslambda-auto.ts` is a file which the lambda layer uses to automatically init the SDK. Does it need to be
Expand Down
1 change: 1 addition & 0 deletions packages/aws-serverless/scripts/buildLambdaLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ async function pruneNodeModules(): Promise<void> {
'./build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/build/npm/cjs/index.js',
'./build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/build/npm/cjs/awslambda-auto.js',
'./build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/build/npm/esm/awslambda-auto.js',
'./build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/build/npm/run-lambda-handler.mjs',
];

const { fileList } = await nodeFileTrace(entrypoints, {
Expand Down
119 changes: 119 additions & 0 deletions packages/aws-serverless/src/handlerResolution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// The handler string parsing and module resolution logic in this file is ported from the
// AWS Lambda runtime interface client:
// https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/main/src/UserFunction.js
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as fs from 'node:fs';
import * as path from 'node:path';

export interface ParsedHandler {
/** Directory portion of the handler string, e.g. `src/` for `src/index.handler`. */
moduleRoot: string;
/** Module file name without extension, e.g. `index` for `src/index.handler`. */
moduleName: string;
/** Export path within the module, e.g. `handler` or `nested.handler`. */
functionPath: string;
}

/**
* Parses a Lambda handler string (`<module-root>/<module>.<function-path>`) the same way
* the AWS Lambda runtime interface client does: the module name is the part of the
* basename up to the first dot, the function path is everything after it (and may itself
* contain dots for nested exports).
*
* @see https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/main/src/UserFunction.js
*/
export function parseHandlerString(handlerString: string): ParsedHandler | undefined {
const basename = path.basename(handlerString);
const moduleRoot = handlerString.substring(0, handlerString.length - basename.length);

const match = basename.match(/^([^.]*)\.(.*)$/);
if (!match?.[1] || !match[2]) {
return undefined;
}

return { moduleRoot, moduleName: match[1], functionPath: match[2] };
}

export interface ResolvedHandlerFile {
file: string;
format: 'cjs' | 'esm';
}

/**
* Resolves the handler module to a concrete file, mirroring the AWS Lambda runtime's
* probing order: extensionless file, then `.js` (ESM only when the nearest `package.json`
* declares `"type": "module"`), then `.mjs`, then `.cjs`.
*
* Extensionless files are always CJS: the runtime `require()`s them unconditionally,
* before its `"type": "module"` check (which only affects `.js` files), and Node cannot
* `import()` a file without an extension anyway.
*/
export function resolveHandlerFile(
taskRoot: string,
moduleRoot: string,
moduleName: string,
): ResolvedHandlerFile | undefined {
const basePath = path.resolve(taskRoot, moduleRoot, moduleName);

if (isFile(basePath)) {
return { file: basePath, format: 'cjs' };
Comment thread
cursor[bot] marked this conversation as resolved.
}
if (isFile(`${basePath}.js`)) {
return { file: `${basePath}.js`, format: hasTypeModulePackageJson(path.dirname(basePath)) ? 'esm' : 'cjs' };

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: Do lambdas always have a package.json? Or IOW, could we run into issues here where users define an index.js file in ESM but we identify CJS?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's an issue indeed but that issue also exists for the user, i.e. AWS looks at it the same way. If there's a .js file but no package.json then it automatically treats it as CJS regardless of what's in the file.

}
if (isFile(`${basePath}.mjs`)) {
return { file: `${basePath}.mjs`, format: 'esm' };
}
if (isFile(`${basePath}.cjs`)) {
return { file: `${basePath}.cjs`, format: 'cjs' };
}

return undefined;
}

function isFile(filePath: string): boolean {
try {
return fs.statSync(filePath).isFile();
} catch {
return false;
}
}

/**
* Walks up from `dir` looking for the nearest `package.json`, mirroring the AWS runtime's
* ESM detection. The walk stops at the filesystem root or a `node_modules` boundary.
*/
function hasTypeModulePackageJson(dir: string): boolean {
let current = dir;
while (true) {
const packageJsonPath = path.join(current, 'package.json');
if (isFile(packageJsonPath)) {
try {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) as { type?: string };
return packageJson.type === 'module';
} catch {
return false;
}
}

const parent = path.dirname(current);
if (parent === current || path.basename(current) === 'node_modules') {
return false;
}
current = parent;
}
}
Loading
Loading