alepha 0.14.3 → 0.14.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 (114) hide show
  1. package/README.md +1 -1
  2. package/dist/api/audits/index.d.ts +338 -417
  3. package/dist/api/audits/index.d.ts.map +1 -1
  4. package/dist/api/files/index.d.ts +1 -80
  5. package/dist/api/files/index.d.ts.map +1 -1
  6. package/dist/api/jobs/index.d.ts +156 -235
  7. package/dist/api/jobs/index.d.ts.map +1 -1
  8. package/dist/api/notifications/index.d.ts +170 -249
  9. package/dist/api/notifications/index.d.ts.map +1 -1
  10. package/dist/api/parameters/index.d.ts +266 -345
  11. package/dist/api/parameters/index.d.ts.map +1 -1
  12. package/dist/api/users/index.d.ts +755 -834
  13. package/dist/api/users/index.d.ts.map +1 -1
  14. package/dist/api/verifications/index.d.ts +125 -125
  15. package/dist/api/verifications/index.d.ts.map +1 -1
  16. package/dist/cli/index.d.ts +116 -20
  17. package/dist/cli/index.d.ts.map +1 -1
  18. package/dist/cli/index.js +212 -124
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/command/index.d.ts +6 -11
  21. package/dist/command/index.d.ts.map +1 -1
  22. package/dist/command/index.js +2 -2
  23. package/dist/command/index.js.map +1 -1
  24. package/dist/core/index.browser.js +26 -4
  25. package/dist/core/index.browser.js.map +1 -1
  26. package/dist/core/index.d.ts +16 -1
  27. package/dist/core/index.d.ts.map +1 -1
  28. package/dist/core/index.js +26 -4
  29. package/dist/core/index.js.map +1 -1
  30. package/dist/core/index.native.js +26 -4
  31. package/dist/core/index.native.js.map +1 -1
  32. package/dist/logger/index.d.ts +1 -1
  33. package/dist/logger/index.d.ts.map +1 -1
  34. package/dist/logger/index.js +12 -2
  35. package/dist/logger/index.js.map +1 -1
  36. package/dist/mcp/index.d.ts.map +1 -1
  37. package/dist/mcp/index.js +1 -1
  38. package/dist/mcp/index.js.map +1 -1
  39. package/dist/orm/index.d.ts +37 -173
  40. package/dist/orm/index.d.ts.map +1 -1
  41. package/dist/orm/index.js +193 -422
  42. package/dist/orm/index.js.map +1 -1
  43. package/dist/server/auth/index.d.ts +167 -167
  44. package/dist/server/cache/index.d.ts +12 -0
  45. package/dist/server/cache/index.d.ts.map +1 -1
  46. package/dist/server/cache/index.js +55 -2
  47. package/dist/server/cache/index.js.map +1 -1
  48. package/dist/server/compress/index.d.ts +6 -0
  49. package/dist/server/compress/index.d.ts.map +1 -1
  50. package/dist/server/compress/index.js +36 -1
  51. package/dist/server/compress/index.js.map +1 -1
  52. package/dist/server/core/index.browser.js +2 -2
  53. package/dist/server/core/index.browser.js.map +1 -1
  54. package/dist/server/core/index.d.ts +10 -10
  55. package/dist/server/core/index.d.ts.map +1 -1
  56. package/dist/server/core/index.js +6 -3
  57. package/dist/server/core/index.js.map +1 -1
  58. package/dist/server/links/index.d.ts +39 -39
  59. package/dist/server/links/index.d.ts.map +1 -1
  60. package/dist/server/security/index.d.ts +9 -9
  61. package/dist/server/static/index.d.ts.map +1 -1
  62. package/dist/server/static/index.js +4 -0
  63. package/dist/server/static/index.js.map +1 -1
  64. package/dist/server/swagger/index.d.ts.map +1 -1
  65. package/dist/server/swagger/index.js +2 -3
  66. package/dist/server/swagger/index.js.map +1 -1
  67. package/dist/vite/index.d.ts +101 -106
  68. package/dist/vite/index.d.ts.map +1 -1
  69. package/dist/vite/index.js +571 -508
  70. package/dist/vite/index.js.map +1 -1
  71. package/package.json +1 -1
  72. package/src/cli/apps/AlephaCli.ts +0 -2
  73. package/src/cli/atoms/buildOptions.ts +88 -0
  74. package/src/cli/commands/build.ts +32 -69
  75. package/src/cli/commands/db.ts +0 -4
  76. package/src/cli/commands/dev.ts +16 -4
  77. package/src/cli/commands/gen/env.ts +53 -0
  78. package/src/cli/commands/gen/openapi.ts +1 -1
  79. package/src/cli/commands/gen/resource.ts +15 -0
  80. package/src/cli/commands/gen.ts +7 -1
  81. package/src/cli/commands/init.ts +0 -1
  82. package/src/cli/commands/test.ts +0 -1
  83. package/src/cli/commands/verify.ts +1 -1
  84. package/src/cli/defineConfig.ts +49 -7
  85. package/src/cli/index.ts +0 -1
  86. package/src/cli/services/AlephaCliUtils.ts +36 -25
  87. package/src/command/helpers/Runner.spec.ts +2 -2
  88. package/src/command/helpers/Runner.ts +1 -1
  89. package/src/command/primitives/$command.ts +0 -6
  90. package/src/command/providers/CliProvider.ts +1 -3
  91. package/src/core/Alepha.ts +42 -0
  92. package/src/logger/index.ts +15 -3
  93. package/src/mcp/transports/StdioMcpTransport.ts +1 -1
  94. package/src/orm/index.ts +2 -8
  95. package/src/queue/core/providers/WorkerProvider.spec.ts +48 -32
  96. package/src/server/cache/providers/ServerCacheProvider.spec.ts +183 -0
  97. package/src/server/cache/providers/ServerCacheProvider.ts +94 -9
  98. package/src/server/compress/providers/ServerCompressProvider.ts +61 -2
  99. package/src/server/core/helpers/ServerReply.ts +2 -2
  100. package/src/server/core/providers/ServerProvider.ts +11 -1
  101. package/src/server/static/providers/ServerStaticProvider.ts +10 -0
  102. package/src/server/swagger/providers/ServerSwaggerProvider.ts +5 -8
  103. package/src/vite/helpers/importViteReact.ts +13 -0
  104. package/src/vite/index.ts +1 -21
  105. package/src/vite/plugins/viteAlephaDev.ts +16 -1
  106. package/src/vite/plugins/viteAlephaSsrPreload.ts +222 -0
  107. package/src/vite/tasks/buildClient.ts +11 -0
  108. package/src/vite/tasks/buildServer.ts +47 -3
  109. package/src/vite/tasks/devServer.ts +69 -0
  110. package/src/vite/tasks/index.ts +2 -1
  111. package/src/cli/assets/viteConfigTs.ts +0 -14
  112. package/src/cli/commands/run.ts +0 -24
  113. package/src/vite/plugins/viteAlepha.ts +0 -37
  114. package/src/vite/plugins/viteAlephaBuild.ts +0 -281
