Skip to content

Commit fe69198

Browse files
GeoffreyBoothtargos
authored andcommitted
esm: unflag extensionless javascript and wasm in module scope
PR-URL: #49974 Backport-PR-URL: #50669 Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Guy Bedford <[email protected]> Reviewed-By: Michaël Zasso <[email protected]>
1 parent 56bd9a8 commit fe69198

File tree

14 files changed

+174
-47
lines changed

14 files changed

+174
-47
lines changed

‎doc/api/esm.md‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,8 +1008,12 @@ _isImports_, _conditions_)
10081008
> 5. Let _packageURL_ be the result of **LOOKUP\_PACKAGE\_SCOPE**(_url_).
10091009
> 6. Let _pjson_ be the result of **READ\_PACKAGE\_JSON**(_packageURL_).
10101010
> 7. If _pjson?.type_ exists and is _"module"_, then
1011-
> 1. If _url_ ends in _".js"_, then
1012-
> 1. Return _"module"_.
1011+
> 1. If _url_ ends in _".js"_ or has no file extension, then
1012+
> 1. If `--experimental-wasm-modules` is enabled and the file at _url_
1013+
> contains the header for a WebAssembly module, then
1014+
> 1. Return _"wasm"_.
1015+
> 2. Otherwise,
1016+
> 1. Return _"module"_.
10131017
> 2. Return **undefined**.
10141018
> 8. Otherwise,
10151019
> 1. Return **undefined**.

