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.
Files changed (180) hide show
  1. package/README.md +68 -80
  2. package/dist/api/audits/index.d.ts.map +1 -1
  3. package/dist/api/audits/index.js +8 -0
  4. package/dist/api/audits/index.js.map +1 -1
  5. package/dist/api/files/index.d.ts +170 -170
  6. package/dist/api/files/index.d.ts.map +1 -1
  7. package/dist/api/files/index.js +1 -0
  8. package/dist/api/files/index.js.map +1 -1
  9. package/dist/api/jobs/index.d.ts.map +1 -1
  10. package/dist/api/jobs/index.js +3 -0
  11. package/dist/api/jobs/index.js.map +1 -1
  12. package/dist/api/notifications/index.browser.js +1 -0
  13. package/dist/api/notifications/index.browser.js.map +1 -1
  14. package/dist/api/notifications/index.js +1 -0
  15. package/dist/api/notifications/index.js.map +1 -1
  16. package/dist/api/parameters/index.d.ts +260 -260
  17. package/dist/api/parameters/index.d.ts.map +1 -1
  18. package/dist/api/parameters/index.js +10 -0
  19. package/dist/api/parameters/index.js.map +1 -1
  20. package/dist/api/users/index.d.ts +12 -1
  21. package/dist/api/users/index.d.ts.map +1 -1
  22. package/dist/api/users/index.js +18 -2
  23. package/dist/api/users/index.js.map +1 -1
  24. package/dist/batch/index.d.ts +4 -4
  25. package/dist/bucket/index.d.ts +8 -0
  26. package/dist/bucket/index.d.ts.map +1 -1
  27. package/dist/bucket/index.js +7 -2
  28. package/dist/bucket/index.js.map +1 -1
  29. package/dist/cli/index.d.ts +196 -74
  30. package/dist/cli/index.d.ts.map +1 -1
  31. package/dist/cli/index.js +234 -50
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/command/index.d.ts +10 -0
  34. package/dist/command/index.d.ts.map +1 -1
  35. package/dist/command/index.js +67 -13
  36. package/dist/command/index.js.map +1 -1
  37. package/dist/core/index.browser.js +28 -21
  38. package/dist/core/index.browser.js.map +1 -1
  39. package/dist/core/index.d.ts.map +1 -1
  40. package/dist/core/index.js +28 -21
  41. package/dist/core/index.js.map +1 -1
  42. package/dist/core/index.native.js +28 -21
  43. package/dist/core/index.native.js.map +1 -1
  44. package/dist/email/index.d.ts +21 -13
  45. package/dist/email/index.d.ts.map +1 -1
  46. package/dist/email/index.js +10561 -4
  47. package/dist/email/index.js.map +1 -1
  48. package/dist/lock/core/index.d.ts +6 -1
  49. package/dist/lock/core/index.d.ts.map +1 -1
  50. package/dist/lock/core/index.js +9 -1
  51. package/dist/lock/core/index.js.map +1 -1
  52. package/dist/mcp/index.d.ts +5 -5
  53. package/dist/orm/index.bun.js +32 -16
  54. package/dist/orm/index.bun.js.map +1 -1
  55. package/dist/orm/index.d.ts +4 -1
  56. package/dist/orm/index.d.ts.map +1 -1
  57. package/dist/orm/index.js +34 -22
  58. package/dist/orm/index.js.map +1 -1
  59. package/dist/react/auth/index.browser.js +2 -1
  60. package/dist/react/auth/index.browser.js.map +1 -1
  61. package/dist/react/auth/index.js +2 -1
  62. package/dist/react/auth/index.js.map +1 -1
  63. package/dist/react/core/index.d.ts +3 -3
  64. package/dist/react/router/index.browser.js +9 -15
  65. package/dist/react/router/index.browser.js.map +1 -1
  66. package/dist/react/router/index.d.ts +305 -407
  67. package/dist/react/router/index.d.ts.map +1 -1
  68. package/dist/react/router/index.js +581 -781
  69. package/dist/react/router/index.js.map +1 -1
  70. package/dist/scheduler/index.d.ts +13 -1
  71. package/dist/scheduler/index.d.ts.map +1 -1
  72. package/dist/scheduler/index.js +42 -4
  73. package/dist/scheduler/index.js.map +1 -1
  74. package/dist/security/index.d.ts +42 -42
  75. package/dist/security/index.d.ts.map +1 -1
  76. package/dist/security/index.js +8 -7
  77. package/dist/security/index.js.map +1 -1
  78. package/dist/server/auth/index.d.ts +167 -167
  79. package/dist/server/compress/index.d.ts.map +1 -1
  80. package/dist/server/compress/index.js +1 -0
  81. package/dist/server/compress/index.js.map +1 -1
  82. package/dist/server/health/index.d.ts +17 -17
  83. package/dist/server/links/index.d.ts +39 -39
  84. package/dist/server/links/index.js +1 -1
  85. package/dist/server/links/index.js.map +1 -1
  86. package/dist/server/static/index.js +7 -2
  87. package/dist/server/static/index.js.map +1 -1
  88. package/dist/server/swagger/index.d.ts +8 -0
  89. package/dist/server/swagger/index.d.ts.map +1 -1
  90. package/dist/server/swagger/index.js +7 -2
  91. package/dist/server/swagger/index.js.map +1 -1
  92. package/dist/sms/index.d.ts +8 -0
  93. package/dist/sms/index.d.ts.map +1 -1
  94. package/dist/sms/index.js +7 -2
  95. package/dist/sms/index.js.map +1 -1
  96. package/dist/system/index.browser.js +734 -12
  97. package/dist/system/index.browser.js.map +1 -1
  98. package/dist/system/index.d.ts +8 -0
  99. package/dist/system/index.d.ts.map +1 -1
  100. package/dist/system/index.js +7 -2
  101. package/dist/system/index.js.map +1 -1
  102. package/dist/vite/index.d.ts +3 -2
  103. package/dist/vite/index.d.ts.map +1 -1
  104. package/dist/vite/index.js +42 -8
  105. package/dist/vite/index.js.map +1 -1
  106. package/dist/websocket/index.d.ts +34 -34
  107. package/dist/websocket/index.d.ts.map +1 -1
  108. package/package.json +9 -4
  109. package/src/api/audits/controllers/AdminAuditController.ts +8 -0
  110. package/src/api/files/controllers/AdminFileStatsController.ts +1 -0
  111. package/src/api/jobs/controllers/AdminJobController.ts +3 -0
  112. package/src/api/logs/TODO.md +13 -10
  113. package/src/api/notifications/controllers/AdminNotificationController.ts +1 -0
  114. package/src/api/parameters/controllers/AdminConfigController.ts +10 -0
  115. package/src/api/users/controllers/AdminIdentityController.ts +3 -0
  116. package/src/api/users/controllers/AdminSessionController.ts +3 -0
  117. package/src/api/users/controllers/AdminUserController.ts +5 -0
  118. package/src/cli/apps/AlephaPackageBuilderCli.ts +9 -0
  119. package/src/cli/atoms/buildOptions.ts +99 -9
  120. package/src/cli/commands/build.ts +150 -32
  121. package/src/cli/commands/db.ts +5 -7
  122. package/src/cli/commands/init.spec.ts +50 -6
  123. package/src/cli/commands/init.ts +28 -5
  124. package/src/cli/providers/ViteDevServerProvider.ts +31 -9
  125. package/src/cli/services/AlephaCliUtils.ts +16 -0
  126. package/src/cli/services/PackageManagerUtils.ts +2 -0
  127. package/src/cli/services/ProjectScaffolder.spec.ts +97 -0
  128. package/src/cli/services/ProjectScaffolder.ts +28 -6
  129. package/src/cli/templates/agentMd.ts +6 -1
  130. package/src/cli/templates/apiAppSecurityTs.ts +11 -0
  131. package/src/cli/templates/apiIndexTs.ts +18 -4
  132. package/src/cli/templates/webAppRouterTs.ts +25 -1
  133. package/src/cli/templates/webHelloComponentTsx.ts +15 -5
  134. package/src/command/helpers/Runner.spec.ts +135 -0
  135. package/src/command/helpers/Runner.ts +4 -1
  136. package/src/command/providers/CliProvider.spec.ts +325 -0
  137. package/src/command/providers/CliProvider.ts +117 -7
  138. package/src/core/Alepha.ts +32 -25
  139. package/src/email/index.workerd.ts +36 -0
  140. package/src/email/providers/WorkermailerEmailProvider.ts +221 -0
  141. package/src/lock/core/primitives/$lock.ts +13 -1
  142. package/src/orm/index.bun.ts +1 -1
  143. package/src/orm/index.ts +2 -6
  144. package/src/orm/providers/drivers/BunSqliteProvider.ts +4 -1
  145. package/src/orm/providers/drivers/CloudflareD1Provider.ts +57 -30
  146. package/src/orm/providers/drivers/DatabaseProvider.ts +9 -1
  147. package/src/orm/providers/drivers/NodeSqliteProvider.ts +4 -1
  148. package/src/react/auth/services/ReactAuth.ts +3 -1
  149. package/src/react/router/atoms/ssrManifestAtom.ts +7 -0
  150. package/src/react/router/hooks/useActive.ts +1 -1
  151. package/src/react/router/hooks/useRouter.ts +1 -1
  152. package/src/react/router/index.ts +4 -0
  153. package/src/react/router/primitives/$page.browser.spec.tsx +24 -24
  154. package/src/react/router/primitives/$page.spec.tsx +0 -32
  155. package/src/react/router/primitives/$page.ts +6 -14
  156. package/src/react/router/providers/ReactBrowserProvider.ts +6 -3
  157. package/src/react/router/providers/ReactPageProvider.ts +1 -1
  158. package/src/react/router/providers/ReactPreloadProvider.spec.ts +142 -0
  159. package/src/react/router/providers/ReactPreloadProvider.ts +85 -0
  160. package/src/react/router/providers/ReactServerProvider.ts +21 -82
  161. package/src/react/router/providers/ReactServerTemplateProvider.spec.ts +210 -0
  162. package/src/react/router/providers/ReactServerTemplateProvider.ts +228 -665
  163. package/src/react/router/providers/SSRManifestProvider.ts +7 -0
  164. package/src/react/router/services/ReactRouter.ts +13 -13
  165. package/src/scheduler/index.workerd.ts +43 -0
  166. package/src/scheduler/providers/CronProvider.ts +53 -6
  167. package/src/scheduler/providers/WorkerdCronProvider.ts +102 -0
  168. package/src/security/__tests__/ServerSecurityProvider.spec.ts +77 -0
  169. package/src/security/providers/ServerSecurityProvider.ts +30 -22
  170. package/src/server/compress/providers/ServerCompressProvider.ts +6 -0
  171. package/src/server/core/providers/NodeHttpServerProvider.spec.ts +9 -3
  172. package/src/server/links/providers/ServerLinksProvider.spec.ts +332 -0
  173. package/src/server/links/providers/ServerLinksProvider.ts +1 -1
  174. package/src/system/index.browser.ts +25 -0
  175. package/src/system/index.workerd.ts +1 -0
  176. package/src/system/providers/FileSystemProvider.ts +8 -0
  177. package/src/system/providers/NodeFileSystemProvider.ts +11 -2
  178. package/src/vite/tasks/buildServer.ts +2 -12
  179. package/src/vite/tasks/generateCloudflare.ts +47 -8
  180. package/src/vite/tasks/generateDocker.ts +4 -0
