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