@platformatic/watt-extra 0.1.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.
Files changed (95) hide show
  1. package/README.md +87 -0
  2. package/app.js +124 -0
  3. package/cli.js +141 -0
  4. package/clients/compliance/compliance-types.d.ts +887 -0
  5. package/clients/compliance/compliance.mjs +1049 -0
  6. package/clients/compliance/compliance.openapi.json +6127 -0
  7. package/clients/control-plane/control-plane-types.d.ts +2696 -0
  8. package/clients/control-plane/control-plane.mjs +3051 -0
  9. package/clients/control-plane/control-plane.openapi.json +13693 -0
  10. package/clients/cron/cron-types.d.ts +1479 -0
  11. package/clients/cron/cron.mjs +872 -0
  12. package/clients/cron/cron.openapi.json +9330 -0
  13. package/compliance/index.js +21 -0
  14. package/compliance/rules/dependencies.js +76 -0
  15. package/compliance/rules/utils.js +12 -0
  16. package/eslint.config.js +11 -0
  17. package/help/start.txt +12 -0
  18. package/help/watt-extra.txt +12 -0
  19. package/index.js +45 -0
  20. package/lib/banner.js +22 -0
  21. package/lib/errors.js +34 -0
  22. package/lib/utils.js +34 -0
  23. package/lib/wattpro.js +580 -0
  24. package/package.json +50 -0
  25. package/plugins/alerts.js +115 -0
  26. package/plugins/auth.js +89 -0
  27. package/plugins/compliancy.js +70 -0
  28. package/plugins/env.js +58 -0
  29. package/plugins/flamegraphs.js +100 -0
  30. package/plugins/init.js +70 -0
  31. package/plugins/metadata.js +84 -0
  32. package/plugins/scheduler.js +48 -0
  33. package/plugins/update.js +128 -0
  34. package/renovate.json +6 -0
  35. package/test/alerts.test.js +607 -0
  36. package/test/auth.test.js +128 -0
  37. package/test/auto-cache.test.js +401 -0
  38. package/test/cli.test.js +75 -0
  39. package/test/compliancy.test.js +87 -0
  40. package/test/fixtures/runtime-domains/alpha/package.json +5 -0
  41. package/test/fixtures/runtime-domains/alpha/platformatic.json +6 -0
  42. package/test/fixtures/runtime-domains/alpha/plugin.js +16 -0
  43. package/test/fixtures/runtime-domains/beta/package.json +5 -0
  44. package/test/fixtures/runtime-domains/beta/platformatic.json +6 -0
  45. package/test/fixtures/runtime-domains/beta/plugin.js +7 -0
  46. package/test/fixtures/runtime-domains/composer/package.json +5 -0
  47. package/test/fixtures/runtime-domains/composer/platformatic.json +19 -0
  48. package/test/fixtures/runtime-domains/package.json +1 -0
  49. package/test/fixtures/runtime-domains/platformatic.json +27 -0
  50. package/test/fixtures/runtime-health/package.json +20 -0
  51. package/test/fixtures/runtime-health/platformatic.json +16 -0
  52. package/test/fixtures/runtime-health/services/service-1/package.json +17 -0
  53. package/test/fixtures/runtime-health/services/service-1/platformatic.json +16 -0
  54. package/test/fixtures/runtime-health/services/service-1/plugins/example.js +6 -0
  55. package/test/fixtures/runtime-health/services/service-1/routes/root.cjs +8 -0
  56. package/test/fixtures/runtime-health/services/service-2/package.json +17 -0
  57. package/test/fixtures/runtime-health/services/service-2/platformatic.json +16 -0
  58. package/test/fixtures/runtime-health/services/service-2/plugins/example.js +6 -0
  59. package/test/fixtures/runtime-health/services/service-2/routes/root.cjs +8 -0
  60. package/test/fixtures/runtime-next/package.json +5 -0
  61. package/test/fixtures/runtime-next/platformatic.json +9 -0
  62. package/test/fixtures/runtime-next/web/next/next.config.js +2 -0
  63. package/test/fixtures/runtime-next/web/next/package.json +7 -0
  64. package/test/fixtures/runtime-next/web/next/platformatic.json +9 -0
  65. package/test/fixtures/runtime-next/web/next/src/app/direct/route.js +3 -0
  66. package/test/fixtures/runtime-next/web/next/src/app/layout.jsx +7 -0
  67. package/test/fixtures/runtime-next/web/next/src/app/page.jsx +3 -0
  68. package/test/fixtures/runtime-scheduler/main/package.json +5 -0
  69. package/test/fixtures/runtime-scheduler/main/platformatic.json +9 -0
  70. package/test/fixtures/runtime-scheduler/main/routes/root.cjs +11 -0
  71. package/test/fixtures/runtime-scheduler/package.json +1 -0
  72. package/test/fixtures/runtime-scheduler/platformatic.json +27 -0
  73. package/test/fixtures/runtime-service/main/package.json +5 -0
  74. package/test/fixtures/runtime-service/main/platformatic.json +12 -0
  75. package/test/fixtures/runtime-service/main/routes/root.cjs +11 -0
  76. package/test/fixtures/runtime-service/package.json +1 -0
  77. package/test/fixtures/runtime-service/platformatic.json +19 -0
  78. package/test/fixtures/service-1/package.json +7 -0
  79. package/test/fixtures/service-1/platformatic.json +18 -0
  80. package/test/fixtures/service-1/routes/root.cjs +48 -0
  81. package/test/fixtures/service-2/platformatic.json +21 -0
  82. package/test/fixtures/service-2/routes/root.cjs +5 -0
  83. package/test/fixtures/service-3/package.json +5 -0
  84. package/test/fixtures/service-3/platformatic.json +21 -0
  85. package/test/fixtures/service-3/routes/root.cjs +8 -0
  86. package/test/health.test.js +44 -0
  87. package/test/helper.js +274 -0
  88. package/test/init.test.js +243 -0
  89. package/test/patch-config.test.js +434 -0
  90. package/test/scheduler.test.js +71 -0
  91. package/test/send-to-icc-retry.test.js +138 -0
  92. package/test/shared-context.test.js +82 -0
  93. package/test/spawn.test.js +110 -0
  94. package/test/trigger-flamegraphs.test.js +226 -0
  95. package/test/update.test.js +519 -0
