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 numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import { globSync } from 'glob';
constLAMBDA_FUNCTIONS_DIR='./src/lambda-functions-layer';
constLAMBDA_FUNCTION_TIMEOUT=10;
constLAYER_DIR='./node_modules/@sentry/aws-serverless/';
exportconstSAM_PORT=3001;
exportconstSAM_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. */
functionsamLambdaArchitecture(): 'arm64'|'x86_64'{
Expand DownExpand Up@@ -91,7 +91,9 @@ export class LocalLambdaStack extends Stack {
}
}

staticasyncwaitForStack(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.
staticasyncwaitForStack(timeout=180000,port=SAM_PORT){
conststartTime=Date.now();
constmaxWaitTime=timeout;

Expand Down
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
MemberAuthor

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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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 numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import { globSync } from 'glob';
constLAMBDA_FUNCTIONS_DIR='./src/lambda-functions-layer';
constLAMBDA_FUNCTION_TIMEOUT=10;
constLAYER_DIR='./node_modules/@sentry/aws-serverless/';
exportconstSAM_PORT=3001;
exportconstSAM_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. */
functionsamLambdaArchitecture(): 'arm64'|'x86_64'{
Expand DownExpand Up@@ -91,7 +91,9 @@ export class LocalLambdaStack extends Stack {
}
}

staticasyncwaitForStack(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.
staticasyncwaitForStack(timeout=180000,port=SAM_PORT){
conststartTime=Date.now();
constmaxWaitTime=timeout;

Expand Down
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
MemberAuthor

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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import { globSync } from 'glob';
constLAMBDA_FUNCTIONS_DIR='./src/lambda-functions-layer';
constLAMBDA_FUNCTION_TIMEOUT=10;
constLAYER_DIR='./node_modules/@sentry/aws-serverless/';
exportconstSAM_PORT=3001;
exportconstSAM_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. */
functionsamLambdaArchitecture(): 'arm64'|'x86_64'{
Expand DownExpand Up@@ -91,7 +91,9 @@ export class LocalLambdaStack extends Stack {
}
}

staticasyncwaitForStack(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.
staticasyncwaitForStack(timeout=180000,port=SAM_PORT){
conststartTime=Date.now();
constmaxWaitTime=timeout;

Expand Down
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
MemberAuthor

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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import { globSync } from 'glob';
constLAMBDA_FUNCTIONS_DIR='./src/lambda-functions-layer';
constLAMBDA_FUNCTION_TIMEOUT=10;
constLAYER_DIR='./node_modules/@sentry/aws-serverless/';
exportconstSAM_PORT=3001;
exportconstSAM_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. */
functionsamLambdaArchitecture(): 'arm64'|'x86_64'{
Expand DownExpand Up@@ -91,7 +91,9 @@ export class LocalLambdaStack extends Stack {
}
}

staticasyncwaitForStack(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.
staticasyncwaitForStack(timeout=180000,port=SAM_PORT){
conststartTime=Date.now();
constmaxWaitTime=timeout;

Expand Down
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
MemberAuthor

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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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 numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import { globSync } from 'glob';
constLAMBDA_FUNCTIONS_DIR='./src/lambda-functions-layer';
constLAMBDA_FUNCTION_TIMEOUT=10;
constLAYER_DIR='./node_modules/@sentry/aws-serverless/';
exportconstSAM_PORT=3001;
exportconstSAM_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. */
functionsamLambdaArchitecture(): 'arm64'|'x86_64'{
Expand DownExpand Up@@ -91,7 +91,9 @@ export class LocalLambdaStack extends Stack {
}
}

staticasyncwaitForStack(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.
staticasyncwaitForStack(timeout=180000,port=SAM_PORT){
conststartTime=Date.now();
constmaxWaitTime=timeout;

Expand Down
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
MemberAuthor

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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import { globSync } from 'glob';
constLAMBDA_FUNCTIONS_DIR='./src/lambda-functions-layer';
constLAMBDA_FUNCTION_TIMEOUT=10;
constLAYER_DIR='./node_modules/@sentry/aws-serverless/';
exportconstSAM_PORT=3001;
exportconstSAM_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. */
functionsamLambdaArchitecture(): 'arm64'|'x86_64'{
Expand DownExpand Up@@ -91,7 +91,9 @@ export class LocalLambdaStack extends Stack {
}
}

staticasyncwaitForStack(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.
staticasyncwaitForStack(timeout=180000,port=SAM_PORT){
conststartTime=Date.now();
constmaxWaitTime=timeout;

Expand Down
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
MemberAuthor

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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import { globSync } from 'glob';
constLAMBDA_FUNCTIONS_DIR='./src/lambda-functions-layer';
constLAMBDA_FUNCTION_TIMEOUT=10;
constLAYER_DIR='./node_modules/@sentry/aws-serverless/';
exportconstSAM_PORT=3001;
exportconstSAM_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. */
functionsamLambdaArchitecture(): 'arm64'|'x86_64'{
Expand DownExpand Up@@ -91,7 +91,9 @@ export class LocalLambdaStack extends Stack {
}
}

staticasyncwaitForStack(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.
staticasyncwaitForStack(timeout=180000,port=SAM_PORT){
conststartTime=Date.now();
constmaxWaitTime=timeout;

Expand Down
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
MemberAuthor

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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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 numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import { globSync } from 'glob';
constLAMBDA_FUNCTIONS_DIR='./src/lambda-functions-layer';
constLAMBDA_FUNCTION_TIMEOUT=10;
constLAYER_DIR='./node_modules/@sentry/aws-serverless/';
exportconstSAM_PORT=3001;
exportconstSAM_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. */
functionsamLambdaArchitecture(): 'arm64'|'x86_64'{
Expand DownExpand Up@@ -91,7 +91,9 @@ export class LocalLambdaStack extends Stack {
}
}

staticasyncwaitForStack(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.
staticasyncwaitForStack(timeout=180000,port=SAM_PORT){
conststartTime=Date.now();
constmaxWaitTime=timeout;

Expand Down
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
MemberAuthor

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