@platformatic/runtime 3.54.0 → 3.56.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 CHANGED
@@ -42,6 +42,8 @@ export type PlatformaticRuntimeConfig = {
42
42
  maxHeapTotal?: number | string;
43
43
  maxYoungGeneration?: number | string;
44
44
  codeRangeSize?: number | string;
45
+ bufferPoolSize?: number | string;
46
+ defaultHighWaterMark?: number | string;
45
47
  };
46
48
  dependencies?: string[];
47
49
  arguments?: string[];
@@ -238,6 +240,8 @@ export type PlatformaticRuntimeConfig = {
238
240
  maxHeapTotal?: number | string;
239
241
  maxYoungGeneration?: number | string;
240
242
  codeRangeSize?: number | string;
243
+ bufferPoolSize?: number | string;
244
+ defaultHighWaterMark?: number | string;
241
245
  };
242
246
  undici?: {
243
247
  agentOptions?: {
@@ -340,6 +344,10 @@ export type PlatformaticRuntimeConfig = {
340
344
  * The label name to use for the application identifier in metrics (e.g., applicationId, serviceId)
341
345
  */
342
346
  applicationLabel?: string;
347
+ /**
348
+ * Enable outgoing HTTP client request duration metrics
349
+ */
350
+ httpClientMetrics?: boolean | string;
343
351
  readiness?:
344
352
  | boolean
345
353
  | {
package/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { FastifyError } from '@fastify/error'
2
2
  import { Configuration, ConfigurationOptions, logFatalError, parseArgs } from '@platformatic/foundation'
3
3
  import { BaseGenerator } from '@platformatic/generators'
4
+ import { PlatformaticGlobal } from '@platformatic/globals'
4
5
  import { JSONSchemaType } from 'ajv'
5
6
  import * as colorette from 'colorette'
6
7
  import { Logger } from 'pino'
@@ -27,6 +28,12 @@ export interface ApplicationsCommands {
27
28
  help: Record<string, string | (() => string)>
28
29
  }
29
30
 
31
+ export interface LoopbackMessagingOptions {
32
+ logger?: Logger
33
+ mount?: boolean
34
+ runtimeConfig?: { messagingTimeout?: number; [key: string]: unknown }
35
+ }
36
+
30
37
  export namespace errors {
31
38
  export const RuntimeExitedError: () => FastifyError
32
39
  export const UnknownRuntimeAPICommandError: (command: string) => FastifyError
@@ -61,7 +68,6 @@ export namespace symbols {
61
68
  export const kWorkerStatus: unique symbol
62
69
  export const kWorkerHealthSignals: unique symbol
63
70
  export const kStderrMarker: string
64
- export const kInterceptors: unique symbol
65
71
  export const kWorkersBroadcast: unique symbol
66
72
  }
67
73
 
@@ -142,7 +148,26 @@ export class WrappedGenerator extends BaseGenerator {}
142
148
 
143
149
  export declare const schema: JSONSchemaType<PlatformaticRuntimeConfig>
144
150
 
145
- export declare class Runtime {}
151
+ export declare class Runtime {
152
+ init (): Promise<void>
153
+ start (silent?: boolean): Promise<string | undefined>
154
+ stop (silent?: boolean): Promise<void>
155
+ close (silent?: boolean): Promise<void>
156
+ restart (applications?: string[]): Promise<string | undefined>
157
+ inject (id: string, injectParams: InjectParams): Promise<InjectResponse>
158
+ getUrl (): string | undefined
159
+ getRuntimeStatus (): string
160
+ getRuntimeMetadata (): Promise<RuntimeMetadata>
161
+ getRuntimeEnv (): Record<string, string>
162
+ getRuntimeConfig (includeMeta?: boolean): Record<string, unknown>
163
+ getApplicationsIds (): string[]
164
+ getApplicationDetails (id: string, allowUnloaded?: boolean): Promise<ApplicationDetails>
165
+ startApplication (id: string, silent?: boolean): Promise<void>
166
+ stopApplication (id: string, silent?: boolean): Promise<void>
167
+ restartApplication (id: string): Promise<void>
168
+ addApplications (applications: unknown[], start?: boolean): Promise<ApplicationDetails[]>
169
+ removeApplications (applications: string[], silent?: boolean): Promise<ApplicationDetails[]>
170
+ }
146
171
 
147
172
  export function wrapInRuntimeConfig (
148
173
  config: Configuration<unknown>,
@@ -172,3 +197,8 @@ export declare function transform (
172
197
  ): Promise<RuntimeConfiguration>
173
198
 
174
199
  export declare function loadApplicationsCommands (): Promise<ApplicationsCommands>
200
+
201
+ export declare function setupLoopbackMessaging (
202
+ targetId: string,
203
+ options?: LoopbackMessagingOptions
204
+ ): PlatformaticGlobal['messaging'] & { unmount: () => void }
package/index.js CHANGED
@@ -176,6 +176,7 @@ export async function create (configOrRoot, sourceOrConfig, context) {
176
176
  export { prepareApplication, transform, wrapInRuntimeConfig } from './lib/config.js'
177
177
  export * as errors from './lib/errors.js'
178
178
  export { RuntimeGenerator as Generator, WrappedGenerator } from './lib/generator.js'
179
+ export { setupLoopbackMessaging } from './lib/loopback-messaging.js'
179
180
  export { Runtime } from './lib/runtime.js'
180
181
  export { schema } from './lib/schema.js'
181
182
  export * from './lib/version.js'
package/lib/config.js CHANGED
@@ -18,8 +18,7 @@ import {
18
18
  InspectorHostError,
19
19
  InspectorPortError,
20
20
  InvalidArgumentError,
21
- InvalidEntrypointError,
22
- MissingEntrypointError
21
+ InvalidEntrypointError
23
22
  } from './errors.js'
24
23
  import { schema } from './schema.js'
25
24
  import { upgrade } from './upgrade.js'
@@ -438,15 +437,8 @@ export async function transform (config, _, context) {
438
437
  }
439
438
  }
440
439
 
441
- if (!hasValidEntrypoint && !context.allowMissingEntrypoint) {
442
- if (config.entrypoint) {
443
- throw new InvalidEntrypointError(config.entrypoint)
444
- } else if (applications.length >= 1) {
445
- throw new MissingEntrypointError()
446
- }
447
- // If there are no applications, and no entrypoint it's an empty app.
448
- // It won't start, but we should be able to parse and operate on it,
449
- // like adding other applications.
440
+ if (!hasValidEntrypoint && config.entrypoint && !context.allowMissingEntrypoint) {
441
+ throw new InvalidEntrypointError(config.entrypoint)
450
442
  }
451
443
 
452
444
  if (typeof config.metrics === 'boolean') {
package/lib/errors.js CHANGED
@@ -49,6 +49,10 @@ export const ApplicationStartTimeoutError = createError(
49
49
  `${ERROR_PREFIX}_APPLICATION_START_TIMEOUT`,
50
50
  "Application with id '%s' failed to start in %dms."
51
51
  )
52
+ export const ApplicationDependencyNotFoundError = createError(
53
+ `${ERROR_PREFIX}_APPLICATION_DEPENDENCY_NOT_FOUND`,
54
+ 'Application dependency %s not found. Available applications are: %s'
55
+ )
52
56
  export const ApplicationsDependenciesCycleError = createError(
53
57
  `${ERROR_PREFIX}_APPLICATIONS_DEPENDENCIES_CYCLE`,
54
58
  'Detected a cycle in the applications dependencies: %s'
package/lib/generator.js CHANGED
@@ -19,17 +19,6 @@ import { transform } from './config.js'
19
19
  import { schema } from './schema.js'
20
20
  import { getArrayDifference } from './utils.js'
21
21
 
22
- const wrappableProperties = {
23
- logger: {
24
- level: '{PLT_SERVER_LOGGER_LEVEL}'
25
- },
26
- server: {
27
- hostname: '{PLT_SERVER_HOSTNAME}',
28
- port: '{PORT}'
29
- },
30
- managementApi: '{PLT_MANAGEMENT_API}'
31
- }
32
-
33
22
  const engines = {
34
23
  node: '>=22.19.0'
35
24
  }
@@ -42,13 +31,36 @@ const NoApplicationNamedError = createError(
42
31
  )
43
32
  const NoEntryPointError = createError(`${ERROR_PREFIX}_NO_ENTRYPOINT`, 'No entrypoint had been defined.')
44
33
 
34
+ function getRuntimeWrappableProperties (skipServer) {
35
+ const wrappableProperties = {
36
+ logger: {
37
+ level: '{PLT_SERVER_LOGGER_LEVEL}'
38
+ },
39
+ managementApi: '{PLT_MANAGEMENT_API}'
40
+ }
41
+
42
+ if (!skipServer) {
43
+ wrappableProperties.server = {
44
+ hostname: '{PLT_SERVER_HOSTNAME}',
45
+ port: '{PORT}'
46
+ }
47
+ }
48
+
49
+ return wrappableProperties
50
+ }
51
+
45
52
  function getRuntimeBaseEnvVars (config) {
46
- return {
47
- PLT_SERVER_HOSTNAME: '127.0.0.1',
48
- PORT: config.port || 3042,
49
- PLT_SERVER_LOGGER_LEVEL: config.logLevel || 'info',
50
- PLT_MANAGEMENT_API: true
53
+ const env = {}
54
+
55
+ if (!config.skipServer) {
56
+ env.PLT_SERVER_HOSTNAME = '127.0.0.1'
57
+ env.PORT = config.port || 3042
51
58
  }
59
+
60
+ env.PLT_SERVER_LOGGER_LEVEL = config.logLevel || 'info'
61
+ env.PLT_MANAGEMENT_API = true
62
+
63
+ return env
52
64
  }
53
65
 
54
66
  export class RuntimeGenerator extends BaseGenerator {
@@ -195,7 +207,7 @@ export class RuntimeGenerator extends BaseGenerator {
195
207
  path: this.config.autoload || this.applicationsFolder,
196
208
  exclude: ['docs']
197
209
  },
198
- ...wrappableProperties
210
+ ...getRuntimeWrappableProperties(this.config.skipServer)
199
211
  }
200
212
 
201
213
  return config
@@ -251,13 +263,15 @@ export class RuntimeGenerator extends BaseGenerator {
251
263
  return
252
264
  }
253
265
 
254
- // port
255
- this.questions.push({
256
- type: 'input',
257
- name: 'port',
258
- default: 3042,
259
- message: 'What port do you want to use?'
260
- })
266
+ if (!this.config.skipServer) {
267
+ // port
268
+ this.questions.push({
269
+ type: 'input',
270
+ name: 'port',
271
+ default: 3042,
272
+ message: 'What port do you want to use?'
273
+ })
274
+ }
261
275
  }
262
276
 
263
277
  setApplicationsDirectory () {
@@ -611,7 +625,7 @@ export class WrappedGenerator extends BaseGenerator {
611
625
  async #createConfigFile () {
612
626
  const config = {
613
627
  $schema: `https://schemas.platformatic.dev/${this.module}/${this.platformaticVersion}.json`,
614
- runtime: wrappableProperties
628
+ runtime: getRuntimeWrappableProperties(this.config.skipServer)
615
629
  }
616
630
 
617
631
  this.addFile({
package/lib/logger.js CHANGED
@@ -63,6 +63,7 @@ const colors = [
63
63
  function createLoggerContext () {
64
64
  const context = {
65
65
  colors: {},
66
+ closeables: [],
66
67
  maxLength: 0,
67
68
  updatePrefixes (ids) {
68
69
  context.colors = {}
@@ -179,28 +180,29 @@ export async function createLogger (config) {
179
180
  const multiStream = pino.multistream([{ stream: cliStream, level: loggerConfig.level }])
180
181
 
181
182
  if (config.telemetry && config.logger.openTelemetryExporter) {
182
- multiStream.add(
183
- pino.transport({
184
- target: 'pino-opentelemetry-transport',
185
- options: {
186
- resourceAttributes: {
187
- 'service.name': config.telemetry.applicationName,
188
- 'service.version': config.telemetry.version
189
- },
190
- logRecordProcessorOptions: [
191
- {
192
- recordProcessorType: 'simple',
193
- exporterOptions: {
194
- protocol: config.logger.openTelemetryExporter.protocol,
195
- httpExporterOptions: {
196
- url: config.logger.openTelemetryExporter.url
197
- }
183
+ const openTelemetryTransport = pino.transport({
184
+ target: 'pino-opentelemetry-transport',
185
+ options: {
186
+ resourceAttributes: {
187
+ 'service.name': config.telemetry.applicationName,
188
+ 'service.version': config.telemetry.version
189
+ },
190
+ logRecordProcessorOptions: [
191
+ {
192
+ recordProcessorType: 'simple',
193
+ exporterOptions: {
194
+ protocol: config.logger.openTelemetryExporter.protocol,
195
+ httpExporterOptions: {
196
+ url: config.logger.openTelemetryExporter.url
198
197
  }
199
198
  }
200
- ]
201
- }
202
- })
203
- )
199
+ }
200
+ ]
201
+ }
202
+ })
203
+
204
+ context.closeables.push(openTelemetryTransport)
205
+ multiStream.add(openTelemetryTransport)
204
206
  }
205
207
 
206
208
  const logsFileMb = 5
@@ -0,0 +1,78 @@
1
+ import { abstractLogger } from '@platformatic/foundation'
2
+ import { getMessaging, removeGlobals, updateGlobals } from '@platformatic/globals'
3
+ import { MessageChannel } from 'node:worker_threads'
4
+ import { MessagingITC } from './worker/messaging.js'
5
+
6
+ class LoopbackMessagingITC extends MessagingITC {
7
+ #application
8
+ #channel
9
+
10
+ constructor (id, application, runtimeConfig, logger, channel) {
11
+ super(id, runtimeConfig, logger)
12
+
13
+ this.#application = application
14
+ this.#channel = channel
15
+ this.addSource(channel)
16
+ }
17
+
18
+ // Force the next worker to be the channel
19
+ _getNextWorker () {
20
+ return { application: this.#application, channel: this.#channel }
21
+ }
22
+ }
23
+
24
+ class LoopbackMessaging extends LoopbackMessagingITC {
25
+ #target
26
+ #channel
27
+ #previousGlobals
28
+
29
+ constructor (targetId, config, logger) {
30
+ const channel = new MessageChannel()
31
+
32
+ super('loopback-messaging', targetId, config, logger, channel.port1)
33
+ this.#channel = channel
34
+ this.#target = new LoopbackMessagingITC(targetId, 'loopback-messaging', config, logger, channel.port2)
35
+ }
36
+
37
+ mount () {
38
+ this.#previousGlobals = getMessaging({ throwOnMissing: false })
39
+
40
+ updateGlobals({
41
+ messaging: {
42
+ handle: this.#target.handle.bind(this.#target),
43
+ send: this.#target.send.bind(this.#target),
44
+ notify: this.#target.notify.bind(this.#target)
45
+ }
46
+ })
47
+
48
+ return this
49
+ }
50
+
51
+ unmount () {
52
+ if (this.#previousGlobals) {
53
+ updateGlobals({ messaging: this.#previousGlobals })
54
+ } else {
55
+ removeGlobals(['messaging'])
56
+ }
57
+
58
+ this.close()
59
+ this.#target.close()
60
+ this.#channel.port1.close()
61
+ this.#channel.port2.close()
62
+ }
63
+ }
64
+
65
+ export function setupLoopbackMessaging (targetId, options) {
66
+ options ??= {}
67
+
68
+ const runtimeConfig = { ...(options.runtimeConfig ?? {}) }
69
+ runtimeConfig.messagingTimeout ??= 5000
70
+
71
+ const messaging = new LoopbackMessaging(targetId, runtimeConfig, options.logger ?? abstractLogger)
72
+
73
+ if (options.mount !== false) {
74
+ messaging.mount()
75
+ }
76
+
77
+ return messaging
78
+ }
@@ -9,7 +9,7 @@ import {
9
9
  } from '@platformatic/foundation'
10
10
  import fastify from 'fastify'
11
11
  import { platform, tmpdir } from 'node:os'
12
- import { join } from 'node:path'
12
+ import { dirname, join } from 'node:path'
13
13
  import { setTimeout as sleep } from 'node:timers/promises'
14
14
  import { createWebSocketStream } from 'ws'
15
15
  import { prepareApplication } from './config.js'
@@ -342,8 +342,8 @@ export async function startManagementApi (runtime, config) {
342
342
  const customSocket = typeof config === 'object' ? config?.socket : null
343
343
 
344
344
  const runtimePIDDir = join(PLATFORMATIC_TMP_DIR, runtimePID.toString())
345
- if (platform() !== 'win32' && !customSocket) {
346
- await createDirectory(runtimePIDDir, true)
345
+ if (platform() !== 'win32') {
346
+ await createDirectory(customSocket ? dirname(customSocket) : runtimePIDDir, typeof customSocket !== 'string')
347
347
  }
348
348
 
349
349
  let socketPath = null
package/lib/runtime.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  kTimeout,
10
10
  parseMemorySize
11
11
  } from '@platformatic/foundation'
12
+ import { getExecutable } from '@platformatic/globals'
12
13
  import { ITC } from '@platformatic/itc'
13
14
  import { collectProcessMetrics, client as metricsClient } from '@platformatic/metrics'
14
15
  import fastify from 'fastify'
@@ -20,6 +21,7 @@ import { createRequire } from 'node:module'
20
21
  import { availableParallelism } from 'node:os'
21
22
  import { dirname, isAbsolute, join } from 'node:path'
22
23
  import { Readable } from 'node:stream'
24
+ import { finished } from 'node:stream/promises'
23
25
  import { setImmediate as immediate, setTimeout as sleep } from 'node:timers/promises'
24
26
  import { pathToFileURL } from 'node:url'
25
27
  import { Worker } from 'node:worker_threads'
@@ -35,7 +37,6 @@ import {
35
37
  CannotRemoveEntrypointError,
36
38
  InvalidArgumentError,
37
39
  MessagingError,
38
- MissingEntrypointError,
39
40
  MissingPprofCapture,
40
41
  RuntimeAbortedError,
41
42
  WorkerInterceptorJoinTimeoutError,
@@ -273,14 +274,18 @@ export class Runtime extends EventEmitter {
273
274
  await this.init()
274
275
  }
275
276
 
276
- if (typeof this.#config.entrypoint === 'undefined') {
277
- throw new MissingEntrypointError()
278
- }
279
277
  this.#updateStatus('starting')
280
278
  this.#createWorkersBroadcastChannel()
281
279
 
282
280
  try {
283
- await this.startApplications(this.getApplicationsIds(), silent)
281
+ const applications = this.getApplicationsIds()
282
+ await this.startApplications(applications, silent)
283
+
284
+ if (applications.length === 0) {
285
+ this.#updateStatus('started')
286
+ await this.close(silent)
287
+ return
288
+ }
284
289
 
285
290
  if (this.#config.inspectorOptions) {
286
291
  const { port } = this.#config.inspectorOptions
@@ -326,7 +331,9 @@ export class Runtime extends EventEmitter {
326
331
  this.#startHealthMetricsCollectionIfNeeded()
327
332
 
328
333
  await this.#dynamicWorkersScaler?.start()
329
- this.#showUrl()
334
+ if (this.#url) {
335
+ this.#showUrl()
336
+ }
330
337
  return this.#url
331
338
  }
332
339
 
@@ -400,11 +407,22 @@ export class Runtime extends EventEmitter {
400
407
  }
401
408
 
402
409
  if (this.logger) {
403
- this.#loggerDestination?.end()
410
+ const loggerDestination = this.#loggerDestination
411
+ const loggerCloseables = this.#loggerContext?.closeables ?? []
404
412
 
405
413
  this.logger = abstractLogger
406
414
  this.#loggerDestination = null
407
415
  this.#loggerContext = null
416
+
417
+ if (loggerDestination) {
418
+ loggerDestination.end()
419
+ await finished(loggerDestination).catch(() => {})
420
+ }
421
+
422
+ for (const closeable of loggerCloseables) {
423
+ closeable.end?.()
424
+ await finished(closeable).catch(() => {})
425
+ }
408
426
  }
409
427
 
410
428
  this.#updateStatus('closed')
@@ -570,7 +588,7 @@ export class Runtime extends EventEmitter {
570
588
 
571
589
  // Now, topological sort the applications based on their dependencies.
572
590
  // If circular dependencies are detected, an error with proper error code is thrown.
573
- applications = topologicalSort(dependencies)
591
+ applications = topologicalSort(applications, dependencies)
574
592
 
575
593
  // Group into dependency levels so that each level's dependencies are all
576
594
  // in previous levels. Levels are started sequentially, but applications
@@ -1110,6 +1128,10 @@ export class Runtime extends EventEmitter {
1110
1128
  }
1111
1129
 
1112
1130
  async getEntrypointDetails () {
1131
+ if (!this.#entrypointId) {
1132
+ return null
1133
+ }
1134
+
1113
1135
  return this.getApplicationDetails(this.#entrypointId)
1114
1136
  }
1115
1137
 
@@ -1156,6 +1178,10 @@ export class Runtime extends EventEmitter {
1156
1178
  }
1157
1179
 
1158
1180
  async getMetrics (format = 'json') {
1181
+ if (this.#config.metrics === false || this.#config.metrics?.enabled === false) {
1182
+ throw new Error('Metrics are disabled')
1183
+ }
1184
+
1159
1185
  let metrics = null
1160
1186
 
1161
1187
  const applicationRestartMetrics = format === 'json'
@@ -1704,6 +1730,10 @@ export class Runtime extends EventEmitter {
1704
1730
  }
1705
1731
 
1706
1732
  #showUrl () {
1733
+ if (!this.#url) {
1734
+ return
1735
+ }
1736
+
1707
1737
  this.logger.info(`Platformatic is now listening at ${this.#url}`)
1708
1738
  }
1709
1739
 
@@ -1722,7 +1752,7 @@ export class Runtime extends EventEmitter {
1722
1752
  applicationConfig.path = join(this.#root, config.resolvedApplicationsBasePath, id)
1723
1753
 
1724
1754
  if (!existsSync(applicationConfig.path)) {
1725
- const executable = globalThis.platformatic?.executable ?? 'platformatic'
1755
+ const executable = getExecutable() ?? 'platformatic'
1726
1756
  this.logger.error(
1727
1757
  `The path for application "%s" does not exist. Please run "${executable} resolve" and try again.`,
1728
1758
  id
@@ -2701,6 +2731,12 @@ export class Runtime extends EventEmitter {
2701
2731
 
2702
2732
  async #createWorkersBroadcastChannel () {
2703
2733
  this.#workersBroadcastChannel?.close()
2734
+
2735
+ if (this.#config.applications.length === 0) {
2736
+ this.#workersBroadcastChannel = undefined
2737
+ return
2738
+ }
2739
+
2704
2740
  this.#workersBroadcastChannel = new BroadcastChannel(kWorkersBroadcast)
2705
2741
  }
2706
2742
 
@@ -3243,6 +3279,15 @@ export class Runtime extends EventEmitter {
3243
3279
  return argv
3244
3280
  }
3245
3281
 
3282
+ // Starting from Node.js 25 the Permission Model also gates network access
3283
+ // (dns.lookup, server.listen, outbound connections and fetch) behind
3284
+ // --allow-net. Applications always need to bind their HTTP server and reach
3285
+ // other applications through the internal mesh, so we always grant it when
3286
+ // available. On older versions the flag does not exist and must be omitted.
3287
+ if (features.node.permission.network) {
3288
+ allows.add('--allow-net')
3289
+ }
3290
+
3246
3291
  // We need to allow read access to the node_modules folder both at the runtime level and at the application level
3247
3292
  allows.add(`--allow-fs-read=${join(this.#root, 'node_modules', '*')}`)
3248
3293
  allows.add(`--allow-fs-read=${join(applicationConfig.path, 'node_modules', '*')}`)
package/lib/utils.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createHash } from 'node:crypto'
2
2
  import { tmpdir } from 'node:os'
3
3
  import { join } from 'node:path'
4
- import { ApplicationsDependenciesCycleError } from './errors.js'
4
+ import { ApplicationDependencyNotFoundError, ApplicationsDependenciesCycleError } from './errors.js'
5
5
 
6
6
  export function getArrayDifference (a, b) {
7
7
  return a.filter(element => {
@@ -48,13 +48,22 @@ export function topologicalLevels (sorted, graph) {
48
48
  return levels
49
49
  }
50
50
 
51
- // Graph: Map<string, string[]>
52
- export function topologicalSort (graph) {
51
+ // valid: string[] | Map<string, string[]>, graph?: Map<string, string[]>
52
+ export function topologicalSort (valid, graph) {
53
+ if (!graph) {
54
+ graph = valid
55
+ valid = Array.from(graph.keys())
56
+ }
57
+
53
58
  const result = []
54
59
  const visited = new Set()
55
60
  const path = []
56
61
 
57
62
  function visit (node) {
63
+ if (!valid.includes(node)) {
64
+ throw new ApplicationDependencyNotFoundError(node, valid.join(', '))
65
+ }
66
+
58
67
  if (visited.has(node)) {
59
68
  return
60
69
  }