‎lib/internal/errors.js‎

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1686,13 +1686,7 @@ E('ERR_UNHANDLED_ERROR',
16861686
E('ERR_UNKNOWN_BUILTIN_MODULE','No such built-in module: %s',Error);
16871687
E('ERR_UNKNOWN_CREDENTIAL','%s identifier does not exist: %s',Error);
16881688
E('ERR_UNKNOWN_ENCODING','Unknown encoding: %s',TypeError);
1689-
E('ERR_UNKNOWN_FILE_EXTENSION',(ext,path,suggestion)=>{
1690-
letmsg=`Unknown file extension "${ext}" for ${path}`;
1691-
if(suggestion){
1692-
msg+=`. ${suggestion}`;
1693-
}
1694-
returnmsg;
1695-
},TypeError);
1689+
E('ERR_UNKNOWN_FILE_EXTENSION','Unknown file extension "%s" for %s',TypeError);
16961690
E('ERR_UNKNOWN_MODULE_FORMAT','Unknown module format: %s for URL %s',
16971691
RangeError);
16981692
E('ERR_UNKNOWN_SIGNAL','Unknown signal: %s',TypeError);

‎lib/internal/modules/esm/get_format.js‎

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ const{
99
StringPrototypeCharCodeAt,
1010
StringPrototypeSlice,
1111
}=primordials;
12-
const{ basename, relative }=require('path');
1312
const{ getOptionValue }=require('internal/options');
1413
const{
1514
extensionFormatMap,
@@ -25,7 +24,7 @@ const experimentalSpecifierResolution =
2524
constdefaultTypeFlag=getOptionValue('--experimental-default-type');
2625
// The next line is where we flip the default to ES modules someday.
2726
constdefaultType=defaultTypeFlag==='module' ? 'module' : 'commonjs';
28-
const{ getPackageType, getPackageScopeConfig}=require('internal/modules/esm/resolve');
27+
const{ getPackageType }=require('internal/modules/esm/resolve');
2928
const{ fileURLToPath }=require('internal/url');
3029
const{ERR_UNKNOWN_FILE_EXTENSION}=require('internal/errors').codes;
3130

@@ -115,17 +114,16 @@ function getFileProtocolModuleFormat(url, context, ignoreErrors){
115114
if(defaultType==='commonjs'){// Legacy behavior
116115
if(packageType==='none'||packageType==='commonjs'){
117116
return'commonjs';
118-
}
119-
// If package type is `module`, fall through to the error case below
120-
}else{// Else defaultType === 'module'
121-
if(underNodeModules(url)){// Exception for package scopes under `node_modules`
122-
return'commonjs';
123-
}
124-
if(packageType==='none'||packageType==='module'){
125-
returngetFormatOfExtensionlessFile(url);
126-
}// Else packageType === 'commonjs'
127-
return'commonjs';
117+
}// Else packageType === 'module'
118+
returngetFormatOfExtensionlessFile(url);
119+
}// Else defaultType === 'module'
120+
if(underNodeModules(url)){// Exception for package scopes under `node_modules`
121+
returnpackageType==='module' ? getFormatOfExtensionlessFile(url) : 'commonjs';
128122
}
123+
if(packageType==='none'||packageType==='module'){
124+
returngetFormatOfExtensionlessFile(url);
125+
}// Else packageType === 'commonjs'
126+
return'commonjs';
129127
}
130128

131129
constformat=extensionFormatMap[ext];
@@ -135,17 +133,7 @@ function getFileProtocolModuleFormat(url, context, ignoreErrors){
135133
// Explicit undefined return indicates load hook should rerun format check
136134
if(ignoreErrors){returnundefined;}
137135
constfilepath=fileURLToPath(url);
138-
letsuggestion='';
139-
if(getPackageType(url)==='module'&&ext===''){
140-
constconfig=getPackageScopeConfig(url);
141-
constfileBasename=basename(filepath);
142-
constrelativePath=StringPrototypeSlice(relative(config.pjsonPath,filepath),1);
143-
suggestion='Loading extensionless files is not supported inside of "type":"module" package.json contexts '+
144-
`without --experimental-default-type=module. The package.json file ${config.pjsonPath} caused this "type":"module" `+
145-
`context. Try changing ${filepath} to have a file extension. Note the "bin" field of package.json can point `+
146-
`to a file with an extension, for example{"type":"module","bin":{"${fileBasename}":"${relativePath}.js"}}`;
147-
}
148-
thrownewERR_UNKNOWN_FILE_EXTENSION(ext,filepath,suggestion);
136+
thrownewERR_UNKNOWN_FILE_EXTENSION(ext,filepath);
149137
}
150138

151139
returngetLegacyExtensionFormat(ext)??null;
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Flags: --experimental-wasm-modules
2+
import{mustNotCall,spawnPromisified}from'../common/index.mjs';
3+
import*asfixturesfrom'../common/fixtures.mjs';
4+
import{describe,it}from'node:test';
5+
import{match,ok,strictEqual}from'node:assert';
6+
7+
describe('extensionless ES modules within a "type": "module" package scope',{concurrency: true},()=>{
8+
it('should run as the entry point',async()=>{
9+
const{ code, signal, stdout, stderr }=awaitspawnPromisified(process.execPath,[
10+
fixtures.path('es-modules/package-type-module/noext-esm'),
11+
]);
12+
13+
strictEqual(stderr,'');
14+
strictEqual(stdout,'executed\n');
15+
strictEqual(code,0);
16+
strictEqual(signal,null);
17+
});
18+
19+
it('should be importable',async()=>{
20+
const{default: defaultExport}=
21+
awaitimport(fixtures.fileURL('es-modules/package-type-module/noext-esm'));
22+
strictEqual(defaultExport,'module');
23+
});
24+
25+
it('should be importable from a module scope under node_modules',async()=>{
26+
const{default: defaultExport}=
27+
awaitimport(fixtures.fileURL(
28+
'es-modules/package-type-module/node_modules/dep-with-package-json-type-module/noext-esm'));
29+
strictEqual(defaultExport,'module');
30+
});
31+
});
32+
describe('extensionless Wasm modules within a "type": "module" package scope',{concurrency: true},()=>{
33+
it('should run as the entry point',async()=>{
34+
const{ code, signal, stdout, stderr }=awaitspawnPromisified(process.execPath,[
35+
'--experimental-wasm-modules',
36+
'--no-warnings',
37+
fixtures.path('es-modules/package-type-module/noext-wasm'),
38+
]);
39+
40+
strictEqual(stderr,'');
41+
strictEqual(stdout,'executed\n');
42+
strictEqual(code,0);
43+
strictEqual(signal,null);
44+
});
45+
46+
it('should be importable',async()=>{
47+
const{ add }=awaitimport(fixtures.fileURL('es-modules/package-type-module/noext-wasm'));
48+
strictEqual(add(1,2),3);
49+
});
50+
51+
it('should be importable from a module scope under node_modules',async()=>{
52+
const{ add }=awaitimport(fixtures.fileURL(
53+
'es-modules/package-type-module/node_modules/dep-with-package-json-type-module/noext-wasm'));
54+
strictEqual(add(1,2),3);
55+
});
56+
});
57+
58+
describe('extensionless ES modules within no package scope',{concurrency: true},()=>{
59+
// This succeeds with `--experimental-default-type=module`
60+
it('should error as the entry point',async()=>{
61+
const{ code, signal, stdout, stderr }=awaitspawnPromisified(process.execPath,[
62+
fixtures.path('es-modules/noext-esm'),
63+
]);
64+
65+
match(stderr,/SyntaxError/);
66+
strictEqual(stdout,'');
67+
strictEqual(code,1);
68+
strictEqual(signal,null);
69+
});
70+
71+
// This succeeds with `--experimental-default-type=module`
72+
it('should error on import',async()=>{
73+
try{
74+
awaitimport(fixtures.fileURL('es-modules/noext-esm'));
75+
mustNotCall();
76+
}catch(err){
77+
ok(errinstanceofSyntaxError);
78+
}
79+
});
80+
});
81+
82+
describe('extensionless Wasm within no package scope',{concurrency: true},()=>{
83+
// This succeeds with `--experimental-default-type=module`
84+
it('should error as the entry point',async()=>{
85+
const{ code, signal, stdout, stderr }=awaitspawnPromisified(process.execPath,[
86+
'--experimental-wasm-modules',
87+
'--no-warnings',
88+
fixtures.path('es-modules/noext-wasm'),
89+
]);
90+
91+
match(stderr,/SyntaxError/);
92+
strictEqual(stdout,'');
93+
strictEqual(code,1);
94+
strictEqual(signal,null);
95+
});
96+
97+
// This succeeds with `--experimental-default-type=module`
98+
it('should error on import',async()=>{
99+
try{
100+
awaitimport(fixtures.fileURL('es-modules/noext-wasm'));
101+
mustNotCall();
102+
}catch(err){
103+
ok(errinstanceofSyntaxError);
104+
}
105+
});
106+
});

‎test/es-module/test-esm-type-flag-package-scopes.mjs‎

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ describe('the type flag should change the interpretation of certain files within
2424
strictEqual(defaultExport,'module');
2525
});
2626

