alepha 0.14.4 → 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 +44 -102
- package/dist/api/audits/index.d.ts +331 -443
- 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 +0 -113
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/files/index.js +2 -3
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.d.ts +151 -262
- 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 +164 -276
- 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 +265 -377
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/users/index.browser.js +1 -2
- package/dist/api/users/index.browser.js.map +1 -1
- package/dist/api/users/index.d.ts +195 -301
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +203 -184
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts.map +1 -1
- package/dist/batch/index.d.ts.map +1 -1
- package/dist/batch/index.js +1 -2
- package/dist/batch/index.js.map +1 -1
- package/dist/bucket/index.d.ts.map +1 -1
- 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 +5900 -165
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +1481 -639
- package/dist/cli/index.js.map +1 -1
- package/dist/command/index.d.ts +8 -4
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js +29 -25
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +563 -54
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +175 -8
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +564 -54
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +563 -54
- package/dist/core/index.native.js.map +1 -1
- package/dist/datetime/index.d.ts.map +1 -1
- package/dist/datetime/index.js +4 -4
- package/dist/datetime/index.js.map +1 -1
- package/dist/email/index.d.ts +89 -42
- package/dist/email/index.d.ts.map +1 -1
- package/dist/email/index.js +129 -33
- package/dist/email/index.js.map +1 -1
- package/dist/fake/index.d.ts +7969 -2
- 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 +134 -1
- package/dist/file/index.d.ts.map +1 -1
- package/dist/file/index.js +253 -1
- package/dist/file/index.js.map +1 -1
- 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 +1 -2
- 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 +19 -1
- 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/chunk-DH6iiROE.js +38 -0
- package/dist/orm/index.browser.js +9 -9
- package/dist/orm/index.browser.js.map +1 -1
- package/dist/orm/index.bun.js +2821 -0
- package/dist/orm/index.bun.js.map +1 -0
- package/dist/orm/index.d.ts +318 -169
- package/dist/orm/index.d.ts.map +1 -1
- package/dist/orm/index.js +2086 -1776
- package/dist/orm/index.js.map +1 -1
- package/dist/queue/core/index.d.ts +4 -4
- package/dist/queue/core/index.d.ts.map +1 -1
- package/dist/queue/redis/index.d.ts.map +1 -1
- package/dist/redis/index.bun.js +285 -0
- package/dist/redis/index.bun.js.map +1 -0
- package/dist/redis/index.d.ts +13 -31
- package/dist/redis/index.d.ts.map +1 -1
- package/dist/redis/index.js +18 -38
- package/dist/redis/index.js.map +1 -1
- package/dist/retry/index.d.ts.map +1 -1
- package/dist/router/index.d.ts.map +1 -1
- package/dist/scheduler/index.d.ts +83 -1
- 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.browser.js +5 -1
- package/dist/security/index.browser.js.map +1 -1
- package/dist/security/index.d.ts +598 -112
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +1808 -97
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +1200 -175
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +1268 -37
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/cache/index.d.ts +6 -3
- package/dist/server/cache/index.d.ts.map +1 -1
- package/dist/server/cache/index.js +1 -1
- package/dist/server/cache/index.js.map +1 -1
- package/dist/server/compress/index.d.ts.map +1 -1
- package/dist/server/cookies/index.d.ts.map +1 -1
- package/dist/server/cookies/index.js +3 -3
- package/dist/server/cookies/index.js.map +1 -1
- package/dist/server/core/index.d.ts +115 -13
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/core/index.js +321 -139
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/cors/index.d.ts +0 -1
- package/dist/server/cors/index.d.ts.map +1 -1
- package/dist/server/health/index.d.ts +0 -1
- package/dist/server/health/index.d.ts.map +1 -1
- 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 +1 -2
- package/dist/server/links/index.d.ts.map +1 -1
- package/dist/server/links/index.js +14 -7
- 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.map +1 -1
- package/dist/server/proxy/index.d.ts +0 -1
- package/dist/server/proxy/index.d.ts.map +1 -1
- package/dist/server/rate-limit/index.d.ts.map +1 -1
- package/dist/server/static/index.d.ts.map +1 -1
- package/dist/server/swagger/index.d.ts +1 -2
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/server/swagger/index.js +1 -2
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/sms/index.d.ts +3 -1
- package/dist/sms/index.d.ts.map +1 -1
- package/dist/sms/index.js +10 -10
- package/dist/sms/index.js.map +1 -1
- package/dist/thread/index.d.ts +0 -1
- 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.map +1 -1
- package/dist/topic/redis/index.d.ts.map +1 -1
- package/dist/vite/index.d.ts +6315 -149
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +140 -469
- package/dist/vite/index.js.map +1 -1
- package/dist/websocket/index.browser.js +9 -9
- package/dist/websocket/index.browser.js.map +1 -1
- package/dist/websocket/index.d.ts +28 -28
- package/dist/websocket/index.d.ts.map +1 -1
- package/dist/websocket/index.js +9 -9
- package/dist/websocket/index.js.map +1 -1
- package/package.json +13 -18
- package/src/api/files/controllers/AdminFileStatsController.ts +0 -1
- package/src/api/users/atoms/realmAuthSettingsAtom.ts +5 -0
- package/src/api/users/controllers/{UserRealmController.ts → RealmController.ts} +11 -11
- package/src/api/users/entities/users.ts +1 -1
- package/src/api/users/index.ts +8 -8
- package/src/api/users/primitives/{$userRealm.ts → $realm.ts} +17 -19
- package/src/api/users/providers/{UserRealmProvider.ts → RealmProvider.ts} +26 -30
- package/src/api/users/schemas/{userRealmConfigSchema.ts → realmConfigSchema.ts} +2 -2
- package/src/api/users/services/CredentialService.ts +7 -7
- package/src/api/users/services/IdentityService.ts +4 -4
- package/src/api/users/services/RegistrationService.spec.ts +25 -27
- package/src/api/users/services/RegistrationService.ts +38 -27
- package/src/api/users/services/SessionCrudService.ts +3 -3
- package/src/api/users/services/SessionService.spec.ts +3 -3
- package/src/api/users/services/SessionService.ts +27 -18
- package/src/api/users/services/UserService.ts +7 -7
- package/src/batch/providers/BatchProvider.ts +1 -2
- package/src/cli/apps/AlephaCli.ts +2 -2
- package/src/cli/apps/AlephaPackageBuilderCli.ts +47 -20
- package/src/cli/assets/apiHelloControllerTs.ts +19 -0
- package/src/cli/assets/apiIndexTs.ts +16 -0
- package/src/cli/assets/biomeJson.ts +2 -1
- package/src/cli/assets/claudeMd.ts +308 -0
- package/src/cli/assets/dummySpecTs.ts +2 -1
- package/src/cli/assets/editorconfig.ts +2 -1
- package/src/cli/assets/mainBrowserTs.ts +4 -3
- package/src/cli/assets/mainCss.ts +24 -0
- package/src/cli/assets/mainServerTs.ts +24 -0
- package/src/cli/assets/tsconfigJson.ts +2 -1
- package/src/cli/assets/webAppRouterTs.ts +16 -0
- package/src/cli/assets/webHelloComponentTsx.ts +20 -0
- package/src/cli/assets/webIndexTs.ts +16 -0
- 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 +97 -61
- package/src/cli/commands/db.ts +21 -18
- package/src/cli/commands/deploy.ts +17 -5
- package/src/cli/commands/dev.ts +26 -47
- package/src/cli/commands/gen/env.ts +1 -1
- package/src/cli/commands/init.ts +79 -25
- package/src/cli/commands/lint.ts +9 -3
- package/src/cli/commands/test.ts +8 -2
- package/src/cli/commands/typecheck.ts +5 -1
- package/src/cli/commands/verify.ts +4 -2
- 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 +72 -602
- package/src/cli/services/PackageManagerUtils.ts +308 -0
- package/src/cli/services/ProjectScaffolder.ts +329 -0
- package/src/command/helpers/Runner.ts +15 -3
- package/src/core/Alepha.ts +2 -8
- package/src/core/__tests__/Alepha-graph.spec.ts +4 -0
- package/src/core/index.shared.ts +1 -0
- package/src/core/index.ts +2 -0
- package/src/core/primitives/$hook.ts +6 -2
- package/src/core/primitives/$module.spec.ts +4 -0
- package/src/core/primitives/$module.ts +12 -0
- package/src/core/providers/AlsProvider.ts +1 -1
- package/src/core/providers/CodecManager.spec.ts +12 -6
- package/src/core/providers/CodecManager.ts +26 -6
- package/src/core/providers/EventManager.ts +169 -13
- package/src/core/providers/KeylessJsonSchemaCodec.spec.ts +878 -0
- package/src/core/providers/KeylessJsonSchemaCodec.ts +789 -0
- package/src/core/providers/SchemaValidator.spec.ts +236 -0
- package/src/core/providers/StateManager.spec.ts +27 -16
- package/src/email/providers/LocalEmailProvider.spec.ts +111 -87
- package/src/email/providers/LocalEmailProvider.ts +52 -15
- package/src/email/providers/NodemailerEmailProvider.ts +167 -56
- package/src/file/errors/FileError.ts +7 -0
- package/src/file/index.ts +9 -1
- package/src/file/providers/MemoryFileSystemProvider.ts +393 -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/index.browser.ts +1 -19
- package/src/orm/index.bun.ts +77 -0
- package/src/orm/index.shared-server.ts +22 -0
- package/src/orm/index.shared.ts +15 -0
- package/src/orm/index.ts +19 -39
- package/src/orm/providers/DrizzleKitProvider.ts +3 -5
- package/src/orm/providers/drivers/BunPostgresProvider.ts +3 -5
- package/src/orm/providers/drivers/BunSqliteProvider.ts +1 -1
- package/src/orm/providers/drivers/CloudflareD1Provider.ts +4 -0
- package/src/orm/providers/drivers/DatabaseProvider.ts +4 -0
- package/src/orm/providers/drivers/PglitePostgresProvider.ts +4 -0
- package/src/orm/services/Repository.ts +19 -0
- package/src/redis/index.bun.ts +35 -0
- package/src/redis/providers/BunRedisProvider.ts +12 -43
- package/src/redis/providers/BunRedisSubscriberProvider.ts +2 -3
- package/src/redis/providers/NodeRedisProvider.ts +16 -34
- package/src/{server/security → security}/__tests__/BasicAuth.spec.ts +11 -11
- package/src/{server/security → security}/__tests__/ServerSecurityProvider-realm.spec.ts +21 -16
- package/src/{server/security/providers → security/__tests__}/ServerSecurityProvider.spec.ts +5 -5
- package/src/security/index.browser.ts +5 -0
- package/src/security/index.ts +90 -7
- package/src/security/primitives/{$realm.spec.ts → $issuer.spec.ts} +11 -11
- package/src/security/primitives/{$realm.ts → $issuer.ts} +20 -17
- package/src/security/primitives/$role.ts +5 -5
- package/src/security/primitives/$serviceAccount.spec.ts +5 -5
- package/src/security/primitives/$serviceAccount.ts +3 -3
- package/src/{server/security → security}/providers/ServerSecurityProvider.ts +5 -7
- package/src/server/auth/primitives/$auth.ts +10 -10
- package/src/server/auth/primitives/$authCredentials.ts +3 -3
- package/src/server/auth/primitives/$authGithub.ts +3 -3
- package/src/server/auth/primitives/$authGoogle.ts +3 -3
- package/src/server/auth/providers/ServerAuthProvider.ts +13 -13
- package/src/server/cache/providers/ServerCacheProvider.ts +1 -1
- package/src/server/cookies/providers/ServerCookiesProvider.ts +3 -3
- 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 +92 -24
- package/src/server/core/providers/ServerBodyParserProvider.ts +19 -23
- package/src/server/core/providers/ServerLoggerProvider.ts +23 -19
- package/src/server/core/providers/ServerProvider.ts +144 -24
- package/src/server/core/providers/ServerRouterProvider.ts +259 -115
- package/src/server/core/providers/ServerTimingProvider.ts +2 -2
- package/src/server/links/atoms/apiLinksAtom.ts +7 -0
- package/src/server/links/index.browser.ts +2 -0
- package/src/server/links/index.ts +3 -1
- package/src/server/links/providers/LinkProvider.ts +1 -1
- package/src/server/swagger/index.ts +1 -1
- package/src/sms/providers/LocalSmsProvider.spec.ts +153 -111
- package/src/sms/providers/LocalSmsProvider.ts +8 -7
- package/src/vite/index.ts +3 -2
- package/src/vite/tasks/buildClient.ts +0 -1
- package/src/vite/tasks/buildServer.ts +80 -22
- package/src/vite/tasks/copyAssets.ts +5 -4
- package/src/vite/tasks/generateCloudflare.ts +7 -0
- 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/dist/server/security/index.browser.js +0 -13
- package/dist/server/security/index.browser.js.map +0 -1
- package/dist/server/security/index.d.ts +0 -173
- package/dist/server/security/index.d.ts.map +0 -1
- package/dist/server/security/index.js +0 -311
- package/dist/server/security/index.js.map +0 -1
- package/src/cli/assets/appRouterTs.ts +0 -9
- package/src/cli/assets/indexHtml.ts +0 -15
- package/src/cli/assets/mainTs.ts +0 -13
- package/src/cli/commands/format.ts +0 -17
- package/src/server/security/index.browser.ts +0 -10
- package/src/server/security/index.ts +0 -94
- package/src/vite/helpers/boot.ts +0 -106
- package/src/vite/plugins/viteAlephaDev.ts +0 -177
- package/src/vite/tasks/devServer.ts +0 -69
- package/src/vite/tasks/runAlepha.ts +0 -270
- /package/src/{server/security → security}/primitives/$basicAuth.ts +0 -0
- /package/src/{server/security → security}/providers/ServerBasicAuthProvider.ts +0 -0
package/dist/cli/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { $atom, $hook, $inject, $module, $use, Alepha, AlephaError, t } from "alepha";
|
|
2
2
|
import { FileSystemProvider } from "alepha/file";
|
|
3
|
-
import { access, mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
|
|
4
|
-
import { join } from "node:path";
|
|
5
3
|
import { $command, CliProvider, EnvUtils } from "alepha/command";
|
|
6
4
|
import { $logger, ConsoleColorProvider } from "alepha/logger";
|
|
7
|
-
import {
|
|
5
|
+
import { buildClient, buildServer, copyAssets, generateCloudflare, generateDocker, generateSitemap, generateVercel, importVite, importViteReact, prerenderPages, viteAlephaSsrPreload } from "alepha/vite";
|
|
8
6
|
import { exec, spawn } from "node:child_process";
|
|
9
7
|
import { readFileSync } from "node:fs";
|
|
8
|
+
import { basename, dirname, join } from "node:path";
|
|
10
9
|
import { promisify } from "node:util";
|
|
11
10
|
import { ServerSwaggerProvider } from "alepha/server/swagger";
|
|
11
|
+
import { access, readFile, readdir } from "node:fs/promises";
|
|
12
12
|
import * as os from "node:os";
|
|
13
13
|
|
|
14
14
|
//#region ../../src/core/constants/KIND.ts
|
|
@@ -79,7 +79,7 @@ $atom$1[KIND] = "atom";
|
|
|
79
79
|
* Options can be overridden via vite.config.ts or CLI flags.
|
|
80
80
|
*/
|
|
81
81
|
const buildOptions = $atom$1({
|
|
82
|
-
name: "alepha.build.options",
|
|
82
|
+
name: "alepha.cli.build.options",
|
|
83
83
|
description: "Build configuration options",
|
|
84
84
|
schema: t.object({
|
|
85
85
|
stats: t.optional(t.boolean({ default: false })),
|
|
@@ -103,168 +103,203 @@ const buildOptions = $atom$1({
|
|
|
103
103
|
});
|
|
104
104
|
|
|
105
105
|
//#endregion
|
|
106
|
-
//#region ../../src/cli/
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
`.trim();
|
|
116
|
-
|
|
117
|
-
//#endregion
|
|
118
|
-
//#region ../../src/cli/assets/biomeJson.ts
|
|
119
|
-
const biomeJson = `
|
|
120
|
-
{
|
|
121
|
-
"$schema": "https://biomejs.dev/schemas/latest/schema.json",
|
|
122
|
-
"vcs": {
|
|
123
|
-
"enabled": true,
|
|
124
|
-
"clientKind": "git"
|
|
125
|
-
},
|
|
126
|
-
"files": {
|
|
127
|
-
"ignoreUnknown": true,
|
|
128
|
-
"includes": ["**", "!node_modules", "!dist"]
|
|
129
|
-
},
|
|
130
|
-
"formatter": {
|
|
131
|
-
"enabled": true,
|
|
132
|
-
"useEditorconfig": true
|
|
133
|
-
},
|
|
134
|
-
"linter": {
|
|
135
|
-
"enabled": true,
|
|
136
|
-
"rules": {
|
|
137
|
-
"recommended": true
|
|
138
|
-
},
|
|
139
|
-
"domains": {
|
|
140
|
-
"react": "recommended"
|
|
141
|
-
}
|
|
142
|
-
},
|
|
143
|
-
"assist": {
|
|
144
|
-
"actions": {
|
|
145
|
-
"source": {
|
|
146
|
-
"organizeImports": "on"
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
`.trim();
|
|
152
|
-
|
|
153
|
-
//#endregion
|
|
154
|
-
//#region ../../src/cli/assets/dummySpecTs.ts
|
|
155
|
-
const dummySpecTs = () => `
|
|
156
|
-
import { test, expect } from "vitest";
|
|
157
|
-
|
|
158
|
-
test("dummy test", () => {
|
|
159
|
-
expect(1 + 1).toBe(2);
|
|
106
|
+
//#region ../../src/cli/atoms/appEntryOptions.ts
|
|
107
|
+
const appEntryOptions = $atom({
|
|
108
|
+
name: "alepha.cli.appEntry.options",
|
|
109
|
+
schema: t.object({
|
|
110
|
+
server: t.optional(t.text()),
|
|
111
|
+
browser: t.optional(t.text()),
|
|
112
|
+
style: t.optional(t.text())
|
|
113
|
+
}),
|
|
114
|
+
default: {}
|
|
160
115
|
});
|
|
161
|
-
`.trim();
|
|
162
116
|
|
|
163
117
|
//#endregion
|
|
164
|
-
//#region ../../src/cli/
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
118
|
+
//#region ../../src/cli/providers/AppEntryProvider.ts
|
|
119
|
+
/**
|
|
120
|
+
* Service for locating entry files in Alepha projects.
|
|
121
|
+
*
|
|
122
|
+
* Originally in alepha/vite, moved to CLI to avoid cli -> vite dependency.
|
|
123
|
+
*/
|
|
124
|
+
var AppEntryProvider = class {
|
|
125
|
+
fs = $inject(FileSystemProvider);
|
|
126
|
+
options = $use(appEntryOptions);
|
|
127
|
+
serverEntries = [
|
|
128
|
+
"main.server.ts",
|
|
129
|
+
"main.server.tsx",
|
|
130
|
+
"main.ts",
|
|
131
|
+
"main.tsx"
|
|
132
|
+
];
|
|
133
|
+
browserEntries = [
|
|
134
|
+
"main.browser.ts",
|
|
135
|
+
"main.browser.tsx",
|
|
136
|
+
"main.ts",
|
|
137
|
+
"main.tsx"
|
|
138
|
+
];
|
|
139
|
+
styleEntries = [
|
|
140
|
+
"main.css",
|
|
141
|
+
"styles.css",
|
|
142
|
+
"style.css"
|
|
143
|
+
];
|
|
144
|
+
/**
|
|
145
|
+
* Get application entry points.
|
|
146
|
+
*
|
|
147
|
+
* Server entry is required, an error is thrown if not found.
|
|
148
|
+
* Browser entry is optional.
|
|
149
|
+
*
|
|
150
|
+
* It will first check for custom entries in options, see appEntryOptions.
|
|
151
|
+
*/
|
|
152
|
+
async getAppEntry(root) {
|
|
153
|
+
const appEntry = {
|
|
154
|
+
root,
|
|
155
|
+
server: ""
|
|
156
|
+
};
|
|
157
|
+
if (this.options.server) {
|
|
158
|
+
if (!await this.fs.exists(this.fs.join(root, this.options.server))) throw new AlephaError(`Custom server entry "${this.options.server}" not found.`);
|
|
159
|
+
appEntry.server = this.options.server;
|
|
160
|
+
}
|
|
161
|
+
if (this.options.browser) {
|
|
162
|
+
if (!await this.fs.exists(this.fs.join(root, this.options.browser))) throw new AlephaError(`Custom browser entry "${this.options.browser}" not found.`);
|
|
163
|
+
appEntry.browser = this.options.browser;
|
|
164
|
+
}
|
|
165
|
+
if (this.options.style) {
|
|
166
|
+
if (!await this.fs.exists(this.fs.join(root, this.options.style))) throw new AlephaError(`Custom style entry "${this.options.style}" not found.`);
|
|
167
|
+
appEntry.style = this.options.style;
|
|
168
|
+
}
|
|
169
|
+
const srcFiles = await this.fs.ls(this.fs.join(root, "src"));
|
|
170
|
+
if (!appEntry.server) {
|
|
171
|
+
for (const entry of this.serverEntries) if (srcFiles.includes(entry)) {
|
|
172
|
+
appEntry.server = this.fs.join("src", entry);
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (!appEntry.server) throw new AlephaError("No server entry found. Please, add a main.server.ts file in the src/ directory or configure a custom entry in alepha.config.ts.");
|
|
177
|
+
if (!appEntry.browser) {
|
|
178
|
+
for (const entry of this.browserEntries) if (srcFiles.includes(entry)) {
|
|
179
|
+
appEntry.browser = this.fs.join("src", entry);
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (!appEntry.style) {
|
|
184
|
+
for (const entry of this.styleEntries) if (srcFiles.includes(entry)) {
|
|
185
|
+
appEntry.style = this.fs.join("src", entry);
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return appEntry;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
178
192
|
|
|
179
193
|
//#endregion
|
|
180
|
-
//#region ../../src/cli/
|
|
181
|
-
|
|
194
|
+
//#region ../../src/cli/providers/ViteTemplateProvider.ts
|
|
195
|
+
var ViteTemplateProvider = class {
|
|
196
|
+
fs = $inject(FileSystemProvider);
|
|
197
|
+
generateIndexHtml(entry) {
|
|
198
|
+
const style = entry.style;
|
|
199
|
+
const browser = entry.browser ?? entry.server;
|
|
200
|
+
return `
|
|
182
201
|
<!DOCTYPE html>
|
|
183
202
|
<html lang="en">
|
|
184
203
|
<head>
|
|
185
|
-
|
|
186
|
-
|
|
204
|
+
<meta charset="UTF-8" />
|
|
205
|
+
<title>App</title>
|
|
206
|
+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
|
207
|
+
${style ? `<link rel="stylesheet" href="/${style}" />` : ""}
|
|
187
208
|
</head>
|
|
188
209
|
<body>
|
|
189
210
|
<div id="root"></div>
|
|
190
|
-
<script type="module" src="
|
|
211
|
+
<script type="module" src="/${browser}"><\/script>
|
|
191
212
|
</body>
|
|
192
213
|
</html>
|
|
193
214
|
`.trim();
|
|
215
|
+
}
|
|
216
|
+
};
|
|
194
217
|
|
|
195
218
|
//#endregion
|
|
196
|
-
//#region ../../src/cli/
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const
|
|
219
|
+
//#region ../../src/cli/providers/ViteBuildProvider.ts
|
|
220
|
+
var ViteBuildProvider = class {
|
|
221
|
+
alepha;
|
|
222
|
+
appEntry;
|
|
223
|
+
viteDevServer;
|
|
224
|
+
templateProvider = $inject(ViteTemplateProvider);
|
|
225
|
+
/**
|
|
226
|
+
* We need to close the Vite dev server after build is done.
|
|
227
|
+
*/
|
|
228
|
+
onReady = $hook({
|
|
229
|
+
on: "ready",
|
|
230
|
+
priority: "last",
|
|
231
|
+
handler: async () => {
|
|
232
|
+
await this.viteDevServer?.close();
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
onStop = $hook({
|
|
236
|
+
on: "stop",
|
|
237
|
+
handler: async () => {
|
|
238
|
+
await this.viteDevServer?.close();
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
async init(opts) {
|
|
242
|
+
const { createServer } = await importVite();
|
|
243
|
+
process.env.ALEPHA_CLI_IMPORT = "true";
|
|
244
|
+
process.env.NODE_ENV = "production";
|
|
245
|
+
process.env.LOG_LEVEL ??= "warn";
|
|
246
|
+
/**
|
|
247
|
+
* 01/26 Vite 7
|
|
248
|
+
* "runnerImport" doesn't work as expected here. (e.g. build docs fail)
|
|
249
|
+
* -> We still use devServer and ssrLoadModule for now.
|
|
250
|
+
* -> This is clearly a bad stuff, we need to find better way.
|
|
251
|
+
*/
|
|
252
|
+
this.viteDevServer = await createServer({
|
|
253
|
+
server: { middlewareMode: true },
|
|
254
|
+
appType: "custom",
|
|
255
|
+
logLevel: "silent"
|
|
256
|
+
});
|
|
257
|
+
await this.viteDevServer.ssrLoadModule(opts.entry.server);
|
|
258
|
+
const alepha = globalThis.__alepha;
|
|
259
|
+
if (!alepha) throw new AlephaError("Alepha instance not found after loading entry module");
|
|
260
|
+
this.alepha = alepha;
|
|
261
|
+
this.appEntry = opts.entry;
|
|
262
|
+
return alepha;
|
|
263
|
+
}
|
|
264
|
+
hasClient() {
|
|
265
|
+
if (!this.alepha) throw new AlephaError("ViteBuildProvider not initialized");
|
|
266
|
+
try {
|
|
267
|
+
this.alepha.inject("ReactServerProvider");
|
|
268
|
+
return true;
|
|
269
|
+
} catch {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
generateIndexHtml() {
|
|
274
|
+
if (!this.appEntry) throw new AlephaError("ViteBuildProvider not initialized");
|
|
275
|
+
return this.templateProvider.generateIndexHtml(this.appEntry);
|
|
276
|
+
}
|
|
277
|
+
};
|
|
236
278
|
|
|
237
279
|
//#endregion
|
|
238
280
|
//#region ../../src/cli/services/AlephaCliUtils.ts
|
|
239
281
|
/**
|
|
240
|
-
*
|
|
282
|
+
* Core utility service for CLI commands.
|
|
241
283
|
*
|
|
242
|
-
*
|
|
243
|
-
* -
|
|
244
|
-
* -
|
|
245
|
-
* -
|
|
246
|
-
* -
|
|
247
|
-
* - Alepha instance loading
|
|
284
|
+
* Provides:
|
|
285
|
+
* - Command execution
|
|
286
|
+
* - File editing helpers
|
|
287
|
+
* - Drizzle/ORM utilities
|
|
288
|
+
* - Environment loading
|
|
248
289
|
*/
|
|
249
290
|
var AlephaCliUtils = class {
|
|
250
291
|
log = $logger();
|
|
251
292
|
fs = $inject(FileSystemProvider);
|
|
252
293
|
envUtils = $inject(EnvUtils);
|
|
253
|
-
|
|
294
|
+
boot = $inject(AppEntryProvider);
|
|
254
295
|
/**
|
|
255
|
-
* Execute a command
|
|
256
|
-
*
|
|
257
|
-
* @example
|
|
258
|
-
* ```ts
|
|
259
|
-
* const runner = alepha.inject(ProcessRunner);
|
|
260
|
-
* await runner.exec("tsx watch src/index.ts");
|
|
261
|
-
* ```
|
|
296
|
+
* Execute a command with inherited stdio.
|
|
262
297
|
*/
|
|
263
298
|
async exec(command, options = {}) {
|
|
264
|
-
const root = process.cwd();
|
|
299
|
+
const root = options.root ?? process.cwd();
|
|
265
300
|
this.log.debug(`Executing command: ${command}`, { cwd: root });
|
|
266
|
-
const runExec = async (app
|
|
267
|
-
const prog = spawn(app
|
|
301
|
+
const runExec = async (app, args) => {
|
|
302
|
+
const prog = spawn(app, args, {
|
|
268
303
|
stdio: "inherit",
|
|
269
304
|
cwd: root,
|
|
270
305
|
env: {
|
|
@@ -277,120 +312,276 @@ var AlephaCliUtils = class {
|
|
|
277
312
|
}));
|
|
278
313
|
};
|
|
279
314
|
if (options.global) {
|
|
280
|
-
const [app
|
|
281
|
-
await runExec(app
|
|
315
|
+
const [app, ...args] = command.split(" ");
|
|
316
|
+
await runExec(app, args);
|
|
282
317
|
return;
|
|
283
318
|
}
|
|
284
319
|
const suffix = process.platform === "win32" ? ".cmd" : "";
|
|
285
320
|
const [app, ...args] = command.split(" ");
|
|
286
|
-
let execPath = await this.checkFileExists(root, `node_modules/.bin/${app}${suffix}
|
|
287
|
-
if (!execPath) execPath = await this.checkFileExists(root, `node_modules/alepha/node_modules/.bin/${app}${suffix}
|
|
321
|
+
let execPath = await this.checkFileExists(root, `node_modules/.bin/${app}${suffix}`);
|
|
322
|
+
if (!execPath) execPath = await this.checkFileExists(root, `node_modules/alepha/node_modules/.bin/${app}${suffix}`);
|
|
323
|
+
if (!execPath) {
|
|
324
|
+
let parentDir = this.fs.join(root, "..");
|
|
325
|
+
for (let i = 0; i < 3; i++) {
|
|
326
|
+
execPath = await this.checkFileExists(parentDir, `node_modules/.bin/${app}${suffix}`);
|
|
327
|
+
if (execPath) break;
|
|
328
|
+
parentDir = this.fs.join(parentDir, "..");
|
|
329
|
+
}
|
|
330
|
+
}
|
|
288
331
|
if (!execPath) throw new AlephaError(`Could not find executable for command '${app}'. Make sure the package is installed.`);
|
|
289
332
|
await runExec(execPath, args);
|
|
290
333
|
}
|
|
291
334
|
/**
|
|
292
335
|
* Write a configuration file to node_modules/.alepha directory.
|
|
293
|
-
*
|
|
294
|
-
* Creates the .alepha directory if it doesn't exist and writes the file with the given content.
|
|
295
|
-
*
|
|
296
|
-
* @param name - The name of the config file to create
|
|
297
|
-
* @param content - The content to write to the file
|
|
298
|
-
* @param root - The root directory (defaults to process.cwd())
|
|
299
|
-
* @returns The absolute path to the created file
|
|
300
|
-
*
|
|
301
|
-
* @example
|
|
302
|
-
* ```ts
|
|
303
|
-
* const runner = alepha.inject(ProcessRunner);
|
|
304
|
-
* const configPath = await runner.writeConfigFile("biome.json", biomeConfig);
|
|
305
|
-
* ```
|
|
306
336
|
*/
|
|
307
337
|
async writeConfigFile(name, content, root = process.cwd()) {
|
|
308
|
-
const dir = join(root, "node_modules", ".alepha");
|
|
309
|
-
await mkdir(dir, { recursive: true }).catch(() => null);
|
|
310
|
-
const path = join(dir, name);
|
|
311
|
-
await writeFile(path, content);
|
|
338
|
+
const dir = this.fs.join(root, "node_modules", ".alepha");
|
|
339
|
+
await this.fs.mkdir(dir, { recursive: true }).catch(() => null);
|
|
340
|
+
const path = this.fs.join(dir, name);
|
|
341
|
+
await this.fs.writeFile(path, content);
|
|
312
342
|
this.log.debug(`Config file written: ${path}`);
|
|
313
343
|
return path;
|
|
314
344
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
345
|
+
/**
|
|
346
|
+
* Load Alepha instance from a server entry file.
|
|
347
|
+
*/
|
|
348
|
+
async loadAlephaFromServerEntryFile(rootDir, explicitEntry) {
|
|
349
|
+
process.env.ALEPHA_CLI_IMPORT = "true";
|
|
350
|
+
const root = rootDir ?? process.cwd();
|
|
351
|
+
let entry;
|
|
352
|
+
if (explicitEntry) {
|
|
353
|
+
entry = this.fs.join(root, explicitEntry);
|
|
354
|
+
if (!await this.fs.exists(entry)) throw new AlephaError(`Explicit server entry file "${explicitEntry}" not found.`);
|
|
355
|
+
} else {
|
|
356
|
+
const appEntry = await this.boot.getAppEntry(root);
|
|
357
|
+
entry = this.fs.join(root, appEntry.server);
|
|
321
358
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
async removeYarn(root) {
|
|
336
|
-
await this.removeFiles(root, [
|
|
337
|
-
".yarn",
|
|
338
|
-
".yarnrc.yml",
|
|
339
|
-
"yarn.lock"
|
|
340
|
-
]);
|
|
341
|
-
await this.editJsonFile(root, "package.json", (pkg) => {
|
|
342
|
-
delete pkg.packageManager;
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
async removePnpm(root) {
|
|
346
|
-
await this.removeFiles(root, ["pnpm-lock.yaml", "pnpm-workspace.yaml"]);
|
|
347
|
-
await this.editJsonFile(root, "package.json", (pkg) => {
|
|
348
|
-
delete pkg.packageManager;
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
async removeNpm(root) {
|
|
352
|
-
await this.removeFiles(root, ["package-lock.json"]);
|
|
353
|
-
}
|
|
354
|
-
async removeBun(root) {
|
|
355
|
-
await this.removeFiles(root, ["bun.lockb", "bun.lock"]);
|
|
356
|
-
}
|
|
357
|
-
async removeAllPmFilesExcept(root, except) {
|
|
358
|
-
if (except !== "yarn") await this.removeYarn(root);
|
|
359
|
-
if (except !== "pnpm") await this.removePnpm(root);
|
|
360
|
-
if (except !== "npm") await this.removeNpm(root);
|
|
361
|
-
if (except !== "bun") await this.removeBun(root);
|
|
359
|
+
delete global.__alepha;
|
|
360
|
+
const mod = await import(entry);
|
|
361
|
+
this.log.debug(`Load entry: ${entry}`);
|
|
362
|
+
if (mod.default instanceof Alepha) return {
|
|
363
|
+
alepha: mod.default,
|
|
364
|
+
entry
|
|
365
|
+
};
|
|
366
|
+
const g = global;
|
|
367
|
+
if (g.__alepha) return {
|
|
368
|
+
alepha: g.__alepha,
|
|
369
|
+
entry
|
|
370
|
+
};
|
|
371
|
+
throw new AlephaError(`Could not find Alepha instance in entry file: ${entry}`);
|
|
362
372
|
}
|
|
363
373
|
/**
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
* Creates a .yarnrc.yml file with node-modules linker if it doesn't exist.
|
|
367
|
-
*
|
|
368
|
-
* @param root - The root directory of the project
|
|
374
|
+
* Generate JavaScript code for Drizzle entities export.
|
|
369
375
|
*/
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
376
|
+
generateEntitiesJs(entry, provider, models = []) {
|
|
377
|
+
return `
|
|
378
|
+
import "${entry}";
|
|
379
|
+
import { DrizzleKitProvider, Repository } from "alepha/orm";
|
|
380
|
+
|
|
381
|
+
const alepha = globalThis.__alepha;
|
|
382
|
+
const kit = alepha.inject(DrizzleKitProvider);
|
|
383
|
+
const provider = alepha.services(Repository).find((it) => it.provider.name === "${provider}").provider;
|
|
384
|
+
const models = kit.getModels(provider);
|
|
385
|
+
|
|
386
|
+
${models.map((it) => `export const ${it} = models["${it}"];`).join("\n")}
|
|
387
|
+
|
|
388
|
+
`.trim();
|
|
373
389
|
}
|
|
374
|
-
|
|
375
|
-
|
|
390
|
+
/**
|
|
391
|
+
* Load environment variables from a .env file.
|
|
392
|
+
*/
|
|
393
|
+
async loadEnv(root, files = [".env"]) {
|
|
394
|
+
await this.envUtils.loadEnv(root, files);
|
|
376
395
|
}
|
|
377
|
-
async
|
|
378
|
-
|
|
396
|
+
async exists(root, path) {
|
|
397
|
+
return this.fs.exists(this.fs.join(root, path));
|
|
379
398
|
}
|
|
380
|
-
async
|
|
381
|
-
|
|
399
|
+
async checkFileExists(root, name) {
|
|
400
|
+
const configPath = this.fs.join(root, name);
|
|
401
|
+
if (await this.fs.exists(configPath)) return configPath;
|
|
382
402
|
}
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
//#endregion
|
|
406
|
+
//#region ../../src/cli/version.ts
|
|
407
|
+
const packageJson = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf-8"));
|
|
408
|
+
const version = packageJson.version;
|
|
409
|
+
|
|
410
|
+
//#endregion
|
|
411
|
+
//#region ../../src/cli/services/PackageManagerUtils.ts
|
|
412
|
+
/**
|
|
413
|
+
* Utility service for package manager operations.
|
|
414
|
+
*
|
|
415
|
+
* Handles detection, installation, and cleanup for:
|
|
416
|
+
* - Yarn
|
|
417
|
+
* - npm
|
|
418
|
+
* - pnpm
|
|
419
|
+
* - Bun
|
|
420
|
+
*/
|
|
421
|
+
var PackageManagerUtils = class {
|
|
422
|
+
log = $logger();
|
|
423
|
+
fs = $inject(FileSystemProvider);
|
|
424
|
+
alepha = $inject(Alepha);
|
|
383
425
|
/**
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
* @param modes - Configuration for which dependencies to include
|
|
387
|
-
* @returns Package.json partial with dependencies, devDependencies, and scripts
|
|
426
|
+
* Detect the package manager used in the project.
|
|
388
427
|
*/
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
428
|
+
async getPackageManager(root, flags) {
|
|
429
|
+
if (flags?.yarn) return "yarn";
|
|
430
|
+
if (flags?.pnpm) return "pnpm";
|
|
431
|
+
if (flags?.npm) return "npm";
|
|
432
|
+
if (flags?.bun) return "bun";
|
|
433
|
+
if (this.alepha.isBun()) return "bun";
|
|
434
|
+
if (await this.fs.exists(this.fs.join(root, "bun.lock"))) return "bun";
|
|
435
|
+
if (await this.fs.exists(this.fs.join(root, "yarn.lock"))) return "yarn";
|
|
436
|
+
if (await this.fs.exists(this.fs.join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
437
|
+
return "npm";
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Get the install command for a package.
|
|
441
|
+
*/
|
|
442
|
+
async getInstallCommand(root, packageName, dev = true) {
|
|
443
|
+
const pm = await this.getPackageManager(root);
|
|
444
|
+
let cmd;
|
|
445
|
+
switch (pm) {
|
|
446
|
+
case "yarn":
|
|
447
|
+
cmd = `yarn add ${dev ? "-D" : ""} ${packageName}`;
|
|
448
|
+
break;
|
|
449
|
+
case "pnpm":
|
|
450
|
+
cmd = `pnpm add ${dev ? "-D" : ""} ${packageName}`;
|
|
451
|
+
break;
|
|
452
|
+
case "bun":
|
|
453
|
+
cmd = `bun add ${dev ? "-d" : ""} ${packageName}`;
|
|
454
|
+
break;
|
|
455
|
+
default: cmd = `npm install ${dev ? "--save-dev" : ""} ${packageName}`;
|
|
456
|
+
}
|
|
457
|
+
return cmd.replace(/\s+/g, " ").trim();
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Check if a dependency is installed in the project.
|
|
461
|
+
*/
|
|
462
|
+
async hasDependency(root, packageName) {
|
|
463
|
+
try {
|
|
464
|
+
const pkg = await this.readPackageJson(root);
|
|
465
|
+
return !!(pkg.dependencies?.[packageName] || pkg.devDependencies?.[packageName]);
|
|
466
|
+
} catch {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Check if Expo is present in the project.
|
|
472
|
+
*/
|
|
473
|
+
async hasExpo(root) {
|
|
474
|
+
return this.hasDependency(root, "expo");
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Check if React is present in the project.
|
|
478
|
+
*/
|
|
479
|
+
async hasReact(root) {
|
|
480
|
+
return this.hasDependency(root, "react");
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Install a dependency if it's missing from the project.
|
|
484
|
+
*/
|
|
485
|
+
async ensureDependency(root, packageName, options = {}) {
|
|
486
|
+
const { dev = true } = options;
|
|
487
|
+
if (await this.hasDependency(root, packageName)) {
|
|
488
|
+
this.log.debug(`Dependency '${packageName}' is already installed`);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const cmd = await this.getInstallCommand(root, packageName, dev);
|
|
492
|
+
if (options.run) await options.run(cmd, {
|
|
493
|
+
alias: `add ${packageName}`,
|
|
494
|
+
root
|
|
495
|
+
});
|
|
496
|
+
else if (options.exec) {
|
|
497
|
+
this.log.debug(`Installing ${packageName}`);
|
|
498
|
+
await options.exec(cmd, {
|
|
499
|
+
global: true,
|
|
500
|
+
root
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
async ensureYarn(root) {
|
|
505
|
+
const yarnrcPath = this.fs.join(root, ".yarnrc.yml");
|
|
506
|
+
if (!await this.fs.exists(yarnrcPath)) await this.fs.writeFile(yarnrcPath, "nodeLinker: node-modules");
|
|
507
|
+
await this.removeAllPmFilesExcept(root, "yarn");
|
|
508
|
+
}
|
|
509
|
+
async ensureBun(root) {
|
|
510
|
+
await this.removeAllPmFilesExcept(root, "bun");
|
|
511
|
+
}
|
|
512
|
+
async ensurePnpm(root) {
|
|
513
|
+
await this.removeAllPmFilesExcept(root, "pnpm");
|
|
514
|
+
}
|
|
515
|
+
async ensureNpm(root) {
|
|
516
|
+
await this.removeAllPmFilesExcept(root, "npm");
|
|
517
|
+
}
|
|
518
|
+
async removeAllPmFilesExcept(root, except) {
|
|
519
|
+
if (except !== "yarn") await this.removeYarn(root);
|
|
520
|
+
if (except !== "pnpm") await this.removePnpm(root);
|
|
521
|
+
if (except !== "npm") await this.removeNpm(root);
|
|
522
|
+
if (except !== "bun") await this.removeBun(root);
|
|
523
|
+
}
|
|
524
|
+
async removeYarn(root) {
|
|
525
|
+
await this.removeFiles(root, [
|
|
526
|
+
".yarn",
|
|
527
|
+
".yarnrc.yml",
|
|
528
|
+
"yarn.lock"
|
|
529
|
+
]);
|
|
530
|
+
await this.editPackageJson(root, (pkg) => {
|
|
531
|
+
delete pkg.packageManager;
|
|
532
|
+
return pkg;
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
async removePnpm(root) {
|
|
536
|
+
await this.removeFiles(root, ["pnpm-lock.yaml", "pnpm-workspace.yaml"]);
|
|
537
|
+
await this.editPackageJson(root, (pkg) => {
|
|
538
|
+
delete pkg.packageManager;
|
|
539
|
+
return pkg;
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
async removeNpm(root) {
|
|
543
|
+
await this.removeFiles(root, ["package-lock.json"]);
|
|
544
|
+
}
|
|
545
|
+
async removeBun(root) {
|
|
546
|
+
await this.removeFiles(root, ["bun.lockb", "bun.lock"]);
|
|
547
|
+
}
|
|
548
|
+
async readPackageJson(root) {
|
|
549
|
+
const content = await this.fs.createFile({ path: this.fs.join(root, "package.json") }).text();
|
|
550
|
+
return JSON.parse(content);
|
|
551
|
+
}
|
|
552
|
+
async writePackageJson(root, content) {
|
|
553
|
+
await this.fs.writeFile(this.fs.join(root, "package.json"), JSON.stringify(content, null, 2));
|
|
554
|
+
}
|
|
555
|
+
async editPackageJson(root, editFn) {
|
|
556
|
+
try {
|
|
557
|
+
const updated = editFn(await this.readPackageJson(root));
|
|
558
|
+
await this.writePackageJson(root, updated);
|
|
559
|
+
} catch {}
|
|
560
|
+
}
|
|
561
|
+
async ensurePackageJson(root, modes) {
|
|
562
|
+
const packageJsonPath = this.fs.join(root, "package.json");
|
|
563
|
+
if (!await this.fs.exists(packageJsonPath)) {
|
|
564
|
+
const content = this.generatePackageJsonContent(modes);
|
|
565
|
+
await this.writePackageJson(root, content);
|
|
566
|
+
return content;
|
|
567
|
+
}
|
|
568
|
+
const packageJson = await this.readPackageJson(root);
|
|
569
|
+
const newContent = this.generatePackageJsonContent(modes);
|
|
570
|
+
packageJson.type = "module";
|
|
571
|
+
packageJson.dependencies ??= {};
|
|
572
|
+
packageJson.devDependencies ??= {};
|
|
573
|
+
packageJson.scripts ??= {};
|
|
574
|
+
Object.assign(packageJson.dependencies, newContent.dependencies);
|
|
575
|
+
Object.assign(packageJson.devDependencies, newContent.devDependencies);
|
|
576
|
+
Object.assign(packageJson.scripts, newContent.scripts);
|
|
577
|
+
await this.writePackageJson(root, packageJson);
|
|
578
|
+
return packageJson;
|
|
579
|
+
}
|
|
580
|
+
generatePackageJsonContent(modes) {
|
|
581
|
+
const dependencies = { alepha: `^${version}` };
|
|
582
|
+
const devDependencies = {};
|
|
583
|
+
const scripts = {
|
|
584
|
+
dev: "alepha dev",
|
|
394
585
|
build: "alepha build",
|
|
395
586
|
lint: "alepha lint",
|
|
396
587
|
typecheck: "alepha typecheck",
|
|
@@ -413,220 +604,626 @@ var AlephaCliUtils = class {
|
|
|
413
604
|
scripts
|
|
414
605
|
};
|
|
415
606
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
* - Add Alepha dependencies
|
|
422
|
-
* - Add standard scripts
|
|
423
|
-
*
|
|
424
|
-
* @param root - The root directory of the project
|
|
425
|
-
* @param modes - Configuration for which dependencies to include
|
|
426
|
-
*/
|
|
427
|
-
async ensurePackageJson(root, modes) {
|
|
428
|
-
const packageJsonPath = join(root, "package.json");
|
|
429
|
-
try {
|
|
430
|
-
await access(packageJsonPath);
|
|
431
|
-
} catch (error) {
|
|
432
|
-
const obj = this.generatePackageJsonContent(modes);
|
|
433
|
-
await writeFile(packageJsonPath, JSON.stringify(obj, null, 2));
|
|
434
|
-
return obj;
|
|
435
|
-
}
|
|
436
|
-
const content = await readFile(packageJsonPath, "utf8");
|
|
437
|
-
const packageJson$1 = JSON.parse(content);
|
|
438
|
-
const newPackageJson = this.generatePackageJsonContent(modes);
|
|
439
|
-
packageJson$1.type = "module";
|
|
440
|
-
packageJson$1.dependencies ??= {};
|
|
441
|
-
packageJson$1.devDependencies ??= {};
|
|
442
|
-
packageJson$1.scripts ??= {};
|
|
443
|
-
Object.assign(packageJson$1.dependencies, newPackageJson.dependencies);
|
|
444
|
-
Object.assign(packageJson$1.devDependencies, newPackageJson.devDependencies);
|
|
445
|
-
Object.assign(packageJson$1.scripts, newPackageJson.scripts);
|
|
446
|
-
await writeFile(packageJsonPath, JSON.stringify(packageJson$1, null, 2));
|
|
447
|
-
return packageJson$1;
|
|
448
|
-
}
|
|
449
|
-
async ensureConfig(root, opts) {
|
|
450
|
-
const tasks = [];
|
|
451
|
-
if (opts.packageJson) tasks.push(this.ensurePackageJson(root, typeof opts.packageJson === "boolean" ? {} : opts.packageJson));
|
|
452
|
-
if (opts.tsconfigJson) tasks.push(this.ensureTsConfig(root));
|
|
453
|
-
if (opts.indexHtml) tasks.push(this.ensureIndexHtml(root));
|
|
454
|
-
if (opts.biomeJson) tasks.push(this.ensureBiomeConfig(root));
|
|
455
|
-
if (opts.editorconfig) tasks.push(this.ensureEditorConfig(root));
|
|
456
|
-
return await Promise.all(tasks);
|
|
457
|
-
}
|
|
458
|
-
/**
|
|
459
|
-
* Ensure tsconfig.json exists in the project.
|
|
460
|
-
*
|
|
461
|
-
* Creates a standard Alepha tsconfig.json if none exists.
|
|
462
|
-
*
|
|
463
|
-
* @param root - The root directory of the project
|
|
464
|
-
*/
|
|
465
|
-
async ensureTsConfig(root) {
|
|
466
|
-
await this.ensureFileExists(root, "tsconfig.json", tsconfigJson, true);
|
|
467
|
-
}
|
|
468
|
-
async checkFileExists(root, name, checkParentDirectories = false) {
|
|
469
|
-
const configPath = join(root, name);
|
|
470
|
-
if (!checkParentDirectories) try {
|
|
471
|
-
await access(configPath);
|
|
472
|
-
return configPath;
|
|
473
|
-
} catch {
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
let currentDir = root;
|
|
477
|
-
const maxIterations = 10;
|
|
478
|
-
let level = 0;
|
|
479
|
-
while (level < maxIterations) {
|
|
480
|
-
try {
|
|
481
|
-
const maybe = join(currentDir, name);
|
|
482
|
-
await access(maybe);
|
|
483
|
-
return maybe;
|
|
484
|
-
} catch {
|
|
485
|
-
const parentDir = join(currentDir, "..");
|
|
486
|
-
if (parentDir === currentDir) break;
|
|
487
|
-
currentDir = parentDir;
|
|
488
|
-
}
|
|
489
|
-
level += 1;
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
async ensureFileExists(root, name, content, checkParentDirectories = false) {
|
|
493
|
-
if (!await this.checkFileExists(root, name, checkParentDirectories)) await writeFile(join(root, name), content);
|
|
607
|
+
async removeFiles(root, files) {
|
|
608
|
+
await Promise.all(files.map((file) => this.fs.rm(this.fs.join(root, file), {
|
|
609
|
+
force: true,
|
|
610
|
+
recursive: true
|
|
611
|
+
})));
|
|
494
612
|
}
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
//#endregion
|
|
616
|
+
//#region ../../src/cli/assets/apiHelloControllerTs.ts
|
|
617
|
+
const apiHelloControllerTs = () => `
|
|
618
|
+
import { t } from "alepha";
|
|
619
|
+
import { $action } from "alepha/server";
|
|
620
|
+
|
|
621
|
+
export class HelloController {
|
|
622
|
+
hello = $action({
|
|
623
|
+
path: "/hello",
|
|
624
|
+
schema: {
|
|
625
|
+
response: t.object({
|
|
626
|
+
message: t.string(),
|
|
627
|
+
}),
|
|
628
|
+
},
|
|
629
|
+
handler: () => ({
|
|
630
|
+
message: "Hello, Alepha!",
|
|
631
|
+
}),
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
`.trim();
|
|
635
|
+
|
|
636
|
+
//#endregion
|
|
637
|
+
//#region ../../src/cli/assets/apiIndexTs.ts
|
|
638
|
+
const apiIndexTs = (options = {}) => {
|
|
639
|
+
const { appName = "app" } = options;
|
|
640
|
+
return `
|
|
641
|
+
import { $module } from "alepha";
|
|
642
|
+
import { HelloController } from "./controllers/HelloController.ts";
|
|
643
|
+
|
|
644
|
+
export const ApiModule = $module({
|
|
645
|
+
name: "${appName}.api",
|
|
646
|
+
services: [HelloController],
|
|
647
|
+
});
|
|
648
|
+
`.trim();
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
//#endregion
|
|
652
|
+
//#region ../../src/cli/assets/biomeJson.ts
|
|
653
|
+
const biomeJson = () => `
|
|
654
|
+
{
|
|
655
|
+
"$schema": "https://biomejs.dev/schemas/latest/schema.json",
|
|
656
|
+
"vcs": {
|
|
657
|
+
"enabled": true,
|
|
658
|
+
"clientKind": "git"
|
|
659
|
+
},
|
|
660
|
+
"files": {
|
|
661
|
+
"ignoreUnknown": true,
|
|
662
|
+
"includes": ["**", "!node_modules", "!dist"]
|
|
663
|
+
},
|
|
664
|
+
"formatter": {
|
|
665
|
+
"enabled": true,
|
|
666
|
+
"useEditorconfig": true
|
|
667
|
+
},
|
|
668
|
+
"linter": {
|
|
669
|
+
"enabled": true,
|
|
670
|
+
"rules": {
|
|
671
|
+
"recommended": true
|
|
672
|
+
},
|
|
673
|
+
"domains": {
|
|
674
|
+
"react": "recommended"
|
|
675
|
+
}
|
|
676
|
+
},
|
|
677
|
+
"assist": {
|
|
678
|
+
"actions": {
|
|
679
|
+
"source": {
|
|
680
|
+
"organizeImports": "on"
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
`.trim();
|
|
686
|
+
|
|
687
|
+
//#endregion
|
|
688
|
+
//#region ../../src/cli/assets/claudeMd.ts
|
|
689
|
+
const claudeMd = (options = {}) => {
|
|
690
|
+
const { react = false, projectName = "my-app" } = options;
|
|
691
|
+
const reactSection = react ? `
|
|
692
|
+
## React & Frontend
|
|
693
|
+
|
|
694
|
+
### Pages with \`$page\`
|
|
695
|
+
\`\`\`tsx
|
|
696
|
+
import { $page } from "@alepha/react/router";
|
|
697
|
+
import { $client } from "alepha/server/links";
|
|
698
|
+
import type { UserController } from "./UserController.ts";
|
|
699
|
+
|
|
700
|
+
class AppRouter {
|
|
701
|
+
api = $client<UserController>();
|
|
702
|
+
|
|
703
|
+
users = $page({
|
|
704
|
+
path: "/users",
|
|
705
|
+
loader: async () => ({ users: await this.api.listUsers() }),
|
|
706
|
+
component: ({ users }) => (
|
|
707
|
+
<ul>{users.map(u => <li key={u.id}>{u.email}</li>)}</ul>
|
|
708
|
+
),
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
userDetail = $page({
|
|
712
|
+
path: "/users/:id",
|
|
713
|
+
schema: { params: t.object({ id: t.uuid() }) },
|
|
714
|
+
loader: async ({ params }) => ({ user: await this.api.getUser({ params }) }),
|
|
715
|
+
lazy: () => import("./UserDetail.tsx"), // Code splitting
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
\`\`\`
|
|
719
|
+
|
|
720
|
+
### React Hooks
|
|
721
|
+
\`\`\`typescript
|
|
722
|
+
import { useAlepha, useClient, useStore, useAction, useInject } from "@alepha/react";
|
|
723
|
+
import { useRouter, useActive } from "@alepha/react/router";
|
|
724
|
+
import { useForm } from "@alepha/react/form";
|
|
725
|
+
\`\`\`
|
|
726
|
+
|
|
727
|
+
- \`useClient<Controller>()\` - Type-safe API calls
|
|
728
|
+
- \`useStore(atom)\` - Global state (returns \`[value, setValue]\`)
|
|
729
|
+
- \`useAction({ handler })\` - Async operations with loading/error state
|
|
730
|
+
- \`useRouter<AppRouter>()\` - Type-safe navigation
|
|
731
|
+
- \`useForm({ schema, handler })\` - Type-safe forms with validation
|
|
732
|
+
` : "";
|
|
733
|
+
const projectStructure = react ? `
|
|
734
|
+
\`\`\`
|
|
735
|
+
${projectName}/
|
|
736
|
+
├── src/
|
|
737
|
+
│ ├── api/ # Backend
|
|
738
|
+
│ │ ├── controllers/ # API controllers with $action
|
|
739
|
+
│ │ ├── services/ # Business logic
|
|
740
|
+
│ │ ├── entities/ # Database entities with $entity
|
|
741
|
+
│ │ ├── providers/ # External service wrappers
|
|
742
|
+
│ │ └── index.ts # API module definition with $module
|
|
743
|
+
│ ├── web/ # Frontend (React only)
|
|
744
|
+
│ │ ├── components/ # React components
|
|
745
|
+
│ │ ├── atoms/ # State atoms with $atom
|
|
746
|
+
│ │ ├── AppRouter.ts # Routes with $page
|
|
747
|
+
│ │ └── index.ts # Web module definition with $module
|
|
748
|
+
│ ├── main.server.ts # Server entry
|
|
749
|
+
│ ├── main.browser.ts # Browser entry (React only)
|
|
750
|
+
│ └── main.css # CSS entry (React only)
|
|
751
|
+
├── package.json
|
|
752
|
+
└── tsconfig.json
|
|
753
|
+
\`\`\`
|
|
754
|
+
` : `
|
|
755
|
+
\`\`\`
|
|
756
|
+
${projectName}/
|
|
757
|
+
├── src/
|
|
758
|
+
│ ├── api/ # Backend
|
|
759
|
+
│ │ ├── controllers/ # API controllers with $action
|
|
760
|
+
│ │ ├── services/ # Business logic
|
|
761
|
+
│ │ ├── entities/ # Database entities with $entity
|
|
762
|
+
│ │ ├── providers/ # External service wrappers
|
|
763
|
+
│ │ └── index.ts # API module definition with $module
|
|
764
|
+
│ └── main.server.ts # Server entry (always use main.server.ts)
|
|
765
|
+
├── package.json
|
|
766
|
+
└── tsconfig.json
|
|
767
|
+
\`\`\`
|
|
768
|
+
`;
|
|
769
|
+
return `# CLAUDE.md
|
|
770
|
+
|
|
771
|
+
This file provides guidance to Claude Code when working with this Alepha project.
|
|
772
|
+
|
|
773
|
+
## Overview
|
|
774
|
+
|
|
775
|
+
This is an **Alepha** project - a convention-driven TypeScript framework for type-safe full-stack applications.
|
|
776
|
+
|
|
777
|
+
**Key Concepts:**
|
|
778
|
+
- **Primitives**: Features defined with \`$\`-prefixed functions (\`$action\`, \`$entity\`, \`$page\`)
|
|
779
|
+
- **Class-Based**: Services are classes, primitives are class properties
|
|
780
|
+
- **Zero-Config**: Code structure IS the configuration
|
|
781
|
+
- **End-to-End Types**: Types flow from database → API → ${react ? "React" : "client"}
|
|
782
|
+
|
|
783
|
+
## Rules
|
|
784
|
+
|
|
785
|
+
- Use TypeScript strict mode
|
|
786
|
+
- Use Biome for formatting (\`alepha lint\`)
|
|
787
|
+
- Use Vitest for testing
|
|
788
|
+
- One file = one class
|
|
789
|
+
- Primitives are class properties (except \`$entity\`, \`$atom\`)
|
|
790
|
+
- No decorators, no Express/Fastify patterns
|
|
791
|
+
- No manual instantiation - use dependency injection
|
|
792
|
+
- Use \`protected\` instead of \`private\` for class members
|
|
793
|
+
- Import with file extensions: \`import { User } from "./User.ts"\`
|
|
794
|
+
- Use \`t\` from Alepha for schemas (not Zod)
|
|
795
|
+
- Prefer \`t.text()\` over \`t.string()\` for user input (has default max length, auto-trim, supports lowercase option)
|
|
796
|
+
|
|
797
|
+
## Project Structure
|
|
798
|
+
${projectStructure}
|
|
799
|
+
## Core Primitives
|
|
800
|
+
|
|
801
|
+
### API with \`$action\`
|
|
802
|
+
\`\`\`typescript
|
|
803
|
+
import { t } from "alepha";
|
|
804
|
+
import { $action } from "alepha/server";
|
|
805
|
+
|
|
806
|
+
class UserController {
|
|
807
|
+
getUser = $action({
|
|
808
|
+
path: "/users/:id", // → GET /api/users/:id
|
|
809
|
+
schema: {
|
|
810
|
+
params: t.object({ id: t.uuid() }),
|
|
811
|
+
response: t.object({ id: t.uuid(), email: t.email() }),
|
|
812
|
+
},
|
|
813
|
+
handler: async ({ params }) => this.userRepo.findById(params.id),
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
createUser = $action({
|
|
817
|
+
// POST inferred from body schema
|
|
818
|
+
schema: {
|
|
819
|
+
body: t.object({ email: t.email() }),
|
|
820
|
+
response: userEntity.schema,
|
|
821
|
+
},
|
|
822
|
+
handler: async ({ body }) => this.userRepo.create(body),
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
\`\`\`
|
|
826
|
+
|
|
827
|
+
### Database with \`$entity\` and \`$repository\`
|
|
828
|
+
\`\`\`typescript
|
|
829
|
+
import { $entity, $repository, db } from "alepha/orm";
|
|
830
|
+
|
|
831
|
+
// Entity defined at module level (for drizzle-kit compatibility)
|
|
832
|
+
export const userEntity = $entity({
|
|
833
|
+
name: "users",
|
|
834
|
+
schema: t.object({
|
|
835
|
+
id: db.primaryKey(),
|
|
836
|
+
email: t.email(),
|
|
837
|
+
createdAt: db.createdAt(),
|
|
838
|
+
updatedAt: db.updatedAt(),
|
|
839
|
+
}),
|
|
840
|
+
indexes: [{ column: "email", unique: true }],
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
class UserService {
|
|
844
|
+
repo = $repository(userEntity);
|
|
845
|
+
|
|
846
|
+
async findById(id: string) {
|
|
847
|
+
return this.repo.findById(id);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
\`\`\`
|
|
851
|
+
|
|
852
|
+
### Dependency Injection
|
|
853
|
+
\`\`\`typescript
|
|
854
|
+
import { $inject } from "alepha";
|
|
855
|
+
|
|
856
|
+
class OrderService {
|
|
857
|
+
userService = $inject(UserService); // Within same module
|
|
858
|
+
|
|
859
|
+
async createOrder(userId: string) {
|
|
860
|
+
const user = await this.userService.findById(userId);
|
|
861
|
+
// ...
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Cross-module: use $client instead of $inject
|
|
866
|
+
class AppRouter {
|
|
867
|
+
api = $client<OrderController>(); // Type-safe HTTP client
|
|
868
|
+
}
|
|
869
|
+
\`\`\`
|
|
870
|
+
|
|
871
|
+
### Modules with \`$module\`
|
|
872
|
+
\`\`\`typescript
|
|
873
|
+
// src/api/index.ts - Groups all API services
|
|
874
|
+
import { $module } from "alepha";
|
|
875
|
+
|
|
876
|
+
export const ApiModule = $module({
|
|
877
|
+
name: "app.api",
|
|
878
|
+
services: [
|
|
879
|
+
UserController,
|
|
880
|
+
OrderController,
|
|
881
|
+
UserService,
|
|
882
|
+
],
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
// src/web/index.ts - Groups all web services (React only)
|
|
886
|
+
export const WebModule = $module({
|
|
887
|
+
name: "app.web",
|
|
888
|
+
services: [AppRouter, Toaster],
|
|
889
|
+
register(alepha) {
|
|
890
|
+
// Optional: configure additional services
|
|
891
|
+
alepha.with(SomeLibrary);
|
|
892
|
+
},
|
|
893
|
+
});
|
|
894
|
+
\`\`\`
|
|
895
|
+
|
|
896
|
+
### Environment Variables
|
|
897
|
+
\`\`\`typescript
|
|
898
|
+
import { $env, t } from "alepha";
|
|
899
|
+
|
|
900
|
+
class AppConfig {
|
|
901
|
+
env = $env(t.object({
|
|
902
|
+
DATABASE_URL: t.string(),
|
|
903
|
+
API_KEY: t.optional(t.string()),
|
|
904
|
+
}));
|
|
905
|
+
}
|
|
906
|
+
\`\`\`
|
|
907
|
+
${reactSection}
|
|
908
|
+
## Quick Reference
|
|
909
|
+
|
|
910
|
+
| Primitive | Import | Purpose |
|
|
911
|
+
|-----------|--------|---------|
|
|
912
|
+
| \`$inject\` | \`alepha\` | Dependency injection |
|
|
913
|
+
| \`$env\` | \`alepha\` | Environment variables |
|
|
914
|
+
| \`$hook\` | \`alepha\` | Lifecycle hooks |
|
|
915
|
+
| \`$logger\` | \`alepha/logger\` | Structured logging |
|
|
916
|
+
| \`$action\` | \`alepha/server\` | REST API endpoints |
|
|
917
|
+
| \`$route\` | \`alepha/server\` | Low-level HTTP routes |
|
|
918
|
+
| \`$entity\` | \`alepha/orm\` | Database tables |
|
|
919
|
+
| \`$repository\` | \`alepha/orm\` | Type-safe data access |
|
|
920
|
+
| \`$queue\` | \`alepha/queue\` | Background jobs |
|
|
921
|
+
| \`$scheduler\` | \`alepha/scheduler\` | Cron tasks |
|
|
922
|
+
| \`$cache\` | \`alepha/cache\` | Cached computations |
|
|
923
|
+
| \`$bucket\` | \`alepha/bucket\` | File storage |
|
|
924
|
+
| \`$issuer\` | \`alepha/security\` | JWT tokens |
|
|
925
|
+
| \`$command\` | \`alepha/command\` | CLI commands |${react ? `
|
|
926
|
+
| \`$page\` | \`@alepha/react/router\` | React pages with SSR |
|
|
927
|
+
| \`$atom\` | \`alepha\` | Global state |` : ""}
|
|
928
|
+
|
|
929
|
+
## Testing
|
|
930
|
+
|
|
931
|
+
\`\`\`typescript
|
|
932
|
+
import { describe, it, expect } from "vitest";
|
|
933
|
+
import { Alepha } from "alepha";
|
|
934
|
+
|
|
935
|
+
describe("UserService", () => {
|
|
936
|
+
it("should create user", async () => {
|
|
937
|
+
const alepha = Alepha.create().with(UserService);
|
|
938
|
+
const service = alepha.inject(UserService);
|
|
939
|
+
|
|
940
|
+
const user = await service.create({ email: "test@example.com" });
|
|
941
|
+
expect(user.email).toBe("test@example.com");
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
it("should mock dependencies", async () => {
|
|
945
|
+
const alepha = Alepha.create()
|
|
946
|
+
.with(OrderService)
|
|
947
|
+
.with({ provide: PaymentGateway, use: MockPaymentGateway });
|
|
948
|
+
|
|
949
|
+
const service = alepha.inject(OrderService);
|
|
950
|
+
// PaymentGateway is now mocked
|
|
951
|
+
});
|
|
952
|
+
});
|
|
953
|
+
\`\`\`
|
|
954
|
+
|
|
955
|
+
## Common Mistakes
|
|
956
|
+
|
|
957
|
+
1. **DON'T use decorators** - Use primitives (\`$action\`, not \`@Get()\`)
|
|
958
|
+
2. **DON'T use Zod** - Use TypeBox via \`t\` from Alepha
|
|
959
|
+
3. **DON'T use Express patterns** - No \`app.get()\`, \`router.use()\`
|
|
960
|
+
4. **DON'T inject across modules** - Use \`$client\` for cross-module calls
|
|
961
|
+
5. **DON'T use async constructors** - Use \`$hook({ on: "start" })\`
|
|
962
|
+
6. **DON'T instantiate manually** - Let DI container manage instances
|
|
963
|
+
|
|
964
|
+
## After Code Changes
|
|
965
|
+
|
|
966
|
+
Always run:
|
|
967
|
+
\`\`\`bash
|
|
968
|
+
alepha lint # Format and lint
|
|
969
|
+
alepha typecheck # Type checking
|
|
970
|
+
alepha test # Run tests (if applicable)
|
|
971
|
+
alepha build # Build the project
|
|
972
|
+
\`\`\`
|
|
973
|
+
|
|
974
|
+
## Documentation
|
|
975
|
+
|
|
976
|
+
- Full docs: https://alepha.dev/llms.txt
|
|
977
|
+
- Detailed docs: https://alepha.dev/llms-full.txt
|
|
978
|
+
`.trim();
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
//#endregion
|
|
982
|
+
//#region ../../src/cli/assets/dummySpecTs.ts
|
|
983
|
+
const dummySpecTs = () => `
|
|
984
|
+
import { test, expect } from "vitest";
|
|
985
|
+
|
|
986
|
+
test("dummy test", () => {
|
|
987
|
+
expect(1 + 1).toBe(2);
|
|
988
|
+
});
|
|
989
|
+
`.trim();
|
|
990
|
+
|
|
991
|
+
//#endregion
|
|
992
|
+
//#region ../../src/cli/assets/editorconfig.ts
|
|
993
|
+
const editorconfig = () => `
|
|
994
|
+
# https://editorconfig.org
|
|
995
|
+
|
|
996
|
+
root = true
|
|
997
|
+
|
|
998
|
+
[*]
|
|
999
|
+
charset = utf-8
|
|
1000
|
+
end_of_line = lf
|
|
1001
|
+
insert_final_newline = true
|
|
1002
|
+
trim_trailing_whitespace = true
|
|
1003
|
+
indent_style = space
|
|
1004
|
+
indent_size = 2
|
|
1005
|
+
`.trim();
|
|
1006
|
+
|
|
1007
|
+
//#endregion
|
|
1008
|
+
//#region ../../src/cli/assets/mainBrowserTs.ts
|
|
1009
|
+
const mainBrowserTs = () => `
|
|
1010
|
+
import { Alepha, run } from "alepha";
|
|
1011
|
+
import { WebModule } from "./web/index.ts";
|
|
1012
|
+
|
|
1013
|
+
const alepha = Alepha.create();
|
|
1014
|
+
|
|
1015
|
+
alepha.with(WebModule);
|
|
1016
|
+
|
|
1017
|
+
run(alepha);
|
|
1018
|
+
`.trim();
|
|
1019
|
+
|
|
1020
|
+
//#endregion
|
|
1021
|
+
//#region ../../src/cli/assets/mainCss.ts
|
|
1022
|
+
const mainCss = () => `
|
|
1023
|
+
* {
|
|
1024
|
+
box-sizing: border-box;
|
|
1025
|
+
margin: 0;
|
|
1026
|
+
padding: 0;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
html,
|
|
1030
|
+
body {
|
|
1031
|
+
height: 100%;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
body {
|
|
1035
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
1036
|
+
"Helvetica Neue", Arial, sans-serif;
|
|
1037
|
+
line-height: 1.5;
|
|
1038
|
+
-webkit-font-smoothing: antialiased;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
#root {
|
|
1042
|
+
height: 100%;
|
|
1043
|
+
}
|
|
1044
|
+
`.trim();
|
|
1045
|
+
|
|
1046
|
+
//#endregion
|
|
1047
|
+
//#region ../../src/cli/assets/mainServerTs.ts
|
|
1048
|
+
const mainServerTs = (options = {}) => {
|
|
1049
|
+
const { react = false } = options;
|
|
1050
|
+
return `
|
|
1051
|
+
import { Alepha, run } from "alepha";
|
|
1052
|
+
import { ApiModule } from "./api/index.ts";
|
|
1053
|
+
${react ? `import { WebModule } from "./web/index.ts";\n` : ""}
|
|
1054
|
+
const alepha = Alepha.create();
|
|
1055
|
+
|
|
1056
|
+
alepha.with(ApiModule);
|
|
1057
|
+
${react ? `alepha.with(WebModule);\n` : ""}
|
|
1058
|
+
run(alepha);
|
|
1059
|
+
`.trim();
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
//#endregion
|
|
1063
|
+
//#region ../../src/cli/assets/tsconfigJson.ts
|
|
1064
|
+
const tsconfigJson = () => `
|
|
1065
|
+
{
|
|
1066
|
+
"extends": "alepha/tsconfig.base"
|
|
1067
|
+
}
|
|
1068
|
+
`.trim();
|
|
1069
|
+
|
|
1070
|
+
//#endregion
|
|
1071
|
+
//#region ../../src/cli/assets/webAppRouterTs.ts
|
|
1072
|
+
const webAppRouterTs = () => `
|
|
1073
|
+
import { $page } from "@alepha/react/router";
|
|
1074
|
+
import { $client } from "alepha/server/links";
|
|
1075
|
+
import type { HelloController } from "../api/controllers/HelloController.ts";
|
|
1076
|
+
|
|
1077
|
+
export class AppRouter {
|
|
1078
|
+
api = $client<HelloController>();
|
|
1079
|
+
|
|
1080
|
+
home = $page({
|
|
1081
|
+
path: "/",
|
|
1082
|
+
lazy: () => import("./components/Hello.tsx"),
|
|
1083
|
+
loader: () => this.api.hello(),
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
`.trim();
|
|
1087
|
+
|
|
1088
|
+
//#endregion
|
|
1089
|
+
//#region ../../src/cli/assets/webHelloComponentTsx.ts
|
|
1090
|
+
const webHelloComponentTsx = () => `import { useState } from "react";
|
|
1091
|
+
|
|
1092
|
+
interface Props {
|
|
1093
|
+
message: string;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const Hello = (props: Props) => {
|
|
1097
|
+
const [message, setMessage] = useState(props.message);
|
|
1098
|
+
return (
|
|
1099
|
+
<div>
|
|
1100
|
+
<h1>{message}</h1>
|
|
1101
|
+
<input value={message} onChange={e => setMessage(e.target.value)} />
|
|
1102
|
+
<p>Edit this component in src/web/components/Hello.tsx</p>
|
|
1103
|
+
</div>
|
|
1104
|
+
);
|
|
1105
|
+
};
|
|
1106
|
+
|
|
1107
|
+
export default Hello;
|
|
1108
|
+
`.trim();
|
|
1109
|
+
|
|
1110
|
+
//#endregion
|
|
1111
|
+
//#region ../../src/cli/assets/webIndexTs.ts
|
|
1112
|
+
const webIndexTs = (options = {}) => {
|
|
1113
|
+
const { appName = "app" } = options;
|
|
1114
|
+
return `
|
|
1115
|
+
import { $module } from "alepha";
|
|
1116
|
+
import { AppRouter } from "./AppRouter.ts";
|
|
1117
|
+
|
|
1118
|
+
export const WebModule = $module({
|
|
1119
|
+
name: "${appName}.web",
|
|
1120
|
+
services: [AppRouter],
|
|
1121
|
+
});
|
|
1122
|
+
`.trim();
|
|
1123
|
+
};
|
|
1124
|
+
|
|
1125
|
+
//#endregion
|
|
1126
|
+
//#region ../../src/cli/services/ProjectScaffolder.ts
|
|
1127
|
+
/**
|
|
1128
|
+
* Service for scaffolding new Alepha projects.
|
|
1129
|
+
*
|
|
1130
|
+
* Handles creation of:
|
|
1131
|
+
* - Project structure (src/api, src/web)
|
|
1132
|
+
* - Configuration files (tsconfig, biome, editorconfig)
|
|
1133
|
+
* - Entry points (main.server.ts, main.browser.ts)
|
|
1134
|
+
* - Example code (HelloController, Hello component)
|
|
1135
|
+
*/
|
|
1136
|
+
var ProjectScaffolder = class {
|
|
1137
|
+
log = $logger();
|
|
1138
|
+
fs = $inject(FileSystemProvider);
|
|
1139
|
+
pm = $inject(PackageManagerUtils);
|
|
495
1140
|
/**
|
|
496
|
-
* Get the
|
|
1141
|
+
* Get the app name from the directory name.
|
|
497
1142
|
*
|
|
498
|
-
*
|
|
1143
|
+
* Converts the directory name to a valid module name:
|
|
1144
|
+
* - Converts to lowercase
|
|
1145
|
+
* - Replaces spaces, dashes, underscores with nothing
|
|
1146
|
+
* - Falls back to "app" if empty
|
|
499
1147
|
*/
|
|
500
|
-
|
|
501
|
-
|
|
1148
|
+
getAppName(root) {
|
|
1149
|
+
return basename(root).toLowerCase().replace(/[\s\-_]/g, "") || "app";
|
|
502
1150
|
}
|
|
503
1151
|
/**
|
|
504
|
-
* Ensure
|
|
505
|
-
*
|
|
506
|
-
* Creates a standard .editorconfig if none exists.
|
|
507
|
-
*
|
|
508
|
-
* @param root - The root directory of the project
|
|
1152
|
+
* Ensure all configuration files exist.
|
|
509
1153
|
*/
|
|
510
|
-
async
|
|
511
|
-
|
|
1154
|
+
async ensureConfig(root, opts) {
|
|
1155
|
+
const tasks = [];
|
|
1156
|
+
const force = opts.force ?? false;
|
|
1157
|
+
if (opts.packageJson) tasks.push(this.pm.ensurePackageJson(root, typeof opts.packageJson === "boolean" ? {} : opts.packageJson).then(() => {}));
|
|
1158
|
+
if (opts.tsconfigJson) tasks.push(this.ensureTsConfig(root, { force }));
|
|
1159
|
+
if (opts.indexHtml) tasks.push(this.ensureReactProject(root, { force }));
|
|
1160
|
+
if (opts.biomeJson) tasks.push(this.ensureBiomeConfig(root, { force }));
|
|
1161
|
+
if (opts.editorconfig) tasks.push(this.ensureEditorConfig(root, { force }));
|
|
1162
|
+
if (opts.claudeMd) tasks.push(this.ensureClaudeMd(root, typeof opts.claudeMd === "boolean" ? { force } : {
|
|
1163
|
+
...opts.claudeMd,
|
|
1164
|
+
force
|
|
1165
|
+
}));
|
|
1166
|
+
await Promise.all(tasks);
|
|
512
1167
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
* Dynamically imports the server entry file and extracts the Alepha instance.
|
|
517
|
-
* Skips the automatic start process.
|
|
518
|
-
*
|
|
519
|
-
* @param rootDir - The root directory of the project
|
|
520
|
-
* @param explicitEntry - Optional explicit path to the entry file
|
|
521
|
-
* @returns Object containing the Alepha instance and the entry file path
|
|
522
|
-
* @throws {AlephaError} If the Alepha instance cannot be found
|
|
523
|
-
*/
|
|
524
|
-
async loadAlephaFromServerEntryFile(rootDir, explicitEntry) {
|
|
525
|
-
process.env.ALEPHA_CLI_IMPORT = "true";
|
|
526
|
-
const entry = await boot.getServerEntry(rootDir, explicitEntry);
|
|
527
|
-
delete global.__alepha;
|
|
528
|
-
const mod = await import(entry);
|
|
529
|
-
this.log.debug(`Load entry: ${entry}`);
|
|
530
|
-
if (mod.default instanceof Alepha) return {
|
|
531
|
-
alepha: mod.default,
|
|
532
|
-
entry
|
|
533
|
-
};
|
|
534
|
-
const g = global;
|
|
535
|
-
if (g.__alepha) return {
|
|
536
|
-
alepha: g.__alepha,
|
|
537
|
-
entry
|
|
538
|
-
};
|
|
539
|
-
throw new AlephaError(`Could not find Alepha instance in entry file: ${entry}`);
|
|
1168
|
+
async ensureTsConfig(root, opts = {}) {
|
|
1169
|
+
if (!opts.force && await this.existsInParents(root, "tsconfig.json")) return;
|
|
1170
|
+
await this.fs.writeFile(this.fs.join(root, "tsconfig.json"), tsconfigJson());
|
|
540
1171
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
* @param models - Array of model names to export
|
|
550
|
-
* @returns JavaScript code as a string
|
|
551
|
-
*/
|
|
552
|
-
generateEntitiesJs(entry, provider, models = []) {
|
|
553
|
-
return `
|
|
554
|
-
import "${entry}";
|
|
555
|
-
import { DrizzleKitProvider, Repository } from "alepha/orm";
|
|
556
|
-
|
|
557
|
-
const alepha = globalThis.__alepha;
|
|
558
|
-
const kit = alepha.inject(DrizzleKitProvider);
|
|
559
|
-
const provider = alepha.services(Repository).find((it) => it.provider.name === "${provider}").provider;
|
|
560
|
-
const models = kit.getModels(provider);
|
|
561
|
-
|
|
562
|
-
${models.map((it) => `export const ${it} = models["${it}"];`).join("\n")}
|
|
563
|
-
|
|
564
|
-
`.trim();
|
|
1172
|
+
async ensureBiomeConfig(root, opts = {}) {
|
|
1173
|
+
await this.ensureFile(root, "biome.json", biomeJson(), opts.force);
|
|
1174
|
+
}
|
|
1175
|
+
async ensureEditorConfig(root, opts = {}) {
|
|
1176
|
+
await this.ensureFile(root, ".editorconfig", editorconfig(), opts.force);
|
|
1177
|
+
}
|
|
1178
|
+
async ensureClaudeMd(root, options = {}) {
|
|
1179
|
+
await this.ensureFile(root, "CLAUDE.md", claudeMd(options), options.force);
|
|
565
1180
|
}
|
|
566
1181
|
/**
|
|
567
|
-
*
|
|
1182
|
+
* Ensure src/main.server.ts exists with full API structure.
|
|
568
1183
|
*
|
|
569
|
-
*
|
|
570
|
-
*
|
|
1184
|
+
* Creates:
|
|
1185
|
+
* - src/main.server.ts (entry point)
|
|
1186
|
+
* - src/api/index.ts (API module)
|
|
1187
|
+
* - src/api/controllers/HelloController.ts (example controller)
|
|
571
1188
|
*/
|
|
572
|
-
async
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
if (await this.checkFileExists(root, "pnpm-lock.yaml", true)) return "pnpm";
|
|
583
|
-
return "npm";
|
|
584
|
-
}
|
|
585
|
-
async ensureIndexHtml(root) {
|
|
586
|
-
if (await this.fs.exists(join(root, "index.html"))) return;
|
|
587
|
-
const serverEntry = "src/main.server.ts";
|
|
588
|
-
const browserEntry = "src/main.browser.ts";
|
|
589
|
-
const appRouter = "src/AppRouter.ts";
|
|
590
|
-
await this.fs.writeFile(join(root, "index.html"), indexHtml(browserEntry));
|
|
591
|
-
try {
|
|
592
|
-
await this.fs.mkdir(join(root, "src"), { recursive: true });
|
|
593
|
-
} catch {}
|
|
594
|
-
if (!await this.fs.exists(join(root, browserEntry))) await this.fs.writeFile(join(root, browserEntry), mainBrowserTs());
|
|
595
|
-
if (!await this.fs.exists(join(root, serverEntry))) await this.fs.writeFile(join(root, serverEntry), mainBrowserTs());
|
|
596
|
-
if (!await this.fs.exists(join(root, appRouter))) await this.fs.writeFile(join(root, appRouter), appRouterTs());
|
|
597
|
-
}
|
|
598
|
-
async exists(root, dirName) {
|
|
599
|
-
return this.fs.exists(join(root, dirName));
|
|
1189
|
+
async ensureApiProject(root, opts = {}) {
|
|
1190
|
+
const srcDir = this.fs.join(root, "src");
|
|
1191
|
+
if (!opts.force && await this.fs.exists(srcDir)) {
|
|
1192
|
+
if ((await this.fs.ls(srcDir)).length > 0) return;
|
|
1193
|
+
}
|
|
1194
|
+
const appName = this.getAppName(root);
|
|
1195
|
+
await this.fs.mkdir(this.fs.join(root, "src/api/controllers"), { recursive: true });
|
|
1196
|
+
await this.ensureFile(srcDir, "main.server.ts", mainServerTs(), opts.force);
|
|
1197
|
+
await this.ensureFile(srcDir, "api/index.ts", apiIndexTs({ appName }), opts.force);
|
|
1198
|
+
await this.ensureFile(srcDir, "api/controllers/HelloController.ts", apiHelloControllerTs(), opts.force);
|
|
600
1199
|
}
|
|
601
1200
|
/**
|
|
602
|
-
* Ensure
|
|
1201
|
+
* Ensure full React project structure exists.
|
|
603
1202
|
*
|
|
604
|
-
* Creates
|
|
605
|
-
*
|
|
606
|
-
*
|
|
607
|
-
*
|
|
1203
|
+
* Creates:
|
|
1204
|
+
* - src/main.server.ts, src/main.browser.ts
|
|
1205
|
+
* - src/api/index.ts, src/api/controllers/HelloController.ts
|
|
1206
|
+
* - src/web/index.ts, src/web/AppRouter.ts, src/web/components/Hello.tsx
|
|
608
1207
|
*/
|
|
609
|
-
async
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
}
|
|
617
|
-
|
|
1208
|
+
async ensureReactProject(root, opts = {}) {
|
|
1209
|
+
const appName = this.getAppName(root);
|
|
1210
|
+
await this.fs.mkdir(this.fs.join(root, "src/api/controllers"), { recursive: true });
|
|
1211
|
+
await this.fs.mkdir(this.fs.join(root, "src/web/components"), { recursive: true });
|
|
1212
|
+
await this.ensureFile(root, "src/main.css", mainCss(), opts.force);
|
|
1213
|
+
await this.ensureFile(root, "src/api/index.ts", apiIndexTs({ appName }), opts.force);
|
|
1214
|
+
await this.ensureFile(root, "src/api/controllers/HelloController.ts", apiHelloControllerTs(), opts.force);
|
|
1215
|
+
await this.ensureFile(root, "src/main.server.ts", mainServerTs({ react: true }), opts.force);
|
|
1216
|
+
await this.ensureFile(root, "src/web/index.ts", webIndexTs({ appName }), opts.force);
|
|
1217
|
+
await this.ensureFile(root, "src/web/AppRouter.ts", webAppRouterTs(), opts.force);
|
|
1218
|
+
await this.ensureFile(root, "src/web/components/Hello.tsx", webHelloComponentTsx(), opts.force);
|
|
1219
|
+
await this.ensureFile(root, "src/main.browser.ts", mainBrowserTs(), opts.force);
|
|
618
1220
|
}
|
|
619
1221
|
/**
|
|
620
1222
|
* Ensure test directory exists with a dummy test file.
|
|
621
|
-
*
|
|
622
|
-
* Creates the test directory and a dummy.spec.ts file if the test directory
|
|
623
|
-
* doesn't exist or is empty.
|
|
624
|
-
*
|
|
625
|
-
* @param root - The root directory of the project
|
|
626
1223
|
*/
|
|
627
1224
|
async ensureTestDir(root) {
|
|
628
|
-
const testDir = join(root, "test");
|
|
629
|
-
const dummyPath = join(testDir, "dummy.spec.ts");
|
|
1225
|
+
const testDir = this.fs.join(root, "test");
|
|
1226
|
+
const dummyPath = this.fs.join(testDir, "dummy.spec.ts");
|
|
630
1227
|
if (!await this.fs.exists(testDir)) {
|
|
631
1228
|
await this.fs.mkdir(testDir, { recursive: true });
|
|
632
1229
|
await this.fs.writeFile(dummyPath, dummySpecTs());
|
|
@@ -634,68 +1231,23 @@ ${models.map((it) => `export const ${it} = models["${it}"];`).join("\n")}
|
|
|
634
1231
|
}
|
|
635
1232
|
if ((await this.fs.ls(testDir)).length === 0) await this.fs.writeFile(dummyPath, dummySpecTs());
|
|
636
1233
|
}
|
|
637
|
-
async readPackageJson(root) {
|
|
638
|
-
const packageJson$1 = await this.fs.createFile({ path: this.fs.join(root, "package.json") }).text();
|
|
639
|
-
return JSON.parse(packageJson$1);
|
|
640
|
-
}
|
|
641
|
-
/**
|
|
642
|
-
* Check if a dependency is installed in the project.
|
|
643
|
-
*
|
|
644
|
-
* @param root - The root directory of the project
|
|
645
|
-
* @param packageName - The name of the package to check
|
|
646
|
-
* @returns True if the package is in dependencies or devDependencies
|
|
647
|
-
*/
|
|
648
|
-
async hasDependency(root, packageName) {
|
|
649
|
-
try {
|
|
650
|
-
const pkg = await this.readPackageJson(root);
|
|
651
|
-
return !!(pkg.dependencies?.[packageName] || pkg.devDependencies?.[packageName]);
|
|
652
|
-
} catch {
|
|
653
|
-
return false;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
1234
|
/**
|
|
657
|
-
*
|
|
658
|
-
*
|
|
659
|
-
* @param root - The root directory of the project
|
|
660
|
-
* @returns True if expo is in dependencies or devDependencies
|
|
1235
|
+
* Write a file, optionally overriding if it exists.
|
|
661
1236
|
*/
|
|
662
|
-
async
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
async getInstallCommand(root, packageName, dev = true) {
|
|
666
|
-
const pm = await this.getPackageManager(root);
|
|
667
|
-
let cmd;
|
|
668
|
-
switch (pm) {
|
|
669
|
-
case "yarn":
|
|
670
|
-
cmd = `yarn add ${dev ? "-D" : ""} ${packageName}`;
|
|
671
|
-
break;
|
|
672
|
-
case "pnpm":
|
|
673
|
-
cmd = `pnpm add ${dev ? "-D" : ""} ${packageName}`;
|
|
674
|
-
break;
|
|
675
|
-
case "bun":
|
|
676
|
-
cmd = `bun add ${dev ? "-d" : ""} ${packageName}`;
|
|
677
|
-
break;
|
|
678
|
-
default: cmd = `npm install ${dev ? "--save-dev" : ""} ${packageName}`;
|
|
679
|
-
}
|
|
680
|
-
return cmd.replace(/\s+/g, " ").trim();
|
|
1237
|
+
async ensureFile(root, relativePath, content, force) {
|
|
1238
|
+
const fullPath = this.fs.join(root, relativePath);
|
|
1239
|
+
if (force || !await this.fs.exists(fullPath)) await this.fs.writeFile(fullPath, content);
|
|
681
1240
|
}
|
|
682
1241
|
/**
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
* Automatically detects the package manager (yarn, pnpm, npm) and installs
|
|
686
|
-
* the package as a dev dependency if not already present.
|
|
1242
|
+
* Check if a file exists in the given directory or any parent directory.
|
|
687
1243
|
*/
|
|
688
|
-
async
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
this.
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
if (options.run) await options.run(cmd, { alias: `installing ${packageName}` });
|
|
696
|
-
else {
|
|
697
|
-
this.log.debug(`Installing ${packageName}`);
|
|
698
|
-
await this.exec(cmd, { global: true });
|
|
1244
|
+
async existsInParents(root, filename) {
|
|
1245
|
+
let current = root;
|
|
1246
|
+
while (true) {
|
|
1247
|
+
if (await this.fs.exists(this.fs.join(current, filename))) return true;
|
|
1248
|
+
const parent = dirname(current);
|
|
1249
|
+
if (parent === current) return false;
|
|
1250
|
+
current = parent;
|
|
699
1251
|
}
|
|
700
1252
|
}
|
|
701
1253
|
};
|
|
@@ -704,70 +1256,93 @@ ${models.map((it) => `export const ${it} = models["${it}"];`).join("\n")}
|
|
|
704
1256
|
//#region ../../src/cli/commands/build.ts
|
|
705
1257
|
var BuildCommand = class {
|
|
706
1258
|
log = $logger();
|
|
1259
|
+
fs = $inject(FileSystemProvider);
|
|
707
1260
|
utils = $inject(AlephaCliUtils);
|
|
1261
|
+
pm = $inject(PackageManagerUtils);
|
|
1262
|
+
scaffolder = $inject(ProjectScaffolder);
|
|
1263
|
+
boot = $inject(AppEntryProvider);
|
|
1264
|
+
viteBuildProvider = $inject(ViteBuildProvider);
|
|
708
1265
|
options = $use(buildOptions);
|
|
709
1266
|
build = $command({
|
|
710
1267
|
name: "build",
|
|
711
1268
|
mode: "production",
|
|
712
1269
|
description: "Build the project for production",
|
|
713
|
-
args: t.optional(t.text({
|
|
714
|
-
title: "path",
|
|
715
|
-
description: "Filepath to build"
|
|
716
|
-
})),
|
|
717
1270
|
flags: t.object({
|
|
718
1271
|
stats: t.optional(t.boolean({ description: "Generate build stats report" })),
|
|
719
1272
|
vercel: t.optional(t.boolean({ description: "Generate Vercel deployment configuration" })),
|
|
720
1273
|
cloudflare: t.optional(t.boolean({ description: "Generate Cloudflare Workers configuration" })),
|
|
721
1274
|
docker: t.optional(t.boolean({ description: "Generate Docker configuration" })),
|
|
722
|
-
sitemap: t.optional(t.text({ description: "Generate sitemap.xml with base URL" }))
|
|
1275
|
+
sitemap: t.optional(t.text({ description: "Generate sitemap.xml with base URL" })),
|
|
1276
|
+
bun: t.optional(t.boolean({ description: "Prioritize .bun.ts entry files for Bun runtime" }))
|
|
723
1277
|
}),
|
|
724
|
-
handler: async ({ flags,
|
|
725
|
-
process.env.ALEPHA_BUILD_MODE = "cli";
|
|
1278
|
+
handler: async ({ flags, run, root }) => {
|
|
726
1279
|
process.env.NODE_ENV = "production";
|
|
727
|
-
if (await this.
|
|
728
|
-
await this.
|
|
729
|
-
const entry = await boot.
|
|
1280
|
+
if (await this.pm.hasExpo(root)) return;
|
|
1281
|
+
await this.scaffolder.ensureConfig(root, { tsconfigJson: true });
|
|
1282
|
+
const entry = await this.boot.getAppEntry(root);
|
|
730
1283
|
this.log.trace("Entry file found", { entry });
|
|
731
1284
|
const distDir = "dist";
|
|
732
|
-
const
|
|
733
|
-
await this.
|
|
1285
|
+
const publicDir = "public";
|
|
1286
|
+
await this.pm.ensureDependency(root, "vite", {
|
|
1287
|
+
run,
|
|
1288
|
+
exec: (cmd, opts) => this.utils.exec(cmd, opts)
|
|
1289
|
+
});
|
|
734
1290
|
await run.rm("dist", { alias: "clean dist" });
|
|
735
1291
|
const options = this.options;
|
|
736
1292
|
await this.utils.loadEnv(root, [".env", ".env.production"]);
|
|
737
1293
|
const stats = flags.stats ?? options.stats ?? false;
|
|
1294
|
+
let template = "";
|
|
738
1295
|
let hasClient = false;
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
dist: `${distDir}/${clientDir}`,
|
|
748
|
-
stats,
|
|
749
|
-
precompress: true
|
|
750
|
-
})
|
|
1296
|
+
let alepha;
|
|
1297
|
+
await run({
|
|
1298
|
+
name: "analyze app",
|
|
1299
|
+
handler: async () => {
|
|
1300
|
+
alepha = await this.viteBuildProvider.init({ entry });
|
|
1301
|
+
hasClient = this.viteBuildProvider.hasClient();
|
|
1302
|
+
if (hasClient) template = this.viteBuildProvider.generateIndexHtml();
|
|
1303
|
+
}
|
|
751
1304
|
});
|
|
1305
|
+
if (!alepha) throw new AlephaError("Alepha instance not found");
|
|
1306
|
+
if (hasClient) {
|
|
1307
|
+
const indexHtmlPath = this.fs.join(root, "index.html");
|
|
1308
|
+
await this.fs.writeFile(indexHtmlPath, template);
|
|
1309
|
+
try {
|
|
1310
|
+
await run({
|
|
1311
|
+
name: "vite build client",
|
|
1312
|
+
handler: () => buildClient({
|
|
1313
|
+
silent: true,
|
|
1314
|
+
dist: `${distDir}/${publicDir}`,
|
|
1315
|
+
stats,
|
|
1316
|
+
precompress: true
|
|
1317
|
+
})
|
|
1318
|
+
});
|
|
1319
|
+
} finally {
|
|
1320
|
+
await this.fs.rm(indexHtmlPath);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
752
1323
|
await run({
|
|
753
1324
|
name: "vite build server",
|
|
754
1325
|
handler: async () => {
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
1326
|
+
if (!alepha) throw new AlephaError("Alepha instance not found");
|
|
1327
|
+
const clientIndexPath = `${distDir}/${publicDir}/index.html`;
|
|
1328
|
+
const clientBuilt = await this.fs.exists(clientIndexPath);
|
|
1329
|
+
const conditions = [];
|
|
1330
|
+
if (flags.bun) conditions.push("bun");
|
|
1331
|
+
if (options.cloudflare) conditions.push("workerd");
|
|
760
1332
|
await buildServer({
|
|
761
1333
|
silent: true,
|
|
762
|
-
entry,
|
|
1334
|
+
entry: entry.server,
|
|
763
1335
|
distDir,
|
|
764
|
-
clientDir: clientBuilt ?
|
|
765
|
-
stats
|
|
1336
|
+
clientDir: clientBuilt ? publicDir : void 0,
|
|
1337
|
+
stats,
|
|
1338
|
+
conditions,
|
|
1339
|
+
alepha
|
|
766
1340
|
});
|
|
767
|
-
if (clientBuilt) await
|
|
1341
|
+
if (clientBuilt) await this.fs.rm(clientIndexPath);
|
|
768
1342
|
}
|
|
769
1343
|
});
|
|
770
1344
|
await copyAssets({
|
|
1345
|
+
alepha,
|
|
771
1346
|
root,
|
|
772
1347
|
entry: `${distDir}/index.js`,
|
|
773
1348
|
distDir,
|
|
@@ -775,31 +1350,24 @@ var BuildCommand = class {
|
|
|
775
1350
|
});
|
|
776
1351
|
if (hasClient) {
|
|
777
1352
|
const sitemapHostname = flags.sitemap ?? options.sitemap?.hostname;
|
|
778
|
-
if (sitemapHostname) await
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
baseUrl: sitemapHostname
|
|
784
|
-
}));
|
|
785
|
-
}
|
|
1353
|
+
if (sitemapHostname) await generateSitemap({
|
|
1354
|
+
alepha,
|
|
1355
|
+
baseUrl: sitemapHostname,
|
|
1356
|
+
output: `${distDir}/${publicDir}/sitemap.xml`,
|
|
1357
|
+
run
|
|
786
1358
|
});
|
|
787
|
-
await
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
entry: `${distDir}/index.js`,
|
|
793
|
-
compress: true
|
|
794
|
-
});
|
|
795
|
-
}
|
|
1359
|
+
await prerenderPages({
|
|
1360
|
+
alepha,
|
|
1361
|
+
dist: `${distDir}/${publicDir}`,
|
|
1362
|
+
compress: true,
|
|
1363
|
+
run
|
|
796
1364
|
});
|
|
797
1365
|
}
|
|
798
1366
|
if (flags.vercel || options.vercel) await run({
|
|
799
1367
|
name: "add Vercel config",
|
|
800
1368
|
handler: () => generateVercel({
|
|
801
1369
|
distDir,
|
|
802
|
-
clientDir,
|
|
1370
|
+
clientDir: publicDir,
|
|
803
1371
|
config: options.vercel
|
|
804
1372
|
})
|
|
805
1373
|
});
|
|
@@ -844,6 +1412,7 @@ const drizzleCommandFlags = t.object({
|
|
|
844
1412
|
});
|
|
845
1413
|
var DbCommand = class {
|
|
846
1414
|
log = $logger();
|
|
1415
|
+
fs = $inject(FileSystemProvider);
|
|
847
1416
|
utils = $inject(AlephaCliUtils);
|
|
848
1417
|
/**
|
|
849
1418
|
* Check if database migrations are up to date.
|
|
@@ -868,15 +1437,16 @@ var DbCommand = class {
|
|
|
868
1437
|
const providerName = provider.name;
|
|
869
1438
|
if (accepted.has(providerName)) continue;
|
|
870
1439
|
accepted.add(providerName);
|
|
871
|
-
const migrationDir = join(rootDir, "migrations", providerName);
|
|
872
|
-
const
|
|
873
|
-
if (!
|
|
1440
|
+
const migrationDir = this.fs.join(rootDir, "migrations", providerName);
|
|
1441
|
+
const journalBuffer = await this.fs.readFile(`${migrationDir}/meta/_journal.json`).catch(() => null);
|
|
1442
|
+
if (!journalBuffer) {
|
|
874
1443
|
this.log.info("No migration journal found.");
|
|
875
1444
|
return;
|
|
876
1445
|
}
|
|
877
|
-
const journal = JSON.parse(
|
|
1446
|
+
const journal = JSON.parse(journalBuffer.toString("utf-8"));
|
|
878
1447
|
const lastMigration = journal.entries[journal.entries.length - 1];
|
|
879
|
-
const
|
|
1448
|
+
const snapshotBuffer = await this.fs.readFile(`${migrationDir}/meta/${String(lastMigration.idx).padStart(4, "0")}_snapshot.json`);
|
|
1449
|
+
const lastSnapshot = JSON.parse(snapshotBuffer.toString("utf-8"));
|
|
880
1450
|
const models = drizzleKitProvider.getModels(provider);
|
|
881
1451
|
const kit = drizzleKitProvider.importDrizzleKit();
|
|
882
1452
|
const now = kit.generateDrizzleJson(models, lastSnapshot.id);
|
|
@@ -1030,6 +1600,7 @@ var DbCommand = class {
|
|
|
1030
1600
|
const provider = primitive.provider;
|
|
1031
1601
|
const providerName = provider.name;
|
|
1032
1602
|
const dialect = provider.dialect;
|
|
1603
|
+
if (providerName === "") continue;
|
|
1033
1604
|
if (accepted.has(providerName)) continue;
|
|
1034
1605
|
accepted.add(providerName);
|
|
1035
1606
|
if (options.provider && options.provider !== providerName) {
|
|
@@ -1043,6 +1614,7 @@ var DbCommand = class {
|
|
|
1043
1614
|
provider,
|
|
1044
1615
|
providerName,
|
|
1045
1616
|
providerUrl: provider.url,
|
|
1617
|
+
providerDriver: provider.driver,
|
|
1046
1618
|
dialect,
|
|
1047
1619
|
entry,
|
|
1048
1620
|
rootDir
|
|
@@ -1064,9 +1636,9 @@ var DbCommand = class {
|
|
|
1064
1636
|
dbCredentials: { url: options.providerUrl }
|
|
1065
1637
|
};
|
|
1066
1638
|
if (options.provider.schema) config.schemaFilter = options.provider.schema;
|
|
1067
|
-
if (options.
|
|
1068
|
-
if (options.
|
|
1069
|
-
if (options.dialect === "sqlite") if (options.
|
|
1639
|
+
if (options.providerDriver === "d1") config.driver = "d1-http";
|
|
1640
|
+
if (options.providerDriver === "pglite") config.driver = "pglite";
|
|
1641
|
+
if (options.dialect === "sqlite") if (options.providerDriver === "d1") {
|
|
1070
1642
|
const token = process.env.CLOUDFLARE_API_TOKEN;
|
|
1071
1643
|
if (!token) throw new AlephaError("CLOUDFLARE_API_TOKEN environment variable is not set. https://orm.drizzle.team/docs/guides/d1-http-with-drizzle-kit");
|
|
1072
1644
|
const accountId = process.env.CLOUDFLARE_ACCOUNT_ID;
|
|
@@ -1083,7 +1655,7 @@ var DbCommand = class {
|
|
|
1083
1655
|
} else {
|
|
1084
1656
|
let url = options.providerUrl;
|
|
1085
1657
|
url = url.replace("sqlite://", "").replace("file://", "");
|
|
1086
|
-
url = join(options.rootDir, url);
|
|
1658
|
+
url = this.fs.join(options.rootDir, url);
|
|
1087
1659
|
config.dbCredentials = { url };
|
|
1088
1660
|
}
|
|
1089
1661
|
const drizzleConfigJs = `export default ${JSON.stringify(config, null, 2)}`;
|
|
@@ -1095,7 +1667,9 @@ var DbCommand = class {
|
|
|
1095
1667
|
//#region ../../src/cli/commands/deploy.ts
|
|
1096
1668
|
var DeployCommand = class {
|
|
1097
1669
|
log = $logger();
|
|
1670
|
+
fs = $inject(FileSystemProvider);
|
|
1098
1671
|
utils = $inject(AlephaCliUtils);
|
|
1672
|
+
pm = $inject(PackageManagerUtils);
|
|
1099
1673
|
/**
|
|
1100
1674
|
* Deploy the project to a hosting platform (e.g., Vercel, Cloudflare, Surge)
|
|
1101
1675
|
*
|
|
@@ -1143,7 +1717,10 @@ var DeployCommand = class {
|
|
|
1143
1717
|
this.log.debug("Running database migrations before deployment...");
|
|
1144
1718
|
await this.utils.exec(`alepha db migrate --mode=${mode}`);
|
|
1145
1719
|
}
|
|
1146
|
-
await this.
|
|
1720
|
+
await this.pm.ensureDependency(root, "vercel", {
|
|
1721
|
+
dev: true,
|
|
1722
|
+
exec: (cmd, opts) => this.utils.exec(cmd, opts)
|
|
1723
|
+
});
|
|
1147
1724
|
const command = `vercel . --cwd=dist ${mode === "production" ? "--prod" : ""}`.trim();
|
|
1148
1725
|
this.log.debug(`Deploying to Vercel with command: ${command}`);
|
|
1149
1726
|
await this.utils.exec(command);
|
|
@@ -1154,15 +1731,21 @@ var DeployCommand = class {
|
|
|
1154
1731
|
this.log.debug("Running database migrations before deployment...");
|
|
1155
1732
|
await this.utils.exec(`alepha db migrate --mode=${mode}`);
|
|
1156
1733
|
}
|
|
1157
|
-
await this.
|
|
1734
|
+
await this.pm.ensureDependency(root, "wrangler", {
|
|
1735
|
+
dev: true,
|
|
1736
|
+
exec: (cmd, opts) => this.utils.exec(cmd, opts)
|
|
1737
|
+
});
|
|
1158
1738
|
const command = `wrangler deploy ${mode === "production" ? "" : "--env preview"} --config=dist/wrangler.jsonc`.trim();
|
|
1159
1739
|
this.log.info(`Deploying to Cloudflare with command: ${command}`);
|
|
1160
1740
|
await this.utils.exec(command);
|
|
1161
1741
|
return;
|
|
1162
1742
|
}
|
|
1163
1743
|
if (await this.utils.exists(root, "dist/public/404.html")) {
|
|
1164
|
-
await this.
|
|
1165
|
-
|
|
1744
|
+
await this.pm.ensureDependency(root, "surge", {
|
|
1745
|
+
dev: true,
|
|
1746
|
+
exec: (cmd, opts) => this.utils.exec(cmd, opts)
|
|
1747
|
+
});
|
|
1748
|
+
const distPath = this.fs.join(root, "dist/public");
|
|
1166
1749
|
this.log.debug(`Deploying to Surge from directory: ${distPath}`);
|
|
1167
1750
|
await this.utils.exec(`surge ${distPath}`);
|
|
1168
1751
|
return;
|
|
@@ -1173,75 +1756,271 @@ var DeployCommand = class {
|
|
|
1173
1756
|
};
|
|
1174
1757
|
|
|
1175
1758
|
//#endregion
|
|
1176
|
-
//#region ../../src/cli/
|
|
1177
|
-
|
|
1759
|
+
//#region ../../src/cli/providers/ViteDevServerProvider.ts
|
|
1760
|
+
/**
|
|
1761
|
+
* Vite development server with Alepha integration.
|
|
1762
|
+
*
|
|
1763
|
+
* Architecture:
|
|
1764
|
+
* - Vite runs in middleware mode (no HTTP server)
|
|
1765
|
+
* - Alepha is the HTTP server via server:onRequest event
|
|
1766
|
+
* - Request flow: Page requests → Alepha SSR, Assets → Vite middleware
|
|
1767
|
+
*
|
|
1768
|
+
* HMR Strategy:
|
|
1769
|
+
* - Browser-only changes (CSS, client components) → Vite HMR (React Fast Refresh)
|
|
1770
|
+
* - Server-only changes → Restart Alepha → Full browser reload
|
|
1771
|
+
* - Shared changes → Restart Alepha → Let Vite HMR propagate
|
|
1772
|
+
*
|
|
1773
|
+
* Features:
|
|
1774
|
+
* - Automatic .env reload detection
|
|
1775
|
+
* - Error recovery on next file change
|
|
1776
|
+
* - Optimized module invalidation (only changed files + importers)
|
|
1777
|
+
*/
|
|
1778
|
+
var ViteDevServerProvider = class {
|
|
1178
1779
|
log = $logger();
|
|
1179
|
-
|
|
1180
|
-
|
|
1780
|
+
fs = $inject(FileSystemProvider);
|
|
1781
|
+
templateProvider = $inject(ViteTemplateProvider);
|
|
1782
|
+
server;
|
|
1783
|
+
options;
|
|
1784
|
+
alepha = null;
|
|
1785
|
+
hasError = false;
|
|
1786
|
+
changedFiles = /* @__PURE__ */ new Set();
|
|
1787
|
+
async init(options) {
|
|
1788
|
+
this.options = options;
|
|
1789
|
+
await this.createViteServer();
|
|
1790
|
+
return await this.loadAlepha(true);
|
|
1791
|
+
}
|
|
1792
|
+
async start() {
|
|
1793
|
+
await this.alepha?.start();
|
|
1794
|
+
}
|
|
1181
1795
|
/**
|
|
1182
|
-
*
|
|
1183
|
-
*
|
|
1184
|
-
* - If an index.html file is found in the project root, it will run Vite in dev mode.
|
|
1185
|
-
* - Otherwise, it will look for a server entry file and run it with tsx in watch mode.
|
|
1796
|
+
* Create the Vite server in middleware mode.
|
|
1186
1797
|
*/
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1798
|
+
async createViteServer() {
|
|
1799
|
+
const { createServer } = await importVite();
|
|
1800
|
+
const viteReact = await importViteReact();
|
|
1801
|
+
const plugins = [];
|
|
1802
|
+
if (viteReact) plugins.push(viteReact());
|
|
1803
|
+
plugins.push(viteAlephaSsrPreload());
|
|
1804
|
+
plugins.push(this.createHmrPlugin());
|
|
1805
|
+
this.server = await createServer({
|
|
1806
|
+
root: this.options.root,
|
|
1807
|
+
plugins,
|
|
1808
|
+
server: { middlewareMode: true },
|
|
1809
|
+
appType: "custom",
|
|
1810
|
+
customLogger: {
|
|
1811
|
+
info: () => {},
|
|
1812
|
+
warn: this.log.warn.bind(this.log),
|
|
1813
|
+
error: this.log.error.bind(this.log),
|
|
1814
|
+
warnOnce: this.log.warn.bind(this.log),
|
|
1815
|
+
clearScreen: () => {},
|
|
1816
|
+
hasWarned: false,
|
|
1817
|
+
hasErrorLogged: () => false
|
|
1200
1818
|
}
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1819
|
+
});
|
|
1820
|
+
this.server.restart = async () => {
|
|
1821
|
+
const startTime = Date.now();
|
|
1822
|
+
try {
|
|
1823
|
+
this.hasError = true;
|
|
1824
|
+
await this.loadAlepha(false);
|
|
1825
|
+
await this.alepha?.start();
|
|
1826
|
+
this.log.debug(`Env reloaded in ${Date.now() - startTime}ms`);
|
|
1827
|
+
this.sendBrowserReload();
|
|
1828
|
+
} catch (err) {
|
|
1829
|
+
this.hasError = true;
|
|
1830
|
+
this.log.error("Reload failed", err);
|
|
1831
|
+
this.log.warn("Waiting for file changes to retry...");
|
|
1832
|
+
this.alepha = null;
|
|
1833
|
+
}
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1836
|
+
/**
|
|
1837
|
+
* Vite plugin to handle HMR for Alepha.
|
|
1838
|
+
*/
|
|
1839
|
+
createHmrPlugin() {
|
|
1840
|
+
return {
|
|
1841
|
+
name: "alepha-hmr",
|
|
1842
|
+
handleHotUpdate: async (ctx) => {
|
|
1843
|
+
if (ctx.file.includes("/.idea/")) return [];
|
|
1844
|
+
const firstModule = ctx.modules[0];
|
|
1845
|
+
const isBrowserOnly = firstModule && !firstModule._ssrModule;
|
|
1846
|
+
const isServerOnly = firstModule && !firstModule._clientModule;
|
|
1847
|
+
if (isBrowserOnly) return;
|
|
1848
|
+
const startTime = Date.now();
|
|
1849
|
+
try {
|
|
1850
|
+
this.changedFiles.add(ctx.file);
|
|
1851
|
+
await this.loadAlepha(false);
|
|
1852
|
+
await this.alepha?.start();
|
|
1853
|
+
this.log.debug(`Reloaded in ${Date.now() - startTime}ms`);
|
|
1854
|
+
if (isServerOnly) {
|
|
1855
|
+
this.sendBrowserReload();
|
|
1856
|
+
return [];
|
|
1857
|
+
}
|
|
1858
|
+
return;
|
|
1859
|
+
} catch (err) {
|
|
1860
|
+
this.hasError = true;
|
|
1861
|
+
this.log.error("Reload failed", err);
|
|
1862
|
+
this.log.warn("Waiting for file changes to retry...");
|
|
1863
|
+
this.alepha = null;
|
|
1864
|
+
return [];
|
|
1865
|
+
}
|
|
1210
1866
|
}
|
|
1211
|
-
|
|
1212
|
-
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
/**
|
|
1870
|
+
* Send browser reload signal via custom event.
|
|
1871
|
+
* Browser listens for 'alepha:reload' and does window.location.reload()
|
|
1872
|
+
*/
|
|
1873
|
+
sendBrowserReload() {
|
|
1874
|
+
this.server.ws.send({
|
|
1875
|
+
type: "custom",
|
|
1876
|
+
event: "alepha:reload",
|
|
1877
|
+
data: {}
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Setup environment variables for dev mode.
|
|
1882
|
+
*/
|
|
1883
|
+
async setupEnvironment() {
|
|
1884
|
+
const { loadEnv } = await importVite();
|
|
1885
|
+
const env = loadEnv(process.env.NODE_ENV || "development", this.options.root, "");
|
|
1886
|
+
process.env.NODE_ENV ??= "development";
|
|
1887
|
+
process.env.VITE_ALEPHA_DEV = "true";
|
|
1888
|
+
process.env.SERVER_HOST ??= this.options.host?.toString() ?? "localhost";
|
|
1889
|
+
process.env.SERVER_PORT ??= String(this.options.port ?? (process.env.SERVER_PORT ? Number(process.env.SERVER_PORT) : 3e3));
|
|
1890
|
+
for (const [key, value] of Object.entries(env)) process.env[key] ??= value;
|
|
1891
|
+
}
|
|
1892
|
+
/**
|
|
1893
|
+
* Load or reload the Alepha instance.
|
|
1894
|
+
*/
|
|
1895
|
+
async loadAlepha(isInitialLoad = false) {
|
|
1896
|
+
if (this.alepha) {
|
|
1897
|
+
await this.alepha.stop().catch((err) => this.log.warn("Error stopping Alepha", err));
|
|
1898
|
+
this.alepha = null;
|
|
1213
1899
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1900
|
+
if (isInitialLoad || this.hasError) this.server.moduleGraph.invalidateAll();
|
|
1901
|
+
else this.invalidateModulesWithImporters();
|
|
1902
|
+
this.changedFiles.clear();
|
|
1903
|
+
const envSnapshot = { ...process.env };
|
|
1904
|
+
await this.setupEnvironment();
|
|
1905
|
+
await this.server.ssrLoadModule(this.options.entry.server);
|
|
1906
|
+
const alepha = globalThis.__alepha;
|
|
1907
|
+
if (!alepha) throw new AlephaError("Alepha instance not found after loading entry module");
|
|
1908
|
+
this.alepha = alepha;
|
|
1909
|
+
await this.setupAlepha();
|
|
1910
|
+
this.hasError = false;
|
|
1911
|
+
process.env = envSnapshot;
|
|
1912
|
+
return alepha;
|
|
1913
|
+
}
|
|
1914
|
+
hasReact() {
|
|
1217
1915
|
try {
|
|
1218
|
-
|
|
1916
|
+
this.alepha?.inject("ReactServerProvider");
|
|
1219
1917
|
return true;
|
|
1220
1918
|
} catch {
|
|
1221
1919
|
return false;
|
|
1222
1920
|
}
|
|
1223
1921
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1922
|
+
/**
|
|
1923
|
+
* Setup Alepha instance with Vite middleware and template.
|
|
1924
|
+
*/
|
|
1925
|
+
async setupAlepha() {
|
|
1926
|
+
if (!this.alepha || !this.hasReact()) return;
|
|
1927
|
+
const template = await this.server.transformIndexHtml("/", this.templateProvider.generateIndexHtml(this.options.entry));
|
|
1928
|
+
this.alepha.store.set("alepha.react.server.template", template);
|
|
1929
|
+
this.alepha.events.on("server:onRequest", {
|
|
1930
|
+
priority: "first",
|
|
1931
|
+
callback: async ({ request }) => {
|
|
1932
|
+
const node = request.raw.node;
|
|
1933
|
+
if (!node || this.isPageRequest(node.req)) return;
|
|
1934
|
+
if (await this.runViteMiddleware(node.req, node.res, request)) {
|
|
1935
|
+
request.reply.status = node.res.statusCode || 200;
|
|
1936
|
+
request.reply.body = null;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* Check if request is for an HTML page (not an asset).
|
|
1943
|
+
*/
|
|
1944
|
+
isPageRequest(req) {
|
|
1945
|
+
const url = req.url || "/";
|
|
1946
|
+
if (url === "/" || url === "/index.html") return true;
|
|
1947
|
+
if (url.startsWith("/@") || url.startsWith("/__vite")) return false;
|
|
1948
|
+
if (/\.\w+$/.test(url.split("?")[0])) return false;
|
|
1949
|
+
return true;
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* Run Vite middleware and detect if it handled the request.
|
|
1953
|
+
*/
|
|
1954
|
+
async runViteMiddleware(req, res, ctx) {
|
|
1955
|
+
return new Promise((resolve) => {
|
|
1956
|
+
let resolved = false;
|
|
1957
|
+
const done = (handled) => {
|
|
1958
|
+
if (resolved) return;
|
|
1959
|
+
resolved = true;
|
|
1960
|
+
if (handled) ctx.metadata.vite = true;
|
|
1961
|
+
resolve(handled);
|
|
1962
|
+
};
|
|
1963
|
+
res.on("finish", () => done(true));
|
|
1964
|
+
res.on("close", () => res.headersSent && done(true));
|
|
1965
|
+
this.server.middlewares(req, res, () => done(false));
|
|
1966
|
+
setImmediate(() => {
|
|
1967
|
+
if (res.headersSent || res.writableEnded) done(true);
|
|
1968
|
+
});
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
/**
|
|
1972
|
+
* Invalidate modules and all their importers.
|
|
1973
|
+
*/
|
|
1974
|
+
invalidateModulesWithImporters() {
|
|
1975
|
+
const invalidated = /* @__PURE__ */ new Set();
|
|
1976
|
+
const queue = [...this.changedFiles];
|
|
1977
|
+
while (queue.length > 0) {
|
|
1978
|
+
const file = queue.pop();
|
|
1979
|
+
if (invalidated.has(file)) continue;
|
|
1980
|
+
const mod = this.server.moduleGraph.getModuleById(file);
|
|
1981
|
+
if (!mod) continue;
|
|
1982
|
+
this.server.moduleGraph.invalidateModule(mod);
|
|
1983
|
+
invalidated.add(file);
|
|
1984
|
+
for (const importer of mod.importers) if (importer.id && !invalidated.has(importer.id)) queue.push(importer.id);
|
|
1230
1985
|
}
|
|
1231
1986
|
}
|
|
1232
1987
|
};
|
|
1233
1988
|
|
|
1234
1989
|
//#endregion
|
|
1235
|
-
//#region ../../src/cli/commands/
|
|
1236
|
-
var
|
|
1990
|
+
//#region ../../src/cli/commands/dev.ts
|
|
1991
|
+
var DevCommand = class {
|
|
1992
|
+
log = $logger();
|
|
1993
|
+
fs = $inject(FileSystemProvider);
|
|
1237
1994
|
utils = $inject(AlephaCliUtils);
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1995
|
+
pm = $inject(PackageManagerUtils);
|
|
1996
|
+
scaffolder = $inject(ProjectScaffolder);
|
|
1997
|
+
alepha = $inject(Alepha);
|
|
1998
|
+
viteDevServer = $inject(ViteDevServerProvider);
|
|
1999
|
+
boot = $inject(AppEntryProvider);
|
|
2000
|
+
/**
|
|
2001
|
+
* Will run the project in watch mode.
|
|
2002
|
+
*
|
|
2003
|
+
* - If an index.html file is found in the project root, it will run Vite in dev mode.
|
|
2004
|
+
* - Otherwise, it will look for a server entry file and run it with tsx in watch mode.
|
|
2005
|
+
*/
|
|
2006
|
+
dev = $command({
|
|
2007
|
+
name: "dev",
|
|
2008
|
+
description: "Run the project in development mode",
|
|
1241
2009
|
handler: async ({ root }) => {
|
|
1242
|
-
await this.
|
|
1243
|
-
await this.
|
|
1244
|
-
|
|
2010
|
+
const [expo, react] = await Promise.all([this.pm.hasExpo(root), this.pm.hasReact(root)]);
|
|
2011
|
+
await this.scaffolder.ensureConfig(root, { tsconfigJson: true });
|
|
2012
|
+
if (expo) {
|
|
2013
|
+
await this.utils.exec("expo start");
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
const entry = await this.boot.getAppEntry(root);
|
|
2017
|
+
this.log.debug("Entry file found", { entry });
|
|
2018
|
+
await this.pm.ensureDependency(root, "vite", { exec: (cmd, opts) => this.utils.exec(cmd, opts) });
|
|
2019
|
+
await this.viteDevServer.init({
|
|
2020
|
+
root,
|
|
2021
|
+
entry
|
|
2022
|
+
});
|
|
2023
|
+
await this.viteDevServer.start();
|
|
1245
2024
|
}
|
|
1246
2025
|
});
|
|
1247
2026
|
};
|
|
@@ -1276,7 +2055,7 @@ const DEFAULT_IGNORE = [
|
|
|
1276
2055
|
* ```
|
|
1277
2056
|
*/
|
|
1278
2057
|
const changelogOptions = $atom({
|
|
1279
|
-
name: "alepha.changelog",
|
|
2058
|
+
name: "alepha.cli.changelog.options",
|
|
1280
2059
|
schema: t.object({ ignore: t.optional(t.array(t.string())) }),
|
|
1281
2060
|
default: { ignore: DEFAULT_IGNORE }
|
|
1282
2061
|
});
|
|
@@ -1484,7 +2263,7 @@ var GenEnvCommand = class {
|
|
|
1484
2263
|
if (value.description) dotEnvFile += `# ${value.description.split("\n").join("\n# ")}\n`;
|
|
1485
2264
|
if (value.required && !value.default) dotEnvFile += `# (required)\n`;
|
|
1486
2265
|
if (value.enum) dotEnvFile += `# Possible values: ${value.enum.join(", ")}\n`;
|
|
1487
|
-
dotEnvFile +=
|
|
2266
|
+
dotEnvFile += `#${key}=${value.default || ""}\n\n`;
|
|
1488
2267
|
}
|
|
1489
2268
|
if (flags.out) await this.fs.writeFile(this.fs.join(root, flags.out), dotEnvFile);
|
|
1490
2269
|
else this.log.info(dotEnvFile);
|
|
@@ -1560,6 +2339,9 @@ var GenCommand = class {
|
|
|
1560
2339
|
//#region ../../src/cli/commands/init.ts
|
|
1561
2340
|
var InitCommand = class {
|
|
1562
2341
|
utils = $inject(AlephaCliUtils);
|
|
2342
|
+
pm = $inject(PackageManagerUtils);
|
|
2343
|
+
scaffolder = $inject(ProjectScaffolder);
|
|
2344
|
+
fs = $inject(FileSystemProvider);
|
|
1563
2345
|
/**
|
|
1564
2346
|
* Ensure the project has the necessary Alepha configuration files.
|
|
1565
2347
|
* Add the correct dependencies to package.json and install them.
|
|
@@ -1567,45 +2349,84 @@ var InitCommand = class {
|
|
|
1567
2349
|
init = $command({
|
|
1568
2350
|
name: "init",
|
|
1569
2351
|
description: "Add missing Alepha configuration files to the project",
|
|
2352
|
+
args: t.optional(t.text({
|
|
2353
|
+
title: "path",
|
|
2354
|
+
trim: true,
|
|
2355
|
+
lowercase: true
|
|
2356
|
+
})),
|
|
1570
2357
|
flags: t.object({
|
|
2358
|
+
agent: t.optional(t.boolean({
|
|
2359
|
+
aliases: ["a"],
|
|
2360
|
+
description: "Add CLAUDE.md for Claude Code AI assistant"
|
|
2361
|
+
})),
|
|
1571
2362
|
yarn: t.optional(t.boolean({ description: "Use Yarn package manager" })),
|
|
1572
2363
|
pnpm: t.optional(t.boolean({ description: "Use pnpm package manager" })),
|
|
1573
2364
|
npm: t.optional(t.boolean({ description: "Use npm package manager" })),
|
|
1574
2365
|
bun: t.optional(t.boolean({ description: "Use Bun package manager" })),
|
|
1575
|
-
react: t.optional(t.boolean({
|
|
2366
|
+
react: t.optional(t.boolean({
|
|
2367
|
+
aliases: ["r"],
|
|
2368
|
+
description: "Include Alepha React dependencies"
|
|
2369
|
+
})),
|
|
1576
2370
|
ui: t.optional(t.boolean({ description: "Include Alepha UI dependencies" })),
|
|
1577
|
-
test: t.optional(t.boolean({ description: "Include Vitest and create test directory" }))
|
|
2371
|
+
test: t.optional(t.boolean({ description: "Include Vitest and create test directory" })),
|
|
2372
|
+
force: t.optional(t.boolean({
|
|
2373
|
+
aliases: ["f"],
|
|
2374
|
+
description: "Override existing files"
|
|
2375
|
+
}))
|
|
1578
2376
|
}),
|
|
1579
|
-
handler: async ({ run, flags, root }) => {
|
|
1580
|
-
if (flags.
|
|
1581
|
-
|
|
2377
|
+
handler: async ({ run, flags, root, args }) => {
|
|
2378
|
+
if (flags.react) flags.ui = true;
|
|
2379
|
+
if (args) {
|
|
2380
|
+
root = this.fs.join(root, args);
|
|
2381
|
+
await this.fs.mkdir(root);
|
|
2382
|
+
}
|
|
2383
|
+
const isExpo = await this.pm.hasExpo(root);
|
|
2384
|
+
const force = !!flags.force;
|
|
1582
2385
|
await run({
|
|
1583
2386
|
name: "ensuring configuration files",
|
|
1584
2387
|
handler: async () => {
|
|
1585
|
-
await this.
|
|
2388
|
+
await this.scaffolder.ensureConfig(root, {
|
|
2389
|
+
force,
|
|
1586
2390
|
tsconfigJson: true,
|
|
1587
2391
|
packageJson: flags,
|
|
1588
2392
|
biomeJson: true,
|
|
1589
2393
|
editorconfig: true,
|
|
1590
|
-
indexHtml: !!flags.react && !isExpo
|
|
2394
|
+
indexHtml: !!flags.react && !isExpo,
|
|
2395
|
+
claudeMd: flags.agent ? {
|
|
2396
|
+
react: !!flags.react,
|
|
2397
|
+
ui: !!flags.ui
|
|
2398
|
+
} : false
|
|
1591
2399
|
});
|
|
1592
|
-
if (!flags.react) await this.
|
|
2400
|
+
if (!flags.react) await this.scaffolder.ensureApiProject(root, { force });
|
|
1593
2401
|
}
|
|
1594
2402
|
});
|
|
1595
|
-
const
|
|
1596
|
-
if (
|
|
1597
|
-
await this.
|
|
1598
|
-
await run("yarn set version stable");
|
|
1599
|
-
} else if (
|
|
1600
|
-
else if (
|
|
1601
|
-
else await this.
|
|
1602
|
-
await run(`${
|
|
1603
|
-
|
|
1604
|
-
|
|
2403
|
+
const pmName = await this.pm.getPackageManager(root, flags);
|
|
2404
|
+
if (pmName === "yarn") {
|
|
2405
|
+
await this.pm.ensureYarn(root);
|
|
2406
|
+
await run("yarn set version stable", { root });
|
|
2407
|
+
} else if (pmName === "bun") await this.pm.ensureBun(root);
|
|
2408
|
+
else if (pmName === "pnpm") await this.pm.ensurePnpm(root);
|
|
2409
|
+
else await this.pm.ensureNpm(root);
|
|
2410
|
+
await run(`${pmName} install`, {
|
|
2411
|
+
alias: `installing dependencies with ${pmName}`,
|
|
2412
|
+
root
|
|
2413
|
+
});
|
|
2414
|
+
if (!isExpo) await this.pm.ensureDependency(root, "vite", {
|
|
2415
|
+
run,
|
|
2416
|
+
exec: (cmd, opts) => this.utils.exec(cmd, opts)
|
|
2417
|
+
});
|
|
2418
|
+
await this.pm.ensureDependency(root, "@biomejs/biome", {
|
|
2419
|
+
run,
|
|
2420
|
+
exec: (cmd, opts) => this.utils.exec(cmd, opts)
|
|
2421
|
+
});
|
|
1605
2422
|
if (flags.test) {
|
|
1606
|
-
await this.
|
|
1607
|
-
await run(`${
|
|
2423
|
+
await this.scaffolder.ensureTestDir(root);
|
|
2424
|
+
await run(`${pmName} ${pmName === "yarn" ? "add" : "install"} -D vitest`, { alias: "setup testing with Vitest" });
|
|
1608
2425
|
}
|
|
2426
|
+
await run(`${pmName} run lint`, {
|
|
2427
|
+
alias: "running linter",
|
|
2428
|
+
root
|
|
2429
|
+
});
|
|
1609
2430
|
}
|
|
1610
2431
|
});
|
|
1611
2432
|
};
|
|
@@ -1614,13 +2435,15 @@ var InitCommand = class {
|
|
|
1614
2435
|
//#region ../../src/cli/commands/lint.ts
|
|
1615
2436
|
var LintCommand = class {
|
|
1616
2437
|
utils = $inject(AlephaCliUtils);
|
|
2438
|
+
pm = $inject(PackageManagerUtils);
|
|
2439
|
+
scaffolder = $inject(ProjectScaffolder);
|
|
1617
2440
|
lint = $command({
|
|
1618
2441
|
name: "lint",
|
|
1619
2442
|
description: "Run linter across the codebase using Biome",
|
|
1620
2443
|
handler: async ({ root }) => {
|
|
1621
|
-
await this.
|
|
1622
|
-
await this.
|
|
1623
|
-
await this.utils.exec("biome check --
|
|
2444
|
+
await this.scaffolder.ensureConfig(root, { biomeJson: true });
|
|
2445
|
+
await this.pm.ensureDependency(root, "@biomejs/biome", { exec: (cmd, opts) => this.utils.exec(cmd, opts) });
|
|
2446
|
+
await this.utils.exec("biome check --fix");
|
|
1624
2447
|
}
|
|
1625
2448
|
});
|
|
1626
2449
|
};
|
|
@@ -1657,6 +2480,8 @@ var RootCommand = class {
|
|
|
1657
2480
|
//#region ../../src/cli/commands/test.ts
|
|
1658
2481
|
var TestCommand = class {
|
|
1659
2482
|
utils = $inject(AlephaCliUtils);
|
|
2483
|
+
pm = $inject(PackageManagerUtils);
|
|
2484
|
+
scaffolder = $inject(ProjectScaffolder);
|
|
1660
2485
|
test = $command({
|
|
1661
2486
|
name: "test",
|
|
1662
2487
|
description: "Run tests using Vitest",
|
|
@@ -1669,8 +2494,8 @@ var TestCommand = class {
|
|
|
1669
2494
|
description: "Additional arguments to pass to Vitest. E.g., --coverage"
|
|
1670
2495
|
})) }),
|
|
1671
2496
|
handler: async ({ root, flags, env }) => {
|
|
1672
|
-
await this.
|
|
1673
|
-
await this.
|
|
2497
|
+
await this.scaffolder.ensureConfig(root, { tsconfigJson: true });
|
|
2498
|
+
await this.pm.ensureDependency(root, "vitest", { exec: (cmd, opts) => this.utils.exec(cmd, opts) });
|
|
1674
2499
|
const config = flags.config ? `--config=${flags.config}` : "";
|
|
1675
2500
|
await this.utils.exec(`vitest run ${config} ${env.VITEST_ARGS}`);
|
|
1676
2501
|
}
|
|
@@ -1681,6 +2506,7 @@ var TestCommand = class {
|
|
|
1681
2506
|
//#region ../../src/cli/commands/typecheck.ts
|
|
1682
2507
|
var TypecheckCommand = class {
|
|
1683
2508
|
utils = $inject(AlephaCliUtils);
|
|
2509
|
+
pm = $inject(PackageManagerUtils);
|
|
1684
2510
|
log = $logger();
|
|
1685
2511
|
/**
|
|
1686
2512
|
* Run TypeScript type checking across the codebase with no emit.
|
|
@@ -1691,7 +2517,7 @@ var TypecheckCommand = class {
|
|
|
1691
2517
|
description: "Check TypeScript types across the codebase",
|
|
1692
2518
|
handler: async ({ root }) => {
|
|
1693
2519
|
this.log.info("Starting TypeScript type checking...");
|
|
1694
|
-
await this.
|
|
2520
|
+
await this.pm.ensureDependency(root, "typescript", { exec: (cmd, opts) => this.utils.exec(cmd, opts) });
|
|
1695
2521
|
await this.utils.exec("tsc --noEmit");
|
|
1696
2522
|
this.log.info("TypeScript type checking completed successfully.");
|
|
1697
2523
|
}
|
|
@@ -1702,6 +2528,7 @@ var TypecheckCommand = class {
|
|
|
1702
2528
|
//#region ../../src/cli/commands/verify.ts
|
|
1703
2529
|
var VerifyCommand = class {
|
|
1704
2530
|
utils = $inject(AlephaCliUtils);
|
|
2531
|
+
pm = $inject(PackageManagerUtils);
|
|
1705
2532
|
/**
|
|
1706
2533
|
* Run a series of verification commands to ensure code quality and correctness.
|
|
1707
2534
|
*
|
|
@@ -1723,9 +2550,9 @@ var VerifyCommand = class {
|
|
|
1723
2550
|
await run("alepha format");
|
|
1724
2551
|
await run("alepha lint");
|
|
1725
2552
|
await run("alepha typecheck");
|
|
1726
|
-
if ((await this.
|
|
2553
|
+
if ((await this.pm.readPackageJson(root)).devDependencies?.vitest) await run("alepha test");
|
|
1727
2554
|
if (await this.utils.exists(root, "migrations")) await run("alepha db check-migrations");
|
|
1728
|
-
if (!await this.
|
|
2555
|
+
if (!await this.pm.hasExpo(root)) await run("alepha build");
|
|
1729
2556
|
await run("alepha clean");
|
|
1730
2557
|
}
|
|
1731
2558
|
});
|
|
@@ -1769,7 +2596,6 @@ const AlephaCli = $module({
|
|
|
1769
2596
|
DbCommand,
|
|
1770
2597
|
DeployCommand,
|
|
1771
2598
|
DevCommand,
|
|
1772
|
-
FormatCommand,
|
|
1773
2599
|
InitCommand,
|
|
1774
2600
|
LintCommand,
|
|
1775
2601
|
RootCommand,
|
|
@@ -1777,6 +2603,7 @@ const AlephaCli = $module({
|
|
|
1777
2603
|
TypecheckCommand,
|
|
1778
2604
|
VerifyCommand,
|
|
1779
2605
|
GenCommand,
|
|
2606
|
+
AppEntryProvider,
|
|
1780
2607
|
GitProvider
|
|
1781
2608
|
]
|
|
1782
2609
|
});
|
|
@@ -1791,11 +2618,11 @@ var AlephaPackageBuilderCli = class {
|
|
|
1791
2618
|
root: true,
|
|
1792
2619
|
handler: async ({ run, root }) => {
|
|
1793
2620
|
const modules = [];
|
|
1794
|
-
const
|
|
1795
|
-
const pkgData = JSON.parse(
|
|
2621
|
+
const pkgBuffer = await this.fs.readFile("package.json");
|
|
2622
|
+
const pkgData = JSON.parse(pkgBuffer.toString("utf-8"));
|
|
1796
2623
|
const packageName = pkgData.name;
|
|
1797
2624
|
await run("analyze modules", async () => {
|
|
1798
|
-
modules.push(...await analyzeModules(join(root, this.src), packageName));
|
|
2625
|
+
modules.push(...await analyzeModules(this.fs.join(root, this.src), packageName));
|
|
1799
2626
|
});
|
|
1800
2627
|
pkgData.exports = {};
|
|
1801
2628
|
for (const item of modules) {
|
|
@@ -1807,6 +2634,7 @@ var AlephaPackageBuilderCli = class {
|
|
|
1807
2634
|
if (item.native) pkgData.exports[path]["react-native"] = `./src/${item.name}/index.native.ts`;
|
|
1808
2635
|
else if (item.browser) pkgData.exports[path]["react-native"] = `./src/${item.name}/index.browser.ts`;
|
|
1809
2636
|
if (item.browser) pkgData.exports[path].browser = `./src/${item.name}/index.browser.ts`;
|
|
2637
|
+
if (item.bun) pkgData.exports[path].bun = `./src/${item.name}/index.bun.ts`;
|
|
1810
2638
|
pkgData.exports[path].import = `./src/${item.name}/index.ts`;
|
|
1811
2639
|
pkgData.exports[path].default = `./src/${item.name}/index.ts`;
|
|
1812
2640
|
}
|
|
@@ -1819,48 +2647,58 @@ var AlephaPackageBuilderCli = class {
|
|
|
1819
2647
|
pkgData.exports["./json/styles"] = "./src/json/styles.css";
|
|
1820
2648
|
}
|
|
1821
2649
|
await this.fs.writeFile("package.json", JSON.stringify(pkgData, null, 2));
|
|
1822
|
-
const tmpDir = join(root, "node_modules/.alepha");
|
|
2650
|
+
const tmpDir = this.fs.join(root, "node_modules/.alepha");
|
|
1823
2651
|
await this.fs.mkdir(tmpDir, { recursive: true }).catch(() => {});
|
|
1824
|
-
await this.fs.writeFile(join(tmpDir, "module-dependencies.json"), JSON.stringify(modules, null, 2));
|
|
1825
|
-
const
|
|
1826
|
-
const external = Object.keys(JSON.parse(
|
|
2652
|
+
await this.fs.writeFile(this.fs.join(tmpDir, "module-dependencies.json"), JSON.stringify(modules, null, 2));
|
|
2653
|
+
const tsconfigBuffer = await this.fs.readFile(this.fs.join(root, "../../tsconfig.json"));
|
|
2654
|
+
const external = Object.keys(JSON.parse(tsconfigBuffer.toString("utf-8")).compilerOptions.paths);
|
|
1827
2655
|
external.push("bun");
|
|
1828
2656
|
external.push("bun:sqlite");
|
|
1829
2657
|
await run.rm(this.dist);
|
|
1830
2658
|
const build = async (item) => {
|
|
1831
2659
|
const entries = [];
|
|
1832
|
-
const src = join(root, this.src, item.name);
|
|
1833
|
-
const dest = join(root, this.dist, item.name);
|
|
2660
|
+
const src = this.fs.join(root, this.src, item.name);
|
|
2661
|
+
const dest = this.fs.join(root, this.dist, item.name);
|
|
1834
2662
|
entries.push({
|
|
1835
|
-
entry: join(src, "index.ts"),
|
|
2663
|
+
entry: this.fs.join(src, "index.ts"),
|
|
1836
2664
|
outDir: dest,
|
|
1837
2665
|
format: ["esm"],
|
|
1838
2666
|
sourcemap: true,
|
|
1839
2667
|
fixedExtension: false,
|
|
1840
2668
|
platform: "node",
|
|
2669
|
+
inlineOnly: false,
|
|
1841
2670
|
external,
|
|
1842
|
-
dts: {
|
|
1843
|
-
sourcemap: true,
|
|
1844
|
-
resolve: false
|
|
1845
|
-
}
|
|
2671
|
+
dts: { sourcemap: true }
|
|
1846
2672
|
});
|
|
1847
2673
|
if (item.native) entries.push({
|
|
1848
|
-
entry: join(src, "index.native.ts"),
|
|
2674
|
+
entry: this.fs.join(src, "index.native.ts"),
|
|
1849
2675
|
outDir: dest,
|
|
1850
2676
|
platform: "neutral",
|
|
1851
2677
|
sourcemap: true,
|
|
1852
2678
|
dts: false,
|
|
2679
|
+
inlineOnly: false,
|
|
1853
2680
|
external
|
|
1854
2681
|
});
|
|
1855
2682
|
if (item.browser) entries.push({
|
|
1856
|
-
entry: join(src, "index.browser.ts"),
|
|
2683
|
+
entry: this.fs.join(src, "index.browser.ts"),
|
|
1857
2684
|
outDir: dest,
|
|
1858
2685
|
platform: "browser",
|
|
1859
2686
|
sourcemap: true,
|
|
1860
2687
|
dts: false,
|
|
2688
|
+
inlineOnly: false,
|
|
2689
|
+
external
|
|
2690
|
+
});
|
|
2691
|
+
if (item.bun) entries.push({
|
|
2692
|
+
entry: this.fs.join(src, "index.bun.ts"),
|
|
2693
|
+
outDir: dest,
|
|
2694
|
+
platform: "node",
|
|
2695
|
+
sourcemap: true,
|
|
2696
|
+
fixedExtension: false,
|
|
2697
|
+
dts: false,
|
|
2698
|
+
inlineOnly: false,
|
|
1861
2699
|
external
|
|
1862
2700
|
});
|
|
1863
|
-
const config = join(tmpDir, `tsdown-${item.name.replace("/", "-")}.config.js`);
|
|
2701
|
+
const config = this.fs.join(tmpDir, `tsdown-${item.name.replace("/", "-")}.config.js`);
|
|
1864
2702
|
await this.fs.writeFile(config, `export default ${JSON.stringify(entries, null, 2)};`);
|
|
1865
2703
|
await run(`npx tsdown -c=${config}`);
|
|
1866
2704
|
};
|
|
@@ -1881,6 +2719,7 @@ var AlephaPackageBuilderCli = class {
|
|
|
1881
2719
|
}
|
|
1882
2720
|
});
|
|
1883
2721
|
};
|
|
2722
|
+
var AlephaPackageBuilderCli_default = AlephaPackageBuilderCli;
|
|
1884
2723
|
async function getAllFiles(dir) {
|
|
1885
2724
|
const files = [];
|
|
1886
2725
|
async function scan(currentDir) {
|
|
@@ -1934,7 +2773,7 @@ function detectCircularDependencies(modules) {
|
|
|
1934
2773
|
}
|
|
1935
2774
|
for (const module of modules) {
|
|
1936
2775
|
const cycle = hasCycle(module.name);
|
|
1937
|
-
if (cycle) throw new
|
|
2776
|
+
if (cycle) throw new AlephaError(`Circular dependency detected: ${cycle.join(" -> ")}`);
|
|
1938
2777
|
}
|
|
1939
2778
|
}
|
|
1940
2779
|
async function analyzeModules(srcDir, packageName) {
|
|
@@ -1948,6 +2787,7 @@ async function analyzeModules(srcDir, packageName) {
|
|
|
1948
2787
|
const dependencies = /* @__PURE__ */ new Set();
|
|
1949
2788
|
const hasBrowser = await fileExists(join(modulePath, "index.browser.ts"));
|
|
1950
2789
|
const hasNative = await fileExists(join(modulePath, "index.native.ts"));
|
|
2790
|
+
const hasBun = await fileExists(join(modulePath, "index.bun.ts"));
|
|
1951
2791
|
const hasNode = await fileExists(join(modulePath, "index.node.ts"));
|
|
1952
2792
|
const files = await getAllFiles(modulePath);
|
|
1953
2793
|
for (const file of files) {
|
|
@@ -1964,6 +2804,7 @@ async function analyzeModules(srcDir, packageName) {
|
|
|
1964
2804
|
};
|
|
1965
2805
|
if (hasNative) module.native = true;
|
|
1966
2806
|
if (hasBrowser) module.browser = true;
|
|
2807
|
+
if (hasBun) module.bun = true;
|
|
1967
2808
|
if (hasNode) module.node = true;
|
|
1968
2809
|
modules.push(module);
|
|
1969
2810
|
} else await scanDirectory(modulePath, moduleName);
|
|
@@ -1982,6 +2823,7 @@ const defineConfig = (runConfig) => {
|
|
|
1982
2823
|
if (config.services) for (const it of config.services) alepha.with(it);
|
|
1983
2824
|
if (config.env) for (const [key, value] of Object.entries(config.env)) process.env[key] = String(value);
|
|
1984
2825
|
if (config.build) alepha.set(buildOptions, config.build);
|
|
2826
|
+
if (config.entry) alepha.set(appEntryOptions, config.entry);
|
|
1985
2827
|
return { ...config.commands };
|
|
1986
2828
|
};
|
|
1987
2829
|
};
|
|
@@ -1991,5 +2833,5 @@ const defineConfig = (runConfig) => {
|
|
|
1991
2833
|
const defineAlephaConfig = defineConfig;
|
|
1992
2834
|
|
|
1993
2835
|
//#endregion
|
|
1994
|
-
export { AlephaCli, AlephaCliUtils, AlephaPackageBuilderCli, BuildCommand, ChangelogCommand, CleanCommand, DEFAULT_IGNORE, DbCommand, DeployCommand, DevCommand,
|
|
2836
|
+
export { AlephaCli, AlephaCliUtils, AlephaPackageBuilderCli_default as AlephaPackageBuilderCli, AppEntryProvider, BuildCommand, ChangelogCommand, CleanCommand, DEFAULT_IGNORE, DbCommand, DeployCommand, DevCommand, GitMessageParser, GitProvider, InitCommand, LintCommand, OpenApiCommand, RootCommand, TestCommand, TypecheckCommand, VerifyCommand, analyzeModules, changelogOptions, defineAlephaConfig, defineConfig, version };
|
|
1995
2837
|
//# sourceMappingURL=index.js.map
|