@platformatic/runtime 0.26.0
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/LICENSE +201 -0
- package/NOTICE +13 -0
- package/README.md +13 -0
- package/fixtures/configs/invalid-entrypoint.json +14 -0
- package/fixtures/configs/missing-property.config.json +13 -0
- package/fixtures/configs/missing-service-config.json +7 -0
- package/fixtures/configs/monorepo-composer.json +19 -0
- package/fixtures/configs/monorepo-create-cycle.json +21 -0
- package/fixtures/configs/monorepo-no-cycles.json +20 -0
- package/fixtures/configs/monorepo.json +20 -0
- package/fixtures/configs/no-services.config.json +5 -0
- package/fixtures/configs/no-sources.config.json +4 -0
- package/fixtures/configs/service-throws-on-start.json +11 -0
- package/fixtures/monorepo/composerApp/platformatic.composer.json +39 -0
- package/fixtures/monorepo/docs/README.md +1 -0
- package/fixtures/monorepo/serviceApp/deps/dep1.js +7 -0
- package/fixtures/monorepo/serviceApp/deps/dep2.mjs +2 -0
- package/fixtures/monorepo/serviceApp/deps/dep3.mjs +2 -0
- package/fixtures/monorepo/serviceApp/deps/dep4.js +1 -0
- package/fixtures/monorepo/serviceApp/platformatic.service.json +22 -0
- package/fixtures/monorepo/serviceApp/plugin.js +25 -0
- package/fixtures/monorepo/serviceApp/with-logger/package.json +5 -0
- package/fixtures/monorepo/serviceApp/with-logger/with-logger.cjs +25 -0
- package/fixtures/monorepo/serviceApp/with-logger/with-logger.d.ts +38 -0
- package/fixtures/monorepo/serviceApp/with-logger/with-logger.openapi.json +22 -0
- package/fixtures/monorepo/serviceAppWithLogger/platformatic.service.json +19 -0
- package/fixtures/monorepo/serviceAppWithLogger/plugin.js +8 -0
- package/fixtures/monorepo/serviceAppWithMultiplePlugins/platformatic.service.json +26 -0
- package/fixtures/monorepo/serviceAppWithMultiplePlugins/plugin.js +8 -0
- package/fixtures/monorepo/serviceAppWithMultiplePlugins/plugin2.mjs +5 -0
- package/fixtures/serviceAppThrowsOnStart/platformatic.service.json +13 -0
- package/fixtures/serviceAppThrowsOnStart/plugin.js +6 -0
- package/help/help.txt +5 -0
- package/help/start.txt +5 -0
- package/index.d.ts +13 -0
- package/index.js +33 -0
- package/index.test-d.ts +18 -0
- package/lib/app.js +261 -0
- package/lib/config.js +209 -0
- package/lib/loader.mjs +101 -0
- package/lib/schema.js +89 -0
- package/lib/start.js +77 -0
- package/lib/worker.js +88 -0
- package/package.json +52 -0
- package/runtime.mjs +43 -0
- package/test/app.test.js +245 -0
- package/test/cli/helper.mjs +46 -0
- package/test/cli/start.test.mjs +61 -0
- package/test/cli/validations.test.mjs +52 -0
- package/test/cli/watch.test.mjs +131 -0
- package/test/config.test.js +58 -0
- package/test/schema.test.js +13 -0
- package/test/start.test.js +101 -0
- package/test/worker.test.js +21 -0
package/help/help.txt
ADDED
package/help/start.txt
ADDED
package/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { InjectOptions, LightMyRequestResponse } from 'fastify'
|
|
2
|
+
|
|
3
|
+
export type pltRuntimeBuildServer = {
|
|
4
|
+
address: string
|
|
5
|
+
port: number
|
|
6
|
+
restart: () => Promise<void>
|
|
7
|
+
stop: () => Promise<void>
|
|
8
|
+
inject: (opts: InjectOptions | string) => Promise<LightMyRequestResponse>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare module '@platformatic/runtime' {
|
|
12
|
+
export function buildServer(opts: object): Promise<pltRuntimeBuildServer>
|
|
13
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
const ConfigManager = require('@platformatic/config')
|
|
3
|
+
const { platformaticRuntime } = require('./lib/config')
|
|
4
|
+
const { start, startWithConfig } = require('./lib/start')
|
|
5
|
+
|
|
6
|
+
async function buildServer (options = {}) {
|
|
7
|
+
if (!options.configManager) {
|
|
8
|
+
// Instantiate a new config manager from the current options.
|
|
9
|
+
const cm = new ConfigManager({
|
|
10
|
+
...platformaticRuntime.configManagerConfig,
|
|
11
|
+
source: options
|
|
12
|
+
})
|
|
13
|
+
await cm.parseAndValidate()
|
|
14
|
+
|
|
15
|
+
if (typeof options === 'string') {
|
|
16
|
+
options = { configManager: cm }
|
|
17
|
+
} else {
|
|
18
|
+
options.configManager = cm
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// The transformConfig() function can't be sent between threads.
|
|
23
|
+
delete options.configManager._transformConfig
|
|
24
|
+
|
|
25
|
+
return startWithConfig(options.configManager)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
buildServer,
|
|
30
|
+
platformaticRuntime,
|
|
31
|
+
schema: platformaticRuntime.schema,
|
|
32
|
+
start
|
|
33
|
+
}
|
package/index.test-d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { expectError, expectType } from 'tsd';
|
|
2
|
+
import { LightMyRequestResponse } from 'fastify';
|
|
3
|
+
import { pltRuntimeBuildServer } from '.';
|
|
4
|
+
|
|
5
|
+
const server: pltRuntimeBuildServer = {
|
|
6
|
+
address: 'localhost',
|
|
7
|
+
port: 3000,
|
|
8
|
+
restart: async () => {},
|
|
9
|
+
stop: async () => {},
|
|
10
|
+
inject: async () => ({} as LightMyRequestResponse),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
expectType<pltRuntimeBuildServer>(server);
|
|
14
|
+
expectError<pltRuntimeBuildServer>({...server, address: 42 });
|
|
15
|
+
expectError<pltRuntimeBuildServer>({...server, port: 'WRONG' });
|
|
16
|
+
expectError<pltRuntimeBuildServer>({...server, restart: 'WRONG' });
|
|
17
|
+
expectError<pltRuntimeBuildServer>({...server, stop: 'WRONG' });
|
|
18
|
+
expectError<pltRuntimeBuildServer>({...server, inject: 'WRONG' });
|
package/lib/app.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
const { once } = require('node:events')
|
|
3
|
+
const { dirname } = require('node:path')
|
|
4
|
+
const {
|
|
5
|
+
addLoggerToTheConfig
|
|
6
|
+
} = require('@platformatic/service')
|
|
7
|
+
const {
|
|
8
|
+
buildServer,
|
|
9
|
+
loadConfig
|
|
10
|
+
} = require('@platformatic/start')
|
|
11
|
+
const { FileWatcher } = require('@platformatic/utils')
|
|
12
|
+
|
|
13
|
+
class PlatformaticApp {
|
|
14
|
+
#hotReload
|
|
15
|
+
#loaderPort
|
|
16
|
+
#restarting
|
|
17
|
+
#started
|
|
18
|
+
#originalWatch
|
|
19
|
+
|
|
20
|
+
constructor (appConfig, loaderPort) {
|
|
21
|
+
this.appConfig = appConfig
|
|
22
|
+
this.config = null
|
|
23
|
+
this.#hotReload = false
|
|
24
|
+
this.#loaderPort = loaderPort
|
|
25
|
+
this.#restarting = false
|
|
26
|
+
this.server = null
|
|
27
|
+
this.#started = false
|
|
28
|
+
this.#originalWatch = null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async restart (force) {
|
|
32
|
+
if (this.#restarting) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!this.#hotReload && !force) {
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.#restarting = true
|
|
41
|
+
await this.stop()
|
|
42
|
+
|
|
43
|
+
/* c8 ignore next 4 - tests may not pass in a MessagePort. */
|
|
44
|
+
if (this.#loaderPort) {
|
|
45
|
+
this.#loaderPort.postMessage('plt:clear-cache')
|
|
46
|
+
await once(this.#loaderPort, 'message')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await this.start()
|
|
50
|
+
this.#restarting = false
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async start () {
|
|
54
|
+
if (this.#started) {
|
|
55
|
+
throw new Error('application is already started')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
await this.#initializeConfig()
|
|
59
|
+
const { configManager } = this.config
|
|
60
|
+
const config = configManager.current
|
|
61
|
+
|
|
62
|
+
this.#originalWatch = config.watch
|
|
63
|
+
config.watch = false
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
// If this is a restart, have the fastify server restart itself. If this
|
|
67
|
+
// is not a restart, then create a new server.
|
|
68
|
+
if (this.#restarting) {
|
|
69
|
+
await this.server.restart()
|
|
70
|
+
} else {
|
|
71
|
+
this.server = await buildServer({
|
|
72
|
+
...config,
|
|
73
|
+
configManager
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
this.#logAndExit(err)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.server.platformatic.configManager = configManager
|
|
81
|
+
this.server.platformatic.config = config
|
|
82
|
+
|
|
83
|
+
if (config.plugins !== undefined && this.#originalWatch !== false) {
|
|
84
|
+
this.#startFileWatching()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.#started = true
|
|
88
|
+
|
|
89
|
+
if (this.appConfig.entrypoint && !this.#restarting) {
|
|
90
|
+
try {
|
|
91
|
+
await this.server.start()
|
|
92
|
+
/* c8 ignore next 5 */
|
|
93
|
+
} catch (err) {
|
|
94
|
+
this.server.log.error({ err })
|
|
95
|
+
process.exit(1)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async stop () {
|
|
101
|
+
if (!this.#started) {
|
|
102
|
+
throw new Error('application has not been started')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!this.#restarting) {
|
|
106
|
+
await this.server.close()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
await this.#stopFileWatching()
|
|
110
|
+
this.#started = false
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async handleProcessLevelEvent ({ msg, signal, err }) {
|
|
114
|
+
if (msg === 'plt:start') {
|
|
115
|
+
await this.start()
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!this.server) {
|
|
120
|
+
return false
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (msg === 'plt:restart') {
|
|
124
|
+
await this.restart(true)
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (msg === 'plt:stop') {
|
|
129
|
+
await this.stop()
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (signal === 'SIGUSR2') {
|
|
134
|
+
this.server.log.info('reloading configuration')
|
|
135
|
+
await this.restart()
|
|
136
|
+
return false
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (err) {
|
|
140
|
+
this.server.log.error({
|
|
141
|
+
err: {
|
|
142
|
+
message: err?.message,
|
|
143
|
+
stack: err?.stack
|
|
144
|
+
}
|
|
145
|
+
}, 'exiting')
|
|
146
|
+
} else if (signal) {
|
|
147
|
+
this.server.log.info({ signal }, 'received signal')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (this.#started) {
|
|
151
|
+
await this.stop()
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async #initializeConfig () {
|
|
156
|
+
const appConfig = this.appConfig
|
|
157
|
+
|
|
158
|
+
this.config = await loadConfig({}, ['-c', appConfig.config], null, {
|
|
159
|
+
watch: true,
|
|
160
|
+
onMissingEnv (key) {
|
|
161
|
+
return appConfig.localServiceEnvVars.get(key)
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
const { args, configManager } = this.config
|
|
165
|
+
|
|
166
|
+
if (appConfig._configOverrides instanceof Map) {
|
|
167
|
+
try {
|
|
168
|
+
appConfig._configOverrides.forEach((value, key) => {
|
|
169
|
+
if (typeof key !== 'string') {
|
|
170
|
+
throw new Error('config path must be a string.')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const parts = key.split('.')
|
|
174
|
+
let next = configManager.current
|
|
175
|
+
let obj
|
|
176
|
+
|
|
177
|
+
for (let i = 0; next !== undefined && i < parts.length; ++i) {
|
|
178
|
+
obj = next
|
|
179
|
+
next = obj[parts[i]]
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (next !== undefined) {
|
|
183
|
+
obj[parts.at(-1)] = value
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
} catch (err) {
|
|
187
|
+
configManager.stopWatching()
|
|
188
|
+
throw err
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Set the logger if not present (and the config supports it).
|
|
193
|
+
if (configManager.current.server) {
|
|
194
|
+
addLoggerToTheConfig(configManager.current)
|
|
195
|
+
configManager.current.server.logger.name = this.appConfig.id
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
this.#hotReload = args.hotReload && this.appConfig.hotReload
|
|
199
|
+
|
|
200
|
+
if (configManager.current.plugins) {
|
|
201
|
+
if (this.#hotReload) {
|
|
202
|
+
this.#hotReload = configManager.current.plugins.hotReload
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
configManager.current.plugins.hotReload = false
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
configManager.on('update', async (newConfig) => {
|
|
209
|
+
/* c8 ignore next 4 */
|
|
210
|
+
this.server.platformatic.config = newConfig
|
|
211
|
+
this.server.log.debug('config changed')
|
|
212
|
+
this.server.log.trace({ newConfig }, 'new config')
|
|
213
|
+
await this.restart()
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
configManager.on('error', (err) => {
|
|
217
|
+
/* c8 ignore next */
|
|
218
|
+
this.server.log.error({ err }, 'error reloading the configuration')
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
#startFileWatching () {
|
|
223
|
+
const server = this.server
|
|
224
|
+
const { configManager } = server.platformatic
|
|
225
|
+
const fileWatcher = new FileWatcher({
|
|
226
|
+
path: dirname(configManager.fullPath),
|
|
227
|
+
/* c8 ignore next 2 */
|
|
228
|
+
allowToWatch: this.#originalWatch?.allow,
|
|
229
|
+
watchIgnore: this.#originalWatch?.ignore
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
fileWatcher.on('update', async () => {
|
|
233
|
+
this.server.log.debug('files changed')
|
|
234
|
+
this.restart()
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
fileWatcher.startWatching()
|
|
238
|
+
server.log.debug('start watching files')
|
|
239
|
+
server.platformatic.fileWatcher = fileWatcher
|
|
240
|
+
server.platformatic.configManager.startWatching()
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async #stopFileWatching () {
|
|
244
|
+
const watcher = this.server.platformatic.fileWatcher
|
|
245
|
+
|
|
246
|
+
if (watcher) {
|
|
247
|
+
await watcher.stopWatching()
|
|
248
|
+
this.server.log.debug('stop watching files')
|
|
249
|
+
this.server.platformatic.fileWatcher = undefined
|
|
250
|
+
this.server.platformatic.configManager.stopWatching()
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
#logAndExit (err) {
|
|
255
|
+
this.config?.configManager?.stopWatching()
|
|
256
|
+
console.error(err)
|
|
257
|
+
process.exit(1)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
module.exports = { PlatformaticApp }
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
const { readFile, readdir } = require('node:fs/promises')
|
|
3
|
+
const { join, resolve: pathResolve } = require('node:path')
|
|
4
|
+
const Topo = require('@hapi/topo')
|
|
5
|
+
const ConfigManager = require('@platformatic/config')
|
|
6
|
+
const { schema } = require('./schema')
|
|
7
|
+
|
|
8
|
+
async function _transformConfig (configManager) {
|
|
9
|
+
const config = configManager.current
|
|
10
|
+
const services = config.services ?? []
|
|
11
|
+
|
|
12
|
+
if (config.autoload) {
|
|
13
|
+
const { path, exclude = [], mappings = {} } = config.autoload
|
|
14
|
+
const entries = await readdir(path, { withFileTypes: true })
|
|
15
|
+
|
|
16
|
+
for (let i = 0; i < entries.length; ++i) {
|
|
17
|
+
const entry = entries[i]
|
|
18
|
+
|
|
19
|
+
if (exclude.includes(entry.name) || !entry.isDirectory()) {
|
|
20
|
+
continue
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const mapping = mappings[entry.name] ?? {}
|
|
24
|
+
const id = mapping.id ?? entry.name
|
|
25
|
+
const entryPath = join(path, entry.name)
|
|
26
|
+
const configFilename = mapping.config ?? await ConfigManager.findConfigFile(entryPath)
|
|
27
|
+
|
|
28
|
+
if (typeof configFilename !== 'string') {
|
|
29
|
+
throw new Error(`no config file found for service '${id}'`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const config = join(entryPath, configFilename)
|
|
33
|
+
|
|
34
|
+
services.push({ id, config, path: entryPath })
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
configManager.current.allowCycles = !!configManager.current.allowCycles
|
|
39
|
+
configManager.current.serviceMap = new Map()
|
|
40
|
+
|
|
41
|
+
let hasValidEntrypoint = false
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < services.length; ++i) {
|
|
44
|
+
const service = services[i]
|
|
45
|
+
|
|
46
|
+
service.config = pathResolve(service.path, service.config)
|
|
47
|
+
service.entrypoint = service.id === config.entrypoint
|
|
48
|
+
service.hotReload = !!config.hotReload
|
|
49
|
+
service.dependencies = []
|
|
50
|
+
service.dependents = []
|
|
51
|
+
service.localServiceEnvVars = new Map()
|
|
52
|
+
service.localUrl = `http://${service.id}.plt.local`
|
|
53
|
+
|
|
54
|
+
if (service.entrypoint) {
|
|
55
|
+
hasValidEntrypoint = true
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
configManager.current.serviceMap.set(service.id, service)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!hasValidEntrypoint) {
|
|
62
|
+
throw new Error(`invalid entrypoint: '${config.entrypoint}' does not exist`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
configManager.current.services = services
|
|
66
|
+
await parseClientsAndComposer(configManager)
|
|
67
|
+
|
|
68
|
+
if (!configManager.current.allowCycles) {
|
|
69
|
+
topologicalSort(configManager)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function parseClientsAndComposer (configManager) {
|
|
74
|
+
for (let i = 0; i < configManager.current.services.length; ++i) {
|
|
75
|
+
const service = configManager.current.services[i]
|
|
76
|
+
const cm = new ConfigManager({ source: service.config })
|
|
77
|
+
const configString = await cm.load()
|
|
78
|
+
const parsed = cm._parser(configString)
|
|
79
|
+
|
|
80
|
+
if (Array.isArray(parsed.composer?.services)) {
|
|
81
|
+
for (let i = 0; i < parsed.composer.services.length; ++i) {
|
|
82
|
+
const dep = parsed.composer.services[i]
|
|
83
|
+
/* c8 ignore next 4 - why c8? */
|
|
84
|
+
const clientName = dep.id ?? ''
|
|
85
|
+
const dependency = configManager.current.serviceMap.get(clientName)
|
|
86
|
+
|
|
87
|
+
if (dependency === undefined) {
|
|
88
|
+
/* c8 ignore next 2 */
|
|
89
|
+
throw new Error(`service '${service.id}' has unknown dependency: '${clientName}'`)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
dependency.dependents.push(service.id)
|
|
93
|
+
|
|
94
|
+
if (dep.origin) {
|
|
95
|
+
try {
|
|
96
|
+
await cm.replaceEnv(dep.origin)
|
|
97
|
+
/* c8 ignore next 4 */
|
|
98
|
+
} catch (err) {
|
|
99
|
+
if (err.name !== 'MissingValueError') {
|
|
100
|
+
throw err
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (dep.origin === `{${err.key}}`) {
|
|
104
|
+
service.localServiceEnvVars.set(err.key, `${clientName}.plt.local`)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
service.dependencies.push({
|
|
110
|
+
id: clientName,
|
|
111
|
+
url: `http://${clientName}.plt.local`,
|
|
112
|
+
local: true
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (Array.isArray(parsed.clients)) {
|
|
118
|
+
const promises = parsed.clients.map((client) => {
|
|
119
|
+
// eslint-disable-next-line no-async-promise-executor
|
|
120
|
+
return new Promise(async (resolve, reject) => {
|
|
121
|
+
let clientUrl
|
|
122
|
+
let missingKey
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
clientUrl = await cm.replaceEnv(client.url)
|
|
126
|
+
/* c8 ignore next 2 - unclear why c8 is unhappy here */
|
|
127
|
+
} catch (err) {
|
|
128
|
+
if (err.name !== 'MissingValueError') {
|
|
129
|
+
/* c8 ignore next 3 */
|
|
130
|
+
reject(err)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
missingKey = err.key
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const isLocal = missingKey && client.url === `{${missingKey}}`
|
|
138
|
+
const clientAbsolutePath = pathResolve(service.path, client.path)
|
|
139
|
+
const clientPackageJson = join(clientAbsolutePath, 'package.json')
|
|
140
|
+
const clientMetadata = JSON.parse(await readFile(clientPackageJson, 'utf8'))
|
|
141
|
+
/* c8 ignore next 20 - unclear why c8 is unhappy for nearly 20 lines here */
|
|
142
|
+
const clientName = clientMetadata.name ?? ''
|
|
143
|
+
|
|
144
|
+
if (clientUrl === undefined) {
|
|
145
|
+
// Combine the service name with the client name to avoid collisions
|
|
146
|
+
// if two or more services have a client with the same name pointing
|
|
147
|
+
// to different services.
|
|
148
|
+
clientUrl = isLocal ? `http://${clientName}.plt.local` : client.url
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
service.dependencies.push({
|
|
152
|
+
id: clientName,
|
|
153
|
+
url: clientUrl,
|
|
154
|
+
local: isLocal
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const dependency = configManager.current.serviceMap.get(clientName)
|
|
158
|
+
|
|
159
|
+
if (dependency === undefined) {
|
|
160
|
+
/* c8 ignore next 3 */
|
|
161
|
+
reject(new Error(`service '${service.id}' has unknown dependency: '${clientName}'`))
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
dependency.dependents.push(service.id)
|
|
166
|
+
|
|
167
|
+
if (isLocal) {
|
|
168
|
+
service.localServiceEnvVars.set(missingKey, clientUrl)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
resolve()
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
await Promise.all(promises)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function topologicalSort (configManager) {
|
|
181
|
+
const { services } = configManager.current
|
|
182
|
+
const topo = new Topo.Sorter()
|
|
183
|
+
|
|
184
|
+
for (let i = 0; i < services.length; ++i) {
|
|
185
|
+
const service = services[i]
|
|
186
|
+
const dependencyIds = service.dependencies.map(dep => dep.id)
|
|
187
|
+
|
|
188
|
+
topo.add(service, { group: service.id, after: dependencyIds, manual: true })
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
configManager.current.services = topo.sort()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function platformaticRuntime () {
|
|
195
|
+
// No-op. Here for consistency with other app types.
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
platformaticRuntime[Symbol.for('skip-override')] = true
|
|
199
|
+
platformaticRuntime.schema = schema
|
|
200
|
+
platformaticRuntime.configType = 'runtime'
|
|
201
|
+
platformaticRuntime.configManagerConfig = {
|
|
202
|
+
schema,
|
|
203
|
+
allowToWatch: ['.env'],
|
|
204
|
+
async transformConfig () {
|
|
205
|
+
await _transformConfig(this)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = { platformaticRuntime }
|
package/lib/loader.mjs
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createRequire, isBuiltin } from 'node:module'
|
|
2
|
+
import { dirname, isAbsolute, resolve as pathResolve } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
const require = createRequire(import.meta.url)
|
|
5
|
+
const thisFile = fileURLToPath(import.meta.url)
|
|
6
|
+
const isWindows = process.platform === 'win32'
|
|
7
|
+
let timestamp = process.hrtime.bigint()
|
|
8
|
+
let port
|
|
9
|
+
|
|
10
|
+
function bustEsmCache () {
|
|
11
|
+
timestamp = process.hrtime.bigint()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function clearCjsCache () {
|
|
15
|
+
// This evicts all of the modules from the require() cache.
|
|
16
|
+
// Note: This does not clean up children references to the deleted module.
|
|
17
|
+
// It's likely not a big deal for most cases, but it is a leak. The child
|
|
18
|
+
// references can be cleaned up, but it is expensive and involves walking
|
|
19
|
+
// the entire require() cache. See the DEP0144 documentation for how to do
|
|
20
|
+
// it.
|
|
21
|
+
Object.keys(require.cache).forEach((key) => {
|
|
22
|
+
delete require.cache[key]
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isRelativePath (p) {
|
|
27
|
+
// This function is extracted from Node core, so it should work.
|
|
28
|
+
return p.charAt(0) === '.' &&
|
|
29
|
+
/* c8 ignore next 9 */
|
|
30
|
+
(
|
|
31
|
+
p.length === 1 ||
|
|
32
|
+
p.charAt(1) === '/' ||
|
|
33
|
+
(isWindows && p.charAt(1) === '\\') ||
|
|
34
|
+
(p.charAt(1) === '.' && ((
|
|
35
|
+
p.length === 2 ||
|
|
36
|
+
p.charAt(2) === '/') ||
|
|
37
|
+
(isWindows && p.charAt(2) === '\\')))
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function specifierToPath (specifier, referencingModuleId) {
|
|
42
|
+
// Convert the specifier into an absolute path if possible. If the specifier
|
|
43
|
+
// cannot be converted to a path (for example for a core module), then return
|
|
44
|
+
// null.
|
|
45
|
+
try {
|
|
46
|
+
const url = new URL(specifier)
|
|
47
|
+
|
|
48
|
+
if (url.protocol === 'file:') {
|
|
49
|
+
specifier = url.pathname
|
|
50
|
+
} else {
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// Ignore error.
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (isBuiltin(specifier)) {
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (isAbsolute(specifier)) {
|
|
62
|
+
return specifier
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* c8 ignore next 3 */
|
|
66
|
+
if (!referencingModuleId) {
|
|
67
|
+
throw new Error(`cannot map '${specifier}' to an absolute path`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (isRelativePath(specifier)) {
|
|
71
|
+
return pathResolve(dirname(fileURLToPath(referencingModuleId)), specifier)
|
|
72
|
+
} else {
|
|
73
|
+
// The specifier is something in node_modules/.
|
|
74
|
+
const req = createRequire(referencingModuleId)
|
|
75
|
+
|
|
76
|
+
return req.resolve(specifier)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function resolve (specifier, context, nextResolve) {
|
|
81
|
+
const path = specifierToPath(specifier, context.parentURL)
|
|
82
|
+
|
|
83
|
+
// If the specifier could not be mapped to a file, or the path is this file,
|
|
84
|
+
// then don't do anything.
|
|
85
|
+
if (typeof path !== 'string' || path === thisFile) {
|
|
86
|
+
return nextResolve(specifier, context)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return nextResolve(`${path}?ts=${timestamp}`, context)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function globalPreload (context) {
|
|
93
|
+
port = context.port
|
|
94
|
+
port.on('message', () => {
|
|
95
|
+
bustEsmCache()
|
|
96
|
+
clearCjsCache()
|
|
97
|
+
port.postMessage('plt:cache-cleared')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
return 'globalThis.LOADER_PORT = port;'
|
|
101
|
+
}
|