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
package/README.md
CHANGED
|
@@ -16,11 +16,11 @@ pnpm add react react-dom
|
|
|
16
16
|
## Quick start
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
extro dev # dev server with HMR, writes
|
|
20
|
-
extro build # production build to
|
|
19
|
+
extro dev # dev server with HMR, writes output/chrome-mv3-dev/
|
|
20
|
+
extro build # production build to output/chrome-mv3-prod/
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
Load
|
|
23
|
+
Load `output/chrome-mv3-dev/` (or `output/chrome-mv3-prod/`) in Chrome via **Load Unpacked**.
|
|
24
24
|
|
|
25
25
|
## Docs and source
|
|
26
26
|
|
package/client.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Ambient types for Extro's env, for code that imports nothing else from the
|
|
2
|
+
// framework (a bare background or content script). Opt in once:
|
|
3
|
+
// /// <reference types="extrojs/client" />
|
|
4
|
+
// The single source of the env shape is src/react/env.ts (ADR 0002); this
|
|
5
|
+
// re-surfaces that one declaration for the explicit-reference path, so the two
|
|
6
|
+
// opt-in routes never declare ImportMetaEnv twice. EXTRO_PUBLIC_* is inlined
|
|
7
|
+
// into every surface via import.meta.env.
|
|
8
|
+
/// <reference path="./dist/react/env.d.ts" />
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @describe Parses argv and dispatches. cac itself prints --help/--version
|
|
3
|
+
* during parse() (scoped to the matched command), so we only handle the
|
|
4
|
+
* fallback: bare `extro` and unknown commands print top-level help (exit 0
|
|
5
|
+
* when bare, exit 1 when the command was unknown).
|
|
6
|
+
*/
|
|
7
|
+
export declare const run: () => Promise<void>;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { cac } from "cac";
|
|
2
|
+
import { pkg } from "./pkg.js";
|
|
3
|
+
import { dev } from "./commands/dev.js";
|
|
4
|
+
import { build } from "./commands/build.js";
|
|
5
|
+
const cli = cac("extro");
|
|
6
|
+
cli.command("dev", "Start the dev server with HMR").action(dev);
|
|
7
|
+
cli.command("build", "Build a production extension bundle").action(build);
|
|
8
|
+
cli.help();
|
|
9
|
+
cli.version(pkg.version);
|
|
10
|
+
/**
|
|
11
|
+
* @describe Parses argv and dispatches. cac itself prints --help/--version
|
|
12
|
+
* during parse() (scoped to the matched command), so we only handle the
|
|
13
|
+
* fallback: bare `extro` and unknown commands print top-level help (exit 0
|
|
14
|
+
* when bare, exit 1 when the command was unknown).
|
|
15
|
+
*/
|
|
16
|
+
export const run = async () => {
|
|
17
|
+
const parsed = cli.parse(process.argv, { run: false });
|
|
18
|
+
// cac has already printed help/version to stdout at this point.
|
|
19
|
+
if (parsed.options.help || parsed.options.version)
|
|
20
|
+
return;
|
|
21
|
+
if (!cli.matchedCommand) {
|
|
22
|
+
cli.outputHelp();
|
|
23
|
+
process.exitCode = process.argv.slice(2).length > 0 ? 1 : 0;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
await cli.runMatchedCommand();
|
|
27
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { build as viteBuild } from "vite";
|
|
2
|
+
import { extro } from "../plugin/index.js";
|
|
3
|
+
import react from "@vitejs/plugin-react";
|
|
4
|
+
import { loadConfig } from "../load-config.js";
|
|
5
|
+
import { loadEnvIntoProcess } from "../env.js";
|
|
6
|
+
import { outputDir } from "../paths.js";
|
|
7
|
+
import { createViteLogger, log } from "../logger.js";
|
|
8
|
+
/**
|
|
9
|
+
* @describe Produces a standalone production bundle in
|
|
10
|
+
* <outDir>/chrome-mv3-prod/: manifest, HTML shells, and script bundles.
|
|
11
|
+
*/
|
|
12
|
+
export const build = async () => {
|
|
13
|
+
const root = process.cwd();
|
|
14
|
+
log.info("Building extension for production...");
|
|
15
|
+
loadEnvIntoProcess(root, "production");
|
|
16
|
+
const config = await loadConfig(root);
|
|
17
|
+
const prodOutDir = outputDir(root, config, "prod");
|
|
18
|
+
await viteBuild({
|
|
19
|
+
root,
|
|
20
|
+
plugins: [react(), extro({ root, config })],
|
|
21
|
+
build: { outDir: prodOutDir },
|
|
22
|
+
customLogger: createViteLogger(),
|
|
23
|
+
});
|
|
24
|
+
log.success("Build complete");
|
|
25
|
+
log.info(`Unpacked extension: ${prodOutDir}`);
|
|
26
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @describe Starts the Extro dev server: a Vite server for routable surfaces, a
|
|
3
|
+
* signal WS the extension's BG SW connects to, and a build-watch sidecar for
|
|
4
|
+
* background + content scripts. Writes the dev manifest/HTML into the dev
|
|
5
|
+
* output dir and wires SIGINT/SIGTERM to a clean shutdown.
|
|
6
|
+
*/
|
|
7
|
+
export declare const dev: () => Promise<void>;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { createServer, build as viteBuild } from "vite";
|
|
2
|
+
import { WebSocketServer } from "ws";
|
|
3
|
+
import { extro } from "../plugin/index.js";
|
|
4
|
+
import react from "@vitejs/plugin-react";
|
|
5
|
+
import { scanAppTree, classifyScriptChange, mergeDirty, resolveFlush, } from "../plugin/internal.js";
|
|
6
|
+
import { loadConfig } from "../load-config.js";
|
|
7
|
+
import { loadEnvIntoProcess } from "../env.js";
|
|
8
|
+
import { outputDir } from "../paths.js";
|
|
9
|
+
import { writeDevAssets } from "../dev-assets.js";
|
|
10
|
+
import { pkg } from "../pkg.js";
|
|
11
|
+
import { banner, createViteLogger, log } from "../logger.js";
|
|
12
|
+
const once = (emitter, event) => new Promise((resolve) => emitter.once(event, () => resolve()));
|
|
13
|
+
/**
|
|
14
|
+
* @describe Throws a helpful error when the user's src/app has no entrypoints,
|
|
15
|
+
* so `extro dev` fails fast with guidance instead of starting an empty server.
|
|
16
|
+
*/
|
|
17
|
+
const validateTree = (tree) => {
|
|
18
|
+
const empty = Object.keys(tree.scripts).length === 0 &&
|
|
19
|
+
Object.keys(tree.surfaces).length === 0;
|
|
20
|
+
if (!empty)
|
|
21
|
+
return;
|
|
22
|
+
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");
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* @describe Starts the Extro dev server: a Vite server for routable surfaces, a
|
|
26
|
+
* signal WS the extension's BG SW connects to, and a build-watch sidecar for
|
|
27
|
+
* background + content scripts. Writes the dev manifest/HTML into the dev
|
|
28
|
+
* output dir and wires SIGINT/SIGTERM to a clean shutdown.
|
|
29
|
+
*/
|
|
30
|
+
export const dev = async () => {
|
|
31
|
+
const root = process.cwd();
|
|
32
|
+
loadEnvIntoProcess(root, "development");
|
|
33
|
+
const config = await loadConfig(root);
|
|
34
|
+
// Separate output dirs let dev artifacts (with the bridge installed) persist
|
|
35
|
+
// across `extro dev` sessions without needing a prod-restore on shutdown.
|
|
36
|
+
const devOutDir = outputDir(root, config, "dev");
|
|
37
|
+
// 1. Scan once up front so we can decide what to start.
|
|
38
|
+
const tree = await scanAppTree(root);
|
|
39
|
+
validateTree(tree);
|
|
40
|
+
// 2. Signal WS — the dev bridge in the extension's BG SW connects here.
|
|
41
|
+
// Fixed port (not :0) so the port baked into a previously-loaded BG SW
|
|
42
|
+
// keeps working across `extro dev` restarts — otherwise users have to
|
|
43
|
+
// refresh the extension every time to pick up a new random port.
|
|
44
|
+
const signalPort = config.dev?.bridgePort ?? 9012;
|
|
45
|
+
const wss = new WebSocketServer({ port: signalPort });
|
|
46
|
+
wss.on("error", (err) => {
|
|
47
|
+
if (err.code === "EADDRINUSE") {
|
|
48
|
+
log.error(`Signal port ${signalPort} is in use. Is another \`extro dev\` already running?`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
throw err;
|
|
52
|
+
});
|
|
53
|
+
await once(wss, "listening");
|
|
54
|
+
const broadcast = (msg) => {
|
|
55
|
+
const payload = JSON.stringify(msg);
|
|
56
|
+
for (const client of wss.clients) {
|
|
57
|
+
if (client.readyState === 1)
|
|
58
|
+
client.send(payload);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
// 3. Vite dev server for routable surfaces.
|
|
62
|
+
// `broadcastHmr` lets the plugin push HMR updates over our signal WS —
|
|
63
|
+
// we can't piggy-back on Vite's own HMR WS because its origin check
|
|
64
|
+
// rejects chrome-extension:// service workers.
|
|
65
|
+
const viteLogger = createViteLogger();
|
|
66
|
+
const server = await createServer({
|
|
67
|
+
root,
|
|
68
|
+
plugins: [
|
|
69
|
+
react(),
|
|
70
|
+
extro({
|
|
71
|
+
root,
|
|
72
|
+
config,
|
|
73
|
+
broadcastHmr: (payload) => broadcast({ kind: "vite-hmr", payload }),
|
|
74
|
+
}),
|
|
75
|
+
],
|
|
76
|
+
server: { cors: true, port: config.dev?.port, strictPort: config.dev?.strictPort },
|
|
77
|
+
// Keep the user's scrollback + our banner; Vite clears the terminal by
|
|
78
|
+
// default on start and on each HMR.
|
|
79
|
+
clearScreen: false,
|
|
80
|
+
customLogger: viteLogger,
|
|
81
|
+
});
|
|
82
|
+
await server.listen();
|
|
83
|
+
const addr = server.httpServer?.address();
|
|
84
|
+
const port = addr && typeof addr === "object"
|
|
85
|
+
? addr.port
|
|
86
|
+
: server.config.server.port ?? 5173;
|
|
87
|
+
// 4. Dev manifest + HTML + icons.
|
|
88
|
+
await writeDevAssets({ tree, root, outDir: devOutDir, port, signalPort, config });
|
|
89
|
+
// 5. Build-watch sidecar for background + content. Always runs in dev so
|
|
90
|
+
// the dev bridge gets bundled into background.js (even if the user has
|
|
91
|
+
// no BG of their own).
|
|
92
|
+
const watcher = await viteBuild({
|
|
93
|
+
root,
|
|
94
|
+
// Same mode as the dev server (createServer defaults to development) so
|
|
95
|
+
// background/content resolve the same .env set as the routables. Without
|
|
96
|
+
// this the sidecar defaults to production and the scripts would load
|
|
97
|
+
// .env.production while the popup loads .env.development. See ADR 0002.
|
|
98
|
+
mode: "development",
|
|
99
|
+
plugins: [extro({ root, config, scriptsOnly: true, devBridge: { signalPort, vitePort: port } })],
|
|
100
|
+
// emptyOutDir: false so the watcher doesn't wipe the manifest / HTML /
|
|
101
|
+
// icons that writeDevAssets just put down.
|
|
102
|
+
build: { watch: {}, emptyOutDir: false, outDir: devOutDir },
|
|
103
|
+
logLevel: "error",
|
|
104
|
+
});
|
|
105
|
+
// The watcher is a RollupWatcher. We split the rebuild signal by which
|
|
106
|
+
// entry changed: a background-only edit must not reload tabs / remount CSUI,
|
|
107
|
+
// and a content-only edit must not reload the extension. `change` events
|
|
108
|
+
// accumulate the Dev reaction until the next `BUNDLE_END`; the classify and
|
|
109
|
+
// flush rules (incl. shared-code-dirties-both) live in `dev-reactions.ts`.
|
|
110
|
+
if (watcher && typeof watcher.on === "function") {
|
|
111
|
+
const w = watcher;
|
|
112
|
+
let dirty = { background: false, content: false };
|
|
113
|
+
w.on("change", (id) => {
|
|
114
|
+
dirty = mergeDirty(dirty, classifyScriptChange(String(id)));
|
|
115
|
+
});
|
|
116
|
+
w.on("event", (event) => {
|
|
117
|
+
if (event.code !== "BUNDLE_END")
|
|
118
|
+
return;
|
|
119
|
+
const { background, content } = resolveFlush(dirty);
|
|
120
|
+
dirty = { background: false, content: false };
|
|
121
|
+
if (background)
|
|
122
|
+
broadcast({ kind: "bg-rebuilt" });
|
|
123
|
+
if (content)
|
|
124
|
+
broadcast({ kind: "cs-rebuilt" });
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
banner({
|
|
128
|
+
mode: "dev",
|
|
129
|
+
version: pkg.version,
|
|
130
|
+
rows: [
|
|
131
|
+
{ label: "Server", value: `http://localhost:${port}` },
|
|
132
|
+
{ label: "Unpacked", value: devOutDir },
|
|
133
|
+
],
|
|
134
|
+
hint: 'Load the unpacked dir in Chrome via "Load Unpacked".',
|
|
135
|
+
});
|
|
136
|
+
let shuttingDown = false;
|
|
137
|
+
const shutdown = async () => {
|
|
138
|
+
if (shuttingDown)
|
|
139
|
+
return;
|
|
140
|
+
shuttingDown = true;
|
|
141
|
+
process.off("SIGINT", shutdown);
|
|
142
|
+
process.off("SIGTERM", shutdown);
|
|
143
|
+
log.info("Shutting down dev server...");
|
|
144
|
+
if (watcher && typeof watcher.close === "function") {
|
|
145
|
+
await watcher.close();
|
|
146
|
+
}
|
|
147
|
+
wss.close();
|
|
148
|
+
await server.close();
|
|
149
|
+
// No prod-restore: dev artifacts live in their own output/chrome-mv3-dev
|
|
150
|
+
// dir so the loaded extension stays untouched. Run `extro build` for a
|
|
151
|
+
// standalone prod bundle in output/chrome-mv3-prod.
|
|
152
|
+
process.exit(0);
|
|
153
|
+
};
|
|
154
|
+
process.on("SIGINT", shutdown);
|
|
155
|
+
process.on("SIGTERM", shutdown);
|
|
156
|
+
};
|
package/dist/config.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { ExtroConfig } from "
|
|
2
|
-
export type { ExtroConfig } from "
|
|
1
|
+
import type { ExtroConfig } from "./types/index.js";
|
|
2
|
+
export type { ExtroConfig } from "./types/index.js";
|
|
3
3
|
export declare const defineConfig: (config: ExtroConfig) => ExtroConfig;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a public asset to its extension URL. Works in every surface (popup,
|
|
3
|
+
* options, sidepanel, background, content), unlike a root-relative `/logo.svg`
|
|
4
|
+
* which resolves against a content script's host-page origin.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* import { asset } from "extrojs/asset"
|
|
8
|
+
* <img src={asset("logo.svg")} />
|
|
9
|
+
*/
|
|
10
|
+
export declare const asset: (path: string) => string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a public asset to its extension URL. Works in every surface (popup,
|
|
3
|
+
* options, sidepanel, background, content), unlike a root-relative `/logo.svg`
|
|
4
|
+
* which resolves against a content script's host-page origin.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* import { asset } from "extrojs/asset"
|
|
8
|
+
* <img src={asset("logo.svg")} />
|
|
9
|
+
*/
|
|
10
|
+
export const asset = (path) => chrome.runtime.getURL(path);
|
package/dist/dev-assets.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ExtroConfig } from "
|
|
2
|
-
import { type AppTree } from "
|
|
1
|
+
import type { ExtroConfig } from "./types/index.js";
|
|
2
|
+
import { type AppTree } from "./plugin/internal.js";
|
|
3
3
|
interface WriteDevAssetsOptions {
|
|
4
4
|
tree: AppTree;
|
|
5
5
|
root: string;
|
|
@@ -12,7 +12,7 @@ interface WriteDevAssetsOptions {
|
|
|
12
12
|
* @describe Writes the dev manifest + HTML shells + icons to disk so Chrome
|
|
13
13
|
* can load the unpacked extension while the Vite dev server serves modules
|
|
14
14
|
* over HTTP. Background/content scripts are written by the build-watch
|
|
15
|
-
* sidecar (a Vite watch-mode build)
|
|
15
|
+
* sidecar (a Vite watch-mode build), not here.
|
|
16
16
|
*/
|
|
17
17
|
export declare const writeDevAssets: ({ tree, root, outDir, port, signalPort, config, }: WriteDevAssetsOptions) => Promise<void>;
|
|
18
18
|
export {};
|
package/dist/dev-assets.js
CHANGED
|
@@ -1,26 +1,47 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { emitAssets, } from "
|
|
3
|
+
import { emitAssets, discoverAssets, } from "./plugin/internal.js";
|
|
4
4
|
/**
|
|
5
5
|
* @describe Writes the dev manifest + HTML shells + icons to disk so Chrome
|
|
6
6
|
* can load the unpacked extension while the Vite dev server serves modules
|
|
7
7
|
* over HTTP. Background/content scripts are written by the build-watch
|
|
8
|
-
* sidecar (a Vite watch-mode build)
|
|
8
|
+
* sidecar (a Vite watch-mode build), not here.
|
|
9
9
|
*/
|
|
10
10
|
export const writeDevAssets = async ({ tree, root, outDir, port, signalPort, config, }) => {
|
|
11
11
|
await fs.mkdir(outDir, { recursive: true });
|
|
12
12
|
const pkgRaw = await fs.readFile(path.join(root, "package.json"), "utf8").catch(() => "{}");
|
|
13
13
|
const pkg = JSON.parse(pkgRaw);
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// One discovery pass for the dev manifest, the icon copy, and the public
|
|
15
|
+
// copy, mirroring the prod path. See the Asset inventory.
|
|
16
|
+
const inventory = discoverAssets(root, tree);
|
|
17
|
+
await emitAssets({ tree, inventory, pkg, config, dev: { port, signalPort } }, (fileName, source) => fs.writeFile(path.join(outDir, fileName), source));
|
|
18
|
+
await copyIcons(root, outDir, inventory.icons);
|
|
19
|
+
await copyPublic(root, outDir, inventory.public);
|
|
16
20
|
};
|
|
17
|
-
const copyIcons = async (root, outDir) => {
|
|
18
|
-
|
|
19
|
-
const exists = await fs.stat(srcDir).then((s) => s.isDirectory()).catch(() => false);
|
|
20
|
-
if (!exists)
|
|
21
|
+
const copyIcons = async (root, outDir, icons) => {
|
|
22
|
+
if (!icons)
|
|
21
23
|
return;
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
await
|
|
24
|
+
const entries = Object.values(icons);
|
|
25
|
+
if (entries.length === 0)
|
|
26
|
+
return;
|
|
27
|
+
await fs.mkdir(path.join(outDir, "icons"), { recursive: true });
|
|
28
|
+
await Promise.all(entries.map((rel) => fs.copyFile(path.join(root, rel), path.join(outDir, rel))));
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* @describe Copies Public assets into the dev output dir so they resolve at
|
|
32
|
+
* the extension origin in dev exactly as in prod (chrome.runtime.getURL, or a
|
|
33
|
+
* root-relative ref on a routable surface). Mirrors copyIcons; the collision
|
|
34
|
+
* guard from the Asset inventory keeps a stray file from shadowing a
|
|
35
|
+
* generated output.
|
|
36
|
+
*/
|
|
37
|
+
const copyPublic = async (root, outDir, publicAssets) => {
|
|
38
|
+
const { files, conflicts } = publicAssets;
|
|
39
|
+
for (const conflict of conflicts) {
|
|
40
|
+
console.warn(`[extro] public/${conflict} collides with a generated output; skipping. Rename it to ship it.`);
|
|
41
|
+
}
|
|
42
|
+
await Promise.all(files.map(async (rel) => {
|
|
43
|
+
const dst = path.join(outDir, rel);
|
|
44
|
+
await fs.mkdir(path.dirname(dst), { recursive: true });
|
|
45
|
+
await fs.copyFile(path.join(root, "public", rel), dst);
|
|
46
|
+
}));
|
|
26
47
|
};
|
package/dist/env.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @describe Loads `.env` files into `process.env` before config is read, so
|
|
3
|
+
* `extro.config.ts` and manifest generation (the build-time env tier) can see
|
|
4
|
+
* them. Uses Vite's own `loadEnv` with an empty prefix (all vars, same
|
|
5
|
+
* stacking as `import.meta.env`), and does NOT override existing keys, so real
|
|
6
|
+
* process env (CI) wins over `.env` files. The public tier (`EXTRO_PUBLIC_*`
|
|
7
|
+
* via `import.meta.env`) is handled separately by Vite's `envPrefix`. See ADR
|
|
8
|
+
* 0002.
|
|
9
|
+
*/
|
|
10
|
+
export declare const loadEnvIntoProcess: (root: string, mode: "development" | "production") => void;
|
package/dist/env.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { loadEnv } from "vite";
|
|
2
|
+
/**
|
|
3
|
+
* @describe Loads `.env` files into `process.env` before config is read, so
|
|
4
|
+
* `extro.config.ts` and manifest generation (the build-time env tier) can see
|
|
5
|
+
* them. Uses Vite's own `loadEnv` with an empty prefix (all vars, same
|
|
6
|
+
* stacking as `import.meta.env`), and does NOT override existing keys, so real
|
|
7
|
+
* process env (CI) wins over `.env` files. The public tier (`EXTRO_PUBLIC_*`
|
|
8
|
+
* via `import.meta.env`) is handled separately by Vite's `envPrefix`. See ADR
|
|
9
|
+
* 0002.
|
|
10
|
+
*/
|
|
11
|
+
export const loadEnvIntoProcess = (root, mode) => {
|
|
12
|
+
const env = loadEnv(mode, root, "");
|
|
13
|
+
for (const [key, value] of Object.entries(env)) {
|
|
14
|
+
if (process.env[key] === undefined) {
|
|
15
|
+
process.env[key] = value;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { asset } from "../core/asset.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { asset } from "../core/asset.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Link } from "../router/index.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Link } from "../router/index.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useLocation, useParams, useRouter, useSearchParams, } from "../router/index.js";
|
package/dist/index.js
CHANGED
|
@@ -1,137 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import { scanAppTree } from "@extrojs/vite-plugin/internal";
|
|
8
|
-
import { loadConfig } from "./load-config.js";
|
|
9
|
-
import { writeDevAssets } from "./dev-assets.js";
|
|
10
|
-
const command = process.argv[2];
|
|
11
|
-
const root = process.cwd();
|
|
12
|
-
// Separate output dirs let dev artifacts (with the bridge installed) persist
|
|
13
|
-
// across `extro dev` sessions without needing a prod-restore on shutdown.
|
|
14
|
-
const devOutDir = path.join(root, ".output", "chrome-mv3-dev");
|
|
15
|
-
const prodOutDir = path.join(root, ".output", "chrome-mv3-prod");
|
|
16
|
-
const dev = async () => {
|
|
17
|
-
const config = await loadConfig(root);
|
|
18
|
-
// 1. Scan once up front so we can decide what to start.
|
|
19
|
-
const tree = await scanAppTree(root);
|
|
20
|
-
validateTree(tree);
|
|
21
|
-
// 2. Signal WS — the dev bridge in the extension's BG SW connects here.
|
|
22
|
-
// Fixed port (not :0) so the port baked into a previously-loaded BG SW
|
|
23
|
-
// keeps working across `extro dev` restarts — otherwise users have to
|
|
24
|
-
// refresh the extension every time to pick up a new random port.
|
|
25
|
-
const signalPort = 9012;
|
|
26
|
-
const wss = new WebSocketServer({ port: signalPort });
|
|
27
|
-
wss.on("error", (err) => {
|
|
28
|
-
if (err.code === "EADDRINUSE") {
|
|
29
|
-
console.error(`\n[extro] Signal port ${signalPort} is in use — is another \`extro dev\` already running?\n`);
|
|
30
|
-
process.exit(1);
|
|
31
|
-
}
|
|
32
|
-
throw err;
|
|
33
|
-
});
|
|
34
|
-
await once(wss, "listening");
|
|
35
|
-
const broadcast = (msg) => {
|
|
36
|
-
const payload = JSON.stringify(msg);
|
|
37
|
-
for (const client of wss.clients) {
|
|
38
|
-
if (client.readyState === 1)
|
|
39
|
-
client.send(payload);
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
// 3. Vite dev server for routable surfaces.
|
|
43
|
-
// `broadcastHmr` lets the plugin push HMR updates over our signal WS —
|
|
44
|
-
// we can't piggy-back on Vite's own HMR WS because its origin check
|
|
45
|
-
// rejects chrome-extension:// service workers.
|
|
46
|
-
const server = await createServer({
|
|
47
|
-
root,
|
|
48
|
-
plugins: [
|
|
49
|
-
react(),
|
|
50
|
-
extro({
|
|
51
|
-
root,
|
|
52
|
-
config,
|
|
53
|
-
broadcastHmr: (payload) => broadcast({ kind: "vite-hmr", payload }),
|
|
54
|
-
}),
|
|
55
|
-
],
|
|
56
|
-
server: { cors: true },
|
|
57
|
-
});
|
|
58
|
-
await server.listen();
|
|
59
|
-
const addr = server.httpServer?.address();
|
|
60
|
-
const port = addr && typeof addr === "object" ? addr.port : server.config.server.port ?? 5173;
|
|
61
|
-
// 4. Dev manifest + HTML + icons.
|
|
62
|
-
await writeDevAssets({ tree, root, outDir: devOutDir, port, signalPort, config });
|
|
63
|
-
// 5. Build-watch sidecar for background + content. Always runs in dev so
|
|
64
|
-
// the dev bridge gets bundled into background.js (even if the user has
|
|
65
|
-
// no BG of their own).
|
|
66
|
-
const watcher = await viteBuild({
|
|
67
|
-
root,
|
|
68
|
-
plugins: [extro({ root, config, scriptsOnly: true, devBridge: { signalPort } })],
|
|
69
|
-
// emptyOutDir: false so the watcher doesn't wipe the manifest / HTML /
|
|
70
|
-
// icons that writeDevAssets just put down.
|
|
71
|
-
build: { watch: {}, emptyOutDir: false, outDir: devOutDir },
|
|
72
|
-
logLevel: "error",
|
|
73
|
-
});
|
|
74
|
-
// The watcher is a RollupWatcher; bundle events fire on every rebuild
|
|
75
|
-
// (including the first one). Broadcast on each so the bridge reloads.
|
|
76
|
-
if (watcher && typeof watcher.on === "function") {
|
|
77
|
-
;
|
|
78
|
-
watcher.on("event", (event) => {
|
|
79
|
-
if (event.code === "BUNDLE_END") {
|
|
80
|
-
broadcast({ kind: "scripts-rebuilt" });
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
console.log(`\nExtro dev server: http://localhost:${port}`);
|
|
85
|
-
console.log(`Load unpacked extension from: ${devOutDir}\n`);
|
|
86
|
-
let shuttingDown = false;
|
|
87
|
-
const shutdown = async () => {
|
|
88
|
-
if (shuttingDown) {
|
|
89
|
-
console.log("\nAlready shutting down, please wait...");
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
shuttingDown = true;
|
|
93
|
-
console.log("\nShutting down dev server...");
|
|
94
|
-
if (watcher && typeof watcher.close === "function") {
|
|
95
|
-
await watcher.close();
|
|
96
|
-
}
|
|
97
|
-
wss.close();
|
|
98
|
-
await server.close();
|
|
99
|
-
// No prod-restore: dev artifacts live in their own .output/chrome-mv3-dev
|
|
100
|
-
// dir so the loaded extension stays untouched. Run `extro build` for a
|
|
101
|
-
// standalone prod bundle in .output/chrome-mv3-prod.
|
|
102
|
-
process.exit(0);
|
|
103
|
-
};
|
|
104
|
-
process.on("SIGINT", shutdown);
|
|
105
|
-
process.on("SIGTERM", shutdown);
|
|
106
|
-
};
|
|
107
|
-
const validateTree = (tree) => {
|
|
108
|
-
const empty = Object.keys(tree.scripts).length === 0 &&
|
|
109
|
-
Object.keys(tree.surfaces).length === 0;
|
|
110
|
-
if (!empty)
|
|
111
|
-
return;
|
|
112
|
-
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");
|
|
113
|
-
};
|
|
114
|
-
const once = (emitter, event) => new Promise((resolve) => emitter.once(event, () => resolve()));
|
|
115
|
-
const build = async () => {
|
|
116
|
-
console.log("Building extension...");
|
|
117
|
-
const config = await loadConfig(root);
|
|
118
|
-
await viteBuild({
|
|
119
|
-
root,
|
|
120
|
-
plugins: [react(), extro({ root, config })],
|
|
121
|
-
build: { outDir: prodOutDir },
|
|
122
|
-
});
|
|
123
|
-
console.log(`Build complete. Load unpacked extension from: ${prodOutDir}`);
|
|
124
|
-
};
|
|
125
|
-
switch (command) {
|
|
126
|
-
case "dev":
|
|
127
|
-
await dev();
|
|
128
|
-
break;
|
|
129
|
-
case "build":
|
|
130
|
-
await build();
|
|
131
|
-
break;
|
|
132
|
-
case "init":
|
|
133
|
-
console.log("Initializing Extro project...");
|
|
134
|
-
break;
|
|
135
|
-
default:
|
|
136
|
-
console.log("Extro CLI");
|
|
137
|
-
}
|
|
2
|
+
import { run } from "./cli.js";
|
|
3
|
+
run().catch((err) => {
|
|
4
|
+
console.error(err);
|
|
5
|
+
process.exit(1);
|
|
6
|
+
});
|
package/dist/load-config.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { ExtroConfig } from "
|
|
1
|
+
import type { ExtroConfig } from "./types/index.js";
|
|
2
2
|
export declare const loadConfig: (root: string) => Promise<ExtroConfig>;
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type Logger } from "vite";
|
|
2
|
+
interface BannerRow {
|
|
3
|
+
label: string;
|
|
4
|
+
value: string;
|
|
5
|
+
}
|
|
6
|
+
interface BannerOptions {
|
|
7
|
+
mode: string;
|
|
8
|
+
version: string;
|
|
9
|
+
rows: BannerRow[];
|
|
10
|
+
hint?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare const log: {
|
|
13
|
+
success: (msg: string) => void;
|
|
14
|
+
info: (msg: string) => void;
|
|
15
|
+
warn: (msg: string) => void;
|
|
16
|
+
error: (msg: string) => void;
|
|
17
|
+
/** Low-key line for frequent, low-importance output (e.g. HMR updates).
|
|
18
|
+
* Multi-line input is prefixed per line so the `›` rail stays continuous. */
|
|
19
|
+
muted: (msg: string) => void;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* @describe A Vite `customLogger` that rebadges Vite's own info chatter
|
|
23
|
+
* (HMR updates, page reloads, dep optimization, "building for production",
|
|
24
|
+
* bundle sizes, etc.) as Extro `›` muted lines. Warnings + errors still
|
|
25
|
+
* pass through Vite's logger untouched so they keep their semantics.
|
|
26
|
+
*/
|
|
27
|
+
export declare const createViteLogger: () => Logger;
|
|
28
|
+
/**
|
|
29
|
+
* @describe Prints the grouped startup banner: a brand tag with version/mode,
|
|
30
|
+
* then aligned label/value rows and an optional dimmed hint. Mirrors Vite's
|
|
31
|
+
* own startup idiom so the two read as a coherent stack.
|
|
32
|
+
*/
|
|
33
|
+
export declare const banner: ({ mode, version, rows, hint }: BannerOptions) => void;
|
|
34
|
+
export {};
|