Skip to content

Commit 14dcf7c

Browse files
ExE-Bosstargos
authored andcommitted
repl: add auto‑completion for dynamic import calls
Refs: nodejs#33238 Refs: nodejs#33282 Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com> PR-URL: nodejs#37178 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Juan José Arboleda <soyjuanarbol@gmail.com> Reviewed-By: Rich Trott <rtrott@gmail.com>
1 parent c7ea266 commit 14dcf7c

File tree

4 files changed

+248
-4
lines changed

4 files changed

+248
-4
lines changed

lib/internal/modules/esm/get_format.js

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

99
const experimentalJsonModules = getOptionValue('--experimental-json-modules');
10-
const experimentalSpeciferResolution =
10+
const experimentalSpecifierResolution =
1111
getOptionValue('--experimental-specifier-resolution');
1212
const experimentalWasmModules = 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

+79-1
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+
const nodeSchemeBuiltinLibs = ArrayPrototypeMap(
132+
_builtinLibs, (lib) => `node:${lib}`);
130133
const domain = require('domain');
131134
let debug = require('internal/util/debuglog').debuglog('repl', (fn) => {
132135
debug = fn;
@@ -168,6 +171,11 @@ const {
168171
} = internalBinding('contextify');
169172

170173
const history = require('internal/repl/history');
174+
const {
175+
extensionFormatMap,
176+
legacyExtensionFormatMap,
177+
} = require('internal/modules/esm/get_format');
178+
171179
let nextREPLResourceNumber = 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+
const importRE = /\bimport\s*\(\s*['"`](([\w@./:-]+\/)?(?:[\w@./:-]*))(?![^'"`])$/;
11381147
const requireRE = /\brequire\s*\(\s*['"`](([\w@./-]+\/)?(?:[\w@./-]*))(?![^'"`])$/;
11391148
const fsAutoCompleteRE = /fs(?:\.promises)?\.\s*[a-z][a-zA-Z]+\(\s*["'](.*)/;
11401149
const simpleExpressionRE =
11411150
/(?:[a-zA-Z_$](?:\w|\$)*\??\.)*[a-zA-Z_$](?:\w|\$)*\??\.?$/;
1151+
const versionedFileNamesRe = /-\d+\.\d+/;
11421152

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

12501259
const match = StringPrototypeMatch(line, requireRE);
12511260
completeOn = match[1];
@@ -1299,6 +1308,75 @@ function complete(line, callback) {
12991308
if (!subdir) {
13001309
ArrayPrototypePush(completionGroups, _builtinLibs);
13011310
}
1311+
} else if (RegExpPrototypeTest(importRE, line) &&
1312+
this.allowBlockingCompletions) {
1313+
// import('...<Tab>')
1314+
// File extensions that can be imported:
1315+
const extensions = ObjectKeys(
1316+
getOptionValue('--experimental-specifier-resolution') === 'node' ?
1317+
legacyExtensionFormatMap :
1318+
extensionFormatMap);
1319+
1320+
// Only used when loading bare module specifiers from `node_modules`:
1321+
const indexes = ArrayPrototypeMap(extensions, (ext) => `index${ext}`);
1322+
ArrayPrototypePush(indexes, 'package.json');
1323+
1324+
const match = StringPrototypeMatch(line, importRE);
1325+
completeOn = match[1];
1326+
const subdir = match[2] || '';
1327+
filter = completeOn;
1328+
group = [];
1329+
let paths = [];
1330+
if (completeOn === '.') {
1331+
group = ['./', '../'];
1332+
} else if (completeOn === '..') {
1333+
group = ['../'];
1334+
} else if (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+
const isInNodeModules = path.basename(dir) === 'node_modules';
1343+
const dirents = 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+
const extension = 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+
const absolute = path.resolve(dir, name);
1363+
const subfiles = gracefulReaddir(absolute) || [];
1364+
if (ArrayPrototypeSome(subfiles, (subfile) => {
1365+
return ArrayPrototypeIncludes(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
} else if (RegExpPrototypeTest(fsAutoCompleteRE, line) &&
13031381
this.allowBlockingCompletions) {
13041382
({ 0: completionGroups, 1: completeOn } = completeFSFunctions(line));

test/parallel/test-repl-autocomplete.js

+3
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ const tests = [
103103
yield 'require("./';
104104
yield TABULATION;
105105
yield SIGINT;
106+
yield 'import("./';
107+
yield TABULATION;
108+
yield SIGINT;
106109
yield 'Array.proto';
107110
yield RIGHT;
108111
yield '.pu';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const ArrayStream = require('../common/arraystream');
5+
const fixtures = require('../common/fixtures');
6+
const assert = require('assert');
7+
const { builtinModules } = require('module');
8+
const publicModules = 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+
const repl = require('repl');
21+
22+
const putIn = new ArrayStream();
23+
const testMe = 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+
const newModule = '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+
const completions = 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+
const expected = ['@nodejsscope', '@nodejsscope/'];
72+
// Import calls should handle all types of quotation marks.
73+
for (const quotationMark of ["'", '"', '`']) {
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+
const input = `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+
const cwd = 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+
const path = '../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)