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
43 changes: 39 additions & 4 deletions packages/cloudflare/src/vite/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,14 +393,49 @@ function wrapDefaultExport(node: ExportDefaultNode, ctx: TransformContext, state
}
}

// `export default <expr>` → `const __SENTRY_DEFAULT_EXPORT__ = <expr>`
// MagicString positions are always relative to the original source.
state.ms.overwrite(node.start, decl.start, 'const __SENTRY_DEFAULT_EXPORT__ = ');
state.ms.append(`\nexport default __SENTRY__.withSentry(${state.optionsFn}, __SENTRY_DEFAULT_EXPORT__);\n`);
// MagicString positions are always relative to the original source. Only the `export default`
// keywords are replaced, so parentheses around the expression stay.
const source = state.ms.original;
const keywordsEnd = node.start + source.slice(node.start, decl.start).lastIndexOf('default') + 'default'.length;
const declaredName =
decl.type === 'ClassDeclaration' || decl.type === 'FunctionDeclaration'
? (decl as ClassDeclarationNode).id?.name
: undefined;

// As with `export { handler as default }`, the export is set in two steps: bound as-is where the
// original statement set it, then rebound at the end of the module to the wrapper, which sees
// methods added in between. See `wrapDefaultSpecifier`.
let wrapped = '__SENTRY_DEFAULT_EXPORT__';
if (declaredName) {
// `export default class Handler {}`: the declaration stays, so `Handler` stays bound in the module.
state.ms.remove(node.start, decl.start);
state.ms.appendLeft(node.end, `\nvar __SENTRY_DEFAULT_EXPORT__ = ${declaredName};`);
wrapped = declaredName;
} else if (isAnonymousFunctionOrClass(decl)) {
// As a property value, the class or function gets the name `default`, as `export default` gives it.
const hasSemicolon = source[node.end - 1] === ';';
state.ms.overwrite(node.start, keywordsEnd, 'var __SENTRY_DEFAULT_EXPORT__ = { default:');
state.ms.appendLeft(hasSemicolon ? node.end - 1 : node.end, hasSemicolon ? ' }.default' : ' }.default;');
} else {
state.ms.overwrite(node.start, keywordsEnd, 'var __SENTRY_DEFAULT_EXPORT__ =');
}
state.ms.appendLeft(node.end, '\nexport { __SENTRY_DEFAULT_EXPORT__ as default };');
state.ms.append(`\n__SENTRY_DEFAULT_EXPORT__ = __SENTRY__.withSentry(${state.optionsFn}, ${wrapped});`);
state.needsImport = true;
state.autoWrapped.add(DEFAULT_EXPORT);
}

function isAnonymousFunctionOrClass(node: BaseNode): boolean {
if (node.type === 'ArrowFunctionExpression') return true;
const isFunctionOrClass = [
'FunctionExpression',
'FunctionDeclaration',
'ClassExpression',
'ClassDeclaration',
].includes(node.type);
return isFunctionOrClass && !(node as ClassDeclarationNode).id;
}
Comment thread
isaacs marked this conversation as resolved.

