-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathloader.ts
284 lines (259 loc) · 8.53 KB
/
loader.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
import type { ModuleOrigin } from "./specifier.js";
import type { ImportAttributes, InitializeHook, LoadHook, ModuleFormat, ResolveHook } from "node:module";
import type { MessagePort } from "node:worker_threads";
import * as assert from "node:assert/strict";
import { Buffer } from "node:buffer";
import * as fs from "node:fs/promises";
import { Fn } from "@braidai/lang/functional";
import convertSourceMap from "convert-source-map";
import { LoaderHot } from "./loader-hot.js";
import { extractModuleOrigin, makeModuleOrigin } from "./specifier.js";
import { transformModuleSource } from "./transform.js";
/**
* This is the value of the `hot` attribute attached to `import` requests. Note that this payload
* only makes it through to the resolver, where it is processed and discarded. The `hot` attribute
* received in the loader is something different.
*
* This type is declared and exported since the controller runtime needs to make payloads of this
* format.
* @internal
*/
export type HotResolverPayload =
HotResolveExpressionDirective |
HotResolveReloadDirective;
interface HotResolveExpressionDirective {
hot: "expression";
parentURL: string;
}
interface HotResolveReloadDirective {
hot: "reload";
format: ModuleFormat;
version: number;
}
/** @internal */
export interface LoaderParameters {
ignore?: RegExp;
port: MessagePort;
silent?: boolean;
}
let ignorePattern: RegExp;
let port: MessagePort;
let runtimeURL: string;
/** @internal */
export const initialize: InitializeHook<LoaderParameters> = options => {
port = options.port;
ignorePattern = options.ignore ?? /[/\\]node_modules[/\\]/;
const root = String(new URL("..", new URL(import.meta.url)));
runtimeURL = `${root}runtime/runtime.js?${String(new URLSearchParams({
...options.silent ? { silent: "" } : {},
}))}`;
};
const makeAdapterModule = (origin: ModuleOrigin, importAttributes: ImportAttributes) => {
const encodedURL = JSON.stringify(origin.moduleURL);
return (
// eslint-disable-next-line @stylistic/indent
`import * as namespace from ${encodedURL} with ${JSON.stringify(importAttributes)};
import { adapter } from "hot:runtime";
const module = adapter(${encodedURL}, namespace);
export default function() { return module; };\n`
);
};
const makeJsonModule = (origin: ModuleOrigin, json: string, importAttributes: ImportAttributes) =>
// eslint-disable-next-line @stylistic/indent
`import { acquire } from "hot:runtime";
function* execute() {
yield [ () => {}, { default: () => json } ];
yield;
const json = JSON.parse(${JSON.stringify(json)});
}
export default function module() {
return acquire(${JSON.stringify(origin.moduleURL)});
}
module().load(${JSON.stringify(origin.backingModuleURL)}, { async: false, execute }, null, false, "json", ${JSON.stringify(importAttributes)}, []);\n`;
const makeReloadableModule = async (origin: ModuleOrigin, watch: readonly string[], source: string, importAttributes: ImportAttributes) => {
const sourceMap = await async function(): Promise<unknown> {
try {
const map = convertSourceMap.fromComment(source);
return map.toObject();
} catch {}
try {
const map = await convertSourceMap.fromMapFileSource(
source,
(fileName: string) => fs.readFile(new URL(fileName, origin.moduleURL), "utf8"));
return map?.toObject();
} catch {}
}();
// Loaders earlier in the chain are allowed to overwrite `responseURL`, which is fine, but we
// need to notate this in the runtime. `responseURL` can be anything, doesn't have to be unique,
// and is observable via `import.meta.url` and stack traces [unless there is a source map]. On
// the other hand, `moduleURL` uniquely identifies an instance of a module, and is used as the
// `parentURL` in the resolve callback. We will "burn in" `moduleURL` into the transformed
// source as a post-transformation process.
return (
// eslint-disable-next-line @stylistic/indent
`${transformModuleSource(origin.moduleURL, origin.backingModuleURL, importAttributes, source, sourceMap)}
import { acquire } from "hot:runtime";
export default function module() { return acquire(${JSON.stringify(origin.moduleURL)}, ${JSON.stringify(watch)}); }\n`
);
};
function asString(sourceText: any) {
if (sourceText instanceof Buffer) {
return sourceText.toString("utf8");
} else if (typeof sourceText === "string") {
return sourceText;
} else if (sourceText === undefined) {
return "";
} else {
return Buffer.from(sourceText).toString("utf8");
}
}
function extractResolverImportAttributes(importAttributes: ImportAttributes): ImportAttributes {
return Fn.pipe(
Object.entries(importAttributes),
$$ => Fn.reject($$, ([ key ]) => key === "hot"),
$$ => Object.fromEntries($$));
}
/** @internal */
export const resolve: ResolveHook = (specifier, context, nextResolve) => {
// Forward root module to "hot:main"
if (context.parentURL === undefined) {
return {
url: "hot:main",
format: "module",
importAttributes: {
hot: specifier,
},
shortCircuit: true,
};
}
// Pass through requests for the runtime
if (specifier === "hot:runtime") {
return {
format: "module",
shortCircuit: true,
url: runtimeURL,
};
}
// Bail on non-hot module resolution
const parentModuleOrigin = extractModuleOrigin(context.parentURL);
const hotParam = context.importAttributes.hot;
if (hotParam === undefined) {
return nextResolve(specifier, {
...context,
parentURL: parentModuleOrigin?.moduleURL ?? context.parentURL,
});
}
// Resolve hot module controller
return async function() {
const importAttributes = extractResolverImportAttributes(context.importAttributes);
// `import {} from "./specifier"`;
if (hotParam === "import") {
const next = await nextResolve(specifier, {
...context,
importAttributes,
parentURL: parentModuleOrigin?.moduleURL,
});
return {
...next,
url: makeModuleOrigin(next.url, next.importAttributes),
importAttributes: {
...importAttributes,
...next.importAttributes,
hot: next.format ?? "hot",
},
};
}
const hot = JSON.parse(hotParam) as HotResolverPayload;
switch (hot.hot) {
// `await import(url)`
case "expression": {
const parentModuleURL = hot.parentURL;
const next = await nextResolve(specifier, {
...context,
importAttributes,
parentURL: parentModuleURL,
});
return {
...next,
url: makeModuleOrigin(next.url, next.importAttributes),
importAttributes: {
...importAttributes,
...next.importAttributes,
hot: next.format ?? "hot",
},
};
}
// Reload, from `hot.invalidate()` (or the file watcher that invokes it)
case "reload": {
return {
format: hot.format,
importAttributes: {
...importAttributes,
hot: hot.format,
},
shortCircuit: true,
url: makeModuleOrigin(specifier, importAttributes, hot.version),
};
}
}
}();
};
/** @internal */
export const load: LoadHook = (urlString, context, nextLoad) => {
// Early bail on node_modules or CommonJS graph
const hotParam = context.importAttributes.hot;
if (hotParam === undefined) {
return nextLoad(urlString, context);
}
// Main entrypoint shim
if (urlString === "hot:main") {
// nb: `hotParam` is the specifier, as supplied on the nodejs command line
return {
format: "module",
shortCircuit: true,
source:
`import controller from ${JSON.stringify(hotParam)} with { hot: "import" };\n` +
"await controller().main();\n",
};
}
// nb: `hotParam` is the resolved format
return async function() {
// Request code from next loader in the chain
const origin = extractModuleOrigin(urlString);
assert.ok(origin);
const hot = new LoaderHot(urlString, port);
const importAttributes = extractResolverImportAttributes(context.importAttributes);
const result = await nextLoad(origin.moduleURL, {
...context,
format: function() {
switch (hotParam) {
case "commonjs":
case "module":
case "json":
return hotParam;
default:
return undefined;
}
}(),
importAttributes,
hot,
});
// Render hot module controller
if (!ignorePattern.test(urlString)) {
switch (result.format) {
case "json": {
const source = makeJsonModule(origin, asString(result.source), importAttributes);
return { format: "module", source };
}
case "module": {
const source = await makeReloadableModule(origin, hot.get(), asString(result.source), importAttributes);
return { format: "module", source };
}
default: break;
}
}
// Otherwise this is an non-hot adapter module
const source = makeAdapterModule(origin, importAttributes);
return { format: "module", source };
}();
};