mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
repl: add auto‑completion for dynamic import calls
Refs: https://github.com/nodejs/node/issues/33238 Refs: https://github.com/nodejs/node/pull/33282 Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com> PR-URL: https://github.com/nodejs/node/pull/37178 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Juan José Arboleda <soyjuanarbol@gmail.com> Reviewed-By: Rich Trott <rtrott@gmail.com>
This commit is contained in:
1 parent
6b8b165f90
commit
3aba6fef07
4 files changed
+247
-4
No files matched your search
@@ -7,7 +7,7 @@ const { extname } = require('path');
|
||||
const { getOptionValue } = require('internal/options');
|
||||
|
||||
const experimentalJsonModules = getOptionValue('--experimental-json-modules');
|
||||
const experimentalSpeciferResolution =
|
||||
const experimentalSpecifierResolution =
|
||||
getOptionValue('--experimental-specifier-resolution');
|
||||
const experimentalWasmModules = getOptionValue('--experimental-wasm-modules');
|
||||
const { getPackageType } = require('internal/modules/esm/resolve');
|
||||
@@ -62,7 +62,7 @@ function defaultGetFormat(url, context, defaultGetFormatUnused) {
|
||||
format = extensionFormatMap[ext];
|
||||
}
|
||||
if (!format) {
|
||||
if (experimentalSpeciferResolution === 'node') {
|
||||
if (experimentalSpecifierResolution === 'node') {
|
||||
process.emitWarning(
|
||||
'The Node.js specifier resolution in ESM is experimental.',
|
||||
'ExperimentalWarning');
|
||||
@@ -75,4 +75,9 @@ function defaultGetFormat(url, context, defaultGetFormatUnused) {
|
||||
}
|
||||
return { format: null };
|
||||
}
|
||||
exports.defaultGetFormat = defaultGetFormat;
|
||||
|
||||
module.exports = {
|
||||
defaultGetFormat,
|
||||
extensionFormatMap,
|
||||
legacyExtensionFormatMap,
|
||||
};
|
||||
+78
-1
@@ -54,6 +54,7 @@ const {
|
||||
ArrayPrototypePush,
|
||||
ArrayPrototypeReverse,
|
||||
ArrayPrototypeShift,
|
||||
ArrayPrototypeSlice,
|
||||
ArrayPrototypeSome,
|
||||
ArrayPrototypeSort,
|
||||
ArrayPrototypeSplice,
|
||||
@@ -126,6 +127,8 @@ let _builtinLibs = ArrayPrototypeFilter(
|
||||
CJSModule.builtinModules,
|
||||
(e) => !StringPrototypeStartsWith(e, '_') && !StringPrototypeIncludes(e, '/')
|
||||
);
|
||||
const nodeSchemeBuiltinLibs = ArrayPrototypeMap(
|
||||
_builtinLibs, (lib) => `node:${lib}`);
|
||||
const domain = require('domain');
|
||||
let debug = require('internal/util/debuglog').debuglog('repl', (fn) => {
|
||||
debug = fn;
|
||||
@@ -171,6 +174,10 @@ const {
|
||||
} = internalBinding('contextify');
|
||||
|
||||
const history = require('internal/repl/history');
|
||||
const {
|
||||
extensionFormatMap,
|
||||
legacyExtensionFormatMap,
|
||||
} = require('internal/modules/esm/get_format');
|
||||
|
||||
let nextREPLResourceNumber = 1;
|
||||
// This prevents v8 code cache from getting confused and using a different
|
||||
@@ -1105,10 +1112,12 @@ REPLServer.prototype.setPrompt = function setPrompt(prompt) {
|
||||
ReflectApply(Interface.prototype.setPrompt, this, [prompt]);
|
||||
};
|
||||
|
||||
const importRE = /\bimport\s*\(\s*['"`](([\w@./:-]+\/)?(?:[\w@./:-]*))(?![^'"`])$/;
|
||||
const requireRE = /\brequire\s*\(\s*['"`](([\w@./-]+\/)?(?:[\w@./-]*))(?![^'"`])$/;
|
||||
const fsAutoCompleteRE = /fs(?:\.promises)?\.\s*[a-z][a-zA-Z]+\(\s*["'](.*)/;
|
||||
const simpleExpressionRE =
|
||||
/(?:[a-zA-Z_$](?:\w|\$)*\??\.)*[a-zA-Z_$](?:\w|\$)*\??\.?$/;
|
||||
const versionedFileNamesRe = /-\d+\.\d+/;
|
||||
|
||||
function isIdentifier(str) {
|
||||
if (str === '') {
|
||||
@@ -1215,7 +1224,6 @@ function complete(line, callback) {
|
||||
const indexes = ArrayPrototypeMap(extensions,
|
||||
(extension) => `index${extension}`);
|
||||
ArrayPrototypePush(indexes, 'package.json', 'index');
|
||||
const versionedFileNamesRe = /-\d+\.\d+/;
|
||||
|
||||
const match = StringPrototypeMatch(line, requireRE);
|
||||
completeOn = match[1];
|
||||
@@ -1269,6 +1277,75 @@ function complete(line, callback) {
|
||||
if (!subdir) {
|
||||
ArrayPrototypePush(completionGroups, _builtinLibs);
|
||||
}
|
||||
} else if (RegExpPrototypeTest(importRE, line) &&
|
||||
this.allowBlockingCompletions) {
|
||||
// import('...<Tab>')
|
||||
// File extensions that can be imported:
|
||||
const extensions = ObjectKeys(
|
||||
getOptionValue('--experimental-specifier-resolution') === 'node' ?
|
||||
legacyExtensionFormatMap :
|
||||
extensionFormatMap);
|
||||
|
||||
// Only used when loading bare module specifiers from `node_modules`:
|
||||
const indexes = ArrayPrototypeMap(extensions, (ext) => `index${ext}`);
|
||||
ArrayPrototypePush(indexes, 'package.json');
|
||||
|
||||
const match = StringPrototypeMatch(line, importRE);
|
||||
completeOn = match[1];
|
||||
const subdir = match[2] || '';
|
||||
filter = completeOn;
|
||||
group = [];
|
||||
let paths = [];
|
||||
if (completeOn === '.') {
|
||||
group = ['./', '../'];
|
||||
} else if (completeOn === '..') {
|
||||
group = ['../'];
|
||||
} else if (RegExpPrototypeTest(/^\.\.?\//, completeOn)) {
|
||||
paths = [process.cwd()];
|
||||
} else {
|
||||
paths = ArrayPrototypeSlice(module.paths);
|
||||
}
|
||||
|
||||
ArrayPrototypeForEach(paths, (dir) => {
|
||||
dir = path.resolve(dir, subdir);
|
||||
const isInNodeModules = path.basename(dir) === 'node_modules';
|
||||
const dirents = gracefulReaddir(dir, { withFileTypes: true }) || [];
|
||||
ArrayPrototypeForEach(dirents, (dirent) => {
|
||||
const { name } = dirent;
|
||||
if (RegExpPrototypeTest(versionedFileNamesRe, name) ||
|
||||
name === '.npm') {
|
||||
// Exclude versioned names that 'npm' installs.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dirent.isDirectory()) {
|
||||
const extension = path.extname(name);
|
||||
if (StringPrototypeIncludes(extensions, extension)) {
|
||||
ArrayPrototypePush(group, `${subdir}${name}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ArrayPrototypePush(group, `${subdir}${name}/`);
|
||||
if (!subdir && isInNodeModules) {
|
||||
const absolute = path.resolve(dir, name);
|
||||
const subfiles = gracefulReaddir(absolute) || [];
|
||||
if (ArrayPrototypeSome(subfiles, (subfile) => {
|
||||
return ArrayPrototypeIncludes(indexes, subfile);
|
||||
})) {
|
||||
ArrayPrototypePush(group, `${subdir}${name}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (group.length) {
|
||||
ArrayPrototypePush(completionGroups, group);
|
||||
}
|
||||
|
||||
if (!subdir) {
|
||||
ArrayPrototypePush(completionGroups, _builtinLibs, nodeSchemeBuiltinLibs);
|
||||
}
|
||||
} else if (RegExpPrototypeTest(fsAutoCompleteRE, line) &&
|
||||
this.allowBlockingCompletions) {
|
||||
({ 0: completionGroups, 1: completeOn } = completeFSFunctions(line));
|
||||
|
||||
@@ -103,6 +103,9 @@ const tests = [
|
||||
yield 'require("./';
|
||||
yield TABULATION;
|
||||
yield SIGINT;
|
||||
yield 'import("./';
|
||||
yield TABULATION;
|
||||
yield SIGINT;
|
||||
yield 'Array.proto';
|
||||
yield RIGHT;
|
||||
yield '.pu';
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const ArrayStream = require('../common/arraystream');
|
||||
const fixtures = require('../common/fixtures');
|
||||
const assert = require('assert');
|
||||
const { builtinModules } = require('module');
|
||||
const publicModules = builtinModules.filter(
|
||||
(lib) => !lib.startsWith('_') && !lib.includes('/'),
|
||||
);
|
||||
|
||||
if (!common.isMainThread)
|
||||
common.skip('process.chdir is not available in Workers');
|
||||
|
||||
// We have to change the directory to ../fixtures before requiring repl
|
||||
// in order to make the tests for completion of node_modules work properly
|
||||
// since repl modifies module.paths.
|
||||
process.chdir(fixtures.fixturesDir);
|
||||
|
||||
const repl = require('repl');
|
||||
|
||||
const putIn = new ArrayStream();
|
||||
const testMe = repl.start({
|
||||
prompt: '',
|
||||
input: putIn,
|
||||
output: process.stdout,
|
||||
allowBlockingCompletions: true
|
||||
});
|
||||
|
||||
// Some errors are passed to the domain, but do not callback
|
||||
testMe._domain.on('error', assert.ifError);
|
||||
|
||||
// Tab complete provides built in libs for import()
|
||||
testMe.complete('import(\'', common.mustCall((error, data) => {
|
||||
assert.strictEqual(error, null);
|
||||
publicModules.forEach((lib) => {
|
||||
assert(
|
||||
data[0].includes(lib) && data[0].includes(`node:${lib}`),
|
||||
`${lib} not found`,
|
||||
);
|
||||
});
|
||||
const newModule = 'foobar';
|
||||
assert(!builtinModules.includes(newModule));
|
||||
repl.builtinModules.push(newModule);
|
||||
testMe.complete('import(\'', common.mustCall((_, [modules]) => {
|
||||
assert.strictEqual(data[0].length + 1, modules.length);
|
||||
assert(modules.includes(newModule) &&
|
||||
!modules.includes(`node:${newModule}`));
|
||||
}));
|
||||
}));
|
||||
|
||||
testMe.complete("import\t( 'n", common.mustCall((error, data) => {
|
||||
assert.strictEqual(error, null);
|
||||
assert.strictEqual(data.length, 2);
|
||||
assert.strictEqual(data[1], 'n');
|
||||
const completions = data[0];
|
||||
// import(...) completions include `node:` URL modules:
|
||||
publicModules.forEach((lib, index) =>
|
||||
assert.strictEqual(completions[index], `node:${lib}`));
|
||||
assert.strictEqual(completions[publicModules.length], '');
|
||||
// There is only one Node.js module that starts with n:
|
||||
assert.strictEqual(completions[publicModules.length + 1], 'net');
|
||||
assert.strictEqual(completions[publicModules.length + 2], '');
|
||||
// It's possible to pick up non-core modules too
|
||||
completions.slice(publicModules.length + 3).forEach((completion) => {
|
||||
assert.match(completion, /^n/);
|
||||
});
|
||||
}));
|
||||
|
||||
{
|
||||
const expected = ['@nodejsscope', '@nodejsscope/'];
|
||||
// Import calls should handle all types of quotation marks.
|
||||
for (const quotationMark of ["'", '"', '`']) {
|
||||
putIn.run(['.clear']);
|
||||
testMe.complete('import(`@nodejs', common.mustCall((err, data) => {
|
||||
assert.strictEqual(err, null);
|
||||
assert.deepStrictEqual(data, [expected, '@nodejs']);
|
||||
}));
|
||||
|
||||
putIn.run(['.clear']);
|
||||
// Completions should not be greedy in case the quotation ends.
|
||||
const input = `import(${quotationMark}@nodejsscope${quotationMark}`;
|
||||
testMe.complete(input, common.mustCall((err, data) => {
|
||||
assert.strictEqual(err, null);
|
||||
assert.deepStrictEqual(data, [[], undefined]);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
putIn.run(['.clear']);
|
||||
// Completions should find modules and handle whitespace after the opening
|
||||
// bracket.
|
||||
testMe.complete('import \t("no_ind', common.mustCall((err, data) => {
|
||||
assert.strictEqual(err, null);
|
||||
assert.deepStrictEqual(data, [['no_index', 'no_index/'], 'no_ind']);
|
||||
}));
|
||||
}
|
||||
|
||||
// Test tab completion for import() relative to the current directory
|
||||
{
|
||||
putIn.run(['.clear']);
|
||||
|
||||
const cwd = process.cwd();
|
||||
process.chdir(__dirname);
|
||||
|
||||
['import(\'.', 'import(".'].forEach((input) => {
|
||||
testMe.complete(input, common.mustCall((err, data) => {
|
||||
assert.strictEqual(err, null);
|
||||
assert.strictEqual(data.length, 2);
|
||||
assert.strictEqual(data[1], '.');
|
||||
assert.strictEqual(data[0].length, 2);
|
||||
assert.ok(data[0].includes('./'));
|
||||
assert.ok(data[0].includes('../'));
|
||||
}));
|
||||
});
|
||||
|
||||
['import(\'..', 'import("..'].forEach((input) => {
|
||||
testMe.complete(input, common.mustCall((err, data) => {
|
||||
assert.strictEqual(err, null);
|
||||
assert.deepStrictEqual(data, [['../'], '..']);
|
||||
}));
|
||||
});
|
||||
|
||||
['./', './test-'].forEach((path) => {
|
||||
[`import('${path}`, `import("${path}`].forEach((input) => {
|
||||
testMe.complete(input, common.mustCall((err, data) => {
|
||||
assert.strictEqual(err, null);
|
||||
assert.strictEqual(data.length, 2);
|
||||
assert.strictEqual(data[1], path);
|
||||
assert.ok(data[0].includes('./test-repl-tab-complete.js'));
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
['../parallel/', '../parallel/test-'].forEach((path) => {
|
||||
[`import('${path}`, `import("${path}`].forEach((input) => {
|
||||
testMe.complete(input, common.mustCall((err, data) => {
|
||||
assert.strictEqual(err, null);
|
||||
assert.strictEqual(data.length, 2);
|
||||
assert.strictEqual(data[1], path);
|
||||
assert.ok(data[0].includes('../parallel/test-repl-tab-complete.js'));
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
{
|
||||
const path = '../fixtures/repl-folder-extensions/f';
|
||||
testMe.complete(`import('${path}`, common.mustSucceed((data) => {
|
||||
assert.strictEqual(data.length, 2);
|
||||
assert.strictEqual(data[1], path);
|
||||
assert.ok(data[0].includes(
|
||||
'../fixtures/repl-folder-extensions/foo.js/'));
|
||||
}));
|
||||
}
|
||||
|
||||
process.chdir(cwd);
|
||||
}
|
||||
Reference in new issue
Block a user