mirror of
https://github.com/anomalyco/opentui.git
synced 2026-09-19 01:26:03 +08:00
558 lines
20 KiB
TypeScript
558 lines
20 KiB
TypeScript
import { spawnSync, type SpawnSyncReturns } from "node:child_process"
|
|
import { existsSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs"
|
|
import { tmpdir } from "node:os"
|
|
import { dirname, join, relative, resolve } from "node:path"
|
|
import process from "node:process"
|
|
import { fileURLToPath } from "node:url"
|
|
|
|
import { requireNode26 } from "../../../scripts/node26.mjs"
|
|
|
|
interface PackageJson {
|
|
name: string
|
|
version: string
|
|
}
|
|
|
|
const __filename = fileURLToPath(import.meta.url)
|
|
const __dirname = dirname(__filename)
|
|
const rootDir = resolve(__dirname, "..")
|
|
const distDir = join(rootDir, "dist")
|
|
const args = new Set(process.argv.slice(2))
|
|
const keepTemp = args.has("--keep-temp")
|
|
const skipBuild = args.has("--skip-build")
|
|
const nodePath = requireNode26()
|
|
|
|
const packageJson = JSON.parse(readFileSync(join(rootDir, "package.json"), "utf8")) as PackageJson
|
|
const nativePackageName = `${packageJson.name}-${process.platform}-${process.arch}`
|
|
const nativePackageDir = join(rootDir, "node_modules", nativePackageName)
|
|
|
|
const declarationPaths = ["index.d.ts", "node-assets.d.ts", "testing.d.ts", "lib/tree-sitter/parser.worker.d.ts"]
|
|
|
|
function runCommand(
|
|
command: string,
|
|
commandArgs: string[],
|
|
cwd: string,
|
|
errorMessage: string,
|
|
options: { stdio?: "inherit" | "pipe"; timeout?: number } = {},
|
|
): SpawnSyncReturns<Buffer> {
|
|
const result = spawnSync(command, commandArgs, {
|
|
cwd,
|
|
stdio: options.stdio ?? "inherit",
|
|
timeout: options.timeout,
|
|
})
|
|
|
|
if (result.error) {
|
|
throw new Error(`${errorMessage}: ${result.error.message}`)
|
|
}
|
|
|
|
if (result.status !== 0) {
|
|
throw new Error(errorMessage)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
function runCommandExpectFailure(
|
|
command: string,
|
|
commandArgs: string[],
|
|
cwd: string,
|
|
errorMessage: string,
|
|
): SpawnSyncReturns<Buffer> {
|
|
const result = spawnSync(command, commandArgs, {
|
|
cwd,
|
|
stdio: "pipe",
|
|
})
|
|
|
|
if (result.error) {
|
|
throw new Error(`${errorMessage}: ${result.error.message}`)
|
|
}
|
|
|
|
if (result.status === 0) {
|
|
throw new Error(errorMessage)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
function ensureBuildArtifacts(): void {
|
|
if (!skipBuild) {
|
|
runCommand("bun", ["run", "build"], rootDir, "Dist test build failed")
|
|
}
|
|
|
|
if (!existsSync(distDir)) {
|
|
throw new Error(`Missing dist directory at ${distDir}. Run bun run build first.`)
|
|
}
|
|
|
|
if (!existsSync(nativePackageDir)) {
|
|
throw new Error(`Missing native package directory at ${nativePackageDir}. Run bun run build first.`)
|
|
}
|
|
if (!existsSync(join(nativePackageDir, "LICENSE-GHOSTTY"))) {
|
|
throw new Error("Native package is missing the Ghostty and Unicode license notices")
|
|
}
|
|
const leakedGhosttyFiles = readdirSync(nativePackageDir).filter(
|
|
(name) => name.includes("ghostty-vt") || name.endsWith(".a") || name.endsWith(".lib"),
|
|
)
|
|
if (leakedGhosttyFiles.length > 0) {
|
|
throw new Error(`Native package contains unbundled Ghostty artifacts: ${leakedGhosttyFiles.join(", ")}`)
|
|
}
|
|
const leakedSymbols = readdirSync(nativePackageDir).filter((name) => /\.(debug|pdb|dSYM)$/.test(name))
|
|
if (leakedSymbols.length)
|
|
throw new Error(`Native package contains separate debug symbols: ${leakedSymbols.join(", ")}`)
|
|
|
|
if (process.platform === "linux") {
|
|
const nativeLibrary = join(nativePackageDir, "libopentui.so")
|
|
const elf = runCommand(
|
|
"readelf",
|
|
["--wide", "--section-headers", "--dyn-syms", nativeLibrary],
|
|
rootDir,
|
|
"Failed to inspect packaged native ELF",
|
|
{ stdio: "pipe" },
|
|
).stdout.toString("utf8")
|
|
|
|
if (/\s\.(?:z)?debug(?:_|\s)|\s\.symtab\s/.test(elf)) {
|
|
throw new Error("Packaged production native library is not stripped")
|
|
}
|
|
if (!/\bgetBuildOptions\b/.test(elf)) {
|
|
throw new Error("Packaged production native library lost its dynamic FFI exports")
|
|
}
|
|
if (!elf.includes(".note.gnu.build-id") || !elf.includes(".gnu_debuglink")) {
|
|
throw new Error("Packaged native ELF cannot be matched to its separate release symbols")
|
|
}
|
|
}
|
|
}
|
|
|
|
function assertPortableDeclarations(): void {
|
|
for (const declarationPath of declarationPaths) {
|
|
const fullPath = join(distDir, declarationPath)
|
|
const contents = readFileSync(fullPath, "utf8")
|
|
if (contents.includes("bun:ffi")) {
|
|
throw new Error(`Portable declaration ${declarationPath} still references bun:ffi`)
|
|
}
|
|
}
|
|
}
|
|
|
|
function assertRuntimeOutputs(): void {
|
|
const nodeSource = readRuntimeGraph(["index.node.js", "testing.js", "yoga.js"], "chunk-node-")
|
|
const bunSource = readRuntimeGraph(["index.bun.js", "testing.bun.js", "yoga.bun.js"], "chunk-bun-")
|
|
const workerSource = readFileSync(join(distDir, "parser.worker.js"), "utf8")
|
|
const distPackage = JSON.parse(readFileSync(join(distDir, "package.json"), "utf8")) as {
|
|
exports: Record<string, Record<string, string>>
|
|
}
|
|
|
|
for (const [name, source] of [
|
|
["Node root", nodeSource],
|
|
["Node parser worker", workerSource],
|
|
] as const) {
|
|
if (/with:\s*\{\s*type:\s*["'](?:file|wasm)["']/.test(source)) {
|
|
throw new Error(`${name} contains a Bun file import attribute`)
|
|
}
|
|
}
|
|
if (/import\(["']@opentui\/core-(?:darwin|linux|win32)-/.test(nodeSource)) {
|
|
throw new Error("Node root contains a statically resolved OpenTUI native package import")
|
|
}
|
|
if (!/with:\s*\{\s*type:\s*["']file["']/.test(bunSource)) {
|
|
throw new Error("Bun root does not retain literal file imports")
|
|
}
|
|
if (/\b(?:from|import\()\s*["']web-tree-sitter["']/.test(workerSource)) {
|
|
throw new Error("Node parser worker still imports web-tree-sitter at runtime")
|
|
}
|
|
if (!workerSource.includes('"web-tree-sitter/tree-sitter.wasm"')) {
|
|
throw new Error("Node parser worker does not reference the stable tree-sitter WASM key")
|
|
}
|
|
|
|
const rootExport = distPackage.exports["."]
|
|
if (
|
|
rootExport?.bun !== "./index.bun.js" ||
|
|
rootExport?.node !== "./index.node.js" ||
|
|
rootExport?.import !== "./index.node.js"
|
|
) {
|
|
throw new Error("Root package export does not select separate Bun and Node runtime outputs")
|
|
}
|
|
if (distPackage.exports["./node-assets"]?.import !== "./node-assets.js") {
|
|
throw new Error("Missing @opentui/core/node-assets package export")
|
|
}
|
|
const workerExport = distPackage.exports["./parser.worker"]
|
|
if (
|
|
workerExport?.bun !== "./parser.worker.js" ||
|
|
workerExport?.node !== "./parser.worker.js" ||
|
|
workerExport?.import !== "./parser.worker.js"
|
|
) {
|
|
throw new Error("Parser worker package export does not select the shared worker output")
|
|
}
|
|
|
|
for (const sourceMap of ["index.node.js.map", "index.bun.js.map", "parser.worker.js.map"]) {
|
|
if (!existsSync(join(distDir, sourceMap))) {
|
|
throw new Error(`Missing source map ${sourceMap}`)
|
|
}
|
|
}
|
|
const workerSourceMap = JSON.parse(readFileSync(join(distDir, "parser.worker.js.map"), "utf8")) as {
|
|
sources?: string[]
|
|
}
|
|
const workerSourcePath = workerSourceMap.sources?.find((source) => source.endsWith("/parser.worker.ts"))
|
|
if (!workerSourcePath || !existsSync(resolve(distDir, workerSourcePath))) {
|
|
throw new Error("Parser worker source map does not resolve to parser.worker.ts")
|
|
}
|
|
if (existsSync(join(distDir, "parser.worker.bun.js"))) {
|
|
throw new Error("Found obsolete Bun-specific parser worker")
|
|
}
|
|
}
|
|
|
|
function readRuntimeGraph(entryPaths: string[], chunkPrefix: string): string {
|
|
const chunkPaths = readdirSync(distDir)
|
|
.filter((name) => name.startsWith(chunkPrefix) && name.endsWith(".js"))
|
|
.sort()
|
|
return [...entryPaths, ...chunkPaths].map((path) => readFileSync(join(distDir, path), "utf8")).join("\n")
|
|
}
|
|
|
|
function packArtifact(packageDir: string, packDir: string): string {
|
|
const result = runCommand(
|
|
"npm",
|
|
["pack", "--pack-destination", packDir],
|
|
packageDir,
|
|
`Failed to pack ${packageDir}`,
|
|
{
|
|
stdio: "pipe",
|
|
},
|
|
)
|
|
|
|
const tarballName = result.stdout.toString("utf8").trim().split(/\r?\n/).at(-1)
|
|
|
|
if (!tarballName) {
|
|
throw new Error(`Failed to determine tarball name for ${packageDir}`)
|
|
}
|
|
|
|
return join(packDir, tarballName)
|
|
}
|
|
|
|
function writeConsumerPackage(consumerDir: string, coreTarball: string, nativeTarball: string, name: string): void {
|
|
const coreDependency = `file:${relative(consumerDir, coreTarball).replaceAll("\\", "/")}`
|
|
const nativeDependency = `file:${relative(consumerDir, nativeTarball).replaceAll("\\", "/")}`
|
|
|
|
writeFileSync(
|
|
join(consumerDir, "package.json"),
|
|
JSON.stringify(
|
|
{
|
|
name,
|
|
private: true,
|
|
type: "module",
|
|
dependencies: {
|
|
[packageJson.name]: coreDependency,
|
|
[nativePackageName]: nativeDependency,
|
|
},
|
|
overrides: {
|
|
[nativePackageName]: nativeDependency,
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
)
|
|
}
|
|
|
|
function writeNodeTest(nodeDir: string): void {
|
|
writeFileSync(
|
|
join(nodeDir, "index.mjs"),
|
|
`import assert from "node:assert/strict"
|
|
import { mkdtempSync, rmSync } from "node:fs"
|
|
import { tmpdir } from "node:os"
|
|
import { join } from "node:path"
|
|
|
|
const nativePackageName = ${JSON.stringify(nativePackageName)}
|
|
|
|
const core = await import(${JSON.stringify(packageJson.name)})
|
|
const nodeAssets = await import(${JSON.stringify(`${packageJson.name}/node-assets`)})
|
|
const testing = await import(${JSON.stringify(`${packageJson.name}/testing`)})
|
|
const yoga = await import(${JSON.stringify(`${packageJson.name}/yoga`)})
|
|
const parserWorker = await import(${JSON.stringify(`${packageJson.name}/parser.worker`)})
|
|
const nativePackage = await import(nativePackageName)
|
|
|
|
assert.equal(typeof core.createCliRenderer, "function")
|
|
assert.equal(typeof core.Audio, "function")
|
|
assert.equal(typeof core.AudioCaptureStream, "function")
|
|
assert.equal(typeof core.AudioCaptureStreamError, "function")
|
|
assert.equal(typeof core.AudioRecorder, "function")
|
|
assert.equal(typeof core.AudioRecorderError, "function")
|
|
assert.equal(typeof core.AudioStreamError, "function")
|
|
assert.equal(typeof core.NativeImage, "function")
|
|
assert.equal(typeof core.NativeImagePool, "function")
|
|
assert.equal(typeof core.ImageRenderable, "function")
|
|
assert.equal(typeof core.Audio.prototype.openCapture, "function")
|
|
assert.equal(typeof core.Audio.prototype.recordToFile, "function")
|
|
assert.equal(typeof core.createIcyStreamDemuxer, "function")
|
|
assert.equal(core.NativeAudioStreamCloseReason.TransportError, 1)
|
|
assert.equal(core.NativeAudioStreamFormat.Mp3, 1)
|
|
assert.equal(core.NativeAudioStreamFormat.Flac, 2)
|
|
assert.equal(typeof testing.createTestRenderer, "function")
|
|
assert.equal(core.Yoga.Node, yoga.Node)
|
|
assert.equal(Object.getPrototypeOf(testing.MockTreeSitterClient.prototype), core.TreeSitterClient.prototype)
|
|
assert.equal(typeof parserWorker, "object")
|
|
assert.equal(typeof nativePackage.default, "string")
|
|
|
|
const manifest = nodeAssets.getNodeAssets({
|
|
platform: process.platform,
|
|
arch: process.arch,
|
|
...(process.platform === "linux" ? { libc: "glibc" } : {}),
|
|
})
|
|
assert.equal(manifest.length, 14)
|
|
assert.deepEqual(
|
|
manifest.map((asset) => asset.key),
|
|
manifest.map((asset) => asset.key).toSorted(),
|
|
)
|
|
|
|
const buffer = core.OptimizedBuffer.create(2, 1, "unicode")
|
|
assert.equal(buffer.width, 2)
|
|
buffer.destroy()
|
|
|
|
const image = core.NativeImage.fromRgba(Uint8Array.of(1, 2, 3, 255), 1, 1)
|
|
const raw = image.takeRaw()
|
|
try {
|
|
assert.deepEqual([...raw.data], [1, 2, 3, 255])
|
|
assert.throws(() => image.info(), /disposed/)
|
|
} finally {
|
|
raw.dispose()
|
|
}
|
|
|
|
const imported = core.NativeImage.fromPixels(Uint8Array.of(99, 3, 2, 1, 0).subarray(1), 1, 1, {
|
|
format: "bgra8", alpha: "opaque", stride: 256,
|
|
})
|
|
try {
|
|
assert.deepEqual([...imported.raw().data], [1, 2, 3, 255])
|
|
assert.equal(imported.info().hasAlpha, false)
|
|
} finally {
|
|
imported.dispose()
|
|
}
|
|
|
|
const pool = new core.NativeImagePool({ width: 1, height: 1, capacity: 1 })
|
|
try {
|
|
for (const red of [255, 128]) {
|
|
const frame = pool.publishRgba(Uint8Array.of(red, 0, 0, 255))
|
|
try {
|
|
assert.deepEqual([...frame.raw().data], [red, 0, 0, 255])
|
|
} finally {
|
|
frame.dispose()
|
|
}
|
|
}
|
|
const frame = pool.publishPixels(Uint8Array.of(3, 2, 1, 0), { format: "bgra8", alpha: "opaque", stride: 256 })
|
|
try {
|
|
assert.deepEqual([...frame.raw().data], [1, 2, 3, 255])
|
|
assert.equal(frame.info().hasAlpha, false)
|
|
} finally {
|
|
frame.dispose()
|
|
}
|
|
} finally {
|
|
pool.dispose()
|
|
}
|
|
|
|
const dataPath = mkdtempSync(join(tmpdir(), "opentui-node-dist-tree-sitter-"))
|
|
const client = new core.TreeSitterClient({ dataPath })
|
|
try {
|
|
const result = await client.highlightOnce(${JSON.stringify("# Title\n\n```js\nconst answer = 42\n```\n")}, "markdown")
|
|
assert.equal(result.error, undefined)
|
|
assert.ok(result.highlights?.length)
|
|
} finally {
|
|
await client.destroy()
|
|
rmSync(dataPath, { recursive: true, force: true })
|
|
}
|
|
|
|
const expectBunOnlyFailure = async (specifier, expectedMessage) => {
|
|
await assert.rejects(import(specifier), (error) => {
|
|
return error instanceof Error && error.message.includes(expectedMessage)
|
|
})
|
|
}
|
|
|
|
await expectBunOnlyFailure(${JSON.stringify(`${packageJson.name}/runtime-plugin`)}, ${JSON.stringify(`${packageJson.name}/runtime-plugin is Bun-only`)})
|
|
await expectBunOnlyFailure(
|
|
${JSON.stringify(`${packageJson.name}/runtime-plugin-support`)},
|
|
${JSON.stringify(`${packageJson.name}/runtime-plugin-support is Bun-only`)},
|
|
)
|
|
await expectBunOnlyFailure(
|
|
${JSON.stringify(`${packageJson.name}/runtime-plugin-support/configure`)},
|
|
${JSON.stringify(`${packageJson.name}/runtime-plugin-support/configure is Bun-only`)},
|
|
)
|
|
|
|
console.log("Node dist smoke test passed")
|
|
`,
|
|
)
|
|
|
|
writeFileSync(
|
|
join(nodeDir, "require.cjs"),
|
|
`const assert = require("node:assert/strict")
|
|
|
|
assert.throws(
|
|
() => require(${JSON.stringify(packageJson.name)}),
|
|
(error) => error?.code === "ERR_REQUIRE_ASYNC_MODULE",
|
|
${JSON.stringify(`Expected ${packageJson.name} CommonJS require to reject its async ESM graph`)},
|
|
)
|
|
|
|
for (const specifier of [${JSON.stringify(`${packageJson.name}/testing`)}, ${JSON.stringify(`${packageJson.name}/tree-sitter/update-assets`)}]) {
|
|
assert.throws(
|
|
() => require(specifier),
|
|
(error) => error?.code === "ERR_PACKAGE_PATH_NOT_EXPORTED",
|
|
\`Expected \${specifier} to remain import-only in Node\`,
|
|
)
|
|
}
|
|
|
|
const workerPath = require.resolve(${JSON.stringify(`${packageJson.name}/parser.worker`)})
|
|
assert.match(workerPath, /parser\\.worker\\.js$/)
|
|
|
|
console.log("Node CommonJS export smoke test passed")
|
|
`,
|
|
)
|
|
}
|
|
|
|
function writeBunTest(bunDir: string): void {
|
|
writeFileSync(
|
|
join(bunDir, "index.test.ts"),
|
|
`import { describe, expect, test } from "bun:test"
|
|
|
|
describe("${packageJson.name} dist smoke test", () => {
|
|
test("imports portable and Bun-only entrypoints", async () => {
|
|
const core = await import(${JSON.stringify(packageJson.name)})
|
|
const testing = await import(${JSON.stringify(`${packageJson.name}/testing`)})
|
|
const yoga = await import(${JSON.stringify(`${packageJson.name}/yoga`)})
|
|
const parserWorker = await import(${JSON.stringify(`${packageJson.name}/parser.worker`)})
|
|
const runtimePlugin = await import(${JSON.stringify(`${packageJson.name}/runtime-plugin`)})
|
|
const nativePackage = await import(${JSON.stringify(nativePackageName)})
|
|
|
|
expect(typeof core.createCliRenderer).toBe("function")
|
|
expect(typeof core.Audio).toBe("function")
|
|
expect(typeof core.AudioCaptureStream).toBe("function")
|
|
expect(typeof core.AudioCaptureStreamError).toBe("function")
|
|
expect(typeof core.AudioRecorder).toBe("function")
|
|
expect(typeof core.AudioRecorderError).toBe("function")
|
|
expect(typeof core.AudioStreamError).toBe("function")
|
|
expect(typeof core.NativeImage).toBe("function")
|
|
expect(typeof core.NativeImagePool).toBe("function")
|
|
expect(typeof core.ImageRenderable).toBe("function")
|
|
expect(typeof core.Audio.prototype.openCapture).toBe("function")
|
|
expect(typeof core.Audio.prototype.recordToFile).toBe("function")
|
|
expect(core.NativeAudioStreamCloseReason.TransportError).toBe(1)
|
|
expect(core.NativeAudioStreamFormat.Flac).toBe(2)
|
|
expect(typeof testing.createTestRenderer).toBe("function")
|
|
expect(core.Yoga.Node).toBe(yoga.Node)
|
|
expect(Object.getPrototypeOf(testing.MockTreeSitterClient.prototype)).toBe(core.TreeSitterClient.prototype)
|
|
expect(typeof parserWorker).toBe("object")
|
|
expect(typeof runtimePlugin.createRuntimePlugin).toBe("function")
|
|
expect(typeof nativePackage.default).toBe("string")
|
|
const image = core.NativeImage.fromRgba(Uint8Array.of(1, 2, 3, 255), 1, 1)
|
|
const raw = image.takeRaw()
|
|
try {
|
|
expect([...raw.data]).toEqual([1, 2, 3, 255])
|
|
expect(() => image.info()).toThrow(/disposed/)
|
|
} finally {
|
|
raw.dispose()
|
|
}
|
|
const imported = core.NativeImage.fromPixels(Uint8Array.of(99, 3, 2, 1, 0).subarray(1), 1, 1, {
|
|
format: "bgra8", alpha: "opaque", stride: 256,
|
|
})
|
|
try {
|
|
expect([...imported.raw().data]).toEqual([1, 2, 3, 255])
|
|
expect(imported.info().hasAlpha).toBe(false)
|
|
} finally {
|
|
imported.dispose()
|
|
}
|
|
})
|
|
})
|
|
`,
|
|
)
|
|
}
|
|
|
|
function assertNodeStaticImportFailure(
|
|
nodeDir: string,
|
|
importedName: string,
|
|
specifier: string,
|
|
expectedMessage: string,
|
|
): void {
|
|
const result = runCommandExpectFailure(
|
|
nodePath,
|
|
["--input-type=module", "-e", `import { ${importedName} } from ${JSON.stringify(specifier)}`],
|
|
nodeDir,
|
|
`Expected static Node import of ${specifier} to fail`,
|
|
)
|
|
|
|
const output = `${result.stdout.toString("utf8")}\n${result.stderr.toString("utf8")}`
|
|
|
|
if (output.includes("does not provide an export named")) {
|
|
throw new Error(`Static Node import of ${specifier} failed before the Bun-only stub could run`)
|
|
}
|
|
|
|
if (!output.includes(expectedMessage)) {
|
|
throw new Error(`Static Node import of ${specifier} did not report the expected Bun-only error`)
|
|
}
|
|
}
|
|
|
|
function installAndTest(nodeDir: string, bunDir: string): void {
|
|
runCommand(
|
|
"npm",
|
|
["install", "--engine-strict", "--ignore-scripts", "--no-package-lock"],
|
|
nodeDir,
|
|
"Node dist test install failed",
|
|
)
|
|
runCommand(nodePath, ["-e", `import(${JSON.stringify(packageJson.name)})`], nodeDir, "Node import smoke check failed")
|
|
runCommand(nodePath, ["--experimental-ffi", "--no-warnings", "index.mjs"], nodeDir, "Node dist smoke tests failed", {
|
|
timeout: 60_000,
|
|
})
|
|
runCommand(nodePath, ["require.cjs"], nodeDir, "Node CommonJS export smoke tests failed")
|
|
|
|
assertNodeStaticImportFailure(
|
|
nodeDir,
|
|
"createRuntimePlugin",
|
|
`${packageJson.name}/runtime-plugin`,
|
|
`${packageJson.name}/runtime-plugin is Bun-only`,
|
|
)
|
|
assertNodeStaticImportFailure(
|
|
nodeDir,
|
|
"ensureRuntimePluginSupport",
|
|
`${packageJson.name}/runtime-plugin-support`,
|
|
`${packageJson.name}/runtime-plugin-support is Bun-only`,
|
|
)
|
|
assertNodeStaticImportFailure(
|
|
nodeDir,
|
|
"ensureRuntimePluginSupport",
|
|
`${packageJson.name}/runtime-plugin-support/configure`,
|
|
`${packageJson.name}/runtime-plugin-support/configure is Bun-only`,
|
|
)
|
|
|
|
runCommand("bun", ["install", "--ignore-scripts"], bunDir, "Bun dist test install failed")
|
|
runCommand("bun", ["test", "index.test.ts"], bunDir, "Bun dist smoke tests failed")
|
|
}
|
|
|
|
let tempRoot: string | undefined
|
|
|
|
try {
|
|
ensureBuildArtifacts()
|
|
assertPortableDeclarations()
|
|
assertRuntimeOutputs()
|
|
|
|
tempRoot = mkdtempSync(join(tmpdir(), "opentui-core-dist-test-"))
|
|
const packDir = join(tempRoot, "packs")
|
|
const nodeDir = join(tempRoot, "node")
|
|
const bunDir = join(tempRoot, "bun")
|
|
|
|
mkdirSync(packDir, { recursive: true })
|
|
mkdirSync(nodeDir, { recursive: true })
|
|
mkdirSync(bunDir, { recursive: true })
|
|
|
|
const coreTarball = packArtifact(distDir, packDir)
|
|
const nativeTarball = packArtifact(nativePackageDir, packDir)
|
|
|
|
writeConsumerPackage(nodeDir, coreTarball, nativeTarball, "opentui-core-dist-test-node")
|
|
writeConsumerPackage(bunDir, coreTarball, nativeTarball, "opentui-core-dist-test-bun")
|
|
writeNodeTest(nodeDir)
|
|
writeBunTest(bunDir)
|
|
|
|
installAndTest(nodeDir, bunDir)
|
|
|
|
if (!keepTemp) {
|
|
rmSync(tempRoot, { recursive: true, force: true })
|
|
tempRoot = undefined
|
|
}
|
|
|
|
console.log("Packed dist smoke tests passed")
|
|
} catch (error) {
|
|
console.error(error instanceof Error ? error.message : error)
|
|
if (tempRoot) {
|
|
console.error(`Dist test workspace kept at ${tempRoot}`)
|
|
}
|
|
process.exit(1)
|
|
}
|