@animus-ui/next-plugin 0.1.8 → 0.1.11

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/dist/index.cjs CHANGED
@@ -1,1330 +1,10 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_session_paths = require("./session-paths-CZoi19NW.cjs");
3
- const require_singleton = require("./singleton-D4vhawsW.cjs");
4
2
  require("./turbopack-loader.cjs");
5
3
  let _animus_ui_extract_pipeline = require("@animus-ui/extract/pipeline");
4
+ let _animus_ui_extract_session = require("@animus-ui/extract/session");
6
5
  let fs = require("fs");
7
6
  let path = require("path");
8
7
  let url = require("url");
9
- //#region src/resolve-packages.ts
10
- /**
11
- * Resolve external DS package specifiers to rootDir-relative entry paths.
12
- * Node-generic (no webpack APIs): Pass 1 walks the consumer's workspaces for
13
- * matching package names; Pass 2 falls back to `require.resolve`. Specifiers
14
- * that resolve nowhere are simply omitted (spec: silent skip).
15
- */
16
- function resolvePackagesByName(rootDir, names) {
17
- if (names.length === 0) return {};
18
- const nameSet = new Set(names);
19
- const resolved = /* @__PURE__ */ new Set();
20
- const packageMap = {};
21
- try {
22
- const rootPkg = JSON.parse((0, fs.readFileSync)((0, path.join)(rootDir, "package.json"), "utf-8"));
23
- const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : rootPkg.workspaces?.packages ?? [];
24
- for (const ws of workspaces) {
25
- const wsDir = (0, path.resolve)(rootDir, ws);
26
- if (!(0, fs.existsSync)(wsDir)) continue;
27
- try {
28
- const pkg = JSON.parse((0, fs.readFileSync)((0, path.join)(wsDir, "package.json"), "utf-8"));
29
- const name = pkg.name || "";
30
- if (nameSet.has(name)) {
31
- const entryPath = (0, path.resolve)(wsDir, pkg.main || pkg.module || "index.ts");
32
- if ((0, fs.existsSync)(entryPath)) {
33
- packageMap[name] = (0, path.relative)(rootDir, entryPath);
34
- resolved.add(name);
35
- }
36
- }
37
- } catch {}
38
- }
39
- } catch {}
40
- for (const name of nameSet) {
41
- if (resolved.has(name)) continue;
42
- try {
43
- packageMap[name] = (0, path.relative)(rootDir, require.resolve(name, { paths: [rootDir] }));
44
- } catch {}
45
- }
46
- return packageMap;
47
- }
48
- //#endregion
49
- //#region src/timing.ts
50
- /**
51
- * Verbose build-timing waterfall: JS phases, analysis sub-phases, and the
52
- * shared Rust phase table. `ANIMUS_TIMING_JSON=1` additionally dumps a
53
- * machine-readable merged record.
54
- */
55
- function logBuildTimings(bt, rustTiming, log, verbose) {
56
- if (!verbose) return;
57
- const pad = (label, width) => label + " ".repeat(Math.max(0, width - label.length));
58
- for (const [key, label] of [
59
- ["systemLoad", "system-load"],
60
- ["fileDiscovery", "file-discovery"],
61
- ["fileRead", "file-read+hash"],
62
- ["packageResolve", "pkg-resolve"],
63
- ["analysis", "analysis"]
64
- ]) {
65
- const ms = bt[key] ?? 0;
66
- if (ms === 0 && !Object.hasOwn(bt, key)) continue;
67
- log(` ${pad(label, 17)}${String(ms).padStart(5)}ms`);
68
- if (key === "analysis") for (const [sk, sl] of [
69
- ["jsonSerialize", "json-serialize"],
70
- ["rustExtract", "rust-extract"],
71
- ["jsonParse", "json-parse"]
72
- ]) {
73
- const sms = bt[sk] ?? 0;
74
- log(` ${pad(sl, 15)}${String(sms).padStart(5)}ms`);
75
- if (sk === "rustExtract" && rustTiming) for (const line of (0, _animus_ui_extract_pipeline.formatRustTimingWaterfall)(rustTiming, {
76
- indent: " ",
77
- labelWidth: 13
78
- })) log(line);
79
- }
80
- }
81
- log(` total ${String(bt.total).padStart(5)}ms`);
82
- if (process.env.ANIMUS_TIMING_JSON === "1") {
83
- const merged = {};
84
- for (const [k, v] of Object.entries(bt)) merged[`buildStart.${k}`] = v;
85
- if (rustTiming) {
86
- for (const [k, v] of Object.entries(rustTiming)) if (typeof v === "number") merged[`rust.${k}`] = v;
87
- }
88
- console.info(`[animus:timing] ${JSON.stringify(merged)}`);
89
- }
90
- }
91
- //#endregion
92
- //#region src/extraction-session.ts
93
- /**
94
- * Module id the Rust emitter injects for the extracted stylesheet — also the
95
- * exact resolve.alias key with-animus registers for it, which the adapter's
96
- * alias harvesting must skip.
97
- */
98
- const ANIMUS_CSS_MODULE_ID = ".animus/styles.css";
99
- /** Retention window for sibling session directories (design D2). */
100
- const SESSION_DIR_MAX_AGE_MS = 1440 * 60 * 1e3;
101
- /** Watchdog added to the debounce ceiling for status deadlines (D3). */
102
- const STATUS_WATCHDOG_MS = 2e3;
103
- /** Flat legacy `.animus/` artifacts removed at session start — unreachable
104
- * by session-scoped loaders and no longer written by anyone. */
105
- const LEGACY_FLAT_ARTIFACTS = [
106
- require_session_paths.MANIFEST_ARTIFACT,
107
- require_session_paths.ANALYSIS_INPUTS_ARTIFACT,
108
- require_session_paths.STYLES_ARTIFACT,
109
- require_session_paths.SYSTEM_PROPS_ARTIFACT,
110
- require_session_paths.REPLACEMENT_EPOCH_ARTIFACT,
111
- require_session_paths.ANALYSIS_COMMIT_ARTIFACT,
112
- require_session_paths.ANALYSIS_STATUS_ARTIFACT
113
- ];
114
- /** Default path fragments excluded from source discovery (full + watch). */
115
- const DEFAULT_EXCLUDE = [
116
- "node_modules",
117
- "dist",
118
- ".test.",
119
- ".spec.",
120
- ".next",
121
- ".animus"
122
- ];
123
- /**
124
- * Bundler-agnostic extraction pipeline: system loading, source discovery
125
- * and ingestion, external-package collection, analysis, stylesheet and
126
- * system-props emission, and watch-cycle diffing.
127
- *
128
- * The webpack adapter (plugin.ts) owns only bundler wiring: hook
129
- * registration, alias harvesting from the compiler config, in-memory asset
130
- * replacement, and translating watch events into `handleWatchUpdate`
131
- * change sets. Keeping this class free of webpack types is deliberate — a
132
- * future Turbopack integration must drive the same session from outside
133
- * the bundler (Turbopack has no compiler-hook surface), reusing everything
134
- * here unchanged.
135
- *
136
- * Outputs are published through the package singleton (shared CSS,
137
- * manifest, system props) and written to `.animus/` on disk.
138
- */
139
- var ExtractionSession = class {
140
- /** Set by the adapter before any pipeline run. */
141
- rootDir = null;
142
- /** Serialized path aliases, harvested by the adapter from bundler config. */
143
- pathAliasesJson = null;
144
- assetCopyCache = /* @__PURE__ */ new Map();
145
- /** Physical asset files registered with the host watcher. */
146
- assetDependencyPaths = /* @__PURE__ */ new Set();
147
- assetDependencyKeys = /* @__PURE__ */ new Set();
148
- /** Emitter identity override for the system-props module id. Webpack mode
149
- * (null) injects the absolute `.animus/system-props.js` path, resolved by
150
- * NormalModuleReplacement; Turbopack rejects absolute-path imports, so
151
- * its driver sets the virtual id that `resolveAlias` maps to disk. */
152
- systemPropsModuleId = null;
153
- /** Whether the analysis-inputs hydration corpus is serialized + written.
154
- * False (webpack mode): the loader shares the pipeline process and reads
155
- * the manifest from memory — the corpus is never persisted. True is set
156
- * by the Turbopack orchestration driver, whose isolated loader workers
157
- * replay it (spec: next-turbopack-integration, "Manifest disk artifact" /
158
- * "Webpack mode skips the hydration corpus"). */
159
- persistAnalysisInputs = false;
160
- /** Absolute directory prefixes for external DS packages (loader allowlisting). */
161
- externalPackageDirs = [];
162
- /** Absolute package dir → owning specifier (cross-source correlation). */
163
- externalDirOwners = {};
164
- /** rootDir-relative external file → owning specifier (correlation join). */
165
- externalFileOwners = {};
166
- /** External package specifier → absolute source entry path. */
167
- externalSourceEntries = /* @__PURE__ */ new Map();
168
- /** SourceId derivation authority for the current generation (openspec:
169
- * external-source-watch-ingestion, design D2) — canonical roots
170
- * realpath'd once per full pipeline; alias→SourceId associations
171
- * recorded while files exist so deletion resolves through the cache. */
172
- sourceIdentity = null;
173
- /** Canonical admitted external root → declared specifiers (set-valued
174
- * ownership, design D2; duplicate specifiers resolving to one canonical
175
- * root share it). Engine watch wiring and tests read this. */
176
- externalRootOwners = /* @__PURE__ */ new Map();
177
- /** Canonical admitted external watch roots — the engine-side watch
178
- * surface (webpack contextDependencies / Turbopack watchers). */
179
- externalWatchRoots = [];
180
- /** Canonical external root → (raw sourceKey → {raw content hash, recorded
181
- * abs spelling}): the previous-generation inventory a dirty-root rewalk
182
- * diffs against (design D3). Raw hashes — MDX preprocessing never skews
183
- * the diff; the recorded abs path is a registered deletion alias, so a
184
- * reconstructed deletion always resolves through cached identity. */
185
- externalInventory = /* @__PURE__ */ new Map();
186
- /** Sticky diagnostics (design D5/D7): stable key → message, re-surfaced
187
- * on every full pipeline until the underlying condition clears. */
188
- stickyDiagnostics = /* @__PURE__ */ new Map();
189
- /** Volume-membership predicate for the cross-volume gate (design D5).
190
- * Injected seam: the shared predicate only discriminates on Windows
191
- * (win32 semantics are unit-tested via path.win32), so runtime tests
192
- * drive the gate through this. */
193
- sharesProjectVolume = _animus_ui_extract_pipeline.sharesVolumeRoot;
194
- /** Orchestrator seams (openspec: external-source-watch-ingestion, design
195
- * D4). `onExternalRootResolved` fires per volume-admitted external root
196
- * DURING collection — after resolution, BEFORE that root's sources are
197
- * walked — so a watching host can open its handle with no blind gap
198
- * between scan and watch. `onExternalRootsCommitted` fires once after a
199
- * successful full-pipeline publication with the complete admitted
200
- * canonical root set (the host's promote/replay/close-old phase; a
201
- * failed pipeline never fires it, so the host rolls back). */
202
- onExternalRootResolved = null;
203
- onExternalRootsCommitted = null;
204
- options;
205
- staticCssJson;
206
- system = null;
207
- /** Discovery-time keyframes diagnostics awaiting the shared surfacing pass. */
208
- externalKeyframesDiagnostics = [];
209
- /** Full package-resolution map from the last full pipeline — replayed by
210
- * incremental passes (sourceEntries alone omits dist-resolved packages). */
211
- lastPackageMap = {};
212
- fileCache = /* @__PURE__ */ new Map();
213
- systemDependencyKeys = /* @__PURE__ */ new Set();
214
- /** Loader-reported dependency paths for bundler watch registration. */
215
- systemDependencyPaths = [];
216
- systemDependencyHashes = /* @__PURE__ */ new Map();
217
- lastSystemPropsHash = null;
218
- /** Per-payload write guards: payloadHash gates rewrites (byte-identical
219
- * payloads leave disk untouched); diskHash is the hash of the CURRENT
220
- * disk bytes (envelope included) recorded in the analysis-commit. null =
221
- * not yet seeded (first publication reads the artifact's envelope so a
222
- * same-session restart never rewrites byte-identical artifacts). */
223
- artifactRecords = {
224
- manifest: null,
225
- inputs: null,
226
- styles: null
227
- };
228
- /** Last written analysis-commit; null = seeded from disk on first use. */
229
- lastCommit = null;
230
- /** Session identity — claimed once per PROCESS (one Next invocation),
231
- * adopted by every subsequent session instance so all compilers share
232
- * one artifact tree (design D2: `next dev`/`next build` co-writing are
233
- * separate processes and therefore separate sessions). */
234
- sessionId = require_singleton.claimProcessSessionId();
235
- /** Epoch value of the last published analysis; null = not yet known
236
- * (seeded from the session dir's artifact on first publication so a
237
- * same-session restart with unchanged plans never rewrites bytes). */
238
- lastEpochValue = null;
239
- /** Watcher debounce ceiling feeding status deadlines (design D3). */
240
- debounceCeilingMs = 75;
241
- /** Test seam: observes every session-artifact write (name, content)
242
- * post-rename — write ORDER is part of the transaction contract. */
243
- onArtifactWrite = null;
244
- /** Status attempt bookkeeping (design D3). */
245
- statusAttemptId = 0;
246
- statusAttemptOpen = false;
247
- /** Debounce-window observations pending analysis (sourceKey → hash). */
248
- debouncePending = /* @__PURE__ */ new Map();
249
- /** Session-start hygiene (pruning + legacy cleanup) runs once. */
250
- sessionStartHygieneDone = false;
251
- lcssTargets = null;
252
- constructor(options) {
253
- this.options = options;
254
- this.staticCssJson = (0, _animus_ui_extract_pipeline.serializeStaticCss)(options.staticCss);
255
- }
256
- /** This session's artifact directory. Requires `rootDir` to be set. */
257
- get sessionDir() {
258
- return require_session_paths.sessionArtifactDir(this.rootDir, this.sessionId);
259
- }
260
- get verbose() {
261
- return this.options.verbose === true || process.env.ANIMUS_DEBUG === "1" || process.env.ANIMUS_DEBUG === "true";
262
- }
263
- log(msg) {
264
- if (this.verbose) console.info(`[animus] ${msg}`);
265
- }
266
- warn(msg) {
267
- console.warn(`[animus] ${msg}`);
268
- }
269
- now() {
270
- return this.verbose ? performance.now() : 0;
271
- }
272
- elapsed(t) {
273
- return this.verbose ? Math.round(performance.now() - t) : 0;
274
- }
275
- /** Lazily-computed scan config (options are constructor-fixed, so the
276
- * derivation is stable for the session's lifetime). */
277
- scanConfigMemo = null;
278
- /** Resolve the scan configuration from options — the single source of the
279
- * exclude/extension policy shared by the full and incremental pipelines. */
280
- resolveScanConfig() {
281
- if (this.scanConfigMemo === null) {
282
- const extensionsSet = new Set(this.options.extensions ?? _animus_ui_extract_pipeline.DEFAULT_EXTENSIONS);
283
- this.scanConfigMemo = {
284
- excludePatterns: this.options.exclude ?? DEFAULT_EXCLUDE,
285
- extensionsSet,
286
- shouldHandleMdx: extensionsSet.has(".mdx")
287
- };
288
- }
289
- return this.scanConfigMemo;
290
- }
291
- /**
292
- * Watch entry point: one single-flight analysis transaction per event
293
- * batch (openspec: next-webpack-served-transform-coherence, design D3).
294
- * The first entering session runs the transaction; every concurrent
295
- * entry — same session re-entered, or another compiler's session
296
- * (client/server/RSC each hold their own instance) — joins the in-flight
297
- * promise, so no compiler proceeds against a generation older than the
298
- * one the transaction publishes. A rejected transaction rejects every
299
- * joiner; the gate always clears.
300
- */
301
- async handleWatchUpdate(changes) {
302
- const inflight = require_singleton.getWatchTransaction();
303
- if (inflight) {
304
- await inflight;
305
- return;
306
- }
307
- if (!this.system) return;
308
- const transaction = this.processWatchUpdate(changes);
309
- require_singleton.setWatchTransaction(transaction);
310
- try {
311
- await transaction;
312
- } finally {
313
- require_singleton.setWatchTransaction(null);
314
- }
315
- }
316
- /**
317
- * Incremental watch pass: geological reset when the system file changed,
318
- * otherwise content-hash diffing restricted to the watcher's change sets
319
- * (falling back to a full discovery walk when no sets are provided).
320
- */
321
- async processWatchUpdate(changes) {
322
- const rootDir = this.rootDir;
323
- try {
324
- changes = this.reconcileExternalRoots(changes);
325
- } catch (err) {
326
- this.warn(`external root reconciliation failed: ${String(err)}`);
327
- }
328
- const changed = [...changes.modifiedFiles ?? [], ...changes.removedFiles ?? []];
329
- let assetChanged = false;
330
- try {
331
- let systemHit;
332
- if (!changes.modifiedFiles && !changes.removedFiles) for (const [dep, hash] of this.systemDependencyHashes) {
333
- let current = "";
334
- try {
335
- current = (0, _animus_ui_extract_pipeline.contentHash)((0, fs.readFileSync)(dep, "utf-8"));
336
- } catch {}
337
- if (current !== hash) {
338
- systemHit = dep;
339
- break;
340
- }
341
- }
342
- else systemHit = changed.find((path$2) => (0, _animus_ui_extract_pipeline.toWatchKeys)(path$2).some((key) => this.systemDependencyKeys.has(key)));
343
- if (systemHit) {
344
- this.log(`geological reset: system dependency changed (${(0, path.relative)(rootDir, systemHit)})`);
345
- this.resetForHmr();
346
- const promise = this.runFullPipeline(this.pendingFromBatch(changes));
347
- require_singleton.setAnalysisPromise(promise);
348
- await promise;
349
- return;
350
- }
351
- if (changed.some((path$3) => (0, _animus_ui_extract_pipeline.toWatchKeys)(path$3).some((key) => this.assetDependencyKeys.has(key)))) {
352
- this.assetCopyCache.clear();
353
- assetChanged = true;
354
- }
355
- } catch (err) {
356
- this.warn(`HMR geological-reset check failed: ${String(err)}`);
357
- }
358
- const { excludePatterns, extensionsSet, shouldHandleMdx } = this.resolveScanConfig();
359
- const priorCacheEntries = /* @__PURE__ */ new Map();
360
- const recordPrior = (key) => {
361
- if (!priorCacheEntries.has(key)) priorCacheEntries.set(key, this.fileCache.get(key) ?? null);
362
- };
363
- const inventoryUpdates = [];
364
- let removedAny = false;
365
- if (changes.removedFiles) for (const removedPath of changes.removedFiles) {
366
- let key;
367
- let owningRoot = null;
368
- if ((0, _animus_ui_extract_pipeline.isPathWithinRoot)(rootDir, removedPath)) key = (0, path.relative)(rootDir, removedPath);
369
- else {
370
- const resolved = this.sourceIdentity?.resolveDeletedSourceId(removedPath);
371
- if (!resolved) continue;
372
- key = resolved.sourceKey;
373
- owningRoot = resolved.owningRoot;
374
- }
375
- recordPrior(key);
376
- recordPrior(key + ".tsx");
377
- if (this.fileCache.delete(key) || this.fileCache.delete(key + ".tsx")) {
378
- removedAny = true;
379
- if (owningRoot) {
380
- inventoryUpdates.push({
381
- root: owningRoot,
382
- key,
383
- hash: null,
384
- abs: removedPath
385
- });
386
- delete this.externalFileOwners[key];
387
- delete this.externalFileOwners[key + ".tsx"];
388
- }
389
- }
390
- }
391
- let targets;
392
- if (changes.modifiedFiles) {
393
- targets = [];
394
- for (const modifiedPath of changes.modifiedFiles) {
395
- const classified = this.classifyWatchPath(modifiedPath, {
396
- excludePatterns,
397
- extensionsSet
398
- });
399
- if (classified) targets.push({
400
- abs: modifiedPath,
401
- ...classified
402
- });
403
- }
404
- } else targets = (0, _animus_ui_extract_pipeline.discoverFiles)(rootDir, rootDir, excludePatterns, extensionsSet).map((abs) => ({
405
- abs,
406
- key: (0, path.relative)(rootDir, abs),
407
- owningRoot: null
408
- }));
409
- const changedPaths = [];
410
- for (const target of targets) {
411
- let relPath = target.key;
412
- let source;
413
- try {
414
- source = (0, fs.readFileSync)(target.abs, "utf-8");
415
- } catch {
416
- continue;
417
- }
418
- const rawHash = (0, _animus_ui_extract_pipeline.contentHash)(source);
419
- if (shouldHandleMdx && (0, path.extname)(target.abs) === ".mdx") {
420
- const processed = await this.preprocessMdxEntry(source, relPath, { warn: false });
421
- if (!processed) continue;
422
- source = processed.source;
423
- relPath = processed.relPath;
424
- }
425
- const cached = this.fileCache.get(relPath);
426
- const hash = relPath === target.key ? rawHash : (0, _animus_ui_extract_pipeline.contentHash)(source);
427
- if (!cached || cached.hash !== hash) {
428
- changedPaths.push(relPath);
429
- recordPrior(relPath);
430
- this.fileCache.set(relPath, {
431
- hash,
432
- source
433
- });
434
- if (target.owningRoot) {
435
- const owner = this.externalRootOwners.get(target.owningRoot)?.values().next().value;
436
- if (owner !== void 0) this.externalFileOwners[relPath] ??= owner;
437
- inventoryUpdates.push({
438
- root: target.owningRoot,
439
- key: target.key,
440
- hash: rawHash,
441
- abs: target.abs
442
- });
443
- }
444
- }
445
- }
446
- if (changedPaths.length > 0 || removedAny || assetChanged) {
447
- const fileEntries = this.buildFileEntriesFromCache();
448
- const pending = changedPaths.map((rel) => [rel, this.fileCache.get(rel).hash]);
449
- require_singleton.resetAnalysisPromise();
450
- const promise = this.runIncrementalPipeline(fileEntries, pending);
451
- require_singleton.setAnalysisPromise(promise);
452
- try {
453
- await promise;
454
- } catch (err) {
455
- for (const [key, prior] of priorCacheEntries) if (prior === null) this.fileCache.delete(key);
456
- else this.fileCache.set(key, prior);
457
- throw err;
458
- }
459
- for (const update of inventoryUpdates) {
460
- let inventory = this.externalInventory.get(update.root);
461
- if (!inventory) {
462
- inventory = /* @__PURE__ */ new Map();
463
- this.externalInventory.set(update.root, inventory);
464
- }
465
- if (update.hash === null) inventory.delete(update.key);
466
- else inventory.set(update.key, {
467
- hash: update.hash,
468
- abs: update.abs
469
- });
470
- }
471
- } else if (this.statusAttemptOpen) {
472
- this.debouncePending.clear();
473
- this.writeAnalysisStatus("idle", []);
474
- }
475
- }
476
- /**
477
- * Preprocess one `.mdx` entry into scanner-consumable tsx. Returns the
478
- * rewritten source plus the `relPath + '.tsx'` path on success, or null
479
- * when the file must be skipped.
480
- *
481
- * `warn: false` (incremental watch pass) skips silently — the full
482
- * pipeline already surfaced the warning. `warn: true` (full pipeline)
483
- * warns ONCE for a missing @mdx-js/mdx dependency via the shared
484
- * `missingDepFlag` holder, and every time for a preprocessing error.
485
- */
486
- async preprocessMdxEntry(source, relPath, opts) {
487
- const result = await (0, _animus_ui_extract_pipeline.preprocessMdx)(source, relPath);
488
- if (result.kind === "missing-dep") {
489
- if (opts.warn && opts.missingDepFlag && !opts.missingDepFlag.warned) {
490
- console.warn("[animus] ⚠ .mdx in extensions but @mdx-js/mdx not installed; MDX files skipped");
491
- opts.missingDepFlag.warned = true;
492
- }
493
- return null;
494
- }
495
- if (result.kind === "error") {
496
- if (opts.warn) console.warn(`[animus] ⚠ MDX preprocessing failed for ${relPath}: ${result.error}`);
497
- return null;
498
- }
499
- return {
500
- source: result.source,
501
- relPath: relPath + ".tsx"
502
- };
503
- }
504
- async runFullPipeline(pending = []) {
505
- const pipelineStart = this.now();
506
- const bt = {};
507
- this.runSessionStartHygiene();
508
- require_singleton.setSessionArtifactDir(this.sessionDir);
509
- (0, _animus_ui_extract_pipeline.clearEngineCache)(require_singleton.engineApi);
510
- const rootDir = this.rootDir;
511
- const resolvedSystemPath = (0, path.resolve)(rootDir, this.options.system);
512
- let t = this.now();
513
- this.system = (0, _animus_ui_extract_pipeline.loadSystemConfig)(require_singleton.engineApi, {
514
- systemPath: resolvedSystemPath,
515
- rootDir,
516
- prefix: this.options.prefix
517
- });
518
- this.assetCopyCache.clear();
519
- this.assetDependencyPaths.clear();
520
- this.assetDependencyKeys.clear();
521
- {
522
- const deps = this.system.dependencies ?? [];
523
- const keys = /* @__PURE__ */ new Set();
524
- for (const key of (0, _animus_ui_extract_pipeline.toWatchKeys)(resolvedSystemPath)) keys.add(key);
525
- for (const dep of deps) for (const key of (0, _animus_ui_extract_pipeline.toWatchKeys)(dep)) keys.add(key);
526
- this.systemDependencyKeys = keys;
527
- this.systemDependencyPaths = deps;
528
- const hashes = /* @__PURE__ */ new Map();
529
- for (const dep of deps.length > 0 ? deps : [resolvedSystemPath]) try {
530
- hashes.set(dep, (0, _animus_ui_extract_pipeline.contentHash)((0, fs.readFileSync)(dep, "utf-8")));
531
- } catch {
532
- hashes.set(dep, "");
533
- }
534
- this.systemDependencyHashes = hashes;
535
- }
536
- bt.systemLoad = this.elapsed(t);
537
- t = this.now();
538
- const { excludePatterns, extensionsSet, shouldHandleMdx } = this.resolveScanConfig();
539
- const missingDepFlag = { warned: false };
540
- const files = (0, _animus_ui_extract_pipeline.discoverFiles)(rootDir, rootDir, excludePatterns, extensionsSet);
541
- bt.fileDiscovery = this.elapsed(t);
542
- t = this.now();
543
- const fileEntries = [];
544
- for (const filePath of files) {
545
- let source = (0, fs.readFileSync)(filePath, "utf-8");
546
- let relPath = (0, path.relative)(rootDir, filePath);
547
- if (shouldHandleMdx && (0, path.extname)(filePath) === ".mdx") {
548
- const processed = await this.preprocessMdxEntry(source, relPath, {
549
- warn: true,
550
- missingDepFlag
551
- });
552
- if (!processed) continue;
553
- source = processed.source;
554
- relPath = processed.relPath;
555
- }
556
- const hash = (0, _animus_ui_extract_pipeline.contentHash)(source);
557
- this.fileCache.set(relPath, {
558
- hash,
559
- source
560
- });
561
- fileEntries.push({
562
- path: relPath,
563
- source,
564
- hash
565
- });
566
- }
567
- bt.fileRead = this.elapsed(t);
568
- bt.fileCount = fileEntries.length;
569
- t = this.now();
570
- const packageNames = (0, _animus_ui_extract_pipeline.extractSystemFilePackages)(resolvedSystemPath);
571
- const preResolved = resolvePackagesByName(rootDir, packageNames);
572
- const rawExternalFiles = /* @__PURE__ */ new Map();
573
- const collected = await (0, _animus_ui_extract_pipeline.collectExternalPackageSources)({
574
- specifiers: packageNames,
575
- resolveSpecifier: (name) => preResolved[name] ? (0, path.resolve)(rootDir, preResolved[name]) : null,
576
- rootDir,
577
- extensionsSet,
578
- hasEntry: (relPath) => fileEntries.some((e) => e.path === relPath),
579
- preprocessFile: async (source, relPath, absPath) => {
580
- rawExternalFiles.set(absPath, (0, _animus_ui_extract_pipeline.contentHash)(source));
581
- if (shouldHandleMdx && (0, path.extname)(absPath) === ".mdx") return this.preprocessMdxEntry(source, relPath, {
582
- warn: true,
583
- missingDepFlag
584
- });
585
- return {
586
- source,
587
- relPath
588
- };
589
- },
590
- onUnreadable: (relPath, err) => this.warn(`skipped unreadable package file ${relPath}: ${String(err)}`),
591
- onPackageResolved: (_specifier, packageDir) => {
592
- if (!this.onExternalRootResolved) return;
593
- if (!this.sharesProjectVolume(rootDir, packageDir)) return;
594
- let canonical = packageDir;
595
- try {
596
- canonical = (0, fs.realpathSync)(packageDir);
597
- } catch {}
598
- this.onExternalRootResolved(canonical);
599
- }
600
- });
601
- for (const record of collected.outcomes) if (record.outcome === "empty") this.warn(`include '${record.specifier}' resolved but discovered no component sources`);
602
- const unresolvableMessage = (0, _animus_ui_extract_pipeline.unresolvableIncludesMessage)(collected.outcomes);
603
- if (unresolvableMessage !== null) {
604
- if (this.options.strict) throw new Error(unresolvableMessage);
605
- this.warn(unresolvableMessage);
606
- }
607
- const staleDistMessage = (0, _animus_ui_extract_pipeline.staleDistIncludesMessage)(collected.outcomes);
608
- if (staleDistMessage !== null) {
609
- if (this.options.strict) throw new Error(staleDistMessage);
610
- this.warn(staleDistMessage);
611
- }
612
- const rejectedSpecifiers = /* @__PURE__ */ new Set();
613
- const crossVolumeDetails = [];
614
- for (const [dir, specifiers] of Object.entries(collected.dirOwnerSets)) {
615
- if (this.sharesProjectVolume(rootDir, dir)) continue;
616
- for (const specifier of specifiers) rejectedSpecifiers.add(specifier);
617
- crossVolumeDetails.push(`${specifiers.join(", ")} → ${dir}`);
618
- }
619
- this.stickyDiagnostics.delete("cross-volume");
620
- if (rejectedSpecifiers.size > 0) {
621
- const message = `ANIMUS_EXTERNAL_CROSS_VOLUME_UNSUPPORTED: external package source root(s) on a different volume than the project root (${rootDir}): ${crossVolumeDetails.join("; ")} — cross-volume workspace sources are unsupported; the package(s) are excluded from extraction and watching`;
622
- if (this.options.strict) throw new Error(`[animus-next] ${message}`);
623
- this.stickyDiagnostics.set("cross-volume", message);
624
- }
625
- for (const message of this.stickyDiagnostics.values()) this.warn(message);
626
- const admitted = (0, _animus_ui_extract_pipeline.excludeCollectedPackages)(collected, rejectedSpecifiers, rootDir);
627
- const identity = (0, _animus_ui_extract_pipeline.createSourceIdentity)(rootDir);
628
- this.externalRootOwners = /* @__PURE__ */ new Map();
629
- for (const [dir, specifiers] of Object.entries(admitted.dirOwnerSets)) {
630
- const canonical = identity.registerExternalRoot(dir);
631
- const owners = this.externalRootOwners.get(canonical) ?? /* @__PURE__ */ new Set();
632
- for (const specifier of specifiers) owners.add(specifier);
633
- this.externalRootOwners.set(canonical, owners);
634
- }
635
- this.sourceIdentity = identity;
636
- this.externalWatchRoots = identity.externalRoots();
637
- this.externalInventory = /* @__PURE__ */ new Map();
638
- for (const [absPath, rawHash] of rawExternalFiles) {
639
- const resolved = identity.resolveSourceId(absPath);
640
- if (!resolved?.owningRoot) continue;
641
- let inventory = this.externalInventory.get(resolved.owningRoot);
642
- if (!inventory) {
643
- inventory = /* @__PURE__ */ new Map();
644
- this.externalInventory.set(resolved.owningRoot, inventory);
645
- }
646
- inventory.set(resolved.sourceKey, {
647
- hash: rawHash,
648
- abs: absPath
649
- });
650
- }
651
- const packageMap = admitted.packageMap;
652
- this.lastPackageMap = packageMap;
653
- this.externalDirOwners = (0, _animus_ui_extract_pipeline.firstOwners)(admitted.dirOwnerSets);
654
- this.externalFileOwners = admitted.fileOwners;
655
- this.externalSourceEntries = admitted.sourceEntries;
656
- for (const entry of admitted.entries) {
657
- const hash = (0, _animus_ui_extract_pipeline.contentHash)(entry.source);
658
- this.fileCache.set(entry.path, {
659
- hash,
660
- source: entry.source
661
- });
662
- fileEntries.push({
663
- path: entry.path,
664
- source: entry.source,
665
- hash
666
- });
667
- }
668
- this.externalPackageDirs = admitted.packageDirs;
669
- require_singleton.setSharedExternalDirs(admitted.packageDirs);
670
- require_singleton.setSharedExternalEntries(admitted.sourceEntries);
671
- if (this.system && admitted.keyframesScanEntries.size > 0) {
672
- const api = require_singleton.engineApi();
673
- const merge = (0, _animus_ui_extract_pipeline.mergeExternalKeyframes)((entry, root) => api.scanKeyframesExports(entry, root), this.system.keyframesJson, admitted.keyframesScanEntries.values(), this.rootDir);
674
- this.system.keyframesJson = merge.keyframesJson;
675
- this.externalKeyframesDiagnostics = merge.diagnostics;
676
- }
677
- bt.packageResolve = this.elapsed(t);
678
- const universe = new Set(fileEntries.map((entry) => entry.path));
679
- for (const key of [...this.fileCache.keys()]) if (!universe.has(key)) this.fileCache.delete(key);
680
- await this.analyzeAndEmit(fileEntries, packageMap, false, bt, pipelineStart, pending);
681
- this.onExternalRootsCommitted?.(this.externalWatchRoots);
682
- }
683
- /**
684
- * Reset analysis state for HMR geological reset. Payload write guards go
685
- * back to null so the next publication reseeds them from the disk
686
- * envelopes — a byte-identical post-reset artifact is still not
687
- * rewritten.
688
- */
689
- resetForHmr() {
690
- require_singleton.resetAnalysisPromise();
691
- this.artifactRecords = {
692
- manifest: null,
693
- inputs: null,
694
- styles: null
695
- };
696
- this.lastSystemPropsHash = null;
697
- (0, _animus_ui_extract_pipeline.clearEngineCache)(require_singleton.engineApi);
698
- }
699
- /**
700
- * The (sourceKey, observedSourceHash) pairs of a watch batch — cheap
701
- * evidence for the status file's pending set on the geological-reset
702
- * path, where the batch's component edits ride along with the system
703
- * edit (design D3: loaders wait only on observed inputs).
704
- */
705
- pendingFromBatch(changes) {
706
- const { excludePatterns, extensionsSet } = this.resolveScanConfig();
707
- const pending = [];
708
- for (const path$4 of changes.modifiedFiles ?? []) {
709
- const classified = this.classifyWatchPath(path$4, {
710
- excludePatterns,
711
- extensionsSet
712
- });
713
- if (!classified) continue;
714
- try {
715
- pending.push([classified.key, (0, _animus_ui_extract_pipeline.contentHash)((0, fs.readFileSync)(path$4, "utf-8"))]);
716
- } catch {}
717
- }
718
- return pending;
719
- }
720
- /**
721
- * Root-dirty inventory reconciliation (design D3): partition the
722
- * watcher's change sets into explicit FILE events and external ROOT hits
723
- * (a reported path that IS an admitted root, or a directory inside one),
724
- * rewalk each dirty root through the shared discovery policy, diff the
725
- * SourceId+raw-hash inventory against the previous generation, and merge
726
- * the reconstructed created/edited/deleted deltas with the explicit
727
- * events (identity-level dedup happens downstream at the cache gate).
728
- * Deletion falls out of the inventory diff — webpack may report only the
729
- * directory (probe-proven).
730
- */
731
- reconcileExternalRoots(changes) {
732
- const identity = this.sourceIdentity;
733
- if (!identity) return changes;
734
- if (!changes.modifiedFiles && !changes.removedFiles) return changes;
735
- if (this.externalWatchRoots.length === 0) return changes;
736
- const { extensionsSet } = this.resolveScanConfig();
737
- const dirtyRoots = /* @__PURE__ */ new Set();
738
- const modified = /* @__PURE__ */ new Set();
739
- const removed = /* @__PURE__ */ new Set();
740
- const rootHitFor = (path$5) => {
741
- const containing = identity.containingExternalRoot(path$5);
742
- if (!containing) return null;
743
- try {
744
- return (0, fs.statSync)(path$5).isDirectory() ? containing : null;
745
- } catch {
746
- return identity.resolveDeletedSourceId(path$5) ? null : containing;
747
- }
748
- };
749
- for (const path$6 of changes.modifiedFiles ?? []) {
750
- const root = rootHitFor(path$6);
751
- if (root) dirtyRoots.add(root);
752
- else modified.add(path$6);
753
- }
754
- for (const path$7 of changes.removedFiles ?? []) {
755
- const root = rootHitFor(path$7);
756
- if (root) dirtyRoots.add(root);
757
- else removed.add(path$7);
758
- }
759
- for (const root of dirtyRoots) {
760
- const previous = this.externalInventory.get(root) ?? /* @__PURE__ */ new Map();
761
- const seen = /* @__PURE__ */ new Set();
762
- const walked = (0, _animus_ui_extract_pipeline.walkPackageSources)(root, extensionsSet);
763
- for (const abs of walked) {
764
- const resolved = identity.resolveSourceId(abs);
765
- if (!resolved || resolved.owningRoot !== root) continue;
766
- seen.add(resolved.sourceKey);
767
- let rawHash;
768
- try {
769
- rawHash = (0, _animus_ui_extract_pipeline.contentHash)((0, fs.readFileSync)(abs, "utf-8"));
770
- } catch {
771
- continue;
772
- }
773
- if (previous.get(resolved.sourceKey)?.hash !== rawHash) modified.add(abs);
774
- }
775
- for (const [key, entry] of previous) if (!seen.has(key)) removed.add(entry.abs);
776
- }
777
- return {
778
- modifiedFiles: modified,
779
- removedFiles: removed
780
- };
781
- }
782
- /**
783
- * Route one watcher-reported path (design D1/D2): a project-root member
784
- * (lexical containment — existing local semantics preserved; consumer
785
- * exclude patterns apply) or an admitted external member (identity
786
- * resolution with symlink-escape rejection; package-relative excludes
787
- * mirror the collection walk's filters — guardrail G1, one policy).
788
- * Returns null for dropped paths.
789
- */
790
- classifyWatchPath(absPath, scan) {
791
- if (!scan.extensionsSet.has((0, path.extname)(absPath))) return null;
792
- const rootDir = this.rootDir;
793
- if ((0, _animus_ui_extract_pipeline.isPathWithinRoot)(rootDir, absPath)) {
794
- const rel = (0, path.relative)(rootDir, absPath);
795
- if (scan.excludePatterns.some((pattern) => absPath.includes(pattern) || rel.includes(pattern))) return null;
796
- return {
797
- key: rel,
798
- owningRoot: null
799
- };
800
- }
801
- const resolved = this.sourceIdentity?.resolveSourceId(absPath);
802
- if (!resolved) return null;
803
- if (resolved.owningRoot === null) {
804
- if (scan.excludePatterns.some((pattern) => resolved.sourceKey.includes(pattern))) return null;
805
- return {
806
- key: resolved.sourceKey,
807
- owningRoot: null
808
- };
809
- }
810
- if ((0, _animus_ui_extract_pipeline.isExcludedPackageRelativePath)(resolved.pathInRoot)) return null;
811
- return {
812
- key: resolved.sourceKey,
813
- owningRoot: resolved.owningRoot
814
- };
815
- }
816
- /**
817
- * Build file entries from cache: every cached file rides with full source.
818
- * The v2 engine has NO Rust-side cache (arch-extract-v2-spine: uncached
819
- * re-analysis beats a cache-hit path), so it must always receive full sources
820
- * (openspec: retire-extract-v1 removed the v1 empty-source cache contract).
821
- */
822
- buildFileEntriesFromCache() {
823
- const entries = [];
824
- for (const [path$8, { hash, source }] of this.fileCache) entries.push({
825
- path: path$8,
826
- source,
827
- hash
828
- });
829
- return entries;
830
- }
831
- /**
832
- * Run incremental pipeline with cache-aware file entries.
833
- * Reuses system config from the last full pipeline run.
834
- */
835
- async runIncrementalPipeline(fileEntries, pending = []) {
836
- const bt = {};
837
- const pipelineStart = this.now();
838
- await this.analyzeAndEmit(fileEntries, this.lastPackageMap, true, bt, pipelineStart, pending);
839
- }
840
- /**
841
- * Shared analysis + emit core for both pipelines — the single call site
842
- * that routes every manifest through the shared `runProjectAnalysis`,
843
- * reachable from both runFullPipeline (production) and
844
- * runIncrementalPipeline (HMR).
845
- *
846
- * Owns diagnostic surfacing, CSS assembly + styles.css write guard,
847
- * system-props module emit, and the timing log. The `devMode` flag is the
848
- * ONLY behavioral fork:
849
- *
850
- * - `false` (production): computes bt.analysis + logs the extraction
851
- * report, and writes system-props.js UNCONDITIONALLY (no
852
- * lastSystemPropsHash guard).
853
- * - `true` (HMR): skips the report log, and guards the system-props.js
854
- * write by lastSystemPropsHash.
855
- *
856
- * The disk artifacts land under the session directory in transaction
857
- * order (design D1): manifest → analysis-inputs (Turbopack orchestration
858
- * only) → styles.css → system-props → analysis-commit →
859
- * replacements-epoch (last, only when moved). The analysis-status file walks starting → analyzing →
860
- * committing → idle around the attempt, landing in `failed` (with the
861
- * diagnostic) on any throw (design D3).
862
- */
863
- async analyzeAndEmit(fileEntries, packageMap, devMode, bt, pipelineStart, pending = []) {
864
- this.beginStatusAttempt();
865
- this.debouncePending.clear();
866
- this.writeAnalysisStatus("starting", pending);
867
- try {
868
- await this.analyzeAndEmitAttempt(fileEntries, packageMap, devMode, bt, pipelineStart, pending);
869
- this.writeAnalysisStatus("idle", []);
870
- } catch (err) {
871
- this.writeAnalysisStatus("failed", pending, String(err));
872
- throw err;
873
- }
874
- }
875
- async analyzeAndEmitAttempt(fileEntries, packageMap, devMode, bt, pipelineStart, pending) {
876
- const system = this.system;
877
- const analysisOptions = {
878
- fileEntries,
879
- packageMap,
880
- system,
881
- emitter: {
882
- runtimeImport: "@animus-ui/system/runtime",
883
- cssModuleId: ANIMUS_CSS_MODULE_ID,
884
- systemPropsModuleId: this.systemPropsModuleId ?? require_session_paths.systemPropsPath(this.sessionDir)
885
- },
886
- pathAliasesJson: this.pathAliasesJson,
887
- staticCssJson: this.staticCssJson,
888
- externalDirs: this.externalPackageDirs.map((dir) => (0, path.relative)(this.rootDir, dir)),
889
- devMode
890
- };
891
- this.writeAnalysisStatus("analyzing", pending);
892
- const result = (0, _animus_ui_extract_pipeline.runProjectAnalysis)(require_singleton.engineApi, {
893
- ...analysisOptions,
894
- warn: (message) => this.warn(message),
895
- strict: this.options.strict,
896
- extraDiagnostics: this.externalKeyframesDiagnostics
897
- });
898
- (0, _animus_ui_extract_pipeline.enforceExternalTokenContracts)({
899
- diagnostics: result.manifest?.diagnostics,
900
- fileOwners: this.externalFileOwners,
901
- dirOwners: this.externalDirOwners,
902
- sourceThemeManifestsJson: system.sourceThemeManifestsJson,
903
- strict: this.options.strict,
904
- prefix: "[animus-next]",
905
- warn: (message) => this.warn(message)
906
- });
907
- bt.jsonSerialize = result.timings.serializeMs;
908
- bt.rustExtract = result.timings.extractMs;
909
- bt.jsonParse = result.timings.parseMs;
910
- const manifest = result.manifest;
911
- if (!devMode) {
912
- bt.analysis = (bt.jsonSerialize ?? 0) + (bt.rustExtract ?? 0) + (bt.jsonParse ?? 0);
913
- if (manifest?.report) this.log(`Extracted ${manifest.report.components_extracted ?? "?"}/${manifest.report.components_total ?? "?"} components (${bt.analysis}ms)`);
914
- }
915
- const globalCss = this.substituteAssetReferences(result.globalCss, devMode);
916
- const { declaration, variables, body } = (0, _animus_ui_extract_pipeline.assembleStylesheet)({
917
- layers: this.options.layers,
918
- variableCss: system.variableCss,
919
- globalCss,
920
- componentCss: result.componentCss,
921
- split: true
922
- });
923
- if (this.lcssTargets === null) this.lcssTargets = (0, _animus_ui_extract_pipeline.resolveLightningTargets)(this.options.targets, this.rootDir);
924
- const fullCss = [
925
- declaration,
926
- variables,
927
- (0, _animus_ui_extract_pipeline.postProcessCss)(body, {
928
- minify: this.options.minify ?? process.env.NODE_ENV === "production",
929
- targets: this.lcssTargets,
930
- warnFn: (msg) => this.warn(msg)
931
- })
932
- ].filter(Boolean).join("\n");
933
- require_singleton.setSharedCss(fullCss);
934
- const systemPropsContent = (0, _animus_ui_extract_pipeline.buildSystemPropsModule)({
935
- systemPropMapJson: JSON.stringify(manifest?.system_prop_map ?? {}),
936
- groupRegistryJson: system.groupRegistryJson,
937
- dynamicProps: manifest?.dynamic_props ?? {}
938
- });
939
- require_singleton.setSharedSystemProps(systemPropsContent);
940
- require_singleton.setManifestJson(result.manifestJson);
941
- require_singleton.setAnalyzedHashes(new Map(fileEntries.map((entry) => [entry.path, entry.hash])));
942
- this.writeAnalysisStatus("committing", pending);
943
- const epoch = (0, _animus_ui_extract_pipeline.hashReplacementPlans)((0, _animus_ui_extract_pipeline.snapshotFilePlans)(manifest), systemPropsContent);
944
- if (this.lastCommit === null) this.lastCommit = this.seedCommitFromDisk();
945
- const generation = (this.lastCommit?.generation ?? 0) + 1;
946
- this.publishPayloadArtifact("manifest", require_session_paths.MANIFEST_ARTIFACT, result.manifestJson, epoch, generation, require_session_paths.envelopeJsonArtifact);
947
- if (this.persistAnalysisInputs) this.publishPayloadArtifact("inputs", require_session_paths.ANALYSIS_INPUTS_ARTIFACT, JSON.stringify({
948
- analyzedHashes: Object.fromEntries(fileEntries.map((entry) => [entry.path, entry.hash])),
949
- ...result.inputs
950
- }), epoch, generation, require_session_paths.envelopeJsonArtifact);
951
- this.publishPayloadArtifact("styles", require_session_paths.STYLES_ARTIFACT, fullCss, epoch, generation, require_session_paths.envelopeCssArtifact);
952
- if (devMode) {
953
- const systemPropsHash = (0, _animus_ui_extract_pipeline.contentHash)(systemPropsContent);
954
- if (systemPropsHash !== this.lastSystemPropsHash) {
955
- this.writeSessionArtifact(require_session_paths.SYSTEM_PROPS_ARTIFACT, systemPropsContent);
956
- this.lastSystemPropsHash = systemPropsHash;
957
- }
958
- } else this.writeSessionArtifact(require_session_paths.SYSTEM_PROPS_ARTIFACT, systemPropsContent);
959
- this.publishAnalysisCommit(epoch, generation);
960
- this.publishReplacementEpoch(epoch);
961
- bt.total = this.elapsed(pipelineStart);
962
- logBuildTimings(bt, manifest?.timing, (msg) => this.log(msg), this.verbose);
963
- }
964
- /** Open a status attempt (attemptId increments once per burst — a
965
- * debouncing pre-write and the analysis that follows share one id). */
966
- beginStatusAttempt() {
967
- if (!this.statusAttemptOpen) {
968
- this.statusAttemptId += 1;
969
- this.statusAttemptOpen = true;
970
- }
971
- }
972
- /** Write the session's analysis-status artifact (design D3). Terminal
973
- * states (idle/failed) close the attempt. */
974
- writeAnalysisStatus(state, pending, diagnostic) {
975
- const status = {
976
- schema: 1,
977
- sessionId: this.sessionId,
978
- attemptId: this.statusAttemptId,
979
- state,
980
- pending,
981
- deadlineAt: Date.now() + this.debounceCeilingMs + STATUS_WATCHDOG_MS,
982
- ...diagnostic !== void 0 ? { diagnostic } : {}
983
- };
984
- this.writeSessionArtifact(require_session_paths.ANALYSIS_STATUS_ARTIFACT, JSON.stringify(status));
985
- if (state === "idle" || state === "failed") this.statusAttemptOpen = false;
986
- }
987
- /**
988
- * Orchestrator seam (Turbopack watcher): record watch events observed
989
- * during the debounce window so a loader running ahead of the analysis
990
- * has positive evidence to wait on (design D3 'debouncing'). Paths are
991
- * filtered by the scan config and hashed at observation time.
992
- */
993
- noteDebouncedWatchEvents(absPaths) {
994
- if (!this.rootDir) return;
995
- const additions = this.pendingFromBatch({ modifiedFiles: new Set(absPaths) });
996
- if (additions.length === 0) return;
997
- this.beginStatusAttempt();
998
- for (const [key, hash] of additions) this.debouncePending.set(key, hash);
999
- if (this.debounceStatusWriteScheduled) return;
1000
- this.debounceStatusWriteScheduled = true;
1001
- queueMicrotask(() => {
1002
- this.debounceStatusWriteScheduled = false;
1003
- if (this.debouncePending.size === 0) return;
1004
- this.writeAnalysisStatus("debouncing", [...this.debouncePending.entries()]);
1005
- });
1006
- }
1007
- /** One pending microtask flushes a burst of debounce observations. */
1008
- debounceStatusWriteScheduled = false;
1009
- /**
1010
- * Write one payload artifact into the session directory, enveloped with
1011
- * `{sessionId, generation, replacementEpoch, payloadHash}` and rewritten
1012
- * only when the PAYLOAD bytes changed (byte-identical re-analyses leave
1013
- * disk untouched — spec: "Unchanged manifest is not rewritten"). The
1014
- * recorded diskHash (hash of the enveloped bytes) feeds the
1015
- * analysis-commit.
1016
- */
1017
- publishPayloadArtifact(key, name, payload, epoch, generation, wrap) {
1018
- const payloadHash = (0, _animus_ui_extract_pipeline.contentHash)(payload);
1019
- if (this.artifactRecords[key] === null) this.artifactRecords[key] = this.seedPayloadRecord(key, name);
1020
- const record = this.artifactRecords[key];
1021
- if (record !== null && record.payloadHash === payloadHash) return;
1022
- const envelope = {
1023
- sessionId: this.sessionId,
1024
- generation,
1025
- replacementEpoch: epoch,
1026
- payloadHash
1027
- };
1028
- const bytes = wrap(payload, JSON.stringify(envelope));
1029
- this.writeSessionArtifact(name, bytes);
1030
- this.artifactRecords[key] = {
1031
- payloadHash,
1032
- diskHash: (0, _animus_ui_extract_pipeline.contentHash)(bytes)
1033
- };
1034
- }
1035
- /** Reconstruct a payload write guard from the on-disk artifact's envelope
1036
- * (same-session restart: a byte-identical payload must not rewrite). */
1037
- seedPayloadRecord(key, name) {
1038
- let bytes;
1039
- try {
1040
- bytes = (0, fs.readFileSync)((0, path.join)(this.sessionDir, name), "utf-8");
1041
- } catch {
1042
- return null;
1043
- }
1044
- let envelope;
1045
- try {
1046
- envelope = key === "styles" ? require_session_paths.readCssEnvelope(bytes) : require_session_paths.readJsonEnvelope(bytes);
1047
- } catch {
1048
- return null;
1049
- }
1050
- if (!envelope || typeof envelope.payloadHash !== "string") return null;
1051
- return {
1052
- payloadHash: envelope.payloadHash,
1053
- diskHash: (0, _animus_ui_extract_pipeline.contentHash)(bytes)
1054
- };
1055
- }
1056
- /** Last analysis-commit persisted in this session's directory, or null. */
1057
- seedCommitFromDisk() {
1058
- try {
1059
- const parsed = JSON.parse((0, fs.readFileSync)(require_session_paths.analysisCommitPath(this.sessionDir), "utf-8"));
1060
- return parsed.schema === 1 && parsed.sessionId === this.sessionId && typeof parsed.generation === "number" ? parsed : null;
1061
- } catch {
1062
- return null;
1063
- }
1064
- }
1065
- /**
1066
- * Publish the analysis-commit — the transaction identity (design D1).
1067
- * Written AFTER every payload and BEFORE the epoch; skipped entirely when
1068
- * the payload set (disk hashes) and epoch are unchanged, so a no-op
1069
- * re-analysis neither re-keys loader-worker hydration nor burns a
1070
- * generation.
1071
- */
1072
- publishAnalysisCommit(epoch, generation) {
1073
- const manifestHash = this.artifactRecords.manifest?.diskHash ?? "";
1074
- const inputsHash = this.persistAnalysisInputs ? this.artifactRecords.inputs?.diskHash ?? "" : void 0;
1075
- const stylesHash = this.artifactRecords.styles?.diskHash ?? "";
1076
- const prev = this.lastCommit;
1077
- if (prev !== null && prev.manifestHash === manifestHash && prev.inputsHash === inputsHash && prev.stylesHash === stylesHash && prev.replacementEpoch === epoch) return;
1078
- const commit = {
1079
- schema: 1,
1080
- sessionId: this.sessionId,
1081
- generation,
1082
- replacementEpoch: epoch,
1083
- manifestHash,
1084
- ...inputsHash !== void 0 ? { inputsHash } : {},
1085
- stylesHash
1086
- };
1087
- this.writeSessionArtifact(require_session_paths.ANALYSIS_COMMIT_ARTIFACT, JSON.stringify(commit));
1088
- this.lastCommit = commit;
1089
- }
1090
- /**
1091
- * Publish the canonical replacement epoch: maintain the session-scoped
1092
- * disk witness `{schema, sessionId, epoch}`, rewritten ONLY when the
1093
- * epoch VALUE changes so style-only analyses and same-session restarts
1094
- * leave bytes and mtime untouched, then expose the value through the
1095
- * singleton for the plugin's needBuild fan-out and the loader's catch-up
1096
- * re-check. Every VALUE move also reconciles sibling sessions' epoch
1097
- * artifacts (below) — the webpack cold-cache validity witness.
1098
- */
1099
- publishReplacementEpoch(epoch) {
1100
- if (this.lastEpochValue === null) this.lastEpochValue = this.diskEpochValue();
1101
- if (epoch !== this.lastEpochValue) {
1102
- this.writeSessionArtifact(require_session_paths.REPLACEMENT_EPOCH_ARTIFACT, JSON.stringify({
1103
- schema: 1,
1104
- sessionId: this.sessionId,
1105
- epoch
1106
- }));
1107
- this.lastEpochValue = epoch;
1108
- this.reconcileSiblingEpochs(epoch);
1109
- }
1110
- require_singleton.setReplacementEpoch(epoch);
1111
- }
1112
- /**
1113
- * Sibling epoch reconciliation — the cross-session half of the webpack
1114
- * persistent-cache witness (spec: dev-served-transform-coherence,
1115
- * "Offline change invalidates restored modules"). Restored-module
1116
- * snapshots reference the epoch artifact of the session that BUILT them;
1117
- * with session-scoped trees that file would otherwise sit untouched
1118
- * forever, keeping stale snapshots valid. Whenever this session's epoch
1119
- * value moves, delete every sibling epoch artifact that disagrees (their
1120
- * snapshots must invalidate — restore-on-demand then rebuilds from the
1121
- * current generation) and leave agreeing siblings byte-untouched (warm
1122
- * restores stay valid). Deleting a stale artifact is pruning, not a
1123
- * foreign-session write; races with concurrent sessions are tolerated
1124
- * (S14).
1125
- */
1126
- /** Sibling ids already reconciled away (artifact deleted or absent) —
1127
- * nothing left to invalidate for them on later moves. */
1128
- reconciledSiblingIds = /* @__PURE__ */ new Set();
1129
- /** sessions-root listing memo, keyed by the root dir's mtime — a new or
1130
- * pruned sibling DIRECTORY moves it; agreeing siblings stay listed. */
1131
- siblingListing = null;
1132
- reconcileSiblingEpochs(epoch) {
1133
- const rootPath = require_session_paths.sessionsRootDir(this.rootDir);
1134
- let entries;
1135
- try {
1136
- const mtimeMs = (0, fs.statSync)(rootPath).mtimeMs;
1137
- if (this.siblingListing?.mtimeMs === mtimeMs) entries = this.siblingListing.entries;
1138
- else {
1139
- entries = (0, fs.readdirSync)(rootPath);
1140
- this.siblingListing = {
1141
- mtimeMs,
1142
- entries
1143
- };
1144
- }
1145
- } catch {
1146
- return;
1147
- }
1148
- for (const entry of entries) {
1149
- if (entry === this.sessionId) continue;
1150
- if (this.reconciledSiblingIds.has(entry)) continue;
1151
- const siblingEpochPath = (0, path.join)(rootPath, entry, require_session_paths.REPLACEMENT_EPOCH_ARTIFACT);
1152
- try {
1153
- if (JSON.parse((0, fs.readFileSync)(siblingEpochPath, "utf-8")).epoch === epoch) continue;
1154
- } catch (err) {
1155
- if (err?.code === "ENOENT") {
1156
- this.reconciledSiblingIds.add(entry);
1157
- continue;
1158
- }
1159
- }
1160
- try {
1161
- (0, fs.unlinkSync)(siblingEpochPath);
1162
- } catch {}
1163
- this.reconciledSiblingIds.add(entry);
1164
- }
1165
- }
1166
- /** Epoch value held by this session's on-disk artifact, or null when
1167
- * absent, unreadable, or not the expected schema. */
1168
- diskEpochValue() {
1169
- try {
1170
- const parsed = JSON.parse((0, fs.readFileSync)(require_session_paths.replacementEpochPath(this.sessionDir), "utf-8"));
1171
- return parsed.schema === 1 && typeof parsed.epoch === "string" ? parsed.epoch : null;
1172
- } catch {
1173
- return null;
1174
- }
1175
- }
1176
- /** Session-start hygiene (design D2): delete legacy flat artifacts
1177
- * (unreachable by session-scoped loaders) and prune sibling session
1178
- * directories older than the retention window — never the own dir,
1179
- * tolerating races with concurrently-pruning sessions (S14). */
1180
- runSessionStartHygiene() {
1181
- if (this.sessionStartHygieneDone) return;
1182
- this.sessionStartHygieneDone = true;
1183
- const animusDir = (0, path.join)(this.rootDir, ".animus");
1184
- for (const name of LEGACY_FLAT_ARTIFACTS) try {
1185
- (0, fs.unlinkSync)((0, path.join)(animusDir, name));
1186
- } catch {}
1187
- let entries;
1188
- try {
1189
- entries = (0, fs.readdirSync)(require_session_paths.sessionsRootDir(this.rootDir));
1190
- } catch {
1191
- return;
1192
- }
1193
- const cutoff = Date.now() - SESSION_DIR_MAX_AGE_MS;
1194
- for (const entry of entries) {
1195
- if (entry === this.sessionId) continue;
1196
- const dir = (0, path.join)(require_session_paths.sessionsRootDir(this.rootDir), entry);
1197
- try {
1198
- if ((0, fs.statSync)(dir).mtimeMs < cutoff) (0, fs.rmSync)(dir, {
1199
- recursive: true,
1200
- force: true
1201
- });
1202
- } catch {}
1203
- }
1204
- }
1205
- /** Ensure the session directory exists and write one artifact into it.
1206
- * Write-then-rename so cross-process readers (Turbopack loader workers)
1207
- * can never observe a torn half-written file. The tmp name carries the
1208
- * pid — Next dev evaluates the config in more than one process, and two
1209
- * sessions writing the same artifact must not race on one tmp path. */
1210
- writeSessionArtifact(name, content) {
1211
- const dir = this.sessionDir;
1212
- (0, fs.mkdirSync)(dir, { recursive: true });
1213
- const tmpPath = (0, path.join)(dir, `.${name}.${process.pid}.tmp`);
1214
- (0, fs.writeFileSync)(tmpPath, content);
1215
- (0, fs.renameSync)(tmpPath, (0, path.join)(dir, name));
1216
- this.onArtifactWrite?.(name, content);
1217
- }
1218
- /**
1219
- * asset() placeholder substitution (global-styles-system): resolve each
1220
- * referenced specifier through Node resolution, copy the bytes into the
1221
- * session directory's `assets/` under a content-hashed name, and
1222
- * substitute a RELATIVE url — relative to the session-scoped styles.css,
1223
- * which sits beside `assets/`, so the emitted `./assets/<file>` form is
1224
- * unchanged. The stylesheet is processed by Next's own CSS pipeline
1225
- * (webpack and Turbopack alike), which applies its native asset handling
1226
- * — publicPath and output hashing — to relative url() references.
1227
- * Unsubstitutable specifiers warn and emit literally in non-strict mode,
1228
- * fail the build under `strict: true`.
1229
- */
1230
- substituteAssetReferences(globalCss, devMode) {
1231
- const specifiers = (0, _animus_ui_extract_pipeline.findAssetSpecifiers)(globalCss);
1232
- const assetsDir = (0, path.join)(this.sessionDir, require_session_paths.SESSION_ASSETS_DIR);
1233
- const expected = /* @__PURE__ */ new Set();
1234
- this.assetDependencyPaths.clear();
1235
- this.assetDependencyKeys.clear();
1236
- const urlBySpecifier = /* @__PURE__ */ new Map();
1237
- for (const specifier of specifiers) {
1238
- const cached = this.assetCopyCache.get(specifier);
1239
- if (cached && (0, fs.existsSync)((0, path.join)(assetsDir, cached.fileName))) try {
1240
- const current = (0, fs.statSync)(cached.sourcePath);
1241
- if (current.mtimeMs === cached.mtimeMs && current.size === cached.size) {
1242
- this.trackAssetDependency(cached.sourcePath);
1243
- expected.add(cached.fileName);
1244
- urlBySpecifier.set(specifier, cached.url);
1245
- continue;
1246
- }
1247
- } catch {}
1248
- const resolvedPath = this.resolveAssetSpecifier(specifier);
1249
- if (!resolvedPath) {
1250
- const message = `unresolvable asset() specifier: ${specifier}`;
1251
- if (this.options.strict) throw new Error(`[animus-next] ${message}`);
1252
- this.warn(message);
1253
- urlBySpecifier.set(specifier, specifier);
1254
- continue;
1255
- }
1256
- const bytes = (0, fs.readFileSync)(resolvedPath);
1257
- const sourceStat = (0, fs.statSync)(resolvedPath);
1258
- this.trackAssetDependency(resolvedPath);
1259
- const ext = (0, path.extname)(resolvedPath);
1260
- const fileName = `${(0, path.basename)(resolvedPath, ext)}.${(0, _animus_ui_extract_pipeline.contentHash)(bytes).slice(0, 8)}${ext}`;
1261
- expected.add(fileName);
1262
- if (!(0, fs.existsSync)(assetsDir)) (0, fs.mkdirSync)(assetsDir, { recursive: true });
1263
- const assetPath = (0, path.join)(assetsDir, fileName);
1264
- if (!(0, fs.existsSync)(assetPath)) (0, fs.writeFileSync)(assetPath, bytes);
1265
- const url = `./assets/${fileName}`;
1266
- urlBySpecifier.set(specifier, url);
1267
- this.assetCopyCache.set(specifier, {
1268
- sourcePath: resolvedPath,
1269
- mtimeMs: sourceStat.mtimeMs,
1270
- size: sourceStat.size,
1271
- fileName,
1272
- url
1273
- });
1274
- }
1275
- if (!devMode) pruneStaleAssets(assetsDir, expected);
1276
- return (0, _animus_ui_extract_pipeline.substituteAssetPlaceholders)(globalCss, urlBySpecifier);
1277
- }
1278
- trackAssetDependency(path$9) {
1279
- this.assetDependencyPaths.add(path$9);
1280
- for (const key of (0, _animus_ui_extract_pipeline.toWatchKeys)(path$9)) this.assetDependencyKeys.add(key);
1281
- }
1282
- /**
1283
- * Resolve an asset specifier to an absolute file via the shared pipeline
1284
- * resolver (host aliases → Node resolution → package root), with one
1285
- * session-local last resort: an already-discovered source entry's package
1286
- * root (dist-less workspace kits the shared resolver cannot see).
1287
- */
1288
- resolveAssetSpecifier(specifier) {
1289
- const resolved = (0, _animus_ui_extract_pipeline.resolveAssetFile)(specifier, this.rootDir, this.pathAliasesJson);
1290
- if (resolved) return resolved;
1291
- const segments = specifier.split("/");
1292
- const packageName = specifier.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0];
1293
- const subpath = specifier.slice(packageName.length + 1);
1294
- if (!subpath) return null;
1295
- const sourceEntry = this.externalSourceEntries.get(packageName) ?? [...this.externalSourceEntries].find(([declared]) => declared === packageName || declared.startsWith(`${packageName}/`))?.[1];
1296
- if (sourceEntry) {
1297
- const candidate = (0, path.join)((0, _animus_ui_extract_pipeline.findPackageRoot)(sourceEntry), subpath);
1298
- if ((0, fs.existsSync)(candidate)) return candidate;
1299
- }
1300
- return null;
1301
- }
1302
- };
1303
- /**
1304
- * Sync `.animus/assets/` to the current build's content-hashed file set:
1305
- * anything else in the directory is a superseded revision (the copies are
1306
- * content-addressed and never overwritten) or the leftover of an asset()
1307
- * reference that no longer exists. Failures are tolerated per entry — Next
1308
- * dev evaluates the config in more than one process, and a concurrent
1309
- * session may have removed (or be about to rewrite) the same file; every
1310
- * pass rewrites whatever of its own set is missing, so races self-heal.
1311
- */
1312
- function pruneStaleAssets(assetsDir, expected) {
1313
- if (!(0, fs.existsSync)(assetsDir)) return;
1314
- let entries;
1315
- try {
1316
- entries = (0, fs.readdirSync)(assetsDir);
1317
- } catch {
1318
- return;
1319
- }
1320
- for (const entry of entries) {
1321
- if (expected.has(entry)) continue;
1322
- try {
1323
- (0, fs.unlinkSync)((0, path.join)(assetsDir, entry));
1324
- } catch {}
1325
- }
1326
- }
1327
- //#endregion
1328
8
  //#region src/loader-path.ts
