diff --git a/index.js b/index.js index bc8ba26..3035604 100644 --- a/index.js +++ b/index.js @@ -2,6 +2,7 @@ const path = require('bare-path') const resolve = require('bare-module-resolve') const Bundle = require('bare-bundle') +const { parse } = require('cjs-module-lexer') const { fileURLToPath, pathToFileURL } = require('url-file-url') const Protocol = require('./lib/protocol') const constants = require('./lib/constants') @@ -27,6 +28,7 @@ const Module = module.exports = exports = class Module { this._conditions = null this._protocol = null this._bundle = null + this._function = null this._handle = null Module._modules.add(this) @@ -112,42 +114,99 @@ const Module = module.exports = exports = class Module { _transform (isImport, isDynamicImport) { if (isDynamicImport) { this._synthesize() - this._evaluate() + this._evaluate(true /* eagerRun */) } else if (isImport) { this._synthesize() - } else if (this._type === constants.types.MODULE) { + } else { this._evaluate() } return this } - _evaluate () { - if ((this._state & constants.states.EVALUATED) !== 0) return + _synthesize () { + if ((this._state & constants.states.SYNTHESIZED) !== 0) return - binding.runModule(this._handle, Module._handle) + this._state |= constants.states.SYNTHESIZED - if (this._type === constants.types.MODULE) { - this._exports = binding.getNamespace(this._handle) + if (this._type === constants.types.MODULE) return + + const names = ['default'] + const queue = [this] + const seen = new Set() + + while (queue.length) { + const module = queue.pop() + + if (seen.has(module)) continue + + seen.add(module) + + switch (module._type) { + case constants.types.SCRIPT: { + const result = parse(module._function.toString()) + + names.push(...result.exports) + + const referrer = module + + for (const specifier of result.reexports) { + const resolved = Module.resolve(specifier, referrer._url, { isImport: true, referrer }) + + const module = Module.load(resolved, { isImport: true, referrer }) + + queue.push(module) + } + + break + } + + case constants.types.MODULE: { + module._evaluate() + + for (const name of Object.keys(module._exports)) { + names.push(name) + } + break + } + + case constants.types.JSON: { + for (const name of Object.keys(module._exports)) { + names.push(name) + } + } + } } - this._state |= constants.states.EVALUATED + this._handle = binding.createSyntheticModule(this._url.href, names, Module._handle) } - _synthesize () { - if ((this._state & constants.states.SYNTHESIZED) !== 0) return + _evaluate (eagerRun = false) { + if ((this._state & constants.states.EVALUATED) !== 0) return - if (this._type !== constants.types.MODULE) { - const names = ['default'] + this._state |= constants.states.EVALUATED - for (const key of Object.keys(this._exports)) { - if (key !== 'default') names.push(key) - } + if (this._type === constants.types.SCRIPT) { + const require = createRequire(this._url, { module: this }) + + this._exports = {} + + this._function( + require, + this, + this._exports, + urlToPath(this._url), + urlToDirname(this._url) + ) - this._handle = binding.createSyntheticModule(this._url.href, names, Module._handle) + if (eagerRun) binding.runModule(this._handle, Module._handle) } - this._state |= constants.states.SYNTHESIZED + if (this._type === constants.types.MODULE) { + binding.runModule(this._handle, Module._handle) + + this._exports = binding.getNamespace(this._handle) + } } [Symbol.for('bare.inspect')] () { @@ -218,6 +277,8 @@ const Module = module.exports = exports = class Module { throw errors.MODULE_NOT_FOUND(`Cannot find module '${href}'`) } + module._evaluate() + binding.setExport(module._handle, 'default', module._exports) for (const [key, value] of Object.entries(module._exports)) { @@ -415,7 +476,7 @@ exports.isBuiltin = function isBuiltin () { return false } -exports.createRequire = function createRequire (parentURL, opts = {}) { +const createRequire = exports.createRequire = function createRequire (parentURL, opts = {}) { const self = Module if (typeof parentURL === 'string') { @@ -427,6 +488,8 @@ exports.createRequire = function createRequire (parentURL, opts = {}) { } let { + module = null, + referrer = null, type = constants.types.SCRIPT, defaultType = referrer ? referrer._defaultType : constants.types.SCRIPT, @@ -439,17 +502,19 @@ exports.createRequire = function createRequire (parentURL, opts = {}) { conditions = referrer ? referrer._conditions : self._conditions } = opts - const module = new Module(parentURL) + if (module === null) { + module = new Module(parentURL) - module._type = type - module._defaultType = defaultType - module._cache = cache - module._main = main || module - module._protocol = protocol - module._imports = imports - module._resolutions = resolutions - module._builtins = builtins - module._conditions = conditions + module._type = type + module._defaultType = defaultType + module._cache = cache + module._main = main || module + module._protocol = protocol + module._imports = imports + module._resolutions = resolutions + module._builtins = builtins + module._conditions = conditions + } referrer = module @@ -522,8 +587,6 @@ Module._extensions['.js'] = function (module, source, referrer) { } Module._extensions['.cjs'] = function (module, source, referrer) { - const self = Module - const protocol = module._protocol module._type = constants.types.SCRIPT @@ -535,49 +598,7 @@ Module._extensions['.cjs'] = function (module, source, referrer) { if (typeof source !== 'string') source = Buffer.coerce(source).toString() - referrer = module - - addon.host = Bare.Addon.host - - require.main = module._main - require.cache = module._cache - require.resolve = resolve - require.addon = addon - - module._exports = {} - - binding.createFunction(module._url.href, ['require', 'module', 'exports', '__filename', '__dirname'], source, 0)( - require, - module, - module._exports, - urlToPath(module._url), - urlToDirname(module._url) - ) - - function require (specifier) { - const resolved = self.resolve(specifier, referrer._url, { referrer }) - - const module = self.load(resolved, { referrer }) - - return module._exports - } - - function resolve (specifier) { - const resolved = self.resolve(specifier, referrer._url, { referrer }) - - switch (resolved.protocol) { - case 'builtin:': return resolved.pathname - default: urlToPath(resolved) - } - } - - function addon (specifier = '.') { - const resolved = Bare.Addon.resolve(specifier, referrer._url, { referrer }) - - const addon = Bare.Addon.load(resolved, { referrer }) - - return addon._exports - } + module._function = binding.createFunction(module._url.href, ['require', 'module', 'exports', '__filename', '__dirname'], source, 0) } } diff --git a/package.json b/package.json index ba06656..b394a06 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "bare-bundle": "^1.0.0", "bare-module-resolve": "^1.4.4", "bare-path": "^2.0.0", + "cjs-module-lexer": "^1.2.3", "url-file-url": "^1.0.2" }, "devDependencies": { diff --git a/test.js b/test.js index 77bce58..805d7e8 100644 --- a/test.js +++ b/test.js @@ -358,6 +358,154 @@ test('load .mjs with .cjs import', (t) => { Module.load(new URL(root + '/index.mjs'), { protocol }) }) +test('load .mjs with named .cjs import', (t) => { + t.teardown(onteardown) + + const protocol = new Module.Protocol({ + exists (url) { + return url.href === root + '/foo.cjs' + }, + + read (url) { + if (url.href === root + '/index.mjs') { + return 'import { foo } from \'/foo.cjs\'' + } + + if (url.href === root + '/foo.cjs') { + return 'exports.foo = 42' + } + + t.fail() + } + }) + + Module.load(new URL(root + '/index.mjs'), { protocol }) +}) + +test('load .mjs with .cjs import with reexports from .cjs import', (t) => { + t.teardown(onteardown) + + const protocol = new Module.Protocol({ + exists (url) { + return ( + url.href === root + '/foo.cjs' || + url.href === root + '/bar.cjs' + ) + }, + + read (url) { + if (url.href === root + '/index.mjs') { + return 'import { bar } from \'/foo.cjs\'' + } + + if (url.href === root + '/foo.cjs') { + return 'module.exports = require(\'/bar.cjs\')' + } + + if (url.href === root + '/bar.cjs') { + return 'exports.bar = 42' + } + + t.fail() + } + }) + + Module.load(new URL(root + '/index.mjs'), { protocol }) +}) + +test('load .mjs with .cjs import with cyclic reexports from .cjs import', (t) => { + t.teardown(onteardown) + + const protocol = new Module.Protocol({ + exists (url) { + return ( + url.href === root + '/foo.cjs' || + url.href === root + '/bar.cjs' + ) + }, + + read (url) { + if (url.href === root + '/index.mjs') { + return 'import \'/foo.cjs\'' + } + + if (url.href === root + '/foo.cjs') { + return 'module.exports = require(\'/bar.cjs\')' + } + + if (url.href === root + '/bar.cjs') { + return 'module.exports = require(\'/foo.cjs\')' + } + + t.fail() + } + }) + + Module.load(new URL(root + '/index.mjs'), { protocol }) +}) + +test('load .mjs with .cjs import with reexports from .mjs import', (t) => { + t.teardown(onteardown) + + const protocol = new Module.Protocol({ + exists (url) { + return ( + url.href === root + '/foo.cjs' || + url.href === root + '/bar.mjs' + ) + }, + + read (url) { + if (url.href === root + '/index.mjs') { + return 'import { bar } from \'/foo.cjs\'' + } + + if (url.href === root + '/foo.cjs') { + return 'module.exports = require(\'/bar.mjs\')' + } + + if (url.href === root + '/bar.mjs') { + return 'export const bar = 42' + } + + t.fail() + } + }) + + Module.load(new URL(root + '/index.mjs'), { protocol }) +}) + +test('load .mjs with .cjs import with reexports from .json import', (t) => { + t.teardown(onteardown) + + const protocol = new Module.Protocol({ + exists (url) { + return ( + url.href === root + '/foo.cjs' || + url.href === root + '/bar.json' + ) + }, + + read (url) { + if (url.href === root + '/index.mjs') { + return 'import { bar } from \'/foo.cjs\'' + } + + if (url.href === root + '/foo.cjs') { + return 'module.exports = require(\'/bar.json\')' + } + + if (url.href === root + '/bar.json') { + return '{ "bar": 42 }' + } + + t.fail() + } + }) + + Module.load(new URL(root + '/index.mjs'), { protocol }) +}) + test('load .mjs with .js import', (t) => { t.teardown(onteardown) @@ -502,6 +650,59 @@ test('load .mjs with top-level await .mjs import', (t) => { Module.load(new URL(root + '/foo.mjs'), { protocol }) }) +test('load .cjs and .mjs from .mjs', (t) => { + t.teardown(onteardown) + + const order = global.order = [] + + const protocol = new Module.Protocol({ + exists (url) { + return ( + url.href === root + '/b.mjs' || + url.href === root + '/c.cjs' || + url.href === root + '/d.mjs' || + url.href === root + '/e.cjs' + ) + }, + + read (url) { + if (url.href === root + '/a.mjs') { + return 'order.push(\'a.mjs\'); import \'/b.mjs\'; import \'/c.cjs\'; import \'/d.mjs\'; import \'/e.cjs\'' + } + + if (url.href === root + '/b.mjs') { + return 'order.push(\'b.mjs\')' + } + + if (url.href === root + '/c.cjs') { + return 'order.push(\'c.cjs\')' + } + + if (url.href === root + '/d.mjs') { + return 'order.push(\'d.mjs\')' + } + + if (url.href === root + '/e.cjs') { + return 'order.push(\'e.cjs\')' + } + + t.fail() + } + }) + + Module.load(new URL(root + '/a.mjs'), { protocol }) + + delete global.order + + t.alike(order, [ + 'b.mjs', + 'c.cjs', + 'd.mjs', + 'e.cjs', + 'a.mjs' + ]) +}) + test('load .json', (t) => { t.teardown(onteardown)