diff --git a/mobile/scripts/build-terminal-document-fixture.mjs b/mobile/scripts/build-terminal-document-fixture.mjs index f4c65a09c49..885aca358d7 100644 --- a/mobile/scripts/build-terminal-document-fixture.mjs +++ b/mobile/scripts/build-terminal-document-fixture.mjs @@ -1,6 +1,6 @@ import { writeFile } from 'node:fs/promises' import path from 'node:path' -import * as esbuild from 'esbuild' +import { importTypeScriptModule } from './import-typescript-module.mjs' /** * Writes the committed copy of the terminal WebView document that @@ -36,19 +36,6 @@ export const TERMINAL_DOCUMENT_FIXTURE_PATH = path.join( export const ENGINE_JS_PLACEHOLDER = '__ORCA_TERMINAL_ENGINE_JS__' export const ENGINE_CSS_PLACEHOLDER = '__ORCA_TERMINAL_ENGINE_CSS__' -async function loadModule(entryPoint) { - const result = await esbuild.build({ - entryPoints: [entryPoint], - bundle: true, - format: 'esm', - platform: 'node', - write: false, - logLevel: 'silent' - }) - const code = result.outputFiles[0].text - return import(`data:text/javascript;base64,${Buffer.from(code, 'utf8').toString('base64')}`) -} - /** * The document with both generated sections replaced by their placeholders. * @@ -76,8 +63,8 @@ export function terminalDocumentFixture(document, engineJs, engineCss) { async function main() { const [{ XTERM_HTML }, { XTERM_ENGINE_JS, XTERM_ENGINE_CSS }] = await Promise.all([ - loadModule(entry), - loadModule(enginePath) + importTypeScriptModule(entry), + importTypeScriptModule(enginePath) ]) const fixture = terminalDocumentFixture(XTERM_HTML, XTERM_ENGINE_JS, XTERM_ENGINE_CSS) await writeFile(TERMINAL_DOCUMENT_FIXTURE_PATH, fixture) diff --git a/mobile/scripts/build-terminal-document-script.mjs b/mobile/scripts/build-terminal-document-script.mjs index f4b045541ef..ccdb8721d60 100644 --- a/mobile/scripts/build-terminal-document-script.mjs +++ b/mobile/scripts/build-terminal-document-script.mjs @@ -1,5 +1,7 @@ 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 @@ -20,6 +22,34 @@ import * as esbuild from 'esbuild' */ 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 opens an import the document does not need. */ function isImportLine(line) { return /^import[\s{'"]/.test(line) @@ -68,7 +98,15 @@ export async function emitTerminalDocumentModule(modulePath) { } kept.push(line.startsWith('export ') ? line.slice('export '.length) : line) } - const body = kept.join('\n').trim() + 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}`)) diff --git a/mobile/scripts/build-terminal-document-script.test.ts b/mobile/scripts/build-terminal-document-script.test.ts index 9d223e2cdf8..0520176a738 100644 --- a/mobile/scripts/build-terminal-document-script.test.ts +++ b/mobile/scripts/build-terminal-document-script.test.ts @@ -3,6 +3,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { emitTerminalDocumentModule } from './build-terminal-document-script.mjs' +import { terminalBackgroundFallback } from '../src/terminal/document/document-constants' /** * What the generator drops, what it keeps, and how it puts a module back into the document. @@ -61,4 +62,15 @@ describe('emitting one terminal document module', () => { ) ).toBe(' function f(v) {\n return v.a;\n }') }) + + it('substitutes a build-time constant the document carries as a literal', async () => { + const emitted = await emit( + "import { terminalBackgroundFallback } from '../src/terminal/document/document-constants'\n" + + 'export function paint() {\n' + + ' return terminalBackgroundFallback\n' + + '}\n' + ) + expect(emitted).toContain(JSON.stringify(terminalBackgroundFallback)) + expect(emitted).not.toContain('terminalBackgroundFallback') + }) }) diff --git a/mobile/scripts/import-typescript-module.mjs b/mobile/scripts/import-typescript-module.mjs new file mode 100644 index 00000000000..54f84ab800b --- /dev/null +++ b/mobile/scripts/import-typescript-module.mjs @@ -0,0 +1,21 @@ +import * as esbuild from 'esbuild' + +/** + * Imports a TypeScript module from a build script, by bundling it to a data URL. + * + * Node cannot import TypeScript and these scripts run outside the app's bundler, so the values the + * document is built from — the theme, the URL limits, the caret options — would otherwise have to be + * restated here. Restating them is what the generator exists to avoid. + */ +export async function importTypeScriptModule(entryPoint) { + const result = await esbuild.build({ + entryPoints: [entryPoint], + bundle: true, + format: 'esm', + platform: 'node', + write: false, + logLevel: 'silent' + }) + const code = result.outputFiles[0].text + return import(`data:text/javascript;base64,${Buffer.from(code, 'utf8').toString('base64')}`) +} diff --git a/mobile/src/terminal/document/document-constants.ts b/mobile/src/terminal/document/document-constants.ts new file mode 100644 index 00000000000..003ab55b741 --- /dev/null +++ b/mobile/src/terminal/document/document-constants.ts @@ -0,0 +1,15 @@ +import { colors } from '../../theme/mobile-theme' + +/** + * The build-time values the document's script text carries as literals. + * + * The document is a string, so it cannot import: today each of these is interpolated into a + * template literal at the site that needs it. A module cannot do that and still be the same + * program, so the generator substitutes these exports into the text it emits, and the web page + * imports the very same bindings. One source either way. + * + * Every export must be JSON-serialisable, because a substitution is a JSON literal. + */ + +/** The page background before a theme arrives, and the fallback when a theme omits one. */ +export const terminalBackgroundFallback = colors.terminalBg