Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions args.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
}
Expand Down
6 changes: 4 additions & 2 deletions generate-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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')

Expand Down
2 changes: 1 addition & 1 deletion generate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
56 changes: 38 additions & 18 deletions lib/parse-args.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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++
Expand All @@ -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
Expand All @@ -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
Expand All @@ -119,28 +135,32 @@ 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
const result = {}
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
// We split positionals into main positionals and rest positionals
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') {
Expand All @@ -156,7 +176,7 @@ function parseArgsStandard (args, config) {
}
}
result._ = mainPositionals
result['--'] = rest
result['--'] = separatorIndex === -1 ? rest : inputArgs.slice(separatorIndex + 1)
} else {
result._ = parsed.positionals
}
Expand Down
2 changes: 1 addition & 1 deletion lib/watch/fork.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict'

const chalk = require('chalk')
const { default: chalk } = require('chalk')
const { stop, runFastify } = require('../../start')

const {
Expand Down
2 changes: 1 addition & 1 deletion lib/watch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down
2 changes: 1 addition & 1 deletion lib/watch/utils.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict'

const chalk = require('chalk')
const { default: chalk } = require('chalk')
const path = require('node:path')

const arrayToRegExp = (arr) => {
Expand Down
2 changes: 1 addition & 1 deletion log.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use strict'

const chalk = require('chalk')
const { default: chalk } = require('chalk')

const levels = {
debug: 0,
Expand Down
3 changes: 2 additions & 1 deletion start.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')({
Expand Down
36 changes: 28 additions & 8 deletions suite-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
14 changes: 13 additions & 1 deletion test/args.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion test/configs/ts-cjs.tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"extends": "../../node_modules/fastify-tsconfig/tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"sourceMap": true
"sourceMap": true,
"types": ["node"]
},
"include": ["src/**/*.ts"]
}
3 changes: 2 additions & 1 deletion test/configs/ts-esm.tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"moduleResolution": "NodeNext",
"module": "NodeNext",
"target": "ES2022",
"esModuleInterop": true
"esModuleInterop": true,
"types": ["node"]
},
"include": ["src/**/*.ts"]
}
2 changes: 1 addition & 1 deletion util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down