@platformatic/service 0.33.1 → 0.34.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/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  'use strict'
2
2
 
3
3
  const { isKeyEnabled } = require('@platformatic/utils')
4
+ const { readFile } = require('fs/promises')
5
+ const { dirname, join } = require('path')
4
6
 
5
7
  const compiler = require('./lib/compile')
6
8
  const setupCors = require('./lib/plugins/cors')
@@ -65,13 +67,6 @@ async function platformaticService (app, opts, toLoad = []) {
65
67
  await app.register(loadPlugins)
66
68
  }
67
69
 
68
- if (isKeyEnabled('watch', config)) {
69
- // If file watching is enabled here, that means the service was started
70
- // without the runtime because the runtime explicitly disables watching on
71
- // services that it starts. Warn the user that things will not go as planned.
72
- app.log.warn('service was started with file watching enabled but watching is only available via the runtime')
73
- }
74
-
75
70
  if (config.server.cors) {
76
71
  app.register(setupCors, config.server.cors)
77
72
  }
@@ -98,11 +93,32 @@ platformaticService.configManagerConfig = {
98
93
  allErrors: true,
99
94
  strict: false
100
95
  },
101
- transformConfig () {
96
+ async transformConfig () {
102
97
  // Set watch to true by default. This is not possible
103
98
  // to do in the schema, because it is uses an anyOf.
104
99
  if (this.current.watch === undefined) {
105
- this.current.watch = true
100
+ this.current.watch = { enabled: false }
101
+ }
102
+
103
+ if (typeof this.current.watch !== 'object') {
104
+ this.current.watch = { enabled: this.current.watch || false }
105
+ }
106
+
107
+ const typescript = this.current.plugins?.typescript
108
+ if (typescript) {
109
+ let outDir = typescript.outDir
110
+ if (outDir === undefined) {
111
+ let tsConfigFile = typescript.tsConfigFile || 'tsconfig.json'
112
+ tsConfigFile = join(dirname(this.fullPath), tsConfigFile)
113
+ try {
114
+ const tsConfig = JSON.parse(await readFile(tsConfigFile, 'utf8'))
115
+ outDir = tsConfig.compilerOptions.outDir
116
+ } catch {}
117
+ outDir ||= 'dist'
118
+ }
119
+
120
+ this.current.watch.ignore ||= []
121
+ this.current.watch.ignore.push(outDir + '/**/*')
106
122
  }
107
123
  }
108
124
  }
@@ -10,11 +10,21 @@ module.exports = fp(async function (app, opts) {
10
10
  plugin = { path: plugin, encapsulate: true }
11
11
  }
12
12
  if ((await stat(plugin.path)).isDirectory()) {
13
+ const patternOptions = patternOptionsFromPlugin(plugin)
14
+
13
15
  app.register(autoload, {
14
16
  dir: plugin.path,
15
17
  encapsulate: plugin.encapsulate !== false,
16
18
  maxDepth: plugin.maxDepth,
17
- options: plugin.options
19
+ options: plugin.options,
20
+ autoHooks: plugin.autoHooks,
21
+ cascadeHooks: plugin.cascadeHooks,
22
+ overwriteHooks: plugin.overwriteHooks,
23
+ routeParams: plugin.routeParams,
24
+ forceESM: plugin.forceESM,
25
+ ignoreFilter: plugin.ignoreFilter,
26
+ matchFilter: plugin.matchFilter,
27
+ ...patternOptions
18
28
  })
19
29
  } else {
20
30
  let loaded = await import(`file://${plugin.path}`)
@@ -23,13 +33,58 @@ module.exports = fp(async function (app, opts) {
23
33
  loaded = loaded.default
24
34
  }
25
35
 
26
- let skipOverride
27
36
  if (plugin.encapsulate === false) {
28
- skipOverride = loaded[Symbol.for('skip-override')]
37
+ const skipOverride = loaded[Symbol.for('skip-override')]
29
38
  loaded[Symbol.for('skip-override')] = true
39
+ await app.register(loaded, plugin.options)
40
+ loaded[Symbol.for('skip-override')] = skipOverride
41
+ } else {
42
+ await app.register(loaded, plugin.options)
30
43
  }
31
- await app.register(loaded, plugin.options)
32
- loaded[Symbol.for('skip-override')] = skipOverride
33
44
  }
34
45
  }
35
46
  })
