@platformatic/runtime 3.4.1 → 3.5.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.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/config.d.ts +224 -77
  3. package/eslint.config.js +3 -5
  4. package/index.d.ts +73 -24
  5. package/index.js +173 -29
  6. package/lib/config.js +279 -197
  7. package/lib/errors.js +126 -34
  8. package/lib/generator.js +640 -0
  9. package/lib/logger.js +43 -41
  10. package/lib/management-api.js +109 -118
  11. package/lib/prom-server.js +202 -16
  12. package/lib/runtime.js +1963 -585
  13. package/lib/scheduler.js +119 -0
  14. package/lib/schema.js +22 -234
  15. package/lib/shared-http-cache.js +43 -0
  16. package/lib/upgrade.js +6 -8
  17. package/lib/utils.js +6 -61
  18. package/lib/version.js +7 -0
  19. package/lib/versions/v1.36.0.js +2 -4
  20. package/lib/versions/v1.5.0.js +2 -4
  21. package/lib/versions/v2.0.0.js +3 -5
  22. package/lib/versions/v3.0.0.js +16 -0
  23. package/lib/worker/controller.js +302 -0
  24. package/lib/worker/http-cache.js +171 -0
  25. package/lib/worker/interceptors.js +190 -10
  26. package/lib/worker/itc.js +146 -59
  27. package/lib/worker/main.js +220 -81
  28. package/lib/worker/messaging.js +182 -0
  29. package/lib/worker/round-robin-map.js +62 -0
  30. package/lib/worker/shared-context.js +22 -0
  31. package/lib/worker/symbols.js +14 -5
  32. package/package.json +47 -38
  33. package/schema.json +1383 -55
  34. package/help/compile.txt +0 -8
  35. package/help/help.txt +0 -5
  36. package/help/start.txt +0 -21
  37. package/index.test-d.ts +0 -41
  38. package/lib/build-server.js +0 -69
  39. package/lib/compile.js +0 -98
  40. package/lib/dependencies.js +0 -59
  41. package/lib/generator/README.md +0 -32
  42. package/lib/generator/errors.js +0 -10
  43. package/lib/generator/runtime-generator.d.ts +0 -37
  44. package/lib/generator/runtime-generator.js +0 -498
  45. package/lib/start.js +0 -190
  46. package/lib/worker/app.js +0 -278
  47. package/lib/worker/default-stackable.js +0 -33
  48. package/lib/worker/metrics.js +0 -122
  49. package/runtime.mjs +0 -54
