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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .changepacks/changepack_log_atom_hoist_names.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
"changes": {
"bindings/devup-ui-wasm/package.json": "Patch",
"packages/next-plugin/package.json": "Patch",
"packages/plugin-utils/package.json": "Patch",
"packages/rsbuild-plugin/package.json": "Patch",
"packages/vite-plugin/package.json": "Patch",
"packages/webpack-plugin/package.json": "Patch"
},
"note": "Enable atomHoist by default at two routes in Webpack, Rsbuild and Next Webpack, with numeric zero opting out and no effect in singleCss mode. Keep Vite and Next Turbopack in per-file mode until their shared-slot reservation lands, warning once for explicit positive atomHoist requests. Allocate shared and file-local short counter names through deterministic reserved slots instead of content names; preserve frozen route plans, dynamic !important, local keyframes, first-value responsive tokens, shared CSS updates and persisted placement. Bump the naming cache generation. No new build errors.",
"note": "Enable atomHoist by default at two boundaries in Vite, Webpack, Rsbuild and Next Webpack, with numeric zero opting out and no effect in singleCss mode. Keep Next Turbopack in per-file mode until its shared-slot reservation lands, warning once for explicit positive atomHoist requests. Allocate shared and file-local short counter names through deterministic reserved slots instead of content names; preserve frozen route plans, dynamic !important, local keyframes, first-value responsive tokens, shared CSS updates and persisted placement. Bump the naming cache generation. Stabilize Vite dev-sheet module identity with independent content-write revisions, preserving cache busting for changed CSS text and unchanged build paths. Track inherited configuration inputs, serialize refreshes and drain owned engine work before releasing shared build ownership. Reuse exact-input semantic extraction only in eligible exclusive serve sessions, registering dependencies on every request without retaining CSS snapshots or replaying writes; builds continue extracting normally. No new build errors.",
"date": "2026-10-01T00:00:00.000Z"
}
7 changes: 7 additions & 0 deletions .changepacks/changepack_log_bun_latest_coverage.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"changes": {
"packages/eslint-plugin/package.json": "Patch"
},
"note": "Fix sparse trailing-null autofixes to preserve authored values, complete parenthesized syntax and earlier holes using token-based edit boundaries while removing the maximal trailing run of nulls and holes only when the last element is canonical literal null; preserve regular-expression and BigInt literals without validating regex syntax, preserve existing comment and trailing-comma handling, remove an unreachable AST guard and restore complete behavior coverage on the latest Bun without exclusions or new build errors.",
"date": "2026-10-22T00:00:00.000Z"
}
8 changes: 8 additions & 0 deletions .changepacks/changepack_log_vite_shared_slots.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"changes": {
"packages/plugin-utils/package.json": "Patch",
"packages/vite-plugin/package.json": "Patch"
},
"note": "Reserve short shared class and CSS-variable slots before Vite extraction across build, serve, and shared environments. Enable atom hoisting by default at two distinct physical module boundaries plus reachable dynamic targets, with zero opting out and singleCss unchanged. Install the closed-inventory canonical and reach plan before scratch and freeze it for the build lifetime, including watch updates. Discover static physical HTML inputs with a Vite-only build-time parse5 parser, keeping it outside non-Vite plugin and loader graphs, and refresh only dirty revisions without styling runtime or new intended source build-error categories.",
"date": "2026-10-18T00:00:00.000Z"
}
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,7 @@ jobs:
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
files: ./coverage/lcov.info
files: ./coverage/root/lcov.info,./coverage/vite/lcov.info

# Pages deployment: the only job with `pages` and `id-token` write, and it
# only runs for a push to main whose validation passed.
Expand Down
8 changes: 7 additions & 1 deletion bun.lock

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

36 changes: 36 additions & 0 deletions bun.matrix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
export {}

function cartesianProduct<T extends any[][]>(arrays: T) {
return arrays.reduce(
(acc, curr) => acc.flatMap((x) => curr.map((y) => [...x, y])),
[[]],
)
}

