@sixb/desktop 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,17 +24,19 @@ origin. Both generate the same Electron app first:
24
24
  ▼
25
25
  .sixb/desktop/app/ rewritten on every call (src/generate.ts)
26
26
  main.js the main process, compiled from src/shell/main.ts
27
+ preload.js what the page can reach, compiled from src/shell/preload.ts
27
28
  shell.json name, app URL, API origin
28
29
  index.html the page shown while the app loads
29
- preload.js, icon.png
30
+ icon.png
30
31
  │
31
32
  ├─ openDesktopApp electron .sixb/desktop/app
32
33
  └─ buildDesktopApp @electron/packager ─▶ .sixb/desktop/dist/
33
34
  ```
34
35
 
35
36
  The window loads the app from where it is served; `app/` is not compiled into the binary. The
36
- page runs sandboxed, with no Node and no channel into the main process, and the window stays on the
37
- app and API origins (`src/shell/window.ts`): any other link opens in the user's browser.
37
+ page runs sandboxed, with no Node, and reaches the shell only through what `src/shell/bridge.ts`
38
+ exposes, which says how to add to it. The window stays on the app and API origins
39
+ (`src/shell/window.ts`): any other link opens in the user's browser.
38
40
 
39
41
  ### macOS builds
40
42
 
@@ -1 +1 @@
1
- {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../src/generate.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,iBAAiB;IAChC,gFAAgF;IAChF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,qGAAqG;IACrG,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,6EAA6E;IAC7E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,2EAA2E;AAC3E,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B;AAKD,gFAAgF;AAChF,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,GAAG,MAAM,GAAG,MAAM,CAE5E;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,YAAY,CAAC,CA4CvF;AAED,uFAAuF;AACvF,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAC/E,MAAM,CAYR"}
1
+ {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../src/generate.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,iBAAiB;IAChC,gFAAgF;IAChF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,qGAAqG;IACrG,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,6EAA6E;IAC7E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,2EAA2E;AAC3E,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B;AAKD,gFAAgF;AAChF,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,GAAG,MAAM,GAAG,MAAM,CAE5E;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,YAAY,CAAC,CA4CvF;AAED,uFAAuF;AACvF,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CAAE,GAC/E,MAAM,CAYR"}
package/dist/index.js CHANGED
@@ -139,63 +139,6 @@ function run([input, output]) {
139
139
  }
140
140
  `;
141
141
 
142
- // src/shell/window.ts
143
- var CONTROLS_HEIGHT_PX = 36;
144
- var HIDDEN_TITLE_BAR_CSS = [
145
- `#sixb-desktop-drag{position:fixed;top:0;left:0;right:0;height:${CONTROLS_HEIGHT_PX}px;`,
146
- "z-index:2147483647;pointer-events:none;-webkit-app-region:drag}",
147
- `${[
148
- "a",
149
- "button",
150
- "input",
151
- "select",
152
- "textarea",
153
- "summary",
154
- "label",
155
- "[role=button]",
156
- "[role=link]",
157
- "[role=tab]",
158
- "[role=menuitem]",
159
- "[role=switch]",
160
- "[role=checkbox]",
161
- "[role=combobox]",
162
- "[contenteditable]",
163
- "[tabindex]:not([tabindex='-1'])"
164
- ].join(",")}{-webkit-app-region:no-drag}`,
165
- `[data-slot="sidebar-inner"]{padding-top:calc(${CONTROLS_HEIGHT_PX}px - 0.5rem)}`
166
- ].join("");
167
- var HIDDEN_TITLE_BAR_PRELOAD = `(() => {
168
- const mark = () => {
169
- const root = document.documentElement
170
- if (!root) return false
171
- root.dataset.sixbDesktop = "inset"
172
- root.style.setProperty("--sixb-desktop-controls-height", "${CONTROLS_HEIGHT_PX}px")
173
- return true
174
- }
175
- if (!mark()) {
176
- const observer = new MutationObserver(() => {
177
- if (mark()) observer.disconnect()
178
- })
179
- observer.observe(document, { childList: true })
180
- }
181
-
182
- const addHandle = () => {
183
- if (!document.body || document.getElementById("sixb-desktop-drag")) return
184
- const handle = document.createElement("div")
185
- handle.id = "sixb-desktop-drag"
186
- handle.setAttribute("aria-hidden", "true")
187
- // First in the body, not last: the draggable area is built in document order, so a control
188
- // is only cut out of it if it comes after the handle.
189
- document.body.prepend(handle)
190
- }
191
- if (document.readyState === "loading") {
192
- document.addEventListener("DOMContentLoaded", addHandle, { once: true })
193
- } else {
194
- addHandle()
195
- }
196
- })()
197
- `;
198
-
199
142
  // src/generate.ts
200
143
  var SHELL_SOURCE_DIR = fileURLToPath(new URL("../src/shell", import.meta.url));
