Skip to content

Commit 88b02cb

Browse files
ExE-Bosstargos
authored andcommitted
repl: add auto‑completion for dynamic import calls
Refs: #33238 Refs: #33282 Co-authored-by: Antoine du Hamel <[email protected]> PR-URL: #37178 Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: Juan José Arboleda <[email protected]> Reviewed-By: Rich Trott <[email protected]>
1 parent c13eadc commit 88b02cb

File tree

4 files changed

+248
-4
lines changed

4 files changed

+248
-4
lines changed

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const{extname } = require('path');
77
const{ getOptionValue }=require('internal/options');
88

99
constexperimentalJsonModules=getOptionValue('--experimental-json-modules');
10-
constexperimentalSpeciferResolution=
10+
constexperimentalSpecifierResolution=
1111
getOptionValue('--experimental-specifier-resolution');
1212
constexperimentalWasmModules=getOptionValue('--experimental-wasm-modules');
1313
const{ getPackageType }=require('internal/modules/esm/resolve');
@@ -62,7 +62,7 @@ function defaultGetFormat(url, context, defaultGetFormatUnused){
6262
format=extensionFormatMap[ext];
6363
}
6464
if(!format){
65-
if(experimentalSpeciferResolution==='node'){
65+
if(experimentalSpecifierResolution==='node'){
6666
process.emitWarning(
6767
'The Node.js specifier resolution in ESM is experimental.',
6868
'ExperimentalWarning');
@@ -75,4 +75,9 @@ function defaultGetFormat(url, context, defaultGetFormatUnused){
7575
}
7676
return{format: null};
7777
}
78-
exports.defaultGetFormat=defaultGetFormat;
78+
79+
module.exports={
80+
defaultGetFormat,
81+
extensionFormatMap,
82+
legacyExtensionFormatMap,
83+
};

‎lib/repl.js‎

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const{
5454
ArrayPrototypePush,
5555
ArrayPrototypeReverse,
5656
ArrayPrototypeShift,
57+
ArrayPrototypeSlice,
5758
ArrayPrototypeSome,
5859
ArrayPrototypeSort,
5960
ArrayPrototypeSplice,
@@ -127,6 +128,8 @@ let _builtinLibs = ArrayPrototypeFilter(
127128
CJSModule.builtinModules,
128129
(e)=>!StringPrototypeStartsWith(e,'_')&&!StringPrototypeIncludes(e,'/')
129130
);
131+
constnodeSchemeBuiltinLibs=ArrayPrototypeMap(
132+
_builtinLibs,(lib)=>`node:${lib}`);
130133
constdomain=require('domain');
131134
letdebug=require('internal/util/debuglog').debuglog('repl',(fn)=>{
132135
debug=fn;
@@ -168,6 +171,11 @@ const{
168171
}=internalBinding('contextify');
169172

170173
consthistory=require('internal/repl/history');
174+
const{
175+
extensionFormatMap,
176+
legacyExtensionFormatMap,
177+
}=require('internal/modules/esm/get_format');
178+
171179
letnextREPLResourceNumber=1;
172180
// This prevents v8 code cache from getting confused and using a different
173181
// cache from a resource of the same name
@@ -1135,10 +1143,12 @@ REPLServer.prototype.turnOffEditorMode = deprecate(
11351143
'REPLServer.turnOffEditorMode() is deprecated',
11361144
'DEP0078');
11371145

1146+
constimportRE=/\bimport\s*\(\s*['"`](([\w@./:-]+\/)?(?:[\w@./:-]*))(?![^'"`])$/;
11381147
constrequireRE=/\brequire\s*\(\s*['"`](([\w@./-]+\/)?(?:[\w@./-]*))(?![^'"`])$/;
11391148
constfsAutoCompleteRE=/fs(?:\.promises)?\.\s*[a-z][a-zA-Z]+\(\s*["'](.*)/;
11401149
constsimpleExpressionRE=
11411150
/(?:[a-zA-Z_$](?:\w|\$)*\??\.)*[a-zA-Z_$](?:\w|\$)*\??\.?$/;
1151+
constversionedFileNamesRe=/-\d+\.\d+/;
11421152

11431153
functionisIdentifier(str){
11441154
if(str===''){
@@ -1245,7 +1255,6 @@ function complete(line, callback){
12451255
constindexes=ArrayPrototypeMap(extensions,
12461256
(extension)=>`index${extension}`);
12471257
ArrayPrototypePush(indexes,'package.json','index');
1248-
constversionedFileNamesRe=/-\d+\.\d+/;
12491258

12501259
constmatch=StringPrototypeMatch(line,requireRE);
12511260
completeOn=match[1];
@@ -1299,6 +1308,75 @@ function complete(line, callback){
12991308
if(!subdir){
13001309
ArrayPrototypePush(completionGroups,_builtinLibs);
13011310
}
1311+
}elseif(RegExpPrototypeTest(importRE,line)&&
1312+
this.allowBlockingCompletions){
1313+
// import('...<Tab>')
1314+
// File extensions that can be imported:
1315+
constextensions=ObjectKeys(
1316+
getOptionValue('--experimental-specifier-resolution')==='node' ?
1317+
legacyExtensionFormatMap :
1318+
extensionFormatMap);
1319+
1320+
// Only used when loading bare module specifiers from `node_modules`:
1321+
constindexes=ArrayPrototypeMap(extensions,(ext)=>`index${ext}`);
1322+
ArrayPrototypePush(indexes,'package.json');
1323+
1324+
constmatch=StringPrototypeMatch(line,importRE);
1325+
completeOn=match[1];
1326+
constsubdir=match[2]||'';
1327+
filter=completeOn;
1328+
group=[];
1329+
letpaths=[];
1330+
if(completeOn==='.'){
1331+
group=['./','../'];
1332+
}elseif(completeOn==='..'){
1333+
group=['../'];
1334+
}elseif(RegExpPrototypeTest(/^\.\.?\//,completeOn)){
1335+
paths=[process.cwd()];
1336+
}else{
1337+
paths=ArrayPrototypeSlice(module.paths);
1338+
}
1339+
1340+
ArrayPrototypeForEach(paths,(dir)=>{
1341+
dir=path.resolve(dir,subdir);
1342+
constisInNodeModules=path.basename(dir)==='node_modules';
1343+
constdirents=gracefulReaddir(dir,{withFileTypes: true})||[];
1344+
ArrayPrototypeForEach(dirents,(dirent)=>{
1345+
const{ name }=dirent;
1346+
if(RegExpPrototypeTest(versionedFileNamesRe,name)||
1347+
name==='.npm'){
1348+
// Exclude versioned names that 'npm' installs.
1349+
return;
1350+
}
1351+
1352+
if(!dirent.isDirectory()){
1353+
constextension=path.extname(name);
1354+
if(StringPrototypeIncludes(extensions,extension)){
1355+
ArrayPrototypePush(group,`${subdir}${name}`);
1356+
}
1357+
return;
1358+
}
1359+
1360+
ArrayPrototypePush(group,`${subdir}${name}/`);
1361+
if(!subdir&&isInNodeModules){
1362+
constabsolute=path.resolve(dir,name);
1363+
constsubfiles=gracefulReaddir(absolute)||[];
1364+
if(ArrayPrototypeSome(subfiles,(subfile)=>{
1365+
returnArrayPrototypeIncludes(indexes,subfile);
1366+
})){
1367+
ArrayPrototypePush(group,`${subdir}${name}`);
1368+
}
1369+
}
1370+
});
1371+
});
1372+
1373+
if(group.length){
1374+
ArrayPrototypePush(completionGroups,group);
1375+
}
1376+
1377+
if(!subdir){
1378+
ArrayPrototypePush(completionGroups,_builtinLibs,nodeSchemeBuiltinLibs);
1379+
}
13021380
}elseif(RegExpPrototypeTest(fsAutoCompleteRE,line)&&
13031381
this.allowBlockingCompletions){
13041382
({0: completionGroups,1: completeOn}=completeFSFunctions(line));

‎test/parallel/test-repl-autocomplete.js‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ const tests = [
103103
yield'require("./';
104104
yieldTABULATION;
105105
yieldSIGINT;
106+
yield'import("./';
107+
yieldTABULATION;
108+
yieldSIGINT;
106109
yield'Array.proto';
107110
yieldRIGHT;
108111
yield'.pu';
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
'use strict';
2+
3+
constcommon=require('../common');
4+
constArrayStream=require('../common/arraystream');
5+
constfixtures=require('../common/fixtures');
6+
constassert=require('assert');
7+
const{ builtinModules }=require('module');
8+
constpublicModules=builtinModules.filter(
9+
(lib)=>!lib.startsWith('_')&&!lib.includes('/'),
10+
);
11+
12+
if(!common.isMainThread)
13+
common.skip('process.chdir is not available in Workers');
14+
15+
// We have to change the directory to ../fixtures before requiring repl
16+
// in order to make the tests for completion of node_modules work properly
17+
// since repl modifies module.paths.
18+
process.chdir(fixtures.fixturesDir);
19+
20+
constrepl=require('repl');
21+
22+
constputIn=newArrayStream();
23+
consttestMe=repl.start({
24+
prompt: '',
25+
input: putIn,
26+
output: process.stdout,
27+
allowBlockingCompletions: true
28+
});
29+
30+
// Some errors are passed to the domain, but do not callback
31+
testMe._domain.on('error',assert.ifError);
32+
33+
// Tab complete provides built in libs for import()
34+
testMe.complete('import(\'',common.mustCall((error,data)=>{
35+
assert.strictEqual(error,null);
36+
publicModules.forEach((lib)=>{
37+
assert(
38+
data[0].includes(lib)&&data[0].includes(`node:${lib}`),
39+
`${lib} not found`,
40+
);
41+
});
42+
constnewModule='foobar';
43+
assert(!builtinModules.includes(newModule));
44+
repl.builtinModules.push(newModule);
45+
testMe.complete('import(\'',common.mustCall((_,[modules])=>{
46+
assert.strictEqual(data[0].length+1,modules.length);
47+
assert(modules.includes(newModule)&&
48+
!modules.includes(`node:${newModule}`));
49+
}));
50+
}));
51+
52+
testMe.complete("import\t( 'n",common.mustCall((error,data)=>{
53+
assert.strictEqual(error,null);
54+
assert.strictEqual(data.length,2);
55+
assert.strictEqual(data[1],'n');
56+
constcompletions=data[0];
57+
// import(...) completions include `node:` URL modules:
58+
publicModules.forEach((lib,index)=>
59+
assert.strictEqual(completions[index],`node:${lib}`));
60+
assert.strictEqual(completions[publicModules.length],'');
61+
// There is only one Node.js module that starts with n:
62+
assert.strictEqual(completions[publicModules.length+1],'net');
63+
assert.strictEqual(completions[publicModules.length+2],'');
64+
// It's possible to pick up non-core modules too
65+
completions.slice(publicModules.length+3).forEach((completion)=>{
66+
assert.match(completion,/^n/);
67+
});
68+
}));
69+
70+
{
71+
constexpected=['@nodejsscope','@nodejsscope/'];
72+
// Import calls should handle all types of quotation marks.
73+
for(constquotationMarkof["'",'"','`']){
74+
putIn.run(['.clear']);
75+
testMe.complete('import(`@nodejs',common.mustCall((err,data)=>{
76+
assert.strictEqual(err,null);
77+
assert.deepStrictEqual(data,[expected,'@nodejs']);
78+
}));
79+
80+
putIn.run(['.clear']);
81+
// Completions should not be greedy in case the quotation ends.
82+
constinput=`import(${quotationMark}@nodejsscope${quotationMark}`;
83+
testMe.complete(input,common.mustCall((err,data)=>{
84+
assert.strictEqual(err,null);
85+
assert.deepStrictEqual(data,[[],undefined]);
86+
}));
87+
}
88+
}
89+
90+
{
91+
putIn.run(['.clear']);
92+
// Completions should find modules and handle whitespace after the opening
93+
// bracket.
94+
testMe.complete('import \t("no_ind',common.mustCall((err,data)=>{
95+
assert.strictEqual(err,null);
96+
assert.deepStrictEqual(data,[['no_index','no_index/'],'no_ind']);
97+
}));
98+
}
99+
100+
// Test tab completion for import() relative to the current directory
101+
{
102+
putIn.run(['.clear']);
103+
104+
constcwd=process.cwd();
105+
process.chdir(__dirname);
106+
107+
['import(\'.','import(".'].forEach((input)=>{
108+
testMe.complete(input,common.mustCall((err,data)=>{
109+
assert.strictEqual(err,null);
110+
assert.strictEqual(data.length,2);
111+
assert.strictEqual(data[1],'.');
112+
assert.strictEqual(data[0].length,2);
113+
assert.ok(data[0].includes('./'));
114+
assert.ok(data[0].includes('../'));
115+
}));
116+
});
117+
118+
['import(\'..','import("..'].forEach((input)=>{
119+
testMe.complete(input,common.mustCall((err,data)=>{
120+
assert.strictEqual(err,null);
121+
assert.deepStrictEqual(data,[['../'],'..']);
122+
}));
123+
});
124+
125+
['./','./test-'].forEach((path)=>{
126+
[`import('${path}`,`import("${path}`].forEach((input)=>{
127+
testMe.complete(input,common.mustCall((err,data)=>{
128+
assert.strictEqual(err,null);
129+
assert.strictEqual(data.length,2);
130+
assert.strictEqual(data[1],path);
131+
assert.ok(data[0].includes('./test-repl-tab-complete.js'));
132+
}));
133+
});
134+
});
135+
136+
['../parallel/','../parallel/test-'].forEach((path)=>{
137+
[`import('${path}`,`import("${path}`].forEach((input)=>{
138+
testMe.complete(input,common.mustCall((err,data)=>{
139+
assert.strictEqual(err,null);
140+
assert.strictEqual(data.length,2);
141+
assert.strictEqual(data[1],path);
142+
assert.ok(data[0].includes('../parallel/test-repl-tab-complete.js'));
143+
}));
144+
});
145+
});
146+
147+
{
148+
constpath='../fixtures/repl-folder-extensions/f';
149+
testMe.complete(`import('${path}`,common.mustSucceed((data)=>{
150+
assert.strictEqual(data.length,2);
151+
assert.strictEqual(data[1],path);
152+
assert.ok(data[0].includes(
153+
'../fixtures/repl-folder-extensions/foo.js/'));
154+
}));
155+
}
156+
157+
process.chdir(cwd);
158+
}

0 commit comments

Comments
(0)