Skip to content

Instantly share code, notes, and snippets.

@Jessidhia
Forked from bmeck/esm-example.js
Last active February 14, 2017 01:28
Show Gist options
  • Save Jessidhia/610ad7bd9231477744206a2a8db42abd to your computer and use it in GitHub Desktop.
Save Jessidhia/610ad7bd9231477744206a2a8db42abd to your computer and use it in GitHub Desktop.
// // 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