201
144
  function desktopDir(projectRoot, part) {
@@ -226,17 +169,17 @@ async function writeDesktopApp(options) {
226
169
  `),
227
170
  writeFile(join(dir, "shell.json"), `${JSON.stringify(config, null, 2)}
228
171
  `),
229
- writeFile(join(dir, "preload.js"), HIDDEN_TITLE_BAR_PRELOAD),
230
172
  writeFile(join(dir, "index.html"), renderShellPage(template, { ...config, icon: !!icon }))
231
173
  ]);
232
174
  await run([
233
175
  process.execPath,
234
176
  "build",
235
177
  join(SHELL_SOURCE_DIR, "main.ts"),
178
+ join(SHELL_SOURCE_DIR, "preload.ts"),
236
179
  "--target=node",
237
180
  "--format=cjs",
238
181
  "--external=electron",
239
- `--outfile=${join(dir, "main.js")}`
182
+ `--outdir=${dir}`
240
183
  ], "Could not compile the desktop shell.");
241
184
  return { dir, name: identity.name, version, icon };
242
185
  }
@@ -380,4 +323,4 @@ export {
380
323
  openDesktopApp
381
324
  };
382
325
 
383
- //# debugId=30FED6904B68AC4164756E2164756E21
326
+ //# debugId=AA1E41BFA25EB6CC64756E2164756E21
package/dist/index.js.map CHANGED
@@ -1,15 +1,14 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/index.ts", "../src/generate.ts", "../src/identity.ts", "../src/run.ts", "../src/mac-icon.ts", "../src/shell/window.ts"],
3
+ "sources": ["../src/index.ts", "../src/generate.ts", "../src/identity.ts", "../src/run.ts", "../src/mac-icon.ts"],
4
4
  "sourcesContent": [
5
5
  "import { mkdir, readdir, rm } from \"node:fs/promises\"\nimport { createRequire } from \"node:module\"\nimport { dirname, join } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\nimport { type DesktopAppOptions, desktopDir, writeDesktopApp } from \"./generate\"\nimport { bundleIdentifier } from \"./identity\"\nimport { run } from \"./run\"\n\nexport type { DesktopAppOptions } from \"./generate\"\n\nexport interface DesktopWindow {\n /** Resolves with Electron's exit code once the user quits it. */\n readonly exited: Promise<number>\n close(): void\n}\n\nexport interface DesktopBuild {\n /** What to hand to users: a zip of the app on macOS, the app's folder elsewhere. */\n readonly path: string\n /** Whether it was signed and notarized, and so opens without a warning. */\n readonly signed: boolean\n}\n\nconst packageRequire = createRequire(import.meta.url)\n// From the package's `src`, like the shell: see `SHELL_SOURCE_DIR`.\nconst PACKAGER_SCRIPT = fileURLToPath(new URL(\"../src/packager.mjs\", import.meta.url))\n\n/** Opens the app in a desktop window. `sixb dev --desktop` calls this. */\nexport async function openDesktopApp(options: DesktopAppOptions): Promise<DesktopWindow> {\n const app = await writeDesktopApp(options)\n // Chromium writes plenty to stderr when all is well, so it goes to a file, named if the window\n // quits on its own with an error.\n const log = join(dirname(app.dir), \"electron.log\")\n const electron = Bun.spawn([electronBinary(), app.dir], {\n stdio: [\"ignore\", \"ignore\", Bun.file(log)],\n })\n let closing = false\n const exited = electron.exited.then((code) => {\n if (code !== 0 && !closing) {\n console.error(`[SixbDesktop] The desktop window quit with exit code ${code}. See ${log}.`)\n }\n return code\n })\n return {\n exited,\n close: () => {\n closing = true\n electron.kill()\n },\n }\n}\n\n/** Packages the app for this machine's platform. `sixb desktop build` calls this. */\nexport async function buildDesktopApp(options: DesktopAppOptions): Promise<DesktopBuild> {\n const app = await writeDesktopApp(options)\n const out = desktopDir(options.projectRoot, \"dist\")\n await rm(out, { recursive: true, force: true })\n\n const mac = process.platform === \"darwin\"\n const notarize = mac ? appleNotarization() : null\n const packaged = await packageApp({\n dir: app.dir,\n name: app.name,\n out,\n appBundleId: bundleIdentifier(options.appUrl),\n appVersion: app.version,\n electronVersion: (packageRequire(\"electron/package.json\") as { readonly version: string })\n .version,\n ...(mac && app.icon ? { icon: await macIcon(app.icon) } : {}),\n // Signs with the Developer ID certificate in the keychain, which notarization requires.\n ...(notarize ? { osxSign: {}, osxNotarize: notarize } : {}),\n })\n if (!mac) return { path: packaged, signed: false }\n\n const name = (await readdir(packaged)).find((entry) => entry.endsWith(\".app\"))\n if (!name) throw new Error(`[SixbDesktop] The packaged app is missing from ${packaged}.`)\n const bundle = join(packaged, name)\n // Packaging breaks the ad-hoc signature Electron ships with. macOS calls a downloaded app with a\n // broken signature damaged, and offers no way to open it; signed ad hoc, it can be allowed.\n if (!notarize) {\n await run([\"codesign\", \"--force\", \"--deep\", \"--sign\", \"-\", bundle], \"Could not sign the app.\")\n }\n // `ditto`, not `zip`: it keeps the bundle's symlinks, and with them its signature.\n const zip = `${packaged}.zip`\n await run([\"ditto\", \"-c\", \"-k\", \"--keepParent\", bundle, zip], \"Could not zip the app.\")\n return { path: zip, signed: notarize !== null }\n}\n\n/**\n * An App Store Connect API key, which notarizes the app so macOS opens it without a warning.\n * Without one, the app is built unsigned.\n */\nfunction appleNotarization(): {\n readonly appleApiKey: string\n readonly appleApiKeyId: string\n readonly appleApiIssuer: string\n} | null {\n const { APPLE_API_KEY, APPLE_API_KEY_ID, APPLE_API_ISSUER } = process.env\n if (!APPLE_API_KEY && !APPLE_API_KEY_ID && !APPLE_API_ISSUER) return null\n if (!APPLE_API_KEY || !APPLE_API_KEY_ID || !APPLE_API_ISSUER) {\n throw new Error(\n \"[SixbDesktop] Set all of APPLE_API_KEY, APPLE_API_KEY_ID and APPLE_API_ISSUER to sign the \" +\n \"macOS app, or none of them to build it unsigned.\"\n )\n }\n return {\n appleApiKey: APPLE_API_KEY,\n appleApiKeyId: APPLE_API_KEY_ID,\n appleApiIssuer: APPLE_API_ISSUER,\n }\n}\n\n/**\n * The packager wants `.icns`: every size macOS shows, scaled from the 1024-pixel icon, in one file.\n * `sips` and `iconutil` ship with every Mac. Written beside the app, not in it: everything in the\n * app's directory ships inside the bundle.\n */\nasync function macIcon(png: string): Promise<string> {\n const desktop = dirname(dirname(png))\n const iconset = join(desktop, \"icon.iconset\")\n await rm(iconset, { recursive: true, force: true })\n await mkdir(iconset)\n const sizes = [16, 32, 128, 256, 512].flatMap((points) => [\n { name: `icon_${points}x${points}.png`, pixels: points },\n { name: `icon_${points}x${points}@2x.png`, pixels: points * 2 },\n ])\n await Promise.all(\n sizes.map(({ name, pixels }) =>\n run(\n [\"sips\", \"-z\", `${pixels}`, `${pixels}`, png, \"--out\", join(iconset, name)],\n \"Could not scale the icon.\"\n )\n )\n )\n const icns = join(desktop, \"icon.icns\")\n await run([\"iconutil\", \"-c\", \"icns\", iconset, \"-o\", icns], \"Could not convert the icon.\")\n return icns\n}\n\n/**\n * Runs `@electron/packager` into `options.out`, which starts empty, and resolves with the one\n * directory it wrote there. Under Electron's own binary, which doubles as the Node the packager was\n * written for: under Bun, the packager fails whenever it verifies a download (its checksum\n * dependency does not load).\n */\nasync function packageApp(\n options: { readonly out: string } & Record<string, unknown>\n): Promise<string> {\n const packager = Bun.spawn(\n [electronBinary(), PACKAGER_SCRIPT, JSON.stringify({ ...options, quiet: true })],\n {\n stdio: [\"ignore\", \"inherit\", \"inherit\"],\n // Without the second, Electron's file system reads the `app.asar` the packager writes as a\n // directory.\n env: { ...process.env, ELECTRON_RUN_AS_NODE: \"1\", ELECTRON_NO_ASAR: \"1\" },\n }\n )\n if ((await packager.exited) !== 0) {\n throw new Error(\"[SixbDesktop] Packaging failed. See the output above.\")\n }\n const [written] = await readdir(options.out)\n if (!written) throw new Error(`[SixbDesktop] The packager wrote nothing to ${options.out}.`)\n return join(options.out, written)\n}\n\n// Outside Electron, `require(\"electron\")` is the path of the binary its install script fetched.\nfunction electronBinary(): string {\n try {\n return packageRequire(\"electron\") as string\n } catch {\n const electronDir = dirname(packageRequire.resolve(\"electron/package.json\"))\n throw new Error(\n \"[SixbDesktop] Electron's binary is missing: its install script did not run. \" +\n `Run \\`bun run --cwd ${electronDir} postinstall\\`, then try again.`\n )\n }\n}\n",
6
- "import { mkdir, readFile, rm, writeFile } from \"node:fs/promises\"\nimport { join } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\nimport { readAppIdentity } from \"./identity\"\nimport { drawMacIcon } from \"./mac-icon\"\nimport { run } from \"./run\"\nimport type { ShellConfig } from \"./shell/navigation\"\nimport { HIDDEN_TITLE_BAR_PRELOAD } from \"./shell/window\"\n\nexport interface DesktopAppOptions {\n /** The Sixb project. The desktop app is generated under its `.sixb/desktop`. */\n readonly projectRoot: string\n /** Where the app is served. The window opens it; the desktop app takes its name and icon from it. */\n readonly appUrl: string\n /** The project's API. Its sign-in pages are where the window is approved. */\n readonly apiOrigin: string\n}\n\n/** The Electron app `writeDesktopApp` generated, and what it is called. */\nexport interface GeneratedApp {\n readonly dir: string\n readonly name: string\n readonly version: string\n /** The app's icon in `dir`, when it declares one. */\n readonly icon: string | null\n}\n\n// Read from the package's `src`, which ships, whether this module runs from `src` or `dist`.\nconst SHELL_SOURCE_DIR = fileURLToPath(new URL(\"../src/shell\", import.meta.url))\n\n/** Where a project's desktop app is generated (`app`) and packaged (`dist`). */\nexport function desktopDir(projectRoot: string, part: \"app\" | \"dist\"): string {\n return join(projectRoot, \".sixb\", \"desktop\", part)\n}\n\n/**\n * Writes the Electron app that opens `appUrl` into the project's `.sixb/desktop/app`. The directory\n * is build-owned, like the rest of `.sixb`: it is cleared on every call.\n */\nexport async function writeDesktopApp(options: DesktopAppOptions): Promise<GeneratedApp> {\n const identity = await readAppIdentity(options.appUrl)\n const version = await readProjectVersion(options.projectRoot)\n const dir = desktopDir(options.projectRoot, \"app\")\n await rm(dir, { recursive: true, force: true })\n await mkdir(dir, { recursive: true })\n\n const config: ShellConfig = {\n name: identity.name,\n appUrl: new URL(options.appUrl).href,\n apiOrigin: new URL(options.apiOrigin).origin,\n }\n const manifest = {\n name: \"sixb-desktop-app\",\n // Names the folder the window's cookies are kept in, so each app has its own sign-in.\n productName: identity.name,\n version,\n private: true,\n main: \"main.js\",\n }\n const icon = identity.icon ? await writeIcon(dir, identity.icon) : null\n const template = await readFile(join(SHELL_SOURCE_DIR, \"index.html\"), \"utf-8\")\n await Promise.all([\n writeFile(join(dir, \"package.json\"), `${JSON.stringify(manifest, null, 2)}\\n`),\n writeFile(join(dir, \"shell.json\"), `${JSON.stringify(config, null, 2)}\\n`),\n writeFile(join(dir, \"preload.js\"), HIDDEN_TITLE_BAR_PRELOAD),\n writeFile(join(dir, \"index.html\"), renderShellPage(template, { ...config, icon: !!icon })),\n ])\n\n // Electron loads plain JavaScript. A child process, because an in-process `Bun.build()` that\n // wedges takes its caller down with it.\n await run(\n [\n process.execPath,\n \"build\",\n join(SHELL_SOURCE_DIR, \"main.ts\"),\n \"--target=node\",\n \"--format=cjs\",\n \"--external=electron\",\n `--outfile=${join(dir, \"main.js\")}`,\n ],\n \"Could not compile the desktop shell.\"\n )\n return { dir, name: identity.name, version, icon }\n}\n\n/** The page the window shows while the app loads, and when it cannot reach the app. */\nexport function renderShellPage(\n template: string,\n page: { readonly name: string; readonly appUrl: string; readonly icon: boolean }\n): string {\n const initial = Array.from(page.name.trim())[0]?.toUpperCase() ?? \"\"\n const markup: Record<string, string> = {\n name: escapeHtml(page.name),\n host: escapeHtml(new URL(page.appUrl).host),\n appUrl: escapeHtml(page.appUrl),\n // The app's icon, or the first letter of its name standing in for one.\n mark: page.icon\n ? '<img class=\"mark\" src=\"icon.png\" alt=\"\" />'\n : `<div class=\"mark\" aria-hidden=\"true\">${escapeHtml(initial)}</div>`,\n }\n return template.replaceAll(/\\{\\{(\\w+)\\}\\}/g, (_match, key: string) => markup[key] ?? \"\")\n}\n\n/** Writes the app's icon into `dir` as `icon.png`; on macOS, drawn the way macOS draws app icons. */\nasync function writeIcon(dir: string, png: Uint8Array): Promise<string> {\n const icon = join(dir, \"icon.png\")\n if (process.platform !== \"darwin\") {\n await writeFile(icon, png)\n return icon\n }\n // The original goes beside the app, not in it: everything in the app's directory ships.\n const source = join(dir, \"..\", \"icon-source.png\")\n await writeFile(source, png)\n await drawMacIcon(source, icon)\n return icon\n}\n\nasync function readProjectVersion(projectRoot: string): Promise<string> {\n try {\n const manifest: unknown = JSON.parse(await readFile(join(projectRoot, \"package.json\"), \"utf-8\"))\n const version = (manifest as { readonly version?: unknown } | null)?.version\n return typeof version === \"string\" ? version : \"0.0.0\"\n } catch {\n return \"0.0.0\"\n }\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replaceAll(\"&\", \"&amp;\")\n .replaceAll(\"<\", \"&lt;\")\n .replaceAll(\">\", \"&gt;\")\n .replaceAll('\"', \"&quot;\")\n}\n",
6
+ "import { mkdir, readFile, rm, writeFile } from \"node:fs/promises\"\nimport { join } from \"node:path\"\nimport { fileURLToPath } from \"node:url\"\nimport { readAppIdentity } from \"./identity\"\nimport { drawMacIcon } from \"./mac-icon\"\nimport { run } from \"./run\"\nimport type { ShellConfig } from \"./shell/navigation\"\n\nexport interface DesktopAppOptions {\n /** The Sixb project. The desktop app is generated under its `.sixb/desktop`. */\n readonly projectRoot: string\n /** Where the app is served. The window opens it; the desktop app takes its name and icon from it. */\n readonly appUrl: string\n /** The project's API. Its sign-in pages are where the window is approved. */\n readonly apiOrigin: string\n}\n\n/** The Electron app `writeDesktopApp` generated, and what it is called. */\nexport interface GeneratedApp {\n readonly dir: string\n readonly name: string\n readonly version: string\n /** The app's icon in `dir`, when it declares one. */\n readonly icon: string | null\n}\n\n// Read from the package's `src`, which ships, whether this module runs from `src` or `dist`.\nconst SHELL_SOURCE_DIR = fileURLToPath(new URL(\"../src/shell\", import.meta.url))\n\n/** Where a project's desktop app is generated (`app`) and packaged (`dist`). */\nexport function desktopDir(projectRoot: string, part: \"app\" | \"dist\"): string {\n return join(projectRoot, \".sixb\", \"desktop\", part)\n}\n\n/**\n * Writes the Electron app that opens `appUrl` into the project's `.sixb/desktop/app`. The directory\n * is build-owned, like the rest of `.sixb`: it is cleared on every call.\n */\nexport async function writeDesktopApp(options: DesktopAppOptions): Promise<GeneratedApp> {\n const identity = await readAppIdentity(options.appUrl)\n const version = await readProjectVersion(options.projectRoot)\n const dir = desktopDir(options.projectRoot, \"app\")\n await rm(dir, { recursive: true, force: true })\n await mkdir(dir, { recursive: true })\n\n const config: ShellConfig = {\n name: identity.name,\n appUrl: new URL(options.appUrl).href,\n apiOrigin: new URL(options.apiOrigin).origin,\n }\n const manifest = {\n name: \"sixb-desktop-app\",\n // Names the folder the window's cookies are kept in, so each app has its own sign-in.\n productName: identity.name,\n version,\n private: true,\n main: \"main.js\",\n }\n const icon = identity.icon ? await writeIcon(dir, identity.icon) : null\n const template = await readFile(join(SHELL_SOURCE_DIR, \"index.html\"), \"utf-8\")\n await Promise.all([\n writeFile(join(dir, \"package.json\"), `${JSON.stringify(manifest, null, 2)}\\n`),\n writeFile(join(dir, \"shell.json\"), `${JSON.stringify(config, null, 2)}\\n`),\n writeFile(join(dir, \"index.html\"), renderShellPage(template, { ...config, icon: !!icon })),\n ])\n\n // Electron loads plain JavaScript. A child process, because an in-process `Bun.build()` that\n // wedges takes its caller down with it.\n await run(\n [\n process.execPath,\n \"build\",\n join(SHELL_SOURCE_DIR, \"main.ts\"),\n join(SHELL_SOURCE_DIR, \"preload.ts\"),\n \"--target=node\",\n \"--format=cjs\",\n \"--external=electron\",\n `--outdir=${dir}`,\n ],\n \"Could not compile the desktop shell.\"\n )\n return { dir, name: identity.name, version, icon }\n}\n\n/** The page the window shows while the app loads, and when it cannot reach the app. */\nexport function renderShellPage(\n template: string,\n page: { readonly name: string; readonly appUrl: string; readonly icon: boolean }\n): string {\n const initial = Array.from(page.name.trim())[0]?.toUpperCase() ?? \"\"\n const markup: Record<string, string> = {\n name: escapeHtml(page.name),\n host: escapeHtml(new URL(page.appUrl).host),\n appUrl: escapeHtml(page.appUrl),\n // The app's icon, or the first letter of its name standing in for one.\n mark: page.icon\n ? '<img class=\"mark\" src=\"icon.png\" alt=\"\" />'\n : `<div class=\"mark\" aria-hidden=\"true\">${escapeHtml(initial)}</div>`,\n }\n return template.replaceAll(/\\{\\{(\\w+)\\}\\}/g, (_match, key: string) => markup[key] ?? \"\")\n}\n\n/** Writes the app's icon into `dir` as `icon.png`; on macOS, drawn the way macOS draws app icons. */\nasync function writeIcon(dir: string, png: Uint8Array): Promise<string> {\n const icon = join(dir, \"icon.png\")\n if (process.platform !== \"darwin\") {\n await writeFile(icon, png)\n return icon\n }\n // The original goes beside the app, not in it: everything in the app's directory ships.\n const source = join(dir, \"..\", \"icon-source.png\")\n await writeFile(source, png)\n await drawMacIcon(source, icon)\n return icon\n}\n\nasync function readProjectVersion(projectRoot: string): Promise<string> {\n try {\n const manifest: unknown = JSON.parse(await readFile(join(projectRoot, \"package.json\"), \"utf-8\"))\n const version = (manifest as { readonly version?: unknown } | null)?.version\n return typeof version === \"string\" ? version : \"0.0.0\"\n } catch {\n return \"0.0.0\"\n }\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replaceAll(\"&\", \"&amp;\")\n .replaceAll(\"<\", \"&lt;\")\n .replaceAll(\">\", \"&gt;\")\n .replaceAll('\"', \"&quot;\")\n}\n",
7
7
  "/** What the desktop app takes from the web app it opens. */\nexport interface AppIdentity {\n /** The app's name: `metadata.title` in `app/layout.tsx`. */\n readonly name: string\n /**\n * The app's maskable icon, such as `app/public/icon-maskable-512.png`, or else its largest PNG\n * icon, such as `app/public/icon-512.png`.\n */\n readonly icon: Uint8Array | null\n}\n\n/**\n * Reads the app's web app manifest, the one a browser installs the app from. The desktop app is\n * the same app installed another way, so it takes the same name and icon and has no settings of\n * its own.\n */\nexport async function readAppIdentity(appUrl: string): Promise<AppIdentity> {\n const manifestUrl = new URL(\"/app.webmanifest\", appUrl)\n const manifest: unknown = await (await fetchFromApp(manifestUrl)).json()\n const { name, icons } = (typeof manifest === \"object\" && manifest !== null ? manifest : {}) as {\n readonly name?: unknown\n readonly icons?: unknown\n }\n\n const iconUrl = desktopIcon(icons, manifestUrl)\n return {\n name: typeof name === \"string\" && name.trim() ? name.trim() : \"Sixb\",\n icon: iconUrl ? new Uint8Array(await (await fetchFromApp(iconUrl)).arrayBuffer()) : null,\n }\n}\n\n/**\n * The app's id on the desktop, from the address it opens: `https://orders.acme.com` becomes\n * `com.acme.orders`. Each deployment gets its own, so a staging build installs beside production.\n */\nexport function bundleIdentifier(appUrl: string): string {\n const labels = new URL(appUrl).hostname\n .split(\".\")\n .reverse()\n .map((label) => label.toLowerCase().replaceAll(/[^a-z0-9-]/g, \"-\"))\n .filter(Boolean)\n return (labels.length > 1 ? labels : [\"local\", ...labels]).join(\".\")\n}\n\n// A maskable icon is drawn to its edges for a platform to cut its own shape from, which is what\n// macOS does with it. Any other icon may carry its own shape and margins.\nfunction desktopIcon(icons: unknown, manifestUrl: URL): URL | null {\n let best: { readonly url: URL; readonly rank: readonly [number, number] } | null = null\n for (const icon of Array.isArray(icons) ? icons : []) {\n if (typeof icon !== \"object\" || icon === null) continue\n const { src, sizes, type, purpose } = icon as Record<string, unknown>\n if (typeof src !== \"string\") continue\n const url = new URL(src, manifestUrl)\n if (type === undefined ? !url.pathname.endsWith(\".png\") : type !== \"image/png\") continue\n\n const maskable = typeof purpose === \"string\" && purpose.split(/\\s+/).includes(\"maskable\")\n const size = Math.max(\n 0,\n ...(typeof sizes === \"string\" ? sizes.split(/\\s+/) : []).map(\n (entry) => Number.parseInt(entry, 10) || 0\n )\n )\n const rank = [maskable ? 1 : 0, size] as const\n if (!best || rank[0] > best.rank[0] || (rank[0] === best.rank[0] && rank[1] > best.rank[1])) {\n best = { url, rank }\n }\n }\n return best?.url ?? null\n}\n\nasync function fetchFromApp(url: URL): Promise<Response> {\n let response: Response\n try {\n response = await fetch(url)\n } catch (error) {\n throw new Error(\n `[SixbDesktop] Could not reach ${url} (${error instanceof Error ? error.message : error}). ` +\n \"The desktop app takes its name and icon from the app, so the app must be running there.\"\n )\n }\n if (!response.ok) throw new Error(`[SixbDesktop] ${url} answered ${response.status}.`)\n return response\n}\n",
8
8
  "/** Runs a command to completion, quietly unless it fails: then it throws `failure` and its errors. */\nexport async function run(command: readonly string[], failure: string): Promise<void> {\n const child = Bun.spawn([...command], {\n stdio: [\"ignore\", \"ignore\", \"pipe\"],\n timeout: 120_000,\n })\n const [exitCode, errors] = await Promise.all([child.exited, new Response(child.stderr).text()])\n if (exitCode !== 0) {\n throw new Error(`[SixbDesktop] ${failure}${errors.trim() ? `\\n${errors.trim()}` : \"\"}`)\n }\n}\n",
9
- "import { run } from \"./run\"\n\n/**\n * Draws `png` the way macOS draws app icons, into `out`: on Apple's grid, a rounded square of 824\n * pixels with continuous corners in a canvas of 1024, over the soft shadow Apple's own icons have.\n * An icon drawn to its edges otherwise stands larger than every other icon in the Dock.\n */\nexport async function drawMacIcon(png: string, out: string): Promise<void> {\n await run(\n [\"osascript\", \"-l\", \"JavaScript\", \"-e\", MAC_ICON_SCRIPT, png, out],\n \"Could not draw the macOS icon.\"\n )\n}\n\n// AppKit, through the JavaScript `osascript` runs, which every Mac has. The corner is the curve\n// UIKit rounds continuous corners with; with this radius it matches the outline of Apple's Notes\n// icon to a few pixels, and the shadow matches its shadow.\nconst MAC_ICON_SCRIPT = `\nObjC.import(\"AppKit\")\nObjC.import(\"CoreGraphics\")\n\nfunction run([input, output]) {\n const canvas = 1024\n const body = 824\n const radius = 185.4\n const min = (canvas - body) / 2\n const max = min + body\n\n // One corner, as offsets in radii from the edge it leaves and the edge it joins.\n const corner = [\n [[1.52866483, 0]],\n [[0.66993427, 0.065496], [1.08849323, 0], [0.86840689, 0.02065986]],\n [[0.63149399, 0.074911]],\n [[0.074911, 0.63149399], [0.37282392, 0.16906933], [0.16906933, 0.37282392]],\n [[0.065496, 0.66993427]],\n [[0, 1.52866483], [0.02065986, 0.86840689], [0, 1.08849323]],\n ]\n const corners = [\n ([u, v]) => $.NSMakePoint(max - u * radius, max - v * radius),\n ([u, v]) => $.NSMakePoint(max - v * radius, min + u * radius),\n ([u, v]) => $.NSMakePoint(min + u * radius, min + v * radius),\n ([u, v]) => $.NSMakePoint(min + v * radius, max - u * radius),\n ]\n const shape = $.NSBezierPath.bezierPath\n shape.moveToPoint(corners[3]([0, 1.52866483]))\n for (const at of corners) {\n for (const [to, c1, c2] of corner) {\n if (c1) shape.curveToPointControlPoint1ControlPoint2(at(to), at(c1), at(c2))\n else shape.lineToPoint(at(to))\n }\n }\n shape.closePath\n\n const image = $.NSImage.alloc.initWithContentsOfFile(input)\n if (image.isNil()) throw new Error(\"Cannot read \" + input)\n const bitmap = $.NSBitmapImageRep.alloc\n .initWithBitmapDataPlanesPixelsWidePixelsHighBitsPerSampleSamplesPerPixelHasAlphaIsPlanarColorSpaceNameBytesPerRowBitsPerPixel(\n null, canvas, canvas, 8, 4, true, false, $.NSDeviceRGBColorSpace, 0, 0\n )\n const context = $.NSGraphicsContext.graphicsContextWithBitmapImageRep(bitmap)\n $.NSGraphicsContext.setCurrentContext(context)\n context.setImageInterpolation($.NSImageInterpolationHigh)\n\n const shadow = $.NSShadow.alloc.init\n shadow.setShadowOffset($.NSMakeSize(0, -8))\n shadow.setShadowBlurRadius(30)\n shadow.setShadowColor($.NSColor.colorWithWhiteAlpha(0, 0.22))\n shadow.set\n // A layer, so the shadow falls from the clipped icon and not from the clip.\n $.CGContextBeginTransparencyLayer(context.CGContext, null)\n shape.addClip\n image.drawInRectFromRectOperationFraction(\n $.NSMakeRect(min, min, body, body), $.NSZeroRect, $.NSCompositingOperationSourceOver, 1\n )\n $.CGContextEndTransparencyLayer(context.CGContext)\n context.flushGraphics\n\n const png = bitmap.representationUsingTypeProperties($.NSBitmapImageFileTypePNG, $.NSDictionary.dictionary)\n if (!png.writeToFileAtomically(output, true)) throw new Error(\"Cannot write \" + output)\n}\n`\n",
10
- "import type { ShellConfig } from \"./navigation\"\n\n/**\n * Whether the window itself may show `url`: only the app and the API that serves its sign-in\n * pages. Anything else a page links to opens in the user's browser instead.\n */\nexport function mayNavigate(config: ShellConfig, url: string): boolean {\n try {\n const { origin } = new URL(url)\n return origin === new URL(config.appUrl).origin || origin === new URL(config.apiOrigin).origin\n } catch {\n return false\n }\n}\n\n/**\n * Whether `url` is on the app's own origin. Camera, location and the like are the app's to ask for,\n * and not a frame it embeds from elsewhere.\n */\nexport function isAppUrl(config: ShellConfig, url: string): boolean {\n try {\n return new URL(url).origin === new URL(config.appUrl).origin\n } catch {\n return false\n }\n}\n\n/**\n * What the window paints before a page has: the loading page's own background, so the window\n * opens, and passes from that page to the app, without a flash of white.\n */\nexport const WINDOW_BACKGROUND = { light: \"#fafafa\", dark: \"#0a0a0a\" } as const\n\n/** The band at the top of a window without a title bar where macOS draws the window controls. */\nexport const CONTROLS_HEIGHT_PX = 36\n\n/**\n * Styles for a window without a title bar, inserted into every page it shows.\n *\n * The page reaches the top edge, under the window controls, so two things a title bar did have\n * to come from the page: somewhere to grab the window, and room for the controls.\n * - `#sixb-desktop-drag` (added by the preload) is the drag handle: a strip across the top, above\n * the page. It ignores the pointer, and every control is cut out of the draggable area, so\n * buttons and links under the strip still take their clicks and the space between them moves\n * the window. Something clickable that is none of the things listed here needs\n * `-webkit-app-region: no-drag` of its own.\n * - The shared sidebar is moved down so most apps need nothing. Other layouts can use\n * `html[data-sixb-desktop]` and `--sixb-desktop-controls-height`, which the preload sets.\n */\nexport const HIDDEN_TITLE_BAR_CSS = [\n `#sixb-desktop-drag{position:fixed;top:0;left:0;right:0;height:${CONTROLS_HEIGHT_PX}px;`,\n \"z-index:2147483647;pointer-events:none;-webkit-app-region:drag}\",\n `${[\n \"a\",\n \"button\",\n \"input\",\n \"select\",\n \"textarea\",\n \"summary\",\n \"label\",\n \"[role=button]\",\n \"[role=link]\",\n \"[role=tab]\",\n \"[role=menuitem]\",\n \"[role=switch]\",\n \"[role=checkbox]\",\n \"[role=combobox]\",\n \"[contenteditable]\",\n \"[tabindex]:not([tabindex='-1'])\",\n ].join(\",\")}{-webkit-app-region:no-drag}`,\n `[data-slot=\"sidebar-inner\"]{padding-top:calc(${CONTROLS_HEIGHT_PX}px - 0.5rem)}`,\n].join(\"\")\n\n/**\n * The preload of a window without a title bar: runs in every page before the page's own scripts.\n *\n * It marks `<html>` the moment the element exists, so an app can read the mark on its first\n * render, and adds the drag handle once there is a body to hold it. Written to a file by\n * `writeDesktopApp`, which is why it is source text and not a function.\n */\nexport const HIDDEN_TITLE_BAR_PRELOAD = `(() => {\n const mark = () => {\n const root = document.documentElement\n if (!root) return false\n root.dataset.sixbDesktop = \"inset\"\n root.style.setProperty(\"--sixb-desktop-controls-height\", \"${CONTROLS_HEIGHT_PX}px\")\n return true\n }\n if (!mark()) {\n const observer = new MutationObserver(() => {\n if (mark()) observer.disconnect()\n })\n observer.observe(document, { childList: true })\n }\n\n const addHandle = () => {\n if (!document.body || document.getElementById(\"sixb-desktop-drag\")) return\n const handle = document.createElement(\"div\")\n handle.id = \"sixb-desktop-drag\"\n handle.setAttribute(\"aria-hidden\", \"true\")\n // First in the body, not last: the draggable area is built in document order, so a control\n // is only cut out of it if it comes after the handle.\n document.body.prepend(handle)\n }\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", addHandle, { once: true })\n } else {\n addHandle()\n }\n})()\n`\n"
9
+ "import { run } from \"./run\"\n\n/**\n * Draws `png` the way macOS draws app icons, into `out`: on Apple's grid, a rounded square of 824\n * pixels with continuous corners in a canvas of 1024, over the soft shadow Apple's own icons have.\n * An icon drawn to its edges otherwise stands larger than every other icon in the Dock.\n */\nexport async function drawMacIcon(png: string, out: string): Promise<void> {\n await run(\n [\"osascript\", \"-l\", \"JavaScript\", \"-e\", MAC_ICON_SCRIPT, png, out],\n \"Could not draw the macOS icon.\"\n )\n}\n\n// AppKit, through the JavaScript `osascript` runs, which every Mac has. The corner is the curve\n// UIKit rounds continuous corners with; with this radius it matches the outline of Apple's Notes\n// icon to a few pixels, and the shadow matches its shadow.\nconst MAC_ICON_SCRIPT = `\nObjC.import(\"AppKit\")\nObjC.import(\"CoreGraphics\")\n\nfunction run([input, output]) {\n const canvas = 1024\n const body = 824\n const radius = 185.4\n const min = (canvas - body) / 2\n const max = min + body\n\n // One corner, as offsets in radii from the edge it leaves and the edge it joins.\n const corner = [\n [[1.52866483, 0]],\n [[0.66993427, 0.065496], [1.08849323, 0], [0.86840689, 0.02065986]],\n [[0.63149399, 0.074911]],\n [[0.074911, 0.63149399], [0.37282392, 0.16906933], [0.16906933, 0.37282392]],\n [[0.065496, 0.66993427]],\n [[0, 1.52866483], [0.02065986, 0.86840689], [0, 1.08849323]],\n ]\n const corners = [\n ([u, v]) => $.NSMakePoint(max - u * radius, max - v * radius),\n ([u, v]) => $.NSMakePoint(max - v * radius, min + u * radius),\n ([u, v]) => $.NSMakePoint(min + u * radius, min + v * radius),\n ([u, v]) => $.NSMakePoint(min + v * radius, max - u * radius),\n ]\n const shape = $.NSBezierPath.bezierPath\n shape.moveToPoint(corners[3]([0, 1.52866483]))\n for (const at of corners) {\n for (const [to, c1, c2] of corner) {\n if (c1) shape.curveToPointControlPoint1ControlPoint2(at(to), at(c1), at(c2))\n else shape.lineToPoint(at(to))\n }\n }\n shape.closePath\n\n const image = $.NSImage.alloc.initWithContentsOfFile(input)\n if (image.isNil()) throw new Error(\"Cannot read \" + input)\n const bitmap = $.NSBitmapImageRep.alloc\n .initWithBitmapDataPlanesPixelsWidePixelsHighBitsPerSampleSamplesPerPixelHasAlphaIsPlanarColorSpaceNameBytesPerRowBitsPerPixel(\n null, canvas, canvas, 8, 4, true, false, $.NSDeviceRGBColorSpace, 0, 0\n )\n const context = $.NSGraphicsContext.graphicsContextWithBitmapImageRep(bitmap)\n $.NSGraphicsContext.setCurrentContext(context)\n context.setImageInterpolation($.NSImageInterpolationHigh)\n\n const shadow = $.NSShadow.alloc.init\n shadow.setShadowOffset($.NSMakeSize(0, -8))\n shadow.setShadowBlurRadius(30)\n shadow.setShadowColor($.NSColor.colorWithWhiteAlpha(0, 0.22))\n shadow.set\n // A layer, so the shadow falls from the clipped icon and not from the clip.\n $.CGContextBeginTransparencyLayer(context.CGContext, null)\n shape.addClip\n image.drawInRectFromRectOperationFraction(\n $.NSMakeRect(min, min, body, body), $.NSZeroRect, $.NSCompositingOperationSourceOver, 1\n )\n $.CGContextEndTransparencyLayer(context.CGContext)\n context.flushGraphics\n\n const png = bitmap.representationUsingTypeProperties($.NSBitmapImageFileTypePNG, $.NSDictionary.dictionary)\n if (!png.writeToFileAtomically(output, true)) throw new Error(\"Cannot write \" + output)\n}\n`\n"
11
10
  ],
