Files
orca/mobile/scripts/build-terminal-document-script.mjs
T
Jinwoo-H 0e9f38b8a5 refactor(mobile): make the mouse-report cell a module the page can import
The first of the twelve groups the document already names. `*-injected.ts` has
been splicing JS strings into the document for a while, and tests evaluate
those strings, so the one-source-two-consumers shape is already there; what is
missing is that a string cannot be imported by the web page, typechecked, or
linted. This turns one of them into a module and adds the generator that puts
it back into the document.

The generator is a transform, not a bundle: a bundler orders its output by the
dependency graph, and the document's order is part of what the equivalence test
holds fixed. Imports are dropped rather than resolved, because inside the
document every name is already in scope — that is what the single IIFE means —
and `document-externals.ts` declares the names whose groups have not moved yet
and emits nothing at all. esbuild prints an ESM module's exports as a trailing
block, so that block is dropped whole rather than by its keyword; leaving the
keyword behind would put a bare block statement in the document.

Both sides of the comparison now go through that same printer before being
read. Otherwise every choice the printer makes — semicolons, property
shorthand, quote style — reads as a difference in the program when it is a
difference in who typed it, and each would need its own rule. A script that
does not parse is reported as a refusal naming its side, not thrown.

`let` is contextual outside strict mode, so acorn reports it as a name and not
as a keyword; without that the var-to-let rewrite the linter performs would be
refused on every reassigned local.

The group's counts are pinned exactly: nine references gained the qualifier
(`term` seven times, `panX` and `panY` once each), nine locals became `const`
or `let`, thirteen one-statement `if` bodies gained braces, no declaration
moved onto the scope object and no catch clause lost a binding.

The document is untouched, so the byte pin from 3006d8dfdf is still green.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-09-20 06:21:34 -04:00

68 lines
2.7 KiB
JavaScript

import { readFile } from 'node:fs/promises'
import * as esbuild from 'esbuild'
/**
* Turns one module of the in-WebView terminal document back into the script text the document
* carries.
*
* The document is a string the native WebView loads, so its parts cannot be imported by anything;
* the web page needs exactly those parts and must not re-implement them. So the parts are modules,
* and this is the other direction: the modules' declarations, with their imports removed and their
* exports unmarked, spliced into the one function scope the document has always been.
*
* Imports are dropped rather than resolved because inside the document every name is already in
* scope — that is what the single IIFE means. `document-externals.ts` declares the names that have
* not moved into modules yet, and it emits nothing at all.
*
* `esbuild` does the TypeScript, as it already does for the xterm engine beside this file. It is a
* transform and not a bundle: a bundler would order the output by its dependency graph, and the
* document's order is part of what the equivalence test holds fixed.
*/
const INDENT = ' '
/** Whether a line opens an import the document does not need. */
function isImportLine(line) {
return /^import[\s{'"]/.test(line)
}
/**
* The emitted text of one module: transpiled, unexported, un-imported and indented into the IIFE.
*
* Multi-line imports are handled by dropping through to the line that closes them, which esbuild's
* output makes safe: it prints one import per line.
*/
export async function emitTerminalDocumentModule(modulePath) {
const source = await readFile(modulePath, 'utf8')
const { code } = await esbuild.transform(source, {
loader: 'ts',
format: 'esm',
target: 'chrome74',
// The document is read by people as well as by a WebView, and the equivalence test compares
// tokens, so keeping the printer's own layout costs nothing and keeps the diff legible.
minify: false
})
const kept = []
let inExportList = false
for (const line of code.split('\n')) {
if (inExportList) {
inExportList = !line.startsWith('}')
continue
}
if (isImportLine(line)) {
continue
}
// esbuild prints an ESM module's exports as one trailing `export { … };` block. Dropping only
// the keyword would leave a bare block statement in the document.
if (line.startsWith('export {')) {
inExportList = !line.includes('}')
continue
}
kept.push(line.startsWith('export ') ? line.slice('export '.length) : line)
}
const body = kept.join('\n').trim()
return body
.split('\n')
.map((line) => (line.length === 0 ? line : `${INDENT}${line}`))
.join('\n')
}