@animus-ui/vite-plugin 0.1.11 → 0.1.13
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/README.md +1 -0
- package/dist/build-start.d.ts +2 -6
- package/dist/build-start.d.ts.map +1 -1
- package/dist/config.d.ts +0 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/constants.d.ts +4 -16
- package/dist/constants.d.ts.map +1 -1
- package/dist/context.d.ts +54 -233
- package/dist/context.d.ts.map +1 -1
- package/dist/css.d.ts +0 -6
- package/dist/css.d.ts.map +1 -1
- package/dist/hmr.d.ts +2 -19
- package/dist/hmr.d.ts.map +1 -1
- package/dist/hot-update-events.d.ts +14 -44
- package/dist/hot-update-events.d.ts.map +1 -1
- package/dist/index-html.d.ts +2 -23
- package/dist/index-html.d.ts.map +1 -1
- package/dist/index.cjs +315 -622
- package/dist/index.d.ts +25 -92
- package/dist/index.d.ts.map +1 -1
- package/dist/module-invalidation.d.ts +2 -10
- package/dist/module-invalidation.d.ts.map +1 -1
- package/dist/rediscovery.d.ts +6 -24
- package/dist/rediscovery.d.ts.map +1 -1
- package/dist/reset-coalescer.d.ts +6 -8
- package/dist/reset-coalescer.d.ts.map +1 -1
- package/dist/transform.d.ts +2 -16
- package/dist/transform.d.ts.map +1 -1
- package/dist/virtual-modules.d.ts +0 -3
- package/dist/virtual-modules.d.ts.map +1 -1
- package/package.json +7 -4
package/dist/index.cjs
CHANGED
|
@@ -8,12 +8,8 @@ let path = require("path");
|
|
|
8
8
|
let crypto = require("crypto");
|
|
9
9
|
//#region src/build-start.ts
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* `resolveSpecifier` is the bundler seam — Vite's `this.resolve` mapped to
|
|
14
|
-
* an absolute id. `emitAsset` (build mode only) is Rollup's `this.emitFile`
|
|
15
|
-
* for `asset()` placeholder substitution; absent in dev, where resolved
|
|
16
|
-
* files are served via base + `/@fs/` instead.
|
|
11
|
+
* `resolveSpecifier` maps a specifier to an absolute id. `emitAsset` is
|
|
12
|
+
* present in build only, where `asset()` files become Rollup assets.
|
|
17
13
|
*/
|
|
18
14
|
async function runBuildStart(ctx, resolveSpecifier, emitAsset) {
|
|
19
15
|
(0, _animus_ui_extract_pipeline.clearEngineCache)(ctx.engineApi);
|
|
@@ -50,7 +46,6 @@ async function runBuildStart(ctx, resolveSpecifier, emitAsset) {
|
|
|
50
46
|
const localFileCount = rawEntries.length;
|
|
51
47
|
const packageSpecifiers = (0, _animus_ui_extract_pipeline.extractSystemFilePackages)(ctx.resolvedSystemPath);
|
|
52
48
|
ctx.externalSourceEntries.clear();
|
|
53
|
-
ctx.externalKeyframesScanEntries.clear();
|
|
54
49
|
const collected = await (0, _animus_ui_extract_pipeline.collectExternalPackageSources)({
|
|
55
50
|
specifiers: packageSpecifiers,
|
|
56
51
|
resolveSpecifier,
|
|
@@ -65,7 +60,6 @@ async function runBuildStart(ctx, resolveSpecifier, emitAsset) {
|
|
|
65
60
|
ctx.externalFileOwners = collected.fileOwners;
|
|
66
61
|
ctx.enforceIncludeResolution();
|
|
67
62
|
for (const [specifier, srcEntry] of collected.sourceEntries) ctx.externalSourceEntries.set(specifier, srcEntry);
|
|
68
|
-
for (const [specifier, scanEntry] of collected.keyframesScanEntries) ctx.externalKeyframesScanEntries.set(specifier, scanEntry);
|
|
69
63
|
for (const entry of collected.entries) {
|
|
70
64
|
const hash = !ctx.isProd ? (0, _animus_ui_extract_pipeline.contentHash)(entry.source) : void 0;
|
|
71
65
|
rawEntries.push({
|
|
@@ -76,17 +70,19 @@ async function runBuildStart(ctx, resolveSpecifier, emitAsset) {
|
|
|
76
70
|
}
|
|
77
71
|
ctx.externalPackageDirs = collected.packageDirs;
|
|
78
72
|
ctx.registerSystemWatchPaths();
|
|
79
|
-
ctx.applyExternalKeyframes();
|
|
80
73
|
const packageFileCount = rawEntries.length - localFileCount;
|
|
81
74
|
ctx.log(`Discovered ${rawEntries.length} files (${packageFileCount} from packages) (${Math.round(performance.now() - t0)}ms)`);
|
|
82
75
|
t0 = performance.now();
|
|
83
76
|
await ctx.analyzeIngested({
|
|
84
77
|
rawEntries,
|
|
85
78
|
beforeAnalysis: (accepted) => {
|
|
86
|
-
if (!ctx.isProd) ctx.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
79
|
+
if (!ctx.isProd) ctx.mutateFileCache((cache) => {
|
|
80
|
+
cache.clear();
|
|
81
|
+
for (const entry of accepted.originalEntries) cache.set(entry.path, {
|
|
82
|
+
hash: entry.hash,
|
|
83
|
+
source: entry.source
|
|
84
|
+
});
|
|
85
|
+
});
|
|
90
86
|
}
|
|
91
87
|
});
|
|
92
88
|
const assetSpecifiers = (0, _animus_ui_extract_pipeline.findAssetSpecifiers)(ctx.globalCss);
|
|
@@ -108,17 +104,14 @@ async function runBuildStart(ctx, resolveSpecifier, emitAsset) {
|
|
|
108
104
|
ctx.assetPassComplete = true;
|
|
109
105
|
if (ctx.storedManifest) {
|
|
110
106
|
const report = ctx.storedManifest.report;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
else if (d.kind === "state") ctx.warn(`⚠ ${d.component} state '${d.name}' pruned: ${d.reason}`);
|
|
120
|
-
}
|
|
121
|
-
ctx.log(`CSS: ${ctx.resolvedComponentCss.length} bytes (${Object.keys(ctx.storedManifest.components || {}).length} components)`);
|
|
107
|
+
ctx.log(`Extracted ${report.components_extracted}/${report.components_total} components (${Math.round(performance.now() - t0)}ms)`);
|
|
108
|
+
ctx.logTimingWaterfall(ctx.storedManifest.timing);
|
|
109
|
+
ctx.log(`Reconciliation: ${report.components_extracted} kept, ${report.variants_eliminated} variants pruned, ${report.states_eliminated} states pruned`);
|
|
110
|
+
for (const d of report.eliminated_details) if (d.kind === "component") ctx.warn(`⚠ ${d.component} eliminated: ${d.reason}`);
|
|
111
|
+
else if (d.kind === "prospective_component") ctx.warn(`⚠ ${d.component} would be eliminated in production: ${d.reason}`);
|
|
112
|
+
else if (d.kind === "variant") ctx.warn(`⚠ ${d.component} variant '${d.name}' pruned: ${d.reason}`);
|
|
113
|
+
else if (d.kind === "state") ctx.warn(`⚠ ${d.component} state '${d.name}' pruned: ${d.reason}`);
|
|
114
|
+
ctx.log(`CSS: ${ctx.resolvedComponentCss.length} bytes (${Object.keys(ctx.storedManifest.components).length} components)`);
|
|
122
115
|
if (!ctx.isProd && ctx.storedSheets) {
|
|
123
116
|
const staticSize = (0, _animus_ui_extract_pipeline.assembleStylesheet)({
|
|
124
117
|
layers: ctx.options.layers,
|
|
@@ -140,11 +133,6 @@ async function runBuildStart(ctx, resolveSpecifier, emitAsset) {
|
|
|
140
133
|
}
|
|
141
134
|
//#endregion
|
|
142
135
|
//#region src/config.ts
|
|
143
|
-
/**
|
|
144
|
-
* configResolved: capture build mode, root, and logger; resolve Lightning
|
|
145
|
-
* CSS targets; harvest resolve.alias into the Rust `pathAliasesJson`
|
|
146
|
-
* contract via the shared encoder.
|
|
147
|
-
*/
|
|
148
136
|
function applyResolvedConfig(ctx, config) {
|
|
149
137
|
ctx.isProd = config.command === "build";
|
|
150
138
|
ctx.emissionProd = (0, _animus_ui_extract_pipeline.resolveMode)(ctx.options.mode, () => config.command === "build" ? "production" : "development").mode === "production";
|
|
@@ -153,35 +141,26 @@ function applyResolvedConfig(ctx, config) {
|
|
|
153
141
|
ctx.base = config.base ?? "/";
|
|
154
142
|
ctx.lcssTargets = (0, _animus_ui_extract_pipeline.resolveLightningTargets)(ctx.options.targets, ctx.rootDir);
|
|
155
143
|
ctx.log(`Lightning CSS targets resolved (${Object.keys(ctx.lcssTargets).length} browsers)`);
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
const built = (0, _animus_ui_extract_pipeline.buildPathAliasesJson)(pairs, ctx.rootDir);
|
|
172
|
-
if (built) {
|
|
173
|
-
ctx.pathAliasesJson = built.json;
|
|
174
|
-
ctx.log(`Path aliases forwarded: ${built.count} entries`);
|
|
175
|
-
}
|
|
144
|
+
const pairs = [];
|
|
145
|
+
for (const entry of config.resolve.alias) {
|
|
146
|
+
if (entry.find instanceof RegExp) continue;
|
|
147
|
+
pairs.push({
|
|
148
|
+
pattern: entry.find,
|
|
149
|
+
target: entry.replacement,
|
|
150
|
+
kind: "prefix"
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
const built = (0, _animus_ui_extract_pipeline.buildPathAliasesJson)(pairs, ctx.rootDir);
|
|
154
|
+
if (built) {
|
|
155
|
+
ctx.pathAliasesJson = built.json;
|
|
156
|
+
ctx.log(`Path aliases forwarded: ${built.count} entries`);
|
|
176
157
|
}
|
|
177
158
|
}
|
|
178
159
|
//#endregion
|
|
179
160
|
//#region src/constants.ts
|
|
180
161
|
/**
|
|
181
|
-
* The namespace every virtual module
|
|
182
|
-
*
|
|
183
|
-
* matches on the same string the ids are built from — a rename of the
|
|
184
|
-
* namespace cannot leave the guard behind.
|
|
162
|
+
* The namespace every virtual module here lives under; the `transform` guard
|
|
163
|
+
* matches on this same string so a rename cannot strand it.
|
|
185
164
|
*/
|
|
186
165
|
const VIRTUAL_PREFIX = "virtual:animus/";
|
|
187
166
|
const VIRTUAL_CSS_ID = `${VIRTUAL_PREFIX}styles.css`;
|
|
@@ -191,18 +170,8 @@ const RESOLVED_COMPONENTS_ID = `\0${VIRTUAL_COMPONENTS_ID}`;
|
|
|
191
170
|
const VIRTUAL_BRIDGE_ID = `${VIRTUAL_PREFIX}hmr-bridge.js`;
|
|
192
171
|
const RESOLVED_BRIDGE_ID = `\0${VIRTUAL_BRIDGE_ID}`;
|
|
193
172
|
/**
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
* convention lives here; everything else points at it.
|
|
197
|
-
*
|
|
198
|
-
* Vite reserves `/@id/` for module ids that are not file paths. Its transform
|
|
199
|
-
* middleware normalizes such a URL before serving it (`unwrapId`): strip the
|
|
200
|
-
* prefix, then decode the `__x00__` placeholder back to a NUL byte. The
|
|
201
|
-
* UNPREFIXED specifier is what travels here — `resolveVirtualId` is the hook
|
|
202
|
-
* that answers it, and it answers with the `\0` form. (Vite's own `wrapId`
|
|
203
|
-
* encodes an already-`\0`-prefixed id as `__x00__`; that form resolves too, but
|
|
204
|
-
* it leans on the placeholder convention rather than on this plugin's own
|
|
205
|
-
* `resolveId` contract.)
|
|
173
|
+
* Vite reserves `/@id/` for module ids that are not file paths and strips the
|
|
174
|
+
* prefix before `resolveId`, so the unprefixed specifier travels here.
|
|
206
175
|
*/
|
|
207
176
|
const BRIDGE_SCRIPT_SRC = `/@id/${VIRTUAL_BRIDGE_ID}`;
|
|
208
177
|
const VIRTUAL_SYSTEM_PROPS_ID = `${VIRTUAL_PREFIX}system-props`;
|
|
@@ -210,50 +179,56 @@ const RESOLVED_SYSTEM_PROPS_ID = `\0${VIRTUAL_SYSTEM_PROPS_ID}`;
|
|
|
210
179
|
//#endregion
|
|
211
180
|
//#region src/hot-update-events.ts
|
|
212
181
|
/**
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
* Vite 8 calls `hotUpdate` once per environment for a single file event — the
|
|
216
|
-
* client environment first, then every non-client environment (see
|
|
217
|
-
* `handleHMRUpdate` in vite/dist/node/chunks/node.js). Analysis work (cache
|
|
218
|
-
* mutation, engine re-analysis, geological reset scheduling) must happen
|
|
219
|
-
* exactly once per event; module invalidation must happen per environment
|
|
220
|
-
* against that environment's own graph.
|
|
221
|
-
*
|
|
222
|
-
* The client dispatch always claims — it is dispatched first, so nothing can
|
|
223
|
-
* have claimed the event before it, and claiming unconditionally keeps a
|
|
224
|
-
* same-millisecond `(file, timestamp)` collision from ever costing a client
|
|
225
|
-
* analysis. A non-client dispatch claims only when the event carries no
|
|
226
|
-
* recorded claim at all, which happens when the plugin is filtered out of the
|
|
227
|
-
* client environment.
|
|
228
|
-
*
|
|
229
|
-
* Keys are retained for a bounded number of recent events so that interleaved
|
|
230
|
-
* file events (Vite does not serialize watcher handlers) still find their own
|
|
231
|
-
* result.
|
|
182
|
+
* Vite dispatches `hotUpdate` once per environment, client first: analysis
|
|
183
|
+
* runs for one dispatch, invalidation for all, coordinated through these keys.
|
|
232
184
|
*/
|
|
233
185
|
var HotUpdateEvents = class {
|
|
186
|
+
historyLimit;
|
|
234
187
|
results = /* @__PURE__ */ new Map();
|
|
188
|
+
/** Keys whose decision aged out of `results`, in eviction order. */
|
|
189
|
+
evicted = /* @__PURE__ */ new Set();
|
|
235
190
|
constructor(historyLimit = 16) {
|
|
236
191
|
this.historyLimit = historyLimit;
|
|
237
192
|
}
|
|
238
193
|
/** True when this environment dispatch owns the event's analysis work. */
|
|
239
194
|
claim(environmentName, file, timestamp) {
|
|
240
195
|
const key = eventKey(file, timestamp);
|
|
241
|
-
if (environmentName !== "client" && this.results.has(key)) return false;
|
|
196
|
+
if (environmentName !== "client" && (this.results.has(key) || this.evicted.has(key))) return false;
|
|
242
197
|
this.results.set(key, { kind: "ignored" });
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (oldest === void 0) break;
|
|
246
|
-
this.results.delete(oldest);
|
|
247
|
-
}
|
|
198
|
+
this.evicted.delete(key);
|
|
199
|
+
this.retireOldest();
|
|
248
200
|
return true;
|
|
249
201
|
}
|
|
250
202
|
/** Publish the owning dispatch's decision to the other environments. */
|
|
251
203
|
record(file, timestamp, result) {
|
|
252
|
-
|
|
204
|
+
const key = eventKey(file, timestamp);
|
|
205
|
+
this.results.set(key, result);
|
|
206
|
+
this.evicted.delete(key);
|
|
207
|
+
this.retireOldest();
|
|
253
208
|
}
|
|
254
|
-
/**
|
|
209
|
+
/**
|
|
210
|
+
* The decision for this event — `evicted` once it aged out of the decision
|
|
211
|
+
* window, `ignored` only when no dispatch ever claimed it.
|
|
212
|
+
*/
|
|
255
213
|
resultOf(file, timestamp) {
|
|
256
|
-
|
|
214
|
+
const key = eventKey(file, timestamp);
|
|
215
|
+
const result = this.results.get(key);
|
|
216
|
+
if (result) return result;
|
|
217
|
+
return this.evicted.has(key) ? { kind: "evicted" } : { kind: "ignored" };
|
|
218
|
+
}
|
|
219
|
+
/** Hold both windows at `historyLimit`, oldest insertion first. */
|
|
220
|
+
retireOldest() {
|
|
221
|
+
while (this.results.size > this.historyLimit) {
|
|
222
|
+
const oldest = this.results.keys().next().value;
|
|
223
|
+
if (oldest === void 0) break;
|
|
224
|
+
this.results.delete(oldest);
|
|
225
|
+
this.evicted.add(oldest);
|
|
226
|
+
}
|
|
227
|
+
while (this.evicted.size > this.historyLimit) {
|
|
228
|
+
const oldest = this.evicted.values().next().value;
|
|
229
|
+
if (oldest === void 0) break;
|
|
230
|
+
this.evicted.delete(oldest);
|
|
231
|
+
}
|
|
257
232
|
}
|
|
258
233
|
};
|
|
259
234
|
/** A path can hold any character but NUL, so NUL is the safe separator. */
|
|
@@ -263,16 +238,8 @@ function eventKey(file, timestamp) {
|
|
|
263
238
|
//#endregion
|
|
264
239
|
//#region src/module-invalidation.ts
|
|
265
240
|
/**
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
* sets (covers query-suffixed variants), plus the absolute-path id lookup.
|
|
269
|
-
* Falls back to the mixed compat graph for hosts without environment
|
|
270
|
-
* graphs. Returns the number of nodes invalidated.
|
|
271
|
-
*
|
|
272
|
-
* Completeness here is load-bearing: the file-plan diff is the ONLY
|
|
273
|
-
* invalidation candidate source, so a node shape this enumeration misses
|
|
274
|
-
* stays stale for the life of the server (openspec: dev-transform-coherence,
|
|
275
|
-
* "Client and SSR nodes are both evicted").
|
|
241
|
+
* Completeness is load-bearing: the file-plan diff is the only invalidation
|
|
242
|
+
* candidate source, so a node shape missed here stays stale for the session.
|
|
276
243
|
*/
|
|
277
244
|
function invalidateFileModules(ctx, relPaths) {
|
|
278
245
|
const server = ctx.devServer;
|
|
@@ -300,16 +267,12 @@ function invalidateFileModules(ctx, relPaths) {
|
|
|
300
267
|
}
|
|
301
268
|
//#endregion
|
|
302
269
|
//#region src/reset-coalescer.ts
|
|
303
|
-
/**
|
|
304
|
-
* Coalesces bursts of system-dependency events into single geological
|
|
305
|
-
* resets: a quiescence window before starting, at most one reset in
|
|
306
|
-
* flight, and exactly one follow-up when requests arrive mid-reset. A
|
|
307
|
-
* package build regenerating its dist fires one reset, not one per file.
|
|
308
|
-
*
|
|
309
|
-
* Timer functions are injected seams — `vi.mock` on node builtins silently
|
|
310
|
-
* no-ops under vite-plus-test, so tests drive a manual scheduler instead.
|
|
311
|
-
*/
|
|
312
270
|
var ResetCoalescer = class {
|
|
271
|
+
run;
|
|
272
|
+
onError;
|
|
273
|
+
quietMs;
|
|
274
|
+
schedule;
|
|
275
|
+
cancel;
|
|
313
276
|
timer = null;
|
|
314
277
|
running = false;
|
|
315
278
|
dirty = false;
|
|
@@ -347,7 +310,6 @@ var ResetCoalescer = class {
|
|
|
347
310
|
};
|
|
348
311
|
//#endregion
|
|
349
312
|
//#region src/context.ts
|
|
350
|
-
/** Pre-load / failed-load defaults — the plugin's historical initial state. */
|
|
351
313
|
function emptySystemConfig() {
|
|
352
314
|
return {
|
|
353
315
|
propConfigJson: "{}",
|
|
@@ -361,24 +323,14 @@ function emptySystemConfig() {
|
|
|
361
323
|
keyframesJson: null
|
|
362
324
|
};
|
|
363
325
|
}
|
|
364
|
-
/** Full raw originals for one adaptation attempt. */
|
|
365
|
-
function buildRawEntriesFromCache(cache) {
|
|
366
|
-
return [...cache].map(([path$1, { hash, source }]) => ({
|
|
367
|
-
path: path$1,
|
|
368
|
-
source,
|
|
369
|
-
hash
|
|
370
|
-
}));
|
|
371
|
-
}
|
|
372
326
|
const analysisChains = /* @__PURE__ */ new WeakMap();
|
|
373
|
-
/**
|
|
374
|
-
*
|
|
375
|
-
* geological reset); helpers they call internally must stay unlocked. */
|
|
327
|
+
/** Runs `task` after every analysis transaction already scheduled for this
|
|
328
|
+
* context. Entry points only — a nested call waits on its own chain. */
|
|
376
329
|
function runExclusiveAnalysis(ctx, task) {
|
|
377
330
|
const result = (analysisChains.get(ctx) ?? Promise.resolve()).then(task);
|
|
378
331
|
analysisChains.set(ctx, result.then(() => void 0, () => void 0));
|
|
379
332
|
return result;
|
|
380
333
|
}
|
|
381
|
-
/** Generate the module from the four inputs the context currently holds. */
|
|
382
334
|
function generateSystemPropsModule(ctx) {
|
|
383
335
|
return (0, _animus_ui_extract_pipeline.buildSystemPropsModule)({
|
|
384
336
|
systemPropMapJson: ctx.storedSystemPropMapJson,
|
|
@@ -387,52 +339,47 @@ function generateSystemPropsModule(ctx) {
|
|
|
387
339
|
transformsSource: ctx.storedTransformsSource
|
|
388
340
|
});
|
|
389
341
|
}
|
|
390
|
-
/**
|
|
391
|
-
*
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
342
|
+
/** Memoized on the inputs themselves, so no writer owes the memo a refresh
|
|
343
|
+
* and a stale memo cannot serve a module from a generation that is gone. */
|
|
344
|
+
const systemPropsModules = /* @__PURE__ */ new WeakMap();
|
|
345
|
+
/** NUL cannot appear in JSON text, so concatenation is injective. */
|
|
346
|
+
function systemPropsModuleKey(ctx) {
|
|
347
|
+
return [
|
|
348
|
+
ctx.storedSystemPropMapJson,
|
|
349
|
+
ctx.system.groupRegistryJson,
|
|
350
|
+
ctx.storedDynamicPropsJson,
|
|
351
|
+
ctx.storedTransformsSource
|
|
352
|
+
].join("\0");
|
|
353
|
+
}
|
|
354
|
+
/** The exact source `virtual:animus/system-props` serves now — one definition,
|
|
355
|
+
* so the served bytes and the HMR change decision cannot diverge. */
|
|
400
356
|
function systemPropsModuleSource(ctx) {
|
|
401
|
-
|
|
357
|
+
const key = systemPropsModuleKey(ctx);
|
|
358
|
+
const memo = systemPropsModules.get(ctx);
|
|
359
|
+
if (memo && memo.key === key) return memo.source;
|
|
360
|
+
const source = generateSystemPropsModule(ctx);
|
|
361
|
+
systemPropsModules.set(ctx, {
|
|
362
|
+
key,
|
|
363
|
+
source
|
|
364
|
+
});
|
|
365
|
+
return source;
|
|
402
366
|
}
|
|
403
|
-
/**
|
|
404
|
-
*
|
|
405
|
-
* Generated MDX/Svelte children live in `analysisEntryCache` and disappear
|
|
406
|
-
* atomically when the next source-ingestion result publishes. External
|
|
407
|
-
* package entries are rootDir-relative too (with leading `..` segments).
|
|
408
|
-
*/
|
|
367
|
+
/** Drop a deleted or renamed-away raw original from the dev file cache.
|
|
368
|
+
* Generated MDX/Svelte children live only in the published corpus. */
|
|
409
369
|
function pruneFileCache(cache, rootDir, absPath) {
|
|
410
370
|
const rel = (0, path.relative)(rootDir, (0, path.resolve)(absPath));
|
|
411
371
|
return cache.delete(rel);
|
|
412
372
|
}
|
|
413
|
-
/**
|
|
414
|
-
* Per-plugin-instance state and the pipeline operations over it. Hook
|
|
415
|
-
* bodies live in their own modules (build-start, virtual-modules,
|
|
416
|
-
* transform, hmr) and receive this context — the plugin factory in
|
|
417
|
-
* index.ts only wires Vite hooks to those functions.
|
|
418
|
-
*
|
|
419
|
-
* A class rather than closure variables so each hook module names exactly
|
|
420
|
-
* the state it touches, and the engine store (per-instance, never
|
|
421
|
-
* module-level) is explicit.
|
|
422
|
-
*/
|
|
423
373
|
var PluginContext = class {
|
|
424
374
|
options;
|
|
425
375
|
verbose;
|
|
426
|
-
/** Serialized staticCss forced-emission declarations (stable key order). */
|
|
427
376
|
staticCssJson;
|
|
428
377
|
isProd = false;
|
|
429
|
-
/** Emission
|
|
430
|
-
*
|
|
431
|
-
* `isProd`. */
|
|
378
|
+
/** Emission mode (the explicit `mode` option wins over the command); feeds
|
|
379
|
+
* engine devMode and the minify default. Lifecycle stays on `isProd`. */
|
|
432
380
|
emissionProd = false;
|
|
433
381
|
rootDir = "";
|
|
434
382
|
logger = null;
|
|
435
|
-
/** System-derived config (shared SystemConfig shape). */
|
|
436
383
|
system = emptySystemConfig();
|
|
437
384
|
lcssTargets = {};
|
|
438
385
|
pathAliasesJson = null;
|
|
@@ -448,46 +395,43 @@ var PluginContext = class {
|
|
|
448
395
|
recordTransformOutput(relativePath, code) {
|
|
449
396
|
this.transformOutputHashes.set(relativePath, (0, _animus_ui_extract_pipeline.contentHash)(code));
|
|
450
397
|
}
|
|
451
|
-
/**
|
|
452
|
-
*
|
|
453
|
-
|
|
454
|
-
* `applyExternalKeyframes` merge starts from this baseline, so repeated
|
|
455
|
-
* merges (a --watch rebuild's loadSystem + rediscovery, a geological
|
|
456
|
-
* reset) never compound prior external state — a removed include's
|
|
457
|
-
* keyframes disappear with it.
|
|
458
|
-
*/
|
|
459
|
-
consumerKeyframesJson = null;
|
|
460
|
-
/**
|
|
461
|
-
* Merge `Keyframes` collections from discovered external package entries
|
|
462
|
-
* into the system's collections (keyframes-only carve-out — the consumer
|
|
463
|
-
* system stays the singular config authority). Runs after buildStart
|
|
464
|
-
* discovery AND after every geological-reset system reload, since a reload
|
|
465
|
-
* rebuilds `this.system` from the consumer entry alone. Merges from the
|
|
466
|
-
* consumer-only baseline, never from the previously merged value.
|
|
467
|
-
*/
|
|
468
|
-
applyExternalKeyframes() {
|
|
469
|
-
if (this.externalKeyframesScanEntries.size === 0) {
|
|
470
|
-
this.system.keyframesJson = this.consumerKeyframesJson;
|
|
471
|
-
this.externalKeyframesDiagnostics = [];
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
|
-
const merge = (0, _animus_ui_extract_pipeline.mergeExternalKeyframes)((entry, root) => this.engineApi().scanKeyframesExports(entry, root), this.consumerKeyframesJson, this.externalKeyframesScanEntries.values(), this.rootDir);
|
|
475
|
-
this.system.keyframesJson = merge.keyframesJson;
|
|
476
|
-
this.externalKeyframesDiagnostics = merge.diagnostics;
|
|
477
|
-
}
|
|
478
|
-
/** Discovery-time keyframes diagnostics awaiting the next analysis's
|
|
479
|
-
* shared surfacing pass. */
|
|
480
|
-
externalKeyframesDiagnostics = [];
|
|
398
|
+
/** Vocabulary witnesses from the sealed system, awaiting the next analysis.
|
|
399
|
+
* Surfacing happens only inside `runProjectAnalysis`. */
|
|
400
|
+
systemVocabularyDiagnostics = [];
|
|
481
401
|
reverseProvenance = {};
|
|
482
402
|
storedSystemPropMapJson = "{}";
|
|
483
403
|
storedDynamicPropsJson = "{}";
|
|
484
404
|
storedTransformsSource = "{}";
|
|
485
|
-
systemPropsModuleMemo = null;
|
|
486
405
|
fileCache = /* @__PURE__ */ new Map();
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
406
|
+
/** Mutation attempts against `fileCache`, for memos that must not key on
|
|
407
|
+
* `size` — a delete plus an unrelated create leaves it unchanged. */
|
|
408
|
+
fileCacheGeneration = 0;
|
|
409
|
+
/** The one mutator of the raw-source cache: every add, replacement and
|
|
410
|
+
* deletion goes through here so no write escapes the counter above. */
|
|
411
|
+
mutateFileCache(mutate) {
|
|
412
|
+
const cache = this.fileCache;
|
|
413
|
+
const result = mutate(cache);
|
|
414
|
+
this.fileCacheGeneration += 1;
|
|
415
|
+
return result;
|
|
416
|
+
}
|
|
417
|
+
/** Generated MDX/Svelte paths never enter `fileCache` — they live only in
|
|
418
|
+
* this corpus's published projection. */
|
|
419
|
+
corpus = (0, _animus_ui_extract_pipeline.createSourceCorpus)({
|
|
420
|
+
engineApi: () => this.engineApi(),
|
|
421
|
+
prefix: "[animus-extract]",
|
|
422
|
+
strict: () => !!this.options.strict,
|
|
423
|
+
warn: (message) => this.warn(message)
|
|
424
|
+
});
|
|
490
425
|
rawExtensionFallbacks = /* @__PURE__ */ new Set();
|
|
426
|
+
/** The one mutator of a file's fallback state — every raw-serve exit of the
|
|
427
|
+
* transform hook reports here. Production keeps the set empty. The
|
|
428
|
+
* barrier's one-shot withhold release must not route through here: it
|
|
429
|
+
* retires other files' records, no claim about their current serve. */
|
|
430
|
+
recordFallbackState(relativePath, isFallback) {
|
|
431
|
+
if (this.isProd) return;
|
|
432
|
+
if (isFallback) this.rawExtensionFallbacks.add(relativePath);
|
|
433
|
+
else this.rawExtensionFallbacks.delete(relativePath);
|
|
434
|
+
}
|
|
491
435
|
hotUpdateEvents = new HotUpdateEvents();
|
|
492
436
|
pendingReloadTimer = null;
|
|
493
437
|
packageMap = {};
|
|
@@ -499,7 +443,6 @@ var PluginContext = class {
|
|
|
499
443
|
externalDirOwners = {};
|
|
500
444
|
externalFileOwners = {};
|
|
501
445
|
externalSourceEntries = /* @__PURE__ */ new Map();
|
|
502
|
-
externalKeyframesScanEntries = /* @__PURE__ */ new Map();
|
|
503
446
|
externalPackageOutcomes = [];
|
|
504
447
|
devServer;
|
|
505
448
|
resolvedSystemPath = null;
|
|
@@ -508,7 +451,7 @@ var PluginContext = class {
|
|
|
508
451
|
v2Engine = null;
|
|
509
452
|
v2SentSources = null;
|
|
510
453
|
v2DriftWarned = false;
|
|
511
|
-
/**
|
|
454
|
+
/** Every native extraction call goes through this one accessor. */
|
|
512
455
|
engineApi;
|
|
513
456
|
constructor(options, engineApiOverride) {
|
|
514
457
|
this.options = options;
|
|
@@ -527,8 +470,9 @@ var PluginContext = class {
|
|
|
527
470
|
loadNativeEngine: () => require(engineModuleId),
|
|
528
471
|
rehydrateFilesJson: (filesJsonRaw) => {
|
|
529
472
|
if (!filesJsonRaw.includes("\"source\":\"\"")) return filesJsonRaw;
|
|
530
|
-
const entries =
|
|
531
|
-
|
|
473
|
+
const entries = (0, _animus_ui_extract_pipeline.parseFilesJson)(filesJsonRaw, "animus-extract");
|
|
474
|
+
const analysisEntries = this.corpus.published.analysisEntries;
|
|
475
|
+
for (const entry of entries) if (entry.source === "") entry.source = analysisEntries.get(entry.path)?.source ?? "";
|
|
532
476
|
return JSON.stringify(entries);
|
|
533
477
|
},
|
|
534
478
|
store: {
|
|
@@ -550,12 +494,8 @@ var PluginContext = class {
|
|
|
550
494
|
log(msg) {
|
|
551
495
|
if (this.verbose) (this.logger ?? console).info(`[animus] ${msg}`);
|
|
552
496
|
}
|
|
553
|
-
/**
|
|
554
|
-
*
|
|
555
|
-
* Reserved for events a developer must see without opting in, e.g. a file
|
|
556
|
-
* created after buildStart being folded into the analysis (openspec:
|
|
557
|
-
* hmr-new-file-detection, "New file detection logging").
|
|
558
|
-
*/
|
|
497
|
+
/** Logs whether or not `verbose` is on — for events a developer must see
|
|
498
|
+
* without opting in. */
|
|
559
499
|
info(msg) {
|
|
560
500
|
(this.logger ?? console).info(`[animus] ${msg}`);
|
|
561
501
|
}
|
|
@@ -569,11 +509,8 @@ var PluginContext = class {
|
|
|
569
509
|
labelWidth: 15
|
|
570
510
|
})) this.log(line);
|
|
571
511
|
}
|
|
572
|
-
/**
|
|
573
|
-
*
|
|
574
|
-
* `this.system`. On failure the previous config is kept (strict mode
|
|
575
|
-
* throws instead).
|
|
576
|
-
*/
|
|
512
|
+
/** Load the system into `this.system`. On failure the previous config is
|
|
513
|
+
* kept; strict mode throws instead. */
|
|
577
514
|
loadSystem() {
|
|
578
515
|
this.resolvedSystemPath = (0, path.resolve)(this.rootDir, this.options.system);
|
|
579
516
|
for (const key of (0, _animus_ui_extract_pipeline.toWatchKeys)(this.resolvedSystemPath)) this.systemDependencyKeys.add(key);
|
|
@@ -590,23 +527,14 @@ var PluginContext = class {
|
|
|
590
527
|
this.systemDependencyKeys = keys;
|
|
591
528
|
this.systemDependencyPaths = deps;
|
|
592
529
|
this.registerSystemWatchPaths();
|
|
593
|
-
this.
|
|
594
|
-
this.applyExternalKeyframes();
|
|
530
|
+
this.systemVocabularyDiagnostics = (0, _animus_ui_extract_pipeline.vocabularyWitnessDiagnostics)(this.system.vocabularyWitnessesJson);
|
|
595
531
|
} catch (e) {
|
|
596
532
|
if (this.options.strict) throw new Error(`[animus-extract] Failed to load system from ${this.resolvedSystemPath}: ${e}`, { cause: e });
|
|
597
533
|
console.warn(`[animus-extract] Failed to load system from ${this.resolvedSystemPath}:`, e);
|
|
598
534
|
}
|
|
599
|
-
this.systemPropsModuleMemo = generateSystemPropsModule(this);
|
|
600
535
|
}
|
|
601
|
-
/**
|
|
602
|
-
*
|
|
603
|
-
* all manifest-derived state. Returns whether the analysis PUBLISHED —
|
|
604
|
-
* `false` means the previous manifest is still current, and callers that
|
|
605
|
-
* advanced the file cache for this attempt must roll that entry back or
|
|
606
|
-
* the content-hash gate will suppress the same-content retry forever
|
|
607
|
-
* (openspec: dev-transform-coherence, "Failed analyses do not suppress
|
|
608
|
-
* equal-content retries"). Strict mode still throws.
|
|
609
|
-
*/
|
|
536
|
+
/** Runs project analysis and updates every manifest-derived state. Returns
|
|
537
|
+
* false when nothing published, and the caller must roll its cache back. */
|
|
610
538
|
runAnalysis(fileEntries) {
|
|
611
539
|
let result;
|
|
612
540
|
try {
|
|
@@ -624,81 +552,42 @@ var PluginContext = class {
|
|
|
624
552
|
devMode: !this.emissionProd,
|
|
625
553
|
warn: (m) => this.warn(m),
|
|
626
554
|
strict: this.options.strict,
|
|
627
|
-
extraDiagnostics: this.
|
|
555
|
+
extraDiagnostics: this.systemVocabularyDiagnostics
|
|
628
556
|
});
|
|
629
557
|
} catch (e) {
|
|
630
558
|
if (this.options.strict) throw new Error(`[animus-extract] analyzeProject failed: ${e}`, { cause: e });
|
|
631
559
|
console.warn("[animus-extract] analyzeProject failed:", e);
|
|
632
560
|
return false;
|
|
633
561
|
}
|
|
634
|
-
(0, _animus_ui_extract_pipeline.assertNoErrorDiagnostics)(result.manifest
|
|
562
|
+
(0, _animus_ui_extract_pipeline.assertNoErrorDiagnostics)(result.manifest.diagnostics);
|
|
635
563
|
this.assertRuntimeImportSuppliesTerminals(result.manifest);
|
|
636
564
|
this.storedManifest = result.manifest;
|
|
637
565
|
this.storedManifestJson = result.manifestJson;
|
|
638
|
-
this.storedSystemPropMapJson = JSON.stringify(result.manifest
|
|
639
|
-
this.storedDynamicPropsJson = JSON.stringify(result.manifest
|
|
640
|
-
this.reverseProvenance = result.manifest
|
|
641
|
-
this.storedSheets = result.manifest
|
|
566
|
+
this.storedSystemPropMapJson = JSON.stringify(result.manifest.system_prop_map);
|
|
567
|
+
this.storedDynamicPropsJson = JSON.stringify(result.manifest.dynamic_props);
|
|
568
|
+
this.reverseProvenance = result.manifest.reverse_provenance;
|
|
569
|
+
this.storedSheets = result.manifest.sheets;
|
|
642
570
|
this.globalCss = result.globalCss;
|
|
643
571
|
this.resolvedComponentCss = result.componentCss;
|
|
644
|
-
this.systemPropsModuleMemo = generateSystemPropsModule(this);
|
|
645
572
|
this.applyAssetSubstitutions();
|
|
646
573
|
return true;
|
|
647
574
|
}
|
|
648
|
-
/**
|
|
649
|
-
* `runtimeImport` swaps ONE module specifier under every generated
|
|
650
|
-
* factory import, and the engine performs no export validation — an
|
|
651
|
-
* override that supplies only `createClassResolver` (the documented
|
|
652
|
-
* class-resolver entry contract) breaks the bundle at LOAD the moment any
|
|
653
|
-
* non-`.asClass()` terminal exists, with the error pointing at generated
|
|
654
|
-
* code. Guaranteed-broken output escalates in every mode, exactly like
|
|
655
|
-
* error diagnostics (extraction-diagnostics §Error diagnostics fail the
|
|
656
|
-
* build), naming the offending components instead of the import site.
|
|
657
|
-
*/
|
|
658
575
|
assertRuntimeImportSuppliesTerminals(manifest) {
|
|
659
576
|
const override = this.options.runtimeImport;
|
|
660
577
|
if (!override || override === "@animus-ui/system") return;
|
|
661
578
|
const offenders = [];
|
|
662
|
-
for (const [id, descriptor] of Object.entries(manifest
|
|
663
|
-
const replacement = String(descriptor?.replacement ?? "");
|
|
664
|
-
if (/\bcreateComponent\(|\bcreateComposedFamily\(/.test(replacement)) offenders.push(id);
|
|
665
|
-
}
|
|
579
|
+
for (const [id, descriptor] of Object.entries(manifest.components)) if (/\bcreateComponent\(|\bcreateComposedFamily\(/.test(descriptor.replacement)) offenders.push(id);
|
|
666
580
|
if (offenders.length === 0) return;
|
|
667
581
|
const shown = offenders.slice(0, 5).join(", ");
|
|
668
582
|
throw new Error(`[animus-extract] runtimeImport '${override}' is valid only when every extracted terminal is .asClass(), but ${offenders.length} component(s) need createComponent/createComposedFamily from the default '@animus-ui/system' runtime: ${shown}` + (offenders.length > 5 ? ", …" : "") + `. Remove the override or convert these terminals to .asClass().`);
|
|
669
583
|
}
|
|
670
|
-
/** The
|
|
671
|
-
*
|
|
672
|
-
sourceIngestor = (0, _animus_ui_extract_pipeline.createSourceIngestor)({
|
|
673
|
-
engineApi: () => this.engineApi(),
|
|
674
|
-
prefix: "[animus-extract]",
|
|
675
|
-
strict: () => !!this.options.strict,
|
|
676
|
-
warn: (message) => this.warn(message)
|
|
677
|
-
});
|
|
678
|
-
/** Prepare one raw-source corpus through the shared adaptation boundary. */
|
|
679
|
-
async ingestRawSources(fileEntries) {
|
|
680
|
-
return this.sourceIngestor.ingest(fileEntries);
|
|
681
|
-
}
|
|
682
|
-
/** Surface adapter diagnostics under the shared strict/warn policy. */
|
|
683
|
-
surfaceSourceDiagnostics(diagnostics) {
|
|
684
|
-
return this.sourceIngestor.surfaceDiagnostics(diagnostics);
|
|
685
|
-
}
|
|
686
|
-
/**
|
|
687
|
-
* The one ingest → quarantine → analyze → publish transaction, shared by
|
|
688
|
-
* buildStart and every incremental path. Per-file quarantine: one invalid
|
|
689
|
-
* original warns and drops without aborting the rest of the corpus
|
|
690
|
-
* (strict mode throws from the shared policy). `runAnalysis(...) !==
|
|
691
|
-
* false` is the documented success contract (a `void` behavioral test
|
|
692
|
-
* double reads as success), and the accepted corpus publishes only on
|
|
693
|
-
* success. `beforeAnalysis` runs between quarantine and analysis for the
|
|
694
|
-
* two sites with a documented ordering constraint: buildStart's dev-cache
|
|
695
|
-
* seed must survive a strict analysis throw, and the geological reset
|
|
696
|
-
* clears the engine cache only after strict ingestion diagnostics had
|
|
697
|
-
* their chance to throw.
|
|
698
|
-
*/
|
|
584
|
+
/** The one ingest → quarantine → analyze → publish transaction. The corpus
|
|
585
|
+
* publishes only on success; `beforeAnalysis` runs after quarantine. */
|
|
699
586
|
async analyzeIngested(options) {
|
|
700
|
-
const
|
|
701
|
-
|
|
587
|
+
const accepted = await this.corpus.prepare(options?.rawEntries ?? {
|
|
588
|
+
fileCache: this.fileCache,
|
|
589
|
+
externalFileOwners: this.externalFileOwners
|
|
590
|
+
});
|
|
702
591
|
options?.beforeAnalysis?.(accepted);
|
|
703
592
|
const ok = this.runAnalysis(accepted.analysisEntries) !== false;
|
|
704
593
|
if (ok) this.publishSourceIngestion(accepted);
|
|
@@ -707,48 +596,22 @@ var PluginContext = class {
|
|
|
707
596
|
accepted
|
|
708
597
|
};
|
|
709
598
|
}
|
|
710
|
-
/** Publish every parser child and ownership edge atomically after analysis. */
|
|
711
599
|
publishSourceIngestion(result) {
|
|
712
|
-
this.
|
|
713
|
-
|
|
714
|
-
this.analysisEntryCache = new Map(result.analysisEntries.map((entry) => [entry.path, {
|
|
715
|
-
hash: entry.hash,
|
|
716
|
-
source: entry.source
|
|
717
|
-
}]));
|
|
718
|
-
this.sourceOwnership = result.ownership;
|
|
719
|
-
this.analysisOwnerByPath = /* @__PURE__ */ new Map();
|
|
720
|
-
for (const owner of Object.values(result.ownership)) for (const analysisPath of owner.analysisPaths) {
|
|
721
|
-
this.analysisOwnerByPath.set(analysisPath, owner.originalPath);
|
|
722
|
-
const externalOwner = this.externalFileOwners[owner.originalPath];
|
|
723
|
-
if (externalOwner) this.externalFileOwners[analysisPath] = externalOwner;
|
|
724
|
-
}
|
|
725
|
-
for (const stalePath of priorAnalysisPaths) if (!this.analysisOwnerByPath.has(stalePath)) delete this.externalFileOwners[stalePath];
|
|
600
|
+
this.corpus.publish(result);
|
|
601
|
+
this.externalFileOwners = (0, _animus_ui_extract_pipeline.projectExternalFileOwners)(result, this.externalFileOwners);
|
|
726
602
|
this.enforceExternalTokenContracts();
|
|
727
603
|
}
|
|
728
|
-
/** Base-prefixed dev URL for an absolute file (Vite mounts /@fs under base). */
|
|
729
604
|
devFsUrl(absPath) {
|
|
730
605
|
return `${this.base.endsWith("/") ? this.base.slice(0, -1) : this.base}/@fs${absPath}`;
|
|
731
606
|
}
|
|
732
|
-
/**
|
|
733
|
-
* The shared unresolvable-asset policy: fail the build under `strict`,
|
|
734
|
-
* else warn and record the identity mapping so the specifier emits
|
|
735
|
-
* literally.
|
|
736
|
-
*/
|
|
737
607
|
assetFallback(specifier, message, cause) {
|
|
738
608
|
if (this.options.strict) throw new Error(message, cause === void 0 ? void 0 : { cause });
|
|
739
609
|
this.warn(message);
|
|
740
610
|
this.assetUrlBySpecifier.set(specifier, specifier);
|
|
741
611
|
this.assetResolutionFailures.add(specifier);
|
|
742
612
|
}
|
|
743
|
-
/**
|
|
744
|
-
*
|
|
745
|
-
* already mapped by buildStart's bundler-resolved pass (a geological
|
|
746
|
-
* reset regenerates globalCss from an edited system, which may reference
|
|
747
|
-
* NEW assets) is resolved Node-side first — host aliases, then Node,
|
|
748
|
-
* then package root; bundler-only resolutions can differ, since the
|
|
749
|
-
* plugin hook context is unavailable on this path. Strict semantics
|
|
750
|
-
* match buildStart.
|
|
751
|
-
*/
|
|
613
|
+
/** Resolves asset specifiers a system reload introduced. Node-side only —
|
|
614
|
+
* the plugin hook context is unavailable here, so results can differ. */
|
|
752
615
|
applyAssetSubstitutions() {
|
|
753
616
|
if (!this.isProd && this.assetPassComplete) for (const specifier of (0, _animus_ui_extract_pipeline.findAssetSpecifiers)(this.globalCss)) {
|
|
754
617
|
if (this.assetResolutionFailures.delete(specifier)) this.assetUrlBySpecifier.delete(specifier);
|
|
@@ -760,47 +623,33 @@ var PluginContext = class {
|
|
|
760
623
|
this.globalCss = (0, _animus_ui_extract_pipeline.substituteAssetPlaceholders)(this.globalCss, this.assetUrlBySpecifier);
|
|
761
624
|
}
|
|
762
625
|
resetCoalescer = null;
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
this.warn(`[animus-extract] geological reset failed: ${err}`);
|
|
783
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
784
|
-
const stack = err instanceof Error ? err.stack ?? "" : "";
|
|
785
|
-
this.devServer?.hot?.send({
|
|
786
|
-
type: "error",
|
|
787
|
-
err: {
|
|
788
|
-
message,
|
|
789
|
-
stack,
|
|
790
|
-
plugin: "animus-extract"
|
|
626
|
+
requestSystemReload(trigger) {
|
|
627
|
+
this.log(`HMR system reload scheduled: ${trigger}`);
|
|
628
|
+
this.resetCoalescer ??= new ResetCoalescer(
|
|
629
|
+
async () => this.performSystemReload(),
|
|
630
|
+
/** The coalescer fires from a bare timer, outside Vite's error
|
|
631
|
+
* handling, so an unhandled strict throw would exit the process. */
|
|
632
|
+
(err) => {
|
|
633
|
+
const display = String(err);
|
|
634
|
+
this.warn(`[animus-extract] system reload failed: ${display}`);
|
|
635
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
636
|
+
const stack = err instanceof Error ? err.stack ?? "" : "";
|
|
637
|
+
this.devServer?.hot?.send({
|
|
638
|
+
type: "error",
|
|
639
|
+
err: {
|
|
640
|
+
message,
|
|
641
|
+
stack,
|
|
642
|
+
plugin: "animus-extract"
|
|
643
|
+
}
|
|
644
|
+
});
|
|
791
645
|
}
|
|
792
|
-
|
|
646
|
+
);
|
|
647
|
+
this.resetCoalescer.request();
|
|
793
648
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
* set), clear the Rust per-file cache, re-analyze everything with full
|
|
797
|
-
* sources, then invalidate the static/component/system-prop virtual
|
|
798
|
-
* modules and reload the client.
|
|
799
|
-
*/
|
|
800
|
-
async performGeologicalReset() {
|
|
801
|
-
return runExclusiveAnalysis(this, () => this.performGeologicalResetExclusive());
|
|
649
|
+
async performSystemReload() {
|
|
650
|
+
return runExclusiveAnalysis(this, () => this.performSystemReloadExclusive());
|
|
802
651
|
}
|
|
803
|
-
async
|
|
652
|
+
async performSystemReloadExclusive() {
|
|
804
653
|
const resetStart = performance.now();
|
|
805
654
|
const prevPlans = (0, _animus_ui_extract_pipeline.snapshotFilePlans)(this.storedManifest);
|
|
806
655
|
try {
|
|
@@ -808,12 +657,12 @@ var PluginContext = class {
|
|
|
808
657
|
const { ok } = await this.analyzeIngested({ beforeAnalysis: () => (0, _animus_ui_extract_pipeline.clearEngineCache)(this.engineApi) });
|
|
809
658
|
if (!ok) return;
|
|
810
659
|
invalidateFileModules(this, (0, _animus_ui_extract_pipeline.diffFilePlans)(prevPlans, (0, _animus_ui_extract_pipeline.snapshotFilePlans)(this.storedManifest)));
|
|
811
|
-
this.log(`HMR
|
|
660
|
+
this.log(`HMR system reload complete: ${Math.round(performance.now() - resetStart)}ms`);
|
|
812
661
|
} finally {
|
|
813
|
-
this.
|
|
662
|
+
this.invalidateSystemReloadModules();
|
|
814
663
|
}
|
|
815
664
|
}
|
|
816
|
-
|
|
665
|
+
invalidateSystemReloadModules() {
|
|
817
666
|
const server = this.devServer;
|
|
818
667
|
if (!server) return;
|
|
819
668
|
for (const moduleId of [
|
|
@@ -826,49 +675,21 @@ var PluginContext = class {
|
|
|
826
675
|
}
|
|
827
676
|
server.hot?.send({ type: "full-reload" });
|
|
828
677
|
}
|
|
829
|
-
/**
|
|
830
|
-
* True when the file is a member of the system's evaluated module-file
|
|
831
|
-
* set. Tested BEFORE component-scan exclude filters: a system dependency
|
|
832
|
-
* edit invalidates the compiler registry regardless of what the user
|
|
833
|
-
* excluded from component scanning.
|
|
834
|
-
*/
|
|
835
678
|
isSystemDependency(absFile) {
|
|
836
679
|
return (0, _animus_ui_extract_pipeline.toWatchKeys)(absFile).some((key) => this.systemDependencyKeys.has(key));
|
|
837
680
|
}
|
|
838
|
-
/**
|
|
839
|
-
*
|
|
840
|
-
* Covers system modules unreachable from app imports (workspace package
|
|
841
|
-
* config files). Vite's watcher hard-ignores `node_modules`, so
|
|
842
|
-
* node_modules-installed system dependencies produce no events — a
|
|
843
|
-
* documented limitation; workspace deps resolve to real paths outside
|
|
844
|
-
* node_modules and are watchable. No-ops without a dev server.
|
|
845
|
-
*/
|
|
681
|
+
/** Watch every loader-reported dependency path. Vite hard-ignores
|
|
682
|
+
* node_modules, so dependencies installed there produce no events. */
|
|
846
683
|
registerSystemWatchPaths() {
|
|
847
684
|
const watcher = this.devServer?.watcher;
|
|
848
685
|
if (!watcher) return;
|
|
849
686
|
if (this.systemDependencyPaths.length > 0) watcher.add(this.systemDependencyPaths);
|
|
850
687
|
if (this.externalPackageDirs.length > 0) watcher.add(this.externalPackageDirs);
|
|
851
688
|
}
|
|
852
|
-
/**
|
|
853
|
-
*
|
|
854
|
-
*
|
|
855
|
-
*
|
|
856
|
-
* (the `hotUpdate` delete event) — which both mutate the cache, re-run
|
|
857
|
-
* analysis, and then need the client to pick up the regenerated CSS.
|
|
858
|
-
* No-ops outside dev.
|
|
859
|
-
*
|
|
860
|
-
* `devServer.moduleGraph` is Vite's back-compat mixed graph: its
|
|
861
|
-
* `getModuleById` searches the client AND ssr environment graphs and its
|
|
862
|
-
* `invalidateModule` invalidates both instances behind the returned node,
|
|
863
|
-
* which is exactly the reach this path wants. It stays the seam here.
|
|
864
|
-
*
|
|
865
|
-
* Both modules are invalidated unconditionally, and deliberately so: the
|
|
866
|
-
* appearance or disappearance of a whole component file is not the
|
|
867
|
-
* steady-state edit the change-gated path governs, and a client reload does
|
|
868
|
-
* NOT rescue a module that was never invalidated — Vite keeps serving its
|
|
869
|
-
* cached transform result across reloads (openspec: hmr-new-file-detection,
|
|
870
|
-
* "CSS invalidation after new file analysis").
|
|
871
|
-
*/
|
|
689
|
+
/** Invalidate the component CSS and system-props modules, then reload. The
|
|
690
|
+
* reload alone rescues nothing: Vite keeps serving its cached transform.
|
|
691
|
+
* `devServer.moduleGraph` is the mixed back-compat graph on purpose — it
|
|
692
|
+
* reaches client and ssr at once; per-environment graphs do not. */
|
|
872
693
|
invalidateExtractedModules() {
|
|
873
694
|
const server = this.devServer;
|
|
874
695
|
if (!server) return;
|
|
@@ -882,16 +703,8 @@ var PluginContext = class {
|
|
|
882
703
|
this.devServer?.hot?.send({ type: "full-reload" });
|
|
883
704
|
}, 100);
|
|
884
705
|
}
|
|
885
|
-
/**
|
|
886
|
-
*
|
|
887
|
-
* (external-package-file-discovery: silence is never an outcome). An
|
|
888
|
-
* unresolvable `.includes()` specifier warns in non-strict mode and FAILS
|
|
889
|
-
* the build under `strict: true`, naming every offending specifier —
|
|
890
|
-
* a typo'd include must not ship a build missing its component CSS. A
|
|
891
|
-
* stale dist entry under an extended package (first-class-extension D13)
|
|
892
|
-
* rides the same seam: a merge against it would silently skew registry
|
|
893
|
-
* content the discovered sources no longer match.
|
|
894
|
-
*/
|
|
706
|
+
/** An unresolvable `.includes()` specifier or a stale dist entry under an
|
|
707
|
+
* extended package warns, and fails the build under `strict`. */
|
|
895
708
|
enforceIncludeResolution() {
|
|
896
709
|
for (const message of [(0, _animus_ui_extract_pipeline.unresolvableIncludesMessage)(this.externalPackageOutcomes), (0, _animus_ui_extract_pipeline.staleDistIncludesMessage)(this.externalPackageOutcomes)]) {
|
|
897
710
|
if (message === null) continue;
|
|
@@ -899,14 +712,6 @@ var PluginContext = class {
|
|
|
899
712
|
this.warn(message);
|
|
900
713
|
}
|
|
901
714
|
}
|
|
902
|
-
/**
|
|
903
|
-
* The post-analysis gate over cross-source token contracts
|
|
904
|
-
* (extraction-diagnostics): a discovered component referencing a token its
|
|
905
|
-
* OWN package defines but the consumer theme does not gets the teaching
|
|
906
|
-
* error naming the token, component, source package, and the missing
|
|
907
|
-
* `createTheme().extend(...)`. Wiring and severity routing live in the
|
|
908
|
-
* shared pipeline gate (next-plugin parity by construction).
|
|
909
|
-
*/
|
|
910
715
|
enforceExternalTokenContracts() {
|
|
911
716
|
(0, _animus_ui_extract_pipeline.enforceExternalTokenContracts)({
|
|
912
717
|
diagnostics: this.storedManifest?.diagnostics,
|
|
@@ -938,15 +743,16 @@ var PluginContext = class {
|
|
|
938
743
|
};
|
|
939
744
|
//#endregion
|
|
940
745
|
//#region src/rediscovery.ts
|
|
941
|
-
/** The current manifest's unresolved-parent drops, parsed from diagnostics
|
|
942
|
-
* (shared Rust-mirror matcher — the regex lives in manifest-diagnostics). */
|
|
746
|
+
/** The current manifest's unresolved-parent drops, parsed from diagnostics. */
|
|
943
747
|
function unresolvedParentDrops(ctx) {
|
|
944
748
|
const drops = [];
|
|
945
|
-
|
|
749
|
+
const manifest = ctx.storedManifest;
|
|
750
|
+
if (!manifest) return drops;
|
|
751
|
+
for (const d of manifest.diagnostics) {
|
|
946
752
|
const parent = (0, _animus_ui_extract_pipeline.unresolvedParentName)(d);
|
|
947
753
|
if (parent !== null) drops.push({
|
|
948
|
-
file:
|
|
949
|
-
component:
|
|
754
|
+
file: d.file,
|
|
755
|
+
component: d.component,
|
|
950
756
|
parent
|
|
951
757
|
});
|
|
952
758
|
}
|
|
@@ -954,39 +760,24 @@ function unresolvedParentDrops(ctx) {
|
|
|
954
760
|
}
|
|
955
761
|
const EMPTY_DROP_FILES = /* @__PURE__ */ new Set();
|
|
956
762
|
const dropFilesByManifest = /* @__PURE__ */ new WeakMap();
|
|
957
|
-
/** Files carrying an unresolved-parent drop in the
|
|
958
|
-
* derived once per manifest publication, then a set lookup. */
|
|
763
|
+
/** Files carrying an unresolved-parent drop in the current manifest. */
|
|
959
764
|
function unresolvedDropFiles(ctx) {
|
|
960
765
|
const manifest = ctx.storedManifest;
|
|
961
766
|
if (!manifest) return EMPTY_DROP_FILES;
|
|
962
767
|
let files = dropFilesByManifest.get(manifest);
|
|
963
768
|
if (!files) {
|
|
964
769
|
const derived = /* @__PURE__ */ new Set();
|
|
965
|
-
for (const d of manifest.diagnostics
|
|
770
|
+
for (const d of manifest.diagnostics) if ((0, _animus_ui_extract_pipeline.isUnresolvedParentDrop)(d)) derived.add(d.file);
|
|
966
771
|
files = derived;
|
|
967
772
|
dropFilesByManifest.set(manifest, files);
|
|
968
773
|
}
|
|
969
774
|
return files;
|
|
970
775
|
}
|
|
971
776
|
/**
|
|
972
|
-
*
|
|
973
|
-
*
|
|
974
|
-
* "Source-universe reconciliation precedes unresolved-parent fallbacks").
|
|
975
|
-
*
|
|
976
|
-
* Drop-triggered: runs only when the current manifest reports `chain
|
|
977
|
-
* dropped: could not resolve parent component`. One discovery walk (the
|
|
978
|
-
* same walk and policy the server start uses) folds every eligible on-disk
|
|
979
|
-
* file the cache does not know, then one re-analysis resolves the whole
|
|
980
|
-
* extension graph — dependency depth never requires iteration depth, so the
|
|
981
|
-
* loop re-enters only when a re-analysis leaves NEW drops and the previous
|
|
982
|
-
* walk actually folded something (a changed discovery domain). The
|
|
983
|
-
* iteration cap is containment, not semantics.
|
|
984
|
-
*
|
|
985
|
-
* Returns whether any re-analysis ran (callers diff plans across the WHOLE
|
|
986
|
-
* transaction, so a fold-and-reanalyze is invisible to them beyond the
|
|
987
|
-
* final manifest).
|
|
777
|
+
* Folds undiscovered on-disk files when the manifest reports an unresolved
|
|
778
|
+
* parent, then re-analyzes; the iteration cap is containment, not semantics.
|
|
988
779
|
*/
|
|
989
|
-
async function
|
|
780
|
+
async function reconcileSourceCorpus(ctx) {
|
|
990
781
|
if (ctx.isProd) return false;
|
|
991
782
|
let reanalyzed = false;
|
|
992
783
|
for (let iteration = 0; iteration < 3; iteration++) {
|
|
@@ -997,12 +788,12 @@ async function stabilizeSourceUniverse(ctx) {
|
|
|
997
788
|
const drops = unresolvedParentDrops(ctx);
|
|
998
789
|
const dropKey = drops.map((d) => `${d.file}\0${d.component}\0${d.parent}`).sort().join("\n");
|
|
999
790
|
const memo = barrenWalkMemos.get(ctx);
|
|
1000
|
-
if (memo && memo.dropKey === dropKey && memo.
|
|
791
|
+
if (memo && memo.dropKey === dropKey && memo.cacheGeneration === ctx.fileCacheGeneration) return reanalyzed;
|
|
1001
792
|
const folded = foldUndiscoveredFiles(ctx);
|
|
1002
793
|
if (folded.length === 0) {
|
|
1003
794
|
barrenWalkMemos.set(ctx, {
|
|
1004
795
|
dropKey,
|
|
1005
|
-
|
|
796
|
+
cacheGeneration: ctx.fileCacheGeneration
|
|
1006
797
|
});
|
|
1007
798
|
warnInadmissibleParents(ctx, drops);
|
|
1008
799
|
return reanalyzed;
|
|
@@ -1014,26 +805,22 @@ async function stabilizeSourceUniverse(ctx) {
|
|
|
1014
805
|
try {
|
|
1015
806
|
published = (await ctx.analyzeIngested()).ok;
|
|
1016
807
|
} finally {
|
|
1017
|
-
if (!published)
|
|
808
|
+
if (!published) ctx.mutateFileCache((cache) => {
|
|
809
|
+
for (const key of folded) cache.delete(key);
|
|
810
|
+
});
|
|
1018
811
|
}
|
|
1019
812
|
if (!published) return reanalyzed;
|
|
1020
813
|
}
|
|
1021
|
-
if (unresolvedDropFiles(ctx).size > 0) ctx.warn("source-
|
|
814
|
+
if (unresolvedDropFiles(ctx).size > 0) ctx.warn("source-corpus rediscovery did not stabilize after bounded retries");
|
|
1022
815
|
return reanalyzed;
|
|
1023
816
|
}
|
|
1024
|
-
/**
|
|
1025
|
-
*
|
|
1026
|
-
* folds every eligible file `fileCache` does not hold. Returns the fold
|
|
1027
|
-
* count.
|
|
1028
|
-
*/
|
|
1029
|
-
/** Returns the cache keys this fold ADDED, so a failed analysis can roll
|
|
1030
|
-
* them back — `runAnalysis` requires callers that advanced the file cache to
|
|
1031
|
-
* restore it, or the content-hash gate suppresses the equal-content retry
|
|
1032
|
-
* forever. */
|
|
817
|
+
/** Returns the cache keys this fold added: a failed analysis must restore
|
|
818
|
+
* the cache, or the content-hash gate suppresses the retry forever. */
|
|
1033
819
|
function foldUndiscoveredFiles(ctx) {
|
|
1034
820
|
const excludeMatcher = ctx.excludeMatcher;
|
|
1035
821
|
const filePaths = (0, _animus_ui_extract_pipeline.discoverFiles)(ctx.rootDir, ctx.rootDir, excludeMatcher, ctx.extensionsSet);
|
|
1036
822
|
const folded = [];
|
|
823
|
+
const pending = [];
|
|
1037
824
|
for (const filePath of filePaths) {
|
|
1038
825
|
if ((0, path.extname)(filePath) === ".mdx") continue;
|
|
1039
826
|
const relPath = (0, path.relative)(ctx.rootDir, filePath);
|
|
@@ -1044,35 +831,32 @@ function foldUndiscoveredFiles(ctx) {
|
|
|
1044
831
|
} catch {
|
|
1045
832
|
continue;
|
|
1046
833
|
}
|
|
1047
|
-
|
|
834
|
+
pending.push([relPath, {
|
|
1048
835
|
hash: (0, _animus_ui_extract_pipeline.contentHash)(source),
|
|
1049
836
|
source
|
|
1050
|
-
});
|
|
837
|
+
}]);
|
|
1051
838
|
folded.push(relPath);
|
|
1052
839
|
}
|
|
840
|
+
if (pending.length > 0) ctx.mutateFileCache((cache) => {
|
|
841
|
+
for (const [relPath, entry] of pending) cache.set(relPath, entry);
|
|
842
|
+
});
|
|
1053
843
|
return folded;
|
|
1054
844
|
}
|
|
1055
|
-
/**
|
|
1056
|
-
* last walk that folded nothing
|
|
845
|
+
/** Per context: the drop tuple-set and the cache mutation generation at the
|
|
846
|
+
* last walk that folded nothing. */
|
|
1057
847
|
const barrenWalkMemos = /* @__PURE__ */ new WeakMap();
|
|
1058
848
|
/** Per-context (file, parent, condition) verdicts already warned — each
|
|
1059
849
|
* condition warns once; cleared when the drops disappear. */
|
|
1060
850
|
const warnedVerdicts = /* @__PURE__ */ new WeakMap();
|
|
1061
|
-
|
|
1062
|
-
* no-behavior-change choice: the teaching probe resolves what it always did). */
|
|
1063
|
-
const PARENT_PROBE_EXTENSIONS = new Set([
|
|
851
|
+
const PARENT_PROBE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1064
852
|
".tsx",
|
|
1065
853
|
".ts",
|
|
1066
854
|
".jsx",
|
|
1067
855
|
".js"
|
|
1068
856
|
]);
|
|
1069
857
|
/**
|
|
1070
|
-
*
|
|
1071
|
-
*
|
|
1072
|
-
* Resolution is a best-effort scan of the consumer's own import statements
|
|
1073
|
-
* for the parent binding — enough for the relative-specifier case a
|
|
1074
|
-
* developer actually hits; a parent this scan cannot resolve keeps the
|
|
1075
|
-
* engine's own diagnostic.
|
|
858
|
+
* Warns for parents that resolve on disk but cannot be admitted. Resolution
|
|
859
|
+
* is a best-effort scan of the consumer's own relative import specifiers.
|
|
1076
860
|
*/
|
|
1077
861
|
function warnInadmissibleParents(ctx, drops) {
|
|
1078
862
|
const excludeMatcher = ctx.excludeMatcher;
|
|
@@ -1092,7 +876,7 @@ function warnInadmissibleParents(ctx, drops) {
|
|
|
1092
876
|
if (!resolved) continue;
|
|
1093
877
|
const relPath = (0, path.relative)(ctx.rootDir, resolved);
|
|
1094
878
|
const excludedBy = excludeMatcher.explain(resolved, relPath);
|
|
1095
|
-
const reason = excludedBy ? `which is excluded by pattern '${excludedBy}'. Include that file in the extraction
|
|
879
|
+
const reason = excludedBy ? `which is excluded by pattern '${excludedBy}'. Include that file in the extraction sources or remove this extension.` : !(0, _animus_ui_extract_pipeline.isPathWithinRoot)(ctx.rootDir, resolved) ? "outside the extraction root. Declare its package in the system includes or move it under the project root." : !ctx.extensionsSet.has((0, path.extname)(resolved)) ? `whose extension is not in the configured source extensions.` : null;
|
|
1096
880
|
if (reason === null) continue;
|
|
1097
881
|
const verdictKey = `${drop.file}\0${drop.parent}\0${reason}`;
|
|
1098
882
|
if (warned.has(verdictKey)) continue;
|
|
@@ -1100,7 +884,6 @@ function warnInadmissibleParents(ctx, drops) {
|
|
|
1100
884
|
ctx.warn(`cannot extract '${drop.component}': parent '${drop.parent}' resolves to ${relPath}, ${reason}`);
|
|
1101
885
|
}
|
|
1102
886
|
}
|
|
1103
|
-
/** The module specifier a named import binds `binding` from, if scannable. */
|
|
1104
887
|
function importSpecifierFor(source, binding) {
|
|
1105
888
|
const importRe = /import\s+([^;]+?)\s+from\s+['"]([^'"]+)['"]/g;
|
|
1106
889
|
let match;
|
|
@@ -1113,25 +896,17 @@ function importSpecifierFor(source, binding) {
|
|
|
1113
896
|
//#endregion
|
|
1114
897
|
//#region src/transform.ts
|
|
1115
898
|
/**
|
|
1116
|
-
* The
|
|
1117
|
-
*
|
|
1118
|
-
* prologue the engine hoists to byte 0 — prepending above it would demote
|
|
1119
|
-
* 'use client'/'use strict' to an ordinary expression statement, silently
|
|
1120
|
-
* un-marking client modules on exactly the RSC-capable hosts this delivery
|
|
1121
|
-
* path serves.
|
|
899
|
+
* The import goes after the directive prologue: above it, 'use client'
|
|
900
|
+
* becomes an ordinary expression and the module loses its client marking.
|
|
1122
901
|
*/
|
|
1123
902
|
function applyDevBridgeImport(code) {
|
|
1124
903
|
const prologue = /^(?:(['"])use [a-z -]+\1;?\r?\n)*/.exec(code)?.[0] ?? "";
|
|
1125
904
|
return prologue + `import '${VIRTUAL_BRIDGE_ID}';\n` + code.slice(prologue.length);
|
|
1126
905
|
}
|
|
1127
|
-
/**
|
|
1128
|
-
* Live raw-fallback files reachable from `relPath`'s components through
|
|
1129
|
-
* extension provenance — the transitive closure, so a grandparent's serve is
|
|
1130
|
-
* withheld exactly like a direct parent's.
|
|
1131
|
-
*/
|
|
1132
906
|
function rawFallbackDescendants(ctx, relPath) {
|
|
907
|
+
const manifest = ctx.storedManifest;
|
|
1133
908
|
const conflicted = /* @__PURE__ */ new Set();
|
|
1134
|
-
const queue = [...
|
|
909
|
+
const queue = [...manifest?.files[relPath] ?? []];
|
|
1135
910
|
const seen = new Set(queue);
|
|
1136
911
|
while (queue.length > 0) {
|
|
1137
912
|
const id = queue.shift();
|
|
@@ -1139,33 +914,23 @@ function rawFallbackDescendants(ctx, relPath) {
|
|
|
1139
914
|
if (seen.has(childId)) continue;
|
|
1140
915
|
seen.add(childId);
|
|
1141
916
|
queue.push(childId);
|
|
1142
|
-
const childFile =
|
|
917
|
+
const childFile = manifest?.components[childId]?.file;
|
|
1143
918
|
if (childFile && childFile !== relPath && ctx.rawExtensionFallbacks.has(childFile)) conflicted.add(childFile);
|
|
1144
919
|
}
|
|
1145
920
|
}
|
|
1146
921
|
return [...conflicted].sort();
|
|
1147
922
|
}
|
|
1148
|
-
/**
|
|
1149
|
-
* transform: replace builder chains with `createComponent()` calls using
|
|
1150
|
-
* the pre-built manifest; detect files created after buildStart and fold
|
|
1151
|
-
* them into the analysis.
|
|
1152
|
-
*
|
|
1153
|
-
* The HMR bridge is NOT injected here — `transformIndexHtml` delivers it as a
|
|
1154
|
-
* `<script type="module">` per served document (openspec:
|
|
1155
|
-
* dev-stylesheet-management, "HMR bridge auto-injected in dev mode"; "Transform
|
|
1156
|
-
* emitter unchanged" forbids the emitter importing it).
|
|
1157
|
-
*/
|
|
1158
923
|
async function transformSource(ctx, code, id) {
|
|
1159
924
|
if (!ctx.storedManifest) return null;
|
|
1160
925
|
if (id.startsWith("\0") || id.startsWith("virtual:animus/")) return null;
|
|
1161
926
|
const isExternalPkg = ctx.externalPackageDirs.some((dir) => (0, _animus_ui_extract_pipeline.isPathWithinRoot)(dir, id));
|
|
1162
927
|
const relativePath = (0, path.relative)(ctx.rootDir, id);
|
|
1163
928
|
if (!isExternalPkg) {
|
|
1164
|
-
if (
|
|
929
|
+
if (!(0, _animus_ui_extract_pipeline.isEngineTransformExtension)(id)) return null;
|
|
1165
930
|
if (id.includes("node_modules")) return null;
|
|
1166
931
|
if (!(0, _animus_ui_extract_pipeline.isPathWithinRoot)(ctx.rootDir, id)) return null;
|
|
1167
932
|
}
|
|
1168
|
-
if (!ctx.storedManifest.files
|
|
933
|
+
if (!ctx.storedManifest.files[relativePath]?.length) {
|
|
1169
934
|
if (!ctx.isProd && !ctx.fileCache.has(relativePath)) await runExclusiveAnalysis(ctx, async () => {
|
|
1170
935
|
if (ctx.fileCache.has(relativePath)) return;
|
|
1171
936
|
if (isExternalPkg) {
|
|
@@ -1173,28 +938,27 @@ async function transformSource(ctx, code, id) {
|
|
|
1173
938
|
if (owner) ctx.externalFileOwners[relativePath] = owner[1];
|
|
1174
939
|
}
|
|
1175
940
|
const hash = (0, _animus_ui_extract_pipeline.contentHash)(code);
|
|
1176
|
-
ctx.
|
|
941
|
+
ctx.mutateFileCache((cache) => cache.set(relativePath, {
|
|
1177
942
|
hash,
|
|
1178
943
|
source: code
|
|
1179
|
-
});
|
|
944
|
+
}));
|
|
1180
945
|
const prevPlans = (0, _animus_ui_extract_pipeline.snapshotFilePlans)(ctx.storedManifest);
|
|
1181
946
|
let analysisOk = false;
|
|
1182
947
|
try {
|
|
1183
948
|
analysisOk = (await ctx.analyzeIngested()).ok;
|
|
1184
949
|
} finally {
|
|
1185
|
-
if (!analysisOk) ctx.
|
|
950
|
+
if (!analysisOk) ctx.mutateFileCache((cache) => cache.delete(relativePath));
|
|
1186
951
|
}
|
|
1187
952
|
if (analysisOk) {
|
|
1188
|
-
await
|
|
953
|
+
await reconcileSourceCorpus(ctx);
|
|
1189
954
|
invalidateFileModules(ctx, (0, _animus_ui_extract_pipeline.diffFilePlans)(prevPlans, (0, _animus_ui_extract_pipeline.snapshotFilePlans)(ctx.storedManifest), { exclude: relativePath }));
|
|
1190
|
-
const compCount = ctx.storedManifest
|
|
955
|
+
const compCount = ctx.storedManifest?.files[relativePath]?.length ?? 0;
|
|
1191
956
|
ctx.info(`New file detected: ${relativePath} — ${compCount ? `${compCount} components extracted` : "no components"}`);
|
|
1192
957
|
ctx.invalidateExtractedModules();
|
|
1193
958
|
}
|
|
1194
959
|
});
|
|
1195
|
-
if (!ctx.storedManifest.files
|
|
1196
|
-
|
|
1197
|
-
else ctx.rawExtensionFallbacks.delete(relativePath);
|
|
960
|
+
if (!ctx.storedManifest.files[relativePath]?.length) {
|
|
961
|
+
ctx.recordFallbackState(relativePath, unresolvedDropFiles(ctx).has(relativePath));
|
|
1198
962
|
return null;
|
|
1199
963
|
}
|
|
1200
964
|
}
|
|
@@ -1210,17 +974,20 @@ async function transformSource(ctx, code, id) {
|
|
|
1210
974
|
try {
|
|
1211
975
|
const { transformFile } = ctx.engineApi();
|
|
1212
976
|
const result = transformFile(code, relativePath, ctx.storedManifestJson);
|
|
1213
|
-
if (!result.hasComponents)
|
|
977
|
+
if (!result.hasComponents) {
|
|
978
|
+
ctx.recordFallbackState(relativePath, true);
|
|
979
|
+
return null;
|
|
980
|
+
}
|
|
1214
981
|
if (ctx.verbose) {
|
|
1215
|
-
const compCount = ctx.storedManifest.files
|
|
982
|
+
const compCount = ctx.storedManifest.files[relativePath]?.length ?? 0;
|
|
1216
983
|
ctx.log(`transform ${relativePath}: ${compCount} components`);
|
|
1217
984
|
}
|
|
1218
985
|
let outputCode = result.code;
|
|
1219
986
|
if (!ctx.isProd) {
|
|
1220
987
|
outputCode = applyDevBridgeImport(result.code);
|
|
1221
988
|
ctx.recordTransformOutput(relativePath, outputCode);
|
|
1222
|
-
ctx.rawExtensionFallbacks.delete(relativePath);
|
|
1223
989
|
}
|
|
990
|
+
ctx.recordFallbackState(relativePath, false);
|
|
1224
991
|
return {
|
|
1225
992
|
code: outputCode,
|
|
1226
993
|
map: null
|
|
@@ -1228,30 +995,14 @@ async function transformSource(ctx, code, id) {
|
|
|
1228
995
|
} catch (e) {
|
|
1229
996
|
if (ctx.options.strict) throw new Error(`[animus-extract] Failed to transform ${id}: ${e}`, { cause: e });
|
|
1230
997
|
console.warn(`[animus-extract] Failed to transform ${id}:`, e);
|
|
998
|
+
ctx.recordFallbackState(relativePath, true);
|
|
1231
999
|
return null;
|
|
1232
1000
|
}
|
|
1233
1001
|
}
|
|
1234
1002
|
//#endregion
|
|
1235
1003
|
//#region src/hmr.ts
|
|
1236
|
-
/**
|
|
1237
|
-
*
|
|
1238
|
-
* membership (geological reset), content-hash diffing with incremental
|
|
1239
|
-
* re-analysis, deleted-file cache pruning, and targeted module invalidation
|
|
1240
|
-
* (component CSS, system props, and definition files whose replacement
|
|
1241
|
-
* changed).
|
|
1242
|
-
*
|
|
1243
|
-
* Vite 8 dispatches this hook once per environment for ONE file event — the
|
|
1244
|
-
* client environment first, then every non-client environment (see
|
|
1245
|
-
* `handleHMRUpdate` in vite/dist/node/chunks/node.js). The analysis half is
|
|
1246
|
-
* therefore claimed by exactly one dispatch (`ctx.hotUpdateEvents`), while the
|
|
1247
|
-
* invalidation half runs in every environment against its own module graph —
|
|
1248
|
-
* what the mixed-graph `handleHotUpdate` used to achieve implicitly by
|
|
1249
|
-
* invalidating the client and SSR instance behind one module node.
|
|
1250
|
-
*
|
|
1251
|
-
* The hook fires for every watched file whether or not it has modules in any
|
|
1252
|
-
* graph, so system dependencies registered through `watcher.add` outside the
|
|
1253
|
-
* root still reach the reset branch, with an empty `modules` list.
|
|
1254
|
-
*/
|
|
1004
|
+
/** Vite dispatches this hook once per environment for one file event: the
|
|
1005
|
+
* analysis half is claimed by one dispatch, invalidation runs in each. */
|
|
1255
1006
|
async function handleHotUpdate(ctx, environment, options) {
|
|
1256
1007
|
if (ctx.isProd) return;
|
|
1257
1008
|
return runExclusiveAnalysis(ctx, () => handleHotUpdateExclusive(ctx, environment, options));
|
|
@@ -1263,7 +1014,7 @@ async function handleHotUpdateExclusive(ctx, environment, { type, file, timestam
|
|
|
1263
1014
|
if (ctx.isSystemDependency(absFile)) {
|
|
1264
1015
|
if (ownsEvent) {
|
|
1265
1016
|
await reconcileSourceEntry(ctx, absFile, type, read);
|
|
1266
|
-
ctx.
|
|
1017
|
+
ctx.requestSystemReload((0, path.relative)(ctx.rootDir, absFile));
|
|
1267
1018
|
}
|
|
1268
1019
|
return [];
|
|
1269
1020
|
}
|
|
@@ -1274,47 +1025,30 @@ async function handleHotUpdateExclusive(ctx, environment, { type, file, timestam
|
|
|
1274
1025
|
if (ownsEvent) ctx.hotUpdateEvents.record(file, timestamp, await analyzeChangedFile(ctx, file, absFile, read));
|
|
1275
1026
|
const result = ctx.hotUpdateEvents.resultOf(file, timestamp);
|
|
1276
1027
|
if (result.kind === "ignored") return;
|
|
1028
|
+
if (result.kind === "evicted") {
|
|
1029
|
+
ctx.log(`HMR (${environment.name}): decision for ${(0, path.relative)(ctx.rootDir, absFile)} evicted — invalidating conservatively`);
|
|
1030
|
+
return invalidateStaleModules(ctx, environment, modules, {
|
|
1031
|
+
staleDefinitionFiles: [],
|
|
1032
|
+
systemPropsChanged: true,
|
|
1033
|
+
presentationOnly: false
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1277
1036
|
if (result.kind === "unchanged") {
|
|
1278
1037
|
if (type === "create") return;
|
|
1279
1038
|
return [];
|
|
1280
1039
|
}
|
|
1281
1040
|
return invalidateStaleModules(ctx, environment, modules, result);
|
|
1282
1041
|
}
|
|
1283
|
-
/**
|
|
1284
|
-
*
|
|
1285
|
-
*
|
|
1286
|
-
* Editors save atomically — truncate, then rewrite — so a watcher event can
|
|
1287
|
-
* arrive while the path is momentarily EMPTY. Vite's `read()` helper retries on
|
|
1288
|
-
* empty content for exactly that reason; reading the path directly at the same
|
|
1289
|
-
* moment yields `''`, and since the corrective content produces no second
|
|
1290
|
-
* event, that empty source would be cached permanently.
|
|
1291
|
-
*
|
|
1292
|
-
* Vite always supplies `read`, so the direct read is NOT a fallback for Vite:
|
|
1293
|
-
* it exists for hosts that drive this hook without one. The dev-lane's adapter
|
|
1294
|
-
* contract is deliberately bundler-neutral, and a second runtime satisfying it
|
|
1295
|
-
* must not be forced to fabricate a helper to get correct behavior.
|
|
1296
|
-
*/
|
|
1042
|
+
/** Prefers Vite's `read`, which retries on the empty content an atomic save
|
|
1043
|
+
* exposes; the direct read serves hosts that supply no `read`. */
|
|
1297
1044
|
async function readChangedSource(absFile, read) {
|
|
1298
1045
|
return read ? await read() : (0, fs.readFileSync)(absFile, "utf-8");
|
|
1299
1046
|
}
|
|
1300
|
-
/**
|
|
1301
|
-
*
|
|
1302
|
-
* and a discovered component source, since the dependency branch is terminal —
|
|
1303
|
-
* neither the edit path's cache write nor `pruneDeletedFile` runs for it.
|
|
1304
|
-
*
|
|
1305
|
-
* An edit refreshes the entry: `performGeologicalReset` rebuilds its
|
|
1306
|
-
* full-source analysis from this cache, so a stale entry would be re-analyzed
|
|
1307
|
-
* on every later reset. A delete prunes it, exactly as the ordinary delete path
|
|
1308
|
-
* would (openspec: hmr-new-file-detection, "Watcher deletion pruning") — a
|
|
1309
|
-
* surviving entry is a ghost source no watcher event can ever name again.
|
|
1310
|
-
*
|
|
1311
|
-
* Dependency-only files have no entry: `pruneFileCache` no-ops for them, and
|
|
1312
|
-
* the edit path must not create one — the cache is the component-source set,
|
|
1313
|
-
* and a phantom entry would feed the engine a file it never discovered.
|
|
1314
|
-
*/
|
|
1047
|
+
/** Keeps the cache entry of a file that is both a system dependency and a
|
|
1048
|
+
* discovered source; a file with no entry must never gain one here. */
|
|
1315
1049
|
async function reconcileSourceEntry(ctx, absFile, type, read) {
|
|
1316
1050
|
if (type === "delete") {
|
|
1317
|
-
pruneFileCache(
|
|
1051
|
+
ctx.mutateFileCache((cache) => pruneFileCache(cache, ctx.rootDir, absFile));
|
|
1318
1052
|
return;
|
|
1319
1053
|
}
|
|
1320
1054
|
const relPath = (0, path.relative)(ctx.rootDir, absFile);
|
|
@@ -1323,19 +1057,16 @@ async function reconcileSourceEntry(ctx, absFile, type, read) {
|
|
|
1323
1057
|
try {
|
|
1324
1058
|
source = await readChangedSource(absFile, read);
|
|
1325
1059
|
} catch {
|
|
1326
|
-
ctx.warn(`could not re-read ${relPath} after a system-dependency edit —
|
|
1060
|
+
ctx.warn(`could not re-read ${relPath} after a system-dependency edit — system reloads will analyze its pre-edit text until a later edit is read successfully`);
|
|
1327
1061
|
return;
|
|
1328
1062
|
}
|
|
1329
|
-
ctx.
|
|
1063
|
+
ctx.mutateFileCache((cache) => cache.set(relPath, {
|
|
1330
1064
|
hash: (0, _animus_ui_extract_pipeline.contentHash)(source),
|
|
1331
1065
|
source
|
|
1332
|
-
});
|
|
1066
|
+
}));
|
|
1333
1067
|
}
|
|
1334
|
-
/**
|
|
1335
|
-
*
|
|
1336
|
-
* refresh the cache, and re-run project analysis. The returned result is what
|
|
1337
|
-
* the remaining environments act on.
|
|
1338
|
-
*/
|
|
1068
|
+
/** The once-per-event analysis half; its result is what the remaining
|
|
1069
|
+
* environments act on. */
|
|
1339
1070
|
async function analyzeChangedFile(ctx, file, absFile, read) {
|
|
1340
1071
|
const ext = (0, path.extname)(file);
|
|
1341
1072
|
if (!ctx.extensionsSet.has(ext)) return { kind: "ignored" };
|
|
@@ -1360,19 +1091,21 @@ async function analyzeChangedFile(ctx, file, absFile, read) {
|
|
|
1360
1091
|
const owner = Object.entries(ctx.externalDirOwners).find(([dir]) => (0, _animus_ui_extract_pipeline.isPathWithinRoot)(dir, absFile));
|
|
1361
1092
|
if (owner) ctx.externalFileOwners[relPath] = owner[1];
|
|
1362
1093
|
}
|
|
1363
|
-
ctx.
|
|
1094
|
+
ctx.mutateFileCache((cache) => cache.set(relPath, {
|
|
1364
1095
|
hash,
|
|
1365
1096
|
source
|
|
1366
|
-
});
|
|
1097
|
+
}));
|
|
1367
1098
|
const restoreEntry = () => {
|
|
1368
|
-
|
|
1369
|
-
|
|
1099
|
+
ctx.mutateFileCache((cache) => {
|
|
1100
|
+
if (cached) cache.set(relPath, cached);
|
|
1101
|
+
else cache.delete(relPath);
|
|
1102
|
+
});
|
|
1370
1103
|
if (priorExternalOwner) ctx.externalFileOwners[relPath] = priorExternalOwner;
|
|
1371
1104
|
else delete ctx.externalFileOwners[relPath];
|
|
1372
1105
|
};
|
|
1373
1106
|
const hmrStart = performance.now();
|
|
1374
1107
|
const prevPlans = (0, _animus_ui_extract_pipeline.snapshotFilePlans)(ctx.storedManifest);
|
|
1375
|
-
const directComponentIds = (ctx.
|
|
1108
|
+
const directComponentIds = (ctx.corpus.published.ownership[relPath]?.analysisPaths ?? [relPath]).flatMap((analysisPath) => ctx.storedManifest?.files[analysisPath] ?? []);
|
|
1376
1109
|
const invalidatedIds = new Set(directComponentIds);
|
|
1377
1110
|
const queue = [...directComponentIds];
|
|
1378
1111
|
while (queue.length > 0) {
|
|
@@ -1400,7 +1133,7 @@ async function analyzeChangedFile(ctx, file, absFile, read) {
|
|
|
1400
1133
|
return { kind: "ignored" };
|
|
1401
1134
|
}
|
|
1402
1135
|
try {
|
|
1403
|
-
await
|
|
1136
|
+
await reconcileSourceCorpus(ctx);
|
|
1404
1137
|
} catch (e) {
|
|
1405
1138
|
restoreEntry();
|
|
1406
1139
|
throw e;
|
|
@@ -1408,7 +1141,7 @@ async function analyzeChangedFile(ctx, file, absFile, read) {
|
|
|
1408
1141
|
const systemPropsChanged = systemPropsModuleSource(ctx) !== systemPropsBefore;
|
|
1409
1142
|
const analysisMs = Math.round(performance.now() - analysisStart);
|
|
1410
1143
|
const staleDefinitionFiles = (0, _animus_ui_extract_pipeline.diffFilePlans)(prevPlans, (0, _animus_ui_extract_pipeline.snapshotFilePlans)(ctx.storedManifest)).filter((defFile) => (0, path.resolve)(ctx.rootDir, defFile) !== absFile);
|
|
1411
|
-
const nativeEntry = ctx.
|
|
1144
|
+
const nativeEntry = ctx.corpus.published.analysisEntries.get(relPath);
|
|
1412
1145
|
const presentationOnly = nativeEntry ? isPresentationOnlyEdit(ctx, relPath, nativeEntry.source) : false;
|
|
1413
1146
|
const hmrMs = Math.round(performance.now() - hmrStart);
|
|
1414
1147
|
ctx.log(`HMR update: ${relPath} — analysis ${analysisMs}ms, total ${hmrMs}ms${presentationOnly ? " (presentation-only)" : ""}`);
|
|
@@ -1420,23 +1153,12 @@ async function analyzeChangedFile(ctx, file, absFile, read) {
|
|
|
1420
1153
|
presentationOnly
|
|
1421
1154
|
};
|
|
1422
1155
|
}
|
|
1423
|
-
/**
|
|
1424
|
-
*
|
|
1425
|
-
* edit — a presentation-only change (style values are not part of the emitted
|
|
1426
|
-
* replacement, whose class names hash `filename::binding`).
|
|
1427
|
-
*
|
|
1428
|
-
* Compares a POST-analysis re-transform of the file (the engine transforms
|
|
1429
|
-
* from its retained facts, so this runs against the just-updated analysis)
|
|
1430
|
-
* with the hash of what the module LAST SERVED (`recordTransformOutput` in
|
|
1431
|
-
* the transform hook, bridge import included). Comparing the full served
|
|
1432
|
-
* output — never replacement strings — is what keeps a mixed style+code edit
|
|
1433
|
-
* deliverable. Fails open: a transform error, a file the manifest does not
|
|
1434
|
-
* list, or a module that never served (no recorded hash) delivers normally.
|
|
1435
|
-
*/
|
|
1156
|
+
/** True when the file's transform output is byte-identical across the edit.
|
|
1157
|
+
* Compares the full served bytes and fails open, so mixed edits deliver. */
|
|
1436
1158
|
function isPresentationOnlyEdit(ctx, scannerRelPath, source) {
|
|
1437
1159
|
const servedHash = ctx.transformOutputHashes.get(scannerRelPath);
|
|
1438
1160
|
if (!servedHash || !ctx.storedManifestJson) return false;
|
|
1439
|
-
if (!ctx.storedManifest?.files
|
|
1161
|
+
if (!ctx.storedManifest?.files[scannerRelPath]?.length) return false;
|
|
1440
1162
|
try {
|
|
1441
1163
|
const { transformFile } = ctx.engineApi();
|
|
1442
1164
|
const fresh = transformFile(source, scannerRelPath, ctx.storedManifestJson);
|
|
@@ -1446,17 +1168,12 @@ function isPresentationOnlyEdit(ctx, scannerRelPath, source) {
|
|
|
1446
1168
|
return false;
|
|
1447
1169
|
}
|
|
1448
1170
|
}
|
|
1449
|
-
/**
|
|
1450
|
-
*
|
|
1451
|
-
*
|
|
1452
|
-
* Without this the removed file's last-known source stays in `ctx.fileCache`,
|
|
1453
|
-
* and shared source ingestion re-feeds that ghost original to the engine on
|
|
1454
|
-
* every later re-analysis — the deleted component's CSS survives for the life
|
|
1455
|
-
* of the process.
|
|
1456
|
-
*/
|
|
1171
|
+
/** Prune a deleted file from the cache: a surviving entry is re-fed to the
|
|
1172
|
+
* engine forever, so the deleted component's CSS never disappears. */
|
|
1457
1173
|
async function pruneDeletedFile(ctx, absFile) {
|
|
1458
1174
|
const relPath = (0, path.relative)(ctx.rootDir, absFile);
|
|
1459
|
-
if (!ctx.fileCache.get(relPath)
|
|
1175
|
+
if (!ctx.fileCache.get(relPath)) return;
|
|
1176
|
+
if (!ctx.mutateFileCache((cache) => pruneFileCache(cache, ctx.rootDir, absFile))) return;
|
|
1460
1177
|
const prevPlans = (0, _animus_ui_extract_pipeline.snapshotFilePlans)(ctx.storedManifest);
|
|
1461
1178
|
const { ok } = await ctx.analyzeIngested();
|
|
1462
1179
|
if (!ok) return;
|
|
@@ -1464,12 +1181,8 @@ async function pruneDeletedFile(ctx, absFile) {
|
|
|
1464
1181
|
invalidateFileModules(ctx, (0, _animus_ui_extract_pipeline.diffFilePlans)(prevPlans, (0, _animus_ui_extract_pipeline.snapshotFilePlans)(ctx.storedManifest)));
|
|
1465
1182
|
ctx.invalidateExtractedModules();
|
|
1466
1183
|
}
|
|
1467
|
-
/**
|
|
1468
|
-
*
|
|
1469
|
-
* environment serves and widen its update set with them. Static CSS
|
|
1470
|
-
* (virtual:animus/styles.css) is NOT invalidated here — it only changes on a
|
|
1471
|
-
* geological reset (vars/globals are stable during dev).
|
|
1472
|
-
*/
|
|
1184
|
+
/** The per-environment invalidation half. Static CSS is left alone: it only
|
|
1185
|
+
* changes on a system reload, since vars and globals are stable in dev. */
|
|
1473
1186
|
function invalidateStaleModules(ctx, environment, modules, analyzed) {
|
|
1474
1187
|
const graph = environment.moduleGraph;
|
|
1475
1188
|
const modulesToUpdate = analyzed.presentationOnly ? [] : [...modules];
|
|
@@ -1505,29 +1218,8 @@ function invalidateStaleModules(ctx, environment, modules, analyzed) {
|
|
|
1505
1218
|
//#endregion
|
|
1506
1219
|
//#region src/index-html.ts
|
|
1507
1220
|
/**
|
|
1508
|
-
*
|
|
1509
|
-
*
|
|
1510
|
-
* Three independent tags, all delivery-only:
|
|
1511
|
-
* - the appearance bootstrap script, when the host build passed a
|
|
1512
|
-
* pre-generated artifact via `appearanceBootstrap` (absent otherwise);
|
|
1513
|
-
* - the `@layer` declaration style, when `buildStart` computed one;
|
|
1514
|
-
* - the HMR bridge module script, in dev only.
|
|
1515
|
-
*
|
|
1516
|
-
* The two build-time branches are guarded on non-empty CONTENT, not on mere
|
|
1517
|
-
* presence: an artifact whose `code` is empty is a caller defect, and emitting
|
|
1518
|
-
* `<script data-animus-bootstrap><\/script>` for it would break the
|
|
1519
|
-
* unconfigured-parity contract in spirit (a tag that does nothing) while
|
|
1520
|
-
* hiding the defect. Symmetric with `layerDeclaration`. The bridge has no
|
|
1521
|
-
* content of its own — it is a `src` reference — so its only guard is the mode.
|
|
1522
|
-
*
|
|
1523
|
-
* Ordering: every descriptor rides the same `head-prepend` bucket, which Vite
|
|
1524
|
-
* serializes in array order (`serializeTags`) and splices in immediately after
|
|
1525
|
-
* `<head>`. The bootstrap script is therefore FIRST in the array so it runs
|
|
1526
|
-
* before the layer declaration and — since built stylesheet links are appended
|
|
1527
|
-
* at `</head>` — before any stylesheet reference.
|
|
1528
|
-
*
|
|
1529
|
-
* With neither build-time input set, a production build emits no tag,
|
|
1530
|
-
* attribute, or whitespace of its own.
|
|
1221
|
+
* Vite serializes one `head-prepend` bucket in array order, so the bootstrap
|
|
1222
|
+
* script precedes the layer declaration and every stylesheet reference.
|
|
1531
1223
|
*/
|
|
1532
1224
|
function buildIndexHtmlTags(ctx) {
|
|
1533
1225
|
const tags = [];
|
|
@@ -1557,7 +1249,13 @@ function buildIndexHtmlTags(ctx) {
|
|
|
1557
1249
|
}
|
|
1558
1250
|
//#endregion
|
|
1559
1251
|
//#region src/virtual-modules.ts
|
|
1560
|
-
/**
|
|
1252
|
+
/**
|
|
1253
|
+
* Keyed on the whole option record: two instances sharing a key means the
|
|
1254
|
+
* later `replaceSync` erases the earlier one's component CSS.
|
|
1255
|
+
*/
|
|
1256
|
+
function sheetRegistryHash(ctx) {
|
|
1257
|
+
return (0, crypto.createHash)("md5").update((0, _animus_ui_extract_pipeline.stableStringify)(ctx.options)).digest("hex").slice(0, 8);
|
|
1258
|
+
}
|
|
1561
1259
|
function resolveVirtualId(ctx, id) {
|
|
1562
1260
|
if (id === VIRTUAL_CSS_ID) return RESOLVED_CSS_ID;
|
|
1563
1261
|
if (id === VIRTUAL_COMPONENTS_ID) return RESOLVED_COMPONENTS_ID;
|
|
@@ -1567,8 +1265,6 @@ function resolveVirtualId(ctx, id) {
|
|
|
1567
1265
|
if (srcEntry) return srcEntry;
|
|
1568
1266
|
return null;
|
|
1569
1267
|
}
|
|
1570
|
-
/** load: serve the virtual stylesheet, components CSS, HMR bridge, and
|
|
1571
|
-
* system-props modules from manifest-derived state. */
|
|
1572
1268
|
function loadVirtualModule(ctx, id) {
|
|
1573
1269
|
const lcssOpts = {
|
|
1574
1270
|
minify: ctx.options.minify ?? ctx.emissionProd,
|
|
@@ -1597,19 +1293,22 @@ function loadVirtualModule(ctx, id) {
|
|
|
1597
1293
|
});
|
|
1598
1294
|
return [variables, (0, _animus_ui_extract_pipeline.postProcessCss)(body, lcssOpts)].filter(Boolean).join("\n");
|
|
1599
1295
|
}
|
|
1600
|
-
if (id === RESOLVED_COMPONENTS_ID)
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1296
|
+
if (id === RESOLVED_COMPONENTS_ID) {
|
|
1297
|
+
const strippedCss = (0, _animus_ui_extract_pipeline.stripLeadingLayerDeclaration)(ctx.resolvedComponentCss || "");
|
|
1298
|
+
return `export default \`${(0, _animus_ui_extract_pipeline.postProcessCss)(strippedCss, {
|
|
1299
|
+
...lcssOpts,
|
|
1300
|
+
minify: false
|
|
1301
|
+
}).replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$")}\`;`;
|
|
1302
|
+
}
|
|
1303
|
+
if (id === RESOLVED_BRIDGE_ID) {
|
|
1304
|
+
const sheetHash = sheetRegistryHash(ctx);
|
|
1305
|
+
return `
|
|
1605
1306
|
import css from '${VIRTUAL_COMPONENTS_ID}';
|
|
1606
1307
|
|
|
1607
|
-
const GLOBAL_KEY = '__animus_sheet_${
|
|
1308
|
+
const GLOBAL_KEY = '__animus_sheet_${sheetHash}__';
|
|
1608
1309
|
let sheet = globalThis[GLOBAL_KEY] || null;
|
|
1609
1310
|
|
|
1610
|
-
// Server evaluation is a no-op:
|
|
1611
|
-
// bridge import prepended to transformed component modules, and only the
|
|
1612
|
-
// browser pass owns a document.
|
|
1311
|
+
// Server evaluation is a no-op: only the browser pass owns a document.
|
|
1613
1312
|
if (typeof document !== 'undefined') {
|
|
1614
1313
|
if (typeof CSSStyleSheet !== 'undefined' && 'adoptedStyleSheets' in document) {
|
|
1615
1314
|
if (!sheet) {
|
|
@@ -1619,11 +1318,10 @@ if (typeof document !== 'undefined') {
|
|
|
1619
1318
|
}
|
|
1620
1319
|
sheet.replaceSync(css);
|
|
1621
1320
|
} else {
|
|
1622
|
-
|
|
1623
|
-
let el = document.querySelector('style[data-animus-components]');
|
|
1321
|
+
let el = document.querySelector('style[data-animus-components="${sheetHash}"]');
|
|
1624
1322
|
if (!el) {
|
|
1625
1323
|
el = document.createElement('style');
|
|
1626
|
-
el.setAttribute('data-animus-components', '');
|
|
1324
|
+
el.setAttribute('data-animus-components', '${sheetHash}');
|
|
1627
1325
|
document.head.appendChild(el);
|
|
1628
1326
|
}
|
|
1629
1327
|
el.textContent = css;
|
|
@@ -1632,28 +1330,23 @@ if (typeof document !== 'undefined') {
|
|
|
1632
1330
|
|
|
1633
1331
|
if (import.meta.hot) {
|
|
1634
1332
|
import.meta.hot.accept('${VIRTUAL_COMPONENTS_ID}', (newModule) => {
|
|
1635
|
-
// The server module runner has a hot channel too
|
|
1636
|
-
// pass owns a document to update.
|
|
1333
|
+
// The server module runner has a hot channel too, but owns no document.
|
|
1637
1334
|
if (typeof document === 'undefined') return;
|
|
1638
1335
|
if (sheet) {
|
|
1639
1336
|
sheet.replaceSync(newModule.default);
|
|
1640
1337
|
} else {
|
|
1641
|
-
const el = document.querySelector('style[data-animus-components]');
|
|
1338
|
+
const el = document.querySelector('style[data-animus-components="${sheetHash}"]');
|
|
1642
1339
|
if (el) el.textContent = newModule.default;
|
|
1643
1340
|
}
|
|
1644
1341
|
});
|
|
1645
1342
|
}
|
|
1646
1343
|
`;
|
|
1344
|
+
}
|
|
1647
1345
|
if (id === RESOLVED_SYSTEM_PROPS_ID) return systemPropsModuleSource(ctx);
|
|
1648
1346
|
return null;
|
|
1649
1347
|
}
|
|
1650
1348
|
//#endregion
|
|
1651
1349
|
//#region src/index.ts
|
|
1652
|
-
/**
|
|
1653
|
-
* Vite adapter for the extraction pipeline. State and pipeline operations
|
|
1654
|
-
* live in PluginContext; hook bodies live in their own modules — this
|
|
1655
|
-
* factory only validates options and wires Vite hooks to those functions.
|
|
1656
|
-
*/
|
|
1657
1350
|
function animusExtract(options) {
|
|
1658
1351
|
(0, _animus_ui_extract_pipeline.assertNoRetiredEngineSelection)(options.engine);
|
|
1659
1352
|
(0, _animus_ui_extract_pipeline.assertKnownOptionKeys)(options, ["verify", "appearanceBootstrap"], [{
|