@yahoo/uds 3.162.0 → 3.162.2

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 (51) hide show
  1. package/dist/cli/commands/purge.cjs +8 -1
  2. package/dist/cli/commands/purge.helpers.cjs +7 -0
  3. package/dist/cli/commands/purge.helpers.d.cts +3 -0
  4. package/dist/cli/commands/purge.helpers.d.ts +3 -0
  5. package/dist/cli/commands/purge.helpers.js +7 -0
  6. package/dist/cli/commands/purge.js +8 -1
  7. package/dist/components/client/Menu/Menu.ItemCheckbox.d.cts +1 -1
  8. package/dist/components/client/Menu/Menu.ItemCheckbox.d.ts +1 -1
  9. package/dist/components/client/Popover/UDSPopoverConfigProvider.d.cts +1 -1
  10. package/dist/components/client/Popover/UDSPopoverConfigProvider.d.ts +1 -1
  11. package/dist/components/client/Switch/Switch.cjs +5 -2
  12. package/dist/components/client/Switch/Switch.js +5 -2
  13. package/dist/components/client/Switch/switchStyles.cjs +2 -2
  14. package/dist/components/client/Switch/switchStyles.js +2 -2
  15. package/dist/css/dist/commands/css.cjs +2 -0
  16. package/dist/css/dist/commands/css.helpers.cjs +2 -0
  17. package/dist/css/dist/commands/css.helpers.js +2 -0
  18. package/dist/css/dist/commands/css.js +2 -0
  19. package/dist/css/dist/css/nodeUtils.cjs +28 -11
  20. package/dist/css/dist/css/nodeUtils.js +29 -12
  21. package/dist/css/dist/css/runner.cjs +8 -3
  22. package/dist/css/dist/css/runner.js +8 -3
  23. package/dist/css/dist/css/theme.d.cts +16 -0
  24. package/dist/css/dist/css/theme.d.ts +16 -0
  25. package/dist/css/dist/css/workerPool.cjs +2 -1
  26. package/dist/css/dist/css/workerPool.js +2 -1
  27. package/dist/css/dist/purger/optimized/ast/expressions.cjs +104 -11
  28. package/dist/css/dist/purger/optimized/ast/expressions.js +104 -11
  29. package/dist/css/dist/purger/optimized/ast/jsx.cjs +74 -0
  30. package/dist/css/dist/purger/optimized/ast/jsx.js +73 -0
  31. package/dist/css/dist/purger/optimized/ast/props.cjs +47 -0
  32. package/dist/css/dist/purger/optimized/ast/props.js +46 -0
  33. package/dist/css/dist/purger/optimized/ast/spread.cjs +87 -0
  34. package/dist/css/dist/purger/optimized/ast/spread.js +86 -0
  35. package/dist/css/dist/purger/optimized/purge.cjs +14 -8
  36. package/dist/css/dist/purger/optimized/purge.js +15 -9
  37. package/dist/css/dist/purger/optimized/purgeFromCode.cjs +176 -18
  38. package/dist/css/dist/purger/optimized/purgeFromCode.js +176 -19
  39. package/dist/css/dist/purger/optimized/utils/files.cjs +22 -2
  40. package/dist/css/dist/purger/optimized/utils/files.js +21 -3
  41. package/dist/hooks/useRtl.cjs +4 -8
  42. package/dist/hooks/useRtl.js +4 -8
  43. package/dist/styles/styler.d.cts +25 -25
  44. package/dist/styles/styler.d.ts +25 -25
  45. package/dist/uds/generated/componentData.cjs +2555 -2553
  46. package/dist/uds/generated/componentData.js +2555 -2553
  47. package/dist/uds/generated/tailwindPurge.cjs +1 -1
  48. package/dist/uds/generated/tailwindPurge.js +1 -1
  49. package/generated/componentData.json +3340 -3338
  50. package/generated/tailwindPurge.ts +1 -1
  51. package/package.json +1 -1
