Skip to content

Commit df86793

Browse files
hybristtargos
authored andcommitted
module: resolve self-references
Adds the ability to `import` or `require` a package from within its own source code. This allows tests and examples to be written using the package name, making them easier to reuse by consumers of the package. Assuming the `name` field in `package.json` is set to `my-pkg`, its test could use `require('my-pkg')` or `import 'my-pkg'` even if there's no `node_modules/my-pkg` while testing the package itself. An important difference between this and relative specifiers like `require('../')` is that self-references use the public interface of the package as defined in the `exports` field while relative specifiers don't. This behavior is guarded by a new experimental flag (`--experimental-resolve-self`). PR-URL: #29327 Reviewed-By: Guy Bedford <guybedford@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Michaël Zasso <targos@protonmail.com>
1 parent e08c008 commit df86793

12 files changed

Lines changed: 294 additions & 44 deletions

File tree

‎doc/api/cli.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,14 @@ added: v11.8.0
196196

197197
Enable experimental diagnostic report feature.
198198

199+
### `--experimental-resolve-self`
200+
<!-- YAML
201+
added: REPLACEME
202+
-->
203+
204+
Enable experimental support for a package using `require` or `import` to load
205+
itself.
206+
199207
### `--experimental-vm-modules`
200208
<!-- YAML
201209
added: v9.6.0
@@ -1053,6 +1061,7 @@ Node.js options that are allowed are:
10531061
*`--experimental-policy`
10541062
*`--experimental-repl-await`
10551063
*`--experimental-report`
1064+
*`--experimental-resolve-self`
10561065
*`--experimental-vm-modules`
10571066
*`--experimental-wasm-modules`
10581067
*`--force-context-aware`

‎doc/api/esm.md‎

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -838,9 +838,6 @@ _isMain_ is **true** when resolving the Node.js application entry point.
838838
> 1. Let _packageSubpath_ be *undefined*.
839839
> 1. If _packageSpecifier_ is an empty string, then
840840
> 1. Throw an _Invalid Specifier_ error.
841-
> 1. If _packageSpecifier_ does not start with _"@"_, then
842-
> 1. Set _packageName_ to the substring of _packageSpecifier_ until the
843-
> first _"/"_ separator or the end of the string.
844841
> 1. Otherwise,
845842
> 1. If _packageSpecifier_ does not contain a _"/"_ separator, then
846843
> 1. Throw an _Invalid Specifier_ error.
@@ -854,7 +851,7 @@ _isMain_ is **true** when resolving the Node.js application entry point.
854851
> 1. Set _packageSubpath_ to _"."_ concatenated with the substring of
855852
> _packageSpecifier_ from the position at the length of _packageName_.
856853
> 1. If _packageSubpath_ contains any _"."_ or _".."_ segments or percent
857-
> encoded strings for _"/"_ or _"\\"_ then,
854+
> encoded strings for _"/"_ or _"\\"_, then
858855
> 1. Throw an _Invalid Specifier_ error.
859856
> 1. If _packageSubpath_ is _undefined_ and _packageName_ is a Node.js builtin
860857
> module, then
@@ -877,8 +874,31 @@ _isMain_ is **true** when resolving the Node.js application entry point.
877874
> 1. Return **PACKAGE_EXPORTS_RESOLVE**(_packageURL_,
878875
> _packageSubpath_, _pjson.exports_).
879876
> 1. Return the URL resolution of _packageSubpath_ in _packageURL_.
877+
> 1. Set _selfUrl_ to the result of
878+
> **SELF_REFERENCE_RESOLE**(_packageSpecifier_, _parentURL_).
879+
> 1. If _selfUrl_ isn't empty, return _selfUrl_.
880880
> 1. Throw a _Module Not Found_ error.
881881
882+
**SELF_REFERENCE_RESOLVE**(_specifier_, _parentURL_)
883+
884+
> 1. Let _packageURL_ be the result of **READ_PACKAGE_SCOPE**(_parentURL_).
885+
> 1. If _packageURL_ is **null**, then
886+
> 1. Return an empty result.
887+
> 1. Let _pjson_ be the result of **READ_PACKAGE_JSON**(_packageURL_).
888+
> 1. Set _name_ to _pjson.name_.
889+
> 1. If _name_ is empty, then return an empty result.
890+
> 1. If _name_ is equal to _specifier_, then
891+
> 1. Return the result of **PACKAGE_MAIN_RESOLVE**(_packageURL_, _pjson_).
892+
> 1. If _specifier_ starts with _name_ followed by "/", then
893+
> 1. Set _subpath_ to everything after the "/".
894+
> 1. If _pjson_ is not **null** and _pjson_ has an _"exports"_ key, then
895+
> 1. Let _exports_ be _pjson.exports_.
896+
> 1. If _exports_ is not **null** or **undefined**, then
897+
> 1. Return **PACKAGE_EXPORTS_RESOLVE**(_packageURL_, _subpath_,
898+
> _pjson.exports_).
899+
> 1. Return the URL resolution of _subpath_ in _packageURL_.
900+
> 1. Otherwise return an empty result.
901+
882902
**PACKAGE_MAIN_RESOLVE**(_packageURL_, _pjson_)
883903
884904
> 1. If _pjson_ is **null**, then

