@platformatic/basic 2.0.0-alpha.3 → 2.0.0-alpha.4

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 (69) hide show
  1. package/config.d.ts +159 -0
  2. package/eslint.config.js +3 -1
  3. package/index.js +15 -2
  4. package/lib/base.js +7 -5
  5. package/lib/errors.js +8 -0
  6. package/lib/next.js +105 -0
  7. package/lib/schema.js +23 -0
  8. package/lib/server.js +8 -9
  9. package/lib/utils.js +5 -34
  10. package/lib/vite.js +105 -0
  11. package/lib/worker/child-manager.js +78 -0
  12. package/lib/worker/child-process.js +84 -0
  13. package/lib/worker/next-loader.js +137 -0
  14. package/lib/worker/server-listener.js +29 -0
  15. package/package.json +22 -8
  16. package/schema.json +522 -0
  17. package/test/express.test.js +9 -9
  18. package/test/fastify.test.js +9 -9
  19. package/test/fixtures/next/composer-autodetect-prefix/next.config.mjs +5 -0
  20. package/test/fixtures/next/composer-autodetect-prefix/package.json +15 -0
  21. package/test/fixtures/next/composer-autodetect-prefix/platformatic.application.json +8 -0
  22. package/test/fixtures/next/composer-autodetect-prefix/platformatic.runtime.json +21 -0
  23. package/test/fixtures/next/composer-autodetect-prefix/src/app/layout.js +7 -0
  24. package/test/fixtures/next/composer-autodetect-prefix/src/app/page.js +5 -0
  25. package/test/fixtures/next/composer-with-prefix/next.config.js +1 -0
  26. package/test/fixtures/next/composer-with-prefix/package.json +15 -0
  27. package/test/fixtures/next/composer-with-prefix/platformatic.application.json +11 -0
  28. package/test/fixtures/next/composer-with-prefix/platformatic.runtime.json +21 -0
  29. package/test/fixtures/next/composer-with-prefix/src/app/layout.js +7 -0
  30. package/test/fixtures/next/composer-with-prefix/src/app/page.js +5 -0
  31. package/test/fixtures/next/composer-without-prefix/next.config.mjs +3 -0
  32. package/test/fixtures/next/composer-without-prefix/package.json +15 -0
  33. package/test/fixtures/next/composer-without-prefix/platformatic.application.json +8 -0
  34. package/test/fixtures/next/composer-without-prefix/platformatic.runtime.json +21 -0
  35. package/test/fixtures/next/composer-without-prefix/src/app/layout.js +7 -0
  36. package/test/fixtures/next/composer-without-prefix/src/app/page.js +5 -0
  37. package/test/fixtures/next/standalone/next.config.mjs +3 -0
  38. package/test/fixtures/next/standalone/package.json +15 -0
  39. package/test/fixtures/next/standalone/platformatic.runtime.json +18 -0
  40. package/test/fixtures/next/standalone/src/app/layout.js +7 -0
  41. package/test/fixtures/next/standalone/src/app/page.js +5 -0
  42. package/test/fixtures/platformatic-composer/platformatic.composer.json +20 -0
  43. package/test/fixtures/platformatic-composer/platformatic.no-prefix.composer.json +23 -0
  44. package/test/fixtures/platformatic-composer/plugin.js +9 -0
  45. package/test/fixtures/vite/composer-autodetect-prefix/custom.vite.config.js +3 -0
  46. package/test/fixtures/vite/composer-autodetect-prefix/index.html +13 -0
  47. package/test/fixtures/vite/composer-autodetect-prefix/main.js +3 -0
  48. package/test/fixtures/vite/composer-autodetect-prefix/package.json +14 -0
  49. package/test/fixtures/vite/composer-autodetect-prefix/platformatic.application.json +11 -0
  50. package/test/fixtures/vite/composer-autodetect-prefix/platformatic.runtime.json +21 -0
  51. package/test/fixtures/vite/composer-with-prefix/index.html +13 -0
  52. package/test/fixtures/vite/composer-with-prefix/main.js +3 -0
  53. package/test/fixtures/vite/composer-with-prefix/package.json +14 -0
  54. package/test/fixtures/vite/composer-with-prefix/platformatic.application.json +11 -0
  55. package/test/fixtures/vite/composer-with-prefix/platformatic.runtime.json +21 -0
  56. package/test/fixtures/vite/composer-without-prefix/index.html +13 -0
  57. package/test/fixtures/vite/composer-without-prefix/main.js +3 -0
  58. package/test/fixtures/vite/composer-without-prefix/package.json +14 -0
  59. package/test/fixtures/vite/composer-without-prefix/platformatic.application.json +8 -0
  60. package/test/fixtures/vite/composer-without-prefix/platformatic.runtime.json +21 -0
  61. package/test/fixtures/vite/standalone/custom.vite.config.js +3 -0
  62. package/test/fixtures/vite/standalone/index.html +13 -0
  63. package/test/fixtures/vite/standalone/main.js +3 -0
  64. package/test/fixtures/vite/standalone/package.json +14 -0
  65. package/test/fixtures/vite/standalone/platformatic.runtime.json +18 -0
  66. package/test/helper.js +85 -14
  67. package/test/next.test.js +85 -0
  68. package/test/node.test.js +13 -13
  69. package/test/vite.test.js +89 -0
