fix(web): verify current GUI updates end to end

This commit is contained in:
NI0317
2026-07-29 11:22:48 +08:00
parent d8004e9956
commit cd88a339fa
27 changed files with 438 additions and 89 deletions
+42
View File
@@ -0,0 +1,42 @@
import { mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { expect, it } from 'vitest'
import type { TsdownBundle } from 'tsdown'
import { watchClientPlugins } from './dev-web.ts'
it('rebuilds a client-plugin bundle after its source changes', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-dev-web-watch-'))
let bundles: TsdownBundle[] = []
try {
await symlink(join(import.meta.dirname, '..', 'node_modules'), join(root, 'node_modules'), 'dir')
await writeFile(join(root, 'package.json'), JSON.stringify({ name: '@dsh-test/dev-web-watch', private: true, type: 'module' }))
await writeFile(join(root, 'tsdown.config.ts'), `
import { defineConfig } from 'tsdown'
export default defineConfig({
entry: { client: 'src.ts' }, outDir: 'lib', format: 'cjs', platform: 'browser', dts: false, clean: false,
outputOptions: { entryFileNames: 'client.js' },
})
`)
const sourcePath = join(root, 'src.ts')
const bundlePath = join(root, 'lib/client.js')
await writeFile(sourcePath, 'export const version = "watch-v1"\n')
bundles = await watchClientPlugins(root, ['.'], 50)
await expect.poll(async () => {
try {
return (await readFile(bundlePath, 'utf8')).includes('watch-v1')
} catch {
return false
}
}, { timeout: 10_000 }).toBe(true)
await new Promise(resolve => setTimeout(resolve, 1_000))
await writeFile(sourcePath, `export const version = "watch-v2-${'x'.repeat(100)}"\n`)
await expect.poll(async () => (await readFile(bundlePath, 'utf8')).includes('watch-v2-'), {
timeout: 10_000,
}).toBe(true)
} finally {
for (const bundle of bundles) await bundle[Symbol.asyncDispose]()
await rm(root, { recursive: true, force: true })
}
}, 20_000)
+53 -34
View File
@@ -18,9 +18,10 @@
* keys under each package's file config, and no package config defines it).
*/
import { globSync, readFileSync } from 'node:fs'
import { dirname, join, sep } from 'node:path'
import { fileURLToPath } from 'node:url'
import { dirname, join, resolve, sep } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { build } from 'tsdown'
import type { TsdownBundle } from 'tsdown'
const repoRoot = fileURLToPath(new URL('..', import.meta.url))
@@ -29,46 +30,64 @@ const repoRoot = fileURLToPath(new URL('..', import.meta.url))
* whose package.json carries `dshClient` with platform "web" is a client
* plugin bundle emitter. Scanned once at startup — a package added while
* watching means restarting this script.
* @param root - repository root containing the grouped package directories.
* @returns workspace-relative plugin package directories.
*/
function discoverPluginDirs(): string[] {
export function discoverPluginDirs(root = repoRoot): string[] {
const dirs: string[] = []
for (const manifestPath of globSync('packages/*/*/package.json', { cwd: repoRoot }).sort()) {
const manifest = JSON.parse(readFileSync(join(repoRoot, manifestPath), 'utf8')) as { dshClient?: { platform?: unknown } }
for (const manifestPath of globSync('packages/*/*/package.json', { cwd: root }).sort()) {
const manifest = JSON.parse(readFileSync(join(root, manifestPath), 'utf8')) as { dshClient?: { platform?: unknown } }
if (manifest.dshClient?.platform === 'web') dirs.push(dirname(manifestPath).split(sep).join('/'))
}
return dirs
}
const PLUGIN_DIRS = discoverPluginDirs()
if (PLUGIN_DIRS.length === 0) {
console.error('dev-web: no dshClient (platform "web") packages found under packages/')
process.exit(1)
/**
* Start the tsdown watch build used by `pnpm run dev:web`.
* @param root - repository or fixture root passed to tsdown.
* @param pluginDirs - workspace-relative package directories to watch.
* @param pollInterval - optional source-watcher polling interval in milliseconds.
* @returns live bundles whose async disposers stop every watcher.
*/
export async function watchClientPlugins(
root: string,
pluginDirs: readonly string[],
pollInterval?: number,
): Promise<TsdownBundle[]> {
return build({
cwd: root,
workspace: [...pluginDirs],
watch: true,
...pollInterval !== undefined
? { inputOptions: { watch: { watcher: { usePolling: true, pollInterval } } } }
: {},
})
}
const args = process.argv.slice(2)
const pollArg = args.find(a => a === '--poll' || a.startsWith('--poll='))
if (args.some(a => a !== pollArg)) {
console.error('dev-web: usage: tsx scripts/dev-web.ts [--poll[=ms]]')
process.exit(1)
}
const pollInterval = pollArg === undefined ? undefined : Number(pollArg.split('=')[1] ?? '500')
if (pollInterval !== undefined && (!Number.isInteger(pollInterval) || pollInterval <= 0)) {
console.error(`dev-web: invalid --poll interval "${pollArg ?? ''}"`)
process.exit(1)
}
const invokedPath = process.argv[1]
const isMain = invokedPath !== undefined && import.meta.url === pathToFileURL(resolve(invokedPath)).href
if (isMain) {
const pluginDirs = discoverPluginDirs()
if (pluginDirs.length === 0) {
console.error('dev-web: no dshClient (platform "web") packages found under packages/')
process.exit(1)
}
await build({
cwd: repoRoot,
workspace: PLUGIN_DIRS,
watch: true,
// Rolldown watch options ride through inputOptions (tsdown has no watcher
// tuning of its own); polling is opt-in for network mounts without inotify.
...pollInterval !== undefined
? { inputOptions: { watch: { watcher: { usePolling: true, pollInterval } } } }
: {},
})
console.log(
`dev-web: watching ${String(PLUGIN_DIRS.length)} dshClient plugin packages`
+ `${pollInterval !== undefined ? ` (polling ${String(pollInterval)}ms)` : ''}:\n ${PLUGIN_DIRS.join('\n ')}`,
)
const args = process.argv.slice(2)
const pollArg = args.find(a => a === '--poll' || a.startsWith('--poll='))
if (args.some(a => a !== pollArg)) {
console.error('dev-web: usage: tsx scripts/dev-web.ts [--poll[=ms]]')
process.exit(1)
}
const pollInterval = pollArg === undefined ? undefined : Number(pollArg.split('=')[1] ?? '500')
if (pollInterval !== undefined && (!Number.isInteger(pollInterval) || pollInterval <= 0)) {
console.error(`dev-web: invalid --poll interval "${pollArg ?? ''}"`)
process.exit(1)
}
await watchClientPlugins(repoRoot, pluginDirs, pollInterval)
console.log(
`dev-web: watching ${String(pluginDirs.length)} dshClient plugin packages`
+ `${pollInterval !== undefined ? ` (polling ${String(pollInterval)}ms)` : ''}:\n ${pluginDirs.join('\n ')}`,
)
}