Skip to content
Open
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 @@ -11,6 +11,7 @@
"@react-router/serve": "^8.0.0",
"@sentry/bun": "file:../../packed/sentry-bun-packed.tgz",
"@sentry/cloudflare": "file:../../packed/sentry-cloudflare-packed.tgz",
"@sentry/core": "file:../../packed/sentry-core-packed.tgz",
"@sentry/deno": "file:../../packed/sentry-deno-packed.tgz",
"@sentry/react-router": "file:../../packed/sentry-react-router-packed.tgz",
"ioredis": "^5.11.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { readdirSync, readFileSync } from 'node:fs';
import path from 'node:path';
import { expect, test } from '@playwright/test';
import { bundleReferencesModule } from '@sentry-internal/test-utils';
import { RUNTIME } from './constants';

// `@sentry/core` is a direct dependency of this app so that Vite resolves it from the app root and
// keeps it external, as in an npm install. Bundled into the server build, it hides the case where
// bundled CommonJS `require()`s `@sentry/core`: Vite 5-7 turn that into a default import, and
// `@sentry/core` has no default export, so the server crashes at startup.
test.describe('server build', () => {
test.skip(RUNTIME === 'cloudflare', 'The Cloudflare build bundles all dependencies.');

test('imports the external @sentry/core by name only', () => {
const root = path.join(process.cwd(), 'build/server');
const serverBundle = readdirSync(root, { recursive: true, encoding: 'utf8' })
.filter(file => file.endsWith('.js'))
.map(file => readFileSync(path.join(root, file), 'utf8'))
.join('\n');

expect(bundleReferencesModule(serverBundle, '@sentry/core'), 'Vite must keep @sentry/core external').toBe(true);
expect(serverBundle.match(/\bimport\s+[\w$]+\s*(?:,\s*\{[^}]*\})?\s*from\s*["']@sentry\/[^"']+["']/g)).toBeNull();
});
});
15 changes: 13 additions & 2 deletions packages/server-utils/src/orchestrion/bundler/vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ function ssrOnlyTransform(transform: Plugin['transform']): Plugin['transform'] {
return transform;
}

// Maps the CJS entry of `@sentry/server-utils` to its ESM entry; any other id is returned unchanged.
function toEsmEntry(id: string): string {
return id.replace(/([/\\])build[/\\]cjs[/\\]index\.js$/, '$1build$1esm$1index.js');
}

/**
* Vite plugin that runs the orchestrion code transform on the bundled output.
*
Expand Down Expand Up @@ -71,15 +76,21 @@ export function sentryOrchestrionPlugin(options: PluginOptions = {}): Plugin {
// normal resolution fails, fall back to this package's own resolution so it
// gets bundled from its real on-disk path. SSR-gated like the transform: the
// specifier only appears in SSR modules.
//
// Always bundle the ESM entry. In a CommonJS module the snippet `require()`s the specifier,
// which resolves to the CJS build. Vite 5-7 turn that build's `require()`s of external
// packages into default imports, and `@sentry/core` has no default export, so the server
// crashes at startup. Vite 8 keeps the `require()`s, which loads a second copy of `@sentry/core`.
async resolveId(source, importer, resolveOptions) {
if (source !== SNIPPET_IMPORT_SPECIFIER || !resolveOptions?.ssr) {
return null;
}
const resolved = await this.resolve(source, importer, { ...resolveOptions, skipSelf: true });
if (resolved) {
return resolved;
return resolved.external ? resolved : { ...resolved, id: toEsmEntry(resolved.id) };
}
return resolveOrchestrionRuntimeRequest(source) ?? null;
const fallback = resolveOrchestrionRuntimeRequest(source);
return fallback ? toEsmEntry(fallback) : null;
},
applyToEnvironment(environment) {
// Orchestrion splices `node:diagnostics_channel` calls into instrumented modules, which only
Expand Down
44 changes: 44 additions & 0 deletions packages/server-utils/test/orchestrion/bundler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,50 @@ describe('sentryOrchestrionPlugin (vite)', () => {
expect(typeof fallback).toBe('string');
expect(fallback).toContain('server-utils');
});

it('resolves the snippet import to the ESM entry, also for require() calls', async () => {
const plugin = vitePlugin();
const resolveId = plugin.resolveId as (
this: unknown,
source: string,
importer: string | undefined,
opts?: { ssr?: boolean },
) => Promise<unknown>;

const resolve = vi.fn().mockResolvedValue(null);
// How Vite 5-7 resolve a `require()` in a CommonJS module.
const requireOptions = { ssr: true, custom: { 'node-resolve': { isRequire: true } } };

resolve.mockResolvedValueOnce({
id: '/app/node_modules/@sentry/server-utils/build/cjs/index.js',
moduleSideEffects: false,
});
await expect(
resolveId.call({ resolve }, '@sentry/server-utils', '/app/node_modules/mysql/index.js', requireOptions),
).resolves.toEqual({
id: '/app/node_modules/@sentry/server-utils/build/esm/index.js',
moduleSideEffects: false,
});
expect(resolve).toHaveBeenCalledWith('@sentry/server-utils', '/app/node_modules/mysql/index.js', {
...requireOptions,
skipSelf: true,
});

resolve.mockResolvedValueOnce({ id: 'C:\\app\\node_modules\\@sentry\\server-utils\\build\\cjs\\index.js' });
await expect(resolveId.call({ resolve }, '@sentry/server-utils', '/x.js', requireOptions)).resolves.toEqual({
id: 'C:\\app\\node_modules\\@sentry\\server-utils\\build\\esm\\index.js',
});

resolve.mockResolvedValueOnce({ id: '@sentry/server-utils', external: true });
await expect(resolveId.call({ resolve }, '@sentry/server-utils', '/x.js', requireOptions)).resolves.toEqual({
id: '@sentry/server-utils',
external: true,
});

const fallback = await resolveId.call({ resolve }, '@sentry/server-utils', '/x.js', requireOptions);
expect(fallback).toMatch(/[/\\]build[/\\]esm[/\\]index\.js$/);
expect(existsSync(fallback as string)).toBe(true);
});
});

describe('sentryOrchestrionPlugin (bun)', () => {
Expand Down
Loading