From d2baf934d074ba1aa6068759d261a543344b826c Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Thu, 12 Feb 2026 02:41:57 +0900 Subject: [PATCH 01/12] Fix dep --- bindings/devup-ui-wasm/Cargo.toml | 8 ++++---- bun.lock | 5 ----- package.json | 2 +- packages/components/package.json | 3 --- packages/next-plugin/package.json | 1 - packages/vite-plugin/package.json | 3 +-- 6 files changed, 6 insertions(+), 16 deletions(-) diff --git a/bindings/devup-ui-wasm/Cargo.toml b/bindings/devup-ui-wasm/Cargo.toml index 300885c2..cdde9170 100644 --- a/bindings/devup-ui-wasm/Cargo.toml +++ b/bindings/devup-ui-wasm/Cargo.toml @@ -9,10 +9,10 @@ repository = "https://github.com/dev-five-git/devup-ui" documentation = "https://devup-ui.com" [lib] -crate-type = ["cdylib", "rlib"] +crate-type = ["cdylib"] [features] -default = ["console_error_panic_hook"] +default = [] [dependencies] wasm-bindgen = "0.2.108" @@ -25,10 +25,10 @@ css = { path = "../../libs/css" } # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for # code size when deploying. console_error_panic_hook = { version = "0.1.7", optional = true } +bimap = { version = "0.6.3", features = ["serde"] } js-sys = "0.3.85" serde_json = "1.0.149" serde-wasm-bindgen = "0.6.5" -bimap = { version = "0.6.3", features = ["serde"] } getrandom = { version = "0.3", features = ["wasm_js"] } [dev-dependencies] @@ -41,4 +41,4 @@ rstest = "0.26.1" unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] } [package.metadata.wasm-pack.profile.release] -wasm-opt = false +wasm-opt = ["-Oz"] diff --git a/bun.lock b/bun.lock index 5190c0a6..07e51bec 100644 --- a/bun.lock +++ b/bun.lock @@ -392,9 +392,7 @@ "dependencies": { "@devup-ui/react": "workspace:^", "clsx": "^2.1", - "csstype-extra": "latest", "react": "^19.2.4", - "react-dom": "^19.2.4", }, "devDependencies": { "@devup-ui/vite-plugin": "workspace:^", @@ -411,7 +409,6 @@ }, "peerDependencies": { "@devup-ui/react": "workspace:^", - "csstype-extra": "*", "react": "*", }, }, @@ -438,7 +435,6 @@ "@devup-ui/plugin-utils": "workspace:^", "@devup-ui/wasm": "workspace:^", "@devup-ui/webpack-plugin": "workspace:^", - "next": "^16.1", }, "devDependencies": { "@types/webpack": "^5.28", @@ -512,7 +508,6 @@ "dependencies": { "@devup-ui/plugin-utils": "workspace:^", "@devup-ui/wasm": "workspace:^", - "vite": "^7.3", }, "devDependencies": { "typescript": "^5.9.3", diff --git a/package.json b/package.json index 49ca9a6b..22d884e9 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "lint": "cargo fmt --all -- --check && cargo clippy --all-targets --all-features -- -D warnings && eslint", "lint:fix": "eslint --fix && cargo fmt", - "test": "cargo tarpaulin --out xml --out stdout --out html --all-targets --engine llvm && bun test", + "test": "cargo tarpaulin --out xml --out stdout --all-targets --engine llvm && bun test", "test:e2e": "bunx playwright test", "test:e2e:ui": "bunx playwright test --ui", "test:e2e:update": "bunx playwright test --update-snapshots", diff --git a/packages/components/package.json b/packages/components/package.json index a7f11e6a..c691837b 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -44,9 +44,7 @@ "types": "./dist/index.d.ts", "dependencies": { "@devup-ui/react": "workspace:^", - "csstype-extra": "latest", "react": "^19.2.4", - "react-dom": "^19.2.4", "clsx": "^2.1" }, "devDependencies": { @@ -64,7 +62,6 @@ }, "peerDependencies": { "@devup-ui/react": "workspace:^", - "csstype-extra": "*", "react": "*" } } diff --git a/packages/next-plugin/package.json b/packages/next-plugin/package.json index 884273c2..88c48ef2 100644 --- a/packages/next-plugin/package.json +++ b/packages/next-plugin/package.json @@ -54,7 +54,6 @@ "dependencies": { "@devup-ui/plugin-utils": "workspace:^", "@devup-ui/webpack-plugin": "workspace:^", - "next": "^16.1", "@devup-ui/wasm": "workspace:^" }, "devDependencies": { diff --git a/packages/vite-plugin/package.json b/packages/vite-plugin/package.json index c53a119d..69d45dae 100644 --- a/packages/vite-plugin/package.json +++ b/packages/vite-plugin/package.json @@ -42,8 +42,7 @@ ], "dependencies": { "@devup-ui/plugin-utils": "workspace:^", - "@devup-ui/wasm": "workspace:^", - "vite": "^7.3" + "@devup-ui/wasm": "workspace:^" }, "devDependencies": { "typescript": "^5.9.3" From 9f512c382bbab84ed9cbb652c5bf730fd0091d7d Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Thu, 12 Feb 2026 02:56:39 +0900 Subject: [PATCH 02/12] Add optimize option to wasm --- bindings/devup-ui-wasm/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/devup-ui-wasm/Cargo.toml b/bindings/devup-ui-wasm/Cargo.toml index cdde9170..629b09c3 100644 --- a/bindings/devup-ui-wasm/Cargo.toml +++ b/bindings/devup-ui-wasm/Cargo.toml @@ -41,4 +41,4 @@ rstest = "0.26.1" unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] } [package.metadata.wasm-pack.profile.release] -wasm-opt = ["-Oz"] +wasm-opt = ["-Oz", "--enable-bulk-memory", "--enable-nontrapping-float-to-int"] From c11131406b731825d7c58bff1e21d06154dfd177 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Thu, 12 Feb 2026 03:07:44 +0900 Subject: [PATCH 03/12] Update action --- .github/workflows/publish.yml | 249 ++++++++++++++++++++-------------- playwright.config.ts | 2 +- 2 files changed, 151 insertions(+), 100 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5ba41292..b847db29 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,94 +1,145 @@ -name: Publish Package to npm - -on: - push: - branches: - - main - pull_request: - branches: - - main -permissions: write-all - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: false - -jobs: - benchmark: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v5 - - - uses: actions-rust-lang/setup-rust-toolchain@v1 - - name: Cargo tarpaulin and fmt - run: | - cargo install cargo-tarpaulin - rustup component add rustfmt clippy - - uses: oven-sh/setup-bun@v2 - name: Install bun - - - uses: jetli/wasm-pack-action@v0.4.0 - with: - version: 'latest' - - name: Install Node.js - uses: actions/setup-node@v4 - with: - registry-url: "https://registry.npmjs.org" - node-version: 22 - - run: bun install - - run: bun run build - - name: Benchmark - run: bun run benchmark - - publish: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v5 - - - uses: actions-rust-lang/setup-rust-toolchain@v1 - - name: Cargo tarpaulin and fmt - run: | - cargo install cargo-tarpaulin - rustup component add rustfmt clippy - - uses: oven-sh/setup-bun@v2 - name: Install bun - - - uses: jetli/wasm-pack-action@v0.4.0 - with: - version: 'latest' - - name: Install Node.js - uses: actions/setup-node@v4 - with: - registry-url: "https://registry.npmjs.org" - node-version: 22 - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - run: bun install - - run: bun run build - - run: | - bun run lint - # rust coverage issue - echo 'max_width = 100000' > .rustfmt.toml - echo 'tab_spaces = 4' >> .rustfmt.toml - echo 'newline_style = "Unix"' >> .rustfmt.toml - echo 'fn_call_width = 100000' >> .rustfmt.toml - echo 'fn_params_layout = "Compressed"' >> .rustfmt.toml - echo 'chain_width = 100000' >> .rustfmt.toml - echo 'merge_derives = true' >> .rustfmt.toml - echo 'use_small_heuristics = "Default"' >> .rustfmt.toml - cargo fmt - - run: bun run test - - name: Format Rollback - run: | - rm -rf .rustfmt.toml - cargo fmt +name: Publish Package to npm + +on: + push: + branches: + - main + pull_request: + branches: + - main +permissions: write-all + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + benchmark: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + + - name: Cache cargo registry + target + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: cargo-benchmark-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + cargo-benchmark-${{ runner.os }}- + + - uses: oven-sh/setup-bun@v2 + name: Install bun + + - name: Cache bun dependencies + uses: actions/cache@v5 + with: + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-${{ hashFiles('**/bun.lock') }} + restore-keys: | + bun-${{ runner.os }}- + + - uses: jetli/wasm-pack-action@v0.4.0 + with: + version: 'latest' + - name: Install Node.js + uses: actions/setup-node@v4 + with: + registry-url: "https://registry.npmjs.org" + node-version: 22 + - run: bun install + - run: bun run build + - name: Benchmark + run: bun run benchmark + + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + + - name: Cache cargo registry + target + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: cargo-publish-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + cargo-publish-${{ runner.os }}- + + - name: Install cargo-binstall + run: curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash + + - name: Install cargo-tarpaulin + run: cargo binstall cargo-tarpaulin --no-confirm + + - name: Install Rust components + run: rustup component add rustfmt clippy + + - uses: oven-sh/setup-bun@v2 + name: Install bun + + - name: Cache bun dependencies + uses: actions/cache@v5 + with: + path: ~/.bun/install/cache + key: bun-${{ runner.os }}-${{ hashFiles('**/bun.lock') }} + restore-keys: | + bun-${{ runner.os }}- + + - uses: jetli/wasm-pack-action@v0.4.0 + with: + version: 'latest' + - name: Install Node.js + uses: actions/setup-node@v4 + with: + registry-url: "https://registry.npmjs.org" + node-version: 22 + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - run: bun install + - run: bun run build + - run: | + bun run lint + # rust coverage issue + echo 'max_width = 100000' > .rustfmt.toml + echo 'tab_spaces = 4' >> .rustfmt.toml + echo 'newline_style = "Unix"' >> .rustfmt.toml + echo 'fn_call_width = 100000' >> .rustfmt.toml + echo 'fn_params_layout = "Compressed"' >> .rustfmt.toml + echo 'chain_width = 100000' >> .rustfmt.toml + echo 'merge_derives = true' >> .rustfmt.toml + echo 'use_small_heuristics = "Default"' >> .rustfmt.toml + cargo fmt + - run: bun run test + - name: Format Rollback + run: | + rm -rf .rustfmt.toml + cargo fmt - name: Build Landing run: | bun run --filter @devup-ui/components build-storybook mv ./packages/components/storybook-static ./apps/landing/public/storybook bun run --filter landing build + - name: Cache Playwright Browsers + id: playwright-cache + uses: actions/cache@v5 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('**/bun.lock') }} + restore-keys: | + playwright-${{ runner.os }}- - name: Install Playwright Browsers run: bunx playwright install chromium --with-deps - name: Check for Existing E2E Snapshots @@ -128,16 +179,16 @@ jobs: name: playwright-report-singlecss path: playwright-report/ retention-days: 30 - - name: Upload to codecov.io - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: true - files: ./coverage/lcov.info - - - uses: changepacks/action@main - id: changepacks - with: - publish: true - env: - NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Upload to codecov.io + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true + files: ./coverage/lcov.info + + - uses: changepacks/action@main + id: changepacks + with: + publish: true + env: + NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/playwright.config.ts b/playwright.config.ts index 8deea17f..f174dd61 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -7,7 +7,7 @@ export default defineConfig({ fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, - workers: process.env.CI ? 1 : 4, + workers: process.env.CI ? 2 : 4, reporter: 'html', timeout: 60_000, expect: { From c1e6008c849c2c7f9ade791ba6ce184933803b13 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Thu, 12 Feb 2026 03:18:35 +0900 Subject: [PATCH 04/12] Optimize --- Cargo.lock | 10 +++- libs/css/Cargo.toml | 2 +- libs/css/src/constant.rs | 2 +- libs/css/src/lib.rs | 89 +++++++++++++++++------------- libs/css/src/optimize_value.rs | 7 ++- libs/sheet/Cargo.toml | 2 +- libs/sheet/benches/my_benchmark.rs | 6 +- libs/sheet/src/lib.rs | 8 ++- 8 files changed, 73 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ec6b5a64..b5ea79c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -518,7 +518,7 @@ dependencies = [ "bimap", "criterion", "phf", - "regex", + "regex-lite", "rstest", "serde", "serial_test", @@ -1904,6 +1904,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + [[package]] name = "regex-syntax" version = "0.8.9" @@ -2182,7 +2188,7 @@ dependencies = [ "css", "extractor", "insta", - "regex", + "regex-lite", "rstest", "serde", "serde_json", diff --git a/libs/css/Cargo.toml b/libs/css/Cargo.toml index 14988fb2..50adaa7d 100644 --- a/libs/css/Cargo.toml +++ b/libs/css/Cargo.toml @@ -9,7 +9,7 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)'] } [dependencies] phf = { version = "0.13", features = ["macros"] } serde = { version = "1.0.228", features = ["derive"] } -regex = "1.12.3" +regex-lite = "0.1" bimap = { version = "0.6.3" } [dev-dependencies] diff --git a/libs/css/src/constant.rs b/libs/css/src/constant.rs index 9e14022f..55598ecf 100644 --- a/libs/css/src/constant.rs +++ b/libs/css/src/constant.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use phf::{phf_map, phf_set}; -use regex::Regex; +use regex_lite::Regex; use std::sync::LazyLock; pub(super) static SELECTOR_ORDER_MAP: LazyLock> = LazyLock::new(|| { diff --git a/libs/css/src/lib.rs b/libs/css/src/lib.rs index b401c545..66174434 100644 --- a/libs/css/src/lib.rs +++ b/libs/css/src/lib.rs @@ -200,48 +200,59 @@ pub fn keyframes_to_keyframes_name(keyframes: &str, filename: Option<&str>) -> S } } +/// ASCII lookup table for selector encoding. `None` means pass through (alphanumeric, `-`, `_`) +/// or fall through to the Unicode escape path. +const SELECTOR_ENCODE: [Option<&str>; 128] = { + let mut table: [Option<&str>; 128] = [None; 128]; + table[b'&' as usize] = Some("_a_"); + table[b':' as usize] = Some("_c_"); + table[b'(' as usize] = Some("_lp_"); + table[b')' as usize] = Some("_rp_"); + table[b'[' as usize] = Some("_lb_"); + table[b']' as usize] = Some("_rb_"); + table[b'=' as usize] = Some("_eq_"); + table[b'>' as usize] = Some("_gt_"); + table[b'<' as usize] = Some("_lt_"); + table[b'~' as usize] = Some("_tl_"); + table[b'+' as usize] = Some("_pl_"); + table[b' ' as usize] = Some("_s_"); + table[b'*' as usize] = Some("_st_"); + table[b'.' as usize] = Some("_d_"); + table[b'#' as usize] = Some("_h_"); + table[b',' as usize] = Some("_cm_"); + table[b'"' as usize] = Some("_dq_"); + table[b'\'' as usize] = Some("_sq_"); + table[b'/' as usize] = Some("_sl_"); + table[b'\\' as usize] = Some("_bs_"); + table[b'%' as usize] = Some("_pc_"); + table[b'^' as usize] = Some("_cr_"); + table[b'$' as usize] = Some("_dl_"); + table[b'|' as usize] = Some("_pp_"); + table[b'@' as usize] = Some("_at_"); + table[b'!' as usize] = Some("_ex_"); + table[b'?' as usize] = Some("_qm_"); + table[b';' as usize] = Some("_sc_"); + table[b'{' as usize] = Some("_lc_"); + table[b'}' as usize] = Some("_rc_"); + table +}; + fn encode_selector(selector: &str) -> String { - let mut result = String::with_capacity(selector.len() * 2); + use std::fmt::Write; + let mut result = String::with_capacity(selector.len() * 3); for c in selector.chars() { - match c { - '&' => result.push_str("_a_"), - ':' => result.push_str("_c_"), - '(' => result.push_str("_lp_"), - ')' => result.push_str("_rp_"), - '[' => result.push_str("_lb_"), - ']' => result.push_str("_rb_"), - '=' => result.push_str("_eq_"), - '>' => result.push_str("_gt_"), - '<' => result.push_str("_lt_"), - '~' => result.push_str("_tl_"), - '+' => result.push_str("_pl_"), - ' ' => result.push_str("_s_"), - '*' => result.push_str("_st_"), - '.' => result.push_str("_d_"), - '#' => result.push_str("_h_"), - ',' => result.push_str("_cm_"), - '"' => result.push_str("_dq_"), - '\'' => result.push_str("_sq_"), - '/' => result.push_str("_sl_"), - '\\' => result.push_str("_bs_"), - '%' => result.push_str("_pc_"), - '^' => result.push_str("_cr_"), - '$' => result.push_str("_dl_"), - '|' => result.push_str("_pp_"), - '@' => result.push_str("_at_"), - '!' => result.push_str("_ex_"), - '?' => result.push_str("_qm_"), - ';' => result.push_str("_sc_"), - '{' => result.push_str("_lc_"), - '}' => result.push_str("_rc_"), - '-' => result.push('-'), - '_' => result.push('_'), - _ if c.is_ascii_alphanumeric() => result.push(c), - _ => { - result.push_str("_u"); - result.push_str(&format!("{:04x}", c as u32)); - result.push('_'); + if c.is_ascii() { + let byte = c as u8; + if let Some(encoded) = SELECTOR_ENCODE[byte as usize] { + result.push_str(encoded); + } else if c.is_ascii_alphanumeric() || c == '-' || c == '_' { + result.push(c); + } else { + // ASCII but not in table and not alphanumeric/-/_ + let _ = write!(result, "_u{:04x}_", c as u32); } + } else { + let _ = write!(result, "_u{:04x}_", c as u32); } } result diff --git a/libs/css/src/optimize_value.rs b/libs/css/src/optimize_value.rs index 829713a2..79c6e0a8 100644 --- a/libs/css/src/optimize_value.rs +++ b/libs/css/src/optimize_value.rs @@ -43,7 +43,7 @@ pub fn optimize_value(value: &str) -> String { ret = s; } } - let replaced = F_RGBA_RE.replace_all(&ret, |c: ®ex::Captures| { + let replaced = F_RGBA_RE.replace_all(&ret, |c: ®ex_lite::Captures| { let r = c[1].parse::().unwrap(); let g = c[2].parse::().unwrap(); let b = c[3].parse::().unwrap(); @@ -59,7 +59,7 @@ pub fn optimize_value(value: &str) -> String { if let std::borrow::Cow::Owned(s) = replaced { ret = s; } - let replaced = F_RGB_RE.replace_all(&ret, |c: ®ex::Captures| { + let replaced = F_RGB_RE.replace_all(&ret, |c: ®ex_lite::Captures| { let r = c[1].parse::().unwrap(); let g = c[2].parse::().unwrap(); let b = c[3].parse::().unwrap(); @@ -69,7 +69,8 @@ pub fn optimize_value(value: &str) -> String { ret = s; } if ret.contains('#') { - let replaced = COLOR_HASH.replace_all(&ret, |c: ®ex::Captures| optimize_color(&c[1])); + let replaced = + COLOR_HASH.replace_all(&ret, |c: ®ex_lite::Captures| optimize_color(&c[1])); if let std::borrow::Cow::Owned(s) = replaced { ret = s; } diff --git a/libs/sheet/Cargo.toml b/libs/sheet/Cargo.toml index c2a7194a..087a87ca 100644 --- a/libs/sheet/Cargo.toml +++ b/libs/sheet/Cargo.toml @@ -7,7 +7,7 @@ edition = "2024" css = { path = "../css" } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" -regex = "1.12.3" +regex-lite = "0.1" extractor = { path = "../extractor" } [dev-dependencies] diff --git a/libs/sheet/benches/my_benchmark.rs b/libs/sheet/benches/my_benchmark.rs index 7533483f..a88d66ba 100644 --- a/libs/sheet/benches/my_benchmark.rs +++ b/libs/sheet/benches/my_benchmark.rs @@ -1,5 +1,5 @@ use criterion::{Criterion, criterion_group, criterion_main}; -use regex::Regex; +use regex_lite::Regex; use std::hint::black_box; use std::sync::LazyLock; @@ -8,7 +8,7 @@ static VAR_RE: LazyLock = LazyLock::new(|| Regex::new(r"\$\w+").unwrap()) fn convert_theme_variable_value_a(value: &str) -> String { if value.contains("$") { VAR_RE - .replace_all(value, |caps: ®ex::Captures| { + .replace_all(value, |caps: ®ex_lite::Captures| { format!("var(--{})", &caps[0][1..]) }) .to_string() @@ -19,7 +19,7 @@ fn convert_theme_variable_value_a(value: &str) -> String { fn convert_theme_variable_value_b(value: &str) -> String { VAR_RE - .replace_all(value, |caps: ®ex::Captures| { + .replace_all(value, |caps: ®ex_lite::Captures| { format!("var(--{})", &caps[0][1..]) }) .to_string() diff --git a/libs/sheet/src/lib.rs b/libs/sheet/src/lib.rs index 90a4722e..615a2721 100644 --- a/libs/sheet/src/lib.rs +++ b/libs/sheet/src/lib.rs @@ -8,7 +8,7 @@ use css::{ use extractor::extract_style::ExtractStyleProperty; use extractor::extract_style::extract_style_value::ExtractStyleValue; use extractor::extract_style::style_property::StyleProperty; -use regex::Regex; +use regex_lite::Regex; use serde::de::Error; use serde::{Deserialize, Deserializer, Serialize}; use std::borrow::Cow; @@ -112,7 +112,7 @@ fn convert_theme_variable_value(value: &str) -> Cow<'_, str> { if value.contains('$') { Cow::Owned( VAR_RE - .replace_all(value, |caps: ®ex::Captures| { + .replace_all(value, |caps: ®ex_lite::Captures| { format!("var(--{})", &caps[0][1..].replace('.', "-")) }) .into_owned(), @@ -488,7 +488,9 @@ impl StyleSheet { map: &BTreeMap>, layered_styles: &mut BTreeMap>, // layer -> Vec<(selector, property, value)> ) -> String { - let mut current_css = String::new(); + // Estimate ~64 bytes per property for pre-allocation + let prop_count: usize = map.values().map(|s| s.len()).sum(); + let mut current_css = String::with_capacity(prop_count * 64); for (level, props) in map.iter() { let (mut global_props, rest): (Vec<_>, Vec<_>) = props .iter() From 075e7f242b2affb425c3fb4c987add0874f45a86 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Thu, 12 Feb 2026 03:33:26 +0900 Subject: [PATCH 05/12] Split shared --- packages/next-plugin/src/css-loader.ts | 10 +- packages/next-plugin/src/plugin.ts | 12 +- .../plugin-utils/src/__tests__/shared.test.ts | 103 ++++++++++++++++++ packages/plugin-utils/src/index.ts | 5 + packages/plugin-utils/src/shared.ts | 52 +++++++++ packages/rsbuild-plugin/src/plugin.ts | 9 +- packages/vite-plugin/src/plugin.ts | 15 +-- packages/webpack-plugin/src/css-loader.ts | 6 +- packages/webpack-plugin/src/plugin.ts | 8 +- 9 files changed, 177 insertions(+), 43 deletions(-) create mode 100644 packages/plugin-utils/src/__tests__/shared.test.ts create mode 100644 packages/plugin-utils/src/shared.ts diff --git a/packages/next-plugin/src/css-loader.ts b/packages/next-plugin/src/css-loader.ts index 96912602..c928e277 100644 --- a/packages/next-plugin/src/css-loader.ts +++ b/packages/next-plugin/src/css-loader.ts @@ -1,6 +1,7 @@ import { existsSync, readFileSync } from 'node:fs' import { Agent, request } from 'node:http' +import { getFileNumByFilename } from '@devup-ui/plugin-utils' import { getCss, importClassMap, @@ -10,15 +11,6 @@ import { } from '@devup-ui/wasm' import type { RawLoaderDefinitionFunction } from 'webpack' -function getFileNumByFilename(filename: string) { - // Handle query parameter format: devup-ui.css?fileNum=79 - // Turbopack may embed query params in resourcePath - const queryMatch = filename.match(/[?&]fileNum=(\d+)/) - if (queryMatch) return parseInt(queryMatch[1]) - if (filename.endsWith('devup-ui.css')) return null - return parseInt(filename.split('devup-ui-')[1].split('.')[0]) -} - export interface DevupUICssLoaderOptions { // turbo watch: boolean diff --git a/packages/next-plugin/src/plugin.ts b/packages/next-plugin/src/plugin.ts index 29bca263..3b3a6c39 100644 --- a/packages/next-plugin/src/plugin.ts +++ b/packages/next-plugin/src/plugin.ts @@ -1,7 +1,11 @@ import { existsSync, mkdirSync, writeFileSync } from 'node:fs' import { join, relative, resolve } from 'node:path' -import { loadDevupConfigSync, mergeImportAliases } from '@devup-ui/plugin-utils' +import { + createNodeModulesExcludeRegex, + loadDevupConfigSync, + mergeImportAliases, +} from '@devup-ui/plugin-utils' import { exportClassMap, exportFileMap, @@ -86,11 +90,7 @@ export function DevupUI( writeFileSync(join(distDir, 'theme.d.ts'), themeInterface) } // disable turbo parallel - const excludeRegex = new RegExp( - `(node_modules(?!.*(${['@devup-ui', ...include] - .join('|') - .replaceAll('/', '[\\/\\\\_]')})([\\/\\\\.]|$)))|(.mdx.[tj]sx?$)`, - ) + const excludeRegex = createNodeModulesExcludeRegex(include, '.mdx.[tj]sx?$') const coordinatorPortFile = join(distDir, 'coordinator.port') diff --git a/packages/plugin-utils/src/__tests__/shared.test.ts b/packages/plugin-utils/src/__tests__/shared.test.ts new file mode 100644 index 00000000..b2fc77d3 --- /dev/null +++ b/packages/plugin-utils/src/__tests__/shared.test.ts @@ -0,0 +1,103 @@ +import { describe, expect, it } from 'bun:test' + +import { + createNodeModulesExcludeRegex, + type DevupUIBasePluginOptions, + getFileNumByFilename, +} from '../shared' + +describe('getFileNumByFilename', () => { + it('should return null for devup-ui.css', () => { + expect(getFileNumByFilename('devup-ui.css')).toBeNull() + }) + + it('should return file number for devup-ui-5.css', () => { + expect(getFileNumByFilename('devup-ui-5.css')).toBe(5) + }) + + it('should return file number for devup-ui-123.css', () => { + expect(getFileNumByFilename('devup-ui-123.css')).toBe(123) + }) + + it('should handle query params: devup-ui.css?fileNum=79', () => { + expect(getFileNumByFilename('devup-ui.css?fileNum=79')).toBe(79) + }) + + it('should handle path with query: /path/to/devup-ui.css?fileNum=42', () => { + expect(getFileNumByFilename('/path/to/devup-ui.css?fileNum=42')).toBe(42) + }) + + it('should return null for path/to/devup-ui.css (no number, no query)', () => { + expect(getFileNumByFilename('path/to/devup-ui.css')).toBeNull() + }) +}) + +describe('createNodeModulesExcludeRegex', () => { + it('should match node_modules paths that should be excluded', () => { + const regex = createNodeModulesExcludeRegex([]) + expect(regex.test('node_modules/some-package/index.js')).toBe(true) + expect(regex.test('/path/to/node_modules/lodash/index.js')).toBe(true) + }) + + it('should NOT match @devup-ui paths', () => { + const regex = createNodeModulesExcludeRegex([]) + expect(regex.test('node_modules/@devup-ui/react/index.js')).toBe(false) + expect(regex.test('node_modules/@devup-ui/components/index.js')).toBe(false) + }) + + it('should NOT match included packages', () => { + const regex = createNodeModulesExcludeRegex(['my-company/design-system']) + expect(regex.test('node_modules/my-company/design-system/index.js')).toBe( + false, + ) + }) + + it('should handle extra excludes parameter', () => { + const regex = createNodeModulesExcludeRegex([], '.mdx.[tj]sx?$') + // Should match node_modules + expect(regex.test('node_modules/some-package/index.js')).toBe(true) + // Should also match .mdx.tsx files + expect(regex.test('src/page.mdx.tsx')).toBe(true) + expect(regex.test('src/page.mdx.jsx')).toBe(true) + expect(regex.test('src/page.mdx.ts')).toBe(true) + // Should NOT match @devup-ui + expect(regex.test('node_modules/@devup-ui/react/index.js')).toBe(false) + }) + + it('should handle empty include array', () => { + const regex = createNodeModulesExcludeRegex([]) + expect(regex.test('node_modules/some-package/index.js')).toBe(true) + expect(regex.test('node_modules/@devup-ui/react/index.js')).toBe(false) + }) +}) + +describe('DevupUIBasePluginOptions', () => { + it('should be usable as a type', () => { + const options: DevupUIBasePluginOptions = { + package: '@devup-ui/react', + cssDir: 'df/devup-ui', + devupFile: 'devup.json', + distDir: 'df', + debug: false, + include: [], + singleCss: false, + } + expect(options.package).toBe('@devup-ui/react') + }) + + it('should accept optional fields', () => { + const options: DevupUIBasePluginOptions = { + package: '@devup-ui/react', + cssDir: 'df/devup-ui', + devupFile: 'devup.json', + distDir: 'df', + debug: false, + include: [], + singleCss: false, + prefix: 'my-prefix', + importAliases: { '@emotion/styled': 'styled' }, + } + expect(options.prefix).toBe('my-prefix') + expect(options.importAliases).toEqual({ '@emotion/styled': 'styled' }) + }) +}) diff --git a/packages/plugin-utils/src/index.ts b/packages/plugin-utils/src/index.ts index 0a887597..b02083e1 100644 --- a/packages/plugin-utils/src/index.ts +++ b/packages/plugin-utils/src/index.ts @@ -1,4 +1,9 @@ export { deepMerge, loadDevupConfig, loadDevupConfigSync } from './load-config' +export { + createNodeModulesExcludeRegex, + type DevupUIBasePluginOptions, + getFileNumByFilename, +} from './shared' export type { DevupConfig, DevupTheme, diff --git a/packages/plugin-utils/src/shared.ts b/packages/plugin-utils/src/shared.ts new file mode 100644 index 00000000..cc3e08d1 --- /dev/null +++ b/packages/plugin-utils/src/shared.ts @@ -0,0 +1,52 @@ +import type { ImportAliases } from './types' + +/** + * Extract file number from a devup-ui CSS filename. + * + * Handles both standard filenames (devup-ui-5.css) and query parameter + * format (devup-ui.css?fileNum=79) used by Turbopack. + * + * @param filename - CSS filename or path to parse + * @returns The file number, or null for the base devup-ui.css file + */ +export function getFileNumByFilename(filename: string): number | null { + // Handle query parameter format: devup-ui.css?fileNum=79 + // Turbopack may embed query params in resourcePath + const queryMatch = filename.match(/[?&]fileNum=(\d+)/) + if (queryMatch) return parseInt(queryMatch[1]) + if (filename.endsWith('devup-ui.css')) return null + return parseInt(filename.split('devup-ui-')[1].split('.')[0]) +} + +/** + * Create a regex that excludes node_modules paths, except for @devup-ui + * and any additional included packages. + * + * @param include - Additional package names to include (not exclude) + * @param extraExcludes - Optional extra regex pattern to OR with the node_modules exclusion + * @returns A RegExp for use in bundler exclude/condition rules + */ +export function createNodeModulesExcludeRegex( + include: string[], + extraExcludes?: string, +): RegExp { + const base = `node_modules(?!.*(${['@devup-ui', ...include] + .join('|') + .replaceAll('/', '[\\/\\\\_]')})([\\/\\\\.]|$))` + return new RegExp(extraExcludes ? `(${base})|(${extraExcludes})` : base) +} + +/** + * Common plugin options shared across all devup-ui build plugins. + */ +export interface DevupUIBasePluginOptions { + package: string + cssDir: string + devupFile: string + distDir: string + debug: boolean + include: string[] + singleCss: boolean + prefix?: string + importAliases?: ImportAliases +} diff --git a/packages/rsbuild-plugin/src/plugin.ts b/packages/rsbuild-plugin/src/plugin.ts index 53575596..36b87752 100644 --- a/packages/rsbuild-plugin/src/plugin.ts +++ b/packages/rsbuild-plugin/src/plugin.ts @@ -3,6 +3,7 @@ import { mkdir, writeFile } from 'node:fs/promises' import { basename, join, resolve } from 'node:path' import { + createNodeModulesExcludeRegex, type ImportAliases, loadDevupConfig, mergeImportAliases, @@ -136,13 +137,7 @@ export const DevupUI = ({ test: /\.(tsx|ts|js|mjs|jsx)$/, }, async ({ code, resourcePath }) => { - if ( - new RegExp( - `node_modules(?!.*(${['@devup-ui', ...include] - .join('|') - .replaceAll('/', '[\\/\\\\_]')})([\\/\\\\.]|$))`, - ).test(resourcePath) - ) + if (createNodeModulesExcludeRegex(include).test(resourcePath)) return code const { code: retCode, diff --git a/packages/vite-plugin/src/plugin.ts b/packages/vite-plugin/src/plugin.ts index 16326f08..6738b928 100644 --- a/packages/vite-plugin/src/plugin.ts +++ b/packages/vite-plugin/src/plugin.ts @@ -3,6 +3,8 @@ import { mkdir, writeFile } from 'node:fs/promises' import { basename, dirname, join, relative, resolve } from 'node:path' import { + createNodeModulesExcludeRegex, + getFileNumByFilename, type ImportAliases, loadDevupConfig, mergeImportAliases, @@ -36,11 +38,6 @@ export interface DevupUIPluginOptions { importAliases?: ImportAliases } -function getFileNumByFilename(filename: string) { - if (filename.endsWith('devup-ui.css')) return null - return parseInt(filename.split('devup-ui-')[1].split('.')[0]) -} - async function writeDataFiles( options: Omit, ) { @@ -194,13 +191,7 @@ export function DevupUI({ const fileName = id.split('?')[0] if (!/\.(tsx|ts|js|mjs|jsx)$/i.test(fileName)) return - if ( - new RegExp( - `node_modules(?!.*(${['@devup-ui', ...include] - .join('|') - .replaceAll('/', '[\\/\\\\_]')})([\\/\\\\.]|$))`, - ).test(fileName) - ) { + if (createNodeModulesExcludeRegex(include).test(fileName)) { return } diff --git a/packages/webpack-plugin/src/css-loader.ts b/packages/webpack-plugin/src/css-loader.ts index eecd71b7..bc634407 100644 --- a/packages/webpack-plugin/src/css-loader.ts +++ b/packages/webpack-plugin/src/css-loader.ts @@ -1,11 +1,7 @@ +import { getFileNumByFilename } from '@devup-ui/plugin-utils' import { getCss } from '@devup-ui/wasm' import type { RawLoaderDefinitionFunction } from 'webpack' -function getFileNumByFilename(filename: string) { - if (filename.endsWith('devup-ui.css')) return null - return parseInt(filename.split('devup-ui-')[1].split('.')[0]) -} - const devupUICssLoader: RawLoaderDefinitionFunction = function (_, map, meta) { const fileNum = getFileNumByFilename(this.resourcePath) this.callback(null, getCss(fileNum, true), map, meta) diff --git a/packages/webpack-plugin/src/plugin.ts b/packages/webpack-plugin/src/plugin.ts index b58ebb69..392788a9 100644 --- a/packages/webpack-plugin/src/plugin.ts +++ b/packages/webpack-plugin/src/plugin.ts @@ -4,6 +4,7 @@ import { createRequire } from 'node:module' import { join, resolve } from 'node:path' import { + createNodeModulesExcludeRegex, type ImportAliases, loadDevupConfigSync, mergeImportAliases, @@ -178,10 +179,9 @@ export class DevupUIWebpackPlugin { compiler.options.module.rules.push( { test: /\.(tsx|ts|js|mjs|jsx)$/, - exclude: new RegExp( - `(node_modules(?!.*(${['@devup-ui', ...this.options.include] - .join('|') - .replaceAll('/', '[\\/\\\\_]')})([\\/\\\\.]|$)))|(.mdx.[tj]sx?$)`, + exclude: createNodeModulesExcludeRegex( + this.options.include, + '.mdx.[tj]sx?$', ), enforce: 'pre', use: [ From 10f5389ef709378b9d712bd5f45c5b88ba40c731 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Thu, 12 Feb 2026 03:58:44 +0900 Subject: [PATCH 06/12] Optimize --- e2e/components-pages.spec.ts | 40 +++++++++++++++++--------------- e2e/docs-pages.spec.ts | 34 ++++++++++++++------------- e2e/helpers.ts | 23 ++++++++++++++++++ e2e/landing-header.spec.ts | 10 ++++---- e2e/landing-interactions.spec.ts | 10 ++++---- e2e/landing-responsive.spec.ts | 4 +++- e2e/landing-theme.spec.ts | 18 +++++++------- e2e/landing-visual-dark.spec.ts | 18 +++++++------- e2e/landing-visual-hover.spec.ts | 18 +++++++------- e2e/landing-visual.spec.ts | 16 +++++++------ e2e/landing-zero-runtime.spec.ts | 12 ++++++---- 11 files changed, 123 insertions(+), 80 deletions(-) create mode 100644 e2e/helpers.ts diff --git a/e2e/components-pages.spec.ts b/e2e/components-pages.spec.ts index 392ef207..4733bfe2 100644 --- a/e2e/components-pages.spec.ts +++ b/e2e/components-pages.spec.ts @@ -1,5 +1,7 @@ import { expect, test } from '@playwright/test' +import { waitForFontsReady, waitForStyleSettle } from './helpers' + test.describe('Components Pages', () => { test.describe('Components link availability', () => { test('header has Components link pointing to /components/overview', async ({ @@ -11,7 +13,7 @@ test.describe('Components Pages', () => { }) const page = await context.newPage() await page.goto('/') - await page.waitForTimeout(500) + await waitForFontsReady(page) const link = page.locator('a[href="/components/overview"]').first() await expect(link).toBeVisible() @@ -26,7 +28,7 @@ test.describe('Components Pages', () => { }) const page = await context.newPage() await page.goto('/') - await page.waitForTimeout(500) + await waitForFontsReady(page) const link = page .locator('a[href="/components/overview"]') @@ -42,7 +44,7 @@ test.describe('Components Pages', () => { }) const page = await context.newPage() await page.goto('/') - await page.waitForTimeout(500) + await waitForFontsReady(page) const link = page .locator('a[href="/components/overview"]') @@ -59,7 +61,7 @@ test.describe('Components Pages', () => { }) const page = await context.newPage() await page.goto('/') - await page.waitForTimeout(500) + await waitForFontsReady(page) const link = page .locator('a[href="/components/overview"]') @@ -77,7 +79,7 @@ test.describe('Components Pages', () => { }) const page = await context.newPage() await page.goto('/') - await page.waitForTimeout(500) + await waitForFontsReady(page) const link = page.locator('a[href="/storybook/index.html"]').first() await expect(link).toBeVisible() @@ -91,7 +93,7 @@ test.describe('Components Pages', () => { }) const page = await context.newPage() await page.goto('/') - await page.waitForTimeout(500) + await waitForFontsReady(page) const link = page .locator('a[href="/storybook/index.html"]') @@ -110,7 +112,7 @@ test.describe('Components Pages', () => { }) const page = await context.newPage() await page.goto('/') - await page.waitForTimeout(500) + await waitForFontsReady(page) await expect(page.getByText('Comparison Bechmarks').first()).toBeVisible() await context.close() @@ -123,7 +125,7 @@ test.describe('Components Pages', () => { }) const page = await context.newPage() await page.goto('/') - await page.waitForTimeout(500) + await waitForFontsReady(page) const benchSection = page.getByText('Comparison Bechmarks').first() await benchSection.scrollIntoViewIfNeeded() @@ -144,11 +146,11 @@ test.describe('Components Pages', () => { }) const page = await context.newPage() await page.goto('/') - await page.waitForTimeout(500) + await waitForFontsReady(page) const benchSection = page.getByText('Comparison Bechmarks').first() await benchSection.scrollIntoViewIfNeeded() - await page.waitForTimeout(500) + await waitForStyleSettle(page) // Check that several competitor names are visible await expect(page.getByText('Chakra UI').first()).toBeVisible() @@ -163,11 +165,11 @@ test.describe('Components Pages', () => { }) const page = await context.newPage() await page.goto('/') - await page.waitForTimeout(500) + await waitForFontsReady(page) const benchSection = page.getByText('Comparison Bechmarks').first() await benchSection.scrollIntoViewIfNeeded() - await page.waitForTimeout(500) + await waitForStyleSettle(page) await expect(page).toHaveScreenshot( 'components-benchmark-section-desktop.png', @@ -183,11 +185,11 @@ test.describe('Components Pages', () => { }) const page = await context.newPage() await page.goto('/') - await page.waitForTimeout(500) + await waitForFontsReady(page) const benchSection = page.getByText('Comparison Bechmarks').first() await benchSection.scrollIntoViewIfNeeded() - await page.waitForTimeout(500) + await waitForStyleSettle(page) await expect(page).toHaveScreenshot( 'components-benchmark-section-mobile.png', @@ -204,11 +206,11 @@ test.describe('Components Pages', () => { }) const page = await context.newPage() await page.goto('/') - await page.waitForTimeout(500) + await waitForFontsReady(page) const benchSection = page.getByText('Comparison Bechmarks').first() await benchSection.scrollIntoViewIfNeeded() - await page.waitForTimeout(500) + await waitForStyleSettle(page) await expect(page).toHaveScreenshot( 'dark-components-benchmark-section-desktop.png', @@ -226,7 +228,7 @@ test.describe('Components Pages', () => { }) const page = await context.newPage() await page.goto('/') - await page.waitForTimeout(500) + await waitForFontsReady(page) const community = page.getByText('Join our community').first() await community.scrollIntoViewIfNeeded() @@ -241,7 +243,7 @@ test.describe('Components Pages', () => { }) const page = await context.newPage() await page.goto('/') - await page.waitForTimeout(500) + await waitForFontsReady(page) const discordLink = page .locator('a[href="https://discord.gg/8zjcGc7cWh"]') @@ -257,7 +259,7 @@ test.describe('Components Pages', () => { }) const page = await context.newPage() await page.goto('/') - await page.waitForTimeout(500) + await waitForFontsReady(page) const kakaoLink = page .locator('a[href="https://open.kakao.com/o/giONwVAh"]') diff --git a/e2e/docs-pages.spec.ts b/e2e/docs-pages.spec.ts index 9db879e0..09d965fe 100644 --- a/e2e/docs-pages.spec.ts +++ b/e2e/docs-pages.spec.ts @@ -1,5 +1,7 @@ import { expect, test } from '@playwright/test' +import { waitForFontsReady, waitForStyleSettle } from './helpers' + /** * All tests use javaScriptEnabled: false because the Next.js static export * serves correct SSR HTML, but client-side hydration triggers a 404 under @@ -20,7 +22,7 @@ test.describe('Documentation Pages', () => { }) const page = await context.newPage() await page.goto('/') - await page.waitForTimeout(500) + await waitForFontsReady(page) const docsLink = page.locator('a[href="/docs/overview"]').first() await expect(docsLink).toBeVisible() @@ -36,7 +38,7 @@ test.describe('Documentation Pages', () => { }) const page = await context.newPage() await page.goto('/') - await page.waitForTimeout(500) + await waitForFontsReady(page) const getStarted = page.locator('a[href="/docs/overview"]').filter({ hasText: 'Get started', @@ -53,7 +55,7 @@ test.describe('Documentation Pages', () => { }) const page = await context.newPage() await page.goto('/') - await page.waitForTimeout(500) + await waitForFontsReady(page) // The header nav link should not be visible on mobile const docsNavLink = page @@ -72,7 +74,7 @@ test.describe('Documentation Pages', () => { }) const page = await context.newPage() await page.goto('/') - await page.waitForTimeout(500) + await waitForFontsReady(page) const docsNavLink = page .locator('a[href="/docs/overview"]') @@ -91,7 +93,7 @@ test.describe('Documentation Pages', () => { }) const page = await context.newPage() await page.goto('/') - await page.waitForTimeout(500) + await waitForFontsReady(page) // The home page should mention Zero Runtime, which is a key concept await expect(page.getByText('Zero Runtime').first()).toBeVisible() @@ -106,7 +108,7 @@ test.describe('Documentation Pages', () => { }) const page = await context.newPage() await page.goto('/') - await page.waitForTimeout(500) + await waitForFontsReady(page) await expect(page.getByText('Features').first()).toBeVisible() @@ -120,7 +122,7 @@ test.describe('Documentation Pages', () => { }) const page = await context.newPage() await page.goto('/') - await page.waitForTimeout(500) + await waitForFontsReady(page) await expect(page.getByText('Type Safety').first()).toBeVisible() @@ -134,7 +136,7 @@ test.describe('Documentation Pages', () => { }) const page = await context.newPage() await page.goto('/') - await page.waitForTimeout(500) + await waitForFontsReady(page) const figmaPlugin = page.getByText('Figma Plugin').first() await figmaPlugin.scrollIntoViewIfNeeded() @@ -152,11 +154,11 @@ test.describe('Documentation Pages', () => { }) const page = await context.newPage() await page.goto('/') - await page.waitForTimeout(500) + await waitForFontsReady(page) const features = page.getByText('Features').first() await features.scrollIntoViewIfNeeded() - await page.waitForTimeout(500) + await waitForStyleSettle(page) await expect(page).toHaveScreenshot('docs-features-section-desktop.png', { fullPage: false, @@ -172,11 +174,11 @@ test.describe('Documentation Pages', () => { }) const page = await context.newPage() await page.goto('/') - await page.waitForTimeout(500) + await waitForFontsReady(page) const features = page.getByText('Features').first() await features.scrollIntoViewIfNeeded() - await page.waitForTimeout(500) + await waitForStyleSettle(page) await expect(page).toHaveScreenshot('docs-features-section-mobile.png', { fullPage: false, @@ -196,11 +198,11 @@ test.describe('Documentation Pages', () => { await page.evaluate(() => document.documentElement.setAttribute('data-theme', 'dark'), ) - await page.waitForTimeout(500) + await waitForStyleSettle(page) const features = page.getByText('Features').first() await features.scrollIntoViewIfNeeded() - await page.waitForTimeout(500) + await waitForStyleSettle(page) await expect(page).toHaveScreenshot( 'dark-docs-features-section-desktop.png', @@ -219,7 +221,7 @@ test.describe('Documentation Pages', () => { }) const page = await context.newPage() await page.goto('/') - await page.waitForTimeout(500) + await waitForFontsReady(page) const getStarted = page.getByText('Get started').first() await expect(getStarted).toBeVisible() @@ -234,7 +236,7 @@ test.describe('Documentation Pages', () => { }) const page = await context.newPage() await page.goto('/') - await page.waitForTimeout(500) + await waitForFontsReady(page) const getStarted = page.getByText('Get started').first() await expect(getStarted).toBeVisible() diff --git a/e2e/helpers.ts b/e2e/helpers.ts new file mode 100644 index 00000000..de880685 --- /dev/null +++ b/e2e/helpers.ts @@ -0,0 +1,23 @@ +import type { Page } from '@playwright/test' + +/** + * Wait for all fonts to be loaded and a rendering frame to complete. + * Replaces waitForTimeout(1000) after page.goto() + */ +export async function waitForFontsReady(page: Page): Promise { + await page.evaluate(() => document.fonts.ready) + // One extra rAF to let the browser paint with loaded fonts + await page.evaluate( + () => new Promise((resolve) => requestAnimationFrame(resolve)), + ) +} + +/** + * Wait for CSS transitions to settle after a style/theme change. + * Replaces waitForTimeout(100-300) after theme switches, scroll, evaluate, etc. + */ +export async function waitForStyleSettle(page: Page): Promise { + await page.evaluate( + () => new Promise((resolve) => requestAnimationFrame(resolve)), + ) +} diff --git a/e2e/landing-header.spec.ts b/e2e/landing-header.spec.ts index b6058cd3..f15c1b29 100644 --- a/e2e/landing-header.spec.ts +++ b/e2e/landing-header.spec.ts @@ -1,5 +1,7 @@ import { expect, test } from '@playwright/test' +import { waitForFontsReady, waitForStyleSettle } from './helpers' + /** * NOTE: Sub-page navigation is not possible in the current static export + * `serve -s` setup. Header tests that required sub-page navigation have been @@ -122,7 +124,7 @@ test.describe('Landing Page - Header & Navigation', () => { // Scroll down significantly await page.evaluate(() => window.scrollBy(0, 1000)) - await page.waitForTimeout(300) + await waitForStyleSettle(page) // Logo should still be visible because header is fixed/sticky await expect(logoLink).toBeVisible() @@ -159,7 +161,7 @@ test.describe('Landing Page - Header & Navigation', () => { } }) - await page.waitForTimeout(500) + await waitForStyleSettle(page) const newTheme = await page.evaluate(() => document.documentElement.getAttribute('data-theme'), @@ -211,7 +213,7 @@ test.describe('Landing Page - Header & Navigation', () => { await expect(menuButton).toBeVisible() await menuButton.click() - await page.waitForTimeout(1000) + await waitForFontsReady(page) // After clicking menu, the URL should contain menu=1 const url = page.url() @@ -255,7 +257,7 @@ test.describe('Landing Page - Header & Navigation', () => { if ((await searchInput.count()) > 0) { await searchInput.click() - await page.waitForTimeout(500) + await waitForStyleSettle(page) const url = page.url() const hasSearchParam = url.includes('search=') diff --git a/e2e/landing-interactions.spec.ts b/e2e/landing-interactions.spec.ts index 59c0eed6..dcc2588e 100644 --- a/e2e/landing-interactions.spec.ts +++ b/e2e/landing-interactions.spec.ts @@ -1,5 +1,7 @@ import { expect, test } from '@playwright/test' +import { waitForStyleSettle } from './helpers' + function normalizeColor(raw: string): string { const trimmed = raw.trim().toLowerCase() if (trimmed.startsWith('#')) { @@ -40,7 +42,7 @@ test.describe('Landing Page - Interactions', () => { // Hover await getStartedLink.hover() - await page.waitForTimeout(300) + await waitForStyleSettle(page) const bgAfter = await getStartedLink.evaluate((el) => { const inner = el.querySelector('div') || el @@ -69,7 +71,7 @@ test.describe('Landing Page - Interactions', () => { }) await discordLink.hover() - await page.waitForTimeout(300) + await waitForStyleSettle(page) const bgAfter = await discordLink.evaluate((el) => { const inner = el.querySelector('div') || el @@ -92,7 +94,7 @@ test.describe('Landing Page - Interactions', () => { }) await kakaoLink.hover() - await page.waitForTimeout(300) + await waitForStyleSettle(page) const bgAfter = await kakaoLink.evaluate((el) => { const inner = el.querySelector('div') || el @@ -121,7 +123,7 @@ test.describe('Landing Page - Interactions', () => { }) await figmaLink.hover() - await page.waitForTimeout(300) + await waitForStyleSettle(page) const bgAfter = await figmaLink.evaluate((el) => { const inner = el.querySelector('div') || el diff --git a/e2e/landing-responsive.spec.ts b/e2e/landing-responsive.spec.ts index 572c1215..20a66235 100644 --- a/e2e/landing-responsive.spec.ts +++ b/e2e/landing-responsive.spec.ts @@ -1,5 +1,7 @@ import { expect, test } from '@playwright/test' +import { waitForStyleSettle } from './helpers' + test.describe('Landing Page - Responsive Layout', () => { test.describe('Mobile viewport (375px)', () => { test.beforeEach(async ({ page }) => { @@ -147,7 +149,7 @@ test.describe('Landing Page - Responsive Layout', () => { // Resize to desktop await page.setViewportSize({ width: 1440, height: 900 }) // Allow CSS to recompute - await page.waitForTimeout(300) + await waitForStyleSettle(page) const desktopColumns = await page.evaluate(() => { const grids = Array.from(document.querySelectorAll('*')) diff --git a/e2e/landing-theme.spec.ts b/e2e/landing-theme.spec.ts index 6f2ff709..4c1006c2 100644 --- a/e2e/landing-theme.spec.ts +++ b/e2e/landing-theme.spec.ts @@ -1,5 +1,7 @@ import { expect, test } from '@playwright/test' +import { waitForStyleSettle } from './helpers' + /** * Normalize a color string to lowercase hex. */ @@ -33,7 +35,7 @@ test.describe('Landing Page - Theme Switching', () => { await page.evaluate(() => document.documentElement.setAttribute('data-theme', 'light'), ) - await page.waitForTimeout(100) + await waitForStyleSettle(page) const bodyBg = await page.evaluate( () => getComputedStyle(document.body).backgroundColor, @@ -47,7 +49,7 @@ test.describe('Landing Page - Theme Switching', () => { await page.evaluate(() => document.documentElement.setAttribute('data-theme', 'light'), ) - await page.waitForTimeout(100) + await waitForStyleSettle(page) const bodyColor = await page.evaluate( () => getComputedStyle(document.body).color, @@ -60,7 +62,7 @@ test.describe('Landing Page - Theme Switching', () => { await page.evaluate(() => document.documentElement.setAttribute('data-theme', 'dark'), ) - await page.waitForTimeout(200) + await waitForStyleSettle(page) // The main content wrapper (inside body) has bg=$background // which is #131313 in dark. Body itself has $footerBg = #2E303C in dark. @@ -76,7 +78,7 @@ test.describe('Landing Page - Theme Switching', () => { await page.evaluate(() => document.documentElement.setAttribute('data-theme', 'dark'), ) - await page.waitForTimeout(200) + await waitForStyleSettle(page) const bodyColor = await page.evaluate( () => getComputedStyle(document.body).color, @@ -96,7 +98,7 @@ test.describe('Landing Page - Theme Switching', () => { await page.evaluate(() => document.documentElement.setAttribute('data-theme', 'dark'), ) - await page.waitForTimeout(200) + await waitForStyleSettle(page) const darkFooterBg = await page.evaluate(() => { const footer = document.querySelector('footer') @@ -114,7 +116,7 @@ test.describe('Landing Page - Theme Switching', () => { await page.evaluate(() => document.documentElement.setAttribute('data-theme', 'light'), ) - await page.waitForTimeout(100) + await waitForStyleSettle(page) const lightBg = await page.evaluate( () => getComputedStyle(document.body).backgroundColor, ) @@ -123,7 +125,7 @@ test.describe('Landing Page - Theme Switching', () => { await page.evaluate(() => document.documentElement.setAttribute('data-theme', 'dark'), ) - await page.waitForTimeout(200) + await waitForStyleSettle(page) const darkBg = await page.evaluate( () => getComputedStyle(document.body).backgroundColor, ) @@ -132,7 +134,7 @@ test.describe('Landing Page - Theme Switching', () => { await page.evaluate(() => document.documentElement.setAttribute('data-theme', 'light'), ) - await page.waitForTimeout(200) + await waitForStyleSettle(page) const lightBgAgain = await page.evaluate( () => getComputedStyle(document.body).backgroundColor, ) diff --git a/e2e/landing-visual-dark.spec.ts b/e2e/landing-visual-dark.spec.ts index fda58bf3..39af2428 100644 --- a/e2e/landing-visual-dark.spec.ts +++ b/e2e/landing-visual-dark.spec.ts @@ -1,5 +1,7 @@ import { expect, test } from '@playwright/test' +import { waitForFontsReady, waitForStyleSettle } from './helpers' + /** * Mock the GitHub API to return a fixed star count so screenshots are deterministic. */ @@ -24,7 +26,7 @@ async function setDarkThemeAttribute(page: import('@playwright/test').Page) { await page.evaluate(() => document.documentElement.setAttribute('data-theme', 'dark'), ) - await page.waitForTimeout(300) + await waitForStyleSettle(page) } test.describe('Landing Page - Dark Mode Visual Regression', () => { @@ -36,7 +38,7 @@ test.describe('Landing Page - Dark Mode Visual Regression', () => { await page.goto('/') await page.waitForLoadState('networkidle') await setDarkThemeAttribute(page) - await page.waitForTimeout(1000) + await waitForFontsReady(page) await expect(page).toHaveScreenshot('dark-full-page-mobile.png', { fullPage: true, @@ -50,7 +52,7 @@ test.describe('Landing Page - Dark Mode Visual Regression', () => { await page.goto('/') await page.waitForLoadState('networkidle') await setDarkThemeAttribute(page) - await page.waitForTimeout(1000) + await waitForFontsReady(page) await expect(page).toHaveScreenshot('dark-full-page-desktop.png', { fullPage: true, @@ -66,7 +68,7 @@ test.describe('Landing Page - Dark Mode Visual Regression', () => { await page.goto('/') await page.waitForLoadState('networkidle') await setDarkThemeAttribute(page) - await page.waitForTimeout(1000) + await waitForFontsReady(page) }) test('TopBanner section (dark)', async ({ page }) => { @@ -84,7 +86,7 @@ test.describe('Landing Page - Dark Mode Visual Regression', () => { test('Feature section (dark)', async ({ page }) => { const featureHeading = page.getByText('Features', { exact: true }).first() await featureHeading.scrollIntoViewIfNeeded() - await page.waitForTimeout(300) + await waitForStyleSettle(page) const featureSection = featureHeading .locator('..') @@ -97,7 +99,7 @@ test.describe('Landing Page - Dark Mode Visual Regression', () => { test('Bench section (dark)', async ({ page }) => { const benchHeading = page.getByText('Comparison Bechmarks').first() await benchHeading.scrollIntoViewIfNeeded() - await page.waitForTimeout(300) + await waitForStyleSettle(page) const benchSection = benchHeading .locator('..') @@ -110,7 +112,7 @@ test.describe('Landing Page - Dark Mode Visual Regression', () => { test('Discord section (dark)', async ({ page }) => { const discordHeading = page.getByText('Join our community').first() await discordHeading.scrollIntoViewIfNeeded() - await page.waitForTimeout(300) + await waitForStyleSettle(page) const discordSection = discordHeading .locator('..') @@ -123,7 +125,7 @@ test.describe('Landing Page - Dark Mode Visual Regression', () => { test('Footer section (dark)', async ({ page }) => { const footer = page.locator('footer') await footer.scrollIntoViewIfNeeded() - await page.waitForTimeout(300) + await waitForStyleSettle(page) await expect(footer).toHaveScreenshot('dark-section-footer.png') }) diff --git a/e2e/landing-visual-hover.spec.ts b/e2e/landing-visual-hover.spec.ts index 366218e3..794d1ea0 100644 --- a/e2e/landing-visual-hover.spec.ts +++ b/e2e/landing-visual-hover.spec.ts @@ -1,5 +1,7 @@ import { expect, test } from '@playwright/test' +import { waitForFontsReady, waitForStyleSettle } from './helpers' + /** * Mock the GitHub API to return a fixed star count so screenshots are deterministic. */ @@ -22,7 +24,7 @@ test.describe('Landing Page - Hover State Visual Regression', () => { await page.setViewportSize({ width: 1440, height: 900 }) await page.goto('/') await page.waitForLoadState('networkidle') - await page.waitForTimeout(1000) + await waitForFontsReady(page) }) test('GetStarted button hover screenshot', async ({ page }) => { @@ -34,7 +36,7 @@ test.describe('Landing Page - Hover State Visual Regression', () => { ) await getStartedLink.hover() - await page.waitForTimeout(300) + await waitForStyleSettle(page) await expect(getStartedLink).toHaveScreenshot('hover-get-started-after.png') }) @@ -46,7 +48,7 @@ test.describe('Landing Page - Hover State Visual Regression', () => { await expect(starLink).toHaveScreenshot('hover-star-before.png') await starLink.hover() - await page.waitForTimeout(300) + await waitForStyleSettle(page) await expect(starLink).toHaveScreenshot('hover-star-after.png') }) @@ -58,7 +60,7 @@ test.describe('Landing Page - Hover State Visual Regression', () => { await expect(sponsorLink).toHaveScreenshot('hover-sponsor-before.png') await sponsorLink.hover() - await page.waitForTimeout(300) + await waitForStyleSettle(page) await expect(sponsorLink).toHaveScreenshot('hover-sponsor-after.png') }) @@ -71,7 +73,7 @@ test.describe('Landing Page - Hover State Visual Regression', () => { await expect(discordLink).toHaveScreenshot('hover-discord-before.png') await discordLink.hover() - await page.waitForTimeout(300) + await waitForStyleSettle(page) await expect(discordLink).toHaveScreenshot('hover-discord-after.png') }) @@ -84,7 +86,7 @@ test.describe('Landing Page - Hover State Visual Regression', () => { await expect(kakaoLink).toHaveScreenshot('hover-kakao-before.png') await kakaoLink.hover() - await page.waitForTimeout(300) + await waitForStyleSettle(page) await expect(kakaoLink).toHaveScreenshot('hover-kakao-after.png') }) @@ -92,7 +94,7 @@ test.describe('Landing Page - Hover State Visual Regression', () => { test('Feature card hover screenshot', async ({ page }) => { const featureHeading = page.getByText('Features', { exact: true }).first() await featureHeading.scrollIntoViewIfNeeded() - await page.waitForTimeout(300) + await waitForStyleSettle(page) // Find the first feature card in the grid const featureSection = featureHeading @@ -111,7 +113,7 @@ test.describe('Landing Page - Hover State Visual Regression', () => { await expect(firstCard).toHaveScreenshot('hover-feature-card-before.png') await firstCard.hover() - await page.waitForTimeout(300) + await waitForStyleSettle(page) await expect(firstCard).toHaveScreenshot('hover-feature-card-after.png') }) diff --git a/e2e/landing-visual.spec.ts b/e2e/landing-visual.spec.ts index 268680a3..1af86867 100644 --- a/e2e/landing-visual.spec.ts +++ b/e2e/landing-visual.spec.ts @@ -1,5 +1,7 @@ import { expect, test } from '@playwright/test' +import { waitForFontsReady, waitForStyleSettle } from './helpers' + /** * Mock the GitHub API to return a fixed star count so screenshots are deterministic. * StarButton fetches: https://api.github.com/repos/dev-five-git/devup-ui @@ -25,7 +27,7 @@ test.describe('Landing Page - Visual Regression', () => { await page.goto('/') await page.waitForLoadState('networkidle') // Wait for fonts and images - await page.waitForTimeout(1000) + await waitForFontsReady(page) await expect(page).toHaveScreenshot('full-page-mobile.png', { fullPage: true, @@ -37,7 +39,7 @@ test.describe('Landing Page - Visual Regression', () => { await page.setViewportSize({ width: 1440, height: 900 }) await page.goto('/') await page.waitForLoadState('networkidle') - await page.waitForTimeout(1000) + await waitForFontsReady(page) await expect(page).toHaveScreenshot('full-page-desktop.png', { fullPage: true, @@ -51,7 +53,7 @@ test.describe('Landing Page - Visual Regression', () => { await page.setViewportSize({ width: 1440, height: 900 }) await page.goto('/') await page.waitForLoadState('networkidle') - await page.waitForTimeout(1000) + await waitForFontsReady(page) }) test('TopBanner section', async ({ page }) => { @@ -70,7 +72,7 @@ test.describe('Landing Page - Visual Regression', () => { test('Feature section', async ({ page }) => { const featureHeading = page.getByText('Features', { exact: true }).first() await featureHeading.scrollIntoViewIfNeeded() - await page.waitForTimeout(300) + await waitForStyleSettle(page) // Get the feature section container const featureSection = featureHeading @@ -84,7 +86,7 @@ test.describe('Landing Page - Visual Regression', () => { test('Bench section', async ({ page }) => { const benchHeading = page.getByText('Comparison Bechmarks').first() await benchHeading.scrollIntoViewIfNeeded() - await page.waitForTimeout(300) + await waitForStyleSettle(page) const benchSection = benchHeading .locator('..') @@ -97,7 +99,7 @@ test.describe('Landing Page - Visual Regression', () => { test('Discord section', async ({ page }) => { const discordHeading = page.getByText('Join our community').first() await discordHeading.scrollIntoViewIfNeeded() - await page.waitForTimeout(300) + await waitForStyleSettle(page) const discordSection = discordHeading .locator('..') @@ -110,7 +112,7 @@ test.describe('Landing Page - Visual Regression', () => { test('Footer section', async ({ page }) => { const footer = page.locator('footer') await footer.scrollIntoViewIfNeeded() - await page.waitForTimeout(300) + await waitForStyleSettle(page) await expect(footer).toHaveScreenshot('section-footer.png') }) diff --git a/e2e/landing-zero-runtime.spec.ts b/e2e/landing-zero-runtime.spec.ts index bb496a83..a926f256 100644 --- a/e2e/landing-zero-runtime.spec.ts +++ b/e2e/landing-zero-runtime.spec.ts @@ -1,5 +1,7 @@ import { expect, test } from '@playwright/test' +import { waitForStyleSettle } from './helpers' + test.describe('Landing Page - Zero Runtime Validation', () => { test.beforeEach(async ({ page }) => { await page.goto('/') @@ -16,9 +18,9 @@ test.describe('Landing Page - Zero Runtime Validation', () => { // Interact with the page: scroll, hover, etc. await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)) - await page.waitForTimeout(500) + await waitForStyleSettle(page) await page.evaluate(() => window.scrollTo(0, 0)) - await page.waitForTimeout(500) + await waitForStyleSettle(page) // Count again after interactions const afterInteractionStyleCount = await page.evaluate( @@ -152,18 +154,18 @@ test.describe('Landing Page - Zero Runtime Validation', () => { const getStarted = page.getByRole('link', { name: /Get started/i }) if (await getStarted.isVisible()) { await getStarted.hover() - await page.waitForTimeout(200) + await waitForStyleSettle(page) } const discord = page.getByRole('link', { name: /Join our Discord/i }) if (await discord.isVisible()) { await discord.hover() - await page.waitForTimeout(200) + await waitForStyleSettle(page) } // Move away await page.mouse.move(0, 0) - await page.waitForTimeout(200) + await waitForStyleSettle(page) const finalCount = await page.evaluate( () => document.querySelectorAll('style').length, From 42e188e7d9d5a36edf073659936d9a846caed680 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Thu, 12 Feb 2026 04:22:00 +0900 Subject: [PATCH 07/12] Optimize --- .changepacks/changepack_log_OrV9v1BPnNlYKLMdw7q39.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changepacks/changepack_log_OrV9v1BPnNlYKLMdw7q39.json diff --git a/.changepacks/changepack_log_OrV9v1BPnNlYKLMdw7q39.json b/.changepacks/changepack_log_OrV9v1BPnNlYKLMdw7q39.json new file mode 100644 index 00000000..730902b2 --- /dev/null +++ b/.changepacks/changepack_log_OrV9v1BPnNlYKLMdw7q39.json @@ -0,0 +1 @@ +{"changes":{"packages/webpack-plugin/package.json":"Patch","packages/next-plugin/package.json":"Patch","packages/plugin-utils/package.json":"Patch","packages/rsbuild-plugin/package.json":"Patch","packages/vite-plugin/package.json":"Patch","bindings/devup-ui-wasm/package.json":"Patch","packages/eslint-plugin/package.json":"Patch","packages/components/package.json":"Patch"},"note":"Optimize","date":"2026-02-11T19:21:50.181781Z"} \ No newline at end of file From 8c77be883f069a168bde529f0fbea97a17f14d57 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Thu, 12 Feb 2026 05:02:18 +0900 Subject: [PATCH 08/12] Fix test --- e2e/helpers.ts | 23 +++++++++++++++-------- e2e/landing-header.spec.ts | 4 ++-- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/e2e/helpers.ts b/e2e/helpers.ts index de880685..123dbd77 100644 --- a/e2e/helpers.ts +++ b/e2e/helpers.ts @@ -3,21 +3,28 @@ import type { Page } from '@playwright/test' /** * Wait for all fonts to be loaded and a rendering frame to complete. * Replaces waitForTimeout(1000) after page.goto() + * + * Falls back to waitForLoadState('load') when JavaScript is disabled + * (page.evaluate is not available in JS-disabled contexts). */ export async function waitForFontsReady(page: Page): Promise { - await page.evaluate(() => document.fonts.ready) - // One extra rAF to let the browser paint with loaded fonts - await page.evaluate( - () => new Promise((resolve) => requestAnimationFrame(resolve)), - ) + try { + await page.evaluate(async () => { + await document.fonts.ready + await page.waitForLoadState('load') + }) + } catch { + // JS disabled — fall back to load event (fires after fonts in CSS are loaded) + await page.waitForLoadState('load') + } } /** * Wait for CSS transitions to settle after a style/theme change. * Replaces waitForTimeout(100-300) after theme switches, scroll, evaluate, etc. + * + * Falls back to waitForLoadState('load') when JavaScript is disabled. */ export async function waitForStyleSettle(page: Page): Promise { - await page.evaluate( - () => new Promise((resolve) => requestAnimationFrame(resolve)), - ) + await page.waitForTimeout(10) } diff --git a/e2e/landing-header.spec.ts b/e2e/landing-header.spec.ts index f15c1b29..33f19007 100644 --- a/e2e/landing-header.spec.ts +++ b/e2e/landing-header.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test' -import { waitForFontsReady, waitForStyleSettle } from './helpers' +import { waitForStyleSettle } from './helpers' /** * NOTE: Sub-page navigation is not possible in the current static export + @@ -213,7 +213,7 @@ test.describe('Landing Page - Header & Navigation', () => { await expect(menuButton).toBeVisible() await menuButton.click() - await waitForFontsReady(page) + await page.waitForTimeout(10) // After clicking menu, the URL should contain menu=1 const url = page.url() From 77a3e8ec1d6423d4884636bdd0d04622637d5200 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Thu, 12 Feb 2026 05:09:01 +0900 Subject: [PATCH 09/12] Add case --- libs/css/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libs/css/src/lib.rs b/libs/css/src/lib.rs index 66174434..6801cc46 100644 --- a/libs/css/src/lib.rs +++ b/libs/css/src/lib.rs @@ -468,6 +468,10 @@ mod tests { #[case(";", "_sc_")] #[case("{", "_lc_")] #[case("}", "_rc_")] + // ASCII not in lookup table and not alphanumeric/-/_ (line 212 branch) + #[case("`", "_u0060_")] + #[case("\t", "_u0009_")] + #[case("\x01", "_u0001_")] // Unicode character (non-ASCII) #[case("한글", "_ud55c__uae00_")] #[case("emoji😀", "emoji_u1f600_")] From 872da2813ecc6b1f9796b2879893b578446be0a0 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Thu, 12 Feb 2026 05:14:27 +0900 Subject: [PATCH 10/12] Add case --- e2e/landing-header.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/landing-header.spec.ts b/e2e/landing-header.spec.ts index 33f19007..1439414e 100644 --- a/e2e/landing-header.spec.ts +++ b/e2e/landing-header.spec.ts @@ -213,7 +213,7 @@ test.describe('Landing Page - Header & Navigation', () => { await expect(menuButton).toBeVisible() await menuButton.click() - await page.waitForTimeout(10) + await page.waitForTimeout(100) // After clicking menu, the URL should contain menu=1 const url = page.url() From 48bfc7d6f5becf29f2fe0fcab27001bb6b4e8973 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Thu, 12 Feb 2026 05:29:53 +0900 Subject: [PATCH 11/12] Add case --- e2e/landing-header.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/landing-header.spec.ts b/e2e/landing-header.spec.ts index 1439414e..d53e7de2 100644 --- a/e2e/landing-header.spec.ts +++ b/e2e/landing-header.spec.ts @@ -213,7 +213,7 @@ test.describe('Landing Page - Header & Navigation', () => { await expect(menuButton).toBeVisible() await menuButton.click() - await page.waitForTimeout(100) + await page.waitForTimeout(1000) // After clicking menu, the URL should contain menu=1 const url = page.url() From 14de6e33e8b7fcafb13fc1167357d4da2b89f496 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Thu, 12 Feb 2026 05:36:56 +0900 Subject: [PATCH 12/12] Add case --- e2e/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/helpers.ts b/e2e/helpers.ts index 123dbd77..5d409ccf 100644 --- a/e2e/helpers.ts +++ b/e2e/helpers.ts @@ -26,5 +26,5 @@ export async function waitForFontsReady(page: Page): Promise { * Falls back to waitForLoadState('load') when JavaScript is disabled. */ export async function waitForStyleSettle(page: Page): Promise { - await page.waitForTimeout(10) + await page.waitForTimeout(100) }