@animus-ui/extract 0.1.8 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +1 -1
  2. package/crates/extract-v2/animus-extract-v2.darwin-arm64.node +0 -0
  3. package/crates/extract-v2/animus-extract-v2.linux-arm64-gnu.node +0 -0
  4. package/crates/extract-v2/animus-extract-v2.linux-x64-gnu.node +0 -0
  5. package/crates/extract-v2/index.d.ts +16 -0
  6. package/dist/index.cjs +73 -2070
  7. package/dist/index.mjs +2 -1996
  8. package/dist/{analyze-project-args.d.ts → pipeline/analyze-project-args.d.ts} +3 -1
  9. package/dist/pipeline/analyze-project-args.d.ts.map +1 -0
  10. package/dist/pipeline/assemble-stylesheet.d.ts.map +1 -0
  11. package/dist/pipeline/asset-placeholders.d.ts.map +1 -0
  12. package/dist/pipeline/content-hash.d.ts.map +1 -0
  13. package/dist/pipeline/core-options.d.ts +162 -0
  14. package/dist/pipeline/core-options.d.ts.map +1 -0
  15. package/dist/pipeline/correlate-external-tokens.d.ts.map +1 -0
  16. package/dist/pipeline/discover-files.d.ts +12 -0
  17. package/dist/pipeline/discover-files.d.ts.map +1 -0
  18. package/dist/{discover-packages.d.ts → pipeline/discover-packages.d.ts} +13 -8
  19. package/dist/pipeline/discover-packages.d.ts.map +1 -0
  20. package/dist/pipeline/dynamic-prop-config.d.ts.map +1 -0
  21. package/dist/{engine-adapter.d.ts → pipeline/engine-adapter.d.ts} +3 -1
  22. package/dist/pipeline/engine-adapter.d.ts.map +1 -0
  23. package/dist/pipeline/engine-retirement.d.ts.map +1 -0
  24. package/dist/pipeline/error-diagnostics.d.ts +32 -0
  25. package/dist/pipeline/error-diagnostics.d.ts.map +1 -0
  26. package/dist/pipeline/external-keyframes.d.ts.map +1 -0
  27. package/dist/{index.d.ts → pipeline/index.d.ts} +12 -1
  28. package/dist/pipeline/index.d.ts.map +1 -0
  29. package/dist/pipeline/manifest-diagnostics.d.ts.map +1 -0
  30. package/dist/{mdx-preprocessor.d.ts → pipeline/mdx-preprocessor.d.ts} +10 -0
  31. package/dist/pipeline/mdx-preprocessor.d.ts.map +1 -0
  32. package/dist/pipeline/path-aliases.d.ts.map +1 -0
  33. package/dist/pipeline/post-process-css.d.ts.map +1 -0
  34. package/dist/pipeline/prefix.d.ts.map +1 -0
  35. package/dist/pipeline/replacement-plans.d.ts.map +1 -0
  36. package/dist/pipeline/resolve-asset.d.ts.map +1 -0
  37. package/dist/pipeline/run-analysis.d.ts.map +1 -0
  38. package/dist/pipeline/source-identity.d.ts.map +1 -0
  39. package/dist/pipeline/source-ingestion.d.ts +173 -0
  40. package/dist/pipeline/source-ingestion.d.ts.map +1 -0
  41. package/dist/pipeline/static-css.d.ts.map +1 -0
  42. package/dist/pipeline/structural-self-check.d.ts +32 -0
  43. package/dist/pipeline/structural-self-check.d.ts.map +1 -0
  44. package/dist/pipeline/svelte-source-adapter.d.ts +88 -0
  45. package/dist/pipeline/svelte-source-adapter.d.ts.map +1 -0
  46. package/dist/{system-config.d.ts → pipeline/system-config.d.ts} +8 -0
  47. package/dist/pipeline/system-config.d.ts.map +1 -0
  48. package/dist/{system-props-module.d.ts → pipeline/system-props-module.d.ts} +4 -3
  49. package/dist/pipeline/system-props-module.d.ts.map +1 -0
  50. package/dist/pipeline/timing-waterfall.d.ts.map +1 -0
  51. package/dist/pipeline/tsconfig-paths.d.ts.map +1 -0
  52. package/dist/pipeline/unit-fallback.d.ts.map +1 -0
  53. package/dist/pipeline/utils.d.ts.map +1 -0
  54. package/dist/pipeline/watch-keys.d.ts.map +1 -0
  55. package/dist/pipeline-lkbOoyAr.mjs +3228 -0
  56. package/dist/pipeline-ojNeABi0.cjs +3685 -0
  57. package/dist/session/extraction-session.d.ts +446 -0
  58. package/dist/session/extraction-session.d.ts.map +1 -0
  59. package/dist/session/index.d.ts +24 -0
  60. package/dist/session/index.d.ts.map +1 -0
  61. package/dist/session/published-set.d.ts +32 -0
  62. package/dist/session/published-set.d.ts.map +1 -0
  63. package/dist/session/resolve-packages.d.ts +8 -0
  64. package/dist/session/resolve-packages.d.ts.map +1 -0
  65. package/dist/session/session-paths.d.ts +131 -0
  66. package/dist/session/session-paths.d.ts.map +1 -0
  67. package/dist/session/singleton.d.ts +124 -0
  68. package/dist/session/singleton.d.ts.map +1 -0
  69. package/dist/session/timing.d.ts +7 -0
  70. package/dist/session/timing.d.ts.map +1 -0
  71. package/dist/session/turbopack-orchestrator.d.ts +43 -0
  72. package/dist/session/turbopack-orchestrator.d.ts.map +1 -0
  73. package/dist/session.cjs +2146 -0
  74. package/dist/session.mjs +2110 -0
  75. package/package.json +18 -6
  76. package/dist/analyze-project-args.d.ts.map +0 -1
  77. package/dist/assemble-stylesheet.d.ts.map +0 -1
  78. package/dist/asset-placeholders.d.ts.map +0 -1
  79. package/dist/content-hash.d.ts.map +0 -1
  80. package/dist/correlate-external-tokens.d.ts.map +0 -1
  81. package/dist/discover-files.d.ts +0 -10
  82. package/dist/discover-files.d.ts.map +0 -1
  83. package/dist/discover-packages.d.ts.map +0 -1
  84. package/dist/dynamic-prop-config.d.ts.map +0 -1
  85. package/dist/engine-adapter.d.ts.map +0 -1
  86. package/dist/engine-retirement.d.ts.map +0 -1
  87. package/dist/external-keyframes.d.ts.map +0 -1
  88. package/dist/index.d.ts.map +0 -1
  89. package/dist/manifest-diagnostics.d.ts.map +0 -1
  90. package/dist/mdx-preprocessor.d.ts.map +0 -1
  91. package/dist/path-aliases.d.ts.map +0 -1
  92. package/dist/post-process-css.d.ts.map +0 -1
  93. package/dist/prefix.d.ts.map +0 -1
  94. package/dist/replacement-plans.d.ts.map +0 -1
  95. package/dist/resolve-asset.d.ts.map +0 -1
  96. package/dist/run-analysis.d.ts.map +0 -1
  97. package/dist/source-identity.d.ts.map +0 -1
  98. package/dist/static-css.d.ts.map +0 -1
  99. package/dist/system-config.d.ts.map +0 -1
  100. package/dist/system-props-module.d.ts.map +0 -1
  101. package/dist/timing-waterfall.d.ts.map +0 -1
  102. package/dist/tsconfig-paths.d.ts.map +0 -1
  103. package/dist/unit-fallback.d.ts.map +0 -1
  104. package/dist/utils.d.ts.map +0 -1
  105. package/dist/watch-keys.d.ts.map +0 -1
  106. /package/dist/{assemble-stylesheet.d.ts → pipeline/assemble-stylesheet.d.ts} +0 -0
  107. /package/dist/{asset-placeholders.d.ts → pipeline/asset-placeholders.d.ts} +0 -0
  108. /package/dist/{content-hash.d.ts → pipeline/content-hash.d.ts} +0 -0
  109. /package/dist/{correlate-external-tokens.d.ts → pipeline/correlate-external-tokens.d.ts} +0 -0
  110. /package/dist/{dynamic-prop-config.d.ts → pipeline/dynamic-prop-config.d.ts} +0 -0
  111. /package/dist/{engine-retirement.d.ts → pipeline/engine-retirement.d.ts} +0 -0
  112. /package/dist/{external-keyframes.d.ts → pipeline/external-keyframes.d.ts} +0 -0
  113. /package/dist/{manifest-diagnostics.d.ts → pipeline/manifest-diagnostics.d.ts} +0 -0
  114. /package/dist/{path-aliases.d.ts → pipeline/path-aliases.d.ts} +0 -0
  115. /package/dist/{post-process-css.d.ts → pipeline/post-process-css.d.ts} +0 -0
  116. /package/dist/{prefix.d.ts → pipeline/prefix.d.ts} +0 -0
  117. /package/dist/{replacement-plans.d.ts → pipeline/replacement-plans.d.ts} +0 -0
  118. /package/dist/{resolve-asset.d.ts → pipeline/resolve-asset.d.ts} +0 -0
  119. /package/dist/{run-analysis.d.ts → pipeline/run-analysis.d.ts} +0 -0
  120. /package/dist/{source-identity.d.ts → pipeline/source-identity.d.ts} +0 -0
  121. /package/dist/{static-css.d.ts → pipeline/static-css.d.ts} +0 -0
  122. /package/dist/{timing-waterfall.d.ts → pipeline/timing-waterfall.d.ts} +0 -0
  123. /package/dist/{tsconfig-paths.d.ts → pipeline/tsconfig-paths.d.ts} +0 -0
  124. /package/dist/{unit-fallback.d.ts → pipeline/unit-fallback.d.ts} +0 -0
  125. /package/dist/{utils.d.ts → pipeline/utils.d.ts} +0 -0
  126. /package/dist/{watch-keys.d.ts → pipeline/watch-keys.d.ts} +0 -0
