extrojs 0.2.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 (83) hide show
  1. package/client.d.ts +7 -15
  2. package/dist/commands/build.js +1 -1
  3. package/dist/commands/dev.js +12 -29
  4. package/dist/config.d.ts +2 -2
  5. package/dist/core/asset.d.ts +10 -0
  6. package/dist/core/asset.js +10 -0
  7. package/dist/dev-assets.d.ts +3 -3
  8. package/dist/dev-assets.js +18 -16
  9. package/dist/exports/asset.d.ts +1 -0
  10. package/dist/exports/asset.js +1 -0
  11. package/dist/exports/link.d.ts +1 -0
  12. package/dist/exports/link.js +1 -0
  13. package/dist/exports/navigation.d.ts +2 -0
  14. package/dist/exports/navigation.js +1 -0
  15. package/dist/exports/runtime.d.ts +2 -0
  16. package/dist/exports/runtime.js +3 -0
  17. package/dist/load-config.d.ts +1 -1
  18. package/dist/paths.d.ts +1 -1
  19. package/dist/plugin/app-tree.d.ts +59 -0
  20. package/dist/plugin/app-tree.js +214 -0
  21. package/dist/plugin/asset-inventory.d.ts +24 -0
  22. package/dist/plugin/asset-inventory.js +9 -0
  23. package/dist/plugin/dev-reactions.d.ts +59 -0
  24. package/dist/plugin/dev-reactions.js +62 -0
  25. package/dist/plugin/emit-assets.d.ts +50 -0
  26. package/dist/plugin/emit-assets.js +40 -0
  27. package/dist/plugin/generators/html.d.ts +39 -0
  28. package/dist/plugin/generators/html.js +127 -0
  29. package/dist/plugin/generators/icons.d.ts +15 -0
  30. package/dist/plugin/generators/icons.js +16 -0
  31. package/dist/plugin/generators/public.d.ts +17 -0
  32. package/dist/plugin/generators/public.js +20 -0
  33. package/dist/plugin/icons.d.ts +5 -0
  34. package/dist/plugin/icons.js +20 -0
  35. package/dist/plugin/index.d.ts +31 -0
  36. package/dist/plugin/index.js +246 -0
  37. package/dist/plugin/internal.d.ts +14 -0
  38. package/dist/plugin/internal.js +6 -0
  39. package/dist/plugin/manifest.d.ts +29 -0
  40. package/dist/plugin/manifest.js +68 -0
  41. package/dist/plugin/public.d.ts +21 -0
  42. package/dist/plugin/public.js +63 -0
  43. package/dist/plugin/runtimes/clients/csui-mount.js +90 -0
  44. package/dist/plugin/runtimes/clients/dev-bridge.js +194 -0
  45. package/dist/plugin/runtimes/csui-mount.d.ts +18 -0
  46. package/dist/plugin/runtimes/csui-mount.js +21 -0
  47. package/dist/plugin/runtimes/dev-bridge.d.ts +22 -0
  48. package/dist/plugin/runtimes/dev-bridge.js +19 -0
  49. package/dist/plugin/runtimes/routes-module.d.ts +20 -0
  50. package/dist/plugin/runtimes/routes-module.js +51 -0
  51. package/dist/plugin/runtimes/runtime-module.d.ts +16 -0
  52. package/dist/plugin/runtimes/runtime-module.js +40 -0
  53. package/dist/plugin/surfaces.d.ts +37 -0
  54. package/dist/plugin/surfaces.js +67 -0
  55. package/dist/plugin/types/index.d.ts +9 -0
  56. package/dist/plugin/types/index.js +1 -0
  57. package/dist/plugin/utils/read-json.d.ts +1 -0
  58. package/dist/plugin/utils/read-json.js +8 -0
  59. package/dist/react/env.d.ts +13 -0
  60. package/dist/react/env.js +1 -0
  61. package/dist/router/build-tree.d.ts +46 -0
  62. package/dist/router/build-tree.js +56 -0
  63. package/dist/router/context.d.ts +13 -0
  64. package/dist/router/context.js +2 -0
  65. package/dist/router/create-router.d.ts +10 -0
  66. package/dist/router/create-router.js +126 -0
  67. package/dist/router/defaults.d.ts +24 -0
  68. package/dist/router/defaults.js +25 -0
  69. package/dist/router/error-boundary.d.ts +23 -0
  70. package/dist/router/error-boundary.js +21 -0
  71. package/dist/router/hooks.d.ts +18 -0
  72. package/dist/router/hooks.js +34 -0
  73. package/dist/router/index.d.ts +8 -0
  74. package/dist/router/index.js +7 -0
  75. package/dist/router/link.d.ts +305 -0
  76. package/dist/router/link.js +30 -0
  77. package/dist/router/match.d.ts +14 -0
  78. package/dist/router/match.js +27 -0
  79. package/dist/router/types.d.ts +55 -0
  80. package/dist/router/types.js +1 -0
  81. package/dist/types/index.d.ts +152 -0
  82. package/dist/types/index.js +1 -0
  83. package/package.json +39 -7
