Skip to content
Open
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
12 changes: 12 additions & 0 deletions .changepacks/changepack_log_w74-native-engine.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"changes": {
"packages/plugin-utils/package.json": "Patch",
"packages/webpack-plugin/package.json": "Patch",
"packages/rsbuild-plugin/package.json": "Patch",
"packages/vite-plugin/package.json": "Patch",
"packages/bun-plugin/package.json": "Patch",
"packages/next-plugin/package.json": "Patch"
},
"note": "Add shared native/WASM compiler selection, structural compiler types and exact native platform-load diagnostics for build plugins.",
"date": "2026-10-30T17:29:00.000Z"
}
1 change: 1 addition & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
pull_request:
branches:
- main
- feat/napi-binding

# Pull requests and every job below run read-only. Only the `release` and
# `deploy-pages` jobs get write permissions, and they run on pushes to main
Expand Down
12 changes: 11 additions & 1 deletion apps/vinext-rsc-css-repro/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,19 @@ import { DevupUI } from '@devup-ui/vite-plugin'
import vinext from 'vinext'
import { defineConfig } from 'vite'

const engine = process.env.DEVUP_ENGINE
switch (engine) {
case undefined:
case 'native':
case 'wasm':
break
default:
throw new TypeError(`Invalid DEVUP_ENGINE: ${JSON.stringify(engine)}`)
}

export default defineConfig({
plugins: [
DevupUI({ singleCss: process.env.DEVUP_SINGLE_CSS === '1' }),
DevupUI({ engine, singleCss: process.env.DEVUP_SINGLE_CSS === '1' }),
vinext(),
],
})
93 changes: 40 additions & 53 deletions bun.lock

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion bun.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import './bun.matrix'
import { afterEach, beforeEach, spyOn } from 'bun:test'

import { resetWasmForTesting } from './packages/next-plugin/src/wasm'
import { createModuleResolver } from './packages/plugin-utils/src'
import { createModuleResolver, loadCompiler } from './packages/plugin-utils/src'