package/dist/index.mjs CHANGED
@@ -1,1996 +1,2 @@
1
- import { createRequire } from "node:module";
2
- import { createHash } from "crypto";
3
- import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "fs";
4
- import { dirname, extname, isAbsolute, join, relative, resolve } from "path";
5
- import { realpathSync as realpathSync$1 } from "node:fs";
6
- import path, { basename, dirname as dirname$1, join as join$1, normalize } from "node:path";
7
- import { createRequire as createRequire$1 } from "module";
8
- import browserslist from "browserslist";
9
- import { browserslistToTargets, transform } from "lightningcss";
10
- import { isUnitlessProperty } from "@animus-ui/properties";
11
- //#region \0rolldown/runtime.js
12
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
13
- //#endregion
14
- //#region pipeline/assemble-stylesheet.ts
15
- /**
16
- * The 7 Animus cascade layers. Always prefixed with `anm-` to avoid
17
- * collision with other frameworks' layer names (e.g., Tailwind's `base`).
18
- */
19
- const ANIMUS_LAYERS = [
20
- "anm-global",
21
- "anm-base",
22
- "anm-variants",
23
- "anm-compounds",
24
- "anm-states",
25
- "anm-system",
26
- "anm-custom"
27
- ];
28
- /**
29
- * Build the `@layer` declaration line.
30
- *
31
- * When `isCustom` is false (default layers), uses ANIMUS_LAYERS directly.
32
- * When `isCustom` is true, passes names through as-is — the consumer already
33
- * wrote the final names including the `anm-` prefixed entries.
34
- */
35
- function buildLayerDeclaration(layers, isCustom) {
36
- return `@layer ${(isCustom ? layers : [...ANIMUS_LAYERS]).join(", ")};\n`;
37
- }
38
- /**
39
- * Validate that a consumer `layers` array contains all 7 Animus layers
40
- * in the correct relative order. Consumer layers may be interleaved.
41
- *
42
- * @throws Error with descriptive message on violation
43
- */
44
- function validateLayerOrder(layers) {
45
- const expected = [...ANIMUS_LAYERS];
46
- let cursor = 0;
47
- for (const layer of layers) if (cursor < expected.length && layer === expected[cursor]) cursor++;
48
- if (cursor < expected.length) {
49
- const missing = expected.slice(cursor);
50
- const found = expected.slice(0, cursor);
51
- if (!expected.every((l) => layers.includes(l))) {
52
- const absent = expected.filter((l) => !layers.includes(l));
53
- throw new Error(`[animus-extract] Custom layers missing required layers: ${absent.join(", ")}. All 7 Animus layers must be present: ${expected.join(", ")}`);
54
- }
55
- throw new Error(`[animus-extract] Custom layers have wrong order. Found ${found.join(", ")} but then expected ${missing[0]}. Required order: (${expected.join(" < ")}). You may interleave custom layers but must preserve this subsequence.`);
56
- }
57
- }
58
- /**
59
- * Strip a leading `@layer ...;` declaration line from CSS if present.
60
- * The Rust crate embeds this in prod-mode output; we strip it so the
61
- * shared assembler controls placement.
62
- */
63
- function stripLeadingLayerDeclaration(css) {
64
- return css.replace(/^@layer\s+[^;{]+;\s*\n?/, "");
65
- }
66
- function assembleStylesheet(options) {
67
- const hasCustomLayers = !!options.layers;
68
- const layers = options.layers ?? ANIMUS_LAYERS;
69
- if (options.layers) validateLayerOrder(options.layers);
70
- const declaration = buildLayerDeclaration(layers, hasCustomLayers);
71
- const variables = options.variableCss || "";
72
- const componentCss = options.componentCss ? stripLeadingLayerDeclaration(options.componentCss) : "";
73
- const body = [options.globalCss || "", componentCss].filter(Boolean).join("\n");
74
- if (options.split) return {
75
- declaration,
76
- variables,
77
- body
78
- };
79
- return [
80
- declaration,
81
- variables,
82
- body
83
- ].filter(Boolean).join("\n");
84
- }
85
- //#endregion
86
- //#region pipeline/analyze-project-args.ts
87
- /** @internal */
88
- function buildAnalyzeProjectArgs(inputs) {
89
- return [
90
- inputs.filesJson,
91
- inputs.scalesJson,
92
- inputs.variableMapJson,
93
- inputs.contextualVarsJson,
94
- inputs.propConfigJson,
95
- inputs.groupRegistryJson,
96
- inputs.packageResolutionJson,
97
- inputs.devMode,
98
- inputs.emitterConfigJson,
99
- inputs.selectorAliasesJson,
100
- null,
101
- inputs.globalStyleBlocksJson,
102
- inputs.pathAliasesJson,
103
- inputs.keyframesJson,
104
- inputs.staticCssJson,
105
- inputs.conditionAliasesJson,
106
- inputs.externalDirsJson
107
- ];
108
- }
109
- //#endregion
110
- //#region pipeline/engine-adapter.ts
111
- /**
112
- * Build the engine-agnostic `engineApi()` both plugins call. Returns a function
113
- * (invoked per call site) so `isV2()` and `loadNativeEngine()` re-evaluate each
114
- * time — next reads its engine choice and native module dynamically from
115
- * globalThis.
116
- */
117
- function createV2EngineApi(deps) {
118
- const { label, isV2, loadNativeEngine, store } = deps;
119
- return () => {
120
- if (!isV2()) return loadNativeEngine();
121
- const native = loadNativeEngine();
122
- return {
123
- loadSystemModule: (...args) => native.loadSystemModule(...args),
124
- scanKeyframesExports: (entryPath, rootDir) => native.scanKeyframesExports(entryPath, rootDir) ?? null,
125
- analyzeProject: (filesJsonRaw, scalesJson, variableMapJson, contextualVarsJson, propConfigJson, groupRegistryJson, packageResolutionJson, devMode, emitterConfigJson, selectorAliasesJson, _selectorOrderJson, globalStyleBlocksJson, pathAliasesJson, keyframesJson, staticCssJson, conditionAliasesJson, externalDirsJson) => {
126
- const filesJson = deps.rehydrateFilesJson ? deps.rehydrateFilesJson(filesJsonRaw) : filesJsonRaw;
127
- const sent = /* @__PURE__ */ new Map();
128
- for (const entry of JSON.parse(filesJson)) sent.set(entry.path, entry.source);
129
- store.setSentSources(sent);
130
- const emitterConfig = emitterConfigJson ? JSON.parse(emitterConfigJson) : {};
131
- store.setEngine(null);
132
- const config = {
133
- runtimeImport: emitterConfig.runtime_import ?? void 0,
134
- cssModuleId: emitterConfig.css_module_id ?? void 0,
135
- systemPropsModuleId: emitterConfig.system_props_module_id ?? void 0,
136
- themeJson: scalesJson,
137
- variableMapJson,
138
- contextualVarsJson: contextualVarsJson ?? void 0,
139
- configJson: propConfigJson,
140
- groupRegistryJson,
141
- selectorAliasesJson: selectorAliasesJson ?? void 0,
142
- conditionAliasesJson: conditionAliasesJson ?? void 0,
143
- globalStyleBlocksJson: globalStyleBlocksJson ?? void 0,
144
- keyframesJson: keyframesJson ?? void 0,
145
- packageResolutionJson: packageResolutionJson ?? void 0,
146
- pathAliasesJson: pathAliasesJson ?? void 0,
147
- staticCssJson: staticCssJson ?? void 0,
148
- externalDirsJson: externalDirsJson ?? void 0,
149
- devMode
150
- };
151
- const engine = new native.ExtractEngine(config);
152
- store.setEngine(engine);
153
- return engine.analyze(filesJson);
154
- },
155
- transformFile: (source, path, _manifest) => {
156
- const engine = store.getEngine();
157
- if (!engine) throw new Error(`[${label}] v2 transform before analyze — engine instance not initialized`);
158
- const sentMap = store.getSentSources();
159
- if (deps.passThroughUnknownPaths && sentMap && !sentMap.has(path)) return {
160
- code: source,
161
- hasComponents: false
162
- };
163
- const sent = sentMap?.get(path);
164
- if (sent !== void 0 && sent !== source && !store.getDriftWarned()) {
165
- store.setDriftWarned(true);
166
- console.warn(`[${label}] v2: transform-time source for ${path} differs from analyze-time source — an upstream transform may be reverted`);
167
- }
168
- return JSON.parse(engine.transformFile(path));
169
- },
170
- clearAnalysisCache: () => {
171
- const engine = store.getEngine();
172
- if (engine) engine.clearCache();
173
- store.setEngine(null);
174
- }
175
- };
176
- };
177
- }
178
- //#endregion
179
- //#region pipeline/engine-retirement.ts
180
- /**
181
- * Loud rejection of the retired v1 extraction engine (openspec:
182
- * retire-extract-v1).
183
- *
184
- * v2 is the only engine. Selecting v1 — via a plugin `engine: 'v1'` option or
185
- * the `ANIMUS_ENGINE=v1` fixture env override — is a build-stopping error, never
186
- * a silent upgrade to v2 (design D1: a silent flip would let a pinned consumer's
187
- * build change engines without notice). Both extraction plugins call this before
188
- * any engine selection so the rejection is uniform and the message is defined
189
- * once.
190
- */
191
- /** The one canonical retirement message, shared verbatim by both plugins and
192
- * the env path (tasks.md cross-cutting: define once, reuse). */
193
- const RETIRED_ENGINE_MESSAGE = "animus: extraction engine 'v1' was retired (openspec: retire-extract-v1) — v2 is the only engine; remove the engine option / ANIMUS_ENGINE override";
194
- /**
195
- * Throw the canonical retirement error when v1 is selected through either
196
- * surface. Accepts the raw engine option as a string because the option type no
197
- * longer admits `'v1'` — callers cast it so a stale `'v1'` config still fails
198
- * loud at runtime instead of being silently coerced.
199
- */
200
- function assertNoRetiredEngineSelection(engineOption) {
201
- if (engineOption === "v1" || process.env.ANIMUS_ENGINE === "v1") throw new Error(RETIRED_ENGINE_MESSAGE);
202
- }
203
- //#endregion
204
- //#region pipeline/content-hash.ts
205
- /**
206
- * MD5 content hash used for file-change detection (HMR diffing) by both
207
- * extraction plugins, and for content-addressing copied asset bytes. The
208
- * algorithm/encoding is a cross-plugin contract: cache keys written by one
209
- * build path must compare equal in the next. Accepts a Buffer directly so
210
- * binary sources hash without an intermediate string copy.
211
- */
212
- function contentHash(source) {
213
- return createHash("md5").update(source).digest("hex");
214
- }
215
- //#endregion
216
- //#region pipeline/external-keyframes.ts
217
- /** Stable codes for external keyframes discovery. */
218
- const KEYFRAMES_EXTERNAL_ENTRY_FAILED = "animus.keyframes.external-entry-failed";
219
- const KEYFRAMES_EXPORT_COLLISION = "animus.keyframes.export-collision";
220
- /**
221
- * Merge `Keyframes` collections discovered from external package entries into
222
- * the consumer system's collections. Keyframes are the sole carve-out from
223
- * the consumer-config singular-authority rule: nothing but branded
224
- * collections is read from an entry, an entry that fails to evaluate
225
- * degrades to a coded diagnostic naming it, and a collection whose export
226
- * name collides keeps the earlier (consumer-first) collection with a coded
227
- * diagnostic instead of silently reordering names.
228
- */
229
- function mergeExternalKeyframes(scan, consumerKeyframesJson, externalEntries, rootDir) {
230
- const merged = {};
231
- if (consumerKeyframesJson) try {
232
- Object.assign(merged, JSON.parse(consumerKeyframesJson));
233
- } catch {
234
- return {
235
- keyframesJson: consumerKeyframesJson,
236
- diagnostics: []
237
- };
238
- }
239
- const diagnostics = [];
240
- const seenEntries = /* @__PURE__ */ new Set();
241
- for (const entryPath of externalEntries) {
242
- if (seenEntries.has(entryPath)) continue;
243
- seenEntries.add(entryPath);
244
- let scanned;
245
- try {
246
- scanned = scan(entryPath, rootDir);
247
- } catch (error) {
248
- diagnostics.push({
249
- file: entryPath,
250
- component: "keyframes",
251
- kind: "warn",
252
- message: `external package entry failed the keyframes scan — its collections are invisible to extraction: ${String(error)}. Keyframe collections must be reachable from the package's definition entry without evaluating framework re-exports: export them (directly or as a named re-export) from the definition entry, and avoid \`export *\` of framework packages there (${KEYFRAMES_EXTERNAL_ENTRY_FAILED})`,
253
- code: KEYFRAMES_EXTERNAL_ENTRY_FAILED,
254
- severity: "warn"
255
- });
256
- continue;
257
- }
258
- if (!scanned) continue;
259
- let collections;
260
- try {
261
- collections = JSON.parse(scanned);
262
- } catch {
263
- continue;
264
- }
265
- for (const [exportName, collection] of Object.entries(collections)) {
266
- const existing = merged[exportName];
267
- if (existing !== void 0) {
268
- if (JSON.stringify(existing) !== JSON.stringify(collection)) diagnostics.push({
269
- file: entryPath,
270
- component: exportName,
271
- kind: "warn",
272
- message: `keyframes collection export '${exportName}' collides with an earlier collection of the same name — the earlier one wins (${KEYFRAMES_EXPORT_COLLISION})`,
273
- code: KEYFRAMES_EXPORT_COLLISION,
274
- severity: "warn"
275
- });
276
- continue;
277
- }
278
- merged[exportName] = collection;
279
- }
280
- }
281
- return {
282
- keyframesJson: Object.keys(merged).length > 0 ? JSON.stringify(merged) : null,
283
- diagnostics
284
- };
285
- }
286
- //#endregion
287
- //#region pipeline/discover-files.ts
288
- /**
289
- * Recursively discover source files under `dir`, skipping any path that
290
- * contains one of `excludePatterns` (matched as substrings of both the full
291
- * and root-relative path) and keeping files whose extension is in
292
- * `extensionsSet`.
293
- *
294
- * Single authoritative copy for both extraction plugins.
295
- */
296
- function discoverFiles(dir, rootDir, excludePatterns, extensionsSet) {
297
- const results = [];
298
- let entries;
299
- try {
300
- entries = readdirSync(dir, { encoding: "utf8" });
301
- } catch {
302
- return results;
303
- }
304
- for (const entry of entries) {
305
- const fullPath = join(dir, entry);
306
- const relativePath = relative(rootDir, fullPath);
307
- if (excludePatterns.some((pattern) => fullPath.includes(pattern) || relativePath.includes(pattern))) continue;
308
- let stat;
309
- try {
310
- stat = statSync(fullPath);
311
- } catch {
312
- continue;
313
- }
314
- if (stat.isDirectory()) results.push(...discoverFiles(fullPath, rootDir, excludePatterns, extensionsSet));
315
- else if (extensionsSet.has(extname(entry))) results.push(fullPath);
316
- }
317
- return results;
318
- }
319
- //#endregion
320
- //#region pipeline/source-identity.ts
321
- /**
322
- * SourceId derivation authority + allowlist membership (openspec:
323
- * external-source-watch-ingestion, design D1/D2/D5).
324
- *
325
- * One shared derivation produces the canonical source identity consumed by
326
- * discovery, watch ingestion, ownership, deletion pruning, and diagnostics
327
- * — raw event paths are lookup INPUTS only, so event spelling (symlink
328
- * alias vs canonical, lexical vs realpath) can never fork identity.
329
- *
330
- * The containment and volume helpers are pure and path-API-injectable so
331
- * Windows semantics are unit-testable via `path.win32` on any host; runtime
332
- * callers use the ambient platform implementation.
333
- */
334
- /**
335
- * Structural containment: `target` is the root itself or a descendant of it,
336
- * decided by `relative()` shape — never by string prefixing, so `/ui` can
337
- * never claim `/ui-old`, and a cross-drive win32 target (whose relative()
338
- * result is absolute) is never contained.
339
- */
340
- function isPathWithinRoot(root, target, pathApi = path) {
341
- const rel = pathApi.relative(root, target);
342
- return rel === "" || !pathApi.isAbsolute(rel) && rel !== ".." && !rel.startsWith(".." + pathApi.sep);
343
- }
344
- /**
345
- * Cross-volume gate (design D5): two paths share a platform path root
346
- * (win32 drive letter or UNC share; always true on posix). Compared
347
- * case-insensitively — win32 drive letters are case-insensitive.
348
- */
349
- function sharesVolumeRoot(a, b, pathApi = path) {
350
- const volumeOf = (p) => pathApi.parse(pathApi.resolve(p)).root.toLowerCase();
351
- return volumeOf(a) === volumeOf(b);
352
- }
353
- function canonicalize(p) {
354
- try {
355
- return realpathSync$1(p);
356
- } catch {
357
- return null;
358
- }
359
- }
360
- function createSourceIdentity(rootDir) {
361
- const normalizedRootDir = path.normalize(rootDir);
362
- const canonicalRootDir = canonicalize(normalizedRootDir) ?? normalizedRootDir;
363
- const roots = [];
364
- const aliases = /* @__PURE__ */ new Map();
365
- const owningRecord = (canonicalPath) => {
366
- let owner = null;
367
- for (const record of roots) {
368
- if (!isPathWithinRoot(record.canonical, canonicalPath)) continue;
369
- if (!owner || record.canonical.length > owner.canonical.length) owner = record;
370
- }
371
- return owner;
372
- };
373
- return {
374
- rootDir: normalizedRootDir,
375
- canonicalRootDir,
376
- registerExternalRoot(root) {
377
- const asRegistered = path.normalize(root);
378
- const canonical = canonicalize(asRegistered) ?? asRegistered;
379
- const existing = roots.find((record) => record.canonical === canonical);
380
- if (existing) return existing.canonical;
381
- roots.push({
382
- asRegistered,
383
- canonical
384
- });
385
- return canonical;
386
- },
387
- externalRoots() {
388
- return roots.map((record) => record.canonical);
389
- },
390
- resolveSourceId(inputPath) {
391
- const lexical = path.normalize(inputPath);
392
- const canonical = canonicalize(lexical);
393
- if (canonical === null) return null;
394
- const owner = owningRecord(canonical);
395
- const ownedBy = (record) => {
396
- const pathInRoot = path.relative(record.canonical, canonical);
397
- return {
398
- sourceKey: path.relative(normalizedRootDir, path.join(record.asRegistered, pathInRoot)),
399
- owningRoot: record.canonical,
400
- pathInRoot
401
- };
402
- };
403
- const ownerIsMoreSpecific = owner !== null && owner.canonical.length > canonicalRootDir.length;
404
- let resolved;
405
- if (!ownerIsMoreSpecific && isPathWithinRoot(canonicalRootDir, canonical)) {
406
- const key = path.relative(canonicalRootDir, canonical);
407
- resolved = {
408
- sourceKey: key,
409
- owningRoot: null,
410
- pathInRoot: key
411
- };
412
- } else if (owner) resolved = ownedBy(owner);
413
- else return null;
414
- aliases.set(lexical, resolved);
415
- aliases.set(canonical, resolved);
416
- return resolved;
417
- },
418
- resolveDeletedSourceId(inputPath) {
419
- return aliases.get(path.normalize(inputPath)) ?? null;
420
- },
421
- containingExternalRoot(inputPath) {
422
- const lexical = path.normalize(inputPath);
423
- let owner = null;
424
- for (const record of roots) {
425
- if (!(isPathWithinRoot(record.canonical, lexical) || isPathWithinRoot(record.asRegistered, lexical))) continue;
426
- if (!owner || record.canonical.length > owner.canonical.length) owner = record;
427
- }
428
- if (owner) return owner.canonical;
429
- const canonical = canonicalize(lexical);
430
- if (canonical === null) return null;
431
- for (const record of roots) {
432
- if (!isPathWithinRoot(record.canonical, canonical)) continue;
433
- if (!owner || record.canonical.length > owner.canonical.length) owner = record;
434
- }
435
- return owner?.canonical ?? null;
436
- }
437
- };
438
- }
439
- //#endregion
440
- //#region pipeline/discover-packages.ts
441
- /**
442
- * Walk up from a resolved package entry file to the nearest directory
443
- * containing a package.json (the package root). Stops at the filesystem
444
- * root and returns it if no package.json is found on the way up.
445
- */
446
- function findPackageRoot(absEntryPath) {
447
- let pkgRoot = dirname(absEntryPath);
448
- while (pkgRoot !== dirname(pkgRoot) && !existsSync(join(pkgRoot, "package.json"))) pkgRoot = dirname(pkgRoot);
449
- return pkgRoot;
450
- }
451
- /**
452
- * Exclusion fragments for discovery inside an external package's src/,
453
- * matched against paths RELATIVE TO the src dir. Per spec
454
- * external-package-file-discovery, the filters apply "within the package" —
455
- * an npm-installed package whose own location is under node_modules (or a
456
- * repo path containing 'dist') must still have its sources discovered.
457
- */
458
- const PACKAGE_SRC_EXCLUDES = [
459
- "dist",
460
- "node_modules",
461
- ".test.",
462
- ".spec."
463
- ];
464
- /** True when a PACKAGE-RELATIVE path matches a package-source exclusion
465
- * fragment (the "within the package" filter — see PACKAGE_SRC_EXCLUDES). */
466
- function isExcludedPackageRelativePath(rel) {
467
- return PACKAGE_SRC_EXCLUDES.some((pattern) => rel.includes(pattern));
468
- }
469
- /**
470
- * THE package-source walk (guardrail G1 — one policy): shared discovery with
471
- * no consumer patterns, then exclusion by package-relative path so fragments
472
- * in the package's own location can't blank out its sources. Used by the
473
- * collector, hosts' dirty-root rewalks, and watch-path classification alike.
474
- */
475
- function walkPackageSources(packageDir, extensionsSet) {
476
- return discoverFiles(packageDir, packageDir, [], extensionsSet).filter((absPath) => !isExcludedPackageRelativePath(relative(packageDir, absPath)));
477
- }
478
- /** Exclusions for the compiled-output fallback when a package does not ship src/. */
479
- const PACKAGE_OUTPUT_EXCLUDES = [
480
- "node_modules",
481
- ".test.",
482
- ".spec.",
483
- ".d.ts",
484
- ".map"
485
- ];
486
- const isFile = (path) => {
487
- try {
488
- return statSync(path).isFile();
489
- } catch {
490
- return false;
491
- }
492
- };
493
- /**
494
- * Resolve an absolute-path specifier (a relative `includes` entry already
495
- * resolved against the system file) without a bundler: the path itself when it
496
- * is a file, otherwise the extension and directory-index candidates built from
497
- * the caller's extension set. Used only as a fallback — a bundler resolver that
498
- * answers first still wins, since it also knows aliases and `.js`→`.ts` mapping.
499
- */
500
- function resolveAbsolutePathSpecifier(absSpecifier, extensionsSet) {
501
- return [
502
- absSpecifier,
503
- ...Array.from(extensionsSet, (ext) => absSpecifier + ext),
504
- ...Array.from(extensionsSet, (ext) => join(absSpecifier, `index${ext}`))
505
- ].find(isFile) ?? null;
506
- }
507
- /** The package-name half of a bare specifier (`@scope/name/sub` → `@scope/name`). */
508
- function bareSpecifierPackageName(specifier) {
509
- const segments = specifier.split("/");
510
- return specifier.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0];
511
- }
512
- function sourceEntryForSpecifier(specifier, srcDir, extensionsSet) {
513
- if (isAbsolute(specifier)) {
514
- const resolved = resolveAbsolutePathSpecifier(specifier, extensionsSet);
515
- if (!resolved) return null;
516
- return isPathWithinRoot(srcDir, resolved) ? resolved : null;
517
- }
518
- const packageName = bareSpecifierPackageName(specifier);
519
- return resolveAbsolutePathSpecifier(join(srcDir, specifier.slice(packageName.length + 1) || "index"), extensionsSet);
520
- }
521
- /**
522
- * The D13 dist-freshness check: applicable only when a specifier has BOTH a
523
- * walked src/ tree and a resolved dist entry that exists on disk outside that
524
- * tree; stale when the dist entry's mtime is older than the newest walked
525
- * source file's. Not applicable (never stale) when the entry resolves inside
526
- * src/ (no dist to skew), when the entry file is missing, or when no source
527
- * file's mtime is readable. Detection only — reporting is the caller's policy.
528
- */
529
- function distEntryIsStale(absEntry, srcDir, srcFiles) {
530
- if (isPathWithinRoot(srcDir, absEntry)) return false;
531
- let distMtime;
532
- try {
533
- distMtime = statSync(absEntry).mtimeMs;
534
- } catch {
535
- return false;
536
- }
537
- let newestSrcMtime = -Infinity;
538
- for (const srcFile of srcFiles) try {
539
- newestSrcMtime = Math.max(newestSrcMtime, statSync(srcFile).mtimeMs);
540
- } catch {}
541
- return distMtime < newestSrcMtime;
542
- }
543
- /**
544
- * Shared external-package source collection (spec:
545
- * external-package-file-discovery), consumed by both extraction plugins.
546
- * For each specifier: resolve to an absolute entry (null/throw → silently
547
- * skip; an absolute-path specifier falls back to a filesystem probe), walk up
548
- * to the package root, then either discover sources under src/ (redirecting
549
- * module resolution to src/index.ts when present) or fall back to ingesting
550
- * the resolved entry file itself.
551
- *
552
- * The only bundler-specific seams are callbacks: specifier resolution,
553
- * dedup against already-ingested files, per-file preprocessing (MDX), and
554
- * the unreadable-file warning. Hashing and caching stay in the plugins —
555
- * their cache policies legitimately differ.
556
- *
557
- * Alongside the aggregates, one `ExternalPackageOutcome` per declared
558
- * specifier records what that specifier produced. Collection never reports on
559
- * them — any warning or gate is the caller's policy.
560
- */
561
- async function collectExternalPackageSources(opts) {
562
- const { specifiers, resolveSpecifier, rootDir, extensionsSet, hasEntry, preprocessFile, onUnreadable, onPackageResolved } = opts;
563
- const entries = [];
564
- const pushed = /* @__PURE__ */ new Set();
565
- const packageMap = {};
566
- const sourceEntries = /* @__PURE__ */ new Map();
567
- const keyframesScanEntries = /* @__PURE__ */ new Map();
568
- const packageDirs = [];
569
- const dirOwnerSets = {};
570
- const fileOwners = {};
571
- const outcomes = [];
572
- const claimDir = (dir, specifier) => {
573
- (dirOwnerSets[dir] ??= []).push(specifier);
574
- };
575
- const alreadyIngested = (relPath) => hasEntry(relPath) || pushed.has(relPath);
576
- for (const specifier of specifiers) {
577
- let absEntry;
578
- try {
579
- absEntry = await resolveSpecifier(specifier);
580
- } catch {
581
- absEntry = null;
582
- }
583
- if (!absEntry && isAbsolute(specifier)) absEntry = resolveAbsolutePathSpecifier(specifier, extensionsSet);
584
- if (!absEntry) {
585
- outcomes.push({
586
- specifier,
587
- outcome: "unresolvable",
588
- fileCount: 0
589
- });
590
- continue;
591
- }
592
- const srcDir = join(findPackageRoot(absEntry), "src");
593
- let fileCount = 0;
594
- let staleDist = false;
595
- if (existsSync(srcDir)) {
596
- packageDirs.push(srcDir);
597
- claimDir(srcDir, specifier);
598
- onPackageResolved?.(specifier, srcDir);
599
- const srcEntry = sourceEntryForSpecifier(specifier, srcDir, extensionsSet);
600
- if (srcEntry) {
601
- packageMap[specifier] = relative(rootDir, srcEntry);
602
- sourceEntries.set(specifier, srcEntry);
603
- } else packageMap[specifier] = relative(rootDir, absEntry);
604
- keyframesScanEntries.set(specifier, srcEntry ?? absEntry);
605
- if (!isAbsolute(specifier)) {
606
- const packageName = bareSpecifierPackageName(specifier);
607
- if (packageName !== specifier && !(packageName in packageMap)) {
608
- const rootEntry = sourceEntryForSpecifier(packageName, srcDir, extensionsSet);
609
- if (rootEntry) {
610
- packageMap[packageName] = relative(rootDir, rootEntry);
611
- sourceEntries.set(packageName, rootEntry);
612
- keyframesScanEntries.set(packageName, rootEntry);
613
- }
614
- }
615
- }
616
- const pkgFiles = walkPackageSources(srcDir, extensionsSet);
617
- staleDist = distEntryIsStale(absEntry, srcDir, pkgFiles);
618
- for (const pkgFile of pkgFiles) {
619
- const relPath = relative(rootDir, pkgFile);
620
- if (alreadyIngested(relPath)) {
621
- fileCount++;
622
- continue;
623
- }
624
- let source;
625
- try {
626
- source = readFileSync(pkgFile, "utf-8");
627
- } catch (err) {
628
- onUnreadable(relPath, err);
629
- continue;
630
- }
631
- const processed = await preprocessFile(source, relPath, pkgFile);
632
- if (!processed) continue;
633
- entries.push({
634
- path: processed.relPath,
635
- source: processed.source
636
- });
637
- pushed.add(processed.relPath);
638
- fileOwners[processed.relPath] ??= specifier;
639
- fileCount++;
640
- }
641
- } else {
642
- const outputDir = dirname(absEntry);
643
- packageDirs.push(outputDir);
644
- claimDir(outputDir, specifier);
645
- onPackageResolved?.(specifier, outputDir);
646
- packageMap[specifier] = relative(rootDir, absEntry);
647
- keyframesScanEntries.set(specifier, absEntry);
648
- const outputExtensions = new Set(extensionsSet);
649
- outputExtensions.add(extname(absEntry));
650
- const outputFiles = discoverFiles(outputDir, outputDir, [], outputExtensions).filter((file) => {
651
- const relToOutput = relative(outputDir, file);
652
- return !PACKAGE_OUTPUT_EXCLUDES.some((pattern) => relToOutput.includes(pattern));
653
- });
654
- if (!outputFiles.includes(absEntry)) outputFiles.unshift(absEntry);
655
- for (const outputFile of outputFiles) {
656
- const outputRelPath = relative(rootDir, outputFile);
657
- if (alreadyIngested(outputRelPath)) {
658
- fileCount++;
659
- continue;
660
- }
661
- let source;
662
- try {
663
- source = readFileSync(outputFile, "utf-8");
664
- } catch (err) {
665
- onUnreadable(outputRelPath, err);
666
- continue;
667
- }
668
- const processed = await preprocessFile(source, outputRelPath, outputFile);
669
- if (!processed) continue;
670
- entries.push({
671
- path: processed.relPath,
672
- source: processed.source
673
- });
674
- pushed.add(processed.relPath);
675
- fileOwners[processed.relPath] ??= specifier;
676
- fileCount++;
677
- }
678
- }
679
- outcomes.push({
680
- specifier,
681
- outcome: staleDist ? "stale-dist" : fileCount > 0 ? "resolved" : "empty",
682
- fileCount
683
- });
684
- }
685
- return {
686
- entries,
687
- packageMap,
688
- sourceEntries,
689
- keyframesScanEntries,
690
- packageDirs,
691
- dirOwnerSets,
692
- fileOwners,
693
- outcomes
694
- };
695
- }
696
- /** First-declared specifier per package dir — the single-value ownership
697
- * view correlation consumers key on, derived from `dirOwnerSets`. */
698
- function firstOwners(dirOwnerSets) {
699
- const owners = {};
700
- for (const [dir, specifiers] of Object.entries(dirOwnerSets)) if (specifiers.length > 0) owners[dir] = specifiers[0];
701
- return owners;
702
- }
703
- /**
704
- * Excise a set of rejected specifiers from a collection result ATOMICALLY
705
- * (openspec: external-source-watch-ingestion, design D5): the returned copy
706
- * carries no entries, package-map targets, source entries, package dirs, or
707
- * ownership for the rejected packages — a package dir is removed only when
708
- * EVERY specifier that claimed it is rejected (a dir-level rejection, such
709
- * as the cross-volume gate, always rejects them together). Outcome records
710
- * are reporting inputs, not membership, and pass through untouched.
711
- */
712
- function excludeCollectedPackages(collected, rejectedSpecifiers, rootDir) {
713
- if (rejectedSpecifiers.size === 0) return collected;
714
- const rejectedDirs = Object.entries(collected.dirOwnerSets).filter(([, specs]) => specs.every((s) => rejectedSpecifiers.has(s))).map(([dir]) => dir);
715
- const underRejectedDir = (absPath) => rejectedDirs.some((dir) => isPathWithinRoot(dir, absPath));
716
- const targetRejected = (specifier, absTarget) => rejectedSpecifiers.has(specifier) || underRejectedDir(absTarget);
717
- const packageMap = {};
718
- for (const [specifier, relTarget] of Object.entries(collected.packageMap)) {
719
- if (targetRejected(specifier, resolve(rootDir, relTarget))) continue;
720
- packageMap[specifier] = relTarget;
721
- }
722
- const sourceEntries = /* @__PURE__ */ new Map();
723
- for (const [specifier, absEntry] of collected.sourceEntries) {
724
- if (targetRejected(specifier, absEntry)) continue;
725
- sourceEntries.set(specifier, absEntry);
726
- }
727
- const keyframesScanEntries = /* @__PURE__ */ new Map();
728
- for (const [specifier, absEntry] of collected.keyframesScanEntries) {
729
- if (targetRejected(specifier, absEntry)) continue;
730
- keyframesScanEntries.set(specifier, absEntry);
731
- }
732
- const dirOwnerSets = {};
733
- for (const [dir, specs] of Object.entries(collected.dirOwnerSets)) {
734
- const kept = specs.filter((s) => !rejectedSpecifiers.has(s));
735
- if (kept.length === 0) continue;
736
- dirOwnerSets[dir] = kept;
737
- }
738
- const fileOwners = {};
739
- for (const [relPath, owner] of Object.entries(collected.fileOwners)) {
740
- if (rejectedSpecifiers.has(owner)) continue;
741
- fileOwners[relPath] = owner;
742
- }
743
- return {
744
- entries: collected.entries.filter((entry) => !rejectedSpecifiers.has(collected.fileOwners[entry.path])),
745
- packageMap,
746
- sourceEntries,
747
- keyframesScanEntries,
748
- packageDirs: collected.packageDirs.filter((dir) => !rejectedDirs.includes(dir)),
749
- dirOwnerSets,
750
- fileOwners,
751
- outcomes: collected.outcomes
752
- };
753
- }
754
- /**
755
- * Extract external DS package names from inheritance declarations in the
756
- * system file.
757
- *
758
- * Supports four forms:
759
- * - Primary: `createSystem(...).extend(identifier)` chain calls
760
- * (repeatable, mixable with `.from()` links; each call
761
- * contributes one source; a library-bundle identifier
762
- * traces its base import)
763
- * - Deprecated chain: `createSystem(...).from(identifier)` chain calls
764
- * - Deprecated alias: `createSystem({ includes: [identifier, ...] })`
765
- * - Legacy: `.includes([identifier, ...])` chain method (RC
766
- * migration fallback)
767
- *
768
- * For each identifier found, traces back to its import declaration and returns
769
- * the import specifier: a bare specifier normalized to its package name, a
770
- * relative specifier resolved against the system file's directory into an
771
- * absolute path (so a sibling package referenced by path contributes discovery
772
- * too). Only packages explicitly declared through one of these forms are
773
- * treated as external DS dependencies.
774
- *
775
- * Falls back to empty array if no declaration is found.
776
- */
777
- /**
778
- * The message for the strict/warn gate over unresolvable includes, or null
779
- * when every declared specifier resolved (external-package-file-discovery:
780
- * silence is never an outcome — non-strict consumers warn with this line,
781
- * strict consumers throw it).
782
- */
783
- function unresolvableIncludesMessage(outcomes) {
784
- const unresolvable = outcomes.filter((record) => record.outcome === "unresolvable").map((record) => record.specifier);
785
- if (unresolvable.length === 0) return null;
786
- return `[animus-extract] unresolvable include specifier(s): ${unresolvable.join(", ")}`;
787
- }
788
- /**
789
- * The message for the strict/warn gate over stale dist entries
790
- * (first-class-extension D13), or null when no declared specifier is stale.
791
- * A stale dist silently skews merged registry content while discovery
792
- * compiles the fresh sources, so it surfaces like an unresolvable specifier:
793
- * non-strict consumers warn with this line, strict consumers throw it.
794
- */
795
- function staleDistIncludesMessage(outcomes) {
796
- const stale = outcomes.filter((record) => record.outcome === "stale-dist").map((record) => record.specifier);
797
- if (stale.length === 0) return null;
798
- return `[animus-extract] stale dist for include specifier(s): ${stale.join(", ")} — dist entry is older than the newest src/ file; rebuild the package(s) before extracting`;
799
- }
800
- function extractSystemFilePackages(systemFilePath) {
801
- let source;
802
- try {
803
- source = readFileSync(systemFilePath, "utf-8");
804
- } catch {
805
- return [];
806
- }
807
- const identifiers = /* @__PURE__ */ new Set();
808
- const constructorRegex = /createSystem\s*\(\s*\{[^}]*?\bincludes\s*:\s*\[([^\]]*)\]/gs;
809
- const chainRegex = /\.includes\s*\(\s*\[([^\]]*)\]\s*\)/gs;
810
- const collectIdentifiers = (regex) => {
811
- let match;
812
- while ((match = regex.exec(source)) !== null) {
813
- const inner = match[1];
814
- for (const token of inner.split(",")) {
815
- const id = token.trim();
816
- if (id && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(id)) identifiers.add(id);
817
- }
818
- }
819
- };
820
- collectIdentifiers(constructorRegex);
821
- collectIdentifiers(chainRegex);
822
- const IDENT_START_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*/;
823
- /** Position after any run of whitespace, line comments, block comments. */
824
- const skipTrivia = (from) => {
825
- let pos = from;
826
- for (;;) {
827
- while (pos < source.length && /\s/.test(source[pos])) pos++;
828
- if (source.startsWith("//", pos)) {
829
- const newline = source.indexOf("\n", pos);
830
- pos = newline === -1 ? source.length : newline + 1;
831
- continue;
832
- }
833
- if (source.startsWith("/*", pos)) {
834
- const close = source.indexOf("*/", pos + 2);
835
- pos = close === -1 ? source.length : close + 2;
836
- continue;
837
- }
838
- return pos;
839
- }
840
- };
841
- /**
842
- * Consume consecutive extend/from links starting at `from`, collecting
843
- * each link's base identifier; tolerates trivia between tokens and a
844
- * trailing comma in the argument list. Returns the position after the
845
- * last consumed link (`from` itself when none matched).
846
- */
847
- const consumeChainLinks = (from) => {
848
- let pos = from;
849
- for (;;) {
850
- let cursor = skipTrivia(pos);
851
- if (source[cursor] !== ".") return pos;
852
- cursor = skipTrivia(cursor + 1);
853
- const method = IDENT_START_RE.exec(source.slice(cursor))?.[0];
854
- if (method !== "extend" && method !== "from") return pos;
855
- cursor = skipTrivia(cursor + method.length);
856
- if (source[cursor] !== "(") return pos;
857
- cursor = skipTrivia(cursor + 1);
858
- const base = IDENT_START_RE.exec(source.slice(cursor))?.[0];
859
- if (!base) return pos;
860
- cursor = skipTrivia(cursor + base.length);
861
- while (source[cursor] === ".") {
862
- const afterDot = skipTrivia(cursor + 1);
863
- const segment = IDENT_START_RE.exec(source.slice(afterDot))?.[0];
864
- if (!segment) break;
865
- cursor = skipTrivia(afterDot + segment.length);
866
- }
867
- if (source[cursor] === ",") cursor = skipTrivia(cursor + 1);
868
- if (source[cursor] !== ")") return pos;
869
- identifiers.add(base);
870
- pos = cursor + 1;
871
- }
872
- };
873
- /** Identifier assigned directly before `index` (`const x = <here>`), if any. */
874
- const boundIdentifierBefore = (index) => {
875
- const match = /([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*$/.exec(source.slice(0, index));
876
- return match ? match[1] : null;
877
- };
878
- const chainRootIdentifiers = /* @__PURE__ */ new Set();
879
- const createSystemAnchor = /createSystem\s*\(/g;
880
- let anchorMatch;
881
- while ((anchorMatch = createSystemAnchor.exec(source)) !== null) {
882
- let pos = anchorMatch.index + anchorMatch[0].length;
883
- let depth = 1;
884
- while (pos < source.length && depth > 0) {
885
- const ch = source[pos];
886
- if (ch === "(") depth++;
887
- else if (ch === ")") depth--;
888
- pos++;
889
- }
890
- consumeChainLinks(pos);
891
- const bound = boundIdentifierBefore(anchorMatch.index);
892
- if (bound) chainRootIdentifiers.add(bound);
893
- }
894
- const scannedRoots = /* @__PURE__ */ new Set();
895
- for (;;) {
896
- const pendingRoots = [...chainRootIdentifiers].filter((root) => !scannedRoots.has(root));
897
- if (pendingRoots.length === 0) break;
898
- for (const root of pendingRoots) {
899
- scannedRoots.add(root);
900
- const rootUse = new RegExp(`(?<![a-zA-Z0-9_$])${root.replace(/\$/g, "\\$")}(?![a-zA-Z0-9_$])`, "g");
901
- let useMatch;
902
- while ((useMatch = rootUse.exec(source)) !== null) {
903
- const afterIdentifier = useMatch.index + useMatch[0].length;
904
- if (consumeChainLinks(afterIdentifier) === afterIdentifier) continue;
905
- const bound = boundIdentifierBefore(useMatch.index);
906
- if (bound) chainRootIdentifiers.add(bound);
907
- }
908
- }
909
- }
910
- if (identifiers.size === 0) return [];
911
- const importMap = /* @__PURE__ */ new Map();
912
- const importRegex = /^\s*import\s+(?:([a-zA-Z_$][a-zA-Z0-9_$]*)\s*,\s*)?(?:\{([^}]*)\}|([a-zA-Z_$][a-zA-Z0-9_$]*))\s+from\s+['"]([^'"]+)['"]/gm;
913
- let importMatch;
914
- while ((importMatch = importRegex.exec(source)) !== null) {
915
- const [, comboDefault, namedImports, defaultImport, specifier] = importMatch;
916
- if (comboDefault) importMap.set(comboDefault, specifier);
917
- if (defaultImport) importMap.set(defaultImport, specifier);
918
- if (namedImports) for (const binding of namedImports.split(",")) {
919
- const parts = binding.trim().split(/\s+as\s+/);
920
- const localName = (parts[1] || parts[0]).trim();
921
- if (localName) importMap.set(localName, specifier);
922
- }
923
- }
924
- const systemFileDir = dirname(systemFilePath);
925
- const packages = /* @__PURE__ */ new Set();
926
- for (const id of identifiers) {
927
- const specifier = importMap.get(id);
928
- if (!specifier) continue;
929
- if (specifier.startsWith(".")) packages.add(resolve(systemFileDir, specifier));
930
- else packages.add(specifier);
931
- }
932
- return Array.from(packages);
933
- }
934
- //#endregion
935
- //#region pipeline/asset-placeholders.ts
936
- /**
937
- * Host-side handling of `asset()` placeholders (global-styles-system):
938
- * `animus-asset:<specifier>` markers ride through the sandbox and the
939
- * emitter byte-exact; each HOST plugin substitutes them with its bundler's
940
- * resolved asset URL after extraction. This module owns the shared scanning
941
- * and replacement mechanics — resolution and strict gating stay at the
942
- * plugin call sites.
943
- */
944
- /** Mirrors the producer-side constant in `@animus-ui/system`'s `asset.ts`
945
- * (extract must not take a runtime dependency on system); the pairing is
946
- * pinned by a contract test in `tests/asset-placeholders.test.ts`. */
947
- const ASSET_PLACEHOLDER_PREFIX = "animus-asset:";
948
- const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
949
- const PREFIX_RE = escapeRegExp(ASSET_PLACEHOLDER_PREFIX);
950
- /**
951
- * Quoted `url('animus-asset:<specifier>')` form — the emitter always quotes
952
- * `url()` values, so a quoted match carries the FULL specifier, including
953
- * whitespace and parentheses (D5 pins the placeholder as specifier-verbatim,
954
- * so scanning fidelity is the only place such characters can be honored).
955
- */
956
- const QUOTED_PLACEHOLDER_RE = new RegExp(`(['"])${PREFIX_RE}([^'"]*?)\\1`, "g");
957
- /** Bare-form fallback: a placeholder up to CSS url()/quote/whitespace. */
958
- const BARE_PLACEHOLDER_RE = new RegExp(`${PREFIX_RE}([^'")\\s]+)`, "g");
959
- /** Unique asset specifiers referenced by placeholders, in appearance order. */
960
- function findAssetSpecifiers(css) {
961
- if (!css.includes("animus-asset:")) return [];
962
- const seen = /* @__PURE__ */ new Set();
963
- const remainder = css.replace(QUOTED_PLACEHOLDER_RE, (_match, _quote, specifier) => {
964
- seen.add(specifier);
965
- return "";
966
- });
967
- for (const match of remainder.matchAll(BARE_PLACEHOLDER_RE)) seen.add(match[1]);
968
- return [...seen];
969
- }
970
- /**
971
- * Replace each specifier's placeholder with its substitution. Specifiers
972
- * absent from the map keep their placeholder — the caller decides whether
973
- * that is a strict failure or a warn-and-emit-literally. A substitution may
974
- * itself be a bundler marker (e.g. Vite's `__VITE_ASSET__<ref>__`) that the
975
- * host's own asset pipeline resolves to a hashed file name later.
976
- *
977
- * Each replacement is anchored to a following delimiter (quote, `)`,
978
- * whitespace, or end of input) so a specifier that textually prefixes a
979
- * longer one can never clobber the longer placeholder; longest-first
980
- * ordering additionally lets the longer of two mapped specifiers claim its
981
- * occurrences before the shorter runs.
982
- */
983
- function substituteAssetPlaceholders(css, urlBySpecifier) {
984
- if (urlBySpecifier.size === 0) return css;
985
- if (!css.includes("animus-asset:")) return css;
986
- const specifiers = [...urlBySpecifier.keys()].sort((a, b) => b.length - a.length);
987
- let out = css;
988
- for (const specifier of specifiers) {
989
- const placeholder = new RegExp(escapeRegExp(ASSET_PLACEHOLDER_PREFIX + specifier) + String.raw`(?=['")\s]|$)`, "g");
990
- out = out.replace(placeholder, () => urlBySpecifier.get(specifier));
991
- }
992
- return out;
993
- }
994
- //#endregion
995
- //#region pipeline/resolve-asset.ts
996
- /**
997
- * Node-side asset specifier resolution shared by the host plugins (the
998
- * asset() contract, global-styles-system): host path aliases apply FIRST —
999
- * an alias such as `@fonts` works in application modules, so
1000
- * `asset('@fonts/inter.woff2')` must resolve identically — then direct Node
1001
- * resolution, then the package-root fallback (exports maps rarely list
1002
- * asset subpaths). Bundler-native resolution (Vite's `this.resolve`) is
1003
- * still preferred where available; this resolver covers the paths that have
1004
- * no bundler hook (Next's session, Vite's dev re-analysis).
1005
- */
1006
- const requireByRoot = /* @__PURE__ */ new Map();
1007
- function requireAnchoredAt(rootDir) {
1008
- let req = requireByRoot.get(rootDir);
1009
- if (!req) {
1010
- req = createRequire$1(join(rootDir, "package.json"));
1011
- requireByRoot.set(rootDir, req);
1012
- }
1013
- return req;
1014
- }
1015
- function packageRootFromEntry(entry) {
1016
- let current = dirname(entry);
1017
- while (true) {
1018
- if (existsSync(join(current, "package.json"))) return current;
1019
- const parent = dirname(current);
1020
- if (parent === current) return null;
1021
- current = parent;
1022
- }
1023
- }
1024
- const aliasTableCache = /* @__PURE__ */ new Map();
1025
- function parseAliasTable(pathAliasesJson) {
1026
- const cached = aliasTableCache.get(pathAliasesJson);
1027
- if (cached) return cached;
1028
- let aliases;
1029
- try {
1030
- aliases = JSON.parse(pathAliasesJson).aliases ?? [];
1031
- } catch {
1032
- aliases = [];
1033
- }
1034
- aliasTableCache.set(pathAliasesJson, aliases);
1035
- return aliases;
1036
- }
1037
- /**
1038
- * Map a specifier through the harvested host alias table (the same
1039
- * `pathAliasesJson` wire the engine consumes — entries pre-sorted longest
1040
- * pattern first by `buildPathAliasesJson`). Returns an absolute path to an
1041
- * EXISTING file, or null when no alias claims the specifier.
1042
- */
1043
- function resolveThroughPathAliases(specifier, rootDir, pathAliasesJson) {
1044
- if (!pathAliasesJson) return null;
1045
- const aliases = parseAliasTable(pathAliasesJson);
1046
- for (const alias of aliases) {
1047
- let mapped = null;
1048
- if (alias.type === "exact") {
1049
- if (alias.pattern === specifier) mapped = alias.replacement;
1050
- } else if (specifier.startsWith(alias.pattern)) mapped = alias.replacement + specifier.slice(alias.pattern.length);
1051
- if (mapped === null) continue;
1052
- const absolute = isAbsolute(mapped) ? mapped : join(rootDir, mapped);
1053
- if (existsSync(absolute)) return absolute;
1054
- }
1055
- return null;
1056
- }
1057
- /**
1058
- * Resolve an asset specifier to an absolute file: host aliases, then direct
1059
- * Node resolution anchored at `rootDir`, then the package-root fallback.
1060
- * Returns null when nothing matches — strict gating stays at the caller.
1061
- */
1062
- function resolveAssetFile(specifier, rootDir, pathAliasesJson) {
1063
- const aliased = resolveThroughPathAliases(specifier, rootDir, pathAliasesJson);
1064
- if (aliased) return aliased;
1065
- const requireFromRoot = requireAnchoredAt(rootDir);
1066
- try {
1067
- return requireFromRoot.resolve(specifier);
1068
- } catch {}
1069
- const segments = specifier.split("/");
1070
- const packageName = specifier.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0];
1071
- const subpath = specifier.slice(packageName.length + 1);
1072
- if (!subpath) return null;
1073
- for (const modulesDir of requireFromRoot.resolve.paths(packageName) ?? []) {
1074
- const packageRoot = join(modulesDir, packageName);
1075
- if (!existsSync(join(packageRoot, "package.json"))) continue;
1076
- const candidate = join(packageRoot, subpath);
1077
- if (existsSync(candidate)) return candidate;
1078
- }
1079
- try {
1080
- const packageRoot = packageRootFromEntry(requireFromRoot.resolve(packageName));
1081
- if (!packageRoot) return null;
1082
- const candidate = join(packageRoot, subpath);
1083
- return existsSync(candidate) ? candidate : null;
1084
- } catch {
1085
- return null;
1086
- }
1087
- }
1088
- //#endregion
1089
- //#region pipeline/correlate-external-tokens.ts
1090
- /**
1091
- * Index the loader-captured theme manifests by owning specifier. Module paths
1092
- * are canonical absolute paths (symlinks resolved), so each package dir is
1093
- * realpath'd before prefix-matching; a module outside every known package dir
1094
- * (typically the consumer's own theme) contributes nothing.
1095
- */
1096
- function buildSourceTokenIndex(opts) {
1097
- const index = /* @__PURE__ */ new Map();
1098
- if (!opts.sourceThemeManifestsJson) return index;
1099
- let manifests;
1100
- try {
1101
- manifests = JSON.parse(opts.sourceThemeManifestsJson);
1102
- } catch {
1103
- return index;
1104
- }
1105
- const realDirOwners = [];
1106
- const addOwnerDir = (dir, specifier) => {
1107
- if (!realDirOwners.some((entry) => entry.dir === dir)) realDirOwners.push({
1108
- dir,
1109
- specifier
1110
- });
1111
- };
1112
- for (const [dir, specifier] of Object.entries(opts.dirOwners)) {
1113
- let real = dir;
1114
- try {
1115
- real = realpathSync(dir);
1116
- } catch {}
1117
- addOwnerDir(real, specifier);
1118
- let packageRoot = real;
1119
- while (packageRoot !== dirname(packageRoot) && !existsSync(join(packageRoot, "package.json"))) packageRoot = dirname(packageRoot);
1120
- if (existsSync(join(packageRoot, "package.json"))) addOwnerDir(packageRoot, specifier);
1121
- }
1122
- for (const [modulePath, exports] of Object.entries(manifests)) {
1123
- const owner = realDirOwners.find(({ dir }) => isPathWithinRoot(dir, modulePath));
1124
- if (!owner) continue;
1125
- let tokens = index.get(owner.specifier);
1126
- if (!tokens) {
1127
- tokens = /* @__PURE__ */ new Set();
1128
- index.set(owner.specifier, tokens);
1129
- }
1130
- for (const paths of Object.values(exports)) for (const token of paths) tokens.add(token);
1131
- }
1132
- return index;
1133
- }
1134
- /**
1135
- * The correlation join. Returns one teaching-error message per distinct
1136
- * (component, token, specifier) whose file belongs to a discovered source AND
1137
- * whose token that source's manifest defines. The messages follow the
1138
- * standard severity routing at the CALLER (throw under `strict`, warn
1139
- * otherwise) — this join never reports on its own.
1140
- */
1141
- function correlateExternalTokenDiagnostics(opts) {
1142
- const messages = [];
1143
- const seen = /* @__PURE__ */ new Set();
1144
- for (const diagnostic of opts.diagnostics ?? []) {
1145
- if (diagnostic.kind !== "external-token-candidate" || !diagnostic.token) continue;
1146
- const specifier = opts.fileOwners[diagnostic.file];
1147
- if (!specifier) continue;
1148
- if (!opts.sourceTokens.get(specifier)?.has(diagnostic.token)) continue;
1149
- const key = `${diagnostic.component}\u0000${diagnostic.token}\u0000${specifier}`;
1150
- if (seen.has(key)) continue;
1151
- seen.add(key);
1152
- messages.push(`${diagnostic.component} (from '${specifier}') references token '${diagnostic.token}', which the consumer theme does not define — inherit the source's tokens with createTheme().extend(...) using the tokens (or bundle) export of '${specifier}'`);
1153
- }
1154
- return messages;
1155
- }
1156
- const indexCache = /* @__PURE__ */ new WeakMap();
1157
- /**
1158
- * The complete cross-source token-contract gate both plugins share
1159
- * (extraction-diagnostics): index the loader-captured source manifests,
1160
- * join them against the engine's candidates, and route the resulting
1161
- * teaching errors — throw under `strict`, warn otherwise. One entry point
1162
- * so the two hosts cannot drift on wiring or severity semantics.
1163
- */
1164
- function enforceExternalTokenContracts(opts) {
1165
- const manifestsJson = opts.sourceThemeManifestsJson ?? "";
1166
- const cached = indexCache.get(opts.dirOwners);
1167
- let sourceTokens;
1168
- if (cached && cached.manifestsJson === manifestsJson) sourceTokens = cached.index;
1169
- else {
1170
- sourceTokens = buildSourceTokenIndex({
1171
- sourceThemeManifestsJson: opts.sourceThemeManifestsJson,
1172
- dirOwners: opts.dirOwners
1173
- });
1174
- indexCache.set(opts.dirOwners, {
1175
- manifestsJson,
1176
- index: sourceTokens
1177
- });
1178
- }
1179
- const messages = correlateExternalTokenDiagnostics({
1180
- diagnostics: opts.diagnostics,
1181
- fileOwners: opts.fileOwners,
1182
- sourceTokens
1183
- });
1184
- if (messages.length === 0) return;
1185
- if (opts.strict) throw new Error(`${opts.prefix} ${messages.join("\n")}`);
1186
- for (const message of messages) opts.warn(message);
1187
- }
1188
- //#endregion
1189
- //#region pipeline/path-aliases.ts
1190
- /**
1191
- * Normalize alias pairs: strip a `rootDir` prefix from targets, classify
1192
- * exact (file) vs prefix (directory) matches, force trailing slashes on
1193
- * prefix entries, and sort longest pattern first for correct matching
1194
- * priority. Returns null when no entries survive.
1195
- */
1196
- function buildPathAliasesJson(pairs, rootDir) {
1197
- const entries = [];
1198
- for (const { pattern, target, kind } of pairs) {
1199
- const replacement = target.startsWith(rootDir) ? target.slice(rootDir.length + 1) : target;
1200
- if (kind ? kind === "exact" : /\.\w+$/.test(replacement)) entries.push({
1201
- pattern,
1202
- replacement,
1203
- type: "exact"
1204
- });
1205
- else entries.push({
1206
- pattern: pattern.endsWith("/") ? pattern : pattern + "/",
1207
- replacement: replacement.endsWith("/") ? replacement : replacement + "/",
1208
- type: "prefix"
1209
- });
1210
- }
1211
- entries.sort((a, b) => b.pattern.length - a.pattern.length);
1212
- if (entries.length === 0) return null;
1213
- return {
1214
- json: JSON.stringify({ aliases: entries }),
1215
- count: entries.length
1216
- };
1217
- }
1218
- //#endregion
1219
- //#region pipeline/post-process-css.ts
1220
- /**
1221
- * CSS post-processing (spec: css-post-processing) — Lightning CSS
1222
- * autoprefixing, syntax lowering, and optional minification against
1223
- * browserslist targets. The single implementation consumed by both the
1224
- * Vite plugin (virtual-module serve time) and the Next.js plugin
1225
- * (ExtractionSession artifact emission).
1226
- */
1227
- /**
1228
- * Resolve browser targets for Lightning CSS.
1229
- * Priority: explicit config → project browserslist → 'defaults' fallback.
1230
- */
1231
- function resolveLightningTargets(explicitTargets, rootDir) {
1232
- let queries;
1233
- if (explicitTargets) queries = Array.isArray(explicitTargets) ? explicitTargets : [explicitTargets];
1234
- else {
1235
- const detected = browserslist(void 0, { path: rootDir });
1236
- queries = detected.length > 0 ? detected : browserslist("defaults");
1237
- }
1238
- return browserslistToTargets(Array.isArray(queries) && typeof queries[0] === "string" && queries[0].includes(" ") ? browserslist(queries) : queries);
1239
- }
1240
- /**
1241
- * Post-process CSS with Lightning CSS: autoprefixing + optional minification.
1242
- * On failure, returns the original CSS and logs a warning.
1243
- */
1244
- function postProcessCss(css, opts) {
1245
- if (!css) return css;
1246
- try {
1247
- return transform({
1248
- filename: "animus-extracted.css",
1249
- code: Buffer.from(css),
1250
- minify: opts.minify,
1251
- targets: opts.targets
1252
- }).code.toString();
1253
- } catch (e) {
1254
- const msg = e instanceof Error ? e.message : String(e);
1255
- (opts.warnFn ?? console.warn)(`[animus] Lightning CSS post-processing failed: ${msg}`);
1256
- return css;
1257
- }
1258
- }
1259
- //#endregion
1260
- //#region pipeline/manifest-diagnostics.ts
1261
- /** Stable code for selector forms with no substitutable subject.
1262
- * Mirrors the Rust constant in `extract-v2/src/eval.rs`. */
1263
- const SELECTOR_UNSUPPORTED_SUBJECT = "animus.selector.unsupported-subject";
1264
- /** Unresolved-parent chain-drop message (mirrors the Rust diagnostic in
1265
- * `extract-v2` — the ONE encoding of this message shape; consumers match
1266
- * through the helpers below, never their own regex copies). */
1267
- const UNRESOLVED_PARENT_RE = /chain dropped: could not resolve parent component '([^']+)'/;
1268
- /** True when the diagnostic reports a chain dropped for an unresolved
1269
- * parent component. */
1270
- function isUnresolvedParentDrop(diagnostic) {
1271
- return typeof diagnostic?.message === "string" && UNRESOLVED_PARENT_RE.test(diagnostic.message);
1272
- }
1273
- /** The parent binding named by an unresolved-parent drop, or null when the
1274
- * diagnostic is not one. */
1275
- function unresolvedParentName(diagnostic) {
1276
- if (typeof diagnostic?.message !== "string") return null;
1277
- return UNRESOLVED_PARENT_RE.exec(diagnostic.message)?.[1] ?? null;
1278
- }
1279
- /** True when the selector string carries at least one substitutable `&`
1280
- * subject outside quoted text (mirrors the Rust `selector_subject` walk —
1281
- * ancestor, leading, and repeated subjects all count; a `&` inside a
1282
- * quoted attribute value does not). */
1283
- function hasSelectorSubject(value) {
1284
- let quote = null;
1285
- let escaped = false;
1286
- for (const c of value) {
1287
- if (escaped) {
1288
- escaped = false;
1289
- continue;
1290
- }
1291
- if (c === "\\") {
1292
- escaped = true;
1293
- continue;
1294
- }
1295
- if (quote !== null) {
1296
- if (c === quote) quote = null;
1297
- } else if (c === "\"" || c === "'") quote = c;
1298
- else if (c === "&") return true;
1299
- }
1300
- return false;
1301
- }
1302
- /**
1303
- * Synthesize coded diagnostics for registered selector-alias values that
1304
- * look selector-shaped (`&` present) but carry no substitutable subject —
1305
- * every `&` sits inside quoted text, so there is nothing to anchor the
1306
- * class to. Ancestor-prefixed and repeated subjects are supported and pass
1307
- * validation. The system-config boundary is where these must fail loud
1308
- * (alias values never reach the evaluator's key guard).
1309
- */
1310
- function collectSelectorAliasDiagnostics(selectorAliasesJson) {
1311
- if (!selectorAliasesJson) return [];
1312
- let aliases;
1313
- try {
1314
- aliases = JSON.parse(selectorAliasesJson);
1315
- } catch {
1316
- return [];
1317
- }
1318
- const diagnostics = [];
1319
- for (const [name, value] of Object.entries(aliases)) {
1320
- if (typeof value !== "string") continue;
1321
- if (value.includes("&") && !hasSelectorSubject(value)) diagnostics.push({
1322
- file: "system",
1323
- component: name,
1324
- kind: "warn",
1325
- message: `selector alias '${name}' value '${value}' has no substitutable '&' subject outside quoted text (${SELECTOR_UNSUPPORTED_SUBJECT})`,
1326
- code: SELECTOR_UNSUPPORTED_SUBJECT,
1327
- severity: "error"
1328
- });
1329
- }
1330
- return diagnostics;
1331
- }
1332
- /**
1333
- * Surface extraction-manifest diagnostics through a plugin's warn channel.
1334
- *
1335
- * Single authoritative copy for both extraction plugins — and the single
1336
- * strict-escalation policy point: error-severity diagnostics throw one
1337
- * Error naming every offender when `policy.strict`, and print as warnings
1338
- * otherwise. Surfaces `bail` (component not extracted), `skip` (component
1339
- * skipped), and `warn` kinds; unknown kinds stay silent. Printed lines
1340
- * include the diagnostic code when the message doesn't already carry it.
1341
- */
1342
- function surfaceManifestDiagnostics(manifest, warn, policy = {}) {
1343
- const errors = [];
1344
- const diagnostics = policy.prepend?.length ? [...policy.prepend, ...manifest.diagnostics ?? []] : manifest.diagnostics ?? [];
1345
- for (const diagnostic of diagnostics) {
1346
- let line = null;
1347
- if (diagnostic.kind === "bail") line = `⚠ ${diagnostic.component} not extracted: ${diagnostic.message}`;
1348
- else if (diagnostic.kind === "skip") line = `⚠ ${diagnostic.component}: skipped ${diagnostic.message}`;
1349
- else if (diagnostic.kind === "warn") line = `⚠ ${diagnostic.file}: ${diagnostic.component}: ${diagnostic.message}`;
1350
- if (line === null) continue;
1351
- if (diagnostic.code && !diagnostic.message.includes(diagnostic.code)) line += ` [${diagnostic.code}]`;
1352
- if (policy.strict && diagnostic.severity === "error") {
1353
- errors.push(`${diagnostic.code ?? "error"} — ${diagnostic.component}: ${diagnostic.message}`);
1354
- continue;
1355
- }
1356
- warn(line);
1357
- }
1358
- if (errors.length > 0) throw new Error(`[animus] strict: ${errors.length} error diagnostic(s):\n${errors.join("\n")}`);
1359
- }
1360
- //#endregion
1361
- //#region pipeline/unit-fallback.ts
1362
- /**
1363
- * Append `px` to bare numeric values in CSS declarations for properties
1364
- * that expect length units. Unitless properties are preserved as-is.
1365
- * Numbers inside CSS function calls (cubic-bezier, rgb, calc, etc.) are skipped.
1366
- */
1367
- function applyUnitFallback(css) {
1368
- return css.replace(/([a-z-]+)\s*:\s*([^;{}]+);/g, (match, prop, value) => {
1369
- if (isUnitlessProperty(prop)) return match;
1370
- let depth = 0;
1371
- let fixed = "";
1372
- let i = 0;
1373
- while (i < value.length) if (value[i] === "(") {
1374
- depth++;
1375
- fixed += value[i];
1376
- i++;
1377
- } else if (value[i] === ")") {
1378
- depth--;
1379
- fixed += value[i];
1380
- i++;
1381
- } else if (depth > 0) {
1382
- fixed += value[i];
1383
- i++;
1384
- } else {
1385
- const numMatch = value.slice(i).match(/^(-?\d+\.?\d*)/);
1386
- if (numMatch) {
1387
- const num = numMatch[1];
1388
- const after = value[i + num.length];
1389
- if (after && /[a-z%]/i.test(after)) fixed += num;
1390
- else fixed += num + "px";
1391
- i += num.length;
1392
- } else {
1393
- fixed += value[i];
1394
- i++;
1395
- }
1396
- }
1397
- return fixed !== value ? `${prop}:${fixed};` : match;
1398
- });
1399
- }
1400
- //#endregion
1401
- //#region pipeline/run-analysis.ts
1402
- /**
1403
- * Build the named `analyzeProject` input set from analysis options. Also
1404
- * the persistence shape for `.animus/analysis-inputs.json` — an isolated
1405
- * process can replay the analysis from exactly this object
1406
- * (spec: next-turbopack-integration).
1407
- */
1408
- function buildAnalysisInputs(opts) {
1409
- return {
1410
- filesJson: JSON.stringify(opts.fileEntries),
1411
- scalesJson: opts.system.scalesJson,
1412
- variableMapJson: opts.system.variableMapJson,
1413
- contextualVarsJson: opts.system.contextualVarsJson,
1414
- propConfigJson: opts.system.propConfigJson,
1415
- groupRegistryJson: opts.system.groupRegistryJson,
1416
- packageResolutionJson: JSON.stringify(opts.packageMap),
1417
- devMode: opts.devMode,
1418
- emitterConfigJson: JSON.stringify({
1419
- runtime_import: opts.emitter.runtimeImport,
1420
- css_module_id: opts.emitter.cssModuleId,
1421
- ...opts.emitter.systemPropsModuleId ? { system_props_module_id: opts.emitter.systemPropsModuleId } : {}
1422
- }),
1423
- selectorAliasesJson: opts.system.selectorAliasesJson,
1424
- globalStyleBlocksJson: opts.system.globalStyleBlocksJson,
1425
- pathAliasesJson: opts.pathAliasesJson,
1426
- keyframesJson: opts.system.keyframesJson,
1427
- staticCssJson: opts.staticCssJson ?? null,
1428
- conditionAliasesJson: opts.system.conditionAliasesJson ?? null,
1429
- externalDirsJson: opts.externalDirs?.length && hasSourceThemeManifests(opts.system) ? JSON.stringify(opts.externalDirs) : null
1430
- };
1431
- }
1432
- /** Whether the loader captured at least one source built-theme manifest. */
1433
- function hasSourceThemeManifests(system) {
1434
- const json = system.sourceThemeManifestsJson;
1435
- return typeof json === "string" && json.length > 0 && json !== "{}";
1436
- }
1437
- /**
1438
- * The one analysis invocation both plugins share: build the emitter
1439
- * config, serialize inputs, call the NAPI `analyzeProject`, parse the
1440
- * manifest, surface its diagnostics, and resolve the CSS outputs.
1441
- *
1442
- * Error handling stays at the call site (strict-mode throw vs warn).
1443
- */
1444
- function runProjectAnalysis(engineApi, opts) {
1445
- const { analyzeProject } = engineApi();
1446
- let t = performance.now();
1447
- const inputs = buildAnalysisInputs(opts);
1448
- const serializeMs = Math.round(performance.now() - t);
1449
- t = performance.now();
1450
- const manifestJson = analyzeProject(...buildAnalyzeProjectArgs(inputs));
1451
- const extractMs = Math.round(performance.now() - t);
1452
- t = performance.now();
1453
- const manifest = JSON.parse(manifestJson);
1454
- surfaceManifestDiagnostics(manifest, opts.warn, {
1455
- strict: opts.strict,
1456
- prepend: [...collectSelectorAliasDiagnostics(opts.system.selectorAliasesJson), ...opts.extraDiagnostics ?? []]
1457
- });
1458
- const parseMs = Math.round(performance.now() - t);
1459
- return {
1460
- manifest,
1461
- manifestJson,
1462
- globalCss: manifest?.sheets?.global || "",
1463
- componentCss: applyUnitFallback(manifest?.css || ""),
1464
- inputs,
1465
- timings: {
1466
- serializeMs,
1467
- extractMs,
1468
- parseMs
1469
- }
1470
- };
1471
- }
1472
- /**
1473
- * Clear the engine's per-file analysis cache so stale results from a prior
1474
- * build never bleed into a fresh run. Tolerates engines without the
1475
- * capability (older builds) — the probe is benign.
1476
- */
1477
- function clearEngineCache(engineApi) {
1478
- try {
1479
- const { clearAnalysisCache } = engineApi();
1480
- clearAnalysisCache();
1481
- } catch {}
1482
- }
1483
- //#endregion
1484
- //#region pipeline/static-css.ts
1485
- /** Key-sorted stringify so semantically identical configs serialize
1486
- * identically (stable analysis-inputs hashing). */
1487
- function stableStringify(value) {
1488
- if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
1489
- if (value !== null && typeof value === "object") return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
1490
- return JSON.stringify(value);
1491
- }
1492
- /** Serialize a staticCss option for the engine; null when absent/empty. */
1493
- function serializeStaticCss(config) {
1494
- if (!config) return null;
1495
- const json = stableStringify(config);
1496
- return json === "{}" ? null : json;
1497
- }
1498
- //#endregion
1499
- //#region pipeline/prefix.ts
1500
- /**
1501
- * Apply namespace prefix to a variable map, CSS variable declarations, and theme JSON.
1502
- *
1503
- * Variable map: `{ "colors.ember": "--color-ember" }` -> `{ "colors.ember": "--prefix-color-ember" }`
1504
- * Variable CSS: `--color-ember: #FF2800` -> `--prefix-color-ember: #FF2800`
1505
- * `var(--color-ember)` -> `var(--prefix-color-ember)`
1506
- * Theme JSON: `{ "colors.ember": "var(--color-ember)" }` -> `{ "colors.ember": "var(--prefix-color-ember)" }`
1507
- */
1508
- function applyPrefix(prefix, variableMapJson, variableCss, themeJson, contextualVarsJson) {
1509
- if (!prefix) return {
1510
- variableMapJson,
1511
- variableCss,
1512
- themeJson,
1513
- contextualVarsJson
1514
- };
1515
- const varRefRe = /var\(--([a-zA-Z][\w-]*)\)/g;
1516
- const map = JSON.parse(variableMapJson);
1517
- const prefixed = {};
1518
- for (const [key, varName] of Object.entries(map)) prefixed[key] = varName.startsWith("--") ? `--${prefix}-${varName.slice(2)}` : varName;
1519
- let css = variableCss;
1520
- css = css.replace(/--([a-zA-Z][\w-]*)\s*:/g, `--${prefix}-$1:`);
1521
- css = css.replace(varRefRe, `var(--${prefix}-$1)`);
1522
- const result = {
1523
- variableMapJson: JSON.stringify(prefixed),
1524
- variableCss: css
1525
- };
1526
- if (themeJson) result.themeJson = themeJson.replace(varRefRe, `var(--${prefix}-$1)`);
1527
- if (contextualVarsJson) {
1528
- const ctxVars = JSON.parse(contextualVarsJson);
1529
- const prefixedCtx = {};
1530
- for (const [scale, names] of Object.entries(ctxVars)) prefixedCtx[scale] = names.map((name) => `${prefix}-${name}`);
1531
- result.contextualVarsJson = JSON.stringify(prefixedCtx);
1532
- }
1533
- return result;
1534
- }
1535
- //#endregion
1536
- //#region pipeline/system-config.ts
1537
- /**
1538
- * Load a SystemInstance via the engine's `loadSystemModule` and normalize
1539
- * it into a SystemConfig. When `prefix` is set, CSS variable names in the
1540
- * variable map/css (and theme + contextual vars when affected) are
1541
- * namespaced via the shared `applyPrefix`.
1542
- *
1543
- * Error handling stays at the call site — the Vite plugin warns (or throws
1544
- * in strict mode) and keeps its previous config, the Next plugin lets the
1545
- * failure propagate.
1546
- */
1547
- function loadSystemConfig(engineApi, opts) {
1548
- const { loadSystemModule } = engineApi();
1549
- const config = loadSystemModule(opts.systemPath, opts.rootDir);
1550
- let scalesJson = config.scalesJson;
1551
- let variableMapJson = config.variableMapJson;
1552
- let variableCss = config.variableCss;
1553
- let contextualVarsJson = config.contextualVarsJson || null;
1554
- if (opts.prefix) {
1555
- const prefixed = applyPrefix(opts.prefix, variableMapJson, variableCss, scalesJson, contextualVarsJson || void 0);
1556
- variableMapJson = prefixed.variableMapJson;
1557
- variableCss = prefixed.variableCss;
1558
- if (prefixed.themeJson) scalesJson = prefixed.themeJson;
1559
- if (prefixed.contextualVarsJson) contextualVarsJson = prefixed.contextualVarsJson;
1560
- }
1561
- return {
1562
- propConfigJson: config.propConfig,
1563
- groupRegistryJson: config.groupRegistry,
1564
- scalesJson,
1565
- variableMapJson,
1566
- variableCss,
1567
- contextualVarsJson,
1568
- selectorAliasesJson: config.selectorAliases || null,
1569
- conditionAliasesJson: config.conditionAliases || null,
1570
- globalStyleBlocksJson: config.globalStyleBlocks || null,
1571
- keyframesJson: config.keyframesBlocks || null,
1572
- dependencies: config.dependencies ?? [],
1573
- sourceThemeManifestsJson: config.sourceThemeManifests || null
1574
- };
1575
- }
1576
- //#endregion
1577
- //#region pipeline/dynamic-prop-config.ts
1578
- function buildDynamicPropConfig(dynamicProps) {
1579
- const configEntries = {};
1580
- for (const [propName, meta] of Object.entries(dynamicProps)) {
1581
- if (!meta.varName || !meta.slotClass) throw new Error(`buildDynamicPropConfig: dynamic prop '${propName}' carries no slot metadata — expected varName and slotClass, got keys [${Object.keys(meta).join(", ")}].`);
1582
- const entry = {
1583
- varName: meta.varName,
1584
- slotClass: meta.slotClass
1585
- };
1586
- if (meta.property) entry.property = meta.property;
1587
- if (meta.properties && meta.properties.length > 0) entry.properties = meta.properties;
1588
- if (meta.transformName) entry.transformName = meta.transformName;
1589
- if (meta.scaleValues && Object.keys(meta.scaleValues).length > 0) entry.scaleValues = meta.scaleValues;
1590
- configEntries[propName] = entry;
1591
- }
1592
- return configEntries;
1593
- }
1594
- //#endregion
1595
- //#region pipeline/system-props-module.ts
1596
- /**
1597
- * Build the source of the runtime system-props module — served as
1598
- * `virtual:animus/system-props` by the Vite plugin and written to
1599
- * `.animus/system-props.js` by the Next plugin. One generator so the two
1600
- * runtimes can never drift.
1601
- *
1602
- * Runtime transform functions for dynamic props are not yet supported
1603
- * (transforms are resolved at extraction time via boa_engine in Rust), so
1604
- * `transforms` defaults to an empty object literal.
1605
- *
1606
- * `dynamicPropConfig` entries carry the CSS property the slot class declares
1607
- * — the runtime's unit-fallback decision. Their field order and omission
1608
- * rules belong to buildDynamicPropConfig.
1609
- */
1610
- function buildSystemPropsModule(opts) {
1611
- const dynamicPropConfig = buildDynamicPropConfig(opts.dynamicProps);
1612
- return `export const systemPropMap = ${opts.systemPropMapJson};\nexport const systemPropGroups = ${opts.groupRegistryJson};\nexport const dynamicPropConfig = ${JSON.stringify(dynamicPropConfig)};\nexport const transforms = ${opts.transformsSource ?? "{}"};\n`;
1613
- }
1614
- //#endregion
1615
- //#region pipeline/timing-waterfall.ts
1616
- /**
1617
- * Rust-side timing phases as reported in `manifest.timing`, in waterfall
1618
- * order, with their display labels. Mirrors the timing struct in the
1619
- * extract-v2 crate — a renamed or added Rust phase is updated here once,
1620
- * not per plugin.
1621
- */
1622
- const RUST_TIMING_PHASES = [
1623
- ["parseAndWalk", "parse+walk"],
1624
- ["importResolution", "imports"],
1625
- ["extensionProvenance", "provenance"],
1626
- ["topologicalSort", "topo-sort"],
1627
- ["chainEvaluation", "chains"],
1628
- ["jsxScanning", "jsx-scan"],
1629
- ["systemPropAggregation", "sys-props"],
1630
- ["usageLedger", "usage"],
1631
- ["reconciliation", "reconcile"],
1632
- ["cssGeneration", "css-gen"],
1633
- ["manifestSerialization", "serialize"]
1634
- ];
1635
- /**
1636
- * Format the Rust timing waterfall as log lines. `indent` and `labelWidth`
1637
- * let each plugin keep its established column layout; the phase table and
1638
- * the `(N files, M cached)` suffix are shared.
1639
- */
1640
- function formatRustTimingWaterfall(timing, opts) {
1641
- const lines = [];
1642
- for (const [key, label] of RUST_TIMING_PHASES) {
1643
- const ms = timing[key] ?? 0;
1644
- const pad = " ".repeat(Math.max(0, opts.labelWidth - label.length));
1645
- const extra = key === "parseAndWalk" ? ` (${timing.fileCount ?? 0} files, ${timing.cacheHits ?? 0} cached)` : "";
1646
- lines.push(`${opts.indent}${label}${pad}${String(ms).padStart(5)}ms${extra}`);
1647
- }
1648
- return lines;
1649
- }
1650
- //#endregion
1651
- //#region pipeline/tsconfig-paths.ts
1652
- /**
1653
- * tsconfig `paths` → alias pairs for the Rust `pathAliasesJson` contract
1654
- * (via `buildPathAliasesJson`). Used by drivers that cannot harvest aliases
1655
- * from a live bundler config — Turbopack exposes none.
1656
- *
1657
- * Semantics (deliberately the TypeScript subset that matters for import
1658
- * provenance): JSONC tolerated (comments + trailing commas); `extends`
1659
- * chains followed (string or array; missing parents and cycles skipped);
1660
- * the NEAREST config declaring `paths` wins wholesale (TS replaces the
1661
- * object, never merges); targets resolve against the nearest `baseUrl`
1662
- * (resolved from its declaring config's directory) or, absent one, the
1663
- * directory of the config declaring `paths`; first target per pattern.
1664
- * `x/*` patterns become prefix pairs, non-wildcard patterns exact pairs,
1665
- * bare `*` catch-alls and mid-pattern wildcards are skipped.
1666
- */
1667
- /** String-aware JSONC → JSON: strips // and block comments + trailing commas. */
1668
- function stripJsonc(text) {
1669
- let out = "";
1670
- let inString = false;
1671
- let inLine = false;
1672
- let inBlock = false;
1673
- for (let i = 0; i < text.length; i++) {
1674
- const ch = text[i];
1675
- const next = text[i + 1];
1676
- if (inLine) {
1677
- if (ch === "\n") {
1678
- inLine = false;
1679
- out += ch;
1680
- }
1681
- continue;
1682
- }
1683
- if (inBlock) {
1684
- if (ch === "*" && next === "/") {
1685
- inBlock = false;
1686
- i++;
1687
- }
1688
- continue;
1689
- }
1690
- if (inString) {
1691
- out += ch;
1692
- if (ch === "\\") {
1693
- out += next ?? "";
1694
- i++;
1695
- } else if (ch === "\"") inString = false;
1696
- continue;
1697
- }
1698
- if (ch === "\"") {
1699
- inString = true;
1700
- out += ch;
1701
- continue;
1702
- }
1703
- if (ch === "/" && next === "/") {
1704
- inLine = true;
1705
- i++;
1706
- continue;
1707
- }
1708
- if (ch === "/" && next === "*") {
1709
- inBlock = true;
1710
- i++;
1711
- continue;
1712
- }
1713
- out += ch;
1714
- }
1715
- return out.replace(/,(\s*[}\]])/g, "$1");
1716
- }
1717
- function readConfig(path) {
1718
- let raw;
1719
- try {
1720
- raw = readFileSync(path, "utf-8");
1721
- } catch {
1722
- return null;
1723
- }
1724
- try {
1725
- const json = JSON.parse(stripJsonc(raw));
1726
- return {
1727
- dir: dirname(path),
1728
- compilerOptions: json.compilerOptions ?? {},
1729
- extends: json.extends
1730
- };
1731
- } catch {
1732
- return null;
1733
- }
1734
- }
1735
- function resolveExtendsTarget(specifier, fromDir) {
1736
- if (specifier.startsWith(".") || isAbsolute(specifier)) {
1737
- const base = resolve(fromDir, specifier);
1738
- for (const candidate of [
1739
- base,
1740
- `${base}.json`,
1741
- join(base, "tsconfig.json")
1742
- ]) if (existsSync(candidate)) return candidate;
1743
- return null;
1744
- }
1745
- try {
1746
- return __require.resolve(specifier.endsWith(".json") ? specifier : `${specifier}/tsconfig.json`, { paths: [fromDir] });
1747
- } catch {
1748
- try {
1749
- return __require.resolve(specifier, { paths: [fromDir] });
1750
- } catch {
1751
- return null;
1752
- }
1753
- }
1754
- }
1755
- /** Child-first flattened extends chain starting at `entryPath`. */
1756
- function loadChain(entryPath) {
1757
- const chain = [];
1758
- const visited = /* @__PURE__ */ new Set();
1759
- const queue = [entryPath];
1760
- while (queue.length > 0) {
1761
- const key = resolve(queue.shift());
1762
- if (visited.has(key)) continue;
1763
- visited.add(key);
1764
- const node = readConfig(key);
1765
- if (!node) continue;
1766
- chain.push(node);
1767
- const parents = Array.isArray(node.extends) ? node.extends : node.extends !== void 0 ? [node.extends] : [];
1768
- for (const parent of parents) {
1769
- if (typeof parent !== "string") continue;
1770
- const resolved = resolveExtendsTarget(parent, node.dir);
1771
- if (resolved) queue.push(resolved);
1772
- }
1773
- }
1774
- return chain;
1775
- }
1776
- /**
1777
- * Read the project's tsconfig path aliases as pairs consumable by
1778
- * `buildPathAliasesJson`. Returns an empty array when no readable tsconfig
1779
- * (or no usable `paths`) exists.
1780
- */
1781
- function readTsconfigAliasPairs(rootDir) {
1782
- const chain = loadChain(join(rootDir, "tsconfig.json"));
1783
- if (chain.length === 0) return [];
1784
- const pathsOwner = chain.find((node) => node.compilerOptions.paths && typeof node.compilerOptions.paths === "object");
1785
- if (!pathsOwner) return [];
1786
- const paths = pathsOwner.compilerOptions.paths;
1787
- const baseOwner = chain.find((node) => typeof node.compilerOptions.baseUrl === "string");
1788
- const base = baseOwner ? resolve(baseOwner.dir, baseOwner.compilerOptions.baseUrl) : pathsOwner.dir;
1789
- const pairs = [];
1790
- for (const [pattern, targets] of Object.entries(paths)) {
1791
- const target = Array.isArray(targets) ? targets[0] : void 0;
1792
- if (typeof target !== "string") continue;
1793
- const patternStars = pattern.split("*").length - 1;
1794
- const targetStars = target.split("*").length - 1;
1795
- if (pattern === "*" || patternStars > 1 || targetStars > 1) continue;
1796
- if (pattern.endsWith("/*")) {
1797
- if (!target.endsWith("/*")) continue;
1798
- pairs.push({
1799
- pattern: pattern.slice(0, -2),
1800
- target: resolve(base, target.slice(0, -2)),
1801
- kind: "prefix"
1802
- });
1803
- continue;
1804
- }
1805
- if (patternStars > 0 || targetStars > 0) continue;
1806
- pairs.push({
1807
- pattern,
1808
- target: resolve(base, target),
1809
- kind: "exact"
1810
- });
1811
- }
1812
- return pairs;
1813
- }
1814
- //#endregion
1815
- //#region pipeline/mdx-preprocessor.ts
1816
- /**
1817
- * MDX source preprocessor for the extraction pipeline.
1818
- *
1819
- * MDX files are first-class consumers of ds-built components but are not
1820
- * directly parseable by OXC (which the scanner uses for .tsx/.jsx). This
1821
- * module compiles MDX sources to scanner-consumable JSX using @mdx-js/mdx.
1822
- *
1823
- * @mdx-js/mdx is declared as `peerDependenciesMeta.optional` on the
1824
- * plugin packages that import this module. The dynamic import with
1825
- * .catch() ensures non-MDX consumers (who configure `extensions` to
1826
- * exclude .mdx) never trigger the resolution — zero install-footprint
1827
- * cost for them.
1828
- *
1829
- * The `DEFAULT_EXTENSIONS` constant is the shared source of truth for
1830
- * both `@animus-ui/vite-plugin` and `@animus-ui/next-plugin`. Each plugin
1831
- * imports it directly; independent redeclaration of default extensions
1832
- * is considered a regression.
1833
- */
1834
- const DEFAULT_EXTENSIONS = [
1835
- ".ts",
1836
- ".tsx",
1837
- ".js",
1838
- ".jsx",
1839
- ".mdx"
1840
- ];
1841
- /**
1842
- * Preprocess an MDX source string into scanner-consumable JSX.
1843
- *
1844
- * - Returns `{ kind: 'ok', source }` with JSX-compiled output on success.
1845
- * - Returns `{ kind: 'missing-dep' }` if @mdx-js/mdx is not resolvable
1846
- * (consumer needs to install it).
1847
- * - Returns `{ kind: 'error', error }` on compile failure (e.g. malformed
1848
- * MDX syntax). Plugins SHALL warn + skip affected files; the build
1849
- * continues with remaining files.
1850
- */
1851
- async function preprocessMdx(source, filename) {
1852
- const mdxMod = await import("@mdx-js/mdx").catch(() => null);
1853
- if (mdxMod === null) return { kind: "missing-dep" };
1854
- try {
1855
- const vfile = await mdxMod.compile(source, {
1856
- outputFormat: "program",
1857
- development: false,
1858
- jsx: true
1859
- });
1860
- return {
1861
- kind: "ok",
1862
- source: `/* @mdx-source: ${filename} */\n${String(vfile)}`
1863
- };
1864
- } catch (err) {
1865
- return {
1866
- kind: "error",
1867
- error: err instanceof Error ? err.message : String(err)
1868
- };
1869
- }
1870
- }
1871
- //#endregion
1872
- //#region pipeline/replacement-plans.ts
1873
- const snapshotByManifest = /* @__PURE__ */ new WeakMap();
1874
- function snapshotFilePlans(manifest) {
1875
- const memoKey = manifest !== null && typeof manifest === "object" ? manifest : null;
1876
- if (memoKey) {
1877
- const cached = snapshotByManifest.get(memoKey);
1878
- if (cached) return cached;
1879
- }
1880
- const entriesByFile = /* @__PURE__ */ new Map();
1881
- for (const [id, desc] of Object.entries(manifest?.components ?? {})) {
1882
- const file = desc.file;
1883
- if (!file) continue;
1884
- const list = entriesByFile.get(file) ?? [];
1885
- list.push([id, desc.replacement ?? ""]);
1886
- entriesByFile.set(file, list);
1887
- }
1888
- const snapshot = /* @__PURE__ */ new Map();
1889
- for (const [file, entries] of entriesByFile) {
1890
- entries.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1891
- snapshot.set(file, JSON.stringify(entries));
1892
- }
1893
- if (memoKey) snapshotByManifest.set(memoKey, snapshot);
1894
- return snapshot;
1895
- }
1896
- /**
1897
- * Rel paths whose plan changed between two snapshots: absent→present,
1898
- * present→absent, and any change to membership or replacement content.
1899
- * `exclude` drops one path (the changed file itself on the edit path — its
1900
- * own modules are already in the update set).
1901
- */
1902
- function diffFilePlans(prev, next, opts) {
1903
- const changed = /* @__PURE__ */ new Set();
1904
- for (const [file, plan] of next) if (prev.get(file) !== plan) changed.add(file);
1905
- for (const file of prev.keys()) if (!next.has(file)) changed.add(file);
1906
- if (opts?.exclude) changed.delete(opts.exclude);
1907
- return [...changed].sort();
1908
- }
1909
- /** Domain prefix keeping the epoch hash disjoint from every other
1910
- * contentHash use in the pipeline (design D5). */
1911
- const REPLACEMENT_PLANS_DOMAIN = "animus-replacement-plans-v1\0";
1912
- /** Section marker for the served-dependency witness. Leads with NUL: every
1913
- * plan entry ends `…\0` and neither file paths nor plan JSON contain NUL,
1914
- * so `\0\0` is unreachable from entry serialization alone — a snapshot can
1915
- * never forge the witness section. */
1916
- const SERVED_DEPENDENCY_DOMAIN = "\0animus-served-dependency-v1\0";
1917
- /**
1918
- * Canonical replacement epoch: contentHash over the domain-prefixed, sorted
1919
- * canonical serialization of a file-plan snapshot — each entry contributes
1920
- * `file + '\0' + plan + '\0'` (unambiguous: neither file paths nor the
1921
- * JSON plan strings contain NUL). Byte-identical plans → identical epoch;
1922
- * any membership/replacement/absent↔present change moves it.
1923
- *
1924
- * `servedDependencyWitness` extends the epoch to content the plans cannot
1925
- * see but restored modules depend on: transformed modules import the
1926
- * session's served system-props artifact, so a consumer passes that
1927
- * artifact's content here and an offline change to it moves the epoch —
1928
- * invalidating persistent-cache snapshots that reference the old epoch
1929
- * artifact. Absence and empty-string are distinct values.
1930
- *
1931
- * This is THE epoch definition — integrations consume this hash (or the
1932
- * snapshot diff) and never define their own. Epoch values compare only for
1933
- * equality (a moved value invalidates), so the serialization is free to
1934
- * evolve with a domain bump: sessions running different serializations
1935
- * simply disagree once and rebuild.
1936
- */
1937
- function hashReplacementPlans(snapshot, servedDependencyWitness) {
1938
- const entries = [...snapshot.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
1939
- let serialized = REPLACEMENT_PLANS_DOMAIN;
1940
- for (const [file, plan] of entries) serialized += file + "\0" + plan + "\0";
1941
- if (servedDependencyWitness !== void 0) serialized += SERVED_DEPENDENCY_DOMAIN + servedDependencyWitness;
1942
- return contentHash(serialized);
1943
- }
1944
- //#endregion
1945
- //#region pipeline/utils.ts
1946
- /** Convert camelCase CSS property names to kebab-case. */
1947
- function camelToKebab(str) {
1948
- return str.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
1949
- }
1950
- //#endregion
1951
- //#region pipeline/watch-keys.ts
1952
- /**
1953
- * Membership keys for a filesystem path in the system-dependency watch set.
1954
- *
1955
- * The loader reports symlink-resolved canonical paths; watcher events carry
1956
- * lexical absolute paths that may traverse symlinks (bun workspace links) and
1957
- * — for deletions — may no longer exist, so `realpathSync` on the event path
1958
- * alone cannot produce the canonical form. Both insertion and lookup go
1959
- * through this one normalizer: the lexical form plus a canonical form
1960
- * reconstructed through the nearest existing ancestor.
1961
- */
1962
- function toWatchKeys(input) {
1963
- const lexical = normalize(input);
1964
- const canonical = realpathThroughNearestExistingAncestor(lexical);
1965
- return canonical === lexical ? [lexical] : [lexical, canonical];
1966
- }
1967
- /**
1968
- * Canonicalize a path that may not exist: walk up to the nearest existing
1969
- * ancestor, realpath it, and re-append the missing suffix. Handles deleted
1970
- * files under still-existing (possibly symlinked) directories and atomic
1971
- * unlink/rename saves. Falls back to the input when no ancestor resolves.
1972
- *
1973
- * Known limit: a symlink retargeted AFTER the system load canonicalizes to
1974
- * the new target, which cannot match the recorded (old-target) dependency —
1975
- * the membership test then fails open (no reset), identical to not having
1976
- * this reconstruction at all. Retarget sensitivity is a resolution-input
1977
- * concern, deliberately out of scope for the watch set.
1978
- */
1979
- function realpathThroughNearestExistingAncestor(path) {
1980
- try {
1981
- return realpathSync$1(path);
1982
- } catch {}
1983
- const suffix = [];
1984
- let current = path;
1985
- for (;;) {
1986
- const parent = dirname$1(current);
1987
- if (parent === current) return path;
1988
- suffix.unshift(basename(current));
1989
- current = parent;
1990
- try {
1991
- return join$1(realpathSync$1(current), ...suffix);
1992
- } catch {}
1993
- }
1994
- }
1995
- //#endregion
1996
- export { ANIMUS_LAYERS, DEFAULT_EXTENSIONS, KEYFRAMES_EXPORT_COLLISION, KEYFRAMES_EXTERNAL_ENTRY_FAILED, RETIRED_ENGINE_MESSAGE, applyPrefix, applyUnitFallback, assembleStylesheet, assertNoRetiredEngineSelection, buildAnalysisInputs, buildAnalyzeProjectArgs, buildDynamicPropConfig, buildPathAliasesJson, buildSystemPropsModule, camelToKebab, clearEngineCache, collectExternalPackageSources, contentHash, createSourceIdentity, createV2EngineApi, diffFilePlans, discoverFiles, enforceExternalTokenContracts, excludeCollectedPackages, extractSystemFilePackages, findAssetSpecifiers, findPackageRoot, firstOwners, formatRustTimingWaterfall, hashReplacementPlans, isExcludedPackageRelativePath, isPathWithinRoot, isUnresolvedParentDrop, loadSystemConfig, mergeExternalKeyframes, postProcessCss, preprocessMdx, readTsconfigAliasPairs, resolveAbsolutePathSpecifier, resolveAssetFile, resolveLightningTargets, resolveThroughPathAliases, runProjectAnalysis, serializeStaticCss, sharesVolumeRoot, snapshotFilePlans, staleDistIncludesMessage, stripLeadingLayerDeclaration, substituteAssetPlaceholders, surfaceManifestDiagnostics, toWatchKeys, unresolvableIncludesMessage, unresolvedParentName, validateLayerOrder, walkPackageSources };
1
+ import { $ as CORE_OPTION_KEYS, A as createSourceIngestor, B as extractSystemFilePackages, C as resolveLightningTargets, D as resolveThroughPathAliases, E as resolveAssetFile, F as ENGINE_TRANSFORM_EXTENSIONS, G as staleDistIncludesMessage, H as firstOwners, I as preprocessMdx, J as createSourceIdentity, K as unresolvableIncludesMessage, L as adaptSvelteSource, M as isAdvisorySourceDiagnostic, N as withoutInvalidOriginals, O as findAssetSpecifiers, P as DEFAULT_EXTENSIONS, Q as AnimusConfigError, R as collectExternalPackageSources, S as postProcessCss, T as enforceExternalTokenContracts, U as isExcludedPackageRelativePath, V as findPackageRoot, W as resolveAbsolutePathSpecifier, X as sharesVolumeRoot, Y as isPathWithinRoot, Z as discoverFiles, _ as runProjectAnalysis, _t as stripLeadingLayerDeclaration, a as snapshotFilePlans, at as createExcludeMatcher, b as surfaceManifestDiagnostics, c as formatRustTimingWaterfall, ct as KEYFRAMES_EXTERNAL_ENTRY_FAILED, d as runStructuralSelfCheck, dt as RETIRED_ENGINE_MESSAGE, et as DEFAULT_EXCLUDE, f as loadSystemConfig, ft as assertNoRetiredEngineSelection, g as clearEngineCache, gt as assembleStylesheet, h as buildAnalysisInputs, ht as ANIMUS_LAYERS, i as hashReplacementPlans, it as assertKnownOptionKeys, j as ingestSourceEntries, k as substituteAssetPlaceholders, l as buildSystemPropsModule, lt as mergeExternalKeyframes, m as serializeStaticCss, mt as buildAnalyzeProjectArgs, n as camelToKebab, nt as REPLACEABLE_DEFAULT_EXCLUDE, o as assertNoErrorDiagnostics, ot as resolveMode, p as applyPrefix, pt as createV2EngineApi, q as walkPackageSources, r as diffFilePlans, rt as STRUCTURAL_EXCLUDE, s as readTsconfigAliasPairs, st as KEYFRAMES_EXPORT_COLLISION, t as toWatchKeys, tt as DRIVER_NAMESPACES, u as buildDynamicPropConfig, ut as contentHash, v as applyUnitFallback, vt as validateLayerOrder, w as buildPathAliasesJson, x as unresolvedParentName, y as isUnresolvedParentDrop, z as excludeCollectedPackages } from "./pipeline-lkbOoyAr.mjs";
2
+ export { ANIMUS_LAYERS, AnimusConfigError, CORE_OPTION_KEYS, DEFAULT_EXCLUDE, DEFAULT_EXTENSIONS, DRIVER_NAMESPACES, ENGINE_TRANSFORM_EXTENSIONS, KEYFRAMES_EXPORT_COLLISION, KEYFRAMES_EXTERNAL_ENTRY_FAILED, REPLACEABLE_DEFAULT_EXCLUDE, RETIRED_ENGINE_MESSAGE, STRUCTURAL_EXCLUDE, adaptSvelteSource, applyPrefix, applyUnitFallback, assembleStylesheet, assertKnownOptionKeys, assertNoErrorDiagnostics, assertNoRetiredEngineSelection, buildAnalysisInputs, buildAnalyzeProjectArgs, buildDynamicPropConfig, buildPathAliasesJson, buildSystemPropsModule, camelToKebab, clearEngineCache, collectExternalPackageSources, contentHash, createExcludeMatcher, createSourceIdentity, createSourceIngestor, createV2EngineApi, diffFilePlans, discoverFiles, enforceExternalTokenContracts, excludeCollectedPackages, extractSystemFilePackages, findAssetSpecifiers, findPackageRoot, firstOwners, formatRustTimingWaterfall, hashReplacementPlans, ingestSourceEntries, isAdvisorySourceDiagnostic, isExcludedPackageRelativePath, isPathWithinRoot, isUnresolvedParentDrop, loadSystemConfig, mergeExternalKeyframes, postProcessCss, preprocessMdx, readTsconfigAliasPairs, resolveAbsolutePathSpecifier, resolveAssetFile, resolveLightningTargets, resolveMode, resolveThroughPathAliases, runProjectAnalysis, runStructuralSelfCheck, serializeStaticCss, sharesVolumeRoot, snapshotFilePlans, staleDistIncludesMessage, stripLeadingLayerDeclaration, substituteAssetPlaceholders, surfaceManifestDiagnostics, toWatchKeys, unresolvableIncludesMessage, unresolvedParentName, validateLayerOrder, walkPackageSources, withoutInvalidOriginals };