Comparing version 1.1.0 to 1.1.1
type ModuleNamespace = Record<string, unknown>; | ||
export declare class Hot<Data extends Record<keyof any, unknown> = Record<keyof any, unknown>> { | ||
#private; | ||
/** | ||
@@ -8,3 +9,4 @@ * This is the `data` object passed to the `dispose` handler of the previous `Hot` instance. | ||
*/ | ||
readonly data?: Data; | ||
readonly data?: Data | undefined; | ||
constructor(module: unknown, instance: unknown, usesDynamicImport: boolean, data?: Data); | ||
/** | ||
@@ -46,4 +48,8 @@ * Accept updates for this module. When any unaccepted dependencies are updated this module will | ||
prune(onPrune: () => Promise<void> | void): void; | ||
/** | ||
* Listen for informative messages which are sent to `console`. | ||
*/ | ||
on(event: "message", callback: (message: string, ...params: unknown[]) => void): () => void; | ||
} | ||
export {}; | ||
//# sourceMappingURL=hot.d.ts.map |
@@ -6,2 +6,3 @@ import * as assert from "node:assert/strict"; | ||
import Fn from "dynohot/functional"; | ||
import { maybeThen } from "dynohot/runtime/utility"; | ||
import { transformModuleSource } from "./transform.js"; | ||
@@ -12,3 +13,3 @@ const self = new URL(import.meta.url); | ||
const root = String(new URL("..", self)); | ||
const runtimeURL = `${root}runtime/runtime.js`; | ||
const runtimeURL = `${root}runtime/runtime.js?${String(self.searchParams)}`; | ||
function extractImportAssertions(params) { | ||
@@ -63,24 +64,2 @@ const entries = Array.from(Fn.transform(Fn.filter(params, entry => entry[0] === "with"), entry => new URLSearchParams(entry[1]))); | ||
}; | ||
/** | ||
* Resolvers are ~allowed~ to return promises, but actually they shouldn't. nodejs accidentally | ||
* locked themselves into a promise-based API but then the `import.meta.resolve` specification | ||
* changed to be synchronous. So they work around it with some horrific `Atomics.wait` thing which | ||
* hard locks the thread until the promise resolves. | ||
* | ||
* This utility is used to detect a promise from an underlying loader and `then` it, or just execute | ||
* the callback synchronously. | ||
*/ | ||
function maybeThen(maybePromise, then) { | ||
// For the life of me, I can't figure out how to type this. `maybePromise` should probably also | ||
// include `& { then?: never }` and maybe the primitives, but that causes other problems. | ||
// @ts-expect-error -- I don't know how to type this | ||
if (typeof maybePromise.then === "function") { | ||
// @ts-expect-error -- I don't know how to type this | ||
return maybePromise.then(then); | ||
} | ||
else { | ||
// @ts-expect-error -- I don't know how to type this | ||
return then(maybePromise); | ||
} | ||
} | ||
function asString(sourceText) { | ||
@@ -104,5 +83,8 @@ if (sourceText instanceof Buffer) { | ||
if (context.parentURL === undefined) { | ||
return maybeThen(nextResolve(specifier, context), result => ({ | ||
url: `hot:main?url=${encodeURIComponent(result.url)}`, | ||
})); | ||
return maybeThen(function* () { | ||
const result = yield nextResolve(specifier, context); | ||
return { | ||
url: `hot:main?url=${encodeURIComponent(result.url)}`, | ||
}; | ||
}); | ||
} | ||
@@ -119,7 +101,8 @@ // [static imports] Convert "hot:module?specifier=..." to "hot:module?url=..." | ||
const importAssertions = extractImportAssertions(resolutionURL.searchParams); | ||
return maybeThen(nextResolve(resolutionSpecifier, { | ||
...context, | ||
parentURL: parentModuleURL, | ||
importAssertions, | ||
}), result => { | ||
return maybeThen(function* () { | ||
const result = yield nextResolve(resolutionSpecifier, { | ||
...context, | ||
parentURL: parentModuleURL, | ||
importAssertions, | ||
}); | ||
const params = new URLSearchParams([ | ||
@@ -144,7 +127,8 @@ ["url", result.url], | ||
const importAssertions = extractImportAssertions(resolutionURL.searchParams); | ||
return maybeThen(nextResolve(resolutionSpecifier, { | ||
...context, | ||
parentURL: parentModuleURL, | ||
importAssertions, | ||
}), result => { | ||
return maybeThen(function* () { | ||
const result = yield nextResolve(resolutionSpecifier, { | ||
...context, | ||
parentURL: parentModuleURL, | ||
importAssertions, | ||
}); | ||
const params = new URLSearchParams([ | ||
@@ -151,0 +135,0 @@ ["url", result.url], |
@@ -11,3 +11,2 @@ import Fn from "dynohot/functional"; | ||
url; | ||
linkIndex = 0; | ||
namespace; | ||
@@ -44,5 +43,2 @@ resolutions; | ||
} | ||
resolveExportInner(exportName) { | ||
return this.resolveExport(exportName); | ||
} | ||
select() { | ||
@@ -49,0 +45,0 @@ return this; |
@@ -0,2 +1,49 @@ | ||
/// <reference types="node" resolution-mode="require"/> | ||
import type { ModuleNamespace } from "./module.js"; | ||
import { EventEmitter } from "node:events"; | ||
export interface HotApplication { | ||
dynamicImport: DynamicImport; | ||
emitter: EventEmitter; | ||
log: (message: string, ...params: any[]) => void; | ||
requestUpdate: () => Promise<void>; | ||
requestUpdateResult: () => Promise<UpdateResult>; | ||
} | ||
type DynamicImport = (specifier: string, importAssertions?: Record<string, string>) => Promise<ModuleNamespace>; | ||
type UpdateResult = undefined | UpdateSuccess | UpdateDeclined | UpdateUnaccepted | UpdateEvaluationError | UpdateFatalError | UpdateLinkError; | ||
interface UpdateSuccess { | ||
type: UpdateStatus.success | UpdateStatus.unacceptedEvaluation; | ||
stats: () => UpdateStats; | ||
} | ||
interface UpdateDeclined { | ||
type: UpdateStatus.declined; | ||
declined: readonly ReloadableModuleController[]; | ||
} | ||
interface UpdateUnaccepted { | ||
type: UpdateStatus.unaccepted; | ||
chain: readonly InvalidationChain[]; | ||
} | ||
interface UpdateEvaluationError { | ||
type: UpdateStatus.evaluationFailure; | ||
error: unknown; | ||
stats: () => UpdateStats; | ||
} | ||
interface UpdateLinkError { | ||
type: UpdateStatus.linkError; | ||
error: unknown; | ||
} | ||
interface UpdateFatalError { | ||
type: UpdateStatus.fatalError; | ||
error: unknown; | ||
} | ||
interface UpdateStats { | ||
duration: number; | ||
loads: number; | ||
reevaluations: number; | ||
} | ||
interface InvalidationChain { | ||
modules: readonly ReloadableModuleController[]; | ||
/** `undefined` means it's the end of the chain, `null` means we've seen this branch before */ | ||
next: readonly InvalidationChain[] | undefined | null; | ||
} | ||
export {}; | ||
//# sourceMappingURL=controller.d.ts.map |
import * as assert from "node:assert/strict"; | ||
import { EventEmitter } from "node:events"; | ||
import { EOL } from "node:os"; | ||
import Fn from "dynohot/functional"; | ||
@@ -8,10 +10,19 @@ import { BindingType } from "./binding.js"; | ||
import { makeAcquireVisitIndex, makeTraversalState, traverseDepthFirst } from "./traverse.js"; | ||
import { debounceAsync, debounceTimer, discriminatedTypePredicate, evictModule, iterateWithRollback, makeRelative, plural } from "./utility.js"; | ||
import { debounceAsync, debounceTimer, discriminatedTypePredicate, evictModule, makeRelative, maybeAll, maybeThen, plural } from "./utility.js"; | ||
import { FileWatcher } from "./watcher.js"; | ||
/** @internal */ | ||
export function makeAcquire(dynamicImport) { | ||
export function makeAcquire(dynamicImport, params) { | ||
const emitter = new EventEmitter; | ||
const useLogs = params.silent === undefined; | ||
const application = { | ||
dynamicImport, | ||
emitter, | ||
requestUpdate: defaultRequestUpdate, | ||
requestUpdateResult: defaultRequestUpdateResult, | ||
log(message, ...params) { | ||
if (useLogs) { | ||
console.error(`[hot] ${message}`, ...params); | ||
} | ||
emitter.emit("message", message, ...params); | ||
}, | ||
}; | ||
@@ -28,3 +39,2 @@ const modules = new Map(); | ||
function defaultRequestUpdate() { | ||
console.error("[hot] Update requested before fully loaded."); | ||
return Promise.resolve(); | ||
@@ -47,3 +57,3 @@ } | ||
})(UpdateStatus || (UpdateStatus = {})); | ||
function logUpdate(update) { | ||
function logUpdate(app, update) { | ||
if (update === undefined) { | ||
@@ -54,6 +64,7 @@ return; | ||
case UpdateStatus.declined: | ||
console.error(`[hot] A pending update was explicitly declined:\n${update.declined.map(module => `- ${makeRelative(module.url)}`).join("\n")}`); | ||
app.log("A pending update was explicitly declined:" + | ||
update.declined.map(() => `${EOL}- %s`).join(), ...update.declined.map(declined => makeRelative(declined.url))); | ||
break; | ||
case UpdateStatus.fatalError: | ||
console.error("[hot] A fatal error was encountered. The application should be restarted!"); | ||
app.log("A fatal error was encountered. The application should be restarted!"); | ||
break; | ||
@@ -63,4 +74,4 @@ case UpdateStatus.evaluationFailure: { | ||
const ms = Math.round(duration); | ||
console.error(`[hot] Loaded ${loads} new ${plural("module", loads)}, reevaluated ${reevaluations} existing ${plural("module", reevaluations)} in ${ms}ms.`); | ||
console.error("[hot] Caught evaluation error:", update.error); | ||
app.log(`Loaded %d new ${plural("module", loads)}, reevaluated %d existing ${plural("module", reevaluations)} in %dms.`, loads, reevaluations, ms); | ||
app.log("Caught evaluation error:", update.error); | ||
break; | ||
@@ -70,8 +81,9 @@ } | ||
const { error } = update; | ||
console.error("[hot] Caught link error:"); | ||
if (error instanceof SyntaxError && "url" in error) { | ||
console.error(`${error.name}: ${error.message}\n at (${String(error.url)})`); | ||
app.log("Caught link error:" + EOL + | ||
"%s: %s" + EOL + | ||
" at (%s)", error.name, error.message, error.url); | ||
} | ||
else { | ||
console.error(error); | ||
app.log(`Caught link error:${EOL}%O`, error); | ||
} | ||
@@ -83,29 +95,9 @@ break; | ||
const ms = Math.round(duration); | ||
console.error(`[hot] Loaded ${loads} new ${plural("module", loads)}, reevaluated ${reevaluations} existing ${plural("module", reevaluations)} in ${ms}ms.`); | ||
app.log(`Loaded %d new ${plural("module", loads)}, reevaluated %d existing ${plural("module", reevaluations)} in %dms.`, loads, reevaluations, ms); | ||
break; | ||
} | ||
case UpdateStatus.unaccepted: { | ||
// Nice: | ||
// https://stackoverflow.com/questions/4965335/how-to-print-binary-tree-diagram-in-java/8948691#8948691 | ||
const logChain = (chain, prefix = "", childrenPrefix = "") => { | ||
const suffix = chain.next === undefined ? " 🔄" : ""; | ||
console.error(` ${prefix}[${chain.modules.map(module => makeRelative(module.url)).join(", ")}]${suffix}`); | ||
if (chain.next === null) { | ||
console.error(` ${childrenPrefix}[...]`); | ||
} | ||
else if (chain.next !== undefined) { | ||
for (const [ii, child] of chain.next.entries()) { | ||
if (ii === chain.next.length - 1) { | ||
logChain(child, `${childrenPrefix}└─ `, `${childrenPrefix} `); | ||
} | ||
else { | ||
logChain(child, `${childrenPrefix}├─ `, `${childrenPrefix}│ `); | ||
} | ||
} | ||
} | ||
}; | ||
console.error("[hot] A pending update was not accepted, and reached the root module:"); | ||
for (const chain of update.chain) { | ||
logChain(chain); | ||
} | ||
const messages = [...Fn.transform(update.chain, flattenInvalidationTree)]; | ||
app.log("A pending update was not accepted, and reached the root module:" + | ||
messages.map(([message]) => `${EOL}${message}`).join(""), ...Fn.transform(messages, ([, ...params]) => params)); | ||
break; | ||
@@ -116,4 +108,4 @@ } | ||
const ms = Math.round(duration); | ||
console.error(`[hot] Loaded ${loads} new ${plural("module", loads)}, reevaluated ${reevaluations} existing ${plural("module", reevaluations)} in ${ms}ms.`); | ||
console.error("[hot] Unaccepted update reached the root module. The application should be restarted!"); | ||
app.log(`Loaded %d new ${plural("module", loads)}, reevaluated %d existing ${plural("module", reevaluations)} in %dms.`, loads, reevaluations, ms); | ||
app.log("Unaccepted update reached the root module. The application should be restarted!"); | ||
break; | ||
@@ -123,2 +115,24 @@ } | ||
} | ||
// Nice: | ||
// https://stackoverflow.com/questions/4965335/how-to-print-binary-tree-diagram-in-java/8948691#8948691 | ||
function* flattenInvalidationTree(chain, prefix = "", childrenPrefix = "") { | ||
const suffix = chain.next === undefined ? " 🔄" : ""; | ||
yield [ | ||
` ${prefix}[${chain.modules.map(() => "%s").join(", ")}]${suffix}`, | ||
...chain.modules.map(module => makeRelative(module.url)), | ||
]; | ||
if (chain.next === null) { | ||
yield [` %s${childrenPrefix}[...]`]; | ||
} | ||
else if (chain.next !== undefined) { | ||
for (const [ii, child] of chain.next.entries()) { | ||
if (ii === chain.next.length - 1) { | ||
yield* flattenInvalidationTree(child, `${childrenPrefix}└─ `, `${childrenPrefix} `); | ||
} | ||
else { | ||
yield* flattenInvalidationTree(child, `${childrenPrefix}├─ `, `${childrenPrefix}│ `); | ||
} | ||
} | ||
} | ||
} | ||
const acquireVisitIndex = makeAcquireVisitIndex(); | ||
@@ -163,3 +177,3 @@ /** @internal */ | ||
catch (error) { | ||
console.log(error); | ||
this.application.log(`Error in module '%s':${EOL}%O`, this.url, error); | ||
return; | ||
@@ -176,3 +190,3 @@ } | ||
const update = await this.requestUpdate(); | ||
logUpdate(update); | ||
logUpdate(this.application, update); | ||
})); | ||
@@ -193,25 +207,25 @@ this.application.requestUpdateResult = () => this.requestUpdate(); | ||
return node.iterate(); | ||
}, async (cycleNodes) => { | ||
}, cycleNodes => maybeThen(function* () { | ||
// Instantiate the cycle (cannot fail) | ||
for (const node of cycleNodes) { | ||
const maybe = node.select().instantiate(); | ||
if (maybe) { | ||
await maybe; | ||
} | ||
yield node.select().instantiate(); | ||
} | ||
// Link the cycle | ||
const withRollback = iterateWithRollback(cycleNodes, nodes => { | ||
for (const node of nodes) { | ||
if (node.select().unlink()) { | ||
node.current = undefined; | ||
for (let ii = 0; ii < cycleNodes.length; ++ii) { | ||
const node = cycleNodes[ii]; | ||
try { | ||
yield node.select().link(); | ||
} | ||
catch (error) { | ||
// Unlink failed cycle nodes | ||
for (let jj = ii; jj < cycleNodes.length; ++jj) { | ||
if (node.select().unlink()) { | ||
node.current = undefined; | ||
} | ||
} | ||
throw error; | ||
} | ||
}); | ||
for (const node of withRollback) { | ||
const maybe = node.select().link(); | ||
if (maybe) { | ||
await maybe; | ||
} | ||
} | ||
}, pendingNodes => { | ||
return undefined; | ||
}), pendingNodes => { | ||
for (const node of pendingNodes) { | ||
@@ -227,14 +241,9 @@ if (node.select().unlink()) { | ||
return node.iterate(); | ||
}, async (cycleNodes) => { | ||
for (const node of cycleNodes) { | ||
const current = node.select(); | ||
if (current === node.staging) { | ||
node.staging = undefined; | ||
} | ||
const maybe = current.evaluate(); | ||
if (maybe) { | ||
await maybe; | ||
} | ||
}, cycleNodes => maybeAll(Fn.map(cycleNodes, node => { | ||
const current = node.select(); | ||
if (current === node.staging) { | ||
node.staging = undefined; | ||
} | ||
}); | ||
return current.evaluate(); | ||
}))); | ||
} | ||
@@ -451,3 +460,3 @@ } | ||
return node.iterateWithDynamics(controller => controller.pending, controller => controller.previous ?? controller.pending); | ||
}, async (cycleNodes, forwardResults) => { | ||
}, (cycleNodes, forwardResults) => maybeThen(function* () { | ||
let hasUpdate = Fn.some(forwardResults); | ||
@@ -467,6 +476,3 @@ if (!hasUpdate) { | ||
node.temporary = pending.clone(); | ||
const maybe = node.temporary.instantiate(); | ||
if (maybe) { | ||
await maybe; | ||
} | ||
yield node.temporary.instantiate(); | ||
instantiated.push(node); | ||
@@ -476,10 +482,7 @@ } | ||
const temporary = node.select(controller => controller.temporary); | ||
const maybe = temporary.link(controller => controller.temporary ?? controller.pending); | ||
if (maybe) { | ||
await maybe; | ||
} | ||
yield temporary.link(controller => controller.temporary ?? controller.pending); | ||
} | ||
} | ||
return hasUpdate; | ||
}); | ||
})); | ||
} | ||
@@ -538,3 +541,3 @@ catch (error) { | ||
const pending = node.select(controller => controller.pending); | ||
const data = await async function () { | ||
const data = await (async () => { | ||
try { | ||
@@ -544,8 +547,7 @@ return node.current === undefined ? undefined : await dispose(node.current); | ||
catch (error) { | ||
console.error(`[hot] Caught error in module '${node.url}' during dispose:`); | ||
console.error(error); | ||
this.application.log(`Caught error in module '%s' during dispose:${EOL}%O`, node.url, error); | ||
dispatchLinkErrorType = UpdateStatus.fatalError; | ||
throw error; | ||
} | ||
}(); | ||
})(); | ||
if (node.current === pending) { | ||
@@ -570,12 +572,3 @@ node.current = node.current.clone(); | ||
// 3) Evaluate | ||
const withRollback = iterateWithRollback(cycleNodes, nodes => { | ||
for (const node of nodes) { | ||
const current = node.select(); | ||
assert.ok(current.state.status === ModuleStatus.evaluated); | ||
if (current.state.evaluationError !== undefined) { | ||
node.current = node.previous; | ||
} | ||
} | ||
}); | ||
for (const node of withRollback) { | ||
const maybe = maybeAll(Fn.map(cycleNodes, node => maybeThen(function* () { | ||
const current = node.select(); | ||
@@ -588,6 +581,13 @@ if (node.previous !== undefined && current.declaration === node.previous.declaration) { | ||
} | ||
const maybe = current.evaluate(); | ||
if (maybe) { | ||
await maybe; | ||
try { | ||
yield current.evaluate(); | ||
} | ||
catch (error) { | ||
const current = node.select(); | ||
assert.ok(current.state.status === ModuleStatus.evaluated); | ||
if (current.state.evaluationError !== undefined) { | ||
node.current = node.previous; | ||
} | ||
throw error; | ||
} | ||
node.pending = undefined; | ||
@@ -597,2 +597,6 @@ if (current === node.staging) { | ||
} | ||
return undefined; | ||
}))); | ||
if (maybe) { | ||
await maybe; | ||
} | ||
@@ -656,4 +660,3 @@ // Try self-accept | ||
catch (error) { | ||
console.error(`[hot] Caught error in module '${controller.url}' during prune:`); | ||
console.error(error); | ||
this.application.log(`Caught error in module '%s' during prune:${EOL}%O`, controller.url, error); | ||
// eslint-disable-next-line no-unsafe-finally | ||
@@ -660,0 +663,0 @@ return this.fatalError = { type: UpdateStatus.fatalError, error }; |
@@ -47,4 +47,8 @@ type ModuleNamespace = Record<string, unknown>; | ||
prune(onPrune: () => Promise<void> | void): void; | ||
/** | ||
* Listen for informative messages which are sent to `console`. | ||
*/ | ||
on(event: "message", callback: (message: string, ...params: unknown[]) => void): () => void; | ||
} | ||
export {}; | ||
//# sourceMappingURL=hot.d.ts.map |
import * as assert from "node:assert/strict"; | ||
import { EOL } from "node:os"; | ||
import Fn from "dynohot/functional"; | ||
@@ -93,5 +94,5 @@ import { ReloadableModuleController } from "./controller.js"; | ||
})); | ||
console.error(`[hot] Warning: ${plural("specifier", specifiers.length)} ${specifiers.map(specifier => JSON.stringify(specifier)).join(", ")} from ${makeRelative(hot.#module.url)} could not be resolved.`); | ||
hot.#module.application.log(`Warning: ${plural("specifier", specifiers.length)} ${specifiers.map(() => "%s").join(", ")} from %s could not be resolved.`, ...specifiers.map(specifier => JSON.stringify(specifier)), makeRelative(hot.#module.url)); | ||
if (accepts.localEntries.length > 1) { | ||
console.error(`[hot] The entire accept group of: ${accepts.localEntries.map(entry => JSON.stringify(entry.specifier)).join(", ")} will be ignored.`); | ||
hot.#module.application.log(`The entire accept group of: ${accepts.localEntries.map(() => "%s").join(", ")} will be ignored.`, ...accepts.localEntries.map(entry => JSON.stringify(entry.specifier))); | ||
} | ||
@@ -154,4 +155,3 @@ } | ||
catch (error) { | ||
console.error(`[hot] Caught error in module '${hot.#module.url}' during accept of [${accepts.localEntries.map(entry => `'${entry.specifier}'`).join(", ")}]:`); | ||
console.error(error); | ||
hot.#module.application.log(`Caught error in module '%s' during accept of [${accepts.localEntries.map(() => "%s").join(", ")}]:${EOL}%O`, hot.#module.url, ...accepts.localEntries.map(entry => `'${entry.specifier}'`), error); | ||
return false; | ||
@@ -178,4 +178,3 @@ } | ||
catch (error) { | ||
console.error(`[hot] Caught error in module '${hot.#module.url}' during self-accept:`); | ||
console.error(error); | ||
hot.#module.application.log(`Caught error in module '%s' during self-accept:${EOL}%O`, hot.#module.url, error); | ||
return false; | ||
@@ -262,4 +261,10 @@ } | ||
} | ||
on(event, callback) { | ||
this.#module.application.emitter.addListener(event, callback); | ||
return () => { | ||
this.#module.application.emitter.removeListener(event, callback); | ||
}; | ||
} | ||
} | ||
Object.freeze(Hot.prototype); | ||
//# sourceMappingURL=hot.js.map |
@@ -8,5 +8,3 @@ import * as assert from "node:assert/strict"; | ||
import { ModuleStatus } from "./module.js"; | ||
import { makeAcquireVisitIndex } from "./traverse.js"; | ||
import { moduleNamespacePropertyDescriptor, withResolvers } from "./utility.js"; | ||
const acquireLinkIndex = makeAcquireVisitIndex(); | ||
/** @internal */ | ||
@@ -17,3 +15,2 @@ export class ReloadableModuleInstance { | ||
reloadable = true; | ||
linkIndex = 0; | ||
state = { status: ModuleStatus.new }; | ||
@@ -87,3 +84,3 @@ dynamicImports = []; | ||
module.state.status === ModuleStatus.evaluatingAsync); | ||
return module.resolveExport(exportName, select); | ||
return module.resolveExport(exportName, select, undefined); | ||
}); | ||
@@ -96,3 +93,2 @@ const imports = Object.fromEntries(bindings); | ||
environment: this.state.environment, | ||
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type | ||
completion: withResolvers(), | ||
@@ -127,3 +123,3 @@ }; | ||
module.state.status === ModuleStatus.evaluatingAsync); | ||
return module.resolveExport(exportName, select); | ||
return module.resolveExport(exportName, select, undefined); | ||
}); | ||
@@ -171,3 +167,3 @@ if (this.state.status !== ModuleStatus.linked) { | ||
assert.ok(next.done); | ||
completion.resolve(); | ||
completion.resolve(undefined); | ||
this.state = { | ||
@@ -198,3 +194,3 @@ status: ModuleStatus.evaluated, | ||
assert.ok(next.done); | ||
completion.resolve(); | ||
completion.resolve(undefined); | ||
this.state = { | ||
@@ -299,3 +295,3 @@ status: ModuleStatus.evaluated, | ||
if (entry.binding.type === BindingType.indirectExport) { | ||
const resolution = instance.resolveExport(entry.binding.name, select); | ||
const resolution = instance.resolveExport(entry.binding.name, select, undefined); | ||
assert.ok(resolution != null); | ||
@@ -329,12 +325,3 @@ resolutions.set(entry.binding.as ?? entry.binding.name, resolution); | ||
// 16.2.1.6.3 ResolveExport ( exportName [ , resolveSet ] ) | ||
resolveExport(exportName, select) { | ||
const [release, linkIndex] = acquireLinkIndex(); | ||
try { | ||
return this.resolveExportInner(exportName, select, linkIndex); | ||
} | ||
finally { | ||
release(); | ||
} | ||
} | ||
resolveExportInner(exportName, select, linkIndex) { | ||
resolveExport(exportName, select, resolveSet = new Map) { | ||
// 1. Assert: module.[[Status]] is not new. | ||
@@ -344,22 +331,17 @@ assert.ok(this.state.status !== ModuleStatus.new); | ||
// 3. For each Record { [[Module]], [[ExportName]] } r of resolveSet, do | ||
// TODO: Finish this. `resolveSet` is responsible for detecting unresolved circular | ||
// imports. For well-typed code it isn't a problem | ||
const resolveSet = new Map(); | ||
let imports = resolveSet.get(this); | ||
if (imports === undefined) { | ||
imports = new Set(); | ||
resolveSet.set(this, imports); | ||
const moduleResolveSet = resolveSet.get(this); | ||
if (moduleResolveSet) { | ||
if (moduleResolveSet.has(exportName)) { | ||
// a. If module and r.[[Module]] are the same Module Record and SameValue(exportName, r. | ||
// [[ExportName]]) is true, then | ||
// i. Assert: This is a circular import request. | ||
// ii. Return null. | ||
return null; | ||
} | ||
moduleResolveSet.add(exportName); | ||
} | ||
else if (imports.has(exportName)) { | ||
// a. If module and r.[[Module]] are the same Module Record and SameValue(exportName, r. | ||
// [[ExportName]]) is true, then | ||
// i. Assert: This is a circular import request. | ||
// ii. Return null. | ||
return null; | ||
else { | ||
// 4. Append the Record { [[Module]]: module, [[ExportName]]: exportName } to resolveSet. | ||
resolveSet.set(this, new Set([exportName])); | ||
} | ||
// 4. Append the Record { [[Module]]: module, [[ExportName]]: exportName } to resolveSet. | ||
if (this.linkIndex === linkIndex) { | ||
return null; | ||
} | ||
this.linkIndex = linkIndex; | ||
// 5. For each ExportEntry Record e of module.[[LocalExportEntries]], do | ||
@@ -391,3 +373,3 @@ const localExport = this.state.environment.exports[exportName]; | ||
// 2. Return importedModule.ResolveExport(e.[[ImportName]], resolveSet). | ||
return importedModule.resolveExportInner(indirectExport.binding.name, select, linkIndex); | ||
return importedModule.resolveExport(indirectExport.binding.name, select, resolveSet); | ||
} | ||
@@ -409,3 +391,3 @@ } | ||
// b. Let resolution be importedModule.ResolveExport(exportName, resolveSet). | ||
const resolution = importedModule.resolveExportInner(exportName, select, linkIndex); | ||
const resolution = importedModule.resolveExport(exportName, select, resolveSet); | ||
// c. If resolution is ambiguous, return ambiguous. | ||
@@ -412,0 +394,0 @@ if (resolution === undefined) { |
import { AdapterModuleController } from "./adapter.js"; | ||
import { makeAcquire } from "./controller.js"; | ||
const self = new URL(import.meta.url); | ||
const params = self.searchParams; | ||
/** @internal */ | ||
export const acquire = makeAcquire((specifier, assertions) => import(specifier, assertions)); | ||
export const acquire = makeAcquire((specifier, assertions) => import(specifier, assertions), Object.fromEntries(params)); | ||
/** @internal */ | ||
@@ -6,0 +8,0 @@ export function adapter(url, namespace) { |
@@ -11,4 +11,9 @@ import * as assert from "node:assert/strict"; | ||
lock = true; | ||
const release = () => { lock = false; }; | ||
return [release, ++currentVisitIndex]; | ||
const visitIndex = ++currentVisitIndex; | ||
const release = () => { | ||
assert.ok(lock); | ||
assert.equal(currentVisitIndex, visitIndex); | ||
lock = false; | ||
}; | ||
return [release, visitIndex]; | ||
}; | ||
@@ -84,3 +89,2 @@ }; | ||
const cycleNodes = stack.splice(stackIndex); | ||
cycleNodes.reverse(); | ||
// Collect forward results from cycle nodes | ||
@@ -87,0 +91,0 @@ let hasPromise = false; |
@@ -1,2 +0,11 @@ | ||
export {}; | ||
/** | ||
* Given an iterable of "maybe" promises this returns either `undefined` or a promise indicating | ||
* that all the yielded promises have resolved. | ||
*/ | ||
export declare function maybeAll(maybePromises: Iterable<MaybePromise<undefined>>): MaybePromise<undefined>; | ||
/** | ||
* Very simple version of something like `gensync`. For functions which might be async, but probably | ||
* aren't. | ||
*/ | ||
export declare function maybeThen<Return, Async extends NotPromiseLike>(task: () => Generator<MaybePromise<Async>, MaybePromise<Return>, Async>): MaybePromise<Return>; | ||
//# sourceMappingURL=utility.d.ts.map |
@@ -5,2 +5,3 @@ import * as assert from "node:assert"; | ||
import { fileURLToPath } from "node:url"; | ||
import Fn from "dynohot/functional"; | ||
/** @internal */ | ||
@@ -91,22 +92,38 @@ export const moduleNamespacePropertyDescriptor = { | ||
/** | ||
* Returns a delegate iterable to an array which invokes a rollback function on the iterated | ||
* elements if iteration didn't complete [due to an exception hopefully]. | ||
* @internal | ||
* Given an iterable of "maybe" promises this returns either `undefined` or a promise indicating | ||
* that all the yielded promises have resolved. | ||
*/ | ||
export function* iterateWithRollback(vector, rollback) { | ||
let ii = 0; | ||
try { | ||
for (; ii < vector.length; ++ii) { | ||
yield vector[ii]; | ||
} | ||
export function maybeAll(maybePromises) { | ||
const promises = Array.from(Fn.filter(maybePromises)); | ||
if (promises.length > 0) { | ||
return async function () { | ||
// Don't continue until all settled | ||
await Promise.allSettled(promises); | ||
// Throw on failure | ||
await Promise.all(promises); | ||
}(); | ||
} | ||
finally { | ||
if (ii !== vector.length) { | ||
rollback(function* () { | ||
for (let jj = ii; jj >= 0; --jj) { | ||
yield vector[jj]; | ||
} | ||
/** | ||
* Very simple version of something like `gensync`. For functions which might be async, but probably | ||
* aren't. | ||
*/ | ||
export function maybeThen(task) { | ||
const iterator = task(); | ||
let next = iterator.next(); | ||
while (!next.done) { | ||
if (next.value != null && | ||
typeof next.value.then === "function") { | ||
// We are now a promise | ||
return async function () { | ||
while (!next.done) { | ||
next = iterator.next(await next.value); | ||
} | ||
}()); | ||
return next.value; | ||
}(); | ||
} | ||
// Continue synchronously | ||
next = iterator.next(next.value); | ||
} | ||
return next.value; | ||
} | ||
@@ -113,0 +130,0 @@ const cwd = process.cwd(); |
{ | ||
"name": "dynohot", | ||
"type": "module", | ||
"version": "1.1.0", | ||
"version": "1.1.1", | ||
"exports": { | ||
".": "./dist/loader/loader.js", | ||
"./?": "./dist/loader/loader.js", | ||
"./?*": "./dist/loader/loader.js?*", | ||
"./register": "./dist/loader/register.js", | ||
"./register?": "./dist/loader/register.js", | ||
"./register?*": "./dist/loader/register.js?*", | ||
@@ -21,3 +23,3 @@ "./import-meta": { | ||
"prepare": "rm -rf dist && tsc -b", | ||
"test": "NODE_OPTIONS='--no-warnings --experimental-vm-modules' npx jest --silent" | ||
"test": "NODE_OPTIONS='--no-warnings --experimental-vm-modules' npx jest" | ||
}, | ||
@@ -24,0 +26,0 @@ "dependencies": { |
@@ -172,2 +172,7 @@ [![npm version](https://badgen.now.sh/npm/v/dynohot)](https://www.npmjs.com/package/dynohot) | ||
prune(onPrune: () => Promise<void> | void): void; | ||
/** | ||
* Listen for informative messages which are sent to `console`. | ||
*/ | ||
on(event: "message", callback: (message: string, ...params: unknown[]) => void): () => void; | ||
} | ||
@@ -284,2 +289,13 @@ ``` | ||
OPTIONS | ||
------- | ||
You can pass options to dynohot using `--import dynohot/register?option=value` or `--loader dynohot/?option=value`. | ||
* `ignore` - Pass `?ignore=regexpPattern` to explicitly ignore certain file paths. By default this is | ||
`ignore=[/\]node_modules[/\]`. | ||
* `silent` - Pass `?silent` to prevent logging messages to stderr console. You might want this if | ||
you're using something like Winston or Pino. Be sure to use `import.meta.on("message", ...)` to | ||
raise informative dynohot messages to the developer's attention. | ||
TRANSFORMATION | ||
@@ -286,0 +302,0 @@ -------------- |
139156
2884
366