const { resetStateForTesting, setDebug, setModuleResolver } =
await import('./bindings/devup-ui-wasm/pkg/index.js')
const testModuleResolver = createModuleResolver()
const nativeCompiler = loadCompiler()
const nativeBunPlugin = Bun.plugin
export const bunRegistration = Object.assign(spyOn(Bun, 'plugin'), {
clearAll: nativeBunPlugin.clearAll,
Expand All @@ -21,6 +22,9 @@ try {
}

function resetTestState() {
nativeCompiler.resetStateForTesting()
nativeCompiler.setDebug(true)
nativeCompiler.setModuleResolver(testModuleResolver)
resetStateForTesting()
setDebug(true)
setModuleResolver(testModuleResolver)
Expand Down
99 changes: 99 additions & 0 deletions e2e/binding/package/build-host.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { execFileSync } from 'node:child_process'
import { resolve } from 'node:path'
import { fileURLToPath } from 'node:url'

import { targets } from './matrix.mjs'

const root = fileURLToPath(new URL('../../../', import.meta.url))
const actualHost = {
platform: process.platform,
arch: process.arch,
report: process.report.getReport,
}
const actualIo = {
run: execFileSync,
now: () => performance.now(),
emit: (record) => console.info(JSON.stringify(record)),
}

export class HostBuildTargetError extends Error {
constructor(host, cause) {
super(
`Unsupported or indeterminate native host: ${host.platform}/${host.arch}`,
{ cause },
)
this.name = 'HostBuildTargetError'
this.platform = host.platform
this.arch = host.arch
}
}

export function selectHostTarget(host = actualHost) {
const candidates = targets.filter(
(row) => row.os === host.platform && row.cpu === host.arch,
)
if (candidates.length === 0) throw new HostBuildTargetError(host)
if (host.platform !== 'linux') return candidates[0]
try {
const report = host.report()
if (
typeof report !== 'object' ||
report === null ||
typeof report.header !== 'object' ||
report.header === null
) {
throw new HostBuildTargetError(host)
}
const libc = report.header.glibcVersionRuntime ? 'glibc' : 'musl'
return candidates.find((row) => row.libc === libc)
} catch (cause) {
if (cause instanceof HostBuildTargetError) throw cause
throw new HostBuildTargetError(host, cause)
}
}

export function buildHost(host = actualHost, io = actualIo) {
const row = selectHostTarget(host)
if (!row) throw new HostBuildTargetError(host)
const context = {
event: 'devup.host-native-build',
host: `${host.platform}/${host.arch}`,
target: row.target,
suffix: row.suffix,
node: process.version,
job: process.env.GITHUB_JOB ?? null,
run: process.env.GITHUB_RUN_ID ?? null,
}
const started = io.now()
io.emit({ ...context, phase: 'start' })
let status = 'failed'
try {
io.run(
process.execPath,
[
resolve(root, 'bindings/devup-ui-binding/build.mjs'),
'--target',
row.target,
],
{
cwd: root,
env: process.env,
stdio: 'inherit',
},
)
status = 'success'
} finally {
io.emit({
...context,
phase: 'finish',
status,
elapsedMs: io.now() - started,
})
}
}

if (
process.argv[1] &&
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
)
buildHost()
166 changes: 166 additions & 0 deletions e2e/binding/package/build-host.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import assert from 'node:assert/strict'
import { resolve } from 'node:path'
import { test } from 'node:test'
import { fileURLToPath } from 'node:url'

import {
buildHost,
HostBuildTargetError,
selectHostTarget,
} from './build-host.mjs'
import { targets } from './matrix.mjs'

const root = fileURLToPath(new URL('../../../', import.meta.url))

for (const row of targets) {
test(`selects canonical host triple for ${row.suffix}`, () => {
// Given
let reports = 0
const host = {
platform: row.os,
arch: row.cpu,
report() {
reports++
return {
header: row.libc === 'glibc' ? { glibcVersionRuntime: '2.39' } : {},
}
},
}
// When
const selected = selectHostTarget(host)
// Then
assert.equal(selected, row)
assert.equal(reports, row.os === 'linux' ? 1 : 0)
})
}

for (const [platform, arch] of [
['freebsd', 'x64'],
['win32', 'ia32'],
['linux', 'riscv64'],
]) {
test(`rejects unsupported ${platform}/${arch} before any spawn`, () => {
// Given
const calls = []
const host = { platform, arch, report: () => calls.push('report') }
const io = {
run: () => calls.push('run'),
now: () => calls.push('clock'),
emit: () => calls.push('emit'),
}
// When / Then
assert.throws(() => buildHost(host, io), HostBuildTargetError)
assert.deepEqual(calls, [])
})
}

test('preserves a Linux report failure without spawning', () => {
// Given
const failure = new Error('report unavailable')
const calls = []
const host = {
platform: 'linux',
arch: 'arm64',
report() {
throw failure
},
}
const io = {
run: () => calls.push('run'),
now: () => 0,
emit: () => calls.push('emit'),
}
// When / Then
assert.throws(
() => buildHost(host, io),
(error) => error instanceof HostBuildTargetError && error.cause === failure,
)
assert.deepEqual(calls, [])
})

for (const report of [null, '', {}, { header: null }, { header: 'bad' }]) {
test(`rejects indeterminate report ${JSON.stringify(report)}`, () => {
// Given
const host = { platform: 'linux', arch: 'x64', report: () => report }
// When / Then
assert.throws(() => selectHostTarget(host), HostBuildTargetError)
})
}

test('delegates to the existing driver and emits measured completion', () => {
// Given
const calls = []
const records = []
let ticks = 0
const host = {
platform: 'win32',
arch: 'arm64',
report() {
assert.fail('non-Linux host')
},
}
const io = {
run: (...args) => calls.push(args),
now: () => 100 + ticks++ * 75,
emit: (record) => records.push(record),
}
// When
buildHost(host, io)
// Then
assert.deepEqual(calls, [
[
process.execPath,
[
resolve(root, 'bindings/devup-ui-binding/build.mjs'),
'--target',
'aarch64-pc-windows-msvc',
],
{
cwd: root,
env: process.env,
stdio: 'inherit',
},
],
])
assert.equal(records[0].phase, 'start')
assert.deepEqual(records[1], {
...records[0],
phase: 'finish',
status: 'success',
elapsedMs: 75,
})
assert.equal(records[1].host, 'win32/arm64')
assert.equal(records[1].target, 'aarch64-pc-windows-msvc')
})

test('propagates driver failure and emits a measured failed finish', () => {
// Given
const failure = new Error('driver failed')
const records = []
let ticks = 0
const host = {
platform: 'darwin',
arch: 'x64',
report() {
assert.fail('non-Linux host')
},
}
const io = {
run() {
throw failure
},
now: () => ticks++ * 25,
emit: (record) => records.push(record),
}
// When / Then
assert.throws(
() => buildHost(host, io),
(error) => error === failure,
)
assert.deepEqual(records[1], {
...records[0],
phase: 'finish',
status: 'failed',
elapsedMs: 25,
})
})
Loading
Loading