mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
The ninth named group, and a pure query: it reads no shared state, so it has no qualifier sites at all. Two things this group forced. The generator now drops lint directive lines before the transform, because a directive inside an expression makes esbuild parenthesise that expression to keep the comment where it was, and those parentheses are tokens the document does not have. And the two regexes keep their `no-useless-escape` escapes behind a line-scoped disable, for the same reason the theme group keeps `Math.pow`. One name the document declares twice in one function stays `var`. Two block-scoped declarations would be two bindings where the document has one, and esbuild renames the inner one to say so. Counts: qualified 0, scope declarations 0, rebindings 31, braced bodies 20, unbound catches 0, number properties 2. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
131 lines
4.7 KiB
JavaScript
131 lines
4.7 KiB
JavaScript
import { readFile } from 'node:fs/promises'
|
|
import path from 'node:path'
|
|
import * as esbuild from 'esbuild'
|
|
import { importTypeScriptModule } from './import-typescript-module.mjs'
|
|
|
|
/**
|
|
* 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 = ' '
|
|
|
|
const constantsPath = path.join(
|
|
import.meta.dirname,
|
|
'..',
|
|
'src',
|
|
'terminal',
|
|
'document',
|
|
'document-constants.ts'
|
|
)
|
|
|
|
let substitutions = null
|
|
|
|
/**
|
|
* `document-constants.ts` as esbuild `define` entries.
|
|
*
|
|
* Substitution happens after the import lines are dropped, when the names are free again; while the
|
|
* import is still there esbuild sees a bound name and leaves it alone, which is the correct thing
|
|
* for the page and the wrong thing for the document.
|
|
*/
|
|
async function documentConstantSubstitutions() {
|
|
if (substitutions === null) {
|
|
const module = await importTypeScriptModule(constantsPath)
|
|
substitutions = Object.fromEntries(
|
|
Object.entries(module).map(([name, value]) => [name, JSON.stringify(value)])
|
|
)
|
|
}
|
|
return substitutions
|
|
}
|
|
|
|
/**
|
|
* Whether a line is a lint directive.
|
|
*
|
|
* These are removed before the transform, not after it: a directive inside an expression makes
|
|
* esbuild wrap that expression in parentheses to keep the comment where it was, and those
|
|
* parentheses are tokens the document does not have. They are tooling metadata about the source,
|
|
* not part of the program the WebView runs.
|
|
*/
|
|
function isLintDirectiveLine(line) {
|
|
return /^\s*\/\/\s*oxlint-disable/.test(line)
|
|
}
|
|
|
|
/** Whether a line opens an import the document does not need. */
|
|
function isImportLine(line) {
|
|
return /^import[\s{'"]/.test(line)
|
|
}
|
|
|
|
/** Whether a statement that started on this line also ended on it. */
|
|
function closesOnSameLine(line, closer) {
|
|
return line.includes(closer)
|
|
}
|
|
|
|
/**
|
|
* 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 program = source
|
|
.split('\n')
|
|
.filter((line) => !isLintDirectiveLine(line))
|
|
.join('\n')
|
|
const { code } = await esbuild.transform(program, {
|
|
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 = []
|
|
// esbuild wraps a long import or export list across lines, so both are skipped to their closer
|
|
// rather than by their first line. An export list dropped by its keyword alone would leave a
|
|
// bare block statement in the document, and an import list would leave its names loose.
|
|
let skipUntil = null
|
|
for (const line of code.split('\n')) {
|
|
if (skipUntil !== null) {
|
|
if (closesOnSameLine(line, skipUntil)) {
|
|
skipUntil = null
|
|
}
|
|
continue
|
|
}
|
|
if (isImportLine(line)) {
|
|
skipUntil = closesOnSameLine(line, ' from ') || closesOnSameLine(line, ';') ? null : ' from '
|
|
continue
|
|
}
|
|
if (line.startsWith('export {')) {
|
|
skipUntil = closesOnSameLine(line, '}') ? null : '}'
|
|
continue
|
|
}
|
|
kept.push(line.startsWith('export ') ? line.slice('export '.length) : line)
|
|
}
|
|
const define = await documentConstantSubstitutions()
|
|
const substituted = await esbuild.transform(kept.join('\n'), {
|
|
loader: 'js',
|
|
format: 'esm',
|
|
target: 'chrome74',
|
|
minify: false,
|
|
define
|
|
})
|
|
const body = substituted.code.trim()
|
|
return body
|
|
.split('\n')
|
|
.map((line) => (line.length === 0 ? line : `${INDENT}${line}`))
|
|
.join('\n')
|
|
}
|