47
+
48
+ /**
49
+ * Creates an object for pattern specific options. This ensures that
50
+ * only configurations that have been provided are included in the
51
+ * final result. This prevents 'cannot read properties of undefined'
52
+ * errors when undefined configs are provided to the underlying
53
+ * @fastify/autoload plugin.
54
+ */
55
+ function patternOptionsFromPlugin (plugin) {
56
+ const config = {}
57
+
58
+ // Expected keys for autoload plugin options that expect regexp patterns
59
+ const patternOptionKeys = ['ignorePattern', 'indexPattern', 'autoHooksPattern']
60
+
61
+ for (const key of patternOptionKeys) {
62
+ const pattern = plugin[key]
63
+
64
+ // If pattern key not found in plugin object, move on
65
+ if (!pattern) {
66
+ continue
67
+ }
68
+
69
+ // Build an instance of a RegExp. If this comes back undefined,
70
+ // then an invalid value was provided. Move on.
71
+ const regExpPattern = stringPatternToRegExp(pattern)
72
+ if (!regExpPattern) {
73
+ continue
74
+ }
75
+
76
+ // We have a valid RegExp so add the option to the config to pass along to
77
+ // autoload.
78
+ config[key] = regExpPattern
79
+ }
80
+
81
+ return config
82
+ }
83
+
84
+ function stringPatternToRegExp (stringPattern) {
85
+ try {
86
+ return new RegExp(stringPattern)
87
+ } catch (err) {
88
+ return undefined
89
+ }
90
+ }
package/lib/schema.js CHANGED
@@ -334,6 +334,14 @@ const server = {
334
334
  const watch = {
335
335
  type: 'object',
336
336
  properties: {
337
+ enabled: {
338
+ default: true,
339
+ anyOf: [{
340
+ type: 'boolean'
341
+ }, {
342
+ type: 'string'
343
+ }]
344
+ },
337
345
  allow: {
338
346
  type: 'array',
339
347
  items: {
@@ -379,6 +387,39 @@ const plugins = {
379
387
  maxDepth: {
380
388
  type: 'integer'
381
389
  },
390
+ autoHooks: {
391
+ type: 'boolean'
392
+ },
393
+ autoHooksPattern: {
394
+ type: 'string'
395
+ },
396
+ cascadeHooks: {
397
+ type: 'boolean'
398
+ },
399
+ overwriteHooks: {
400
+ type: 'boolean'
401
+ },
402
+ routeParams: {
403
+ type: 'boolean'
404
+ },
405
+ forceESM: {
406
+ type: 'boolean'
407
+ },
408
+ ignoreFilter: {
409
+ type: 'string'
410
+ },
411
+ matchFilter: {
412
+ type: 'string'
413
+ },
414
+ ignorePattern: {
415
+ type: 'string'
416
+ },
417
+ scriptPattern: {
418
+ type: 'string'
419
+ },
420
+ indexPattern: {
421
+ type: 'string'
422
+ },
382
423
  options: {
383
424
  type: 'object',
384
425
  additionalProperties: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/service",
3
- "version": "0.33.1",
3
+ "version": "0.34.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -65,12 +65,12 @@
65
65
  "pino-pretty": "^10.0.0",
66
66
  "rfdc": "^1.3.0",
67
67
  "ua-parser-js": "^1.0.35",
68
- "@platformatic/client": "0.33.1",
69
- "@platformatic/config": "0.33.1",
70
- "@platformatic/swagger-ui-theme": "0.33.1",
71
- "@platformatic/types": "0.33.1",
72
- "@platformatic/utils": "0.33.1",
73
- "@platformatic/telemetry": "0.33.1"
68
+ "@platformatic/client": "0.34.0",
69
+ "@platformatic/config": "0.34.0",
70
+ "@platformatic/swagger-ui-theme": "0.34.0",
71
+ "@platformatic/types": "0.34.0",
72
+ "@platformatic/utils": "0.34.0",
73
+ "@platformatic/telemetry": "0.34.0"
74
74
  },
75
75
  "standard": {
76
76
  "ignore": [
@@ -342,3 +342,145 @@ test('disable encapsulation for a single file / different order', async ({ teard
342
342
  equal(body, 'bar')
343
343
  }
344
344
  })
345
+
346
+ test('autoload with ignorePattern, indexPattern and autoHooksPattern options', async ({ teardown, equal }) => {
347
+ const config = {
348
+ server: {
349
+ hostname: '127.0.0.1',
350
+ port: 0
351
+ },
352
+ plugins: {
353
+ paths: [
354
+ {
355
+ path: join(__dirname, 'fixtures', 'directories', 'routes'),
356
+
357
+ // Ignore the bar.js which should return a 404 for requests made to /bar
358
+ ignorePattern: '^.*(?:bar).js$',
359
+
360
+ // Set index2.js as the index file which sets the root as /index2
361
+ indexPattern: '^index2(?:.js)$',
362
+
363
+ // Override default autohooks.js with auto.hooks.js which overrides
364
+ // the response body
365
+ autoHooksPattern: '^auto.hooks.js$',
366
+ autoHooks: true
367
+ }
368
+ ]
369
+ },
370
+ watch: false,
371
+ metrics: false
372
+ }
373
+
374
+ const app = await buildServer(config)
375
+ teardown(async () => {
376
+ await app.close()
377
+ })
378
+ await app.start()
379
+
380
+ {
381
+ const res = await request(`${app.url}/`)
382
+ equal(res.statusCode, 200, 'status code')
383
+ const body = await res.body.json()
384
+ equal(body.hello, 'from root', 'body')
385
+ }
386
+
387
+ {
388
+ const res = await request(`${app.url}/foo/bar`)
389
+ equal(res.statusCode, 404, 'status code')
390
+ }
391
+
392
+ {
393
+ const res = await request(`${app.url}/foo/baz/index2`)
394
+ equal(res.statusCode, 200, 'status code')
395
+ const body = await res.body.json()
396
+ equal(body.hello, 'from baz with index2.js', 'body')
397
+ }
398
+
399
+ {
400
+ const res = await request(`${app.url}/oof`)
401
+ equal(res.statusCode, 200, 'status code')
402
+ const body = await res.body.json()
403
+ equal(body.hello, 'from auto.hooks.js', 'body')
404
+ }
405
+ })
406
+
407
+ test('autoload with INVALID ignorePattern, indexPattern and autoHooksPattern options', async ({ teardown, equal }) => {
408
+ const config = {
409
+ server: {
410
+ hostname: '127.0.0.1',
411
+ port: 0
412
+ },
413
+ plugins: {
414
+ paths: [
415
+ {
416
+ path: join(__dirname, 'fixtures', 'directories', 'routes'),
417
+ ignorePattern: '***',
418
+ indexPattern: '***terrible)))_pattern',
419
+ autoHooksPattern: ''
420
+ }
421
+ ]
422
+ },
423
+ watch: false,
424
+ metrics: false
425
+ }
426
+
427
+ const app = await buildServer(config)
428
+ teardown(async () => {
429
+ await app.close()
430
+ })
431
+ await app.start()
432
+
433
+ {
434
+ const res = await request(`${app.url}/`)
435
+ equal(res.statusCode, 200, 'status code')
436
+ }
437
+ })
438
+
439
+ test('loads encapsulated plugin twice', async ({ teardown, equal, strictSame }) => {
440
+ const config = {
441
+ server: {
442
+ hostname: '127.0.0.1',
443
+ port: 0,
444
+ // Windows CI is slow
445
+ pluginTimeout: 60 * 1000
446
+ },
447
+ service: {
448
+ openapi: true
449
+ },
450
+ plugins: {
451
+ paths: [{
452
+ path: join(__dirname, 'fixtures', 'directories', 'routes')
453
+ }, {
454
+ path: join(__dirname, 'fixtures', 'directories', 'plugins', 'decorator.js')
455
+ }]
456
+ }
457
+ }
458
+
459
+ {
460
+ // First time plugin is loaded from file
461
+ const app = await buildServer(config)
462
+ teardown(async () => {
463
+ await app.close()
464
+ })
465
+ await app.start()
466
+
467
+ const res = await request(`${app.url}/foo/with-decorator`)
468
+ equal(res.statusCode, 200, 'status code')
469
+ const body = await res.body.json()
470
+ strictSame(body, { hello: 'bar' })
471
+ }
472
+
473
+ {
474
+ // Second time plugin is loaded from cache
475
+ const app = await buildServer(config)
476
+ teardown(async () => {
477
+ await app.close()
478
+ })
479
+ await app.start()
480
+
481
+ const res = await request(`${app.url}/foo/with-decorator`)
482
+ equal(res.statusCode, 200, 'status code')
483
+ const body = await res.body.json()
484
+ strictSame(body, { hello: 'bar' })
485
+ }
486
+ })
@@ -6,7 +6,7 @@ const { buildServer } = require('..')
6
6
  const { request } = require('undici')
7
7
  const { join } = require('path')
8
8
  const os = require('os')
9
- const { writeFile } = require('fs/promises')
9
+ const { writeFile, rm } = require('fs/promises')
10
10
 
11
11
  test('config reloads', async ({ teardown, equal, pass, same }) => {
12
12
  const file = join(os.tmpdir(), `${process.pid}-1.js`)
@@ -295,3 +295,22 @@ test('config reloads', async ({ teardown, equal, pass, same }) => {
295
295
  same(await res.body.text(), 'ciao mondo', 'response')
296
296
  }
297
297
  })
298
+
299
+ test('do not watch typescript outDir', async ({ teardown, equal, pass, same }) => {
300
+ process.env.PLT_CLIENT_URL = 'http://localhost:3042'
301
+ const targetDir = join(__dirname, '..', 'fixtures', 'hello-client-ts')
302
+
303
+ try {
304
+ await rm(join(targetDir, 'dist'), { recursive: true })
305
+ } catch {}
306
+
307
+ const app = await buildServer(join(targetDir, 'platformatic.service.json'))
308
+ teardown(async () => {
309
+ await app.close()
310
+ })
311
+
312
+ same(app.platformatic.configManager.current.watch, {
313
+ enabled: false,
314
+ ignore: ['dist/**/*']
315
+ })
316
+ })
@@ -0,0 +1,11 @@
1
+ 'use strict'
2
+
3
+ /*
4
+ * Used for providing an alternative `index.js` naming convention
5
+ * when specified from the autoload `indexPattern` property.
6
+ */
7
+ module.exports = async function (fastify, opts) {
8
+ fastify.get('/index2', async function (request, reply) {
9
+ return { hello: 'from baz with index2.js' }
10
+ })
11
+ }
@@ -0,0 +1,5 @@
1
+ module.exports = async function (app, opts) {
2
+ app.addHook('onRequest', async (req, reply) => {
3
+ reply.send({ hello: 'from auto.hooks.js' })
4
+ })
5
+ }
@@ -0,0 +1,7 @@
1
+ 'use strict'
2
+
3
+ module.exports = async function (fastify, opts) {
4
+ fastify.get('/', async function (request, reply) {
5
+ return { hello: 'from oof' }
6
+ })
7
+ }