alepha 0.15.1 → 0.15.3
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 +68 -80
- package/dist/api/audits/index.d.ts +10 -33
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/audits/index.js +10 -33
- package/dist/api/audits/index.js.map +1 -1
- package/dist/api/files/index.d.ts +10 -3
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/files/index.js +10 -3
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.d.ts +162 -155
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +10 -3
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/keys/index.d.ts +413 -0
- package/dist/api/keys/index.d.ts.map +1 -0
- package/dist/api/keys/index.js +476 -0
- package/dist/api/keys/index.js.map +1 -0
- package/dist/api/notifications/index.d.ts +10 -4
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/notifications/index.js +10 -4
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/parameters/index.d.ts +43 -50
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +30 -37
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/api/users/index.d.ts +1081 -760
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +2539 -218
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts +138 -132
- package/dist/api/verifications/index.d.ts.map +1 -1
- package/dist/api/verifications/index.js +12 -4
- package/dist/api/verifications/index.js.map +1 -1
- package/dist/batch/index.d.ts +20 -40
- package/dist/batch/index.d.ts.map +1 -1
- package/dist/batch/index.js +31 -44
- package/dist/batch/index.js.map +1 -1
- package/dist/bucket/index.d.ts +440 -8
- package/dist/bucket/index.d.ts.map +1 -1
- package/dist/bucket/index.js +1861 -12
- package/dist/bucket/index.js.map +1 -1
- package/dist/cache/core/index.d.ts +179 -7
- package/dist/cache/core/index.d.ts.map +1 -1
- package/dist/cache/core/index.js +213 -7
- package/dist/cache/core/index.js.map +1 -1
- package/dist/cache/redis/index.d.ts +1 -0
- package/dist/cache/redis/index.d.ts.map +1 -1
- package/dist/cache/redis/index.js +4 -0
- package/dist/cache/redis/index.js.map +1 -1
- package/dist/cli/index.d.ts +638 -5645
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2550 -368
- package/dist/cli/index.js.map +1 -1
- package/dist/command/index.d.ts +203 -45
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js +2060 -71
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +70 -40
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +34 -13
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +90 -40
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +70 -40
- package/dist/core/index.native.js.map +1 -1
- package/dist/datetime/index.d.ts +15 -0
- package/dist/datetime/index.d.ts.map +1 -1
- package/dist/datetime/index.js +15 -0
- package/dist/datetime/index.js.map +1 -1
- package/dist/email/index.d.ts +323 -20
- package/dist/email/index.d.ts.map +1 -1
- package/dist/email/index.js +1857 -7
- package/dist/email/index.js.map +1 -1
- package/dist/fake/index.d.ts +90 -8
- package/dist/fake/index.d.ts.map +1 -1
- package/dist/fake/index.js +91 -20
- package/dist/fake/index.js.map +1 -1
- package/dist/lock/core/index.d.ts +11 -4
- package/dist/lock/core/index.d.ts.map +1 -1
- package/dist/lock/core/index.js +11 -4
- package/dist/lock/core/index.js.map +1 -1
- package/dist/logger/index.d.ts +17 -66
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/logger/index.js +14 -63
- package/dist/logger/index.js.map +1 -1
- package/dist/mcp/index.d.ts +10 -30
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +12 -35
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/index.browser.js +3 -3
- package/dist/orm/index.browser.js.map +1 -1
- package/dist/orm/index.bun.js +39 -20
- package/dist/orm/index.bun.js.map +1 -1
- package/dist/orm/index.d.ts +517 -540
- package/dist/orm/index.d.ts.map +1 -1
- package/dist/orm/index.js +58 -71
- package/dist/orm/index.js.map +1 -1
- package/dist/queue/core/index.d.ts +18 -10
- package/dist/queue/core/index.d.ts.map +1 -1
- package/dist/queue/core/index.js +14 -6
- package/dist/queue/core/index.js.map +1 -1
- package/dist/react/auth/index.browser.js +108 -0
- package/dist/react/auth/index.browser.js.map +1 -0
- package/dist/react/auth/index.d.ts +100 -0
- package/dist/react/auth/index.d.ts.map +1 -0
- package/dist/react/auth/index.js +145 -0
- package/dist/react/auth/index.js.map +1 -0
- package/dist/react/core/index.d.ts +469 -0
- package/dist/react/core/index.d.ts.map +1 -0
- package/dist/react/core/index.js +464 -0
- package/dist/react/core/index.js.map +1 -0
- package/dist/react/form/index.d.ts +232 -0
- package/dist/react/form/index.d.ts.map +1 -0
- package/dist/react/form/index.js +432 -0
- package/dist/react/form/index.js.map +1 -0
- package/dist/react/head/index.browser.js +423 -0
- package/dist/react/head/index.browser.js.map +1 -0
- package/dist/react/head/index.d.ts +288 -0
- package/dist/react/head/index.d.ts.map +1 -0
- package/dist/react/head/index.js +465 -0
- package/dist/react/head/index.js.map +1 -0
- package/dist/react/i18n/index.d.ts +175 -0
- package/dist/react/i18n/index.d.ts.map +1 -0
- package/dist/react/i18n/index.js +224 -0
- package/dist/react/i18n/index.js.map +1 -0
- package/dist/react/router/index.browser.js +1974 -0
- package/dist/react/router/index.browser.js.map +1 -0
- package/dist/react/router/index.d.ts +1956 -0
- package/dist/react/router/index.d.ts.map +1 -0
- package/dist/react/router/index.js +4722 -0
- package/dist/react/router/index.js.map +1 -0
- package/dist/react/websocket/index.d.ts +117 -0
- package/dist/react/websocket/index.d.ts.map +1 -0
- package/dist/react/websocket/index.js +107 -0
- package/dist/react/websocket/index.js.map +1 -0
- package/dist/redis/index.bun.js +4 -0
- package/dist/redis/index.bun.js.map +1 -1
- package/dist/redis/index.d.ts +41 -44
- package/dist/redis/index.d.ts.map +1 -1
- package/dist/redis/index.js +16 -25
- package/dist/redis/index.js.map +1 -1
- package/dist/retry/index.d.ts +11 -2
- package/dist/retry/index.d.ts.map +1 -1
- package/dist/retry/index.js +11 -2
- package/dist/retry/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +11 -2
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +11 -2
- package/dist/scheduler/index.js.map +1 -1
- package/dist/security/index.d.ts +140 -49
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +164 -32
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +12 -7
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +12 -7
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/cache/index.d.ts +7 -22
- package/dist/server/cache/index.d.ts.map +1 -1
- package/dist/server/cache/index.js +7 -22
- package/dist/server/cache/index.js.map +1 -1
- package/dist/server/compress/index.d.ts +10 -2
- package/dist/server/compress/index.d.ts.map +1 -1
- package/dist/server/compress/index.js +10 -2
- package/dist/server/compress/index.js.map +1 -1
- package/dist/server/cookies/index.d.ts +40 -16
- package/dist/server/cookies/index.d.ts.map +1 -1
- package/dist/server/cookies/index.js +7 -5
- package/dist/server/cookies/index.js.map +1 -1
- package/dist/server/core/index.d.ts +124 -23
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/core/index.js +231 -14
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/cors/index.d.ts +13 -23
- package/dist/server/cors/index.d.ts.map +1 -1
- package/dist/server/cors/index.js +7 -21
- package/dist/server/cors/index.js.map +1 -1
- package/dist/server/health/index.d.ts +8 -2
- package/dist/server/health/index.d.ts.map +1 -1
- package/dist/server/health/index.js +8 -2
- package/dist/server/health/index.js.map +1 -1
- package/dist/server/helmet/index.d.ts +11 -3
- package/dist/server/helmet/index.d.ts.map +1 -1
- package/dist/server/helmet/index.js +11 -3
- package/dist/server/helmet/index.js.map +1 -1
- package/dist/server/links/index.d.ts +11 -6
- package/dist/server/links/index.d.ts.map +1 -1
- package/dist/server/links/index.js +11 -6
- package/dist/server/links/index.js.map +1 -1
- package/dist/server/metrics/index.d.ts +10 -3
- package/dist/server/metrics/index.d.ts.map +1 -1
- package/dist/server/metrics/index.js +10 -3
- package/dist/server/metrics/index.js.map +1 -1
- package/dist/server/multipart/index.d.ts +9 -3
- package/dist/server/multipart/index.d.ts.map +1 -1
- package/dist/server/multipart/index.js +9 -3
- package/dist/server/multipart/index.js.map +1 -1
- package/dist/server/proxy/index.d.ts +8 -2
- package/dist/server/proxy/index.d.ts.map +1 -1
- package/dist/server/proxy/index.js +8 -2
- package/dist/server/proxy/index.js.map +1 -1
- package/dist/server/rate-limit/index.d.ts +30 -35
- package/dist/server/rate-limit/index.d.ts.map +1 -1
- package/dist/server/rate-limit/index.js +18 -55
- package/dist/server/rate-limit/index.js.map +1 -1
- package/dist/server/static/index.d.ts +137 -4
- package/dist/server/static/index.d.ts.map +1 -1
- package/dist/server/static/index.js +1853 -5
- package/dist/server/static/index.js.map +1 -1
- package/dist/server/swagger/index.d.ts +309 -6
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/server/swagger/index.js +1854 -6
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/sms/index.d.ts +309 -7
- package/dist/sms/index.d.ts.map +1 -1
- package/dist/sms/index.js +1856 -7
- package/dist/sms/index.js.map +1 -1
- package/dist/system/index.browser.js +1218 -0
- package/dist/system/index.browser.js.map +1 -0
- package/dist/{file → system}/index.d.ts +343 -16
- package/dist/system/index.d.ts.map +1 -0
- package/dist/{file → system}/index.js +419 -22
- package/dist/system/index.js.map +1 -0
- package/dist/thread/index.d.ts +11 -2
- package/dist/thread/index.d.ts.map +1 -1
- package/dist/thread/index.js +11 -2
- package/dist/thread/index.js.map +1 -1
- package/dist/topic/core/index.d.ts +12 -5
- package/dist/topic/core/index.d.ts.map +1 -1
- package/dist/topic/core/index.js +12 -5
- package/dist/topic/core/index.js.map +1 -1
- package/dist/vite/index.d.ts +5 -6272
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +23 -10
- package/dist/vite/index.js.map +1 -1
- package/dist/websocket/index.d.ts +12 -8
- package/dist/websocket/index.d.ts.map +1 -1
- package/dist/websocket/index.js +12 -8
- package/dist/websocket/index.js.map +1 -1
- package/package.json +82 -11
- package/src/api/audits/index.ts +10 -33
- package/src/api/files/__tests__/$bucket.spec.ts +1 -1
- package/src/api/files/controllers/AdminFileStatsController.spec.ts +1 -1
- package/src/api/files/controllers/FileController.spec.ts +1 -1
- package/src/api/files/index.ts +10 -3
- package/src/api/files/jobs/FileJobs.spec.ts +1 -1
- package/src/api/files/services/FileService.spec.ts +1 -1
- package/src/api/jobs/index.ts +10 -3
- package/src/api/keys/controllers/AdminApiKeyController.ts +75 -0
- package/src/api/keys/controllers/ApiKeyController.ts +103 -0
- package/src/api/keys/entities/apiKeyEntity.ts +41 -0
- package/src/api/keys/index.ts +49 -0
- package/src/api/keys/schemas/adminApiKeyQuerySchema.ts +7 -0
- package/src/api/keys/schemas/adminApiKeyResourceSchema.ts +17 -0
- package/src/api/keys/schemas/createApiKeyBodySchema.ts +7 -0
- package/src/api/keys/schemas/createApiKeyResponseSchema.ts +11 -0
- package/src/api/keys/schemas/listApiKeyResponseSchema.ts +15 -0
- package/src/api/keys/schemas/revokeApiKeyParamsSchema.ts +5 -0
- package/src/api/keys/schemas/revokeApiKeyResponseSchema.ts +5 -0
- package/src/api/keys/services/ApiKeyService.spec.ts +553 -0
- package/src/api/keys/services/ApiKeyService.ts +306 -0
- package/src/api/logs/TODO.md +55 -0
- package/src/api/notifications/index.ts +10 -4
- package/src/api/parameters/index.ts +9 -30
- package/src/api/parameters/primitives/$config.ts +12 -4
- package/src/api/parameters/services/ConfigStore.ts +9 -3
- package/src/api/users/__tests__/ApiKeys-integration.spec.ts +1035 -0
- package/src/api/users/__tests__/ApiKeys.spec.ts +401 -0
- package/src/api/users/index.ts +14 -3
- package/src/api/users/primitives/$realm.ts +33 -5
- package/src/api/users/providers/RealmProvider.ts +1 -12
- package/src/api/users/services/SessionService.ts +1 -1
- package/src/api/verifications/controllers/VerificationController.ts +2 -0
- package/src/api/verifications/index.ts +10 -4
- package/src/batch/index.ts +9 -36
- package/src/batch/primitives/$batch.ts +0 -8
- package/src/batch/providers/BatchProvider.ts +29 -2
- package/src/bucket/__tests__/shared.ts +1 -1
- package/src/bucket/index.ts +13 -6
- package/src/bucket/primitives/$bucket.ts +1 -1
- package/src/bucket/providers/LocalFileStorageProvider.ts +1 -1
- package/src/bucket/providers/MemoryFileStorageProvider.ts +1 -1
- package/src/cache/core/__tests__/shared.ts +30 -0
- package/src/cache/core/index.ts +11 -6
- package/src/cache/core/primitives/$cache.spec.ts +5 -0
- package/src/cache/core/providers/CacheProvider.ts +17 -0
- package/src/cache/core/providers/MemoryCacheProvider.ts +300 -1
- package/src/cache/redis/__tests__/cache-redis.spec.ts +5 -0
- package/src/cache/redis/providers/RedisCacheProvider.ts +9 -0
- package/src/cli/apps/AlephaCli.ts +1 -14
- package/src/cli/apps/AlephaPackageBuilderCli.ts +10 -1
- package/src/cli/atoms/buildOptions.ts +99 -9
- package/src/cli/commands/build.ts +150 -37
- package/src/cli/commands/db.ts +22 -18
- package/src/cli/commands/deploy.ts +1 -1
- package/src/cli/commands/dev.ts +1 -20
- package/src/cli/commands/gen/env.ts +5 -2
- package/src/cli/commands/gen/openapi.ts +5 -2
- package/src/cli/commands/init.spec.ts +588 -0
- package/src/cli/commands/init.ts +115 -58
- package/src/cli/commands/lint.ts +7 -1
- package/src/cli/commands/typecheck.ts +11 -0
- package/src/cli/providers/AppEntryProvider.ts +1 -1
- package/src/cli/providers/ViteBuildProvider.ts +8 -50
- package/src/cli/providers/ViteDevServerProvider.ts +35 -16
- package/src/cli/services/AlephaCliUtils.ts +52 -121
- package/src/cli/services/PackageManagerUtils.ts +129 -11
- package/src/cli/services/ProjectScaffolder.spec.ts +97 -0
- package/src/cli/services/ProjectScaffolder.ts +148 -81
- package/src/cli/services/ViteUtils.ts +82 -0
- package/src/cli/{assets/claudeMd.ts → templates/agentMd.ts} +37 -24
- package/src/cli/templates/apiAppSecurityTs.ts +11 -0
- package/src/cli/templates/apiIndexTs.ts +30 -0
- package/src/cli/templates/gitignore.ts +39 -0
- package/src/cli/{assets → templates}/mainCss.ts +11 -2
- package/src/cli/templates/mainServerTs.ts +33 -0
- package/src/cli/templates/webAppRouterTs.ts +74 -0
- package/src/cli/templates/webHelloComponentTsx.ts +30 -0
- package/src/command/helpers/Runner.spec.ts +139 -0
- package/src/command/helpers/Runner.ts +7 -22
- package/src/command/index.ts +12 -4
- package/src/command/providers/CliProvider.spec.ts +1392 -0
- package/src/command/providers/CliProvider.ts +320 -47
- package/src/core/Alepha.ts +34 -27
- package/src/core/__tests__/Alepha-start.spec.ts +4 -4
- package/src/core/helpers/jsonSchemaToTypeBox.spec.ts +771 -0
- package/src/core/helpers/jsonSchemaToTypeBox.ts +62 -10
- package/src/core/index.shared.ts +1 -0
- package/src/core/index.ts +20 -0
- package/src/core/providers/EventManager.spec.ts +0 -71
- package/src/core/providers/EventManager.ts +3 -15
- package/src/core/providers/Json.ts +2 -14
- package/src/datetime/index.ts +15 -0
- package/src/email/index.ts +10 -5
- package/src/email/providers/LocalEmailProvider.spec.ts +1 -1
- package/src/email/providers/LocalEmailProvider.ts +1 -1
- package/src/fake/__tests__/keyName.example.ts +1 -1
- package/src/fake/__tests__/keyName.spec.ts +5 -5
- package/src/fake/index.ts +9 -6
- package/src/fake/providers/FakeProvider.spec.ts +258 -40
- package/src/fake/providers/FakeProvider.ts +133 -19
- package/src/lock/core/index.ts +11 -4
- package/src/logger/index.ts +17 -66
- package/src/mcp/index.ts +10 -27
- package/src/mcp/transports/SseMcpTransport.ts +0 -11
- package/src/orm/__tests__/PostgresProvider.spec.ts +2 -2
- package/src/orm/index.browser.ts +2 -2
- package/src/orm/index.bun.ts +5 -3
- package/src/orm/index.ts +23 -53
- package/src/orm/providers/drivers/BunSqliteProvider.ts +5 -1
- package/src/orm/providers/drivers/CloudflareD1Provider.ts +57 -30
- package/src/orm/providers/drivers/DatabaseProvider.ts +9 -1
- package/src/orm/providers/drivers/NodeSqliteProvider.ts +4 -1
- package/src/orm/services/Repository.ts +7 -3
- package/src/queue/core/index.ts +14 -6
- package/src/react/auth/__tests__/$auth.spec.ts +202 -0
- package/src/react/auth/hooks/useAuth.ts +32 -0
- package/src/react/auth/index.browser.ts +13 -0
- package/src/react/auth/index.shared.ts +2 -0
- package/src/react/auth/index.ts +48 -0
- package/src/react/auth/providers/ReactAuthProvider.ts +16 -0
- package/src/react/auth/services/ReactAuth.ts +135 -0
- package/src/react/core/__tests__/Router.spec.tsx +169 -0
- package/src/react/core/components/ClientOnly.tsx +49 -0
- package/src/react/core/components/ErrorBoundary.tsx +73 -0
- package/src/react/core/contexts/AlephaContext.ts +7 -0
- package/src/react/core/contexts/AlephaProvider.tsx +42 -0
- package/src/react/core/hooks/useAction.browser.spec.tsx +569 -0
- package/src/react/core/hooks/useAction.ts +480 -0
- package/src/react/core/hooks/useAlepha.ts +26 -0
- package/src/react/core/hooks/useClient.ts +17 -0
- package/src/react/core/hooks/useEvents.ts +51 -0
- package/src/react/core/hooks/useInject.ts +12 -0
- package/src/react/core/hooks/useStore.ts +52 -0
- package/src/react/core/index.ts +90 -0
- package/src/react/form/components/FormState.tsx +17 -0
- package/src/react/form/errors/FormValidationError.ts +18 -0
- package/src/react/form/hooks/useForm.browser.spec.tsx +366 -0
- package/src/react/form/hooks/useForm.ts +47 -0
- package/src/react/form/hooks/useFormState.ts +130 -0
- package/src/react/form/index.ts +44 -0
- package/src/react/form/services/FormModel.ts +614 -0
- package/src/react/head/helpers/SeoExpander.spec.ts +203 -0
- package/src/react/head/helpers/SeoExpander.ts +142 -0
- package/src/react/head/hooks/useHead.spec.tsx +288 -0
- package/src/react/head/hooks/useHead.ts +62 -0
- package/src/react/head/index.browser.ts +26 -0
- package/src/react/head/index.ts +44 -0
- package/src/react/head/interfaces/Head.ts +105 -0
- package/src/react/head/primitives/$head.ts +25 -0
- package/src/react/head/providers/BrowserHeadProvider.browser.spec.ts +196 -0
- package/src/react/head/providers/BrowserHeadProvider.ts +212 -0
- package/src/react/head/providers/HeadProvider.ts +168 -0
- package/src/react/head/providers/ServerHeadProvider.ts +31 -0
- package/src/react/i18n/__tests__/integration.spec.tsx +239 -0
- package/src/react/i18n/components/Localize.spec.tsx +357 -0
- package/src/react/i18n/components/Localize.tsx +35 -0
- package/src/react/i18n/hooks/useI18n.browser.spec.tsx +438 -0
- package/src/react/i18n/hooks/useI18n.ts +18 -0
- package/src/react/i18n/index.ts +41 -0
- package/src/react/i18n/primitives/$dictionary.ts +69 -0
- package/src/react/i18n/providers/I18nProvider.spec.ts +389 -0
- package/src/react/i18n/providers/I18nProvider.ts +278 -0
- package/src/react/router/__tests__/page-head-browser.browser.spec.ts +95 -0
- package/src/react/router/__tests__/page-head.spec.ts +48 -0
- package/src/react/router/__tests__/seo-head.spec.ts +125 -0
- package/src/react/router/atoms/ssrManifestAtom.ts +58 -0
- package/src/react/router/components/ErrorViewer.tsx +872 -0
- package/src/react/router/components/Link.tsx +23 -0
- package/src/react/router/components/NestedView.tsx +223 -0
- package/src/react/router/components/NotFound.tsx +30 -0
- package/src/react/router/constants/PAGE_PRELOAD_KEY.ts +6 -0
- package/src/react/router/contexts/RouterLayerContext.ts +12 -0
- package/src/react/router/errors/Redirection.ts +28 -0
- package/src/react/router/hooks/useActive.ts +52 -0
- package/src/react/router/hooks/useQueryParams.ts +63 -0
- package/src/react/router/hooks/useRouter.ts +20 -0
- package/src/react/router/hooks/useRouterState.ts +11 -0
- package/src/react/router/index.browser.ts +45 -0
- package/src/react/router/index.shared.ts +19 -0
- package/src/react/router/index.ts +146 -0
- package/src/react/router/primitives/$page.browser.spec.tsx +851 -0
- package/src/react/router/primitives/$page.spec.tsx +676 -0
- package/src/react/router/primitives/$page.ts +489 -0
- package/src/react/router/providers/ReactBrowserProvider.ts +312 -0
- package/src/react/router/providers/ReactBrowserRendererProvider.ts +25 -0
- package/src/react/router/providers/ReactBrowserRouterProvider.ts +168 -0
- package/src/react/router/providers/ReactPageProvider.ts +726 -0
- package/src/react/router/providers/ReactPreloadProvider.spec.ts +142 -0
- package/src/react/router/providers/ReactPreloadProvider.ts +85 -0
- package/src/react/router/providers/ReactServerProvider.spec.tsx +316 -0
- package/src/react/router/providers/ReactServerProvider.ts +487 -0
- package/src/react/router/providers/ReactServerTemplateProvider.spec.ts +210 -0
- package/src/react/router/providers/ReactServerTemplateProvider.ts +542 -0
- package/src/react/router/providers/SSRManifestProvider.ts +334 -0
- package/src/react/router/services/ReactPageServerService.ts +48 -0
- package/src/react/router/services/ReactPageService.ts +27 -0
- package/src/react/router/services/ReactRouter.ts +262 -0
- package/src/react/websocket/hooks/useRoom.tsx +242 -0
- package/src/react/websocket/index.ts +7 -0
- package/src/redis/__tests__/redis.spec.ts +13 -0
- package/src/redis/index.ts +9 -25
- package/src/redis/providers/BunRedisProvider.ts +9 -0
- package/src/redis/providers/NodeRedisProvider.ts +8 -0
- package/src/redis/providers/RedisProvider.ts +16 -0
- package/src/retry/index.ts +11 -2
- package/src/router/index.ts +15 -0
- package/src/scheduler/index.ts +11 -2
- package/src/security/__tests__/BasicAuth.spec.ts +2 -0
- package/src/security/__tests__/ServerSecurityProvider.spec.ts +90 -5
- package/src/security/index.ts +15 -10
- package/src/security/interfaces/IssuerResolver.ts +27 -0
- package/src/security/primitives/$issuer.ts +55 -0
- package/src/security/providers/SecurityProvider.ts +179 -0
- package/src/security/providers/ServerBasicAuthProvider.ts +6 -2
- package/src/security/providers/ServerSecurityProvider.ts +63 -41
- package/src/server/auth/index.ts +12 -7
- package/src/server/cache/index.ts +7 -22
- package/src/server/compress/index.ts +10 -2
- package/src/server/cookies/index.ts +7 -5
- package/src/server/cookies/primitives/$cookie.ts +33 -11
- package/src/server/core/index.ts +16 -6
- package/src/server/core/interfaces/ServerRequest.ts +83 -1
- package/src/server/core/primitives/$action.spec.ts +1 -1
- package/src/server/core/primitives/$action.ts +8 -3
- package/src/server/core/providers/NodeHttpServerProvider.spec.ts +9 -3
- package/src/server/core/providers/NodeHttpServerProvider.ts +9 -3
- package/src/server/core/services/ServerRequestParser.spec.ts +520 -0
- package/src/server/core/services/ServerRequestParser.ts +306 -13
- package/src/server/cors/index.ts +7 -21
- package/src/server/cors/primitives/$cors.ts +6 -2
- package/src/server/health/index.ts +8 -2
- package/src/server/helmet/index.ts +11 -3
- package/src/server/links/index.ts +11 -6
- package/src/server/metrics/index.ts +10 -3
- package/src/server/multipart/index.ts +9 -3
- package/src/server/proxy/index.ts +8 -2
- package/src/server/rate-limit/index.ts +21 -25
- package/src/server/rate-limit/primitives/$rateLimit.ts +6 -2
- package/src/server/rate-limit/providers/ServerRateLimitProvider.spec.ts +38 -14
- package/src/server/rate-limit/providers/ServerRateLimitProvider.ts +22 -56
- package/src/server/static/index.ts +8 -2
- package/src/server/static/providers/ServerStaticProvider.ts +1 -1
- package/src/server/swagger/index.ts +9 -4
- package/src/server/swagger/providers/ServerSwaggerProvider.ts +1 -1
- package/src/sms/index.ts +9 -5
- package/src/sms/providers/LocalSmsProvider.spec.ts +1 -1
- package/src/sms/providers/LocalSmsProvider.ts +1 -1
- package/src/system/index.browser.ts +36 -0
- package/src/system/index.ts +62 -0
- package/src/system/index.workerd.ts +1 -0
- package/src/{file → system}/providers/FileSystemProvider.ts +24 -0
- package/src/{file → system}/providers/MemoryFileSystemProvider.ts +116 -3
- package/src/system/providers/MemoryShellProvider.ts +164 -0
- package/src/{file → system}/providers/NodeFileSystemProvider.spec.ts +2 -2
- package/src/{file → system}/providers/NodeFileSystemProvider.ts +47 -2
- package/src/system/providers/NodeShellProvider.ts +184 -0
- package/src/system/providers/ShellProvider.ts +74 -0
- package/src/{file → system}/services/FileDetector.spec.ts +2 -2
- package/src/thread/index.ts +11 -2
- package/src/topic/core/index.ts +12 -5
- package/src/vite/tasks/buildClient.ts +2 -7
- package/src/vite/tasks/buildServer.ts +19 -13
- package/src/vite/tasks/generateCloudflare.ts +10 -7
- package/src/vite/tasks/generateDocker.ts +4 -0
- package/src/websocket/index.ts +12 -8
- package/dist/file/index.d.ts.map +0 -1
- package/dist/file/index.js.map +0 -1
- package/src/cli/assets/apiIndexTs.ts +0 -16
- package/src/cli/assets/mainServerTs.ts +0 -24
- package/src/cli/assets/webAppRouterTs.ts +0 -16
- package/src/cli/assets/webHelloComponentTsx.ts +0 -20
- package/src/cli/providers/ViteTemplateProvider.ts +0 -27
- package/src/file/index.ts +0 -43
- /package/src/cli/{assets → templates}/apiHelloControllerTs.ts +0 -0
- /package/src/cli/{assets → templates}/biomeJson.ts +0 -0
- /package/src/cli/{assets → templates}/dummySpecTs.ts +0 -0
- /package/src/cli/{assets → templates}/editorconfig.ts +0 -0
- /package/src/cli/{assets → templates}/mainBrowserTs.ts +0 -0
- /package/src/cli/{assets → templates}/tsconfigJson.ts +0 -0
- /package/src/cli/{assets → templates}/webIndexTs.ts +0 -0
- /package/src/{file → system}/errors/FileError.ts +0 -0
- /package/src/{file → system}/services/FileDetector.ts +0 -0
package/dist/cli/index.js
CHANGED
|
@@ -1,16 +1,1857 @@
|
|
|
1
|
-
import { $atom, $hook, $inject, $module, $use, Alepha, AlephaError, t } from "alepha";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { $atom, $hook, $inject, $module, $use, Alepha, AlephaError, Json, isFileLike, t } from "alepha";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
|
+
import { createReadStream, readFileSync } from "node:fs";
|
|
4
|
+
import { access, copyFile, cp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
import { PassThrough, Readable } from "node:stream";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { exec, spawn } from "node:child_process";
|
|
4
8
|
import { $logger, ConsoleColorProvider } from "alepha/logger";
|
|
9
|
+
import { $command, CliProvider, EnvUtils } from "alepha/command";
|
|
5
10
|
import { buildClient, buildServer, copyAssets, generateCloudflare, generateDocker, generateSitemap, generateVercel, importVite, importViteReact, prerenderPages, viteAlephaSsrPreload } from "alepha/vite";
|
|
6
|
-
import { exec, spawn } from "node:child_process";
|
|
7
|
-
import { readFileSync } from "node:fs";
|
|
8
|
-
import { basename, dirname, join } from "node:path";
|
|
9
11
|
import { promisify } from "node:util";
|
|
10
12
|
import { ServerSwaggerProvider } from "alepha/server/swagger";
|
|
11
|
-
import { access, readFile, readdir } from "node:fs/promises";
|
|
12
13
|
import * as os from "node:os";
|
|
13
14
|
|
|
15
|
+
//#region ../../src/system/providers/FileSystemProvider.ts
|
|
16
|
+
/**
|
|
17
|
+
* FileSystem interface providing utilities for working with files.
|
|
18
|
+
*/
|
|
19
|
+
var FileSystemProvider = class {};
|
|
20
|
+
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region ../../src/system/providers/MemoryFileSystemProvider.ts
|
|
23
|
+
/**
|
|
24
|
+
* In-memory implementation of FileSystemProvider for testing.
|
|
25
|
+
*
|
|
26
|
+
* This provider stores all files and directories in memory, making it ideal for
|
|
27
|
+
* unit tests that need to verify file operations without touching the real file system.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* // In tests, substitute the real FileSystemProvider with MemoryFileSystemProvider
|
|
32
|
+
* const alepha = Alepha.create().with({
|
|
33
|
+
* provide: FileSystemProvider,
|
|
34
|
+
* use: MemoryFileSystemProvider,
|
|
35
|
+
* });
|
|
36
|
+
*
|
|
37
|
+
* // Run code that uses FileSystemProvider
|
|
38
|
+
* const service = alepha.inject(MyService);
|
|
39
|
+
* await service.saveFile("test.txt", "Hello World");
|
|
40
|
+
*
|
|
41
|
+
* // Verify the file was written
|
|
42
|
+
* const memoryFs = alepha.inject(MemoryFileSystemProvider);
|
|
43
|
+
* expect(memoryFs.files.get("test.txt")?.toString()).toBe("Hello World");
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
var MemoryFileSystemProvider = class {
|
|
47
|
+
json = $inject(Json);
|
|
48
|
+
/**
|
|
49
|
+
* In-memory storage for files (path -> content)
|
|
50
|
+
*/
|
|
51
|
+
files = /* @__PURE__ */ new Map();
|
|
52
|
+
/**
|
|
53
|
+
* In-memory storage for directories
|
|
54
|
+
*/
|
|
55
|
+
directories = /* @__PURE__ */ new Set();
|
|
56
|
+
/**
|
|
57
|
+
* Track mkdir calls for test assertions
|
|
58
|
+
*/
|
|
59
|
+
mkdirCalls = [];
|
|
60
|
+
/**
|
|
61
|
+
* Track writeFile calls for test assertions
|
|
62
|
+
*/
|
|
63
|
+
writeFileCalls = [];
|
|
64
|
+
/**
|
|
65
|
+
* Track readFile calls for test assertions
|
|
66
|
+
*/
|
|
67
|
+
readFileCalls = [];
|
|
68
|
+
/**
|
|
69
|
+
* Track rm calls for test assertions
|
|
70
|
+
*/
|
|
71
|
+
rmCalls = [];
|
|
72
|
+
/**
|
|
73
|
+
* Track join calls for test assertions
|
|
74
|
+
*/
|
|
75
|
+
joinCalls = [];
|
|
76
|
+
/**
|
|
77
|
+
* Error to throw on mkdir (for testing error handling)
|
|
78
|
+
*/
|
|
79
|
+
mkdirError = null;
|
|
80
|
+
/**
|
|
81
|
+
* Error to throw on writeFile (for testing error handling)
|
|
82
|
+
*/
|
|
83
|
+
writeFileError = null;
|
|
84
|
+
/**
|
|
85
|
+
* Error to throw on readFile (for testing error handling)
|
|
86
|
+
*/
|
|
87
|
+
readFileError = null;
|
|
88
|
+
constructor(options = {}) {
|
|
89
|
+
this.mkdirError = options.mkdirError ?? null;
|
|
90
|
+
this.writeFileError = options.writeFileError ?? null;
|
|
91
|
+
this.readFileError = options.readFileError ?? null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Join path segments using forward slashes.
|
|
95
|
+
* Uses Node's path.join for proper normalization (handles .. and .)
|
|
96
|
+
*/
|
|
97
|
+
join(...paths) {
|
|
98
|
+
this.joinCalls.push(paths);
|
|
99
|
+
return join(...paths);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Create a FileLike object from various sources.
|
|
103
|
+
*/
|
|
104
|
+
createFile(options) {
|
|
105
|
+
if ("path" in options) {
|
|
106
|
+
const filePath = options.path;
|
|
107
|
+
const buffer = this.files.get(filePath);
|
|
108
|
+
if (buffer === void 0) throw new Error(`ENOENT: no such file or directory, open '${filePath}'`);
|
|
109
|
+
return {
|
|
110
|
+
name: options.name ?? filePath.split("/").pop() ?? "file",
|
|
111
|
+
type: options.type ?? "application/octet-stream",
|
|
112
|
+
size: buffer.byteLength,
|
|
113
|
+
lastModified: Date.now(),
|
|
114
|
+
stream: () => {
|
|
115
|
+
throw new Error("Stream not implemented in MemoryFileSystemProvider");
|
|
116
|
+
},
|
|
117
|
+
arrayBuffer: async () => buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength),
|
|
118
|
+
text: async () => buffer.toString("utf-8")
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if ("buffer" in options) {
|
|
122
|
+
const buffer = options.buffer;
|
|
123
|
+
return {
|
|
124
|
+
name: options.name ?? "file",
|
|
125
|
+
type: options.type ?? "application/octet-stream",
|
|
126
|
+
size: buffer.byteLength,
|
|
127
|
+
lastModified: Date.now(),
|
|
128
|
+
stream: () => {
|
|
129
|
+
throw new Error("Stream not implemented in MemoryFileSystemProvider");
|
|
130
|
+
},
|
|
131
|
+
arrayBuffer: async () => buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength),
|
|
132
|
+
text: async () => buffer.toString("utf-8")
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if ("text" in options) {
|
|
136
|
+
const buffer = Buffer.from(options.text, "utf-8");
|
|
137
|
+
return {
|
|
138
|
+
name: options.name ?? "file.txt",
|
|
139
|
+
type: options.type ?? "text/plain",
|
|
140
|
+
size: buffer.byteLength,
|
|
141
|
+
lastModified: Date.now(),
|
|
142
|
+
stream: () => {
|
|
143
|
+
throw new Error("Stream not implemented in MemoryFileSystemProvider");
|
|
144
|
+
},
|
|
145
|
+
arrayBuffer: async () => buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength),
|
|
146
|
+
text: async () => options.text
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
throw new Error("MemoryFileSystemProvider.createFile: unsupported options. Only buffer and text are supported.");
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Remove a file or directory from memory.
|
|
153
|
+
*/
|
|
154
|
+
async rm(path, options) {
|
|
155
|
+
this.rmCalls.push({
|
|
156
|
+
path,
|
|
157
|
+
options
|
|
158
|
+
});
|
|
159
|
+
if (!(this.files.has(path) || this.directories.has(path)) && !options?.force) throw new Error(`ENOENT: no such file or directory, rm '${path}'`);
|
|
160
|
+
if (this.directories.has(path)) if (options?.recursive) {
|
|
161
|
+
this.directories.delete(path);
|
|
162
|
+
for (const filePath of this.files.keys()) if (filePath.startsWith(`${path}/`)) this.files.delete(filePath);
|
|
163
|
+
for (const dirPath of this.directories) if (dirPath.startsWith(`${path}/`)) this.directories.delete(dirPath);
|
|
164
|
+
} else throw new Error(`EISDIR: illegal operation on a directory, rm '${path}'`);
|
|
165
|
+
else this.files.delete(path);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Copy a file or directory in memory.
|
|
169
|
+
*/
|
|
170
|
+
async cp(src, dest, options) {
|
|
171
|
+
if (this.directories.has(src)) {
|
|
172
|
+
if (!options?.recursive) throw new Error(`Cannot copy directory without recursive option: ${src}`);
|
|
173
|
+
this.directories.add(dest);
|
|
174
|
+
for (const [filePath, content] of this.files) if (filePath.startsWith(`${src}/`)) {
|
|
175
|
+
const newPath = filePath.replace(src, dest);
|
|
176
|
+
this.files.set(newPath, Buffer.from(content));
|
|
177
|
+
}
|
|
178
|
+
} else if (this.files.has(src)) {
|
|
179
|
+
const content = this.files.get(src);
|
|
180
|
+
this.files.set(dest, Buffer.from(content));
|
|
181
|
+
} else throw new Error(`ENOENT: no such file or directory, cp '${src}'`);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Move/rename a file or directory in memory.
|
|
185
|
+
*/
|
|
186
|
+
async mv(src, dest) {
|
|
187
|
+
if (this.directories.has(src)) {
|
|
188
|
+
this.directories.delete(src);
|
|
189
|
+
this.directories.add(dest);
|
|
190
|
+
for (const [filePath, content] of this.files) if (filePath.startsWith(`${src}/`)) {
|
|
191
|
+
const newPath = filePath.replace(src, dest);
|
|
192
|
+
this.files.delete(filePath);
|
|
193
|
+
this.files.set(newPath, content);
|
|
194
|
+
}
|
|
195
|
+
} else if (this.files.has(src)) {
|
|
196
|
+
const content = this.files.get(src);
|
|
197
|
+
this.files.delete(src);
|
|
198
|
+
this.files.set(dest, content);
|
|
199
|
+
} else throw new Error(`ENOENT: no such file or directory, mv '${src}'`);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Create a directory in memory.
|
|
203
|
+
*/
|
|
204
|
+
async mkdir(path, options) {
|
|
205
|
+
this.mkdirCalls.push({
|
|
206
|
+
path,
|
|
207
|
+
options
|
|
208
|
+
});
|
|
209
|
+
if (this.mkdirError) throw this.mkdirError;
|
|
210
|
+
if (this.directories.has(path) && !options?.recursive) throw new Error(`EEXIST: file already exists, mkdir '${path}'`);
|
|
211
|
+
this.directories.add(path);
|
|
212
|
+
if (options?.recursive) {
|
|
213
|
+
const parts = path.split("/").filter(Boolean);
|
|
214
|
+
let current = "";
|
|
215
|
+
for (const part of parts) {
|
|
216
|
+
current = current ? `${current}/${part}` : part;
|
|
217
|
+
this.directories.add(current);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* List files in a directory.
|
|
223
|
+
*/
|
|
224
|
+
async ls(path, options) {
|
|
225
|
+
const normalizedPath = path.replace(/\/$/, "");
|
|
226
|
+
const entries = /* @__PURE__ */ new Set();
|
|
227
|
+
for (const filePath of this.files.keys()) if (filePath.startsWith(`${normalizedPath}/`)) {
|
|
228
|
+
const relativePath = filePath.slice(normalizedPath.length + 1);
|
|
229
|
+
const parts = relativePath.split("/");
|
|
230
|
+
if (options?.recursive) entries.add(relativePath);
|
|
231
|
+
else entries.add(parts[0]);
|
|
232
|
+
}
|
|
233
|
+
for (const dirPath of this.directories) if (dirPath.startsWith(`${normalizedPath}/`) && dirPath !== normalizedPath) {
|
|
234
|
+
const relativePath = dirPath.slice(normalizedPath.length + 1);
|
|
235
|
+
const parts = relativePath.split("/");
|
|
236
|
+
if (options?.recursive) entries.add(relativePath);
|
|
237
|
+
else if (parts.length === 1) entries.add(parts[0]);
|
|
238
|
+
}
|
|
239
|
+
let result = Array.from(entries);
|
|
240
|
+
if (!options?.hidden) result = result.filter((entry) => !entry.startsWith("."));
|
|
241
|
+
return result.sort();
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Check if a file or directory exists in memory.
|
|
245
|
+
*/
|
|
246
|
+
async exists(path) {
|
|
247
|
+
return this.files.has(path) || this.directories.has(path);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Read a file from memory.
|
|
251
|
+
*/
|
|
252
|
+
async readFile(path) {
|
|
253
|
+
this.readFileCalls.push(path);
|
|
254
|
+
if (this.readFileError) throw this.readFileError;
|
|
255
|
+
const content = this.files.get(path);
|
|
256
|
+
if (!content) throw new Error(`ENOENT: no such file or directory, open '${path}'`);
|
|
257
|
+
return content;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Read a file from memory as text.
|
|
261
|
+
*/
|
|
262
|
+
async readTextFile(path) {
|
|
263
|
+
return (await this.readFile(path)).toString("utf-8");
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Read a file from memory as JSON.
|
|
267
|
+
*/
|
|
268
|
+
async readJsonFile(path) {
|
|
269
|
+
const text = await this.readTextFile(path);
|
|
270
|
+
return this.json.parse(text);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Write a file to memory.
|
|
274
|
+
*/
|
|
275
|
+
async writeFile(path, data) {
|
|
276
|
+
const dataStr = typeof data === "string" ? data : data instanceof Buffer || data instanceof Uint8Array ? data.toString("utf-8") : await data.text();
|
|
277
|
+
this.writeFileCalls.push({
|
|
278
|
+
path,
|
|
279
|
+
data: dataStr
|
|
280
|
+
});
|
|
281
|
+
if (this.writeFileError) throw this.writeFileError;
|
|
282
|
+
const buffer = typeof data === "string" ? Buffer.from(data, "utf-8") : data instanceof Buffer ? data : data instanceof Uint8Array ? Buffer.from(data) : Buffer.from(await data.text(), "utf-8");
|
|
283
|
+
this.files.set(path, buffer);
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Reset all in-memory state (useful between tests).
|
|
287
|
+
*/
|
|
288
|
+
reset() {
|
|
289
|
+
this.files.clear();
|
|
290
|
+
this.directories.clear();
|
|
291
|
+
this.mkdirCalls = [];
|
|
292
|
+
this.writeFileCalls = [];
|
|
293
|
+
this.readFileCalls = [];
|
|
294
|
+
this.rmCalls = [];
|
|
295
|
+
this.joinCalls = [];
|
|
296
|
+
this.mkdirError = null;
|
|
297
|
+
this.writeFileError = null;
|
|
298
|
+
this.readFileError = null;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Check if a file was written during the test.
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* ```typescript
|
|
305
|
+
* expect(fs.wasWritten("/project/tsconfig.json")).toBe(true);
|
|
306
|
+
* ```
|
|
307
|
+
*/
|
|
308
|
+
wasWritten(path) {
|
|
309
|
+
return this.writeFileCalls.some((call) => call.path === path);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Check if a file was written with content matching a pattern.
|
|
313
|
+
*
|
|
314
|
+
* @example
|
|
315
|
+
* ```typescript
|
|
316
|
+
* expect(fs.wasWrittenMatching("/project/tsconfig.json", /extends/)).toBe(true);
|
|
317
|
+
* ```
|
|
318
|
+
*/
|
|
319
|
+
wasWrittenMatching(path, pattern) {
|
|
320
|
+
const call = this.writeFileCalls.find((c) => c.path === path);
|
|
321
|
+
return call ? pattern.test(call.data) : false;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Check if a file was read during the test.
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* ```typescript
|
|
328
|
+
* expect(fs.wasRead("/project/package.json")).toBe(true);
|
|
329
|
+
* ```
|
|
330
|
+
*/
|
|
331
|
+
wasRead(path) {
|
|
332
|
+
return this.readFileCalls.includes(path);
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Check if a file was deleted during the test.
|
|
336
|
+
*
|
|
337
|
+
* @example
|
|
338
|
+
* ```typescript
|
|
339
|
+
* expect(fs.wasDeleted("/project/old-file.txt")).toBe(true);
|
|
340
|
+
* ```
|
|
341
|
+
*/
|
|
342
|
+
wasDeleted(path) {
|
|
343
|
+
return this.rmCalls.some((call) => call.path === path);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Get the content of a file as a string (convenience method for testing).
|
|
347
|
+
*/
|
|
348
|
+
getFileContent(path) {
|
|
349
|
+
return this.files.get(path)?.toString("utf-8");
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
//#endregion
|
|
354
|
+
//#region ../../src/system/providers/MemoryShellProvider.ts
|
|
355
|
+
/**
|
|
356
|
+
* In-memory implementation of ShellProvider for testing.
|
|
357
|
+
*
|
|
358
|
+
* Records all commands that would be executed without actually running them.
|
|
359
|
+
* Can be configured to return specific outputs or throw errors for testing.
|
|
360
|
+
*
|
|
361
|
+
* @example
|
|
362
|
+
* ```typescript
|
|
363
|
+
* // In tests, substitute the real ShellProvider with MemoryShellProvider
|
|
364
|
+
* const alepha = Alepha.create().with({
|
|
365
|
+
* provide: ShellProvider,
|
|
366
|
+
* use: MemoryShellProvider,
|
|
367
|
+
* });
|
|
368
|
+
*
|
|
369
|
+
* // Configure mock behavior
|
|
370
|
+
* const shell = alepha.inject(MemoryShellProvider);
|
|
371
|
+
* shell.configure({
|
|
372
|
+
* outputs: { "echo hello": "hello\n" },
|
|
373
|
+
* errors: { "failing-cmd": "Command failed" },
|
|
374
|
+
* });
|
|
375
|
+
*
|
|
376
|
+
* // Or use the fluent API
|
|
377
|
+
* shell.outputs.set("another-cmd", "output");
|
|
378
|
+
* shell.errors.set("another-error", "Error message");
|
|
379
|
+
*
|
|
380
|
+
* // Run code that uses ShellProvider
|
|
381
|
+
* const service = alepha.inject(MyService);
|
|
382
|
+
* await service.doSomething();
|
|
383
|
+
*
|
|
384
|
+
* // Verify commands were called
|
|
385
|
+
* expect(shell.calls).toHaveLength(2);
|
|
386
|
+
* expect(shell.calls[0].command).toBe("yarn install");
|
|
387
|
+
* ```
|
|
388
|
+
*/
|
|
389
|
+
var MemoryShellProvider = class {
|
|
390
|
+
/**
|
|
391
|
+
* All recorded shell calls.
|
|
392
|
+
*/
|
|
393
|
+
calls = [];
|
|
394
|
+
/**
|
|
395
|
+
* Simulated outputs for specific commands.
|
|
396
|
+
*/
|
|
397
|
+
outputs = /* @__PURE__ */ new Map();
|
|
398
|
+
/**
|
|
399
|
+
* Commands that should throw an error.
|
|
400
|
+
*/
|
|
401
|
+
errors = /* @__PURE__ */ new Map();
|
|
402
|
+
/**
|
|
403
|
+
* Commands considered installed in the system PATH.
|
|
404
|
+
*/
|
|
405
|
+
installedCommands = /* @__PURE__ */ new Set();
|
|
406
|
+
/**
|
|
407
|
+
* Configure the mock with predefined outputs, errors, and installed commands.
|
|
408
|
+
*/
|
|
409
|
+
configure(options) {
|
|
410
|
+
if (options.outputs) for (const [cmd, output] of Object.entries(options.outputs)) this.outputs.set(cmd, output);
|
|
411
|
+
if (options.errors) for (const [cmd, error] of Object.entries(options.errors)) this.errors.set(cmd, error);
|
|
412
|
+
if (options.installedCommands) for (const cmd of options.installedCommands) this.installedCommands.add(cmd);
|
|
413
|
+
return this;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Record command and return simulated output.
|
|
417
|
+
*/
|
|
418
|
+
async run(command, options = {}) {
|
|
419
|
+
this.calls.push({
|
|
420
|
+
command,
|
|
421
|
+
options
|
|
422
|
+
});
|
|
423
|
+
const errorMsg = this.errors.get(command);
|
|
424
|
+
if (errorMsg) throw new Error(errorMsg);
|
|
425
|
+
return this.outputs.get(command) ?? "";
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Check if a specific command was called.
|
|
429
|
+
*/
|
|
430
|
+
wasCalled(command) {
|
|
431
|
+
return this.calls.some((call) => call.command === command);
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Check if a command matching a pattern was called.
|
|
435
|
+
*/
|
|
436
|
+
wasCalledMatching(pattern) {
|
|
437
|
+
return this.calls.some((call) => pattern.test(call.command));
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Get all calls matching a pattern.
|
|
441
|
+
*/
|
|
442
|
+
getCallsMatching(pattern) {
|
|
443
|
+
return this.calls.filter((call) => pattern.test(call.command));
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Check if a command is installed.
|
|
447
|
+
*/
|
|
448
|
+
async isInstalled(command) {
|
|
449
|
+
return this.installedCommands.has(command);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Reset all recorded state.
|
|
453
|
+
*/
|
|
454
|
+
reset() {
|
|
455
|
+
this.calls = [];
|
|
456
|
+
this.outputs.clear();
|
|
457
|
+
this.errors.clear();
|
|
458
|
+
this.installedCommands.clear();
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
//#endregion
|
|
463
|
+
//#region ../../src/system/services/FileDetector.ts
|
|
464
|
+
/**
|
|
465
|
+
* Service for detecting file types and getting content types.
|
|
466
|
+
*
|
|
467
|
+
* @example
|
|
468
|
+
* ```typescript
|
|
469
|
+
* const detector = alepha.inject(FileDetector);
|
|
470
|
+
*
|
|
471
|
+
* // Get content type from filename
|
|
472
|
+
* const mimeType = detector.getContentType("image.png"); // "image/png"
|
|
473
|
+
*
|
|
474
|
+
* // Detect file type by magic bytes
|
|
475
|
+
* const stream = createReadStream('image.png');
|
|
476
|
+
* const result = await detector.detectFileType(stream, 'image.png');
|
|
477
|
+
* console.log(result.mimeType); // 'image/png'
|
|
478
|
+
* console.log(result.verified); // true if magic bytes match
|
|
479
|
+
* ```
|
|
480
|
+
*/
|
|
481
|
+
var FileDetector = class FileDetector {
|
|
482
|
+
/**
|
|
483
|
+
* Magic byte signatures for common file formats.
|
|
484
|
+
* Each signature is represented as an array of bytes or null (wildcard).
|
|
485
|
+
*/
|
|
486
|
+
static MAGIC_BYTES = {
|
|
487
|
+
png: [{
|
|
488
|
+
signature: [
|
|
489
|
+
137,
|
|
490
|
+
80,
|
|
491
|
+
78,
|
|
492
|
+
71,
|
|
493
|
+
13,
|
|
494
|
+
10,
|
|
495
|
+
26,
|
|
496
|
+
10
|
|
497
|
+
],
|
|
498
|
+
mimeType: "image/png"
|
|
499
|
+
}],
|
|
500
|
+
jpg: [
|
|
501
|
+
{
|
|
502
|
+
signature: [
|
|
503
|
+
255,
|
|
504
|
+
216,
|
|
505
|
+
255,
|
|
506
|
+
224
|
|
507
|
+
],
|
|
508
|
+
mimeType: "image/jpeg"
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
signature: [
|
|
512
|
+
255,
|
|
513
|
+
216,
|
|
514
|
+
255,
|
|
515
|
+
225
|
|
516
|
+
],
|
|
517
|
+
mimeType: "image/jpeg"
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
signature: [
|
|
521
|
+
255,
|
|
522
|
+
216,
|
|
523
|
+
255,
|
|
524
|
+
226
|
|
525
|
+
],
|
|
526
|
+
mimeType: "image/jpeg"
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
signature: [
|
|
530
|
+
255,
|
|
531
|
+
216,
|
|
532
|
+
255,
|
|
533
|
+
227
|
|
534
|
+
],
|
|
535
|
+
mimeType: "image/jpeg"
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
signature: [
|
|
539
|
+
255,
|
|
540
|
+
216,
|
|
541
|
+
255,
|
|
542
|
+
232
|
|
543
|
+
],
|
|
544
|
+
mimeType: "image/jpeg"
|
|
545
|
+
}
|
|
546
|
+
],
|
|
547
|
+
jpeg: [
|
|
548
|
+
{
|
|
549
|
+
signature: [
|
|
550
|
+
255,
|
|
551
|
+
216,
|
|
552
|
+
255,
|
|
553
|
+
224
|
|
554
|
+
],
|
|
555
|
+
mimeType: "image/jpeg"
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
signature: [
|
|
559
|
+
255,
|
|
560
|
+
216,
|
|
561
|
+
255,
|
|
562
|
+
225
|
|
563
|
+
],
|
|
564
|
+
mimeType: "image/jpeg"
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
signature: [
|
|
568
|
+
255,
|
|
569
|
+
216,
|
|
570
|
+
255,
|
|
571
|
+
226
|
|
572
|
+
],
|
|
573
|
+
mimeType: "image/jpeg"
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
signature: [
|
|
577
|
+
255,
|
|
578
|
+
216,
|
|
579
|
+
255,
|
|
580
|
+
227
|
|
581
|
+
],
|
|
582
|
+
mimeType: "image/jpeg"
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
signature: [
|
|
586
|
+
255,
|
|
587
|
+
216,
|
|
588
|
+
255,
|
|
589
|
+
232
|
|
590
|
+
],
|
|
591
|
+
mimeType: "image/jpeg"
|
|
592
|
+
}
|
|
593
|
+
],
|
|
594
|
+
gif: [{
|
|
595
|
+
signature: [
|
|
596
|
+
71,
|
|
597
|
+
73,
|
|
598
|
+
70,
|
|
599
|
+
56,
|
|
600
|
+
55,
|
|
601
|
+
97
|
|
602
|
+
],
|
|
603
|
+
mimeType: "image/gif"
|
|
604
|
+
}, {
|
|
605
|
+
signature: [
|
|
606
|
+
71,
|
|
607
|
+
73,
|
|
608
|
+
70,
|
|
609
|
+
56,
|
|
610
|
+
57,
|
|
611
|
+
97
|
|
612
|
+
],
|
|
613
|
+
mimeType: "image/gif"
|
|
614
|
+
}],
|
|
615
|
+
webp: [{
|
|
616
|
+
signature: [
|
|
617
|
+
82,
|
|
618
|
+
73,
|
|
619
|
+
70,
|
|
620
|
+
70,
|
|
621
|
+
null,
|
|
622
|
+
null,
|
|
623
|
+
null,
|
|
624
|
+
null,
|
|
625
|
+
87,
|
|
626
|
+
69,
|
|
627
|
+
66,
|
|
628
|
+
80
|
|
629
|
+
],
|
|
630
|
+
mimeType: "image/webp"
|
|
631
|
+
}],
|
|
632
|
+
bmp: [{
|
|
633
|
+
signature: [66, 77],
|
|
634
|
+
mimeType: "image/bmp"
|
|
635
|
+
}],
|
|
636
|
+
ico: [{
|
|
637
|
+
signature: [
|
|
638
|
+
0,
|
|
639
|
+
0,
|
|
640
|
+
1,
|
|
641
|
+
0
|
|
642
|
+
],
|
|
643
|
+
mimeType: "image/x-icon"
|
|
644
|
+
}],
|
|
645
|
+
tiff: [{
|
|
646
|
+
signature: [
|
|
647
|
+
73,
|
|
648
|
+
73,
|
|
649
|
+
42,
|
|
650
|
+
0
|
|
651
|
+
],
|
|
652
|
+
mimeType: "image/tiff"
|
|
653
|
+
}, {
|
|
654
|
+
signature: [
|
|
655
|
+
77,
|
|
656
|
+
77,
|
|
657
|
+
0,
|
|
658
|
+
42
|
|
659
|
+
],
|
|
660
|
+
mimeType: "image/tiff"
|
|
661
|
+
}],
|
|
662
|
+
tif: [{
|
|
663
|
+
signature: [
|
|
664
|
+
73,
|
|
665
|
+
73,
|
|
666
|
+
42,
|
|
667
|
+
0
|
|
668
|
+
],
|
|
669
|
+
mimeType: "image/tiff"
|
|
670
|
+
}, {
|
|
671
|
+
signature: [
|
|
672
|
+
77,
|
|
673
|
+
77,
|
|
674
|
+
0,
|
|
675
|
+
42
|
|
676
|
+
],
|
|
677
|
+
mimeType: "image/tiff"
|
|
678
|
+
}],
|
|
679
|
+
pdf: [{
|
|
680
|
+
signature: [
|
|
681
|
+
37,
|
|
682
|
+
80,
|
|
683
|
+
68,
|
|
684
|
+
70,
|
|
685
|
+
45
|
|
686
|
+
],
|
|
687
|
+
mimeType: "application/pdf"
|
|
688
|
+
}],
|
|
689
|
+
zip: [
|
|
690
|
+
{
|
|
691
|
+
signature: [
|
|
692
|
+
80,
|
|
693
|
+
75,
|
|
694
|
+
3,
|
|
695
|
+
4
|
|
696
|
+
],
|
|
697
|
+
mimeType: "application/zip"
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
signature: [
|
|
701
|
+
80,
|
|
702
|
+
75,
|
|
703
|
+
5,
|
|
704
|
+
6
|
|
705
|
+
],
|
|
706
|
+
mimeType: "application/zip"
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
signature: [
|
|
710
|
+
80,
|
|
711
|
+
75,
|
|
712
|
+
7,
|
|
713
|
+
8
|
|
714
|
+
],
|
|
715
|
+
mimeType: "application/zip"
|
|
716
|
+
}
|
|
717
|
+
],
|
|
718
|
+
rar: [{
|
|
719
|
+
signature: [
|
|
720
|
+
82,
|
|
721
|
+
97,
|
|
722
|
+
114,
|
|
723
|
+
33,
|
|
724
|
+
26,
|
|
725
|
+
7
|
|
726
|
+
],
|
|
727
|
+
mimeType: "application/vnd.rar"
|
|
728
|
+
}],
|
|
729
|
+
"7z": [{
|
|
730
|
+
signature: [
|
|
731
|
+
55,
|
|
732
|
+
122,
|
|
733
|
+
188,
|
|
734
|
+
175,
|
|
735
|
+
39,
|
|
736
|
+
28
|
|
737
|
+
],
|
|
738
|
+
mimeType: "application/x-7z-compressed"
|
|
739
|
+
}],
|
|
740
|
+
tar: [{
|
|
741
|
+
signature: [
|
|
742
|
+
117,
|
|
743
|
+
115,
|
|
744
|
+
116,
|
|
745
|
+
97,
|
|
746
|
+
114
|
|
747
|
+
],
|
|
748
|
+
mimeType: "application/x-tar"
|
|
749
|
+
}],
|
|
750
|
+
gz: [{
|
|
751
|
+
signature: [31, 139],
|
|
752
|
+
mimeType: "application/gzip"
|
|
753
|
+
}],
|
|
754
|
+
tgz: [{
|
|
755
|
+
signature: [31, 139],
|
|
756
|
+
mimeType: "application/gzip"
|
|
757
|
+
}],
|
|
758
|
+
mp3: [
|
|
759
|
+
{
|
|
760
|
+
signature: [255, 251],
|
|
761
|
+
mimeType: "audio/mpeg"
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
signature: [255, 243],
|
|
765
|
+
mimeType: "audio/mpeg"
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
signature: [255, 242],
|
|
769
|
+
mimeType: "audio/mpeg"
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
signature: [
|
|
773
|
+
73,
|
|
774
|
+
68,
|
|
775
|
+
51
|
|
776
|
+
],
|
|
777
|
+
mimeType: "audio/mpeg"
|
|
778
|
+
}
|
|
779
|
+
],
|
|
780
|
+
wav: [{
|
|
781
|
+
signature: [
|
|
782
|
+
82,
|
|
783
|
+
73,
|
|
784
|
+
70,
|
|
785
|
+
70,
|
|
786
|
+
null,
|
|
787
|
+
null,
|
|
788
|
+
null,
|
|
789
|
+
null,
|
|
790
|
+
87,
|
|
791
|
+
65,
|
|
792
|
+
86,
|
|
793
|
+
69
|
|
794
|
+
],
|
|
795
|
+
mimeType: "audio/wav"
|
|
796
|
+
}],
|
|
797
|
+
ogg: [{
|
|
798
|
+
signature: [
|
|
799
|
+
79,
|
|
800
|
+
103,
|
|
801
|
+
103,
|
|
802
|
+
83
|
|
803
|
+
],
|
|
804
|
+
mimeType: "audio/ogg"
|
|
805
|
+
}],
|
|
806
|
+
flac: [{
|
|
807
|
+
signature: [
|
|
808
|
+
102,
|
|
809
|
+
76,
|
|
810
|
+
97,
|
|
811
|
+
67
|
|
812
|
+
],
|
|
813
|
+
mimeType: "audio/flac"
|
|
814
|
+
}],
|
|
815
|
+
mp4: [
|
|
816
|
+
{
|
|
817
|
+
signature: [
|
|
818
|
+
null,
|
|
819
|
+
null,
|
|
820
|
+
null,
|
|
821
|
+
null,
|
|
822
|
+
102,
|
|
823
|
+
116,
|
|
824
|
+
121,
|
|
825
|
+
112
|
|
826
|
+
],
|
|
827
|
+
mimeType: "video/mp4"
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
signature: [
|
|
831
|
+
null,
|
|
832
|
+
null,
|
|
833
|
+
null,
|
|
834
|
+
null,
|
|
835
|
+
102,
|
|
836
|
+
116,
|
|
837
|
+
121,
|
|
838
|
+
112,
|
|
839
|
+
105,
|
|
840
|
+
115,
|
|
841
|
+
111,
|
|
842
|
+
109
|
|
843
|
+
],
|
|
844
|
+
mimeType: "video/mp4"
|
|
845
|
+
},
|
|
846
|
+
{
|
|
847
|
+
signature: [
|
|
848
|
+
null,
|
|
849
|
+
null,
|
|
850
|
+
null,
|
|
851
|
+
null,
|
|
852
|
+
102,
|
|
853
|
+
116,
|
|
854
|
+
121,
|
|
855
|
+
112,
|
|
856
|
+
109,
|
|
857
|
+
112,
|
|
858
|
+
52,
|
|
859
|
+
50
|
|
860
|
+
],
|
|
861
|
+
mimeType: "video/mp4"
|
|
862
|
+
}
|
|
863
|
+
],
|
|
864
|
+
webm: [{
|
|
865
|
+
signature: [
|
|
866
|
+
26,
|
|
867
|
+
69,
|
|
868
|
+
223,
|
|
869
|
+
163
|
|
870
|
+
],
|
|
871
|
+
mimeType: "video/webm"
|
|
872
|
+
}],
|
|
873
|
+
avi: [{
|
|
874
|
+
signature: [
|
|
875
|
+
82,
|
|
876
|
+
73,
|
|
877
|
+
70,
|
|
878
|
+
70,
|
|
879
|
+
null,
|
|
880
|
+
null,
|
|
881
|
+
null,
|
|
882
|
+
null,
|
|
883
|
+
65,
|
|
884
|
+
86,
|
|
885
|
+
73,
|
|
886
|
+
32
|
|
887
|
+
],
|
|
888
|
+
mimeType: "video/x-msvideo"
|
|
889
|
+
}],
|
|
890
|
+
mov: [{
|
|
891
|
+
signature: [
|
|
892
|
+
null,
|
|
893
|
+
null,
|
|
894
|
+
null,
|
|
895
|
+
null,
|
|
896
|
+
102,
|
|
897
|
+
116,
|
|
898
|
+
121,
|
|
899
|
+
112,
|
|
900
|
+
113,
|
|
901
|
+
116,
|
|
902
|
+
32,
|
|
903
|
+
32
|
|
904
|
+
],
|
|
905
|
+
mimeType: "video/quicktime"
|
|
906
|
+
}],
|
|
907
|
+
mkv: [{
|
|
908
|
+
signature: [
|
|
909
|
+
26,
|
|
910
|
+
69,
|
|
911
|
+
223,
|
|
912
|
+
163
|
|
913
|
+
],
|
|
914
|
+
mimeType: "video/x-matroska"
|
|
915
|
+
}],
|
|
916
|
+
docx: [{
|
|
917
|
+
signature: [
|
|
918
|
+
80,
|
|
919
|
+
75,
|
|
920
|
+
3,
|
|
921
|
+
4
|
|
922
|
+
],
|
|
923
|
+
mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
924
|
+
}],
|
|
925
|
+
xlsx: [{
|
|
926
|
+
signature: [
|
|
927
|
+
80,
|
|
928
|
+
75,
|
|
929
|
+
3,
|
|
930
|
+
4
|
|
931
|
+
],
|
|
932
|
+
mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
|
933
|
+
}],
|
|
934
|
+
pptx: [{
|
|
935
|
+
signature: [
|
|
936
|
+
80,
|
|
937
|
+
75,
|
|
938
|
+
3,
|
|
939
|
+
4
|
|
940
|
+
],
|
|
941
|
+
mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
942
|
+
}],
|
|
943
|
+
doc: [{
|
|
944
|
+
signature: [
|
|
945
|
+
208,
|
|
946
|
+
207,
|
|
947
|
+
17,
|
|
948
|
+
224,
|
|
949
|
+
161,
|
|
950
|
+
177,
|
|
951
|
+
26,
|
|
952
|
+
225
|
|
953
|
+
],
|
|
954
|
+
mimeType: "application/msword"
|
|
955
|
+
}],
|
|
956
|
+
xls: [{
|
|
957
|
+
signature: [
|
|
958
|
+
208,
|
|
959
|
+
207,
|
|
960
|
+
17,
|
|
961
|
+
224,
|
|
962
|
+
161,
|
|
963
|
+
177,
|
|
964
|
+
26,
|
|
965
|
+
225
|
|
966
|
+
],
|
|
967
|
+
mimeType: "application/vnd.ms-excel"
|
|
968
|
+
}],
|
|
969
|
+
ppt: [{
|
|
970
|
+
signature: [
|
|
971
|
+
208,
|
|
972
|
+
207,
|
|
973
|
+
17,
|
|
974
|
+
224,
|
|
975
|
+
161,
|
|
976
|
+
177,
|
|
977
|
+
26,
|
|
978
|
+
225
|
|
979
|
+
],
|
|
980
|
+
mimeType: "application/vnd.ms-powerpoint"
|
|
981
|
+
}]
|
|
982
|
+
};
|
|
983
|
+
/**
|
|
984
|
+
* All possible format signatures for checking against actual file content
|
|
985
|
+
*/
|
|
986
|
+
static ALL_SIGNATURES = Object.entries(FileDetector.MAGIC_BYTES).flatMap(([ext, signatures]) => signatures.map((sig) => ({
|
|
987
|
+
ext,
|
|
988
|
+
...sig
|
|
989
|
+
})));
|
|
990
|
+
/**
|
|
991
|
+
* MIME type map for file extensions.
|
|
992
|
+
*
|
|
993
|
+
* Can be used to get the content type of file based on its extension.
|
|
994
|
+
* Feel free to add more mime types in your project!
|
|
995
|
+
*/
|
|
996
|
+
static mimeMap = {
|
|
997
|
+
json: "application/json",
|
|
998
|
+
txt: "text/plain",
|
|
999
|
+
html: "text/html",
|
|
1000
|
+
htm: "text/html",
|
|
1001
|
+
xml: "application/xml",
|
|
1002
|
+
csv: "text/csv",
|
|
1003
|
+
pdf: "application/pdf",
|
|
1004
|
+
md: "text/markdown",
|
|
1005
|
+
markdown: "text/markdown",
|
|
1006
|
+
rtf: "application/rtf",
|
|
1007
|
+
css: "text/css",
|
|
1008
|
+
js: "application/javascript",
|
|
1009
|
+
mjs: "application/javascript",
|
|
1010
|
+
ts: "application/typescript",
|
|
1011
|
+
jsx: "text/jsx",
|
|
1012
|
+
tsx: "text/tsx",
|
|
1013
|
+
zip: "application/zip",
|
|
1014
|
+
rar: "application/vnd.rar",
|
|
1015
|
+
"7z": "application/x-7z-compressed",
|
|
1016
|
+
tar: "application/x-tar",
|
|
1017
|
+
gz: "application/gzip",
|
|
1018
|
+
tgz: "application/gzip",
|
|
1019
|
+
png: "image/png",
|
|
1020
|
+
jpg: "image/jpeg",
|
|
1021
|
+
jpeg: "image/jpeg",
|
|
1022
|
+
gif: "image/gif",
|
|
1023
|
+
webp: "image/webp",
|
|
1024
|
+
svg: "image/svg+xml",
|
|
1025
|
+
bmp: "image/bmp",
|
|
1026
|
+
ico: "image/x-icon",
|
|
1027
|
+
tiff: "image/tiff",
|
|
1028
|
+
tif: "image/tiff",
|
|
1029
|
+
mp3: "audio/mpeg",
|
|
1030
|
+
wav: "audio/wav",
|
|
1031
|
+
ogg: "audio/ogg",
|
|
1032
|
+
m4a: "audio/mp4",
|
|
1033
|
+
aac: "audio/aac",
|
|
1034
|
+
flac: "audio/flac",
|
|
1035
|
+
mp4: "video/mp4",
|
|
1036
|
+
webm: "video/webm",
|
|
1037
|
+
avi: "video/x-msvideo",
|
|
1038
|
+
mov: "video/quicktime",
|
|
1039
|
+
wmv: "video/x-ms-wmv",
|
|
1040
|
+
flv: "video/x-flv",
|
|
1041
|
+
mkv: "video/x-matroska",
|
|
1042
|
+
doc: "application/msword",
|
|
1043
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1044
|
+
xls: "application/vnd.ms-excel",
|
|
1045
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
1046
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
1047
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
1048
|
+
woff: "font/woff",
|
|
1049
|
+
woff2: "font/woff2",
|
|
1050
|
+
ttf: "font/ttf",
|
|
1051
|
+
otf: "font/otf",
|
|
1052
|
+
eot: "application/vnd.ms-fontobject"
|
|
1053
|
+
};
|
|
1054
|
+
/**
|
|
1055
|
+
* Reverse MIME type map for looking up extensions from MIME types.
|
|
1056
|
+
* Prefers shorter, more common extensions when multiple exist.
|
|
1057
|
+
*/
|
|
1058
|
+
static reverseMimeMap = (() => {
|
|
1059
|
+
const reverse = {};
|
|
1060
|
+
for (const [ext, mimeType] of Object.entries(FileDetector.mimeMap)) if (!reverse[mimeType]) reverse[mimeType] = ext;
|
|
1061
|
+
return reverse;
|
|
1062
|
+
})();
|
|
1063
|
+
/**
|
|
1064
|
+
* Returns the file extension for a given MIME type.
|
|
1065
|
+
*
|
|
1066
|
+
* @param mimeType - The MIME type to look up
|
|
1067
|
+
* @returns The file extension (without dot), or "bin" if not found
|
|
1068
|
+
*
|
|
1069
|
+
* @example
|
|
1070
|
+
* ```typescript
|
|
1071
|
+
* const detector = alepha.inject(FileDetector);
|
|
1072
|
+
* const ext = detector.getExtensionFromMimeType("image/png"); // "png"
|
|
1073
|
+
* const ext2 = detector.getExtensionFromMimeType("application/octet-stream"); // "bin"
|
|
1074
|
+
* ```
|
|
1075
|
+
*/
|
|
1076
|
+
getExtensionFromMimeType(mimeType) {
|
|
1077
|
+
return FileDetector.reverseMimeMap[mimeType] || "bin";
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Returns the content type of file based on its filename.
|
|
1081
|
+
*
|
|
1082
|
+
* @param filename - The filename to check
|
|
1083
|
+
* @returns The MIME type
|
|
1084
|
+
*
|
|
1085
|
+
* @example
|
|
1086
|
+
* ```typescript
|
|
1087
|
+
* const detector = alepha.inject(FileDetector);
|
|
1088
|
+
* const mimeType = detector.getContentType("image.png"); // "image/png"
|
|
1089
|
+
* ```
|
|
1090
|
+
*/
|
|
1091
|
+
getContentType(filename) {
|
|
1092
|
+
const ext = filename.toLowerCase().split(".").pop() || "";
|
|
1093
|
+
return FileDetector.mimeMap[ext] || "application/octet-stream";
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* Detects the file type by checking magic bytes against the stream content.
|
|
1097
|
+
*
|
|
1098
|
+
* @param stream - The readable stream to check
|
|
1099
|
+
* @param filename - The filename (used to get the extension)
|
|
1100
|
+
* @returns File type information including MIME type, extension, and verification status
|
|
1101
|
+
*
|
|
1102
|
+
* @example
|
|
1103
|
+
* ```typescript
|
|
1104
|
+
* const detector = alepha.inject(FileDetector);
|
|
1105
|
+
* const stream = createReadStream('image.png');
|
|
1106
|
+
* const result = await detector.detectFileType(stream, 'image.png');
|
|
1107
|
+
* console.log(result.mimeType); // 'image/png'
|
|
1108
|
+
* console.log(result.verified); // true if magic bytes match
|
|
1109
|
+
* ```
|
|
1110
|
+
*/
|
|
1111
|
+
async detectFileType(stream, filename) {
|
|
1112
|
+
const expectedMimeType = this.getContentType(filename);
|
|
1113
|
+
const lastDotIndex = filename.lastIndexOf(".");
|
|
1114
|
+
const ext = lastDotIndex > 0 ? filename.substring(lastDotIndex + 1).toLowerCase() : "";
|
|
1115
|
+
const { buffer, stream: newStream } = await this.peekBytes(stream, 16);
|
|
1116
|
+
const expectedSignatures = FileDetector.MAGIC_BYTES[ext];
|
|
1117
|
+
if (expectedSignatures) {
|
|
1118
|
+
for (const { signature, mimeType } of expectedSignatures) if (this.matchesSignature(buffer, signature)) return {
|
|
1119
|
+
mimeType,
|
|
1120
|
+
extension: ext,
|
|
1121
|
+
verified: true,
|
|
1122
|
+
stream: newStream
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
for (const { ext: detectedExt, signature, mimeType } of FileDetector.ALL_SIGNATURES) if (detectedExt !== ext && this.matchesSignature(buffer, signature)) return {
|
|
1126
|
+
mimeType,
|
|
1127
|
+
extension: detectedExt,
|
|
1128
|
+
verified: true,
|
|
1129
|
+
stream: newStream
|
|
1130
|
+
};
|
|
1131
|
+
return {
|
|
1132
|
+
mimeType: expectedMimeType,
|
|
1133
|
+
extension: ext,
|
|
1134
|
+
verified: false,
|
|
1135
|
+
stream: newStream
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Reads all bytes from a stream and returns the first N bytes along with a new stream containing all data.
|
|
1140
|
+
* This approach reads the entire stream upfront to avoid complex async handling issues.
|
|
1141
|
+
*
|
|
1142
|
+
* @protected
|
|
1143
|
+
*/
|
|
1144
|
+
async peekBytes(stream, numBytes) {
|
|
1145
|
+
const chunks = [];
|
|
1146
|
+
for await (const chunk of stream) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1147
|
+
const allData = Buffer.concat(chunks);
|
|
1148
|
+
return {
|
|
1149
|
+
buffer: allData.subarray(0, numBytes),
|
|
1150
|
+
stream: Readable.from(allData)
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Checks if a buffer matches a magic byte signature.
|
|
1155
|
+
*
|
|
1156
|
+
* @protected
|
|
1157
|
+
*/
|
|
1158
|
+
matchesSignature(buffer, signature) {
|
|
1159
|
+
if (buffer.length < signature.length) return false;
|
|
1160
|
+
for (let i = 0; i < signature.length; i++) if (signature[i] !== null && buffer[i] !== signature[i]) return false;
|
|
1161
|
+
return true;
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
|
|
1165
|
+
//#endregion
|
|
1166
|
+
//#region ../../src/system/providers/NodeFileSystemProvider.ts
|
|
1167
|
+
/**
|
|
1168
|
+
* Node.js implementation of FileSystem interface.
|
|
1169
|
+
*
|
|
1170
|
+
* @example
|
|
1171
|
+
* ```typescript
|
|
1172
|
+
* const fs = alepha.inject(NodeFileSystemProvider);
|
|
1173
|
+
*
|
|
1174
|
+
* // Create from URL
|
|
1175
|
+
* const file1 = fs.createFile({ url: "file:///path/to/file.png" });
|
|
1176
|
+
*
|
|
1177
|
+
* // Create from Buffer
|
|
1178
|
+
* const file2 = fs.createFile({ buffer: Buffer.from("hello"), name: "hello.txt" });
|
|
1179
|
+
*
|
|
1180
|
+
* // Create from text
|
|
1181
|
+
* const file3 = fs.createFile({ text: "Hello, world!", name: "greeting.txt" });
|
|
1182
|
+
*
|
|
1183
|
+
* // File operations
|
|
1184
|
+
* await fs.mkdir("/tmp/mydir", { recursive: true });
|
|
1185
|
+
* await fs.cp("/src/file.txt", "/dest/file.txt");
|
|
1186
|
+
* await fs.mv("/old/path.txt", "/new/path.txt");
|
|
1187
|
+
* const files = await fs.ls("/tmp");
|
|
1188
|
+
* await fs.rm("/tmp/file.txt");
|
|
1189
|
+
* ```
|
|
1190
|
+
*/
|
|
1191
|
+
var NodeFileSystemProvider = class {
|
|
1192
|
+
detector = $inject(FileDetector);
|
|
1193
|
+
json = $inject(Json);
|
|
1194
|
+
join(...paths) {
|
|
1195
|
+
return join(...paths);
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Creates a FileLike object from various sources.
|
|
1199
|
+
*
|
|
1200
|
+
* @param options - Options for creating the file
|
|
1201
|
+
* @returns A FileLike object
|
|
1202
|
+
*
|
|
1203
|
+
* @example
|
|
1204
|
+
* ```typescript
|
|
1205
|
+
* const fs = alepha.inject(NodeFileSystemProvider);
|
|
1206
|
+
*
|
|
1207
|
+
* // From URL
|
|
1208
|
+
* const file1 = fs.createFile({ url: "https://example.com/image.png" });
|
|
1209
|
+
*
|
|
1210
|
+
* // From Buffer
|
|
1211
|
+
* const file2 = fs.createFile({
|
|
1212
|
+
* buffer: Buffer.from("hello"),
|
|
1213
|
+
* name: "hello.txt",
|
|
1214
|
+
* type: "text/plain"
|
|
1215
|
+
* });
|
|
1216
|
+
*
|
|
1217
|
+
* // From text
|
|
1218
|
+
* const file3 = fs.createFile({ text: "Hello!", name: "greeting.txt" });
|
|
1219
|
+
*
|
|
1220
|
+
* // From stream with detection
|
|
1221
|
+
* const stream = createReadStream("/path/to/file.png");
|
|
1222
|
+
* const file4 = fs.createFile({ stream, name: "image.png" });
|
|
1223
|
+
* ```
|
|
1224
|
+
*/
|
|
1225
|
+
createFile(options) {
|
|
1226
|
+
if ("path" in options) {
|
|
1227
|
+
const path = options.path;
|
|
1228
|
+
const filename = path.split("/").pop() || "file";
|
|
1229
|
+
return this.createFileFromUrl(`file://${path}`, {
|
|
1230
|
+
type: options.type,
|
|
1231
|
+
name: options.name || filename
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
if ("url" in options) return this.createFileFromUrl(options.url, {
|
|
1235
|
+
type: options.type,
|
|
1236
|
+
name: options.name
|
|
1237
|
+
});
|
|
1238
|
+
if ("response" in options) {
|
|
1239
|
+
if (!options.response.body) throw new AlephaError("Response has no body stream");
|
|
1240
|
+
const res = options.response;
|
|
1241
|
+
const sizeHeader = res.headers.get("content-length");
|
|
1242
|
+
const size = sizeHeader ? parseInt(sizeHeader, 10) : void 0;
|
|
1243
|
+
let name = options.name;
|
|
1244
|
+
const contentDisposition = res.headers.get("content-disposition");
|
|
1245
|
+
if (contentDisposition && !name) {
|
|
1246
|
+
const match = contentDisposition.match(/filename="?([^"]+)"?/);
|
|
1247
|
+
if (match) name = match[1];
|
|
1248
|
+
}
|
|
1249
|
+
const type = options.type || res.headers.get("content-type") || void 0;
|
|
1250
|
+
return this.createFileFromStream(options.response.body, {
|
|
1251
|
+
type,
|
|
1252
|
+
name,
|
|
1253
|
+
size
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
if ("file" in options) return this.createFileFromWebFile(options.file, {
|
|
1257
|
+
type: options.type,
|
|
1258
|
+
name: options.name,
|
|
1259
|
+
size: options.size
|
|
1260
|
+
});
|
|
1261
|
+
if ("buffer" in options) return this.createFileFromBuffer(options.buffer, {
|
|
1262
|
+
type: options.type,
|
|
1263
|
+
name: options.name
|
|
1264
|
+
});
|
|
1265
|
+
if ("arrayBuffer" in options) return this.createFileFromBuffer(Buffer.from(options.arrayBuffer), {
|
|
1266
|
+
type: options.type,
|
|
1267
|
+
name: options.name
|
|
1268
|
+
});
|
|
1269
|
+
if ("text" in options) return this.createFileFromBuffer(Buffer.from(options.text, "utf-8"), {
|
|
1270
|
+
type: options.type || "text/plain",
|
|
1271
|
+
name: options.name || "file.txt"
|
|
1272
|
+
});
|
|
1273
|
+
if ("stream" in options) return this.createFileFromStream(options.stream, {
|
|
1274
|
+
type: options.type,
|
|
1275
|
+
name: options.name,
|
|
1276
|
+
size: options.size
|
|
1277
|
+
});
|
|
1278
|
+
throw new AlephaError("Invalid createFile options: no valid source provided");
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Removes a file or directory.
|
|
1282
|
+
*
|
|
1283
|
+
* @param path - The path to remove
|
|
1284
|
+
* @param options - Remove options
|
|
1285
|
+
*
|
|
1286
|
+
* @example
|
|
1287
|
+
* ```typescript
|
|
1288
|
+
* const fs = alepha.inject(NodeFileSystemProvider);
|
|
1289
|
+
*
|
|
1290
|
+
* // Remove a file
|
|
1291
|
+
* await fs.rm("/tmp/file.txt");
|
|
1292
|
+
*
|
|
1293
|
+
* // Remove a directory recursively
|
|
1294
|
+
* await fs.rm("/tmp/mydir", { recursive: true });
|
|
1295
|
+
*
|
|
1296
|
+
* // Remove with force (no error if doesn't exist)
|
|
1297
|
+
* await fs.rm("/tmp/maybe-exists.txt", { force: true });
|
|
1298
|
+
* ```
|
|
1299
|
+
*/
|
|
1300
|
+
async rm(path, options) {
|
|
1301
|
+
await rm(path, options);
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Copies a file or directory.
|
|
1305
|
+
*
|
|
1306
|
+
* @param src - Source path
|
|
1307
|
+
* @param dest - Destination path
|
|
1308
|
+
* @param options - Copy options
|
|
1309
|
+
*
|
|
1310
|
+
* @example
|
|
1311
|
+
* ```typescript
|
|
1312
|
+
* const fs = alepha.inject(NodeFileSystemProvider);
|
|
1313
|
+
*
|
|
1314
|
+
* // Copy a file
|
|
1315
|
+
* await fs.cp("/src/file.txt", "/dest/file.txt");
|
|
1316
|
+
*
|
|
1317
|
+
* // Copy a directory recursively
|
|
1318
|
+
* await fs.cp("/src/dir", "/dest/dir", { recursive: true });
|
|
1319
|
+
*
|
|
1320
|
+
* // Copy with force (overwrite existing)
|
|
1321
|
+
* await fs.cp("/src/file.txt", "/dest/file.txt", { force: true });
|
|
1322
|
+
* ```
|
|
1323
|
+
*/
|
|
1324
|
+
async cp(src, dest, options) {
|
|
1325
|
+
if ((await stat(src)).isDirectory()) {
|
|
1326
|
+
if (!options?.recursive) throw new Error(`Cannot copy directory without recursive option: ${src}`);
|
|
1327
|
+
await cp(src, dest, {
|
|
1328
|
+
recursive: true,
|
|
1329
|
+
force: options?.force ?? false
|
|
1330
|
+
});
|
|
1331
|
+
} else await copyFile(src, dest);
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Moves/renames a file or directory.
|
|
1335
|
+
*
|
|
1336
|
+
* @param src - Source path
|
|
1337
|
+
* @param dest - Destination path
|
|
1338
|
+
*
|
|
1339
|
+
* @example
|
|
1340
|
+
* ```typescript
|
|
1341
|
+
* const fs = alepha.inject(NodeFileSystemProvider);
|
|
1342
|
+
*
|
|
1343
|
+
* // Move/rename a file
|
|
1344
|
+
* await fs.mv("/old/path.txt", "/new/path.txt");
|
|
1345
|
+
*
|
|
1346
|
+
* // Move a directory
|
|
1347
|
+
* await fs.mv("/old/dir", "/new/dir");
|
|
1348
|
+
* ```
|
|
1349
|
+
*/
|
|
1350
|
+
async mv(src, dest) {
|
|
1351
|
+
await rename(src, dest);
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Creates a directory.
|
|
1355
|
+
*
|
|
1356
|
+
* @param path - The directory path to create
|
|
1357
|
+
* @param options - Mkdir options
|
|
1358
|
+
*
|
|
1359
|
+
* @example
|
|
1360
|
+
* ```typescript
|
|
1361
|
+
* const fs = alepha.inject(NodeFileSystemProvider);
|
|
1362
|
+
*
|
|
1363
|
+
* // Create a directory
|
|
1364
|
+
* await fs.mkdir("/tmp/mydir");
|
|
1365
|
+
*
|
|
1366
|
+
* // Create nested directories
|
|
1367
|
+
* await fs.mkdir("/tmp/path/to/dir", { recursive: true });
|
|
1368
|
+
*
|
|
1369
|
+
* // Create with specific permissions
|
|
1370
|
+
* await fs.mkdir("/tmp/mydir", { mode: 0o755 });
|
|
1371
|
+
* ```
|
|
1372
|
+
*/
|
|
1373
|
+
async mkdir(path, options = {}) {
|
|
1374
|
+
const p = mkdir(path, {
|
|
1375
|
+
recursive: options.recursive ?? true,
|
|
1376
|
+
mode: options.mode
|
|
1377
|
+
});
|
|
1378
|
+
if (options.force === false) await p;
|
|
1379
|
+
else await p.catch(() => {});
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Lists files in a directory.
|
|
1383
|
+
*
|
|
1384
|
+
* @param path - The directory path to list
|
|
1385
|
+
* @param options - List options
|
|
1386
|
+
* @returns Array of filenames
|
|
1387
|
+
*
|
|
1388
|
+
* @example
|
|
1389
|
+
* ```typescript
|
|
1390
|
+
* const fs = alepha.inject(NodeFileSystemProvider);
|
|
1391
|
+
*
|
|
1392
|
+
* // List files in a directory
|
|
1393
|
+
* const files = await fs.ls("/tmp");
|
|
1394
|
+
* console.log(files); // ["file1.txt", "file2.txt", "subdir"]
|
|
1395
|
+
*
|
|
1396
|
+
* // List with hidden files
|
|
1397
|
+
* const allFiles = await fs.ls("/tmp", { hidden: true });
|
|
1398
|
+
*
|
|
1399
|
+
* // List recursively
|
|
1400
|
+
* const allFilesRecursive = await fs.ls("/tmp", { recursive: true });
|
|
1401
|
+
* ```
|
|
1402
|
+
*/
|
|
1403
|
+
async ls(path, options) {
|
|
1404
|
+
const entries = await readdir(path);
|
|
1405
|
+
const filteredEntries = options?.hidden ? entries : entries.filter((e) => !e.startsWith("."));
|
|
1406
|
+
if (options?.recursive) {
|
|
1407
|
+
const allFiles = [];
|
|
1408
|
+
for (const entry of filteredEntries) {
|
|
1409
|
+
const fullPath = join(path, entry);
|
|
1410
|
+
if ((await stat(fullPath)).isDirectory()) {
|
|
1411
|
+
allFiles.push(entry);
|
|
1412
|
+
const subFiles = await this.ls(fullPath, options);
|
|
1413
|
+
allFiles.push(...subFiles.map((f) => join(entry, f)));
|
|
1414
|
+
} else allFiles.push(entry);
|
|
1415
|
+
}
|
|
1416
|
+
return allFiles;
|
|
1417
|
+
}
|
|
1418
|
+
return filteredEntries;
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* Checks if a file or directory exists.
|
|
1422
|
+
*
|
|
1423
|
+
* @param path - The path to check
|
|
1424
|
+
* @returns True if the path exists, false otherwise
|
|
1425
|
+
*
|
|
1426
|
+
* @example
|
|
1427
|
+
* ```typescript
|
|
1428
|
+
* const fs = alepha.inject(NodeFileSystemProvider);
|
|
1429
|
+
*
|
|
1430
|
+
* if (await fs.exists("/tmp/file.txt")) {
|
|
1431
|
+
* console.log("File exists");
|
|
1432
|
+
* }
|
|
1433
|
+
* ```
|
|
1434
|
+
*/
|
|
1435
|
+
async exists(path) {
|
|
1436
|
+
try {
|
|
1437
|
+
await access(path);
|
|
1438
|
+
return true;
|
|
1439
|
+
} catch {
|
|
1440
|
+
return false;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Reads the content of a file.
|
|
1445
|
+
*
|
|
1446
|
+
* @param path - The file path to read
|
|
1447
|
+
* @returns The file content as a Buffer
|
|
1448
|
+
*
|
|
1449
|
+
* @example
|
|
1450
|
+
* ```typescript
|
|
1451
|
+
* const fs = alepha.inject(NodeFileSystemProvider);
|
|
1452
|
+
*
|
|
1453
|
+
* const buffer = await fs.readFile("/tmp/file.txt");
|
|
1454
|
+
* console.log(buffer.toString("utf-8"));
|
|
1455
|
+
* ```
|
|
1456
|
+
*/
|
|
1457
|
+
async readFile(path) {
|
|
1458
|
+
return await readFile(path);
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Writes data to a file.
|
|
1462
|
+
*
|
|
1463
|
+
* @param path - The file path to write to
|
|
1464
|
+
* @param data - The data to write (Buffer or string)
|
|
1465
|
+
*
|
|
1466
|
+
* @example
|
|
1467
|
+
* ```typescript
|
|
1468
|
+
* const fs = alepha.inject(NodeFileSystemProvider);
|
|
1469
|
+
*
|
|
1470
|
+
* // Write string
|
|
1471
|
+
* await fs.writeFile("/tmp/file.txt", "Hello, world!");
|
|
1472
|
+
*
|
|
1473
|
+
* // Write Buffer
|
|
1474
|
+
* await fs.writeFile("/tmp/file.bin", Buffer.from([0x01, 0x02, 0x03]));
|
|
1475
|
+
* ```
|
|
1476
|
+
*/
|
|
1477
|
+
async writeFile(path, data) {
|
|
1478
|
+
if (isFileLike(data)) {
|
|
1479
|
+
await writeFile(path, Readable.from(data.stream()));
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
await writeFile(path, data);
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* Reads the content of a file as a string.
|
|
1486
|
+
*
|
|
1487
|
+
* @param path - The file path to read
|
|
1488
|
+
* @returns The file content as a string
|
|
1489
|
+
*
|
|
1490
|
+
* @example
|
|
1491
|
+
* ```typescript
|
|
1492
|
+
* const fs = alepha.inject(NodeFileSystemProvider);
|
|
1493
|
+
* const content = await fs.readTextFile("/tmp/file.txt");
|
|
1494
|
+
* ```
|
|
1495
|
+
*/
|
|
1496
|
+
async readTextFile(path) {
|
|
1497
|
+
return (await this.readFile(path)).toString("utf-8");
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Reads the content of a file as JSON.
|
|
1501
|
+
*
|
|
1502
|
+
* @param path - The file path to read
|
|
1503
|
+
* @returns The parsed JSON content
|
|
1504
|
+
*
|
|
1505
|
+
* @example
|
|
1506
|
+
* ```typescript
|
|
1507
|
+
* const fs = alepha.inject(NodeFileSystemProvider);
|
|
1508
|
+
* const config = await fs.readJsonFile<{ name: string }>("/tmp/config.json");
|
|
1509
|
+
* ```
|
|
1510
|
+
*/
|
|
1511
|
+
async readJsonFile(path) {
|
|
1512
|
+
const text = await this.readTextFile(path);
|
|
1513
|
+
return this.json.parse(text);
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* Creates a FileLike object from a Web File.
|
|
1517
|
+
*
|
|
1518
|
+
* @protected
|
|
1519
|
+
*/
|
|
1520
|
+
createFileFromWebFile(source, options = {}) {
|
|
1521
|
+
const name = options.name ?? source.name;
|
|
1522
|
+
return {
|
|
1523
|
+
name,
|
|
1524
|
+
type: options.type ?? (source.type || this.detector.getContentType(name)),
|
|
1525
|
+
size: options.size ?? source.size ?? 0,
|
|
1526
|
+
lastModified: source.lastModified || Date.now(),
|
|
1527
|
+
stream: () => source.stream(),
|
|
1528
|
+
arrayBuffer: async () => {
|
|
1529
|
+
return await source.arrayBuffer();
|
|
1530
|
+
},
|
|
1531
|
+
text: async () => {
|
|
1532
|
+
return await source.text();
|
|
1533
|
+
}
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
/**
|
|
1537
|
+
* Creates a FileLike object from a Buffer.
|
|
1538
|
+
*
|
|
1539
|
+
* @protected
|
|
1540
|
+
*/
|
|
1541
|
+
createFileFromBuffer(source, options = {}) {
|
|
1542
|
+
const name = options.name ?? "file";
|
|
1543
|
+
return {
|
|
1544
|
+
name,
|
|
1545
|
+
type: options.type ?? this.detector.getContentType(options.name ?? name),
|
|
1546
|
+
size: source.byteLength,
|
|
1547
|
+
lastModified: Date.now(),
|
|
1548
|
+
stream: () => Readable.from(source),
|
|
1549
|
+
arrayBuffer: async () => {
|
|
1550
|
+
return this.bufferToArrayBuffer(source);
|
|
1551
|
+
},
|
|
1552
|
+
text: async () => {
|
|
1553
|
+
return source.toString("utf-8");
|
|
1554
|
+
}
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* Creates a FileLike object from a stream.
|
|
1559
|
+
*
|
|
1560
|
+
* @protected
|
|
1561
|
+
*/
|
|
1562
|
+
createFileFromStream(source, options = {}) {
|
|
1563
|
+
let buffer = null;
|
|
1564
|
+
return {
|
|
1565
|
+
name: options.name ?? "file",
|
|
1566
|
+
type: options.type ?? this.detector.getContentType(options.name ?? "file"),
|
|
1567
|
+
size: options.size ?? 0,
|
|
1568
|
+
lastModified: Date.now(),
|
|
1569
|
+
stream: () => source,
|
|
1570
|
+
_buffer: null,
|
|
1571
|
+
arrayBuffer: async () => {
|
|
1572
|
+
buffer ??= await this.streamToBuffer(source);
|
|
1573
|
+
return this.bufferToArrayBuffer(buffer);
|
|
1574
|
+
},
|
|
1575
|
+
text: async () => {
|
|
1576
|
+
buffer ??= await this.streamToBuffer(source);
|
|
1577
|
+
return buffer.toString("utf-8");
|
|
1578
|
+
}
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Creates a FileLike object from a URL.
|
|
1583
|
+
*
|
|
1584
|
+
* @protected
|
|
1585
|
+
*/
|
|
1586
|
+
createFileFromUrl(url, options = {}) {
|
|
1587
|
+
const parsedUrl = new URL(url);
|
|
1588
|
+
const filename = options.name || parsedUrl.pathname.split("/").pop() || "file";
|
|
1589
|
+
let buffer = null;
|
|
1590
|
+
return {
|
|
1591
|
+
name: filename,
|
|
1592
|
+
type: options.type ?? this.detector.getContentType(filename),
|
|
1593
|
+
size: 0,
|
|
1594
|
+
lastModified: Date.now(),
|
|
1595
|
+
stream: () => this.createStreamFromUrl(url),
|
|
1596
|
+
arrayBuffer: async () => {
|
|
1597
|
+
buffer ??= await this.loadFromUrl(url);
|
|
1598
|
+
return this.bufferToArrayBuffer(buffer);
|
|
1599
|
+
},
|
|
1600
|
+
text: async () => {
|
|
1601
|
+
buffer ??= await this.loadFromUrl(url);
|
|
1602
|
+
return buffer.toString("utf-8");
|
|
1603
|
+
},
|
|
1604
|
+
filepath: url
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
/**
|
|
1608
|
+
* Gets a streaming response from a URL.
|
|
1609
|
+
*
|
|
1610
|
+
* @protected
|
|
1611
|
+
*/
|
|
1612
|
+
getStreamingResponse(url) {
|
|
1613
|
+
const stream = new PassThrough();
|
|
1614
|
+
fetch(url).then((res) => Readable.fromWeb(res.body).pipe(stream)).catch((err) => stream.destroy(err));
|
|
1615
|
+
return stream;
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* Loads data from a URL.
|
|
1619
|
+
*
|
|
1620
|
+
* @protected
|
|
1621
|
+
*/
|
|
1622
|
+
async loadFromUrl(url) {
|
|
1623
|
+
const parsedUrl = new URL(url);
|
|
1624
|
+
if (parsedUrl.protocol === "file:") return await readFile(fileURLToPath(url));
|
|
1625
|
+
else if (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:") {
|
|
1626
|
+
const response = await fetch(url);
|
|
1627
|
+
if (!response.ok) throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
|
|
1628
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
1629
|
+
return Buffer.from(arrayBuffer);
|
|
1630
|
+
} else throw new Error(`Unsupported protocol: ${parsedUrl.protocol}`);
|
|
1631
|
+
}
|
|
1632
|
+
/**
|
|
1633
|
+
* Creates a stream from a URL.
|
|
1634
|
+
*
|
|
1635
|
+
* @protected
|
|
1636
|
+
*/
|
|
1637
|
+
createStreamFromUrl(url) {
|
|
1638
|
+
const parsedUrl = new URL(url);
|
|
1639
|
+
if (parsedUrl.protocol === "file:") return createReadStream(fileURLToPath(url));
|
|
1640
|
+
else if (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:") return this.getStreamingResponse(url);
|
|
1641
|
+
else throw new AlephaError(`Unsupported protocol: ${parsedUrl.protocol}`);
|
|
1642
|
+
}
|
|
1643
|
+
/**
|
|
1644
|
+
* Converts a stream-like object to a Buffer.
|
|
1645
|
+
*
|
|
1646
|
+
* @protected
|
|
1647
|
+
*/
|
|
1648
|
+
async streamToBuffer(streamLike) {
|
|
1649
|
+
const stream = streamLike instanceof Readable ? streamLike : Readable.fromWeb(streamLike);
|
|
1650
|
+
return new Promise((resolve, reject) => {
|
|
1651
|
+
const buffer = [];
|
|
1652
|
+
stream.on("data", (chunk) => buffer.push(Buffer.from(chunk)));
|
|
1653
|
+
stream.on("end", () => resolve(Buffer.concat(buffer)));
|
|
1654
|
+
stream.on("error", (err) => reject(new AlephaError("Error converting stream", { cause: err })));
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
/**
|
|
1658
|
+
* Converts a Node.js Buffer to an ArrayBuffer.
|
|
1659
|
+
*
|
|
1660
|
+
* @protected
|
|
1661
|
+
*/
|
|
1662
|
+
bufferToArrayBuffer(buffer) {
|
|
1663
|
+
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
1664
|
+
}
|
|
1665
|
+
};
|
|
1666
|
+
|
|
1667
|
+
//#endregion
|
|
1668
|
+
//#region ../../src/system/providers/NodeShellProvider.ts
|
|
1669
|
+
/**
|
|
1670
|
+
* Node.js implementation of ShellProvider.
|
|
1671
|
+
*
|
|
1672
|
+
* Executes shell commands using Node.js child_process module.
|
|
1673
|
+
* Supports binary resolution from node_modules/.bin for local packages.
|
|
1674
|
+
*/
|
|
1675
|
+
var NodeShellProvider = class {
|
|
1676
|
+
log = $logger();
|
|
1677
|
+
fs = $inject(FileSystemProvider);
|
|
1678
|
+
/**
|
|
1679
|
+
* Run a shell command or binary.
|
|
1680
|
+
*/
|
|
1681
|
+
async run(command, options = {}) {
|
|
1682
|
+
const { resolve = false, capture = false, root, env } = options;
|
|
1683
|
+
const cwd = root ?? process.cwd();
|
|
1684
|
+
this.log.debug(`Shell: ${command}`, {
|
|
1685
|
+
cwd,
|
|
1686
|
+
resolve,
|
|
1687
|
+
capture
|
|
1688
|
+
});
|
|
1689
|
+
let executable;
|
|
1690
|
+
let args;
|
|
1691
|
+
if (resolve) {
|
|
1692
|
+
const [bin, ...rest] = command.split(" ");
|
|
1693
|
+
executable = await this.resolveExecutable(bin, cwd);
|
|
1694
|
+
args = rest;
|
|
1695
|
+
} else [executable, ...args] = command.split(" ");
|
|
1696
|
+
if (capture) return this.execCapture(command, {
|
|
1697
|
+
cwd,
|
|
1698
|
+
env
|
|
1699
|
+
});
|
|
1700
|
+
return this.execInherit(executable, args, {
|
|
1701
|
+
cwd,
|
|
1702
|
+
env
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
/**
|
|
1706
|
+
* Execute command with inherited stdio (streams to terminal).
|
|
1707
|
+
*/
|
|
1708
|
+
async execInherit(executable, args, options) {
|
|
1709
|
+
const proc = spawn(executable, args, {
|
|
1710
|
+
stdio: "inherit",
|
|
1711
|
+
cwd: options.cwd,
|
|
1712
|
+
env: {
|
|
1713
|
+
...process.env,
|
|
1714
|
+
...options.env
|
|
1715
|
+
}
|
|
1716
|
+
});
|
|
1717
|
+
return new Promise((resolve, reject) => {
|
|
1718
|
+
proc.on("exit", (code) => {
|
|
1719
|
+
if (code === 0 || code === null) resolve("");
|
|
1720
|
+
else reject(new AlephaError(`Command exited with code ${code}`));
|
|
1721
|
+
});
|
|
1722
|
+
proc.on("error", reject);
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
/**
|
|
1726
|
+
* Execute command and capture stdout.
|
|
1727
|
+
*/
|
|
1728
|
+
execCapture(command, options) {
|
|
1729
|
+
return new Promise((resolve, reject) => {
|
|
1730
|
+
exec(command, {
|
|
1731
|
+
cwd: options.cwd,
|
|
1732
|
+
env: {
|
|
1733
|
+
...process.env,
|
|
1734
|
+
LOG_FORMAT: "pretty",
|
|
1735
|
+
...options.env
|
|
1736
|
+
}
|
|
1737
|
+
}, (err, stdout) => {
|
|
1738
|
+
if (err) {
|
|
1739
|
+
err.stdout = stdout;
|
|
1740
|
+
reject(err);
|
|
1741
|
+
} else resolve(stdout);
|
|
1742
|
+
});
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1746
|
+
* Resolve executable path from node_modules/.bin.
|
|
1747
|
+
*
|
|
1748
|
+
* Search order:
|
|
1749
|
+
* 1. Local: node_modules/.bin/
|
|
1750
|
+
* 2. Pnpm nested: node_modules/alepha/node_modules/.bin/
|
|
1751
|
+
* 3. Monorepo: Walk up to 3 parent directories
|
|
1752
|
+
*/
|
|
1753
|
+
async resolveExecutable(name, root) {
|
|
1754
|
+
const suffix = process.platform === "win32" ? ".cmd" : "";
|
|
1755
|
+
let execPath = await this.findExecutable(root, `node_modules/.bin/${name}${suffix}`);
|
|
1756
|
+
if (!execPath) execPath = await this.findExecutable(root, `node_modules/alepha/node_modules/.bin/${name}${suffix}`);
|
|
1757
|
+
if (!execPath) {
|
|
1758
|
+
let parentDir = this.fs.join(root, "..");
|
|
1759
|
+
for (let i = 0; i < 3; i++) {
|
|
1760
|
+
execPath = await this.findExecutable(parentDir, `node_modules/.bin/${name}${suffix}`);
|
|
1761
|
+
if (execPath) break;
|
|
1762
|
+
parentDir = this.fs.join(parentDir, "..");
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
if (!execPath) throw new AlephaError(`Could not find executable for '${name}'. Make sure the package is installed.`);
|
|
1766
|
+
return execPath;
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* Check if executable exists at path.
|
|
1770
|
+
*/
|
|
1771
|
+
async findExecutable(root, relativePath) {
|
|
1772
|
+
const fullPath = this.fs.join(root, relativePath);
|
|
1773
|
+
if (await this.fs.exists(fullPath)) return fullPath;
|
|
1774
|
+
}
|
|
1775
|
+
/**
|
|
1776
|
+
* Check if a command is installed and available in the system PATH.
|
|
1777
|
+
*/
|
|
1778
|
+
isInstalled(command) {
|
|
1779
|
+
return new Promise((resolve) => {
|
|
1780
|
+
exec(process.platform === "win32" ? `where ${command}` : `command -v ${command}`, (error) => resolve(!error));
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
};
|
|
1784
|
+
|
|
1785
|
+
//#endregion
|
|
1786
|
+
//#region ../../src/system/providers/ShellProvider.ts
|
|
1787
|
+
/**
|
|
1788
|
+
* Abstract provider for executing shell commands and binaries.
|
|
1789
|
+
*
|
|
1790
|
+
* Implementations:
|
|
1791
|
+
* - `NodeShellProvider` - Real shell execution using Node.js child_process
|
|
1792
|
+
* - `MemoryShellProvider` - In-memory mock for testing
|
|
1793
|
+
*
|
|
1794
|
+
* @example
|
|
1795
|
+
* ```typescript
|
|
1796
|
+
* class MyService {
|
|
1797
|
+
* protected readonly shell = $inject(ShellProvider);
|
|
1798
|
+
*
|
|
1799
|
+
* async build() {
|
|
1800
|
+
* // Run shell command directly
|
|
1801
|
+
* await this.shell.run("yarn install");
|
|
1802
|
+
*
|
|
1803
|
+
* // Run local binary with resolution
|
|
1804
|
+
* await this.shell.run("vite build", { resolve: true });
|
|
1805
|
+
*
|
|
1806
|
+
* // Capture output
|
|
1807
|
+
* const output = await this.shell.run("echo hello", { capture: true });
|
|
1808
|
+
* }
|
|
1809
|
+
* }
|
|
1810
|
+
* ```
|
|
1811
|
+
*/
|
|
1812
|
+
var ShellProvider = class {};
|
|
1813
|
+
|
|
1814
|
+
//#endregion
|
|
1815
|
+
//#region ../../src/system/index.ts
|
|
1816
|
+
/**
|
|
1817
|
+
* | type | quality | stability |
|
|
1818
|
+
* |------|---------|-----------|
|
|
1819
|
+
* | tooling | standard | stable |
|
|
1820
|
+
*
|
|
1821
|
+
* System-level abstractions for portable code across runtimes.
|
|
1822
|
+
*
|
|
1823
|
+
* **Features:**
|
|
1824
|
+
* - File system operations (read, write, exists, etc.)
|
|
1825
|
+
* - Shell command execution
|
|
1826
|
+
* - File type detection and MIME utilities
|
|
1827
|
+
* - Memory implementations for testing
|
|
1828
|
+
*
|
|
1829
|
+
* @module alepha.system
|
|
1830
|
+
*/
|
|
1831
|
+
const AlephaSystem = $module({
|
|
1832
|
+
name: "alepha.system",
|
|
1833
|
+
primitives: [],
|
|
1834
|
+
services: [
|
|
1835
|
+
FileDetector,
|
|
1836
|
+
FileSystemProvider,
|
|
1837
|
+
MemoryFileSystemProvider,
|
|
1838
|
+
NodeFileSystemProvider,
|
|
1839
|
+
ShellProvider,
|
|
1840
|
+
MemoryShellProvider,
|
|
1841
|
+
NodeShellProvider
|
|
1842
|
+
],
|
|
1843
|
+
register: (alepha) => alepha.with({
|
|
1844
|
+
optional: true,
|
|
1845
|
+
provide: FileSystemProvider,
|
|
1846
|
+
use: NodeFileSystemProvider
|
|
1847
|
+
}).with({
|
|
1848
|
+
optional: true,
|
|
1849
|
+
provide: ShellProvider,
|
|
1850
|
+
use: alepha.isTest() ? MemoryShellProvider : NodeShellProvider
|
|
1851
|
+
})
|
|
1852
|
+
});
|
|
1853
|
+
|
|
1854
|
+
//#endregion
|
|
14
1855
|
//#region ../../src/core/constants/KIND.ts
|
|
15
1856
|
/**
|
|
16
1857
|
* Used for identifying primitives.
|
|
@@ -76,13 +1917,24 @@ $atom$1[KIND] = "atom";
|
|
|
76
1917
|
* Build options atom for CLI build command.
|
|
77
1918
|
*
|
|
78
1919
|
* Defines the available build configuration options with their defaults.
|
|
79
|
-
* Options can be overridden via
|
|
1920
|
+
* Options can be overridden via alepha.config.ts or CLI flags.
|
|
80
1921
|
*/
|
|
81
1922
|
const buildOptions = $atom$1({
|
|
82
1923
|
name: "alepha.cli.build.options",
|
|
83
1924
|
description: "Build configuration options",
|
|
84
1925
|
schema: t.object({
|
|
85
1926
|
stats: t.optional(t.boolean({ default: false })),
|
|
1927
|
+
target: t.optional(t.enum([
|
|
1928
|
+
"bare",
|
|
1929
|
+
"docker",
|
|
1930
|
+
"vercel",
|
|
1931
|
+
"cloudflare"
|
|
1932
|
+
])),
|
|
1933
|
+
runtime: t.optional(t.enum([
|
|
1934
|
+
"node",
|
|
1935
|
+
"bun",
|
|
1936
|
+
"workerd"
|
|
1937
|
+
])),
|
|
86
1938
|
vercel: t.optional(t.object({
|
|
87
1939
|
projectName: t.optional(t.string()),
|
|
88
1940
|
orgId: t.optional(t.string()),
|
|
@@ -94,8 +1946,13 @@ const buildOptions = $atom$1({
|
|
|
94
1946
|
})),
|
|
95
1947
|
cloudflare: t.optional(t.object({ config: t.optional(t.json()) })),
|
|
96
1948
|
docker: t.optional(t.object({
|
|
97
|
-
|
|
98
|
-
command: t.optional(t.string(
|
|
1949
|
+
from: t.optional(t.string()),
|
|
1950
|
+
command: t.optional(t.string()),
|
|
1951
|
+
image: t.optional(t.object({
|
|
1952
|
+
tag: t.string(),
|
|
1953
|
+
args: t.optional(t.string()),
|
|
1954
|
+
oci: t.optional(t.boolean())
|
|
1955
|
+
}))
|
|
99
1956
|
})),
|
|
100
1957
|
sitemap: t.optional(t.object({ hostname: t.string() }))
|
|
101
1958
|
}),
|
|
@@ -191,9 +2048,10 @@ var AppEntryProvider = class {
|
|
|
191
2048
|
};
|
|
192
2049
|
|
|
193
2050
|
//#endregion
|
|
194
|
-
//#region ../../src/cli/
|
|
195
|
-
var
|
|
2051
|
+
//#region ../../src/cli/services/ViteUtils.ts
|
|
2052
|
+
var ViteUtils = class {
|
|
196
2053
|
fs = $inject(FileSystemProvider);
|
|
2054
|
+
viteDevServer;
|
|
197
2055
|
generateIndexHtml(entry) {
|
|
198
2056
|
const style = entry.style;
|
|
199
2057
|
const browser = entry.browser ?? entry.server;
|
|
@@ -213,15 +2071,6 @@ ${style ? `<link rel="stylesheet" href="/${style}" />` : ""}
|
|
|
213
2071
|
</html>
|
|
214
2072
|
`.trim();
|
|
215
2073
|
}
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
//#endregion
|
|
219
|
-
//#region ../../src/cli/providers/ViteBuildProvider.ts
|
|
220
|
-
var ViteBuildProvider = class {
|
|
221
|
-
alepha;
|
|
222
|
-
appEntry;
|
|
223
|
-
viteDevServer;
|
|
224
|
-
templateProvider = $inject(ViteTemplateProvider);
|
|
225
2074
|
/**
|
|
226
2075
|
* We need to close the Vite dev server after build is done.
|
|
227
2076
|
*/
|
|
@@ -238,10 +2087,10 @@ var ViteBuildProvider = class {
|
|
|
238
2087
|
await this.viteDevServer?.close();
|
|
239
2088
|
}
|
|
240
2089
|
});
|
|
241
|
-
async
|
|
2090
|
+
async runAlepha(opts) {
|
|
242
2091
|
const { createServer } = await importVite();
|
|
2092
|
+
process.env.NODE_ENV = opts.mode;
|
|
243
2093
|
process.env.ALEPHA_CLI_IMPORT = "true";
|
|
244
|
-
process.env.NODE_ENV = "production";
|
|
245
2094
|
process.env.LOG_LEVEL ??= "warn";
|
|
246
2095
|
/**
|
|
247
2096
|
* 01/26 Vite 7
|
|
@@ -257,6 +2106,21 @@ var ViteBuildProvider = class {
|
|
|
257
2106
|
await this.viteDevServer.ssrLoadModule(opts.entry.server);
|
|
258
2107
|
const alepha = globalThis.__alepha;
|
|
259
2108
|
if (!alepha) throw new AlephaError("Alepha instance not found after loading entry module");
|
|
2109
|
+
return alepha;
|
|
2110
|
+
}
|
|
2111
|
+
};
|
|
2112
|
+
|
|
2113
|
+
//#endregion
|
|
2114
|
+
//#region ../../src/cli/providers/ViteBuildProvider.ts
|
|
2115
|
+
var ViteBuildProvider = class {
|
|
2116
|
+
alepha;
|
|
2117
|
+
appEntry;
|
|
2118
|
+
viteUtils = $inject(ViteUtils);
|
|
2119
|
+
async init(opts) {
|
|
2120
|
+
const alepha = await this.viteUtils.runAlepha({
|
|
2121
|
+
entry: opts.entry,
|
|
2122
|
+
mode: "production"
|
|
2123
|
+
});
|
|
260
2124
|
this.alepha = alepha;
|
|
261
2125
|
this.appEntry = opts.entry;
|
|
262
2126
|
return alepha;
|
|
@@ -272,7 +2136,7 @@ var ViteBuildProvider = class {
|
|
|
272
2136
|
}
|
|
273
2137
|
generateIndexHtml() {
|
|
274
2138
|
if (!this.appEntry) throw new AlephaError("ViteBuildProvider not initialized");
|
|
275
|
-
return this.
|
|
2139
|
+
return this.viteUtils.generateIndexHtml(this.appEntry);
|
|
276
2140
|
}
|
|
277
2141
|
};
|
|
278
2142
|
|
|
@@ -292,44 +2156,23 @@ var AlephaCliUtils = class {
|
|
|
292
2156
|
fs = $inject(FileSystemProvider);
|
|
293
2157
|
envUtils = $inject(EnvUtils);
|
|
294
2158
|
boot = $inject(AppEntryProvider);
|
|
2159
|
+
shell = $inject(ShellProvider);
|
|
2160
|
+
viteUtils = $inject(ViteUtils);
|
|
295
2161
|
/**
|
|
296
2162
|
* Execute a command with inherited stdio.
|
|
2163
|
+
*
|
|
2164
|
+
* @param command - The command to execute
|
|
2165
|
+
* @param options.root - Working directory
|
|
2166
|
+
* @param options.env - Additional environment variables
|
|
2167
|
+
* @param options.global - If true, run command directly without resolving from node_modules
|
|
297
2168
|
*/
|
|
298
2169
|
async exec(command, options = {}) {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
env: {
|
|
306
|
-
...process.env,
|
|
307
|
-
...options.env
|
|
308
|
-
}
|
|
309
|
-
});
|
|
310
|
-
await new Promise((resolve) => prog.on("exit", () => {
|
|
311
|
-
resolve();
|
|
312
|
-
}));
|
|
313
|
-
};
|
|
314
|
-
if (options.global) {
|
|
315
|
-
const [app, ...args] = command.split(" ");
|
|
316
|
-
await runExec(app, args);
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
const suffix = process.platform === "win32" ? ".cmd" : "";
|
|
320
|
-
const [app, ...args] = command.split(" ");
|
|
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
|
-
}
|
|
331
|
-
if (!execPath) throw new AlephaError(`Could not find executable for command '${app}'. Make sure the package is installed.`);
|
|
332
|
-
await runExec(execPath, args);
|
|
2170
|
+
await this.shell.run(command, {
|
|
2171
|
+
root: options.root,
|
|
2172
|
+
env: options.env,
|
|
2173
|
+
resolve: !options.global,
|
|
2174
|
+
capture: false
|
|
2175
|
+
});
|
|
333
2176
|
}
|
|
334
2177
|
/**
|
|
335
2178
|
* Write a configuration file to node_modules/.alepha directory.
|
|
@@ -342,33 +2185,14 @@ var AlephaCliUtils = class {
|
|
|
342
2185
|
this.log.debug(`Config file written: ${path}`);
|
|
343
2186
|
return path;
|
|
344
2187
|
}
|
|
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();
|
|
2188
|
+
async loadAlephaFromServerEntryFile(opts) {
|
|
351
2189
|
let entry;
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
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}`);
|
|
2190
|
+
if ("root" in opts) entry = await this.boot.getAppEntry(opts.root);
|
|
2191
|
+
else entry = opts.entry;
|
|
2192
|
+
return await this.viteUtils.runAlepha({
|
|
2193
|
+
entry,
|
|
2194
|
+
mode: opts.mode
|
|
2195
|
+
});
|
|
372
2196
|
}
|
|
373
2197
|
/**
|
|
374
2198
|
* Generate JavaScript code for Drizzle entities export.
|
|
@@ -396,12 +2220,52 @@ ${models.map((it) => `export const ${it} = models["${it}"];`).join("\n")}
|
|
|
396
2220
|
async exists(root, path) {
|
|
397
2221
|
return this.fs.exists(this.fs.join(root, path));
|
|
398
2222
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
2223
|
+
/**
|
|
2224
|
+
* Check if a command is installed and available in the system PATH.
|
|
2225
|
+
*/
|
|
2226
|
+
isInstalledAsync(cmd) {
|
|
2227
|
+
return this.shell.isInstalled(cmd);
|
|
2228
|
+
}
|
|
2229
|
+
/**
|
|
2230
|
+
* Get the current git revision (commit SHA).
|
|
2231
|
+
*
|
|
2232
|
+
* @returns The short commit SHA or "unknown" if not in a git repo
|
|
2233
|
+
*/
|
|
2234
|
+
async getGitRevision() {
|
|
2235
|
+
try {
|
|
2236
|
+
return (await this.shell.run("git rev-parse --short HEAD", { capture: true })).trim();
|
|
2237
|
+
} catch {
|
|
2238
|
+
return "unknown";
|
|
2239
|
+
}
|
|
402
2240
|
}
|
|
403
2241
|
};
|
|
404
2242
|
|
|
2243
|
+
//#endregion
|
|
2244
|
+
//#region ../../package.json
|
|
2245
|
+
var devDependencies = {
|
|
2246
|
+
"@biomejs/biome": "^2.3.13",
|
|
2247
|
+
"@electric-sql/pglite": "^0.3.15",
|
|
2248
|
+
"@faker-js/faker": "^10.2.0",
|
|
2249
|
+
"@testing-library/dom": "^10.4.1",
|
|
2250
|
+
"@testing-library/react": "^16.3.2",
|
|
2251
|
+
"@types/node": "^25.1.0",
|
|
2252
|
+
"@types/nodemailer": "^7.0.9",
|
|
2253
|
+
"@types/react": "^19.2.10",
|
|
2254
|
+
"@types/react-dom": "^19.2.3",
|
|
2255
|
+
"@types/ws": "^8.18.1",
|
|
2256
|
+
"@vitejs/plugin-react": "^5.1.2",
|
|
2257
|
+
"cron-schedule": "^6.0.0",
|
|
2258
|
+
"jose": "^6.1.3",
|
|
2259
|
+
"jsdom": "^27.4.0",
|
|
2260
|
+
"openid-client": "^6.8.1",
|
|
2261
|
+
"prom-client": "^15.1.3",
|
|
2262
|
+
"react": "^19.2.4",
|
|
2263
|
+
"react-dom": "^19.2.4",
|
|
2264
|
+
"swagger-ui-dist": "^5.31.0",
|
|
2265
|
+
"tsdown": "^0.20.1",
|
|
2266
|
+
"vitest": "^4.0.18"
|
|
2267
|
+
};
|
|
2268
|
+
|
|
405
2269
|
//#endregion
|
|
406
2270
|
//#region ../../src/cli/version.ts
|
|
407
2271
|
const packageJson = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf-8"));
|
|
@@ -425,11 +2289,8 @@ var PackageManagerUtils = class {
|
|
|
425
2289
|
/**
|
|
426
2290
|
* Detect the package manager used in the project.
|
|
427
2291
|
*/
|
|
428
|
-
async getPackageManager(root,
|
|
429
|
-
if (
|
|
430
|
-
if (flags?.pnpm) return "pnpm";
|
|
431
|
-
if (flags?.npm) return "npm";
|
|
432
|
-
if (flags?.bun) return "bun";
|
|
2292
|
+
async getPackageManager(root, pm) {
|
|
2293
|
+
if (pm) return pm;
|
|
433
2294
|
if (this.alepha.isBun()) return "bun";
|
|
434
2295
|
if (await this.fs.exists(this.fs.join(root, "bun.lock"))) return "bun";
|
|
435
2296
|
if (await this.fs.exists(this.fs.join(root, "yarn.lock"))) return "yarn";
|
|
@@ -437,6 +2298,46 @@ var PackageManagerUtils = class {
|
|
|
437
2298
|
return "npm";
|
|
438
2299
|
}
|
|
439
2300
|
/**
|
|
2301
|
+
* Detect workspace context when inside a monorepo package.
|
|
2302
|
+
*
|
|
2303
|
+
* Checks if we're inside a workspace package (e.g., packages/my-pkg or apps/my-app)
|
|
2304
|
+
* by looking 2 levels up for workspace indicators like lockfiles and config files.
|
|
2305
|
+
*
|
|
2306
|
+
* @param root - The current package directory
|
|
2307
|
+
* @returns Workspace context with root path, PM, and config presence
|
|
2308
|
+
*/
|
|
2309
|
+
async getWorkspaceContext(root) {
|
|
2310
|
+
const workspaceRoot = this.fs.join(root, "..", "..");
|
|
2311
|
+
const [hasYarnLock, hasPnpmLock, hasNpmLock, hasBunLock] = await Promise.all([
|
|
2312
|
+
this.fs.exists(this.fs.join(workspaceRoot, "yarn.lock")),
|
|
2313
|
+
this.fs.exists(this.fs.join(workspaceRoot, "pnpm-lock.yaml")),
|
|
2314
|
+
this.fs.exists(this.fs.join(workspaceRoot, "package-lock.json")),
|
|
2315
|
+
this.fs.exists(this.fs.join(workspaceRoot, "bun.lock"))
|
|
2316
|
+
]);
|
|
2317
|
+
const [hasBiome, hasEditorConfig, hasTsConfig, hasWorkspacePackageJson] = await Promise.all([
|
|
2318
|
+
this.fs.exists(this.fs.join(workspaceRoot, "biome.json")),
|
|
2319
|
+
this.fs.exists(this.fs.join(workspaceRoot, ".editorconfig")),
|
|
2320
|
+
this.fs.exists(this.fs.join(workspaceRoot, "tsconfig.json")),
|
|
2321
|
+
this.fs.exists(this.fs.join(workspaceRoot, "package.json"))
|
|
2322
|
+
]);
|
|
2323
|
+
const isPackage = (hasYarnLock || hasPnpmLock || hasNpmLock || hasBunLock) && hasWorkspacePackageJson;
|
|
2324
|
+
let packageManager = null;
|
|
2325
|
+
if (hasYarnLock) packageManager = "yarn";
|
|
2326
|
+
else if (hasPnpmLock) packageManager = "pnpm";
|
|
2327
|
+
else if (hasBunLock) packageManager = "bun";
|
|
2328
|
+
else if (hasNpmLock) packageManager = "npm";
|
|
2329
|
+
return {
|
|
2330
|
+
isPackage,
|
|
2331
|
+
workspaceRoot: isPackage ? workspaceRoot : null,
|
|
2332
|
+
packageManager,
|
|
2333
|
+
config: {
|
|
2334
|
+
biomeJson: hasBiome,
|
|
2335
|
+
editorconfig: hasEditorConfig,
|
|
2336
|
+
tsconfigJson: hasTsConfig
|
|
2337
|
+
}
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
/**
|
|
440
2341
|
* Get the install command for a package.
|
|
441
2342
|
*/
|
|
442
2343
|
async getInstallCommand(root, packageName, dev = true) {
|
|
@@ -481,13 +2382,23 @@ var PackageManagerUtils = class {
|
|
|
481
2382
|
}
|
|
482
2383
|
/**
|
|
483
2384
|
* Install a dependency if it's missing from the project.
|
|
2385
|
+
* Optionally checks workspace root for the dependency in monorepo setups.
|
|
484
2386
|
*/
|
|
485
2387
|
async ensureDependency(root, packageName, options = {}) {
|
|
486
|
-
const { dev = true } = options;
|
|
2388
|
+
const { dev = true, checkWorkspace = false } = options;
|
|
487
2389
|
if (await this.hasDependency(root, packageName)) {
|
|
488
2390
|
this.log.debug(`Dependency '${packageName}' is already installed`);
|
|
489
2391
|
return;
|
|
490
2392
|
}
|
|
2393
|
+
if (checkWorkspace) {
|
|
2394
|
+
const workspace = await this.getWorkspaceContext(root);
|
|
2395
|
+
if (workspace.workspaceRoot) {
|
|
2396
|
+
if (await this.hasDependency(workspace.workspaceRoot, packageName)) {
|
|
2397
|
+
this.log.debug(`Dependency '${packageName}' is already installed in workspace root`);
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
491
2402
|
const cmd = await this.getInstallCommand(root, packageName, dev);
|
|
492
2403
|
if (options.run) await options.run(cmd, {
|
|
493
2404
|
alias: `add ${packageName}`,
|
|
@@ -578,122 +2489,61 @@ var PackageManagerUtils = class {
|
|
|
578
2489
|
return packageJson;
|
|
579
2490
|
}
|
|
580
2491
|
generatePackageJsonContent(modes) {
|
|
2492
|
+
const alephaDeps = devDependencies;
|
|
581
2493
|
const dependencies = { alepha: `^${version}` };
|
|
582
|
-
const devDependencies = {};
|
|
2494
|
+
const devDependencies$1 = {};
|
|
2495
|
+
if (!modes.isPackage) {
|
|
2496
|
+
devDependencies$1["@biomejs/biome"] = alephaDeps["@biomejs/biome"];
|
|
2497
|
+
if (modes.test) devDependencies$1.vitest = alephaDeps.vitest;
|
|
2498
|
+
}
|
|
583
2499
|
const scripts = {
|
|
584
2500
|
dev: "alepha dev",
|
|
585
2501
|
build: "alepha build",
|
|
586
|
-
lint: "alepha lint",
|
|
587
|
-
typecheck: "alepha typecheck",
|
|
588
|
-
verify: "alepha verify"
|
|
589
|
-
};
|
|
590
|
-
if (modes.
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
dependencies.react =
|
|
597
|
-
dependencies["react-dom"] = "
|
|
598
|
-
devDependencies["@
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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();
|
|
2502
|
+
lint: "alepha lint",
|
|
2503
|
+
typecheck: "alepha typecheck",
|
|
2504
|
+
verify: "alepha verify"
|
|
2505
|
+
};
|
|
2506
|
+
if (modes.test) scripts.test = "vitest run";
|
|
2507
|
+
if (modes.ui) {
|
|
2508
|
+
dependencies["@alepha/ui"] = `^${version}`;
|
|
2509
|
+
modes.react = true;
|
|
2510
|
+
}
|
|
2511
|
+
if (modes.react) {
|
|
2512
|
+
dependencies.react = alephaDeps.react;
|
|
2513
|
+
dependencies["react-dom"] = alephaDeps["react-dom"];
|
|
2514
|
+
devDependencies$1["@vitejs/plugin-react"] = alephaDeps["@vitejs/plugin-react"];
|
|
2515
|
+
devDependencies$1["@types/react"] = alephaDeps["@types/react"];
|
|
2516
|
+
}
|
|
2517
|
+
return {
|
|
2518
|
+
type: "module",
|
|
2519
|
+
dependencies,
|
|
2520
|
+
devDependencies: devDependencies$1,
|
|
2521
|
+
scripts
|
|
2522
|
+
};
|
|
2523
|
+
}
|
|
2524
|
+
async removeFiles(root, files) {
|
|
2525
|
+
await Promise.all(files.map((file) => this.fs.rm(this.fs.join(root, file), {
|
|
2526
|
+
force: true,
|
|
2527
|
+
recursive: true
|
|
2528
|
+
})));
|
|
2529
|
+
}
|
|
649
2530
|
};
|
|
650
2531
|
|
|
651
2532
|
//#endregion
|
|
652
|
-
//#region ../../src/cli/
|
|
653
|
-
const
|
|
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 = {}) => {
|
|
2533
|
+
//#region ../../src/cli/templates/agentMd.ts
|
|
2534
|
+
const agentMd = (type, options = {}) => {
|
|
690
2535
|
const { react = false, projectName = "my-app" } = options;
|
|
2536
|
+
const header = type === "claude" ? `# CLAUDE.md
|
|
2537
|
+
|
|
2538
|
+
This file provides guidance to Claude Code when working with this Alepha project.` : `# AGENTS.md
|
|
2539
|
+
|
|
2540
|
+
This file provides guidance to AI coding assistants when working with this Alepha project.`;
|
|
691
2541
|
const reactSection = react ? `
|
|
692
2542
|
## React & Frontend
|
|
693
2543
|
|
|
694
2544
|
### Pages with \`$page\`
|
|
695
2545
|
\`\`\`tsx
|
|
696
|
-
import { $page } from "
|
|
2546
|
+
import { $page } from "alepha/react/router";
|
|
697
2547
|
import { $client } from "alepha/server/links";
|
|
698
2548
|
import type { UserController } from "./UserController.ts";
|
|
699
2549
|
|
|
@@ -719,9 +2569,9 @@ class AppRouter {
|
|
|
719
2569
|
|
|
720
2570
|
### React Hooks
|
|
721
2571
|
\`\`\`typescript
|
|
722
|
-
import { useAlepha, useClient, useStore, useAction, useInject } from "
|
|
723
|
-
import { useRouter, useActive } from "
|
|
724
|
-
import { useForm } from "
|
|
2572
|
+
import { useAlepha, useClient, useStore, useAction, useInject } from "alepha/react";
|
|
2573
|
+
import { useRouter, useActive } from "alepha/react/router";
|
|
2574
|
+
import { useForm } from "alepha/react/form";
|
|
725
2575
|
\`\`\`
|
|
726
2576
|
|
|
727
2577
|
- \`useClient<Controller>()\` - Type-safe API calls
|
|
@@ -766,9 +2616,7 @@ ${projectName}/
|
|
|
766
2616
|
└── tsconfig.json
|
|
767
2617
|
\`\`\`
|
|
768
2618
|
`;
|
|
769
|
-
return
|
|
770
|
-
|
|
771
|
-
This file provides guidance to Claude Code when working with this Alepha project.
|
|
2619
|
+
return `${header}
|
|
772
2620
|
|
|
773
2621
|
## Overview
|
|
774
2622
|
|
|
@@ -782,10 +2630,10 @@ This is an **Alepha** project - a convention-driven TypeScript framework for typ
|
|
|
782
2630
|
|
|
783
2631
|
## Rules
|
|
784
2632
|
|
|
785
|
-
- Use TypeScript strict mode
|
|
2633
|
+
- Use TypeScript strict mode, always check types (\`alepha typecheck\`)
|
|
786
2634
|
- Use Biome for formatting (\`alepha lint\`)
|
|
787
|
-
- Use Vitest for testing
|
|
788
|
-
- One file = one class
|
|
2635
|
+
- Use Vitest for testing (\`alepha test\`)
|
|
2636
|
+
- One file = one class, multiple interfaces/types allowed
|
|
789
2637
|
- Primitives are class properties (except \`$entity\`, \`$atom\`)
|
|
790
2638
|
- No decorators, no Express/Fastify patterns
|
|
791
2639
|
- No manual instantiation - use dependency injection
|
|
@@ -793,6 +2641,7 @@ This is an **Alepha** project - a convention-driven TypeScript framework for typ
|
|
|
793
2641
|
- Import with file extensions: \`import { User } from "./User.ts"\`
|
|
794
2642
|
- Use \`t\` from Alepha for schemas (not Zod)
|
|
795
2643
|
- Prefer \`t.text()\` over \`t.string()\` for user input (has default max length, auto-trim, supports lowercase option)
|
|
2644
|
+
- One file = one schema (schemas/createUserSchema.ts)
|
|
796
2645
|
|
|
797
2646
|
## Project Structure
|
|
798
2647
|
${projectStructure}
|
|
@@ -835,17 +2684,12 @@ export const userEntity = $entity({
|
|
|
835
2684
|
id: db.primaryKey(),
|
|
836
2685
|
email: t.email(),
|
|
837
2686
|
createdAt: db.createdAt(),
|
|
838
|
-
updatedAt: db.updatedAt(),
|
|
839
2687
|
}),
|
|
840
2688
|
indexes: [{ column: "email", unique: true }],
|
|
841
2689
|
});
|
|
842
2690
|
|
|
843
2691
|
class UserService {
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
async findById(id: string) {
|
|
847
|
-
return this.repo.findById(id);
|
|
848
|
-
}
|
|
2692
|
+
userRepository = $repository(userEntity);
|
|
849
2693
|
}
|
|
850
2694
|
\`\`\`
|
|
851
2695
|
|
|
@@ -855,11 +2699,6 @@ import { $inject } from "alepha";
|
|
|
855
2699
|
|
|
856
2700
|
class OrderService {
|
|
857
2701
|
userService = $inject(UserService); // Within same module
|
|
858
|
-
|
|
859
|
-
async createOrder(userId: string) {
|
|
860
|
-
const user = await this.userService.findById(userId);
|
|
861
|
-
// ...
|
|
862
|
-
}
|
|
863
2702
|
}
|
|
864
2703
|
|
|
865
2704
|
// Cross-module: use $client instead of $inject
|
|
@@ -923,7 +2762,7 @@ ${reactSection}
|
|
|
923
2762
|
| \`$bucket\` | \`alepha/bucket\` | File storage |
|
|
924
2763
|
| \`$issuer\` | \`alepha/security\` | JWT tokens |
|
|
925
2764
|
| \`$command\` | \`alepha/command\` | CLI commands |${react ? `
|
|
926
|
-
| \`$page\` |
|
|
2765
|
+
| \`$page\` | \`alepha/react/router\` | React pages with SSR |
|
|
927
2766
|
| \`$atom\` | \`alepha\` | Global state |` : ""}
|
|
928
2767
|
|
|
929
2768
|
## Testing
|
|
@@ -975,11 +2814,114 @@ alepha build # Build the project
|
|
|
975
2814
|
|
|
976
2815
|
- Full docs: https://alepha.dev/llms.txt
|
|
977
2816
|
- Detailed docs: https://alepha.dev/llms-full.txt
|
|
2817
|
+
|
|
2818
|
+
## Source Code Access
|
|
2819
|
+
|
|
2820
|
+
Full framework source available at \`node_modules/alepha/src/\`.
|
|
2821
|
+
|
|
2822
|
+
**IMPORTANT:** When answering questions about Alepha primitives, APIs, or internals:
|
|
2823
|
+
1. ALWAYS read the local source code first at \`node_modules/alepha/src/\` or \`node_modules/@alepha/ui/src/\` for UI-related questions
|
|
2824
|
+
2. Use \`Glob\` to find relevant files: \`node_modules/alepha/src/**/primitives/$<name>.ts\`
|
|
2825
|
+
3. Read the implementation AND the \`.spec.ts\` test files for usage examples
|
|
2826
|
+
4. Use external documentation as a fallback if source code is insufficient
|
|
2827
|
+
`.trim();
|
|
2828
|
+
};
|
|
2829
|
+
|
|
2830
|
+
//#endregion
|
|
2831
|
+
//#region ../../src/cli/templates/apiAppSecurityTs.ts
|
|
2832
|
+
const apiAppSecurityTs = () => {
|
|
2833
|
+
return `
|
|
2834
|
+
import { $realm } from "alepha/api/users";
|
|
2835
|
+
|
|
2836
|
+
export class AppSecurity {
|
|
2837
|
+
users = $realm({
|
|
2838
|
+
// configure your realm here
|
|
2839
|
+
});
|
|
2840
|
+
}
|
|
2841
|
+
`.trim();
|
|
2842
|
+
};
|
|
2843
|
+
|
|
2844
|
+
//#endregion
|
|
2845
|
+
//#region ../../src/cli/templates/apiHelloControllerTs.ts
|
|
2846
|
+
const apiHelloControllerTs = () => `
|
|
2847
|
+
import { t } from "alepha";
|
|
2848
|
+
import { $action } from "alepha/server";
|
|
2849
|
+
|
|
2850
|
+
export class HelloController {
|
|
2851
|
+
hello = $action({
|
|
2852
|
+
path: "/hello",
|
|
2853
|
+
schema: {
|
|
2854
|
+
response: t.object({
|
|
2855
|
+
message: t.string(),
|
|
2856
|
+
}),
|
|
2857
|
+
},
|
|
2858
|
+
handler: () => ({
|
|
2859
|
+
message: "Hello, Alepha!",
|
|
2860
|
+
}),
|
|
2861
|
+
});
|
|
2862
|
+
}
|
|
2863
|
+
`.trim();
|
|
2864
|
+
|
|
2865
|
+
//#endregion
|
|
2866
|
+
//#region ../../src/cli/templates/apiIndexTs.ts
|
|
2867
|
+
const apiIndexTs = (options = {}) => {
|
|
2868
|
+
const { appName = "app", auth = false } = options;
|
|
2869
|
+
const imports = ["import { $module } from \"alepha\";"];
|
|
2870
|
+
const services = [];
|
|
2871
|
+
if (auth) {
|
|
2872
|
+
imports.push("import { AppSecurity } from \"./AppSecurity.ts\";");
|
|
2873
|
+
services.push("AppSecurity");
|
|
2874
|
+
}
|
|
2875
|
+
imports.push("import { HelloController } from \"./controllers/HelloController.ts\";");
|
|
2876
|
+
services.push("HelloController");
|
|
2877
|
+
return `
|
|
2878
|
+
${imports.join("\n")}
|
|
2879
|
+
|
|
2880
|
+
export const ApiModule = $module({
|
|
2881
|
+
name: "${appName}.api",
|
|
2882
|
+
services: [${services.join(", ")}],
|
|
2883
|
+
});
|
|
978
2884
|
`.trim();
|
|
979
2885
|
};
|
|
980
2886
|
|
|
981
2887
|
//#endregion
|
|
982
|
-
//#region ../../src/cli/
|
|
2888
|
+
//#region ../../src/cli/templates/biomeJson.ts
|
|
2889
|
+
const biomeJson = () => `
|
|
2890
|
+
{
|
|
2891
|
+
"$schema": "https://biomejs.dev/schemas/latest/schema.json",
|
|
2892
|
+
"vcs": {
|
|
2893
|
+
"enabled": true,
|
|
2894
|
+
"clientKind": "git"
|
|
2895
|
+
},
|
|
2896
|
+
"files": {
|
|
2897
|
+
"ignoreUnknown": true,
|
|
2898
|
+
"includes": ["**", "!node_modules", "!dist"]
|
|
2899
|
+
},
|
|
2900
|
+
"formatter": {
|
|
2901
|
+
"enabled": true,
|
|
2902
|
+
"useEditorconfig": true
|
|
2903
|
+
},
|
|
2904
|
+
"linter": {
|
|
2905
|
+
"enabled": true,
|
|
2906
|
+
"rules": {
|
|
2907
|
+
"recommended": true
|
|
2908
|
+
},
|
|
2909
|
+
"domains": {
|
|
2910
|
+
"react": "recommended"
|
|
2911
|
+
}
|
|
2912
|
+
},
|
|
2913
|
+
"assist": {
|
|
2914
|
+
"actions": {
|
|
2915
|
+
"source": {
|
|
2916
|
+
"organizeImports": "on"
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
`.trim();
|
|
2922
|
+
|
|
2923
|
+
//#endregion
|
|
2924
|
+
//#region ../../src/cli/templates/dummySpecTs.ts
|
|
983
2925
|
const dummySpecTs = () => `
|
|
984
2926
|
import { test, expect } from "vitest";
|
|
985
2927
|
|
|
@@ -989,7 +2931,7 @@ test("dummy test", () => {
|
|
|
989
2931
|
`.trim();
|
|
990
2932
|
|
|
991
2933
|
//#endregion
|
|
992
|
-
//#region ../../src/cli/
|
|
2934
|
+
//#region ../../src/cli/templates/editorconfig.ts
|
|
993
2935
|
const editorconfig = () => `
|
|
994
2936
|
# https://editorconfig.org
|
|
995
2937
|
|
|
@@ -1005,7 +2947,48 @@ indent_size = 2
|
|
|
1005
2947
|
`.trim();
|
|
1006
2948
|
|
|
1007
2949
|
//#endregion
|
|
1008
|
-
//#region ../../src/cli/
|
|
2950
|
+
//#region ../../src/cli/templates/gitignore.ts
|
|
2951
|
+
const gitignore = () => `
|
|
2952
|
+
# Dependencies
|
|
2953
|
+
node_modules/
|
|
2954
|
+
|
|
2955
|
+
# Build outputs
|
|
2956
|
+
dist/
|
|
2957
|
+
.vite/
|
|
2958
|
+
|
|
2959
|
+
# Environment files
|
|
2960
|
+
.env
|
|
2961
|
+
.env.*
|
|
2962
|
+
!.env.example
|
|
2963
|
+
|
|
2964
|
+
# IDE
|
|
2965
|
+
.idea/
|
|
2966
|
+
*.swp
|
|
2967
|
+
*.swo
|
|
2968
|
+
|
|
2969
|
+
# OS
|
|
2970
|
+
.DS_Store
|
|
2971
|
+
Thumbs.db
|
|
2972
|
+
|
|
2973
|
+
# Logs
|
|
2974
|
+
*.log
|
|
2975
|
+
logs/
|
|
2976
|
+
|
|
2977
|
+
# Test coverage
|
|
2978
|
+
coverage/
|
|
2979
|
+
|
|
2980
|
+
# Yarn
|
|
2981
|
+
.yarn/*
|
|
2982
|
+
!.yarn/patches
|
|
2983
|
+
!.yarn/plugins
|
|
2984
|
+
!.yarn/releases
|
|
2985
|
+
!.yarn/sdks
|
|
2986
|
+
!.yarn/versions
|
|
2987
|
+
.pnp.*
|
|
2988
|
+
`.trim();
|
|
2989
|
+
|
|
2990
|
+
//#endregion
|
|
2991
|
+
//#region ../../src/cli/templates/mainBrowserTs.ts
|
|
1009
2992
|
const mainBrowserTs = () => `
|
|
1010
2993
|
import { Alepha, run } from "alepha";
|
|
1011
2994
|
import { WebModule } from "./web/index.ts";
|
|
@@ -1018,8 +3001,10 @@ run(alepha);
|
|
|
1018
3001
|
`.trim();
|
|
1019
3002
|
|
|
1020
3003
|
//#endregion
|
|
1021
|
-
//#region ../../src/cli/
|
|
1022
|
-
const mainCss = () =>
|
|
3004
|
+
//#region ../../src/cli/templates/mainCss.ts
|
|
3005
|
+
const mainCss = (options = {}) => {
|
|
3006
|
+
if (options.ui) return `@import "@alepha/ui/styles";`;
|
|
3007
|
+
return `
|
|
1023
3008
|
* {
|
|
1024
3009
|
box-sizing: border-box;
|
|
1025
3010
|
margin: 0;
|
|
@@ -1042,25 +3027,34 @@ body {
|
|
|
1042
3027
|
height: 100%;
|
|
1043
3028
|
}
|
|
1044
3029
|
`.trim();
|
|
3030
|
+
};
|
|
1045
3031
|
|
|
1046
3032
|
//#endregion
|
|
1047
|
-
//#region ../../src/cli/
|
|
3033
|
+
//#region ../../src/cli/templates/mainServerTs.ts
|
|
1048
3034
|
const mainServerTs = (options = {}) => {
|
|
1049
|
-
const { react = false } = options;
|
|
3035
|
+
const { api = false, react = false } = options;
|
|
3036
|
+
const imports = [];
|
|
3037
|
+
const withs = [];
|
|
3038
|
+
if (api) {
|
|
3039
|
+
imports.push(`import { ApiModule } from "./api/index.ts";`);
|
|
3040
|
+
withs.push(`alepha.with(ApiModule);`);
|
|
3041
|
+
}
|
|
3042
|
+
if (react) {
|
|
3043
|
+
imports.push(`import { WebModule } from "./web/index.ts";`);
|
|
3044
|
+
withs.push(`alepha.with(WebModule);`);
|
|
3045
|
+
}
|
|
1050
3046
|
return `
|
|
1051
3047
|
import { Alepha, run } from "alepha";
|
|
1052
|
-
|
|
1053
|
-
${react ? `import { WebModule } from "./web/index.ts";\n` : ""}
|
|
3048
|
+
${imports.length > 0 ? `${imports.join("\n")}\n` : ""}
|
|
1054
3049
|
const alepha = Alepha.create();
|
|
3050
|
+
${withs.length > 0 ? `\n${withs.join("\n")}` : ""}
|
|
1055
3051
|
|
|
1056
|
-
alepha.with(ApiModule);
|
|
1057
|
-
${react ? `alepha.with(WebModule);\n` : ""}
|
|
1058
3052
|
run(alepha);
|
|
1059
3053
|
`.trim();
|
|
1060
3054
|
};
|
|
1061
3055
|
|
|
1062
3056
|
//#endregion
|
|
1063
|
-
//#region ../../src/cli/
|
|
3057
|
+
//#region ../../src/cli/templates/tsconfigJson.ts
|
|
1064
3058
|
const tsconfigJson = () => `
|
|
1065
3059
|
{
|
|
1066
3060
|
"extends": "alepha/tsconfig.base"
|
|
@@ -1068,47 +3062,74 @@ const tsconfigJson = () => `
|
|
|
1068
3062
|
`.trim();
|
|
1069
3063
|
|
|
1070
3064
|
//#endregion
|
|
1071
|
-
//#region ../../src/cli/
|
|
1072
|
-
const webAppRouterTs = () =>
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
import
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
3065
|
+
//#region ../../src/cli/templates/webAppRouterTs.ts
|
|
3066
|
+
const webAppRouterTs = (options) => {
|
|
3067
|
+
const imports = [];
|
|
3068
|
+
const classMembers = [];
|
|
3069
|
+
if (options.ui) imports.push("import { $ui } from \"@alepha/ui\";");
|
|
3070
|
+
if (options.auth) imports.push("import { $uiAuth } from \"@alepha/ui/auth\";");
|
|
3071
|
+
if (options.admin) imports.push("import { $uiAdmin } from \"@alepha/ui/admin\";");
|
|
3072
|
+
imports.push("import { $page } from \"alepha/react/router\";");
|
|
3073
|
+
if (options.api) {
|
|
3074
|
+
imports.push("import { $client } from \"alepha/server/links\";");
|
|
3075
|
+
imports.push("import type { HelloController } from \"../api/controllers/HelloController.ts\";");
|
|
3076
|
+
classMembers.push(" api = $client<HelloController>();");
|
|
3077
|
+
}
|
|
3078
|
+
if (options.ui) {
|
|
3079
|
+
classMembers.push(" ui = $ui();");
|
|
3080
|
+
if (options.auth) classMembers.push(" uiAuth = $uiAuth();");
|
|
3081
|
+
if (options.admin) classMembers.push(" uiAdmin = $uiAdmin();");
|
|
3082
|
+
classMembers.push(` layout = $page({
|
|
3083
|
+
parent: this.ui.root,
|
|
3084
|
+
children: () => [this.home],
|
|
3085
|
+
});`);
|
|
3086
|
+
}
|
|
3087
|
+
if (options.api) classMembers.push(` home = $page({
|
|
1081
3088
|
path: "/",
|
|
1082
3089
|
lazy: () => import("./components/Hello.tsx"),
|
|
1083
3090
|
loader: () => this.api.hello(),
|
|
1084
|
-
});
|
|
1085
|
-
|
|
1086
|
-
|
|
3091
|
+
});`);
|
|
3092
|
+
else classMembers.push(` home = $page({
|
|
3093
|
+
path: "/",
|
|
3094
|
+
lazy: () => import("./components/Hello.tsx"),
|
|
3095
|
+
});`);
|
|
3096
|
+
return `${imports.join("\n")}
|
|
3097
|
+
|
|
3098
|
+
export class AppRouter {
|
|
3099
|
+
${classMembers.join("\n\n")}
|
|
3100
|
+
}`;
|
|
3101
|
+
};
|
|
1087
3102
|
|
|
1088
3103
|
//#endregion
|
|
1089
|
-
//#region ../../src/cli/
|
|
1090
|
-
const webHelloComponentTsx = (
|
|
3104
|
+
//#region ../../src/cli/templates/webHelloComponentTsx.ts
|
|
3105
|
+
const webHelloComponentTsx = (options = {}) => {
|
|
3106
|
+
const imports = [];
|
|
3107
|
+
if (options.auth) imports.push("import { UserButton } from \"@alepha/ui/auth\";");
|
|
3108
|
+
imports.push("import { useState } from \"react\";");
|
|
3109
|
+
const userButton = options.auth ? "\n <UserButton />" : "";
|
|
3110
|
+
return `${imports.join("\n")}
|
|
1091
3111
|
|
|
1092
3112
|
interface Props {
|
|
1093
|
-
message
|
|
3113
|
+
message?: string;
|
|
1094
3114
|
}
|
|
1095
3115
|
|
|
1096
3116
|
const Hello = (props: Props) => {
|
|
1097
|
-
const [message, setMessage] = useState(props.message);
|
|
3117
|
+
const [message, setMessage] = useState(props.message ?? "");
|
|
1098
3118
|
return (
|
|
1099
3119
|
<div>
|
|
1100
3120
|
<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
|
|
3121
|
+
<input value={message} onChange={(e) => setMessage(e.target.value)} />
|
|
3122
|
+
<p>Edit this component in src/web/components/Hello.tsx</p>${userButton}
|
|
1103
3123
|
</div>
|
|
1104
3124
|
);
|
|
1105
3125
|
};
|
|
1106
3126
|
|
|
1107
3127
|
export default Hello;
|
|
1108
|
-
|
|
3128
|
+
`;
|
|
3129
|
+
};
|
|
1109
3130
|
|
|
1110
3131
|
//#endregion
|
|
1111
|
-
//#region ../../src/cli/
|
|
3132
|
+
//#region ../../src/cli/templates/webIndexTs.ts
|
|
1112
3133
|
const webIndexTs = (options = {}) => {
|
|
1113
3134
|
const { appName = "app" } = options;
|
|
1114
3135
|
return `
|
|
@@ -1137,6 +3158,7 @@ var ProjectScaffolder = class {
|
|
|
1137
3158
|
log = $logger();
|
|
1138
3159
|
fs = $inject(FileSystemProvider);
|
|
1139
3160
|
pm = $inject(PackageManagerUtils);
|
|
3161
|
+
utils = $inject(AlephaCliUtils);
|
|
1140
3162
|
/**
|
|
1141
3163
|
* Get the app name from the directory name.
|
|
1142
3164
|
*
|
|
@@ -1146,7 +3168,7 @@ var ProjectScaffolder = class {
|
|
|
1146
3168
|
* - Falls back to "app" if empty
|
|
1147
3169
|
*/
|
|
1148
3170
|
getAppName(root) {
|
|
1149
|
-
return basename(root).toLowerCase().replace(/[\s\-_]/g, "") || "app";
|
|
3171
|
+
return basename(root).toLowerCase().replace(/[\s\-_.\d]/g, "") || "app";
|
|
1150
3172
|
}
|
|
1151
3173
|
/**
|
|
1152
3174
|
* Ensure all configuration files exist.
|
|
@@ -1154,13 +3176,19 @@ var ProjectScaffolder = class {
|
|
|
1154
3176
|
async ensureConfig(root, opts) {
|
|
1155
3177
|
const tasks = [];
|
|
1156
3178
|
const force = opts.force ?? false;
|
|
3179
|
+
const checkWorkspace = opts.checkWorkspace ?? false;
|
|
1157
3180
|
if (opts.packageJson) tasks.push(this.pm.ensurePackageJson(root, typeof opts.packageJson === "boolean" ? {} : opts.packageJson).then(() => {}));
|
|
1158
3181
|
if (opts.tsconfigJson) tasks.push(this.ensureTsConfig(root, { force }));
|
|
1159
|
-
if (opts.
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
3182
|
+
if (opts.biomeJson) tasks.push(this.ensureBiomeConfig(root, {
|
|
3183
|
+
force,
|
|
3184
|
+
checkWorkspace
|
|
3185
|
+
}));
|
|
3186
|
+
if (opts.editorconfig) tasks.push(this.ensureEditorConfig(root, {
|
|
3187
|
+
force,
|
|
3188
|
+
checkWorkspace
|
|
3189
|
+
}));
|
|
3190
|
+
if (opts.agentMd) tasks.push(this.ensureAgentMd(root, {
|
|
3191
|
+
...opts.agentMd,
|
|
1164
3192
|
force
|
|
1165
3193
|
}));
|
|
1166
3194
|
await Promise.all(tasks);
|
|
@@ -1170,52 +3198,81 @@ var ProjectScaffolder = class {
|
|
|
1170
3198
|
await this.fs.writeFile(this.fs.join(root, "tsconfig.json"), tsconfigJson());
|
|
1171
3199
|
}
|
|
1172
3200
|
async ensureBiomeConfig(root, opts = {}) {
|
|
3201
|
+
if (!opts.force && opts.checkWorkspace && await this.existsInParents(root, "biome.json")) return;
|
|
1173
3202
|
await this.ensureFile(root, "biome.json", biomeJson(), opts.force);
|
|
1174
3203
|
}
|
|
1175
3204
|
async ensureEditorConfig(root, opts = {}) {
|
|
3205
|
+
if (!opts.force && opts.checkWorkspace && await this.existsInParents(root, ".editorconfig")) return;
|
|
1176
3206
|
await this.ensureFile(root, ".editorconfig", editorconfig(), opts.force);
|
|
1177
3207
|
}
|
|
1178
|
-
|
|
1179
|
-
|
|
3208
|
+
/**
|
|
3209
|
+
* Ensure git repository is initialized with .gitignore.
|
|
3210
|
+
*
|
|
3211
|
+
* @returns true if git was initialized, false if already exists or git unavailable
|
|
3212
|
+
*/
|
|
3213
|
+
async ensureGitRepo(root, opts = {}) {
|
|
3214
|
+
const gitDir = this.fs.join(root, ".git");
|
|
3215
|
+
if (!opts.force && await this.fs.exists(gitDir)) return false;
|
|
3216
|
+
if (!await this.utils.isInstalledAsync("git")) return false;
|
|
3217
|
+
await this.utils.exec("git init", {
|
|
3218
|
+
root,
|
|
3219
|
+
global: true
|
|
3220
|
+
});
|
|
3221
|
+
await this.ensureFile(root, ".gitignore", gitignore(), opts.force);
|
|
3222
|
+
return true;
|
|
3223
|
+
}
|
|
3224
|
+
async ensureAgentMd(root, options) {
|
|
3225
|
+
const filename = options.type === "claude" ? "CLAUDE.md" : "AGENTS.md";
|
|
3226
|
+
await this.ensureFile(root, filename, agentMd(options.type, options), options.force);
|
|
3227
|
+
}
|
|
3228
|
+
/**
|
|
3229
|
+
* Ensure src/main.server.ts exists with correct module imports.
|
|
3230
|
+
*/
|
|
3231
|
+
async ensureMainServerTs(root, opts = {}) {
|
|
3232
|
+
const srcDir = this.fs.join(root, "src");
|
|
3233
|
+
await this.fs.mkdir(srcDir, { recursive: true });
|
|
3234
|
+
await this.ensureFile(srcDir, "main.server.ts", mainServerTs({
|
|
3235
|
+
api: opts.api,
|
|
3236
|
+
react: opts.react
|
|
3237
|
+
}), opts.force);
|
|
1180
3238
|
}
|
|
1181
3239
|
/**
|
|
1182
|
-
* Ensure
|
|
3240
|
+
* Ensure API module structure exists.
|
|
1183
3241
|
*
|
|
1184
3242
|
* Creates:
|
|
1185
|
-
* - src/main.server.ts (entry point)
|
|
1186
3243
|
* - src/api/index.ts (API module)
|
|
1187
3244
|
* - src/api/controllers/HelloController.ts (example controller)
|
|
1188
3245
|
*/
|
|
1189
3246
|
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
3247
|
const appName = this.getAppName(root);
|
|
1195
3248
|
await this.fs.mkdir(this.fs.join(root, "src/api/controllers"), { recursive: true });
|
|
1196
|
-
await this.ensureFile(
|
|
1197
|
-
|
|
1198
|
-
|
|
3249
|
+
await this.ensureFile(root, "src/api/index.ts", apiIndexTs({
|
|
3250
|
+
appName,
|
|
3251
|
+
auth: opts.auth
|
|
3252
|
+
}), opts.force);
|
|
3253
|
+
await this.ensureFile(root, "src/api/controllers/HelloController.ts", apiHelloControllerTs(), opts.force);
|
|
3254
|
+
if (opts.auth) await this.ensureFile(root, "src/api/AppSecurity.ts", apiAppSecurityTs(), opts.force);
|
|
1199
3255
|
}
|
|
1200
3256
|
/**
|
|
1201
|
-
* Ensure
|
|
3257
|
+
* Ensure web/React project structure exists.
|
|
1202
3258
|
*
|
|
1203
3259
|
* Creates:
|
|
1204
|
-
* - src/main.
|
|
1205
|
-
* - src/
|
|
3260
|
+
* - src/main.browser.ts
|
|
3261
|
+
* - src/main.css
|
|
1206
3262
|
* - src/web/index.ts, src/web/AppRouter.ts, src/web/components/Hello.tsx
|
|
1207
3263
|
*/
|
|
1208
|
-
async
|
|
3264
|
+
async ensureWebProject(root, opts = {}) {
|
|
1209
3265
|
const appName = this.getAppName(root);
|
|
1210
|
-
await this.fs.mkdir(this.fs.join(root, "src/api/controllers"), { recursive: true });
|
|
1211
3266
|
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);
|
|
3267
|
+
await this.ensureFile(root, "src/main.css", mainCss({ ui: opts.ui }), opts.force);
|
|
1216
3268
|
await this.ensureFile(root, "src/web/index.ts", webIndexTs({ appName }), opts.force);
|
|
1217
|
-
await this.ensureFile(root, "src/web/AppRouter.ts", webAppRouterTs(
|
|
1218
|
-
|
|
3269
|
+
await this.ensureFile(root, "src/web/AppRouter.ts", webAppRouterTs({
|
|
3270
|
+
api: opts.api,
|
|
3271
|
+
ui: opts.ui,
|
|
3272
|
+
auth: opts.auth,
|
|
3273
|
+
admin: opts.admin
|
|
3274
|
+
}), opts.force);
|
|
3275
|
+
await this.ensureFile(root, "src/web/components/Hello.tsx", webHelloComponentTsx({ auth: opts.auth }), opts.force);
|
|
1219
3276
|
await this.ensureFile(root, "src/main.browser.ts", mainBrowserTs(), opts.force);
|
|
1220
3277
|
}
|
|
1221
3278
|
/**
|
|
@@ -1263,17 +3320,55 @@ var BuildCommand = class {
|
|
|
1263
3320
|
boot = $inject(AppEntryProvider);
|
|
1264
3321
|
viteBuildProvider = $inject(ViteBuildProvider);
|
|
1265
3322
|
options = $use(buildOptions);
|
|
3323
|
+
/**
|
|
3324
|
+
* Resolve the effective runtime based on target and explicit runtime flag.
|
|
3325
|
+
*
|
|
3326
|
+
* Some targets force a specific runtime:
|
|
3327
|
+
* - `cloudflare` always uses `workerd`
|
|
3328
|
+
* - `vercel` always uses `node`
|
|
3329
|
+
* - `docker` and bare deployments respect the runtime flag
|
|
3330
|
+
*
|
|
3331
|
+
* @throws {AlephaError} If an incompatible runtime is specified for a target
|
|
3332
|
+
*/
|
|
3333
|
+
resolveRuntime(target, runtime) {
|
|
3334
|
+
if (target === "cloudflare") {
|
|
3335
|
+
if (runtime && runtime !== "workerd") throw new AlephaError(`Target 'cloudflare' requires 'workerd' runtime, got '${runtime}'`);
|
|
3336
|
+
return "workerd";
|
|
3337
|
+
}
|
|
3338
|
+
if (target === "vercel") {
|
|
3339
|
+
if (runtime && runtime !== "node") throw new AlephaError(`Target 'vercel' requires 'node' runtime, got '${runtime}'`);
|
|
3340
|
+
return "node";
|
|
3341
|
+
}
|
|
3342
|
+
return runtime ?? "node";
|
|
3343
|
+
}
|
|
1266
3344
|
build = $command({
|
|
1267
3345
|
name: "build",
|
|
1268
3346
|
mode: "production",
|
|
1269
3347
|
description: "Build the project for production",
|
|
1270
3348
|
flags: t.object({
|
|
1271
3349
|
stats: t.optional(t.boolean({ description: "Generate build stats report" })),
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
3350
|
+
target: t.optional(t.enum([
|
|
3351
|
+
"bare",
|
|
3352
|
+
"docker",
|
|
3353
|
+
"vercel",
|
|
3354
|
+
"cloudflare"
|
|
3355
|
+
], {
|
|
3356
|
+
aliases: ["t"],
|
|
3357
|
+
description: "Deployment target"
|
|
3358
|
+
})),
|
|
3359
|
+
runtime: t.optional(t.enum([
|
|
3360
|
+
"node",
|
|
3361
|
+
"bun",
|
|
3362
|
+
"workerd"
|
|
3363
|
+
], {
|
|
3364
|
+
aliases: ["r"],
|
|
3365
|
+
description: "JavaScript runtime"
|
|
3366
|
+
})),
|
|
3367
|
+
image: t.optional(t.union([t.boolean(), t.text()], {
|
|
3368
|
+
aliases: ["i"],
|
|
3369
|
+
description: "Build Docker image. Use -i for latest, -i=<version> for specific version"
|
|
3370
|
+
})),
|
|
3371
|
+
sitemap: t.optional(t.text({ description: "Generate sitemap.xml with base URL" }))
|
|
1277
3372
|
}),
|
|
1278
3373
|
handler: async ({ flags, run, root }) => {
|
|
1279
3374
|
process.env.NODE_ENV = "production";
|
|
@@ -1283,13 +3378,16 @@ var BuildCommand = class {
|
|
|
1283
3378
|
this.log.trace("Entry file found", { entry });
|
|
1284
3379
|
const distDir = "dist";
|
|
1285
3380
|
const publicDir = "public";
|
|
1286
|
-
await this.pm.ensureDependency(root, "vite", {
|
|
1287
|
-
run,
|
|
1288
|
-
exec: (cmd, opts) => this.utils.exec(cmd, opts)
|
|
1289
|
-
});
|
|
1290
3381
|
await run.rm("dist", { alias: "clean dist" });
|
|
1291
3382
|
const options = this.options;
|
|
1292
3383
|
await this.utils.loadEnv(root, [".env", ".env.production"]);
|
|
3384
|
+
const target = flags.target ?? options.target;
|
|
3385
|
+
const runtime = this.resolveRuntime(target, flags.runtime ?? options.runtime);
|
|
3386
|
+
if (flags.image && target !== "docker") throw new AlephaError(`Flag '--image' requires '--target=docker', got '${target ?? "bare"}'`);
|
|
3387
|
+
this.log.trace("Build configuration", {
|
|
3388
|
+
target,
|
|
3389
|
+
runtime
|
|
3390
|
+
});
|
|
1293
3391
|
const stats = flags.stats ?? options.stats ?? false;
|
|
1294
3392
|
let template = "";
|
|
1295
3393
|
let hasClient = false;
|
|
@@ -1327,8 +3425,8 @@ var BuildCommand = class {
|
|
|
1327
3425
|
const clientIndexPath = `${distDir}/${publicDir}/index.html`;
|
|
1328
3426
|
const clientBuilt = await this.fs.exists(clientIndexPath);
|
|
1329
3427
|
const conditions = [];
|
|
1330
|
-
if (
|
|
1331
|
-
if (
|
|
3428
|
+
if (runtime === "bun") conditions.push("bun");
|
|
3429
|
+
else if (runtime === "workerd") conditions.push("workerd");
|
|
1332
3430
|
await buildServer({
|
|
1333
3431
|
silent: true,
|
|
1334
3432
|
entry: entry.server,
|
|
@@ -1363,7 +3461,7 @@ var BuildCommand = class {
|
|
|
1363
3461
|
run
|
|
1364
3462
|
});
|
|
1365
3463
|
}
|
|
1366
|
-
if (
|
|
3464
|
+
if (target === "vercel") await run({
|
|
1367
3465
|
name: "add Vercel config",
|
|
1368
3466
|
handler: () => generateVercel({
|
|
1369
3467
|
distDir,
|
|
@@ -1371,20 +3469,56 @@ var BuildCommand = class {
|
|
|
1371
3469
|
config: options.vercel
|
|
1372
3470
|
})
|
|
1373
3471
|
});
|
|
1374
|
-
if (
|
|
3472
|
+
if (target === "cloudflare") await run({
|
|
1375
3473
|
name: "add Cloudflare config",
|
|
1376
3474
|
handler: () => generateCloudflare({
|
|
1377
3475
|
distDir,
|
|
1378
3476
|
config: options.cloudflare?.config
|
|
1379
3477
|
})
|
|
1380
3478
|
});
|
|
1381
|
-
if (
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
3479
|
+
if (target === "docker") {
|
|
3480
|
+
const dockerFrom = options.docker?.from ?? (runtime === "bun" ? "oven/bun:alpine" : "node:24-alpine");
|
|
3481
|
+
const dockerCommand = options.docker?.command ?? (runtime === "bun" ? "bun" : "node");
|
|
3482
|
+
await run({
|
|
3483
|
+
name: "add Docker config",
|
|
3484
|
+
handler: () => generateDocker({
|
|
3485
|
+
distDir,
|
|
3486
|
+
image: dockerFrom,
|
|
3487
|
+
command: dockerCommand
|
|
3488
|
+
})
|
|
3489
|
+
});
|
|
3490
|
+
if (flags.image) {
|
|
3491
|
+
const imageConfig = options.docker?.image;
|
|
3492
|
+
const flagValue = typeof flags.image === "string" ? flags.image : null;
|
|
3493
|
+
let imageTag;
|
|
3494
|
+
let version;
|
|
3495
|
+
if (!flagValue) {
|
|
3496
|
+
if (!imageConfig?.tag) throw new AlephaError("Flag '--image' requires 'build.docker.image.tag' in config");
|
|
3497
|
+
version = "latest";
|
|
3498
|
+
imageTag = `${imageConfig.tag}:${version}`;
|
|
3499
|
+
} else if (flagValue.startsWith(":")) {
|
|
3500
|
+
if (!imageConfig?.tag) throw new AlephaError("Flag '--image=:version' requires 'build.docker.image.tag' in config");
|
|
3501
|
+
version = flagValue.slice(1);
|
|
3502
|
+
imageTag = `${imageConfig.tag}:${version}`;
|
|
3503
|
+
} else if (flagValue.includes(":")) {
|
|
3504
|
+
imageTag = flagValue;
|
|
3505
|
+
version = flagValue.split(":")[1];
|
|
3506
|
+
} else {
|
|
3507
|
+
imageTag = `${flagValue}:latest`;
|
|
3508
|
+
version = "latest";
|
|
3509
|
+
}
|
|
3510
|
+
const args = [];
|
|
3511
|
+
if (imageConfig?.args) args.push(imageConfig.args);
|
|
3512
|
+
if (imageConfig?.oci) {
|
|
3513
|
+
const revision = await this.utils.getGitRevision();
|
|
3514
|
+
const created = (/* @__PURE__ */ new Date()).toISOString();
|
|
3515
|
+
args.push(`--label "org.opencontainers.image.revision=${revision}"`);
|
|
3516
|
+
args.push(`--label "org.opencontainers.image.created=${created}"`);
|
|
3517
|
+
args.push(`--label "org.opencontainers.image.version=${version}"`);
|
|
3518
|
+
}
|
|
3519
|
+
await run(`docker build ${args.length > 0 ? `${args.join(" ")} ` : ""}-t ${imageTag} ${distDir}`, { alias: `docker build ${imageTag}` });
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
1388
3522
|
}
|
|
1389
3523
|
});
|
|
1390
3524
|
};
|
|
@@ -1414,6 +3548,7 @@ var DbCommand = class {
|
|
|
1414
3548
|
log = $logger();
|
|
1415
3549
|
fs = $inject(FileSystemProvider);
|
|
1416
3550
|
utils = $inject(AlephaCliUtils);
|
|
3551
|
+
entryProvider = $inject(AppEntryProvider);
|
|
1417
3552
|
/**
|
|
1418
3553
|
* Check if database migrations are up to date.
|
|
1419
3554
|
*/
|
|
@@ -1428,7 +3563,11 @@ var DbCommand = class {
|
|
|
1428
3563
|
handler: async ({ args, root }) => {
|
|
1429
3564
|
const rootDir = root;
|
|
1430
3565
|
this.log.debug(`Using project root: ${rootDir}`);
|
|
1431
|
-
const
|
|
3566
|
+
const entry = await this.entryProvider.getAppEntry(root);
|
|
3567
|
+
const alepha = await this.utils.loadAlephaFromServerEntryFile({
|
|
3568
|
+
mode: "development",
|
|
3569
|
+
entry
|
|
3570
|
+
});
|
|
1432
3571
|
const repositoryProvider = alepha.inject("RepositoryProvider");
|
|
1433
3572
|
const drizzleKitProvider = alepha.inject("DrizzleKitProvider");
|
|
1434
3573
|
const accepted = /* @__PURE__ */ new Set([]);
|
|
@@ -1592,7 +3731,11 @@ var DbCommand = class {
|
|
|
1592
3731
|
if (options.env) envFiles.push(`.env.${options.env}`);
|
|
1593
3732
|
await this.utils.loadEnv(rootDir, envFiles);
|
|
1594
3733
|
this.log.debug(`Using project root: ${rootDir}`);
|
|
1595
|
-
const
|
|
3734
|
+
const entry = await this.entryProvider.getAppEntry(rootDir);
|
|
3735
|
+
const alepha = await this.utils.loadAlephaFromServerEntryFile({
|
|
3736
|
+
mode: "development",
|
|
3737
|
+
entry
|
|
3738
|
+
});
|
|
1596
3739
|
const drizzleKitProvider = alepha.inject("DrizzleKitProvider");
|
|
1597
3740
|
const repositoryProvider = alepha.inject("RepositoryProvider");
|
|
1598
3741
|
const accepted = /* @__PURE__ */ new Set([]);
|
|
@@ -1616,11 +3759,11 @@ var DbCommand = class {
|
|
|
1616
3759
|
providerUrl: provider.url,
|
|
1617
3760
|
providerDriver: provider.driver,
|
|
1618
3761
|
dialect,
|
|
1619
|
-
entry,
|
|
3762
|
+
entry: this.fs.join(rootDir, entry.server),
|
|
1620
3763
|
rootDir
|
|
1621
3764
|
});
|
|
1622
3765
|
const flags = options.commandFlags ? ` ${options.commandFlags}` : "";
|
|
1623
|
-
await this.utils.exec(`drizzle-kit ${options.command} --config=${drizzleConfigJsPath}${flags}`, { env: { NODE_OPTIONS: "--import tsx" } });
|
|
3766
|
+
await this.utils.exec(`drizzle-kit ${options.command} --config=${drizzleConfigJsPath}${flags}`, { env: { NODE_OPTIONS: [process.env.NODE_OPTIONS, "--import tsx"].filter(Boolean).join(" ") } });
|
|
1624
3767
|
}
|
|
1625
3768
|
}
|
|
1626
3769
|
/**
|
|
@@ -1644,9 +3787,9 @@ var DbCommand = class {
|
|
|
1644
3787
|
const accountId = process.env.CLOUDFLARE_ACCOUNT_ID;
|
|
1645
3788
|
if (!accountId) throw new AlephaError("CLOUDFLARE_ACCOUNT_ID environment variable is not set. https://orm.drizzle.team/docs/guides/d1-http-with-drizzle-kit");
|
|
1646
3789
|
const url = options.providerUrl;
|
|
1647
|
-
if (!url.startsWith("
|
|
1648
|
-
const [, databaseId] = url.replace("
|
|
1649
|
-
if (!databaseId) throw new AlephaError("Database ID is missing in the D1 provider URL. Cloudflare D1 URL format:
|
|
3790
|
+
if (!url.startsWith("d1://")) throw new AlephaError("D1 provider URL must start with 'd1://'.");
|
|
3791
|
+
const [, databaseId] = url.replace("d1://", "").replace("d1:", "").split(":");
|
|
3792
|
+
if (!databaseId) throw new AlephaError("Database ID is missing in the D1 provider URL. Cloudflare D1 URL format: d1://<database_name>:<database_id>");
|
|
1650
3793
|
config.dbCredentials = {
|
|
1651
3794
|
accountId,
|
|
1652
3795
|
databaseId,
|
|
@@ -1778,7 +3921,6 @@ var DeployCommand = class {
|
|
|
1778
3921
|
var ViteDevServerProvider = class {
|
|
1779
3922
|
log = $logger();
|
|
1780
3923
|
fs = $inject(FileSystemProvider);
|
|
1781
|
-
templateProvider = $inject(ViteTemplateProvider);
|
|
1782
3924
|
server;
|
|
1783
3925
|
options;
|
|
1784
3926
|
alepha = null;
|
|
@@ -1883,11 +4025,11 @@ var ViteDevServerProvider = class {
|
|
|
1883
4025
|
async setupEnvironment() {
|
|
1884
4026
|
const { loadEnv } = await importVite();
|
|
1885
4027
|
const env = loadEnv(process.env.NODE_ENV || "development", this.options.root, "");
|
|
4028
|
+
for (const [key, value] of Object.entries(env)) process.env[key] ??= value;
|
|
1886
4029
|
process.env.NODE_ENV ??= "development";
|
|
1887
4030
|
process.env.VITE_ALEPHA_DEV = "true";
|
|
1888
4031
|
process.env.SERVER_HOST ??= this.options.host?.toString() ?? "localhost";
|
|
1889
4032
|
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
4033
|
}
|
|
1892
4034
|
/**
|
|
1893
4035
|
* Load or reload the Alepha instance.
|
|
@@ -1920,12 +4062,10 @@ var ViteDevServerProvider = class {
|
|
|
1920
4062
|
}
|
|
1921
4063
|
}
|
|
1922
4064
|
/**
|
|
1923
|
-
* Setup Alepha instance with Vite middleware
|
|
4065
|
+
* Setup Alepha instance with Vite middleware.
|
|
1924
4066
|
*/
|
|
1925
4067
|
async setupAlepha() {
|
|
1926
4068
|
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
4069
|
this.alepha.events.on("server:onRequest", {
|
|
1930
4070
|
priority: "first",
|
|
1931
4071
|
callback: async ({ request }) => {
|
|
@@ -1952,6 +4092,7 @@ var ViteDevServerProvider = class {
|
|
|
1952
4092
|
* Run Vite middleware and detect if it handled the request.
|
|
1953
4093
|
*/
|
|
1954
4094
|
async runViteMiddleware(req, res, ctx) {
|
|
4095
|
+
if (res.headersSent || res.writableEnded) return false;
|
|
1955
4096
|
return new Promise((resolve) => {
|
|
1956
4097
|
let resolved = false;
|
|
1957
4098
|
const done = (handled) => {
|
|
@@ -1960,6 +4101,18 @@ var ViteDevServerProvider = class {
|
|
|
1960
4101
|
if (handled) ctx.metadata.vite = true;
|
|
1961
4102
|
resolve(handled);
|
|
1962
4103
|
};
|
|
4104
|
+
const originalSetHeader = res.setHeader.bind(res);
|
|
4105
|
+
const originalWriteHead = res.writeHead?.bind(res);
|
|
4106
|
+
const originalWrite = res.write.bind(res);
|
|
4107
|
+
const originalEnd = res.end.bind(res);
|
|
4108
|
+
const guardedCall = (fn, ...args) => {
|
|
4109
|
+
if (resolved && !ctx.metadata.vite) return;
|
|
4110
|
+
return fn(...args);
|
|
4111
|
+
};
|
|
4112
|
+
res.setHeader = (...args) => guardedCall(originalSetHeader, ...args);
|
|
4113
|
+
if (originalWriteHead) res.writeHead = (...args) => guardedCall(originalWriteHead, ...args);
|
|
4114
|
+
res.write = (...args) => guardedCall(originalWrite, ...args);
|
|
4115
|
+
res.end = (...args) => guardedCall(originalEnd, ...args);
|
|
1963
4116
|
res.on("finish", () => done(true));
|
|
1964
4117
|
res.on("close", () => res.headersSent && done(true));
|
|
1965
4118
|
this.server.middlewares(req, res, () => done(false));
|
|
@@ -1999,23 +4152,14 @@ var DevCommand = class {
|
|
|
1999
4152
|
boot = $inject(AppEntryProvider);
|
|
2000
4153
|
/**
|
|
2001
4154
|
* 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
4155
|
*/
|
|
2006
4156
|
dev = $command({
|
|
2007
4157
|
name: "dev",
|
|
2008
4158
|
description: "Run the project in development mode",
|
|
2009
4159
|
handler: async ({ root }) => {
|
|
2010
|
-
const [expo, react] = await Promise.all([this.pm.hasExpo(root), this.pm.hasReact(root)]);
|
|
2011
4160
|
await this.scaffolder.ensureConfig(root, { tsconfigJson: true });
|
|
2012
|
-
if (expo) {
|
|
2013
|
-
await this.utils.exec("expo start");
|
|
2014
|
-
return;
|
|
2015
|
-
}
|
|
2016
4161
|
const entry = await this.boot.getAppEntry(root);
|
|
2017
4162
|
this.log.debug("Entry file found", { entry });
|
|
2018
|
-
await this.pm.ensureDependency(root, "vite", { exec: (cmd, opts) => this.utils.exec(cmd, opts) });
|
|
2019
4163
|
await this.viteDevServer.init({
|
|
2020
4164
|
root,
|
|
2021
4165
|
entry
|
|
@@ -2255,7 +4399,10 @@ var GenEnvCommand = class {
|
|
|
2255
4399
|
description: "Output file path (e.g., .env)"
|
|
2256
4400
|
})) }),
|
|
2257
4401
|
handler: async ({ root, flags }) => {
|
|
2258
|
-
const
|
|
4402
|
+
const alepha = await this.utils.loadAlephaFromServerEntryFile({
|
|
4403
|
+
root,
|
|
4404
|
+
mode: "development"
|
|
4405
|
+
});
|
|
2259
4406
|
try {
|
|
2260
4407
|
const { env } = alepha.dump();
|
|
2261
4408
|
let dotEnvFile = "";
|
|
@@ -2288,7 +4435,10 @@ var OpenApiCommand = class {
|
|
|
2288
4435
|
description: "Output file path"
|
|
2289
4436
|
})) }),
|
|
2290
4437
|
handler: async ({ root, flags }) => {
|
|
2291
|
-
const
|
|
4438
|
+
const alepha = await this.utils.loadAlephaFromServerEntryFile({
|
|
4439
|
+
root,
|
|
4440
|
+
mode: "development"
|
|
4441
|
+
});
|
|
2292
4442
|
try {
|
|
2293
4443
|
const openapiProvider = alepha.inject(ServerSwaggerProvider);
|
|
2294
4444
|
await alepha.events.emit("configure", alepha);
|
|
@@ -2355,19 +4505,21 @@ var InitCommand = class {
|
|
|
2355
4505
|
lowercase: true
|
|
2356
4506
|
})),
|
|
2357
4507
|
flags: t.object({
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
4508
|
+
ai: t.optional(t.boolean({ description: "Add AI agent instructions (CLAUDE.md if claude CLI installed, else AGENTS.md)" })),
|
|
4509
|
+
pm: t.optional(t.enum([
|
|
4510
|
+
"yarn",
|
|
4511
|
+
"npm",
|
|
4512
|
+
"pnpm",
|
|
4513
|
+
"bun"
|
|
4514
|
+
], { description: "Package manager to use" })),
|
|
4515
|
+
api: t.optional(t.boolean({ description: "Include API module structure (src/api/)" })),
|
|
2366
4516
|
react: t.optional(t.boolean({
|
|
2367
4517
|
aliases: ["r"],
|
|
2368
|
-
description: "Include
|
|
4518
|
+
description: "Include React dependencies and web module (src/web/)"
|
|
2369
4519
|
})),
|
|
2370
|
-
ui: t.optional(t.boolean({ description: "Include
|
|
4520
|
+
ui: t.optional(t.boolean({ description: "Include @alepha/ui (components, auth portal, admin portal)" })),
|
|
4521
|
+
auth: t.optional(t.boolean({ description: "Include authentication (AppSecurity, $uiAuth). Implies --api --ui --react" })),
|
|
4522
|
+
admin: t.optional(t.boolean({ description: "Include admin portal ($uiAdmin). Implies --auth" })),
|
|
2371
4523
|
test: t.optional(t.boolean({ description: "Include Vitest and create test directory" })),
|
|
2372
4524
|
force: t.optional(t.boolean({
|
|
2373
4525
|
aliases: ["f"],
|
|
@@ -2375,11 +4527,19 @@ var InitCommand = class {
|
|
|
2375
4527
|
}))
|
|
2376
4528
|
}),
|
|
2377
4529
|
handler: async ({ run, flags, root, args }) => {
|
|
2378
|
-
if (flags.react) flags.ui = true;
|
|
2379
4530
|
if (args) {
|
|
2380
4531
|
root = this.fs.join(root, args);
|
|
2381
|
-
await this.fs.mkdir(root);
|
|
4532
|
+
await this.fs.mkdir(root, { force: true });
|
|
2382
4533
|
}
|
|
4534
|
+
if (flags.admin) flags.auth = true;
|
|
4535
|
+
if (flags.auth) {
|
|
4536
|
+
flags.api = true;
|
|
4537
|
+
flags.ui = true;
|
|
4538
|
+
}
|
|
4539
|
+
if (flags.ui) flags.react = true;
|
|
4540
|
+
const workspace = await this.pm.getWorkspaceContext(root);
|
|
4541
|
+
let agentType = false;
|
|
4542
|
+
if (flags.ai) agentType = await this.utils.isInstalledAsync("claude") ? "claude" : "agents";
|
|
2383
4543
|
const isExpo = await this.pm.hasExpo(root);
|
|
2384
4544
|
const force = !!flags.force;
|
|
2385
4545
|
await run({
|
|
@@ -2387,46 +4547,60 @@ var InitCommand = class {
|
|
|
2387
4547
|
handler: async () => {
|
|
2388
4548
|
await this.scaffolder.ensureConfig(root, {
|
|
2389
4549
|
force,
|
|
2390
|
-
tsconfigJson:
|
|
2391
|
-
packageJson:
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
4550
|
+
tsconfigJson: !workspace.config.tsconfigJson,
|
|
4551
|
+
packageJson: {
|
|
4552
|
+
...flags,
|
|
4553
|
+
isPackage: workspace.isPackage
|
|
4554
|
+
},
|
|
4555
|
+
biomeJson: !workspace.config.biomeJson,
|
|
4556
|
+
editorconfig: !workspace.config.editorconfig,
|
|
4557
|
+
agentMd: agentType ? {
|
|
4558
|
+
type: agentType,
|
|
2396
4559
|
react: !!flags.react,
|
|
2397
4560
|
ui: !!flags.ui
|
|
2398
4561
|
} : false
|
|
2399
4562
|
});
|
|
2400
|
-
|
|
4563
|
+
await this.scaffolder.ensureMainServerTs(root, {
|
|
4564
|
+
api: !!flags.api,
|
|
4565
|
+
react: !!flags.react && !isExpo,
|
|
4566
|
+
force
|
|
4567
|
+
});
|
|
4568
|
+
if (flags.api) await this.scaffolder.ensureApiProject(root, {
|
|
4569
|
+
auth: !!flags.auth,
|
|
4570
|
+
force
|
|
4571
|
+
});
|
|
4572
|
+
if (flags.react && !isExpo) await this.scaffolder.ensureWebProject(root, {
|
|
4573
|
+
api: !!flags.api,
|
|
4574
|
+
ui: !!flags.ui,
|
|
4575
|
+
auth: !!flags.auth,
|
|
4576
|
+
admin: !!flags.admin,
|
|
4577
|
+
force
|
|
4578
|
+
});
|
|
2401
4579
|
}
|
|
2402
4580
|
});
|
|
2403
|
-
const pmName = await this.pm.getPackageManager(root, flags);
|
|
2404
|
-
if (pmName === "yarn") {
|
|
4581
|
+
const pmName = await this.pm.getPackageManager(workspace.workspaceRoot ?? root, flags.pm ?? workspace.packageManager ?? void 0);
|
|
4582
|
+
if (!workspace.isPackage) if (pmName === "yarn") {
|
|
2405
4583
|
await this.pm.ensureYarn(root);
|
|
2406
4584
|
await run("yarn set version stable", { root });
|
|
2407
4585
|
} else if (pmName === "bun") await this.pm.ensureBun(root);
|
|
2408
4586
|
else if (pmName === "pnpm") await this.pm.ensurePnpm(root);
|
|
2409
4587
|
else await this.pm.ensureNpm(root);
|
|
4588
|
+
const installRoot = workspace.workspaceRoot ?? root;
|
|
2410
4589
|
await run(`${pmName} install`, {
|
|
2411
4590
|
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)
|
|
4591
|
+
root: installRoot
|
|
2421
4592
|
});
|
|
2422
|
-
if (flags.test)
|
|
2423
|
-
await this.scaffolder.ensureTestDir(root);
|
|
2424
|
-
await run(`${pmName} ${pmName === "yarn" ? "add" : "install"} -D vitest`, { alias: "setup testing with Vitest" });
|
|
2425
|
-
}
|
|
4593
|
+
if (flags.test) await this.scaffolder.ensureTestDir(root);
|
|
2426
4594
|
await run(`${pmName} run lint`, {
|
|
2427
4595
|
alias: "running linter",
|
|
2428
|
-
root
|
|
4596
|
+
root: installRoot
|
|
2429
4597
|
});
|
|
4598
|
+
if (!workspace.isPackage) {
|
|
4599
|
+
if (await this.scaffolder.ensureGitRepo(root, { force })) await run("git add .", {
|
|
4600
|
+
alias: "staging generated files",
|
|
4601
|
+
root
|
|
4602
|
+
});
|
|
4603
|
+
}
|
|
2430
4604
|
}
|
|
2431
4605
|
});
|
|
2432
4606
|
};
|
|
@@ -2441,8 +4615,14 @@ var LintCommand = class {
|
|
|
2441
4615
|
name: "lint",
|
|
2442
4616
|
description: "Run linter across the codebase using Biome",
|
|
2443
4617
|
handler: async ({ root }) => {
|
|
2444
|
-
await this.scaffolder.ensureConfig(root, {
|
|
2445
|
-
|
|
4618
|
+
await this.scaffolder.ensureConfig(root, {
|
|
4619
|
+
biomeJson: true,
|
|
4620
|
+
checkWorkspace: true
|
|
4621
|
+
});
|
|
4622
|
+
await this.pm.ensureDependency(root, "@biomejs/biome", {
|
|
4623
|
+
checkWorkspace: true,
|
|
4624
|
+
exec: (cmd, opts) => this.utils.exec(cmd, opts)
|
|
4625
|
+
});
|
|
2446
4626
|
await this.utils.exec("biome check --fix");
|
|
2447
4627
|
}
|
|
2448
4628
|
});
|
|
@@ -2508,6 +4688,7 @@ var TypecheckCommand = class {
|
|
|
2508
4688
|
utils = $inject(AlephaCliUtils);
|
|
2509
4689
|
pm = $inject(PackageManagerUtils);
|
|
2510
4690
|
log = $logger();
|
|
4691
|
+
scaffolder = $inject(ProjectScaffolder);
|
|
2511
4692
|
/**
|
|
2512
4693
|
* Run TypeScript type checking across the codebase with no emit.
|
|
2513
4694
|
*/
|
|
@@ -2517,7 +4698,14 @@ var TypecheckCommand = class {
|
|
|
2517
4698
|
description: "Check TypeScript types across the codebase",
|
|
2518
4699
|
handler: async ({ root }) => {
|
|
2519
4700
|
this.log.info("Starting TypeScript type checking...");
|
|
2520
|
-
await this.
|
|
4701
|
+
await this.scaffolder.ensureConfig(root, {
|
|
4702
|
+
tsconfigJson: true,
|
|
4703
|
+
checkWorkspace: true
|
|
4704
|
+
});
|
|
4705
|
+
await this.pm.ensureDependency(root, "typescript", {
|
|
4706
|
+
checkWorkspace: true,
|
|
4707
|
+
exec: (cmd, opts) => this.utils.exec(cmd, opts)
|
|
4708
|
+
});
|
|
2521
4709
|
await this.utils.exec("tsc --noEmit");
|
|
2522
4710
|
this.log.info("TypeScript type checking completed successfully.");
|
|
2523
4711
|
}
|
|
@@ -2561,15 +4749,6 @@ var VerifyCommand = class {
|
|
|
2561
4749
|
//#endregion
|
|
2562
4750
|
//#region ../../src/cli/apps/AlephaCli.ts
|
|
2563
4751
|
/**
|
|
2564
|
-
* Register `tsx` when running in Node.js, ignore for Bun.
|
|
2565
|
-
*
|
|
2566
|
-
* It's required to have a full TypeScript support. (mostly .tsx files)
|
|
2567
|
-
*/
|
|
2568
|
-
if (typeof Bun === "undefined") {
|
|
2569
|
-
const { register } = await import("tsx/esm/api");
|
|
2570
|
-
register();
|
|
2571
|
-
}
|
|
2572
|
-
/**
|
|
2573
4752
|
* Allow to extend Alepha CLI via `alepha.config.ts` file located in the project root.
|
|
2574
4753
|
*/
|
|
2575
4754
|
var AlephaCliExtension = class {
|
|
@@ -2633,6 +4812,7 @@ var AlephaPackageBuilderCli = class {
|
|
|
2633
4812
|
pkgData.exports[path].types = `./src/${item.name}/index.ts`;
|
|
2634
4813
|
if (item.native) pkgData.exports[path]["react-native"] = `./src/${item.name}/index.native.ts`;
|
|
2635
4814
|
else if (item.browser) pkgData.exports[path]["react-native"] = `./src/${item.name}/index.browser.ts`;
|
|
4815
|
+
if (item.workerd) pkgData.exports[path].workerd = `./src/${item.name}/index.workerd.ts`;
|
|
2636
4816
|
if (item.browser) pkgData.exports[path].browser = `./src/${item.name}/index.browser.ts`;
|
|
2637
4817
|
if (item.bun) pkgData.exports[path].bun = `./src/${item.name}/index.bun.ts`;
|
|
2638
4818
|
pkgData.exports[path].import = `./src/${item.name}/index.ts`;
|
|
@@ -2789,6 +4969,7 @@ async function analyzeModules(srcDir, packageName) {
|
|
|
2789
4969
|
const hasNative = await fileExists(join(modulePath, "index.native.ts"));
|
|
2790
4970
|
const hasBun = await fileExists(join(modulePath, "index.bun.ts"));
|
|
2791
4971
|
const hasNode = await fileExists(join(modulePath, "index.node.ts"));
|
|
4972
|
+
const hasEdge = await fileExists(join(modulePath, "index.workerd.ts"));
|
|
2792
4973
|
const files = await getAllFiles(modulePath);
|
|
2793
4974
|
for (const file of files) {
|
|
2794
4975
|
const deps = extractAlephaDependencies(await readFile(file, "utf-8"), packageName, moduleName);
|
|
@@ -2803,6 +4984,7 @@ async function analyzeModules(srcDir, packageName) {
|
|
|
2803
4984
|
dependencies: Array.from(dependencies)
|
|
2804
4985
|
};
|
|
2805
4986
|
if (hasNative) module.native = true;
|
|
4987
|
+
if (hasEdge) module.workerd = true;
|
|
2806
4988
|
if (hasBrowser) module.browser = true;
|
|
2807
4989
|
if (hasBun) module.bun = true;
|
|
2808
4990
|
if (hasNode) module.node = true;
|