@@ -0,0 +1,62 @@
1
+ import { routeManifest } from "./app-tree.js";
2
+ /**
3
+ * A changed file under `src/app/background/` dirties background; under
4
+ * `src/app/content/` dirties content; anything else (shared code) dirties both.
5
+ * This is the only place that knows the `src/app` layout, so the CLI no longer
6
+ * hardcodes those paths.
7
+ */
8
+ export function classifyScriptChange(changedPath) {
9
+ const p = changedPath.replace(/\\/g, "/");
10
+ const background = p.includes("/src/app/background/");
11
+ const content = p.includes("/src/app/content/");
12
+ if (!background && !content)
13
+ return { background: true, content: true };
14
+ return { background, content };
15
+ }
16
+ /** Union two dirty states (accumulating `change` events across one build). */
17
+ export function mergeDirty(a, b) {
18
+ return {
19
+ background: a.background || b.background,
20
+ content: a.content || b.content,
21
+ };
22
+ }
23
+ /**
24
+ * Resolve the accumulated dirty state at `BUNDLE_END`. No classified change
25
+ * (the initial build, or an `extro dev` restart) conservatively means both,
26
+ * matching the broadcast-on-first-build behavior.
27
+ */
28
+ export function resolveFlush(dirty) {
29
+ if (!dirty.background && !dirty.content) {
30
+ return { background: true, content: true };
31
+ }
32
+ return { background: dirty.background, content: dirty.content };
33
+ }
34
+ /**
35
+ * Diff two AppTrees into a {@link DevReaction}. A birth short-circuits the
36
+ * invalidate scan: the session has to restart regardless of any route delta.
37
+ */
38
+ export function decideTreeReaction(prev, next, routables) {
39
+ if (!prev.scripts.background && next.scripts.background) {
40
+ return { kind: "restart", surface: "background" };
41
+ }
42
+ if (!prev.scripts.content && next.scripts.content) {
43
+ return { kind: "restart", surface: "content" };
44
+ }
45
+ for (const surface of routables) {
46
+ const had = (prev.surfaces[surface]?.routes.length ?? 0) > 0;
47
+ const has = (next.surfaces[surface]?.routes.length ?? 0) > 0;
48
+ if (has && !had)
49
+ return { kind: "restart", surface };
50
+ }
51
+ // The Route manifest IS what the routes module emits and is fully
52
+ // serializable, so its stable stringify is a faithful identity: invalidation
53
+ // can no longer drift from the contract (ADR 0005). This is what kills the
54
+ // historical HMR-drift bug class.
55
+ const changed = [];
56
+ for (const surface of routables) {
57
+ const key = (t) => JSON.stringify(routeManifest(t, surface));
58
+ if (key(prev) !== key(next))
59
+ changed.push(surface);
60
+ }
61
+ return changed.length ? { kind: "invalidate", surfaces: changed } : { kind: "noop" };
62
+ }
@@ -0,0 +1,50 @@
1
+ import type { ExtroConfig, ManifestV3 } from "../types/index.js";
2
+ import type { AppTree } from "./app-tree.js";
3
+ import type { AssetInventory } from "./asset-inventory.js";
4
+ import type { RoutableSurface } from "./surfaces.js";
5
+ /**
6
+ * Receives one finished asset at a time. Sync (Vite's `emitFile`) and async
7
+ * (`fs.writeFile`) sinks both fit — callers that await the seam will see
8
+ * any rejection from the async path.
9
+ */
10
+ export type EmitSink = (fileName: string, source: string) => void | Promise<void>;
11
+ export interface AssetOptions {
12
+ tree: AppTree;
13
+ /**
14
+ * The build's discovered icons + Public assets. Both call paths
15
+ * (`generateBundle`, `writeDevAssets`) run `discoverAssets` once at their
16
+ * filesystem edge and pass the result in, so composition stays pure.
17
+ */
18
+ inventory: AssetInventory;
19
+ pkg: {
20
+ name?: string;
21
+ description?: string;
22
+ version?: string;
23
+ };
24
+ config: ExtroConfig;
25
+ dev?: {
26
+ port: number;
27
+ signalPort: number;
28
+ };
29
+ }
30
+ export interface Artifacts {
31
+ manifest: ManifestV3;
32
+ html: Partial<Record<RoutableSurface, string>>;
33
+ }
34
+ /**
35
+ * Pure projection from inputs to artifact strings: no filesystem access (the
36
+ * Asset inventory is discovered at the caller's edge and passed in). Exposed
37
+ * alongside `emitAssets` so tests can assert "given this tree + inventory, this
38
+ * is the manifest" without a sink and without staging files on disk.
39
+ */
40
+ export declare function composeArtifacts(opts: AssetOptions): Artifacts;
41
+ /**
42
+ * Canonical emission of Extro's static outputs (manifest + per-surface HTML).
43
+ * Both the build (`generateBundle`) and dev (`writeDevAssets`) call paths go
44
+ * through here — the only difference between them is the sink.
45
+ *
46
+ * Icons live outside this seam: they are emitted once during build via
47
+ * Rollup's binary-asset path and are not needed in dev (the initial
48
+ * `viteBuild` already wrote them to disk).
49
+ */
50
+ export declare function emitAssets(opts: AssetOptions, emit: EmitSink): Promise<void>;
@@ -0,0 +1,40 @@
1
+ import { generateManifest } from "./manifest.js";
2
+ import { generateHTML } from "./generators/html.js";
3
+ // ---------------------------------------------------------------------------
4
+ // Public API
5
+ // ---------------------------------------------------------------------------
6
+ /**
7
+ * Pure projection from inputs to artifact strings: no filesystem access (the
8
+ * Asset inventory is discovered at the caller's edge and passed in). Exposed
9
+ * alongside `emitAssets` so tests can assert "given this tree + inventory, this
10
+ * is the manifest" without a sink and without staging files on disk.
11
+ */
12
+ export function composeArtifacts(opts) {
13
+ const manifest = generateManifest(opts);
14
+ const html = {};
15
+ for (const surface of Object.keys(opts.tree.surfaces)) {
16
+ html[surface] = generateHTML({
17
+ surface,
18
+ dev: opts.dev ? { port: opts.dev.port } : undefined,
19
+ });
20
+ }
21
+ return { manifest, html };
22
+ }
23
+ /**
24
+ * Canonical emission of Extro's static outputs (manifest + per-surface HTML).
25
+ * Both the build (`generateBundle`) and dev (`writeDevAssets`) call paths go
26
+ * through here — the only difference between them is the sink.
27
+ *
28
+ * Icons live outside this seam: they are emitted once during build via
29
+ * Rollup's binary-asset path and are not needed in dev (the initial
30
+ * `viteBuild` already wrote them to disk).
31
+ */
32
+ export async function emitAssets(opts, emit) {
33
+ const { manifest, html } = composeArtifacts(opts);
34
+ await emit("manifest.json", JSON.stringify(manifest, null, 2));
35
+ for (const [surface, body] of Object.entries(html)) {
36
+ if (!body)
37
+ continue;
38
+ await emit(`${surface}.html`, body);
39
+ }
40
+ }
@@ -0,0 +1,39 @@
1
+ interface GenerateHTMLOptions {
2
+ surface: string;
3
+ dev?: {
4
+ port: number;
5
+ };
6
+ }
7
+ /**
8
+ * @describe Generates the HTML shell for a surface. In dev mode, the shell
9
+ * points at the Vite dev server (with @vite/client for HMR) instead of the
10
+ * built bundle, and pre-renders a brand-styled "dev server offline" screen
11
+ * inside #root. React's createRoot replaces #root's children on mount, so
12
+ * the screen vanishes on a normal start; if the dev server never comes up,
13
+ * the screen stays visible.
14
+ */
15
+ export declare function generateHTML({ surface, dev }: GenerateHTMLOptions): string;
16
+ export interface DevScreen {
17
+ title: string;
18
+ /** HTML body content (paragraphs, code, etc.). Inserted as-is. */
19
+ body: string;
20
+ /**
21
+ * When true, the screen fills the viewport and centers its card. Use for
22
+ * surfaces with a real viewport (options tab, sidepanel). When false
23
+ * (popup), the screen sizes to content with a fixed minimum.
24
+ */
25
+ fill?: boolean;
26
+ }
27
+ /**
28
+ * @describe Renders a brand-styled "developer screen" card. Used as the
29
+ * pre-render inside #root for offline-dev-server fallback today, intended
30
+ * for reuse in other build-time / runtime dev panels.
31
+ */
32
+ export declare const renderDevScreen: ({ title, body, fill }: DevScreen) => string;
33
+ /**
34
+ * @describe Global styles for any `.extro-dev-screen` rendered in dev. Body
35
+ * is painted dark only while a screen is in the DOM (via `:has()`), so the
36
+ * user's app reverts to default styling once React mounts.
37
+ */
38
+ export declare const devScreenStyles: () => string;
39
+ export {};
@@ -0,0 +1,127 @@
1
+ /**
2
+ * @describe Generates the HTML shell for a surface. In dev mode, the shell
3
+ * points at the Vite dev server (with @vite/client for HMR) instead of the
4
+ * built bundle, and pre-renders a brand-styled "dev server offline" screen
5
+ * inside #root. React's createRoot replaces #root's children on mount, so
6
+ * the screen vanishes on a normal start; if the dev server never comes up,
7
+ * the screen stays visible.
8
+ */
9
+ export function generateHTML({ surface, dev }) {
10
+ const title = surface.charAt(0).toUpperCase() + surface.slice(1);
11
+ const scripts = dev
12
+ ? `
13
+ <script type="module" src="http://localhost:${dev.port}/@vite/client"></script>
14
+ <script type="module" src="http://localhost:${dev.port}/@id/@vitejs/plugin-react/preamble"></script>
15
+ <script type="module" src="http://localhost:${dev.port}/@id/virtual:extro/runtime/${surface}"></script>
16
+ `
17
+ : `<script type="module" src="./${surface}.js"></script>`;
18
+ const rootContent = dev
19
+ ? renderDevScreen({
20
+ title: "Dev server isn't running",
21
+ body: `
22
+ <p>Start it with <code>extro dev</code>, then reopen this surface.</p>
23
+ <p>Expected at <code>http://localhost:${dev.port}</code>.</p>
24
+ `,
25
+ // Popups size to content (no viewport to fill); everything else
26
+ // (options/sidepanel/tab) gets a full-viewport centered layout.
27
+ fill: surface !== "popup",
28
+ })
29
+ : "";
30
+ return `
31
+ <!doctype html>
32
+ <html>
33
+ <head>
34
+ <title>${title}</title>
35
+ ${dev ? devScreenStyles() : ""}
36
+ </head>
37
+ <body>
38
+ <div id="root">${rootContent}</div>
39
+ ${scripts.trim()}
40
+ </body>
41
+ </html>
42
+ `.trim();
43
+ }
44
+ /**
45
+ * @describe Renders a brand-styled "developer screen" card. Used as the
46
+ * pre-render inside #root for offline-dev-server fallback today, intended
47
+ * for reuse in other build-time / runtime dev panels.
48
+ */
49
+ export const renderDevScreen = ({ title, body, fill = false }) => {
50
+ const cls = `extro-dev-screen${fill ? " extro-dev-screen--fill" : ""}`;
51
+ return `
52
+ <div class="${cls}">
53
+ <div class="extro-dev-screen__card">
54
+ <span class="extro-dev-screen__tag">EXTRO</span>
55
+ <h1>${title}</h1>
56
+ ${body.trim()}
57
+ </div>
58
+ </div>
59
+ `.trim();
60
+ };
61
+ /**
62
+ * @describe Global styles for any `.extro-dev-screen` rendered in dev. Body
63
+ * is painted dark only while a screen is in the DOM (via `:has()`), so the
64
+ * user's app reverts to default styling once React mounts.
65
+ */
66
+ export const devScreenStyles = () => `
67
+ <style>
68
+ body { margin: 0; }
69
+ body:has(.extro-dev-screen) { background: #0a0a0a; }
70
+ .extro-dev-screen {
71
+ box-sizing: border-box;
72
+ min-width: 360px;
73
+ min-height: 240px;
74
+ padding: 24px 28px;
75
+ background: #0a0a0a;
76
+ display: flex;
77
+ align-items: center;
78
+ justify-content: center;
79
+ opacity: 0;
80
+ animation: extro-dev-screen-in 0.18s 0.05s forwards;
81
+ }
82
+ .extro-dev-screen--fill {
83
+ min-height: 100vh;
84
+ }
85
+ .extro-dev-screen__card {
86
+ box-sizing: border-box;
87
+ width: 100%;
88
+ max-width: 480px;
89
+ display: flex;
90
+ flex-direction: column;
91
+ gap: 10px;
92
+ font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
93
+ color: #e5e5e5;
94
+ }
95
+ .extro-dev-screen__tag {
96
+ align-self: flex-start;
97
+ font-size: 11px;
98
+ font-weight: 600;
99
+ letter-spacing: 0.08em;
100
+ padding: 3px 8px;
101
+ color: #0a0a0a;
102
+ background: #CC785C;
103
+ border-radius: 3px;
104
+ }
105
+ .extro-dev-screen h1 {
106
+ margin: 4px 0 0;
107
+ font-size: 15px;
108
+ font-weight: 600;
109
+ color: #fafafa;
110
+ }
111
+ .extro-dev-screen p {
112
+ margin: 0;
113
+ font-size: 13px;
114
+ line-height: 1.5;
115
+ color: #a3a3a3;
116
+ }
117
+ .extro-dev-screen code {
118
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
119
+ font-size: 12px;
120
+ padding: 1px 6px;
121
+ color: #CC785C;
122
+ background: #1a1a1a;
123
+ border-radius: 3px;
124
+ }
125
+ @keyframes extro-dev-screen-in { to { opacity: 1; } }
126
+ </style>
127
+ `.trim();
@@ -0,0 +1,15 @@
1
+ import type { PluginContextLike } from "../types/index.js";
2
+ interface EmitIconsOptions {
3
+ ctx: PluginContextLike;
4
+ root: string;
5
+ /** The recognized icon set from the Asset inventory (size -> "icons/16.png"). */
6
+ icons: Record<string, string> | null;
7
+ }
8
+ /**
9
+ * @file generators/icons.ts
10
+ * @description Emits the recognized extension icons named by the Asset
11
+ * inventory. Discovery already decided which sizes exist, so this only reads
12
+ * bytes: what ships is exactly what `manifest.icons` references.
13
+ */
14
+ export declare function emitIcons({ ctx, root, icons }: EmitIconsOptions): void;
15
+ export {};
@@ -0,0 +1,16 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ /**
4
+ * @file generators/icons.ts
5
+ * @description Emits the recognized extension icons named by the Asset
6
+ * inventory. Discovery already decided which sizes exist, so this only reads
7
+ * bytes: what ships is exactly what `manifest.icons` references.
8
+ */
9
+ export function emitIcons({ ctx, root, icons }) {
10
+ if (!icons)
11
+ return;
12
+ for (const rel of Object.values(icons)) {
13
+ const source = fs.readFileSync(path.join(root, rel));
14
+ ctx.emitFile({ type: "asset", fileName: rel, source });
15
+ }
16
+ }
@@ -0,0 +1,17 @@
1
+ import type { PluginContextLike } from "../types/index.js";
2
+ import { type PublicAssets } from "../public.js";
3
+ interface EmitPublicAssetsOptions {
4
+ ctx: PluginContextLike;
5
+ root: string;
6
+ /** The partitioned Public assets from the Asset inventory. */
7
+ publicAssets: PublicAssets;
8
+ }
9
+ /**
10
+ * @file generators/public.ts
11
+ * @description Emits Public assets into the build output with their original
12
+ * names, from the partition the Asset inventory already computed. Mirrors
13
+ * `emitIcons`; the collision guard skips any file that would overwrite a
14
+ * generated output and warns instead.
15
+ */
16
+ export declare function emitPublicAssets({ ctx, root, publicAssets }: EmitPublicAssetsOptions): void;
17
+ export {};
@@ -0,0 +1,20 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { PUBLIC_DIR } from "../public.js";
4
+ /**
5
+ * @file generators/public.ts
6
+ * @description Emits Public assets into the build output with their original
7
+ * names, from the partition the Asset inventory already computed. Mirrors
8
+ * `emitIcons`; the collision guard skips any file that would overwrite a
9
+ * generated output and warns instead.
10
+ */
11
+ export function emitPublicAssets({ ctx, root, publicAssets }) {
12
+ const { files, conflicts } = publicAssets;
13
+ for (const conflict of conflicts) {
14
+ ctx.warn(`public/${conflict} collides with a generated output; skipping. Rename it to ship it.`);
15
+ }
16
+ for (const file of files) {
17
+ const source = fs.readFileSync(path.join(root, PUBLIC_DIR, file));
18
+ ctx.emitFile({ type: "asset", fileName: file, source });
19
+ }
20
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @file icons.ts
3
+ * @description Detects the icons for the extension.
4
+ */
5
+ export declare function detectIcons(root: string): Record<string, string> | undefined;
@@ -0,0 +1,20 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ /**
4
+ * @file icons.ts
5
+ * @description Detects the icons for the extension.
6
+ */
7
+ export function detectIcons(root) {
8
+ const iconsDir = path.join(root, "icons");
9
+ if (!fs.existsSync(iconsDir))
10
+ return undefined;
11
+ const sizes = ["16", "32", "48", "128"];
12
+ const icons = {};
13
+ for (const size of sizes) {
14
+ const file = path.join(iconsDir, `${size}.png`);
15
+ if (fs.existsSync(file)) {
16
+ icons[size] = `icons/${size}.png`;
17
+ }
18
+ }
19
+ return Object.keys(icons).length ? icons : undefined;
20
+ }
@@ -0,0 +1,31 @@
1
+ import type { Plugin } from "vite";
2
+ import type { ExtroConfig } from "../types/index.js";
3
+ interface ExtroPluginOptions {
4
+ root: string;
5
+ config?: ExtroConfig;
6
+ /**
7
+ * When true, only background/content are bundled. Manifest + HTML emission
8
+ * is skipped (writeDevAssets handles those separately during `extro dev`).
9
+ * Used by the dev build-watch sidecar.
10
+ */
11
+ scriptsOnly?: boolean;
12
+ /**
13
+ * When set, wrap the background entry in the dev bridge so a service
14
+ * worker exists in dev mode (even if the user didn't write one) to
15
+ * receive rebuild signals from the CLI's WS server and forward Vite HMR
16
+ * events to content scripts.
17
+ */
18
+ devBridge?: {
19
+ signalPort: number;
20
+ vitePort: number;
21
+ };
22
+ /**
23
+ * Called from the dev-server plugin's `handleHotUpdate` with a payload
24
+ * shaped like Vite's own HMR `update` event. The CLI uses this to
25
+ * broadcast HMR over the signal WS (avoiding Vite's HMR WS, whose
26
+ * origin check rejects chrome-extension:// service workers).
27
+ */
28
+ broadcastHmr?: (payload: object) => void;
29
+ }
30
+ export declare function extro(options: ExtroPluginOptions): Plugin;
31
+ export {};