12
- "mappings": "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",
13
- "debugId": "30FED6904B68AC4164756E2164756E21",
11
+ "mappings": "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",
12
+ "debugId": "AA1E41BFA25EB6CC64756E2164756E21",
14
13
  "names": []
15
14
  }
@@ -0,0 +1,16 @@
1
+ /** The name a page finds the desktop app under, through `useDesktop` in `@sixb/client/hooks`. */
2
+ export declare const DESKTOP_BRIDGE_NAME = "sixbDesktop";
3
+ /**
4
+ * What the shell offers the page. `SixbDesktop` in `@sixb/client` is the page's view of this, and
5
+ * `tests/app.test.ts` holds the two together.
6
+ *
7
+ * The page is remote, and anything here is open to whatever runs in it. To offer more:
8
+ * 1. Add one narrow method here, calling `ipcRenderer.invoke`, never a general way to run a command
9
+ * or read a file. The page asks; the person decides, in a native dialog, what it gets.
10
+ * 2. Handle it in `main.ts`, refusing any sender that `isAppUrl` does not accept.
11
+ * 3. Add it to `SixbDesktop` as optional, because a page can outlive the shell it runs in.
12
+ */
13
+ export declare function desktopBridge<const Platform extends NodeJS.Platform>(platform: Platform): {
14
+ platform: Platform;
15
+ };
16
+ //# sourceMappingURL=bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../../src/shell/bridge.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,eAAO,MAAM,mBAAmB,gBAAgB,CAAA;AAEhD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,KAAK,CAAC,QAAQ,SAAS,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ;;EAEvF"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=preload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preload.d.ts","sourceRoot":"","sources":["../../src/shell/preload.ts"],"names":[],"mappings":""}
@@ -17,6 +17,18 @@ export declare const WINDOW_BACKGROUND: {
17
17
  readonly light: "#fafafa";
18
18
  readonly dark: "#0a0a0a";
19
19
  };
