mirror of
https://github.com/stablyai/orca.git
synced 2026-10-08 16:02:37 +00:00
build(mobile): emit the page's terminal document factory beside the WebView's
Ruling 23, and the first half of C7.5b commit 2: the artifact the page will import. The page cannot run the native script, because building a function from a string needs `eval` and the page's policy refuses it, and it cannot run the modules either, because they are one singleton while the whole point of the factory is a scope per call. So one emitted body gets two wrappers. `buildTerminalDocumentFactoryBody` is now the shared half: the modules in order, the start sequence, the stop handle and the return. The native script wraps it in the declaration and the trailing call, exactly as before. The new `terminal-webview-document-factory.generated.ts` wraps the same lines in a `@ts-nocheck` module whose only other content is the type import and the annotated signature. One generator run writes both, so the page's factory cannot be a build behind the WebView's. `@ts-nocheck` covers this one generated file. Every line of its body is esbuild output from a module that was type-checked at its source, with `declare global` blocks and type re-exports already erased and constants already substituted; the one line a caller reads is the signature, and the generator writes it with its types. `TerminalDocument` joins `TerminalDocumentHost` in `document-host-seams.ts` as the shape the factory returns. The pin is byte equality. `document-factory-artifacts.test.ts` strips each wrapper and holds the remaining text equal, so the byte golden pins the page's artifact by construction rather than by a second golden; it also reads the file on disk against what the generator would write now, since that file is gitignored and built by postinstall, and it refuses a trailing call in the page's copy, which would start a document as the module was imported. The path joins `.gitignore` and the oxlint ignore list beside the engine artifact. The consumer census gains the generated file by name: it is the document, and its one import is the host contract its signature is written against. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
@@ -2,6 +2,7 @@ node_modules/
|
||||
src/terminal/terminal-webview-engine.generated.ts
|
||||
src/terminal/terminal-webview-engine-css.generated.ts
|
||||
src/terminal/terminal-webview-document-script.generated.ts
|
||||
src/terminal/terminal-webview-document-factory.generated.ts
|
||||
src/components/pr-sidebar/mermaid-webview-engine.generated.ts
|
||||
.expo/
|
||||
dist/
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"extends": ["../.oxlintrc.json"],
|
||||
"ignorePatterns": ["src/terminal/terminal-webview-engine.generated.ts"],
|
||||
"ignorePatterns": [
|
||||
"src/terminal/terminal-webview-engine.generated.ts",
|
||||
"src/terminal/terminal-webview-document-factory.generated.ts"
|
||||
],
|
||||
"rules": {
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"react/no-unescaped-entities": "off",
|
||||
|
||||
@@ -154,6 +154,19 @@ export async function emitTerminalDocumentModule(modulePath) {
|
||||
|
||||
const documentDirectory = path.join(import.meta.dirname, '..', 'src', 'terminal', 'document')
|
||||
|
||||
const GENERATED_HEADER =
|
||||
`// Generated by scripts/build-terminal-document-script.mjs. Do not edit.\n` +
|
||||
`// The source is mobile/src/terminal/document/, in the order\n` +
|
||||
`// scripts/terminal-document-module-order.mjs pins.`
|
||||
|
||||
export const TERMINAL_DOCUMENT_FACTORY_MODULE_PATH = path.join(
|
||||
import.meta.dirname,
|
||||
'..',
|
||||
'src',
|
||||
'terminal',
|
||||
'terminal-webview-document-factory.generated.ts'
|
||||
)
|
||||
|
||||
export const TERMINAL_DOCUMENT_SCRIPT_PATH = path.join(
|
||||
import.meta.dirname,
|
||||
'..',
|
||||
@@ -229,8 +242,8 @@ export async function emitDocumentedTerminalModule(moduleName) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The document's whole script: one factory whose body is every module in the order the document
|
||||
* had, the call sequence that starts them, and the handle that stops them again.
|
||||
* The factory's body: every module in the order the document had, the call sequence that starts
|
||||
* them, the handle that stops them again, and the return. Shared by both artifacts (ruling 23).
|
||||
*
|
||||
* Ruling 22. The concatenation already gave the modules one function scope with one local `scope`;
|
||||
* naming that scope a function is what makes it the shape both hosts run. Every call gets its own
|
||||
@@ -238,7 +251,7 @@ export async function emitDocumentedTerminalModule(moduleName) {
|
||||
* claim on the page — a second terminal is a second call. The native document is this function and
|
||||
* one call with no argument, which is what it has always been.
|
||||
*/
|
||||
export async function buildTerminalDocumentScript() {
|
||||
export async function buildTerminalDocumentFactoryBody() {
|
||||
const emitted = []
|
||||
// The scope object goes first: every module below reads it, and the document is one function
|
||||
// scope, so it has to exist before any of them run. It is the only part of the emitted script
|
||||
@@ -271,25 +284,62 @@ export async function buildTerminalDocumentScript() {
|
||||
`${INDENT}}`
|
||||
]
|
||||
return [
|
||||
`function ${TERMINAL_DOCUMENT_FACTORY_NAME}(${HOST_PARAMETER}) {`,
|
||||
...emitted,
|
||||
...calls,
|
||||
...stopBody,
|
||||
`${INDENT}return { send: handleMsg, stop: stop };`,
|
||||
`${INDENT}return { send: handleMsg, stop: stop };`
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/** The document's script, as the native WebView carries it: the factory, then the one call. */
|
||||
export async function buildTerminalDocumentScript() {
|
||||
const body = await buildTerminalDocumentFactoryBody()
|
||||
return [
|
||||
`function ${TERMINAL_DOCUMENT_FACTORY_NAME}(${HOST_PARAMETER}) {`,
|
||||
body,
|
||||
`}`,
|
||||
`${TERMINAL_DOCUMENT_FACTORY_NAME}();`
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* The same factory, as a module the page imports.
|
||||
*
|
||||
* Ruling 23: one emitted body, two wrappers. The page cannot run the native script — building a
|
||||
* function from a string needs `eval`, which its policy refuses — and it cannot run the modules
|
||||
* either, because they are one singleton and the whole point of the factory is a scope per call.
|
||||
* So it imports this, whose body is the native factory's body line for line; the artifact test
|
||||
* holds the two equal, which is how the byte golden ends up pinning this file too.
|
||||
*
|
||||
* `@ts-nocheck` covers exactly one generated file. Every line below is esbuild output from a module
|
||||
* that was type-checked at its source, with its `declare global` blocks and type re-exports already
|
||||
* erased and its constants already substituted; the one line a caller reads is the signature, and
|
||||
* the generator writes that with its types.
|
||||
*/
|
||||
export async function buildTerminalDocumentFactoryModule() {
|
||||
const body = await buildTerminalDocumentFactoryBody()
|
||||
return [
|
||||
GENERATED_HEADER,
|
||||
'// @ts-nocheck -- ruling 23: the body is emitted text, type-checked at each source module.',
|
||||
`import type { TerminalDocument, TerminalDocumentHost } from './document/document-host-seams'`,
|
||||
'',
|
||||
`export function ${TERMINAL_DOCUMENT_FACTORY_NAME}(`,
|
||||
`${INDENT}${HOST_PARAMETER}: TerminalDocumentHost`,
|
||||
`): TerminalDocument {`,
|
||||
body,
|
||||
`}`,
|
||||
''
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const script = await buildTerminalDocumentScript()
|
||||
await writeFile(
|
||||
TERMINAL_DOCUMENT_SCRIPT_PATH,
|
||||
`// Generated by scripts/build-terminal-document-script.mjs. Do not edit.\n` +
|
||||
`// The source is mobile/src/terminal/document/, in the order\n` +
|
||||
`// scripts/terminal-document-module-order.mjs pins.\n` +
|
||||
`export const TERMINAL_DOCUMENT_SCRIPT = ${JSON.stringify(script)}\n`
|
||||
`${GENERATED_HEADER}\nexport const TERMINAL_DOCUMENT_SCRIPT = ${JSON.stringify(script)}\n`
|
||||
)
|
||||
// One run writes both, so the page's factory can never be a build behind the WebView's.
|
||||
await writeFile(TERMINAL_DOCUMENT_FACTORY_MODULE_PATH, await buildTerminalDocumentFactoryModule())
|
||||
}
|
||||
|
||||
if (process.argv[1] === import.meta.filename) {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
buildTerminalDocumentFactoryBody,
|
||||
buildTerminalDocumentFactoryModule,
|
||||
buildTerminalDocumentScript,
|
||||
TERMINAL_DOCUMENT_FACTORY_MODULE_PATH,
|
||||
TERMINAL_DOCUMENT_FACTORY_NAME
|
||||
} from '../../../scripts/build-terminal-document-script.mjs'
|
||||
|
||||
/**
|
||||
* The two artifacts the generator writes, held to one body (ruling 23).
|
||||
*
|
||||
* The WebView gets a string it loads; the page gets a module it imports, because building a
|
||||
* function from that string needs `eval` and the page's policy refuses it. Two files is the cost of
|
||||
* that, and the risk is the obvious one: they drift, and the terminal on the page stops being the
|
||||
* terminal on the phone while every other test stays green. So the wrappers are stripped and the
|
||||
* remainder compared byte for byte, which is also what makes the byte golden pin the page's file.
|
||||
*/
|
||||
|
||||
/** What is left of the native script once its declaration line, closing brace and call are gone. */
|
||||
function nativeFactoryBody(script: string): string {
|
||||
const open = `function ${TERMINAL_DOCUMENT_FACTORY_NAME}(host) {\n`
|
||||
const close = `\n}\n${TERMINAL_DOCUMENT_FACTORY_NAME}();`
|
||||
expect(script.startsWith(open), 'the native script opens with the factory').toBe(true)
|
||||
expect(script.endsWith(close), 'the native script ends with the closing brace and the call').toBe(
|
||||
true
|
||||
)
|
||||
return script.slice(open.length, script.length - close.length)
|
||||
}
|
||||
|
||||
/** What is left of the page module once its header, directive, import and signature are gone. */
|
||||
function pageFactoryBody(module: string): string {
|
||||
const open = `): TerminalDocument {\n`
|
||||
const at = module.indexOf(open)
|
||||
expect(at, 'the page module declares the annotated signature').toBeGreaterThan(0)
|
||||
const close = '\n}\n'
|
||||
expect(module.endsWith(close), 'the page module ends with the closing brace').toBe(true)
|
||||
return module.slice(at + open.length, module.length - close.length)
|
||||
}
|
||||
|
||||
describe('the two terminal document artifacts', () => {
|
||||
it('carry the same factory body, byte for byte', async () => {
|
||||
const body = await buildTerminalDocumentFactoryBody()
|
||||
expect(nativeFactoryBody(await buildTerminalDocumentScript())).toBe(body)
|
||||
expect(pageFactoryBody(await buildTerminalDocumentFactoryModule())).toBe(body)
|
||||
})
|
||||
|
||||
it('is on disk as the generator would write it now', async () => {
|
||||
// The page's file is gitignored and built by postinstall, so a tree whose modules moved after
|
||||
// the last build would import yesterday's document. The native script's staleness is already
|
||||
// caught by the byte golden; this is the same reading for the file beside it.
|
||||
const onDisk = await readFile(TERMINAL_DOCUMENT_FACTORY_MODULE_PATH, 'utf8')
|
||||
expect(onDisk).toBe(await buildTerminalDocumentFactoryModule())
|
||||
})
|
||||
|
||||
it('gives the page a factory it can call and no call of its own', async () => {
|
||||
// A trailing call would start a document as the module was imported, which is the parse-time
|
||||
// work ruling 20 removed — and on the page it would run before any host element existed.
|
||||
const module = await buildTerminalDocumentFactoryModule()
|
||||
expect(module).toContain(`export function ${TERMINAL_DOCUMENT_FACTORY_NAME}(`)
|
||||
expect(module).not.toContain(`\n${TERMINAL_DOCUMENT_FACTORY_NAME}();`)
|
||||
})
|
||||
})
|
||||
@@ -56,6 +56,18 @@ export type TerminalDocumentHostSeams = {
|
||||
*/
|
||||
export type TerminalDocumentHost = Partial<TerminalDocumentHostSeams>
|
||||
|
||||
/**
|
||||
* A running document: the two things a host can do to one it has started.
|
||||
*
|
||||
* `send` is the router the WebView already reached through its message listener, which the page
|
||||
* calls directly. `stop` runs every module's stop and takes back the frames the document is owed;
|
||||
* the page's dispose calls it, and the WebView never does.
|
||||
*/
|
||||
export type TerminalDocument = {
|
||||
send: (message: Record<string, unknown>) => void
|
||||
stop: () => void
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
ReactNativeWebView?: { postMessage: (message: string) => void }
|
||||
|
||||
@@ -25,7 +25,10 @@ const ALLOWED_INSIDE_TERMINAL = new Set([
|
||||
'terminal-webview-html.ts',
|
||||
'terminal-webview-html.web.ts',
|
||||
// Test scaffolding that runs the WebView's own document text; it is not shipped in either build.
|
||||
'terminal-webview-mouse-test-harness.ts'
|
||||
'terminal-webview-mouse-test-harness.ts',
|
||||
// Generated, and it *is* the document: its body is the modules' own emitted text (ruling 23).
|
||||
// Its one import is the host contract the signature is written against.
|
||||
'terminal-webview-document-factory.generated.ts'
|
||||
])
|
||||
|
||||
const FORBIDDEN_ABOVE_THE_CONTRACT = [
|
||||
|
||||
Reference in New Issue
Block a user