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.
- package/dist/cli.js +0 -0
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +49 -13
- package/dist/commands/dev.js.map +1 -1
- package/dist/components/Font.d.ts +8 -3
- package/dist/components/Font.d.ts.map +1 -1
- package/dist/components/Font.js +12 -2
- package/dist/components/Font.js.map +1 -1
- package/dist/components/Script.d.ts +19 -7
- package/dist/components/Script.d.ts.map +1 -1
- package/dist/components/Script.js +28 -1
- package/dist/components/Script.js.map +1 -1
- package/dist/ssr/html.d.ts +14 -0
- package/dist/ssr/html.d.ts.map +1 -1
- package/dist/ssr/html.js +48 -0
- package/dist/ssr/html.js.map +1 -1
- package/package.json +14 -14
- package/src/commands/dev.ts +49 -13
- package/src/components/Font.tsx +12 -2
- package/src/components/Script.tsx +47 -6
- package/src/ssr/html.ts +55 -0
- package/src/types/plugins.d.ts +16 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { useEffect, type HTMLAttributes } from "react";
|
|
1
|
+
import { useEffect, type HTMLAttributes, type ReactNode } from "react";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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 {
|
package/src/types/plugins.d.ts
CHANGED
|
@@ -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
|
+
}
|