mirror of https://github.com/lukechilds/node.git
Browse Source
This enables a --loader flag for Node, which can provide custom "resolve" and "dynamicInstantiate" methods for custom ES module loading. In the process, module providers have been converted from classes into functions and the module APIs have been made to pass URL strings over objects. PR-URL: https://github.com/nodejs/node/pull/15445 Reviewed-By: Bradley Farias <bradley.meck@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Jeremiah Senkpiel <fishrock123@rocketmail.com> Reviewed-By: Michaël Zasso <targos@protonmail.com> Reviewed-By: Timothy Gu <timothygu99@gmail.com>v9.x-staging
guybedford
7 years ago
committed by
Bradley Farias
23 changed files with 507 additions and 198 deletions
@ -0,0 +1,122 @@ |
|||
'use strict'; |
|||
|
|||
const fs = require('fs'); |
|||
const internalCJSModule = require('internal/module'); |
|||
const internalURLModule = require('internal/url'); |
|||
const internalFS = require('internal/fs'); |
|||
const NativeModule = require('native_module'); |
|||
const { extname, _makeLong } = require('path'); |
|||
const { URL } = require('url'); |
|||
const { realpathSync } = require('fs'); |
|||
const preserveSymlinks = !!process.binding('config').preserveSymlinks; |
|||
const { |
|||
ModuleWrap, |
|||
createDynamicModule |
|||
} = require('internal/loader/ModuleWrap'); |
|||
const errors = require('internal/errors'); |
|||
|
|||
const search = require('internal/loader/search'); |
|||
const asyncReadFile = require('util').promisify(require('fs').readFile); |
|||
const debug = require('util').debuglog('esm'); |
|||
|
|||
const realpathCache = new Map(); |
|||
|
|||
const loaders = new Map(); |
|||
exports.loaders = loaders; |
|||
|
|||
// Strategy for loading a standard JavaScript module
|
|||
loaders.set('esm', async (url) => { |
|||
const source = `${await asyncReadFile(new URL(url))}`; |
|||
debug(`Loading StandardModule ${url}`); |
|||
return { |
|||
module: new ModuleWrap(internalCJSModule.stripShebang(source), url), |
|||
reflect: undefined |
|||
}; |
|||
}); |
|||
|
|||
// Strategy for loading a node-style CommonJS module
|
|||
loaders.set('cjs', async (url) => { |
|||
return createDynamicModule(['default'], url, (reflect) => { |
|||
debug(`Loading CJSModule ${url}`); |
|||
const CJSModule = require('module'); |
|||
const pathname = internalURLModule.getPathFromURL(new URL(url)); |
|||
CJSModule._load(pathname); |
|||
}); |
|||
}); |
|||
|
|||
// Strategy for loading a node builtin CommonJS module that isn't
|
|||
// through normal resolution
|
|||
loaders.set('builtin', async (url) => { |
|||
return createDynamicModule(['default'], url, (reflect) => { |
|||
debug(`Loading BuiltinModule ${url}`); |
|||
const exports = NativeModule.require(url.substr(5)); |
|||
reflect.exports.default.set(exports); |
|||
}); |
|||
}); |
|||
|
|||
loaders.set('addon', async (url) => { |
|||
const ctx = createDynamicModule(['default'], url, (reflect) => { |
|||
debug(`Loading NativeModule ${url}`); |
|||
const module = { exports: {} }; |
|||
const pathname = internalURLModule.getPathFromURL(new URL(url)); |
|||
process.dlopen(module, _makeLong(pathname)); |
|||
reflect.exports.default.set(module.exports); |
|||
}); |
|||
return ctx; |
|||
}); |
|||
|
|||
loaders.set('json', async (url) => { |
|||
return createDynamicModule(['default'], url, (reflect) => { |
|||
debug(`Loading JSONModule ${url}`); |
|||
const pathname = internalURLModule.getPathFromURL(new URL(url)); |
|||
const content = fs.readFileSync(pathname, 'utf8'); |
|||
try { |
|||
const exports = JSON.parse(internalCJSModule.stripBOM(content)); |
|||
reflect.exports.default.set(exports); |
|||
} catch (err) { |
|||
err.message = pathname + ': ' + err.message; |
|||
throw err; |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
exports.resolve = (specifier, parentURL) => { |
|||
if (NativeModule.nonInternalExists(specifier)) { |
|||
return { |
|||
url: specifier, |
|||
format: 'builtin' |
|||
}; |
|||
} |
|||
|
|||
let url = search(specifier, parentURL); |
|||
|
|||
if (url.protocol !== 'file:') { |
|||
throw new errors.Error('ERR_INVALID_PROTOCOL', |
|||
url.protocol, 'file:'); |
|||
} |
|||
|
|||
if (!preserveSymlinks) { |
|||
const real = realpathSync(internalURLModule.getPathFromURL(url), { |
|||
[internalFS.realpathCacheKey]: realpathCache |
|||
}); |
|||
const old = url; |
|||
url = internalURLModule.getURLFromFilePath(real); |
|||
url.search = old.search; |
|||
url.hash = old.hash; |
|||
} |
|||
|
|||
const ext = extname(url.pathname); |
|||
switch (ext) { |
|||
case '.mjs': |
|||
return { url: `${url}`, format: 'esm' }; |
|||
case '.json': |
|||
return { url: `${url}`, format: 'json' }; |
|||
case '.node': |
|||
return { url: `${url}`, format: 'addon' }; |
|||
case '.js': |
|||
return { url: `${url}`, format: 'cjs' }; |
|||
default: |
|||
throw new errors.Error('ERR_UNKNOWN_FILE_EXTENSION', |
|||
internalURLModule.getPathFromURL(url)); |
|||
} |
|||
}; |
@ -1,104 +0,0 @@ |
|||
'use strict'; |
|||
|
|||
const { URL } = require('url'); |
|||
const internalCJSModule = require('internal/module'); |
|||
const internalURLModule = require('internal/url'); |
|||
const internalFS = require('internal/fs'); |
|||
const NativeModule = require('native_module'); |
|||
const { extname } = require('path'); |
|||
const { realpathSync } = require('fs'); |
|||
const preserveSymlinks = !!process.binding('config').preserveSymlinks; |
|||
const { |
|||
ModuleWrap, |
|||
createDynamicModule |
|||
} = require('internal/loader/ModuleWrap'); |
|||
const errors = require('internal/errors'); |
|||
|
|||
const search = require('internal/loader/search'); |
|||
const asyncReadFile = require('util').promisify(require('fs').readFile); |
|||
const debug = require('util').debuglog('esm'); |
|||
|
|||
const realpathCache = new Map(); |
|||
|
|||
class ModuleRequest { |
|||
constructor(url) { |
|||
this.url = url; |
|||
} |
|||
} |
|||
Object.setPrototypeOf(ModuleRequest.prototype, null); |
|||
|
|||
// Strategy for loading a standard JavaScript module
|
|||
class StandardModuleRequest extends ModuleRequest { |
|||
async createModule() { |
|||
const source = `${await asyncReadFile(this.url)}`; |
|||
debug(`Loading StandardModule ${this.url}`); |
|||
return new ModuleWrap(internalCJSModule.stripShebang(source), |
|||
`${this.url}`); |
|||
} |
|||
} |
|||
|
|||
// Strategy for loading a node-style CommonJS module
|
|||
class CJSModuleRequest extends ModuleRequest { |
|||
async createModule() { |
|||
const ctx = createDynamicModule(['default'], this.url, (reflect) => { |
|||
debug(`Loading CJSModule ${this.url.pathname}`); |
|||
const CJSModule = require('module'); |
|||
const pathname = internalURLModule.getPathFromURL(this.url); |
|||
CJSModule._load(pathname); |
|||
}); |
|||
this.finish = (module) => { |
|||
ctx.reflect.exports.default.set(module.exports); |
|||
}; |
|||
return ctx.module; |
|||
} |
|||
} |
|||
|
|||
// Strategy for loading a node builtin CommonJS module that isn't
|
|||
// through normal resolution
|
|||
class NativeModuleRequest extends CJSModuleRequest { |
|||
async createModule() { |
|||
const ctx = createDynamicModule(['default'], this.url, (reflect) => { |
|||
debug(`Loading NativeModule ${this.url.pathname}`); |
|||
const exports = require(this.url.pathname); |
|||
reflect.exports.default.set(exports); |
|||
}); |
|||
return ctx.module; |
|||
} |
|||
} |
|||
|
|||
const normalizeBaseURL = (baseURLOrString) => { |
|||
if (baseURLOrString instanceof URL) return baseURLOrString; |
|||
if (typeof baseURLOrString === 'string') return new URL(baseURLOrString); |
|||
return undefined; |
|||
}; |
|||
|
|||
const resolveRequestUrl = (baseURLOrString, specifier) => { |
|||
if (NativeModule.nonInternalExists(specifier)) { |
|||
return new NativeModuleRequest(new URL(`node:${specifier}`)); |
|||
} |
|||
|
|||
const baseURL = normalizeBaseURL(baseURLOrString); |
|||
let url = search(specifier, baseURL); |
|||
|
|||
if (url.protocol !== 'file:') { |
|||
throw new errors.Error('ERR_INVALID_PROTOCOL', url.protocol, 'file:'); |
|||
} |
|||
|
|||
if (!preserveSymlinks) { |
|||
const real = realpathSync(internalURLModule.getPathFromURL(url), { |
|||
[internalFS.realpathCacheKey]: realpathCache |
|||
}); |
|||
const old = url; |
|||
url = internalURLModule.getURLFromFilePath(real); |
|||
url.search = old.search; |
|||
url.hash = old.hash; |
|||
} |
|||
|
|||
const ext = extname(url.pathname); |
|||
if (ext === '.mjs') { |
|||
return new StandardModuleRequest(url); |
|||
} |
|||
|
|||
return new CJSModuleRequest(url); |
|||
}; |
|||
module.exports = resolveRequestUrl; |
@ -0,0 +1,3 @@ |
|||
{ |
|||
"val": 42 |
|||
} |
@ -0,0 +1,7 @@ |
|||
// Flags: --experimental-modules
|
|||
/* eslint-disable required-modules */ |
|||
|
|||
import assert from 'assert'; |
|||
import binding from '../addons/hello-world/build/Release/binding.node'; |
|||
assert.strictEqual(binding.hello(), 'world'); |
|||
console.log('binding.hello() =', binding.hello()); |
@ -0,0 +1,6 @@ |
|||
// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/example-loader.mjs
|
|||
/* eslint-disable required-modules */ |
|||
import assert from 'assert'; |
|||
import ok from './test-esm-ok.mjs'; |
|||
|
|||
assert(ok); |
@ -0,0 +1,8 @@ |
|||
// Flags: --experimental-modules
|
|||
/* eslint-disable required-modules */ |
|||
import assert from 'assert'; |
|||
import ok from './test-esm-ok.mjs'; |
|||
import json from './json.json'; |
|||
|
|||
assert(ok); |
|||
assert.strictEqual(json.val, 42); |
@ -0,0 +1,8 @@ |
|||
// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/builtin-named-exports-loader.mjs
|
|||
/* eslint-disable required-modules */ |
|||
import { readFile } from 'fs'; |
|||
import assert from 'assert'; |
|||
import ok from './test-esm-ok.mjs'; |
|||
|
|||
assert(ok); |
|||
assert(readFile); |
@ -0,0 +1,8 @@ |
|||
// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/js-loader.mjs
|
|||
/* eslint-disable required-modules */ |
|||
import { namedExport } from '../fixtures/es-module-loaders/js-as-esm.js'; |
|||
import assert from 'assert'; |
|||
import ok from './test-esm-ok.mjs'; |
|||
|
|||
assert(ok); |
|||
assert(namedExport); |
@ -0,0 +1,29 @@ |
|||
import module from 'module'; |
|||
|
|||
const builtins = new Set( |
|||
Object.keys(process.binding('natives')).filter(str => |
|||
/^(?!(?:internal|node|v8)\/)/.test(str)) |
|||
) |
|||
|
|||
export function resolve (specifier, base, defaultResolver) { |
|||
if (builtins.has(specifier)) { |
|||
return { |
|||
url: `node:${specifier}`, |
|||
format: 'dynamic' |
|||
}; |
|||
} |
|||
return defaultResolver(specifier, base); |
|||
} |
|||
|
|||
export async function dynamicInstantiate (url) { |
|||
const builtinInstance = module._load(url.substr(5)); |
|||
const builtinExports = ['default', ...Object.keys(builtinInstance)]; |
|||
return { |
|||
exports: builtinExports, |
|||
execute: exports => { |
|||
for (let name of builtinExports) |
|||
exports[name].set(builtinInstance[name]); |
|||
exports.default.set(builtinInstance); |
|||
} |
|||
}; |
|||
} |
@ -0,0 +1,34 @@ |
|||
import url from 'url'; |
|||
import path from 'path'; |
|||
import process from 'process'; |
|||
|
|||
const builtins = new Set( |
|||
Object.keys(process.binding('natives')).filter((str) => |
|||
/^(?!(?:internal|node|v8)\/)/.test(str)) |
|||
); |
|||
const JS_EXTENSIONS = new Set(['.js', '.mjs']); |
|||
|
|||
export function resolve(specifier, parentModuleURL/*, defaultResolve */) { |
|||
if (builtins.has(specifier)) { |
|||
return { |
|||
url: specifier, |
|||
format: 'builtin' |
|||
}; |
|||
} |
|||
if (/^\.{0,2}[/]/.test(specifier) !== true && !specifier.startsWith('file:')) { |
|||
// For node_modules support:
|
|||
// return defaultResolve(specifier, parentModuleURL);
|
|||
throw new Error( |
|||
`imports must begin with '/', './', or '../'; '${specifier}' does not`); |
|||
} |
|||
const resolved = new url.URL(specifier, parentModuleURL); |
|||
const ext = path.extname(resolved.pathname); |
|||
if (!JS_EXTENSIONS.has(ext)) { |
|||
throw new Error( |
|||
`Cannot load file with non-JavaScript file extension ${ext}.`); |
|||
} |
|||
return { |
|||
url: resolved.href, |
|||
format: 'esm' |
|||
}; |
|||
} |
@ -0,0 +1 @@ |
|||
export const namedExport = 'named-export'; |
@ -0,0 +1,19 @@ |
|||
import _url from 'url'; |
|||
const builtins = new Set( |
|||
Object.keys(process.binding('natives')).filter(str => |
|||
/^(?!(?:internal|node|v8)\/)/.test(str)) |
|||
) |
|||
export function resolve (specifier, base) { |
|||
if (builtins.has(specifier)) { |
|||
return { |
|||
url: specifier, |
|||
format: 'builtin' |
|||
}; |
|||
} |
|||
// load all dependencies as esm, regardless of file extension
|
|||
const url = new _url.URL(specifier, base).href; |
|||
return { |
|||
url, |
|||
format: 'esm' |
|||
}; |
|||
} |
Loading…
Reference in new issue