extrojs 0.3.0 → 0.3.1

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.
@@ -30,6 +30,12 @@ export interface AssetOptions {
30
30
  export interface Artifacts {
31
31
  manifest: ManifestV3;
32
32
  html: Partial<Record<RoutableSurface, string>>;
33
+ /**
34
+ * The dev probe (`extro-dev.js`) the dev shells load to reveal the
35
+ * offline screen when the dev server is unreachable. Absent in prod
36
+ * builds and when there are no HTML surfaces to probe for.
37
+ */
38
+ devProbe?: string;
33
39
  }
34
40
  /**
35
41
  * Pure projection from inputs to artifact strings: no filesystem access (the
@@ -1,5 +1,5 @@
1
1
  import { generateManifest } from "./manifest.js";
2
- import { generateHTML } from "./generators/html.js";
2
+ import { DEV_PROBE_FILE, generateDevProbe, generateHTML, } from "./generators/html.js";
3
3
  // ---------------------------------------------------------------------------
4
4
  // Public API
5
5
  // ---------------------------------------------------------------------------
@@ -11,14 +11,18 @@ import { generateHTML } from "./generators/html.js";
11
11
  */
12
12
  export function composeArtifacts(opts) {
13
13
  const manifest = generateManifest(opts);
14
+ const surfaces = Object.keys(opts.tree.surfaces);
14
15
  const html = {};
15
- for (const surface of Object.keys(opts.tree.surfaces)) {
16
+ for (const surface of surfaces) {
16
17
  html[surface] = generateHTML({
17
18
  surface,
18
19
  dev: opts.dev ? { port: opts.dev.port } : undefined,
19
20
  });
20
21
  }
21
- return { manifest, html };
22
+ const devProbe = opts.dev && surfaces.length > 0
23
+ ? generateDevProbe({ port: opts.dev.port })
24
+ : undefined;
25
+ return { manifest, html, devProbe };
22
26
  }
23
27
  /**
24
28
  * Canonical emission of Extro's static outputs (manifest + per-surface HTML).
@@ -30,8 +34,11 @@ export function composeArtifacts(opts) {
30
34
  * `viteBuild` already wrote them to disk).
31
35
  */
32
36
  export async function emitAssets(opts, emit) {
33
- const { manifest, html } = composeArtifacts(opts);
37
+ const { manifest, html, devProbe } = composeArtifacts(opts);
34
38
  await emit("manifest.json", JSON.stringify(manifest, null, 2));
39
+ if (devProbe) {
40
+ await emit(DEV_PROBE_FILE, devProbe);
41
+ }
35
42
  for (const [surface, body] of Object.entries(html)) {
36
43
  if (!body)
37
44
  continue;
@@ -4,13 +4,19 @@ interface GenerateHTMLOptions {
4
4
  port: number;
5
5
  };
6
6
  }
7
+ /**
8
+ * The dev probe script emitted next to the HTML shells in the dev output
9
+ * dir. Referenced by the shells and reserved against Public assets, so the
10
+ * name lives here once.
11
+ */
12
+ export declare const DEV_PROBE_FILE = "extro-dev.js";
7
13
  /**
8
14
  * @describe Generates the HTML shell for a surface. In dev mode, the shell
9
15
  * 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.
16
+ * built bundle, and carries a hidden brand-styled "dev server offline"
17
+ * screen inside #root. The probe script (generateDevProbe) reveals it only
18
+ * when the dev server is unreachable; on a normal start it never paints,
19
+ * and React's createRoot replaces #root's children on mount.
14
20
  */
15
21
  export declare function generateHTML({ surface, dev }: GenerateHTMLOptions): string;
16
22
  export interface DevScreen {
@@ -23,17 +29,36 @@ export interface DevScreen {
23
29
  * (popup), the screen sizes to content with a fixed minimum.
24
30
  */
25
31
  fill?: boolean;
32
+ /**
33
+ * When true, the screen ships with the `hidden` attribute and stays out
34
+ * of the first paint until something removes it (the dev probe, for the
35
+ * offline screen).
36
+ */
37
+ hidden?: boolean;
26
38
  }
27
39
  /**
28
40
  * @describe Renders a brand-styled "developer screen" card. Used as the
29
41
  * pre-render inside #root for offline-dev-server fallback today, intended
30
42
  * for reuse in other build-time / runtime dev panels.
31
43
  */
32
- export declare const renderDevScreen: ({ title, body, fill }: DevScreen) => string;
44
+ export declare const renderDevScreen: ({ title, body, fill, hidden, }: DevScreen) => string;
33
45
  /**
34
46
  * @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.
47
+ * is painted dark only while a visible screen is in the DOM (via `:has()`),
48
+ * so the user's app keeps default styling both before React mounts (screen
49
+ * still hidden) and after (screen wiped from #root).
37
50
  */
38
51
  export declare const devScreenStyles: () => string;
52
+ /**
53
+ * @describe Generates the dev probe (`extro-dev.js`), emitted into the dev
54
+ * output dir next to the HTML shells. MV3 CSP bans inline scripts but allows
55
+ * 'self', so the probe loads from the extension origin and runs even when
56
+ * the Vite dev server is down. It reveals the hidden offline screen only
57
+ * when the @vite/client script fails to fetch, the one unambiguous signal
58
+ * that the server is unreachable; a failing user module while the server is
59
+ * up stays Vite's error overlay's problem.
60
+ */
61
+ export declare const generateDevProbe: ({ port }: {
62
+ port: number;
63
+ }) => string;
39
64
  export {};
@@ -1,15 +1,22 @@
1
+ /**
2
+ * The dev probe script emitted next to the HTML shells in the dev output
3
+ * dir. Referenced by the shells and reserved against Public assets, so the
4
+ * name lives here once.
5
+ */
6
+ export const DEV_PROBE_FILE = "extro-dev.js";
1
7
  /**
2
8
  * @describe Generates the HTML shell for a surface. In dev mode, the shell
3
9
  * 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.
10
+ * built bundle, and carries a hidden brand-styled "dev server offline"
11
+ * screen inside #root. The probe script (generateDevProbe) reveals it only
12
+ * when the dev server is unreachable; on a normal start it never paints,
13
+ * and React's createRoot replaces #root's children on mount.
8
14
  */
9
15
  export function generateHTML({ surface, dev }) {
10
16
  const title = surface.charAt(0).toUpperCase() + surface.slice(1);
11
17
  const scripts = dev
12
18
  ? `
19
+ <script src="./${DEV_PROBE_FILE}"></script>
13
20
  <script type="module" src="http://localhost:${dev.port}/@vite/client"></script>
14
21
  <script type="module" src="http://localhost:${dev.port}/@id/@vitejs/plugin-react/preamble"></script>
15
22
  <script type="module" src="http://localhost:${dev.port}/@id/virtual:extro/runtime/${surface}"></script>
@@ -25,6 +32,10 @@ export function generateHTML({ surface, dev }) {
25
32
  // Popups size to content (no viewport to fill); everything else
26
33
  // (options/sidepanel/tab) gets a full-viewport centered layout.
27
34
  fill: surface !== "popup",
35
+ // Hidden until the probe proves the dev server is down, so a normal
36
+ // start never paints it (the screen used to flash for the moment
37
+ // between document load and React mount).
38
+ hidden: true,
28
39
  })
29
40
  : "";
30
41
  return `
@@ -46,10 +57,10 @@ export function generateHTML({ surface, dev }) {
46
57
  * pre-render inside #root for offline-dev-server fallback today, intended
47
58
  * for reuse in other build-time / runtime dev panels.
48
59
  */
49
- export const renderDevScreen = ({ title, body, fill = false }) => {
60
+ export const renderDevScreen = ({ title, body, fill = false, hidden = false, }) => {
50
61
  const cls = `extro-dev-screen${fill ? " extro-dev-screen--fill" : ""}`;
51
62
  return `
52
- <div class="${cls}">
63
+ <div class="${cls}"${hidden ? " hidden" : ""}>
53
64
  <div class="extro-dev-screen__card">
54
65
  <span class="extro-dev-screen__tag">EXTRO</span>
55
66
  <h1>${title}</h1>
@@ -60,13 +71,14 @@ export const renderDevScreen = ({ title, body, fill = false }) => {
60
71
  };
61
72
  /**
62
73
  * @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.
74
+ * is painted dark only while a visible screen is in the DOM (via `:has()`),
75
+ * so the user's app keeps default styling both before React mounts (screen
76
+ * still hidden) and after (screen wiped from #root).
65
77
  */
66
78
  export const devScreenStyles = () => `
67
79
  <style>
68
80
  body { margin: 0; }
69
- body:has(.extro-dev-screen) { background: #0a0a0a; }
81
+ body:has(.extro-dev-screen:not([hidden])) { background: #0a0a0a; }
70
82
  .extro-dev-screen {
71
83
  box-sizing: border-box;
72
84
  min-width: 360px;
@@ -77,7 +89,10 @@ export const devScreenStyles = () => `
77
89
  align-items: center;
78
90
  justify-content: center;
79
91
  opacity: 0;
80
- animation: extro-dev-screen-in 0.18s 0.05s forwards;
92
+ animation: extro-dev-screen-in 0.18s forwards;
93
+ }
94
+ .extro-dev-screen[hidden] {
95
+ display: none;
81
96
  }
82
97
  .extro-dev-screen--fill {
83
98
  min-height: 100vh;
@@ -125,3 +140,28 @@ export const devScreenStyles = () => `
125
140
  @keyframes extro-dev-screen-in { to { opacity: 1; } }
126
141
  </style>
127
142
  `.trim();
143
+ /**
144
+ * @describe Generates the dev probe (`extro-dev.js`), emitted into the dev
145
+ * output dir next to the HTML shells. MV3 CSP bans inline scripts but allows
146
+ * 'self', so the probe loads from the extension origin and runs even when
147
+ * the Vite dev server is down. It reveals the hidden offline screen only
148
+ * when the @vite/client script fails to fetch, the one unambiguous signal
149
+ * that the server is unreachable; a failing user module while the server is
150
+ * up stays Vite's error overlay's problem.
151
+ */
152
+ export const generateDevProbe = ({ port }) => `
153
+ // Generated by Extro. Reveals the "dev server isn't running" screen when
154
+ // the Vite dev server can't be reached. Script fetch errors don't bubble,
155
+ // hence the capture-phase listener; this classic script runs before the
156
+ // module scripts below it can fail.
157
+ window.addEventListener(
158
+ "error",
159
+ (event) => {
160
+ const el = event.target;
161
+ if (!(el instanceof HTMLScriptElement)) return;
162
+ if (el.src !== "http://localhost:${port}/@vite/client") return;
163
+ document.querySelector(".extro-dev-screen")?.removeAttribute("hidden");
164
+ },
165
+ true,
166
+ );
167
+ `.trimStart();
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { DEV_PROBE_FILE } from "./generators/html.js";
3
4
  /**
4
5
  * @file public.ts
5
6
  * @description Discovers Public assets (static files under the project-root
@@ -31,6 +32,11 @@ function walk(dir, base) {
31
32
  function isReservedName(name, tree) {
32
33
  if (name === "manifest.json")
33
34
  return true;
35
+ // Dev-only output, but reserved in all modes so the partition (and the
36
+ // web_accessible_resources list derived from it) doesn't depend on the
37
+ // build mode.
38
+ if (name === DEV_PROBE_FILE)
39
+ return true;
34
40
  if (name === "icons" || name.startsWith("icons/"))
35
41
  return true;
36
42
  for (const surface of Object.keys(tree.surfaces)) {
@@ -15,6 +15,7 @@ interface LinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"
15
15
  export declare const Link: ({ href, replace, onClick, ...rest }: LinkProps) => import("react").DetailedReactHTMLElement<{
16
16
  content?: string | undefined | undefined;
17
17
  title?: string | undefined | undefined;
18
+ hidden?: boolean | undefined | undefined;
18
19
  type?: string | undefined | undefined;
19
20
  slot?: string | undefined | undefined;
20
21
  style?: import("react").CSSProperties | undefined;
@@ -39,7 +40,6 @@ export declare const Link: ({ href, replace, onClick, ...rest }: LinkProps) => i
39
40
  dir?: string | undefined | undefined;
40
41
  draggable?: (boolean | "true" | "false") | undefined;
41
42
  enterKeyHint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
42
- hidden?: boolean | undefined | undefined;
43
43
  id?: string | undefined | undefined;
44
44
  lang?: string | undefined | undefined;
45
45
  nonce?: string | undefined | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "extrojs",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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",