@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.
Files changed (54) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +13 -0
  3. package/README.md +13 -0
  4. package/fixtures/configs/invalid-entrypoint.json +14 -0
  5. package/fixtures/configs/missing-property.config.json +13 -0
  6. package/fixtures/configs/missing-service-config.json +7 -0
  7. package/fixtures/configs/monorepo-composer.json +19 -0
  8. package/fixtures/configs/monorepo-create-cycle.json +21 -0
  9. package/fixtures/configs/monorepo-no-cycles.json +20 -0
  10. package/fixtures/configs/monorepo.json +20 -0
  11. package/fixtures/configs/no-services.config.json +5 -0
  12. package/fixtures/configs/no-sources.config.json +4 -0
  13. package/fixtures/configs/service-throws-on-start.json +11 -0
  14. package/fixtures/monorepo/composerApp/platformatic.composer.json +39 -0
  15. package/fixtures/monorepo/docs/README.md +1 -0
  16. package/fixtures/monorepo/serviceApp/deps/dep1.js +7 -0
  17. package/fixtures/monorepo/serviceApp/deps/dep2.mjs +2 -0
  18. package/fixtures/monorepo/serviceApp/deps/dep3.mjs +2 -0
  19. package/fixtures/monorepo/serviceApp/deps/dep4.js +1 -0
  20. package/fixtures/monorepo/serviceApp/platformatic.service.json +22 -0
  21. package/fixtures/monorepo/serviceApp/plugin.js +25 -0
  22. package/fixtures/monorepo/serviceApp/with-logger/package.json +5 -0
  23. package/fixtures/monorepo/serviceApp/with-logger/with-logger.cjs +25 -0
  24. package/fixtures/monorepo/serviceApp/with-logger/with-logger.d.ts +38 -0
  25. package/fixtures/monorepo/serviceApp/with-logger/with-logger.openapi.json +22 -0
  26. package/fixtures/monorepo/serviceAppWithLogger/platformatic.service.json +19 -0
  27. package/fixtures/monorepo/serviceAppWithLogger/plugin.js +8 -0
  28. package/fixtures/monorepo/serviceAppWithMultiplePlugins/platformatic.service.json +26 -0
  29. package/fixtures/monorepo/serviceAppWithMultiplePlugins/plugin.js +8 -0
  30. package/fixtures/monorepo/serviceAppWithMultiplePlugins/plugin2.mjs +5 -0
  31. package/fixtures/serviceAppThrowsOnStart/platformatic.service.json +13 -0
  32. package/fixtures/serviceAppThrowsOnStart/plugin.js +6 -0
  33. package/help/help.txt +5 -0
  34. package/help/start.txt +5 -0
  35. package/index.d.ts +13 -0
  36. package/index.js +33 -0
  37. package/index.test-d.ts +18 -0
  38. package/lib/app.js +261 -0
  39. package/lib/config.js +209 -0
  40. package/lib/loader.mjs +101 -0
  41. package/lib/schema.js +89 -0
  42. package/lib/start.js +77 -0
  43. package/lib/worker.js +88 -0
  44. package/package.json +52 -0
  45. package/runtime.mjs +43 -0
  46. package/test/app.test.js +245 -0
  47. package/test/cli/helper.mjs +46 -0
  48. package/test/cli/start.test.mjs +61 -0
  49. package/test/cli/validations.test.mjs +52 -0
  50. package/test/cli/watch.test.mjs +131 -0
  51. package/test/config.test.js +58 -0
  52. package/test/schema.test.js +13 -0
  53. package/test/start.test.js +101 -0
  54. package/test/worker.test.js +21 -0
