@rip-lang/server 1.3.115 → 1.3.117
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 +435 -622
- package/api.rip +4 -4
- package/control/cli.rip +221 -1
- package/control/control.rip +9 -0
- package/control/lifecycle.rip +6 -1
- package/control/watchers.rip +10 -0
- package/control/workers.rip +9 -5
- package/default.rip +3 -1
- package/docs/READ_VALIDATORS.md +656 -0
- package/docs/edge/CONFIG_LIFECYCLE.md +70 -32
- package/docs/edge/CONTRACTS.md +60 -69
- package/docs/edge/EDGEFILE_CONTRACT.md +258 -29
- package/docs/edge/M0B_REVIEW_NOTES.md +5 -5
- package/edge/config.rip +584 -52
- package/edge/forwarding.rip +6 -2
- package/edge/metrics.rip +19 -1
- package/edge/registry.rip +29 -3
- package/edge/router.rip +138 -0
- package/edge/runtime.rip +98 -0
- package/edge/static.rip +69 -0
- package/edge/tls.rip +23 -0
- package/edge/upstream.rip +272 -0
- package/edge/verify.rip +73 -0
- package/middleware.rip +3 -3
- package/package.json +2 -2
- package/server.rip +775 -393
- package/tests/control.rip +18 -0
- package/tests/edgefile.rip +165 -0
- package/tests/metrics.rip +16 -0
- package/tests/proxy.rip +22 -1
- package/tests/registry.rip +27 -0
- package/tests/router.rip +101 -0
- package/tests/runtime_entrypoints.rip +16 -0
- package/tests/servers.rip +262 -0
- package/tests/static.rip +64 -0
- package/tests/streams_clienthello.rip +108 -0
- package/tests/streams_index.rip +53 -0
- package/tests/streams_pipe.rip +70 -0
- package/tests/streams_router.rip +39 -0
- package/tests/streams_runtime.rip +38 -0
- package/tests/streams_upstream.rip +34 -0
- package/tests/upstream.rip +191 -0
- package/tests/verify.rip +148 -0
- package/tests/watchers.rip +15 -0
package/edge/config.rip
CHANGED
|
@@ -1,75 +1,607 @@
|
|
|
1
1
|
# ==============================================================================
|
|
2
|
-
# edge/config.rip —
|
|
2
|
+
# edge/config.rip — legacy config.rip and Edgefile.rip loading/validation
|
|
3
3
|
# ==============================================================================
|
|
4
4
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# export default
|
|
10
|
-
# apps:
|
|
11
|
-
# main:
|
|
12
|
-
# entry: './index.rip'
|
|
13
|
-
# hosts: ['example.com', 'www.example.com']
|
|
14
|
-
# workers: 4
|
|
15
|
-
# api:
|
|
16
|
-
# entry: './api/index.rip'
|
|
17
|
-
# hosts: ['api.example.com']
|
|
18
|
-
# workers: 2
|
|
19
|
-
# maxQueue: 1024
|
|
20
|
-
|
|
21
|
-
import { existsSync } from 'node:fs'
|
|
22
|
-
import { join, dirname, resolve } from 'node:path'
|
|
5
|
+
# config.rip remains the legacy app-registry config.
|
|
6
|
+
# Edgefile.rip is the stricter edge config surface for the pure Bun edge work.
|
|
7
|
+
# ==============================================================================
|
|
23
8
|
|
|
24
|
-
|
|
9
|
+
import { existsSync, statSync } from 'node:fs'
|
|
10
|
+
import { join, dirname, resolve, basename } from 'node:path'
|
|
11
|
+
import { normalizeStreamUpstreams, normalizeStreams, parseHostPort } from '../streams/config.rip'
|
|
12
|
+
|
|
13
|
+
EDGEFILE_NAME = 'Edgefile.rip'
|
|
14
|
+
LEGACY_CONFIG_NAME = 'config.rip'
|
|
15
|
+
|
|
16
|
+
isPlainObject = (value) ->
|
|
17
|
+
value? and typeof value is 'object' and not Array.isArray(value)
|
|
18
|
+
|
|
19
|
+
toArray = (value) ->
|
|
20
|
+
if Array.isArray(value) then value else []
|
|
21
|
+
|
|
22
|
+
pushError = (errors, code, path, message, hint = null) ->
|
|
23
|
+
errors.push({ code, path, message, hint })
|
|
24
|
+
|
|
25
|
+
validationError = (label, errors) ->
|
|
26
|
+
primary = errors[0]
|
|
27
|
+
err = new Error(primary?.message or "#{label} is invalid")
|
|
28
|
+
err.code = 'RIP_CONFIG_INVALID'
|
|
29
|
+
err.validationErrors = errors
|
|
30
|
+
err.label = label
|
|
31
|
+
err
|
|
32
|
+
|
|
33
|
+
export formatConfigErrors = (label, errors) ->
|
|
34
|
+
lines = ["rip-server: #{label} validation failed (#{errors.length} error#{if errors.length is 1 then '' else 's'})"]
|
|
35
|
+
for err in errors
|
|
36
|
+
line = "- [#{err.code}] #{err.path}: #{err.message}"
|
|
37
|
+
line += " Hint: #{err.hint}" if err.hint
|
|
38
|
+
lines.push(line)
|
|
39
|
+
lines.join('\n')
|
|
40
|
+
|
|
41
|
+
moduleObject = (configPath, label) ->
|
|
25
42
|
return null unless existsSync(configPath)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
43
|
+
mod = import!(configPath)
|
|
44
|
+
await Promise.resolve()
|
|
45
|
+
config = mod?.default or mod
|
|
46
|
+
if config?.then
|
|
47
|
+
throw validationError(label, [
|
|
48
|
+
{
|
|
49
|
+
code: 'E_CONFIG_ASYNC'
|
|
50
|
+
path: '$'
|
|
51
|
+
message: "#{label} must export a plain object, not a promise"
|
|
52
|
+
hint: "Export `default` as a synchronous object literal."
|
|
53
|
+
}
|
|
54
|
+
])
|
|
55
|
+
unless isPlainObject(config)
|
|
56
|
+
throw validationError(label, [
|
|
57
|
+
{
|
|
58
|
+
code: 'E_CONFIG_TYPE'
|
|
59
|
+
path: '$'
|
|
60
|
+
message: "#{label} must export a plain object"
|
|
61
|
+
hint: "Use `export default { ... }`."
|
|
62
|
+
}
|
|
63
|
+
])
|
|
64
|
+
config
|
|
35
65
|
|
|
36
|
-
|
|
37
|
-
|
|
66
|
+
normalizeHosts = (value, path, errors) ->
|
|
67
|
+
hosts = []
|
|
68
|
+
unless value?
|
|
69
|
+
return hosts
|
|
70
|
+
unless Array.isArray(value)
|
|
71
|
+
pushError(errors, 'E_HOSTS_TYPE', path, 'hosts must be an array of strings', "Use `hosts: ['example.com']`.")
|
|
72
|
+
return hosts
|
|
73
|
+
for host, idx in value
|
|
74
|
+
unless typeof host is 'string' and host.trim()
|
|
75
|
+
pushError(errors, 'E_HOST_TYPE', "#{path}[#{idx}]", 'host must be a non-empty string', "Use `'api.example.com'`, `'*.example.com'`, or `'*'`.")
|
|
76
|
+
continue
|
|
77
|
+
normalized = host.trim().toLowerCase()
|
|
78
|
+
if normalized.startsWith('*.')
|
|
79
|
+
base = normalized.slice(2)
|
|
80
|
+
labels = base.split('.').filter(Boolean)
|
|
81
|
+
if labels.length < 2
|
|
82
|
+
pushError(errors, 'E_HOST_WILDCARD_BASE', "#{path}[#{idx}]", 'wildcard hosts must have a base domain with at least two labels', "Use `'*.example.com'`, not `'*.com'`.")
|
|
83
|
+
continue
|
|
84
|
+
hosts.push(normalized)
|
|
85
|
+
hosts
|
|
86
|
+
|
|
87
|
+
normalizeEnv = (value, path, errors) ->
|
|
88
|
+
return {} unless value?
|
|
89
|
+
unless isPlainObject(value)
|
|
90
|
+
pushError(errors, 'E_ENV_TYPE', path, 'env must be an object', "Use `env: { FOO: 'bar' }`.")
|
|
91
|
+
return {}
|
|
92
|
+
out = {}
|
|
93
|
+
for key, val of value
|
|
94
|
+
unless ['string', 'number', 'boolean'].includes(typeof val)
|
|
95
|
+
pushError(errors, 'E_ENV_VALUE', "#{path}.#{key}", 'env values must be strings, numbers, or booleans', "Convert the value to a string if needed.")
|
|
96
|
+
continue
|
|
97
|
+
out[key] = String(val)
|
|
98
|
+
out
|
|
99
|
+
|
|
100
|
+
normalizePositiveInt = (value, fallback, path, errors, label) ->
|
|
101
|
+
return fallback unless value?
|
|
102
|
+
n = parseInt(String(value))
|
|
103
|
+
unless Number.isFinite(n) and n >= 0
|
|
104
|
+
fieldName = path.split('.').slice(-1)[0]
|
|
105
|
+
pushError(errors, 'E_NUMBER_TYPE', path, "#{label} must be a non-negative integer", "Set #{fieldName}: #{fallback} or another whole number.")
|
|
106
|
+
return fallback
|
|
107
|
+
n
|
|
108
|
+
|
|
109
|
+
normalizeAppConfigInto = (appId, appConfig, baseDir, path, errors) ->
|
|
110
|
+
unless isPlainObject(appConfig)
|
|
111
|
+
pushError(errors, 'E_APP_TYPE', path, 'app config must be an object', "Use #{appId}: { entry: './index.rip' }.")
|
|
112
|
+
appConfig = {}
|
|
113
|
+
entry = if typeof appConfig.entry is 'string' and appConfig.entry.trim()
|
|
38
114
|
if appConfig.entry.startsWith('/') then appConfig.entry else resolve(baseDir, appConfig.entry)
|
|
115
|
+
else if appConfig.entry?
|
|
116
|
+
pushError(errors, 'E_APP_ENTRY', "#{path}.entry", 'entry must be a string path', "Use `entry: './index.rip'`.")
|
|
117
|
+
null
|
|
39
118
|
else
|
|
40
119
|
null
|
|
41
|
-
hosts = appConfig.hosts or []
|
|
42
|
-
workers = appConfig.workers or null
|
|
43
|
-
maxQueue = appConfig.maxQueue or 512
|
|
44
|
-
queueTimeoutMs = appConfig.queueTimeoutMs or 30000
|
|
45
|
-
readTimeoutMs = appConfig.readTimeoutMs or 30000
|
|
46
|
-
|
|
47
120
|
{
|
|
48
121
|
id: appId
|
|
49
122
|
entry
|
|
50
|
-
hosts
|
|
51
|
-
workers
|
|
52
|
-
maxQueue
|
|
53
|
-
queueTimeoutMs
|
|
54
|
-
readTimeoutMs
|
|
55
|
-
env:
|
|
123
|
+
hosts: normalizeHosts(appConfig.hosts, "#{path}.hosts", errors)
|
|
124
|
+
workers: if appConfig.workers? then normalizePositiveInt(appConfig.workers, 0, "#{path}.workers", errors, 'workers') else null
|
|
125
|
+
maxQueue: normalizePositiveInt(appConfig.maxQueue, 512, "#{path}.maxQueue", errors, 'maxQueue')
|
|
126
|
+
queueTimeoutMs: normalizePositiveInt(appConfig.queueTimeoutMs, 30000, "#{path}.queueTimeoutMs", errors, 'queueTimeoutMs')
|
|
127
|
+
readTimeoutMs: normalizePositiveInt(appConfig.readTimeoutMs, 30000, "#{path}.readTimeoutMs", errors, 'readTimeoutMs')
|
|
128
|
+
env: do ->
|
|
129
|
+
env = normalizeEnv(appConfig.env, "#{path}.env", errors)
|
|
130
|
+
if typeof appConfig.root is 'string' and appConfig.root.trim()
|
|
131
|
+
env.APP_BASE_DIR = if appConfig.root.startsWith('/') then appConfig.root else resolve(baseDir, appConfig.root)
|
|
132
|
+
env
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
normalizeTimeouts = (value, path, errors) ->
|
|
136
|
+
return {} unless value?
|
|
137
|
+
unless isPlainObject(value)
|
|
138
|
+
pushError(errors, 'E_TIMEOUTS_TYPE', path, 'timeouts must be an object', "Use `timeouts: { connectMs: 2000, readMs: 30000 }`.")
|
|
139
|
+
return {}
|
|
140
|
+
{
|
|
141
|
+
connectMs: normalizePositiveInt(value.connectMs, 2000, "#{path}.connectMs", errors, 'connectMs')
|
|
142
|
+
readMs: normalizePositiveInt(value.readMs, 30000, "#{path}.readMs", errors, 'readMs')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
normalizeVerifyPolicy = (value, path, errors) ->
|
|
146
|
+
return {
|
|
147
|
+
requireHealthyUpstreams: true
|
|
148
|
+
requireReadyApps: true
|
|
149
|
+
includeUnroutedManagedApps: true
|
|
150
|
+
minHealthyTargetsPerUpstream: 1
|
|
151
|
+
} unless value?
|
|
152
|
+
unless isPlainObject(value)
|
|
153
|
+
pushError(errors, 'E_VERIFY_TYPE', path, 'verify must be an object', "Use `verify: { requireHealthyUpstreams: true }`.")
|
|
154
|
+
return {
|
|
155
|
+
requireHealthyUpstreams: true
|
|
156
|
+
requireReadyApps: true
|
|
157
|
+
includeUnroutedManagedApps: true
|
|
158
|
+
minHealthyTargetsPerUpstream: 1
|
|
159
|
+
}
|
|
160
|
+
{
|
|
161
|
+
requireHealthyUpstreams: if value.requireHealthyUpstreams? then value.requireHealthyUpstreams is true else true
|
|
162
|
+
requireReadyApps: if value.requireReadyApps? then value.requireReadyApps is true else true
|
|
163
|
+
includeUnroutedManagedApps: if value.includeUnroutedManagedApps? then value.includeUnroutedManagedApps is true else true
|
|
164
|
+
minHealthyTargetsPerUpstream: Math.max(1, normalizePositiveInt(value.minHealthyTargetsPerUpstream, 1, "#{path}.minHealthyTargetsPerUpstream", errors, 'minHealthyTargetsPerUpstream'))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
normalizeEdgeSettings = (value, errors) ->
|
|
168
|
+
unless isPlainObject(value)
|
|
169
|
+
pushError(errors, 'E_EDGE_REQUIRED', 'edge', 'edge must be an object', "Use `edge: {}` or omit it to use defaults.")
|
|
170
|
+
value = {}
|
|
171
|
+
certPath = if typeof value.cert is 'string' and value.cert.trim() then resolve(process.cwd(), value.cert) else null
|
|
172
|
+
keyPath = if typeof value.key is 'string' and value.key.trim() then resolve(process.cwd(), value.key) else null
|
|
173
|
+
if (certPath and not keyPath) or (keyPath and not certPath)
|
|
174
|
+
pushError(errors, 'E_TLS_PAIR', 'edge', 'manual TLS requires both cert and key', "Provide both `edge.cert` and `edge.key`, or remove both and rely on ACME/manual defaults.")
|
|
175
|
+
acmeDomains = []
|
|
176
|
+
if value.acmeDomains?
|
|
177
|
+
unless Array.isArray(value.acmeDomains)
|
|
178
|
+
pushError(errors, 'E_ACME_DOMAINS', 'edge.acmeDomains', 'acmeDomains must be an array of exact hostnames', "Use `acmeDomains: ['example.com', 'www.example.com']`.")
|
|
179
|
+
else
|
|
180
|
+
for domain, idx in value.acmeDomains
|
|
181
|
+
unless typeof domain is 'string' and domain.trim()
|
|
182
|
+
pushError(errors, 'E_ACME_DOMAIN_VALUE', "edge.acmeDomains[#{idx}]", 'acme domain must be a non-empty string', "Use `'example.com'`.")
|
|
183
|
+
continue
|
|
184
|
+
if domain.includes('*')
|
|
185
|
+
pushError(errors, 'E_ACME_WILDCARD', "edge.acmeDomains[#{idx}]", 'ACME HTTP-01 cannot issue wildcard certificates', "Remove the wildcard domain or provide `edge.cert` and `edge.key` manually.")
|
|
186
|
+
continue
|
|
187
|
+
acmeDomains.push(domain.trim().toLowerCase())
|
|
188
|
+
{
|
|
189
|
+
acme: value.acme is true
|
|
190
|
+
acmeDomains
|
|
191
|
+
cert: certPath
|
|
192
|
+
key: keyPath
|
|
193
|
+
hsts: value.hsts is true
|
|
194
|
+
trustedProxies: normalizeHosts(value.trustedProxies, 'edge.trustedProxies', errors)
|
|
195
|
+
timeouts: normalizeTimeouts(value.timeouts, 'edge.timeouts', errors)
|
|
196
|
+
verify: normalizeVerifyPolicy(value.verify, 'edge.verify', errors)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
normalizeUpstreamConfig = (upstreamId, upstreamConfig, path, errors) ->
|
|
200
|
+
unless isPlainObject(upstreamConfig)
|
|
201
|
+
pushError(errors, 'E_UPSTREAM_TYPE', path, 'upstream config must be an object', "Use #{upstreamId}: { targets: ['http://127.0.0.1:3000'] }.")
|
|
202
|
+
upstreamConfig = {}
|
|
203
|
+
targets = []
|
|
204
|
+
unless Array.isArray(upstreamConfig.targets) and upstreamConfig.targets.length > 0
|
|
205
|
+
pushError(errors, 'E_UPSTREAM_TARGETS', "#{path}.targets", 'upstream must define at least one target URL', "Use `targets: ['http://127.0.0.1:3000']`.")
|
|
206
|
+
else
|
|
207
|
+
for target, idx in upstreamConfig.targets
|
|
208
|
+
unless typeof target is 'string' and /^https?:\/\//.test(target)
|
|
209
|
+
pushError(errors, 'E_UPSTREAM_TARGET', "#{path}.targets[#{idx}]", 'target must be an absolute http:// or https:// URL', "Use `'http://app.incusbr0:3000'`.")
|
|
210
|
+
continue
|
|
211
|
+
targets.push(target)
|
|
212
|
+
{
|
|
213
|
+
id: upstreamId
|
|
214
|
+
targets
|
|
215
|
+
healthCheck:
|
|
216
|
+
path: if typeof upstreamConfig.healthCheck?.path is 'string' and upstreamConfig.healthCheck.path.startsWith('/') then upstreamConfig.healthCheck.path else '/health'
|
|
217
|
+
intervalMs: normalizePositiveInt(upstreamConfig.healthCheck?.intervalMs, 5000, "#{path}.healthCheck.intervalMs", errors, 'healthCheck.intervalMs')
|
|
218
|
+
timeoutMs: normalizePositiveInt(upstreamConfig.healthCheck?.timeoutMs, 2000, "#{path}.healthCheck.timeoutMs", errors, 'healthCheck.timeoutMs')
|
|
219
|
+
retry:
|
|
220
|
+
attempts: normalizePositiveInt(upstreamConfig.retry?.attempts, 2, "#{path}.retry.attempts", errors, 'retry.attempts')
|
|
221
|
+
retryOn: if Array.isArray(upstreamConfig.retry?.retryOn) and upstreamConfig.retry.retryOn.length > 0 then upstreamConfig.retry.retryOn else [502, 503, 504]
|
|
222
|
+
timeouts: normalizeTimeouts(upstreamConfig.timeouts or upstreamConfig.timeout, "#{path}.timeouts", errors)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
normalizeRouteConfig = (routeConfig, idx, errors, knownUpstreams, knownApps) ->
|
|
226
|
+
path = "routes[#{idx}]"
|
|
227
|
+
unless isPlainObject(routeConfig)
|
|
228
|
+
pushError(errors, 'E_ROUTE_TYPE', path, 'route must be an object', "Use `{ path: '/api/*', upstream: 'app' }`.")
|
|
229
|
+
routeConfig = {}
|
|
230
|
+
actionKeys = ['upstream', 'app', 'static', 'redirect', 'headers'].filter((key) -> routeConfig[key]?)
|
|
231
|
+
if actionKeys.length isnt 1
|
|
232
|
+
pushError(errors, 'E_ROUTE_ACTION', path, 'route must define exactly one action: upstream, app, static, redirect, or headers', "Choose one action field per route.")
|
|
233
|
+
routePath = if typeof routeConfig.path is 'string' and routeConfig.path.startsWith('/') then routeConfig.path else null
|
|
234
|
+
pushError(errors, 'E_ROUTE_PATH', "#{path}.path", 'route path must be a string starting with /', "Use `path: '/api/*'`.") unless routePath
|
|
235
|
+
host = if routeConfig.host? then routeConfig.host else '*'
|
|
236
|
+
if typeof host is 'string' and host.startsWith('*.')
|
|
237
|
+
labels = host.slice(2).split('.').filter(Boolean)
|
|
238
|
+
pushError(errors, 'E_ROUTE_HOST_WILDCARD', "#{path}.host", 'wildcard route hosts must have a base domain with at least two labels', "Use `'*.example.com'`, not `'*.com'`.") if labels.length < 2
|
|
239
|
+
else if typeof host isnt 'string'
|
|
240
|
+
pushError(errors, 'E_ROUTE_HOST', "#{path}.host", 'route host must be a string', "Use `'api.example.com'` or `'*'`.")
|
|
241
|
+
if routeConfig.upstream? and not knownUpstreams.has(routeConfig.upstream)
|
|
242
|
+
pushError(errors, 'E_ROUTE_UPSTREAM_REF', "#{path}.upstream", "unknown upstream #{routeConfig.upstream}", 'Define the upstream under `upstreams` first.')
|
|
243
|
+
if routeConfig.app? and not knownApps.has(routeConfig.app)
|
|
244
|
+
pushError(errors, 'E_ROUTE_APP_REF', "#{path}.app", "unknown app #{routeConfig.app}", 'Define the app under `apps` first.')
|
|
245
|
+
if routeConfig.websocket is true and not routeConfig.upstream?
|
|
246
|
+
pushError(errors, 'E_ROUTE_WEBSOCKET', "#{path}.websocket", 'websocket routes currently require an upstream action', 'Set `upstream: ...` on the route or remove `websocket: true`.')
|
|
247
|
+
{
|
|
248
|
+
id: routeConfig.id or "route-#{idx + 1}"
|
|
249
|
+
host: if typeof host is 'string' then host.toLowerCase() else '*'
|
|
250
|
+
path: routePath or '/'
|
|
251
|
+
methods: if routeConfig.methods? then routeConfig.methods else '*'
|
|
252
|
+
priority: normalizePositiveInt(routeConfig.priority, idx, "#{path}.priority", errors, 'priority')
|
|
253
|
+
upstream: routeConfig.upstream or null
|
|
254
|
+
app: routeConfig.app or null
|
|
255
|
+
static: routeConfig.static or null
|
|
256
|
+
root: routeConfig.root or null
|
|
257
|
+
spa: routeConfig.spa is true
|
|
258
|
+
redirect: routeConfig.redirect or null
|
|
259
|
+
headers: routeConfig.headers or null
|
|
260
|
+
websocket: routeConfig.websocket is true
|
|
261
|
+
timeouts: normalizeTimeouts(routeConfig.timeouts, "#{path}.timeouts", errors)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
normalizeSiteConfig = (host, siteConfig, idx, errors, knownUpstreams, knownApps) ->
|
|
265
|
+
path = "sites.#{host}"
|
|
266
|
+
unless isPlainObject(siteConfig)
|
|
267
|
+
pushError(errors, 'E_SITE_TYPE', path, 'site config must be an object', "Use `'api.example.com': { routes: [...] }`.")
|
|
268
|
+
siteConfig = {}
|
|
269
|
+
routes = []
|
|
270
|
+
if siteConfig.routes?
|
|
271
|
+
unless Array.isArray(siteConfig.routes)
|
|
272
|
+
pushError(errors, 'E_SITE_ROUTES', "#{path}.routes", 'site.routes must be an array', "Use `routes: [{ path: '/*', upstream: 'app' }]`.")
|
|
273
|
+
else
|
|
274
|
+
for routeConfig, routeIdx in siteConfig.routes
|
|
275
|
+
routes.push(normalizeRouteConfig(routeConfig, routeIdx, errors, knownUpstreams, knownApps))
|
|
276
|
+
{
|
|
277
|
+
host: host.toLowerCase()
|
|
278
|
+
timeouts: normalizeTimeouts(siteConfig.timeouts, "#{path}.timeouts", errors)
|
|
279
|
+
routes
|
|
56
280
|
}
|
|
57
281
|
|
|
282
|
+
topLevelKeys = new Set(['version', 'edge', 'hosts', 'upstreams', 'apps', 'streamUpstreams', 'streams'])
|
|
283
|
+
|
|
284
|
+
VALID_REDIRECT_STATUSES = new Set([301, 302, 307, 308])
|
|
285
|
+
|
|
286
|
+
normalizeServerRouteConfig = (routeConfig, idx, serverHost, serverRoot, baseDir, errors, knownUpstreams, knownApps) ->
|
|
287
|
+
path = "hosts.#{serverHost}.routes[#{idx}]"
|
|
288
|
+
unless isPlainObject(routeConfig)
|
|
289
|
+
pushError(errors, 'E_ROUTE_TYPE', path, 'route must be an object', "Use `{ path: '/*', static: '.' }`.")
|
|
290
|
+
routeConfig = {}
|
|
291
|
+
if routeConfig.host?
|
|
292
|
+
pushError(errors, 'E_SERVER_ROUTE_HOST', "#{path}.host", 'routes inside a server block must not specify host (inherited from server key)', "Remove the `host` field.")
|
|
293
|
+
actionKeys = ['upstream', 'app', 'static', 'redirect', 'headers'].filter((key) -> routeConfig[key]?)
|
|
294
|
+
if actionKeys.length isnt 1
|
|
295
|
+
pushError(errors, 'E_ROUTE_ACTION', path, 'route must define exactly one action: upstream, app, static, redirect, or headers', "Choose one action field per route.")
|
|
296
|
+
routePath = if typeof routeConfig.path is 'string' and routeConfig.path.startsWith('/') then routeConfig.path else null
|
|
297
|
+
pushError(errors, 'E_ROUTE_PATH', "#{path}.path", 'route path must be a string starting with /', "Use `path: '/*'`.") unless routePath
|
|
298
|
+
if routeConfig.upstream? and not knownUpstreams.has(routeConfig.upstream)
|
|
299
|
+
pushError(errors, 'E_ROUTE_UPSTREAM_REF', "#{path}.upstream", "unknown upstream #{routeConfig.upstream}", 'Define the upstream under `upstreams` first.')
|
|
300
|
+
if routeConfig.app? and not knownApps.has(routeConfig.app)
|
|
301
|
+
pushError(errors, 'E_ROUTE_APP_REF', "#{path}.app", "unknown app #{routeConfig.app}", 'Define the app under `apps` first.')
|
|
302
|
+
if routeConfig.websocket is true and not routeConfig.upstream?
|
|
303
|
+
pushError(errors, 'E_ROUTE_WEBSOCKET', "#{path}.websocket", 'websocket routes currently require an upstream action', 'Set `upstream: ...` on the route or remove `websocket: true`.')
|
|
304
|
+
if routeConfig.static?
|
|
305
|
+
pushError(errors, 'E_ROUTE_STATIC_TYPE', "#{path}.static", 'static must be a string path', "Use `static: '.'` or `static: '/path/to/dir'`.") unless typeof routeConfig.static is 'string'
|
|
306
|
+
pushError(errors, 'E_ROUTE_SPA_TYPE', "#{path}.spa", 'spa must be a boolean', "Use `spa: true` or remove it.") if routeConfig.spa? and typeof routeConfig.spa isnt 'boolean'
|
|
307
|
+
pushError(errors, 'E_ROUTE_ROOT_TYPE', "#{path}.root", 'root must be a string path', "Use `root: '/path/to/dir'`.") if routeConfig.root? and typeof routeConfig.root isnt 'string'
|
|
308
|
+
if routeConfig.redirect?
|
|
309
|
+
unless isPlainObject(routeConfig.redirect) and typeof routeConfig.redirect.to is 'string'
|
|
310
|
+
pushError(errors, 'E_REDIRECT_TO', "#{path}.redirect", 'redirect must have a `to` string', "Use `redirect: { to: 'https://example.com', status: 301 }`.")
|
|
311
|
+
if routeConfig.redirect?.status? and not VALID_REDIRECT_STATUSES.has(routeConfig.redirect.status)
|
|
312
|
+
pushError(errors, 'E_REDIRECT_STATUS', "#{path}.redirect.status", 'redirect status must be 301, 302, 307, or 308', "Use `status: 301` or `status: 302`.")
|
|
313
|
+
effectiveRoot = if typeof routeConfig.root is 'string' then resolve(baseDir, routeConfig.root) else if serverRoot then serverRoot else null
|
|
314
|
+
{
|
|
315
|
+
id: routeConfig.id or "route-#{idx + 1}"
|
|
316
|
+
host: serverHost.toLowerCase()
|
|
317
|
+
path: routePath or '/'
|
|
318
|
+
methods: if routeConfig.methods? then routeConfig.methods else '*'
|
|
319
|
+
priority: normalizePositiveInt(routeConfig.priority, idx, "#{path}.priority", errors, 'priority')
|
|
320
|
+
upstream: routeConfig.upstream or null
|
|
321
|
+
app: routeConfig.app or null
|
|
322
|
+
static: routeConfig.static or null
|
|
323
|
+
root: effectiveRoot
|
|
324
|
+
spa: routeConfig.spa is true
|
|
325
|
+
browse: routeConfig.browse is true
|
|
326
|
+
redirect: routeConfig.redirect or null
|
|
327
|
+
headers: routeConfig.headers or null
|
|
328
|
+
websocket: routeConfig.websocket is true
|
|
329
|
+
timeouts: normalizeTimeouts(routeConfig.timeouts, "#{path}.timeouts", errors)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
normalizeServerBlock = (host, serverConfig, baseDir, errors, knownUpstreams, knownApps) ->
|
|
333
|
+
path = "hosts.#{host}"
|
|
334
|
+
unless isPlainObject(serverConfig)
|
|
335
|
+
pushError(errors, 'E_SERVER_TYPE', path, 'server block must be an object', "Use '#{host}': { routes: [...] }.")
|
|
336
|
+
return { host: host.toLowerCase(), routes: [], timeouts: {}, certPath: null, keyPath: null, passthrough: null }
|
|
337
|
+
if serverConfig.passthrough?
|
|
338
|
+
unless typeof serverConfig.passthrough is 'string' and serverConfig.passthrough.trim()
|
|
339
|
+
pushError(errors, 'E_SERVER_PASSTHROUGH', "#{path}.passthrough", 'passthrough must be a host:port string', "Use passthrough: '127.0.0.1:8443'.")
|
|
340
|
+
return {
|
|
341
|
+
host: host.toLowerCase()
|
|
342
|
+
routes: []
|
|
343
|
+
timeouts: {}
|
|
344
|
+
certPath: null
|
|
345
|
+
keyPath: null
|
|
346
|
+
passthrough: if typeof serverConfig.passthrough is 'string' then serverConfig.passthrough.trim() else null
|
|
347
|
+
}
|
|
348
|
+
hasCert = serverConfig.cert?
|
|
349
|
+
hasKey = serverConfig.key?
|
|
350
|
+
if hasCert isnt hasKey
|
|
351
|
+
pushError(errors, 'E_SERVER_TLS_PAIR', path, 'server block must provide both cert and key or neither', "Add both cert and key, or remove both.")
|
|
352
|
+
certPath = if hasCert and typeof serverConfig.cert is 'string' then resolve(baseDir, serverConfig.cert) else null
|
|
353
|
+
keyPath = if hasKey and typeof serverConfig.key is 'string' then resolve(baseDir, serverConfig.key) else null
|
|
354
|
+
serverRoot = if typeof serverConfig.root is 'string' then resolve(baseDir, serverConfig.root) else null
|
|
355
|
+
serverSpa = serverConfig.spa is true
|
|
356
|
+
serverBrowse = serverConfig.browse is true
|
|
357
|
+
serverApp = null
|
|
358
|
+
inlineApp = null
|
|
359
|
+
if typeof serverConfig.app is 'string' and serverConfig.app.trim()
|
|
360
|
+
serverApp = serverConfig.app.trim()
|
|
361
|
+
if not knownApps.has(serverApp)
|
|
362
|
+
pushError(errors, 'E_SERVER_APP_REF', "#{path}.app", "unknown app #{serverApp}", 'Define the app under `apps` first.')
|
|
363
|
+
else if isPlainObject(serverConfig.app)
|
|
364
|
+
serverApp = "app-#{host.toLowerCase()}"
|
|
365
|
+
inlineAppConfig = Object.assign({}, serverConfig.app, { hosts: [host.toLowerCase()] })
|
|
366
|
+
inlineApp = normalizeAppConfigInto(serverApp, inlineAppConfig, baseDir, "#{path}.app", errors)
|
|
367
|
+
routes = []
|
|
368
|
+
if Array.isArray(serverConfig.routes) and serverConfig.routes.length > 0
|
|
369
|
+
for routeConfig, idx in serverConfig.routes
|
|
370
|
+
routes.push(normalizeServerRouteConfig(routeConfig, idx, host, serverRoot, baseDir, errors, knownUpstreams, knownApps))
|
|
371
|
+
else if serverApp
|
|
372
|
+
routes.push
|
|
373
|
+
id: 'route-1'
|
|
374
|
+
host: host.toLowerCase()
|
|
375
|
+
path: '/*'
|
|
376
|
+
methods: '*'
|
|
377
|
+
priority: 0
|
|
378
|
+
upstream: null
|
|
379
|
+
app: serverApp
|
|
380
|
+
static: null
|
|
381
|
+
root: null
|
|
382
|
+
spa: false
|
|
383
|
+
browse: false
|
|
384
|
+
redirect: null
|
|
385
|
+
headers: null
|
|
386
|
+
websocket: false
|
|
387
|
+
timeouts: {}
|
|
388
|
+
else if serverRoot
|
|
389
|
+
routes.push
|
|
390
|
+
id: 'route-1'
|
|
391
|
+
host: host.toLowerCase()
|
|
392
|
+
path: '/*'
|
|
393
|
+
methods: '*'
|
|
394
|
+
priority: 0
|
|
395
|
+
upstream: null
|
|
396
|
+
app: null
|
|
397
|
+
static: '.'
|
|
398
|
+
root: serverRoot
|
|
399
|
+
spa: serverSpa
|
|
400
|
+
browse: serverBrowse
|
|
401
|
+
redirect: null
|
|
402
|
+
headers: null
|
|
403
|
+
websocket: false
|
|
404
|
+
timeouts: {}
|
|
405
|
+
else
|
|
406
|
+
pushError(errors, 'E_SERVER_ROUTES', "#{path}", 'server block must define routes, root, app, or passthrough', "Add routes: [...], root: '/path', app: 'name', or passthrough: 'host:port'.")
|
|
407
|
+
{
|
|
408
|
+
host: host.toLowerCase()
|
|
409
|
+
routes: routes
|
|
410
|
+
timeouts: normalizeTimeouts(serverConfig.timeouts, "#{path}.timeouts", errors)
|
|
411
|
+
certPath: certPath
|
|
412
|
+
keyPath: keyPath
|
|
413
|
+
passthrough: null
|
|
414
|
+
inlineApp: inlineApp
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
normalizeServersConfig = (config, baseDir) ->
|
|
418
|
+
errors = []
|
|
419
|
+
for key of config
|
|
420
|
+
pushError(errors, 'E_UNKNOWN_KEY', key, "unknown top-level key #{key}", 'Allowed keys: version, edge, hosts, upstreams, apps, streamUpstreams, streams.') unless topLevelKeys.has(key)
|
|
421
|
+
edgeInput = config.edge or {}
|
|
422
|
+
edge = normalizeEdgeSettings(edgeInput, errors)
|
|
423
|
+
upstreams = {}
|
|
424
|
+
if config.upstreams?
|
|
425
|
+
unless isPlainObject(config.upstreams)
|
|
426
|
+
pushError(errors, 'E_UPSTREAMS_TYPE', 'upstreams', 'upstreams must be an object keyed by upstream ID', "Use upstreams: { app: { targets: ['http://127.0.0.1:3000'] } }.")
|
|
427
|
+
else
|
|
428
|
+
for upstreamId, upstreamConfig of config.upstreams
|
|
429
|
+
upstreams[upstreamId] = normalizeUpstreamConfig(upstreamId, upstreamConfig, "upstreams.#{upstreamId}", errors)
|
|
430
|
+
apps = {}
|
|
431
|
+
if config.apps?
|
|
432
|
+
unless isPlainObject(config.apps)
|
|
433
|
+
pushError(errors, 'E_APPS_TYPE', 'apps', 'apps must be an object keyed by app ID', "Use apps: { admin: { entry: './admin/index.rip' } }.")
|
|
434
|
+
else
|
|
435
|
+
for appId, appConfig of config.apps
|
|
436
|
+
apps[appId] = normalizeAppConfigInto(appId, appConfig, baseDir, "apps.#{appId}", errors)
|
|
437
|
+
knownUpstreams = new Set(Object.keys(upstreams))
|
|
438
|
+
knownApps = new Set(Object.keys(apps))
|
|
439
|
+
sites = {}
|
|
440
|
+
certs = {}
|
|
441
|
+
passthroughUpstreams = {}
|
|
442
|
+
passthroughStreams = []
|
|
443
|
+
unless isPlainObject(config.hosts)
|
|
444
|
+
pushError(errors, 'E_HOSTS_TYPE', 'hosts', 'hosts must be an object keyed by hostname', "Use hosts: { '*.example.com': { root: '/mnt/site' } }.")
|
|
445
|
+
else
|
|
446
|
+
for host, serverConfig of config.hosts
|
|
447
|
+
normalized = normalizeServerBlock(host, serverConfig, baseDir, errors, knownUpstreams, knownApps)
|
|
448
|
+
if normalized.passthrough
|
|
449
|
+
upstreamId = "passthrough-#{normalized.host}"
|
|
450
|
+
parsed = parseHostPort(normalized.passthrough)
|
|
451
|
+
if parsed
|
|
452
|
+
passthroughUpstreams[upstreamId] = { id: upstreamId, targets: [parsed], connectTimeoutMs: 5000 }
|
|
453
|
+
passthroughStreams.push({ id: upstreamId, order: passthroughStreams.length, listen: 443, sni: [normalized.host], upstream: upstreamId, timeouts: {} })
|
|
454
|
+
else
|
|
455
|
+
pushError(errors, 'E_SERVER_PASSTHROUGH', "hosts.#{host}.passthrough", "invalid passthrough target #{normalized.passthrough}", "Use passthrough: '127.0.0.1:8443'.")
|
|
456
|
+
else
|
|
457
|
+
sites[normalized.host] = { host: normalized.host, timeouts: normalized.timeouts, routes: normalized.routes }
|
|
458
|
+
if normalized.certPath and normalized.keyPath
|
|
459
|
+
certs[normalized.host] = { certPath: normalized.certPath, keyPath: normalized.keyPath }
|
|
460
|
+
if normalized.inlineApp
|
|
461
|
+
apps[normalized.inlineApp.id] = normalized.inlineApp
|
|
462
|
+
streamUpstreams = normalizeStreamUpstreams(config.streamUpstreams, errors)
|
|
463
|
+
for id, upstream of passthroughUpstreams
|
|
464
|
+
streamUpstreams[id] = upstream
|
|
465
|
+
knownStreamUpstreams = new Set(Object.keys(streamUpstreams))
|
|
466
|
+
streams = normalizeStreams(config.streams, knownStreamUpstreams, errors)
|
|
467
|
+
streams = passthroughStreams.concat(streams)
|
|
468
|
+
throw validationError(EDGEFILE_NAME, errors) if errors.length > 0
|
|
469
|
+
{ kind: 'edge', version: config.version or 1, edge, upstreams, apps, routes: [], sites, certs, streamUpstreams, streams }
|
|
470
|
+
|
|
471
|
+
export createConfigError = (code, path, message, hint = null) ->
|
|
472
|
+
{ code, path, message, hint }
|
|
473
|
+
|
|
474
|
+
export normalizeAppConfig = (appId, appConfig, baseDir) ->
|
|
475
|
+
errors = []
|
|
476
|
+
normalized = normalizeAppConfigInto(appId, appConfig or {}, baseDir, "apps.#{appId}", errors)
|
|
477
|
+
throw validationError("app #{appId}", errors) if errors.length > 0
|
|
478
|
+
normalized
|
|
479
|
+
|
|
480
|
+
export normalizeLegacyConfig = (config, baseDir) ->
|
|
481
|
+
errors = []
|
|
482
|
+
unless isPlainObject(config)
|
|
483
|
+
pushError(errors, 'E_CONFIG_TYPE', '$', 'config.rip must export an object', "Use `export default { apps: { ... } }`.")
|
|
484
|
+
throw validationError(LEGACY_CONFIG_NAME, errors)
|
|
485
|
+
for key of config
|
|
486
|
+
pushError(errors, 'E_LEGACY_KEY', key, "unknown top-level key #{key} in config.rip", 'Use only the `apps` key in config.rip, or migrate to Edgefile.rip.') unless key is 'apps'
|
|
487
|
+
apps = {}
|
|
488
|
+
if config.apps?
|
|
489
|
+
unless isPlainObject(config.apps)
|
|
490
|
+
pushError(errors, 'E_APPS_TYPE', 'apps', 'apps must be an object keyed by app ID', "Use `apps: { main: { entry: './index.rip' } }`.")
|
|
491
|
+
else
|
|
492
|
+
for appId, appConfig of config.apps
|
|
493
|
+
apps[appId] = normalizeAppConfigInto(appId, appConfig, baseDir, "apps.#{appId}", errors)
|
|
494
|
+
throw validationError(LEGACY_CONFIG_NAME, errors) if errors.length > 0
|
|
495
|
+
{ kind: 'legacy', version: null, edge: {}, upstreams: {}, apps, routes: [], sites: {} }
|
|
496
|
+
|
|
497
|
+
export normalizeEdgeConfig = (config, baseDir) ->
|
|
498
|
+
normalizeServersConfig(config, baseDir)
|
|
499
|
+
|
|
500
|
+
export summarizeConfig = (configPath, normalized) ->
|
|
501
|
+
path = resolve(configPath)
|
|
502
|
+
kind = normalized.kind or (if basename(path) is EDGEFILE_NAME then 'edge' else 'legacy')
|
|
503
|
+
{
|
|
504
|
+
kind
|
|
505
|
+
path
|
|
506
|
+
version: normalized.version or null
|
|
507
|
+
counts:
|
|
508
|
+
upstreams: Object.keys(normalized.upstreams or {}).length
|
|
509
|
+
apps: Object.keys(normalized.apps or {}).length
|
|
510
|
+
routes: (normalized.routes or []).length
|
|
511
|
+
sites: Object.keys(normalized.sites or {}).length
|
|
512
|
+
streamUpstreams: Object.keys(normalized.streamUpstreams or {}).length
|
|
513
|
+
streams: (normalized.streams or []).length
|
|
514
|
+
canonicalShape:
|
|
515
|
+
version: true
|
|
516
|
+
edge: true
|
|
517
|
+
upstreams: true
|
|
518
|
+
apps: true
|
|
519
|
+
routes: true
|
|
520
|
+
sites: true
|
|
521
|
+
streamUpstreams: true
|
|
522
|
+
streams: true
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export loadConfig = (configPath) ->
|
|
526
|
+
return null unless existsSync(configPath)
|
|
527
|
+
try
|
|
528
|
+
raw = moduleObject!(configPath, LEGACY_CONFIG_NAME)
|
|
529
|
+
normalizeLegacyConfig(raw, dirname(configPath))
|
|
530
|
+
catch e
|
|
531
|
+
if e.validationErrors
|
|
532
|
+
console.error formatConfigErrors(LEGACY_CONFIG_NAME, e.validationErrors)
|
|
533
|
+
else
|
|
534
|
+
console.error "rip-server: failed to load config.rip: #{e.message}"
|
|
535
|
+
null
|
|
536
|
+
|
|
537
|
+
export loadEdgeConfig = (configPath) ->
|
|
538
|
+
return null unless existsSync(configPath)
|
|
539
|
+
raw = moduleObject!(configPath, EDGEFILE_NAME)
|
|
540
|
+
normalizeEdgeConfig(raw, dirname(configPath))
|
|
541
|
+
|
|
542
|
+
export checkConfigFile = (configPath) ->
|
|
543
|
+
resolved = resolve(configPath)
|
|
544
|
+
kind = if basename(resolved) is EDGEFILE_NAME then 'edge' else 'legacy'
|
|
545
|
+
normalized = if kind is 'edge' then loadEdgeConfig!(resolved) else normalizeLegacyConfig(moduleObject!(resolved, LEGACY_CONFIG_NAME), dirname(resolved))
|
|
546
|
+
{ kind, normalized, summary: summarizeConfig(resolved, normalized) }
|
|
547
|
+
|
|
58
548
|
export findConfigFile = (appEntry) ->
|
|
59
|
-
dir = dirname(appEntry)
|
|
60
|
-
configPath = join(dir,
|
|
549
|
+
dir = if existsSync(appEntry) and statSync(appEntry).isDirectory() then appEntry else dirname(appEntry)
|
|
550
|
+
configPath = join(dir, LEGACY_CONFIG_NAME)
|
|
61
551
|
if existsSync(configPath) then configPath else null
|
|
62
552
|
|
|
553
|
+
export findEdgeFile = (appEntry) ->
|
|
554
|
+
dir = if existsSync(appEntry) and statSync(appEntry).isDirectory() then appEntry else dirname(appEntry)
|
|
555
|
+
edgePath = join(dir, EDGEFILE_NAME)
|
|
556
|
+
if existsSync(edgePath) then edgePath else null
|
|
557
|
+
|
|
558
|
+
export resolveConfigSource = (appEntry, edgefilePath = null) ->
|
|
559
|
+
if edgefilePath
|
|
560
|
+
resolved = if edgefilePath.startsWith('/') then edgefilePath else resolve(process.cwd(), edgefilePath)
|
|
561
|
+
return { kind: 'edge', path: resolved }
|
|
562
|
+
if basename(appEntry) is EDGEFILE_NAME
|
|
563
|
+
return { kind: 'edge', path: resolve(appEntry) }
|
|
564
|
+
if basename(appEntry) is LEGACY_CONFIG_NAME
|
|
565
|
+
return { kind: 'legacy', path: resolve(appEntry) }
|
|
566
|
+
edgePath = findEdgeFile(appEntry)
|
|
567
|
+
return { kind: 'edge', path: edgePath } if edgePath
|
|
568
|
+
legacyPath = findConfigFile(appEntry)
|
|
569
|
+
return { kind: 'legacy', path: legacyPath } if legacyPath
|
|
570
|
+
null
|
|
571
|
+
|
|
63
572
|
export applyConfig = (config, registry, registerAppFn, baseDir) ->
|
|
64
|
-
|
|
573
|
+
normalized = if config?.kind then config else normalizeLegacyConfig(config or {}, baseDir)
|
|
574
|
+
apps = normalized.apps or {}
|
|
65
575
|
registered = []
|
|
66
576
|
for appId, appConfig of apps
|
|
67
|
-
|
|
577
|
+
normalizedApp = normalizeAppConfigInto(appId, appConfig, baseDir, "apps.#{appId}", [])
|
|
578
|
+
registerAppFn(registry, appId,
|
|
579
|
+
entry: normalizedApp.entry
|
|
580
|
+
appBaseDir: if normalizedApp.entry then dirname(normalizedApp.entry) else baseDir
|
|
581
|
+
hosts: normalizedApp.hosts
|
|
582
|
+
workers: normalizedApp.workers
|
|
583
|
+
maxQueue: normalizedApp.maxQueue
|
|
584
|
+
queueTimeoutMs: normalizedApp.queueTimeoutMs
|
|
585
|
+
readTimeoutMs: normalizedApp.readTimeoutMs
|
|
586
|
+
env: normalizedApp.env
|
|
587
|
+
)
|
|
588
|
+
registered.push(normalizedApp)
|
|
589
|
+
registered
|
|
590
|
+
|
|
591
|
+
export applyEdgeConfig = (config, registry, registerAppFn, baseDir) ->
|
|
592
|
+
registered = []
|
|
593
|
+
apps = config.apps or {}
|
|
594
|
+
for appId, appConfig of apps
|
|
595
|
+
normalizedApp = normalizeAppConfigInto(appId, appConfig, baseDir, "apps.#{appId}", [])
|
|
68
596
|
registerAppFn(registry, appId,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
597
|
+
entry: normalizedApp.entry
|
|
598
|
+
appBaseDir: if normalizedApp.entry then dirname(normalizedApp.entry) else baseDir
|
|
599
|
+
hosts: normalizedApp.hosts
|
|
600
|
+
workers: normalizedApp.workers
|
|
601
|
+
maxQueue: normalizedApp.maxQueue
|
|
602
|
+
queueTimeoutMs: normalizedApp.queueTimeoutMs
|
|
603
|
+
readTimeoutMs: normalizedApp.readTimeoutMs
|
|
604
|
+
env: normalizedApp.env
|
|
73
605
|
)
|
|
74
|
-
registered.push(
|
|
606
|
+
registered.push(normalizedApp)
|
|
75
607
|
registered
|