@pitlane/assets 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/LICENSE.upstream +23 -0
- package/README.md +114 -2
- package/dist/build-BbA-Zq1m.mjs +161 -0
- package/dist/build.d.mts +21 -0
- package/dist/build.mjs +2 -0
- package/dist/index.d.mts +48 -0
- package/dist/index.mjs +146 -0
- package/dist/manifest.d.mts +18 -0
- package/dist/manifest.mjs +17 -0
- package/dist/specifier-DLC81sXY.mjs +47 -0
- package/dist/types-DlTsImzF.d.mts +106 -0
- package/dist/vite-plugin.d.mts +58 -0
- package/dist/vite-plugin.mjs +1411 -0
- package/package.json +71 -3
|
@@ -0,0 +1,1411 @@
|
|
|
1
|
+
import { n as quotedList, t as assetsSpecifier } from "./specifier-DLC81sXY.mjs";
|
|
2
|
+
import { t as createAssetManifest } from "./build-BbA-Zq1m.mjs";
|
|
3
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, posix, relative, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { createFilter, isCSSRequest, normalizePath, parseSync } from "vite";
|
|
7
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
8
|
+
import MagicString from "magic-string";
|
|
9
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
10
|
+
//#region src/vite/boundary-policy.ts
|
|
11
|
+
/**
|
|
12
|
+
* Builds the access check `remix/assets` applies to the files it serves.
|
|
13
|
+
* `root` is a real path; files passed to the check are real absolute paths.
|
|
14
|
+
*/
|
|
15
|
+
function createBrowserBoundary(root, options) {
|
|
16
|
+
let allowMatchers = options.allowFiles.map((pattern) => fileMatcher(root, pattern));
|
|
17
|
+
let denyMatchers = (options.denyFiles ?? []).map((pattern) => ({
|
|
18
|
+
pattern,
|
|
19
|
+
matches: fileMatcher(root, pattern)
|
|
20
|
+
}));
|
|
21
|
+
let packageRoots = allowedPackageRoots(root, (options.allowPackages ?? []).map((name) => name.trim()));
|
|
22
|
+
return (file) => {
|
|
23
|
+
if (file.includes("/node_modules/@oxc-project/runtime/")) return { allowed: true };
|
|
24
|
+
if (!(allowMatchers.some((matches) => matches(file)) || packageRoots.some((directory) => within(directory, file)))) return { allowed: false };
|
|
25
|
+
let denial = denyMatchers.find(({ matches }) => matches(file));
|
|
26
|
+
return denial ? {
|
|
27
|
+
allowed: false,
|
|
28
|
+
deniedBy: denial.pattern
|
|
29
|
+
} : { allowed: true };
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Whether `file` is `directory` or below it. */
|
|
33
|
+
function within(directory, file) {
|
|
34
|
+
return file === directory || file.startsWith(`${directory}/`);
|
|
35
|
+
}
|
|
36
|
+
function fileMatcher(root, pattern) {
|
|
37
|
+
let path = normalizePath(isAbsolute(pattern) ? pattern : join(root, pattern));
|
|
38
|
+
if (/[*?[\]{}()!+@]/.test(pattern)) return createFilter(path, null, { resolve: false });
|
|
39
|
+
if (!existsSync(path)) return (file) => file === path;
|
|
40
|
+
let real = normalizePath(realpathSync(path));
|
|
41
|
+
return statSync(real).isDirectory() ? (file) => within(real, file) : (file) => file === real;
|
|
42
|
+
}
|
|
43
|
+
/** Whether `name` is a bare or scoped package name, not a path, as `remix/assets` requires. */
|
|
44
|
+
function isPackageName(name) {
|
|
45
|
+
let scoped = name.startsWith("@");
|
|
46
|
+
let parts = (scoped ? name.slice(1) : name).split("/");
|
|
47
|
+
return parts.length === (scoped ? 2 : 1) && parts.every((part) => /^[\w.~-]+$/.test(part) && part !== "." && part !== "..");
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The real directories of each named package and of everything its
|
|
51
|
+
* `dependencies` and installed `optionalDependencies` pull in, transitively.
|
|
52
|
+
* Peer dependencies are left out, as `remix/assets` leaves them out.
|
|
53
|
+
*/
|
|
54
|
+
function allowedPackageRoots(root, names) {
|
|
55
|
+
let roots = /* @__PURE__ */ new Set();
|
|
56
|
+
let pending = names.map((name) => {
|
|
57
|
+
if (!isPackageName(name)) throw new Error(`[assets] allowPackages values must be package names. Received ${JSON.stringify(name)}.`);
|
|
58
|
+
let directory = packageDirectory(name, root);
|
|
59
|
+
if (!directory) throw new Error(`[assets] Could not resolve allowed package "${name}".`);
|
|
60
|
+
return directory;
|
|
61
|
+
});
|
|
62
|
+
while (pending.length > 0) {
|
|
63
|
+
let directory = pending.pop();
|
|
64
|
+
if (roots.has(directory)) continue;
|
|
65
|
+
roots.add(directory);
|
|
66
|
+
let manifest = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
|
|
67
|
+
for (let name of Object.keys(manifest.dependencies ?? {})) {
|
|
68
|
+
let dependency = packageDirectory(name, directory);
|
|
69
|
+
if (!dependency) throw new Error(`[assets] Could not resolve dependency "${name}" of allowed package ${directory}.`);
|
|
70
|
+
pending.push(dependency);
|
|
71
|
+
}
|
|
72
|
+
for (let name of Object.keys(manifest.optionalDependencies ?? {})) {
|
|
73
|
+
let dependency = packageDirectory(name, directory);
|
|
74
|
+
if (dependency) pending.push(dependency);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return [...roots];
|
|
78
|
+
}
|
|
79
|
+
/** The real directory Node's lookup finds for package `name` from `from`, or `undefined`. */
|
|
80
|
+
function packageDirectory(name, from) {
|
|
81
|
+
for (let directory = from;; directory = dirname(directory)) {
|
|
82
|
+
let candidate = join(directory, "node_modules", name);
|
|
83
|
+
if (existsSync(join(candidate, "package.json"))) return normalizePath(realpathSync(candidate));
|
|
84
|
+
if (dirname(directory) === directory) return void 0;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/vite/entries.ts
|
|
89
|
+
function sourceKey(root, id) {
|
|
90
|
+
let file = id.split("?")[0].split("#")[0];
|
|
91
|
+
if (file.startsWith("file://")) file = fileURLToPath(file);
|
|
92
|
+
return normalizePath(relative(root, file));
|
|
93
|
+
}
|
|
94
|
+
function inputPath(root, key) {
|
|
95
|
+
let path = key.split("#")[0];
|
|
96
|
+
if (path.startsWith("file://")) return fileURLToPath(path);
|
|
97
|
+
if (path.startsWith("file:")) path = path.slice(5);
|
|
98
|
+
return resolve(root, path.replace(/^\/+/, ""));
|
|
99
|
+
}
|
|
100
|
+
function fileModule(id) {
|
|
101
|
+
return !id.startsWith("\0") && isAbsolute(id.split("?")[0]);
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/vite/folded-files.ts
|
|
105
|
+
/** `context`, with `addWatchFile` reporting each file to `observe` before Vite records it. */
|
|
106
|
+
function watching(context, observe) {
|
|
107
|
+
return new Proxy(context, { get(target, property) {
|
|
108
|
+
if (property === "addWatchFile") return (file) => {
|
|
109
|
+
observe(file);
|
|
110
|
+
target.addWatchFile(file);
|
|
111
|
+
};
|
|
112
|
+
let value = Reflect.get(target, property, target);
|
|
113
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
114
|
+
} });
|
|
115
|
+
}
|
|
116
|
+
function unsupported(name) {
|
|
117
|
+
return /* @__PURE__ */ new Error(`[assets] This Vite version does not expose the ${name} hook the browser boundary reads. Use a supported Vite version.`);
|
|
118
|
+
}
|
|
119
|
+
/** Calls `observe` with every file the folding plugins report, by wrapping their hooks' contexts. */
|
|
120
|
+
function observeFoldedFiles(config, observe) {
|
|
121
|
+
let plugin = (name) => config.plugins.find((candidate) => candidate.name === name);
|
|
122
|
+
for (let name of ["vite:css", "vite:worker-import-meta-url"]) {
|
|
123
|
+
let hook = plugin(name)?.transform;
|
|
124
|
+
if (typeof hook !== "object") throw unsupported(name);
|
|
125
|
+
let handler = hook.handler;
|
|
126
|
+
hook.handler = function(code, id, options) {
|
|
127
|
+
let context = watching(this, (file) => observe(this.environment, name, id, file));
|
|
128
|
+
return handler.call(context, code, id, options);
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
let hook = plugin("vite:worker")?.load;
|
|
132
|
+
if (typeof hook !== "object") throw unsupported("vite:worker");
|
|
133
|
+
let handler = hook.handler;
|
|
134
|
+
hook.handler = function(id, options) {
|
|
135
|
+
let context = watching(this, (file) => observe(this.environment, "vite:worker", id, file));
|
|
136
|
+
return handler.call(context, id, options);
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region src/vite/boundary.ts
|
|
141
|
+
function describe({ file, importer, deniedBy }) {
|
|
142
|
+
let reason = deniedBy ? `matches denyFiles ${JSON.stringify(deniedBy)}` : "not in allowFiles or allowPackages";
|
|
143
|
+
return `${file}${importer ? ` (imported by ${importer})` : ""}: ${reason}`;
|
|
144
|
+
}
|
|
145
|
+
/** Query-free path of a module id or emitted file name. */
|
|
146
|
+
function filePath(id) {
|
|
147
|
+
return normalizePath(id.split(/[?#]/, 1)[0]);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* A page, or an inline script Vite proxies out of one: the boundary governs
|
|
151
|
+
* what a page loads, not the page. An HTML file a module imports is data.
|
|
152
|
+
*/
|
|
153
|
+
function htmlDocument(id) {
|
|
154
|
+
return filePath(id).endsWith(".html") && (!id.includes("?") || /[?&]html-proxy\b/.test(id));
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The server files a build copies into the client output: every stylesheet
|
|
158
|
+
* the server graph imports, except as a string or URL, and the assets its
|
|
159
|
+
* chunks link.
|
|
160
|
+
*/
|
|
161
|
+
function publishedServerFiles(bundle) {
|
|
162
|
+
let files = [];
|
|
163
|
+
for (let output of Object.values(bundle)) {
|
|
164
|
+
if (output.type !== "chunk" || !output.viteMetadata) continue;
|
|
165
|
+
for (let id of output.moduleIds) if (isCSSRequest(id) && !/[?&](?:inline|raw|url)\b/.test(id)) files.push(id);
|
|
166
|
+
for (let fileName of output.viteMetadata.importedAssets) {
|
|
167
|
+
let asset = bundle[fileName];
|
|
168
|
+
if (asset?.type === "asset") files.push(...asset.originalFileNames);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return files;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Holds the client graph and every file the build publishes for the browser
|
|
175
|
+
* to `allowFiles`, `allowPackages`, and `denyFiles`, the way `remix/assets`
|
|
176
|
+
* holds the files its asset server answers.
|
|
177
|
+
*/
|
|
178
|
+
function browserBoundary(options, serverEnvironments) {
|
|
179
|
+
let root = "";
|
|
180
|
+
let inspect;
|
|
181
|
+
let viteClient = normalizePath(dirname(fileURLToPath(import.meta.resolve("vite/dist/client/client.mjs"))));
|
|
182
|
+
/** Build module id → the files a folding plugin read into it. */
|
|
183
|
+
let foldedFiles = /* @__PURE__ */ new Map();
|
|
184
|
+
function refusal(file, importer) {
|
|
185
|
+
let verdict = inspect(file);
|
|
186
|
+
if (verdict.allowed) return;
|
|
187
|
+
return {
|
|
188
|
+
file: sourceKey(root, file),
|
|
189
|
+
importer: importer && sourceKey(root, importer),
|
|
190
|
+
deniedBy: verdict.deniedBy
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/** The real path of the file `path` names from the root, or `undefined` when none exists. */
|
|
194
|
+
function existingFile(path) {
|
|
195
|
+
let file = resolve(root, filePath(path));
|
|
196
|
+
if (!existsSync(file) || !statSync(file).isFile()) return;
|
|
197
|
+
return normalizePath(realpathSync(file));
|
|
198
|
+
}
|
|
199
|
+
/** The source file a dev request reaches, or `undefined` when Vite itself supplies it. */
|
|
200
|
+
function servedSource(environment, file) {
|
|
201
|
+
if (within(viteClient, file)) return;
|
|
202
|
+
let optimizer = environment.depsOptimizer;
|
|
203
|
+
if (!optimizer?.isOptimizedDepFile(file)) return file;
|
|
204
|
+
let { optimized, discovered } = optimizer.metadata;
|
|
205
|
+
let dependency = [...Object.values(optimized), ...Object.values(discovered)].find((info) => normalizePath(info.file) === file);
|
|
206
|
+
return dependency?.src && normalizePath(dependency.src);
|
|
207
|
+
}
|
|
208
|
+
function devRefusal(environment, file, importer) {
|
|
209
|
+
let source = servedSource(environment, file);
|
|
210
|
+
if (!source) return;
|
|
211
|
+
importer ??= [...environment.moduleGraph.getModulesByFile(file) ?? []].flatMap((module) => [...module.importers]).find((module) => module.file)?.file ?? void 0;
|
|
212
|
+
return refusal(source, importer);
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
name: "pitlane-assets-boundary",
|
|
216
|
+
sharedDuringBuild: true,
|
|
217
|
+
configResolved(config) {
|
|
218
|
+
root = config.root;
|
|
219
|
+
inspect = createBrowserBoundary(normalizePath(realpathSync(root)), options);
|
|
220
|
+
observeFoldedFiles(config, (environment, plugin, module, file) => {
|
|
221
|
+
if (plugin === "vite:css" && !isCSSRequest(file) && !config.assetsInclude(file)) return;
|
|
222
|
+
if (environment.mode === "build") {
|
|
223
|
+
let files = foldedFiles.get(module);
|
|
224
|
+
if (!files) foldedFiles.set(module, files = /* @__PURE__ */ new Set());
|
|
225
|
+
files.add(file);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (environment.name !== "client" || environment.mode !== "dev") return;
|
|
229
|
+
let source = existingFile(file);
|
|
230
|
+
let refused = source && devRefusal(environment, source, module);
|
|
231
|
+
if (refused) throw new Error(`[assets] Outside the browser boundary: ${describe(refused)}`);
|
|
232
|
+
});
|
|
233
|
+
},
|
|
234
|
+
configureServer(server) {
|
|
235
|
+
let client = server.environments.client;
|
|
236
|
+
let { publicDir, base } = server.config;
|
|
237
|
+
server.middlewares.use(((request, _response, next) => {
|
|
238
|
+
let path;
|
|
239
|
+
try {
|
|
240
|
+
path = decodeURIComponent(filePath(request.url ?? "/"));
|
|
241
|
+
} catch {
|
|
242
|
+
return next();
|
|
243
|
+
}
|
|
244
|
+
if (base !== "/" && path.startsWith(base)) path = `/${path.slice(base.length)}`;
|
|
245
|
+
if (path.startsWith("/@") && !path.startsWith("/@fs/")) return next();
|
|
246
|
+
if (publicDir && existsSync(join(publicDir, path))) return next();
|
|
247
|
+
if (htmlDocument(path)) return next();
|
|
248
|
+
let requested = join(root, path);
|
|
249
|
+
if (path.startsWith("/@fs/")) {
|
|
250
|
+
let fsPath = path.slice(5);
|
|
251
|
+
requested = /^[A-Za-z]:\//.test(fsPath) ? fsPath : `/${fsPath}`;
|
|
252
|
+
}
|
|
253
|
+
let file = existingFile(requested);
|
|
254
|
+
let refused = file && devRefusal(client, file);
|
|
255
|
+
if (!refused) return next();
|
|
256
|
+
next(/* @__PURE__ */ new Error(`[assets] Outside the browser boundary: ${describe(refused)}`));
|
|
257
|
+
}));
|
|
258
|
+
},
|
|
259
|
+
transform: {
|
|
260
|
+
order: "pre",
|
|
261
|
+
handler(_code, id) {
|
|
262
|
+
let environment = this.environment;
|
|
263
|
+
if (environment.mode !== "dev" || environment.name !== "client" || !fileModule(id)) return;
|
|
264
|
+
if (htmlDocument(id)) return;
|
|
265
|
+
let refused = devRefusal(environment, existingFile(id) ?? filePath(id));
|
|
266
|
+
if (refused) throw new Error(`[assets] Outside the browser boundary: ${describe(refused)}`);
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
generateBundle(_options, bundle) {
|
|
270
|
+
let name = this.environment.name;
|
|
271
|
+
let published = [];
|
|
272
|
+
if (name === "client") {
|
|
273
|
+
for (let id of this.getModuleIds()) {
|
|
274
|
+
if (!fileModule(id)) continue;
|
|
275
|
+
let info = this.getModuleInfo(id);
|
|
276
|
+
let importer = [...info?.importers ?? [], ...info?.dynamicImporters ?? []].find(fileModule);
|
|
277
|
+
published.push({
|
|
278
|
+
path: id,
|
|
279
|
+
importer
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
for (let output of Object.values(bundle)) {
|
|
283
|
+
if (output.type !== "asset" || output.names.some(isCSSRequest)) continue;
|
|
284
|
+
for (let path of output.originalFileNames) published.push({ path });
|
|
285
|
+
}
|
|
286
|
+
} else if (serverEnvironments.includes(name)) for (let path of publishedServerFiles(bundle)) {
|
|
287
|
+
let importer = this.getModuleInfo(path)?.importers.find(fileModule);
|
|
288
|
+
published.push({
|
|
289
|
+
path,
|
|
290
|
+
importer
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
let refusals = /* @__PURE__ */ new Map();
|
|
294
|
+
for (let { path, importer } of published) {
|
|
295
|
+
let entries = [{
|
|
296
|
+
path,
|
|
297
|
+
importer
|
|
298
|
+
}, ...[...foldedFiles.get(path) ?? []].map((file) => ({
|
|
299
|
+
path: file,
|
|
300
|
+
importer: path
|
|
301
|
+
}))];
|
|
302
|
+
for (let entry of entries) {
|
|
303
|
+
let file = !htmlDocument(entry.path) && existingFile(entry.path);
|
|
304
|
+
let refused = file && refusal(file, entry.importer);
|
|
305
|
+
if (refused && !refusals.get(refused.file)?.importer) refusals.set(refused.file, refused);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (refusals.size === 0) return;
|
|
309
|
+
let lines = [...refusals.values()].map(describe);
|
|
310
|
+
this.error(`[assets] Outside the browser boundary:\n ${lines.join("\n ")}`);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
//#endregion
|
|
315
|
+
//#region src/vite/output.ts
|
|
316
|
+
function captureOutput(bundle, config, role, assetFiles, entryFiles) {
|
|
317
|
+
let graph = {
|
|
318
|
+
role,
|
|
319
|
+
chunks: Object.create(null),
|
|
320
|
+
entries: Object.create(null),
|
|
321
|
+
assets: Object.create(null)
|
|
322
|
+
};
|
|
323
|
+
let globalStylesheets = config.build.cssCodeSplit ? [] : Object.values(bundle).flatMap((output) => output.type === "asset" && output.originalFileNames.includes("style.css") ? [output.fileName] : []);
|
|
324
|
+
let linked = role === "server" ? linkedFiles(bundle, globalStylesheets) : void 0;
|
|
325
|
+
for (let output of Object.values(bundle)) {
|
|
326
|
+
if (output.type === "asset") {
|
|
327
|
+
if (linked && !linked.has(output.fileName)) continue;
|
|
328
|
+
let stylesheet = output.names.some(isCSSRequest);
|
|
329
|
+
for (let file of output.originalFileNames) {
|
|
330
|
+
if (stylesheet && !isCSSRequest(file)) continue;
|
|
331
|
+
let key = fileModule(file) ? sourceKey(config.root, file) : file.replace(/^\.\//, "");
|
|
332
|
+
graph.assets[key] = output.fileName;
|
|
333
|
+
}
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
graph.chunks[output.fileName] = {
|
|
337
|
+
file: output.fileName,
|
|
338
|
+
modules: [...new Set(output.moduleIds.filter(fileModule).map((id) => sourceKey(config.root, id)))],
|
|
339
|
+
imports: output.imports.filter((id) => bundle[id]?.type === "chunk"),
|
|
340
|
+
dynamicImports: output.dynamicImports.filter((id) => bundle[id]?.type === "chunk"),
|
|
341
|
+
stylesheets: [...globalStylesheets, ...output.viteMetadata?.importedCss ?? []]
|
|
342
|
+
};
|
|
343
|
+
if (role === "client" && output.isEntry && output.facadeModuleId && fileModule(output.facadeModuleId)) graph.entries[sourceKey(config.root, output.facadeModuleId)] = output.fileName;
|
|
344
|
+
}
|
|
345
|
+
for (let [key, file] of assetFiles) graph.assets[key] = file;
|
|
346
|
+
for (let [key, file] of entryFiles) if (bundle[file]?.type === "chunk") graph.entries[key] = file;
|
|
347
|
+
return graph;
|
|
348
|
+
}
|
|
349
|
+
function linkedFiles(bundle, globalStylesheets) {
|
|
350
|
+
let files = new Set(globalStylesheets);
|
|
351
|
+
for (let output of Object.values(bundle)) {
|
|
352
|
+
if (output.type !== "chunk" || !output.viteMetadata) continue;
|
|
353
|
+
for (let file of output.viteMetadata.importedCss) files.add(file);
|
|
354
|
+
for (let file of output.viteMetadata.importedAssets) files.add(file);
|
|
355
|
+
}
|
|
356
|
+
return files;
|
|
357
|
+
}
|
|
358
|
+
function captureImportMap(bundle, fileName) {
|
|
359
|
+
let output = bundle[fileName];
|
|
360
|
+
if (output?.type !== "asset") throw new Error(`[assets] The client build did not emit its configured import map "${fileName}".`);
|
|
361
|
+
let text = typeof output.source === "string" ? output.source : new TextDecoder().decode(output.source);
|
|
362
|
+
return JSON.parse(text);
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* The public URL `renderBuiltUrl` chooses for a built file. A server document
|
|
366
|
+
* has no importer to be relative to, so `{ relative: true }` keeps the
|
|
367
|
+
* base-joined path.
|
|
368
|
+
*/
|
|
369
|
+
function publicFile(config, file) {
|
|
370
|
+
let render = config.experimental.renderBuiltUrl;
|
|
371
|
+
if (!render) return file;
|
|
372
|
+
let rendered = render(file, {
|
|
373
|
+
type: "asset",
|
|
374
|
+
hostType: "html",
|
|
375
|
+
hostId: "",
|
|
376
|
+
ssr: true
|
|
377
|
+
});
|
|
378
|
+
if (typeof rendered === "string") return rendered;
|
|
379
|
+
if (rendered && "runtime" in rendered && rendered.runtime) throw new Error("[assets] renderBuiltUrl must return a URL for server HTML; runtime JavaScript cannot be serialized in an asset manifest.");
|
|
380
|
+
return file;
|
|
381
|
+
}
|
|
382
|
+
//#endregion
|
|
383
|
+
//#region src/vite/binding-names.ts
|
|
384
|
+
/** Calls `visit` on every AST node below `root`, in no particular order. */
|
|
385
|
+
function forEachNode(root, visit) {
|
|
386
|
+
let pending = [root];
|
|
387
|
+
while (pending.length) {
|
|
388
|
+
let node = pending.pop();
|
|
389
|
+
if (!node || typeof node !== "object") continue;
|
|
390
|
+
if (Array.isArray(node)) {
|
|
391
|
+
pending.push(...node);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
visit(node);
|
|
395
|
+
for (let child of Object.values(node)) if (child && typeof child === "object") pending.push(child);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function patternNames(pattern) {
|
|
399
|
+
switch (pattern?.type) {
|
|
400
|
+
case "Identifier": return [pattern.name];
|
|
401
|
+
case "AssignmentPattern": return patternNames(pattern.left);
|
|
402
|
+
case "RestElement": return patternNames(pattern.argument);
|
|
403
|
+
case "TSParameterProperty": return patternNames(pattern.parameter);
|
|
404
|
+
case "ArrayPattern": return pattern.elements.flatMap(patternNames);
|
|
405
|
+
case "ObjectPattern": return pattern.properties.flatMap((property) => patternNames(property.type === "RestElement" ? property : property.value));
|
|
406
|
+
default: return [];
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/** The names a declaration binds in the scope that contains it. */
|
|
410
|
+
function declaredNames(node) {
|
|
411
|
+
switch (node?.type) {
|
|
412
|
+
case "VariableDeclaration": return node.declarations.flatMap((declarator) => patternNames(declarator.id));
|
|
413
|
+
case "FunctionDeclaration":
|
|
414
|
+
case "TSDeclareFunction":
|
|
415
|
+
case "ClassDeclaration":
|
|
416
|
+
case "TSEnumDeclaration":
|
|
417
|
+
case "TSImportEqualsDeclaration": return node.id ? [node.id.name] : [];
|
|
418
|
+
case "TSModuleDeclaration": return node.id.type === "Identifier" ? [node.id.name] : [];
|
|
419
|
+
default: return [];
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function boundNames(node) {
|
|
423
|
+
switch (node.type) {
|
|
424
|
+
case "VariableDeclaration": return declaredNames(node);
|
|
425
|
+
case "FunctionDeclaration":
|
|
426
|
+
case "FunctionExpression":
|
|
427
|
+
case "ArrowFunctionExpression": {
|
|
428
|
+
let parameters = node.params.flatMap(patternNames);
|
|
429
|
+
return node.type !== "ArrowFunctionExpression" && node.id ? [node.id.name, ...parameters] : parameters;
|
|
430
|
+
}
|
|
431
|
+
case "ClassExpression": return node.id ? [node.id.name] : [];
|
|
432
|
+
case "CatchClause": return patternNames(node.param);
|
|
433
|
+
case "ImportSpecifier":
|
|
434
|
+
case "ImportDefaultSpecifier":
|
|
435
|
+
case "ImportNamespaceSpecifier": return [node.local.name];
|
|
436
|
+
default: return declaredNames(node);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
/** How many times a module binds each name, counting every declaration in every scope. */
|
|
440
|
+
function bindingCounts(program) {
|
|
441
|
+
let counts = /* @__PURE__ */ new Map();
|
|
442
|
+
forEachNode(program, (node) => {
|
|
443
|
+
for (let name of boundNames(node)) counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
444
|
+
});
|
|
445
|
+
return counts;
|
|
446
|
+
}
|
|
447
|
+
//#endregion
|
|
448
|
+
//#region src/vite/resolver-bindings.ts
|
|
449
|
+
function moduleExportName(node) {
|
|
450
|
+
return node.type === "Literal" ? node.value : node.name;
|
|
451
|
+
}
|
|
452
|
+
function constructs(node, constructors) {
|
|
453
|
+
while (node?.type === "AwaitExpression" || node?.type === "ParenthesizedExpression") node = node.type === "AwaitExpression" ? node.argument : node.expression;
|
|
454
|
+
return node?.type === "CallExpression" && node.callee.type === "Identifier" && constructors.has(node.callee.name);
|
|
455
|
+
}
|
|
456
|
+
/** The name a chain of top-level `const a = b` copies starts from. */
|
|
457
|
+
function aliasRoot(name, initializers) {
|
|
458
|
+
let seen = /* @__PURE__ */ new Set();
|
|
459
|
+
let init = initializers.get(name);
|
|
460
|
+
while (init?.type === "Identifier" && !seen.has(name)) {
|
|
461
|
+
seen.add(name);
|
|
462
|
+
name = init.name;
|
|
463
|
+
init = initializers.get(name);
|
|
464
|
+
}
|
|
465
|
+
return name;
|
|
466
|
+
}
|
|
467
|
+
function topLevelBindings(program) {
|
|
468
|
+
let counts = bindingCounts(program);
|
|
469
|
+
let importedConstructors = /* @__PURE__ */ new Set();
|
|
470
|
+
let imports = /* @__PURE__ */ new Map();
|
|
471
|
+
let initializers = /* @__PURE__ */ new Map();
|
|
472
|
+
for (let statement of program.body) {
|
|
473
|
+
if (statement.type === "ImportDeclaration") {
|
|
474
|
+
let module = statement.source.value;
|
|
475
|
+
for (let specifier of statement.specifiers) {
|
|
476
|
+
let local = specifier.local.name;
|
|
477
|
+
if (specifier.type === "ImportNamespaceSpecifier" || counts.get(local) !== 1) continue;
|
|
478
|
+
let name = specifier.type === "ImportDefaultSpecifier" ? "default" : moduleExportName(specifier.imported);
|
|
479
|
+
if (name === "createAssetResolver" && (module === "@pitlane/assets" || module === "pitlane/assets")) importedConstructors.add(local);
|
|
480
|
+
else imports.set(local, {
|
|
481
|
+
module,
|
|
482
|
+
name
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
let declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
|
|
487
|
+
if (declaration?.type !== "VariableDeclaration") continue;
|
|
488
|
+
for (let { id, init } of declaration.declarations) if (id.type === "Identifier" && init && counts.get(id.name) === 1) initializers.set(id.name, init);
|
|
489
|
+
}
|
|
490
|
+
let constructors = new Set(importedConstructors);
|
|
491
|
+
for (let name of initializers.keys()) if (importedConstructors.has(aliasRoot(name, initializers))) constructors.add(name);
|
|
492
|
+
let resolvers = /* @__PURE__ */ new Map();
|
|
493
|
+
for (let name of [...imports.keys(), ...initializers.keys()]) {
|
|
494
|
+
let root = aliasRoot(name, initializers);
|
|
495
|
+
let origin = imports.get(root) ?? (constructs(initializers.get(root), constructors) ? "local" : void 0);
|
|
496
|
+
if (origin) resolvers.set(name, origin);
|
|
497
|
+
}
|
|
498
|
+
return {
|
|
499
|
+
constructors,
|
|
500
|
+
resolvers
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
/** Where the resolver an expression evaluates to comes from, when the expression can name one. */
|
|
504
|
+
function resolverOrigin(node, bindings) {
|
|
505
|
+
if (constructs(node, bindings.constructors)) return "local";
|
|
506
|
+
if (node.type === "Identifier") return bindings.resolvers.get(node.name);
|
|
507
|
+
}
|
|
508
|
+
//#endregion
|
|
509
|
+
//#region src/vite/resolver-usage.ts
|
|
510
|
+
const RELEVANT = /\bget(?:ScriptEntry|Href)\b|\bcreateAssetResolver\b|\bexport\b/;
|
|
511
|
+
/** The method a member call names, through `.name` or a string-literal `["name"]`. */
|
|
512
|
+
function memberName(callee) {
|
|
513
|
+
let { property } = callee;
|
|
514
|
+
if (!callee.computed) return property.type === "Identifier" ? property.name : void 0;
|
|
515
|
+
return property.type === "Literal" && typeof property.value === "string" ? property.value : void 0;
|
|
516
|
+
}
|
|
517
|
+
function literalCall(call, bindings) {
|
|
518
|
+
let { callee, arguments: args } = call;
|
|
519
|
+
if (callee.type !== "MemberExpression") return;
|
|
520
|
+
let method = memberName(callee);
|
|
521
|
+
let argument = args[0];
|
|
522
|
+
if (method !== "getScriptEntry" && method !== "getHref") return;
|
|
523
|
+
if (argument?.type !== "Literal" || typeof argument.value !== "string") return;
|
|
524
|
+
let receiver = resolverOrigin(callee.object, bindings);
|
|
525
|
+
if (!receiver) return;
|
|
526
|
+
return {
|
|
527
|
+
input: {
|
|
528
|
+
key: argument.value,
|
|
529
|
+
kind: method === "getScriptEntry" ? "script" : "asset"
|
|
530
|
+
},
|
|
531
|
+
receiver
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
function resolverExports(program, bindings) {
|
|
535
|
+
let exported = /* @__PURE__ */ new Map();
|
|
536
|
+
for (let statement of program.body) {
|
|
537
|
+
if (statement.type === "ExportDefaultDeclaration") exported.set("default", resolverOrigin(statement.declaration, bindings) ?? null);
|
|
538
|
+
if (statement.type === "ExportAllDeclaration" && statement.exported) exported.set(moduleExportName(statement.exported), null);
|
|
539
|
+
if (statement.type !== "ExportNamedDeclaration") continue;
|
|
540
|
+
for (let name of declaredNames(statement.declaration)) exported.set(name, bindings.resolvers.get(name) ?? null);
|
|
541
|
+
for (let specifier of statement.specifiers) {
|
|
542
|
+
let found = statement.source ? {
|
|
543
|
+
module: statement.source.value,
|
|
544
|
+
name: moduleExportName(specifier.local)
|
|
545
|
+
} : resolverOrigin(specifier.local, bindings);
|
|
546
|
+
exported.set(moduleExportName(specifier.exported), found ?? null);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return exported;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* A module's literal `getScriptEntry` and `getHref` calls whose receiver may be
|
|
553
|
+
* a resolver, and its exports that may name one. Import specifiers stay
|
|
554
|
+
* unresolved; pass the result to {@link linkResolverUsage}.
|
|
555
|
+
*/
|
|
556
|
+
function scanResolverUsage(code, id) {
|
|
557
|
+
if (!RELEVANT.test(code)) return;
|
|
558
|
+
let { program } = parseSync(id, code);
|
|
559
|
+
let bindings = topLevelBindings(program);
|
|
560
|
+
let calls = [];
|
|
561
|
+
forEachNode(program, (node) => {
|
|
562
|
+
let call = node.type === "CallExpression" ? literalCall(node, bindings) : void 0;
|
|
563
|
+
if (call) calls.push(call);
|
|
564
|
+
});
|
|
565
|
+
let exported = resolverExports(program, bindings);
|
|
566
|
+
let starExports = program.body.flatMap((statement) => statement.type === "ExportAllDeclaration" && !statement.exported ? [statement.source.value] : []);
|
|
567
|
+
let namesResolver = [...exported.values()].some((origin) => origin !== null);
|
|
568
|
+
if (calls.length === 0 && !namesResolver && starExports.length === 0) return;
|
|
569
|
+
return {
|
|
570
|
+
calls,
|
|
571
|
+
resolverExports: exported,
|
|
572
|
+
starExports
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
/** Replaces import specifiers with module ids, dropping those that are external or unresolved. */
|
|
576
|
+
async function linkResolverUsage(usage, resolve) {
|
|
577
|
+
let specifiers = new Set(usage.starExports);
|
|
578
|
+
for (let origin of [...usage.calls.map((call) => call.receiver), ...usage.resolverExports.values()]) if (origin && origin !== "local") specifiers.add(origin.module);
|
|
579
|
+
let resolved = /* @__PURE__ */ new Map();
|
|
580
|
+
await Promise.all([...specifiers].map(async (specifier) => {
|
|
581
|
+
let module = await resolve(specifier);
|
|
582
|
+
if (module && !module.external) resolved.set(specifier, module.id);
|
|
583
|
+
}));
|
|
584
|
+
let link = (origin) => {
|
|
585
|
+
if (origin === "local") return origin;
|
|
586
|
+
let module = resolved.get(origin.module);
|
|
587
|
+
return module === void 0 ? void 0 : {
|
|
588
|
+
module,
|
|
589
|
+
name: origin.name
|
|
590
|
+
};
|
|
591
|
+
};
|
|
592
|
+
let calls = [];
|
|
593
|
+
for (let call of usage.calls) {
|
|
594
|
+
let receiver = link(call.receiver);
|
|
595
|
+
if (receiver) calls.push({
|
|
596
|
+
input: call.input,
|
|
597
|
+
receiver
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
let exported = /* @__PURE__ */ new Map();
|
|
601
|
+
for (let [name, origin] of usage.resolverExports) exported.set(name, origin && (link(origin) ?? null));
|
|
602
|
+
return {
|
|
603
|
+
calls,
|
|
604
|
+
resolverExports: exported,
|
|
605
|
+
starExports: usage.starExports.flatMap((specifier) => resolved.get(specifier) ?? [])
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
function isResolver(modules, origin, seen) {
|
|
609
|
+
if (origin === "local") return true;
|
|
610
|
+
let binding = `${origin.module}\0${origin.name}`;
|
|
611
|
+
if (seen.has(binding)) return false;
|
|
612
|
+
seen.add(binding);
|
|
613
|
+
let usage = modules.get(origin.module);
|
|
614
|
+
let exported = usage?.resolverExports.get(origin.name);
|
|
615
|
+
if (!usage || exported === null) return false;
|
|
616
|
+
if (exported) return isResolver(modules, exported, seen);
|
|
617
|
+
return origin.name !== "default" && usage.starExports.some((module) => isResolver(modules, {
|
|
618
|
+
module,
|
|
619
|
+
name: origin.name
|
|
620
|
+
}, seen));
|
|
621
|
+
}
|
|
622
|
+
/** The browser inputs a module's literal calls register, given its environment's linked usage. */
|
|
623
|
+
function literalInputs(modules, id) {
|
|
624
|
+
return (modules.get(id)?.calls ?? []).filter((call) => isResolver(modules, call.receiver, /* @__PURE__ */ new Set())).map((call) => call.input);
|
|
625
|
+
}
|
|
626
|
+
//#endregion
|
|
627
|
+
//#region src/vite/state.ts
|
|
628
|
+
/**
|
|
629
|
+
* The manifest module a build writes at the root of each server environment's
|
|
630
|
+
* output directory. The server chunks that read the manifest import this file
|
|
631
|
+
* by a relative path, so a host that deploys a single module must bundle it in.
|
|
632
|
+
*/
|
|
633
|
+
const ASSETS_MANIFEST_FILE = "__pitlane_assets_manifest.js";
|
|
634
|
+
const MANIFEST_ID = "\0pitlane:assets-manifest";
|
|
635
|
+
const MANIFEST_EXTERNAL = "pitlane:assets-build-manifest";
|
|
636
|
+
const EMPTY_INPUT = "\0pitlane:assets-empty-input";
|
|
637
|
+
/** The manifest was imported from a server environment `assets()` does not serve. */
|
|
638
|
+
function unservedEnvironmentError(name, served) {
|
|
639
|
+
return /* @__PURE__ */ new Error(`[assets] The "${name}" environment imported ${assetsSpecifier("manifest")}, but assets() serves only ${quotedList(served, "conjunction")}. Add "${name}" to assets({ serverEnvironments }) from ${assetsSpecifier("vite-plugin")}.`);
|
|
640
|
+
}
|
|
641
|
+
//#endregion
|
|
642
|
+
//#region src/vite/build.ts
|
|
643
|
+
async function writeManifests(state) {
|
|
644
|
+
let config = state.config;
|
|
645
|
+
let client = state.outputs.get("client");
|
|
646
|
+
if (!client || state.serverEnvironments.some((name) => !state.outputs.has(name))) return;
|
|
647
|
+
let clientStylesheets = /* @__PURE__ */ new Map();
|
|
648
|
+
for (let asset of Object.values(client.bundle)) if (asset.type === "asset" && isCSSRequest(asset.fileName)) clientStylesheets.set(Buffer.from(asset.source).toString("latin1"), asset.fileName);
|
|
649
|
+
let environments = Object.create(null);
|
|
650
|
+
let twins = /* @__PURE__ */ new Map();
|
|
651
|
+
for (let [name, output] of state.outputs) {
|
|
652
|
+
let graph = output.graph;
|
|
653
|
+
let twin = /* @__PURE__ */ new Map();
|
|
654
|
+
if (output !== client) for (let asset of Object.values(output.bundle)) {
|
|
655
|
+
if (asset.type !== "asset" || !isCSSRequest(asset.fileName)) continue;
|
|
656
|
+
let file = clientStylesheets.get(Buffer.from(asset.source).toString("latin1"));
|
|
657
|
+
if (file) twin.set(asset.fileName, file);
|
|
658
|
+
}
|
|
659
|
+
twins.set(name, twin);
|
|
660
|
+
environments[name] = {
|
|
661
|
+
...graph,
|
|
662
|
+
chunks: Object.fromEntries(Object.entries(graph.chunks).map(([key, chunk]) => [key, {
|
|
663
|
+
...chunk,
|
|
664
|
+
file: publicFile(config, chunk.file),
|
|
665
|
+
stylesheets: chunk.stylesheets.map((file) => publicFile(config, twin.get(file) ?? file))
|
|
666
|
+
}])),
|
|
667
|
+
assets: Object.fromEntries(Object.entries(graph.assets).map(([key, file]) => [key, publicFile(config, twin.get(file) ?? file)]))
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
for (let name of state.serverEnvironments) {
|
|
671
|
+
let manifest = createAssetManifest({
|
|
672
|
+
base: config.base,
|
|
673
|
+
environments: {
|
|
674
|
+
client: environments.client,
|
|
675
|
+
[name]: environments[name]
|
|
676
|
+
},
|
|
677
|
+
importMap: state.importMap
|
|
678
|
+
});
|
|
679
|
+
let output = state.outputs.get(name);
|
|
680
|
+
await mkdir(output.outDir, { recursive: true });
|
|
681
|
+
await writeFile(resolve(output.outDir, ASSETS_MANIFEST_FILE), `export default JSON.parse(${JSON.stringify(JSON.stringify({
|
|
682
|
+
...manifest,
|
|
683
|
+
serverEnvironment: name
|
|
684
|
+
}))});\n`);
|
|
685
|
+
await publishLinkedFiles(output, twins.get(name), client.outDir);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Copies the stylesheets and assets a server graph links into the public client
|
|
690
|
+
* output, so every URL its manifest names is served. A stylesheet with a client
|
|
691
|
+
* twin already is.
|
|
692
|
+
*/
|
|
693
|
+
async function publishLinkedFiles(output, twin, publicDir) {
|
|
694
|
+
let linked = /* @__PURE__ */ new Set([...Object.values(output.graph.chunks).flatMap((chunk) => chunk.stylesheets), ...Object.values(output.graph.assets)]);
|
|
695
|
+
for (let file of linked) {
|
|
696
|
+
let asset = output.bundle[file];
|
|
697
|
+
if (asset?.type !== "asset" || twin.has(file)) continue;
|
|
698
|
+
let destination = resolve(publicDir, file);
|
|
699
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
700
|
+
await writeFile(destination, asset.source);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
function importMapFileName(config) {
|
|
704
|
+
let options = config.build.rolldownOptions.experimental?.chunkImportMap;
|
|
705
|
+
return typeof options === "object" ? options.fileName ?? "importmap.json" : "importmap.json";
|
|
706
|
+
}
|
|
707
|
+
function assetBuild(state) {
|
|
708
|
+
return {
|
|
709
|
+
name: "pitlane-assets-build",
|
|
710
|
+
sharedDuringBuild: true,
|
|
711
|
+
apply: "build",
|
|
712
|
+
configEnvironment(name, config) {
|
|
713
|
+
if (state.serverEnvironments.includes(name)) return { build: { emitAssets: true } };
|
|
714
|
+
if (name === "client" && !config.build?.rolldownOptions?.input && !config.build?.rollupOptions?.input) return { build: { rolldownOptions: { input: EMPTY_INPUT } } };
|
|
715
|
+
},
|
|
716
|
+
resolveId(id) {
|
|
717
|
+
if (id === "\0pitlane:assets-empty-input") return id;
|
|
718
|
+
},
|
|
719
|
+
load(id) {
|
|
720
|
+
if (id === "\0pitlane:assets-empty-input") return "export {};";
|
|
721
|
+
},
|
|
722
|
+
async buildStart() {
|
|
723
|
+
if (this.environment.name !== "client") return;
|
|
724
|
+
let missing = state.serverEnvironments.filter((name) => !state.outputs.has(name));
|
|
725
|
+
if (missing.length) throw new Error(`[assets] Client built before server environments: ${missing.join(", ")}. Build servers before client.`);
|
|
726
|
+
let root = state.config.root;
|
|
727
|
+
state.assetReferences.clear();
|
|
728
|
+
state.scriptReferences.clear();
|
|
729
|
+
let inputs = new Map(state.inputs);
|
|
730
|
+
for (let modules of state.resolverUsage.values()) for (let owner of modules.keys()) for (let input of literalInputs(modules, owner)) {
|
|
731
|
+
let file = inputPath(root, input.key);
|
|
732
|
+
inputs.set(file, {
|
|
733
|
+
...input,
|
|
734
|
+
key: sourceKey(root, file)
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
for (let input of inputs.values()) {
|
|
738
|
+
let id = inputPath(state.config.root, input.key);
|
|
739
|
+
if (input.kind === "asset" && !isCSSRequest(id) && state.config.assetsInclude(id)) {
|
|
740
|
+
let reference = this.emitFile({
|
|
741
|
+
type: "asset",
|
|
742
|
+
name: basename(id),
|
|
743
|
+
originalFileName: id,
|
|
744
|
+
source: await readFile(id)
|
|
745
|
+
});
|
|
746
|
+
state.assetReferences.set(sourceKey(root, id), reference);
|
|
747
|
+
} else {
|
|
748
|
+
let reference = this.emitFile({
|
|
749
|
+
type: "chunk",
|
|
750
|
+
id,
|
|
751
|
+
preserveSignature: "exports-only"
|
|
752
|
+
});
|
|
753
|
+
if (!isCSSRequest(id)) state.scriptReferences.set(sourceKey(root, id), reference);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
},
|
|
757
|
+
renderChunk(code, chunk) {
|
|
758
|
+
if (!code.includes("pitlane:assets-build-manifest")) return;
|
|
759
|
+
let file = posix.relative(posix.dirname(chunk.fileName), ASSETS_MANIFEST_FILE);
|
|
760
|
+
if (!file.startsWith(".")) file = `./${file}`;
|
|
761
|
+
return {
|
|
762
|
+
code: code.replaceAll(MANIFEST_EXTERNAL, file),
|
|
763
|
+
map: null
|
|
764
|
+
};
|
|
765
|
+
},
|
|
766
|
+
generateBundle: {
|
|
767
|
+
order: "post",
|
|
768
|
+
handler(_options, bundle) {
|
|
769
|
+
let removed = /* @__PURE__ */ new Set();
|
|
770
|
+
for (let [name, output] of Object.entries(bundle)) if (output.type === "chunk" && output.facadeModuleId === "\0pitlane:assets-empty-input") {
|
|
771
|
+
removed.add(name);
|
|
772
|
+
delete bundle[name];
|
|
773
|
+
}
|
|
774
|
+
if (this.environment.name !== "client" || !state.mapsEnabled) return;
|
|
775
|
+
let fileName = importMapFileName(this.environment.config);
|
|
776
|
+
let map = captureImportMap(bundle, fileName);
|
|
777
|
+
let base = this.environment.config.base;
|
|
778
|
+
for (let [key, href] of Object.entries(map.imports)) {
|
|
779
|
+
if (!href.startsWith(base)) continue;
|
|
780
|
+
let file = href.slice(base.length);
|
|
781
|
+
if (removed.has(file) || !(file in bundle)) delete map.imports[key];
|
|
782
|
+
}
|
|
783
|
+
let artifact = bundle[fileName];
|
|
784
|
+
if (artifact.type === "asset") artifact.source = JSON.stringify(map);
|
|
785
|
+
}
|
|
786
|
+
},
|
|
787
|
+
writeBundle: {
|
|
788
|
+
order: "post",
|
|
789
|
+
async handler(_options, bundle) {
|
|
790
|
+
let name = this.environment.name;
|
|
791
|
+
if (name !== "client" && !state.serverEnvironments.includes(name)) return;
|
|
792
|
+
let config = this.environment.config;
|
|
793
|
+
let assets = /* @__PURE__ */ new Map();
|
|
794
|
+
let scripts = /* @__PURE__ */ new Map();
|
|
795
|
+
if (name === "client") {
|
|
796
|
+
for (let [key, reference] of state.assetReferences) assets.set(key, this.getFileName(reference));
|
|
797
|
+
for (let [key, reference] of state.scriptReferences) scripts.set(key, this.getFileName(reference));
|
|
798
|
+
state.importMap = state.mapsEnabled ? captureImportMap(bundle, importMapFileName(config)) : void 0;
|
|
799
|
+
}
|
|
800
|
+
state.outputs.set(name, {
|
|
801
|
+
graph: captureOutput(bundle, config, name === "client" ? "client" : "server", assets, scripts),
|
|
802
|
+
bundle,
|
|
803
|
+
outDir: resolve(config.root, config.build.outDir)
|
|
804
|
+
});
|
|
805
|
+
await writeManifests(state);
|
|
806
|
+
}
|
|
807
|
+
},
|
|
808
|
+
buildApp: {
|
|
809
|
+
order: "pre",
|
|
810
|
+
async handler(builder) {
|
|
811
|
+
let build = builder.build.bind(builder);
|
|
812
|
+
builder.build = (async (environment) => {
|
|
813
|
+
if (environment.isBuilt) return;
|
|
814
|
+
return build(environment);
|
|
815
|
+
});
|
|
816
|
+
for (let name of state.serverEnvironments) {
|
|
817
|
+
let environment = builder.environments[name];
|
|
818
|
+
if (!environment) throw new Error(`[assets] Unknown server environment: ${name}`);
|
|
819
|
+
await builder.build(environment);
|
|
820
|
+
}
|
|
821
|
+
await builder.build(builder.environments.client);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
//#endregion
|
|
827
|
+
//#region src/vite/dev-css.ts
|
|
828
|
+
function linkedStylesheetLookup(config) {
|
|
829
|
+
return `
|
|
830
|
+
function pitlaneStyleKey(href) {
|
|
831
|
+
const url = new URL(href, document.baseURI);
|
|
832
|
+
url.searchParams.delete("t");
|
|
833
|
+
url.searchParams.delete("direct");
|
|
834
|
+
url.searchParams.sort();
|
|
835
|
+
return url.href;
|
|
836
|
+
}
|
|
837
|
+
function pitlaneHasLinkedStylesheet(id) {
|
|
838
|
+
const root = ${JSON.stringify(config.root + "/")};
|
|
839
|
+
const path = id.startsWith(root) ? id.slice(root.length) : "@fs/" + id.replace(/^\\//, "");
|
|
840
|
+
const key = pitlaneStyleKey(${JSON.stringify(config.base)} + path);
|
|
841
|
+
for (const link of document.querySelectorAll('link[rel="stylesheet"][href]')) {
|
|
842
|
+
if (pitlaneStyleKey(link.href) === key) return true;
|
|
843
|
+
}
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
`;
|
|
847
|
+
}
|
|
848
|
+
function assetStyles() {
|
|
849
|
+
let config;
|
|
850
|
+
let clientPath = normalizePath(fileURLToPath(import.meta.resolve("vite/dist/client/client.mjs")));
|
|
851
|
+
return {
|
|
852
|
+
name: "pitlane-assets-css",
|
|
853
|
+
apply: "serve",
|
|
854
|
+
configResolved(resolved) {
|
|
855
|
+
config = resolved;
|
|
856
|
+
},
|
|
857
|
+
configureServer(server) {
|
|
858
|
+
server.middlewares.use((request, _response, next) => {
|
|
859
|
+
if (request.headers.accept?.includes("text/css") && request.url?.includes("&lang.css=")) request.url = request.url.replace("&lang.css=", "?lang.css");
|
|
860
|
+
next();
|
|
861
|
+
});
|
|
862
|
+
},
|
|
863
|
+
transform: {
|
|
864
|
+
order: "post",
|
|
865
|
+
handler(code, id) {
|
|
866
|
+
if (id === clientPath) {
|
|
867
|
+
let position = code.indexOf("function updateStyle(id, content) {");
|
|
868
|
+
if (position === -1) throw new Error("[assets] This Vite client does not expose the expected CSS update function. Use a supported Vite version.");
|
|
869
|
+
let transformed = new MagicString(code);
|
|
870
|
+
transformed.prepend(linkedStylesheetLookup(config));
|
|
871
|
+
transformed.appendLeft(position + 35, "\nif (pitlaneHasLinkedStylesheet(id)) return;\n");
|
|
872
|
+
return {
|
|
873
|
+
code: transformed.toString(),
|
|
874
|
+
map: transformed.generateMap({ hires: true })
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
if (this.environment.mode !== "dev" || this.environment.name !== "client" || !isCSSRequest(id) || !/[?&]direct\b/.test(id)) return;
|
|
878
|
+
let module = this.environment.moduleGraph.getModuleById(id);
|
|
879
|
+
if (module) module.isSelfAccepting = true;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
//#endregion
|
|
885
|
+
//#region src/vite/transform-start.ts
|
|
886
|
+
let invalidations = 0;
|
|
887
|
+
let requestStart = new AsyncLocalStorage();
|
|
888
|
+
/** Counts a hard invalidation and returns its place on the timeline. */
|
|
889
|
+
function countInvalidation() {
|
|
890
|
+
return ++invalidations;
|
|
891
|
+
}
|
|
892
|
+
/** Runs `request`, which loads and transforms a module, as starting now. */
|
|
893
|
+
function startRequest(request) {
|
|
894
|
+
return requestStart.run(invalidations, request);
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* The place on the timeline where the calling transform's request started.
|
|
898
|
+
* A transform no request started, such as one a plugin runs through
|
|
899
|
+
* `pluginContainer.transform` itself, started where this is called.
|
|
900
|
+
*/
|
|
901
|
+
function requestStarted() {
|
|
902
|
+
return requestStart.getStore() ?? invalidations;
|
|
903
|
+
}
|
|
904
|
+
//#endregion
|
|
905
|
+
//#region src/vite/dev-graph.ts
|
|
906
|
+
/** CSS requests that import a string or URL rather than apply a stylesheet. */
|
|
907
|
+
const NON_STYLESHEET_QUERY = /[?&](?:inline|url|raw)\b/;
|
|
908
|
+
/**
|
|
909
|
+
* The import specifiers of transformed JavaScript. Only literal `import()`
|
|
910
|
+
* arguments count as dynamic edges; a computed argument names no module.
|
|
911
|
+
*/
|
|
912
|
+
function importSpecifiers(code) {
|
|
913
|
+
let { module } = parseSync("module.js", code, { sourceType: "module" });
|
|
914
|
+
let staticSpecifiers = new Set(module.staticImports.map((entry) => entry.moduleRequest.value));
|
|
915
|
+
for (let entry of module.staticExports) for (let item of entry.entries) if (item.moduleRequest) staticSpecifiers.add(item.moduleRequest.value);
|
|
916
|
+
let dynamicSpecifiers = /* @__PURE__ */ new Set();
|
|
917
|
+
for (let entry of module.dynamicImports) {
|
|
918
|
+
let literal = /^(["'`])([^"'`$]*)\1$/.exec(code.slice(entry.moduleRequest.start, entry.moduleRequest.end).trim());
|
|
919
|
+
if (literal) dynamicSpecifiers.add(literal[2]);
|
|
920
|
+
}
|
|
921
|
+
return {
|
|
922
|
+
static: [...staticSpecifiers],
|
|
923
|
+
dynamic: [...dynamicSpecifiers]
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
/** The path Vite's dev server serves a resolved module id at, before `base`. */
|
|
927
|
+
function servedPath(root, id) {
|
|
928
|
+
if (id.startsWith("\0")) return `/@id/__x00__${id.slice(1)}`;
|
|
929
|
+
let query = id.indexOf("?");
|
|
930
|
+
let file = query === -1 ? id : id.slice(0, query);
|
|
931
|
+
let search = query === -1 ? "" : id.slice(query);
|
|
932
|
+
if (!isAbsolute(file)) return `/@id/${id}`;
|
|
933
|
+
if (file.startsWith(`${root}/`)) return file.slice(root.length) + search;
|
|
934
|
+
return `/@fs/${file.replace(/^\//, "")}${search}`;
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Whether discovery transforms a module. A server environment reads a package
|
|
938
|
+
* from `node_modules` only when Vite transforms rather than externalizes it.
|
|
939
|
+
* The client follows packages Vite serves as source, but not a prebundled
|
|
940
|
+
* dependency: the optimizer's cache file is not a source module, and loading
|
|
941
|
+
* it would wait for the optimizer.
|
|
942
|
+
*/
|
|
943
|
+
function followed(environment, id) {
|
|
944
|
+
if (id === "\0pitlane:assets-manifest" || environment.depsOptimizer?.isOptimizedDepFile(id)) return false;
|
|
945
|
+
let pkg = /.*\/node_modules\/((?:@[^/]+\/)?[^/?]+)/.exec(id)?.[1];
|
|
946
|
+
if (pkg === void 0 || environment.config.consumer === "client") return true;
|
|
947
|
+
let { noExternal } = environment.config.resolve;
|
|
948
|
+
if (noExternal === true) return true;
|
|
949
|
+
return (noExternal === void 0 ? [] : Array.isArray(noExternal) ? noExternal : [noExternal]).some((pattern) => typeof pattern === "string" ? pattern === pkg : pattern.test(pkg));
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* A module is analyzed once. Its edge record was written by the transform
|
|
953
|
+
* hook, whether the runner or discovery ran that transform, and stays
|
|
954
|
+
* current until the module is invalidated, which drops the record. Keying
|
|
955
|
+
* this on `transformResult` instead would re-transform every module the
|
|
956
|
+
* runner has not executed on each rebuild.
|
|
957
|
+
*
|
|
958
|
+
* Client discovery goes through `transformRequest`, so it shares Vite's
|
|
959
|
+
* in-flight and cached transforms with the import analysis that warms a
|
|
960
|
+
* browser module's dependencies, and the browser later reads the cache.
|
|
961
|
+
* Server discovery must not populate `transformResult`: the module runner
|
|
962
|
+
* asks whether a module it already evaluated is still current by that
|
|
963
|
+
* field, so a transform cached between a file change and the runner's next
|
|
964
|
+
* import would keep its stale instance alive.
|
|
965
|
+
*/
|
|
966
|
+
async function analyze(environment, id, edges) {
|
|
967
|
+
if (edges.has(id)) return;
|
|
968
|
+
let { root } = environment.config;
|
|
969
|
+
try {
|
|
970
|
+
let url = servedPath(root, id);
|
|
971
|
+
if (environment.config.consumer === "client") {
|
|
972
|
+
await environment.transformRequest(url.startsWith("/@id/") ? id : url);
|
|
973
|
+
if (edges.has(id)) return;
|
|
974
|
+
}
|
|
975
|
+
await startRequest(() => transformOutsideCache(environment, id, url));
|
|
976
|
+
} catch (error) {
|
|
977
|
+
let name = fileModule(id) ? sourceKey(root, id) : id;
|
|
978
|
+
throw new Error(`[assets] Could not analyze "${name}" for the "${environment.name}" development asset manifest: ${error.message}`, { cause: error });
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Loads and transforms a module as `transformRequest` does, handing the load
|
|
983
|
+
* result's source map and module type to the transform, but without reading
|
|
984
|
+
* or populating Vite's transform cache.
|
|
985
|
+
*/
|
|
986
|
+
async function transformOutsideCache(environment, id, url) {
|
|
987
|
+
await environment.moduleGraph.ensureEntryFromUrl(url);
|
|
988
|
+
let loaded = await environment.pluginContainer.load(id);
|
|
989
|
+
let source = typeof loaded === "string" ? { code: loaded } : loaded;
|
|
990
|
+
let code = source?.code;
|
|
991
|
+
if (code == null) {
|
|
992
|
+
if (!fileModule(id)) throw new Error("No plugin loaded the module.");
|
|
993
|
+
code = await readFile(id.split("?")[0], "utf8");
|
|
994
|
+
}
|
|
995
|
+
await environment.pluginContainer.transform(code, id, {
|
|
996
|
+
inMap: source?.map,
|
|
997
|
+
moduleType: source?.moduleType
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Every module reachable from `roots` through static and dynamic edges,
|
|
1002
|
+
* transforming each one the graph has not analyzed yet. Stylesheets are
|
|
1003
|
+
* leaves: their own `@import`s are inlined by Vite's CSS pipeline. Passing
|
|
1004
|
+
* the result of an earlier walk extends it without revisiting its modules.
|
|
1005
|
+
*/
|
|
1006
|
+
async function walkGraph(environment, roots, edges, visited = /* @__PURE__ */ new Set()) {
|
|
1007
|
+
let frontier = [...roots];
|
|
1008
|
+
while (frontier.length > 0) {
|
|
1009
|
+
let next = [];
|
|
1010
|
+
await Promise.all(frontier.map(async (id) => {
|
|
1011
|
+
if (visited.has(id) || !(isCSSRequest(id) || followed(environment, id))) return;
|
|
1012
|
+
visited.add(id);
|
|
1013
|
+
if (isCSSRequest(id)) return;
|
|
1014
|
+
await analyze(environment, id, edges);
|
|
1015
|
+
let record = edges.get(id);
|
|
1016
|
+
if (record) next.push(...record.static, ...record.dynamic);
|
|
1017
|
+
}));
|
|
1018
|
+
frontier = next;
|
|
1019
|
+
}
|
|
1020
|
+
return visited;
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* The stylesheet ids a module requires through static imports, in execution
|
|
1024
|
+
* order: each dependency's stylesheets before its importer's later imports.
|
|
1025
|
+
*/
|
|
1026
|
+
function staticStylesheets(edges, start) {
|
|
1027
|
+
let seen = /* @__PURE__ */ new Set();
|
|
1028
|
+
let stylesheets = [];
|
|
1029
|
+
let visit = (id) => {
|
|
1030
|
+
if (seen.has(id)) return;
|
|
1031
|
+
seen.add(id);
|
|
1032
|
+
if (isCSSRequest(id)) {
|
|
1033
|
+
if (!NON_STYLESHEET_QUERY.test(id)) stylesheets.push(id);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
for (let dependency of edges.get(id)?.static ?? []) visit(dependency);
|
|
1037
|
+
};
|
|
1038
|
+
visit(start);
|
|
1039
|
+
return stylesheets;
|
|
1040
|
+
}
|
|
1041
|
+
//#endregion
|
|
1042
|
+
//#region src/vite/dev-snapshot.ts
|
|
1043
|
+
function environmentEdges(graph, name) {
|
|
1044
|
+
let edges = graph.edges.get(name);
|
|
1045
|
+
if (!edges) graph.edges.set(name, edges = /* @__PURE__ */ new Map());
|
|
1046
|
+
return edges;
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Whether a module id is a source file with a key of its own. Vite's `?v=`
|
|
1050
|
+
* version hash on a package module does not change which file it is; any
|
|
1051
|
+
* other query, such as a single-file component's style block, names a part
|
|
1052
|
+
* of its file that the file's own key already covers.
|
|
1053
|
+
*/
|
|
1054
|
+
function sourceModule(id) {
|
|
1055
|
+
return fileModule(id) && !id.replace(/\?v=[\w-]+$/, "").includes("?");
|
|
1056
|
+
}
|
|
1057
|
+
/** The resolved, non-HTML build inputs an environment declares. */
|
|
1058
|
+
async function configuredInputs(environment) {
|
|
1059
|
+
let { build, root } = environment.config;
|
|
1060
|
+
let input = build.rolldownOptions.input;
|
|
1061
|
+
let inputs = typeof input === "string" ? [input] : Array.isArray(input) ? input : Object.values(input ?? {});
|
|
1062
|
+
let ids = [];
|
|
1063
|
+
for (let entry of inputs) {
|
|
1064
|
+
if (entry.endsWith(".html")) continue;
|
|
1065
|
+
let file = resolve(root, entry);
|
|
1066
|
+
let resolved = await environment.pluginContainer.resolveId(existsSync(file) ? file : entry);
|
|
1067
|
+
if (!resolved) throw new Error(`[assets] Could not resolve the "${environment.name}" environment's input "${entry}".`);
|
|
1068
|
+
ids.push(resolved.id);
|
|
1069
|
+
}
|
|
1070
|
+
return ids;
|
|
1071
|
+
}
|
|
1072
|
+
async function createDevSnapshot(server, state, graph, name) {
|
|
1073
|
+
let config = state.config;
|
|
1074
|
+
let { root } = config;
|
|
1075
|
+
let base = config.base.replace(/\/$/, "");
|
|
1076
|
+
let serverEnvironment = server.environments[name];
|
|
1077
|
+
let clientEnvironment = server.environments.client;
|
|
1078
|
+
let serverEdges = environmentEdges(graph, name);
|
|
1079
|
+
let serverIds = await walkGraph(serverEnvironment, [...await configuredInputs(serverEnvironment), ...graph.roots.get(name) ?? []], serverEdges);
|
|
1080
|
+
await clientEnvironment.depsOptimizer?.init();
|
|
1081
|
+
let entryIds = /* @__PURE__ */ new Set([...state.inputs.keys(), ...await configuredInputs(clientEnvironment)]);
|
|
1082
|
+
let usage = graph.resolverUsage.get(name) ?? /* @__PURE__ */ new Map();
|
|
1083
|
+
let registrations = state.registrations.get(name);
|
|
1084
|
+
for (let id of serverIds) {
|
|
1085
|
+
for (let input of literalInputs(usage, id)) entryIds.add(inputPath(root, input.key));
|
|
1086
|
+
for (let entry of registrations?.get(id) ?? []) entryIds.add(entry);
|
|
1087
|
+
}
|
|
1088
|
+
let entries = Object.create(null);
|
|
1089
|
+
let assets = Object.create(null);
|
|
1090
|
+
let scripts = [];
|
|
1091
|
+
for (let id of entryIds) {
|
|
1092
|
+
let url = base + servedPath(root, id);
|
|
1093
|
+
if (isCSSRequest(id) || config.assetsInclude(id.split("?")[0])) assets[sourceKey(root, id)] = url;
|
|
1094
|
+
else {
|
|
1095
|
+
entries[sourceKey(root, id)] = url;
|
|
1096
|
+
scripts.push(id);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
let clientIds = await walkGraph(clientEnvironment, scripts, environmentEdges(graph, "client"));
|
|
1100
|
+
let serverModules = Object.create(null);
|
|
1101
|
+
for (let id of serverIds) {
|
|
1102
|
+
if (!sourceModule(id)) continue;
|
|
1103
|
+
serverModules[sourceKey(root, id)] = {
|
|
1104
|
+
preloads: [],
|
|
1105
|
+
stylesheets: staticStylesheets(serverEdges, id).map((stylesheet) => base + servedPath(root, stylesheet))
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
let clientModules = Object.create(null);
|
|
1109
|
+
for (let id of clientIds) if (sourceModule(id)) clientModules[sourceKey(root, id)] = {
|
|
1110
|
+
preloads: [],
|
|
1111
|
+
stylesheets: []
|
|
1112
|
+
};
|
|
1113
|
+
return {
|
|
1114
|
+
manifest: {
|
|
1115
|
+
mode: "dev",
|
|
1116
|
+
environments: {
|
|
1117
|
+
client: {
|
|
1118
|
+
role: "client",
|
|
1119
|
+
modules: clientModules
|
|
1120
|
+
},
|
|
1121
|
+
[name]: {
|
|
1122
|
+
role: "server",
|
|
1123
|
+
modules: serverModules
|
|
1124
|
+
}
|
|
1125
|
+
},
|
|
1126
|
+
entries,
|
|
1127
|
+
assets,
|
|
1128
|
+
serverEnvironment: name
|
|
1129
|
+
},
|
|
1130
|
+
server: serverIds,
|
|
1131
|
+
client: clientIds
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
//#endregion
|
|
1135
|
+
//#region src/vite/dev.ts
|
|
1136
|
+
/**
|
|
1137
|
+
* Serves `@pitlane/assets/manifest` during `vite dev` as a data snapshot of
|
|
1138
|
+
* each server environment's module graph, and invalidates it when a change
|
|
1139
|
+
* could alter the stylesheets, routes, or browser entries it records.
|
|
1140
|
+
*/
|
|
1141
|
+
function assetDevelopment(state) {
|
|
1142
|
+
let server;
|
|
1143
|
+
let graph = {
|
|
1144
|
+
roots: /* @__PURE__ */ new Map(),
|
|
1145
|
+
edges: /* @__PURE__ */ new Map(),
|
|
1146
|
+
resolverUsage: /* @__PURE__ */ new Map()
|
|
1147
|
+
};
|
|
1148
|
+
let snapshots = /* @__PURE__ */ new Map();
|
|
1149
|
+
let building = /* @__PURE__ */ new Set();
|
|
1150
|
+
/** Server environments invalidated while their snapshot was being built. */
|
|
1151
|
+
let stale = /* @__PURE__ */ new Set();
|
|
1152
|
+
/** Environment name → module id → where its last hard invalidation is on the transform timeline. */
|
|
1153
|
+
let invalidations = /* @__PURE__ */ new Map();
|
|
1154
|
+
/**
|
|
1155
|
+
* Invalidates a server environment's manifest and, through Vite, every
|
|
1156
|
+
* module importing it. A manifest being built is marked instead, and
|
|
1157
|
+
* `load` builds it again, since Vite would cache the code `load` returns.
|
|
1158
|
+
*/
|
|
1159
|
+
function invalidate(name, timestamp) {
|
|
1160
|
+
if (building.has(name)) return void stale.add(name);
|
|
1161
|
+
let environment = server?.environments[name];
|
|
1162
|
+
let manifest = environment?.moduleGraph.getModuleById(MANIFEST_ID);
|
|
1163
|
+
if (manifest) environment.moduleGraph.invalidateModule(manifest, /* @__PURE__ */ new Set(), timestamp, true);
|
|
1164
|
+
}
|
|
1165
|
+
/** The modules of `environment` a server environment's snapshot was computed from. */
|
|
1166
|
+
function tracked(environment, serverName) {
|
|
1167
|
+
let snapshot = snapshots.get(serverName);
|
|
1168
|
+
if (environment === serverName) return snapshot?.server;
|
|
1169
|
+
if (environment === "client") return snapshot?.client;
|
|
1170
|
+
}
|
|
1171
|
+
/** Invalidates every snapshot computed, or being computed, from module `id` of `environment`. */
|
|
1172
|
+
function invalidateSnapshotsWith(environment, id, timestamp) {
|
|
1173
|
+
for (let serverName of snapshots.keys()) if (tracked(environment, serverName)?.has(id)) invalidate(serverName, timestamp);
|
|
1174
|
+
for (let serverName of building) if (environment === serverName || environment === "client") stale.add(serverName);
|
|
1175
|
+
}
|
|
1176
|
+
function sameEdges(previous, next) {
|
|
1177
|
+
return previous.static.length === next.static.length && previous.dynamic.length === next.dynamic.length && previous.static.every((edge, index) => edge === next.static[index]) && previous.dynamic.every((edge, index) => edge === next.dynamic[index]);
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Records what a transform of module `id` found: its edges and, in a
|
|
1181
|
+
* server environment, its resolver usage.
|
|
1182
|
+
*/
|
|
1183
|
+
function recordTransform(environment, id, edges, usage) {
|
|
1184
|
+
let edgeRecords = environmentEdges(graph, environment);
|
|
1185
|
+
let previous = edgeRecords.get(id);
|
|
1186
|
+
edgeRecords.set(id, edges);
|
|
1187
|
+
if (previous && !sameEdges(previous, edges)) invalidateSnapshotsWith(environment, id);
|
|
1188
|
+
let usageRecords = graph.resolverUsage.get(environment);
|
|
1189
|
+
if (usage) usageRecords?.set(id, usage);
|
|
1190
|
+
else usageRecords?.delete(id);
|
|
1191
|
+
}
|
|
1192
|
+
return {
|
|
1193
|
+
name: "pitlane-assets-dev",
|
|
1194
|
+
apply: "serve",
|
|
1195
|
+
enforce: "post",
|
|
1196
|
+
configureServer: {
|
|
1197
|
+
order: "pre",
|
|
1198
|
+
handler(devServer) {
|
|
1199
|
+
server = devServer;
|
|
1200
|
+
for (let environment of Object.values(devServer.environments)) {
|
|
1201
|
+
let { name, moduleGraph } = environment;
|
|
1202
|
+
let edges = environmentEdges(graph, name);
|
|
1203
|
+
let invalidated = /* @__PURE__ */ new Map();
|
|
1204
|
+
invalidations.set(name, invalidated);
|
|
1205
|
+
let invalidateModule = moduleGraph.invalidateModule;
|
|
1206
|
+
moduleGraph.invalidateModule = (mod, seen, timestamp, isHmr, softInvalidate) => {
|
|
1207
|
+
if (!softInvalidate && mod.id && mod.id !== "\0pitlane:assets-manifest" && !isCSSRequest(mod.id)) {
|
|
1208
|
+
edges.delete(mod.id);
|
|
1209
|
+
if (!isHmr) invalidated.set(mod.id, countInvalidation());
|
|
1210
|
+
invalidateSnapshotsWith(name, mod.id, timestamp);
|
|
1211
|
+
}
|
|
1212
|
+
invalidateModule.call(moduleGraph, mod, seen, timestamp, isHmr, softInvalidate);
|
|
1213
|
+
};
|
|
1214
|
+
let { transformRequest, warmupRequest } = environment;
|
|
1215
|
+
environment.transformRequest = (url) => startRequest(() => transformRequest.call(environment, url));
|
|
1216
|
+
environment.warmupRequest = (url) => startRequest(() => warmupRequest.call(environment, url));
|
|
1217
|
+
if (!state.serverEnvironments.includes(name)) continue;
|
|
1218
|
+
let roots = /* @__PURE__ */ new Set();
|
|
1219
|
+
graph.roots.set(name, roots);
|
|
1220
|
+
graph.resolverUsage.set(name, /* @__PURE__ */ new Map());
|
|
1221
|
+
let fetchModule = environment.fetchModule;
|
|
1222
|
+
environment.fetchModule = async (id, importer, options) => {
|
|
1223
|
+
let fetched = await fetchModule.call(environment, id, importer, options);
|
|
1224
|
+
if (!importer && "id" in fetched && fetched.id !== "\0pitlane:assets-manifest") {
|
|
1225
|
+
roots.add(fetched.id);
|
|
1226
|
+
let snapshot = snapshots.get(name);
|
|
1227
|
+
if (snapshot && !snapshot.server.has(fetched.id)) invalidate(name);
|
|
1228
|
+
}
|
|
1229
|
+
return fetched;
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
state.onRegistrationChange = (environment, owner) => {
|
|
1233
|
+
if (building.has(environment)) return;
|
|
1234
|
+
if (snapshots.get(environment)?.server.has(owner)) invalidate(environment);
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
},
|
|
1238
|
+
async load(id) {
|
|
1239
|
+
if (id !== "\0pitlane:assets-manifest") return;
|
|
1240
|
+
let { name, config } = this.environment;
|
|
1241
|
+
if (config.consumer === "client") {
|
|
1242
|
+
let message = `[assets] ${assetsSpecifier("manifest")} describes the server's module graph and cannot run in the browser. Resolve asset URLs in server code and pass them to the page.`;
|
|
1243
|
+
return `throw new Error(${JSON.stringify(message)});\nexport default undefined;\n`;
|
|
1244
|
+
}
|
|
1245
|
+
if (!state.serverEnvironments.includes(name)) throw unservedEnvironmentError(name, state.serverEnvironments);
|
|
1246
|
+
building.add(name);
|
|
1247
|
+
try {
|
|
1248
|
+
let snapshot;
|
|
1249
|
+
let passes = 0;
|
|
1250
|
+
do {
|
|
1251
|
+
stale.delete(name);
|
|
1252
|
+
snapshot = await createDevSnapshot(server, state, graph, name);
|
|
1253
|
+
} while (stale.has(name) && ++passes < 3);
|
|
1254
|
+
snapshots.set(name, snapshot);
|
|
1255
|
+
if (stale.has(name)) {
|
|
1256
|
+
let { moduleGraph } = server.environments[name];
|
|
1257
|
+
moduleGraph.invalidateModule(await moduleGraph.ensureEntryFromUrl(MANIFEST_ID));
|
|
1258
|
+
}
|
|
1259
|
+
return `export default JSON.parse(${JSON.stringify(JSON.stringify(snapshot.manifest))});\n`;
|
|
1260
|
+
} finally {
|
|
1261
|
+
building.delete(name);
|
|
1262
|
+
stale.delete(name);
|
|
1263
|
+
}
|
|
1264
|
+
},
|
|
1265
|
+
transform: { async handler(code, id) {
|
|
1266
|
+
if (id === "\0pitlane:assets-manifest" || isCSSRequest(id)) return;
|
|
1267
|
+
let name = this.environment.name;
|
|
1268
|
+
let invalidated = invalidations.get(name);
|
|
1269
|
+
let started = requestStarted();
|
|
1270
|
+
let resolve = (specifier) => this.resolve(specifier, id);
|
|
1271
|
+
let resolveAll = async (list) => {
|
|
1272
|
+
let resolved = await Promise.all(list.map(resolve));
|
|
1273
|
+
return [...new Set(resolved.flatMap((result) => result && !result.external ? [result.id] : []))];
|
|
1274
|
+
};
|
|
1275
|
+
let specifiers = importSpecifiers(code);
|
|
1276
|
+
let usage = state.serverEnvironments.includes(name) ? scanResolverUsage(code, id) : void 0;
|
|
1277
|
+
let [staticEdges, dynamicEdges, linkedUsage] = await Promise.all([
|
|
1278
|
+
resolveAll(specifiers.static),
|
|
1279
|
+
resolveAll(specifiers.dynamic),
|
|
1280
|
+
usage && linkResolverUsage(usage, resolve)
|
|
1281
|
+
]);
|
|
1282
|
+
if ((invalidated?.get(id) ?? 0) > started) return;
|
|
1283
|
+
recordTransform(name, id, {
|
|
1284
|
+
static: staticEdges,
|
|
1285
|
+
dynamic: dynamicEdges
|
|
1286
|
+
}, linkedUsage);
|
|
1287
|
+
} },
|
|
1288
|
+
hotUpdate: {
|
|
1289
|
+
order: "post",
|
|
1290
|
+
handler({ type, file, modules, timestamp }) {
|
|
1291
|
+
if (type === "update" && isCSSRequest(file)) return;
|
|
1292
|
+
let path = normalizePath(file);
|
|
1293
|
+
if (type === "delete") {
|
|
1294
|
+
for (let roots of graph.roots.values()) for (let root of roots) if (root.split("?")[0] === path) roots.delete(root);
|
|
1295
|
+
for (let owners of [...state.registrations.values(), ...graph.resolverUsage.values()]) for (let owner of owners.keys()) if (owner.split("?")[0] === path) owners.delete(owner);
|
|
1296
|
+
}
|
|
1297
|
+
let name = this.environment.name;
|
|
1298
|
+
let ids = modules.flatMap((module) => module.id ? [module.id] : []);
|
|
1299
|
+
for (let serverName of snapshots.keys()) {
|
|
1300
|
+
let set = tracked(name, serverName);
|
|
1301
|
+
if (set && (set.has(path) || ids.some((id) => set.has(id)))) invalidate(serverName, timestamp);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
//#endregion
|
|
1308
|
+
//#region src/vite-plugin.ts
|
|
1309
|
+
/**
|
|
1310
|
+
* The Vite plugins that give `@pitlane/assets` its manifest: they serve
|
|
1311
|
+
* browser entries during `vite dev`, emit them in a build, and replace
|
|
1312
|
+
* `@pitlane/assets/manifest` with the manifest of the current dev server or
|
|
1313
|
+
* build. Setting `allowFiles` also enforces the browser boundary.
|
|
1314
|
+
*
|
|
1315
|
+
* @throws When `allowPackages` or `denyFiles` is set without `allowFiles`.
|
|
1316
|
+
*/
|
|
1317
|
+
function assets(options = {}) {
|
|
1318
|
+
let { allowFiles, allowPackages, denyFiles } = options;
|
|
1319
|
+
if (!allowFiles && (allowPackages || denyFiles)) throw new Error("[assets] allowPackages and denyFiles refine allowFiles. Set allowFiles to turn on the browser boundary.");
|
|
1320
|
+
let state = {
|
|
1321
|
+
serverEnvironments: options.serverEnvironments ?? ["ssr"],
|
|
1322
|
+
inputs: /* @__PURE__ */ new Map(),
|
|
1323
|
+
resolverUsage: /* @__PURE__ */ new Map(),
|
|
1324
|
+
registrations: /* @__PURE__ */ new Map(),
|
|
1325
|
+
assetReferences: /* @__PURE__ */ new Map(),
|
|
1326
|
+
scriptReferences: /* @__PURE__ */ new Map(),
|
|
1327
|
+
outputs: /* @__PURE__ */ new Map(),
|
|
1328
|
+
mapsEnabled: false
|
|
1329
|
+
};
|
|
1330
|
+
return [
|
|
1331
|
+
{
|
|
1332
|
+
name: "pitlane-assets",
|
|
1333
|
+
api: { setBrowserEntries({ environment, owner, entries }) {
|
|
1334
|
+
if (!state.serverEnvironments.includes(environment)) throw new Error(`[assets] Cannot register browser entries from "${environment}": it is not in serverEnvironments.`);
|
|
1335
|
+
let owners = state.registrations.get(environment);
|
|
1336
|
+
let previous = owners?.get(owner);
|
|
1337
|
+
if (!previous && entries.length === 0 || previous?.length === entries.length && entries.every((id, index) => id === previous[index])) return;
|
|
1338
|
+
if (entries.length === 0) {
|
|
1339
|
+
owners.delete(owner);
|
|
1340
|
+
if (owners.size === 0) state.registrations.delete(environment);
|
|
1341
|
+
} else {
|
|
1342
|
+
if (!owners) state.registrations.set(environment, owners = /* @__PURE__ */ new Map());
|
|
1343
|
+
owners.set(owner, entries.slice());
|
|
1344
|
+
}
|
|
1345
|
+
state.onRegistrationChange?.(environment, owner);
|
|
1346
|
+
} },
|
|
1347
|
+
enforce: "post",
|
|
1348
|
+
sharedDuringBuild: true,
|
|
1349
|
+
config(config) {
|
|
1350
|
+
state.mapsEnabled = options.chunkImportMap ?? config.environments?.client?.build?.chunkImportMap ?? config.build?.chunkImportMap ?? false;
|
|
1351
|
+
if (state.mapsEnabled && config.experimental?.renderBuiltUrl) throw new Error("[assets] chunkImportMap: true cannot be combined with experimental.renderBuiltUrl.");
|
|
1352
|
+
return {
|
|
1353
|
+
builder: {},
|
|
1354
|
+
build: { assetsInlineLimit: 0 },
|
|
1355
|
+
optimizeDeps: { exclude: ["@pitlane/assets/manifest", "pitlane/assets/manifest"] },
|
|
1356
|
+
environments: { client: { build: { chunkImportMap: state.mapsEnabled } } }
|
|
1357
|
+
};
|
|
1358
|
+
},
|
|
1359
|
+
configResolved(config) {
|
|
1360
|
+
state.config = config;
|
|
1361
|
+
for (let key of options.include ?? []) {
|
|
1362
|
+
let id = inputPath(config.root, key);
|
|
1363
|
+
state.inputs.set(id, {
|
|
1364
|
+
key: sourceKey(config.root, id),
|
|
1365
|
+
kind: "asset"
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
},
|
|
1369
|
+
configEnvironment(name) {
|
|
1370
|
+
if (!state.serverEnvironments.includes(name)) return;
|
|
1371
|
+
return {
|
|
1372
|
+
resolve: { noExternal: [/^@pitlane\/assets(?:\/|$)/, /^pitlane(?:\/|$)/] },
|
|
1373
|
+
optimizeDeps: { exclude: ["@pitlane/assets/manifest", "pitlane/assets/manifest"] },
|
|
1374
|
+
build: { chunkImportMap: false }
|
|
1375
|
+
};
|
|
1376
|
+
},
|
|
1377
|
+
resolveId: {
|
|
1378
|
+
order: "pre",
|
|
1379
|
+
handler(id) {
|
|
1380
|
+
if (id !== "@pitlane/assets/manifest" && id !== "pitlane/assets/manifest") return;
|
|
1381
|
+
if (this.environment.mode === "build" && this.environment.name === "client") throw new Error("[assets] The asset manifest is server-only; keep resolver lookups out of browser modules.");
|
|
1382
|
+
if (this.environment.mode !== "build") return MANIFEST_ID;
|
|
1383
|
+
if (!state.serverEnvironments.includes(this.environment.name)) throw unservedEnvironmentError(this.environment.name, state.serverEnvironments);
|
|
1384
|
+
return {
|
|
1385
|
+
id: MANIFEST_EXTERNAL,
|
|
1386
|
+
external: true
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
},
|
|
1390
|
+
async transform(code, id) {
|
|
1391
|
+
let { mode, name } = this.environment;
|
|
1392
|
+
if (mode !== "build" || !state.serverEnvironments.includes(name)) return;
|
|
1393
|
+
let modules = state.resolverUsage.get(name);
|
|
1394
|
+
if (!modules) state.resolverUsage.set(name, modules = /* @__PURE__ */ new Map());
|
|
1395
|
+
let usage = scanResolverUsage(code, id);
|
|
1396
|
+
if (!usage) return void modules.delete(id);
|
|
1397
|
+
modules.set(id, await linkResolverUsage(usage, (specifier) => this.resolve(specifier, id)));
|
|
1398
|
+
}
|
|
1399
|
+
},
|
|
1400
|
+
assetBuild(state),
|
|
1401
|
+
assetDevelopment(state),
|
|
1402
|
+
assetStyles(),
|
|
1403
|
+
allowFiles && browserBoundary({
|
|
1404
|
+
allowFiles,
|
|
1405
|
+
allowPackages,
|
|
1406
|
+
denyFiles
|
|
1407
|
+
}, state.serverEnvironments)
|
|
1408
|
+
];
|
|
1409
|
+
}
|
|
1410
|
+
//#endregion
|
|
1411
|
+
export { ASSETS_MANIFEST_FILE, assets };
|