alabjs 0.2.2 → 0.2.4

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.
@@ -1,8 +1,7 @@
1
- import { useEffect, type HTMLAttributes } from "react";
1
+ import { useEffect, type HTMLAttributes, type ReactNode } from "react";
2
2
 
3
- export interface ScriptProps extends Omit<HTMLAttributes<HTMLScriptElement>, "src"> {
4
- /** URL of the external script to load. */
5
- src: string;
3
+ /** Props shared by both external-src and inline-children variants. */
4
+ interface ScriptBaseProps extends Omit<HTMLAttributes<HTMLScriptElement>, "src"> {
6
5
  /**
7
6
  * Loading strategy:
8
7
  * - `"beforeInteractive"` — injected into `<head>` during SSR; blocks page rendering.
@@ -13,12 +12,27 @@ export interface ScriptProps extends Omit<HTMLAttributes<HTMLScriptElement>, "sr
13
12
  * Best for low-priority scripts like A/B testing, heatmaps, social embeds.
14
13
  */
15
14
  strategy?: "beforeInteractive" | "afterInteractive" | "lazyOnload";
16
- /** Called once the script has loaded successfully. */
15
+ /** Called once the script has loaded successfully (external scripts only). */
17
16
  onLoad?: () => void;
18
- /** Called if the script fails to load. */
17
+ /** Called if the script fails to load (external scripts only). */
19
18
  onError?: () => void;
20
19
  }
21
20
 
21
+ /** External script variant — `src` is required and `children` must be absent. */
22
+ interface ExternalScriptProps extends ScriptBaseProps {
23
+ /** URL of the external script to load. */
24
+ src: string;
25
+ children?: never;
26
+ }
27
+
28
+ /** Inline script variant — `children` contains the script body; `src` must be absent. */
29
+ interface InlineScriptProps extends ScriptBaseProps {
30
+ src?: never;
31
+ children: ReactNode;
32
+ }
33
+
34
+ export type ScriptProps = ExternalScriptProps | InlineScriptProps;
35
+
22
36
  /**
23
37
  * Load a third-party script with strategy control.
24
38
  *
@@ -40,8 +54,35 @@ export function Script({
40
54
  strategy = "afterInteractive",
41
55
  onLoad,
42
56
  onError,
57
+ children,
43
58
  ...rest
44
59
  }: ScriptProps) {
60
+ // ── Inline script: render <script>{children}</script> directly ──────────────
61
+ // Inline scripts have no loading strategy — they are always rendered as-is.
62
+ // For SSR (beforeInteractive) they land in the HTML stream; for client
63
+ // renders they are injected once via useEffect.
64
+ if (!src) {
65
+ if (strategy === "beforeInteractive") {
66
+ if (typeof window !== "undefined") return null;
67
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
+ return <script {...(rest as any)}>{children}</script>;
69
+ }
70
+ // eslint-disable-next-line react-hooks/rules-of-hooks
71
+ useEffect(() => {
72
+ const el = document.createElement("script");
73
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
74
+ if (typeof children === "string") el.textContent = children;
75
+ for (const [k, v] of Object.entries(rest)) {
76
+ if (typeof v === "string") el.setAttribute(k, v);
77
+ }
78
+ document.head.appendChild(el);
79
+ // eslint-disable-next-line react-hooks/exhaustive-deps
80
+ }, []);
81
+ return null;
82
+ }
83
+
84
+ // ── External script ─────────────────────────────────────────────────────────
85
+
45
86
  // `beforeInteractive` is handled at SSR time by rendering a real <script> tag.
46
87
  // The component returns null on the client to avoid duplicate injection.
47
88
  if (strategy === "beforeInteractive") {
package/src/ssr/html.ts CHANGED
@@ -101,6 +101,61 @@ export function htmlShellAfter(opts: { nonce?: string | undefined }): string {
101
101
  </html>`;
102
102
  }
103
103
 
104
+ /**
105
+ * Build just the alab `<meta>` tags used for client hydration.
106
+ * Used when the page component renders a full `<html>` document so the tags
107
+ * can be injected into the user-controlled `<head>` rather than the shell.
108
+ */
109
+ export function buildAlabMetaTags(opts: Omit<HtmlShellOptions, "metadata" | "headExtra">): string {
110
+ const { routeFile, ssr, paramsJson, searchParamsJson, layoutsJson, loadingFile, buildId } = opts;
111
+ return [
112
+ `<meta name="alabjs-route" content="${escAttr(routeFile)}" />`,
113
+ `<meta name="alabjs-ssr" content="${ssr ? "true" : "false"}" />`,
114
+ `<meta name="alabjs-params" content="${escAttr(paramsJson)}" />`,
115
+ `<meta name="alabjs-search-params" content="${escAttr(searchParamsJson)}" />`,
116
+ layoutsJson ? `<meta name="alabjs-layouts" content="${escAttr(layoutsJson)}" />` : "",
117
+ loadingFile ? `<meta name="alabjs-loading" content="${escAttr(loadingFile)}" />` : "",
118
+ buildId ? `<meta name="alabjs-build-id" content="${escAttr(buildId)}" />` : "",
119
+ // Signal to the client bootstrap that it must use hydrateRoot(document, …)
120
+ // instead of mounting into <div id="alabjs-root">.
121
+ `<meta name="alabjs-full-document" content="true" />`,
122
+ ].filter(Boolean).join("\n ");
123
+ }
124
+
125
+ /**
126
+ * Inject alab hydration meta tags and the client bootstrap script into a
127
+ * full-document SSR string (i.e. one whose root element is `<html>`).
128
+ *
129
+ * The meta tags are appended before `</head>` and the client script before
130
+ * `</body>`. If neither marker is found the strings are appended verbatim.
131
+ */
132
+ export function injectIntoFullDocument(
133
+ ssrHtml: string,
134
+ opts: Omit<HtmlShellOptions, "metadata" | "headExtra">,
135
+ ): string {
136
+ const metaTags = buildAlabMetaTags(opts);
137
+ const nonceAttr = opts.nonce ? ` nonce="${escAttr(opts.nonce)}"` : "";
138
+ const clientScript = `<script type="module" src="/@alabjs/client"${nonceAttr}></script>`;
139
+
140
+ let result = ssrHtml;
141
+
142
+ // Inject meta tags before </head> (case-insensitive).
143
+ if (/<\/head>/i.test(result)) {
144
+ result = result.replace(/<\/head>/i, ` ${metaTags}\n </head>`);
145
+ } else {
146
+ result = metaTags + "\n" + result;
147
+ }
148
+
149
+ // Inject client bootstrap script before </body>.
150
+ if (/<\/body>/i.test(result)) {
151
+ result = result.replace(/<\/body>/i, ` ${clientScript}\n </body>`);
152
+ } else {
153
+ result = result + "\n" + clientScript;
154
+ }
155
+
156
+ return result;
157
+ }
158
+
104
159
  // ─── Helpers ──────────────────────────────────────────────────────────────────
105
160
 
106
161
  function escHtml(s: string): string {
@@ -1,3 +1,19 @@
1
1
  declare module "alabjs-vite-plugin" {
2
2
  export function alabPlugin(options?: { mode?: "dev" | "build" }): import("vite").Plugin[];
3
3
  }
4
+
5
+ // Augment ImportMeta so `import.meta.env.DEV` (injected by Vite at build time)
6
+ // is recognised by TypeScript when compiling alabjs component source files.
7
+ // Vite replaces `import.meta.env.DEV` with a literal boolean at bundle time;
8
+ // this declaration just provides the compile-time type so TS does not error.
9
+ interface ImportMeta {
10
+ readonly env: {
11
+ /** `true` in Vite dev server, `false` in production builds. */
12
+ readonly DEV: boolean;
13
+ readonly PROD: boolean;
14
+ readonly SSR: boolean;
15
+ readonly MODE: string;
16
+ readonly BASE_URL: string;
17
+ readonly [key: string]: unknown;
18
+ };
19
+ }