@@ -136,12 +136,71 @@ export class ServerCompressProvider {
136
136
 
137
137
  if (typeof body === "object" && body instanceof ReadableStream) {
138
138
  this.setHeaders(response, encoding);
139
- response.body = Readable.fromWeb(body).pipe(
140
- compressor.stream({ params }),
139
+ // For streaming responses, use flush mode to avoid buffering
140
+ response.body = this.createFlushingCompressStream(
141
+ body,
142
+ compressor.stream,
143
+ encoding,
144
+ params,
141
145
  );
142
146
  }
143
147
  }
144
148
 
149
+ /**
150
+ * Create a compressed stream that flushes after each chunk.
151
+ * This is essential for streaming SSR - ensures each chunk is sent immediately.
152
+ */
153
+ protected createFlushingCompressStream(
154
+ input: ReadableStream,
155
+ createCompressor: (options?: any) => Transform,
156
+ encoding: string,
157
+ params: Record<number, any>,
158
+ ): ReadableStream<Uint8Array> {
159
+ const compressor = createCompressor({
160
+ params,
161
+ flush: zlib.constants.Z_SYNC_FLUSH,
162
+ });
163
+ const reader = Readable.fromWeb(input);
164
+
165
+ return new ReadableStream<Uint8Array>({
166
+ start(controller) {
167
+ compressor.on("data", (chunk: Buffer) => {
168
+ controller.enqueue(new Uint8Array(chunk));
169
+ });
170
+
171
+ compressor.on("end", () => {
172
+ controller.close();
173
+ });
174
+
175
+ compressor.on("error", (err) => {
176
+ controller.error(err);
177
+ });
178
+
179
+ reader.on("data", (chunk: Buffer) => {
180
+ compressor.write(chunk);
181
+ // Force flush after each chunk for streaming
182
+ // Cast to any because flush() exists on zlib streams but not in Transform type
183
+ const zlibStream = compressor as any;
184
+ if (encoding === "gzip") {
185
+ zlibStream.flush(zlib.constants.Z_SYNC_FLUSH);
186
+ } else if (encoding === "br") {
187
+ zlibStream.flush(zlib.constants.BROTLI_OPERATION_FLUSH);
188
+ } else if (encoding === "zstd") {
189
+ zlibStream.flush();
190
+ }
191
+ });
192
+
193
+ reader.on("end", () => {
194
+ compressor.end();
195
+ });
196
+
197
+ reader.on("error", (err) => {
198
+ controller.error(err);
199
+ });
200
+ },
201
+ });
202
+ }
203
+
145
204
  protected getParams(
146
205
  encoding: keyof typeof ServerCompressProvider.compressors,
147
206
  ): Record<number, any> {
@@ -12,9 +12,9 @@ export class ServerReply {
12
12
  public body?: any;
13
13
 
14
14
  /**
15
- * Redirect to a given URL with optional status code (default 302).
15
+ * Redirect to a given URL with optional status code (default 301).
16
16
  */
17
- public redirect(url: string, status: number = 302): void {
17
+ public redirect(url: string, status: number = 301): void {
18
18
  this.status = status;
19
19
  this.headers.location = url;
20
20
  }
@@ -27,6 +27,9 @@ export class ServerProvider {
27
27
  protected readonly internalServerErrorMessage = "Internal Server Error";
28
28
 
29
29
  public get hostname(): string {
30
+ if (this.alepha.isViteDev()) {
31
+ return `http://localhost:${this.alepha.env.SERVER_PORT}`;
32
+ }
30
33
  return ""; // no hostname in serverless mode
31
34
  }
32
35
 
@@ -116,9 +119,16 @@ export class ServerProvider {
116
119
  // if response.body is web stream
117
120
  if (response.body instanceof ReadableStream) {
118
121
  res.writeHead(response.status, response.headers);
122
+ // Flush headers immediately and disable Nagle's algorithm for streaming
123
+ res.flushHeaders();
124
+ res.socket?.setNoDelay(true);
119
125
  try {
120
126
  for await (const chunk of response.body) {
121
- res.write(chunk);
127
+ const canContinue = res.write(chunk);
128
+ // If the internal buffer is full, wait for it to drain
129
+ if (!canContinue) {
130
+ await new Promise<void>((resolve) => res.once("drain", resolve));
131
+ }
122
132
  }
123
133
  } catch (error) {
124
134
  this.log.error("Error piping proxy response stream", error);
@@ -136,6 +136,16 @@ export class ServerStaticProvider {
136
136
  const { headers, reply } = request;
137
137
  let path = filepath;
138
138
 
139
+ // 01/26 - when calling "/directory", redirect to "/directory/"
140
+ if (
141
+ options.path &&
142
+ options.path === request.url.pathname &&
143
+ !options.path.endsWith("/")
144
+ ) {
145
+ reply.redirect(`${options.path}/`);
146
+ return;
147
+ }
148
+
139
149
  const encoding = headers["accept-encoding"];
140
150
  if (encoding) {
141
151
  if (hasBr && encoding.includes("br")) {
@@ -80,10 +80,6 @@ export class ServerSwaggerProvider {
80
80
  }
81
81
 
82
82
  this.json = await this.setupSwaggerPlugin(options);
83
-
84
- if (this.json) {
85
- this.log.info("Swagger documentation generated successfully.");
86
- }
87
83
  },
88
84
  });
89
85
 
@@ -115,6 +111,8 @@ export class ServerSwaggerProvider {
115
111
 
116
112
  if (options.ui !== false) {
117
113
  await this.configureSwaggerUi(prefix, options);
114
+ } else {
115
+ this.log.info(`Swagger API available at ${prefix}/json`);
118
116
  }
119
117
 
120
118
  return json;
@@ -362,7 +360,6 @@ export class ServerSwaggerProvider {
362
360
  },
363
361
  handler: () => json,
364
362
  });
365
- this.log.info(`Swagger API available at ${prefix}/json`);
366
363
  }
367
364
 
368
365
  protected async configureSwaggerUi(
@@ -428,9 +425,9 @@ window.onload = function() {
428
425
  },
429
426
  });
430
427
 
431
- this.log.info(
432
- `Swagger UI available at ${this.serverProvider.hostname}${prefix}/`,
433
- );
428
+ this.log.info("SwaggerUI OK", {
429
+ url: `${this.serverProvider.hostname}${prefix}`,
430
+ });
434
431
  }
435
432
 
436
433
  protected async getAssetPath(
@@ -0,0 +1,13 @@
1
+ import { createRequire } from "node:module";
2
+
3
+ export const importViteReact = async (): Promise<any> => {
4
+ // Add React plugin for JSX/TSX compilation
5
+ try {
6
+ const { default: viteReact } = createRequire(import.meta.url)(
7
+ "@vitejs/plugin-react",
8
+ );
9
+ return viteReact;
10
+ } catch {
11
+ // @vitejs/plugin-react not installed, skip
12
+ }
13
+ };
package/src/vite/index.ts CHANGED
@@ -4,9 +4,8 @@ import type { Alepha } from "alepha";
4
4
  export * from "./helpers/boot.ts";
5
5
  export * from "./helpers/createBufferedLogger.ts";
6
6
  // Plugins (public API)
7
- export * from "./plugins/viteAlepha.ts";
8
- export * from "./plugins/viteAlephaBuild.ts";
9
7
  export * from "./plugins/viteAlephaDev.ts";
8
+ export * from "./plugins/viteAlephaSsrPreload.ts";
10
9
  export * from "./plugins/viteCompress.ts";
11
10
  // Tasks (for CLI integration)
12
11
  export * from "./tasks/index.ts";
@@ -14,22 +13,3 @@ export * from "./tasks/index.ts";
14
13
  declare global {
15
14
  var __cli_alepha: Alepha;
16
15
  }
17
-
18
- /**
19
- * Plugin vite for Alepha framework.
20
- *
21
- * This module provides Vite plugins and configurations to integrate Alepha applications with Vite's build and development processes.
22
- *
23
- * @example
24
- * ```ts
25
- * import { defineConfig } from "vite";
26
- * import { viteAlepha } from "alepha/vite";
27
- *
28
- * export default defineConfig({
29
- * plugins: [viteAlepha()],
30
- * // other Vite configurations...
31
- * });
32
- * ```
33
- *
34
- * @module alepha.vite
35
- */
@@ -1,6 +1,7 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
- import type { Plugin, ResolvedConfig } from "vite";
2
+ import type { Plugin, ResolvedConfig, UserConfig } from "vite";
3
3
  import { boot } from "../helpers/boot.ts";
4
+ import { importVite } from "../helpers/importVite.ts";
4
5
  import { createAlephaRunner, isViteInternalPath } from "../tasks/runAlepha.ts";
5
6
 
6
7
  export interface ViteAlephaDevOptions {
@@ -44,9 +45,20 @@ export async function viteAlephaDev(
44
45
  debug: options.debug,
45
46
  });
46
47
 
48
+ const { loadEnv } = await importVite();
49
+ const env = loadEnv("development", process.cwd(), "SERVER");
50
+
51
+ const config: UserConfig = {};
52
+ if (env.SERVER_PORT) {
53
+ config.server = {
54
+ port: parseInt(env.SERVER_PORT, 10),
55
+ };
56
+ }
57
+
47
58
  return {
48
59
  name: "alepha-dev",
49
60
  apply: "serve",
61
+ config: () => config,
50
62
  configResolved(resolvedConfig: ResolvedConfig) {
51
63
  runner.setConfig(resolvedConfig);
52
64
  },
@@ -103,6 +115,9 @@ export async function viteAlephaDev(
103
115
  }
104
116
  },
105
117
  async configureServer(server) {
118
+ if (env.SERVER_PORT) {
119
+ server.config.server.port = parseInt(env.SERVER_PORT, 10);
120
+ }
106
121
  const middleware = (
107
122
  req: IncomingMessage,
108
123
  res: ServerResponse,
@@ -0,0 +1,222 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
+ import { dirname, join, relative, resolve } from "node:path";
4
+ import type { Plugin } from "vite";
5
+
6
+ /**
7
+ * Preload manifest mapping short keys to source paths.
8
+ * Generated at build time, consumed by SSRManifestProvider at runtime.
9
+ */
10
+ export interface PreloadManifest {
11
+ [key: string]: string;
12
+ }
13
+
14
+ /**
15
+ * Vite plugin that generates a preload manifest for SSR module preloading.
16
+ *
17
+ * Instead of injecting source paths directly into $page definitions (which would
18
+ * leak component paths in the browser bundle), this plugin:
19
+ *
20
+ * 1. Collects all lazy import paths from $page definitions during transform
21
+ * 2. Generates a manifest file mapping short keys to resolved source paths
22
+ * 3. Injects only the short key into $page definitions
23
+ *
24
+ * The manifest is written to `.vite/preload-manifest.json` alongside Vite's
25
+ * other manifests. The CLI build command moves all manifests to
26
+ * `dist/server/.ssr/` where SSRManifestProvider loads them at runtime.
27
+ *
28
+ * Before:
29
+ * ```typescript
30
+ * $page({
31
+ * path: '/users/:id',
32
+ * lazy: () => import('./UserDetail.tsx'),
33
+ * })
34
+ * ```
35
+ *
36
+ * After:
37
+ * ```typescript
38
+ * $page({
39
+ * path: '/users/:id',
40
+ * lazy: () => import('./UserDetail.tsx'),
41
+ * [Symbol.for("alepha.page.preload")]: "a1b2c3",
42
+ * })
43
+ * ```
44
+ *
45
+ * Manifest (.alepha/preload-manifest.json):
46
+ * ```json
47
+ * {
48
+ * "a1b2c3": "src/pages/UserDetail.tsx"
49
+ * }
50
+ * ```
51
+ */
52
+ export function viteAlephaSsrPreload(): Plugin {
53
+ let root = "";
54
+ const preloadMap = new Map<string, string>(); // key -> sourcePath
55
+
56
+ /**
57
+ * Generate a short hash key for a source path.
58
+ * Uses first 8 chars of MD5 hash for brevity while avoiding collisions.
59
+ */
60
+ function generateKey(sourcePath: string): string {
61
+ return createHash("md5").update(sourcePath).digest("hex").slice(0, 8);
62
+ }
63
+
64
+ return {
65
+ name: "alepha-preload",
66
+ configResolved(config) {
67
+ root = config.root;
68
+ },
69
+ transform(code, id) {
70
+ // Only process TypeScript/JavaScript files
71
+ if (!id.match(/\.[tj]sx?$/)) {
72
+ return null;
73
+ }
74
+
75
+ // Skip node_modules
76
+ if (id.includes("node_modules")) {
77
+ return null;
78
+ }
79
+
80
+ // Quick check if file contains $page with lazy
81
+ if (!code.includes("$page") || !code.includes("lazy")) {
82
+ return null;
83
+ }
84
+
85
+ // Collect all insertions first, then apply in reverse order
86
+ const insertions: Array<{ position: number; text: string }> = [];
87
+
88
+ // Find all $page({ occurrences
89
+ const pageStartRegex = /\$page\s*\(\s*\{/g;
90
+ let pageMatch: RegExpExecArray | null = pageStartRegex.exec(code);
91
+
92
+ while (pageMatch !== null) {
93
+ const startIndex = pageMatch.index;
94
+ const objectStartIndex = startIndex + pageMatch[0].length - 1; // Position of '{'
95
+
96
+ // Find the matching closing brace using brace counting
97
+ let braceCount = 1;
98
+ let i = objectStartIndex + 1;
99
+ while (i < code.length && braceCount > 0) {
100
+ if (code[i] === "{") braceCount++;
101
+ else if (code[i] === "}") braceCount--;
102
+ i++;
103
+ }
104
+
105
+ // Malformed, skip
106
+ if (braceCount !== 0) {
107
+ pageMatch = pageStartRegex.exec(code);
108
+ continue;
109
+ }
110
+
111
+ const objectEndIndex = i - 1; // Position of matching '}'
112
+ const pageContent = code.slice(objectStartIndex, objectEndIndex + 1);
113
+
114
+ // Skip if already has preload symbol
115
+ if (pageContent.includes("alepha.page.preload")) {
116
+ pageMatch = pageStartRegex.exec(code);
117
+ continue;
118
+ }
119
+
120
+ // Find lazy: () => import('...') within this $page block
121
+ const lazyRegex =
122
+ /lazy\s*:\s*\(\s*\)\s*=>\s*import\s*\(\s*['"]([^'"]+)['"]\s*\)/;
123
+ const lazyMatch = lazyRegex.exec(pageContent);
124
+
125
+ if (!lazyMatch) {
126
+ pageMatch = pageStartRegex.exec(code);
127
+ continue;
128
+ }
129
+
130
+ const importPath = lazyMatch[1];
131
+
132
+ // Resolve the import path relative to the current file
133
+ const currentDir = dirname(id);
134
+ let resolvedPath: string;
135
+
136
+ if (importPath.startsWith(".")) {
137
+ // Relative import
138
+ resolvedPath = resolve(currentDir, importPath);
139
+ } else if (importPath.startsWith("/")) {
140
+ // Absolute import from root
141
+ resolvedPath = resolve(root, importPath.slice(1));
142
+ } else {
143
+ // Package import - skip preloading for external packages
144
+ pageMatch = pageStartRegex.exec(code);
145
+ continue;
146
+ }
147
+
148
+ // Make path relative to root for SSR manifest lookup
149
+ let relativePath = relative(root, resolvedPath);
150
+
151
+ // Normalize path separators for cross-platform compatibility
152
+ relativePath = relativePath.replace(/\\/g, "/");
153
+
154
+ // Handle extension - Vite resolves .jsx imports to .tsx files
155
+ // Try to use .tsx when the source has .jsx since TypeScript is more common
156
+ if (!relativePath.match(/\.[tj]sx?$/)) {
157
+ relativePath = `${relativePath}.tsx`;
158
+ } else if (relativePath.endsWith(".jsx")) {
159
+ // Import said .jsx but actual file is likely .tsx
160
+ relativePath = relativePath.replace(/\.jsx$/, ".tsx");
161
+ } else if (relativePath.endsWith(".js")) {
162
+ // Import said .js but actual file is likely .ts
163
+ relativePath = relativePath.replace(/\.js$/, ".ts");
164
+ }
165
+
166
+ // Generate short key and store mapping
167
+ const key = generateKey(relativePath);
168
+ preloadMap.set(key, relativePath);
169
+
170
+ // Check if we need a comma (look at character before closing brace)
171
+ const beforeBrace = code.slice(0, objectEndIndex).trimEnd();
172
+ const needsComma = !beforeBrace.endsWith(",");
173
+ // Use Symbol.for() so it can be looked up at runtime without importing
174
+ // Only inject the short key, not the full path
175
+ const preloadProperty = `${needsComma ? "," : ""} [Symbol.for("alepha.page.preload")]: "${key}"`;
176
+
177
+ insertions.push({ position: objectEndIndex, text: preloadProperty });
178
+
179
+ pageMatch = pageStartRegex.exec(code);
180
+ }
181
+
182
+ if (insertions.length === 0) {
183
+ return null;
184
+ }
185
+
186
+ // Apply insertions in reverse order to preserve positions
187
+ let result = code;
188
+ for (let j = insertions.length - 1; j >= 0; j--) {
189
+ const { position, text } = insertions[j];
190
+ result = result.slice(0, position) + text + result.slice(position);
191
+ }
192
+
193
+ return {
194
+ code: result,
195
+ map: null,
196
+ };
197
+ },
198
+ writeBundle(options) {
199
+ // Only process client build (outputs to dist/public or similar)
200
+ // Skip server build which outputs to dist/server
201
+ const outDir = options.dir || "";
202
+ if (outDir.includes("server")) {
203
+ return;
204
+ }
205
+
206
+ // Write the preload manifest to the same .vite directory Vite uses
207
+ // The CLI build command will move all manifests to dist/server/.ssr/
208
+ if (preloadMap.size > 0) {
209
+ const viteDir = join(outDir, ".vite");
210
+
211
+ // Ensure .vite directory exists
212
+ if (!existsSync(viteDir)) {
213
+ mkdirSync(viteDir, { recursive: true });
214
+ }
215
+
216
+ const manifest: PreloadManifest = Object.fromEntries(preloadMap);
217
+ const manifestPath = join(viteDir, "preload-manifest.json");
218
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
219
+ }
220
+ },
221
+ };
222
+ }
@@ -2,6 +2,8 @@ import type { UserConfig } from "vite";
2
2
  import { analyzer as viteAnalyzer } from "vite-bundle-analyzer";
3
3
  import { createBufferedLogger } from "../helpers/createBufferedLogger.ts";
4
4
  import { importVite } from "../helpers/importVite.ts";
5
+ import { importViteReact } from "../helpers/importViteReact.ts";
6
+ import { viteAlephaSsrPreload } from "../plugins/viteAlephaSsrPreload.ts";
5
7
  import {
6
8
  type ViteCompressOptions,
7
9
  viteCompress,
@@ -62,6 +64,12 @@ export async function buildClient(opts: BuildClientOptions): Promise<void> {
62
64
  const { build: viteBuild, mergeConfig } = await importVite();
63
65
  const plugins: any[] = [];
64
66
 
67
+ const viteReact = await importViteReact();
68
+ if (viteReact) plugins.push(viteReact());
69
+
70
+ // Add preload plugin for SSR module preloading
71
+ plugins.push(viteAlephaSsrPreload());
72
+
65
73
  const compress: ViteCompressOptions | undefined = opts.precompress
66
74
  ? typeof opts.precompress === "object"
67
75
  ? opts.precompress
@@ -93,6 +101,9 @@ export async function buildClient(opts: BuildClientOptions): Promise<void> {
93
101
  build: {
94
102
  chunkSizeWarningLimit: 1000,
95
103
  outDir: opts.dist,
104
+ // Generate manifest for SSR module preloading
105
+ manifest: true,
106
+ ssrManifest: true,
96
107
  rollupOptions: {
97
108
  output: {
98
109
  entryFileNames: "entry.[hash].js",
@@ -1,4 +1,4 @@
1
- import { readFile, writeFile } from "node:fs/promises";
1
+ import { readFile, rm, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { AlephaError } from "alepha";
4
4
  import type * as vite from "vite";
@@ -6,6 +6,8 @@ import type { UserConfig } from "vite";
6
6
  import { analyzer as viteAnalyzer } from "vite-bundle-analyzer";
7
7
  import { createBufferedLogger } from "../helpers/createBufferedLogger.ts";
8
8
  import { importVite } from "../helpers/importVite.ts";
9
+ import { importViteReact } from "../helpers/importViteReact.ts";
10
+ import { viteAlephaSsrPreload } from "../plugins/viteAlephaSsrPreload.ts";
9
11
  import { generateExternals } from "./generateExternals.ts";
10
12
 
11
13
  export interface BuildServerOptions {
@@ -63,6 +65,13 @@ export async function buildServer(
63
65
  const { build: viteBuild, mergeConfig } = await importVite();
64
66
  const plugins: any[] = [];
65
67
 
68
+ const viteReact = await importViteReact();
69
+ if (viteReact && opts.clientDir) {
70
+ plugins.push(viteReact());
71
+ }
72
+
73
+ plugins.push(viteAlephaSsrPreload());
74
+
66
75
  if (opts.stats) {
67
76
  plugins.push(
68
77
  viteAnalyzer({
@@ -85,7 +94,7 @@ export async function buildServer(
85
94
  noExternal: true,
86
95
  },
87
96
  build: {
88
- sourcemap: true, // or "hidden" if you don't want to expose source maps
97
+ sourcemap: true,
89
98
  ssr: opts.entry,
90
99
  outDir: `${opts.distDir}/server`,
91
100
  minify: true,
@@ -138,6 +147,29 @@ export async function buildServer(
138
147
  template = `__alepha.set("alepha.react.server.template", \`${index.replace(/>\s*</g, "><").trim()}\`);\n`;
139
148
  }
140
149
 
150
+ // Embed SSR manifests if client was built
151
+ // This bundles all manifest data into index.js for serverless deployments
152
+ let manifest = "";
153
+ if (opts.clientDir) {
154
+ const viteDir = `${opts.distDir}/${opts.clientDir}/.vite`;
155
+ const ssrManifest = await loadJsonFile(`${viteDir}/ssr-manifest.json`);
156
+ const clientManifest = await loadJsonFile(`${viteDir}/manifest.json`);
157
+ const preloadManifest = await loadJsonFile(
158
+ `${viteDir}/preload-manifest.json`,
159
+ );
160
+
161
+ const combined = {
162
+ ssr: ssrManifest,
163
+ client: clientManifest,
164
+ preload: preloadManifest,
165
+ };
166
+
167
+ manifest = `__alepha.set("alepha.react.ssr.manifest", ${JSON.stringify(combined)});\n`;
168
+
169
+ // Remove .vite directory - no longer needed at runtime
170
+ await rm(viteDir, { recursive: true, force: true });
171
+ }
172
+
141
173
  const warning =
142
174
  "// This file was automatically generated. DO NOT MODIFY." +
143
175
  "\n" +
@@ -145,12 +177,24 @@ export async function buildServer(
145
177
 
146
178
  await writeFile(
147
179
  `${opts.distDir}/index.js`,
148
- `${warning}\nimport './server/${entryFile}';\n\n${template}`.trim(),
180
+ `${warning}\n${template}${manifest}import './server/${entryFile}';\n`.trim(),
149
181
  );
150
182
 
151
183
  return { entryFile };
152
184
  }
153
185
 
186
+ /**
187
+ * Load a JSON file, returning undefined if it doesn't exist.
188
+ */
189
+ async function loadJsonFile(path: string): Promise<any> {
190
+ try {
191
+ const content = await readFile(path, "utf-8");
192
+ return JSON.parse(content);
193
+ } catch {
194
+ return undefined;
195
+ }
196
+ }
197
+
154
198
  /**
155
199
  * Extract entry filename from Vite build result.
156
200
  */
@@ -0,0 +1,69 @@
1
+ import type { InlineConfig } from "vite";
2
+ import { importVite } from "../helpers/importVite.ts";
3
+ import { importViteReact } from "../helpers/importViteReact.ts";
4
+ import { viteAlephaDev } from "../plugins/viteAlephaDev.ts";
5
+ import { viteAlephaSsrPreload } from "../plugins/viteAlephaSsrPreload.ts";
6
+
7
+ export interface DevServerOptions {
8
+ /**
9
+ * Path to the server entry file.
10
+ * If not provided, will auto-detect.
11
+ */
12
+ entry?: string;
13
+
14
+ /**
15
+ * Port to run the dev server on.
16
+ */
17
+ port?: number;
18
+
19
+ /**
20
+ * Host to bind the dev server to.
21
+ */
22
+ host?: string | boolean;
23
+
24
+ /**
25
+ * Enable debug logging.
26
+ */
27
+ debug?: boolean;
28
+ }
29
+
30
+ /**
31
+ * Start Vite development server with Alepha plugins.
32
+ *
33
+ * This task starts the Vite dev server with all required plugins:
34
+ * - @vitejs/plugin-react (JSX/TSX compilation)
35
+ * - viteAlephaDev (Alepha server integration)
36
+ * - viteAlephaSsrPreload (SSR module preloading)
37
+ */
38
+ export async function devServer(opts: DevServerOptions = {}): Promise<void> {
39
+ const { createServer, mergeConfig } = await importVite();
40
+ const plugins: any[] = [];
41
+
42
+ // Add React plugin for JSX/TSX compilation
43
+ const viteReact = await importViteReact();
44
+ if (viteReact) plugins.push(viteReact());
45
+
46
+ // Add SSR preload plugin
47
+ plugins.push(viteAlephaSsrPreload());
48
+
49
+ // Add Alepha dev plugin
50
+ plugins.push(
51
+ await viteAlephaDev({
52
+ serverEntry: opts.entry,
53
+ debug: opts.debug,
54
+ }),
55
+ );
56
+
57
+ const config: InlineConfig = {
58
+ plugins,
59
+ server: {
60
+ port: opts.port,
61
+ host: opts.host,
62
+ },
63
+ };
64
+
65
+ const server = await createServer(mergeConfig(config, {}));
66
+ await server.listen();
67
+
68
+ // server.printUrls();
69
+ }