@animus-ui/next-plugin 0.1.1 → 0.1.3

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 (42) hide show
  1. package/README.md +83 -4
  2. package/dist/chunk-y_GBKt04.mjs +5 -0
  3. package/dist/extraction-session.d.ts +138 -0
  4. package/dist/extraction-session.d.ts.map +1 -0
  5. package/dist/index.cjs +766 -540
  6. package/dist/index.mjs +766 -539
  7. package/dist/loader-core-CbaYNhBu.cjs +71 -0
  8. package/dist/loader-core-D6Tm_b8F.mjs +66 -0
  9. package/dist/loader-core.d.ts +26 -0
  10. package/dist/loader-core.d.ts.map +1 -0
  11. package/dist/loader.cjs +12 -43
  12. package/dist/loader.d.ts +5 -9
  13. package/dist/loader.d.ts.map +1 -1
  14. package/dist/loader.mjs +12 -43
  15. package/dist/plugin.d.ts +24 -42
  16. package/dist/plugin.d.ts.map +1 -1
  17. package/dist/resolve-packages.d.ts +8 -0
  18. package/dist/resolve-packages.d.ts.map +1 -0
  19. package/dist/singleton-BKALu7Mk.mjs +108 -0
  20. package/dist/{singleton-CttvA4Rg.cjs → singleton-DH7yeWZa.cjs} +29 -60
  21. package/dist/singleton.d.ts +1 -8
  22. package/dist/singleton.d.ts.map +1 -1
  23. package/dist/timing.d.ts +7 -0
  24. package/dist/timing.d.ts.map +1 -0
  25. package/dist/turbopack-config.d.ts +55 -0
  26. package/dist/turbopack-config.d.ts.map +1 -0
  27. package/dist/turbopack-loader.cjs +90 -0
  28. package/dist/turbopack-loader.d.ts +16 -0
  29. package/dist/turbopack-loader.d.ts.map +1 -0
  30. package/dist/turbopack-loader.mjs +91 -0
  31. package/dist/turbopack-orchestrator.d.ts +31 -0
  32. package/dist/turbopack-orchestrator.d.ts.map +1 -0
  33. package/dist/types.d.ts +67 -10
  34. package/dist/types.d.ts.map +1 -1
  35. package/dist/with-animus.d.ts +1 -1
  36. package/dist/with-animus.d.ts.map +1 -1
  37. package/package.json +5 -5
  38. package/dist/analyze-project-args.d.ts +0 -33
  39. package/dist/analyze-project-args.d.ts.map +0 -1
  40. package/dist/manifest-diagnostics.d.ts +0 -10
  41. package/dist/manifest-diagnostics.d.ts.map +0 -1
  42. package/dist/singleton-CmwFBCJ5.mjs +0 -136
package/dist/index.mjs CHANGED
@@ -1,67 +1,161 @@
1
- import { a as getSharedEngine, c as resetAnalysisPromise, d as setSharedCss, f as setSharedEngine, g as __require, h as setSharedSystemProps, i as getSharedCss, l as setAnalysisPromise, m as setSharedExternalEntries, n as getAnalysisPromise, o as getSharedExternalDirs, p as setSharedExternalDirs, s as getSharedExternalEntries, t as engineApi, u as setManifestJson } from "./singleton-CmwFBCJ5.mjs";
2
- import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
1
+ import { t as __require } from "./chunk-y_GBKt04.mjs";
2
+ import { a as getSharedExternalDirs, c as setAnalysisPromise, d as setSharedEngine, f as setSharedExternalDirs, i as getSharedCss, l as setManifestJson, m as setSharedSystemProps, n as getAnalysisPromise, o as getSharedExternalEntries, p as setSharedExternalEntries, s as resetAnalysisPromise, t as engineApi, u as setSharedCss } from "./singleton-BKALu7Mk.mjs";
3
+ import { DEFAULT_EXTENSIONS, assembleStylesheet, assertNoRetiredEngineSelection, buildPathAliasesJson, buildSystemPropsModule, clearEngineCache, collectExternalPackageSources, contentHash, discoverFiles, extractSystemFilePackages, formatRustTimingWaterfall, loadSystemConfig, postProcessCss, preprocessMdx, readTsconfigAliasPairs, resolveLightningTargets, runProjectAnalysis, serializeStaticCss, toWatchKeys, unresolvableIncludesMessage } from "@animus-ui/extract/pipeline";
4
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, watch, writeFileSync } from "fs";
3
5
  import { dirname, extname, join, relative, resolve, sep } from "path";
4
6
  import { fileURLToPath } from "url";
5
- import { DEFAULT_EXTENSIONS, applyPrefix, applyUnitFallback, assembleStylesheet, extractSystemFilePackages, preprocessMdx } from "@animus-ui/extract/pipeline";
6
- import { createHash } from "crypto";
7
- //#region src/analyze-project-args.ts
8
- function buildAnalyzeProjectArgs(inputs, devMode) {
9
- return [
10
- inputs.filesJson,
11
- inputs.scalesJson,
12
- inputs.variableMapJson,
13
- inputs.contextualVarsJson,
14
- inputs.propConfigJson,
15
- inputs.groupRegistryJson,
16
- inputs.packageResolutionJson,
17
- devMode,
18
- inputs.emitterConfigJson,
19
- inputs.selectorAliasesJson,
20
- null,
21
- inputs.globalStyleBlocksJson,
22
- inputs.pathAliasesJson,
23
- inputs.keyframesJson
24
- ];
25
- }
26
- function buildProductionAnalyzeProjectArgs(inputs) {
27
- return buildAnalyzeProjectArgs(inputs, false);
28
- }
29
- function buildHmrAnalyzeProjectArgs(inputs) {
30
- return buildAnalyzeProjectArgs(inputs, true);
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;
31
45
  }
32
46
  //#endregion
