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.
- package/README.md +1 -1
- package/dist/api/audits/index.d.ts +338 -417
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/files/index.d.ts +1 -80
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/jobs/index.d.ts +156 -235
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/notifications/index.d.ts +170 -249
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/parameters/index.d.ts +266 -345
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/users/index.d.ts +755 -834
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/verifications/index.d.ts +125 -125
- package/dist/api/verifications/index.d.ts.map +1 -1
- package/dist/cli/index.d.ts +116 -20
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +212 -124
- package/dist/cli/index.js.map +1 -1
- package/dist/command/index.d.ts +6 -11
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js +2 -2
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +26 -4
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +16 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +26 -4
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +26 -4
- package/dist/core/index.native.js.map +1 -1
- package/dist/logger/index.d.ts +1 -1
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/logger/index.js +12 -2
- package/dist/logger/index.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/index.d.ts +37 -173
- package/dist/orm/index.d.ts.map +1 -1
- package/dist/orm/index.js +193 -422
- package/dist/orm/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +167 -167
- package/dist/server/cache/index.d.ts +12 -0
- package/dist/server/cache/index.d.ts.map +1 -1
- package/dist/server/cache/index.js +55 -2
- package/dist/server/cache/index.js.map +1 -1
- package/dist/server/compress/index.d.ts +6 -0
- package/dist/server/compress/index.d.ts.map +1 -1
- package/dist/server/compress/index.js +36 -1
- package/dist/server/compress/index.js.map +1 -1
- package/dist/server/core/index.browser.js +2 -2
- package/dist/server/core/index.browser.js.map +1 -1
- package/dist/server/core/index.d.ts +10 -10
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/core/index.js +6 -3
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/links/index.d.ts +39 -39
- package/dist/server/links/index.d.ts.map +1 -1
- package/dist/server/security/index.d.ts +9 -9
- package/dist/server/static/index.d.ts.map +1 -1
- package/dist/server/static/index.js +4 -0
- package/dist/server/static/index.js.map +1 -1
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/server/swagger/index.js +2 -3
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/vite/index.d.ts +101 -106
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +571 -508
- package/dist/vite/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/apps/AlephaCli.ts +0 -2
- package/src/cli/atoms/buildOptions.ts +88 -0
- package/src/cli/commands/build.ts +32 -69
- package/src/cli/commands/db.ts +0 -4
- package/src/cli/commands/dev.ts +16 -4
- package/src/cli/commands/gen/env.ts +53 -0
- package/src/cli/commands/gen/openapi.ts +1 -1
- package/src/cli/commands/gen/resource.ts +15 -0
- package/src/cli/commands/gen.ts +7 -1
- package/src/cli/commands/init.ts +0 -1
- package/src/cli/commands/test.ts +0 -1
- package/src/cli/commands/verify.ts +1 -1
- package/src/cli/defineConfig.ts +49 -7
- package/src/cli/index.ts +0 -1
- package/src/cli/services/AlephaCliUtils.ts +36 -25
- package/src/command/helpers/Runner.spec.ts +2 -2
- package/src/command/helpers/Runner.ts +1 -1
- package/src/command/primitives/$command.ts +0 -6
- package/src/command/providers/CliProvider.ts +1 -3
- package/src/core/Alepha.ts +42 -0
- package/src/logger/index.ts +15 -3
- package/src/mcp/transports/StdioMcpTransport.ts +1 -1
- package/src/orm/index.ts +2 -8
- package/src/queue/core/providers/WorkerProvider.spec.ts +48 -32
- package/src/server/cache/providers/ServerCacheProvider.spec.ts +183 -0
- package/src/server/cache/providers/ServerCacheProvider.ts +94 -9
- package/src/server/compress/providers/ServerCompressProvider.ts +61 -2
- package/src/server/core/helpers/ServerReply.ts +2 -2
- package/src/server/core/providers/ServerProvider.ts +11 -1
- package/src/server/static/providers/ServerStaticProvider.ts +10 -0
- package/src/server/swagger/providers/ServerSwaggerProvider.ts +5 -8
- package/src/vite/helpers/importViteReact.ts +13 -0
- package/src/vite/index.ts +1 -21
- package/src/vite/plugins/viteAlephaDev.ts +16 -1
- package/src/vite/plugins/viteAlephaSsrPreload.ts +222 -0
- package/src/vite/tasks/buildClient.ts +11 -0
- package/src/vite/tasks/buildServer.ts +47 -3
- package/src/vite/tasks/devServer.ts +69 -0
- package/src/vite/tasks/index.ts +2 -1
- package/src/cli/assets/viteConfigTs.ts +0 -14
- package/src/cli/commands/run.ts +0 -24
- package/src/vite/plugins/viteAlepha.ts +0 -37
- 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
|
-
|
|
140
|
-
|
|
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
|
|
15
|
+
* Redirect to a given URL with optional status code (default 301).
|
|
16
16
|
*/
|
|
17
|
-
public redirect(url: string, status: number =
|
|
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
|
-
|
|
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,
|
|
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}\
|
|
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
|
+
}
|