@animus-ui/extract 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/crates/extract-v2/animus-extract-v2.darwin-arm64.node +0 -0
- package/crates/extract-v2/animus-extract-v2.linux-arm64-gnu.node +0 -0
- package/crates/extract-v2/animus-extract-v2.linux-x64-gnu.node +0 -0
- package/dist/index.cjs +73 -2101
- package/dist/index.mjs +2 -2026
- package/dist/pipeline/analyze-project-args.d.ts.map +1 -0
- package/dist/pipeline/assemble-stylesheet.d.ts.map +1 -0
- package/dist/pipeline/asset-placeholders.d.ts.map +1 -0
- package/dist/pipeline/content-hash.d.ts.map +1 -0
- package/dist/pipeline/core-options.d.ts +162 -0
- package/dist/pipeline/core-options.d.ts.map +1 -0
- package/dist/pipeline/correlate-external-tokens.d.ts.map +1 -0
- package/dist/pipeline/discover-files.d.ts +12 -0
- package/dist/pipeline/discover-files.d.ts.map +1 -0
- package/dist/{discover-packages.d.ts → pipeline/discover-packages.d.ts} +7 -8
- package/dist/pipeline/discover-packages.d.ts.map +1 -0
- package/dist/pipeline/dynamic-prop-config.d.ts.map +1 -0
- package/dist/{engine-adapter.d.ts → pipeline/engine-adapter.d.ts} +2 -0
- package/dist/pipeline/engine-adapter.d.ts.map +1 -0
- package/dist/pipeline/engine-retirement.d.ts.map +1 -0
- package/dist/pipeline/error-diagnostics.d.ts.map +1 -0
- package/dist/pipeline/external-keyframes.d.ts.map +1 -0
- package/dist/{index.d.ts → pipeline/index.d.ts} +10 -1
- package/dist/pipeline/index.d.ts.map +1 -0
- package/dist/pipeline/manifest-diagnostics.d.ts.map +1 -0
- package/dist/{mdx-preprocessor.d.ts → pipeline/mdx-preprocessor.d.ts} +10 -0
- package/dist/pipeline/mdx-preprocessor.d.ts.map +1 -0
- package/dist/pipeline/path-aliases.d.ts.map +1 -0
- package/dist/pipeline/post-process-css.d.ts.map +1 -0
- package/dist/pipeline/prefix.d.ts.map +1 -0
- package/dist/pipeline/replacement-plans.d.ts.map +1 -0
- package/dist/pipeline/resolve-asset.d.ts.map +1 -0
- package/dist/pipeline/run-analysis.d.ts.map +1 -0
- package/dist/pipeline/source-identity.d.ts.map +1 -0
- package/dist/pipeline/source-ingestion.d.ts +173 -0
- package/dist/pipeline/source-ingestion.d.ts.map +1 -0
- package/dist/pipeline/static-css.d.ts.map +1 -0
- package/dist/pipeline/structural-self-check.d.ts +32 -0
- package/dist/pipeline/structural-self-check.d.ts.map +1 -0
- package/dist/pipeline/svelte-source-adapter.d.ts +88 -0
- package/dist/pipeline/svelte-source-adapter.d.ts.map +1 -0
- package/dist/pipeline/system-config.d.ts.map +1 -0
- package/dist/{system-props-module.d.ts → pipeline/system-props-module.d.ts} +4 -3
- package/dist/pipeline/system-props-module.d.ts.map +1 -0
- package/dist/pipeline/timing-waterfall.d.ts.map +1 -0
- package/dist/pipeline/tsconfig-paths.d.ts.map +1 -0
- package/dist/pipeline/unit-fallback.d.ts.map +1 -0
- package/dist/pipeline/utils.d.ts.map +1 -0
- package/dist/pipeline/watch-keys.d.ts.map +1 -0
- package/dist/pipeline-lkbOoyAr.mjs +3228 -0
- package/dist/pipeline-ojNeABi0.cjs +3685 -0
- package/dist/session/extraction-session.d.ts +446 -0
- package/dist/session/extraction-session.d.ts.map +1 -0
- package/dist/session/index.d.ts +24 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/published-set.d.ts +32 -0
- package/dist/session/published-set.d.ts.map +1 -0
- package/dist/session/resolve-packages.d.ts +8 -0
- package/dist/session/resolve-packages.d.ts.map +1 -0
- package/dist/session/session-paths.d.ts +131 -0
- package/dist/session/session-paths.d.ts.map +1 -0
- package/dist/session/singleton.d.ts +124 -0
- package/dist/session/singleton.d.ts.map +1 -0
- package/dist/session/timing.d.ts +7 -0
- package/dist/session/timing.d.ts.map +1 -0
- package/dist/session/turbopack-orchestrator.d.ts +43 -0
- package/dist/session/turbopack-orchestrator.d.ts.map +1 -0
- package/dist/session.cjs +2146 -0
- package/dist/session.mjs +2110 -0
- package/package.json +18 -6
- package/dist/analyze-project-args.d.ts.map +0 -1
- package/dist/assemble-stylesheet.d.ts.map +0 -1
- package/dist/asset-placeholders.d.ts.map +0 -1
- package/dist/content-hash.d.ts.map +0 -1
- package/dist/correlate-external-tokens.d.ts.map +0 -1
- package/dist/discover-files.d.ts +0 -10
- package/dist/discover-files.d.ts.map +0 -1
- package/dist/discover-packages.d.ts.map +0 -1
- package/dist/dynamic-prop-config.d.ts.map +0 -1
- package/dist/engine-adapter.d.ts.map +0 -1
- package/dist/engine-retirement.d.ts.map +0 -1
- package/dist/error-diagnostics.d.ts.map +0 -1
- package/dist/external-keyframes.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/manifest-diagnostics.d.ts.map +0 -1
- package/dist/mdx-preprocessor.d.ts.map +0 -1
- package/dist/path-aliases.d.ts.map +0 -1
- package/dist/post-process-css.d.ts.map +0 -1
- package/dist/prefix.d.ts.map +0 -1
- package/dist/replacement-plans.d.ts.map +0 -1
- package/dist/resolve-asset.d.ts.map +0 -1
- package/dist/run-analysis.d.ts.map +0 -1
- package/dist/source-identity.d.ts.map +0 -1
- package/dist/static-css.d.ts.map +0 -1
- package/dist/system-config.d.ts.map +0 -1
- package/dist/system-props-module.d.ts.map +0 -1
- package/dist/timing-waterfall.d.ts.map +0 -1
- package/dist/tsconfig-paths.d.ts.map +0 -1
- package/dist/unit-fallback.d.ts.map +0 -1
- package/dist/utils.d.ts.map +0 -1
- package/dist/watch-keys.d.ts.map +0 -1
- /package/dist/{analyze-project-args.d.ts → pipeline/analyze-project-args.d.ts} +0 -0
- /package/dist/{assemble-stylesheet.d.ts → pipeline/assemble-stylesheet.d.ts} +0 -0
- /package/dist/{asset-placeholders.d.ts → pipeline/asset-placeholders.d.ts} +0 -0
- /package/dist/{content-hash.d.ts → pipeline/content-hash.d.ts} +0 -0
- /package/dist/{correlate-external-tokens.d.ts → pipeline/correlate-external-tokens.d.ts} +0 -0
- /package/dist/{dynamic-prop-config.d.ts → pipeline/dynamic-prop-config.d.ts} +0 -0
- /package/dist/{engine-retirement.d.ts → pipeline/engine-retirement.d.ts} +0 -0
- /package/dist/{error-diagnostics.d.ts → pipeline/error-diagnostics.d.ts} +0 -0
- /package/dist/{external-keyframes.d.ts → pipeline/external-keyframes.d.ts} +0 -0
- /package/dist/{manifest-diagnostics.d.ts → pipeline/manifest-diagnostics.d.ts} +0 -0
- /package/dist/{path-aliases.d.ts → pipeline/path-aliases.d.ts} +0 -0
- /package/dist/{post-process-css.d.ts → pipeline/post-process-css.d.ts} +0 -0
- /package/dist/{prefix.d.ts → pipeline/prefix.d.ts} +0 -0
- /package/dist/{replacement-plans.d.ts → pipeline/replacement-plans.d.ts} +0 -0
- /package/dist/{resolve-asset.d.ts → pipeline/resolve-asset.d.ts} +0 -0
- /package/dist/{run-analysis.d.ts → pipeline/run-analysis.d.ts} +0 -0
- /package/dist/{source-identity.d.ts → pipeline/source-identity.d.ts} +0 -0
- /package/dist/{static-css.d.ts → pipeline/static-css.d.ts} +0 -0
- /package/dist/{system-config.d.ts → pipeline/system-config.d.ts} +0 -0
- /package/dist/{timing-waterfall.d.ts → pipeline/timing-waterfall.d.ts} +0 -0
- /package/dist/{tsconfig-paths.d.ts → pipeline/tsconfig-paths.d.ts} +0 -0
- /package/dist/{unit-fallback.d.ts → pipeline/unit-fallback.d.ts} +0 -0
- /package/dist/{utils.d.ts → pipeline/utils.d.ts} +0 -0
- /package/dist/{watch-keys.d.ts → pipeline/watch-keys.d.ts} +0 -0
|
@@ -0,0 +1,3228 @@
|
|
|
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 as basename$1, dirname as dirname$1, join as join$1, normalize, posix } 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
|
+
inputs.transformSourcesJson
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
111
|
+
//#region pipeline/engine-adapter.ts
|
|
112
|
+
/**
|
|
113
|
+
* Build the engine-agnostic `engineApi()` both plugins call. Returns a function
|
|
114
|
+
* (invoked per call site) so `isV2()` and `loadNativeEngine()` re-evaluate each
|
|
115
|
+
* time — next reads its engine choice and native module dynamically from
|
|
116
|
+
* globalThis.
|
|
117
|
+
*/
|
|
118
|
+
function createV2EngineApi(deps) {
|
|
119
|
+
const { label, isV2, loadNativeEngine, store } = deps;
|
|
120
|
+
return () => {
|
|
121
|
+
if (!isV2()) return loadNativeEngine();
|
|
122
|
+
const native = loadNativeEngine();
|
|
123
|
+
return {
|
|
124
|
+
loadSystemModule: (...args) => native.loadSystemModule(...args),
|
|
125
|
+
scanKeyframesExports: (entryPath, rootDir) => native.scanKeyframesExports(entryPath, rootDir) ?? null,
|
|
126
|
+
extractFacts: (filesJson) => native.extractFacts(filesJson),
|
|
127
|
+
analyzeProject: (filesJsonRaw, scalesJson, variableMapJson, contextualVarsJson, propConfigJson, groupRegistryJson, packageResolutionJson, devMode, emitterConfigJson, selectorAliasesJson, _selectorOrderJson, globalStyleBlocksJson, pathAliasesJson, keyframesJson, staticCssJson, conditionAliasesJson, externalDirsJson, transformSourcesJson) => {
|
|
128
|
+
const filesJson = deps.rehydrateFilesJson ? deps.rehydrateFilesJson(filesJsonRaw) : filesJsonRaw;
|
|
129
|
+
const sent = /* @__PURE__ */ new Map();
|
|
130
|
+
for (const entry of JSON.parse(filesJson)) sent.set(entry.path, entry.source);
|
|
131
|
+
store.setSentSources(sent);
|
|
132
|
+
const emitterConfig = emitterConfigJson ? JSON.parse(emitterConfigJson) : {};
|
|
133
|
+
store.setEngine(null);
|
|
134
|
+
const config = {
|
|
135
|
+
runtimeImport: emitterConfig.runtime_import ?? void 0,
|
|
136
|
+
cssModuleId: emitterConfig.css_module_id ?? void 0,
|
|
137
|
+
systemPropsModuleId: emitterConfig.system_props_module_id ?? void 0,
|
|
138
|
+
themeJson: scalesJson,
|
|
139
|
+
variableMapJson,
|
|
140
|
+
contextualVarsJson: contextualVarsJson ?? void 0,
|
|
141
|
+
configJson: propConfigJson,
|
|
142
|
+
groupRegistryJson,
|
|
143
|
+
selectorAliasesJson: selectorAliasesJson ?? void 0,
|
|
144
|
+
conditionAliasesJson: conditionAliasesJson ?? void 0,
|
|
145
|
+
globalStyleBlocksJson: globalStyleBlocksJson ?? void 0,
|
|
146
|
+
keyframesJson: keyframesJson ?? void 0,
|
|
147
|
+
packageResolutionJson: packageResolutionJson ?? void 0,
|
|
148
|
+
pathAliasesJson: pathAliasesJson ?? void 0,
|
|
149
|
+
staticCssJson: staticCssJson ?? void 0,
|
|
150
|
+
externalDirsJson: externalDirsJson ?? void 0,
|
|
151
|
+
transformSourcesJson: transformSourcesJson ?? void 0,
|
|
152
|
+
devMode
|
|
153
|
+
};
|
|
154
|
+
const engine = new native.ExtractEngine(config);
|
|
155
|
+
store.setEngine(engine);
|
|
156
|
+
return engine.analyze(filesJson);
|
|
157
|
+
},
|
|
158
|
+
transformFile: (source, path, _manifest) => {
|
|
159
|
+
const engine = store.getEngine();
|
|
160
|
+
if (!engine) throw new Error(`[${label}] v2 transform before analyze — engine instance not initialized`);
|
|
161
|
+
const sentMap = store.getSentSources();
|
|
162
|
+
if (deps.passThroughUnknownPaths && sentMap && !sentMap.has(path)) return {
|
|
163
|
+
code: source,
|
|
164
|
+
hasComponents: false
|
|
165
|
+
};
|
|
166
|
+
const sent = sentMap?.get(path);
|
|
167
|
+
if (sent !== void 0 && sent !== source && !store.getDriftWarned()) {
|
|
168
|
+
store.setDriftWarned(true);
|
|
169
|
+
console.warn(`[${label}] v2: transform-time source for ${path} differs from analyze-time source — an upstream transform may be reverted`);
|
|
170
|
+
}
|
|
171
|
+
return JSON.parse(engine.transformFile(path));
|
|
172
|
+
},
|
|
173
|
+
clearAnalysisCache: () => {
|
|
174
|
+
const engine = store.getEngine();
|
|
175
|
+
if (engine) engine.clearCache();
|
|
176
|
+
store.setEngine(null);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region pipeline/engine-retirement.ts
|
|
183
|
+
/**
|
|
184
|
+
* Loud rejection of the retired v1 extraction engine (openspec:
|
|
185
|
+
* retire-extract-v1).
|
|
186
|
+
*
|
|
187
|
+
* v2 is the only engine. Selecting v1 — via a plugin `engine: 'v1'` option or
|
|
188
|
+
* the `ANIMUS_ENGINE=v1` fixture env override — is a build-stopping error, never
|
|
189
|
+
* a silent upgrade to v2 (design D1: a silent flip would let a pinned consumer's
|
|
190
|
+
* build change engines without notice). Both extraction plugins call this before
|
|
191
|
+
* any engine selection so the rejection is uniform and the message is defined
|
|
192
|
+
* once.
|
|
193
|
+
*/
|
|
194
|
+
/** The one canonical retirement message, shared verbatim by both plugins and
|
|
195
|
+
* the env path (tasks.md cross-cutting: define once, reuse). */
|
|
196
|
+
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";
|
|
197
|
+
/**
|
|
198
|
+
* Throw the canonical retirement error when v1 is selected through either
|
|
199
|
+
* surface. Accepts the raw engine option as a string because the option type no
|
|
200
|
+
* longer admits `'v1'` — callers cast it so a stale `'v1'` config still fails
|
|
201
|
+
* loud at runtime instead of being silently coerced.
|
|
202
|
+
*/
|
|
203
|
+
function assertNoRetiredEngineSelection(engineOption) {
|
|
204
|
+
if (engineOption === "v1" || process.env.ANIMUS_ENGINE === "v1") throw new Error(RETIRED_ENGINE_MESSAGE);
|
|
205
|
+
}
|
|
206
|
+
//#endregion
|
|
207
|
+
//#region pipeline/content-hash.ts
|
|
208
|
+
/**
|
|
209
|
+
* MD5 content hash used for file-change detection (HMR diffing) by both
|
|
210
|
+
* extraction plugins, and for content-addressing copied asset bytes. The
|
|
211
|
+
* algorithm/encoding is a cross-plugin contract: cache keys written by one
|
|
212
|
+
* build path must compare equal in the next. Accepts a Buffer directly so
|
|
213
|
+
* binary sources hash without an intermediate string copy.
|
|
214
|
+
*/
|
|
215
|
+
function contentHash(source) {
|
|
216
|
+
return createHash("md5").update(source).digest("hex");
|
|
217
|
+
}
|
|
218
|
+
//#endregion
|
|
219
|
+
//#region pipeline/external-keyframes.ts
|
|
220
|
+
/** Stable codes for external keyframes discovery. */
|
|
221
|
+
const KEYFRAMES_EXTERNAL_ENTRY_FAILED = "animus.keyframes.external-entry-failed";
|
|
222
|
+
const KEYFRAMES_EXPORT_COLLISION = "animus.keyframes.export-collision";
|
|
223
|
+
/**
|
|
224
|
+
* Merge `Keyframes` collections discovered from external package entries into
|
|
225
|
+
* the consumer system's collections. Keyframes are the sole carve-out from
|
|
226
|
+
* the consumer-config singular-authority rule: nothing but branded
|
|
227
|
+
* collections is read from an entry, an entry that fails to evaluate
|
|
228
|
+
* degrades to a coded diagnostic naming it, and a collection whose export
|
|
229
|
+
* name collides keeps the earlier (consumer-first) collection with a coded
|
|
230
|
+
* diagnostic instead of silently reordering names.
|
|
231
|
+
*/
|
|
232
|
+
function mergeExternalKeyframes(scan, consumerKeyframesJson, externalEntries, rootDir) {
|
|
233
|
+
const merged = {};
|
|
234
|
+
if (consumerKeyframesJson) try {
|
|
235
|
+
Object.assign(merged, JSON.parse(consumerKeyframesJson));
|
|
236
|
+
} catch {
|
|
237
|
+
return {
|
|
238
|
+
keyframesJson: consumerKeyframesJson,
|
|
239
|
+
diagnostics: []
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
const diagnostics = [];
|
|
243
|
+
const seenEntries = /* @__PURE__ */ new Set();
|
|
244
|
+
for (const entryPath of externalEntries) {
|
|
245
|
+
if (seenEntries.has(entryPath)) continue;
|
|
246
|
+
seenEntries.add(entryPath);
|
|
247
|
+
let scanned;
|
|
248
|
+
try {
|
|
249
|
+
scanned = scan(entryPath, rootDir);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
diagnostics.push({
|
|
252
|
+
file: entryPath,
|
|
253
|
+
component: "keyframes",
|
|
254
|
+
kind: "warn",
|
|
255
|
+
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})`,
|
|
256
|
+
code: KEYFRAMES_EXTERNAL_ENTRY_FAILED,
|
|
257
|
+
severity: "warn"
|
|
258
|
+
});
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (!scanned) continue;
|
|
262
|
+
let collections;
|
|
263
|
+
try {
|
|
264
|
+
collections = JSON.parse(scanned);
|
|
265
|
+
} catch {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
for (const [exportName, collection] of Object.entries(collections)) {
|
|
269
|
+
const existing = merged[exportName];
|
|
270
|
+
if (existing !== void 0) {
|
|
271
|
+
if (JSON.stringify(existing) !== JSON.stringify(collection)) diagnostics.push({
|
|
272
|
+
file: entryPath,
|
|
273
|
+
component: exportName,
|
|
274
|
+
kind: "warn",
|
|
275
|
+
message: `keyframes collection export '${exportName}' collides with an earlier collection of the same name — the earlier one wins (${KEYFRAMES_EXPORT_COLLISION})`,
|
|
276
|
+
code: KEYFRAMES_EXPORT_COLLISION,
|
|
277
|
+
severity: "warn"
|
|
278
|
+
});
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
merged[exportName] = collection;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
keyframesJson: Object.keys(merged).length > 0 ? JSON.stringify(merged) : null,
|
|
286
|
+
diagnostics
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region pipeline/core-options.ts
|
|
291
|
+
/** Closed set of driver namespaces a shared config file may carry. */
|
|
292
|
+
const DRIVER_NAMESPACES = [
|
|
293
|
+
"vite",
|
|
294
|
+
"next",
|
|
295
|
+
"cli"
|
|
296
|
+
];
|
|
297
|
+
/**
|
|
298
|
+
* Structural exclusions applied unconditionally, whether or not the user
|
|
299
|
+
* supplies an `exclude` list. `node_modules` is owned by the external-package
|
|
300
|
+
* collection path (never the source walk), and `.next`/`.animus` are artifact
|
|
301
|
+
* output directories inside the scanned root that must never be re-ingested
|
|
302
|
+
* as source.
|
|
303
|
+
*/
|
|
304
|
+
const STRUCTURAL_EXCLUDE = [
|
|
305
|
+
"node_modules",
|
|
306
|
+
".next",
|
|
307
|
+
".animus"
|
|
308
|
+
];
|
|
309
|
+
/**
|
|
310
|
+
* Convenience defaults applied only when the user supplies no `exclude`
|
|
311
|
+
* list — a user list replaces these (but never the structural set).
|
|
312
|
+
*/
|
|
313
|
+
const REPLACEABLE_DEFAULT_EXCLUDE = [
|
|
314
|
+
"dist",
|
|
315
|
+
".test.",
|
|
316
|
+
".spec."
|
|
317
|
+
];
|
|
318
|
+
/**
|
|
319
|
+
* Default path patterns excluded from source discovery — the single
|
|
320
|
+
* authoritative set for every driver when no user `exclude` is given.
|
|
321
|
+
* Composed, never hand-listed: a structural addition must not silently
|
|
322
|
+
* miss this union.
|
|
323
|
+
*/
|
|
324
|
+
const DEFAULT_EXCLUDE = [...STRUCTURAL_EXCLUDE, ...REPLACEABLE_DEFAULT_EXCLUDE];
|
|
325
|
+
/** Core keys accepted at the top level of any driver's options. */
|
|
326
|
+
const CORE_OPTION_KEYS = new Set([
|
|
327
|
+
"system",
|
|
328
|
+
"exclude",
|
|
329
|
+
"extensions",
|
|
330
|
+
"strict",
|
|
331
|
+
"verbose",
|
|
332
|
+
"prefix",
|
|
333
|
+
"targets",
|
|
334
|
+
"minify",
|
|
335
|
+
"staticCss",
|
|
336
|
+
"layers",
|
|
337
|
+
"engine",
|
|
338
|
+
"mode",
|
|
339
|
+
"root"
|
|
340
|
+
]);
|
|
341
|
+
/** Configuration errors carry a stable name so drivers can map them to
|
|
342
|
+
* their config-error surface (the CLI's exit code 2). */
|
|
343
|
+
var AnimusConfigError = class extends Error {
|
|
344
|
+
constructor(message) {
|
|
345
|
+
super(message);
|
|
346
|
+
this.name = "AnimusConfigError";
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
/**
|
|
350
|
+
* Reject unknown top-level option keys and invalid core values. `ownKeys`
|
|
351
|
+
* is the calling driver's legacy/driver-specific top-level surface (e.g.
|
|
352
|
+
* Vite's `verify`, Next's `cssImportTarget`); driver namespaces are always
|
|
353
|
+
* legal and inert for non-owning drivers. `rejectKeys` names core keys the
|
|
354
|
+
* calling driver does NOT honor — accepting-and-ignoring a key the
|
|
355
|
+
* validator vouches for is the exact silent failure this seam exists to
|
|
356
|
+
* kill, so the driver must reject it with its reason (or, in `'warn'`
|
|
357
|
+
* mode, name it loudly while the key stays inert as it always was).
|
|
358
|
+
*/
|
|
359
|
+
function assertKnownOptionKeys(raw, ownKeys = [], rejectKeys = [], opts = {}) {
|
|
360
|
+
const surface = (message) => {
|
|
361
|
+
if (opts.onUnknownKey === "warn") {
|
|
362
|
+
opts.warn(message);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
throw new AnimusConfigError(message);
|
|
366
|
+
};
|
|
367
|
+
const own = new Set(ownKeys);
|
|
368
|
+
for (const key of Object.keys(raw)) {
|
|
369
|
+
if (raw[key] === void 0) continue;
|
|
370
|
+
const rejected = rejectKeys.find((entry) => entry.key === key);
|
|
371
|
+
if (rejected) {
|
|
372
|
+
surface(`Option "${key}" is not supported by this driver: ${rejected.reason}`);
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (CORE_OPTION_KEYS.has(key) || DRIVER_NAMESPACES.includes(key) || own.has(key)) continue;
|
|
376
|
+
const nearest = suggestNearest(key);
|
|
377
|
+
surface(`Unknown option "${key}".${nearest ? ` Did you mean "${nearest}"?` : ""} Core keys: ${[...CORE_OPTION_KEYS].join(", ")}. Driver namespaces: ${DRIVER_NAMESPACES.join(", ")}.`);
|
|
378
|
+
}
|
|
379
|
+
const mode = raw["mode"];
|
|
380
|
+
if (mode !== void 0 && mode !== "development" && mode !== "production") throw new AnimusConfigError(`Invalid mode "${String(mode)}" — expected "development" or "production".`);
|
|
381
|
+
for (const { key, ok, expected } of CORE_VALUE_GATES) {
|
|
382
|
+
const value = raw[key];
|
|
383
|
+
if (value === void 0 || ok(value)) continue;
|
|
384
|
+
throw new AnimusConfigError(`Invalid value for "${key}" — expected ${expected}, got ${JSON.stringify(value)}.`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const isStringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
388
|
+
const CORE_VALUE_GATES = [
|
|
389
|
+
{
|
|
390
|
+
key: "system",
|
|
391
|
+
ok: (v) => typeof v === "string",
|
|
392
|
+
expected: "a string path"
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
key: "exclude",
|
|
396
|
+
ok: isStringArray,
|
|
397
|
+
expected: "an array of string patterns"
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
key: "extensions",
|
|
401
|
+
ok: isStringArray,
|
|
402
|
+
expected: "an array of string extensions"
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
key: "strict",
|
|
406
|
+
ok: (v) => typeof v === "boolean",
|
|
407
|
+
expected: "a boolean"
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
key: "verbose",
|
|
411
|
+
ok: (v) => typeof v === "boolean",
|
|
412
|
+
expected: "a boolean"
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
key: "minify",
|
|
416
|
+
ok: (v) => typeof v === "boolean",
|
|
417
|
+
expected: "a boolean"
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
key: "prefix",
|
|
421
|
+
ok: (v) => typeof v === "string",
|
|
422
|
+
expected: "a string"
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
key: "targets",
|
|
426
|
+
ok: (v) => typeof v === "string" || isStringArray(v),
|
|
427
|
+
expected: "a string or an array of strings"
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
key: "layers",
|
|
431
|
+
ok: isStringArray,
|
|
432
|
+
expected: "an array of layer names"
|
|
433
|
+
}
|
|
434
|
+
];
|
|
435
|
+
function suggestNearest(key) {
|
|
436
|
+
const lower = key.toLowerCase();
|
|
437
|
+
let best = null;
|
|
438
|
+
let bestScore = 3;
|
|
439
|
+
for (const candidate of CORE_OPTION_KEYS) {
|
|
440
|
+
const score = editDistance(lower, candidate.toLowerCase());
|
|
441
|
+
if (score < bestScore) {
|
|
442
|
+
bestScore = score;
|
|
443
|
+
best = candidate;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return best;
|
|
447
|
+
}
|
|
448
|
+
function editDistance(a, b) {
|
|
449
|
+
const dp = Array.from({ length: a.length + 1 }, (_, i) => [i]);
|
|
450
|
+
for (let j = 1; j <= b.length; j++) dp[0][j] = j;
|
|
451
|
+
for (let i = 1; i <= a.length; i++) for (let j = 1; j <= b.length; j++) dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
452
|
+
return dp[a.length][b.length];
|
|
453
|
+
}
|
|
454
|
+
const GLOB_META = /[*?]/;
|
|
455
|
+
/**
|
|
456
|
+
* Compile one exclusion pattern. Glob patterns (containing `*` or `?`)
|
|
457
|
+
* compile to a RegExp over the slash-normalized root-relative path — a
|
|
458
|
+
* match on the path itself OR any ancestor directory excludes it, so a
|
|
459
|
+
* directory-shaped glob (double-star slash "generated") excludes files
|
|
460
|
+
* inside that directory identically for tree discovery (which prunes at
|
|
461
|
+
* the directory) and per-file watch classification (which only ever sees
|
|
462
|
+
* file paths).
|
|
463
|
+
* Plain patterns keep the historical substring semantics over both path
|
|
464
|
+
* forms.
|
|
465
|
+
* A leading `./` is stripped first: relative paths are computed without
|
|
466
|
+
* one, so `./fixtures/**` must mean exactly `fixtures/**` rather than
|
|
467
|
+
* compiling to a regex that can never match.
|
|
468
|
+
*/
|
|
469
|
+
function compilePattern(raw) {
|
|
470
|
+
let pattern = raw.split("\\").join("/");
|
|
471
|
+
while (pattern.startsWith("./")) pattern = pattern.slice(2);
|
|
472
|
+
if (pattern === "") return () => false;
|
|
473
|
+
if (!GLOB_META.test(pattern)) return (full, rel) => full.includes(pattern) || rel.includes(pattern);
|
|
474
|
+
const regex = globToRegExp(pattern);
|
|
475
|
+
return (_full, rel) => {
|
|
476
|
+
const normalized = rel.split("\\").join("/");
|
|
477
|
+
if (regex.test(normalized)) return true;
|
|
478
|
+
for (let slash = normalized.indexOf("/"); slash !== -1; slash = normalized.indexOf("/", slash + 1)) if (regex.test(normalized.slice(0, slash))) return true;
|
|
479
|
+
return false;
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
/** Minimal glob → RegExp: `**` spans segments, `*`/`?` stay within one. */
|
|
483
|
+
function globToRegExp(glob) {
|
|
484
|
+
let out = "";
|
|
485
|
+
let i = 0;
|
|
486
|
+
const normalized = glob.split("\\").join("/");
|
|
487
|
+
while (i < normalized.length) {
|
|
488
|
+
const ch = normalized[i];
|
|
489
|
+
if (ch === "*") if (normalized[i + 1] === "*") if (normalized[i + 2] === "/") {
|
|
490
|
+
out += "(?:[^/]+/)*";
|
|
491
|
+
i += 3;
|
|
492
|
+
} else {
|
|
493
|
+
out += ".*";
|
|
494
|
+
i += 2;
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
out += "[^/]*";
|
|
498
|
+
i += 1;
|
|
499
|
+
}
|
|
500
|
+
else if (ch === "?") {
|
|
501
|
+
out += "[^/]";
|
|
502
|
+
i += 1;
|
|
503
|
+
} else {
|
|
504
|
+
out += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
505
|
+
i += 1;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return new RegExp(`^${out}$`);
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Build the effective exclusion matcher. A user list REPLACES the
|
|
512
|
+
* replaceable defaults (`dist`, `.test.`, `.spec.`) — the HEAD driver
|
|
513
|
+
* contract (`options.exclude ?? DEFAULT_EXCLUDE`) — while the structural
|
|
514
|
+
* exclusions (`node_modules`, `.next`, `.animus`) always apply: those
|
|
515
|
+
* protect pipeline invariants, not preferences, so no user list can
|
|
516
|
+
* re-admit them. `extraStructural` joins that never-replaceable set — the
|
|
517
|
+
* channel for driver-owned invariants (a CLI outDir inside the root),
|
|
518
|
+
* which must NOT ride the user list: appending there would flip the
|
|
519
|
+
* replace semantics and silently drop the replaceable defaults.
|
|
520
|
+
*/
|
|
521
|
+
function createExcludeMatcher(userPatterns, extraStructural = []) {
|
|
522
|
+
const merged = [...STRUCTURAL_EXCLUDE];
|
|
523
|
+
for (const pattern of extraStructural) if (!merged.includes(pattern)) merged.push(pattern);
|
|
524
|
+
for (const pattern of userPatterns ?? REPLACEABLE_DEFAULT_EXCLUDE) if (!merged.includes(pattern)) merged.push(pattern);
|
|
525
|
+
const compiled = merged.map((pattern) => [pattern, compilePattern(pattern)]);
|
|
526
|
+
const hits = new Map(merged.map((p) => [p, 0]));
|
|
527
|
+
const explain = (full, rel) => {
|
|
528
|
+
for (const [pattern, test] of compiled) if (test(full, rel)) {
|
|
529
|
+
hits.set(pattern, (hits.get(pattern) ?? 0) + 1);
|
|
530
|
+
return pattern;
|
|
531
|
+
}
|
|
532
|
+
return null;
|
|
533
|
+
};
|
|
534
|
+
return {
|
|
535
|
+
patterns: merged,
|
|
536
|
+
matches: (full, rel) => explain(full, rel) !== null,
|
|
537
|
+
explain,
|
|
538
|
+
stats: () => hits
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Resolve the effective mode: the explicit `mode` key wins over every
|
|
543
|
+
* environment-derived signal; otherwise the driver's documented default
|
|
544
|
+
* applies.
|
|
545
|
+
*/
|
|
546
|
+
function resolveMode(explicit, driverDefault) {
|
|
547
|
+
if (explicit === "development" || explicit === "production") return {
|
|
548
|
+
mode: explicit,
|
|
549
|
+
provenance: "explicit"
|
|
550
|
+
};
|
|
551
|
+
return {
|
|
552
|
+
mode: driverDefault(),
|
|
553
|
+
provenance: "driver-default"
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
//#endregion
|
|
557
|
+
//#region pipeline/discover-files.ts
|
|
558
|
+
/**
|
|
559
|
+
* Recursively discover source files under `dir`, skipping excluded paths
|
|
560
|
+
* (an `ExcludeMatcher` — see core-options, the one pattern authority — or
|
|
561
|
+
* undefined for NO exclusion at all: the dist-package walks deliberately
|
|
562
|
+
* run under node_modules, which `createExcludeMatcher`'s structural set
|
|
563
|
+
* would prune) and keeping files whose extension is in `extensionsSet`.
|
|
564
|
+
*
|
|
565
|
+
* Single authoritative copy for both extraction plugins.
|
|
566
|
+
*/
|
|
567
|
+
function discoverFiles(dir, rootDir, exclude, extensionsSet) {
|
|
568
|
+
const results = [];
|
|
569
|
+
let entries;
|
|
570
|
+
try {
|
|
571
|
+
entries = readdirSync(dir, { encoding: "utf8" });
|
|
572
|
+
} catch {
|
|
573
|
+
return results;
|
|
574
|
+
}
|
|
575
|
+
entries.sort();
|
|
576
|
+
const isExcluded = exclude ? (full, rel) => exclude.matches(full, rel) : () => false;
|
|
577
|
+
for (const entry of entries) {
|
|
578
|
+
const fullPath = join(dir, entry);
|
|
579
|
+
if (isExcluded(fullPath, relative(rootDir, fullPath))) continue;
|
|
580
|
+
let stat;
|
|
581
|
+
try {
|
|
582
|
+
stat = statSync(fullPath);
|
|
583
|
+
} catch {
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
if (stat.isDirectory()) results.push(...discoverFiles(fullPath, rootDir, exclude, extensionsSet));
|
|
587
|
+
else if (extensionsSet.has(extname(entry))) results.push(fullPath);
|
|
588
|
+
}
|
|
589
|
+
return results;
|
|
590
|
+
}
|
|
591
|
+
//#endregion
|
|
592
|
+
//#region pipeline/source-identity.ts
|
|
593
|
+
/**
|
|
594
|
+
* SourceId derivation authority + allowlist membership (openspec:
|
|
595
|
+
* external-source-watch-ingestion, design D1/D2/D5).
|
|
596
|
+
*
|
|
597
|
+
* One shared derivation produces the canonical source identity consumed by
|
|
598
|
+
* discovery, watch ingestion, ownership, deletion pruning, and diagnostics
|
|
599
|
+
* — raw event paths are lookup INPUTS only, so event spelling (symlink
|
|
600
|
+
* alias vs canonical, lexical vs realpath) can never fork identity.
|
|
601
|
+
*
|
|
602
|
+
* The containment and volume helpers are pure and path-API-injectable so
|
|
603
|
+
* Windows semantics are unit-testable via `path.win32` on any host; runtime
|
|
604
|
+
* callers use the ambient platform implementation.
|
|
605
|
+
*/
|
|
606
|
+
/**
|
|
607
|
+
* Structural containment: `target` is the root itself or a descendant of it,
|
|
608
|
+
* decided by `relative()` shape — never by string prefixing, so `/ui` can
|
|
609
|
+
* never claim `/ui-old`, and a cross-drive win32 target (whose relative()
|
|
610
|
+
* result is absolute) is never contained.
|
|
611
|
+
*/
|
|
612
|
+
function isPathWithinRoot(root, target, pathApi = path) {
|
|
613
|
+
const rel = pathApi.relative(root, target);
|
|
614
|
+
return rel === "" || !pathApi.isAbsolute(rel) && rel !== ".." && !rel.startsWith(".." + pathApi.sep);
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Cross-volume gate (design D5): two paths share a platform path root
|
|
618
|
+
* (win32 drive letter or UNC share; always true on posix). Compared
|
|
619
|
+
* case-insensitively — win32 drive letters are case-insensitive.
|
|
620
|
+
*/
|
|
621
|
+
function sharesVolumeRoot(a, b, pathApi = path) {
|
|
622
|
+
const volumeOf = (p) => pathApi.parse(pathApi.resolve(p)).root.toLowerCase();
|
|
623
|
+
return volumeOf(a) === volumeOf(b);
|
|
624
|
+
}
|
|
625
|
+
function canonicalize(p) {
|
|
626
|
+
try {
|
|
627
|
+
return realpathSync$1(p);
|
|
628
|
+
} catch {
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
function createSourceIdentity(rootDir) {
|
|
633
|
+
const normalizedRootDir = path.normalize(rootDir);
|
|
634
|
+
const canonicalRootDir = canonicalize(normalizedRootDir) ?? normalizedRootDir;
|
|
635
|
+
const roots = [];
|
|
636
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
637
|
+
const owningRecord = (canonicalPath) => {
|
|
638
|
+
let owner = null;
|
|
639
|
+
for (const record of roots) {
|
|
640
|
+
if (!isPathWithinRoot(record.canonical, canonicalPath)) continue;
|
|
641
|
+
if (!owner || record.canonical.length > owner.canonical.length) owner = record;
|
|
642
|
+
}
|
|
643
|
+
return owner;
|
|
644
|
+
};
|
|
645
|
+
return {
|
|
646
|
+
rootDir: normalizedRootDir,
|
|
647
|
+
canonicalRootDir,
|
|
648
|
+
registerExternalRoot(root) {
|
|
649
|
+
const asRegistered = path.normalize(root);
|
|
650
|
+
const canonical = canonicalize(asRegistered) ?? asRegistered;
|
|
651
|
+
const existing = roots.find((record) => record.canonical === canonical);
|
|
652
|
+
if (existing) return existing.canonical;
|
|
653
|
+
roots.push({
|
|
654
|
+
asRegistered,
|
|
655
|
+
canonical
|
|
656
|
+
});
|
|
657
|
+
return canonical;
|
|
658
|
+
},
|
|
659
|
+
externalRoots() {
|
|
660
|
+
return roots.map((record) => record.canonical);
|
|
661
|
+
},
|
|
662
|
+
resolveSourceId(inputPath) {
|
|
663
|
+
const lexical = path.normalize(inputPath);
|
|
664
|
+
const canonical = canonicalize(lexical);
|
|
665
|
+
if (canonical === null) return null;
|
|
666
|
+
const owner = owningRecord(canonical);
|
|
667
|
+
const ownedBy = (record) => {
|
|
668
|
+
const pathInRoot = path.relative(record.canonical, canonical);
|
|
669
|
+
return {
|
|
670
|
+
sourceKey: path.relative(normalizedRootDir, path.join(record.asRegistered, pathInRoot)),
|
|
671
|
+
owningRoot: record.canonical,
|
|
672
|
+
pathInRoot
|
|
673
|
+
};
|
|
674
|
+
};
|
|
675
|
+
const ownerIsMoreSpecific = owner !== null && owner.canonical.length > canonicalRootDir.length;
|
|
676
|
+
let resolved;
|
|
677
|
+
if (!ownerIsMoreSpecific && isPathWithinRoot(canonicalRootDir, canonical)) {
|
|
678
|
+
const key = path.relative(canonicalRootDir, canonical);
|
|
679
|
+
resolved = {
|
|
680
|
+
sourceKey: key,
|
|
681
|
+
owningRoot: null,
|
|
682
|
+
pathInRoot: key
|
|
683
|
+
};
|
|
684
|
+
} else if (owner) resolved = ownedBy(owner);
|
|
685
|
+
else return null;
|
|
686
|
+
aliases.set(lexical, resolved);
|
|
687
|
+
aliases.set(canonical, resolved);
|
|
688
|
+
return resolved;
|
|
689
|
+
},
|
|
690
|
+
resolveDeletedSourceId(inputPath) {
|
|
691
|
+
return aliases.get(path.normalize(inputPath)) ?? null;
|
|
692
|
+
},
|
|
693
|
+
containingExternalRoot(inputPath) {
|
|
694
|
+
const lexical = path.normalize(inputPath);
|
|
695
|
+
let owner = null;
|
|
696
|
+
for (const record of roots) {
|
|
697
|
+
if (!(isPathWithinRoot(record.canonical, lexical) || isPathWithinRoot(record.asRegistered, lexical))) continue;
|
|
698
|
+
if (!owner || record.canonical.length > owner.canonical.length) owner = record;
|
|
699
|
+
}
|
|
700
|
+
if (owner) return owner.canonical;
|
|
701
|
+
const canonical = canonicalize(lexical);
|
|
702
|
+
if (canonical === null) return null;
|
|
703
|
+
for (const record of roots) {
|
|
704
|
+
if (!isPathWithinRoot(record.canonical, canonical)) continue;
|
|
705
|
+
if (!owner || record.canonical.length > owner.canonical.length) owner = record;
|
|
706
|
+
}
|
|
707
|
+
return owner?.canonical ?? null;
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
//#endregion
|
|
712
|
+
//#region pipeline/discover-packages.ts
|
|
713
|
+
/**
|
|
714
|
+
* Walk up from a resolved package entry file to the nearest directory
|
|
715
|
+
* containing a package.json (the package root). Stops at the filesystem
|
|
716
|
+
* root and returns it if no package.json is found on the way up.
|
|
717
|
+
*/
|
|
718
|
+
function findPackageRoot(absEntryPath) {
|
|
719
|
+
let pkgRoot = dirname(absEntryPath);
|
|
720
|
+
while (pkgRoot !== dirname(pkgRoot) && !existsSync(join(pkgRoot, "package.json"))) pkgRoot = dirname(pkgRoot);
|
|
721
|
+
return pkgRoot;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Exclusion fragments for discovery inside an external package's src/,
|
|
725
|
+
* matched against paths RELATIVE TO the src dir. Per spec
|
|
726
|
+
* external-package-file-discovery, the filters apply "within the package" —
|
|
727
|
+
* an npm-installed package whose own location is under node_modules (or a
|
|
728
|
+
* repo path containing 'dist') must still have its sources discovered.
|
|
729
|
+
*/
|
|
730
|
+
const PACKAGE_SRC_EXCLUDES = [
|
|
731
|
+
"dist",
|
|
732
|
+
"node_modules",
|
|
733
|
+
".test.",
|
|
734
|
+
".spec."
|
|
735
|
+
];
|
|
736
|
+
/** True when a PACKAGE-RELATIVE path matches a package-source exclusion
|
|
737
|
+
* fragment (the "within the package" filter — see PACKAGE_SRC_EXCLUDES). */
|
|
738
|
+
function isExcludedPackageRelativePath(rel) {
|
|
739
|
+
return PACKAGE_SRC_EXCLUDES.some((pattern) => rel.includes(pattern));
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* THE package-source walk (guardrail G1 — one policy): shared discovery with
|
|
743
|
+
* no consumer patterns, then exclusion by package-relative path so fragments
|
|
744
|
+
* in the package's own location can't blank out its sources. Used by the
|
|
745
|
+
* collector, hosts' dirty-root rewalks, and watch-path classification alike.
|
|
746
|
+
*/
|
|
747
|
+
function walkPackageSources(packageDir, extensionsSet) {
|
|
748
|
+
return discoverFiles(packageDir, packageDir, void 0, extensionsSet).filter((absPath) => !isExcludedPackageRelativePath(relative(packageDir, absPath)));
|
|
749
|
+
}
|
|
750
|
+
/** Exclusions for the compiled-output fallback when a package does not ship src/. */
|
|
751
|
+
const PACKAGE_OUTPUT_EXCLUDES = [
|
|
752
|
+
"node_modules",
|
|
753
|
+
".test.",
|
|
754
|
+
".spec.",
|
|
755
|
+
".d.ts",
|
|
756
|
+
".map"
|
|
757
|
+
];
|
|
758
|
+
const isFile = (path) => {
|
|
759
|
+
try {
|
|
760
|
+
return statSync(path).isFile();
|
|
761
|
+
} catch {
|
|
762
|
+
return false;
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
/**
|
|
766
|
+
* Resolve an absolute-path specifier (a relative `includes` entry already
|
|
767
|
+
* resolved against the system file) without a bundler: the path itself when it
|
|
768
|
+
* is a file, otherwise the extension and directory-index candidates built from
|
|
769
|
+
* the caller's extension set. Used only as a fallback — a bundler resolver that
|
|
770
|
+
* answers first still wins, since it also knows aliases and `.js`→`.ts` mapping.
|
|
771
|
+
*/
|
|
772
|
+
function resolveAbsolutePathSpecifier(absSpecifier, extensionsSet) {
|
|
773
|
+
return [
|
|
774
|
+
absSpecifier,
|
|
775
|
+
...Array.from(extensionsSet, (ext) => absSpecifier + ext),
|
|
776
|
+
...Array.from(extensionsSet, (ext) => join(absSpecifier, `index${ext}`))
|
|
777
|
+
].find(isFile) ?? null;
|
|
778
|
+
}
|
|
779
|
+
/** The package-name half of a bare specifier (`@scope/name/sub` → `@scope/name`). */
|
|
780
|
+
function bareSpecifierPackageName(specifier) {
|
|
781
|
+
const segments = specifier.split("/");
|
|
782
|
+
return specifier.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0];
|
|
783
|
+
}
|
|
784
|
+
function sourceEntryForSpecifier(specifier, srcDir, extensionsSet) {
|
|
785
|
+
if (isAbsolute(specifier)) {
|
|
786
|
+
const resolved = resolveAbsolutePathSpecifier(specifier, extensionsSet);
|
|
787
|
+
if (!resolved) return null;
|
|
788
|
+
return isPathWithinRoot(srcDir, resolved) ? resolved : null;
|
|
789
|
+
}
|
|
790
|
+
const packageName = bareSpecifierPackageName(specifier);
|
|
791
|
+
return resolveAbsolutePathSpecifier(join(srcDir, specifier.slice(packageName.length + 1) || "index"), extensionsSet);
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* The D13 dist-freshness check: applicable only when a specifier has BOTH a
|
|
795
|
+
* walked src/ tree and a resolved dist entry that exists on disk outside that
|
|
796
|
+
* tree; stale when the dist entry's mtime is older than the newest walked
|
|
797
|
+
* source file's. Not applicable (never stale) when the entry resolves inside
|
|
798
|
+
* src/ (no dist to skew), when the entry file is missing, or when no source
|
|
799
|
+
* file's mtime is readable. Detection only — reporting is the caller's policy.
|
|
800
|
+
*/
|
|
801
|
+
function distEntryIsStale(absEntry, srcDir, srcFiles) {
|
|
802
|
+
if (isPathWithinRoot(srcDir, absEntry)) return false;
|
|
803
|
+
let distMtime;
|
|
804
|
+
try {
|
|
805
|
+
distMtime = statSync(absEntry).mtimeMs;
|
|
806
|
+
} catch {
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
let newestSrcMtime = -Infinity;
|
|
810
|
+
for (const srcFile of srcFiles) try {
|
|
811
|
+
newestSrcMtime = Math.max(newestSrcMtime, statSync(srcFile).mtimeMs);
|
|
812
|
+
} catch {}
|
|
813
|
+
return distMtime < newestSrcMtime;
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Shared external-package source collection (spec:
|
|
817
|
+
* external-package-file-discovery), consumed by both extraction plugins.
|
|
818
|
+
* For each specifier: resolve to an absolute entry (null/throw → silently
|
|
819
|
+
* skip; an absolute-path specifier falls back to a filesystem probe), walk up
|
|
820
|
+
* to the package root, then either discover sources under src/ (redirecting
|
|
821
|
+
* module resolution to src/index.ts when present) or fall back to ingesting
|
|
822
|
+
* the resolved entry file itself.
|
|
823
|
+
*
|
|
824
|
+
* The only bundler-specific seams are callbacks: specifier resolution,
|
|
825
|
+
* dedup against already-ingested files, per-file preprocessing (MDX), and
|
|
826
|
+
* the unreadable-file warning. Hashing and caching stay in the plugins —
|
|
827
|
+
* their cache policies legitimately differ.
|
|
828
|
+
*
|
|
829
|
+
* Alongside the aggregates, one `ExternalPackageOutcome` per declared
|
|
830
|
+
* specifier records what that specifier produced. Collection never reports on
|
|
831
|
+
* them — any warning or gate is the caller's policy.
|
|
832
|
+
*/
|
|
833
|
+
async function collectExternalPackageSources(opts) {
|
|
834
|
+
const { specifiers, resolveSpecifier, rootDir, extensionsSet, hasEntry, onSourceRead, onUnreadable, onPackageResolved } = opts;
|
|
835
|
+
const entries = [];
|
|
836
|
+
const pushed = /* @__PURE__ */ new Set();
|
|
837
|
+
const packageMap = {};
|
|
838
|
+
const sourceEntries = /* @__PURE__ */ new Map();
|
|
839
|
+
const keyframesScanEntries = /* @__PURE__ */ new Map();
|
|
840
|
+
const packageDirs = [];
|
|
841
|
+
const dirOwnerSets = {};
|
|
842
|
+
const dirExtensions = {};
|
|
843
|
+
const fileOwners = {};
|
|
844
|
+
const outcomes = [];
|
|
845
|
+
const claimDir = (dir, specifier) => {
|
|
846
|
+
(dirOwnerSets[dir] ??= []).push(specifier);
|
|
847
|
+
};
|
|
848
|
+
const alreadyIngested = (relPath) => hasEntry(relPath) || pushed.has(relPath);
|
|
849
|
+
for (const specifier of specifiers) {
|
|
850
|
+
let absEntry;
|
|
851
|
+
try {
|
|
852
|
+
absEntry = await resolveSpecifier(specifier);
|
|
853
|
+
} catch {
|
|
854
|
+
absEntry = null;
|
|
855
|
+
}
|
|
856
|
+
if (!absEntry && isAbsolute(specifier)) absEntry = resolveAbsolutePathSpecifier(specifier, extensionsSet);
|
|
857
|
+
if (!absEntry) {
|
|
858
|
+
outcomes.push({
|
|
859
|
+
specifier,
|
|
860
|
+
outcome: "unresolvable",
|
|
861
|
+
fileCount: 0
|
|
862
|
+
});
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
const srcDir = join(findPackageRoot(absEntry), "src");
|
|
866
|
+
let fileCount = 0;
|
|
867
|
+
let staleDist = false;
|
|
868
|
+
if (existsSync(srcDir)) {
|
|
869
|
+
packageDirs.push(srcDir);
|
|
870
|
+
claimDir(srcDir, specifier);
|
|
871
|
+
dirExtensions[srcDir] = [...extensionsSet];
|
|
872
|
+
onPackageResolved?.(specifier, srcDir);
|
|
873
|
+
const srcEntry = sourceEntryForSpecifier(specifier, srcDir, extensionsSet);
|
|
874
|
+
if (srcEntry) {
|
|
875
|
+
packageMap[specifier] = relative(rootDir, srcEntry);
|
|
876
|
+
sourceEntries.set(specifier, srcEntry);
|
|
877
|
+
} else packageMap[specifier] = relative(rootDir, absEntry);
|
|
878
|
+
keyframesScanEntries.set(specifier, srcEntry ?? absEntry);
|
|
879
|
+
if (!isAbsolute(specifier)) {
|
|
880
|
+
const packageName = bareSpecifierPackageName(specifier);
|
|
881
|
+
if (packageName !== specifier && !(packageName in packageMap)) {
|
|
882
|
+
const rootEntry = sourceEntryForSpecifier(packageName, srcDir, extensionsSet);
|
|
883
|
+
if (rootEntry) {
|
|
884
|
+
packageMap[packageName] = relative(rootDir, rootEntry);
|
|
885
|
+
sourceEntries.set(packageName, rootEntry);
|
|
886
|
+
keyframesScanEntries.set(packageName, rootEntry);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
const pkgFiles = walkPackageSources(srcDir, extensionsSet);
|
|
891
|
+
staleDist = distEntryIsStale(absEntry, srcDir, pkgFiles);
|
|
892
|
+
for (const pkgFile of pkgFiles) {
|
|
893
|
+
const relPath = relative(rootDir, pkgFile);
|
|
894
|
+
if (alreadyIngested(relPath)) {
|
|
895
|
+
fileCount++;
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
let source;
|
|
899
|
+
try {
|
|
900
|
+
source = readFileSync(pkgFile, "utf-8");
|
|
901
|
+
} catch (err) {
|
|
902
|
+
onUnreadable(relPath, err);
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
onSourceRead?.(source, relPath, pkgFile);
|
|
906
|
+
entries.push({
|
|
907
|
+
path: relPath,
|
|
908
|
+
source
|
|
909
|
+
});
|
|
910
|
+
pushed.add(relPath);
|
|
911
|
+
fileOwners[relPath] ??= specifier;
|
|
912
|
+
fileCount++;
|
|
913
|
+
}
|
|
914
|
+
} else {
|
|
915
|
+
const outputDir = dirname(absEntry);
|
|
916
|
+
packageDirs.push(outputDir);
|
|
917
|
+
claimDir(outputDir, specifier);
|
|
918
|
+
onPackageResolved?.(specifier, outputDir);
|
|
919
|
+
packageMap[specifier] = relative(rootDir, absEntry);
|
|
920
|
+
keyframesScanEntries.set(specifier, absEntry);
|
|
921
|
+
const outputExtensions = new Set(extensionsSet);
|
|
922
|
+
outputExtensions.add(extname(absEntry));
|
|
923
|
+
dirExtensions[outputDir] = [...outputExtensions];
|
|
924
|
+
const outputFiles = discoverFiles(outputDir, outputDir, void 0, outputExtensions).filter((file) => {
|
|
925
|
+
const relToOutput = relative(outputDir, file);
|
|
926
|
+
return !PACKAGE_OUTPUT_EXCLUDES.some((pattern) => relToOutput.includes(pattern));
|
|
927
|
+
});
|
|
928
|
+
if (!outputFiles.includes(absEntry)) outputFiles.unshift(absEntry);
|
|
929
|
+
for (const outputFile of outputFiles) {
|
|
930
|
+
const outputRelPath = relative(rootDir, outputFile);
|
|
931
|
+
if (alreadyIngested(outputRelPath)) {
|
|
932
|
+
fileCount++;
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
let source;
|
|
936
|
+
try {
|
|
937
|
+
source = readFileSync(outputFile, "utf-8");
|
|
938
|
+
} catch (err) {
|
|
939
|
+
onUnreadable(outputRelPath, err);
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
onSourceRead?.(source, outputRelPath, outputFile);
|
|
943
|
+
entries.push({
|
|
944
|
+
path: outputRelPath,
|
|
945
|
+
source
|
|
946
|
+
});
|
|
947
|
+
pushed.add(outputRelPath);
|
|
948
|
+
fileOwners[outputRelPath] ??= specifier;
|
|
949
|
+
fileCount++;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
outcomes.push({
|
|
953
|
+
specifier,
|
|
954
|
+
outcome: staleDist ? "stale-dist" : fileCount > 0 ? "resolved" : "empty",
|
|
955
|
+
fileCount
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
return {
|
|
959
|
+
entries,
|
|
960
|
+
packageMap,
|
|
961
|
+
sourceEntries,
|
|
962
|
+
keyframesScanEntries,
|
|
963
|
+
packageDirs,
|
|
964
|
+
dirOwnerSets,
|
|
965
|
+
dirExtensions,
|
|
966
|
+
fileOwners,
|
|
967
|
+
outcomes
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
/** First-declared specifier per package dir — the single-value ownership
|
|
971
|
+
* view correlation consumers key on, derived from `dirOwnerSets`. */
|
|
972
|
+
function firstOwners(dirOwnerSets) {
|
|
973
|
+
const owners = {};
|
|
974
|
+
for (const [dir, specifiers] of Object.entries(dirOwnerSets)) if (specifiers.length > 0) owners[dir] = specifiers[0];
|
|
975
|
+
return owners;
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Excise a set of rejected specifiers from a collection result ATOMICALLY
|
|
979
|
+
* (openspec: external-source-watch-ingestion, design D5): the returned copy
|
|
980
|
+
* carries no entries, package-map targets, source entries, package dirs, or
|
|
981
|
+
* ownership for the rejected packages — a package dir is removed only when
|
|
982
|
+
* EVERY specifier that claimed it is rejected (a dir-level rejection, such
|
|
983
|
+
* as the cross-volume gate, always rejects them together). Outcome records
|
|
984
|
+
* are reporting inputs, not membership, and pass through untouched.
|
|
985
|
+
*/
|
|
986
|
+
function excludeCollectedPackages(collected, rejectedSpecifiers, rootDir) {
|
|
987
|
+
if (rejectedSpecifiers.size === 0) return collected;
|
|
988
|
+
const rejectedDirs = Object.entries(collected.dirOwnerSets).filter(([, specs]) => specs.every((s) => rejectedSpecifiers.has(s))).map(([dir]) => dir);
|
|
989
|
+
const underRejectedDir = (absPath) => rejectedDirs.some((dir) => isPathWithinRoot(dir, absPath));
|
|
990
|
+
const targetRejected = (specifier, absTarget) => rejectedSpecifiers.has(specifier) || underRejectedDir(absTarget);
|
|
991
|
+
const packageMap = {};
|
|
992
|
+
for (const [specifier, relTarget] of Object.entries(collected.packageMap)) {
|
|
993
|
+
if (targetRejected(specifier, resolve(rootDir, relTarget))) continue;
|
|
994
|
+
packageMap[specifier] = relTarget;
|
|
995
|
+
}
|
|
996
|
+
const sourceEntries = /* @__PURE__ */ new Map();
|
|
997
|
+
for (const [specifier, absEntry] of collected.sourceEntries) {
|
|
998
|
+
if (targetRejected(specifier, absEntry)) continue;
|
|
999
|
+
sourceEntries.set(specifier, absEntry);
|
|
1000
|
+
}
|
|
1001
|
+
const keyframesScanEntries = /* @__PURE__ */ new Map();
|
|
1002
|
+
for (const [specifier, absEntry] of collected.keyframesScanEntries) {
|
|
1003
|
+
if (targetRejected(specifier, absEntry)) continue;
|
|
1004
|
+
keyframesScanEntries.set(specifier, absEntry);
|
|
1005
|
+
}
|
|
1006
|
+
const dirOwnerSets = {};
|
|
1007
|
+
for (const [dir, specs] of Object.entries(collected.dirOwnerSets)) {
|
|
1008
|
+
const kept = specs.filter((s) => !rejectedSpecifiers.has(s));
|
|
1009
|
+
if (kept.length === 0) continue;
|
|
1010
|
+
dirOwnerSets[dir] = kept;
|
|
1011
|
+
}
|
|
1012
|
+
const dirExtensions = {};
|
|
1013
|
+
for (const [dir, exts] of Object.entries(collected.dirExtensions)) {
|
|
1014
|
+
if (rejectedDirs.includes(dir)) continue;
|
|
1015
|
+
dirExtensions[dir] = exts;
|
|
1016
|
+
}
|
|
1017
|
+
const fileOwners = {};
|
|
1018
|
+
for (const [relPath, owner] of Object.entries(collected.fileOwners)) {
|
|
1019
|
+
if (rejectedSpecifiers.has(owner)) continue;
|
|
1020
|
+
fileOwners[relPath] = owner;
|
|
1021
|
+
}
|
|
1022
|
+
return {
|
|
1023
|
+
entries: collected.entries.filter((entry) => !rejectedSpecifiers.has(collected.fileOwners[entry.path])),
|
|
1024
|
+
packageMap,
|
|
1025
|
+
sourceEntries,
|
|
1026
|
+
keyframesScanEntries,
|
|
1027
|
+
packageDirs: collected.packageDirs.filter((dir) => !rejectedDirs.includes(dir)),
|
|
1028
|
+
dirOwnerSets,
|
|
1029
|
+
dirExtensions,
|
|
1030
|
+
fileOwners,
|
|
1031
|
+
outcomes: collected.outcomes
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Extract external DS package names from inheritance declarations in the
|
|
1036
|
+
* system file.
|
|
1037
|
+
*
|
|
1038
|
+
* Supports four forms:
|
|
1039
|
+
* - Primary: `createSystem(...).extend(identifier)` chain calls
|
|
1040
|
+
* (repeatable, mixable with `.from()` links; each call
|
|
1041
|
+
* contributes one source; a library-bundle identifier
|
|
1042
|
+
* traces its base import)
|
|
1043
|
+
* - Deprecated chain: `createSystem(...).from(identifier)` chain calls
|
|
1044
|
+
* - Deprecated alias: `createSystem({ includes: [identifier, ...] })`
|
|
1045
|
+
* - Legacy: `.includes([identifier, ...])` chain method (RC
|
|
1046
|
+
* migration fallback)
|
|
1047
|
+
*
|
|
1048
|
+
* For each identifier found, traces back to its import declaration and returns
|
|
1049
|
+
* the import specifier: a bare specifier normalized to its package name, a
|
|
1050
|
+
* relative specifier resolved against the system file's directory into an
|
|
1051
|
+
* absolute path (so a sibling package referenced by path contributes discovery
|
|
1052
|
+
* too). Only packages explicitly declared through one of these forms are
|
|
1053
|
+
* treated as external DS dependencies.
|
|
1054
|
+
*
|
|
1055
|
+
* Falls back to empty array if no declaration is found.
|
|
1056
|
+
*/
|
|
1057
|
+
/**
|
|
1058
|
+
* The message for the strict/warn gate over unresolvable includes, or null
|
|
1059
|
+
* when every declared specifier resolved (external-package-file-discovery:
|
|
1060
|
+
* silence is never an outcome — non-strict consumers warn with this line,
|
|
1061
|
+
* strict consumers throw it).
|
|
1062
|
+
*/
|
|
1063
|
+
function unresolvableIncludesMessage(outcomes) {
|
|
1064
|
+
const unresolvable = outcomes.filter((record) => record.outcome === "unresolvable").map((record) => record.specifier);
|
|
1065
|
+
if (unresolvable.length === 0) return null;
|
|
1066
|
+
return `[animus-extract] unresolvable include specifier(s): ${unresolvable.join(", ")}`;
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* The message for the strict/warn gate over stale dist entries
|
|
1070
|
+
* (first-class-extension D13), or null when no declared specifier is stale.
|
|
1071
|
+
* A stale dist silently skews merged registry content while discovery
|
|
1072
|
+
* compiles the fresh sources, so it surfaces like an unresolvable specifier:
|
|
1073
|
+
* non-strict consumers warn with this line, strict consumers throw it.
|
|
1074
|
+
*/
|
|
1075
|
+
function staleDistIncludesMessage(outcomes) {
|
|
1076
|
+
const stale = outcomes.filter((record) => record.outcome === "stale-dist").map((record) => record.specifier);
|
|
1077
|
+
if (stale.length === 0) return null;
|
|
1078
|
+
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`;
|
|
1079
|
+
}
|
|
1080
|
+
function extractSystemFilePackages(systemFilePath) {
|
|
1081
|
+
let source;
|
|
1082
|
+
try {
|
|
1083
|
+
source = readFileSync(systemFilePath, "utf-8");
|
|
1084
|
+
} catch {
|
|
1085
|
+
return [];
|
|
1086
|
+
}
|
|
1087
|
+
const identifiers = /* @__PURE__ */ new Set();
|
|
1088
|
+
const constructorRegex = /createSystem\s*\(\s*\{[^}]*?\bincludes\s*:\s*\[([^\]]*)\]/gs;
|
|
1089
|
+
const chainRegex = /\.includes\s*\(\s*\[([^\]]*)\]\s*\)/gs;
|
|
1090
|
+
const collectIdentifiers = (regex) => {
|
|
1091
|
+
let match;
|
|
1092
|
+
while ((match = regex.exec(source)) !== null) {
|
|
1093
|
+
const inner = match[1];
|
|
1094
|
+
for (const token of inner.split(",")) {
|
|
1095
|
+
const id = token.trim();
|
|
1096
|
+
if (id && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(id)) identifiers.add(id);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
collectIdentifiers(constructorRegex);
|
|
1101
|
+
collectIdentifiers(chainRegex);
|
|
1102
|
+
const IDENT_START_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*/;
|
|
1103
|
+
/** Position after any run of whitespace, line comments, block comments. */
|
|
1104
|
+
const skipTrivia = (from) => {
|
|
1105
|
+
let pos = from;
|
|
1106
|
+
for (;;) {
|
|
1107
|
+
while (pos < source.length && /\s/.test(source[pos])) pos++;
|
|
1108
|
+
if (source.startsWith("//", pos)) {
|
|
1109
|
+
const newline = source.indexOf("\n", pos);
|
|
1110
|
+
pos = newline === -1 ? source.length : newline + 1;
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
if (source.startsWith("/*", pos)) {
|
|
1114
|
+
const close = source.indexOf("*/", pos + 2);
|
|
1115
|
+
pos = close === -1 ? source.length : close + 2;
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
return pos;
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
/**
|
|
1122
|
+
* Consume consecutive extend/from links starting at `from`, collecting
|
|
1123
|
+
* each link's base identifier; tolerates trivia between tokens and a
|
|
1124
|
+
* trailing comma in the argument list. Returns the position after the
|
|
1125
|
+
* last consumed link (`from` itself when none matched).
|
|
1126
|
+
*/
|
|
1127
|
+
const consumeChainLinks = (from) => {
|
|
1128
|
+
let pos = from;
|
|
1129
|
+
for (;;) {
|
|
1130
|
+
let cursor = skipTrivia(pos);
|
|
1131
|
+
if (source[cursor] !== ".") return pos;
|
|
1132
|
+
cursor = skipTrivia(cursor + 1);
|
|
1133
|
+
const method = IDENT_START_RE.exec(source.slice(cursor))?.[0];
|
|
1134
|
+
if (method !== "extend" && method !== "from") return pos;
|
|
1135
|
+
cursor = skipTrivia(cursor + method.length);
|
|
1136
|
+
if (source[cursor] !== "(") return pos;
|
|
1137
|
+
cursor = skipTrivia(cursor + 1);
|
|
1138
|
+
const base = IDENT_START_RE.exec(source.slice(cursor))?.[0];
|
|
1139
|
+
if (!base) return pos;
|
|
1140
|
+
cursor = skipTrivia(cursor + base.length);
|
|
1141
|
+
while (source[cursor] === ".") {
|
|
1142
|
+
const afterDot = skipTrivia(cursor + 1);
|
|
1143
|
+
const segment = IDENT_START_RE.exec(source.slice(afterDot))?.[0];
|
|
1144
|
+
if (!segment) break;
|
|
1145
|
+
cursor = skipTrivia(afterDot + segment.length);
|
|
1146
|
+
}
|
|
1147
|
+
if (source[cursor] === ",") cursor = skipTrivia(cursor + 1);
|
|
1148
|
+
if (source[cursor] !== ")") return pos;
|
|
1149
|
+
identifiers.add(base);
|
|
1150
|
+
pos = cursor + 1;
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
/** Identifier assigned directly before `index` (`const x = <here>`), if any. */
|
|
1154
|
+
const boundIdentifierBefore = (index) => {
|
|
1155
|
+
const match = /([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*$/.exec(source.slice(0, index));
|
|
1156
|
+
return match ? match[1] : null;
|
|
1157
|
+
};
|
|
1158
|
+
const chainRootIdentifiers = /* @__PURE__ */ new Set();
|
|
1159
|
+
const createSystemAnchor = /createSystem\s*\(/g;
|
|
1160
|
+
let anchorMatch;
|
|
1161
|
+
while ((anchorMatch = createSystemAnchor.exec(source)) !== null) {
|
|
1162
|
+
let pos = anchorMatch.index + anchorMatch[0].length;
|
|
1163
|
+
let depth = 1;
|
|
1164
|
+
while (pos < source.length && depth > 0) {
|
|
1165
|
+
const ch = source[pos];
|
|
1166
|
+
if (ch === "(") depth++;
|
|
1167
|
+
else if (ch === ")") depth--;
|
|
1168
|
+
pos++;
|
|
1169
|
+
}
|
|
1170
|
+
consumeChainLinks(pos);
|
|
1171
|
+
const bound = boundIdentifierBefore(anchorMatch.index);
|
|
1172
|
+
if (bound) chainRootIdentifiers.add(bound);
|
|
1173
|
+
}
|
|
1174
|
+
const scannedRoots = /* @__PURE__ */ new Set();
|
|
1175
|
+
for (;;) {
|
|
1176
|
+
const pendingRoots = [...chainRootIdentifiers].filter((root) => !scannedRoots.has(root));
|
|
1177
|
+
if (pendingRoots.length === 0) break;
|
|
1178
|
+
for (const root of pendingRoots) {
|
|
1179
|
+
scannedRoots.add(root);
|
|
1180
|
+
const rootUse = new RegExp(`(?<![a-zA-Z0-9_$])${root.replace(/\$/g, "\\$")}(?![a-zA-Z0-9_$])`, "g");
|
|
1181
|
+
let useMatch;
|
|
1182
|
+
while ((useMatch = rootUse.exec(source)) !== null) {
|
|
1183
|
+
const afterIdentifier = useMatch.index + useMatch[0].length;
|
|
1184
|
+
if (consumeChainLinks(afterIdentifier) === afterIdentifier) continue;
|
|
1185
|
+
const bound = boundIdentifierBefore(useMatch.index);
|
|
1186
|
+
if (bound) chainRootIdentifiers.add(bound);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
if (identifiers.size === 0) return [];
|
|
1191
|
+
const importMap = /* @__PURE__ */ new Map();
|
|
1192
|
+
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;
|
|
1193
|
+
let importMatch;
|
|
1194
|
+
while ((importMatch = importRegex.exec(source)) !== null) {
|
|
1195
|
+
const [, comboDefault, namedImports, defaultImport, specifier] = importMatch;
|
|
1196
|
+
if (comboDefault) importMap.set(comboDefault, specifier);
|
|
1197
|
+
if (defaultImport) importMap.set(defaultImport, specifier);
|
|
1198
|
+
if (namedImports) for (const binding of namedImports.split(",")) {
|
|
1199
|
+
const parts = binding.trim().split(/\s+as\s+/);
|
|
1200
|
+
const localName = (parts[1] || parts[0]).trim();
|
|
1201
|
+
if (localName) importMap.set(localName, specifier);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
const systemFileDir = dirname(systemFilePath);
|
|
1205
|
+
const packages = /* @__PURE__ */ new Set();
|
|
1206
|
+
for (const id of identifiers) {
|
|
1207
|
+
const specifier = importMap.get(id);
|
|
1208
|
+
if (!specifier) continue;
|
|
1209
|
+
if (specifier.startsWith(".")) packages.add(resolve(systemFileDir, specifier));
|
|
1210
|
+
else packages.add(specifier);
|
|
1211
|
+
}
|
|
1212
|
+
return Array.from(packages);
|
|
1213
|
+
}
|
|
1214
|
+
//#endregion
|
|
1215
|
+
//#region pipeline/svelte-source-adapter.ts
|
|
1216
|
+
function isNode(value) {
|
|
1217
|
+
return typeof value === "object" && value !== null && typeof value.type === "string" && typeof value.start === "number" && typeof value.end === "number";
|
|
1218
|
+
}
|
|
1219
|
+
function childNode(value) {
|
|
1220
|
+
return isNode(value) ? value : null;
|
|
1221
|
+
}
|
|
1222
|
+
function nodeName(node) {
|
|
1223
|
+
return node?.type === "Identifier" && typeof node.name === "string" ? node.name : null;
|
|
1224
|
+
}
|
|
1225
|
+
/** Adaptation touches one source at a time, so a last-value memo makes the
|
|
1226
|
+
* all-ASCII check O(n) once per file instead of per span endpoint. */
|
|
1227
|
+
let asciiMemoSource = null;
|
|
1228
|
+
let asciiMemoValue = false;
|
|
1229
|
+
function isAsciiOnly(source) {
|
|
1230
|
+
if (asciiMemoSource !== source) {
|
|
1231
|
+
asciiMemoSource = source;
|
|
1232
|
+
asciiMemoValue = Buffer.byteLength(source) === source.length;
|
|
1233
|
+
}
|
|
1234
|
+
return asciiMemoValue;
|
|
1235
|
+
}
|
|
1236
|
+
function byteOffset(source, characterOffset) {
|
|
1237
|
+
if (isAsciiOnly(source)) return characterOffset;
|
|
1238
|
+
return Buffer.byteLength(source.slice(0, characterOffset));
|
|
1239
|
+
}
|
|
1240
|
+
function byteSpan(source, node) {
|
|
1241
|
+
return {
|
|
1242
|
+
start: byteOffset(source, node.start),
|
|
1243
|
+
end: byteOffset(source, node.end)
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
function sourcePosition(source, characterOffset) {
|
|
1247
|
+
let line = 1;
|
|
1248
|
+
let lineStart = 0;
|
|
1249
|
+
for (let index = 0; index < characterOffset; index += 1) {
|
|
1250
|
+
const code = source.charCodeAt(index);
|
|
1251
|
+
if (code === 13) {
|
|
1252
|
+
if (index + 1 < characterOffset && source.charCodeAt(index + 1) === 10) index += 1;
|
|
1253
|
+
line += 1;
|
|
1254
|
+
lineStart = index + 1;
|
|
1255
|
+
} else if (code === 10 || code === 8232 || code === 8233) {
|
|
1256
|
+
line += 1;
|
|
1257
|
+
lineStart = index + 1;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
return {
|
|
1261
|
+
line,
|
|
1262
|
+
column: characterOffset - lineStart
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
function sourceLocation(source, node) {
|
|
1266
|
+
return {
|
|
1267
|
+
start: sourcePosition(source, node.start),
|
|
1268
|
+
end: sourcePosition(source, node.end)
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
function createsNestedBindingScope(node) {
|
|
1272
|
+
return node.type === "BlockStatement" || node.type === "CatchClause" || node.type === "ClassBody" || node.type === "ClassDeclaration" || node.type === "ClassExpression" || node.type === "ForInStatement" || node.type === "ForOfStatement" || node.type === "ForStatement" || node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression" || node.type === "StaticBlock" || node.type === "SwitchStatement" || node.type === "TSModuleBlock";
|
|
1273
|
+
}
|
|
1274
|
+
function walk(node, visit, nestedScopeDepth = 0) {
|
|
1275
|
+
visit(node, nestedScopeDepth);
|
|
1276
|
+
const childScopeDepth = nestedScopeDepth + (createsNestedBindingScope(node) ? 1 : 0);
|
|
1277
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1278
|
+
if (key === "loc" || key === "start" || key === "end") continue;
|
|
1279
|
+
if (Array.isArray(value)) {
|
|
1280
|
+
for (const item of value) if (isNode(item)) walk(item, visit, childScopeDepth);
|
|
1281
|
+
} else if (isNode(value)) walk(value, visit, childScopeDepth);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Fragment traversal: Svelte `Fragment` containers (the template root and
|
|
1286
|
+
* every block body) carry no `start`/`end`, so the script walker's isNode
|
|
1287
|
+
* gate would stop at each one. Descend through every plain object/array,
|
|
1288
|
+
* visit only span-carrying nodes, and guard against metadata back-references.
|
|
1289
|
+
*/
|
|
1290
|
+
function walkFragment(value, visit) {
|
|
1291
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
1292
|
+
const descend = (current) => {
|
|
1293
|
+
if (Array.isArray(current)) {
|
|
1294
|
+
for (const item of current) descend(item);
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
if (typeof current !== "object" || current === null) return;
|
|
1298
|
+
if (seen.has(current)) return;
|
|
1299
|
+
seen.add(current);
|
|
1300
|
+
if (isNode(current)) visit(current);
|
|
1301
|
+
for (const [key, child] of Object.entries(current)) {
|
|
1302
|
+
if (key === "loc" || key === "start" || key === "end" || key === "metadata" || key === "parent") continue;
|
|
1303
|
+
descend(child);
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
descend(value);
|
|
1307
|
+
}
|
|
1308
|
+
function importBindings(program) {
|
|
1309
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
1310
|
+
for (const declaration of program.body) {
|
|
1311
|
+
if (declaration.type !== "ImportDeclaration" || declaration.importKind === "type" || !Array.isArray(declaration.specifiers)) continue;
|
|
1312
|
+
for (const value of declaration.specifiers) {
|
|
1313
|
+
if (!isNode(value) || value.importKind === "type") continue;
|
|
1314
|
+
const local = childNode(value.local);
|
|
1315
|
+
const localName = nodeName(local);
|
|
1316
|
+
if (!local || !localName) continue;
|
|
1317
|
+
const sourceNode = childNode(declaration.source);
|
|
1318
|
+
const importSource = sourceNode && typeof sourceNode.value === "string" ? sourceNode.value : null;
|
|
1319
|
+
if (!importSource) continue;
|
|
1320
|
+
const namedImported = importedName(value);
|
|
1321
|
+
if (value.type === "ImportSpecifier" && namedImported === null) continue;
|
|
1322
|
+
const importedNode = childNode(value.imported);
|
|
1323
|
+
const kind = value.type === "ImportSpecifier" ? importedNode?.type === "Literal" ? "string-named" : namedImported === "default" ? "named-default" : "named" : value.type === "ImportDefaultSpecifier" ? "default" : value.type === "ImportNamespaceSpecifier" ? "namespace" : null;
|
|
1324
|
+
if (kind) bindings.set(localName, {
|
|
1325
|
+
kind,
|
|
1326
|
+
source: importSource,
|
|
1327
|
+
imported: kind === "default" || kind === "named-default" ? "default" : kind === "namespace" ? "*" : namedImported,
|
|
1328
|
+
declaration,
|
|
1329
|
+
specifier: value,
|
|
1330
|
+
local
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
return bindings;
|
|
1335
|
+
}
|
|
1336
|
+
function diagnostic(code, message, originalPath, source, node, location) {
|
|
1337
|
+
return {
|
|
1338
|
+
code,
|
|
1339
|
+
message,
|
|
1340
|
+
originalPath,
|
|
1341
|
+
...node ? {
|
|
1342
|
+
span: byteSpan(source, node),
|
|
1343
|
+
location: location ?? sourceLocation(source, node)
|
|
1344
|
+
} : {}
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
function compilerParseErrorRange(error, source) {
|
|
1348
|
+
if (typeof error !== "object" || error === null) return null;
|
|
1349
|
+
const candidate = error;
|
|
1350
|
+
const positions = Array.isArray(candidate.position) ? candidate.position : [];
|
|
1351
|
+
const startCharacter = typeof candidate.start?.character === "number" ? candidate.start.character : typeof positions[0] === "number" ? positions[0] : null;
|
|
1352
|
+
const endCharacter = typeof candidate.end?.character === "number" ? candidate.end.character : typeof positions[1] === "number" ? positions[1] : startCharacter;
|
|
1353
|
+
if (startCharacter === null || endCharacter === null || startCharacter < 0 || endCharacter < startCharacter || endCharacter > source.length) return null;
|
|
1354
|
+
const node = {
|
|
1355
|
+
type: "SvelteParseError",
|
|
1356
|
+
start: startCharacter,
|
|
1357
|
+
end: endCharacter
|
|
1358
|
+
};
|
|
1359
|
+
const fallback = sourceLocation(source, node);
|
|
1360
|
+
const point = (value, fallbackPoint) => ({
|
|
1361
|
+
line: typeof value?.line === "number" ? value.line : fallbackPoint.line,
|
|
1362
|
+
column: typeof value?.column === "number" ? value.column : fallbackPoint.column
|
|
1363
|
+
});
|
|
1364
|
+
return {
|
|
1365
|
+
node,
|
|
1366
|
+
location: {
|
|
1367
|
+
start: point(candidate.start, fallback.start),
|
|
1368
|
+
end: point(candidate.end, fallback.end)
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
function calleeParts(node) {
|
|
1373
|
+
if (node.type !== "CallExpression") return null;
|
|
1374
|
+
const callee = childNode(node.callee);
|
|
1375
|
+
if (!callee || callee.type !== "MemberExpression") return null;
|
|
1376
|
+
const object = childNode(callee.object);
|
|
1377
|
+
const property = childNode(callee.property);
|
|
1378
|
+
if (!object || !property) return null;
|
|
1379
|
+
if ((nodeName(property) ?? (property.type === "Literal" && property.value === "attrs" ? "attrs" : null)) !== "attrs") return null;
|
|
1380
|
+
return {
|
|
1381
|
+
object,
|
|
1382
|
+
property,
|
|
1383
|
+
computed: callee.computed === true,
|
|
1384
|
+
optional: callee.optional === true || node.optional === true
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
function resolverImportAccess(receiver, bindings) {
|
|
1388
|
+
const directName = nodeName(receiver);
|
|
1389
|
+
if (directName) {
|
|
1390
|
+
const binding = bindings.get(directName);
|
|
1391
|
+
if (!binding) return null;
|
|
1392
|
+
return {
|
|
1393
|
+
binding,
|
|
1394
|
+
resolver: receiver,
|
|
1395
|
+
displayName: directName,
|
|
1396
|
+
request: {
|
|
1397
|
+
source: binding.source,
|
|
1398
|
+
imported: binding.imported,
|
|
1399
|
+
local: directName,
|
|
1400
|
+
access: {
|
|
1401
|
+
kind: "direct",
|
|
1402
|
+
importKind: binding.kind
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
if (receiver.type !== "MemberExpression" || receiver.computed === true || receiver.optional === true) return null;
|
|
1408
|
+
const namespaceNode = childNode(receiver.object);
|
|
1409
|
+
const memberNode = childNode(receiver.property);
|
|
1410
|
+
const namespaceName = nodeName(namespaceNode);
|
|
1411
|
+
const memberName = nodeName(memberNode) ?? (memberNode?.type === "Literal" && typeof memberNode.value === "string" ? memberNode.value : null);
|
|
1412
|
+
if (!namespaceNode || !namespaceName || !memberName) return null;
|
|
1413
|
+
const binding = bindings.get(namespaceName);
|
|
1414
|
+
if (!binding || binding.kind !== "namespace") return null;
|
|
1415
|
+
return {
|
|
1416
|
+
binding,
|
|
1417
|
+
resolver: receiver,
|
|
1418
|
+
displayName: `${namespaceName}.${memberName}`,
|
|
1419
|
+
request: {
|
|
1420
|
+
source: binding.source,
|
|
1421
|
+
imported: memberName,
|
|
1422
|
+
local: namespaceName,
|
|
1423
|
+
access: {
|
|
1424
|
+
kind: "namespace-member",
|
|
1425
|
+
importKind: "namespace"
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
};
|
|
1429
|
+
}
|
|
1430
|
+
function isSupportedResolverAccess(access) {
|
|
1431
|
+
return access.binding.kind === "named" && access.request.access.kind === "direct";
|
|
1432
|
+
}
|
|
1433
|
+
function resolverCallSite(node, bindings) {
|
|
1434
|
+
if (node.type !== "CallExpression") return null;
|
|
1435
|
+
const parts = calleeParts(node);
|
|
1436
|
+
if (parts) {
|
|
1437
|
+
const access = resolverImportAccess(parts.object, bindings);
|
|
1438
|
+
if (!access) return null;
|
|
1439
|
+
return {
|
|
1440
|
+
access,
|
|
1441
|
+
form: "attrs",
|
|
1442
|
+
computed: parts.computed,
|
|
1443
|
+
optional: parts.optional
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
const callee = childNode(node.callee);
|
|
1447
|
+
if (!callee) return null;
|
|
1448
|
+
const access = resolverImportAccess(callee, bindings);
|
|
1449
|
+
if (!access) return null;
|
|
1450
|
+
return {
|
|
1451
|
+
access,
|
|
1452
|
+
form: "callable",
|
|
1453
|
+
computed: callee.type === "MemberExpression" && callee.computed === true,
|
|
1454
|
+
optional: node.optional === true || callee.type === "MemberExpression" && callee.optional === true
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
function argumentSpan(argumentsList, fallback) {
|
|
1458
|
+
if (argumentsList.length === 0) return fallback;
|
|
1459
|
+
return {
|
|
1460
|
+
type: "ArgumentRange",
|
|
1461
|
+
start: argumentsList[0].start,
|
|
1462
|
+
end: argumentsList[argumentsList.length - 1].end
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
function propertyName(node) {
|
|
1466
|
+
const key = childNode(node.key);
|
|
1467
|
+
if (!key) return null;
|
|
1468
|
+
if (key.type === "Identifier" && typeof key.name === "string") return key.name;
|
|
1469
|
+
if (key.type === "Literal" && typeof key.value === "string") return key.value;
|
|
1470
|
+
return null;
|
|
1471
|
+
}
|
|
1472
|
+
function isJsxAttributeName(name) {
|
|
1473
|
+
return /^[A-Za-z_$][A-Za-z0-9_$-]*$/.test(name);
|
|
1474
|
+
}
|
|
1475
|
+
function tsxIncompatibleValue(value) {
|
|
1476
|
+
let incompatible = null;
|
|
1477
|
+
walk(value, (node) => {
|
|
1478
|
+
if (incompatible) return;
|
|
1479
|
+
if (node.type === "TSTypeAssertion" || node.type === "ArrowFunctionExpression" && childNode(node.typeParameters) !== null) incompatible = node;
|
|
1480
|
+
});
|
|
1481
|
+
return incompatible;
|
|
1482
|
+
}
|
|
1483
|
+
function readProperties(argument, source, originalPath) {
|
|
1484
|
+
if (!Array.isArray(argument.properties)) return diagnostic("SVELTE_ATTRS_ARGUMENT_UNRESOLVED", "Resolver .attrs() requires an object literal with statically named properties.", originalPath, source, argument);
|
|
1485
|
+
const properties = [];
|
|
1486
|
+
for (const value of argument.properties) {
|
|
1487
|
+
if (!isNode(value)) continue;
|
|
1488
|
+
if (value.type === "SpreadElement") return diagnostic("SVELTE_ATTRS_SPREAD_UNRESOLVED", "Resolver .attrs() object spreads are unresolved; enumerate properties explicitly.", originalPath, source, value);
|
|
1489
|
+
if (value.type !== "Property") return diagnostic("SVELTE_ATTRS_PROPERTY_UNSUPPORTED", "Resolver .attrs() accepts ordinary object properties only.", originalPath, source, value);
|
|
1490
|
+
if (value.computed === true) return diagnostic("SVELTE_ATTRS_COMPUTED_KEY", "Resolver .attrs() computed property names cannot be attributed safely.", originalPath, source, value);
|
|
1491
|
+
if (value.method === true || value.kind !== "init") return diagnostic("SVELTE_ATTRS_PROPERTY_UNSUPPORTED", "Resolver .attrs() methods and accessors cannot be projected as attributes.", originalPath, source, value);
|
|
1492
|
+
const key = childNode(value.key);
|
|
1493
|
+
const propertyValue = childNode(value.value);
|
|
1494
|
+
const name = propertyName(value);
|
|
1495
|
+
if (!key || !propertyValue || !name || !isJsxAttributeName(name)) return diagnostic("SVELTE_ATTRS_PROPERTY_UNSUPPORTED", "Resolver .attrs() property names must be JSX-compatible identifier or string names.", originalPath, source, value);
|
|
1496
|
+
const incompatibleValue = tsxIncompatibleValue(propertyValue);
|
|
1497
|
+
if (incompatibleValue) return diagnostic("SVELTE_ATTRS_VALUE_UNSUPPORTED", "Resolver .attrs() values using angle-bracket type assertions or generic arrow parameters cannot be projected into TSX safely.", originalPath, source, incompatibleValue);
|
|
1498
|
+
properties.push({
|
|
1499
|
+
name,
|
|
1500
|
+
property: value,
|
|
1501
|
+
key,
|
|
1502
|
+
value: propertyValue
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
return properties;
|
|
1506
|
+
}
|
|
1507
|
+
var VirtualSourceBuilder = class {
|
|
1508
|
+
chunks = [];
|
|
1509
|
+
bytes = 0;
|
|
1510
|
+
mappings = [];
|
|
1511
|
+
constructor(originalSource) {
|
|
1512
|
+
this.originalSource = originalSource;
|
|
1513
|
+
}
|
|
1514
|
+
append(text) {
|
|
1515
|
+
this.chunks.push(text);
|
|
1516
|
+
this.bytes += Buffer.byteLength(text);
|
|
1517
|
+
}
|
|
1518
|
+
appendMapped(text, kind, original) {
|
|
1519
|
+
const start = this.bytes;
|
|
1520
|
+
this.append(text);
|
|
1521
|
+
this.mappings.push({
|
|
1522
|
+
kind,
|
|
1523
|
+
generated: {
|
|
1524
|
+
start,
|
|
1525
|
+
end: this.bytes
|
|
1526
|
+
},
|
|
1527
|
+
original: byteSpan(this.originalSource, original)
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
toString() {
|
|
1531
|
+
return this.chunks.join("");
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
1534
|
+
function importedName(specifier) {
|
|
1535
|
+
const imported = childNode(specifier.imported);
|
|
1536
|
+
if (!imported) return null;
|
|
1537
|
+
if (imported.type === "Identifier" && typeof imported.name === "string") return imported.name;
|
|
1538
|
+
if (imported.type === "Literal" && typeof imported.value === "string") return imported.value;
|
|
1539
|
+
return null;
|
|
1540
|
+
}
|
|
1541
|
+
function renderImport(declaration, witnessedBindings, source) {
|
|
1542
|
+
if (!Array.isArray(declaration.specifiers)) return null;
|
|
1543
|
+
const names = [];
|
|
1544
|
+
const locals = [];
|
|
1545
|
+
for (const value of declaration.specifiers) {
|
|
1546
|
+
if (!isNode(value) || value.type !== "ImportSpecifier") continue;
|
|
1547
|
+
const localName = nodeName(childNode(value.local));
|
|
1548
|
+
if (!localName || !witnessedBindings.has(localName)) continue;
|
|
1549
|
+
names.push(source.slice(value.start, value.end));
|
|
1550
|
+
locals.push(localName);
|
|
1551
|
+
}
|
|
1552
|
+
if (names.length === 0) return null;
|
|
1553
|
+
const sourceNode = childNode(declaration.source);
|
|
1554
|
+
const sourceText = sourceNode && typeof sourceNode.raw === "string" ? sourceNode.raw : sourceNode && typeof sourceNode.value === "string" ? JSON.stringify(sourceNode.value) : null;
|
|
1555
|
+
return sourceText ? {
|
|
1556
|
+
text: `import { ${names.join(", ")} } from ${sourceText};\n`,
|
|
1557
|
+
locals
|
|
1558
|
+
} : null;
|
|
1559
|
+
}
|
|
1560
|
+
/** Memoizes success only: a missing optional peer retries per file, so
|
|
1561
|
+
* installing `svelte` mid-session recovers without a process restart. */
|
|
1562
|
+
let compilerMemo = null;
|
|
1563
|
+
async function loadCompiler() {
|
|
1564
|
+
if (compilerMemo) return compilerMemo;
|
|
1565
|
+
compilerMemo = await import("svelte/compiler").catch(() => null);
|
|
1566
|
+
return compilerMemo;
|
|
1567
|
+
}
|
|
1568
|
+
function projectScope(script, scope, source, originalPath, options, inherited, precomputedOwnBindings) {
|
|
1569
|
+
const ownBindings = precomputedOwnBindings ?? importBindings(script.content);
|
|
1570
|
+
const bindings = inherited ? new Map([...inherited.bindings, ...ownBindings]) : ownBindings;
|
|
1571
|
+
const witnesses = [];
|
|
1572
|
+
const diagnostics = [];
|
|
1573
|
+
if (bindings.size > 0) walk(script.content, (node, nestedScopeDepth) => {
|
|
1574
|
+
const site = resolverCallSite(node, bindings);
|
|
1575
|
+
if (!site) return;
|
|
1576
|
+
const { access, form } = site;
|
|
1577
|
+
const attribution = options.attributeResolver(access.request);
|
|
1578
|
+
if (attribution === "other") return;
|
|
1579
|
+
const resolverName = access.displayName;
|
|
1580
|
+
const callDisplay = form === "attrs" ? `${resolverName}.attrs()` : `${resolverName}()`;
|
|
1581
|
+
if (attribution === "unsupported-resolver-form" || !isSupportedResolverAccess(access)) {
|
|
1582
|
+
diagnostics.push(diagnostic("SVELTE_ATTRS_IMPORT_UNSUPPORTED", `Resolver '${resolverName}' uses an import/access form that the extraction engine cannot attribute to its exported .asClass() binding.`, originalPath, source, access.resolver));
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
if (nestedScopeDepth > 0) {
|
|
1586
|
+
diagnostics.push(diagnostic("SVELTE_ATTRS_SCOPE_UNSUPPORTED", `Resolver '${callDisplay}' is inside a nested binding scope; move the call to the top-level script scope so its import identity is unambiguous.`, originalPath, source, access.resolver));
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
if (site.computed || site.optional) {
|
|
1590
|
+
diagnostics.push(diagnostic("SVELTE_ATTRS_CALLEE_UNSUPPORTED", `Resolver '${resolverName}' must be called directly as ${resolverName}(...) or ${resolverName}.attrs(...).`, originalPath, source, node));
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
const argumentsList = Array.isArray(node.arguments) ? node.arguments.filter(isNode) : [];
|
|
1594
|
+
if (argumentsList.length === 0) {
|
|
1595
|
+
witnesses.push({
|
|
1596
|
+
binding: access.binding,
|
|
1597
|
+
resolver: access.resolver,
|
|
1598
|
+
properties: []
|
|
1599
|
+
});
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
if (argumentsList.length !== 1 || argumentsList[0].type !== "ObjectExpression") {
|
|
1603
|
+
diagnostics.push(diagnostic("SVELTE_ATTRS_ARGUMENT_UNRESOLVED", `Resolver '${callDisplay}' accepts no argument or one object literal in Svelte projection.`, originalPath, source, argumentSpan(argumentsList, node)));
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
const properties = readProperties(argumentsList[0], source, originalPath);
|
|
1607
|
+
if (!Array.isArray(properties)) {
|
|
1608
|
+
diagnostics.push(properties);
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
witnesses.push({
|
|
1612
|
+
binding: access.binding,
|
|
1613
|
+
resolver: access.resolver,
|
|
1614
|
+
properties
|
|
1615
|
+
});
|
|
1616
|
+
});
|
|
1617
|
+
if (diagnostics.length > 0 || witnesses.length === 0) return { diagnostics };
|
|
1618
|
+
const witnessedBindings = new Set(witnesses.map((witness) => nodeName(witness.binding.local)));
|
|
1619
|
+
const builder = new VirtualSourceBuilder(source);
|
|
1620
|
+
const pendingNames = new Set(witnessedBindings);
|
|
1621
|
+
const renderDeclarations = (declarations) => {
|
|
1622
|
+
for (const declaration of declarations) {
|
|
1623
|
+
if (declaration.type !== "ImportDeclaration") continue;
|
|
1624
|
+
const rendered = renderImport(declaration, pendingNames, source);
|
|
1625
|
+
if (!rendered) continue;
|
|
1626
|
+
builder.append(rendered.text);
|
|
1627
|
+
for (const local of rendered.locals) pendingNames.delete(local);
|
|
1628
|
+
}
|
|
1629
|
+
};
|
|
1630
|
+
renderDeclarations(script.content.body);
|
|
1631
|
+
if (inherited) renderDeclarations(inherited.importDeclarations);
|
|
1632
|
+
for (const witness of witnesses) {
|
|
1633
|
+
const resolverName = nodeName(witness.resolver);
|
|
1634
|
+
builder.append("<");
|
|
1635
|
+
builder.appendMapped(resolverName, "resolver", witness.resolver);
|
|
1636
|
+
for (const property of witness.properties) {
|
|
1637
|
+
builder.append(" ");
|
|
1638
|
+
builder.appendMapped(property.name, "attribute", property.key);
|
|
1639
|
+
builder.append("={");
|
|
1640
|
+
builder.appendMapped(source.slice(property.value.start, property.value.end), "value", property.value);
|
|
1641
|
+
builder.append("}");
|
|
1642
|
+
}
|
|
1643
|
+
builder.append(" />;\n");
|
|
1644
|
+
}
|
|
1645
|
+
return {
|
|
1646
|
+
diagnostics: [],
|
|
1647
|
+
entry: {
|
|
1648
|
+
scope,
|
|
1649
|
+
path: `${originalPath}.${scope}.tsx`,
|
|
1650
|
+
source: builder.toString(),
|
|
1651
|
+
mappings: builder.mappings
|
|
1652
|
+
}
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
/**
|
|
1656
|
+
* Project native Svelte script scopes into parser-only TSX witnesses.
|
|
1657
|
+
*
|
|
1658
|
+
* This adapter parses source, never compiler-generated runtime JavaScript.
|
|
1659
|
+
* Module and instance scripts project separately (module bindings are in
|
|
1660
|
+
* scope for the instance script, per Svelte semantics) and contribute only
|
|
1661
|
+
* witnessed named imports plus synthetic JSX attributes for direct resolver
|
|
1662
|
+
* `.attrs()` and callable-string calls. Caller-owned metadata attributes
|
|
1663
|
+
* candidate imports; unsupported call shapes — including resolver calls
|
|
1664
|
+
* written in the template fragment — fail the whole source closed.
|
|
1665
|
+
*/
|
|
1666
|
+
async function adaptSvelteSource(source, originalPath, options) {
|
|
1667
|
+
const original = {
|
|
1668
|
+
path: originalPath,
|
|
1669
|
+
hash: contentHash(source)
|
|
1670
|
+
};
|
|
1671
|
+
const compiler = await loadCompiler();
|
|
1672
|
+
if (!compiler) return {
|
|
1673
|
+
kind: "missing-dep",
|
|
1674
|
+
original,
|
|
1675
|
+
dependency: "svelte/compiler"
|
|
1676
|
+
};
|
|
1677
|
+
let ast;
|
|
1678
|
+
try {
|
|
1679
|
+
ast = compiler.parse(source, {
|
|
1680
|
+
filename: originalPath,
|
|
1681
|
+
modern: true
|
|
1682
|
+
});
|
|
1683
|
+
} catch (error) {
|
|
1684
|
+
const range = compilerParseErrorRange(error, source);
|
|
1685
|
+
return {
|
|
1686
|
+
kind: "error",
|
|
1687
|
+
original,
|
|
1688
|
+
diagnostics: [diagnostic("SVELTE_PARSE_ERROR", error instanceof Error ? error.message : String(error), originalPath, source, range?.node, range?.location)]
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
const projections = [];
|
|
1692
|
+
const moduleBindings = ast.module ? importBindings(ast.module.content) : void 0;
|
|
1693
|
+
const instanceBindings = ast.instance ? importBindings(ast.instance.content) : void 0;
|
|
1694
|
+
const moduleScope = ast.module && moduleBindings ? {
|
|
1695
|
+
bindings: moduleBindings,
|
|
1696
|
+
importDeclarations: ast.module.content.body.filter((declaration) => declaration.type === "ImportDeclaration")
|
|
1697
|
+
} : void 0;
|
|
1698
|
+
if (ast.module) projections.push(projectScope(ast.module, "module", source, originalPath, options, void 0, moduleBindings));
|
|
1699
|
+
if (ast.instance) projections.push(projectScope(ast.instance, "instance", source, originalPath, options, moduleScope, instanceBindings));
|
|
1700
|
+
const diagnostics = projections.flatMap((projection) => projection.diagnostics);
|
|
1701
|
+
if (ast.fragment) {
|
|
1702
|
+
const scriptBindings = instanceBindings ?? /* @__PURE__ */ new Map();
|
|
1703
|
+
const fragmentBindings = moduleScope ? new Map([...moduleScope.bindings, ...scriptBindings]) : scriptBindings;
|
|
1704
|
+
if (fragmentBindings.size > 0) walkFragment(ast.fragment, (node) => {
|
|
1705
|
+
const site = resolverCallSite(node, fragmentBindings);
|
|
1706
|
+
if (!site) return;
|
|
1707
|
+
if (options.attributeResolver(site.access.request) === "other") return;
|
|
1708
|
+
diagnostics.push(diagnostic("SVELTE_ATTRS_TEMPLATE_UNSUPPORTED", `Resolver '${site.access.displayName}' is called inside the template; assign the result to a const in the instance script and spread that instead.`, originalPath, source, node));
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
if (diagnostics.length > 0) return {
|
|
1712
|
+
kind: "error",
|
|
1713
|
+
original,
|
|
1714
|
+
diagnostics
|
|
1715
|
+
};
|
|
1716
|
+
return {
|
|
1717
|
+
kind: "ok",
|
|
1718
|
+
original,
|
|
1719
|
+
entries: projections.flatMap((projection) => projection.entry ? [projection.entry] : [])
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
//#endregion
|
|
1723
|
+
//#region pipeline/mdx-preprocessor.ts
|
|
1724
|
+
/**
|
|
1725
|
+
* MDX source preprocessor for the extraction pipeline.
|
|
1726
|
+
*
|
|
1727
|
+
* MDX files are first-class consumers of ds-built components but are not
|
|
1728
|
+
* directly parseable by OXC (which the scanner uses for .tsx/.jsx). This
|
|
1729
|
+
* module compiles MDX sources to scanner-consumable JSX using @mdx-js/mdx.
|
|
1730
|
+
*
|
|
1731
|
+
* @mdx-js/mdx is declared as `peerDependenciesMeta.optional` on the
|
|
1732
|
+
* plugin packages that import this module. The dynamic import with
|
|
1733
|
+
* .catch() ensures non-MDX consumers (who configure `extensions` to
|
|
1734
|
+
* exclude .mdx) never trigger the resolution — zero install-footprint
|
|
1735
|
+
* cost for them.
|
|
1736
|
+
*
|
|
1737
|
+
* The `DEFAULT_EXTENSIONS` constant is the shared source of truth for
|
|
1738
|
+
* both `@animus-ui/vite-plugin` and `@animus-ui/next-plugin`. Each plugin
|
|
1739
|
+
* imports it directly; independent redeclaration of default extensions
|
|
1740
|
+
* is considered a regression.
|
|
1741
|
+
*/
|
|
1742
|
+
const DEFAULT_EXTENSIONS = [
|
|
1743
|
+
".ts",
|
|
1744
|
+
".tsx",
|
|
1745
|
+
".js",
|
|
1746
|
+
".jsx",
|
|
1747
|
+
".mdx"
|
|
1748
|
+
];
|
|
1749
|
+
/**
|
|
1750
|
+
* Extensions the ENGINE TRANSFORM may rewrite (distinct from
|
|
1751
|
+
* `DEFAULT_EXTENSIONS`, the discovery set: `.mjs` is here so dist-entry
|
|
1752
|
+
* kits reach the engine, `.mdx` is not — MDX is preprocessed to `.tsx`
|
|
1753
|
+
* before the engine sees it). The single source for the Turbopack rule
|
|
1754
|
+
* glob and the unplugin transform gate; independent redeclaration of this
|
|
1755
|
+
* set is considered a regression — a missed copy silently skips a whole
|
|
1756
|
+
* file class on one bundler family.
|
|
1757
|
+
*/
|
|
1758
|
+
const ENGINE_TRANSFORM_EXTENSIONS = [
|
|
1759
|
+
"ts",
|
|
1760
|
+
"tsx",
|
|
1761
|
+
"js",
|
|
1762
|
+
"jsx",
|
|
1763
|
+
"mjs"
|
|
1764
|
+
];
|
|
1765
|
+
/**
|
|
1766
|
+
* Preprocess an MDX source string into scanner-consumable JSX.
|
|
1767
|
+
*
|
|
1768
|
+
* - Returns `{ kind: 'ok', source }` with JSX-compiled output on success.
|
|
1769
|
+
* - Returns `{ kind: 'missing-dep' }` if @mdx-js/mdx is not resolvable
|
|
1770
|
+
* (consumer needs to install it).
|
|
1771
|
+
* - Returns `{ kind: 'error', error }` on compile failure (e.g. malformed
|
|
1772
|
+
* MDX syntax). Plugins SHALL warn + skip affected files; the build
|
|
1773
|
+
* continues with remaining files.
|
|
1774
|
+
*/
|
|
1775
|
+
async function preprocessMdx(source, filename) {
|
|
1776
|
+
const mdxMod = await import("@mdx-js/mdx").catch(() => null);
|
|
1777
|
+
if (mdxMod === null) return { kind: "missing-dep" };
|
|
1778
|
+
try {
|
|
1779
|
+
const vfile = await mdxMod.compile(source, {
|
|
1780
|
+
outputFormat: "program",
|
|
1781
|
+
development: false,
|
|
1782
|
+
jsx: true
|
|
1783
|
+
});
|
|
1784
|
+
return {
|
|
1785
|
+
kind: "ok",
|
|
1786
|
+
source: `/* @mdx-source: ${filename} */\n${String(vfile)}`
|
|
1787
|
+
};
|
|
1788
|
+
} catch (err) {
|
|
1789
|
+
return {
|
|
1790
|
+
kind: "error",
|
|
1791
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
//#endregion
|
|
1796
|
+
//#region pipeline/source-ingestion.ts
|
|
1797
|
+
/**
|
|
1798
|
+
* Advisory diagnostics surface as warnings in EVERY mode and never
|
|
1799
|
+
* quarantine their file. OXC reports recovered parse diagnostics for
|
|
1800
|
+
* sources the consumer's own toolchain may accept (`.js` now parses
|
|
1801
|
+
* JSX-enabled, so genuinely malformed code is the remaining producer),
|
|
1802
|
+
* and the engine still analyzes whatever the recovered AST carries —
|
|
1803
|
+
* failing the build for that would make extraction stricter than the
|
|
1804
|
+
* host bundler, which surfaces its own error for the same file. Everything
|
|
1805
|
+
* else stays fatal: strict throws, non-strict warns and quarantines.
|
|
1806
|
+
*/
|
|
1807
|
+
function isAdvisorySourceDiagnostic(diagnostic) {
|
|
1808
|
+
return diagnostic.code === "SOURCE_NATIVE_PARSE_ERROR";
|
|
1809
|
+
}
|
|
1810
|
+
const RELATIVE_PROBE_SUFFIXES = [
|
|
1811
|
+
"",
|
|
1812
|
+
".ts",
|
|
1813
|
+
".tsx",
|
|
1814
|
+
".js",
|
|
1815
|
+
".jsx",
|
|
1816
|
+
"/index.ts",
|
|
1817
|
+
"/index.tsx",
|
|
1818
|
+
"/index.js",
|
|
1819
|
+
"/index.jsx"
|
|
1820
|
+
];
|
|
1821
|
+
function extension(path) {
|
|
1822
|
+
const basename = posix.basename(path);
|
|
1823
|
+
const dot = basename.lastIndexOf(".");
|
|
1824
|
+
return dot === -1 ? "" : basename.slice(dot).toLowerCase();
|
|
1825
|
+
}
|
|
1826
|
+
function canonicalResolverPath(path) {
|
|
1827
|
+
return posix.normalize(path.replaceAll("\\", "/"));
|
|
1828
|
+
}
|
|
1829
|
+
/** NodeNext-style relative specifiers carry the EMITTED extension
|
|
1830
|
+
* (`./definition.js` for `definition.ts`); map each back to its source
|
|
1831
|
+
* forms. The exact spelling is probed first by the suffix loop's empty
|
|
1832
|
+
* suffix, so a literal `.js` neighbor still wins. */
|
|
1833
|
+
const NODE_NEXT_EXTENSION_MAP = {
|
|
1834
|
+
".js": [
|
|
1835
|
+
".ts",
|
|
1836
|
+
".tsx",
|
|
1837
|
+
".jsx"
|
|
1838
|
+
],
|
|
1839
|
+
".mjs": [".mts"],
|
|
1840
|
+
".cjs": [".cts"]
|
|
1841
|
+
};
|
|
1842
|
+
function resolveRelativeSource(importerPath, specifier, files) {
|
|
1843
|
+
if (!specifier.startsWith(".")) return null;
|
|
1844
|
+
const base = posix.normalize(posix.join(posix.dirname(canonicalResolverPath(importerPath)), specifier));
|
|
1845
|
+
for (const suffix of RELATIVE_PROBE_SUFFIXES) {
|
|
1846
|
+
const candidate = `${base}${suffix}`;
|
|
1847
|
+
const actualPath = files.get(candidate);
|
|
1848
|
+
if (actualPath !== void 0) return actualPath;
|
|
1849
|
+
}
|
|
1850
|
+
const explicitExtension = extension(base);
|
|
1851
|
+
for (const sourceExtension of NODE_NEXT_EXTENSION_MAP[explicitExtension] ?? []) {
|
|
1852
|
+
const candidate = `${base.slice(0, -explicitExtension.length)}${sourceExtension}`;
|
|
1853
|
+
const actualPath = files.get(candidate);
|
|
1854
|
+
if (actualPath !== void 0) return actualPath;
|
|
1855
|
+
}
|
|
1856
|
+
return null;
|
|
1857
|
+
}
|
|
1858
|
+
var ResolverExportIndex = class {
|
|
1859
|
+
collisions;
|
|
1860
|
+
files;
|
|
1861
|
+
resolverBindings = /* @__PURE__ */ new Map();
|
|
1862
|
+
/** The same (importer, request) pair repeats for every call site of one
|
|
1863
|
+
* resolver in one file; the index is rebuilt per ingest, so neither memo
|
|
1864
|
+
* needs invalidation. */
|
|
1865
|
+
attributionMemo = /* @__PURE__ */ new Map();
|
|
1866
|
+
resolveMemo = /* @__PURE__ */ new Map();
|
|
1867
|
+
constructor(facts, analysisPaths) {
|
|
1868
|
+
this.facts = facts;
|
|
1869
|
+
const pathsByCanonical = /* @__PURE__ */ new Map();
|
|
1870
|
+
for (const path of analysisPaths) {
|
|
1871
|
+
const canonicalPath = canonicalResolverPath(path);
|
|
1872
|
+
const paths = pathsByCanonical.get(canonicalPath) ?? [];
|
|
1873
|
+
paths.push(path);
|
|
1874
|
+
pathsByCanonical.set(canonicalPath, paths);
|
|
1875
|
+
}
|
|
1876
|
+
this.collisions = [...pathsByCanonical].filter(([, paths]) => paths.length > 1).map(([canonicalPath, paths]) => ({
|
|
1877
|
+
canonicalPath,
|
|
1878
|
+
paths
|
|
1879
|
+
}));
|
|
1880
|
+
const factPathsByCanonical = /* @__PURE__ */ new Map();
|
|
1881
|
+
for (const path of Object.keys(facts.files)) {
|
|
1882
|
+
const canonicalPath = canonicalResolverPath(path);
|
|
1883
|
+
const paths = factPathsByCanonical.get(canonicalPath) ?? [];
|
|
1884
|
+
paths.push(path);
|
|
1885
|
+
factPathsByCanonical.set(canonicalPath, paths);
|
|
1886
|
+
}
|
|
1887
|
+
const files = /* @__PURE__ */ new Map();
|
|
1888
|
+
for (const [canonicalPath, paths] of pathsByCanonical) {
|
|
1889
|
+
const factPaths = factPathsByCanonical.get(canonicalPath) ?? [];
|
|
1890
|
+
if (paths.length === 1 && factPaths.length === 1) files.set(canonicalPath, factPaths[0]);
|
|
1891
|
+
}
|
|
1892
|
+
this.files = files;
|
|
1893
|
+
for (const [path, file] of Object.entries(facts.files)) this.resolverBindings.set(path, new Set(file.chains.filter((chain) => chain.descriptor.terminal === "asClass" && chain.descriptor.extractable && chain.fatalError === null).map((chain) => chain.descriptor.binding)));
|
|
1894
|
+
}
|
|
1895
|
+
attribute(importerPath, request) {
|
|
1896
|
+
const memoKey = `${importerPath}\0${request.source}\0${request.imported}\0${request.access.kind}\0${request.access.importKind}`;
|
|
1897
|
+
const memoized = this.attributionMemo.get(memoKey);
|
|
1898
|
+
if (memoized !== void 0) return memoized;
|
|
1899
|
+
const attribution = this.attributeUncached(importerPath, request);
|
|
1900
|
+
this.attributionMemo.set(memoKey, attribution);
|
|
1901
|
+
return attribution;
|
|
1902
|
+
}
|
|
1903
|
+
attributeUncached(importerPath, request) {
|
|
1904
|
+
const importedFile = this.resolveSource(importerPath, request.source);
|
|
1905
|
+
if (!importedFile) return "other";
|
|
1906
|
+
if (this.resolveClassExport(importedFile, request.imported, /* @__PURE__ */ new Set()) === null) return "other";
|
|
1907
|
+
return request.access.kind === "direct" && request.access.importKind === "named" ? "resolver" : "unsupported-resolver-form";
|
|
1908
|
+
}
|
|
1909
|
+
resolveSource(importerPath, specifier) {
|
|
1910
|
+
const key = `${importerPath}\0${specifier}`;
|
|
1911
|
+
let resolved = this.resolveMemo.get(key);
|
|
1912
|
+
if (resolved === void 0) {
|
|
1913
|
+
resolved = resolveRelativeSource(importerPath, specifier, this.files);
|
|
1914
|
+
this.resolveMemo.set(key, resolved);
|
|
1915
|
+
}
|
|
1916
|
+
return resolved;
|
|
1917
|
+
}
|
|
1918
|
+
resolveClassExport(path, exportedName, seen) {
|
|
1919
|
+
const identity = `${path}\0${exportedName}`;
|
|
1920
|
+
if (seen.has(identity)) return null;
|
|
1921
|
+
seen.add(identity);
|
|
1922
|
+
const file = this.facts.files[path];
|
|
1923
|
+
const exported = file?.exports.find((candidate) => candidate.exported === exportedName);
|
|
1924
|
+
if (!file || !exported) return null;
|
|
1925
|
+
if (exported.source === null) {
|
|
1926
|
+
if (exported.local === null) return null;
|
|
1927
|
+
if (this.resolverBindings.get(path)?.has(exported.local)) return exported.local;
|
|
1928
|
+
const imported = file.imports.find((candidate) => candidate.local === exported.local);
|
|
1929
|
+
if (!imported) return null;
|
|
1930
|
+
const importedFile = this.resolveSource(path, imported.source);
|
|
1931
|
+
return importedFile ? this.resolveClassExport(importedFile, imported.imported, seen) : null;
|
|
1932
|
+
}
|
|
1933
|
+
if (exported.original === null) return null;
|
|
1934
|
+
const nextFile = this.resolveSource(path, exported.source);
|
|
1935
|
+
return nextFile ? this.resolveClassExport(nextFile, exported.original, seen) : null;
|
|
1936
|
+
}
|
|
1937
|
+
};
|
|
1938
|
+
/**
|
|
1939
|
+
* Convert raw source identities into parser-ready analysis entries once.
|
|
1940
|
+
*
|
|
1941
|
+
* Native and MDX entries establish the resolver-export index through the
|
|
1942
|
+
* native fact collector before any Svelte projection. Svelte calls are then
|
|
1943
|
+
* attributed by exact relative import/export identity and parsed once. Bare
|
|
1944
|
+
* package and configured-alias sources deliberately remain `other`; callers
|
|
1945
|
+
* must not guess those identities.
|
|
1946
|
+
*/
|
|
1947
|
+
async function ingestSourceEntries(rawEntries, options) {
|
|
1948
|
+
const mdxAdapter = options.preprocessMdx ?? preprocessMdx;
|
|
1949
|
+
const svelteAdapter = options.adaptSvelte ?? adaptSvelteSource;
|
|
1950
|
+
const originalEntries = rawEntries.map((entry) => ({
|
|
1951
|
+
...entry,
|
|
1952
|
+
hash: entry.hash ?? contentHash(entry.source)
|
|
1953
|
+
}));
|
|
1954
|
+
const rawOriginalPaths = new Set(originalEntries.map((entry) => entry.path));
|
|
1955
|
+
const analysisEntries = [];
|
|
1956
|
+
const ownership = {};
|
|
1957
|
+
const analysisOwner = /* @__PURE__ */ new Map();
|
|
1958
|
+
const diagnostics = [];
|
|
1959
|
+
const svelteEntries = [];
|
|
1960
|
+
const addAnalysisEntry = (originalPath, path, source, hash) => {
|
|
1961
|
+
analysisEntries.push({
|
|
1962
|
+
path,
|
|
1963
|
+
source,
|
|
1964
|
+
hash: hash ?? contentHash(source)
|
|
1965
|
+
});
|
|
1966
|
+
ownership[originalPath].analysisPaths.push(path);
|
|
1967
|
+
analysisOwner.set(path, originalPath);
|
|
1968
|
+
};
|
|
1969
|
+
const addAdaptedEntries = (originalPath, entries) => {
|
|
1970
|
+
const pendingPaths = /* @__PURE__ */ new Set();
|
|
1971
|
+
for (const entry of entries) {
|
|
1972
|
+
const conflictingOriginalPath = rawOriginalPaths.has(entry.path) ? entry.path : analysisOwner.get(entry.path) ?? (pendingPaths.has(entry.path) ? originalPath : null);
|
|
1973
|
+
if (conflictingOriginalPath !== null) {
|
|
1974
|
+
diagnostics.push({
|
|
1975
|
+
code: "SOURCE_ANALYSIS_PATH_COLLISION",
|
|
1976
|
+
message: `Generated analysis path '${entry.path}' collides with '${conflictingOriginalPath}'. Rename one of the source files so adapted analysis paths remain unique.`,
|
|
1977
|
+
originalPath,
|
|
1978
|
+
analysisPath: entry.path,
|
|
1979
|
+
conflictingOriginalPath
|
|
1980
|
+
});
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
pendingPaths.add(entry.path);
|
|
1984
|
+
}
|
|
1985
|
+
for (const entry of entries) addAnalysisEntry(originalPath, entry.path, entry.source);
|
|
1986
|
+
};
|
|
1987
|
+
for (const original of originalEntries) {
|
|
1988
|
+
ownership[original.path] = {
|
|
1989
|
+
originalPath: original.path,
|
|
1990
|
+
originalHash: original.hash,
|
|
1991
|
+
analysisPaths: []
|
|
1992
|
+
};
|
|
1993
|
+
const kind = extension(original.path);
|
|
1994
|
+
if (kind === ".svelte") {
|
|
1995
|
+
svelteEntries.push(original);
|
|
1996
|
+
continue;
|
|
1997
|
+
}
|
|
1998
|
+
if (kind !== ".mdx") {
|
|
1999
|
+
addAnalysisEntry(original.path, original.path, original.source, original.hash);
|
|
2000
|
+
continue;
|
|
2001
|
+
}
|
|
2002
|
+
const result = await mdxAdapter(original.source, original.path);
|
|
2003
|
+
if (result.kind === "missing-dep") {
|
|
2004
|
+
diagnostics.push({
|
|
2005
|
+
code: "SOURCE_MDX_DEPENDENCY_MISSING",
|
|
2006
|
+
message: "Install optional peer dependency '@mdx-js/mdx' to analyze opted-in MDX source.",
|
|
2007
|
+
originalPath: original.path
|
|
2008
|
+
});
|
|
2009
|
+
continue;
|
|
2010
|
+
}
|
|
2011
|
+
if (result.kind === "error" || result.source === void 0) {
|
|
2012
|
+
diagnostics.push({
|
|
2013
|
+
code: "SOURCE_MDX_PARSE_ERROR",
|
|
2014
|
+
message: result.error ?? "MDX preprocessing produced no source.",
|
|
2015
|
+
originalPath: original.path
|
|
2016
|
+
});
|
|
2017
|
+
continue;
|
|
2018
|
+
}
|
|
2019
|
+
addAdaptedEntries(original.path, [{
|
|
2020
|
+
path: `${original.path}.tsx`,
|
|
2021
|
+
source: result.source
|
|
2022
|
+
}]);
|
|
2023
|
+
}
|
|
2024
|
+
const facts = collectFileFacts(analysisEntries, options);
|
|
2025
|
+
for (const [analysisPath, file] of Object.entries(facts.files)) {
|
|
2026
|
+
const originalPath = analysisOwner.get(analysisPath) ?? analysisPath;
|
|
2027
|
+
for (const message of file.parseDiagnostics) diagnostics.push({
|
|
2028
|
+
code: "SOURCE_NATIVE_PARSE_ERROR",
|
|
2029
|
+
message,
|
|
2030
|
+
originalPath,
|
|
2031
|
+
analysisPath
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
const resolvers = new ResolverExportIndex(facts, analysisEntries.map((entry) => entry.path));
|
|
2035
|
+
for (const { canonicalPath, paths } of resolvers.collisions) for (let index = 0; index < paths.length; index += 1) {
|
|
2036
|
+
const path = paths[index];
|
|
2037
|
+
const conflictingPath = paths[(index + 1) % paths.length];
|
|
2038
|
+
diagnostics.push({
|
|
2039
|
+
code: "SOURCE_RESOLVER_IDENTITY_COLLISION",
|
|
2040
|
+
message: `Resolver lookup paths '${path}' and '${conflictingPath}' normalize to the same private identity '${canonicalPath}'. Rename one of the source files so resolver identities remain unique.`,
|
|
2041
|
+
originalPath: analysisOwner.get(path) ?? path,
|
|
2042
|
+
canonicalPath,
|
|
2043
|
+
conflictingOriginalPath: analysisOwner.get(conflictingPath) ?? conflictingPath
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
for (const original of svelteEntries) {
|
|
2047
|
+
const result = await svelteAdapter(original.source, original.path, { attributeResolver: (request) => resolvers.attribute(original.path, request) });
|
|
2048
|
+
if (result.kind === "missing-dep") {
|
|
2049
|
+
diagnostics.push({
|
|
2050
|
+
code: "SOURCE_SVELTE_DEPENDENCY_MISSING",
|
|
2051
|
+
message: "Install optional peer dependency 'svelte' to analyze opted-in Svelte source.",
|
|
2052
|
+
originalPath: original.path
|
|
2053
|
+
});
|
|
2054
|
+
continue;
|
|
2055
|
+
}
|
|
2056
|
+
if (result.kind === "error") {
|
|
2057
|
+
diagnostics.push(...result.diagnostics);
|
|
2058
|
+
continue;
|
|
2059
|
+
}
|
|
2060
|
+
addAdaptedEntries(original.path, result.entries);
|
|
2061
|
+
}
|
|
2062
|
+
return {
|
|
2063
|
+
originalEntries,
|
|
2064
|
+
analysisEntries,
|
|
2065
|
+
ownership,
|
|
2066
|
+
diagnostics
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
/**
|
|
2070
|
+
* Run the native fact collector, re-extracting only changed entries when the
|
|
2071
|
+
* caller supplies a `factsCache`. `parseCount` keeps its meaning — parses
|
|
2072
|
+
* performed by THIS call — so a fully-memoized pass reports zero.
|
|
2073
|
+
*/
|
|
2074
|
+
function collectFileFacts(analysisEntries, options) {
|
|
2075
|
+
const cache = options.factsCache;
|
|
2076
|
+
if (!cache) return JSON.parse(options.extractFacts(JSON.stringify(analysisEntries)));
|
|
2077
|
+
const pending = analysisEntries.filter((entry) => cache.get(entry.path)?.hash !== entry.hash);
|
|
2078
|
+
let parseCount = 0;
|
|
2079
|
+
if (pending.length > 0) {
|
|
2080
|
+
const fresh = JSON.parse(options.extractFacts(JSON.stringify(pending)));
|
|
2081
|
+
parseCount = fresh.parseCount;
|
|
2082
|
+
for (const entry of pending) {
|
|
2083
|
+
const facts = fresh.files[entry.path];
|
|
2084
|
+
if (facts) cache.set(entry.path, {
|
|
2085
|
+
hash: entry.hash,
|
|
2086
|
+
facts
|
|
2087
|
+
});
|
|
2088
|
+
else cache.delete(entry.path);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
const currentPaths = new Set(analysisEntries.map((entry) => entry.path));
|
|
2092
|
+
for (const path of [...cache.keys()]) if (!currentPaths.has(path)) cache.delete(path);
|
|
2093
|
+
const files = {};
|
|
2094
|
+
for (const entry of analysisEntries) {
|
|
2095
|
+
const cached = cache.get(entry.path);
|
|
2096
|
+
if (cached) files[entry.path] = cached.facts;
|
|
2097
|
+
}
|
|
2098
|
+
return {
|
|
2099
|
+
files,
|
|
2100
|
+
parseCount
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
2103
|
+
/**
|
|
2104
|
+
* The per-file quarantine: drop every original a diagnostic named — and the
|
|
2105
|
+
* analysis children it owns — so one invalid source never aborts the rest
|
|
2106
|
+
* of the corpus. Shared by buildStart AND every incremental path in both
|
|
2107
|
+
* hosts; asymmetry here is how a permanently-diagnosable file (an `.mdx`
|
|
2108
|
+
* with the optional peer absent, an unsupported `.svelte` shape) froze all
|
|
2109
|
+
* re-analysis for the life of the dev server.
|
|
2110
|
+
*/
|
|
2111
|
+
function withoutInvalidOriginals(result, invalidOriginals) {
|
|
2112
|
+
if (invalidOriginals.size === 0) return result;
|
|
2113
|
+
const ownership = Object.fromEntries(Object.entries(result.ownership).filter(([originalPath]) => !invalidOriginals.has(originalPath)));
|
|
2114
|
+
const analysisPaths = new Set(Object.values(ownership).flatMap((owner) => owner.analysisPaths));
|
|
2115
|
+
return {
|
|
2116
|
+
...result,
|
|
2117
|
+
originalEntries: result.originalEntries.filter((entry) => !invalidOriginals.has(entry.path)),
|
|
2118
|
+
analysisEntries: result.analysisEntries.filter((entry) => analysisPaths.has(entry.path)),
|
|
2119
|
+
ownership
|
|
2120
|
+
};
|
|
2121
|
+
}
|
|
2122
|
+
/**
|
|
2123
|
+
* The one source-ingestion policy point shared by every host (vite-plugin,
|
|
2124
|
+
* next-plugin, cli, unplugin). Hosts hold exactly their prefix, strict flag,
|
|
2125
|
+
* and warn sink; the capability guard, facts memo, and warn-dedupe lifecycle
|
|
2126
|
+
* live here so the plugins cannot fork (precedent: pkg-collection
|
|
2127
|
+
* divergences; see also `enforceExternalTokenContracts`).
|
|
2128
|
+
*/
|
|
2129
|
+
function createSourceIngestor(host) {
|
|
2130
|
+
const factsCache = /* @__PURE__ */ new Map();
|
|
2131
|
+
/** Non-strict warn dedupe: a quarantined-but-retained original re-ingests
|
|
2132
|
+
* on every later corpus pass, and re-warning each save is noise. Keyed by
|
|
2133
|
+
* original path; cleared when that original publishes clean again. */
|
|
2134
|
+
const warnedByOriginal = /* @__PURE__ */ new Map();
|
|
2135
|
+
return {
|
|
2136
|
+
async ingest(entries) {
|
|
2137
|
+
const extractFacts = host.engineApi().extractFacts;
|
|
2138
|
+
if (typeof extractFacts !== "function") throw new Error(`${host.prefix} native engine does not expose extractFacts required for source adaptation`);
|
|
2139
|
+
return ingestSourceEntries(entries, {
|
|
2140
|
+
extractFacts,
|
|
2141
|
+
factsCache
|
|
2142
|
+
});
|
|
2143
|
+
},
|
|
2144
|
+
surfaceDiagnostics(diagnostics) {
|
|
2145
|
+
const fatal = diagnostics.filter((diagnostic) => !isAdvisorySourceDiagnostic(diagnostic));
|
|
2146
|
+
const invalidOriginals = new Set(fatal.map((diagnostic) => diagnostic.originalPath));
|
|
2147
|
+
if (diagnostics.length === 0) return invalidOriginals;
|
|
2148
|
+
if (host.strict() && fatal.length > 0) {
|
|
2149
|
+
const lines = fatal.map((diagnostic) => `${diagnostic.code} ${diagnostic.originalPath}: ${diagnostic.message}`);
|
|
2150
|
+
throw new Error(`${host.prefix} ${lines.join(`\n${host.prefix} `)}`);
|
|
2151
|
+
}
|
|
2152
|
+
for (const diagnostic of diagnostics) {
|
|
2153
|
+
const line = `${diagnostic.code} ${diagnostic.originalPath}: ${diagnostic.message}`;
|
|
2154
|
+
let warned = warnedByOriginal.get(diagnostic.originalPath);
|
|
2155
|
+
if (!warned) {
|
|
2156
|
+
warned = /* @__PURE__ */ new Set();
|
|
2157
|
+
warnedByOriginal.set(diagnostic.originalPath, warned);
|
|
2158
|
+
}
|
|
2159
|
+
if (warned.has(line)) continue;
|
|
2160
|
+
warned.add(line);
|
|
2161
|
+
host.warn(`${host.prefix} ${line}`);
|
|
2162
|
+
}
|
|
2163
|
+
return invalidOriginals;
|
|
2164
|
+
},
|
|
2165
|
+
markPublished(result) {
|
|
2166
|
+
for (const originalPath of Object.keys(result.ownership)) warnedByOriginal.delete(originalPath);
|
|
2167
|
+
}
|
|
2168
|
+
};
|
|
2169
|
+
}
|
|
2170
|
+
//#endregion
|
|
2171
|
+
//#region pipeline/asset-placeholders.ts
|
|
2172
|
+
/**
|
|
2173
|
+
* Host-side handling of `asset()` placeholders (global-styles-system):
|
|
2174
|
+
* `animus-asset:<specifier>` markers ride through the sandbox and the
|
|
2175
|
+
* emitter byte-exact; each HOST plugin substitutes them with its bundler's
|
|
2176
|
+
* resolved asset URL after extraction. This module owns the shared scanning
|
|
2177
|
+
* and replacement mechanics — resolution and strict gating stay at the
|
|
2178
|
+
* plugin call sites.
|
|
2179
|
+
*/
|
|
2180
|
+
/** Mirrors the producer-side constant in `@animus-ui/system`'s `asset.ts`
|
|
2181
|
+
* (extract must not take a runtime dependency on system); the pairing is
|
|
2182
|
+
* pinned by a contract test in `tests/asset-placeholders.test.ts`. */
|
|
2183
|
+
const ASSET_PLACEHOLDER_PREFIX = "animus-asset:";
|
|
2184
|
+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2185
|
+
const PREFIX_RE = escapeRegExp(ASSET_PLACEHOLDER_PREFIX);
|
|
2186
|
+
/**
|
|
2187
|
+
* Quoted `url('animus-asset:<specifier>')` form — the emitter always quotes
|
|
2188
|
+
* `url()` values, so a quoted match carries the FULL specifier, including
|
|
2189
|
+
* whitespace and parentheses (D5 pins the placeholder as specifier-verbatim,
|
|
2190
|
+
* so scanning fidelity is the only place such characters can be honored).
|
|
2191
|
+
*/
|
|
2192
|
+
const QUOTED_PLACEHOLDER_RE = new RegExp(`(['"])${PREFIX_RE}([^'"]*?)\\1`, "g");
|
|
2193
|
+
/** Bare-form fallback: a placeholder up to CSS url()/quote/whitespace. */
|
|
2194
|
+
const BARE_PLACEHOLDER_RE = new RegExp(`${PREFIX_RE}([^'")\\s]+)`, "g");
|
|
2195
|
+
/** Unique asset specifiers referenced by placeholders, in appearance order. */
|
|
2196
|
+
function findAssetSpecifiers(css) {
|
|
2197
|
+
if (!css.includes("animus-asset:")) return [];
|
|
2198
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2199
|
+
const remainder = css.replace(QUOTED_PLACEHOLDER_RE, (_match, _quote, specifier) => {
|
|
2200
|
+
seen.add(specifier);
|
|
2201
|
+
return "";
|
|
2202
|
+
});
|
|
2203
|
+
for (const match of remainder.matchAll(BARE_PLACEHOLDER_RE)) seen.add(match[1]);
|
|
2204
|
+
return [...seen];
|
|
2205
|
+
}
|
|
2206
|
+
/**
|
|
2207
|
+
* Replace each specifier's placeholder with its substitution. Specifiers
|
|
2208
|
+
* absent from the map keep their placeholder — the caller decides whether
|
|
2209
|
+
* that is a strict failure or a warn-and-emit-literally. A substitution may
|
|
2210
|
+
* itself be a bundler marker (e.g. Vite's `__VITE_ASSET__<ref>__`) that the
|
|
2211
|
+
* host's own asset pipeline resolves to a hashed file name later.
|
|
2212
|
+
*
|
|
2213
|
+
* Each replacement is anchored to a following delimiter (quote, `)`,
|
|
2214
|
+
* whitespace, or end of input) so a specifier that textually prefixes a
|
|
2215
|
+
* longer one can never clobber the longer placeholder; longest-first
|
|
2216
|
+
* ordering additionally lets the longer of two mapped specifiers claim its
|
|
2217
|
+
* occurrences before the shorter runs.
|
|
2218
|
+
*/
|
|
2219
|
+
function substituteAssetPlaceholders(css, urlBySpecifier) {
|
|
2220
|
+
if (urlBySpecifier.size === 0) return css;
|
|
2221
|
+
if (!css.includes("animus-asset:")) return css;
|
|
2222
|
+
const specifiers = [...urlBySpecifier.keys()].sort((a, b) => b.length - a.length);
|
|
2223
|
+
let out = css;
|
|
2224
|
+
for (const specifier of specifiers) {
|
|
2225
|
+
const placeholder = new RegExp(escapeRegExp(ASSET_PLACEHOLDER_PREFIX + specifier) + String.raw`(?=['")\s]|$)`, "g");
|
|
2226
|
+
out = out.replace(placeholder, () => urlBySpecifier.get(specifier));
|
|
2227
|
+
}
|
|
2228
|
+
return out;
|
|
2229
|
+
}
|
|
2230
|
+
//#endregion
|
|
2231
|
+
//#region pipeline/resolve-asset.ts
|
|
2232
|
+
/**
|
|
2233
|
+
* Node-side asset specifier resolution shared by the host plugins (the
|
|
2234
|
+
* asset() contract, global-styles-system): host path aliases apply FIRST —
|
|
2235
|
+
* an alias such as `@fonts` works in application modules, so
|
|
2236
|
+
* `asset('@fonts/inter.woff2')` must resolve identically — then direct Node
|
|
2237
|
+
* resolution, then the package-root fallback (exports maps rarely list
|
|
2238
|
+
* asset subpaths). Bundler-native resolution (Vite's `this.resolve`) is
|
|
2239
|
+
* still preferred where available; this resolver covers the paths that have
|
|
2240
|
+
* no bundler hook (Next's session, Vite's dev re-analysis).
|
|
2241
|
+
*/
|
|
2242
|
+
const requireByRoot = /* @__PURE__ */ new Map();
|
|
2243
|
+
function requireAnchoredAt(rootDir) {
|
|
2244
|
+
let req = requireByRoot.get(rootDir);
|
|
2245
|
+
if (!req) {
|
|
2246
|
+
req = createRequire$1(join(rootDir, "package.json"));
|
|
2247
|
+
requireByRoot.set(rootDir, req);
|
|
2248
|
+
}
|
|
2249
|
+
return req;
|
|
2250
|
+
}
|
|
2251
|
+
function packageRootFromEntry(entry) {
|
|
2252
|
+
let current = dirname(entry);
|
|
2253
|
+
while (true) {
|
|
2254
|
+
if (existsSync(join(current, "package.json"))) return current;
|
|
2255
|
+
const parent = dirname(current);
|
|
2256
|
+
if (parent === current) return null;
|
|
2257
|
+
current = parent;
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
const aliasTableCache = /* @__PURE__ */ new Map();
|
|
2261
|
+
function parseAliasTable(pathAliasesJson) {
|
|
2262
|
+
const cached = aliasTableCache.get(pathAliasesJson);
|
|
2263
|
+
if (cached) return cached;
|
|
2264
|
+
let aliases;
|
|
2265
|
+
try {
|
|
2266
|
+
aliases = JSON.parse(pathAliasesJson).aliases ?? [];
|
|
2267
|
+
} catch {
|
|
2268
|
+
aliases = [];
|
|
2269
|
+
}
|
|
2270
|
+
aliasTableCache.set(pathAliasesJson, aliases);
|
|
2271
|
+
return aliases;
|
|
2272
|
+
}
|
|
2273
|
+
/**
|
|
2274
|
+
* Map a specifier through the harvested host alias table (the same
|
|
2275
|
+
* `pathAliasesJson` wire the engine consumes — entries pre-sorted longest
|
|
2276
|
+
* pattern first by `buildPathAliasesJson`). Returns an absolute path to an
|
|
2277
|
+
* EXISTING file, or null when no alias claims the specifier.
|
|
2278
|
+
*/
|
|
2279
|
+
function resolveThroughPathAliases(specifier, rootDir, pathAliasesJson) {
|
|
2280
|
+
if (!pathAliasesJson) return null;
|
|
2281
|
+
const aliases = parseAliasTable(pathAliasesJson);
|
|
2282
|
+
for (const alias of aliases) {
|
|
2283
|
+
let mapped = null;
|
|
2284
|
+
if (alias.type === "exact") {
|
|
2285
|
+
if (alias.pattern === specifier) mapped = alias.replacement;
|
|
2286
|
+
} else if (specifier.startsWith(alias.pattern)) mapped = alias.replacement + specifier.slice(alias.pattern.length);
|
|
2287
|
+
if (mapped === null) continue;
|
|
2288
|
+
const absolute = isAbsolute(mapped) ? mapped : join(rootDir, mapped);
|
|
2289
|
+
if (existsSync(absolute)) return absolute;
|
|
2290
|
+
}
|
|
2291
|
+
return null;
|
|
2292
|
+
}
|
|
2293
|
+
/**
|
|
2294
|
+
* Resolve an asset specifier to an absolute file: host aliases, then direct
|
|
2295
|
+
* Node resolution anchored at `rootDir`, then the package-root fallback.
|
|
2296
|
+
* Returns null when nothing matches — strict gating stays at the caller.
|
|
2297
|
+
*/
|
|
2298
|
+
function resolveAssetFile(specifier, rootDir, pathAliasesJson) {
|
|
2299
|
+
const aliased = resolveThroughPathAliases(specifier, rootDir, pathAliasesJson);
|
|
2300
|
+
if (aliased) return aliased;
|
|
2301
|
+
const requireFromRoot = requireAnchoredAt(rootDir);
|
|
2302
|
+
try {
|
|
2303
|
+
return requireFromRoot.resolve(specifier);
|
|
2304
|
+
} catch {}
|
|
2305
|
+
const segments = specifier.split("/");
|
|
2306
|
+
const packageName = specifier.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0];
|
|
2307
|
+
const subpath = specifier.slice(packageName.length + 1);
|
|
2308
|
+
if (!subpath) return null;
|
|
2309
|
+
for (const modulesDir of requireFromRoot.resolve.paths(packageName) ?? []) {
|
|
2310
|
+
const packageRoot = join(modulesDir, packageName);
|
|
2311
|
+
if (!existsSync(join(packageRoot, "package.json"))) continue;
|
|
2312
|
+
const candidate = join(packageRoot, subpath);
|
|
2313
|
+
if (existsSync(candidate)) return candidate;
|
|
2314
|
+
}
|
|
2315
|
+
try {
|
|
2316
|
+
const packageRoot = packageRootFromEntry(requireFromRoot.resolve(packageName));
|
|
2317
|
+
if (!packageRoot) return null;
|
|
2318
|
+
const candidate = join(packageRoot, subpath);
|
|
2319
|
+
return existsSync(candidate) ? candidate : null;
|
|
2320
|
+
} catch {
|
|
2321
|
+
return null;
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
//#endregion
|
|
2325
|
+
//#region pipeline/correlate-external-tokens.ts
|
|
2326
|
+
/**
|
|
2327
|
+
* Index the loader-captured theme manifests by owning specifier. Module paths
|
|
2328
|
+
* are canonical absolute paths (symlinks resolved), so each package dir is
|
|
2329
|
+
* realpath'd before prefix-matching; a module outside every known package dir
|
|
2330
|
+
* (typically the consumer's own theme) contributes nothing.
|
|
2331
|
+
*/
|
|
2332
|
+
function buildSourceTokenIndex(opts) {
|
|
2333
|
+
const index = /* @__PURE__ */ new Map();
|
|
2334
|
+
if (!opts.sourceThemeManifestsJson) return index;
|
|
2335
|
+
let manifests;
|
|
2336
|
+
try {
|
|
2337
|
+
manifests = JSON.parse(opts.sourceThemeManifestsJson);
|
|
2338
|
+
} catch {
|
|
2339
|
+
return index;
|
|
2340
|
+
}
|
|
2341
|
+
const realDirOwners = [];
|
|
2342
|
+
const addOwnerDir = (dir, specifier) => {
|
|
2343
|
+
if (!realDirOwners.some((entry) => entry.dir === dir)) realDirOwners.push({
|
|
2344
|
+
dir,
|
|
2345
|
+
specifier
|
|
2346
|
+
});
|
|
2347
|
+
};
|
|
2348
|
+
for (const [dir, specifier] of Object.entries(opts.dirOwners)) {
|
|
2349
|
+
let real = dir;
|
|
2350
|
+
try {
|
|
2351
|
+
real = realpathSync(dir);
|
|
2352
|
+
} catch {}
|
|
2353
|
+
addOwnerDir(real, specifier);
|
|
2354
|
+
let packageRoot = real;
|
|
2355
|
+
while (packageRoot !== dirname(packageRoot) && !existsSync(join(packageRoot, "package.json"))) packageRoot = dirname(packageRoot);
|
|
2356
|
+
if (existsSync(join(packageRoot, "package.json"))) addOwnerDir(packageRoot, specifier);
|
|
2357
|
+
}
|
|
2358
|
+
for (const [modulePath, exports] of Object.entries(manifests)) {
|
|
2359
|
+
const owner = realDirOwners.find(({ dir }) => isPathWithinRoot(dir, modulePath));
|
|
2360
|
+
if (!owner) continue;
|
|
2361
|
+
let tokens = index.get(owner.specifier);
|
|
2362
|
+
if (!tokens) {
|
|
2363
|
+
tokens = /* @__PURE__ */ new Set();
|
|
2364
|
+
index.set(owner.specifier, tokens);
|
|
2365
|
+
}
|
|
2366
|
+
for (const paths of Object.values(exports)) for (const token of paths) tokens.add(token);
|
|
2367
|
+
}
|
|
2368
|
+
return index;
|
|
2369
|
+
}
|
|
2370
|
+
/**
|
|
2371
|
+
* The correlation join. Returns one teaching-error message per distinct
|
|
2372
|
+
* (component, token, specifier) whose file belongs to a discovered source AND
|
|
2373
|
+
* whose token that source's manifest defines. The messages follow the
|
|
2374
|
+
* standard severity routing at the CALLER (throw under `strict`, warn
|
|
2375
|
+
* otherwise) — this join never reports on its own.
|
|
2376
|
+
*/
|
|
2377
|
+
function correlateExternalTokenDiagnostics(opts) {
|
|
2378
|
+
const messages = [];
|
|
2379
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2380
|
+
for (const diagnostic of opts.diagnostics ?? []) {
|
|
2381
|
+
if (diagnostic.kind !== "external-token-candidate" || !diagnostic.token) continue;
|
|
2382
|
+
const specifier = opts.fileOwners[diagnostic.file];
|
|
2383
|
+
if (!specifier) continue;
|
|
2384
|
+
if (!opts.sourceTokens.get(specifier)?.has(diagnostic.token)) continue;
|
|
2385
|
+
const key = `${diagnostic.component}\u0000${diagnostic.token}\u0000${specifier}`;
|
|
2386
|
+
if (seen.has(key)) continue;
|
|
2387
|
+
seen.add(key);
|
|
2388
|
+
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}'`);
|
|
2389
|
+
}
|
|
2390
|
+
return messages;
|
|
2391
|
+
}
|
|
2392
|
+
const indexCache = /* @__PURE__ */ new WeakMap();
|
|
2393
|
+
/**
|
|
2394
|
+
* The complete cross-source token-contract gate both plugins share
|
|
2395
|
+
* (extraction-diagnostics): index the loader-captured source manifests,
|
|
2396
|
+
* join them against the engine's candidates, and route the resulting
|
|
2397
|
+
* teaching errors — throw under `strict`, warn otherwise. One entry point
|
|
2398
|
+
* so the two hosts cannot drift on wiring or severity semantics.
|
|
2399
|
+
*/
|
|
2400
|
+
function enforceExternalTokenContracts(opts) {
|
|
2401
|
+
const manifestsJson = opts.sourceThemeManifestsJson ?? "";
|
|
2402
|
+
const cached = indexCache.get(opts.dirOwners);
|
|
2403
|
+
let sourceTokens;
|
|
2404
|
+
if (cached && cached.manifestsJson === manifestsJson) sourceTokens = cached.index;
|
|
2405
|
+
else {
|
|
2406
|
+
sourceTokens = buildSourceTokenIndex({
|
|
2407
|
+
sourceThemeManifestsJson: opts.sourceThemeManifestsJson,
|
|
2408
|
+
dirOwners: opts.dirOwners
|
|
2409
|
+
});
|
|
2410
|
+
indexCache.set(opts.dirOwners, {
|
|
2411
|
+
manifestsJson,
|
|
2412
|
+
index: sourceTokens
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2415
|
+
const messages = correlateExternalTokenDiagnostics({
|
|
2416
|
+
diagnostics: opts.diagnostics,
|
|
2417
|
+
fileOwners: opts.fileOwners,
|
|
2418
|
+
sourceTokens
|
|
2419
|
+
});
|
|
2420
|
+
if (messages.length === 0) return;
|
|
2421
|
+
if (opts.strict) throw new Error(`${opts.prefix} ${messages.join("\n")}`);
|
|
2422
|
+
for (const message of messages) opts.warn(message);
|
|
2423
|
+
}
|
|
2424
|
+
//#endregion
|
|
2425
|
+
//#region pipeline/path-aliases.ts
|
|
2426
|
+
/**
|
|
2427
|
+
* Normalize alias pairs: strip a `rootDir` prefix from targets, classify
|
|
2428
|
+
* exact (file) vs prefix (directory) matches, force trailing slashes on
|
|
2429
|
+
* prefix entries, and sort longest pattern first for correct matching
|
|
2430
|
+
* priority. Returns null when no entries survive.
|
|
2431
|
+
*/
|
|
2432
|
+
function buildPathAliasesJson(pairs, rootDir) {
|
|
2433
|
+
const entries = [];
|
|
2434
|
+
for (const { pattern, target, kind } of pairs) {
|
|
2435
|
+
const replacement = target.startsWith(rootDir) ? target.slice(rootDir.length + 1) : target;
|
|
2436
|
+
if (kind ? kind === "exact" : /\.\w+$/.test(replacement)) entries.push({
|
|
2437
|
+
pattern,
|
|
2438
|
+
replacement,
|
|
2439
|
+
type: "exact"
|
|
2440
|
+
});
|
|
2441
|
+
else entries.push({
|
|
2442
|
+
pattern: pattern.endsWith("/") ? pattern : pattern + "/",
|
|
2443
|
+
replacement: replacement.endsWith("/") ? replacement : replacement + "/",
|
|
2444
|
+
type: "prefix"
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
entries.sort((a, b) => b.pattern.length - a.pattern.length);
|
|
2448
|
+
if (entries.length === 0) return null;
|
|
2449
|
+
return {
|
|
2450
|
+
json: JSON.stringify({ aliases: entries }),
|
|
2451
|
+
count: entries.length
|
|
2452
|
+
};
|
|
2453
|
+
}
|
|
2454
|
+
//#endregion
|
|
2455
|
+
//#region pipeline/post-process-css.ts
|
|
2456
|
+
/**
|
|
2457
|
+
* CSS post-processing (spec: css-post-processing) — Lightning CSS
|
|
2458
|
+
* autoprefixing, syntax lowering, and optional minification against
|
|
2459
|
+
* browserslist targets. The single implementation consumed by both the
|
|
2460
|
+
* Vite plugin (virtual-module serve time) and the Next.js plugin
|
|
2461
|
+
* (ExtractionSession artifact emission).
|
|
2462
|
+
*/
|
|
2463
|
+
/**
|
|
2464
|
+
* Resolve browser targets for Lightning CSS.
|
|
2465
|
+
* Priority: explicit config → project browserslist → 'defaults' fallback.
|
|
2466
|
+
*/
|
|
2467
|
+
function resolveLightningTargets(explicitTargets, rootDir) {
|
|
2468
|
+
let queries;
|
|
2469
|
+
if (explicitTargets) queries = Array.isArray(explicitTargets) ? explicitTargets : [explicitTargets];
|
|
2470
|
+
else {
|
|
2471
|
+
const detected = browserslist(void 0, { path: rootDir });
|
|
2472
|
+
queries = detected.length > 0 ? detected : browserslist("defaults");
|
|
2473
|
+
}
|
|
2474
|
+
return browserslistToTargets(Array.isArray(queries) && typeof queries[0] === "string" && queries[0].includes(" ") ? browserslist(queries) : queries);
|
|
2475
|
+
}
|
|
2476
|
+
/**
|
|
2477
|
+
* Post-process CSS with Lightning CSS: autoprefixing + optional minification.
|
|
2478
|
+
* On failure, returns the original CSS and logs a warning.
|
|
2479
|
+
*/
|
|
2480
|
+
function postProcessCss(css, opts) {
|
|
2481
|
+
if (!css) return css;
|
|
2482
|
+
try {
|
|
2483
|
+
return transform({
|
|
2484
|
+
filename: "animus-extracted.css",
|
|
2485
|
+
code: Buffer.from(css),
|
|
2486
|
+
minify: opts.minify,
|
|
2487
|
+
targets: opts.targets
|
|
2488
|
+
}).code.toString();
|
|
2489
|
+
} catch (e) {
|
|
2490
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2491
|
+
(opts.warnFn ?? console.warn)(`[animus] Lightning CSS post-processing failed: ${msg}`);
|
|
2492
|
+
return css;
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
//#endregion
|
|
2496
|
+
//#region pipeline/manifest-diagnostics.ts
|
|
2497
|
+
/** Stable code for selector forms with no substitutable subject.
|
|
2498
|
+
* Mirrors the Rust constant in `extract-v2/src/eval.rs`. */
|
|
2499
|
+
const SELECTOR_UNSUPPORTED_SUBJECT = "animus.selector.unsupported-subject";
|
|
2500
|
+
/** Unresolved-parent chain-drop message (mirrors the Rust diagnostic in
|
|
2501
|
+
* `extract-v2` — the ONE encoding of this message shape; consumers match
|
|
2502
|
+
* through the helpers below, never their own regex copies). */
|
|
2503
|
+
const UNRESOLVED_PARENT_RE = /chain dropped: could not resolve parent component '([^']+)'/;
|
|
2504
|
+
/** True when the diagnostic reports a chain dropped for an unresolved
|
|
2505
|
+
* parent component. */
|
|
2506
|
+
function isUnresolvedParentDrop(diagnostic) {
|
|
2507
|
+
return typeof diagnostic?.message === "string" && UNRESOLVED_PARENT_RE.test(diagnostic.message);
|
|
2508
|
+
}
|
|
2509
|
+
/** The parent binding named by an unresolved-parent drop, or null when the
|
|
2510
|
+
* diagnostic is not one. */
|
|
2511
|
+
function unresolvedParentName(diagnostic) {
|
|
2512
|
+
if (typeof diagnostic?.message !== "string") return null;
|
|
2513
|
+
return UNRESOLVED_PARENT_RE.exec(diagnostic.message)?.[1] ?? null;
|
|
2514
|
+
}
|
|
2515
|
+
/** True when the selector string carries at least one substitutable `&`
|
|
2516
|
+
* subject outside quoted text (mirrors the Rust `selector_subject` walk —
|
|
2517
|
+
* ancestor, leading, and repeated subjects all count; a `&` inside a
|
|
2518
|
+
* quoted attribute value does not). */
|
|
2519
|
+
function hasSelectorSubject(value) {
|
|
2520
|
+
let quote = null;
|
|
2521
|
+
let escaped = false;
|
|
2522
|
+
for (const c of value) {
|
|
2523
|
+
if (escaped) {
|
|
2524
|
+
escaped = false;
|
|
2525
|
+
continue;
|
|
2526
|
+
}
|
|
2527
|
+
if (c === "\\") {
|
|
2528
|
+
escaped = true;
|
|
2529
|
+
continue;
|
|
2530
|
+
}
|
|
2531
|
+
if (quote !== null) {
|
|
2532
|
+
if (c === quote) quote = null;
|
|
2533
|
+
} else if (c === "\"" || c === "'") quote = c;
|
|
2534
|
+
else if (c === "&") return true;
|
|
2535
|
+
}
|
|
2536
|
+
return false;
|
|
2537
|
+
}
|
|
2538
|
+
/**
|
|
2539
|
+
* Synthesize coded diagnostics for registered selector-alias values that
|
|
2540
|
+
* look selector-shaped (`&` present) but carry no substitutable subject —
|
|
2541
|
+
* every `&` sits inside quoted text, so there is nothing to anchor the
|
|
2542
|
+
* class to. Ancestor-prefixed and repeated subjects are supported and pass
|
|
2543
|
+
* validation. The system-config boundary is where these must fail loud
|
|
2544
|
+
* (alias values never reach the evaluator's key guard).
|
|
2545
|
+
*/
|
|
2546
|
+
function collectSelectorAliasDiagnostics(selectorAliasesJson) {
|
|
2547
|
+
if (!selectorAliasesJson) return [];
|
|
2548
|
+
let aliases;
|
|
2549
|
+
try {
|
|
2550
|
+
aliases = JSON.parse(selectorAliasesJson);
|
|
2551
|
+
} catch {
|
|
2552
|
+
return [];
|
|
2553
|
+
}
|
|
2554
|
+
const diagnostics = [];
|
|
2555
|
+
for (const [name, value] of Object.entries(aliases)) {
|
|
2556
|
+
if (typeof value !== "string") continue;
|
|
2557
|
+
if (value.includes("&") && !hasSelectorSubject(value)) diagnostics.push({
|
|
2558
|
+
file: "system",
|
|
2559
|
+
component: name,
|
|
2560
|
+
kind: "warn",
|
|
2561
|
+
message: `selector alias '${name}' value '${value}' has no substitutable '&' subject outside quoted text (${SELECTOR_UNSUPPORTED_SUBJECT})`,
|
|
2562
|
+
code: SELECTOR_UNSUPPORTED_SUBJECT,
|
|
2563
|
+
severity: "error"
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2566
|
+
return diagnostics;
|
|
2567
|
+
}
|
|
2568
|
+
/**
|
|
2569
|
+
* Surface extraction-manifest diagnostics through a plugin's warn channel.
|
|
2570
|
+
*
|
|
2571
|
+
* Single authoritative copy for both extraction plugins — and the single
|
|
2572
|
+
* strict-escalation policy point: error-severity diagnostics throw one
|
|
2573
|
+
* Error naming every offender when `policy.strict`, and print as warnings
|
|
2574
|
+
* otherwise. Surfaces `bail` (component not extracted), `skip` (component
|
|
2575
|
+
* skipped), and `warn` kinds; unknown kinds stay silent. Printed lines
|
|
2576
|
+
* include the diagnostic code when the message doesn't already carry it.
|
|
2577
|
+
*/
|
|
2578
|
+
function surfaceManifestDiagnostics(manifest, warn, policy = {}) {
|
|
2579
|
+
const errors = [];
|
|
2580
|
+
const diagnostics = policy.prepend?.length ? [...policy.prepend, ...manifest.diagnostics ?? []] : manifest.diagnostics ?? [];
|
|
2581
|
+
for (const diagnostic of diagnostics) {
|
|
2582
|
+
let line = null;
|
|
2583
|
+
if (diagnostic.kind === "bail") line = `⚠ ${diagnostic.component} not extracted: ${diagnostic.message}`;
|
|
2584
|
+
else if (diagnostic.kind === "skip") line = `⚠ ${diagnostic.component}: skipped ${diagnostic.message}`;
|
|
2585
|
+
else if (diagnostic.kind === "warn") line = `⚠ ${diagnostic.file}: ${diagnostic.component}: ${diagnostic.message}`;
|
|
2586
|
+
if (line === null) continue;
|
|
2587
|
+
if (diagnostic.code && !diagnostic.message.includes(diagnostic.code)) line += ` [${diagnostic.code}]`;
|
|
2588
|
+
if (policy.strict && diagnostic.severity === "error") {
|
|
2589
|
+
errors.push(`${diagnostic.code ?? "error"} — ${diagnostic.component}: ${diagnostic.message}`);
|
|
2590
|
+
continue;
|
|
2591
|
+
}
|
|
2592
|
+
warn(line);
|
|
2593
|
+
}
|
|
2594
|
+
if (errors.length > 0) throw new Error(`[animus] strict: ${errors.length} error diagnostic(s):\n${errors.join("\n")}`);
|
|
2595
|
+
}
|
|
2596
|
+
//#endregion
|
|
2597
|
+
//#region pipeline/unit-fallback.ts
|
|
2598
|
+
/**
|
|
2599
|
+
* Append `px` to bare numeric values in CSS declarations for properties
|
|
2600
|
+
* that expect length units. Unitless properties are preserved as-is.
|
|
2601
|
+
* Numbers inside CSS function calls (cubic-bezier, rgb, calc, etc.) are skipped.
|
|
2602
|
+
*/
|
|
2603
|
+
function applyUnitFallback(css) {
|
|
2604
|
+
return css.replace(/([a-z-]+)\s*:\s*([^;{}]+);/g, (match, prop, value) => {
|
|
2605
|
+
if (isUnitlessProperty(prop)) return match;
|
|
2606
|
+
let depth = 0;
|
|
2607
|
+
let fixed = "";
|
|
2608
|
+
let i = 0;
|
|
2609
|
+
while (i < value.length) if (value[i] === "(") {
|
|
2610
|
+
depth++;
|
|
2611
|
+
fixed += value[i];
|
|
2612
|
+
i++;
|
|
2613
|
+
} else if (value[i] === ")") {
|
|
2614
|
+
depth--;
|
|
2615
|
+
fixed += value[i];
|
|
2616
|
+
i++;
|
|
2617
|
+
} else if (depth > 0) {
|
|
2618
|
+
fixed += value[i];
|
|
2619
|
+
i++;
|
|
2620
|
+
} else {
|
|
2621
|
+
const numMatch = value.slice(i).match(/^(-?\d+\.?\d*)/);
|
|
2622
|
+
if (numMatch) {
|
|
2623
|
+
const num = numMatch[1];
|
|
2624
|
+
const after = value[i + num.length];
|
|
2625
|
+
if (after && /[a-z%]/i.test(after)) fixed += num;
|
|
2626
|
+
else fixed += num + "px";
|
|
2627
|
+
i += num.length;
|
|
2628
|
+
} else {
|
|
2629
|
+
fixed += value[i];
|
|
2630
|
+
i++;
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
return fixed !== value ? `${prop}:${fixed};` : match;
|
|
2634
|
+
});
|
|
2635
|
+
}
|
|
2636
|
+
//#endregion
|
|
2637
|
+
//#region pipeline/run-analysis.ts
|
|
2638
|
+
/**
|
|
2639
|
+
* Build the named `analyzeProject` input set from analysis options. Also
|
|
2640
|
+
* the persistence shape for `.animus/analysis-inputs.json` — an isolated
|
|
2641
|
+
* process can replay the analysis from exactly this object
|
|
2642
|
+
* (spec: next-turbopack-integration).
|
|
2643
|
+
*/
|
|
2644
|
+
function buildAnalysisInputs(opts) {
|
|
2645
|
+
return {
|
|
2646
|
+
filesJson: JSON.stringify(opts.fileEntries),
|
|
2647
|
+
scalesJson: opts.system.scalesJson,
|
|
2648
|
+
variableMapJson: opts.system.variableMapJson,
|
|
2649
|
+
contextualVarsJson: opts.system.contextualVarsJson,
|
|
2650
|
+
propConfigJson: opts.system.propConfigJson,
|
|
2651
|
+
groupRegistryJson: opts.system.groupRegistryJson,
|
|
2652
|
+
packageResolutionJson: JSON.stringify(opts.packageMap),
|
|
2653
|
+
devMode: opts.devMode,
|
|
2654
|
+
emitterConfigJson: JSON.stringify({
|
|
2655
|
+
runtime_import: opts.emitter.runtimeImport,
|
|
2656
|
+
css_module_id: opts.emitter.cssModuleId,
|
|
2657
|
+
...opts.emitter.systemPropsModuleId ? { system_props_module_id: opts.emitter.systemPropsModuleId } : {}
|
|
2658
|
+
}),
|
|
2659
|
+
selectorAliasesJson: opts.system.selectorAliasesJson,
|
|
2660
|
+
globalStyleBlocksJson: opts.system.globalStyleBlocksJson,
|
|
2661
|
+
pathAliasesJson: opts.pathAliasesJson,
|
|
2662
|
+
keyframesJson: opts.system.keyframesJson,
|
|
2663
|
+
staticCssJson: opts.staticCssJson ?? null,
|
|
2664
|
+
conditionAliasesJson: opts.system.conditionAliasesJson ?? null,
|
|
2665
|
+
transformSourcesJson: opts.system.transformSourcesJson ?? null,
|
|
2666
|
+
externalDirsJson: opts.externalDirs?.length && hasSourceThemeManifests(opts.system) ? JSON.stringify(opts.externalDirs) : null
|
|
2667
|
+
};
|
|
2668
|
+
}
|
|
2669
|
+
/** Whether the loader captured at least one source built-theme manifest. */
|
|
2670
|
+
function hasSourceThemeManifests(system) {
|
|
2671
|
+
const json = system.sourceThemeManifestsJson;
|
|
2672
|
+
return typeof json === "string" && json.length > 0 && json !== "{}";
|
|
2673
|
+
}
|
|
2674
|
+
/**
|
|
2675
|
+
* The one analysis invocation both plugins share: build the emitter
|
|
2676
|
+
* config, serialize inputs, call the NAPI `analyzeProject`, parse the
|
|
2677
|
+
* manifest, surface its diagnostics, and resolve the CSS outputs.
|
|
2678
|
+
*
|
|
2679
|
+
* Error handling stays at the call site (strict-mode throw vs warn).
|
|
2680
|
+
*/
|
|
2681
|
+
function runProjectAnalysis(engineApi, opts) {
|
|
2682
|
+
const { analyzeProject } = engineApi();
|
|
2683
|
+
let t = performance.now();
|
|
2684
|
+
const inputs = buildAnalysisInputs(opts);
|
|
2685
|
+
const serializeMs = Math.round(performance.now() - t);
|
|
2686
|
+
t = performance.now();
|
|
2687
|
+
const manifestJson = analyzeProject(...buildAnalyzeProjectArgs(inputs));
|
|
2688
|
+
const extractMs = Math.round(performance.now() - t);
|
|
2689
|
+
t = performance.now();
|
|
2690
|
+
const manifest = JSON.parse(manifestJson);
|
|
2691
|
+
surfaceManifestDiagnostics(manifest, opts.warn, {
|
|
2692
|
+
strict: opts.strict,
|
|
2693
|
+
prepend: [...collectSelectorAliasDiagnostics(opts.system.selectorAliasesJson), ...opts.extraDiagnostics ?? []]
|
|
2694
|
+
});
|
|
2695
|
+
const parseMs = Math.round(performance.now() - t);
|
|
2696
|
+
return {
|
|
2697
|
+
manifest,
|
|
2698
|
+
manifestJson,
|
|
2699
|
+
globalCss: manifest?.sheets?.global || "",
|
|
2700
|
+
componentCss: applyUnitFallback(manifest?.css || ""),
|
|
2701
|
+
inputs,
|
|
2702
|
+
timings: {
|
|
2703
|
+
serializeMs,
|
|
2704
|
+
extractMs,
|
|
2705
|
+
parseMs
|
|
2706
|
+
}
|
|
2707
|
+
};
|
|
2708
|
+
}
|
|
2709
|
+
/**
|
|
2710
|
+
* Clear the engine's per-file analysis cache so stale results from a prior
|
|
2711
|
+
* build never bleed into a fresh run. Tolerates engines without the
|
|
2712
|
+
* capability (older builds) — the probe is benign.
|
|
2713
|
+
*/
|
|
2714
|
+
function clearEngineCache(engineApi) {
|
|
2715
|
+
try {
|
|
2716
|
+
const { clearAnalysisCache } = engineApi();
|
|
2717
|
+
clearAnalysisCache();
|
|
2718
|
+
} catch {}
|
|
2719
|
+
}
|
|
2720
|
+
//#endregion
|
|
2721
|
+
//#region pipeline/static-css.ts
|
|
2722
|
+
/** Key-sorted stringify so semantically identical configs serialize
|
|
2723
|
+
* identically (stable analysis-inputs hashing). */
|
|
2724
|
+
function stableStringify(value) {
|
|
2725
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
2726
|
+
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(",")}}`;
|
|
2727
|
+
return JSON.stringify(value);
|
|
2728
|
+
}
|
|
2729
|
+
/** Serialize a staticCss option for the engine; null when absent/empty. */
|
|
2730
|
+
function serializeStaticCss(config) {
|
|
2731
|
+
if (!config) return null;
|
|
2732
|
+
const json = stableStringify(config);
|
|
2733
|
+
return json === "{}" ? null : json;
|
|
2734
|
+
}
|
|
2735
|
+
//#endregion
|
|
2736
|
+
//#region pipeline/prefix.ts
|
|
2737
|
+
/**
|
|
2738
|
+
* Apply namespace prefix to a variable map, CSS variable declarations, and theme JSON.
|
|
2739
|
+
*
|
|
2740
|
+
* Variable map: `{ "colors.ember": "--color-ember" }` -> `{ "colors.ember": "--prefix-color-ember" }`
|
|
2741
|
+
* Variable CSS: `--color-ember: #FF2800` -> `--prefix-color-ember: #FF2800`
|
|
2742
|
+
* `var(--color-ember)` -> `var(--prefix-color-ember)`
|
|
2743
|
+
* Theme JSON: `{ "colors.ember": "var(--color-ember)" }` -> `{ "colors.ember": "var(--prefix-color-ember)" }`
|
|
2744
|
+
*/
|
|
2745
|
+
function applyPrefix(prefix, variableMapJson, variableCss, themeJson, contextualVarsJson) {
|
|
2746
|
+
if (!prefix) return {
|
|
2747
|
+
variableMapJson,
|
|
2748
|
+
variableCss,
|
|
2749
|
+
themeJson,
|
|
2750
|
+
contextualVarsJson
|
|
2751
|
+
};
|
|
2752
|
+
const varRefRe = /var\(--([a-zA-Z][\w-]*)\)/g;
|
|
2753
|
+
const map = JSON.parse(variableMapJson);
|
|
2754
|
+
const prefixed = {};
|
|
2755
|
+
for (const [key, varName] of Object.entries(map)) prefixed[key] = varName.startsWith("--") ? `--${prefix}-${varName.slice(2)}` : varName;
|
|
2756
|
+
let css = variableCss;
|
|
2757
|
+
css = css.replace(/--([a-zA-Z][\w-]*)\s*:/g, `--${prefix}-$1:`);
|
|
2758
|
+
css = css.replace(varRefRe, `var(--${prefix}-$1)`);
|
|
2759
|
+
const result = {
|
|
2760
|
+
variableMapJson: JSON.stringify(prefixed),
|
|
2761
|
+
variableCss: css
|
|
2762
|
+
};
|
|
2763
|
+
if (themeJson) result.themeJson = themeJson.replace(varRefRe, `var(--${prefix}-$1)`);
|
|
2764
|
+
if (contextualVarsJson) {
|
|
2765
|
+
const ctxVars = JSON.parse(contextualVarsJson);
|
|
2766
|
+
const prefixedCtx = {};
|
|
2767
|
+
for (const [scale, names] of Object.entries(ctxVars)) prefixedCtx[scale] = names.map((name) => `${prefix}-${name}`);
|
|
2768
|
+
result.contextualVarsJson = JSON.stringify(prefixedCtx);
|
|
2769
|
+
}
|
|
2770
|
+
return result;
|
|
2771
|
+
}
|
|
2772
|
+
//#endregion
|
|
2773
|
+
//#region pipeline/system-config.ts
|
|
2774
|
+
/**
|
|
2775
|
+
* Load a SystemInstance via the engine's `loadSystemModule` and normalize
|
|
2776
|
+
* it into a SystemConfig. When `prefix` is set, CSS variable names in the
|
|
2777
|
+
* variable map/css (and theme + contextual vars when affected) are
|
|
2778
|
+
* namespaced via the shared `applyPrefix`.
|
|
2779
|
+
*
|
|
2780
|
+
* Error handling stays at the call site — the Vite plugin warns (or throws
|
|
2781
|
+
* in strict mode) and keeps its previous config, the Next plugin lets the
|
|
2782
|
+
* failure propagate.
|
|
2783
|
+
*/
|
|
2784
|
+
function loadSystemConfig(engineApi, opts) {
|
|
2785
|
+
const { loadSystemModule } = engineApi();
|
|
2786
|
+
const config = loadSystemModule(opts.systemPath, opts.rootDir);
|
|
2787
|
+
let scalesJson = config.scalesJson;
|
|
2788
|
+
let variableMapJson = config.variableMapJson;
|
|
2789
|
+
let variableCss = config.variableCss;
|
|
2790
|
+
let contextualVarsJson = config.contextualVarsJson || null;
|
|
2791
|
+
if (opts.prefix) {
|
|
2792
|
+
const prefixed = applyPrefix(opts.prefix, variableMapJson, variableCss, scalesJson, contextualVarsJson || void 0);
|
|
2793
|
+
variableMapJson = prefixed.variableMapJson;
|
|
2794
|
+
variableCss = prefixed.variableCss;
|
|
2795
|
+
if (prefixed.themeJson) scalesJson = prefixed.themeJson;
|
|
2796
|
+
if (prefixed.contextualVarsJson) contextualVarsJson = prefixed.contextualVarsJson;
|
|
2797
|
+
}
|
|
2798
|
+
return {
|
|
2799
|
+
propConfigJson: config.propConfig,
|
|
2800
|
+
groupRegistryJson: config.groupRegistry,
|
|
2801
|
+
scalesJson,
|
|
2802
|
+
variableMapJson,
|
|
2803
|
+
variableCss,
|
|
2804
|
+
contextualVarsJson,
|
|
2805
|
+
selectorAliasesJson: config.selectorAliases || null,
|
|
2806
|
+
conditionAliasesJson: config.conditionAliases || null,
|
|
2807
|
+
transformSourcesJson: config.transformSources || null,
|
|
2808
|
+
globalStyleBlocksJson: config.globalStyleBlocks || null,
|
|
2809
|
+
keyframesJson: config.keyframesBlocks || null,
|
|
2810
|
+
dependencies: config.dependencies ?? [],
|
|
2811
|
+
sourceThemeManifestsJson: config.sourceThemeManifests || null
|
|
2812
|
+
};
|
|
2813
|
+
}
|
|
2814
|
+
//#endregion
|
|
2815
|
+
//#region pipeline/structural-self-check.ts
|
|
2816
|
+
/**
|
|
2817
|
+
* Structural self-check over an emitted extraction result — the affirmative
|
|
2818
|
+
* post-condition against silent-empty output (openspec:
|
|
2819
|
+
* standalone-extraction-cli, "Silent-empty success is impossible").
|
|
2820
|
+
*
|
|
2821
|
+
* One shared implementation for every driver: the Vite plugin's `verify`
|
|
2822
|
+
* option and the standalone CLI (default-on) both call this; the caller
|
|
2823
|
+
* owns the failure POLICY (warn vs throw vs exit code), never the checks.
|
|
2824
|
+
*/
|
|
2825
|
+
/** Run every structural check; returns human-readable failure lines
|
|
2826
|
+
* (empty = pass). */
|
|
2827
|
+
function runStructuralSelfCheck(input) {
|
|
2828
|
+
const failures = [];
|
|
2829
|
+
if (input.componentCount === 0) failures.push("No component CSS produced — check the system file and its includes list");
|
|
2830
|
+
else if (input.assembledCss !== void 0 ? !/@layer\s+anm-base\s*\{/.test(input.assembledCss) : input.componentCss.trim().length === 0) failures.push(`${input.componentCount} component(s) discovered but the emitted component CSS is empty`);
|
|
2831
|
+
for (const { specifier, outcome } of input.externalOutcomes ?? []) if (outcome === "empty") failures.push(`include '${specifier}' resolved but discovered no component sources`);
|
|
2832
|
+
else if (outcome === "unresolvable") failures.push(`include '${specifier}' could not be resolved`);
|
|
2833
|
+
if (!input.variableCss.includes(":root")) failures.push("No :root variable block found in variable CSS");
|
|
2834
|
+
if ((input.assembledCss ?? `${input.variableCss}\n${input.globalCss}\n${input.componentCss}`).includes("__TRANSFORM__")) failures.push("Unresolved __TRANSFORM__ placeholders found in CSS output");
|
|
2835
|
+
if (input.componentCount > 0 && (input.componentCss.length > 0 || input.assembledCss !== void 0)) {
|
|
2836
|
+
const assembled = input.assembledCss ?? assembleStylesheet({
|
|
2837
|
+
layers: input.layers,
|
|
2838
|
+
variableCss: input.variableCss,
|
|
2839
|
+
globalCss: input.globalCss,
|
|
2840
|
+
componentCss: input.componentCss
|
|
2841
|
+
});
|
|
2842
|
+
const baseIdx = assembled.search(/@layer\s+anm-base\s*\{/);
|
|
2843
|
+
const variantsIdx = assembled.search(/@layer\s+anm-variants\s*\{/);
|
|
2844
|
+
if (baseIdx !== -1 && variantsIdx !== -1 && baseIdx >= variantsIdx) failures.push(`CSS layer ordering violated — @layer anm-base (offset ${baseIdx}) must precede @layer anm-variants (offset ${variantsIdx})`);
|
|
2845
|
+
}
|
|
2846
|
+
return failures;
|
|
2847
|
+
}
|
|
2848
|
+
//#endregion
|
|
2849
|
+
//#region pipeline/dynamic-prop-config.ts
|
|
2850
|
+
function buildDynamicPropConfig(dynamicProps) {
|
|
2851
|
+
const configEntries = {};
|
|
2852
|
+
for (const [propName, meta] of Object.entries(dynamicProps)) {
|
|
2853
|
+
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(", ")}].`);
|
|
2854
|
+
const entry = {
|
|
2855
|
+
varName: meta.varName,
|
|
2856
|
+
slotClass: meta.slotClass
|
|
2857
|
+
};
|
|
2858
|
+
if (meta.property) entry.property = meta.property;
|
|
2859
|
+
if (meta.properties && meta.properties.length > 0) entry.properties = meta.properties;
|
|
2860
|
+
if (meta.transformName) entry.transformName = meta.transformName;
|
|
2861
|
+
if (meta.scaleValues && Object.keys(meta.scaleValues).length > 0) entry.scaleValues = meta.scaleValues;
|
|
2862
|
+
configEntries[propName] = entry;
|
|
2863
|
+
}
|
|
2864
|
+
return configEntries;
|
|
2865
|
+
}
|
|
2866
|
+
//#endregion
|
|
2867
|
+
//#region pipeline/system-props-module.ts
|
|
2868
|
+
/**
|
|
2869
|
+
* Build the source of the runtime system-props module — served as
|
|
2870
|
+
* `virtual:animus/system-props` by the Vite plugin and written as the
|
|
2871
|
+
* session-scoped `system-props.js` artifact by the extraction session
|
|
2872
|
+
* (`@animus-ui/extract/session`). One generator so the runtimes can never
|
|
2873
|
+
* drift.
|
|
2874
|
+
*
|
|
2875
|
+
* Runtime transform functions for dynamic props are not yet supported
|
|
2876
|
+
* (transforms are resolved at extraction time via boa_engine in Rust), so
|
|
2877
|
+
* `transforms` defaults to an empty object literal.
|
|
2878
|
+
*
|
|
2879
|
+
* `dynamicPropConfig` entries carry the CSS property the slot class declares
|
|
2880
|
+
* — the runtime's unit-fallback decision. Their field order and omission
|
|
2881
|
+
* rules belong to buildDynamicPropConfig.
|
|
2882
|
+
*/
|
|
2883
|
+
function buildSystemPropsModule(opts) {
|
|
2884
|
+
const dynamicPropConfig = buildDynamicPropConfig(opts.dynamicProps);
|
|
2885
|
+
return `export const systemPropMap = ${opts.systemPropMapJson};\nexport const systemPropGroups = ${opts.groupRegistryJson};\nexport const dynamicPropConfig = ${JSON.stringify(dynamicPropConfig)};\nexport const transforms = ${opts.transformsSource ?? "{}"};\n`;
|
|
2886
|
+
}
|
|
2887
|
+
//#endregion
|
|
2888
|
+
//#region pipeline/timing-waterfall.ts
|
|
2889
|
+
/**
|
|
2890
|
+
* Rust-side timing phases as reported in `manifest.timing`, in waterfall
|
|
2891
|
+
* order, with their display labels. Mirrors the timing struct in the
|
|
2892
|
+
* extract-v2 crate — a renamed or added Rust phase is updated here once,
|
|
2893
|
+
* not per plugin.
|
|
2894
|
+
*/
|
|
2895
|
+
const RUST_TIMING_PHASES = [
|
|
2896
|
+
["parseAndWalk", "parse+walk"],
|
|
2897
|
+
["importResolution", "imports"],
|
|
2898
|
+
["extensionProvenance", "provenance"],
|
|
2899
|
+
["topologicalSort", "topo-sort"],
|
|
2900
|
+
["chainEvaluation", "chains"],
|
|
2901
|
+
["jsxScanning", "jsx-scan"],
|
|
2902
|
+
["systemPropAggregation", "sys-props"],
|
|
2903
|
+
["usageLedger", "usage"],
|
|
2904
|
+
["reconciliation", "reconcile"],
|
|
2905
|
+
["cssGeneration", "css-gen"],
|
|
2906
|
+
["manifestSerialization", "serialize"]
|
|
2907
|
+
];
|
|
2908
|
+
/**
|
|
2909
|
+
* Format the Rust timing waterfall as log lines. `indent` and `labelWidth`
|
|
2910
|
+
* let each plugin keep its established column layout; the phase table and
|
|
2911
|
+
* the `(N files, M cached)` suffix are shared.
|
|
2912
|
+
*/
|
|
2913
|
+
function formatRustTimingWaterfall(timing, opts) {
|
|
2914
|
+
const lines = [];
|
|
2915
|
+
for (const [key, label] of RUST_TIMING_PHASES) {
|
|
2916
|
+
const ms = timing[key] ?? 0;
|
|
2917
|
+
const pad = " ".repeat(Math.max(0, opts.labelWidth - label.length));
|
|
2918
|
+
const extra = key === "parseAndWalk" ? ` (${timing.fileCount ?? 0} files, ${timing.cacheHits ?? 0} cached)` : "";
|
|
2919
|
+
lines.push(`${opts.indent}${label}${pad}${String(ms).padStart(5)}ms${extra}`);
|
|
2920
|
+
}
|
|
2921
|
+
return lines;
|
|
2922
|
+
}
|
|
2923
|
+
//#endregion
|
|
2924
|
+
//#region pipeline/tsconfig-paths.ts
|
|
2925
|
+
/**
|
|
2926
|
+
* tsconfig `paths` → alias pairs for the Rust `pathAliasesJson` contract
|
|
2927
|
+
* (via `buildPathAliasesJson`). Used by drivers that cannot harvest aliases
|
|
2928
|
+
* from a live bundler config — Turbopack exposes none.
|
|
2929
|
+
*
|
|
2930
|
+
* Semantics (deliberately the TypeScript subset that matters for import
|
|
2931
|
+
* provenance): JSONC tolerated (comments + trailing commas); `extends`
|
|
2932
|
+
* chains followed (string or array; missing parents and cycles skipped);
|
|
2933
|
+
* the NEAREST config declaring `paths` wins wholesale (TS replaces the
|
|
2934
|
+
* object, never merges); targets resolve against the nearest `baseUrl`
|
|
2935
|
+
* (resolved from its declaring config's directory) or, absent one, the
|
|
2936
|
+
* directory of the config declaring `paths`; first target per pattern.
|
|
2937
|
+
* `x/*` patterns become prefix pairs, non-wildcard patterns exact pairs,
|
|
2938
|
+
* bare `*` catch-alls and mid-pattern wildcards are skipped.
|
|
2939
|
+
*/
|
|
2940
|
+
/** String-aware JSONC → JSON: strips // and block comments + trailing commas. */
|
|
2941
|
+
function stripJsonc(text) {
|
|
2942
|
+
let out = "";
|
|
2943
|
+
let inString = false;
|
|
2944
|
+
let inLine = false;
|
|
2945
|
+
let inBlock = false;
|
|
2946
|
+
for (let i = 0; i < text.length; i++) {
|
|
2947
|
+
const ch = text[i];
|
|
2948
|
+
const next = text[i + 1];
|
|
2949
|
+
if (inLine) {
|
|
2950
|
+
if (ch === "\n") {
|
|
2951
|
+
inLine = false;
|
|
2952
|
+
out += ch;
|
|
2953
|
+
}
|
|
2954
|
+
continue;
|
|
2955
|
+
}
|
|
2956
|
+
if (inBlock) {
|
|
2957
|
+
if (ch === "*" && next === "/") {
|
|
2958
|
+
inBlock = false;
|
|
2959
|
+
i++;
|
|
2960
|
+
}
|
|
2961
|
+
continue;
|
|
2962
|
+
}
|
|
2963
|
+
if (inString) {
|
|
2964
|
+
out += ch;
|
|
2965
|
+
if (ch === "\\") {
|
|
2966
|
+
out += next ?? "";
|
|
2967
|
+
i++;
|
|
2968
|
+
} else if (ch === "\"") inString = false;
|
|
2969
|
+
continue;
|
|
2970
|
+
}
|
|
2971
|
+
if (ch === "\"") {
|
|
2972
|
+
inString = true;
|
|
2973
|
+
out += ch;
|
|
2974
|
+
continue;
|
|
2975
|
+
}
|
|
2976
|
+
if (ch === "/" && next === "/") {
|
|
2977
|
+
inLine = true;
|
|
2978
|
+
i++;
|
|
2979
|
+
continue;
|
|
2980
|
+
}
|
|
2981
|
+
if (ch === "/" && next === "*") {
|
|
2982
|
+
inBlock = true;
|
|
2983
|
+
i++;
|
|
2984
|
+
continue;
|
|
2985
|
+
}
|
|
2986
|
+
out += ch;
|
|
2987
|
+
}
|
|
2988
|
+
return out.replace(/,(\s*[}\]])/g, "$1");
|
|
2989
|
+
}
|
|
2990
|
+
function readConfig(path) {
|
|
2991
|
+
let raw;
|
|
2992
|
+
try {
|
|
2993
|
+
raw = readFileSync(path, "utf-8");
|
|
2994
|
+
} catch {
|
|
2995
|
+
return null;
|
|
2996
|
+
}
|
|
2997
|
+
try {
|
|
2998
|
+
const json = JSON.parse(stripJsonc(raw));
|
|
2999
|
+
return {
|
|
3000
|
+
dir: dirname(path),
|
|
3001
|
+
compilerOptions: json.compilerOptions ?? {},
|
|
3002
|
+
extends: json.extends
|
|
3003
|
+
};
|
|
3004
|
+
} catch {
|
|
3005
|
+
return null;
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
function resolveExtendsTarget(specifier, fromDir) {
|
|
3009
|
+
if (specifier.startsWith(".") || isAbsolute(specifier)) {
|
|
3010
|
+
const base = resolve(fromDir, specifier);
|
|
3011
|
+
for (const candidate of [
|
|
3012
|
+
base,
|
|
3013
|
+
`${base}.json`,
|
|
3014
|
+
join(base, "tsconfig.json")
|
|
3015
|
+
]) if (existsSync(candidate)) return candidate;
|
|
3016
|
+
return null;
|
|
3017
|
+
}
|
|
3018
|
+
try {
|
|
3019
|
+
return __require.resolve(specifier.endsWith(".json") ? specifier : `${specifier}/tsconfig.json`, { paths: [fromDir] });
|
|
3020
|
+
} catch {
|
|
3021
|
+
try {
|
|
3022
|
+
return __require.resolve(specifier, { paths: [fromDir] });
|
|
3023
|
+
} catch {
|
|
3024
|
+
return null;
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
/** Child-first flattened extends chain starting at `entryPath`. */
|
|
3029
|
+
function loadChain(entryPath) {
|
|
3030
|
+
const chain = [];
|
|
3031
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3032
|
+
const queue = [entryPath];
|
|
3033
|
+
while (queue.length > 0) {
|
|
3034
|
+
const key = resolve(queue.shift());
|
|
3035
|
+
if (visited.has(key)) continue;
|
|
3036
|
+
visited.add(key);
|
|
3037
|
+
const node = readConfig(key);
|
|
3038
|
+
if (!node) continue;
|
|
3039
|
+
chain.push(node);
|
|
3040
|
+
const parents = Array.isArray(node.extends) ? node.extends : node.extends !== void 0 ? [node.extends] : [];
|
|
3041
|
+
for (const parent of parents) {
|
|
3042
|
+
if (typeof parent !== "string") continue;
|
|
3043
|
+
const resolved = resolveExtendsTarget(parent, node.dir);
|
|
3044
|
+
if (resolved) queue.push(resolved);
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
return chain;
|
|
3048
|
+
}
|
|
3049
|
+
/**
|
|
3050
|
+
* Read the project's tsconfig path aliases as pairs consumable by
|
|
3051
|
+
* `buildPathAliasesJson`. Returns an empty array when no readable tsconfig
|
|
3052
|
+
* (or no usable `paths`) exists.
|
|
3053
|
+
*/
|
|
3054
|
+
function readTsconfigAliasPairs(rootDir) {
|
|
3055
|
+
const chain = loadChain(join(rootDir, "tsconfig.json"));
|
|
3056
|
+
if (chain.length === 0) return [];
|
|
3057
|
+
const pathsOwner = chain.find((node) => node.compilerOptions.paths && typeof node.compilerOptions.paths === "object");
|
|
3058
|
+
if (!pathsOwner) return [];
|
|
3059
|
+
const paths = pathsOwner.compilerOptions.paths;
|
|
3060
|
+
const baseOwner = chain.find((node) => typeof node.compilerOptions.baseUrl === "string");
|
|
3061
|
+
const base = baseOwner ? resolve(baseOwner.dir, baseOwner.compilerOptions.baseUrl) : pathsOwner.dir;
|
|
3062
|
+
const pairs = [];
|
|
3063
|
+
for (const [pattern, targets] of Object.entries(paths)) {
|
|
3064
|
+
const target = Array.isArray(targets) ? targets[0] : void 0;
|
|
3065
|
+
if (typeof target !== "string") continue;
|
|
3066
|
+
const patternStars = pattern.split("*").length - 1;
|
|
3067
|
+
const targetStars = target.split("*").length - 1;
|
|
3068
|
+
if (pattern === "*" || patternStars > 1 || targetStars > 1) continue;
|
|
3069
|
+
if (pattern.endsWith("/*")) {
|
|
3070
|
+
if (!target.endsWith("/*")) continue;
|
|
3071
|
+
pairs.push({
|
|
3072
|
+
pattern: pattern.slice(0, -2),
|
|
3073
|
+
target: resolve(base, target.slice(0, -2)),
|
|
3074
|
+
kind: "prefix"
|
|
3075
|
+
});
|
|
3076
|
+
continue;
|
|
3077
|
+
}
|
|
3078
|
+
if (patternStars > 0 || targetStars > 0) continue;
|
|
3079
|
+
pairs.push({
|
|
3080
|
+
pattern,
|
|
3081
|
+
target: resolve(base, target),
|
|
3082
|
+
kind: "exact"
|
|
3083
|
+
});
|
|
3084
|
+
}
|
|
3085
|
+
return pairs;
|
|
3086
|
+
}
|
|
3087
|
+
//#endregion
|
|
3088
|
+
//#region pipeline/error-diagnostics.ts
|
|
3089
|
+
/**
|
|
3090
|
+
* Throw when the diagnostics carry any `kind: "error"` entry — one
|
|
3091
|
+
* aggregated `Error` naming EVERY offender (component, file, message — one
|
|
3092
|
+
* line per entry, each prefixed `[animus]`). Warning-only diagnostics
|
|
3093
|
+
* (`bail`/`skip`/`warn`, any severity) never trip this gate; their routing
|
|
3094
|
+
* stays with `surfaceManifestDiagnostics`. Callers accept the manifest only
|
|
3095
|
+
* past this point, so no stylesheet from a failing analysis is published.
|
|
3096
|
+
*/
|
|
3097
|
+
function assertNoErrorDiagnostics(diagnostics) {
|
|
3098
|
+
const errors = (diagnostics ?? []).filter((diagnostic) => diagnostic.kind === "error");
|
|
3099
|
+
if (errors.length === 0) return;
|
|
3100
|
+
const lines = [...new Set(errors.map((diagnostic) => `[animus] ${diagnostic.component || "<unknown component>"} (${diagnostic.file || "<unknown file>"}): ${diagnostic.message || "<no message>"}`))];
|
|
3101
|
+
throw new Error(lines.join("\n"));
|
|
3102
|
+
}
|
|
3103
|
+
//#endregion
|
|
3104
|
+
//#region pipeline/replacement-plans.ts
|
|
3105
|
+
const snapshotByManifest = /* @__PURE__ */ new WeakMap();
|
|
3106
|
+
function snapshotFilePlans(manifest) {
|
|
3107
|
+
const memoKey = manifest !== null && typeof manifest === "object" ? manifest : null;
|
|
3108
|
+
if (memoKey) {
|
|
3109
|
+
const cached = snapshotByManifest.get(memoKey);
|
|
3110
|
+
if (cached) return cached;
|
|
3111
|
+
}
|
|
3112
|
+
const entriesByFile = /* @__PURE__ */ new Map();
|
|
3113
|
+
for (const [id, desc] of Object.entries(manifest?.components ?? {})) {
|
|
3114
|
+
const file = desc.file;
|
|
3115
|
+
if (!file) continue;
|
|
3116
|
+
const list = entriesByFile.get(file) ?? [];
|
|
3117
|
+
list.push([id, desc.replacement ?? ""]);
|
|
3118
|
+
entriesByFile.set(file, list);
|
|
3119
|
+
}
|
|
3120
|
+
const snapshot = /* @__PURE__ */ new Map();
|
|
3121
|
+
for (const [file, entries] of entriesByFile) {
|
|
3122
|
+
entries.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
3123
|
+
snapshot.set(file, JSON.stringify(entries));
|
|
3124
|
+
}
|
|
3125
|
+
if (memoKey) snapshotByManifest.set(memoKey, snapshot);
|
|
3126
|
+
return snapshot;
|
|
3127
|
+
}
|
|
3128
|
+
/**
|
|
3129
|
+
* Rel paths whose plan changed between two snapshots: absent→present,
|
|
3130
|
+
* present→absent, and any change to membership or replacement content.
|
|
3131
|
+
* `exclude` drops one path (the changed file itself on the edit path — its
|
|
3132
|
+
* own modules are already in the update set).
|
|
3133
|
+
*/
|
|
3134
|
+
function diffFilePlans(prev, next, opts) {
|
|
3135
|
+
const changed = /* @__PURE__ */ new Set();
|
|
3136
|
+
for (const [file, plan] of next) if (prev.get(file) !== plan) changed.add(file);
|
|
3137
|
+
for (const file of prev.keys()) if (!next.has(file)) changed.add(file);
|
|
3138
|
+
if (opts?.exclude) changed.delete(opts.exclude);
|
|
3139
|
+
return [...changed].sort();
|
|
3140
|
+
}
|
|
3141
|
+
/** Domain prefix keeping the epoch hash disjoint from every other
|
|
3142
|
+
* contentHash use in the pipeline (design D5). */
|
|
3143
|
+
const REPLACEMENT_PLANS_DOMAIN = "animus-replacement-plans-v1\0";
|
|
3144
|
+
/** Section marker for the served-dependency witness. Leads with NUL: every
|
|
3145
|
+
* plan entry ends `…\0` and neither file paths nor plan JSON contain NUL,
|
|
3146
|
+
* so `\0\0` is unreachable from entry serialization alone — a snapshot can
|
|
3147
|
+
* never forge the witness section. */
|
|
3148
|
+
const SERVED_DEPENDENCY_DOMAIN = "\0animus-served-dependency-v1\0";
|
|
3149
|
+
/**
|
|
3150
|
+
* Canonical replacement epoch: contentHash over the domain-prefixed, sorted
|
|
3151
|
+
* canonical serialization of a file-plan snapshot — each entry contributes
|
|
3152
|
+
* `file + '\0' + plan + '\0'` (unambiguous: neither file paths nor the
|
|
3153
|
+
* JSON plan strings contain NUL). Byte-identical plans → identical epoch;
|
|
3154
|
+
* any membership/replacement/absent↔present change moves it.
|
|
3155
|
+
*
|
|
3156
|
+
* `servedDependencyWitness` extends the epoch to content the plans cannot
|
|
3157
|
+
* see but restored modules depend on: transformed modules import the
|
|
3158
|
+
* session's served system-props artifact, so a consumer passes that
|
|
3159
|
+
* artifact's content here and an offline change to it moves the epoch —
|
|
3160
|
+
* invalidating persistent-cache snapshots that reference the old epoch
|
|
3161
|
+
* artifact. Absence and empty-string are distinct values.
|
|
3162
|
+
*
|
|
3163
|
+
* This is THE epoch definition — integrations consume this hash (or the
|
|
3164
|
+
* snapshot diff) and never define their own. Epoch values compare only for
|
|
3165
|
+
* equality (a moved value invalidates), so the serialization is free to
|
|
3166
|
+
* evolve with a domain bump: sessions running different serializations
|
|
3167
|
+
* simply disagree once and rebuild.
|
|
3168
|
+
*/
|
|
3169
|
+
function hashReplacementPlans(snapshot, servedDependencyWitness) {
|
|
3170
|
+
const entries = [...snapshot.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
3171
|
+
let serialized = REPLACEMENT_PLANS_DOMAIN;
|
|
3172
|
+
for (const [file, plan] of entries) serialized += file + "\0" + plan + "\0";
|
|
3173
|
+
if (servedDependencyWitness !== void 0) serialized += SERVED_DEPENDENCY_DOMAIN + servedDependencyWitness;
|
|
3174
|
+
return contentHash(serialized);
|
|
3175
|
+
}
|
|
3176
|
+
//#endregion
|
|
3177
|
+
//#region pipeline/utils.ts
|
|
3178
|
+
/** Convert camelCase CSS property names to kebab-case. */
|
|
3179
|
+
function camelToKebab(str) {
|
|
3180
|
+
return str.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
3181
|
+
}
|
|
3182
|
+
//#endregion
|
|
3183
|
+
//#region pipeline/watch-keys.ts
|
|
3184
|
+
/**
|
|
3185
|
+
* Membership keys for a filesystem path in the system-dependency watch set.
|
|
3186
|
+
*
|
|
3187
|
+
* The loader reports symlink-resolved canonical paths; watcher events carry
|
|
3188
|
+
* lexical absolute paths that may traverse symlinks (bun workspace links) and
|
|
3189
|
+
* — for deletions — may no longer exist, so `realpathSync` on the event path
|
|
3190
|
+
* alone cannot produce the canonical form. Both insertion and lookup go
|
|
3191
|
+
* through this one normalizer: the lexical form plus a canonical form
|
|
3192
|
+
* reconstructed through the nearest existing ancestor.
|
|
3193
|
+
*/
|
|
3194
|
+
function toWatchKeys(input) {
|
|
3195
|
+
const lexical = normalize(input);
|
|
3196
|
+
const canonical = realpathThroughNearestExistingAncestor(lexical);
|
|
3197
|
+
return canonical === lexical ? [lexical] : [lexical, canonical];
|
|
3198
|
+
}
|
|
3199
|
+
/**
|
|
3200
|
+
* Canonicalize a path that may not exist: walk up to the nearest existing
|
|
3201
|
+
* ancestor, realpath it, and re-append the missing suffix. Handles deleted
|
|
3202
|
+
* files under still-existing (possibly symlinked) directories and atomic
|
|
3203
|
+
* unlink/rename saves. Falls back to the input when no ancestor resolves.
|
|
3204
|
+
*
|
|
3205
|
+
* Known limit: a symlink retargeted AFTER the system load canonicalizes to
|
|
3206
|
+
* the new target, which cannot match the recorded (old-target) dependency —
|
|
3207
|
+
* the membership test then fails open (no reset), identical to not having
|
|
3208
|
+
* this reconstruction at all. Retarget sensitivity is a resolution-input
|
|
3209
|
+
* concern, deliberately out of scope for the watch set.
|
|
3210
|
+
*/
|
|
3211
|
+
function realpathThroughNearestExistingAncestor(path) {
|
|
3212
|
+
try {
|
|
3213
|
+
return realpathSync$1(path);
|
|
3214
|
+
} catch {}
|
|
3215
|
+
const suffix = [];
|
|
3216
|
+
let current = path;
|
|
3217
|
+
for (;;) {
|
|
3218
|
+
const parent = dirname$1(current);
|
|
3219
|
+
if (parent === current) return path;
|
|
3220
|
+
suffix.unshift(basename$1(current));
|
|
3221
|
+
current = parent;
|
|
3222
|
+
try {
|
|
3223
|
+
return join$1(realpathSync$1(current), ...suffix);
|
|
3224
|
+
} catch {}
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
//#endregion
|
|
3228
|
+
export { CORE_OPTION_KEYS as $, createSourceIngestor as A, extractSystemFilePackages as B, resolveLightningTargets as C, resolveThroughPathAliases as D, resolveAssetFile as E, ENGINE_TRANSFORM_EXTENSIONS as F, staleDistIncludesMessage as G, firstOwners as H, preprocessMdx as I, createSourceIdentity as J, unresolvableIncludesMessage as K, adaptSvelteSource as L, isAdvisorySourceDiagnostic as M, withoutInvalidOriginals as N, findAssetSpecifiers as O, DEFAULT_EXTENSIONS as P, AnimusConfigError as Q, collectExternalPackageSources as R, postProcessCss as S, enforceExternalTokenContracts as T, isExcludedPackageRelativePath as U, findPackageRoot as V, resolveAbsolutePathSpecifier as W, sharesVolumeRoot as X, isPathWithinRoot as Y, discoverFiles as Z, runProjectAnalysis as _, stripLeadingLayerDeclaration as _t, snapshotFilePlans as a, createExcludeMatcher as at, surfaceManifestDiagnostics as b, formatRustTimingWaterfall as c, KEYFRAMES_EXTERNAL_ENTRY_FAILED as ct, runStructuralSelfCheck as d, RETIRED_ENGINE_MESSAGE as dt, DEFAULT_EXCLUDE as et, loadSystemConfig as f, assertNoRetiredEngineSelection as ft, clearEngineCache as g, assembleStylesheet as gt, buildAnalysisInputs as h, ANIMUS_LAYERS as ht, hashReplacementPlans as i, assertKnownOptionKeys as it, ingestSourceEntries as j, substituteAssetPlaceholders as k, buildSystemPropsModule as l, mergeExternalKeyframes as lt, serializeStaticCss as m, buildAnalyzeProjectArgs as mt, camelToKebab as n, REPLACEABLE_DEFAULT_EXCLUDE as nt, assertNoErrorDiagnostics as o, resolveMode as ot, applyPrefix as p, createV2EngineApi as pt, walkPackageSources as q, diffFilePlans as r, STRUCTURAL_EXCLUDE as rt, readTsconfigAliasPairs as s, KEYFRAMES_EXPORT_COLLISION as st, toWatchKeys as t, DRIVER_NAMESPACES as tt, buildDynamicPropConfig as u, contentHash as ut, applyUnitFallback as v, validateLayerOrder as vt, buildPathAliasesJson as w, unresolvedParentName as x, isUnresolvedParentDrop as y, __require as yt, excludeCollectedPackages as z };
|