Skip to content

Instantly share code, notes, and snippets.

@bmeck
Created January 11, 2017 14:51
Show Gist options
  • Save bmeck/49e67d1d8778fccbdd821eb2506e73dc to your computer and use it in GitHub Desktop.
Save bmeck/49e67d1d8778fccbdd821eb2506e73dc to your computer and use it in GitHub Desktop.
// // 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