| /* | |
| * 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)); | |
| } |