package/lib/wattpro.js ADDED
@@ -0,0 +1,580 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { join, resolve } from 'node:path'
3
+ import { createRequire } from 'node:module'
4
+ import { loadConfiguration } from '@platformatic/runtime'
5
+
6
+ const require = createRequire(import.meta.url)
7
+
8
+ // Simple replacement for ensureLoggableError
9
+ function ensureLoggableError (err) {
10
+ if (!err) return err
11
+ if (typeof err === 'string') return new Error(err)
12
+ if (err instanceof Error) return err
13
+ return new Error(String(err))
14
+ }
15
+
16
+ async function restartRuntime (runtime) {
17
+ runtime.logger.info('Received SIGUSR2, restarting all services ...')
18
+
19
+ try {
20
+ await runtime.restart()
21
+ } catch (err) {
22
+ runtime.logger.error(
23
+ { err: ensureLoggableError(err) },
24
+ 'Failed to restart services.'
25
+ )
26
+ }
27
+ }
28
+
29
+ class WattPro {
30
+ #env
31
+ #logger
32
+ #require
33
+ #appDir
34
+ #applicationName
35
+ #instanceId
36
+ #instanceConfig
37
+ #originalConfig
38
+ #config
39
+ #sharedContext
40
+
41
+ constructor (app) {
42
+ this.#env = app.env
43
+ this.#logger = app.log
44
+ this.#appDir = app.env.PLT_APP_DIR
45
+ this.#applicationName = app.applicationName || app.env.PLT_APP_NAME
46
+ this.#require = createRequire(join(this.#appDir, 'package.json'))
47
+ this.#instanceId = app.instanceId
48
+ this.runtime = null
49
+ this.#sharedContext = {}
50
+ this.#instanceConfig = app.instanceConfig
51
+ }
52
+
53
+ async spawn () {
54
+ try {
55
+ this.runtime = await this.#createRuntime()
56
+ this.#logger.info('Starting runtime -WATT')
57
+ await this.runtime.start()
58
+ await this.updateSharedContext(this.#sharedContext)
59
+ this.#logger.info('Runtime started')
60
+ } catch (err) {
61
+ this.#logger.error(
62
+ { err: ensureLoggableError(err) },
63
+ 'Failed to start runtime'
64
+ )
65
+ throw err
66
+ }
67
+ }
68
+
69
+ async close () {
70
+ if (this.runtime) {
71
+ const runtime = this.runtime
72
+ this.runtime = null
73
+
74
+ this.#logger.info('Closing runtime')
75
+ await runtime.close()
76
+ this.#logger.info('Runtime closed')
77
+ }
78
+ }
79
+
80
+ async applyIccConfigUpdates (config) {
81
+ this.#logger.info({ config }, 'Applying ICC config updates')
82
+
83
+ if (this.#instanceConfig) {
84
+ this.#instanceConfig.config = config
85
+ }
86
+
87
+ if (config.httpCacheConfig) {
88
+ try {
89
+ const undiciConfig = await this.#getUndiciConfig()
90
+ await this.runtime.updateUndiciInterceptors?.(undiciConfig)
91
+ } catch (err) {
92
+ this.#logger.error({ err }, 'Failed to update undici interceptors')
93
+ }
94
+ }
95
+
96
+ if (config.resources?.services && config.resources.services.length > 0) {
97
+ const resourceUpdates = config.resources.services.map((service) => ({
98
+ service: service.name,
99
+ workers: service.threads,
100
+ health: {
101
+ maxHeapTotal: `${service.heap}MB`,
102
+ },
103
+ }))
104
+
105
+ try {
106
+ await this.runtime.updateServicesResources(resourceUpdates)
107
+ this.#logger.info(
108
+ { resourceUpdates },
109
+ 'Successfully updated service resources'
110
+ )
111
+ } catch (err) {
112
+ this.#logger.error(
113
+ { err, resourceUpdates },
114
+ 'Failed to update service resources'
115
+ )
116
+ }
117
+ }
118
+ }
119
+
120
+ async updateSharedContext (context) {
121
+ this.#sharedContext = context
122
+ await this.runtime?.updateSharedContext?.({ context })
123
+ }
124
+
125
+ async #loadAppConfig () {
126
+ this.#logger.info('Loading app config')
127
+ try {
128
+ const config = await loadConfiguration(this.#appDir)
129
+ return config
130
+ } catch (err) {
131
+ this.#logger.error(err, 'Failed to load app config')
132
+ throw new Error('Failed to load app config.', { cause: err })
133
+ }
134
+ }
135
+
136
+ async #createRuntime () {
137
+ this.#logger.info('Creating runtime')
138
+ const { Runtime } = this.#require('@platformatic/runtime')
139
+
140
+ this.#config = await this.#loadAppConfig()
141
+
142
+ this.#logger.info('Patching runtime config')
143
+
144
+ this.#originalConfig = structuredClone(this.#config)
145
+
146
+ if (this.#config) {
147
+ this.#patchRuntimeConfig(this.#config)
148
+ }
149
+
150
+ this.#logger.info('Building runtime')
151
+
152
+ const runtime = new Runtime(this.#config, { isProduction: true })
153
+
154
+ /* c8 ignore next 3 */
155
+ const restartListener = restartRuntime.bind(null, runtime)
156
+ process.on('SIGUSR2', restartListener)
157
+ runtime.on('closed', () => {
158
+ process.removeListener('SIGUSR2', restartListener)
159
+ })
160
+
161
+ await this.#configureServices(runtime)
162
+
163
+ try {
164
+ await runtime.init()
165
+ } catch (e) {
166
+ await runtime.close()
167
+ throw e
168
+ }
169
+
170
+ return runtime
171
+ }
172
+
173
+ #patchRuntimeConfig (config) {
174
+ this.#configureRuntime(config)
175
+ this.#configureTelemetry(config)
176
+ this.#configureHttpCaching(config)
177
+ this.#configureHealth(config)
178
+ this.#configureSystemResources(config)
179
+ this.#configureScheduler(config)
180
+ }
181
+
182
+ #configureRuntime (config) {
183
+ const { https, ...serverConfig } = config.server ?? {}
184
+ config.server = {
185
+ ...serverConfig,
186
+ hostname: this.#env.PLT_APP_HOSTNAME || serverConfig.hostname,
187
+ port: this.#env.PLT_APP_PORT || serverConfig.port,
188
+ }
189
+
190
+ config.hotReload = false
191
+ config.restartOnError = 1000
192
+ config.metrics = {
193
+ server: 'hide',
194
+ defaultMetrics: {
195
+ enabled: true,
196
+ },
197
+ hostname: this.#env.PLT_APP_HOSTNAME || '127.0.0.1',
198
+ port: this.#env.PLT_METRICS_PORT || 9090,
199
+ labels: {
200
+ serviceId: 'main',
201
+ applicationId: this.#instanceConfig.applicationId,
202
+ instanceId: this.#instanceId,
203
+ },
204
+ }
205
+
206
+ if (this.#env.PLT_DISABLE_FLAMEGRAPHS !== true) {
207
+ if (config.preload === undefined) {
208
+ config.preload = []
209
+ }
210
+ const pprofPath = require.resolve('@platformatic/wattpm-pprof-capture')
211
+ config.preload.push(pprofPath)
212
+ }
213
+
214
+ this.#configureUndici(config)
215
+ config.managementApi = true
216
+ }
217
+
218
+ #getUndiciConfig () {
219
+ const config = this.#config
220
+
221
+ const undiciConfig = structuredClone(this.#originalConfig.undici ?? {})
222
+
223
+ if (undiciConfig.interceptors === undefined) {
224
+ undiciConfig.interceptors = []
225
+ }
226
+
227
+ const enableSlicerInterceptor =
228
+ this.#instanceConfig?.enableSlicerInterceptor ?? false
229
+ if (enableSlicerInterceptor) {
230
+ const slicerInterceptorConfig = this.#getSlicerInterceptorConfig(config)
231
+ if (slicerInterceptorConfig) {
232
+ undiciConfig.interceptors.push(slicerInterceptorConfig)
233
+ }
234
+ }
235
+
236
+ const enableTrafficInterceptor =
237
+ this.#instanceConfig?.enableTrafficInterceptor ?? false
238
+ if (enableTrafficInterceptor) {
239
+ const trafficInterceptorConfig =
240
+ this.#getTrafficInterceptorConfig()
241
+ if (trafficInterceptorConfig) {
242
+ undiciConfig.interceptors.push(trafficInterceptorConfig)
243
+ }
244
+ }
245
+
246
+ return undiciConfig
247
+ }
248
+
249
+ #configureUndici (config) {
250
+ config.undici = this.#getUndiciConfig(config)
251
+ }
252
+
253
+ #getTrafficInterceptorConfig () {
254
+ if (!this.#instanceConfig?.iccServices?.trafficante?.url) {
255
+ return
256
+ }
257
+ const { origin: trafficanteOrigin, pathname: trafficantePath } = new URL(
258
+ this.#instanceConfig.iccServices.trafficante.url
259
+ )
260
+ return {
261
+ module: this.#require.resolve(
262
+ 'undici-traffic-interceptor'
263
+ ),
264
+ options: {
265
+ labels: {
266
+ applicationId: this.#instanceConfig.applicationId,
267
+ },
268
+ bloomFilter: {
269
+ size: 100000,
270
+ errorRate: 0.01,
271
+ },
272
+ maxResponseSize: 5 * 1024 * 1024, // 5MB
273
+ trafficInspectorOptions: {
274
+ url: trafficanteOrigin,
275
+ pathSendBody: join(trafficantePath, '/requests'),
276
+ pathSendMeta: join(trafficantePath, '/requests/hash'),
277
+ },
278
+ matchingDomains: [this.#env.PLT_APP_INTERNAL_SUB_DOMAIN],
279
+ },
280
+ }
281
+ }
282
+
283
+ #getSlicerInterceptorConfig (config) {
284
+ // We need to initialize the slicer interceptor even if there is no cache config
285
+ // to be able to update the onfiguration at runtime
286
+ const defaultCacheConfig = {
287
+ rules: [
288
+ {
289
+ routeToMatch: 'http://plt.slicer.default/',
290
+ headers: {},
291
+ },
292
+ ],
293
+ }
294
+
295
+ // This is the cache config from ICC
296
+ const httpCacheConfig =
297
+ this.#instanceConfig?.config?.httpCacheConfig ?? defaultCacheConfig
298
+ let autoGeneratedConfig = null
299
+ if (httpCacheConfig) {
300
+ try {
301
+ autoGeneratedConfig = httpCacheConfig
302
+ } catch (e) {
303
+ this.#logger.error(
304
+ { err: ensureLoggableError(e) },
305
+ 'Failed to parse auto generated cache config'
306
+ )
307
+ }
308
+ }
309
+
310
+ let userConfig = null
311
+ // This is the user config from the environment variable
312
+ if (this.#env.PLT_CACHE_CONFIG) {
313
+ try {
314
+ userConfig = JSON.parse(this.#env.PLT_CACHE_CONFIG)
315
+ } catch (e) {
316
+ this.#logger.error(
317
+ { err: ensureLoggableError(e) },
318
+ 'Failed to parse user cache config'
319
+ )
320
+ }
321
+ }
322
+
323
+ if (!userConfig && !autoGeneratedConfig) return null
324
+
325
+ let cacheConfig = userConfig ?? autoGeneratedConfig
326
+ if (autoGeneratedConfig && userConfig) {
327
+ cacheConfig = this.#mergeCacheConfigs(autoGeneratedConfig, userConfig)
328
+ }
329
+
330
+ const cacheTagsHeader = this.#getCacheTagsHeader(config)
331
+
332
+ for (const rule of cacheConfig.rules ?? []) {
333
+ if (rule.cacheTags) {
334
+ if (!rule.headers) {
335
+ rule.headers = {}
336
+ }
337
+ rule.headers[cacheTagsHeader] = rule.cacheTags
338
+ delete rule.cacheTags
339
+ }
340
+ }
341
+
342
+ return {
343
+ module: this.#require.resolve('@platformatic/slicer-interceptor'),
344
+ options: cacheConfig,
345
+ }
346
+ }
347
+
348
+ #mergeCacheConfigs (autoGeneratedConfig, userConfig) {
349
+ const mergedConfig = { ...userConfig }
350
+
351
+ for (const rule of autoGeneratedConfig.rules ?? []) {
352
+ const ruleIndex = mergedConfig.rules.findIndex(
353
+ (r) => r.routeToMatch === rule.routeToMatch
354
+ )
355
+
356
+ if (ruleIndex === -1) {
357
+ mergedConfig.rules.push(rule)
358
+ }
359
+ }
360
+
361
+ return mergedConfig
362
+ }
363
+
364
+ #configureTelemetry (config) {
365
+ const enableOpenTelemetry =
366
+ !!this.#instanceConfig?.enableOpenTelemetry &&
367
+ !!this.#instanceConfig?.iccServices?.riskEngine?.url
368
+
369
+ // We need to always set an opentelemetry config to pass a telemetry
370
+ // applicationName to render a taxonomy diagram
371
+ config.telemetry = config.telemetry ?? {
372
+ enabled: enableOpenTelemetry,
373
+ applicationName: `${this.#applicationName}`,
374
+ skip: [
375
+ { method: 'GET', path: '/documentation' },
376
+ { method: 'GET', path: '/documentation/json' },
377
+ ],
378
+ exporter: {
379
+ type: 'otlp',
380
+ options: {
381
+ url:
382
+ this.#instanceConfig?.iccServices?.riskEngine?.url + '/v1/traces',
383
+ headers: {
384
+ 'x-platformatic-application-id': this.#instanceConfig.applicationId,
385
+ },
386
+ keepAlive: true,
387
+ httpAgentOptions: {
388
+ rejectUnauthorized: false,
389
+ },
390
+ },
391
+ },
392
+ }
393
+ }
394
+
395
+ #configureHttpCaching (config) {
396
+ const cacheTagsHeader = this.#getCacheTagsHeader(config)
397
+ const httpCache = this.#instanceConfig?.httpCache?.clientOpts
398
+
399
+ if (!httpCache?.host) {
400
+ this.#logger.warn(
401
+ 'Missing required environment variables for Redis cache, not setting up HTTP cache'
402
+ )
403
+ return
404
+ }
405
+
406
+ config.httpCache = {
407
+ ...config.httpCache,
408
+ cacheTagsHeader,
409
+ store: this.#require.resolve('@platformatic/undici-cache-redis'),
410
+ clientOpts: httpCache,
411
+ }
412
+ }
413
+
414
+ #configureHealth (config) {
415
+ config.health = {
416
+ ...config.health,
417
+ enabled: true,
418
+ interval: 1000,
419
+ maxUnhealthyChecks: 30,
420
+ }
421
+ }
422
+
423
+ #configureScheduler (config) {
424
+ // Disable all watt schedules. We do that because
425
+ // we will create/update them in ICC, not on watt in memory
426
+ if (config.scheduler) {
427
+ config.scheduler = config.scheduler.map((scheduler) => ({
428
+ ...scheduler,
429
+ enabled: false,
430
+ }))
431
+ }
432
+ }
433
+
434
+ #configureSystemResources (config) {
435
+ if (!this.#instanceConfig) {
436
+ return
437
+ }
438
+ // Set system wide resources
439
+ const resources = this.#instanceConfig?.config?.resources
440
+ if (!resources) {
441
+ return
442
+ }
443
+
444
+ const { threads, heap } = resources
445
+
446
+ if (threads > 0) {
447
+ config.workers = threads
448
+ }
449
+
450
+ if (heap > 0) {
451
+ config.health ??= {}
452
+ config.health.maxHeapTotal = heap * 1024 * 1024
453
+ }
454
+
455
+ // In v3 we renamed services to applications, so we support both (note that this is coming from icc)
456
+ const res = resources.services || resources.applications
457
+
458
+ // Set services resources
459
+ for (const application of config.applications ?? []) {
460
+ let applicationResources = res?.find((s) => s.name === application.id)
461
+
462
+ if (!applicationResources) {
463
+ applicationResources = {
464
+ threads,
465
+ heap,
466
+ }
467
+ }
468
+ application.workers = applicationResources.threads
469
+ application.health ??= {}
470
+ application.health.maxHeapTotal = applicationResources.heap * 1024 * 1024
471
+ }
472
+ }
473
+
474
+ async #configureServices (runtime) {
475
+ if (typeof runtime.setApplicationConfigPatch !== 'function') {
476
+ return
477
+ }
478
+
479
+ const config = runtime.getRuntimeConfig(true)
480
+
481
+ for (const app of config.applications ?? []) {
482
+ if (app.type === 'next') {
483
+ await this.#configureNextService(runtime, app)
484
+ } else if (
485
+ [
486
+ '@platformatic/service',
487
+ '@platformatic/composer',
488
+ '@platformatic/db',
489
+ ].includes(app.type)
490
+ ) {
491
+ await this.#configurePlatformaticServices(runtime, app)
492
+ }
493
+ }
494
+ }
495
+
496
+ async #configureNextService (runtime, service) {
497
+ let nextSchema
498
+
499
+ try {
500
+ const nextPackage = createRequire(
501
+ resolve(service.path, 'index.js')
502
+ ).resolve('@platformatic/next')
503
+ nextSchema = JSON.parse(
504
+ await readFile(resolve(nextPackage, '../schema.json'), 'utf8')
505
+ )
506
+ } catch (e) {
507
+ this.#logger.error(
508
+ { err: ensureLoggableError(e) },
509
+ `Failed to load @platformatic/next schema for service ${service.id}`
510
+ )
511
+ throw e
512
+ }
513
+
514
+ const patches = []
515
+
516
+ if ('cache' in nextSchema.properties) {
517
+ const httpCache = this.#instanceConfig?.httpCache?.clientOpts || {}
518
+ const { keyPrefix, host, port, username, password } = httpCache
519
+
520
+ if (!keyPrefix || !host || !port) {
521
+ this.#logger.warn(
522
+ 'Missing required environment variables for Redis cache, not setting up HTTP next cache'
523
+ )
524
+ } else {
525
+ patches.push({
526
+ op: 'add',
527
+ path: '/cache',
528
+ value: {
529
+ adapter: 'valkey',
530
+ url: `valkey://${username}:${password}@${host}:${port}`,
531
+ prefix: keyPrefix,
532
+ maxTTL: 604800, // 86400 * 7
533
+ },
534
+ })
535
+ }
536
+ }
537
+
538
+ // Add trailingSlash true to Next entrypoints that support it
539
+ // This is technically useless as Next.js will manage it at build time, but we keep it
540
+ // in case in the future they compare build and production next.config.js
541
+ if (
542
+ service.entrypoint &&
543
+ nextSchema.properties.next?.properties.trailingSlash?.type === 'boolean'
544
+ ) {
545
+ patches.push({ op: 'add', path: '/next/trailingSlash', value: true })
546
+ }
547
+
548
+ if (patches.length) {
549
+ this.#patchService(runtime, service.id, patches)
550
+ }
551
+ }
552
+
553
+ async #configurePlatformaticServices (runtime, app) {
554
+ if (app.entrypoint) {
555
+ const config = app
556
+ const patches = [{ op: 'add', path: '/server/trustProxy', value: true }]
557
+
558
+ if (!config.server) {
559
+ patches.unshift({ op: 'add', path: '/server', value: {} })
560
+ }
561
+
562
+ patches.push({ op: 'remove', path: '/server/https' })
563
+
564
+ this.#patchService(runtime, app.id, patches)
565
+ }
566
+ }
567
+
568
+ async #patchService (runtime, id, patches) {
569
+ this.#logger.info({ patches }, `Applying patches to service ${id} ...`)
570
+ runtime.setApplicationConfigPatch(id, patches)
571
+ }
572
+
573
+ #getCacheTagsHeader (config) {
574
+ const customCacheTagsHeader = config.httpCache?.cacheTagsHeader
575
+ const defaultCacheTagsHeader = this.#env.PLT_DEFAULT_CACHE_TAGS_HEADER
576
+ return customCacheTagsHeader ?? defaultCacheTagsHeader
577
+ }
578
+ }
579
+
580
+ export default WattPro
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@platformatic/watt-extra",
3
+ "version": "0.1.0",
4
+ "description": "The Platformatic runtime manager",
5
+ "type": "module",
6
+ "scripts": {
7
+ "lint": "eslint",
8
+ "test": "pnpm run lint && borp -c 1 --timeout=180000 ./test/*.test.js",
9
+ "start": "node index.js"
10
+ },
11
+ "bin": {
12
+ "watt-extra": "./cli.js"
13
+ },
14
+ "devDependencies": {
15
+ "@fastify/websocket": "^11.1.0",
16
+ "@platformatic/composer": "^3.0.1",
17
+ "@platformatic/next": "^3.0.1",
18
+ "@platformatic/node": "^3.0.1",
19
+ "@platformatic/service": "^3.0.1",
20
+ "borp": "^0.20.0",
21
+ "eslint": "9",
22
+ "fastify": "^5.4.0",
23
+ "fastify-plugin": "^5.0.1",
24
+ "neostandard": "^0.12.0",
25
+ "next": "^15.3.4",
26
+ "platformatic": "^3.0.1",
27
+ "pprof-format": "^2.1.0",
28
+ "why-is-node-running": "^2.3.0"
29
+ },
30
+ "dependencies": {
31
+ "@datadog/pprof": "^5.9.0",
32
+ "@fastify/error": "^4.2.0",
33
+ "@platformatic/runtime": "^3.0.1",
34
+ "@platformatic/slicer-interceptor": "^0.3.0",
35
+ "@platformatic/undici-cache-redis": "^0.7.1",
36
+ "@platformatic/wattpm-pprof-capture": "^3.0.1",
37
+ "undici-traffic-interceptor": "^0.1.4",
38
+ "avvio": "^9.1.0",
39
+ "chalk": "^4.1.2",
40
+ "commist": "^3.2.0",
41
+ "env-schema": "^6.0.1",
42
+ "execa": "^9.6.0",
43
+ "help-me": "^5.0.0",
44
+ "minimist": "^1.2.8",
45
+ "pino": "^9.7.0",
46
+ "pino-pretty": "^13.0.0",
47
+ "undici": "^7.11.0",
48
+ "ws": "^8.18.3"
49
+ }
50
+ }