mirror of
https://github.com/anomalyco/opentui.git
synced 2026-09-19 01:26:03 +08:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
})
|
||||
@@ -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,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> = {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user