mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
* perf(dev): keep pairing web bundle after test file changes * fix(dev): detect web input replacement within one timestamp tick * test(dev): use real files for web replacement coverage --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
222 lines
7.1 KiB
JavaScript
222 lines
7.1 KiB
JavaScript
import {
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
renameSync,
|
|
rmSync,
|
|
statSync,
|
|
utimesSync,
|
|
writeFileSync
|
|
} from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { dirname, join } from 'node:path'
|
|
import { afterEach, expect, it } from 'vitest'
|
|
import {
|
|
captureDevWebClientInputs,
|
|
isDevWebClientFresh,
|
|
recordDevWebClientInputs
|
|
} from './dev-web-client-freshness.mjs'
|
|
|
|
const roots = []
|
|
const OLD = new Date(Date.now() - 120_000)
|
|
const BUILT = new Date(Date.now() - 60_000)
|
|
const EDITED = new Date()
|
|
|
|
afterEach(() => {
|
|
for (const root of roots.splice(0)) {
|
|
rmSync(root, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
function fixture() {
|
|
const root = mkdtempSync(join(tmpdir(), 'orca-dev-web-freshness-'))
|
|
roots.push(root)
|
|
const files = [
|
|
'vite.web.config.ts',
|
|
'src/renderer/src/app.tsx',
|
|
'src/renderer/src/app.test.tsx',
|
|
'src/renderer/src/app.spec.ts',
|
|
'src/renderer/src/main.css',
|
|
'src/shared/value.ts',
|
|
'src/shared/value.test.ts',
|
|
'src/preload/api-types.ts',
|
|
'out/web/web-index.html'
|
|
]
|
|
const directories = new Set()
|
|
for (const file of files) {
|
|
const absolute = join(root, file)
|
|
mkdirSync(dirname(absolute), { recursive: true })
|
|
writeFileSync(absolute, '')
|
|
utimesSync(absolute, OLD, OLD)
|
|
for (let directory = dirname(absolute); directory !== root; directory = dirname(directory)) {
|
|
directories.add(directory)
|
|
}
|
|
}
|
|
for (const directory of directories) {
|
|
utimesSync(directory, OLD, OLD)
|
|
}
|
|
utimesSync(join(root, 'out/web/web-index.html'), BUILT, BUILT)
|
|
return root
|
|
}
|
|
|
|
it('requires a web bundle before declaring it fresh', () => {
|
|
const root = fixture()
|
|
expect(isDevWebClientFresh(root)).toBe(true)
|
|
rmSync(join(root, 'out/web/web-index.html'))
|
|
expect(isDevWebClientFresh(root)).toBe(false)
|
|
})
|
|
|
|
it('keeps an existing bundle fresh after editing renderer or shared tests', () => {
|
|
const root = fixture()
|
|
for (const file of [
|
|
'src/renderer/src/app.test.tsx',
|
|
'src/renderer/src/app.spec.ts',
|
|
'src/shared/value.test.ts'
|
|
]) {
|
|
writeFileSync(join(root, file), 'changed test')
|
|
utimesSync(join(root, file), EDITED, EDITED)
|
|
}
|
|
expect(isDevWebClientFresh(root)).toBe(true)
|
|
})
|
|
|
|
it.each([
|
|
'src/renderer/src/app.tsx',
|
|
'src/renderer/src/main.css',
|
|
'src/shared/value.ts',
|
|
'src/preload/api-types.ts',
|
|
'vite.web.config.ts'
|
|
])('rebuilds when %s changes', (file) => {
|
|
const root = fixture()
|
|
expect(isDevWebClientFresh(root)).toBe(true)
|
|
utimesSync(join(root, file), EDITED, EDITED)
|
|
expect(isDevWebClientFresh(root)).toBe(false)
|
|
})
|
|
|
|
it.each(['add', 'delete'])('rebuilds after a production source %s', (operation) => {
|
|
const root = fixture()
|
|
const file = join(root, 'src/renderer/src/app.tsx')
|
|
if (operation === 'delete') {
|
|
rmSync(file)
|
|
} else {
|
|
writeFileSync(join(root, 'src/renderer/src/new.tsx'), '')
|
|
}
|
|
utimesSync(dirname(file), EDITED, EDITED)
|
|
expect(isDevWebClientFresh(root)).toBe(false)
|
|
})
|
|
|
|
it.each(['test.ts', 'spec.tsx', 'test.mjs', 'spec.cts'])(
|
|
'keeps a tracked bundle fresh after adding and deleting a %s file',
|
|
(suffix) => {
|
|
const root = fixture()
|
|
expect(isDevWebClientFresh(root)).toBe(true)
|
|
const file = join(root, `src/renderer/src/new.${suffix}`)
|
|
writeFileSync(file, '')
|
|
utimesSync(dirname(file), EDITED, EDITED)
|
|
expect(isDevWebClientFresh(root)).toBe(true)
|
|
rmSync(file)
|
|
expect(isDevWebClientFresh(root)).toBe(true)
|
|
}
|
|
)
|
|
|
|
it('ignores adding and removing a directory containing only tests', () => {
|
|
const root = fixture()
|
|
expect(isDevWebClientFresh(root)).toBe(true)
|
|
const directory = join(root, 'src/shared/new-tests')
|
|
mkdirSync(directory)
|
|
writeFileSync(join(directory, 'value.test.ts'), '')
|
|
expect(isDevWebClientFresh(root)).toBe(true)
|
|
rmSync(directory, { recursive: true })
|
|
expect(isDevWebClientFresh(root)).toBe(true)
|
|
})
|
|
|
|
it.each(['add', 'delete', 'rename'])(
|
|
'invalidates a tracked bundle after a production source %s with old timestamps',
|
|
(operation) => {
|
|
const root = fixture()
|
|
expect(isDevWebClientFresh(root)).toBe(true)
|
|
if (operation !== 'add') {
|
|
rmSync(join(root, 'src/renderer/src/app.tsx'))
|
|
}
|
|
if (operation !== 'delete') {
|
|
const file = join(root, 'src/renderer/src/added.tsx')
|
|
writeFileSync(file, '')
|
|
utimesSync(file, OLD, OLD)
|
|
}
|
|
utimesSync(join(root, 'src/renderer/src'), OLD, OLD)
|
|
expect(isDevWebClientFresh(root)).toBe(false)
|
|
}
|
|
)
|
|
|
|
it('invalidates a tracked bundle when a source is removed in a nested directory', () => {
|
|
const root = fixture()
|
|
const directory = join(root, 'src/shared/nested')
|
|
mkdirSync(directory)
|
|
writeFileSync(join(directory, 'value.ts'), '')
|
|
utimesSync(join(directory, 'value.ts'), OLD, OLD)
|
|
utimesSync(directory, OLD, OLD)
|
|
utimesSync(dirname(directory), OLD, OLD)
|
|
expect(isDevWebClientFresh(root)).toBe(true)
|
|
rmSync(directory, { recursive: true })
|
|
utimesSync(dirname(directory), OLD, OLD)
|
|
expect(isDevWebClientFresh(root)).toBe(false)
|
|
})
|
|
|
|
it.each(['missing', 'corrupt', 'version', 'output'])(
|
|
'falls back conservatively when the inventory is %s',
|
|
(state) => {
|
|
const root = fixture()
|
|
expect(isDevWebClientFresh(root)).toBe(true)
|
|
const marker = join(root, 'out/web/.orca-dev-web-inputs.json')
|
|
if (state === 'missing') {
|
|
rmSync(marker)
|
|
}
|
|
if (state === 'corrupt') {
|
|
writeFileSync(marker, '{')
|
|
}
|
|
if (state === 'version') {
|
|
writeFileSync(marker, JSON.stringify({ version: 0 }))
|
|
}
|
|
if (state === 'output') {
|
|
utimesSync(join(root, 'out/web/web-index.html'), OLD, OLD)
|
|
}
|
|
rmSync(join(root, 'src/renderer/src/app.tsx'))
|
|
utimesSync(join(root, 'src/renderer/src'), EDITED, EDITED)
|
|
expect(isDevWebClientFresh(root)).toBe(false)
|
|
}
|
|
)
|
|
|
|
it('adopts a fresh replacement output without reusing its previous inventory', () => {
|
|
const root = fixture()
|
|
expect(isDevWebClientFresh(root)).toBe(true)
|
|
writeFileSync(join(root, 'src/renderer/src/app.tsx'), 'updated')
|
|
utimesSync(join(root, 'src/renderer/src/app.tsx'), BUILT, BUILT)
|
|
utimesSync(join(root, 'out/web/web-index.html'), EDITED, EDITED)
|
|
expect(isDevWebClientFresh(root)).toBe(true)
|
|
})
|
|
|
|
it('retains the inputs from before a build so concurrent edits invalidate its output', () => {
|
|
const root = fixture()
|
|
const inputs = captureDevWebClientInputs(root)
|
|
writeFileSync(join(root, 'src/shared/value.ts'), 'changed while building')
|
|
utimesSync(join(root, 'out/web/web-index.html'), EDITED, EDITED)
|
|
recordDevWebClientInputs(root, inputs)
|
|
expect(isDevWebClientFresh(root)).toBe(false)
|
|
})
|
|
|
|
it('detects same-size atomic replacement with a restored mtime', () => {
|
|
const root = fixture()
|
|
const file = join(root, 'src/shared/value.ts')
|
|
const replacement = join(root, 'replacement.ts')
|
|
expect(isDevWebClientFresh(root)).toBe(true)
|
|
writeFileSync(replacement, '')
|
|
utimesSync(replacement, OLD, OLD)
|
|
const originalIdentity = statSync(file, { bigint: true })
|
|
const replacementIdentity = statSync(replacement, { bigint: true })
|
|
expect([replacementIdentity.dev, replacementIdentity.ino]).not.toEqual([
|
|
originalIdentity.dev,
|
|
originalIdentity.ino
|
|
])
|
|
renameSync(replacement, file)
|
|
utimesSync(dirname(file), OLD, OLD)
|
|
expect(isDevWebClientFresh(root)).toBe(false)
|
|
})
|