Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 21 additions & 18 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions packages/plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"//check:tui-compiled": "Check drift on output already generated by build or build:tui; this check never regenerates files. CI and prepublishOnly build first.",
"//test": "Unit shards run small behavioral fixtures. The nightly/check-branch slow-unit lane runs full scale and latency gates in fresh processes once per mode, not once per shard. JSC's optimizing WebAssembly tier stays disabled in tests; production host options are unchanged.",
"dependencies": {
"@cortexkit/opencode-sidebar": "^0.2.0",
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
"@cortexkit/subc-client": "0.11.1",
"@jitl/quickjs-singlefile-cjs-release-asyncify": "0.32.0",
"@opencode-ai/plugin": "^1.17.11",
Expand All @@ -73,8 +74,8 @@
"sharp": "^0.35.0"
},
"devDependencies": {
"@opentui/core": "^0.4.2",
"@opentui/solid": "^0.4.2",
"@opentui/core": "^0.4.5",
"@opentui/solid": "^0.4.5",
"solid-js": "1.9.12",
"@opencode/plugin": "2.0.22",
"@opencode/ai": "2.0.22",
Expand Down Expand Up @@ -108,8 +109,8 @@
],
"peerDependencies": {
"@opencode-ai/plugin": ">=1.15.0",
"@opentui/core": ">=0.4.2",
"@opentui/solid": ">=0.4.2",
"@opentui/core": ">=0.4.5",
"@opentui/solid": ">=0.4.5",
"solid-js": "^1.9.12"
},
"peerDependenciesMeta": {
Expand Down
50 changes: 50 additions & 0 deletions packages/plugin/scripts/build-tui.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { expect, test } from "bun:test";
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import packageJson from "../package.json";

test("declares the OpenTUI minimum required by the shared sidebar", () => {
const sidebar = JSON.parse(
readFileSync(
join(
dirname(Bun.resolveSync("@cortexkit/opencode-sidebar", import.meta.dir)),
"../package.json",
),
"utf8",
),
) as { peerDependencies: Record<string, string> };
for (const name of ["@opentui/core", "@opentui/solid"] as const) {
expect(packageJson.peerDependencies[name]).toMatch(/^>=\d+\.\d+\.\d+$/);
expect(
Bun.semver.satisfies(
packageJson.peerDependencies[name].slice(2),
sidebar.peerDependencies[name],
),
).toBe(true);
expect(
Bun.semver.satisfies(
packageJson.devDependencies[name].slice(1),
sidebar.peerDependencies[name],
),
).toBe(true);
}
});

test("removes obsolete generated JavaScript without removing the current sidebar bundle", () => {
const stale = join(import.meta.dir, "../src/tui-compiled/obsolete-sidebar.js");
writeFileSync(stale, "export {};\n");
try {
const result = Bun.spawnSync([process.execPath, join(import.meta.dir, "build-tui.ts")], {
env: { ...process.env, BUN_BE_BUN: "1" },
stdout: "pipe",
stderr: "pipe",
});
expect(result.exitCode, result.stderr.toString()).toBe(0);
expect(existsSync(stale)).toBe(false);
expect(existsSync(join(import.meta.dir, "../src/tui-compiled/quota-sidebar.js"))).toBe(
true,
);
} finally {
rmSync(stale, { force: true });
}
}, 120_000);
85 changes: 76 additions & 9 deletions packages/plugin/scripts/build-tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const pluginRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
const sourceRoot = join(pluginRoot, "src/tui");
const outputRoot = join(pluginRoot, "src/tui-compiled");
const runtimeSpecifiers: Set<string> = new Set(TUI_RUNTIME_SPECIFIERS);
const sidebarBundleName = "quota-sidebar.js";
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved

type TransformSolidSource = (
code: string,
Expand All @@ -23,7 +24,6 @@ type SolidTransformModule = {
transformSolidSource?: TransformSolidSource;
};


/**
* The Solid transform emits every runtime helper AND every control-flow builtin
* (`For`, `Show`, `Index`, `Switch`, `Match`, `ErrorBoundary`, `Suspense`, ...)
Expand Down Expand Up @@ -103,7 +103,9 @@ function redirectBuiltinImports(

function asTransformSolidSource(mod: SolidTransformModule, from: string): TransformSolidSource {
if (typeof mod.transformSolidSource !== "function") {
throw new Error(`@opentui/solid transform loaded from ${from} without transformSolidSource`);
throw new Error(
`@opentui/solid transform loaded from ${from} without transformSolidSource`,
);
}
return mod.transformSolidSource;
}
Expand All @@ -120,7 +122,9 @@ async function resolveSolidTransformPath(): Promise<string> {
const packageJsonUrl = import.meta.resolve(packageJsonSpecifier);
return join(dirname(fileURLToPath(packageJsonUrl)), "scripts/solid-transform.js");
} catch (error) {
errors.push(`import.meta.resolve: ${error instanceof Error ? error.message : String(error)}`);
errors.push(
`import.meta.resolve: ${error instanceof Error ? error.message : String(error)}`,
);
}

try {
Expand Down Expand Up @@ -155,16 +159,19 @@ function isShippedSourceFile(filePath: string): boolean {
return filePath.endsWith(".tsx") || filePath.endsWith(".ts");
}

async function listSourceFiles(dir: string): Promise<string[]> {
async function listSourceFiles(dir: string, includeJavaScript = false): Promise<string[]> {
const entries = await readdir(dir, { withFileTypes: true });
entries.sort((a, b) => a.name.localeCompare(b.name));

const files: string[] = [];
for (const entry of entries) {
const entryPath = join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...(await listSourceFiles(entryPath)));
} else if (entry.isFile() && isShippedSourceFile(entryPath)) {
files.push(...(await listSourceFiles(entryPath, includeJavaScript)));
} else if (
entry.isFile() &&
(isShippedSourceFile(entryPath) || (includeJavaScript && entryPath.endsWith(".js")))
) {
files.push(entryPath);
}
}
Expand Down Expand Up @@ -196,6 +203,61 @@ const transformSolidSource = await loadTransformSolidSource();
const runtimeExportSets = await loadRuntimeExportSets();
const files = await listSourceFiles(sourceRoot);

// OpenTUI does not transform dependency TSX. The two entry paths must ship the
// frame without a second copy of the host's Solid or OpenTUI runtime.
async function bundleSidebar(hostRuntime: boolean): Promise<boolean> {
const outputFile = join(hostRuntime ? outputRoot : sourceRoot, sidebarBundleName);
const result = await Bun.build({
entrypoints: [fileURLToPath(import.meta.resolve("@cortexkit/opencode-sidebar"))],
target: "bun",
format: "esm",
external: [...runtimeSpecifiers],
plugins: [
{
name: "quota-sidebar-solid",
setup(build) {
build.onResolve({ filter: /^opentui:runtime-module:/ }, (args) => ({
path: args.path,
external: true,
}));
build.onLoad({ filter: /\.tsx$/ }, async (args) => {
const compiled = await transformSolidSource(
await readFile(args.path, "utf8"),
{
filename: args.path,
moduleName: runtimeModuleId("@opentui/solid"),
resolvePath: (specifier) =>
runtimeSpecifiers.has(specifier)
? runtimeModuleId(specifier)
: null,
},
);
const redirected = redirectBuiltinImports(
compiled,
runtimeExportSets,
args.path,
);
return {
contents: hostRuntime
? redirected
: redirected.replace(
/opentui:runtime-module:([^"']+)/g,
(_, id: string) => decodeURIComponent(id),
),
loader: "ts",
};
});
},
},
],
});
if (!result.success) throw new AggregateError(result.logs, "Shared sidebar build failed");
const output = result.outputs[0];
if (result.outputs.length !== 1 || !output)
throw new Error("Expected one shared sidebar bundle");
return writeIfChanged(outputFile, await output.text());
}

// Regenerate in place rather than removing the output tree first. Consumers
// read this directory live: a Parallels VM sharing the checkout caches
// directory lookups, and an rm-then-recreate left it holding a stale negative
Expand All @@ -204,6 +266,10 @@ const files = await listSourceFiles(sourceRoot);
// inode; only outputs whose source is gone are removed.
const expectedOutputs = new Set<string>();
let written = 0;
const sidebarOutput = join(outputRoot, sidebarBundleName);
expectedOutputs.add(sidebarOutput);
if (await bundleSidebar(true)) written++;
const rawSidebarWritten = await bundleSidebar(false);

for (const sourceFile of files) {
const relativePath = relative(sourceRoot, sourceFile);
Expand All @@ -217,19 +283,20 @@ for (const sourceFile of files) {
// the sidebar freezes on its first paint. The virtual ids are required so
// the compiled package binds the host process's single OpenTUI/Solid
// runtime instead of loading a second copy from the plugin package.
if (await compileTsx(transformSolidSource, sourceFile, outputFile, runtimeExportSets)) written++;
if (await compileTsx(transformSolidSource, sourceFile, outputFile, runtimeExportSets))
written++;
} else {
if (await copyPlainTypeScript(sourceFile, outputFile)) written++;
}
}

let stale = 0;
for (const existing of await listSourceFiles(outputRoot)) {
for (const existing of await listSourceFiles(outputRoot, true)) {
if (expectedOutputs.has(existing)) continue;
await rm(existing, { force: true });
stale += 1;
}

console.log(
`build-tui: checked ${files.length} file(s), wrote ${written}, unchanged ${files.length - written} in ${relative(pluginRoot, outputRoot)}${stale ? ` (removed ${stale} stale)` : ""}`,
`build-tui: checked ${files.length + 1} file(s), wrote ${written}, unchanged ${files.length + 1 - written} in ${relative(pluginRoot, outputRoot)}; raw sidebar wrote ${Number(rawSidebarWritten)}${stale ? ` (removed ${stale} stale)` : ""}`,
);
Loading
Loading