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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ function readOrchestrionPluginGraphSources(): string[] {
const entrySource = fs.readFileSync(pluginEntry, 'utf8');
return [
entrySource,
...[...entrySource.matchAll(/require\('(\.\.?\/[^']+)'\)/g)].map(([, specifier]) =>
...[...entrySource.matchAll(/require\(['"](\.\.?\/[^'"]+)['"]\)/g)].map(([, specifier]) =>
fs.readFileSync(path.resolve(path.dirname(pluginEntry), specifier), 'utf8'),
),
];
Expand Down
2 changes: 1 addition & 1 deletion dev-packages/node-integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"scripts": {
"build": "pnpm run build:transpile && pnpm run build:types",
"build:dev": "pnpm run build",
"build:transpile": "rollup -c rollup.npm.config.mjs",
"build:transpile": "rolldown -c rollup.npm.config.mjs",
"build:types": "tsc -p tsconfig.types.json",
"clean": "rimraf -g suites/**/node_modules suites/**/tmp_* && pnpm run clean:script",
"clean:script": "node scripts/clean.js",
Expand Down
14 changes: 14 additions & 0 deletions dev-packages/node-integration-tests/suites/esbuild/app-init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as Sentry from '@sentry/node';

// `Sentry.init()` synchronously calls `registerDiagnosticsChannelInjection()`, which reaches the
// vendored orchestrion transformer. That chain is split so a bundler can tree-shake it (see
// `makeCjsExportsSplitPlugin` in server-utils' rollup config); this asserts the split degrades to
// "no channel injection" rather than throwing when the bundler drops it.
Sentry.init({
dsn: 'https://[email protected]/1337',
tracesSampleRate: 0,
autoSessionTracking: false,
});

// eslint-disable-next-line no-console
console.log(`SENTRY_NODE_INITIALIZED client=${Boolean(Sentry.getClient())}`);
30 changes: 30 additions & 0 deletions dev-packages/node-integration-tests/suites/esbuild/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,34 @@ describe('esbuild bundling', () => {
rmSync(outDir, { recursive: true, force: true });
}
});

test('@sentry/node survives init() when bundled to ESM with tree-shaking', async () => {
const outDir = mkdtempSync(join(tmpdir(), 'sentry-esbuild-esm-'));
const outfile = join(outDir, 'bundle.mjs');

try {
await build({
entryPoints: [join(__dirname, 'app-init.ts')],
outfile,
platform: 'node',
format: 'esm',
bundle: true,
// `@sentry/server-utils` is `sideEffects: false`, so this is where a bundler is free to
// drop the vendored orchestrion transformer.
treeShaking: true,
logLevel: 'silent',
});

const result = spawnSync('node', [outfile], { encoding: 'utf-8' });

// Dropping the transformer must not take `init()` with it: reading a tree-shaken CJS
// container has to yield `undefined`, not throw on destructuring or on a call.
expect(result.stderr).not.toContain('Cannot destructure');
expect(result.stderr).not.toContain('is not a function');
expect(result.status).toBe(0);
expect(result.stdout).toContain('SENTRY_NODE_INITIALIZED client=true');
} finally {
rmSync(outDir, { recursive: true, force: true });
}
});
});
65 changes: 36 additions & 29 deletions dev-packages/rollup-utils/bundleHelpers.mjs
Original file line number Diff line number Diff line change
@@ -1,56 +1,50 @@
/**
* Rollup config docs: https://rollupjs.org/guide/en/#big-list-of-options
* Rolldown config docs: https://rolldown.rs/reference/config-options
*/

import { builtinModules } from 'module';
import * as fs from 'fs';
import * as path from 'path';

import deepMerge from 'deepmerge';

import {
makeBannerOptions,
makeBrowserBuildPlugin,
makeCommonJSPlugin,
makeEsbuildPlugin,
makeIsDebugBuildPlugin,
makeLicensePlugin,
makeNodeResolvePlugin,
makeMinifierOptions,
makeRrwebBuildPlugin,
makeSetSDKSourcePlugin,
makeTerserPlugin,
} from './plugins/index.mjs';
import { mergePlugins } from './utils.mjs';
import { getNodeBuiltIns, mergePlugins, treeShakePreset } from './utils.mjs';
import { makeProductionReplacePlugin } from './plugins/npmPlugins.mjs';

