extrojs 0.1.0 → 0.3.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 +3 -3
- package/client.d.ts +8 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +27 -0
- package/dist/commands/build.d.ts +5 -0
- package/dist/commands/build.js +26 -0
- package/dist/commands/dev.d.ts +7 -0
- package/dist/commands/dev.js +156 -0
- package/dist/config.d.ts +2 -2
- package/dist/core/asset.d.ts +10 -0
- package/dist/core/asset.js +10 -0
- package/dist/dev-assets.d.ts +3 -3
- package/dist/dev-assets.js +33 -12
- package/dist/env.d.ts +10 -0
- package/dist/env.js +18 -0
- package/dist/exports/asset.d.ts +1 -0
- package/dist/exports/asset.js +1 -0
- package/dist/exports/link.d.ts +1 -0
- package/dist/exports/link.js +1 -0
- package/dist/exports/navigation.d.ts +2 -0
- package/dist/exports/navigation.js +1 -0
- package/dist/exports/runtime.d.ts +2 -0
- package/dist/exports/runtime.js +3 -0
- package/dist/index.js +5 -136
- package/dist/load-config.d.ts +1 -1
- package/dist/logger.d.ts +34 -0
- package/dist/logger.js +65 -0
- package/dist/paths.d.ts +8 -0
- package/dist/paths.js +8 -0
- package/dist/pkg.d.ts +6 -0
- package/dist/pkg.js +5 -0
- package/dist/plugin/app-tree.d.ts +59 -0
- package/dist/plugin/app-tree.js +214 -0
- package/dist/plugin/asset-inventory.d.ts +24 -0
- package/dist/plugin/asset-inventory.js +9 -0
- package/dist/plugin/dev-reactions.d.ts +59 -0
- package/dist/plugin/dev-reactions.js +62 -0
- package/dist/plugin/emit-assets.d.ts +50 -0
- package/dist/plugin/emit-assets.js +40 -0
- package/dist/plugin/generators/html.d.ts +39 -0
- package/dist/plugin/generators/html.js +127 -0
- package/dist/plugin/generators/icons.d.ts +15 -0
- package/dist/plugin/generators/icons.js +16 -0
- package/dist/plugin/generators/public.d.ts +17 -0
- package/dist/plugin/generators/public.js +20 -0
- package/dist/plugin/icons.d.ts +5 -0
- package/dist/plugin/icons.js +20 -0
- package/dist/plugin/index.d.ts +31 -0
- package/dist/plugin/index.js +246 -0
- package/dist/plugin/internal.d.ts +14 -0
- package/dist/plugin/internal.js +6 -0
- package/dist/plugin/manifest.d.ts +29 -0
- package/dist/plugin/manifest.js +68 -0
- package/dist/plugin/public.d.ts +21 -0
- package/dist/plugin/public.js +63 -0
- package/dist/plugin/runtimes/clients/csui-mount.js +90 -0
- package/dist/plugin/runtimes/clients/dev-bridge.js +194 -0
- package/dist/plugin/runtimes/csui-mount.d.ts +18 -0
- package/dist/plugin/runtimes/csui-mount.js +21 -0
- package/dist/plugin/runtimes/dev-bridge.d.ts +22 -0
- package/dist/plugin/runtimes/dev-bridge.js +19 -0
- package/dist/plugin/runtimes/routes-module.d.ts +20 -0
- package/dist/plugin/runtimes/routes-module.js +51 -0
- package/dist/plugin/runtimes/runtime-module.d.ts +16 -0
- package/dist/plugin/runtimes/runtime-module.js +40 -0
- package/dist/plugin/surfaces.d.ts +37 -0
- package/dist/plugin/surfaces.js +67 -0
- package/dist/plugin/types/index.d.ts +9 -0
- package/dist/plugin/types/index.js +1 -0
- package/dist/plugin/utils/read-json.d.ts +1 -0
- package/dist/plugin/utils/read-json.js +8 -0
- package/dist/react/env.d.ts +13 -0
- package/dist/react/env.js +1 -0
- package/dist/router/build-tree.d.ts +46 -0
- package/dist/router/build-tree.js +56 -0
- package/dist/router/context.d.ts +13 -0
- package/dist/router/context.js +2 -0
- package/dist/router/create-router.d.ts +10 -0
- package/dist/router/create-router.js +126 -0
- package/dist/router/defaults.d.ts +24 -0
- package/dist/router/defaults.js +25 -0
- package/dist/router/error-boundary.d.ts +23 -0
- package/dist/router/error-boundary.js +21 -0
- package/dist/router/hooks.d.ts +18 -0
- package/dist/router/hooks.js +34 -0
- package/dist/router/index.d.ts +8 -0
- package/dist/router/index.js +7 -0
- package/dist/router/link.d.ts +305 -0
- package/dist/router/link.js +30 -0
- package/dist/router/match.d.ts +14 -0
- package/dist/router/match.js +27 -0
- package/dist/router/types.d.ts +55 -0
- package/dist/router/types.js +1 -0
- package/dist/types/index.d.ts +152 -0
- package/dist/types/index.js +1 -0
- package/package.json +47 -9
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @describe Generates the HTML shell for a surface. In dev mode, the shell
|
|
3
|
+
* points at the Vite dev server (with @vite/client for HMR) instead of the
|
|
4
|
+
* built bundle, and pre-renders a brand-styled "dev server offline" screen
|
|
5
|
+
* inside #root. React's createRoot replaces #root's children on mount, so
|
|
6
|
+
* the screen vanishes on a normal start; if the dev server never comes up,
|
|
7
|
+
* the screen stays visible.
|
|
8
|
+
*/
|
|
9
|
+
export function generateHTML({ surface, dev }) {
|
|
10
|
+
const title = surface.charAt(0).toUpperCase() + surface.slice(1);
|
|
11
|
+
const scripts = dev
|
|
12
|
+
? `
|
|
13
|
+
<script type="module" src="http://localhost:${dev.port}/@vite/client"></script>
|
|
14
|
+
<script type="module" src="http://localhost:${dev.port}/@id/@vitejs/plugin-react/preamble"></script>
|
|
15
|
+
<script type="module" src="http://localhost:${dev.port}/@id/virtual:extro/runtime/${surface}"></script>
|
|
16
|
+
`
|
|
17
|
+
: `<script type="module" src="./${surface}.js"></script>`;
|
|
18
|
+
const rootContent = dev
|
|
19
|
+
? renderDevScreen({
|
|
20
|
+
title: "Dev server isn't running",
|
|
21
|
+
body: `
|
|
22
|
+
<p>Start it with <code>extro dev</code>, then reopen this surface.</p>
|
|
23
|
+
<p>Expected at <code>http://localhost:${dev.port}</code>.</p>
|
|
24
|
+
`,
|
|
25
|
+
// Popups size to content (no viewport to fill); everything else
|
|
26
|
+
// (options/sidepanel/tab) gets a full-viewport centered layout.
|
|
27
|
+
fill: surface !== "popup",
|
|
28
|
+
})
|
|
29
|
+
: "";
|
|
30
|
+
return `
|
|
31
|
+
<!doctype html>
|
|
32
|
+
<html>
|
|
33
|
+
<head>
|
|
34
|
+
<title>${title}</title>
|
|
35
|
+
${dev ? devScreenStyles() : ""}
|
|
36
|
+
</head>
|
|
37
|
+
<body>
|
|
38
|
+
<div id="root">${rootContent}</div>
|
|
39
|
+
${scripts.trim()}
|
|
40
|
+
</body>
|
|
41
|
+
</html>
|
|
42
|
+
`.trim();
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* @describe Renders a brand-styled "developer screen" card. Used as the
|
|
46
|
+
* pre-render inside #root for offline-dev-server fallback today, intended
|
|
47
|
+
* for reuse in other build-time / runtime dev panels.
|
|
48
|
+
*/
|
|
49
|
+
export const renderDevScreen = ({ title, body, fill = false }) => {
|
|
50
|
+
const cls = `extro-dev-screen${fill ? " extro-dev-screen--fill" : ""}`;
|
|
51
|
+
return `
|
|
52
|
+
<div class="${cls}">
|
|
53
|
+
<div class="extro-dev-screen__card">
|
|
54
|
+
<span class="extro-dev-screen__tag">EXTRO</span>
|
|
55
|
+
<h1>${title}</h1>
|
|
56
|
+
${body.trim()}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
`.trim();
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* @describe Global styles for any `.extro-dev-screen` rendered in dev. Body
|
|
63
|
+
* is painted dark only while a screen is in the DOM (via `:has()`), so the
|
|
64
|
+
* user's app reverts to default styling once React mounts.
|
|
65
|
+
*/
|
|
66
|
+
export const devScreenStyles = () => `
|
|
67
|
+
<style>
|
|
68
|
+
body { margin: 0; }
|
|
69
|
+
body:has(.extro-dev-screen) { background: #0a0a0a; }
|
|
70
|
+
.extro-dev-screen {
|
|
71
|
+
box-sizing: border-box;
|
|
72
|
+
min-width: 360px;
|
|
73
|
+
min-height: 240px;
|
|
74
|
+
padding: 24px 28px;
|
|
75
|
+
background: #0a0a0a;
|
|
76
|
+
display: flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
justify-content: center;
|
|
79
|
+
opacity: 0;
|
|
80
|
+
animation: extro-dev-screen-in 0.18s 0.05s forwards;
|
|
81
|
+
}
|
|
82
|
+
.extro-dev-screen--fill {
|
|
83
|
+
min-height: 100vh;
|
|
84
|
+
}
|
|
85
|
+
.extro-dev-screen__card {
|
|
86
|
+
box-sizing: border-box;
|
|
87
|
+
width: 100%;
|
|
88
|
+
max-width: 480px;
|
|
89
|
+
display: flex;
|
|
90
|
+
flex-direction: column;
|
|
91
|
+
gap: 10px;
|
|
92
|
+
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
93
|
+
color: #e5e5e5;
|
|
94
|
+
}
|
|
95
|
+
.extro-dev-screen__tag {
|
|
96
|
+
align-self: flex-start;
|
|
97
|
+
font-size: 11px;
|
|
98
|
+
font-weight: 600;
|
|
99
|
+
letter-spacing: 0.08em;
|
|
100
|
+
padding: 3px 8px;
|
|
101
|
+
color: #0a0a0a;
|
|
102
|
+
background: #CC785C;
|
|
103
|
+
border-radius: 3px;
|
|
104
|
+
}
|
|
105
|
+
.extro-dev-screen h1 {
|
|
106
|
+
margin: 4px 0 0;
|
|
107
|
+
font-size: 15px;
|
|
108
|
+
font-weight: 600;
|
|
109
|
+
color: #fafafa;
|
|
110
|
+
}
|
|
111
|
+
.extro-dev-screen p {
|
|
112
|
+
margin: 0;
|
|
113
|
+
font-size: 13px;
|
|
114
|
+
line-height: 1.5;
|
|
115
|
+
color: #a3a3a3;
|
|
116
|
+
}
|
|
117
|
+
.extro-dev-screen code {
|
|
118
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
119
|
+
font-size: 12px;
|
|
120
|
+
padding: 1px 6px;
|
|
121
|
+
color: #CC785C;
|
|
122
|
+
background: #1a1a1a;
|
|
123
|
+
border-radius: 3px;
|
|
124
|
+
}
|
|
125
|
+
@keyframes extro-dev-screen-in { to { opacity: 1; } }
|
|
126
|
+
</style>
|
|
127
|
+
`.trim();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PluginContextLike } from "../types/index.js";
|
|
2
|
+
interface EmitIconsOptions {
|
|
3
|
+
ctx: PluginContextLike;
|
|
4
|
+
root: string;
|
|
5
|
+
/** The recognized icon set from the Asset inventory (size -> "icons/16.png"). */
|
|
6
|
+
icons: Record<string, string> | null;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* @file generators/icons.ts
|
|
10
|
+
* @description Emits the recognized extension icons named by the Asset
|
|
11
|
+
* inventory. Discovery already decided which sizes exist, so this only reads
|
|
12
|
+
* bytes: what ships is exactly what `manifest.icons` references.
|
|
13
|
+
*/
|
|
14
|
+
export declare function emitIcons({ ctx, root, icons }: EmitIconsOptions): void;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* @file generators/icons.ts
|
|
5
|
+
* @description Emits the recognized extension icons named by the Asset
|
|
6
|
+
* inventory. Discovery already decided which sizes exist, so this only reads
|
|
7
|
+
* bytes: what ships is exactly what `manifest.icons` references.
|
|
8
|
+
*/
|
|
9
|
+
export function emitIcons({ ctx, root, icons }) {
|
|
10
|
+
if (!icons)
|
|
11
|
+
return;
|
|
12
|
+
for (const rel of Object.values(icons)) {
|
|
13
|
+
const source = fs.readFileSync(path.join(root, rel));
|
|
14
|
+
ctx.emitFile({ type: "asset", fileName: rel, source });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PluginContextLike } from "../types/index.js";
|
|
2
|
+
import { type PublicAssets } from "../public.js";
|
|
3
|
+
interface EmitPublicAssetsOptions {
|
|
4
|
+
ctx: PluginContextLike;
|
|
5
|
+
root: string;
|
|
6
|
+
/** The partitioned Public assets from the Asset inventory. */
|
|
7
|
+
publicAssets: PublicAssets;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* @file generators/public.ts
|
|
11
|
+
* @description Emits Public assets into the build output with their original
|
|
12
|
+
* names, from the partition the Asset inventory already computed. Mirrors
|
|
13
|
+
* `emitIcons`; the collision guard skips any file that would overwrite a
|
|
14
|
+
* generated output and warns instead.
|
|
15
|
+
*/
|
|
16
|
+
export declare function emitPublicAssets({ ctx, root, publicAssets }: EmitPublicAssetsOptions): void;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { PUBLIC_DIR } from "../public.js";
|
|
4
|
+
/**
|
|
5
|
+
* @file generators/public.ts
|
|
6
|
+
* @description Emits Public assets into the build output with their original
|
|
7
|
+
* names, from the partition the Asset inventory already computed. Mirrors
|
|
8
|
+
* `emitIcons`; the collision guard skips any file that would overwrite a
|
|
9
|
+
* generated output and warns instead.
|
|
10
|
+
*/
|
|
11
|
+
export function emitPublicAssets({ ctx, root, publicAssets }) {
|
|
12
|
+
const { files, conflicts } = publicAssets;
|
|
13
|
+
for (const conflict of conflicts) {
|
|
14
|
+
ctx.warn(`public/${conflict} collides with a generated output; skipping. Rename it to ship it.`);
|
|
15
|
+
}
|
|
16
|
+
for (const file of files) {
|
|
17
|
+
const source = fs.readFileSync(path.join(root, PUBLIC_DIR, file));
|
|
18
|
+
ctx.emitFile({ type: "asset", fileName: file, source });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* @file icons.ts
|
|
5
|
+
* @description Detects the icons for the extension.
|
|
6
|
+
*/
|
|
7
|
+
export function detectIcons(root) {
|
|
8
|
+
const iconsDir = path.join(root, "icons");
|
|
9
|
+
if (!fs.existsSync(iconsDir))
|
|
10
|
+
return undefined;
|
|
11
|
+
const sizes = ["16", "32", "48", "128"];
|
|
12
|
+
const icons = {};
|
|
13
|
+
for (const size of sizes) {
|
|
14
|
+
const file = path.join(iconsDir, `${size}.png`);
|
|
15
|
+
if (fs.existsSync(file)) {
|
|
16
|
+
icons[size] = `icons/${size}.png`;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return Object.keys(icons).length ? icons : undefined;
|
|
20
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
import type { ExtroConfig } from "../types/index.js";
|
|
3
|
+
interface ExtroPluginOptions {
|
|
4
|
+
root: string;
|
|
5
|
+
config?: ExtroConfig;
|
|
6
|
+
/**
|
|
7
|
+
* When true, only background/content are bundled. Manifest + HTML emission
|
|
8
|
+
* is skipped (writeDevAssets handles those separately during `extro dev`).
|
|
9
|
+
* Used by the dev build-watch sidecar.
|
|
10
|
+
*/
|
|
11
|
+
scriptsOnly?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* When set, wrap the background entry in the dev bridge so a service
|
|
14
|
+
* worker exists in dev mode (even if the user didn't write one) to
|
|
15
|
+
* receive rebuild signals from the CLI's WS server and forward Vite HMR
|
|
16
|
+
* events to content scripts.
|
|
17
|
+
*/
|
|
18
|
+
devBridge?: {
|
|
19
|
+
signalPort: number;
|
|
20
|
+
vitePort: number;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Called from the dev-server plugin's `handleHotUpdate` with a payload
|
|
24
|
+
* shaped like Vite's own HMR `update` event. The CLI uses this to
|
|
25
|
+
* broadcast HMR over the signal WS (avoiding Vite's HMR WS, whose
|
|
26
|
+
* origin check rejects chrome-extension:// service workers).
|
|
27
|
+
*/
|
|
28
|
+
broadcastHmr?: (payload: object) => void;
|
|
29
|
+
}
|
|
30
|
+
export declare function extro(options: ExtroPluginOptions): Plugin;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { scanAppTree, routeManifest, APP_FILE_BASENAMES, } from "./app-tree.js";
|
|
3
|
+
import { emitAssets } from "./emit-assets.js";
|
|
4
|
+
import { discoverAssets } from "./asset-inventory.js";
|
|
5
|
+
import { decideTreeReaction } from "./dev-reactions.js";
|
|
6
|
+
import { SURFACES } from "./surfaces.js";
|
|
7
|
+
import { emitIcons } from "./generators/icons.js";
|
|
8
|
+
import { emitPublicAssets } from "./generators/public.js";
|
|
9
|
+
import { emit } from "./runtimes/routes-module.js";
|
|
10
|
+
import { generateRuntimeModule } from "./runtimes/runtime-module.js";
|
|
11
|
+
import { DEV_BRIDGE_ENTRY_ID, DEV_BRIDGE_CONFIG_ID, DEV_BRIDGE_USER_BG_ID, loadDevBridgeClient, devBridgeConfigSource, devBridgeUserBackgroundSource, } from "./runtimes/dev-bridge.js";
|
|
12
|
+
import { CSUI_ENTRY_ID, CSUI_CONFIG_ID, CSUI_USER_PAGE_ID, CSUI_USER_SCRIPT_ID, loadCSUIClient, csuiConfigSource, csuiUserPageSource, csuiUserScriptSource, } from "./runtimes/csui-mount.js";
|
|
13
|
+
import { readJson } from "./utils/read-json.js";
|
|
14
|
+
// Virtual IDs follow a slash-namespaced convention so each surface has its
|
|
15
|
+
// own runtime + routes module. Resolved IDs are prefixed with "\0" per
|
|
16
|
+
// Rollup's convention for internal/virtual modules.
|
|
17
|
+
const routesId = (surface) => `virtual:extro/routes/${surface}`;
|
|
18
|
+
const runtimeId = (surface) => `virtual:extro/runtime/${surface}`;
|
|
19
|
+
const resolved = (id) => `\0${id}`;
|
|
20
|
+
export function extro(options) {
|
|
21
|
+
const root = options.root;
|
|
22
|
+
const config = options.config ?? {};
|
|
23
|
+
const scriptsOnly = options.scriptsOnly ?? false;
|
|
24
|
+
const devBridge = options.devBridge;
|
|
25
|
+
const broadcastHmr = options.broadcastHmr;
|
|
26
|
+
let tree = { scripts: {}, surfaces: {} };
|
|
27
|
+
let pkg = {};
|
|
28
|
+
return {
|
|
29
|
+
name: "extro",
|
|
30
|
+
async config() {
|
|
31
|
+
tree = await scanAppTree(root);
|
|
32
|
+
const empty = Object.keys(tree.scripts).length === 0 &&
|
|
33
|
+
Object.keys(tree.surfaces).length === 0;
|
|
34
|
+
if (empty && !devBridge) {
|
|
35
|
+
// In dev with bridge, an entry-less project still gets a synthesized
|
|
36
|
+
// background SW for HMR signalling. Otherwise, complain.
|
|
37
|
+
throw new Error("Extro: No extension entrypoints found.\n\nExpected files like:\n src/app/popup/page.tsx\n src/app/options/page.tsx\n src/app/sidepanel/page.tsx\n src/app/background/index.ts\n src/app/content/index.ts");
|
|
38
|
+
}
|
|
39
|
+
pkg = readJson("package.json", root) ?? {};
|
|
40
|
+
const input = {};
|
|
41
|
+
const contentEntry = tree.scripts.content?.csui
|
|
42
|
+
? CSUI_ENTRY_ID
|
|
43
|
+
: tree.scripts.content?.script;
|
|
44
|
+
if (devBridge) {
|
|
45
|
+
// Force a background entry in dev — wraps user's BG (if any) with
|
|
46
|
+
// the WS bridge.
|
|
47
|
+
input.background = DEV_BRIDGE_ENTRY_ID;
|
|
48
|
+
if (contentEntry)
|
|
49
|
+
input.content = contentEntry;
|
|
50
|
+
}
|
|
51
|
+
else if (scriptsOnly) {
|
|
52
|
+
if (tree.scripts.background)
|
|
53
|
+
input.background = tree.scripts.background;
|
|
54
|
+
if (contentEntry)
|
|
55
|
+
input.content = contentEntry;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
if (tree.scripts.background)
|
|
59
|
+
input.background = tree.scripts.background;
|
|
60
|
+
if (contentEntry)
|
|
61
|
+
input.content = contentEntry;
|
|
62
|
+
for (const surface of Object.keys(tree.surfaces)) {
|
|
63
|
+
input[surface] = runtimeId(surface);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
// Extro owns `public/` emission through its own pipeline (collision
|
|
68
|
+
// guard + WAR list + dev-output parity), so Vite's native copy is
|
|
69
|
+
// off. See ADR 0004.
|
|
70
|
+
publicDir: false,
|
|
71
|
+
// Only EXTRO_PUBLIC_* is inlined into surfaces via import.meta.env.
|
|
72
|
+
// Exactly EXTRO_PUBLIC_, never EXTRO_ (that would leak EXTRO_CRX_KEY
|
|
73
|
+
// and other build-time vars into bundles). See ADR 0002.
|
|
74
|
+
envPrefix: "EXTRO_PUBLIC_",
|
|
75
|
+
build: {
|
|
76
|
+
rollupOptions: {
|
|
77
|
+
input,
|
|
78
|
+
output: {
|
|
79
|
+
entryFileNames: "[name].js",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
async generateBundle() {
|
|
86
|
+
if (scriptsOnly)
|
|
87
|
+
return;
|
|
88
|
+
// One discovery pass feeds the manifest (pure), the icon emit, and the
|
|
89
|
+
// public emit. See the Asset inventory.
|
|
90
|
+
const inventory = discoverAssets(root, tree);
|
|
91
|
+
await emitAssets({ tree, inventory, pkg, config }, (fileName, source) => {
|
|
92
|
+
this.emitFile({ type: "asset", fileName, source });
|
|
93
|
+
});
|
|
94
|
+
emitIcons({ ctx: this, root, icons: inventory.icons });
|
|
95
|
+
emitPublicAssets({ ctx: this, root, publicAssets: inventory.public });
|
|
96
|
+
},
|
|
97
|
+
configureServer(server) {
|
|
98
|
+
if (scriptsOnly)
|
|
99
|
+
return;
|
|
100
|
+
const basenames = APP_FILE_BASENAMES.join(",");
|
|
101
|
+
server.watcher.add(path.join(root, `src/app/**/{${basenames}}.{ts,tsx}`));
|
|
102
|
+
// Warm the CSUI module into the dev server's module graph so its
|
|
103
|
+
// transitive imports are tracked too. Without this, content files
|
|
104
|
+
// never appear in handleHotUpdate's ctx.modules (the dev server
|
|
105
|
+
// only owns the routable surfaces — popup / options / sidepanel —
|
|
106
|
+
// as Rollup inputs), so edits under src/app/content/ would never
|
|
107
|
+
// produce an HMR payload for the BG-fetch RFR transport.
|
|
108
|
+
const csui = tree.scripts.content?.csui;
|
|
109
|
+
if (csui && broadcastHmr) {
|
|
110
|
+
const url = "/" + path.relative(root, csui).split(path.sep).join("/");
|
|
111
|
+
server.warmupRequest(url).catch(() => { });
|
|
112
|
+
}
|
|
113
|
+
// Same source as the scanner glob — see APP_FILE_BASENAMES.
|
|
114
|
+
const isAppEntry = new RegExp(`^src/app/[^/]+/(?:.+/)?(?:${APP_FILE_BASENAMES.join("|")})\\.tsx?$`);
|
|
115
|
+
const routableSurfaceList = SURFACES
|
|
116
|
+
.filter((s) => s.kind === "routable")
|
|
117
|
+
.map((s) => s.name);
|
|
118
|
+
const handleChange = async (file) => {
|
|
119
|
+
const rel = path.relative(root, file).split(path.sep).join("/");
|
|
120
|
+
if (!isAppEntry.test(rel))
|
|
121
|
+
return;
|
|
122
|
+
const prevTree = tree;
|
|
123
|
+
tree = await scanAppTree(root);
|
|
124
|
+
// Decide the Dev reaction (pure); the handler owns the effects.
|
|
125
|
+
const reaction = decideTreeReaction(prevTree, tree, routableSurfaceList);
|
|
126
|
+
if (reaction.kind === "restart") {
|
|
127
|
+
// A Surface born mid-session: its Rollup input was fixed at config()
|
|
128
|
+
// time, so a fresh dev session is required to register the new entry.
|
|
129
|
+
const what = reaction.surface === "background" || reaction.surface === "content"
|
|
130
|
+
? `${reaction.surface} entrypoint`
|
|
131
|
+
: `${reaction.surface} surface`;
|
|
132
|
+
console.log(`\n[extro] New ${what} detected. Restart \`extro dev\` to pick it up.\n`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (reaction.kind === "invalidate") {
|
|
136
|
+
// Reload each changed surface's routes virtual module; the runtime
|
|
137
|
+
// module's accept("virtual:extro/routes/<surface>") boundary picks
|
|
138
|
+
// up the new array and calls handle.update without a remount.
|
|
139
|
+
for (const surface of reaction.surfaces) {
|
|
140
|
+
const mod = server.moduleGraph.getModuleById(resolved(routesId(surface)));
|
|
141
|
+
if (mod) {
|
|
142
|
+
await server.reloadModule(mod);
|
|
143
|
+
console.log(`[extro] Routes updated for ${surface}.`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
server.watcher.on("add", handleChange);
|
|
149
|
+
server.watcher.on("unlink", handleChange);
|
|
150
|
+
},
|
|
151
|
+
resolveId(id) {
|
|
152
|
+
// Dev bridge virtuals
|
|
153
|
+
if (devBridge && id === DEV_BRIDGE_ENTRY_ID)
|
|
154
|
+
return resolved(DEV_BRIDGE_ENTRY_ID);
|
|
155
|
+
if (devBridge && id === DEV_BRIDGE_CONFIG_ID)
|
|
156
|
+
return resolved(DEV_BRIDGE_CONFIG_ID);
|
|
157
|
+
if (devBridge && id === DEV_BRIDGE_USER_BG_ID)
|
|
158
|
+
return resolved(DEV_BRIDGE_USER_BG_ID);
|
|
159
|
+
// CSUI virtuals
|
|
160
|
+
if (id === CSUI_ENTRY_ID)
|
|
161
|
+
return resolved(CSUI_ENTRY_ID);
|
|
162
|
+
if (id === CSUI_CONFIG_ID)
|
|
163
|
+
return resolved(CSUI_CONFIG_ID);
|
|
164
|
+
if (id === CSUI_USER_PAGE_ID)
|
|
165
|
+
return resolved(CSUI_USER_PAGE_ID);
|
|
166
|
+
if (id === CSUI_USER_SCRIPT_ID)
|
|
167
|
+
return resolved(CSUI_USER_SCRIPT_ID);
|
|
168
|
+
if (scriptsOnly)
|
|
169
|
+
return;
|
|
170
|
+
for (const desc of SURFACES) {
|
|
171
|
+
if (desc.kind !== "routable")
|
|
172
|
+
continue;
|
|
173
|
+
const surface = desc.name;
|
|
174
|
+
if (id === runtimeId(surface))
|
|
175
|
+
return resolved(runtimeId(surface));
|
|
176
|
+
if (id === routesId(surface))
|
|
177
|
+
return resolved(routesId(surface));
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
load(id) {
|
|
181
|
+
// Dev bridge: entry + its config + the optional user-background re-export.
|
|
182
|
+
if (devBridge && id === resolved(DEV_BRIDGE_ENTRY_ID)) {
|
|
183
|
+
return loadDevBridgeClient();
|
|
184
|
+
}
|
|
185
|
+
if (devBridge && id === resolved(DEV_BRIDGE_CONFIG_ID)) {
|
|
186
|
+
return devBridgeConfigSource({
|
|
187
|
+
signalPort: devBridge.signalPort,
|
|
188
|
+
vitePort: devBridge.vitePort,
|
|
189
|
+
hasCSUI: !!tree.scripts.content?.csui,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
if (devBridge && id === resolved(DEV_BRIDGE_USER_BG_ID)) {
|
|
193
|
+
return devBridgeUserBackgroundSource(tree.scripts.background);
|
|
194
|
+
}
|
|
195
|
+
// CSUI mount: entry + its config + the user page + optional side-effect script.
|
|
196
|
+
const content = tree.scripts.content;
|
|
197
|
+
if (id === resolved(CSUI_ENTRY_ID) && content?.csui) {
|
|
198
|
+
return loadCSUIClient();
|
|
199
|
+
}
|
|
200
|
+
if (id === resolved(CSUI_CONFIG_ID)) {
|
|
201
|
+
return csuiConfigSource(!!devBridge);
|
|
202
|
+
}
|
|
203
|
+
if (id === resolved(CSUI_USER_PAGE_ID) && content?.csui) {
|
|
204
|
+
return csuiUserPageSource(content.csui);
|
|
205
|
+
}
|
|
206
|
+
if (id === resolved(CSUI_USER_SCRIPT_ID)) {
|
|
207
|
+
return csuiUserScriptSource(content?.script);
|
|
208
|
+
}
|
|
209
|
+
if (scriptsOnly)
|
|
210
|
+
return;
|
|
211
|
+
for (const desc of SURFACES) {
|
|
212
|
+
if (desc.kind !== "routable")
|
|
213
|
+
continue;
|
|
214
|
+
const surface = desc.name;
|
|
215
|
+
if (id === resolved(runtimeId(surface))) {
|
|
216
|
+
return generateRuntimeModule({ surface });
|
|
217
|
+
}
|
|
218
|
+
if (id === resolved(routesId(surface))) {
|
|
219
|
+
return emit(routeManifest(tree, surface));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
handleHotUpdate(ctx) {
|
|
224
|
+
if (!broadcastHmr)
|
|
225
|
+
return;
|
|
226
|
+
// Mirror Vite's own HMR `update` payload shape. Modules without a
|
|
227
|
+
// resolved url (rare — e.g. virtual-only) are skipped; the CSUI
|
|
228
|
+
// re-mount signal still covers those via `cs-rebuilt`. Content
|
|
229
|
+
// files reach this list naturally because configureServer warms the
|
|
230
|
+
// CSUI module into the graph at startup.
|
|
231
|
+
const updates = ctx.modules
|
|
232
|
+
.filter((m) => !!m.url)
|
|
233
|
+
.map((m) => ({
|
|
234
|
+
type: m.type === "css" ? "css-update" : "js-update",
|
|
235
|
+
path: m.url,
|
|
236
|
+
acceptedPath: m.url,
|
|
237
|
+
timestamp: ctx.timestamp,
|
|
238
|
+
explicitImportRequired: false,
|
|
239
|
+
isWithinCircularImport: false,
|
|
240
|
+
}));
|
|
241
|
+
if (updates.length === 0)
|
|
242
|
+
return;
|
|
243
|
+
broadcastHmr({ type: "update", updates });
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { scanAppTree, routeManifest } from "./app-tree.js";
|
|
2
|
+
export type { AppTree, ContentSlot } from "./app-tree.js";
|
|
3
|
+
export type { ManifestRoute, RouteManifest } from "../types/index.js";
|
|
4
|
+
export { emitAssets, composeArtifacts } from "./emit-assets.js";
|
|
5
|
+
export type { EmitSink, AssetOptions, Artifacts } from "./emit-assets.js";
|
|
6
|
+
export { discoverAssets } from "./asset-inventory.js";
|
|
7
|
+
export type { AssetInventory } from "./asset-inventory.js";
|
|
8
|
+
export type { PublicAssets } from "./public.js";
|
|
9
|
+
export { classifyScriptChange, mergeDirty, resolveFlush } from "./dev-reactions.js";
|
|
10
|
+
export type { ScriptDirty } from "./dev-reactions.js";
|
|
11
|
+
export { renderDevScreen, devScreenStyles } from "./generators/html.js";
|
|
12
|
+
export type { DevScreen } from "./generators/html.js";
|
|
13
|
+
export { SURFACES } from "./surfaces.js";
|
|
14
|
+
export type { RoutableSurface, ScriptSurface, SurfaceName, SurfaceKind, SurfaceContext, SurfaceDescriptor, } from "./surfaces.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { scanAppTree, routeManifest } from "./app-tree.js";
|
|
2
|
+
export { emitAssets, composeArtifacts } from "./emit-assets.js";
|
|
3
|
+
export { discoverAssets } from "./asset-inventory.js";
|
|
4
|
+
export { classifyScriptChange, mergeDirty, resolveFlush } from "./dev-reactions.js";
|
|
5
|
+
export { renderDevScreen, devScreenStyles } from "./generators/html.js";
|
|
6
|
+
export { SURFACES } from "./surfaces.js";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ExtroConfig, ManifestV3 } from "../types/index.js";
|
|
2
|
+
import type { AppTree } from "./app-tree.js";
|
|
3
|
+
import type { AssetInventory } from "./asset-inventory.js";
|
|
4
|
+
interface GenerateManifestOptions {
|
|
5
|
+
tree: AppTree;
|
|
6
|
+
/**
|
|
7
|
+
* The build's discovered icons + Public assets. Passed in as data so this
|
|
8
|
+
* generator never touches the filesystem: its inputs are the test surface.
|
|
9
|
+
*/
|
|
10
|
+
inventory: AssetInventory;
|
|
11
|
+
pkg: {
|
|
12
|
+
name?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
version?: string;
|
|
15
|
+
};
|
|
16
|
+
config: ExtroConfig;
|
|
17
|
+
/**
|
|
18
|
+
* When set, the manifest is generated for a dev session:
|
|
19
|
+
* - CSP relaxed for the Vite dev server + signal WS
|
|
20
|
+
* - Background descriptor reports present (bridge runs there)
|
|
21
|
+
* - Background descriptor adds `tabs` to permissions
|
|
22
|
+
*/
|
|
23
|
+
dev?: {
|
|
24
|
+
port: number;
|
|
25
|
+
signalPort: number;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export declare function generateManifest({ tree, inventory, pkg, config, dev, }: GenerateManifestOptions): ManifestV3;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { SURFACES } from "./surfaces.js";
|
|
2
|
+
export function generateManifest({ tree, inventory, pkg, config, dev, }) {
|
|
3
|
+
const manifest = {
|
|
4
|
+
manifest_version: 3,
|
|
5
|
+
name: config.name ?? pkg.name ?? "Extro Extension",
|
|
6
|
+
version: config.version ?? pkg.version ?? "0.0.1",
|
|
7
|
+
};
|
|
8
|
+
const description = config.description ?? pkg.description;
|
|
9
|
+
if (description) {
|
|
10
|
+
manifest.description = description;
|
|
11
|
+
}
|
|
12
|
+
const publicAssets = inventory.public.files;
|
|
13
|
+
const ctx = { tree, config, dev, publicAssets };
|
|
14
|
+
const permissions = new Set(config.permissions ?? []);
|
|
15
|
+
const hostPermissions = new Set(config.hostPermissions ?? []);
|
|
16
|
+
for (const desc of SURFACES) {
|
|
17
|
+
if (!desc.isPresent(ctx))
|
|
18
|
+
continue;
|
|
19
|
+
Object.assign(manifest, desc.manifestContribution(ctx));
|
|
20
|
+
if (!config.permissions && desc.permissions) {
|
|
21
|
+
for (const p of desc.permissions(ctx))
|
|
22
|
+
permissions.add(p);
|
|
23
|
+
}
|
|
24
|
+
if (!config.hostPermissions && desc.hostPermissions) {
|
|
25
|
+
for (const p of desc.hostPermissions(ctx))
|
|
26
|
+
hostPermissions.add(p);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (permissions.size > 0) {
|
|
30
|
+
manifest.permissions = [...permissions];
|
|
31
|
+
}
|
|
32
|
+
if (hostPermissions.size > 0) {
|
|
33
|
+
manifest.host_permissions = [...hostPermissions];
|
|
34
|
+
}
|
|
35
|
+
const icons = config.icons ?? inventory.icons ?? undefined;
|
|
36
|
+
if (icons) {
|
|
37
|
+
manifest.icons = icons;
|
|
38
|
+
}
|
|
39
|
+
if (dev) {
|
|
40
|
+
// CSP relaxed for dev: Vite dev server (HTTP + HMR WS) + the CLI's
|
|
41
|
+
// signal WS that the dev bridge connects to.
|
|
42
|
+
const origin = `http://localhost:${dev.port}`;
|
|
43
|
+
const viteWs = `ws://localhost:${dev.port}`;
|
|
44
|
+
const signalWs = `ws://localhost:${dev.signalPort}`;
|
|
45
|
+
manifest.content_security_policy = {
|
|
46
|
+
extension_pages: [
|
|
47
|
+
`script-src 'self' ${origin} 'wasm-unsafe-eval'`,
|
|
48
|
+
`object-src 'self'`,
|
|
49
|
+
`connect-src 'self' ${origin} ${viteWs} ${signalWs}`,
|
|
50
|
+
].join("; "),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// EXTRO_CRX_KEY pins the extension ID (ADR 0002). Set before the
|
|
54
|
+
// config.manifest merge so an explicit manifest.key still wins.
|
|
55
|
+
const crxKey = process.env.EXTRO_CRX_KEY;
|
|
56
|
+
if (crxKey) {
|
|
57
|
+
manifest.key = crxKey;
|
|
58
|
+
}
|
|
59
|
+
if (config.manifest) {
|
|
60
|
+
Object.assign(manifest, config.manifest);
|
|
61
|
+
}
|
|
62
|
+
// Final imperative hook: sees the fully generated manifest and may mutate it
|
|
63
|
+
// or return a replacement. Runs last so it can change anything (ADR 0008).
|
|
64
|
+
if (config.transformManifest) {
|
|
65
|
+
return config.transformManifest(manifest) ?? manifest;
|
|
66
|
+
}
|
|
67
|
+
return manifest;
|
|
68
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AppTree } from "./app-tree.js";
|
|
2
|
+
/**
|
|
3
|
+
* @file public.ts
|
|
4
|
+
* @description Discovers Public assets (static files under the project-root
|
|
5
|
+
* `public/` directory) and partitions them against the names Extro itself
|
|
6
|
+
* emits, so a stray `public/manifest.json` can never shadow the real one.
|
|
7
|
+
* The same partition drives prod emission, dev copy, and the
|
|
8
|
+
* `web_accessible_resources` list, so all three agree on exactly what ships.
|
|
9
|
+
*/
|
|
10
|
+
export declare const PUBLIC_DIR = "public";
|
|
11
|
+
export interface PublicAssets {
|
|
12
|
+
/** Posix-relative paths that will ship, sorted for stable output. */
|
|
13
|
+
files: string[];
|
|
14
|
+
/** Files skipped because their name collides with a generated output. */
|
|
15
|
+
conflicts: string[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Partition `public/` into shippable files and conflicts. Returns empty
|
|
19
|
+
* lists when `public/` is absent.
|
|
20
|
+
*/
|
|
21
|
+
export declare function collectPublicAssets(root: string, tree: AppTree): PublicAssets;
|