@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/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
- }