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
173 changes: 172 additions & 1 deletion src/tools/esbuild/node-modules-bundler.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { requiresLinking } from './node-modules-bundler.js';
import {
buildSyntheticCjsEntry,
canSkipTransform,
isCommonjsCandidate,
isEsmInteropError,
isIdentifierName,
planCjsWrap,
requiresLinking,
} from './node-modules-bundler.js';

describe('requiresLinking', () => {
it('returns true for partially-compiled sources containing a declaration prefix', () => {
Expand Down Expand Up @@ -32,3 +40,166 @@ describe('requiresLinking', () => {
expect(requiresLinking('/node_modules/@angular/common/fesm2022/common.mjs', source)).toBe(true);
});
});

describe('canSkipTransform', () => {
it('skips .mjs files in dev that do not need linking', () => {
expect(canSkipTransform('/node_modules/some-lib/index.mjs', false, false)).toBe(true);
});

it('does not skip .js files in dev (may be CommonJS/UMD)', () => {
expect(canSkipTransform('/node_modules/dayjs/dayjs.min.js', false, false)).toBe(false);
});

it('does not skip when advancedOptimizations is enabled', () => {
expect(canSkipTransform('/node_modules/some-lib/index.mjs', false, true)).toBe(false);
});

it('does not skip when linking is required', () => {
expect(canSkipTransform('/node_modules/my-design-system/fesm2022/lib.mjs', true, false)).toBe(
false
);
});
});

describe('isIdentifierName', () => {
it('accepts valid identifiers', () => {
expect(isIdentifierName('isDayjs')).toBe(true);
expect(isIdentifierName('_private')).toBe(true);
expect(isIdentifierName('$dollar')).toBe(true);
expect(isIdentifierName('en')).toBe(true);
});

it('accepts reserved words (valid after `as` in an export clause)', () => {
expect(isIdentifierName('default')).toBe(true);
expect(isIdentifierName('class')).toBe(true);
expect(isIdentifierName('import')).toBe(true);
});

it('rejects non-identifier keys', () => {
expect(isIdentifierName('foo-bar')).toBe(false);
expect(isIdentifierName('1abc')).toBe(false);
expect(isIdentifierName('with space')).toBe(false);
expect(isIdentifierName('')).toBe(false);
});
});

describe('planCjsWrap', () => {
it('wraps a UMD function whose named exports hang off it (dayjs shape)', () => {
const dayjs = (() => undefined) as (() => void) & Record<string, unknown>;
dayjs.extend = () => undefined;
dayjs.isDayjs = () => undefined;
dayjs.locale = () => undefined;

const plan = planCjsWrap(dayjs);
expect(plan.wrap).toBe(true);
expect(plan.keys).toEqual(['extend', 'isDayjs', 'locale']);
});

it('wraps a plain CommonJS exports object', () => {
expect(planCjsWrap({ Op: 1, AttributeMap: 2 })).toEqual({
wrap: true,
keys: ['Op', 'AttributeMap'],
});
});

it('does not wrap an ES module namespace', () => {
const ns = { foo: 1, default: 2, [Symbol.toStringTag]: 'Module' };
expect(planCjsWrap(ns)).toEqual({ wrap: false, keys: [] });
});

it('does not wrap a transpiled __esModule interop object', () => {
expect(planCjsWrap({ __esModule: true, foo: 1 })).toEqual({ wrap: false, keys: [] });
});

it('does not wrap when there are no re-exportable named keys', () => {
expect(planCjsWrap({ default: 1 })).toEqual({ wrap: false, keys: [] });
const fn = () => undefined;
expect(planCjsWrap(fn)).toEqual({ wrap: false, keys: [] });
});

it('excludes default, __esModule and invalid identifiers but keeps reserved words', () => {
const plan = planCjsWrap({ good: 1, class: 2, default: 3, __esModule: false, 'bad-key': 4 });
expect(plan.keys).toEqual(['good', 'class']);
});

it('does not wrap primitives or null', () => {
expect(planCjsWrap(null)).toEqual({ wrap: false, keys: [] });
expect(planCjsWrap(42)).toEqual({ wrap: false, keys: [] });
expect(planCjsWrap('str')).toEqual({ wrap: false, keys: [] });
});
});

describe('buildSyntheticCjsEntry', () => {
it('re-exports default plus each named binding via an alias export clause', () => {
const out = buildSyntheticCjsEntry('/abs/dayjs.min.js', ['isDayjs', 'extend']);
expect(out).toBe(
[
'import _nfDefault from "/abs/dayjs.min.js";',
'export default _nfDefault;',
'const _nf0 = _nfDefault["isDayjs"];',
'const _nf1 = _nfDefault["extend"];',
'export { _nf0 as isDayjs, _nf1 as extend };',
'',
].join('\n')
);
});

it('re-exports a reserved-word key without escaping (valid after `as`)', () => {
const out = buildSyntheticCjsEntry('/abs/x.js', ['class']);
expect(out).toContain('const _nf0 = _nfDefault["class"];');
expect(out).toContain('export { _nf0 as class };');
});

it('emits only the default re-export when there are no named keys', () => {
const out = buildSyntheticCjsEntry('/abs/x.js', []);
expect(out).toBe('import _nfDefault from "/abs/x.js";\nexport default _nfDefault;\n');
});

it('escapes the import path (Windows backslashes)', () => {
const out = buildSyntheticCjsEntry('C:\\pkg\\index.js', []);
expect(out).toContain('import _nfDefault from "C:\\\\pkg\\\\index.js";');
});
});

describe('isCommonjsCandidate', () => {
it('treats .cjs as CommonJS regardless of package type', () => {
expect(isCommonjsCandidate('/pkg/index.cjs', undefined)).toBe(true);
expect(isCommonjsCandidate('/pkg/index.cjs', 'module')).toBe(true);
});

it('treats .mjs as ESM regardless of package type', () => {
expect(isCommonjsCandidate('/pkg/core.mjs', undefined)).toBe(false);
expect(isCommonjsCandidate('/pkg/core.mjs', 'commonjs')).toBe(false);
});

it('treats .js as CommonJS unless the nearest package.json is type: module', () => {
expect(isCommonjsCandidate('/pkg/dayjs.min.js', undefined)).toBe(true);
expect(isCommonjsCandidate('/pkg/index.js', 'commonjs')).toBe(true);
expect(isCommonjsCandidate('/pkg/index.js', 'module')).toBe(false);
});

it('does not treat other extensions as CommonJS candidates', () => {
expect(isCommonjsCandidate('/pkg/data.json', undefined)).toBe(false);
expect(isCommonjsCandidate('/pkg/index.wasm', 'commonjs')).toBe(false);
});
});

describe('isEsmInteropError', () => {
it('recognizes require()-of-ESM error codes as benign', () => {
expect(isEsmInteropError({ code: 'ERR_REQUIRE_ESM' })).toBe(true);
expect(isEsmInteropError({ code: 'ERR_REQUIRE_ASYNC_MODULE' })).toBe(true);
});

it('recognizes ESM-syntax parse failures as benign', () => {
expect(isEsmInteropError(new SyntaxError("Unexpected token 'export'"))).toBe(true);
expect(isEsmInteropError(new SyntaxError('Cannot use import statement outside a module'))).toBe(
true
);
});

it('does not mask genuine runtime failures', () => {
expect(isEsmInteropError(new ReferenceError('window is not defined'))).toBe(false);
expect(isEsmInteropError(new TypeError('Cannot read properties of undefined'))).toBe(false);
expect(isEsmInteropError(undefined)).toBe(false);
});
});
Loading
Loading