chore: packaging and test fixes (#1517)

pPacked parser asset paths, runtime-plugin string-literal rewrites, Node
snapshot parsing, Solid plugin file reads, SSH test retries, Node
example argv forwarding, etc. etc.
This commit is contained in:
Simon Klee
2026-09-18 11:09:57 +02:00
committed by GitHub
parent c01292fd08
commit 4954312d74
13 changed files with 120 additions and 38 deletions
+4
View File
@@ -9,6 +9,10 @@
input-driven work and test lifecycle failures.
- Do not interchange byte lengths, code points, graphemes, and terminal display-cell widths.
- `oxfmt` is the formatting source of truth (`semi: false`, `printWidth: 120`); avoid unrelated formatting churn.
- In delegated tasks, name the local rendering behavior, owned files, and required checks. Describe ordinary
renderer work as correctness and compatibility testing; reserve security terminology for actual security work.
Use concrete scopes such as frame parity, callback ordering, resource cleanup, or loopback SSH output tests.
Request an independent correctness review and a separate reproduction check of actionable findings.
## Tooling And Runtimes
+2 -1
View File
@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test"
import { isAbsolute } from "node:path"
import { defaultParserAssetPaths } from "./lib/tree-sitter/default-parsers.js"
import { getNodeAssets } from "./node-assets.js"
describe("getNodeAssets", () => {
@@ -28,7 +29,7 @@ describe("getNodeAssets", () => {
expect(first).toEqual(second)
expect(keys).toEqual([...keys].sort())
expect(keys).toHaveLength(14)
expect(keys).toHaveLength(3 + defaultParserAssetPaths.length)
expect(keys).toContain("@opentui/core/parser.worker.js")
expect(keys).toContain("@opentui/core/assets/markdown/highlights.scm")
expect(keys).toContain("web-tree-sitter/tree-sitter.wasm")
+6 -1
View File
@@ -20,13 +20,14 @@ const TREE_SITTER_WASM_KEY = "web-tree-sitter/tree-sitter.wasm"
export function getNodeAssets(target: NodeAssetTarget): readonly NodeAsset[] {
const native = getNativeAssetDescriptor(target)
const coreRoot = resolveCoreRuntimeRoot()
const parserRoot = resolve(dirname(fileURLToPath(import.meta.url)), "lib/tree-sitter")
const nativeRoot = dirname(resolvePackageEntry(native.packageName))
const assets: NodeAsset[] = [
{ key: native.key, source: join(nativeRoot, native.fileName) },
{ key: PARSER_WORKER_KEY, source: join(coreRoot, "parser.worker.js") },
...defaultParserAssetPaths.map((relativePath) => ({
key: `${CORE_PREFIX}${relativePath}`,
source: join(coreRoot, relativePath),
source: firstExistingFile(join(coreRoot, relativePath), join(parserRoot, relativePath)),
})),
{ key: TREE_SITTER_WASM_KEY, source: resolvePackageEntry(TREE_SITTER_WASM_KEY) },
]
@@ -57,6 +58,10 @@ function resolveCoreRuntimeRoot(): string {
return candidates.find((candidate) => statIsFile(join(candidate, "parser.worker.js"))) ?? moduleDirectory
}
function firstExistingFile(...paths: string[]): string {
return paths.find(statIsFile) ?? paths[0]!
}
function resolvePackageEntry(specifier: string): string {
return fileURLToPath(import.meta.resolve(specifier))
}
+27 -10
View File
@@ -28,7 +28,9 @@
* packages already marked for runtime rewriting.
*
* Notes:
* - import scanning is regex-based, not a full parser.
* - import scanning is regex-based, not a full parser. A match that starts
* immediately inside quotes is left unchanged so string literals such as
* `'from "@opentui/core"'` are not rewritten.
* - CJS helper libraries that themselves import runtime modules are still not
* supported.
* - `package.json#type` caching is per plugin setup, not module-global, so a
@@ -264,6 +266,11 @@ const resolveImportSpecifierPatterns = [
/(require\s*\(\s*["'])([^"']+)(["']\s*\))/g,
] as const
const isImportLikeMatchInsideQuotes = (code: string, offset: number): boolean => {
const previous = code[offset - 1]
return previous === '"' || previous === "'" || previous === "`"
}
const isBareSpecifier = (specifier: string): boolean => {
if (specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("\\")) {
return false
@@ -304,14 +311,21 @@ const rewriteImportSpecifiers = (code: string, resolveReplacement: (specifier: s
let transformedCode = code
for (const pattern of resolveImportSpecifierPatterns) {
transformedCode = transformedCode.replace(pattern, (fullMatch, prefix, specifier, suffix) => {
const replacement = resolveReplacement(specifier)
if (!replacement || replacement === specifier) {
return fullMatch
}
transformedCode = transformedCode.replace(
pattern,
(fullMatch: string, prefix: string, specifier: string, suffix: string, offset: number) => {
if (isImportLikeMatchInsideQuotes(transformedCode, offset)) {
return fullMatch
}
return `${prefix}${replacement}${suffix}`
})
const replacement = resolveReplacement(specifier)
if (!replacement || replacement === specifier) {
return fullMatch
}
return `${prefix}${replacement}${suffix}`
},
)
}
return transformedCode
@@ -321,8 +335,11 @@ const collectImportSpecifiers = (code: string): string[] => {
const specifiers = new Set<string>()
for (const pattern of resolveImportSpecifierPatterns) {
code.replace(pattern, (_fullMatch, _prefix, specifier) => {
specifiers.add(specifier)
code.replace(pattern, (_fullMatch: string, _prefix: string, specifier: string, _suffix: string, offset: number) => {
if (!isImportLikeMatchInsideQuotes(code, offset)) {
specifiers.add(specifier)
}
return _fullMatch
})
}
+13 -3
View File
@@ -116,10 +116,16 @@ function readSnapshotFile(snapshotPath: string): Map<string, string> {
if (existsSync(snapshotPath)) {
const contents = normalizeNewlines(readFileSync(snapshotPath, "utf8"))
const snapshotPattern = /exports\[`([\s\S]*?)`\] = `\n([\s\S]*?)\n`;/g
const snapshotPattern = /exports\[`((?:\\[\s\S]|[^\\`])*)`\] = `((?:\\[\s\S]|[^\\`])*)`;/g
for (const match of contents.matchAll(snapshotPattern)) {
snapshots.set(match[1], match[2])
const key = match[1].replace(/\\(\\|`|\$\{)/g, "$1")
// Undo template delimiters, but keep doubled backslashes for serializeSnapshotValue().
let value = match[2].replace(/\\(\\|`|\$\{)/g, (escape, value) => (value === "\\" ? escape : value))
if (value.startsWith("\n") && value.endsWith("\n")) {
value = value.slice(1, -1)
}
snapshots.set(key, value)
}
}
@@ -228,7 +234,11 @@ function createTestVariant(base: AnyFunction): AnyFunction {
function formatEachName(name: string, args: readonly unknown[]): string {
let index = 0
return name.replace(/%s/g, () => String(args[index++]))
return name.replace(/%[sj]/g, (placeholder) => {
const value = args[index++]
if (placeholder === "%j") return JSON.stringify(value) ?? placeholder
return typeof value === "string" ? value : placeholder
})
}
function createEach(base: AnyFunction) {
+1 -1
View File
@@ -19,7 +19,7 @@ prepareCorePackage()
buildNodeExamples()
copyCoreDistPackage()
const result = spawnSync(nodePath, ["--experimental-ffi", "--no-warnings", bundleEntry], {
const result = spawnSync(nodePath, ["--experimental-ffi", "--no-warnings", bundleEntry, ...process.argv.slice(2)], {
cwd: packageRoot,
stdio: "inherit",
env: {
@@ -0,0 +1,36 @@
import { expect, spyOn, test } from "bun:test"
import * as childProcess from "node:child_process"
import * as fs from "node:fs"
import { fileURLToPath } from "node:url"
test.each([{ args: [] }, { args: ["--help"] }])("Node launcher forwards browser arguments %j", async ({ args }) => {
const argv = process.argv
const path = process.env.PATH
const stop = new Error("launcher exited")
const spawn = spyOn(childProcess, "spawnSync").mockImplementation(((_command: string, args: string[]) => ({
status: 0,
stdout: args.includes("--eval") ? JSON.stringify({ version: "v26.4.0", execPath: "/node26" }) : "",
})) as typeof childProcess.spawnSync)
const copy = spyOn(fs, "cpSync").mockImplementation(() => {})
const mkdir = spyOn(fs, "mkdirSync").mockImplementation(() => undefined)
const remove = spyOn(fs, "rmSync").mockImplementation(() => {})
const exit = spyOn(process, "exit").mockImplementation(() => {
throw stop
})
try {
process.argv = [process.execPath, fileURLToPath(new URL("./run-node26.mjs", import.meta.url)), ...args]
await expect(import(`./run-node26.mjs?args=${args.join(",")}`)).rejects.toBe(stop)
expect(spawn.mock.calls.at(-1)?.slice(0, 2)).toEqual([
"/node26",
["--experimental-ffi", "--no-warnings", fileURLToPath(new URL("../.node/index.js", import.meta.url)), ...args],
])
} finally {
process.argv = argv
process.env.PATH = path
spawn.mockRestore()
copy.mockRestore()
mkdir.mockRestore()
remove.mockRestore()
exit.mockRestore()
}
})
+4 -7
View File
@@ -1,4 +1,5 @@
import { plugin as registerBunPlugin, type BunPlugin } from "bun"
import { readFile } from "node:fs/promises"
import { stripQueryAndHash, transformSolidSource, type ResolveImportPath } from "./solid-transform.js"
const solidTransformStateKey = Symbol.for("opentui.solid.transform")
@@ -71,8 +72,7 @@ export function createSolidTransformPlugin(input: CreateSolidTransformPluginOpti
setup: (build) => {
build.onLoad({ filter: /[/\\]node_modules[/\\]solid-js[/\\]dist[/\\]server\.js(?:[?#].*)?$/ }, async (args) => {
const path = stripQueryAndHash(args.path).replace("server.js", "solid.js")
const file = Bun.file(path)
const code = await file.text()
const code = await readFile(path, "utf8")
return { contents: code, loader: "js" }
})
@@ -80,17 +80,14 @@ export function createSolidTransformPlugin(input: CreateSolidTransformPluginOpti
{ filter: /[/\\]node_modules[/\\]solid-js[/\\]store[/\\]dist[/\\]server\.js(?:[?#].*)?$/ },
async (args) => {
const path = stripQueryAndHash(args.path).replace("server.js", "store.js")
const file = Bun.file(path)
const code = await file.text()
const code = await readFile(path, "utf8")
return { contents: code, loader: "js" }
},
)
build.onLoad({ filter: sourceFilter }, async (args) => {
const path = stripQueryAndHash(args.path)
const file = Bun.file(path)
const code = await file.text()
const code = await readFile(path, "utf8")
const runtime = getSolidTransformRuntime()
const moduleName = input.moduleName ?? runtime.moduleName ?? "@opentui/solid"
const resolvePath = input.resolvePath ?? runtime.resolvePath
+1 -2
View File
@@ -1,12 +1,11 @@
import { rmSync, mkdtempSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { plugin as registerPlugin } from "bun"
import { createRuntimePlugin, runtimeModuleIdForSpecifier, type RuntimeModuleEntry } from "@opentui/core/runtime-plugin"
import * as solidRuntime from "../index.js"
import { createSolidTransformPlugin } from "../scripts/solid-plugin.js"
const tempRoot = mkdtempSync(join(tmpdir(), "solid-plugin-fixture-"))
const tempRoot = mkdtempSync(join(import.meta.dir, "solid-plugin-fixture-"))
const entryPath = join(tempRoot, "entry.tsx")
const additionalRuntimeModules: Record<string, RuntimeModuleEntry> = {
+12 -5
View File
@@ -1,9 +1,8 @@
import { describe, expect, it } from "bun:test"
import { afterEach, beforeEach, describe, expect, it } from "bun:test"
import { mkdtempSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { runtimeModuleIdForSpecifier } from "@opentui/core/runtime-plugin"
import { createSolidTransformPlugin } from "../scripts/solid-plugin.js"
import { createSolidTransformPlugin, resetSolidTransformPluginState } from "../scripts/solid-plugin.js"
type ResolveCallback = (args: { path: string; importer: string }) => unknown | Promise<unknown>
type LoadResult = { contents: string; loader: string } | void
@@ -61,7 +60,7 @@ const runLoad = async (handlers: LoadHandler[], path: string): Promise<LoadResul
}
const createTempTsxFile = (source: string): { path: string; dispose: () => void } => {
const tempRoot = mkdtempSync(join(tmpdir(), "solid-plugin-test-"))
const tempRoot = mkdtempSync(join(import.meta.dir, "solid-plugin-test-"))
const path = join(tempRoot, "fixture.tsx")
writeFileSync(path, source)
@@ -74,6 +73,14 @@ const createTempTsxFile = (source: string): { path: string; dispose: () => void
}
describe("solid transform plugin", () => {
beforeEach(() => {
resetSolidTransformPluginState()
})
afterEach(() => {
resetSolidTransformPluginState()
})
it("does not register runtime module resolvers by default", () => {
const { build, resolveFilters, modules } = createMockBuild()
createSolidTransformPlugin().setup(build as any)
@@ -196,7 +203,7 @@ describe("solid transform plugin", () => {
})
it("ignores host project babel config when transforming plugins", async () => {
const hostile = mkdtempSync(join(tmpdir(), "solid-plugin-hostile-config-"))
const hostile = mkdtempSync(join(import.meta.dir, "solid-plugin-hostile-config-"))
const tsxPath = join(hostile, "fixture.tsx")
writeFileSync(tsxPath, "const node = <text>ok</text>\nexport { node }")
writeFileSync(
@@ -5,12 +5,13 @@ import { expect, spyOn, test } from "bun:test"
import { utils } from "ssh2"
import { createServer } from "../../index.js"
import { parseOneKey, sha256Fingerprint } from "../../keys.js"
import { createHarness, HOST_KEY } from "../support.js"
import { createHarness, generateParseableKey, HOST_KEY } from "../support.js"
const { track, tmpDir } = createHarness()
test("listen reports every configured host-key fingerprint", async () => {
const ed25519 = utils.generateKeyPairSync("ed25519").private
// ssh2's ed25519 keygen can emit a key its own parser rejects; retry past it.
const ed25519 = generateParseableKey().private
const rsa = utils.generateKeyPairSync("rsa", { bits: 2048 }).private
const expected = [ed25519, rsa].map((pem) => {
const key = parseOneKey(pem)
@@ -1,10 +1,13 @@
import { readFileSync } from "node:fs"
import { existsSync, readFileSync } from "node:fs"
import { join } from "node:path"
import { expect, test } from "bun:test"
test("built package preserves declared runtime engines", () => {
const root = join(import.meta.dir, "../../..")
const source = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { engines?: object }
const dist = JSON.parse(readFileSync(join(root, "dist/package.json"), "utf8")) as { engines?: object }
expect(readFileSync(join(root, "scripts/build.ts"), "utf8")).toContain("engines: packageJson.engines")
const distPath = join(root, "dist/package.json")
if (!existsSync(distPath)) return
const dist = JSON.parse(readFileSync(distPath, "utf8")) as { engines?: object }
expect(dist.engines).toEqual(source.engines)
})
+6 -4
View File
@@ -164,17 +164,19 @@
width: max-content;
min-width: 0;
margin: 0;
padding: 0;
/* Leave room for glyph bounds beyond the tight diagram line boxes. */
padding: 0.1em 0;
border: 0;
overflow: visible;
/* Override Shiki's inline overflow-x. Only the outer figure may scroll. */
overflow: visible !important;
font-size: inherit;
line-height: 1.2;
font-variant-ligatures: none;
/* Keep diagram symbols in one cell, including arrows with wide font variants. */
font-feature-settings: "NWID";
}
.prose .terminal-frame {
/* Keep ambiguous-width symbols in the same single cells as the terminal. */
font-feature-settings: "NWID";
--terminal-color-235: color-mix(in srgb, var(--page-color) 7%, var(--page-background));
--terminal-color-238: color-mix(in srgb, var(--page-color) 16%, var(--page-background));
--terminal-color-243: var(--terminal-color-238);