const BUNDLE_VARIANTS = ['.js', '.min.js', '.debug.min.js'];

const packageDotJSON = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), './package.json'), { encoding: 'utf8' }));

export function makeBaseBundleConfig(options) {
const { bundleType, entrypoints, licenseTitle, outputFileBase, packageSpecificConfig, esbuild } = options;
const { bundleType, entrypoints, licenseTitle, outputFileBase, packageSpecificConfig } = options;

const nodeResolvePlugin = makeNodeResolvePlugin();
const transpilePlugin = makeEsbuildPlugin(esbuild);
const markAsBrowserBuildPlugin = makeBrowserBuildPlugin(true);
const licensePlugin = makeLicensePlugin(licenseTitle);
const banner = makeBannerOptions(licenseTitle, packageDotJSON.version);
const rrwebBuildPlugin = makeRrwebBuildPlugin({
excludeIframe: false,
excludeShadowDom: false,
});
const productionReplacePlugin = makeProductionReplacePlugin();

// The `commonjs` plugin is the `esModuleInterop` of the bundling world. When used with `transformMixedEsModules`, it
// will include all dependencies, imported or required, in the final bundle. (Without it, CJS modules aren't included
// at all, and without `transformMixedEsModules`, they're only included if they're imported, not if they're required.)
const commonJSPlugin = makeCommonJSPlugin({ transformMixedEsModules: true });

// used by `@sentry/browser`
const standAloneBundleConfig = {
output: {
banner,
format: 'iife',
name: 'Sentry',
intro: () => {
return 'exports = window.Sentry || {};';
},
},
context: 'window',
plugins: [rrwebBuildPlugin, markAsBrowserBuildPlugin, licensePlugin],
plugins: [rrwebBuildPlugin, markAsBrowserBuildPlugin],
};

// used by `@sentry/wasm` & pluggable integrations from core/browser (bundles which need to be combined with a stand-alone SDK bundle)
Expand All @@ -61,7 +55,7 @@ export function makeBaseBundleConfig(options) {
format: 'cjs',

// code to add before the CJS wrapper
banner: '(function (__window) {',
banner: `${banner}\n(function (__window) {`,

// code to add just inside the CJS wrapper, before any of the wrapped code
intro: 'var exports = {};',
Expand All @@ -84,30 +78,41 @@ export function makeBaseBundleConfig(options) {
// code to add after the CJS wrapper
footer: '}(window));',
},
plugins: [rrwebBuildPlugin, markAsBrowserBuildPlugin, licensePlugin],
plugins: [rrwebBuildPlugin, markAsBrowserBuildPlugin],
};

const workerBundleConfig = {
output: {
banner,
format: 'esm',
minify: makeMinifierOptions(),
},
plugins: [commonJSPlugin, makeTerserPlugin(), licensePlugin],
// Don't bundle any of Node's core modules
external: builtinModules,
external: getNodeBuiltIns(),
};

const awsLambdaExtensionBundleConfig = {
output: {
format: 'esm',
minify: makeMinifierOptions(),
},
plugins: [commonJSPlugin, makeIsDebugBuildPlugin(true), makeTerserPlugin()],
plugins: [makeIsDebugBuildPlugin(true)],
// Don't bundle any of Node's core modules
external: builtinModules,
external: getNodeBuiltIns(),
};

// used by all bundles
const sharedBundleConfig = {
input: entrypoints,

// Point at the package's tsconfig so rolldown picks up its TypeScript & JSX settings.
tsconfig: path.resolve(process.cwd(), './tsconfig.json'),

// ES2020 is our floor: keeps `?.`/`??` native and downlevels everything newer.
transform: {
target: 'es2020',
},

output: {
// a file extension will be added to this base value when we specify either a minified or non-minified build
entryFileNames: outputFileBase,
Expand All @@ -116,8 +121,8 @@ export function makeBaseBundleConfig(options) {
strict: false,
esModule: false,
},
plugins: [productionReplacePlugin, transpilePlugin, nodeResolvePlugin],
treeshake: 'smallest',
plugins: [productionReplacePlugin],
treeshake: treeShakePreset('smallest'),
};

const bundleTypeConfigMap = {
Expand Down Expand Up @@ -147,7 +152,7 @@ export function makeBundleConfigVariants(baseConfig, options = {}) {

const includeDebuggingPlugin = makeIsDebugBuildPlugin(true);
const stripDebuggingPlugin = makeIsDebugBuildPlugin(false);
const terserPlugin = makeTerserPlugin();
const minify = makeMinifierOptions();
const setSdkSourcePlugin = makeSetSDKSourcePlugin('cdn');

// The additional options to use for each variant we're going to create.
Expand All @@ -162,15 +167,17 @@ export function makeBundleConfigVariants(baseConfig, options = {}) {
'.min.js': {
output: {
entryFileNames: chunkInfo => `${baseConfig.output.entryFileNames(chunkInfo)}.min.js`,
minify,
},
plugins: [stripDebuggingPlugin, setSdkSourcePlugin, terserPlugin],
plugins: [stripDebuggingPlugin, setSdkSourcePlugin],
},

'.debug.min.js': {
output: {
entryFileNames: chunkInfo => `${baseConfig.output.entryFileNames(chunkInfo)}.debug.min.js`,
minify,
},
plugins: [includeDebuggingPlugin, setSdkSourcePlugin, terserPlugin],
plugins: [includeDebuggingPlugin, setSdkSourcePlugin],
},
};

Expand Down
2 changes: 1 addition & 1 deletion dev-packages/rollup-utils/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ export { plugins };

export * from './bundleHelpers.mjs';
export * from './npmHelpers.mjs';
export { insertAt } from './utils.mjs';
export { insertAt, treeShakePreset } from './utils.mjs';
68 changes: 42 additions & 26 deletions dev-packages/rollup-utils/npmHelpers.mjs
Original file line number Diff line number Diff line change
@@ -1,27 +1,25 @@
// @ts-check

/**
* Rollup config docs: https://rollupjs.org/guide/en/#big-list-of-options
* Rolldown config docs: https://rolldown.rs/reference/config-options
*/

import * as fs from 'fs';
import { builtinModules, isBuiltin } from 'module';
import { isBuiltin } from 'module';
import * as path from 'path';
import { fileURLToPath } from 'url';

import deepMerge from 'deepmerge';

import { defineConfig } from 'rollup';
import { defineConfig } from 'rolldown';
import {
makeDebugBuildStatementReplacePlugin,
makeEsbuildPlugin,
makeEsmCjsReplacePlugin,
makeNodeResolvePlugin,
makeProductionReplacePlugin,
makeRrwebBuildPlugin,
} from './plugins/index.mjs';
import { makePackageNodeEsm } from './plugins/make-esm-plugin.mjs';
import { mergeExternals, mergePlugins } from './utils.mjs';
import { getNodeBuiltIns, mergeExternals, mergePlugins } from './utils.mjs';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

Expand Down Expand Up @@ -97,20 +95,17 @@ export function makeBaseNPMConfig(options = {}) {
entrypoints = ['src/index.ts'],
hasBundles = false,
packageSpecificConfig = {},
esbuild = {},
bundledBuiltins = [],
} = options;

const nodeResolvePlugin = makeNodeResolvePlugin();
const transpilePlugin = makeEsbuildPlugin(esbuild);
const debugBuildStatementReplacePlugin = makeDebugBuildStatementReplacePlugin();
const rrwebBuildPlugin = makeRrwebBuildPlugin({
excludeShadowDom: undefined,
excludeIframe: undefined,
});

const deps = [
...builtinModules.filter(m => !bundledBuiltins.includes(m)),
...getNodeBuiltIns(bundledBuiltins),
...Object.keys(packageDotJSON.dependencies || {}),
...Object.keys(packageDotJSON.peerDependencies || {}),
...Object.keys(packageDotJSON.optionalDependencies || {}),
Expand All @@ -119,6 +114,31 @@ export function makeBaseNPMConfig(options = {}) {
const defaultBaseConfig = {
input: entrypoints,

// Point at the package's tsconfig so rolldown picks up its TypeScript & JSX settings.
tsconfig: path.resolve(process.cwd(), './tsconfig.json'),

// NOTE: we deliberately leave `platform` unset so rolldown infers it from the output format
// ('node' for cjs, 'browser' for everything else). Every explicit value breaks something:
// 'node' injects `import "node:module"` into each runtime chunk, which webpack rejects for
// browser targets; 'neutral' rewrites `import.meta` to `{}`, so `createRequire(import.meta.url)`
// gets `undefined`. Node-only packages that need the node platform for both halves set it
// themselves (see server-utils and bundler-plugins).

// ES2020 is our floor: keeps `?.`/`??` native and downlevels everything newer. Packages that
// need more (e.g. top-level await) raise it through `packageSpecificConfig`.
transform: {
target: 'es2020',

// The inferred 'browser' platform defines `process.env.NODE_ENV`, which folds every
// `process.env.NODE_ENV !== 'development'` guard to a constant and lets DCE delete the
// branch behind it. That silently dropped the orchestrion and source-map plugins from the
// vite integrations' ESM output while the CJS half kept them. Mapping it to itself keeps
// the runtime lookup, so the guard is evaluated by the consumer as it always was.
define: {
'process.env.NODE_ENV': 'process.env.NODE_ENV',
},
},

output: {
// an appropriately-named directory will be added to this base value when we specify either a cjs or esm build
dir: hasBundles ? 'build/npm' : 'build',
Expand All @@ -130,6 +150,7 @@ export function makeBaseNPMConfig(options = {}) {

// output individual files rather than one big bundle
preserveModules: true,
preserveModulesRoot: 'src',

// Don't hoist imports into entrypoints
// should be ignored when `preserveModules` is used,
Expand All @@ -139,8 +160,6 @@ export function makeBaseNPMConfig(options = {}) {
// Allow wrappers or helper functions generated by rollup to use any ES2015 features
generatedCode: {
preset: 'es2015',
// With arrow functions, Node can't see `export *` exports when ESM code imports our CJS files.
arrowFunctions: false,
},

// don't add `"use strict"` to the top of cjs files
Expand All @@ -154,19 +173,17 @@ export function makeBaseNPMConfig(options = {}) {
// get: () => are.great,
// });
externalLiveBindings: false,

// Don't call `Object.freeze` on the results of `import * as someModule from '...'`
// (We don't need it, so why waste the bytes?)
freeze: false,

// Assume externals are ESM-shaped (`__esModule` + `.default`), which our own `@sentry/*`
// packages satisfy via `esModule: 'if-default-prop'`. This keeps `import * as x` a live
// reference to the real module rather than an `_interopNamespace` copy — instrumentation code
// relies on that to monkey-patch modules like `fs` in place. Packages that pull in bare-CJS
// third-party deps (no `.default`) override this per-module (see server-utils).
interop: 'esModule',
},

// NOTE: rolldown has no equivalent of rollup's `output.interop`. In the CJS build it wraps
// namespace imports of externals as `x = __toESM(require('x'))`, and `__toESM` returns a copy
// whose properties are getter-only forwarders. Reads pass through to the real module but writes
// do not, so code that monkey-patches a module in place has to default-import it (`import x
// from 'x'`, whose `.default` is the live `require()` result) rather than namespace-import it.
// Rolldown errors on a statically visible `ns.foo = ...` (ASSIGN_TO_IMPORT), but it cannot see a
// write made through a helper, and that one fails silently. See
// `packages/node/src/integrations/fs/vendored/instrumentation.ts`.

treeshake: {
moduleSideEffects: (id, external) => {
if (external === false && ignoreSideEffects.test(id)) {
Expand All @@ -186,7 +203,7 @@ export function makeBaseNPMConfig(options = {}) {
},
},

plugins: [nodeResolvePlugin, transpilePlugin, debugBuildStatementReplacePlugin, rrwebBuildPlugin],
plugins: [debugBuildStatementReplacePlugin, rrwebBuildPlugin],

// don't include imported modules from outside the package in the final output
// also treat subpath exports (e.g. `@sentry/core/browser`) as external
Expand Down Expand Up @@ -248,8 +265,7 @@ export function makeNPMConfigVariants(baseConfig, options = {}) {
}

return variantSpecificConfigs.map(variant =>
// Plugin arrays must be merged in the right order or the build silently misbehaves
// (e.g. esbuild strips dev-mode marker comments before the replace plugin can act).
// Plugin arrays must be merged in the right order or the build silently misbehaves.
deepMerge(baseConfig, variant, {
customMerge: key => (key === 'plugins' ? mergePlugins : undefined),
}),
Expand Down
Loading
Loading