From e9d12a431755f5a17e40f37c8171610c4cc9c2ff Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 24 Aug 2026 05:44:44 +0900 Subject: [PATCH] test(landing): gate both bundler plugins with one e2e suite The landing app is the repository's only real-scale acceptance target, and migrating it to vinext left `@devup-ui/next-plugin`'s Turbopack production CSS path with no e2e coverage at all. That path's last regression (#633) only reproduced across a 64-route build's extraction waves, so a toy app cannot replace it. Build the same sources with both bundlers and run the same 128 assertions and the same screenshot baselines against each artifact. vinext still produces the deployed output; the Next build is CI-only. - replace `import.meta.glob` demo discovery with a barrel generated by `script.js`, so the demo graph is statically visible to Turbopack and Vite alike instead of to only one of them - resolve the artifact root and the not-found manifest from LANDING_BUILD_MODE, and teach the static server Next's nested segment-cache payload layout - restore the Next toolchain behind `build:next`, which drops `deploymentId` (Next reads it as skew protection and hard-navigates when a static host cannot echo it) and skips the vinext-only RSC transport - give the local-asset sweep a stated budget and a bounded request pool; it takes ~4s alone but starved past 60s against the route-render tests - run the Next pair in a parallel `landing-next-e2e` job Verified locally, 128/128 in all four combinations: vinext and Next, each with default and DEVUP_SINGLE_CSS=1. --- .github/workflows/publish.yml | 71 +++++++++++++++++++++++ apps/landing/.gitignore | 3 +- apps/landing/next.config.ts | 25 +++++++- apps/landing/package.json | 5 ++ apps/landing/script.js | 46 +++++++++++++++ apps/landing/src/app/layout.tsx | 10 +++- apps/landing/src/utils/demos.generated.ts | 71 +++++++++++++++++++++++ apps/landing/src/utils/get-demos.ts | 23 +++----- apps/landing/tsconfig.json | 14 ++++- bun.lock | 8 +++ e2e/exported-routes.ts | 32 ++++++++-- e2e/landing-routes.spec.ts | 31 ++++++++-- e2e/serve-static.mjs | 51 +++++++++++++++- package.json | 1 + playwright.config.ts | 5 +- 15 files changed, 359 insertions(+), 37 deletions(-) create mode 100644 apps/landing/src/utils/demos.generated.ts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0ad9756f9..39e33597a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -54,6 +54,77 @@ jobs: - name: Benchmark run: bun run benchmark + # The landing app is the only real-scale acceptance target in this repo, and + # `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 + # 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 + steps: + - name: Checkout + uses: actions/checkout@v7 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + + - name: Cache cargo registry + target + uses: actions/cache@v6 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: cargo-landing-next-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} + - uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.4.0" + name: Install bun + + - name: Cache bun dependencies + uses: actions/cache@v6 + with: + path: ~/.bun/install/cache + key: bun-landing-next-${{ 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: 24 + - run: bun install + - run: bun run build + - name: Build Landing (Next) + run: | + bun run --filter @devup-ui/components build-storybook + mv ./packages/components/storybook-static ./apps/landing/public/storybook + bun run --filter landing build:next + - name: Cache Playwright Browsers + uses: actions/cache@v6 + with: + path: ~/.cache/ms-playwright + key: playwright-landing-next-${{ runner.os }}-${{ hashFiles('**/bun.lock') }} + - name: Install Playwright Browsers + run: bunx playwright install chromium --with-deps + - name: Run E2E Tests (Next) + run: bun run test:e2e:next + - name: Build Landing (Next, singleCss) + run: | + rm -rf apps/landing/out apps/landing/.next apps/landing/df + DEVUP_SINGLE_CSS=1 bun run --filter landing build:next + - name: Run E2E Tests (Next, singleCss) + run: bun run test:e2e:next + - name: Upload Next Playwright Report + uses: actions/upload-artifact@v7 + if: ${{ !cancelled() }} + with: + name: playwright-report-next + path: playwright-report/ + retention-days: 1 + publish: runs-on: ubuntu-latest steps: diff --git a/apps/landing/.gitignore b/apps/landing/.gitignore index 7ab304aa5..eeb6ca471 100644 --- a/apps/landing/.gitignore +++ b/apps/landing/.gitignore @@ -16,10 +16,11 @@ public/storybook # testing /coverage -# vinext +# vinext (deployed) and the CI-only Next build /.next/ /.vinext/ /dist/ +/out/ # production /build diff --git a/apps/landing/next.config.ts b/apps/landing/next.config.ts index 637ef2546..84115a0d7 100644 --- a/apps/landing/next.config.ts +++ b/apps/landing/next.config.ts @@ -1,8 +1,29 @@ +import { DevupUI } from '@devup-ui/next-plugin' +import createMDX from '@next/mdx' +import type { NextConfig } from 'next' + import { STATIC_EXPORT_DEPLOYMENT_ID } from './src/utils/static-export-rsc-transport' -export default { - deploymentId: STATIC_EXPORT_DEPLOYMENT_ID, +const baseConfig: NextConfig = { output: 'export', pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'], reactCompiler: true, } + +// vinext produces the deployed artifact and gets the plugin-free config: its +// MDX and Devup wiring live in `vite.config.ts`. `deploymentId` is what the +// static RSC transport stamps onto the payloads it rewrites. +// +// LANDING_BUILD_MODE=next selects the CI-only Next build, which adds the MDX +// loader and `@devup-ui/next-plugin` so this app also gates the Turbopack +// production CSS path. It deliberately drops `deploymentId`: Next reads it as +// skew protection and falls back to a full page load whenever the host cannot +// echo the id, which a plain static host never can. `DevupUI` starts a +// coordinator when called, so it is only invoked on this branch. +export default process.env.LANDING_BUILD_MODE === 'next' + ? createMDX({ extension: /\.mdx?$/ })( + DevupUI(baseConfig, { + singleCss: process.env.DEVUP_SINGLE_CSS === '1', + }), + ) + : { ...baseConfig, deploymentId: STATIC_EXPORT_DEPLOYMENT_ID } diff --git a/apps/landing/package.json b/apps/landing/package.json index a28577e40..d7ae275bd 100644 --- a/apps/landing/package.json +++ b/apps/landing/package.json @@ -7,6 +7,7 @@ "dev": "node ./script.js && vinext dev", "search": "node ./script.js", "build": "node ./script.js && vinext build", + "build:next": "node ./script.js && LANDING_BUILD_MODE=next next build", "start": "npx serve ./dist/client", "lint": "eslint" }, @@ -14,10 +15,13 @@ "@devup-ui/components": "workspace:^", "@devup-ui/react": "workspace:^", "@devup-ui/reset-css": "workspace:^", + "@mdx-js/loader": "^3.1", "@mdx-js/react": "^3.1", + "@next/mdx": "^16.3", "body-scroll-lock": "3.1", "clsx": "^2.1", "lenis": "1.3", + "next": "^16.3", "react": "^19.2", "react-dom": "^19.2", "react-markdown": "^10.1", @@ -26,6 +30,7 @@ "vinext": "^1.0.0-beta.8" }, "devDependencies": { + "@devup-ui/next-plugin": "workspace:^", "@devup-ui/vite-plugin": "workspace:^", "@mdx-js/rollup": "^3.1", "@types/body-scroll-lock": "^3.1", diff --git a/apps/landing/script.js b/apps/landing/script.js index bfb9f1692..5c7665014 100644 --- a/apps/landing/script.js +++ b/apps/landing/script.js @@ -28,3 +28,49 @@ for await (const file of files) { const res = await Promise.all(q) await writeFile('public/search.json', JSON.stringify(res)) + +// Demo barrel. +// +// The component docs render every file under `/demo/`. Discovering +// them at runtime (`readdir` + computed `import()`) or through a bundler +// specific helper (`import.meta.glob`) hides those edges from static import +// analysis, and only one of the two bundlers understands each form. Emitting +// explicit imports keeps the demo graph visible to BOTH Turbopack and Vite, so +// the Next and vinext builds extract styles from exactly the same file set. +const demoFiles = [] +for await (const file of glob('src/app/**/demo/*.tsx')) { + demoFiles.push(file.split(/[/\\]/).join('/')) +} + +const demos = demoFiles + .map((file) => { + const segments = file.split('/') + const filename = segments.at(-1) + + return { + // `src/utils/demos.generated.ts` -> demo module, extension dropped so + // both bundlers resolve it through their standard extension list. + specifier: `../${segments.slice(1, -1).join('/')}/${filename.replace(/\.tsx$/, '')}`, + // Label consumed by `getDemos`: `/demo/`. + label: `${segments.at(-3)}/demo/${filename}`, + } + }) + .sort((left, right) => left.specifier.localeCompare(right.specifier)) + +await writeFile( + 'src/utils/demos.generated.ts', + [ + '// AUTO-GENERATED by apps/landing/script.js - do not edit by hand.', + '// Run `node ./script.js` (part of `dev`, `build` and `build:next`) to refresh.', + "import type { ComponentType } from 'react'", + '', + ...demos.map( + ({ specifier }, index) => `import demo${index} from '${specifier}'`, + ), + '', + 'export const DEMOS: readonly (readonly [string, ComponentType])[] = [', + ...demos.map(({ label }, index) => ` ['${label}', demo${index}],`), + ']', + '', + ].join('\n'), +) diff --git a/apps/landing/src/app/layout.tsx b/apps/landing/src/app/layout.tsx index 23e46b6f9..8b75520aa 100644 --- a/apps/landing/src/app/layout.tsx +++ b/apps/landing/src/app/layout.tsx @@ -14,6 +14,14 @@ import { const staticExportRscTransport = `(${installStaticExportRscTransport.toString()})(${JSON.stringify(STATIC_EXPORT_DEPLOYMENT_ID)})` +// The transport only exists to feed vinext's navigation runtime from `.rsc` +// artifacts on a plain static host. The CI-only Next build emits no such +// artifacts, so installing it there would turn every RSC prefetch into a 404 +// that the acceptance suite (rightly) fails on. +const needsStaticExportRscTransport = + process.env.NODE_ENV === 'production' && + process.env.LANDING_BUILD_MODE !== 'next' + export const metadata: Metadata = { title: 'Devup UI', description: 'Zero Config, Zero FOUC, Zero Runtime, CSS in JS Preprocessor', @@ -105,7 +113,7 @@ export default function RootLayout({ return ( - {process.env.NODE_ENV === 'production' && ( + {needsStaticExportRscTransport && (