Skip to content

Commit 87a7683

Browse files
authoredJun 7, 2024··
fix(esm): ignore transforming .js files with CJS syntax (#40)
fixes #577
1 parent 0a78bfd commit 87a7683

File tree

6 files changed

+92
-67
lines changed

6 files changed

+92
-67
lines changed
 

‎src/cjs/api/module-extensions.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Module from 'node:module';
33
import type { TransformOptions } from 'esbuild';
44
import { transformSync } from '../../utils/transform/index.js';
55
import { transformDynamicImport } from '../../utils/transform/transform-dynamic-import.js';
6-
import { isESM } from '../../utils/esm-pattern.js';
6+
import { isESM } from '../../utils/es-module-lexer.js';
77
import { shouldApplySourceMap, inlineSourceMap } from '../../source-map.js';
88
import { parent } from '../../utils/ipc/client.js';
99
import { fileMatcher } from '../../utils/tsconfig.js';

‎src/esm/hook/load.ts

+35-27
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { parent } from '../../utils/ipc/client.js';
1010
import type { Message } from '../types.js';
1111
import { fileMatcher } from '../../utils/tsconfig.js';
1212
import { isJsonPattern, tsExtensionsPattern } from '../../utils/path-utils.js';
13+
import { isESM } from '../../utils/es-module-lexer.js';
1314
import { getNamespace } from './utils.js';
1415
import { data } from './initialize.js';
1516

@@ -70,34 +71,41 @@ export const load: LoadHook = async (
7071
&& loaded.responseURL?.startsWith('file:') // Could be data:
7172
&& !filePath.endsWith('.cjs') // CJS syntax doesn't need to be transformed for interop
7273
) {
73-
/**
74-
* es or cjs module lexer unfortunately cannot be used because it doesn't support
75-
* typescript syntax
76-
*
77-
* While the full code is transformed, only the exports are used for parsing.
78-
* In fact, the code can't even run because imports cannot be resolved relative
79-
* from the data: URL.
80-
*
81-
* TODO: extract exports only
82-
*/
83-
const transformed = await transform(
84-
await readFile(new URL(url), 'utf8'),
85-
filePath,
86-
{
87-
format: 'cjs',
88-
89-
// CJS Annotations for Node
90-
platform: 'node',
91-
// TODO: disable source maps
92-
},
93-
);
94-
95-
const parameters = new URLSearchParams({ filePath });
96-
if (urlNamespace) {
97-
parameters.set('namespace', urlNamespace);
74+
const code = await readFile(new URL(url), 'utf8');
75+
76+
// if the file extension is .js, only transform if using esm syntax
77+
if (!filePath.endsWith('.js') || isESM(code)) {
78+
/**
79+
* es or cjs module lexer unfortunately cannot be used because it doesn't support
80+
* typescript syntax
81+
*
82+
* While the full code is transformed, only the exports are used for parsing.
83+
* In fact, the code can't even run because imports cannot be resolved relative
84+
* from the data: URL.
85+
*
86+
* TODO: extract exports only
87+
*/
88+
const transformed = await transform(
89+
code,
90+
filePath,
91+
{
92+
format: 'cjs',
93+
94+
// CJS Annotations for Node
95+
platform: 'node',
96+
// TODO: disable source maps
97+
},
98+
);
99+
100+
const parameters = new URLSearchParams({ filePath });
101+
if (urlNamespace) {
102+
parameters.set('namespace', urlNamespace);
103+
}
104+
105+
// TODO: re-exports from relative paths cant get detected because of the data URL
106+
loaded.responseURL = `data:text/javascript,${encodeURIComponent(transformed.code)}?${parameters.toString()}`;
107+
return loaded;
98108
}
99-
loaded.responseURL = `data:text/javascript,${encodeURIComponent(transformed.code)}?${parameters.toString()}`;
100-
return loaded;
101109
}
102110

103111
// CommonJS and Internal modules (e.g. node:*)

‎src/utils/es-module-lexer.ts

+36
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,39 @@ export const parseEsm = (
1616
? parseWasm(code, filename)
1717
: parseJs(code, filename)
1818
);
19+
20+
/*
21+
Previously, this regex was used as a naive ESM catch,
22+
but turns out regex is slower than the lexer so removing
23+
it made the check faster.
24+
25+
Catches:
26+
import a from 'b'
27+
import 'b';
28+
import('b');
29+
export{a};
30+
export default a;
31+
32+
Doesn't catch:
33+
EXPORT{a}
34+
exports.a = 1
35+
module.exports = 1
36+
37+
const esmPattern = /\b(?:import|export)\b/;
38+
*/
39+
40+
export const isESM = (code: string) => {
41+
if (!code.includes('import') && !code.includes('export')) {
42+
return false;
43+
}
44+
try {
45+
const hasModuleSyntax = parseEsm(code)[3];
46+
return hasModuleSyntax;
47+
} catch {
48+
/**
49+
* If it fails to parse, there's a syntax error
50+
* Let esbuild handle it for better error messages
51+
*/
52+
return true;
53+
}
54+
};

‎src/utils/esm-pattern.ts

-37
This file was deleted.

‎tests/fixtures.ts

+10
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,16 @@ export const files = {
257257
}),
258258
'index.js': syntaxLowering,
259259
'ts.ts': syntaxLowering,
260+
'cjs.js': `
261+
const _ = exports;
262+
const cjsJs = true;
263+
_.cjsJs = cjsJs;
264+
265+
// Annotate CommonJS exports for Node
266+
0 && (module.exports = {
267+
cjsJs,
268+
});
269+
`,
260270
},
261271
'pkg-module': {
262272
'package.json': createPackageJson({

‎tests/specs/smoke.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,14 @@ export default testSuite(async ({ describe }, { tsx, supports }: NodeApis) => {
3333
import 'node:fs';
3434
3535
import * as pkgCommonjs from 'pkg-commonjs';
36+
37+
// Named exports from CommonJS
38+
import { cjsJs } from 'pkg-commonjs/cjs.js';
39+
3640
import * as pkgModule from 'pkg-module';
3741
import 'pkg-module/empty-export'; // implicit directory & extension
3842
39-
// .js
43+
// .js in esm syntax
4044
import * as js from './js/index.js';
4145
import './js/index.js?query=123';
4246
import './js/index';
@@ -190,7 +194,10 @@ export default testSuite(async ({ describe }, { tsx, supports }: NodeApis) => {
190194
import 'pkg-commonjs/ts.js';
191195
import 'pkg-module/ts.js';
192196
193-
// .js
197+
// Named exports from CommonJS
198+
import { cjsJs } from 'pkg-commonjs/cjs.js';
199+
200+
// .js in esm syntax
194201
import * as js from './js/index.js';
195202
import './js/index.js?query=123';
196203
import './js/index';
@@ -354,6 +361,7 @@ export default testSuite(async ({ describe }, { tsx, supports }: NodeApis) => {
354361
NODE_V8_COVERAGE: 'coverage',
355362
},
356363
});
364+
357365
onTestFail((error) => {
358366
console.error(error);
359367
console.log(p);

0 commit comments

Comments
 (0)
Please sign in to comment.