Skip to content

Commit 45e39c2

Browse files
committed
feat(aws): Create unified lambda layer for ESM and CJS
feat(aws): Create unified lambda layer for ESM and CJS
1 parent 5310112 commit 45e39c2

File tree

8 files changed

+209
-153
lines changed

8 files changed

+209
-153
lines changed

dev-packages/rollup-utils/bundleHelpers.mjs

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -90,32 +90,6 @@ export function makeBaseBundleConfig(options) {
9090
plugins: [rrwebBuildPlugin, markAsBrowserBuildPlugin],
9191
};
9292

93-
// used by `@sentry/aws-serverless`, when creating the lambda layer
94-
const awsLambdaBundleConfig = {
95-
output: {
96-
format: 'cjs',
97-
},
98-
plugins: [
99-
jsonPlugin,
100-
commonJSPlugin,
101-
// Temporary fix for the lambda layer SDK bundle.
102-
// This is necessary to apply to our lambda layer bundle because calling `new ImportInTheMiddle()` will throw an
103-
// that `ImportInTheMiddle` is not a constructor. Instead we modify the code to call `new ImportInTheMiddle.default()`
104-
// TODO: Remove this plugin once the weird import-in-the-middle exports are fixed, released and we use the respective
105-
// version in our SDKs. See: https://github.com/getsentry/sentry-javascript/issues/12009#issuecomment-2126211967
106-
{
107-
name: 'aws-serverless-lambda-layer-fix',
108-
transform: code => {
109-
if (code.includes('ImportInTheMiddle')) {
110-
return code.replaceAll(/new\s+(ImportInTheMiddle.*)\(/gm, 'new $1.default(');
111-
}
112-
},
113-
},
114-
],
115-
// Don't bundle any of Node's core modules
116-
external: builtinModules,
117-
};
118-
11993
const workerBundleConfig = {
12094
output: {
12195
format: 'esm',
@@ -143,7 +117,6 @@ export function makeBaseBundleConfig(options) {
143117
const bundleTypeConfigMap = {
144118
standalone: standAloneBundleConfig,
145119
addon: addOnBundleConfig,
146-
'aws-lambda': awsLambdaBundleConfig,
147120
'node-worker': workerBundleConfig,
148121
};
149122

dev-packages/rollup-utils/plugins/bundlePlugins.mjs

Lines changed: 58 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -86,65 +86,71 @@ export function makeBrowserBuildPlugin(isBrowserBuild) {
8686
});
8787
}
8888

89-
// `terser` options reference: https://github.com/terser/terser#api-reference
90-
// `rollup-plugin-terser` options reference: https://github.com/TrySound/rollup-plugin-terser#options
89+
/**
90+
* Terser options for bundling the SDK.
91+
*
92+
* @see https://github.com/terser/terser#api-reference
93+
* @see https://github.com/TrySound/rollup-plugin-terser#options
94+
* @type {import('terser').MinifyOptions}
95+
*/
96+
export const terserOptions = {
97+
mangle: {
98+
// `captureException` and `captureMessage` are public API methods and they don't need to be listed here, as the
99+
// mangler won't touch user-facing things, but `sentryWrapped` is not user-facing, and would be mangled during
100+
// minification. (We need it in its original form to correctly detect our internal frames for stripping.) All three
101+
// are all listed here just for the clarity's sake, as they are all used in the frames manipulation process.
102+
reserved: ['captureException', 'captureMessage', 'sentryWrapped'],
103+
properties: {
104+
// allow mangling of private field names...
105+
regex: /^_[^_]/,
106+
reserved: [
107+
// ...except for `_experiments`, which we want to remain usable from the outside
108+
'_experiments',
109+
// We want to keep some replay fields unmangled to enable integration tests to access them
110+
'_replay',
111+
'_canvas',
112+
// We also can't mangle rrweb private fields when bundling rrweb in the replay CDN bundles
113+
'_cssText',
114+
// We want to keep the _integrations variable unmangled to send all installed integrations from replay
115+
'_integrations',
116+
// _meta is used to store metadata of replay network events
117+
'_meta',
118+
// We store SDK metadata in the options
119+
'_metadata',
120+
// Object we inject debug IDs into with bundler plugins
121+
'_sentryDebugIds',
122+
// These are used by instrument.ts in utils for identifying HTML elements & events
123+
'_sentryCaptured',
124+
'_sentryId',
125+
// Keeps the frozen DSC on a Sentry Span
126+
'_frozenDsc',
127+
// These are used to keep span & scope relationships
128+
'_sentryRootSpan',
129+
'_sentryChildSpans',
130+
'_sentrySpan',
131+
'_sentryScope',
132+
'_sentryIsolationScope',
133+
// require-in-the-middle calls `Module._resolveFilename`. We cannot mangle this (AWS lambda layer bundle).
134+
'_resolveFilename',
135+
// Set on e.g. the shim feedbackIntegration to be able to detect it
136+
'_isShim',
137+
// This is used in metadata integration
138+
'_sentryModuleMetadata',
139+
],
140+
},
141+
},
142+
output: {
143+
comments: false,
144+
},
145+
}
91146

92147
/**
93148
* Create a plugin to perform minification using `terser`.
94149
*
95150
* @returns An instance of the `terser` plugin
96151
*/
97152
export function makeTerserPlugin() {
98-
return terser({
99-
mangle: {
100-
// `captureException` and `captureMessage` are public API methods and they don't need to be listed here, as the
101-
// mangler won't touch user-facing things, but `sentryWrapped` is not user-facing, and would be mangled during
102-
// minification. (We need it in its original form to correctly detect our internal frames for stripping.) All three
103-
// are all listed here just for the clarity's sake, as they are all used in the frames manipulation process.
104-
reserved: ['captureException', 'captureMessage', 'sentryWrapped'],
105-
properties: {
106-
// allow mangling of private field names...
107-
regex: /^_[^_]/,
108-
reserved: [
109-
// ...except for `_experiments`, which we want to remain usable from the outside
110-
'_experiments',
111-
// We want to keep some replay fields unmangled to enable integration tests to access them
112-
'_replay',
113-
'_canvas',
114-
// We also can't mangle rrweb private fields when bundling rrweb in the replay CDN bundles
115-
'_cssText',
116-
// We want to keep the _integrations variable unmangled to send all installed integrations from replay
117-
'_integrations',
118-
// _meta is used to store metadata of replay network events
119-
'_meta',
120-
// We store SDK metadata in the options
121-
'_metadata',
122-
// Object we inject debug IDs into with bundler plugins
123-
'_sentryDebugIds',
124-
// These are used by instrument.ts in utils for identifying HTML elements & events
125-
'_sentryCaptured',
126-
'_sentryId',
127-
// Keeps the frozen DSC on a Sentry Span
128-
'_frozenDsc',
129-
// These are used to keep span & scope relationships
130-
'_sentryRootSpan',
131-
'_sentryChildSpans',
132-
'_sentrySpan',
133-
'_sentryScope',
134-
'_sentryIsolationScope',
135-
// require-in-the-middle calls `Module._resolveFilename`. We cannot mangle this (AWS lambda layer bundle).
136-
'_resolveFilename',
137-
// Set on e.g. the shim feedbackIntegration to be able to detect it
138-
'_isShim',
139-
// This is used in metadata integration
140-
'_sentryModuleMetadata',
141-
],
142-
},
143-
},
144-
output: {
145-
comments: false,
146-
},
147-
});
153+
return terser(terserOptions);
148154
}
149155

150156
// We don't pass these plugins any options which need to be calculated or changed by us, so no need to wrap them in

packages/aws-serverless/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"/build/loader-hook.mjs"
1616
],
1717
"main": "build/npm/cjs/index.js",
18+
"module": "build/npm/esm/index.js",
1819
"types": "build/npm/types/index.d.ts",
1920
"exports": {
2021
"./package.json": "./package.json",
@@ -73,10 +74,12 @@
7374
"@types/aws-lambda": "^8.10.62"
7475
},
7576
"devDependencies": {
76-
"@types/node": "^18.19.1"
77+
"@types/node": "^18.19.1",
78+
"@vercel/nft": "^0.29.4",
79+
"terser": "^5.43.1"
7780
},
7881
"scripts": {
79-
"build": "run-p build:transpile build:types build:bundle",
82+
"build": "run-p build:transpile build:types",
8083
"build:bundle": "yarn build:layer",
8184
"build:layer": "yarn ts-node scripts/buildLambdaLayer.ts",
8285
"build:dev": "run-p build:transpile build:types",

packages/aws-serverless/rollup.aws.config.mjs

Lines changed: 0 additions & 39 deletions
This file was deleted.

packages/aws-serverless/scripts/buildLambdaLayer.ts

Lines changed: 126 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
/* eslint-disable no-console */
2+
// @ts-expect-error - no types
3+
import { terserOptions } from '@sentry-internal/rollup-utils';
4+
import { nodeFileTrace } from '@vercel/nft';
25
import * as childProcess from 'child_process';
36
import * as fs from 'fs';
7+
import * as path from 'path';
8+
import { minify } from 'terser';
49
import { version } from '../package.json';
510

611
/**
@@ -11,21 +16,19 @@ function run(cmd: string, options?: childProcess.ExecSyncOptions): string {
1116
return String(childProcess.execSync(cmd, { stdio: 'inherit', ...options }));
1217
}
1318

19+
/**
20+
* Build the AWS lambda layer by first installing the local package into `build/aws/dist-serverless/nodejs`.
21+
* Then, prune the node_modules directory to remove unused files by first getting all necessary files with
22+
* `@vercel/nft` and then deleting all other files inside `node_modules`.
23+
* Finally, minify the files and create a zip file of the layer.
24+
*/
1425
async function buildLambdaLayer(): Promise<void> {
15-
// Create the main SDK bundle
16-
run('yarn rollup --config rollup.aws.config.mjs');
17-
18-
// We build a minified bundle, but it's standing in for the regular `index.js` file listed in `package.json`'s `main`
19-
// property, so we have to rename it so it's findable.
20-
fs.renameSync(
21-
'build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/build/npm/cjs/index.debug.min.js',
22-
'build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/build/npm/cjs/index.js',
23-
);
26+
console.log('Installing local @sentry/aws-serverless into build/aws/dist-serverless/nodejs.');
27+
run('npm install . --prefix ./build/aws/dist-serverless/nodejs --install-links --silent');
2428

25-
// We're creating a bundle for the SDK, but still using it in a Node context, so we need to copy in `package.json`,
26-
// purely for its `main` property.
27-
console.log('Copying `package.json` into lambda layer.');
28-
fs.copyFileSync('package.json', 'build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/package.json');
29+
await pruneNodeModules();
30+
fs.unlinkSync('./build/aws/dist-serverless/nodejs/package.json');
31+
fs.unlinkSync('./build/aws/dist-serverless/nodejs/package-lock.json');
2932

3033
// The layer also includes `awslambda-auto.js`, a helper file which calls `Sentry.init()` and wraps the lambda
3134
// handler. It gets run when Node is launched inside the lambda, using the environment variable
@@ -61,3 +64,113 @@ function fsForceMkdirSync(path: string): void {
6164
fs.rmSync(path, { recursive: true, force: true });
6265
fs.mkdirSync(path);
6366
}
67+
68+
async function pruneNodeModules(): Promise<void> {
69+
const entrypoints = [
70+
'./build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/build/npm/esm/index.js',
71+
'./build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/build/npm/cjs/index.js',
72+
'./build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/build/npm/cjs/awslambda-auto.js',
73+
'./build/aws/dist-serverless/nodejs/node_modules/@sentry/aws-serverless/build/npm/esm/awslambda-auto.js',
74+
];
75+
76+
const { fileList } = await nodeFileTrace(entrypoints);
77+
78+
const allFiles = getAllFiles('./build/aws/dist-serverless/nodejs/node_modules');
79+
80+
const filesToDelete = allFiles.filter(file => !fileList.has(file));
81+
console.log(`Removing ${filesToDelete.length} unused files from node_modules.`);
82+
83+
for (const file of filesToDelete) {
84+
try {
85+
fs.unlinkSync(file);
86+
} catch {
87+
console.error(`Error deleting ${file}`);
88+
}
89+
}
90+
91+
console.log('Cleaning up empty directories.');
92+
93+
removeEmptyDirs('./build/aws/dist-serverless/nodejs/node_modules');
94+
95+
await minifyJavaScriptFiles(fileList);
96+
}
97+
98+
function removeEmptyDirs(dir: string): void {
99+
try {
100+
const entries = fs.readdirSync(dir);
101+
102+
for (const entry of entries) {
103+
const fullPath = path.join(dir, entry);
104+
const stat = fs.statSync(fullPath);
105+
if (stat.isDirectory()) {
106+
removeEmptyDirs(fullPath);
107+
}
108+
}
109+
110+
const remainingEntries = fs.readdirSync(dir);
111+
112+
if (remainingEntries.length === 0) {
113+
fs.rmdirSync(dir);
114+
}
115+
} catch (error) {
116+
// Directory might not exist or might not be empty, that's ok
117+
}
118+
}
119+
120+
function getAllFiles(dir: string): string[] {
121+
const files: string[] = [];
122+
123+
function walkDirectory(currentPath: string): void {
124+
try {
125+
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
126+
127+
for (const entry of entries) {
128+
const fullPath = path.join(currentPath, entry.name);
129+
const relativePath = path.relative(process.cwd(), fullPath);
130+
131+
if (entry.isDirectory()) {
132+
walkDirectory(fullPath);
133+
} else {
134+
files.push(relativePath);
135+
}
136+
}
137+
} catch {
138+
console.log(`Skipping directory ${currentPath}`);
139+
}
140+
}
141+
142+
walkDirectory(dir);
143+
return files;
144+
}
145+
146+
async function minifyJavaScriptFiles(fileList: Set<string>): Promise<void> {
147+
console.log('Minifying JavaScript files.');
148+
let minifiedCount = 0;
149+
150+
for (const file of fileList) {
151+
if (!file.endsWith('.js') && !file.endsWith('.mjs') && !file.endsWith('.cjs')) {
152+
continue;
153+
}
154+
155+
// Skip minification for OpenTelemetry files to avoid CommonJS/ESM interop issues
156+
if (file.includes('@opentelemetry')) {
157+
continue;
158+
}
159+
160+
try {
161+
const fullPath = path.resolve(file);
162+
const code = fs.readFileSync(fullPath, 'utf-8');
163+
164+
const result = await minify(code, terserOptions);
165+
166+
if (result.code) {
167+
fs.writeFileSync(fullPath, result.code, 'utf-8');
168+
minifiedCount++;
169+
}
170+
} catch (error) {
171+
console.error(`Error minifying ${file}`, error);
172+
}
173+
}
174+
175+
console.log(`Minified ${minifiedCount} files.`);
176+
}

packages/aws-serverless/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"extends": "../../tsconfig.json",
33

4-
"include": ["src/**/*"],
4+
"include": ["src/**/*", "scripts/**/*"],
55

66
"compilerOptions": {
77
// package-specific options

0 commit comments

Comments
 (0)