koa 3.0.0-alpha.0 → 3.0.0-alpha.2

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/Readme.md CHANGED
@@ -17,7 +17,7 @@
17
17
 
18
18
  ## Installation
19
19
 
20
- Koa requires __node v12.17.0__ or higher for ES2015 and async function support.
20
+ Koa requires __node v18.0.0__ or higher for ES2015 and async function support.
21
21
 
22
22
  ```
23
23
  $ npm install koa
@@ -1,4 +1,3 @@
1
-
2
1
  'use strict'
3
2
 
4
3
  /**
@@ -17,7 +16,7 @@ const Emitter = require('events')
17
16
  const util = require('util')
18
17
  const Stream = require('stream')
19
18
  const http = require('http')
20
- const only = require('only')
19
+ const only = require('./only.js')
21
20
  const { HttpError } = require('http-errors')
22
21
 
23
22
  /** @typedef {typeof import ('./context') & {
@@ -55,6 +54,8 @@ module.exports = class Application extends Emitter {
55
54
  * @param {number} [options.subdomainOffset] Subdomain offset
56
55
  * @param {string} [options.proxyIpHeader] Proxy IP header, defaults to X-Forwarded-For
57
56
  * @param {number} [options.maxIpsCount] Max IPs read from proxy IP header, default to 0 (means infinity)
57
+ * @param {function} [options.compose] Function to handle middleware composition
58
+ * @param {boolean} [options.asyncLocalStorage] Max IPs read from proxy IP header, default to 0 (means infinity)
58
59
  *
59
60
  */
60
61
 
@@ -81,7 +82,6 @@ module.exports = class Application extends Emitter {
81
82
  const { AsyncLocalStorage } = require('async_hooks')
82
83
  assert(AsyncLocalStorage, 'Requires node 12.17.0 or higher to enable asyncLocalStorage')
83
84
  this.ctxStorage = new AsyncLocalStorage()
84
- this.use(this.createAsyncCtxStorageMiddleware())
85
85
  }
86
86
  }
87
87
 
@@ -160,7 +160,12 @@ module.exports = class Application extends Emitter {
160
160
 
161
161
  const handleRequest = (req, res) => {
162
162
  const ctx = this.createContext(req, res)
163
- return this.handleRequest(ctx, fn)
163
+ if (!this.ctxStorage) {
164
+ return this.handleRequest(ctx, fn)
165
+ }
166
+ return this.ctxStorage.run(ctx, async () => {
167
+ return await this.handleRequest(ctx, fn)
168
+ })
164
169
  }
165
170
 
166
171
  return handleRequest
@@ -243,15 +248,6 @@ module.exports = class Application extends Emitter {
243
248
  static get default () {
244
249
  return Application
245
250
  }
246
-
247
- createAsyncCtxStorageMiddleware () {
248
- const app = this
249
- return async function asyncCtxStorage (ctx, next) {
250
- await app.ctxStorage.run(ctx, async () => {
251
- return await next()
252
- })
253
- }
254
- }
255
251
  }
256
252
 
257
253
  /**
@@ -284,7 +280,7 @@ function respond (ctx) {
284
280
  }
285
281
 
286
282
  // status body
287
- if (body == null) {
283
+ if (body === null || body === undefined) {
288
284
  if (ctx.response._explicitNullBody) {
289
285
  ctx.response.remove('Content-Type')
290
286
  ctx.response.remove('Transfer-Encoding')
@@ -304,9 +300,13 @@ function respond (ctx) {
304
300
  }
305
301
 
306
302
  // responses
303
+
307
304
  if (Buffer.isBuffer(body)) return res.end(body)
308
305
  if (typeof body === 'string') return res.end(body)
309
306
  if (body instanceof Stream) return body.pipe(res)
307
+ if (body instanceof Blob) return Stream.Readable.from(body.stream()).pipe(res)
308
+ if (body instanceof ReadableStream) return Stream.Readable.from(body).pipe(res)
309
+ if (body instanceof Response) return Stream.Readable.from(body?.body || '').pipe(res)
310
310
 
311
311
  // body: json
312
312
  body = JSON.stringify(body)
package/lib/context.js CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  'use strict'
3
2
 
4
3
  /**
@@ -155,10 +154,10 @@ const proto = module.exports = {
155
154
  if (err.code === 'ENOENT') statusCode = 404
156
155
 
157
156
  // default to 500
158
- if (typeof statusCode !== 'number' || !statuses[statusCode]) statusCode = 500
157
+ if (typeof statusCode !== 'number' || !statuses.message[statusCode]) statusCode = 500
159
158
 
160
159
  // respond
161
- const code = statuses[statusCode]
160
+ const code = statuses.message[statusCode]
162
161
  const msg = err.expose ? err.message : code
163
162
  this.status = err.status = statusCode
164
163
  this.length = Buffer.byteLength(msg)
@@ -205,6 +204,7 @@ delegate(proto, 'response')
205
204
  .method('set')
206
205
  .method('append')
207
206
  .method('flushHeaders')
207
+ .method('back')
208
208
  .access('status')
209
209
  .access('message')
210
210
  .access('body')
package/lib/only.js ADDED
@@ -0,0 +1,9 @@
1
+ module.exports = (obj, keys) => {
2
+ const ret = {}
3
+ for (let i = 0; i < keys.length; i++) {
4
+ const key = keys[i]
5
+ if (obj[key] == null) continue
6
+ ret[key] = obj[key]
7
+ }
8
+ return ret
9
+ }
package/lib/request.js CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  'use strict'
3
2
 
4
3
  /**
@@ -11,10 +10,11 @@ const accepts = require('accepts')
11
10
  const contentType = require('content-type')
12
11
  const stringify = require('url').format
13
12
  const parse = require('parseurl')
14
- const qs = require('querystring')
13
+ const sp = require('./search-params.js')
14
+
15
15
  const typeis = require('type-is')
16
16
  const fresh = require('fresh')
17
- const only = require('only')
17
+ const only = require('./only.js')
18
18
  const util = require('util')
19
19
 
20
20
  const IP = Symbol('context#ip')
@@ -172,7 +172,7 @@ module.exports = {
172
172
  get query () {
173
173
  const str = this.querystring
174
174
  const c = this._querycache = this._querycache || {}
175
- return c[str] || (c[str] = qs.parse(str))
175
+ return c[str] || (c[str] = sp.parse(str))
176
176
  },
177
177
 
178
178
  /**
@@ -183,7 +183,7 @@ module.exports = {
183
183
  */
184
184
 
185
185
  set query (obj) {
186
- this.querystring = qs.stringify(obj)
186
+ this.querystring = sp.stringify(obj)
187
187
  },
188
188
 
189
189
  /**
@@ -211,7 +211,6 @@ module.exports = {
211
211
 
212
212
  url.search = str
213
213
  url.path = null
214
-
215
214
  this.url = stringify(url)
216
215
  },
217
216
 
@@ -376,7 +375,7 @@ module.exports = {
376
375
  /**
377
376
  * Return parsed Content-Length when present.
378
377
  *
379
- * @return {Number}
378
+ * @return {Number|void}
380
379
  * @api public
381
380
  */
382
381
 
@@ -503,7 +502,7 @@ module.exports = {
503
502
  /**
504
503
  * Set accept object.
505
504
  *
506
- * @param {Object}
505
+ * @param {Object} obj
507
506
  * @api private
508
507
  */
509
508
 
package/lib/response.js CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  'use strict'
3
2
 
4
3
  /**
@@ -15,10 +14,11 @@ const destroy = require('destroy')
15
14
  const assert = require('assert')
16
15
  const extname = require('path').extname
17
16
  const vary = require('vary')
18
- const only = require('only')
17
+ const only = require('./only.js')
19
18
  const util = require('util')
20
19
  const encodeUrl = require('encodeurl')
21
20
  const Stream = require('stream')
21
+ const deprecate = require('depd')('koa')
22
22
 
23
23
  /**
24
24
  * Prototype.
@@ -87,7 +87,7 @@ module.exports = {
87
87
  assert(code >= 100 && code <= 999, `invalid status code: ${code}`)
88
88
  this._explicitStatus = true
89
89
  this.res.statusCode = code
90
- if (this.req.httpVersionMajor < 2) this.res.statusMessage = statuses[code]
90
+ if (this.req.httpVersionMajor < 2) this.res.statusMessage = statuses.message[code]
91
91
  if (this.body && statuses.empty[code]) this.body = null
92
92
  },
93
93
 
@@ -99,7 +99,7 @@ module.exports = {
99
99
  */
100
100
 
101
101
  get message () {
102
- return this.res.statusMessage || statuses[this.status]
102
+ return this.res.statusMessage || statuses.message[this.status]
103
103
  },
104
104
 
105
105
  /**
@@ -127,15 +127,15 @@ module.exports = {
127
127
  /**
128
128
  * Set response body.
129
129
  *
130
- * @param {String|Buffer|Object|Stream} val
130
+ * @param {String|Buffer|Object|Stream|ReadableStream|Blob|Response} val
131
131
  * @api public
132
132
  */
133
133
 
134
134
  set body (val) {
135
135
  const original = this._body
136
136
  this._body = val
137
-
138
137
  // no content
138
+
139
139
  if (val == null) {
140
140
  if (!statuses.empty[this.status]) {
141
141
  if (this.type === 'application/json') {
@@ -184,6 +184,31 @@ module.exports = {
184
184
  return
185
185
  }
186
186
 
187
+ // ReadableStream
188
+ if (val instanceof ReadableStream) {
189
+ if (setType) this.type = 'bin'
190
+ return
191
+ }
192
+
193
+ // blob
194
+ if (val instanceof Blob) {
195
+ if (setType) this.type = 'bin'
196
+ this.length = val.size
197
+ return
198
+ }
199
+
200
+ // Response
201
+ if (val instanceof Response) {
202
+ this.status = val.status
203
+ if (setType) this.type = 'bin'
204
+ const headers = val.headers
205
+ for (const key of headers.keys()) {
206
+ this.set(key, headers.get(key))
207
+ }
208
+
209
+ return
210
+ }
211
+
187
212
  // json
188
213
  this.remove('Content-Length')
189
214
  this.type = 'json'
@@ -235,7 +260,7 @@ module.exports = {
235
260
  /**
236
261
  * Vary on `field`.
237
262
  *
238
- * @param {String} field
263
+ * @param {String|String[]} field
239
264
  * @api public
240
265
  */
241
266
 
@@ -266,7 +291,15 @@ module.exports = {
266
291
 
267
292
  redirect (url, alt) {
268
293
  // location
269
- if (url === 'back') url = this.ctx.get('Referrer') || alt || '/'
294
+ if (url === 'back') {
295
+ deprecate('Special-cased string "back" through redirect will be removed in v3, ' +
296
+ 'consider migrating usage to ctx.back() instead.')
297
+ url = this.ctx.get('Referrer') || alt || '/'
298
+ }
299
+ if (/^https?:\/\//i.test(url)) {
300
+ // formatting url again avoid security escapes
301
+ url = new URL(url).toString()
302
+ }
270
303
  this.set('Location', encodeUrl(url))
271
304
 
272
305
  // status
@@ -285,10 +318,31 @@ module.exports = {
285
318
  this.body = `Redirecting to ${url}.`
286
319
  },
287
320
 
321
+ /**
322
+ * Perform a special-cased "back" to provide Referrer support.
323
+ * When Referrer is not present, `alt` or "/" is used.
324
+ *
325
+ * Examples:
326
+ *
327
+ * ctx.back()
328
+ * ctx.back('/index.html')
329
+ *
330
+ * @param {String} [alt]
331
+ * @api public
332
+ */
333
+
334
+ back (alt) {
335
+ const url = this.ctx.get('Referrer') || alt || '/'
336
+ this.redirect(url)
337
+ },
338
+
288
339
  /**
289
340
  * Set Content-Disposition header to "attachment" with optional `filename`.
290
341
  *
291
- * @param {String} filename
342
+ * @param {String} [filename]
343
+ * @param {object} [options]
344
+ * @param {string} [options.type=attachment]
345
+ * @param {string|boolean} [options.fallback=true]
292
346
  * @api public
293
347
  */
294
348
 
@@ -328,7 +382,7 @@ module.exports = {
328
382
  * this.response.lastModified = new Date();
329
383
  * this.response.lastModified = '2013-09-13';
330
384
  *
331
- * @param {String|Date} type
385
+ * @param {String|Date} val
332
386
  * @api public
333
387
  */
334
388
 
@@ -357,7 +411,7 @@ module.exports = {
357
411
  * this.response.etag = '"md5hashsum"';
358
412
  * this.response.etag = 'W/"123456789"';
359
413
  *
360
- * @param {String} etag
414
+ * @param {String} val
361
415
  * @api public
362
416
  */
363
417
 
@@ -460,7 +514,7 @@ module.exports = {
460
514
  * this.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
461
515
  *
462
516
  * @param {String|Object|Array} field
463
- * @param {String} val
517
+ * @param {*} val
464
518
  * @api public
465
519
  */
466
520
 
@@ -490,7 +544,7 @@ module.exports = {
490
544
  * ```
