diff --git a/packages/shared-lib-node/src/env.ts b/packages/shared-lib-node/src/env.ts index d20ec398..03670d07 100644 --- a/packages/shared-lib-node/src/env.ts +++ b/packages/shared-lib-node/src/env.ts @@ -34,6 +34,10 @@ export const yargsOptionsBuilderForEnv = { type: 'string', default: '.env.example', }, + 'quiet-env': { + description: 'Suppress .env file loading information.', + type: 'boolean', + }, verbose: { description: 'Whether to show verbose information', type: 'boolean', @@ -77,7 +81,8 @@ export function readEnvironmentVariables( ); } envPaths = envPaths.filter((envPath) => fs.existsSync(envPath)).map((envPath) => path.relative(cwd, envPath)); - if (argv.verbose) { + const shouldSuppressOutput = shouldSuppressEnvironmentOutput(argv); + if (argv.verbose && !shouldSuppressOutput) { console.info(`WB_ENV: ${process.env.WB_ENV}, NODE_ENV: ${process.env.NODE_ENV}`); console.info('Reading env files:', envPaths.join(', ')); } @@ -93,11 +98,11 @@ export function readEnvironmentVariables( } } envPathAndLoadedEnvVarNames.push([envPath, keys]); - if (argv.verbose && keys.length > 0) { + if (argv.verbose && !shouldSuppressOutput && keys.length > 0) { console.info(`Read ${keys.length} environment variables from ${envPath}`); } } - if (!argv.verbose) { + if (!argv.verbose && !shouldSuppressOutput) { console.info( `Read env files: ${envPathAndLoadedEnvVarNames.map(([envPath, keys]) => (keys.length > 0 ? `${envPath} (${keys.join(', ')})` : envPath)).join(', ') || 'nothing'}` ); @@ -113,6 +118,11 @@ export function readEnvironmentVariables( return [expand({ parsed: envVars, processEnv: {} }).parsed ?? envVars, envPathAndLoadedEnvVarNames]; } +export function shouldSuppressEnvironmentOutput(argv: EnvReaderOptions): boolean { + const outputOptions = argv as EnvReaderOptions & { quietEnv?: boolean; silent?: boolean }; + return outputOptions.quietEnv === true || (outputOptions.quietEnv !== false && outputOptions.silent === true); +} + /** * This function read environment variables from `.env` files and assign them in `process.env`. * */ diff --git a/packages/shared-lib-node/src/index.ts b/packages/shared-lib-node/src/index.ts index 0b388c05..74a3d1f9 100644 --- a/packages/shared-lib-node/src/index.ts +++ b/packages/shared-lib-node/src/index.ts @@ -2,6 +2,7 @@ export { readEnvironmentVariables, readAndApplyEnvironmentVariables, removeNpmAndYarnEnvironmentVariables, + shouldSuppressEnvironmentOutput, yargsOptionsBuilderForEnv, } from './env.js'; export type { EnvReaderOptions } from './env.js'; diff --git a/packages/wb/src/commands/test.ts b/packages/wb/src/commands/test.ts index 1f427622..8384047c 100644 --- a/packages/wb/src/commands/test.ts +++ b/packages/wb/src/commands/test.ts @@ -131,7 +131,7 @@ export async function test(argv: TestCommandArgv): Promise { const targets = unitTargets.length > 0 ? unitTargets : defaultUnitTargets.length > 0 ? defaultUnitTargets : undefined; const unitArgv = { ...argv, targets }; - await runTestCommand(scripts.testUnit(project, unitArgv), project, argv, { timeout: argv.unitTimeout }); + await runUnitTestCommand(scripts.testUnit(project, unitArgv), project, argv, { timeout: argv.unitTimeout }); } // Skip e2e tests if not needed or no e2e directory exists if (!shouldRunE2e || !fs.existsSync(path.join(project.dirPath, 'test', 'e2e'))) { @@ -283,6 +283,21 @@ function runTestCommand( }); } +function runUnitTestCommand( + script: string, + project: Project, + argv: Parameters[2], + options: Parameters[3] = {} +): Promise { + return runWithSpawn(script, project, argv, { + ...options, + omitSilentStart: true, + printSilentOutputOnFailureOnly: true, + silentProgressIntervalMs: 10_000, + silentSuccessMessage: 'Unit tests passed.', + }); +} + function dedupeNoisyTestOutput(output: string): string { const recentPrintedLines: string[] = []; const recentPrintedLineSet = new Set(); diff --git a/packages/wb/src/project.ts b/packages/wb/src/project.ts index 05d6b1ca..2bd59b4e 100644 --- a/packages/wb/src/project.ts +++ b/packages/wb/src/project.ts @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import type { EnvReaderOptions } from '@willbooster/shared-lib-node/src'; -import { readEnvironmentVariables } from '@willbooster/shared-lib-node/src'; +import { readEnvironmentVariables, shouldSuppressEnvironmentOutput } from '@willbooster/shared-lib-node/src'; import { memoizeOne } from 'at-decorators'; import { globby } from 'globby'; import type { PackageJson } from 'type-fest'; @@ -110,8 +110,10 @@ export class Project { if (!this.loadEnv) return process.env; const [envVars, envPathAndLoadedEnvVarCountPairs] = readEnvironmentVariables(this.argv, this.dirPath); - for (const [envPath, count] of envPathAndLoadedEnvVarCountPairs) { - console.info(`Loaded ${count} environment variables from ${envPath}`); + if (!shouldSuppressEnvironmentOutput(this.argv)) { + for (const [envPath, count] of envPathAndLoadedEnvVarCountPairs) { + console.info(`Loaded ${count} environment variables from ${envPath}`); + } } return { ...envVars, ...process.env }; } diff --git a/packages/wb/src/scripts/run.ts b/packages/wb/src/scripts/run.ts index 01a2dcd2..fa5f1c27 100644 --- a/packages/wb/src/scripts/run.ts +++ b/packages/wb/src/scripts/run.ts @@ -11,8 +11,12 @@ interface Options { exitIfFailed?: boolean; onSignal?: (signal: NodeJS.Signals | null) => void; forceColor?: boolean; + omitSilentStart?: boolean; processSilentOutput?: (output: string) => string; printRawOutput?: boolean; + printSilentOutputOnFailureOnly?: boolean; + silentProgressIntervalMs?: number; + silentSuccessMessage?: string; timeout?: number; } @@ -32,7 +36,9 @@ export async function runWithSpawn( opts: Options = defaultOptions ): Promise { const normalizedScript = normalizeScript(script, project); - printStart(normalizedScript.printable, project, argv.silent ? 'Command' : 'Start'); + if (!(argv.silent && opts.omitSilentStart && !argv.dryRun)) { + printStart(normalizedScript.printable, project, argv.silent ? 'Command' : 'Start'); + } if (argv.verbose) { printStart(normalizedScript.runnable, project, 'Start (raw)', true); } @@ -41,7 +47,17 @@ export async function runWithSpawn( return 0; } - const shouldProcessSilentOutput = Boolean(argv.silent && opts.processSilentOutput); + const shouldProcessSilentOutput = Boolean( + argv.silent && (opts.processSilentOutput ?? opts.printSilentOutputOnFailureOnly) + ); + let wroteSilentProgress = false; + const progressTimer = + argv.silent && opts.silentProgressIntervalMs + ? setInterval(() => { + process.stdout.write('.'); + wroteSilentProgress = true; + }, opts.silentProgressIntervalMs) + : undefined; const ret = await spawnAsync(normalizedScript.runnable, undefined, { cwd: project.dirPath, env: configureEnv(project.env, opts), @@ -54,17 +70,28 @@ export async function runWithSpawn( printingStderr: argv.silent && !shouldProcessSilentOutput, omitBlankLinesWhilePrinting: argv.silent, verbose: argv.verbose, + }).finally(() => { + if (progressTimer) { + clearInterval(progressTimer); + } }); - if (shouldProcessSilentOutput) { - const output = opts.processSilentOutput?.(ret.stdout).trim(); + const exitCode = ret.status ?? 1; + if (wroteSilentProgress) { + process.stdout.write('\n'); + } + if (shouldProcessSilentOutput && (!opts.printSilentOutputOnFailureOnly || exitCode !== 0)) { + const output = (opts.processSilentOutput ? opts.processSilentOutput(ret.stdout) : ret.stdout).trim(); if (output) { process.stdout.write(output); process.stdout.write('\n'); } } + if (argv.silent && exitCode === 0 && opts.silentSuccessMessage) { + console.info(chalk.green(opts.silentSuccessMessage)); + } opts.onSignal?.(ret.signal); - printFinishedAndExitIfNeeded(normalizedScript.printable, ret.status, opts, { silentSuccess: argv.silent }); - return ret.status ?? 1; + printFinishedAndExitIfNeeded(normalizedScript.printable, exitCode, opts, { silentSuccess: argv.silent }); + return exitCode; } export function runWithSpawnInParallel( diff --git a/packages/wb/src/sharedOptionsBuilder.ts b/packages/wb/src/sharedOptionsBuilder.ts index eee66096..082c1ce3 100644 --- a/packages/wb/src/sharedOptionsBuilder.ts +++ b/packages/wb/src/sharedOptionsBuilder.ts @@ -36,6 +36,12 @@ export function buildEnvReaderOptionArgs(argv: EnvReaderOptions): string[] { } } } + if ( + (argv as EnvReaderOptions & { silent?: boolean }).silent === true && + getOptionValue(argv, 'quiet-env') === undefined + ) { + args.push('--quiet-env'); + } return args; } diff --git a/packages/wbfy/src/generators/yarnrc.ts b/packages/wbfy/src/generators/yarnrc.ts index e22ffb70..3b57188a 100644 --- a/packages/wbfy/src/generators/yarnrc.ts +++ b/packages/wbfy/src/generators/yarnrc.ts @@ -88,6 +88,7 @@ export async function generateYarnrcYml(config: PackageConfig): Promise { // To deal with CVE like https://nextjs.org/blog/CVE-2025-66478 'next', '@next/*', + '@types/*', '@typescript/*', '@oxfmt/*', '@oxlint/*',