-
-
Save Jessidhia/610ad7bd9231477744206a2a8db42abd to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// // given | |
// | |
// import foo from "bar" | |
// export let a | |
// export { a as b } | |
// export function hoisted() {} | |
// export {readFile} from "fs" | |
// export * from "path" | |
// | |
// console.log(foo) | |
// | |
// causes *all* imports from this file to be guessed as available | |
// in spec collisions from different module records == SyntaxError | |
// should actually throw at runtime as there is no "readFile" export in "fs" | |
// | |
/* eslint-disable strict, no-console */ | |
'use strict' | |
/** | |
* @typedef {{ | |
__proto__: null, | |
[exportName: string]: any | |
}} ModuleRecord | |
* Also has a [Symbol.for('ModuleDriverEvaluate')] key to eagerly evaluate without deadlocking | |
*/ | |
/** | |
* @typedef {{ | |
source: string, | |
importName: string|string[]|undefined | |
}} ImportDescriptor | |
* importName is plain string for named import, | |
* string[] for namespace import with known accessed imports, | |
* undefined for namespace import with unknown (dynamic) import access | |
*/ | |
/** | |
* @typedef {{ | |
[name: string]: ImportDescriptor | |
}} ImportDescriptorMap | |
*/ | |
/** | |
* @typedef {{ | |
dependencies: [string, () => any][], | |
imports: ImportDescriptorMap, | |
exports: { | |
[exportName: string]: () => any | |
}, | |
reexports: ImportDescriptorMap, | |
starReexports: string[] | |
}} RecordConfig | |
* the "() => any" in dependencies should be a call to require() for the appropriate module | |
* the "() => any" in exports is a getter body | |
*/ | |
const Evaluate = Symbol.for('ModuleDriverEvaluate') | |
function isCJS (exports) { | |
return !(exports && exports.__esModule || exports instanceof Promise) | |
} | |
// ModuleDriver is defined in a way that allows it to be put in | |
// an external runtime module to avoid excess duplicated code | |
class ModuleDriver { | |
/** | |
* @param {function(ModuleDriver): Iterator<(ModuleRecord|Promise<void>)>} moduleGenerator | |
*/ | |
constructor (moduleGenerator) { | |
this.generator = moduleGenerator(this) | |
/** @type {ModuleRecord|undefined} */ | |
this.moduleRecord = undefined | |
/** @type {boolean} */ | |
this.evaluated = false | |
/** @type {RecordConfig|undefined} */ | |
this.config = undefined | |
/** @type {{[name: string]: any}} */ | |
this.imports = Object.create(null) | |
/** @type {{[name: string]: () => any}} */ | |
this.getters = Object.create(null) | |
/** @type {{[name: string]: ModuleRecord}} */ | |
this.dependencyCache = Object.create(null) | |
} | |
/** | |
* @param {string} name | |
*/ | |
static tdz (name) { | |
throw new ReferenceError(`${name} is not defined`) | |
} | |
/** | |
* @param {RecordConfig} config | |
*/ | |
makeModuleRecord (config) { | |
this.config = Object.freeze(config) | |
/** @type {ModuleRecord} */ | |
const record = this.moduleRecord = Object.create(null, { | |
[Symbol.toStringTag]: { value: 'Module' } | |
}) | |
for (const [exportName, getter] of Object.entries(config.exports)) { | |
Object.defineProperty(record, exportName, { | |
enumerable: true, | |
get: this.makeExport(exportName, getter) | |
}) | |
} | |
for (const [exportName, descriptor] of Object.entries(config.reexports)) { | |
Object.defineProperty(record, exportName, { | |
enumerable: true, | |
get: this.makeReexport(exportName, descriptor) | |
}) | |
} | |
return record | |
} | |
/** | |
* Freezes module record, runs imports, validates reexports, applies star reexports. | |
*/ | |
async finalizeRecord () { | |
const record = this.moduleRecord | |
// do requires of each dependency | |
for (const [name, factory] of this.config.dependencies) { | |
if (this.dependencyCache[name]) continue | |
const imported = factory() | |
if (isCJS(imported)) { | |
// a module record with only a default export | |
this.dependencyCache[name] = Object.create(null, { | |
[Symbol.toStringTag]: { value: 'Module' }, | |
default: { enumerable: true, value: imported } | |
}) | |
} else if (!(imported instanceof Promise)) { | |
// traditional babelmodule | |
this.dependencyCache[name] = imported | |
} else { | |
if ('__esModule' in imported) { | |
// a ModuleDriver module | |
this.dependencyCache[name] = imported.__esModule | |
// we can't await on it otherwise this would deadlock on circular imports | |
// rely on microtask loop timing instead 😨 | |
if (Evaluate in imported) { | |
/** @type {function (): void} */ | |
const evaluate = imported[Evaluate] | |
Reflect.deleteProperty(imported, Evaluate) | |
evaluate() | |
} | |
} else { | |
// native ESM? shouldn't happen but... | |
this.dependencyCache[name] = await imported // eslint-disable-line babel/no-await-in-loop | |
} | |
} | |
} | |
if (this.config.starReexports.length > 0) { | |
// exports were not frozen in this case, so try to | |
// delete any potential configurable keys | |
for (const sym of Object.getOwnPropertySymbols(record)) { | |
Reflect.deleteProperty(record, sym) | |
} | |
for (const name of Object.getOwnPropertyNames(record)) { | |
Reflect.deleteProperty(record, name) | |
} | |
// get a list of all explicit exports | |
const ownExports = new Set(Object.keys(this.moduleRecord)) | |
for (const moduleName of this.config.starReexports) { | |
const starRecord = this.dependencyCache[moduleName] | |
for (const exportName of Object.keys(starRecord)) { | |
// ignore default or names that match explicit exports | |
if (exportName === 'default' || ownExports.has(exportName)) continue | |
// ensure any potential duplicate is SameValue | |
// this will probably throw with non-hoisted declarations, | |
// nothing that can be done about it other than not checking :/ | |
if (exportName in this.moduleRecord && !Object.is(this.moduleRecord[exportName], starRecord[exportName])) { | |
throw new SyntaxError(`Conflicting star reexport ${exportName}`) | |
} | |
const descriptor = Object.getOwnPropertyDescriptor(starRecord, exportName) | |
Object.defineProperty(this.moduleRecord, exportName, { | |
enumerable: true, | |
get: descriptor.get || this.makeReexport(exportName, { source: moduleName, importName: exportName }) | |
}) | |
} | |
} | |
Object.freeze(record) | |
} | |
// validate and initialize imports | |
for (const [localName, { source, importName }] of Object.entries(this.config.imports)) { | |
const depRecord = this.dependencyCache[source] | |
if (importName === undefined) { | |
Object.defineProperty(this.imports, localName, { get () { return depRecord } }) | |
} else { | |
const isNamespace = Array.isArray(importName) | |
for (const name of isNamespace ? importName : [importName]) { | |
const descriptor = Object.getOwnPropertyDescriptor(depRecord, name) | |
if (!descriptor || !descriptor.enumerable) throw new SyntaxError(`Export ${name} not found in ${source}`) | |
// it doesn't _really_ matter if it's configurable so | |
// just copy the descriptor as long as it's a getter. Otherwise, | |
// set up a getter to ensure live binding. | |
if (!isNamespace) { | |
Object.defineProperty(this.imports, localName, descriptor.get && !descriptor.set ? descriptor : { get () { return depRecord[name] } }) | |
} | |
} | |
if (isNamespace) { | |
Object.defineProperty(this.imports, localName, { enumerable: true, get () { return depRecord } }) | |
} | |
} | |
} | |
// validate reexports, and try to flatten getters if possible | |
for (const [exportName, { source, importName }] of Object.entries(this.config.reexports)) { | |
const depRecord = this.dependencyCache[source] | |
if (importName === undefined || Array.isArray(importName)) { | |
// validating whether importers only import | |
// available exports here would require Proxy | |
this.getters[exportName] = () => depRecord | |
} else { | |
const descriptor = Object.getOwnPropertyDescriptor(depRecord, importName) | |
if (!descriptor || !descriptor.enumerable) throw new SyntaxError(`Export ${importName} not found in ${source}`) | |
if (descriptor.get) { | |
// try to flatten getters | |
this.getters[exportName] = descriptor.get | |
} else { | |
this.getters[exportName] = () => depRecord[importName] | |
} | |
} | |
} | |
Object.freeze(this.getters) | |
} | |
/** | |
* @param {string} exportName | |
* @param {function(): any} getter | |
*/ | |
makeExport (exportName, getter = () => ModuleDriver.tdz(exportName)) { | |
return () => (0, getter)() | |
} | |
/** | |
* @param {string} exportName | |
* @param {ImportDescriptor} descriptor | |
*/ | |
makeReexport (exportName, { source, importName }) { | |
this.getters[exportName] = () => ModuleDriver.tdz(exportName) | |
return () => (0, this.getters[exportName])() | |
} | |
makeModuleExports () { | |
const record = this.generator.next().value | |
const exports = (async () => { | |
await undefined // await for event loop | |
await this.evaluate() | |
return this.moduleRecord | |
})() | |
Object.defineProperty(exports, Evaluate, { configurable: true, value: () => this.evaluate() }) | |
Object.defineProperty(exports, '__esModule', { value: record }) | |
return exports | |
} | |
async evaluate () { | |
if (!this.evaluated) { | |
Object.defineProperty(this, 'evaluated', { value: true }) | |
let next = this.generator.next() | |
if (!next.done) { | |
await next.value // await for imports | |
next = this.generator.next() | |
} | |
} | |
} | |
} | |
const moduleDriver = new ModuleDriver(function * (driver) { | |
yield driver.makeModuleRecord({ | |
dependencies: [ | |
['bar', () => require('bar')], | |
['fs', () => require('fs')], | |
['path', () => require('path')] | |
], | |
imports: { | |
'foo': { | |
source: 'bar', | |
importName: 'default' | |
} | |
}, | |
exports: { | |
'a': () => a, | |
'b': () => a, | |
'hoisted': () => hoisted | |
}, | |
reexports: { | |
'readFile': { | |
source: 'fs', | |
importName: 'readFile' | |
} | |
}, | |
starReexports: [ 'path' ] | |
}) | |
yield driver.finalizeRecord() | |
let a | |
function hoisted () {} | |
console.log(driver.imports.foo) | |
}) | |
module.exports = moduleDriver.makeModuleExports() | |
/* eslint-enable */ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment