@platformatic/runtime 0.31.1 → 0.33.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @platformatic/runtime
2
2
 
3
- Check out the full documentation for Platformatic Runtime on [our website](https://oss.platformatic.dev/docs/getting-started/quick-start-guide).
3
+ Check out the full documentation for Platformatic Runtime on [our website](https://docs.platformatic.dev/docs/getting-started/quick-start-guide).
4
4
 
5
5
  ## Install
6
6
 
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://platformatic.dev/schemas/v0.32.0/runtime",
3
+ "entrypoint": "serviceApp",
4
+ "allowCycles": true,
5
+ "hotReload": true,
6
+ "autoload": {
7
+ "path": "../monorepo",
8
+ "exclude": ["docs", "composerApp"],
9
+ "mappings": {
10
+ "serviceAppWithLogger": {
11
+ "id": "with-logger",
12
+ "config": "platformatic.service.json"
13
+ },
14
+ "serviceAppWithMultiplePlugins": {
15
+ "id": "multi-plugin-service",
16
+ "config": "platformatic.service.json"
17
+ }
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,4 @@
1
+ PLT_SERVER_HOSTNAME=127.0.0.1
2
+ PORT=3043
3
+ PLT_SERVER_LOGGER_LEVEL=info
4
+ PLT_EXAMPLE=world
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "https://platformatic.dev/schemas/v0.31.0/runtime",
3
+ "entrypoint": "rainy-empire",
4
+ "allowCycles": false,
5
+ "hotReload": true,
6
+ "autoload": {
7
+ "path": "services",
8
+ "exclude": [
9
+ "docs"
10
+ ]
11
+ }
12
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://platformatic.dev/schemas/v0.31.0/service",
3
+ "server": {
4
+ "hostname": "{PLT_SERVER_HOSTNAME}",
5
+ "port": "{PORT}",
6
+ "logger": {
7
+ "level": "{PLT_SERVER_LOGGER_LEVEL}"
8
+ }
9
+ },
10
+ "service": {
11
+ "openapi": true
12
+ },
13
+ "plugins": {
14
+ "paths": [
15
+ {
16
+ "path": "./plugins",
17
+ "encapsulate": false,
18
+ "options": {
19
+ "example": "{PLT_EXAMPLE}"
20
+ }
21
+ },
22
+ "./routes"
23
+ ]
24
+ }
25
+ }
@@ -0,0 +1,6 @@
1
+ /// <reference types="@platformatic/service" />
2
+ 'use strict'
3
+ /** @param {import('fastify').FastifyInstance} fastify */
4
+ module.exports = async function (fastify, opts) {
5
+ fastify.decorate('example', 'foobar')
6
+ }
@@ -0,0 +1,8 @@
1
+ /// <reference types="@platformatic/service" />
2
+ 'use strict'
3
+ /** @param {import('fastify').FastifyInstance} fastify */
4
+ module.exports = async function (fastify, opts) {
5
+ fastify.get('/', async (request, reply) => {
6
+ return { hello: fastify.example }
7
+ })
8
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://platformatic.dev/schemas/v0.31.0/composer",
3
+ "server": {
4
+ "hostname": "{PLT_SERVER_HOSTNAME}",
5
+ "port": "{PORT}",
6
+ "logger": {
7
+ "level": "{PLT_SERVER_LOGGER_LEVEL}"
8
+ }
9
+ },
10
+ "composer": {
11
+ "services": [
12
+ {
13
+ "id": "deeply-splitte",
14
+ "openapi": {
15
+ "url": "/documentation/json"
16
+ }
17
+ }
18
+ ]
19
+ },
20
+ "watch": false
21
+ }
@@ -15,5 +15,6 @@
15
15
  "paths": [
16
16
  "plugin.js"
17
17
  ]
18
- }
18
+ },
19
+ "watch": true
19
20
  }
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://platformatic.dev/schemas/v0.31.0/service",
3
+ "server": {
4
+ "hostname": "{PLT_SERVER_HOSTNAME}",
5
+ "port": "{PORT}",
6
+ "logger": {
7
+ "level": "{PLT_SERVER_LOGGER_LEVEL}"
8
+ }
9
+ },
10
+ "service": {
11
+ "openapi": true
12
+ },
13
+ "plugins": {
14
+ "paths": [
15
+ {
16
+ "path": "./plugins",
17
+ "encapsulate": false,
18
+ "options": {
19
+ "example": "{PLT_EXAMPLE}"
20
+ }
21
+ },
22
+ "./routes"
23
+ ]
24
+ }
25
+ }
@@ -1,10 +1,10 @@
1
1
  'use strict'
2
2
  const assert = require('node:assert')
3
3
  const { request } = require('undici')
4
- const { startCommandInRuntime } = require('../lib/unified-api')
4
+ const { startCommand } = require('../lib/unified-api')
5
5
 
