Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion apps/landing/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ public/storybook
# testing
/coverage

# vinext
# vinext (deployed) and the CI-only Next build
/.next/
/.vinext/
/dist/
/out/

# production
/build
Expand Down
25 changes: 23 additions & 2 deletions apps/landing/next.config.ts
Original file line number Diff line number Diff line change
@@ -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 }
5 changes: 5 additions & 0 deletions apps/landing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,21 @@
"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"
},
"dependencies": {
"@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",
Expand All @@ -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",
Expand Down
46 changes: 46 additions & 0 deletions apps/landing/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<component>/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`: `<component>/demo/<filename>`.
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'),
)
10 changes: 9 additions & 1 deletion apps/landing/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@

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',
Expand Down Expand Up @@ -105,14 +113,14 @@
return (
<html lang="en" suppressHydrationWarning>
<head>
{process.env.NODE_ENV === 'production' && (
{needsStaticExportRscTransport && (
<script
dangerouslySetInnerHTML={{ __html: staticExportRscTransport }}

Check warning on line 118 in apps/landing/src/app/layout.tsx

View workflow job for this annotation

GitHub Actions / publish

Using 'dangerouslySetInnerHTML' may have security implications
data-vinext-static-rsc-transport=""
/>
)}
<script
dangerouslySetInnerHTML={{

Check warning on line 123 in apps/landing/src/app/layout.tsx

View workflow job for this annotation

GitHub Actions / publish

Using 'dangerouslySetInnerHTML' may have security implications
__html: `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
Expand Down
71 changes: 71 additions & 0 deletions apps/landing/src/utils/demos.generated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// 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'

import demo0 from '../app/(detail)/components/[component]/button/demo/1_Variants'
import demo1 from '../app/(detail)/components/[component]/button/demo/2_Icon'
import demo2 from '../app/(detail)/components/[component]/button/demo/3_Danger'
import demo3 from '../app/(detail)/components/[component]/button/demo/4_Disabled'
import demo4 from '../app/(detail)/components/[component]/button/demo/5_Colors'
import demo5 from '../app/(detail)/components/[component]/checkbox/demo/1_Default'
import demo6 from '../app/(detail)/components/[component]/checkbox/demo/2_Checked'
import demo7 from '../app/(detail)/components/[component]/checkbox/demo/3_Disabled'
import demo8 from '../app/(detail)/components/[component]/checkbox/demo/4_Colors'
import demo9 from '../app/(detail)/components/[component]/radio/demo/1_Default'
import demo10 from '../app/(detail)/components/[component]/radio/demo/2_Variant'
import demo11 from '../app/(detail)/components/[component]/radio/demo/3_Direction'
import demo12 from '../app/(detail)/components/[component]/radio/demo/4_Disabled'
import demo13 from '../app/(detail)/components/[component]/select/demo/1_Default'
import demo14 from '../app/(detail)/components/[component]/select/demo/2_Radio'
import demo15 from '../app/(detail)/components/[component]/select/demo/3_Checkbox'
import demo16 from '../app/(detail)/components/[component]/select/demo/4_Options'
import demo17 from '../app/(detail)/components/[component]/stepper/demo/1_Default'
import demo18 from '../app/(detail)/components/[component]/stepper/demo/2_Type'
import demo19 from '../app/(detail)/components/[component]/stepper/demo/3_MinMax'
import demo20 from '../app/(detail)/components/[component]/textarea/demo/1_Default'
import demo21 from '../app/(detail)/components/[component]/textarea/demo/2_Error'
import demo22 from '../app/(detail)/components/[component]/textarea/demo/3_Disabled'
import demo23 from '../app/(detail)/components/[component]/textarea/demo/4_Colors'
import demo24 from '../app/(detail)/components/[component]/textbox/demo/1_Default'
import demo25 from '../app/(detail)/components/[component]/textbox/demo/2_Error'
import demo26 from '../app/(detail)/components/[component]/textbox/demo/3_Disabled'
import demo27 from '../app/(detail)/components/[component]/textbox/demo/4_Icon'
import demo28 from '../app/(detail)/components/[component]/toggle/demo/1_Default'
import demo29 from '../app/(detail)/components/[component]/toggle/demo/2_Variant'
import demo30 from '../app/(detail)/components/[component]/toggle/demo/3_Disabled'
import demo31 from '../app/(detail)/components/[component]/toggle/demo/4_Colors'

export const DEMOS: readonly (readonly [string, ComponentType])[] = [
['button/demo/1_Variants.tsx', demo0],
['button/demo/2_Icon.tsx', demo1],
['button/demo/3_Danger.tsx', demo2],
['button/demo/4_Disabled.tsx', demo3],
['button/demo/5_Colors.tsx', demo4],
['checkbox/demo/1_Default.tsx', demo5],
['checkbox/demo/2_Checked.tsx', demo6],
['checkbox/demo/3_Disabled.tsx', demo7],
['checkbox/demo/4_Colors.tsx', demo8],
['radio/demo/1_Default.tsx', demo9],
['radio/demo/2_Variant.tsx', demo10],
['radio/demo/3_Direction.tsx', demo11],
['radio/demo/4_Disabled.tsx', demo12],
['select/demo/1_Default.tsx', demo13],
['select/demo/2_Radio.tsx', demo14],
['select/demo/3_Checkbox.tsx', demo15],
['select/demo/4_Options.tsx', demo16],
['stepper/demo/1_Default.tsx', demo17],
['stepper/demo/2_Type.tsx', demo18],
['stepper/demo/3_MinMax.tsx', demo19],
['textarea/demo/1_Default.tsx', demo20],
['textarea/demo/2_Error.tsx', demo21],
['textarea/demo/3_Disabled.tsx', demo22],
['textarea/demo/4_Colors.tsx', demo23],
['textbox/demo/1_Default.tsx', demo24],
['textbox/demo/2_Error.tsx', demo25],
['textbox/demo/3_Disabled.tsx', demo26],
['textbox/demo/4_Icon.tsx', demo27],
['toggle/demo/1_Default.tsx', demo28],
['toggle/demo/2_Variant.tsx', demo29],
['toggle/demo/3_Disabled.tsx', demo30],
['toggle/demo/4_Colors.tsx', demo31],
]
23 changes: 7 additions & 16 deletions apps/landing/src/utils/get-demos.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,13 @@
type DemoModule = {
default: React.ComponentType
}
import type { ComponentType } from 'react'

const demoModules = import.meta.glob<DemoModule>('../app/**/demo/*.tsx', {
eager: true,
})
import { DEMOS } from './demos.generated'

export async function getDemos(
dir: string,
): Promise<[React.ComponentType, string][]> {
const directoryMarker = `/${dir}/demo/`

return Object.entries(demoModules)
.filter(([path]) => path.includes(directoryMarker))
.sort(([left], [right]) => left.localeCompare(right))
.map(([path, module]) => {
const filename = path.slice(path.lastIndexOf('/') + 1)
): Promise<[ComponentType, string][]> {
const prefix = `${dir}/demo/`

return [module.default, `${dir}/demo/${filename}`]
})
return DEMOS.filter(([label]) => label.startsWith(prefix)).map<
[ComponentType, string]
>(([label, component]) => [component, label])
}
14 changes: 12 additions & 2 deletions apps/landing/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,18 @@
"incremental": true,
"paths": {
"@/*": ["./src/*"]
}
},
"plugins": [
{
"name": "next"
}
]
},
"include": ["**/*.ts", "**/*.tsx"],
"include": [
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules"]
}
8 changes: 8 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading