mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-10 07:11:25 +03:00
perf(site): update Text.js to 1.6.0 (#62269)
This commit is contained in:
@@ -971,10 +971,10 @@
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@solidjs/router": "catalog:",
|
||||
"@solidjs/start": "catalog:",
|
||||
"@textjs/app": "1.5.0",
|
||||
"@textjs/core": "1.5.0",
|
||||
"@textjs/opencode": "1.5.0",
|
||||
"@textjs/react": "1.5.0",
|
||||
"@textjs/app": "1.6.0",
|
||||
"@textjs/core": "1.6.0",
|
||||
"@textjs/opencode": "1.6.0",
|
||||
"@textjs/react": "1.6.0",
|
||||
"fuzzysort": "3.1.0",
|
||||
"nitro": "3.0.260903-beta",
|
||||
"number-flow": "0.6.2",
|
||||
@@ -2557,13 +2557,13 @@
|
||||
|
||||
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.8", "", {}, "sha512-BfEvehNpOT75r5Ksc5xW6NZuXujTfb7nlSEyVu4XHG3gdxNg1KqXruWbDewXOUaUYIo4oRbSfkjIajz4MAT8tA=="],
|
||||
|
||||
"@textjs/app": ["@textjs/app@1.5.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "^1.32.2", "@textjs/core": "1.5.0", "@textjs/opencode": "1.5.0", "@textjs/react": "1.5.0", "@vitejs/plugin-react": "^4.5.2", "vite": "^6.3.5", "zod": "^4" }, "peerDependencies": { "react": "^19.0.0", "react-dom": "^19.0.0" }, "bin": { "text": "bin/text.js" } }, "sha512-+FFKhriOCzrIvn2/Qa7CxTKkRwASf8+y+EVbDEIZln1nDgJSgr9t4vtbv1Is+HKoe8tK0CZhXGzN0Mqyb8WnbA=="],
|
||||
"@textjs/app": ["@textjs/app@1.6.0", "", { "dependencies": { "@modelcontextprotocol/sdk": "^1.32.2", "@textjs/core": "1.6.0", "@textjs/opencode": "1.6.0", "@textjs/react": "1.6.0", "@vitejs/plugin-react": "^4.5.2", "vite": "^6.3.5", "zod": "^4" }, "peerDependencies": { "react": "^19.0.0", "react-dom": "^19.0.0" }, "bin": { "text": "bin/text.js" } }, "sha512-aK0NR5IQQTDpBTG4FXVVIal2KfuYlT+O/B/Yle51oDodf/HdAQq7cYbEBSiBXkeGnS3aUiRJw2OE/+tFQYLC6g=="],
|
||||
|
||||
"@textjs/core": ["@textjs/core@1.5.0", "", { "dependencies": { "yoga-layout": "^3.2.1" } }, "sha512-1JAXz2KS/VytPhd92AmvebghOPMQKxKpAzvIhiWI/x9hKEUR4pAmug9cov7l7BUycXiNowxRIfJicJhBNgUIYA=="],
|
||||
"@textjs/core": ["@textjs/core@1.6.0", "", { "dependencies": { "yoga-layout": "3.2.1" } }, "sha512-ubN6p4PAjQpurUlIP/EyA/2pEGgB/UXb3sMCTDmj3nqtw9dGBbiOEzwuLCc232Nqk5VuaAE8VcZHG/Wvs7GVFw=="],
|
||||
|
||||
"@textjs/opencode": ["@textjs/opencode@1.5.0", "", { "bin": { "textjs-opencode": "bin/textjs-opencode.js" } }, "sha512-eSX6nkFmbmOhWJSxDTNrLBy216M0cGECetCb3pkAw8s4HGEqEDPqLOjFtQX9neHwxAqgq2H4eHA3+0clDWtyHA=="],
|
||||
"@textjs/opencode": ["@textjs/opencode@1.6.0", "", { "bin": { "textjs-opencode": "bin/textjs-opencode.js" } }, "sha512-Aa6Z8rDQxZAHFFKpAzNhb34Y90Ko7gyhOpkDxjdv2LeisZLtKuggL1nZW7s4AkVHpe858rTgqkoc2PVlwUA3WQ=="],
|
||||
|
||||
"@textjs/react": ["@textjs/react@1.5.0", "", { "dependencies": { "@textjs/core": "1.5.0", "react-reconciler": "^0.34.0", "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.0.0", "react-dom": "^19.0.0" }, "optionalPeers": ["react-dom"] }, "sha512-wSSZDC7P3kW4fbJoyvFuXBypFwyOJ7qOYk3DYnI6eVezOaQ5pkgG5KWSneApVXsg0q+RAo68MXFJTcJfI46N/g=="],
|
||||
"@textjs/react": ["@textjs/react@1.6.0", "", { "dependencies": { "@textjs/core": "1.6.0", "react-reconciler": "^0.34.0", "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.0.0", "react-dom": "^19.0.0" }, "optionalPeers": ["react-dom"] }, "sha512-KBNGzUBgyV0v+vLQ02M8Efew2SPujL8L9UUDrhFQv4CZdFP/OxJ/JNi+V2KTj6MQanIRypZqm8TRNdP+ZtKdsQ=="],
|
||||
|
||||
"@trickfilm400/rollup-plugin-off-main-thread": ["@trickfilm400/rollup-plugin-off-main-thread@3.0.0-pre1", "", { "dependencies": { "ejs": "^3.1.10", "json5": "^2.2.3", "magic-string": "^0.30.21", "string.prototype.matchall": "^4.0.12" } }, "sha512-/67zpWDBLV+oYAEL682s1ktXL0HgqX76f6gaVGkGnVZlBbm1zd0v4Bz8MFF2GGhoX9rvfq3KSQHubFHwa6w6/Q=="],
|
||||
|
||||
|
||||
@@ -27,10 +27,10 @@
|
||||
"@solidjs/meta": "catalog:",
|
||||
"@solidjs/router": "catalog:",
|
||||
"@solidjs/start": "catalog:",
|
||||
"@textjs/app": "1.5.0",
|
||||
"@textjs/core": "1.5.0",
|
||||
"@textjs/opencode": "1.5.0",
|
||||
"@textjs/react": "1.5.0",
|
||||
"@textjs/app": "1.6.0",
|
||||
"@textjs/core": "1.6.0",
|
||||
"@textjs/opencode": "1.6.0",
|
||||
"@textjs/react": "1.6.0",
|
||||
"fuzzysort": "3.1.0",
|
||||
"nitro": "3.0.260903-beta",
|
||||
"number-flow": "0.6.2",
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
/textjs/picker-*
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
# Yoga's WebAssembly, named by a hash of its bytes (textjs/script/build.ts).
|
||||
/textjs/yoga-*
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
# Link preview cards, named by a hash of the PNG (script/og.ts).
|
||||
/og/*
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// anomalyco/textjs (MIT), which isn't published to npm; it is downloaded at the pinned commit into
|
||||
// .upstream/. If the download fails the build still writes the picker, and try() falls back to a
|
||||
// link to textjs.org/opencode.
|
||||
import { createHash } from "node:crypto"
|
||||
import { mkdir, rm } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
|
||||
@@ -14,6 +15,13 @@ const root = path.resolve(import.meta.dir, "..")
|
||||
const upstream = path.join(root, ".upstream")
|
||||
const out = path.resolve(root, "../public/textjs")
|
||||
|
||||
// Text.js 1.6 loads Yoga from a .wasm file and the bidi engine from its own chunk under the
|
||||
// `@textjs/client` resolve condition, which `text build` sets. Bun's bundler leaves the loader's
|
||||
// `new URL("@textjs/core/yoga.wasm", import.meta.url)` alone, so the entries load the file
|
||||
// themselves (`initYoga({ wasm })`) from a name hashed by its contents, defined into them below.
|
||||
const yoga = await Bun.file(new URL(import.meta.resolve("@textjs/core/yoga.wasm"))).bytes()
|
||||
const yogaName = `yoga-${createHash("sha256").update(yoga).digest("hex").slice(0, 8)}.wasm`
|
||||
|
||||
const lite = await fetchUpstream().then(
|
||||
() => true,
|
||||
(error) => {
|
||||
@@ -29,22 +37,30 @@ const result = await Bun.build({
|
||||
format: "esm",
|
||||
splitting: true,
|
||||
minify: true,
|
||||
conditions: ["@textjs/client"],
|
||||
naming: { entry: "[name]-[hash].[ext]", chunk: "chunk-[hash].[ext]" },
|
||||
define: { "process.env.NODE_ENV": '"production"' },
|
||||
define: {
|
||||
"process.env.NODE_ENV": '"production"',
|
||||
__TEXTJS_YOGA__: JSON.stringify(`./${yogaName}`),
|
||||
},
|
||||
})
|
||||
if (!result.success) throw new AggregateError(result.logs, "textjs build failed")
|
||||
await Bun.write(path.join(out, yogaName), yoga)
|
||||
const entries = result.outputs.filter((output) => output.kind === "entry-point")
|
||||
const manifest = Object.fromEntries(
|
||||
entries.map((entry) => [path.basename(entry.path).split("-")[0], path.basename(entry.path)]),
|
||||
)
|
||||
await Bun.write(path.join(out, "manifest.json"), JSON.stringify(manifest))
|
||||
// What each app downloads: its entry plus every chunk it imports statically.
|
||||
// What each app downloads: its entry, every chunk it imports statically, and then the chunks it
|
||||
// fetches on demand (the bidi engine, the session dialogs), plus Yoga's wasm.
|
||||
const sizes = new Map(
|
||||
await Promise.all(
|
||||
result.outputs.map(async (output) => {
|
||||
const text = await Bun.file(output.path).text()
|
||||
const bytes = await Bun.file(output.path).bytes()
|
||||
const text = new TextDecoder().decode(bytes)
|
||||
const imports = [...text.matchAll(/from"\.\/([\w-]+\.js)"/g)].map((match) => match[1])
|
||||
return [path.basename(output.path), { bytes: text.length, gzip: Bun.gzipSync(text).length, imports }] as const
|
||||
const lazy = [...text.matchAll(/import\("\.\/([\w-]+\.js)"\)/g)].map((match) => match[1])
|
||||
return [path.basename(output.path), { bytes: bytes.length, gzip: Bun.gzipSync(text).length, imports, lazy }] as const
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -54,11 +70,21 @@ const closure = (file: string, seen = new Set<string>()): Set<string> => {
|
||||
sizes.get(file)?.imports.forEach((next) => closure(next, seen))
|
||||
return seen
|
||||
}
|
||||
const lazyClosure = (eager: string[]): Set<string> => {
|
||||
const seen = new Set<string>()
|
||||
const known = new Set(eager)
|
||||
for (const file of eager) sizes.get(file)?.lazy.forEach((next) => closure(next, seen))
|
||||
return new Set([...seen].filter((file) => !known.has(file)))
|
||||
}
|
||||
for (const [name, file] of Object.entries(manifest)) {
|
||||
const files = [...closure(file)]
|
||||
const kb = (key: "bytes" | "gzip") =>
|
||||
(files.reduce((sum, f) => sum + (sizes.get(f)?.[key] ?? 0), 0) / 1024).toFixed(1)
|
||||
console.log(`textjs: ${name} ${kb("bytes")} KB, ${kb("gzip")} KB gzip (${files.length} files)`)
|
||||
const kb = (key: "bytes" | "gzip", list: string[]) =>
|
||||
(list.reduce((sum, f) => sum + (sizes.get(f)?.[key] ?? 0), 0) / 1024).toFixed(1)
|
||||
console.log(`textjs: ${name} ${kb("bytes", files)} KB, ${kb("gzip", files)} KB gzip (${files.length} files)`)
|
||||
const lazy = [...lazyClosure(files)]
|
||||
if (lazy.length) console.log(`textjs: ${name} +${kb("gzip", lazy)} KB gzip on demand (${lazy.join(", ")})`)
|
||||
const yogaKb = `${(yoga.length / 1024).toFixed(1)} KB, ${(Bun.gzipSync(yoga).length / 1024).toFixed(1)} KB gzip`
|
||||
console.log(`textjs: ${name} +${yogaKb} yoga (${yogaName})`)
|
||||
}
|
||||
|
||||
async function fetchUpstream() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { initYoga } from "@textjs/core"
|
||||
import { renderToConsole } from "@textjs/react"
|
||||
import { EmbeddedOpenCode } from "../.upstream/opencode-console/embed"
|
||||
import { createDemoClient } from "../.upstream/opencode-console/lib/demo"
|
||||
@@ -6,7 +7,13 @@ import { createDemoClient } from "../.upstream/opencode-console/lib/demo"
|
||||
// in-memory backend scripts the agent, so nothing runs and nothing is installed. The source comes
|
||||
// from anomalyco/textjs (apps/opencode-console) at the commit script/build.ts pins.
|
||||
|
||||
// Text.js 1.6 fetches Yoga's WebAssembly instead of carrying it inline. The build names the file
|
||||
// after its contents (script/build.ts), and Bun doesn't resolve the loader's default
|
||||
// `new URL("@textjs/core/yoga.wasm", import.meta.url)`, so load it from next to the chunks.
|
||||
declare const __TEXTJS_YOGA__: string
|
||||
|
||||
export async function start() {
|
||||
await initYoga({ wasm: new URL(__TEXTJS_YOGA__, import.meta.url) })
|
||||
// The app sets its own theme (OpenCode dark), like the native TUI.
|
||||
const root = await renderToConsole(<EmbeddedOpenCode client={createDemoClient()} onExit={() => root.unmount()} />, {
|
||||
responsive: true,
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { initYoga } from "@textjs/core"
|
||||
import { Box, renderToConsole, Select, Text, useInput, type SelectOption } from "@textjs/react"
|
||||
|
||||
// opencode.install.pick(): the install commands and desktop builds as a Text.js list in the
|
||||
// DevTools console. The site passes the options, so this bundle holds no site data.
|
||||
|
||||
// Text.js 1.6 fetches Yoga's WebAssembly instead of carrying it inline; script/build.ts hashes the
|
||||
// file and defines its name. See src/lite.tsx.
|
||||
declare const __TEXTJS_YOGA__: string
|
||||
|
||||
type Input = { options: SelectOption<string>[]; selected?: string; dark: boolean }
|
||||
|
||||
export function pick(input: Input) {
|
||||
export async function pick(input: Input) {
|
||||
await initYoga({ wasm: new URL(__TEXTJS_YOGA__, import.meta.url) })
|
||||
return new Promise<string | undefined>((resolve) => {
|
||||
const done = (value?: string) => {
|
||||
root.then((app) => app.unmount())
|
||||
|
||||
Reference in New Issue
Block a user