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.
Files changed (96) hide show
  1. package/README.md +3 -3
  2. package/client.d.ts +8 -0
  3. package/dist/cli.d.ts +7 -0
  4. package/dist/cli.js +27 -0
  5. package/dist/commands/build.d.ts +5 -0
  6. package/dist/commands/build.js +26 -0
  7. package/dist/commands/dev.d.ts +7 -0
  8. package/dist/commands/dev.js +156 -0
  9. package/dist/config.d.ts +2 -2
  10. package/dist/core/asset.d.ts +10 -0
  11. package/dist/core/asset.js +10 -0
  12. package/dist/dev-assets.d.ts +3 -3
  13. package/dist/dev-assets.js +33 -12
  14. package/dist/env.d.ts +10 -0
  15. package/dist/env.js +18 -0
  16. package/dist/exports/asset.d.ts +1 -0
  17. package/dist/exports/asset.js +1 -0
  18. package/dist/exports/link.d.ts +1 -0
  19. package/dist/exports/link.js +1 -0
  20. package/dist/exports/navigation.d.ts +2 -0
  21. package/dist/exports/navigation.js +1 -0
  22. package/dist/exports/runtime.d.ts +2 -0
  23. package/dist/exports/runtime.js +3 -0
  24. package/dist/index.js +5 -136
  25. package/dist/load-config.d.ts +1 -1
  26. package/dist/logger.d.ts +34 -0
  27. package/dist/logger.js +65 -0
  28. package/dist/paths.d.ts +8 -0
  29. package/dist/paths.js +8 -0
  30. package/dist/pkg.d.ts +6 -0
  31. package/dist/pkg.js +5 -0
  32. package/dist/plugin/app-tree.d.ts +59 -0
  33. package/dist/plugin/app-tree.js +214 -0
  34. package/dist/plugin/asset-inventory.d.ts +24 -0
  35. package/dist/plugin/asset-inventory.js +9 -0
  36. package/dist/plugin/dev-reactions.d.ts +59 -0
  37. package/dist/plugin/dev-reactions.js +62 -0
  38. package/dist/plugin/emit-assets.d.ts +50 -0
  39. package/dist/plugin/emit-assets.js +40 -0
  40. package/dist/plugin/generators/html.d.ts +39 -0
  41. package/dist/plugin/generators/html.js +127 -0
  42. package/dist/plugin/generators/icons.d.ts +15 -0
  43. package/dist/plugin/generators/icons.js +16 -0
  44. package/dist/plugin/generators/public.d.ts +17 -0
  45. package/dist/plugin/generators/public.js +20 -0
  46. package/dist/plugin/icons.d.ts +5 -0
  47. package/dist/plugin/icons.js +20 -0
  48. package/dist/plugin/index.d.ts +31 -0
  49. package/dist/plugin/index.js +246 -0
  50. package/dist/plugin/internal.d.ts +14 -0
  51. package/dist/plugin/internal.js +6 -0
  52. package/dist/plugin/manifest.d.ts +29 -0
  53. package/dist/plugin/manifest.js +68 -0
  54. package/dist/plugin/public.d.ts +21 -0
  55. package/dist/plugin/public.js +63 -0
  56. package/dist/plugin/runtimes/clients/csui-mount.js +90 -0
  57. package/dist/plugin/runtimes/clients/dev-bridge.js +194 -0
  58. package/dist/plugin/runtimes/csui-mount.d.ts +18 -0
  59. package/dist/plugin/runtimes/csui-mount.js +21 -0
  60. package/dist/plugin/runtimes/dev-bridge.d.ts +22 -0
  61. package/dist/plugin/runtimes/dev-bridge.js +19 -0
  62. package/dist/plugin/runtimes/routes-module.d.ts +20 -0
  63. package/dist/plugin/runtimes/routes-module.js +51 -0
  64. package/dist/plugin/runtimes/runtime-module.d.ts +16 -0
  65. package/dist/plugin/runtimes/runtime-module.js +40 -0
  66. package/dist/plugin/surfaces.d.ts +37 -0
  67. package/dist/plugin/surfaces.js +67 -0
  68. package/dist/plugin/types/index.d.ts +9 -0
  69. package/dist/plugin/types/index.js +1 -0
  70. package/dist/plugin/utils/read-json.d.ts +1 -0
  71. package/dist/plugin/utils/read-json.js +8 -0
  72. package/dist/react/env.d.ts +13 -0
  73. package/dist/react/env.js +1 -0
  74. package/dist/router/build-tree.d.ts +46 -0
  75. package/dist/router/build-tree.js +56 -0
  76. package/dist/router/context.d.ts +13 -0
  77. package/dist/router/context.js +2 -0
  78. package/dist/router/create-router.d.ts +10 -0
  79. package/dist/router/create-router.js +126 -0
  80. package/dist/router/defaults.d.ts +24 -0
  81. package/dist/router/defaults.js +25 -0
  82. package/dist/router/error-boundary.d.ts +23 -0
  83. package/dist/router/error-boundary.js +21 -0
  84. package/dist/router/hooks.d.ts +18 -0
  85. package/dist/router/hooks.js +34 -0
  86. package/dist/router/index.d.ts +8 -0
  87. package/dist/router/index.js +7 -0
  88. package/dist/router/link.d.ts +305 -0
  89. package/dist/router/link.js +30 -0
  90. package/dist/router/match.d.ts +14 -0
  91. package/dist/router/match.js +27 -0
  92. package/dist/router/types.d.ts +55 -0
  93. package/dist/router/types.js +1 -0
  94. package/dist/types/index.d.ts +152 -0
  95. package/dist/types/index.js +1 -0
  96. 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 .output/chrome-mv3-dev/