@@ -0,0 +1,221 @@
1
+ import { $atom, $env, $use, type Static, t } from "alepha";
2
+ import { $logger } from "alepha/logger";
3
+ import { WorkerMailer } from "worker-mailer";
4
+ import { EmailError } from "../errors/EmailError.ts";
5
+ import type { EmailProvider, EmailSendOptions } from "./EmailProvider.ts";
6
+
7
+ // ---------------------------------------------------------------------------------------------------------------------
8
+
9
+ /**
10
+ * Environment variables for worker-mailer configuration
11
+ */
12
+ const envSchema = t.object({
13
+ EMAIL_HOST: t.optional(
14
+ t.text({
15
+ description: "SMTP server host",
16
+ }),
17
+ ),
18
+ EMAIL_PORT: t.number({
19
+ default: 587,
20
+ description: "SMTP server port (465 or 587, not 25)",
21
+ }),
22
+ EMAIL_USER: t.optional(
23
+ t.text({
24
+ description: "SMTP authentication username",
25
+ }),
26
+ ),
27
+ EMAIL_PASS: t.optional(
28
+ t.text({
29
+ description: "SMTP authentication password",
30
+ }),
31
+ ),
32
+ EMAIL_FROM: t.optional(
33
+ t.text({
34
+ description: "Default from email address",
35
+ }),
36
+ ),
37
+ EMAIL_FROM_NAME: t.optional(
38
+ t.text({
39
+ description: "Default from name",
40
+ }),
41
+ ),
42
+ EMAIL_SECURE: t.boolean({
43
+ default: true,
44
+ description: "Use secure connection (TLS/STARTTLS)",
45
+ }),
46
+ EMAIL_AUTH_TYPE: t.optional(
47
+ t.enum(["plain", "login", "cram-md5"], {
48
+ default: "plain",
49
+ description: "SMTP authentication type",
50
+ }),
51
+ ),
52
+ });
53
+
54
+ // ---------------------------------------------------------------------------------------------------------------------
55
+
56
+ /**
57
+ * Worker-mailer specific options
58
+ */
59
+ export const workermailerEmailOptions = $atom({
60
+ name: "alepha.email.workermailer.options",
61
+ schema: t.object({
62
+ authType: t.optional(
63
+ t.enum(["plain", "login", "cram-md5"], {
64
+ description: "SMTP authentication type (default: plain)",
65
+ }),
66
+ ),
67
+ }),
68
+ default: {},
69
+ });
70
+
71
+ export type WorkermailerEmailProviderOptions = Static<
72
+ typeof workermailerEmailOptions.schema
73
+ >;
74
+
75
+ declare module "alepha" {
76
+ interface State {
77
+ [workermailerEmailOptions.key]: WorkermailerEmailProviderOptions;
78
+ }
79
+ }
80
+
81
+ // ---------------------------------------------------------------------------------------------------------------------
82
+
83
+ /**
84
+ * Email provider using worker-mailer for Cloudflare Workers.
85
+ *
86
+ * This provider uses Cloudflare's TCP Sockets API to send emails via SMTP,
87
+ * making it suitable for edge runtime environments.
88
+ *
89
+ * Configuration is provided via environment variables:
90
+ * - EMAIL_HOST: SMTP server host
91
+ * - EMAIL_PORT: SMTP server port (default: 587, note: port 25 is blocked)
92
+ * - EMAIL_USER: SMTP authentication username
93
+ * - EMAIL_PASS: SMTP authentication password
94
+ * - EMAIL_FROM: Default from email address
95
+ * - EMAIL_FROM_NAME: Default from name (optional)
96
+ * - EMAIL_SECURE: Use secure connection (default: true)
97
+ * - EMAIL_AUTH_TYPE: Authentication type - plain, login, or cram-md5 (default: plain)
98
+ *
99
+ * @example
100
+ * ```typescript
101
+ * // Configure via environment variables
102
+ * // EMAIL_HOST=smtp.example.com
103
+ * // EMAIL_PORT=587
104
+ * // EMAIL_USER=user@example.com
105
+ * // EMAIL_PASS=secret
106
+ * // EMAIL_FROM=noreply@example.com
107
+ * // EMAIL_FROM_NAME=My App
108
+ * // EMAIL_SECURE=true
109
+ * // EMAIL_AUTH_TYPE=plain
110
+ * ```
111
+ *
112
+ * @see https://github.com/zou-yu/worker-mailer
113
+ */
114
+ export class WorkermailerEmailProvider implements EmailProvider {
115
+ protected readonly env = $env(envSchema);
116
+ protected readonly log = $logger();
117
+ protected readonly options = $use(workermailerEmailOptions);
118
+
119
+ protected get host(): string {
120
+ const host = this.env.EMAIL_HOST;
121
+ if (!host) {
122
+ throw new EmailError(
123
+ "Email host not configured. Set EMAIL_HOST env var.",
124
+ );
125
+ }
126
+ return host;
127
+ }
128
+
129
+ protected get port(): number {
130
+ return this.env.EMAIL_PORT;
131
+ }
132
+
133
+ protected get secure(): boolean {
134
+ return this.env.EMAIL_SECURE;
135
+ }
136
+
137
+ protected get user(): string | undefined {
138
+ return this.env.EMAIL_USER;
139
+ }
140
+
141
+ protected get pass(): string | undefined {
142
+ return this.env.EMAIL_PASS;
143
+ }
144
+
145
+ protected get fromAddress(): string {
146
+ const from = this.env.EMAIL_FROM;
147
+ if (!from) {
148
+ throw new EmailError(
149
+ "Email from address not configured. Set EMAIL_FROM env var.",
150
+ );
151
+ }
152
+ return from;
153
+ }
154
+
155
+ protected get fromName(): string | undefined {
156
+ return this.env.EMAIL_FROM_NAME;
157
+ }
158
+
159
+ protected get authType(): "plain" | "login" | "cram-md5" {
160
+ return this.options.authType ?? this.env.EMAIL_AUTH_TYPE ?? "plain";
161
+ }
162
+
163
+ public async send(options: EmailSendOptions): Promise<void> {
164
+ const { to, subject, body } = options;
165
+ this.log.debug("Sending email via worker-mailer", { to, subject });
166
+
167
+ const user = this.user;
168
+ const pass = this.pass;
169
+
170
+ if (!user || !pass) {
171
+ throw new EmailError(
172
+ "Email credentials not configured. Set EMAIL_USER and EMAIL_PASS env vars.",
173
+ );
174
+ }
175
+
176
+ let mailer: WorkerMailer | undefined;
177
+
178
+ try {
179
+ mailer = await WorkerMailer.connect({
180
+ credentials: {
181
+ username: user,
182
+ password: pass,
183
+ },
184
+ authType: this.authType,
185
+ host: this.host,
186
+ port: this.port,
187
+ secure: this.secure,
188
+ });
189
+
190
+ const recipients = Array.isArray(to) ? to : [to];
191
+
192
+ for (const recipient of recipients) {
193
+ await mailer.send({
194
+ from: this.fromName
195
+ ? { name: this.fromName, email: this.fromAddress }
196
+ : { email: this.fromAddress },
197
+ to: { email: recipient },
198
+ subject,
199
+ html: body,
200
+ });
201
+
202
+ this.log.info("Email sent successfully", {
203
+ to: recipient,
204
+ subject,
205
+ });
206
+ }
207
+ } catch (error) {
208
+ const message = `Failed to send email via worker-mailer: ${error instanceof Error ? error.message : String(error)}`;
209
+ this.log.error(message, { to, subject });
210
+ throw new EmailError(message, error instanceof Error ? error : undefined);
211
+ } finally {
212
+ if (mailer) {
213
+ try {
214
+ await mailer.close();
215
+ } catch {
216
+ // Ignore close errors
217
+ }
218
+ }
219
+ }
220
+ }
221
+ }
@@ -302,7 +302,19 @@ export class LockPrimitive<TFunc extends AsyncFn> extends Primitive<
302
302
  protected readonly provider = $inject(LockProvider);
