@platformatic/basic 3.4.1 → 3.5.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/config.d.ts +321 -1
- package/eslint.config.js +1 -3
- package/index.d.ts +81 -0
- package/index.js +4 -155
- package/lib/capability.js +734 -0
- package/lib/config.js +81 -0
- package/lib/creation.js +9 -0
- package/lib/errors.js +1 -1
- package/lib/modules.js +101 -0
- package/lib/schema.js +26 -6
- package/lib/utils.js +4 -2
- package/lib/worker/child-manager.js +82 -37
- package/lib/worker/child-process.js +320 -93
- package/lib/worker/listeners.js +10 -4
- package/package.json +19 -18
- package/schema.json +1277 -2
- package/lib/base.js +0 -276
- package/lib/worker/child-transport.js +0 -59
package/lib/base.js
DELETED
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
import { deepmerge } from '@platformatic/utils'
|
|
2
|
-
import { parseCommandString } from 'execa'
|
|
3
|
-
import { spawn } from 'node:child_process'
|
|
4
|
-
import { once } from 'node:events'
|
|
5
|
-
import { existsSync } from 'node:fs'
|
|
6
|
-
import { platform } from 'node:os'
|
|
7
|
-
import { pathToFileURL } from 'node:url'
|
|
8
|
-
import pino from 'pino'
|
|
9
|
-
import split2 from 'split2'
|
|
10
|
-
import { NonZeroExitCode } from './errors.js'
|
|
11
|
-
import { cleanBasePath } from './utils.js'
|
|
12
|
-
import { ChildManager } from './worker/child-manager.js'
|
|
13
|
-
|
|
14
|
-
export class BaseStackable {
|
|
15
|
-
#childManager
|
|
16
|
-
#subprocess
|
|
17
|
-
#subprocessStarted
|
|
18
|
-
|
|
19
|
-
constructor (type, version, options, root, configManager) {
|
|
20
|
-
this.type = type
|
|
21
|
-
this.version = version
|
|
22
|
-
this.id = options.context.serviceId
|
|
23
|
-
this.telemetryConfig = options.context.telemetryConfig
|
|
24
|
-
this.options = options
|
|
25
|
-
this.root = root
|
|
26
|
-
this.configManager = configManager
|
|
27
|
-
this.serverConfig = deepmerge(options.context.serverConfig ?? {}, configManager.current.server ?? {})
|
|
28
|
-
this.openapiSchema = null
|
|
29
|
-
this.graphqlSchema = null
|
|
30
|
-
this.isEntrypoint = options.context.isEntrypoint
|
|
31
|
-
this.isProduction = options.context.isProduction
|
|
32
|
-
|
|
33
|
-
// Setup the logger
|
|
34
|
-
const pinoOptions = {
|
|
35
|
-
level: this.serverConfig?.logger?.level ?? 'trace'
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (this.id) {
|
|
39
|
-
pinoOptions.name = this.id
|
|
40
|
-
}
|
|
41
|
-
this.logger = pino(pinoOptions)
|
|
42
|
-
|
|
43
|
-
// Setup globals
|
|
44
|
-
this.registerGlobals({
|
|
45
|
-
setOpenapiSchema: this.setOpenapiSchema.bind(this),
|
|
46
|
-
setGraphqlSchema: this.setGraphqlSchema.bind(this),
|
|
47
|
-
setBasePath: this.setBasePath.bind(this)
|
|
48
|
-
})
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
getUrl () {
|
|
52
|
-
return this.url
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async getConfig () {
|
|
56
|
-
return this.configManager.current
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async getEnv () {
|
|
60
|
-
return this.configManager.env
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async getWatchConfig () {
|
|
64
|
-
const config = this.configManager.current
|
|
65
|
-
|
|
66
|
-
const enabled = config.watch?.enabled !== false
|
|
67
|
-
|
|
68
|
-
if (!enabled) {
|
|
69
|
-
return { enabled, path: this.root }
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return {
|
|
73
|
-
enabled,
|
|
74
|
-
path: this.root,
|
|
75
|
-
allow: config.watch?.allow,
|
|
76
|
-
ignore: config.watch?.ignore
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async getInfo () {
|
|
81
|
-
return { type: this.type, version: this.version }
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
getDispatchFunc () {
|
|
85
|
-
return this
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async collectMetrics () {
|
|
89
|
-
return {
|
|
90
|
-
defaultMetrics: true,
|
|
91
|
-
httpMetrics: false
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async getOpenapiSchema () {
|
|
96
|
-
return this.openapiSchema
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async getGraphqlSchema () {
|
|
100
|
-
return this.graphqlSchema
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
setOpenapiSchema (schema) {
|
|
104
|
-
this.openapiSchema = schema
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
setGraphqlSchema (schema) {
|
|
108
|
-
this.graphqlSchema = schema
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
setBasePath (basePath) {
|
|
112
|
-
this.basePath = basePath
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
async log ({ message, level }) {
|
|
116
|
-
const logLevel = level ?? 'info'
|
|
117
|
-
this.logger[logLevel](message)
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
registerGlobals (globals) {
|
|
121
|
-
globalThis.platformatic = Object.assign(globalThis.platformatic ?? {}, globals)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
verifyOutputDirectory (path) {
|
|
125
|
-
if (this.isProduction && !existsSync(path)) {
|
|
126
|
-
throw new Error(
|
|
127
|
-
`Cannot access directory '${path}'. Please run the 'build' command before running in production mode.`
|
|
128
|
-
)
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
async buildWithCommand (command, basePath, loader, scripts) {
|
|
133
|
-
if (Array.isArray(command)) {
|
|
134
|
-
command = command.join(' ')
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
this.logger.debug(`Executing "${command}" ...`)
|
|
138
|
-
|
|
139
|
-
this.#childManager = new ChildManager({
|
|
140
|
-
logger: this.logger,
|
|
141
|
-
loader,
|
|
142
|
-
scripts,
|
|
143
|
-
context: {
|
|
144
|
-
id: this.id,
|
|
145
|
-
// Always use URL to avoid serialization problem in Windows
|
|
146
|
-
root: pathToFileURL(this.root).toString(),
|
|
147
|
-
basePath,
|
|
148
|
-
logLevel: this.logger.level,
|
|
149
|
-
/* c8 ignore next 2 */
|
|
150
|
-
port: (this.isEntrypoint ? this.serverConfig?.port || 0 : undefined) ?? true,
|
|
151
|
-
host: (this.isEntrypoint ? this.serverConfig?.hostname : undefined) ?? true
|
|
152
|
-
}
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
try {
|
|
156
|
-
await this.#childManager.inject()
|
|
157
|
-
|
|
158
|
-
const subprocess = this.spawn(command)
|
|
159
|
-
|
|
160
|
-
// Wait for the process to be started
|
|
161
|
-
await new Promise((resolve, reject) => {
|
|
162
|
-
subprocess.on('spawn', resolve)
|
|
163
|
-
subprocess.on('error', reject)
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
// Route anything not catched by child process logger to the logger manually
|
|
167
|
-
/* c8 ignore next 3 */
|
|
168
|
-
subprocess.stdout.pipe(split2()).on('data', line => {
|
|
169
|
-
this.logger.info(line)
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
/* c8 ignore next 3 */
|
|
173
|
-
subprocess.stderr.pipe(split2()).on('data', line => {
|
|
174
|
-
this.logger.error(line)
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
const [exitCode] = await once(subprocess, 'exit')
|
|
178
|
-
|
|
179
|
-
if (exitCode !== 0) {
|
|
180
|
-
const error = new NonZeroExitCode(exitCode)
|
|
181
|
-
error.exitCode = exitCode
|
|
182
|
-
throw error
|
|
183
|
-
}
|
|
184
|
-
} finally {
|
|
185
|
-
await this.#childManager.eject()
|
|
186
|
-
await this.#childManager.close()
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
async startWithCommand (command, loader) {
|
|
191
|
-
const config = this.configManager.current
|
|
192
|
-
const basePath = config.application?.basePath ? cleanBasePath(config.application?.basePath) : ''
|
|
193
|
-
this.#childManager = new ChildManager({
|
|
194
|
-
logger: this.logger,
|
|
195
|
-
loader,
|
|
196
|
-
context: {
|
|
197
|
-
id: this.id,
|
|
198
|
-
// Always use URL to avoid serialization problem in Windows
|
|
199
|
-
root: pathToFileURL(this.root).toString(),
|
|
200
|
-
basePath,
|
|
201
|
-
logLevel: this.logger.level,
|
|
202
|
-
/* c8 ignore next 2 */
|
|
203
|
-
port: (this.isEntrypoint ? this.serverConfig?.port || 0 : undefined) ?? true,
|
|
204
|
-
host: (this.isEntrypoint ? this.serverConfig?.hostname : undefined) ?? true,
|
|
205
|
-
telemetry: this.telemetryConfig
|
|
206
|
-
}
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
this.#childManager.on('config', config => {
|
|
210
|
-
this.subprocessConfig = config
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
try {
|
|
214
|
-
await this.#childManager.inject()
|
|
215
|
-
|
|
216
|
-
this.subprocess = this.spawn(command)
|
|
217
|
-
|
|
218
|
-
// Route anything not catched by child process logger to the logger manually
|
|
219
|
-
/* c8 ignore next 3 */
|
|
220
|
-
this.subprocess.stdout.pipe(split2()).on('data', line => {
|
|
221
|
-
this.logger.info(line)
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
/* c8 ignore next 3 */
|
|
225
|
-
this.subprocess.stderr.pipe(split2()).on('data', line => {
|
|
226
|
-
this.logger.error(line)
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
// Wait for the process to be started
|
|
230
|
-
await new Promise((resolve, reject) => {
|
|
231
|
-
this.subprocess.on('spawn', resolve)
|
|
232
|
-
this.subprocess.on('error', reject)
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
this.#subprocessStarted = true
|
|
236
|
-
} catch (e) {
|
|
237
|
-
this.#childManager.close('SIGKILL')
|
|
238
|
-
throw new Error(`Cannot execute command "${command}": executable not found`)
|
|
239
|
-
} finally {
|
|
240
|
-
await this.#childManager.eject()
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// If the process exits prematurely, terminate the thread with the same code
|
|
244
|
-
this.subprocess.on('exit', code => {
|
|
245
|
-
if (this.#subprocessStarted && typeof code === 'number' && code !== 0) {
|
|
246
|
-
this.#childManager.close('SIGKILL')
|
|
247
|
-
process.exit(code)
|
|
248
|
-
}
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
const [url] = await once(this.#childManager, 'url')
|
|
252
|
-
this.url = url
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
async stopCommand () {
|
|
256
|
-
this.#subprocessStarted = false
|
|
257
|
-
const exitPromise = once(this.subprocess, 'exit')
|
|
258
|
-
|
|
259
|
-
this.#childManager.close(this.subprocessTerminationSignal ?? 'SIGINT')
|
|
260
|
-
this.subprocess.kill(this.subprocessTerminationSignal ?? 'SIGINT')
|
|
261
|
-
await exitPromise
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
getChildManager () {
|
|
265
|
-
return this.#childManager
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
spawn (command) {
|
|
269
|
-
const [executable, ...args] = parseCommandString(command)
|
|
270
|
-
|
|
271
|
-
/* c8 ignore next 3 */
|
|
272
|
-
return platform() === 'win32'
|
|
273
|
-
? spawn(command, { cwd: this.root, shell: true, windowsVerbatimArguments: true })
|
|
274
|
-
: spawn(executable, args, { cwd: this.root })
|
|
275
|
-
}
|
|
276
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { generateRequest } from '@platformatic/itc'
|
|
2
|
-
import { ensureLoggableError } from '@platformatic/utils'
|
|
3
|
-
import { once } from 'node:events'
|
|
4
|
-
import { platform } from 'node:os'
|
|
5
|
-
import { workerData } from 'node:worker_threads'
|
|
6
|
-
import build from 'pino-abstract-transport'
|
|
7
|
-
import { WebSocket } from 'ws'
|
|
8
|
-
import { getSocketPath } from './child-manager.js'
|
|
9
|
-
|
|
10
|
-
/* c8 ignore next 5 */
|
|
11
|
-
function logDirectError (message, error) {
|
|
12
|
-
process._rawDebug(`Logger thread for child process of service ${workerData.id} ${message}.`, {
|
|
13
|
-
error: ensureLoggableError(error)
|
|
14
|
-
})
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/* c8 ignore next 4 */
|
|
18
|
-
function handleUnhandled (type, error) {
|
|
19
|
-
logDirectError(`threw an ${type}`, error)
|
|
20
|
-
process.exit(6)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
process.on('uncaughtException', handleUnhandled.bind(null, 'uncaught exception'))
|
|
24
|
-
process.on('unhandledRejection', handleUnhandled.bind(null, 'unhandled rejection'))
|
|
25
|
-
|
|
26
|
-
export default async function (opts) {
|
|
27
|
-
try {
|
|
28
|
-
/* c8 ignore next */
|
|
29
|
-
const protocol = platform() === 'win32' ? 'ws+unix:' : 'ws+unix://'
|
|
30
|
-
const socket = new WebSocket(`${protocol}${getSocketPath(process.env.PLT_MANAGER_ID)}`)
|
|
31
|
-
|
|
32
|
-
await once(socket, 'open')
|
|
33
|
-
|
|
34
|
-
// Do not process responses but empty the socket inbound queue
|
|
35
|
-
socket.on('message', () => {})
|
|
36
|
-
|
|
37
|
-
/* c8 ignore next 3 */
|
|
38
|
-
socket.on('error', error => {
|
|
39
|
-
logDirectError('threw a socket error', error)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
return build(
|
|
43
|
-
async function (source) {
|
|
44
|
-
for await (const obj of source) {
|
|
45
|
-
socket.send(JSON.stringify(generateRequest('log', { logs: [obj] })))
|
|
46
|
-
}
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
close (_, cb) {
|
|
50
|
-
socket.close()
|
|
51
|
-
cb()
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
)
|
|
55
|
-
/* c8 ignore next 3 */
|
|
56
|
-
} catch (error) {
|
|
57
|
-
logDirectError('threw a connection error', error)
|
|
58
|
-
}
|
|
59
|
-
}
|