perf(site): update Text.js to 1.6.0 (#62269)

This commit is contained in:
Adam
2026-11-05 22:26:29 -06:00
committed by GitHub
Unverified
parent 82229fb7d4
commit d86331ce73
6 changed files with 62 additions and 20 deletions
+8 -8
View File
@@ -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=="],
+4 -4
View File
@@ -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",
+3
View File
@@ -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
+33 -7
View File
@@ -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() {
+7
View File
@@ -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,
+7 -1
View File
@@ -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())