package/config.d.ts ADDED
@@ -0,0 +1,159 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * This file was automatically generated by json-schema-to-typescript.
4
+ * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
5
+ * and run json-schema-to-typescript to regenerate this file.
6
+ */
7
+
8
+ export interface PlatformaticStackable {
9
+ $schema?: string;
10
+ server?: {
11
+ hostname?: string;
12
+ port?: number | string;
13
+ pluginTimeout?: number;
14
+ healthCheck?:
15
+ | boolean
16
+ | {
17
+ enabled?: boolean;
18
+ interval?: number;
19
+ [k: string]: unknown;
20
+ };
21
+ ignoreTrailingSlash?: boolean;
22
+ ignoreDuplicateSlashes?: boolean;
23
+ connectionTimeout?: number;
24
+ keepAliveTimeout?: number;
25
+ maxRequestsPerSocket?: number;
26
+ forceCloseConnections?: boolean | string;
27
+ requestTimeout?: number;
28
+ bodyLimit?: number;
29
+ maxParamLength?: number;
30
+ disableRequestLogging?: boolean;
31
+ exposeHeadRoutes?: boolean;
32
+ logger?:
33
+ | boolean
34
+ | {
35
+ level?: string;
36
+ transport?:
37
+ | {
38
+ target?: string;
39
+ options?: {
40
+ [k: string]: unknown;
41
+ };
42
+ }
43
+ | {
44
+ targets?: {
45
+ target?: string;
46
+ options?: {
47
+ [k: string]: unknown;
48
+ };
49
+ level?: string;
50
+ additionalProperties?: never;
51
+ [k: string]: unknown;
52
+ }[];
53
+ options?: {
54
+ [k: string]: unknown;
55
+ };
56
+ };
57
+ pipeline?: {
58
+ target?: string;
59
+ options?: {
60
+ [k: string]: unknown;
61
+ };
62
+ };
63
+ [k: string]: unknown;
64
+ };
65
+ serializerOpts?: {
66
+ schema?: {
67
+ [k: string]: unknown;
68
+ };
69
+ ajv?: {
70
+ [k: string]: unknown;
71
+ };
72
+ rounding?: "floor" | "ceil" | "round" | "trunc";
73
+ debugMode?: boolean;
74
+ mode?: "debug" | "standalone";
75
+ largeArraySize?: number | string;
76
+ largeArrayMechanism?: "default" | "json-stringify";
77
+ [k: string]: unknown;
78
+ };
79
+ caseSensitive?: boolean;
80
+ requestIdHeader?: string | false;
81
+ requestIdLogLabel?: string;
82
+ jsonShorthand?: boolean;
83
+ trustProxy?: boolean | string | string[] | number;
84
+ http2?: boolean;
85
+ https?: {
86
+ allowHTTP1?: boolean;
87
+ key:
88
+ | string
89
+ | {
90
+ path?: string;
91
+ }
92
+ | (
93
+ | string
94
+ | {
95
+ path?: string;
96
+ }
97
+ )[];
98
+ cert:
99
+ | string
100
+ | {
101
+ path?: string;
102
+ }
103
+ | (
104
+ | string
105
+ | {
106
+ path?: string;
107
+ }
108
+ )[];
109
+ requestCert?: boolean;
110
+ rejectUnauthorized?: boolean;
111
+ };
112
+ cors?: {
113
+ origin?:
114
+ | boolean
115
+ | string
116
+ | (
117
+ | string
118
+ | {
119
+ regexp: string;
120
+ [k: string]: unknown;
121
+ }
122
+ )[]
123
+ | {
124
+ regexp: string;
125
+ [k: string]: unknown;
126
+ };
127
+ methods?: string[];
128
+ /**
129
+ * Comma separated string of allowed headers.
130
+ */
131
+ allowedHeaders?: string;
132
+ exposedHeaders?: string[] | string;
133
+ credentials?: boolean;
134
+ maxAge?: number;
135
+ preflightContinue?: boolean;
136
+ optionsSuccessStatus?: number;
137
+ preflight?: boolean;
138
+ strictPreflight?: boolean;
139
+ hideOptionsRoute?: boolean;
140
+ };
141
+ };
142
+ watch?:
143
+ | {
144
+ enabled?: boolean | string;
145
+ /**
146
+ * @minItems 1
147
+ */
148
+ allow?: [string, ...string[]];
149
+ ignore?: string[];
150
+ }
151
+ | boolean
152
+ | string;
153
+ application?: {
154
+ base?: string;
155
+ };
156
+ vite?: {
157
+ configFile?: string | boolean;
158
+ };
159
+ }
package/eslint.config.js CHANGED
@@ -1,3 +1,5 @@
1
1
  import neostandard from 'neostandard'
