Skip to content

Commit 3fee5b2

Browse files
ExE-Bossdanielleadams
authored andcommitted
repl: add auto‑completion for dynamic import calls
Refs: #33238 Refs: #33282 Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com> PR-URL: #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 c302450 commit 3fee5b2

File tree

4 files changed

+247
-4
lines changed

4 files changed

+247
-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

+78-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,
@@ -126,6 +127,8 @@ let _builtinLibs = ArrayPrototypeFilter(
126127
CJSModule.builtinModules,
127128
(e) => !StringPrototypeStartsWith(e, '_') && !StringPrototypeIncludes(e, '/')
128129
);
130+
const nodeSchemeBuiltinLibs = ArrayPrototypeMap(
131+
_builtinLibs, (lib) => `node:${lib}`);
129132
const domain = require('domain');
130133
let debug = require('internal/util/debuglog').debuglog('repl', (fn) => {
131134
debug = fn;
@@ -171,6 +174,10 @@ const {
171174
} = internalBinding('contextify');
172175

173176
const history = require('internal/repl/history');
177+
const {
178+
extensionFormatMap,
179+
legacyExtensionFormatMap,
180+
} = require('internal/modules/esm/get_format');
174181

175182
let nextREPLResourceNumber = 1;
176183
// This prevents v8 code cache from getting confused and using a different
@@ -1105,10 +1112,12 @@ REPLServer.prototype.setPrompt = function setPrompt(prompt) {
11051112
ReflectApply(Interface.prototype.setPrompt, this, [prompt]);
11061113
};
11071114

1115+
const importRE = /\bimport\s*\(\s*['"`](([\w@./:-]+\/)?(?:[\w@./:-]*))(?![^'"`])$/;
11081116
const requireRE = /\brequire\s*\(\s*['"`](([\w@./-]+\/)?(?:[\w@./-]*))(?![^'"`])$/;
11091117
const fsAutoCompleteRE = /fs(?:\.promises)?\.\s*[a-z][a-zA-Z]+\(\s*["'](.*)/;
11101118
const simpleExpressionRE =
11111119
/(?:[a-zA-Z_$](?:\w|\$)*\??\.)*[a-zA-Z_$](?:\w|\$)*\??\.?$/;
1120+
const versionedFileNamesRe = /-\d+\.\d+/;
11121121

11131122
function isIdentifier(str) {
11141123
if (str === '') {
@@ -1215,7 +1224,6 @@ function complete(line, callback) {
12151224
const indexes = ArrayPrototypeMap(extensions,
12161225
(extension) => `index${extension}`);
12171226
ArrayPrototypePush(indexes, 'package.json', 'index');
1218-
const versionedFileNamesRe = /-\d+\.\d+/;
12191227

12201228
const match = StringPrototypeMatch(line, requireRE);
12211229
completeOn = match[1];
@@ -1269,6 +1277,75 @@ function complete(line, callback) {
12691277
if (!subdir) {
12701278
ArrayPrototypePush(completionGroups, _builtinLibs);
12711279
}
1280+
} else if (RegExpPrototypeTest(importRE, line) &&
1281+
this.allowBlockingCompletions) {
1282+
// import('...<Tab>')
1283+
// File extensions that can be imported:
1284+
const extensions = ObjectKeys(
1285+
getOptionValue('--experimental-specifier-resolution') === 'node' ?
1286+
legacyExtensionFormatMap :
1287+
extensionFormatMap);
1288+
1289+
// Only used when loading bare module specifiers from `node_modules`:
1290+
const indexes = ArrayPrototypeMap(extensions, (ext) => `index${ext}`);
1291+
ArrayPrototypePush(indexes, 'package.json');
1292+
1293+
const match = StringPrototypeMatch(line, importRE);
1294+
completeOn = match[1];
1295+
const subdir = match[2] || '';
1296+
filter = completeOn;
1297+
group = [];
1298+
let paths = [];
1299+
if (completeOn === '.') {
1300+
group = ['./', '../'];
1301+
} else if (completeOn === '..') {
1302+
group = ['../'];
1303+
} else if (RegExpPrototypeTest(/^\.\.?\//, completeOn)) {
1304+
paths = [process.cwd()];
1305+
} else {
1306+
paths = ArrayPrototypeSlice(module.paths);
1307+
}
1308+
1309+
ArrayPrototypeForEach(paths, (dir) => {
1310+
dir = path.resolve(dir, subdir);
1311+
const isInNodeModules = path.basename(dir) === 'node_modules';
1312+
const dirents = gracefulReaddir(dir, { withFileTypes: true }) || [];
1313+
ArrayPrototypeForEach(dirents, (dirent) => {
1314+
const { name } = dirent;
1315+
if (RegExpPrototypeTest(versionedFileNamesRe, name) ||
1316+
name === '.npm') {
1317+
// Exclude versioned names that 'npm' installs.
1318+
return;
1319+
}
1320+
1321+
if (!dirent.isDirectory()) {
1322+
const extension = path.extname(name);
1323+
if (StringPrototypeIncludes(extensions, extension)) {
1324+
ArrayPrototypePush(group, `${subdir}${name}`);
1325+
}
1326+
return;
1327+
}
1328+
1329+
ArrayPrototypePush(group, `${subdir}${name}/`);
1330+
if (!subdir && isInNodeModules) {
1331+
const absolute = path.resolve(dir, name);
1332+
const subfiles = gracefulReaddir(absolute) || [];
1333+
if (ArrayPrototypeSome(subfiles, (subfile) => {
1334+
return ArrayPrototypeIncludes(indexes, subfile);
1335+
})) {
1336+
ArrayPrototypePush(group, `${subdir}${name}`);
1337+
}
1338+
}
1339+
});
1340+
});
1341+
1342+
if (group.length) {
1343+
ArrayPrototypePush(completionGroups, group);
1344+
}
1345+
1346+
if (!subdir) {
1347+
ArrayPrototypePush(completionGroups, _builtinLibs, nodeSchemeBuiltinLibs);
1348+
}
12721349
} else if (RegExpPrototypeTest(fsAutoCompleteRE, line) &&
12731350
this.allowBlockingCompletions) {
12741351
({ 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)