@wildwinter/app-shell 0.56.0 → 0.58.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 +215 -0
- package/dist/chunk-4JQWNC6Z.js +319 -0
- package/dist/chunk-4JQWNC6Z.js.map +1 -0
- package/dist/chunk-ICYPXUNA.js +8 -0
- package/dist/chunk-ICYPXUNA.js.map +1 -0
- package/dist/{chunk-CPNTROWD.js → chunk-OZQ2BYXH.js} +13 -4
- package/dist/chunk-OZQ2BYXH.js.map +1 -0
- package/dist/controls.css +52 -0
- package/dist/controls.css.map +1 -1
- package/dist/index.cjs +352 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +24 -1
- package/dist/index.js.map +1 -1
- package/dist/tool-window.cjs +11 -2
- package/dist/tool-window.cjs.map +1 -1
- package/dist/tool-window.d.cts +10 -4
- package/dist/tool-window.d.ts +10 -4
- package/dist/tool-window.js +1 -1
- package/dist/updater.cjs.map +1 -1
- package/dist/updater.js +1 -1
- package/dist/value-rows.cjs +200 -10
- package/dist/value-rows.cjs.map +1 -1
- package/dist/value-rows.d.cts +36 -1
- package/dist/value-rows.d.ts +36 -1
- package/dist/value-rows.js +197 -12
- package/dist/value-rows.js.map +1 -1
- package/dist/view-client.cjs +172 -0
- package/dist/view-client.cjs.map +1 -0
- package/dist/view-client.d.cts +46 -0
- package/dist/view-client.d.ts +46 -0
- package/dist/view-client.js +165 -0
- package/dist/view-client.js.map +1 -0
- package/dist/view-host-web.cjs +327 -0
- package/dist/view-host-web.cjs.map +1 -0
- package/dist/view-host-web.d.cts +61 -0
- package/dist/view-host-web.d.ts +61 -0
- package/dist/view-host-web.js +4 -0
- package/dist/view-host-web.js.map +1 -0
- package/dist/view-host.cjs +532 -0
- package/dist/view-host.cjs.map +1 -0
- package/dist/view-host.d.cts +178 -0
- package/dist/view-host.d.ts +178 -0
- package/dist/view-host.js +426 -0
- package/dist/view-host.js.map +1 -0
- package/dist/view-preload.cjs +16 -0
- package/dist/view-preload.cjs.map +1 -0
- package/dist/view-preload.js +8 -0
- package/dist/view-preload.js.map +1 -0
- package/dist/view-protocol.cjs +12 -0
- package/dist/view-protocol.cjs.map +1 -0
- package/dist/view-protocol.d.cts +257 -0
- package/dist/view-protocol.d.ts +257 -0
- package/dist/view-protocol.js +3 -0
- package/dist/view-protocol.js.map +1 -0
- package/package.json +25 -4
- package/dist/chunk-CPNTROWD.js.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tool-window.ts","../src/view-protocol.ts","../src/view-serve.ts","../src/view-package.ts","../src/view-host.ts"],"names":["app","BrowserWindow","join","path","extname","viewsFor","readFile","relative","isAbsolute","realpath","session","lstat","basename","MANIFEST_FILE_SUFFIX","parseManifest","manifestProblems","readdir","why","gunzipSync","mkdir","dirname","writeFile","rename","rm","fileURLToPath","MessageChannelMain"],"mappings":"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","file":"view-host.cjs","sourcesContent":["// ---------------------------------------------------------------------------\n// The tool-window kit, MAIN-PROCESS half: the detached helper-window\n// machinery both apps had grown four copies of (Patterpad's Play + Search +\n// Coverage, Storyletter's Board + Find). Remembered bounds with a\n// disconnected-monitor guard, debounced persistence, the Reset View rescue,\n// and the pinnable window factory they compose into.\n//\n// This module imports electron, so it ships as its own subpath\n// (@wildwinter/app-shell/tool-window) - never import it from renderer code.\n// The renderer half (pin button, .swin drag-bar grammar) lives in the root\n// export + tool-window.css.\n//\n// Two deliberate corrections over the Patterpad originals, for when it\n// migrates here: rescueToolWindow re-pins EVERY window it rescues (Patterpad\n// re-pinned only Play, so its store lied about Search + Coverage), and the\n// factory always wires closed-cleanup through an identity guard.\n// ---------------------------------------------------------------------------\n\nimport { BrowserWindow, app, screen } from \"electron\";\nimport { join } from \"node:path\";\nimport type { Satellite } from \"./session.js\";\n\nexport interface ToolWindowSize {\n width: number;\n height: number;\n}\n\nexport interface ToolWindowBounds {\n x?: number;\n y?: number;\n width: number;\n height: number;\n}\n\n/** The per-window store slice every app remembers: where it was, whether it\n * floats. (Patterpad's PlayWindowState, generalised.) */\nexport interface ToolWindowState {\n bounds?: ToolWindowBounds;\n pinned: boolean;\n}\n\n/** Use a remembered rect only if it still intersects a live display (a window\n * saved on a now-disconnected monitor must not open offscreen). Falls back\n * to the default size; a remembered size is kept even when the position is\n * dropped (Electron then centres it). */\nexport function savedWindowRect(\n saved: ToolWindowBounds | undefined,\n def: ToolWindowSize,\n min: ToolWindowSize,\n): ToolWindowBounds {\n if (!saved) return { ...def };\n const w = Math.max(min.width, saved.width), h = Math.max(min.height, saved.height);\n if (saved.x != null && saved.y != null) {\n const onScreen = screen.getAllDisplays().some((d) => {\n const a = d.workArea;\n return saved.x! + w > a.x + 40 && saved.x! < a.x + a.width - 40 && saved.y! + h > a.y + 20 && saved.y! < a.y + a.height - 20;\n });\n if (onScreen) return { x: saved.x, y: saved.y, width: w, height: h };\n }\n return { width: w, height: h };\n}\n\n/** Persist a helper window's bounds as the user moves / resizes / closes it\n * (debounced 400ms; close flushes synchronously). */\nexport function rememberBounds(w: BrowserWindow, write: (bounds: ToolWindowBounds) => void): void {\n const saveBounds = (): void => { if (!w.isDestroyed()) write(w.getBounds()); };\n let boundsTimer: ReturnType<typeof setTimeout> | undefined;\n const queueSave = (): void => { clearTimeout(boundsTimer); boundsTimer = setTimeout(saveBounds, 400); };\n w.on(\"resize\", queueSave);\n w.on(\"move\", queueSave);\n w.on(\"close\", () => { clearTimeout(boundsTimer); saveBounds(); });\n}\n\n/** Centre a rect on the primary display's work area. */\nexport function centeredOnPrimary(size: ToolWindowSize): { x: number; y: number } {\n const a = screen.getPrimaryDisplay().workArea;\n return { x: Math.round(a.x + (a.width - size.width) / 2), y: Math.round(a.y + (a.height - size.height) / 2) };\n}\n\n/** Reset View's per-window half: un-minimise, re-pin (always - the store\n * says pinned:true after a rescue, so the window must agree), default size,\n * centred on the primary display, shown. The host clears its remembered\n * bounds itself (that's a store concern). No focus steal: helpers show\n * behind the editor. */\nexport function rescueToolWindow(w: BrowserWindow | undefined | null, def: ToolWindowSize): void {\n if (!w || w.isDestroyed()) return;\n if (w.isMinimized()) w.restore();\n w.setBounds({ ...def, ...centeredOnPrimary(def) });\n w.show();\n // Raise within OUR stack. This used to setAlwaysOnTop(true), which on macOS\n // and Windows floats the window above every other APPLICATION - and stuck\n // that way, since nothing ever unset it.\n w.moveTop();\n}\n\n/** macOS pinned windows, for the activation toggle below. */\nconst macPinned = new Set<BrowserWindow>();\nlet macActivationWired = false;\nfunction wireMacActivation(): void {\n if (macActivationWired) return;\n macActivationWired = true;\n // Floating is a GLOBAL window level: left on while another app is front,\n // the pinned window would sit over that app too - the exact 2026-08-25\n // complaint. Scoping it to our app's active state keeps the pin meaning\n // \"above my editor\" and nothing more.\n // Not while a modal has the pins held (frontForModal): the editor is in\n // front for it, and coming back to the app must not cover it again.\n app.on(\"did-become-active\", () => { if (pinsHeld > 0) return; for (const w of macPinned) if (!w.isDestroyed()) w.setAlwaysOnTop(true, \"floating\"); });\n app.on(\"did-resign-active\", () => { for (const w of macPinned) if (!w.isDestroyed()) w.setAlwaysOnTop(false); });\n}\n\n/**\n * The pin: keep a tool window above THIS APP'S MAIN WINDOW, not above the\n * world. Two findings shaped the mechanism, one per platform family:\n *\n * - Bare `alwaysOnTop` (the first implementation) floats the window over\n * every other application on macOS and Windows, so a pinned Board meant\n * nothing else on the machine could come to the front (a Storyletter user\n * report, 2026-08-25).\n * - A CHILD window (the second implementation) is position-coupled to its\n * parent on macOS: dragging the editor dragged the Board with it (a\n * Storyletter user report, 2026-08-28). Windows and Linux owned windows do\n * not move with their owner, so the child mechanism stays right for them.\n *\n * So: on macOS the pin is ACTIVATION-SCOPED alwaysOnTop - floating while\n * this app is active, dropped the moment it resigns - which is above the\n * editor without following its drags and without covering anyone else. On\n * Windows and Linux the pin is a child window, which stays over its parent\n * and stacks normally against everything else. Each branch clears the other\n * mechanism first, which also heals a window an older build pinned the old\n * way. `platform` is a seam for the tests; leave it defaulted.\n */\nexport function pinToolWindow(\n w: BrowserWindow | undefined | null, parent: BrowserWindow | undefined | null, on: boolean,\n platform: NodeJS.Platform = process.platform,\n): void {\n if (!w || w.isDestroyed()) return;\n if (platform === \"darwin\") {\n w.setParentWindow(null);\n if (on) {\n if (!macPinned.has(w)) {\n macPinned.add(w);\n w.once(\"closed\", () => macPinned.delete(w));\n }\n wireMacActivation();\n // Raise only when the app is active now and no modal has the pins\n // held; otherwise the next did-become-active, or the release, raises it.\n w.setAlwaysOnTop(pinsHeld === 0 && BrowserWindow.getFocusedWindow() !== null, \"floating\");\n } else {\n macPinned.delete(w);\n w.setAlwaysOnTop(false);\n }\n return;\n }\n w.setAlwaysOnTop(false);\n const owner = on && parent && !parent.isDestroyed() ? parent : null;\n // While a modal has the pins held, remember the choice and apply it at the\n // release, so the editor stays in front until the modal closes.\n if (pinsHeld > 0) {\n if (owner) heldChildren.set(w, owner); else heldChildren.delete(w);\n w.setParentWindow(null);\n return;\n }\n w.setParentWindow(owner);\n}\n\n// ---------------------------------------------------------------------------\n// Fronting the main window for a modal. A pinned tool window rides above the\n// editor by design, so a modal the EDITOR shows (the updater's prompts) opened\n// underneath one: the editor blurred behind its scrim and the prompt was\n// hidden, with nothing on screen to say why (both apps, 2026-10-02, the Play\n// window over the editor). While such a modal is up the pins step aside and\n// the editor comes forward; when it closes, every pin goes back as it was.\n// ---------------------------------------------------------------------------\n\n/** How many modals are holding the pins down. The pins return at zero. */\nlet pinsHeld = 0;\n/** Windows and Linux: the tool windows unparented by the hold, and the window\n * each goes back to. */\nconst heldChildren = new Map<BrowserWindow, BrowserWindow>();\n\n/**\n * Bring `main` in front of every tool window, pinned ones included, for as\n * long as a modal it shows is open: un-minimise, show, focus, with the pins\n * set aside. Returns the release, which puts the pins back (idempotent; holds\n * nest, and the pins return when the last is released). Call it only while\n * the app is active: focusing a window of an inactive app is the focus steal\n * the platforms forbid. `platform` is a seam for the tests.\n */\nexport function frontForModal(main: BrowserWindow, platform: NodeJS.Platform = process.platform): () => void {\n if (main.isDestroyed()) return () => {};\n pinsHeld++;\n if (platform === \"darwin\") {\n for (const w of macPinned) if (!w.isDestroyed()) w.setAlwaysOnTop(false);\n } else {\n for (const w of BrowserWindow.getAllWindows()) {\n if (w === main || w.isDestroyed()) continue;\n const owner = w.getParentWindow();\n if (owner !== main) continue;\n heldChildren.set(w, main);\n w.setParentWindow(null);\n }\n }\n if (main.isMinimized()) main.restore();\n main.show();\n main.focus();\n\n let released = false;\n return () => {\n if (released) return;\n released = true;\n if (--pinsHeld > 0) return;\n if (platform === \"darwin\") {\n // Only float again if the app is active; otherwise did-become-active does.\n const active = BrowserWindow.getFocusedWindow() !== null;\n if (active) for (const w of macPinned) if (!w.isDestroyed()) w.setAlwaysOnTop(true, \"floating\");\n } else {\n for (const [w, owner] of heldChildren) {\n if (!w.isDestroyed() && !owner.isDestroyed()) w.setParentWindow(owner);\n }\n heldChildren.clear();\n }\n };\n}\n\nexport interface ToolWindowOptions {\n title: string;\n /** Renderer entry, relative to rendererDir (e.g. \"table.html\", \"search/index.html\"). Not used with `url`. */\n page?: string;\n /** Absolute path of the built renderer dir (join(import.meta.dirname, \"../renderer\")). Not used with `url`. */\n rendererDir?: string;\n /** Load this URL instead of a page from the renderer dir (a view window's `patter-view://...`). */\n url?: string;\n /** Absolute path of the preload bridge. */\n preload: string;\n /** A session partition of the window's own (a view's in-memory `view-<id>`); the default session otherwise. */\n partition?: string;\n /** Whether the window may open devtools; Electron's default (yes) when absent. */\n devTools?: boolean;\n rect: ToolWindowBounds;\n min?: ToolWindowSize;\n /** false = frameless: the renderer draws its own slim drag bar (.swin-head). */\n frame?: boolean;\n /** The window to PIN ABOVE: the app's main window, resolved at open time (a\n * getter, because the main window can be recreated). With `pinned` true\n * the new tool window rides above it - on macOS via activation-scoped\n * alwaysOnTop (a child window there follows the editor's drags), on\n * Windows and Linux as a child window (bare alwaysOnTop there floats over\n * the whole machine). See pinToolWindow for both findings. */\n pinTo?: () => BrowserWindow | undefined;\n /** Start pinned (the host remembers); flip later with `pinToolWindow`. */\n pinned?: boolean;\n /** Wire debounced bounds persistence into the host's store slice. */\n remember?: (bounds: ToolWindowBounds) => void;\n /** Fired when THIS window closes (identity-guarded; a stale close never\n * clears a newer window). Null the host's handle here. */\n onClosed?: () => void;\n}\n\n/** The pinnable detached-window factory (hardened webPreferences, one\n * preload, electron-vite's dev-URL/file split). Focuses and returns the\n * existing window when there is one; otherwise creates, wires, and loads. */\nexport function openToolWindow(existing: BrowserWindow | undefined | null, opts: ToolWindowOptions): BrowserWindow {\n if (existing && !existing.isDestroyed()) { existing.focus(); return existing; }\n if (opts.url === undefined && (opts.page === undefined || opts.rendererDir === undefined)) {\n throw new Error(\"openToolWindow needs a url, or a page and its rendererDir\");\n }\n const w = new BrowserWindow({\n ...opts.rect,\n ...(opts.min !== undefined ? { minWidth: opts.min.width, minHeight: opts.min.height } : {}),\n show: false,\n title: opts.title,\n ...(opts.frame !== undefined ? { frame: opts.frame } : {}),\n webPreferences: {\n contextIsolation: true, nodeIntegration: false, sandbox: true,\n // Electron's defaults, said out loud: no webview tag, and a dropped file never navigates the window.\n webviewTag: false, navigateOnDragDrop: false,\n preload: opts.preload,\n ...(opts.partition !== undefined ? { partition: opts.partition } : {}),\n ...(opts.devTools !== undefined ? { devTools: opts.devTools } : {}),\n },\n });\n if (opts.pinTo !== undefined) pinToolWindow(w, opts.pinTo(), opts.pinned ?? false);\n if (opts.remember) rememberBounds(w, opts.remember);\n w.once(\"ready-to-show\", () => w.show());\n if (opts.onClosed) w.on(\"closed\", opts.onClosed);\n if (opts.url !== undefined) void w.loadURL(opts.url);\n else if (process.env[\"ELECTRON_RENDERER_URL\"]) void w.loadURL(`${process.env[\"ELECTRON_RENDERER_URL\"]}/${opts.page}`);\n else void w.loadFile(join(opts.rendererDir!, opts.page!));\n return w;\n}\n\n// ---------------------------------------------------------------------------\n// The tool windows AS DATA: one row per window, and everything that walks the\n// windows walks the rows. Lifted from Storyletter's TOOL_WINDOWS table +\n// openToolWindowFor + rescueWindows (studio/src/main/index.ts), where it had\n// replaced four near-identical openX() functions whose differences were all\n// mistakes (only one forwarded its console; Reset View rescued two of four).\n// Patterpad's three hand copies (createPlayWindow / createSearchWindow /\n// createCoverageWindow and a four-branch rescueWindows) become three rows.\n//\n// Two things the table makes structurally impossible: forgetting to register\n// a window as a satellite for the project-changed nudge (every row is\n// registered when a session is given), and Reset View rescuing a subset (it\n// walks the rows).\n// ---------------------------------------------------------------------------\n\n\nexport interface ToolWindowSpec<N extends string = string> {\n /** The window's key: the store slice it remembers under, the dev-console\n * prefix, and what `open(name)` takes. */\n name: N;\n title: string;\n /** Renderer entry, relative to the env's rendererDir. */\n page: string;\n def: ToolWindowSize;\n min: ToolWindowSize;\n /** The remembered rect, read at open time (a store read-through). */\n bounds: () => ToolWindowBounds | undefined;\n /** Persist a moved / resized rect (debounced by the factory). */\n remember: (bounds: ToolWindowBounds) => void;\n /** Whether it starts pinned above the editor (both apps default true). */\n pinned: () => boolean;\n /** Frameless (the family's shape: the renderer draws `toolWindowHead`).\n * Default false = frameless; pass `frame: true` for an OS title bar. */\n frame?: boolean;\n /** The project-changed channel for THIS window when it differs from the\n * env's (Storyletter's Links window listens on \"links:focus\"). */\n channel?: string;\n /** Drop what main caches for the outgoing project on this window's behalf\n * (a coverage report, a search anchor). Runs whether the window is open\n * or not: see session.ts. */\n clear?: () => void;\n}\n\nexport interface ToolWindowEnv {\n /** Absolute path of the built renderer dir. */\n rendererDir: string;\n /** Absolute path of the preload bridge. */\n preload: string;\n /** The window to pin above: the app's main window, resolved at open time. */\n pinTo: () => BrowserWindow | undefined;\n /** Register every row as a satellite of the project session, so a new\n * project cannot open underneath a tool window that still shows the old\n * one. `channel` is the nudge each window listens on (a row may override). */\n session?: { addSatellite: (satellite: Satellite) => () => void; channel: string };\n /** Reset View's store half, run first: forget every remembered rect and\n * set pinned true (`resetWindows(store)` from app-store, or the app's own). */\n resetStore?: () => void;\n /** The channel a rescued window's renderer hears its re-pin on, so its pin\n * button agrees with the store (default \"state:pinned\"). */\n pinChannel?: string;\n /** Runs once per window created: forward its console in dev, say. */\n onOpened?: (w: BrowserWindow, name: string) => void;\n}\n\nexport interface ToolWindows<N extends string> {\n /** Open (or focus) one window by name. */\n open(name: N): BrowserWindow;\n /** The live window, if open. */\n get(name: N): BrowserWindow | undefined;\n /** Reset View's window half, over the table: store reset, then every open\n * window restored, default size, centred, re-pinned, and told so. */\n rescue(): void;\n /** Every window that is open right now. */\n all(): BrowserWindow[];\n readonly names: readonly N[];\n}\n\n/** Build the table. The windows are owned here: a host reads them through\n * `get(name)` rather than holding module variables of its own. */\nexport function defineToolWindows<N extends string>(specs: ToolWindowSpec<N>[], env: ToolWindowEnv): ToolWindows<N> {\n const live = new Map<N, BrowserWindow>();\n const byName = (name: N): ToolWindowSpec<N> => {\n const found = specs.find((s) => s.name === name);\n if (!found) throw new Error(`no tool window \"${name}\"`);\n return found;\n };\n const get = (name: N): BrowserWindow | undefined => {\n const w = live.get(name);\n return w && !w.isDestroyed() ? w : undefined;\n };\n if (env.session) {\n for (const spec of specs) {\n env.session.addSatellite({\n window: () => get(spec.name),\n channel: spec.channel ?? env.session.channel,\n ...(spec.clear ? { clear: spec.clear } : {}),\n });\n }\n }\n const open = (name: N): BrowserWindow => {\n const spec = byName(name);\n const existing = get(name);\n const opened = openToolWindow(existing, {\n title: spec.title, page: spec.page, frame: spec.frame ?? false,\n rendererDir: env.rendererDir, preload: env.preload,\n rect: savedWindowRect(spec.bounds(), spec.def, spec.min),\n min: spec.min,\n pinTo: env.pinTo,\n pinned: spec.pinned(),\n remember: spec.remember,\n });\n if (opened !== existing) {\n live.set(name, opened);\n // Identity-guarded: a stale close never clears a newer window.\n opened.on(\"closed\", () => { if (live.get(name) === opened) live.delete(name); });\n env.onOpened?.(opened, name);\n }\n return opened;\n };\n const rescue = (): void => {\n env.resetStore?.();\n for (const spec of specs) rescueToolWindow(get(spec.name), spec.def);\n // ACTUALLY pin them, then tell them. `rescueToolWindow` restores, resizes,\n // centres and raises but never pins; the store now says pinned, so the\n // window and its button have to agree before the claim is made.\n for (const spec of specs) {\n const w = get(spec.name);\n if (!w) continue;\n pinToolWindow(w, env.pinTo(), true);\n w.webContents.send(env.pinChannel ?? \"state:pinned\", true);\n }\n };\n return {\n open, get, rescue,\n all: () => specs.map((s) => get(s.name)).filter((w): w is BrowserWindow => w !== undefined),\n names: specs.map((s) => s.name),\n };\n}\n","// ---------------------------------------------------------------------------\n// The view host API (tier 2 view extensions): the messages between a view, a\n// web component an extension's installed package supplies, and the app that\n// hosts it in a sandboxed floating window (patterkit design/view-extensions.md).\n//\n// Both ends import this module: the app through `hostView`, a view through\n// `@wildwinter/app-shell/view-client`. It is types and constants only, so a\n// view's bundle takes nothing else from the shell.\n//\n// The transport is a MessagePort straight from the view to the editor's\n// renderer, which holds what the writer sees in both apps. Nothing is\n// answered before the view's hello names the API it was built for; a host\n// that can't serve that API refuses it with a sentence and closes the port.\n// ---------------------------------------------------------------------------\n\n/** The API this shell's hosts and clients speak. A host serves exactly this version. */\nexport const VIEW_API = 1;\n\n/** A host: the app a view runs in. */\nexport type ViewHostName = \"patter\" | \"storylets\";\n\n/** A JSON value, as extension data and host fields are. */\nexport type ViewJson = string | number | boolean | null | ViewJson[] | { [key: string]: ViewJson };\n\n// --- What a host hands a view ---------------------------------------------\n\n/**\n * A node of the host's project, in one envelope for both hosts.\n *\n * `kind` is the manifest's node kind (`line`, `card`, `project`, ...). `host`\n * holds the host's own fields for that kind, documented per host in the\n * shell's README; `ext` holds extension data by extension id, the view's own\n * and those its manifest `reads`, nothing else.\n */\nexport interface ViewNode {\n readonly id: string;\n readonly kind: string;\n /** The containing node's id; null for the project. */\n readonly parent: string | null;\n /** A short label for the node, in the source language, as the editor names it. */\n readonly label: string;\n readonly host: { readonly [field: string]: ViewJson };\n readonly ext: { readonly [extension: string]: { readonly [field: string]: ViewJson } };\n /** The node's children in order, when they were asked for. */\n readonly children?: readonly ViewNode[];\n /** Set when somebody else holds the view's own extension data on this node: the sentence saying who. A write\n * touching the node is refused `held`; a view can show the node read-only meanwhile. */\n readonly held?: string;\n}\n\n/** The selection, innermost first (a line, then its snippet, block, and scene). Empty when nothing is selected. */\nexport interface ViewSelection {\n readonly path: readonly { readonly id: string; readonly kind: string }[];\n}\n\n/** A line's length: a recording's when there is one (`measured`), else the host's estimate from its text. */\nexport interface ViewMediaLength {\n readonly seconds: number;\n readonly measured: boolean;\n}\n\n/** The app's theme, for the view to draw in: its CSS custom properties and its colour scheme. */\nexport interface ViewTheme {\n readonly scheme: \"light\" | \"dark\";\n /** Custom property names, with their leading `--`, and their computed values. */\n readonly tokens: { readonly [name: string]: string };\n}\n\n/** Whether the view may write, and why not when it may not, as a sentence for the view to show. */\nexport interface ViewWritable {\n readonly readOnly: boolean;\n readonly why?: string;\n}\n\n/**\n * One write to the view's own extension data: a field on a node, set to a value, or cleared when `value` is absent.\n *\n * `field` is a top-level field the manifest declares for that node's kind, never a path into one: changing one\n * entry of a map or record writes the whole field's new value. Read the node again just before writing, so the\n * write builds on any edit made in the editor meanwhile; requests are answered in order, so nothing lands between.\n *\n * One write is one undo step in one history: its edits are all on the project node, or none of them are (the\n * project's data and a scene's sit in different histories in Patterpad). `hostView` refuses a write that mixes them.\n */\nexport interface ViewEdit {\n readonly node: string;\n readonly field: string;\n readonly value?: ViewJson;\n}\n\n/** Why a request was refused: a stable code for the view, and a sentence for the writer. */\nexport interface ViewRefusal {\n readonly code:\n | \"bad-request\" // the request's shape is wrong\n | \"unsupported\" // the host has no such thing (media in Storyletter)\n | \"gone\" // a node the request names doesn't exist\n | \"held\" // somebody else holds the data the write would change\n | \"read-only\" // the project or the view is read-only\n | \"failed\"; // the host tried and couldn't\n readonly message: string;\n}\n\n// --- Requests --------------------------------------------------------------\n\n/** Each request a view can make: what it sends, and what a granted reply carries. */\nexport interface ViewRequests {\n /** The project node: the host's project facts (Patter: cast, qualifiers, pad default; Storylets: boxes, tag groups). */\n project: { args: Record<string, never>; value: ViewNode };\n /** One node by id, and its children in order when `children` is true; null when there is no such node. */\n node: { args: { id: string; children?: boolean }; value: ViewNode | null };\n selection: { args: Record<string, never>; value: ViewSelection };\n /** Lines' lengths by id. Refused `unsupported` by a host with no media. */\n media: { args: { ids: string[] }; value: { [id: string]: ViewMediaLength } };\n /** Writes to the view's own extension, applied as one undo step with the label, or refused whole. */\n write: { args: { label: string; edits: ViewEdit[] }; value: { written: number } };\n /** Reveal and select a node in the editor. */\n select: { args: { id: string }; value: boolean };\n}\n\nexport type ViewOp = keyof ViewRequests;\n\n// --- Messages ----------------------------------------------------------------\n\n/** A view's first message. */\nexport interface ViewHello { readonly t: \"hello\"; readonly api: number }\n\nexport interface ViewRequest<K extends ViewOp = ViewOp> {\n readonly t: \"request\";\n readonly id: number;\n readonly op: K;\n readonly args: ViewRequests[K][\"args\"];\n}\n\n/** A key combination the view didn't handle, in the shell's portable spelling (\"Mod+Z\", \"Mod+Shift+Z\"), for the host to run. */\nexport interface ViewKey { readonly t: \"key\"; readonly combo: string }\n\nexport type ViewToHost = ViewHello | ViewRequest | ViewKey;\n\n/** The host's answer to a hello it can serve. */\nexport interface ViewWelcome {\n readonly t: \"welcome\";\n readonly api: number;\n readonly host: ViewHostName;\n /** The view's extension: its id, the installed version, and the version the project pins, if any. */\n readonly extension: { readonly id: string; readonly version: string; readonly pinned?: string };\n /** The view's own id within its manifest. */\n readonly view: string;\n /** The extension's project settings, by name. */\n readonly settings: { readonly [name: string]: string };\n readonly theme: ViewTheme;\n readonly writable: ViewWritable;\n}\n\n/** The host's answer to a hello it can't serve, before it closes the port. */\nexport interface ViewRefused { readonly t: \"refused\"; readonly message: string }\n\nexport type ViewReply =\n | { readonly t: \"reply\"; readonly id: number; readonly ok: true; readonly value: unknown }\n | { readonly t: \"reply\"; readonly id: number; readonly ok: false; readonly refusal: ViewRefusal };\n\n/** What the host tells a view unasked. */\nexport interface ViewEvents {\n /** The project changed: these nodes, or anything (\"all\"). Debounced; read again what you show. */\n changed: { nodes: readonly string[] | \"all\" };\n selection: ViewSelection;\n theme: ViewTheme;\n writable: ViewWritable;\n /** The window is closing; the port goes next. */\n closing: Record<string, never>;\n}\n\nexport type ViewEventName = keyof ViewEvents;\n\nexport interface ViewEvent<K extends ViewEventName = ViewEventName> {\n readonly t: \"event\";\n readonly name: K;\n readonly value: ViewEvents[K];\n}\n\nexport type HostToView = ViewWelcome | ViewRefused | ViewReply | ViewEvent;\n\n// --- The port's arrival --------------------------------------------------------\n\n/** The IPC channel main sends each end's port on. */\nexport const VIEW_PORT_CHANNEL = \"wildwinter-view:port\";\n\n/** The window message a preload posts a port with, so page code (which can't see ipcRenderer) can take it. */\nexport const VIEW_PORT_MESSAGE = \"wildwinter-view:port\";\n\n/** What main tells the editor about a view whose port it is handing over. */\nexport interface ViewPortMeta {\n /** The view window's id, unique while it's open. */\n readonly window: number;\n readonly extension: string;\n readonly view: string;\n readonly version: string;\n readonly pinned?: string;\n /** Set, with no port, when the view window has closed: the editor drops its handle for `window`. */\n readonly closed?: true;\n}\n\n// --- The host's side: what an app implements, and what hostView gives back ----\n\n/**\n * What an app supplies `hostView` for one open view: its own model, as the\n * writer sees it (unsaved edits included). `hostView` answers the protocol\n * from it, checks every request's shape, keeps `ext` to the view's own\n * extension and its manifest's `reads`, and refuses writes when not writable.\n * Any method may return a promise.\n */\nexport interface ViewAdapter {\n readonly host: ViewHostName;\n /** The project node. */\n project(): ViewNode | Promise<ViewNode>;\n node(id: string, children: boolean): ViewNode | null | Promise<ViewNode | null>;\n selection(): ViewSelection | Promise<ViewSelection>;\n /** Absent in a host with no media; `hostView` then refuses `media` as unsupported. */\n media?(ids: readonly string[]): { [id: string]: ViewMediaLength } | Promise<{ [id: string]: ViewMediaLength }>;\n /** Apply the edits to `extension`'s data as one undo step with the label, or refuse them all. */\n write(extension: string, label: string, edits: readonly ViewEdit[]): void | ViewRefusal | Promise<void | ViewRefusal>;\n select(id: string): boolean | Promise<boolean>;\n writable(extension: string): ViewWritable;\n /** The extension's project settings, by name. */\n settings(extension: string): { [name: string]: string };\n /** Run a key combination the view passed on (undo, redo, the app's shortcuts). */\n key?(combo: string): void;\n}\n\n/** What `hostView` returns, and `hostViews` for every open view: the app calls these as its model, selection,\n * theme, or locks change. */\nexport interface ViewHandle {\n changed(nodes: readonly string[] | \"all\"): void;\n selection(): void;\n theme(): void;\n writable(): void;\n close(): void;\n}\n","// ---------------------------------------------------------------------------\n// The view scheme (tier 2 view extensions): `<scheme>://<extension-id>/...`\n// serves an installed view package's files and nothing else, every response\n// under the view CSP, and the view's session reaches nothing off the scheme.\n//\n// No Electron import: each function takes the protocol or session it works\n// on, so the whole of it runs under test.\n// ---------------------------------------------------------------------------\n\nimport { readFile, realpath } from \"node:fs/promises\";\nimport { extname, isAbsolute, join, relative } from \"node:path\";\nimport { viewsFor } from \"@wildwinter/toolkit/extensions\";\nimport type { HostName, Manifest, ViewDecl } from \"@wildwinter/toolkit/extensions\";\n\n/** The view CSP: the package's own scripts and styles, inline styles, images and fonts from the package or data\n * URLs, and no network, frames, forms, or plugins. Sent as a header on every response. */\nexport const VIEW_CSP = [\n \"default-src 'self'\",\n \"script-src 'self'\",\n \"style-src 'self' 'unsafe-inline'\",\n \"img-src 'self' data: blob:\",\n \"font-src 'self' data:\",\n \"connect-src 'none'\",\n \"object-src 'none'\",\n \"base-uri 'none'\",\n \"form-action 'none'\",\n \"frame-src 'none'\",\n].join(\"; \");\n\n/** The entry an app adds to its ONE `protocol.registerSchemesAsPrivileged` call (Electron keeps only the last\n * call, so an app with other schemes lists them all there). Never `bypassCSP`. */\nexport function viewSchemePrivileges(scheme: string): {\n scheme: string; privileges: { standard: true; secure: true; supportFetchAPI: true };\n} {\n return { scheme, privileges: { standard: true, secure: true, supportFetchAPI: true } };\n}\n\n/** What the scheme serves for one extension id: the installed version's folder and its manifest. */\nexport interface ViewSource { readonly root: string; readonly manifest: Manifest }\n\nexport interface ServeViewsOptions {\n /** The scheme, without its colon: `patter-view`. */\n scheme: string;\n /** The app the views open in: a view the manifest doesn't offer this host isn't served. */\n host: HostName;\n /** The installed package for an extension id, or null for none. Read at each request. */\n resolve: (extension: string) => ViewSource | null | Promise<ViewSource | null>;\n}\n\n/** The URL of a view's page. */\nexport function viewPageUrl(scheme: string, extension: string, view: string): string {\n return `${scheme}://${extension}/?view=${encodeURIComponent(view)}`;\n}\n\nconst TYPES: Record<string, string> = {\n \".js\": \"text/javascript; charset=utf-8\", \".mjs\": \"text/javascript; charset=utf-8\",\n \".css\": \"text/css; charset=utf-8\", \".html\": \"text/html; charset=utf-8\", \".json\": \"application/json; charset=utf-8\",\n \".map\": \"application/json; charset=utf-8\", \".txt\": \"text/plain; charset=utf-8\", \".svg\": \"image/svg+xml\",\n \".png\": \"image/png\", \".jpg\": \"image/jpeg\", \".jpeg\": \"image/jpeg\", \".gif\": \"image/gif\", \".webp\": \"image/webp\",\n \".avif\": \"image/avif\", \".ico\": \"image/x-icon\", \".woff\": \"font/woff\", \".woff2\": \"font/woff2\", \".ttf\": \"font/ttf\",\n \".otf\": \"font/otf\", \".wasm\": \"application/wasm\",\n};\n\n/** The content type a package file is served with, by its extension. */\nexport function viewContentType(path: string): string {\n return TYPES[extname(path).toLowerCase()] ?? \"application/octet-stream\";\n}\n\nconst headers = (type: string): Record<string, string> => ({\n \"Content-Type\": type,\n \"Content-Security-Policy\": VIEW_CSP,\n \"X-Content-Type-Options\": \"nosniff\",\n \"Cache-Control\": \"no-store\",\n});\n\nconst notFound = (): Response => new Response(\"Not found\", { status: 404, headers: headers(\"text/plain; charset=utf-8\") });\n\nconst escapeHtml = (s: string): string =>\n s.replace(/&/g, \"&\").replace(/</g, \"<\").replace(/>/g, \">\").replace(/\"/g, \""\");\n\n/** The page that hosts a view: its entry as a module script and its element filling the window. */\nexport function viewPage(view: Pick<ViewDecl, \"name\" | \"entry\" | \"element\">): string {\n const el = view.element; // checked against the custom-element pattern by viewsFor\n return `<!doctype html>\n<html><head><meta charset=\"utf-8\"><title>${escapeHtml(view.name)}</title>\n<style>\nhtml, body { margin: 0; height: 100%; }\nbody { background: var(--bg, Canvas); color: var(--ink, CanvasText); font-family: var(--font-ui, system-ui, sans-serif); color-scheme: var(--color-scheme, light dark); }\nbody > ${el} { display: block; height: 100%; overflow: auto; }\n</style>\n<script type=\"module\" src=\"/${view.entry.split(\"/\").map(encodeURIComponent).join(\"/\")}\"></script>\n</head><body><${el}></${el}></body></html>\n`;\n}\n\n/** The response for one request on the view scheme. */\nexport async function viewResponse(url: string, opts: ServeViewsOptions): Promise<Response> {\n let u: URL;\n try { u = new URL(url); } catch { return notFound(); }\n if (u.protocol !== `${opts.scheme}:`) return notFound();\n const source = await opts.resolve(u.hostname);\n if (!source) return notFound();\n if (u.pathname === \"/\" || u.pathname === \"\") {\n const id = u.searchParams.get(\"view\");\n const view = id === null ? undefined : viewsFor(source.manifest, opts.host).find((v) => v.id === id);\n if (!view) return notFound();\n return new Response(viewPage(view), { status: 200, headers: headers(\"text/html; charset=utf-8\") });\n }\n const file = await confined(source.root, u.pathname);\n if (!file) return notFound();\n try {\n return new Response(new Uint8Array(await readFile(file)), { status: 200, headers: headers(viewContentType(file)) });\n } catch { return notFound(); }\n}\n\n/** The file a URL path names inside `root`, or null when it would land anywhere else (or isn't there). */\nexport async function confined(root: string, pathname: string): Promise<string | null> {\n let path: string;\n try { path = decodeURIComponent(pathname); } catch { return null; }\n if (path.includes(\"\\0\") || path.includes(\"\\\\\")) return null;\n const parts = path.replace(/^\\/+/, \"\").split(\"/\");\n if (parts.some((p) => p === \"\" || p === \".\" || p === \"..\")) return null;\n const full = join(root, ...parts);\n const inside = (base: string, p: string): boolean => {\n const rel = relative(base, p);\n return rel !== \"\" && !rel.startsWith(\"..\") && !isAbsolute(rel);\n };\n if (!inside(root, full)) return null;\n // Through links too: the real file must be inside the real root.\n try {\n const [realRoot, realFull] = await Promise.all([realpath(root), realpath(full)]);\n return inside(realRoot, realFull) ? realFull : null;\n } catch { return null; }\n}\n\n/** The part of Electron's `Protocol` the scheme needs. */\nexport interface ProtocolLike {\n handle(scheme: string, handler: (request: Request) => Response | Promise<Response>): void;\n}\n\n/** Serve the view scheme on a protocol: the view session's own (`session.fromPartition(...).protocol`), since\n * views run in a partition of their own. */\nexport function serveViews(protocol: ProtocolLike, opts: ServeViewsOptions): void {\n protocol.handle(opts.scheme, (request) => viewResponse(request.url, opts));\n}\n\n/** The part of Electron's `Session` the guard needs. */\nexport interface ViewSessionLike {\n webRequest: {\n onBeforeRequest(listener: (details: { url: string }, callback: (response: { cancel?: boolean }) => void) => void): void;\n };\n setPermissionRequestHandler(handler: ((wc: unknown, permission: string, callback: (granted: boolean) => void) => void) | null): void;\n setPermissionCheckHandler?(handler: ((...args: unknown[]) => boolean) | null): void;\n}\n\n/** Whether a view session may load a URL: its own scheme, devtools, and inline data only. */\nexport function viewRequestAllowed(url: string, scheme: string): boolean {\n const colon = url.indexOf(\":\");\n const protocol = colon < 0 ? \"\" : url.slice(0, colon).toLowerCase();\n return protocol === scheme || protocol === \"devtools\" || protocol === \"data\" || protocol === \"blob\";\n}\n\n/** No network and no permissions for a view's session: anything off the view scheme is cancelled, and every\n * permission (camera, notifications, clipboard, ...) is refused. */\nexport function guardViewSession(session: ViewSessionLike, scheme: string): void {\n session.webRequest.onBeforeRequest((details, callback) => callback({ cancel: !viewRequestAllowed(details.url, scheme) }));\n session.setPermissionRequestHandler((_wc, _permission, callback) => callback(false));\n session.setPermissionCheckHandler?.(() => false);\n}\n","// ---------------------------------------------------------------------------\n// View packages (tier 2 view extensions, patterkit design/view-extensions.md):\n// reading an extension's local package, a folder or an `npm pack` tarball,\n// installing it on this machine under `<dir>/<id>/<version>/`, listing what's\n// installed, and choosing the installed version for a project's pin.\n//\n// Main-process code with no Electron import, so it runs under test as it is.\n// The tarball reader is a small ustar reader over node:zlib (pax and GNU long\n// names included), so the package takes no new dependency.\n// ---------------------------------------------------------------------------\n\nimport { gunzipSync } from \"node:zlib\";\nimport { lstat, mkdir, readFile, readdir, rename, rm, writeFile } from \"node:fs/promises\";\nimport { basename, dirname, join } from \"node:path\";\nimport { manifestProblems, parseManifest, MANIFEST_FILE_SUFFIX } from \"@wildwinter/toolkit/extensions\";\nimport type { Manifest, Problem } from \"@wildwinter/toolkit/extensions\";\n\nconst ID = /^[a-z][a-z0-9-]*$/;\nconst VERSION = /^\\d+\\.\\d+\\.\\d+$/;\n/** The most a package may hold, so a stray folder can't fill memory. */\nconst MAX_BYTES = 64 * 1024 * 1024;\n\n/** An extension's package, read: its manifest, the manifest's problems, and its files by relative path. */\nexport interface ExtensionPackage {\n readonly manifest: Manifest;\n /** The manifest's problems; a package with an error among them won't install. */\n readonly problems: readonly Problem[];\n /** Every file, by its forward-slash path relative to the package root. No node_modules, no dotfiles. */\n readonly files: ReadonlyMap<string, Uint8Array>;\n}\n\nexport type PackageRead = ({ readonly ok: true } & ExtensionPackage) | { readonly ok: false; readonly why: string };\n\n/** One installed version of an extension. */\nexport interface InstalledExtension {\n readonly id: string;\n readonly version: string;\n readonly manifest: Manifest;\n /** The version's folder: `<dir>/<id>/<version>`. */\n readonly root: string;\n}\n\n/** A path inside a package is kept unless a segment is a dotfile or node_modules. */\nconst kept = (path: string): boolean => path.split(\"/\").every((s) => s !== \"node_modules\" && !s.startsWith(\".\"));\n/** A relative, forward-slash path with no empty, `.` or `..` segment. */\nconst safePath = (path: string): boolean =>\n path !== \"\" && !path.includes(\"\\\\\") && !path.includes(\"\\0\") && !path.startsWith(\"/\")\n && path.split(\"/\").every((s) => s !== \"\" && s !== \".\" && s !== \"..\");\n\n// --- Reading ---------------------------------------------------------------------\n\n/** Read a package from a folder or an `npm pack` .tgz, and find its one `<id>.extension.json` at the top level. */\nexport async function readExtensionPackage(path: string): Promise<PackageRead> {\n let stat;\n try { stat = await lstat(path); } catch { return { ok: false, why: `There's nothing at ${path}.` }; }\n let files: Map<string, Uint8Array> | string;\n if (stat.isDirectory()) files = await readFolder(path);\n else if (stat.isFile()) files = readTarball(await readFile(path));\n else return { ok: false, why: `${basename(path)} isn't a folder or an npm package tarball (.tgz).` };\n if (typeof files === \"string\") return { ok: false, why: files };\n return packageFromFiles(files);\n}\n\n/** A package from its files, as read: find the manifest, parse it, check it. */\nexport function packageFromFiles(files: Map<string, Uint8Array>): PackageRead {\n const manifests = [...files.keys()].filter((p) => !p.includes(\"/\") && p.endsWith(MANIFEST_FILE_SUFFIX));\n if (manifests.length === 0) {\n return { ok: false, why: `This package has no extension manifest (a file such as pattershot${MANIFEST_FILE_SUFFIX}) at its top level.` };\n }\n if (manifests.length > 1) {\n return { ok: false, why: `This package has more than one extension manifest at its top level: ${manifests.sort().join(\", \")}.` };\n }\n const file = manifests[0]!;\n let manifest: Manifest;\n try { manifest = parseManifest(new TextDecoder().decode(files.get(file)!)); } catch (e) {\n return { ok: false, why: `The manifest ${file} won't parse: ${(e as Error).message}` };\n }\n const problems = manifestProblems(manifest);\n const stem = file.slice(0, -MANIFEST_FILE_SUFFIX.length);\n if (typeof manifest === \"object\" && manifest !== null && typeof manifest.id === \"string\" && manifest.id !== stem) {\n return { ok: false, why: `The manifest ${file} names the extension \"${manifest.id}\"; its file name should match.` };\n }\n return { ok: true, manifest, problems, files };\n}\n\nasync function readFolder(root: string): Promise<Map<string, Uint8Array> | string> {\n const files = new Map<string, Uint8Array>();\n let total = 0;\n const walk = async (dir: string, rel: string): Promise<string | null> => {\n for (const entry of await readdir(dir, { withFileTypes: true })) {\n const path = rel === \"\" ? entry.name : `${rel}/${entry.name}`;\n if (!kept(path)) continue;\n // Links are skipped: a package is its own files, never somewhere else's.\n if (entry.isDirectory()) {\n const why = await walk(join(dir, entry.name), path);\n if (why) return why;\n } else if (entry.isFile()) {\n const bytes = await readFile(join(dir, entry.name));\n total += bytes.length;\n if (total > MAX_BYTES) return tooBig();\n files.set(path, bytes);\n }\n }\n return null;\n };\n const why = await walk(root, \"\");\n return why ?? files;\n}\n\nconst tooBig = (): string => `This package is bigger than ${MAX_BYTES / 1024 / 1024} MB, which is more than a view should need.`;\nconst notATarball = \"This file isn't a folder or an npm package tarball (.tgz).\";\n\n/** The files in a gzipped tar, with the one top folder every entry shares (`package/` from npm) taken off. */\nexport function readTarball(gz: Uint8Array): Map<string, Uint8Array> | string {\n let tar: Buffer;\n try { tar = gunzipSync(gz, { maxOutputLength: MAX_BYTES * 2 }); } catch { return notATarball; }\n const raw: [string, Uint8Array][] = [];\n let at = 0;\n let longName: string | null = null;\n let total = 0;\n const text = (from: number, len: number): string => {\n const slice = tar.subarray(from, from + len);\n const end = slice.indexOf(0);\n return slice.subarray(0, end < 0 ? slice.length : end).toString(\"utf8\");\n };\n while (at + 512 <= tar.length) {\n const header = tar.subarray(at, at + 512);\n if (header.every((b) => b === 0)) break;\n if (text(at + 257, 5) !== \"ustar\") return notATarball;\n const size = parseInt(text(at + 124, 12).trim() || \"0\", 8);\n if (!Number.isFinite(size) || size < 0) return notATarball;\n const type = String.fromCharCode(header[156]!);\n const prefix = text(at + 345, 155);\n const name = prefix ? `${prefix}/${text(at, 100)}` : text(at, 100);\n const body = tar.subarray(at + 512, at + 512 + size);\n if (body.length < size) return notATarball;\n at += 512 + Math.ceil(size / 512) * 512;\n if (type === \"x\") { longName = paxPath(body.toString(\"utf8\")) ?? longName; continue; }\n if (type === \"L\") { longName = body.toString(\"utf8\").replace(/\\0+$/, \"\"); continue; }\n if (type === \"g\") continue;\n const path = (longName ?? name).replace(/^\\.\\//, \"\");\n longName = null;\n // Regular files only: directories are implied, and links are never followed.\n if (type !== \"0\" && type !== \"\\0\") continue;\n if (!safePath(path)) return \"This package holds a file that would land outside it, so it won't be read.\";\n total += size;\n if (total > MAX_BYTES) return tooBig();\n raw.push([path, new Uint8Array(body)]);\n }\n const tops = new Set(raw.map(([p]) => p.split(\"/\")[0]));\n const strip = tops.size === 1 && raw.every(([p]) => p.includes(\"/\"));\n const files = new Map<string, Uint8Array>();\n for (const [p, bytes] of raw) {\n const path = strip ? p.slice(p.indexOf(\"/\") + 1) : p;\n if (kept(path)) files.set(path, bytes);\n }\n return files;\n}\n\n/** The `path` record of a pax extended header. */\nfunction paxPath(records: string): string | null {\n for (const line of records.split(\"\\n\")) {\n const m = /^\\d+ path=(.*)$/.exec(line);\n if (m) return m[1]!;\n }\n return null;\n}\n\n// --- Installing ----------------------------------------------------------------------\n\n/**\n * Copy a package's files into `<dir>/<id>/<version>/`, replacing the same version if it's there. Written to a\n * temporary folder first and renamed into place, so a failed install leaves what was there before.\n */\nexport async function installExtensionPackage(\n pkg: ExtensionPackage, dir: string,\n): Promise<{ readonly ok: true; readonly installed: InstalledExtension } | { readonly ok: false; readonly why: string }> {\n const { manifest } = pkg;\n const name = typeof manifest.name === \"string\" ? manifest.name : \"This extension\";\n const errors = pkg.problems.filter((p) => p.severity === \"error\");\n if (errors.length > 0 || !ID.test(manifest.id) || !VERSION.test(manifest.version)) {\n const first = errors[0];\n return { ok: false, why: `${name}'s manifest has a fault, so it won't install${first ? `: ${first.message}` : \"\"}.` };\n }\n const home = join(dir, manifest.id);\n const target = join(home, manifest.version);\n const stamp = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\n const temp = join(home, `.installing-${manifest.version}-${stamp}`);\n const old = join(home, `.replaced-${manifest.version}-${stamp}`);\n try {\n await mkdir(temp, { recursive: true });\n for (const [path, bytes] of pkg.files) {\n if (!safePath(path) || !kept(path)) continue;\n const to = join(temp, ...path.split(\"/\"));\n await mkdir(dirname(to), { recursive: true });\n await writeFile(to, bytes);\n }\n let replaced = false;\n try { await rename(target, old); replaced = true; } catch { /* nothing there yet */ }\n try { await rename(temp, target); } catch (e) {\n if (replaced) await rename(old, target).catch(() => {});\n throw e;\n }\n if (replaced) await rm(old, { recursive: true, force: true });\n } catch (e) {\n await rm(temp, { recursive: true, force: true }).catch(() => {});\n return { ok: false, why: `${name} couldn't be installed: ${(e as Error).message}` };\n }\n return { ok: true, installed: { id: manifest.id, version: manifest.version, manifest, root: target } };\n}\n\n/** Remove an installed extension: one version, or every version when none is named. */\nexport async function removeInstalledExtension(dir: string, id: string, version?: string): Promise<void> {\n if (!ID.test(id) || (version !== undefined && !VERSION.test(version))) return;\n await rm(version === undefined ? join(dir, id) : join(dir, id, version), { recursive: true, force: true });\n}\n\n/** Every installed version of every extension under `dir`, by id and then version, oldest first. */\nexport async function installedExtensions(dir: string): Promise<InstalledExtension[]> {\n const out: InstalledExtension[] = [];\n const names = async (path: string): Promise<string[]> => {\n try { return (await readdir(path, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name); } catch { return []; }\n };\n for (const id of (await names(dir)).filter((n) => ID.test(n)).sort()) {\n for (const version of (await names(join(dir, id))).filter((n) => VERSION.test(n))) {\n const root = join(dir, id, version);\n try {\n const manifest = parseManifest(await readFile(join(root, `${id}${MANIFEST_FILE_SUFFIX}`), \"utf8\"));\n if (manifest.id !== id || manifest.version !== version) continue;\n out.push({ id, version, manifest, root });\n } catch { /* a broken install is as good as none */ }\n }\n }\n return out.sort((a, b) => (a.id === b.id ? compareVersions(a.version, b.version) : a.id < b.id ? -1 : 1));\n}\n\n/** Compare two major.minor.patch versions. */\nexport function compareVersions(a: string, b: string): number {\n const pa = a.split(\".\").map(Number), pb = b.split(\".\").map(Number);\n for (let i = 0; i < 3; i++) if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) - (pb[i] ?? 0);\n return 0;\n}\n\n/** Which installed version a project gets: the one to open, with a warning to show, or none and why. */\nexport interface InstalledChoice {\n readonly installed: InstalledExtension | null;\n /** The installed version isn't the pinned one but shares its major version: it opens, with this shown. */\n readonly warning?: string;\n /** Why nothing opens. */\n readonly why?: string;\n}\n\n/**\n * Choose among installed versions for a project's pin: the pinned version exactly, else the highest with the\n * same major version (with a warning), else none. With no pin, the highest. `name` is the extension's name for\n * the sentences, when the project knows it (the id otherwise, or the installed manifest's name).\n */\nexport function chooseInstalled(\n installed: readonly InstalledExtension[], id: string, pinned?: string, name?: string,\n): InstalledChoice {\n const mine = installed.filter((x) => x.id === id).sort((a, b) => compareVersions(b.version, a.version));\n const called = name ?? mine[0]?.manifest.name ?? id;\n if (mine.length === 0) return { installed: null, why: `${called} isn't installed on this machine.` };\n if (pinned === undefined) return { installed: mine[0]! };\n const exact = mine.find((x) => x.version === pinned);\n if (exact) return { installed: exact };\n const major = pinned.split(\".\")[0];\n const same = mine.find((x) => x.version.split(\".\")[0] === major);\n if (same) return { installed: same, warning: `This project expects ${called} ${pinned}; ${same.version} is installed.` };\n return {\n installed: null,\n why: `This project expects ${called} ${pinned}; ${mine[0]!.version} is installed, and a different major version won't open.`,\n };\n}\n\n/** `chooseInstalled` over what's installed under `dir`. */\nexport async function resolveInstalled(dir: string, id: string, pinned?: string, name?: string): Promise<InstalledChoice> {\n return chooseInstalled(await installedExtensions(dir), id, pinned, name);\n}\n","// ---------------------------------------------------------------------------\n// The view host, MAIN-PROCESS half (tier 2 view extensions, patterkit\n// design/view-extensions.md): installed view packages, the view scheme, and\n// the view window, which main opens and then hands a MessageChannelMain pair\n// to, one port to the view and one to the editor's renderer. After that main\n// is out of the path: the editor's renderer answers the view (`hostView`).\n//\n// This module imports electron, so it ships as its own subpath\n// (@wildwinter/app-shell/view-host); never import it from renderer code. The\n// packages and the scheme are Electron-free modules of their own, re-exported\n// here, so they run under test as they are.\n// ---------------------------------------------------------------------------\n\nimport { BrowserWindow, MessageChannelMain, session } from \"electron\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { viewsFor } from \"@wildwinter/toolkit/extensions\";\nimport type { HostName } from \"@wildwinter/toolkit/extensions\";\nimport { openToolWindow } from \"./tool-window.js\";\nimport type { ToolWindowBounds, ToolWindowSize } from \"./tool-window.js\";\nimport { VIEW_PORT_CHANNEL } from \"./view-protocol.js\";\nimport type { ViewPortMeta } from \"./view-protocol.js\";\nimport { guardViewSession, serveViews, viewPageUrl } from \"./view-serve.js\";\nimport type { ViewSource } from \"./view-serve.js\";\nimport type { InstalledExtension } from \"./view-package.js\";\n\nexport {\n readExtensionPackage, packageFromFiles, readTarball, installExtensionPackage, removeInstalledExtension,\n installedExtensions, chooseInstalled, resolveInstalled, compareVersions,\n} from \"./view-package.js\";\nexport type { ExtensionPackage, PackageRead, InstalledExtension, InstalledChoice } from \"./view-package.js\";\nexport {\n VIEW_CSP, viewSchemePrivileges, serveViews, viewResponse, viewPage, viewPageUrl, viewContentType, confined,\n guardViewSession, viewRequestAllowed,\n} from \"./view-serve.js\";\nexport type { ServeViewsOptions, ViewSource, ProtocolLike, ViewSessionLike } from \"./view-serve.js\";\n\n/** Where the built view preload sits: beside this module in dist. An app that bundles its main process (so\n * this module's own path is the bundle's) passes `preload` to `openViewWindow` instead. */\nexport function viewPreloadPath(): string {\n const here = typeof __dirname === \"string\" ? __dirname : dirname(fileURLToPath(import.meta.url));\n return join(here, \"view-preload.cjs\");\n}\n\n/** The session partition a view runs in: in memory, one per extension. */\nexport const viewPartition = (extension: string): string => `view-${extension}`;\n\n/** The installed package each prepared partition serves, read at each request (a reinstall takes effect). */\nconst served = new Map<string, ViewSource>();\n\n/** Serve the scheme and guard the session of one extension's partition, once. Called by `openViewWindow`. */\nfunction prepareViewSession(extension: string, source: ViewSource, scheme: string, host: HostName): void {\n const partition = viewPartition(extension);\n const known = served.has(partition);\n served.set(partition, source);\n if (known) return;\n const ses = session.fromPartition(partition);\n serveViews(ses.protocol, { scheme, host, resolve: (id) => (id === extension ? served.get(partition) ?? null : null) });\n guardViewSession(ses, scheme);\n}\n\nexport interface ViewWindowOptions {\n /** The installed version to open (from `resolveInstalled`). */\n installed: InstalledExtension;\n /** The view's id in the manifest. */\n view: string;\n /** This app. */\n host: HostName;\n /** The view scheme, registered privileged at startup (`viewSchemePrivileges`). */\n scheme: string;\n /** The editor window whose renderer hosts the view (`onViewPort` + `hostView`). */\n editor: BrowserWindow;\n /** The version the project pins, if any: told to both ends. */\n pinnedVersion?: string;\n /** The window's title; the view's name by default (an app may add a version warning). */\n title?: string;\n rect: ToolWindowBounds;\n min?: ToolWindowSize;\n /** OS title bar by default: the view's page is the author's, so it has no shell head. */\n frame?: boolean;\n pinTo?: () => BrowserWindow | undefined;\n pinned?: boolean;\n remember?: (bounds: ToolWindowBounds) => void;\n onClosed?: () => void;\n /** Whether the view window may open devtools (default false). */\n devTools?: boolean;\n /** The view preload's path, when `viewPreloadPath()` can't find it (a bundled main process). */\n preload?: string;\n}\n\nexport interface ViewWindow {\n readonly window: BrowserWindow;\n readonly meta: ViewPortMeta;\n /** Close the view (the project changed, say). Closing the editor closes it too. */\n close(): void;\n}\n\n/**\n * Open a view in a sandboxed tool window of its own: its own in-memory partition serving only its package,\n * no network, no permissions, the view preload and nothing else. Each time the view's page loads, and each\n * time the editor's does while the view is open, both ends get a fresh `MessageChannelMain` pair on\n * `VIEW_PORT_CHANNEL`, the editor's with the `ViewPortMeta`. Throws when the manifest offers no such view here.\n */\nexport function openViewWindow(opts: ViewWindowOptions): ViewWindow {\n const { installed, editor } = opts;\n const decl = viewsFor(installed.manifest, opts.host).find((v) => v.id === opts.view);\n if (!decl) throw new Error(`${installed.manifest.name} has no view called \"${opts.view}\" in this app.`);\n prepareViewSession(installed.id, { root: installed.root, manifest: installed.manifest }, opts.scheme, opts.host);\n\n const w = openToolWindow(null, {\n title: opts.title ?? decl.name,\n url: viewPageUrl(opts.scheme, installed.id, decl.id),\n preload: opts.preload ?? viewPreloadPath(),\n partition: viewPartition(installed.id),\n devTools: opts.devTools ?? false,\n rect: opts.rect,\n frame: opts.frame ?? true,\n ...(opts.min !== undefined ? { min: opts.min } : {}),\n ...(opts.pinTo !== undefined ? { pinTo: opts.pinTo } : {}),\n ...(opts.pinned !== undefined ? { pinned: opts.pinned } : {}),\n ...(opts.remember !== undefined ? { remember: opts.remember } : {}),\n ...(opts.onClosed !== undefined ? { onClosed: opts.onClosed } : {}),\n });\n const meta: ViewPortMeta = {\n window: w.id, extension: installed.id, view: decl.id, version: installed.version,\n ...(opts.pinnedVersion !== undefined ? { pinned: opts.pinnedVersion } : {}),\n };\n\n let viewLoaded = false;\n const handOver = (): void => {\n if (w.isDestroyed() || editor.isDestroyed()) return;\n const { port1, port2 } = new MessageChannelMain();\n w.webContents.postMessage(VIEW_PORT_CHANNEL, null, [port1]);\n editor.webContents.postMessage(VIEW_PORT_CHANNEL, meta, [port2]);\n };\n const onViewLoad = (): void => { viewLoaded = true; handOver(); };\n // The editor reloaded: its old port went with its page, so the view needs a new one too.\n const onEditorLoad = (): void => { if (viewLoaded) handOver(); };\n const close = (): void => { if (!w.isDestroyed()) w.close(); };\n w.webContents.on(\"did-finish-load\", onViewLoad);\n editor.webContents.on(\"did-finish-load\", onEditorLoad);\n editor.once(\"closed\", close);\n w.once(\"closed\", () => {\n if (!editor.isDestroyed()) {\n // Tell the editor the view has gone, the same way its port came: meta with `closed`, and no port.\n editor.webContents.postMessage(VIEW_PORT_CHANNEL, { ...meta, closed: true }, []);\n editor.webContents.removeListener(\"did-finish-load\", onEditorLoad);\n editor.removeListener(\"closed\", close);\n }\n });\n return { window: w, meta, close };\n}\n"]}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { BrowserWindow } from 'electron';
|
|
2
|
+
import { Manifest, Problem, HostName, ViewDecl } from '@wildwinter/toolkit/extensions';
|
|
3
|
+
import { ToolWindowBounds, ToolWindowSize } from './tool-window.cjs';
|
|
4
|
+
import { ViewPortMeta } from './view-protocol.cjs';
|
|
5
|
+
import './session.cjs';
|
|
6
|
+
import './app-store.cjs';
|
|
7
|
+
|
|
8
|
+
/** An extension's package, read: its manifest, the manifest's problems, and its files by relative path. */
|
|
9
|
+
interface ExtensionPackage {
|
|
10
|
+
readonly manifest: Manifest;
|
|
11
|
+
/** The manifest's problems; a package with an error among them won't install. */
|
|
12
|
+
readonly problems: readonly Problem[];
|
|
13
|
+
/** Every file, by its forward-slash path relative to the package root. No node_modules, no dotfiles. */
|
|
14
|
+
readonly files: ReadonlyMap<string, Uint8Array>;
|
|
15
|
+
}
|
|
16
|
+
type PackageRead = ({
|
|
17
|
+
readonly ok: true;
|
|
18
|
+
} & ExtensionPackage) | {
|
|
19
|
+
readonly ok: false;
|
|
20
|
+
readonly why: string;
|
|
21
|
+
};
|
|
22
|
+
/** One installed version of an extension. */
|
|
23
|
+
interface InstalledExtension {
|
|
24
|
+
readonly id: string;
|
|
25
|
+
readonly version: string;
|
|
26
|
+
readonly manifest: Manifest;
|
|
27
|
+
/** The version's folder: `<dir>/<id>/<version>`. */
|
|
28
|
+
readonly root: string;
|
|
29
|
+
}
|
|
30
|
+
/** Read a package from a folder or an `npm pack` .tgz, and find its one `<id>.extension.json` at the top level. */
|
|
31
|
+
declare function readExtensionPackage(path: string): Promise<PackageRead>;
|
|
32
|
+
/** A package from its files, as read: find the manifest, parse it, check it. */
|
|
33
|
+
declare function packageFromFiles(files: Map<string, Uint8Array>): PackageRead;
|
|
34
|
+
/** The files in a gzipped tar, with the one top folder every entry shares (`package/` from npm) taken off. */
|
|
35
|
+
declare function readTarball(gz: Uint8Array): Map<string, Uint8Array> | string;
|
|
36
|
+
/**
|
|
37
|
+
* Copy a package's files into `<dir>/<id>/<version>/`, replacing the same version if it's there. Written to a
|
|
38
|
+
* temporary folder first and renamed into place, so a failed install leaves what was there before.
|
|
39
|
+
*/
|
|
40
|
+
declare function installExtensionPackage(pkg: ExtensionPackage, dir: string): Promise<{
|
|
41
|
+
readonly ok: true;
|
|
42
|
+
readonly installed: InstalledExtension;
|
|
43
|
+
} | {
|
|
44
|
+
readonly ok: false;
|
|
45
|
+
readonly why: string;
|
|
46
|
+
}>;
|
|
47
|
+
/** Remove an installed extension: one version, or every version when none is named. */
|
|
48
|
+
declare function removeInstalledExtension(dir: string, id: string, version?: string): Promise<void>;
|
|
49
|
+
/** Every installed version of every extension under `dir`, by id and then version, oldest first. */
|
|
50
|
+
declare function installedExtensions(dir: string): Promise<InstalledExtension[]>;
|
|
51
|
+
/** Compare two major.minor.patch versions. */
|
|
52
|
+
declare function compareVersions(a: string, b: string): number;
|
|
53
|
+
/** Which installed version a project gets: the one to open, with a warning to show, or none and why. */
|
|
54
|
+
interface InstalledChoice {
|
|
55
|
+
readonly installed: InstalledExtension | null;
|
|
56
|
+
/** The installed version isn't the pinned one but shares its major version: it opens, with this shown. */
|
|
57
|
+
readonly warning?: string;
|
|
58
|
+
/** Why nothing opens. */
|
|
59
|
+
readonly why?: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Choose among installed versions for a project's pin: the pinned version exactly, else the highest with the
|
|
63
|
+
* same major version (with a warning), else none. With no pin, the highest. `name` is the extension's name for
|
|
64
|
+
* the sentences, when the project knows it (the id otherwise, or the installed manifest's name).
|
|
65
|
+
*/
|
|
66
|
+
declare function chooseInstalled(installed: readonly InstalledExtension[], id: string, pinned?: string, name?: string): InstalledChoice;
|
|
67
|
+
/** `chooseInstalled` over what's installed under `dir`. */
|
|
68
|
+
declare function resolveInstalled(dir: string, id: string, pinned?: string, name?: string): Promise<InstalledChoice>;
|
|
69
|
+
|
|
70
|
+
/** The view CSP: the package's own scripts and styles, inline styles, images and fonts from the package or data
|
|
71
|
+
* URLs, and no network, frames, forms, or plugins. Sent as a header on every response. */
|
|
72
|
+
declare const VIEW_CSP: string;
|
|
73
|
+
/** The entry an app adds to its ONE `protocol.registerSchemesAsPrivileged` call (Electron keeps only the last
|
|
74
|
+
* call, so an app with other schemes lists them all there). Never `bypassCSP`. */
|
|
75
|
+
declare function viewSchemePrivileges(scheme: string): {
|
|
76
|
+
scheme: string;
|
|
77
|
+
privileges: {
|
|
78
|
+
standard: true;
|
|
79
|
+
secure: true;
|
|
80
|
+
supportFetchAPI: true;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
/** What the scheme serves for one extension id: the installed version's folder and its manifest. */
|
|
84
|
+
interface ViewSource {
|
|
85
|
+
readonly root: string;
|
|
86
|
+
readonly manifest: Manifest;
|
|
87
|
+
}
|
|
88
|
+
interface ServeViewsOptions {
|
|
89
|
+
/** The scheme, without its colon: `patter-view`. */
|
|
90
|
+
scheme: string;
|
|
91
|
+
/** The app the views open in: a view the manifest doesn't offer this host isn't served. */
|
|
92
|
+
host: HostName;
|
|
93
|
+
/** The installed package for an extension id, or null for none. Read at each request. */
|
|
94
|
+
resolve: (extension: string) => ViewSource | null | Promise<ViewSource | null>;
|
|
95
|
+
}
|
|
96
|
+
/** The URL of a view's page. */
|
|
97
|
+
declare function viewPageUrl(scheme: string, extension: string, view: string): string;
|
|
98
|
+
/** The content type a package file is served with, by its extension. */
|
|
99
|
+
declare function viewContentType(path: string): string;
|
|
100
|
+
/** The page that hosts a view: its entry as a module script and its element filling the window. */
|
|
101
|
+
declare function viewPage(view: Pick<ViewDecl, "name" | "entry" | "element">): string;
|
|
102
|
+
/** The response for one request on the view scheme. */
|
|
103
|
+
declare function viewResponse(url: string, opts: ServeViewsOptions): Promise<Response>;
|
|
104
|
+
/** The file a URL path names inside `root`, or null when it would land anywhere else (or isn't there). */
|
|
105
|
+
declare function confined(root: string, pathname: string): Promise<string | null>;
|
|
106
|
+
/** The part of Electron's `Protocol` the scheme needs. */
|
|
107
|
+
interface ProtocolLike {
|
|
108
|
+
handle(scheme: string, handler: (request: Request) => Response | Promise<Response>): void;
|
|
109
|
+
}
|
|
110
|
+
/** Serve the view scheme on a protocol: the view session's own (`session.fromPartition(...).protocol`), since
|
|
111
|
+
* views run in a partition of their own. */
|
|
112
|
+
declare function serveViews(protocol: ProtocolLike, opts: ServeViewsOptions): void;
|
|
113
|
+
/** The part of Electron's `Session` the guard needs. */
|
|
114
|
+
interface ViewSessionLike {
|
|
115
|
+
webRequest: {
|
|
116
|
+
onBeforeRequest(listener: (details: {
|
|
117
|
+
url: string;
|
|
118
|
+
}, callback: (response: {
|
|
119
|
+
cancel?: boolean;
|
|
120
|
+
}) => void) => void): void;
|
|
121
|
+
};
|
|
122
|
+
setPermissionRequestHandler(handler: ((wc: unknown, permission: string, callback: (granted: boolean) => void) => void) | null): void;
|
|
123
|
+
setPermissionCheckHandler?(handler: ((...args: unknown[]) => boolean) | null): void;
|
|
124
|
+
}
|
|
125
|
+
/** Whether a view session may load a URL: its own scheme, devtools, and inline data only. */
|
|
126
|
+
declare function viewRequestAllowed(url: string, scheme: string): boolean;
|
|
127
|
+
/** No network and no permissions for a view's session: anything off the view scheme is cancelled, and every
|
|
128
|
+
* permission (camera, notifications, clipboard, ...) is refused. */
|
|
129
|
+
declare function guardViewSession(session: ViewSessionLike, scheme: string): void;
|
|
130
|
+
|
|
131
|
+
/** Where the built view preload sits: beside this module in dist. An app that bundles its main process (so
|
|
132
|
+
* this module's own path is the bundle's) passes `preload` to `openViewWindow` instead. */
|
|
133
|
+
declare function viewPreloadPath(): string;
|
|
134
|
+
/** The session partition a view runs in: in memory, one per extension. */
|
|
135
|
+
declare const viewPartition: (extension: string) => string;
|
|
136
|
+
interface ViewWindowOptions {
|
|
137
|
+
/** The installed version to open (from `resolveInstalled`). */
|
|
138
|
+
installed: InstalledExtension;
|
|
139
|
+
/** The view's id in the manifest. */
|
|
140
|
+
view: string;
|
|
141
|
+
/** This app. */
|
|
142
|
+
host: HostName;
|
|
143
|
+
/** The view scheme, registered privileged at startup (`viewSchemePrivileges`). */
|
|
144
|
+
scheme: string;
|
|
145
|
+
/** The editor window whose renderer hosts the view (`onViewPort` + `hostView`). */
|
|
146
|
+
editor: BrowserWindow;
|
|
147
|
+
/** The version the project pins, if any: told to both ends. */
|
|
148
|
+
pinnedVersion?: string;
|
|
149
|
+
/** The window's title; the view's name by default (an app may add a version warning). */
|
|
150
|
+
title?: string;
|
|
151
|
+
rect: ToolWindowBounds;
|
|
152
|
+
min?: ToolWindowSize;
|
|
153
|
+
/** OS title bar by default: the view's page is the author's, so it has no shell head. */
|
|
154
|
+
frame?: boolean;
|
|
155
|
+
pinTo?: () => BrowserWindow | undefined;
|
|
156
|
+
pinned?: boolean;
|
|
157
|
+
remember?: (bounds: ToolWindowBounds) => void;
|
|
158
|
+
onClosed?: () => void;
|
|
159
|
+
/** Whether the view window may open devtools (default false). */
|
|
160
|
+
devTools?: boolean;
|
|
161
|
+
/** The view preload's path, when `viewPreloadPath()` can't find it (a bundled main process). */
|
|
162
|
+
preload?: string;
|
|
163
|
+
}
|
|
164
|
+
interface ViewWindow {
|
|
165
|
+
readonly window: BrowserWindow;
|
|
166
|
+
readonly meta: ViewPortMeta;
|
|
167
|
+
/** Close the view (the project changed, say). Closing the editor closes it too. */
|
|
168
|
+
close(): void;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Open a view in a sandboxed tool window of its own: its own in-memory partition serving only its package,
|
|
172
|
+
* no network, no permissions, the view preload and nothing else. Each time the view's page loads, and each
|
|
173
|
+
* time the editor's does while the view is open, both ends get a fresh `MessageChannelMain` pair on
|
|
174
|
+
* `VIEW_PORT_CHANNEL`, the editor's with the `ViewPortMeta`. Throws when the manifest offers no such view here.
|
|
175
|
+
*/
|
|
176
|
+
declare function openViewWindow(opts: ViewWindowOptions): ViewWindow;
|
|
177
|
+
|
|
178
|
+
export { type ExtensionPackage, type InstalledChoice, type InstalledExtension, type PackageRead, type ProtocolLike, type ServeViewsOptions, VIEW_CSP, type ViewSessionLike, type ViewSource, type ViewWindow, type ViewWindowOptions, chooseInstalled, compareVersions, confined, guardViewSession, installExtensionPackage, installedExtensions, openViewWindow, packageFromFiles, readExtensionPackage, readTarball, removeInstalledExtension, resolveInstalled, serveViews, viewContentType, viewPage, viewPageUrl, viewPartition, viewPreloadPath, viewRequestAllowed, viewResponse, viewSchemePrivileges };
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { BrowserWindow } from 'electron';
|
|
2
|
+
import { Manifest, Problem, HostName, ViewDecl } from '@wildwinter/toolkit/extensions';
|
|
3
|
+
import { ToolWindowBounds, ToolWindowSize } from './tool-window.js';
|
|
4
|
+
import { ViewPortMeta } from './view-protocol.js';
|
|
5
|
+
import './session.js';
|
|
6
|
+
import './app-store.js';
|
|
7
|
+
|
|
8
|
+
/** An extension's package, read: its manifest, the manifest's problems, and its files by relative path. */
|
|
9
|
+
interface ExtensionPackage {
|
|
10
|
+
readonly manifest: Manifest;
|
|
11
|
+
/** The manifest's problems; a package with an error among them won't install. */
|
|
12
|
+
readonly problems: readonly Problem[];
|
|
13
|
+
/** Every file, by its forward-slash path relative to the package root. No node_modules, no dotfiles. */
|
|
14
|
+
readonly files: ReadonlyMap<string, Uint8Array>;
|
|
15
|
+
}
|
|
16
|
+
type PackageRead = ({
|
|
17
|
+
readonly ok: true;
|
|
18
|
+
} & ExtensionPackage) | {
|
|
19
|
+
readonly ok: false;
|
|
20
|
+
readonly why: string;
|
|
21
|
+
};
|
|
22
|
+
/** One installed version of an extension. */
|
|
23
|
+
interface InstalledExtension {
|
|
24
|
+
readonly id: string;
|
|
25
|
+
readonly version: string;
|
|
26
|
+
readonly manifest: Manifest;
|
|
27
|
+
/** The version's folder: `<dir>/<id>/<version>`. */
|
|
28
|
+
readonly root: string;
|
|
29
|
+
}
|
|
30
|
+
/** Read a package from a folder or an `npm pack` .tgz, and find its one `<id>.extension.json` at the top level. */
|
|
31
|
+
declare function readExtensionPackage(path: string): Promise<PackageRead>;
|
|
32
|
+
/** A package from its files, as read: find the manifest, parse it, check it. */
|
|
33
|
+
declare function packageFromFiles(files: Map<string, Uint8Array>): PackageRead;
|
|
34
|
+
/** The files in a gzipped tar, with the one top folder every entry shares (`package/` from npm) taken off. */
|
|
35
|
+
declare function readTarball(gz: Uint8Array): Map<string, Uint8Array> | string;
|
|
36
|
+
/**
|
|
37
|
+
* Copy a package's files into `<dir>/<id>/<version>/`, replacing the same version if it's there. Written to a
|
|
38
|
+
* temporary folder first and renamed into place, so a failed install leaves what was there before.
|
|
39
|
+
*/
|
|
40
|
+
declare function installExtensionPackage(pkg: ExtensionPackage, dir: string): Promise<{
|
|
41
|
+
readonly ok: true;
|
|
42
|
+
readonly installed: InstalledExtension;
|
|
43
|
+
} | {
|
|
44
|
+
readonly ok: false;
|
|
45
|
+
readonly why: string;
|
|
46
|
+
}>;
|
|
47
|
+
/** Remove an installed extension: one version, or every version when none is named. */
|
|
48
|
+
declare function removeInstalledExtension(dir: string, id: string, version?: string): Promise<void>;
|
|
49
|
+
/** Every installed version of every extension under `dir`, by id and then version, oldest first. */
|
|
50
|
+
declare function installedExtensions(dir: string): Promise<InstalledExtension[]>;
|
|
51
|
+
/** Compare two major.minor.patch versions. */
|
|
52
|
+
declare function compareVersions(a: string, b: string): number;
|
|
53
|
+
/** Which installed version a project gets: the one to open, with a warning to show, or none and why. */
|
|
54
|
+
interface InstalledChoice {
|
|
55
|
+
readonly installed: InstalledExtension | null;
|
|
56
|
+
/** The installed version isn't the pinned one but shares its major version: it opens, with this shown. */
|
|
57
|
+
readonly warning?: string;
|
|
58
|
+
/** Why nothing opens. */
|
|
59
|
+
readonly why?: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Choose among installed versions for a project's pin: the pinned version exactly, else the highest with the
|
|
63
|
+
* same major version (with a warning), else none. With no pin, the highest. `name` is the extension's name for
|
|
64
|
+
* the sentences, when the project knows it (the id otherwise, or the installed manifest's name).
|
|
65
|
+
*/
|
|
66
|
+
declare function chooseInstalled(installed: readonly InstalledExtension[], id: string, pinned?: string, name?: string): InstalledChoice;
|
|
67
|
+
/** `chooseInstalled` over what's installed under `dir`. */
|
|
68
|
+
declare function resolveInstalled(dir: string, id: string, pinned?: string, name?: string): Promise<InstalledChoice>;
|
|
69
|
+
|
|
70
|
+
/** The view CSP: the package's own scripts and styles, inline styles, images and fonts from the package or data
|
|
71
|
+
* URLs, and no network, frames, forms, or plugins. Sent as a header on every response. */
|
|
72
|
+
declare const VIEW_CSP: string;
|
|
73
|
+
/** The entry an app adds to its ONE `protocol.registerSchemesAsPrivileged` call (Electron keeps only the last
|
|
74
|
+
* call, so an app with other schemes lists them all there). Never `bypassCSP`. */
|
|
75
|
+
declare function viewSchemePrivileges(scheme: string): {
|
|
76
|
+
scheme: string;
|
|
77
|
+
privileges: {
|
|
78
|
+
standard: true;
|
|
79
|
+
secure: true;
|
|
80
|
+
supportFetchAPI: true;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
/** What the scheme serves for one extension id: the installed version's folder and its manifest. */
|
|
84
|
+
interface ViewSource {
|
|
85
|
+
readonly root: string;
|
|
86
|
+
readonly manifest: Manifest;
|
|
87
|
+
}
|
|
88
|
+
interface ServeViewsOptions {
|
|
89
|
+
/** The scheme, without its colon: `patter-view`. */
|
|
90
|
+
scheme: string;
|
|
91
|
+
/** The app the views open in: a view the manifest doesn't offer this host isn't served. */
|
|
92
|
+
host: HostName;
|
|
93
|
+
/** The installed package for an extension id, or null for none. Read at each request. */
|
|
94
|
+
resolve: (extension: string) => ViewSource | null | Promise<ViewSource | null>;
|
|
95
|
+
}
|
|
96
|
+
/** The URL of a view's page. */
|
|
97
|
+
declare function viewPageUrl(scheme: string, extension: string, view: string): string;
|
|
98
|
+
/** The content type a package file is served with, by its extension. */
|
|
99
|
+
declare function viewContentType(path: string): string;
|
|
100
|
+
/** The page that hosts a view: its entry as a module script and its element filling the window. */
|
|
101
|
+
declare function viewPage(view: Pick<ViewDecl, "name" | "entry" | "element">): string;
|
|
102
|
+
/** The response for one request on the view scheme. */
|
|
103
|
+
declare function viewResponse(url: string, opts: ServeViewsOptions): Promise<Response>;
|
|
104
|
+
/** The file a URL path names inside `root`, or null when it would land anywhere else (or isn't there). */
|
|
105
|
+
declare function confined(root: string, pathname: string): Promise<string | null>;
|
|
106
|
+
/** The part of Electron's `Protocol` the scheme needs. */
|
|
107
|
+
interface ProtocolLike {
|
|
108
|
+
handle(scheme: string, handler: (request: Request) => Response | Promise<Response>): void;
|
|
109
|
+
}
|
|
110
|
+
/** Serve the view scheme on a protocol: the view session's own (`session.fromPartition(...).protocol`), since
|
|
111
|
+
* views run in a partition of their own. */
|
|
112
|
+
declare function serveViews(protocol: ProtocolLike, opts: ServeViewsOptions): void;
|
|
113
|
+
/** The part of Electron's `Session` the guard needs. */
|
|
114
|
+
interface ViewSessionLike {
|
|
115
|
+
webRequest: {
|
|
116
|
+
onBeforeRequest(listener: (details: {
|
|
117
|
+
url: string;
|
|
118
|
+
}, callback: (response: {
|
|
119
|
+
cancel?: boolean;
|
|
120
|
+
}) => void) => void): void;
|
|
121
|
+
};
|
|
122
|
+
setPermissionRequestHandler(handler: ((wc: unknown, permission: string, callback: (granted: boolean) => void) => void) | null): void;
|
|
123
|
+
setPermissionCheckHandler?(handler: ((...args: unknown[]) => boolean) | null): void;
|
|
124
|
+
}
|
|
125
|
+
/** Whether a view session may load a URL: its own scheme, devtools, and inline data only. */
|
|
126
|
+
declare function viewRequestAllowed(url: string, scheme: string): boolean;
|
|
127
|
+
/** No network and no permissions for a view's session: anything off the view scheme is cancelled, and every
|
|
128
|
+
* permission (camera, notifications, clipboard, ...) is refused. */
|
|
129
|
+
declare function guardViewSession(session: ViewSessionLike, scheme: string): void;
|
|
130
|
+
|
|
131
|
+
/** Where the built view preload sits: beside this module in dist. An app that bundles its main process (so
|
|
132
|
+
* this module's own path is the bundle's) passes `preload` to `openViewWindow` instead. */
|
|
133
|
+
declare function viewPreloadPath(): string;
|
|
134
|
+
/** The session partition a view runs in: in memory, one per extension. */
|
|
135
|
+
declare const viewPartition: (extension: string) => string;
|
|
136
|
+
interface ViewWindowOptions {
|
|
137
|
+
/** The installed version to open (from `resolveInstalled`). */
|
|
138
|
+
installed: InstalledExtension;
|
|
139
|
+
/** The view's id in the manifest. */
|
|
140
|
+
view: string;
|
|
141
|
+
/** This app. */
|
|
142
|
+
host: HostName;
|
|
143
|
+
/** The view scheme, registered privileged at startup (`viewSchemePrivileges`). */
|
|
144
|
+
scheme: string;
|
|
145
|
+
/** The editor window whose renderer hosts the view (`onViewPort` + `hostView`). */
|
|
146
|
+
editor: BrowserWindow;
|
|
147
|
+
/** The version the project pins, if any: told to both ends. */
|
|
148
|
+
pinnedVersion?: string;
|
|
149
|
+
/** The window's title; the view's name by default (an app may add a version warning). */
|
|
150
|
+
title?: string;
|
|
151
|
+
rect: ToolWindowBounds;
|
|
152
|
+
min?: ToolWindowSize;
|
|
153
|
+
/** OS title bar by default: the view's page is the author's, so it has no shell head. */
|
|
154
|
+
frame?: boolean;
|
|
155
|
+
pinTo?: () => BrowserWindow | undefined;
|
|
156
|
+
pinned?: boolean;
|
|
157
|
+
remember?: (bounds: ToolWindowBounds) => void;
|
|
158
|
+
onClosed?: () => void;
|
|
159
|
+
/** Whether the view window may open devtools (default false). */
|
|
160
|
+
devTools?: boolean;
|
|
161
|
+
/** The view preload's path, when `viewPreloadPath()` can't find it (a bundled main process). */
|
|
162
|
+
preload?: string;
|
|
163
|
+
}
|
|
164
|
+
interface ViewWindow {
|
|
165
|
+
readonly window: BrowserWindow;
|
|
166
|
+
readonly meta: ViewPortMeta;
|
|
167
|
+
/** Close the view (the project changed, say). Closing the editor closes it too. */
|
|
168
|
+
close(): void;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Open a view in a sandboxed tool window of its own: its own in-memory partition serving only its package,
|
|
172
|
+
* no network, no permissions, the view preload and nothing else. Each time the view's page loads, and each
|
|
173
|
+
* time the editor's does while the view is open, both ends get a fresh `MessageChannelMain` pair on
|
|
174
|
+
* `VIEW_PORT_CHANNEL`, the editor's with the `ViewPortMeta`. Throws when the manifest offers no such view here.
|
|
175
|
+
*/
|
|
176
|
+
declare function openViewWindow(opts: ViewWindowOptions): ViewWindow;
|
|
177
|
+
|
|
178
|
+
export { type ExtensionPackage, type InstalledChoice, type InstalledExtension, type PackageRead, type ProtocolLike, type ServeViewsOptions, VIEW_CSP, type ViewSessionLike, type ViewSource, type ViewWindow, type ViewWindowOptions, chooseInstalled, compareVersions, confined, guardViewSession, installExtensionPackage, installedExtensions, openViewWindow, packageFromFiles, readExtensionPackage, readTarball, removeInstalledExtension, resolveInstalled, serveViews, viewContentType, viewPage, viewPageUrl, viewPartition, viewPreloadPath, viewRequestAllowed, viewResponse, viewSchemePrivileges };
|