@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/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
|
+
}
|
package/test/app.test.js
ADDED
|
@@ -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
|
+
})
|