diff --git a/.changeset/full-tips-raise.md b/.changeset/full-tips-raise.md new file mode 100644 index 000000000..d1d640897 --- /dev/null +++ b/.changeset/full-tips-raise.md @@ -0,0 +1,5 @@ +--- +'@fuzdev/fuz_css': minor +--- + +switch to blake3 hashing, add optional peer dep `@fuzdev/blake3_wasm` diff --git a/package-lock.json b/package-lock.json index c60a1c966..c3c4c1fee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,15 +10,16 @@ "license": "MIT", "devDependencies": { "@changesets/changelog-git": "^0.2.1", + "@fuzdev/blake3_wasm": "^0.1.0", "@fuzdev/fuz_code": "^0.45.1", "@fuzdev/fuz_ui": "^0.185.2", - "@fuzdev/fuz_util": "^0.52.1", + "@fuzdev/fuz_util": "^0.53.0", "@fuzdev/gro": "^0.196.0", "@jridgewell/trace-mapping": "^0.3.31", "@ryanatkn/eslint-config": "^0.9.0", - "@sveltejs/acorn-typescript": "^1.0.8", + "@sveltejs/acorn-typescript": "^1.0.9", "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.50.1", + "@sveltejs/kit": "^2.53.4", "@sveltejs/package": "^2.5.7", "@sveltejs/vite-plugin-svelte": "^6.2.4", "@types/estree": "^1.0.8", @@ -31,9 +32,9 @@ "magic-string": "^0.30.21", "prettier": "^3.7.4", "prettier-plugin-svelte": "^3.4.1", - "svelte": "^5.49.1", - "svelte-check": "^4.3.6", - "svelte2tsx": "^0.7.47", + "svelte": "^5.53.7", + "svelte-check": "^4.4.4", + "svelte2tsx": "^0.7.51", "tslib": "^2.8.1", "typescript": "^5.9.3", "typescript-eslint": "^8.48.1", @@ -48,6 +49,7 @@ "url": "https://www.ryanatkn.com/funding" }, "peerDependencies": { + "@fuzdev/blake3_wasm": "^0.1.0", "@fuzdev/fuz_util": ">=0.52.0", "@fuzdev/gro": ">=0.195.0", "@sveltejs/acorn-typescript": "^1", @@ -57,6 +59,9 @@ "zod": "^4" }, "peerDependenciesMeta": { + "@fuzdev/blake3_wasm": { + "optional": true + }, "@fuzdev/fuz_util": { "optional": true }, @@ -756,6 +761,19 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fuzdev/blake3_wasm": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@fuzdev/blake3_wasm/-/blake3_wasm-0.1.0.tgz", + "integrity": "sha512-EU5uUcSX55Li3IXi1NiBDoVlxCN8ip9wqAhVZlMBEUa+cFQtLL6Z8GpYjlWy0KosLmxy2Z9WQv49PAkiAzFppg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://www.ryanatkn.com/funding" + } + }, "node_modules/@fuzdev/fuz_code": { "version": "0.45.1", "resolved": "https://registry.npmjs.org/@fuzdev/fuz_code/-/fuz_code-0.45.1.tgz", @@ -893,9 +911,9 @@ } }, "node_modules/@fuzdev/fuz_util": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@fuzdev/fuz_util/-/fuz_util-0.52.1.tgz", - "integrity": "sha512-kOqhYkMi3liA9hd69/TGgUiBElfSwnijkxYg31Pea6YyLGqL0pd9uNj7mSHsCaVBUuV5hmVF1FbaBMO5fIeHxg==", + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@fuzdev/fuz_util/-/fuz_util-0.53.0.tgz", + "integrity": "sha512-ydiB85grq38hoktAAm4Ci4y3z++WRv1EuxsN0M774V5u3gRfvz9vTAOpwqTwfxEJDCrTrTPyGSVCNmfMpoBt5w==", "dev": true, "license": "MIT", "engines": { @@ -905,6 +923,7 @@ "url": "https://www.ryanatkn.com/funding" }, "peerDependencies": { + "@fuzdev/blake3_wasm": "^0.1.0", "@types/estree": "^1", "@types/node": "^24", "esm-env": "^1.2.2", @@ -912,6 +931,9 @@ "zod": "^4.0.14" }, "peerDependenciesMeta": { + "@fuzdev/blake3_wasm": { + "optional": true + }, "@types/estree": { "optional": true }, @@ -1719,9 +1741,9 @@ "license": "MIT" }, "node_modules/@sveltejs/acorn-typescript": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.8.tgz", - "integrity": "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", + "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1739,9 +1761,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.50.1", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.50.1.tgz", - "integrity": "sha512-XRHD2i3zC4ukhz2iCQzO4mbsts081PAZnnMAQ7LNpWeYgeBmwMsalf0FGSwhFXBbtr2XViPKnFJBDCckWqrsLw==", + "version": "2.53.4", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.53.4.tgz", + "integrity": "sha512-iAIPEahFgDJJyvz8g0jP08KvqnM6JvdW8YfsygZ+pMeMvyM2zssWMltcsotETvjSZ82G3VlitgDtBIvpQSZrTA==", "dev": true, "license": "MIT", "dependencies": { @@ -1750,13 +1772,12 @@ "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", - "devalue": "^5.6.2", + "devalue": "^5.6.3", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", - "sade": "^1.8.1", - "set-cookie-parser": "^2.6.0", + "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "bin": { @@ -1767,10 +1788,10 @@ }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", - "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3", - "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "peerDependenciesMeta": { "@opentelemetry/api": { @@ -1933,6 +1954,13 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.48.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.1.tgz", @@ -2375,9 +2403,9 @@ "license": "Python-2.0" }, "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2608,9 +2636,9 @@ } }, "node_modules/devalue": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz", - "integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.3.tgz", + "integrity": "sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==", "dev": true, "license": "MIT" }, @@ -3776,9 +3804,9 @@ } }, "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.0.1.tgz", + "integrity": "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==", "dev": true, "license": "MIT" }, @@ -3878,9 +3906,9 @@ } }, "node_modules/svelte": { - "version": "5.49.1", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.49.1.tgz", - "integrity": "sha512-jj95WnbKbXsXXngYj28a4zx8jeZx50CN/J4r0CEeax2pbfdsETv/J1K8V9Hbu3DCXnpHz5qAikICuxEooi7eNQ==", + "version": "5.53.7", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.53.7.tgz", + "integrity": "sha512-uxck1KI7JWtlfP3H6HOWi/94soAl23jsGJkBzN2BAWcQng0+lTrRNhxActFqORgnO9BHVd1hKJhG+ljRuIUWfQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3888,11 +3916,12 @@ "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", - "aria-query": "^5.3.1", + "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", - "devalue": "^5.6.2", + "devalue": "^5.6.3", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", @@ -3905,9 +3934,9 @@ } }, "node_modules/svelte-check": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.6.tgz", - "integrity": "sha512-uBkz96ElE3G4pt9E1Tw0xvBfIUQkeH794kDQZdAUk795UVMr+NJZpuFSS62vcmO/DuSalK83LyOwhgWq8YGU1Q==", + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.4.tgz", + "integrity": "sha512-F1pGqXc710Oi/wTI4d/x7d6lgPwwfx1U6w3Q35n4xsC2e8C/yN2sM1+mWxjlMcpAfWucjlq4vPi+P4FZ8a14sQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3959,9 +3988,9 @@ } }, "node_modules/svelte2tsx": { - "version": "0.7.47", - "resolved": "https://registry.npmjs.org/svelte2tsx/-/svelte2tsx-0.7.47.tgz", - "integrity": "sha512-1aw/MFKVPM96OBevJdC12do2an9t5Zwr3Va9amLgTLpJje36ibD1iIHpuqCYWUrdR9vw6g6btKGQPmsqE8ZYCw==", + "version": "0.7.51", + "resolved": "https://registry.npmjs.org/svelte2tsx/-/svelte2tsx-0.7.51.tgz", + "integrity": "sha512-YbVMQi5LtQkVGOMdATTY8v3SMtkNjzYtrVDGaN3Bv+0LQ47tGXu/Oc8ryTkcYuEJWTZFJ8G2+2I8ORcQVGt9Ag==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 0d74b30d2..8594bfd17 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "node": ">=22.15" }, "peerDependencies": { + "@fuzdev/blake3_wasm": "^0.1.0", "@fuzdev/fuz_util": ">=0.52.0", "@fuzdev/gro": ">=0.195.0", "@sveltejs/acorn-typescript": "^1", @@ -39,6 +40,9 @@ "zod": "^4" }, "peerDependenciesMeta": { + "@fuzdev/blake3_wasm": { + "optional": true + }, "@fuzdev/fuz_util": { "optional": true }, @@ -63,15 +67,16 @@ }, "devDependencies": { "@changesets/changelog-git": "^0.2.1", + "@fuzdev/blake3_wasm": "^0.1.0", "@fuzdev/fuz_code": "^0.45.1", "@fuzdev/fuz_ui": "^0.185.2", - "@fuzdev/fuz_util": "^0.52.1", + "@fuzdev/fuz_util": "^0.53.0", "@fuzdev/gro": "^0.196.0", "@jridgewell/trace-mapping": "^0.3.31", "@ryanatkn/eslint-config": "^0.9.0", - "@sveltejs/acorn-typescript": "^1.0.8", + "@sveltejs/acorn-typescript": "^1.0.9", "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.50.1", + "@sveltejs/kit": "^2.53.4", "@sveltejs/package": "^2.5.7", "@sveltejs/vite-plugin-svelte": "^6.2.4", "@types/estree": "^1.0.8", @@ -84,9 +89,9 @@ "magic-string": "^0.30.21", "prettier": "^3.7.4", "prettier-plugin-svelte": "^3.4.1", - "svelte": "^5.49.1", - "svelte-check": "^4.3.6", - "svelte2tsx": "^0.7.47", + "svelte": "^5.53.7", + "svelte-check": "^4.4.4", + "svelte2tsx": "^0.7.51", "tslib": "^2.8.1", "typescript": "^5.9.3", "typescript-eslint": "^8.48.1", diff --git a/src/lib/css_ruleset_parser.ts b/src/lib/css_ruleset_parser.ts index 6fe0865c6..34fa7eb59 100644 --- a/src/lib/css_ruleset_parser.ts +++ b/src/lib/css_ruleset_parser.ts @@ -63,7 +63,7 @@ export const parse_ruleset = (css: string): ParsedRuleset => { * @mutates rules - pushes extracted rules to the array */ const walk_css_children = ( - node: Omit | AST.CSS.Atrule, + node: Omit | AST.CSS.Atrule, original_css: string, rules: Array, ): void => { @@ -72,7 +72,6 @@ const walk_css_children = ( for (const child of children) { if (child.type === 'Rule') { extract_rule(child, original_css, rules); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition } else if (child.type === 'Atrule' && child.block) { // Recurse into at-rules (like @media) - rules are in block.children walk_css_block(child.block, original_css, rules); diff --git a/src/lib/gen_fuz_css.ts b/src/lib/gen_fuz_css.ts index cceaeaa6d..aad549cae 100644 --- a/src/lib/gen_fuz_css.ts +++ b/src/lib/gen_fuz_css.ts @@ -168,12 +168,12 @@ export const gen_fuz_css = (options: GenFuzCssOptions = {}): Gen => { if (!style_rule_index) { if (typeof base_css === 'string') { // Custom CSS string provided (replacement) - style_rule_index = await create_style_rule_index(base_css); + style_rule_index = create_style_rule_index(base_css); } else if (typeof base_css === 'function') { // Callback to modify default CSS const default_css = await load_default_style_css(deps); const modified_css = base_css(default_css); - style_rule_index = await create_style_rule_index(modified_css); + style_rule_index = create_style_rule_index(modified_css); } else { // Use default style.css (undefined or null - null handled by include_base flag) style_rule_index = await load_style_rule_index(deps); diff --git a/src/lib/style_rule_parser.ts b/src/lib/style_rule_parser.ts index 571e900c4..ccd659234 100644 --- a/src/lib/style_rule_parser.ts +++ b/src/lib/style_rule_parser.ts @@ -10,7 +10,7 @@ */ import {parseCss, type AST} from 'svelte/compiler'; -import {hash_secure} from '@fuzdev/fuz_util/hash.js'; +import {hash_blake3} from '@fuzdev/fuz_util/hash_blake3.js'; import {extract_css_variables} from './css_variable_utils.js'; import type {CacheDeps} from './deps.js'; @@ -503,7 +503,7 @@ export const load_style_rule_index = async ( if (css === null) { throw new Error(`Failed to read style.css from ${path}`); } - const content_hash = await hash_secure(css); + const content_hash = hash_blake3(css); return parse_style_css(css, content_hash); }; @@ -512,10 +512,10 @@ export const load_style_rule_index = async ( * Use this to parse user-provided base styles instead of loading from file. * * @param css - raw CSS string to parse - * @returns promise resolving to `StyleRuleIndex` + * @returns `StyleRuleIndex` */ -export const create_style_rule_index = async (css: string): Promise => { - const content_hash = await hash_secure(css); +export const create_style_rule_index = (css: string): StyleRuleIndex => { + const content_hash = hash_blake3(css); return parse_style_css(css, content_hash); }; diff --git a/src/lib/vite_plugin_fuz_css.ts b/src/lib/vite_plugin_fuz_css.ts index 67a01ea96..58a97bf2d 100644 --- a/src/lib/vite_plugin_fuz_css.ts +++ b/src/lib/vite_plugin_fuz_css.ts @@ -26,7 +26,7 @@ import type {Logger as ViteLogger, Plugin, ViteDevServer} from 'vite'; import {join} from 'node:path'; -import {hash_secure} from '@fuzdev/fuz_util/hash.js'; +import {hash_blake3} from '@fuzdev/fuz_util/hash_blake3.js'; import {extract_css_classes_with_locations} from './css_class_extractor.js'; import {type Diagnostic, format_diagnostic, CssGenerationError} from './diagnostics.js'; @@ -200,12 +200,12 @@ export const vite_plugin_fuz_css = (options: VitePluginFuzCssOptions = {}): Plug // Load style rule index based on base_css option if (typeof base_css === 'string') { // Custom CSS string (replacement) - style_rule_index = await create_style_rule_index(base_css); + style_rule_index = create_style_rule_index(base_css); } else if (typeof base_css === 'function') { // Callback to modify default CSS const default_css = await load_default_style_css(deps); const modified_css = base_css(default_css); - style_rule_index = await create_style_rule_index(modified_css); + style_rule_index = create_style_rule_index(modified_css); } else { // Use default style.css (undefined or null) style_rule_index = await load_style_rule_index(deps); @@ -529,7 +529,7 @@ export {}; } // Compute content hash - const hash = await hash_secure(code); + const hash = hash_blake3(code); const existing_hash = hashes.get(id); // Check if unchanged diff --git a/src/routes/library.json b/src/routes/library.json index c7607c66e..dd7e3da7e 100644 --- a/src/routes/library.json +++ b/src/routes/library.json @@ -41,6 +41,7 @@ "node": ">=22.15" }, "peerDependencies": { + "@fuzdev/blake3_wasm": "^0.1.0", "@fuzdev/fuz_util": ">=0.52.0", "@fuzdev/gro": ">=0.195.0", "@sveltejs/acorn-typescript": "^1", @@ -50,6 +51,9 @@ "zod": "^4" }, "peerDependenciesMeta": { + "@fuzdev/blake3_wasm": { + "optional": true + }, "@fuzdev/fuz_util": { "optional": true }, @@ -74,15 +78,16 @@ }, "devDependencies": { "@changesets/changelog-git": "^0.2.1", + "@fuzdev/blake3_wasm": "^0.1.0", "@fuzdev/fuz_code": "^0.45.1", "@fuzdev/fuz_ui": "^0.185.2", - "@fuzdev/fuz_util": "^0.52.1", + "@fuzdev/fuz_util": "^0.53.0", "@fuzdev/gro": "^0.196.0", "@jridgewell/trace-mapping": "^0.3.31", "@ryanatkn/eslint-config": "^0.9.0", - "@sveltejs/acorn-typescript": "^1.0.8", + "@sveltejs/acorn-typescript": "^1.0.9", "@sveltejs/adapter-static": "^3.0.10", - "@sveltejs/kit": "^2.50.1", + "@sveltejs/kit": "^2.53.4", "@sveltejs/package": "^2.5.7", "@sveltejs/vite-plugin-svelte": "^6.2.4", "@types/estree": "^1.0.8", @@ -95,9 +100,9 @@ "magic-string": "^0.30.21", "prettier": "^3.7.4", "prettier-plugin-svelte": "^3.4.1", - "svelte": "^5.49.1", - "svelte-check": "^4.3.6", - "svelte2tsx": "^0.7.47", + "svelte": "^5.53.7", + "svelte-check": "^4.4.4", + "svelte2tsx": "^0.7.51", "tslib": "^2.8.1", "typescript": "^5.9.3", "typescript-eslint": "^8.48.1", @@ -2491,7 +2496,7 @@ "name": "is_single_selector_ruleset", "kind": "function", "doc_comment": "Checks if a ruleset has only a single simple selector (just the class name).\nUsed to detect rulesets that could be converted to declaration format.", - "source_line": 141, + "source_line": 140, "type_signature": "(rules: ParsedRule[], escaped_class_name: string): boolean", "return_type": "boolean", "return_description": "true if there's exactly one rule with selector \".class_name\"", @@ -2512,7 +2517,7 @@ "name": "ruleset_contains_class", "kind": "function", "doc_comment": "Checks if any selector in the ruleset contains the expected class name.\nUsed to validate that ruleset definitions match their key.", - "source_line": 164, + "source_line": 163, "type_signature": "(rules: ParsedRule[], escaped_class_name: string): boolean", "return_type": "boolean", "return_description": "true if at least one selector contains \".class_name\"", @@ -2533,7 +2538,7 @@ "name": "extract_css_comment", "kind": "function", "doc_comment": "Extracts the CSS comment from a ruleset (if any).\nLooks for comments before the first rule.", - "source_line": 181, + "source_line": 180, "type_signature": "(css: string, rules: ParsedRule[]): string | null", "return_type": "string | null", "return_description": "comment text without delimiters, or null if no comment", @@ -2554,7 +2559,7 @@ "name": "SkippedModifierInfo", "kind": "type", "doc_comment": "Information about a modifier that was skipped for a selector during ruleset modification.\nThe selector is still included in output, just without the conflicting modifier applied.", - "source_line": 205, + "source_line": 204, "type_signature": "SkippedModifierInfo", "properties": [ { @@ -2581,7 +2586,7 @@ "name": "ModifiedSelectorGroupResult", "kind": "type", "doc_comment": "Result from modifying a selector group with conflict detection.\n\nUses `| null` for skipped_modifiers to avoid allocating empty arrays.\nCallers should use a guard pattern: `if (result.skipped_modifiers) { ... }`", - "source_line": 220, + "source_line": 219, "type_signature": "ModifiedSelectorGroupResult", "properties": [ { @@ -2602,7 +2607,7 @@ "name": "ModifiedRulesetResult", "kind": "type", "doc_comment": "Result from generating a modified ruleset.\n\nUses `| null` for skipped_modifiers to avoid allocating empty arrays.\nCallers should use a guard pattern: `if (result.skipped_modifiers) { ... }`", - "source_line": 233, + "source_line": 232, "type_signature": "ModifiedRulesetResult", "properties": [ { @@ -2626,7 +2631,7 @@ "examples": [ "```ts\nsplit_selector_list('.a, .b') // → ['.a', '.b']\nsplit_selector_list('.a:not(.b), .c') // → ['.a:not(.b)', '.c']\nsplit_selector_list('.a[data-x=\"a,b\"], .c') // → ['.a[data-x=\"a,b\"]', '.c']\n```" ], - "source_line": 297, + "source_line": 296, "type_signature": "(selector_group: string): string[]", "return_type": "string[]", "parameters": [ @@ -2640,7 +2645,7 @@ "name": "find_compound_end", "kind": "function", "doc_comment": "Finds the end position of the compound selector containing the class at class_pos.\nA compound selector is a sequence of simple selectors without combinators.", - "source_line": 354, + "source_line": 353, "type_signature": "(selector: string, class_pos: number): number", "return_type": "number", "return_description": "position where state modifiers should be inserted (before any pseudo-element)", @@ -2664,7 +2669,7 @@ "examples": [ "```ts\nmodify_single_selector('.menu_item', 'menu_item', 'hover\\\\:menu_item', ':hover', '')\n// → '.hover\\\\:menu_item:hover'\n\nmodify_single_selector('.menu_item .icon', 'menu_item', 'hover\\\\:menu_item', ':hover', '')\n// → '.hover\\\\:menu_item:hover .icon'\n\nmodify_single_selector('.menu_item.selected', 'menu_item', 'hover\\\\:menu_item', ':hover', '')\n// → '.hover\\\\:menu_item.selected:hover'\n```" ], - "source_line": 445, + "source_line": 444, "type_signature": "(selector: string, original_class: string, new_class_escaped: string, state_css: string, pseudo_element_css: string): string", "return_type": "string", "return_description": "modified selector", @@ -2700,7 +2705,7 @@ "name": "modify_selector_group", "kind": "function", "doc_comment": "Modifies a selector list (comma-separated selectors) to add modifiers.\nHandles conflicts per-selector: if one selector in a list has a conflict,\nonly that selector skips the modifier; other selectors still get it.", - "source_line": 485, + "source_line": 484, "type_signature": "(selector_group: string, original_class: string, new_class_escaped: string, states_to_add: string[], pseudo_element_css: string): ModifiedSelectorGroupResult", "return_type": "ModifiedSelectorGroupResult", "return_description": "result with modified selector list and information about skipped modifiers", @@ -2736,7 +2741,7 @@ "name": "generate_modified_ruleset", "kind": "function", "doc_comment": "Generates CSS for a modified ruleset with applied modifiers.\n\nConflict handling is per-selector within selector lists:\n- For `.plain:hover, .plain:active` with `hover:` modifier, only `.plain:hover` skips\n the `:hover` addition; `.plain:active` still gets `:hover` appended.\n- For multiple states like `:hover:focus`, each is checked individually; conflicting\n states are skipped while non-conflicting ones are still applied.", - "source_line": 564, + "source_line": 563, "type_signature": "(original_ruleset: string, original_class: string, new_class_escaped: string, state_css: string, pseudo_element_css: string, media_wrapper: string | null, ancestor_wrapper: string | null): ModifiedRulesetResult", "return_type": "ModifiedRulesetResult", "return_description": "result with generated CSS and information about skipped modifiers", @@ -3699,9 +3704,9 @@ "kind": "function", "doc_comment": "Creates a `StyleRuleIndex` from a custom CSS string.\nUse this to parse user-provided base styles instead of loading from file.", "source_line": 517, - "type_signature": "(css: string): Promise", - "return_type": "Promise", - "return_description": "promise resolving to `StyleRuleIndex`", + "type_signature": "(css: string): StyleRuleIndex", + "return_type": "StyleRuleIndex", + "return_description": "`StyleRuleIndex`", "parameters": [ { "name": "css", diff --git a/src/test/style_rule_parser.custom.test.ts b/src/test/style_rule_parser.custom.test.ts index 5733d62ff..f2cb07cbf 100644 --- a/src/test/style_rule_parser.custom.test.ts +++ b/src/test/style_rule_parser.custom.test.ts @@ -16,38 +16,38 @@ import { describe('create_style_rule_index', () => { describe('basic parsing', () => { - test('parses simple rules', async () => { + test('parses simple rules', () => { const css = ` button { color: blue; } input { border: 1px solid; } `; - const index = await create_style_rule_index(css); + const index = create_style_rule_index(css); expect(index.rules.length).toBe(2); expect(index.by_element.has('button')).toBe(true); expect(index.by_element.has('input')).toBe(true); }); - test('indexes by element name', async () => { + test('indexes by element name', () => { const css = ` button { color: red; } button:hover { color: blue; } `; - const index = await create_style_rule_index(css); + const index = create_style_rule_index(css); const button_rules = index.by_element.get('button'); expect(button_rules?.length).toBe(2); }); - test('indexes by class name', async () => { + test('indexes by class name', () => { const css = ` .active { color: green; } button.primary { color: blue; } `; - const index = await create_style_rule_index(css); + const index = create_style_rule_index(css); expect(index.by_class.has('active')).toBe(true); expect(index.by_class.has('primary')).toBe(true); @@ -55,28 +55,28 @@ describe('create_style_rule_index', () => { }); describe('core rules', () => { - test('marks * rules as core', async () => { + test('marks * rules as core', () => { const css = `* { box-sizing: border-box; }`; - const index = await create_style_rule_index(css); + const index = create_style_rule_index(css); expect(index.rules[0]?.is_core).toBe(true); expect(index.rules[0]?.core_reason).toBe('universal'); }); - test('marks :root rules as core', async () => { + test('marks :root rules as core', () => { const css = `:root { font-size: 16px; }`; - const index = await create_style_rule_index(css); + const index = create_style_rule_index(css); expect(index.rules[0]?.is_core).toBe(true); expect(index.rules[0]?.core_reason).toBe('root'); }); - test('marks body rules as core', async () => { + test('marks body rules as core', () => { const css = `body { margin: 0; }`; - const index = await create_style_rule_index(css); + const index = create_style_rule_index(css); expect(index.rules[0]?.is_core).toBe(true); expect(index.rules[0]?.core_reason).toBe('body'); @@ -84,19 +84,19 @@ describe('create_style_rule_index', () => { }); describe('variable extraction', () => { - test('extracts CSS variables from rules', async () => { + test('extracts CSS variables from rules', () => { const css = `button { color: var(--btn_color); background: var(--btn_bg); }`; - const index = await create_style_rule_index(css); + const index = create_style_rule_index(css); expect(index.rules[0]?.variables_used.has('btn_color')).toBe(true); expect(index.rules[0]?.variables_used.has('btn_bg')).toBe(true); }); - test('extracts multiple variables from single property', async () => { + test('extracts multiple variables from single property', () => { const css = `div { box-shadow: var(--shadow_x) var(--shadow_y) var(--shadow_blur) var(--shadow_color); }`; - const index = await create_style_rule_index(css); + const index = create_style_rule_index(css); expect(index.rules[0]?.variables_used.size).toBe(4); expect(index.rules[0]?.variables_used.has('shadow_x')).toBe(true); @@ -105,60 +105,60 @@ describe('create_style_rule_index', () => { expect(index.rules[0]?.variables_used.has('shadow_color')).toBe(true); }); - test('extracts variables from pseudo-class rules', async () => { + test('extracts variables from pseudo-class rules', () => { const css = ` button:hover { background: var(--hover_bg); } button:focus { outline-color: var(--focus_color); } `; - const index = await create_style_rule_index(css); + const index = create_style_rule_index(css); expect(index.rules[0]?.variables_used.has('hover_bg')).toBe(true); expect(index.rules[1]?.variables_used.has('focus_color')).toBe(true); }); - test('extracts variables from @media rules', async () => { + test('extracts variables from @media rules', () => { const css = ` @media (min-width: 768px) { button { padding: var(--p_lg); margin: var(--m_lg); } } `; - const index = await create_style_rule_index(css); + const index = create_style_rule_index(css); expect(index.rules[0]?.variables_used.has('p_lg')).toBe(true); expect(index.rules[0]?.variables_used.has('m_lg')).toBe(true); }); - test('extracts variables with fallbacks', async () => { + test('extracts variables with fallbacks', () => { const css = `button { color: var(--primary, blue); background: var(--bg, white); }`; - const index = await create_style_rule_index(css); + const index = create_style_rule_index(css); expect(index.rules[0]?.variables_used.has('primary')).toBe(true); expect(index.rules[0]?.variables_used.has('bg')).toBe(true); }); - test('extracts nested variable fallbacks', async () => { + test('extracts nested variable fallbacks', () => { const css = `div { color: var(--a, var(--b, var(--c))); }`; - const index = await create_style_rule_index(css); + const index = create_style_rule_index(css); expect(index.rules[0]?.variables_used.has('a')).toBe(true); expect(index.rules[0]?.variables_used.has('b')).toBe(true); expect(index.rules[0]?.variables_used.has('c')).toBe(true); }); - test('extracts variables in calc()', async () => { + test('extracts variables in calc()', () => { const css = `div { width: calc(100% - var(--sidebar_width) - var(--gap)); }`; - const index = await create_style_rule_index(css); + const index = create_style_rule_index(css); expect(index.rules[0]?.variables_used.has('sidebar_width')).toBe(true); expect(index.rules[0]?.variables_used.has('gap')).toBe(true); }); - test('deduplicates repeated variables', async () => { + test('deduplicates repeated variables', () => { const css = ` div { border: var(--border_width) solid var(--border_color); @@ -166,39 +166,39 @@ describe('create_style_rule_index', () => { } `; - const index = await create_style_rule_index(css); + const index = create_style_rule_index(css); expect(index.rules[0]?.variables_used.size).toBe(2); expect(index.rules[0]?.variables_used.has('border_width')).toBe(true); expect(index.rules[0]?.variables_used.has('border_color')).toBe(true); }); - test('returns empty set for rule without variables', async () => { + test('returns empty set for rule without variables', () => { const css = `button { color: red; background: blue; }`; - const index = await create_style_rule_index(css); + const index = create_style_rule_index(css); expect(index.rules[0]?.variables_used.size).toBe(0); }); - test('extracts variables from multiple rules', async () => { + test('extracts variables from multiple rules', () => { const css = ` button { color: var(--btn_text); } input { border-color: var(--input_border); } a { color: var(--link_color); } `; - const index = await create_style_rule_index(css); + const index = create_style_rule_index(css); expect(index.rules[0]?.variables_used.has('btn_text')).toBe(true); expect(index.rules[1]?.variables_used.has('input_border')).toBe(true); expect(index.rules[2]?.variables_used.has('link_color')).toBe(true); }); - test('handles hyphens and underscores in variable names', async () => { + test('handles hyphens and underscores in variable names', () => { const css = `div { color: var(--my-color); background: var(--my_bg_color); }`; - const index = await create_style_rule_index(css); + const index = create_style_rule_index(css); expect(index.rules[0]?.variables_used.has('my-color')).toBe(true); expect(index.rules[0]?.variables_used.has('my_bg_color')).toBe(true); @@ -206,26 +206,26 @@ describe('create_style_rule_index', () => { }); describe('tree-shaking integration', () => { - test('get_matching_rules filters by elements', async () => { + test('get_matching_rules filters by elements', () => { const css = ` button { color: blue; } input { border: 1px solid; } a { text-decoration: none; } `; - const index = await create_style_rule_index(css); + const index = create_style_rule_index(css); const matched = get_matching_rules(index, new Set(['button']), new Set()); expect(matched.size).toBe(1); }); - test('generate_base_css outputs only matched rules', async () => { + test('generate_base_css outputs only matched rules', () => { const css = ` button { color: blue; } input { border: 1px solid; } `; - const index = await create_style_rule_index(css); + const index = create_style_rule_index(css); const matched = get_matching_rules(index, new Set(['button']), new Set()); const output = generate_base_css(index, matched); @@ -235,12 +235,12 @@ describe('create_style_rule_index', () => { }); describe('metadata', () => { - test('generates content hash', async () => { + test('generates content hash', () => { const css1 = 'button { color: red; }'; const css2 = 'button { color: blue; }'; - const index1 = await create_style_rule_index(css1); - const index2 = await create_style_rule_index(css2); + const index1 = create_style_rule_index(css1); + const index2 = create_style_rule_index(css2); // Different content should produce different hashes expect(index1.content_hash).not.toBe(index2.content_hash); @@ -248,28 +248,28 @@ describe('create_style_rule_index', () => { }); describe('edge cases', () => { - test('handles empty CSS', async () => { - const index = await create_style_rule_index(''); + test('handles empty CSS', () => { + const index = create_style_rule_index(''); expect(index.rules.length).toBe(0); }); - test('handles CSS with comments', async () => { + test('handles CSS with comments', () => { const css = ` /* This is a comment */ button { color: blue; } `; - const index = await create_style_rule_index(css); + const index = create_style_rule_index(css); expect(index.rules.length).toBe(1); }); - test('handles @media rules', async () => { + test('handles @media rules', () => { const css = ` button { font-size: 14px; } @media (min-width: 768px) { button { font-size: 16px; } } `; - const index = await create_style_rule_index(css); + const index = create_style_rule_index(css); // Both rules should be indexed under button expect(index.by_element.get('button')?.length).toBe(2); diff --git a/svelte.config.js b/svelte.config.js index 1e5e7a430..a95c22256 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -12,7 +12,7 @@ export default { vitePlugin: {inspector: true}, kit: { adapter: adapter(), - paths: {relative: false}, // use root-absolute paths for SSR path comparison: https://kit.svelte.dev/docs/configuration#paths + paths: {relative: false}, // use root-absolute paths for SSR path comparison: https://svelte.dev/docs/kit/configuration#paths alias: {$routes: 'src/routes', '@fuzdev/fuz_css': 'src/lib'}, csp: { directives: create_csp_directives({