diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0ad9756f..39e33597 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 7ab304aa..eeb6ca47 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 637ef254..84115a0d 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 a28577e4..d7ae275b 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 bfb9f169..5c766501 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 23e46b6f..8b75520a 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 && (