blob: a3f476f1201a625ea280fe03be0f4b87efacf73a [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// Build the cursor overlay renderer bundle + preload into apps/desktop/dist/overlay.
// - cursor-overlay.js: the Canvas engine host (IIFE, browser). The `js→ts` resolve
// shim lets us bundle the engine's NodeNext `./x.js` imports straight from source.
// - cursor-overlay-preload.cjs: receive-only main→renderer bridge (CJS, electron external).
// - cursor-overlay.html: copied verbatim.
import * as esbuild from 'esbuild';
import { resolve, dirname, join } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { existsSync } from 'node:fs';
import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
const here = dirname(fileURLToPath(import.meta.url));
const desktop = resolve(here, '..', 'apps', 'desktop');
const srcOverlay = join(desktop, 'src', 'overlay');
const outDir = join(desktop, 'dist', 'overlay');
const jsToTs = {
name: 'js-to-ts',
setup(build) {
build.onResolve({ filter: /^\.\.?\/.*\.js$/ }, (args) => {
const source = resolve(args.resolveDir, args.path.replace(/\.js$/, '.ts'));
return existsSync(source) ? { path: source } : undefined;
});
},
};
/** Build the overlay renderer bundle + preload + html into dist/overlay. */
export async function buildCursorOverlay({ logLevel = 'info' } = {}) {
await mkdir(outDir, { recursive: true });
await buildBrowserDialogDesignTokens();
await esbuild.build({
entryPoints: [join(srcOverlay, 'cursor-overlay.ts')],
bundle: true,
format: 'iife',
platform: 'browser',
target: 'chrome120',
outfile: join(outDir, 'cursor-overlay.js'),
plugins: [jsToTs],
logLevel,
});
await esbuild.build({
entryPoints: [join(srcOverlay, 'cursor-overlay-preload.ts')],
bundle: true,
format: 'cjs',
platform: 'node',
external: ['electron'],
outfile: join(outDir, 'cursor-overlay-preload.cjs'),
logLevel,
});
await copyFile(join(srcOverlay, 'cursor-overlay.html'), join(outDir, 'cursor-overlay.html'));
await buildPermissionOverlay({ logLevel });
await buildComputerUsePip({ logLevel });
await buildPetOverlay({ logLevel });
return outDir;
}
/**
* Browser-backed recovery dialogs run outside the React renderer, but they
* still consume the same generated Astryx scale and Maka palette authority.
* Keep only maka-tokens.css's token/palette prefix: its later base/component
* recipes target the main document and must not leak into a standalone card.
*/
async function buildBrowserDialogDesignTokens() {
const renderer = join(desktop, 'src', 'renderer');
const [astryxTheme, makaTokens] = await Promise.all([
readFile(join(renderer, 'astryx-theme', 'maka.css'), 'utf8'),
readFile(join(renderer, 'maka-tokens.css'), 'utf8'),
]);
const astryxComponentsMarker = '\n .astryx-heading[data-level="1"] {';
const astryxTokenEnd = astryxTheme.indexOf(astryxComponentsMarker);
const typeRolesMarker =
'/* =============================================================================\n TYPE ROLES';
const tokenEnd = makaTokens.indexOf(typeRolesMarker);
if (astryxTokenEnd < 0 || tokenEnd < 0) {
throw new Error('Unable to locate the dialog design-token boundaries');
}
const astryxTokens = `${astryxTheme.slice(0, astryxTokenEnd)}\n}\n}\n`;
await writeFile(
join(outDir, 'browser-dialog-design-tokens.css'),
`${astryxTokens}\n${makaTokens.slice(0, tokenEnd)}`,
'utf8',
);
}
/**
* The Computer Use picture-in-picture mirror. Same shape as the cursor overlay
* — module page bundle + CJS preload + verbatim html — sharing `dist/overlay`
* so one `build:overlay` step covers all three panels.
*
* Its preload is receive-only and narrower than the cursor overlay's: the
* mirror displays frames main hands it and has nothing to report back.
*/
export async function buildComputerUsePip({ logLevel = 'info' } = {}) {
await mkdir(outDir, { recursive: true });
await esbuild.build({
entryPoints: [join(srcOverlay, 'pip.ts')],
bundle: true,
format: 'esm',
platform: 'browser',
target: 'chrome120',
outfile: join(outDir, 'pip.js'),
plugins: [jsToTs],
logLevel,
});
await esbuild.build({
entryPoints: [join(srcOverlay, 'pip-preload.ts')],
bundle: true,
format: 'cjs',
platform: 'node',
external: ['electron'],
outfile: join(outDir, 'pip-preload.cjs'),
logLevel,
});
await copyFile(join(srcOverlay, 'pip.html'), join(outDir, 'pip.html'));
return outDir;
}
/**
* The drag-to-grant permission card. Same shape as the cursor overlay —
* IIFE page bundle + CJS preload + verbatim html — and it shares
* `dist/overlay`, so one `build:overlay` step covers both panels.
*
* Unlike the cursor overlay this page IS interactive (the user grabs the
* row out of it), so its preload is send-capable; see the preload source
* for what it is allowed to say.
*/
export async function buildPermissionOverlay({ logLevel = 'info' } = {}) {
await mkdir(outDir, { recursive: true });
await esbuild.build({
entryPoints: [join(srcOverlay, 'permission-overlay.ts')],
bundle: true,
format: 'iife',
platform: 'browser',
target: 'chrome120',
outfile: join(outDir, 'permission-overlay.js'),
plugins: [jsToTs],
logLevel,
});
await esbuild.build({
entryPoints: [join(srcOverlay, 'permission-overlay-preload.ts')],
bundle: true,
format: 'cjs',
platform: 'node',
external: ['electron'],
outfile: join(outDir, 'permission-overlay-preload.cjs'),
logLevel,
});
await copyFile(
join(srcOverlay, 'permission-overlay.html'),
join(outDir, 'permission-overlay.html'),
);
return outDir;
}
// Run directly (npm run build:overlay) or import buildCursorOverlay (dev.mjs).
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
const dir = await buildCursorOverlay();
console.log('overlays built →', dir);
}
async function buildPetOverlay({ logLevel }) {
await esbuild.build({
entryPoints: [join(srcOverlay, 'pet.ts')],
bundle: true,
format: 'iife',
platform: 'browser',
target: 'chrome120',
outfile: join(outDir, 'pet.js'),
plugins: [jsToTs],
logLevel,
});
await esbuild.build({
entryPoints: [join(srcOverlay, 'pet-preload.ts')],
bundle: true,
format: 'cjs',
platform: 'node',
external: ['electron'],
outfile: join(outDir, 'pet-preload.cjs'),
logLevel,
});
for (const file of ['pet.html', 'pet.css'])
await copyFile(join(srcOverlay, file), join(outDir, file));
}