diff --git a/.changepacks/changepack_log_ci_hardening.json b/.changepacks/changepack_log_ci_hardening.json new file mode 100644 index 000000000..b03f09a46 --- /dev/null +++ b/.changepacks/changepack_log_ci_hardening.json @@ -0,0 +1,7 @@ +{ + "changes": { + "bindings/devup-ui-wasm/package.json": "Patch" + }, + "note": "CI and test infrastructure: the workflow always runs the latest releases (the ubuntu-latest runner, the newest major version of every action, and the latest stable Rust, Bun, Node.js, wasm-pack, cargo-binstall, cargo-tarpaulin and changepacks), runs pull requests read-only and moves npm publishing and Pages deployment into jobs that run only on pushes to main, never creates or commits e2e baselines, and sets timeouts. The wasm package build now starts from an empty pkg/ and verify:dist rejects files of the removed lite export, its README describes the real API, state and errors, and a Bun test checks the JS/WASM interface. The README benchmark tables are generated from one checked result file in both languages with the CSS size in its own column.", + "date": "2026-10-01T00:00:00.000Z" +} \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0e6edbd21..2cad8f11a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -7,9 +7,17 @@ on: pull_request: branches: - main + +# Pull requests and every job below run read-only. Only the `release` and +# `deploy-pages` jobs get write permissions, and they run on pushes to main +# alone, so no step a pull request controls can publish, tag, comment or deploy. permissions: contents: read +# CI always runs the latest releases: the `ubuntu-latest` runner, the newest +# major version of every action, and the latest stable Rust, Bun, Node.js, +# wasm-pack, cargo-binstall, cargo-tarpaulin and changepacks. + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: false @@ -17,11 +25,15 @@ concurrency: jobs: benchmark: runs-on: ubuntu-latest + timeout-minutes: 60 steps: - name: Checkout uses: actions/checkout@v7 - uses: actions-rust-lang/setup-rust-toolchain@v2 + with: + toolchain: stable + components: clippy, rustfmt - name: Cache cargo registry + target uses: actions/cache@v6 @@ -34,7 +46,7 @@ jobs: key: cargo-benchmark-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} - uses: oven-sh/setup-bun@v2 with: - bun-version: "1.4.0" + bun-version: latest name: Install bun - name: Cache bun dependencies @@ -44,16 +56,24 @@ jobs: key: bun-benchmark-${{ runner.os }}-${{ hashFiles('**/bun.lock') }} - uses: jetli/wasm-pack-action@v0.4.0 with: - version: "latest" + version: latest - name: Install Node.js uses: actions/setup-node@v7 with: registry-url: "https://registry.npmjs.org" - node-version: 24 + node-version: latest - run: bun install - run: bun run build - name: Benchmark run: bun run benchmark + - name: Upload the measured run + uses: actions/upload-artifact@v7 + if: ${{ !cancelled() }} + with: + name: benchmark-run + path: benchmark-run.json + if-no-files-found: ignore + retention-days: 30 - name: Profile Devup UI Turbopack env: DEVUP_UI_PROFILE: "1" @@ -67,16 +87,20 @@ jobs: # `publish` runs it against the deployed vinext/@devup-ui/vite-plugin build. # This job runs the SAME 128 assertions and the SAME screenshot baselines # against a Next 16 + Turbopack build, so @devup-ui/next-plugin's production - # CSS path stays gated at 64 routes — the scale where extraction-wave races + # CSS path stays gated at 64 routes - the scale where extraction-wave races # actually reproduce. It runs in parallel so it stays off the publish critical # path, and its artifact is never deployed. landing-next-e2e: runs-on: ubuntu-latest + timeout-minutes: 60 steps: - name: Checkout uses: actions/checkout@v7 - uses: actions-rust-lang/setup-rust-toolchain@v2 + with: + toolchain: stable + components: clippy, rustfmt - name: Cache cargo registry + target uses: actions/cache@v6 @@ -89,7 +113,7 @@ jobs: key: cargo-landing-next-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} - uses: oven-sh/setup-bun@v2 with: - bun-version: "1.4.0" + bun-version: latest name: Install bun - name: Cache bun dependencies @@ -99,11 +123,11 @@ jobs: key: bun-landing-next-${{ runner.os }}-${{ hashFiles('**/bun.lock') }} - uses: jetli/wasm-pack-action@v0.4.0 with: - version: "latest" + version: latest - name: Install Node.js uses: actions/setup-node@v7 with: - node-version: 24 + node-version: latest - run: bun install - run: bun run build - name: Build Landing (Next) @@ -138,6 +162,65 @@ jobs: path: playwright-report/ retention-days: 1 + # The packages take React ^18 || ^19 as a peer. This builds a React 18.3 app + # with the Vite plugin and the UI kit, then checks refs, context and events + # in Chromium, so React-19-only APIs cannot slip back in. + react-18: + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - name: Checkout + uses: actions/checkout@v7 + + - uses: actions-rust-lang/setup-rust-toolchain@v2 + with: + toolchain: stable + components: clippy, rustfmt + + - name: Cache cargo registry + target + uses: actions/cache@v6 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: cargo-react-18-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + name: Install bun + + - name: Cache bun dependencies + uses: actions/cache@v6 + with: + path: ~/.bun/install/cache + key: bun-react-18-${{ runner.os }}-${{ hashFiles('**/bun.lock') }} + - uses: jetli/wasm-pack-action@v0.4.0 + with: + version: latest + - name: Install Node.js + uses: actions/setup-node@v7 + with: + node-version: latest + - run: bun install + - run: bun run build + - name: Type-check and build the React 18 app + working-directory: apps/react-18 + run: | + bun ../../node_modules/@typescript/native/bin/tsc + bun run build + - name: Cache Playwright Browsers + uses: actions/cache@v6 + with: + path: ~/.cache/ms-playwright + key: playwright-react-18-${{ runner.os }}-${{ hashFiles('**/bun.lock') }} + - name: Install Playwright Browsers + run: bunx playwright install chromium --with-deps + - name: Run React 18 E2E Tests + working-directory: apps/react-18 + run: bun run test:e2e + # @devup-ui/vite-plugin must keep the CSS every server page needs when the # build is vinext/RSC. The fixture shares one Devup UI header between server # list pages and a client page, and the tests read the styles the browser @@ -146,11 +229,16 @@ jobs: vinext-rsc-css-e2e: runs-on: ubuntu-latest timeout-minutes: 30 + permissions: + contents: read steps: - name: Checkout uses: actions/checkout@v7 - uses: actions-rust-lang/setup-rust-toolchain@v2 + with: + toolchain: stable + components: clippy, rustfmt - name: Cache cargo registry + target uses: actions/cache@v6 @@ -163,7 +251,7 @@ jobs: key: cargo-vinext-rsc-css-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} - uses: oven-sh/setup-bun@v2 with: - bun-version: "1.4.0" + bun-version: latest name: Install bun - name: Cache bun dependencies @@ -173,11 +261,11 @@ jobs: key: bun-vinext-rsc-css-${{ runner.os }}-${{ hashFiles('**/bun.lock') }} - uses: jetli/wasm-pack-action@v0.4.0 with: - version: "latest" + version: latest - name: Install Node.js uses: actions/setup-node@v7 with: - node-version: 24 + node-version: latest - run: bun install - run: bun run build - name: Cache Playwright Browsers @@ -197,24 +285,27 @@ jobs: DEVUP_SINGLE_CSS=1 bun run --filter vinext-rsc-css-repro build - name: Run E2E Tests (singleCss) run: bun run test:e2e:rsc-css + + # Validation: runs on pull requests and on pushes to main, read-only. The job + # id stays `publish` because it is the status check the repository requires; + # the privileged release steps live in `release` and `deploy-pages` below. publish: runs-on: ubuntu-latest - permissions: - contents: write - id-token: write - pages: write - pull-requests: write + timeout-minutes: 90 steps: - name: Checkout uses: actions/checkout@v7 with: # changepacks diffs HEAD against the previous release commit. The # default shallow fetch grafts away every parent, so that lookup dies - # with "Could not find a merge-base" and the release never publishes. + # with "Could not find a merge-base". fetch-depth: 0 fetch-tags: true - uses: actions-rust-lang/setup-rust-toolchain@v2 + with: + toolchain: stable + components: clippy, rustfmt - 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 @@ -233,7 +324,7 @@ jobs: - uses: oven-sh/setup-bun@v2 with: - bun-version: "1.4.0" + bun-version: latest name: Install bun - name: Cache bun dependencies @@ -244,16 +335,18 @@ jobs: - uses: jetli/wasm-pack-action@v0.4.0 with: - version: "latest" + version: latest - name: Install Node.js uses: actions/setup-node@v7 with: registry-url: "https://registry.npmjs.org" - node-version: 24 + node-version: latest - run: bun install - run: bun run build - name: Verify built package entry points run: bun run verify:dist + - name: Verify the README benchmark tables + run: bun render-benchmark-readme.js --check - name: Verify publish tarballs shell: bash run: | @@ -294,10 +387,16 @@ jobs: rm -rf .rustfmt.toml # Formatting with the temporary coverage config and then with the # default config is not a reversible round trip. Restore the exact - # checked-in sources so changepacks can safely switch commits. + # checked-in sources so the checkout is clean again. git restore --source=HEAD --staged --worktree -- . cargo fmt --all -- --check git diff --exit-code + - name: Check the changepacks of this change + if: github.event_name == 'pull_request' + run: | + # changepacks compares the change against a local main, which a pull request checkout does not have + git fetch --no-tags origin main:refs/heads/main + bunx @changepacks/cli@latest check --format json - name: Build Landing env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -313,21 +412,17 @@ jobs: key: playwright-publish-${{ runner.os }}-${{ hashFiles('**/bun.lock') }} - name: Install Playwright Browsers run: bunx playwright install chromium --with-deps - - name: Check for Existing E2E Snapshots - id: check-snapshots + # A run only compares against the screenshots checked into the repository: + # it never creates or commits them. A new visual test needs its baselines + # generated by a maintainer (`bun run test:e2e:update`) and committed. + - name: Require the E2E baselines run: | - SNAPSHOT_COUNT=$(find e2e -name '*.png' -path '*-snapshots/*' 2>/dev/null | wc -l) - echo "count=$SNAPSHOT_COUNT" >> $GITHUB_OUTPUT - echo "Found $SNAPSHOT_COUNT existing snapshot(s)" - - name: Generate E2E Snapshots - if: steps.check-snapshots.outputs.count == '0' - run: bunx playwright test --update-snapshots - - name: Commit E2E Snapshots - if: steps.check-snapshots.outputs.count == '0' && github.event_name == 'pull_request' - uses: stefanzweifel/git-auto-commit-action@v7 - with: - commit_message: "chore: generate e2e visual snapshots from CI" - file_pattern: "e2e/**/*-snapshots/*.png" + SNAPSHOT_COUNT=$(find e2e -name '*-linux.png' -path '*-snapshots/*' 2>/dev/null | wc -l) + echo "Found $SNAPSHOT_COUNT linux baseline snapshot(s)" + if [ "$SNAPSHOT_COUNT" -eq 0 ]; then + echo "No checked-in baselines under e2e/**/*-snapshots; generate them with 'bun run test:e2e:update' and commit them." >&2 + exit 1 + fi - name: Run E2E Tests run: bun run test:e2e - name: Build Landing (singleCss) @@ -345,14 +440,12 @@ jobs: name: playwright-report-singlecss path: playwright-report/ retention-days: 1 - - name: Upload artifact + - name: Upload the site for deployment uses: actions/upload-pages-artifact@v5 with: path: ./apps/landing/dist/client retention-days: 1 if: github.event_name == 'push' && github.ref == 'refs/heads/main' - - uses: actions/deploy-pages@v5 - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - name: Upload to codecov.io uses: codecov/codecov-action@v7 with: @@ -360,19 +453,94 @@ jobs: fail_ci_if_error: true files: ./coverage/lcov.info - # Publishes through npm OIDC trusted publishing. The job's - # `id-token: write` permission lets npm exchange GitHub's short-lived OIDC - # token, and public packages receive provenance automatically. - # - # PREREQUISITE: every @devup-ui/* package must be registered as a Trusted - # Publisher on npmjs.com against repo dev-five-git/devup-ui and workflow - # file `publish.yml`. npm matches that filename exactly, so renaming or - # moving this file breaks publishing until the entries are updated. - # - # The publish command itself is selected in .changepacks/config.json. - # Bun packs each workspace so dependency ranges are materialized, then - # npm publishes that tarball with OIDC (Bun cannot do OIDC itself: - # oven-sh/bun#22423). + # Pages deployment: the only job with `pages` and `id-token` write, and it + # only runs for a push to main whose validation passed. + deploy-pages: + needs: [publish] + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + pages: write + id-token: write + steps: + - uses: actions/deploy-pages@v5 + + # Publishes through npm OIDC trusted publishing. The job's `id-token: write` + # permission lets npm exchange GitHub's short-lived OIDC token, and public + # packages receive provenance automatically. + # + # PREREQUISITE: every @devup-ui/* package must be registered as a Trusted + # Publisher on npmjs.com against repo dev-five-git/devup-ui and workflow + # file `publish.yml`. npm matches that filename exactly, so renaming or + # moving this file breaks publishing until the entries are updated. + # + # The publish command itself is selected in .changepacks/config.json. + # Bun packs each workspace so dependency ranges are materialized, then + # npm publishes that tarball with OIDC (Bun cannot do OIDC itself: + # oven-sh/bun#22423). + # + # It runs only for a push to main whose validation and end-to-end jobs + # passed, and is the only job that can write contents, open pull requests or + # mint an OIDC token for npm. + release: + needs: [publish, landing-next-e2e, react-18] + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 45 + permissions: + contents: write + id-token: write + pull-requests: write + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + # changepacks diffs HEAD against the previous release commit. The + # default shallow fetch grafts away every parent, so that lookup dies + # with "Could not find a merge-base" and the release never publishes. + fetch-depth: 0 + fetch-tags: true + + - uses: actions-rust-lang/setup-rust-toolchain@v2 + with: + toolchain: stable + components: clippy, rustfmt + + - name: Cache cargo registry + target + uses: actions/cache@v6 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: cargo-release-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + name: Install bun + + - name: Cache bun dependencies + uses: actions/cache@v6 + with: + path: ~/.bun/install/cache + key: bun-release-${{ runner.os }}-${{ hashFiles('**/bun.lock') }} + + - uses: jetli/wasm-pack-action@v0.4.0 + with: + version: latest + - name: Install Node.js + uses: actions/setup-node@v7 + with: + registry-url: "https://registry.npmjs.org" + node-version: latest + - run: bun install + - run: bun run build + - name: Verify built package entry points + run: bun run verify:dist - name: Verify clean publish checkout run: git diff --exit-code - uses: changepacks/action@main diff --git a/.gitignore b/.gitignore index eb80866ac..7e5ea71be 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ playwright-report .playwright-mcp .omo /.changepacks/publish.tgz +benchmark-run.json diff --git a/README.md b/README.md index 556be5e8e..49e4e4606 100644 --- a/README.md +++ b/README.md @@ -79,31 +79,35 @@ npm install @devup-ui/webpack-plugin ## Comparison Benchmarks -[Latest CI benchmark](https://github.com/dev-five-git/devup-ui/actions/runs/33254030962) on `ubuntu-latest` with Next.js 16.3.3. All Next.js builds use the native TypeScript 7 CLI for type checking. + +[Latest CI benchmark](https://github.com/dev-five-git/devup-ui/actions/runs/36702867475) (commit `8d6c2f62`, Devup UI 1.0.44) on `ubuntu-24.04` with Next.js 16.3.3. All Next.js builds use the native TypeScript 7 CLI for type checking. The English and Korean tables are generated from the same checked result file, `benchmark-results.json`. + +Build Size is every byte of the build output directory (`.next`, or `dist` for vinext); CSS Size is the emitted `.css` files alone, measured separately. Webpack values are one cold build: -| Library | Version | Build Time | Build Size | -| --------------------------- | ------- | ---------- | ----------------- | -| tailwindcss | 4.3.3 | 12.81s | 66,453,190 bytes | -| styleX | 0.19.0 | 27.61s | 95,413,403 bytes | -| vanilla-extract | 1.21.2 | 11.96s | 67,689,923 bytes | -| kuma-ui | 1.6.4 | 13.07s | 74,747,637 bytes | -| panda-css | 1.12.0 | 13.20s | 70,970,208 bytes | -| chakra-ui | 3.37.0 | 19.98s | 206,589,898 bytes | -| mui | 9.4.0 | 13.78s | 100,603,186 bytes | -| **devup-ui (per-file CSS)** | 1.0.40 | **10.73s** | 66,535,095 bytes | -| **devup-ui (single CSS)** | 1.0.40 | **10.81s** | 66,535,321 bytes | +| Library | Version | Build Time | Build Size | CSS Size | +| --------------------------- | ------- | ---------- | ----------------- | ------------ | +| tailwindcss | 4.3.3 | 16.67s | 67,332,642 bytes | 5,852 bytes | +| styleX | 0.19.0 | 38.08s | 96,291,287 bytes | 425 bytes | +| vanilla-extract | 1.21.2 | 16.09s | 68,565,036 bytes | 294 bytes | +| kuma-ui | 1.6.4 | 17.91s | 75,627,783 bytes | 340 bytes | +| panda-css | 1.12.0 | 18.16s | 71,849,418 bytes | 15,615 bytes | +| chakra-ui | 3.37.0 | 27.22s | 207,426,274 bytes | 0 bytes | +| mui | 9.4.0 | 18.78s | 101,473,260 bytes | 0 bytes | +| **devup-ui (per-file CSS)** | 1.0.44 | **14.27s** | 67,424,897 bytes | 562 bytes | +| **devup-ui (single CSS)** | 1.0.44 | **14.17s** | 67,426,959 bytes | 790 bytes | Turbopack values are medians of six cold builds in alternating order: -| Library | Version | Median Build Time | Build Size | -| -------------------------------------- | ------- | ----------------- | -------------------- | -| tailwindcss | 4.3.3 | 5.52s | 38,425,132 bytes | -| **devup-ui (direct APIs, single CSS)** | 1.0.40 | **5.46s** | **36,507,782 bytes** | -| **devup-ui (static `.css.ts`)** | 1.0.40 | **5.41s** | 36,586,883 bytes | +| Library | Version | Median Build Time | Build Size | CSS Size | +| -------------------------------------- | ------- | ----------------- | ---------------- | ----------- | +| tailwindcss | 4.3.3 | 6.98s | 38,422,141 bytes | 6,197 bytes | +| **devup-ui (direct APIs, single CSS)** | 1.0.44 | **6.93s** | 36,518,952 bytes | 327 bytes | +| **devup-ui (static `.css.ts`)** | 1.0.44 | **6.92s** | 36,578,506 bytes | 268 bytes | -The Turbopack ranges overlap, so the direct-API median is 0.06s (1.1%) ahead but still effectively parity with Tailwind on this fixture. The six cold samples were Tailwind `5.42, 5.56, 5.70, 5.53, 5.41, 5.52s`, direct Devup UI `5.64, 5.40, 5.42, 5.46, 5.54, 5.46s`, and static `.css.ts` `5.42, 5.38, 5.42, 5.47, 5.40, 5.38s`. The fixtures have comparable app shapes, not pixel-identical styling: Tailwind styles the leading paragraph and button more heavily, while Devup UI exercises typed component/style props. Treat these as build-pipeline results rather than a per-rule microbenchmark. The static `.css.ts` row was measured with the former `lite` fast path for static modules; every `.css.ts` module now runs on the full Boa evaluator, which adds about 20 ms for the first module and under 1 ms for each further one. +The Turbopack ranges overlap, so the direct-API median is 0.05s (0.7%) ahead of Tailwind on this fixture and the two are effectively at parity. The six cold samples were Tailwind `7.10s, 6.99s, 6.98s, 6.90s, 6.91s, 6.97s`, direct Devup UI `6.98s, 6.83s, 6.95s, 6.91s, 6.90s, 6.94s`, and static `.css.ts` `6.93s, 6.95s, 6.85s, 6.97s, 6.87s, 6.91s`. The fixtures have comparable app shapes, not pixel-identical styling: Tailwind styles the leading paragraph and button more heavily, while Devup UI exercises typed component/style props. Treat these as build-pipeline results rather than a per-rule microbenchmark. Every `.css.ts` module runs on the full Boa evaluator, which adds about 20 ms for the first module and under 1 ms for each further one. + ## How it works diff --git a/README_ko.md b/README_ko.md index 9dadc6606..ff9ca962a 100644 --- a/README_ko.md +++ b/README_ko.md @@ -79,31 +79,35 @@ npm install @devup-ui/webpack-plugin ## 비교 벤치마크 -[최신 CI 벤치마크](https://github.com/dev-five-git/devup-ui/actions/runs/33239265133)는 `ubuntu-latest`에서 실행했습니다. 모든 Next.js 빌드는 네이티브 TypeScript 7 CLI로 타입 검사를 수행합니다. + +[최신 CI 벤치마크](https://github.com/dev-five-git/devup-ui/actions/runs/36702867475) (커밋 `8d6c2f62`, Devup UI 1.0.44)는 `ubuntu-24.04`에서 Next.js 16.3.3로 실행했습니다. 모든 Next.js 빌드는 네이티브 TypeScript 7 CLI로 타입 검사를 수행합니다. 영어와 한국어 표는 같은 결과 파일 `benchmark-results.json`에서 생성합니다. + +빌드 사이즈는 빌드 출력 디렉터리(`.next`, vinext는 `dist`)의 모든 바이트이고, CSS 사이즈는 출력된 `.css` 파일만 따로 잰 값입니다. Webpack 값은 cold build 1회 결과입니다. -| 라이브러리 | 버전 | 빌드 시간 | 빌드 사이즈 | -| --------------------------- | ------ | ---------- | -------------------- | -| tailwindcss | 4.3.3 | 15.55s | 66,479,450 bytes | -| styleX | 0.19.0 | 34.27s | 95,417,163 bytes | -| vanilla-extract | 1.21.2 | 14.91s | 67,711,539 bytes | -| kuma-ui | 1.6.4 | 16.43s | 74,774,187 bytes | -| panda-css | 1.12.0 | 16.82s | 70,983,831 bytes | -| chakra-ui | 3.37.0 | 24.79s | 206,598,161 bytes | -| mui | 9.4.0 | 17.12s | 100,621,370 bytes | -| **devup-ui (per-file CSS)** | 1.0.40 | **13.43s** | 66,577,087 bytes | -| **devup-ui (single CSS)** | 1.0.40 | **13.37s** | **66,564,796 bytes** | +| 라이브러리 | 버전 | 빌드 시간 | 빌드 사이즈 | CSS 사이즈 | +| --------------------------- | ------ | ---------- | ----------------- | ------------ | +| tailwindcss | 4.3.3 | 16.67s | 67,332,642 bytes | 5,852 bytes | +| styleX | 0.19.0 | 38.08s | 96,291,287 bytes | 425 bytes | +| vanilla-extract | 1.21.2 | 16.09s | 68,565,036 bytes | 294 bytes | +| kuma-ui | 1.6.4 | 17.91s | 75,627,783 bytes | 340 bytes | +| panda-css | 1.12.0 | 18.16s | 71,849,418 bytes | 15,615 bytes | +| chakra-ui | 3.37.0 | 27.22s | 207,426,274 bytes | 0 bytes | +| mui | 9.4.0 | 18.78s | 101,473,260 bytes | 0 bytes | +| **devup-ui (per-file CSS)** | 1.0.44 | **14.27s** | 67,424,897 bytes | 562 bytes | +| **devup-ui (single CSS)** | 1.0.44 | **14.17s** | 67,426,959 bytes | 790 bytes | Turbopack 값은 실행 순서를 번갈아 측정한 cold build 6회의 중앙값입니다. -| 라이브러리 | 버전 | 빌드 시간 중앙값 | 빌드 사이즈 | -| -------------------------------------- | ------ | ---------------- | -------------------- | -| tailwindcss | 4.3.3 | 6.55s | 38,386,955 bytes | -| **devup-ui (direct APIs, single CSS)** | 1.0.40 | **6.54s** | **36,519,234 bytes** | -| **devup-ui (static `.css.ts`)** | 1.0.40 | **6.47s** | 36,550,197 bytes | +| 라이브러리 | 버전 | 빌드 시간 중앙값 | 빌드 사이즈 | CSS 사이즈 | +| -------------------------------------- | ------ | --------- | ---------------- | ----------- | +| tailwindcss | 4.3.3 | 6.98s | 38,422,141 bytes | 6,197 bytes | +| **devup-ui (direct APIs, single CSS)** | 1.0.44 | **6.93s** | 36,518,952 bytes | 327 bytes | +| **devup-ui (static `.css.ts`)** | 1.0.44 | **6.92s** | 36,578,506 bytes | 268 bytes | -Turbopack 측정 범위는 서로 겹치므로 direct API 결과는 이 fixture에서 Tailwind와 사실상 동률입니다. static `.css.ts` 행은 정적 모듈용 `lite` 빠른 경로가 있던 때에 측정한 값입니다. 지금은 모든 `.css.ts`를 full Boa evaluator로 처리하며, 첫 모듈에 약 20ms, 이후 모듈마다 1ms 미만이 더 듭니다. +Turbopack 측정 범위는 서로 겹치므로 direct API 중앙값은 이 fixture에서 Tailwind보다 0.05s(0.7%) 빠르며 사실상 동률입니다. cold 샘플 6개는 Tailwind `7.10s, 6.99s, 6.98s, 6.90s, 6.91s, 6.97s`, direct Devup UI `6.98s, 6.83s, 6.95s, 6.91s, 6.90s, 6.94s`, static `.css.ts` `6.93s, 6.95s, 6.85s, 6.97s, 6.87s, 6.91s`입니다. fixture는 앱 구조만 비슷하고 스타일이 픽셀 단위로 같지는 않습니다. Tailwind는 첫 문단과 버튼에 더 많은 스타일을 주고, Devup UI는 타입이 있는 컴포넌트/스타일 props를 사용합니다. 규칙 단위 마이크로벤치마크가 아니라 빌드 파이프라인 결과로 보세요. 모든 `.css.ts`는 full Boa evaluator로 처리하며, 첫 모듈에 약 20ms, 이후 모듈마다 1ms 미만이 더 듭니다. + ## 작동 원리 diff --git a/apps/react-18/e2e/react-18.spec.ts b/apps/react-18/e2e/react-18.spec.ts new file mode 100644 index 000000000..3ff8e655d --- /dev/null +++ b/apps/react-18/e2e/react-18.spec.ts @@ -0,0 +1,24 @@ +import { expect, test } from '@playwright/test' + +test('Devup UI runs on React 18', async ({ page }) => { + const errors: string[] = [] + page.on('pageerror', (error) => errors.push(error.message)) + page.on('console', (message) => { + if (message.type() === 'error') errors.push(message.text()) + }) + await page.goto('/') + + await expect(page.getByTestId('version')).toHaveText(/^18\./) + await expect(page.getByTestId('refs')).toHaveText( + 'button,input,input,input,textarea,input', + ) + + await page.getByText('select', { exact: true }).click() + await page.getByText('second', { exact: true }).click() + await expect(page.getByTestId('selected')).toHaveText('second') + + await page.getByTestId('increase').click() + await expect(page.getByTestId('stepper')).toHaveValue('1') + + expect(errors).toEqual([]) +}) diff --git a/apps/react-18/index.html b/apps/react-18/index.html new file mode 100644 index 000000000..9dfc14d76 --- /dev/null +++ b/apps/react-18/index.html @@ -0,0 +1,12 @@ + + + + + + Devup UI on React 18 + + +
+ + + diff --git a/apps/react-18/package.json b/apps/react-18/package.json new file mode 100644 index 000000000..6044cca14 --- /dev/null +++ b/apps/react-18/package.json @@ -0,0 +1,28 @@ +{ + "name": "react-18-example", + "version": "0.1.0", + "type": "module", + "private": true, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview --port 3098 --strictPort", + "test:e2e": "bunx playwright test -c playwright.config.ts", + "lint": "bun ../../node_modules/@typescript/native/bin/tsc && eslint" + }, + "dependencies": { + "@devup-ui/components": "workspace:^", + "@devup-ui/react": "workspace:^", + "react": "18.3.1", + "react-dom": "18.3.1" + }, + "devDependencies": { + "@devup-ui/vite-plugin": "workspace:^", + "@playwright/test": "^1.63.0", + "@types/react": "^18.3", + "@types/react-dom": "^18.3", + "@vitejs/plugin-react": "^6.1", + "typescript": "^7.0.2", + "vite": "^8.3" + } +} diff --git a/apps/react-18/playwright.config.ts b/apps/react-18/playwright.config.ts new file mode 100644 index 000000000..36905cf4c --- /dev/null +++ b/apps/react-18/playwright.config.ts @@ -0,0 +1,18 @@ +import { defineConfig, devices } from '@playwright/test' + +export default defineConfig({ + testDir: './e2e', + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + reporter: 'list', + use: { + baseURL: 'http://localhost:3098', + }, + projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }], + webServer: { + command: 'bun run preview', + url: 'http://localhost:3098', + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, +}) diff --git a/apps/react-18/src/App.tsx b/apps/react-18/src/App.tsx new file mode 100644 index 000000000..49ccf0502 --- /dev/null +++ b/apps/react-18/src/App.tsx @@ -0,0 +1,64 @@ +import { + Button, + Checkbox, + Input, + Radio, + Select, + SelectContainer, + SelectOption, + SelectTrigger, + Stepper, + StepperDecreaseButton, + StepperIncreaseButton, + StepperInput, + Textarea, +} from '@devup-ui/components' +import { Box, styled } from '@devup-ui/react' +import { useEffect, useRef, useState, version } from 'react' + +const Field = styled('input', { color: 'red', p: 1 }) + +export function App() { + const buttonRef = useRef(null) + const checkboxRef = useRef(null) + const inputRef = useRef(null) + const radioRef = useRef(null) + const textareaRef = useRef(null) + const fieldRef = useRef(null) + const [refs, setRefs] = useState('') + const [selected, setSelected] = useState('') + + useEffect(() => { + setRefs( + [buttonRef, checkboxRef, inputRef, radioRef, textareaRef, fieldRef] + .map((ref) => ref.current?.tagName.toLowerCase() ?? 'null') + .join(','), + ) + }, []) + + return ( + + {version} + + checkbox + + radio +