alepha 0.15.2 → 0.15.3
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/README.md +68 -80
- package/dist/api/audits/index.d.ts +332 -332
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/files/index.d.ts +170 -170
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/jobs/index.d.ts +151 -151
- package/dist/api/keys/index.d.ts +195 -195
- package/dist/api/keys/index.d.ts.map +1 -1
- package/dist/api/parameters/index.d.ts +260 -260
- package/dist/api/users/index.d.ts +22 -11
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +7 -2
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts +128 -128
- package/dist/api/verifications/index.d.ts.map +1 -1
- package/dist/bucket/index.d.ts +8 -0
- package/dist/bucket/index.d.ts.map +1 -1
- package/dist/bucket/index.js +7 -2
- package/dist/bucket/index.js.map +1 -1
- package/dist/cli/index.d.ts +191 -74
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +215 -48
- package/dist/cli/index.js.map +1 -1
- package/dist/command/index.d.ts +10 -0
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js +67 -13
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +28 -21
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +28 -21
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +28 -21
- package/dist/core/index.native.js.map +1 -1
- package/dist/email/index.d.ts +8 -0
- package/dist/email/index.d.ts.map +1 -1
- package/dist/email/index.js +7 -2
- package/dist/email/index.js.map +1 -1
- package/dist/mcp/index.d.ts +5 -5
- package/dist/orm/index.bun.js +32 -16
- package/dist/orm/index.bun.js.map +1 -1
- package/dist/orm/index.d.ts +4 -1
- package/dist/orm/index.d.ts.map +1 -1
- package/dist/orm/index.js +34 -22
- package/dist/orm/index.js.map +1 -1
- package/dist/react/router/index.browser.js +9 -15
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +295 -407
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js +566 -776
- package/dist/react/router/index.js.map +1 -1
- package/dist/redis/index.d.ts +19 -19
- package/dist/security/index.d.ts +42 -42
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +8 -7
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +167 -167
- package/dist/server/core/index.d.ts +9 -9
- package/dist/server/health/index.d.ts +17 -17
- package/dist/server/links/index.d.ts +39 -39
- package/dist/server/static/index.js +7 -2
- package/dist/server/static/index.js.map +1 -1
- package/dist/server/swagger/index.d.ts +8 -0
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/server/swagger/index.js +7 -2
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/sms/index.d.ts +8 -0
- package/dist/sms/index.d.ts.map +1 -1
- package/dist/sms/index.js +7 -2
- package/dist/sms/index.js.map +1 -1
- package/dist/system/index.browser.js +734 -12
- package/dist/system/index.browser.js.map +1 -1
- package/dist/system/index.d.ts +8 -0
- package/dist/system/index.d.ts.map +1 -1
- package/dist/system/index.js +7 -2
- package/dist/system/index.js.map +1 -1
- package/dist/vite/index.d.ts +1 -1
- package/dist/vite/index.js +15 -7
- package/dist/vite/index.js.map +1 -1
- package/package.json +4 -2
- package/src/api/logs/TODO.md +13 -10
- package/src/cli/apps/AlephaPackageBuilderCli.ts +9 -0
- package/src/cli/atoms/buildOptions.ts +99 -9
- package/src/cli/commands/build.ts +149 -32
- package/src/cli/commands/db.ts +5 -7
- package/src/cli/commands/init.spec.ts +50 -6
- package/src/cli/commands/init.ts +28 -5
- package/src/cli/providers/ViteDevServerProvider.ts +1 -10
- package/src/cli/services/AlephaCliUtils.ts +16 -0
- package/src/cli/services/PackageManagerUtils.ts +2 -0
- package/src/cli/services/ProjectScaffolder.spec.ts +97 -0
- package/src/cli/services/ProjectScaffolder.ts +28 -6
- package/src/cli/templates/agentMd.ts +6 -1
- package/src/cli/templates/apiAppSecurityTs.ts +11 -0
- package/src/cli/templates/apiIndexTs.ts +18 -4
- package/src/cli/templates/webAppRouterTs.ts +25 -1
- package/src/cli/templates/webHelloComponentTsx.ts +15 -5
- package/src/command/helpers/Runner.spec.ts +135 -0
- package/src/command/helpers/Runner.ts +4 -1
- package/src/command/providers/CliProvider.spec.ts +325 -0
- package/src/command/providers/CliProvider.ts +117 -7
- package/src/core/Alepha.ts +32 -25
- package/src/orm/index.bun.ts +1 -1
- package/src/orm/index.ts +2 -6
- package/src/orm/providers/drivers/BunSqliteProvider.ts +4 -1
- package/src/orm/providers/drivers/CloudflareD1Provider.ts +57 -30
- package/src/orm/providers/drivers/DatabaseProvider.ts +9 -1
- package/src/orm/providers/drivers/NodeSqliteProvider.ts +4 -1
- package/src/react/router/hooks/useActive.ts +1 -1
- package/src/react/router/hooks/useRouter.ts +1 -1
- package/src/react/router/index.ts +4 -0
- package/src/react/router/primitives/$page.browser.spec.tsx +24 -24
- package/src/react/router/primitives/$page.spec.tsx +0 -32
- package/src/react/router/primitives/$page.ts +6 -14
- package/src/react/router/providers/ReactBrowserProvider.ts +6 -3
- package/src/react/router/providers/ReactPageProvider.ts +1 -1
- package/src/react/router/providers/ReactPreloadProvider.spec.ts +142 -0
- package/src/react/router/providers/ReactPreloadProvider.ts +85 -0
- package/src/react/router/providers/ReactServerProvider.ts +7 -78
- package/src/react/router/providers/ReactServerTemplateProvider.spec.ts +210 -0
- package/src/react/router/providers/ReactServerTemplateProvider.ts +228 -665
- package/src/react/router/services/ReactRouter.ts +13 -13
- package/src/security/__tests__/ServerSecurityProvider.spec.ts +77 -0
- package/src/security/providers/ServerSecurityProvider.ts +30 -22
- package/src/server/core/providers/NodeHttpServerProvider.spec.ts +9 -3
- package/src/system/index.browser.ts +25 -0
- package/src/system/index.workerd.ts +1 -0
- package/src/system/providers/FileSystemProvider.ts +8 -0
- package/src/system/providers/NodeFileSystemProvider.ts +11 -2
- package/src/vite/tasks/buildServer.ts +2 -12
- package/src/vite/tasks/generateCloudflare.ts +10 -7
- package/src/vite/tasks/generateDocker.ts +4 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { $hook, $inject, Alepha } from "alepha";
|
|
2
|
+
import { SSRManifestProvider } from "./SSRManifestProvider.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Adds HTTP Link headers for preloading entry assets.
|
|
6
|
+
*
|
|
7
|
+
* Benefits:
|
|
8
|
+
* - Early Hints (103): Servers can send preload hints before the full response
|
|
9
|
+
* - CDN optimization: Many CDNs use Link headers to optimize asset delivery
|
|
10
|
+
* - Browser prefetching: Browsers can start fetching resources earlier
|
|
11
|
+
*
|
|
12
|
+
* The Link header is computed once at first request and cached for reuse.
|
|
13
|
+
*/
|
|
14
|
+
export class ReactPreloadProvider {
|
|
15
|
+
protected readonly alepha = $inject(Alepha);
|
|
16
|
+
protected readonly ssrManifest = $inject(SSRManifestProvider);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Cached Link header value - computed once, reused for all requests.
|
|
20
|
+
*/
|
|
21
|
+
protected cachedLinkHeader: string | null | undefined;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build the Link header string from entry assets.
|
|
25
|
+
*
|
|
26
|
+
* Format: <url>; rel=preload; as=type, <url>; rel=modulepreload
|
|
27
|
+
*
|
|
28
|
+
* @returns Link header string or null if no assets
|
|
29
|
+
*/
|
|
30
|
+
protected buildLinkHeader(): string | null {
|
|
31
|
+
const assets = this.ssrManifest.getEntryAssets();
|
|
32
|
+
if (!assets) return null;
|
|
33
|
+
|
|
34
|
+
const links: string[] = [];
|
|
35
|
+
|
|
36
|
+
// CSS - preload as style
|
|
37
|
+
for (const css of assets.css) {
|
|
38
|
+
links.push(`<${css}>; rel=preload; as=style`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// JS - modulepreload for ES modules
|
|
42
|
+
if (assets.js) {
|
|
43
|
+
links.push(`<${assets.js}>; rel=modulepreload`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return links.length > 0 ? links.join(", ") : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get the cached Link header, computing it on first access.
|
|
51
|
+
*/
|
|
52
|
+
protected getLinkHeader(): string | null {
|
|
53
|
+
if (this.cachedLinkHeader === undefined) {
|
|
54
|
+
this.cachedLinkHeader = this.buildLinkHeader();
|
|
55
|
+
}
|
|
56
|
+
return this.cachedLinkHeader;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Add Link header to HTML responses for asset preloading.
|
|
61
|
+
*/
|
|
62
|
+
protected readonly onResponse = $hook({
|
|
63
|
+
on: "server:onResponse",
|
|
64
|
+
priority: "first",
|
|
65
|
+
handler: ({ response }) => {
|
|
66
|
+
// Only add to HTML responses (SSR pages)
|
|
67
|
+
const contentType = response.headers["content-type"];
|
|
68
|
+
if (!contentType || !contentType.includes("text/html")) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const linkHeader = this.getLinkHeader();
|
|
73
|
+
if (!linkHeader) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Append to existing Link header if present
|
|
78
|
+
if (response.headers.link) {
|
|
79
|
+
response.headers.link = `${response.headers.link}, ${linkHeader}`;
|
|
80
|
+
} else {
|
|
81
|
+
response.headers.link = linkHeader;
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
$inject,
|
|
7
7
|
$use,
|
|
8
8
|
Alepha,
|
|
9
|
-
AlephaError,
|
|
10
9
|
type Static,
|
|
11
10
|
t,
|
|
12
11
|
} from "alepha";
|
|
@@ -101,54 +100,21 @@ export class ReactServerProvider {
|
|
|
101
100
|
}
|
|
102
101
|
|
|
103
102
|
if (ssrEnabled) {
|
|
104
|
-
|
|
103
|
+
this.registerPages();
|
|
105
104
|
this.log.info("SSR OK");
|
|
106
105
|
return;
|
|
107
106
|
}
|
|
108
107
|
|
|
109
|
-
// no SSR enabled, serve
|
|
110
|
-
this.log.info("SSR is disabled
|
|
111
|
-
this.serverRouterProvider.createRoute({
|
|
112
|
-
path: "*",
|
|
113
|
-
handler: async ({ url, reply }) => {
|
|
114
|
-
if (url.pathname.includes(".")) {
|
|
115
|
-
// If the request is for a file (e.g., /style.css), do not fallback
|
|
116
|
-
reply.headers["content-type"] = "text/plain";
|
|
117
|
-
reply.body = "Not Found";
|
|
118
|
-
reply.status = 404;
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
reply.headers["content-type"] = "text/html";
|
|
123
|
-
|
|
124
|
-
// serve index.html for all unmatched routes
|
|
125
|
-
return this.template;
|
|
126
|
-
},
|
|
127
|
-
});
|
|
108
|
+
// no SSR enabled, serve a minimal fallback
|
|
109
|
+
this.log.info("SSR is disabled");
|
|
128
110
|
},
|
|
129
111
|
});
|
|
130
112
|
|
|
131
|
-
/**
|
|
132
|
-
* Get the current HTML template.
|
|
133
|
-
*/
|
|
134
|
-
public get template() {
|
|
135
|
-
return (
|
|
136
|
-
this.alepha.store.get("alepha.react.server.template") ??
|
|
137
|
-
"<!DOCTYPE html><html lang='en'><head></head><body><div id='root'></div></body></html>"
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
113
|
/**
|
|
142
114
|
* Register all pages as server routes.
|
|
143
115
|
*/
|
|
144
|
-
protected
|
|
145
|
-
//
|
|
146
|
-
const template = await templateLoader();
|
|
147
|
-
if (template) {
|
|
148
|
-
this.templateProvider.parseTemplate(template);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Set up early head content (entry assets preloads)
|
|
116
|
+
protected registerPages(): void {
|
|
117
|
+
// Set up early head content (entry assets)
|
|
152
118
|
this.setupEarlyHeadContent();
|
|
153
119
|
|
|
154
120
|
// Cache ServerLinksProvider check at startup
|
|
@@ -163,7 +129,7 @@ export class ReactServerProvider {
|
|
|
163
129
|
schema: undefined, // schema is handled by the page primitive provider
|
|
164
130
|
method: "GET",
|
|
165
131
|
path: page.match,
|
|
166
|
-
handler: this.createHandler(page
|
|
132
|
+
handler: this.createHandler(page),
|
|
167
133
|
});
|
|
168
134
|
}
|
|
169
135
|
}
|
|
@@ -174,13 +140,6 @@ export class ReactServerProvider {
|
|
|
174
140
|
*
|
|
175
141
|
* This content is sent immediately when streaming starts, before page loaders run,
|
|
176
142
|
* allowing the browser to start downloading entry.js and CSS files early.
|
|
177
|
-
*
|
|
178
|
-
* Uses <script type="module"> instead of <link rel="modulepreload"> for JS
|
|
179
|
-
* because the script needs to execute anyway - this way the browser starts
|
|
180
|
-
* downloading, parsing, AND will execute as soon as ready.
|
|
181
|
-
*
|
|
182
|
-
* Also injects critical meta tags (charset, viewport) if not specified in $head,
|
|
183
|
-
* and strips these assets from the original template head to avoid duplicates.
|
|
184
143
|
*/
|
|
185
144
|
protected setupEarlyHeadContent(): void {
|
|
186
145
|
const assets = this.ssrManifestProvider.getEntryAssets();
|
|
@@ -189,13 +148,9 @@ export class ReactServerProvider {
|
|
|
189
148
|
const parts: string[] = [];
|
|
190
149
|
|
|
191
150
|
if (assets) {
|
|
192
|
-
// Add CSS stylesheets (critical for rendering)
|
|
193
151
|
for (const css of assets.css) {
|
|
194
152
|
parts.push(`<link rel="stylesheet" href="${css}" crossorigin="">`);
|
|
195
153
|
}
|
|
196
|
-
|
|
197
|
-
// Add entry JS as script module (not just modulepreload)
|
|
198
|
-
// This starts download, parse, AND execution immediately
|
|
199
154
|
if (assets.js) {
|
|
200
155
|
parts.push(
|
|
201
156
|
`<script type="module" crossorigin="" src="${assets.js}"></script>`,
|
|
@@ -203,11 +158,9 @@ export class ReactServerProvider {
|
|
|
203
158
|
}
|
|
204
159
|
}
|
|
205
160
|
|
|
206
|
-
// Pass global head so critical meta tags can be injected if missing
|
|
207
161
|
this.templateProvider.setEarlyHeadContent(
|
|
208
162
|
parts.length > 0 ? `${parts.join("\n")}\n` : "",
|
|
209
163
|
globalHead,
|
|
210
|
-
assets ?? undefined,
|
|
211
164
|
);
|
|
212
165
|
|
|
213
166
|
this.log.debug("Early head content set", {
|
|
@@ -251,23 +204,10 @@ export class ReactServerProvider {
|
|
|
251
204
|
/**
|
|
252
205
|
* Create the request handler for a page route.
|
|
253
206
|
*/
|
|
254
|
-
protected createHandler(
|
|
255
|
-
route: PageRoute,
|
|
256
|
-
templateLoader: TemplateLoader,
|
|
257
|
-
): ServerHandler {
|
|
207
|
+
protected createHandler(route: PageRoute): ServerHandler {
|
|
258
208
|
return async (serverRequest) => {
|
|
259
209
|
const { url, reply, query, params } = serverRequest;
|
|
260
210
|
|
|
261
|
-
// Ensure template is parsed (handles dev mode where template may change)
|
|
262
|
-
if (!this.templateProvider.isReady()) {
|
|
263
|
-
const template = await templateLoader();
|
|
264
|
-
if (!template) {
|
|
265
|
-
throw new AlephaError("Missing template for SSR rendering");
|
|
266
|
-
}
|
|
267
|
-
this.templateProvider.parseTemplate(template);
|
|
268
|
-
this.setupEarlyHeadContent();
|
|
269
|
-
}
|
|
270
|
-
|
|
271
211
|
this.log.trace("Rendering page", { name: route.name });
|
|
272
212
|
|
|
273
213
|
// Initialize router state
|
|
@@ -446,12 +386,6 @@ export class ReactServerProvider {
|
|
|
446
386
|
|
|
447
387
|
await this.alepha.events.emit("react:server:render:begin", { state });
|
|
448
388
|
|
|
449
|
-
// Ensure template is parsed with early head content (entry.js, CSS)
|
|
450
|
-
if (!this.templateProvider.isReady()) {
|
|
451
|
-
this.templateProvider.parseTemplate(this.template);
|
|
452
|
-
this.setupEarlyHeadContent();
|
|
453
|
-
}
|
|
454
|
-
|
|
455
389
|
// Use shared rendering logic
|
|
456
390
|
const result = await this.renderPage(page, state);
|
|
457
391
|
|
|
@@ -508,10 +442,6 @@ export class ReactServerProvider {
|
|
|
508
442
|
|
|
509
443
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
510
444
|
|
|
511
|
-
type TemplateLoader = () => Promise<string | undefined>;
|
|
512
|
-
|
|
513
|
-
// ---------------------------------------------------------------------------------------------------------------------
|
|
514
|
-
|
|
515
445
|
const envSchema = t.object({
|
|
516
446
|
REACT_SSR_ENABLED: t.optional(t.boolean()),
|
|
517
447
|
});
|
|
@@ -520,7 +450,6 @@ declare module "alepha" {
|
|
|
520
450
|
interface Env extends Partial<Static<typeof envSchema>> {}
|
|
521
451
|
interface State {
|
|
522
452
|
"alepha.react.server.ssr"?: boolean;
|
|
523
|
-
"alepha.react.server.template"?: string;
|
|
524
453
|
}
|
|
525
454
|
}
|
|
526
455
|
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { Alepha } from "alepha";
|
|
2
|
+
import { $head } from "alepha/react/head";
|
|
3
|
+
import { HttpClient, ServerProvider } from "alepha/server";
|
|
4
|
+
import { describe, it } from "vitest";
|
|
5
|
+
import { ssrManifestAtom } from "../atoms/ssrManifestAtom.ts";
|
|
6
|
+
import { $page } from "../index.ts";
|
|
7
|
+
|
|
8
|
+
describe("ReactServerTemplateProvider", () => {
|
|
9
|
+
describe("streaming", () => {
|
|
10
|
+
class App {
|
|
11
|
+
head = $head({
|
|
12
|
+
htmlAttributes: { lang: "en" },
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
home = $page({
|
|
16
|
+
path: "/",
|
|
17
|
+
head: {
|
|
18
|
+
title: "Test Page",
|
|
19
|
+
meta: [{ name: "description", content: "Test description" }],
|
|
20
|
+
},
|
|
21
|
+
component: () => "Hello World",
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
withLoader = $page({
|
|
25
|
+
path: "/with-loader",
|
|
26
|
+
head: { title: "Loader Page" },
|
|
27
|
+
loader: async () => ({ data: "loaded" }),
|
|
28
|
+
component: ({ data }) => `Data: ${data}`,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
it("should stream complete HTML document with correct structure", async ({
|
|
33
|
+
expect,
|
|
34
|
+
}) => {
|
|
35
|
+
const alepha = Alepha.create({
|
|
36
|
+
env: { LOG_LEVEL: "error", SERVER_PORT: 0 },
|
|
37
|
+
}).with(App);
|
|
38
|
+
|
|
39
|
+
await alepha.start();
|
|
40
|
+
|
|
41
|
+
const server = alepha.inject(ServerProvider);
|
|
42
|
+
const http = alepha.inject(HttpClient);
|
|
43
|
+
|
|
44
|
+
const response = await http.fetch(`${server.hostname}/`);
|
|
45
|
+
|
|
46
|
+
// Verify HTML structure
|
|
47
|
+
expect(response.data).toContain("<!DOCTYPE html>");
|
|
48
|
+
expect(response.data).toContain('<html lang="en">');
|
|
49
|
+
expect(response.data).toContain("<head>");
|
|
50
|
+
expect(response.data).toContain('<meta charset="UTF-8">');
|
|
51
|
+
expect(response.data).toContain('<meta name="viewport"');
|
|
52
|
+
expect(response.data).toContain("<title>Test Page</title>");
|
|
53
|
+
expect(response.data).toContain(
|
|
54
|
+
'<meta name="description" content="Test description">',
|
|
55
|
+
);
|
|
56
|
+
expect(response.data).toContain("</head>");
|
|
57
|
+
expect(response.data).toContain("<body>");
|
|
58
|
+
expect(response.data).toContain('<div id="root">');
|
|
59
|
+
expect(response.data).toContain("Hello World");
|
|
60
|
+
expect(response.data).toContain("</div>");
|
|
61
|
+
expect(response.data).toContain("</body>");
|
|
62
|
+
expect(response.data).toContain("</html>");
|
|
63
|
+
|
|
64
|
+
await alepha.stop();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should include hydration data when enabled", async ({ expect }) => {
|
|
68
|
+
const alepha = Alepha.create({
|
|
69
|
+
env: { LOG_LEVEL: "error", SERVER_PORT: 0 },
|
|
70
|
+
}).with(App);
|
|
71
|
+
|
|
72
|
+
await alepha.start();
|
|
73
|
+
|
|
74
|
+
const server = alepha.inject(ServerProvider);
|
|
75
|
+
const http = alepha.inject(HttpClient);
|
|
76
|
+
|
|
77
|
+
const response = await http.fetch(`${server.hostname}/with-loader`);
|
|
78
|
+
|
|
79
|
+
// Verify hydration script is present
|
|
80
|
+
expect(response.data).toContain("<script>window.__ssr=");
|
|
81
|
+
expect(response.data).toContain("</script>");
|
|
82
|
+
|
|
83
|
+
// Verify hydration data structure
|
|
84
|
+
expect(response.data).toMatch(/window\.__ssr=\{.*"layers".*\}/);
|
|
85
|
+
|
|
86
|
+
await alepha.stop();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should include entry assets in head when manifest is available", async ({
|
|
90
|
+
expect,
|
|
91
|
+
}) => {
|
|
92
|
+
const alepha = Alepha.create({
|
|
93
|
+
env: { LOG_LEVEL: "error", SERVER_PORT: 0 },
|
|
94
|
+
}).with(App);
|
|
95
|
+
|
|
96
|
+
// Set up mock SSR manifest
|
|
97
|
+
alepha.store.set(ssrManifestAtom, {
|
|
98
|
+
client: {
|
|
99
|
+
"src/entry.tsx": {
|
|
100
|
+
file: "assets/entry.abc123.js",
|
|
101
|
+
isEntry: true,
|
|
102
|
+
css: ["assets/style.def456.css"],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await alepha.start();
|
|
108
|
+
|
|
109
|
+
const server = alepha.inject(ServerProvider);
|
|
110
|
+
const http = alepha.inject(HttpClient);
|
|
111
|
+
|
|
112
|
+
const response = await http.fetch(`${server.hostname}/`);
|
|
113
|
+
|
|
114
|
+
// Verify entry assets are in the head
|
|
115
|
+
expect(response.data).toContain(
|
|
116
|
+
'<link rel="stylesheet" href="/assets/style.def456.css" crossorigin="">',
|
|
117
|
+
);
|
|
118
|
+
expect(response.data).toContain(
|
|
119
|
+
'<script type="module" crossorigin="" src="/assets/entry.abc123.js"></script>',
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
await alepha.stop();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should handle pages with loaders correctly", async ({ expect }) => {
|
|
126
|
+
const alepha = Alepha.create({
|
|
127
|
+
env: { LOG_LEVEL: "error", SERVER_PORT: 0 },
|
|
128
|
+
}).with(App);
|
|
129
|
+
|
|
130
|
+
await alepha.start();
|
|
131
|
+
|
|
132
|
+
const server = alepha.inject(ServerProvider);
|
|
133
|
+
const http = alepha.inject(HttpClient);
|
|
134
|
+
|
|
135
|
+
const response = await http.fetch(`${server.hostname}/with-loader`);
|
|
136
|
+
|
|
137
|
+
// Verify loader data is rendered
|
|
138
|
+
expect(response.data).toContain("Data: loaded");
|
|
139
|
+
|
|
140
|
+
await alepha.stop();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should set correct content-type header", async ({ expect }) => {
|
|
144
|
+
const alepha = Alepha.create({
|
|
145
|
+
env: { LOG_LEVEL: "error", SERVER_PORT: 0 },
|
|
146
|
+
}).with(App);
|
|
147
|
+
|
|
148
|
+
await alepha.start();
|
|
149
|
+
|
|
150
|
+
const server = alepha.inject(ServerProvider);
|
|
151
|
+
const http = alepha.inject(HttpClient);
|
|
152
|
+
|
|
153
|
+
const response = await http.fetch(`${server.hostname}/`);
|
|
154
|
+
|
|
155
|
+
expect(response.headers.get("content-type")).toBe("text/html");
|
|
156
|
+
|
|
157
|
+
await alepha.stop();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("should set cache-control headers for SSR responses", async ({
|
|
161
|
+
expect,
|
|
162
|
+
}) => {
|
|
163
|
+
const alepha = Alepha.create({
|
|
164
|
+
env: { LOG_LEVEL: "error", SERVER_PORT: 0 },
|
|
165
|
+
}).with(App);
|
|
166
|
+
|
|
167
|
+
await alepha.start();
|
|
168
|
+
|
|
169
|
+
const server = alepha.inject(ServerProvider);
|
|
170
|
+
const http = alepha.inject(HttpClient);
|
|
171
|
+
|
|
172
|
+
const response = await http.fetch(`${server.hostname}/`);
|
|
173
|
+
|
|
174
|
+
expect(response.headers.get("cache-control")).toContain("no-store");
|
|
175
|
+
|
|
176
|
+
await alepha.stop();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("error handling", () => {
|
|
181
|
+
class ErrorApp {
|
|
182
|
+
errorPage = $page({
|
|
183
|
+
path: "/error",
|
|
184
|
+
loader: async () => {
|
|
185
|
+
throw new Error("Loader error");
|
|
186
|
+
},
|
|
187
|
+
component: () => "Should not render",
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
it("should render error when loader throws", async ({ expect }) => {
|
|
192
|
+
const alepha = Alepha.create({
|
|
193
|
+
env: { LOG_LEVEL: "error", SERVER_PORT: 0 },
|
|
194
|
+
}).with(ErrorApp);
|
|
195
|
+
|
|
196
|
+
await alepha.start();
|
|
197
|
+
|
|
198
|
+
const server = alepha.inject(ServerProvider);
|
|
199
|
+
const http = alepha.inject(HttpClient);
|
|
200
|
+
|
|
201
|
+
const response = await http.fetch(`${server.hostname}/error`);
|
|
202
|
+
|
|
203
|
+
// Should still return a valid HTML response with error
|
|
204
|
+
expect(response.data).toContain("<!DOCTYPE html>");
|
|
205
|
+
expect(response.data).toContain("Loader error");
|
|
206
|
+
|
|
207
|
+
await alepha.stop();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|