mirror of
https://github.com/stablyai/orca.git
synced 2026-10-06 00:02:43 +00:00
The page mounts xterm itself, so it needs the engine's stylesheet and must never resolve the engine string: 612 KiB of minified IIFE built to be injected as text into a WebView document, unusable under the shell's `script-src 'self'` with no nested frame to load one into, and the largest single module the session route's closure would carry. Both lived in `terminal-webview-engine.generated.ts`, so one import of the CSS pulled the string in behind it. `build-terminal-webview-engine.mjs` now writes `terminal-webview-engine-css.generated.ts` beside it from the same read of `@xterm/xterm/css/xterm.css`, with the same comment strip and the same `http%3A//` scrub the no-external-URL gate wants. Gitignored beside its neighbour and written by the same postinstall step, so a fresh tree gets both or neither. `document-shell.ts` takes the CSS from the new module and the engine string from the old one; `build-terminal-document-fixture.mjs` and the two tests that hold both constants read them from their new homes. The document did not move: `terminal-document-golden.txt` is byte for byte what the last commit left, 105,968 bytes, and the payload digest is unchanged. The fence is `config/scripts/mobile-web-terminal-engine-closure.test.mjs`. It walks every module under `src/terminal/document/` as an entry point — the document is one script whose modules reach each other by side effect, so no single one of them roots a graph holding the rest — and asserts the engine string is in none of their closures, with two modules named as the precondition that the walk resolved anything at all. The native document's own closure is asserted to still hold both generated modules, so the first case cannot pass by the CSS having gone missing. And the third case plants a document module that imports the engine string in a scratch tree and shows the walk reports it, which is what makes the absence above a measurement. `mobileWebAppRouteClosure` is now a caller of `mobileWebAppEntryClosure`, which takes the entry points and an optional working directory; the route closure's own two entry points and its extensionless-specifier reason are unchanged. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
135 lines
5.1 KiB
JavaScript
135 lines
5.1 KiB
JavaScript
import { readFile, writeFile } from 'node:fs/promises'
|
|
import path from 'node:path'
|
|
import { createRequire } from 'node:module'
|
|
import * as esbuild from 'esbuild'
|
|
|
|
const require = createRequire(import.meta.url)
|
|
const scriptDir = import.meta.dirname
|
|
const mobileRoot = path.resolve(scriptDir, '..')
|
|
const outputPath = path.join(mobileRoot, 'src', 'terminal', 'terminal-webview-engine.generated.ts')
|
|
/**
|
|
* The stylesheet is written beside the engine rather than inside it because the two have different
|
|
* consumers. The WebView document needs both, as text; the web page needs the CSS and must never
|
|
* resolve the 612 KiB engine string, which is unusable under the shell's `script-src 'self'` and
|
|
* is the largest module the session route's closure would otherwise carry. One file each is what
|
|
* lets the page import one without reaching the other.
|
|
*/
|
|
const cssOutputPath = path.join(
|
|
mobileRoot,
|
|
'src',
|
|
'terminal',
|
|
'terminal-webview-engine-css.generated.ts'
|
|
)
|
|
const target = 'chrome74'
|
|
|
|
const packages = ['@xterm/xterm', '@xterm/addon-unicode11', '@xterm/addon-webgl']
|
|
|
|
async function readPackageVersion(packageName) {
|
|
// Why: a package.json module specifier must use '/' — path.join emits '\' on
|
|
// Windows, yielding an unresolvable bare specifier that fails postinstall there.
|
|
const packageJsonPath = require.resolve(`${packageName}/package.json`)
|
|
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'))
|
|
return `${packageName}@${packageJson.version}`
|
|
}
|
|
|
|
function htmlText(value, closingTag) {
|
|
return value.replace(new RegExp(`</${closingTag}`, 'gi'), `<\\/${closingTag}`)
|
|
}
|
|
|
|
async function buildEngineJs() {
|
|
const result = await esbuild.build({
|
|
stdin: {
|
|
contents: `
|
|
import { Terminal } from '@xterm/xterm'
|
|
import { Unicode11Addon } from '@xterm/addon-unicode11'
|
|
import { WebglAddon } from '@xterm/addon-webgl'
|
|
|
|
// Why: xterm reaches for these runtime APIs on the terminal-bringup path,
|
|
// and esbuild lowers syntax but not runtime APIs. Guarded shims let the
|
|
// chrome74-syntax bundle actually run on old WebViews (the #7030 goal)
|
|
// instead of throwing at construction and only surfacing the error overlay.
|
|
|
|
// WeakRef (Chrome 84+): lazily constructed in window-tracking paths.
|
|
// Strong retention is fine for a single-document terminal WebView.
|
|
if (typeof window.WeakRef === 'undefined') {
|
|
window.WeakRef = function WeakRefShim(target) { this.__target = target }
|
|
window.WeakRef.prototype.deref = function () { return this.__target }
|
|
}
|
|
|
|
// structuredClone (Chrome 98+): xterm clones its plain-data DEC-mode default
|
|
// objects at Terminal construction; a JSON round-trip clones those correctly
|
|
// (any undefined-valued keys drop, but every reader treats absent == undefined).
|
|
if (typeof window.structuredClone === 'undefined') {
|
|
window.structuredClone = function (value) { return JSON.parse(JSON.stringify(value)) }
|
|
}
|
|
|
|
// Element.prototype.replaceChildren (Chrome 86+): used on the row/selection
|
|
// render path; polyfill via remove-all + append (Chrome 54+, under the floor).
|
|
if (typeof Element !== 'undefined' && !Element.prototype.replaceChildren) {
|
|
Element.prototype.replaceChildren = function () {
|
|
while (this.firstChild) this.removeChild(this.firstChild)
|
|
this.append.apply(this, arguments)
|
|
}
|
|
}
|
|
|
|
window.Terminal = Terminal
|
|
window.Unicode11Addon = { Unicode11Addon }
|
|
window.WebglAddon = { WebglAddon }
|
|
`,
|
|
resolveDir: mobileRoot,
|
|
sourcefile: 'terminal-webview-engine-entry.js'
|
|
},
|
|
bundle: true,
|
|
format: 'iife',
|
|
minify: true,
|
|
platform: 'browser',
|
|
target,
|
|
legalComments: 'none',
|
|
write: false,
|
|
logLevel: 'silent'
|
|
})
|
|
|
|
return result.outputFiles[0].text
|
|
}
|
|
|
|
async function main() {
|
|
const [engineJs, rawEngineCss, ...versions] = await Promise.all([
|
|
buildEngineJs(),
|
|
readFile(require.resolve('@xterm/xterm/css/xterm.css'), 'utf8'),
|
|
...packages.map(readPackageVersion)
|
|
])
|
|
// Why: the no-external-URL regression gate bans http(s):// anywhere in the
|
|
// terminal document. These xmlns URIs live inside data: URLs (never fetched);
|
|
// percent-encoding the scheme colon satisfies the gate and URI-decodes back
|
|
// before the SVG is parsed.
|
|
const engineCss = rawEngineCss
|
|
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
.replace(/http:\/\/www\.w3\.org\/2000\/svg/g, 'http%3A//www.w3.org/2000/svg')
|
|
|
|
const header = [
|
|
'// Generated by scripts/build-terminal-webview-engine.mjs.',
|
|
`// Packages: ${versions.join(', ')}.`,
|
|
`// Target: ${target}. Do not edit by hand; regenerate via pnpm postinstall.`
|
|
]
|
|
await Promise.all([
|
|
writeFile(
|
|
outputPath,
|
|
[
|
|
...header,
|
|
`export const XTERM_ENGINE_JS = ${JSON.stringify(htmlText(engineJs, 'script'))}`,
|
|
''
|
|
].join('\n')
|
|
),
|
|
writeFile(
|
|
cssOutputPath,
|
|
[
|
|
...header,
|
|
`export const XTERM_ENGINE_CSS = ${JSON.stringify(htmlText(engineCss, 'style'))}`,
|
|
''
|
|
].join('\n')
|
|
)
|
|
])
|
|
}
|
|
|
|
await main()
|