package/lib/schema.js ADDED
@@ -0,0 +1,89 @@
1
+ #! /usr/bin/env node
2
+ 'use strict'
3
+
4
+ const pkg = require('../package.json')
5
+ const version = 'v' + pkg.version
6
+ const platformaticRuntimeSchema = {
7
+ $id: `https://platformatic.dev/schemas/${version}/runtime`,
8
+ $schema: 'http://json-schema.org/draft-07/schema#',
9
+ type: 'object',
10
+ properties: {
11
+ autoload: {
12
+ type: 'object',
13
+ additionalProperties: false,
14
+ required: ['path'],
15
+ properties: {
16
+ path: {
17
+ type: 'string',
18
+ resolvePath: true
19
+ },
20
+ exclude: {
21
+ type: 'array',
22
+ default: [],
23
+ items: {
24
+ type: 'string'
25
+ }
26
+ },
27
+ mappings: {
28
+ type: 'object',
29
+ additionalProperties: {
30
+ type: 'object',
31
+ additionalProperties: false,
32
+ required: ['id', 'config'],
33
+ properties: {
34
+ id: {
35
+ type: 'string'
36
+ },
37
+ config: {
38
+ type: 'string'
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
44
+ },
45
+ services: {
46
+ type: 'array',
47
+ default: [],
48
+ minItems: 1,
49
+ items: {
50
+ type: 'object',
51
+ required: ['id', 'path', 'config'],
52
+ properties: {
53
+ id: {
54
+ type: 'string'
55
+ },
56
+ path: {
57
+ type: 'string',
58
+ resolvePath: true
59
+ },
60
+ config: {
61
+ type: 'string'
62
+ }
63
+ }
64
+ }
65
+ },
66
+ entrypoint: {
67
+ type: 'string'
68
+ },
69
+ hotReload: {
70
+ type: 'boolean'
71
+ },
72
+ allowCycles: {
73
+ type: 'boolean'
74
+ },
75
+ $schema: {
76
+ type: 'string'
77
+ }
78
+ },
79
+ anyOf: [
80
+ { required: ['autoload', 'entrypoint'] },
81
+ { required: ['services', 'entrypoint'] }
82
+ ]
83
+ }
84
+
85
+ module.exports.schema = platformaticRuntimeSchema
86
+
87
+ if (require.main === module) {
88
+ console.log(JSON.stringify(platformaticRuntimeSchema, null, 2))
89
+ }
package/lib/start.js ADDED
@@ -0,0 +1,77 @@
1
+ 'use strict'
2
+ const { once } = require('node:events')
3
+ const { join } = require('node:path')
4
+ const { Worker } = require('node:worker_threads')
5
+ const closeWithGrace = require('close-with-grace')
6
+ const { loadConfig } = require('@platformatic/service')
7
+ const { platformaticRuntime } = require('./config')
8
+ const kLoaderFile = join(__dirname, 'loader.mjs')
9
+ const kWorkerFile = join(__dirname, 'worker.js')
10
+ const kWorkerExecArgv = [
11
+ '--no-warnings',
12
+ '--experimental-loader',
13
+ kLoaderFile
14
+ ]
15
+
16
+ async function start (argv) {
17
+ const { configManager } = await loadConfig({}, argv, platformaticRuntime, {
18
+ watch: true
19
+ })
20
+ const app = await startWithConfig(configManager)
21
+
22
+ await app.start()
23
+ return app
24
+ }
25
+
26
+ async function startWithConfig (configManager) {
27
+ const config = configManager.current
28
+ const worker = new Worker(kWorkerFile, {
29
+ /* c8 ignore next */
30
+ execArgv: config.hotReload ? kWorkerExecArgv : [],
31
+ workerData: { config }
32
+ })
33
+
34
+ worker.on('exit', () => {
35
+ configManager.fileWatcher?.stopWatching()
36
+ })
37
+
38
+ worker.on('error', (err) => {
39
+ console.error(err)
40
+ process.exit(1)
41
+ })
42
+
43
+ /* c8 ignore next 3 */
44
+ process.on('SIGUSR2', () => {
45
+ worker.postMessage({ signal: 'SIGUSR2' })
46
+ })
47
+
48
+ closeWithGrace((event) => {
49
+ worker.postMessage(event)
50
+ })
51
+
52
+ /* c8 ignore next 3 */
53
+ configManager.on('update', () => {
54
+ // TODO(cjihrig): Need to clean up and restart the worker.
55
+ })
56
+
57
+ await once(worker, 'message') // plt:init
58
+
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
+ }
75
+ }
76
+
77
+ module.exports = { start, startWithConfig }
package/lib/worker.js ADDED
@@ -0,0 +1,88 @@
1
+ 'use strict'
2
+ const { parentPort, workerData } = require('node:worker_threads')
3
+ const FastifyUndiciDispatcher = require('fastify-undici-dispatcher')
4
+ const { Agent, setGlobalDispatcher } = require('undici')
5
+ const { PlatformaticApp } = require('./app')
6
+ const loaderPort = globalThis.LOADER_PORT // Added by loader.mjs.
7
+ const globalAgent = new Agent()
8
+ const globalDispatcher = new FastifyUndiciDispatcher({
9
+ dispatcher: globalAgent,
10
+ // setting the domain here allows for fail-fast scenarios
11
+ domain: '.plt.local'
12
+ })
13
+ const applications = new Map()
14
+ let entrypoint
15
+
16
+ delete globalThis.LOADER_PORT
17
+ setGlobalDispatcher(globalDispatcher)
18
+
19
+ parentPort.on('message', async (msg) => {
20
+ for (const app of applications.values()) {
21
+ await app.handleProcessLevelEvent(msg)
22
+
23
+ if (msg?.msg === 'plt:start' || msg?.msg === 'plt:restart') {
24
+ const serviceUrl = new URL(app.appConfig.localUrl)
25
+
26
+ globalDispatcher.route(serviceUrl.host, app.server)
27
+ }
28
+ }
29
+
30
+ switch (msg?.msg) {
31
+ case 'plt:start':
32
+ configureDispatcher()
33
+ parentPort.postMessage({ msg: 'plt:started', url: entrypoint.server.url })
34
+ break
35
+ case 'plt:restart':
36
+ configureDispatcher()
37
+ parentPort.postMessage({ msg: 'plt:restarted', url: entrypoint.server.url })
38
+ break
39
+ case 'plt:stop':
40
+ process.exit() // Exit the worker thread.
41
+ break
42
+ case undefined:
43
+ // Ignore
44
+ break
45
+ default:
46
+ throw new Error(`unknown message type: '${msg.msg}'`)
47
+ }
48
+ })
49
+
50
+ async function main () {
51
+ const { services } = workerData.config
52
+
53
+ for (let i = 0; i < services.length; ++i) {
54
+ const service = services[i]
55
+ const app = new PlatformaticApp(service, loaderPort)
56
+
57
+ applications.set(service.id, app)
58
+
59
+ if (service.entrypoint) {
60
+ entrypoint = app
61
+ }
62
+ }
63
+
64
+ parentPort.postMessage('plt:init')
65
+ }
66
+
67
+ function configureDispatcher () {
68
+ const { services } = workerData.config
69
+
70
+ // Setup the local services in the global dispatcher.
71
+ for (let i = 0; i < services.length; ++i) {
72
+ const service = services[i]
73
+ const serviceApp = applications.get(service.id)
74
+ const serviceUrl = new URL(service.localUrl)
75
+
76
+ globalDispatcher.route(serviceUrl.host, serviceApp.server)
77
+
78
+ for (let j = 0; j < service.dependencies.length; ++j) {
79
+ const depConfig = service.dependencies[j]
80
+ const depApp = applications.get(depConfig.id)
81
+ const depUrl = new URL(depConfig.url)
82
+
83
+ globalDispatcher.route(depUrl.host, depApp.server)
84
+ }
85
+ }
86
+ }
87
+
88
+ main()
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@platformatic/runtime",
3
+ "version": "0.26.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "plt-runtime": "./runtime.mjs"
8
+ },
9
+ "author": "Matteo Collina <hello@matteocollina.com>",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/platformatic/platformatic.git"
13
+ },
14
+ "license": "Apache-2.0",
15
+ "bugs": {
16
+ "url": "https://github.com/platformatic/platformatic/issues"
17
+ },
18
+ "homepage": "https://github.com/platformatic/platformatic#readme",
19
+ "devDependencies": {
20
+ "c8": "^7.13.0",
21
+ "execa": "^7.0.0",
22
+ "snazzy": "^9.0.0",
23
+ "split2": "^4.1.0",
24
+ "standard": "^17.0.0",
25
+ "tsd": "^0.28.0"
26
+ },
27
+ "dependencies": {
28
+ "@hapi/topo": "^6.0.2",
29
+ "close-with-grace": "^1.2.0",
30
+ "commist": "^3.2.0",
31
+ "desm": "^1.3.0",
32
+ "es-main": "^1.2.0",
33
+ "fastify": "^4.17.0",
34
+ "fastify-undici-dispatcher": "^0.4.0",
35
+ "help-me": "^4.2.0",
36
+ "minimist": "^1.2.8",
37
+ "undici": "^5.20.0",
38
+ "@platformatic/config": "0.26.0",
39
+ "@platformatic/service": "0.26.0",
40
+ "@platformatic/start": "0.26.0",
41
+ "@platformatic/utils": "0.26.0"
42
+ },
43
+ "standard": {
44
+ "ignore": [
45
+ "**/dist/*"
46
+ ]
47
+ },
48
+ "scripts": {
49
+ "test": "npm run lint && c8 -x fixtures -x test node --test && tsd",
50
+ "lint": "standard | snazzy"
51
+ }
52
+ }
package/runtime.mjs ADDED
@@ -0,0 +1,43 @@
1
+ #! /usr/bin/env node
2
+
3
+ import { readFile } from 'node:fs/promises'
4
+ import commist from 'commist'
5
+ import { join } from 'desm'
6
+ import isMain from 'es-main'
7
+ import helpMe from 'help-me'
8
+ import parseArgs from 'minimist'
9
+ import { start } from './lib/start.js'
10
+
11
+ const help = helpMe({
12
+ dir: join(import.meta.url, 'help'),
13
+ // the default
14
+ ext: '.txt'
15
+ })
16
+
17
+ const program = commist({ maxDistance: 2 })
18
+
19
+ program.register('help', help.toStdout)
20
+ program.register('help start', help.toStdout.bind(null, ['start']))
21
+ program.register('start', start)
22
+
23
+ export async function run (argv) {
24
+ const args = parseArgs(argv, {
25
+ alias: {
26
+ v: 'version'
27
+ }
28
+ })
29
+
30
+ if (args.version) {
31
+ console.log('v' + JSON.parse(await readFile(join(import.meta.url, 'package.json'))).version)
32
+ process.exit(0)
33
+ }
34
+
35
+ return {
36
+ output: await program.parseAsync(argv),
37
+ help
38
+ }
39
+ }
40
+
41
+ if (isMain(import.meta)) {
42
+ await run(process.argv.splice(2))
43
+ }
@@ -0,0 +1,245 @@
1
+ 'use strict'
2
+
3
+ const assert = require('node:assert')
4
+ const { join } = require('node:path')
5
+ const { test } = require('node:test')
6
+ const { PlatformaticApp } = require('../lib/app')
7
+ const fixturesDir = join(__dirname, '..', 'fixtures')
8
+
9
+ test('logs errors during startup', async (t) => {
10
+ const appPath = join(fixturesDir, 'serviceAppThrowsOnStart')
11
+ const configFile = join(appPath, 'platformatic.service.json')
12
+ const config = {
13
+ id: 'serviceAppThrowsOnStart',
14
+ config: configFile,
15
+ path: appPath,
16
+ entrypoint: true,
17
+ hotReload: true
18
+ }
19
+ const app = new PlatformaticApp(config, null)
20
+
21
+ t.mock.method(console, 'error', () => {})
22
+ t.mock.method(process, 'exit', () => { throw new Error('exited') })
23
+
24
+ await assert.rejects(async () => {
25
+ await app.start()
26
+ }, /exited/)
27
+ assert.strictEqual(process.exit.mock.calls.length, 1)
28
+ assert.strictEqual(process.exit.mock.calls[0].arguments[0], 1)
29
+ assert.strictEqual(console.error.mock.calls.length, 1)
30
+ assert.strictEqual(console.error.mock.calls[0].arguments[0].message, 'boom')
31
+ })
32
+
33
+ test('errors when starting an already started application', async (t) => {
34
+ const appPath = join(fixturesDir, 'monorepo', 'serviceApp')
35
+ const configFile = join(appPath, 'platformatic.service.json')
36
+ const config = {
37
+ id: 'serviceApp',
38
+ config: configFile,
39
+ path: appPath,
40
+ entrypoint: true,
41
+ hotReload: true,
42
+ dependencies: [],
43
+ dependents: [],
44
+ localServiceEnvVars: new Map([['PLT_WITH_LOGGER_URL', ' ']])
45
+ }
46
+ const app = new PlatformaticApp(config, null)
47
+
48
+ t.after(app.stop.bind(app))
49
+ await app.start()
50
+ await assert.rejects(async () => {
51
+ await app.start()
52
+ }, /application is already started/)
53
+ })
54
+
55
+ test('errors when stopping an already stopped application', async (t) => {
56
+ const appPath = join(fixturesDir, 'monorepo', 'serviceApp')
57
+ const configFile = join(appPath, 'platformatic.service.json')
58
+ const config = {
59
+ id: 'serviceApp',
60
+ config: configFile,
61
+ path: appPath,
62
+ entrypoint: true,
63
+ hotReload: true,
64
+ dependencies: [],
65
+ dependents: [],
66
+ localServiceEnvVars: new Map([['PLT_WITH_LOGGER_URL', ' ']])
67
+ }
68
+ const app = new PlatformaticApp(config, null)
69
+
70
+ await assert.rejects(async () => {
71
+ await app.stop()
72
+ }, /application has not been started/)
73
+ })
74
+
75
+ test('does not restart while restarting', async (t) => {
76
+ const appPath = join(fixturesDir, 'monorepo', 'serviceApp')
77
+ const configFile = join(appPath, 'platformatic.service.json')
78
+ const config = {
79
+ id: 'serviceApp',
80
+ config: configFile,
81
+ path: appPath,
82
+ entrypoint: true,
83
+ hotReload: true,
84
+ dependencies: [],
85
+ dependents: [],
86
+ localServiceEnvVars: new Map([['PLT_WITH_LOGGER_URL', ' ']])
87
+ }
88
+ const app = new PlatformaticApp(config, null)
89
+
90
+ t.after(app.stop.bind(app))
91
+ await app.start()
92
+ t.mock.method(app, 'stop')
93
+ await Promise.all([
94
+ app.restart(),
95
+ app.restart(),
96
+ app.restart()
97
+ ])
98
+
99
+ // stop() should have only been called once despite three restart() calls.
100
+ assert.strictEqual(app.stop.mock.calls.length, 1)
101
+ })
102
+
103
+ test('restarts on SIGUSR2', async (t) => {
104
+ const appPath = join(fixturesDir, 'monorepo', 'serviceApp')
105
+ const configFile = join(appPath, 'platformatic.service.json')
106
+ const config = {
107
+ id: 'serviceApp',
108
+ config: configFile,
109
+ path: appPath,
110
+ entrypoint: true,
111
+ hotReload: true,
112
+ dependencies: [],
113
+ dependents: [],
114
+ localServiceEnvVars: new Map([['PLT_WITH_LOGGER_URL', ' ']])
115
+ }
116
+ const app = new PlatformaticApp(config, null)
117
+
118
+ t.after(app.stop.bind(app))
119
+ await app.start()
120
+ t.mock.method(app, 'restart')
121
+ await app.handleProcessLevelEvent({ signal: 'SIGUSR2' })
122
+ assert(app.restart.mock.calls.length)
123
+ })
124
+
125
+ test('stops on signals other than SIGUSR2', async (t) => {
126
+ const appPath = join(fixturesDir, 'monorepo', 'serviceApp')
127
+ const configFile = join(appPath, 'platformatic.service.json')
128
+ const config = {
129
+ id: 'serviceApp',
130
+ config: configFile,
131
+ path: appPath,
132
+ entrypoint: true,
133
+ hotReload: true,
134
+ dependencies: [],
135
+ dependents: [],
136
+ localServiceEnvVars: new Map([['PLT_WITH_LOGGER_URL', ' ']])
137
+ }
138
+ const app = new PlatformaticApp(config, null)
139
+
140
+ t.after(async () => {
141
+ try {
142
+ await app.stop()
143
+ } catch {
144
+ // Ignore. The server should be stopped if nothing went wrong.
145
+ }
146
+ })
147
+ await app.start()
148
+ t.mock.method(app, 'stop')
149
+ await app.handleProcessLevelEvent({ signal: 'SIGINT' })
150
+ assert.strictEqual(app.stop.mock.calls.length, 1)
151
+ })
152
+
153
+ test('stops on uncaught exceptions', async (t) => {
154
+ const appPath = join(fixturesDir, 'monorepo', 'serviceApp')
155
+ const configFile = join(appPath, 'platformatic.service.json')
156
+ const config = {
157
+ id: 'serviceApp',
158
+ config: configFile,
159
+ path: appPath,
160
+ entrypoint: true,
161
+ hotReload: true,
162
+ dependencies: [],
163
+ dependents: [],
164
+ localServiceEnvVars: new Map([['PLT_WITH_LOGGER_URL', ' ']])
165
+ }
166
+ const app = new PlatformaticApp(config, null)
167
+
168
+ t.after(async () => {
169
+ try {
170
+ await app.stop()
171
+ } catch {
172
+ // Ignore. The server should be stopped if nothing went wrong.
173
+ }
174
+ })
175
+ await app.start()
176
+ t.mock.method(app, 'stop')
177
+ await app.handleProcessLevelEvent({ err: new Error('boom') })
178
+ assert.strictEqual(app.stop.mock.calls.length, 1)
179
+ })
180
+
181
+ test('supports configuration overrides', async (t) => {
182
+ const appPath = join(fixturesDir, 'monorepo', 'serviceApp')
183
+ const configFile = join(appPath, 'platformatic.service.json')
184
+ const config = {
185
+ id: 'serviceApp',
186
+ config: configFile,
187
+ path: appPath,
188
+ entrypoint: true,
189
+ hotReload: true,
190
+ dependencies: [],
191
+ dependents: [],
192
+ localServiceEnvVars: new Map([['PLT_WITH_LOGGER_URL', ' ']])
193
+ }
194
+
195
+ await t.test('throws on non-string config paths', async (t) => {
196
+ config._configOverrides = new Map([[null, 5]])
197
+ const app = new PlatformaticApp(config, null)
198
+
199
+ t.after(async () => {
200
+ try {
201
+ await app.stop()
202
+ } catch {
203
+ // Ignore. The server should be stopped if nothing went wrong.
204
+ }
205
+ })
206
+
207
+ await assert.rejects(async () => {
208
+ await app.start()
209
+ }, /config path must be a string/)
210
+ })
211
+
212
+ await t.test('ignores invalid config paths', async (t) => {
213
+ config._configOverrides = new Map([['foo.bar.baz', 5]])
214
+ const app = new PlatformaticApp(config, null)
215
+
216
+ t.after(async () => {
217
+ try {
218
+ await app.stop()
219
+ } catch {
220
+ // Ignore. The server should be stopped if nothing went wrong.
221
+ }
222
+ })
223
+
224
+ await app.start()
225
+ })
226
+
227
+ await t.test('sets valid config paths', async (t) => {
228
+ config._configOverrides = new Map([
229
+ ['server.keepAliveTimeout', 1],
230
+ ['server.port', 0]
231
+ ])
232
+ const app = new PlatformaticApp(config, null)
233
+
234
+ t.after(async () => {
235
+ try {
236
+ await app.stop()
237
+ } catch {
238
+ // Ignore. The server should be stopped if nothing went wrong.
239
+ }
240
+ })
241
+
242
+ await app.start()
243
+ assert.strictEqual(app.config.configManager.current.server.keepAliveTimeout, 1)
244
+ })
245
+ })
@@ -0,0 +1,46 @@
1
+ import { on } from 'node:events'
2
+ import { Agent, setGlobalDispatcher } from 'undici'
3
+ import { execa } from 'execa'
4
+ import split from 'split2'
5
+ import { join } from 'desm'
6
+
7
+ setGlobalDispatcher(new Agent({
8
+ keepAliveTimeout: 10,
9
+ keepAliveMaxTimeout: 10,
10
+ tls: {
11
+ rejectUnauthorized: false
12
+ }
13
+ }))
14
+
15
+ export const cliPath = join(import.meta.url, '..', '..', 'runtime.mjs')
16
+
17
+ export async function start (...args) {
18
+ const child = execa(process.execPath, [cliPath, 'start', ...args])
19
+ child.stderr.pipe(process.stdout)
20
+
21
+ const output = child.stdout.pipe(split(function (line) {
22
+ try {
23
+ const obj = JSON.parse(line)
24
+ return obj
25
+ } catch (err) {
26
+ console.log(line)
27
+ }
28
+ }))
29
+ child.ndj = output
30
+
31
+ const errorTimeout = setTimeout(() => {
32
+ throw new Error('Couldn\'t start server')
33
+ }, 30000)
34
+
35
+ for await (const messages of on(output, 'data')) {
36
+ for (const message of messages) {
37
+ const url = message.url ??
38
+ message.msg.match(/server listening at (.+)/i)?.[1]
39
+
40
+ if (url !== undefined) {
41
+ clearTimeout(errorTimeout)
42
+ return { child, url, output }
43
+ }
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,61 @@
1
+ import assert from 'node:assert'
2
+ import { on, once } from 'node:events'
3
+ import { test } from 'node:test'
4
+ import { join } from 'desm'
5
+ import { request } from 'undici'
6
+ import { cliPath, start } from './helper.mjs'
7
+
8
+ test('autostart', async () => {
9
+ const config = join(import.meta.url, '..', '..', 'fixtures', 'configs', 'monorepo.json')
10
+ const { child, url } = await start('-c', config)
11
+ const res = await request(url)
12
+
13
+ assert.strictEqual(res.statusCode, 200)
14
+ assert.deepStrictEqual(await res.body.json(), { hello: 'hello123' })
15
+ child.kill('SIGINT')
16
+ })
17
+
18
+ test('start command', async () => {
19
+ const config = join(import.meta.url, '..', '..', 'fixtures', 'configs', 'monorepo.json')
20
+ const { child, url } = await start('start', '-c', config)
21
+ const res = await request(url)
22
+
23
+ assert.strictEqual(res.statusCode, 200)
24
+ assert.deepStrictEqual(await res.body.json(), { hello: 'hello123' })
25
+ child.kill('SIGINT')
26
+ })
27
+
28
+ test('handles startup errors', async (t) => {
29
+ const { execa } = await import('execa')
30
+ const config = join(import.meta.url, '..', '..', 'fixtures', 'configs', 'service-throws-on-start.json')
31
+ const child = execa(process.execPath, [cliPath, 'start', '-c', config], { encoding: 'utf8' })
32
+ let stderr = ''
33
+ let found = false
34
+
35
+ for await (const messages of on(child.stderr, 'data')) {
36
+ for (const message of messages) {
37
+ stderr += message
38
+
39
+ if (/Error: boom/.test(stderr)) {
40
+ found = true
41
+ break
42
+ }
43
+ }
44
+
45
+ if (found) {
46
+ break
47
+ }
48
+ }
49
+
50
+ assert(found)
51
+ })
52
+
53
+ test('exits on error', async () => {
54
+ const config = join(import.meta.url, '..', '..', 'fixtures', 'configs', 'monorepo.json')
55
+ const { child, url } = await start('start', '-c', config)
56
+ const res = await request(url + '/crash')
57
+ const [exitCode] = await once(child, 'exit')
58
+
59
+ assert.strictEqual(res.statusCode, 200)
60
+ assert.strictEqual(exitCode, 1)
61
+ })