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