@@ -0,0 +1,87 @@
1
+ /*! © 2026 Yahoo, Inc. UDS v0.0.0-development */
2
+ require("../../../../../_virtual/_rolldown/runtime.cjs");
3
+ const require_jsx = require("./jsx.cjs");
4
+ const require_props = require("./props.cjs");
5
+ let ts_morph = require("ts-morph");
6
+ //#region ../css/dist/purger/optimized/ast/spread.mjs
7
+ /*! © 2026 Yahoo, Inc. UDS CSS v0.0.0-development */
8
+ /**
9
+ * Find all usages of a component across the entire project.
10
+ * This is the key function for spread resolution - when we encounter
11
+ * {...props} on a UDS component inside a wrapper, we trace back to
12
+ * find all places where the wrapper is used and extract actual values.
13
+ *
14
+ * @param componentName Name of the component to find usages of
15
+ * @param project The ts-morph project
16
+ * @param cache Optional cache to avoid re-scanning
17
+ * @returns Array of component usages with their props
18
+ */
19
+ const findComponentUsages = (componentName, project, cache) => {
20
+ if (cache?.has(componentName)) return cache.get(componentName);
21
+ const usages = [];
22
+ project.getSourceFiles().forEach((sourceFile) => {
23
+ if (!fileReferencesComponent(sourceFile, componentName)) return;
24
+ [...sourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.JsxSelfClosingElement), ...sourceFile.getDescendantsOfKind(ts_morph.SyntaxKind.JsxOpeningElement)].filter((el) => el.getTagNameNode().getText() === componentName).forEach((element) => {
25
+ const props = require_props.extractExplicitProps(element);
26
+ usages.push({
27
+ componentName,
28
+ filePath: sourceFile.getFilePath(),
29
+ line: element.getStartLineNumber(),
30
+ props
31
+ });
32
+ });
33
+ });
34
+ if (cache) cache.set(componentName, usages);
35
+ return usages;
36
+ };
37
+ /**
38
+ * Check if a source file references a component (by import or definition)
39
+ */
40
+ const fileReferencesComponent = (sourceFile, componentName) => {
41
+ if (sourceFile.getImportDeclarations().some((imp) => imp.getNamedImports().some((named) => named.getName() === componentName))) return true;
42
+ if (sourceFile.getVariableDeclaration(componentName)) return true;
43
+ if (sourceFile.getFunction(componentName)) return true;
44
+ return false;
45
+ };
46
+ /**
47
+ * Resolve spread props by tracing component usage.
48
+ *
49
+ * When we encounter {...props} on a UDS component inside a wrapper:
50
+ * 1. Find the parent component that contains this spread
51
+ * 2. Search ALL files for usages of that parent component
52
+ * 3. Extract actual prop values passed at each usage site
53
+ * 4. Return ONLY those values - never fall back to type properties
54
+ *
55
+ * @param element The JSX element with the spread
56
+ * @param spreadIdentifier The identifier being spread (e.g., 'rest' from {...rest})
57
+ * @param context The purge context for caching
58
+ * @returns Map of prop names to their resolved values, or undefined if couldn't trace
59
+ */
60
+ const resolveSpreadFromUsages = (element, spreadIdentifier, context) => {
61
+ const parentInfo = require_jsx.getParentComponentInfo(element);
62
+ if (!parentInfo) return;
63
+ if (!(parentInfo.spreadRestIdentifier === spreadIdentifier || parentInfo.paramIdentifier === spreadIdentifier)) return;
64
+ const usages = findComponentUsages(parentInfo.componentName, context.project, context.componentUsageCache);
65
+ if (usages.length === 0) return;
66
+ context.stats.spreadsTraced++;
67
+ const resolvedProps = /* @__PURE__ */ new Map();
68
+ usages.forEach((usage) => {
69
+ usage.props.forEach((prop) => {
70
+ if (prop.fromSpread) return;
71
+ const existing = resolvedProps.get(prop.name) ?? [];
72
+ prop.values.forEach((value) => {
73
+ if (!existing.includes(value)) existing.push(value);
74
+ });
75
+ resolvedProps.set(prop.name, existing);
76
+ });
77
+ });
78
+ const cacheKey = `${element.getSourceFile().getFilePath()}:${element.getStartLineNumber()}:${spreadIdentifier}`;
79
+ context.spreadCache.set(cacheKey, Array.from(resolvedProps).map(([name, values]) => ({
80
+ name,
81
+ values,
82
+ fromSpread: true
83
+ })));
84
+ return resolvedProps;
85
+ };
86
+ //#endregion
87
+ exports.resolveSpreadFromUsages = resolveSpreadFromUsages;
@@ -0,0 +1,86 @@
1
+ /*! © 2026 Yahoo, Inc. UDS v0.0.0-development */
2
+ import { getParentComponentInfo } from "./jsx.js";
3
+ import { extractExplicitProps } from "./props.js";
4
+ import { SyntaxKind } from "ts-morph";
5
+ //#region ../css/dist/purger/optimized/ast/spread.mjs
6
+ /*! © 2026 Yahoo, Inc. UDS CSS v0.0.0-development */
7
+ /**
8
+ * Find all usages of a component across the entire project.
9
+ * This is the key function for spread resolution - when we encounter
10
+ * {...props} on a UDS component inside a wrapper, we trace back to
11
+ * find all places where the wrapper is used and extract actual values.
12
+ *
13
+ * @param componentName Name of the component to find usages of
14
+ * @param project The ts-morph project
15
+ * @param cache Optional cache to avoid re-scanning
16
+ * @returns Array of component usages with their props
17
+ */
18
+ const findComponentUsages = (componentName, project, cache) => {
19
+ if (cache?.has(componentName)) return cache.get(componentName);
20
+ const usages = [];
21
+ project.getSourceFiles().forEach((sourceFile) => {
22
+ if (!fileReferencesComponent(sourceFile, componentName)) return;
23
+ [...sourceFile.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement), ...sourceFile.getDescendantsOfKind(SyntaxKind.JsxOpeningElement)].filter((el) => el.getTagNameNode().getText() === componentName).forEach((element) => {
24
+ const props = extractExplicitProps(element);
25
+ usages.push({
26
+ componentName,
27
+ filePath: sourceFile.getFilePath(),
28
+ line: element.getStartLineNumber(),
29
+ props
30
+ });
31
+ });
32
+ });
33
+ if (cache) cache.set(componentName, usages);
34
+ return usages;
35
+ };
36
+ /**
37
+ * Check if a source file references a component (by import or definition)
38
+ */
39
+ const fileReferencesComponent = (sourceFile, componentName) => {
40
+ if (sourceFile.getImportDeclarations().some((imp) => imp.getNamedImports().some((named) => named.getName() === componentName))) return true;
41
+ if (sourceFile.getVariableDeclaration(componentName)) return true;
42
+ if (sourceFile.getFunction(componentName)) return true;
43
+ return false;
44
+ };
45
+ /**
46
+ * Resolve spread props by tracing component usage.
47
+ *
48
+ * When we encounter {...props} on a UDS component inside a wrapper:
49
+ * 1. Find the parent component that contains this spread
50
+ * 2. Search ALL files for usages of that parent component
51
+ * 3. Extract actual prop values passed at each usage site
52
+ * 4. Return ONLY those values - never fall back to type properties
53
+ *
54
+ * @param element The JSX element with the spread
55
+ * @param spreadIdentifier The identifier being spread (e.g., 'rest' from {...rest})
56
+ * @param context The purge context for caching
57
+ * @returns Map of prop names to their resolved values, or undefined if couldn't trace
58
+ */
59
+ const resolveSpreadFromUsages = (element, spreadIdentifier, context) => {
60
+ const parentInfo = getParentComponentInfo(element);
61
+ if (!parentInfo) return;
62
+ if (!(parentInfo.spreadRestIdentifier === spreadIdentifier || parentInfo.paramIdentifier === spreadIdentifier)) return;
63
+ const usages = findComponentUsages(parentInfo.componentName, context.project, context.componentUsageCache);
64
+ if (usages.length === 0) return;
65
+ context.stats.spreadsTraced++;
66
+ const resolvedProps = /* @__PURE__ */ new Map();
67
+ usages.forEach((usage) => {
68
+ usage.props.forEach((prop) => {
69
+ if (prop.fromSpread) return;
70
+ const existing = resolvedProps.get(prop.name) ?? [];
71
+ prop.values.forEach((value) => {
72
+ if (!existing.includes(value)) existing.push(value);
73
+ });
74
+ resolvedProps.set(prop.name, existing);
75
+ });
76
+ });
77
+ const cacheKey = `${element.getSourceFile().getFilePath()}:${element.getStartLineNumber()}:${spreadIdentifier}`;
78
+ context.spreadCache.set(cacheKey, Array.from(resolvedProps).map(([name, values]) => ({
79
+ name,
80
+ values,
81
+ fromSpread: true
82
+ })));
83
+ return resolvedProps;
84
+ };
85
+ //#endregion
86
+ export { resolveSpreadFromUsages };
@@ -2,8 +2,8 @@
2
2
  const require_runtime = require("../../../../_virtual/_rolldown/runtime.cjs");