20
+ /**
21
+ * What the window's page runs with. The page is remote: it gets no Node and a world apart from the
22
+ * preload's, on every platform, and reaches the shell only through what the preload exposes.
23
+ */
24
+ export declare function pageWebPreferences(preload: string): {
25
+ sandbox: boolean;
26
+ contextIsolation: boolean;
27
+ nodeIntegration: boolean;
28
+ preload: string;
29
+ };
30
+ /** Only macOS draws window controls over the page; elsewhere the title bar stays. */
31
+ export declare function hasHiddenTitleBar(platform: NodeJS.Platform): boolean;
20
32
  /** The band at the top of a window without a title bar where macOS draws the window controls. */
21
33
  export declare const CONTROLS_HEIGHT_PX = 36;
22
34
  /**
@@ -33,12 +45,4 @@ export declare const CONTROLS_HEIGHT_PX = 36;
33
45
  * `html[data-sixb-desktop]` and `--sixb-desktop-controls-height`, which the preload sets.
34
46
  */
35
47
  export declare const HIDDEN_TITLE_BAR_CSS: string;
36
- /**
37
- * The preload of a window without a title bar: runs in every page before the page's own scripts.
38
- *
39
- * It marks `<html>` the moment the element exists, so an app can read the mark on its first
40
- * render, and adds the drag handle once there is a body to hold it. Written to a file by
41
- * `writeDesktopApp`, which is why it is source text and not a function.
42
- */
43
- export declare const HIDDEN_TITLE_BAR_PRELOAD = "(() => {\n const mark = () => {\n const root = document.documentElement\n if (!root) return false\n root.dataset.sixbDesktop = \"inset\"\n root.style.setProperty(\"--sixb-desktop-controls-height\", \"36px\")\n return true\n }\n if (!mark()) {\n const observer = new MutationObserver(() => {\n if (mark()) observer.disconnect()\n })\n observer.observe(document, { childList: true })\n }\n\n const addHandle = () => {\n if (!document.body || document.getElementById(\"sixb-desktop-drag\")) return\n const handle = document.createElement(\"div\")\n handle.id = \"sixb-desktop-drag\"\n handle.setAttribute(\"aria-hidden\", \"true\")\n // First in the body, not last: the draggable area is built in document order, so a control\n // is only cut out of it if it comes after the handle.\n document.body.prepend(handle)\n }\n if (document.readyState === \"loading\") {\n document.addEventListener(\"DOMContentLoaded\", addHandle, { once: true })\n } else {\n addHandle()\n }\n})()\n";
44
48
  //# sourceMappingURL=window.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"window.d.ts","sourceRoot":"","sources":["../../src/shell/window.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE/C;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAOrE;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAMlE;AAED;;;GAGG;AACH,eAAO,MAAM,iBAAiB;;;CAAiD,CAAA;AAE/E,iGAAiG;AACjG,eAAO,MAAM,kBAAkB,KAAK,CAAA;AAEpC;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,QAsBvB,CAAA;AAEV;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,ohCA8BpC,CAAA"}
