@ditojs/server 1.5.0 → 1.5.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ditojs/server",
3
- "version": "1.5.0",
3
+ "version": "1.5.3",
4
4
  "type": "module",
5
5
  "description": "Dito.js Server – Dito.js is a declarative and modern web framework, based on Objection.js, Koa.js and Vue.js",
6
6
  "repository": "https://github.com/ditojs/dito/tree/master/packages/server",
@@ -30,7 +30,7 @@
30
30
  "@originjs/vite-plugin-commonjs": "^1.0.3",
31
31
  "ajv": "^8.11.0",
32
32
  "ajv-formats": "^2.1.1",
33
- "aws-sdk": "^2.1106.0",
33
+ "aws-sdk": "^2.1108.0",
34
34
  "axios": "^0.26.1",
35
35
  "bcryptjs": "^2.4.3",
36
36
  "bytes": "^3.1.2",
@@ -60,6 +60,7 @@
60
60
  "passport-local": "^1.0.0",
61
61
  "passthrough-counter": "^1.0.0",
62
62
  "picocolors": "^1.0.0",
63
+ "picomatch": "^2.3.1",
63
64
  "pino": "^7.9.2",
64
65
  "pino-pretty": "^7.6.0",
65
66
  "pluralize": "^8.0.0",
@@ -75,10 +76,10 @@
75
76
  "objection": "^3.0.1"
76
77
  },
77
78
  "devDependencies": {
78
- "knex": "^1.0.4",
79
+ "knex": "^1.0.5",
79
80
  "objection": "^3.0.1",
80
81
  "pg": "^8.7.3",
81
82
  "sqlite3": "^5.0.2"
82
83
  },
83
- "gitHead": "36a57512228cc233211bf6e4038123a9e775f5c5"
84
+ "gitHead": "ec436ef535d96b1653914df6194cc865dfe16ead"
84
85
  }
@@ -22,8 +22,8 @@ import helmet from 'koa-helmet'
22
22
  import responseTime from 'koa-response-time'
23
23
  import Router from '@ditojs/router'
24
24
  import {
25
- isArray, isObject, isString, asArray, isPlainObject, hyphenate, clone, merge,
26
- parseDataPath, normalizeDataPath, isModule
25
+ isArray, isObject, isString, asArray, isPlainObject, isModule,
26
+ hyphenate, clone, merge, parseDataPath, normalizeDataPath, toPromiseCallback
27
27
  } from '@ditojs/utils'
28
28
  import SessionStore from './SessionStore.js'
29
29
  import { Validator } from './Validator.js'
