Created
January 11, 2017 14:51
-
-
Save bmeck/49e67d1d8778fccbdd821eb2506e73dc 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 local; | |
// 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 | |
// export * from "path"; | |
// | |
'use strict'; | |
let evaluating = false; | |
// [ExportName]: {Getter,Watchers} | |
// THIS IS MUTABLE | |
// IndirectExports need to point to this | |
// StarExports need to point to this | |
// LocalExports MUST NOT point to this | |
const NamespaceGetters = Object.create(null); | |
// ModuleNamespace Object | |
// FROZEN just prior to eval (which is async) | |
// - CJS dependent can only access the Promise, freezes prior to this | |
// - ESM dependent manually links up using NamespaceGetters | |
const Namespace = Object.create(null, { | |
[Symbol.toStringTag]: { | |
value: "Module", | |
configurable: false, | |
writable: false, | |
}, | |
}); | |
const WatchersForAll = []; | |
const Imports = Object.create(null); | |
const main = (function* () { | |
const ModuleRecord = { | |
// Gets a getter for this name, replace may be called to | |
// tell the consumer to replace it with a better function. | |
// necessary to flatten re-exports | |
GetExport(name, watcher) { | |
const NSGetter = SetupNamespaceGetter(name); | |
NSGetter.Watchers.push(watcher); | |
watcher(NSGetter.Getter); | |
}, | |
// IndirectExports may replace Getters | |
// NOTE: this does not manipulate the Namespace | |
UpdateExport(name, getter) { | |
let NSGetter = SetupNamespaceGetter(name); | |
NSGetter.Getter = getter; | |
for (let watcher of NSGetter.Watchers) { | |
watcher(getter); | |
} | |
for (let watcher of WatchersForAll) { | |
watcher(name, getter); | |
} | |
}, | |
WatchAllExports(watcher) { | |
WatchersForAll.push(watcher); | |
for (let name of Object.getOwnPropertyNames(Namespace)) { | |
watcher | |
} | |
}, | |
// Used to start evaluating body | |
// Also used to prevent turning the event loop while evaluating | |
// ESM graphs | |
Evaluate() { | |
if (evaluating) return; | |
evaluating = true; | |
return main.next(); | |
}, | |
// Required to preserve order of loading | |
RequestedModules: ['bar', 'fs', ], | |
// Idempotentcy and comm channel for imports | |
DependencyCache: Object.create(null), | |
// Setup these first | |
// This solves the HoistableDeclaration problem | |
LocalExportEntries: [ | |
{ | |
ExportName: 'local', | |
Getter: () => local, | |
}, | |
{ | |
ExportName: 'hoisted', | |
Getter: () => hoisted, | |
}, | |
], | |
// Setup this after eval starts | |
// Circular Deps during eval for CJS will only see the Promise | |
// Transpilers will pick this up and GetExport's | |
IndirectExportEntries: [ | |
{ | |
ModuleRequest: 'fs', | |
ImportName: 'readFile', | |
ExportName: 'readFile', | |
} | |
], | |
StarExportEntries: [ | |
{ | |
ModuleRequest: 'path', | |
} | |
], | |
// Imports come after Exports in ModuleDeclarationInstanciation | |
ImportEntries: [ | |
{ | |
ModuleRequest: 'bar', | |
ImportName: 'default', | |
LocalName: 'foo', | |
}, | |
], | |
} | |
setupLocalExportEntries(ModuleRecord); | |
yield ModuleRecord; | |
setupDependencies(ModuleRecord); | |
let a; | |
function hoisted() {}; | |
console.log((0, Imports.foo)()); | |
})(); | |
const ModuleRecord = gen.next(); | |
module.exports = (async () => { | |
// turn the event loop | |
await null; | |
// to preserve depth first eval order between CJS and ESM | |
// don't do a linking phase | |
// | |
// NOTE: this means linking errors only occur at runtime | |
// | |
// eval entire depgraph sync | |
ModuleRecord.evaluate(); | |
return ModuleRecord; | |
})(); | |
// this is used to setup any given entry on the Namespace | |
// PURGATORY is a special TDZ for prior to linking | |
// It is finished prior to yielding ModuleRecord for LocalExports | |
// It is finished after setupDependencies for IndirectExports | |
// Due to how StarExports work and lack of pre-parsing for the | |
// ESM graph we can have permanent PURGATORY for non-existant imports: | |
// | |
// // a.js | |
// import {c} from 'b'; | |
// // b.js | |
// export * from 'c'; | |
// // c.js | |
// // no exports | |
// | |
let PURGATORY = () => {throw Error('TDZ');}; | |
function SetupNamespaceGetter(name, Getter = PURGATORY) { | |
if (!NamespaceGetters[name]) { | |
NamespaceGetters[name] = { | |
Getter, | |
Watchers: [], | |
}; | |
Object.defineProperty(Namespace, name, { | |
get: () => (0, NamespaceGetters[name].Getter)(), | |
configurable: false, | |
}); | |
for (let watcher of WatchersForAll) { | |
watcher(name, Getter); | |
} | |
} | |
return NamespaceGetters[name]; | |
} | |
function setupLocalExportEntries(ModuleRecord) { | |
for (let [ExportName,Getter] of ModuleRecord.LocalExportEntries) { | |
SetupNamespaceGetter(ExportName, Getter); | |
} | |
} | |
function isCJS(module_exports) { | |
return !module_exports.__esModule || | |
module_exports instanceof Promise !== true; | |
} | |
// Called after LocalExports are setup | |
// | |
function setupDependencies(ModuleRecord) { | |
// setup exports | |
for (let RequestedModule of ModuleRecord.RequestedModules) { | |
// order of eval is preserved here | |
// | |
// NOTE: ESM can import namespace prior to it being frozen here due to lack | |
// of preparse phase | |
// | |
// After loop we attemt to restore "proper" state by | |
// deleting unexpected additions | |
const dependency = ModuleRecord.DependencyCache[RequestedModule] || require(RequestedModule); | |
if (isCJS(dependency)) { | |
// CJS | |
for (let { | |
ModuleRequest, | |
ImportName, | |
ExportName | |
} of ModuleRecord.IndirectExportEntries) { | |
if (ModuleRequest !== RequestedModule) continue; | |
if (ImportName !== 'default') { | |
throw Error(`unable to import ${ImportName} from ${ModuleRequest}`); | |
} | |
ModuleRecord.Imports[ExportName] = dependency; | |
} | |
// StarExportEntries skips default exports | |
continue; | |
} | |
else { | |
// Faux-ESM | |
for (let { | |
ModuleRequest, | |
ImportName, | |
ExportName | |
} of ModuleRecord.IndirectExportEntries) { | |
if (ModuleRequest !== RequestedModule) continue; | |
dependency.__esModule.GetExport( | |
ImportName, Getter => { | |
ModuleRecord.UpdateExport(ExportName, Getter); | |
} | |
); | |
} | |
for (let { | |
ModuleRequest | |
} of ModuleRecord.StarExportEntries) { | |
if (ModuleRequest !== RequestedModule) continue; | |
if (ImportName === 'default') { | |
continue; | |
} | |
// the dependency shape is always finalized after Evaluate() | |
// which is called right after this, at which point it is frozen | |
dependency.__esModule.WatchAllExports( | |
(ExportName, Getter) => { | |
ModuleRecord.UpdateExport(ExportName, Getter); | |
} | |
); | |
// to perserve order of eval we need the dependency to | |
// start executing immediately (we are already in an ESM graph) | |
dependency.__esModule.Evaluate(); | |
} | |
} | |
} | |
// attempt cleanup of accidental mutation | |
// non-configurable properties won't be affected by delete | |
for (let sym of Object.getOwnPropertySymbols(Namespace)) { | |
delete Namespace[sym]; | |
} | |
for (let name in Object.getOwnPropertyNames(Namespace)) { | |
delete Namespace[name]; | |
} | |
Object.freeze(Namespace); | |
// imports | |
for (let RequestedModule of ModuleRecord.RequestedModules) { | |
if (isCJS(ModuleRecord.DependencyCache[RequestedModule])) { | |
for (let { | |
ModuleRequest, | |
LocalName, | |
ExportName | |
} of ModuleRecord.ImportEntries) { | |
if (ModuleRequest !== RequestedModule) continue; | |
if (ExportName !== 'default') { | |
throw Error(`unable to import ${ExportName} from ${ModuleRequest}`); | |
} | |
ModuleRecord.Imports[LocalName] = dependency; | |
} | |
} | |
else { | |
for (let { | |
ModuleRequest, | |
LocalName, | |
ExportName | |
} of ModuleRecord.ImportEntries) { | |
if (ModuleRequest !== RequestedModule) continue; | |
dependency.__esModule.GetExport( | |
ExportName, Getter => ModuleRecord.Imports[LocalName] = Getter | |
); | |
} | |
} | |
} | |
Object.freeze(ModuleRecord.Namespace); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment