alepha 0.15.1 → 0.15.2
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/dist/api/audits/index.d.ts +342 -365
- 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 +180 -173
- 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 +294 -301
- 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 +1079 -769
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +2534 -218
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts +10 -4
- 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 +432 -8
- package/dist/bucket/index.d.ts.map +1 -1
- package/dist/bucket/index.js +1856 -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 +488 -5612
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2326 -311
- package/dist/cli/index.js.map +1 -1
- package/dist/command/index.d.ts +194 -46
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js +1995 -60
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +42 -19
- 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 +62 -19
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +42 -19
- 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 +315 -20
- package/dist/email/index.d.ts.map +1 -1
- package/dist/email/index.js +1852 -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 +15 -35
- 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 +7 -4
- package/dist/orm/index.bun.js.map +1 -1
- package/dist/orm/index.d.ts +514 -540
- package/dist/orm/index.d.ts.map +1 -1
- package/dist/orm/index.js +24 -49
- 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 +1980 -0
- package/dist/react/router/index.browser.js.map +1 -0
- package/dist/react/router/index.d.ts +2068 -0
- package/dist/react/router/index.d.ts.map +1 -0
- package/dist/react/router/index.js +4932 -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 +22 -25
- 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 +110 -19
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +157 -26
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +179 -174
- 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 +115 -14
- 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 +25 -19
- 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 +50 -45
- 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 +1848 -5
- package/dist/server/static/index.js.map +1 -1
- package/dist/server/swagger/index.d.ts +301 -6
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/server/swagger/index.js +1849 -6
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/sms/index.d.ts +301 -7
- package/dist/sms/index.d.ts.map +1 -1
- package/dist/sms/index.js +1851 -7
- package/dist/sms/index.js.map +1 -1
- package/dist/system/index.browser.js +496 -0
- package/dist/system/index.browser.js.map +1 -0
- package/dist/{file → system}/index.d.ts +335 -16
- package/dist/system/index.d.ts.map +1 -0
- package/dist/{file → system}/index.js +412 -20
- 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 +4 -6271
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +8 -3
- 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 +80 -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 +52 -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 +1 -1
- package/src/cli/commands/build.ts +1 -5
- package/src/cli/commands/db.ts +17 -11
- 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 +544 -0
- package/src/cli/commands/init.ts +89 -55
- 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 +36 -8
- package/src/cli/services/AlephaCliUtils.ts +37 -122
- package/src/cli/services/PackageManagerUtils.ts +127 -11
- package/src/cli/services/ProjectScaffolder.ts +122 -77
- package/src/cli/services/ViteUtils.ts +82 -0
- package/src/cli/{assets/claudeMd.ts → templates/agentMd.ts} +32 -24
- 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 +50 -0
- package/src/cli/{assets → templates}/webHelloComponentTsx.ts +2 -2
- package/src/command/helpers/Runner.spec.ts +4 -0
- package/src/command/helpers/Runner.ts +3 -21
- package/src/command/index.ts +12 -4
- package/src/command/providers/CliProvider.spec.ts +1067 -0
- package/src/command/providers/CliProvider.ts +203 -40
- package/src/core/Alepha.ts +2 -2
- 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 +4 -2
- package/src/orm/index.ts +21 -47
- package/src/orm/providers/drivers/BunSqliteProvider.ts +1 -0
- 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 +142 -0
- package/src/react/router/primitives/$page.browser.spec.tsx +851 -0
- package/src/react/router/primitives/$page.spec.tsx +708 -0
- package/src/react/router/primitives/$page.ts +497 -0
- package/src/react/router/providers/ReactBrowserProvider.ts +309 -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/ReactServerProvider.spec.tsx +316 -0
- package/src/react/router/providers/ReactServerProvider.ts +558 -0
- package/src/react/router/providers/ReactServerTemplateProvider.ts +979 -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 +13 -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 +36 -22
- 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.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 +11 -0
- package/src/system/index.ts +62 -0
- package/src/{file → system}/providers/FileSystemProvider.ts +16 -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 +36 -0
- 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 +17 -1
- 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/mainServerTs.ts +0 -24
- package/src/cli/assets/webAppRouterTs.ts +0 -16
- 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}/apiIndexTs.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,1852 @@
|
|
|
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
|
+
await mkdir(path, options);
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Lists files in a directory.
|
|
1378
|
+
*
|
|
1379
|
+
* @param path - The directory path to list
|
|
1380
|
+
* @param options - List options
|
|
1381
|
+
* @returns Array of filenames
|
|
1382
|
+
*
|
|
1383
|
+
* @example
|
|
1384
|
+
* ```typescript
|
|
1385
|
+
* const fs = alepha.inject(NodeFileSystemProvider);
|
|
1386
|
+
*
|
|
1387
|
+
* // List files in a directory
|
|
1388
|
+
* const files = await fs.ls("/tmp");
|
|
1389
|
+
* console.log(files); // ["file1.txt", "file2.txt", "subdir"]
|
|
1390
|
+
*
|
|
1391
|
+
* // List with hidden files
|
|
1392
|
+
* const allFiles = await fs.ls("/tmp", { hidden: true });
|
|
1393
|
+
*
|
|
1394
|
+
* // List recursively
|
|
1395
|
+
* const allFilesRecursive = await fs.ls("/tmp", { recursive: true });
|
|
1396
|
+
* ```
|
|
1397
|
+
*/
|
|
1398
|
+
async ls(path, options) {
|
|
1399
|
+
const entries = await readdir(path);
|
|
1400
|
+
const filteredEntries = options?.hidden ? entries : entries.filter((e) => !e.startsWith("."));
|
|
1401
|
+
if (options?.recursive) {
|
|
1402
|
+
const allFiles = [];
|
|
1403
|
+
for (const entry of filteredEntries) {
|
|
1404
|
+
const fullPath = join(path, entry);
|
|
1405
|
+
if ((await stat(fullPath)).isDirectory()) {
|
|
1406
|
+
allFiles.push(entry);
|
|
1407
|
+
const subFiles = await this.ls(fullPath, options);
|
|
1408
|
+
allFiles.push(...subFiles.map((f) => join(entry, f)));
|
|
1409
|
+
} else allFiles.push(entry);
|
|
1410
|
+
}
|
|
1411
|
+
return allFiles;
|
|
1412
|
+
}
|
|
1413
|
+
return filteredEntries;
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Checks if a file or directory exists.
|
|
1417
|
+
*
|
|
1418
|
+
* @param path - The path to check
|
|
1419
|
+
* @returns True if the path exists, false otherwise
|
|
1420
|
+
*
|
|
1421
|
+
* @example
|
|
1422
|
+
* ```typescript
|
|
1423
|
+
* const fs = alepha.inject(NodeFileSystemProvider);
|
|
1424
|
+
*
|
|
1425
|
+
* if (await fs.exists("/tmp/file.txt")) {
|
|
1426
|
+
* console.log("File exists");
|
|
1427
|
+
* }
|
|
1428
|
+
* ```
|
|
1429
|
+
*/
|
|
1430
|
+
async exists(path) {
|
|
1431
|
+
try {
|
|
1432
|
+
await access(path);
|
|
1433
|
+
return true;
|
|
1434
|
+
} catch {
|
|
1435
|
+
return false;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Reads the content of a file.
|
|
1440
|
+
*
|
|
1441
|
+
* @param path - The file path to read
|
|
1442
|
+
* @returns The file content as a Buffer
|
|
1443
|
+
*
|
|
1444
|
+
* @example
|
|
1445
|
+
* ```typescript
|
|
1446
|
+
* const fs = alepha.inject(NodeFileSystemProvider);
|
|
1447
|
+
*
|
|
1448
|
+
* const buffer = await fs.readFile("/tmp/file.txt");
|
|
1449
|
+
* console.log(buffer.toString("utf-8"));
|
|
1450
|
+
* ```
|
|
1451
|
+
*/
|
|
1452
|
+
async readFile(path) {
|
|
1453
|
+
return await readFile(path);
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Writes data to a file.
|
|
1457
|
+
*
|
|
1458
|
+
* @param path - The file path to write to
|
|
1459
|
+
* @param data - The data to write (Buffer or string)
|
|
1460
|
+
*
|
|
1461
|
+
* @example
|
|
1462
|
+
* ```typescript
|
|
1463
|
+
* const fs = alepha.inject(NodeFileSystemProvider);
|
|
1464
|
+
*
|
|
1465
|
+
* // Write string
|
|
1466
|
+
* await fs.writeFile("/tmp/file.txt", "Hello, world!");
|
|
1467
|
+
*
|
|
1468
|
+
* // Write Buffer
|
|
1469
|
+
* await fs.writeFile("/tmp/file.bin", Buffer.from([0x01, 0x02, 0x03]));
|
|
1470
|
+
* ```
|
|
1471
|
+
*/
|
|
1472
|
+
async writeFile(path, data) {
|
|
1473
|
+
if (isFileLike(data)) {
|
|
1474
|
+
await writeFile(path, Readable.from(data.stream()));
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
await writeFile(path, data);
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Reads the content of a file as a string.
|
|
1481
|
+
*
|
|
1482
|
+
* @param path - The file path to read
|
|
1483
|
+
* @returns The file content as a string
|
|
1484
|
+
*
|
|
1485
|
+
* @example
|
|
1486
|
+
* ```typescript
|
|
1487
|
+
* const fs = alepha.inject(NodeFileSystemProvider);
|
|
1488
|
+
* const content = await fs.readTextFile("/tmp/file.txt");
|
|
1489
|
+
* ```
|
|
1490
|
+
*/
|
|
1491
|
+
async readTextFile(path) {
|
|
1492
|
+
return (await this.readFile(path)).toString("utf-8");
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* Reads the content of a file as JSON.
|
|
1496
|
+
*
|
|
1497
|
+
* @param path - The file path to read
|
|
1498
|
+
* @returns The parsed JSON content
|
|
1499
|
+
*
|
|
1500
|
+
* @example
|
|
1501
|
+
* ```typescript
|
|
1502
|
+
* const fs = alepha.inject(NodeFileSystemProvider);
|
|
1503
|
+
* const config = await fs.readJsonFile<{ name: string }>("/tmp/config.json");
|
|
1504
|
+
* ```
|
|
1505
|
+
*/
|
|
1506
|
+
async readJsonFile(path) {
|
|
1507
|
+
const text = await this.readTextFile(path);
|
|
1508
|
+
return this.json.parse(text);
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* Creates a FileLike object from a Web File.
|
|
1512
|
+
*
|
|
1513
|
+
* @protected
|
|
1514
|
+
*/
|
|
1515
|
+
createFileFromWebFile(source, options = {}) {
|
|
1516
|
+
const name = options.name ?? source.name;
|
|
1517
|
+
return {
|
|
1518
|
+
name,
|
|
1519
|
+
type: options.type ?? (source.type || this.detector.getContentType(name)),
|
|
1520
|
+
size: options.size ?? source.size ?? 0,
|
|
1521
|
+
lastModified: source.lastModified || Date.now(),
|
|
1522
|
+
stream: () => source.stream(),
|
|
1523
|
+
arrayBuffer: async () => {
|
|
1524
|
+
return await source.arrayBuffer();
|
|
1525
|
+
},
|
|
1526
|
+
text: async () => {
|
|
1527
|
+
return await source.text();
|
|
1528
|
+
}
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
/**
|
|
1532
|
+
* Creates a FileLike object from a Buffer.
|
|
1533
|
+
*
|
|
1534
|
+
* @protected
|
|
1535
|
+
*/
|
|
1536
|
+
createFileFromBuffer(source, options = {}) {
|
|
1537
|
+
const name = options.name ?? "file";
|
|
1538
|
+
return {
|
|
1539
|
+
name,
|
|
1540
|
+
type: options.type ?? this.detector.getContentType(options.name ?? name),
|
|
1541
|
+
size: source.byteLength,
|
|
1542
|
+
lastModified: Date.now(),
|
|
1543
|
+
stream: () => Readable.from(source),
|
|
1544
|
+
arrayBuffer: async () => {
|
|
1545
|
+
return this.bufferToArrayBuffer(source);
|
|
1546
|
+
},
|
|
1547
|
+
text: async () => {
|
|
1548
|
+
return source.toString("utf-8");
|
|
1549
|
+
}
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Creates a FileLike object from a stream.
|
|
1554
|
+
*
|
|
1555
|
+
* @protected
|
|
1556
|
+
*/
|
|
1557
|
+
createFileFromStream(source, options = {}) {
|
|
1558
|
+
let buffer = null;
|
|
1559
|
+
return {
|
|
1560
|
+
name: options.name ?? "file",
|
|
1561
|
+
type: options.type ?? this.detector.getContentType(options.name ?? "file"),
|
|
1562
|
+
size: options.size ?? 0,
|
|
1563
|
+
lastModified: Date.now(),
|
|
1564
|
+
stream: () => source,
|
|
1565
|
+
_buffer: null,
|
|
1566
|
+
arrayBuffer: async () => {
|
|
1567
|
+
buffer ??= await this.streamToBuffer(source);
|
|
1568
|
+
return this.bufferToArrayBuffer(buffer);
|
|
1569
|
+
},
|
|
1570
|
+
text: async () => {
|
|
1571
|
+
buffer ??= await this.streamToBuffer(source);
|
|
1572
|
+
return buffer.toString("utf-8");
|
|
1573
|
+
}
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Creates a FileLike object from a URL.
|
|
1578
|
+
*
|
|
1579
|
+
* @protected
|
|
1580
|
+
*/
|
|
1581
|
+
createFileFromUrl(url, options = {}) {
|
|
1582
|
+
const parsedUrl = new URL(url);
|
|
1583
|
+
const filename = options.name || parsedUrl.pathname.split("/").pop() || "file";
|
|
1584
|
+
let buffer = null;
|
|
1585
|
+
return {
|
|
1586
|
+
name: filename,
|
|
1587
|
+
type: options.type ?? this.detector.getContentType(filename),
|
|
1588
|
+
size: 0,
|
|
1589
|
+
lastModified: Date.now(),
|
|
1590
|
+
stream: () => this.createStreamFromUrl(url),
|
|
1591
|
+
arrayBuffer: async () => {
|
|
1592
|
+
buffer ??= await this.loadFromUrl(url);
|
|
1593
|
+
return this.bufferToArrayBuffer(buffer);
|
|
1594
|
+
},
|
|
1595
|
+
text: async () => {
|
|
1596
|
+
buffer ??= await this.loadFromUrl(url);
|
|
1597
|
+
return buffer.toString("utf-8");
|
|
1598
|
+
},
|
|
1599
|
+
filepath: url
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
/**
|
|
1603
|
+
* Gets a streaming response from a URL.
|
|
1604
|
+
*
|
|
1605
|
+
* @protected
|
|
1606
|
+
*/
|
|
1607
|
+
getStreamingResponse(url) {
|
|
1608
|
+
const stream = new PassThrough();
|
|
1609
|
+
fetch(url).then((res) => Readable.fromWeb(res.body).pipe(stream)).catch((err) => stream.destroy(err));
|
|
1610
|
+
return stream;
|
|
1611
|
+
}
|
|
1612
|
+
/**
|
|
1613
|
+
* Loads data from a URL.
|
|
1614
|
+
*
|
|
1615
|
+
* @protected
|
|
1616
|
+
*/
|
|
1617
|
+
async loadFromUrl(url) {
|
|
1618
|
+
const parsedUrl = new URL(url);
|
|
1619
|
+
if (parsedUrl.protocol === "file:") return await readFile(fileURLToPath(url));
|
|
1620
|
+
else if (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:") {
|
|
1621
|
+
const response = await fetch(url);
|
|
1622
|
+
if (!response.ok) throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
|
|
1623
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
1624
|
+
return Buffer.from(arrayBuffer);
|
|
1625
|
+
} else throw new Error(`Unsupported protocol: ${parsedUrl.protocol}`);
|
|
1626
|
+
}
|
|
1627
|
+
/**
|
|
1628
|
+
* Creates a stream from a URL.
|
|
1629
|
+
*
|
|
1630
|
+
* @protected
|
|
1631
|
+
*/
|
|
1632
|
+
createStreamFromUrl(url) {
|
|
1633
|
+
const parsedUrl = new URL(url);
|
|
1634
|
+
if (parsedUrl.protocol === "file:") return createReadStream(fileURLToPath(url));
|
|
1635
|
+
else if (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:") return this.getStreamingResponse(url);
|
|
1636
|
+
else throw new AlephaError(`Unsupported protocol: ${parsedUrl.protocol}`);
|
|
1637
|
+
}
|
|
1638
|
+
/**
|
|
1639
|
+
* Converts a stream-like object to a Buffer.
|
|
1640
|
+
*
|
|
1641
|
+
* @protected
|
|
1642
|
+
*/
|
|
1643
|
+
async streamToBuffer(streamLike) {
|
|
1644
|
+
const stream = streamLike instanceof Readable ? streamLike : Readable.fromWeb(streamLike);
|
|
1645
|
+
return new Promise((resolve, reject) => {
|
|
1646
|
+
const buffer = [];
|
|
1647
|
+
stream.on("data", (chunk) => buffer.push(Buffer.from(chunk)));
|
|
1648
|
+
stream.on("end", () => resolve(Buffer.concat(buffer)));
|
|
1649
|
+
stream.on("error", (err) => reject(new AlephaError("Error converting stream", { cause: err })));
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* Converts a Node.js Buffer to an ArrayBuffer.
|
|
1654
|
+
*
|
|
1655
|
+
* @protected
|
|
1656
|
+
*/
|
|
1657
|
+
bufferToArrayBuffer(buffer) {
|
|
1658
|
+
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
1659
|
+
}
|
|
1660
|
+
};
|
|
1661
|
+
|
|
1662
|
+
//#endregion
|
|
1663
|
+
//#region ../../src/system/providers/NodeShellProvider.ts
|
|
1664
|
+
/**
|
|
1665
|
+
* Node.js implementation of ShellProvider.
|
|
1666
|
+
*
|
|
1667
|
+
* Executes shell commands using Node.js child_process module.
|
|
1668
|
+
* Supports binary resolution from node_modules/.bin for local packages.
|
|
1669
|
+
*/
|
|
1670
|
+
var NodeShellProvider = class {
|
|
1671
|
+
log = $logger();
|
|
1672
|
+
fs = $inject(FileSystemProvider);
|
|
1673
|
+
/**
|
|
1674
|
+
* Run a shell command or binary.
|
|
1675
|
+
*/
|
|
1676
|
+
async run(command, options = {}) {
|
|
1677
|
+
const { resolve = false, capture = false, root, env } = options;
|
|
1678
|
+
const cwd = root ?? process.cwd();
|
|
1679
|
+
this.log.debug(`Shell: ${command}`, {
|
|
1680
|
+
cwd,
|
|
1681
|
+
resolve,
|
|
1682
|
+
capture
|
|
1683
|
+
});
|
|
1684
|
+
let executable;
|
|
1685
|
+
let args;
|
|
1686
|
+
if (resolve) {
|
|
1687
|
+
const [bin, ...rest] = command.split(" ");
|
|
1688
|
+
executable = await this.resolveExecutable(bin, cwd);
|
|
1689
|
+
args = rest;
|
|
1690
|
+
} else [executable, ...args] = command.split(" ");
|
|
1691
|
+
if (capture) return this.execCapture(command, {
|
|
1692
|
+
cwd,
|
|
1693
|
+
env
|
|
1694
|
+
});
|
|
1695
|
+
return this.execInherit(executable, args, {
|
|
1696
|
+
cwd,
|
|
1697
|
+
env
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
/**
|
|
1701
|
+
* Execute command with inherited stdio (streams to terminal).
|
|
1702
|
+
*/
|
|
1703
|
+
async execInherit(executable, args, options) {
|
|
1704
|
+
const proc = spawn(executable, args, {
|
|
1705
|
+
stdio: "inherit",
|
|
1706
|
+
cwd: options.cwd,
|
|
1707
|
+
env: {
|
|
1708
|
+
...process.env,
|
|
1709
|
+
...options.env
|
|
1710
|
+
}
|
|
1711
|
+
});
|
|
1712
|
+
return new Promise((resolve, reject) => {
|
|
1713
|
+
proc.on("exit", (code) => {
|
|
1714
|
+
if (code === 0 || code === null) resolve("");
|
|
1715
|
+
else reject(new AlephaError(`Command exited with code ${code}`));
|
|
1716
|
+
});
|
|
1717
|
+
proc.on("error", reject);
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
/**
|
|
1721
|
+
* Execute command and capture stdout.
|
|
1722
|
+
*/
|
|
1723
|
+
execCapture(command, options) {
|
|
1724
|
+
return new Promise((resolve, reject) => {
|
|
1725
|
+
exec(command, {
|
|
1726
|
+
cwd: options.cwd,
|
|
1727
|
+
env: {
|
|
1728
|
+
...process.env,
|
|
1729
|
+
LOG_FORMAT: "pretty",
|
|
1730
|
+
...options.env
|
|
1731
|
+
}
|
|
1732
|
+
}, (err, stdout) => {
|
|
1733
|
+
if (err) {
|
|
1734
|
+
err.stdout = stdout;
|
|
1735
|
+
reject(err);
|
|
1736
|
+
} else resolve(stdout);
|
|
1737
|
+
});
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
/**
|
|
1741
|
+
* Resolve executable path from node_modules/.bin.
|
|
1742
|
+
*
|
|
1743
|
+
* Search order:
|
|
1744
|
+
* 1. Local: node_modules/.bin/
|
|
1745
|
+
* 2. Pnpm nested: node_modules/alepha/node_modules/.bin/
|
|
1746
|
+
* 3. Monorepo: Walk up to 3 parent directories
|
|
1747
|
+
*/
|
|
1748
|
+
async resolveExecutable(name, root) {
|
|
1749
|
+
const suffix = process.platform === "win32" ? ".cmd" : "";
|
|
1750
|
+
let execPath = await this.findExecutable(root, `node_modules/.bin/${name}${suffix}`);
|
|
1751
|
+
if (!execPath) execPath = await this.findExecutable(root, `node_modules/alepha/node_modules/.bin/${name}${suffix}`);
|
|
1752
|
+
if (!execPath) {
|
|
1753
|
+
let parentDir = this.fs.join(root, "..");
|
|
1754
|
+
for (let i = 0; i < 3; i++) {
|
|
1755
|
+
execPath = await this.findExecutable(parentDir, `node_modules/.bin/${name}${suffix}`);
|
|
1756
|
+
if (execPath) break;
|
|
1757
|
+
parentDir = this.fs.join(parentDir, "..");
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
if (!execPath) throw new AlephaError(`Could not find executable for '${name}'. Make sure the package is installed.`);
|
|
1761
|
+
return execPath;
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* Check if executable exists at path.
|
|
1765
|
+
*/
|
|
1766
|
+
async findExecutable(root, relativePath) {
|
|
1767
|
+
const fullPath = this.fs.join(root, relativePath);
|
|
1768
|
+
if (await this.fs.exists(fullPath)) return fullPath;
|
|
1769
|
+
}
|
|
1770
|
+
/**
|
|
1771
|
+
* Check if a command is installed and available in the system PATH.
|
|
1772
|
+
*/
|
|
1773
|
+
isInstalled(command) {
|
|
1774
|
+
return new Promise((resolve) => {
|
|
1775
|
+
exec(process.platform === "win32" ? `where ${command}` : `command -v ${command}`, (error) => resolve(!error));
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1778
|
+
};
|
|
1779
|
+
|
|
1780
|
+
//#endregion
|
|
1781
|
+
//#region ../../src/system/providers/ShellProvider.ts
|
|
1782
|
+
/**
|
|
1783
|
+
* Abstract provider for executing shell commands and binaries.
|
|
1784
|
+
*
|
|
1785
|
+
* Implementations:
|
|
1786
|
+
* - `NodeShellProvider` - Real shell execution using Node.js child_process
|
|
1787
|
+
* - `MemoryShellProvider` - In-memory mock for testing
|
|
1788
|
+
*
|
|
1789
|
+
* @example
|
|
1790
|
+
* ```typescript
|
|
1791
|
+
* class MyService {
|
|
1792
|
+
* protected readonly shell = $inject(ShellProvider);
|
|
1793
|
+
*
|
|
1794
|
+
* async build() {
|
|
1795
|
+
* // Run shell command directly
|
|
1796
|
+
* await this.shell.run("yarn install");
|
|
1797
|
+
*
|
|
1798
|
+
* // Run local binary with resolution
|
|
1799
|
+
* await this.shell.run("vite build", { resolve: true });
|
|
1800
|
+
*
|
|
1801
|
+
* // Capture output
|
|
1802
|
+
* const output = await this.shell.run("echo hello", { capture: true });
|
|
1803
|
+
* }
|
|
1804
|
+
* }
|
|
1805
|
+
* ```
|
|
1806
|
+
*/
|
|
1807
|
+
var ShellProvider = class {};
|
|
1808
|
+
|
|
1809
|
+
//#endregion
|
|
1810
|
+
//#region ../../src/system/index.ts
|
|
1811
|
+
/**
|
|
1812
|
+
* | type | quality | stability |
|
|
1813
|
+
* |------|---------|-----------|
|
|
1814
|
+
* | tooling | standard | stable |
|
|
1815
|
+
*
|
|
1816
|
+
* System-level abstractions for portable code across runtimes.
|
|
1817
|
+
*
|
|
1818
|
+
* **Features:**
|
|
1819
|
+
* - File system operations (read, write, exists, etc.)
|
|
1820
|
+
* - Shell command execution
|
|
1821
|
+
* - File type detection and MIME utilities
|
|
1822
|
+
* - Memory implementations for testing
|
|
1823
|
+
*
|
|
1824
|
+
* @module alepha.system
|
|
1825
|
+
*/
|
|
1826
|
+
const AlephaSystem = $module({
|
|
1827
|
+
name: "alepha.system",
|
|
1828
|
+
primitives: [],
|
|
1829
|
+
services: [
|
|
1830
|
+
FileDetector,
|
|
1831
|
+
FileSystemProvider,
|
|
1832
|
+
MemoryFileSystemProvider,
|
|
1833
|
+
NodeFileSystemProvider,
|
|
1834
|
+
ShellProvider,
|
|
1835
|
+
MemoryShellProvider,
|
|
1836
|
+
NodeShellProvider
|
|
1837
|
+
],
|
|
1838
|
+
register: (alepha) => alepha.with({
|
|
1839
|
+
optional: true,
|
|
1840
|
+
provide: FileSystemProvider,
|
|
1841
|
+
use: NodeFileSystemProvider
|
|
1842
|
+
}).with({
|
|
1843
|
+
optional: true,
|
|
1844
|
+
provide: ShellProvider,
|
|
1845
|
+
use: alepha.isTest() ? MemoryShellProvider : NodeShellProvider
|
|
1846
|
+
})
|
|
1847
|
+
});
|
|
1848
|
+
|
|
1849
|
+
//#endregion
|
|
14
1850
|
//#region ../../src/core/constants/KIND.ts
|
|
15
1851
|
/**
|
|
16
1852
|
* Used for identifying primitives.
|
|
@@ -191,9 +2027,10 @@ var AppEntryProvider = class {
|
|
|
191
2027
|
};
|
|
192
2028
|
|
|
193
2029
|
//#endregion
|
|
194
|
-
//#region ../../src/cli/
|
|
195
|
-
var
|
|
2030
|
+
//#region ../../src/cli/services/ViteUtils.ts
|
|
2031
|
+
var ViteUtils = class {
|
|
196
2032
|
fs = $inject(FileSystemProvider);
|
|
2033
|
+
viteDevServer;
|
|
197
2034
|
generateIndexHtml(entry) {
|
|
198
2035
|
const style = entry.style;
|
|
199
2036
|
const browser = entry.browser ?? entry.server;
|
|
@@ -213,15 +2050,6 @@ ${style ? `<link rel="stylesheet" href="/${style}" />` : ""}
|
|
|
213
2050
|
</html>
|
|
214
2051
|
`.trim();
|
|
215
2052
|
}
|
|
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
2053
|
/**
|
|
226
2054
|
* We need to close the Vite dev server after build is done.
|
|
227
2055
|
*/
|
|
@@ -238,10 +2066,10 @@ var ViteBuildProvider = class {
|
|
|
238
2066
|
await this.viteDevServer?.close();
|
|
239
2067
|
}
|
|
240
2068
|
});
|
|
241
|
-
async
|
|
2069
|
+
async runAlepha(opts) {
|
|
242
2070
|
const { createServer } = await importVite();
|
|
2071
|
+
process.env.NODE_ENV = opts.mode;
|
|
243
2072
|
process.env.ALEPHA_CLI_IMPORT = "true";
|
|
244
|
-
process.env.NODE_ENV = "production";
|
|
245
2073
|
process.env.LOG_LEVEL ??= "warn";
|
|
246
2074
|
/**
|
|
247
2075
|
* 01/26 Vite 7
|
|
@@ -257,6 +2085,21 @@ var ViteBuildProvider = class {
|
|
|
257
2085
|
await this.viteDevServer.ssrLoadModule(opts.entry.server);
|
|
258
2086
|
const alepha = globalThis.__alepha;
|
|
259
2087
|
if (!alepha) throw new AlephaError("Alepha instance not found after loading entry module");
|
|
2088
|
+
return alepha;
|
|
2089
|
+
}
|
|
2090
|
+
};
|
|
2091
|
+
|
|
2092
|
+
//#endregion
|
|
2093
|
+
//#region ../../src/cli/providers/ViteBuildProvider.ts
|
|
2094
|
+
var ViteBuildProvider = class {
|
|
2095
|
+
alepha;
|
|
2096
|
+
appEntry;
|
|
2097
|
+
viteUtils = $inject(ViteUtils);
|
|
2098
|
+
async init(opts) {
|
|
2099
|
+
const alepha = await this.viteUtils.runAlepha({
|
|
2100
|
+
entry: opts.entry,
|
|
2101
|
+
mode: "production"
|
|
2102
|
+
});
|
|
260
2103
|
this.alepha = alepha;
|
|
261
2104
|
this.appEntry = opts.entry;
|
|
262
2105
|
return alepha;
|
|
@@ -272,7 +2115,7 @@ var ViteBuildProvider = class {
|
|
|
272
2115
|
}
|
|
273
2116
|
generateIndexHtml() {
|
|
274
2117
|
if (!this.appEntry) throw new AlephaError("ViteBuildProvider not initialized");
|
|
275
|
-
return this.
|
|
2118
|
+
return this.viteUtils.generateIndexHtml(this.appEntry);
|
|
276
2119
|
}
|
|
277
2120
|
};
|
|
278
2121
|
|
|
@@ -292,44 +2135,23 @@ var AlephaCliUtils = class {
|
|
|
292
2135
|
fs = $inject(FileSystemProvider);
|
|
293
2136
|
envUtils = $inject(EnvUtils);
|
|
294
2137
|
boot = $inject(AppEntryProvider);
|
|
2138
|
+
shell = $inject(ShellProvider);
|
|
2139
|
+
viteUtils = $inject(ViteUtils);
|
|
295
2140
|
/**
|
|
296
2141
|
* Execute a command with inherited stdio.
|
|
2142
|
+
*
|
|
2143
|
+
* @param command - The command to execute
|
|
2144
|
+
* @param options.root - Working directory
|
|
2145
|
+
* @param options.env - Additional environment variables
|
|
2146
|
+
* @param options.global - If true, run command directly without resolving from node_modules
|
|
297
2147
|
*/
|
|
298
2148
|
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);
|
|
2149
|
+
await this.shell.run(command, {
|
|
2150
|
+
root: options.root,
|
|
2151
|
+
env: options.env,
|
|
2152
|
+
resolve: !options.global,
|
|
2153
|
+
capture: false
|
|
2154
|
+
});
|
|
333
2155
|
}
|
|
334
2156
|
/**
|
|
335
2157
|
* Write a configuration file to node_modules/.alepha directory.
|
|
@@ -342,33 +2164,14 @@ var AlephaCliUtils = class {
|
|
|
342
2164
|
this.log.debug(`Config file written: ${path}`);
|
|
343
2165
|
return path;
|
|
344
2166
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
entry = this.fs.join(root, explicitEntry);
|
|
354
|
-
if (!await this.fs.exists(entry)) throw new AlephaError(`Explicit server entry file "${explicitEntry}" not found.`);
|
|
355
|
-
} else {
|
|
356
|
-
const appEntry = await this.boot.getAppEntry(root);
|
|
357
|
-
entry = this.fs.join(root, appEntry.server);
|
|
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}`);
|
|
2167
|
+
async loadAlephaFromServerEntryFile(opts) {
|
|
2168
|
+
let entry;
|
|
2169
|
+
if ("root" in opts) entry = await this.boot.getAppEntry(opts.root);
|
|
2170
|
+
else entry = opts.entry;
|
|
2171
|
+
return await this.viteUtils.runAlepha({
|
|
2172
|
+
entry,
|
|
2173
|
+
mode: opts.mode
|
|
2174
|
+
});
|
|
372
2175
|
}
|
|
373
2176
|
/**
|
|
374
2177
|
* Generate JavaScript code for Drizzle entities export.
|
|
@@ -396,12 +2199,39 @@ ${models.map((it) => `export const ${it} = models["${it}"];`).join("\n")}
|
|
|
396
2199
|
async exists(root, path) {
|
|
397
2200
|
return this.fs.exists(this.fs.join(root, path));
|
|
398
2201
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
2202
|
+
/**
|
|
2203
|
+
* Check if a command is installed and available in the system PATH.
|
|
2204
|
+
*/
|
|
2205
|
+
isInstalledAsync(cmd) {
|
|
2206
|
+
return this.shell.isInstalled(cmd);
|
|
402
2207
|
}
|
|
403
2208
|
};
|
|
404
2209
|
|
|
2210
|
+
//#endregion
|
|
2211
|
+
//#region ../../package.json
|
|
2212
|
+
var devDependencies = {
|
|
2213
|
+
"@biomejs/biome": "^2.3.13",
|
|
2214
|
+
"@electric-sql/pglite": "^0.3.15",
|
|
2215
|
+
"@faker-js/faker": "^10.2.0",
|
|
2216
|
+
"@testing-library/dom": "^10.4.1",
|
|
2217
|
+
"@testing-library/react": "^16.3.2",
|
|
2218
|
+
"@types/node": "^25.1.0",
|
|
2219
|
+
"@types/nodemailer": "^7.0.9",
|
|
2220
|
+
"@types/react": "^19.2.10",
|
|
2221
|
+
"@types/react-dom": "^19.2.3",
|
|
2222
|
+
"@types/ws": "^8.18.1",
|
|
2223
|
+
"cron-schedule": "^6.0.0",
|
|
2224
|
+
"jose": "^6.1.3",
|
|
2225
|
+
"jsdom": "^27.4.0",
|
|
2226
|
+
"openid-client": "^6.8.1",
|
|
2227
|
+
"prom-client": "^15.1.3",
|
|
2228
|
+
"react": "^19.2.4",
|
|
2229
|
+
"react-dom": "^19.2.4",
|
|
2230
|
+
"swagger-ui-dist": "^5.31.0",
|
|
2231
|
+
"tsdown": "^0.20.1",
|
|
2232
|
+
"vitest": "^4.0.18"
|
|
2233
|
+
};
|
|
2234
|
+
|
|
405
2235
|
//#endregion
|
|
406
2236
|
//#region ../../src/cli/version.ts
|
|
407
2237
|
const packageJson = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf-8"));
|
|
@@ -425,11 +2255,8 @@ var PackageManagerUtils = class {
|
|
|
425
2255
|
/**
|
|
426
2256
|
* Detect the package manager used in the project.
|
|
427
2257
|
*/
|
|
428
|
-
async getPackageManager(root,
|
|
429
|
-
if (
|
|
430
|
-
if (flags?.pnpm) return "pnpm";
|
|
431
|
-
if (flags?.npm) return "npm";
|
|
432
|
-
if (flags?.bun) return "bun";
|
|
2258
|
+
async getPackageManager(root, pm) {
|
|
2259
|
+
if (pm) return pm;
|
|
433
2260
|
if (this.alepha.isBun()) return "bun";
|
|
434
2261
|
if (await this.fs.exists(this.fs.join(root, "bun.lock"))) return "bun";
|
|
435
2262
|
if (await this.fs.exists(this.fs.join(root, "yarn.lock"))) return "yarn";
|
|
@@ -437,6 +2264,46 @@ var PackageManagerUtils = class {
|
|
|
437
2264
|
return "npm";
|
|
438
2265
|
}
|
|
439
2266
|
/**
|
|
2267
|
+
* Detect workspace context when inside a monorepo package.
|
|
2268
|
+
*
|
|
2269
|
+
* Checks if we're inside a workspace package (e.g., packages/my-pkg or apps/my-app)
|
|
2270
|
+
* by looking 2 levels up for workspace indicators like lockfiles and config files.
|
|
2271
|
+
*
|
|
2272
|
+
* @param root - The current package directory
|
|
2273
|
+
* @returns Workspace context with root path, PM, and config presence
|
|
2274
|
+
*/
|
|
2275
|
+
async getWorkspaceContext(root) {
|
|
2276
|
+
const workspaceRoot = this.fs.join(root, "..", "..");
|
|
2277
|
+
const [hasYarnLock, hasPnpmLock, hasNpmLock, hasBunLock] = await Promise.all([
|
|
2278
|
+
this.fs.exists(this.fs.join(workspaceRoot, "yarn.lock")),
|
|
2279
|
+
this.fs.exists(this.fs.join(workspaceRoot, "pnpm-lock.yaml")),
|
|
2280
|
+
this.fs.exists(this.fs.join(workspaceRoot, "package-lock.json")),
|
|
2281
|
+
this.fs.exists(this.fs.join(workspaceRoot, "bun.lock"))
|
|
2282
|
+
]);
|
|
2283
|
+
const [hasBiome, hasEditorConfig, hasTsConfig, hasWorkspacePackageJson] = await Promise.all([
|
|
2284
|
+
this.fs.exists(this.fs.join(workspaceRoot, "biome.json")),
|
|
2285
|
+
this.fs.exists(this.fs.join(workspaceRoot, ".editorconfig")),
|
|
2286
|
+
this.fs.exists(this.fs.join(workspaceRoot, "tsconfig.json")),
|
|
2287
|
+
this.fs.exists(this.fs.join(workspaceRoot, "package.json"))
|
|
2288
|
+
]);
|
|
2289
|
+
const isPackage = (hasYarnLock || hasPnpmLock || hasNpmLock || hasBunLock) && hasWorkspacePackageJson;
|
|
2290
|
+
let packageManager = null;
|
|
2291
|
+
if (hasYarnLock) packageManager = "yarn";
|
|
2292
|
+
else if (hasPnpmLock) packageManager = "pnpm";
|
|
2293
|
+
else if (hasBunLock) packageManager = "bun";
|
|
2294
|
+
else if (hasNpmLock) packageManager = "npm";
|
|
2295
|
+
return {
|
|
2296
|
+
isPackage,
|
|
2297
|
+
workspaceRoot: isPackage ? workspaceRoot : null,
|
|
2298
|
+
packageManager,
|
|
2299
|
+
config: {
|
|
2300
|
+
biomeJson: hasBiome,
|
|
2301
|
+
editorconfig: hasEditorConfig,
|
|
2302
|
+
tsconfigJson: hasTsConfig
|
|
2303
|
+
}
|
|
2304
|
+
};
|
|
2305
|
+
}
|
|
2306
|
+
/**
|
|
440
2307
|
* Get the install command for a package.
|
|
441
2308
|
*/
|
|
442
2309
|
async getInstallCommand(root, packageName, dev = true) {
|
|
@@ -481,13 +2348,23 @@ var PackageManagerUtils = class {
|
|
|
481
2348
|
}
|
|
482
2349
|
/**
|
|
483
2350
|
* Install a dependency if it's missing from the project.
|
|
2351
|
+
* Optionally checks workspace root for the dependency in monorepo setups.
|
|
484
2352
|
*/
|
|
485
2353
|
async ensureDependency(root, packageName, options = {}) {
|
|
486
|
-
const { dev = true } = options;
|
|
2354
|
+
const { dev = true, checkWorkspace = false } = options;
|
|
487
2355
|
if (await this.hasDependency(root, packageName)) {
|
|
488
2356
|
this.log.debug(`Dependency '${packageName}' is already installed`);
|
|
489
2357
|
return;
|
|
490
2358
|
}
|
|
2359
|
+
if (checkWorkspace) {
|
|
2360
|
+
const workspace = await this.getWorkspaceContext(root);
|
|
2361
|
+
if (workspace.workspaceRoot) {
|
|
2362
|
+
if (await this.hasDependency(workspace.workspaceRoot, packageName)) {
|
|
2363
|
+
this.log.debug(`Dependency '${packageName}' is already installed in workspace root`);
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
491
2368
|
const cmd = await this.getInstallCommand(root, packageName, dev);
|
|
492
2369
|
if (options.run) await options.run(cmd, {
|
|
493
2370
|
alias: `add ${packageName}`,
|
|
@@ -578,8 +2455,13 @@ var PackageManagerUtils = class {
|
|
|
578
2455
|
return packageJson;
|
|
579
2456
|
}
|
|
580
2457
|
generatePackageJsonContent(modes) {
|
|
2458
|
+
const alephaDeps = devDependencies;
|
|
581
2459
|
const dependencies = { alepha: `^${version}` };
|
|
582
|
-
const devDependencies = {};
|
|
2460
|
+
const devDependencies$1 = {};
|
|
2461
|
+
if (!modes.isPackage) {
|
|
2462
|
+
devDependencies$1["@biomejs/biome"] = alephaDeps["@biomejs/biome"];
|
|
2463
|
+
if (modes.test) devDependencies$1.vitest = alephaDeps.vitest;
|
|
2464
|
+
}
|
|
583
2465
|
const scripts = {
|
|
584
2466
|
dev: "alepha dev",
|
|
585
2467
|
build: "alepha build",
|
|
@@ -587,20 +2469,20 @@ var PackageManagerUtils = class {
|
|
|
587
2469
|
typecheck: "alepha typecheck",
|
|
588
2470
|
verify: "alepha verify"
|
|
589
2471
|
};
|
|
2472
|
+
if (modes.test) scripts.test = "vitest run";
|
|
590
2473
|
if (modes.ui) {
|
|
591
2474
|
dependencies["@alepha/ui"] = `^${version}`;
|
|
592
2475
|
modes.react = true;
|
|
593
2476
|
}
|
|
594
2477
|
if (modes.react) {
|
|
595
|
-
dependencies
|
|
596
|
-
dependencies
|
|
597
|
-
|
|
598
|
-
devDependencies["@types/react"] = "^19.2.0";
|
|
2478
|
+
dependencies.react = alephaDeps.react;
|
|
2479
|
+
dependencies["react-dom"] = alephaDeps["react-dom"];
|
|
2480
|
+
devDependencies$1["@types/react"] = alephaDeps["@types/react"];
|
|
599
2481
|
}
|
|
600
2482
|
return {
|
|
601
2483
|
type: "module",
|
|
602
2484
|
dependencies,
|
|
603
|
-
devDependencies,
|
|
2485
|
+
devDependencies: devDependencies$1,
|
|
604
2486
|
scripts
|
|
605
2487
|
};
|
|
606
2488
|
}
|
|
@@ -613,87 +2495,20 @@ var PackageManagerUtils = class {
|
|
|
613
2495
|
};
|
|
614
2496
|
|
|
615
2497
|
//#endregion
|
|
616
|
-
//#region ../../src/cli/
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
export class HelloController {
|
|
622
|
-
hello = $action({
|
|
623
|
-
path: "/hello",
|
|
624
|
-
schema: {
|
|
625
|
-
response: t.object({
|
|
626
|
-
message: t.string(),
|
|
627
|
-
}),
|
|
628
|
-
},
|
|
629
|
-
handler: () => ({
|
|
630
|
-
message: "Hello, Alepha!",
|
|
631
|
-
}),
|
|
632
|
-
});
|
|
633
|
-
}
|
|
634
|
-
`.trim();
|
|
635
|
-
|
|
636
|
-
//#endregion
|
|
637
|
-
//#region ../../src/cli/assets/apiIndexTs.ts
|
|
638
|
-
const apiIndexTs = (options = {}) => {
|
|
639
|
-
const { appName = "app" } = options;
|
|
640
|
-
return `
|
|
641
|
-
import { $module } from "alepha";
|
|
642
|
-
import { HelloController } from "./controllers/HelloController.ts";
|
|
643
|
-
|
|
644
|
-
export const ApiModule = $module({
|
|
645
|
-
name: "${appName}.api",
|
|
646
|
-
services: [HelloController],
|
|
647
|
-
});
|
|
648
|
-
`.trim();
|
|
649
|
-
};
|
|
2498
|
+
//#region ../../src/cli/templates/agentMd.ts
|
|
2499
|
+
const agentMd = (type, options = {}) => {
|
|
2500
|
+
const { react = false, projectName = "my-app" } = options;
|
|
2501
|
+
const header = type === "claude" ? `# CLAUDE.md
|
|
650
2502
|
|
|
651
|
-
|
|
652
|
-
//#region ../../src/cli/assets/biomeJson.ts
|
|
653
|
-
const biomeJson = () => `
|
|
654
|
-
{
|
|
655
|
-
"$schema": "https://biomejs.dev/schemas/latest/schema.json",
|
|
656
|
-
"vcs": {
|
|
657
|
-
"enabled": true,
|
|
658
|
-
"clientKind": "git"
|
|
659
|
-
},
|
|
660
|
-
"files": {
|
|
661
|
-
"ignoreUnknown": true,
|
|
662
|
-
"includes": ["**", "!node_modules", "!dist"]
|
|
663
|
-
},
|
|
664
|
-
"formatter": {
|
|
665
|
-
"enabled": true,
|
|
666
|
-
"useEditorconfig": true
|
|
667
|
-
},
|
|
668
|
-
"linter": {
|
|
669
|
-
"enabled": true,
|
|
670
|
-
"rules": {
|
|
671
|
-
"recommended": true
|
|
672
|
-
},
|
|
673
|
-
"domains": {
|
|
674
|
-
"react": "recommended"
|
|
675
|
-
}
|
|
676
|
-
},
|
|
677
|
-
"assist": {
|
|
678
|
-
"actions": {
|
|
679
|
-
"source": {
|
|
680
|
-
"organizeImports": "on"
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
`.trim();
|
|
2503
|
+
This file provides guidance to Claude Code when working with this Alepha project.` : `# AGENTS.md
|
|
686
2504
|
|
|
687
|
-
|
|
688
|
-
//#region ../../src/cli/assets/claudeMd.ts
|
|
689
|
-
const claudeMd = (options = {}) => {
|
|
690
|
-
const { react = false, projectName = "my-app" } = options;
|
|
2505
|
+
This file provides guidance to AI coding assistants when working with this Alepha project.`;
|
|
691
2506
|
const reactSection = react ? `
|
|
692
2507
|
## React & Frontend
|
|
693
2508
|
|
|
694
2509
|
### Pages with \`$page\`
|
|
695
2510
|
\`\`\`tsx
|
|
696
|
-
import { $page } from "
|
|
2511
|
+
import { $page } from "alepha/react/router";
|
|
697
2512
|
import { $client } from "alepha/server/links";
|
|
698
2513
|
import type { UserController } from "./UserController.ts";
|
|
699
2514
|
|
|
@@ -719,9 +2534,9 @@ class AppRouter {
|
|
|
719
2534
|
|
|
720
2535
|
### React Hooks
|
|
721
2536
|
\`\`\`typescript
|
|
722
|
-
import { useAlepha, useClient, useStore, useAction, useInject } from "
|
|
723
|
-
import { useRouter, useActive } from "
|
|
724
|
-
import { useForm } from "
|
|
2537
|
+
import { useAlepha, useClient, useStore, useAction, useInject } from "alepha/react";
|
|
2538
|
+
import { useRouter, useActive } from "alepha/react/router";
|
|
2539
|
+
import { useForm } from "alepha/react/form";
|
|
725
2540
|
\`\`\`
|
|
726
2541
|
|
|
727
2542
|
- \`useClient<Controller>()\` - Type-safe API calls
|
|
@@ -766,9 +2581,7 @@ ${projectName}/
|
|
|
766
2581
|
└── tsconfig.json
|
|
767
2582
|
\`\`\`
|
|
768
2583
|
`;
|
|
769
|
-
return
|
|
770
|
-
|
|
771
|
-
This file provides guidance to Claude Code when working with this Alepha project.
|
|
2584
|
+
return `${header}
|
|
772
2585
|
|
|
773
2586
|
## Overview
|
|
774
2587
|
|
|
@@ -782,10 +2595,10 @@ This is an **Alepha** project - a convention-driven TypeScript framework for typ
|
|
|
782
2595
|
|
|
783
2596
|
## Rules
|
|
784
2597
|
|
|
785
|
-
- Use TypeScript strict mode
|
|
2598
|
+
- Use TypeScript strict mode, always check types (\`alepha typecheck\`)
|
|
786
2599
|
- Use Biome for formatting (\`alepha lint\`)
|
|
787
|
-
- Use Vitest for testing
|
|
788
|
-
- One file = one class
|
|
2600
|
+
- Use Vitest for testing (\`alepha test\`)
|
|
2601
|
+
- One file = one class, multiple interfaces/types allowed
|
|
789
2602
|
- Primitives are class properties (except \`$entity\`, \`$atom\`)
|
|
790
2603
|
- No decorators, no Express/Fastify patterns
|
|
791
2604
|
- No manual instantiation - use dependency injection
|
|
@@ -793,6 +2606,7 @@ This is an **Alepha** project - a convention-driven TypeScript framework for typ
|
|
|
793
2606
|
- Import with file extensions: \`import { User } from "./User.ts"\`
|
|
794
2607
|
- Use \`t\` from Alepha for schemas (not Zod)
|
|
795
2608
|
- Prefer \`t.text()\` over \`t.string()\` for user input (has default max length, auto-trim, supports lowercase option)
|
|
2609
|
+
- One file = one schema (schemas/createUserSchema.ts)
|
|
796
2610
|
|
|
797
2611
|
## Project Structure
|
|
798
2612
|
${projectStructure}
|
|
@@ -835,17 +2649,12 @@ export const userEntity = $entity({
|
|
|
835
2649
|
id: db.primaryKey(),
|
|
836
2650
|
email: t.email(),
|
|
837
2651
|
createdAt: db.createdAt(),
|
|
838
|
-
updatedAt: db.updatedAt(),
|
|
839
2652
|
}),
|
|
840
2653
|
indexes: [{ column: "email", unique: true }],
|
|
841
2654
|
});
|
|
842
2655
|
|
|
843
2656
|
class UserService {
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
async findById(id: string) {
|
|
847
|
-
return this.repo.findById(id);
|
|
848
|
-
}
|
|
2657
|
+
userRepository = $repository(userEntity);
|
|
849
2658
|
}
|
|
850
2659
|
\`\`\`
|
|
851
2660
|
|
|
@@ -855,11 +2664,6 @@ import { $inject } from "alepha";
|
|
|
855
2664
|
|
|
856
2665
|
class OrderService {
|
|
857
2666
|
userService = $inject(UserService); // Within same module
|
|
858
|
-
|
|
859
|
-
async createOrder(userId: string) {
|
|
860
|
-
const user = await this.userService.findById(userId);
|
|
861
|
-
// ...
|
|
862
|
-
}
|
|
863
2667
|
}
|
|
864
2668
|
|
|
865
2669
|
// Cross-module: use $client instead of $inject
|
|
@@ -923,7 +2727,7 @@ ${reactSection}
|
|
|
923
2727
|
| \`$bucket\` | \`alepha/bucket\` | File storage |
|
|
924
2728
|
| \`$issuer\` | \`alepha/security\` | JWT tokens |
|
|
925
2729
|
| \`$command\` | \`alepha/command\` | CLI commands |${react ? `
|
|
926
|
-
| \`$page\` |
|
|
2730
|
+
| \`$page\` | \`alepha/react/router\` | React pages with SSR |
|
|
927
2731
|
| \`$atom\` | \`alepha\` | Global state |` : ""}
|
|
928
2732
|
|
|
929
2733
|
## Testing
|
|
@@ -975,11 +2779,88 @@ alepha build # Build the project
|
|
|
975
2779
|
|
|
976
2780
|
- Full docs: https://alepha.dev/llms.txt
|
|
977
2781
|
- Detailed docs: https://alepha.dev/llms-full.txt
|
|
2782
|
+
|
|
2783
|
+
## Source Code Access
|
|
2784
|
+
|
|
2785
|
+
Full framework source available at \`node_modules/alepha/src/\`.
|
|
2786
|
+
Read primitives directly when you need implementation details.
|
|
2787
|
+
`.trim();
|
|
2788
|
+
};
|
|
2789
|
+
|
|
2790
|
+
//#endregion
|
|
2791
|
+
//#region ../../src/cli/templates/apiHelloControllerTs.ts
|
|
2792
|
+
const apiHelloControllerTs = () => `
|
|
2793
|
+
import { t } from "alepha";
|
|
2794
|
+
import { $action } from "alepha/server";
|
|
2795
|
+
|
|
2796
|
+
export class HelloController {
|
|
2797
|
+
hello = $action({
|
|
2798
|
+
path: "/hello",
|
|
2799
|
+
schema: {
|
|
2800
|
+
response: t.object({
|
|
2801
|
+
message: t.string(),
|
|
2802
|
+
}),
|
|
2803
|
+
},
|
|
2804
|
+
handler: () => ({
|
|
2805
|
+
message: "Hello, Alepha!",
|
|
2806
|
+
}),
|
|
2807
|
+
});
|
|
2808
|
+
}
|
|
2809
|
+
`.trim();
|
|
2810
|
+
|
|
2811
|
+
//#endregion
|
|
2812
|
+
//#region ../../src/cli/templates/apiIndexTs.ts
|
|
2813
|
+
const apiIndexTs = (options = {}) => {
|
|
2814
|
+
const { appName = "app" } = options;
|
|
2815
|
+
return `
|
|
2816
|
+
import { $module } from "alepha";
|
|
2817
|
+
import { HelloController } from "./controllers/HelloController.ts";
|
|
2818
|
+
|
|
2819
|
+
export const ApiModule = $module({
|
|
2820
|
+
name: "${appName}.api",
|
|
2821
|
+
services: [HelloController],
|
|
2822
|
+
});
|
|
978
2823
|
`.trim();
|
|
979
2824
|
};
|
|
980
2825
|
|
|
981
2826
|
//#endregion
|
|
982
|
-
//#region ../../src/cli/
|
|
2827
|
+
//#region ../../src/cli/templates/biomeJson.ts
|
|
2828
|
+
const biomeJson = () => `
|
|
2829
|
+
{
|
|
2830
|
+
"$schema": "https://biomejs.dev/schemas/latest/schema.json",
|
|
2831
|
+
"vcs": {
|
|
2832
|
+
"enabled": true,
|
|
2833
|
+
"clientKind": "git"
|
|
2834
|
+
},
|
|
2835
|
+
"files": {
|
|
2836
|
+
"ignoreUnknown": true,
|
|
2837
|
+
"includes": ["**", "!node_modules", "!dist"]
|
|
2838
|
+
},
|
|
2839
|
+
"formatter": {
|
|
2840
|
+
"enabled": true,
|
|
2841
|
+
"useEditorconfig": true
|
|
2842
|
+
},
|
|
2843
|
+
"linter": {
|
|
2844
|
+
"enabled": true,
|
|
2845
|
+
"rules": {
|
|
2846
|
+
"recommended": true
|
|
2847
|
+
},
|
|
2848
|
+
"domains": {
|
|
2849
|
+
"react": "recommended"
|
|
2850
|
+
}
|
|
2851
|
+
},
|
|
2852
|
+
"assist": {
|
|
2853
|
+
"actions": {
|
|
2854
|
+
"source": {
|
|
2855
|
+
"organizeImports": "on"
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
`.trim();
|
|
2861
|
+
|
|
2862
|
+
//#endregion
|
|
2863
|
+
//#region ../../src/cli/templates/dummySpecTs.ts
|
|
983
2864
|
const dummySpecTs = () => `
|
|
984
2865
|
import { test, expect } from "vitest";
|
|
985
2866
|
|
|
@@ -989,7 +2870,7 @@ test("dummy test", () => {
|
|
|
989
2870
|
`.trim();
|
|
990
2871
|
|
|
991
2872
|
//#endregion
|
|
992
|
-
//#region ../../src/cli/
|
|
2873
|
+
//#region ../../src/cli/templates/editorconfig.ts
|
|
993
2874
|
const editorconfig = () => `
|
|
994
2875
|
# https://editorconfig.org
|
|
995
2876
|
|
|
@@ -1005,7 +2886,48 @@ indent_size = 2
|
|
|
1005
2886
|
`.trim();
|
|
1006
2887
|
|
|
1007
2888
|
//#endregion
|
|
1008
|
-
//#region ../../src/cli/
|
|
2889
|
+
//#region ../../src/cli/templates/gitignore.ts
|
|
2890
|
+
const gitignore = () => `
|
|
2891
|
+
# Dependencies
|
|
2892
|
+
node_modules/
|
|
2893
|
+
|
|
2894
|
+
# Build outputs
|
|
2895
|
+
dist/
|
|
2896
|
+
.vite/
|
|
2897
|
+
|
|
2898
|
+
# Environment files
|
|
2899
|
+
.env
|
|
2900
|
+
.env.*
|
|
2901
|
+
!.env.example
|
|
2902
|
+
|
|
2903
|
+
# IDE
|
|
2904
|
+
.idea/
|
|
2905
|
+
*.swp
|
|
2906
|
+
*.swo
|
|
2907
|
+
|
|
2908
|
+
# OS
|
|
2909
|
+
.DS_Store
|
|
2910
|
+
Thumbs.db
|
|
2911
|
+
|
|
2912
|
+
# Logs
|
|
2913
|
+
*.log
|
|
2914
|
+
logs/
|
|
2915
|
+
|
|
2916
|
+
# Test coverage
|
|
2917
|
+
coverage/
|
|
2918
|
+
|
|
2919
|
+
# Yarn
|
|
2920
|
+
.yarn/*
|
|
2921
|
+
!.yarn/patches
|
|
2922
|
+
!.yarn/plugins
|
|
2923
|
+
!.yarn/releases
|
|
2924
|
+
!.yarn/sdks
|
|
2925
|
+
!.yarn/versions
|
|
2926
|
+
.pnp.*
|
|
2927
|
+
`.trim();
|
|
2928
|
+
|
|
2929
|
+
//#endregion
|
|
2930
|
+
//#region ../../src/cli/templates/mainBrowserTs.ts
|
|
1009
2931
|
const mainBrowserTs = () => `
|
|
1010
2932
|
import { Alepha, run } from "alepha";
|
|
1011
2933
|
import { WebModule } from "./web/index.ts";
|
|
@@ -1018,8 +2940,10 @@ run(alepha);
|
|
|
1018
2940
|
`.trim();
|
|
1019
2941
|
|
|
1020
2942
|
//#endregion
|
|
1021
|
-
//#region ../../src/cli/
|
|
1022
|
-
const mainCss = () =>
|
|
2943
|
+
//#region ../../src/cli/templates/mainCss.ts
|
|
2944
|
+
const mainCss = (options = {}) => {
|
|
2945
|
+
if (options.ui) return `@import "@alepha/ui/styles";`;
|
|
2946
|
+
return `
|
|
1023
2947
|
* {
|
|
1024
2948
|
box-sizing: border-box;
|
|
1025
2949
|
margin: 0;
|
|
@@ -1042,25 +2966,34 @@ body {
|
|
|
1042
2966
|
height: 100%;
|
|
1043
2967
|
}
|
|
1044
2968
|
`.trim();
|
|
2969
|
+
};
|
|
1045
2970
|
|
|
1046
2971
|
//#endregion
|
|
1047
|
-
//#region ../../src/cli/
|
|
2972
|
+
//#region ../../src/cli/templates/mainServerTs.ts
|
|
1048
2973
|
const mainServerTs = (options = {}) => {
|
|
1049
|
-
const { react = false } = options;
|
|
2974
|
+
const { api = false, react = false } = options;
|
|
2975
|
+
const imports = [];
|
|
2976
|
+
const withs = [];
|
|
2977
|
+
if (api) {
|
|
2978
|
+
imports.push(`import { ApiModule } from "./api/index.ts";`);
|
|
2979
|
+
withs.push(`alepha.with(ApiModule);`);
|
|
2980
|
+
}
|
|
2981
|
+
if (react) {
|
|
2982
|
+
imports.push(`import { WebModule } from "./web/index.ts";`);
|
|
2983
|
+
withs.push(`alepha.with(WebModule);`);
|
|
2984
|
+
}
|
|
1050
2985
|
return `
|
|
1051
2986
|
import { Alepha, run } from "alepha";
|
|
1052
|
-
|
|
1053
|
-
${react ? `import { WebModule } from "./web/index.ts";\n` : ""}
|
|
2987
|
+
${imports.length > 0 ? `${imports.join("\n")}\n` : ""}
|
|
1054
2988
|
const alepha = Alepha.create();
|
|
2989
|
+
${withs.length > 0 ? `\n${withs.join("\n")}` : ""}
|
|
1055
2990
|
|
|
1056
|
-
alepha.with(ApiModule);
|
|
1057
|
-
${react ? `alepha.with(WebModule);\n` : ""}
|
|
1058
2991
|
run(alepha);
|
|
1059
2992
|
`.trim();
|
|
1060
2993
|
};
|
|
1061
2994
|
|
|
1062
2995
|
//#endregion
|
|
1063
|
-
//#region ../../src/cli/
|
|
2996
|
+
//#region ../../src/cli/templates/tsconfigJson.ts
|
|
1064
2997
|
const tsconfigJson = () => `
|
|
1065
2998
|
{
|
|
1066
2999
|
"extends": "alepha/tsconfig.base"
|
|
@@ -1068,33 +3001,50 @@ const tsconfigJson = () => `
|
|
|
1068
3001
|
`.trim();
|
|
1069
3002
|
|
|
1070
3003
|
//#endregion
|
|
1071
|
-
//#region ../../src/cli/
|
|
1072
|
-
const webAppRouterTs = () =>
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
import
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
3004
|
+
//#region ../../src/cli/templates/webAppRouterTs.ts
|
|
3005
|
+
const webAppRouterTs = (options) => {
|
|
3006
|
+
const imports = [];
|
|
3007
|
+
const classMembers = [];
|
|
3008
|
+
if (options.ui) imports.push("import { $ui } from \"@alepha/ui\";");
|
|
3009
|
+
imports.push("import { $page } from \"alepha/react/router\";");
|
|
3010
|
+
if (options.api) {
|
|
3011
|
+
imports.push("import { $client } from \"alepha/server/links\";");
|
|
3012
|
+
imports.push("import type { HelloController } from \"../api/controllers/HelloController.ts\";");
|
|
3013
|
+
classMembers.push(" api = $client<HelloController>();");
|
|
3014
|
+
}
|
|
3015
|
+
if (options.ui) {
|
|
3016
|
+
classMembers.push(" ui = $ui();");
|
|
3017
|
+
classMembers.push(` layout = $page({
|
|
3018
|
+
parent: this.ui.root,
|
|
3019
|
+
children: () => [this.home],
|
|
3020
|
+
});`);
|
|
3021
|
+
}
|
|
3022
|
+
if (options.api) classMembers.push(` home = $page({
|
|
1081
3023
|
path: "/",
|
|
1082
3024
|
lazy: () => import("./components/Hello.tsx"),
|
|
1083
3025
|
loader: () => this.api.hello(),
|
|
1084
|
-
});
|
|
1085
|
-
|
|
1086
|
-
|
|
3026
|
+
});`);
|
|
3027
|
+
else classMembers.push(` home = $page({
|
|
3028
|
+
path: "/",
|
|
3029
|
+
lazy: () => import("./components/Hello.tsx"),
|
|
3030
|
+
});`);
|
|
3031
|
+
return `${imports.join("\n")}
|
|
3032
|
+
|
|
3033
|
+
export class AppRouter {
|
|
3034
|
+
${classMembers.join("\n\n")}
|
|
3035
|
+
}`;
|
|
3036
|
+
};
|
|
1087
3037
|
|
|
1088
3038
|
//#endregion
|
|
1089
|
-
//#region ../../src/cli/
|
|
3039
|
+
//#region ../../src/cli/templates/webHelloComponentTsx.ts
|
|
1090
3040
|
const webHelloComponentTsx = () => `import { useState } from "react";
|
|
1091
3041
|
|
|
1092
3042
|
interface Props {
|
|
1093
|
-
message
|
|
3043
|
+
message?: string;
|
|
1094
3044
|
}
|
|
1095
3045
|
|
|
1096
3046
|
const Hello = (props: Props) => {
|
|
1097
|
-
const [message, setMessage] = useState(props.message);
|
|
3047
|
+
const [message, setMessage] = useState(props.message ?? "");
|
|
1098
3048
|
return (
|
|
1099
3049
|
<div>
|
|
1100
3050
|
<h1>{message}</h1>
|
|
@@ -1108,7 +3058,7 @@ export default Hello;
|
|
|
1108
3058
|
`.trim();
|
|
1109
3059
|
|
|
1110
3060
|
//#endregion
|
|
1111
|
-
//#region ../../src/cli/
|
|
3061
|
+
//#region ../../src/cli/templates/webIndexTs.ts
|
|
1112
3062
|
const webIndexTs = (options = {}) => {
|
|
1113
3063
|
const { appName = "app" } = options;
|
|
1114
3064
|
return `
|
|
@@ -1137,6 +3087,7 @@ var ProjectScaffolder = class {
|
|
|
1137
3087
|
log = $logger();
|
|
1138
3088
|
fs = $inject(FileSystemProvider);
|
|
1139
3089
|
pm = $inject(PackageManagerUtils);
|
|
3090
|
+
utils = $inject(AlephaCliUtils);
|
|
1140
3091
|
/**
|
|
1141
3092
|
* Get the app name from the directory name.
|
|
1142
3093
|
*
|
|
@@ -1154,13 +3105,19 @@ var ProjectScaffolder = class {
|
|
|
1154
3105
|
async ensureConfig(root, opts) {
|
|
1155
3106
|
const tasks = [];
|
|
1156
3107
|
const force = opts.force ?? false;
|
|
3108
|
+
const checkWorkspace = opts.checkWorkspace ?? false;
|
|
1157
3109
|
if (opts.packageJson) tasks.push(this.pm.ensurePackageJson(root, typeof opts.packageJson === "boolean" ? {} : opts.packageJson).then(() => {}));
|
|
1158
3110
|
if (opts.tsconfigJson) tasks.push(this.ensureTsConfig(root, { force }));
|
|
1159
|
-
if (opts.
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
3111
|
+
if (opts.biomeJson) tasks.push(this.ensureBiomeConfig(root, {
|
|
3112
|
+
force,
|
|
3113
|
+
checkWorkspace
|
|
3114
|
+
}));
|
|
3115
|
+
if (opts.editorconfig) tasks.push(this.ensureEditorConfig(root, {
|
|
3116
|
+
force,
|
|
3117
|
+
checkWorkspace
|
|
3118
|
+
}));
|
|
3119
|
+
if (opts.agentMd) tasks.push(this.ensureAgentMd(root, {
|
|
3120
|
+
...opts.agentMd,
|
|
1164
3121
|
force
|
|
1165
3122
|
}));
|
|
1166
3123
|
await Promise.all(tasks);
|
|
@@ -1170,51 +3127,74 @@ var ProjectScaffolder = class {
|
|
|
1170
3127
|
await this.fs.writeFile(this.fs.join(root, "tsconfig.json"), tsconfigJson());
|
|
1171
3128
|
}
|
|
1172
3129
|
async ensureBiomeConfig(root, opts = {}) {
|
|
3130
|
+
if (!opts.force && opts.checkWorkspace && await this.existsInParents(root, "biome.json")) return;
|
|
1173
3131
|
await this.ensureFile(root, "biome.json", biomeJson(), opts.force);
|
|
1174
3132
|
}
|
|
1175
3133
|
async ensureEditorConfig(root, opts = {}) {
|
|
3134
|
+
if (!opts.force && opts.checkWorkspace && await this.existsInParents(root, ".editorconfig")) return;
|
|
1176
3135
|
await this.ensureFile(root, ".editorconfig", editorconfig(), opts.force);
|
|
1177
3136
|
}
|
|
1178
|
-
|
|
1179
|
-
|
|
3137
|
+
/**
|
|
3138
|
+
* Ensure git repository is initialized with .gitignore.
|
|
3139
|
+
*
|
|
3140
|
+
* @returns true if git was initialized, false if already exists or git unavailable
|
|
3141
|
+
*/
|
|
3142
|
+
async ensureGitRepo(root, opts = {}) {
|
|
3143
|
+
const gitDir = this.fs.join(root, ".git");
|
|
3144
|
+
if (!opts.force && await this.fs.exists(gitDir)) return false;
|
|
3145
|
+
if (!await this.utils.isInstalledAsync("git")) return false;
|
|
3146
|
+
await this.utils.exec("git init", {
|
|
3147
|
+
root,
|
|
3148
|
+
global: true
|
|
3149
|
+
});
|
|
3150
|
+
await this.ensureFile(root, ".gitignore", gitignore(), opts.force);
|
|
3151
|
+
return true;
|
|
3152
|
+
}
|
|
3153
|
+
async ensureAgentMd(root, options) {
|
|
3154
|
+
const filename = options.type === "claude" ? "CLAUDE.md" : "AGENTS.md";
|
|
3155
|
+
await this.ensureFile(root, filename, agentMd(options.type, options), options.force);
|
|
3156
|
+
}
|
|
3157
|
+
/**
|
|
3158
|
+
* Ensure src/main.server.ts exists with correct module imports.
|
|
3159
|
+
*/
|
|
3160
|
+
async ensureMainServerTs(root, opts = {}) {
|
|
3161
|
+
const srcDir = this.fs.join(root, "src");
|
|
3162
|
+
await this.fs.mkdir(srcDir, { recursive: true });
|
|
3163
|
+
await this.ensureFile(srcDir, "main.server.ts", mainServerTs({
|
|
3164
|
+
api: opts.api,
|
|
3165
|
+
react: opts.react
|
|
3166
|
+
}), opts.force);
|
|
1180
3167
|
}
|
|
1181
3168
|
/**
|
|
1182
|
-
* Ensure
|
|
3169
|
+
* Ensure API module structure exists.
|
|
1183
3170
|
*
|
|
1184
3171
|
* Creates:
|
|
1185
|
-
* - src/main.server.ts (entry point)
|
|
1186
3172
|
* - src/api/index.ts (API module)
|
|
1187
3173
|
* - src/api/controllers/HelloController.ts (example controller)
|
|
1188
3174
|
*/
|
|
1189
3175
|
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
3176
|
const appName = this.getAppName(root);
|
|
1195
3177
|
await this.fs.mkdir(this.fs.join(root, "src/api/controllers"), { recursive: true });
|
|
1196
|
-
await this.ensureFile(
|
|
1197
|
-
await this.ensureFile(
|
|
1198
|
-
await this.ensureFile(srcDir, "api/controllers/HelloController.ts", apiHelloControllerTs(), opts.force);
|
|
3178
|
+
await this.ensureFile(root, "src/api/index.ts", apiIndexTs({ appName }), opts.force);
|
|
3179
|
+
await this.ensureFile(root, "src/api/controllers/HelloController.ts", apiHelloControllerTs(), opts.force);
|
|
1199
3180
|
}
|
|
1200
3181
|
/**
|
|
1201
|
-
* Ensure
|
|
3182
|
+
* Ensure web/React project structure exists.
|
|
1202
3183
|
*
|
|
1203
3184
|
* Creates:
|
|
1204
|
-
* - src/main.
|
|
1205
|
-
* - src/
|
|
3185
|
+
* - src/main.browser.ts
|
|
3186
|
+
* - src/main.css
|
|
1206
3187
|
* - src/web/index.ts, src/web/AppRouter.ts, src/web/components/Hello.tsx
|
|
1207
3188
|
*/
|
|
1208
|
-
async
|
|
3189
|
+
async ensureWebProject(root, opts = {}) {
|
|
1209
3190
|
const appName = this.getAppName(root);
|
|
1210
|
-
await this.fs.mkdir(this.fs.join(root, "src/api/controllers"), { recursive: true });
|
|
1211
3191
|
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);
|
|
3192
|
+
await this.ensureFile(root, "src/main.css", mainCss({ ui: opts.ui }), opts.force);
|
|
1216
3193
|
await this.ensureFile(root, "src/web/index.ts", webIndexTs({ appName }), opts.force);
|
|
1217
|
-
await this.ensureFile(root, "src/web/AppRouter.ts", webAppRouterTs(
|
|
3194
|
+
await this.ensureFile(root, "src/web/AppRouter.ts", webAppRouterTs({
|
|
3195
|
+
api: opts.api,
|
|
3196
|
+
ui: opts.ui
|
|
3197
|
+
}), opts.force);
|
|
1218
3198
|
await this.ensureFile(root, "src/web/components/Hello.tsx", webHelloComponentTsx(), opts.force);
|
|
1219
3199
|
await this.ensureFile(root, "src/main.browser.ts", mainBrowserTs(), opts.force);
|
|
1220
3200
|
}
|
|
@@ -1283,10 +3263,6 @@ var BuildCommand = class {
|
|
|
1283
3263
|
this.log.trace("Entry file found", { entry });
|
|
1284
3264
|
const distDir = "dist";
|
|
1285
3265
|
const publicDir = "public";
|
|
1286
|
-
await this.pm.ensureDependency(root, "vite", {
|
|
1287
|
-
run,
|
|
1288
|
-
exec: (cmd, opts) => this.utils.exec(cmd, opts)
|
|
1289
|
-
});
|
|
1290
3266
|
await run.rm("dist", { alias: "clean dist" });
|
|
1291
3267
|
const options = this.options;
|
|
1292
3268
|
await this.utils.loadEnv(root, [".env", ".env.production"]);
|
|
@@ -1414,6 +3390,7 @@ var DbCommand = class {
|
|
|
1414
3390
|
log = $logger();
|
|
1415
3391
|
fs = $inject(FileSystemProvider);
|
|
1416
3392
|
utils = $inject(AlephaCliUtils);
|
|
3393
|
+
entryProvider = $inject(AppEntryProvider);
|
|
1417
3394
|
/**
|
|
1418
3395
|
* Check if database migrations are up to date.
|
|
1419
3396
|
*/
|
|
@@ -1428,7 +3405,11 @@ var DbCommand = class {
|
|
|
1428
3405
|
handler: async ({ args, root }) => {
|
|
1429
3406
|
const rootDir = root;
|
|
1430
3407
|
this.log.debug(`Using project root: ${rootDir}`);
|
|
1431
|
-
const
|
|
3408
|
+
const entry = await this.entryProvider.getAppEntry(root);
|
|
3409
|
+
const alepha = await this.utils.loadAlephaFromServerEntryFile({
|
|
3410
|
+
mode: "development",
|
|
3411
|
+
entry
|
|
3412
|
+
});
|
|
1432
3413
|
const repositoryProvider = alepha.inject("RepositoryProvider");
|
|
1433
3414
|
const drizzleKitProvider = alepha.inject("DrizzleKitProvider");
|
|
1434
3415
|
const accepted = /* @__PURE__ */ new Set([]);
|
|
@@ -1592,7 +3573,11 @@ var DbCommand = class {
|
|
|
1592
3573
|
if (options.env) envFiles.push(`.env.${options.env}`);
|
|
1593
3574
|
await this.utils.loadEnv(rootDir, envFiles);
|
|
1594
3575
|
this.log.debug(`Using project root: ${rootDir}`);
|
|
1595
|
-
const
|
|
3576
|
+
const entry = await this.entryProvider.getAppEntry(rootDir);
|
|
3577
|
+
const alepha = await this.utils.loadAlephaFromServerEntryFile({
|
|
3578
|
+
mode: "development",
|
|
3579
|
+
entry
|
|
3580
|
+
});
|
|
1596
3581
|
const drizzleKitProvider = alepha.inject("DrizzleKitProvider");
|
|
1597
3582
|
const repositoryProvider = alepha.inject("RepositoryProvider");
|
|
1598
3583
|
const accepted = /* @__PURE__ */ new Set([]);
|
|
@@ -1616,11 +3601,11 @@ var DbCommand = class {
|
|
|
1616
3601
|
providerUrl: provider.url,
|
|
1617
3602
|
providerDriver: provider.driver,
|
|
1618
3603
|
dialect,
|
|
1619
|
-
entry,
|
|
3604
|
+
entry: this.fs.join(rootDir, entry.server),
|
|
1620
3605
|
rootDir
|
|
1621
3606
|
});
|
|
1622
3607
|
const flags = options.commandFlags ? ` ${options.commandFlags}` : "";
|
|
1623
|
-
await this.utils.exec(`drizzle-kit ${options.command} --config=${drizzleConfigJsPath}${flags}`, { env: { NODE_OPTIONS: "--import tsx" } });
|
|
3608
|
+
await this.utils.exec(`drizzle-kit ${options.command} --config=${drizzleConfigJsPath}${flags}`, { env: { NODE_OPTIONS: [process.env.NODE_OPTIONS, "--import tsx"].filter(Boolean).join(" ") } });
|
|
1624
3609
|
}
|
|
1625
3610
|
}
|
|
1626
3611
|
/**
|
|
@@ -1778,7 +3763,7 @@ var DeployCommand = class {
|
|
|
1778
3763
|
var ViteDevServerProvider = class {
|
|
1779
3764
|
log = $logger();
|
|
1780
3765
|
fs = $inject(FileSystemProvider);
|
|
1781
|
-
templateProvider = $inject(
|
|
3766
|
+
templateProvider = $inject(ViteUtils);
|
|
1782
3767
|
server;
|
|
1783
3768
|
options;
|
|
1784
3769
|
alepha = null;
|
|
@@ -1883,11 +3868,11 @@ var ViteDevServerProvider = class {
|
|
|
1883
3868
|
async setupEnvironment() {
|
|
1884
3869
|
const { loadEnv } = await importVite();
|
|
1885
3870
|
const env = loadEnv(process.env.NODE_ENV || "development", this.options.root, "");
|
|
3871
|
+
for (const [key, value] of Object.entries(env)) process.env[key] ??= value;
|
|
1886
3872
|
process.env.NODE_ENV ??= "development";
|
|
1887
3873
|
process.env.VITE_ALEPHA_DEV = "true";
|
|
1888
3874
|
process.env.SERVER_HOST ??= this.options.host?.toString() ?? "localhost";
|
|
1889
3875
|
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
3876
|
}
|
|
1892
3877
|
/**
|
|
1893
3878
|
* Load or reload the Alepha instance.
|
|
@@ -1952,6 +3937,7 @@ var ViteDevServerProvider = class {
|
|
|
1952
3937
|
* Run Vite middleware and detect if it handled the request.
|
|
1953
3938
|
*/
|
|
1954
3939
|
async runViteMiddleware(req, res, ctx) {
|
|
3940
|
+
if (res.headersSent || res.writableEnded) return false;
|
|
1955
3941
|
return new Promise((resolve) => {
|
|
1956
3942
|
let resolved = false;
|
|
1957
3943
|
const done = (handled) => {
|
|
@@ -1960,6 +3946,18 @@ var ViteDevServerProvider = class {
|
|
|
1960
3946
|
if (handled) ctx.metadata.vite = true;
|
|
1961
3947
|
resolve(handled);
|
|
1962
3948
|
};
|
|
3949
|
+
const originalSetHeader = res.setHeader.bind(res);
|
|
3950
|
+
const originalWriteHead = res.writeHead?.bind(res);
|
|
3951
|
+
const originalWrite = res.write.bind(res);
|
|
3952
|
+
const originalEnd = res.end.bind(res);
|
|
3953
|
+
const guardedCall = (fn, ...args) => {
|
|
3954
|
+
if (resolved && !ctx.metadata.vite) return;
|
|
3955
|
+
return fn(...args);
|
|
3956
|
+
};
|
|
3957
|
+
res.setHeader = (...args) => guardedCall(originalSetHeader, ...args);
|
|
3958
|
+
if (originalWriteHead) res.writeHead = (...args) => guardedCall(originalWriteHead, ...args);
|
|
3959
|
+
res.write = (...args) => guardedCall(originalWrite, ...args);
|
|
3960
|
+
res.end = (...args) => guardedCall(originalEnd, ...args);
|
|
1963
3961
|
res.on("finish", () => done(true));
|
|
1964
3962
|
res.on("close", () => res.headersSent && done(true));
|
|
1965
3963
|
this.server.middlewares(req, res, () => done(false));
|
|
@@ -1999,23 +3997,14 @@ var DevCommand = class {
|
|
|
1999
3997
|
boot = $inject(AppEntryProvider);
|
|
2000
3998
|
/**
|
|
2001
3999
|
* 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
4000
|
*/
|
|
2006
4001
|
dev = $command({
|
|
2007
4002
|
name: "dev",
|
|
2008
4003
|
description: "Run the project in development mode",
|
|
2009
4004
|
handler: async ({ root }) => {
|
|
2010
|
-
const [expo, react] = await Promise.all([this.pm.hasExpo(root), this.pm.hasReact(root)]);
|
|
2011
4005
|
await this.scaffolder.ensureConfig(root, { tsconfigJson: true });
|
|
2012
|
-
if (expo) {
|
|
2013
|
-
await this.utils.exec("expo start");
|
|
2014
|
-
return;
|
|
2015
|
-
}
|
|
2016
4006
|
const entry = await this.boot.getAppEntry(root);
|
|
2017
4007
|
this.log.debug("Entry file found", { entry });
|
|
2018
|
-
await this.pm.ensureDependency(root, "vite", { exec: (cmd, opts) => this.utils.exec(cmd, opts) });
|
|
2019
4008
|
await this.viteDevServer.init({
|
|
2020
4009
|
root,
|
|
2021
4010
|
entry
|
|
@@ -2255,7 +4244,10 @@ var GenEnvCommand = class {
|
|
|
2255
4244
|
description: "Output file path (e.g., .env)"
|
|
2256
4245
|
})) }),
|
|
2257
4246
|
handler: async ({ root, flags }) => {
|
|
2258
|
-
const
|
|
4247
|
+
const alepha = await this.utils.loadAlephaFromServerEntryFile({
|
|
4248
|
+
root,
|
|
4249
|
+
mode: "development"
|
|
4250
|
+
});
|
|
2259
4251
|
try {
|
|
2260
4252
|
const { env } = alepha.dump();
|
|
2261
4253
|
let dotEnvFile = "";
|
|
@@ -2288,7 +4280,10 @@ var OpenApiCommand = class {
|
|
|
2288
4280
|
description: "Output file path"
|
|
2289
4281
|
})) }),
|
|
2290
4282
|
handler: async ({ root, flags }) => {
|
|
2291
|
-
const
|
|
4283
|
+
const alepha = await this.utils.loadAlephaFromServerEntryFile({
|
|
4284
|
+
root,
|
|
4285
|
+
mode: "development"
|
|
4286
|
+
});
|
|
2292
4287
|
try {
|
|
2293
4288
|
const openapiProvider = alepha.inject(ServerSwaggerProvider);
|
|
2294
4289
|
await alepha.events.emit("configure", alepha);
|
|
@@ -2357,17 +4352,20 @@ var InitCommand = class {
|
|
|
2357
4352
|
flags: t.object({
|
|
2358
4353
|
agent: t.optional(t.boolean({
|
|
2359
4354
|
aliases: ["a"],
|
|
2360
|
-
description: "Add CLAUDE.md
|
|
4355
|
+
description: "Add AI agent instructions (CLAUDE.md if claude CLI installed, else AGENTS.md)"
|
|
2361
4356
|
})),
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
4357
|
+
pm: t.optional(t.enum([
|
|
4358
|
+
"yarn",
|
|
4359
|
+
"npm",
|
|
4360
|
+
"pnpm",
|
|
4361
|
+
"bun"
|
|
4362
|
+
], { description: "Package manager to use" })),
|
|
4363
|
+
api: t.optional(t.boolean({ description: "Include API module structure (src/api/)" })),
|
|
2366
4364
|
react: t.optional(t.boolean({
|
|
2367
4365
|
aliases: ["r"],
|
|
2368
|
-
description: "Include
|
|
4366
|
+
description: "Include React dependencies and web module (src/web/)"
|
|
2369
4367
|
})),
|
|
2370
|
-
ui: t.optional(t.boolean({ description: "Include
|
|
4368
|
+
ui: t.optional(t.boolean({ description: "Include @alepha/ui (components, auth portal, admin portal)" })),
|
|
2371
4369
|
test: t.optional(t.boolean({ description: "Include Vitest and create test directory" })),
|
|
2372
4370
|
force: t.optional(t.boolean({
|
|
2373
4371
|
aliases: ["f"],
|
|
@@ -2375,11 +4373,14 @@ var InitCommand = class {
|
|
|
2375
4373
|
}))
|
|
2376
4374
|
}),
|
|
2377
4375
|
handler: async ({ run, flags, root, args }) => {
|
|
2378
|
-
if (flags.react) flags.ui = true;
|
|
2379
4376
|
if (args) {
|
|
2380
4377
|
root = this.fs.join(root, args);
|
|
2381
4378
|
await this.fs.mkdir(root);
|
|
2382
4379
|
}
|
|
4380
|
+
if (flags.ui) flags.react = true;
|
|
4381
|
+
const workspace = await this.pm.getWorkspaceContext(root);
|
|
4382
|
+
let agentType = false;
|
|
4383
|
+
if (flags.agent) agentType = await this.utils.isInstalledAsync("claude") ? "claude" : "agents";
|
|
2383
4384
|
const isExpo = await this.pm.hasExpo(root);
|
|
2384
4385
|
const force = !!flags.force;
|
|
2385
4386
|
await run({
|
|
@@ -2387,46 +4388,55 @@ var InitCommand = class {
|
|
|
2387
4388
|
handler: async () => {
|
|
2388
4389
|
await this.scaffolder.ensureConfig(root, {
|
|
2389
4390
|
force,
|
|
2390
|
-
tsconfigJson:
|
|
2391
|
-
packageJson:
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
4391
|
+
tsconfigJson: !workspace.config.tsconfigJson,
|
|
4392
|
+
packageJson: {
|
|
4393
|
+
...flags,
|
|
4394
|
+
isPackage: workspace.isPackage
|
|
4395
|
+
},
|
|
4396
|
+
biomeJson: !workspace.config.biomeJson,
|
|
4397
|
+
editorconfig: !workspace.config.editorconfig,
|
|
4398
|
+
agentMd: agentType ? {
|
|
4399
|
+
type: agentType,
|
|
2396
4400
|
react: !!flags.react,
|
|
2397
4401
|
ui: !!flags.ui
|
|
2398
4402
|
} : false
|
|
2399
4403
|
});
|
|
2400
|
-
|
|
4404
|
+
await this.scaffolder.ensureMainServerTs(root, {
|
|
4405
|
+
api: !!flags.api,
|
|
4406
|
+
react: !!flags.react && !isExpo,
|
|
4407
|
+
force
|
|
4408
|
+
});
|
|
4409
|
+
if (flags.api) await this.scaffolder.ensureApiProject(root, { force });
|
|
4410
|
+
if (flags.react && !isExpo) await this.scaffolder.ensureWebProject(root, {
|
|
4411
|
+
api: !!flags.api,
|
|
4412
|
+
ui: !!flags.ui,
|
|
4413
|
+
force
|
|
4414
|
+
});
|
|
2401
4415
|
}
|
|
2402
4416
|
});
|
|
2403
|
-
const pmName = await this.pm.getPackageManager(root, flags);
|
|
2404
|
-
if (pmName === "yarn") {
|
|
4417
|
+
const pmName = await this.pm.getPackageManager(workspace.workspaceRoot ?? root, flags.pm ?? workspace.packageManager ?? void 0);
|
|
4418
|
+
if (!workspace.isPackage) if (pmName === "yarn") {
|
|
2405
4419
|
await this.pm.ensureYarn(root);
|
|
2406
4420
|
await run("yarn set version stable", { root });
|
|
2407
4421
|
} else if (pmName === "bun") await this.pm.ensureBun(root);
|
|
2408
4422
|
else if (pmName === "pnpm") await this.pm.ensurePnpm(root);
|
|
2409
4423
|
else await this.pm.ensureNpm(root);
|
|
4424
|
+
const installRoot = workspace.workspaceRoot ?? root;
|
|
2410
4425
|
await run(`${pmName} install`, {
|
|
2411
4426
|
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)
|
|
4427
|
+
root: installRoot
|
|
2421
4428
|
});
|
|
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
|
-
}
|
|
4429
|
+
if (flags.test) await this.scaffolder.ensureTestDir(root);
|
|
2426
4430
|
await run(`${pmName} run lint`, {
|
|
2427
4431
|
alias: "running linter",
|
|
2428
|
-
root
|
|
4432
|
+
root: installRoot
|
|
2429
4433
|
});
|
|
4434
|
+
if (!workspace.isPackage) {
|
|
4435
|
+
if (await this.scaffolder.ensureGitRepo(root, { force })) await run("git add .", {
|
|
4436
|
+
alias: "staging generated files",
|
|
4437
|
+
root
|
|
4438
|
+
});
|
|
4439
|
+
}
|
|
2430
4440
|
}
|
|
2431
4441
|
});
|
|
2432
4442
|
};
|
|
@@ -2441,8 +4451,14 @@ var LintCommand = class {
|
|
|
2441
4451
|
name: "lint",
|
|
2442
4452
|
description: "Run linter across the codebase using Biome",
|
|
2443
4453
|
handler: async ({ root }) => {
|
|
2444
|
-
await this.scaffolder.ensureConfig(root, {
|
|
2445
|
-
|
|
4454
|
+
await this.scaffolder.ensureConfig(root, {
|
|
4455
|
+
biomeJson: true,
|
|
4456
|
+
checkWorkspace: true
|
|
4457
|
+
});
|
|
4458
|
+
await this.pm.ensureDependency(root, "@biomejs/biome", {
|
|
4459
|
+
checkWorkspace: true,
|
|
4460
|
+
exec: (cmd, opts) => this.utils.exec(cmd, opts)
|
|
4461
|
+
});
|
|
2446
4462
|
await this.utils.exec("biome check --fix");
|
|
2447
4463
|
}
|
|
2448
4464
|
});
|
|
@@ -2508,6 +4524,7 @@ var TypecheckCommand = class {
|
|
|
2508
4524
|
utils = $inject(AlephaCliUtils);
|
|
2509
4525
|
pm = $inject(PackageManagerUtils);
|
|
2510
4526
|
log = $logger();
|
|
4527
|
+
scaffolder = $inject(ProjectScaffolder);
|
|
2511
4528
|
/**
|
|
2512
4529
|
* Run TypeScript type checking across the codebase with no emit.
|
|
2513
4530
|
*/
|
|
@@ -2517,7 +4534,14 @@ var TypecheckCommand = class {
|
|
|
2517
4534
|
description: "Check TypeScript types across the codebase",
|
|
2518
4535
|
handler: async ({ root }) => {
|
|
2519
4536
|
this.log.info("Starting TypeScript type checking...");
|
|
2520
|
-
await this.
|
|
4537
|
+
await this.scaffolder.ensureConfig(root, {
|
|
4538
|
+
tsconfigJson: true,
|
|
4539
|
+
checkWorkspace: true
|
|
4540
|
+
});
|
|
4541
|
+
await this.pm.ensureDependency(root, "typescript", {
|
|
4542
|
+
checkWorkspace: true,
|
|
4543
|
+
exec: (cmd, opts) => this.utils.exec(cmd, opts)
|
|
4544
|
+
});
|
|
2521
4545
|
await this.utils.exec("tsc --noEmit");
|
|
2522
4546
|
this.log.info("TypeScript type checking completed successfully.");
|
|
2523
4547
|
}
|
|
@@ -2561,15 +4585,6 @@ var VerifyCommand = class {
|
|
|
2561
4585
|
//#endregion
|
|
2562
4586
|
//#region ../../src/cli/apps/AlephaCli.ts
|
|
2563
4587
|
/**
|
|
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
4588
|
* Allow to extend Alepha CLI via `alepha.config.ts` file located in the project root.
|
|
2574
4589
|
*/
|
|
2575
4590
|
var AlephaCliExtension = class {
|