20
- extro build # production build to .output/chrome-mv3-prod/
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 `.output/chrome-mv3-dev/` (or `.output/chrome-mv3-prod/`) in Chrome via **Load Unpacked**.
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,5 @@
1
+ /**
2
+ * @describe Produces a standalone production bundle in
3
+ * <outDir>/chrome-mv3-prod/: manifest, HTML shells, and script bundles.
4
+ */
5
+ export declare const build: () => Promise<void>;
@@ -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 "@extrojs/types";
2
- export type { ExtroConfig } from "@extrojs/types";
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);
@@ -1,5 +1,5 @@
1
- import type { ExtroConfig } from "@extrojs/types";
2
- import { type AppTree } from "@extrojs/vite-plugin/internal";
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) not here.
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 {};
@@ -1,26 +1,47 @@
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, 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) not here.
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
- await emitAssets({ tree, root, pkg, config, dev: { port, signalPort } }, (fileName, source) => fs.writeFile(path.join(outDir, fileName), source));
15
- await copyIcons(root, outDir);
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
- const srcDir = path.join(root, "icons");
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 dstDir = path.join(outDir, "icons");
23
- await fs.mkdir(dstDir, { recursive: true });
24
- const files = await fs.readdir(srcDir);
25
- await Promise.all(files.map((f) => fs.copyFile(path.join(srcDir, f), path.join(dstDir, f))));
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,2 @@
1
+ export { useLocation, useParams, useRouter, useSearchParams, } from "../router/index.js";
2
+ export type { ErrorProps, LayoutProps, PageProps, Router } from "../router/index.js";
@@ -0,0 +1 @@
1
+ export { useLocation, useParams, useRouter, useSearchParams, } from "../router/index.js";
@@ -0,0 +1,2 @@
1
+ export { createExtroRouter, matchRoutes } from "../router/index.js";
2
+ export type { CreateRouterOptions, DynamicRoute, ExtroRouterHandle, Route, RouteMatch, StaticRoute, } from "../router/index.js";
@@ -0,0 +1,3 @@
1
+ // Internal subpath. Emitted by the generated Runtime module
2
+ // (`virtual:extro/runtime/<surface>`), not for direct user import.
3
+ export { createExtroRouter, matchRoutes } from "../router/index.js";
package/dist/index.js CHANGED
@@ -1,137 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import path from "node:path";
3
- import { createServer, build as viteBuild } from "vite";
4
- import { WebSocketServer } from "ws";
5
- import { extro } from "@extrojs/vite-plugin";
6
- import react from "@vitejs/plugin-react";
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
+ });
@@ -1,2 +1,2 @@
1
- import type { ExtroConfig } from "@extrojs/types";
1
+ import type { ExtroConfig } from "./types/index.js";
2
2
  export declare const loadConfig: (root: string) => Promise<ExtroConfig>;
@@ -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 {};