@@ -42,6 +42,7 @@ import {
42
42
  import {
43
43
  attachLogger,
44
44
  createTransaction,
45
+ ensureRunning,
45
46
  findRoute,
46
47
  handleError,
47
48
  handleRoute,
@@ -88,6 +89,9 @@ export class Application extends Koa {
88
89
  this.models = Object.create(null)
89
90
  this.services = Object.create(null)
90
91
  this.controllers = Object.create(null)
92
+ this.server = null
93
+ this.isRunning = false
94
+
91
95
  this.setupLogger()
92
96
  this.setupKnex()
93
97
  this.setupMiddleware(middleware)
@@ -283,10 +287,6 @@ export class Application extends Koa {
283
287
  return Object.values(this.services).find(callback)
284
288
  }
285
289
 
286
- forEachService(callback) {
287
- return Promise.all(Object.values(this.services).map(callback))
288
- }
289
-
290
290
  addControllers(controllers, namespace) {
291
291
  for (const [key, value] of Object.entries(controllers)) {
292
292
  if (isModule(value) || isPlainObject(value)) {
@@ -508,6 +508,7 @@ export class Application extends Koa {
508
508
 
509
509
  // Setup global middleware
510
510
 
511
+ this.use(ensureRunning(this))
511
512
  this.use(attachLogger(this.logger))
512
513
  if (app.responseTime !== false) {
513
514
  this.use(responseTime(getOptions(app.responseTime)))
@@ -718,46 +719,56 @@ export class Application extends Koa {
718
719
  this.on('error', this.logError)
719
720
  }
720
721
  await this.emit('before:start')
721
- await this.forEachService(service => service.start())
722
- const { server: { host, port } } = this.config
723
- this.server = await new Promise((resolve, reject) => {
724
- const server = this.listen(port, host, () => {
725
- const { port } = server.address()
722
+ this.server = await new Promise(resolve => {
723
+ const server = this.listen(this.config.server, () => {
724
+ const { address, port } = server.address()
726
725
  console.info(
727
- `Dito server started at http://${host}:${port}`
726
+ `Dito.js server started at http://${address}:${port}`
728
727
  )
729
728
  resolve(server)
730
729
  })
731
- if (!server) {
732
- reject(new Error(`Unable to start server at http://${host}:${port}`))
733
- }
734
730
  })
731
+ if (!this.server) {
732
+ throw new Error('Unable to start Dito.js server')
733
+ }
734
+ this.isRunning = true
735
735
  await this.emit('after:start')
736
736
  }
737
737
 
738
- async stop() {
739
- await this.emit('before:stop')
740
- this.server = await new Promise((resolve, reject) => {
741
- const { server } = this
742
- if (server) {
743
- server.close(err => {
744
- if (err) {
745
- reject(err)
746
- } else {
747
- resolve(null)
748
- }
749
- })
750
- // Hack to make sure that we close the server,
751
- // even if sockets are still open.
752
- // Taken from https://stackoverflow.com/a/36830072.
753
- // A proper solution would be to use a library, ex: https://github.com/godaddy/terminus
754
- setImmediate(() => server.emit('close'))
755
- } else {
756
- reject(new Error('Server is not running'))
757
- }
758
- })
759
- await this.forEachService(service => service.stop())
760
- await this.emit('after:stop')
738
+ async stop(timeout = 0) {
739
+ if (!this.server) {
740
+ throw new Error('Dito.js server is not running')
741
+ }
742
+
743
+ const promise = (async () => {
744
+ await this.emit('before:stop')
745
+ this.isRunning = false
746
+ await new Promise((resolve, reject) => {
747
+ this.server.close(toPromiseCallback(resolve, reject))
748
+ })
749
+ // Hack to make sure that the server is closed, even if sockets are still
750
+ // open after `server.close()`, see: https://stackoverflow.com/a/36830072
751
+ this.server.emit('close')
752
+ this.server = null
753
+ await this.emit('after:stop')
754
+ })()
755
+
756
+ if (timeout > 0) {
757
+ await Promise.race([
758
+ promise,
759
+ new Promise((resolve, reject) =>
760
+ setTimeout(reject,
761
+ timeout,
762
+ new Error(
763
+ `Timeout reached while stopping Dito.js server (${timeout}ms)`
764
+ )
765
+ )
766
+ )
767
+ ])
768
+ } else {
769
+ await promise
770
+ }
771
+
761
772
  if (this.config.log.errors !== false) {
762
773
  this.off('error', this.logError)
763
774
  }
@@ -1,4 +1,5 @@
1
1
  import path from 'path'
2
+ import { exit } from 'process'
2
3
  import Koa from 'koa'
3
4
  import serve from 'koa-static'
4
5
  import { defineConfig, createServer } from 'vite'
@@ -90,6 +91,7 @@ export class AdminController extends Controller {
90
91
  }
91
92
  }
92
93
 
94
+ // @override
93
95
  compose() {
94
96
  this.koa = new Koa()
95
97
  this.koa.use(this.middleware())
@@ -128,6 +130,32 @@ export class AdminController extends Controller {
128
130
  }
129
131
  }
130
132
  })
133
+
134
+ let closed = false
135
+
136
+ // Monkey-patch `process.exit()` to filter out the calls caused by vite's
137
+ // handling of SIGTERM, see: https://github.com/vitejs/vite/issues/7627
138
+ process.exit = code => {
139
+ // Filter out calls from inside vite by looking at the stack trace.
140
+ if (new Error().stack.includes('/vite/dist/')) {
141
+ // vite's own `exitProcess()` just called `process.exit(), and this
142
+ // means it has already called `server.close()` internally.
143
+ closed = true
144
+ process.exit = exit
145
+ } else {
146
+ exit(code)
147
+ }
148
+ }
149
+
150
+ this.app.once('before:stop', () => {
151
+ // For good timing it seems crucial to not add more ticks with async
152
+ // signature, so we directly return the `server.close()` promise instead.
153
+ if (!closed) {
154
+ closed = true
155
+ return server.close()
156
+ }
157
+ })
158
+
131
159
  this.koa.use(handleConnectMiddleware(server.middlewares, {
132
160
  expandMountPath: true
133
161
  }))
@@ -26,6 +26,12 @@ export class Controller {
26
26
  initialize() {
27
27
  }
28
28
 
29
+ // @return {Application|Function} [app or function]
30
+ compose() {
31
+ // To be overridden in sub-classes, if the controller needs to install
32
+ // middleware. For normal routes, use `this.app.addRoute()` instead.
33
+ }
34
+
29
35
  setup(isRoot = true, setupActionsObject = true) {
30
36
  this._setupEmitter(this.hooks, {
31
37
  // Support wildcard hooks only on controllers:
@@ -246,12 +252,6 @@ export class Controller {
246
252
  ])
247
253
  }
248
254
 
249
- // @return {Application|Function} [app or function]
250
- compose() {
251
- // To be overridden in sub-classes, if the controller needs to install
252
- // middleware. For normal routes, use `this.app.addRoute()` instead.
253
- }
254
-
255
255
  getPath(type, path) {
256
256
  // To be overridden by sub-classes.
257
257
  return path
@@ -0,0 +1,13 @@
1
+ export function ensureRunning(app) {
2
+ return async (ctx, next) => {
3
+ if (app.isRunning) {
4
+ await next()
5
+ } else {
6
+ // When the app isn't running, e.g. while stopping, we don't want to send
7
+ // content back anymore even if the controllers would still respond. This
8
+ // is to avoid strange behavior during shut-down of the vite dev server.
9
+ // For that scenario, sending back an empty 205 response seems to work.
10
+ ctx.status = 205 // HTTP_RESET_CONTENT
11
+ }
12
+ }
13
+ }
@@ -1,5 +1,6 @@
1
1
  export * from './attachLogger.js'
2
2
  export * from './createTransaction.js'
3
+ export * from './ensureRunning.js'
3
4
  export * from './findRoute.js'
4
5
  export * from './handleConnectMiddleware.js'
5
6
  export * from './handleError.js'
@@ -11,6 +11,8 @@ export class Service {
11
11
 
12
12
  setup(config) {
13
13
  this.config = config
14
+ this.app.on('before:start', () => this.start())
15
+ this.app.on('after:stop', () => this.stop())
14
16
  }
15
17
 
16
18
  // @overridable
@@ -1,11 +1,11 @@
1
1
  import path from 'path'
2
+ import { URL } from 'url'
2
3
  import multer from '@koa/multer'
4
+ import picomatch from 'picomatch'
3
5
  import imageSize from 'image-size'
4
6
  import { PassThrough } from 'stream'
5
- import { URL } from 'url'
6
7
  import { hyphenate, toPromiseCallback } from '@ditojs/utils'
7
8
  import { AssetFile } from './AssetFile.js'
8
- import { matchGlobPattern } from '../utils/glob.js'
9
9
 
10
10
  const storageClasses = {}
11
11
 
@@ -59,11 +59,7 @@ export class Storage {
59
59
  }
60
60
 
61
61
  isImportSourceAllowed(url) {
62
- const { allowedImports = [] } = this.config
63
- for (const pattern of allowedImports) {
64
- if (matchGlobPattern(url, pattern)) return true
65
- }
66
- return false
62
+ return picomatch.isMatch(url, this.config.allowedImports || [])
67
63
  }
68
64
 
69
65
  convertAssetFile(file) {
package/src/utils/glob.js DELETED
@@ -1,9 +0,0 @@
1
- import { escapeRegexp } from '@ditojs/utils'
2
-
3
- export function matchGlobPattern(str, pattern) {
4
- const exp = escapeRegexp(pattern).replace(
5
- /\\([*?])/,
6
- (_, chr) => chr === '*' ? '.*' : chr
7
- )
8
- return new RegExp(`^${exp}$`).test(str)
9
- }