6
6
  async function main () {
7
- const entrypoint = await startCommandInRuntime(['-c', process.argv[2]])
7
+ const entrypoint = await startCommand(['-c', process.argv[2]])
8
8
  const endpoint = process.argv[3] ?? '/'
9
9
  const res = await request(entrypoint + endpoint)
10
10
 
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://platformatic.dev/schemas/v0.31.0/runtime",
3
+ "entrypoint": "echo",
4
+ "allowCycles": false,
5
+ "hotReload": true,
6
+ "autoload": {
7
+ "path": "services",
8
+ "exclude": [
9
+ "docs"
10
+ ]
11
+ },
12
+ "telemetry": {
13
+ "serviceName": "test-runtime",
14
+ "version": "1.0.0",
15
+ "exporter": {
16
+ "type": "memory"
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://platformatic.dev/schemas/v0.28.1/service",
3
+ "server": {
4
+ "hostname": "127.0.0.1",
5
+ "port": "0",
6
+ "logger": {
7
+ "level": "info"
8
+ }
9
+ },
10
+ "service": {
11
+ "openapi": true
12
+ },
13
+ "plugins": {
14
+ "paths": [
15
+ "./routes"
16
+ ],
17
+ "typescript": false
18
+ }
19
+ }
@@ -0,0 +1,8 @@
1
+ 'use strict'
2
+ module.exports = async function (fastify, opts) {
3
+ // This returns the traceId set on the span by the service
4
+ fastify.get('/', async (request, reply) => {
5
+ const traceId = request.span.spanContext().traceId
6
+ return { traceId }
7
+ })
8
+ }
package/lib/api.js CHANGED
@@ -11,10 +11,12 @@ class RuntimeApi {
11
11
 
12
12
  constructor (config, logger, loaderPort) {
13
13
  this.#services = new Map()
14
+ const telemetryConfig = config.telemetry
14
15
 
15
16
  for (let i = 0; i < config.services.length; ++i) {
16
17
  const service = config.services[i]
17
- const app = new PlatformaticApp(service, loaderPort, logger)
18
+ const serviceTelemetryConfig = telemetryConfig ? { ...telemetryConfig, serviceName: `${telemetryConfig.serviceName}-${service.id}` } : null
19
+ const app = new PlatformaticApp(service, loaderPort, logger, serviceTelemetryConfig)
18
20
 
19
21
  this.#services.set(service.id, app)
20
22
  }
@@ -47,9 +49,9 @@ class RuntimeApi {
47
49
  }
48
50
 
49
51
  async #handleProcessLevelEvent (message) {
50
- for (const service of this.#services.values()) {
52
+ await Promise.allSettled(this.#services.values().map(async (service) => {
51
53
  await service.handleProcessLevelEvent(message)
52
- }
54
+ }))
53
55
  }
54
56
 
55
57
  async #executeCommand (message) {
package/lib/app.js CHANGED
@@ -1,7 +1,9 @@
1
1
  'use strict'
2
+
2
3
  const { once } = require('node:events')
3
4
  const { dirname, basename } = require('node:path')
4
5
  const { FileWatcher } = require('@platformatic/utils')
6
+ const debounce = require('debounce')
5
7
  const {
6
8
  buildServer,
7
9
  loadConfig
@@ -13,9 +15,11 @@ class PlatformaticApp {
13
15
  #restarting
14
16
  #started
15
17
  #originalWatch
18
+ #fileWatcher
16
19
  #logger
20
+ #telemetryConfig
17
21
 
18
- constructor (appConfig, loaderPort, logger) {
22
+ constructor (appConfig, loaderPort, logger, telemetryConfig) {
19
23
  this.appConfig = appConfig
20
24
  this.config = null
21
25
  this.#hotReload = false
@@ -24,7 +28,11 @@ class PlatformaticApp {
24
28
  this.server = null
25
29
  this.#started = false
26
30
  this.#originalWatch = null
27
- this.#logger = logger
31
+ this.#fileWatcher = null
32
+ this.#logger = logger.child({
33
+ name: this.appConfig.id
34
+ })
35
+ this.#telemetryConfig = telemetryConfig
28
36
  }
29
37
 
30
38
  getStatus () {
@@ -36,16 +44,11 @@ class PlatformaticApp {
36
44
  }
37
45
 
38
46
  async restart (force) {
39
- if (this.#restarting) {
40
- return
41
- }
42
-
43
- if (!this.#hotReload && !force) {
47
+ if (this.#restarting || !this.#started || (!this.#hotReload && !force)) {
44
48
  return
45
49
  }
46
50
 
47
51
  this.#restarting = true
48
- await this.stop()
49
52
 
50
53
  /* c8 ignore next 4 - tests may not pass in a MessagePort. */
51
54
  if (this.#loaderPort) {
@@ -53,7 +56,13 @@ class PlatformaticApp {
53
56
  await once(this.#loaderPort, 'message')
54
57
  }
55
58
 
56
- await this.start()
59
+ this.#setuplogger(this.config.configManager)
60
+ try {
61
+ await this.server.restart()
62
+ } catch (err) {
63
+ this.#logAndExit(err)
64
+ }
65
+
57
66
  this.#restarting = false
58
67
  }
59
68
 
@@ -62,38 +71,37 @@ class PlatformaticApp {
62
71
  throw new Error('application is already started')
63
72
  }
64
73
 
65
- if (!this.#restarting) {
66
- await this.#initializeConfig()
67
- }
74
+ this.#started = true
75
+
76
+ await this.#initializeConfig()
77
+ this.#originalWatch = this.config.configManager.current.watch
78
+ this.config.configManager.current.watch = false
79
+
68
80
  const { configManager } = this.config
81
+ configManager.update({
82
+ ...configManager.current,
83
+ telemetry: this.#telemetryConfig
84
+ })
69
85
  const config = configManager.current
70
86
 
71
- this.#originalWatch = config.watch
72
- config.watch = false
73
87
  this.#setuplogger(configManager)
74
88
 
75
89
  try {
76
90
  // If this is a restart, have the fastify server restart itself. If this
77
91
  // is not a restart, then create a new server.
78
- if (this.#restarting) {
79
- await this.server.restart()
80
- } else {
81
- this.server = await buildServer({
82
- ...config,
83
- configManager
84
- })
85
- }
92
+ this.server = await buildServer({
93
+ ...config,
94
+ configManager
95
+ })
86
96
  } catch (err) {
87
97
  this.#logAndExit(err)
88
98
  }
89
99
 
90
- if (config.plugins !== undefined && this.#originalWatch !== false) {
100
+ if (config.plugins !== undefined) {
91
101
  this.#startFileWatching()
92
102
  }
93
103
 
94
- this.#started = true
95
-
96
- if (this.appConfig.entrypoint && !this.#restarting) {
104
+ if (this.appConfig.entrypoint) {
97
105
  try {
98
106
  await this.server.start()
99
107
  /* c8 ignore next 5 */
@@ -109,11 +117,9 @@ class PlatformaticApp {
109
117
  throw new Error('application has not been started')
110
118
  }
111
119
 
112
- if (!this.#restarting) {
113
- await this.server.close()
114
- }
115
-
116
120
  await this.#stopFileWatching()
121
+ await this.server.close()
122
+
117
123
  this.#started = false
118
124
  }
119
125
 
@@ -148,12 +154,19 @@ class PlatformaticApp {
148
154
  async #initializeConfig () {
149
155
  const appConfig = this.appConfig
150
156
 
151
- this.config = await loadConfig({}, ['-c', appConfig.config], null, {
152
- watch: true,
153
- onMissingEnv (key) {
154
- return appConfig.localServiceEnvVars.get(key)
155
- }
156
- })
157
+ let _config
158
+ try {
159
+ _config = await loadConfig({}, ['-c', appConfig.config], null, {
160
+ watch: true,
161
+ onMissingEnv (key) {
162
+ return appConfig.localServiceEnvVars.get(key)
163
+ }
164
+ })
165
+ } catch (err) {
166
+ this.#logAndExit(err)
167
+ }
168
+
169
+ this.config = _config
157
170
  const { configManager } = this.config
158
171
 
159
172
  function applyOverrides () {
@@ -190,11 +203,13 @@ class PlatformaticApp {
190
203
  this.#hotReload = this.appConfig.hotReload
191
204
 
192
205
  configManager.on('update', async (newConfig) => {
193
- this.server.platformatic.config = newConfig
194
- applyOverrides()
195
- this.server.log.debug('config changed')
196
- this.server.log.trace({ newConfig }, 'new config')
197
- await this.restart()
206
+ if (this.server) { // when we setup telemetry on config, we don't have a server yet
207
+ this.server.platformatic.config = newConfig
208
+ applyOverrides()
209
+ this.server.log.debug('config changed')
210
+ this.server.log.trace({ newConfig }, 'new config')
211
+ await this.restart()
212
+ }
198
213
  })
199
214
 
200
215
  configManager.on('error', (err) => {
@@ -206,14 +221,15 @@ class PlatformaticApp {
206
221
  #setuplogger (configManager) {
207
222
  // Set the logger if not present (and the config supports it).
208
223
  if (configManager.current.server) {
209
- const childLogger = this.#logger.child({
210
- name: this.appConfig.id
211
- }, { level: configManager.current.server.logger?.level || 'info' })
224
+ const childLogger = this.#logger.child({}, { level: configManager.current.server.logger?.level || 'info' })
212
225
  configManager.current.server.logger = childLogger
213
226
  }
214
227
  }
215
228
 
216
229
  #startFileWatching () {
230
+ if (this.#fileWatcher) {
231
+ return
232
+ }
217
233
  const server = this.server
218
234
  const { configManager } = server.platformatic
219
235
  // TODO FileWatcher and ConfigManager both watch the configuration file
@@ -226,27 +242,30 @@ class PlatformaticApp {
226
242
  })
227
243
 
228
244
  /* c8 ignore next 4 */
229
- fileWatcher.on('update', async () => {
230
- this.server.log.debug('files changed')
245
+ const restart = debounce(() => {
246
+ this.server.log.info('files changed')
231
247
  this.restart()
232
- })
248
+ }, 100) // debounce restart for 100ms
249
+ fileWatcher.on('update', restart)
233
250
 
234
251
  fileWatcher.startWatching()
235
252
  server.log.debug('start watching files')
236
253
  server.platformatic.fileWatcher = fileWatcher
237
254
  server.platformatic.configManager.startWatching()
255
+ this.#fileWatcher = fileWatcher
238
256
  }
239
257
 
240
258
  async #stopFileWatching () {
241
- const watcher = this.server.platformatic.fileWatcher
242
259
  // The configManager automatically watches for the config file changes
243
260
  // therefore we need to stop it all the times.
244
261
  await this.config.configManager.stopWatching()
245
262
 
263
+ const watcher = this.#fileWatcher
246
264
  if (watcher) {
247
265
  this.server.log.debug('stop watching files')
248
266
  await watcher.stopWatching()
249
267
  this.server.platformatic.fileWatcher = undefined
268
+ this.#fileWatcher = null
250
269
  }
251
270
  }
252
271
 
package/lib/config.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
  const { readFile, readdir } = require('node:fs/promises')
3
3
  const { basename, join, resolve: pathResolve } = require('node:path')
4
+ const { closest } = require('fastest-levenshtein')
4
5
  const Topo = require('@hapi/topo')
5
6
  const ConfigManager = require('@platformatic/config')
6
7
  const { schema } = require('./schema')
@@ -71,6 +72,15 @@ async function _transformConfig (configManager) {
71
72
  }
72
73
  }
73
74
 
75
+ function missingDependencyErrorMessage (clientName, service, configManager) {
76
+ const closestName = closest(clientName, [...configManager.current.serviceMap.keys()])
77
+ let errorMsg = `service '${service.id}' has unknown dependency: '${clientName}'.`
78
+ if (closestName) {
79
+ errorMsg += ` Did you mean '${closestName}'?`
80
+ }
81
+ return errorMsg
82
+ }
83
+
74
84
  async function parseClientsAndComposer (configManager) {
75
85
  for (let i = 0; i < configManager.current.services.length; ++i) {
76
86
  const service = configManager.current.services[i]
@@ -86,8 +96,7 @@ async function parseClientsAndComposer (configManager) {
86
96
  const dependency = configManager.current.serviceMap.get(clientName)
87
97
 
88
98
  if (dependency === undefined) {
89
- /* c8 ignore next 2 */
90
- throw new Error(`service '${service.id}' has unknown dependency: '${clientName}'`)
99
+ throw new Error(missingDependencyErrorMessage(clientName, service, configManager))
91
100
  }
92
101
 
93
102
  dependency.dependents.push(service.id)
@@ -170,7 +179,7 @@ async function parseClientsAndComposer (configManager) {
170
179
 
171
180
  /* c8 ignore next 4 */
172
181
  if (dependency === undefined) {
173
- reject(new Error(`service '${service.id}' has unknown dependency: '${clientName}'`))
182
+ reject(new Error(missingDependencyErrorMessage(clientName, service, configManager)))
174
183
  return
175
184
  }
176
185
 
package/lib/schema.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #! /usr/bin/env node
2
2
  'use strict'
3
3
 
4
+ const telemetry = require('@platformatic/telemetry').schema
4
5
  const pkg = require('../package.json')
5
6
  const version = 'v' + pkg.version
6
7
  const platformaticRuntimeSchema = {
@@ -42,6 +43,7 @@ const platformaticRuntimeSchema = {
42
43
  }
43
44
  }
44
45
  },
46
+ telemetry,
45
47
  services: {
46
48
  type: 'array',
47
49
  default: [],
@@ -67,7 +69,14 @@ const platformaticRuntimeSchema = {
67
69
  type: 'string'
68
70
  },
69
71
  hotReload: {
70
- type: 'boolean'
72
+ anyOf: [
73
+ {
74
+ type: 'boolean'
75
+ },
76
+ {
77
+ type: 'string'
78
+ }
79
+ ]
71
80
  },
72
81
  allowCycles: {
73
82
  type: 'boolean'
package/lib/start.js CHANGED
@@ -46,8 +46,12 @@ async function startWithConfig (configManager, env = process.env) {
46
46
  env
47
47
  })
48
48
 
49
+ let exited = null
49
50
  worker.on('exit', () => {
50
51
  configManager.fileWatcher?.stopWatching()
52
+ if (typeof exited === 'function') {
53
+ exited()
54
+ }
51
55
  })
52
56
 
53
57
  worker.on('error', () => {
@@ -65,8 +69,9 @@ async function startWithConfig (configManager, env = process.env) {
65
69
  worker.postMessage({ signal: 'SIGUSR2' })
66
70
  })
67
71
 
68
- closeWithGrace((event) => {
72
+ closeWithGrace((event, cb) => {
69
73
  worker.postMessage(event)
74
+ exited = cb
70
75
  })
71
76
 
72
77
  /* c8 ignore next 3 */
@@ -154,14 +154,6 @@ async function _start (args) {
154
154
  }
155
155
 
156
156
  async function startCommand (args) {
157
- try {
158
- await _start(args)
159
- } catch (err) {
160
- logErrorAndExit(err)
161
- }
162
- }
163
-
164
- async function startCommandInRuntime (args) {
165
157
  try {
166
158
  const configType = await getConfigType(args)
167
159
  const config = await _loadConfig({}, args, configType)
@@ -200,6 +192,5 @@ module.exports = {
200
192
  loadConfig: _loadConfig,
201
193
  start: _start,
202
194
  startCommand,
203
- startCommandInRuntime,
204
195
  getApp
205
196
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "0.31.1",
3
+ "version": "0.33.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -24,15 +24,17 @@
24
24
  "standard": "^17.1.0",
25
25
  "tsd": "^0.28.1",
26
26
  "typescript": "^5.1.6",
27
- "@platformatic/sql-mapper": "0.31.1",
28
- "@platformatic/sql-graphql": "0.31.1"
27
+ "@platformatic/sql-graphql": "0.33.0",
28
+ "@platformatic/sql-mapper": "0.33.0"
29
29
  },
30
30
  "dependencies": {
31
31
  "@hapi/topo": "^6.0.2",
32
32
  "close-with-grace": "^1.2.0",
33
33
  "commist": "^3.2.0",
34
+ "debounce": "^1.2.1",
34
35
  "desm": "^1.3.0",
35
36
  "es-main": "^1.2.0",
37
+ "fastest-levenshtein": "^1.0.16",
36
38
  "fastify": "^4.18.0",
37
39
  "fastify-undici-dispatcher": "^0.4.1",
38
40
  "help-me": "^4.2.0",
@@ -40,11 +42,12 @@
40
42
  "pino": "^8.14.1",
41
43
  "pino-pretty": "^10.0.0",
42
44
  "undici": "^5.22.1",
43
- "@platformatic/composer": "0.31.1",
44
- "@platformatic/config": "0.31.1",
45
- "@platformatic/db": "0.31.1",
46
- "@platformatic/service": "0.31.1",
47
- "@platformatic/utils": "0.31.1"
45
+ "@platformatic/composer": "0.33.0",
46
+ "@platformatic/config": "0.33.0",
47
+ "@platformatic/db": "0.33.0",
48
+ "@platformatic/service": "0.33.0",
49
+ "@platformatic/telemetry": "0.33.0",
50
+ "@platformatic/utils": "0.33.0"
48
51
  },
49
52
  "standard": {
50
53
  "ignore": [
@@ -53,7 +56,8 @@
53
56
  ]
54
57
  },
55
58
  "scripts": {
56
- "test": "npm run lint && c8 -x fixtures -x test node --test && tsd",
59
+ "test": "npm run lint && node --test && tsd",
60
+ "coverage": "npm run lint && c8 -x fixtures -x test node --test && tsd",
57
61
  "lint": "standard | snazzy"
58
62
  }
59
63
  }
package/runtime.mjs CHANGED
@@ -6,7 +6,7 @@ import { join } from 'desm'
6
6
  import isMain from 'es-main'
7
7
  import helpMe from 'help-me'
8
8
  import parseArgs from 'minimist'
9
- import { start } from './lib/start.js'
9
+ import { startCommand } from './lib/unified-api.js'
10
10
  import { compile as compileCmd } from './lib/compile.js'
11
11
 
12
12
  export const compile = compileCmd
@@ -22,7 +22,7 @@ const program = commist({ maxDistance: 2 })
22
22
  program.register('help', help.toStdout)
23
23
  program.register('help start', help.toStdout.bind(null, ['start']))
24
24
  program.register('help compile', help.toStdout.bind(null, ['compile']))
25
- program.register('start', start)
25
+ program.register('start', startCommand)
26
26
  program.register('compile', compile)
27
27
 
28
28
  export async function run (argv) {
package/test/app.test.js CHANGED
@@ -90,7 +90,7 @@ test('errors when stopping an already stopped application', async (t) => {
90
90
  })
91
91
 
92
92
  test('does not restart while restarting', async (t) => {
93
- const { logger } = getLoggerAndStream()
93
+ const { logger, stream } = getLoggerAndStream()
94
94
  const appPath = join(fixturesDir, 'monorepo', 'serviceApp')
95
95
  const configFile = join(appPath, 'platformatic.service.json')
96
96
  const config = {
@@ -105,17 +105,33 @@ test('does not restart while restarting', async (t) => {
105
105
  }
106
106
  const app = new PlatformaticApp(config, null, logger)
107
107
 
108
- t.after(app.stop.bind(app))
108
+ t.after(async () => {
109
+ try {
110
+ await app.stop()
111
+ } catch {}
112
+ })
109
113
  await app.start()
110
- t.mock.method(app, 'stop')
111
114
  await Promise.all([
112
115
  app.restart(),
113
116
  app.restart(),
114
117
  app.restart()
115
118
  ])
119
+ await app.stop()
120
+ stream.end()
121
+ const lines = []
122
+ for await (const line of stream) {
123
+ lines.push(line)
124
+ }
116
125
 
117
- // stop() should have only been called once despite three restart() calls.
118
- assert.strictEqual(app.stop.mock.calls.length, 1)
126
+ let count = 0
127
+ for (const line of lines) {
128
+ // every time we restart we log listening
129
+ if (line.msg.match(/listening/)) {
130
+ count++
131
+ }
132
+ }
133
+
134
+ assert.strictEqual(count, 2)
119
135
  })
120
136
 
121
137
  test('restarts on SIGUSR2', async (t) => {
@@ -308,3 +324,35 @@ test('restarts on config change without overriding the configManager', async (t)
308
324
  }
309
325
  assert.strictEqual(configManager, app.server.platformatic.configManager)
310
326
  })
327
+
328
+ test('logs errors if an env variable is missing', async (t) => {
329
+ const { logger, stream } = getLoggerAndStream()
330
+ const configFile = join(fixturesDir, 'no-env.service.json')
331
+ const config = {
332
+ id: 'no-env',
333
+ config: configFile,
334
+ path: fixturesDir,
335
+ entrypoint: true,
336
+ hotReload: true
337
+ }
338
+ const app = new PlatformaticApp(config, null, logger)
339
+
340
+ t.mock.method(process, 'exit', () => {
341
+ throw new Error('exited')
342
+ })
343
+
344
+ await assert.rejects(async () => {
345
+ await app.start()
346
+ }, /exited/)
347
+ assert.strictEqual(process.exit.mock.calls.length, 1)
348
+ assert.strictEqual(process.exit.mock.calls[0].arguments[0], 1)
349
+
350
+ stream.end()
351
+ const lines = []
352
+ for await (const line of stream) {
353
+ lines.push(line)
354
+ }
355
+ const lastLine = lines[lines.length - 1]
356
+ assert.strictEqual(lastLine.name, 'no-env')
357
+ assert.strictEqual(lastLine.msg, 'Cannot parse config file. Cannot read properties of undefined (reading \'get\')')
358
+ })
@@ -0,0 +1,69 @@
1
+ import { test } from 'node:test'
2
+ import assert from 'node:assert'
3
+ import { join } from 'desm'
4
+ import path from 'node:path'
5
+ import { cliPath, delDir } from './helper.mjs'
6
+ import { execa } from 'execa'
7
+ import { mkdtemp, cp, mkdir } from 'node:fs/promises'
8
+
9
+ const base = join(import.meta.url, '..', 'tmp')
10
+
11
+ try {
12
+ await mkdir(base, { recursive: true })
13
+ } catch {
14
+ }
15
+
16
+ test('compile with tsconfig custom flags', async (t) => {
17
+ const tmpDir = await mkdtemp(path.join(base, 'test-runtime-compile-'))
18
+ const prev = process.cwd()
19
+ process.chdir(tmpDir)
20
+ t.after(() => {
21
+ process.chdir(prev)
22
+ })
23
+
24
+ t.after(delDir(tmpDir))
25
+
26
+ const folder = join(import.meta.url, '..', '..', 'fixtures', 'typescript-custom-flags')
27
+ await cp(folder, tmpDir, { recursive: true })
28
+
29
+ const { stdout } = await execa(cliPath, ['compile'])
30
+
31
+ const lines = stdout.split('\n').map(JSON.parse)
32
+ const expected = [{
33
+ name: 'movies',
34
+ msg: 'Typescript compilation completed successfully.'
35
+ }, {
36
+ name: 'titles',
37
+ msg: 'Typescript compilation completed successfully.'
38
+ }]
39
+
40
+ for (let i = 0; i < expected.length; i++) {
41
+ assert.deepStrictEqual(lines[i].name, expected[i].name)
42
+ assert.deepStrictEqual(lines[i].msg, expected[i].msg)
43
+ }
44
+ })
45
+
46
+ test('compile single service', async (t) => {
47
+ const tmpDir = await mkdtemp(path.join(base, 'test-runtime-compile-'))
48
+ const prev = process.cwd()
49
+ process.chdir(tmpDir)
50
+ t.after(() => {
51
+ process.chdir(prev)
52
+ })
53
+
54
+ t.after(delDir(tmpDir))
55
+
56
+ const folder = join(import.meta.url, '..', '..', 'fixtures', 'typescript', 'services', 'movies')
57
+ await cp(folder, tmpDir, { recursive: true })
58
+
59
+ const { stdout } = await execa(cliPath, ['compile'])
60
+
61
+ const lines = stdout.split('\n').map(JSON.parse)
62
+ const expected = [{
63
+ msg: 'Typescript compilation completed successfully.'
64
+ }]
65
+
66
+ for (let i = 0; i < expected.length; i++) {
67
+ assert.deepStrictEqual(lines[i].msg, expected[i].msg)
68
+ }
69
+ })
@@ -2,10 +2,9 @@ import { test } from 'node:test'
2
2
  import assert from 'node:assert'
3
3
  import { join } from 'desm'
4
4
  import path from 'node:path'
5
- import { cliPath } from './helper.mjs'
5
+ import { cliPath, delDir } from './helper.mjs'
6
6
  import { execa } from 'execa'
7
- import { mkdtemp, rm, cp, mkdir } from 'node:fs/promises'
8
- import { setTimeout as sleep } from 'node:timers/promises'
7
+ import { mkdtemp, cp, mkdir } from 'node:fs/promises'
9
8
 
10
9
  const base = join(import.meta.url, '..', 'tmp')
11
10
 
@@ -14,25 +13,6 @@ try {
14
13
  } catch {
15
14
  }
16
15
 
17
- function delDir (tmpDir) {
18
- return async function () {
19
- // We give up after 10s.
20
- // This is because on Windows, it's very hard to delete files if the file
21
- // system is not collaborating.
22
- for (let i = 0; i < 10; i++) {
23
- try {
24
- await rm(tmpDir, { recursive: true, force: true })
25
- break
26
- } catch (err) {
27
- if (err.code === 'EBUSY') {
28
- await sleep(1000)
29
- continue
30
- }
31
- }
32
- }
33
- }
34
- }
35
-
36
16
  test('compile without tsconfigs', async () => {
37
17
  const config = join(import.meta.url, '..', '..', 'fixtures', 'configs', 'monorepo.json')
38
18
  await execa(cliPath, ['compile', '-c', config])
@@ -3,6 +3,7 @@ import { Agent, setGlobalDispatcher } from 'undici'
3
3
  import { execa } from 'execa'
4
4
  import split from 'split2'
5
5
  import { join } from 'desm'
6
+ import { rm } from 'node:fs/promises'
6
7
 
7
8
  setGlobalDispatcher(new Agent({
8
9
  keepAliveTimeout: 10,
@@ -34,13 +35,34 @@ export async function start (...args) {
34
35
 
35
36
  for await (const messages of on(output, 'data')) {
36
37
  for (const message of messages) {
37
- const url = message.url ??
38
- message.msg.match(/server listening at (.+)/i)?.[1]
38
+ if (message.msg) {
39
+ const url = message.url ??
40
+ message.msg.match(/server listening at (.+)/i)?.[1]
39
41
 
40
- if (url !== undefined) {
41
- clearTimeout(errorTimeout)
42
- return { child, url, output }
42
+ if (url !== undefined) {
43
+ clearTimeout(errorTimeout)
44
+ return { child, url, output }
45
+ }
43
46
  }
44
47
  }
45
48
  }
46
49
  }
50
+
51
+ export function delDir (tmpDir) {
52
+ return async function () {
53
+ console.time('delDir')
54
+ // We give up after 10s.
55
+ // This is because on Windows, it's very hard to delete files if the file
56
+ // system is not collaborating.
57
+ try {
58
+ await rm(tmpDir, { recursive: true, force: true })
59
+ } catch (err) {
60
+ if (err.code !== 'EBUSY') {
61
+ throw err
62
+ } else {
63
+ console.log('Could not delete directory, retrying', tmpDir)
64
+ }
65
+ }
66
+ console.timeEnd('delDir')
67
+ }
68
+ }
@@ -74,7 +74,7 @@ test('does not start if node inspector flags are provided', async (t) => {
74
74
  for (const message of messages) {
75
75
  stderr += message
76
76
 
77
- if (/Error: The Node.js inspector flags are not supported/.test(stderr)) {
77
+ if (/The Node.js inspector flags are not supported/.test(stderr)) {
78
78
  found = true
79
79
  break
80
80
  }
@@ -47,9 +47,13 @@ function createEsmLoggingPlugin (text, reloaded) {
47
47
  `
48
48
  }
49
49
 
50
+ function saferm (path) {
51
+ return rm(path, { recursive: true, force: true }).catch(() => {})
52
+ }
53
+
50
54
  test('watches CommonJS files', async (t) => {
51
55
  const tmpDir = await mkdtemp(join(base, 'watch-'))
52
- t.after(() => rm(tmpDir, { recursive: true, force: true }))
56
+ t.after(() => saferm(tmpDir))
53
57
  t.diagnostic(`using ${tmpDir}`)
54
58
  const configFileSrc = join(fixturesDir, 'configs', 'monorepo.json')
55
59
  const configFileDst = join(tmpDir, 'configs', 'monorepo.json')
@@ -65,8 +69,6 @@ test('watches CommonJS files', async (t) => {
65
69
  await writeFile(cjsPluginFilePath, createCjsLoggingPlugin('v1', false))
66
70
  const { child } = await start('-c', configFileDst)
67
71
  t.after(() => child.kill('SIGINT'))
68
- child.stdout.pipe(process.stderr)
69
- child.stderr.pipe(process.stderr)
70
72
 
71
73
  await writeFile(cjsPluginFilePath, createCjsLoggingPlugin('v2', true))
72
74
 
@@ -79,7 +81,7 @@ test('watches CommonJS files', async (t) => {
79
81
 
80
82
  test('watches ESM files', async (t) => {
81
83
  const tmpDir = await mkdtemp(join(base, 'watch-'))
82
- t.after(() => rm(tmpDir, { recursive: true, force: true }))
84
+ t.after(() => saferm(tmpDir))
83
85
  t.diagnostic(`using ${tmpDir}`)
84
86
  const configFileSrc = join(fixturesDir, 'configs', 'monorepo.json')
85
87
  const configFileDst = join(tmpDir, 'configs', 'monorepo.json')
@@ -95,8 +97,6 @@ test('watches ESM files', async (t) => {
95
97
  await writeFile(esmPluginFilePath, createEsmLoggingPlugin('v1', false))
96
98
  const { child } = await start('-c', configFileDst)
97
99
  t.after(() => child.kill('SIGINT'))
98
- child.stdout.pipe(process.stderr)
99
- child.stderr.pipe(process.stderr)
100
100
  await writeFile(esmPluginFilePath, createEsmLoggingPlugin('v2', true))
101
101
 
102
102
  for await (const log of child.ndj) {
@@ -108,7 +108,7 @@ test('watches ESM files', async (t) => {
108
108
 
109
109
  test('should not hot reload files with `--hot-reload false', async (t) => {
110
110
  const tmpDir = await mkdtemp(join(base, 'watch-'))
111
- t.after(() => rm(tmpDir, { recursive: true, force: true }))
111
+ t.after(() => saferm(tmpDir))
112
112
  t.diagnostic(`using ${tmpDir}`)
113
113
  const configFileSrc = join(fixturesDir, 'configs', 'monorepo.json')
114
114
  const configFileDst = join(tmpDir, 'configs', 'monorepo.json')
@@ -124,11 +124,84 @@ test('should not hot reload files with `--hot-reload false', async (t) => {
124
124
  await writeFile(cjsPluginFilePath, createCjsLoggingPlugin('v1', false))
125
125
  const { child, url } = await start('-c', configFileDst, '--hot-reload', 'false')
126
126
  t.after(() => child.kill('SIGINT'))
127
- child.stdout.pipe(process.stderr)
128
- child.stderr.pipe(process.stderr)
129
127
  await writeFile(cjsPluginFilePath, createCjsLoggingPlugin('v2', true))
130
128
  await sleep(5000)
131
129
  const res = await request(`${url}/version`)
132
130
  const version = await res.body.text()
133
131
  assert.strictEqual(version, 'v1')
134
132
  })
133
+
134
+ test('watches CommonJS files with hotreload', { timeout: 30000, skip: process.env.CI }, async (t) => {
135
+ const tmpDir = await mkdtemp(join(base, 'watch-'))
136
+ t.after(() => saferm(tmpDir))
137
+ t.diagnostic(`using ${tmpDir}`)
138
+ const configFileSrc = join(fixturesDir, 'configs', 'hotreload.json')
139
+ const configFileDst = join(tmpDir, 'configs', 'monorepo.json')
140
+ const appSrc = join(fixturesDir, 'monorepo')
141
+ const appDst = join(tmpDir, 'monorepo')
142
+ const cjsPluginFilePath = join(appDst, 'serviceAppWithLogger', 'plugin.js')
143
+
144
+ await Promise.all([
145
+ cp(configFileSrc, configFileDst),
146
+ cp(appSrc, appDst, { recursive: true })
147
+ ])
148
+
149
+ await writeFile(cjsPluginFilePath, createCjsLoggingPlugin('v1', false))
150
+ const { child } = await start('-c', configFileDst)
151
+ t.after(() => child.kill('SIGINT'))
152
+
153
+ await writeFile(cjsPluginFilePath, createCjsLoggingPlugin('v2', true))
154
+
155
+ let restartedSecondTime = false
156
+ let restartedThirdTime = false
157
+
158
+ for await (const log of child.ndj) {
159
+ if (log.msg === 'RELOADED v2') {
160
+ restartedSecondTime = true
161
+ } else if (log.msg === 'RELOADED v3') {
162
+ restartedThirdTime = true
163
+ break
164
+ } else if (log.msg?.match(/watching/)) {
165
+ await writeFile(cjsPluginFilePath, createCjsLoggingPlugin('v3', true))
166
+ }
167
+ }
168
+
169
+ assert.ok(restartedSecondTime)
170
+ assert.ok(restartedThirdTime)
171
+ })
172
+
173
+ test('watches CommonJS files with hotreload on a single service', { timeout: 30000, skip: process.env.CI }, async (t) => {
174
+ const tmpDir = await mkdtemp(join(base, 'watch-'))
175
+ t.after(() => saferm(tmpDir))
176
+ t.diagnostic(`using ${tmpDir}`)
177
+ const appSrc = join(fixturesDir, 'monorepo', 'serviceAppWithLogger')
178
+ const appDst = join(tmpDir)
179
+ const cjsPluginFilePath = join(appDst, 'plugin.js')
180
+
181
+ await Promise.all([
182
+ cp(appSrc, appDst, { recursive: true })
183
+ ])
184
+
185
+ await writeFile(cjsPluginFilePath, createCjsLoggingPlugin('v1', false))
186
+ const { child } = await start('-c', join(appDst, 'platformatic.service.json'))
187
+ t.after(() => child.kill('SIGINT'))
188
+
189
+ await writeFile(cjsPluginFilePath, createCjsLoggingPlugin('v2', true))
190
+
191
+ let restartedSecondTime = false
192
+ let restartedThirdTime = false
193
+
194
+ for await (const log of child.ndj) {
195
+ if (log.msg === 'RELOADED v2') {
196
+ restartedSecondTime = true
197
+ } else if (log.msg === 'RELOADED v3') {
198
+ assert.ok(restartedSecondTime)
199
+ restartedThirdTime = true
200
+ break
201
+ } else if (log.msg?.match(/listening/)) {
202
+ await writeFile(cjsPluginFilePath, createCjsLoggingPlugin('v3', true))
203
+ }
204
+ }
205
+
206
+ assert.ok(restartedThirdTime)
207
+ })
@@ -55,6 +55,14 @@ test('performs a topological sort on services depending on allowCycles', async (
55
55
  await loadConfig({}, ['-c', configFile], platformaticRuntime)
56
56
  })
57
57
  })
58
+
59
+ await t.test('throws by adding the most probable service ', async () => {
60
+ const configFile = join(fixturesDir, 'leven', 'platformatic.runtime.json')
61
+
62
+ await assert.rejects(async () => {
63
+ await loadConfig({}, ['-c', configFile], platformaticRuntime)
64
+ }, 'service \'rainy-empire\' has unordered dependency: \'deeply-splitte\'. Did you mean \'deeply-spittle\'?')
65
+ })
58
66
  })
59
67
 
60
68
  test('can resolve service id from client package.json if not provided', async () => {
@@ -68,7 +68,7 @@ test('composer', async (t) => {
68
68
  const res = await request(entryUrl)
69
69
 
70
70
  assert.strictEqual(res.statusCode, 200)
71
- assert.deepStrictEqual(await res.body.json(), { message: 'Welcome to Platformatic! Please visit https://oss.platformatic.dev' })
71
+ assert.deepStrictEqual(await res.body.json(), { message: 'Welcome to Platformatic! Please visit https://docs.platformatic.dev' })
72
72
  }
73
73
 
74
74
  {
@@ -0,0 +1,37 @@
1
+ 'use strict'
2
+
3
+ const assert = require('node:assert')
4
+ const { request } = require('undici')
5
+ const { test } = require('node:test')
6
+ const { join } = require('node:path')
7
+ const { loadConfig } = require('@platformatic/service')
8
+ const { platformaticRuntime } = require('..')
9
+ const { startWithConfig } = require('../lib/start')
10
+ const fixturesDir = join(__dirname, '..', 'fixtures')
11
+
12
+ test('propagate the traceId correctly to runtime services', async (t) => {
13
+ const configFile = join(fixturesDir, 'telemetry', 'platformatic.runtime.json')
14
+ const config = await loadConfig({}, ['-c', configFile], platformaticRuntime)
15
+ const app = await startWithConfig(config.configManager)
16
+
17
+ t.after(async () => {
18
+ await app.close()
19
+ })
20
+
21
+ const entryUrl = await app.start()
22
+
23
+ const traceId = '5e994e8fb53b27c91dcd2fec22771d15'
24
+ const spanId = '166f3ab30f21800b'
25
+ const traceparent = `00-${traceId}-${spanId}-01`
26
+ const res = await request(entryUrl, {
27
+ method: 'GET',
28
+ path: '/',
29
+ headers: {
30
+ traceparent
31
+ }
32
+ })
33
+
34
+ assert.strictEqual(res.statusCode, 200)
35
+ const response = await res.body.json()
36
+ assert.strictEqual(response.traceId, traceId)
37
+ })
@@ -254,7 +254,6 @@ test('start()', async (t) => {
254
254
  const scriptFile = join(fixturesDir, 'starter.js')
255
255
  const configFile = join(fixturesDir, 'monorepo', 'serviceAppWithLogger', 'platformatic.service.json')
256
256
  const child = spawn(process.execPath, [scriptFile, configFile])
257
- child.stdout.pipe(process.stdout)
258
257
  child.stderr.pipe(process.stderr)
259
258
  const [exitCode] = await once(child, 'exit')
260
259
 
@@ -318,14 +317,11 @@ test('startCommand()', async (t) => {
318
317
 
319
318
  assert.strictEqual(exitCode, 42)
320
319
  })
321
- })
322
320
 
323
- test('startCommandInRuntime()', async (t) => {
324
321
  await t.test('can start a non-runtime application', async (t) => {
325
322
  const scriptFile = join(fixturesDir, 'start-command-in-runtime.js')
326
323
  const configFile = join(fixturesDir, 'monorepo', 'serviceAppWithLogger', 'platformatic.service.json')
327
324
  const child = spawn(process.execPath, [scriptFile, configFile])
328
- child.stdout.pipe(process.stdout)
329
325
  child.stderr.pipe(process.stderr)
330
326
  const [exitCode] = await once(child, 'exit')
331
327
 
@@ -336,7 +332,6 @@ test('startCommandInRuntime()', async (t) => {
336
332
  const scriptFile = join(fixturesDir, 'start-command-in-runtime.js')
337
333
  const configFile = join(fixturesDir, 'configs', 'monorepo.json')
338
334
  const child = spawn(process.execPath, [scriptFile, configFile])
339
- child.stdout.pipe(process.stdout)
340
335
  child.stderr.pipe(process.stderr)
341
336
  const [exitCode] = await once(child, 'exit')
342
337