alepha 0.15.0 → 0.15.1
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 +43 -98
- package/dist/api/audits/index.d.ts +240 -240
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/audits/index.js +2 -2
- package/dist/api/audits/index.js.map +1 -1
- package/dist/api/files/index.d.ts +185 -185
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/files/index.js +2 -2
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.d.ts +245 -245
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/notifications/index.browser.js +4 -4
- package/dist/api/notifications/index.browser.js.map +1 -1
- package/dist/api/notifications/index.d.ts +74 -74
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/notifications/index.js +4 -4
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/parameters/index.d.ts +221 -221
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/users/index.d.ts +1632 -1631
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +26 -34
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts +132 -132
- package/dist/api/verifications/index.d.ts.map +1 -1
- package/dist/batch/index.d.ts +122 -122
- package/dist/batch/index.d.ts.map +1 -1
- package/dist/bucket/index.d.ts +163 -163
- package/dist/bucket/index.d.ts.map +1 -1
- package/dist/cache/core/index.d.ts +46 -46
- package/dist/cache/core/index.d.ts.map +1 -1
- package/dist/cache/redis/index.d.ts.map +1 -1
- package/dist/cache/redis/index.js +2 -2
- package/dist/cache/redis/index.js.map +1 -1
- package/dist/cli/index.d.ts +5933 -201
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +609 -169
- package/dist/cli/index.js.map +1 -1
- package/dist/command/index.d.ts +296 -296
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js +19 -19
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +268 -79
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +768 -694
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +268 -79
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +268 -79
- package/dist/core/index.native.js.map +1 -1
- package/dist/datetime/index.d.ts +44 -44
- package/dist/datetime/index.d.ts.map +1 -1
- package/dist/email/index.d.ts +25 -25
- package/dist/email/index.d.ts.map +1 -1
- package/dist/fake/index.d.ts +5409 -5409
- package/dist/fake/index.d.ts.map +1 -1
- package/dist/fake/index.js +22 -22
- package/dist/fake/index.js.map +1 -1
- package/dist/file/index.d.ts +435 -435
- package/dist/file/index.d.ts.map +1 -1
- package/dist/lock/core/index.d.ts +208 -208
- package/dist/lock/core/index.d.ts.map +1 -1
- package/dist/lock/redis/index.d.ts.map +1 -1
- package/dist/logger/index.d.ts +24 -24
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/logger/index.js +1 -5
- package/dist/logger/index.js.map +1 -1
- package/dist/mcp/index.d.ts +216 -198
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +28 -4
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/index.browser.js +9 -9
- package/dist/orm/index.browser.js.map +1 -1
- package/dist/orm/index.bun.js +83 -76
- package/dist/orm/index.bun.js.map +1 -1
- package/dist/orm/index.d.ts +961 -960
- package/dist/orm/index.d.ts.map +1 -1
- package/dist/orm/index.js +88 -81
- package/dist/orm/index.js.map +1 -1
- package/dist/queue/core/index.d.ts +244 -244
- package/dist/queue/core/index.d.ts.map +1 -1
- package/dist/queue/redis/index.d.ts.map +1 -1
- package/dist/redis/index.d.ts +105 -105
- package/dist/redis/index.d.ts.map +1 -1
- package/dist/retry/index.d.ts +69 -69
- package/dist/retry/index.d.ts.map +1 -1
- package/dist/router/index.d.ts +6 -6
- package/dist/router/index.d.ts.map +1 -1
- package/dist/scheduler/index.d.ts +108 -26
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +393 -1
- package/dist/scheduler/index.js.map +1 -1
- package/dist/security/index.d.ts +532 -209
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +1422 -11
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +1296 -271
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +1249 -18
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/cache/index.d.ts +56 -56
- package/dist/server/cache/index.d.ts.map +1 -1
- package/dist/server/compress/index.d.ts +3 -3
- package/dist/server/compress/index.d.ts.map +1 -1
- package/dist/server/cookies/index.d.ts +6 -6
- package/dist/server/cookies/index.d.ts.map +1 -1
- package/dist/server/core/index.d.ts +196 -186
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/core/index.js +43 -27
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/cors/index.d.ts +11 -11
- package/dist/server/cors/index.d.ts.map +1 -1
- package/dist/server/health/index.d.ts.map +1 -1
- package/dist/server/helmet/index.d.ts +2 -2
- package/dist/server/helmet/index.d.ts.map +1 -1
- package/dist/server/links/index.browser.js +9 -1
- package/dist/server/links/index.browser.js.map +1 -1
- package/dist/server/links/index.d.ts +83 -83
- package/dist/server/links/index.d.ts.map +1 -1
- package/dist/server/links/index.js +13 -5
- package/dist/server/links/index.js.map +1 -1
- package/dist/server/metrics/index.d.ts +514 -1
- package/dist/server/metrics/index.d.ts.map +1 -1
- package/dist/server/metrics/index.js +4462 -4
- package/dist/server/metrics/index.js.map +1 -1
- package/dist/server/multipart/index.d.ts +6 -6
- package/dist/server/multipart/index.d.ts.map +1 -1
- package/dist/server/proxy/index.d.ts +102 -102
- package/dist/server/proxy/index.d.ts.map +1 -1
- package/dist/server/rate-limit/index.d.ts +16 -16
- package/dist/server/rate-limit/index.d.ts.map +1 -1
- package/dist/server/static/index.d.ts +44 -44
- package/dist/server/static/index.d.ts.map +1 -1
- package/dist/server/swagger/index.d.ts +47 -47
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/sms/index.d.ts +11 -11
- package/dist/sms/index.d.ts.map +1 -1
- package/dist/sms/index.js +3 -3
- package/dist/sms/index.js.map +1 -1
- package/dist/thread/index.d.ts +71 -71
- package/dist/thread/index.d.ts.map +1 -1
- package/dist/thread/index.js +2 -2
- package/dist/thread/index.js.map +1 -1
- package/dist/topic/core/index.d.ts +318 -318
- package/dist/topic/core/index.d.ts.map +1 -1
- package/dist/topic/redis/index.d.ts +6 -6
- package/dist/topic/redis/index.d.ts.map +1 -1
- package/dist/vite/index.d.ts +2324 -1719
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +123 -475
- package/dist/vite/index.js.map +1 -1
- package/dist/websocket/index.browser.js +3 -3
- package/dist/websocket/index.browser.js.map +1 -1
- package/dist/websocket/index.d.ts +275 -275
- package/dist/websocket/index.d.ts.map +1 -1
- package/dist/websocket/index.js +3 -3
- package/dist/websocket/index.js.map +1 -1
- package/package.json +9 -9
- package/src/api/users/services/SessionService.ts +0 -10
- package/src/cli/apps/AlephaCli.ts +2 -2
- package/src/cli/apps/AlephaPackageBuilderCli.ts +9 -1
- package/src/cli/assets/apiHelloControllerTs.ts +2 -1
- package/src/cli/assets/biomeJson.ts +2 -1
- package/src/cli/assets/claudeMd.ts +9 -4
- package/src/cli/assets/dummySpecTs.ts +2 -1
- package/src/cli/assets/editorconfig.ts +2 -1
- package/src/cli/assets/mainBrowserTs.ts +2 -1
- package/src/cli/assets/mainCss.ts +24 -0
- package/src/cli/assets/tsconfigJson.ts +2 -1
- package/src/cli/assets/webAppRouterTs.ts +2 -1
- package/src/cli/assets/webHelloComponentTsx.ts +6 -2
- package/src/cli/atoms/appEntryOptions.ts +13 -0
- package/src/cli/atoms/buildOptions.ts +1 -1
- package/src/cli/atoms/changelogOptions.ts +1 -1
- package/src/cli/commands/build.ts +63 -47
- package/src/cli/commands/dev.ts +16 -33
- package/src/cli/commands/gen/env.ts +1 -1
- package/src/cli/commands/init.ts +17 -8
- package/src/cli/commands/lint.ts +1 -1
- package/src/cli/defineConfig.ts +9 -0
- package/src/cli/index.ts +2 -1
- package/src/cli/providers/AppEntryProvider.ts +131 -0
- package/src/cli/providers/ViteBuildProvider.ts +82 -0
- package/src/cli/providers/ViteDevServerProvider.ts +350 -0
- package/src/cli/providers/ViteTemplateProvider.ts +27 -0
- package/src/cli/services/AlephaCliUtils.ts +33 -2
- package/src/cli/services/PackageManagerUtils.ts +13 -6
- package/src/cli/services/ProjectScaffolder.ts +72 -49
- package/src/core/Alepha.ts +2 -8
- package/src/core/primitives/$module.ts +12 -0
- package/src/core/providers/KeylessJsonSchemaCodec.spec.ts +257 -0
- package/src/core/providers/KeylessJsonSchemaCodec.ts +396 -14
- package/src/core/providers/SchemaValidator.spec.ts +236 -0
- package/src/logger/providers/PrettyFormatterProvider.ts +0 -9
- package/src/mcp/errors/McpError.ts +30 -0
- package/src/mcp/index.ts +3 -0
- package/src/mcp/transports/SseMcpTransport.ts +16 -6
- package/src/orm/providers/DrizzleKitProvider.ts +3 -5
- package/src/orm/services/Repository.ts +11 -0
- package/src/server/core/index.ts +1 -1
- package/src/server/core/providers/BunHttpServerProvider.ts +1 -1
- package/src/server/core/providers/NodeHttpServerProvider.spec.ts +125 -0
- package/src/server/core/providers/NodeHttpServerProvider.ts +71 -22
- package/src/server/core/providers/ServerLoggerProvider.ts +2 -2
- package/src/server/core/providers/ServerProvider.ts +9 -12
- package/src/server/links/atoms/apiLinksAtom.ts +7 -0
- package/src/server/links/index.browser.ts +2 -0
- package/src/server/links/index.ts +2 -0
- package/src/vite/index.ts +3 -2
- package/src/vite/tasks/buildClient.ts +0 -1
- package/src/vite/tasks/buildServer.ts +68 -21
- package/src/vite/tasks/copyAssets.ts +5 -4
- package/src/vite/tasks/generateSitemap.ts +64 -23
- package/src/vite/tasks/index.ts +0 -2
- package/src/vite/tasks/prerenderPages.ts +49 -24
- package/src/cli/assets/indexHtml.ts +0 -15
- package/src/cli/commands/format.ts +0 -23
- package/src/vite/helpers/boot.ts +0 -117
- package/src/vite/plugins/viteAlephaDev.ts +0 -177
- package/src/vite/tasks/devServer.ts +0 -71
- package/src/vite/tasks/runAlepha.ts +0 -270
- /package/dist/orm/{chunk-DtkW-qnP.js → chunk-DH6iiROE.js} +0 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { $inject, $use, AlephaError } from "alepha";
|
|
2
|
+
import { FileSystemProvider } from "alepha/file";
|
|
3
|
+
import { appEntryOptions } from "../atoms/appEntryOptions.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Service for locating entry files in Alepha projects.
|
|
7
|
+
*
|
|
8
|
+
* Originally in alepha/vite, moved to CLI to avoid cli -> vite dependency.
|
|
9
|
+
*/
|
|
10
|
+
export class AppEntryProvider {
|
|
11
|
+
protected readonly fs = $inject(FileSystemProvider);
|
|
12
|
+
protected readonly options = $use(appEntryOptions);
|
|
13
|
+
|
|
14
|
+
protected readonly serverEntries = [
|
|
15
|
+
"main.server.ts",
|
|
16
|
+
"main.server.tsx",
|
|
17
|
+
"main.ts",
|
|
18
|
+
"main.tsx",
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
protected readonly browserEntries = [
|
|
22
|
+
"main.browser.ts",
|
|
23
|
+
"main.browser.tsx",
|
|
24
|
+
"main.ts",
|
|
25
|
+
"main.tsx",
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
protected readonly styleEntries = [
|
|
29
|
+
"main.css",
|
|
30
|
+
"styles.css",
|
|
31
|
+
"style.css",
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get application entry points.
|
|
36
|
+
*
|
|
37
|
+
* Server entry is required, an error is thrown if not found.
|
|
38
|
+
* Browser entry is optional.
|
|
39
|
+
*
|
|
40
|
+
* It will first check for custom entries in options, see appEntryOptions.
|
|
41
|
+
*/
|
|
42
|
+
public async getAppEntry(root: string): Promise<AppEntry> {
|
|
43
|
+
const appEntry: AppEntry = {
|
|
44
|
+
root,
|
|
45
|
+
server: "",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
if (this.options.server) {
|
|
49
|
+
const serverExists = await this.fs.exists(
|
|
50
|
+
this.fs.join(root, this.options.server),
|
|
51
|
+
);
|
|
52
|
+
if (!serverExists) {
|
|
53
|
+
throw new AlephaError(
|
|
54
|
+
`Custom server entry "${this.options.server}" not found.`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
appEntry.server = this.options.server;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (this.options.browser) {
|
|
61
|
+
const browserExists = await this.fs.exists(
|
|
62
|
+
this.fs.join(root, this.options.browser),
|
|
63
|
+
);
|
|
64
|
+
if (!browserExists) {
|
|
65
|
+
throw new AlephaError(
|
|
66
|
+
`Custom browser entry "${this.options.browser}" not found.`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
appEntry.browser = this.options.browser;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (this.options.style) {
|
|
73
|
+
const styleExists = await this.fs.exists(
|
|
74
|
+
this.fs.join(root, this.options.style),
|
|
75
|
+
);
|
|
76
|
+
if (!styleExists) {
|
|
77
|
+
throw new AlephaError(
|
|
78
|
+
`Custom style entry "${this.options.style}" not found.`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
appEntry.style = this.options.style;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const srcFiles = await this.fs.ls(this.fs.join(root, "src"));
|
|
85
|
+
|
|
86
|
+
if (!appEntry.server) {
|
|
87
|
+
// find in conventional locations
|
|
88
|
+
for (const entry of this.serverEntries) {
|
|
89
|
+
if (srcFiles.includes(entry)) {
|
|
90
|
+
appEntry.server = this.fs.join("src", entry);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!appEntry.server) {
|
|
97
|
+
throw new AlephaError(
|
|
98
|
+
"No server entry found. Please, add a main.server.ts file in the src/ directory or configure a custom entry in alepha.config.ts.",
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!appEntry.browser) {
|
|
103
|
+
// find in conventional locations
|
|
104
|
+
for (const entry of this.browserEntries) {
|
|
105
|
+
if (srcFiles.includes(entry)) {
|
|
106
|
+
appEntry.browser = this.fs.join("src", entry);
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!appEntry.style) {
|
|
113
|
+
// find in conventional locations
|
|
114
|
+
for (const entry of this.styleEntries) {
|
|
115
|
+
if (srcFiles.includes(entry)) {
|
|
116
|
+
appEntry.style = this.fs.join("src", entry);
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return appEntry;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface AppEntry {
|
|
127
|
+
root: string;
|
|
128
|
+
server: string;
|
|
129
|
+
browser?: string;
|
|
130
|
+
style?: string;
|
|
131
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { $hook, $inject, type Alepha, AlephaError } from "alepha";
|
|
2
|
+
import { importVite } from "alepha/vite";
|
|
3
|
+
import type { InlineConfig, ViteDevServer } from "vite";
|
|
4
|
+
import type { AppEntry } from "./AppEntryProvider.ts";
|
|
5
|
+
import { ViteTemplateProvider } from "./ViteTemplateProvider.ts";
|
|
6
|
+
|
|
7
|
+
export class ViteBuildProvider {
|
|
8
|
+
protected alepha?: Alepha;
|
|
9
|
+
protected appEntry?: AppEntry;
|
|
10
|
+
protected viteDevServer?: ViteDevServer;
|
|
11
|
+
protected readonly templateProvider = $inject(ViteTemplateProvider);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* We need to close the Vite dev server after build is done.
|
|
15
|
+
*/
|
|
16
|
+
protected onReady = $hook({
|
|
17
|
+
on: "ready",
|
|
18
|
+
priority: "last",
|
|
19
|
+
handler: async () => {
|
|
20
|
+
await this.viteDevServer?.close();
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
protected onStop = $hook({
|
|
24
|
+
on: "stop",
|
|
25
|
+
handler: async () => {
|
|
26
|
+
await this.viteDevServer?.close();
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
public async init(opts: { entry: AppEntry }) {
|
|
31
|
+
const { createServer } = await importVite();
|
|
32
|
+
|
|
33
|
+
process.env.ALEPHA_CLI_IMPORT = "true"; // signal Alepha App about CLI import, run(alepha) won't start server
|
|
34
|
+
process.env.NODE_ENV = "production"; // force Alepha App in production mode for getting "production" metadata
|
|
35
|
+
process.env.LOG_LEVEL ??= "warn"; // reduce log noise
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 01/26 Vite 7
|
|
39
|
+
* "runnerImport" doesn't work as expected here. (e.g. build docs fail)
|
|
40
|
+
* -> We still use devServer and ssrLoadModule for now.
|
|
41
|
+
* -> This is clearly a bad stuff, we need to find better way.
|
|
42
|
+
*/
|
|
43
|
+
this.viteDevServer = await createServer({
|
|
44
|
+
server: { middlewareMode: true },
|
|
45
|
+
appType: "custom",
|
|
46
|
+
logLevel: "silent",
|
|
47
|
+
} satisfies InlineConfig);
|
|
48
|
+
|
|
49
|
+
await this.viteDevServer.ssrLoadModule(opts.entry.server);
|
|
50
|
+
|
|
51
|
+
const alepha: Alepha = (globalThis as any).__alepha;
|
|
52
|
+
if (!alepha) {
|
|
53
|
+
throw new AlephaError(
|
|
54
|
+
"Alepha instance not found after loading entry module",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.alepha = alepha;
|
|
59
|
+
this.appEntry = opts.entry;
|
|
60
|
+
|
|
61
|
+
return alepha;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public hasClient(): boolean {
|
|
65
|
+
if (!this.alepha) {
|
|
66
|
+
throw new AlephaError("ViteBuildProvider not initialized");
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
this.alepha.inject("ReactServerProvider");
|
|
70
|
+
return true;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public generateIndexHtml(): string {
|
|
77
|
+
if (!this.appEntry) {
|
|
78
|
+
throw new AlephaError("ViteBuildProvider not initialized");
|
|
79
|
+
}
|
|
80
|
+
return this.templateProvider.generateIndexHtml(this.appEntry);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { $inject, type Alepha, AlephaError } from "alepha";
|
|
2
|
+
import { FileSystemProvider } from "alepha/file";
|
|
3
|
+
import { $logger } from "alepha/logger";
|
|
4
|
+
import { importVite, importViteReact, viteAlephaSsrPreload } from "alepha/vite";
|
|
5
|
+
import type { InlineConfig, Plugin, ViteDevServer } from "vite";
|
|
6
|
+
import type { AppEntry } from "./AppEntryProvider.ts";
|
|
7
|
+
import { ViteTemplateProvider } from "./ViteTemplateProvider.ts";
|
|
8
|
+
|
|
9
|
+
export interface ViteDevServerOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Root directory of the project.
|
|
12
|
+
*/
|
|
13
|
+
root: string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Path to the server entry file.
|
|
17
|
+
*/
|
|
18
|
+
entry: AppEntry;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Port to run the dev server on.
|
|
22
|
+
*/
|
|
23
|
+
port?: number;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Host to bind the dev server to.
|
|
27
|
+
*/
|
|
28
|
+
host?: string | boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Vite development server with Alepha integration.
|
|
33
|
+
*
|
|
34
|
+
* Architecture:
|
|
35
|
+
* - Vite runs in middleware mode (no HTTP server)
|
|
36
|
+
* - Alepha is the HTTP server via server:onRequest event
|
|
37
|
+
* - Request flow: Page requests → Alepha SSR, Assets → Vite middleware
|
|
38
|
+
*
|
|
39
|
+
* HMR Strategy:
|
|
40
|
+
* - Browser-only changes (CSS, client components) → Vite HMR (React Fast Refresh)
|
|
41
|
+
* - Server-only changes → Restart Alepha → Full browser reload
|
|
42
|
+
* - Shared changes → Restart Alepha → Let Vite HMR propagate
|
|
43
|
+
*
|
|
44
|
+
* Features:
|
|
45
|
+
* - Automatic .env reload detection
|
|
46
|
+
* - Error recovery on next file change
|
|
47
|
+
* - Optimized module invalidation (only changed files + importers)
|
|
48
|
+
*/
|
|
49
|
+
export class ViteDevServerProvider {
|
|
50
|
+
protected readonly log = $logger();
|
|
51
|
+
protected readonly fs = $inject(FileSystemProvider);
|
|
52
|
+
protected readonly templateProvider = $inject(ViteTemplateProvider);
|
|
53
|
+
protected server!: ViteDevServer;
|
|
54
|
+
protected options!: ViteDevServerOptions;
|
|
55
|
+
protected alepha: Alepha | null = null;
|
|
56
|
+
protected hasError = false;
|
|
57
|
+
protected changedFiles = new Set<string>();
|
|
58
|
+
|
|
59
|
+
public async init(options: ViteDevServerOptions): Promise<Alepha> {
|
|
60
|
+
this.options = options;
|
|
61
|
+
await this.createViteServer();
|
|
62
|
+
return await this.loadAlepha(true);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public async start(): Promise<void> {
|
|
66
|
+
await this.alepha?.start();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create the Vite server in middleware mode.
|
|
71
|
+
*/
|
|
72
|
+
protected async createViteServer(): Promise<void> {
|
|
73
|
+
const { createServer } = await importVite();
|
|
74
|
+
const viteReact = await importViteReact();
|
|
75
|
+
|
|
76
|
+
const plugins: Plugin[] = [];
|
|
77
|
+
if (viteReact) plugins.push(viteReact());
|
|
78
|
+
plugins.push(viteAlephaSsrPreload());
|
|
79
|
+
plugins.push(this.createHmrPlugin());
|
|
80
|
+
|
|
81
|
+
this.server = await createServer({
|
|
82
|
+
root: this.options.root,
|
|
83
|
+
plugins,
|
|
84
|
+
server: { middlewareMode: true },
|
|
85
|
+
appType: "custom",
|
|
86
|
+
customLogger: {
|
|
87
|
+
info: () => {},
|
|
88
|
+
warn: this.log.warn.bind(this.log),
|
|
89
|
+
error: this.log.error.bind(this.log),
|
|
90
|
+
warnOnce: this.log.warn.bind(this.log),
|
|
91
|
+
clearScreen: () => {},
|
|
92
|
+
hasWarned: false,
|
|
93
|
+
hasErrorLogged: () => false,
|
|
94
|
+
},
|
|
95
|
+
} satisfies InlineConfig);
|
|
96
|
+
|
|
97
|
+
// Intercept .env changes (Vite calls restart() for .env files)
|
|
98
|
+
this.server.restart = async () => {
|
|
99
|
+
const startTime = Date.now();
|
|
100
|
+
try {
|
|
101
|
+
this.hasError = true; // Force full invalidation for env changes
|
|
102
|
+
await this.loadAlepha(false);
|
|
103
|
+
await this.alepha?.start();
|
|
104
|
+
this.log.debug(`Env reloaded in ${Date.now() - startTime}ms`);
|
|
105
|
+
this.sendBrowserReload();
|
|
106
|
+
} catch (err) {
|
|
107
|
+
this.hasError = true;
|
|
108
|
+
this.log.error("Reload failed", err);
|
|
109
|
+
this.log.warn("Waiting for file changes to retry...");
|
|
110
|
+
this.alepha = null;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Vite plugin to handle HMR for Alepha.
|
|
117
|
+
*/
|
|
118
|
+
protected createHmrPlugin(): Plugin {
|
|
119
|
+
return {
|
|
120
|
+
name: "alepha-hmr",
|
|
121
|
+
handleHotUpdate: async (ctx) => {
|
|
122
|
+
if (ctx.file.includes("/.idea/")) return [];
|
|
123
|
+
|
|
124
|
+
const firstModule = ctx.modules[0] as any;
|
|
125
|
+
const isBrowserOnly = firstModule && !firstModule._ssrModule;
|
|
126
|
+
const isServerOnly = firstModule && !firstModule._clientModule;
|
|
127
|
+
|
|
128
|
+
// Browser-only: let Vite HMR handle it (React Fast Refresh)
|
|
129
|
+
if (isBrowserOnly) return;
|
|
130
|
+
|
|
131
|
+
// Server or shared change: restart Alepha
|
|
132
|
+
const startTime = Date.now();
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
this.changedFiles.add(ctx.file);
|
|
136
|
+
await this.loadAlepha(false);
|
|
137
|
+
await this.alepha?.start();
|
|
138
|
+
this.log.debug(`Reloaded in ${Date.now() - startTime}ms`);
|
|
139
|
+
|
|
140
|
+
// Server-only: full browser reload
|
|
141
|
+
if (isServerOnly) {
|
|
142
|
+
this.sendBrowserReload();
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Shared: let HMR propagate to browser
|
|
147
|
+
return;
|
|
148
|
+
} catch (err) {
|
|
149
|
+
this.hasError = true;
|
|
150
|
+
this.log.error("Reload failed", err);
|
|
151
|
+
this.log.warn("Waiting for file changes to retry...");
|
|
152
|
+
this.alepha = null;
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Send browser reload signal via custom event.
|
|
161
|
+
* Browser listens for 'alepha:reload' and does window.location.reload()
|
|
162
|
+
*/
|
|
163
|
+
protected sendBrowserReload(): void {
|
|
164
|
+
this.server.ws.send({
|
|
165
|
+
type: "custom",
|
|
166
|
+
event: "alepha:reload",
|
|
167
|
+
data: {},
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Setup environment variables for dev mode.
|
|
173
|
+
*/
|
|
174
|
+
protected async setupEnvironment(): Promise<void> {
|
|
175
|
+
const { loadEnv } = await importVite();
|
|
176
|
+
const mode = process.env.NODE_ENV || "development";
|
|
177
|
+
const env = loadEnv(mode, this.options.root, "");
|
|
178
|
+
|
|
179
|
+
process.env.NODE_ENV ??= "development";
|
|
180
|
+
process.env.VITE_ALEPHA_DEV = "true";
|
|
181
|
+
process.env.SERVER_HOST ??= this.options.host?.toString() ?? "localhost";
|
|
182
|
+
process.env.SERVER_PORT ??= String(
|
|
183
|
+
this.options.port ??
|
|
184
|
+
(process.env.SERVER_PORT ? Number(process.env.SERVER_PORT) : 3000),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
// Merge into process.env (only set if not already defined)
|
|
188
|
+
for (const [key, value] of Object.entries(env)) {
|
|
189
|
+
process.env[key] ??= value;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Load or reload the Alepha instance.
|
|
195
|
+
*/
|
|
196
|
+
protected async loadAlepha(isInitialLoad = false): Promise<Alepha> {
|
|
197
|
+
if (this.alepha) {
|
|
198
|
+
await this.alepha
|
|
199
|
+
.stop()
|
|
200
|
+
.catch((err) => this.log.warn("Error stopping Alepha", err));
|
|
201
|
+
this.alepha = null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (isInitialLoad || this.hasError) {
|
|
205
|
+
this.server.moduleGraph.invalidateAll();
|
|
206
|
+
} else {
|
|
207
|
+
this.invalidateModulesWithImporters();
|
|
208
|
+
}
|
|
209
|
+
this.changedFiles.clear();
|
|
210
|
+
|
|
211
|
+
// Snapshot and restore process.env to isolate each reload
|
|
212
|
+
const envSnapshot = { ...process.env };
|
|
213
|
+
await this.setupEnvironment();
|
|
214
|
+
|
|
215
|
+
await this.server.ssrLoadModule(this.options.entry.server);
|
|
216
|
+
|
|
217
|
+
const alepha: Alepha = (globalThis as any).__alepha;
|
|
218
|
+
if (!alepha) {
|
|
219
|
+
throw new AlephaError(
|
|
220
|
+
"Alepha instance not found after loading entry module",
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
this.alepha = alepha;
|
|
225
|
+
await this.setupAlepha();
|
|
226
|
+
|
|
227
|
+
this.hasError = false;
|
|
228
|
+
process.env = envSnapshot;
|
|
229
|
+
|
|
230
|
+
return alepha;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
public hasReact(): boolean {
|
|
234
|
+
try {
|
|
235
|
+
this.alepha?.inject("ReactServerProvider");
|
|
236
|
+
return true;
|
|
237
|
+
} catch {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Setup Alepha instance with Vite middleware and template.
|
|
244
|
+
*/
|
|
245
|
+
protected async setupAlepha(): Promise<void> {
|
|
246
|
+
if (!this.alepha || !this.hasReact()) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const template = await this.server.transformIndexHtml(
|
|
251
|
+
"/",
|
|
252
|
+
this.templateProvider.generateIndexHtml(this.options.entry),
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
this.alepha.store.set("alepha.react.server.template", template);
|
|
256
|
+
|
|
257
|
+
this.alepha.events.on("server:onRequest", {
|
|
258
|
+
priority: "first",
|
|
259
|
+
callback: async ({ request }) => {
|
|
260
|
+
const node = request.raw.node;
|
|
261
|
+
if (!node || this.isPageRequest(node.req)) return;
|
|
262
|
+
|
|
263
|
+
const handled = await this.runViteMiddleware(
|
|
264
|
+
node.req,
|
|
265
|
+
node.res,
|
|
266
|
+
request,
|
|
267
|
+
);
|
|
268
|
+
if (handled) {
|
|
269
|
+
request.reply.status = node.res.statusCode || 200;
|
|
270
|
+
request.reply.body = null;
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Check if request is for an HTML page (not an asset).
|
|
278
|
+
*/
|
|
279
|
+
protected isPageRequest(req: any): boolean {
|
|
280
|
+
const url = req.url || "/";
|
|
281
|
+
|
|
282
|
+
// Root and index.html are page requests
|
|
283
|
+
if (url === "/" || url === "/index.html") return true;
|
|
284
|
+
|
|
285
|
+
// Vite internal routes
|
|
286
|
+
if (url.startsWith("/@") || url.startsWith("/__vite")) return false;
|
|
287
|
+
|
|
288
|
+
// Files with extensions are assets
|
|
289
|
+
if (/\.\w+$/.test(url.split("?")[0])) return false;
|
|
290
|
+
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Run Vite middleware and detect if it handled the request.
|
|
296
|
+
*/
|
|
297
|
+
protected async runViteMiddleware(
|
|
298
|
+
req: any,
|
|
299
|
+
res: any,
|
|
300
|
+
ctx: { metadata: any },
|
|
301
|
+
): Promise<boolean> {
|
|
302
|
+
return new Promise((resolve) => {
|
|
303
|
+
let resolved = false;
|
|
304
|
+
|
|
305
|
+
const done = (handled: boolean) => {
|
|
306
|
+
if (resolved) return;
|
|
307
|
+
resolved = true;
|
|
308
|
+
if (handled) ctx.metadata.vite = true;
|
|
309
|
+
resolve(handled);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
res.on("finish", () => done(true));
|
|
313
|
+
res.on("close", () => res.headersSent && done(true));
|
|
314
|
+
|
|
315
|
+
this.server.middlewares(req, res, () => done(false));
|
|
316
|
+
|
|
317
|
+
// Check after microtask if Vite started writing (for async handlers)
|
|
318
|
+
setImmediate(() => {
|
|
319
|
+
if (res.headersSent || res.writableEnded) {
|
|
320
|
+
done(true);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Invalidate modules and all their importers.
|
|
328
|
+
*/
|
|
329
|
+
protected invalidateModulesWithImporters(): void {
|
|
330
|
+
const invalidated = new Set<string>();
|
|
331
|
+
const queue: string[] = [...this.changedFiles];
|
|
332
|
+
|
|
333
|
+
while (queue.length > 0) {
|
|
334
|
+
const file = queue.pop()!;
|
|
335
|
+
if (invalidated.has(file)) continue;
|
|
336
|
+
|
|
337
|
+
const mod = this.server.moduleGraph.getModuleById(file);
|
|
338
|
+
if (!mod) continue;
|
|
339
|
+
|
|
340
|
+
this.server.moduleGraph.invalidateModule(mod);
|
|
341
|
+
invalidated.add(file);
|
|
342
|
+
|
|
343
|
+
for (const importer of mod.importers) {
|
|
344
|
+
if (importer.id && !invalidated.has(importer.id)) {
|
|
345
|
+
queue.push(importer.id);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { $inject } from "alepha";
|
|
2
|
+
import { FileSystemProvider } from "alepha/file";
|
|
3
|
+
import type { AppEntry } from "./AppEntryProvider.ts";
|
|
4
|
+
|
|
5
|
+
export class ViteTemplateProvider {
|
|
6
|
+
protected readonly fs = $inject(FileSystemProvider);
|
|
7
|
+
|
|
8
|
+
public generateIndexHtml(entry: AppEntry): string {
|
|
9
|
+
const style = entry.style;
|
|
10
|
+
const browser = entry.browser ?? entry.server;
|
|
11
|
+
return `
|
|
12
|
+
<!DOCTYPE html>
|
|
13
|
+
<html lang="en">
|
|
14
|
+
<head>
|
|
15
|
+
<meta charset="UTF-8" />
|
|
16
|
+
<title>App</title>
|
|
17
|
+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
18
|
+
${style ? `<link rel="stylesheet" href="/${style}" />` : ""}
|
|
19
|
+
</head>
|
|
20
|
+
<body>
|
|
21
|
+
<div id="root"></div>
|
|
22
|
+
<script type="module" src="/${browser}"></script>
|
|
23
|
+
</body>
|
|
24
|
+
</html>
|
|
25
|
+
`.trim();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -3,7 +3,7 @@ import { $inject, Alepha, AlephaError } from "alepha";
|
|
|
3
3
|
import { EnvUtils } from "alepha/command";
|
|
4
4
|
import { FileSystemProvider } from "alepha/file";
|
|
5
5
|
import { $logger } from "alepha/logger";
|
|
6
|
-
import {
|
|
6
|
+
import { AppEntryProvider } from "../providers/AppEntryProvider.ts";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Core utility service for CLI commands.
|
|
@@ -18,6 +18,7 @@ export class AlephaCliUtils {
|
|
|
18
18
|
protected readonly log = $logger();
|
|
19
19
|
protected readonly fs = $inject(FileSystemProvider);
|
|
20
20
|
protected readonly envUtils = $inject(EnvUtils);
|
|
21
|
+
protected readonly boot = $inject(AppEntryProvider);
|
|
21
22
|
|
|
22
23
|
// ===========================================
|
|
23
24
|
// Command Execution
|
|
@@ -77,6 +78,21 @@ export class AlephaCliUtils {
|
|
|
77
78
|
);
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
// check if parent folder (monorepo) has the executable (check 3 times)
|
|
82
|
+
if (!execPath) {
|
|
83
|
+
let parentDir = this.fs.join(root, "..");
|
|
84
|
+
for (let i = 0; i < 3; i++) {
|
|
85
|
+
execPath = await this.checkFileExists(
|
|
86
|
+
parentDir,
|
|
87
|
+
`node_modules/.bin/${app}${suffix}`,
|
|
88
|
+
);
|
|
89
|
+
if (execPath) {
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
parentDir = this.fs.join(parentDir, "..");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
80
96
|
if (!execPath) {
|
|
81
97
|
throw new AlephaError(
|
|
82
98
|
`Could not find executable for command '${app}'. Make sure the package is installed.`,
|
|
@@ -118,7 +134,22 @@ export class AlephaCliUtils {
|
|
|
118
134
|
}> {
|
|
119
135
|
process.env.ALEPHA_CLI_IMPORT = "true";
|
|
120
136
|
|
|
121
|
-
const
|
|
137
|
+
const root = rootDir ?? process.cwd();
|
|
138
|
+
let entry: string;
|
|
139
|
+
|
|
140
|
+
if (explicitEntry) {
|
|
141
|
+
// Explicit entry provided
|
|
142
|
+
entry = this.fs.join(root, explicitEntry);
|
|
143
|
+
if (!(await this.fs.exists(entry))) {
|
|
144
|
+
throw new AlephaError(
|
|
145
|
+
`Explicit server entry file "${explicitEntry}" not found.`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
// Auto-discover entry
|
|
150
|
+
const appEntry = await this.boot.getAppEntry(root);
|
|
151
|
+
entry = this.fs.join(root, appEntry.server);
|
|
152
|
+
}
|
|
122
153
|
|
|
123
154
|
delete (global as any).__alepha;
|
|
124
155
|
|
|
@@ -89,6 +89,13 @@ export class PackageManagerUtils {
|
|
|
89
89
|
return this.hasDependency(root, "expo");
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Check if React is present in the project.
|
|
94
|
+
*/
|
|
95
|
+
public async hasReact(root: string): Promise<boolean> {
|
|
96
|
+
return this.hasDependency(root, "react");
|
|
97
|
+
}
|
|
98
|
+
|
|
92
99
|
/**
|
|
93
100
|
* Install a dependency if it's missing from the project.
|
|
94
101
|
*/
|
|
@@ -114,7 +121,7 @@ export class PackageManagerUtils {
|
|
|
114
121
|
const cmd = await this.getInstallCommand(root, packageName, dev);
|
|
115
122
|
|
|
116
123
|
if (options.run) {
|
|
117
|
-
await options.run(cmd, { alias: `
|
|
124
|
+
await options.run(cmd, { alias: `add ${packageName}`, root });
|
|
118
125
|
} else if (options.exec) {
|
|
119
126
|
this.log.debug(`Installing ${packageName}`);
|
|
120
127
|
await options.exec(cmd, { global: true, root });
|
|
@@ -261,12 +268,12 @@ export class PackageManagerUtils {
|
|
|
261
268
|
verify: "alepha verify",
|
|
262
269
|
};
|
|
263
270
|
|
|
264
|
-
if (modes.
|
|
271
|
+
if (modes.ui) {
|
|
265
272
|
dependencies["@alepha/ui"] = `^${version}`;
|
|
266
|
-
modes.
|
|
273
|
+
modes.react = true;
|
|
267
274
|
}
|
|
268
275
|
|
|
269
|
-
if (modes.
|
|
276
|
+
if (modes.react) {
|
|
270
277
|
dependencies["@alepha/react"] = `^${version}`;
|
|
271
278
|
dependencies.react = "^19.2.0";
|
|
272
279
|
dependencies["react-dom"] = "^19.2.0";
|
|
@@ -295,7 +302,7 @@ export class PackageManagerUtils {
|
|
|
295
302
|
}
|
|
296
303
|
|
|
297
304
|
export interface DependencyModes {
|
|
298
|
-
|
|
299
|
-
|
|
305
|
+
react?: boolean;
|
|
306
|
+
ui?: boolean;
|
|
300
307
|
expo?: boolean;
|
|
301
308
|
}
|