@c9up/ream 0.2.13 → 0.2.15
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 +1 -0
- package/dist/Application.d.ts.map +1 -1
- package/dist/Application.js +6 -1
- package/dist/Application.js.map +1 -1
- package/dist/GracefulShutdown.js +6 -5
- package/dist/GracefulShutdown.js.map +1 -1
- package/dist/Provider.d.ts +20 -2
- package/dist/Provider.d.ts.map +1 -1
- package/dist/Provider.js.map +1 -1
- package/dist/bodyparser/MultipartFile.d.ts +33 -7
- package/dist/bodyparser/MultipartFile.d.ts.map +1 -1
- package/dist/bodyparser/MultipartFile.js +61 -12
- package/dist/bodyparser/MultipartFile.js.map +1 -1
- package/dist/bodyparser/parseSize.d.ts.map +1 -1
- package/dist/bodyparser/parseSize.js +3 -2
- package/dist/bodyparser/parseSize.js.map +1 -1
- package/dist/commands/MigrateBase.d.ts.map +1 -1
- package/dist/commands/MigrateBase.js +4 -1
- package/dist/commands/MigrateBase.js.map +1 -1
- package/dist/console/Kernel.d.ts.map +1 -1
- package/dist/console/Kernel.js +7 -0
- package/dist/console/Kernel.js.map +1 -1
- package/dist/console/types.d.ts +14 -0
- package/dist/console/types.d.ts.map +1 -1
- package/dist/console/types.js.map +1 -1
- package/dist/container/Container.d.ts +19 -3
- package/dist/container/Container.d.ts.map +1 -1
- package/dist/container/Container.js +89 -31
- package/dist/container/Container.js.map +1 -1
- package/dist/container/ContainerResolver.js.map +1 -1
- package/dist/container/types.d.ts +11 -1
- package/dist/container/types.d.ts.map +1 -1
- package/dist/decorators/Service.d.ts +1 -1
- package/dist/decorators/Service.d.ts.map +1 -1
- package/dist/env/Env.d.ts.map +1 -1
- package/dist/env/Env.js +4 -2
- package/dist/env/Env.js.map +1 -1
- package/dist/env/interpolate.d.ts.map +1 -1
- package/dist/env/interpolate.js +5 -3
- package/dist/env/interpolate.js.map +1 -1
- package/dist/errors/FuzzyMatcher.d.ts.map +1 -1
- package/dist/errors/FuzzyMatcher.js +30 -13
- package/dist/errors/FuzzyMatcher.js.map +1 -1
- package/dist/errors/PipelineStageError.d.ts.map +1 -1
- package/dist/errors/PipelineStageError.js +4 -2
- package/dist/errors/PipelineStageError.js.map +1 -1
- package/dist/events/Emitter.d.ts +21 -4
- package/dist/events/Emitter.d.ts.map +1 -1
- package/dist/events/Emitter.js +6 -2
- package/dist/events/Emitter.js.map +1 -1
- package/dist/events/testing/FakeBus.d.ts +1 -1
- package/dist/events/testing/FakeBus.d.ts.map +1 -1
- package/dist/events/testing/FakeBus.js.map +1 -1
- package/dist/graphql/GraphQLEngine.d.ts +2 -2
- package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
- package/dist/graphql/GraphQLEngine.js +4 -2
- package/dist/graphql/GraphQLEngine.js.map +1 -1
- package/dist/helpers/bytes.d.ts.map +1 -1
- package/dist/helpers/bytes.js +13 -2
- package/dist/helpers/bytes.js.map +1 -1
- package/dist/helpers/string.js +3 -3
- package/dist/helpers/string.js.map +1 -1
- package/dist/http/Exception.d.ts +8 -4
- package/dist/http/Exception.d.ts.map +1 -1
- package/dist/http/Exception.js +14 -8
- package/dist/http/Exception.js.map +1 -1
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +10 -7
- package/dist/http/Request.js.map +1 -1
- package/dist/http/Response.d.ts.map +1 -1
- package/dist/http/Response.js +4 -9
- package/dist/http/Response.js.map +1 -1
- package/dist/http/SseStream.d.ts.map +1 -1
- package/dist/http/SseStream.js +10 -1
- package/dist/http/SseStream.js.map +1 -1
- package/dist/http/etag.d.ts +44 -0
- package/dist/http/etag.d.ts.map +1 -0
- package/dist/http/etag.js +65 -0
- package/dist/http/etag.js.map +1 -0
- package/dist/http/mime.d.ts +33 -0
- package/dist/http/mime.d.ts.map +1 -0
- package/dist/http/mime.js +1347 -0
- package/dist/http/mime.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/native/generated.d.ts +12 -8
- package/dist/native/generated.d.ts.map +1 -1
- package/dist/openapi/OpenApiGenerator.d.ts.map +1 -1
- package/dist/openapi/OpenApiGenerator.js +6 -4
- package/dist/openapi/OpenApiGenerator.js.map +1 -1
- package/dist/router/Router.d.ts +10 -1
- package/dist/router/Router.d.ts.map +1 -1
- package/dist/router/Router.js +22 -10
- package/dist/router/Router.js.map +1 -1
- package/dist/rpc/RpcRouter.d.ts +16 -3
- package/dist/rpc/RpcRouter.d.ts.map +1 -1
- package/dist/rpc/RpcRouter.js +34 -3
- package/dist/rpc/RpcRouter.js.map +1 -1
- package/dist/scheduler/Schedule.d.ts +6 -1
- package/dist/scheduler/Schedule.d.ts.map +1 -1
- package/dist/scheduler/Schedule.js +5 -0
- package/dist/scheduler/Schedule.js.map +1 -1
- package/dist/security/CookieSigner.d.ts.map +1 -1
- package/dist/security/CookieSigner.js +12 -5
- package/dist/security/CookieSigner.js.map +1 -1
- package/dist/security/SignedUrl.js +3 -2
- package/dist/security/SignedUrl.js.map +1 -1
- package/dist/session/drivers/DatabaseDriver.d.ts +13 -1
- package/dist/session/drivers/DatabaseDriver.d.ts.map +1 -1
- package/dist/session/drivers/DatabaseDriver.js +23 -3
- package/dist/session/drivers/DatabaseDriver.js.map +1 -1
- package/dist/session/drivers/RedisDriver.d.ts.map +1 -1
- package/dist/session/drivers/RedisDriver.js +10 -2
- package/dist/session/drivers/RedisDriver.js.map +1 -1
- package/dist/storage/StaticMiddleware.d.ts.map +1 -1
- package/dist/storage/StaticMiddleware.js +78 -59
- package/dist/storage/StaticMiddleware.js.map +1 -1
- package/dist/testing/ApiResponse.d.ts.map +1 -1
- package/dist/testing/ApiResponse.js +10 -11
- package/dist/testing/ApiResponse.js.map +1 -1
- package/dist/testing/TestClient.js +6 -6
- package/dist/testing/TestClient.js.map +1 -1
- package/dist/utils/Macroable.d.ts +6 -2
- package/dist/utils/Macroable.d.ts.map +1 -1
- package/dist/utils/Macroable.js +4 -0
- package/dist/utils/Macroable.js.map +1 -1
- package/dist/utils/objectPath.d.ts.map +1 -1
- package/dist/utils/objectPath.js +14 -6
- package/dist/utils/objectPath.js.map +1 -1
- package/events.darwin-arm64.node +0 -0
- package/events.darwin-x64.node +0 -0
- package/events.linux-arm64-gnu.node +0 -0
- package/events.linux-x64-gnu.node +0 -0
- package/events.win32-x64-msvc.node +0 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +5 -7
- package/scheduler.darwin-arm64.node +0 -0
- package/scheduler.darwin-x64.node +0 -0
- package/scheduler.linux-arm64-gnu.node +0 -0
- package/scheduler.linux-x64-gnu.node +0 -0
- package/scheduler.win32-x64-msvc.node +0 -0
- package/scripts/build-napi-types.mjs +4 -3
- package/scripts/generate-mime.mjs +119 -0
- package/scripts/generate-napi-types.mjs +28 -12
- package/src/Application.ts +6 -1
- package/src/GracefulShutdown.ts +5 -5
- package/src/Provider.ts +21 -2
- package/src/bodyparser/MultipartFile.ts +94 -16
- package/src/bodyparser/parseSize.ts +3 -2
- package/src/commands/MigrateBase.ts +4 -1
- package/src/console/BaseCommand.ts +1 -1
- package/src/console/Kernel.ts +8 -0
- package/src/console/types.ts +15 -0
- package/src/container/Container.ts +120 -57
- package/src/container/ContainerResolver.ts +1 -1
- package/src/container/types.ts +11 -1
- package/src/decorators/Service.ts +1 -1
- package/src/env/Env.ts +4 -2
- package/src/env/interpolate.ts +5 -3
- package/src/errors/FuzzyMatcher.ts +34 -13
- package/src/errors/PipelineStageError.ts +4 -2
- package/src/events/Emitter.ts +34 -8
- package/src/events/testing/FakeBus.ts +12 -5
- package/src/graphql/GraphQLEngine.ts +11 -7
- package/src/helpers/bytes.ts +13 -2
- package/src/helpers/string.ts +3 -3
- package/src/http/Exception.ts +14 -8
- package/src/http/Request.ts +8 -5
- package/src/http/Response.ts +4 -8
- package/src/http/SseStream.ts +16 -7
- package/src/http/etag.ts +68 -0
- package/src/http/mime.ts +1347 -0
- package/src/index.ts +3 -3
- package/src/native/generated.ts +12 -8
- package/src/openapi/OpenApiGenerator.ts +6 -4
- package/src/router/Router.ts +31 -11
- package/src/rpc/RpcRouter.ts +38 -8
- package/src/scheduler/Schedule.ts +8 -1
- package/src/security/CookieSigner.ts +15 -5
- package/src/security/SignedUrl.ts +3 -2
- package/src/session/drivers/DatabaseDriver.ts +36 -8
- package/src/session/drivers/RedisDriver.ts +13 -5
- package/src/storage/StaticMiddleware.ts +81 -66
- package/src/testing/ApiResponse.ts +9 -10
- package/src/testing/TestClient.ts +6 -6
- package/src/utils/Macroable.ts +6 -2
- package/src/utils/objectPath.ts +12 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c9up/ream",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.15",
|
|
4
4
|
"description": "Ream — Rust-powered Node.js application framework",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -212,9 +212,7 @@
|
|
|
212
212
|
"tsconfig.client.json"
|
|
213
213
|
],
|
|
214
214
|
"dependencies": {
|
|
215
|
-
"etag": "^1.8.1",
|
|
216
215
|
"file-type": "^22.0.1",
|
|
217
|
-
"mime-types": "^3.0.1",
|
|
218
216
|
"reflect-metadata": "^0.2"
|
|
219
217
|
},
|
|
220
218
|
"peerDependencies": {
|
|
@@ -230,11 +228,11 @@
|
|
|
230
228
|
}
|
|
231
229
|
},
|
|
232
230
|
"devDependencies": {
|
|
233
|
-
"@biomejs/biome": "
|
|
231
|
+
"@biomejs/biome": "2.5.12",
|
|
234
232
|
"@c9up/comet": "^0.1.0",
|
|
235
|
-
"@types/etag": "^1.8.4",
|
|
236
|
-
"@types/mime-types": "^3.0.1",
|
|
237
233
|
"@types/node": "^22.19.15",
|
|
234
|
+
"mime-db": "^1.54.0",
|
|
235
|
+
"mime-types": "^3.0.2",
|
|
238
236
|
"typescript": "^6.0.2",
|
|
239
237
|
"vitest": "^4.1.6"
|
|
240
238
|
},
|
|
@@ -259,6 +257,6 @@
|
|
|
259
257
|
"lint": "biome check src/",
|
|
260
258
|
"test:coverage": "vitest run --coverage",
|
|
261
259
|
"typecheck": "tsc --noEmit",
|
|
262
|
-
"typecheck:
|
|
260
|
+
"typecheck:tests": "tsc --noEmit -p tsconfig.tests.json"
|
|
263
261
|
}
|
|
264
262
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* cargo a `/tmp/...` path the native proc-macro cannot write to, so the
|
|
8
8
|
* type-def file comes back empty and the build fails for no visible reason.
|
|
9
9
|
*
|
|
10
|
-
* One crate at a time on purpose: napi-derive
|
|
10
|
+
* One crate at a time on purpose: with napi-derive 2 the writes all went to a
|
|
11
|
+
* single `TYPE_DEF_TMP_PATH` and a parallel build interleaved them
|
|
11
12
|
* while cargo compiles, and a parallel build interleaves the writes —
|
|
12
13
|
* definitions go missing, silently, and the generated file comes out short.
|
|
13
14
|
*/
|
|
@@ -28,11 +29,11 @@ const scratch = mkdtempSync(join(tmpdir(), 'napi-types-'))
|
|
|
28
29
|
try {
|
|
29
30
|
const lines = []
|
|
30
31
|
for (const crate of CRATES) {
|
|
31
|
-
const perCrate = join(scratch,
|
|
32
|
+
const perCrate = join(scratch, crate)
|
|
32
33
|
writeFileSync(perCrate, '')
|
|
33
34
|
execFileSync('cargo', ['build', '-p', crate], {
|
|
34
35
|
cwd: packageRoot,
|
|
35
|
-
env: { ...process.env,
|
|
36
|
+
env: { ...process.env, NAPI_TYPE_DEF_TMP_FOLDER: scratch },
|
|
36
37
|
stdio: ['ignore', 'ignore', 'inherit'],
|
|
37
38
|
})
|
|
38
39
|
const emitted = readFileSync(perCrate, 'utf8').split('\n').filter(Boolean)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regenerate `src/http/mime.ts` from mime-db.
|
|
3
|
+
*
|
|
4
|
+
* The table is data, not logic, and hand-maintaining it is how `.yaml` and
|
|
5
|
+
* `.sql` went missing. `mime-types` reads the same two fields out of mime-db —
|
|
6
|
+
* the resolved extension map and the per-type `charset` — so generating from
|
|
7
|
+
* there keeps `contentType()` answering exactly what the package answered.
|
|
8
|
+
*
|
|
9
|
+
* mime-db and mime-types are dev-only: they are read HERE, at generation time,
|
|
10
|
+
* and never imported by the framework.
|
|
11
|
+
*
|
|
12
|
+
* node scripts/generate-mime.mjs
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createRequire } from 'node:module'
|
|
16
|
+
import { writeFile } from 'node:fs/promises'
|
|
17
|
+
import { fileURLToPath } from 'node:url'
|
|
18
|
+
import { dirname, join } from 'node:path'
|
|
19
|
+
|
|
20
|
+
const require = createRequire(import.meta.url)
|
|
21
|
+
const mimeTypes = require('mime-types')
|
|
22
|
+
const db = require('mime-db')
|
|
23
|
+
const dbVersion = require('mime-db/package.json').version
|
|
24
|
+
|
|
25
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
26
|
+
const target = join(here, '..', 'src', 'http', 'mime.ts')
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Emitted in the package's own style — single quotes, no semicolons — so a
|
|
30
|
+
* regenerated file is already what `biome check` wants and the diff after a
|
|
31
|
+
* regeneration is the data that changed, nothing else.
|
|
32
|
+
*/
|
|
33
|
+
const quote = (value) => `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
|
|
34
|
+
/** An identifier when it is one, a quoted key otherwise (`7z`, `3gp`…). */
|
|
35
|
+
const key = (ext) => (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(ext) ? ext : quote(ext))
|
|
36
|
+
|
|
37
|
+
const extensions = Object.keys(mimeTypes.types).sort()
|
|
38
|
+
const rows = extensions.map((e) => ` ${key(e)}: ${quote(mimeTypes.types[e])},`).join('\n')
|
|
39
|
+
|
|
40
|
+
const charsets = Object.entries(db)
|
|
41
|
+
.filter(([, entry]) => entry.charset)
|
|
42
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
43
|
+
.map(([type, entry]) => ` ${quote(type)}: ${quote(entry.charset.toLowerCase())},`)
|
|
44
|
+
.join('\n')
|
|
45
|
+
|
|
46
|
+
await writeFile(target, render({ rows, charsets, dbVersion, count: extensions.length }))
|
|
47
|
+
process.stdout.write(`mime.ts: ${extensions.length} extensions from mime-db ${dbVersion}\n`)
|
|
48
|
+
|
|
49
|
+
function render({ rows, charsets, dbVersion, count }) {
|
|
50
|
+
return `/**
|
|
51
|
+
* Content types for a response, resolved here rather than pulled in.
|
|
52
|
+
*
|
|
53
|
+
* \`response.type("txt")\` has to become \`text/plain; charset=utf-8\`, which is a
|
|
54
|
+
* lookup table and one charset rule — the same job \`@c9up/archive\` already
|
|
55
|
+
* does in its own table, so the framework was carrying a dependency for work
|
|
56
|
+
* the ecosystem already owned.
|
|
57
|
+
*
|
|
58
|
+
* A hand-written table is what made that a regression rather than a saving:
|
|
59
|
+
* it knew the extensions someone thought of, so \`.yaml\`, \`.sql\`, \`.opus\` and
|
|
60
|
+
* \`.xhtml\` fell through and the caller's own string went out as the content
|
|
61
|
+
* type. The tables below are GENERATED from mime-db instead, which is where
|
|
62
|
+
* \`mime-types\` reads them too, so the resolution is the same one and stays
|
|
63
|
+
* the same one.
|
|
64
|
+
*
|
|
65
|
+
* Regenerate with:
|
|
66
|
+
*
|
|
67
|
+
* node scripts/generate-mime.mjs
|
|
68
|
+
*
|
|
69
|
+
* Do not edit {@link TYPES} or {@link CHARSETS} by hand.
|
|
70
|
+
*
|
|
71
|
+
* @generated from mime-db ${dbVersion} (${count} extensions)
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/** Extension → type. No leading dot; lowercase. */
|
|
75
|
+
const TYPES: Record<string, string> = {
|
|
76
|
+
${rows}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Types that declare a charset of their own, lowercased.
|
|
81
|
+
*
|
|
82
|
+
* Everything else under \`text/\` defaults to utf-8; everything else has none.
|
|
83
|
+
* That is mime-db's \`charset\` field plus \`mime-types\`' \`text/*\` fallback.
|
|
84
|
+
*/
|
|
85
|
+
const CHARSETS: Record<string, string> = {
|
|
86
|
+
${charsets}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The charset for a type, or \`undefined\` when it carries none. */
|
|
90
|
+
function charsetFor(type: string): string | undefined {
|
|
91
|
+
const base = type.split(';')[0].trim().toLowerCase()
|
|
92
|
+
const declared = CHARSETS[base]
|
|
93
|
+
if (declared !== undefined) return declared
|
|
94
|
+
return base.startsWith('text/') ? 'utf-8' : undefined
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The type for a bare extension, or \`undefined\` when it is not one we know. */
|
|
98
|
+
export function lookupType(extension: string): string | undefined {
|
|
99
|
+
const ext = extension.replace(/^.*\\./, '').toLowerCase()
|
|
100
|
+
return TYPES[ext]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A full \`Content-Type\` for an extension or a type (\`mime-types.contentType\`).
|
|
105
|
+
*
|
|
106
|
+
* Returns \`false\` for something it cannot resolve, as the package did, so the
|
|
107
|
+
* caller keeps its existing fallback rather than writing \`content-type: false\`.
|
|
108
|
+
*/
|
|
109
|
+
export function contentType(input: string): string | false {
|
|
110
|
+
if (input.length === 0) return false
|
|
111
|
+
// Already a type — possibly with parameters the caller set itself.
|
|
112
|
+
const type = input.includes('/') ? input : lookupType(input)
|
|
113
|
+
if (type === undefined) return false
|
|
114
|
+
if (type.includes('charset=')) return type
|
|
115
|
+
const charset = charsetFor(type)
|
|
116
|
+
return charset === undefined ? type : \`\${type}; charset=\${charset}\`
|
|
117
|
+
}
|
|
118
|
+
`
|
|
119
|
+
}
|
|
@@ -29,27 +29,40 @@ import { readFileSync, writeFileSync } from 'node:fs'
|
|
|
29
29
|
* entry must match, or the script fails: a stale refinement is how a hand
|
|
30
30
|
* annotation quietly stops describing the Rust.
|
|
31
31
|
*/
|
|
32
|
+
/**
|
|
33
|
+
* napi-derive 3 emits `callback: (arg: unknown) => unknown` where 2 emitted
|
|
34
|
+
* `(...args: any[]) => any` — better, since it no longer reaches for `any`,
|
|
35
|
+
* but a different string to match on. These refinements exist because the
|
|
36
|
+
* generated shape says nothing about what the callback actually receives.
|
|
37
|
+
*/
|
|
32
38
|
const CALLBACK_REFINEMENTS = {
|
|
33
39
|
'EventBus.subscribe': {
|
|
34
|
-
from: 'callback: (
|
|
40
|
+
from: 'callback: (arg: unknown) => unknown',
|
|
35
41
|
to: 'callback: (eventJson: string) => void',
|
|
36
42
|
},
|
|
37
43
|
'EventBus.onRequest': {
|
|
38
|
-
from: 'callback: (
|
|
44
|
+
from: 'callback: (arg: unknown) => unknown',
|
|
39
45
|
to: 'callback: (eventJson: string, reply: (response: string) => void) => void',
|
|
40
46
|
},
|
|
41
47
|
'HyperServer.onRequest': {
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
// Only the argument is named. The `to` used to say
|
|
49
|
+
// `(request: string) => Promise<string> | string`, which described the
|
|
50
|
+
// boundary as it was BEFORE it stopped serialising: the Rust hands the
|
|
51
|
+
// handler a JsObject and awaits a `NapiResponse`, and napi's own output
|
|
52
|
+
// already said so. The refinement was overwriting a correct shape with a
|
|
53
|
+
// stale one, and nothing noticed because every caller of `onRequest` was
|
|
54
|
+
// outside the typecheck.
|
|
55
|
+
from: 'callback: (arg: unknown) => Promise<NapiResponse>',
|
|
56
|
+
to: 'callback: (request: Record<string, unknown>) => Promise<NapiResponse>',
|
|
44
57
|
},
|
|
45
58
|
'HyperServer.onStreamDisconnect': {
|
|
46
|
-
from: 'callback: (
|
|
59
|
+
from: 'callback: (arg: unknown) => unknown',
|
|
47
60
|
to: 'callback: (streamId: string) => void',
|
|
48
61
|
},
|
|
49
62
|
// The Rust hands over a serialised JsTaskPayload — `{ task_name,
|
|
50
63
|
// scheduled_for_ms }` renamed to camelCase by serde.
|
|
51
64
|
'RustScheduler.register': {
|
|
52
|
-
from: 'callback: (
|
|
65
|
+
from: 'callback: (arg: unknown) => unknown',
|
|
53
66
|
to: 'callback: (invocation: { taskName: string; scheduledForMs: number }) => void',
|
|
54
67
|
},
|
|
55
68
|
}
|
|
@@ -150,12 +163,15 @@ for (const fn of fns) {
|
|
|
150
163
|
// napi-derive emits the whole declaration for a function, unlike a struct
|
|
151
164
|
// where `def` holds only the members.
|
|
152
165
|
const declaration = fn.def.trim()
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
166
|
+
// napi-derive 3 emits a bare `function name(...)` where 2 emitted the
|
|
167
|
+
// signature after the name; concatenating onto the former produced
|
|
168
|
+
// `function xfunction x(...)`.
|
|
169
|
+
const rendered = declaration.startsWith('export declare function')
|
|
170
|
+
? declaration
|
|
171
|
+
: declaration.startsWith('function ')
|
|
172
|
+
? `export declare ${declaration}`
|
|
173
|
+
: `export declare function ${fn.name}${declaration}`
|
|
174
|
+
out.push(`${rendered};`, '')
|
|
159
175
|
}
|
|
160
176
|
|
|
161
177
|
const stale = Object.keys(CALLBACK_REFINEMENTS).filter((k) => !used.has(k))
|
package/src/Application.ts
CHANGED
|
@@ -181,7 +181,12 @@ export class Application implements AppContext {
|
|
|
181
181
|
|
|
182
182
|
/** Resolve inside one of the conventional directories. */
|
|
183
183
|
#inDirectory(key: keyof DirectoriesNode, paths: string[]): string {
|
|
184
|
-
|
|
184
|
+
// `DirectoriesNode` is an index signature, so a key nobody registered
|
|
185
|
+
// reads as undefined rather than failing to compile. Falling back to the
|
|
186
|
+
// key itself keeps the convention: `configPath()` under a node with no
|
|
187
|
+
// `config` entry resolves to `<root>/config`, which is where it would
|
|
188
|
+
// have been anyway.
|
|
189
|
+
return this.makePath(this.#directories[key] ?? String(key), ...paths)
|
|
185
190
|
}
|
|
186
191
|
|
|
187
192
|
/** Absolute path inside the `config/` directory. */
|
package/src/GracefulShutdown.ts
CHANGED
|
@@ -65,12 +65,12 @@ export function installGracefulShutdown(options: ShutdownOptions): ShutdownHandl
|
|
|
65
65
|
// Second signal while draining — escalate immediately.
|
|
66
66
|
logger.error('Forced exit: second signal received during shutdown')
|
|
67
67
|
process.exit(1)
|
|
68
|
-
|
|
68
|
+
} else {
|
|
69
|
+
shutdown().catch((err) => {
|
|
70
|
+
logger.error(`Fatal: ${err}`)
|
|
71
|
+
process.exit(1)
|
|
72
|
+
})
|
|
69
73
|
}
|
|
70
|
-
shutdown().catch((err) => {
|
|
71
|
-
logger.error(`Fatal: ${err}`)
|
|
72
|
-
process.exit(1)
|
|
73
|
-
})
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
const onSigterm = handleSignal
|
package/src/Provider.ts
CHANGED
|
@@ -7,12 +7,31 @@
|
|
|
7
7
|
* register() → boot() → start() → ready() → ... → shutdown()
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type { ConfigStore } from './ConfigLoader.js'
|
|
11
10
|
import type { Container } from './container/Container.js'
|
|
12
11
|
|
|
12
|
+
/**
|
|
13
|
+
* The config a provider reads, as a CONTRACT rather than as the class.
|
|
14
|
+
*
|
|
15
|
+
* `ConfigStore` satisfies it, so nothing that holds a real one changes. But
|
|
16
|
+
* typed as the class, `AppContext` demanded a private `#tree` no one outside
|
|
17
|
+
* `ConfigLoader` can produce — so a test could not substitute a config, and an
|
|
18
|
+
* independently-published package could not describe an `AppContext` at all
|
|
19
|
+
* without importing `@c9up/ream`, which is the coupling the comment below this
|
|
20
|
+
* one exists to avoid. bay, station and blackhole each re-declared their own
|
|
21
|
+
* for exactly that reason.
|
|
22
|
+
*
|
|
23
|
+
* `get` and `set` are what providers actually use; a `ConfigStore`'s other
|
|
24
|
+
* methods stay reachable on the concrete type.
|
|
25
|
+
*/
|
|
26
|
+
export interface ConfigReader {
|
|
27
|
+
get<T = unknown>(key: string): T | undefined
|
|
28
|
+
get<T = unknown>(key: string, defaultValue: T): T
|
|
29
|
+
set(key: string, value: unknown): void
|
|
30
|
+
}
|
|
31
|
+
|
|
13
32
|
export interface AppContext {
|
|
14
33
|
container: Container
|
|
15
|
-
config:
|
|
34
|
+
config: ConfigReader
|
|
16
35
|
}
|
|
17
36
|
|
|
18
37
|
/**
|
|
@@ -1,3 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The top-level mime types IANA registers.
|
|
3
|
+
*
|
|
4
|
+
* Closed on purpose. Typing {@link MultipartFile.type} as a bare `string` let
|
|
5
|
+
* `file.type === 'image/png'` compile and quietly never match — a mime filter
|
|
6
|
+
* that passes everything, which is the shape a comment used to warn about and
|
|
7
|
+
* a compiler can simply refuse. Against this union that comparison is an
|
|
8
|
+
* error, and {@link MultipartFile.mime} is what a full-mime check wants.
|
|
9
|
+
*
|
|
10
|
+
* A top-level type outside this list is malformed: an unregistered format
|
|
11
|
+
* belongs under one of these trees, `application/x-…` rather than `x-foo/bar`.
|
|
12
|
+
*
|
|
13
|
+
* Additive, not a replacement: {@link MultipartFile.type} keeps upstream's
|
|
14
|
+
* `string` and its raw segment, and {@link MultipartFile.registeredType}
|
|
15
|
+
* narrows it here for callers who want `=== 'image/png'` refused at compile
|
|
16
|
+
* time. Nothing a valid AdonisJS call site does stops compiling, and no type
|
|
17
|
+
* outside this set is swallowed — closing `type` itself would have done both.
|
|
18
|
+
*/
|
|
19
|
+
export type MimeType =
|
|
20
|
+
| 'application'
|
|
21
|
+
| 'audio'
|
|
22
|
+
| 'example'
|
|
23
|
+
| 'font'
|
|
24
|
+
| 'image'
|
|
25
|
+
| 'message'
|
|
26
|
+
| 'model'
|
|
27
|
+
| 'multipart'
|
|
28
|
+
| 'text'
|
|
29
|
+
| 'video'
|
|
30
|
+
|
|
31
|
+
const MIME_TYPES = new Set<string>([
|
|
32
|
+
'application',
|
|
33
|
+
'audio',
|
|
34
|
+
'example',
|
|
35
|
+
'font',
|
|
36
|
+
'image',
|
|
37
|
+
'message',
|
|
38
|
+
'model',
|
|
39
|
+
'multipart',
|
|
40
|
+
'text',
|
|
41
|
+
'video',
|
|
42
|
+
])
|
|
43
|
+
|
|
44
|
+
/** Whether a parsed top-level type is one IANA registers. */
|
|
45
|
+
function isMimeType(value: string): value is MimeType {
|
|
46
|
+
return MIME_TYPES.has(value)
|
|
47
|
+
}
|
|
48
|
+
|
|
1
49
|
/**
|
|
2
50
|
* MultipartFile — represents an uploaded file.
|
|
3
51
|
*
|
|
@@ -28,14 +76,32 @@ export class MultipartFile {
|
|
|
28
76
|
* `image/png` would otherwise sail through a mime allowlist. A renamed file
|
|
29
77
|
* reports what it actually is.
|
|
30
78
|
*
|
|
31
|
-
* BREAKING as of 0.2.0: it used to hold the whole `image/png`.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
79
|
+
* BREAKING as of 0.2.0: it used to hold the whole `image/png`. A comparison
|
|
80
|
+
* against a full mime — `file.type === 'image/png'` — compiles and never
|
|
81
|
+
* matches, which is what {@link mime} exists for, and what
|
|
82
|
+
* {@link registeredType} refuses at compile time for callers who want that.
|
|
83
|
+
*
|
|
84
|
+
* `string` and the raw segment, as upstream: a type outside the registered
|
|
85
|
+
* set is still reported rather than swallowed.
|
|
34
86
|
*/
|
|
35
87
|
get type(): string | undefined {
|
|
36
88
|
return this.#mimeParts()[0]
|
|
37
89
|
}
|
|
38
90
|
|
|
91
|
+
/**
|
|
92
|
+
* The same primary type, narrowed to what IANA registers.
|
|
93
|
+
*
|
|
94
|
+
* Additive, and the point of it: against a closed union
|
|
95
|
+
* `file.registeredType === 'image/png'` is a compile error instead of a
|
|
96
|
+
* comparison that always fails. `undefined` where {@link type} holds
|
|
97
|
+
* something outside the set — a malformed or unregistered tree — so the two
|
|
98
|
+
* disagree exactly there and nowhere else.
|
|
99
|
+
*/
|
|
100
|
+
get registeredType(): MimeType | undefined {
|
|
101
|
+
const type = this.#mimeParts()[0]
|
|
102
|
+
return type !== undefined && isMimeType(type) ? type : undefined
|
|
103
|
+
}
|
|
104
|
+
|
|
39
105
|
/** The mime SUBTYPE — `png` for `image/png`. Same source as {@link type}. */
|
|
40
106
|
get subtype(): string | undefined {
|
|
41
107
|
return this.#mimeParts()[1]
|
|
@@ -59,7 +125,10 @@ export class MultipartFile {
|
|
|
59
125
|
#mimeParts(): [string | undefined, string | undefined] {
|
|
60
126
|
const source = this.#detected?.mime ?? this.#headerMime
|
|
61
127
|
if (!source) return [undefined, undefined]
|
|
62
|
-
|
|
128
|
+
// `split` always yields a first element; naming it is how that stops
|
|
129
|
+
// being something the compiler has to be told.
|
|
130
|
+
const [head = ''] = source.split(';')
|
|
131
|
+
const [type, subtype] = head.trim().split('/')
|
|
63
132
|
return [type || undefined, subtype || undefined]
|
|
64
133
|
}
|
|
65
134
|
|
|
@@ -297,7 +366,7 @@ export class MultipartFile {
|
|
|
297
366
|
* passing `file.clientName` straight through.
|
|
298
367
|
*/
|
|
299
368
|
async move(location: string, options?: { name?: string; overwrite?: boolean }): Promise<void> {
|
|
300
|
-
const { mkdir, writeFile
|
|
369
|
+
const { mkdir, writeFile } = await import('node:fs/promises')
|
|
301
370
|
const { join } = await import('node:path')
|
|
302
371
|
|
|
303
372
|
const name = options?.name ?? `${randomBytes(16).toString('hex')}.${this.extname || 'unknown'}`
|
|
@@ -308,20 +377,28 @@ export class MultipartFile {
|
|
|
308
377
|
}
|
|
309
378
|
|
|
310
379
|
const filePath = join(location, name)
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
380
|
+
await mkdir(location, { recursive: true })
|
|
381
|
+
|
|
382
|
+
// NAMED DEVIATION — the refusal is ATOMIC where upstream's is not.
|
|
383
|
+
//
|
|
384
|
+
// Upstream checks `pathExists` and then renames, so two uploads landing on
|
|
385
|
+
// the same name both see nothing and both succeed: the second silently
|
|
386
|
+
// replaces the first, which is the one thing `overwrite: false` exists to
|
|
387
|
+
// prevent. `wx` is O_EXCL — the kernel decides, and exactly one of the two
|
|
388
|
+
// writes wins.
|
|
389
|
+
//
|
|
390
|
+
// The message is upstream's, word for word: a caller matching on it keeps
|
|
391
|
+
// working, and only the race is gone.
|
|
392
|
+
try {
|
|
393
|
+
await writeFile(filePath, this.content, options?.overwrite === false ? { flag: 'wx' } : {})
|
|
394
|
+
} catch (error) {
|
|
395
|
+
if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
|
|
317
396
|
throw new Error(
|
|
318
397
|
`"${name}" already exists at "${location}". Set "overwrite = true" to overwrite it`,
|
|
319
398
|
)
|
|
320
399
|
}
|
|
400
|
+
throw error
|
|
321
401
|
}
|
|
322
|
-
|
|
323
|
-
await mkdir(location, { recursive: true })
|
|
324
|
-
await writeFile(filePath, this.content)
|
|
325
402
|
this.markAsMoved(name, filePath)
|
|
326
403
|
}
|
|
327
404
|
|
|
@@ -358,8 +435,9 @@ function isSafeFilename(name: string): boolean {
|
|
|
358
435
|
function parseFileSize(size: string): number {
|
|
359
436
|
const match = size.match(/^(\d+)(kb|mb|gb)?$/i)
|
|
360
437
|
if (!match) return 1024 * 1024
|
|
361
|
-
const
|
|
362
|
-
|
|
438
|
+
const [, digits, unit] = match
|
|
439
|
+
const num = parseInt(digits ?? '0', 10)
|
|
440
|
+
switch (unit?.toLowerCase()) {
|
|
363
441
|
case 'kb':
|
|
364
442
|
return num * 1024
|
|
365
443
|
case 'mb':
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
export function parseSize(size: string): number {
|
|
7
7
|
const match = size.match(/^(\d+)(kb|mb|gb)?$/i)
|
|
8
8
|
if (!match) return 1024 * 1024 // default 1mb
|
|
9
|
-
const
|
|
10
|
-
|
|
9
|
+
const [, digits, unit] = match
|
|
10
|
+
const num = parseInt(digits ?? '0', 10)
|
|
11
|
+
switch (unit?.toLowerCase()) {
|
|
11
12
|
case 'kb':
|
|
12
13
|
return num * 1024
|
|
13
14
|
case 'mb':
|
|
@@ -19,7 +19,10 @@ import type { MigrationRegistry } from '../migrations/MigrationRegistry.js'
|
|
|
19
19
|
import type { RegisteredMigrationSource } from '../migrations/types.js'
|
|
20
20
|
|
|
21
21
|
export abstract class MigrateBase extends BaseCommand {
|
|
22
|
-
|
|
22
|
+
// `drivesMigrations` because these three ARE the migration run: without it the
|
|
23
|
+
// boot applies everything first, so `migrate` says "nothing to migrate" after
|
|
24
|
+
// the work is done, and `migrate:status` mutates the schema it reports on.
|
|
25
|
+
static override options: CommandOptions = { startApp: true, drivesMigrations: true }
|
|
23
26
|
|
|
24
27
|
@flags.string({
|
|
25
28
|
description: 'Only this migration source, by the name its package registered',
|
|
@@ -117,7 +117,7 @@ export abstract class BaseCommand {
|
|
|
117
117
|
error?: unknown
|
|
118
118
|
|
|
119
119
|
/** Assigned by the kernel — see {@link terminate}. */
|
|
120
|
-
protected
|
|
120
|
+
declare protected onTerminate?: () => void | Promise<void>
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
123
|
* Lifecycle hooks, run in this order around `run()`: `prepare`, `interact`,
|
package/src/console/Kernel.ts
CHANGED
|
@@ -999,6 +999,14 @@ export class Kernel {
|
|
|
999
999
|
)
|
|
1000
1000
|
}
|
|
1001
1001
|
|
|
1002
|
+
// Before the boot, not after: a data package that migrates on boot for the
|
|
1003
|
+
// convenience of `dev` would otherwise apply everything a moment before the
|
|
1004
|
+
// command that exists to apply it runs — leaving `migrate` with nothing to
|
|
1005
|
+
// report and `migrate:status` having changed what it was asked to look at.
|
|
1006
|
+
if (Command.options?.drivesMigrations === true) {
|
|
1007
|
+
process.env.REAM_SKIP_BOOT_MIGRATE = '1'
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1002
1010
|
return this.#startApp()
|
|
1003
1011
|
}
|
|
1004
1012
|
|
package/src/console/types.ts
CHANGED
|
@@ -37,6 +37,21 @@ export interface CommandOptions {
|
|
|
37
37
|
* default so a typo (`--warm` for `--warn`) is reported, not swallowed.
|
|
38
38
|
*/
|
|
39
39
|
allowUnknownFlags?: boolean
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* This command drives migrations itself, so booting must not run them.
|
|
43
|
+
*
|
|
44
|
+
* A data package may migrate on boot for the convenience of `dev` — atlas
|
|
45
|
+
* does, outside production. For the commands that exist to migrate, that
|
|
46
|
+
* convenience is the bug: the boot applies everything, and then `migrate`
|
|
47
|
+
* reports "nothing to migrate" while `migrate:status`, asked only to look,
|
|
48
|
+
* has changed the schema it is reporting on.
|
|
49
|
+
*
|
|
50
|
+
* Declaring this sets `REAM_SKIP_BOOT_MIGRATE=1` before the application
|
|
51
|
+
* starts, which is the flag a data package watches for. Any package's own
|
|
52
|
+
* migration command opts in the same way; the framework names none of them.
|
|
53
|
+
*/
|
|
54
|
+
drivesMigrations?: boolean
|
|
40
55
|
}
|
|
41
56
|
|
|
42
57
|
/** Metadata recorded by an `@args.*` decorator. */
|