3
3
  const require_purgeFromCode = require("./purgeFromCode.cjs");
4
4
  const require_entryPoints = require("../../../../tailwind-internal/dist/utils/entryPoints.cjs");
5
- const require_safelist = require("./utils/safelist.cjs");
6
5
  const require_files = require("./utils/files.cjs");
6
+ const require_safelist = require("./utils/safelist.cjs");
7
7
  let node_path = require("node:path");
8
8
  node_path = require_runtime.__toESM(node_path, 1);
9
9
  let node_fs = require("node:fs");
@@ -24,11 +24,11 @@ const clearWarnings = () => {
24
24
  * between CLI and playground.
25
25
  */
26
26
  const purgeOptimized = async (options) => {
27
- const { entry = require_entryPoints.DEFAULT_ENTRY, output = "dist/safelist.ts", colorModes = ["dark"], variants, autoVariants, componentData } = options;
27
+ const { entry = require_entryPoints.DEFAULT_ENTRY, output = "dist/safelist.ts", colorModes = ["dark"], ignoreDevFiles = true, ignore = [], includeStories = false, variants, autoVariants, componentData } = options;
28
28
  const startTime = performance.now();
29
29
  clearWarnings();
30
30
  const workspaceDir = process.env.PWD ?? process.cwd();
31
- const files = await require_files.scanForFiles(entry);
31
+ const files = await require_files.scanForFiles(entry, ignoreDevFiles && !includeStories, ignoreDevFiles && includeStories ? [...ignore, ...require_files.TEST_FILE_IGNORE_GLOBS] : ignore);
32
32
  const allClasses = [];
33
33
  const allImports = /* @__PURE__ */ new Set();
34
34
  const allComponents = /* @__PURE__ */ new Set();
@@ -37,26 +37,32 @@ const purgeOptimized = async (options) => {
37
37
  timeMs: 0,
38
38
  classesGenerated: 0,
39
39
  spreadsTraced: 0,
40
+ spreadsUnresolved: 0,
40
41
  expressionsResolved: 0
41
42
  };
42
- (await Promise.all(files.map(async (filePath) => {
43
- return require_purgeFromCode.purgeFromCodeOptimized(node_fs.default.readFileSync(filePath, "utf-8"), {
43
+ const fileContents = files.map((filePath) => node_fs.default.readFileSync(filePath, "utf-8"));
44
+ const results = await Promise.all(fileContents.map(async (code) => {
45
+ return require_purgeFromCode.purgeFromCodeOptimized(code, {
44
46
  colorModes,
45
47
  variants,
46
48
  autoVariants,
47
- componentData
49
+ componentData,
50
+ deferWrapperSpreadResolution: true
48
51
  });
49
- }))).forEach((result) => {
52
+ }));
53
+ results.forEach((result) => {
50
54
  allClasses.push(...result.safelist);
51
55
  result.imports.forEach((imp) => allImports.add(imp));
52
56
  result.components.forEach((comp) => allComponents.add(comp));
53
57
  stats.spreadsTraced += result.stats.spreadsTraced;
58
+ stats.spreadsUnresolved += result.stats.spreadsUnresolved;
54
59
  stats.expressionsResolved += result.stats.expressionsResolved;
55
60
  });
61
+ allClasses.push(...require_purgeFromCode.resolveWrapperSpreadSafelist(results.flatMap((result) => result.unresolvedWrapperSpreads), fileContents, variants));
56
62
  allClasses.push(...require_safelist.getThemeAndScaleClasses(colorModes));
57
63
  allClasses.push(...require_safelist.getInternalSafelistClasses());
58
64
  const finalSafelist = require_safelist.deduplicateSafelist(allClasses).sort();
59
- await require_safelist.saveSafelistToFile(finalSafelist, node_path.default.join(workspaceDir, output));
65
+ await require_safelist.saveSafelistToFile(finalSafelist, node_path.default.isAbsolute(output) ? output : node_path.default.join(workspaceDir, output));
60
66
  stats.timeMs = Math.round(performance.now() - startTime);
61
67
  stats.classesGenerated = finalSafelist.length;
62
68
  return {
@@ -1,8 +1,8 @@
1
1
  /*! © 2026 Yahoo, Inc. UDS v0.0.0-development */
2
- import { purgeFromCodeOptimized } from "./purgeFromCode.js";
2
+ import { purgeFromCodeOptimized, resolveWrapperSpreadSafelist } from "./purgeFromCode.js";
3
3
  import { DEFAULT_ENTRY } from "../../../../tailwind-internal/dist/utils/entryPoints.js";
4
+ import { TEST_FILE_IGNORE_GLOBS, scanForFiles } from "./utils/files.js";
4
5
  import { deduplicateSafelist, getInternalSafelistClasses, getThemeAndScaleClasses, saveSafelistToFile } from "./utils/safelist.js";
5
- import { scanForFiles } from "./utils/files.js";
6
6
  import path from "node:path";
7
7
  import fs from "node:fs";
8
8
  //#region ../css/dist/purger/optimized/purge.mjs
@@ -21,11 +21,11 @@ const clearWarnings = () => {
21
21
  * between CLI and playground.
22
22
  */
23
23
  const purgeOptimized = async (options) => {
24
- const { entry = DEFAULT_ENTRY, output = "dist/safelist.ts", colorModes = ["dark"], variants, autoVariants, componentData } = options;
24
+ const { entry = DEFAULT_ENTRY, output = "dist/safelist.ts", colorModes = ["dark"], ignoreDevFiles = true, ignore = [], includeStories = false, variants, autoVariants, componentData } = options;
25
25
  const startTime = performance.now();
26
26
  clearWarnings();
27
27
  const workspaceDir = process.env.PWD ?? process.cwd();
28
- const files = await scanForFiles(entry);
28
+ const files = await scanForFiles(entry, ignoreDevFiles && !includeStories, ignoreDevFiles && includeStories ? [...ignore, ...TEST_FILE_IGNORE_GLOBS] : ignore);
29
29
  const allClasses = [];
30
30
  const allImports = /* @__PURE__ */ new Set();
31
31
  const allComponents = /* @__PURE__ */ new Set();
@@ -34,26 +34,32 @@ const purgeOptimized = async (options) => {
34
34
  timeMs: 0,
35
35
  classesGenerated: 0,
36
36
  spreadsTraced: 0,
37
+ spreadsUnresolved: 0,
37
38
  expressionsResolved: 0
38
39
  };
39
- (await Promise.all(files.map(async (filePath) => {
40
- return purgeFromCodeOptimized(fs.readFileSync(filePath, "utf-8"), {
40
+ const fileContents = files.map((filePath) => fs.readFileSync(filePath, "utf-8"));
41
+ const results = await Promise.all(fileContents.map(async (code) => {
42
+ return purgeFromCodeOptimized(code, {
41
43
  colorModes,
42
44
  variants,
43
45
  autoVariants,
44
- componentData
46
+ componentData,
47
+ deferWrapperSpreadResolution: true
45
48
  });
46
- }))).forEach((result) => {
49
+ }));
50
+ results.forEach((result) => {
47
51
  allClasses.push(...result.safelist);
48
52
  result.imports.forEach((imp) => allImports.add(imp));
49
53
  result.components.forEach((comp) => allComponents.add(comp));
50
54
  stats.spreadsTraced += result.stats.spreadsTraced;
55
+ stats.spreadsUnresolved += result.stats.spreadsUnresolved;
51
56
  stats.expressionsResolved += result.stats.expressionsResolved;
52
57
  });
58
+ allClasses.push(...resolveWrapperSpreadSafelist(results.flatMap((result) => result.unresolvedWrapperSpreads), fileContents, variants));
53
59
  allClasses.push(...getThemeAndScaleClasses(colorModes));
54
60
  allClasses.push(...getInternalSafelistClasses());
55
61
  const finalSafelist = deduplicateSafelist(allClasses).sort();
56
- await saveSafelistToFile(finalSafelist, path.join(workspaceDir, output));
62
+ await saveSafelistToFile(finalSafelist, path.isAbsolute(output) ? output : path.join(workspaceDir, output));
57
63
  stats.timeMs = Math.round(performance.now() - startTime);
58
64
  stats.classesGenerated = finalSafelist.length;
59
65
  return {
@@ -2,6 +2,8 @@
2
2
  require("../../../../_virtual/_rolldown/runtime.cjs");
3
3
  const require_perf = require("../../css/perf.cjs");
4
4
  const require_expressions = require("./ast/expressions.cjs");
5
+ const require_jsx = require("./ast/jsx.cjs");
6
+ const require_spread = require("./ast/spread.cjs");
5
7
  let ts_morph = require("ts-morph");
6
8
  //#region ../css/dist/purger/optimized/purgeFromCode.mjs
7
9
  /*! © 2026 Yahoo, Inc. UDS CSS v0.0.0-development */
@@ -71,10 +73,10 @@ const normalizeComponentName = (name) => {
71
73
  return name.replace(/\W/g, "").toLowerCase();
72
74
  };
73
75
  /**
74
- * Fallback: infer prop -> variant key mapping from autoVariants when the
75
- * generated component data misses it (e.g., helpers like buildAvatarRootClasses).
76
+ * All autoVariants group keys belonging to a component (by normalized name
77
+ * prefix, e.g. Button -> buttonVariantRoot, buttonSizeIcon, ...).
76
78
  */
77
- const getAutoVariantKeysForProp = (componentName, propName) => {
79
+ const getComponentAutoVariantKeys = (componentName) => {
78
80
  if (!autoVariantsCache) return [];
79
81
  const segments = componentName.match(/[A-Z][a-z0-9]*/g) ?? [];
80
82
  const candidates = new Set([normalizeComponentName(componentName)]);
@@ -82,12 +84,19 @@ const getAutoVariantKeysForProp = (componentName, propName) => {
82
84
  const partial = segments.slice(0, i).join("");
83
85
  candidates.add(normalizeComponentName(partial));
84
86
  });
85
- const propLower = propName.toLowerCase();
86
87
  return Object.keys(autoVariantsCache).filter((key) => {
87
88
  const keyLower = key.toLowerCase();
88
- return Array.from(candidates).some((candidate) => keyLower.startsWith(candidate) && keyLower.includes(propLower));
89
+ return Array.from(candidates).some((candidate) => keyLower.startsWith(candidate));
89
90
  });
90
91
  };
92
+ /**
93
+ * Fallback: infer prop -> variant key mapping from autoVariants when the
94
+ * generated component data misses it (e.g., helpers like buildAvatarRootClasses).
95
+ */
96
+ const getAutoVariantKeysForProp = (componentName, propName) => {
97
+ const propLower = propName.toLowerCase();
98
+ return getComponentAutoVariantKeys(componentName).filter((key) => key.toLowerCase().includes(propLower));
99
+ };
91
100
  const getVariantKeysForProp = (componentInfo, propName) => {
92
101
  const keys = new Set(componentInfo.propToVariantKeys?.[propName] ?? []);
93
102
  getAutoVariantKeysForProp(componentInfo.name, propName).forEach((inferred) => keys.add(inferred));
@@ -169,19 +178,30 @@ const purgeFromCodeOptimized = async (code, options) => {
169
178
  timeMs: 0,
170
179
  classesGenerated: 0,
171
180
  spreadsTraced: 0,
181
+ spreadsUnresolved: 0,
172
182
  expressionsResolved: 0
173
- }
183
+ },
184
+ unresolvedWrapperSpreads: []
174
185
  };
175
186
  const projectCreateStart = performance.now();
176
- const sourceFile = new ts_morph.Project({ useInMemoryFileSystem: true }).createSourceFile(options.filePath ?? "input.tsx", code, { overwrite: true });
187
+ const project = new ts_morph.Project({ useInMemoryFileSystem: true });
188
+ const sourceFile = project.createSourceFile(options.filePath ?? "input.tsx", code, { overwrite: true });
177
189
  const projectCreateMs = performance.now() - projectCreateStart;
178
190
  const stats = {
179
191
  filesScanned: 1,
180
192
  timeMs: 0,
181
193
  classesGenerated: 0,
182
194
  spreadsTraced: 0,
195
+ spreadsUnresolved: 0,
183
196
  expressionsResolved: 0
184
197
  };
198
+ const purgeContext = {
199
+ project,
200
+ componentUsageCache: /* @__PURE__ */ new Map(),
201
+ spreadCache: /* @__PURE__ */ new Map(),
202
+ udsImportingFiles: /* @__PURE__ */ new Set(),
203
+ stats
204
+ };
185
205
  const importParseStart = performance.now();
186
206
  const imports = [];
187
207
  sourceFile.getImportDeclarations().forEach((importDecl) => {
@@ -192,6 +212,8 @@ const purgeFromCodeOptimized = async (code, options) => {
192
212
  const importParseMs = performance.now() - importParseStart;
193
213
  const componentProps = /* @__PURE__ */ new Map();
194
214
  const componentUnresolvedSpreadProps = /* @__PURE__ */ new Map();
215
+ const componentsWithUnknownSpreads = /* @__PURE__ */ new Set();
216
+ const unresolvedWrapperSpreads = [];
195
217
  const referencedComponents = /* @__PURE__ */ new Set();
196
218
  const getStylesExtractStart = performance.now();
197
219
  const getStylesProps = extractGetStylesCalls(sourceFile, stats, variants);
@@ -204,7 +226,8 @@ const purgeFromCodeOptimized = async (code, options) => {
204
226
  referencedComponents.add(componentName);
205
227
  const propsMap = componentProps.get(componentName) ?? /* @__PURE__ */ new Map();
206
228
  const unresolvedSpreadProps = componentUnresolvedSpreadProps.get(componentName) ?? /* @__PURE__ */ new Set();
207
- extractPropsFromReference(reference, stats, sourceFile).forEach(({ propName, values, fromSpread }) => {
229
+ const { props, unknownSpreadWrappers } = extractPropsFromReference(reference, stats, sourceFile, purgeContext);
230
+ props.forEach(({ propName, values, fromSpread }) => {
208
231
  if (values.length > 0) {
209
232
  const existing = propsMap.get(propName) ?? /* @__PURE__ */ new Set();
210
233
  values.forEach((val) => existing.add(val));
@@ -213,6 +236,13 @@ const purgeFromCodeOptimized = async (code, options) => {
213
236
  }
214
237
  if (fromSpread) unresolvedSpreadProps.add(propName);
215
238
  });
239
+ unknownSpreadWrappers.forEach((wrapperName) => {
240
+ if (wrapperName) unresolvedWrapperSpreads.push({
241
+ wrapperName,
242
+ targetComponent: componentName
243
+ });
244
+ if (!wrapperName || !options.deferWrapperSpreadResolution) componentsWithUnknownSpreads.add(componentName);
245
+ });
216
246
  componentProps.set(componentName, propsMap);
217
247
  componentUnresolvedSpreadProps.set(componentName, unresolvedSpreadProps);
218
248
  });
@@ -242,6 +272,7 @@ const purgeFromCodeOptimized = async (code, options) => {
242
272
  allComponents.forEach((componentInfo) => {
243
273
  const userProps = componentProps.get(componentInfo.name) ?? /* @__PURE__ */ new Map();
244
274
  const unresolvedSpreadProps = componentUnresolvedSpreadProps.get(componentInfo.name) ?? /* @__PURE__ */ new Set();
275
+ if (componentsWithUnknownSpreads.has(componentInfo.name)) collectUnknownSpreadClasses(componentInfo, variants, safelist);
245
276
  Object.entries(componentInfo.defaultProps).forEach(([propName, propValue]) => {
246
277
  if (userProps.has(propName)) return;
247
278
  const variantGroup = variants[propName];
@@ -367,7 +398,8 @@ const purgeFromCodeOptimized = async (code, options) => {
367
398
  safelist: finalSafelist,
368
399
  imports,
369
400
  components: [...seenComponents],
370
- stats
401
+ stats,
402
+ unresolvedWrapperSpreads
371
403
  };
372
404
  };
373
405
  /**
@@ -400,9 +432,16 @@ const findComponentReferences = (sourceFile, componentName, jsxElementsCache) =>
400
432
  };
401
433
  /**
402
434
  * Extract props from a JSX reference.
435
+ *
436
+ * Spread resolution order (UDS-2557):
437
+ * 1. Same-file const object (`traceSpreadInFile`)
438
+ * 2. Wrapper call sites within the project (`resolveSpreadFromUsages`)
439
+ * 3. Otherwise the spread is unknown: reported via `unknownSpreadWrappers` so the
440
+ * caller can over-include variant classes and emit cross-file metadata.
403
441
  */
404
- const extractPropsFromReference = (reference, stats, sourceFile) => {
442
+ const extractPropsFromReference = (reference, stats, sourceFile, purgeContext) => {
405
443
  const props = [];
444
+ const unknownSpreadWrappers = [];
406
445
  reference.getAttributes().forEach((attr) => {
407
446
  if (attr.asKind(ts_morph.SyntaxKind.JsxAttribute)) {
408
447
  const jsxAttr = attr.asKindOrThrow(ts_morph.SyntaxKind.JsxAttribute);
@@ -427,17 +466,40 @@ const extractPropsFromReference = (reference, stats, sourceFile) => {
427
466
  });
428
467
  }
429
468
  if (attr.asKind(ts_morph.SyntaxKind.JsxSpreadAttribute)) {
430
- stats.spreadsTraced++;
431
- traceSpreadInFile(attr.asKindOrThrow(ts_morph.SyntaxKind.JsxSpreadAttribute).getExpression().getText(), sourceFile).forEach(([propName, values]) => {
432
- props.push({
433
- propName,
434
- values,
435
- fromSpread: true
469
+ const spreadName = attr.asKindOrThrow(ts_morph.SyntaxKind.JsxSpreadAttribute).getExpression().getText();
470
+ const resolvedSpreadProps = traceSpreadInFile(spreadName, sourceFile);
471
+ if (resolvedSpreadProps.length > 0) {
472
+ stats.spreadsTraced++;
473
+ resolvedSpreadProps.forEach(([propName, values]) => {
474
+ props.push({
475
+ propName,
476
+ values,
477
+ fromSpread: true
478
+ });
436
479
  });
437
- });
480
+ return;
481
+ }
482
+ const usageResolved = require_spread.resolveSpreadFromUsages(reference, spreadName, purgeContext);
483
+ if (usageResolved) {
484
+ usageResolved.forEach((values, propName) => {
485
+ props.push({
486
+ propName,
487
+ values,
488
+ fromSpread: true
489
+ });
490
+ });
491
+ return;
492
+ }
493
+ stats.spreadsUnresolved++;
494
+ const parentInfo = require_jsx.getParentComponentInfo(reference);
495
+ const isWrapperParamSpread = parentInfo && (parentInfo.spreadRestIdentifier === spreadName || parentInfo.paramIdentifier === spreadName);
496
+ unknownSpreadWrappers.push(isWrapperParamSpread ? parentInfo.componentName : void 0);
438
497
  }
439
498
  });
440
- return props;
499
+ return {
500
+ props,
501
+ unknownSpreadWrappers
502
+ };
441
503
  };
442
504
  /**
443
505
  * Trace spread props to their source within the same file.
@@ -514,5 +576,101 @@ const extractGetStylesCalls = (sourceFile, stats, variants) => {
514
576
  });
515
577
  return props;
516
578
  };
579
+ /**
580
+ * Over-include every variant-prop class for a component (and its internal deps).
581
+ * Used when a spread's props are unknown: any variant prop could flow through it,
582
+ * so accuracy demands including all of them (UDS-2557).
583
+ */
584
+ const collectUnknownSpreadClasses = (componentInfo, variants, safelist) => {
585
+ getComponentWithDeps(componentInfo.name).forEach((comp) => {
586
+ Object.keys(comp.propToVariantKeys ?? {}).forEach((propName) => {
587
+ addVariantGroupClasses(variants[propName] ?? autoVariantsCache?.[propName], safelist);
588
+ getVariantKeysForProp(comp, propName).forEach((variantKey) => {
589
+ addVariantGroupClasses(variants[variantKey] ?? autoVariantsCache?.[variantKey], safelist);
590
+ });
591
+ });
592
+ getComponentAutoVariantKeys(comp.name).forEach((variantKey) => {
593
+ addVariantGroupClasses(autoVariantsCache?.[variantKey], safelist);
594
+ });
595
+ });
596
+ };
597
+ /**
598
+ * Cross-file pass for UDS-2557: resolve wrapper rest-spread props from wrapper
599
+ * call sites found in other scanned files.
600
+ *
601
+ * `purgeFromCodeOptimized` analyzes one file at a time, so when a consumer
602
+ * wrapper (e.g. an i13n wrapper) spreads its props onto a UDS component, the
603
+ * wrapper's call sites usually live in other files. Aggregators pass the
604
+ * unresolved wrapper spreads plus the raw source of every scanned file; this
605
+ * parses only the files that mention a wrapper and maps the explicit prop
606
+ * values at each call site to the target UDS component's classes.
607
+ *
608
+ * Must be called after at least one `purgeFromCodeOptimized` run so the
609
+ * module-level component/autoVariants caches are populated.
610
+ */
611
+ const resolveWrapperSpreadSafelist = (wrapperSpreads, sources, variants) => {
612
+ if (wrapperSpreads.length === 0) return [];
613
+ const targetsByWrapper = /* @__PURE__ */ new Map();
614
+ wrapperSpreads.forEach(({ wrapperName, targetComponent }) => {
615
+ const targets = targetsByWrapper.get(wrapperName) ?? /* @__PURE__ */ new Set();
616
+ targets.add(targetComponent);
617
+ targetsByWrapper.set(wrapperName, targets);
618
+ });
619
+ const wrapperNames = [...targetsByWrapper.keys()];
620
+ const usageProps = /* @__PURE__ */ new Map();
621
+ sources.filter((code) => wrapperNames.some((name) => code.includes(`<${name}`))).forEach((code) => {
622
+ new ts_morph.Project({ useInMemoryFileSystem: true }).createSourceFile("wrapper-usage.tsx", code, { overwrite: true }).getDescendants().forEach((node) => {
623
+ if (!ts_morph.Node.isJsxOpeningElement(node) && !ts_morph.Node.isJsxSelfClosingElement(node)) return;
624
+ const tagName = node.getTagNameNode().getText();
625
+ if (!targetsByWrapper.has(tagName)) return;
626
+ const propsMap = usageProps.get(tagName) ?? /* @__PURE__ */ new Map();
627
+ node.getAttributes().forEach((attr) => {
628
+ const jsxAttr = attr.asKind(ts_morph.SyntaxKind.JsxAttribute);
629
+ if (!jsxAttr) return;
630
+ const propName = jsxAttr.getNameNode().getText();
631
+ const values = [];
632
+ const initializer = jsxAttr.getInitializer();
633
+ if (initializer && ts_morph.Node.isStringLiteral(initializer)) values.push(initializer.getLiteralText());
634
+ else if (initializer && ts_morph.Node.isJsxExpression(initializer)) {
635
+ const expression = initializer.getExpression();
636
+ if (expression) values.push(...require_expressions.extractStringLiterals(expression));
637
+ }
638
+ if (values.length === 0) return;
639
+ const existing = propsMap.get(propName) ?? /* @__PURE__ */ new Set();
640
+ values.forEach((value) => existing.add(value));
641
+ propsMap.set(propName, existing);
642
+ });
643
+ usageProps.set(tagName, propsMap);
644
+ });
645
+ });
646
+ const safelist = [];
647
+ const pushClass = (variantGroup, value) => {
648
+ const cls = variantGroup?.[value];
649
+ if (cls) safelist.push(cls.replaceAll("\\", ""));
650
+ };
651
+ targetsByWrapper.forEach((targets, wrapperName) => {
652
+ const propsMap = usageProps.get(wrapperName);
653
+ targets.forEach((targetComponent) => {
654
+ const componentInfo = resolveComponentInfo(targetComponent);
655
+ if (!componentInfo) return;
656
+ if (!propsMap || propsMap.size === 0) {
657
+ collectUnknownSpreadClasses(componentInfo, variants, safelist);
658
+ return;
659
+ }
660
+ propsMap.forEach((values, propName) => {
661
+ const variantGroup = variants[propName];
662
+ values.forEach((value) => pushClass(variantGroup, value));
663
+ getComponentWithDeps(componentInfo.name).forEach((comp) => {
664
+ getVariantKeysForProp(comp, propName).forEach((variantKey) => {
665
+ const mappedVariantGroup = variants[variantKey] ?? autoVariantsCache?.[variantKey];
666
+ values.forEach((value) => pushClass(mappedVariantGroup, value));
667
+ });
668
+ });
669
+ });
670
+ });
671
+ });
672
+ return safelist;
673
+ };
517
674
  //#endregion
518
675
  exports.purgeFromCodeOptimized = purgeFromCodeOptimized;
676
+ exports.resolveWrapperSpreadSafelist = resolveWrapperSpreadSafelist;