@sxl-studio/token-transformer 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.en.md +77 -608
  2. package/README.md +77 -685
  3. package/config/sxl-transform.config.yaml +120 -0
  4. package/dist/cli.d.ts +1 -1
  5. package/dist/cli.js +411 -141
  6. package/dist/cli.js.map +1 -1
  7. package/dist/config/loader.d.ts +6 -0
  8. package/dist/config/loader.js +160 -0
  9. package/dist/config/loader.js.map +1 -0
  10. package/dist/config/schema.d.ts +847 -0
  11. package/dist/config/schema.js +123 -0
  12. package/dist/config/schema.js.map +1 -0
  13. package/dist/core/color-modifiers.d.ts +31 -0
  14. package/dist/core/color-modifiers.js +289 -0
  15. package/dist/core/color-modifiers.js.map +1 -0
  16. package/dist/core/color-parser.d.ts +24 -0
  17. package/dist/core/color-parser.js +281 -0
  18. package/dist/core/color-parser.js.map +1 -0
  19. package/dist/core/debug-report.d.ts +11 -0
  20. package/dist/core/debug-report.js +161 -0
  21. package/dist/core/debug-report.js.map +1 -0
  22. package/dist/core/incremental.d.ts +18 -0
  23. package/dist/core/incremental.js +105 -0
  24. package/dist/core/incremental.js.map +1 -0
  25. package/dist/core/math.d.ts +3 -0
  26. package/dist/core/math.js +261 -0
  27. package/dist/core/math.js.map +1 -0
  28. package/dist/core/parser.d.ts +4 -3
  29. package/dist/core/parser.js +68 -172
  30. package/dist/core/parser.js.map +1 -1
  31. package/dist/core/resolver.d.ts +26 -0
  32. package/dist/core/resolver.js +431 -0
  33. package/dist/core/resolver.js.map +1 -0
  34. package/dist/core/token-loader.d.ts +11 -0
  35. package/dist/core/token-loader.js +380 -0
  36. package/dist/core/token-loader.js.map +1 -0
  37. package/dist/core/token-parser.d.ts +9 -0
  38. package/dist/core/token-parser.js +138 -0
  39. package/dist/core/token-parser.js.map +1 -0
  40. package/dist/core/token-types.d.ts +7 -0
  41. package/dist/core/token-types.js +132 -0
  42. package/dist/core/token-types.js.map +1 -0
  43. package/dist/core/types.d.ts +154 -63
  44. package/dist/core/writer.d.ts +18 -5
  45. package/dist/core/writer.js +545 -91
  46. package/dist/core/writer.js.map +1 -1
  47. package/dist/emit/css.d.ts +2 -0
  48. package/dist/emit/css.js +538 -0
  49. package/dist/emit/css.js.map +1 -0
  50. package/dist/emit/kotlin.d.ts +2 -0
  51. package/dist/emit/kotlin.js +406 -0
  52. package/dist/emit/kotlin.js.map +1 -0
  53. package/dist/emit/shared.d.ts +13 -0
  54. package/dist/emit/shared.js +127 -0
  55. package/dist/emit/shared.js.map +1 -0
  56. package/dist/emit/swift.d.ts +2 -0
  57. package/dist/emit/swift.js +432 -0
  58. package/dist/emit/swift.js.map +1 -0
  59. package/dist/emit/typography.d.ts +17 -0
  60. package/dist/emit/typography.js +132 -0
  61. package/dist/emit/typography.js.map +1 -0
  62. package/dist/emit/xml.d.ts +2 -0
  63. package/dist/emit/xml.js +311 -0
  64. package/dist/emit/xml.js.map +1 -0
  65. package/dist/index.d.ts +15 -6
  66. package/dist/index.js +13 -5
  67. package/dist/index.js.map +1 -1
  68. package/dist/transformers/css.d.ts +1 -1
  69. package/dist/transformers/css.js +13 -482
  70. package/dist/transformers/css.js.map +1 -1
  71. package/dist/transformers/kotlin.d.ts +2 -2
  72. package/dist/transformers/kotlin.js +14 -442
  73. package/dist/transformers/kotlin.js.map +1 -1
  74. package/dist/transformers/swiftui.d.ts +2 -2
  75. package/dist/transformers/swiftui.js +14 -433
  76. package/dist/transformers/swiftui.js.map +1 -1
  77. package/dist/utils/color.d.ts +7 -5
  78. package/dist/utils/color.js +90 -86
  79. package/dist/utils/color.js.map +1 -1
  80. package/dist/utils/dimension.d.ts +8 -5
  81. package/dist/utils/dimension.js +54 -52
  82. package/dist/utils/dimension.js.map +1 -1
  83. package/dist/utils/naming.d.ts +10 -12
  84. package/dist/utils/naming.js +102 -44
  85. package/dist/utils/naming.js.map +1 -1
  86. package/package.json +30 -10
  87. package/config.example.json +0 -45
  88. package/dist/core/loader.d.ts +0 -8
  89. package/dist/core/loader.js +0 -105
  90. package/dist/core/loader.js.map +0 -1
  91. package/dist/transformers/vue3.d.ts +0 -28
  92. package/dist/transformers/vue3.js +0 -534
  93. package/dist/transformers/vue3.js.map +0 -1
