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.
Files changed (222) hide show
  1. package/README.md +43 -98
  2. package/dist/api/audits/index.d.ts +240 -240
  3. package/dist/api/audits/index.d.ts.map +1 -1
  4. package/dist/api/audits/index.js +2 -2
  5. package/dist/api/audits/index.js.map +1 -1
  6. package/dist/api/files/index.d.ts +185 -185
  7. package/dist/api/files/index.d.ts.map +1 -1
  8. package/dist/api/files/index.js +2 -2
  9. package/dist/api/files/index.js.map +1 -1
  10. package/dist/api/jobs/index.d.ts +245 -245
  11. package/dist/api/jobs/index.d.ts.map +1 -1
  12. package/dist/api/notifications/index.browser.js +4 -4
  13. package/dist/api/notifications/index.browser.js.map +1 -1
  14. package/dist/api/notifications/index.d.ts +74 -74
  15. package/dist/api/notifications/index.d.ts.map +1 -1
  16. package/dist/api/notifications/index.js +4 -4
  17. package/dist/api/notifications/index.js.map +1 -1
  18. package/dist/api/parameters/index.d.ts +221 -221
  19. package/dist/api/parameters/index.d.ts.map +1 -1
  20. package/dist/api/users/index.d.ts +1632 -1631
  21. package/dist/api/users/index.d.ts.map +1 -1
  22. package/dist/api/users/index.js +26 -34
  23. package/dist/api/users/index.js.map +1 -1
  24. package/dist/api/verifications/index.d.ts +132 -132
  25. package/dist/api/verifications/index.d.ts.map +1 -1
  26. package/dist/batch/index.d.ts +122 -122
  27. package/dist/batch/index.d.ts.map +1 -1
  28. package/dist/bucket/index.d.ts +163 -163
  29. package/dist/bucket/index.d.ts.map +1 -1
  30. package/dist/cache/core/index.d.ts +46 -46
  31. package/dist/cache/core/index.d.ts.map +1 -1
  32. package/dist/cache/redis/index.d.ts.map +1 -1
  33. package/dist/cache/redis/index.js +2 -2
  34. package/dist/cache/redis/index.js.map +1 -1
  35. package/dist/cli/index.d.ts +5933 -201
  36. package/dist/cli/index.d.ts.map +1 -1
  37. package/dist/cli/index.js +609 -169
  38. package/dist/cli/index.js.map +1 -1
  39. package/dist/command/index.d.ts +296 -296
  40. package/dist/command/index.d.ts.map +1 -1
  41. package/dist/command/index.js +19 -19
  42. package/dist/command/index.js.map +1 -1
  43. package/dist/core/index.browser.js +268 -79
  44. package/dist/core/index.browser.js.map +1 -1
  45. package/dist/core/index.d.ts +768 -694
  46. package/dist/core/index.d.ts.map +1 -1
  47. package/dist/core/index.js +268 -79
  48. package/dist/core/index.js.map +1 -1
  49. package/dist/core/index.native.js +268 -79
  50. package/dist/core/index.native.js.map +1 -1
  51. package/dist/datetime/index.d.ts +44 -44
  52. package/dist/datetime/index.d.ts.map +1 -1
  53. package/dist/email/index.d.ts +25 -25
  54. package/dist/email/index.d.ts.map +1 -1
  55. package/dist/fake/index.d.ts +5409 -5409
  56. package/dist/fake/index.d.ts.map +1 -1
  57. package/dist/fake/index.js +22 -22
  58. package/dist/fake/index.js.map +1 -1
  59. package/dist/file/index.d.ts +435 -435
  60. package/dist/file/index.d.ts.map +1 -1
  61. package/dist/lock/core/index.d.ts +208 -208
  62. package/dist/lock/core/index.d.ts.map +1 -1
  63. package/dist/lock/redis/index.d.ts.map +1 -1
  64. package/dist/logger/index.d.ts +24 -24
  65. package/dist/logger/index.d.ts.map +1 -1
  66. package/dist/logger/index.js +1 -5
  67. package/dist/logger/index.js.map +1 -1
  68. package/dist/mcp/index.d.ts +216 -198
  69. package/dist/mcp/index.d.ts.map +1 -1
  70. package/dist/mcp/index.js +28 -4
  71. package/dist/mcp/index.js.map +1 -1
  72. package/dist/orm/index.browser.js +9 -9
  73. package/dist/orm/index.browser.js.map +1 -1
  74. package/dist/orm/index.bun.js +83 -76
  75. package/dist/orm/index.bun.js.map +1 -1
  76. package/dist/orm/index.d.ts +961 -960
  77. package/dist/orm/index.d.ts.map +1 -1
  78. package/dist/orm/index.js +88 -81
  79. package/dist/orm/index.js.map +1 -1
  80. package/dist/queue/core/index.d.ts +244 -244
  81. package/dist/queue/core/index.d.ts.map +1 -1
  82. package/dist/queue/redis/index.d.ts.map +1 -1
  83. package/dist/redis/index.d.ts +105 -105
  84. package/dist/redis/index.d.ts.map +1 -1
  85. package/dist/retry/index.d.ts +69 -69
  86. package/dist/retry/index.d.ts.map +1 -1
  87. package/dist/router/index.d.ts +6 -6
  88. package/dist/router/index.d.ts.map +1 -1
  89. package/dist/scheduler/index.d.ts +108 -26
  90. package/dist/scheduler/index.d.ts.map +1 -1
  91. package/dist/scheduler/index.js +393 -1
  92. package/dist/scheduler/index.js.map +1 -1
  93. package/dist/security/index.d.ts +532 -209
  94. package/dist/security/index.d.ts.map +1 -1
  95. package/dist/security/index.js +1422 -11
  96. package/dist/security/index.js.map +1 -1
  97. package/dist/server/auth/index.d.ts +1296 -271
  98. package/dist/server/auth/index.d.ts.map +1 -1
  99. package/dist/server/auth/index.js +1249 -18
  100. package/dist/server/auth/index.js.map +1 -1
  101. package/dist/server/cache/index.d.ts +56 -56
  102. package/dist/server/cache/index.d.ts.map +1 -1
  103. package/dist/server/compress/index.d.ts +3 -3
  104. package/dist/server/compress/index.d.ts.map +1 -1
  105. package/dist/server/cookies/index.d.ts +6 -6
  106. package/dist/server/cookies/index.d.ts.map +1 -1
  107. package/dist/server/core/index.d.ts +196 -186
  108. package/dist/server/core/index.d.ts.map +1 -1
  109. package/dist/server/core/index.js +43 -27
  110. package/dist/server/core/index.js.map +1 -1
  111. package/dist/server/cors/index.d.ts +11 -11
  112. package/dist/server/cors/index.d.ts.map +1 -1
  113. package/dist/server/health/index.d.ts.map +1 -1
  114. package/dist/server/helmet/index.d.ts +2 -2
  115. package/dist/server/helmet/index.d.ts.map +1 -1
  116. package/dist/server/links/index.browser.js +9 -1
  117. package/dist/server/links/index.browser.js.map +1 -1
  118. package/dist/server/links/index.d.ts +83 -83
  119. package/dist/server/links/index.d.ts.map +1 -1
  120. package/dist/server/links/index.js +13 -5
  121. package/dist/server/links/index.js.map +1 -1
  122. package/dist/server/metrics/index.d.ts +514 -1
  123. package/dist/server/metrics/index.d.ts.map +1 -1
  124. package/dist/server/metrics/index.js +4462 -4
  125. package/dist/server/metrics/index.js.map +1 -1
  126. package/dist/server/multipart/index.d.ts +6 -6
  127. package/dist/server/multipart/index.d.ts.map +1 -1
  128. package/dist/server/proxy/index.d.ts +102 -102
  129. package/dist/server/proxy/index.d.ts.map +1 -1
  130. package/dist/server/rate-limit/index.d.ts +16 -16
  131. package/dist/server/rate-limit/index.d.ts.map +1 -1
  132. package/dist/server/static/index.d.ts +44 -44
  133. package/dist/server/static/index.d.ts.map +1 -1
  134. package/dist/server/swagger/index.d.ts +47 -47
  135. package/dist/server/swagger/index.d.ts.map +1 -1
  136. package/dist/sms/index.d.ts +11 -11
  137. package/dist/sms/index.d.ts.map +1 -1
  138. package/dist/sms/index.js +3 -3
  139. package/dist/sms/index.js.map +1 -1
  140. package/dist/thread/index.d.ts +71 -71
  141. package/dist/thread/index.d.ts.map +1 -1
  142. package/dist/thread/index.js +2 -2
  143. package/dist/thread/index.js.map +1 -1
  144. package/dist/topic/core/index.d.ts +318 -318
  145. package/dist/topic/core/index.d.ts.map +1 -1
  146. package/dist/topic/redis/index.d.ts +6 -6
  147. package/dist/topic/redis/index.d.ts.map +1 -1
  148. package/dist/vite/index.d.ts +2324 -1719
  149. package/dist/vite/index.d.ts.map +1 -1
  150. package/dist/vite/index.js +123 -475
  151. package/dist/vite/index.js.map +1 -1
  152. package/dist/websocket/index.browser.js +3 -3
  153. package/dist/websocket/index.browser.js.map +1 -1
  154. package/dist/websocket/index.d.ts +275 -275
  155. package/dist/websocket/index.d.ts.map +1 -1
  156. package/dist/websocket/index.js +3 -3
  157. package/dist/websocket/index.js.map +1 -1
  158. package/package.json +9 -9
  159. package/src/api/users/services/SessionService.ts +0 -10
  160. package/src/cli/apps/AlephaCli.ts +2 -2
  161. package/src/cli/apps/AlephaPackageBuilderCli.ts +9 -1
  162. package/src/cli/assets/apiHelloControllerTs.ts +2 -1
  163. package/src/cli/assets/biomeJson.ts +2 -1
  164. package/src/cli/assets/claudeMd.ts +9 -4
  165. package/src/cli/assets/dummySpecTs.ts +2 -1
  166. package/src/cli/assets/editorconfig.ts +2 -1
  167. package/src/cli/assets/mainBrowserTs.ts +2 -1
  168. package/src/cli/assets/mainCss.ts +24 -0
  169. package/src/cli/assets/tsconfigJson.ts +2 -1
  170. package/src/cli/assets/webAppRouterTs.ts +2 -1
  171. package/src/cli/assets/webHelloComponentTsx.ts +6 -2
  172. package/src/cli/atoms/appEntryOptions.ts +13 -0
  173. package/src/cli/atoms/buildOptions.ts +1 -1
  174. package/src/cli/atoms/changelogOptions.ts +1 -1
  175. package/src/cli/commands/build.ts +63 -47
  176. package/src/cli/commands/dev.ts +16 -33
  177. package/src/cli/commands/gen/env.ts +1 -1
  178. package/src/cli/commands/init.ts +17 -8
  179. package/src/cli/commands/lint.ts +1 -1
  180. package/src/cli/defineConfig.ts +9 -0
  181. package/src/cli/index.ts +2 -1
  182. package/src/cli/providers/AppEntryProvider.ts +131 -0
  183. package/src/cli/providers/ViteBuildProvider.ts +82 -0
  184. package/src/cli/providers/ViteDevServerProvider.ts +350 -0
  185. package/src/cli/providers/ViteTemplateProvider.ts +27 -0
  186. package/src/cli/services/AlephaCliUtils.ts +33 -2
  187. package/src/cli/services/PackageManagerUtils.ts +13 -6
  188. package/src/cli/services/ProjectScaffolder.ts +72 -49
  189. package/src/core/Alepha.ts +2 -8
  190. package/src/core/primitives/$module.ts +12 -0
  191. package/src/core/providers/KeylessJsonSchemaCodec.spec.ts +257 -0
  192. package/src/core/providers/KeylessJsonSchemaCodec.ts +396 -14
  193. package/src/core/providers/SchemaValidator.spec.ts +236 -0
  194. package/src/logger/providers/PrettyFormatterProvider.ts +0 -9
  195. package/src/mcp/errors/McpError.ts +30 -0
  196. package/src/mcp/index.ts +3 -0
  197. package/src/mcp/transports/SseMcpTransport.ts +16 -6
  198. package/src/orm/providers/DrizzleKitProvider.ts +3 -5
  199. package/src/orm/services/Repository.ts +11 -0
  200. package/src/server/core/index.ts +1 -1
  201. package/src/server/core/providers/BunHttpServerProvider.ts +1 -1
  202. package/src/server/core/providers/NodeHttpServerProvider.spec.ts +125 -0
  203. package/src/server/core/providers/NodeHttpServerProvider.ts +71 -22
  204. package/src/server/core/providers/ServerLoggerProvider.ts +2 -2
  205. package/src/server/core/providers/ServerProvider.ts +9 -12
  206. package/src/server/links/atoms/apiLinksAtom.ts +7 -0
  207. package/src/server/links/index.browser.ts +2 -0
  208. package/src/server/links/index.ts +2 -0
  209. package/src/vite/index.ts +3 -2
  210. package/src/vite/tasks/buildClient.ts +0 -1
  211. package/src/vite/tasks/buildServer.ts +68 -21
  212. package/src/vite/tasks/copyAssets.ts +5 -4
  213. package/src/vite/tasks/generateSitemap.ts +64 -23
  214. package/src/vite/tasks/index.ts +0 -2
  215. package/src/vite/tasks/prerenderPages.ts +49 -24
  216. package/src/cli/assets/indexHtml.ts +0 -15
  217. package/src/cli/commands/format.ts +0 -23
  218. package/src/vite/helpers/boot.ts +0 -117
  219. package/src/vite/plugins/viteAlephaDev.ts +0 -177
  220. package/src/vite/tasks/devServer.ts +0 -71
  221. package/src/vite/tasks/runAlepha.ts +0 -270
  222. /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 { boot } from "alepha/vite";
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 entry = await boot.getServerEntry(rootDir, explicitEntry);
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: `installing ${packageName}`, root });
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.admin) {
271
+ if (modes.ui) {
265
272
  dependencies["@alepha/ui"] = `^${version}`;
266
- modes.web = true;
273
+ modes.react = true;
267
274
  }
268
275
 
269
- if (modes.web) {
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
- web?: boolean;
299
- admin?: boolean;
305
+ react?: boolean;
306
+ ui?: boolean;
300
307
  expo?: boolean;
301
308
  }