alepha 0.15.2 → 0.15.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/README.md +68 -80
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/audits/index.js +8 -0
- package/dist/api/audits/index.js.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/files/index.js +1 -0
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +3 -0
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/notifications/index.browser.js +1 -0
- package/dist/api/notifications/index.browser.js.map +1 -1
- package/dist/api/notifications/index.js +1 -0
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/parameters/index.d.ts +260 -260
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +10 -0
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/api/users/index.d.ts +12 -1
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +18 -2
- package/dist/api/users/index.js.map +1 -1
- package/dist/batch/index.d.ts +4 -4
- 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 +196 -74
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +234 -50
- 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 +21 -13
- package/dist/email/index.d.ts.map +1 -1
- package/dist/email/index.js +10561 -4
- package/dist/email/index.js.map +1 -1
- package/dist/lock/core/index.d.ts +6 -1
- package/dist/lock/core/index.d.ts.map +1 -1
- package/dist/lock/core/index.js +9 -1
- package/dist/lock/core/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/auth/index.browser.js +2 -1
- package/dist/react/auth/index.browser.js.map +1 -1
- package/dist/react/auth/index.js +2 -1
- package/dist/react/auth/index.js.map +1 -1
- package/dist/react/core/index.d.ts +3 -3
- 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 +305 -407
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js +581 -781
- package/dist/react/router/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +13 -1
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +42 -4
- package/dist/scheduler/index.js.map +1 -1
- 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/compress/index.d.ts.map +1 -1
- package/dist/server/compress/index.js +1 -0
- package/dist/server/compress/index.js.map +1 -1
- package/dist/server/health/index.d.ts +17 -17
- package/dist/server/links/index.d.ts +39 -39
- package/dist/server/links/index.js +1 -1
- package/dist/server/links/index.js.map +1 -1
- 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 +3 -2
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +42 -8
- package/dist/vite/index.js.map +1 -1
- package/dist/websocket/index.d.ts +34 -34
- package/dist/websocket/index.d.ts.map +1 -1
- package/package.json +9 -4
- package/src/api/audits/controllers/AdminAuditController.ts +8 -0
- package/src/api/files/controllers/AdminFileStatsController.ts +1 -0
- package/src/api/jobs/controllers/AdminJobController.ts +3 -0
- package/src/api/logs/TODO.md +13 -10
- package/src/api/notifications/controllers/AdminNotificationController.ts +1 -0
- package/src/api/parameters/controllers/AdminConfigController.ts +10 -0
- package/src/api/users/controllers/AdminIdentityController.ts +3 -0
- package/src/api/users/controllers/AdminSessionController.ts +3 -0
- package/src/api/users/controllers/AdminUserController.ts +5 -0
- package/src/cli/apps/AlephaPackageBuilderCli.ts +9 -0
- package/src/cli/atoms/buildOptions.ts +99 -9
- package/src/cli/commands/build.ts +150 -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 +31 -9
- 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/email/index.workerd.ts +36 -0
- package/src/email/providers/WorkermailerEmailProvider.ts +221 -0
- package/src/lock/core/primitives/$lock.ts +13 -1
- 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/auth/services/ReactAuth.ts +3 -1
- package/src/react/router/atoms/ssrManifestAtom.ts +7 -0
- 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 +21 -82
- package/src/react/router/providers/ReactServerTemplateProvider.spec.ts +210 -0
- package/src/react/router/providers/ReactServerTemplateProvider.ts +228 -665
- package/src/react/router/providers/SSRManifestProvider.ts +7 -0
- package/src/react/router/services/ReactRouter.ts +13 -13
- package/src/scheduler/index.workerd.ts +43 -0
- package/src/scheduler/providers/CronProvider.ts +53 -6
- package/src/scheduler/providers/WorkerdCronProvider.ts +102 -0
- package/src/security/__tests__/ServerSecurityProvider.spec.ts +77 -0
- package/src/security/providers/ServerSecurityProvider.ts +30 -22
- package/src/server/compress/providers/ServerCompressProvider.ts +6 -0
- package/src/server/core/providers/NodeHttpServerProvider.spec.ts +9 -3
- package/src/server/links/providers/ServerLinksProvider.spec.ts +332 -0
- package/src/server/links/providers/ServerLinksProvider.ts +1 -1
- 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 +47 -8
- package/src/vite/tasks/generateDocker.ts +4 -0
|
@@ -34,7 +34,7 @@ describe("$page browser tests", () => {
|
|
|
34
34
|
const router = alepha.inject(ReactRouter);
|
|
35
35
|
|
|
36
36
|
await act(async () => {
|
|
37
|
-
await router.
|
|
37
|
+
await router.push("/");
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
await waitFor(() => {
|
|
@@ -64,7 +64,7 @@ describe("$page browser tests", () => {
|
|
|
64
64
|
|
|
65
65
|
// Navigate to home
|
|
66
66
|
await act(async () => {
|
|
67
|
-
await router.
|
|
67
|
+
await router.push("/");
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
await waitFor(() => {
|
|
@@ -73,7 +73,7 @@ describe("$page browser tests", () => {
|
|
|
73
73
|
|
|
74
74
|
// Navigate to about
|
|
75
75
|
await act(async () => {
|
|
76
|
-
await router.
|
|
76
|
+
await router.push("/about");
|
|
77
77
|
});
|
|
78
78
|
|
|
79
79
|
await waitFor(() => {
|
|
@@ -119,7 +119,7 @@ describe("$page browser tests", () => {
|
|
|
119
119
|
const router = alepha.inject(ReactRouter);
|
|
120
120
|
|
|
121
121
|
await act(async () => {
|
|
122
|
-
await router.
|
|
122
|
+
await router.push("/user/123");
|
|
123
123
|
});
|
|
124
124
|
|
|
125
125
|
await waitFor(() => {
|
|
@@ -153,7 +153,7 @@ describe("$page browser tests", () => {
|
|
|
153
153
|
const router = alepha.inject(ReactRouter);
|
|
154
154
|
|
|
155
155
|
await act(async () => {
|
|
156
|
-
await router.
|
|
156
|
+
await router.push("/async");
|
|
157
157
|
});
|
|
158
158
|
|
|
159
159
|
await waitFor(
|
|
@@ -204,7 +204,7 @@ describe("$page browser tests", () => {
|
|
|
204
204
|
|
|
205
205
|
// Try to access protected - should redirect to login
|
|
206
206
|
await act(async () => {
|
|
207
|
-
await router.
|
|
207
|
+
await router.push("/protected");
|
|
208
208
|
});
|
|
209
209
|
|
|
210
210
|
await waitFor(() => {
|
|
@@ -215,7 +215,7 @@ describe("$page browser tests", () => {
|
|
|
215
215
|
isAuthenticated = true;
|
|
216
216
|
|
|
217
217
|
await act(async () => {
|
|
218
|
-
await router.
|
|
218
|
+
await router.push("/protected");
|
|
219
219
|
});
|
|
220
220
|
|
|
221
221
|
await waitFor(() => {
|
|
@@ -248,7 +248,7 @@ describe("$page browser tests", () => {
|
|
|
248
248
|
const router = alepha.inject(ReactRouter);
|
|
249
249
|
|
|
250
250
|
await act(async () => {
|
|
251
|
-
await router.
|
|
251
|
+
await router.push("/error");
|
|
252
252
|
});
|
|
253
253
|
|
|
254
254
|
await waitFor(() => {
|
|
@@ -294,7 +294,7 @@ describe("$page browser tests", () => {
|
|
|
294
294
|
|
|
295
295
|
// Navigate to home - should show layout and home
|
|
296
296
|
await act(async () => {
|
|
297
|
-
await router.
|
|
297
|
+
await router.push("/");
|
|
298
298
|
});
|
|
299
299
|
|
|
300
300
|
await waitFor(() => {
|
|
@@ -306,7 +306,7 @@ describe("$page browser tests", () => {
|
|
|
306
306
|
|
|
307
307
|
// Navigate to about - layout should persist
|
|
308
308
|
await act(async () => {
|
|
309
|
-
await router.
|
|
309
|
+
await router.push("/about");
|
|
310
310
|
});
|
|
311
311
|
|
|
312
312
|
await waitFor(() => {
|
|
@@ -348,7 +348,7 @@ describe("$page browser tests", () => {
|
|
|
348
348
|
const router = alepha.inject(ReactRouter);
|
|
349
349
|
|
|
350
350
|
await act(async () => {
|
|
351
|
-
await router.
|
|
351
|
+
await router.push("/page");
|
|
352
352
|
});
|
|
353
353
|
|
|
354
354
|
await waitFor(() => {
|
|
@@ -404,7 +404,7 @@ describe("$page browser tests", () => {
|
|
|
404
404
|
const router = alepha.inject(ReactRouter);
|
|
405
405
|
|
|
406
406
|
await act(async () => {
|
|
407
|
-
await router.
|
|
407
|
+
await router.push("/section/page");
|
|
408
408
|
});
|
|
409
409
|
|
|
410
410
|
await waitFor(() => {
|
|
@@ -441,7 +441,7 @@ describe("$page browser tests", () => {
|
|
|
441
441
|
|
|
442
442
|
// Navigate to home
|
|
443
443
|
await act(async () => {
|
|
444
|
-
await router.
|
|
444
|
+
await router.push("/");
|
|
445
445
|
});
|
|
446
446
|
|
|
447
447
|
await waitFor(() => {
|
|
@@ -452,7 +452,7 @@ describe("$page browser tests", () => {
|
|
|
452
452
|
|
|
453
453
|
// Navigate away from home to about
|
|
454
454
|
await act(async () => {
|
|
455
|
-
await router.
|
|
455
|
+
await router.push("/about");
|
|
456
456
|
});
|
|
457
457
|
|
|
458
458
|
await waitFor(() => {
|
|
@@ -490,7 +490,7 @@ describe("$page browser tests", () => {
|
|
|
490
490
|
|
|
491
491
|
// Navigate to home first
|
|
492
492
|
await act(async () => {
|
|
493
|
-
await router.
|
|
493
|
+
await router.push("/");
|
|
494
494
|
});
|
|
495
495
|
|
|
496
496
|
await waitFor(() => {
|
|
@@ -503,7 +503,7 @@ describe("$page browser tests", () => {
|
|
|
503
503
|
|
|
504
504
|
// Navigate to about - onEnter should be called
|
|
505
505
|
await act(async () => {
|
|
506
|
-
await router.
|
|
506
|
+
await router.push("/about");
|
|
507
507
|
});
|
|
508
508
|
|
|
509
509
|
await waitFor(() => {
|
|
@@ -533,7 +533,7 @@ describe("$page browser tests", () => {
|
|
|
533
533
|
|
|
534
534
|
// Navigate to home - onEnter should be called
|
|
535
535
|
await act(async () => {
|
|
536
|
-
await router.
|
|
536
|
+
await router.push("/");
|
|
537
537
|
});
|
|
538
538
|
|
|
539
539
|
await waitFor(() => {
|
|
@@ -582,7 +582,7 @@ describe("$page browser tests", () => {
|
|
|
582
582
|
|
|
583
583
|
// Navigate to child1
|
|
584
584
|
await act(async () => {
|
|
585
|
-
await router.
|
|
585
|
+
await router.push("/child1");
|
|
586
586
|
});
|
|
587
587
|
|
|
588
588
|
await waitFor(() => {
|
|
@@ -595,7 +595,7 @@ describe("$page browser tests", () => {
|
|
|
595
595
|
|
|
596
596
|
// Navigate to child2 - parent onEnter should NOT be called again
|
|
597
597
|
await act(async () => {
|
|
598
|
-
await router.
|
|
598
|
+
await router.push("/child2");
|
|
599
599
|
});
|
|
600
600
|
|
|
601
601
|
await waitFor(() => {
|
|
@@ -633,7 +633,7 @@ describe("$page browser tests", () => {
|
|
|
633
633
|
const router = alepha.inject(ReactRouter);
|
|
634
634
|
|
|
635
635
|
await act(async () => {
|
|
636
|
-
await router.
|
|
636
|
+
await router.push("/user/abc123");
|
|
637
637
|
});
|
|
638
638
|
|
|
639
639
|
await waitFor(() => {
|
|
@@ -678,7 +678,7 @@ describe("$page browser tests", () => {
|
|
|
678
678
|
const router = alepha.inject(ReactRouter);
|
|
679
679
|
|
|
680
680
|
await act(async () => {
|
|
681
|
-
await router.
|
|
681
|
+
await router.push("/search?q=typescript&page=2");
|
|
682
682
|
});
|
|
683
683
|
|
|
684
684
|
await waitFor(() => {
|
|
@@ -728,7 +728,7 @@ describe("$page browser tests", () => {
|
|
|
728
728
|
|
|
729
729
|
// Navigate with query params
|
|
730
730
|
await act(async () => {
|
|
731
|
-
await router.
|
|
731
|
+
await router.push("/search?q=alepha&page=5");
|
|
732
732
|
});
|
|
733
733
|
|
|
734
734
|
await waitFor(() => {
|
|
@@ -783,7 +783,7 @@ describe("$page browser tests", () => {
|
|
|
783
783
|
const router = alepha.inject(ReactRouter);
|
|
784
784
|
|
|
785
785
|
await act(async () => {
|
|
786
|
-
await router.
|
|
786
|
+
await router.push("/users/john/posts?sort=popular&limit=20");
|
|
787
787
|
});
|
|
788
788
|
|
|
789
789
|
await waitFor(() => {
|
|
@@ -835,7 +835,7 @@ describe("$page browser tests", () => {
|
|
|
835
835
|
|
|
836
836
|
// Navigate without query params - should use defaults
|
|
837
837
|
await act(async () => {
|
|
838
|
-
await router.
|
|
838
|
+
await router.push("/search");
|
|
839
839
|
});
|
|
840
840
|
|
|
841
841
|
await waitFor(() => {
|
|
@@ -521,38 +521,6 @@ describe("$page primitive tests", () => {
|
|
|
521
521
|
expect(functionRendered.html).toBe("Function animation");
|
|
522
522
|
});
|
|
523
523
|
|
|
524
|
-
test("$page - match method (not implemented)", ({ expect }) => {
|
|
525
|
-
class App {
|
|
526
|
-
page = $page({
|
|
527
|
-
path: "/test",
|
|
528
|
-
component: () => "test",
|
|
529
|
-
});
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
const app = alepha.inject(App);
|
|
533
|
-
|
|
534
|
-
expect(app.page.match("/test")).toBe(false);
|
|
535
|
-
expect(app.page.match("/other")).toBe(false);
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
test("$page - pathname method", ({ expect }) => {
|
|
539
|
-
class App {
|
|
540
|
-
withPath = $page({
|
|
541
|
-
path: "/test/:id",
|
|
542
|
-
component: () => "test",
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
withoutPath = $page({
|
|
546
|
-
component: () => "test",
|
|
547
|
-
});
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
const app = alepha.inject(App);
|
|
551
|
-
|
|
552
|
-
expect(app.withPath.pathname({})).toBe("/test/:id");
|
|
553
|
-
expect(app.withoutPath.pathname({})).toBe("");
|
|
554
|
-
});
|
|
555
|
-
|
|
556
524
|
test("$page - complex schema with nested objects", async ({ expect }) => {
|
|
557
525
|
class App {
|
|
558
526
|
complex = $page({
|
|
@@ -368,10 +368,7 @@ export interface PagePrimitiveOptions<
|
|
|
368
368
|
[PAGE_PRELOAD_KEY]?: string;
|
|
369
369
|
}
|
|
370
370
|
|
|
371
|
-
|
|
372
|
-
error: Error,
|
|
373
|
-
state: ReactRouterState,
|
|
374
|
-
) => ReactNode | Redirection | undefined;
|
|
371
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
375
372
|
|
|
376
373
|
export class PagePrimitive<
|
|
377
374
|
TConfig extends PageConfigSchema = PageConfigSchema,
|
|
@@ -413,22 +410,17 @@ export class PagePrimitive<
|
|
|
413
410
|
}> {
|
|
414
411
|
return this.reactPageService.fetch(this.options.path || "", options);
|
|
415
412
|
}
|
|
416
|
-
|
|
417
|
-
public match(url: string): boolean {
|
|
418
|
-
// TODO: Implement a way to match the URL against the pathname
|
|
419
|
-
return false;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
public pathname(config: any) {
|
|
423
|
-
// TODO: Implement a way to generate the pathname based on the config
|
|
424
|
-
return this.options.path || "";
|
|
425
|
-
}
|
|
426
413
|
}
|
|
427
414
|
|
|
428
415
|
$page[KIND] = PagePrimitive;
|
|
429
416
|
|
|
430
417
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
431
418
|
|
|
419
|
+
export type ErrorHandler = (
|
|
420
|
+
error: Error,
|
|
421
|
+
state: ReactRouterState,
|
|
422
|
+
) => ReactNode | Redirection | undefined;
|
|
423
|
+
|
|
432
424
|
export interface PageConfigSchema {
|
|
433
425
|
query?: TSchema;
|
|
434
426
|
params?: TSchema;
|
|
@@ -12,14 +12,14 @@ import { DateTimeProvider } from "alepha/datetime";
|
|
|
12
12
|
import { $logger } from "alepha/logger";
|
|
13
13
|
import { BrowserHeadProvider } from "alepha/react/head";
|
|
14
14
|
import { LinkProvider } from "alepha/server/links";
|
|
15
|
-
import type {
|
|
15
|
+
import type { RouterPushOptions } from "../services/ReactRouter.ts";
|
|
16
16
|
import { ReactBrowserRouterProvider } from "./ReactBrowserRouterProvider.ts";
|
|
17
17
|
import type {
|
|
18
18
|
PreviousLayerData,
|
|
19
19
|
ReactRouterState,
|
|
20
20
|
} from "./ReactPageProvider.ts";
|
|
21
21
|
|
|
22
|
-
export type {
|
|
22
|
+
export type { RouterPushOptions } from "../services/ReactRouter.ts";
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* React browser renderer configuration atom
|
|
@@ -158,7 +158,10 @@ export class ReactBrowserProvider {
|
|
|
158
158
|
await this.render({ previous });
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
public async
|
|
161
|
+
public async push(
|
|
162
|
+
url: string,
|
|
163
|
+
options: RouterPushOptions = {},
|
|
164
|
+
): Promise<void> {
|
|
162
165
|
this.log.trace(`Going to ${url}`, {
|
|
163
166
|
url,
|
|
164
167
|
options,
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { Alepha } from "alepha";
|
|
2
|
+
import { HttpClient, ServerProvider } from "alepha/server";
|
|
3
|
+
import { describe, it } from "vitest";
|
|
4
|
+
import { ssrManifestAtom } from "../atoms/ssrManifestAtom.ts";
|
|
5
|
+
import { $page } from "../index.ts";
|
|
6
|
+
|
|
7
|
+
describe("ReactPreloadProvider", () => {
|
|
8
|
+
class App {
|
|
9
|
+
home = $page({
|
|
10
|
+
path: "/",
|
|
11
|
+
component: () => "Hello World",
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
it("should add Link header with entry assets to HTML responses", async ({
|
|
16
|
+
expect,
|
|
17
|
+
}) => {
|
|
18
|
+
const alepha = Alepha.create({
|
|
19
|
+
env: { LOG_LEVEL: "error", SERVER_PORT: 0 },
|
|
20
|
+
}).with(App);
|
|
21
|
+
|
|
22
|
+
// Set up mock SSR manifest with entry assets
|
|
23
|
+
alepha.store.set(ssrManifestAtom, {
|
|
24
|
+
client: {
|
|
25
|
+
"src/entry.tsx": {
|
|
26
|
+
file: "assets/entry.abc123.js",
|
|
27
|
+
isEntry: true,
|
|
28
|
+
css: ["assets/style.def456.css"],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
await alepha.start();
|
|
34
|
+
|
|
35
|
+
const server = alepha.inject(ServerProvider);
|
|
36
|
+
const http = alepha.inject(HttpClient);
|
|
37
|
+
|
|
38
|
+
const response = await http.fetch(`${server.hostname}/`);
|
|
39
|
+
|
|
40
|
+
// Check that the Link header is present
|
|
41
|
+
const linkHeader = response.headers.get("link");
|
|
42
|
+
expect(linkHeader).toBeTruthy();
|
|
43
|
+
expect(linkHeader).toContain(
|
|
44
|
+
"</assets/style.def456.css>; rel=preload; as=style",
|
|
45
|
+
);
|
|
46
|
+
expect(linkHeader).toContain(
|
|
47
|
+
"</assets/entry.abc123.js>; rel=modulepreload",
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
await alepha.stop();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should not add Link header when no SSR manifest is available", async ({
|
|
54
|
+
expect,
|
|
55
|
+
}) => {
|
|
56
|
+
const alepha = Alepha.create({
|
|
57
|
+
env: { LOG_LEVEL: "error", SERVER_PORT: 0 },
|
|
58
|
+
}).with(App);
|
|
59
|
+
|
|
60
|
+
// No SSR manifest set
|
|
61
|
+
|
|
62
|
+
await alepha.start();
|
|
63
|
+
|
|
64
|
+
const server = alepha.inject(ServerProvider);
|
|
65
|
+
const http = alepha.inject(HttpClient);
|
|
66
|
+
|
|
67
|
+
const response = await http.fetch(`${server.hostname}/`);
|
|
68
|
+
|
|
69
|
+
// Link header should not be present (or empty)
|
|
70
|
+
const linkHeader = response.headers.get("link");
|
|
71
|
+
expect(linkHeader).toBeNull();
|
|
72
|
+
|
|
73
|
+
await alepha.stop();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("should handle base path in manifest", async ({ expect }) => {
|
|
77
|
+
const alepha = Alepha.create({
|
|
78
|
+
env: { LOG_LEVEL: "error", SERVER_PORT: 0 },
|
|
79
|
+
}).with(App);
|
|
80
|
+
|
|
81
|
+
// Set up mock SSR manifest with base path
|
|
82
|
+
alepha.store.set(ssrManifestAtom, {
|
|
83
|
+
base: "/app",
|
|
84
|
+
client: {
|
|
85
|
+
"src/entry.tsx": {
|
|
86
|
+
file: "assets/entry.abc123.js",
|
|
87
|
+
isEntry: true,
|
|
88
|
+
css: ["assets/style.def456.css"],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await alepha.start();
|
|
94
|
+
|
|
95
|
+
const server = alepha.inject(ServerProvider);
|
|
96
|
+
const http = alepha.inject(HttpClient);
|
|
97
|
+
|
|
98
|
+
const response = await http.fetch(`${server.hostname}/`);
|
|
99
|
+
|
|
100
|
+
// Check that the Link header includes base path
|
|
101
|
+
const linkHeader = response.headers.get("link");
|
|
102
|
+
expect(linkHeader).toBeTruthy();
|
|
103
|
+
expect(linkHeader).toContain(
|
|
104
|
+
"</app/assets/style.def456.css>; rel=preload; as=style",
|
|
105
|
+
);
|
|
106
|
+
expect(linkHeader).toContain(
|
|
107
|
+
"</app/assets/entry.abc123.js>; rel=modulepreload",
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
await alepha.stop();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should handle entry with only JS (no CSS)", async ({ expect }) => {
|
|
114
|
+
const alepha = Alepha.create({
|
|
115
|
+
env: { LOG_LEVEL: "error", SERVER_PORT: 0 },
|
|
116
|
+
}).with(App);
|
|
117
|
+
|
|
118
|
+
// Set up mock SSR manifest with only JS entry
|
|
119
|
+
alepha.store.set(ssrManifestAtom, {
|
|
120
|
+
client: {
|
|
121
|
+
"src/entry.tsx": {
|
|
122
|
+
file: "assets/entry.abc123.js",
|
|
123
|
+
isEntry: true,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
await alepha.start();
|
|
129
|
+
|
|
130
|
+
const server = alepha.inject(ServerProvider);
|
|
131
|
+
const http = alepha.inject(HttpClient);
|
|
132
|
+
|
|
133
|
+
const response = await http.fetch(`${server.hostname}/`);
|
|
134
|
+
|
|
135
|
+
// Check that the Link header contains only JS modulepreload
|
|
136
|
+
const linkHeader = response.headers.get("link");
|
|
137
|
+
expect(linkHeader).toBeTruthy();
|
|
138
|
+
expect(linkHeader).toBe("</assets/entry.abc123.js>; rel=modulepreload");
|
|
139
|
+
|
|
140
|
+
await alepha.stop();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -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
|
+
}
|