@@ -1,124 +1,578 @@
1
- import fs from "node:fs";
1
+ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { loadTokens, groupTokensByFile } from "./loader.js";
4
- import { transformCSS } from "../transformers/css.js";
5
- import { transformSwiftUI } from "../transformers/swiftui.js";
6
- import { transformKotlin } from "../transformers/kotlin.js";
7
- export async function runTransform(config) {
8
- const { tokens, collections, errors: loadErrors } = await loadTokens(config);
9
- const results = [];
10
- for (const [platform, platformConfig] of Object.entries(config.platforms)) {
11
- if (!platformConfig)
12
- continue;
13
- const result = {
14
- platform,
3
+ import { minimatch } from "minimatch";
4
+ import { shouldSkipByType } from "./token-types.js";
5
+ import { applyTokenFilter } from "../emit/shared.js";
6
+ import { emitCss } from "../emit/css.js";
7
+ import { emitSwift } from "../emit/swift.js";
8
+ import { emitKotlin } from "../emit/kotlin.js";
9
+ import { emitXml } from "../emit/xml.js";
10
+ import { loadTokenSets, scanSourceFiles } from "./token-loader.js";
11
+ import { resolveTokens } from "./resolver.js";
12
+ import { buildOutputStateEntries, computeContentHash, computeConfigHash, createTransformState, diffSourceFiles, } from "./incremental.js";
13
+ export async function runTransform(config, runOptions = {}) {
14
+ const diagnostics = [];
15
+ const mode = runOptions.mode ?? "smart";
16
+ const previousState = runOptions.previousState ?? null;
17
+ const configHash = runOptions.configHash ?? computeConfigHash(config);
18
+ const allOutputIds = config.outputs.map((output) => output.id);
19
+ const scannedSourceFiles = await scanSourceFiles(config.source.tokenDir, config.source.include, config.source.exclude);
20
+ const configChanged = !previousState || previousState.configHash !== configHash;
21
+ const sourceDiff = previousState
22
+ ? diffSourceFiles(previousState.sourceFiles, scannedSourceFiles)
23
+ : {
24
+ changed: new Set(Object.keys(scannedSourceFiles)),
25
+ removed: new Set(),
26
+ };
27
+ const missingOutputIds = !modeIsForce(mode) && previousState
28
+ ? await detectMissingOutputIds(previousState.outputs)
29
+ : new Set();
30
+ const fullRebuild = mode === "force" || !previousState || configChanged;
31
+ const fullReason = mode === "force"
32
+ ? "force mode"
33
+ : !previousState
34
+ ? "missing previous state"
35
+ : configChanged
36
+ ? "config changed"
37
+ : "source changes";
38
+ if (!fullRebuild &&
39
+ sourceDiff.changed.size === 0 &&
40
+ sourceDiff.removed.size === 0 &&
41
+ missingOutputIds.size === 0 &&
42
+ previousState) {
43
+ const state = createTransformState({
44
+ configHash,
45
+ sourceFiles: scannedSourceFiles,
46
+ outputs: previousState.outputs,
47
+ });
48
+ return {
15
49
  files: [],
16
- tokenCount: 0,
17
- errors: [...loadErrors],
50
+ removedFiles: [],
51
+ diagnostics,
52
+ tokenSets: [],
53
+ state,
54
+ meta: {
55
+ mode,
56
+ fullRebuild: false,
57
+ reason: "no source changes",
58
+ changedSourceFiles: [],
59
+ removedSourceFiles: [],
60
+ affectedTokenSets: [],
61
+ affectedOutputIds: [],
62
+ skippedOutputCount: allOutputIds.length,
63
+ },
18
64
  };
19
- try {
20
- if (platformConfig.fileMapping && platformConfig.fileMapping.length > 0) {
21
- result.files = buildMappedFiles(tokens, collections, platformConfig, platform);
65
+ }
66
+ const loaded = await loadTokenSets(config);
67
+ diagnostics.push(...loaded.diagnostics);
68
+ const changedFiles = fullRebuild
69
+ ? new Set(Object.keys(loaded.sourceFiles))
70
+ : sourceDiff.changed;
71
+ const removedFiles = fullRebuild
72
+ ? new Set(Object.keys(previousState?.sourceFiles ?? {}).filter((filePath) => !(filePath in loaded.sourceFiles)))
73
+ : sourceDiff.removed;
74
+ const affectedTokenSets = new Set();
75
+ if (fullRebuild) {
76
+ for (const tokenSet of loaded.tokenSets)
77
+ affectedTokenSets.add(tokenSet.id);
78
+ }
79
+ else {
80
+ for (const tokenSet of loaded.tokenSets) {
81
+ const files = collectTokenSetSourceFiles(tokenSet);
82
+ if (intersects(files, changedFiles) || intersects(files, removedFiles)) {
83
+ affectedTokenSets.add(tokenSet.id);
84
+ }
85
+ }
86
+ }
87
+ const affectedOutputIds = resolveAffectedOutputIds(config, affectedTokenSets, changedFiles, removedFiles, missingOutputIds, previousState, fullRebuild);
88
+ const shouldProcessOutput = (outputId) => {
89
+ if (fullRebuild)
90
+ return true;
91
+ return affectedOutputIds.has(outputId);
92
+ };
93
+ const tokenSetIdsToResolve = new Set();
94
+ if (fullRebuild) {
95
+ for (const tokenSet of loaded.tokenSets)
96
+ tokenSetIdsToResolve.add(tokenSet.id);
97
+ }
98
+ else {
99
+ for (const output of config.outputs) {
100
+ if (!shouldProcessOutput(output.id))
101
+ continue;
102
+ for (const mapping of output.files)
103
+ tokenSetIdsToResolve.add(mapping.tokenSet);
104
+ }
105
+ }
106
+ const resolvedTokenSets = loaded.tokenSets.map((tokenSet) => {
107
+ if (!tokenSetIdsToResolve.has(tokenSet.id)) {
108
+ return tokenSet;
109
+ }
110
+ const cleanTokens = filterUnsupportedTokens(tokenSet.tokens, config, diagnostics, tokenSet.id);
111
+ const resolverScope = tokenSet.resolverTokens.length > 0 ? tokenSet.resolverTokens : tokenSet.tokens;
112
+ const resolved = resolveTokens(cleanTokens, {
113
+ remBase: config.options.remBase,
114
+ maxDepth: config.options.maxAliasDepth,
115
+ unresolvedAlias: tokenSet.unresolvedAliases,
116
+ autoFixAliasSyntax: runOptions.autoFixAliasSyntax ?? false,
117
+ skipTokensWithIssues: runOptions.skipTokensWithIssues ?? false,
118
+ }, {
119
+ lookupTokens: resolverScope,
120
+ });
121
+ diagnostics.push(...resolved.diagnostics);
122
+ return {
123
+ ...tokenSet,
124
+ tokens: resolved.tokens,
125
+ };
126
+ });
127
+ const tokenSetMap = new Map(resolvedTokenSets.map((item) => [item.id, item]));
128
+ const generatedFiles = [];
129
+ for (const output of config.outputs) {
130
+ if (!shouldProcessOutput(output.id))
131
+ continue;
132
+ for (const mapping of output.files) {
133
+ const tokenSet = tokenSetMap.get(mapping.tokenSet);
134
+ if (!tokenSet) {
135
+ diagnostics.push({
136
+ level: "error",
137
+ code: "OUTPUT_TOKEN_SET_MISSING",
138
+ message: `Output "${output.id}" references missing tokenSet "${mapping.tokenSet}"`,
139
+ outputId: output.id,
140
+ });
141
+ continue;
142
+ }
143
+ const filtered = applyTokenFilter(tokenSet.tokens, mapping.filter);
144
+ if (filtered.length === 0)
145
+ continue;
146
+ if (mapping.splitBySourceFile) {
147
+ const grouped = groupTokensBySourceFile(filtered, mapping.splitBySourceFile.include, mapping.splitBySourceFile.exclude);
148
+ if (grouped.size === 0) {
149
+ diagnostics.push({
150
+ level: "warn",
151
+ code: "SPLIT_BY_SOURCE_EMPTY",
152
+ message: `Output "${output.id}" splitBySourceFile produced no groups for tokenSet "${mapping.tokenSet}"`,
153
+ outputId: output.id,
154
+ });
155
+ continue;
156
+ }
157
+ for (const [sourceFile, sourceTokens] of grouped) {
158
+ const context = buildSplitContext(sourceFile);
159
+ const renderedOutput = renderTemplate(mapping.splitBySourceFile.outputPattern, context);
160
+ if (renderedOutput.missing.length > 0) {
161
+ diagnostics.push({
162
+ level: "error",
163
+ code: "SPLIT_TEMPLATE_MISSING_FIELD",
164
+ message: `Output "${output.id}" has unknown placeholders in split outputPattern: ` +
165
+ renderedOutput.missing.join(", "),
166
+ outputId: output.id,
167
+ });
168
+ continue;
169
+ }
170
+ const relativeOutputRaw = mapping.output
171
+ ? path.posix.join(mapping.output, renderedOutput.value)
172
+ : renderedOutput.value;
173
+ const relativeOutput = normalizeRelativeOutput(relativeOutputRaw);
174
+ if (!relativeOutput) {
175
+ diagnostics.push({
176
+ level: "error",
177
+ code: "SPLIT_OUTPUT_PATH_INVALID",
178
+ message: `Output "${output.id}" resolved invalid split output path "${relativeOutputRaw}"`,
179
+ outputId: output.id,
180
+ });
181
+ continue;
182
+ }
183
+ const splitPrefix = resolveSplitPrefix(mapping, context, diagnostics, output.id);
184
+ const prefix = resolveOutputPrefix(output, mapping, splitPrefix);
185
+ emitMappingFiles(output, relativeOutput, sourceTokens, prefix, config, diagnostics, generatedFiles, mapping.tokenSet);
186
+ }
187
+ continue;
22
188
  }
23
- else {
24
- result.files = buildAutoFiles(tokens, collections, platformConfig, platform);
189
+ if (!mapping.output) {
190
+ diagnostics.push({
191
+ level: "error",
192
+ code: "OUTPUT_MAPPING_MISSING_OUTPUT",
193
+ message: `Output "${output.id}" mapping for tokenSet "${mapping.tokenSet}" has no output path`,
194
+ outputId: output.id,
195
+ });
196
+ continue;
25
197
  }
26
- result.tokenCount = result.files.reduce((sum, f) => sum + f.tokenCount, 0);
198
+ const relativeOutput = normalizeRelativeOutput(mapping.output);
199
+ if (!relativeOutput) {
200
+ diagnostics.push({
201
+ level: "error",
202
+ code: "OUTPUT_PATH_INVALID",
203
+ message: `Output "${output.id}" has invalid output path "${mapping.output}"`,
204
+ outputId: output.id,
205
+ });
206
+ continue;
207
+ }
208
+ const prefix = resolveOutputPrefix(output, mapping);
209
+ emitMappingFiles(output, relativeOutput, filtered, prefix, config, diagnostics, generatedFiles, mapping.tokenSet);
27
210
  }
28
- catch (err) {
29
- result.errors.push(`Transform error (${platform}): ${err}`);
211
+ }
212
+ const generatedEntries = buildOutputStateEntries(generatedFiles);
213
+ const staleOutputFiles = collectStaleOutputFiles(previousState, generatedEntries, affectedOutputIds, fullRebuild);
214
+ const filesToWrite = filterFilesToWrite(generatedFiles, fullRebuild ? null : previousState, missingOutputIds);
215
+ const mergedOutputState = mergeOutputState(previousState, generatedEntries, affectedOutputIds, fullRebuild);
216
+ const state = createTransformState({
217
+ configHash,
218
+ sourceFiles: loaded.sourceFiles,
219
+ outputs: mergedOutputState,
220
+ });
221
+ return {
222
+ files: filesToWrite,
223
+ removedFiles: staleOutputFiles,
224
+ diagnostics,
225
+ tokenSets: resolvedTokenSets,
226
+ state,
227
+ meta: {
228
+ mode,
229
+ fullRebuild,
230
+ reason: fullRebuild ? fullReason : "affected outputs only",
231
+ changedSourceFiles: [...changedFiles].sort(),
232
+ removedSourceFiles: [...removedFiles].sort(),
233
+ affectedTokenSets: [...affectedTokenSets].sort(),
234
+ affectedOutputIds: [...affectedOutputIds].sort(),
235
+ skippedOutputCount: config.outputs.filter((output) => !shouldProcessOutput(output.id)).length,
236
+ },
237
+ };
238
+ }
239
+ export async function writeOutputFiles(files, removedFiles = []) {
240
+ const diagnostics = [];
241
+ let written = 0;
242
+ let removed = 0;
243
+ for (const filePath of [...new Set(removedFiles)]) {
244
+ try {
245
+ await fs.rm(filePath, { force: true });
246
+ removed += 1;
247
+ }
248
+ catch (error) {
249
+ diagnostics.push({
250
+ level: "error",
251
+ code: "OUTPUT_REMOVE_ERROR",
252
+ message: `Failed to remove stale output ${filePath}: ${String(error)}`,
253
+ });
254
+ }
255
+ }
256
+ for (const file of files) {
257
+ try {
258
+ await fs.mkdir(path.dirname(file.path), { recursive: true });
259
+ await fs.writeFile(file.path, file.content, "utf-8");
260
+ written += 1;
261
+ }
262
+ catch (error) {
263
+ diagnostics.push({
264
+ level: "error",
265
+ code: "OUTPUT_WRITE_ERROR",
266
+ message: `Failed to write ${file.path}: ${String(error)}`,
267
+ });
30
268
  }
31
- results.push(result);
32
269
  }
33
- return results;
270
+ return { written, removed, diagnostics };
34
271
  }
35
- function buildMappedFiles(allTokens, _collections, config, platform) {
36
- const files = [];
37
- for (const mapping of config.fileMapping) {
38
- const filtered = filterTokens(allTokens, mapping);
39
- if (filtered.length === 0)
40
- continue;
41
- const content = transformTokens(filtered, config, platform, allTokens);
272
+ function defaultResolveAliases(platform) {
273
+ return platform !== "css";
274
+ }
275
+ function resolveOutputPrefix(output, mapping, splitPrefix) {
276
+ if (Object.prototype.hasOwnProperty.call(mapping, "prefix")) {
277
+ return mapping.prefix ?? undefined;
278
+ }
279
+ if (splitPrefix !== undefined)
280
+ return splitPrefix;
281
+ return output.prefix ?? undefined;
282
+ }
283
+ function emitMappingFiles(output, relativeOutput, tokens, prefix, config, diagnostics, files, tokenSetId) {
284
+ const emitInput = {
285
+ tokens,
286
+ platform: output.platform,
287
+ prefix,
288
+ resolveAliases: output.resolveAliases ?? defaultResolveAliases(output.platform),
289
+ splitEffects: output.splitEffects ?? true,
290
+ showDescriptions: output.showDescriptions ?? true,
291
+ collisions: config.options.collisionStrategy,
292
+ outputId: output.id,
293
+ remBase: config.options.remBase,
294
+ };
295
+ const sourceFiles = collectTokenSourceFiles(tokens);
296
+ if (output.platform === "css") {
297
+ const emitted = emitCss(emitInput);
298
+ diagnostics.push(...emitted.diagnostics);
42
299
  files.push({
43
- path: path.join(config.outputDir, mapping.output),
44
- content,
45
- tokenCount: filtered.length,
300
+ path: path.join(output.outputDir, relativeOutput),
301
+ content: emitted.content,
302
+ tokenCount: tokens.length,
303
+ outputId: output.id,
304
+ tokenSetId,
305
+ sourceFiles,
46
306
  });
307
+ return;
47
308
  }
48
- return files;
49
- }
50
- function buildAutoFiles(allTokens, collections, config, platform) {
51
- const files = [];
52
- const groups = groupTokensByFile(allTokens, collections);
53
- const ext = platformExtension(platform);
54
- for (const [groupKey, tokens] of groups) {
55
- const sanitized = groupKey
56
- .replace(/\.json$/, "")
57
- .replace(/[/\\]/g, path.sep);
58
- const filePath = path.join(config.outputDir, `${sanitized}${ext}`);
59
- const content = transformTokens(tokens, config, platform, allTokens);
309
+ if (output.platform === "swift") {
310
+ const emitted = emitSwift(emitInput);
311
+ diagnostics.push(...emitted.diagnostics);
312
+ files.push({
313
+ path: path.join(output.outputDir, relativeOutput),
314
+ content: emitted.content,
315
+ tokenCount: tokens.length,
316
+ outputId: output.id,
317
+ tokenSetId,
318
+ sourceFiles,
319
+ });
320
+ return;
321
+ }
322
+ if (output.platform === "kotlin") {
323
+ const emitted = emitKotlin(emitInput);
324
+ diagnostics.push(...emitted.diagnostics);
60
325
  files.push({
61
- path: filePath,
62
- content,
326
+ path: path.join(output.outputDir, relativeOutput),
327
+ content: emitted.content,
63
328
  tokenCount: tokens.length,
329
+ outputId: output.id,
330
+ tokenSetId,
331
+ sourceFiles,
64
332
  });
333
+ return;
334
+ }
335
+ const emitted = emitXml(emitInput, relativeOutput);
336
+ diagnostics.push(...emitted.diagnostics);
337
+ const xmlFiles = emitted.extraFiles ?? [];
338
+ for (const xmlFile of xmlFiles) {
339
+ files.push({
340
+ path: path.join(output.outputDir, xmlFile.relativePath),
341
+ content: xmlFile.content,
342
+ tokenCount: xmlFile.tokenCount ?? tokens.length,
343
+ outputId: output.id,
344
+ tokenSetId,
345
+ sourceFiles,
346
+ });
347
+ }
348
+ }
349
+ function groupTokensBySourceFile(tokens, include, exclude) {
350
+ const grouped = new Map();
351
+ for (const token of tokens) {
352
+ const sourceFile = normalizeSourceFile(token.sourceFile);
353
+ const included = include.length === 0 || include.some((pattern) => minimatch(sourceFile, pattern, { dot: true }));
354
+ const excluded = exclude.some((pattern) => minimatch(sourceFile, pattern, { dot: true }));
355
+ if (!included || excluded)
356
+ continue;
357
+ const list = grouped.get(sourceFile) ?? [];
358
+ list.push(token);
359
+ grouped.set(sourceFile, list);
360
+ }
361
+ return new Map([...grouped.entries()].sort(([a], [b]) => a.localeCompare(b)));
362
+ }
363
+ function normalizeSourceFile(filePath) {
364
+ return filePath.replace(/\\/g, "/").replace(/^\.\//, "");
365
+ }
366
+ function buildSplitContext(sourceFile) {
367
+ const normalized = normalizeSourceFile(sourceFile);
368
+ const sourceDir = path.posix.dirname(normalized);
369
+ const sourceBase = path.posix.basename(normalized);
370
+ const sourceName = sourceBase.replace(/\.[^.]+$/u, "");
371
+ const sourceStem = sourceName.replace(/\.style$/iu, "");
372
+ const segments = normalized.split("/").filter(Boolean);
373
+ const componentsIndex = segments.indexOf("components");
374
+ const component = componentsIndex >= 0 && segments.length > componentsIndex + 1
375
+ ? segments[componentsIndex + 1]
376
+ : sourceStem;
377
+ return {
378
+ sourceFile: normalized,
379
+ sourceDir: sourceDir === "." ? "" : sourceDir,
380
+ sourceBase,
381
+ sourceName,
382
+ sourceStem,
383
+ component,
384
+ fileName: sourceStem,
385
+ };
386
+ }
387
+ function resolveSplitPrefix(mapping, context, diagnostics, outputId) {
388
+ if (!mapping.splitBySourceFile?.prefixPattern)
389
+ return undefined;
390
+ const rendered = renderTemplate(mapping.splitBySourceFile.prefixPattern, context);
391
+ if (rendered.missing.length > 0) {
392
+ diagnostics.push({
393
+ level: "error",
394
+ code: "SPLIT_PREFIX_TEMPLATE_MISSING_FIELD",
395
+ message: `Output "${outputId}" has unknown placeholders in split prefixPattern: ` +
396
+ rendered.missing.join(", "),
397
+ outputId,
398
+ });
399
+ return undefined;
400
+ }
401
+ const value = rendered.value.trim();
402
+ return value.length > 0 ? value : undefined;
403
+ }
404
+ function renderTemplate(template, context) {
405
+ const missing = new Set();
406
+ const value = template.replace(/\{([a-zA-Z0-9_]+)\}/g, (_full, rawKey) => {
407
+ const key = rawKey.trim();
408
+ if (!(key in context)) {
409
+ missing.add(key);
410
+ return "";
411
+ }
412
+ return context[key];
413
+ });
414
+ return {
415
+ value,
416
+ missing: [...missing],
417
+ };
418
+ }
419
+ function normalizeRelativeOutput(value) {
420
+ const normalized = path.posix.normalize(value.replace(/\\/g, "/")).replace(/^(\.\/)+/, "");
421
+ if (!normalized || normalized === "." || path.posix.isAbsolute(normalized))
422
+ return null;
423
+ if (normalized.split("/").some((segment) => segment === ".."))
424
+ return null;
425
+ return normalized;
426
+ }
427
+ function filterUnsupportedTokens(tokens, config, diagnostics, tokenSetId) {
428
+ const kept = [];
429
+ for (const token of tokens) {
430
+ const normalized = token.type.trim();
431
+ const hasExplicitRule = config.options.unsupportedTypes.types[normalized] !== undefined ||
432
+ config.options.unsupportedTypes.types[normalized.toLowerCase()] !== undefined;
433
+ const action = config.options.unsupportedTypes.types[normalized] ??
434
+ config.options.unsupportedTypes.types[normalized.toLowerCase()] ??
435
+ config.options.unsupportedTypes.default;
436
+ const implicitSkip = shouldSkipByType(normalized) && !hasExplicitRule;
437
+ if (implicitSkip || (hasExplicitRule && action === "skip")) {
438
+ if (action === "warn") {
439
+ diagnostics.push({
440
+ level: "warn",
441
+ code: "UNSUPPORTED_TOKEN_TYPE",
442
+ message: `Skipped token type ${normalized} in tokenSet ${tokenSetId}`,
443
+ tokenPath: token.path,
444
+ });
445
+ }
446
+ continue;
447
+ }
448
+ if (hasExplicitRule && action === "error") {
449
+ diagnostics.push({
450
+ level: "error",
451
+ code: "UNSUPPORTED_TOKEN_TYPE",
452
+ message: `Blocked token type ${normalized} in tokenSet ${tokenSetId}`,
453
+ tokenPath: token.path,
454
+ });
455
+ continue;
456
+ }
457
+ kept.push(token);
458
+ }
459
+ return kept;
460
+ }
461
+ function collectTokenSourceFiles(tokens) {
462
+ return [...new Set(tokens.map((token) => normalizeSourceFile(token.sourceFile)))].sort();
463
+ }
464
+ function collectTokenSetSourceFiles(tokenSet) {
465
+ const files = new Set();
466
+ for (const selector of tokenSet.selectors) {
467
+ for (const filePath of selector.files)
468
+ files.add(normalizeSourceFile(filePath));
469
+ for (const filePath of selector.resolverFiles)
470
+ files.add(normalizeSourceFile(filePath));
65
471
  }
66
472
  return files;
67
473
  }
68
- function filterTokens(tokens, mapping) {
69
- let filtered = tokens;
70
- if (mapping.sources.length > 0) {
71
- filtered = filtered.filter(t => mapping.sources.some(s => {
72
- if (s.includes("*")) {
73
- const regex = new RegExp("^" + s.replace(/\*/g, ".*") + "$");
74
- return regex.test(t.sourceFile || "");
474
+ function intersects(left, right) {
475
+ if (left.size === 0 || right.size === 0)
476
+ return false;
477
+ const [small, large] = left.size <= right.size ? [left, right] : [right, left];
478
+ for (const value of small) {
479
+ if (large.has(value))
480
+ return true;
481
+ }
482
+ return false;
483
+ }
484
+ function resolveAffectedOutputIds(config, affectedTokenSets, changedFiles, removedFiles, missingOutputIds, previousState, fullRebuild) {
485
+ const affected = new Set();
486
+ if (fullRebuild) {
487
+ for (const output of config.outputs)
488
+ affected.add(output.id);
489
+ return affected;
490
+ }
491
+ for (const output of config.outputs) {
492
+ for (const mapping of output.files) {
493
+ if (affectedTokenSets.has(mapping.tokenSet)) {
494
+ affected.add(output.id);
495
+ break;
75
496
  }
76
- return (t.sourceFile || "") === s;
77
- }));
497
+ }
498
+ }
499
+ if (!previousState)
500
+ return affected;
501
+ for (const outputId of missingOutputIds) {
502
+ affected.add(outputId);
78
503
  }
79
- if (mapping.filter) {
80
- if (mapping.filter.types && mapping.filter.types.length > 0) {
81
- filtered = filtered.filter(t => mapping.filter.types.includes(t.type));
504
+ for (const item of Object.values(previousState.outputs)) {
505
+ const sourceSet = new Set(item.sourceFiles.map((filePath) => normalizeSourceFile(filePath)));
506
+ if (intersects(sourceSet, changedFiles) || intersects(sourceSet, removedFiles)) {
507
+ affected.add(item.outputId);
82
508
  }
83
- if (mapping.filter.paths && mapping.filter.paths.length > 0) {
84
- filtered = filtered.filter(t => mapping.filter.paths.some(p => t.path.startsWith(p)));
509
+ }
510
+ return affected;
511
+ }
512
+ function modeIsForce(mode) {
513
+ return mode === "force";
514
+ }
515
+ async function detectMissingOutputIds(outputs) {
516
+ const missing = new Set();
517
+ for (const item of Object.values(outputs)) {
518
+ try {
519
+ await fs.stat(item.path);
85
520
  }
86
- if (mapping.filter.excludePaths && mapping.filter.excludePaths.length > 0) {
87
- filtered = filtered.filter(t => !mapping.filter.excludePaths.some(p => t.path.startsWith(p)));
521
+ catch {
522
+ missing.add(item.outputId);
88
523
  }
89
524
  }
90
- return filtered;
525
+ return missing;
91
526
  }
92
- function transformTokens(tokens, config, platform, allTokens) {
93
- switch (platform) {
94
- case "css": return transformCSS(tokens, config);
95
- case "swiftui": return transformSwiftUI(tokens, config, allTokens);
96
- case "kotlin": return transformKotlin(tokens, config, allTokens);
527
+ function collectStaleOutputFiles(previousState, generatedEntries, affectedOutputIds, fullRebuild) {
528
+ if (!previousState)
529
+ return [];
530
+ const stale = [];
531
+ for (const [filePath, state] of Object.entries(previousState.outputs)) {
532
+ const shouldCheck = fullRebuild || affectedOutputIds.has(state.outputId);
533
+ if (!shouldCheck)
534
+ continue;
535
+ if (generatedEntries[filePath])
536
+ continue;
537
+ stale.push(filePath);
97
538
  }
539
+ return stale.sort();
98
540
  }
99
- function platformExtension(platform) {
100
- switch (platform) {
101
- case "css": return ".css";
102
- case "swiftui": return ".swift";
103
- case "kotlin": return ".kt";
541
+ function mergeOutputState(previousState, generatedEntries, affectedOutputIds, fullRebuild) {
542
+ if (!previousState || fullRebuild) {
543
+ return generatedEntries;
104
544
  }
545
+ const next = {};
546
+ for (const [filePath, state] of Object.entries(previousState.outputs)) {
547
+ if (affectedOutputIds.has(state.outputId))
548
+ continue;
549
+ next[filePath] = state;
550
+ }
551
+ for (const [filePath, state] of Object.entries(generatedEntries)) {
552
+ next[filePath] = state;
553
+ }
554
+ return next;
105
555
  }
106
- export function writeOutputFiles(results) {
107
- let written = 0;
108
- const errors = [];
109
- for (const result of results) {
110
- for (const file of result.files) {
111
- try {
112
- const dir = path.dirname(file.path);
113
- fs.mkdirSync(dir, { recursive: true });
114
- fs.writeFileSync(file.path, file.content, "utf-8");
115
- written++;
116
- }
117
- catch (err) {
118
- errors.push(`Failed to write ${file.path}: ${err}`);
119
- }
556
+ function filterFilesToWrite(files, previousState, missingOutputIds) {
557
+ if (!previousState)
558
+ return files;
559
+ const filtered = [];
560
+ const previousEntries = previousState.outputs;
561
+ for (const file of files) {
562
+ if (file.outputId && missingOutputIds.has(file.outputId)) {
563
+ filtered.push(file);
564
+ continue;
565
+ }
566
+ const previous = previousEntries[file.path];
567
+ if (!previous) {
568
+ filtered.push(file);
569
+ continue;
570
+ }
571
+ const nextHash = computeContentHash(file.content);
572
+ if (previous.contentHash !== nextHash) {
573
+ filtered.push(file);
120
574
  }
121
575
  }
122
- return { written, errors };
576
+ return filtered;
123
577
  }
124
578
  //# sourceMappingURL=writer.js.map