threadforge 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -20
- package/bin/forge.js +2 -1058
- package/bin/host-commands.d.ts +2 -0
- package/bin/host-commands.d.ts.map +1 -0
- package/bin/host-commands.js +7 -8
- package/bin/platform-commands.d.ts +2 -0
- package/bin/platform-commands.d.ts.map +1 -0
- package/bin/platform-commands.js +118 -36
- package/dist/cli/base-command.d.ts +12 -0
- package/dist/cli/base-command.d.ts.map +1 -0
- package/dist/cli/base-command.js +25 -0
- package/dist/cli/base-command.js.map +1 -0
- package/dist/cli/commands/build.d.ts +10 -0
- package/dist/cli/commands/build.d.ts.map +1 -0
- package/dist/cli/commands/build.js +110 -0
- package/dist/cli/commands/build.js.map +1 -0
- package/dist/cli/commands/deploy.d.ts +12 -0
- package/dist/cli/commands/deploy.d.ts.map +1 -0
- package/dist/cli/commands/deploy.js +143 -0
- package/dist/cli/commands/deploy.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +10 -0
- package/dist/cli/commands/dev.d.ts.map +1 -0
- package/dist/cli/commands/dev.js +138 -0
- package/dist/cli/commands/dev.js.map +1 -0
- package/dist/cli/commands/generate.d.ts +10 -0
- package/dist/cli/commands/generate.d.ts.map +1 -0
- package/dist/cli/commands/generate.js +76 -0
- package/dist/cli/commands/generate.js.map +1 -0
- package/dist/cli/commands/host.d.ts +8 -0
- package/dist/cli/commands/host.d.ts.map +1 -0
- package/dist/cli/commands/host.js +20 -0
- package/dist/cli/commands/host.js.map +1 -0
- package/dist/cli/commands/init.d.ts +16 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +246 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/platform.d.ts +8 -0
- package/dist/cli/commands/platform.d.ts.map +1 -0
- package/dist/cli/commands/platform.js +20 -0
- package/dist/cli/commands/platform.js.map +1 -0
- package/dist/cli/commands/restart.d.ts +8 -0
- package/dist/cli/commands/restart.d.ts.map +1 -0
- package/dist/cli/commands/restart.js +13 -0
- package/dist/cli/commands/restart.js.map +1 -0
- package/dist/cli/commands/scaffold/frontend.d.ts +10 -0
- package/dist/cli/commands/scaffold/frontend.d.ts.map +1 -0
- package/dist/cli/commands/scaffold/frontend.js +130 -0
- package/dist/cli/commands/scaffold/frontend.js.map +1 -0
- package/dist/cli/commands/scaffold/react.d.ts +7 -0
- package/dist/cli/commands/scaffold/react.d.ts.map +1 -0
- package/dist/cli/commands/scaffold/react.js +12 -0
- package/dist/cli/commands/scaffold/react.js.map +1 -0
- package/dist/cli/commands/scale.d.ts +8 -0
- package/dist/cli/commands/scale.d.ts.map +1 -0
- package/dist/cli/commands/scale.js +13 -0
- package/dist/cli/commands/scale.js.map +1 -0
- package/dist/cli/commands/start.d.ts +10 -0
- package/dist/cli/commands/start.d.ts.map +1 -0
- package/dist/cli/commands/start.js +71 -0
- package/dist/cli/commands/start.js.map +1 -0
- package/dist/cli/commands/status.d.ts +11 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +60 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/stop.d.ts +10 -0
- package/dist/cli/commands/stop.d.ts.map +1 -0
- package/dist/cli/commands/stop.js +89 -0
- package/dist/cli/commands/stop.js.map +1 -0
- package/dist/cli/util/config-discovery.d.ts +8 -0
- package/dist/cli/util/config-discovery.d.ts.map +1 -0
- package/dist/cli/util/config-discovery.js +70 -0
- package/dist/cli/util/config-discovery.js.map +1 -0
- package/dist/cli/util/config-patcher.d.ts +17 -0
- package/dist/cli/util/config-patcher.d.ts.map +1 -0
- package/dist/cli/util/config-patcher.js +439 -0
- package/dist/cli/util/config-patcher.js.map +1 -0
- package/dist/cli/util/frontend-dev.d.ts +8 -0
- package/dist/cli/util/frontend-dev.d.ts.map +1 -0
- package/dist/cli/util/frontend-dev.js +117 -0
- package/dist/cli/util/frontend-dev.js.map +1 -0
- package/dist/cli/util/process.d.ts +5 -0
- package/dist/cli/util/process.d.ts.map +1 -0
- package/dist/cli/util/process.js +17 -0
- package/dist/cli/util/process.js.map +1 -0
- package/dist/cli/util/templates.d.ts +10 -0
- package/dist/cli/util/templates.d.ts.map +1 -0
- package/dist/cli/util/templates.js +157 -0
- package/dist/cli/util/templates.js.map +1 -0
- package/dist/core/AlertSink.d.ts +83 -0
- package/dist/core/AlertSink.d.ts.map +1 -0
- package/dist/core/AlertSink.js +126 -0
- package/dist/core/AlertSink.js.map +1 -0
- package/dist/core/DirectMessageBus.d.ts +88 -0
- package/dist/core/DirectMessageBus.d.ts.map +1 -0
- package/dist/core/DirectMessageBus.js +352 -0
- package/dist/core/DirectMessageBus.js.map +1 -0
- package/dist/core/EndpointResolver.d.ts +111 -0
- package/dist/core/EndpointResolver.d.ts.map +1 -0
- package/dist/core/EndpointResolver.js +336 -0
- package/dist/core/EndpointResolver.js.map +1 -0
- package/dist/core/ForgeContext.d.ts +221 -0
- package/dist/core/ForgeContext.d.ts.map +1 -0
- package/dist/core/ForgeContext.js +1169 -0
- package/dist/core/ForgeContext.js.map +1 -0
- package/dist/core/ForgeEndpoints.d.ts +71 -0
- package/dist/core/ForgeEndpoints.d.ts.map +1 -0
- package/dist/core/ForgeEndpoints.js +442 -0
- package/dist/core/ForgeEndpoints.js.map +1 -0
- package/dist/core/ForgeHost.d.ts +82 -0
- package/dist/core/ForgeHost.d.ts.map +1 -0
- package/dist/core/ForgeHost.js +107 -0
- package/dist/core/ForgeHost.js.map +1 -0
- package/dist/core/ForgePlatform.d.ts +96 -0
- package/dist/core/ForgePlatform.d.ts.map +1 -0
- package/dist/core/ForgePlatform.js +136 -0
- package/dist/core/ForgePlatform.js.map +1 -0
- package/dist/core/ForgeWebSocket.d.ts +56 -0
- package/dist/core/ForgeWebSocket.d.ts.map +1 -0
- package/dist/core/ForgeWebSocket.js +415 -0
- package/dist/core/ForgeWebSocket.js.map +1 -0
- package/dist/core/Ingress.d.ts +329 -0
- package/dist/core/Ingress.d.ts.map +1 -0
- package/dist/core/Ingress.js +694 -0
- package/dist/core/Ingress.js.map +1 -0
- package/dist/core/Interceptors.d.ts +134 -0
- package/dist/core/Interceptors.d.ts.map +1 -0
- package/dist/core/Interceptors.js +416 -0
- package/dist/core/Interceptors.js.map +1 -0
- package/dist/core/Logger.d.ts +20 -0
- package/dist/core/Logger.d.ts.map +1 -0
- package/dist/core/Logger.js +77 -0
- package/dist/core/Logger.js.map +1 -0
- package/dist/core/MessageBus.d.ts +15 -0
- package/dist/core/MessageBus.d.ts.map +1 -0
- package/dist/core/MessageBus.js +18 -0
- package/dist/core/MessageBus.js.map +1 -0
- package/dist/core/Prometheus.d.ts +80 -0
- package/dist/core/Prometheus.d.ts.map +1 -0
- package/dist/core/Prometheus.js +332 -0
- package/dist/core/Prometheus.js.map +1 -0
- package/dist/core/RequestContext.d.ts +214 -0
- package/dist/core/RequestContext.d.ts.map +1 -0
- package/dist/core/RequestContext.js +556 -0
- package/dist/core/RequestContext.js.map +1 -0
- package/dist/core/Router.d.ts +45 -0
- package/dist/core/Router.d.ts.map +1 -0
- package/dist/core/Router.js +285 -0
- package/dist/core/Router.js.map +1 -0
- package/dist/core/RoutingStrategy.d.ts +116 -0
- package/dist/core/RoutingStrategy.d.ts.map +1 -0
- package/dist/core/RoutingStrategy.js +306 -0
- package/dist/core/RoutingStrategy.js.map +1 -0
- package/dist/core/RpcConfig.d.ts +72 -0
- package/dist/core/RpcConfig.d.ts.map +1 -0
- package/dist/core/RpcConfig.js +127 -0
- package/dist/core/RpcConfig.js.map +1 -0
- package/dist/core/SignatureCache.d.ts +81 -0
- package/dist/core/SignatureCache.d.ts.map +1 -0
- package/dist/core/SignatureCache.js +172 -0
- package/dist/core/SignatureCache.js.map +1 -0
- package/dist/core/StaticFileServer.d.ts +34 -0
- package/dist/core/StaticFileServer.d.ts.map +1 -0
- package/dist/core/StaticFileServer.js +497 -0
- package/dist/core/StaticFileServer.js.map +1 -0
- package/dist/core/Supervisor.d.ts +198 -0
- package/dist/core/Supervisor.d.ts.map +1 -0
- package/dist/core/Supervisor.js +1418 -0
- package/dist/core/Supervisor.js.map +1 -0
- package/dist/core/ThreadAllocator.d.ts +52 -0
- package/dist/core/ThreadAllocator.d.ts.map +1 -0
- package/dist/core/ThreadAllocator.js +174 -0
- package/dist/core/ThreadAllocator.js.map +1 -0
- package/dist/core/WorkerChannelManager.d.ts +130 -0
- package/dist/core/WorkerChannelManager.d.ts.map +1 -0
- package/dist/core/WorkerChannelManager.js +956 -0
- package/dist/core/WorkerChannelManager.js.map +1 -0
- package/dist/core/config-enums.d.ts +41 -0
- package/dist/core/config-enums.d.ts.map +1 -0
- package/dist/core/config-enums.js +59 -0
- package/dist/core/config-enums.js.map +1 -0
- package/dist/core/config.d.ts +159 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +694 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/host-config.d.ts +146 -0
- package/dist/core/host-config.d.ts.map +1 -0
- package/dist/core/host-config.js +312 -0
- package/dist/core/host-config.js.map +1 -0
- package/dist/core/ipc-errors.d.ts +27 -0
- package/dist/core/ipc-errors.d.ts.map +1 -0
- package/dist/core/ipc-errors.js +36 -0
- package/dist/core/ipc-errors.js.map +1 -0
- package/dist/core/network-utils.d.ts +35 -0
- package/dist/core/network-utils.d.ts.map +1 -0
- package/dist/core/network-utils.js +145 -0
- package/dist/core/network-utils.js.map +1 -0
- package/dist/core/platform-config.d.ts +142 -0
- package/dist/core/platform-config.d.ts.map +1 -0
- package/dist/core/platform-config.js +299 -0
- package/dist/core/platform-config.js.map +1 -0
- package/dist/decorators/ServiceProxy.d.ts +175 -0
- package/dist/decorators/ServiceProxy.d.ts.map +1 -0
- package/dist/decorators/ServiceProxy.js +969 -0
- package/dist/decorators/ServiceProxy.js.map +1 -0
- package/dist/decorators/index.d.ts +146 -0
- package/dist/decorators/index.d.ts.map +1 -0
- package/dist/decorators/index.js +545 -0
- package/dist/decorators/index.js.map +1 -0
- package/dist/deploy/NginxGenerator.d.ts +165 -0
- package/dist/deploy/NginxGenerator.d.ts.map +1 -0
- package/dist/deploy/NginxGenerator.js +781 -0
- package/dist/deploy/NginxGenerator.js.map +1 -0
- package/dist/deploy/PlatformManifestGenerator.d.ts +43 -0
- package/dist/deploy/PlatformManifestGenerator.d.ts.map +1 -0
- package/dist/deploy/PlatformManifestGenerator.js +80 -0
- package/dist/deploy/PlatformManifestGenerator.js.map +1 -0
- package/dist/deploy/RouteManifestGenerator.d.ts +42 -0
- package/dist/deploy/RouteManifestGenerator.d.ts.map +1 -0
- package/dist/deploy/RouteManifestGenerator.js +105 -0
- package/dist/deploy/RouteManifestGenerator.js.map +1 -0
- package/dist/deploy/index.d.ts +210 -0
- package/dist/deploy/index.d.ts.map +1 -0
- package/dist/deploy/index.js +918 -0
- package/dist/deploy/index.js.map +1 -0
- package/dist/frontend/FrontendDevLifecycle.d.ts +26 -0
- package/dist/frontend/FrontendDevLifecycle.d.ts.map +1 -0
- package/dist/frontend/FrontendDevLifecycle.js +60 -0
- package/dist/frontend/FrontendDevLifecycle.js.map +1 -0
- package/dist/frontend/FrontendPluginOrchestrator.d.ts +64 -0
- package/dist/frontend/FrontendPluginOrchestrator.d.ts.map +1 -0
- package/dist/frontend/FrontendPluginOrchestrator.js +167 -0
- package/dist/frontend/FrontendPluginOrchestrator.js.map +1 -0
- package/dist/frontend/SiteResolver.d.ts +33 -0
- package/dist/frontend/SiteResolver.d.ts.map +1 -0
- package/dist/frontend/SiteResolver.js +53 -0
- package/dist/frontend/SiteResolver.js.map +1 -0
- package/dist/frontend/StaticMountRegistry.d.ts +36 -0
- package/dist/frontend/StaticMountRegistry.d.ts.map +1 -0
- package/dist/frontend/StaticMountRegistry.js +94 -0
- package/dist/frontend/StaticMountRegistry.js.map +1 -0
- package/dist/frontend/index.d.ts +7 -0
- package/dist/frontend/index.d.ts.map +1 -0
- package/{src → dist}/frontend/index.js +4 -2
- package/dist/frontend/index.js.map +1 -0
- package/dist/frontend/pathUtils.d.ts +8 -0
- package/dist/frontend/pathUtils.d.ts.map +1 -0
- package/dist/frontend/pathUtils.js +17 -0
- package/dist/frontend/pathUtils.js.map +1 -0
- package/dist/frontend/plugins/index.d.ts +2 -0
- package/dist/frontend/plugins/index.d.ts.map +1 -0
- package/{src → dist}/frontend/plugins/index.js +1 -1
- package/dist/frontend/plugins/index.js.map +1 -0
- package/dist/frontend/plugins/viteFrontend.d.ts +51 -0
- package/dist/frontend/plugins/viteFrontend.d.ts.map +1 -0
- package/dist/frontend/plugins/viteFrontend.js +134 -0
- package/dist/frontend/plugins/viteFrontend.js.map +1 -0
- package/dist/frontend/types.d.ts +25 -0
- package/dist/frontend/types.d.ts.map +1 -0
- package/dist/frontend/types.js +2 -0
- package/dist/frontend/types.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/internals.d.ts +21 -0
- package/dist/internals.d.ts.map +1 -0
- package/{src → dist}/internals.js +12 -14
- package/dist/internals.js.map +1 -0
- package/dist/plugins/PluginManager.d.ts +209 -0
- package/dist/plugins/PluginManager.d.ts.map +1 -0
- package/dist/plugins/PluginManager.js +365 -0
- package/dist/plugins/PluginManager.js.map +1 -0
- package/dist/plugins/ScopedPostgres.d.ts +78 -0
- package/dist/plugins/ScopedPostgres.d.ts.map +1 -0
- package/dist/plugins/ScopedPostgres.js +190 -0
- package/dist/plugins/ScopedPostgres.js.map +1 -0
- package/dist/plugins/ScopedRedis.d.ts +88 -0
- package/dist/plugins/ScopedRedis.d.ts.map +1 -0
- package/dist/plugins/ScopedRedis.js +169 -0
- package/dist/plugins/ScopedRedis.js.map +1 -0
- package/dist/plugins/index.d.ts +289 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +1942 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/plugins/types.d.ts +59 -0
- package/dist/plugins/types.d.ts.map +1 -0
- package/dist/plugins/types.js +2 -0
- package/dist/plugins/types.js.map +1 -0
- package/dist/registry/ServiceRegistry.d.ts +305 -0
- package/dist/registry/ServiceRegistry.d.ts.map +1 -0
- package/dist/registry/ServiceRegistry.js +735 -0
- package/dist/registry/ServiceRegistry.js.map +1 -0
- package/dist/scaling/ScaleAdvisor.d.ts +214 -0
- package/dist/scaling/ScaleAdvisor.d.ts.map +1 -0
- package/dist/scaling/ScaleAdvisor.js +526 -0
- package/dist/scaling/ScaleAdvisor.js.map +1 -0
- package/dist/services/Service.d.ts +164 -0
- package/dist/services/Service.d.ts.map +1 -0
- package/dist/services/Service.js +106 -0
- package/dist/services/Service.js.map +1 -0
- package/dist/services/worker-bootstrap.d.ts +15 -0
- package/dist/services/worker-bootstrap.d.ts.map +1 -0
- package/dist/services/worker-bootstrap.js +744 -0
- package/dist/services/worker-bootstrap.js.map +1 -0
- package/dist/templates/auth-service.d.ts +42 -0
- package/dist/templates/auth-service.d.ts.map +1 -0
- package/dist/templates/auth-service.js +54 -0
- package/dist/templates/auth-service.js.map +1 -0
- package/dist/templates/identity-service.d.ts +50 -0
- package/dist/templates/identity-service.d.ts.map +1 -0
- package/dist/templates/identity-service.js +62 -0
- package/dist/templates/identity-service.js.map +1 -0
- package/dist/types/contract.d.ts +120 -0
- package/dist/types/contract.d.ts.map +1 -0
- package/dist/types/contract.js +69 -0
- package/dist/types/contract.js.map +1 -0
- package/package.json +78 -20
- package/src/core/DirectMessageBus.js +0 -364
- package/src/core/EndpointResolver.js +0 -259
- package/src/core/ForgeContext.js +0 -2236
- package/src/core/ForgeHost.js +0 -122
- package/src/core/ForgePlatform.js +0 -145
- package/src/core/Ingress.js +0 -768
- package/src/core/Interceptors.js +0 -420
- package/src/core/MessageBus.js +0 -321
- package/src/core/Prometheus.js +0 -305
- package/src/core/RequestContext.js +0 -413
- package/src/core/RoutingStrategy.js +0 -330
- package/src/core/Supervisor.js +0 -1349
- package/src/core/ThreadAllocator.js +0 -196
- package/src/core/WorkerChannelManager.js +0 -879
- package/src/core/config.js +0 -637
- package/src/core/host-config.js +0 -311
- package/src/core/network-utils.js +0 -166
- package/src/core/platform-config.js +0 -308
- package/src/decorators/ServiceProxy.js +0 -904
- package/src/decorators/index.js +0 -571
- package/src/deploy/NginxGenerator.js +0 -865
- package/src/deploy/PlatformManifestGenerator.js +0 -96
- package/src/deploy/RouteManifestGenerator.js +0 -112
- package/src/deploy/index.js +0 -984
- package/src/frontend/FrontendDevLifecycle.js +0 -65
- package/src/frontend/FrontendPluginOrchestrator.js +0 -187
- package/src/frontend/SiteResolver.js +0 -63
- package/src/frontend/StaticMountRegistry.js +0 -90
- package/src/frontend/plugins/viteFrontend.js +0 -79
- package/src/frontend/types.js +0 -35
- package/src/index.js +0 -58
- package/src/plugins/PluginManager.js +0 -537
- package/src/plugins/ScopedPostgres.js +0 -192
- package/src/plugins/ScopedRedis.js +0 -142
- package/src/plugins/index.js +0 -1756
- package/src/registry/ServiceRegistry.js +0 -797
- package/src/scaling/ScaleAdvisor.js +0 -442
- package/src/services/Service.js +0 -195
- package/src/services/worker-bootstrap.js +0 -679
- package/src/templates/auth-service.js +0 -65
- package/src/templates/identity-service.js +0 -75
package/src/deploy/index.js
DELETED
|
@@ -1,984 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Deployment System
|
|
3
|
-
*
|
|
4
|
-
* The problem: you have 8 services and 3 machines. You need to
|
|
5
|
-
* decide what runs where, generate the right configs for each
|
|
6
|
-
* machine, ensure discovery works, handle rolling deploys, and
|
|
7
|
-
* do it all without the developer manually editing addresses.
|
|
8
|
-
*
|
|
9
|
-
* ═══════════════════════════════════════════════════════════════
|
|
10
|
-
* THE DEPLOYMENT MANIFEST
|
|
11
|
-
* ═══════════════════════════════════════════════════════════════
|
|
12
|
-
*
|
|
13
|
-
* One file describes your entire cluster: forge.deploy.js
|
|
14
|
-
*
|
|
15
|
-
* export default {
|
|
16
|
-
* cluster: 'my-saas-prod',
|
|
17
|
-
*
|
|
18
|
-
* nodes: {
|
|
19
|
-
* 'web-1': {
|
|
20
|
-
* host: '10.0.1.10',
|
|
21
|
-
* services: ['gateway', 'auth'],
|
|
22
|
-
* role: 'edge',
|
|
23
|
-
* },
|
|
24
|
-
* 'api-1': {
|
|
25
|
-
* host: '10.0.1.20',
|
|
26
|
-
* services: ['users', 'billing', 'search'],
|
|
27
|
-
* role: 'api',
|
|
28
|
-
* },
|
|
29
|
-
* 'worker-1': {
|
|
30
|
-
* host: '10.0.1.30',
|
|
31
|
-
* services: ['notifications', 'analytics', 'media'],
|
|
32
|
-
* role: 'worker',
|
|
33
|
-
* },
|
|
34
|
-
* },
|
|
35
|
-
*
|
|
36
|
-
* // Optional: override per-node settings
|
|
37
|
-
* overrides: {
|
|
38
|
-
* 'api-1': {
|
|
39
|
-
* users: { threads: 4, weight: 5 },
|
|
40
|
-
* },
|
|
41
|
-
* },
|
|
42
|
-
*
|
|
43
|
-
* registry: 'multicast', // or 'redis://...' or 'etcd://...'
|
|
44
|
-
*
|
|
45
|
-
* httpBasePort: 4000,
|
|
46
|
-
* };
|
|
47
|
-
*
|
|
48
|
-
* From this manifest, `forge deploy` generates:
|
|
49
|
-
*
|
|
50
|
-
* 1. Per-node forge.config.js (services this node runs locally,
|
|
51
|
-
* all others as type: 'remote' with addresses)
|
|
52
|
-
*
|
|
53
|
-
* 2. Docker Compose / docker-compose.yml for local dev that
|
|
54
|
-
* simulates the multi-machine topology
|
|
55
|
-
*
|
|
56
|
-
* 3. Systemd unit files for production deployment
|
|
57
|
-
*
|
|
58
|
-
* 4. HTTP-based inter-service communication for all services
|
|
59
|
-
* that cross machine boundaries
|
|
60
|
-
*
|
|
61
|
-
* 5. Deployment scripts (rsync + restart)
|
|
62
|
-
*
|
|
63
|
-
* ═══════════════════════════════════════════════════════════════
|
|
64
|
-
* NODE CONFIG GENERATION
|
|
65
|
-
* ═══════════════════════════════════════════════════════════════
|
|
66
|
-
*
|
|
67
|
-
* For web-1 (runs gateway + auth), the generated config looks like:
|
|
68
|
-
*
|
|
69
|
-
* // AUTO-GENERATED by forge deploy — do not edit
|
|
70
|
-
* export default defineServices({
|
|
71
|
-
* // Local services (this machine runs these)
|
|
72
|
-
* gateway: {
|
|
73
|
-
* entry: './services/gateway.js',
|
|
74
|
-
* type: 'edge',
|
|
75
|
-
* port: 3000,
|
|
76
|
-
* threads: 'auto',
|
|
77
|
-
* connects: ['auth', 'users', 'search'],
|
|
78
|
-
* },
|
|
79
|
-
* auth: {
|
|
80
|
-
* entry: './services/auth.js',
|
|
81
|
-
* type: 'internal',
|
|
82
|
-
* connects: ['users'],
|
|
83
|
-
* },
|
|
84
|
-
*
|
|
85
|
-
* // Remote services (on other machines, reached via HTTP)
|
|
86
|
-
* users: { type: 'remote', address: 'http://10.0.1.20:4000' },
|
|
87
|
-
* billing: { type: 'remote', address: 'http://10.0.1.20:4001' },
|
|
88
|
-
* search: { type: 'remote', address: 'http://10.0.1.20:4002' },
|
|
89
|
-
* notifications: { type: 'remote', address: 'http://10.0.1.30:4000' },
|
|
90
|
-
* analytics: { type: 'remote', address: 'http://10.0.1.30:4001' },
|
|
91
|
-
* media: { type: 'remote', address: 'http://10.0.1.30:4002' },
|
|
92
|
-
* }, {
|
|
93
|
-
* registryMode: 'multicast',
|
|
94
|
-
* host: '10.0.1.10',
|
|
95
|
-
* });
|
|
96
|
-
*
|
|
97
|
-
* The developer's service code is IDENTICAL on every machine.
|
|
98
|
-
* Only the generated config differs.
|
|
99
|
-
*/
|
|
100
|
-
|
|
101
|
-
import fs from "node:fs";
|
|
102
|
-
import path from "node:path";
|
|
103
|
-
import { resolveAppDomains } from "../core/platform-config.js";
|
|
104
|
-
import {
|
|
105
|
-
buildNginxServiceMap,
|
|
106
|
-
buildNginxWebSocketRoutes,
|
|
107
|
-
generateNginxConfig,
|
|
108
|
-
generatePlatformNginxConfig,
|
|
109
|
-
} from "./NginxGenerator.js";
|
|
110
|
-
import { generatePlatformManifest } from "./PlatformManifestGenerator.js";
|
|
111
|
-
|
|
112
|
-
// ── Port assignment helper ────────────────────────────────────
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Assign HTTP ports to all services across all nodes.
|
|
116
|
-
*
|
|
117
|
-
* Edge services keep their configured port; internal/background services
|
|
118
|
-
* get sequential ports starting from httpBasePort, skipping any already
|
|
119
|
-
* claimed by edge services on that node.
|
|
120
|
-
*
|
|
121
|
-
* @param {Object} manifest - Validated manifest with .nodes
|
|
122
|
-
* @param {Object} serviceConfigs - Base service configs
|
|
123
|
-
* @param {number} httpBasePort - Starting port for auto-assignment
|
|
124
|
-
* @returns {Object} servicePorts — { serviceName: [{ host, port }, ...] }
|
|
125
|
-
*/
|
|
126
|
-
function _assignPorts(manifest, serviceConfigs, httpBasePort) {
|
|
127
|
-
const servicePorts = {};
|
|
128
|
-
for (const [nodeName, node] of Object.entries(manifest.nodes)) {
|
|
129
|
-
const usedPorts = new Set();
|
|
130
|
-
for (const svcName of node.services) {
|
|
131
|
-
const existingPort = serviceConfigs[svcName]?.port;
|
|
132
|
-
if (existingPort) usedPorts.add(existingPort);
|
|
133
|
-
}
|
|
134
|
-
let nextPort = httpBasePort;
|
|
135
|
-
for (const svcName of node.services) {
|
|
136
|
-
const existingPort = serviceConfigs[svcName]?.port;
|
|
137
|
-
if (existingPort) {
|
|
138
|
-
if (!servicePorts[svcName]) servicePorts[svcName] = [];
|
|
139
|
-
servicePorts[svcName].push({ host: node.host, port: existingPort });
|
|
140
|
-
} else {
|
|
141
|
-
while (usedPorts.has(nextPort)) nextPort++;
|
|
142
|
-
if (nextPort > 65535) {
|
|
143
|
-
throw new Error(`Port exhaustion on node "${nodeName}": cannot allocate ports beyond 65535`);
|
|
144
|
-
}
|
|
145
|
-
const port = nextPort++;
|
|
146
|
-
usedPorts.add(port);
|
|
147
|
-
if (!servicePorts[svcName]) servicePorts[svcName] = [];
|
|
148
|
-
servicePorts[svcName].push({ host: node.host, port });
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return servicePorts;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// ── Input validation for deploy safety ───────────────────────
|
|
156
|
-
|
|
157
|
-
const NODE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
|
|
158
|
-
const HOSTNAME_RE = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$/;
|
|
159
|
-
const IPV4_RE = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
160
|
-
const IPV6_RE = /^[0-9a-fA-F:]+$/;
|
|
161
|
-
const SHELL_METACHAR_RE = /[`$;|&<>(){}[\]\\'"!\s]/;
|
|
162
|
-
|
|
163
|
-
function validateNodeName(name) {
|
|
164
|
-
if (!NODE_NAME_RE.test(name)) {
|
|
165
|
-
throw new Error(`Invalid node name "${name}": must start with alphanumeric and contain only [a-zA-Z0-9_-]`);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function validateHostname(host, nodeName) {
|
|
170
|
-
// H-DEPLOY-2: Reject hosts with shell metacharacters first
|
|
171
|
-
if (SHELL_METACHAR_RE.test(host)) {
|
|
172
|
-
throw new Error(`Invalid host "${host}" for node "${nodeName}": contains shell metacharacters`);
|
|
173
|
-
}
|
|
174
|
-
if (HOSTNAME_RE.test(host) || IPV4_RE.test(host) || IPV6_RE.test(host)) {
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
throw new Error(`Invalid host "${host}" for node "${nodeName}": must be a valid hostname, IPv4, or IPv6 address`);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Load and validate a deployment manifest.
|
|
182
|
-
*
|
|
183
|
-
* @param {Object} manifest - The deployment manifest
|
|
184
|
-
* @param {Object} serviceConfigs - The base service configs from forge.config.js
|
|
185
|
-
* @returns {Object} Validated manifest with computed fields
|
|
186
|
-
*/
|
|
187
|
-
export function loadManifest(manifest, serviceConfigs) {
|
|
188
|
-
if (!manifest.nodes || Object.keys(manifest.nodes).length === 0) {
|
|
189
|
-
throw new Error("Deployment manifest must define at least one node");
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Validate nodes
|
|
193
|
-
const allAssigned = new Set();
|
|
194
|
-
for (const [nodeName, node] of Object.entries(manifest.nodes)) {
|
|
195
|
-
validateNodeName(nodeName);
|
|
196
|
-
if (!node.host) {
|
|
197
|
-
throw new Error(`Node "${nodeName}" is missing a host address`);
|
|
198
|
-
}
|
|
199
|
-
validateHostname(node.host, nodeName);
|
|
200
|
-
if (!node.services || node.services.length === 0) {
|
|
201
|
-
throw new Error(`Node "${nodeName}" has no services assigned`);
|
|
202
|
-
}
|
|
203
|
-
for (const svc of node.services) {
|
|
204
|
-
allAssigned.add(svc);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Check for unassigned services
|
|
209
|
-
for (const svcName of Object.keys(serviceConfigs)) {
|
|
210
|
-
if (!allAssigned.has(svcName)) {
|
|
211
|
-
throw new Error(
|
|
212
|
-
`Service "${svcName}" from forge.config.js is not assigned to any node. ` +
|
|
213
|
-
`Add it to a node in forge.deploy.js.`,
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Build the map: service → [nodes]
|
|
219
|
-
const serviceToNodes = {};
|
|
220
|
-
for (const [nodeName, node] of Object.entries(manifest.nodes)) {
|
|
221
|
-
for (const svc of node.services) {
|
|
222
|
-
if (!serviceToNodes[svc]) serviceToNodes[svc] = [];
|
|
223
|
-
serviceToNodes[svc].push({ node: nodeName, host: node.host });
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return {
|
|
228
|
-
...manifest,
|
|
229
|
-
serviceToNode: serviceToNodes, // now an array
|
|
230
|
-
allServices: Object.keys(serviceConfigs),
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Generate per-node forge.config.js files.
|
|
236
|
-
*
|
|
237
|
-
* Each node gets a config where:
|
|
238
|
-
* - Services it runs are defined with their full config (entry, type, etc.)
|
|
239
|
-
* - Services on OTHER nodes are defined as type: 'remote' with HTTP addresses
|
|
240
|
-
*
|
|
241
|
-
* @param {Object} manifest - Validated manifest
|
|
242
|
-
* @param {Object} serviceConfigs - Base service configs
|
|
243
|
-
* @param {Object} options
|
|
244
|
-
* @returns {Map<string, string>} nodeName → generated config content
|
|
245
|
-
*/
|
|
246
|
-
export function generateNodeConfigs(manifest, serviceConfigs, _options = {}) {
|
|
247
|
-
const configs = new Map();
|
|
248
|
-
const httpBasePort = manifest.httpBasePort ?? 4000;
|
|
249
|
-
const registryMode = manifest.registry ?? "multicast";
|
|
250
|
-
|
|
251
|
-
// Assign HTTP ports: edge services keep their port, others get sequential ports.
|
|
252
|
-
// Each service needs an HTTP port so remote machines can reach /__forge/invoke.
|
|
253
|
-
|
|
254
|
-
// Global port collision check BEFORE assignment
|
|
255
|
-
const globalPorts = new Map(); // "nodeName:port" → service name
|
|
256
|
-
for (const [nodeName, node] of Object.entries(manifest.nodes)) {
|
|
257
|
-
for (const svcName of node.services) {
|
|
258
|
-
const port = serviceConfigs[svcName]?.port;
|
|
259
|
-
if (port) {
|
|
260
|
-
const key = `${nodeName}:${port}`;
|
|
261
|
-
const existing = globalPorts.get(key);
|
|
262
|
-
if (existing) {
|
|
263
|
-
throw new Error(
|
|
264
|
-
`Port ${port} collision on node "${nodeName}": services "${existing}" and "${svcName}" both claim port ${port}`,
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
globalPorts.set(key, svcName);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const servicePorts = _assignPorts(manifest, serviceConfigs, httpBasePort);
|
|
273
|
-
|
|
274
|
-
// Final duplicate check per node
|
|
275
|
-
for (const [nodeName, node] of Object.entries(manifest.nodes)) {
|
|
276
|
-
const nodePorts = node.services.map((s) => servicePorts[s]?.find((e) => e.host === node.host)?.port).filter(Boolean);
|
|
277
|
-
const uniquePorts = new Set(nodePorts);
|
|
278
|
-
if (uniquePorts.size !== nodePorts.length) {
|
|
279
|
-
// M-DEPLOY-1: Include both service names and node name in collision message
|
|
280
|
-
const portToServices = {};
|
|
281
|
-
for (const svc of node.services) {
|
|
282
|
-
const port = servicePorts[svc]?.find((e) => e.host === node.host)?.port;
|
|
283
|
-
if (port) {
|
|
284
|
-
if (!portToServices[port]) portToServices[port] = [];
|
|
285
|
-
portToServices[port].push(svc);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
const dupeDescs = Object.entries(portToServices)
|
|
289
|
-
.filter(([, svcs]) => svcs.length > 1)
|
|
290
|
-
.map(([p, svcs]) => `port ${p} (services: ${svcs.join(", ")})`);
|
|
291
|
-
throw new Error(
|
|
292
|
-
`Port collision on node "${nodeName}": ${dupeDescs.join("; ")}. Note: ports can be reused across different nodes.`,
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
for (const [nodeName, node] of Object.entries(manifest.nodes)) {
|
|
298
|
-
const overrides = manifest.overrides?.[nodeName] ?? {};
|
|
299
|
-
|
|
300
|
-
let config = `// forge.config.js — Auto-generated for node: ${nodeName}
|
|
301
|
-
// Host: ${node.host} | Role: ${node.role ?? "general"}
|
|
302
|
-
// Generated by: forge deploy
|
|
303
|
-
// Do not edit manually — regenerate with: forge deploy --generate
|
|
304
|
-
|
|
305
|
-
import { defineServices } from 'threadforge';
|
|
306
|
-
|
|
307
|
-
export default defineServices({
|
|
308
|
-
|
|
309
|
-
// ── Local services (this node runs these) ──
|
|
310
|
-
`;
|
|
311
|
-
|
|
312
|
-
// Local services
|
|
313
|
-
for (const svcName of node.services) {
|
|
314
|
-
const svc = serviceConfigs[svcName];
|
|
315
|
-
if (!svc) {
|
|
316
|
-
throw new Error(`Node "${nodeName}" references service "${svcName}" which has no configuration`);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const override = overrides[svcName] ?? {};
|
|
320
|
-
const merged = { ...svc, ...override };
|
|
321
|
-
const assignedPort = servicePorts[svcName]?.find((e) => e.host === node.host)?.port;
|
|
322
|
-
|
|
323
|
-
config += `\n ${svcName}: {\n`;
|
|
324
|
-
config += ` entry: '${merged.entry}',\n`;
|
|
325
|
-
config += ` type: '${merged.type}',\n`;
|
|
326
|
-
|
|
327
|
-
// All services get an HTTP port in multi-machine mode
|
|
328
|
-
config += ` port: ${assignedPort},\n`;
|
|
329
|
-
|
|
330
|
-
if (merged.threads) {
|
|
331
|
-
config += ` threads: ${typeof merged.threads === "string" ? `'${merged.threads}'` : merged.threads},\n`;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if (merged.weight && merged.weight > 1) {
|
|
335
|
-
config += ` weight: ${merged.weight},\n`;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
if (merged.connects && merged.connects.length > 0) {
|
|
339
|
-
config += ` connects: [${merged.connects.map((c) => `'${c}'`).join(", ")}],\n`;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
if (merged.group) {
|
|
343
|
-
config += ` group: '${merged.group}',\n`;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (merged.websocket) {
|
|
347
|
-
config += ` websocket: ${JSON.stringify(merged.websocket)},\n`;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
if (merged.routing && merged.routing.strategy !== "round-robin") {
|
|
351
|
-
config += ` routing: ${JSON.stringify(merged.routing)},\n`;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
config += ` },\n`;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Remote services
|
|
358
|
-
const remoteServices = manifest.allServices.filter((s) => !node.services.includes(s));
|
|
359
|
-
|
|
360
|
-
if (remoteServices.length > 0) {
|
|
361
|
-
config += `\n // ── Remote services (on other machines, via HTTP) ──\n`;
|
|
362
|
-
|
|
363
|
-
for (const svcName of remoteServices) {
|
|
364
|
-
const remote = servicePorts[svcName]?.[0]; // Use first endpoint as primary
|
|
365
|
-
if (!remote) continue;
|
|
366
|
-
|
|
367
|
-
const targetNodes = manifest.serviceToNode[svcName];
|
|
368
|
-
const targetLabel = Array.isArray(targetNodes)
|
|
369
|
-
? targetNodes.map((n) => n.node).join(", ")
|
|
370
|
-
: (targetNodes?.node ?? "unknown");
|
|
371
|
-
config += `\n ${svcName}: {\n`;
|
|
372
|
-
config += ` type: 'remote',\n`;
|
|
373
|
-
config += ` address: 'http://${remote.host}:${remote.port}',`;
|
|
374
|
-
config += ` // ${targetLabel}\n`;
|
|
375
|
-
if (servicePorts[svcName]?.length > 1) {
|
|
376
|
-
config += ` endpoints: ${JSON.stringify(servicePorts[svcName])},\n`;
|
|
377
|
-
}
|
|
378
|
-
config += ` },\n`;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
config += `\n}, {\n`;
|
|
383
|
-
config += ` registryMode: '${registryMode}',\n`;
|
|
384
|
-
config += ` host: '${node.host}',\n`;
|
|
385
|
-
config += ` metricsPort: ${manifest.metricsPort ?? 9090},\n`;
|
|
386
|
-
config += `});\n`;
|
|
387
|
-
|
|
388
|
-
configs.set(nodeName, config);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
return configs;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
/**
|
|
395
|
-
* Generate Docker Compose for local development that simulates
|
|
396
|
-
* the multi-machine topology.
|
|
397
|
-
*/
|
|
398
|
-
export function generateDockerCompose(manifest, serviceConfigs) {
|
|
399
|
-
let compose = `# docker-compose.yml — Auto-generated by forge deploy
|
|
400
|
-
# Simulates multi-machine topology for local development
|
|
401
|
-
|
|
402
|
-
services:
|
|
403
|
-
`;
|
|
404
|
-
|
|
405
|
-
// Compute HTTP ports using the shared helper
|
|
406
|
-
const httpBasePort = manifest.httpBasePort ?? 4000;
|
|
407
|
-
const portMap = _assignPorts(manifest, serviceConfigs, httpBasePort);
|
|
408
|
-
|
|
409
|
-
// Build a flat nodeName:svc → port lookup for Docker Compose (keyed per-node)
|
|
410
|
-
const servicePorts = {};
|
|
411
|
-
for (const [nodeName, node] of Object.entries(manifest.nodes)) {
|
|
412
|
-
for (const svc of node.services) {
|
|
413
|
-
const entry = portMap[svc]?.find((e) => e.host === node.host);
|
|
414
|
-
if (entry) servicePorts[`${nodeName}:${svc}`] = entry.port;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Build a service→endpoint map using Docker hostnames instead of IPs
|
|
419
|
-
const serviceEndpointMap = {};
|
|
420
|
-
for (const [nn, nd] of Object.entries(manifest.nodes)) {
|
|
421
|
-
for (const svc of nd.services) {
|
|
422
|
-
const port = servicePorts[`${nn}:${svc}`];
|
|
423
|
-
if (port) {
|
|
424
|
-
if (!serviceEndpointMap[svc]) serviceEndpointMap[svc] = [];
|
|
425
|
-
serviceEndpointMap[svc].push({ host: nn, port: Number(port), remote: true });
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
for (const [nodeName, node] of Object.entries(manifest.nodes)) {
|
|
431
|
-
// Build FORGE_SERVICE_ENDPOINTS using Docker hostnames for this node (JSON format)
|
|
432
|
-
const endpointMap = {};
|
|
433
|
-
for (const [svc, endpoints] of Object.entries(serviceEndpointMap)) {
|
|
434
|
-
if (!node.services.includes(svc)) {
|
|
435
|
-
endpointMap[svc] = endpoints;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
const endpointsEnv = Object.keys(endpointMap).length > 0 ? JSON.stringify(endpointMap) : "";
|
|
439
|
-
// YAML single-quoted strings escape ' as '' (per YAML spec §7.3.2).
|
|
440
|
-
// The value is JSON (double-quoted keys/values) so single quotes are
|
|
441
|
-
// unlikely, but we handle them defensively for correctness.
|
|
442
|
-
const yamlSafeEndpoints = endpointsEnv.replace(/'/g, "''");
|
|
443
|
-
|
|
444
|
-
compose += `
|
|
445
|
-
${nodeName}:
|
|
446
|
-
build: .
|
|
447
|
-
hostname: ${nodeName}
|
|
448
|
-
restart: on-failure
|
|
449
|
-
env_file: ./.env
|
|
450
|
-
environment:
|
|
451
|
-
- FORGE_NODE=${nodeName}
|
|
452
|
-
- FORGE_HOST=${nodeName}
|
|
453
|
-
- FORGE_REGION=${manifest.region ?? "local"}${yamlSafeEndpoints ? `\n - FORGE_SERVICE_ENDPOINTS='${yamlSafeEndpoints}'` : ""}
|
|
454
|
-
volumes:
|
|
455
|
-
- ./deploy/${nodeName}/forge.config.js:/app/deploy/${nodeName}/forge.config.js:ro
|
|
456
|
-
working_dir: /app
|
|
457
|
-
command: node bin/forge.js start --config ./deploy/${nodeName}/forge.config.js
|
|
458
|
-
stop_grace_period: 35s
|
|
459
|
-
healthcheck:
|
|
460
|
-
test: ["CMD", "wget", "-q", "--spider", "http://localhost:9090/health"]
|
|
461
|
-
interval: 10s
|
|
462
|
-
timeout: 3s
|
|
463
|
-
retries: 3
|
|
464
|
-
start_period: 30s
|
|
465
|
-
logging:
|
|
466
|
-
driver: json-file
|
|
467
|
-
options:
|
|
468
|
-
max-size: "10m"
|
|
469
|
-
max-file: "3"
|
|
470
|
-
deploy:
|
|
471
|
-
resources:
|
|
472
|
-
limits:
|
|
473
|
-
cpus: "2"
|
|
474
|
-
memory: 2G
|
|
475
|
-
`;
|
|
476
|
-
|
|
477
|
-
// Expose HTTP ports for all services (edge + internal with assigned ports)
|
|
478
|
-
const ports = node.services
|
|
479
|
-
.map((svc) => ({
|
|
480
|
-
svc,
|
|
481
|
-
port: servicePorts[`${nodeName}:${svc}`],
|
|
482
|
-
type: serviceConfigs[svc]?.type ?? "internal",
|
|
483
|
-
}))
|
|
484
|
-
.filter((p) => p.port);
|
|
485
|
-
|
|
486
|
-
if (ports.length > 0) {
|
|
487
|
-
const edgePorts = ports.filter(p => p.type === "edge");
|
|
488
|
-
const internalPorts = ports.filter(p => p.type !== "edge");
|
|
489
|
-
if (edgePorts.length > 0) {
|
|
490
|
-
compose += ` ports:\n`;
|
|
491
|
-
for (const { svc, port } of edgePorts) {
|
|
492
|
-
compose += ` - "${port}:${port}" # ${svc} HTTP\n`;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
if (internalPorts.length > 0) {
|
|
496
|
-
compose += ` expose:\n`;
|
|
497
|
-
for (const { svc, port } of internalPorts) {
|
|
498
|
-
compose += ` - "${port}" # ${svc} HTTP (internal)\n`;
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
compose += ` networks:\n`;
|
|
504
|
-
compose += ` - forge-net\n`;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
compose += `
|
|
508
|
-
networks:
|
|
509
|
-
forge-net:
|
|
510
|
-
driver: bridge
|
|
511
|
-
`;
|
|
512
|
-
|
|
513
|
-
return compose;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/**
|
|
517
|
-
* Generate a systemd service unit file for a node.
|
|
518
|
-
*/
|
|
519
|
-
export function generateSystemdUnit(nodeName, node, manifest) {
|
|
520
|
-
const endpoints = node.endpoints || {};
|
|
521
|
-
const envFilePath = `/etc/threadforge/${nodeName}.env`;
|
|
522
|
-
const envFileContent = `FORGE_SERVICE_ENDPOINTS=${JSON.stringify(endpoints)}`;
|
|
523
|
-
|
|
524
|
-
return `[Unit]
|
|
525
|
-
Description=ThreadForge - ${nodeName}
|
|
526
|
-
After=network-online.target
|
|
527
|
-
Wants=network-online.target
|
|
528
|
-
StartLimitIntervalSec=300
|
|
529
|
-
StartLimitBurst=5
|
|
530
|
-
|
|
531
|
-
[Service]
|
|
532
|
-
Type=simple
|
|
533
|
-
User=forge
|
|
534
|
-
Group=forge
|
|
535
|
-
WorkingDirectory=/opt/forge
|
|
536
|
-
ExecStart=/usr/bin/node bin/forge.js start --config ./deploy/${nodeName}/forge.config.js
|
|
537
|
-
ExecStop=/bin/kill -SIGTERM $MAINPID
|
|
538
|
-
Restart=always
|
|
539
|
-
RestartSec=5
|
|
540
|
-
TimeoutStopSec=30
|
|
541
|
-
Environment=NODE_ENV=production
|
|
542
|
-
Environment=FORGE_NODE=${nodeName}
|
|
543
|
-
Environment=FORGE_HOST=${node.host}
|
|
544
|
-
Environment=FORGE_REGION=${manifest.region ?? "production"}
|
|
545
|
-
EnvironmentFile=${envFilePath}
|
|
546
|
-
EnvironmentFile=-/etc/threadforge/secrets/${nodeName}.env
|
|
547
|
-
|
|
548
|
-
# Security hardening
|
|
549
|
-
NoNewPrivileges=true
|
|
550
|
-
PrivateTmp=true
|
|
551
|
-
ProtectSystem=strict
|
|
552
|
-
ReadWritePaths=/opt/forge
|
|
553
|
-
|
|
554
|
-
# Resource limits
|
|
555
|
-
LimitNOFILE=65536
|
|
556
|
-
MemoryMax=2G
|
|
557
|
-
|
|
558
|
-
[Install]
|
|
559
|
-
WantedBy=multi-user.target
|
|
560
|
-
`;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
/**
|
|
564
|
-
* Generate the environment file content for a systemd unit.
|
|
565
|
-
* Write this to /etc/threadforge/<nodeName>.env
|
|
566
|
-
*
|
|
567
|
-
* @param {string} nodeName
|
|
568
|
-
* @param {Object} node
|
|
569
|
-
* @returns {{ path: string, content: string }}
|
|
570
|
-
*/
|
|
571
|
-
export function generateSystemdEnvFile(nodeName, node) {
|
|
572
|
-
const endpoints = node.endpoints || {};
|
|
573
|
-
return {
|
|
574
|
-
path: `/etc/threadforge/${nodeName}.env`,
|
|
575
|
-
content: `FORGE_SERVICE_ENDPOINTS='${JSON.stringify(endpoints).replace(/'/g, "'\\''")}'\n`,
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
/**
|
|
580
|
-
* Generate a deployment script that syncs code and restarts services.
|
|
581
|
-
*/
|
|
582
|
-
export function generateDeployScript(manifest) {
|
|
583
|
-
let script = `#!/bin/bash
|
|
584
|
-
# deploy.sh — Auto-generated by forge deploy
|
|
585
|
-
# Usage: ./deploy.sh [node-name] Deploy to a specific node
|
|
586
|
-
# ./deploy.sh Deploy to all nodes
|
|
587
|
-
# ./deploy.sh --rolling Rolling deploy (one at a time)
|
|
588
|
-
# ./deploy.sh --rollback <node> Rollback a node to latest backup
|
|
589
|
-
|
|
590
|
-
set -euo pipefail
|
|
591
|
-
|
|
592
|
-
DEPLOY_DIR="/opt/forge"
|
|
593
|
-
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
594
|
-
BACKUP_BASE="/opt/forge/backups"
|
|
595
|
-
|
|
596
|
-
# ── Rollback helper ──────────────────────────────────────────
|
|
597
|
-
rollback() {
|
|
598
|
-
local node_host="\$1"
|
|
599
|
-
local node_name="\$2"
|
|
600
|
-
echo "Rolling back \${node_name} (\${node_host})..."
|
|
601
|
-
local latest
|
|
602
|
-
latest=\$(ssh "forge@\${node_host}" "ls -1t \\"\${BACKUP_BASE}\\" 2>/dev/null | head -1")
|
|
603
|
-
if [ -z "\$latest" ]; then
|
|
604
|
-
echo " No backup found for \${node_name}"
|
|
605
|
-
return 1
|
|
606
|
-
fi
|
|
607
|
-
ssh "forge@\${node_host}" "rsync -a \\"\${BACKUP_BASE}/\${latest}/\\" \\"\${DEPLOY_DIR}/\\""
|
|
608
|
-
ssh "forge@\${node_host}" "sudo systemctl restart forge-\${node_name}"
|
|
609
|
-
echo " Rolled back to backup: \${latest}"
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
`;
|
|
613
|
-
|
|
614
|
-
// Validate node names to prevent shell injection
|
|
615
|
-
for (const nodeName of Object.keys(manifest.nodes)) {
|
|
616
|
-
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(nodeName)) {
|
|
617
|
-
throw new Error(`Invalid node name "${nodeName}": must contain only alphanumeric characters, hyphens, and underscores`);
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
// Node deployment functions
|
|
622
|
-
for (const [nodeName, node] of Object.entries(manifest.nodes)) {
|
|
623
|
-
script += `deploy_${nodeName.replace(/-/g, "_")}() {
|
|
624
|
-
echo "Deploying to ${nodeName} (${node.host})..."
|
|
625
|
-
|
|
626
|
-
# Create timestamped backup of current deployment
|
|
627
|
-
local BACKUP_DIR="\${BACKUP_BASE}/\$(date +%Y%m%d-%H%M%S)"
|
|
628
|
-
ssh "forge@${node.host}" "mkdir -p \\"\${BACKUP_DIR}\\" && rsync -a \\"\${DEPLOY_DIR}/\\" \\"\${BACKUP_DIR}/\\" 2>/dev/null || true"
|
|
629
|
-
|
|
630
|
-
# Sync code (excluding node_modules, .git)
|
|
631
|
-
rsync -azP --delete \\
|
|
632
|
-
--exclude 'node_modules' \\
|
|
633
|
-
--exclude '.git' \\
|
|
634
|
-
--exclude 'deploy/*/node_modules' \\
|
|
635
|
-
"\${PROJECT_DIR}/" "forge@${node.host}:\${DEPLOY_DIR}/"
|
|
636
|
-
|
|
637
|
-
# Install dependencies on remote
|
|
638
|
-
ssh "forge@${node.host}" "cd \\"\${DEPLOY_DIR}\\" && npm ci --omit=dev"
|
|
639
|
-
|
|
640
|
-
# Generate route manifests if needed
|
|
641
|
-
ssh "forge@${node.host}" "cd \\"\${DEPLOY_DIR}\\" && node bin/forge.js generate --config ./deploy/${nodeName}/forge.config.js" 2>/dev/null || true
|
|
642
|
-
|
|
643
|
-
# Restart service
|
|
644
|
-
ssh "forge@${node.host}" "sudo systemctl restart forge-${nodeName}"
|
|
645
|
-
|
|
646
|
-
# Wait for health check
|
|
647
|
-
echo -n " Waiting for health..."
|
|
648
|
-
for i in \$(seq 1 30); do
|
|
649
|
-
if ssh "forge@${node.host}" "curl -sf http://localhost:9090/health" >/dev/null 2>&1; then
|
|
650
|
-
echo " \u2713 healthy"
|
|
651
|
-
return 0
|
|
652
|
-
fi
|
|
653
|
-
echo -n "."
|
|
654
|
-
sleep 1
|
|
655
|
-
done
|
|
656
|
-
echo " \u2717 FAILED — rolling back..."
|
|
657
|
-
rollback "${node.host}" "${nodeName}"
|
|
658
|
-
return 1
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
`;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// Main dispatch
|
|
665
|
-
const nodeNames = Object.keys(manifest.nodes);
|
|
666
|
-
const funcNames = nodeNames.map((n) => `deploy_${n.replace(/-/g, "_")}`);
|
|
667
|
-
|
|
668
|
-
// Build a node→host map for rollback dispatch
|
|
669
|
-
const nodeHostEntries = Object.entries(manifest.nodes).map(
|
|
670
|
-
([n, nd]) => ` "${n}") rollback "${nd.host}" "${n}" ;;`,
|
|
671
|
-
);
|
|
672
|
-
|
|
673
|
-
script += `# \u2500\u2500 Main \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
674
|
-
|
|
675
|
-
if [ "\${1:-}" = "--rollback" ]; then
|
|
676
|
-
if [ -z "\${2:-}" ]; then
|
|
677
|
-
echo "Usage: ./deploy.sh --rollback <node-name>"
|
|
678
|
-
echo "Available: ${nodeNames.join(", ")}"
|
|
679
|
-
exit 1
|
|
680
|
-
fi
|
|
681
|
-
[[ "$2" =~ ^[a-zA-Z0-9_-]+$ ]] || { echo "Invalid node name: $2"; exit 1; }
|
|
682
|
-
case "$2" in
|
|
683
|
-
${nodeHostEntries.join("\n")}
|
|
684
|
-
*) echo "Unknown node: $2"; echo "Available: ${nodeNames.join(", ")}"; exit 1 ;;
|
|
685
|
-
esac
|
|
686
|
-
elif [ "\${1:-}" = "--rolling" ]; then
|
|
687
|
-
echo "Rolling deploy to ${nodeNames.length} nodes..."
|
|
688
|
-
for fn in ${funcNames.join(" ")}; do
|
|
689
|
-
$fn
|
|
690
|
-
echo " Draining for 10s..."
|
|
691
|
-
sleep 10
|
|
692
|
-
done
|
|
693
|
-
elif [ -n "\${1:-}" ]; then
|
|
694
|
-
# Validate node name to prevent command injection
|
|
695
|
-
[[ "$1" =~ ^[a-zA-Z0-9_-]+$ ]] || { echo "Invalid node name: $1"; exit 1; }
|
|
696
|
-
fn="deploy_\${1//-/_}"
|
|
697
|
-
if type "$fn" &>/dev/null; then
|
|
698
|
-
$fn
|
|
699
|
-
else
|
|
700
|
-
echo "Unknown node: $1"
|
|
701
|
-
echo "Available: ${nodeNames.join(", ")}"
|
|
702
|
-
exit 1
|
|
703
|
-
fi
|
|
704
|
-
else
|
|
705
|
-
echo "Deploying to all ${nodeNames.length} nodes in parallel..."
|
|
706
|
-
PIDS=()
|
|
707
|
-
${funcNames.map((fn) => `${fn} & PIDS+=($!)`).join("\n ")}
|
|
708
|
-
FAIL=0
|
|
709
|
-
for pid in "\${PIDS[@]}"; do
|
|
710
|
-
wait $pid || FAIL=1
|
|
711
|
-
done
|
|
712
|
-
if [ $FAIL -ne 0 ]; then
|
|
713
|
-
echo "ERROR: One or more deployments failed"
|
|
714
|
-
exit 1
|
|
715
|
-
fi
|
|
716
|
-
fi
|
|
717
|
-
|
|
718
|
-
echo "Deploy complete."
|
|
719
|
-
`;
|
|
720
|
-
|
|
721
|
-
return script;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
/**
|
|
725
|
-
* Generate a Dockerfile for the deployment.
|
|
726
|
-
*
|
|
727
|
-
* @param {Object} manifest - Validated manifest
|
|
728
|
-
* @param {Object} serviceConfigs - Base service configs
|
|
729
|
-
* @returns {string} Dockerfile content
|
|
730
|
-
*/
|
|
731
|
-
export function generateDockerfile(manifest, serviceConfigs) {
|
|
732
|
-
// Find the first edge service port, or fall back to httpBasePort
|
|
733
|
-
let httpPort = manifest.httpBasePort ?? 4000;
|
|
734
|
-
for (const svc of Object.values(serviceConfigs)) {
|
|
735
|
-
if (svc.type === "edge" && svc.port) {
|
|
736
|
-
httpPort = svc.port;
|
|
737
|
-
break;
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
return `FROM node:20.11-slim AS base
|
|
742
|
-
RUN apt-get update && apt-get install -y tini && rm -rf /var/lib/apt/lists/*
|
|
743
|
-
WORKDIR /app
|
|
744
|
-
COPY package*.json ./
|
|
745
|
-
RUN npm ci --omit=dev
|
|
746
|
-
COPY . .
|
|
747
|
-
ENV NODE_ENV=production
|
|
748
|
-
EXPOSE ${httpPort}
|
|
749
|
-
ENTRYPOINT ["/usr/bin/tini", "--"]
|
|
750
|
-
CMD ["node", "bin/forge.js", "start"]
|
|
751
|
-
`;
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
/**
|
|
755
|
-
* Generate a .dockerignore file.
|
|
756
|
-
*
|
|
757
|
-
* @returns {string} .dockerignore content
|
|
758
|
-
*/
|
|
759
|
-
export function generateDockerignore() {
|
|
760
|
-
return `.git
|
|
761
|
-
node_modules
|
|
762
|
-
forgeproxy/target
|
|
763
|
-
.env
|
|
764
|
-
secrets/
|
|
765
|
-
*.log
|
|
766
|
-
deploy/
|
|
767
|
-
`;
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
/**
|
|
771
|
-
* Generate all deployment artifacts from a manifest.
|
|
772
|
-
*
|
|
773
|
-
* @param {Object} manifest
|
|
774
|
-
* @param {Object} serviceConfigs
|
|
775
|
-
* @param {string} outputDir
|
|
776
|
-
*/
|
|
777
|
-
export function generateAll(manifest, serviceConfigs, outputDir) {
|
|
778
|
-
const validated = loadManifest(manifest, serviceConfigs);
|
|
779
|
-
|
|
780
|
-
// Per-node configs
|
|
781
|
-
const nodeConfigs = generateNodeConfigs(validated, serviceConfigs);
|
|
782
|
-
for (const [nodeName, config] of nodeConfigs) {
|
|
783
|
-
const dir = path.join(outputDir, nodeName);
|
|
784
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
785
|
-
fs.writeFileSync(path.join(dir, "forge.config.js"), config);
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
// Docker Compose
|
|
789
|
-
const compose = generateDockerCompose(validated, serviceConfigs);
|
|
790
|
-
fs.writeFileSync(path.join(outputDir, "docker-compose.yml"), compose);
|
|
791
|
-
|
|
792
|
-
// Compute endpoint map for systemd env files using shared helper
|
|
793
|
-
const httpBasePort = validated.httpBasePort ?? 4000;
|
|
794
|
-
const servicePorts = _assignPorts(validated, serviceConfigs, httpBasePort);
|
|
795
|
-
|
|
796
|
-
// Systemd units + env files
|
|
797
|
-
for (const [nodeName, node] of Object.entries(validated.nodes)) {
|
|
798
|
-
// Build endpoint map for this node: remote services → host:port
|
|
799
|
-
const endpointMap = {};
|
|
800
|
-
for (const [svcName, endpoints] of Object.entries(servicePorts)) {
|
|
801
|
-
if (!node.services.includes(svcName)) {
|
|
802
|
-
endpointMap[svcName] = endpoints.map(e => ({ host: e.host, port: Number(e.port), remote: true }));
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
const nodeWithEndpoints = { ...node, endpoints: endpointMap };
|
|
806
|
-
|
|
807
|
-
const unit = generateSystemdUnit(nodeName, nodeWithEndpoints, validated);
|
|
808
|
-
fs.writeFileSync(path.join(outputDir, `forge-${nodeName}.service`), unit);
|
|
809
|
-
|
|
810
|
-
const envFile = generateSystemdEnvFile(nodeName, nodeWithEndpoints);
|
|
811
|
-
fs.writeFileSync(path.join(outputDir, `${nodeName}.env`), envFile.content);
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
// Deploy script
|
|
815
|
-
const deployScript = generateDeployScript(validated);
|
|
816
|
-
const scriptPath = path.join(outputDir, "deploy.sh");
|
|
817
|
-
fs.writeFileSync(scriptPath, deployScript);
|
|
818
|
-
fs.chmodSync(scriptPath, "755");
|
|
819
|
-
|
|
820
|
-
// Nginx config — routes directly to edge services, no gateway
|
|
821
|
-
const nginxServices = buildNginxServiceMap(validated, serviceConfigs);
|
|
822
|
-
const websocketRoutes = buildNginxWebSocketRoutes(serviceConfigs, nginxServices, {
|
|
823
|
-
websockets: manifest.websockets ?? false,
|
|
824
|
-
websocketPaths: manifest.websocketPaths ?? ["/ws"],
|
|
825
|
-
});
|
|
826
|
-
const nginxConfig = generateNginxConfig({
|
|
827
|
-
domain: manifest.domain ?? "localhost",
|
|
828
|
-
services: nginxServices,
|
|
829
|
-
ssl: manifest.ssl,
|
|
830
|
-
rateLimits: manifest.rateLimits ?? {},
|
|
831
|
-
staticDir: manifest.staticDir,
|
|
832
|
-
websocketRoutes,
|
|
833
|
-
// Backward compatibility for older manifests that don't define service.websocket
|
|
834
|
-
websockets: manifest.websockets ?? false,
|
|
835
|
-
websocketPaths: manifest.websocketPaths ?? ["/ws"],
|
|
836
|
-
maxBodySize: manifest.maxBodySize ?? 10,
|
|
837
|
-
});
|
|
838
|
-
fs.writeFileSync(path.join(outputDir, "nginx.conf"), nginxConfig);
|
|
839
|
-
|
|
840
|
-
// C1: Generate Dockerfile
|
|
841
|
-
const dockerfile = generateDockerfile(manifest, serviceConfigs);
|
|
842
|
-
fs.writeFileSync(path.join(outputDir, "Dockerfile"), dockerfile);
|
|
843
|
-
|
|
844
|
-
// H4: Generate .dockerignore
|
|
845
|
-
const dockerignore = generateDockerignore();
|
|
846
|
-
fs.writeFileSync(path.join(outputDir, ".dockerignore"), dockerignore);
|
|
847
|
-
|
|
848
|
-
// C-DEPLOY-1: Generate .env.example template for secrets management
|
|
849
|
-
const envExample = `# ThreadForge Deployment Secrets — DO NOT COMMIT TO VERSION CONTROL
|
|
850
|
-
# Copy to .env and fill in values
|
|
851
|
-
DATABASE_URL=
|
|
852
|
-
REDIS_URL=
|
|
853
|
-
JWT_SECRET=CHANGE_ME_BEFORE_DEPLOY
|
|
854
|
-
NODE_ENV=production
|
|
855
|
-
FORGE_METRICS_PORT=9090
|
|
856
|
-
FORGE_METRICS_BIND=127.0.0.1
|
|
857
|
-
FORGE_CLUSTER_SECRET=
|
|
858
|
-
FORGE_LOG_LEVEL=info
|
|
859
|
-
`;
|
|
860
|
-
fs.writeFileSync(path.join(outputDir, ".env.example"), envExample);
|
|
861
|
-
|
|
862
|
-
// Generate .gitignore to protect .env from accidental commits
|
|
863
|
-
const gitignore = `# Secrets — never commit
|
|
864
|
-
.env
|
|
865
|
-
`;
|
|
866
|
-
fs.writeFileSync(path.join(outputDir, ".gitignore"), gitignore);
|
|
867
|
-
|
|
868
|
-
return {
|
|
869
|
-
nodes: [...nodeConfigs.keys()],
|
|
870
|
-
files: [
|
|
871
|
-
...[...nodeConfigs.keys()].map((n) => `${n}/forge.config.js`),
|
|
872
|
-
"docker-compose.yml",
|
|
873
|
-
"nginx.conf",
|
|
874
|
-
...Object.keys(validated.nodes).map((n) => `forge-${n}.service`),
|
|
875
|
-
...Object.keys(validated.nodes).map((n) => `${n}.env`),
|
|
876
|
-
"deploy.sh",
|
|
877
|
-
"Dockerfile",
|
|
878
|
-
".dockerignore",
|
|
879
|
-
".env.example",
|
|
880
|
-
".gitignore",
|
|
881
|
-
],
|
|
882
|
-
};
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
/**
|
|
886
|
-
* Generate all platform deployment artifacts.
|
|
887
|
-
*
|
|
888
|
-
* @param {Object} platformConfig - The platform block from the user config
|
|
889
|
-
* @param {Object} services - Flat namespaced service map from resolveHostConfig
|
|
890
|
-
* @param {Object} hostMeta - From resolveHostConfig().hostMeta
|
|
891
|
-
* @param {string} outputDir - Directory to write artifacts to
|
|
892
|
-
* @param {Object} [options]
|
|
893
|
-
* @param {Object} [options.sites] - Resolved site map from resolveHostConfig().sites
|
|
894
|
-
* @param {{sites?: Array, mounts?: Object}} [options.frontendManifest] - Output from forge build
|
|
895
|
-
* @returns {Object} { files: string[] }
|
|
896
|
-
*/
|
|
897
|
-
export function generatePlatformAll(platformConfig, services, hostMeta, outputDir, options = {}) {
|
|
898
|
-
const deployDir = path.join(outputDir, "deploy");
|
|
899
|
-
const routesDir = path.join(outputDir, "routes");
|
|
900
|
-
fs.mkdirSync(deployDir, { recursive: true });
|
|
901
|
-
fs.mkdirSync(routesDir, { recursive: true });
|
|
902
|
-
|
|
903
|
-
const files = [];
|
|
904
|
-
const sites = options.sites ?? {};
|
|
905
|
-
|
|
906
|
-
/** @type {Record<string, string>} */
|
|
907
|
-
const builtOutDirBySite = {};
|
|
908
|
-
if (Array.isArray(options.frontendManifest?.sites)) {
|
|
909
|
-
for (const site of options.frontendManifest.sites) {
|
|
910
|
-
if (site?.siteId && site?.outDir) {
|
|
911
|
-
builtOutDirBySite[site.siteId] = site.outDir;
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
/** @type {Record<string, any[]>} */
|
|
917
|
-
const mountsFromManifest = {};
|
|
918
|
-
if (options.frontendManifest?.mounts && typeof options.frontendManifest.mounts === "object") {
|
|
919
|
-
for (const [siteId, mounts] of Object.entries(options.frontendManifest.mounts)) {
|
|
920
|
-
if (Array.isArray(mounts)) mountsFromManifest[siteId] = mounts;
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
const apps = {};
|
|
925
|
-
for (const [appId, app] of Object.entries(platformConfig.apps ?? {})) {
|
|
926
|
-
const site = sites?.[appId];
|
|
927
|
-
const frontend = site?.frontend ? { ...site.frontend } : (app.frontend ? { ...app.frontend } : null);
|
|
928
|
-
if (frontend && builtOutDirBySite[appId]) {
|
|
929
|
-
frontend.outDir = builtOutDirBySite[appId];
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
const staticMounts = mountsFromManifest[appId]
|
|
933
|
-
? mountsFromManifest[appId]
|
|
934
|
-
: (() => {
|
|
935
|
-
if (frontend) {
|
|
936
|
-
return [{
|
|
937
|
-
siteId: appId,
|
|
938
|
-
domains: site?.domains ?? resolveAppDomains(app),
|
|
939
|
-
basePath: frontend.basePath ?? "/",
|
|
940
|
-
dir: frontend.outDir,
|
|
941
|
-
spaFallback: frontend.spaFallback ?? true,
|
|
942
|
-
cachePolicy: "short",
|
|
943
|
-
}];
|
|
944
|
-
}
|
|
945
|
-
const legacyStatic = site?.staticDir ?? app.static ?? null;
|
|
946
|
-
if (!legacyStatic) return [];
|
|
947
|
-
return [{
|
|
948
|
-
siteId: appId,
|
|
949
|
-
domains: site?.domains ?? resolveAppDomains(app),
|
|
950
|
-
basePath: "/static",
|
|
951
|
-
dir: legacyStatic,
|
|
952
|
-
spaFallback: false,
|
|
953
|
-
cachePolicy: "short",
|
|
954
|
-
}];
|
|
955
|
-
})();
|
|
956
|
-
|
|
957
|
-
apps[appId] = {
|
|
958
|
-
domains: resolveAppDomains(app),
|
|
959
|
-
ssl: app.ssl ?? null,
|
|
960
|
-
static: site?.staticDir ?? app.static ?? null,
|
|
961
|
-
frontend,
|
|
962
|
-
staticMounts,
|
|
963
|
-
services: app.services ?? [],
|
|
964
|
-
};
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
// Generate platform nginx config
|
|
968
|
-
const nginxConfig = generatePlatformNginxConfig({
|
|
969
|
-
apps,
|
|
970
|
-
services,
|
|
971
|
-
hostMeta,
|
|
972
|
-
defaultSsl: platformConfig.ssl ?? null,
|
|
973
|
-
maxBodySize: platformConfig.maxBodySize ?? 10,
|
|
974
|
-
});
|
|
975
|
-
fs.writeFileSync(path.join(deployDir, "nginx.conf"), nginxConfig);
|
|
976
|
-
files.push("deploy/nginx.conf");
|
|
977
|
-
|
|
978
|
-
// Generate platform.yaml manifest
|
|
979
|
-
const manifest = generatePlatformManifest(platformConfig, hostMeta, sites);
|
|
980
|
-
fs.writeFileSync(path.join(routesDir, "platform.yaml"), manifest);
|
|
981
|
-
files.push("routes/platform.yaml");
|
|
982
|
-
|
|
983
|
-
return { files };
|
|
984
|
-
}
|