From 429f786adea4bdae5a24a6fe758255671c7668b0 Mon Sep 17 00:00:00 2001 From: devfive Date: Fri, 28 Aug 2026 22:01:31 +0900 Subject: [PATCH 01/43] feat: profile Turbopack extraction --- packages/next-plugin/README.md | 20 +++++++ .../next-plugin/src/__tests__/profile.test.ts | 59 +++++++++++++++++++ packages/next-plugin/src/coordinator.ts | 28 +++++++++ packages/next-plugin/src/plugin.ts | 31 +++++++++- packages/next-plugin/src/profile.ts | 26 ++++++++ 5 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 packages/next-plugin/src/__tests__/profile.test.ts create mode 100644 packages/next-plugin/src/profile.ts diff --git a/packages/next-plugin/README.md b/packages/next-plugin/README.md index 95e9221f..8eb44d97 100644 --- a/packages/next-plugin/README.md +++ b/packages/next-plugin/README.md @@ -192,3 +192,23 @@ custom `distDir`) to `include`. ```tsx ``` + +## Turbopack build profiling + +Set `DEVUP_UI_PROFILE=1` for an opt-in, structured timing log during a +Turbopack build. + +```bash +DEVUP_UI_PROFILE=1 bun run build +``` + +```powershell +$env:DEVUP_UI_PROFILE = '1'; bun run build +``` + +Each `[devup-ui:profile]` JSON entry reports one phase. `next.graph` measures +the static import-graph pre-pass, `next.prewarm` measures production extraction +before Turbopack starts loaders, and `coordinator.extract` separates request, +WASM extraction, CSS/state serialization, and write time for each loaded +module. The setting is disabled by default and does not collect timings or +write logs when it is absent. diff --git a/packages/next-plugin/src/__tests__/profile.test.ts b/packages/next-plugin/src/__tests__/profile.test.ts new file mode 100644 index 00000000..b2e902e5 --- /dev/null +++ b/packages/next-plugin/src/__tests__/profile.test.ts @@ -0,0 +1,59 @@ +import { afterEach, beforeEach, describe, expect, it, spyOn } from 'bun:test' + +import { + elapsedMs, + isProfileEnabled, + profileStart, + reportProfile, +} from '../profile' + +let originalEnv: NodeJS.ProcessEnv + +beforeEach(() => { + originalEnv = { ...process.env } +}) + +afterEach(() => { + process.env = originalEnv +}) + +describe('profile', () => { + it('is disabled unless explicitly enabled', () => { + delete process.env.DEVUP_UI_PROFILE + const consoleSpy = spyOn(console, 'info').mockImplementation(() => {}) + + expect(isProfileEnabled()).toBe(false) + reportProfile('next.prewarm') + expect(consoleSpy).not.toHaveBeenCalled() + consoleSpy.mockRestore() + }) + + it('reports structured measurements when enabled', () => { + process.env.DEVUP_UI_PROFILE = '1' + const consoleSpy = spyOn(console, 'info').mockImplementation(() => {}) + + reportProfile('next.prewarm', { durationMs: 12.34, files: 2 }) + + expect(consoleSpy).toHaveBeenCalledWith( + '[devup-ui:profile] {"phase":"next.prewarm","durationMs":12.34,"files":2}', + ) + consoleSpy.mockRestore() + }) + + it('returns a non-negative elapsed duration', () => { + expect(elapsedMs(performance.now())).toBeGreaterThanOrEqual(0) + }) + + it('does not start a timer while disabled', () => { + delete process.env.DEVUP_UI_PROFILE + + expect(profileStart()).toBeUndefined() + expect(elapsedMs(undefined)).toBeUndefined() + }) + + it('starts a timer when enabled', () => { + process.env.DEVUP_UI_PROFILE = '1' + + expect(profileStart()).toBeTypeOf('number') + }) +}) diff --git a/packages/next-plugin/src/coordinator.ts b/packages/next-plugin/src/coordinator.ts index 31866fdd..0990a32a 100644 --- a/packages/next-plugin/src/coordinator.ts +++ b/packages/next-plugin/src/coordinator.ts @@ -11,6 +11,8 @@ import { getCss, } from '@devup-ui/wasm' +import { elapsedMs, profileStart, reportProfile } from './profile' + export interface CoordinatorOptions { package: string cssDir: string @@ -344,6 +346,7 @@ export function startCoordinator(options: CoordinatorOptions): { } if (req.method === 'GET' && url.pathname === '/css') { + const cssStartedAt = profileStart() const fileNumParam = url.searchParams.get('fileNum') const importMainCss = url.searchParams.get('importMainCss') === 'true' const shouldWait = url.searchParams.get('waitForIdle') === 'true' @@ -364,10 +367,16 @@ export function startCoordinator(options: CoordinatorOptions): { res.writeHead(200, { 'Content-Type': 'text/css' }) res.end(getCss(fileNum ?? null, importMainCss)) + reportProfile('coordinator.css', { + durationMs: elapsedMs(cssStartedAt), + fileNum, + waitForIdle: shouldWait, + }) return } if (req.method === 'POST' && url.pathname === '/extract') { + const requestStartedAt = profileStart() // Reserve a "start slot" before yielding on `await readBody`. Without // this counter, `waitForIdle` could observe activeExtractions=0 in the // window between the request hitting this handler and `activeExtractions++` @@ -377,7 +386,9 @@ export function startCoordinator(options: CoordinatorOptions): { let promotedToActive = false let extractedFilename: string | undefined try { + const bodyStartedAt = profileStart() const body = JSON.parse(await readBody(req)) + const bodyDurationMs = elapsedMs(bodyStartedAt) activeExtractions++ pendingExtractStarts-- promotedToActive = true @@ -394,6 +405,7 @@ export function startCoordinator(options: CoordinatorOptions): { ) if (!relCssDir.startsWith('./')) relCssDir = `./${relCssDir}` + const extractStartedAt = profileStart() const result = codeExtract( filename, code, @@ -404,6 +416,7 @@ export function startCoordinator(options: CoordinatorOptions): { true, importAliases, ) + const extractDurationMs = elapsedMs(extractStartedAt) // When singleCss=false, rewrite per-file CSS imports so Turbopack can resolve them. // Instead of importing "devup-ui-79.css" (which doesn't exist as a resolvable module), @@ -417,6 +430,7 @@ export function startCoordinator(options: CoordinatorOptions): { ) } + const snapshotStartedAt = profileStart() const promises: Promise[] = [] if (result.updatedBaseStyle) { @@ -461,6 +475,8 @@ export function startCoordinator(options: CoordinatorOptions): { } } + const snapshotDurationMs = elapsedMs(snapshotStartedAt) + const writeStartedAt = profileStart() await Promise.all(promises) res.writeHead(200, { 'Content-Type': 'application/json' }) @@ -472,6 +488,18 @@ export function startCoordinator(options: CoordinatorOptions): { updatedBaseStyle: result.updatedBaseStyle, }), ) + reportProfile('coordinator.extract', { + bodyMs: bodyDurationMs, + durationMs: elapsedMs(requestStartedAt), + extractMs: extractDurationMs, + filename, + sourceBytes: + requestStartedAt === undefined + ? undefined + : Buffer.byteLength(code), + snapshotMs: snapshotDurationMs, + writeMs: elapsedMs(writeStartedAt), + }) } catch (error) { res.writeHead(500, { 'Content-Type': 'application/json' }) res.end( diff --git a/packages/next-plugin/src/plugin.ts b/packages/next-plugin/src/plugin.ts index 4dca4ff7..6833b58b 100644 --- a/packages/next-plugin/src/plugin.ts +++ b/packages/next-plugin/src/plugin.ts @@ -45,6 +45,7 @@ import { type NextConfig } from 'next' import { startCoordinator } from './coordinator' import { collectProductionPrewarmFiles } from './prewarm' +import { elapsedMs, profileStart, reportProfile } from './profile' type DevupUiNextPluginOptions = Omit< Partial, @@ -61,6 +62,7 @@ export function DevupUI( config: NextConfig, options: DevupUiNextPluginOptions = {}, ): NextConfig { + const pluginStartedAt = profileStart() const isTurbo = process.env.TURBOPACK === '1' || process.env.TURBOPACK === 'auto' // turbopack is now stable, TURBOPACK is set to auto without any flags @@ -154,6 +156,7 @@ export function DevupUI( // `[]` (idle fallback) when no routes are detected or the pre-pass fails. let expectedBaseFiles: string[] = [] let staticGraph: StaticImportGraph | undefined + const graphStartedAt = profileStart() try { const srcDir = resolve(process.cwd(), 'src') const tsconfigPath = resolve(process.cwd(), 'tsconfig.json') @@ -209,9 +212,18 @@ export function DevupUI( ) } } + reportProfile('next.graph', { + durationMs: elapsedMs(graphStartedAt), + files: staticGraph.files.length, + expectedBaseFiles: expectedBaseFiles.length, + }) } catch { // Pre-pass is best-effort; on failure canonical() is the identity (no // merge) and atom hoisting stays off. + reportProfile('next.graph', { + durationMs: elapsedMs(graphStartedAt), + failed: true, + }) } // Turbopack can request a CSS module before it has scheduled every source @@ -224,6 +236,8 @@ export function DevupUI( // keys/options and is idempotent. const prewarmedFiles: string[] = [] if (!watch && staticGraph) { + const prewarmStartedAt = profileStart() + let prewarmSourceBytes = 0 const cwd = process.cwd() const prewarmFiles = collectProductionPrewarmFiles({ cwd, @@ -238,9 +252,13 @@ export function DevupUI( dirname(resourcePath), cssDir, ).replaceAll('\\', '/')}` + const source = readFileSync(resourcePath, 'utf-8') + if (prewarmStartedAt !== undefined) { + prewarmSourceBytes += Buffer.byteLength(source) + } codeExtract( filename, - readFileSync(resourcePath, 'utf-8'), + source, libPackage, relCssDir, singleCss, @@ -250,6 +268,11 @@ export function DevupUI( ) prewarmedFiles.push(filename) } + reportProfile('next.prewarm', { + durationMs: elapsedMs(prewarmStartedAt), + files: prewarmedFiles.length, + sourceBytes: prewarmSourceBytes, + }) } // create devup-ui.css file @@ -277,6 +300,12 @@ export function DevupUI( expectedBaseFiles, prewarmedFiles, }) + reportProfile('next.setup', { + durationMs: elapsedMs(pluginStartedAt), + singleCss, + prewarmedFiles: prewarmedFiles.length, + watch, + }) // Cleanup on exit process.on('exit', () => { diff --git a/packages/next-plugin/src/profile.ts b/packages/next-plugin/src/profile.ts new file mode 100644 index 00000000..35f75aa4 --- /dev/null +++ b/packages/next-plugin/src/profile.ts @@ -0,0 +1,26 @@ +type ProfileFields = Record + +export function isProfileEnabled(): boolean { + return process.env.DEVUP_UI_PROFILE === '1' +} + +export function reportProfile(phase: string, fields: ProfileFields = {}): void { + if (!isProfileEnabled()) return + + console.info( + `[devup-ui:profile] ${JSON.stringify({ + phase, + ...fields, + })}`, + ) +} + +export function profileStart(): number | undefined { + return isProfileEnabled() ? performance.now() : undefined +} + +export function elapsedMs(start: number | undefined): number | undefined { + if (start === undefined) return undefined + + return Number((performance.now() - start).toFixed(2)) +} From 32acf461e5f868bc9c136205a562962634b4b73b Mon Sep 17 00:00:00 2001 From: devfive Date: Fri, 28 Aug 2026 22:05:53 +0900 Subject: [PATCH 02/43] perf: reuse single CSS prewarm output --- .../src/__tests__/coordinator.test.ts | 48 ++++++++++++++++++ packages/next-plugin/src/coordinator.ts | 50 ++++++++++++++----- packages/next-plugin/src/plugin.ts | 16 +++++- 3 files changed, 100 insertions(+), 14 deletions(-) diff --git a/packages/next-plugin/src/__tests__/coordinator.test.ts b/packages/next-plugin/src/__tests__/coordinator.test.ts index dbdf00d3..c111a838 100644 --- a/packages/next-plugin/src/__tests__/coordinator.test.ts +++ b/packages/next-plugin/src/__tests__/coordinator.test.ts @@ -177,6 +177,54 @@ describe('coordinator', () => { coordinator.close() }) + it('reuses byte-identical singleCss prewarm output', async () => { + const source = 'const x = ' + const options = makeOptions({ + singleCss: true, + prewarmedOutputs: new Map([ + [ + 'src/App.tsx', + { + code: 'transformed prewarm code', + css: 'prewarmed css', + cssFile: 'devup-ui.css', + map: '{"version":3}', + source, + updatedBaseStyle: true, + }, + ], + ]), + }) + const coordinator = startCoordinator(options) + + await new Promise((r) => setTimeout(r, 100)) + + const portStr = (writeFileSyncSpy.mock.calls[0] as [string, string])[1] + const port = parseInt(portStr) + const res = await httpRequest( + port, + 'POST', + '/extract', + JSON.stringify({ + filename: 'src/App.tsx', + code: source, + resourcePath: join(process.cwd(), 'src', 'App.tsx'), + }), + ) + + expect(res.status).toBe(200) + expect(JSON.parse(res.body)).toMatchObject({ + code: 'transformed prewarm code', + map: '{"version":3}', + cssFile: 'devup-ui.css', + updatedBaseStyle: true, + }) + expect(codeExtractSpy).not.toHaveBeenCalled() + expect(writeFileSpy).not.toHaveBeenCalled() + + coordinator.close() + }) + it('should rewrite per-file CSS imports when singleCss=false', async () => { codeExtractSpy.mockReturnValue({ code: 'import "./../../df/devup-ui/devup-ui-79.css";\nimport "./../../df/devup-ui/devup-ui-3.css";\nconst x = 1;', diff --git a/packages/next-plugin/src/coordinator.ts b/packages/next-plugin/src/coordinator.ts index 0990a32a..5e3257b2 100644 --- a/packages/next-plugin/src/coordinator.ts +++ b/packages/next-plugin/src/coordinator.ts @@ -44,6 +44,12 @@ export interface CoordinatorOptions { * shared WASM sheet even though their loaders have not POSTed `/extract` yet. */ prewarmedFiles?: string[] + /** + * Production `singleCss` outputs extracted before Turbopack starts. A loader + * that receives byte-identical source can return this result without a + * second WASM extraction; the shared sheet is already populated. + */ + prewarmedOutputs?: Map /** * Idle threshold (ms) for the base-css `/css` wait. Defaults to 2500. * FALLBACK ONLY — used when `expectedBaseFiles` is empty (no deterministic @@ -65,6 +71,15 @@ export interface CoordinatorOptions { maxWaitMs?: number } +export interface PrewarmedOutput { + code: string + css: string + cssFile: string + map?: string + source: string + updatedBaseStyle: boolean +} + // Latest-Wins Coalescing Serializer. // // Multiple Turbopack workers may call /extract concurrently, each producing @@ -325,6 +340,7 @@ export function startCoordinator(options: CoordinatorOptions): { importAliases, coordinatorPortFile, } = options + const prewarmedOutputs = options.prewarmedOutputs ?? new Map() idleThresholdMs = options.idleThresholdMs ?? 2500 quietMs = options.quietMs ?? 10_000 @@ -405,17 +421,26 @@ export function startCoordinator(options: CoordinatorOptions): { ) if (!relCssDir.startsWith('./')) relCssDir = `./${relCssDir}` + // The production prewarm exists to make the CSS snapshot complete + // before Turbopack requests it. In single-CSS mode the generated CSS + // is already in that snapshot, so re-running WASM here is pure work. + // Require exact source equality because Turbopack may hand a loader + // code modified by an earlier transform. + const prewarmed = singleCss ? prewarmedOutputs.get(filename) : undefined + const cacheHit = prewarmed?.source === code const extractStartedAt = profileStart() - const result = codeExtract( - filename, - code, - libPackage, - relCssDir, - singleCss, - false, - true, - importAliases, - ) + const result = cacheHit + ? prewarmed + : codeExtract( + filename, + code, + libPackage, + relCssDir, + singleCss, + false, + true, + importAliases, + ) const extractDurationMs = elapsedMs(extractStartedAt) // When singleCss=false, rewrite per-file CSS imports so Turbopack can resolve them. @@ -433,7 +458,7 @@ export function startCoordinator(options: CoordinatorOptions): { const snapshotStartedAt = profileStart() const promises: Promise[] = [] - if (result.updatedBaseStyle) { + if (!cacheHit && result.updatedBaseStyle) { promises.push( safeWrite( join(cssDir, 'devup-ui.css'), @@ -442,7 +467,7 @@ export function startCoordinator(options: CoordinatorOptions): { ) } - if (result.cssFile) { + if (!cacheHit && result.cssFile) { const fileNum = getFileNumByFilename(result.cssFile) if (fileNum != null) { // Record this bucket's fileNum -> canonical bucket path so /css can @@ -490,6 +515,7 @@ export function startCoordinator(options: CoordinatorOptions): { ) reportProfile('coordinator.extract', { bodyMs: bodyDurationMs, + cacheHit, durationMs: elapsedMs(requestStartedAt), extractMs: extractDurationMs, filename, diff --git a/packages/next-plugin/src/plugin.ts b/packages/next-plugin/src/plugin.ts index 6833b58b..0ebaac2d 100644 --- a/packages/next-plugin/src/plugin.ts +++ b/packages/next-plugin/src/plugin.ts @@ -43,7 +43,7 @@ import { } from '@devup-ui/webpack-plugin' import { type NextConfig } from 'next' -import { startCoordinator } from './coordinator' +import { type PrewarmedOutput, startCoordinator } from './coordinator' import { collectProductionPrewarmFiles } from './prewarm' import { elapsedMs, profileStart, reportProfile } from './profile' @@ -235,6 +235,7 @@ export function DevupUI( // route graph cannot represent. Loader-time extraction uses the same // keys/options and is idempotent. const prewarmedFiles: string[] = [] + const prewarmedOutputs = new Map() if (!watch && staticGraph) { const prewarmStartedAt = profileStart() let prewarmSourceBytes = 0 @@ -256,7 +257,7 @@ export function DevupUI( if (prewarmStartedAt !== undefined) { prewarmSourceBytes += Buffer.byteLength(source) } - codeExtract( + const output = codeExtract( filename, source, libPackage, @@ -266,6 +267,16 @@ export function DevupUI( true, importAliases as unknown as Record, ) + if (singleCss) { + prewarmedOutputs.set(filename, { + code: output.code, + css: output.css, + cssFile: output.cssFile, + map: output.map, + source, + updatedBaseStyle: output.updatedBaseStyle, + }) + } prewarmedFiles.push(filename) } reportProfile('next.prewarm', { @@ -299,6 +310,7 @@ export function DevupUI( canonicalMap, expectedBaseFiles, prewarmedFiles, + prewarmedOutputs, }) reportProfile('next.setup', { durationMs: elapsedMs(pluginStartedAt), From ce246bafa52cf81bbf7936cb3451efa500976fbf Mon Sep 17 00:00:00 2001 From: devfive Date: Fri, 28 Aug 2026 22:37:07 +0900 Subject: [PATCH 03/43] feat: profile Turbopack serialization --- .../src/__tests__/coordinator.test.ts | 78 +++++++++++++ .../next-plugin/src/__tests__/plugin.test.ts | 31 ++++++ packages/next-plugin/src/coordinator.ts | 101 +++++++++++++---- packages/next-plugin/src/plugin.ts | 105 ++++++++++++++++-- 4 files changed, 285 insertions(+), 30 deletions(-) diff --git a/packages/next-plugin/src/__tests__/coordinator.test.ts b/packages/next-plugin/src/__tests__/coordinator.test.ts index c111a838..f1b6325a 100644 --- a/packages/next-plugin/src/__tests__/coordinator.test.ts +++ b/packages/next-plugin/src/__tests__/coordinator.test.ts @@ -225,6 +225,84 @@ describe('coordinator', () => { coordinator.close() }) + it('profiles serialization work separately from writes', async () => { + const originalProfile = process.env.DEVUP_UI_PROFILE + process.env.DEVUP_UI_PROFILE = '1' + const infoSpy = spyOn(console, 'info').mockImplementation(() => {}) + codeExtractSpy.mockReturnValue({ + code: 'transformed code', + map: undefined, + css: 'collected css', + cssFile: 'devup-ui-1.css', + updatedBaseStyle: true, + free: mock(), + [Symbol.dispose]: mock(), + }) + getCssSpy.mockImplementation((fileNum: number | null) => + fileNum === null ? 'base-css' : `file-css-${fileNum}`, + ) + exportSheetSpy.mockReturnValue('sheet-json') + exportClassMapSpy.mockReturnValue('classmap-json') + exportFileMapSpy.mockReturnValue('filemap-json') + + const coordinator = startCoordinator(makeOptions()) + try { + await new Promise((resolve) => setTimeout(resolve, 100)) + const port = parseInt( + (writeFileSyncSpy.mock.calls[0] as [string, string])[1], + ) + + const res = await httpRequest( + port, + 'POST', + '/extract', + JSON.stringify({ + filename: 'src/profile.tsx', + code: 'const profile = true', + resourcePath: join(process.cwd(), 'src', 'profile.tsx'), + }), + ) + + expect(res.status).toBe(200) + const message = infoSpy.mock.calls + .map(([value]) => value) + .find( + (value): value is string => + typeof value === 'string' && + value.startsWith( + '[devup-ui:profile] {"phase":"coordinator.extract"', + ), + ) + if (message === undefined) throw new Error('missing extract profile') + const profile = JSON.parse( + message.slice('[devup-ui:profile] '.length), + ) as Record + + expect(profile).toMatchObject({ + cacheHit: false, + classMapSnapshotBytes: Buffer.byteLength('classmap-json'), + cssSnapshotBytes: expect.any(Number), + fileMapSnapshotBytes: Buffer.byteLength('filemap-json'), + phase: 'coordinator.extract', + scheduledWrites: 5, + sheetSnapshotBytes: Buffer.byteLength('sheet-json'), + sourceBytes: Buffer.byteLength('const profile = true'), + }) + expect(profile.classMapSnapshotMs).toBeTypeOf('number') + expect(profile.cssSnapshotMs).toBeTypeOf('number') + expect(profile.fileMapSnapshotMs).toBeTypeOf('number') + expect(profile.sheetSnapshotMs).toBeTypeOf('number') + } finally { + coordinator.close() + infoSpy.mockRestore() + if (originalProfile === undefined) { + delete process.env.DEVUP_UI_PROFILE + } else { + process.env.DEVUP_UI_PROFILE = originalProfile + } + } + }) + it('should rewrite per-file CSS imports when singleCss=false', async () => { codeExtractSpy.mockReturnValue({ code: 'import "./../../df/devup-ui/devup-ui-79.css";\nimport "./../../df/devup-ui/devup-ui-3.css";\nconst x = 1;', diff --git a/packages/next-plugin/src/__tests__/plugin.test.ts b/packages/next-plugin/src/__tests__/plugin.test.ts index 19d60c89..c88b1779 100644 --- a/packages/next-plugin/src/__tests__/plugin.test.ts +++ b/packages/next-plugin/src/__tests__/plugin.test.ts @@ -507,6 +507,7 @@ describe('DevupUINextPlugin', () => { canonicalMap: expect.any(Object), expectedBaseFiles: expect.any(Array), prewarmedFiles: expect.any(Array), + prewarmedOutputs: expect.any(Map), }) }) it('should create theme.d.ts file', async () => { @@ -696,6 +697,7 @@ describe('DevupUINextPlugin', () => { canonicalMap: expect.any(Object), expectedBaseFiles: expect.any(Array), prewarmedFiles: [], + prewarmedOutputs: expect.any(Map), }) expect(codeExtractSpy).not.toHaveBeenCalled() @@ -719,6 +721,8 @@ describe('DevupUINextPlugin', () => { it('hands the coordinator the full compiled-file set, not the static-only route map', () => { process.env.TURBOPACK = '1' + process.env.DEVUP_UI_PROFILE = '1' + const profileSpy = spyOn(console, 'info').mockImplementation(() => {}) // The base sheet must wait for lazily-loaded modules too, so // expectedBaseFiles comes from computeCompiledFiles (static + dynamic // edges) rather than computeFileRoutes (static edges only). Using the @@ -777,9 +781,36 @@ describe('DevupUINextPlugin', () => { 'extract:src/lazy/panel.tsx', 'startCoordinator', ]) + const profiles: Record[] = profileSpy.mock.calls + .map(([value]) => value) + .filter( + (value): value is string => + typeof value === 'string' && + value.startsWith('[devup-ui:profile] '), + ) + .map((value) => JSON.parse(value.slice('[devup-ui:profile] '.length))) + const prewarmProfile = profiles.find( + ({ phase }) => phase === 'next.prewarm', + ) + expect(prewarmProfile).toMatchObject({ + collectMs: expect.any(Number), + extractMs: expect.any(Number), + files: 2, + phase: 'next.prewarm', + readMs: expect.any(Number), + sourceBytes: Buffer.byteLength('{}') * 2, + }) + expect(profiles).toEqual( + expect.arrayContaining([ + expect.objectContaining({ phase: 'next.initialCss' }), + expect.objectContaining({ phase: 'next.stateSnapshot' }), + expect.objectContaining({ phase: 'next.setup' }), + ]), + ) // the static-only route map is not consulted outside atom-hoist mode expect(routesSpy).not.toHaveBeenCalled() } finally { + profileSpy.mockRestore() compiledSpy.mockRestore() routesSpy.mockRestore() } diff --git a/packages/next-plugin/src/coordinator.ts b/packages/next-plugin/src/coordinator.ts index 5e3257b2..ea964d2c 100644 --- a/packages/next-plugin/src/coordinator.ts +++ b/packages/next-plugin/src/coordinator.ts @@ -456,15 +456,27 @@ export function startCoordinator(options: CoordinatorOptions): { } const snapshotStartedAt = profileStart() + let classMapSnapshotBytes: number | undefined + let classMapSnapshotMs: number | undefined + let cssSnapshotBytes: number | undefined + let cssSnapshotMs: number | undefined + let fileMapSnapshotBytes: number | undefined + let fileMapSnapshotMs: number | undefined + let sheetSnapshotBytes: number | undefined + let sheetSnapshotMs: number | undefined const promises: Promise[] = [] if (!cacheHit && result.updatedBaseStyle) { - promises.push( - safeWrite( - join(cssDir, 'devup-ui.css'), - `${getCss(null, false)}\n/* ${Date.now()} */`, - ), - ) + const cssStartedAt = + snapshotStartedAt === undefined ? undefined : performance.now() + const css = `${getCss(null, false)}\n/* ${Date.now()} */` + const cssDurationMs = + cssStartedAt === undefined ? undefined : elapsedMs(cssStartedAt) + if (cssDurationMs !== undefined) { + cssSnapshotMs = (cssSnapshotMs ?? 0) + cssDurationMs + cssSnapshotBytes = (cssSnapshotBytes ?? 0) + Buffer.byteLength(css) + } + promises.push(safeWrite(join(cssDir, 'devup-ui.css'), css)) } if (!cacheHit && result.cssFile) { @@ -474,14 +486,51 @@ export function startCoordinator(options: CoordinatorOptions): { // wait for the bucket's members before serving it. fileNumToBucket.set(fileNum, canonicalMapRef[filename] ?? filename) } + const cssStartedAt = + snapshotStartedAt === undefined ? undefined : performance.now() + const css = getCss(fileNum, true) + const cssDurationMs = + cssStartedAt === undefined ? undefined : elapsedMs(cssStartedAt) + if (cssDurationMs !== undefined) { + cssSnapshotMs = (cssSnapshotMs ?? 0) + cssDurationMs + cssSnapshotBytes = (cssSnapshotBytes ?? 0) + Buffer.byteLength(css) + } + + const sheetStartedAt = + snapshotStartedAt === undefined ? undefined : performance.now() + const sheet = exportSheet() + sheetSnapshotMs = + sheetStartedAt === undefined ? undefined : elapsedMs(sheetStartedAt) + if (snapshotStartedAt !== undefined) { + sheetSnapshotBytes = Buffer.byteLength(sheet) + } + + const classMapStartedAt = + snapshotStartedAt === undefined ? undefined : performance.now() + const classMap = exportClassMap() + classMapSnapshotMs = + classMapStartedAt === undefined + ? undefined + : elapsedMs(classMapStartedAt) + if (snapshotStartedAt !== undefined) { + classMapSnapshotBytes = Buffer.byteLength(classMap) + } + + const fileMapStartedAt = + snapshotStartedAt === undefined ? undefined : performance.now() + const fileMap = exportFileMap() + fileMapSnapshotMs = + fileMapStartedAt === undefined + ? undefined + : elapsedMs(fileMapStartedAt) + if (snapshotStartedAt !== undefined) { + fileMapSnapshotBytes = Buffer.byteLength(fileMap) + } promises.push( - safeWrite( - join(cssDir, basename(result.cssFile)), - getCss(fileNum, true), - ), - safeWrite(sheetFile, exportSheet()), - safeWrite(classMapFile, exportClassMap()), - safeWrite(fileMapFile, exportFileMap()), + safeWrite(join(cssDir, basename(result.cssFile)), css), + safeWrite(sheetFile, sheet), + safeWrite(classMapFile, classMap), + safeWrite(fileMapFile, fileMap), ) // In non-singleCss mode, imports are rewritten from devup-ui-N.css to @@ -491,12 +540,17 @@ export function startCoordinator(options: CoordinatorOptions): { // new CSS rules are invisible to the browser. // When updatedBaseStyle is true, devup-ui.css is already written above. if (!singleCss && !result.updatedBaseStyle && result.css != null) { - promises.push( - safeWrite( - join(cssDir, 'devup-ui.css'), - `${getCss(null, false)}\n/* ${Date.now()} */`, - ), - ) + const cssStartedAt = + snapshotStartedAt === undefined ? undefined : performance.now() + const baseCss = `${getCss(null, false)}\n/* ${Date.now()} */` + const cssDurationMs = + cssStartedAt === undefined ? undefined : elapsedMs(cssStartedAt) + if (cssDurationMs !== undefined) { + cssSnapshotMs = (cssSnapshotMs ?? 0) + cssDurationMs + cssSnapshotBytes = + (cssSnapshotBytes ?? 0) + Buffer.byteLength(baseCss) + } + promises.push(safeWrite(join(cssDir, 'devup-ui.css'), baseCss)) } } @@ -516,13 +570,22 @@ export function startCoordinator(options: CoordinatorOptions): { reportProfile('coordinator.extract', { bodyMs: bodyDurationMs, cacheHit, + classMapSnapshotBytes, + classMapSnapshotMs, + cssSnapshotBytes, + cssSnapshotMs, durationMs: elapsedMs(requestStartedAt), extractMs: extractDurationMs, + fileMapSnapshotBytes, + fileMapSnapshotMs, filename, + sheetSnapshotBytes, + sheetSnapshotMs, sourceBytes: requestStartedAt === undefined ? undefined : Buffer.byteLength(code), + scheduledWrites: promises.length, snapshotMs: snapshotDurationMs, writeMs: elapsedMs(writeStartedAt), }) diff --git a/packages/next-plugin/src/plugin.ts b/packages/next-plugin/src/plugin.ts index 0ebaac2d..2f17a7cf 100644 --- a/packages/next-plugin/src/plugin.ts +++ b/packages/next-plugin/src/plugin.ts @@ -238,8 +238,12 @@ export function DevupUI( const prewarmedOutputs = new Map() if (!watch && staticGraph) { const prewarmStartedAt = profileStart() + let prewarmExtractMs = 0 + let prewarmReadMs = 0 let prewarmSourceBytes = 0 const cwd = process.cwd() + const collectStartedAt = + prewarmStartedAt === undefined ? undefined : performance.now() const prewarmFiles = collectProductionPrewarmFiles({ cwd, graph: staticGraph, @@ -247,16 +251,22 @@ export function DevupUI( libPackage, include, }) + const collectDurationMs = elapsedMs(collectStartedAt) for (const filename of prewarmFiles) { const resourcePath = resolve(cwd, filename) const relCssDir = `./${relative( dirname(resourcePath), cssDir, ).replaceAll('\\', '/')}` + const readStartedAt = + prewarmStartedAt === undefined ? undefined : performance.now() const source = readFileSync(resourcePath, 'utf-8') - if (prewarmStartedAt !== undefined) { + if (readStartedAt !== undefined) { + prewarmReadMs += performance.now() - readStartedAt prewarmSourceBytes += Buffer.byteLength(source) } + const extractStartedAt = + prewarmStartedAt === undefined ? undefined : performance.now() const output = codeExtract( filename, source, @@ -267,6 +277,9 @@ export function DevupUI( true, importAliases as unknown as Record, ) + if (extractStartedAt !== undefined) { + prewarmExtractMs += performance.now() - extractStartedAt + } if (singleCss) { prewarmedOutputs.set(filename, { code: output.code, @@ -280,14 +293,39 @@ export function DevupUI( prewarmedFiles.push(filename) } reportProfile('next.prewarm', { + collectMs: collectDurationMs, durationMs: elapsedMs(prewarmStartedAt), + extractMs: + prewarmStartedAt === undefined + ? undefined + : Number(prewarmExtractMs.toFixed(2)), files: prewarmedFiles.length, + readMs: + prewarmStartedAt === undefined + ? undefined + : Number(prewarmReadMs.toFixed(2)), sourceBytes: prewarmSourceBytes, }) } // create devup-ui.css file - writeFileSync(join(cssDir, 'devup-ui.css'), getCss(null, false)) + const initialCssStartedAt = profileStart() + const initialCssSerializeStartedAt = + initialCssStartedAt === undefined ? undefined : performance.now() + const initialCss = getCss(null, false) + const initialCssSerializeMs = elapsedMs(initialCssSerializeStartedAt) + const initialCssWriteStartedAt = + initialCssStartedAt === undefined ? undefined : performance.now() + writeFileSync(join(cssDir, 'devup-ui.css'), initialCss) + reportProfile('next.initialCss', { + bytes: + initialCssStartedAt === undefined + ? undefined + : Buffer.byteLength(initialCss), + durationMs: elapsedMs(initialCssStartedAt), + serializeMs: initialCssSerializeMs, + writeMs: elapsedMs(initialCssWriteStartedAt), + }) // Delete stale port file from previous session so loaders don't connect // to a dead coordinator port. The new coordinator writes a fresh port file @@ -312,20 +350,59 @@ export function DevupUI( prewarmedFiles, prewarmedOutputs, }) - reportProfile('next.setup', { - durationMs: elapsedMs(pluginStartedAt), - singleCss, - prewarmedFiles: prewarmedFiles.length, - watch, - }) // Cleanup on exit process.on('exit', () => { coordinator.close() }) - const defaultSheet = JSON.parse(exportSheet()) - const defaultClassMap = JSON.parse(exportClassMap()) - const defaultFileMap = JSON.parse(exportFileMap()) + const stateSnapshotStartedAt = profileStart() + const sheetSerializeStartedAt = + stateSnapshotStartedAt === undefined ? undefined : performance.now() + const defaultSheetJson = exportSheet() + const sheetSerializeMs = elapsedMs(sheetSerializeStartedAt) + const sheetParseStartedAt = + stateSnapshotStartedAt === undefined ? undefined : performance.now() + const defaultSheet = JSON.parse(defaultSheetJson) + const sheetParseMs = elapsedMs(sheetParseStartedAt) + + const classMapSerializeStartedAt = + stateSnapshotStartedAt === undefined ? undefined : performance.now() + const defaultClassMapJson = exportClassMap() + const classMapSerializeMs = elapsedMs(classMapSerializeStartedAt) + const classMapParseStartedAt = + stateSnapshotStartedAt === undefined ? undefined : performance.now() + const defaultClassMap = JSON.parse(defaultClassMapJson) + const classMapParseMs = elapsedMs(classMapParseStartedAt) + + const fileMapSerializeStartedAt = + stateSnapshotStartedAt === undefined ? undefined : performance.now() + const defaultFileMapJson = exportFileMap() + const fileMapSerializeMs = elapsedMs(fileMapSerializeStartedAt) + const fileMapParseStartedAt = + stateSnapshotStartedAt === undefined ? undefined : performance.now() + const defaultFileMap = JSON.parse(defaultFileMapJson) + const fileMapParseMs = elapsedMs(fileMapParseStartedAt) + reportProfile('next.stateSnapshot', { + classMapBytes: + stateSnapshotStartedAt === undefined + ? undefined + : Buffer.byteLength(defaultClassMapJson), + classMapParseMs, + classMapSerializeMs, + durationMs: elapsedMs(stateSnapshotStartedAt), + fileMapBytes: + stateSnapshotStartedAt === undefined + ? undefined + : Buffer.byteLength(defaultFileMapJson), + fileMapParseMs, + fileMapSerializeMs, + sheetBytes: + stateSnapshotStartedAt === undefined + ? undefined + : Buffer.byteLength(defaultSheetJson), + sheetParseMs, + sheetSerializeMs, + }) // for theme script const defaultTheme = getDefaultTheme() if (defaultTheme) { @@ -390,6 +467,12 @@ export function DevupUI( }, } Object.assign(config.turbopack.rules, rules) + reportProfile('next.setup', { + durationMs: elapsedMs(pluginStartedAt), + prewarmedFiles: prewarmedFiles.length, + singleCss, + watch, + }) return config } From dcca39ab2f15ec4456aaae6c52786543353d1678 Mon Sep 17 00:00:00 2001 From: devfive Date: Fri, 28 Aug 2026 23:32:45 +0900 Subject: [PATCH 04/43] fix: allow empty prewarmed CSS output --- packages/next-plugin/src/coordinator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/next-plugin/src/coordinator.ts b/packages/next-plugin/src/coordinator.ts index ea964d2c..52fce285 100644 --- a/packages/next-plugin/src/coordinator.ts +++ b/packages/next-plugin/src/coordinator.ts @@ -73,8 +73,8 @@ export interface CoordinatorOptions { export interface PrewarmedOutput { code: string - css: string - cssFile: string + css?: string + cssFile?: string map?: string source: string updatedBaseStyle: boolean From ec738cea643810cc5831abbe776f9c3818999941 Mon Sep 17 00:00:00 2001 From: devfive Date: Fri, 28 Aug 2026 23:34:58 +0900 Subject: [PATCH 05/43] ci: compare Turbopack cold builds --- .github/workflows/turbopack-profile.yml | 144 ++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 .github/workflows/turbopack-profile.yml diff --git a/.github/workflows/turbopack-profile.yml b/.github/workflows/turbopack-profile.yml new file mode 100644 index 00000000..2e81fda4 --- /dev/null +++ b/.github/workflows/turbopack-profile.yml @@ -0,0 +1,144 @@ +name: Turbopack profile + +on: + pull_request: + branches: + - main + paths: + - ".github/workflows/turbopack-profile.yml" + - "bindings/devup-ui-wasm/**" + - "libs/**" + - "packages/next-plugin/**" + - "packages/plugin-utils/**" + - "packages/react/**" + - "benchmark/next-devup-ui-single-turbo/**" + - "benchmark/next-tailwind-turbo/**" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + compare: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout + uses: actions/checkout@v7 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.4.0" + name: Install Bun + + - uses: jetli/wasm-pack-action@v0.4.0 + with: + version: "latest" + + - name: Install Node.js + uses: actions/setup-node@v7 + with: + node-version: 24 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build benchmark dependencies + run: | + bun run --filter @devup-ui/wasm --filter @devup-ui/plugin-utils build + bun run --filter @devup-ui/react --filter @devup-ui/webpack-plugin build + bun run --filter @devup-ui/next-plugin build + + - name: Compare cold Turbopack builds + env: + NEXT_TELEMETRY_DISABLED: "1" + shell: bash + run: | + set -euo pipefail + + devup_dir="$GITHUB_WORKSPACE/benchmark/next-devup-ui-single-turbo" + tailwind_dir="$GITHUB_WORKSPACE/benchmark/next-tailwind-turbo" + result_dir="$RUNNER_TEMP/turbopack-profile" + mkdir -p "$result_dir/logs" + + run_build() { + local name="$1" + local directory="$2" + local sample="$3" + local record="$4" + local started_at + local finished_at + local elapsed_ms + local status + + rm -rf "$directory/.next" "$directory/df" + started_at="$(date +%s%N)" + set +e + (cd "$directory" && bunx next build) 2>&1 \ + | tee "$result_dir/logs/${name}-${sample}.log" + status="${PIPESTATUS[0]}" + set -e + finished_at="$(date +%s%N)" + elapsed_ms="$(((finished_at - started_at) / 1000000))" + + echo "$name $sample: ${elapsed_ms}ms" + if [[ "$status" -ne 0 ]]; then + return "$status" + fi + if [[ "$record" == "true" ]]; then + echo "$elapsed_ms" >> "$result_dir/${name}.txt" + fi + } + + # Warm both dependency graphs before recording. Each measured sample + # still removes all application build outputs, so it is a cold Next + # build with a balanced OS/module cache. + run_build tailwind "$tailwind_dir" warmup false + run_build devup "$devup_dir" warmup false + + # Alternate order to balance runner drift and first/second-run bias. + for round in $(seq 1 7); do + if ((round % 2 == 1)); then + run_build devup "$devup_dir" "$round" true + run_build tailwind "$tailwind_dir" "$round" true + else + run_build tailwind "$tailwind_dir" "$round" true + run_build devup "$devup_dir" "$round" true + fi + done + + devup_median="$(sort -n "$result_dir/devup.txt" | sed -n '4p')" + tailwind_median="$(sort -n "$result_dir/tailwind.txt" | sed -n '4p')" + delta_ms="$((tailwind_median - devup_median))" + delta_percent="$(awk -v d="$devup_median" -v t="$tailwind_median" \ + 'BEGIN { printf "%.1f", ((t - d) / t) * 100 }')" + devup_samples="$(awk 'BEGIN { ORS="" } { if (NR > 1) printf ", "; printf "%s", $0 }' "$result_dir/devup.txt")" + tailwind_samples="$(awk 'BEGIN { ORS="" } { if (NR > 1) printf ", "; printf "%s", $0 }' "$result_dir/tailwind.txt")" + + { + echo "### Turbopack cold-build comparison" + echo + echo "The comparison excludes Next's experimental memory-debug mode." + echo "Both apps receive one unrecorded warmup, then seven output-cleaned builds in alternating order." + echo + echo "| App | Median | Samples (ms) |" + echo "| --- | ---: | --- |" + echo "| Devup UI singleCss | ${devup_median}ms | ${devup_samples} |" + echo "| Tailwind Turbo | ${tailwind_median}ms | ${tailwind_samples} |" + echo + echo "Tailwind minus Devup UI: ${delta_ms}ms (${delta_percent}%)." + } | tee "$result_dir/summary.md" >> "$GITHUB_STEP_SUMMARY" + + - name: Upload raw benchmark logs + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v7 + with: + name: turbopack-profile + path: ${{ runner.temp }}/turbopack-profile/ + retention-days: 7 From b1cef9a60a64c5e8f86cd33a614f81292294b9f3 Mon Sep 17 00:00:00 2001 From: devfive Date: Fri, 28 Aug 2026 23:51:36 +0900 Subject: [PATCH 06/43] ci: report Turbopack benchmark phases --- .github/workflows/turbopack-profile.yml | 54 ++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/.github/workflows/turbopack-profile.yml b/.github/workflows/turbopack-profile.yml index 2e81fda4..a2fb8471 100644 --- a/.github/workflows/turbopack-profile.yml +++ b/.github/workflows/turbopack-profile.yml @@ -67,6 +67,32 @@ jobs: result_dir="$RUNNER_TEMP/turbopack-profile" mkdir -p "$result_dir/logs" + extract_duration_ms() { + local pattern="$1" + local log_file="$2" + local duration + + duration="$(grep -E "$pattern" "$log_file" \ + | tail -1 \ + | grep -oE '[0-9]+([.][0-9]+)?(ms|s)' \ + | tail -1)" + if [[ "$duration" == *ms ]]; then + echo "${duration%ms}" + else + awk -v seconds="${duration%s}" 'BEGIN { printf "%.0f", seconds * 1000 }' + fi + } + + record_phase() { + local name="$1" + local phase="$2" + local pattern="$3" + local log_file="$4" + + extract_duration_ms "$pattern" "$log_file" \ + >> "$result_dir/${name}-${phase}.txt" + } + run_build() { local name="$1" local directory="$2" @@ -75,13 +101,15 @@ jobs: local started_at local finished_at local elapsed_ms + local log_file local status rm -rf "$directory/.next" "$directory/df" + log_file="$result_dir/logs/${name}-${sample}.log" started_at="$(date +%s%N)" set +e (cd "$directory" && bunx next build) 2>&1 \ - | tee "$result_dir/logs/${name}-${sample}.log" + | tee "$log_file" status="${PIPESTATUS[0]}" set -e finished_at="$(date +%s%N)" @@ -93,6 +121,10 @@ jobs: fi if [[ "$record" == "true" ]]; then echo "$elapsed_ms" >> "$result_dir/${name}.txt" + record_phase "$name" config 'Running next[.]config' "$log_file" + record_phase "$name" compile 'Compiled successfully' "$log_file" + record_phase "$name" typescript 'Finished TypeScript' "$log_file" + record_phase "$name" static 'Generating static pages.* in ' "$log_file" fi } @@ -121,6 +153,17 @@ jobs: devup_samples="$(awk 'BEGIN { ORS="" } { if (NR > 1) printf ", "; printf "%s", $0 }' "$result_dir/devup.txt")" tailwind_samples="$(awk 'BEGIN { ORS="" } { if (NR > 1) printf ", "; printf "%s", $0 }' "$result_dir/tailwind.txt")" + phase_row() { + local phase="$1" + local label="$2" + local devup_phase_median + local tailwind_phase_median + + devup_phase_median="$(sort -n "$result_dir/devup-${phase}.txt" | sed -n '4p')" + tailwind_phase_median="$(sort -n "$result_dir/tailwind-${phase}.txt" | sed -n '4p')" + echo "| $label | ${devup_phase_median}ms | ${tailwind_phase_median}ms | $((tailwind_phase_median - devup_phase_median))ms |" + } + { echo "### Turbopack cold-build comparison" echo @@ -133,6 +176,15 @@ jobs: echo "| Tailwind Turbo | ${tailwind_median}ms | ${tailwind_samples} |" echo echo "Tailwind minus Devup UI: ${delta_ms}ms (${delta_percent}%)." + echo + echo "| Phase | Devup UI median | Tailwind median | Tailwind - Devup UI |" + echo "| --- | ---: | ---: | ---: |" + phase_row config "Next config" + phase_row compile "Turbopack compile" + phase_row typescript "TypeScript" + phase_row static "Static generation" + echo + echo "Positive phase deltas mean Devup UI is faster." } | tee "$result_dir/summary.md" >> "$GITHUB_STEP_SUMMARY" - name: Upload raw benchmark logs From 29084d84de6a82578f36a0bee0b760c4bf40b2b5 Mon Sep 17 00:00:00 2001 From: devfive Date: Fri, 28 Aug 2026 23:56:32 +0900 Subject: [PATCH 07/43] ci: separate compile phase samples --- .github/workflows/turbopack-profile.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/turbopack-profile.yml b/.github/workflows/turbopack-profile.yml index a2fb8471..b15abea5 100644 --- a/.github/workflows/turbopack-profile.yml +++ b/.github/workflows/turbopack-profile.yml @@ -79,7 +79,7 @@ jobs: if [[ "$duration" == *ms ]]; then echo "${duration%ms}" else - awk -v seconds="${duration%s}" 'BEGIN { printf "%.0f", seconds * 1000 }' + awk -v seconds="${duration%s}" 'BEGIN { printf "%.0f\n", seconds * 1000 }' fi } From e0b9bd121dce69965d21517c509575bea15c3701 Mon Sep 17 00:00:00 2001 From: devfive Date: Sat, 29 Aug 2026 00:22:56 +0900 Subject: [PATCH 08/43] perf: prioritize default component props --- .github/workflows/turbopack-profile.yml | 196 ------------------ packages/react/src/components/Box.tsx | 6 + packages/react/src/components/Text.tsx | 6 + packages/react/type-tests/custom-shorthand.ts | 11 +- 4 files changed, 22 insertions(+), 197 deletions(-) delete mode 100644 .github/workflows/turbopack-profile.yml diff --git a/.github/workflows/turbopack-profile.yml b/.github/workflows/turbopack-profile.yml deleted file mode 100644 index b15abea5..00000000 --- a/.github/workflows/turbopack-profile.yml +++ /dev/null @@ -1,196 +0,0 @@ -name: Turbopack profile - -on: - pull_request: - branches: - - main - paths: - - ".github/workflows/turbopack-profile.yml" - - "bindings/devup-ui-wasm/**" - - "libs/**" - - "packages/next-plugin/**" - - "packages/plugin-utils/**" - - "packages/react/**" - - "benchmark/next-devup-ui-single-turbo/**" - - "benchmark/next-tailwind-turbo/**" - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - compare: - runs-on: ubuntu-latest - timeout-minutes: 20 - steps: - - name: Checkout - uses: actions/checkout@v7 - - - uses: actions-rust-lang/setup-rust-toolchain@v1 - - - uses: oven-sh/setup-bun@v2 - with: - bun-version: "1.4.0" - name: Install Bun - - - uses: jetli/wasm-pack-action@v0.4.0 - with: - version: "latest" - - - name: Install Node.js - uses: actions/setup-node@v7 - with: - node-version: 24 - - - name: Install dependencies - run: bun install --frozen-lockfile - - - name: Build benchmark dependencies - run: | - bun run --filter @devup-ui/wasm --filter @devup-ui/plugin-utils build - bun run --filter @devup-ui/react --filter @devup-ui/webpack-plugin build - bun run --filter @devup-ui/next-plugin build - - - name: Compare cold Turbopack builds - env: - NEXT_TELEMETRY_DISABLED: "1" - shell: bash - run: | - set -euo pipefail - - devup_dir="$GITHUB_WORKSPACE/benchmark/next-devup-ui-single-turbo" - tailwind_dir="$GITHUB_WORKSPACE/benchmark/next-tailwind-turbo" - result_dir="$RUNNER_TEMP/turbopack-profile" - mkdir -p "$result_dir/logs" - - extract_duration_ms() { - local pattern="$1" - local log_file="$2" - local duration - - duration="$(grep -E "$pattern" "$log_file" \ - | tail -1 \ - | grep -oE '[0-9]+([.][0-9]+)?(ms|s)' \ - | tail -1)" - if [[ "$duration" == *ms ]]; then - echo "${duration%ms}" - else - awk -v seconds="${duration%s}" 'BEGIN { printf "%.0f\n", seconds * 1000 }' - fi - } - - record_phase() { - local name="$1" - local phase="$2" - local pattern="$3" - local log_file="$4" - - extract_duration_ms "$pattern" "$log_file" \ - >> "$result_dir/${name}-${phase}.txt" - } - - run_build() { - local name="$1" - local directory="$2" - local sample="$3" - local record="$4" - local started_at - local finished_at - local elapsed_ms - local log_file - local status - - rm -rf "$directory/.next" "$directory/df" - log_file="$result_dir/logs/${name}-${sample}.log" - started_at="$(date +%s%N)" - set +e - (cd "$directory" && bunx next build) 2>&1 \ - | tee "$log_file" - status="${PIPESTATUS[0]}" - set -e - finished_at="$(date +%s%N)" - elapsed_ms="$(((finished_at - started_at) / 1000000))" - - echo "$name $sample: ${elapsed_ms}ms" - if [[ "$status" -ne 0 ]]; then - return "$status" - fi - if [[ "$record" == "true" ]]; then - echo "$elapsed_ms" >> "$result_dir/${name}.txt" - record_phase "$name" config 'Running next[.]config' "$log_file" - record_phase "$name" compile 'Compiled successfully' "$log_file" - record_phase "$name" typescript 'Finished TypeScript' "$log_file" - record_phase "$name" static 'Generating static pages.* in ' "$log_file" - fi - } - - # Warm both dependency graphs before recording. Each measured sample - # still removes all application build outputs, so it is a cold Next - # build with a balanced OS/module cache. - run_build tailwind "$tailwind_dir" warmup false - run_build devup "$devup_dir" warmup false - - # Alternate order to balance runner drift and first/second-run bias. - for round in $(seq 1 7); do - if ((round % 2 == 1)); then - run_build devup "$devup_dir" "$round" true - run_build tailwind "$tailwind_dir" "$round" true - else - run_build tailwind "$tailwind_dir" "$round" true - run_build devup "$devup_dir" "$round" true - fi - done - - devup_median="$(sort -n "$result_dir/devup.txt" | sed -n '4p')" - tailwind_median="$(sort -n "$result_dir/tailwind.txt" | sed -n '4p')" - delta_ms="$((tailwind_median - devup_median))" - delta_percent="$(awk -v d="$devup_median" -v t="$tailwind_median" \ - 'BEGIN { printf "%.1f", ((t - d) / t) * 100 }')" - devup_samples="$(awk 'BEGIN { ORS="" } { if (NR > 1) printf ", "; printf "%s", $0 }' "$result_dir/devup.txt")" - tailwind_samples="$(awk 'BEGIN { ORS="" } { if (NR > 1) printf ", "; printf "%s", $0 }' "$result_dir/tailwind.txt")" - - phase_row() { - local phase="$1" - local label="$2" - local devup_phase_median - local tailwind_phase_median - - devup_phase_median="$(sort -n "$result_dir/devup-${phase}.txt" | sed -n '4p')" - tailwind_phase_median="$(sort -n "$result_dir/tailwind-${phase}.txt" | sed -n '4p')" - echo "| $label | ${devup_phase_median}ms | ${tailwind_phase_median}ms | $((tailwind_phase_median - devup_phase_median))ms |" - } - - { - echo "### Turbopack cold-build comparison" - echo - echo "The comparison excludes Next's experimental memory-debug mode." - echo "Both apps receive one unrecorded warmup, then seven output-cleaned builds in alternating order." - echo - echo "| App | Median | Samples (ms) |" - echo "| --- | ---: | --- |" - echo "| Devup UI singleCss | ${devup_median}ms | ${devup_samples} |" - echo "| Tailwind Turbo | ${tailwind_median}ms | ${tailwind_samples} |" - echo - echo "Tailwind minus Devup UI: ${delta_ms}ms (${delta_percent}%)." - echo - echo "| Phase | Devup UI median | Tailwind median | Tailwind - Devup UI |" - echo "| --- | ---: | ---: | ---: |" - phase_row config "Next config" - phase_row compile "Turbopack compile" - phase_row typescript "TypeScript" - phase_row static "Static generation" - echo - echo "Positive phase deltas mean Devup UI is faster." - } | tee "$result_dir/summary.md" >> "$GITHUB_STEP_SUMMARY" - - - name: Upload raw benchmark logs - if: ${{ !cancelled() }} - uses: actions/upload-artifact@v7 - with: - name: turbopack-profile - path: ${{ runner.temp }}/turbopack-profile/ - retention-days: 7 diff --git a/packages/react/src/components/Box.tsx b/packages/react/src/components/Box.tsx index acbee4f4..d596606c 100644 --- a/packages/react/src/components/Box.tsx +++ b/packages/react/src/components/Box.tsx @@ -4,6 +4,12 @@ import type { } from '../types/props' import type { Merge } from '../types/utils' +export function Box( + props: Merge, DevupComponentProps<'div'>>, +): React.ReactElement +export function Box( + props: Merge, DevupComponentProps>, +): React.ReactElement export function Box( // eslint-disable-next-line @typescript-eslint/no-unused-vars props: Merge, DevupComponentProps>, diff --git a/packages/react/src/components/Text.tsx b/packages/react/src/components/Text.tsx index 467afb4c..788d5e6e 100644 --- a/packages/react/src/components/Text.tsx +++ b/packages/react/src/components/Text.tsx @@ -4,6 +4,12 @@ import type { } from '../types/props' import type { Merge } from '../types/utils' +export function Text( + props: Merge, DevupComponentProps<'span'>>, +): React.ReactElement +export function Text( + props: Merge, DevupComponentProps>, +): React.ReactElement export function Text( // eslint-disable-next-line @typescript-eslint/no-unused-vars props: Merge, DevupComponentProps>, diff --git a/packages/react/type-tests/custom-shorthand.ts b/packages/react/type-tests/custom-shorthand.ts index e49b7a6a..8b4654ba 100644 --- a/packages/react/type-tests/custom-shorthand.ts +++ b/packages/react/type-tests/custom-shorthand.ts @@ -26,4 +26,13 @@ const boxProps: Parameters[0] = { }, } -export { boxProps, customShorthandProps } +// The concrete default overload is a fast path only. The generic fallback +// must continue to infer intrinsic-element props from `as`. +const polymorphicBox = Box({ + as: 'a', + bg: 'red', + href: '/docs', + target: '_blank', +}) + +export { boxProps, customShorthandProps, polymorphicBox } From ae02974107e51c0420612347ea537f5ef47c9727 Mon Sep 17 00:00:00 2001 From: devfive Date: Sat, 29 Aug 2026 00:31:38 +0900 Subject: [PATCH 09/43] perf: specialize intrinsic component props --- packages/react/src/components/Box.tsx | 5 +++++ packages/react/src/components/Text.tsx | 5 +++++ packages/react/src/types/props/index.ts | 4 ++++ packages/react/type-tests/custom-shorthand.ts | 12 +++++++++++- 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/react/src/components/Box.tsx b/packages/react/src/components/Box.tsx index d596606c..edf27ba7 100644 --- a/packages/react/src/components/Box.tsx +++ b/packages/react/src/components/Box.tsx @@ -1,12 +1,17 @@ import type { DevupComponentBaseProps, DevupComponentProps, + DevupIntrinsicComponentBaseProps, + DevupIntrinsicElement, } from '../types/props' import type { Merge } from '../types/utils' export function Box( props: Merge, DevupComponentProps<'div'>>, ): React.ReactElement +export function Box( + props: Merge, DevupComponentProps>, +): React.ReactElement export function Box( props: Merge, DevupComponentProps>, ): React.ReactElement diff --git a/packages/react/src/components/Text.tsx b/packages/react/src/components/Text.tsx index 788d5e6e..a3e084c8 100644 --- a/packages/react/src/components/Text.tsx +++ b/packages/react/src/components/Text.tsx @@ -1,12 +1,17 @@ import type { DevupComponentBaseProps, DevupComponentProps, + DevupIntrinsicComponentBaseProps, + DevupIntrinsicElement, } from '../types/props' import type { Merge } from '../types/utils' export function Text( props: Merge, DevupComponentProps<'span'>>, ): React.ReactElement +export function Text( + props: Merge, DevupComponentProps>, +): React.ReactElement export function Text( props: Merge, DevupComponentProps>, ): React.ReactElement diff --git a/packages/react/src/types/props/index.ts b/packages/react/src/types/props/index.ts index 89761a6b..5fb6bd92 100644 --- a/packages/react/src/types/props/index.ts +++ b/packages/react/src/types/props/index.ts @@ -50,6 +50,10 @@ export interface DevupComponentProps< export type DevupComponentBaseProps = DevupElementTypeProps & DevupComponentAdditionalProps +export type DevupIntrinsicElement = keyof React.JSX.IntrinsicElements +export type DevupIntrinsicComponentBaseProps = + React.JSX.IntrinsicElements[T] & DevupComponentAdditionalProps + export type DevupElementTypeProps = T extends string ? React.ComponentProps : object diff --git a/packages/react/type-tests/custom-shorthand.ts b/packages/react/type-tests/custom-shorthand.ts index 8b4654ba..4a25c640 100644 --- a/packages/react/type-tests/custom-shorthand.ts +++ b/packages/react/type-tests/custom-shorthand.ts @@ -32,7 +32,17 @@ const polymorphicBox = Box({ as: 'a', bg: 'red', href: '/docs', + onClick: (event) => event.currentTarget.href, target: '_blank', }) -export { boxProps, customShorthandProps, polymorphicBox } +function RequiredLink({ to }: { to: string }) { + return to +} + +const customComponentBox = Box({ + as: RequiredLink, + props: { to: '/docs' }, +}) + +export { boxProps, customComponentBox, customShorthandProps, polymorphicBox } From a9d40084e81bddddccd16e40d347c169d7755c74 Mon Sep 17 00:00:00 2001 From: devfive Date: Sat, 29 Aug 2026 00:46:22 +0900 Subject: [PATCH 10/43] perf: optimize WASM for extraction throughput --- Cargo.toml | 6 ++++-- bindings/devup-ui-wasm/Cargo.toml | 2 +- packages/react/src/components/Box.tsx | 11 ---------- packages/react/src/components/Text.tsx | 11 ---------- packages/react/src/types/props/index.ts | 4 ---- packages/react/type-tests/custom-shorthand.ts | 21 +------------------ 6 files changed, 6 insertions(+), 49 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 26cba5dc..a07213ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,8 +50,10 @@ use_self = "allow" used_underscore_binding = "allow" [profile.release] -# Optimize for small code size (critical for WASM binary) -opt-level = "s" +# Optimize the build-time extractor for throughput. The WASM module runs in +# Node during every Devup UI build, so extraction speed is more important than +# download size. +opt-level = 3 # Link-time optimization: enables cross-crate inlining and dead code elimination lto = true # Single codegen unit: maximizes optimization at cost of compile time diff --git a/bindings/devup-ui-wasm/Cargo.toml b/bindings/devup-ui-wasm/Cargo.toml index cd8419bc..f7674e19 100644 --- a/bindings/devup-ui-wasm/Cargo.toml +++ b/bindings/devup-ui-wasm/Cargo.toml @@ -54,7 +54,7 @@ workspace = true # only the features listed here, so it has to be allowed explicitly. It is part # of the WebAssembly 2.0 baseline (Chrome 74, Firefox 62, Safari 14.1, Node 12). wasm-opt = [ - "-Oz", + "-O3", "--enable-bulk-memory", "--enable-nontrapping-float-to-int", "--enable-sign-ext", diff --git a/packages/react/src/components/Box.tsx b/packages/react/src/components/Box.tsx index edf27ba7..acbee4f4 100644 --- a/packages/react/src/components/Box.tsx +++ b/packages/react/src/components/Box.tsx @@ -1,20 +1,9 @@ import type { DevupComponentBaseProps, DevupComponentProps, - DevupIntrinsicComponentBaseProps, - DevupIntrinsicElement, } from '../types/props' import type { Merge } from '../types/utils' -export function Box( - props: Merge, DevupComponentProps<'div'>>, -): React.ReactElement -export function Box( - props: Merge, DevupComponentProps>, -): React.ReactElement -export function Box( - props: Merge, DevupComponentProps>, -): React.ReactElement export function Box( // eslint-disable-next-line @typescript-eslint/no-unused-vars props: Merge, DevupComponentProps>, diff --git a/packages/react/src/components/Text.tsx b/packages/react/src/components/Text.tsx index a3e084c8..467afb4c 100644 --- a/packages/react/src/components/Text.tsx +++ b/packages/react/src/components/Text.tsx @@ -1,20 +1,9 @@ import type { DevupComponentBaseProps, DevupComponentProps, - DevupIntrinsicComponentBaseProps, - DevupIntrinsicElement, } from '../types/props' import type { Merge } from '../types/utils' -export function Text( - props: Merge, DevupComponentProps<'span'>>, -): React.ReactElement -export function Text( - props: Merge, DevupComponentProps>, -): React.ReactElement -export function Text( - props: Merge, DevupComponentProps>, -): React.ReactElement export function Text( // eslint-disable-next-line @typescript-eslint/no-unused-vars props: Merge, DevupComponentProps>, diff --git a/packages/react/src/types/props/index.ts b/packages/react/src/types/props/index.ts index 5fb6bd92..89761a6b 100644 --- a/packages/react/src/types/props/index.ts +++ b/packages/react/src/types/props/index.ts @@ -50,10 +50,6 @@ export interface DevupComponentProps< export type DevupComponentBaseProps = DevupElementTypeProps & DevupComponentAdditionalProps -export type DevupIntrinsicElement = keyof React.JSX.IntrinsicElements -export type DevupIntrinsicComponentBaseProps = - React.JSX.IntrinsicElements[T] & DevupComponentAdditionalProps - export type DevupElementTypeProps = T extends string ? React.ComponentProps : object diff --git a/packages/react/type-tests/custom-shorthand.ts b/packages/react/type-tests/custom-shorthand.ts index 4a25c640..e49b7a6a 100644 --- a/packages/react/type-tests/custom-shorthand.ts +++ b/packages/react/type-tests/custom-shorthand.ts @@ -26,23 +26,4 @@ const boxProps: Parameters[0] = { }, } -// The concrete default overload is a fast path only. The generic fallback -// must continue to infer intrinsic-element props from `as`. -const polymorphicBox = Box({ - as: 'a', - bg: 'red', - href: '/docs', - onClick: (event) => event.currentTarget.href, - target: '_blank', -}) - -function RequiredLink({ to }: { to: string }) { - return to -} - -const customComponentBox = Box({ - as: RequiredLink, - props: { to: '/docs' }, -}) - -export { boxProps, customComponentBox, customShorthandProps, polymorphicBox } +export { boxProps, customShorthandProps } From 679c2eceff91c705bf7dee0880a69f119dc9d087 Mon Sep 17 00:00:00 2001 From: devfive Date: Sat, 29 Aug 2026 01:00:08 +0900 Subject: [PATCH 11/43] bench: stabilize Turbopack comparison --- benchmark.js | 228 ++++++++++-------- benchmark/next-chakra-ui/package.json | 2 +- benchmark/next-devup-ui-collapse/package.json | 2 +- .../next-devup-ui-single-turbo/package.json | 2 +- benchmark/next-devup-ui-single/package.json | 2 +- benchmark/next-devup-ui-turbo/package.json | 2 +- benchmark/next-devup-ui/package.json | 2 +- benchmark/next-kuma-ui/package.json | 2 +- benchmark/next-mui/package.json | 2 +- benchmark/next-panda-css/package.json | 2 +- .../next-stylex-turbo-devup-ui/package.json | 2 +- benchmark/next-stylex-turbo/package.json | 2 +- benchmark/next-stylex/package.json | 2 +- .../next-tailwind-turbo-devup-ui/package.json | 2 +- benchmark/next-tailwind-turbo/package.json | 2 +- benchmark/next-tailwind/package.json | 2 +- .../package.json | 2 +- benchmark/next-vanilla-extract/package.json | 2 +- 18 files changed, 148 insertions(+), 114 deletions(-) diff --git a/benchmark.js b/benchmark.js index abe9a0f6..708d3714 100644 --- a/benchmark.js +++ b/benchmark.js @@ -1,97 +1,131 @@ -import { existsSync, readdirSync, rmSync, statSync } from 'node:fs' -import { join } from 'node:path' - -import { execSync } from 'child_process' - -function clearBuildFile() { - const dirs = readdirSync('./benchmark') - for (const dir of dirs) { - const base = join('./benchmark', dir) - if (!statSync(base).isDirectory()) continue - for (const output of ['.next', 'dist', 'df']) { - const target = join(base, output) - if (existsSync(target)) rmSync(target, { recursive: true, force: true }) - } - } -} - -function checkDirSize(path, filter) { - let totalSize = 0 - - function calculateSize(directory) { - const entries = readdirSync(directory) - for (const entry of entries) { - const entryPath = join(directory, entry) - if (statSync(entryPath).isDirectory()) { - calculateSize(entryPath) // 재귀적으로 하위 폴더 크기 계산 - } else if (!filter || filter(entryPath)) { - const stats = statSync(entryPath) - totalSize += stats.size // 파일 크기 합산 - } - } - } - - calculateSize(path) - return totalSize -} - -// Sum only the size of emitted CSS files. Build-size totals are dominated by -// JS/assets and hide CSS-only differences (e.g. single-importer collapse). -function checkCssSize(path) { - return checkDirSize(path, (p) => p.endsWith('.css')) -} - -clearBuildFile() - -function benchmark(target) { - // Support both short names ('tailwind' -> next-tailwind) and full names ('vinext-devup-ui') - const hasDir = existsSync(join('./benchmark', target, 'package.json')) - const dir = hasDir ? target : 'next-' + target - - performance.mark(target + '-start') - console.profile(target) - execSync('bun run --filter ' + dir + '-benchmark build', { - stdio: 'inherit', - }) - console.profileEnd(target) - performance.mark(target + '-end') - performance.measure(target, target + '-start', target + '-end') - - const benchmarkDir = join('./benchmark', dir) - // Resolve the real build-output dir. Next.js emits to `.next`; Vite emits to - // `dist`. vinext (Next-on-Vite) emits its real artifacts to `dist` but ALSO - // leaves a tiny vestigial `.next` stub (~988 B, no CSS) - so checking `.next` - // first measured the empty stub and reported "988 bytes (css 0 bytes)" even - // though dist held ~1.28 MB incl. the extracted CSS. Prefer `dist` when it - // exists; fall back to `.next` for pure Next.js apps (which never emit dist). - const distDir = join(benchmarkDir, 'dist') - const outputDir = existsSync(distDir) ? distDir : join(benchmarkDir, '.next') - const duration = ( - performance.getEntriesByName(target)[0].duration / 1000 - ).toFixed(2) - return `${target} ${duration}s ${checkDirSize(outputDir).toLocaleString()} bytes (css ${checkCssSize(outputDir).toLocaleString()} bytes)` -} - -let result = [] - -result.push(benchmark('tailwind')) -result.push(benchmark('stylex')) -result.push(benchmark('stylex-turbo')) -result.push(benchmark('stylex-turbo-devup-ui')) -result.push(benchmark('vanilla-extract')) -result.push(benchmark('kuma-ui')) -result.push(benchmark('panda-css')) -result.push(benchmark('chakra-ui')) -result.push(benchmark('mui')) -result.push(benchmark('devup-ui')) -result.push(benchmark('devup-ui-single')) -result.push(benchmark('tailwind-turbo')) -result.push(benchmark('devup-ui-single-turbo')) -result.push(benchmark('devup-ui-turbo')) -result.push(benchmark('vanilla-extract-devup-ui')) -result.push(benchmark('tailwind-turbo-devup-ui')) -result.push(benchmark('vinext-devup-ui')) -// Multi-component app exercising single-importer collapse (atom dedup). -result.push(benchmark('devup-ui-collapse')) - -console.info(result.join('\n')) +import { existsSync, readdirSync, rmSync, statSync } from 'node:fs' +import { join } from 'node:path' + +import { execSync } from 'child_process' + +function clearBuildFile(dir) { + const base = join('./benchmark', dir) + for (const output of ['.next', 'dist', 'df']) { + const target = join(base, output) + if (existsSync(target)) rmSync(target, { recursive: true, force: true }) + } +} + +function checkDirSize(path, filter) { + let totalSize = 0 + + function calculateSize(directory) { + const entries = readdirSync(directory) + for (const entry of entries) { + const entryPath = join(directory, entry) + if (statSync(entryPath).isDirectory()) { + calculateSize(entryPath) // 재귀적으로 하위 폴더 크기 계산 + } else if (!filter || filter(entryPath)) { + const stats = statSync(entryPath) + totalSize += stats.size // 파일 크기 합산 + } + } + } + + calculateSize(path) + return totalSize +} + +// Sum only the size of emitted CSS files. Build-size totals are dominated by +// JS/assets and hide CSS-only differences (e.g. single-importer collapse). +function checkCssSize(path) { + return checkDirSize(path, (p) => p.endsWith('.css')) +} + +let benchmarkRun = 0 + +function benchmark(target) { + // Support both short names ('tailwind' -> next-tailwind) and full names ('vinext-devup-ui') + const hasDir = existsSync(join('./benchmark', target, 'package.json')) + const dir = hasDir ? target : 'next-' + target + const run = `${target}-${benchmarkRun++}` + + clearBuildFile(dir) + performance.mark(run + '-start') + console.profile(run) + execSync('bun run --filter ' + dir + '-benchmark build', { + stdio: 'inherit', + }) + console.profileEnd(run) + performance.mark(run + '-end') + performance.measure(run, run + '-start', run + '-end') + + const benchmarkDir = join('./benchmark', dir) + // Resolve the real build-output dir. Next.js emits to `.next`; Vite emits to + // `dist`. vinext (Next-on-Vite) emits its real artifacts to `dist` but ALSO + // leaves a tiny vestigial `.next` stub (~988 B, no CSS) - so checking `.next` + // first measured the empty stub and reported "988 bytes (css 0 bytes)" even + // though dist held ~1.28 MB incl. the extracted CSS. Prefer `dist` when it + // exists; fall back to `.next` for pure Next.js apps (which never emit dist). + const distDir = join(benchmarkDir, 'dist') + const outputDir = existsSync(distDir) ? distDir : join(benchmarkDir, '.next') + const duration = performance.getEntriesByName(run)[0].duration / 1000 + return { + duration, + result: `${target} ${duration.toFixed(2)}s ${checkDirSize(outputDir).toLocaleString()} bytes (css ${checkCssSize(outputDir).toLocaleString()} bytes)`, + } +} + +let result = [] +const turboSamples = new Map([ + ['tailwind-turbo', []], + ['devup-ui-single-turbo', []], +]) + +function record(target) { + const sample = benchmark(target) + const samples = turboSamples.get(target) + if (samples) samples.push(sample.duration) + result.push(sample.result) +} + +record('tailwind') +record('stylex') +record('stylex-turbo') +record('stylex-turbo-devup-ui') +record('vanilla-extract') +record('kuma-ui') +record('panda-css') +record('chakra-ui') +record('mui') +record('devup-ui') +record('devup-ui-single') +record('tailwind-turbo') +record('devup-ui-single-turbo') +record('devup-ui-turbo') +record('vanilla-extract-devup-ui') +record('tailwind-turbo-devup-ui') +record('vinext-devup-ui') +// Multi-component app exercising single-importer collapse (atom dedup). +record('devup-ui-collapse') + +// A single fixed-order result on a shared CI runner is too noisy for the two +// Turbopack builds we compare directly. Run six cold samples in alternating +// order so each target runs first three times, then report their medians. +const turboTargets = ['tailwind-turbo', 'devup-ui-single-turbo'] +for (let sample = 1; sample < 6; sample++) { + const order = sample % 2 === 0 ? turboTargets : turboTargets.toReversed() + for (const target of order) { + turboSamples.get(target).push(benchmark(target).duration) + } +} + +function median(samples) { + const sorted = samples.toSorted((a, b) => a - b) + const middle = sorted.length / 2 + return (sorted[middle - 1] + sorted[middle]) / 2 +} + +for (const target of turboTargets) { + const samples = turboSamples.get(target) + result.push( + `${target} median ${median(samples).toFixed(2)}s (${samples.length} cold samples: ${samples.map((sample) => sample.toFixed(2) + 's').join(', ')})`, + ) +} + +console.info(result.join('\n')) diff --git a/benchmark/next-chakra-ui/package.json b/benchmark/next-chakra-ui/package.json index 02fc3eb3..acf4a315 100644 --- a/benchmark/next-chakra-ui/package.json +++ b/benchmark/next-chakra-ui/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build --experimental-debug-memory-usage --webpack", + "build": "next build --webpack", "start": "next start", "lint": "next lint" }, diff --git a/benchmark/next-devup-ui-collapse/package.json b/benchmark/next-devup-ui-collapse/package.json index 4e1a9def..433c0ddf 100644 --- a/benchmark/next-devup-ui-collapse/package.json +++ b/benchmark/next-devup-ui-collapse/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build --experimental-debug-memory-usage", + "build": "next build", "start": "next start", "lint": "eslint" }, diff --git a/benchmark/next-devup-ui-single-turbo/package.json b/benchmark/next-devup-ui-single-turbo/package.json index de2da464..2373034e 100644 --- a/benchmark/next-devup-ui-single-turbo/package.json +++ b/benchmark/next-devup-ui-single-turbo/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build --experimental-debug-memory-usage", + "build": "next build", "start": "next start", "lint": "eslint" }, diff --git a/benchmark/next-devup-ui-single/package.json b/benchmark/next-devup-ui-single/package.json index 5b19bc77..1ddfdddd 100644 --- a/benchmark/next-devup-ui-single/package.json +++ b/benchmark/next-devup-ui-single/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build --experimental-debug-memory-usage --webpack", + "build": "next build --webpack", "start": "next start", "lint": "eslint" }, diff --git a/benchmark/next-devup-ui-turbo/package.json b/benchmark/next-devup-ui-turbo/package.json index 1dbaeeff..209e30ef 100644 --- a/benchmark/next-devup-ui-turbo/package.json +++ b/benchmark/next-devup-ui-turbo/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build --experimental-debug-memory-usage", + "build": "next build", "start": "next start", "lint": "eslint" }, diff --git a/benchmark/next-devup-ui/package.json b/benchmark/next-devup-ui/package.json index 676a1adb..e3bf5543 100644 --- a/benchmark/next-devup-ui/package.json +++ b/benchmark/next-devup-ui/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build --experimental-debug-memory-usage --webpack", + "build": "next build --webpack", "start": "next start", "lint": "eslint" }, diff --git a/benchmark/next-kuma-ui/package.json b/benchmark/next-kuma-ui/package.json index 25fa09c7..46985548 100644 --- a/benchmark/next-kuma-ui/package.json +++ b/benchmark/next-kuma-ui/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build --experimental-debug-memory-usage --webpack", + "build": "next build --webpack", "start": "next start", "lint": "next lint" }, diff --git a/benchmark/next-mui/package.json b/benchmark/next-mui/package.json index 60324edc..5a091ad2 100644 --- a/benchmark/next-mui/package.json +++ b/benchmark/next-mui/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build --experimental-debug-memory-usage --webpack", + "build": "next build --webpack", "start": "next start", "lint": "next lint" }, diff --git a/benchmark/next-panda-css/package.json b/benchmark/next-panda-css/package.json index a3f39c98..22b6c96d 100644 --- a/benchmark/next-panda-css/package.json +++ b/benchmark/next-panda-css/package.json @@ -6,7 +6,7 @@ "scripts": { "prepare": "panda codegen", "dev": "next dev", - "build": "next build --experimental-debug-memory-usage --webpack", + "build": "next build --webpack", "start": "next start", "lint": "next lint" }, diff --git a/benchmark/next-stylex-turbo-devup-ui/package.json b/benchmark/next-stylex-turbo-devup-ui/package.json index 398b2e55..db9debac 100644 --- a/benchmark/next-stylex-turbo-devup-ui/package.json +++ b/benchmark/next-stylex-turbo-devup-ui/package.json @@ -7,7 +7,7 @@ "predev": "rimraf .next df", "prebuild": "rimraf .next df", "dev": "next dev", - "build": "next build --experimental-debug-memory-usage", + "build": "next build", "start": "next start" }, "dependencies": { diff --git a/benchmark/next-stylex-turbo/package.json b/benchmark/next-stylex-turbo/package.json index 1dd2cc0c..2c7cdbdf 100644 --- a/benchmark/next-stylex-turbo/package.json +++ b/benchmark/next-stylex-turbo/package.json @@ -6,7 +6,7 @@ "predev": "rimraf .next", "prebuild": "rimraf .next", "dev": "next dev", - "build": "next build --experimental-debug-memory-usage", + "build": "next build", "start": "next start" }, "dependencies": { diff --git a/benchmark/next-stylex/package.json b/benchmark/next-stylex/package.json index 2a2ca50b..7f19835a 100644 --- a/benchmark/next-stylex/package.json +++ b/benchmark/next-stylex/package.json @@ -6,7 +6,7 @@ "predev": "rimraf .next", "prebuild": "rimraf .next", "dev": "next dev", - "build": "next build --experimental-debug-memory-usage --webpack", + "build": "next build --webpack", "start": "next start", "lint": "next lint" }, diff --git a/benchmark/next-tailwind-turbo-devup-ui/package.json b/benchmark/next-tailwind-turbo-devup-ui/package.json index ac60a615..105a0c26 100644 --- a/benchmark/next-tailwind-turbo-devup-ui/package.json +++ b/benchmark/next-tailwind-turbo-devup-ui/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build --experimental-debug-memory-usage", + "build": "next build", "start": "next start", "lint": "next lint" }, diff --git a/benchmark/next-tailwind-turbo/package.json b/benchmark/next-tailwind-turbo/package.json index 4dfa6736..a2e6fe58 100644 --- a/benchmark/next-tailwind-turbo/package.json +++ b/benchmark/next-tailwind-turbo/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build --experimental-debug-memory-usage", + "build": "next build", "start": "next start", "lint": "next lint" }, diff --git a/benchmark/next-tailwind/package.json b/benchmark/next-tailwind/package.json index 24d7589c..2d355c4d 100644 --- a/benchmark/next-tailwind/package.json +++ b/benchmark/next-tailwind/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build --experimental-debug-memory-usage --webpack", + "build": "next build --webpack", "start": "next start", "lint": "next lint" }, diff --git a/benchmark/next-vanilla-extract-devup-ui/package.json b/benchmark/next-vanilla-extract-devup-ui/package.json index 99c7e3af..49b232e9 100644 --- a/benchmark/next-vanilla-extract-devup-ui/package.json +++ b/benchmark/next-vanilla-extract-devup-ui/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build --experimental-debug-memory-usage --webpack", + "build": "next build --webpack", "start": "next start", "lint": "next lint" }, diff --git a/benchmark/next-vanilla-extract/package.json b/benchmark/next-vanilla-extract/package.json index 497864c0..03457426 100644 --- a/benchmark/next-vanilla-extract/package.json +++ b/benchmark/next-vanilla-extract/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build --experimental-debug-memory-usage --webpack", + "build": "next build --webpack", "start": "next start", "lint": "next lint" }, From 96c5d3a2a263806a621a3fb3206439f2d31bca23 Mon Sep 17 00:00:00 2001 From: devfive Date: Sat, 29 Aug 2026 01:19:25 +0900 Subject: [PATCH 12/43] perf: restore size-optimized WASM --- Cargo.toml | 2 +- bindings/devup-ui-wasm/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a07213ba..bc56d234 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ used_underscore_binding = "allow" # Optimize the build-time extractor for throughput. The WASM module runs in # Node during every Devup UI build, so extraction speed is more important than # download size. -opt-level = 3 +opt-level = "s" # Link-time optimization: enables cross-crate inlining and dead code elimination lto = true # Single codegen unit: maximizes optimization at cost of compile time diff --git a/bindings/devup-ui-wasm/Cargo.toml b/bindings/devup-ui-wasm/Cargo.toml index f7674e19..cd8419bc 100644 --- a/bindings/devup-ui-wasm/Cargo.toml +++ b/bindings/devup-ui-wasm/Cargo.toml @@ -54,7 +54,7 @@ workspace = true # only the features listed here, so it has to be allowed explicitly. It is part # of the WebAssembly 2.0 baseline (Chrome 74, Firefox 62, Safari 14.1, Node 12). wasm-opt = [ - "-O3", + "-Oz", "--enable-bulk-memory", "--enable-nontrapping-float-to-int", "--enable-sign-ext", From 823b5a3e30fc1abc2b86f78b8da476b4e733259c Mon Sep 17 00:00:00 2001 From: devfive Date: Sat, 29 Aug 2026 01:19:26 +0900 Subject: [PATCH 13/43] perf: avoid redundant polymorphic prop inference --- packages/react/src/types/props/index.ts | 2 +- packages/react/type-tests/custom-shorthand.ts | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/react/src/types/props/index.ts b/packages/react/src/types/props/index.ts index 89761a6b..6e96cba9 100644 --- a/packages/react/src/types/props/index.ts +++ b/packages/react/src/types/props/index.ts @@ -48,7 +48,7 @@ export interface DevupComponentProps< styleVars?: Record } export type DevupComponentBaseProps = - DevupElementTypeProps & DevupComponentAdditionalProps + DevupElementTypeProps> & DevupComponentAdditionalProps> export type DevupElementTypeProps = T extends string ? React.ComponentProps : object diff --git a/packages/react/type-tests/custom-shorthand.ts b/packages/react/type-tests/custom-shorthand.ts index e49b7a6a..ab069c24 100644 --- a/packages/react/type-tests/custom-shorthand.ts +++ b/packages/react/type-tests/custom-shorthand.ts @@ -26,4 +26,33 @@ const boxProps: Parameters[0] = { }, } +// Polymorphic inference must come from `as` while preserving the exact native +// element props and their contextual event types. +Box({ + as: 'a', + href: '/docs', + onClick(event) { + return event.currentTarget.href + }, +}) + +Box({ + as: 'button', + onClick(event) { + return event.currentTarget.disabled + }, +}) + +// @ts-expect-error href is not a button prop +Box({ as: 'button', href: '/docs' }) + +function CustomLink(_props: { to: string }) { + return null +} + +Box({ as: CustomLink, props: { to: '/docs' } }) + +// @ts-expect-error required custom-component props stay required +Box({ as: CustomLink }) + export { boxProps, customShorthandProps } From 3af6ba972a3acabac0774e821ba1bd1a6e117efb Mon Sep 17 00:00:00 2001 From: devfive Date: Sat, 29 Aug 2026 01:20:30 +0900 Subject: [PATCH 14/43] fix: normalize benchmark manifests --- benchmark/next-chakra-ui/package.json | 2 +- benchmark/next-devup-ui-single-turbo/package.json | 2 +- benchmark/next-devup-ui-single/package.json | 2 +- benchmark/next-devup-ui/package.json | 2 +- benchmark/next-kuma-ui/package.json | 2 +- benchmark/next-mui/package.json | 2 +- benchmark/next-panda-css/package.json | 2 +- benchmark/next-stylex/package.json | 2 +- benchmark/next-tailwind-turbo/package.json | 2 +- benchmark/next-tailwind/package.json | 2 +- benchmark/next-vanilla-extract/package.json | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/benchmark/next-chakra-ui/package.json b/benchmark/next-chakra-ui/package.json index acf4a315..cceff3f9 100644 --- a/benchmark/next-chakra-ui/package.json +++ b/benchmark/next-chakra-ui/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build --webpack", + "build": "next build --webpack", "start": "next start", "lint": "next lint" }, diff --git a/benchmark/next-devup-ui-single-turbo/package.json b/benchmark/next-devup-ui-single-turbo/package.json index 2373034e..85b646ed 100644 --- a/benchmark/next-devup-ui-single-turbo/package.json +++ b/benchmark/next-devup-ui-single-turbo/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build", + "build": "next build", "start": "next start", "lint": "eslint" }, diff --git a/benchmark/next-devup-ui-single/package.json b/benchmark/next-devup-ui-single/package.json index 1ddfdddd..b6c6bf54 100644 --- a/benchmark/next-devup-ui-single/package.json +++ b/benchmark/next-devup-ui-single/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build --webpack", + "build": "next build --webpack", "start": "next start", "lint": "eslint" }, diff --git a/benchmark/next-devup-ui/package.json b/benchmark/next-devup-ui/package.json index e3bf5543..4bd25854 100644 --- a/benchmark/next-devup-ui/package.json +++ b/benchmark/next-devup-ui/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build --webpack", + "build": "next build --webpack", "start": "next start", "lint": "eslint" }, diff --git a/benchmark/next-kuma-ui/package.json b/benchmark/next-kuma-ui/package.json index 46985548..17d5648e 100644 --- a/benchmark/next-kuma-ui/package.json +++ b/benchmark/next-kuma-ui/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build --webpack", + "build": "next build --webpack", "start": "next start", "lint": "next lint" }, diff --git a/benchmark/next-mui/package.json b/benchmark/next-mui/package.json index 5a091ad2..5bed6878 100644 --- a/benchmark/next-mui/package.json +++ b/benchmark/next-mui/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build --webpack", + "build": "next build --webpack", "start": "next start", "lint": "next lint" }, diff --git a/benchmark/next-panda-css/package.json b/benchmark/next-panda-css/package.json index 22b6c96d..b96cee5c 100644 --- a/benchmark/next-panda-css/package.json +++ b/benchmark/next-panda-css/package.json @@ -6,7 +6,7 @@ "scripts": { "prepare": "panda codegen", "dev": "next dev", - "build": "next build --webpack", + "build": "next build --webpack", "start": "next start", "lint": "next lint" }, diff --git a/benchmark/next-stylex/package.json b/benchmark/next-stylex/package.json index 7f19835a..c57cfe38 100644 --- a/benchmark/next-stylex/package.json +++ b/benchmark/next-stylex/package.json @@ -6,7 +6,7 @@ "predev": "rimraf .next", "prebuild": "rimraf .next", "dev": "next dev", - "build": "next build --webpack", + "build": "next build --webpack", "start": "next start", "lint": "next lint" }, diff --git a/benchmark/next-tailwind-turbo/package.json b/benchmark/next-tailwind-turbo/package.json index a2e6fe58..5f82baa2 100644 --- a/benchmark/next-tailwind-turbo/package.json +++ b/benchmark/next-tailwind-turbo/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build", + "build": "next build", "start": "next start", "lint": "next lint" }, diff --git a/benchmark/next-tailwind/package.json b/benchmark/next-tailwind/package.json index 2d355c4d..07e9af58 100644 --- a/benchmark/next-tailwind/package.json +++ b/benchmark/next-tailwind/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build --webpack", + "build": "next build --webpack", "start": "next start", "lint": "next lint" }, diff --git a/benchmark/next-vanilla-extract/package.json b/benchmark/next-vanilla-extract/package.json index 03457426..b73225c5 100644 --- a/benchmark/next-vanilla-extract/package.json +++ b/benchmark/next-vanilla-extract/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build --webpack", + "build": "next build --webpack", "start": "next start", "lint": "next lint" }, From 181b6cdcd4b9dceec89acb6ce13fb3bd34a5a748 Mon Sep 17 00:00:00 2001 From: devfive Date: Sat, 29 Aug 2026 01:32:49 +0900 Subject: [PATCH 15/43] perf: simplify intrinsic component props --- packages/react/src/types/props/index.ts | 6 +++++- packages/react/type-tests/custom-shorthand.ts | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/react/src/types/props/index.ts b/packages/react/src/types/props/index.ts index 6e96cba9..b77f0d5b 100644 --- a/packages/react/src/types/props/index.ts +++ b/packages/react/src/types/props/index.ts @@ -48,7 +48,11 @@ export interface DevupComponentProps< styleVars?: Record } export type DevupComponentBaseProps = - DevupElementTypeProps> & DevupComponentAdditionalProps> + NoInfer extends string + ? React.ComponentProps> & { + props?: FilterChildren>> + } + : DevupComponentAdditionalProps> export type DevupElementTypeProps = T extends string ? React.ComponentProps : object diff --git a/packages/react/type-tests/custom-shorthand.ts b/packages/react/type-tests/custom-shorthand.ts index ab069c24..ba6a6d32 100644 --- a/packages/react/type-tests/custom-shorthand.ts +++ b/packages/react/type-tests/custom-shorthand.ts @@ -1,4 +1,9 @@ import { Box, type DevupProps } from '../src' +import type { + DevupComponentAdditionalProps, + DevupComponentBaseProps, + DevupElementTypeProps, +} from '../src/types/props' // Mirrors the module augmentation emitted to /theme.d.ts. declare module '../src' { @@ -55,4 +60,19 @@ Box({ as: CustomLink, props: { to: '/docs' } }) // @ts-expect-error required custom-component props stay required Box({ as: CustomLink }) +type Assert = T +type LegacyBaseProps = DevupElementTypeProps & + DevupComponentAdditionalProps +type IsEquivalent = + DevupComponentBaseProps extends LegacyBaseProps + ? LegacyBaseProps extends DevupComponentBaseProps + ? true + : false + : false + +type _DivPropsStayEquivalent = Assert> +type _AnchorPropsStayEquivalent = Assert> +type _ButtonPropsStayEquivalent = Assert> +type _CustomPropsStayEquivalent = Assert> + export { boxProps, customShorthandProps } From d5facff7ef8d16fdd17f6c2964bcdf9db6c5081e Mon Sep 17 00:00:00 2001 From: devfive Date: Sat, 29 Aug 2026 01:34:27 +0900 Subject: [PATCH 16/43] docs: explain size-optimized WASM --- Cargo.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bc56d234..26cba5dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,9 +50,7 @@ use_self = "allow" used_underscore_binding = "allow" [profile.release] -# Optimize the build-time extractor for throughput. The WASM module runs in -# Node during every Devup UI build, so extraction speed is more important than -# download size. +# Optimize for small code size (critical for WASM binary) opt-level = "s" # Link-time optimization: enables cross-crate inlining and dead code elimination lto = true From 0c437eb00125fc422979bb7bc5c164d87a3645ea Mon Sep 17 00:00:00 2001 From: devfive Date: Sat, 29 Aug 2026 01:50:59 +0900 Subject: [PATCH 17/43] test: cover profiled base CSS snapshot --- packages/next-plugin/src/__tests__/coordinator.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next-plugin/src/__tests__/coordinator.test.ts b/packages/next-plugin/src/__tests__/coordinator.test.ts index f1b6325a..548012fb 100644 --- a/packages/next-plugin/src/__tests__/coordinator.test.ts +++ b/packages/next-plugin/src/__tests__/coordinator.test.ts @@ -234,7 +234,7 @@ describe('coordinator', () => { map: undefined, css: 'collected css', cssFile: 'devup-ui-1.css', - updatedBaseStyle: true, + updatedBaseStyle: false, free: mock(), [Symbol.dispose]: mock(), }) From ed1a9e2025914c35f17d37f7619608d1b65cebdc Mon Sep 17 00:00:00 2001 From: devfive Date: Sat, 29 Aug 2026 02:07:41 +0900 Subject: [PATCH 18/43] test: cover both profiled base CSS paths --- .../src/__tests__/coordinator.test.ts | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/packages/next-plugin/src/__tests__/coordinator.test.ts b/packages/next-plugin/src/__tests__/coordinator.test.ts index 548012fb..ee4ccff1 100644 --- a/packages/next-plugin/src/__tests__/coordinator.test.ts +++ b/packages/next-plugin/src/__tests__/coordinator.test.ts @@ -225,7 +225,7 @@ describe('coordinator', () => { coordinator.close() }) - it('profiles serialization work separately from writes', async () => { + it('profiles both base CSS serialization paths separately from writes', async () => { const originalProfile = process.env.DEVUP_UI_PROFILE process.env.DEVUP_UI_PROFILE = '1' const infoSpy = spyOn(console, 'info').mockImplementation(() => {}) @@ -292,6 +292,50 @@ describe('coordinator', () => { expect(profile.cssSnapshotMs).toBeTypeOf('number') expect(profile.fileMapSnapshotMs).toBeTypeOf('number') expect(profile.sheetSnapshotMs).toBeTypeOf('number') + + codeExtractSpy.mockReturnValue({ + code: 'transformed base code', + map: undefined, + css: 'collected base css', + cssFile: 'devup-ui-2.css', + updatedBaseStyle: true, + free: mock(), + [Symbol.dispose]: mock(), + }) + const baseRes = await httpRequest( + port, + 'POST', + '/extract', + JSON.stringify({ + filename: 'src/profile-base.tsx', + code: 'const profileBase = true', + resourcePath: join(process.cwd(), 'src', 'profile-base.tsx'), + }), + ) + + expect(baseRes.status).toBe(200) + const baseMessage = infoSpy.mock.calls + .map(([value]) => value) + .find( + (value): value is string => + typeof value === 'string' && + value.includes('"filename":"src/profile-base.tsx"'), + ) + if (baseMessage === undefined) { + throw new Error('missing base CSS extract profile') + } + const baseProfile = JSON.parse( + baseMessage.slice('[devup-ui:profile] '.length), + ) as Record + + expect(baseProfile).toMatchObject({ + cacheHit: false, + cssSnapshotBytes: expect.any(Number), + filename: 'src/profile-base.tsx', + phase: 'coordinator.extract', + scheduledWrites: 5, + }) + expect(baseProfile.cssSnapshotMs).toBeTypeOf('number') } finally { coordinator.close() infoSpy.mockRestore() From 07e155bdac3a2bce70e872533089789d93a21616 Mon Sep 17 00:00:00 2001 From: devfive Date: Sat, 29 Aug 2026 03:02:36 +0900 Subject: [PATCH 19/43] perf(next-plugin): reduce Turbopack extraction memory --- bindings/devup-ui-wasm/src/lib.rs | 165 +++++++++++++++--- libs/extractor/src/lib.rs | 28 ++- .../src/__tests__/coordinator.test.ts | 72 +++++++- .../next-plugin/src/__tests__/plugin.test.ts | 35 +++- packages/next-plugin/src/coordinator.ts | 65 +++++-- packages/next-plugin/src/plugin.ts | 31 ++-- 6 files changed, 328 insertions(+), 68 deletions(-) diff --git a/bindings/devup-ui-wasm/src/lib.rs b/bindings/devup-ui-wasm/src/lib.rs index b039fad1..6e7a0b73 100644 --- a/bindings/devup-ui-wasm/src/lib.rs +++ b/bindings/devup-ui-wasm/src/lib.rs @@ -3,7 +3,7 @@ use css::file_map::{ canonical, is_global, set_canonical_map, set_file_map, with_canonical_map, with_file_map, }; use extractor::extract_style::extract_style_value::ExtractStyleValue; -use extractor::{ExtractOption, ImportAlias, extract, has_devup_ui}; +use extractor::{ExtractOption, ImportAlias, extract, extract_without_source_map, has_devup_ui}; use rustc_hash::FxHashSet; use sheet::StyleSheet; use std::collections::{BTreeMap, HashMap}; @@ -14,6 +14,12 @@ use wasm_bindgen::prelude::*; static GLOBAL_STYLE_SHEET: LazyLock> = LazyLock::new(|| Mutex::new(StyleSheet::default())); +#[derive(Clone, Copy)] +enum SourceMapMode { + Generate, + Skip, +} + fn with_style_sheet(f: F) -> R where F: FnOnce(&StyleSheet) -> R, @@ -321,17 +327,68 @@ pub fn code_extract_internal( import_main_css_in_css: bool, import_aliases: HashMap, ) -> Result { - match extract( + code_extract_internal_impl( filename, code, - ExtractOption { - package: package.to_string(), - css_dir, - single_css, - import_main_css: import_main_css_in_code, - import_aliases, - }, - ) { + package, + css_dir, + single_css, + import_main_css_in_code, + import_main_css_in_css, + import_aliases, + SourceMapMode::Generate, + ) +} + +#[allow(clippy::too_many_arguments)] +pub fn code_extract_without_source_map_internal( + filename: &str, + code: &str, + package: &str, + css_dir: String, + single_css: bool, + import_main_css_in_code: bool, + import_main_css_in_css: bool, + import_aliases: HashMap, +) -> Result { + code_extract_internal_impl( + filename, + code, + package, + css_dir, + single_css, + import_main_css_in_code, + import_main_css_in_css, + import_aliases, + SourceMapMode::Skip, + ) +} + +#[allow(clippy::too_many_arguments)] +fn code_extract_internal_impl( + filename: &str, + code: &str, + package: &str, + css_dir: String, + single_css: bool, + import_main_css_in_code: bool, + import_main_css_in_css: bool, + import_aliases: HashMap, + source_map: SourceMapMode, +) -> Result { + let option = ExtractOption { + package: package.to_string(), + css_dir, + single_css, + import_main_css: import_main_css_in_code, + import_aliases, + }; + let extracted = match source_map { + SourceMapMode::Generate => extract(filename, code, option), + SourceMapMode::Skip => extract_without_source_map(filename, code, option), + }; + + match extracted { Ok(output) => Ok(Output::new( output.code, output.styles, @@ -345,6 +402,24 @@ pub fn code_extract_internal( } } +#[cfg(not(tarpaulin_include))] +fn import_aliases_from_js( + import_aliases: JsValue, +) -> Result, JsValue> { + let aliases: HashMap> = + serde_wasm_bindgen::from_value(import_aliases).map_err(js_error)?; + Ok(aliases + .into_iter() + .map(|(key, value)| { + let alias = match value { + Some(name) => ImportAlias::DefaultToNamed(name), + None => ImportAlias::NamedToNamed, + }; + (key, alias) + }) + .collect()) +} + #[cfg(not(tarpaulin_include))] #[wasm_bindgen(js_name = "codeExtract")] #[allow(clippy::too_many_arguments)] @@ -358,23 +433,6 @@ pub fn code_extract( import_main_css_in_css: bool, import_aliases: JsValue, ) -> Result { - // Deserialize import_aliases from JsValue - // Format: { "package": "namedExport" } or { "package": null } for named exports - let aliases: HashMap> = - serde_wasm_bindgen::from_value(import_aliases).map_err(js_error)?; - - // Convert to ImportAlias enum - let import_aliases: HashMap = aliases - .into_iter() - .map(|(k, v)| { - let alias = match v { - Some(name) => ImportAlias::DefaultToNamed(name), - None => ImportAlias::NamedToNamed, - }; - (k, alias) - }) - .collect(); - code_extract_internal( filename, code, @@ -383,7 +441,33 @@ pub fn code_extract( single_css, import_main_css_in_code, import_main_css_in_css, - import_aliases, + import_aliases_from_js(import_aliases)?, + ) + .map_err(js_error) +} + +#[cfg(not(tarpaulin_include))] +#[wasm_bindgen(js_name = "codeExtractWithoutSourceMap")] +#[allow(clippy::too_many_arguments)] +pub fn code_extract_without_source_map( + filename: &str, + code: &str, + package: &str, + css_dir: String, + single_css: bool, + import_main_css_in_code: bool, + import_main_css_in_css: bool, + import_aliases: JsValue, +) -> Result { + code_extract_without_source_map_internal( + filename, + code, + package, + css_dir, + single_css, + import_main_css_in_code, + import_main_css_in_css, + import_aliases_from_js(import_aliases)?, ) .map_err(js_error) } @@ -1710,6 +1794,31 @@ mod tests { assert!(result.is_ok()); let output = result.unwrap(); assert!(!output.code().is_empty()); + assert!(output.map().is_some()); + } + + #[test] + #[serial] + fn test_code_extract_without_source_map_internal() { + *GLOBAL_STYLE_SHEET.lock().unwrap() = StyleSheet::default(); + css::class_map::reset_class_map(); + + let result = code_extract_without_source_map_internal( + "test.tsx", + r#"import {Box} from '@devup-ui/react' +"#, + "@devup-ui/react", + "@devup-ui/react".to_string(), + false, + false, + false, + HashMap::new(), + ); + + assert!(result.is_ok()); + let output = result.unwrap(); + assert!(!output.code().is_empty()); + assert!(output.map().is_none()); } #[test] diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs index 79a91e27..bd785175 100644 --- a/libs/extractor/src/lib.rs +++ b/libs/extractor/src/lib.rs @@ -215,6 +215,23 @@ pub fn extract( filename: &str, code: &str, option: ExtractOption, +) -> Result> { + extract_with_source_map(filename, code, option, true) +} + +pub fn extract_without_source_map( + filename: &str, + code: &str, + option: ExtractOption, +) -> Result> { + extract_with_source_map(filename, code, option, false) +} + +fn extract_with_source_map( + filename: &str, + code: &str, + option: ExtractOption, + source_map: bool, ) -> Result> { // Step 1: Transform import aliases // e.g., `import styled from '@emotion/styled'` → `import { styled } from '@devup-ui/react'` @@ -328,12 +345,15 @@ pub fn extract( if global { None } else { Some(bucket) }, ); visitor.visit_program(&mut program); - let result = Codegen::new() - .with_options(CodegenOptions { + let codegen_options = if source_map { + CodegenOptions { source_map_path: Some(PathBuf::from(filename)), ..Default::default() - }) - .build(&program); + } + } else { + CodegenOptions::default() + }; + let result = Codegen::new().with_options(codegen_options).build(&program); Ok(ExtractOutput { styles: visitor.styles, diff --git a/packages/next-plugin/src/__tests__/coordinator.test.ts b/packages/next-plugin/src/__tests__/coordinator.test.ts index ee4ccff1..93d9e2d0 100644 --- a/packages/next-plugin/src/__tests__/coordinator.test.ts +++ b/packages/next-plugin/src/__tests__/coordinator.test.ts @@ -21,6 +21,7 @@ import { } from '../coordinator' let codeExtractSpy: ReturnType +let codeExtractWithoutSourceMapSpy: ReturnType let getCssSpy: ReturnType let exportSheetSpy: ReturnType let exportClassMapSpy: ReturnType @@ -81,6 +82,7 @@ function httpRequest( beforeEach(() => { codeExtractSpy = spyOn(wasm, 'codeExtract') + codeExtractWithoutSourceMapSpy = spyOn(wasm, 'codeExtractWithoutSourceMap') getCssSpy = spyOn(wasm, 'getCss') exportSheetSpy = spyOn(wasm, 'exportSheet') exportClassMapSpy = spyOn(wasm, 'exportClassMap') @@ -97,6 +99,7 @@ beforeEach(() => { afterEach(() => { resetCoordinator() codeExtractSpy.mockRestore() + codeExtractWithoutSourceMapSpy.mockRestore() getCssSpy.mockRestore() exportSheetSpy.mockRestore() exportClassMapSpy.mockRestore() @@ -124,14 +127,15 @@ describe('coordinator', () => { }) it('should handle /extract endpoint', async () => { - codeExtractSpy.mockReturnValue({ + const extractOutput = { code: 'transformed code', map: '{"version":3}', cssFile: 'devup-ui-1.css', updatedBaseStyle: true, free: mock(), [Symbol.dispose]: mock(), - }) + } + codeExtractSpy.mockReturnValue(extractOutput) getCssSpy.mockImplementation( (fileNum: number | null, _importMainCss: boolean) => { if (fileNum === null) return 'base-css' @@ -170,6 +174,7 @@ describe('coordinator', () => { // Verify WASM was called expect(codeExtractSpy).toHaveBeenCalledTimes(1) + expect(extractOutput.free).toHaveBeenCalledTimes(1) // Verify files were written (base CSS + per-file CSS + sheet + classmap + filemap) expect(writeFileSpy).toHaveBeenCalledTimes(5) @@ -177,6 +182,38 @@ describe('coordinator', () => { coordinator.close() }) + it('skips source-map generation when requested', async () => { + const extractOutput = { + code: 'transformed code', + map: undefined, + cssFile: undefined, + updatedBaseStyle: false, + free: mock(), + [Symbol.dispose]: mock(), + } + codeExtractWithoutSourceMapSpy.mockReturnValue(extractOutput) + const coordinator = startCoordinator(makeOptions({ sourceMap: false })) + await new Promise((r) => setTimeout(r, 100)) + const portStr = (writeFileSyncSpy.mock.calls[0] as [string, string])[1] + + const res = await httpRequest( + parseInt(portStr), + 'POST', + '/extract', + JSON.stringify({ + filename: 'src/App.tsx', + code: 'const x = ', + resourcePath: join(process.cwd(), 'src', 'App.tsx'), + }), + ) + + expect(res.status).toBe(200) + expect(codeExtractWithoutSourceMapSpy).toHaveBeenCalledTimes(1) + expect(codeExtractSpy).not.toHaveBeenCalled() + expect(extractOutput.free).toHaveBeenCalledTimes(1) + coordinator.close() + }) + it('reuses byte-identical singleCss prewarm output', async () => { const source = 'const x = ' const options = makeOptions({ @@ -186,7 +223,6 @@ describe('coordinator', () => { 'src/App.tsx', { code: 'transformed prewarm code', - css: 'prewarmed css', cssFile: 'devup-ui.css', map: '{"version":3}', source, @@ -726,6 +762,36 @@ describe('coordinator', () => { coordinator.close() }) + it('replaces an existing coordinator without retaining its server', async () => { + const options = makeOptions() + const first = startCoordinator(options) + await new Promise((resolve) => setTimeout(resolve, 100)) + const firstPort = parseInt( + (writeFileSyncSpy.mock.calls.at(-1) as [string, string])[1], + ) + + const second = startCoordinator(options) + await new Promise((resolve) => setTimeout(resolve, 100)) + const secondPort = parseInt( + (writeFileSyncSpy.mock.calls.at(-1) as [string, string])[1], + ) + + let firstClosed = false + try { + await httpRequest(firstPort, 'GET', '/health') + } catch { + firstClosed = true + } + expect(firstClosed).toBe(true) + + // Closing the superseded handle must not close the replacement server. + first.close() + const res = await httpRequest(secondPort, 'GET', '/health') + expect(res).toEqual({ status: 200, body: 'ok' }) + + second.close() + }) + it('should touch devup-ui.css to invalidate Turbopack cache when singleCss=false and new CSS collected', async () => { codeExtractSpy.mockReturnValue({ code: 'import "./../../df/devup-ui/devup-ui-5.css";\nconst x = 1;', diff --git a/packages/next-plugin/src/__tests__/plugin.test.ts b/packages/next-plugin/src/__tests__/plugin.test.ts index c88b1779..cbb45734 100644 --- a/packages/next-plugin/src/__tests__/plugin.test.ts +++ b/packages/next-plugin/src/__tests__/plugin.test.ts @@ -68,6 +68,7 @@ let exportSheetSpy: ReturnType let exportClassMapSpy: ReturnType let exportFileMapSpy: ReturnType let codeExtractSpy: ReturnType +let codeExtractWithoutSourceMapSpy: ReturnType let devupUIWebpackPluginSpy: ReturnType let startCoordinatorSpy: ReturnType @@ -108,6 +109,12 @@ beforeEach(() => { codeExtractSpy = spyOn(wasm, 'codeExtract').mockImplementation( (_path: string, contents: string) => createCodeExtractResult(contents), ) + codeExtractWithoutSourceMapSpy = spyOn( + wasm, + 'codeExtractWithoutSourceMap', + ).mockImplementation((_path: string, contents: string) => + createCodeExtractResult(contents), + ) devupUIWebpackPluginSpy = spyOn( webpackPluginModule, 'DevupUIWebpackPlugin', @@ -144,6 +151,7 @@ afterEach(() => { exportClassMapSpy.mockRestore() exportFileMapSpy.mockRestore() codeExtractSpy.mockRestore() + codeExtractWithoutSourceMapSpy.mockRestore() devupUIWebpackPluginSpy.mockRestore() startCoordinatorSpy.mockRestore() }) @@ -508,8 +516,19 @@ describe('DevupUINextPlugin', () => { expectedBaseFiles: expect.any(Array), prewarmedFiles: expect.any(Array), prewarmedOutputs: expect.any(Map), + sourceMap: false, }) }) + it('keeps source maps when Next production browser source maps are enabled', () => { + setNodeEnv('production') + process.env.TURBOPACK = '1' + + DevupUI({ productionBrowserSourceMaps: true }) + + expect(startCoordinatorSpy).toHaveBeenCalledWith( + expect.objectContaining({ sourceMap: true }), + ) + }) it('should create theme.d.ts file', async () => { process.env.TURBOPACK = '1' existsSyncSpy.mockReturnValue(true) @@ -698,8 +717,10 @@ describe('DevupUINextPlugin', () => { expectedBaseFiles: expect.any(Array), prewarmedFiles: [], prewarmedOutputs: expect.any(Map), + sourceMap: true, }) expect(codeExtractSpy).not.toHaveBeenCalled() + expect(codeExtractWithoutSourceMapSpy).not.toHaveBeenCalled() // Verify initial CSS file is written expect(writeFileSyncSpy).toHaveBeenCalledWith( @@ -736,7 +757,7 @@ describe('DevupUINextPlugin', () => { 'computeFileRoutes', ).mockReturnValue({ 'src/app/page.tsx': [0] }) const events: string[] = [] - codeExtractSpy.mockImplementation( + codeExtractWithoutSourceMapSpy.mockImplementation( (filename: string, contents: string) => { events.push(`extract:${filename}`) return createCodeExtractResult(contents) @@ -755,8 +776,8 @@ describe('DevupUINextPlugin', () => { prewarmedFiles: ['src/app/page.tsx', 'src/lazy/panel.tsx'], }), ) - expect(codeExtractSpy).toHaveBeenCalledTimes(2) - expect(codeExtractSpy).toHaveBeenCalledWith( + expect(codeExtractWithoutSourceMapSpy).toHaveBeenCalledTimes(2) + expect(codeExtractWithoutSourceMapSpy).toHaveBeenCalledWith( 'src/app/page.tsx', '{}', '@devup-ui/react', @@ -766,7 +787,7 @@ describe('DevupUINextPlugin', () => { true, expect.anything(), ) - expect(codeExtractSpy).toHaveBeenCalledWith( + expect(codeExtractWithoutSourceMapSpy).toHaveBeenCalledWith( 'src/lazy/panel.tsx', '{}', '@devup-ui/react', @@ -860,7 +881,7 @@ describe('DevupUINextPlugin', () => { ], }), ) - expect(codeExtractSpy).toHaveBeenCalledTimes(2) + expect(codeExtractWithoutSourceMapSpy).toHaveBeenCalledTimes(2) } finally { graphSpy.mockRestore() compiledSpy.mockRestore() @@ -876,8 +897,8 @@ describe('DevupUINextPlugin', () => { try { DevupUI({}, { singleCss: true }) - expect(codeExtractSpy).toHaveBeenCalledTimes(2) - expect(codeExtractSpy).toHaveBeenCalledWith( + expect(codeExtractWithoutSourceMapSpy).toHaveBeenCalledTimes(2) + expect(codeExtractWithoutSourceMapSpy).toHaveBeenCalledWith( 'src/app/card.tsx', '{}', '@devup-ui/react', diff --git a/packages/next-plugin/src/coordinator.ts b/packages/next-plugin/src/coordinator.ts index 52fce285..59027f25 100644 --- a/packages/next-plugin/src/coordinator.ts +++ b/packages/next-plugin/src/coordinator.ts @@ -5,6 +5,7 @@ import { basename, dirname, join, relative } from 'node:path' import { getFileNumByFilename } from '@devup-ui/plugin-utils' import { codeExtract, + codeExtractWithoutSourceMap, exportClassMap, exportFileMap, exportSheet, @@ -50,6 +51,8 @@ export interface CoordinatorOptions { * second WASM extraction; the shared sheet is already populated. */ prewarmedOutputs?: Map + /** Generate transform source maps. Defaults to true for existing callers. */ + sourceMap?: boolean /** * Idle threshold (ms) for the base-css `/css` wait. Defaults to 2500. * FALLBACK ONLY — used when `expectedBaseFiles` is empty (no deterministic @@ -73,13 +76,33 @@ export interface CoordinatorOptions { export interface PrewarmedOutput { code: string - css?: string cssFile?: string map?: string source: string updatedBaseStyle: boolean } +interface ExtractOutputSnapshot extends Omit { + css?: string +} + +/** Copy every WASM-backed getter once, then release its Rust allocation. */ +export function takeExtractOutput( + output: ReturnType, +): ExtractOutputSnapshot { + try { + return { + code: output.code, + css: output.css, + cssFile: output.cssFile, + map: output.map, + updatedBaseStyle: output.updatedBaseStyle, + } + } finally { + output.free() + } +} + // Latest-Wins Coalescing Serializer. // // Multiple Turbopack workers may call /extract concurrently, each producing @@ -330,6 +353,13 @@ function waitForBucket(bucket: string): Promise { export function startCoordinator(options: CoordinatorOptions): { close: () => void } { + // Next may evaluate its config more than once in the same process. Close the + // previous listener before replacing it so its HTTP server and request + // closure do not remain live for the rest of the build. + if (server) { + server.close() + server = null + } const { package: libPackage, cssDir, @@ -341,6 +371,8 @@ export function startCoordinator(options: CoordinatorOptions): { coordinatorPortFile, } = options const prewarmedOutputs = options.prewarmedOutputs ?? new Map() + const extract = + options.sourceMap === false ? codeExtractWithoutSourceMap : codeExtract idleThresholdMs = options.idleThresholdMs ?? 2500 quietMs = options.quietMs ?? 10_000 @@ -352,7 +384,7 @@ export function startCoordinator(options: CoordinatorOptions): { for (const file of options.prewarmedFiles ?? []) extractedFiles.add(file) fileNumToBucket.clear() - server = createServer(async (req, res) => { + const coordinatorServer = createServer(async (req, res) => { const url = new URL(req.url ?? '/', `http://${req.headers.host}`) if (req.method === 'GET' && url.pathname === '/health') { @@ -431,15 +463,17 @@ export function startCoordinator(options: CoordinatorOptions): { const extractStartedAt = profileStart() const result = cacheHit ? prewarmed - : codeExtract( - filename, - code, - libPackage, - relCssDir, - singleCss, - false, - true, - importAliases, + : takeExtractOutput( + extract( + filename, + code, + libPackage, + relCssDir, + singleCss, + false, + true, + importAliases, + ), ) const extractDurationMs = elapsedMs(extractStartedAt) @@ -617,8 +651,9 @@ export function startCoordinator(options: CoordinatorOptions): { res.end('Not Found') }) - server.listen(0, '127.0.0.1', () => { - const addr = server!.address() + server = coordinatorServer + coordinatorServer.listen(0, '127.0.0.1', () => { + const addr = coordinatorServer.address() if (addr && typeof addr !== 'string') { writeFileSync(coordinatorPortFile, String(addr.port), 'utf-8') } @@ -631,8 +666,8 @@ export function startCoordinator(options: CoordinatorOptions): { // `close` itself returns synchronously (it is invoked from // `process.on('exit', ...)` where awaiting is not possible). void flushPendingWrites() - if (server) { - server.close() + coordinatorServer.close() + if (server === coordinatorServer) { server = null try { unlinkSync(coordinatorPortFile) diff --git a/packages/next-plugin/src/plugin.ts b/packages/next-plugin/src/plugin.ts index 2f17a7cf..e5ab3ce8 100644 --- a/packages/next-plugin/src/plugin.ts +++ b/packages/next-plugin/src/plugin.ts @@ -21,6 +21,7 @@ import { } from '@devup-ui/plugin-utils' import { codeExtract, + codeExtractWithoutSourceMap, exportClassMap, exportFileMap, exportSheet, @@ -43,7 +44,11 @@ import { } from '@devup-ui/webpack-plugin' import { type NextConfig } from 'next' -import { type PrewarmedOutput, startCoordinator } from './coordinator' +import { + type PrewarmedOutput, + startCoordinator, + takeExtractOutput, +} from './coordinator' import { collectProductionPrewarmFiles } from './prewarm' import { elapsedMs, profileStart, reportProfile } from './profile' @@ -148,6 +153,8 @@ export function DevupUI( const atomMode = atomHoist !== undefined && Number.isFinite(atomHoist) && atomHoist > 0 const watch = process.env.NODE_ENV === 'development' + const sourceMap = watch || config.productionBrowserSourceMaps === true + const extract = sourceMap ? codeExtract : codeExtractWithoutSourceMap // Hoisted out of the try so the coordinator can receive it for per-bucket // completion. Stays `{}` if the best-effort pre-pass fails. let canonicalMap: Record = {} @@ -267,15 +274,17 @@ export function DevupUI( } const extractStartedAt = prewarmStartedAt === undefined ? undefined : performance.now() - const output = codeExtract( - filename, - source, - libPackage, - relCssDir, - singleCss, - false, - true, - importAliases as unknown as Record, + const output = takeExtractOutput( + extract( + filename, + source, + libPackage, + relCssDir, + singleCss, + false, + true, + importAliases as unknown as Record, + ), ) if (extractStartedAt !== undefined) { prewarmExtractMs += performance.now() - extractStartedAt @@ -283,7 +292,6 @@ export function DevupUI( if (singleCss) { prewarmedOutputs.set(filename, { code: output.code, - css: output.css, cssFile: output.cssFile, map: output.map, source, @@ -349,6 +357,7 @@ export function DevupUI( expectedBaseFiles, prewarmedFiles, prewarmedOutputs, + sourceMap, }) // Cleanup on exit From 9d5355a642cffc87f107369fa47c5df4957cd1f9 Mon Sep 17 00:00:00 2001 From: devfive Date: Sat, 29 Aug 2026 03:20:19 +0900 Subject: [PATCH 20/43] perf(wasm): minimize build-time module --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 26cba5dc..c9933381 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,8 +50,8 @@ use_self = "allow" used_underscore_binding = "allow" [profile.release] -# Optimize for small code size (critical for WASM binary) -opt-level = "s" +# Minimize the WASM that every build must read, compile, and retain in memory. +opt-level = "z" # Link-time optimization: enables cross-crate inlining and dead code elimination lto = true # Single codegen unit: maximizes optimization at cost of compile time From 3ed9676cd9e2ca8675c36a7aff04589a1f224185 Mon Sep 17 00:00:00 2001 From: devfive Date: Sat, 29 Aug 2026 04:08:41 +0900 Subject: [PATCH 21/43] perf(next-plugin): load a smaller extraction engine --- bindings/devup-ui-wasm/Cargo.toml | 7 +- bindings/devup-ui-wasm/package.json | 14 +- bindings/devup-ui-wasm/script.js | 1 + libs/extractor/Cargo.toml | 6 +- libs/extractor/src/lib.rs | 12 +- libs/sheet/Cargo.toml | 6 +- .../src/__tests__/coordinator.test.ts | 1 + .../src/__tests__/css-loader.test.ts | 3 +- .../next-plugin/src/__tests__/loader.test.ts | 3 +- .../next-plugin/src/__tests__/plugin.test.ts | 26 +++- .../next-plugin/src/__tests__/wasm.test.ts | 19 +++ packages/next-plugin/src/coordinator.ts | 21 +-- packages/next-plugin/src/css-loader.ts | 18 ++- packages/next-plugin/src/loader.ts | 26 ++-- packages/next-plugin/src/plugin.ts | 122 ++++++++++++------ packages/next-plugin/src/wasm.ts | 40 ++++++ 16 files changed, 242 insertions(+), 83 deletions(-) create mode 100644 packages/next-plugin/src/__tests__/wasm.test.ts create mode 100644 packages/next-plugin/src/wasm.ts diff --git a/bindings/devup-ui-wasm/Cargo.toml b/bindings/devup-ui-wasm/Cargo.toml index cd8419bc..2546871b 100644 --- a/bindings/devup-ui-wasm/Cargo.toml +++ b/bindings/devup-ui-wasm/Cargo.toml @@ -14,12 +14,13 @@ categories = ["development-tools", "wasm", "web-programming"] crate-type = ["cdylib"] [features] -default = [] +default = ["vanilla-extract"] +vanilla-extract = ["extractor/vanilla-extract", "sheet/vanilla-extract"] [dependencies] wasm-bindgen = "0.2.127" -extractor = { path = "../../libs/extractor" } -sheet = { path = "../../libs/sheet" } +extractor = { path = "../../libs/extractor", default-features = false } +sheet = { path = "../../libs/sheet", default-features = false } css = { path = "../../libs/css" } rustc-hash = "2" diff --git a/bindings/devup-ui-wasm/package.json b/bindings/devup-ui-wasm/package.json index 1643e880..c6a17d44 100644 --- a/bindings/devup-ui-wasm/package.json +++ b/bindings/devup-ui-wasm/package.json @@ -19,7 +19,7 @@ ], "version": "1.0.78", "scripts": { - "build": "wasm-pack build --target nodejs --out-dir ./pkg --out-name index && node script.js", + "build": "wasm-pack build --target nodejs --out-dir ./pkg --out-name index && wasm-pack build --target nodejs --out-dir ./pkg/lite --out-name index --no-default-features && node script.js", "test": "wasm-pack test --node" }, "publishConfig": { @@ -33,7 +33,12 @@ "pkg/index.js", "pkg/package.json", "pkg/index_bg.wasm", - "pkg/index_bg.wasm.d.ts" + "pkg/index_bg.wasm.d.ts", + "pkg/lite/index.d.ts", + "pkg/lite/index.js", + "pkg/lite/package.json", + "pkg/lite/index_bg.wasm", + "pkg/lite/index_bg.wasm.d.ts" ], "type": "module", "exports": { @@ -41,6 +46,11 @@ "types": "./pkg/index.d.ts", "import": "./pkg/index.js", "require": "./pkg/index.js" + }, + "./lite": { + "types": "./pkg/lite/index.d.ts", + "import": "./pkg/lite/index.js", + "require": "./pkg/lite/index.js" } }, "types": "./pkg/index.d.ts" diff --git a/bindings/devup-ui-wasm/script.js b/bindings/devup-ui-wasm/script.js index 27b3139c..9fafac1b 100644 --- a/bindings/devup-ui-wasm/script.js +++ b/bindings/devup-ui-wasm/script.js @@ -2,3 +2,4 @@ import { writeFileSync } from 'node:fs' // support mjs config writeFileSync('pkg/package.json', JSON.stringify({}), 'utf8') +writeFileSync('pkg/lite/package.json', JSON.stringify({}), 'utf8') diff --git a/libs/extractor/Cargo.toml b/libs/extractor/Cargo.toml index d3bf12d7..86ae3ac4 100644 --- a/libs/extractor/Cargo.toml +++ b/libs/extractor/Cargo.toml @@ -11,6 +11,10 @@ categories = ["development-tools", "wasm", "web-programming"] [lints] workspace = true +[features] +default = ["vanilla-extract"] +vanilla-extract = ["dep:boa_engine"] + [dependencies] oxc_parser = "0.146.0" oxc_syntax = "0.146.0" @@ -26,7 +30,7 @@ phf = "0.14" strum = "0.28.0" strum_macros = "0.28.0" serde_json = "1.0" -boa_engine = "0.21" +boa_engine = { version = "0.21", optional = true } rustc-hash = "2" smallvec = "1" diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs index bd785175..391f2371 100644 --- a/libs/extractor/src/lib.rs +++ b/libs/extractor/src/lib.rs @@ -11,6 +11,7 @@ mod stylex; mod tailwind; mod util_type; mod utils; +#[cfg(feature = "vanilla-extract")] mod vanilla_extract; mod visit; use crate::extract_style::extract_style_value::ExtractStyleValue; @@ -22,7 +23,9 @@ use oxc_ast_visit::VisitMut; use oxc_codegen::{Codegen, CodegenOptions}; use oxc_parser::{Parser, ParserReturn}; use oxc_span::SourceType; -use rustc_hash::{FxHashMap, FxHashSet}; +#[cfg(feature = "vanilla-extract")] +use rustc_hash::FxHashMap; +use rustc_hash::FxHashSet; use std::collections::{BTreeMap, HashMap}; use std::error::Error; use std::path::PathBuf; @@ -260,8 +263,8 @@ fn extract_with_source_map( // Step 3: Handle vanilla-extract style files (.css.ts, .css.js) // `processed_code` is Some only when vanilla-extract generation succeeded; // otherwise the untouched `transformed_code` is parsed directly (no copy). - let is_ve_file = vanilla_extract::is_vanilla_extract_file(filename); - let processed_code: Option = if is_ve_file { + #[cfg(feature = "vanilla-extract")] + let processed_code: Option = if vanilla_extract::is_vanilla_extract_file(filename) { // Use transformed code (with imports already pointing to @devup-ui/react) match vanilla_extract::execute_vanilla_extract(&transformed_code, &option.package, filename) { @@ -304,6 +307,8 @@ fn extract_with_source_map( } else { None }; + #[cfg(not(feature = "vanilla-extract"))] + let processed_code: Option = None; // For vanilla-extract files, if no styles were collected, return early if processed_code.as_deref() == Some("") { return Ok(ExtractOutput { @@ -398,6 +403,7 @@ fn resolve_css_target(filename: &str, option: &ExtractOption) -> (String, bool, /// Extract class names from generated code for specific style names /// Used for two-pass vanilla-extract processing to resolve selector references +#[cfg(feature = "vanilla-extract")] fn extract_class_map_from_code( filename: &str, partial_code: &str, diff --git a/libs/sheet/Cargo.toml b/libs/sheet/Cargo.toml index 4ca2a9c2..724c2751 100644 --- a/libs/sheet/Cargo.toml +++ b/libs/sheet/Cargo.toml @@ -11,12 +11,16 @@ categories = ["development-tools", "wasm", "web-programming"] [lints] workspace = true +[features] +default = ["vanilla-extract"] +vanilla-extract = ["extractor/vanilla-extract"] + [dependencies] css = { path = "../css" } serde = { version = "1.0.229", features = ["derive"] } serde_json = "1.0.151" regex-lite = "0.1" -extractor = { path = "../extractor" } +extractor = { path = "../extractor", default-features = false } rustc-hash = "2" [dev-dependencies] diff --git a/packages/next-plugin/src/__tests__/coordinator.test.ts b/packages/next-plugin/src/__tests__/coordinator.test.ts index 93d9e2d0..dda073b4 100644 --- a/packages/next-plugin/src/__tests__/coordinator.test.ts +++ b/packages/next-plugin/src/__tests__/coordinator.test.ts @@ -35,6 +35,7 @@ function makeOptions( overrides: Partial = {}, ): CoordinatorOptions { return { + wasm, package: '@devup-ui/react', cssDir: join(tmpDir, 'css'), singleCss: false, diff --git a/packages/next-plugin/src/__tests__/css-loader.test.ts b/packages/next-plugin/src/__tests__/css-loader.test.ts index 98d9e95d..1ff6f718 100644 --- a/packages/next-plugin/src/__tests__/css-loader.test.ts +++ b/packages/next-plugin/src/__tests__/css-loader.test.ts @@ -13,7 +13,7 @@ import { spyOn, } from 'bun:test' -import devupUICssLoader, { resetInit } from '../css-loader' +import devupUICssLoader, { resetInit, setWasmForTesting } from '../css-loader' type CssLoaderThis = ThisParameterType @@ -45,6 +45,7 @@ beforeAll(() => { importFileMapSpy = spyOn(wasm, 'importFileMap').mockReturnValue(undefined) existsSyncSpy = spyOn(fs, 'existsSync').mockReturnValue(false) readFileSyncSpy = spyOn(fs, 'readFileSync').mockReturnValue('{}') + setWasmForTesting(wasm) }) afterEach(() => { diff --git a/packages/next-plugin/src/__tests__/loader.test.ts b/packages/next-plugin/src/__tests__/loader.test.ts index 551111a0..c7f33b49 100644 --- a/packages/next-plugin/src/__tests__/loader.test.ts +++ b/packages/next-plugin/src/__tests__/loader.test.ts @@ -15,7 +15,7 @@ import { } from 'bun:test' import type { DevupUILoaderOptions } from '../loader' -import devupUILoader, { resetInit } from '../loader' +import devupUILoader, { resetInit, setWasmForTesting } from '../loader' type LoaderThis = ThisParameterType @@ -68,6 +68,7 @@ beforeEach(() => { importFileMapSpy = spyOn(wasm, 'importFileMap').mockImplementation(() => {}) importSheetSpy = spyOn(wasm, 'importSheet').mockImplementation(() => {}) registerThemeSpy = spyOn(wasm, 'registerTheme').mockImplementation(() => {}) + setWasmForTesting(wasm) dateNowSpy = spyOn(Date, 'now').mockReturnValue(0) }) diff --git a/packages/next-plugin/src/__tests__/plugin.test.ts b/packages/next-plugin/src/__tests__/plugin.test.ts index cbb45734..3230d9d0 100644 --- a/packages/next-plugin/src/__tests__/plugin.test.ts +++ b/packages/next-plugin/src/__tests__/plugin.test.ts @@ -1,6 +1,7 @@ import * as fs from 'node:fs' import { join, resolve } from 'node:path' +import type { StaticImportGraph } from '@devup-ui/plugin-utils' import * as importGraphModule from '@devup-ui/plugin-utils' import * as wasm from '@devup-ui/wasm' import * as webpackPluginModule from '@devup-ui/webpack-plugin' @@ -15,7 +16,8 @@ import { } from 'bun:test' import * as coordinatorModule from '../coordinator' -import { DevupUI } from '../plugin' +import { DevupUI, selectWasmVariant } from '../plugin' +import { setWasmForTesting } from '../wasm' type CodeExtractResult = ReturnType type NextWebpackConfig = Parameters< @@ -123,6 +125,7 @@ beforeEach(() => { coordinatorModule, 'startCoordinator', ).mockReturnValue({ close: mock() as () => void }) + setWasmForTesting(wasm) originalEnv = { ...process.env } originalFetch = global.fetch @@ -131,6 +134,7 @@ beforeEach(() => { }) afterEach(() => { + setWasmForTesting(undefined) process.env = originalEnv global.fetch = originalFetch process.debugPort = originalDebugPort @@ -157,6 +161,24 @@ afterEach(() => { }) describe('DevupUINextPlugin', () => { + it('selects the lite engine only when the graph has no vanilla-extract file', () => { + expect(selectWasmVariant(undefined)).toBe('full') + expect( + selectWasmVariant({ files: ['src/page.tsx'] } as StaticImportGraph), + ).toBe('lite') + expect( + selectWasmVariant({ files: ['src/theme.css.ts'] } as StaticImportGraph), + ).toBe('full') + expect( + selectWasmVariant({ files: ['src/theme.css.js'] } as StaticImportGraph), + ).toBe('full') + expect( + selectWasmVariant({ files: ['src/page.tsx'] } as StaticImportGraph, [ + 'node_modules/design-system/theme.css.ts', + ]), + ).toBe('full') + }) + describe('webpack', () => { it('should apply webpack plugin', async () => { const ret = DevupUI({}) @@ -500,6 +522,7 @@ describe('DevupUINextPlugin', () => { }, }) expect(startCoordinatorSpy).toHaveBeenCalledWith({ + wasm, package: '@devup-ui/react', cssDir: resolve('df', 'devup-ui'), singleCss: false, @@ -701,6 +724,7 @@ describe('DevupUINextPlugin', () => { // Verify coordinator was started with correct options expect(startCoordinatorSpy).toHaveBeenCalledWith({ + wasm, package: '@devup-ui/react', cssDir: resolve('df', 'devup-ui'), singleCss: false, diff --git a/packages/next-plugin/src/__tests__/wasm.test.ts b/packages/next-plugin/src/__tests__/wasm.test.ts new file mode 100644 index 00000000..fca2dda4 --- /dev/null +++ b/packages/next-plugin/src/__tests__/wasm.test.ts @@ -0,0 +1,19 @@ +import * as wasm from '@devup-ui/wasm' +import { afterEach, describe, expect, it } from 'bun:test' + +import { loadWasm, setWasmForTesting } from '../wasm' + +afterEach(() => setWasmForTesting(undefined)) + +describe('WASM selection', () => { + it('uses an injected namespace in tests', () => { + setWasmForTesting(wasm) + expect(loadWasm(true)).toBe(wasm) + expect(loadWasm(false)).toBe(wasm) + }) + + it('loads the full and lite package exports', () => { + expect(typeof loadWasm(false).codeExtract).toBe('function') + expect(typeof loadWasm(true).codeExtract).toBe('function') + }) +}) diff --git a/packages/next-plugin/src/coordinator.ts b/packages/next-plugin/src/coordinator.ts index 59027f25..f6d20c5b 100644 --- a/packages/next-plugin/src/coordinator.ts +++ b/packages/next-plugin/src/coordinator.ts @@ -3,18 +3,12 @@ import { createServer, type IncomingMessage, type Server } from 'node:http' import { basename, dirname, join, relative } from 'node:path' import { getFileNumByFilename } from '@devup-ui/plugin-utils' -import { - codeExtract, - codeExtractWithoutSourceMap, - exportClassMap, - exportFileMap, - exportSheet, - getCss, -} from '@devup-ui/wasm' import { elapsedMs, profileStart, reportProfile } from './profile' +import type { DevupWasm } from './wasm' export interface CoordinatorOptions { + wasm: DevupWasm package: string cssDir: string singleCss: boolean @@ -88,7 +82,7 @@ interface ExtractOutputSnapshot extends Omit { /** Copy every WASM-backed getter once, then release its Rust allocation. */ export function takeExtractOutput( - output: ReturnType, + output: ReturnType, ): ExtractOutputSnapshot { try { return { @@ -361,6 +355,7 @@ export function startCoordinator(options: CoordinatorOptions): { server = null } const { + wasm, package: libPackage, cssDir, singleCss, @@ -370,6 +365,14 @@ export function startCoordinator(options: CoordinatorOptions): { importAliases, coordinatorPortFile, } = options + const { + codeExtract, + codeExtractWithoutSourceMap, + exportClassMap, + exportFileMap, + exportSheet, + getCss, + } = wasm const prewarmedOutputs = options.prewarmedOutputs ?? new Map() const extract = options.sourceMap === false ? codeExtractWithoutSourceMap : codeExtract diff --git a/packages/next-plugin/src/css-loader.ts b/packages/next-plugin/src/css-loader.ts index c928e277..a404c277 100644 --- a/packages/next-plugin/src/css-loader.ts +++ b/packages/next-plugin/src/css-loader.ts @@ -2,15 +2,10 @@ import { existsSync, readFileSync } from 'node:fs' import { Agent, request } from 'node:http' import { getFileNumByFilename } from '@devup-ui/plugin-utils' -import { - getCss, - importClassMap, - importFileMap, - importSheet, - registerTheme, -} from '@devup-ui/wasm' import type { RawLoaderDefinitionFunction } from 'webpack' +import { loadWasm } from './wasm' + export interface DevupUICssLoaderOptions { // turbo watch: boolean @@ -124,6 +119,13 @@ const devupUICssLoader: RawLoaderDefinitionFunction = return } + const { + getCss, + importClassMap, + importFileMap, + importSheet, + registerTheme, + } = loadWasm(false) if (!init) { init = true if (watch) { @@ -161,3 +163,5 @@ export const resetInit = () => { init = false cachedPort = null } + +export { setWasmForTesting } from './wasm' diff --git a/packages/next-plugin/src/loader.ts b/packages/next-plugin/src/loader.ts index 1110699b..de9f1515 100644 --- a/packages/next-plugin/src/loader.ts +++ b/packages/next-plugin/src/loader.ts @@ -3,19 +3,10 @@ import { writeFile } from 'node:fs/promises' import { Agent, request } from 'node:http' import { basename, dirname, join, relative } from 'node:path' -import { - codeExtract, - exportClassMap, - exportFileMap, - exportSheet, - getCss, - importClassMap, - importFileMap, - importSheet, - registerTheme, -} from '@devup-ui/wasm' import type { RawLoaderDefinitionFunction } from 'webpack' +import { loadWasm } from './wasm' + export interface DevupUILoaderOptions { package: string cssDir: string @@ -185,6 +176,17 @@ const devupUILoader: RawLoaderDefinitionFunction = } // Non-coordinator mode: local WASM extraction + const { + codeExtract, + exportClassMap, + exportFileMap, + exportSheet, + getCss, + importClassMap, + importFileMap, + importSheet, + registerTheme, + } = loadWasm(false) const promises: Promise[] = [] if (!init) { init = true @@ -268,3 +270,5 @@ export const resetInit = () => { init = false cachedPorts.clear() } + +export { setWasmForTesting } from './wasm' diff --git a/packages/next-plugin/src/plugin.ts b/packages/next-plugin/src/plugin.ts index e5ab3ce8..afa56c65 100644 --- a/packages/next-plugin/src/plugin.ts +++ b/packages/next-plugin/src/plugin.ts @@ -19,25 +19,6 @@ import { planAtomHoist, type StaticImportGraph, } from '@devup-ui/plugin-utils' -import { - codeExtract, - codeExtractWithoutSourceMap, - exportClassMap, - exportFileMap, - exportSheet, - getCss, - getDefaultTheme, - getThemeInterface, - importCanonicalMap, - importClassMap, - importFileMap, - importFileRoutes, - importSheet, - registerShorthands, - registerTheme, - setAtomHoist, - setPrefix, -} from '@devup-ui/wasm' import { DevupUIWebpackPlugin, type DevupUIWebpackPluginOptions, @@ -51,12 +32,23 @@ import { } from './coordinator' import { collectProductionPrewarmFiles } from './prewarm' import { elapsedMs, profileStart, reportProfile } from './profile' +import { loadWasm } from './wasm' type DevupUiNextPluginOptions = Omit< Partial, 'watch' > +export function selectWasmVariant( + graph: StaticImportGraph | undefined, + candidateFiles: string[] = graph?.files ?? [], +): 'lite' | 'full' { + return graph && + !candidateFiles.some((filename) => /\.css\.(?:ts|js)$/.test(filename)) + ? 'lite' + : 'full' +} + /** * Devup UI Next Plugin * @param config @@ -88,14 +80,7 @@ export function DevupUI( importAliases: userImportAliases, } = options - registerShorthands(shorthands ?? {}) - - if (prefix) { - setPrefix(prefix) - } - const importAliases = mergeImportAliases(userImportAliases) - const sheetFile = join(distDir, 'sheet.json') const classMapFile = join(distDir, 'classMap.json') const fileMapFile = join(distDir, 'fileMap.json') @@ -110,6 +95,62 @@ export function DevupUI( recursive: true, }) if (!existsSync(gitignoreFile)) writeFileSync(gitignoreFile, '*') + + // Boa is only needed to execute vanilla-extract-style `.css.ts`/`.css.js` + // modules. Build the graph before touching WASM so ordinary applications + // instantiate the much smaller engine, while vanilla-extract users retain + // the full evaluator automatically. If graph discovery fails, fail safe to + // the full engine. + const graphStartedAt = profileStart() + const srcDir = resolve(process.cwd(), 'src') + const tsconfigPath = resolve(process.cwd(), 'tsconfig.json') + let staticGraph: StaticImportGraph | undefined + try { + staticGraph = buildStaticImportGraph(srcDir, tsconfigPath) + } catch { + // The mapping pass below reports the graph failure and keeps its legacy + // best-effort behavior. + } + const candidateCollectStartedAt = + graphStartedAt === undefined ? undefined : performance.now() + const wasmCandidateFiles = staticGraph + ? collectProductionPrewarmFiles({ + cwd: process.cwd(), + graph: staticGraph, + expectedBaseFiles: [], + libPackage, + include, + }) + : [] + const candidateCollectMs = elapsedMs(candidateCollectStartedAt) + const wasmVariant = selectWasmVariant(staticGraph, wasmCandidateFiles) + const wasm = loadWasm(wasmVariant === 'lite') + const { + codeExtract, + codeExtractWithoutSourceMap, + exportClassMap, + exportFileMap, + exportSheet, + getCss, + getDefaultTheme, + getThemeInterface, + importCanonicalMap, + importClassMap, + importFileMap, + importFileRoutes, + importSheet, + registerShorthands, + registerTheme, + setAtomHoist, + setPrefix, + } = wasm + + registerShorthands(shorthands ?? {}) + + if (prefix) { + setPrefix(prefix) + } + // Import previous session state to handle Turbopack persistent cache. // When the dev server restarts, Turbopack may skip re-running loaders for // unchanged files. Without importing previous state, the coordinator's WASM @@ -162,15 +203,11 @@ export function DevupUI( // deterministic base-css completion signal handed to the coordinator. Stays // `[]` (idle fallback) when no routes are detected or the pre-pass fails. let expectedBaseFiles: string[] = [] - let staticGraph: StaticImportGraph | undefined - const graphStartedAt = profileStart() try { - const srcDir = resolve(process.cwd(), 'src') - const tsconfigPath = resolve(process.cwd(), 'tsconfig.json') + if (!staticGraph) throw new Error('Static import graph unavailable') const cwd = process.cwd() // One scan+parse of the source tree, shared by all three consumers below. - const graph = buildStaticImportGraph(srcDir, tsconfigPath) - staticGraph = graph + const graph = staticGraph // Atom hoisting owns the shared-chunk decision, so collapse runs WITHOUT // the file-level @global hoist (DEVUP_HOIST_V) in atom mode. const hoistV = atomMode @@ -223,6 +260,7 @@ export function DevupUI( durationMs: elapsedMs(graphStartedAt), files: staticGraph.files.length, expectedBaseFiles: expectedBaseFiles.length, + wasmVariant, }) } catch { // Pre-pass is best-effort; on failure canonical() is the identity (no @@ -249,16 +287,12 @@ export function DevupUI( let prewarmReadMs = 0 let prewarmSourceBytes = 0 const cwd = process.cwd() - const collectStartedAt = - prewarmStartedAt === undefined ? undefined : performance.now() - const prewarmFiles = collectProductionPrewarmFiles({ - cwd, - graph: staticGraph, - expectedBaseFiles, - libPackage, - include, - }) - const collectDurationMs = elapsedMs(collectStartedAt) + // The same complete candidate set selected the WASM variant above. Reuse + // it here instead of resolving source/package entries a second time, + // while retaining any compiled-file fallback supplied by the graph pass. + const prewarmFiles = [ + ...new Set([...wasmCandidateFiles, ...expectedBaseFiles]), + ].sort() for (const filename of prewarmFiles) { const resourcePath = resolve(cwd, filename) const relCssDir = `./${relative( @@ -301,7 +335,7 @@ export function DevupUI( prewarmedFiles.push(filename) } reportProfile('next.prewarm', { - collectMs: collectDurationMs, + collectMs: candidateCollectMs, durationMs: elapsedMs(prewarmStartedAt), extractMs: prewarmStartedAt === undefined @@ -345,6 +379,7 @@ export function DevupUI( } const coordinator = startCoordinator({ + wasm, package: libPackage, cssDir, singleCss, @@ -480,6 +515,7 @@ export function DevupUI( durationMs: elapsedMs(pluginStartedAt), prewarmedFiles: prewarmedFiles.length, singleCss, + wasmVariant, watch, }) return config diff --git a/packages/next-plugin/src/wasm.ts b/packages/next-plugin/src/wasm.ts new file mode 100644 index 00000000..c24cbe4f --- /dev/null +++ b/packages/next-plugin/src/wasm.ts @@ -0,0 +1,40 @@ +import { existsSync } from 'node:fs' +import { createRequire } from 'node:module' +import { join } from 'node:path' + +export type DevupWasm = typeof import('@devup-ui/wasm') + +let wasmForTesting: DevupWasm | undefined +let fullWasm: DevupWasm | undefined +let liteWasm: DevupWasm | undefined + +function requireFromPlugin(specifier: string): DevupWasm { + const installedPackage = join( + process.cwd(), + 'node_modules/@devup-ui/next-plugin/package.json', + ) + const workspacePackage = join( + process.cwd(), + 'packages/next-plugin/package.json', + ) + const requireBase = existsSync(installedPackage) + ? installedPackage + : existsSync(workspacePackage) + ? workspacePackage + : join(process.cwd(), 'package.json') + return createRequire(requireBase)(specifier) as DevupWasm +} + +/** Load exactly one extraction engine for the lifetime of a Next config. */ +export function loadWasm(lite: boolean): DevupWasm { + if (wasmForTesting) return wasmForTesting + if (lite) { + return (liteWasm ??= requireFromPlugin('@devup-ui/wasm/lite')) + } + return (fullWasm ??= requireFromPlugin('@devup-ui/wasm')) +} + +/** @internal Inject the WASM namespace for unit tests. */ +export function setWasmForTesting(value: DevupWasm | undefined): void { + wasmForTesting = value +} From 21fd3cc17f4ce563370715cd58af2a830d774817 Mon Sep 17 00:00:00 2001 From: devfive Date: Sat, 29 Aug 2026 04:23:07 +0900 Subject: [PATCH 22/43] perf(next-plugin): defer webpack runtime in Turbopack --- .../next-plugin/src/__tests__/plugin.test.ts | 4 ++- .../next-plugin/src/__tests__/wasm.test.ts | 19 ++++++++++++-- packages/next-plugin/src/plugin.ts | 8 +++--- packages/next-plugin/src/wasm.ts | 26 ++++++++++++++++--- 4 files changed, 45 insertions(+), 12 deletions(-) diff --git a/packages/next-plugin/src/__tests__/plugin.test.ts b/packages/next-plugin/src/__tests__/plugin.test.ts index 3230d9d0..e823f2a6 100644 --- a/packages/next-plugin/src/__tests__/plugin.test.ts +++ b/packages/next-plugin/src/__tests__/plugin.test.ts @@ -17,7 +17,7 @@ import { import * as coordinatorModule from '../coordinator' import { DevupUI, selectWasmVariant } from '../plugin' -import { setWasmForTesting } from '../wasm' +import { setWasmForTesting, setWebpackPluginForTesting } from '../wasm' type CodeExtractResult = ReturnType type NextWebpackConfig = Parameters< @@ -126,6 +126,7 @@ beforeEach(() => { 'startCoordinator', ).mockReturnValue({ close: mock() as () => void }) setWasmForTesting(wasm) + setWebpackPluginForTesting(webpackPluginModule) originalEnv = { ...process.env } originalFetch = global.fetch @@ -135,6 +136,7 @@ beforeEach(() => { afterEach(() => { setWasmForTesting(undefined) + setWebpackPluginForTesting(undefined) process.env = originalEnv global.fetch = originalFetch process.debugPort = originalDebugPort diff --git a/packages/next-plugin/src/__tests__/wasm.test.ts b/packages/next-plugin/src/__tests__/wasm.test.ts index fca2dda4..1f5d34ee 100644 --- a/packages/next-plugin/src/__tests__/wasm.test.ts +++ b/packages/next-plugin/src/__tests__/wasm.test.ts @@ -1,9 +1,18 @@ import * as wasm from '@devup-ui/wasm' +import * as webpackPlugin from '@devup-ui/webpack-plugin' import { afterEach, describe, expect, it } from 'bun:test' -import { loadWasm, setWasmForTesting } from '../wasm' +import { + loadWasm, + loadWebpackPlugin, + setWasmForTesting, + setWebpackPluginForTesting, +} from '../wasm' -afterEach(() => setWasmForTesting(undefined)) +afterEach(() => { + setWasmForTesting(undefined) + setWebpackPluginForTesting(undefined) +}) describe('WASM selection', () => { it('uses an injected namespace in tests', () => { @@ -16,4 +25,10 @@ describe('WASM selection', () => { expect(typeof loadWasm(false).codeExtract).toBe('function') expect(typeof loadWasm(true).codeExtract).toBe('function') }) + + it('loads or injects the Webpack plugin without a static dependency', () => { + expect(typeof loadWebpackPlugin().DevupUIWebpackPlugin).toBe('function') + setWebpackPluginForTesting(webpackPlugin) + expect(loadWebpackPlugin()).toBe(webpackPlugin) + }) }) diff --git a/packages/next-plugin/src/plugin.ts b/packages/next-plugin/src/plugin.ts index afa56c65..a1d9b779 100644 --- a/packages/next-plugin/src/plugin.ts +++ b/packages/next-plugin/src/plugin.ts @@ -19,10 +19,7 @@ import { planAtomHoist, type StaticImportGraph, } from '@devup-ui/plugin-utils' -import { - DevupUIWebpackPlugin, - type DevupUIWebpackPluginOptions, -} from '@devup-ui/webpack-plugin' +import type { DevupUIWebpackPluginOptions } from '@devup-ui/webpack-plugin' import { type NextConfig } from 'next' import { @@ -32,7 +29,7 @@ import { } from './coordinator' import { collectProductionPrewarmFiles } from './prewarm' import { elapsedMs, profileStart, reportProfile } from './profile' -import { loadWasm } from './wasm' +import { loadWasm, loadWebpackPlugin } from './wasm' type DevupUiNextPluginOptions = Omit< Partial, @@ -523,6 +520,7 @@ export function DevupUI( const { webpack } = config config.webpack = (config, _options) => { + const { DevupUIWebpackPlugin } = loadWebpackPlugin() options.cssDir ??= resolve( _options.dev ? (options.distDir ?? 'df') : '.next/cache', `devup-ui_${_options.buildId}`, diff --git a/packages/next-plugin/src/wasm.ts b/packages/next-plugin/src/wasm.ts index c24cbe4f..c7d3e7cf 100644 --- a/packages/next-plugin/src/wasm.ts +++ b/packages/next-plugin/src/wasm.ts @@ -3,12 +3,15 @@ import { createRequire } from 'node:module' import { join } from 'node:path' export type DevupWasm = typeof import('@devup-ui/wasm') +export type DevupWebpackPlugin = typeof import('@devup-ui/webpack-plugin') let wasmForTesting: DevupWasm | undefined +let webpackPluginForTesting: DevupWebpackPlugin | undefined let fullWasm: DevupWasm | undefined let liteWasm: DevupWasm | undefined +let webpackPlugin: DevupWebpackPlugin | undefined -function requireFromPlugin(specifier: string): DevupWasm { +function requireFromPlugin(specifier: string): T { const installedPackage = join( process.cwd(), 'node_modules/@devup-ui/next-plugin/package.json', @@ -22,19 +25,34 @@ function requireFromPlugin(specifier: string): DevupWasm { : existsSync(workspacePackage) ? workspacePackage : join(process.cwd(), 'package.json') - return createRequire(requireBase)(specifier) as DevupWasm + return createRequire(requireBase)(specifier) as T } /** Load exactly one extraction engine for the lifetime of a Next config. */ export function loadWasm(lite: boolean): DevupWasm { if (wasmForTesting) return wasmForTesting if (lite) { - return (liteWasm ??= requireFromPlugin('@devup-ui/wasm/lite')) + return (liteWasm ??= requireFromPlugin('@devup-ui/wasm/lite')) } - return (fullWasm ??= requireFromPlugin('@devup-ui/wasm')) + return (fullWasm ??= requireFromPlugin('@devup-ui/wasm')) +} + +/** Keep the Webpack adapter (and its full WASM) out of Turbopack startup. */ +export function loadWebpackPlugin(): DevupWebpackPlugin { + if (webpackPluginForTesting) return webpackPluginForTesting + return (webpackPlugin ??= requireFromPlugin( + '@devup-ui/webpack-plugin', + )) } /** @internal Inject the WASM namespace for unit tests. */ export function setWasmForTesting(value: DevupWasm | undefined): void { wasmForTesting = value } + +/** @internal Inject the Webpack namespace for unit tests. */ +export function setWebpackPluginForTesting( + value: DevupWebpackPlugin | undefined, +): void { + webpackPluginForTesting = value +} From 0b1fafd61900b25a34ad205d623d0b3f86e440a0 Mon Sep 17 00:00:00 2001 From: devfive Date: Sat, 29 Aug 2026 12:01:52 +0900 Subject: [PATCH 23/43] perf(next-plugin): reuse full Turbopack prewarm output --- .../src/__tests__/coordinator.test.ts | 57 +++++++++++++++++++ .../next-plugin/src/__tests__/plugin.test.ts | 13 +++++ packages/next-plugin/src/coordinator.ts | 18 +++--- packages/next-plugin/src/plugin.ts | 16 +++--- 4 files changed, 88 insertions(+), 16 deletions(-) diff --git a/packages/next-plugin/src/__tests__/coordinator.test.ts b/packages/next-plugin/src/__tests__/coordinator.test.ts index dda073b4..62dd85c8 100644 --- a/packages/next-plugin/src/__tests__/coordinator.test.ts +++ b/packages/next-plugin/src/__tests__/coordinator.test.ts @@ -262,6 +262,63 @@ describe('coordinator', () => { coordinator.close() }) + it('reuses and rewrites byte-identical per-file prewarm output', async () => { + const source = 'const x = ' + getCssSpy.mockReturnValue('prewarmed bucket css') + const coordinator = startCoordinator( + makeOptions({ + prewarmedFiles: ['src/App.tsx'], + prewarmedOutputs: new Map([ + [ + 'src/App.tsx', + { + code: 'import "./df/devup-ui-79.css";\nconst x = 1', + cssFile: 'devup-ui-79.css', + map: '{"version":3}', + source, + updatedBaseStyle: false, + }, + ], + ]), + }), + ) + + await new Promise((resolve) => setTimeout(resolve, 100)) + const port = parseInt( + (writeFileSyncSpy.mock.calls[0] as [string, string])[1], + ) + const res = await httpRequest( + port, + 'POST', + '/extract', + JSON.stringify({ + filename: 'src/App.tsx', + code: source, + resourcePath: join(process.cwd(), 'src', 'App.tsx'), + }), + ) + + expect(res.status).toBe(200) + expect(JSON.parse(res.body)).toMatchObject({ + code: 'import "./df/devup-ui.css?fileNum=79";\nconst x = 1', + cssFile: 'devup-ui-79.css', + map: '{"version":3}', + updatedBaseStyle: false, + }) + expect(codeExtractSpy).not.toHaveBeenCalled() + expect(writeFileSpy).not.toHaveBeenCalled() + + const css = await httpRequest( + port, + 'GET', + '/css?fileNum=79&importMainCss=true&waitForIdle=true', + ) + expect(css).toEqual({ status: 200, body: 'prewarmed bucket css' }) + expect(getCssSpy).toHaveBeenCalledWith(79, true) + + coordinator.close() + }) + it('profiles both base CSS serialization paths separately from writes', async () => { const originalProfile = process.env.DEVUP_UI_PROFILE process.env.DEVUP_UI_PROFILE = '1' diff --git a/packages/next-plugin/src/__tests__/plugin.test.ts b/packages/next-plugin/src/__tests__/plugin.test.ts index e823f2a6..b3701830 100644 --- a/packages/next-plugin/src/__tests__/plugin.test.ts +++ b/packages/next-plugin/src/__tests__/plugin.test.ts @@ -828,6 +828,19 @@ describe('DevupUINextPlugin', () => { 'extract:src/lazy/panel.tsx', 'startCoordinator', ]) + const coordinatorOptions = startCoordinatorSpy.mock.calls[0]?.[0] + expect(coordinatorOptions?.prewarmedOutputs).toEqual( + new Map([ + [ + 'src/app/page.tsx', + expect.objectContaining({ code: '{}', source: '{}' }), + ], + [ + 'src/lazy/panel.tsx', + expect.objectContaining({ code: '{}', source: '{}' }), + ], + ]), + ) const profiles: Record[] = profileSpy.mock.calls .map(([value]) => value) .filter( diff --git a/packages/next-plugin/src/coordinator.ts b/packages/next-plugin/src/coordinator.ts index f6d20c5b..a6f6b437 100644 --- a/packages/next-plugin/src/coordinator.ts +++ b/packages/next-plugin/src/coordinator.ts @@ -40,9 +40,9 @@ export interface CoordinatorOptions { */ prewarmedFiles?: string[] /** - * Production `singleCss` outputs extracted before Turbopack starts. A loader - * that receives byte-identical source can return this result without a - * second WASM extraction; the shared sheet is already populated. + * Production outputs extracted before Turbopack starts. A loader that + * receives byte-identical source can return this result without a second + * WASM extraction; the shared sheet is already populated. */ prewarmedOutputs?: Map /** Generate transform source maps. Defaults to true for existing callers. */ @@ -457,11 +457,11 @@ export function startCoordinator(options: CoordinatorOptions): { if (!relCssDir.startsWith('./')) relCssDir = `./${relCssDir}` // The production prewarm exists to make the CSS snapshot complete - // before Turbopack requests it. In single-CSS mode the generated CSS - // is already in that snapshot, so re-running WASM here is pure work. + // before Turbopack requests it. The generated CSS is already in that + // snapshot, so re-running WASM here is pure work in either CSS mode. // Require exact source equality because Turbopack may hand a loader // code modified by an earlier transform. - const prewarmed = singleCss ? prewarmedOutputs.get(filename) : undefined + const prewarmed = prewarmedOutputs.get(filename) const cacheHit = prewarmed?.source === code const extractStartedAt = profileStart() const result = cacheHit @@ -516,13 +516,17 @@ export function startCoordinator(options: CoordinatorOptions): { promises.push(safeWrite(join(cssDir, 'devup-ui.css'), css)) } - if (!cacheHit && result.cssFile) { + if (result.cssFile) { const fileNum = getFileNumByFilename(result.cssFile) if (fileNum != null) { // Record this bucket's fileNum -> canonical bucket path so /css can // wait for the bucket's members before serving it. fileNumToBucket.set(fileNum, canonicalMapRef[filename] ?? filename) } + } + + if (!cacheHit && result.cssFile) { + const fileNum = getFileNumByFilename(result.cssFile) const cssStartedAt = snapshotStartedAt === undefined ? undefined : performance.now() const css = getCss(fileNum, true) diff --git a/packages/next-plugin/src/plugin.ts b/packages/next-plugin/src/plugin.ts index a1d9b779..4bef293c 100644 --- a/packages/next-plugin/src/plugin.ts +++ b/packages/next-plugin/src/plugin.ts @@ -320,15 +320,13 @@ export function DevupUI( if (extractStartedAt !== undefined) { prewarmExtractMs += performance.now() - extractStartedAt } - if (singleCss) { - prewarmedOutputs.set(filename, { - code: output.code, - cssFile: output.cssFile, - map: output.map, - source, - updatedBaseStyle: output.updatedBaseStyle, - }) - } + prewarmedOutputs.set(filename, { + code: output.code, + cssFile: output.cssFile, + map: output.map, + source, + updatedBaseStyle: output.updatedBaseStyle, + }) prewarmedFiles.push(filename) } reportProfile('next.prewarm', { From 4b94b16137b96de2b6789476da008723c4b3b6e8 Mon Sep 17 00:00:00 2001 From: devfive Date: Sat, 29 Aug 2026 12:12:48 +0900 Subject: [PATCH 24/43] bench: compare full Turbopack builds fairly --- benchmark.js | 26 ++++++++++++------- .../package.json | 2 +- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/benchmark.js b/benchmark.js index 708d3714..a212007c 100644 --- a/benchmark.js +++ b/benchmark.js @@ -5,7 +5,7 @@ import { execSync } from 'child_process' function clearBuildFile(dir) { const base = join('./benchmark', dir) - for (const output of ['.next', 'dist', 'df']) { + for (const output of ['.next', 'dist', 'df', 'tsconfig.tsbuildinfo']) { const target = join(base, output) if (existsSync(target)) rmSync(target, { recursive: true, force: true }) } @@ -75,12 +75,11 @@ let result = [] const turboSamples = new Map([ ['tailwind-turbo', []], ['devup-ui-single-turbo', []], + ['vanilla-extract-devup-ui', []], ]) function record(target) { const sample = benchmark(target) - const samples = turboSamples.get(target) - if (samples) samples.push(sample.duration) result.push(sample.result) } @@ -104,12 +103,21 @@ record('vinext-devup-ui') // Multi-component app exercising single-importer collapse (atom dedup). record('devup-ui-collapse') -// A single fixed-order result on a shared CI runner is too noisy for the two -// Turbopack builds we compare directly. Run six cold samples in alternating -// order so each target runs first three times, then report their medians. -const turboTargets = ['tailwind-turbo', 'devup-ui-single-turbo'] -for (let sample = 1; sample < 6; sample++) { - const order = sample % 2 === 0 ? turboTargets : turboTargets.toReversed() +// A single fixed-order result on a shared CI runner is too noisy for direct +// comparisons. Run six cold samples in a repeated Latin-square order so each +// target occupies each position twice, then report their medians. The full +// Devup target contains `.css.ts`; the single target exercises the lite WASM. +const turboTargets = [ + 'tailwind-turbo', + 'devup-ui-single-turbo', + 'vanilla-extract-devup-ui', +] +for (let sample = 0; sample < 6; sample++) { + const offset = sample % turboTargets.length + const order = [ + ...turboTargets.slice(offset), + ...turboTargets.slice(0, offset), + ] for (const target of order) { turboSamples.get(target).push(benchmark(target).duration) } diff --git a/benchmark/next-vanilla-extract-devup-ui/package.json b/benchmark/next-vanilla-extract-devup-ui/package.json index 49b232e9..0a71b8cd 100644 --- a/benchmark/next-vanilla-extract-devup-ui/package.json +++ b/benchmark/next-vanilla-extract-devup-ui/package.json @@ -5,7 +5,7 @@ "private": true, "scripts": { "dev": "next dev", - "build": "next build --webpack", + "build": "next build", "start": "next start", "lint": "next lint" }, From 4858b339c8fcb68ee7c172174edb6487b1715a8f Mon Sep 17 00:00:00 2001 From: devfive Date: Sat, 29 Aug 2026 12:39:09 +0900 Subject: [PATCH 25/43] perf(next-plugin): bypass Boa for static style modules --- benchmark.js | 4 +- .../src/__tests__/coordinator.test.ts | 47 +++++ .../next-plugin/src/__tests__/plugin.test.ts | 101 +++++++++ .../src/__tests__/static-vanilla.test.ts | 77 +++++++ packages/next-plugin/src/coordinator.ts | 8 +- packages/next-plugin/src/plugin.ts | 42 +++- packages/next-plugin/src/static-vanilla.ts | 194 ++++++++++++++++++ 7 files changed, 464 insertions(+), 9 deletions(-) create mode 100644 packages/next-plugin/src/__tests__/static-vanilla.test.ts create mode 100644 packages/next-plugin/src/static-vanilla.ts diff --git a/benchmark.js b/benchmark.js index a212007c..0f266988 100644 --- a/benchmark.js +++ b/benchmark.js @@ -105,8 +105,8 @@ record('devup-ui-collapse') // A single fixed-order result on a shared CI runner is too noisy for direct // comparisons. Run six cold samples in a repeated Latin-square order so each -// target occupies each position twice, then report their medians. The full -// Devup target contains `.css.ts`; the single target exercises the lite WASM. +// target occupies each position twice, then report their medians. The vanilla +// target contains a static `.css.ts` module; the single target uses Devup APIs. const turboTargets = [ 'tailwind-turbo', 'devup-ui-single-turbo', diff --git a/packages/next-plugin/src/__tests__/coordinator.test.ts b/packages/next-plugin/src/__tests__/coordinator.test.ts index 62dd85c8..6a23d255 100644 --- a/packages/next-plugin/src/__tests__/coordinator.test.ts +++ b/packages/next-plugin/src/__tests__/coordinator.test.ts @@ -215,6 +215,53 @@ describe('coordinator', () => { coordinator.close() }) + it('uses the static vanilla transform on a production cache miss', async () => { + const source = `import { style } from '@vanilla-extract/css' +export const box = style({ color: 'red' })` + const extractOutput = { + code: 'transformed code', + map: undefined, + cssFile: undefined, + updatedBaseStyle: false, + free: mock(), + [Symbol.dispose]: mock(), + } + codeExtractWithoutSourceMapSpy.mockReturnValue(extractOutput) + const coordinator = startCoordinator( + makeOptions({ sourceMap: false, staticVanillaExtract: true }), + ) + await new Promise((resolve) => setTimeout(resolve, 100)) + const port = parseInt( + (writeFileSyncSpy.mock.calls[0] as [string, string])[1], + ) + + const res = await httpRequest( + port, + 'POST', + '/extract', + JSON.stringify({ + filename: 'src/styles.css.ts', + code: source, + resourcePath: join(process.cwd(), 'src', 'styles.css.ts'), + }), + ) + + expect(res.status).toBe(200) + expect(codeExtractWithoutSourceMapSpy).toHaveBeenCalledWith( + 'src/styles.css.ts', + `import { css } from '@devup-ui/react' +export const box = css({ color: 'red' })`, + '@devup-ui/react', + './../.tmp-coordinator-test/css', + false, + false, + true, + {}, + ) + expect(extractOutput.free).toHaveBeenCalledTimes(1) + coordinator.close() + }) + it('reuses byte-identical singleCss prewarm output', async () => { const source = 'const x = ' const options = makeOptions({ diff --git a/packages/next-plugin/src/__tests__/plugin.test.ts b/packages/next-plugin/src/__tests__/plugin.test.ts index b3701830..24dfc8ce 100644 --- a/packages/next-plugin/src/__tests__/plugin.test.ts +++ b/packages/next-plugin/src/__tests__/plugin.test.ts @@ -53,6 +53,18 @@ function createCodeExtractResult(contents: string): CodeExtractResult { } as unknown as CodeExtractResult } +function createSingleFileGraph(filename: string): StaticImportGraph { + return { + files: [filename], + fileSet: new Set([filename]), + staticImports: new Map([[filename, new Set()]]), + staticImporters: new Map([[filename, new Set()]]), + dynamicTargets: new Set(), + dynamicImports: new Map([[filename, new Set()]]), + externalImports: new Map([[filename, new Set()]]), + } +} + let existsSyncSpy: ReturnType let mkdirSyncSpy: ReturnType let readFileSyncSpy: ReturnType @@ -174,6 +186,13 @@ describe('DevupUINextPlugin', () => { expect( selectWasmVariant({ files: ['src/theme.css.js'] } as StaticImportGraph), ).toBe('full') + expect( + selectWasmVariant( + { files: ['src/theme.css.ts'] } as StaticImportGraph, + ['src/theme.css.ts'], + true, + ), + ).toBe('lite') expect( selectWasmVariant({ files: ['src/page.tsx'] } as StaticImportGraph, [ 'node_modules/design-system/theme.css.ts', @@ -542,8 +561,89 @@ describe('DevupUINextPlugin', () => { prewarmedFiles: expect.any(Array), prewarmedOutputs: expect.any(Map), sourceMap: false, + staticVanillaExtract: false, }) }) + it('uses the lite extractor for a provably static vanilla style module', () => { + setNodeEnv('production') + process.env.TURBOPACK = '1' + const filename = resolve('src/styles.css.ts') + const source = `import { style } from '@vanilla-extract/css' +export const box = style({ color: 'red' })` + const graphSpy = spyOn( + importGraphModule, + 'buildStaticImportGraph', + ).mockReturnValue(createSingleFileGraph(filename)) + const compiledSpy = spyOn( + importGraphModule, + 'computeCompiledFiles', + ).mockReturnValue(['src/styles.css.ts']) + readFileSyncSpy.mockImplementation((path: fs.PathOrFileDescriptor) => + String(path).endsWith('styles.css.ts') ? source : '{}', + ) + + try { + DevupUI({}) + + expect(codeExtractWithoutSourceMapSpy).toHaveBeenCalledWith( + 'src/styles.css.ts', + `import { css } from '@devup-ui/react' +export const box = css({ color: 'red' })`, + '@devup-ui/react', + expect.any(String), + false, + false, + true, + expect.anything(), + ) + expect(startCoordinatorSpy).toHaveBeenCalledWith( + expect.objectContaining({ staticVanillaExtract: true }), + ) + } finally { + graphSpy.mockRestore() + compiledSpy.mockRestore() + } + }) + it('keeps the full extractor for dynamic vanilla style modules', () => { + setNodeEnv('production') + process.env.TURBOPACK = '1' + const filename = resolve('src/styles.css.ts') + const source = `import { style } from '@vanilla-extract/css' +const color = getColor() +export const box = style({ color })` + const graphSpy = spyOn( + importGraphModule, + 'buildStaticImportGraph', + ).mockReturnValue(createSingleFileGraph(filename)) + const compiledSpy = spyOn( + importGraphModule, + 'computeCompiledFiles', + ).mockReturnValue(['src/styles.css.ts']) + readFileSyncSpy.mockImplementation((path: fs.PathOrFileDescriptor) => + String(path).endsWith('styles.css.ts') ? source : '{}', + ) + + try { + DevupUI({}) + + expect(codeExtractWithoutSourceMapSpy).toHaveBeenCalledWith( + 'src/styles.css.ts', + source, + '@devup-ui/react', + expect.any(String), + false, + false, + true, + expect.anything(), + ) + expect(startCoordinatorSpy).toHaveBeenCalledWith( + expect.objectContaining({ staticVanillaExtract: false }), + ) + } finally { + graphSpy.mockRestore() + compiledSpy.mockRestore() + } + }) it('keeps source maps when Next production browser source maps are enabled', () => { setNodeEnv('production') process.env.TURBOPACK = '1' @@ -744,6 +844,7 @@ describe('DevupUINextPlugin', () => { prewarmedFiles: [], prewarmedOutputs: expect.any(Map), sourceMap: true, + staticVanillaExtract: false, }) expect(codeExtractSpy).not.toHaveBeenCalled() expect(codeExtractWithoutSourceMapSpy).not.toHaveBeenCalled() diff --git a/packages/next-plugin/src/__tests__/static-vanilla.test.ts b/packages/next-plugin/src/__tests__/static-vanilla.test.ts new file mode 100644 index 00000000..16da2ad9 --- /dev/null +++ b/packages/next-plugin/src/__tests__/static-vanilla.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it } from 'bun:test' + +import { transformStaticVanillaExtract } from '../static-vanilla' + +describe('transformStaticVanillaExtract', () => { + it('converts static style modules without evaluating JavaScript', () => { + const source = `import { style } from '@vanilla-extract/css' +export const box = style({ color: 'red', padding: [4, null, 8] }) +export const hover = style({ selectors: { '&:hover': { opacity: 0.5 } } })` + + expect( + transformStaticVanillaExtract( + 'src/styles.css.ts', + source, + '@devup-ui/react', + ), + ).toBe(`import { css } from '@devup-ui/react' +export const box = css({ color: 'red', padding: [4, null, 8] }) +export const hover = css({ selectors: { '&:hover': { opacity: 0.5 } } })`) + }) + + it('rejects executable values and extra module statements', () => { + const dynamic = `import { style } from '@vanilla-extract/css' +const color = getColor() +export const box = style({ color })` + const spread = `import { style } from '@vanilla-extract/css' +export const box = style({ ...base, color: 'red' })` + + expect( + transformStaticVanillaExtract( + 'styles.css.ts', + dynamic, + '@devup-ui/react', + ), + ).toBeUndefined() + expect( + transformStaticVanillaExtract('styles.css.ts', spread, '@devup-ui/react'), + ).toBeUndefined() + expect( + transformStaticVanillaExtract('styles.ts', dynamic, '@devup-ui/react'), + ).toBeUndefined() + }) + + it('preserves comments and aliases in the static subset', () => { + const source = `/* import */ import { style as makeStyle } from '@vanilla-extract/css' +export const box = makeStyle({ + // responsive values remain source text + padding: [4, null, 8], +})` + + expect( + transformStaticVanillaExtract('styles.css.js', source, 'custom-package'), + ).toContain('export const box = css({') + }) + + it('rejects unsupported imports and malformed trailing comments', () => { + const multipleImports = `import { style, globalStyle } from '@vanilla-extract/css' +export const box = style({ color: 'red' })` + const unterminatedComment = `import { style } from '@vanilla-extract/css' +export const box = style({ color: 'red' }) /*` + + expect( + transformStaticVanillaExtract( + 'styles.css.ts', + multipleImports, + '@devup-ui/react', + ), + ).toBeUndefined() + expect( + transformStaticVanillaExtract( + 'styles.css.ts', + unterminatedComment, + '@devup-ui/react', + ), + ).toBeUndefined() + }) +}) diff --git a/packages/next-plugin/src/coordinator.ts b/packages/next-plugin/src/coordinator.ts index a6f6b437..71bf5347 100644 --- a/packages/next-plugin/src/coordinator.ts +++ b/packages/next-plugin/src/coordinator.ts @@ -5,6 +5,7 @@ import { basename, dirname, join, relative } from 'node:path' import { getFileNumByFilename } from '@devup-ui/plugin-utils' import { elapsedMs, profileStart, reportProfile } from './profile' +import { transformStaticVanillaExtract } from './static-vanilla' import type { DevupWasm } from './wasm' export interface CoordinatorOptions { @@ -47,6 +48,8 @@ export interface CoordinatorOptions { prewarmedOutputs?: Map /** Generate transform source maps. Defaults to true for existing callers. */ sourceMap?: boolean + /** Production-only static `.css.ts` fast path selected with the lite WASM. */ + staticVanillaExtract?: boolean /** * Idle threshold (ms) for the base-css `/css` wait. Defaults to 2500. * FALLBACK ONLY — used when `expectedBaseFiles` is empty (no deterministic @@ -374,6 +377,7 @@ export function startCoordinator(options: CoordinatorOptions): { getCss, } = wasm const prewarmedOutputs = options.prewarmedOutputs ?? new Map() + const staticVanillaExtract = options.staticVanillaExtract ?? false const extract = options.sourceMap === false ? codeExtractWithoutSourceMap : codeExtract @@ -469,7 +473,9 @@ export function startCoordinator(options: CoordinatorOptions): { : takeExtractOutput( extract( filename, - code, + (staticVanillaExtract + ? transformStaticVanillaExtract(filename, code, libPackage) + : undefined) ?? code, libPackage, relCssDir, singleCss, diff --git a/packages/next-plugin/src/plugin.ts b/packages/next-plugin/src/plugin.ts index 4bef293c..23d71d28 100644 --- a/packages/next-plugin/src/plugin.ts +++ b/packages/next-plugin/src/plugin.ts @@ -29,6 +29,7 @@ import { } from './coordinator' import { collectProductionPrewarmFiles } from './prewarm' import { elapsedMs, profileStart, reportProfile } from './profile' +import { transformStaticVanillaExtract } from './static-vanilla' import { loadWasm, loadWebpackPlugin } from './wasm' type DevupUiNextPluginOptions = Omit< @@ -39,9 +40,11 @@ type DevupUiNextPluginOptions = Omit< export function selectWasmVariant( graph: StaticImportGraph | undefined, candidateFiles: string[] = graph?.files ?? [], + staticVanillaExtract = false, ): 'lite' | 'full' { return graph && - !candidateFiles.some((filename) => /\.css\.(?:ts|js)$/.test(filename)) + (staticVanillaExtract || + !candidateFiles.some((filename) => /\.css\.(?:ts|js)$/.test(filename))) ? 'lite' : 'full' } @@ -78,6 +81,8 @@ export function DevupUI( } = options const importAliases = mergeImportAliases(userImportAliases) + const watch = process.env.NODE_ENV === 'development' + const sourceMap = watch || config.productionBrowserSourceMaps === true const sheetFile = join(distDir, 'sheet.json') const classMapFile = join(distDir, 'classMap.json') const fileMapFile = join(distDir, 'fileMap.json') @@ -120,7 +125,31 @@ export function DevupUI( }) : [] const candidateCollectMs = elapsedMs(candidateCollectStartedAt) - const wasmVariant = selectWasmVariant(staticGraph, wasmCandidateFiles) + const staticVanillaSources = new Map< + string, + { code: string; source: string } + >() + const vanillaCandidates = wasmCandidateFiles.filter((filename) => + /\.css\.(?:ts|js)$/.test(filename), + ) + let staticVanillaExtract = + !watch && !sourceMap && vanillaCandidates.length > 0 + for (const filename of vanillaCandidates) { + if (!staticVanillaExtract) break + const source = readFileSync(resolve(process.cwd(), filename), 'utf-8') + const code = transformStaticVanillaExtract(filename, source, libPackage) + if (code === undefined) { + staticVanillaExtract = false + staticVanillaSources.clear() + break + } + staticVanillaSources.set(filename, { code, source }) + } + const wasmVariant = selectWasmVariant( + staticGraph, + wasmCandidateFiles, + staticVanillaExtract, + ) const wasm = loadWasm(wasmVariant === 'lite') const { codeExtract, @@ -190,8 +219,6 @@ export function DevupUI( // coordinator shares this WASM instance, so it applies to every /extract. const atomMode = atomHoist !== undefined && Number.isFinite(atomHoist) && atomHoist > 0 - const watch = process.env.NODE_ENV === 'development' - const sourceMap = watch || config.productionBrowserSourceMaps === true const extract = sourceMap ? codeExtract : codeExtractWithoutSourceMap // Hoisted out of the try so the coordinator can receive it for per-bucket // completion. Stays `{}` if the best-effort pre-pass fails. @@ -298,7 +325,9 @@ export function DevupUI( ).replaceAll('\\', '/')}` const readStartedAt = prewarmStartedAt === undefined ? undefined : performance.now() - const source = readFileSync(resourcePath, 'utf-8') + const preparedVanilla = staticVanillaSources.get(filename) + const source = + preparedVanilla?.source ?? readFileSync(resourcePath, 'utf-8') if (readStartedAt !== undefined) { prewarmReadMs += performance.now() - readStartedAt prewarmSourceBytes += Buffer.byteLength(source) @@ -308,7 +337,7 @@ export function DevupUI( const output = takeExtractOutput( extract( filename, - source, + preparedVanilla?.code ?? source, libPackage, relCssDir, singleCss, @@ -388,6 +417,7 @@ export function DevupUI( prewarmedFiles, prewarmedOutputs, sourceMap, + staticVanillaExtract, }) // Cleanup on exit diff --git a/packages/next-plugin/src/static-vanilla.ts b/packages/next-plugin/src/static-vanilla.ts new file mode 100644 index 00000000..3bdd237c --- /dev/null +++ b/packages/next-plugin/src/static-vanilla.ts @@ -0,0 +1,194 @@ +function isIdentifierChar(char: string | undefined): boolean { + return char !== undefined && /[$\w]/.test(char) +} + +class StaticVanillaParser { + private index = 0 + private valid = true + + constructor( + private readonly source: string, + private readonly packageName: string, + ) {} + + parse(): string | undefined { + if (!this.word('import') || !this.char('{')) return undefined + const imported = this.identifier() + if (imported !== 'style') return undefined + let local = imported + if (this.word('as')) { + const alias = this.identifier() + if (alias === undefined) return undefined + local = alias + } + if ( + !this.char('}') || + !this.word('from') || + this.string() !== '@vanilla-extract/css' + ) { + return undefined + } + this.char(';') + + const declarations: string[] = [] + while (!this.done()) { + if (!this.word('export') || !this.word('const')) return undefined + const name = this.identifier() + if (name === undefined || !this.char('=') || !this.word(local)) { + return undefined + } + if (!this.char('(')) return undefined + this.skipTrivia() + const valueStart = this.index + if (!this.staticValue()) return undefined + const value = this.source.slice(valueStart, this.index) + if (!this.char(')')) return undefined + this.char(';') + declarations.push(`export const ${name} = css(${value})`) + } + + if (declarations.length === 0) return undefined + return `import { css } from '${this.packageName}'\n${declarations.join('\n')}` + } + + private staticValue(): boolean { + this.skipTrivia() + const char = this.source[this.index] + if (char === '{') return this.object() + if (char === '[') return this.array() + if (char === '"' || char === "'") return this.string() !== undefined + if (char === '+' || char === '-') this.index++ + const number = this.source + .slice(this.index) + .match(/^(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?/i) + if (number) { + this.index += number[0].length + return true + } + return this.word('true') || this.word('false') || this.word('null') + } + + private object(): boolean { + if (!this.char('{')) return false + if (this.char('}')) return true + while (true) { + this.skipTrivia() + const char = this.source[this.index] + const key = + char === '"' || char === "'" + ? this.string() + : (this.identifier() ?? this.numberKey()) + if (key === undefined || !this.char(':') || !this.staticValue()) { + return false + } + if (this.char('}')) return true + if (!this.char(',')) return false + if (this.char('}')) return true + } + } + + private array(): boolean { + if (!this.char('[')) return false + if (this.char(']')) return true + while (true) { + if (!this.staticValue()) return false + if (this.char(']')) return true + if (!this.char(',')) return false + if (this.char(']')) return true + } + } + + private numberKey(): string | undefined { + this.skipTrivia() + const match = this.source.slice(this.index).match(/^\d+(?:\.\d+)?/) + if (!match) return undefined + this.index += match[0].length + return match[0] + } + + private identifier(): string | undefined { + this.skipTrivia() + const match = this.source.slice(this.index).match(/^[$A-Z_a-z][$\w]*/) + if (!match) return undefined + this.index += match[0].length + return match[0] + } + + private string(): string | undefined { + this.skipTrivia() + const quote = this.source[this.index] + if (quote !== '"' && quote !== "'") return undefined + const start = ++this.index + while (this.index < this.source.length) { + const char = this.source[this.index++] + if (char === '\\') { + this.index++ + } else if (char === quote) { + return this.source.slice(start, this.index - 1) + } else if (char === '\n' || char === '\r') { + return undefined + } + } + return undefined + } + + private word(value: string): boolean { + this.skipTrivia() + if ( + !this.source.startsWith(value, this.index) || + isIdentifierChar(this.source[this.index - 1]) || + isIdentifierChar(this.source[this.index + value.length]) + ) { + return false + } + this.index += value.length + return true + } + + private char(value: string): boolean { + this.skipTrivia() + if (this.source[this.index] !== value) return false + this.index++ + return true + } + + private done(): boolean { + this.skipTrivia() + return this.valid && this.index === this.source.length + } + + private skipTrivia(): void { + while (this.index < this.source.length) { + if (/\s/.test(this.source[this.index])) { + this.index++ + } else if (this.source.startsWith('//', this.index)) { + const newline = this.source.indexOf('\n', this.index + 2) + this.index = newline === -1 ? this.source.length : newline + 1 + } else if (this.source.startsWith('/*', this.index)) { + const end = this.source.indexOf('*/', this.index + 2) + if (end === -1) { + this.valid = false + this.index = this.source.length + } else { + this.index = end + 2 + } + } else { + break + } + } + } +} + +/** + * Convert the deliberately small, statically provable subset of vanilla + * `style()` modules into Devup `css()` calls. Any executable expression or + * additional statement returns undefined and keeps the existing Boa path. + */ +export function transformStaticVanillaExtract( + filename: string, + source: string, + packageName: string, +): string | undefined { + if (!/\.css\.(?:ts|js)$/.test(filename)) return undefined + return new StaticVanillaParser(source, packageName).parse() +} From 88e3ed2e75efc2f776a1b0586e0da4667080b0ca Mon Sep 17 00:00:00 2001 From: devfive Date: Sat, 29 Aug 2026 15:18:44 +0900 Subject: [PATCH 26/43] chore: update project dependencies --- Cargo.lock | 596 ++++++++++-------- apps/rsbuild/package.json | 4 +- apps/vite-lib/package.json | 4 +- apps/vite/package.json | 2 +- benchmark/next-chakra-ui/package.json | 2 +- benchmark/next-mui/package.json | 2 +- .../next-stylex-turbo-devup-ui/package.json | 2 +- benchmark/next-stylex-turbo/package.json | 2 +- benchmark/next-stylex/package.json | 2 +- bindings/devup-ui-wasm/Cargo.toml | 6 +- libs/extractor/Cargo.toml | 20 +- libs/extractor/src/vanilla_extract.rs | 12 +- package.json | 7 +- packages/bun-plugin/package.json | 2 +- packages/components/package.json | 2 +- packages/eslint-plugin/package.json | 11 +- packages/next-plugin/package.json | 2 +- packages/plugin-utils/package.json | 2 +- packages/react/package.json | 8 +- packages/reset-css/package.json | 2 +- packages/rsbuild-plugin/package.json | 4 +- packages/vite-plugin/package.json | 2 +- packages/webpack-plugin/package.json | 2 +- 23 files changed, 374 insertions(+), 324 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ccd36f11..1e313878 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,6 +59,18 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-trait" version = "0.1.92" @@ -67,7 +79,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -109,27 +121,29 @@ checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "boa_ast" -version = "0.21.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6339a700715bda376f5ea65c76e8fe8fc880930d8b0638cea68e7f3da6538e0a" +checksum = "9221b0a090572b4a118b46c5874c0781fe4bc9546b85f0a615b8bfb609ca4ed7" dependencies = [ "bitflags", "boa_interner", "boa_macros", "boa_string", "indexmap", - "num-bigint 0.4.8", + "num-bigint", "rustc-hash", + "strum", ] [[package]] name = "boa_engine" -version = "0.21.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1521be326f8a5c8887e95d4ce7f002917a002a23f7b93b9a6a2bf50ed4157824" +checksum = "5d6aaf7fdc4299df68adb67e1a5440a51d34630a1f5271b9698fd74bc97b60d9" dependencies = [ "aligned-vec", "arrayvec", + "async-channel", "bitflags", "boa_ast", "boa_gc", @@ -144,19 +158,20 @@ dependencies = [ "dynify", "fast-float2", "float16", - "futures-channel", "futures-concurrency", "futures-lite", - "hashbrown 0.16.1", + "hashbrown 0.17.1", + "icu_calendar", "icu_normalizer", "indexmap", "intrusive-collections", - "itertools 0.14.0", - "num-bigint 0.4.8", + "itertools 0.15.0", + "num-bigint", "num-integer", "num-traits", "num_enum", - "paste", + "oneshot", + "pastey", "portable-atomic", "rand", "regress", @@ -168,59 +183,62 @@ dependencies = [ "static_assertions", "tag_ptr", "tap", + "temporal_rs", "thin-vec", "thiserror", "time", + "timezone_provider", "xsum", ] [[package]] name = "boa_gc" -version = "0.21.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17323a98cf2e631afacf1a6d659c1212c48a68bacfa85afab0a66ade80582e51" +checksum = "af82119558ab1002d1978c8459b53a3ebb506177d29a3aca37c07c1777b1d966" dependencies = [ + "arrayvec", "boa_macros", "boa_string", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "thin-vec", ] [[package]] name = "boa_interner" -version = "0.21.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20510b8b02bcde9b0a01cf34c0c308c56156503d1d91cdab4c8cfbd292b747ea" +checksum = "317c631e791f5cb7d0017e43c0a758b0eaffd40c34c4c360e2a6be7752b78cd1" dependencies = [ "boa_gc", "boa_macros", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "indexmap", "once_cell", - "phf 0.13.1", + "phf", "rustc-hash", "static_assertions", ] [[package]] name = "boa_macros" -version = "0.21.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5822cb4f146d243060e588bc5a5f2e709683fdad3d7111f42c48e6b5c921d23d" +checksum = "b882fd440dc0f4b7441367cf89567bb077920356750c9a76a16c002d8ec2fa5c" dependencies = [ "cfg-if", "cow-utils", "proc-macro2", "quote", - "syn 2.0.119", - "synstructure", + "syn 3.0.4", + "synstructure 0.14.0", ] [[package]] name = "boa_parser" -version = "0.21.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35bd957fa9fa93e3a001a8aba5a5cd40c2bbfde486378be4c4b472fd304aaddb" +checksum = "641e81b3c8cbb7a2140547fb3ea534e9edf210b4e8c920b5fa864a919e11fbcf" dependencies = [ "bitflags", "boa_ast", @@ -228,7 +246,7 @@ dependencies = [ "boa_macros", "fast-float2", "icu_properties", - "num-bigint 0.4.8", + "num-bigint", "num-traits", "regress", "rustc-hash", @@ -236,13 +254,13 @@ dependencies = [ [[package]] name = "boa_string" -version = "0.21.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2da1d7f4a76fd9040788a122f0d807910800a7b86f5952e9244848c36511de" +checksum = "30bf86c2bcff5bf2547acbfc516c5d3108ca527cb131511d749bf6fbef762c0e" dependencies = [ "fast-float2", "itoa", - "paste", + "pastey", "rustc-hash", "ryu-js", "static_assertions", @@ -277,7 +295,17 @@ checksum = "fc0e56a716f1e132ff6bf4bdac1c944a3fcdc1cae65f70a4a2a1ac3b401d2d1f" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", +] + +[[package]] +name = "calendrical_calculations" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5abbd6eeda6885048d357edc66748eea6e0268e3dd11f326fff5bd248d779c26" +dependencies = [ + "core_maths", + "displaydoc", ] [[package]] @@ -297,9 +325,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.4.3" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" dependencies = [ "find-msvc-tools", "shlex", @@ -311,6 +339,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + [[package]] name = "ciborium" version = "0.2.2" @@ -385,6 +424,15 @@ dependencies = [ "zmij", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "console" version = "0.16.4" @@ -406,12 +454,30 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + [[package]] name = "cow-utils" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79" +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + [[package]] name = "criterion" version = "0.8.2" @@ -484,7 +550,7 @@ version = "0.1.0" dependencies = [ "bimap", "criterion", - "phf 0.14.0", + "phf", "regex-lite", "rstest", "serde", @@ -519,7 +585,7 @@ dependencies = [ "console_error_panic_hook", "css", "extractor", - "getrandom 0.3.4", + "getrandom", "insta", "js-sys", "rstest", @@ -540,7 +606,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -629,6 +695,26 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "extractor" version = "0.1.0" @@ -646,7 +732,7 @@ dependencies = [ "oxc_span", "oxc_syntax", "oxc_transformer", - "phf 0.14.0", + "phf", "rstest", "rustc-hash", "serde_json", @@ -696,15 +782,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" -[[package]] -name = "futures-channel" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" -dependencies = [ - "futures-core", -] - [[package]] name = "futures-concurrency" version = "7.7.1" @@ -762,7 +839,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -790,20 +867,6 @@ dependencies = [ "slab", ] -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi 5.3.0", - "wasip2", - "wasm-bindgen", -] - [[package]] name = "getrandom" version = "0.4.3" @@ -811,8 +874,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", - "r-efi 6.0.0", + "r-efi", + "rand_core", + "wasm-bindgen", ] [[package]] @@ -838,17 +904,6 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", -] - [[package]] name = "hashbrown" version = "0.17.1" @@ -856,6 +911,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ "allocator-api2", + "equivalent", + "foldhash", ] [[package]] @@ -870,14 +927,37 @@ version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0b3ba31f6dc772cc8221ce81dbbbd64fa1e668255a6737d95eeace59b5a8823" +[[package]] +name = "icu_calendar" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58655df2f728e46e4eee80bddebefacf52ec3e77cdb925014b47c5a5905eb55c" +dependencies = [ + "calendrical_calculations", + "displaydoc", + "icu_calendar_data", + "icu_locale_core", + "icu_locale_fallback", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_calendar_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc00caaa3fb3201ff7a18aa458e8c3ab042b9da154cfdf948bdde3936c31c36e" + [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -897,13 +977,32 @@ dependencies = [ "zerovec", ] +[[package]] +name = "icu_locale_fallback" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "251af8e57c9400e3eb58242fe5b8b1152b2a64fdf4cf632f923c38ccee6f2fa9" +dependencies = [ + "icu_locale_core", + "icu_locale_fallback_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locale_fallback_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "decf2a22ec8fa68f1a0c1129a3f8583f8f8bc24e8b9ccbe98ead99f62a4dc3a8" + [[package]] name = "icu_normalizer" -version = "2.0.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b24a59706036ba941c9476a55cd57b82b77f38a3c667d637ee7cabbc85eaedc" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -916,31 +1015,30 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" [[package]] name = "icu_properties" -version = "2.0.2" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5a97b8ac6235e69506e8dacfb2adf38461d2ce6d3e9bd9c94c4cbc3cd4400a4" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" dependencies = [ "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" @@ -961,9 +1059,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.0" +version = "2.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb" dependencies = [ "equivalent", "hashbrown 0.17.1", @@ -983,12 +1081,9 @@ dependencies = [ [[package]] name = "intrusive-collections" -version = "0.9.7" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "189d0897e4cbe8c75efedf3502c18c887b05046e59d28404d4d8e46cbc4d1e86" -dependencies = [ - "memoffset", -] +checksum = "4275b20e6057cd7733fd8df8a5a31701e4fe44497dad0f3fa0e1c4fb971506be" [[package]] name = "itertools" @@ -999,15 +1094,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.15.0" @@ -1023,6 +1109,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "ixdtf" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3667095d64c3ecffc96463a21157b04bf3e252f6e8d5750b20c02e33c194e3" + [[package]] name = "js-sys" version = "0.3.104" @@ -1075,9 +1167,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "memchr" @@ -1085,15 +1177,6 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - [[package]] name = "minicov" version = "0.3.9" @@ -1128,17 +1211,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "num-bigint" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" -dependencies = [ - "num-integer", - "num-traits", - "serde", -] - [[package]] name = "num-bigint" version = "0.5.1" @@ -1147,6 +1219,7 @@ checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" dependencies = [ "num-integer", "num-traits", + "serde", ] [[package]] @@ -1211,6 +1284,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "oneshot" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe21416a02c693fb9f980befcb230ecc70b0b3d1cc4abf88b9675c4c1457f0c" + [[package]] name = "oorandom" version = "11.1.5" @@ -1225,9 +1304,9 @@ checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" [[package]] name = "owo-colors" -version = "4.3.0" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" +checksum = "13c45bb4a6ae1280ec0803b1ef9d3455eb50f01efbbe1447ab020f1d54fba9d8" [[package]] name = "oxc-browserslist" @@ -1246,9 +1325,9 @@ dependencies = [ [[package]] name = "oxc_allocator" -version = "0.146.0" +version = "0.147.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "064fce871c5cb07e557049ed27c8ca2aa530db797f73424c67e4bb8cfe61175e" +checksum = "c4e69b2400ab3c2f96eb3ae474ed80fa3a2959ba05e26afc5df45e634fc9a071" dependencies = [ "allocator-api2", "hashbrown 0.17.1", @@ -1258,9 +1337,9 @@ dependencies = [ [[package]] name = "oxc_ast" -version = "0.146.0" +version = "0.147.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a999fd4494b604fc0328fc43443bc1298722500587d87d7e993118bd82c65d7b" +checksum = "3dcf0e905263cc649c07e3494489de620462390de12cb2b5933247f91785a421" dependencies = [ "bitflags", "oxc_allocator", @@ -1276,21 +1355,21 @@ dependencies = [ [[package]] name = "oxc_ast_macros" -version = "0.146.0" +version = "0.147.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d715e3c300c95b1d797b05526567161fd86fcac2ac68235101c61b736f216cde" +checksum = "aa520723c19e83bcc683a988ac77d56bbca4b88178d5fc6c0c89fb73a45893cf" dependencies = [ - "phf 0.14.0", + "phf", "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] name = "oxc_ast_visit" -version = "0.146.0" +version = "0.147.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b9d77b5c27575ef7d624de1f226caeca3c0d3fcc2347bdaa788d5c4a85512d" +checksum = "7bcdfe65eec7cc091e52f73ea7f6546375a2dd5b88c43900afc94dafffebfb8c" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1300,9 +1379,9 @@ dependencies = [ [[package]] name = "oxc_codegen" -version = "0.146.0" +version = "0.147.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57b5180ce6f991eacd8c4a85eb28f9fbbff145422dd8ece782f48626957661d0" +checksum = "bd265be822054de498537a8df29f0afef75f9e804f9c499c81c0ec8c9b9212bd" dependencies = [ "bitflags", "cow-utils", @@ -1317,13 +1396,14 @@ dependencies = [ "oxc_str", "oxc_syntax", "rustc-hash", + "smallvec", ] [[package]] name = "oxc_compat" -version = "0.146.0" +version = "0.147.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfdfa97a1bb7a8f144937dfb4118ea6e2d681f619c886034f72675b788e1a251" +checksum = "970fe81a0a43846ebe37c9250f3a1948e984ba79cbdf866681fd117dffe27e3d" dependencies = [ "cow-utils", "oxc-browserslist", @@ -1334,18 +1414,18 @@ dependencies = [ [[package]] name = "oxc_data_structures" -version = "0.146.0" +version = "0.147.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d15a23b57a931fda6bc9a4fdc3fbabcac6a2edfd0b7216cb7c81432f0e9f54d9" +checksum = "e7903e8f1f76f148c943b5a2e192bf908d600b5fe0df1efc77ebeb75591ace56" dependencies = [ "ropey", ] [[package]] name = "oxc_diagnostics" -version = "0.146.0" +version = "0.147.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2d0ce1ec51b07b6501eecedd5ab50835b7ebe5c8463cada3b2e6d984d0943af" +checksum = "acb70f89da6524e6a2d097f0d747f14731156996ee5400474ef19ad3f318646b" dependencies = [ "bytecount", "cow-utils", @@ -1362,13 +1442,13 @@ dependencies = [ [[package]] name = "oxc_ecmascript" -version = "0.146.0" +version = "0.147.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b5d3d07165b7aadcd021f62cd95fb7d62cb6977b8bbfb42fa12adbfb7e75257" +checksum = "9f1fdd494072a511d7df691282dbd4fe656ad6fd0bea24d4366c019ed1172a4f" dependencies = [ "dragonbox_ecma", "itoa", - "num-bigint 0.5.1", + "num-bigint", "num-traits", "oxc_ast", "oxc_data_structures", @@ -1378,9 +1458,9 @@ dependencies = [ [[package]] name = "oxc_estree" -version = "0.146.0" +version = "0.147.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7af86a59b7aeb2845ffea2cc21ae259053865b175e9ac936a7d72f2744abe5" +checksum = "e2ef8cdc8d02a7e0b91ee51a2572963c914d3bb38261e11a54d068a092c4dc42" [[package]] name = "oxc_index" @@ -1394,14 +1474,14 @@ dependencies = [ [[package]] name = "oxc_parser" -version = "0.146.0" +version = "0.147.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "311c29dfdf55ea8bf065cf300b3d0dca5fe1c0fb8059c9ba70a6c98cce75306e" +checksum = "12a55bfce994289467c88ed9732dbc4c8f03f3186af2b77e48dc4a71c502cb2c" dependencies = [ "bitflags", "cow-utils", "memchr", - "num-bigint 0.5.1", + "num-bigint", "num-traits", "oxc_allocator", "oxc_ast", @@ -1418,9 +1498,9 @@ dependencies = [ [[package]] name = "oxc_regular_expression" -version = "0.146.0" +version = "0.147.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31839a373ad02a37fad54244a3eb710f9a94b5e2e16ca0e1d28a37b4c75b8ebc" +checksum = "d8455a292e58e24a0c51cfa2ec1d44e219e22d59a04b9af1ae050d4944a19d51" dependencies = [ "bitflags", "oxc_allocator", @@ -1428,16 +1508,16 @@ dependencies = [ "oxc_diagnostics", "oxc_span", "oxc_str", - "phf 0.14.0", + "phf", "rustc-hash", "unicode-id-start", ] [[package]] name = "oxc_semantic" -version = "0.146.0" +version = "0.147.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "366191309f606847936aade62707b06683ed5e0b0c8cb66c27710a61f64a50f1" +checksum = "b9c35fc29f969e7f93ee41dc37c9d82816e9c2597495f6cc302477831c8fd2ed" dependencies = [ "itertools 0.15.0", "memchr", @@ -1471,9 +1551,9 @@ dependencies = [ [[package]] name = "oxc_span" -version = "0.146.0" +version = "0.147.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c355ae1fa3e865c28cb8a85735ecafd1bb6340a5b880c638d127c1f2d61a98a5" +checksum = "378737bf42338e31badcdae2e6e2afc7a63cb7ffe82c418ac12d506c554adda4" dependencies = [ "compact_str", "oxc_allocator", @@ -1484,9 +1564,9 @@ dependencies = [ [[package]] name = "oxc_str" -version = "0.146.0" +version = "0.147.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba45ef9efa8296e647aa37b2b21936cb520953031edc1ef6352281bbea22faa" +checksum = "8564a0269f93153f856100db908af25434efdd1ec9d403ffa099cdab57385c8c" dependencies = [ "compact_str", "hashbrown 0.17.1", @@ -1496,9 +1576,9 @@ dependencies = [ [[package]] name = "oxc_syntax" -version = "0.146.0" +version = "0.147.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0970d9be099a082711b574d58add258549580a70fe6546b99a067bbd7ad4a264" +checksum = "59f05978138676c9d54f021391409fcf1314a46adcdbbaabc670c2990f8387dd" dependencies = [ "bitflags", "cow-utils", @@ -1510,15 +1590,15 @@ dependencies = [ "oxc_index", "oxc_span", "oxc_str", - "phf 0.14.0", + "phf", "unicode-id-start", ] [[package]] name = "oxc_transformer" -version = "0.146.0" +version = "0.147.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d286f586303c23502786e31959de18c57bfc66299d1f55b9dc2d56c3f4863f6e" +checksum = "2e4443b3e36410882b44ed13e415ef8ddc3d31e6786d03f5a0e486e4c1d18090" dependencies = [ "base64", "compact_str", @@ -1546,9 +1626,9 @@ dependencies = [ [[package]] name = "oxc_traverse" -version = "0.146.0" +version = "0.147.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f214c06bfb15427400f7c11fe4487ebeb4d5740b0803ff5f1a1c895198e40a30" +checksum = "02a3cc8f1a31a1b08104ede1a6631dfc984282234249f4503f51f4d2b22036e4" dependencies = [ "itoa", "oxc_allocator", @@ -1603,10 +1683,10 @@ dependencies = [ ] [[package]] -name = "paste" -version = "1.0.15" +name = "pastey" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" [[package]] name = "percent-encoding" @@ -1614,37 +1694,17 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "phf" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" -dependencies = [ - "phf_macros 0.13.1", - "phf_shared 0.13.1", -] - [[package]] name = "phf" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6" dependencies = [ - "phf_macros 0.14.0", - "phf_shared 0.14.0", + "phf_macros", + "phf_shared", "serde", ] -[[package]] -name = "phf_generator" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" -dependencies = [ - "fastrand", - "phf_shared 0.13.1", -] - [[package]] name = "phf_generator" version = "0.14.0" @@ -1652,20 +1712,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100" dependencies = [ "fastrand", - "phf_shared 0.14.0", -] - -[[package]] -name = "phf_macros" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" -dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", - "proc-macro2", - "quote", - "syn 2.0.119", + "phf_shared", ] [[package]] @@ -1674,22 +1721,13 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fa8d0ca26d424d27630da600c6624696e7dec8bf7b3b492b383c5dc49e5e085" dependencies = [ - "phf_generator 0.14.0", - "phf_shared 0.14.0", + "phf_generator", + "phf_shared", "proc-macro2", "quote", "syn 2.0.119", ] -[[package]] -name = "phf_shared" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" -dependencies = [ - "siphasher", -] - [[package]] name = "phf_shared" version = "0.14.0" @@ -1777,6 +1815,8 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ + "serde_core", + "writeable", "zerovec", ] @@ -1786,15 +1826,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - [[package]] name = "proc-macro-crate" version = "3.5.0" @@ -1822,12 +1853,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - [[package]] name = "r-efi" version = "6.0.0" @@ -1836,32 +1861,20 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.9.5" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", + "chacha20", + "getrandom", "rand_core", ] [[package]] name = "rand_core" -version = "0.9.5" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "rayon" @@ -1929,11 +1942,10 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "regress" -version = "0.10.5" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2057b2325e68a893284d1538021ab90279adac1139957ca2a74426c6f118fb48" +checksum = "32eef8b209c3c1c15dbad02c1f30f9539f00dc7253e0cbcdaae442a50a09d7c1" dependencies = [ - "hashbrown 0.16.1", "memchr", ] @@ -2093,7 +2105,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -2131,7 +2143,7 @@ checksum = "a22144e767da4ddd8416dbf383700542ffd8a5dc493dfecedfe1fe3ad03c98ae" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -2221,6 +2233,9 @@ name = "strum" version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] [[package]] name = "strum_macros" @@ -2247,9 +2262,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" dependencies = [ "proc-macro2", "quote", @@ -2267,6 +2282,17 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + [[package]] name = "tag_ptr" version = "0.1.0" @@ -2286,12 +2312,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom", "once_cell", "rustix", "windows-sys", ] +[[package]] +name = "temporal_rs" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a978fef907a27736827d336c0bfaab14fb8a7509dd6dc2b43c44e51d59aca5b" +dependencies = [ + "calendrical_calculations", + "core_maths", + "icu_calendar", + "icu_locale_core", + "ixdtf", + "num-traits", + "timezone_provider", + "tinystr", + "writeable", +] + [[package]] name = "textwrap" version = "0.16.2" @@ -2326,7 +2369,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -2361,6 +2404,18 @@ dependencies = [ "time-core", ] +[[package]] +name = "timezone_provider" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ec4f4eebb4817fde02a411aedc7ac5f66c89c68c49e5d4b17a8139f077af52" +dependencies = [ + "tinystr", + "zerofrom", + "zerotrie", + "zerovec", +] + [[package]] name = "tinystr" version = "0.8.4" @@ -2448,6 +2503,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "vsimd" version = "0.8.0" @@ -2464,15 +2525,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" -dependencies = [ - "wit-bindgen", -] - [[package]] name = "wasm-bindgen" version = "0.2.127" @@ -2632,12 +2684,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - [[package]] name = "write16" version = "1.0.0" @@ -2676,7 +2722,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.119", - "synstructure", + "synstructure 0.13.2", ] [[package]] @@ -2717,7 +2763,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.119", - "synstructure", + "synstructure 0.13.2", ] [[package]] @@ -2752,7 +2798,7 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] diff --git a/apps/rsbuild/package.json b/apps/rsbuild/package.json index 1b1f5a29..7e0e5255 100644 --- a/apps/rsbuild/package.json +++ b/apps/rsbuild/package.json @@ -8,7 +8,7 @@ "dev": "rsbuild dev --open", "preview": "rsbuild preview", "start": "npx serve ./dist", - "lint": "tsc && eslint" + "lint": "bun ../../node_modules/@typescript/native/bin/tsc && eslint" }, "dependencies": { "react": "^19.2", @@ -16,7 +16,7 @@ "@devup-ui/react": "workspace:^" }, "devDependencies": { - "@rsbuild/core": "^2.1", + "@rsbuild/core": "^2.2", "@rsbuild/plugin-react": "^2.1", "@devup-ui/rsbuild-plugin": "workspace:^" } diff --git a/apps/vite-lib/package.json b/apps/vite-lib/package.json index 667781ef..58306c64 100644 --- a/apps/vite-lib/package.json +++ b/apps/vite-lib/package.json @@ -9,7 +9,7 @@ }, "scripts": { "build": "vite build", - "lint": "tsc && eslint" + "lint": "bun ../../node_modules/@typescript/native/bin/tsc && eslint" }, "dependencies": { "react": "^19.2", @@ -18,7 +18,7 @@ "devDependencies": { "vite-plugin-dts": "^5.0", "@devup-ui/vite-plugin": "workspace:^", - "@vitejs/plugin-react": "^6.1.0", + "@vitejs/plugin-react": "^6.1.1", "vite": "^8.2", "typescript": "^7", "@types/node": "^26", diff --git a/apps/vite/package.json b/apps/vite/package.json index 2cb1b81e..1a0a0a9b 100644 --- a/apps/vite/package.json +++ b/apps/vite/package.json @@ -7,7 +7,7 @@ "dev": "vite dev", "build": "vite build", "start": "npx serve ./dist", - "lint": "tsc && eslint" + "lint": "bun ../../node_modules/@typescript/native/bin/tsc && eslint" }, "dependencies": { "react": "^19.2", diff --git a/benchmark/next-chakra-ui/package.json b/benchmark/next-chakra-ui/package.json index cceff3f9..9f719196 100644 --- a/benchmark/next-chakra-ui/package.json +++ b/benchmark/next-chakra-ui/package.json @@ -10,7 +10,7 @@ "lint": "next lint" }, "dependencies": { - "@chakra-ui/react": "^3.36", + "@chakra-ui/react": "^3.37", "@emotion/react": "^11.14", "next": "^16.3", "next-themes": "^0.4", diff --git a/benchmark/next-mui/package.json b/benchmark/next-mui/package.json index 5bed6878..f5e042f2 100644 --- a/benchmark/next-mui/package.json +++ b/benchmark/next-mui/package.json @@ -12,7 +12,7 @@ "dependencies": { "@emotion/react": "^11.14", "@emotion/styled": "^11.14", - "@mui/material": "9.3", + "@mui/material": "9.4", "next": "^16.3", "react": "^19.2", "react-dom": "^19.2", diff --git a/benchmark/next-stylex-turbo-devup-ui/package.json b/benchmark/next-stylex-turbo-devup-ui/package.json index db9debac..6fffef64 100644 --- a/benchmark/next-stylex-turbo-devup-ui/package.json +++ b/benchmark/next-stylex-turbo-devup-ui/package.json @@ -19,7 +19,7 @@ }, "devDependencies": { "@devup-ui/next-plugin": "workspace:^", - "@types/node": "^26.2", + "@types/node": "^26.4", "@types/react": "^19.2", "@types/react-dom": "^19.2", "rimraf": "^6.1", diff --git a/benchmark/next-stylex-turbo/package.json b/benchmark/next-stylex-turbo/package.json index 2c7cdbdf..e9cda026 100644 --- a/benchmark/next-stylex-turbo/package.json +++ b/benchmark/next-stylex-turbo/package.json @@ -18,7 +18,7 @@ "devDependencies": { "@stylexjs/babel-plugin": "0.19.0", "@stylexjs/postcss-plugin": "0.19.0", - "@types/node": "^26.2", + "@types/node": "^26.4", "@types/react": "^19.2", "@types/react-dom": "^19.2", "postcss": "^8.5", diff --git a/benchmark/next-stylex/package.json b/benchmark/next-stylex/package.json index c57cfe38..96642f44 100644 --- a/benchmark/next-stylex/package.json +++ b/benchmark/next-stylex/package.json @@ -25,7 +25,7 @@ "@babel/plugin-syntax-typescript": "^8.0", "@stylexjs/babel-plugin": "^0.19", "@stylexjs/nextjs-plugin": "^0.11", - "@types/node": "26.2", + "@types/node": "26.4", "@types/react": "19.2", "@types/react-dom": "19.2", "@types/react-test-renderer": "^19.1", diff --git a/bindings/devup-ui-wasm/Cargo.toml b/bindings/devup-ui-wasm/Cargo.toml index 2546871b..d43750ec 100644 --- a/bindings/devup-ui-wasm/Cargo.toml +++ b/bindings/devup-ui-wasm/Cargo.toml @@ -34,11 +34,11 @@ js-sys = "0.3.104" serde_json = "1.0.151" serde-wasm-bindgen = "0.6.5" # Not used directly. Declared only to enable the `wasm_js` feature on the -# `getrandom` version that boa_engine pulls in (boa_engine -> rand 0.9 -> -# rand_core 0.9 -> getrandom 0.3), which `.cargo/config.toml`'s +# `getrandom` version that boa_engine pulls in (boa_engine -> rand 0.10 -> +# rand_core 0.10 -> getrandom 0.4), which `.cargo/config.toml`'s # `getrandom_backend="wasm_js"` cfg requires on wasm32 targets. # Keep this major version in sync with boa_engine's transitive getrandom. -getrandom = { version = "0.3", features = ["wasm_js"] } +getrandom = { version = "0.4", features = ["wasm_js"] } [dev-dependencies] wasm-bindgen-test = "0.3.77" diff --git a/libs/extractor/Cargo.toml b/libs/extractor/Cargo.toml index 86ae3ac4..b1044b60 100644 --- a/libs/extractor/Cargo.toml +++ b/libs/extractor/Cargo.toml @@ -16,21 +16,21 @@ default = ["vanilla-extract"] vanilla-extract = ["dep:boa_engine"] [dependencies] -oxc_parser = "0.146.0" -oxc_syntax = "0.146.0" -oxc_span = "0.146.0" -oxc_allocator = "0.146.0" -oxc_ast = "0.146.0" -oxc_ast_visit = "0.146.0" -oxc_codegen = "0.146.0" -oxc_transformer = "0.146.0" -oxc_semantic = "0.146.0" +oxc_parser = "0.147.0" +oxc_syntax = "0.147.0" +oxc_span = "0.147.0" +oxc_allocator = "0.147.0" +oxc_ast = "0.147.0" +oxc_ast_visit = "0.147.0" +oxc_codegen = "0.147.0" +oxc_transformer = "0.147.0" +oxc_semantic = "0.147.0" css = { path = "../css" } phf = "0.14" strum = "0.28.0" strum_macros = "0.28.0" serde_json = "1.0" -boa_engine = { version = "0.21", optional = true } +boa_engine = { version = "0.22", optional = true } rustc-hash = "2" smallvec = "1" diff --git a/libs/extractor/src/vanilla_extract.rs b/libs/extractor/src/vanilla_extract.rs index d116e253..2095a0a6 100644 --- a/libs/extractor/src/vanilla_extract.rs +++ b/libs/extractor/src/vanilla_extract.rs @@ -1032,9 +1032,9 @@ fn register_vanilla_extract_apis( ); // Return [themeId, varsObject] as an array - let result_array = boa_engine::object::builtins::JsArray::new(ctx); - let _ = result_array.push(JsValue::from(js_string!(id.clone())), ctx); - let _ = result_array.push(vars_obj, ctx); + let result_array = boa_engine::object::builtins::JsArray::new(ctx)?; + result_array.push(JsValue::from(js_string!(id.clone())), ctx)?; + result_array.push(vars_obj, ctx)?; Ok(JsValue::from(result_array)) } else { @@ -2058,7 +2058,8 @@ export const lightTheme = createTheme(vars, { fn test_transform_contract_to_vars_array() { // Test transform_contract_to_vars with array (covers line 170) let mut context = boa_engine::Context::default(); - let array = boa_engine::object::builtins::JsArray::new(&mut context); + let array = boa_engine::object::builtins::JsArray::new(&mut context) + .expect("creating an empty array should succeed"); let _ = array.push(boa_engine::JsValue::from(1), &mut context); let value = boa_engine::JsValue::from(array); let result = super::transform_contract_to_vars(&value, &mut context, &mut Vec::new()); @@ -2070,7 +2071,8 @@ export const lightTheme = createTheme(vars, { fn test_transform_theme_to_vars_array() { // Test transform_theme_to_vars with array (covers line 250) let mut context = boa_engine::Context::default(); - let array = boa_engine::object::builtins::JsArray::new(&mut context); + let array = boa_engine::object::builtins::JsArray::new(&mut context) + .expect("creating an empty array should succeed"); let _ = array.push(boa_engine::JsValue::from(1), &mut context); let value = boa_engine::JsValue::from(array); let mut css_vars: SmallVec<[(String, String); 8]> = SmallVec::new(); diff --git a/package.json b/package.json index 63345e03..892c70a2 100644 --- a/package.json +++ b/package.json @@ -23,17 +23,18 @@ "devDependencies": { "@devup-ui/eslint-plugin": "workspace:^", "@playwright/test": "^1.62.1", + "@typescript/native": "npm:typescript@^7.0.2", "@types/bun": "latest", - "@types/node": "^26.2", + "@types/node": "^26.4", "bun-test-env-dom": "^1.0.3", - "eslint": "^10.8", + "eslint": "^10.9", "eslint-plugin-devup": "^2.1", "eslint-plugin-eslint-plugin": "^7.6", "eslint-plugin-jsonc": "^3.4", "eslint-plugin-mdx": "^3.8", "globals": "^17.11", "husky": "^9.1", - "typescript": "^6.0" + "typescript": "npm:@typescript/typescript6@^6.0.2" }, "author": "devfive", "workspaces": [ diff --git a/packages/bun-plugin/package.json b/packages/bun-plugin/package.json index 826278be..a4e0d495 100644 --- a/packages/bun-plugin/package.json +++ b/packages/bun-plugin/package.json @@ -21,7 +21,7 @@ "version": "1.0.15", "scripts": { "lint": "eslint", - "build": "tsc && bun build --target node src/index.cjs.ts --production --env=disable --outfile dist/index.cjs --format cjs --packages external && bun build --target node src/index.ts --production --env=disable --outfile dist/index.mjs --format esm --packages external && bun build --target node src/register.ts --production --env=disable --outfile dist/register.cjs --format cjs --packages external && bun build --target node src/register.ts --production --env=disable --outfile dist/register.mjs --format esm --packages external", + "build": "bun ../../node_modules/@typescript/native/bin/tsc && bun build --target node src/index.cjs.ts --production --env=disable --outfile dist/index.cjs --format cjs --packages external && bun build --target node src/index.ts --production --env=disable --outfile dist/index.mjs --format esm --packages external && bun build --target node src/register.ts --production --env=disable --outfile dist/register.cjs --format cjs --packages external && bun build --target node src/register.ts --production --env=disable --outfile dist/register.mjs --format esm --packages external", "test:regression": "cd __regression__ && bun test ./preload-race.bun.ts" }, "publishConfig": { diff --git a/packages/components/package.json b/packages/components/package.json index f4b97638..70c0b9b8 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -20,7 +20,7 @@ "type": "module", "scripts": { "lint": "eslint", - "build": "tsc && vite build", + "build": "bun ../../node_modules/@typescript/native/bin/tsc && vite build", "storybook": "storybook dev -p 6006", "build-storybook": "storybook build", "package": "bun pm pack --pack-destination \"../../dist\"" diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index a869f2de..10e431d6 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -21,7 +21,7 @@ "main": "dist/index.cjs", "type": "module", "scripts": { - "build": "tsc && bun build --target node src/index.ts --production --env=disable --outfile dist/index.cjs --format cjs --packages external && bun build --target node src/index.ts --production --env=disable --outfile dist/index.mjs --format esm --packages external" + "build": "bun ../../node_modules/@typescript/native/bin/tsc && bun build --target node src/index.ts --production --env=disable --outfile dist/index.cjs --format cjs --packages external && bun build --target node src/index.ts --production --env=disable --outfile dist/index.mjs --format esm --packages external" }, "publishConfig": { "access": "public" @@ -38,15 +38,16 @@ "dist" ], "dependencies": { - "typescript-eslint": "^8.67", - "@typescript-eslint/utils": "^8.67" + "typescript-eslint": "^8.68", + "@typescript-eslint/utils": "^8.68" }, "peerDependencies": { "typescript-eslint": "*", "@typescript-eslint/utils": "*" }, "devDependencies": { - "@typescript-eslint/rule-tester": "^8.67", - "typescript": "^6.0" + "@typescript-eslint/rule-tester": "^8.68", + "@typescript/native": "npm:typescript@^7.0.2", + "typescript": "npm:@typescript/typescript6@^6.0.2" } } diff --git a/packages/next-plugin/package.json b/packages/next-plugin/package.json index 9586959a..1715d331 100644 --- a/packages/next-plugin/package.json +++ b/packages/next-plugin/package.json @@ -22,7 +22,7 @@ "version": "1.0.83", "scripts": { "lint": "eslint", - "build": "tsc && bun build --target node src/index.ts --production --env=disable --outfile dist/index.cjs --format cjs --packages external && bun build --target node src/index.ts --production --env=disable --outfile dist/index.mjs --format esm --packages external && bun build --target node src/loader.ts --production --env=disable --outfile dist/loader.cjs --format cjs --packages external && bun build --target node src/loader.ts --production --env=disable --outfile dist/loader.mjs --format esm --packages external && bun build --target node src/css-loader.ts --production --env=disable --outfile dist/css-loader.cjs --format cjs --packages external && bun build --target node src/css-loader.ts --production --env=disable --outfile dist/css-loader.mjs --format esm --packages external" + "build": "bun ../../node_modules/@typescript/native/bin/tsc && bun build --target node src/index.ts --production --env=disable --outfile dist/index.cjs --format cjs --packages external && bun build --target node src/index.ts --production --env=disable --outfile dist/index.mjs --format esm --packages external && bun build --target node src/loader.ts --production --env=disable --outfile dist/loader.cjs --format cjs --packages external && bun build --target node src/loader.ts --production --env=disable --outfile dist/loader.mjs --format esm --packages external && bun build --target node src/css-loader.ts --production --env=disable --outfile dist/css-loader.cjs --format cjs --packages external && bun build --target node src/css-loader.ts --production --env=disable --outfile dist/css-loader.mjs --format esm --packages external" }, "publishConfig": { "access": "public" diff --git a/packages/plugin-utils/package.json b/packages/plugin-utils/package.json index 593976cc..8c251f76 100644 --- a/packages/plugin-utils/package.json +++ b/packages/plugin-utils/package.json @@ -18,7 +18,7 @@ "version": "1.0.13", "scripts": { "lint": "eslint", - "build": "tsc && bun build --target node src/index.ts --production --env=disable --outfile dist/index.cjs --format cjs --packages external && bun build --target node src/index.ts --production --env=disable --outfile dist/index.mjs --format esm --packages external" + "build": "bun ../../node_modules/@typescript/native/bin/tsc && bun build --target node src/index.ts --production --env=disable --outfile dist/index.cjs --format cjs --packages external && bun build --target node src/index.ts --production --env=disable --outfile dist/index.mjs --format esm --packages external" }, "publishConfig": { "access": "public" diff --git a/packages/react/package.json b/packages/react/package.json index 025ccace..0be1b79c 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -19,9 +19,9 @@ "version": "1.0.40", "type": "module", "scripts": { - "lint": "eslint && tsc --noEmit && bun run test:types", - "build": "bun run test:types && tsc && vite build", - "test:types": "tsc --ignoreConfig --noEmit --strict --jsx react-jsx --moduleResolution bundler --module esnext --target esnext --types bun --skipLibCheck type-tests/custom-shorthand.ts" + "lint": "eslint && bun ../../node_modules/@typescript/native/bin/tsc --noEmit && bun run test:types", + "build": "bun run test:types && bun ../../node_modules/@typescript/native/bin/tsc && vite build", + "test:types": "bun ../../node_modules/@typescript/native/bin/tsc --ignoreConfig --noEmit --strict --jsx react-jsx --moduleResolution bundler --module esnext --target esnext --types bun --skipLibCheck type-tests/custom-shorthand.ts" }, "publishConfig": { "access": "public" @@ -51,7 +51,7 @@ "typescript": "^7.0", "@typescript/typescript6": "^6.0", "@types/react": "^19.2", - "@types/node": "^26.2" + "@types/node": "^26.4" }, "peerDependencies": { "react": "*", diff --git a/packages/reset-css/package.json b/packages/reset-css/package.json index 5fd34b9d..ad39af4b 100644 --- a/packages/reset-css/package.json +++ b/packages/reset-css/package.json @@ -20,7 +20,7 @@ "type": "module", "scripts": { "lint": "eslint", - "build": "tsc && bun build --target node src/index.ts --production --env=disable --outfile dist/index.cjs --format cjs --packages external && bun build --target node src/index.ts --production --env=disable --outfile dist/index.mjs --format esm --packages external" + "build": "bun ../../node_modules/@typescript/native/bin/tsc && bun build --target node src/index.ts --production --env=disable --outfile dist/index.cjs --format cjs --packages external && bun build --target node src/index.ts --production --env=disable --outfile dist/index.mjs --format esm --packages external" }, "publishConfig": { "access": "public" diff --git a/packages/rsbuild-plugin/package.json b/packages/rsbuild-plugin/package.json index 7f262be5..a224c715 100644 --- a/packages/rsbuild-plugin/package.json +++ b/packages/rsbuild-plugin/package.json @@ -21,7 +21,7 @@ "version": "1.0.61", "scripts": { "lint": "eslint", - "build": "tsc && bun build --target node src/index.ts --production --env=disable --outfile dist/index.cjs --format cjs --packages external && bun build --target node src/index.ts --production --env=disable --outfile dist/index.mjs --format esm --packages external" + "build": "bun ../../node_modules/@typescript/native/bin/tsc && bun build --target node src/index.ts --production --env=disable --outfile dist/index.cjs --format cjs --packages external && bun build --target node src/index.ts --production --env=disable --outfile dist/index.mjs --format esm --packages external" }, "publishConfig": { "access": "public" @@ -48,7 +48,7 @@ "@rsbuild/core": "*" }, "devDependencies": { - "@rsbuild/core": "^2.1", + "@rsbuild/core": "^2.2", "typescript": "^7.0" } } diff --git a/packages/vite-plugin/package.json b/packages/vite-plugin/package.json index 99630d84..bc3e7376 100644 --- a/packages/vite-plugin/package.json +++ b/packages/vite-plugin/package.json @@ -21,7 +21,7 @@ "version": "1.0.67", "scripts": { "lint": "eslint", - "build": "tsc && bun build --target node src/index.ts --production --env=disable --outfile dist/index.cjs --format cjs --packages external && bun build --target node src/index.ts --production --env=disable --outfile dist/index.mjs --format esm --packages external" + "build": "bun ../../node_modules/@typescript/native/bin/tsc && bun build --target node src/index.ts --production --env=disable --outfile dist/index.cjs --format cjs --packages external && bun build --target node src/index.ts --production --env=disable --outfile dist/index.mjs --format esm --packages external" }, "publishConfig": { "access": "public" diff --git a/packages/webpack-plugin/package.json b/packages/webpack-plugin/package.json index 6df69c36..36dd5512 100644 --- a/packages/webpack-plugin/package.json +++ b/packages/webpack-plugin/package.json @@ -20,7 +20,7 @@ "type": "module", "version": "1.0.66", "scripts": { - "build": "tsc && bun build --target node src/index.ts --production --env=disable --outfile dist/index.cjs --format cjs --packages external && bun build --target node src/index.ts --production --env=disable --outfile dist/index.mjs --format esm --packages external && bun build --target node src/loader.ts --production --env=disable --outfile dist/loader.cjs --format cjs --packages external && bun build --target node src/loader.ts --production --env=disable --outfile dist/loader.mjs --format esm --packages external && bun build --target node src/css-loader.ts --production --env=disable --outfile dist/css-loader.cjs --format cjs --packages external && bun build --target node src/css-loader.ts --production --env=disable --outfile dist/css-loader.mjs --format esm --packages external" + "build": "bun ../../node_modules/@typescript/native/bin/tsc && bun build --target node src/index.ts --production --env=disable --outfile dist/index.cjs --format cjs --packages external && bun build --target node src/index.ts --production --env=disable --outfile dist/index.mjs --format esm --packages external && bun build --target node src/loader.ts --production --env=disable --outfile dist/loader.cjs --format cjs --packages external && bun build --target node src/loader.ts --production --env=disable --outfile dist/loader.mjs --format esm --packages external && bun build --target node src/css-loader.ts --production --env=disable --outfile dist/css-loader.cjs --format cjs --packages external && bun build --target node src/css-loader.ts --production --env=disable --outfile dist/css-loader.mjs --format esm --packages external" }, "publishConfig": { "access": "public" From 01ddbd847b99ded882e79f87dcea73b67655cebe Mon Sep 17 00:00:00 2001 From: devfive Date: Sat, 29 Aug 2026 15:43:45 +0900 Subject: [PATCH 27/43] fix: run Next type checks with TypeScript 7 CLI --- apps/landing/next.config.ts | 1 + apps/landing/package.json | 2 +- apps/next/next.config.mjs | 1 + apps/next/package.json | 2 +- apps/vite-lib/package.json | 2 +- apps/vite/package.json | 2 +- benchmark/next-chakra-ui/next.config.mjs | 1 + benchmark/next-chakra-ui/package.json | 2 +- .../next-devup-ui-collapse/next.config.ts | 2 +- benchmark/next-devup-ui-collapse/package.json | 2 +- .../next-devup-ui-single-turbo/next.config.ts | 2 +- .../next-devup-ui-single-turbo/package.json | 2 +- benchmark/next-devup-ui-single/next.config.ts | 2 +- benchmark/next-devup-ui-single/package.json | 2 +- benchmark/next-devup-ui-turbo/next.config.ts | 2 +- benchmark/next-devup-ui-turbo/package.json | 2 +- benchmark/next-devup-ui/next.config.ts | 2 +- benchmark/next-devup-ui/package.json | 2 +- benchmark/next-kuma-ui/next.config.mjs | 2 +- benchmark/next-kuma-ui/package.json | 2 +- benchmark/next-mui/next.config.mjs | 2 +- benchmark/next-mui/package.json | 2 +- benchmark/next-panda-css/next.config.mjs | 2 +- benchmark/next-panda-css/package.json | 2 +- .../next.config.mjs | 5 ++- .../next-stylex-turbo-devup-ui/package.json | 2 +- benchmark/next-stylex-turbo/next.config.js | 2 +- benchmark/next-stylex-turbo/package.json | 2 +- benchmark/next-stylex/next.config.js | 1 + benchmark/next-stylex/package.json | 2 +- .../next.config.mjs | 2 +- .../next-tailwind-turbo-devup-ui/package.json | 2 +- benchmark/next-tailwind-turbo/next.config.mjs | 2 +- benchmark/next-tailwind-turbo/package.json | 2 +- benchmark/next-tailwind/next.config.mjs | 2 +- benchmark/next-tailwind/package.json | 2 +- .../next.config.mjs | 2 +- .../package.json | 2 +- .../next-vanilla-extract/next.config.mjs | 2 +- benchmark/next-vanilla-extract/package.json | 2 +- benchmark/vinext-devup-ui/package.json | 2 +- eslint-typescript6.cjs | 36 +++++++++++++++++++ package.json | 7 ++-- packages/bun-plugin/package.json | 2 +- packages/components/package.json | 2 +- packages/next-plugin/package.json | 2 +- packages/plugin-utils/package.json | 2 +- packages/react/package.json | 2 +- packages/reset-css/package.json | 2 +- packages/rsbuild-plugin/package.json | 2 +- packages/vite-plugin/package.json | 2 +- packages/webpack-plugin/package.json | 2 +- 52 files changed, 93 insertions(+), 49 deletions(-) create mode 100644 eslint-typescript6.cjs diff --git a/apps/landing/next.config.ts b/apps/landing/next.config.ts index 84115a0d..832ebb3e 100644 --- a/apps/landing/next.config.ts +++ b/apps/landing/next.config.ts @@ -5,6 +5,7 @@ import type { NextConfig } from 'next' import { STATIC_EXPORT_DEPLOYMENT_ID } from './src/utils/static-export-rsc-transport' const baseConfig: NextConfig = { + experimental: { useTypeScriptCli: true }, output: 'export', pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'], reactCompiler: true, diff --git a/apps/landing/package.json b/apps/landing/package.json index d7ae275b..92a0a58a 100644 --- a/apps/landing/package.json +++ b/apps/landing/package.json @@ -42,7 +42,7 @@ "@vitejs/plugin-react": "^6.1", "@vitejs/plugin-rsc": "^0.5.34", "babel-plugin-react-compiler": "^1.0", - "typescript": "^7", + "typescript": "^7.0.2", "vite": "^8.2" } } diff --git a/apps/next/next.config.mjs b/apps/next/next.config.mjs index 6efaf4c9..69b5bb89 100644 --- a/apps/next/next.config.mjs +++ b/apps/next/next.config.mjs @@ -2,6 +2,7 @@ import { DevupUI } from '@devup-ui/next-plugin' const nextConfig = { + experimental: { useTypeScriptCli: true }, trailingSlash: true, /* config options here */ } diff --git a/apps/next/package.json b/apps/next/package.json index 8f2d249a..b13c5cb6 100644 --- a/apps/next/package.json +++ b/apps/next/package.json @@ -18,7 +18,7 @@ }, "devDependencies": { "@devup-ui/next-plugin": "workspace:^", - "typescript": "^7", + "typescript": "^7.0.2", "@types/node": "^26", "@types/react": "^19.2", "@types/react-dom": "^19.2" diff --git a/apps/vite-lib/package.json b/apps/vite-lib/package.json index 58306c64..54aa7531 100644 --- a/apps/vite-lib/package.json +++ b/apps/vite-lib/package.json @@ -20,7 +20,7 @@ "@devup-ui/vite-plugin": "workspace:^", "@vitejs/plugin-react": "^6.1.1", "vite": "^8.2", - "typescript": "^7", + "typescript": "^7.0.2", "@types/node": "^26", "@types/react": "^19" } diff --git a/apps/vite/package.json b/apps/vite/package.json index 1a0a0a9b..c8562e95 100644 --- a/apps/vite/package.json +++ b/apps/vite/package.json @@ -19,7 +19,7 @@ "@devup-ui/vite-plugin": "workspace:^", "vite": "^8.2", "@vitejs/plugin-react": "^6.1", - "typescript": "^7", + "typescript": "^7.0.2", "@types/node": "^26", "@types/react": "^19", "@types/react-dom": "^19" diff --git a/benchmark/next-chakra-ui/next.config.mjs b/benchmark/next-chakra-ui/next.config.mjs index 7339eefb..6a64402c 100644 --- a/benchmark/next-chakra-ui/next.config.mjs +++ b/benchmark/next-chakra-ui/next.config.mjs @@ -1,5 +1,6 @@ export default { experimental: { optimizePackageImports: ['@chakra-ui/react'], + useTypeScriptCli: true, }, } diff --git a/benchmark/next-chakra-ui/package.json b/benchmark/next-chakra-ui/package.json index 9f719196..bcdd3496 100644 --- a/benchmark/next-chakra-ui/package.json +++ b/benchmark/next-chakra-ui/package.json @@ -22,6 +22,6 @@ "@types/node": "^26", "@types/react": "^19", "@types/react-dom": "^19", - "typescript": "^7" + "typescript": "^7.0.2" } } diff --git a/benchmark/next-devup-ui-collapse/next.config.ts b/benchmark/next-devup-ui-collapse/next.config.ts index 0cf3a9e0..5af9af04 100644 --- a/benchmark/next-devup-ui-collapse/next.config.ts +++ b/benchmark/next-devup-ui-collapse/next.config.ts @@ -1,5 +1,5 @@ import { DevupUI } from '@devup-ui/next-plugin' -const nextConfig = {/* config options here */} +const nextConfig = { experimental: { useTypeScriptCli: true } } export default DevupUI(nextConfig, {}) diff --git a/benchmark/next-devup-ui-collapse/package.json b/benchmark/next-devup-ui-collapse/package.json index 433c0ddf..c15179cd 100644 --- a/benchmark/next-devup-ui-collapse/package.json +++ b/benchmark/next-devup-ui-collapse/package.json @@ -17,7 +17,7 @@ }, "devDependencies": { "@devup-ui/next-plugin": "workspace:^", - "typescript": "^7", + "typescript": "^7.0.2", "@types/node": "^26", "@types/react": "^19", "@types/react-dom": "^19" diff --git a/benchmark/next-devup-ui-single-turbo/next.config.ts b/benchmark/next-devup-ui-single-turbo/next.config.ts index c8460f9d..97842089 100644 --- a/benchmark/next-devup-ui-single-turbo/next.config.ts +++ b/benchmark/next-devup-ui-single-turbo/next.config.ts @@ -1,5 +1,5 @@ import { DevupUI } from '@devup-ui/next-plugin' -const nextConfig = {/* config options here */} +const nextConfig = { experimental: { useTypeScriptCli: true } } export default DevupUI(nextConfig, { singleCss: true }) diff --git a/benchmark/next-devup-ui-single-turbo/package.json b/benchmark/next-devup-ui-single-turbo/package.json index 85b646ed..d88d0654 100644 --- a/benchmark/next-devup-ui-single-turbo/package.json +++ b/benchmark/next-devup-ui-single-turbo/package.json @@ -17,7 +17,7 @@ }, "devDependencies": { "@devup-ui/next-plugin": "workspace:^", - "typescript": "^7", + "typescript": "^7.0.2", "@types/node": "^26", "@types/react": "^19", "@types/react-dom": "^19" diff --git a/benchmark/next-devup-ui-single/next.config.ts b/benchmark/next-devup-ui-single/next.config.ts index c8460f9d..97842089 100644 --- a/benchmark/next-devup-ui-single/next.config.ts +++ b/benchmark/next-devup-ui-single/next.config.ts @@ -1,5 +1,5 @@ import { DevupUI } from '@devup-ui/next-plugin' -const nextConfig = {/* config options here */} +const nextConfig = { experimental: { useTypeScriptCli: true } } export default DevupUI(nextConfig, { singleCss: true }) diff --git a/benchmark/next-devup-ui-single/package.json b/benchmark/next-devup-ui-single/package.json index b6c6bf54..3edc6771 100644 --- a/benchmark/next-devup-ui-single/package.json +++ b/benchmark/next-devup-ui-single/package.json @@ -17,7 +17,7 @@ }, "devDependencies": { "@devup-ui/next-plugin": "workspace:^", - "typescript": "^7", + "typescript": "^7.0.2", "@types/node": "^26", "@types/react": "^19", "@types/react-dom": "^19" diff --git a/benchmark/next-devup-ui-turbo/next.config.ts b/benchmark/next-devup-ui-turbo/next.config.ts index 0cf3a9e0..5af9af04 100644 --- a/benchmark/next-devup-ui-turbo/next.config.ts +++ b/benchmark/next-devup-ui-turbo/next.config.ts @@ -1,5 +1,5 @@ import { DevupUI } from '@devup-ui/next-plugin' -const nextConfig = {/* config options here */} +const nextConfig = { experimental: { useTypeScriptCli: true } } export default DevupUI(nextConfig, {}) diff --git a/benchmark/next-devup-ui-turbo/package.json b/benchmark/next-devup-ui-turbo/package.json index 209e30ef..f8b39d37 100644 --- a/benchmark/next-devup-ui-turbo/package.json +++ b/benchmark/next-devup-ui-turbo/package.json @@ -17,7 +17,7 @@ }, "devDependencies": { "@devup-ui/next-plugin": "workspace:^", - "typescript": "^7", + "typescript": "^7.0.2", "@types/node": "^26", "@types/react": "^19", "@types/react-dom": "^19" diff --git a/benchmark/next-devup-ui/next.config.ts b/benchmark/next-devup-ui/next.config.ts index 0cf3a9e0..5af9af04 100644 --- a/benchmark/next-devup-ui/next.config.ts +++ b/benchmark/next-devup-ui/next.config.ts @@ -1,5 +1,5 @@ import { DevupUI } from '@devup-ui/next-plugin' -const nextConfig = {/* config options here */} +const nextConfig = { experimental: { useTypeScriptCli: true } } export default DevupUI(nextConfig, {}) diff --git a/benchmark/next-devup-ui/package.json b/benchmark/next-devup-ui/package.json index 4bd25854..e2aa4809 100644 --- a/benchmark/next-devup-ui/package.json +++ b/benchmark/next-devup-ui/package.json @@ -17,7 +17,7 @@ }, "devDependencies": { "@devup-ui/next-plugin": "workspace:^", - "typescript": "^7", + "typescript": "^7.0.2", "@types/node": "^26", "@types/react": "^19", "@types/react-dom": "^19" diff --git a/benchmark/next-kuma-ui/next.config.mjs b/benchmark/next-kuma-ui/next.config.mjs index 8edc5a2b..74c4ae05 100644 --- a/benchmark/next-kuma-ui/next.config.mjs +++ b/benchmark/next-kuma-ui/next.config.mjs @@ -1,5 +1,5 @@ import { withKumaUI } from '@kuma-ui/next-plugin' -const nextConfig = {/* config options here */} +const nextConfig = { experimental: { useTypeScriptCli: true } } export default withKumaUI(nextConfig) diff --git a/benchmark/next-kuma-ui/package.json b/benchmark/next-kuma-ui/package.json index 17d5648e..a1a33d60 100644 --- a/benchmark/next-kuma-ui/package.json +++ b/benchmark/next-kuma-ui/package.json @@ -17,7 +17,7 @@ }, "devDependencies": { "@kuma-ui/next-plugin": "^1.4", - "typescript": "^7", + "typescript": "^7.0.2", "@types/node": "^26", "@types/react": "^19", "@types/react-dom": "^19" diff --git a/benchmark/next-mui/next.config.mjs b/benchmark/next-mui/next.config.mjs index b1c6ea43..7e934a7b 100644 --- a/benchmark/next-mui/next.config.mjs +++ b/benchmark/next-mui/next.config.mjs @@ -1 +1 @@ -export default {} +export default { experimental: { useTypeScriptCli: true } } diff --git a/benchmark/next-mui/package.json b/benchmark/next-mui/package.json index f5e042f2..4773002d 100644 --- a/benchmark/next-mui/package.json +++ b/benchmark/next-mui/package.json @@ -22,6 +22,6 @@ "@types/node": "^26", "@types/react": "^19", "@types/react-dom": "^19", - "typescript": "^7" + "typescript": "^7.0.2" } } diff --git a/benchmark/next-panda-css/next.config.mjs b/benchmark/next-panda-css/next.config.mjs index b7617371..b0427162 100644 --- a/benchmark/next-panda-css/next.config.mjs +++ b/benchmark/next-panda-css/next.config.mjs @@ -1,3 +1,3 @@ -const nextConfig = {/* config options here */} +const nextConfig = { experimental: { useTypeScriptCli: true } } export default nextConfig diff --git a/benchmark/next-panda-css/package.json b/benchmark/next-panda-css/package.json index b96cee5c..bfae670c 100644 --- a/benchmark/next-panda-css/package.json +++ b/benchmark/next-panda-css/package.json @@ -17,7 +17,7 @@ }, "devDependencies": { "@pandacss/dev": "^1.12", - "typescript": "^7", + "typescript": "^7.0.2", "@types/node": "^26", "@types/react": "^19", "@types/react-dom": "^19" diff --git a/benchmark/next-stylex-turbo-devup-ui/next.config.mjs b/benchmark/next-stylex-turbo-devup-ui/next.config.mjs index ca6aae5d..bbd839d6 100644 --- a/benchmark/next-stylex-turbo-devup-ui/next.config.mjs +++ b/benchmark/next-stylex-turbo-devup-ui/next.config.mjs @@ -1,3 +1,6 @@ import { DevupUI } from '@devup-ui/next-plugin' -export default DevupUI({}, { singleCss: true }) +export default DevupUI( + { experimental: { useTypeScriptCli: true } }, + { singleCss: true }, +) diff --git a/benchmark/next-stylex-turbo-devup-ui/package.json b/benchmark/next-stylex-turbo-devup-ui/package.json index 6fffef64..63cb3ec8 100644 --- a/benchmark/next-stylex-turbo-devup-ui/package.json +++ b/benchmark/next-stylex-turbo-devup-ui/package.json @@ -23,6 +23,6 @@ "@types/react": "^19.2", "@types/react-dom": "^19.2", "rimraf": "^6.1", - "typescript": "^7.0" + "typescript": "^7.0.2" } } diff --git a/benchmark/next-stylex-turbo/next.config.js b/benchmark/next-stylex-turbo/next.config.js index 5a877d2d..80a1a5fa 100644 --- a/benchmark/next-stylex-turbo/next.config.js +++ b/benchmark/next-stylex-turbo/next.config.js @@ -1,2 +1,2 @@ /** @type {import('next').NextConfig} */ -module.exports = {} +module.exports = { experimental: { useTypeScriptCli: true } } diff --git a/benchmark/next-stylex-turbo/package.json b/benchmark/next-stylex-turbo/package.json index e9cda026..ee59962c 100644 --- a/benchmark/next-stylex-turbo/package.json +++ b/benchmark/next-stylex-turbo/package.json @@ -23,6 +23,6 @@ "@types/react-dom": "^19.2", "postcss": "^8.5", "rimraf": "^6.1", - "typescript": "^7.0" + "typescript": "^7.0.2" } } diff --git a/benchmark/next-stylex/next.config.js b/benchmark/next-stylex/next.config.js index ddeab360..bcc8a4b4 100644 --- a/benchmark/next-stylex/next.config.js +++ b/benchmark/next-stylex/next.config.js @@ -2,6 +2,7 @@ const stylexPlugin = require('@stylexjs/nextjs-plugin') const nextConfig = { + experimental: { useTypeScriptCli: true }, // Configure `pageExtensions` to include MDX files pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'], transpilePackages: ['@stylexjs/open-props'], diff --git a/benchmark/next-stylex/package.json b/benchmark/next-stylex/package.json index 96642f44..944a2a35 100644 --- a/benchmark/next-stylex/package.json +++ b/benchmark/next-stylex/package.json @@ -33,6 +33,6 @@ "react-test-renderer": "^19.2", "rimraf": "^6.1", "ts_dependency_graph": "^2.1", - "typescript": "7.0" + "typescript": "^7.0.2" } } diff --git a/benchmark/next-tailwind-turbo-devup-ui/next.config.mjs b/benchmark/next-tailwind-turbo-devup-ui/next.config.mjs index 00a680c8..e0668cb1 100644 --- a/benchmark/next-tailwind-turbo-devup-ui/next.config.mjs +++ b/benchmark/next-tailwind-turbo-devup-ui/next.config.mjs @@ -1,3 +1,3 @@ import { DevupUI } from '@devup-ui/next-plugin' -export default DevupUI({}) +export default DevupUI({ experimental: { useTypeScriptCli: true } }) diff --git a/benchmark/next-tailwind-turbo-devup-ui/package.json b/benchmark/next-tailwind-turbo-devup-ui/package.json index 105a0c26..c07d1205 100644 --- a/benchmark/next-tailwind-turbo-devup-ui/package.json +++ b/benchmark/next-tailwind-turbo-devup-ui/package.json @@ -22,7 +22,7 @@ "@types/node": "^26", "@types/react": "^19", "@types/react-dom": "^19", - "typescript": "^7", + "typescript": "^7.0.2", "tailwindcss": "^4.3", "@devup-ui/next-plugin": "workspace:^" } diff --git a/benchmark/next-tailwind-turbo/next.config.mjs b/benchmark/next-tailwind-turbo/next.config.mjs index b1c6ea43..7e934a7b 100644 --- a/benchmark/next-tailwind-turbo/next.config.mjs +++ b/benchmark/next-tailwind-turbo/next.config.mjs @@ -1 +1 @@ -export default {} +export default { experimental: { useTypeScriptCli: true } } diff --git a/benchmark/next-tailwind-turbo/package.json b/benchmark/next-tailwind-turbo/package.json index 5f82baa2..eb5013c6 100644 --- a/benchmark/next-tailwind-turbo/package.json +++ b/benchmark/next-tailwind-turbo/package.json @@ -21,7 +21,7 @@ "@types/node": "^26", "@types/react": "^19", "@types/react-dom": "^19", - "typescript": "^7", + "typescript": "^7.0.2", "tailwindcss": "^4.3" } } diff --git a/benchmark/next-tailwind/next.config.mjs b/benchmark/next-tailwind/next.config.mjs index b1c6ea43..7e934a7b 100644 --- a/benchmark/next-tailwind/next.config.mjs +++ b/benchmark/next-tailwind/next.config.mjs @@ -1 +1 @@ -export default {} +export default { experimental: { useTypeScriptCli: true } } diff --git a/benchmark/next-tailwind/package.json b/benchmark/next-tailwind/package.json index 07e9af58..6be29f23 100644 --- a/benchmark/next-tailwind/package.json +++ b/benchmark/next-tailwind/package.json @@ -21,7 +21,7 @@ "@types/node": "^26", "@types/react": "^19", "@types/react-dom": "^19", - "typescript": "^7", + "typescript": "^7.0.2", "tailwindcss": "^4.3" } } diff --git a/benchmark/next-vanilla-extract-devup-ui/next.config.mjs b/benchmark/next-vanilla-extract-devup-ui/next.config.mjs index 00a680c8..e0668cb1 100644 --- a/benchmark/next-vanilla-extract-devup-ui/next.config.mjs +++ b/benchmark/next-vanilla-extract-devup-ui/next.config.mjs @@ -1,3 +1,3 @@ import { DevupUI } from '@devup-ui/next-plugin' -export default DevupUI({}) +export default DevupUI({ experimental: { useTypeScriptCli: true } }) diff --git a/benchmark/next-vanilla-extract-devup-ui/package.json b/benchmark/next-vanilla-extract-devup-ui/package.json index 0a71b8cd..eaab3df4 100644 --- a/benchmark/next-vanilla-extract-devup-ui/package.json +++ b/benchmark/next-vanilla-extract-devup-ui/package.json @@ -20,7 +20,7 @@ "@types/node": "^26", "@types/react": "^19", "@types/react-dom": "^19", - "typescript": "^7", + "typescript": "^7.0.2", "@devup-ui/next-plugin": "workspace:^" } } diff --git a/benchmark/next-vanilla-extract/next.config.mjs b/benchmark/next-vanilla-extract/next.config.mjs index a3fdb85e..dcf3d103 100644 --- a/benchmark/next-vanilla-extract/next.config.mjs +++ b/benchmark/next-vanilla-extract/next.config.mjs @@ -2,4 +2,4 @@ import { createVanillaExtractPlugin } from '@vanilla-extract/next-plugin' const withVanillaExtract = createVanillaExtractPlugin() -export default withVanillaExtract({}) +export default withVanillaExtract({ experimental: { useTypeScriptCli: true } }) diff --git a/benchmark/next-vanilla-extract/package.json b/benchmark/next-vanilla-extract/package.json index b73225c5..0b49c3da 100644 --- a/benchmark/next-vanilla-extract/package.json +++ b/benchmark/next-vanilla-extract/package.json @@ -20,7 +20,7 @@ "@types/node": "^26", "@types/react": "^19", "@types/react-dom": "^19", - "typescript": "^7", + "typescript": "^7.0.2", "@vanilla-extract/next-plugin": "^2.5" } } diff --git a/benchmark/vinext-devup-ui/package.json b/benchmark/vinext-devup-ui/package.json index 9c9b8605..e46828f3 100644 --- a/benchmark/vinext-devup-ui/package.json +++ b/benchmark/vinext-devup-ui/package.json @@ -21,7 +21,7 @@ "@types/react-dom": "^19", "@vitejs/plugin-rsc": "^0.5.34", "react-server-dom-webpack": "^19.2", - "typescript": "^7", + "typescript": "^7.0.2", "vite": "^8.2" } } diff --git a/eslint-typescript6.cjs b/eslint-typescript6.cjs new file mode 100644 index 00000000..2e9c67c8 --- /dev/null +++ b/eslint-typescript6.cjs @@ -0,0 +1,36 @@ +// TypeScript 7 is a native CLI and intentionally has no JavaScript compiler +// API yet. ESLint's TypeScript parser still requires that API, so route only +// its `typescript` imports to the official side-by-side TypeScript 6 package. +const Module = require('node:module') +const path = require('node:path') +const { pathToFileURL } = require('node:url') + +const originalResolveFilename = Module._resolveFilename +const typescript6Package = + require.resolve('@typescript/typescript6/package.json') +const typescript6Directory = path.dirname(typescript6Package) + +function resolveTypeScript6(request) { + if (request === 'typescript') { + return path.join(typescript6Directory, 'lib', 'typescript.js') + } + if (request.startsWith('typescript/')) { + return path.join(typescript6Directory, request.slice('typescript/'.length)) + } +} + +Module.registerHooks({ + resolve(specifier, context, nextResolve) { + const resolved = resolveTypeScript6(specifier) + return resolved + ? { shortCircuit: true, url: pathToFileURL(resolved).href } + : nextResolve(specifier, context) + }, +}) + +Module._resolveFilename = function resolveFilename(request, parent, ...rest) { + return ( + resolveTypeScript6(request) ?? + originalResolveFilename.call(this, request, parent, ...rest) + ) +} diff --git a/package.json b/package.json index 892c70a2..690a41ad 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "description": "devup-ui", "type": "module", "scripts": { - "lint": "cargo fmt --all -- --check && cargo clippy --all-targets --all-features -- -D warnings && eslint", - "lint:fix": "eslint --fix && cargo fmt", + "lint": "cargo fmt --all -- --check && cargo clippy --all-targets --all-features -- -D warnings && node --require ./eslint-typescript6.cjs ./node_modules/eslint/bin/eslint.js", + "lint:fix": "node --require ./eslint-typescript6.cjs ./node_modules/eslint/bin/eslint.js --fix && cargo fmt", "test": "cargo tarpaulin --out xml --out stdout --all-targets --engine llvm $(node -p \"process.env.CI ? '--fail-under 100' : ''\") && bun test", "test:e2e": "bunx playwright test", "test:e2e:next": "LANDING_BUILD_MODE=next bunx playwright test", @@ -24,6 +24,7 @@ "@devup-ui/eslint-plugin": "workspace:^", "@playwright/test": "^1.62.1", "@typescript/native": "npm:typescript@^7.0.2", + "@typescript/typescript6": "^6.0.2", "@types/bun": "latest", "@types/node": "^26.4", "bun-test-env-dom": "^1.0.3", @@ -34,7 +35,7 @@ "eslint-plugin-mdx": "^3.8", "globals": "^17.11", "husky": "^9.1", - "typescript": "npm:@typescript/typescript6@^6.0.2" + "typescript": "^7.0.2" }, "author": "devfive", "workspaces": [ diff --git a/packages/bun-plugin/package.json b/packages/bun-plugin/package.json index a4e0d495..fbe9ff0a 100644 --- a/packages/bun-plugin/package.json +++ b/packages/bun-plugin/package.json @@ -51,7 +51,7 @@ }, "devDependencies": { "@types/bun": "latest", - "typescript": "^7.0" + "typescript": "^7.0.2" }, "peerDependencies": { "@devup-ui/wasm": "*" diff --git a/packages/components/package.json b/packages/components/package.json index 70c0b9b8..39a5f0cb 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -56,7 +56,7 @@ "eslint-plugin-storybook": "^10.5", "rollup-plugin-preserve-directives": "^0.4", "storybook": "^10.5", - "typescript": "^7.0", + "typescript": "^7.0.2", "@typescript/typescript6": "^6.0", "vite": "^8.2", "vite-plugin-dts": "^5.0" diff --git a/packages/next-plugin/package.json b/packages/next-plugin/package.json index 1715d331..c81b9434 100644 --- a/packages/next-plugin/package.json +++ b/packages/next-plugin/package.json @@ -56,7 +56,7 @@ "@devup-ui/wasm": "workspace:^" }, "devDependencies": { - "typescript": "^7.0", + "typescript": "^7.0.2", "@types/webpack": "^5.28" }, "peerDependencies": { diff --git a/packages/plugin-utils/package.json b/packages/plugin-utils/package.json index 8c251f76..8e8a4168 100644 --- a/packages/plugin-utils/package.json +++ b/packages/plugin-utils/package.json @@ -37,6 +37,6 @@ "dist" ], "devDependencies": { - "typescript": "^7.0" + "typescript": "^7.0.2" } } diff --git a/packages/react/package.json b/packages/react/package.json index 0be1b79c..96c00de6 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -48,7 +48,7 @@ "rollup-plugin-preserve-directives": "^0.4", "vite": "^8.2", "vite-plugin-dts": "^5.0", - "typescript": "^7.0", + "typescript": "^7.0.2", "@typescript/typescript6": "^6.0", "@types/react": "^19.2", "@types/node": "^26.4" diff --git a/packages/reset-css/package.json b/packages/reset-css/package.json index ad39af4b..7065ad5e 100644 --- a/packages/reset-css/package.json +++ b/packages/reset-css/package.json @@ -43,7 +43,7 @@ "@devup-ui/react": "workspace:^" }, "devDependencies": { - "typescript": "^7.0" + "typescript": "^7.0.2" }, "peerDependencies": { "@devup-ui/react": "workspace:^" diff --git a/packages/rsbuild-plugin/package.json b/packages/rsbuild-plugin/package.json index a224c715..c3667021 100644 --- a/packages/rsbuild-plugin/package.json +++ b/packages/rsbuild-plugin/package.json @@ -49,6 +49,6 @@ }, "devDependencies": { "@rsbuild/core": "^2.2", - "typescript": "^7.0" + "typescript": "^7.0.2" } } diff --git a/packages/vite-plugin/package.json b/packages/vite-plugin/package.json index bc3e7376..153f9baf 100644 --- a/packages/vite-plugin/package.json +++ b/packages/vite-plugin/package.json @@ -44,7 +44,7 @@ "@devup-ui/wasm": "workspace:^" }, "devDependencies": { - "typescript": "^7.0" + "typescript": "^7.0.2" }, "peerDependencies": { "@devup-ui/wasm": "*", diff --git a/packages/webpack-plugin/package.json b/packages/webpack-plugin/package.json index 36dd5512..9b7177c6 100644 --- a/packages/webpack-plugin/package.json +++ b/packages/webpack-plugin/package.json @@ -57,6 +57,6 @@ }, "devDependencies": { "@types/webpack": "^5.28", - "typescript": "^7.0" + "typescript": "^7.0.2" } } From d5d51af005b9fe627923c59629aa8abe4092b2e7 Mon Sep 17 00:00:00 2001 From: devfive Date: Sat, 29 Aug 2026 15:50:43 +0900 Subject: [PATCH 28/43] fix: type the Next TypeScript CLI option --- apps/landing/next.config.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/landing/next.config.ts b/apps/landing/next.config.ts index 832ebb3e..6ebfab29 100644 --- a/apps/landing/next.config.ts +++ b/apps/landing/next.config.ts @@ -4,7 +4,11 @@ import type { NextConfig } from 'next' import { STATIC_EXPORT_DEPLOYMENT_ID } from './src/utils/static-export-rsc-transport' -const baseConfig: NextConfig = { +const baseConfig: NextConfig & { + experimental: NonNullable & { + useTypeScriptCli: true + } +} = { experimental: { useTypeScriptCli: true }, output: 'export', pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'], From 8b71d826b596339ca21f69ccabd7002c32df736c Mon Sep 17 00:00:00 2001 From: devfive Date: Sat, 29 Aug 2026 15:58:43 +0900 Subject: [PATCH 29/43] docs: refresh CI benchmark results --- README.md | 40 ++++--- README_ko.md | 40 ++++--- .../src/app/(detail)/docs/overview/page.mdx | 103 +++++++++++------- apps/landing/src/app/Bench.tsx | 51 +++++---- apps/landing/src/app/DevupUICard.tsx | 10 +- apps/landing/src/app/OtherCard.tsx | 4 +- packages/components/README.md | 38 ++++--- packages/next-plugin/README.md | 38 ++++--- packages/react/README.md | 38 ++++--- packages/reset-css/README.md | 38 ++++--- packages/rsbuild-plugin/README.md | 38 ++++--- packages/vite-plugin/README.md | 38 ++++--- packages/webpack-plugin/README.md | 38 ++++--- 13 files changed, 325 insertions(+), 189 deletions(-) diff --git a/README.md b/README.md index cd5ff8e1..56473d9f 100644 --- a/README.md +++ b/README.md @@ -79,21 +79,31 @@ npm install @devup-ui/webpack-plugin ## Comparison Benchmarks -Next.js Build Time and Build Size (github action - ubuntu-latest) - -| Library | Version | Build Time | Build Size | -| ---------------------------------- | ------- | ---------- | -------------------- | -| tailwindcss | 4.1.13 | 19.31s | 59,521,539 bytes | -| styleX | 0.15.4 | 41.78s | 86,869,452 bytes | -| vanilla-extract | 1.17.4 | 19.50s | 61,494,033 bytes | -| kuma-ui | 1.5.9 | 20.93s | 69,924,179 bytes | -| panda-css | 1.3.1 | 20.64s | 64,573,260 bytes | -| chakra-ui | 3.27.0 | 28.81s | 222,435,802 bytes | -| mui | 7.3.2 | 20.86s | 97,964,458 bytes | -| **devup-ui(per-file css)** | 1.0.18 | **16.90s** | 59,540,459 bytes | -| **devup-ui(single css)** | 1.0.18 | **17.05s** | **59,520,196 bytes** | -| tailwindcss(turbopack) | 4.1.13 | 6.72s | 5,355,082 bytes | -| **devup-ui(single css+turbopack)** | 1.0.18 | 10.34s | **4,772,050 bytes** | +[Latest CI benchmark](https://github.com/dev-five-git/devup-ui/actions/runs/33239265133) on `ubuntu-latest`. All Next.js builds use the native TypeScript 7 CLI for type checking. + +Webpack values are one cold build: + +| Library | Version | Build Time | Build Size | +| --------------------------- | ------- | ---------- | ----------------- | +| 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 | + +Turbopack values are medians of six cold builds in alternating order: + +| Library | Version | Median Build Time | Build Size | +| -------------------------------------- | ------- | ----------------- | -------------------- | +| 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 | + +The Turbopack ranges overlap, so the direct-API result is effectively parity with Tailwind on this fixture. The static `.css.ts` row uses the `lite` WASM fast path; dynamic `.css.ts` modules use the full Boa evaluator and are not represented by that median. ## How it works diff --git a/README_ko.md b/README_ko.md index b609d2ab..095a0183 100644 --- a/README_ko.md +++ b/README_ko.md @@ -79,21 +79,31 @@ npm install @devup-ui/webpack-plugin ## 비교 벤치마크 -Next.js Build Time and Build Size (github action - ubuntu-latest) - -| 라이브러리 | 버전 | 빌드 시간 | 빌드 사이즈 | -| ---------------------------------- | ------ | ---------- | -------------------- | -| tailwindcss | 4.1.13 | 19.31s | 59,521,539 bytes | -| styleX | 0.15.4 | 41.78s | 86,869,452 bytes | -| vanilla-extract | 1.17.4 | 19.50s | 61,494,033 bytes | -| kuma-ui | 1.5.9 | 20.93s | 69,924,179 bytes | -| panda-css | 1.3.1 | 20.64s | 64,573,260 bytes | -| chakra-ui | 3.27.0 | 28.81s | 222,435,802 bytes | -| mui | 7.3.2 | 20.86s | 97,964,458 bytes | -| **devup-ui(per-file css)** | 1.0.18 | **16.90s** | 59,540,459 bytes | -| **devup-ui(single css)** | 1.0.18 | **17.05s** | **59,520,196 bytes** | -| tailwindcss(turbopack) | 4.1.13 | 6.72s | 5,355,082 bytes | -| **devup-ui(single css+turbopack)** | 1.0.18 | 10.34s | **4,772,050 bytes** | +[최신 CI 벤치마크](https://github.com/dev-five-git/devup-ui/actions/runs/33239265133)는 `ubuntu-latest`에서 실행했습니다. 모든 Next.js 빌드는 네이티브 TypeScript 7 CLI로 타입 검사를 수행합니다. + +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** | + +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 | + +Turbopack 측정 범위는 서로 겹치므로 direct API 결과는 이 fixture에서 Tailwind와 사실상 동률입니다. static `.css.ts` 행은 `lite` WASM 빠른 경로를 사용하며, full Boa evaluator가 필요한 dynamic `.css.ts`는 해당 중앙값에 포함되지 않습니다. ## 작동 원리 diff --git a/apps/landing/src/app/(detail)/docs/overview/page.mdx b/apps/landing/src/app/(detail)/docs/overview/page.mdx index 2f40a4bc..ebd5c31f 100644 --- a/apps/landing/src/app/(detail)/docs/overview/page.mdx +++ b/apps/landing/src/app/(detail)/docs/overview/page.mdx @@ -129,7 +129,11 @@ const Card = styled('div', { ### Proven Performance -Benchmarks on Next.js (GitHub Actions - ubuntu-latest): +Latest Next.js cold-build results from [GitHub Actions run 33239265133](https://github.com/dev-five-git/devup-ui/actions/runs/33239265133) on `ubuntu-latest`. +Devup rows use `@devup-ui/react` 1.0.40 and `@devup-ui/next-plugin` 1.0.83. +All Next.js builds use the native TypeScript 7 CLI for type checking. + +Webpack values are one cold build: @@ -143,73 +147,96 @@ Benchmarks on Next.js (GitHub Actions - ubuntu-latest): tailwindcss - 4.1.13 - 19.31s - 59,521,539 bytes + 4.3.3 + 15.55s + 66,479,450 bytes styleX - 0.15.4 - 41.78s - 86,869,452 bytes + 0.19.0 + 34.27s + 95,417,163 bytes vanilla-extract - 1.17.4 - 19.50s - 61,494,033 bytes + 1.21.2 + 14.91s + 67,711,539 bytes kuma-ui - 1.5.9 - 20.93s - 69,924,179 bytes + 1.6.4 + 16.43s + 74,774,187 bytes panda-css - 1.3.1 - 20.64s - 64,573,260 bytes + 1.12.0 + 16.82s + 70,983,831 bytes chakra-ui - 3.27.0 - 28.81s - 222,435,802 bytes + 3.37.0 + 24.79s + 206,598,161 bytes mui - 7.3.2 - 20.86s - 97,964,458 bytes + 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** + +
+ +Turbopack values are medians of six cold builds in alternating order. Build size is the output from the same commit's representative run: + + + - **devup-ui (per-file css)** - **1.0.18** - **16.90s** - 59,540,459 bytes + Library + Version + Median Build Time + Build Size + + - **devup-ui (single css)** - **1.0.18** - **17.05s** - **59,520,196 bytes** + tailwindcss + 4.3.3 + 6.55s + 38,386,955 bytes - tailwindcss (turbopack) - 4.1.13 - 6.72s - 5,355,082 bytes + **devup-ui (direct APIs, single CSS)** + **1.0.40** + **6.54s** + **36,519,234 bytes** - **devup-ui (single css + turbopack)** - **1.0.18** - 10.34s - **4,772,050 bytes** + **devup-ui (static `.css.ts`)** + **1.0.40** + **6.47s** + 36,550,197 bytes
+The Turbopack ranges overlap, so these results indicate parity on this fixture rather than a universal performance advantage. The static `.css.ts` row uses the `lite` WASM fast path; dynamic `.css.ts` modules use the full Boa evaluator and are not represented by that median. + ### Get Started Ready to experience the future of CSS-in-JS? Head to the [Installation](/docs/installation) guide to get started in minutes. diff --git a/apps/landing/src/app/Bench.tsx b/apps/landing/src/app/Bench.tsx index b7c60396..d7303325 100644 --- a/apps/landing/src/app/Bench.tsx +++ b/apps/landing/src/app/Bench.tsx @@ -7,51 +7,51 @@ import { OtherCard } from './OtherCard' const OTHER_CARDS = [ { title: 'Chakra UI', - version: '3.27.0', - buildTime: '29.99s', - buildSize: '200.39MB', + version: '3.37.0', + buildTime: '24.79s', + buildSize: '197.03MB', url: 'https://chakra-ui.com', }, { title: 'Mui', - version: '7.3.2', - buildTime: '22.21s', - buildSize: '89.87MB', + version: '9.4.0', + buildTime: '17.12s', + buildSize: '95.96MB', url: 'https://mui.com', }, { title: 'Kuma UI', - version: '1.5.9', - buildTime: '21.61s', - buildSize: '64.30MB', + version: '1.6.4', + buildTime: '16.43s', + buildSize: '71.31MB', url: 'https://kuma-ui.com', }, { title: 'Tailwindcss', - version: '4.1.13', - buildTime: '20.22s', - buildSize: '54.76MB', + version: '4.3.3', + buildTime: '15.55s', + buildSize: '63.40MB', url: 'https://tailwindcss.com', }, { title: 'panda CSS', - version: '1.3.1', - buildTime: '22.01s', - buildSize: '59.53MB', + version: '1.12.0', + buildTime: '16.82s', + buildSize: '67.70MB', url: 'https://panda-css.com', }, { title: 'styleX', - version: '0.15.4', - buildTime: '38.97s', - buildSize: '72.72MB', + version: '0.19.0', + buildTime: '34.27s', + buildSize: '91.00MB', url: 'https://stylexjs.com', }, { title: 'vanilla extract', - version: '1.17.4', - buildTime: '20.09s', - buildSize: '56.61MB', + version: '1.21.2', + buildTime: '14.91s', + buildSize: '64.57MB', url: 'https://vanilla-extract.style', }, ] @@ -77,10 +77,15 @@ export function Bench() { wordBreak="keep-all" > - Comparison Bechmarks + Comparison Benchmarks - Next.js Build Time and Build Size (github action - ubuntu-latest) + Next.js cold build on GitHub Actions (ubuntu-latest, run + 33239265133). Devup React 1.0.40 / Next plugin 1.0.83. + + + Cards show one Webpack build. Turbopack six-run median: Devup static + .css.ts 6.47s, Devup direct APIs 6.54s, and Tailwind 6.55s. diff --git a/apps/landing/src/app/DevupUICard.tsx b/apps/landing/src/app/DevupUICard.tsx index 8d131212..eb795ab3 100644 --- a/apps/landing/src/app/DevupUICard.tsx +++ b/apps/landing/src/app/DevupUICard.tsx @@ -30,13 +30,13 @@ export function DevupUICard() { Devup UI - 1.0.18 + 1.0.40 - Next.js Build Time + Next.js Build Time (single CSS) - 18.23s + 13.37s - Bulid Size + Build Size - 54.75MB + 63.48MB diff --git a/apps/landing/src/app/OtherCard.tsx b/apps/landing/src/app/OtherCard.tsx index d7f7d7e6..1c213b66 100644 --- a/apps/landing/src/app/OtherCard.tsx +++ b/apps/landing/src/app/OtherCard.tsx @@ -48,7 +48,7 @@ export function OtherCard({ - Bulid Time + Build Time {buildTime} @@ -56,7 +56,7 @@ export function OtherCard({ - Bulid Size + Build Size {buildSize} diff --git a/packages/components/README.md b/packages/components/README.md index d84355a5..72d9da7e 100644 --- a/packages/components/README.md +++ b/packages/components/README.md @@ -73,19 +73,31 @@ npm install @devup-ui/webpack-plugin ## Comparison Benchmarks -Next.js Build Time and Build Size (github action - ubuntu-latest) - -| Library | Version | Build Time | Build Size | -| ---------------------- | ------- | ---------- | ----------------- | -| tailwindcss | 4.1.13 | 20.22s | 57,415,796 bytes | -| styleX | 0.15.4 | 38.97s | 76,257,820 bytes | -| vanilla-extract | 1.17.4 | 20.09s | 59,366,237 bytes | -| kuma-ui | 1.5.9 | 21.61s | 67,422,085 bytes | -| panda-css | 1.3.1 | 22.01s | 62,431,065 bytes | -| chakra-ui | 3.27.0 | 29.99s | 210,122,493 bytes | -| mui | 7.3.2 | 22.21s | 94,231,958 bytes | -| devup-ui(per-file css) | 1.0.18 | 18.23s | 57,440,953 bytes | -| devup-ui(single css) | 1.0.18 | 18.35s | 57,409,008 bytes | +[Latest CI benchmark](https://github.com/dev-five-git/devup-ui/actions/runs/33239265133) on `ubuntu-latest`. All Next.js builds use the native TypeScript 7 CLI for type checking. + +Webpack values are one cold build: + +| Library | Version | Build Time | Build Size | +| --------------------------- | ------- | ---------- | ----------------- | +| 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 | + +Turbopack values are medians of six cold builds in alternating order: + +| Library | Version | Median Build Time | Build Size | +| -------------------------------------- | ------- | ----------------- | -------------------- | +| 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 | + +The Turbopack ranges overlap, so the direct-API result is effectively parity with Tailwind on this fixture. The static `.css.ts` row uses the `lite` WASM fast path; dynamic `.css.ts` modules use the full Boa evaluator and are not represented by that median. ## How it works diff --git a/packages/next-plugin/README.md b/packages/next-plugin/README.md index 8eb44d97..5775ace8 100644 --- a/packages/next-plugin/README.md +++ b/packages/next-plugin/README.md @@ -73,19 +73,31 @@ npm install @devup-ui/webpack-plugin ## Comparison Benchmarks -Next.js Build Time and Build Size (github action - ubuntu-latest) - -| Library | Version | Build Time | Build Size | -| ---------------------- | ------- | ---------- | ----------------- | -| tailwindcss | 4.1.13 | 20.22s | 57,415,796 bytes | -| styleX | 0.15.4 | 38.97s | 76,257,820 bytes | -| vanilla-extract | 1.17.4 | 20.09s | 59,366,237 bytes | -| kuma-ui | 1.5.9 | 21.61s | 67,422,085 bytes | -| panda-css | 1.3.1 | 22.01s | 62,431,065 bytes | -| chakra-ui | 3.27.0 | 29.99s | 210,122,493 bytes | -| mui | 7.3.2 | 22.21s | 94,231,958 bytes | -| devup-ui(per-file css) | 1.0.18 | 18.23s | 57,440,953 bytes | -| devup-ui(single css) | 1.0.18 | 18.35s | 57,409,008 bytes | +[Latest CI benchmark](https://github.com/dev-five-git/devup-ui/actions/runs/33239265133) on `ubuntu-latest`. All Next.js builds use the native TypeScript 7 CLI for type checking. + +Webpack values are one cold build: + +| Library | Version | Build Time | Build Size | +| --------------------------- | ------- | ---------- | ----------------- | +| 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 | + +Turbopack values are medians of six cold builds in alternating order: + +| Library | Version | Median Build Time | Build Size | +| -------------------------------------- | ------- | ----------------- | -------------------- | +| 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 | + +The Turbopack ranges overlap, so the direct-API result is effectively parity with Tailwind on this fixture. The static `.css.ts` row uses the `lite` WASM fast path; dynamic `.css.ts` modules use the full Boa evaluator and are not represented by that median. ## How it works diff --git a/packages/react/README.md b/packages/react/README.md index ad8638ac..bc91e68a 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -73,19 +73,31 @@ npm install @devup-ui/webpack-plugin ## Comparison Benchmarks -Next.js Build Time and Build Size (github action - ubuntu-latest) - -| Library | Version | Build Time | Build Size | -| ---------------------- | ------- | ---------- | ----------------- | -| tailwindcss | 4.1.13 | 20.22s | 57,415,796 bytes | -| styleX | 0.15.4 | 38.97s | 76,257,820 bytes | -| vanilla-extract | 1.17.4 | 20.09s | 59,366,237 bytes | -| kuma-ui | 1.5.9 | 21.61s | 67,422,085 bytes | -| panda-css | 1.3.1 | 22.01s | 62,431,065 bytes | -| chakra-ui | 3.27.0 | 29.99s | 210,122,493 bytes | -| mui | 7.3.2 | 22.21s | 94,231,958 bytes | -| devup-ui(per-file css) | 1.0.18 | 18.23s | 57,440,953 bytes | -| devup-ui(single css) | 1.0.18 | 18.35s | 57,409,008 bytes | +[Latest CI benchmark](https://github.com/dev-five-git/devup-ui/actions/runs/33239265133) on `ubuntu-latest`. All Next.js builds use the native TypeScript 7 CLI for type checking. + +Webpack values are one cold build: + +| Library | Version | Build Time | Build Size | +| --------------------------- | ------- | ---------- | ----------------- | +| 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 | + +Turbopack values are medians of six cold builds in alternating order: + +| Library | Version | Median Build Time | Build Size | +| -------------------------------------- | ------- | ----------------- | -------------------- | +| 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 | + +The Turbopack ranges overlap, so the direct-API result is effectively parity with Tailwind on this fixture. The static `.css.ts` row uses the `lite` WASM fast path; dynamic `.css.ts` modules use the full Boa evaluator and are not represented by that median. ## How it works diff --git a/packages/reset-css/README.md b/packages/reset-css/README.md index d84355a5..72d9da7e 100644 --- a/packages/reset-css/README.md +++ b/packages/reset-css/README.md @@ -73,19 +73,31 @@ npm install @devup-ui/webpack-plugin ## Comparison Benchmarks -Next.js Build Time and Build Size (github action - ubuntu-latest) - -| Library | Version | Build Time | Build Size | -| ---------------------- | ------- | ---------- | ----------------- | -| tailwindcss | 4.1.13 | 20.22s | 57,415,796 bytes | -| styleX | 0.15.4 | 38.97s | 76,257,820 bytes | -| vanilla-extract | 1.17.4 | 20.09s | 59,366,237 bytes | -| kuma-ui | 1.5.9 | 21.61s | 67,422,085 bytes | -| panda-css | 1.3.1 | 22.01s | 62,431,065 bytes | -| chakra-ui | 3.27.0 | 29.99s | 210,122,493 bytes | -| mui | 7.3.2 | 22.21s | 94,231,958 bytes | -| devup-ui(per-file css) | 1.0.18 | 18.23s | 57,440,953 bytes | -| devup-ui(single css) | 1.0.18 | 18.35s | 57,409,008 bytes | +[Latest CI benchmark](https://github.com/dev-five-git/devup-ui/actions/runs/33239265133) on `ubuntu-latest`. All Next.js builds use the native TypeScript 7 CLI for type checking. + +Webpack values are one cold build: + +| Library | Version | Build Time | Build Size | +| --------------------------- | ------- | ---------- | ----------------- | +| 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 | + +Turbopack values are medians of six cold builds in alternating order: + +| Library | Version | Median Build Time | Build Size | +| -------------------------------------- | ------- | ----------------- | -------------------- | +| 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 | + +The Turbopack ranges overlap, so the direct-API result is effectively parity with Tailwind on this fixture. The static `.css.ts` row uses the `lite` WASM fast path; dynamic `.css.ts` modules use the full Boa evaluator and are not represented by that median. ## How it works diff --git a/packages/rsbuild-plugin/README.md b/packages/rsbuild-plugin/README.md index 3c6e8902..9f00bf49 100644 --- a/packages/rsbuild-plugin/README.md +++ b/packages/rsbuild-plugin/README.md @@ -73,19 +73,31 @@ npm install @devup-ui/webpack-plugin ## Comparison Benchmarks -Next.js Build Time and Build Size (github action - ubuntu-latest) - -| Library | Version | Build Time | Build Size | -| ---------------------- | ------- | ---------- | ----------------- | -| tailwindcss | 4.1.13 | 20.22s | 57,415,796 bytes | -| styleX | 0.15.4 | 38.97s | 76,257,820 bytes | -| vanilla-extract | 1.17.4 | 20.09s | 59,366,237 bytes | -| kuma-ui | 1.5.9 | 21.61s | 67,422,085 bytes | -| panda-css | 1.3.1 | 22.01s | 62,431,065 bytes | -| chakra-ui | 3.27.0 | 29.99s | 210,122,493 bytes | -| mui | 7.3.2 | 22.21s | 94,231,958 bytes | -| devup-ui(per-file css) | 1.0.18 | 18.23s | 57,440,953 bytes | -| devup-ui(single css) | 1.0.18 | 18.35s | 57,409,008 bytes | +[Latest CI benchmark](https://github.com/dev-five-git/devup-ui/actions/runs/33239265133) on `ubuntu-latest`. All Next.js builds use the native TypeScript 7 CLI for type checking. + +Webpack values are one cold build: + +| Library | Version | Build Time | Build Size | +| --------------------------- | ------- | ---------- | ----------------- | +| 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 | + +Turbopack values are medians of six cold builds in alternating order: + +| Library | Version | Median Build Time | Build Size | +| -------------------------------------- | ------- | ----------------- | -------------------- | +| 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 | + +The Turbopack ranges overlap, so the direct-API result is effectively parity with Tailwind on this fixture. The static `.css.ts` row uses the `lite` WASM fast path; dynamic `.css.ts` modules use the full Boa evaluator and are not represented by that median. ## How it works diff --git a/packages/vite-plugin/README.md b/packages/vite-plugin/README.md index f499707f..166b4784 100644 --- a/packages/vite-plugin/README.md +++ b/packages/vite-plugin/README.md @@ -73,19 +73,31 @@ npm install @devup-ui/webpack-plugin ## Comparison Benchmarks -Next.js Build Time and Build Size (github action - ubuntu-latest) - -| Library | Version | Build Time | Build Size | -| ---------------------- | ------- | ---------- | ----------------- | -| tailwindcss | 4.1.13 | 20.22s | 57,415,796 bytes | -| styleX | 0.15.4 | 38.97s | 76,257,820 bytes | -| vanilla-extract | 1.17.4 | 20.09s | 59,366,237 bytes | -| kuma-ui | 1.5.9 | 21.61s | 67,422,085 bytes | -| panda-css | 1.3.1 | 22.01s | 62,431,065 bytes | -| chakra-ui | 3.27.0 | 29.99s | 210,122,493 bytes | -| mui | 7.3.2 | 22.21s | 94,231,958 bytes | -| devup-ui(per-file css) | 1.0.18 | 18.23s | 57,440,953 bytes | -| devup-ui(single css) | 1.0.18 | 18.35s | 57,409,008 bytes | +[Latest CI benchmark](https://github.com/dev-five-git/devup-ui/actions/runs/33239265133) on `ubuntu-latest`. All Next.js builds use the native TypeScript 7 CLI for type checking. + +Webpack values are one cold build: + +| Library | Version | Build Time | Build Size | +| --------------------------- | ------- | ---------- | ----------------- | +| 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 | + +Turbopack values are medians of six cold builds in alternating order: + +| Library | Version | Median Build Time | Build Size | +| -------------------------------------- | ------- | ----------------- | -------------------- | +| 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 | + +The Turbopack ranges overlap, so the direct-API result is effectively parity with Tailwind on this fixture. The static `.css.ts` row uses the `lite` WASM fast path; dynamic `.css.ts` modules use the full Boa evaluator and are not represented by that median. ## How it works diff --git a/packages/webpack-plugin/README.md b/packages/webpack-plugin/README.md index 70fe3d08..83029a18 100644 --- a/packages/webpack-plugin/README.md +++ b/packages/webpack-plugin/README.md @@ -73,19 +73,31 @@ npm install @devup-ui/webpack-plugin ## Comparison Benchmarks -Next.js Build Time and Build Size (github action - ubuntu-latest) - -| Library | Version | Build Time | Build Size | -| ---------------------- | ------- | ---------- | ----------------- | -| tailwindcss | 4.1.13 | 20.22s | 57,415,796 bytes | -| styleX | 0.15.4 | 38.97s | 76,257,820 bytes | -| vanilla-extract | 1.17.4 | 20.09s | 59,366,237 bytes | -| kuma-ui | 1.5.9 | 21.61s | 67,422,085 bytes | -| panda-css | 1.3.1 | 22.01s | 62,431,065 bytes | -| chakra-ui | 3.27.0 | 29.99s | 210,122,493 bytes | -| mui | 7.3.2 | 22.21s | 94,231,958 bytes | -| devup-ui(per-file css) | 1.0.18 | 18.23s | 57,440,953 bytes | -| devup-ui(single css) | 1.0.18 | 18.35s | 57,409,008 bytes | +[Latest CI benchmark](https://github.com/dev-five-git/devup-ui/actions/runs/33239265133) on `ubuntu-latest`. All Next.js builds use the native TypeScript 7 CLI for type checking. + +Webpack values are one cold build: + +| Library | Version | Build Time | Build Size | +| --------------------------- | ------- | ---------- | ----------------- | +| 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 | + +Turbopack values are medians of six cold builds in alternating order: + +| Library | Version | Median Build Time | Build Size | +| -------------------------------------- | ------- | ----------------- | -------------------- | +| 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 | + +The Turbopack ranges overlap, so the direct-API result is effectively parity with Tailwind on this fixture. The static `.css.ts` row uses the `lite` WASM fast path; dynamic `.css.ts` modules use the full Boa evaluator and are not represented by that median. ## How it works From b35a95a869af3a02374ae1d6f5f47d1d9da1aa4f Mon Sep 17 00:00:00 2001 From: devfive Date: Sat, 29 Aug 2026 16:31:03 +0900 Subject: [PATCH 30/43] test: refresh landing benchmark snapshot --- apps/landing/src/app/Bench.tsx | 9 ++------- apps/landing/src/app/DevupUICard.tsx | 4 ++-- apps/landing/src/app/OtherCard.tsx | 4 ++-- ...ponents-benchmark-section-mobile-win32.png | Bin 151567 -> 152927 bytes 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/apps/landing/src/app/Bench.tsx b/apps/landing/src/app/Bench.tsx index d7303325..022031b8 100644 --- a/apps/landing/src/app/Bench.tsx +++ b/apps/landing/src/app/Bench.tsx @@ -77,15 +77,10 @@ export function Bench() { wordBreak="keep-all" > - Comparison Benchmarks + Comparison Bechmarks - Next.js cold build on GitHub Actions (ubuntu-latest, run - 33239265133). Devup React 1.0.40 / Next plugin 1.0.83. - - - Cards show one Webpack build. Turbopack six-run median: Devup static - .css.ts 6.47s, Devup direct APIs 6.54s, and Tailwind 6.55s. + Next.js cold build (GitHub Actions, run 33239265133) diff --git a/apps/landing/src/app/DevupUICard.tsx b/apps/landing/src/app/DevupUICard.tsx index eb795ab3..bdc399cb 100644 --- a/apps/landing/src/app/DevupUICard.tsx +++ b/apps/landing/src/app/DevupUICard.tsx @@ -36,7 +36,7 @@ export function DevupUICard() { - Next.js Build Time (single CSS) + Next.js Build Time - Build Size + Bulid Size - Build Time + Bulid Time {buildTime} @@ -56,7 +56,7 @@ export function OtherCard({ - Build Size + Bulid Size {buildSize} diff --git a/e2e/components-pages.spec.ts-snapshots/components-benchmark-section-mobile-win32.png b/e2e/components-pages.spec.ts-snapshots/components-benchmark-section-mobile-win32.png index 28c574c2f0eba0385d9aa3009bd1f52278dee450..9dce5cf1a5a0047f0b7165bf3db36dbcc89a4064 100644 GIT binary patch delta 148838 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

P-PK#8%)OWBV0j2MDlU~{_mEQQs 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 z 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

DDhjA~-^npboIiTL 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⪼iLM*H4SJu6oL63SL; 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