@pitlane/assets 0.0.0-stage → 0.1.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.
@@ -0,0 +1,968 @@
1
+ import { n as quotedList, t as assetsSpecifier } from "./specifier-DLC81sXY.mjs";
2
+ import { t as createAssetManifest } from "./build-BbA-Zq1m.mjs";
3
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { basename, dirname, isAbsolute, posix, relative, resolve } from "node:path";
5
+ import { isCSSRequest, normalizePath, parseSync } from "vite";
6
+ import { fileURLToPath } from "node:url";
7
+ import MagicString from "magic-string";
8
+ import { existsSync } from "node:fs";
9
+ //#region src/vite/entries.ts
10
+ function sourceKey(root, id) {
11
+ let file = id.split("?")[0].split("#")[0];
12
+ if (file.startsWith("file://")) file = fileURLToPath(file);
13
+ return normalizePath(relative(root, file));
14
+ }
15
+ function inputPath(root, key) {
16
+ let path = key.split("#")[0];
17
+ if (path.startsWith("file://")) return fileURLToPath(path);
18
+ if (path.startsWith("file:")) path = path.slice(5);
19
+ return resolve(root, path.replace(/^\/+/, ""));
20
+ }
21
+ function fileModule(id) {
22
+ return !id.startsWith("\0") && isAbsolute(id.split("?")[0]);
23
+ }
24
+ //#endregion
25
+ //#region src/vite/output.ts
26
+ function captureOutput(bundle, config, role, assetFiles, entryFiles) {
27
+ let graph = {
28
+ role,
29
+ chunks: Object.create(null),
30
+ entries: Object.create(null),
31
+ assets: Object.create(null)
32
+ };
33
+ let globalStylesheets = config.build.cssCodeSplit ? [] : Object.values(bundle).flatMap((output) => output.type === "asset" && output.originalFileNames.includes("style.css") ? [output.fileName] : []);
34
+ for (let output of Object.values(bundle)) {
35
+ if (output.type === "asset") {
36
+ let stylesheet = output.names.some(isCSSRequest);
37
+ for (let file of output.originalFileNames) {
38
+ if (stylesheet && !isCSSRequest(file)) continue;
39
+ let key = fileModule(file) ? sourceKey(config.root, file) : file.replace(/^\.\//, "");
40
+ graph.assets[key] = output.fileName;
41
+ }
42
+ continue;
43
+ }
44
+ graph.chunks[output.fileName] = {
45
+ file: output.fileName,
46
+ modules: [...new Set(output.moduleIds.filter(fileModule).map((id) => sourceKey(config.root, id)))],
47
+ imports: output.imports.filter((id) => bundle[id]?.type === "chunk"),
48
+ dynamicImports: output.dynamicImports.filter((id) => bundle[id]?.type === "chunk"),
49
+ stylesheets: [...globalStylesheets, ...output.viteMetadata?.importedCss ?? []]
50
+ };
51
+ if (role === "client" && output.isEntry && output.facadeModuleId && fileModule(output.facadeModuleId)) graph.entries[sourceKey(config.root, output.facadeModuleId)] = output.fileName;
52
+ }
53
+ for (let [key, file] of assetFiles) graph.assets[key] = file;
54
+ for (let [key, file] of entryFiles) if (bundle[file]?.type === "chunk") graph.entries[key] = file;
55
+ return graph;
56
+ }
57
+ function captureImportMap(bundle, fileName) {
58
+ let output = bundle[fileName];
59
+ if (output?.type !== "asset") throw new Error(`[assets] The client build did not emit its configured import map "${fileName}".`);
60
+ let text = typeof output.source === "string" ? output.source : new TextDecoder().decode(output.source);
61
+ return JSON.parse(text);
62
+ }
63
+ /**
64
+ * The public URL `renderBuiltUrl` chooses for a built file. A server document
65
+ * has no importer to be relative to, so `{ relative: true }` keeps the
66
+ * base-joined path.
67
+ */
68
+ function publicFile(config, file) {
69
+ let render = config.experimental.renderBuiltUrl;
70
+ if (!render) return file;
71
+ let rendered = render(file, {
72
+ type: "asset",
73
+ hostType: "html",
74
+ hostId: "",
75
+ ssr: true
76
+ });
77
+ if (typeof rendered === "string") return rendered;
78
+ 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.");
79
+ return file;
80
+ }
81
+ //#endregion
82
+ //#region src/vite/binding-names.ts
83
+ /** Calls `visit` on every AST node below `root`, in no particular order. */
84
+ function forEachNode(root, visit) {
85
+ let pending = [root];
86
+ while (pending.length) {
87
+ let node = pending.pop();
88
+ if (!node || typeof node !== "object") continue;
89
+ if (Array.isArray(node)) {
90
+ pending.push(...node);
91
+ continue;
92
+ }
93
+ visit(node);
94
+ for (let child of Object.values(node)) if (child && typeof child === "object") pending.push(child);
95
+ }
96
+ }
97
+ function patternNames(pattern) {
98
+ switch (pattern?.type) {
99
+ case "Identifier": return [pattern.name];
100
+ case "AssignmentPattern": return patternNames(pattern.left);
101
+ case "RestElement": return patternNames(pattern.argument);
102
+ case "TSParameterProperty": return patternNames(pattern.parameter);
103
+ case "ArrayPattern": return pattern.elements.flatMap(patternNames);
104
+ case "ObjectPattern": return pattern.properties.flatMap((property) => patternNames(property.type === "RestElement" ? property : property.value));
105
+ default: return [];
106
+ }
107
+ }
108
+ /** The names a declaration binds in the scope that contains it. */
109
+ function declaredNames(node) {
110
+ switch (node?.type) {
111
+ case "VariableDeclaration": return node.declarations.flatMap((declarator) => patternNames(declarator.id));
112
+ case "FunctionDeclaration":
113
+ case "TSDeclareFunction":
114
+ case "ClassDeclaration":
115
+ case "TSEnumDeclaration":
116
+ case "TSImportEqualsDeclaration": return node.id ? [node.id.name] : [];
117
+ case "TSModuleDeclaration": return node.id.type === "Identifier" ? [node.id.name] : [];
118
+ default: return [];
119
+ }
120
+ }
121
+ function boundNames(node) {
122
+ switch (node.type) {
123
+ case "VariableDeclaration": return declaredNames(node);
124
+ case "FunctionDeclaration":
125
+ case "FunctionExpression":
126
+ case "ArrowFunctionExpression": {
127
+ let parameters = node.params.flatMap(patternNames);
128
+ return node.type !== "ArrowFunctionExpression" && node.id ? [node.id.name, ...parameters] : parameters;
129
+ }
130
+ case "ClassExpression": return node.id ? [node.id.name] : [];
131
+ case "CatchClause": return patternNames(node.param);
132
+ case "ImportSpecifier":
133
+ case "ImportDefaultSpecifier":
134
+ case "ImportNamespaceSpecifier": return [node.local.name];
135
+ default: return declaredNames(node);
136
+ }
137
+ }
138
+ /** How many times a module binds each name, counting every declaration in every scope. */
139
+ function bindingCounts(program) {
140
+ let counts = /* @__PURE__ */ new Map();
141
+ forEachNode(program, (node) => {
142
+ for (let name of boundNames(node)) counts.set(name, (counts.get(name) ?? 0) + 1);
143
+ });
144
+ return counts;
145
+ }
146
+ //#endregion
147
+ //#region src/vite/resolver-bindings.ts
148
+ function moduleExportName(node) {
149
+ return node.type === "Literal" ? node.value : node.name;
150
+ }
151
+ function constructs(node, constructors) {
152
+ while (node?.type === "AwaitExpression" || node?.type === "ParenthesizedExpression") node = node.type === "AwaitExpression" ? node.argument : node.expression;
153
+ return node?.type === "CallExpression" && node.callee.type === "Identifier" && constructors.has(node.callee.name);
154
+ }
155
+ /** The name a chain of top-level `const a = b` copies starts from. */
156
+ function aliasRoot(name, initializers) {
157
+ let seen = /* @__PURE__ */ new Set();
158
+ let init = initializers.get(name);
159
+ while (init?.type === "Identifier" && !seen.has(name)) {
160
+ seen.add(name);
161
+ name = init.name;
162
+ init = initializers.get(name);
163
+ }
164
+ return name;
165
+ }
166
+ function topLevelBindings(program) {
167
+ let counts = bindingCounts(program);
168
+ let importedConstructors = /* @__PURE__ */ new Set();
169
+ let imports = /* @__PURE__ */ new Map();
170
+ let initializers = /* @__PURE__ */ new Map();
171
+ for (let statement of program.body) {
172
+ if (statement.type === "ImportDeclaration") {
173
+ let module = statement.source.value;
174
+ for (let specifier of statement.specifiers) {
175
+ let local = specifier.local.name;
176
+ if (specifier.type === "ImportNamespaceSpecifier" || counts.get(local) !== 1) continue;
177
+ let name = specifier.type === "ImportDefaultSpecifier" ? "default" : moduleExportName(specifier.imported);
178
+ if (name === "createAssetResolver" && (module === "@pitlane/assets" || module === "pitlane/assets")) importedConstructors.add(local);
179
+ else imports.set(local, {
180
+ module,
181
+ name
182
+ });
183
+ }
184
+ }
185
+ let declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
186
+ if (declaration?.type !== "VariableDeclaration") continue;
187
+ for (let { id, init } of declaration.declarations) if (id.type === "Identifier" && init && counts.get(id.name) === 1) initializers.set(id.name, init);
188
+ }
189
+ let constructors = new Set(importedConstructors);
190
+ for (let name of initializers.keys()) if (importedConstructors.has(aliasRoot(name, initializers))) constructors.add(name);
191
+ let resolvers = /* @__PURE__ */ new Map();
192
+ for (let name of [...imports.keys(), ...initializers.keys()]) {
193
+ let root = aliasRoot(name, initializers);
194
+ let origin = imports.get(root) ?? (constructs(initializers.get(root), constructors) ? "local" : void 0);
195
+ if (origin) resolvers.set(name, origin);
196
+ }
197
+ return {
198
+ constructors,
199
+ resolvers
200
+ };
201
+ }
202
+ /** Where the resolver an expression evaluates to comes from, when the expression can name one. */
203
+ function resolverOrigin(node, bindings) {
204
+ if (constructs(node, bindings.constructors)) return "local";
205
+ if (node.type === "Identifier") return bindings.resolvers.get(node.name);
206
+ }
207
+ //#endregion
208
+ //#region src/vite/resolver-usage.ts
209
+ const RELEVANT = /\bget(?:ScriptEntry|Href)\b|\bcreateAssetResolver\b|\bexport\b/;
210
+ /** The method a member call names, through `.name` or a string-literal `["name"]`. */
211
+ function memberName(callee) {
212
+ let { property } = callee;
213
+ if (!callee.computed) return property.type === "Identifier" ? property.name : void 0;
214
+ return property.type === "Literal" && typeof property.value === "string" ? property.value : void 0;
215
+ }
216
+ function literalCall(call, bindings) {
217
+ let { callee, arguments: args } = call;
218
+ if (callee.type !== "MemberExpression") return;
219
+ let method = memberName(callee);
220
+ let argument = args[0];
221
+ if (method !== "getScriptEntry" && method !== "getHref") return;
222
+ if (argument?.type !== "Literal" || typeof argument.value !== "string") return;
223
+ let receiver = resolverOrigin(callee.object, bindings);
224
+ if (!receiver) return;
225
+ return {
226
+ input: {
227
+ key: argument.value,
228
+ kind: method === "getScriptEntry" ? "script" : "asset"
229
+ },
230
+ receiver
231
+ };
232
+ }
233
+ function resolverExports(program, bindings) {
234
+ let exported = /* @__PURE__ */ new Map();
235
+ for (let statement of program.body) {
236
+ if (statement.type === "ExportDefaultDeclaration") exported.set("default", resolverOrigin(statement.declaration, bindings) ?? null);
237
+ if (statement.type === "ExportAllDeclaration" && statement.exported) exported.set(moduleExportName(statement.exported), null);
238
+ if (statement.type !== "ExportNamedDeclaration") continue;
239
+ for (let name of declaredNames(statement.declaration)) exported.set(name, bindings.resolvers.get(name) ?? null);
240
+ for (let specifier of statement.specifiers) {
241
+ let found = statement.source ? {
242
+ module: statement.source.value,
243
+ name: moduleExportName(specifier.local)
244
+ } : resolverOrigin(specifier.local, bindings);
245
+ exported.set(moduleExportName(specifier.exported), found ?? null);
246
+ }
247
+ }
248
+ return exported;
249
+ }
250
+ /**
251
+ * A module's literal `getScriptEntry` and `getHref` calls whose receiver may be
252
+ * a resolver, and its exports that may name one. Import specifiers stay
253
+ * unresolved; pass the result to {@link linkResolverUsage}.
254
+ */
255
+ function scanResolverUsage(code, id) {
256
+ if (!RELEVANT.test(code)) return;
257
+ let { program } = parseSync(id, code);
258
+ let bindings = topLevelBindings(program);
259
+ let calls = [];
260
+ forEachNode(program, (node) => {
261
+ let call = node.type === "CallExpression" ? literalCall(node, bindings) : void 0;
262
+ if (call) calls.push(call);
263
+ });
264
+ let exported = resolverExports(program, bindings);
265
+ let starExports = program.body.flatMap((statement) => statement.type === "ExportAllDeclaration" && !statement.exported ? [statement.source.value] : []);
266
+ let namesResolver = [...exported.values()].some((origin) => origin !== null);
267
+ if (calls.length === 0 && !namesResolver && starExports.length === 0) return;
268
+ return {
269
+ calls,
270
+ resolverExports: exported,
271
+ starExports
272
+ };
273
+ }
274
+ /** Replaces import specifiers with module ids, dropping those that are external or unresolved. */
275
+ async function linkResolverUsage(usage, resolve) {
276
+ let specifiers = new Set(usage.starExports);
277
+ for (let origin of [...usage.calls.map((call) => call.receiver), ...usage.resolverExports.values()]) if (origin && origin !== "local") specifiers.add(origin.module);
278
+ let resolved = /* @__PURE__ */ new Map();
279
+ await Promise.all([...specifiers].map(async (specifier) => {
280
+ let module = await resolve(specifier);
281
+ if (module && !module.external) resolved.set(specifier, module.id);
282
+ }));
283
+ let link = (origin) => {
284
+ if (origin === "local") return origin;
285
+ let module = resolved.get(origin.module);
286
+ return module === void 0 ? void 0 : {
287
+ module,
288
+ name: origin.name
289
+ };
290
+ };
291
+ let calls = [];
292
+ for (let call of usage.calls) {
293
+ let receiver = link(call.receiver);
294
+ if (receiver) calls.push({
295
+ input: call.input,
296
+ receiver
297
+ });
298
+ }
299
+ let exported = /* @__PURE__ */ new Map();
300
+ for (let [name, origin] of usage.resolverExports) exported.set(name, origin && (link(origin) ?? null));
301
+ return {
302
+ calls,
303
+ resolverExports: exported,
304
+ starExports: usage.starExports.flatMap((specifier) => resolved.get(specifier) ?? [])
305
+ };
306
+ }
307
+ function isResolver(modules, origin, seen) {
308
+ if (origin === "local") return true;
309
+ let binding = `${origin.module}\0${origin.name}`;
310
+ if (seen.has(binding)) return false;
311
+ seen.add(binding);
312
+ let usage = modules.get(origin.module);
313
+ let exported = usage?.resolverExports.get(origin.name);
314
+ if (!usage || exported === null) return false;
315
+ if (exported) return isResolver(modules, exported, seen);
316
+ return origin.name !== "default" && usage.starExports.some((module) => isResolver(modules, {
317
+ module,
318
+ name: origin.name
319
+ }, seen));
320
+ }
321
+ /** The browser inputs a module's literal calls register, given its environment's linked usage. */
322
+ function literalInputs(modules, id) {
323
+ return (modules.get(id)?.calls ?? []).filter((call) => isResolver(modules, call.receiver, /* @__PURE__ */ new Set())).map((call) => call.input);
324
+ }
325
+ //#endregion
326
+ //#region src/vite/state.ts
327
+ const MANIFEST_FILE = "__pitlane_assets_manifest.js";
328
+ const MANIFEST_ID = "\0pitlane:assets-manifest";
329
+ const MANIFEST_EXTERNAL = "pitlane:assets-build-manifest";
330
+ const EMPTY_INPUT = "\0pitlane:assets-empty-input";
331
+ /** The manifest was imported from a server environment `assets()` does not serve. */
332
+ function unservedEnvironmentError(name, served) {
333
+ 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")}.`);
334
+ }
335
+ //#endregion
336
+ //#region src/vite/build.ts
337
+ async function writeManifests(state) {
338
+ let config = state.config;
339
+ let client = state.outputs.get("client");
340
+ if (!client || state.serverEnvironments.some((name) => !state.outputs.has(name))) return;
341
+ let clientStylesheets = /* @__PURE__ */ new Map();
342
+ 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);
343
+ let environments = Object.create(null);
344
+ let twins = /* @__PURE__ */ new Map();
345
+ for (let [name, output] of state.outputs) {
346
+ let graph = output.graph;
347
+ let twin = /* @__PURE__ */ new Map();
348
+ if (output !== client) for (let asset of Object.values(output.bundle)) {
349
+ if (asset.type !== "asset" || !isCSSRequest(asset.fileName)) continue;
350
+ let file = clientStylesheets.get(Buffer.from(asset.source).toString("latin1"));
351
+ if (file) twin.set(asset.fileName, file);
352
+ }
353
+ twins.set(name, twin);
354
+ environments[name] = {
355
+ ...graph,
356
+ chunks: Object.fromEntries(Object.entries(graph.chunks).map(([key, chunk]) => [key, {
357
+ ...chunk,
358
+ file: publicFile(config, chunk.file),
359
+ stylesheets: chunk.stylesheets.map((file) => publicFile(config, twin.get(file) ?? file))
360
+ }])),
361
+ assets: Object.fromEntries(Object.entries(graph.assets).map(([key, file]) => [key, publicFile(config, twin.get(file) ?? file)]))
362
+ };
363
+ }
364
+ for (let name of state.serverEnvironments) {
365
+ let manifest = createAssetManifest({
366
+ base: config.base,
367
+ environments: {
368
+ client: environments.client,
369
+ [name]: environments[name]
370
+ },
371
+ importMap: state.importMap
372
+ });
373
+ let output = state.outputs.get(name);
374
+ await mkdir(output.outDir, { recursive: true });
375
+ await writeFile(resolve(output.outDir, MANIFEST_FILE), `export default JSON.parse(${JSON.stringify(JSON.stringify({
376
+ ...manifest,
377
+ serverEnvironment: name
378
+ }))});\n`);
379
+ for (let asset of Object.values(output.bundle)) {
380
+ if (asset.type !== "asset" || twins.get(name).has(asset.fileName)) continue;
381
+ let destination = resolve(client.outDir, asset.fileName);
382
+ await mkdir(dirname(destination), { recursive: true });
383
+ await writeFile(destination, asset.source);
384
+ }
385
+ }
386
+ }
387
+ function importMapFileName(config) {
388
+ let options = config.build.rolldownOptions.experimental?.chunkImportMap;
389
+ return typeof options === "object" ? options.fileName ?? "importmap.json" : "importmap.json";
390
+ }
391
+ function assetBuild(state) {
392
+ return {
393
+ name: "pitlane-assets-build",
394
+ sharedDuringBuild: true,
395
+ apply: "build",
396
+ configEnvironment(name, config) {
397
+ if (state.serverEnvironments.includes(name)) return { build: { emitAssets: true } };
398
+ if (name === "client" && !config.build?.rolldownOptions?.input && !config.build?.rollupOptions?.input) return { build: { rolldownOptions: { input: EMPTY_INPUT } } };
399
+ },
400
+ resolveId(id) {
401
+ if (id === "\0pitlane:assets-empty-input") return id;
402
+ },
403
+ load(id) {
404
+ if (id === "\0pitlane:assets-empty-input") return "export {};";
405
+ },
406
+ async buildStart() {
407
+ if (this.environment.name !== "client") return;
408
+ let missing = state.serverEnvironments.filter((name) => !state.outputs.has(name));
409
+ if (missing.length) throw new Error(`[assets] Client built before server environments: ${missing.join(", ")}. Build servers before client.`);
410
+ let root = state.config.root;
411
+ state.assetReferences.clear();
412
+ state.scriptReferences.clear();
413
+ let inputs = new Map(state.inputs);
414
+ for (let modules of state.resolverUsage.values()) for (let owner of modules.keys()) for (let input of literalInputs(modules, owner)) {
415
+ let file = inputPath(root, input.key);
416
+ inputs.set(file, {
417
+ ...input,
418
+ key: sourceKey(root, file)
419
+ });
420
+ }
421
+ for (let input of inputs.values()) {
422
+ let id = inputPath(state.config.root, input.key);
423
+ if (input.kind === "asset" && !isCSSRequest(id) && state.config.assetsInclude(id)) {
424
+ let reference = this.emitFile({
425
+ type: "asset",
426
+ name: basename(id),
427
+ originalFileName: id,
428
+ source: await readFile(id)
429
+ });
430
+ state.assetReferences.set(sourceKey(root, id), reference);
431
+ } else {
432
+ let reference = this.emitFile({
433
+ type: "chunk",
434
+ id,
435
+ preserveSignature: "exports-only"
436
+ });
437
+ if (!isCSSRequest(id)) state.scriptReferences.set(sourceKey(root, id), reference);
438
+ }
439
+ }
440
+ },
441
+ renderChunk(code, chunk) {
442
+ if (!code.includes("pitlane:assets-build-manifest")) return;
443
+ let file = posix.relative(posix.dirname(chunk.fileName), MANIFEST_FILE);
444
+ if (!file.startsWith(".")) file = `./${file}`;
445
+ return {
446
+ code: code.replaceAll(MANIFEST_EXTERNAL, file),
447
+ map: null
448
+ };
449
+ },
450
+ generateBundle: {
451
+ order: "post",
452
+ handler(_options, bundle) {
453
+ let removed = /* @__PURE__ */ new Set();
454
+ for (let [name, output] of Object.entries(bundle)) if (output.type === "chunk" && output.facadeModuleId === "\0pitlane:assets-empty-input") {
455
+ removed.add(name);
456
+ delete bundle[name];
457
+ }
458
+ if (this.environment.name !== "client" || !state.mapsEnabled) return;
459
+ let fileName = importMapFileName(this.environment.config);
460
+ let map = captureImportMap(bundle, fileName);
461
+ let base = this.environment.config.base;
462
+ for (let [key, href] of Object.entries(map.imports)) {
463
+ if (!href.startsWith(base)) continue;
464
+ let file = href.slice(base.length);
465
+ if (removed.has(file) || !(file in bundle)) delete map.imports[key];
466
+ }
467
+ let artifact = bundle[fileName];
468
+ if (artifact.type === "asset") artifact.source = JSON.stringify(map);
469
+ }
470
+ },
471
+ writeBundle: {
472
+ order: "post",
473
+ async handler(_options, bundle) {
474
+ let name = this.environment.name;
475
+ if (name !== "client" && !state.serverEnvironments.includes(name)) return;
476
+ let config = this.environment.config;
477
+ let assets = /* @__PURE__ */ new Map();
478
+ let scripts = /* @__PURE__ */ new Map();
479
+ if (name === "client") {
480
+ for (let [key, reference] of state.assetReferences) assets.set(key, this.getFileName(reference));
481
+ for (let [key, reference] of state.scriptReferences) scripts.set(key, this.getFileName(reference));
482
+ state.importMap = state.mapsEnabled ? captureImportMap(bundle, importMapFileName(config)) : void 0;
483
+ }
484
+ state.outputs.set(name, {
485
+ graph: captureOutput(bundle, config, name === "client" ? "client" : "server", assets, scripts),
486
+ bundle,
487
+ outDir: resolve(config.root, config.build.outDir)
488
+ });
489
+ await writeManifests(state);
490
+ }
491
+ },
492
+ buildApp: {
493
+ order: "pre",
494
+ async handler(builder) {
495
+ let build = builder.build.bind(builder);
496
+ builder.build = (async (environment) => {
497
+ if (environment.isBuilt) return;
498
+ return build(environment);
499
+ });
500
+ for (let name of state.serverEnvironments) {
501
+ let environment = builder.environments[name];
502
+ if (!environment) throw new Error(`[assets] Unknown server environment: ${name}`);
503
+ await builder.build(environment);
504
+ }
505
+ await builder.build(builder.environments.client);
506
+ }
507
+ }
508
+ };
509
+ }
510
+ //#endregion
511
+ //#region src/vite/dev-css.ts
512
+ function linkedStylesheetLookup(config) {
513
+ return `
514
+ function pitlaneStyleKey(href) {
515
+ const url = new URL(href, document.baseURI);
516
+ url.searchParams.delete("t");
517
+ url.searchParams.delete("direct");
518
+ url.searchParams.sort();
519
+ return url.href;
520
+ }
521
+ function pitlaneHasLinkedStylesheet(id) {
522
+ const root = ${JSON.stringify(config.root + "/")};
523
+ const path = id.startsWith(root) ? id.slice(root.length) : "@fs/" + id.replace(/^\\//, "");
524
+ const key = pitlaneStyleKey(${JSON.stringify(config.base)} + path);
525
+ for (const link of document.querySelectorAll('link[rel="stylesheet"][href]')) {
526
+ if (pitlaneStyleKey(link.href) === key) return true;
527
+ }
528
+ return false;
529
+ }
530
+ `;
531
+ }
532
+ function assetStyles() {
533
+ let config;
534
+ let clientPath = normalizePath(fileURLToPath(import.meta.resolve("vite/dist/client/client.mjs")));
535
+ return {
536
+ name: "pitlane-assets-css",
537
+ apply: "serve",
538
+ configResolved(resolved) {
539
+ config = resolved;
540
+ },
541
+ configureServer(server) {
542
+ server.middlewares.use((request, _response, next) => {
543
+ if (request.headers.accept?.includes("text/css") && request.url?.includes("&lang.css=")) request.url = request.url.replace("&lang.css=", "?lang.css");
544
+ next();
545
+ });
546
+ },
547
+ transform: {
548
+ order: "post",
549
+ handler(code, id) {
550
+ if (id === clientPath) {
551
+ let position = code.indexOf("function updateStyle(id, content) {");
552
+ if (position === -1) throw new Error("[assets] This Vite client does not expose the expected CSS update function. Use a supported Vite version.");
553
+ let transformed = new MagicString(code);
554
+ transformed.prepend(linkedStylesheetLookup(config));
555
+ transformed.appendLeft(position + 35, "\nif (pitlaneHasLinkedStylesheet(id)) return;\n");
556
+ return {
557
+ code: transformed.toString(),
558
+ map: transformed.generateMap({ hires: true })
559
+ };
560
+ }
561
+ if (this.environment.mode !== "dev" || this.environment.name !== "client" || !isCSSRequest(id) || !/[?&]direct\b/.test(id)) return;
562
+ let module = this.environment.moduleGraph.getModuleById(id);
563
+ if (module) module.isSelfAccepting = true;
564
+ }
565
+ }
566
+ };
567
+ }
568
+ //#endregion
569
+ //#region src/vite/dev-graph.ts
570
+ /** CSS requests that import a string or URL rather than apply a stylesheet. */
571
+ const NON_STYLESHEET_QUERY = /[?&](?:inline|url|raw)\b/;
572
+ /**
573
+ * The import specifiers of transformed JavaScript. Only literal `import()`
574
+ * arguments count as dynamic edges; a computed argument names no module.
575
+ */
576
+ function importSpecifiers(code) {
577
+ let { module } = parseSync("module.js", code, { sourceType: "module" });
578
+ let staticSpecifiers = new Set(module.staticImports.map((entry) => entry.moduleRequest.value));
579
+ for (let entry of module.staticExports) for (let item of entry.entries) if (item.moduleRequest) staticSpecifiers.add(item.moduleRequest.value);
580
+ let dynamicSpecifiers = /* @__PURE__ */ new Set();
581
+ for (let entry of module.dynamicImports) {
582
+ let literal = /^(["'`])([^"'`$]*)\1$/.exec(code.slice(entry.moduleRequest.start, entry.moduleRequest.end).trim());
583
+ if (literal) dynamicSpecifiers.add(literal[2]);
584
+ }
585
+ return {
586
+ static: [...staticSpecifiers],
587
+ dynamic: [...dynamicSpecifiers]
588
+ };
589
+ }
590
+ /** The path Vite's dev server serves a resolved module id at, before `base`. */
591
+ function servedPath(root, id) {
592
+ if (id.startsWith("\0")) return `/@id/__x00__${id.slice(1)}`;
593
+ let query = id.indexOf("?");
594
+ let file = query === -1 ? id : id.slice(0, query);
595
+ let search = query === -1 ? "" : id.slice(query);
596
+ if (!isAbsolute(file)) return `/@id/${id}`;
597
+ if (file.startsWith(`${root}/`)) return file.slice(root.length) + search;
598
+ return `/@fs/${file.replace(/^\//, "")}${search}`;
599
+ }
600
+ /**
601
+ * Whether discovery transforms a module. A server environment reads a package
602
+ * from `node_modules` only when Vite transforms rather than externalizes it.
603
+ * The client follows packages Vite serves as source, but not a prebundled
604
+ * dependency: the optimizer's cache file is not a source module, and loading
605
+ * it would wait for the optimizer.
606
+ */
607
+ function followed(environment, id) {
608
+ if (id === "\0pitlane:assets-manifest" || environment.depsOptimizer?.isOptimizedDepFile(id)) return false;
609
+ let pkg = /.*\/node_modules\/((?:@[^/]+\/)?[^/?]+)/.exec(id)?.[1];
610
+ if (pkg === void 0 || environment.config.consumer === "client") return true;
611
+ let { noExternal } = environment.config.resolve;
612
+ if (noExternal === true) return true;
613
+ return (noExternal === void 0 ? [] : Array.isArray(noExternal) ? noExternal : [noExternal]).some((pattern) => typeof pattern === "string" ? pattern === pkg : pattern.test(pkg));
614
+ }
615
+ /**
616
+ * Server discovery must not populate `transformResult`: the module runner
617
+ * would treat its stale instance as current. Client transforms may warm
618
+ * and cache their dependencies after the optimizer has initialized.
619
+ */
620
+ async function analyze(environment, id, edges) {
621
+ if (environment.moduleGraph.getModuleById(id)?.transformResult && edges.has(id)) return;
622
+ let { root } = environment.config;
623
+ try {
624
+ await environment.moduleGraph.ensureEntryFromUrl(servedPath(root, id));
625
+ let loaded = await environment.pluginContainer.load(id);
626
+ let code = loaded == null ? null : typeof loaded === "string" ? loaded : loaded.code;
627
+ if (code == null) {
628
+ if (!fileModule(id)) throw new Error("No plugin loaded the module.");
629
+ code = await readFile(id.split("?")[0], "utf8");
630
+ }
631
+ await environment.pluginContainer.transform(code, id);
632
+ } catch (error) {
633
+ let name = fileModule(id) ? sourceKey(root, id) : id;
634
+ throw new Error(`[assets] Could not analyze "${name}" for the "${environment.name}" development asset manifest: ${error.message}`, { cause: error });
635
+ }
636
+ }
637
+ /**
638
+ * Every module reachable from `roots` through static and dynamic edges,
639
+ * transforming each one the graph has not analyzed yet. Stylesheets are
640
+ * leaves: their own `@import`s are inlined by Vite's CSS pipeline. Passing
641
+ * the result of an earlier walk extends it without revisiting its modules.
642
+ */
643
+ async function walkGraph(environment, roots, edges, visited = /* @__PURE__ */ new Set()) {
644
+ let frontier = [...roots];
645
+ while (frontier.length > 0) {
646
+ let next = [];
647
+ await Promise.all(frontier.map(async (id) => {
648
+ if (visited.has(id) || !(isCSSRequest(id) || followed(environment, id))) return;
649
+ visited.add(id);
650
+ if (isCSSRequest(id)) return;
651
+ await analyze(environment, id, edges);
652
+ let record = edges.get(id);
653
+ if (record) next.push(...record.static, ...record.dynamic);
654
+ }));
655
+ frontier = next;
656
+ }
657
+ return visited;
658
+ }
659
+ /**
660
+ * The stylesheet ids a module requires through static imports, in execution
661
+ * order: each dependency's stylesheets before its importer's later imports.
662
+ */
663
+ function staticStylesheets(edges, start) {
664
+ let seen = /* @__PURE__ */ new Set();
665
+ let stylesheets = [];
666
+ let visit = (id) => {
667
+ if (seen.has(id)) return;
668
+ seen.add(id);
669
+ if (isCSSRequest(id)) {
670
+ if (!NON_STYLESHEET_QUERY.test(id)) stylesheets.push(id);
671
+ return;
672
+ }
673
+ for (let dependency of edges.get(id)?.static ?? []) visit(dependency);
674
+ };
675
+ visit(start);
676
+ return stylesheets;
677
+ }
678
+ //#endregion
679
+ //#region src/vite/dev-snapshot.ts
680
+ function environmentEdges(graph, name) {
681
+ let edges = graph.edges.get(name);
682
+ if (!edges) graph.edges.set(name, edges = /* @__PURE__ */ new Map());
683
+ return edges;
684
+ }
685
+ /**
686
+ * Whether a module id is a source file with a key of its own. Vite's `?v=`
687
+ * version hash on a package module does not change which file it is; any
688
+ * other query, such as a single-file component's style block, names a part
689
+ * of its file that the file's own key already covers.
690
+ */
691
+ function sourceModule(id) {
692
+ return fileModule(id) && !id.replace(/\?v=[\w-]+$/, "").includes("?");
693
+ }
694
+ /** The resolved, non-HTML build inputs an environment declares. */
695
+ async function configuredInputs(environment) {
696
+ let { build, root } = environment.config;
697
+ let input = build.rolldownOptions.input;
698
+ let inputs = typeof input === "string" ? [input] : Array.isArray(input) ? input : Object.values(input ?? {});
699
+ let ids = [];
700
+ for (let entry of inputs) {
701
+ if (entry.endsWith(".html")) continue;
702
+ let file = resolve(root, entry);
703
+ let resolved = await environment.pluginContainer.resolveId(existsSync(file) ? file : entry);
704
+ if (!resolved) throw new Error(`[assets] Could not resolve the "${environment.name}" environment's input "${entry}".`);
705
+ ids.push(resolved.id);
706
+ }
707
+ return ids;
708
+ }
709
+ async function createDevSnapshot(server, state, graph, name) {
710
+ let config = state.config;
711
+ let { root } = config;
712
+ let base = config.base.replace(/\/$/, "");
713
+ let serverEnvironment = server.environments[name];
714
+ let clientEnvironment = server.environments.client;
715
+ let serverEdges = environmentEdges(graph, name);
716
+ let serverIds = await walkGraph(serverEnvironment, [...await configuredInputs(serverEnvironment), ...graph.roots.get(name) ?? []], serverEdges);
717
+ await clientEnvironment.depsOptimizer?.init();
718
+ let entryIds = /* @__PURE__ */ new Set([...state.inputs.keys(), ...await configuredInputs(clientEnvironment)]);
719
+ let usage = graph.resolverUsage.get(name) ?? /* @__PURE__ */ new Map();
720
+ let registrations = state.registrations.get(name);
721
+ for (let id of serverIds) {
722
+ for (let input of literalInputs(usage, id)) entryIds.add(inputPath(root, input.key));
723
+ for (let entry of registrations?.get(id) ?? []) entryIds.add(entry);
724
+ }
725
+ let entries = Object.create(null);
726
+ let assets = Object.create(null);
727
+ let scripts = [];
728
+ for (let id of entryIds) {
729
+ let url = base + servedPath(root, id);
730
+ if (isCSSRequest(id) || config.assetsInclude(id.split("?")[0])) assets[sourceKey(root, id)] = url;
731
+ else {
732
+ entries[sourceKey(root, id)] = url;
733
+ scripts.push(id);
734
+ }
735
+ }
736
+ let clientIds = await walkGraph(clientEnvironment, scripts, environmentEdges(graph, "client"));
737
+ let serverModules = Object.create(null);
738
+ for (let id of serverIds) {
739
+ if (!sourceModule(id)) continue;
740
+ serverModules[sourceKey(root, id)] = {
741
+ preloads: [],
742
+ stylesheets: staticStylesheets(serverEdges, id).map((stylesheet) => base + servedPath(root, stylesheet))
743
+ };
744
+ }
745
+ let clientModules = Object.create(null);
746
+ for (let id of clientIds) if (sourceModule(id)) clientModules[sourceKey(root, id)] = {
747
+ preloads: [],
748
+ stylesheets: []
749
+ };
750
+ return {
751
+ manifest: {
752
+ mode: "dev",
753
+ environments: {
754
+ client: {
755
+ role: "client",
756
+ modules: clientModules
757
+ },
758
+ [name]: {
759
+ role: "server",
760
+ modules: serverModules
761
+ }
762
+ },
763
+ entries,
764
+ assets,
765
+ serverEnvironment: name
766
+ },
767
+ server: serverIds,
768
+ client: clientIds
769
+ };
770
+ }
771
+ //#endregion
772
+ //#region src/vite/dev.ts
773
+ /**
774
+ * Serves `@pitlane/assets/manifest` during `vite dev` as a data snapshot of
775
+ * each server environment's module graph, and invalidates it when a change
776
+ * could alter the stylesheets, routes, or browser entries it records.
777
+ */
778
+ function assetDevelopment(state) {
779
+ let server;
780
+ let graph = {
781
+ roots: /* @__PURE__ */ new Map(),
782
+ edges: /* @__PURE__ */ new Map(),
783
+ resolverUsage: /* @__PURE__ */ new Map()
784
+ };
785
+ let snapshots = /* @__PURE__ */ new Map();
786
+ let building = /* @__PURE__ */ new Set();
787
+ /** Invalidates a server environment's manifest and, through Vite, every module importing it. */
788
+ function invalidate(name, timestamp) {
789
+ let environment = server?.environments[name];
790
+ let manifest = environment?.moduleGraph.getModuleById(MANIFEST_ID);
791
+ if (manifest) environment.moduleGraph.invalidateModule(manifest, /* @__PURE__ */ new Set(), timestamp, true);
792
+ }
793
+ return {
794
+ name: "pitlane-assets-dev",
795
+ apply: "serve",
796
+ enforce: "post",
797
+ configureServer: {
798
+ order: "pre",
799
+ handler(devServer) {
800
+ server = devServer;
801
+ for (let environment of Object.values(devServer.environments)) {
802
+ let { name } = environment;
803
+ if (!state.serverEnvironments.includes(name)) continue;
804
+ let roots = /* @__PURE__ */ new Set();
805
+ graph.roots.set(name, roots);
806
+ let fetchModule = environment.fetchModule;
807
+ environment.fetchModule = async (id, importer, options) => {
808
+ let fetched = await fetchModule.call(environment, id, importer, options);
809
+ if (!importer && "id" in fetched && fetched.id !== "\0pitlane:assets-manifest") {
810
+ roots.add(fetched.id);
811
+ let snapshot = snapshots.get(name);
812
+ if (snapshot && !snapshot.server.has(fetched.id)) invalidate(name);
813
+ }
814
+ return fetched;
815
+ };
816
+ }
817
+ state.onRegistrationChange = (environment, owner) => {
818
+ if (building.has(environment)) return;
819
+ if (snapshots.get(environment)?.server.has(owner)) invalidate(environment);
820
+ };
821
+ }
822
+ },
823
+ async load(id) {
824
+ if (id !== "\0pitlane:assets-manifest") return;
825
+ let { name, config } = this.environment;
826
+ if (config.consumer === "client") {
827
+ 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.`;
828
+ return `throw new Error(${JSON.stringify(message)});\nexport default undefined;\n`;
829
+ }
830
+ if (!state.serverEnvironments.includes(name)) throw unservedEnvironmentError(name, state.serverEnvironments);
831
+ building.add(name);
832
+ try {
833
+ let snapshot = await createDevSnapshot(server, state, graph, name);
834
+ snapshots.set(name, snapshot);
835
+ return `export default JSON.parse(${JSON.stringify(JSON.stringify(snapshot.manifest))});\n`;
836
+ } finally {
837
+ building.delete(name);
838
+ }
839
+ },
840
+ transform: { async handler(code, id) {
841
+ if (id === "\0pitlane:assets-manifest" || isCSSRequest(id)) return;
842
+ let name = this.environment.name;
843
+ let specifiers = importSpecifiers(code);
844
+ let resolveAll = async (list) => {
845
+ let resolved = await Promise.all(list.map((specifier) => this.resolve(specifier, id)));
846
+ return [...new Set(resolved.flatMap((result) => result && !result.external ? [result.id] : []))];
847
+ };
848
+ let [staticEdges, dynamicEdges] = await Promise.all([resolveAll(specifiers.static), resolveAll(specifiers.dynamic)]);
849
+ environmentEdges(graph, name).set(id, {
850
+ static: staticEdges,
851
+ dynamic: dynamicEdges
852
+ });
853
+ if (!state.serverEnvironments.includes(name)) return;
854
+ let modules = graph.resolverUsage.get(name);
855
+ if (!modules) graph.resolverUsage.set(name, modules = /* @__PURE__ */ new Map());
856
+ let usage = scanResolverUsage(code, id);
857
+ if (!usage) return void modules.delete(id);
858
+ modules.set(id, await linkResolverUsage(usage, (specifier) => this.resolve(specifier, id)));
859
+ } },
860
+ hotUpdate: {
861
+ order: "post",
862
+ handler({ type, file, modules, timestamp }) {
863
+ if (type === "update" && isCSSRequest(file)) return;
864
+ let path = normalizePath(file);
865
+ if (type === "delete") {
866
+ for (let roots of graph.roots.values()) for (let root of roots) if (root.split("?")[0] === path) roots.delete(root);
867
+ for (let owners of [...state.registrations.values(), ...graph.resolverUsage.values()]) for (let owner of owners.keys()) if (owner.split("?")[0] === path) owners.delete(owner);
868
+ }
869
+ let name = this.environment.name;
870
+ let ids = modules.flatMap((module) => module.id ? [module.id] : []);
871
+ for (let [serverName, snapshot] of snapshots) {
872
+ let tracked = name === serverName ? snapshot.server : name === "client" ? snapshot.client : void 0;
873
+ if (tracked && (tracked.has(path) || ids.some((id) => tracked.has(id)))) invalidate(serverName, timestamp);
874
+ }
875
+ }
876
+ }
877
+ };
878
+ }
879
+ //#endregion
880
+ //#region src/vite-plugin.ts
881
+ function assets(options = {}) {
882
+ let state = {
883
+ serverEnvironments: options.serverEnvironments ?? ["ssr"],
884
+ inputs: /* @__PURE__ */ new Map(),
885
+ resolverUsage: /* @__PURE__ */ new Map(),
886
+ registrations: /* @__PURE__ */ new Map(),
887
+ assetReferences: /* @__PURE__ */ new Map(),
888
+ scriptReferences: /* @__PURE__ */ new Map(),
889
+ outputs: /* @__PURE__ */ new Map(),
890
+ mapsEnabled: false
891
+ };
892
+ return [
893
+ {
894
+ name: "pitlane-assets",
895
+ api: { setBrowserEntries({ environment, owner, entries }) {
896
+ if (!state.serverEnvironments.includes(environment)) throw new Error(`[assets] Cannot register browser entries from "${environment}": it is not in serverEnvironments.`);
897
+ let owners = state.registrations.get(environment);
898
+ let previous = owners?.get(owner);
899
+ if (!previous && entries.length === 0 || previous?.length === entries.length && entries.every((id, index) => id === previous[index])) return;
900
+ if (entries.length === 0) {
901
+ owners.delete(owner);
902
+ if (owners.size === 0) state.registrations.delete(environment);
903
+ } else {
904
+ if (!owners) state.registrations.set(environment, owners = /* @__PURE__ */ new Map());
905
+ owners.set(owner, entries.slice());
906
+ }
907
+ state.onRegistrationChange?.(environment, owner);
908
+ } },
909
+ enforce: "post",
910
+ sharedDuringBuild: true,
911
+ config(config) {
912
+ state.mapsEnabled = options.chunkImportMap ?? config.environments?.client?.build?.chunkImportMap ?? config.build?.chunkImportMap ?? false;
913
+ if (state.mapsEnabled && config.experimental?.renderBuiltUrl) throw new Error("[assets] chunkImportMap: true cannot be combined with experimental.renderBuiltUrl.");
914
+ return {
915
+ builder: {},
916
+ build: { assetsInlineLimit: 0 },
917
+ optimizeDeps: { exclude: ["@pitlane/assets/manifest", "pitlane/assets/manifest"] },
918
+ environments: { client: { build: { chunkImportMap: state.mapsEnabled } } }
919
+ };
920
+ },
921
+ configResolved(config) {
922
+ state.config = config;
923
+ for (let key of options.include ?? []) {
924
+ let id = inputPath(config.root, key);
925
+ state.inputs.set(id, {
926
+ key: sourceKey(config.root, id),
927
+ kind: "asset"
928
+ });
929
+ }
930
+ },
931
+ configEnvironment(name) {
932
+ if (!state.serverEnvironments.includes(name)) return;
933
+ return {
934
+ resolve: { noExternal: [/^@pitlane\/assets(?:\/|$)/, /^pitlane(?:\/|$)/] },
935
+ optimizeDeps: { exclude: ["@pitlane/assets/manifest", "pitlane/assets/manifest"] },
936
+ build: { chunkImportMap: false }
937
+ };
938
+ },
939
+ resolveId: {
940
+ order: "pre",
941
+ handler(id) {
942
+ if (id !== "@pitlane/assets/manifest" && id !== "pitlane/assets/manifest") return;
943
+ 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.");
944
+ if (this.environment.mode !== "build") return MANIFEST_ID;
945
+ if (!state.serverEnvironments.includes(this.environment.name)) throw unservedEnvironmentError(this.environment.name, state.serverEnvironments);
946
+ return {
947
+ id: MANIFEST_EXTERNAL,
948
+ external: true
949
+ };
950
+ }
951
+ },
952
+ async transform(code, id) {
953
+ let { mode, name } = this.environment;
954
+ if (mode !== "build" || !state.serverEnvironments.includes(name)) return;
955
+ let modules = state.resolverUsage.get(name);
956
+ if (!modules) state.resolverUsage.set(name, modules = /* @__PURE__ */ new Map());
957
+ let usage = scanResolverUsage(code, id);
958
+ if (!usage) return void modules.delete(id);
959
+ modules.set(id, await linkResolverUsage(usage, (specifier) => this.resolve(specifier, id)));
960
+ }
961
+ },
962
+ assetBuild(state),
963
+ assetDevelopment(state),
964
+ assetStyles()
965
+ ];
966
+ }
967
+ //#endregion
968
+ export { assets };