@@ -0,0 +1,640 @@
1
+ import createError from '@fastify/error'
2
+ import {
3
+ defaultPackageManager,
4
+ findConfigurationFile,
5
+ generateDashedName,
6
+ kMetadata,
7
+ loadConfiguration,
8
+ loadConfigurationFile,
9
+ safeRemove
10
+ } from '@platformatic/foundation'
11
+ import { BaseGenerator, envObjectToString, getApplicationTemplateFromSchemaUrl } from '@platformatic/generators'
12
+ import { existsSync } from 'node:fs'
13
+ import { readFile, readdir, stat } from 'node:fs/promises'
14
+ import { createRequire } from 'node:module'
15
+ import { basename, join } from 'node:path'
16
+ import { pathToFileURL } from 'node:url'
17
+ import { transform } from './config.js'
18
+ import { schema } from './schema.js'
19
+ import { getArrayDifference } from './utils.js'
20
+
21
+ const wrappableProperties = {
22
+ logger: {
23
+ level: '{PLT_SERVER_LOGGER_LEVEL}'
24
+ },
25
+ server: {
26
+ hostname: '{PLT_SERVER_HOSTNAME}',
27
+ port: '{PORT}'
28
+ },
29
+ managementApi: '{PLT_MANAGEMENT_API}'
30
+ }
31
+
32
+ const engines = {
33
+ node: '>=22.19.0'
34
+ }
35
+
36
+ export const ERROR_PREFIX = 'PLT_RUNTIME_GEN'
37
+
38
+ const NoApplicationNamedError = createError(
39
+ `${ERROR_PREFIX}_NO_APPLICATION_FOUND`,
40
+ "No application named '%s' has been added to this runtime."
41
+ )
42
+ const NoEntryPointError = createError(`${ERROR_PREFIX}_NO_ENTRYPOINT`, 'No entrypoint had been defined.')
43
+
44
+ function getRuntimeBaseEnvVars (config) {
45
+ return {
46
+ PLT_SERVER_HOSTNAME: '127.0.0.1',
47
+ PORT: config.port || 3042,
48
+ PLT_SERVER_LOGGER_LEVEL: config.logLevel || 'info',
49
+ PLT_MANAGEMENT_API: true
50
+ }
51
+ }
52
+
53
+ // This is needed as dotenv-tool is not loading with ESM currently
54
+ export function createDotenvTool (...args) {
55
+ const { DotEnvTool } = createRequire(import.meta.url)('dotenv-tool')
56
+ return new DotEnvTool(...args)
57
+ }
58
+
59
+ export class RuntimeGenerator extends BaseGenerator {
60
+ constructor (opts) {
61
+ super({
62
+ ...opts,
63
+ module: '@platformatic/runtime'
64
+ })
65
+ this.runtimeName = opts.name
66
+ this.applicationsFolder = opts.applicationsFolder ?? 'applications'
67
+ this.applications = []
68
+ this.existingApplications = []
69
+ this.entryPoint = null
70
+ this.packageManager = opts.packageManager ?? defaultPackageManager
71
+ }
72
+
73
+ async addApplication (application, name) {
74
+ // ensure application config is correct
75
+ const originalConfig = application.config
76
+ const applicationName = name || generateDashedName()
77
+ const newConfig = {
78
+ ...originalConfig,
79
+ isRuntimeContext: true,
80
+ applicationName
81
+ }
82
+ // reset all files previously generated by the application
83
+ application.reset()
84
+ application.setConfig(newConfig)
85
+ this.applications.push({
86
+ name: applicationName,
87
+ application
88
+ })
89
+
90
+ application.setRuntime(this)
91
+ }
92
+
93
+ setEntryPoint (entryPoint) {
94
+ const application =
95
+ this.existingApplications.includes(entryPoint) || this.applications.find(svc => svc.name === entryPoint)
96
+ if (!application) {
97
+ throw new NoApplicationNamedError(entryPoint)
98
+ }
99
+ this.entryPoint = application
100
+ }
101
+
102
+ async generatePackageJson () {
103
+ const template = {
104
+ name: `${this.runtimeName}`,
105
+ scripts: {
106
+ dev: this.config.devCommand,
107
+ build: this.config.buildCommand,
108
+ start: this.config.startCommand ?? 'platformatic start'
109
+ },
110
+ devDependencies: {
111
+ fastify: `^${this.fastifyVersion}`
112
+ },
113
+ dependencies: {
114
+ '@platformatic/runtime': `^${this.platformaticVersion}`,
115
+ platformatic: `^${this.platformaticVersion}`,
116
+ wattpm: `^${this.platformaticVersion}`,
117
+ ...this.config.dependencies
118
+ },
119
+ engines
120
+ }
121
+
122
+ if (this.packageManager === 'npm' || this.packageManager === 'yarn') {
123
+ template.workspaces = [this.applicationsFolder + '/*']
124
+ }
125
+
126
+ return template
127
+ }
128
+
129
+ async _beforePrepare () {
130
+ this.setApplicationsDirectory()
131
+ this.setApplicationsConfigValues()
132
+ this.addApplicationsDependencies()
133
+
134
+ this.addEnvVars(getRuntimeBaseEnvVars(this.config), { overwrite: false, default: true })
135
+ }
136
+
137
+ addApplicationsDependencies () {
138
+ this.applications.forEach(({ application }) => {
139
+ if (application.config.dependencies) {
140
+ Object.entries(application.config.dependencies).forEach(kv => {
141
+ this.config.dependencies[kv[0]] = kv[1]
142
+ })
143
+ }
144
+ })
145
+ }
146
+
147
+ async populateFromExistingConfig () {
148
+ if (this._hasCheckedForExistingConfig) {
149
+ return
150
+ }
151
+ this._hasCheckedForExistingConfig = true
152
+ const existingConfigFile = this.runtimeConfig ?? (await findConfigurationFile(this.targetDirectory, 'runtime'))
153
+ if (existingConfigFile && existsSync(join(this.targetDirectory, existingConfigFile))) {
154
+ this.existingConfigRaw = await loadConfigurationFile(join(this.targetDirectory, existingConfigFile))
155
+ this.existingConfig = await loadConfiguration(join(this.targetDirectory, existingConfigFile), schema, {
156
+ transform,
157
+ ignoreProcessEnv: true
158
+ })
159
+
160
+ const { PLT_ROOT, ...existingEnvironment } = this.existingConfig[kMetadata].env
161
+ this.config.env = existingEnvironment
162
+ this.config.port = this.config.env.PORT
163
+ this.entryPoint = this.existingConfig.applications.find(svc => svc.entrypoint)
164
+ this.existingApplications = this.existingConfig.applications.map(s => s.id)
165
+
166
+ this.updateRuntimeConfig(this.existingConfigRaw)
167
+ this.updateRuntimeEnv(await readFile(join(this.targetDirectory, '.env'), 'utf-8'))
168
+ }
169
+ }
170
+
171
+ async prepare () {
172
+ await this.populateFromExistingConfig()
173
+ if (this.existingConfig) {
174
+ this.setApplicationsDirectory()
175
+ this.setApplicationsConfigValues()
176
+ await this._afterPrepare()
177
+ return {
178
+ env: this.config.env,
179
+ targetDirectory: this.targetDirectory
180
+ }
181
+ } else {
182
+ return await super.prepare()
183
+ }
184
+ }
185
+
186
+ setApplicationsConfigValues () {
187
+ this.applications.forEach(({ application }) => {
188
+ if (!application.config) {
189
+ // set default config
190
+ application.setConfig()
191
+ }
192
+ })
193
+ }
194
+
195
+ async _getConfigFileContents () {
196
+ const config = {
197
+ $schema: `https://schemas.platformatic.dev/@platformatic/runtime/${this.platformaticVersion}.json`,
198
+ entrypoint: this.entryPoint.name,
199
+ watch: true,
200
+ autoload: {
201
+ path: this.config.autoload || this.applicationsFolder,
202
+ exclude: ['docs']
203
+ },
204
+ ...wrappableProperties
205
+ }
206
+
207
+ return config
208
+ }
209
+
210
+ async _afterPrepare () {
211
+ if (!this.entryPoint) {
212
+ throw new NoEntryPointError()
213
+ }
214
+ const applicationsEnv = await this.prepareApplicationFiles()
215
+ this.addEnvVars({
216
+ ...this.config.env,
217
+ ...this.getRuntimeEnv(),
218
+ ...applicationsEnv
219
+ })
220
+
221
+ this.updateRuntimeEnv(envObjectToString(this.config.env))
222
+
223
+ this.addFile({
224
+ path: '',
225
+ file: '.env.sample',
226
+ contents: envObjectToString(this.config.defaultEnv)
227
+ })
228
+
229
+ return {
230
+ targetDirectory: this.targetDirectory,
231
+ env: applicationsEnv
232
+ }
233
+ }
234
+
235
+ async writeFiles () {
236
+ for (const { application } of this.applications) {
237
+ await application._beforeWriteFiles?.(this)
238
+ }
239
+
240
+ await super.writeFiles()
241
+
242
+ if (!this.config.isUpdating) {
243
+ for (const { application } of this.applications) {
244
+ await application.writeFiles()
245
+ }
246
+ }
247
+
248
+ for (const { application } of this.applications) {
249
+ await application._afterWriteFiles?.(this)
250
+ }
251
+ }
252
+
253
+ async prepareQuestions () {
254
+ await this.populateFromExistingConfig()
255
+
256
+ if (this.existingConfig) {
257
+ return
258
+ }
259
+
260
+ // port
261
+ this.questions.push({
262
+ type: 'input',
263
+ name: 'port',
264
+ default: 3042,
265
+ message: 'What port do you want to use?'
266
+ })
267
+ }
268
+
269
+ setApplicationsDirectory () {
270
+ this.applications.forEach(({ application }) => {
271
+ if (!application.config) {
272
+ // set default config
273
+ application.setConfig()
274
+ }
275
+ let basePath
276
+ if (this.existingConfig) {
277
+ basePath = this.existingConfig.autoload.path
278
+ } else {
279
+ basePath = join(this.targetDirectory, this.config.autoload || this.applicationsFolder)
280
+ }
281
+ this.applicationsBasePath = basePath
282
+ application.setTargetDirectory(join(basePath, application.config.applicationName))
283
+ })
284
+ }
285
+
286
+ setApplicationsConfig (configToOverride) {
287
+ this.applications.forEach(application => {
288
+ const originalConfig = application.config
289
+ application.setConfig({
290
+ ...originalConfig,
291
+ ...configToOverride
292
+ })
293
+ })
294
+ }
295
+
296
+ async prepareApplicationFiles () {
297
+ let applicationsEnv = {}
298
+ for (const svc of this.applications) {
299
+ const svcEnv = await svc.application.prepare()
300
+ applicationsEnv = {
301
+ ...applicationsEnv,
302
+ ...svcEnv.env
303
+ }
304
+ }
305
+ return applicationsEnv
306
+ }
307
+
308
+ getConfigFieldsDefinitions () {
309
+ return []
310
+ }
311
+
312
+ setConfigFields () {
313
+ // do nothing, makes no sense
314
+ }
315
+
316
+ getRuntimeEnv () {
317
+ return {
318
+ PORT: this.config.port
319
+ }
320
+ }
321
+
322
+ async postInstallActions () {
323
+ for (const { application } of this.applications) {
324
+ await application.postInstallActions()
325
+ }
326
+ }
327
+
328
+ async _getGeneratorForTemplate (dir, pkg) {
329
+ const _require = createRequire(dir)
330
+ const fileToImport = _require.resolve(pkg)
331
+ return (await import(pathToFileURL(fileToImport))).Generator
332
+ }
333
+
334
+ async loadFromDir () {
335
+ const output = {
336
+ applications: []
337
+ }
338
+ const runtimePkgConfigFileData = JSON.parse(await readFile(join(this.targetDirectory, this.runtimeConfig), 'utf-8'))
339
+ const applicationsPath = join(this.targetDirectory, runtimePkgConfigFileData.autoload.path)
340
+
341
+ // load all applications
342
+ const allApplications = await readdir(applicationsPath)
343
+ for (const s of allApplications) {
344
+ // check is a directory
345
+ const currentApplicationPath = join(applicationsPath, s)
346
+ const dirStat = await stat(currentApplicationPath)
347
+ if (dirStat.isDirectory()) {
348
+ // load the application config
349
+ const configFile = await findConfigurationFile(currentApplicationPath)
350
+ const applicationPltJson = JSON.parse(await readFile(join(currentApplicationPath, configFile), 'utf-8'))
351
+ // get module to load
352
+ const template = applicationPltJson.module || getApplicationTemplateFromSchemaUrl(applicationPltJson.$schema)
353
+ const Generator = await this._getGeneratorForTemplate(currentApplicationPath, template)
354
+ const instance = new Generator({
355
+ logger: this.logger
356
+ })
357
+ this.addApplication(instance, s)
358
+ output.applications.push(await instance.loadFromDir(s, this.targetDirectory))
359
+ }
360
+ }
361
+ return output
362
+ }
363
+
364
+ async update (newConfig) {
365
+ let allApplicationsDependencies = {}
366
+ const runtimeAddedEnvKeys = []
367
+
368
+ this.config.isUpdating = true
369
+ const currrentPackageJson = JSON.parse(await readFile(join(this.targetDirectory, 'package.json'), 'utf-8'))
370
+ const currentRuntimeDependencies = currrentPackageJson.dependencies
371
+ // check all applications are present with the same template
372
+ const allCurrentApplicationsNames = this.applications.map(s => s.name)
373
+ const allNewApplicationsNames = newConfig.applications.map(s => s.name)
374
+ // load dotenv tool
375
+ const envTool = createDotenvTool({
376
+ path: join(this.targetDirectory, '.env')
377
+ })
378
+
379
+ await envTool.load()
380
+
381
+ const removedApplications = getArrayDifference(allCurrentApplicationsNames, allNewApplicationsNames)
382
+ if (removedApplications.length > 0) {
383
+ for (const removedApplication of removedApplications) {
384
+ // handle application delete
385
+
386
+ // delete env variables
387
+ const s = this.applications.find(f => f.name === removedApplication)
388
+ const allKeys = envTool.getKeys()
389
+ allKeys.forEach(k => {
390
+ if (k.startsWith(`PLT_${s.application.config.envPrefix}`)) {
391
+ envTool.deleteKey(k)
392
+ }
393
+ })
394
+
395
+ // delete dependencies
396
+ const applicationPath = join(this.targetDirectory, this.applicationsFolder, s.name)
397
+ const configFile = await findConfigurationFile(applicationPath)
398
+ const applicationPackageJson = JSON.parse(await readFile(join(applicationPath, configFile), 'utf-8'))
399
+ if (applicationPackageJson.plugins && applicationPackageJson.plugins.packages) {
400
+ applicationPackageJson.plugins.packages.forEach(p => {
401
+ delete currrentPackageJson.dependencies[p.name]
402
+ })
403
+ }
404
+ // delete directory
405
+ await safeRemove(join(this.targetDirectory, this.applicationsFolder, s.name))
406
+ }
407
+ // throw new CannotRemoveApplicationOnUpdateError(removedApplications.join(', '))
408
+ }
409
+
410
+ // handle new applications
411
+ for (const newApplication of newConfig.applications) {
412
+ // create generator for the application
413
+ const ApplicationGenerator = await this._getGeneratorForTemplate(
414
+ join(this.targetDirectory, 'package.json'),
415
+ newApplication.template
416
+ )
417
+ const applicationInstance = new ApplicationGenerator({
418
+ logger: this.logger
419
+ })
420
+ const baseConfig = {
421
+ isRuntimeContext: true,
422
+ targetDirectory: join(this.targetDirectory, this.applicationsFolder, newApplication.name),
423
+ applicationName: newApplication.name,
424
+ plugin: true
425
+ }
426
+ if (allCurrentApplicationsNames.includes(newApplication.name)) {
427
+ // update existing applications env values
428
+ // otherwise, is a new application
429
+ baseConfig.isUpdating = true
430
+
431
+ // handle application's plugin differences
432
+ const oldApplicationMetadata = await applicationInstance.loadFromDir(newApplication.name, this.targetDirectory)
433
+ const oldApplicationPackages = oldApplicationMetadata.plugins.map(meta => meta.name)
434
+ const newApplicationPackages = newApplication.plugins.map(meta => meta.name)
435
+ const pluginsToRemove = getArrayDifference(oldApplicationPackages, newApplicationPackages)
436
+ pluginsToRemove.forEach(p => delete currentRuntimeDependencies[p])
437
+ } else {
438
+ // add application to the generator
439
+ this.applications.push({
440
+ name: newApplication.name,
441
+ application: applicationInstance
442
+ })
443
+ }
444
+ applicationInstance.setConfig(baseConfig)
445
+ applicationInstance.setConfigFields(newApplication.fields)
446
+
447
+ const applicationEnvPrefix = `PLT_${applicationInstance.config.envPrefix}`
448
+ for (const plug of newApplication.plugins) {
449
+ await applicationInstance.addPackage(plug)
450
+ for (const opt of plug.options) {
451
+ const key = `${applicationEnvPrefix}_${opt.name}`
452
+ runtimeAddedEnvKeys.push(key)
453
+ const value = opt.value
454
+ if (envTool.hasKey(key)) {
455
+ envTool.updateKey(key, value)
456
+ } else {
457
+ envTool.addKey(key, value)
458
+ }
459
+ }
460
+ }
461
+ allApplicationsDependencies = { ...allApplicationsDependencies, ...applicationInstance.config.dependencies }
462
+ const afterPrepareMetadata = await applicationInstance.prepare()
463
+ await applicationInstance.writeFiles()
464
+ // cleanup runtime env removing keys not present anymore in application plugins
465
+ const allKeys = envTool.getKeys()
466
+ allKeys.forEach(k => {
467
+ if (k.startsWith(`${applicationEnvPrefix}_FST_PLUGIN`) && !runtimeAddedEnvKeys.includes(k)) {
468
+ envTool.deleteKey(k)
469
+ }
470
+ })
471
+
472
+ // add application env variables to runtime env
473
+ Object.entries(afterPrepareMetadata.env).forEach(([key, value]) => {
474
+ envTool.addKey(key, value)
475
+ })
476
+ }
477
+ // update runtime package.json dependencies
478
+ currrentPackageJson.dependencies = {
479
+ ...currrentPackageJson.dependencies,
480
+ ...allApplicationsDependencies
481
+ }
482
+ this.addFile({
483
+ path: '',
484
+ file: 'package.json',
485
+ contents: JSON.stringify(currrentPackageJson, null, 2)
486
+ })
487
+
488
+ // set new entrypoint if specified
489
+ const newEntrypoint = newConfig.entrypoint
490
+ if (newEntrypoint) {
491
+ // load platformatic.json runtime config
492
+ const runtimePkgConfigFileData = JSON.parse(
493
+ await readFile(join(this.targetDirectory, this.runtimeConfig), 'utf-8')
494
+ )
495
+
496
+ this.setEntryPoint(newEntrypoint)
497
+ runtimePkgConfigFileData.entrypoint = newEntrypoint
498
+ this.updateRuntimeConfig(runtimePkgConfigFileData)
499
+ }
500
+ await this.writeFiles()
501
+ // save new env
502
+ await envTool.save()
503
+ }
504
+
505
+ async generateConfigFile () {
506
+ this.updateRuntimeConfig(await super.generateConfigFile())
507
+ }
508
+
509
+ async generateEnv () {
510
+ const serialized = await super.generateEnv()
511
+
512
+ if (serialized) {
513
+ this.updateRuntimeEnv(serialized)
514
+ }
515
+ }
516
+
517
+ getRuntimeConfigFileObject () {
518
+ return this.files.find(file => file.tags?.includes('runtime-config')) ?? null
519
+ }
520
+
521
+ getRuntimeEnvFileObject () {
522
+ return this.files.find(file => file.tags?.includes('runtime-env')) ?? null
523
+ }
524
+
525
+ updateRuntimeConfig (config) {
526
+ this.addFile({
527
+ path: '',
528
+ file: this.runtimeConfig,
529
+ contents: JSON.stringify(config, null, 2),
530
+ tags: ['runtime-config']
531
+ })
532
+ }
533
+
534
+ updateRuntimeEnv (contents) {
535
+ this.addFile({
536
+ path: '',
537
+ file: '.env',
538
+ contents,
539
+ tags: ['runtime-env']
540
+ })
541
+ }
542
+
543
+ updateConfigEntryPoint (entrypoint) {
544
+ // This can return null if the generator was not supposed to modify the config
545
+ const configObject = this.getRuntimeConfigFileObject()
546
+ const config = JSON.parse(configObject.contents)
547
+ config.entrypoint = entrypoint
548
+
549
+ this.updateRuntimeConfig(config)
550
+ }
551
+ }
552
+
553
+ export class WrappedGenerator extends BaseGenerator {
554
+ async prepare () {
555
+ await this.getPlatformaticVersion()
556
+ await this.#updateEnvironment()
557
+ await this.#updatePackageJson()
558
+ await this.#createConfigFile()
559
+ }
560
+
561
+ async #updateEnvironment () {
562
+ this.addEnvVars(getRuntimeBaseEnvVars(this.config), { overwrite: false, default: true })
563
+
564
+ this.addFile({
565
+ path: '',
566
+ file: '.env',
567
+ contents: (await this.#readExistingFile('.env', '', '\n')) + envObjectToString(this.config.env)
568
+ })
569
+
570
+ this.addFile({
571
+ path: '',
572
+ file: '.env.sample',
573
+ contents: (await this.#readExistingFile('.env.sample', '', '\n')) + envObjectToString(this.config.defaultEnv)
574
+ })
575
+ }
576
+
577
+ async #updatePackageJson () {
578
+ // Manipulate the package.json, if any
579
+ const packageJson = JSON.parse(await this.#readExistingFile('package.json', '{}'))
580
+ let { name, dependencies, devDependencies, scripts, engines: packageJsonEngines, ...rest } = packageJson
581
+
582
+ // Add the dependencies
583
+ dependencies = {
584
+ ...dependencies,
585
+ [this.module]: `^${this.platformaticVersion}`,
586
+ platformatic: `^${this.platformaticVersion}`,
587
+ wattpm: `^${this.platformaticVersion}`
588
+ }
589
+
590
+ // For easier readbility, sort dependencies and devDependencies by name
591
+ dependencies = Object.fromEntries(Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b)))
592
+ devDependencies = Object.fromEntries(Object.entries(devDependencies ?? {}).sort(([a], [b]) => a.localeCompare(b)))
593
+
594
+ scripts ??= {}
595
+ scripts.dev ??= this.config.devCommand
596
+ scripts.build ??= this.config.buildCommand
597
+ scripts.start ??= this.config.startCommand ?? 'platformatic start'
598
+
599
+ this.addFile({
600
+ path: '',
601
+ file: 'package.json',
602
+ contents: JSON.stringify(
603
+ {
604
+ name: name ?? this.projectName ?? this.runtimeName ?? basename(this.targetDirectory),
605
+ scripts,
606
+ dependencies,
607
+ devDependencies,
608
+ ...rest,
609
+ engines: { ...packageJsonEngines, ...engines }
610
+ },
611
+ null,
612
+ 2
613
+ )
614
+ })
615
+ }
616
+
617
+ async #createConfigFile () {
618
+ const config = {
619
+ $schema: `https://schemas.platformatic.dev/${this.module}/${this.platformaticVersion}.json`,
620
+ runtime: wrappableProperties
621
+ }
622
+
623
+ this.addFile({
624
+ path: '',
625
+ file: 'watt.json',
626
+ contents: JSON.stringify(config, null, 2)
627
+ })
628
+ }
629
+
630
+ async #readExistingFile (path, emptyContents = '', suffix = '') {
631
+ const filePath = join(this.targetDirectory, path)
632
+
633
+ if (!existsSync(filePath)) {
634
+ return emptyContents
635
+ }
636
+
637
+ const contents = await readFile(filePath, 'utf-8')
638
+ return contents + suffix
639
+ }
640
+ }