491
545
  *
492
546
  * @param {String} field
493
- * @param {String|Array} val
547
+ * @param {*} val
494
548
  * @api public
495
549
  */
496
550
 
@@ -509,7 +563,7 @@ module.exports = {
509
563
  /**
510
564
  * Remove header `field`.
511
565
  *
512
- * @param {String} name
566
+ * @param {String} field
513
567
  * @api public
514
568
  */
515
569
 
@@ -0,0 +1,33 @@
1
+ const URLSearchParams = require('url').URLSearchParams
2
+
3
+ module.exports = {
4
+ stringify: (obj) => {
5
+ const searchParams = new URLSearchParams()
6
+ const addKey = (k, v, params) => {
7
+ const val = typeof v === 'string' || typeof v === 'number' ? v : ''
8
+ params.append(k, val)
9
+ }
10
+
11
+ for (const [key, value] of Object.entries(obj)) {
12
+ if (Array.isArray(value)) {
13
+ const lgth = value.length
14
+ for (let i = 0; i < lgth; i++) {
15
+ addKey(key, value[i], searchParams)
16
+ }
17
+ } else {
18
+ addKey(key, value, searchParams)
19
+ }
20
+ }
21
+ return searchParams.toString()
22
+ },
23
+
24
+ parse: (str) => {
25
+ const searchParams = new URLSearchParams(str)
26
+ const obj = {}
27
+ for (const key of searchParams.keys()) {
28
+ const values = searchParams.getAll(key)
29
+ obj[key] = values.length <= 1 ? values[0] : values
30
+ }
31
+ return obj
32
+ }
33
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koa",
3
- "version": "3.0.0-alpha.0",
3
+ "version": "3.0.0-alpha.2",
4
4
  "publishConfig": {
5
5
  "tag": "experimental"
6
6
  },
@@ -9,7 +9,8 @@
9
9
  "exports": {
10
10
  ".": {
11
11
  "require": "./lib/application.js",
12
- "import": "./dist/koa.mjs"
12
+ "import": "./dist/koa.mjs",
13
+ "default": "./dist/koa.mjs"
13
14
  },
14
15
  "./*": "./*.js",
15
16
  "./*.js": "./*.js",
@@ -17,8 +18,10 @@
17
18
  "./package.json": "./package.json"
18
19
  },
19
20
  "scripts": {
20
- "test": "jest --forceExit",
21
- "lint": "eslint --ignore-path .gitignore .",
21
+ "test": "node --test",
22
+ "test:coverage": "node --test --experimental-test-coverage",
23
+ "lint": "standard",
24
+ "lint:pretty": "standard | snazzy",
22
25
  "authors": "git log --format='%aN <%aE>' | sort -u > AUTHORS",
23
26
  "build": "gen-esm-wrapper . ./dist/koa.mjs",
24
27
  "prepare": "npm run build"
@@ -39,42 +42,33 @@
39
42
  "cache-content-type": "^1.0.0",
40
43
  "content-disposition": "~0.5.2",
41
44
  "content-type": "^1.0.4",
42
- "cookies": "~0.8.0",
45
+ "cookies": "~0.9.1",
43
46
  "debug": "^4.3.2",
44
47
  "delegates": "^1.0.0",
45
48
  "destroy": "^1.0.4",
46
- "encodeurl": "^1.0.2",
49
+ "encodeurl": "^2.0.0",
47
50
  "escape-html": "^1.0.3",
48
51
  "fresh": "~0.5.2",
49
52
  "http-assert": "^1.3.0",
50
- "http-errors": "^1.6.3",
53
+ "http-errors": "^2.0.0",
51
54
  "koa-compose": "^4.1.0",
52
55
  "on-finished": "^2.3.0",
53
- "only": "~0.0.2",
54
56
  "parseurl": "^1.3.2",
55
- "statuses": "^1.5.0",
57
+ "statuses": "^2.0.1",
56
58
  "type-is": "^1.6.16",
57
59
  "vary": "^1.1.2"
58
60
  },
59
61
  "devDependencies": {
60
- "eslint": "^7.32.0",
61
- "eslint-config-standard": "^16.0.3",
62
- "eslint-plugin-import": "^2.18.2",
63
- "eslint-plugin-node": "^11.1.0",
64
- "eslint-plugin-promise": "^5.1.0",
65
- "eslint-plugin-standard": "^5.0.0",
66
62
  "gen-esm-wrapper": "^1.0.6",
67
- "jest": "^28.1.2",
68
- "supertest": "^6.2.4"
63
+ "snazzy": "^9.0.0",
64
+ "standard": "^17.1.0",
65
+ "supertest": "^7.0.0"
69
66
  },
70
67
  "engines": {
71
- "node": ">= 12.17.0"
68
+ "node": ">= 18"
72
69
  },
73
70
  "files": [
74
71
  "dist",
75
72
  "lib"
76
- ],
77
- "jest": {
78
- "testEnvironment": "node"
79
- }
73
+ ]
80
74
  }