33
- //#region src/manifest-diagnostics.ts
34
- function surfaceManifestDiagnostics(manifest, warn) {
35
- for (const diagnostic of manifest.diagnostics ?? []) {
36
- if (diagnostic.kind !== "warn") continue;
37
- warn(`⚠ ${diagnostic.file}: ${diagnostic.component}: ${diagnostic.message}`);
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)}`);
38
87
  }
39
88
  }
40
89
  //#endregion
41
- //#region src/plugin.ts
42
- const PLUGIN_NAME = "AnimusWebpackPlugin";
43
- var AnimusWebpackPlugin = class {
44
- options;
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
+ /** Default path fragments excluded from source discovery (full + watch). */
98
+ const DEFAULT_EXCLUDE = [
99
+ "node_modules",
100
+ "dist",
101
+ ".test.",
102
+ ".spec.",
103
+ ".next",
104
+ ".animus"
105
+ ];
106
+ /**
107
+ * Bundler-agnostic extraction pipeline: system loading, source discovery
108
+ * and ingestion, external-package collection, analysis, stylesheet and
109
+ * system-props emission, and watch-cycle diffing.
110
+ *
111
+ * The webpack adapter (plugin.ts) owns only bundler wiring: hook
112
+ * registration, alias harvesting from the compiler config, in-memory asset
113
+ * replacement, and translating watch events into `handleWatchUpdate`
114
+ * change sets. Keeping this class free of webpack types is deliberate — a
115
+ * future Turbopack integration must drive the same session from outside
116
+ * the bundler (Turbopack has no compiler-hook surface), reusing everything
117
+ * here unchanged.
118
+ *
119
+ * Outputs are published through the package singleton (shared CSS,
120
+ * manifest, system props) and written to `.animus/` on disk.
121
+ */
122
+ var ExtractionSession = class {
123
+ /** Set by the adapter before any pipeline run. */
45
124
  rootDir = null;
46
- configJson = "";
47
- groupRegistryJson = "";
48
- themeJson = "";
49
- variableMapJson = "";
50
- variableCss = "";
51
- contextualVarsJson = null;
52
- selectorAliasesJson = null;
53
- globalCss = "";
54
- globalStyleBlocksJson = null;
55
- keyframesBlocksJson = null;
125
+ /** Serialized path aliases, harvested by the adapter from bundler config. */
56
126
  pathAliasesJson = null;
127
+ /** When set (Turbopack orchestration), every analysis also persists
128
+ * `.animus/analysis-inputs.json` so isolated loader workers can hydrate.
129
+ * Webpack mode leaves this off — its loader shares the process. */
130
+ persistAnalysisInputs = false;
131
+ /** Emitter identity override for the system-props module id. Webpack mode
132
+ * (null) injects the absolute `.animus/system-props.js` path, resolved by
133
+ * NormalModuleReplacement; Turbopack rejects absolute-path imports, so
134
+ * its driver sets the virtual id that `resolveAlias` maps to disk. */
135
+ systemPropsModuleId = null;
136
+ /** Absolute directory prefixes for external DS packages (loader allowlisting). */
137
+ externalPackageDirs = [];
138
+ /** External package specifier → absolute source entry path. */
139
+ externalSourceEntries = /* @__PURE__ */ new Map();
140
+ options;
141
+ staticCssJson;
142
+ system = null;
143
+ /** Full package-resolution map from the last full pipeline — replayed by
144
+ * incremental passes (sourceEntries alone omits dist-resolved packages). */
145
+ lastPackageMap = {};
57
146
  fileCache = /* @__PURE__ */ new Map();
147
+ systemDependencyKeys = /* @__PURE__ */ new Set();
148
+ /** Loader-reported dependency paths for bundler watch registration. */
149
+ systemDependencyPaths = [];
150
+ systemDependencyHashes = /* @__PURE__ */ new Map();
58
151
  lastCssHash = null;
59
152
  lastSystemPropsHash = null;
60
- externalPackageDirs = [];
61
- externalSourceEntries = /* @__PURE__ */ new Map();
153
+ lastManifestHash = null;
154
+ lastAnalysisInputsHash = null;
155
+ lcssTargets = null;
62
156
  constructor(options) {
63
157
  this.options = options;
64
- setSharedEngine(options.engine ?? "v2");
158
+ this.staticCssJson = serializeStaticCss(options.staticCss);
65
159
  }
66
160
  get verbose() {
67
161
  return this.options.verbose === true || process.env.ANIMUS_DEBUG === "1" || process.env.ANIMUS_DEBUG === "true";
@@ -78,176 +172,82 @@ var AnimusWebpackPlugin = class {
78
172
  elapsed(t) {
79
173
  return this.verbose ? Math.round(performance.now() - t) : 0;
80
174
  }
81
- logBuildTimings(bt, rustTiming) {
82
- if (!this.verbose) return;
83
- for (const [key, label] of [
84
- ["systemLoad", "system-load"],
85
- ["fileDiscovery", "file-discovery"],
86
- ["fileRead", "file-read+hash"],
87
- ["packageResolve", "pkg-resolve"],
88
- ["analysis", "analysis"]
89
- ]) {
90
- const ms = bt[key] ?? 0;
91
- if (ms === 0 && !Object.hasOwn(bt, key)) continue;
92
- const pad = " ".repeat(Math.max(0, 17 - label.length));
93
- this.log(` ${label}${pad}${String(ms).padStart(5)}ms`);
94
- if (key === "analysis") for (const [sk, sl] of [
95
- ["jsonSerialize", "json-serialize"],
96
- ["rustExtract", "rust-extract"],
97
- ["jsonParse", "json-parse"]
98
- ]) {
99
- const sms = bt[sk] ?? 0;
100
- const spad = " ".repeat(Math.max(0, 15 - sl.length));
101
- this.log(` ${sl}${spad}${String(sms).padStart(5)}ms`);
102
- if (sk === "rustExtract" && rustTiming) for (const [rk, rl] of [
103
- ["parseAndWalk", "parse+walk"],
104
- ["importResolution", "imports"],
105
- ["extensionProvenance", "provenance"],
106
- ["topologicalSort", "topo-sort"],
107
- ["chainEvaluation", "chains"],
108
- ["jsxScanning", "jsx-scan"],
109
- ["systemPropAggregation", "sys-props"],
110
- ["usageLedger", "usage"],
111
- ["reconciliation", "reconcile"],
112
- ["cssGeneration", "css-gen"],
113
- ["manifestSerialization", "serialize"]
114
- ]) {
115
- const rms = rustTiming[rk] ?? 0;
116
- const rpad = " ".repeat(Math.max(0, 13 - rl.length));
117
- const rextra = rk === "parseAndWalk" ? ` (${rustTiming.fileCount ?? 0} files, ${rustTiming.cacheHits ?? 0} cached)` : "";
118
- this.log(` ${rl}${rpad}${String(rms).padStart(5)}ms${rextra}`);
119
- }
120
- }
121
- }
122
- this.log(` total ${String(bt.total).padStart(5)}ms`);
123
- if (process.env.ANIMUS_TIMING_JSON === "1") {
124
- const merged = {};
125
- for (const [k, v] of Object.entries(bt)) merged[`buildStart.${k}`] = v;
126
- if (rustTiming) {
127
- for (const [k, v] of Object.entries(rustTiming)) if (typeof v === "number") merged[`rust.${k}`] = v;
128
- }
129
- console.info(`[animus:timing] ${JSON.stringify(merged)}`);
130
- }
131
- }
132
- initialized = false;
133
- /** Extract path aliases from webpack's resolve.alias config. */
134
- extractAliases(compiler) {
135
- const rootDir = compiler.context;
136
- const rawAlias = compiler.options?.resolve?.alias;
137
- if (!rawAlias || typeof rawAlias !== "object") return;
138
- const entries = [];
139
- for (const [key, value] of Object.entries(rawAlias)) {
140
- const target = Array.isArray(value) ? value[0] : value;
141
- if (typeof target !== "string") continue;
142
- if (key.includes(".animus")) continue;
143
- const replacement = target.startsWith(rootDir) ? target.slice(rootDir.length + 1) : target;
144
- if (/\.\w+$/.test(replacement)) entries.push({
145
- pattern: key,
146
- replacement,
147
- type: "exact"
148
- });
149
- else entries.push({
150
- pattern: key.endsWith("/") ? key : key + "/",
151
- replacement: replacement.endsWith("/") ? replacement : replacement + "/",
152
- type: "prefix"
153
- });
154
- }
155
- entries.sort((a, b) => b.pattern.length - a.pattern.length);
156
- if (entries.length > 0) this.pathAliasesJson = JSON.stringify({ aliases: entries });
157
- }
158
- apply(compiler) {
159
- if (compiler.options?.name === "edge-server") return;
160
- compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
161
- const stage = compiler.webpack?.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL ?? -2e3;
162
- const RawSource = compiler.webpack?.sources.RawSource;
163
- compilation.hooks.processAssets.tap({
164
- name: PLUGIN_NAME,
165
- stage
166
- }, () => {
167
- const css = getSharedCss();
168
- if (!css || !RawSource) return;
169
- const cssPath = join(this.rootDir || compiler.context, ".animus", "styles.css");
170
- if (compilation.getAsset(cssPath)) {
171
- compilation.updateAsset(cssPath, new RawSource(css));
172
- return;
173
- }
174
- const relPath = ".animus/styles.css";
175
- if (compilation.getAsset(relPath)) compilation.updateAsset(relPath, new RawSource(css));
176
- });
177
- });
178
- compiler.hooks.run.tapPromise(PLUGIN_NAME, async (_compiler) => {
179
- this.rootDir = _compiler.context;
180
- this.extractAliases(_compiler);
181
- const existing = getAnalysisPromise();
182
- if (existing) {
183
- await existing;
184
- return;
185
- }
186
- const promise = this.runFullPipeline();
187
- setAnalysisPromise(promise);
188
- await promise;
189
- });
190
- compiler.hooks.watchRun.tapPromise(PLUGIN_NAME, async (_compiler) => {
191
- this.rootDir = _compiler.context;
192
- this.extractAliases(_compiler);
193
- if (!this.initialized) {
194
- const existing = getAnalysisPromise();
195
- if (existing) {
196
- await existing;
197
- this.initialized = true;
198
- return;
199
- }
200
- const promise = this.runFullPipeline();
201
- setAnalysisPromise(promise);
202
- await promise;
203
- this.initialized = true;
204
- return;
205
- }
206
- await this.handleWatchUpdate();
207
- });
175
+ /** Resolve the scan configuration from options — the single source of the
176
+ * exclude/extension policy shared by the full and incremental pipelines. */
177
+ resolveScanConfig() {
178
+ const extensionsSet = new Set(this.options.extensions ?? DEFAULT_EXTENSIONS);
179
+ return {
180
+ excludePatterns: this.options.exclude ?? DEFAULT_EXCLUDE,
181
+ extensionsSet,
182
+ shouldHandleMdx: extensionsSet.has(".mdx")
183
+ };
208
184
  }
209
- async handleWatchUpdate() {
210
- if (this.configJson === "") return;
185
+ /**
186
+ * Incremental watch pass: geological reset when the system file changed,
187
+ * otherwise content-hash diffing restricted to the watcher's change sets
188
+ * (falling back to a full discovery walk when no sets are provided).
189
+ */
190
+ async handleWatchUpdate(changes) {
191
+ if (!this.system) return;
211
192
  const rootDir = this.rootDir;
212
- const resolvedSystemPath = resolve(rootDir, this.options.system);
213
193
  try {
214
- const systemSource = readFileSync(resolvedSystemPath, "utf-8");
215
- const systemRelPath = relative(rootDir, resolvedSystemPath);
216
- const cached = this.fileCache.get(systemRelPath);
217
- const currentHash = createHash("md5").update(systemSource).digest("hex");
218
- if (cached && cached.hash !== currentHash) {
194
+ let systemHit;
195
+ if (!changes.modifiedFiles && !changes.removedFiles) for (const [dep, hash] of this.systemDependencyHashes) {
196
+ let current = "";
197
+ try {
198
+ current = contentHash(readFileSync(dep, "utf-8"));
199
+ } catch {}
200
+ if (current !== hash) {
201
+ systemHit = dep;
202
+ break;
203
+ }
204
+ }
205
+ else systemHit = [...changes.modifiedFiles ?? [], ...changes.removedFiles ?? []].find((path) => toWatchKeys(path).some((key) => this.systemDependencyKeys.has(key)));
206
+ if (systemHit) {
207
+ this.log(`geological reset: system dependency changed (${relative(rootDir, systemHit)})`);
219
208
  this.resetForHmr();
220
- this.initialized = false;
221
209
  const promise = this.runFullPipeline();
222
210
  setAnalysisPromise(promise);
223
211
  await promise;
224
- this.initialized = true;
225
212
  return;
226
213
  }
227
- } catch {}
228
- const excludePatterns = this.options.exclude ?? [
229
- "node_modules",
230
- "dist",
231
- ".test.",
232
- ".spec.",
233
- ".next",
234
- ".animus"
235
- ];
236
- const extensionsSet = new Set(this.options.extensions ?? DEFAULT_EXTENSIONS);
237
- const shouldHandleMdx = extensionsSet.has(".mdx");
238
- const files = this.discoverFiles(rootDir, rootDir, excludePatterns, extensionsSet);
214
+ } catch (err) {
215
+ this.warn(`HMR geological-reset check failed: ${String(err)}`);
216
+ }
217
+ const { excludePatterns, extensionsSet, shouldHandleMdx } = this.resolveScanConfig();
218
+ let removedAny = false;
219
+ if (changes.removedFiles) for (const removedPath of changes.removedFiles) {
220
+ const rel = relative(rootDir, removedPath);
221
+ if (this.fileCache.delete(rel) || this.fileCache.delete(rel + ".tsx")) removedAny = true;
222
+ }
223
+ let files;
224
+ if (changes.modifiedFiles) {
225
+ files = [];
226
+ for (const modifiedPath of changes.modifiedFiles) {
227
+ if (!extensionsSet.has(extname(modifiedPath))) continue;
228
+ const rel = relative(rootDir, modifiedPath);
229
+ if (rel.startsWith("..")) continue;
230
+ if (excludePatterns.some((pattern) => modifiedPath.includes(pattern) || rel.includes(pattern))) continue;
231
+ files.push(modifiedPath);
232
+ }
233
+ } else files = discoverFiles(rootDir, rootDir, excludePatterns, extensionsSet);
239
234
  const changedPaths = [];
240
235
  for (const filePath of files) {
241
236
  let relPath = relative(rootDir, filePath);
242
- let source = readFileSync(filePath, "utf-8");
237
+ let source;
238
+ try {
239
+ source = readFileSync(filePath, "utf-8");
240
+ } catch {
241
+ continue;
242
+ }
243
243
  if (shouldHandleMdx && extname(filePath) === ".mdx") {
244
- const result = await preprocessMdx(source, relPath);
245
- if (result.kind !== "ok") continue;
246
- source = result.source;
247
- relPath = relPath + ".tsx";
244
+ const processed = await this.preprocessMdxEntry(source, relPath, { warn: false });
245
+ if (!processed) continue;
246
+ source = processed.source;
247
+ relPath = processed.relPath;
248
248
  }
249
249
  const cached = this.fileCache.get(relPath);
250
- const hash = createHash("md5").update(source).digest("hex");
250
+ const hash = contentHash(source);
251
251
  if (!cached || cached.hash !== hash) {
252
252
  changedPaths.push(relPath);
253
253
  this.fileCache.set(relPath, {
@@ -256,39 +256,74 @@ var AnimusWebpackPlugin = class {
256
256
  });
257
257
  }
258
258
  }
259
- if (changedPaths.length > 0) {
260
- const fileEntries = this.buildFileEntriesFromCache(changedPaths);
259
+ if (changedPaths.length > 0 || removedAny) {
260
+ const fileEntries = this.buildFileEntriesFromCache();
261
261
  resetAnalysisPromise();
262
262
  const promise = this.runIncrementalPipeline(fileEntries);
263
263
  setAnalysisPromise(promise);
264
264
  await promise;
265
265
  }
266
266
  }
267
+ /**
268
+ * Preprocess one `.mdx` entry into scanner-consumable tsx. Returns the
269
+ * rewritten source plus the `relPath + '.tsx'` path on success, or null
270
+ * when the file must be skipped.
271
+ *
272
+ * `warn: false` (incremental watch pass) skips silently — the full
273
+ * pipeline already surfaced the warning. `warn: true` (full pipeline)
274
+ * warns ONCE for a missing @mdx-js/mdx dependency via the shared
275
+ * `missingDepFlag` holder, and every time for a preprocessing error.
276
+ */
277
+ async preprocessMdxEntry(source, relPath, opts) {
278
+ const result = await preprocessMdx(source, relPath);
279
+ if (result.kind === "missing-dep") {
280
+ if (opts.warn && opts.missingDepFlag && !opts.missingDepFlag.warned) {
281
+ console.warn("[animus] ⚠ .mdx in extensions but @mdx-js/mdx not installed; MDX files skipped");
282
+ opts.missingDepFlag.warned = true;
283
+ }
284
+ return null;
285
+ }
286
+ if (result.kind === "error") {
287
+ if (opts.warn) console.warn(`[animus] ⚠ MDX preprocessing failed for ${relPath}: ${result.error}`);
288
+ return null;
289
+ }
290
+ return {
291
+ source: result.source,
292
+ relPath: relPath + ".tsx"
293
+ };
294
+ }
267
295
  async runFullPipeline() {
268
296
  const pipelineStart = this.now();
269
297
  const bt = {};
270
- try {
271
- const { clearAnalysisCache } = engineApi();
272
- clearAnalysisCache();
273
- } catch {}
298
+ clearEngineCache(engineApi);
274
299
  const rootDir = this.rootDir;
275
300
  const resolvedSystemPath = resolve(rootDir, this.options.system);
276
301
  let t = this.now();
277
- this.loadSystem(rootDir, resolvedSystemPath);
302
+ this.system = loadSystemConfig(engineApi, {
303
+ systemPath: resolvedSystemPath,
304
+ rootDir,
305
+ prefix: this.options.prefix
306
+ });
307
+ {
308
+ const deps = this.system.dependencies ?? [];
309
+ const keys = /* @__PURE__ */ new Set();
310
+ for (const key of toWatchKeys(resolvedSystemPath)) keys.add(key);
311
+ for (const dep of deps) for (const key of toWatchKeys(dep)) keys.add(key);
312
+ this.systemDependencyKeys = keys;
313
+ this.systemDependencyPaths = deps;
314
+ const hashes = /* @__PURE__ */ new Map();
315
+ for (const dep of deps.length > 0 ? deps : [resolvedSystemPath]) try {
316
+ hashes.set(dep, contentHash(readFileSync(dep, "utf-8")));
317
+ } catch {
318
+ hashes.set(dep, "");
319
+ }
320
+ this.systemDependencyHashes = hashes;
321
+ }
278
322
  bt.systemLoad = this.elapsed(t);
279
323
  t = this.now();
280
- const excludePatterns = this.options.exclude ?? [
281
- "node_modules",
282
- "dist",
283
- ".test.",
284
- ".spec.",
285
- ".next",
286
- ".animus"
287
- ];
288
- const extensionsSet = new Set(this.options.extensions ?? DEFAULT_EXTENSIONS);
289
- const shouldHandleMdx = extensionsSet.has(".mdx");
290
- let mdxMissingDepWarned = false;
291
- const files = this.discoverFiles(rootDir, rootDir, excludePatterns, extensionsSet);
324
+ const { excludePatterns, extensionsSet, shouldHandleMdx } = this.resolveScanConfig();
325
+ const missingDepFlag = { warned: false };
326
+ const files = discoverFiles(rootDir, rootDir, excludePatterns, extensionsSet);
292
327
  bt.fileDiscovery = this.elapsed(t);
293
328
  t = this.now();
294
329
  const fileEntries = [];
@@ -296,22 +331,15 @@ var AnimusWebpackPlugin = class {
296
331
  let source = readFileSync(filePath, "utf-8");
297
332
  let relPath = relative(rootDir, filePath);
298
333
  if (shouldHandleMdx && extname(filePath) === ".mdx") {
299
- const result = await preprocessMdx(source, relPath);
300
- if (result.kind === "missing-dep") {
301
- if (!mdxMissingDepWarned) {
302
- console.warn("[animus] ⚠ .mdx in extensions but @mdx-js/mdx not installed; MDX files skipped");
303
- mdxMissingDepWarned = true;
304
- }
305
- continue;
306
- }
307
- if (result.kind === "error") {
308
- console.warn(`[animus] ⚠ MDX preprocessing failed for ${relPath}: ${result.error}`);
309
- continue;
310
- }
311
- source = result.source;
312
- relPath = relPath + ".tsx";
334
+ const processed = await this.preprocessMdxEntry(source, relPath, {
335
+ warn: true,
336
+ missingDepFlag
337
+ });
338
+ if (!processed) continue;
339
+ source = processed.source;
340
+ relPath = processed.relPath;
313
341
  }
314
- const hash = createHash("md5").update(source).digest("hex");
342
+ const hash = contentHash(source);
315
343
  this.fileCache.set(relPath, {
316
344
  hash,
317
345
  source
@@ -326,216 +354,51 @@ var AnimusWebpackPlugin = class {
326
354
  bt.fileCount = fileEntries.length;
327
355
  t = this.now();
328
356
  const packageNames = extractSystemFilePackages(resolvedSystemPath);
329
- const packageMap = this.resolvePackagesByName(rootDir, packageNames);
330
- const excludePatternsForPkgs = [
331
- "dist",
332
- ".test.",
333
- ".spec."
334
- ];
335
- const collectedPkgDirs = [];
336
- this.externalSourceEntries.clear();
337
- for (const [specifier, entryRelPath] of Object.entries(packageMap)) {
338
- const absEntry = resolve(rootDir, entryRelPath);
339
- let pkgRoot = dirname(absEntry);
340
- while (pkgRoot !== dirname(pkgRoot) && !existsSync(join(pkgRoot, "package.json"))) pkgRoot = dirname(pkgRoot);
341
- const srcDir = join(pkgRoot, "src");
342
- if (existsSync(srcDir)) {
343
- collectedPkgDirs.push(srcDir);
344
- const srcEntry = join(srcDir, "index.ts");
345
- if (existsSync(srcEntry)) {
346
- packageMap[specifier] = relative(rootDir, srcEntry);
347
- this.externalSourceEntries.set(specifier, srcEntry);
348
- }
349
- const pkgFiles = this.discoverFiles(srcDir, rootDir, excludePatternsForPkgs, extensionsSet);
350
- for (const pkgFile of pkgFiles) {
351
- let pkgRelPath = relative(rootDir, pkgFile);
352
- if (!fileEntries.some((e) => e.path === pkgRelPath)) try {
353
- let pkgSource = readFileSync(pkgFile, "utf-8");
354
- if (shouldHandleMdx && extname(pkgFile) === ".mdx") {
355
- const result = await preprocessMdx(pkgSource, pkgRelPath);
356
- if (result.kind === "missing-dep") {
357
- if (!mdxMissingDepWarned) {
358
- console.warn("[animus] ⚠ .mdx in extensions but @mdx-js/mdx not installed; MDX files skipped");
359
- mdxMissingDepWarned = true;
360
- }
361
- continue;
362
- }
363
- if (result.kind === "error") {
364
- console.warn(`[animus] ⚠ MDX preprocessing failed for ${pkgRelPath}: ${result.error}`);
365
- continue;
366
- }
367
- pkgSource = result.source;
368
- pkgRelPath = pkgRelPath + ".tsx";
369
- }
370
- const pkgHash = createHash("md5").update(pkgSource).digest("hex");
371
- this.fileCache.set(pkgRelPath, {
372
- hash: pkgHash,
373
- source: pkgSource
374
- });
375
- fileEntries.push({
376
- path: pkgRelPath,
377
- source: pkgSource,
378
- hash: pkgHash
379
- });
380
- } catch {}
381
- }
382
- } else collectedPkgDirs.push(dirname(absEntry));
383
- }
384
- this.externalPackageDirs = collectedPkgDirs;
385
- setSharedExternalDirs(collectedPkgDirs);
386
- setSharedExternalEntries(this.externalSourceEntries);
387
- bt.packageResolve = this.elapsed(t);
388
- const { analyzeProject } = engineApi();
389
- const animusDirPath = join(rootDir, ".animus");
390
- const emitterConfig = JSON.stringify({
391
- runtime_import: "@animus-ui/system/runtime",
392
- css_module_id: ".animus/styles.css",
393
- system_props_module_id: join(animusDirPath, "system-props.js")
394
- });
395
- t = this.now();
396
- const fileEntriesJson = JSON.stringify(fileEntries);
397
- const packageMapJson = JSON.stringify(packageMap);
398
- bt.jsonSerialize = this.elapsed(t);
399
- t = this.now();
400
- const manifestJson = analyzeProject(...buildProductionAnalyzeProjectArgs({
401
- filesJson: fileEntriesJson,
402
- scalesJson: this.themeJson,
403
- variableMapJson: this.variableMapJson,
404
- contextualVarsJson: this.contextualVarsJson || null,
405
- propConfigJson: this.configJson,
406
- groupRegistryJson: this.groupRegistryJson,
407
- packageResolutionJson: packageMapJson,
408
- emitterConfigJson: emitterConfig,
409
- selectorAliasesJson: this.selectorAliasesJson,
410
- globalStyleBlocksJson: this.globalStyleBlocksJson,
411
- pathAliasesJson: this.pathAliasesJson,
412
- keyframesJson: this.keyframesBlocksJson
413
- }));
414
- bt.rustExtract = this.elapsed(t);
415
- t = this.now();
416
- const manifest = JSON.parse(manifestJson);
417
- surfaceManifestDiagnostics(manifest, (message) => this.warn(message));
418
- bt.jsonParse = this.elapsed(t);
419
- bt.analysis = (bt.jsonSerialize ?? 0) + (bt.rustExtract ?? 0) + (bt.jsonParse ?? 0);
420
- if (manifest?.report) this.log(`Extracted ${manifest.report.components_extracted ?? "?"}/${manifest.report.components_total ?? "?"} components (${bt.analysis}ms)`);
421
- this.globalCss = manifest?.sheets?.global || "";
422
- let componentCss = manifest?.css || "";
423
- componentCss = applyUnitFallback(componentCss);
424
- const { declaration, variables, body } = assembleStylesheet({
425
- layers: this.options.layers,
426
- variableCss: this.variableCss,
427
- globalCss: this.globalCss,
428
- componentCss,
429
- split: true
357
+ const preResolved = resolvePackagesByName(rootDir, packageNames);
358
+ const collected = await collectExternalPackageSources({
359
+ specifiers: packageNames,
360
+ resolveSpecifier: (name) => preResolved[name] ? resolve(rootDir, preResolved[name]) : null,
361
+ rootDir,
362
+ extensionsSet,
363
+ hasEntry: (relPath) => fileEntries.some((e) => e.path === relPath),
364
+ preprocessFile: async (source, relPath, absPath) => {
365
+ if (shouldHandleMdx && extname(absPath) === ".mdx") return this.preprocessMdxEntry(source, relPath, {
366
+ warn: true,
367
+ missingDepFlag
368
+ });
369
+ return {
370
+ source,
371
+ relPath
372
+ };
373
+ },
374
+ onUnreadable: (relPath, err) => this.warn(`skipped unreadable package file ${relPath}: ${String(err)}`)
430
375
  });
431
- const fullCss = [
432
- declaration,
433
- variables,
434
- body
435
- ].filter(Boolean).join("\n");
436
- setSharedCss(fullCss);
437
- const cssHash = createHash("md5").update(fullCss).digest("hex");
438
- if (cssHash !== this.lastCssHash) {
439
- const animusDir = join(rootDir, ".animus");
440
- if (!existsSync(animusDir)) mkdirSync(animusDir, { recursive: true });
441
- writeFileSync(join(animusDir, "styles.css"), fullCss);
442
- this.lastCssHash = cssHash;
376
+ for (const record of collected.outcomes) if (record.outcome === "empty") this.warn(`include '${record.specifier}' resolved but discovered no component sources`);
377
+ const unresolvableMessage = unresolvableIncludesMessage(collected.outcomes);
378
+ if (unresolvableMessage !== null) {
379
+ if (this.options.strict) throw new Error(unresolvableMessage);
380
+ this.warn(unresolvableMessage);
443
381
  }
444
- const systemPropMap = JSON.stringify(manifest?.system_prop_map ?? {});
445
- const dynamicProps = manifest?.dynamic_props ?? {};
446
- const dynamicPropConfig = {};
447
- for (const [propName, meta] of Object.entries(dynamicProps)) dynamicPropConfig[propName] = {
448
- varName: meta.var_name,
449
- slotClass: meta.slot_class,
450
- ...meta.transform_name ? { transformName: meta.transform_name } : {},
451
- ...meta.scale_values && Object.keys(meta.scale_values).length > 0 ? { scaleValues: meta.scale_values } : {}
452
- };
453
- const transformsSource = "{}";
454
- const animusDir = join(rootDir, ".animus");
455
- if (!existsSync(animusDir)) mkdirSync(animusDir, { recursive: true });
456
- const systemPropsContent = `export const systemPropMap = ${systemPropMap};\nexport const systemPropGroups = ${this.groupRegistryJson};\nexport const dynamicPropConfig = ${JSON.stringify(dynamicPropConfig)};\nexport const transforms = ${transformsSource};\n`;
457
- setSharedSystemProps(systemPropsContent);
458
- writeFileSync(join(animusDir, "system-props.js"), systemPropsContent);
459
- setManifestJson(manifestJson);
460
- bt.total = this.elapsed(pipelineStart);
461
- this.logBuildTimings(bt, manifest?.timing);
462
- }
463
- loadSystem(rootDir, systemPath) {
464
- const { loadSystemModule } = engineApi();
465
- const config = loadSystemModule(systemPath, rootDir);
466
- this.configJson = config.propConfig;
467
- this.groupRegistryJson = config.groupRegistry;
468
- this.themeJson = config.scalesJson;
469
- this.variableMapJson = config.variableMapJson;
470
- this.variableCss = config.variableCss;
471
- this.contextualVarsJson = config.contextualVarsJson;
472
- this.selectorAliasesJson = config.selectorAliases || null;
473
- if (this.options.prefix) {
474
- const prefixed = applyPrefix(this.options.prefix, this.variableMapJson, this.variableCss, this.themeJson, this.contextualVarsJson || void 0);
475
- this.variableMapJson = prefixed.variableMapJson;
476
- this.variableCss = prefixed.variableCss;
477
- if (prefixed.themeJson) this.themeJson = prefixed.themeJson;
478
- if (prefixed.contextualVarsJson) this.contextualVarsJson = prefixed.contextualVarsJson;
479
- }
480
- this.globalStyleBlocksJson = config.globalStyleBlocks || null;
481
- this.keyframesBlocksJson = config.keyframesBlocks || null;
482
- }
483
- discoverFiles(dir, rootDir, excludePatterns, extensionsSet) {
484
- const results = [];
485
- const walk = (currentDir) => {
486
- let entries;
487
- try {
488
- entries = readdirSync(currentDir);
489
- } catch {
490
- return;
491
- }
492
- for (const entry of entries) {
493
- const fullPath = join(currentDir, entry);
494
- const relPath = relative(rootDir, fullPath);
495
- if (excludePatterns.some((p) => relPath.includes(p))) continue;
496
- let stat;
497
- try {
498
- stat = statSync(fullPath);
499
- } catch {
500
- continue;
501
- }
502
- if (stat.isDirectory()) walk(fullPath);
503
- else if (extensionsSet.has(extname(entry))) results.push(fullPath);
504
- }
505
- };
506
- walk(dir);
507
- return results;
508
- }
509
- resolvePackagesByName(rootDir, names) {
510
- if (names.length === 0) return {};
511
- const nameSet = new Set(names);
512
- const resolved = /* @__PURE__ */ new Set();
513
- const packageMap = {};
514
- try {
515
- const workspaces = JSON.parse(readFileSync(join(rootDir, "package.json"), "utf-8")).workspaces || [];
516
- for (const ws of workspaces) {
517
- const wsDir = resolve(rootDir, ws);
518
- if (!existsSync(wsDir)) continue;
519
- try {
520
- const pkg = JSON.parse(readFileSync(join(wsDir, "package.json"), "utf-8"));
521
- const name = pkg.name || "";
522
- if (nameSet.has(name)) {
523
- const entryPath = resolve(wsDir, pkg.main || pkg.module || "index.ts");
524
- if (existsSync(entryPath)) {
525
- packageMap[name] = relative(rootDir, entryPath);
526
- resolved.add(name);
527
- }
528
- }
529
- } catch {}
530
- }
531
- } catch {}
532
- for (const name of nameSet) {
533
- if (resolved.has(name)) continue;
534
- try {
535
- packageMap[name] = relative(rootDir, __require.resolve(name, { paths: [rootDir] }));
536
- } catch {}
382
+ const packageMap = collected.packageMap;
383
+ this.lastPackageMap = packageMap;
384
+ this.externalSourceEntries = collected.sourceEntries;
385
+ for (const entry of collected.entries) {
386
+ const hash = contentHash(entry.source);
387
+ this.fileCache.set(entry.path, {
388
+ hash,
389
+ source: entry.source
390
+ });
391
+ fileEntries.push({
392
+ path: entry.path,
393
+ source: entry.source,
394
+ hash
395
+ });
537
396
  }
538
- return packageMap;
397
+ this.externalPackageDirs = collected.packageDirs;
398
+ setSharedExternalDirs(collected.packageDirs);
399
+ setSharedExternalEntries(collected.sourceEntries);
400
+ bt.packageResolve = this.elapsed(t);
401
+ await this.analyzeAndEmit(fileEntries, packageMap, false, bt, pipelineStart);
539
402
  }
540
403
  /**
541
404
  * Reset analysis state for HMR geological reset.
@@ -544,22 +407,21 @@ var AnimusWebpackPlugin = class {
544
407
  resetAnalysisPromise();
545
408
  this.lastCssHash = null;
546
409
  this.lastSystemPropsHash = null;
547
- try {
548
- const { clearAnalysisCache } = engineApi();
549
- clearAnalysisCache();
550
- } catch {}
410
+ this.lastManifestHash = null;
411
+ this.lastAnalysisInputsHash = null;
412
+ clearEngineCache(engineApi);
551
413
  }
552
414
  /**
553
- * Build cache-aware file entries: full source for changed files,
554
- * empty source + hash for unchanged (Rust cache-hit path skips these).
415
+ * Build file entries from cache: every cached file rides with full source.
416
+ * The v2 engine has NO Rust-side cache (extract-v2-spine DEF-7: uncached
417
+ * re-analysis beats a cache-hit path), so it must always receive full sources
418
+ * (openspec: retire-extract-v1 removed the v1 empty-source cache contract).
555
419
  */
556
- buildFileEntriesFromCache(changedPaths) {
557
- const changedSet = new Set(changedPaths);
558
- const fullSources = getSharedEngine() === "v2";
420
+ buildFileEntriesFromCache() {
559
421
  const entries = [];
560
422
  for (const [path, { hash, source }] of this.fileCache) entries.push({
561
423
  path,
562
- source: fullSources || changedSet.has(path) ? source : "",
424
+ source,
563
425
  hash
564
426
  });
565
427
  return entries;
@@ -569,88 +431,238 @@ var AnimusWebpackPlugin = class {
569
431
  * Reuses system config from the last full pipeline run.
570
432
  */
571
433
  async runIncrementalPipeline(fileEntries) {
572
- const rootDir = this.rootDir;
573
434
  const bt = {};
574
435
  const pipelineStart = this.now();
575
- const { analyzeProject } = engineApi();
576
- const animusDirPath = join(rootDir, ".animus");
577
- const emitterConfig = JSON.stringify({
578
- runtime_import: "@animus-ui/system/runtime",
579
- css_module_id: ".animus/styles.css",
580
- system_props_module_id: join(animusDirPath, "system-props.js")
581
- });
582
- const packageMapJson = JSON.stringify(Object.fromEntries(Array.from(this.externalSourceEntries).map(([spec, entry]) => [spec, relative(rootDir, entry)])));
583
- let t = this.now();
584
- const fileEntriesJson = JSON.stringify(fileEntries);
585
- bt.jsonSerialize = this.elapsed(t);
586
- t = this.now();
587
- const manifestJson = analyzeProject(...buildHmrAnalyzeProjectArgs({
588
- filesJson: fileEntriesJson,
589
- scalesJson: this.themeJson,
590
- variableMapJson: this.variableMapJson,
591
- contextualVarsJson: this.contextualVarsJson || null,
592
- propConfigJson: this.configJson,
593
- groupRegistryJson: this.groupRegistryJson,
594
- packageResolutionJson: packageMapJson,
595
- emitterConfigJson: emitterConfig,
596
- selectorAliasesJson: this.selectorAliasesJson,
597
- globalStyleBlocksJson: this.globalStyleBlocksJson,
436
+ await this.analyzeAndEmit(fileEntries, this.lastPackageMap, true, bt, pipelineStart);
437
+ }
438
+ /**
439
+ * Shared analysis + emit core for both pipelines — the single call site
440
+ * that routes every manifest through the shared `runProjectAnalysis`,
441
+ * reachable from both runFullPipeline (production) and
442
+ * runIncrementalPipeline (HMR).
443
+ *
444
+ * Owns diagnostic surfacing, CSS assembly + styles.css write guard,
445
+ * system-props module emit, and the timing log. The `devMode` flag is the
446
+ * ONLY behavioral fork:
447
+ *
448
+ * - `false` (production): computes bt.analysis + logs the extraction
449
+ * report, and writes system-props.js UNCONDITIONALLY (no
450
+ * lastSystemPropsHash guard).
451
+ * - `true` (HMR): skips the report log, and guards the system-props.js
452
+ * write by lastSystemPropsHash.
453
+ *
454
+ * The styles.css write guard (lastCssHash) is identical on both paths.
455
+ */
456
+ async analyzeAndEmit(fileEntries, packageMap, devMode, bt, pipelineStart) {
457
+ const system = this.system;
458
+ const result = runProjectAnalysis(engineApi, {
459
+ fileEntries,
460
+ packageMap,
461
+ system,
462
+ emitter: {
463
+ runtimeImport: "@animus-ui/system/runtime",
464
+ cssModuleId: ANIMUS_CSS_MODULE_ID,
465
+ systemPropsModuleId: this.systemPropsModuleId ?? join(this.rootDir, ".animus", "system-props.js")
466
+ },
598
467
  pathAliasesJson: this.pathAliasesJson,
599
- keyframesJson: this.keyframesBlocksJson
600
- }));
601
- bt.rustExtract = this.elapsed(t);
602
- t = this.now();
603
- const manifest = JSON.parse(manifestJson);
604
- surfaceManifestDiagnostics(manifest, (message) => this.warn(message));
605
- bt.jsonParse = this.elapsed(t);
606
- this.globalCss = manifest?.sheets?.global || "";
607
- let componentCss = manifest?.css || "";
608
- componentCss = applyUnitFallback(componentCss);
609
- const { declaration: hmrDecl, variables: hmrVars, body: hmrBody } = assembleStylesheet({
468
+ staticCssJson: this.staticCssJson,
469
+ devMode,
470
+ warn: (message) => this.warn(message)
471
+ });
472
+ bt.jsonSerialize = result.timings.serializeMs;
473
+ bt.rustExtract = result.timings.extractMs;
474
+ bt.jsonParse = result.timings.parseMs;
475
+ const manifest = result.manifest;
476
+ if (!devMode) {
477
+ bt.analysis = (bt.jsonSerialize ?? 0) + (bt.rustExtract ?? 0) + (bt.jsonParse ?? 0);
478
+ if (manifest?.report) this.log(`Extracted ${manifest.report.components_extracted ?? "?"}/${manifest.report.components_total ?? "?"} components (${bt.analysis}ms)`);
479
+ }
480
+ const { declaration, variables, body } = assembleStylesheet({
610
481
  layers: this.options.layers,
611
- variableCss: this.variableCss,
612
- globalCss: this.globalCss,
613
- componentCss,
482
+ variableCss: system.variableCss,
483
+ globalCss: result.globalCss,
484
+ componentCss: result.componentCss,
614
485
  split: true
615
486
  });
487
+ if (this.lcssTargets === null) this.lcssTargets = resolveLightningTargets(this.options.targets, this.rootDir);
616
488
  const fullCss = [
617
- hmrDecl,
618
- hmrVars,
619
- hmrBody
489
+ declaration,
490
+ variables,
491
+ postProcessCss(body, {
492
+ minify: this.options.minify ?? process.env.NODE_ENV === "production",
493
+ targets: this.lcssTargets,
494
+ warnFn: (msg) => this.warn(msg)
495
+ })
620
496
  ].filter(Boolean).join("\n");
621
497
  setSharedCss(fullCss);
622
- const cssHash = createHash("md5").update(fullCss).digest("hex");
498
+ const cssHash = contentHash(fullCss);
623
499
  if (cssHash !== this.lastCssHash) {
624
- const animusDir = join(rootDir, ".animus");
625
- if (!existsSync(animusDir)) mkdirSync(animusDir, { recursive: true });
626
- writeFileSync(join(animusDir, "styles.css"), fullCss);
500
+ this.writeAnimusFile("styles.css", fullCss);
627
501
  this.lastCssHash = cssHash;
628
502
  }
629
- const systemPropMap = JSON.stringify(manifest?.system_prop_map ?? {});
630
- const dynamicProps = manifest?.dynamic_props ?? {};
631
- const dynamicPropConfig = {};
632
- for (const [propName, meta] of Object.entries(dynamicProps)) dynamicPropConfig[propName] = {
633
- varName: meta.var_name,
634
- slotClass: meta.slot_class,
635
- ...meta.transform_name ? { transformName: meta.transform_name } : {},
636
- ...meta.scale_values && Object.keys(meta.scale_values).length > 0 ? { scaleValues: meta.scale_values } : {}
637
- };
638
- const systemPropsContent = `export const systemPropMap = ${systemPropMap};\nexport const systemPropGroups = ${this.groupRegistryJson};\nexport const dynamicPropConfig = ${JSON.stringify(dynamicPropConfig)};\nexport const transforms = {};\n`;
503
+ const systemPropsContent = buildSystemPropsModule({
504
+ systemPropMapJson: JSON.stringify(manifest?.system_prop_map ?? {}),
505
+ groupRegistryJson: system.groupRegistryJson,
506
+ dynamicProps: manifest?.dynamic_props ?? {}
507
+ });
639
508
  setSharedSystemProps(systemPropsContent);
640
- const systemPropsHash = createHash("md5").update(systemPropsContent).digest("hex");
641
- if (systemPropsHash !== this.lastSystemPropsHash) {
642
- const spDir = join(rootDir, ".animus");
643
- if (!existsSync(spDir)) mkdirSync(spDir, { recursive: true });
644
- writeFileSync(join(spDir, "system-props.js"), systemPropsContent);
645
- this.lastSystemPropsHash = systemPropsHash;
509
+ if (devMode) {
510
+ const systemPropsHash = contentHash(systemPropsContent);
511
+ if (systemPropsHash !== this.lastSystemPropsHash) {
512
+ this.writeAnimusFile("system-props.js", systemPropsContent);
513
+ this.lastSystemPropsHash = systemPropsHash;
514
+ }
515
+ } else this.writeAnimusFile("system-props.js", systemPropsContent);
516
+ setManifestJson(result.manifestJson);
517
+ const manifestHash = contentHash(result.manifestJson);
518
+ if (this.lastManifestHash === null) this.lastManifestHash = this.diskArtifactHash("manifest.json");
519
+ if (manifestHash !== this.lastManifestHash) {
520
+ this.writeAnimusFile("manifest.json", result.manifestJson);
521
+ this.lastManifestHash = manifestHash;
522
+ }
523
+ if (this.persistAnalysisInputs) {
524
+ const inputsJson = JSON.stringify(result.inputs);
525
+ const inputsHash = contentHash(inputsJson);
526
+ if (this.lastAnalysisInputsHash === null) this.lastAnalysisInputsHash = this.diskArtifactHash("analysis-inputs.json");
527
+ if (inputsHash !== this.lastAnalysisInputsHash) {
528
+ this.writeAnimusFile("analysis-inputs.json", inputsJson);
529
+ this.lastAnalysisInputsHash = inputsHash;
530
+ }
646
531
  }
647
- setManifestJson(manifestJson);
648
532
  bt.total = this.elapsed(pipelineStart);
649
- this.logBuildTimings(bt, manifest?.timing);
533
+ logBuildTimings(bt, manifest?.timing, (msg) => this.log(msg), this.verbose);
650
534
  }
651
- /** Expose file cache for HMR change detection */
652
- getFileCache() {
653
- return this.fileCache;
535
+ /** Content hash of an existing `.animus/` artifact, or null when absent or
536
+ * unreadable. Seeds a null write guard so a FRESH session never rewrites a
537
+ * byte-identical artifact (spec: next-turbopack-integration — a rewrite
538
+ * changes mtime/inode and needlessly re-keys Turbopack loader-worker
539
+ * hydration). */
540
+ diskArtifactHash(name) {
541
+ try {
542
+ return contentHash(readFileSync(join(this.rootDir, ".animus", name), "utf-8"));
543
+ } catch {
544
+ return null;
545
+ }
546
+ }
547
+ /** Ensure `.animus/` exists and write one generated artifact into it.
548
+ * Write-then-rename so cross-process readers (Turbopack loader workers)
549
+ * can never observe a torn half-written file. The tmp name carries the
550
+ * pid — Next dev evaluates the config in more than one process, and two
551
+ * sessions writing the same artifact must not race on one tmp path. */
552
+ writeAnimusFile(name, content) {
553
+ const dir = join(this.rootDir, ".animus");
554
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
555
+ const tmpPath = join(dir, `.${name}.${process.pid}.tmp`);
556
+ writeFileSync(tmpPath, content);
557
+ renameSync(tmpPath, join(dir, name));
558
+ }
559
+ };
560
+ //#endregion
561
+ //#region src/plugin.ts
562
+ const PLUGIN_NAME = "AnimusWebpackPlugin";
563
+ /**
564
+ * Webpack adapter for the extraction pipeline. All pipeline logic lives in
565
+ * the bundler-agnostic ExtractionSession — this class owns only the webpack
566
+ * wiring: hook registration, cross-compiler analysis dedup, alias
567
+ * harvesting from the compiler config, watch-event translation, and
568
+ * in-memory CSS asset replacement.
569
+ */
570
+ var AnimusWebpackPlugin = class {
571
+ options;
572
+ session;
573
+ initialized = false;
574
+ aliasesExtracted = false;
575
+ constructor(options) {
576
+ this.options = options;
577
+ assertNoRetiredEngineSelection(options.engine);
578
+ setSharedEngine(options.engine ?? "v2");
579
+ this.session = new ExtractionSession(options);
580
+ }
581
+ /** Extract path aliases from webpack's resolve.alias config. Runs once per
582
+ * plugin instance — the resolve config is immutable after apply. */
583
+ extractAliases(compiler) {
584
+ if (this.aliasesExtracted) return;
585
+ this.aliasesExtracted = true;
586
+ const rootDir = compiler.context;
587
+ const rawAlias = compiler.options?.resolve?.alias;
588
+ if (!rawAlias || typeof rawAlias !== "object") return;
589
+ const pairs = [];
590
+ for (const [key, value] of Object.entries(rawAlias)) {
591
+ const target = Array.isArray(value) ? value[0] : value;
592
+ if (typeof target !== "string") continue;
593
+ if (key === ".animus/styles.css") continue;
594
+ pairs.push({
595
+ pattern: key,
596
+ target
597
+ });
598
+ }
599
+ const built = buildPathAliasesJson(pairs, rootDir);
600
+ if (built) this.session.pathAliasesJson = built.json;
601
+ }
602
+ apply(compiler) {
603
+ if (compiler.options?.name === "edge-server") return;
604
+ compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
605
+ const registerSystemDependencies = () => {
606
+ for (const dep of this.session.systemDependencyPaths) if (existsSync(dep)) compilation.fileDependencies.add(dep);
607
+ else compilation.missingDependencies.add(dep);
608
+ };
609
+ registerSystemDependencies();
610
+ const stage = compiler.webpack?.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL ?? -2e3;
611
+ const RawSource = compiler.webpack?.sources.RawSource;
612
+ compilation.hooks.processAssets.tap({
613
+ name: PLUGIN_NAME,
614
+ stage
615
+ }, () => {
616
+ registerSystemDependencies();
617
+ const css = getSharedCss();
618
+ if (!css || !RawSource) return;
619
+ const cssPath = join(this.session.rootDir || compiler.context, ".animus", "styles.css");
620
+ if (compilation.getAsset(cssPath)) {
621
+ compilation.updateAsset(cssPath, new RawSource(css));
622
+ return;
623
+ }
624
+ if (compilation.getAsset(".animus/styles.css")) compilation.updateAsset(ANIMUS_CSS_MODULE_ID, new RawSource(css));
625
+ });
626
+ });
627
+ compiler.hooks.run.tapPromise(PLUGIN_NAME, async (_compiler) => {
628
+ this.session.rootDir = _compiler.context;
629
+ this.extractAliases(_compiler);
630
+ const existing = getAnalysisPromise();
631
+ if (existing) {
632
+ await existing;
633
+ return;
634
+ }
635
+ const promise = this.session.runFullPipeline();
636
+ setAnalysisPromise(promise);
637
+ await promise;
638
+ });
639
+ compiler.hooks.watchRun.tapPromise(PLUGIN_NAME, async (_compiler) => {
640
+ this.session.rootDir = _compiler.context;
641
+ this.extractAliases(_compiler);
642
+ if (!this.initialized) {
643
+ const existing = getAnalysisPromise();
644
+ if (existing) {
645
+ await existing;
646
+ this.initialized = true;
647
+ return;
648
+ }
649
+ const promise = this.session.runFullPipeline();
650
+ setAnalysisPromise(promise);
651
+ await promise;
652
+ this.initialized = true;
653
+ return;
654
+ }
655
+ await this.session.handleWatchUpdate({
656
+ modifiedFiles: _compiler.modifiedFiles,
657
+ removedFiles: _compiler.removedFiles
658
+ });
659
+ });
660
+ }
661
+ /**
662
+ * Reset analysis state for HMR geological reset.
663
+ */
664
+ resetForHmr() {
665
+ this.session.resetForHmr();
654
666
  }
655
667
  /** Expose options for the loader */
656
668
  getOptions() {
@@ -659,18 +671,187 @@ var AnimusWebpackPlugin = class {
659
671
  /** Expose external package directories for webpack loader allowlisting.
660
672
  * Falls back to shared globalThis state for non-owning compiler instances. */
661
673
  getExternalPackageDirs() {
662
- return this.externalPackageDirs.length > 0 ? this.externalPackageDirs : getSharedExternalDirs();
674
+ return this.session.externalPackageDirs.length > 0 ? this.session.externalPackageDirs : getSharedExternalDirs();
663
675
  }
664
676
  /** Expose external package source entries for webpack resolve alias.
665
677
  * Falls back to shared globalThis state for non-owning compiler instances. */
666
678
  getExternalSourceEntries() {
667
- return this.externalSourceEntries.size > 0 ? this.externalSourceEntries : getSharedExternalEntries();
679
+ return this.session.externalSourceEntries.size > 0 ? this.session.externalSourceEntries : getSharedExternalEntries();
668
680
  }
669
681
  };
670
682
  //#endregion
683
+ //#region src/turbopack-config.ts
684
+ /** The single glob the Animus loader registers under. `.mjs` is included
685
+ * for webpack parity: an external package without `src/` is ingested via
686
+ * its resolved dist entry (often `dist/index.mjs`) and must still reach
687
+ * the loader — manifest lookup remains the file-level gate. */
688
+ const ANIMUS_TURBOPACK_RULE_GLOB = "*.{ts,tsx,js,jsx,mjs}";
689
+ /** Virtual system-props id emitted into transformed sources under Turbopack
690
+ * (absolute-path imports are rejected there); resolveAlias maps it to the
691
+ * on-disk artifact. */
692
+ const TURBOPACK_SYSTEM_PROPS_ID = "virtual:animus/system-props";
693
+ /**
694
+ * Resolve whether Turbopack wiring is active for this process. Default is
695
+ * `'auto'`: active exactly when the TURBOPACK environment variable is set
696
+ * (Next sets it for every Turbopack dev/build). The stable `turbopack`
697
+ * option wins over the deprecated `unstable_turbopack` alias.
698
+ */
699
+ function resolveTurbopackMode(options, env = process.env) {
700
+ const mode = options.turbopack?.mode ?? options.unstable_turbopack?.mode ?? "auto";
701
+ if (mode === "on") return true;
702
+ if (mode === "auto") return env.TURBOPACK !== void 0;
703
+ return false;
704
+ }
705
+ /**
706
+ * Build the `turbopack` config fragment: one glob-keyed loader rule
707
+ * (file-level allowlisting lives in the loader via manifest lookup — Next 15
708
+ * rules have no condition algebra) plus resolve aliases for the virtual
709
+ * system-props id, the emitter's stylesheet id, and each collected external
710
+ * package specifier redirected to its source entry.
711
+ */
712
+ function buildTurbopackConfig(args) {
713
+ const { rootDir, loaderPath, options, externalSourceEntries } = args;
714
+ const loaderOptions = {
715
+ rootDir,
716
+ ...options.strict !== void 0 ? { strict: options.strict } : {},
717
+ ...options.cssImportTarget !== void 0 ? { cssImportTarget: options.cssImportTarget } : {}
718
+ };
719
+ const resolveAlias = {
720
+ [TURBOPACK_SYSTEM_PROPS_ID]: "./.animus/system-props.js",
721
+ ".animus/styles.css": "./.animus/styles.css"
722
+ };
723
+ for (const [specifier, srcEntry] of externalSourceEntries) resolveAlias[specifier] = `./${relative(rootDir, srcEntry).replace(/\\/g, "/")}`;
724
+ return {
725
+ rules: { [ANIMUS_TURBOPACK_RULE_GLOB]: { loaders: [{
726
+ loader: loaderPath,
727
+ options: loaderOptions
728
+ }] } },
729
+ resolveAlias
730
+ };
731
+ }
732
+ /**
733
+ * Locate the Turbopack loader module: built dist first, source fallback for
734
+ * in-repo development. CJS preferred — Turbopack's loader-runner follows
735
+ * the webpack loader contract (`module.exports = fn`), and the .cjs build
736
+ * is loadable by every runner version; ESM default-export acceptance is
737
+ * not guaranteed.
738
+ */
739
+ function resolveTurbopackLoaderPath(pluginDir) {
740
+ for (const candidate of ["turbopack-loader.cjs", "turbopack-loader.mjs"]) {
741
+ const distPath = resolve(pluginDir, candidate);
742
+ if (existsSync(distPath)) return distPath;
743
+ }
744
+ return resolve(pluginDir, "turbopack-loader.ts");
745
+ }
746
+ //#endregion
747
+ //#region src/turbopack-orchestrator.ts
748
+ /**
749
+ * Out-of-band pipeline orchestration for Turbopack (spec:
750
+ * next-turbopack-integration). Turbopack exposes no compiler hooks, so the
751
+ * pipeline runs during next.config resolution and, in dev, from a
752
+ * filesystem watcher — the bundler only ever sees the `.animus/` disk
753
+ * artifacts.
754
+ */
755
+ /** Run the full pipeline with the Turbopack emitter identity: hydration
756
+ * artifacts persisted, and the virtual system-props id (Turbopack rejects
757
+ * the webpack path's absolute-path imports). */
758
+ async function runTurbopackPipeline(session) {
759
+ session.persistAnalysisInputs = true;
760
+ session.systemPropsModuleId = TURBOPACK_SYSTEM_PROPS_ID;
761
+ await session.runFullPipeline();
762
+ }
763
+ const activeWatcherRoots = /* @__PURE__ */ new Set();
764
+ const IGNORED_SEGMENTS = new Set([
765
+ ".animus",
766
+ ".next",
767
+ "node_modules"
768
+ ]);
769
+ /**
770
+ * Start the dev watcher: fs.watch per eligible top-level directory (plus a
771
+ * non-recursive watch on the root itself), debounced into
772
+ * existence-partitioned modified/removed sets feeding
773
+ * `session.handleWatchUpdate` (serialized — updates never overlap).
774
+ * Generated and vendored trees (`.animus`, `.next`, node_modules) are never
775
+ * registered with the OS — recursive registration of those trees exhausts
776
+ * inotify/kqueue descriptors (EMFILE/ENOSPC) on large projects.
777
+ * Idempotent per project root; unref'd so it never holds the process open.
778
+ * Asynchronous FSWatcher errors degrade to no-watch with a warning instead
779
+ * of crashing the dev server. Returns a close handle, or null when this
780
+ * root is already watched or the platform lacks recursive fs.watch (Linux
781
+ * before Node 20 — degrades to no-watch with a warning).
782
+ */
783
+ function startTurbopackWatcher(session, rootDir, debounceMs = 75, watchFn = watch) {
784
+ if (activeWatcherRoots.has(rootDir)) return null;
785
+ activeWatcherRoots.add(rootDir);
786
+ const pendingPaths = /* @__PURE__ */ new Set();
787
+ const watchers = /* @__PURE__ */ new Map();
788
+ let timer = null;
789
+ let updateChain = Promise.resolve();
790
+ let closed = false;
791
+ const flush = () => {
792
+ timer = null;
793
+ const modifiedFiles = /* @__PURE__ */ new Set();
794
+ const removedFiles = /* @__PURE__ */ new Set();
795
+ for (const path of pendingPaths) (existsSync(path) ? modifiedFiles : removedFiles).add(path);
796
+ pendingPaths.clear();
797
+ updateChain = updateChain.then(() => session.handleWatchUpdate({
798
+ modifiedFiles,
799
+ removedFiles
800
+ }).catch((err) => {
801
+ console.warn(`[animus-extract] Turbopack watch update failed: ${String(err)}`);
802
+ }));
803
+ };
804
+ const closeAll = () => {
805
+ if (closed) return;
806
+ closed = true;
807
+ if (timer) clearTimeout(timer);
808
+ timer = null;
809
+ for (const watcher of watchers.values()) watcher.close();
810
+ watchers.clear();
811
+ activeWatcherRoots.delete(rootDir);
812
+ };
813
+ const onWatcherError = (err) => {
814
+ closeAll();
815
+ console.warn(`[animus-extract] Turbopack dev watcher failed (${String(err)}); source edits require a dev-server restart`);
816
+ };
817
+ const onEvent = (baseDir, filename) => {
818
+ if (closed || !filename) return;
819
+ const rel = filename.toString();
820
+ if (rel.split(/[\\/]/).some((segment) => IGNORED_SEGMENTS.has(segment))) return;
821
+ const abs = join(baseDir, rel);
822
+ if (baseDir === rootDir && !watchers.has(abs)) try {
823
+ if (statSync(abs).isDirectory()) addWatcher(abs, true);
824
+ } catch {}
825
+ pendingPaths.add(abs);
826
+ if (!timer) {
827
+ timer = setTimeout(flush, debounceMs);
828
+ timer.unref?.();
829
+ }
830
+ };
831
+ const addWatcher = (dir, recursive) => {
832
+ const watcher = watchFn(dir, { recursive }, (_event, filename) => onEvent(dir, filename));
833
+ watcher.on("error", onWatcherError);
834
+ watcher.unref?.();
835
+ watchers.set(dir, watcher);
836
+ };
837
+ try {
838
+ addWatcher(rootDir, false);
839
+ for (const entry of readdirSync(rootDir, { withFileTypes: true })) {
840
+ if (!entry.isDirectory() || IGNORED_SEGMENTS.has(entry.name)) continue;
841
+ addWatcher(join(rootDir, entry.name), true);
842
+ }
843
+ } catch (err) {
844
+ closeAll();
845
+ console.warn(`[animus-extract] Turbopack dev watcher unavailable (${String(err)}); source edits require a dev-server restart`);
846
+ return null;
847
+ }
848
+ return { close: closeAll };
849
+ }
850
+ //#endregion
671
851
  //#region src/with-animus.ts
672
852
  const __dirname = dirname(fileURLToPath(import.meta.url));
673
853
  let warnedGitignore = false;
854
+ let warnedUnstableTurbopack = false;
674
855
  /**
675
856
  * Wrap a Next.js config to enable Animus static CSS extraction.
676
857
  *
@@ -682,16 +863,30 @@ let warnedGitignore = false;
682
863
  */
683
864
  function withAnimus(options) {
684
865
  if (!options.system) throw new Error("[animus-extract] Missing required option `system`. Provide the path to your SystemInstance module: withAnimus({ system: \"./src/ds.ts\" })");
866
+ if (options.unstable_turbopack && !options.turbopack && !warnedUnstableTurbopack) {
867
+ warnedUnstableTurbopack = true;
868
+ console.warn("[animus-extract] `unstable_turbopack` is deprecated — rename it to `turbopack` (same shape)");
869
+ }
685
870
  return (nextConfig) => {
871
+ if (resolveTurbopackMode(options)) return wireTurbopack(nextConfig, options);
686
872
  const existingWebpack = nextConfig.webpack;
687
873
  return {
688
874
  ...nextConfig,
689
875
  webpack(config, context) {
876
+ if (typeof existingWebpack === "function") config = existingWebpack(config, context);
690
877
  const rootDir = process.cwd();
691
878
  const animusDir = join(rootDir, ".animus");
692
879
  if (!existsSync(animusDir)) mkdirSync(animusDir, { recursive: true });
693
880
  const stubCssPath = join(animusDir, "styles.css");
694
- if (!existsSync(stubCssPath)) writeFileSync(stubCssPath, "@layer anm-global, anm-base, anm-variants, anm-compounds, anm-states, anm-system, anm-custom;\n");
881
+ if (!existsSync(stubCssPath)) {
882
+ const { declaration } = assembleStylesheet({
883
+ layers: options.layers,
884
+ variableCss: "",
885
+ globalCss: "",
886
+ split: true
887
+ });
888
+ writeFileSync(stubCssPath, declaration);
889
+ }
695
890
  if (!warnedGitignore) {
696
891
  warnedGitignore = true;
697
892
  try {
@@ -701,19 +896,13 @@ function withAnimus(options) {
701
896
  }
702
897
  } catch {}
703
898
  }
704
- const plugin = new AnimusWebpackPlugin({
705
- system: options.system,
706
- exclude: options.exclude,
707
- strict: options.strict,
708
- verbose: options.verbose,
709
- prefix: options.prefix,
710
- engine: options.engine
711
- });
899
+ const plugin = new AnimusWebpackPlugin(options);
900
+ const isExternalPackageFile = (filePath) => plugin.getExternalPackageDirs().some((dir) => filePath.startsWith(dir + sep) || filePath === dir);
712
901
  config.plugins = config.plugins || [];
713
902
  config.plugins.push(plugin);
714
903
  config.resolve = config.resolve || {};
715
904
  config.resolve.alias = config.resolve.alias || {};
716
- config.resolve.alias[".animus/styles.css"] = join(rootDir, ".animus", "styles.css");
905
+ config.resolve.alias[ANIMUS_CSS_MODULE_ID] = join(rootDir, ".animus", "styles.css");
717
906
  const systemPropsPath = join(rootDir, ".animus", "system-props.js");
718
907
  config.plugins.push({ apply(compiler) {
719
908
  compiler.hooks.normalModuleFactory.tap("AnimusVirtualResolve", (nmf) => {
@@ -732,24 +921,62 @@ function withAnimus(options) {
732
921
  config.module.rules.push({
733
922
  test: (filePath) => {
734
923
  if (/\.[jt]sx?$/.test(filePath)) return true;
735
- if (/\.mjs$/.test(filePath)) return plugin.getExternalPackageDirs().some((dir) => filePath.startsWith(dir + sep) || filePath === dir);
736
- return false;
924
+ return /\.mjs$/.test(filePath) && isExternalPackageFile(filePath);
737
925
  },
738
926
  exclude: (filePath) => {
739
927
  if (!filePath.includes("node_modules")) return false;
740
- return !plugin.getExternalPackageDirs().some((dir) => filePath.startsWith(dir + sep) || filePath === dir);
928
+ return !isExternalPackageFile(filePath);
741
929
  },
742
930
  enforce: "pre",
743
931
  use: [{
744
932
  loader: actualLoaderPath,
745
- options: { strict: options.strict }
933
+ options: {
934
+ strict: options.strict,
935
+ cssImportTarget: options.cssImportTarget
936
+ }
746
937
  }]
747
938
  });
748
- if (typeof existingWebpack === "function") return existingWebpack(config, context);
749
939
  return config;
750
940
  }
751
941
  };
752
942
  };
753
943
  }
944
+ /**
945
+ * Turbopack wiring: run the full extraction now (artifacts on disk before
946
+ * bundling), start the dev watcher, and merge the generated rules/aliases
947
+ * into `nextConfig.turbopack`. Consumer-managed rules for the same glob are
948
+ * a hard error — silently stacking loaders would be undebuggable.
949
+ */
950
+ async function wireTurbopack(nextConfig, options) {
951
+ const rootDir = process.cwd();
952
+ const session = new ExtractionSession(options);
953
+ session.rootDir = rootDir;
954
+ const builtAliases = buildPathAliasesJson(readTsconfigAliasPairs(rootDir), rootDir);
955
+ if (builtAliases) session.pathAliasesJson = builtAliases.json;
956
+ await runTurbopackPipeline(session);
957
+ if (process.env.NODE_ENV === "development") startTurbopackWatcher(session, rootDir);
958
+ const fragment = buildTurbopackConfig({
959
+ rootDir,
960
+ loaderPath: resolveTurbopackLoaderPath(__dirname),
961
+ options,
962
+ externalSourceEntries: session.externalSourceEntries
963
+ });
964
+ const existing = nextConfig.turbopack ?? {};
965
+ 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`);
966
+ return {
967
+ ...nextConfig,
968
+ turbopack: {
969
+ ...existing,
970
+ rules: {
971
+ ...existing.rules,
972
+ ...fragment.rules
973
+ },
974
+ resolveAlias: {
975
+ ...existing.resolveAlias,
976
+ ...fragment.resolveAlias
977
+ }
978
+ }
979
+ };
980
+ }
754
981
  //#endregion
755
982
  export { withAnimus };