@platformatic/runtime 0.27.0 → 0.28.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/lib/loader.mjs CHANGED
@@ -1,16 +1,17 @@
1
1
  import { createRequire, isBuiltin } from 'node:module'
2
2
  import { dirname, isAbsolute, resolve as pathResolve } from 'node:path'
3
- import { fileURLToPath } from 'node:url'
3
+ import { fileURLToPath, pathToFileURL } from 'node:url'
4
4
  const require = createRequire(import.meta.url)
5
- const thisFile = fileURLToPath(import.meta.url)
6
5
  const isWindows = process.platform === 'win32'
7
6
  let timestamp = process.hrtime.bigint()
8
7
  let port
9
8
 
9
+ /* c8 ignore next 3 - c8 upgrade marked many existing things as uncovered */
10
10
  function bustEsmCache () {
11
11
  timestamp = process.hrtime.bigint()
12
12
  }
13
13
 
14
+ /* c8 ignore next 11 - c8 upgrade marked many existing things as uncovered */
14
15
  function clearCjsCache () {
15
16
  // This evicts all of the modules from the require() cache.
16
17
  // Note: This does not clean up children references to the deleted module.
@@ -25,6 +26,7 @@ function clearCjsCache () {
25
26
 
26
27
  function isRelativePath (p) {
27
28
  // This function is extracted from Node core, so it should work.
29
+ /* c8 ignore next - c8 upgrade marked many existing things as uncovered */
28
30
  return p.charAt(0) === '.' &&
29
31
  /* c8 ignore next 9 */
30
32
  (
@@ -38,15 +40,15 @@ function isRelativePath (p) {
38
40
  )
39
41
  }
40
42
 
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.
43
+ function specifierToFileUrl (specifier, referencingModuleId) {
44
+ // Convert the specifier into an absolute path URL if possible. If the
45
+ // specifier cannot be converted to a path (for example for a core module),
46
+ // then return null.
45
47
  try {
46
48
  const url = new URL(specifier)
47
49
 
48
50
  if (url.protocol === 'file:') {
49
- specifier = url.pathname
51
+ return url.href
50
52
  } else {
51
53
  return null
52
54
  }
@@ -54,12 +56,14 @@ function specifierToPath (specifier, referencingModuleId) {
54
56
  // Ignore error.
55
57
  }
56
58
 
59
+ /* c8 ignore next 3 - c8 upgrade marked many existing things as uncovered */
57
60
  if (isBuiltin(specifier)) {
58
61
  return null
59
62
  }
60
63
 
64
+ /* c8 ignore next 3 */
61
65
  if (isAbsolute(specifier)) {
62
- return specifier
66
+ return pathToFileURL(specifier).href
63
67
  }
64
68
 
65
69
  /* c8 ignore next 3 */
@@ -67,31 +71,35 @@ function specifierToPath (specifier, referencingModuleId) {
67
71
  throw new Error(`cannot map '${specifier}' to an absolute path`)
68
72
  }
69
73
 
74
+ /* c8 ignore next 5 - c8 upgrade marked many existing things as uncovered */
70
75
  if (isRelativePath(specifier)) {
71
- return pathResolve(dirname(fileURLToPath(referencingModuleId)), specifier)
76
+ return pathToFileURL(
77
+ pathResolve(dirname(fileURLToPath(referencingModuleId)), specifier)
78
+ ).href
72
79
  } else {
73
80
  // The specifier is something in node_modules/.
74
81
  const req = createRequire(referencingModuleId)
75
82
 
76
- return req.resolve(specifier)
83
+ return pathToFileURL(req.resolve(specifier)).href
77
84
  }
78
85
  }
79
86
 
80
87
  export async function resolve (specifier, context, nextResolve) {
81
- const path = specifierToPath(specifier, context.parentURL)
88
+ const url = specifierToFileUrl(specifier, context.parentURL)
82
89
 
83
90
  // If the specifier could not be mapped to a file, or the path is this file,
84
91
  // then don't do anything.
85
- if (typeof path !== 'string' || path === thisFile) {
92
+ if (typeof url !== 'string' || url === import.meta.url) {
86
93
  return nextResolve(specifier, context)
87
94
  }
88
95
 
89
- return nextResolve(`${path}?ts=${timestamp}`, context)
96
+ return nextResolve(`${url}?ts=${timestamp}`, context)
90
97
  }
91
98
 
92
99
  export function globalPreload (context) {
93
100
  port = context.port
94
101
  port.on('message', () => {
102
+ /* c8 ignore next 3 - c8 upgrade marked many existing things as uncovered */
95
103
  bustEsmCache()
96
104
  clearCjsCache()
97
105
  port.postMessage('plt:cache-cleared')
package/lib/start.js CHANGED
@@ -1,11 +1,13 @@
1
1
  'use strict'
2
2
  const { once } = require('node:events')
3
3
  const { join } = require('node:path')
4
+ const { pathToFileURL } = require('node:url')
4
5
  const { Worker } = require('node:worker_threads')
5
6
  const closeWithGrace = require('close-with-grace')
6
7
  const { loadConfig } = require('@platformatic/service')
7
8
  const { platformaticRuntime } = require('./config')
8
- const kLoaderFile = join(__dirname, 'loader.mjs')
9
+ const { RuntimeApiClient } = require('./api.js')
10
+ const kLoaderFile = pathToFileURL(join(__dirname, 'loader.mjs')).href
9
11
  const kWorkerFile = join(__dirname, 'worker.js')
10
12
  const kWorkerExecArgv = [
11
13
  '--no-warnings',
@@ -56,22 +58,8 @@ async function startWithConfig (configManager) {
56
58
 
57
59
  await once(worker, 'message') // plt:init
58
60
 
59
- return {
60
- async start () {
61
- worker.postMessage({ msg: 'plt:start' })
62
- const [msg] = await once(worker, 'message') // plt:started
63
-
64
- return msg.url
65
- },
66
- async close () {
67
- worker.postMessage({ msg: 'plt:stop' })
68
- await once(worker, 'exit')
69
- },
70
- async restart () {
71
- worker.postMessage({ msg: 'plt:restart' })
72
- await once(worker, 'message') // plt:restarted
73
- }
74
- }
61
+ const runtimeApiClient = new RuntimeApiClient(worker)
62
+ return runtimeApiClient
75
63
  }
76
64
 
77
65
  module.exports = { start, startWithConfig }
@@ -18,9 +18,12 @@ const {
18
18
  platformaticComposer
19
19
  } = require('@platformatic/composer')
20
20
  const { buildServer: runtimeBuildServer } = require('./build-server')
21
- const { platformaticRuntime } = require('./config')
21
+ const { platformaticRuntime, wrapConfigInRuntimeConfig } = require('./config')
22
22
  const { schema: runtimeSchema } = require('./schema')
23
- const { start: runtimeStart } = require('./start')
23
+ const {
24
+ start: runtimeStart,
25
+ startWithConfig: runtimeStartWithConfig
26
+ } = require('./start')
24
27
 
25
28
  const kSupportedAppTypes = new Set(['service', 'db', 'composer', 'runtime'])
26
29
 
@@ -149,15 +152,38 @@ async function startCommand (args) {
149
152
  try {
150
153
  await _start(args)
151
154
  } catch (err) {
152
- delete err?.stack
153
- console.error(err?.message)
155
+ logErrorAndExit(err)
156
+ }
157
+ }
154
158
 
155
- if (err?.cause) {
156
- console.error(`${err.cause}`)
159
+ async function startCommandInRuntime (args) {
160
+ try {
161
+ const configType = await getConfigType(args)
162
+ const config = await _loadConfig({}, args, configType)
163
+ let runtime
164
+
165
+ if (configType === 'runtime') {
166
+ runtime = await runtimeStartWithConfig(config.configManager)
167
+ } else {
168
+ const wrappedConfig = await wrapConfigInRuntimeConfig(config)
169
+ runtime = await runtimeStartWithConfig(wrappedConfig)
157
170
  }
158
171
 
159
- process.exit(1)
172
+ return await runtime.start()
173
+ } catch (err) {
174
+ logErrorAndExit(err)
175
+ }
176
+ }
177
+
178
+ function logErrorAndExit (err) {
179
+ delete err?.stack
180
+ console.error(err?.message)
181
+
182
+ if (err?.cause) {
183
+ console.error(`${err.cause}`)
160
184
  }
185
+
186
+ process.exit(1)
161
187
  }
162
188
 
163
189
  module.exports = {
@@ -167,5 +193,6 @@ module.exports = {
167
193
  loadConfig: _loadConfig,
168
194
  start: _start,
169
195
  startCommand,
196
+ startCommandInRuntime,
170
197
  getApp
171
198
  }
package/lib/worker.js CHANGED
@@ -1,8 +1,11 @@
1
1
  'use strict'
2
+
2
3
  const { parentPort, workerData } = require('node:worker_threads')
3
4
  const FastifyUndiciDispatcher = require('fastify-undici-dispatcher')
4
5
  const { Agent, setGlobalDispatcher } = require('undici')
5
6
  const { PlatformaticApp } = require('./app')
7
+ const { RuntimeApi } = require('./api')
8
+
6
9
  const loaderPort = globalThis.LOADER_PORT // Added by loader.mjs.
7
10
  const globalAgent = new Agent()
8
11
  const globalDispatcher = new FastifyUndiciDispatcher({
@@ -14,7 +17,6 @@ const pino = require('pino')
14
17
  const { isatty } = require('tty')
15
18
 
16
19
  const applications = new Map()
17
- let entrypoint
18
20
 
19
21
  delete globalThis.LOADER_PORT
20
22
  setGlobalDispatcher(globalDispatcher)
@@ -30,6 +32,7 @@ if (isatty(1)) {
30
32
 
31
33
  const logger = pino(transport)
32
34
 
35
+ /* c8 ignore next 4 */
33
36
  process.once('uncaughtException', (err) => {
34
37
  logger.error({ err }, 'runtime error')
35
38
  throw err
@@ -42,38 +45,6 @@ process.once('unhandledRejection', (err) => {
42
45
  throw err
43
46
  })
44
47
 
45
- parentPort.on('message', async (msg) => {
46
- for (const app of applications.values()) {
47
- await app.handleProcessLevelEvent(msg)
48
-
49
- if (msg?.msg === 'plt:start' || msg?.msg === 'plt:restart') {
50
- const serviceUrl = new URL(app.appConfig.localUrl)
51
-
52
- globalDispatcher.route(serviceUrl.host, app.server)
53
- }
54
- }
55
-
56
- switch (msg?.msg) {
57
- case 'plt:start':
58
- configureDispatcher()
59
- parentPort.postMessage({ msg: 'plt:started', url: entrypoint.server.url })
60
- break
61
- case 'plt:restart':
62
- configureDispatcher()
63
- parentPort.postMessage({ msg: 'plt:restarted', url: entrypoint.server.url })
64
- break
65
- case 'plt:stop':
66
- process.exit() // Exit the worker thread.
67
- break
68
- /* c8 ignore next 3 */
69
- case undefined:
70
- // Ignore
71
- break
72
- default:
73
- throw new Error(`unknown message type: '${msg.msg}'`)
74
- }
75
- })
76
-
77
48
  async function main () {
78
49
  const { services } = workerData.config
79
50
 
@@ -82,34 +53,12 @@ async function main () {
82
53
  const app = new PlatformaticApp(service, loaderPort, logger)
83
54
 
84
55
  applications.set(service.id, app)
85
-
86
- if (service.entrypoint) {
87
- entrypoint = app
88
- }
89
56
  }
90
57
 
91
- parentPort.postMessage('plt:init')
92
- }
93
-
94
- function configureDispatcher () {
95
- const { services } = workerData.config
58
+ const runtime = new RuntimeApi(applications, globalDispatcher)
59
+ runtime.startListening(parentPort)
96
60
 
97
- // Setup the local services in the global dispatcher.
98
- for (let i = 0; i < services.length; ++i) {
99
- const service = services[i]
100
- const serviceApp = applications.get(service.id)
101
- const serviceUrl = new URL(service.localUrl)
102
-
103
- globalDispatcher.route(serviceUrl.host, serviceApp.server)
104
-
105
- for (let j = 0; j < service.dependencies.length; ++j) {
106
- const depConfig = service.dependencies[j]
107
- const depApp = applications.get(depConfig.id)
108
- const depUrl = new URL(depConfig.url)
109
-
110
- globalDispatcher.route(depUrl.host, depApp.server)
111
- }
112
- }
61
+ parentPort.postMessage('plt:init')
113
62
  }
114
63
 
115
64
  main()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "0.27.0",
3
+ "version": "0.28.1",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -37,11 +37,11 @@
37
37
  "pino": "^8.14.1",
38
38
  "pino-pretty": "^10.0.0",
39
39
  "undici": "^5.22.1",
40
- "@platformatic/composer": "0.27.0",
41
- "@platformatic/config": "0.27.0",
42
- "@platformatic/db": "0.27.0",
43
- "@platformatic/service": "0.27.0",
44
- "@platformatic/utils": "0.27.0"
40
+ "@platformatic/composer": "0.28.1",
41
+ "@platformatic/config": "0.28.1",
42
+ "@platformatic/db": "0.28.1",
43
+ "@platformatic/service": "0.28.1",
44
+ "@platformatic/utils": "0.28.1"
45
45
  },
46
46
  "standard": {
47
47
  "ignore": [
@@ -0,0 +1,294 @@
1
+ 'use strict'
2
+
3
+ const assert = require('node:assert')
4
+ const { join } = require('node:path')
5
+ const { test } = require('node:test')
6
+
7
+ const { loadConfig } = require('@platformatic/service')
8
+ const { buildServer, platformaticRuntime } = require('..')
9
+ const fixturesDir = join(__dirname, '..', 'fixtures')
10
+
11
+ // Each test runtime app adds own process listeners
12
+ process.setMaxListeners(100)
13
+
14
+ test('should get service details', async (t) => {
15
+ const configFile = join(fixturesDir, 'configs', 'monorepo.json')
16
+ const config = await loadConfig({}, ['-c', configFile], platformaticRuntime)
17
+ const app = await buildServer(config.configManager.current)
18
+
19
+ await app.start()
20
+
21
+ t.after(async () => {
22
+ await app.close()
23
+ })
24
+
25
+ const serviceDetails = await app.getServiceDetails('with-logger')
26
+ assert.deepStrictEqual(serviceDetails, {
27
+ id: 'with-logger',
28
+ status: 'started',
29
+ entrypoint: false,
30
+ localUrl: 'http://with-logger.plt.local',
31
+ dependencies: []
32
+ })
33
+ })
34
+
35
+ test('should get service config', async (t) => {
36
+ const configFile = join(fixturesDir, 'configs', 'monorepo.json')
37
+ const config = await loadConfig({}, ['-c', configFile], platformaticRuntime)
38
+ const app = await buildServer(config.configManager.current)
39
+
40
+ await app.start()
41
+
42
+ t.after(async () => {
43
+ await app.close()
44
+ })
45
+
46
+ const serviceConfig = await app.getServiceConfig('with-logger')
47
+
48
+ // TODO: should return correct logger config
49
+ assert.deepStrictEqual(serviceConfig, {
50
+ $schema: 'https://platformatic.dev/schemas/v0.27.0/service',
51
+ server: {
52
+ hostname: '127.0.0.1',
53
+ port: 0,
54
+ logger: {},
55
+ keepAliveTimeout: 5000
56
+ },
57
+ service: { openapi: true },
58
+ plugins: {
59
+ paths: [
60
+ join(fixturesDir, 'monorepo', 'serviceAppWithLogger', 'plugin.js')
61
+ ]
62
+ },
63
+ watch: false
64
+ })
65
+ })
66
+
67
+ test('should fail to get service config if service is not started', async (t) => {
68
+ const configFile = join(fixturesDir, 'configs', 'monorepo.json')
69
+ const config = await loadConfig({}, ['-c', configFile], platformaticRuntime)
70
+ const app = await buildServer(config.configManager.current)
71
+
72
+ t.after(async () => {
73
+ await app.close()
74
+ })
75
+
76
+ try {
77
+ await app.getServiceConfig('with-logger')
78
+ assert.fail('should have thrown')
79
+ } catch (err) {
80
+ assert.strictEqual(err.message, 'Service with id \'with-logger\' is not started')
81
+ }
82
+ })
83
+
84
+ test('should get services topology', async (t) => {
85
+ const configFile = join(fixturesDir, 'configs', 'monorepo.json')
86
+ const config = await loadConfig({}, ['-c', configFile], platformaticRuntime)
87
+ const app = await buildServer(config.configManager.current)
88
+
89
+ await app.start()
90
+
91
+ t.after(async () => {
92
+ await app.close()
93
+ })
94
+
95
+ const topology = await app.getServices()
96
+
97
+ assert.deepStrictEqual(topology, {
98
+ entrypoint: 'serviceApp',
99
+ services: [
100
+ {
101
+ id: 'serviceApp',
102
+ status: 'started',
103
+ entrypoint: true,
104
+ localUrl: 'http://serviceApp.plt.local',
105
+ dependencies: [
106
+ {
107
+ id: 'with-logger',
108
+ url: 'http://with-logger.plt.local',
109
+ local: true
110
+ }
111
+ ]
112
+ },
113
+ {
114
+ id: 'with-logger',
115
+ status: 'started',
116
+ entrypoint: false,
117
+ localUrl: 'http://with-logger.plt.local',
118
+ dependencies: []
119
+ },
120
+ {
121
+ id: 'multi-plugin-service',
122
+ status: 'started',
123
+ entrypoint: false,
124
+ localUrl: 'http://multi-plugin-service.plt.local',
125
+ dependencies: []
126
+ }
127
+ ]
128
+ })
129
+ })
130
+
131
+ test('should stop service by service id', async (t) => {
132
+ const configFile = join(fixturesDir, 'configs', 'monorepo.json')
133
+ const config = await loadConfig({}, ['-c', configFile], platformaticRuntime)
134
+ const app = await buildServer(config.configManager.current)
135
+
136
+ await app.start()
137
+
138
+ t.after(async () => {
139
+ await app.close()
140
+ })
141
+
142
+ {
143
+ const serviceDetails = await app.getServiceDetails('with-logger')
144
+ assert.strictEqual(serviceDetails.status, 'started')
145
+ }
146
+
147
+ await app.stopService('with-logger')
148
+
149
+ {
150
+ const serviceDetails = await app.getServiceDetails('with-logger')
151
+ assert.strictEqual(serviceDetails.status, 'stopped')
152
+ }
153
+ })
154
+
155
+ test('should fail to stop service with a wrong id', async (t) => {
156
+ const configFile = join(fixturesDir, 'configs', 'monorepo.json')
157
+ const config = await loadConfig({}, ['-c', configFile], platformaticRuntime)
158
+ const app = await buildServer(config.configManager.current)
159
+
160
+ t.after(async () => {
161
+ await app.close()
162
+ })
163
+
164
+ try {
165
+ await app.stopService('wrong-service-id')
166
+ assert.fail('should have thrown')
167
+ } catch (err) {
168
+ assert.strictEqual(err.message, 'Service with id \'wrong-service-id\' not found')
169
+ }
170
+ })
171
+
172
+ test('should start stopped service by service id', async (t) => {
173
+ const configFile = join(fixturesDir, 'configs', 'monorepo.json')
174
+ const config = await loadConfig({}, ['-c', configFile], platformaticRuntime)
175
+ const app = await buildServer(config.configManager.current)
176
+
177
+ await app.start()
178
+
179
+ t.after(async () => {
180
+ await app.close()
181
+ })
182
+
183
+ await app.stopService('with-logger')
184
+
185
+ {
186
+ const serviceDetails = await app.getServiceDetails('with-logger')
187
+ assert.strictEqual(serviceDetails.status, 'stopped')
188
+ }
189
+
190
+ await app.startService('with-logger')
191
+
192
+ {
193
+ const serviceDetails = await app.getServiceDetails('with-logger')
194
+ assert.strictEqual(serviceDetails.status, 'started')
195
+ }
196
+ })
197
+
198
+ test('should fail to start service with a wrong id', async (t) => {
199
+ const configFile = join(fixturesDir, 'configs', 'monorepo.json')
200
+ const config = await loadConfig({}, ['-c', configFile], platformaticRuntime)
201
+ const app = await buildServer(config.configManager.current)
202
+
203
+ t.after(async () => {
204
+ await app.close()
205
+ })
206
+
207
+ try {
208
+ await app.startService('wrong-service-id')
209
+ assert.fail('should have thrown')
210
+ } catch (err) {
211
+ assert.strictEqual(err.message, 'Service with id \'wrong-service-id\' not found')
212
+ }
213
+ })
214
+
215
+ test('should fail to start running service', async (t) => {
216
+ const configFile = join(fixturesDir, 'configs', 'monorepo.json')
217
+ const config = await loadConfig({}, ['-c', configFile], platformaticRuntime)
218
+ const app = await buildServer(config.configManager.current)
219
+
220
+ await app.start()
221
+
222
+ t.after(async () => {
223
+ await app.close()
224
+ })
225
+
226
+ try {
227
+ await app.startService('with-logger')
228
+ assert.fail('should have thrown')
229
+ } catch (err) {
230
+ assert.strictEqual(err.message, 'application is already started')
231
+ }
232
+ })
233
+
234
+ test('should inject request to service', async (t) => {
235
+ const configFile = join(fixturesDir, 'configs', 'monorepo.json')
236
+ const config = await loadConfig({}, ['-c', configFile], platformaticRuntime)
237
+ const app = await buildServer(config.configManager.current)
238
+
239
+ await app.start()
240
+
241
+ t.after(async () => {
242
+ await app.close()
243
+ })
244
+
245
+ const res = await app.inject('with-logger', {
246
+ method: 'GET',
247
+ url: '/'
248
+ })
249
+
250
+ assert.strictEqual(res.statusCode, 200)
251
+ assert.strictEqual(res.statusMessage, 'OK')
252
+
253
+ assert.strictEqual(res.headers['content-type'], 'application/json; charset=utf-8')
254
+ assert.strictEqual(res.headers['content-length'], '17')
255
+ assert.strictEqual(res.headers.connection, 'keep-alive')
256
+
257
+ assert.strictEqual(res.body, '{"hello":"world"}')
258
+ assert.strictEqual(res.payload, '{"hello":"world"}')
259
+ })
260
+
261
+ test('should fail inject request is service is not started', async (t) => {
262
+ const configFile = join(fixturesDir, 'configs', 'monorepo.json')
263
+ const config = await loadConfig({}, ['-c', configFile], platformaticRuntime)
264
+ const app = await buildServer(config.configManager.current)
265
+
266
+ t.after(async () => {
267
+ await app.close()
268
+ })
269
+
270
+ try {
271
+ await app.inject('with-logger', { method: 'GET', url: '/' })
272
+ } catch (err) {
273
+ assert.strictEqual(err.message, 'Service with id \'with-logger\' is not started')
274
+ }
275
+ })
276
+
277
+ test('should handle a lot of runtime api requests', async (t) => {
278
+ const configFile = join(fixturesDir, 'configs', 'monorepo.json')
279
+ const config = await loadConfig({}, ['-c', configFile], platformaticRuntime)
280
+ const app = await buildServer(config.configManager.current)
281
+
282
+ await app.start()
283
+
284
+ t.after(async () => {
285
+ await app.close()
286
+ })
287
+
288
+ const promises = []
289
+ for (let i = 0; i < 100; i++) {
290
+ promises.push(app.getServiceDetails('with-logger'))
291
+ }
292
+
293
+ await Promise.all(promises)
294
+ })
package/test/app.test.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const assert = require('node:assert')
4
4
  const { join } = require('node:path')
5
5
  const { test } = require('node:test')
6
+ const { utimes } = require('node:fs/promises')
6
7
  const { PlatformaticApp } = require('../lib/app')
7
8
  const fixturesDir = join(__dirname, '..', 'fixtures')
8
9
  const pino = require('pino')
@@ -266,3 +267,42 @@ test('supports configuration overrides', async (t) => {
266
267
  assert.strictEqual(app.config.configManager.current.server.keepAliveTimeout, 1)
267
268
  })
268
269
  })
270
+
271
+ test('restarts on config change without overriding the configManager', { only: true }, async (t) => {
272
+ const { logger, stream } = getLoggerAndStream()
273
+ const appPath = join(fixturesDir, 'monorepo', 'serviceApp')
274
+ const configFile = join(appPath, 'platformatic.service.json')
275
+ const config = {
276
+ id: 'serviceApp',
277
+ config: configFile,
278
+ path: appPath,
279
+ entrypoint: true,
280
+ hotReload: true,
281
+ dependencies: [],
282
+ dependents: [],
283
+ localServiceEnvVars: new Map([['PLT_WITH_LOGGER_URL', ' ']])
284
+ }
285
+ const app = new PlatformaticApp(config, null, logger)
286
+
287
+ t.after(async function () {
288
+ try {
289
+ await app.stop()
290
+ } catch (err) {
291
+ console.error(err)
292
+ }
293
+ })
294
+ await app.start()
295
+ const configManager = app.config.configManager
296
+ await utimes(configFile, new Date(), new Date())
297
+ let first = false
298
+ for await (const log of stream) {
299
+ // Wait for the server to restart, it will print a line containing "Server listening"
300
+ if (log.msg.includes('listening')) {
301
+ if (first) {
302
+ break
303
+ }
304
+ first = true
305
+ }
306
+ }
307
+ assert.strictEqual(configManager, app.server.platformatic.configManager)
308
+ })
@@ -56,3 +56,12 @@ test('performs a topological sort on services depending on allowCycles', async (
56
56
  })
57
57
  })
58
58
  })
59
+
60
+ test('can resolve service id from client package.json if not provided', async () => {
61
+ const configFile = join(fixturesDir, 'configs', 'monorepo-client-without-id.json')
62
+ const config = await loadConfig({}, ['-c', configFile], platformaticRuntime)
63
+ const entry = config.configManager.current.serviceMap.get('serviceApp')
64
+
65
+ assert.strictEqual(entry.dependencies.length, 1)
66
+ assert.strictEqual(entry.dependencies[0].id, 'with-logger')
67
+ })