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
|
@@ -64,7 +64,7 @@ export class CloudflareD1Provider extends DatabaseProvider {
|
|
|
64
64
|
protected readonly env = $env(
|
|
65
65
|
t.object({
|
|
66
66
|
DATABASE_URL: t.string({
|
|
67
|
-
description: "Expect to be '
|
|
67
|
+
description: "Expect to be 'd1://name:id'",
|
|
68
68
|
}),
|
|
69
69
|
}),
|
|
70
70
|
);
|
|
@@ -104,41 +104,68 @@ export class CloudflareD1Provider extends DatabaseProvider {
|
|
|
104
104
|
protected readonly onStart = $hook({
|
|
105
105
|
on: "start",
|
|
106
106
|
handler: async () => {
|
|
107
|
-
|
|
108
|
-
"
|
|
109
|
-
|
|
110
|
-
).split(":");
|
|
111
|
-
const cloudflareEnv = this.alepha.store.get("cloudflare.env" as any);
|
|
112
|
-
if (!cloudflareEnv) {
|
|
113
|
-
throw new AlephaError(
|
|
114
|
-
"Cloudflare Workers environment not found in Alepha store under 'cloudflare.env'.",
|
|
107
|
+
try {
|
|
108
|
+
const [bindingName] = this.env.DATABASE_URL.replace("d1://", "").split(
|
|
109
|
+
":",
|
|
115
110
|
);
|
|
111
|
+
const cloudflareEnv = this.alepha.store.get("cloudflare.env" as any);
|
|
112
|
+
if (!cloudflareEnv) {
|
|
113
|
+
throw new AlephaError(
|
|
114
|
+
"Cloudflare Workers environment not found in Alepha store under 'cloudflare.env'.",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const binding = cloudflareEnv[bindingName] as D1Database;
|
|
119
|
+
if (!binding) {
|
|
120
|
+
throw new AlephaError(
|
|
121
|
+
`D1 binding '${bindingName}' not found in Cloudflare Workers environment.`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.d1 = binding;
|
|
126
|
+
|
|
127
|
+
// Dynamic import to avoid crashes when not on Cloudflare
|
|
128
|
+
const { drizzle } = await import("drizzle-orm/d1");
|
|
129
|
+
|
|
130
|
+
this.drizzleDb = drizzle(this.d1) as DrizzleD1Database;
|
|
131
|
+
|
|
132
|
+
// Never migrate in serverless mode - D1 migrations must be applied
|
|
133
|
+
// via `wrangler d1 migrations apply` before deployment
|
|
134
|
+
if (!this.alepha.isServerless()) {
|
|
135
|
+
await this.migrate();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this.log.info("Using Cloudflare D1 database");
|
|
139
|
+
} catch (error) {
|
|
140
|
+
// Log the full error for debugging since Cloudflare Workers
|
|
141
|
+
// doesn't properly display error causes
|
|
142
|
+
const errorMessage =
|
|
143
|
+
error instanceof Error
|
|
144
|
+
? `${error.message}${error.stack ? `\n${error.stack}` : ""}`
|
|
145
|
+
: String(error);
|
|
146
|
+
this.log.error(`D1 initialization failed: ${errorMessage}`);
|
|
147
|
+
throw error;
|
|
116
148
|
}
|
|
117
|
-
|
|
118
|
-
const binding = cloudflareEnv[bindingName] as D1Database;
|
|
119
|
-
if (!binding) {
|
|
120
|
-
throw new AlephaError(
|
|
121
|
-
`D1 binding '${bindingName}' not found in Cloudflare Workers environment.`,
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
this.d1 = binding;
|
|
126
|
-
|
|
127
|
-
// Dynamic import to avoid crashes when not on Cloudflare
|
|
128
|
-
const { drizzle } = await import("drizzle-orm/d1");
|
|
129
|
-
|
|
130
|
-
this.drizzleDb = drizzle(this.d1) as DrizzleD1Database;
|
|
131
|
-
|
|
132
|
-
await this.migrate();
|
|
133
|
-
|
|
134
|
-
this.log.info("Using Cloudflare D1 database");
|
|
135
149
|
},
|
|
136
150
|
});
|
|
137
151
|
|
|
138
152
|
protected async executeMigrations(migrationsFolder: string): Promise<void> {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
153
|
+
this.log.debug(`Running D1 migrations from '${migrationsFolder}'...`);
|
|
154
|
+
try {
|
|
155
|
+
// Dynamic import for D1 migrator
|
|
156
|
+
const { migrate } = await import("drizzle-orm/d1/migrator");
|
|
157
|
+
await migrate(this.db as any, { migrationsFolder });
|
|
158
|
+
this.log.debug("D1 migrations completed successfully");
|
|
159
|
+
} catch (error) {
|
|
160
|
+
const errorMessage =
|
|
161
|
+
error instanceof Error
|
|
162
|
+
? `${error.name}: ${error.message}`
|
|
163
|
+
: String(error);
|
|
164
|
+
throw new AlephaError(
|
|
165
|
+
`D1 migration failed from '${migrationsFolder}': ${errorMessage}`,
|
|
166
|
+
{ cause: error },
|
|
167
|
+
);
|
|
168
|
+
}
|
|
142
169
|
}
|
|
143
170
|
|
|
144
171
|
/**
|
|
@@ -97,9 +97,17 @@ export abstract class DatabaseProvider {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
|
-
* Base migration orchestration - handles environment logic
|
|
100
|
+
* Base migration orchestration - handles environment logic.
|
|
101
|
+
*
|
|
102
|
+
* Never runs in serverless mode - migrations should be applied during
|
|
103
|
+
* deployment, not at runtime (to avoid race conditions and timeouts).
|
|
101
104
|
*/
|
|
102
105
|
public async migrate(): Promise<void> {
|
|
106
|
+
// Never migrate in serverless mode - migrations should be applied during deployment
|
|
107
|
+
if (this.alepha.isServerless()) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
103
111
|
const migrationsFolder = this.getMigrationsFolder();
|
|
104
112
|
|
|
105
113
|
// Handle different environments
|
|
@@ -157,7 +157,10 @@ export class NodeSqliteProvider extends DatabaseProvider {
|
|
|
157
157
|
|
|
158
158
|
this.sqlite = new DatabaseSync(filepath);
|
|
159
159
|
|
|
160
|
-
|
|
160
|
+
// Never migrate in serverless mode - migrations should be applied during deployment
|
|
161
|
+
if (!this.alepha.isServerless()) {
|
|
162
|
+
await this.migrate();
|
|
163
|
+
}
|
|
161
164
|
|
|
162
165
|
this.log.info(`Using SQLite database at ${filepath}`);
|
|
163
166
|
},
|
|
@@ -13,7 +13,7 @@ import { ReactRouter } from "../services/ReactRouter.ts";
|
|
|
13
13
|
* }
|
|
14
14
|
*
|
|
15
15
|
* const router = useRouter<App>();
|
|
16
|
-
* router.
|
|
16
|
+
* router.push("home"); // typesafe
|
|
17
17
|
*/
|
|
18
18
|
export const useRouter = <T extends object = any>(): ReactRouter<T> => {
|
|
19
19
|
return useInject(ReactRouter<T>);
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
ReactPageProvider,
|
|
12
12
|
type ReactRouterState,
|
|
13
13
|
} from "./providers/ReactPageProvider.ts";
|
|
14
|
+
import { ReactPreloadProvider } from "./providers/ReactPreloadProvider.ts";
|
|
14
15
|
import { ReactServerProvider } from "./providers/ReactServerProvider.ts";
|
|
15
16
|
import { ReactServerTemplateProvider } from "./providers/ReactServerTemplateProvider.ts";
|
|
16
17
|
import { SSRManifestProvider } from "./providers/SSRManifestProvider.ts";
|
|
@@ -23,6 +24,7 @@ import { ReactRouter } from "./services/ReactRouter.ts";
|
|
|
23
24
|
export * from "./index.shared.ts";
|
|
24
25
|
export * from "./providers/ReactBrowserProvider.ts";
|
|
25
26
|
export * from "./providers/ReactPageProvider.ts";
|
|
27
|
+
export * from "./providers/ReactPreloadProvider.ts";
|
|
26
28
|
export * from "./providers/ReactServerProvider.ts";
|
|
27
29
|
export * from "./providers/ReactServerTemplateProvider.ts";
|
|
28
30
|
export * from "./providers/SSRManifestProvider.ts";
|
|
@@ -117,6 +119,7 @@ export const AlephaReactRouter = $module({
|
|
|
117
119
|
services: [
|
|
118
120
|
ReactPageProvider,
|
|
119
121
|
ReactPageService,
|
|
122
|
+
ReactPreloadProvider,
|
|
120
123
|
ReactRouter,
|
|
121
124
|
ReactServerProvider,
|
|
122
125
|
ReactServerTemplateProvider,
|
|
@@ -136,6 +139,7 @@ export const AlephaReactRouter = $module({
|
|
|
136
139
|
})
|
|
137
140
|
.with(SSRManifestProvider)
|
|
138
141
|
.with(ReactServerTemplateProvider)
|
|
142
|
+
.with(ReactPreloadProvider)
|
|
139
143
|
.with(ReactServerProvider)
|
|
140
144
|
.with(ReactPageProvider)
|
|
141
145
|
.with(ReactRouter),
|
|
@@ -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
|
+
});
|