function handleNamedExport(node: ExportNamedNode, ctx: TransformContext, state: TransformState): void {
const decl = node.declaration;

Expand Down
32 changes: 18 additions & 14 deletions packages/cloudflare/test/vite/autoInstrument.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,9 @@ describe('sentryCloudflareAutoInstrumentPlugin', () => {
expect(resolve).toHaveBeenCalledWith('@sentry/nextjs/cloudflare', join(dir, 'src/index.ts'));
expect(result.code).toBe(
"import * as __SENTRY__ from '@sentry/nextjs/cloudflare';\n" +
'const __SENTRY_DEFAULT_EXPORT__ = { fetch() { return new Response("ok"); } };\n' +
'export default __SENTRY__.withSentry(() => undefined, __SENTRY_DEFAULT_EXPORT__);\n',
'var __SENTRY_DEFAULT_EXPORT__ = { fetch() { return new Response("ok"); } };\n' +
'export { __SENTRY_DEFAULT_EXPORT__ as default };\n' +
'__SENTRY_DEFAULT_EXPORT__ = __SENTRY__.withSentry(() => undefined, __SENTRY_DEFAULT_EXPORT__);',
);
});

Expand All @@ -98,8 +99,9 @@ describe('sentryCloudflareAutoInstrumentPlugin', () => {

expect(result.code).toBe(
"import * as __SENTRY__ from '@sentry/cloudflare';\n" +
'const __SENTRY_DEFAULT_EXPORT__ = { fetch() { return new Response("ok"); } };\n' +
'export default __SENTRY__.withSentry(() => undefined, __SENTRY_DEFAULT_EXPORT__);\n',
'var __SENTRY_DEFAULT_EXPORT__ = { fetch() { return new Response("ok"); } };\n' +
'export { __SENTRY_DEFAULT_EXPORT__ as default };\n' +
'__SENTRY_DEFAULT_EXPORT__ = __SENTRY__.withSentry(() => undefined, __SENTRY_DEFAULT_EXPORT__);',
);
});

Expand All @@ -118,8 +120,9 @@ describe('sentryCloudflareAutoInstrumentPlugin', () => {

expect(result.code).toBe(
"import * as __SENTRY__ from '@sentry/cloudflare';\n" +
'const __SENTRY_DEFAULT_EXPORT__ = { fetch() { return new Response("ok"); } };\n' +
'export default __SENTRY__.withSentry(() => undefined, __SENTRY_DEFAULT_EXPORT__);\n',
'var __SENTRY_DEFAULT_EXPORT__ = { fetch() { return new Response("ok"); } };\n' +
'export { __SENTRY_DEFAULT_EXPORT__ as default };\n' +
'__SENTRY_DEFAULT_EXPORT__ = __SENTRY__.withSentry(() => undefined, __SENTRY_DEFAULT_EXPORT__);',
);
});

Expand All @@ -142,8 +145,9 @@ describe('sentryCloudflareAutoInstrumentPlugin', () => {

expect(result.code).toBe(
"import * as __SENTRY__ from '@sentry/cloudflare';\n" +
'const __SENTRY_DEFAULT_EXPORT__ = { fetch() { return new Response("ok"); } };\n' +
'export default __SENTRY__.withSentry(() => undefined, __SENTRY_DEFAULT_EXPORT__);\n',
'var __SENTRY_DEFAULT_EXPORT__ = { fetch() { return new Response("ok"); } };\n' +
'export { __SENTRY_DEFAULT_EXPORT__ as default };\n' +
'__SENTRY_DEFAULT_EXPORT__ = __SENTRY__.withSentry(() => undefined, __SENTRY_DEFAULT_EXPORT__);',
);
});

Expand Down Expand Up @@ -518,9 +522,9 @@ describe('wranglerConfigPath option', () => {
expect(result.code).toBe(
[
"import * as __SENTRY__ from '@sentry/cloudflare';",
'const __SENTRY_DEFAULT_EXPORT__ = { fetch() { return new Response("ok"); } };',
'export default __SENTRY__.withSentry(() => undefined, __SENTRY_DEFAULT_EXPORT__);',
'',
'var __SENTRY_DEFAULT_EXPORT__ = { fetch() { return new Response("ok"); } };',
'export { __SENTRY_DEFAULT_EXPORT__ as default };',
'__SENTRY_DEFAULT_EXPORT__ = __SENTRY__.withSentry(() => undefined, __SENTRY_DEFAULT_EXPORT__);',
].join('\n'),
);
});
Expand Down Expand Up @@ -596,9 +600,9 @@ describe('package specifier as wrangler `main`', () => {
expect(result.code).toBe(
[
"import * as __SENTRY__ from '@sentry/cloudflare';",
'const __SENTRY_DEFAULT_EXPORT__ = { fetch() { return new Response("ok"); } };',
'export default __SENTRY__.withSentry(() => undefined, __SENTRY_DEFAULT_EXPORT__);',
'',
'var __SENTRY_DEFAULT_EXPORT__ = { fetch() { return new Response("ok"); } };',
'export { __SENTRY_DEFAULT_EXPORT__ as default };',
'__SENTRY_DEFAULT_EXPORT__ = __SENTRY__.withSentry(() => undefined, __SENTRY_DEFAULT_EXPORT__);',
].join('\n'),
);
});
Expand Down
102 changes: 95 additions & 7 deletions packages/cloudflare/test/vite/transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describe('default export wrapping', () => {
const result = transform(code, ctx)!;
expect(result).toBeDefined();
expect(result.code).toContain("import * as __SENTRY__ from '@sentry/cloudflare'");
expect(result.code).toContain('const __SENTRY_DEFAULT_EXPORT__ = handler');
expect(result.code).toContain('var __SENTRY_DEFAULT_EXPORT__ = handler');
expect(result.code).toContain('__SENTRY__.withSentry((env) => ({}), __SENTRY_DEFAULT_EXPORT__)');
expect(result.code).not.toContain('export default handler');
expect(result.map).toBeDefined();
Expand All @@ -62,7 +62,7 @@ describe('default export wrapping', () => {
const code = 'export default { fetch() { return new Response("ok"); } };';
const result = transform(code, ctx)!;
expect(result).toBeDefined();
expect(result.code).toContain('const __SENTRY_DEFAULT_EXPORT__ =');
expect(result.code).toContain('var __SENTRY_DEFAULT_EXPORT__ =');
expect(result.code).toContain('__SENTRY__.withSentry(');
});

Expand Down Expand Up @@ -286,6 +286,94 @@ describe('default export wrapping', () => {
expect(result.code).toContain('tracesSampleRate: 1.0');
});

/**
* Writes `files` to a temp dir, with `a.mjs` transformed and `withSentry` stubbed to mark each
* method it finds as instrumented, and returns what `node <entry>` prints.
*/
function runWithEntry(files: Record<string, string>, entry: string, transformEntry = true): string {
const dir = mkdtempSync(join(tmpdir(), 'sentry-cf-default-'));
try {
for (const [name, code] of Object.entries(files)) {
const out =
name === 'a.mjs' && transformEntry
? transform(code, ctx)!.code.replace(
"import * as __SENTRY__ from '@sentry/cloudflare';",
'const __SENTRY__ = { withSentry: (_options, handler) => { for (const key of Object.keys(handler)) handler[key].instrumented = true; return handler; } };',
)
: code;
writeFileSync(join(dir, name), out);
}
return execFileSync(process.execPath, [join(dir, entry)], { encoding: 'utf8' }).trim();
} finally {
rmSync(dir, { recursive: true, force: true });
}
}

it.each([
'export default class { fetch() {} }',
'export default function () {}',
'export default () => {};',
'export default (class { fetch() {} });',
'export default class Handler { fetch() {} }',
'export default function handler() {}',
])('keeps the name of the default export of `%s`', code => {
const files = { 'a.mjs': code, 'b.mjs': "import handler from './a.mjs';\nconsole.log(handler.name);" };
expect(runWithEntry(files, 'b.mjs')).toBe(runWithEntry(files, 'b.mjs', false));
});

it('wraps a parenthesized default export', () => {
const code = ['const handler = { fetch() {} };', 'export default (handler);'].join('\n');

expect(transform(code, ctx)!.code).toBe(
[
"import * as __SENTRY__ from '@sentry/cloudflare';",
'const handler = { fetch() {} };',
'var __SENTRY_DEFAULT_EXPORT__ = (handler);',
'export { __SENTRY_DEFAULT_EXPORT__ as default };',
'__SENTRY_DEFAULT_EXPORT__ = __SENTRY__.withSentry((env) => ({}), __SENTRY_DEFAULT_EXPORT__);',
].join('\n'),
);
});

it('keeps the module binding of a named default class', () => {
const code = ['export default class Handler { fetch() {} }', 'Handler.ready = true;'].join('\n');

expect(transform(code, ctx)!.code).toBe(
[
"import * as __SENTRY__ from '@sentry/cloudflare';",
'class Handler { fetch() {} }',
'var __SENTRY_DEFAULT_EXPORT__ = Handler;',
'export { __SENTRY_DEFAULT_EXPORT__ as default };',
'Handler.ready = true;',
'__SENTRY_DEFAULT_EXPORT__ = __SENTRY__.withSentry((env) => ({}), Handler);',
].join('\n'),
);
});

it('wraps a method added to the default export after `export default`', () => {
const files = {
'a.mjs': ['const handler = {};', 'export default handler;', 'handler.fetch = function () {};'].join('\n'),
'main.mjs': "import handler from './a.mjs';\nconsole.log(String(handler.fetch.instrumented));",
};

expect(runWithEntry(files, 'main.mjs')).toBe('true');
});

it('sets the default export where the original statement set it', () => {
// `b` reads the default export of `a` while `a` is still evaluating, after its `export default`.
const files = {
'a.mjs': [
"import { readDefault } from './b.mjs';",
'export default { fetch() {} };',
'export const seen = readDefault();',
].join('\n'),
'b.mjs': "import handler from './a.mjs';\nexport function readDefault() { return typeof handler.fetch; }",
'main.mjs': "import { seen } from './a.mjs';\nconsole.log(seen);",
};

expect(runWithEntry(files, 'main.mjs')).toBe('function');
});

it('skips when already wrapped with withSentry', () => {
const code = [
"import { withSentry } from '@sentry/cloudflare';",
Expand Down Expand Up @@ -932,9 +1020,9 @@ describe('agent class wrapping', () => {
'const __SENTRY_WRAPPED_MyDO__ = __SENTRY__.instrumentDurableObjectWithSentry((env) => ({ dsn: env.SENTRY_DSN }), MyDO);',
'export { __SENTRY_WRAPPED_MyDO__ as MyDO };',
'',
'const __SENTRY_DEFAULT_EXPORT__ = { fetch() {} };',
'export default __SENTRY__.withSentry((env) => ({ dsn: env.SENTRY_DSN }), __SENTRY_DEFAULT_EXPORT__);',
'',
'var __SENTRY_DEFAULT_EXPORT__ = { fetch() {} };',
'export { __SENTRY_DEFAULT_EXPORT__ as default };',
'__SENTRY_DEFAULT_EXPORT__ = __SENTRY__.withSentry((env) => ({ dsn: env.SENTRY_DSN }), __SENTRY_DEFAULT_EXPORT__);',
].join('\n'),
);
});
Expand Down Expand Up @@ -1044,8 +1132,8 @@ describe('combined transforms', () => {
expect(result.code).toContain('export { __SENTRY_WRAPPED_MyDO__ as MyDO };');

// Default export wrapped
expect(result.code).toContain('const __SENTRY_DEFAULT_EXPORT__ =');
expect(result.code).toContain('export default __SENTRY__.withSentry(');
expect(result.code).toContain('var __SENTRY_DEFAULT_EXPORT__ =');
expect(result.code).toContain('__SENTRY_DEFAULT_EXPORT__ = __SENTRY__.withSentry(');

// Single import
const importCount = (result.code.match(/import \* as __SENTRY__/g) ?? []).length;
Expand Down
Loading