Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 3fee5b2

Browse filesBrowse 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
Copy full SHA for 3fee5b2

File tree

Expand file treeCollapse file tree

4 files changed

+247
-4
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

4 files changed

+247
-4
lines changed
Open diff view settings
Collapse file

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

Copy file name to clipboardExpand all lines: lib/internal/modules/esm/get_format.js
+8-3Lines 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
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+
};
Collapse file

‎lib/repl.js‎

Copy file name to clipboardExpand all lines: lib/repl.js
+78-1Lines changed: 78 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,
@@ -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));
Collapse file

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

Copy file name to clipboardExpand all lines: test/parallel/test-repl-autocomplete.js
+3Lines changed: 3 additions & 0 deletions
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';
Collapse file
+158Lines changed: 158 additions & 0 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.