Skip to content

Commit 2c34bd6

Browse files
committed
test(nextjs): Add regression tests for node export condition split
Assert that the `node` condition of the root export has an `import`/`require` split whose entries point at files that actually exist in the ESM and CJS build output, and that the ESM server module graph contains no extensionless `next/*` deep imports (which are unresolvable by Node's ESM resolver because `next` ships no `exports` map). Ref #22791
1 parent b038e63 commit 2c34bd6

1 file changed

Lines changed: 79 additions & 0 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { existsSync, readFileSync } from 'node:fs';
2+
import { dirname, resolve } from 'node:path';
3+
import { describe, expect, it } from 'vitest';
4+
5+
/**
6+
* The `node` export condition must offer an `import`/`require` split like its sibling conditions. When it was a bare
7+
* string pointing at the CJS server build, every ESM consumer under a plain Node.js loader received the CJS build
8+
* (`node` matches before the top-level `import` condition), so `cjs-module-lexer` could not see the bindings
9+
* re-exported from `@sentry/core` / `@sentry/node`: named imports failed to link and namespace imports contained
10+
* silently-undefined members.
11+
*
12+
* Regression test for https://github.com/getsentry/sentry-javascript/issues/22791
13+
*/
14+
describe('package.json exports map', () => {
15+
const packageRoot = resolve(__dirname, '..');
16+
const packageJson = JSON.parse(readFileSync(resolve(packageRoot, 'package.json'), 'utf8')) as {
17+
exports: Record<string, Record<string, unknown>>;
18+
};
19+
20+
const nodeCondition = packageJson.exports['.']?.['node'] as Record<string, string> | undefined;
21+
22+
it('has an `import`/`require` split under the `node` condition of the root export', () => {
23+
expect(nodeCondition).toBeInstanceOf(Object);
24+
expect(typeof nodeCondition?.import).toBe('string');
25+
expect(typeof nodeCondition?.require).toBe('string');
26+
});
27+
28+
it('points the `node.import` condition at an existing file in the ESM build output', () => {
29+
expect(nodeCondition?.import).toMatch(/\/esm\//);
30+
expect(existsSync(resolve(packageRoot, nodeCondition?.import as string))).toBe(true);
31+
});
32+
33+
it('points the `node.require` condition at an existing file in the CJS build output', () => {
34+
expect(nodeCondition?.require).toMatch(/\/cjs\//);
35+
expect(existsSync(resolve(packageRoot, nodeCondition?.require as string))).toBe(true);
36+
});
37+
});
38+
39+
/**
40+
* `next` does not declare an `exports` map, so Node's ESM resolver requires explicit file extensions for deep imports
41+
* like `next/constants.js`. Extensionless specifiers work in webpack/turbopack but throw `ERR_MODULE_NOT_FOUND` under
42+
* a plain Node.js loader, which would make the ESM server build (reachable via `node.import`) unloadable.
43+
*
44+
* Regression test for https://github.com/getsentry/sentry-javascript/issues/22791
45+
*/
46+
describe('ESM server build is loadable by plain Node.js', () => {
47+
it('uses explicit file extensions for all `next/*` deep imports in the ESM server module graph', () => {
48+
const packageRoot = resolve(__dirname, '..');
49+
const entry = resolve(packageRoot, 'build/esm/index.server.js');
50+
expect(existsSync(entry)).toBe(true);
51+
52+
const importSpecifierRegex = /(?:from|import)\s*['"]([^'"]+)['"]/g;
53+
const visited = new Set<string>();
54+
const queue = [entry];
55+
const extensionlessNextImports: string[] = [];
56+
57+
while (queue.length > 0) {
58+
const file = queue.pop() as string;
59+
if (visited.has(file) || !existsSync(file)) {
60+
continue;
61+
}
62+
visited.add(file);
63+
64+
const source = readFileSync(file, 'utf8');
65+
for (const match of source.matchAll(importSpecifierRegex)) {
66+
const specifier = match[1] as string;
67+
if (specifier.startsWith('.')) {
68+
const resolved = resolve(dirname(file), specifier);
69+
queue.push(resolved.endsWith('.js') ? resolved : `${resolved}.js`);
70+
} else if (/^next\/.+/.test(specifier) && !/\.[cm]?js$/.test(specifier)) {
71+
extensionlessNextImports.push(`${specifier} (in ${file.replace(packageRoot, '')})`);
72+
}
73+
}
74+
}
75+
76+
expect(visited.size).toBeGreaterThan(1);
77+
expect(extensionlessNextImports).toEqual([]);
78+
});
79+
});

0 commit comments

Comments
 (0)