303
303
  protected readonly env = $env(envSchema);
304
304
  protected readonly dateTimeProvider = $inject(DateTimeProvider);
305
- protected readonly id = crypto.randomUUID();
305
+
306
+ /**
307
+ * Lazy-initialized UUID to avoid calling crypto.randomUUID() in global scope.
308
+ * Cloudflare Workers doesn't allow random value generation during initialization.
309
+ */
310
+ protected _id?: string;
311
+ protected get id(): string {
312
+ if (!this._id) {
313
+ this._id = crypto.randomUUID();
314
+ }
315
+ return this._id;
316
+ }
317
+
306
318
  public readonly maxDuration = this.dateTimeProvider.duration(
307
319
  this.options.maxDuration ?? [5, "minutes"],
308
320
  );
@@ -52,7 +52,7 @@ export const AlephaOrm = $module({
52
52
  const url = env.DATABASE_URL;
53
53
  const isPostgres = url?.startsWith("postgres:");
54
54
 
55
- if (url?.startsWith("cloudflare-d1:")) {
55
+ if (url?.startsWith("d1:")) {
56
56
  alepha.with({
57
57
  optional: true,
58
58
  provide: DatabaseProvider,
package/src/orm/index.ts CHANGED
@@ -152,14 +152,10 @@ export const AlephaOrm = $module({
152
152
  alepha.with(RepositoryProvider);
153
153
 
154
154
  const url = env.DATABASE_URL;
155
- const hasPGlite = !!PglitePostgresProvider.importPglite();
156
155
  const isPostgres = url?.startsWith("postgres:");
157
- const isSqlite = url?.startsWith("sqlite:");
158
- const isMemory = url?.includes(":memory:");
159
- const isFile = !!url && !isPostgres && !isMemory;
160
156
  const isBun = alepha.isBun();
161
157
 
162
- if (url?.startsWith("cloudflare-d1:")) {
158
+ if (url?.startsWith("d1:")) {
163
159
  alepha.with({
164
160
  optional: true,
165
161
  provide: DatabaseProvider,
@@ -168,7 +164,7 @@ export const AlephaOrm = $module({
168
164
  return;
169
165
  }
170
166
 
171
- if (hasPGlite && (isMemory || isFile || !url) && !isSqlite) {
167
+ if (url?.startsWith("pglite:")) {
172
168
  alepha.with({
173
169
  optional: true,
174
170
  provide: DatabaseProvider,
@@ -154,7 +154,10 @@ export class BunSqliteProvider extends DatabaseProvider {
154
154
  },
155
155
  });
156
156
 
157
- await this.migrate();
157
+ // Never migrate in serverless mode - migrations should be applied during deployment
158
+ if (!this.alepha.isServerless()) {
159
+ await this.migrate();
160
+ }
158
161
 
159
162
  this.log.info(`Using Bun SQLite database at ${filepath}`);
160
163
  },
@@ -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 'cloudflare-d1://name:id'",
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
- const [bindingName] = this.env.DATABASE_URL.replace(
108
- "cloudflare-d1://",
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
- // Dynamic import for D1 migrator
140
- const { migrate } = await import("drizzle-orm/d1/migrator");
141
- await migrate(this.db as any, { migrationsFolder });
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
- await this.migrate();
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
  },
@@ -130,6 +130,8 @@ export class ReactAuth {
130
130
  }
131
131
 
132
132
  public logout() {
133
- window.location.href = `${alephaServerAuthRoutes.logout}?post_logout_redirect_uri=${encodeURIComponent(window.location.origin)}`;
133
+ // Add cache-busting parameter to prevent browser from using cached redirect
134
+ const cacheBuster = Date.now();
135
+ window.location.href = `${alephaServerAuthRoutes.logout}?post_logout_redirect_uri=${encodeURIComponent(window.location.origin)}&_=${cacheBuster}`;
134
136
  }
135
137
  }
@@ -32,6 +32,13 @@ export const ssrManifestAtomSchema = t.object({
32
32
  }),
33
33
  ),
34
34
  ),
35
+
36
+ /**
37
+ * Dev mode head content.
38
+ * Contains pre-transformed scripts injected by Vite and plugins (React, etc.).
39
+ * Only set in dev mode via ViteDevServerProvider.
40
+ */
41
+ devHead: t.optional(t.string()),
35
42
  });
36
43
 
37
44
  /**
@@ -36,7 +36,7 @@ export const useActive = (args: string | UseActiveOptions): UseActiveHook => {
36
36
 
37
37
  setPending(true);
38
38
  try {
39
- await router.go(href);
39
+ await router.push(href);
40
40
  } finally {
41
41
  setPending(false);
42
42
  }
@@ -13,7 +13,7 @@ import { ReactRouter } from "../services/ReactRouter.ts";
13
13
  * }
14
14
  *
15
15
  * const router = useRouter<App>();
16
- * router.go("home"); // typesafe
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),