function createTestMatrix<T extends Record<string, any[]>>(
optionsMap: T,
): {
[K in keyof T]: T[K] extends (infer U)[] ? U : never
}[] {
const keys = Object.keys(optionsMap)
const values = Object.values(optionsMap)

return cartesianProduct(values).map<{
[K in keyof T]: T[K] extends (infer U)[] ? U : never
}>(
(combination) =>
Object.fromEntries(keys.map((key, i) => [key, combination[i]])) as {
[K in keyof T]: T[K] extends (infer U)[] ? U : never
},
)
}

globalThis.createTestMatrix = createTestMatrix

declare global {
function createTestMatrix<T extends Record<string, any[]>>(
optionsMap: T,
): {
[K in keyof T]: T[K] extends (infer U)[] ? U : never
}[]
}
37 changes: 2 additions & 35 deletions bun.setup.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import './bun.matrix'

import { afterEach, beforeEach, spyOn } from 'bun:test'

import { resetWasmForTesting } from './packages/next-plugin/src/wasm'
Expand Down Expand Up @@ -27,38 +29,3 @@ function resetTestState() {

beforeEach(resetTestState)
afterEach(resetTestState)

function cartesianProduct<T extends any[][]>(arrays: T) {
return arrays.reduce(
(acc, curr) => acc.flatMap((x) => curr.map((y) => [...x, y])),
[[]],
)
}

function createTestMatrix<T extends Record<string, any[]>>(
optionsMap: T,
): {
[K in keyof T]: T[K] extends (infer U)[] ? U : never
}[] {
const keys = Object.keys(optionsMap)
const values = Object.values(optionsMap)

return cartesianProduct(values).map<{
[K in keyof T]: T[K] extends (infer U)[] ? U : never
}>(
(combination) =>
Object.fromEntries(keys.map((key, i) => [key, combination[i]])) as {
[K in keyof T]: T[K] extends (infer U)[] ? U : never
},
)
}

globalThis.createTestMatrix = createTestMatrix

declare global {
function createTestMatrix<T extends Record<string, any[]>>(
optionsMap: T,
): {
[K in keyof T]: T[K] extends (infer U)[] ? U : never
}[]
}
2 changes: 2 additions & 0 deletions bunfig.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
[test]
root = "packages"
pathIgnorePatterns = ["**/vite-plugin/**"]
preload = ["./bun.setup.ts", "./packages/bun-plugin/src/index.ts", "bun-test-env-dom"]
# `bun run test` passes `--coverage`; enabling it here would hold focused `bun test <file>` runs to the full-suite threshold.
coverageReporter = ["text", "lcov"]
# Bun ignores unknown keys without a warning; packages/__tests__/coverage-threshold.test.ts checks this gate.
coverageThreshold = { lines = 1.0, functions = 1.0, statements = 1.0 }
coverageDir = "coverage/root"
coverageSkipTestFiles = true
# Generated artifacts are tested separately by the packed-consumer release gate.
coveragePathIgnorePatterns = ["**/pkg/**", "**/dist/**"]
8 changes: 8 additions & 0 deletions bunfig.vite.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[test]
root = "packages/vite-plugin"
preload = ["./bun.matrix.ts"]
coverageReporter = ["text", "lcov"]
coverageThreshold = { lines = 1.0, functions = 1.0, statements = 1.0 }
coverageSkipTestFiles = true
coveragePathIgnorePatterns = ["**/pkg/**", "**/dist/**"]
coverageDir = "coverage/vite"
46 changes: 39 additions & 7 deletions e2e/landing-zero-runtime.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ test.describe('Landing Page - Zero Runtime Validation', () => {

test('no runtime CSS-in-JS globals are present', async ({ page }) => {
const runtimeGlobals = await page.evaluate(() => {
const win = window as Record<string, unknown>
const win = window
return {
// Common CSS-in-JS runtime indicators
__devup__: '__devup__' in win,
Expand Down Expand Up @@ -147,15 +147,47 @@ test.describe('Landing Page - Zero Runtime Validation', () => {
.evaluate((element) => Array.from(element.classList))

expect(headingClasses.length).toBeGreaterThan(1)
const atomicClasses = headingClasses.filter((className) =>
/^(?:[a-z]-)?[a-z0-9_]{1,3}$/i.test(className),
)
expect(atomicClasses.length).toBeGreaterThan(1)
const isCounter = (name: string): boolean => {
// num_to_nm_base: 27 initial symbols, 37 continuation symbols, ad escape.
const raw = name.replace(/a-d$/, 'ad')
if (!/^[a-z_]/.test(raw) || /[^a-z0-9_]/.test(raw)) return false
if (raw.replace(/ad$/, 'a-d') !== name) return false
let value = 'abcdefghijklmnopqrstuvwxyz_'.indexOf(raw.charAt(0))
for (const digit of raw.slice(1)) {
value =
37 * value +
27 +
'abcdefghijklmnopqrstuvwxyz0123456789_'.indexOf(digit)
// The compiler's counter is a wasm32 usize, not a fixed string length.
if (value > 0xffffffff) return false
}
return true
}
const isAtomicClass = (className: string): boolean => {
if (
/^typo-[a-z0-9]+$/i.test(className) ||
/^(?:css|emotion|sc|styled|stitches)-[a-z0-9]+/i.test(className)
)
return false
return (
isCounter(className) ||
Array.from(className).some(
(character, index) =>
character === '-' &&
isCounter(className.slice(0, index)) &&
isCounter(className.slice(index + 1)),
)
)
}
const atomicClasses = headingClasses.filter(isAtomicClass)
expect(
atomicClasses.length,
`Expected multiple compiled atoms, received: ${headingClasses.join(' ')}`,
).toBeGreaterThan(1)
expect(
headingClasses.every(
(className) =>
/^(?:[a-z]-)?[a-z0-9_]{1,3}$/i.test(className) ||
/^typo-[a-z0-9]+$/i.test(className),
isAtomicClass(className) || /^typo-[a-z0-9]+$/i.test(className),
),
`Expected compact base-37 classes, received: ${headingClasses.join(' ')}`,
).toBe(true)
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"scripts": {
"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 --coverage",
"test": "cargo tarpaulin --out xml --out stdout --all-targets --engine llvm $(node -p \"process.env.CI ? '--fail-under 100' : ''\") && bun run test:bun",
"test:bun": "node -e \"require('node:fs').rmSync('coverage/root', { recursive: true, force: true })\" && bun test --coverage && bun run test:vite",
"test:vite": "node -e \"require('node:fs').rmSync('coverage/vite', { recursive: true, force: true })\" && bun --config=./bunfig.vite.toml test --coverage",
"test:e2e": "bunx playwright test",
"test:e2e:next": "LANDING_BUILD_MODE=next bunx playwright test",
"test:e2e:rsc-css": "bunx playwright test -c playwright.rsc-css.config.ts",
Expand Down
37 changes: 35 additions & 2 deletions packages/__tests__/coverage-threshold.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ const manifest: unknown = await Bun.file(resolve(root, 'package.json')).json()
assert(
typeof manifest === 'object' && manifest !== null && 'scripts' in manifest,
)
const { scripts } = manifest
assert(typeof scripts === 'object' && scripts !== null)
const { scripts: scriptValues } = manifest
assert(typeof scriptValues === 'object' && scriptValues !== null)
const scripts = scriptValues

function script(name: string): string {
const command: unknown = Reflect.get(scripts, name)
Expand All @@ -36,6 +37,17 @@ function bunTestCommands(
})
}

function configPath(words: readonly string[]): string | undefined {
for (const [index, word] of words.entries()) {
if (word.startsWith('--config=')) return word.slice('--config='.length)
if (word === '--config') {
const path = words[index + 1]
if (typeof path === 'string') return path
}
}
return undefined
}

// `root` and `preload` name repository files; every other option applies unchanged.
const fixtureConfig = bunfig.replace(/^(?:root|preload) = .*$/gm, '')
const fixtureTest = `import { expect, test } from 'bun:test'
Expand Down Expand Up @@ -78,6 +90,27 @@ it('collects coverage in every Bun test run of the test script', () => {
expect(commands.filter((words) => !words.includes('--coverage'))).toEqual([])
})

it.each([
['--config=PATH', ['bun', '--config=./bunfig.vite.toml', 'test']],
['--config PATH', ['bun', '--config', './bunfig.vite.toml', 'test']],
])('finds explicit config paths written as %s', (_, words) => {
expect(configPath(words)).toBe('./bunfig.vite.toml')
})

it('requires every explicit Bun test config to be fully covered', async () => {
const paths = bunTestCommands('test')
.map(configPath)
.filter((path): path is string => path !== undefined)
expect(paths).not.toEqual([])

for (const path of paths) {
const config = Bun.TOML.parse(await Bun.file(resolve(root, path)).text())
expect(config).toMatchObject({
test: { coverageThreshold: { lines: 1, functions: 1, statements: 1 } },
})
}
})

it.each([
[
'an uncovered line',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,25 @@ describe.each(['css' /* 'globalCss', 'keyframes'*/])(
})
ruleTester.run('css-utils-literal-only rule', cssUtilsLiteralOnly, {
valid: [
...[
"const f = ({'p': n}) => n; css({w: f({p: 1})});",
"const k = 'p'; const f = ({[k]: n}) => n; css({w: f({p: 1})});",
'const L = [1]; unknown(L[0]); css({w: L[0]});',
'const L = [1]; unknown(L[9]); css({w: L[0]});',
'const L = {p: 1}; export const holder = [L]; css({w: L.p});',
'const L = {p: 1}; module.exports.token = L; css({w: L.p});',
"const L = {p: 1}; module['exports'].token = L; css({w: L.p});",
'const N = 4; function f() { return N } css({w: f()});',
'const helper = () => 4; function f() { return helper() } css({w: f()});',
'const helper = function() { return 4 }; function f() { return helper() } css({w: f()});',
].map((use) => ({
code: `import { css } from "@devup-ui/react";\n${use}`,
filename: 'src/app/page.tsx',
})),
{
code: `import { 'create' as create } from '@stylexjs/stylex'; create({a: {width: 1}});`,
filename: 'src/app/page.tsx',
},
{
code: `import { ${code} } from "@devup-ui/react";\n${code}({w: 1})`,
filename: 'src/app/page.tsx',
Expand Down Expand Up @@ -145,6 +164,26 @@ describe.each(['css' /* 'globalCss', 'keyframes'*/])(
},
],
invalid: [
...[
"const f = ({'normalize': n}) => n; css({w: f({normalize: 1})});",
'const L = [{p: 1}]; unknown(L[0]); css({w: L[0].p});',
'const A = [{p: 1}]; const L = [1, ...A]; unknown(L[0]); css({w: A[0].p});',
'const L = {p: 1}; unknown([L]); css({w: L.p});',
'const L = {p: 1}; other.exports.token = L; css({w: L.p});',
'const helper = () => Date.now(); function f() { return helper() } css({w: f()});',
'class Token {} function f() { return Token } css({w: f()});',
].map((use) => ({
code: `import { css } from "@devup-ui/react";\n${use}`,
filename: 'src/app/page.tsx',
output: null,
errors: [{ messageId: 'cssUtilsLiteralOnly' as const }],
})),
{
code: `import { 'create' as create } from '@stylexjs/stylex'; let v = 1; create({a: {width: v}});`,
filename: 'src/app/page.tsx',
output: null,
errors: [{ messageId: 'cssUtilsLiteralOnly' }],
},
...[
[
`export function C() { let fade = keyframes({ from: { opacity: 0 } }); return css({ animationName: fade }); }`,
Expand Down
Loading
Loading