‎doc/api/modules.md‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,9 @@ require(X) from module at path Y
160160
a. LOAD_AS_FILE(Y + X)
161161
b. LOAD_AS_DIRECTORY(Y + X)
162162
c. THROW "not found"
163-
4. LOAD_NODE_MODULES(X, dirname(Y))
164-
5. THROW "not found"
163+
5. LOAD_NODE_MODULES(X, dirname(Y))
164+
4. LOAD_SELF_REFERENCE(X, dirname(Y))
165+
6. THROW "not found"
165166
166167
LOAD_AS_FILE(X)
167168
1. If X is a file, load X as JavaScript text. STOP
@@ -201,6 +202,13 @@ NODE_MODULES_PATHS(START)
201202
c. DIRS = DIRS + DIR
202203
d. let I = I - 1
203204
5. return DIRS
205+
206+
LOAD_SELF_REFERENCE(X, START)
207+
1. Find the closest package scope to START.
208+
2. If no scope was found, throw "not found".
209+
3. If the name in `package.json` isn't a prefix of X, throw "not found".
210+
4. Otherwise, resolve the remainder of X relative to this package as if it
211+
was loaded via `LOAD_NODE_MODULES` with a name in `package.json`.
204212
```
205213

206214
Node.js allows packages loaded via

‎lib/internal/modules/cjs/loader.js‎

Lines changed: 129 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ const enableSourceMaps = getOptionValue('--enable-source-maps');
5959
constpreserveSymlinks=getOptionValue('--preserve-symlinks');
6060
constpreserveSymlinksMain=getOptionValue('--preserve-symlinks-main');
6161
constexperimentalModules=getOptionValue('--experimental-modules');
62+
constexperimentalSelf=getOptionValue('--experimental-resolve-self');
6263
constmanifest=getOptionValue('--experimental-policy') ?
6364
require('internal/process/policy').manifest :
6465
null;
@@ -237,6 +238,7 @@ function readPackage(requestPath) {
237238
try{
238239
constparsed=JSON.parse(json);
239240
constfiltered={
241+
name: parsed.name,
240242
main: parsed.main,
241243
exports: parsed.exports,
242244
type: parsed.type
@@ -366,6 +368,125 @@ function findLongestRegisteredExtension(filename) {
366368
return'.js';
367369
}
368370

371+
functionresolveBasePath(basePath,exts,isMain,trailingSlash,request){
372+
letfilename;
373+
374+
constrc=stat(basePath);
375+
if(!trailingSlash){
376+
if(rc===0){// File.
377+
if(!isMain){
378+
if(preserveSymlinks){
379+
filename=path.resolve(basePath);
380+
}else{
381+
filename=toRealPath(basePath);
382+
}
383+
}elseif(preserveSymlinksMain){
384+
// For the main module, we use the preserveSymlinksMain flag instead
385+
// mainly for backward compatibility, as the preserveSymlinks flag
386+
// historically has not applied to the main module. Most likely this
387+
// was intended to keep .bin/ binaries working, as following those
388+
// symlinks is usually required for the imports in the corresponding
389+
// files to resolve; that said, in some use cases following symlinks
390+
// causes bigger problems which is why the preserveSymlinksMain option
391+
// is needed.
392+
filename=path.resolve(basePath);
393+
}else{
394+
filename=toRealPath(basePath);
395+
}
396+
}
397+
398+
if(!filename){
399+
// Try it with each of the extensions
400+
if(exts===undefined)
401+
exts=Object.keys(Module._extensions);
402+
filename=tryExtensions(basePath,exts,isMain);
403+
}
404+
}
405+
406+
if(!filename&&rc===1){// Directory.
407+
// try it with each of the extensions at "index"
408+
if(exts===undefined)
409+
exts=Object.keys(Module._extensions);
410+
filename=tryPackage(basePath,exts,isMain,request);
411+
}
412+
413+
returnfilename;
414+
}
415+
416+
functiontrySelf(paths,exts,isMain,trailingSlash,request){
417+
if(!experimentalSelf){
418+
returnfalse;
419+
}
420+
421+
const{data: pkg,path: basePath}=readPackageScope(paths[0]);
422+
if(!pkg)returnfalse;
423+
if(typeofpkg.name!=='string')returnfalse;
424+
425+
letexpansion;
426+
if(request===pkg.name){
427+
expansion='';
428+
}elseif(StringPrototype.startsWith(request,`${pkg.name}/`)){
429+
expansion=StringPrototype.slice(request,pkg.name.length);
430+
}else{
431+
returnfalse;
432+
}
433+
434+
if(exts===undefined)
435+
exts=Object.keys(Module._extensions);
436+
437+
if(expansion){
438+
// Use exports
439+
constfromExports=applyExports(basePath,expansion);
440+
if(!fromExports)returnfalse;
441+
returnresolveBasePath(fromExports,exts,isMain,trailingSlash,request);
442+
}else{
443+
// Use main field
444+
returntryPackage(basePath,exts,isMain,request);
445+
}
446+
}
447+
448+
functionapplyExports(basePath,expansion){
449+
constpkgExports=readPackageExports(basePath);
450+
constmappingKey=`.${expansion}`;
451+
452+
if(typeofpkgExports==='object'&&pkgExports!==null){
453+
if(ObjectPrototype.hasOwnProperty(pkgExports,mappingKey)){
454+
constmapping=pkgExports[mappingKey];
455+
returnresolveExportsTarget(pathToFileURL(basePath+'/'),mapping,'',
456+
basePath,mappingKey);
457+
}
458+
459+
letdirMatch='';
460+
for(constcandidateKeyofObject.keys(pkgExports)){
461+
if(candidateKey[candidateKey.length-1]!=='/')continue;
462+
if(candidateKey.length>dirMatch.length&&
463+
StringPrototype.startsWith(mappingKey,candidateKey)){
464+
dirMatch=candidateKey;
465+
}
466+
}
467+
468+
if(dirMatch!==''){
469+
constmapping=pkgExports[dirMatch];
470+
constsubpath=StringPrototype.slice(mappingKey,dirMatch.length);
471+
returnresolveExportsTarget(pathToFileURL(basePath+'/'),mapping,
472+
subpath,basePath,mappingKey);
473+
}
474+
}
475+
if(mappingKey==='.'&&typeofpkgExports==='string'){
476+
returnresolveExportsTarget(pathToFileURL(basePath+'/'),pkgExports,
477+
'',basePath,mappingKey);
478+
}
479+
if(pkgExports!=null){
480+
// eslint-disable-next-line no-restricted-syntax
481+
conste=newError(`Package exports for '${basePath}' do not define `+
482+
`a '${mappingKey}' subpath`);
483+
e.code='MODULE_NOT_FOUND';
484+
throwe;
485+
}
486+
487+
returnpath.resolve(basePath,mappingKey);
488+
}
489+
369490
// This only applies to requests of a specific form:
370491
// 1. name/.*
371492
// 2. @scope/name/.*
@@ -380,43 +501,7 @@ function resolveExports(nmPath, request, absoluteRequest) {
380501
}
381502

382503
constbasePath=path.resolve(nmPath,name);
383-
constpkgExports=readPackageExports(basePath);
384-
constmappingKey=`.${expansion}`;
385-
386-
if(typeofpkgExports==='object'&&pkgExports!==null){
387-
if(ObjectPrototype.hasOwnProperty(pkgExports,mappingKey)){
388-
constmapping=pkgExports[mappingKey];
389-
returnresolveExportsTarget(pathToFileURL(basePath+'/'),mapping,'',
390-
basePath,mappingKey);
391-
}
392-
393-
letdirMatch='';
394-
for(constcandidateKeyofObject.keys(pkgExports)){
395-
if(candidateKey[candidateKey.length-1]!=='/')continue;
396-
if(candidateKey.length>dirMatch.length&&
397-
StringPrototype.startsWith(mappingKey,candidateKey)){
398-
dirMatch=candidateKey;
399-
}
400-
}
401-
402-
if(dirMatch!==''){
403-
constmapping=pkgExports[dirMatch];
404-
constsubpath=StringPrototype.slice(mappingKey,dirMatch.length);
405-
returnresolveExportsTarget(pathToFileURL(basePath+'/'),mapping,
406-
subpath,basePath,mappingKey);
407-
}
408-
}
409-
if(mappingKey==='.'&&typeofpkgExports==='string'){
410-
returnresolveExportsTarget(pathToFileURL(basePath+'/'),pkgExports,
411-
'',basePath,mappingKey);
412-
}
413-
if(pkgExports!=null){
414-
// eslint-disable-next-line no-restricted-syntax
415-
conste=newError(`Package exports for '${basePath}' do not define `+
416-
`a '${mappingKey}' subpath`);
417-
e.code='MODULE_NOT_FOUND';
418-
throwe;
419-
}
504+
returnapplyExports(basePath,expansion);
420505
}
421506

422507
returnpath.resolve(nmPath,request);
@@ -532,6 +617,13 @@ Module._findPath = function(request, paths, isMain) {
532617
returnfilename;
533618
}
534619
}
620+
621+
constselfFilename=trySelf(paths,exts,isMain,trailingSlash,request);
622+
if(selfFilename){
623+
Module._pathCache[cacheKey]=selfFilename;
624+
returnselfFilename;
625+
}
626+
535627
returnfalse;
536628
};
537629

‎src/env.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,15 @@ struct PackageConfig {
9393
enumclassExists { Yes, No };
9494
enumclassIsValid { Yes, No };
9595
enumclassHasMain { Yes, No };
96+
enumclassHasName { Yes, No };
9697
enum PackageType : uint32_t { None = 0, CommonJS, Module };
9798

9899
const Exists exists;
99100
const IsValid is_valid;
100101
const HasMain has_main;
101102
const std::string main;
103+
const HasName has_name;
104+
const std::string name;
102105
const PackageType type;
103106

104107
v8::Global<v8::Value> exports;

0 commit comments

Comments
 (0)