extrojs 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/client.d.ts +16 -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 +173 -0
- package/dist/dev-assets.js +20 -1
- package/dist/env.d.ts +10 -0
- package/dist/env.js +18 -0
- package/dist/index.js +5 -136
- 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/package.json +12 -6
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,16 @@
|
|
|
1
|
+
// Ambient types for Extro's env. Opt in from a project by adding:
|
|
2
|
+
// /// <reference types="extrojs/client" />
|
|
3
|
+
// Public env vars (EXTRO_PUBLIC_*) are inlined into every surface via
|
|
4
|
+
// import.meta.env. See ADR 0002.
|
|
5
|
+
|
|
6
|
+
interface ImportMetaEnv {
|
|
7
|
+
readonly MODE: string
|
|
8
|
+
readonly DEV: boolean
|
|
9
|
+
readonly PROD: boolean
|
|
10
|
+
readonly BASE_URL: string
|
|
11
|
+
readonly [key: `EXTRO_PUBLIC_${string}`]: string | undefined
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ImportMeta {
|
|
15
|
+
readonly env: ImportMetaEnv
|
|
16
|
+
}
|
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 "@extrojs/vite-plugin";
|
|
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,173 @@
|
|
|
1
|
+
import { createServer, build as viteBuild } from "vite";
|
|
2
|
+
import { WebSocketServer } from "ws";
|
|
3
|
+
import { extro } from "@extrojs/vite-plugin";
|
|
4
|
+
import react from "@vitejs/plugin-react";
|
|
5
|
+
import { scanAppTree } from "@extrojs/vite-plugin/internal";
|
|
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
|
|
107
|
+
// CSUI, and a content-only edit must not reload the extension. `change`
|
|
108
|
+
// events (one per changed file) accumulate until the next `BUNDLE_END`;
|
|
109
|
+
// a file outside both surface dirs (shared code) conservatively dirties
|
|
110
|
+
// both. No classified change (initial build, or an `extro dev` restart)
|
|
111
|
+
// also means both, matching the old broadcast-on-first-build behavior.
|
|
112
|
+
if (watcher && typeof watcher.on === "function") {
|
|
113
|
+
const w = watcher;
|
|
114
|
+
let bgDirty = false;
|
|
115
|
+
let csDirty = false;
|
|
116
|
+
w.on("change", (id) => {
|
|
117
|
+
const p = String(id).replace(/\\/g, "/");
|
|
118
|
+
const isBg = p.includes("/src/app/background/");
|
|
119
|
+
const isCs = p.includes("/src/app/content/");
|
|
120
|
+
if (isBg)
|
|
121
|
+
bgDirty = true;
|
|
122
|
+
if (isCs)
|
|
123
|
+
csDirty = true;
|
|
124
|
+
if (!isBg && !isCs) {
|
|
125
|
+
bgDirty = true;
|
|
126
|
+
csDirty = true;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
w.on("event", (event) => {
|
|
130
|
+
if (event.code !== "BUNDLE_END")
|
|
131
|
+
return;
|
|
132
|
+
if (!bgDirty && !csDirty) {
|
|
133
|
+
bgDirty = true;
|
|
134
|
+
csDirty = true;
|
|
135
|
+
}
|
|
136
|
+
if (bgDirty)
|
|
137
|
+
broadcast({ kind: "bg-rebuilt" });
|
|
138
|
+
if (csDirty)
|
|
139
|
+
broadcast({ kind: "cs-rebuilt" });
|
|
140
|
+
bgDirty = false;
|
|
141
|
+
csDirty = false;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
banner({
|
|
145
|
+
mode: "dev",
|
|
146
|
+
version: pkg.version,
|
|
147
|
+
rows: [
|
|
148
|
+
{ label: "Server", value: `http://localhost:${port}` },
|
|
149
|
+
{ label: "Unpacked", value: devOutDir },
|
|
150
|
+
],
|
|
151
|
+
hint: 'Load the unpacked dir in Chrome via "Load Unpacked".',
|
|
152
|
+
});
|
|
153
|
+
let shuttingDown = false;
|
|
154
|
+
const shutdown = async () => {
|
|
155
|
+
if (shuttingDown)
|
|
156
|
+
return;
|
|
157
|
+
shuttingDown = true;
|
|
158
|
+
process.off("SIGINT", shutdown);
|
|
159
|
+
process.off("SIGTERM", shutdown);
|
|
160
|
+
log.info("Shutting down dev server...");
|
|
161
|
+
if (watcher && typeof watcher.close === "function") {
|
|
162
|
+
await watcher.close();
|
|
163
|
+
}
|
|
164
|
+
wss.close();
|
|
165
|
+
await server.close();
|
|
166
|
+
// No prod-restore: dev artifacts live in their own output/chrome-mv3-dev
|
|
167
|
+
// dir so the loaded extension stays untouched. Run `extro build` for a
|
|
168
|
+
// standalone prod bundle in output/chrome-mv3-prod.
|
|
169
|
+
process.exit(0);
|
|
170
|
+
};
|
|
171
|
+
process.on("SIGINT", shutdown);
|
|
172
|
+
process.on("SIGTERM", shutdown);
|
|
173
|
+
};
|
package/dist/dev-assets.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { emitAssets, } from "@extrojs/vite-plugin/internal";
|
|
3
|
+
import { emitAssets, collectPublicAssets, } from "@extrojs/vite-plugin/internal";
|
|
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
|
|
@@ -13,6 +13,7 @@ export const writeDevAssets = async ({ tree, root, outDir, port, signalPort, con
|
|
|
13
13
|
const pkg = JSON.parse(pkgRaw);
|
|
14
14
|
await emitAssets({ tree, root, pkg, config, dev: { port, signalPort } }, (fileName, source) => fs.writeFile(path.join(outDir, fileName), source));
|
|
15
15
|
await copyIcons(root, outDir);
|
|
16
|
+
await copyPublic(root, outDir, tree);
|
|
16
17
|
};
|
|
17
18
|
const copyIcons = async (root, outDir) => {
|
|
18
19
|
const srcDir = path.join(root, "icons");
|
|
@@ -24,3 +25,21 @@ const copyIcons = async (root, outDir) => {
|
|
|
24
25
|
const files = await fs.readdir(srcDir);
|
|
25
26
|
await Promise.all(files.map((f) => fs.copyFile(path.join(srcDir, f), path.join(dstDir, f))));
|
|
26
27
|
};
|
|
28
|
+
/**
|
|
29
|
+
* @describe Copies Public assets into the dev output dir so they resolve at
|
|
30
|
+
* the extension origin in dev exactly as in prod (chrome.runtime.getURL, or a
|
|
31
|
+
* root-relative ref on a routable surface). Mirrors copyIcons; the collision
|
|
32
|
+
* guard from collectPublicAssets keeps a stray file from shadowing a
|
|
33
|
+
* generated output.
|
|
34
|
+
*/
|
|
35
|
+
const copyPublic = async (root, outDir, tree) => {
|
|
36
|
+
const { files, conflicts } = collectPublicAssets(root, tree);
|
|
37
|
+
for (const conflict of conflicts) {
|
|
38
|
+
console.warn(`[extro] public/${conflict} collides with a generated output; skipping. Rename it to ship it.`);
|
|
39
|
+
}
|
|
40
|
+
await Promise.all(files.map(async (rel) => {
|
|
41
|
+
const dst = path.join(outDir, rel);
|
|
42
|
+
await fs.mkdir(path.dirname(dst), { recursive: true });
|
|
43
|
+
await fs.copyFile(path.join(root, "public", rel), dst);
|
|
44
|
+
}));
|
|
45
|
+
};
|
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
|
+
};
|
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/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 {};
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import { createLogger } from "vite";
|
|
3
|
+
// Extro brand terracotta (#CC785C) on the logo's near-black (#0a0a0a).
|
|
4
|
+
// picocolors only does the 16 ANSI names, so emit 24-bit truecolor directly,
|
|
5
|
+
// gated on the same color-support check picocolors uses (auto-plain in pipes).
|
|
6
|
+
const brand = (s) => pc.isColorSupported ? `\x1b[38;2;204;120;92m${s}\x1b[39m` : s;
|
|
7
|
+
const brandTag = (s) => pc.isColorSupported
|
|
8
|
+
? `\x1b[1m\x1b[48;2;204;120;92m\x1b[38;2;10;10;10m${s}\x1b[0m`
|
|
9
|
+
: s;
|
|
10
|
+
const tag = brandTag(" EXTRO ");
|
|
11
|
+
// Status prefixes stay conventional (green/yellow/red are universal terminal
|
|
12
|
+
// semantics); the Extro "voice" (tag, `›`, accents) carries the brand color.
|
|
13
|
+
export const log = {
|
|
14
|
+
success: (msg) => console.log(`${pc.green("✓")} ${msg}`),
|
|
15
|
+
info: (msg) => console.log(`${brand("›")} ${msg}`),
|
|
16
|
+
warn: (msg) => console.warn(`${pc.yellow("⚠")} ${msg}`),
|
|
17
|
+
error: (msg) => console.error(`${pc.red("✗")} ${msg}`),
|
|
18
|
+
/** Low-key line for frequent, low-importance output (e.g. HMR updates).
|
|
19
|
+
* Multi-line input is prefixed per line so the `›` rail stays continuous. */
|
|
20
|
+
muted: (msg) => {
|
|
21
|
+
for (const line of msg.split("\n"))
|
|
22
|
+
console.log(pc.dim(`› ${line}`));
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* @describe A Vite `customLogger` that rebadges Vite's own info chatter
|
|
27
|
+
* (HMR updates, page reloads, dep optimization, "building for production",
|
|
28
|
+
* bundle sizes, etc.) as Extro `›` muted lines. Warnings + errors still
|
|
29
|
+
* pass through Vite's logger untouched so they keep their semantics.
|
|
30
|
+
*/
|
|
31
|
+
export const createViteLogger = () => {
|
|
32
|
+
const vite = createLogger();
|
|
33
|
+
vite.info = (msg) => {
|
|
34
|
+
const clean = msg
|
|
35
|
+
.replace(/\x1b\[[0-9;]*m/g, "")
|
|
36
|
+
.replace(/^[\d:apm.\s]*\[vite\]\s*/i, "")
|
|
37
|
+
.trim();
|
|
38
|
+
if (!clean)
|
|
39
|
+
return;
|
|
40
|
+
// Drop Vite's own startup banner — we already print "Building extension
|
|
41
|
+
// for production..." / the dev banner ourselves.
|
|
42
|
+
if (/^vite v[\d.]+ (building|dev server running)/i.test(clean))
|
|
43
|
+
return;
|
|
44
|
+
log.muted(clean);
|
|
45
|
+
};
|
|
46
|
+
return vite;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* @describe Prints the grouped startup banner: a brand tag with version/mode,
|
|
50
|
+
* then aligned label/value rows and an optional dimmed hint. Mirrors Vite's
|
|
51
|
+
* own startup idiom so the two read as a coherent stack.
|
|
52
|
+
*/
|
|
53
|
+
export const banner = ({ mode, version, rows, hint }) => {
|
|
54
|
+
const width = Math.max(...rows.map((row) => row.label.length));
|
|
55
|
+
const lines = [
|
|
56
|
+
"",
|
|
57
|
+
` ${tag} ${pc.dim(`v${version}`)} ${brand(mode)}`,
|
|
58
|
+
"",
|
|
59
|
+
...rows.map((row) => ` ${brand("➜")} ${row.label.padEnd(width)} ${brand(row.value)}`),
|
|
60
|
+
];
|
|
61
|
+
if (hint)
|
|
62
|
+
lines.push("", ` ${pc.dim(hint)}`);
|
|
63
|
+
lines.push("");
|
|
64
|
+
console.log(lines.join("\n"));
|
|
65
|
+
};
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ExtroConfig } from "@extrojs/types";
|
|
2
|
+
/**
|
|
3
|
+
* @describe Resolved unpacked-extension output dir for a build mode. The base
|
|
4
|
+
* comes from `config.outDir` (default `output`, resolved against the project
|
|
5
|
+
* root); the `chrome-mv3-<mode>` subdir keeps dev and prod artifacts separate
|
|
6
|
+
* and leaves room for other targets later.
|
|
7
|
+
*/
|
|
8
|
+
export declare const outputDir: (root: string, config: ExtroConfig, mode: "dev" | "prod") => string;
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* @describe Resolved unpacked-extension output dir for a build mode. The base
|
|
4
|
+
* comes from `config.outDir` (default `output`, resolved against the project
|
|
5
|
+
* root); the `chrome-mv3-<mode>` subdir keeps dev and prod artifacts separate
|
|
6
|
+
* and leaves room for other targets later.
|
|
7
|
+
*/
|
|
8
|
+
export const outputDir = (root, config, mode) => path.join(path.resolve(root, config.outDir ?? "output"), `chrome-mv3-${mode}`);
|
package/dist/pkg.d.ts
ADDED
package/dist/pkg.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
// Read at runtime rather than `import`ing package.json: it lives outside
|
|
3
|
+
// `rootDir: src`, so tsc would reject a static import. The URL resolves the
|
|
4
|
+
// same in the workspace (dist/pkg.js -> ../package.json) and once published.
|
|
5
|
+
export const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "extrojs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Next.js for Chrome extensions. File-based entrypoints, automatic Manifest V3 generation, and React routing in a single Vite-powered CLI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"extro",
|
|
@@ -29,13 +29,17 @@
|
|
|
29
29
|
".": {
|
|
30
30
|
"types": "./dist/config.d.ts",
|
|
31
31
|
"import": "./dist/config.js"
|
|
32
|
+
},
|
|
33
|
+
"./client": {
|
|
34
|
+
"types": "./client.d.ts"
|
|
32
35
|
}
|
|
33
36
|
},
|
|
34
37
|
"bin": {
|
|
35
38
|
"extro": "./dist/index.js"
|
|
36
39
|
},
|
|
37
40
|
"files": [
|
|
38
|
-
"dist"
|
|
41
|
+
"dist",
|
|
42
|
+
"client.d.ts"
|
|
39
43
|
],
|
|
40
44
|
"publishConfig": {
|
|
41
45
|
"access": "public"
|
|
@@ -45,12 +49,13 @@
|
|
|
45
49
|
},
|
|
46
50
|
"dependencies": {
|
|
47
51
|
"@vitejs/plugin-react": "^6.0.1",
|
|
52
|
+
"cac": "^7.0.0",
|
|
48
53
|
"jiti": "^2.4.2",
|
|
54
|
+
"picocolors": "^1.1.1",
|
|
49
55
|
"vite": "^8.0.0",
|
|
50
56
|
"ws": "^8.20.0",
|
|
51
|
-
"@extrojs/
|
|
52
|
-
"@extrojs/vite-plugin": "0.
|
|
53
|
-
"@extrojs/types": "0.1.0"
|
|
57
|
+
"@extrojs/types": "0.2.0",
|
|
58
|
+
"@extrojs/vite-plugin": "0.2.0"
|
|
54
59
|
},
|
|
55
60
|
"devDependencies": {
|
|
56
61
|
"@types/ws": "^8.18.1"
|
|
@@ -58,6 +63,7 @@
|
|
|
58
63
|
"scripts": {
|
|
59
64
|
"build": "tsc",
|
|
60
65
|
"dev": "tsc -w",
|
|
61
|
-
"typecheck": "tsc --noEmit"
|
|
66
|
+
"typecheck": "tsc --noEmit",
|
|
67
|
+
"test": "vitest run"
|
|
62
68
|
}
|
|
63
69
|
}
|