27+
it('should import an extensionless JavaScript file within a "type": "module" scope under node_modules',
28+
async()=>{
29+
const{default: defaultExport}=
30+
awaitimport(fixtures.fileURL(
31+
'es-modules/package-type-module/node_modules/dep-with-package-json-type-module/noext-esm'));
32+
strictEqual(defaultExport,'module');
33+
});
34+
2735
it('should run as Wasm an extensionless Wasm file within a "type": "module" scope',async()=>{
2836
const{ code, signal, stdout, stderr }=awaitspawnPromisified(process.execPath,[
2937
'--experimental-default-type=module',
@@ -42,6 +50,13 @@ describe('the type flag should change the interpretation of certain files within
4250
const{ add }=awaitimport(fixtures.fileURL('es-modules/package-type-module/noext-wasm'));
4351
strictEqual(add(1,2),3);
4452
});
53+
54+
it('should import an extensionless Wasm file within a "type": "module" scope under node_modules',
55+
async()=>{
56+
const{ add }=awaitimport(fixtures.fileURL(
57+
'es-modules/package-type-module/node_modules/dep-with-package-json-type-module/noext-wasm'));
58+
strictEqual(add(1,2),3);
59+
});
4560
});
4661

4762
describe(`the type flag should change the interpretation of certain files within a package scope that lacks a
@@ -112,7 +127,7 @@ describe(`the type flag should NOT change the interpretation of certain files wi
112127
async()=>{
113128
const{ code, signal, stdout, stderr }=awaitspawnPromisified(process.execPath,[
114129
'--experimental-default-type=module',
115-
fixtures.path('es-modules/package-type-module/node_modules/dep-with-package-json/run.js'),
130+
fixtures.path('es-modules/package-type-module/node_modules/dep-with-package-json-without-type/run.js'),
116131
]);
117132

118133
strictEqual(stderr,'');
@@ -124,15 +139,16 @@ describe(`the type flag should NOT change the interpretation of certain files wi
124139
it(`should import as CommonJS a .js file within a package scope that has no defined "type" and is under
125140
node_modules`,async()=>{
126141
const{default: defaultExport}=
127-
awaitimport(fixtures.fileURL('es-modules/package-type-module/node_modules/dep-with-package-json/run.js'));
142+
awaitimport(fixtures.fileURL(
143+
'es-modules/package-type-module/node_modules/dep-with-package-json-without-type/run.js'));
128144
strictEqual(defaultExport,42);
129145
});
130146

131147
it(`should run as CommonJS an extensionless JavaScript file within a package scope that has no defined "type" and is
132148
under node_modules`,async()=>{
133149
const{ code, signal, stdout, stderr }=awaitspawnPromisified(process.execPath,[
134150
'--experimental-default-type=module',
135-
fixtures.path('es-modules/package-type-module/node_modules/dep-with-package-json/noext-cjs'),
151+
fixtures.path('es-modules/package-type-module/node_modules/dep-with-package-json-without-type/noext-cjs'),
136152
]);
137153

138154
strictEqual(stderr,'');
@@ -144,7 +160,8 @@ describe(`the type flag should NOT change the interpretation of certain files wi
144160
it(`should import as CommonJS an extensionless JavaScript file within a package scope that has no defined "type" and
145161
is under node_modules`,async()=>{
146162
const{default: defaultExport}=
147-
awaitimport(fixtures.fileURL('es-modules/package-type-module/node_modules/dep-with-package-json/noext-cjs'));
163+
awaitimport(fixtures.fileURL(
164+
'es-modules/package-type-module/node_modules/dep-with-package-json-without-type/noext-cjs'));
148165
strictEqual(defaultExport,42);
149166
});
150167
});

test/es-module/test-esm-unknown-or-no-extension.js renamed to test/es-module/test-esm-unknown-extension.js

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,11 @@ const{execPath } = require('node:process');
77
const{ describe, it }=require('node:test');
88

99

10-
// In a "type": "module" package scope, files with unknown extensions or no
11-
// extensions should throw; both when used as a main entry point and also when
12-
// referenced via `import`.
13-
describe('ESM: extensionless and unknown specifiers',{concurrency: true},()=>{
10+
// In a "type": "module" package scope, files with unknown extensions should throw;
11+
// both when used as a main entry point and also when referenced via `import`.
12+
describe('ESM: unknown specifiers',{concurrency: true},()=>{
1413
for(
1514
constfixturePathof[
16-
'/es-modules/package-type-module/noext-esm',
17-
'/es-modules/package-type-module/imports-noext.mjs',
1815
'/es-modules/package-type-module/extension.unknown',
1916
'/es-modules/package-type-module/imports-unknownext.mjs',
2017
]
@@ -27,10 +24,6 @@ describe('ESM: extensionless and unknown specifiers',{concurrency: true }, ()
2724
assert.strictEqual(signal,null);
2825
assert.strictEqual(stdout,'');
2926
assert.match(stderr,/ERR_UNKNOWN_FILE_EXTENSION/);
30-
if(fixturePath.includes('noext')){
31-
// Check for explanation to users
32-
assert.match(stderr,/extensionless/);
33-
}
3427
});
3528
}
3629
});

‎test/fixtures/es-modules/package-type-module/node_modules/dep-with-package-json-type-module/noext-esm‎

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎test/fixtures/es-modules/package-type-module/node_modules/dep-with-package-json-type-module/package.json‎

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎test/fixtures/es-modules/package-type-module/node_modules/dep-with-package-json-type-module/wasm-dep.mjs‎

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
(0)