@csszyx/compiler 0.4.0 → 0.6.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.
package/dist/index.cjs CHANGED
@@ -329,6 +329,39 @@ function stripInvalidColorStrings(sz) {
329
329
  var babel = __toESM(require("@babel/core"), 1);
330
330
  var t = __toESM(require("@babel/types"), 1);
331
331
 
332
+ // src/ast-budget.ts
333
+ var AST_BUDGET = 5e4;
334
+ var ASTBudgetExceededError = class extends Error {
335
+ /**
336
+ *
337
+ * @param filename Source filename if known, otherwise `undefined`.
338
+ * @param nodeCount Node count at the point traversal was aborted.
339
+ * @param budget Effective budget that was exceeded. Defaults to
340
+ * {@link AST_BUDGET} for backwards compatibility with callers that
341
+ * don't pass an override.
342
+ */
343
+ constructor(filename, nodeCount, budget = AST_BUDGET) {
344
+ const where = filename ?? "<anonymous>";
345
+ super(
346
+ `[csszyx] AST budget exceeded: ${where} has more than ${budget} nodes (traversal aborted at ${nodeCount}). Files this large are almost always machine-generated and should be excluded from sz transformation. Either exclude the file from the plugin (Vite: \`csszyx({ exclude: [/large-data\\.ts$/] })\`), or raise the limit globally with \`csszyx({ build: { astBudgetLimit: 100_000 } })\`.`
347
+ );
348
+ this.name = "ASTBudgetExceededError";
349
+ this.filename = filename;
350
+ this.nodeCount = nodeCount;
351
+ this.budget = budget;
352
+ }
353
+ };
354
+
355
+ // src/recovery-tokens.ts
356
+ var import_node_crypto = require("crypto");
357
+ function generateInlineRecoveryToken(filename, line, column, elementType) {
358
+ const input = `${filename}:${line}:${column}:${elementType}`;
359
+ return (0, import_node_crypto.createHash)("sha256").update(input).digest("hex").substring(0, 12);
360
+ }
361
+ function isValidInlineRecoveryMode(value) {
362
+ return value === "csr" || value === "dev-only";
363
+ }
364
+
332
365
  // src/transform-core.ts
333
366
  var PROPERTY_MAP = {
334
367
  // Background
@@ -2321,7 +2354,8 @@ function normalizeClassName(className) {
2321
2354
  }
2322
2355
 
2323
2356
  // src/transform.ts
2324
- function transformSourceCode(source) {
2357
+ function transformSourceCode(source, filename, options) {
2358
+ const astBudget = options?.astBudget ?? AST_BUDGET;
2325
2359
  let usesRuntime = false;
2326
2360
  let usesMerge = false;
2327
2361
  let usesColorVar = false;
@@ -2329,12 +2363,13 @@ function transformSourceCode(source) {
2329
2363
  const collectedClasses = /* @__PURE__ */ new Set();
2330
2364
  const rawClassNames = /* @__PURE__ */ new Set();
2331
2365
  const diagnostics = [];
2366
+ const recoveryTokens = /* @__PURE__ */ new Map();
2332
2367
  if (!source.includes("sz")) {
2333
- return { code: source, transformed: false, usesRuntime: false, usesMerge: false, usesColorVar: false, classes: collectedClasses, rawClassNames, diagnostics };
2368
+ return { code: source, transformed: false, usesRuntime: false, usesMerge: false, usesColorVar: false, classes: collectedClasses, rawClassNames, diagnostics, recoveryTokens };
2334
2369
  }
2335
2370
  try {
2336
2371
  const result = babel.transformSync(source, {
2337
- filename: "file.tsx",
2372
+ filename: filename ?? "file.tsx",
2338
2373
  // Enable TS/JSX parsing
2339
2374
  ast: true,
2340
2375
  code: true,
@@ -2346,6 +2381,21 @@ function transformSourceCode(source) {
2346
2381
  plugins: [
2347
2382
  function() {
2348
2383
  return {
2384
+ // Budget guard runs in `pre` (before the visitor pass)
2385
+ // so it short-circuits pathologically large files
2386
+ // before any sz transform work begins, and doesn't
2387
+ // interfere with the JSXAttribute handler below.
2388
+ pre(file) {
2389
+ let nodeCount = 0;
2390
+ babel.traverse(file.ast, {
2391
+ enter() {
2392
+ nodeCount++;
2393
+ if (nodeCount > astBudget) {
2394
+ throw new ASTBudgetExceededError(filename, nodeCount, astBudget);
2395
+ }
2396
+ }
2397
+ });
2398
+ },
2349
2399
  visitor: {
2350
2400
  JSXAttribute(path) {
2351
2401
  const attrName = t.isJSXIdentifier(path.node.name) ? path.node.name.name : "";
@@ -2360,6 +2410,50 @@ function transformSourceCode(source) {
2360
2410
  }
2361
2411
  return;
2362
2412
  }
2413
+ if (attrName === "szRecover") {
2414
+ const recoverValue = path.node.value;
2415
+ if (!t.isStringLiteral(recoverValue)) {
2416
+ diagnostics.push(
2417
+ `[csszyx] szRecover at ${filename ?? "<anonymous>"}: only string-literal values ("csr" | "dev-only") are supported. Dynamic values disable token emission for this element.`
2418
+ );
2419
+ return;
2420
+ }
2421
+ if (!isValidInlineRecoveryMode(recoverValue.value)) {
2422
+ diagnostics.push(
2423
+ `[csszyx] szRecover at ${filename ?? "<anonymous>"}: unknown mode "${recoverValue.value}" \u2014 expected "csr" or "dev-only". Token emission skipped.`
2424
+ );
2425
+ return;
2426
+ }
2427
+ const opening = path.parentPath;
2428
+ if (!opening?.isJSXOpeningElement()) {
2429
+ return;
2430
+ }
2431
+ const alreadyTagged = opening.node.attributes.some(
2432
+ (attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && attr.name.name === "data-sz-recovery-token"
2433
+ );
2434
+ if (alreadyTagged) {
2435
+ return;
2436
+ }
2437
+ const loc = path.node.loc;
2438
+ const elementType = t.isJSXIdentifier(opening.node.name) ? opening.node.name.name : t.isJSXMemberExpression(opening.node.name) ? "<member>" : "<unknown>";
2439
+ const line = loc?.start.line ?? 0;
2440
+ const column = loc?.start.column ?? 0;
2441
+ const file = filename ?? "file.tsx";
2442
+ const token = generateInlineRecoveryToken(file, line, column, elementType);
2443
+ opening.node.attributes.push(
2444
+ t.jsxAttribute(
2445
+ t.jsxIdentifier("data-sz-recovery-token"),
2446
+ t.stringLiteral(token)
2447
+ )
2448
+ );
2449
+ recoveryTokens.set(token, {
2450
+ mode: recoverValue.value,
2451
+ component: elementType,
2452
+ path: `${file}:${line}:${column}`
2453
+ });
2454
+ transformed = true;
2455
+ return;
2456
+ }
2363
2457
  if (attrName !== "sz") {
2364
2458
  return;
2365
2459
  }
@@ -2862,11 +2956,15 @@ function transformSourceCode(source) {
2862
2956
  usesColorVar,
2863
2957
  classes: collectedClasses,
2864
2958
  rawClassNames,
2865
- diagnostics
2959
+ diagnostics,
2960
+ recoveryTokens
2866
2961
  };
2867
2962
  } catch (e) {
2963
+ if (e instanceof ASTBudgetExceededError) {
2964
+ throw e;
2965
+ }
2868
2966
  console.warn("[csszyx] AST transform failed, falling back to original code:", e);
2869
- return { code: source, transformed: false, usesRuntime: false, usesMerge: false, usesColorVar: false, classes: collectedClasses, rawClassNames, diagnostics };
2967
+ return { code: source, transformed: false, usesRuntime: false, usesMerge: false, usesColorVar: false, classes: collectedClasses, rawClassNames, diagnostics, recoveryTokens };
2870
2968
  }
2871
2969
  }
2872
2970
  function parseStyleStringToObjectExpr(styleStr) {
@@ -3716,7 +3814,7 @@ function hoistCSSVariables(usages, parentMap) {
3716
3814
  }
3717
3815
  function buildParentMap(ast) {
3718
3816
  const map = /* @__PURE__ */ new Map();
3719
- function traverse(node, parent) {
3817
+ function traverse2(node, parent) {
3720
3818
  if (parent) {
3721
3819
  map.set(node, parent);
3722
3820
  }
@@ -3726,16 +3824,16 @@ function buildParentMap(ast) {
3726
3824
  if (Array.isArray(value)) {
3727
3825
  for (const item of value) {
3728
3826
  if (item && typeof item === "object" && "type" in item) {
3729
- traverse(item, node);
3827
+ traverse2(item, node);
3730
3828
  }
3731
3829
  }
3732
3830
  } else if ("type" in value) {
3733
- traverse(value, node);
3831
+ traverse2(value, node);
3734
3832
  }
3735
3833
  }
3736
3834
  }
3737
3835
  }
3738
- traverse(ast);
3836
+ traverse2(ast);
3739
3837
  return map;
3740
3838
  }
3741
3839
 
package/dist/index.d.cts CHANGED
@@ -1,137 +1,6 @@
1
1
  import * as t from '@babel/types';
2
2
  import * as CSS from 'csstype';
3
3
 
4
- /**
5
- * JSX Transform - Converts sz prop to className string.
6
- *
7
- * This module handles the transformation of csszyx object syntax into
8
- * Tailwind CSS class strings. It processes nested objects for variants
9
- * like hover, focus, etc.
10
- */
11
- /**
12
- * Represents a value in the sz object.
13
- * Can be a string, number, boolean, or nested object for variants.
14
- */
15
- type SzValue = string | number | boolean | SzObject;
16
- /**
17
- * Represents the sz object structure.
18
- * Keys are CSS property abbreviations, values can be primitives or nested objects.
19
- */
20
- interface SzObject {
21
- [key: string]: SzValue;
22
- }
23
- declare const PROPERTY_MAP: Record<string, string>;
24
- declare const SUGGESTION_MAP: Record<string, string>;
25
- declare const KNOWN_VARIANTS: Set<string>;
26
- declare const BOOLEAN_SHORTHANDS: Set<string>;
27
- /**
28
- * Represents the result of a transformation.
29
- */
30
- interface TransformResult {
31
- className: string;
32
- attributes: Record<string, string>;
33
- }
34
- /**
35
- * Transforms a csszyx sz object into a Tailwind CSS className string and extracted attributes.
36
- *
37
- * @param {SzObject} szProp - The sz object from JSX
38
- * @param {string} prefix - Variant prefix for nested properties
39
- * @param {Record<string, string>} [mangleMap] - Optional map for property name mangling
40
- * @returns {TransformResult} The transformation result
41
- */
42
- declare function transform(szProp: SzObject, prefix?: string, mangleMap?: Record<string, string>): TransformResult;
43
- /**
44
- * Validates that an sz prop object is valid.
45
- *
46
- * @param {unknown} szProp - The value to validate
47
- * @returns {boolean} True if valid, false otherwise
48
- */
49
- declare function isValidSzProp(szProp: unknown): szProp is SzObject;
50
- /**
51
- * Normalizes a className string by removing extra whitespace.
52
- *
53
- * @param {string} className - The className string to normalize
54
- * @returns {string} The normalized className string
55
- */
56
- declare function normalizeClassName(className: string): string;
57
-
58
- /**
59
- * Transforms all sz props in a source code string into Tailwind classNames.
60
- *
61
- * @param {string} source - The source code to transform
62
- * @returns {object} Transformation result with code and metadata
63
- */
64
- declare function transformSourceCode(source: string): {
65
- code: string;
66
- transformed: boolean;
67
- usesRuntime: boolean;
68
- usesMerge: boolean;
69
- usesColorVar: boolean;
70
- classes: Set<string>;
71
- rawClassNames: Set<string>;
72
- diagnostics: string[];
73
- };
74
-
75
- /**
76
- * Core Compiler class for csszyx.
77
- *
78
- * This class manages the WASM lifecycle and provides high-performance
79
- * transformation methods. It falls back to JavaScript if WASM is not available.
80
- */
81
- declare class CsszyxCompiler {
82
- private static instance;
83
- private wasmLoaded;
84
- /**
85
- * Private constructor to enforce singleton pattern.
86
- */
87
- private constructor();
88
- /**
89
- * Gets the singleton instance of the compiler.
90
- *
91
- * @returns {CsszyxCompiler} The compiler instance.
92
- */
93
- static getInstance(): CsszyxCompiler;
94
- /**
95
- * Initializes the WASM core.
96
- *
97
- * @returns {Promise<void>} Resolves when WASM is ready.
98
- */
99
- init(): Promise<void>;
100
- /**
101
- * Transforms an sz object into Tailwind classes.
102
- *
103
- * @param {SzObject} sz - The object to transform.
104
- * @returns {string} The transformed class string.
105
- */
106
- transform(sz: SzObject): string;
107
- /**
108
- * Checks if the WASM core is currently active.
109
- *
110
- * @returns {boolean} True if WASM is loaded.
111
- */
112
- isWasmActive(): boolean;
113
- /**
114
- * Generates a recovery token using WASM or JS fallback.
115
- *
116
- * @param {object} metadata - Token metadata
117
- * @param metadata.component - Component name
118
- * @param metadata.filePath - File path source
119
- * @param metadata.line - Line number
120
- * @param metadata.column - Column number
121
- * @param metadata.mode - Build mode (dev/prod)
122
- * @param metadata.buildId - Unique build identifier
123
- * @returns {string} The generated token
124
- */
125
- generateRecoveryToken(metadata: {
126
- component: string;
127
- filePath: string;
128
- line: number;
129
- column: number;
130
- mode: 'csr' | 'dev-only';
131
- buildId: string;
132
- }): string;
133
- }
134
-
135
4
  /**
136
5
  * Valid recovery modes for szRecover attribute.
137
6
  */
@@ -426,6 +295,157 @@ declare function validateManifest(manifest: unknown): {
426
295
  error?: string;
427
296
  };
428
297
 
298
+ /**
299
+ * JSX Transform - Converts sz prop to className string.
300
+ *
301
+ * This module handles the transformation of csszyx object syntax into
302
+ * Tailwind CSS class strings. It processes nested objects for variants
303
+ * like hover, focus, etc.
304
+ */
305
+ /**
306
+ * Represents a value in the sz object.
307
+ * Can be a string, number, boolean, or nested object for variants.
308
+ */
309
+ type SzValue = string | number | boolean | SzObject;
310
+ /**
311
+ * Represents the sz object structure.
312
+ * Keys are CSS property abbreviations, values can be primitives or nested objects.
313
+ */
314
+ interface SzObject {
315
+ [key: string]: SzValue;
316
+ }
317
+ declare const PROPERTY_MAP: Record<string, string>;
318
+ declare const SUGGESTION_MAP: Record<string, string>;
319
+ declare const KNOWN_VARIANTS: Set<string>;
320
+ declare const BOOLEAN_SHORTHANDS: Set<string>;
321
+ /**
322
+ * Represents the result of a transformation.
323
+ */
324
+ interface TransformResult {
325
+ className: string;
326
+ attributes: Record<string, string>;
327
+ }
328
+ /**
329
+ * Transforms a csszyx sz object into a Tailwind CSS className string and extracted attributes.
330
+ *
331
+ * @param {SzObject} szProp - The sz object from JSX
332
+ * @param {string} prefix - Variant prefix for nested properties
333
+ * @param {Record<string, string>} [mangleMap] - Optional map for property name mangling
334
+ * @returns {TransformResult} The transformation result
335
+ */
336
+ declare function transform(szProp: SzObject, prefix?: string, mangleMap?: Record<string, string>): TransformResult;
337
+ /**
338
+ * Validates that an sz prop object is valid.
339
+ *
340
+ * @param {unknown} szProp - The value to validate
341
+ * @returns {boolean} True if valid, false otherwise
342
+ */
343
+ declare function isValidSzProp(szProp: unknown): szProp is SzObject;
344
+ /**
345
+ * Normalizes a className string by removing extra whitespace.
346
+ *
347
+ * @param {string} className - The className string to normalize
348
+ * @returns {string} The normalized className string
349
+ */
350
+ declare function normalizeClassName(className: string): string;
351
+
352
+ /**
353
+ * Options for {@link transformSourceCode}.
354
+ */
355
+ interface TransformSourceCodeOptions {
356
+ /**
357
+ * Override the AST node budget. Files larger than this throw
358
+ * {@link ASTBudgetExceededError}. Defaults to {@link AST_BUDGET} (50 000).
359
+ * Useful for repos with legitimately large generated files (json-as-ts
360
+ * fixtures, GraphQL schema snapshots) that exceed the default cap but
361
+ * are still safe to transform.
362
+ */
363
+ astBudget?: number;
364
+ }
365
+ /**
366
+ * Transforms all sz props in a source code string into Tailwind classNames.
367
+ *
368
+ * @param {string} source - The source code to transform
369
+ * @param {string} [filename] - Source filename, used in error messages and
370
+ * passed to Babel as the parser filename. Defaults to a placeholder.
371
+ * @param {TransformSourceCodeOptions} [options] - Optional overrides
372
+ * (currently: `astBudget` to raise/lower the per-file node cap).
373
+ * @returns {object} Transformation result with code and metadata
374
+ * @throws {ASTBudgetExceededError} when the file's AST exceeds the
375
+ * effective budget (`options.astBudget` or {@link AST_BUDGET}).
376
+ */
377
+ declare function transformSourceCode(source: string, filename?: string, options?: TransformSourceCodeOptions): {
378
+ code: string;
379
+ transformed: boolean;
380
+ usesRuntime: boolean;
381
+ usesMerge: boolean;
382
+ usesColorVar: boolean;
383
+ classes: Set<string>;
384
+ rawClassNames: Set<string>;
385
+ diagnostics: string[];
386
+ recoveryTokens: Map<string, TokenData>;
387
+ };
388
+
389
+ /**
390
+ * Core Compiler class for csszyx.
391
+ *
392
+ * This class manages the WASM lifecycle and provides high-performance
393
+ * transformation methods. It falls back to JavaScript if WASM is not available.
394
+ */
395
+ declare class CsszyxCompiler {
396
+ private static instance;
397
+ private wasmLoaded;
398
+ /**
399
+ * Private constructor to enforce singleton pattern.
400
+ */
401
+ private constructor();
402
+ /**
403
+ * Gets the singleton instance of the compiler.
404
+ *
405
+ * @returns {CsszyxCompiler} The compiler instance.
406
+ */
407
+ static getInstance(): CsszyxCompiler;
408
+ /**
409
+ * Initializes the WASM core.
410
+ *
411
+ * @returns {Promise<void>} Resolves when WASM is ready.
412
+ */
413
+ init(): Promise<void>;
414
+ /**
415
+ * Transforms an sz object into Tailwind classes.
416
+ *
417
+ * @param {SzObject} sz - The object to transform.
418
+ * @returns {string} The transformed class string.
419
+ */
420
+ transform(sz: SzObject): string;
421
+ /**
422
+ * Checks if the WASM core is currently active.
423
+ *
424
+ * @returns {boolean} True if WASM is loaded.
425
+ */
426
+ isWasmActive(): boolean;
427
+ /**
428
+ * Generates a recovery token using WASM or JS fallback.
429
+ *
430
+ * @param {object} metadata - Token metadata
431
+ * @param metadata.component - Component name
432
+ * @param metadata.filePath - File path source
433
+ * @param metadata.line - Line number
434
+ * @param metadata.column - Column number
435
+ * @param metadata.mode - Build mode (dev/prod)
436
+ * @param metadata.buildId - Unique build identifier
437
+ * @returns {string} The generated token
438
+ */
439
+ generateRecoveryToken(metadata: {
440
+ component: string;
441
+ filePath: string;
442
+ line: number;
443
+ column: number;
444
+ mode: 'csr' | 'dev-only';
445
+ buildId: string;
446
+ }): string;
447
+ }
448
+
429
449
  /**
430
450
  * Property Type System for CSS Variable Auto-Compile.
431
451
  *
package/dist/index.d.ts CHANGED
@@ -1,137 +1,6 @@
1
1
  import * as t from '@babel/types';
2
2
  import * as CSS from 'csstype';
3
3
 
4
- /**
5
- * JSX Transform - Converts sz prop to className string.
6
- *
7
- * This module handles the transformation of csszyx object syntax into
8
- * Tailwind CSS class strings. It processes nested objects for variants
9
- * like hover, focus, etc.
10
- */
11
- /**
12
- * Represents a value in the sz object.
13
- * Can be a string, number, boolean, or nested object for variants.
14
- */
15
- type SzValue = string | number | boolean | SzObject;
16
- /**
17
- * Represents the sz object structure.
18
- * Keys are CSS property abbreviations, values can be primitives or nested objects.
19
- */
20
- interface SzObject {
21
- [key: string]: SzValue;
22
- }
23
- declare const PROPERTY_MAP: Record<string, string>;
24
- declare const SUGGESTION_MAP: Record<string, string>;
25
- declare const KNOWN_VARIANTS: Set<string>;
26
- declare const BOOLEAN_SHORTHANDS: Set<string>;
27
- /**
28
- * Represents the result of a transformation.
29
- */
30
- interface TransformResult {
31
- className: string;
32
- attributes: Record<string, string>;
33
- }
34
- /**
35
- * Transforms a csszyx sz object into a Tailwind CSS className string and extracted attributes.
36
- *
37
- * @param {SzObject} szProp - The sz object from JSX
38
- * @param {string} prefix - Variant prefix for nested properties
39
- * @param {Record<string, string>} [mangleMap] - Optional map for property name mangling
40
- * @returns {TransformResult} The transformation result
41
- */
42
- declare function transform(szProp: SzObject, prefix?: string, mangleMap?: Record<string, string>): TransformResult;
43
- /**
44
- * Validates that an sz prop object is valid.
45
- *
46
- * @param {unknown} szProp - The value to validate
47
- * @returns {boolean} True if valid, false otherwise
48
- */
49
- declare function isValidSzProp(szProp: unknown): szProp is SzObject;
50
- /**
51
- * Normalizes a className string by removing extra whitespace.
52
- *
53
- * @param {string} className - The className string to normalize
54
- * @returns {string} The normalized className string
55
- */
56
- declare function normalizeClassName(className: string): string;
57
-
58
- /**
59
- * Transforms all sz props in a source code string into Tailwind classNames.
60
- *
61
- * @param {string} source - The source code to transform
62
- * @returns {object} Transformation result with code and metadata
63
- */
64
- declare function transformSourceCode(source: string): {
65
- code: string;
66
- transformed: boolean;
67
- usesRuntime: boolean;
68
- usesMerge: boolean;
69
- usesColorVar: boolean;
70
- classes: Set<string>;
71
- rawClassNames: Set<string>;
72
- diagnostics: string[];
73
- };
74
-
75
- /**
76
- * Core Compiler class for csszyx.
77
- *
78
- * This class manages the WASM lifecycle and provides high-performance
79
- * transformation methods. It falls back to JavaScript if WASM is not available.
80
- */
81
- declare class CsszyxCompiler {
82
- private static instance;
83
- private wasmLoaded;
84
- /**
85
- * Private constructor to enforce singleton pattern.
86
- */
87
- private constructor();
88
- /**
89
- * Gets the singleton instance of the compiler.
90
- *
91
- * @returns {CsszyxCompiler} The compiler instance.
92
- */
93
- static getInstance(): CsszyxCompiler;
94
- /**
95
- * Initializes the WASM core.
96
- *
97
- * @returns {Promise<void>} Resolves when WASM is ready.
98
- */
99
- init(): Promise<void>;
100
- /**
101
- * Transforms an sz object into Tailwind classes.
102
- *
103
- * @param {SzObject} sz - The object to transform.
104
- * @returns {string} The transformed class string.
105
- */
106
- transform(sz: SzObject): string;
107
- /**
108
- * Checks if the WASM core is currently active.
109
- *
110
- * @returns {boolean} True if WASM is loaded.
111
- */
112
- isWasmActive(): boolean;
113
- /**
114
- * Generates a recovery token using WASM or JS fallback.
115
- *
116
- * @param {object} metadata - Token metadata
117
- * @param metadata.component - Component name
118
- * @param metadata.filePath - File path source
119
- * @param metadata.line - Line number
120
- * @param metadata.column - Column number
121
- * @param metadata.mode - Build mode (dev/prod)
122
- * @param metadata.buildId - Unique build identifier
123
- * @returns {string} The generated token
124
- */
125
- generateRecoveryToken(metadata: {
126
- component: string;
127
- filePath: string;
128
- line: number;
129
- column: number;
130
- mode: 'csr' | 'dev-only';
131
- buildId: string;
132
- }): string;
133
- }
134
-
135
4
  /**
136
5
  * Valid recovery modes for szRecover attribute.
137
6
  */
@@ -426,6 +295,157 @@ declare function validateManifest(manifest: unknown): {
426
295
  error?: string;
427
296
  };
428
297
 
298
+ /**
299
+ * JSX Transform - Converts sz prop to className string.
300
+ *
301
+ * This module handles the transformation of csszyx object syntax into
302
+ * Tailwind CSS class strings. It processes nested objects for variants
303
+ * like hover, focus, etc.
304
+ */
305
+ /**
306
+ * Represents a value in the sz object.
307
+ * Can be a string, number, boolean, or nested object for variants.
308
+ */
309
+ type SzValue = string | number | boolean | SzObject;
310
+ /**
311
+ * Represents the sz object structure.
312
+ * Keys are CSS property abbreviations, values can be primitives or nested objects.
313
+ */
314
+ interface SzObject {
315
+ [key: string]: SzValue;
316
+ }
317
+ declare const PROPERTY_MAP: Record<string, string>;
318
+ declare const SUGGESTION_MAP: Record<string, string>;
319
+ declare const KNOWN_VARIANTS: Set<string>;
320
+ declare const BOOLEAN_SHORTHANDS: Set<string>;
321
+ /**
322
+ * Represents the result of a transformation.
323
+ */
324
+ interface TransformResult {
325
+ className: string;
326
+ attributes: Record<string, string>;
327
+ }
328
+ /**
329
+ * Transforms a csszyx sz object into a Tailwind CSS className string and extracted attributes.
330
+ *
331
+ * @param {SzObject} szProp - The sz object from JSX
332
+ * @param {string} prefix - Variant prefix for nested properties
333
+ * @param {Record<string, string>} [mangleMap] - Optional map for property name mangling
334
+ * @returns {TransformResult} The transformation result
335
+ */
336
+ declare function transform(szProp: SzObject, prefix?: string, mangleMap?: Record<string, string>): TransformResult;
337
+ /**
338
+ * Validates that an sz prop object is valid.
339
+ *
340
+ * @param {unknown} szProp - The value to validate
341
+ * @returns {boolean} True if valid, false otherwise
342
+ */
343
+ declare function isValidSzProp(szProp: unknown): szProp is SzObject;
344
+ /**
345
+ * Normalizes a className string by removing extra whitespace.
346
+ *
347
+ * @param {string} className - The className string to normalize
348
+ * @returns {string} The normalized className string
349
+ */
350
+ declare function normalizeClassName(className: string): string;
351
+
352
+ /**
353
+ * Options for {@link transformSourceCode}.
354
+ */
355
+ interface TransformSourceCodeOptions {
356
+ /**
357
+ * Override the AST node budget. Files larger than this throw
358
+ * {@link ASTBudgetExceededError}. Defaults to {@link AST_BUDGET} (50 000).
359
+ * Useful for repos with legitimately large generated files (json-as-ts
360
+ * fixtures, GraphQL schema snapshots) that exceed the default cap but
361
+ * are still safe to transform.
362
+ */
363
+ astBudget?: number;
364
+ }
365
+ /**
366
+ * Transforms all sz props in a source code string into Tailwind classNames.
367
+ *
368
+ * @param {string} source - The source code to transform
369
+ * @param {string} [filename] - Source filename, used in error messages and
370
+ * passed to Babel as the parser filename. Defaults to a placeholder.
371
+ * @param {TransformSourceCodeOptions} [options] - Optional overrides
372
+ * (currently: `astBudget` to raise/lower the per-file node cap).
373
+ * @returns {object} Transformation result with code and metadata
374
+ * @throws {ASTBudgetExceededError} when the file's AST exceeds the
375
+ * effective budget (`options.astBudget` or {@link AST_BUDGET}).
376
+ */
377
+ declare function transformSourceCode(source: string, filename?: string, options?: TransformSourceCodeOptions): {
378
+ code: string;
379
+ transformed: boolean;
380
+ usesRuntime: boolean;
381
+ usesMerge: boolean;
382
+ usesColorVar: boolean;
383
+ classes: Set<string>;
384
+ rawClassNames: Set<string>;
385
+ diagnostics: string[];
386
+ recoveryTokens: Map<string, TokenData>;
387
+ };
388
+
389
+ /**
390
+ * Core Compiler class for csszyx.
391
+ *
392
+ * This class manages the WASM lifecycle and provides high-performance
393
+ * transformation methods. It falls back to JavaScript if WASM is not available.
394
+ */
395
+ declare class CsszyxCompiler {
396
+ private static instance;
397
+ private wasmLoaded;
398
+ /**
399
+ * Private constructor to enforce singleton pattern.
400
+ */
401
+ private constructor();
402
+ /**
403
+ * Gets the singleton instance of the compiler.
404
+ *
405
+ * @returns {CsszyxCompiler} The compiler instance.
406
+ */
407
+ static getInstance(): CsszyxCompiler;
408
+ /**
409
+ * Initializes the WASM core.
410
+ *
411
+ * @returns {Promise<void>} Resolves when WASM is ready.
412
+ */
413
+ init(): Promise<void>;
414
+ /**
415
+ * Transforms an sz object into Tailwind classes.
416
+ *
417
+ * @param {SzObject} sz - The object to transform.
418
+ * @returns {string} The transformed class string.
419
+ */
420
+ transform(sz: SzObject): string;
421
+ /**
422
+ * Checks if the WASM core is currently active.
423
+ *
424
+ * @returns {boolean} True if WASM is loaded.
425
+ */
426
+ isWasmActive(): boolean;
427
+ /**
428
+ * Generates a recovery token using WASM or JS fallback.
429
+ *
430
+ * @param {object} metadata - Token metadata
431
+ * @param metadata.component - Component name
432
+ * @param metadata.filePath - File path source
433
+ * @param metadata.line - Line number
434
+ * @param metadata.column - Column number
435
+ * @param metadata.mode - Build mode (dev/prod)
436
+ * @param metadata.buildId - Unique build identifier
437
+ * @returns {string} The generated token
438
+ */
439
+ generateRecoveryToken(metadata: {
440
+ component: string;
441
+ filePath: string;
442
+ line: number;
443
+ column: number;
444
+ mode: 'csr' | 'dev-only';
445
+ buildId: string;
446
+ }): string;
447
+ }
448
+
429
449
  /**
430
450
  * Property Type System for CSS Variable Auto-Compile.
431
451
  *
package/dist/index.js CHANGED
@@ -270,6 +270,39 @@ function stripInvalidColorStrings(sz) {
270
270
  import * as babel from "@babel/core";
271
271
  import * as t from "@babel/types";
272
272
 
273
+ // src/ast-budget.ts
274
+ var AST_BUDGET = 5e4;
275
+ var ASTBudgetExceededError = class extends Error {
276
+ /**
277
+ *
278
+ * @param filename Source filename if known, otherwise `undefined`.
279
+ * @param nodeCount Node count at the point traversal was aborted.
280
+ * @param budget Effective budget that was exceeded. Defaults to
281
+ * {@link AST_BUDGET} for backwards compatibility with callers that
282
+ * don't pass an override.
283
+ */
284
+ constructor(filename, nodeCount, budget = AST_BUDGET) {
285
+ const where = filename ?? "<anonymous>";
286
+ super(
287
+ `[csszyx] AST budget exceeded: ${where} has more than ${budget} nodes (traversal aborted at ${nodeCount}). Files this large are almost always machine-generated and should be excluded from sz transformation. Either exclude the file from the plugin (Vite: \`csszyx({ exclude: [/large-data\\.ts$/] })\`), or raise the limit globally with \`csszyx({ build: { astBudgetLimit: 100_000 } })\`.`
288
+ );
289
+ this.name = "ASTBudgetExceededError";
290
+ this.filename = filename;
291
+ this.nodeCount = nodeCount;
292
+ this.budget = budget;
293
+ }
294
+ };
295
+
296
+ // src/recovery-tokens.ts
297
+ import { createHash } from "crypto";
298
+ function generateInlineRecoveryToken(filename, line, column, elementType) {
299
+ const input = `${filename}:${line}:${column}:${elementType}`;
300
+ return createHash("sha256").update(input).digest("hex").substring(0, 12);
301
+ }
302
+ function isValidInlineRecoveryMode(value) {
303
+ return value === "csr" || value === "dev-only";
304
+ }
305
+
273
306
  // src/transform-core.ts
274
307
  var PROPERTY_MAP = {
275
308
  // Background
@@ -2262,7 +2295,8 @@ function normalizeClassName(className) {
2262
2295
  }
2263
2296
 
2264
2297
  // src/transform.ts
2265
- function transformSourceCode(source) {
2298
+ function transformSourceCode(source, filename, options) {
2299
+ const astBudget = options?.astBudget ?? AST_BUDGET;
2266
2300
  let usesRuntime = false;
2267
2301
  let usesMerge = false;
2268
2302
  let usesColorVar = false;
@@ -2270,12 +2304,13 @@ function transformSourceCode(source) {
2270
2304
  const collectedClasses = /* @__PURE__ */ new Set();
2271
2305
  const rawClassNames = /* @__PURE__ */ new Set();
2272
2306
  const diagnostics = [];
2307
+ const recoveryTokens = /* @__PURE__ */ new Map();
2273
2308
  if (!source.includes("sz")) {
2274
- return { code: source, transformed: false, usesRuntime: false, usesMerge: false, usesColorVar: false, classes: collectedClasses, rawClassNames, diagnostics };
2309
+ return { code: source, transformed: false, usesRuntime: false, usesMerge: false, usesColorVar: false, classes: collectedClasses, rawClassNames, diagnostics, recoveryTokens };
2275
2310
  }
2276
2311
  try {
2277
2312
  const result = babel.transformSync(source, {
2278
- filename: "file.tsx",
2313
+ filename: filename ?? "file.tsx",
2279
2314
  // Enable TS/JSX parsing
2280
2315
  ast: true,
2281
2316
  code: true,
@@ -2287,6 +2322,21 @@ function transformSourceCode(source) {
2287
2322
  plugins: [
2288
2323
  function() {
2289
2324
  return {
2325
+ // Budget guard runs in `pre` (before the visitor pass)
2326
+ // so it short-circuits pathologically large files
2327
+ // before any sz transform work begins, and doesn't
2328
+ // interfere with the JSXAttribute handler below.
2329
+ pre(file) {
2330
+ let nodeCount = 0;
2331
+ babel.traverse(file.ast, {
2332
+ enter() {
2333
+ nodeCount++;
2334
+ if (nodeCount > astBudget) {
2335
+ throw new ASTBudgetExceededError(filename, nodeCount, astBudget);
2336
+ }
2337
+ }
2338
+ });
2339
+ },
2290
2340
  visitor: {
2291
2341
  JSXAttribute(path) {
2292
2342
  const attrName = t.isJSXIdentifier(path.node.name) ? path.node.name.name : "";
@@ -2301,6 +2351,50 @@ function transformSourceCode(source) {
2301
2351
  }
2302
2352
  return;
2303
2353
  }
2354
+ if (attrName === "szRecover") {
2355
+ const recoverValue = path.node.value;
2356
+ if (!t.isStringLiteral(recoverValue)) {
2357
+ diagnostics.push(
2358
+ `[csszyx] szRecover at ${filename ?? "<anonymous>"}: only string-literal values ("csr" | "dev-only") are supported. Dynamic values disable token emission for this element.`
2359
+ );
2360
+ return;
2361
+ }
2362
+ if (!isValidInlineRecoveryMode(recoverValue.value)) {
2363
+ diagnostics.push(
2364
+ `[csszyx] szRecover at ${filename ?? "<anonymous>"}: unknown mode "${recoverValue.value}" \u2014 expected "csr" or "dev-only". Token emission skipped.`
2365
+ );
2366
+ return;
2367
+ }
2368
+ const opening = path.parentPath;
2369
+ if (!opening?.isJSXOpeningElement()) {
2370
+ return;
2371
+ }
2372
+ const alreadyTagged = opening.node.attributes.some(
2373
+ (attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && attr.name.name === "data-sz-recovery-token"
2374
+ );
2375
+ if (alreadyTagged) {
2376
+ return;
2377
+ }
2378
+ const loc = path.node.loc;
2379
+ const elementType = t.isJSXIdentifier(opening.node.name) ? opening.node.name.name : t.isJSXMemberExpression(opening.node.name) ? "<member>" : "<unknown>";
2380
+ const line = loc?.start.line ?? 0;
2381
+ const column = loc?.start.column ?? 0;
2382
+ const file = filename ?? "file.tsx";
2383
+ const token = generateInlineRecoveryToken(file, line, column, elementType);
2384
+ opening.node.attributes.push(
2385
+ t.jsxAttribute(
2386
+ t.jsxIdentifier("data-sz-recovery-token"),
2387
+ t.stringLiteral(token)
2388
+ )
2389
+ );
2390
+ recoveryTokens.set(token, {
2391
+ mode: recoverValue.value,
2392
+ component: elementType,
2393
+ path: `${file}:${line}:${column}`
2394
+ });
2395
+ transformed = true;
2396
+ return;
2397
+ }
2304
2398
  if (attrName !== "sz") {
2305
2399
  return;
2306
2400
  }
@@ -2803,11 +2897,15 @@ function transformSourceCode(source) {
2803
2897
  usesColorVar,
2804
2898
  classes: collectedClasses,
2805
2899
  rawClassNames,
2806
- diagnostics
2900
+ diagnostics,
2901
+ recoveryTokens
2807
2902
  };
2808
2903
  } catch (e) {
2904
+ if (e instanceof ASTBudgetExceededError) {
2905
+ throw e;
2906
+ }
2809
2907
  console.warn("[csszyx] AST transform failed, falling back to original code:", e);
2810
- return { code: source, transformed: false, usesRuntime: false, usesMerge: false, usesColorVar: false, classes: collectedClasses, rawClassNames, diagnostics };
2908
+ return { code: source, transformed: false, usesRuntime: false, usesMerge: false, usesColorVar: false, classes: collectedClasses, rawClassNames, diagnostics, recoveryTokens };
2811
2909
  }
2812
2910
  }
2813
2911
  function parseStyleStringToObjectExpr(styleStr) {
@@ -3374,7 +3472,7 @@ function injectRecoveryToken(attributes, token) {
3374
3472
  }
3375
3473
 
3376
3474
  // src/manifest.ts
3377
- import { createHash } from "crypto";
3475
+ import { createHash as createHash2 } from "crypto";
3378
3476
  var ManifestBuilder = class {
3379
3477
  /**
3380
3478
  * Creates a new manifest builder.
@@ -3447,7 +3545,7 @@ var ManifestBuilder = class {
3447
3545
  sortedTokens[key] = tokens[key];
3448
3546
  }
3449
3547
  const content = JSON.stringify(sortedTokens);
3450
- return createHash("sha256").update(content).digest("hex");
3548
+ return createHash2("sha256").update(content).digest("hex");
3451
3549
  }
3452
3550
  /**
3453
3551
  * Builds the final recovery manifest.
@@ -3657,7 +3755,7 @@ function hoistCSSVariables(usages, parentMap) {
3657
3755
  }
3658
3756
  function buildParentMap(ast) {
3659
3757
  const map = /* @__PURE__ */ new Map();
3660
- function traverse(node, parent) {
3758
+ function traverse2(node, parent) {
3661
3759
  if (parent) {
3662
3760
  map.set(node, parent);
3663
3761
  }
@@ -3667,16 +3765,16 @@ function buildParentMap(ast) {
3667
3765
  if (Array.isArray(value)) {
3668
3766
  for (const item of value) {
3669
3767
  if (item && typeof item === "object" && "type" in item) {
3670
- traverse(item, node);
3768
+ traverse2(item, node);
3671
3769
  }
3672
3770
  }
3673
3771
  } else if ("type" in value) {
3674
- traverse(value, node);
3772
+ traverse2(value, node);
3675
3773
  }
3676
3774
  }
3677
3775
  }
3678
3776
  }
3679
- traverse(ast);
3777
+ traverse2(ast);
3680
3778
  return map;
3681
3779
  }
3682
3780
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@csszyx/compiler",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Core compiler and transformation logic for csszyx",
5
5
  "keywords": [
6
6
  "csszyx",
@@ -47,7 +47,7 @@
47
47
  "@babel/core": "^7.23.7",
48
48
  "@babel/types": "^7.23.6",
49
49
  "@babel/traverse": "^7.23.7",
50
- "@csszyx/core": "0.4.0"
50
+ "@csszyx/core": "0.6.0"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/babel__core": "^7.20.5",