From 416601f45e7e51387480fe645b492024e6eb5176 Mon Sep 17 00:00:00 2001 From: kilisamemarisaaa <1798456934@qq.com> Date: Fri, 28 Aug 2026 16:36:57 +0800 Subject: [PATCH 1/8] fix(test): await glob in suite-runner so tests actually execute glob v13 removed the callback API: glob(pattern, cb) now returns a promise and the callback never fires, so suite-runner silently ran zero tests and CI stayed green since the glob 11 -> 13 bump (#852). Switch to async/await and fail loudly on glob errors. Co-Authored-By: EvoX --- suite-runner.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/suite-runner.js b/suite-runner.js index 8a1408c5..71f6f410 100644 --- a/suite-runner.js +++ b/suite-runner.js @@ -3,15 +3,12 @@ const { spec } = require('node:test/reporters') const path = require('node:path') const { glob } = require('glob') -const pattern = process.argv[process.argv.length - 1] +async function main () { + const pattern = process.argv[process.argv.length - 1] -console.info(`Running tests matching ${pattern}`) -const timeout = 10 * 60 * 1000 // 10 minutes -glob(pattern, (err, matches) => { - if (err) { - console.error(err) - process.exit(1) - } + console.info(`Running tests matching ${pattern}`) + const timeout = 10 * 60 * 1000 // 10 minutes + const matches = await glob(pattern) const resolved = matches.map(file => path.resolve(file)) const testRs = run({ files: resolved, timeout }) .on('test:fail', () => { @@ -19,4 +16,9 @@ glob(pattern, (err, matches) => { }) .compose(spec) testRs.pipe(process.stdout) +} + +main().catch(err => { + console.error(err) + process.exit(1) }) From 77373374150c76b0c939783823423b30e1634d6f Mon Sep 17 00:00:00 2001 From: kilisamemarisaaa <1798456934@qq.com> Date: Fri, 28 Aug 2026 16:36:58 +0800 Subject: [PATCH 2/8] fix: restore CJS interop for ESM-only dependencies Dependabot bumps moved several runtime deps to ESM-only releases that were never exercised because CI ran zero tests: - pkg-up@5 (ESM named exports): require() returned a namespace, breaking `fastify start` with 'pkgUp is not a function' - is-docker@4 (ESM default export): broke `fastify start` the same way - chalk@6 (ESM default export): broke generate/generate-plugin and watch Use named imports / .default interop at each require site. Co-Authored-By: EvoX --- generate-plugin.js | 2 +- generate.js | 2 +- lib/watch/fork.js | 2 +- lib/watch/index.js | 2 +- lib/watch/utils.js | 2 +- log.js | 2 +- start.js | 2 +- util.js | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/generate-plugin.js b/generate-plugin.js index 8a8c88e4..54c7ea57 100755 --- a/generate-plugin.js +++ b/generate-plugin.js @@ -6,7 +6,7 @@ const { } = require('node:fs').promises const { existsSync } = require('node:fs') const path = require('node:path') -const chalk = require('chalk') +const chalk = require('chalk').default const generify = require('generify') const parseArgs = require('./lib/parse-args') const cliPkg = require('./package') diff --git a/generate.js b/generate.js index 407ccef8..ddc5b6f2 100755 --- a/generate.js +++ b/generate.js @@ -6,7 +6,7 @@ const { existsSync } = require('node:fs') const path = require('node:path') -const chalk = require('chalk') +const chalk = require('chalk').default const generify = require('generify') const parseArgs = require('./lib/parse-args') const cliPkg = require('./package') diff --git a/lib/watch/fork.js b/lib/watch/fork.js index d4ee7922..9f800430 100644 --- a/lib/watch/fork.js +++ b/lib/watch/fork.js @@ -1,6 +1,6 @@ 'use strict' -const chalk = require('chalk') +const chalk = require('chalk').default const { stop, runFastify } = require('../../start') const { diff --git a/lib/watch/index.js b/lib/watch/index.js index b4f53859..d8c736b1 100644 --- a/lib/watch/index.js +++ b/lib/watch/index.js @@ -2,7 +2,7 @@ const path = require('node:path') const cp = require('node:child_process') -const chalk = require('chalk') +const chalk = require('chalk').default const { arrayToRegExp, logWatchVerbose } = require('./utils') const { GRACEFUL_SHUT } = require('./constants.js') diff --git a/lib/watch/utils.js b/lib/watch/utils.js index dae37507..6f49c3c6 100644 --- a/lib/watch/utils.js +++ b/lib/watch/utils.js @@ -1,6 +1,6 @@ 'use strict' -const chalk = require('chalk') +const chalk = require('chalk').default const path = require('node:path') const arrayToRegExp = (arr) => { diff --git a/log.js b/log.js index 84987b9a..ffeeb3c1 100644 --- a/log.js +++ b/log.js @@ -1,6 +1,6 @@ 'use strict' -const chalk = require('chalk') +const chalk = require('chalk').default const levels = { debug: 0, diff --git a/start.js b/start.js index ef0d614c..ed61cd34 100755 --- a/start.js +++ b/start.js @@ -4,7 +4,7 @@ const { loadEnvQuitely } = require('./env-loader') loadEnvQuitely() -const isDocker = require('is-docker') +const isDocker = require('is-docker').default const closeWithGrace = require('close-with-grace') const deepmerge = require('@fastify/deepmerge')({ diff --git a/util.js b/util.js index 7a0bb9d2..2e58fe32 100644 --- a/util.js +++ b/util.js @@ -4,7 +4,7 @@ const fs = require('node:fs') const path = require('node:path') const url = require('node:url') const semver = require('semver') -const pkgUp = require('pkg-up') +const { pkgUp } = require('pkg-up') const resolveFrom = require('resolve-from') const moduleSupport = semver.satisfies(process.version, '>= 14 || >= 12.17.0 < 13.0.0') From 258b99ca7d43476095687214ed8717bb95c9de26 Mon Sep 17 00:00:00 2001 From: kilisamemarisaaa <1798456934@qq.com> Date: Fri, 28 Aug 2026 16:37:27 +0800 Subject: [PATCH 3/8] fix: repair CLI argument parsing after util.parseArgs migration The util.parseArgs migration (#887) left two breakages that CI never caught because the test suite was silently no-op: - cli.js read argv._ which util.parseArgs values never contains; --help and --help crashed with 'Cannot read properties of undefined' - generate-swagger --yaml was rejected by strict mode because yaml was not registered as a known option; register it in args.js (restores the lenient pre-migration behaviour) Co-Authored-By: EvoX --- args.js | 3 ++- cli.js | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/args.js b/args.js index e877f675..478226d8 100644 --- a/args.js +++ b/args.js @@ -52,7 +52,8 @@ const CLI_OPTIONS = { 'include-hooks': { type: 'boolean' }, 'trust-proxy-enabled': { type: 'boolean' }, help: { type: 'boolean', short: 'h' }, - 'debug-port': { type: 'string', short: 'I' } + 'debug-port': { type: 'string', short: 'I' }, + yaml: { type: 'boolean' } } module.exports = function parseCliArgs (args) { diff --git a/cli.js b/cli.js index 0312bb1d..7a606301 100755 --- a/cli.js +++ b/cli.js @@ -5,7 +5,9 @@ const path = require('node:path') const commist = require('commist')() const { parseArgs } = require('node:util') -const argv = parseArgs({ args: process.argv.slice(2), strict: false, allowPositionals: true }).values +const parsed = parseArgs({ args: process.argv.slice(2), strict: false, allowPositionals: true }) +const argv = parsed.values +const positionals = parsed.positionals const help = require('help-me')({ // the default dir: path.join(path.dirname(require.main.filename), 'help') @@ -32,7 +34,7 @@ commist.register('print-routes', printRoutes.cli) commist.register('print-plugins', printPlugins.cli) if (argv.help) { - const command = argv._.splice(2)[0] + const command = positionals[0] help.toStdout(command) } else { From 3150a8036a4a271b9317e4e31cb34cd08427aa2d Mon Sep 17 00:00:00 2001 From: kilisamemarisaaa <1798456934@qq.com> Date: Fri, 28 Aug 2026 16:37:28 +0800 Subject: [PATCH 4/8] fix(generate-plugin): only emit tstyche field when the template provides it Object.assign(pkg.tstyche || {}, template.tstyche) emitted an empty 'tstyche': {} into generated plugin package.json because the plugin template has no tstyche section (regression from the tsd -> tstyche migration, #886). Guard the assignment. Co-Authored-By: EvoX --- generate-plugin.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/generate-plugin.js b/generate-plugin.js index 54c7ea57..0c71a68f 100755 --- a/generate-plugin.js +++ b/generate-plugin.js @@ -69,7 +69,9 @@ async function generate (dir, template) { pkg.scripts = Object.assign(pkg.scripts || {}, template.scripts) pkg.dependencies = Object.assign(pkg.dependencies || {}, template.dependencies) pkg.devDependencies = Object.assign(pkg.devDependencies || {}, template.devDependencies) - pkg.tstyche = Object.assign(pkg.tstyche || {}, template.tstyche) + if (template.tstyche) { + pkg.tstyche = Object.assign(pkg.tstyche || {}, template.tstyche) + } log('debug', 'edited package.json, saving') From 1ac69e64248cbf012076e682aa5fd6127ff4309e Mon Sep 17 00:00:00 2001 From: kilisamemarisaaa <1798456934@qq.com> Date: Fri, 28 Aug 2026 18:08:26 +0800 Subject: [PATCH 5/8] fix: declare node types for the ts-node test suites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under TypeScript 6 the node:test / node:assert types in templates/app-ts(-esm) tests no longer resolve (fastify-tsconfig does not set a 'types' field and @types/node is not picked up automatically), failing compilation with TS2591 — the first time these suites actually ran since CI went hollow. Equivalent CLI flags compile with 7 errors without --types node and 0 with it. Co-Authored-By: EvoX --- test/configs/ts-cjs.tsconfig.json | 3 ++- test/configs/ts-esm.tsconfig.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/test/configs/ts-cjs.tsconfig.json b/test/configs/ts-cjs.tsconfig.json index 5e0e7973..2edc6e4b 100644 --- a/test/configs/ts-cjs.tsconfig.json +++ b/test/configs/ts-cjs.tsconfig.json @@ -2,7 +2,8 @@ "extends": "../../node_modules/fastify-tsconfig/tsconfig.json", "compilerOptions": { "outDir": "dist", - "sourceMap": true + "sourceMap": true, + "types": ["node"] }, "include": ["src/**/*.ts"] } diff --git a/test/configs/ts-esm.tsconfig.json b/test/configs/ts-esm.tsconfig.json index b52b3ee8..f8d13235 100644 --- a/test/configs/ts-esm.tsconfig.json +++ b/test/configs/ts-esm.tsconfig.json @@ -6,7 +6,8 @@ "moduleResolution": "NodeNext", "module": "NodeNext", "target": "ES2022", - "esModuleInterop": true + "esModuleInterop": true, + "types": ["node"] }, "include": ["src/**/*.ts"] } From a8d19b4b55fdabc2e8e15af2bac439e2990a822a Mon Sep 17 00:00:00 2001 From: kilisamemarisaaa <1798456934@qq.com> Date: Sun, 30 Aug 2026 22:21:59 +0800 Subject: [PATCH 6/8] fix(test): register ts-node ESM loader once Pass the ts-node ESM loader only to node:test workers. This avoids Node 26 registering an inherited loader twice while leaving ordinary suites' execArgv untouched. --- package.json | 4 ++-- suite-runner.js | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index ac281f4b..dcfb4e82 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "unit:cjs": "node suite-runner.js \"templates/app/test/**/*.test.js\"", "unit:esm": "node suite-runner.js \"templates/app-esm/test/**/*.test.js\"", "unit:ts-cjs": "cross-env TS_NODE_PROJECT=./test/configs/ts-cjs.tsconfig.json node -r ts-node/register suite-runner.js \"templates/app-ts/test/**/*.test.ts\"", - "unit:ts-esm": "cross-env TS_NODE_PROJECT=./test/configs/ts-esm.tsconfig.json FASTIFY_AUTOLOAD_TYPESCRIPT=1 node -r ts-node/register --loader ts-node/esm suite-runner.js \"templates/app-ts-esm/test/**/*.test.ts\"", + "unit:ts-esm": "cross-env TS_NODE_PROJECT=./test/configs/ts-esm.tsconfig.json FASTIFY_AUTOLOAD_TYPESCRIPT=1 node suite-runner.js \"templates/app-ts-esm/test/**/*.test.ts\" \"--loader=ts-node/esm\"", "unit:suites": "node should-skip-test-suites.js || npm run all-suites", "all-suites": "npm run unit:cjs && npm run unit:esm && npm run unit:ts-cjs && npm run unit:ts-esm", "unit:cli-js-esm": "node suite-runner.js \"test/esm/**/*.test.js\"", @@ -83,4 +83,4 @@ "typescript": "~6.0.2", "walker": "^1.0.8" } -} \ No newline at end of file +} diff --git a/suite-runner.js b/suite-runner.js index 71f6f410..ffe91b57 100644 --- a/suite-runner.js +++ b/suite-runner.js @@ -4,13 +4,17 @@ const path = require('node:path') const { glob } = require('glob') async function main () { - const pattern = process.argv[process.argv.length - 1] + const [pattern, ...workerExecArgv] = process.argv.slice(2) console.info(`Running tests matching ${pattern}`) const timeout = 10 * 60 * 1000 // 10 minutes const matches = await glob(pattern) const resolved = matches.map(file => path.resolve(file)) - const testRs = run({ files: resolved, timeout }) + const runOptions = { files: resolved, timeout } + if (workerExecArgv.length > 0) { + runOptions.execArgv = workerExecArgv + } + const testRs = run(runOptions) .on('test:fail', () => { process.exitCode = 1 }) From d104ffa340e3ce8e6b9aef109f2dcca2479a76fd Mon Sep 17 00:00:00 2001 From: kilisamemarisaaa <1798456934@qq.com> Date: Mon, 31 Aug 2026 22:27:14 +0800 Subject: [PATCH 7/8] fix: preserve values in custom plugin options --- args.js | 5 ++- lib/parse-args.js | 77 +++++++++++++++++++++++++++++++++++++++++++++-- test/args.test.js | 13 +++++++- 3 files changed, 90 insertions(+), 5 deletions(-) diff --git a/args.js b/args.js index 478226d8..2932d3d6 100644 --- a/args.js +++ b/args.js @@ -69,7 +69,10 @@ module.exports = function parseCliArgs (args) { const configFileOptions = commandLineArguments.config ? requireModule(commandLineArguments.config) : undefined const additionalArgs = commandLineArguments['--'] || [] - const pluginParsed = parseArgs(additionalArgs, { options: {}, strict: false }) + const pluginParsed = parseArgs(additionalArgs, { + inferUnknownOptions: true, + strict: true + }) const { _, ...pluginOptions } = pluginParsed const ignoreWatchArg = commandLineArguments.ignoreWatch || configFileOptions?.ignoreWatch || '' const followWatchArg = commandLineArguments.followWatch || configFileOptions?.followWatch || '' diff --git a/lib/parse-args.js b/lib/parse-args.js index 6b7f5995..030d98fb 100644 --- a/lib/parse-args.js +++ b/lib/parse-args.js @@ -74,12 +74,21 @@ function normalizeArgs (args, options) { } } else { // Non-boolean option: --key value - normalized.push(`--${keyKebab}`) i++ if (i < args.length) { // Convert to string because parseArgs requires string values - normalized.push(String(args[i])) + const value = String(args[i]) + // util.parseArgs treats a dash-prefixed separate value as + // ambiguous; use the inline form for negative numeric values. + if (/^-\d/.test(value)) { + normalized.push(`--${keyKebab}=${value}`) + } else { + normalized.push(`--${keyKebab}`) + normalized.push(value) + } i++ + } else { + normalized.push(`--${keyKebab}`) } } continue @@ -100,8 +109,70 @@ function normalizeArgs (args, options) { return normalized } +// Infer option types for the plugin-specific arguments that are intentionally +// not known by fastify-cli. This preserves yargs-parser's `--key value` +// behavior while still using util.parseArgs for the actual parsing. +function inferUnknownOptions (args) { + const options = {} + + function addOption (key, type, short) { + if (!key) return + + const current = options[key] + if (current?.type === 'string' || (type === 'boolean' && current)) return + + options[key] = { type } + if (short) options[key].short = short + } + + for (let i = 0; i < args.length; i++) { + const arg = String(args[i]) + if (arg === '--') break + + if (arg.startsWith('--')) { + const equals = arg.indexOf('=') + if (equals > 2) { + addOption(arg.slice(2, equals), 'string') + continue + } + + const key = arg.slice(2) + const next = args[i + 1] + const hasValue = next !== undefined && + (!String(next).startsWith('-') || /^-\d/.test(String(next))) + addOption(key, hasValue ? 'string' : 'boolean') + if (hasValue) i++ + continue + } + + if (arg.startsWith('-') && arg.length > 1) { + const shortOptions = arg.slice(1) + if (shortOptions.length > 1 && !shortOptions.includes('=')) { + for (const short of shortOptions) addOption(short, 'boolean', short) + continue + } + + const equals = shortOptions.indexOf('=') + const key = equals === -1 ? shortOptions : shortOptions.slice(0, equals) + if (equals !== -1) { + addOption(key, 'string', key) + } else { + const next = args[i + 1] + const hasValue = next !== undefined && + (!String(next).startsWith('-') || /^-\d/.test(String(next))) + addOption(key, hasValue ? 'string' : 'boolean', key) + if (hasValue) i++ + } + } + } + + return options +} + function parseArgsStandard (args, config) { - const options = config.options || {} + const options = config.inferUnknownOptions + ? inferUnknownOptions(args) + : config.options || {} // Build full options map const fullOptions = {} diff --git a/test/args.test.js b/test/args.test.js index b9934711..67418751 100644 --- a/test/args.test.js +++ b/test/args.test.js @@ -293,7 +293,7 @@ test('should parse custom plugin options', t => { a: true, b: true, c: true, - hello: true + hello: 'world' }, bodyLimit: 5242880, debug: true, @@ -308,6 +308,17 @@ test('should parse custom plugin options', t => { }) }) +test('should parse plugin options with negative values', t => { + const parsedArgs = parseArgs([ + 'app.js', + '--', + '--offset', + '-1' + ]) + + t.assert.equal(parsedArgs.pluginOptions.offset, '-1') +}) + test('should parse config file correctly and prefer config values over default ones', t => { t.plan(1) From 50acde915509d406d32040145462671152e731a6 Mon Sep 17 00:00:00 2001 From: kilisamemarisaaa <1798456934@qq.com> Date: Mon, 31 Aug 2026 22:34:47 +0800 Subject: [PATCH 8/8] fix: retain numeric plugin option values --- args.js | 1 + lib/parse-args.js | 17 +++++++++++++++++ test/args.test.js | 2 +- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/args.js b/args.js index 2932d3d6..ce1db6cf 100644 --- a/args.js +++ b/args.js @@ -70,6 +70,7 @@ module.exports = function parseCliArgs (args) { const additionalArgs = commandLineArguments['--'] || [] const pluginParsed = parseArgs(additionalArgs, { + coerceUnknownNumbers: true, inferUnknownOptions: true, strict: true }) diff --git a/lib/parse-args.js b/lib/parse-args.js index 030d98fb..e3ef814c 100644 --- a/lib/parse-args.js +++ b/lib/parse-args.js @@ -169,6 +169,15 @@ function inferUnknownOptions (args) { return options } +function coerceUnknownNumber (value) { + if (Array.isArray(value)) return value.map(coerceUnknownNumber) + if (typeof value !== 'string' || value.length === 0) return value + + const numeric = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i.test(value) || + /^0x[\da-f]+$/i.test(value) + return numeric ? Number(value) : value +} + function parseArgsStandard (args, config) { const options = config.inferUnknownOptions ? inferUnknownOptions(args) @@ -257,6 +266,14 @@ function parseArgsStandard (args, config) { } } + if (config.coerceUnknownNumbers) { + for (const key of Object.keys(result)) { + if (key !== '_' && key !== '--') { + result[key] = coerceUnknownNumber(result[key]) + } + } + } + return result } diff --git a/test/args.test.js b/test/args.test.js index 67418751..1e4a2199 100644 --- a/test/args.test.js +++ b/test/args.test.js @@ -316,7 +316,7 @@ test('should parse plugin options with negative values', t => { '-1' ]) - t.assert.equal(parsedArgs.pluginOptions.offset, '-1') + t.assert.equal(parsedArgs.pluginOptions.offset, -1) }) test('should parse config file correctly and prefer config values over default ones', t => {