2
2
 
3
- export default neostandard({})
3
+ export default neostandard({
4
+ ignores: ['test/tmp/version.js', '**/.next'],
5
+ })
package/index.js CHANGED
@@ -6,8 +6,10 @@ import { resolve } from 'node:path'
6
6
 
7
7
  import { ConfigManager } from '@platformatic/config'
8
8
 
9
+ import { NextStackable } from './lib/next.js'
9
10
  import { packageJson, schema } from './lib/schema.js'
10
11
  import { ServerStackable } from './lib/server.js'
12
+ import { ViteStackable } from './lib/vite.js'
11
13
 
12
14
  const validFields = [
13
15
  'main',
@@ -84,12 +86,23 @@ function transformConfig () {
84
86
 
85
87
  export async function buildStackable (opts) {
86
88
  const root = opts.context.directory
87
- const { entrypoint, hadEntrypointField } = await parsePackageJson(root)
89
+ const {
90
+ entrypoint,
91
+ hadEntrypointField,
92
+ packageJson: { dependencies, devDependencies },
93
+ } = await parsePackageJson(root)
88
94
 
89
95
  const configManager = new ConfigManager({ schema, source: opts.config ?? {} })
90
96
  await configManager.parseAndValidate()
91
97
 
92
- const stackable = new ServerStackable(opts, root, configManager, entrypoint, hadEntrypointField)
98
+ let stackable
99
+ if (dependencies?.next || devDependencies?.next) {
100
+ stackable = new NextStackable(opts, root, configManager)
101
+ } else if (dependencies?.vite || devDependencies?.vite) {
102
+ stackable = new ViteStackable(opts, root, configManager)
103
+ } else {
104
+ stackable = new ServerStackable(opts, root, configManager, entrypoint, hadEntrypointField)
105
+ }
93
106
 
94
107
  return stackable
95
108
  }
package/lib/base.js CHANGED
@@ -12,7 +12,9 @@ export class BaseStackable {
12
12
  this.getGraphqlSchema = null
13
13
 
14
14
  // Setup the logger
15
- const pinoOptions = { level: this.serverConfig?.logger?.level ?? 'trace' }
15
+ const pinoOptions = {
16
+ level: (this.configManager.current.server ?? this.serverConfig)?.logger?.level ?? 'trace',
17
+ }
16
18
 
17
19
  if (this.id) {
18
20
  pinoOptions.name = this.id
@@ -21,8 +23,8 @@ export class BaseStackable {
21
23
 
22
24
  // Setup globals
23
25
  globalThis.platformatic = {
24
- setOpenAPISchema: this.setOpenAPISchema.bind(this),
25
- setGraphQLSchema: this.setGraphQLSchema.bind(this),
26
+ setOpenapiSchema: this.setOpenapiSchema.bind(this),
27
+ setGraphqlSchema: this.setGraphqlSchema.bind(this),
26
28
  }
27
29
  }
28
30
 
@@ -67,11 +69,11 @@ export class BaseStackable {
67
69
  return this.graphqlSchema
68
70
  }
69
71
 
70
- setOpenAPISchema (schema) {
72
+ setOpenapiSchema (schema) {
71
73
  this.openapiSchema = schema
72
74
  }
73
75
 
74
- setGraphQLSchema (schema) {
76
+ setGraphqlSchema (schema) {
75
77
  this.graphqlSchema = schema
76
78
  }
77
79
 
package/lib/errors.js ADDED
@@ -0,0 +1,8 @@
1
+ import createError from '@fastify/error'
2
+
3
+ const ERROR_PREFIX = 'PLT_BASIC'
4
+
5
+ export const UnsupportedVersion = createError(
6
+ `${ERROR_PREFIX}_UNSUPPORTED_VERSION`,
7
+ '%s version %s is not supported. Please use version %s.'
8
+ )
package/lib/next.js ADDED
@@ -0,0 +1,105 @@
1
+ import { once } from 'node:events'
2
+ import { readFile } from 'node:fs/promises'
3
+ import { createRequire } from 'node:module'
4
+ import { dirname, resolve as pathResolve } from 'node:path'
5
+ import { pathToFileURL } from 'node:url'
6
+ import { satisfies } from 'semver'
7
+ import { BaseStackable } from './base.js'
8
+ import { UnsupportedVersion } from './errors.js'
9
+ import { importFile } from './utils.js'
10
+ import { ChildManager } from './worker/child-manager.js'
11
+
12
+ const supportedVersions = '^14.0.0'
13
+
14
+ export class NextStackable extends BaseStackable {
15
+ #basePath
16
+ #next
17
+ #manager
18
+
19
+ constructor (options, root, configManager) {
20
+ super(options, root, configManager)
21
+ this.type = 'next'
22
+ }
23
+
24
+ async init () {
25
+ globalThis[Symbol.for('plt.runtime.itc')].handle('getServiceMeta', this.getMeta.bind(this))
26
+
27
+ this.#next = pathResolve(dirname(createRequire(this.root).resolve('next')), '../..')
28
+ const nextPackage = JSON.parse(await readFile(pathResolve(this.#next, 'package.json')))
29
+
30
+ if (!satisfies(nextPackage.version, supportedVersions)) {
31
+ throw new UnsupportedVersion('next', nextPackage.version, supportedVersions)
32
+ }
33
+ }
34
+
35
+ async start () {
36
+ // Make this idempotent
37
+ if (this.url) {
38
+ return this.url
39
+ }
40
+
41
+ const config = this.configManager.current
42
+ const require = createRequire(this.root)
43
+ const nextRoot = require.resolve('next')
44
+
45
+ const { hostname, port } = this.serverConfig ?? {}
46
+ const serverOptions = {
47
+ host: hostname || '127.0.0.1',
48
+ port: port || 0,
49
+ }
50
+
51
+ this.#basePath = config.application?.base
52
+ ? `/${config.application?.base}`.replaceAll(/\/+/g, '/').replace(/\/$/, '')
53
+ : ''
54
+
55
+ this.#manager = new ChildManager({
56
+ loader: new URL('./worker/next-loader.js', import.meta.url),
57
+ context: {
58
+ // Always use URL to avoid serialization problem in Windows
59
+ root: pathToFileURL(this.root),
60
+ basePath: this.#basePath,
61
+ logger: { id: this.id, level: this.logger.level },
62
+ },
63
+ })
64
+
65
+ this.#manager.on('config', config => {
66
+ this.#basePath = config.basePath.replace(/(^\/)|(\/$)/g, '')
67
+ })
68
+
69
+ const promise = once(this.#manager, 'url')
70
+ await this.#startNext(nextRoot, serverOptions)
71
+ this.url = (await promise)[0]
72
+ }
73
+
74
+ async stop () {
75
+ const exitPromise = once(this.#manager, 'exit')
76
+
77
+ this.#manager.close()
78
+ await exitPromise
79
+ }
80
+
81
+ async getWatchConfig () {
82
+ return {
83
+ enabled: false,
84
+ }
85
+ }
86
+
87
+ getMeta () {
88
+ return {
89
+ composer: {
90
+ tcp: true,
91
+ url: this.url,
92
+ prefix: this.#basePath,
93
+ wantsAbsoluteUrls: true,
94
+ },
95
+ }
96
+ }
97
+
98
+ async #startNext (nextRoot, serverOptions) {
99
+ const { nextDev } = await importFile(pathResolve(this.#next, './dist/cli/next-dev.js'))
100
+
101
+ this.#manager.inject()
102
+ await nextDev(serverOptions, 'default', this.root)
103
+ this.#manager.eject()
104
+ }
105
+ }
package/lib/schema.js CHANGED
@@ -25,6 +25,29 @@ export const schema = {
25
25
  },
26
26
  ],
27
27
  },
28
+ application: {
29
+ type: 'object',
30
+ properties: {
31
+ base: {
32
+ type: 'string',
33
+ },
34
+ },
35
+ additionalProperties: false,
36
+ },
37
+ vite: {
38
+ type: 'object',
39
+ properties: {
40
+ configFile: {
41
+ oneOf: [{ type: 'string' }, { type: 'boolean' }],
42
+ },
43
+ },
44
+ additionalProperties: false,
45
+ },
28
46
  },
29
47
  additionalProperties: false,
30
48
  }
49
+
50
+ /* c8 ignore next 3 */
51
+ if (process.argv[1] === import.meta.filename) {
52
+ console.log(JSON.stringify(schema, null, 2))
53
+ }
package/lib/server.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import inject from 'light-my-request'
2
2
  import { Server } from 'node:http'
3
- import { join } from 'node:path'
4
- import { pathToFileURL } from 'node:url'
3
+ import { resolve as pathResolve } from 'node:path'
5
4
  import { BaseStackable } from './base.js'
6
- import { createPortManager, getServerUrl, injectViaRequest, isFastify } from './utils.js'
5
+ import { getServerUrl, importFile, injectViaRequest, isFastify } from './utils.js'
6
+ import { createServerListener } from './worker/server-listener.js'
7
7
 
8
8
  export class ServerStackable extends BaseStackable {
9
9
  #entrypoint
@@ -39,16 +39,16 @@ export class ServerStackable extends BaseStackable {
39
39
  )
40
40
  }
41
41
 
42
- // The port manager must be created before requiring the entrypoint even if it's not going to be used
42
+ // The server promise must be created before requiring the entrypoint even if it's not going to be used
43
43
  // at all. Otherwise there is chance we miss the listen event.
44
- const portManager = createPortManager()
45
- this.#module = await import(pathToFileURL(join(this.root, this.#entrypoint)))
44
+ const serverPromise = createServerListener()
45
+ this.#module = await importFile(pathResolve(this.root, this.#entrypoint))
46
46
  this.#module = this.#module.default || this.#module
47
47
 
48
48
  // Deal with application
49
49
  if (typeof this.#module.build === 'function') {
50
50
  // We have build function, this Stackable will not use HTTP unless it is the entrypoint
51
- portManager.destroy()
51
+ serverPromise.cancel()
52
52
 
53
53
  this.#app = await this.#module.build()
54
54
  this.#isFastify = isFastify(this.#app)
@@ -61,9 +61,8 @@ export class ServerStackable extends BaseStackable {
61
61
  }
62
62
  } else {
63
63
  // User blackbox function, we wait for it to listen on a port
64
- this.#server = await portManager.getServer()
64
+ this.#server = await serverPromise
65
65
  this.url = getServerUrl(this.#server)
66
- portManager.destroy()
67
66
  }
68
67
 
69
68
  return this.url
package/lib/utils.js CHANGED
@@ -1,6 +1,4 @@
1
- 'use strict'
2
-
3
- import { tracingChannel } from 'node:diagnostics_channel'
1
+ import { pathToFileURL } from 'node:url'
4
2
  import { request } from 'undici'
5
3
 
6
4
  export function getServerUrl (server) {
@@ -14,37 +12,6 @@ export function isFastify (app) {
14
12
  return Object.getOwnPropertySymbols(app).some(s => s.description === 'fastify.state')
15
13
  }
16
14
 
17
- export function createPortManager () {
18
- let resolve
19
- let reject
20
-
21
- const promise = new Promise((_resolve, _reject) => {
22
- resolve = _resolve
23
- reject = _reject
24
- })
25
-
26
- const subscribers = {
27
- asyncStart ({ options }) {
28
- options.port = 0
29
- },
30
- asyncEnd: ({ server }) => {
31
- resolve(server)
32
- },
33
- error: reject,
34
- }
35
-
36
- tracingChannel('net.server.listen').subscribe(subscribers)
37
-
38
- return {
39
- getServer () {
40
- return promise
41
- },
42
- destroy () {
43
- tracingChannel('net.server.listen').unsubscribe(subscribers)
44
- },
45
- }
46
- }
47
-
48
15
  export async function injectViaRequest (baseUrl, injectParams, onInject) {
49
16
  const url = new URL(injectParams.url, baseUrl).href
50
17
  const requestParams = { method: injectParams.method, headers: injectParams.headers }
@@ -75,3 +42,7 @@ export async function injectViaRequest (baseUrl, injectParams, onInject) {
75
42
  throw error
76
43
  }
77
44
  }
45
+
46
+ export function importFile (path) {
47
+ return import(pathToFileURL(path))
48
+ }
package/lib/vite.js ADDED
@@ -0,0 +1,105 @@
1
+ import { createRequire } from 'node:module'
2
+ import { dirname, resolve as pathResolve } from 'node:path'
3
+ import { satisfies } from 'semver'
4
+ import { BaseStackable } from './base.js'
5
+ import { getServerUrl, importFile } from './utils.js'
6
+ import { createServerListener } from './worker/server-listener.js'
7
+
8
+ import { readFile } from 'node:fs/promises'
9
+ import { UnsupportedVersion } from './errors.js'
10
+
11
+ const supportedVersions = '^5.0.0'
12
+
13
+ export class ViteStackable extends BaseStackable {
14
+ #vite
15
+ #app
16
+ #server
17
+ #basePath
18
+
19
+ constructor (options, root, configManager) {
20
+ super(options, root, configManager)
21
+ this.type = 'vite'
22
+ }
23
+
24
+ async init () {
25
+ globalThis[Symbol.for('plt.runtime.itc')].handle('getServiceMeta', this.getMeta.bind(this))
26
+
27
+ this.#vite = dirname(createRequire(this.root).resolve('vite'))
28
+ const vitePackage = JSON.parse(await readFile(pathResolve(this.#vite, 'package.json')))
29
+
30
+ if (!satisfies(vitePackage.version, supportedVersions)) {
31
+ throw new UnsupportedVersion('vite', vitePackage.version, supportedVersions)
32
+ }
33
+ }
34
+
35
+ async start () {
36
+ // Make this idempotent
37
+ if (this.url) {
38
+ return this.url
39
+ }
40
+
41
+ const config = this.configManager.current
42
+
43
+ // Prepare options
44
+ const { hostname, port, https, cors } = this.serverConfig ?? {}
45
+ const configFile = config.vite?.configFile ? pathResolve(this.root, config.vite?.configFile) : undefined
46
+ const basePath = config.application?.base
47
+ ? `/${config.application?.base}`.replaceAll(/\/+/g, '/').replace(/\/$/, '')
48
+ : undefined
49
+
50
+ const serverOptions = {
51
+ host: hostname || '127.0.0.1',
52
+ port: port || 0,
53
+ strictPort: false,
54
+ https,
55
+ cors,
56
+ origin: 'http://localhost',
57
+ hmr: true,
58
+ }
59
+
60
+ // Require Vite
61
+ const serverPromise = createServerListener()
62
+ const { createServer } = await importFile(pathResolve(this.#vite, 'dist/node/index.js'))
63
+
64
+ // Create the server and listen
65
+ this.#app = await createServer({
66
+ root: this.root,
67
+ base: basePath,
68
+ mode: 'development',
69
+ configFile,
70
+ logLevel: this.logger.level,
71
+ clearScreen: false,
72
+ optimizeDeps: { force: false },
73
+ server: serverOptions,
74
+ })
75
+
76
+ await this.#app.listen()
77
+ this.#server = await serverPromise
78
+ this.url = getServerUrl(this.#server)
79
+ }
80
+
81
+ async stop () {
82
+ return this.#app.close()
83
+ }
84
+
85
+ async getWatchConfig () {
86
+ return {
87
+ enabled: false,
88
+ }
89
+ }
90
+
91
+ getMeta () {
92
+ if (!this.#basePath) {
93
+ this.#basePath = this.#app.config.base.replace(/(^\/)|(\/$)/g, '')
94
+ }
95
+
96
+ return {
97
+ composer: {
98
+ tcp: true,
99
+ url: this.url,
100
+ prefix: this.#basePath,
101
+ wantsAbsoluteUrls: true,
102
+ },
103
+ }
104
+ }
105
+ }
@@ -0,0 +1,78 @@
1
+ import { ITC } from '@platformatic/itc'
2
+ import { subscribe, unsubscribe } from 'node:diagnostics_channel'
3
+ import { once } from 'node:events'
4
+ import { workerData } from 'node:worker_threads'
5
+
6
+ export const childProcessWorkerFile = new URL('./child-process.js', import.meta.url)
7
+
8
+ export class ChildManager extends ITC {
9
+ #child
10
+ #listener
11
+ #injectedNodeOptions
12
+ #originalNodeOptions
13
+
14
+ constructor ({ loader, context }) {
15
+ super({})
16
+
17
+ const childHandler = ({ process: child }) => {
18
+ unsubscribe('child_process', childHandler)
19
+
20
+ this.#child = child
21
+ this.#child.once('exit', () => {
22
+ this.emit('exit')
23
+ })
24
+
25
+ this.listen()
26
+ }
27
+
28
+ subscribe('child_process', childHandler)
29
+
30
+ this.handle('log', message => {
31
+ workerData.loggingPort.postMessage(JSON.parse(message))
32
+ })
33
+
34
+ this.#prepareChildEnvironment(loader, context)
35
+ }
36
+
37
+ inject () {
38
+ process.env.NODE_OPTIONS = this.#injectedNodeOptions
39
+ }
40
+
41
+ eject () {
42
+ process.env.NODE_OPTIONS = this.#originalNodeOptions
43
+ }
44
+
45
+ _setupListener (listener) {
46
+ this.#listener = listener
47
+ this.#child.on('message', this.#listener)
48
+ }
49
+
50
+ _send (request) {
51
+ this.#child.send(request)
52
+ }
53
+
54
+ _createClosePromise () {
55
+ return once(this.#child, 'exit')
56
+ }
57
+
58
+ _close () {
59
+ this.#child.removeListener('message', this.#listener)
60
+ this.#child.kill('SIGKILL')
61
+ }
62
+
63
+ #prepareChildEnvironment (loader, context) {
64
+ this.#originalNodeOptions = process.env.NODE_OPTIONS
65
+
66
+ const loaderScript = `
67
+ import { register } from 'node:module';
68
+ globalThis.platformatic=${JSON.stringify(context).replaceAll('"', '\\"')};
69
+ register('${loader}',{ data: globalThis.platformatic });
70
+ `
71
+
72
+ this.#injectedNodeOptions = [
73
+ `--import="data:text/javascript,${loaderScript.replaceAll(/\n/g, '')}"`,
74
+ `--import=${childProcessWorkerFile}`,
75
+ process.env.NODE_OPTIONS ?? '',
76
+ ].join(' ')
77
+ }
78
+ }