1
+ {"version":3,"file":"window.d.ts","sourceRoot":"","sources":["../../src/shell/window.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE/C;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAOrE;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAMlE;AAED;;;GAGG;AACH,eAAO,MAAM,iBAAiB;;;CAAiD,CAAA;AAE/E;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM;;;;;EAEjD;AAED,qFAAqF;AACrF,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,GAAG,OAAO,CAEpE;AAED,iGAAiG;AACjG,eAAO,MAAM,kBAAkB,KAAK,CAAA;AAEpC;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,QAsBvB,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sixb/desktop",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Ship a Sixb project's app as a desktop app",
5
5
  "keywords": [
6
6
  "sixb",
package/src/generate.ts CHANGED
@@ -5,7 +5,6 @@ import { readAppIdentity } from "./identity"
5
5
  import { drawMacIcon } from "./mac-icon"
6
6
  import { run } from "./run"
7
7
  import type { ShellConfig } from "./shell/navigation"
8
- import { HIDDEN_TITLE_BAR_PRELOAD } from "./shell/window"
9
8
 
10
9
  export interface DesktopAppOptions {
11
10
  /** The Sixb project. The desktop app is generated under its `.sixb/desktop`. */
@@ -62,7 +61,6 @@ export async function writeDesktopApp(options: DesktopAppOptions): Promise<Gener
62
61
  await Promise.all([
63
62
  writeFile(join(dir, "package.json"), `${JSON.stringify(manifest, null, 2)}\n`),
64
63
  writeFile(join(dir, "shell.json"), `${JSON.stringify(config, null, 2)}\n`),
65
- writeFile(join(dir, "preload.js"), HIDDEN_TITLE_BAR_PRELOAD),
66
64
  writeFile(join(dir, "index.html"), renderShellPage(template, { ...config, icon: !!icon })),
67
65
  ])
68
66
 
@@ -73,10 +71,11 @@ export async function writeDesktopApp(options: DesktopAppOptions): Promise<Gener
73
71
  process.execPath,
74
72
  "build",
75
73
  join(SHELL_SOURCE_DIR, "main.ts"),
74
+ join(SHELL_SOURCE_DIR, "preload.ts"),
76
75
  "--target=node",
77
76
  "--format=cjs",
78
77
  "--external=electron",
79
- `--outfile=${join(dir, "main.js")}`,
78
+ `--outdir=${dir}`,
80
79
  ],
81
80
  "Could not compile the desktop shell."
82
81
  )
@@ -0,0 +1,16 @@
1
+ /** The name a page finds the desktop app under, through `useDesktop` in `@sixb/client/hooks`. */
2
+ export const DESKTOP_BRIDGE_NAME = "sixbDesktop"
3
+
4
+ /**
5
+ * What the shell offers the page. `SixbDesktop` in `@sixb/client` is the page's view of this, and
6
+ * `tests/app.test.ts` holds the two together.
7
+ *
8
+ * The page is remote, and anything here is open to whatever runs in it. To offer more:
9
+ * 1. Add one narrow method here, calling `ipcRenderer.invoke`, never a general way to run a command
10
+ * or read a file. The page asks; the person decides, in a native dialog, what it gets.
11
+ * 2. Handle it in `main.ts`, refusing any sender that `isAppUrl` does not accept.
12
+ * 3. Add it to `SixbDesktop` as optional, because a page can outlive the shell it runs in.
13
+ */
14
+ export function desktopBridge<const Platform extends NodeJS.Platform>(platform: Platform) {
15
+ return { platform }
16
+ }
package/src/shell/main.ts CHANGED
@@ -4,7 +4,14 @@ import { existsSync, readFileSync } from "node:fs"
4
4
  import { join } from "node:path"
5
5
  import { app, BrowserWindow, type Event, nativeTheme, session, shell } from "electron"
6
6
  import { createNavigationState, isWebUrl, nextNavigationStep, type ShellConfig } from "./navigation"
7
- import { HIDDEN_TITLE_BAR_CSS, isAppUrl, mayNavigate, WINDOW_BACKGROUND } from "./window"
7
+ import {
8
+ HIDDEN_TITLE_BAR_CSS,
9
+ hasHiddenTitleBar,
10
+ isAppUrl,
11
+ mayNavigate,
12
+ pageWebPreferences,
13
+ WINDOW_BACKGROUND,
14
+ } from "./window"
8
15
 
9
16
  // Chromium's code for a load that was replaced by another one, which is not a failure.
10
17
  const ERR_ABORTED = -3
@@ -15,8 +22,7 @@ const appDir = app.getAppPath()
15
22
  const config = JSON.parse(readFileSync(join(appDir, "shell.json"), "utf-8")) as ShellConfig
16
23
  const loadingPage = join(appDir, "index.html")
17
24
  const icon = existsSync(join(appDir, "icon.png")) ? join(appDir, "icon.png") : undefined
18
- // Only macOS draws window controls over the page; elsewhere the title bar stays.
19
- const inset = process.platform === "darwin"
25
+ const inset = hasHiddenTitleBar(process.platform)
20
26
  const state = createNavigationState()
21
27
 
22
28
  app.setName(config.name)
@@ -33,13 +39,7 @@ function createWindow(): void {
33
39
  backgroundColor: nativeTheme.shouldUseDarkColors
34
40
  ? WINDOW_BACKGROUND.dark
35
41
  : WINDOW_BACKGROUND.light,
36
- webPreferences: {
37
- // The window shows a remote page: it gets no Node, and a world apart from the preload's.
38
- sandbox: true,
39
- contextIsolation: true,
40
- nodeIntegration: false,
41
- preload: inset ? join(appDir, "preload.js") : undefined,
42
- },
42
+ webPreferences: pageWebPreferences(join(appDir, "preload.js")),
43
43
  })
44
44
  const contents = win.webContents
45
45
  win.once("ready-to-show", () => win.show())
@@ -0,0 +1,44 @@
1
+ // Runs in every page the window shows, before the page's own scripts, in a world of its own. Electron
2
+ // sandboxes it: of what it imports, only `electron` may remain once compiled.
3
+ import { contextBridge } from "electron"
4
+ import { DESKTOP_BRIDGE_NAME, desktopBridge } from "./bridge"
5
+ import { CONTROLS_HEIGHT_PX, hasHiddenTitleBar } from "./window"
6
+
7
+ contextBridge.exposeInMainWorld(DESKTOP_BRIDGE_NAME, desktopBridge(process.platform))
8
+
9
+ if (hasHiddenTitleBar(process.platform)) installHiddenTitleBar()
10
+
11
+ /**
12
+ * Marks `<html>` the moment the element exists, so an app can read the mark on its first render,
13
+ * and adds the drag handle once there is a body to hold it.
14
+ */
15
+ function installHiddenTitleBar(): void {
16
+ const mark = () => {
17
+ const root = document.documentElement
18
+ if (!root) return false
19
+ root.dataset.sixbDesktop = "inset"
20
+ root.style.setProperty("--sixb-desktop-controls-height", `${CONTROLS_HEIGHT_PX}px`)
21
+ return true
22
+ }
23
+ if (!mark()) {
24
+ const observer = new MutationObserver(() => {
25
+ if (mark()) observer.disconnect()
26
+ })
27
+ observer.observe(document, { childList: true })
28
+ }
29
+
30
+ const addHandle = () => {
31
+ if (!document.body || document.getElementById("sixb-desktop-drag")) return
32
+ const handle = document.createElement("div")
33
+ handle.id = "sixb-desktop-drag"
34
+ handle.setAttribute("aria-hidden", "true")
35
+ // First in the body, not last: the draggable area is built in document order, so a control
36
+ // is only cut out of it if it comes after the handle.
37
+ document.body.prepend(handle)
38
+ }
39
+ if (document.readyState === "loading") {
40
+ document.addEventListener("DOMContentLoaded", addHandle, { once: true })
41
+ } else {
42
+ addHandle()
43
+ }
44
+ }
@@ -31,6 +31,19 @@ export function isAppUrl(config: ShellConfig, url: string): boolean {
31
31
  */
32
32
  export const WINDOW_BACKGROUND = { light: "#fafafa", dark: "#0a0a0a" } as const
33
33
 
34
+ /**
35
+ * What the window's page runs with. The page is remote: it gets no Node and a world apart from the
36
+ * preload's, on every platform, and reaches the shell only through what the preload exposes.
37
+ */
38
+ export function pageWebPreferences(preload: string) {
39
+ return { sandbox: true, contextIsolation: true, nodeIntegration: false, preload }
40
+ }
41
+
42
+ /** Only macOS draws window controls over the page; elsewhere the title bar stays. */
43
+ export function hasHiddenTitleBar(platform: NodeJS.Platform): boolean {
44
+ return platform === "darwin"
45
+ }
46
+
34
47
  /** The band at the top of a window without a title bar where macOS draws the window controls. */
35
48
  export const CONTROLS_HEIGHT_PX = 36
36
49
 
@@ -70,42 +83,3 @@ export const HIDDEN_TITLE_BAR_CSS = [
70
83
  ].join(",")}{-webkit-app-region:no-drag}`,
71
84
  `[data-slot="sidebar-inner"]{padding-top:calc(${CONTROLS_HEIGHT_PX}px - 0.5rem)}`,
72
85
  ].join("")
73
-
74
- /**
75
- * The preload of a window without a title bar: runs in every page before the page's own scripts.
76
- *
77
- * It marks `<html>` the moment the element exists, so an app can read the mark on its first
78
- * render, and adds the drag handle once there is a body to hold it. Written to a file by
79
- * `writeDesktopApp`, which is why it is source text and not a function.
80
- */
81
- export const HIDDEN_TITLE_BAR_PRELOAD = `(() => {
82
- const mark = () => {
83
- const root = document.documentElement
84
- if (!root) return false
85
- root.dataset.sixbDesktop = "inset"
86
- root.style.setProperty("--sixb-desktop-controls-height", "${CONTROLS_HEIGHT_PX}px")
87
- return true
88
- }
89
- if (!mark()) {
90
- const observer = new MutationObserver(() => {
91
- if (mark()) observer.disconnect()
92
- })
93
- observer.observe(document, { childList: true })
94
- }
95
-
96
- const addHandle = () => {
97
- if (!document.body || document.getElementById("sixb-desktop-drag")) return
98
- const handle = document.createElement("div")
99
- handle.id = "sixb-desktop-drag"
100
- handle.setAttribute("aria-hidden", "true")
101
- // First in the body, not last: the draggable area is built in document order, so a control
102
- // is only cut out of it if it comes after the handle.
103
- document.body.prepend(handle)
104
- }
105
- if (document.readyState === "loading") {
106
- document.addEventListener("DOMContentLoaded", addHandle, { once: true })
107
- } else {
108
- addHandle()
109
- }
110
- })()
111
- `