diff --git a/args.js b/args.js index e877f675..89422318 100644 --- a/args.js +++ b/args.js @@ -51,6 +51,7 @@ const CLI_OPTIONS = { 'common-prefix': { type: 'boolean' }, 'include-hooks': { type: 'boolean' }, 'trust-proxy-enabled': { type: 'boolean' }, + yaml: { type: 'boolean' }, help: { type: 'boolean', short: 'h' }, 'debug-port': { type: 'string', short: 'I' } } diff --git a/generate-plugin.js b/generate-plugin.js index 8a8c88e4..bb05acd6 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 { default: chalk } = require('chalk') 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 0641a716..ddc6dd63 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 { default: chalk } = require('chalk') 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..4390266c 100644 --- a/lib/parse-args.js +++ b/lib/parse-args.js @@ -22,11 +22,11 @@ function resolveEnvValue (envPrefix, optionName, type) { // Normalize args so they work with util.parseArgs: // - Convert camelCase option names to kebab-case -// - Handle --bool true -> --bool (remove value after boolean flags) +// - Handle boolean options that use yargs-parser's explicit value syntax +// - Preserve values for unknown plugin options function normalizeArgs (args, options) { - // Build lookup maps const boolKeys = new Set() - const allKeys = new Map() // normalized key -> config + const allKeys = new Map() for (const [key, cfg] of Object.entries(options)) { const kebab = kebabCase(key) allKeys.set(kebab, cfg) @@ -38,22 +38,28 @@ function normalizeArgs (args, options) { } const normalized = [] + const booleanValues = new Map() let i = 0 while (i < args.length) { const arg = args[i] const strArg = String(arg) + if (strArg === '--') { + normalized.push(strArg) + i++ + continue + } + // Long option with = sign if (typeof arg === 'string' && strArg.startsWith('--') && strArg.includes('=')) { const eqIdx = strArg.indexOf('=') const key = strArg.slice(2, eqIdx) const val = strArg.slice(eqIdx + 1) const keyKebab = kebabCase(key) - if (boolKeys.has(key)) { - // --bool=value: drop the value, just use --bool + if (boolKeys.has(key) || boolKeys.has(keyKebab)) { + booleanValues.set(keyKebab, !['false', '0'].includes(val)) normalized.push(`--${keyKebab}`) } else { - // --key=value with inline value normalized.push(`--${keyKebab}=${String(val)}`) } i++ @@ -64,22 +70,30 @@ function normalizeArgs (args, options) { if (typeof arg === 'string' && strArg.startsWith('--')) { const key = strArg.slice(2) const keyKebab = kebabCase(key) - if (boolKeys.has(key)) { - // Boolean flag + if (boolKeys.has(key) || boolKeys.has(keyKebab)) { normalized.push(`--${keyKebab}`) + booleanValues.set(keyKebab, true) i++ - // If next arg is a truthy/falsy value, consume it if (i < args.length && ['true', 'false', '1', '0'].includes(String(args[i]))) { + const value = String(args[i]) + booleanValues.set(keyKebab, !['false', '0'].includes(value)) i++ } } else { - // Non-boolean option: --key value + const known = allKeys.has(key) || allKeys.has(keyKebab) normalized.push(`--${keyKebab}`) i++ if (i < args.length) { - // Convert to string because parseArgs requires string values - normalized.push(String(args[i])) - i++ + const next = String(args[i]) + const isOption = next === '--' || (next.startsWith('--') && next.length > 2) || (next.startsWith('-') && !/^-\d/.test(next)) + if (!isOption) { + if (known) { + normalized.push(next) + } else { + normalized[normalized.length - 1] = `--${keyKebab}=${next}` + } + i++ + } } } continue @@ -92,15 +106,17 @@ function normalizeArgs (args, options) { continue } - // Positional argument (convert to string) normalized.push(String(arg)) i++ } - return normalized + return { args: normalized, booleanValues } } function parseArgsStandard (args, config) { + const inputArgs = Array.isArray(args) + ? args + : String(args).trim().split(/\s+/).filter(Boolean) const options = config.options || {} // Build full options map @@ -119,14 +135,14 @@ function parseArgsStandard (args, config) { } // Normalize args for strict parseArgs compatibility - const normalizedArgs = normalizeArgs(args, options) + const normalized = normalizeArgs(inputArgs, options) const parsed = parseArgs({ strict: config.strict !== false, allowPositionals: true, tokens: config.tokenize !== false, options: fullOptions, - args: normalizedArgs + args: normalized.args }) // Build flat result from values + positionals, converting keys to camelCase @@ -134,6 +150,9 @@ function parseArgsStandard (args, config) { for (const [key, value] of Object.entries(parsed.values)) { result[camelCase(key)] = value } + for (const [key, value] of normalized.booleanValues) { + result[camelCase(key)] = value + } // Handle -- separator (rest tokens) // When -- is present, everything after it becomes positionals @@ -141,6 +160,7 @@ function parseArgsStandard (args, config) { if (config.populateRest) { const rest = [] const mainPositionals = [] + const separatorIndex = inputArgs.indexOf('--') let inRest = false for (const token of parsed.tokens) { if (token.kind === 'option-terminator') { @@ -156,7 +176,7 @@ function parseArgsStandard (args, config) { } } result._ = mainPositionals - result['--'] = rest + result['--'] = separatorIndex === -1 ? rest : inputArgs.slice(separatorIndex + 1) } else { result._ = parsed.positionals } diff --git a/lib/watch/fork.js b/lib/watch/fork.js index d4ee7922..b7625fd8 100644 --- a/lib/watch/fork.js +++ b/lib/watch/fork.js @@ -1,6 +1,6 @@ 'use strict' -const chalk = require('chalk') +const { default: chalk } = require('chalk') const { stop, runFastify } = require('../../start') const { diff --git a/lib/watch/index.js b/lib/watch/index.js index b4f53859..ce32233a 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 { default: chalk } = require('chalk') 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..fe147a09 100644 --- a/lib/watch/utils.js +++ b/lib/watch/utils.js @@ -1,6 +1,6 @@ 'use strict' -const chalk = require('chalk') +const { default: chalk } = require('chalk') const path = require('node:path') const arrayToRegExp = (arr) => { diff --git a/log.js b/log.js index 84987b9a..c47089c2 100644 --- a/log.js +++ b/log.js @@ -1,6 +1,6 @@ 'use strict' -const chalk = require('chalk') +const { default: chalk } = require('chalk') const levels = { debug: 0, diff --git a/start.js b/start.js index a9d97d1b..60f722be 100755 --- a/start.js +++ b/start.js @@ -4,7 +4,8 @@ const { loadEnvQuitely } = require('./env-loader') loadEnvQuitely() -const isDocker = require('is-docker') +const isDockerModule = require('is-docker') +const isDocker = typeof isDockerModule === 'function' ? isDockerModule : isDockerModule.default const closeWithGrace = require('close-with-grace') const deepmerge = require('@fastify/deepmerge')({ diff --git a/suite-runner.js b/suite-runner.js index 8a1408c5..5dd22cc8 100644 --- a/suite-runner.js +++ b/suite-runner.js @@ -5,18 +5,38 @@ const { glob } = require('glob') 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) +async function main () { + console.info(`Running tests matching ${pattern}`) + const timeout = 10 * 60 * 1000 // 10 minutes + const matches = await glob(pattern) + if (matches.length === 0) { + throw new Error(`No test files matched ${pattern}`) } + const resolved = matches.map(file => path.resolve(file)) - const testRs = run({ files: resolved, timeout }) + const runOptions = { + files: resolved, + timeout, + concurrency: 1 + } + if (pattern.endsWith('.ts') && process.execArgv.some(arg => arg.includes('ts-node/esm'))) { + runOptions.isolation = 'none' + } + + const testRs = run(runOptions) .on('test:fail', () => { process.exitCode = 1 }) .compose(spec) - testRs.pipe(process.stdout) + + await new Promise((resolve, reject) => { + testRs.once('error', reject) + testRs.once('end', resolve) + testRs.pipe(process.stdout, { end: false }) + }) +} + +main().catch(err => { + console.error(err) + process.exitCode = 1 }) diff --git a/test/args.test.js b/test/args.test.js index b9934711..a5b83bd8 100644 --- a/test/args.test.js +++ b/test/args.test.js @@ -210,6 +210,18 @@ test('should parse env vars correctly', t => { }) }) +test('should preserve explicit false boolean values', t => { + const parsedArgs = parseArgs([ + '--watch=false', + '--pretty-logs', 'false', + 'app.js' + ]) + + t.assert.strictEqual(parsedArgs.watch, false) + t.assert.strictEqual(parsedArgs.prettyLogs, false) + t.assert.deepStrictEqual(parsedArgs._, ['app.js']) +}) + test('should respect default values', t => { t.plan(14) @@ -293,7 +305,7 @@ test('should parse custom plugin options', t => { a: true, b: true, c: true, - hello: true + hello: 'world' }, bodyLimit: 5242880, debug: true, 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')