@platformatic/basic 2.3.1 → 3.4.1

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 CHANGED
@@ -26,7 +26,7 @@ export class BaseStackable {
26
26
  this.configManager = configManager
27
27
  this.serverConfig = deepmerge(options.context.serverConfig ?? {}, configManager.current.server ?? {})
28
28
  this.openapiSchema = null
29
- this.getGraphqlSchema = null
29
+ this.graphqlSchema = null
30
30
  this.isEntrypoint = options.context.isEntrypoint
31
31
  this.isProduction = options.context.isProduction
32
32
 
@@ -65,6 +65,10 @@ export class BaseStackable {
65
65
 
66
66
  const enabled = config.watch?.enabled !== false
67
67
 
68
+ if (!enabled) {
69
+ return { enabled, path: this.root }
70
+ }
71
+
68
72
  return {
69
73
  enabled,
70
74
  path: this.root,
@@ -125,7 +129,7 @@ export class BaseStackable {
125
129
  }
126
130
  }
127
131
 
128
- async buildWithCommand (command, basePath, loader) {
132
+ async buildWithCommand (command, basePath, loader, scripts) {
129
133
  if (Array.isArray(command)) {
130
134
  command = command.join(' ')
131
135
  }
@@ -135,12 +139,14 @@ export class BaseStackable {
135
139
  this.#childManager = new ChildManager({
136
140
  logger: this.logger,
137
141
  loader,
142
+ scripts,
138
143
  context: {
139
144
  id: this.id,
140
145
  // Always use URL to avoid serialization problem in Windows
141
146
  root: pathToFileURL(this.root).toString(),
142
147
  basePath,
143
148
  logLevel: this.logger.level,
149
+ /* c8 ignore next 2 */
144
150
  port: (this.isEntrypoint ? this.serverConfig?.port || 0 : undefined) ?? true,
145
151
  host: (this.isEntrypoint ? this.serverConfig?.hostname : undefined) ?? true
146
152
  }
@@ -158,10 +164,12 @@ export class BaseStackable {
158
164
  })
159
165
 
160
166
  // Route anything not catched by child process logger to the logger manually
167
+ /* c8 ignore next 3 */
161
168
  subprocess.stdout.pipe(split2()).on('data', line => {
162
169
  this.logger.info(line)
163
170
  })
164
171
 
172
+ /* c8 ignore next 3 */
165
173
  subprocess.stderr.pipe(split2()).on('data', line => {
166
174
  this.logger.error(line)
167
175
  })
@@ -191,6 +199,7 @@ export class BaseStackable {
191
199
  root: pathToFileURL(this.root).toString(),
192
200
  basePath,
193
201
  logLevel: this.logger.level,
202
+ /* c8 ignore next 2 */
194
203
  port: (this.isEntrypoint ? this.serverConfig?.port || 0 : undefined) ?? true,
195
204
  host: (this.isEntrypoint ? this.serverConfig?.hostname : undefined) ?? true,
196
205
  telemetry: this.telemetryConfig
@@ -207,10 +216,12 @@ export class BaseStackable {
207
216
  this.subprocess = this.spawn(command)
208
217
 
209
218
  // Route anything not catched by child process logger to the logger manually
219
+ /* c8 ignore next 3 */
210
220
  this.subprocess.stdout.pipe(split2()).on('data', line => {
211
221
  this.logger.info(line)
212
222
  })
213
223
 
224
+ /* c8 ignore next 3 */
214
225
  this.subprocess.stderr.pipe(split2()).on('data', line => {
215
226
  this.logger.error(line)
216
227
  })
@@ -223,14 +234,16 @@ export class BaseStackable {
223
234
 
224
235
  this.#subprocessStarted = true
225
236
  } catch (e) {
237
+ this.#childManager.close('SIGKILL')
226
238
  throw new Error(`Cannot execute command "${command}": executable not found`)
227
239
  } finally {
228
240
  await this.#childManager.eject()
229
241
  }
230
242
 
231
- // // If the process exits prematurely, terminate the thread with the same code
243
+ // If the process exits prematurely, terminate the thread with the same code
232
244
  this.subprocess.on('exit', code => {
233
245
  if (this.#subprocessStarted && typeof code === 'number' && code !== 0) {
246
+ this.#childManager.close('SIGKILL')
234
247
  process.exit(code)
235
248
  }
236
249
  })
@@ -248,9 +261,14 @@ export class BaseStackable {
248
261
  await exitPromise
249
262
  }
250
263
 
264
+ getChildManager () {
265
+ return this.#childManager
266
+ }
267
+
251
268
  spawn (command) {
252
269
  const [executable, ...args] = parseCommandString(command)
253
270
 
271
+ /* c8 ignore next 3 */
254
272
  return platform() === 'win32'
255
273
  ? spawn(command, { cwd: this.root, shell: true, windowsVerbatimArguments: true })
256
274
  : spawn(executable, args, { cwd: this.root })
package/lib/errors.js CHANGED
@@ -17,5 +17,5 @@ export const UnsupportedVersion = createError(
17
17
 
18
18
  export const NonZeroExitCode = createError(
19
19
  `${ERROR_PREFIX}_NON_ZERO_EXIT_CODE`,
20
- 'Process exit with non zero exit code %d.'
20
+ 'Process exited with non zero exit code %d.'
21
21
  )
package/lib/utils.js CHANGED
@@ -9,15 +9,15 @@ export function getServerUrl (server) {
9
9
  }
10
10
 
11
11
  export async function injectViaRequest (baseUrl, injectParams, onInject) {
12
- const url = new URL(injectParams.url, baseUrl).href
13
- const requestParams = { method: injectParams.method, headers: injectParams.headers }
12
+ try {
13
+ const url = new URL(injectParams.url, baseUrl).href
14
+ const requestParams = { method: injectParams.method, headers: injectParams.headers }
14
15
 
15
- if (injectParams.body) {
16
- const body = injectParams.body
17
- requestParams.body = typeof body === 'object' ? JSON.stringify(body) : body
18
- }
16
+ if (injectParams.body) {
17
+ const body = injectParams.body
18
+ requestParams.body = typeof body === 'object' ? JSON.stringify(body) : body
19
+ }
19
20
 
20
- try {
21
21
  const { statusCode, headers, body } = await request(url, requestParams)
22
22
 
23
23
  const rawPayload = Buffer.from(await body.arrayBuffer())
@@ -53,11 +53,13 @@ export function ensureFileUrl (pathOrUrl) {
53
53
  return pathToFileURL(pathOrUrl)
54
54
  }
55
55
 
56
+ /* c8 ignore next 4 */
56
57
  // This is to avoid common path/URL problems on Windows
57
58
  export function importFile (path) {
58
59
  return import(ensureFileUrl(path))
59
60
  }
60
61
 
62
+ /* c8 ignore next 6 */
61
63
  export function resolvePackage (root, pkg) {
62
64
  const require = createRequire(root)
63
65
 
@@ -11,17 +11,18 @@ import { request } from 'undici'
11
11
  import { WebSocketServer } from 'ws'
12
12
  import { exitCodes } from '../errors.js'
13
13
  import { ensureFileUrl } from '../utils.js'
14
-
15
14
  export const isWindows = platform() === 'win32'
16
15
 
17
16
  // In theory we could use the context.id to namespace even more, but due to
18
17
  // UNIX socket length limitation on MacOS, we don't.
19
- function generateChildrenId (context) {
18
+ export function generateChildrenId (context) {
20
19
  return [process.pid, Date.now()].join('-')
21
20
  }
22
21
 
23
22
  export function getSocketPath (id) {
24
23
  let socketPath = null
24
+
25
+ /* c8 ignore next 7 */
25
26
  if (platform() === 'win32') {
26
27
  socketPath = `\\\\.\\pipe\\plt-${id}`
27
28
  } else {
@@ -59,7 +60,9 @@ export class ChildManager extends ITC {
59
60
  ...itcOpts,
60
61
  handlers: {
61
62
  log: message => {
62
- return this.#log(message)
63
+ /* c8 ignore next */
64
+ const logs = Array.isArray(message.logs) ? message.logs : [message.logs]
65
+ this._forwardLogs(logs)
63
66
  },
64
67
  fetch: request => {
65
68
  return this.#fetch(request)
@@ -106,6 +109,7 @@ export class ChildManager extends ITC {
106
109
  this.#clients.delete(ws)
107
110
  })
108
111
 
112
+ /* c8 ignore next 7 */
109
113
  ws.on('error', error => {
110
114
  this.#handleUnexpectedError(
111
115
  error,
@@ -121,13 +125,16 @@ export class ChildManager extends ITC {
121
125
  }
122
126
 
123
127
  async close (signal) {
124
- await rm(this.#dataPath)
128
+ if (this.#dataPath) {
129
+ await rm(this.#dataPath, { force: true })
130
+ }
125
131
 
126
132
  for (const client of this.#clients) {
127
133
  this.#currentClient = client
128
134
  this._send(generateNotification('close', signal))
129
135
  }
130
136
 
137
+ this.#server?.close()
131
138
  super.close()
132
139
  }
133
140
 
@@ -163,16 +170,24 @@ export class ChildManager extends ITC {
163
170
  process.env.PLT_MANAGER_ID = ''
164
171
  }
165
172
 
173
+ getSocketPath () {
174
+ return this.#socketPath
175
+ }
176
+
177
+ getClients () {
178
+ return this.#clients
179
+ }
180
+
166
181
  register () {
167
182
  register(this.#loader, { data: this.#context })
168
183
  }
169
184
 
170
185
  send (client, name, message) {
171
186
  this.#currentClient = client
172
- super.send(name, message)
187
+ return super.send(name, message)
173
188
  }
174
189
 
175
- _send (message) {
190
+ _send (message, stringify = true) {
176
191
  if (!this.#currentClient) {
177
192
  this.#currentClient = this.#requests.get(message.reqId)
178
193
  this.#requests.delete(message.reqId)
@@ -182,7 +197,7 @@ export class ChildManager extends ITC {
182
197
  }
183
198
  }
184
199
 
185
- this.#currentClient.send(JSON.stringify(message))
200
+ this.#currentClient.send(stringify ? JSON.stringify(message) : message)
186
201
  this.#currentClient = null
187
202
  }
188
203
 
@@ -198,8 +213,8 @@ export class ChildManager extends ITC {
198
213
  this.#server.close()
199
214
  }
200
215
 
201
- #log (message) {
202
- const logs = Array.isArray(message.logs) ? message.logs : [message.logs]
216
+ /* c8 ignore next 3 */
217
+ _forwardLogs (logs) {
203
218
  workerData.loggingPort.postMessage({ logs: logs.map(m => JSON.stringify(m)) })
204
219
  }
205
220
 
@@ -86,6 +86,7 @@ export class ChildProcess extends ITC {
86
86
  constructor () {
87
87
  super({ throwOnMissingHandler: false, name: `${process.env.PLT_MANAGER_ID}-child-process` })
88
88
 
89
+ /* c8 ignore next */
89
90
  const protocol = platform() === 'win32' ? 'ws+unix:' : 'ws+unix://'
90
91
  this.#socket = new WebSocket(`${protocol}${getSocketPath(process.env.PLT_MANAGER_ID)}`)
91
92
  this.#pendingMessages = []
@@ -109,6 +110,7 @@ export class ChildProcess extends ITC {
109
110
  // Never hang the process on this socket.
110
111
  this.#socket._socket.unref()
111
112
 
113
+ /* c8 ignore next 3 */
112
114
  for (const message of this.#pendingMessages) {
113
115
  this.#socket.send(message)
114
116
  }
@@ -123,6 +125,7 @@ export class ChildProcess extends ITC {
123
125
  }
124
126
  })
125
127
 
128
+ /* c8 ignore next 5 */
126
129
  this.#socket.on('error', error => {
127
130
  process._rawDebug(error)
128
131
  // There is nothing to log here as the connection with the parent thread is lost. Exit with a special code
@@ -131,6 +134,7 @@ export class ChildProcess extends ITC {
131
134
  }
132
135
 
133
136
  _send (message) {
137
+ /* c8 ignore next 4 */
134
138
  if (this.#socket.readyState === WebSocket.CONNECTING) {
135
139
  this.#pendingMessages.push(JSON.stringify(message))
136
140
  return
@@ -143,6 +147,7 @@ export class ChildProcess extends ITC {
143
147
  return once(this.#socket, 'close')
144
148
  }
145
149
 
150
+ /* c8 ignore next 3 */
146
151
  _close () {
147
152
  this.#socket.close()
148
153
  }
@@ -167,6 +172,7 @@ export class ChildProcess extends ITC {
167
172
  }
168
173
  }
169
174
 
175
+ /* c8 ignore next 5 */
170
176
  #setupTelemetry () {
171
177
  if (globalThis.platformatic.telemetry) {
172
178
  setupNodeHTTPTelemetry(globalThis.platformatic.telemetry, this.#logger)
@@ -202,11 +208,12 @@ export class ChildProcess extends ITC {
202
208
  }
203
209
 
204
210
  const { family, address: host, port } = address
211
+ /* c8 ignore next */
205
212
  const url = new URL(family === 'IPv6' ? `http://[${host}]:${port}` : `http://${host}:${port}`).origin
206
213
 
207
214
  this.notify('url', url)
208
215
  },
209
- error: error => {
216
+ error: ({ error }) => {
210
217
  tracingChannel('net.server.listen').unsubscribe(subscribers)
211
218
  this.notify('error', error)
212
219
  }
@@ -226,7 +233,8 @@ export class ChildProcess extends ITC {
226
233
  `Child process for service ${globalThis.platformatic.id} threw an ${type}.`
227
234
  )
228
235
 
229
- process.exit(exitCodes.PROCESS_UNHANDLED_ERROR)
236
+ // Give some time to the logger and ITC notifications to land before shutting down
237
+ setTimeout(() => process.exit(exitCodes.PROCESS_UNHANDLED_ERROR), 100)
230
238
  }
231
239
 
232
240
  process.on('uncaughtException', handleUnhandled.bind(this, 'uncaught exception'))
@@ -255,6 +263,7 @@ async function main () {
255
263
  globalThis[Symbol.for('plt.children.itc')] = new ChildProcess()
256
264
  }
257
265
 
266
+ /* c8 ignore next 3 */
258
267
  if (!isWindows || basename(process.argv.at(-1)) !== 'npm-prefix.js') {
259
268
  await main()
260
269
  }
@@ -7,12 +7,14 @@ import build from 'pino-abstract-transport'
7
7
  import { WebSocket } from 'ws'
8
8
  import { getSocketPath } from './child-manager.js'
9
9
 
10
+ /* c8 ignore next 5 */
10
11
  function logDirectError (message, error) {
11
12
  process._rawDebug(`Logger thread for child process of service ${workerData.id} ${message}.`, {
12
13
  error: ensureLoggableError(error)
13
14
  })
14
15
  }
15
16
 
17
+ /* c8 ignore next 4 */
16
18
  function handleUnhandled (type, error) {
17
19
  logDirectError(`threw an ${type}`, error)
18
20
  process.exit(6)
@@ -23,6 +25,7 @@ process.on('unhandledRejection', handleUnhandled.bind(null, 'unhandled rejection
23
25
 
24
26
  export default async function (opts) {
25
27
  try {
28
+ /* c8 ignore next */
26
29
  const protocol = platform() === 'win32' ? 'ws+unix:' : 'ws+unix://'
27
30
  const socket = new WebSocket(`${protocol}${getSocketPath(process.env.PLT_MANAGER_ID)}`)
28
31
 
@@ -31,6 +34,7 @@ export default async function (opts) {
31
34
  // Do not process responses but empty the socket inbound queue
32
35
  socket.on('message', () => {})
33
36
 
37
+ /* c8 ignore next 3 */
34
38
  socket.on('error', error => {
35
39
  logDirectError('threw a socket error', error)
36
40
  })
@@ -48,6 +52,7 @@ export default async function (opts) {
48
52
  }
49
53
  }
50
54
  )
55
+ /* c8 ignore next 3 */
51
56
  } catch (error) {
52
57
  logDirectError('threw a connection error', error)
53
58
  }
@@ -1,7 +1,7 @@
1
1
  import { withResolvers } from '@platformatic/utils'
2
2
  import { subscribe, tracingChannel, unsubscribe } from 'node:diagnostics_channel'
3
3
 
4
- export function createServerListener (overridePort = true, overrideHost = true) {
4
+ export function createServerListener (overridePort = true, overrideHost) {
5
5
  const { promise, resolve, reject } = withResolvers()
6
6
 
7
7
  const subscribers = {
@@ -19,9 +19,10 @@ export function createServerListener (overridePort = true, overrideHost = true)
19
19
  }
20
20
  },
21
21
  asyncEnd ({ server }) {
22
+ cancel()
22
23
  resolve(server)
23
24
  },
24
- error (error) {
25
+ error ({ error }) {
25
26
  cancel()
26
27
  reject(error)
27
28
  }
@@ -32,8 +33,10 @@ export function createServerListener (overridePort = true, overrideHost = true)
32
33
  }
33
34
 
34
35
  tracingChannel('net.server.listen').subscribe(subscribers)
35
- promise.finally(cancel)
36
- promise.cancel = resolve.bind(null, null)
36
+ promise.cancel = function () {
37
+ cancel()
38
+ resolve(null)
39
+ }
37
40
 
38
41
  return promise
39
42
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/basic",
3
- "version": "2.3.1",
3
+ "version": "3.4.1",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -23,10 +23,10 @@
23
23
  "split2": "^4.2.0",
24
24
  "undici": "^6.19.5",
25
25
  "ws": "^8.18.0",
26
- "@platformatic/itc": "2.3.1",
27
- "@platformatic/config": "2.3.1",
28
- "@platformatic/utils": "2.3.1",
29
- "@platformatic/telemetry": "2.3.1"
26
+ "@platformatic/config": "3.4.1",
27
+ "@platformatic/itc": "3.4.1",
28
+ "@platformatic/telemetry": "3.4.1",
29
+ "@platformatic/utils": "3.4.1"
30
30
  },
31
31
  "devDependencies": {
32
32
  "borp": "^0.17.0",
package/schema.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://schemas.platformatic.dev/@platformatic/basic/2.3.1.json",
2
+ "$id": "https://schemas.platformatic.dev/@platformatic/basic/3.4.1.json",
3
3
  "$schema": "http://json-schema.org/draft-07/schema#",
4
4
  "title": "Platformatic Stackable",
5
5
  "type": "object",