diff --git a/args.js b/args.js index e877f675..ce1db6cf 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) { @@ -68,7 +69,11 @@ 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, { + coerceUnknownNumbers: true, + inferUnknownOptions: true, + strict: true + }) const { _, ...pluginOptions } = pluginParsed const ignoreWatchArg = commandLineArguments.ignoreWatch || configFileOptions?.ignoreWatch || '' const followWatchArg = commandLineArguments.followWatch || configFileOptions?.followWatch || '' diff --git a/generate-plugin.js b/generate-plugin.js index 8a8c88e4..0c71a68f 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') @@ -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') 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/parse-args.js b/lib/parse-args.js index 6b7f5995..e3ef814c 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,79 @@ 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 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.options || {} + const options = config.inferUnknownOptions + ? inferUnknownOptions(args) + : config.options || {} // Build full options map const fullOptions = {} @@ -186,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/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/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/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/suite-runner.js b/suite-runner.js index 8a1408c5..ffe91b57 100644 --- a/suite-runner.js +++ b/suite-runner.js @@ -3,20 +3,26 @@ 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, ...workerExecArgv] = process.argv.slice(2) -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 }) + const runOptions = { files: resolved, timeout } + if (workerExecArgv.length > 0) { + runOptions.execArgv = workerExecArgv + } + const testRs = run(runOptions) .on('test:fail', () => { process.exitCode = 1 }) .compose(spec) testRs.pipe(process.stdout) +} + +main().catch(err => { + console.error(err) + process.exit(1) }) diff --git a/test/args.test.js b/test/args.test.js index b9934711..1e4a2199 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) 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"] } 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')