evolit 0.3.0 → 0.4.1

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,713 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import { BUILD_DIRECTORY, DEV_DIRECTORY, INTERNAL_DIRECTORY } from "./constants.js";
5
+ import { ensureDirectory } from "./fs-utils.js";
6
+
7
+ const OUTPUT_KINDS = new Set(["asset", "module", "style"]);
8
+
9
+ function contextualError(error, context) {
10
+ const original = error instanceof Error ? error : new Error(String(error));
11
+ const details = Object.entries(context)
12
+ .filter(([, value]) => value != null)
13
+ .map(([key, value]) => `${key}=${JSON.stringify(value)}`)
14
+ .join(" ");
15
+ return new Error(`LitSX integration failed (${details}): ${original.message}`, { cause: original });
16
+ }
17
+
18
+ function assertRecord(value, label) {
19
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
20
+ throw new TypeError(`Expected ${label} to be an object.`);
21
+ }
22
+ }
23
+
24
+ function normalizeCompilerOptions(value, label) {
25
+ if (value == null) return {};
26
+ assertRecord(value, label);
27
+ return { ...value };
28
+ }
29
+
30
+ function mergeCompilerOptions(base, contribution) {
31
+ const next = { ...base, ...contribution };
32
+ for (const key of ["parserPlugins", "authoringPlugins", "outputPlugins"]) {
33
+ const values = [...(base[key] ?? []), ...(contribution[key] ?? [])];
34
+ if (values.length > 0) next[key] = values;
35
+ }
36
+ return next;
37
+ }
38
+
39
+ function normalizeOutput(output, integrationName) {
40
+ assertRecord(output, `output from LitSX integration ${JSON.stringify(integrationName)}`);
41
+ const { id, kind, content } = output;
42
+ if (typeof id !== "string" || id.length === 0) {
43
+ throw new TypeError(`Expected output.id from LitSX integration ${JSON.stringify(integrationName)} to be non-empty.`);
44
+ }
45
+ if (
46
+ path.isAbsolute(id)
47
+ || id.includes("\\")
48
+ || id.split("/").some((segment) => segment === ".." || segment === "")
49
+ || /^[a-z][a-z\d+.-]*:/iu.test(id)
50
+ ) {
51
+ throw new TypeError(`Unsafe LitSX integration output id ${JSON.stringify(id)} from ${JSON.stringify(integrationName)}.`);
52
+ }
53
+ if (!OUTPUT_KINDS.has(kind)) {
54
+ throw new TypeError(`Unsupported LitSX integration output kind ${JSON.stringify(kind)} from ${JSON.stringify(integrationName)}.`);
55
+ }
56
+ if (typeof content !== "string" && !(content instanceof Uint8Array)) {
57
+ throw new TypeError(`Expected output.content for ${JSON.stringify(id)} to be a string or Uint8Array.`);
58
+ }
59
+ if (kind === "style" && /<\/?(?:html|head|body|style|script)\b/iu.test(String(content))) {
60
+ throw new TypeError(`LitSX integration style output ${JSON.stringify(id)} must contain CSS, not HTML.`);
61
+ }
62
+ const specifier = output.specifier;
63
+ if (specifier != null) {
64
+ if (
65
+ kind !== "module"
66
+ || typeof specifier !== "string"
67
+ || !/^virtual:[@a-z\d][@a-z\d/._-]*$/iu.test(specifier)
68
+ || specifier.split("/").some((segment) => segment === "..")
69
+ ) {
70
+ throw new TypeError(
71
+ `Expected output.specifier for ${JSON.stringify(id)} to be a safe virtual: module identifier.`,
72
+ );
73
+ }
74
+ }
75
+ const bytes = typeof content === "string" ? Buffer.from(content) : content;
76
+ return Object.freeze({
77
+ id,
78
+ kind,
79
+ content,
80
+ document: output.document === true,
81
+ ...(specifier == null ? {} : { specifier }),
82
+ hash: createHash("sha256").update(bytes).digest("hex").slice(0, 8),
83
+ size: bytes.byteLength,
84
+ });
85
+ }
86
+
87
+ function integrationPublicUrl(record) {
88
+ const extension = path.posix.extname(record.id);
89
+ const stem = extension ? record.id.slice(0, -extension.length) : record.id;
90
+ const hashedId = `${stem}.${record.hash}${extension}`;
91
+ return `/_evolit/static/integrations/${[
92
+ record.integration,
93
+ ...hashedId.split("/"),
94
+ ].map((segment) => encodeURIComponent(segment)).join("/")}`;
95
+ }
96
+
97
+ /** Add a finalized integration generation to an Evolit client asset manifest. */
98
+ export function mergeLitsxAssetsIntoManifest(manifest, integrationManifest) {
99
+ if (!manifest || !integrationManifest) return manifest;
100
+ const integrationAssets = integrationManifest.assets.map((record) => ({
101
+ clientModule: null,
102
+ type: record.kind === "style" ? "style" : record.kind === "module" ? "script" : "asset",
103
+ kind: `integration-${record.kind}`,
104
+ publicUrl: integrationPublicUrl(record),
105
+ outputPath: record.outputPath,
106
+ hash: record.hash,
107
+ size: record.size,
108
+ imports: [],
109
+ importUrls: [],
110
+ styleImports: [],
111
+ styleUrls: [],
112
+ assetImports: [],
113
+ assetUrls: [],
114
+ integration: record.integration,
115
+ integrationOutputId: record.id,
116
+ }));
117
+ const integrationUrls = new Map(
118
+ integrationAssets.map((asset) => [`${asset.integration}:${asset.integrationOutputId}`, asset.publicUrl]),
119
+ );
120
+ const documentStyles = integrationManifest.documentStyles
121
+ .map((record) => integrationUrls.get(`${record.integration}:${record.id}`))
122
+ .filter(Boolean);
123
+ return {
124
+ ...manifest,
125
+ byPublicPath: {
126
+ ...(manifest.byPublicPath ?? {}),
127
+ ...Object.fromEntries(integrationAssets.map((asset) => [asset.publicUrl, asset.outputPath])),
128
+ },
129
+ assets: [
130
+ ...(manifest.assets ?? []).filter((asset) => !asset.integration),
131
+ ...integrationAssets,
132
+ ],
133
+ documentStyles: [...new Set(documentStyles)].sort(),
134
+ };
135
+ }
136
+
137
+ function canonicalDependency(projectRoot, dependency) {
138
+ if (typeof dependency !== "string" || dependency.length === 0) {
139
+ throw new TypeError("Expected every LitSX integration dependency to be a non-empty path.");
140
+ }
141
+ if (/^[a-z][a-z\d+.-]*:/iu.test(dependency)) {
142
+ throw new TypeError(`LitSX integration dependency must be a filesystem path: ${JSON.stringify(dependency)}.`);
143
+ }
144
+ return path.resolve(projectRoot, dependency);
145
+ }
146
+
147
+ function normalizeContribution(value, integrationName, projectRoot) {
148
+ if (value == null) return { result: null, dependencies: [], outputs: [] };
149
+ assertRecord(value, `result from LitSX integration ${JSON.stringify(integrationName)}`);
150
+ const dependencies = (value.dependencies ?? []).map((entry) => canonicalDependency(projectRoot, entry));
151
+ const outputs = (value.outputs ?? []).map((entry) => normalizeOutput(entry, integrationName));
152
+ const hasTransform = value.code != null || value.map !== undefined || value.metadata != null;
153
+ return {
154
+ result: hasTransform ? {
155
+ code: value.code,
156
+ map: value.map,
157
+ metadata: value.metadata,
158
+ } : null,
159
+ dependencies,
160
+ outputs,
161
+ };
162
+ }
163
+
164
+ function integrationOutputRoot(projectRoot, mode, integrationName) {
165
+ const safeName = integrationName.replace(/[^a-z\d._-]+/giu, "-");
166
+ return path.join(
167
+ projectRoot,
168
+ INTERNAL_DIRECTORY,
169
+ mode === "development" ? DEV_DIRECTORY : BUILD_DIRECTORY,
170
+ "integrations",
171
+ safeName,
172
+ );
173
+ }
174
+
175
+ function virtualModuleRoot(projectRoot, mode, identity) {
176
+ return path.join(
177
+ projectRoot,
178
+ INTERNAL_DIRECTORY,
179
+ mode === "development" ? DEV_DIRECTORY : BUILD_DIRECTORY,
180
+ "virtual",
181
+ identity.id,
182
+ );
183
+ }
184
+
185
+ async function removePath(candidate) {
186
+ await fs.rm(candidate, { recursive: true, force: true });
187
+ }
188
+
189
+ /**
190
+ * Creates an isolated LitSX compilation pipeline for one server, build, or runtime.
191
+ * Integration descriptors are immutable configuration; all mutable state belongs to
192
+ * the instances returned by their create() methods.
193
+ */
194
+ export async function createLitsxPipeline({ projectRoot, mode, config } = {}) {
195
+ const litsx = config?.litsx;
196
+ if (litsx == null) return null;
197
+ assertRecord(litsx, "litsx in evolit.config.js");
198
+ const compiler = normalizeCompilerOptions(litsx.compiler, "litsx.compiler");
199
+ const descriptors = litsx.integrations ?? [];
200
+ if (!Array.isArray(descriptors)) {
201
+ throw new TypeError("Expected litsx.integrations in evolit.config.js to be an array.");
202
+ }
203
+
204
+ const resolvedProjectRoot = path.resolve(projectRoot ?? process.cwd());
205
+ const identity = Object.freeze({ id: randomUUID() });
206
+ const instances = [];
207
+ const names = new Set();
208
+ let disposed = false;
209
+ let generation = 0;
210
+ let finalizedGeneration = -1;
211
+ const dependencies = new Set();
212
+ const processedModules = new Set();
213
+ const moduleDependencies = new Map();
214
+ const pendingOutputs = new Map();
215
+ const pendingSpecifiers = new Map();
216
+ const publishedRoots = new Map();
217
+ const virtualRoot = virtualModuleRoot(resolvedProjectRoot, mode, identity);
218
+ const virtualModulesByImporter = new Map();
219
+ const virtualTargetsBySpecifier = new Map();
220
+ const virtualOutputRelativePaths = new Map();
221
+
222
+ function rebuildDependencies() {
223
+ dependencies.clear();
224
+ for (const owned of moduleDependencies.values()) {
225
+ for (const dependency of owned) dependencies.add(dependency);
226
+ }
227
+ }
228
+
229
+ function rebuildPendingSpecifiers() {
230
+ pendingSpecifiers.clear();
231
+ for (const entry of pendingOutputs.values()) {
232
+ if (entry.output.specifier) pendingSpecifiers.set(entry.output.specifier, entry);
233
+ }
234
+ }
235
+
236
+ function removeOwnedContributions(owner) {
237
+ for (const [key, entry] of pendingOutputs) {
238
+ if (entry.owner === owner) pendingOutputs.delete(key);
239
+ }
240
+ rebuildPendingSpecifiers();
241
+ }
242
+
243
+ try {
244
+ for (const descriptor of descriptors) {
245
+ assertRecord(descriptor, "every litsx.integrations entry");
246
+ if (typeof descriptor.name !== "string" || descriptor.name.length === 0) {
247
+ throw new TypeError("Expected every LitSX integration to have a non-empty name.");
248
+ }
249
+ if (
250
+ !/^[a-z\d][a-z\d._-]*$/iu.test(descriptor.name)
251
+ || descriptor.name === "."
252
+ || descriptor.name === ".."
253
+ ) {
254
+ throw new TypeError(
255
+ `LitSX integration name ${JSON.stringify(descriptor.name)} must be a safe identifier `
256
+ + "containing only letters, digits, dots, underscores, and hyphens.",
257
+ );
258
+ }
259
+ if (names.has(descriptor.name)) {
260
+ throw new TypeError(`Duplicate LitSX integration name ${JSON.stringify(descriptor.name)}.`);
261
+ }
262
+ if (typeof descriptor.create !== "function") {
263
+ throw new TypeError(`Expected LitSX integration ${JSON.stringify(descriptor.name)} to expose create(context).`);
264
+ }
265
+ names.add(descriptor.name);
266
+ let instance;
267
+ try {
268
+ instance = await descriptor.create(Object.freeze({
269
+ projectRoot: resolvedProjectRoot,
270
+ mode,
271
+ identity,
272
+ }));
273
+ assertRecord(instance, `instance created by LitSX integration ${JSON.stringify(descriptor.name)}`);
274
+ } catch (error) {
275
+ throw contextualError(error, { integration: descriptor.name, phase: "create", mode });
276
+ }
277
+ instances.push({ descriptor, instance });
278
+ }
279
+ } catch (error) {
280
+ for (const { descriptor, instance } of [...instances].reverse()) {
281
+ try {
282
+ await instance.dispose?.();
283
+ } catch {
284
+ // Preserve the primary initialization error.
285
+ }
286
+ }
287
+ throw error;
288
+ }
289
+
290
+ function assertActive() {
291
+ if (disposed) throw new Error("This LitSX pipeline has already been disposed.");
292
+ }
293
+
294
+ function recordContribution(integrationName, moduleId, contribution) {
295
+ if (contribution.dependencies.length > 0) {
296
+ let owned = moduleDependencies.get(moduleId);
297
+ if (!owned) {
298
+ owned = new Set();
299
+ moduleDependencies.set(moduleId, owned);
300
+ }
301
+ for (const dependency of contribution.dependencies) {
302
+ dependencies.add(dependency);
303
+ owned.add(dependency);
304
+ }
305
+ }
306
+ for (const output of contribution.outputs) {
307
+ const collisionKey = output.id;
308
+ const previous = pendingOutputs.get(collisionKey);
309
+ if (previous && (
310
+ previous.integrationName !== integrationName
311
+ || previous.output.kind !== output.kind
312
+ || previous.owner !== moduleId
313
+ )) {
314
+ throw new Error(
315
+ `LitSX integration output collision for ${JSON.stringify(output.id)} between `
316
+ + `${JSON.stringify(previous.integrationName)} and ${JSON.stringify(integrationName)}.`,
317
+ );
318
+ }
319
+ pendingOutputs.set(collisionKey, { integrationName, owner: moduleId, output });
320
+ if (output.specifier) {
321
+ const previousSpecifier = pendingSpecifiers.get(output.specifier);
322
+ if (previousSpecifier && previousSpecifier.integrationName !== integrationName) {
323
+ throw new Error(
324
+ `LitSX integration virtual module collision for ${JSON.stringify(output.specifier)} between `
325
+ + `${JSON.stringify(previousSpecifier.integrationName)} and ${JSON.stringify(integrationName)}.`,
326
+ );
327
+ }
328
+ pendingSpecifiers.set(output.specifier, { integrationName, owner: moduleId, output });
329
+ }
330
+ }
331
+ }
332
+
333
+ const pipeline = {
334
+ identity,
335
+ get generation() {
336
+ return generation;
337
+ },
338
+ get dependencies() {
339
+ return [...dependencies].sort();
340
+ },
341
+ getOutputRelativePath(sourcePath) {
342
+ return virtualOutputRelativePaths.get(path.resolve(sourcePath)) ?? null;
343
+ },
344
+ getCompilerOptions({ filename, ssr, sourceMaps }) {
345
+ assertActive();
346
+ let options = { ...compiler };
347
+ for (const { descriptor, instance } of instances) {
348
+ try {
349
+ const contribution = typeof instance.compiler === "function"
350
+ ? instance.compiler({ filename, ssr, sourceMaps, mode })
351
+ : instance.compiler;
352
+ options = mergeCompilerOptions(
353
+ options,
354
+ normalizeCompilerOptions(contribution, `compiler contribution from ${JSON.stringify(descriptor.name)}`),
355
+ );
356
+ } catch (error) {
357
+ throw contextualError(error, {
358
+ integration: descriptor.name,
359
+ phase: "compiler",
360
+ mode,
361
+ module: filename,
362
+ });
363
+ }
364
+ }
365
+ return {
366
+ ...options,
367
+ filename,
368
+ ssr: ssr === true,
369
+ sourceMaps: options.sourceMaps ?? (sourceMaps === true),
370
+ reactCompat: false,
371
+ };
372
+ },
373
+ async resolveModule(specifier, context) {
374
+ assertActive();
375
+ const declaredOutput = pendingSpecifiers.get(specifier);
376
+ let resolved = null;
377
+ if (declaredOutput) {
378
+ resolved = {
379
+ integrationName: declaredOutput.integrationName,
380
+ code: String(declaredOutput.output.content),
381
+ dependencies: [],
382
+ };
383
+ }
384
+ for (const { descriptor, instance } of instances) {
385
+ if (typeof instance.resolveModule !== "function") continue;
386
+ if (declaredOutput?.integrationName === descriptor.name) continue;
387
+ try {
388
+ const value = await instance.resolveModule(Object.freeze({
389
+ ...context,
390
+ specifier,
391
+ generation,
392
+ mode,
393
+ }));
394
+ if (value == null) continue;
395
+ if (resolved) {
396
+ throw new Error(
397
+ `Virtual module ${JSON.stringify(specifier)} was resolved by both `
398
+ + `${JSON.stringify(resolved.integrationName)} and ${JSON.stringify(descriptor.name)}.`,
399
+ );
400
+ }
401
+ assertRecord(value, `virtual module ${JSON.stringify(specifier)} from ${JSON.stringify(descriptor.name)}`);
402
+ if (typeof value.code !== "string") {
403
+ throw new TypeError(`Expected virtual module ${JSON.stringify(specifier)} to provide string code.`);
404
+ }
405
+ const dependencies = (value.dependencies ?? [])
406
+ .map((entry) => canonicalDependency(resolvedProjectRoot, entry));
407
+ resolved = { integrationName: descriptor.name, code: value.code, dependencies };
408
+ } catch (error) {
409
+ throw contextualError(error, {
410
+ integration: descriptor.name,
411
+ phase: "resolveModule",
412
+ mode,
413
+ target: context.target,
414
+ module: context.importer,
415
+ specifier,
416
+ });
417
+ }
418
+ }
419
+ if (!resolved) return null;
420
+ const moduleKey = createHash("sha256")
421
+ .update(JSON.stringify([specifier, context.importer, context.target]))
422
+ .digest("hex");
423
+ const integrationName = resolved.integrationName.replace(/[^a-z\d._-]+/giu, "-");
424
+ resolved.sourcePath = path.join(virtualRoot, integrationName, `${moduleKey}.mjs`);
425
+ virtualOutputRelativePaths.set(
426
+ path.resolve(resolved.sourcePath),
427
+ path.join("__litsx_virtual__", integrationName, `${moduleKey}.mjs`),
428
+ );
429
+ await ensureDirectory(path.dirname(resolved.sourcePath));
430
+ await fs.writeFile(resolved.sourcePath, resolved.code, "utf8");
431
+ const targets = virtualTargetsBySpecifier.get(specifier) ?? new Set();
432
+ targets.add(resolved.sourcePath);
433
+ virtualTargetsBySpecifier.set(specifier, targets);
434
+ const importer = path.resolve(context.importer);
435
+ const importerVirtualModules = virtualModulesByImporter.get(importer) ?? new Set();
436
+ importerVirtualModules.add(resolved.sourcePath);
437
+ virtualModulesByImporter.set(importer, importerVirtualModules);
438
+ recordContribution(resolved.integrationName, resolved.sourcePath, {
439
+ result: null,
440
+ dependencies: resolved.dependencies,
441
+ outputs: [],
442
+ });
443
+ return resolved.sourcePath;
444
+ },
445
+ registerResolvedModule(specifier, sourcePath, outputPath) {
446
+ assertActive();
447
+ const targets = virtualTargetsBySpecifier.get(specifier) ?? new Set();
448
+ targets.add(path.resolve(sourcePath));
449
+ targets.add(path.resolve(outputPath));
450
+ virtualTargetsBySpecifier.set(specifier, targets);
451
+ },
452
+ async processModule(result, context) {
453
+ assertActive();
454
+ if (finalizedGeneration === generation) {
455
+ generation += 1;
456
+ finalizedGeneration = -1;
457
+ }
458
+ const moduleId = path.resolve(context.sourcePath);
459
+ processedModules.add(moduleId);
460
+ if (moduleDependencies.delete(moduleId)) rebuildDependencies();
461
+ removeOwnedContributions(moduleId);
462
+ let current = result;
463
+ for (const { descriptor, instance } of instances) {
464
+ if (typeof instance.processModule !== "function") continue;
465
+ try {
466
+ const value = await instance.processModule(Object.freeze({
467
+ ...context,
468
+ sourcePath: moduleId,
469
+ result: current,
470
+ generation,
471
+ }));
472
+ const contribution = normalizeContribution(value, descriptor.name, resolvedProjectRoot);
473
+ recordContribution(descriptor.name, moduleId, contribution);
474
+ if (contribution.result) {
475
+ current = {
476
+ ...current,
477
+ ...(contribution.result.code == null ? {} : { code: contribution.result.code }),
478
+ ...(contribution.result.map === undefined ? {} : { map: contribution.result.map }),
479
+ ...(contribution.result.metadata == null ? {} : {
480
+ metadata: { ...(current.metadata ?? {}), ...contribution.result.metadata },
481
+ }),
482
+ };
483
+ }
484
+ } catch (error) {
485
+ throw contextualError(error, {
486
+ integration: descriptor.name,
487
+ phase: "processModule",
488
+ mode,
489
+ target: context.target,
490
+ module: moduleId,
491
+ });
492
+ }
493
+ }
494
+ return {
495
+ ...current,
496
+ evolitDependencies: [...(moduleDependencies.get(moduleId) ?? [])].sort(),
497
+ };
498
+ },
499
+ async finalize(context = {}) {
500
+ assertActive();
501
+ if (finalizedGeneration === generation) return null;
502
+ if (moduleDependencies.delete("<graph>")) rebuildDependencies();
503
+ removeOwnedContributions("<graph>");
504
+ for (const { descriptor, instance } of instances) {
505
+ if (typeof instance.finalize !== "function") continue;
506
+ try {
507
+ const contribution = normalizeContribution(
508
+ await instance.finalize(Object.freeze({ ...context, generation, mode })),
509
+ descriptor.name,
510
+ resolvedProjectRoot,
511
+ );
512
+ recordContribution(descriptor.name, "<graph>", contribution);
513
+ } catch (error) {
514
+ throw contextualError(error, { integration: descriptor.name, phase: "finalize", mode });
515
+ }
516
+ }
517
+
518
+ const outputsByIntegration = new Map();
519
+ for (const { integrationName, output } of pendingOutputs.values()) {
520
+ const list = outputsByIntegration.get(integrationName) ?? [];
521
+ list.push(output);
522
+ outputsByIntegration.set(integrationName, list);
523
+ }
524
+ const manifest = { assets: [], documentStyles: [] };
525
+ const rootOperations = [];
526
+ const patchOperations = [];
527
+ let activeIntegration = null;
528
+ try {
529
+ for (const { descriptor } of instances) {
530
+ activeIntegration = descriptor.name;
531
+ const outputs = outputsByIntegration.get(descriptor.name) ?? [];
532
+ const root = publishedRoots.get(descriptor.name)
533
+ ?? integrationOutputRoot(resolvedProjectRoot, mode, descriptor.name);
534
+ const operation = {
535
+ integrationName: descriptor.name,
536
+ root,
537
+ stagingRoot: null,
538
+ previousRoot: `${root}.previous-${randomUUID()}`,
539
+ hadPreviousRoot: false,
540
+ installed: false,
541
+ };
542
+ if (outputs.length > 0) {
543
+ operation.stagingRoot = `${root}.staging-${randomUUID()}`;
544
+ await ensureDirectory(operation.stagingRoot);
545
+ for (const output of outputs.sort((left, right) => left.id.localeCompare(right.id))) {
546
+ const outputPath = path.join(operation.stagingRoot, ...output.id.split("/"));
547
+ await ensureDirectory(path.dirname(outputPath));
548
+ await fs.writeFile(outputPath, output.content);
549
+ const record = {
550
+ integration: descriptor.name,
551
+ id: output.id,
552
+ kind: output.kind,
553
+ outputPath: path.join(root, ...output.id.split("/")),
554
+ hash: output.hash,
555
+ size: output.size,
556
+ };
557
+ manifest.assets.push(record);
558
+ if (output.kind === "style" && output.document) manifest.documentStyles.push(record);
559
+ }
560
+ }
561
+ rootOperations.push(operation);
562
+ }
563
+
564
+ for (const { output, integrationName } of pendingOutputs.values()) {
565
+ if (!output.specifier) continue;
566
+ activeIntegration = integrationName;
567
+ for (const targetPath of virtualTargetsBySpecifier.get(output.specifier) ?? []) {
568
+ const temporaryPath = `${targetPath}.staging-${randomUUID()}`;
569
+ await ensureDirectory(path.dirname(targetPath));
570
+ await fs.writeFile(temporaryPath, output.content);
571
+ patchOperations.push({
572
+ integrationName,
573
+ targetPath,
574
+ temporaryPath,
575
+ previousPath: `${targetPath}.previous-${randomUUID()}`,
576
+ hadPrevious: false,
577
+ installed: false,
578
+ });
579
+ }
580
+ }
581
+ } catch (error) {
582
+ await Promise.allSettled([
583
+ ...rootOperations.map((operation) => operation.stagingRoot && removePath(operation.stagingRoot)),
584
+ ...patchOperations.map((operation) => removePath(operation.temporaryPath)),
585
+ ]);
586
+ throw contextualError(error, { integration: activeIntegration, phase: "publish", mode });
587
+ }
588
+
589
+ try {
590
+ for (const operation of rootOperations) {
591
+ try {
592
+ await fs.rename(operation.root, operation.previousRoot);
593
+ operation.hadPreviousRoot = true;
594
+ } catch (error) {
595
+ if (error?.code !== "ENOENT") throw error;
596
+ }
597
+ }
598
+ for (const operation of patchOperations) {
599
+ try {
600
+ await fs.rename(operation.targetPath, operation.previousPath);
601
+ operation.hadPrevious = true;
602
+ } catch (error) {
603
+ if (error?.code !== "ENOENT") throw error;
604
+ }
605
+ }
606
+ for (const operation of rootOperations) {
607
+ if (!operation.stagingRoot) continue;
608
+ await fs.rename(operation.stagingRoot, operation.root);
609
+ operation.installed = true;
610
+ }
611
+ for (const operation of patchOperations) {
612
+ await fs.rename(operation.temporaryPath, operation.targetPath);
613
+ operation.installed = true;
614
+ }
615
+ } catch (error) {
616
+ for (const operation of [...patchOperations].reverse()) {
617
+ if (operation.installed) await removePath(operation.targetPath);
618
+ if (operation.hadPrevious) await fs.rename(operation.previousPath, operation.targetPath);
619
+ else await removePath(operation.temporaryPath);
620
+ }
621
+ for (const operation of [...rootOperations].reverse()) {
622
+ if (operation.installed) await removePath(operation.root);
623
+ if (operation.hadPreviousRoot) await fs.rename(operation.previousRoot, operation.root);
624
+ if (operation.stagingRoot) await removePath(operation.stagingRoot);
625
+ }
626
+ throw contextualError(error, { integration: activeIntegration, phase: "publish", mode });
627
+ }
628
+
629
+ await Promise.all([
630
+ ...rootOperations.map((operation) => operation.hadPreviousRoot && removePath(operation.previousRoot)),
631
+ ...patchOperations.map((operation) => operation.hadPrevious && removePath(operation.previousPath)),
632
+ ]);
633
+ for (const operation of rootOperations) {
634
+ if (operation.stagingRoot) publishedRoots.set(operation.integrationName, operation.root);
635
+ else publishedRoots.delete(operation.integrationName);
636
+ }
637
+ finalizedGeneration = generation;
638
+ return manifest;
639
+ },
640
+ async invalidate(changedPaths = null) {
641
+ assertActive();
642
+ const paths = Array.isArray(changedPaths)
643
+ ? changedPaths.map((entry) => canonicalDependency(resolvedProjectRoot, entry))
644
+ : null;
645
+ const affected = paths == null || paths.some((entry) => (
646
+ dependencies.has(entry) || processedModules.has(entry)
647
+ ));
648
+ for (const { descriptor, instance } of instances) {
649
+ if (typeof instance.invalidate !== "function") continue;
650
+ try {
651
+ await instance.invalidate(Object.freeze({ paths, affected, generation, mode }));
652
+ } catch (error) {
653
+ throw contextualError(error, { integration: descriptor.name, phase: "invalidate", mode });
654
+ }
655
+ }
656
+ if (affected) {
657
+ generation += 1;
658
+ finalizedGeneration = -1;
659
+ for (const changedPath of paths ?? []) {
660
+ if (processedModules.has(changedPath)) await pipeline.forget(changedPath);
661
+ }
662
+ }
663
+ return affected;
664
+ },
665
+ async forget(moduleId) {
666
+ assertActive();
667
+ const resolvedModuleId = path.resolve(moduleId);
668
+ const ownedVirtualModules = [...(virtualModulesByImporter.get(resolvedModuleId) ?? [])];
669
+ virtualModulesByImporter.delete(resolvedModuleId);
670
+ for (const { descriptor, instance } of instances) {
671
+ try {
672
+ await instance.forget?.(Object.freeze({ moduleId: resolvedModuleId, generation, mode }));
673
+ } catch (error) {
674
+ throw contextualError(error, { integration: descriptor.name, phase: "forget", mode, module: resolvedModuleId });
675
+ }
676
+ }
677
+ moduleDependencies.delete(resolvedModuleId);
678
+ rebuildDependencies();
679
+ processedModules.delete(resolvedModuleId);
680
+ removeOwnedContributions(resolvedModuleId);
681
+ for (const virtualModule of ownedVirtualModules) {
682
+ await pipeline.forget(virtualModule);
683
+ await removePath(virtualModule);
684
+ }
685
+ },
686
+ async dispose() {
687
+ if (disposed) return;
688
+ disposed = true;
689
+ let firstError = null;
690
+ for (const { descriptor, instance } of [...instances].reverse()) {
691
+ try {
692
+ await instance.dispose?.();
693
+ } catch (error) {
694
+ firstError ??= contextualError(error, { integration: descriptor.name, phase: "dispose", mode });
695
+ }
696
+ }
697
+ await removePath(virtualRoot);
698
+ if (firstError) throw firstError;
699
+ },
700
+ };
701
+
702
+ return pipeline;
703
+ }
704
+
705
+ /** Identity helper for typed evolit.config.js files. */
706
+ export function defineEvolitConfig(config) {
707
+ return config;
708
+ }
709
+
710
+ /** Identity helper for stateless LitSX integration descriptors. */
711
+ export function defineLitsxIntegration(integration) {
712
+ return integration;
713
+ }