1329
9
  const __dirname$2 = (0, path.dirname)((0, url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
1330
10
  /**
@@ -1382,8 +62,8 @@ var AnimusWebpackPlugin = class {
1382
62
  constructor(options) {
1383
63
  this.options = options;
1384
64
  (0, _animus_ui_extract_pipeline.assertNoRetiredEngineSelection)(options.engine);
1385
- require_singleton.setSharedEngine(options.engine ?? "v2");
1386
- this.session = new ExtractionSession(options);
65
+ (0, _animus_ui_extract_session.setSharedEngine)(options.engine ?? "v2");
66
+ this.session = new _animus_ui_extract_session.ExtractionSession(options);
1387
67
  this.animusLoaderPaths = new Set([resolveAnimusLoaderPath()]);
1388
68
  if (options.loaderPath) this.animusLoaderPaths.add(options.loaderPath);
1389
69
  }
@@ -1407,7 +87,7 @@ var AnimusWebpackPlugin = class {
1407
87
  appendEpochWatchIgnore(compiler) {
1408
88
  const options = compiler.options;
1409
89
  if (!options) return;
1410
- const epochPath = require_session_paths.replacementEpochPath(require_session_paths.sessionArtifactDir(this.session.rootDir ?? compiler.context, this.session.sessionId));
90
+ const epochPath = (0, _animus_ui_extract_session.replacementEpochPath)((0, _animus_ui_extract_session.sessionArtifactDir)(this.session.rootDir ?? compiler.context, this.session.sessionId));
1411
91
  options.watchOptions ??= {};
1412
92
  const watchOptions = options.watchOptions;
1413
93
  const ignored = watchOptions.ignored;
@@ -1433,7 +113,7 @@ var AnimusWebpackPlugin = class {
1433
113
  for (const [key, value] of Object.entries(rawAlias)) {
1434
114
  const target = Array.isArray(value) ? value[0] : value;
1435
115
  if (typeof target !== "string") continue;
1436
- if (key === ".animus/styles.css") continue;
116
+ if (key === _animus_ui_extract_session.ANIMUS_CSS_MODULE_ID) continue;
1437
117
  pairs.push({
1438
118
  pattern: key,
1439
119
  target
@@ -1476,48 +156,48 @@ var AnimusWebpackPlugin = class {
1476
156
  stage
1477
157
  }, () => {
1478
158
  registerSystemDependencies();
1479
- const css = require_singleton.getSharedCss();
159
+ const css = (0, _animus_ui_extract_session.getSharedCss)();
1480
160
  if (!css || !RawSource) return;
1481
- const cssPath = require_session_paths.stylesPath(require_session_paths.sessionArtifactDir(this.session.rootDir || compiler.context, this.session.sessionId));
161
+ const cssPath = (0, _animus_ui_extract_session.stylesPath)((0, _animus_ui_extract_session.sessionArtifactDir)(this.session.rootDir || compiler.context, this.session.sessionId));
1482
162
  if (compilation.getAsset(cssPath)) {
1483
163
  compilation.updateAsset(cssPath, new RawSource(css));
1484
164
  return;
1485
165
  }
1486
- if (compilation.getAsset(".animus/styles.css")) compilation.updateAsset(ANIMUS_CSS_MODULE_ID, new RawSource(css));
166
+ if (compilation.getAsset(_animus_ui_extract_session.ANIMUS_CSS_MODULE_ID)) compilation.updateAsset(_animus_ui_extract_session.ANIMUS_CSS_MODULE_ID, new RawSource(css));
1487
167
  });
1488
168
  });
1489
169
  compiler.hooks.run.tapPromise(PLUGIN_NAME, async (_compiler) => {
1490
- this.session.rootDir = _compiler.context;
170
+ this.adoptCompilerContext(_compiler);
1491
171
  this.extractAliases(_compiler);
1492
- const existing = require_singleton.getAnalysisPromise();
172
+ const existing = (0, _animus_ui_extract_session.getAnalysisPromise)();
1493
173
  if (existing) {
1494
174
  await existing;
1495
175
  return;
1496
176
  }
1497
177
  const promise = this.session.runFullPipeline();
1498
- require_singleton.setAnalysisPromise(promise);
178
+ (0, _animus_ui_extract_session.setAnalysisPromise)(promise);
1499
179
  await promise;
1500
180
  });
1501
181
  compiler.hooks.watchRun.tapPromise(PLUGIN_NAME, async (_compiler) => {
1502
- this.session.rootDir = _compiler.context;
182
+ this.adoptCompilerContext(_compiler);
1503
183
  this.extractAliases(_compiler);
1504
184
  if (!this.initialized) {
1505
- const existing = require_singleton.getAnalysisPromise();
185
+ const existing = (0, _animus_ui_extract_session.getAnalysisPromise)();
1506
186
  if (existing) await existing;
1507
187
  else {
1508
188
  const promise = this.session.runFullPipeline();
1509
- require_singleton.setAnalysisPromise(promise);
189
+ (0, _animus_ui_extract_session.setAnalysisPromise)(promise);
1510
190
  await promise;
1511
191
  }
1512
192
  this.initialized = true;
1513
- this.lastBuiltEpoch = require_singleton.getReplacementEpoch();
193
+ this.lastBuiltEpoch = (0, _animus_ui_extract_session.getReplacementEpoch)();
1514
194
  return;
1515
195
  }
1516
196
  await this.session.handleWatchUpdate({
1517
197
  modifiedFiles: _compiler.modifiedFiles,
1518
198
  removedFiles: _compiler.removedFiles
1519
199
  });
1520
- const epoch = require_singleton.getReplacementEpoch();
200
+ const epoch = (0, _animus_ui_extract_session.getReplacementEpoch)();
1521
201
  this.epochMovedForNextCompilation = this.lastBuiltEpoch !== null && epoch !== null && epoch !== this.lastBuiltEpoch;
1522
202
  if (epoch !== null) this.lastBuiltEpoch = epoch;
1523
203
  });
@@ -1534,11 +214,34 @@ var AnimusWebpackPlugin = class {
1534
214
  return this.session.sessionId;
1535
215
  }
1536
216
  /** Set the project root at CONFIG time (with-animus) — the ONE root all
1537
- * session-path derivations use. The compiler hooks re-assert it from
1538
- * compiler.context (identical when compiler.context === cwd). */
217
+ * session-path derivations use. The compiler hooks ADOPT
218
+ * compiler.context only when no root was published (bare-apply
219
+ * harnesses); they never overwrite this. */
1539
220
  setRootDir(rootDir) {
1540
221
  this.session.rootDir = rootDir;
1541
222
  }
223
+ /** One warning per plugin instance for a context/root mismatch. */
224
+ warnedRootDivergence = false;
225
+ /** The run/watchRun taps' root handling: adopt compiler.context when no
226
+ * config-time root exists; otherwise KEEP the configured root — every
227
+ * frozen config-time derivation (stub path, css alias target,
228
+ * virtual:animus/system-props target, watch-ignore) came from it, and a
229
+ * silent overwrite re-keys sessionDir so the pipeline publishes
230
+ * artifacts where none of those consumers look. A disagreement is
231
+ * surfaced loudly instead (with-animus derives the root from Next's own
232
+ * `dir`, which equals compiler.context in real Next runs — a mismatch
233
+ * means a custom-webpack setup wired the plugin differently). */
234
+ adoptCompilerContext(compiler) {
235
+ const context = compiler.context;
236
+ if (!this.session.rootDir) {
237
+ this.session.rootDir = context;
238
+ return;
239
+ }
240
+ if (context && this.session.rootDir !== context && !this.warnedRootDivergence) {
241
+ this.warnedRootDivergence = true;
242
+ console.warn(`[animus-extract] compiler.context (${context}) differs from the configured project root (${this.session.rootDir}); keeping the configured root — run next against the app directory (or align your custom webpack context) so both agree`);
243
+ }
244
+ }
1542
245
  /** This session's artifact directory, derived from the one root. */
1543
246
  get sessionDir() {
1544
247
  return this.session.sessionDir;
@@ -1550,25 +253,23 @@ var AnimusWebpackPlugin = class {
1550
253
  /** Expose external package directories for webpack loader allowlisting.
1551
254
  * Falls back to shared globalThis state for non-owning compiler instances. */
1552
255
  getExternalPackageDirs() {
1553
- return this.session.externalPackageDirs.length > 0 ? this.session.externalPackageDirs : require_singleton.getSharedExternalDirs();
256
+ return this.session.externalPackageDirs.length > 0 ? this.session.externalPackageDirs : (0, _animus_ui_extract_session.getSharedExternalDirs)();
1554
257
  }
1555
258
  /** Expose external package source entries for webpack resolve alias.
1556
259
  * Falls back to shared globalThis state for non-owning compiler instances. */
1557
260
  getExternalSourceEntries() {
1558
- return this.session.externalSourceEntries.size > 0 ? this.session.externalSourceEntries : require_singleton.getSharedExternalEntries();
261
+ return this.session.externalSourceEntries.size > 0 ? this.session.externalSourceEntries : (0, _animus_ui_extract_session.getSharedExternalEntries)();
1559
262
  }
1560
263
  };
1561
264
  //#endregion
1562
265
  //#region src/turbopack-config.ts
1563
- /** The single glob the Animus loader registers under. `.mjs` is included
1564
- * for webpack parity: an external package without `src/` is ingested via
1565
- * its resolved dist entry (often `dist/index.mjs`) and must still reach
1566
- * the loader — manifest lookup remains the file-level gate. */
1567
- const ANIMUS_TURBOPACK_RULE_GLOB = "*.{ts,tsx,js,jsx,mjs}";
1568
- /** Virtual system-props id emitted into transformed sources under Turbopack
1569
- * (absolute-path imports are rejected there); resolveAlias maps it to the
1570
- * on-disk artifact. */
1571
- const TURBOPACK_SYSTEM_PROPS_ID = "virtual:animus/system-props";
266
+ /** The single glob the Animus loader registers under — derived from the
267
+ * ONE shared engine extension set (also the unplugin transform gate's
268
+ * source), so the two bundler families cannot drift. `.mjs` is in that
269
+ * set for webpack parity: an external package without `src/` is ingested
270
+ * via its resolved dist entry (often `dist/index.mjs`) and must still
271
+ * reach the loader — manifest lookup remains the file-level gate. */
272
+ const ANIMUS_TURBOPACK_RULE_GLOB = `*.{${_animus_ui_extract_pipeline.ENGINE_TRANSFORM_EXTENSIONS.join(",")}}`;
1572
273
  /**
1573
274
  * Resolve whether Turbopack wiring is active for this process. Default is
1574
275
  * `'auto'`: active exactly when the TURBOPACK environment variable is set
@@ -1606,8 +307,8 @@ function buildTurbopackConfig(args) {
1606
307
  ...options.cssImportTarget !== void 0 ? { cssImportTarget: options.cssImportTarget } : {}
1607
308
  };
1608
309
  const resolveAlias = {
1609
- [TURBOPACK_SYSTEM_PROPS_ID]: rootRelativeRequest(rootDir, (0, path.join)(sessionDir, require_session_paths.SYSTEM_PROPS_ARTIFACT)),
1610
- ".animus/styles.css": rootRelativeRequest(rootDir, (0, path.join)(sessionDir, require_session_paths.STYLES_ARTIFACT))
310
+ [_animus_ui_extract_session.TURBOPACK_SYSTEM_PROPS_ID]: rootRelativeRequest(rootDir, (0, path.join)(sessionDir, _animus_ui_extract_session.SYSTEM_PROPS_ARTIFACT)),
311
+ ".animus/styles.css": rootRelativeRequest(rootDir, (0, path.join)(sessionDir, _animus_ui_extract_session.STYLES_ARTIFACT))
1611
312
  };
1612
313
  for (const [specifier, srcEntry] of externalSourceEntries) resolveAlias[specifier] = rootRelativeRequest(rootDir, srcEntry);
1613
314
  return {
@@ -1629,211 +330,6 @@ function resolveTurbopackLoaderPath(pluginDir) {
1629
330
  return resolveLoaderPath(pluginDir, ["turbopack-loader.cjs", "turbopack-loader.mjs"], "turbopack-loader.ts");
1630
331
  }
1631
332
  //#endregion
1632
- //#region src/turbopack-orchestrator.ts
1633
- /**
1634
- * Out-of-band pipeline orchestration for Turbopack (spec:
1635
- * next-turbopack-integration). Turbopack exposes no compiler hooks, so the
1636
- * pipeline runs during next.config resolution and, in dev, from a
1637
- * filesystem watcher — the bundler only ever sees the `.animus/` disk
1638
- * artifacts.
1639
- */
1640
- /** Run the full pipeline with the Turbopack emitter identity: the virtual
1641
- * system-props id (Turbopack rejects the webpack path's absolute-path
1642
- * imports). Turbopack orchestration persists the analysis-inputs
1643
- * hydration corpus — its isolated loader workers replay it (spec:
1644
- * next-turbopack-integration, "Manifest disk artifact"; webpack mode
1645
- * skips the corpus). */
1646
- async function runTurbopackPipeline(session) {
1647
- session.systemPropsModuleId = TURBOPACK_SYSTEM_PROPS_ID;
1648
- session.persistAnalysisInputs = true;
1649
- await session.runFullPipeline();
1650
- }
1651
- const activeWatcherRoots = /* @__PURE__ */ new Set();
1652
- const IGNORED_SEGMENTS = new Set([
1653
- ".animus",
1654
- ".next",
1655
- "node_modules"
1656
- ]);
1657
- /**
1658
- * Start the dev watcher: fs.watch per eligible top-level directory (plus a
1659
- * non-recursive watch on the root itself), debounced into
1660
- * existence-partitioned modified/removed sets feeding
1661
- * `session.handleWatchUpdate` (serialized — updates never overlap).
1662
- * Generated and vendored trees (`.animus`, `.next`, node_modules) are never
1663
- * registered with the OS — recursive registration of those trees exhausts
1664
- * inotify/kqueue descriptors (EMFILE/ENOSPC) on large projects.
1665
- * Idempotent per project root; unref'd so it never holds the process open.
1666
- * Asynchronous FSWatcher errors degrade to no-watch with a warning instead
1667
- * of crashing the dev server. Returns a close handle, or null when this
1668
- * root is already watched or the platform lacks recursive fs.watch (Linux
1669
- * before Node 20 — degrades to no-watch with a warning).
1670
- */
1671
- function startTurbopackWatcher(session, rootDir, debounceMs = 75, watchFn = fs.watch) {
1672
- if (activeWatcherRoots.has(rootDir)) return null;
1673
- activeWatcherRoots.add(rootDir);
1674
- if (typeof session.noteDebouncedWatchEvents === "function") session.debounceCeilingMs = debounceMs;
1675
- const pendingPaths = /* @__PURE__ */ new Set();
1676
- const watchers = /* @__PURE__ */ new Map();
1677
- let timer = null;
1678
- let updateChain = Promise.resolve();
1679
- let closed = false;
1680
- const externalWatchers = /* @__PURE__ */ new Map();
1681
- let pendingOpened = /* @__PURE__ */ new Map();
1682
- let capturedDuringSnapshot = [];
1683
- const externalDiagnosticKey = (root) => `external-watch:${root}`;
1684
- const failureReason = (err) => {
1685
- const code = err?.code;
1686
- if (code === "EMFILE" || code === "ENFILE" || code === "ENOSPC" || code === "EPERM") return `capacity(${code})`;
1687
- if (/inotify|too many/i.test(String(err))) return "capacity";
1688
- return code ?? "error";
1689
- };
1690
- const degradeExternalRoot = (root, err) => {
1691
- externalWatchers.get(root)?.close();
1692
- externalWatchers.delete(root);
1693
- pendingOpened.get(root)?.close();
1694
- pendingOpened.delete(root);
1695
- const message = `ANIMUS_EXTERNAL_WATCH_UNAVAILABLE root=${(0, path.relative)(rootDir, root)} reason=${failureReason(err)} effect=changes in this workspace source may require restart`;
1696
- session.stickyDiagnostics?.set(externalDiagnosticKey(root), message);
1697
- console.warn(`[animus-extract] ${message}`);
1698
- };
1699
- const flush = () => {
1700
- timer = null;
1701
- const modifiedFiles = /* @__PURE__ */ new Set();
1702
- const removedFiles = /* @__PURE__ */ new Set();
1703
- for (const path$1 of pendingPaths) ((0, fs.existsSync)(path$1) ? modifiedFiles : removedFiles).add(path$1);
1704
- pendingPaths.clear();
1705
- updateChain = updateChain.then(() => session.handleWatchUpdate({
1706
- modifiedFiles,
1707
- removedFiles
1708
- }).catch((err) => {
1709
- console.warn(`[animus-extract] Turbopack watch update failed: ${String(err)}`);
1710
- }).then(() => {
1711
- if (pendingOpened.size > 0 || capturedDuringSnapshot.length > 0) rollbackPendingExternal();
1712
- }));
1713
- };
1714
- const rollbackPendingExternal = () => {
1715
- for (const watcher of pendingOpened.values()) watcher.close();
1716
- pendingOpened = /* @__PURE__ */ new Map();
1717
- capturedDuringSnapshot = [];
1718
- };
1719
- const closeAll = () => {
1720
- if (closed) return;
1721
- closed = true;
1722
- if (timer) clearTimeout(timer);
1723
- timer = null;
1724
- for (const watcher of watchers.values()) watcher.close();
1725
- watchers.clear();
1726
- for (const watcher of externalWatchers.values()) watcher.close();
1727
- externalWatchers.clear();
1728
- rollbackPendingExternal();
1729
- session.onExternalRootResolved = null;
1730
- session.onExternalRootsCommitted = null;
1731
- activeWatcherRoots.delete(rootDir);
1732
- };
1733
- const onWatcherError = (err) => {
1734
- closeAll();
1735
- console.warn(`[animus-extract] Turbopack dev watcher failed (${String(err)}); source edits require a dev-server restart`);
1736
- };
1737
- const enqueuePath = (abs) => {
1738
- pendingPaths.add(abs);
1739
- try {
1740
- session.noteDebouncedWatchEvents?.([abs]);
1741
- } catch (err) {
1742
- console.warn(`[animus-extract] Turbopack debounce status update failed: ${String(err)}`);
1743
- }
1744
- if (!timer) {
1745
- timer = setTimeout(flush, debounceMs);
1746
- timer.unref?.();
1747
- }
1748
- };
1749
- const onEvent = (baseDir, filename) => {
1750
- if (closed || !filename) return;
1751
- const rel = filename.toString();
1752
- if (rel.split(/[\\/]/).some((segment) => IGNORED_SEGMENTS.has(segment))) return;
1753
- const abs = (0, path.join)(baseDir, rel);
1754
- if (baseDir === rootDir && !watchers.has(abs)) try {
1755
- if ((0, fs.statSync)(abs).isDirectory()) addWatcher(abs, true);
1756
- } catch {}
1757
- enqueuePath(abs);
1758
- };
1759
- /** External-root event path: fenced by watcher identity ({watcherRootId,
1760
- * generation} — a handle that is neither current nor pending belongs to
1761
- * a retired generation and its events are rejected), `filename == null`
1762
- * marks the ROOT dirty for rediscovery, and events observed while the
1763
- * root's generation is still snapshotting are buffered for replay. */
1764
- const onExternalEvent = (root, self, filename) => {
1765
- if (closed) return;
1766
- const watcher = self();
1767
- const active = watcher !== void 0 && externalWatchers.get(root) === watcher;
1768
- const isPending = watcher !== void 0 && pendingOpened.get(root) === watcher;
1769
- if (!active && !isPending) return;
1770
- let abs;
1771
- if (filename == null) abs = root;
1772
- else {
1773
- const rel = filename.toString();
1774
- if (rel.split(/[\\/]/).some((segment) => IGNORED_SEGMENTS.has(segment))) return;
1775
- abs = rel === "" ? root : (0, path.join)(root, rel);
1776
- }
1777
- if (isPending && !active) {
1778
- capturedDuringSnapshot.push(abs);
1779
- return;
1780
- }
1781
- enqueuePath(abs);
1782
- };
1783
- /** Open one external-root watcher into `into` (the committed set at
1784
- * startup, the pending set during reset reconciliation). Failures
1785
- * degrade ONLY this root (design D7); node_modules-resident roots are
1786
- * documented unwatchable and never registered (guardrail G2). */
1787
- const openExternalWatcher = (root, into) => {
1788
- if (root.split(/[\\/]/).includes("node_modules")) return;
1789
- try {
1790
- let watcher;
1791
- watcher = watchFn(root, { recursive: true }, (_event, filename) => onExternalEvent(root, () => watcher, filename));
1792
- watcher.on("error", (err) => degradeExternalRoot(root, err));
1793
- watcher.unref?.();
1794
- into.set(root, watcher);
1795
- session.stickyDiagnostics?.delete(externalDiagnosticKey(root));
1796
- } catch (err) {
1797
- degradeExternalRoot(root, err);
1798
- }
1799
- };
1800
- const addWatcher = (dir, recursive) => {
1801
- const watcher = watchFn(dir, { recursive }, (_event, filename) => onEvent(dir, filename));
1802
- watcher.on("error", onWatcherError);
1803
- watcher.unref?.();
1804
- watchers.set(dir, watcher);
1805
- };
1806
- try {
1807
- addWatcher(rootDir, false);
1808
- for (const entry of (0, fs.readdirSync)(rootDir, { withFileTypes: true })) {
1809
- if (!entry.isDirectory() || IGNORED_SEGMENTS.has(entry.name)) continue;
1810
- addWatcher((0, path.join)(rootDir, entry.name), true);
1811
- }
1812
- } catch (err) {
1813
- closeAll();
1814
- console.warn(`[animus-extract] Turbopack dev watcher unavailable (${String(err)}); source edits require a dev-server restart`);
1815
- return null;
1816
- }
1817
- for (const root of session.externalWatchRoots ?? []) openExternalWatcher(root, externalWatchers);
1818
- session.onExternalRootResolved = (root) => {
1819
- if (closed) return;
1820
- if (externalWatchers.has(root) || pendingOpened.has(root)) return;
1821
- openExternalWatcher(root, pendingOpened);
1822
- };
1823
- session.onExternalRootsCommitted = (roots) => {
1824
- if (closed) return;
1825
- const admitted = new Set(roots);
1826
- for (const [root, watcher] of pendingOpened) externalWatchers.set(root, watcher);
1827
- pendingOpened = /* @__PURE__ */ new Map();
1828
- for (const [root, watcher] of [...externalWatchers]) if (!admitted.has(root)) {
1829
- watcher.close();
1830
- externalWatchers.delete(root);
1831
- }
1832
- for (const abs of capturedDuringSnapshot.splice(0)) enqueuePath(abs);
1833
- };
1834
- return { close: closeAll };
1835
- }
1836
- //#endregion
1837
333
  //#region src/with-animus.ts
1838
334
  const __dirname$1 = (0, path.dirname)((0, url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
1839
335
  let warnedGitignore = false;
@@ -1849,6 +345,18 @@ let warnedUnstableTurbopack = false;
1849
345
  */
1850
346
  function withAnimus(options) {
1851
347
  if (!options.system) throw new Error("[animus-extract] Missing required option `system`. Provide the path to your SystemInstance module: withAnimus({ system: \"./src/ds.ts\" })");
348
+ (0, _animus_ui_extract_pipeline.assertKnownOptionKeys)(options, [
349
+ "cssImportTarget",
350
+ "turbopack",
351
+ "unstable_turbopack",
352
+ "loaderPath"
353
+ ], [{
354
+ key: "root",
355
+ reason: "the Next driver's root is Next's `dir` at config time"
356
+ }], {
357
+ onUnknownKey: "warn",
358
+ warn: (message) => console.warn(`[animus-extract] ${message}`)
359
+ });
1852
360
  if (options.unstable_turbopack && !options.turbopack && !warnedUnstableTurbopack) {
1853
361
  warnedUnstableTurbopack = true;
1854
362
  console.warn("[animus-extract] `unstable_turbopack` is deprecated — rename it to `turbopack` (same shape)");
@@ -1860,12 +368,12 @@ function withAnimus(options) {
1860
368
  ...nextConfig,
1861
369
  webpack(config, context) {
1862
370
  if (typeof existingWebpack === "function") config = existingWebpack(config, context);
1863
- const rootDir = process.cwd();
371
+ const rootDir = typeof context.dir === "string" && context.dir.length > 0 ? context.dir : process.cwd();
1864
372
  const plugin = new AnimusWebpackPlugin(options);
1865
373
  plugin.setRootDir(rootDir);
1866
374
  const sessionDir = plugin.sessionDir;
1867
375
  if (!(0, fs.existsSync)(sessionDir)) (0, fs.mkdirSync)(sessionDir, { recursive: true });
1868
- const stubCssPath = require_session_paths.stylesPath(sessionDir);
376
+ const stubCssPath = (0, _animus_ui_extract_session.stylesPath)(sessionDir);
1869
377
  if (!(0, fs.existsSync)(stubCssPath)) {
1870
378
  const { declaration } = (0, _animus_ui_extract_pipeline.assembleStylesheet)({
1871
379
  layers: options.layers,
@@ -1888,16 +396,16 @@ function withAnimus(options) {
1888
396
  config.plugins = config.plugins || [];
1889
397
  config.plugins.push(plugin);
1890
398
  const { DefinePlugin } = context.webpack ?? {};
1891
- if (typeof DefinePlugin === "function") config.plugins.push(new DefinePlugin({ __ANIMUS_DEV__: JSON.stringify(context.dev === true) }));
399
+ if (typeof DefinePlugin === "function") config.plugins.push(new DefinePlugin({ __ANIMUS_DEV__: JSON.stringify((0, _animus_ui_extract_pipeline.resolveMode)(options.mode, () => context.dev === true ? "development" : "production").mode === "development") }));
1892
400
  config.resolve = config.resolve || {};
1893
401
  config.resolve.alias = config.resolve.alias || {};
1894
- config.resolve.alias[ANIMUS_CSS_MODULE_ID] = require_session_paths.stylesPath(sessionDir);
1895
- const sessionSystemPropsPath = require_session_paths.systemPropsPath(sessionDir);
402
+ config.resolve.alias[_animus_ui_extract_session.ANIMUS_CSS_MODULE_ID] = (0, _animus_ui_extract_session.stylesPath)(sessionDir);
403
+ const sessionSystemPropsPath = (0, _animus_ui_extract_session.systemPropsPath)(sessionDir);
1896
404
  config.plugins.push({ apply(compiler) {
1897
405
  compiler.hooks.normalModuleFactory.tap("AnimusVirtualResolve", (nmf) => {
1898
406
  nmf.hooks.beforeResolve.tap("AnimusVirtualResolve", (resolveData) => {
1899
407
  if (resolveData.request === "virtual:animus/system-props") resolveData.request = sessionSystemPropsPath;
1900
- if (resolveData.request === "virtual:animus/styles.css") resolveData.request = require_session_paths.stylesPath(sessionDir);
408
+ if (resolveData.request === "virtual:animus/styles.css") resolveData.request = (0, _animus_ui_extract_session.stylesPath)(sessionDir);
1901
409
  const srcEntry = plugin.getExternalSourceEntries().get(resolveData.request);
1902
410
  if (srcEntry) resolveData.request = srcEntry;
1903
411
  });
@@ -1937,12 +445,12 @@ function withAnimus(options) {
1937
445
  */
1938
446
  async function wireTurbopack(nextConfig, options) {
1939
447
  const rootDir = process.cwd();
1940
- const session = new ExtractionSession(options);
448
+ const session = new _animus_ui_extract_session.ExtractionSession(options);
1941
449
  session.rootDir = rootDir;
1942
450
  const builtAliases = (0, _animus_ui_extract_pipeline.buildPathAliasesJson)((0, _animus_ui_extract_pipeline.readTsconfigAliasPairs)(rootDir), rootDir);
1943
451
  if (builtAliases) session.pathAliasesJson = builtAliases.json;
1944
- await runTurbopackPipeline(session);
1945
- if (process.env.NODE_ENV === "development") startTurbopackWatcher(session, rootDir);
452
+ await (0, _animus_ui_extract_session.runSessionPipeline)(session);
453
+ if (process.env.NODE_ENV === "development") (0, _animus_ui_extract_session.startTurbopackWatcher)(session, rootDir);
1946
454
  const fragment = buildTurbopackConfig({
1947
455
  rootDir,
1948
456
  loaderPath: resolveTurbopackLoaderPath(__dirname$1),
@@ -1952,7 +460,7 @@ async function wireTurbopack(nextConfig, options) {
1952
460
  sessionDir: session.sessionDir
1953
461
  });
1954
462
  const existing = nextConfig.turbopack ?? {};
1955
- if (existing.rules && "*.{ts,tsx,js,jsx,mjs}" in existing.rules) throw new Error(`[animus-extract] turbopack.rules['${ANIMUS_TURBOPACK_RULE_GLOB}'] is already configured — remove the consumer rule or disable unstable_turbopack`);
463
+ if (existing.rules && ANIMUS_TURBOPACK_RULE_GLOB in existing.rules) throw new Error(`[animus-extract] turbopack.rules['${ANIMUS_TURBOPACK_RULE_GLOB}'] is already configured — remove the consumer rule or disable unstable_turbopack`);
1956
464
  return {
1957
465
  ...nextConfig,
1958
466
  turbopack: {