Skip to content
Merged
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
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 chalk = require('chalk').default
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 chalk = require('chalk').default
const generify = require('generify')
const parseArgs = require('./lib/parse-args')
const cliPkg = require('./package')
Expand Down
152 changes: 127 additions & 25 deletions lib/parse-args.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,88 +20,167 @@ function resolveEnvValue (envPrefix, optionName, type) {
return envValue
}

const FALSY_VALUES = new Set(['false', '0'])
const TRUTHY_VALUES = new Set(['true', '1'])

// Normalize args so they work with util.parseArgs:
// - Convert camelCase option names to kebab-case
// - Handle --bool true -> --bool (remove value after boolean flags)
function normalizeArgs (args, options) {
// - Handle --bool true -> --bool and --bool false / --bool=false -> explicit false
// - In non-strict mode, let unknown options consume the following value
// (--hello world -> --hello=world) to match the previous yargs-parser behavior
function normalizeArgs (args, options, strict) {
// Build lookup maps
const boolKeys = new Set()
const allKeys = new Map() // normalized key -> config
const knownKeys = new Set()
const shortToKey = new Map()
for (const [key, cfg] of Object.entries(options)) {
const kebab = kebabCase(key)
allKeys.set(kebab, cfg)
allKeys.set(key, cfg)
knownKeys.add(kebab)
knownKeys.add(key)
if (cfg.type === 'boolean') {
boolKeys.add(kebab)
boolKeys.add(key)
}
if (cfg.short) {
shortToKey.set(cfg.short, kebab)
}
}

const normalized = []
// Boolean options explicitly set to false (util.parseArgs cannot express them)
const explicitFalse = new Set()

function pushBoolean (keyKebab, value) {
if (value === undefined || TRUTHY_VALUES.has(value)) {
normalized.push(`--${keyKebab}`)
} else {
explicitFalse.add(keyKebab)
}
}

let i = 0
while (i < args.length) {
const arg = args[i]
const strArg = String(arg)

// Option terminator: everything after it is passed through untouched
if (strArg === '--') {
for (; i < args.length; i++) {
normalized.push(String(args[i]))
}
break
}

// 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
normalized.push(`--${keyKebab}`)
pushBoolean(keyKebab, val)
} else {
// --key=value with inline value
normalized.push(`--${keyKebab}=${String(val)}`)
normalized.push(`--${keyKebab}=${val}`)
}
i++
continue
}

// Long option without = sign
if (typeof arg === 'string' && strArg.startsWith('--')) {
if (typeof arg === 'string' && strArg.startsWith('--') && strArg.length > 2) {
const key = strArg.slice(2)
const keyKebab = kebabCase(key)
i++
const next = i < args.length ? String(args[i]) : undefined
if (boolKeys.has(key)) {
// Boolean flag
normalized.push(`--${keyKebab}`)
i++
// If next arg is a truthy/falsy value, consume it
if (i < args.length && ['true', 'false', '1', '0'].includes(String(args[i]))) {
// Boolean flag, optionally followed by an explicit true/false value
if (next !== undefined && (TRUTHY_VALUES.has(next) || FALSY_VALUES.has(next))) {
pushBoolean(keyKebab, next)
i++
} else {
pushBoolean(keyKebab)
}
} else {
} else if (knownKeys.has(key)) {
// Non-boolean option: --key value
normalized.push(`--${keyKebab}`)
i++
if (i < args.length) {
if (next !== undefined) {
// Convert to string because parseArgs requires string values
normalized.push(String(args[i]))
normalized.push(next)
i++
}
} else if (!strict && next !== undefined && !next.startsWith('-')) {
// Unknown option followed by a value: --hello world -> --hello=world
normalized.push(`--${keyKebab}=${next}`)
i++
} else {
// Unknown option without a value
normalized.push(`--${keyKebab}`)
}
continue
}

// Short option group (-abc) or short option with value (-p 3000)
if (typeof arg === 'string' && strArg.startsWith('-') && strArg.length > 1) {
normalized.push(strArg)
if (typeof arg === 'string' && strArg.startsWith('-') && strArg.length > 1 && strArg !== '--') {
i++
const keyKebab = strArg.length === 2 ? shortToKey.get(strArg[1]) : undefined
const next = i < args.length ? String(args[i]) : undefined
if (keyKebab !== undefined && boolKeys.has(keyKebab) &&
next !== undefined && (TRUTHY_VALUES.has(next) || FALSY_VALUES.has(next))) {
// Short boolean alias followed by an explicit true/false value
pushBoolean(keyKebab, next)
i++
} else {
normalized.push(strArg)
}
continue
}

// Positional argument (convert to string)
normalized.push(String(arg))
normalized.push(strArg)
i++
}

return normalized
return { normalized, explicitFalse }
}

// Split a command line string into tokens, honoring single and double quotes
// (yargs-parser accepted strings as well as arrays)
function tokenizeString (str) {
const tokens = []
let current = ''
let quote = null
let hasToken = false
for (const ch of str) {
if (quote) {
if (ch === quote) {
quote = null
} else {
current += ch
}
} else if (ch === '"' || ch === "'") {
quote = ch
hasToken = true
} else if (/\s/.test(ch)) {
if (hasToken) {
tokens.push(current)
current = ''
hasToken = false
}
} else {
current += ch
hasToken = true
}
}
if (hasToken) tokens.push(current)
return tokens
}

function parseArgsStandard (args, config) {
const options = config.options || {}
if (typeof args === 'string') {
args = tokenizeString(args)
}

// Build full options map
const fullOptions = {}
Expand All @@ -118,11 +197,14 @@ function parseArgsStandard (args, config) {
}
}

// Normalize args for strict parseArgs compatibility
const normalizedArgs = normalizeArgs(args, options)
// Like yargs-parser, unknown options are accepted unless explicitly requested
const strict = config.strict === true

// Normalize args for parseArgs compatibility
const { normalized: normalizedArgs, explicitFalse } = normalizeArgs(args, options, strict)

const parsed = parseArgs({
strict: config.strict !== false,
strict,
allowPositionals: true,
tokens: config.tokenize !== false,
options: fullOptions,
Expand All @@ -134,6 +216,26 @@ function parseArgsStandard (args, config) {
for (const [key, value] of Object.entries(parsed.values)) {
result[camelCase(key)] = value
}
for (const key of explicitFalse) {
result[camelCase(key)] = false
}

// Repeated string options (e.g. -r a -r b) become arrays, like yargs-parser did
if (parsed.tokens) {
const repeated = new Map()
for (const token of parsed.tokens) {
if (token.kind === 'option' && token.value !== undefined) {
const camelKey = camelCase(token.name)
if (!repeated.has(camelKey)) repeated.set(camelKey, [])
repeated.get(camelKey).push(token.value)
}
}
for (const [camelKey, values] of repeated) {
if (values.length > 1) {
result[camelKey] = values
}
}
}

// Handle -- separator (rest tokens)
// When -- is present, everything after it becomes 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 chalk = require('chalk').default
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 chalk = require('chalk').default
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 chalk = require('chalk').default
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 chalk = require('chalk').default

const levels = {
debug: 0,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 --import ./test/configs/register-ts-esm.mjs suite-runner.js \"templates/app-ts-esm/test/**/*.test.ts\"",
"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\"",
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
11 changes: 7 additions & 4 deletions suite-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,19 @@ 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)
glob(pattern, { ignore: ['**/node_modules/**', 'test/workdir*/**'] }).then((matches) => {
if (matches.length === 0) {
console.error(`No test files matched ${pattern}`)
process.exit(1)
}
const resolved = matches.map(file => path.resolve(file))
const testRs = run({ files: resolved, timeout })
const testRs = run({ files: resolved, timeout, concurrency: 1 })
.on('test:fail', () => {
process.exitCode = 1
})
.compose(spec)
testRs.pipe(process.stdout)
}, (err) => {
console.error(err)
process.exit(1)
})
Loading