From f180175ec8b61f130c7f34415fd924766841e66b Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Sun, 23 Nov 2025 16:48:20 +0900 Subject: [PATCH 01/15] feat!: restructure wb commands --- packages/wb/src/commands/start.ts | 33 +--- packages/wb/src/commands/test.ts | 23 ++- packages/wb/src/commands/testOnCi.ts | 13 +- packages/wb/src/scripts/dockerScripts.ts | 2 +- .../wb/src/scripts/execution/baseScripts.ts | 142 +++++++++++------- .../wb/src/scripts/execution/blitzScripts.ts | 61 +------- .../scripts/execution/httpServerScripts.ts | 65 +------- .../wb/src/scripts/execution/nextScripts.ts | 59 +------- .../src/scripts/execution/plainAppScripts.ts | 56 ++++--- .../wb/src/scripts/execution/remixScripts.ts | 59 +------- packages/wb/src/scripts/run.ts | 15 +- packages/wb/src/utils/process.ts | 10 +- 12 files changed, 169 insertions(+), 369 deletions(-) diff --git a/packages/wb/src/commands/start.ts b/packages/wb/src/commands/start.ts index ebdd9568..76e2d668 100644 --- a/packages/wb/src/commands/start.ts +++ b/packages/wb/src/commands/start.ts @@ -61,31 +61,25 @@ export const startCommand: CommandModule test.log 2>&1`; -} - -function configureEnvironmentVariables(deps: Partial>, wbEnv: string): string { - process.env.WB_ENV ||= wbEnv; - let prefix = `WB_ENV=${process.env.WB_ENV} `; - if (deps.next) { - process.env.NEXT_PUBLIC_WB_ENV = process.env.WB_ENV; - prefix += `NEXT_PUBLIC_WB_ENV=${process.env.WB_ENV} `; - } - return prefix; -} diff --git a/packages/wb/src/commands/test.ts b/packages/wb/src/commands/test.ts index 15b5f162..9c05afdd 100644 --- a/packages/wb/src/commands/test.ts +++ b/packages/wb/src/commands/test.ts @@ -7,7 +7,6 @@ import type { ArgumentsCamelCase, CommandModule, InferredOptionTypes } from 'yar import type { Project } from '../project.js'; import { findDescendantProjects } from '../project.js'; import type { scriptOptionsBuilder } from '../scripts/builder.js'; -import { toDevNull } from '../scripts/builder.js'; import { dockerScripts } from '../scripts/dockerScripts.js'; import type { BaseScripts } from '../scripts/execution/baseScripts.js'; import { blitzScripts } from '../scripts/execution/blitzScripts.js'; @@ -132,11 +131,11 @@ export async function test( switch (argv.e2e) { case 'headless': { - await runWithSpawn(scripts.testE2E(project, e2eArgv, {}), project, argv); + await runWithSpawn(await scripts.testE2EProduction(project, e2eArgv, {}), project, argv); continue; } case 'headless-dev': { - await runWithSpawn(scripts.testE2EDev(project, e2eArgv, {}), project, argv); + await runWithSpawn(await scripts.testE2EDev(project, e2eArgv, {}), project, argv); continue; } case 'docker': { @@ -154,7 +153,7 @@ export async function test( switch (argv.e2e) { case 'headed': { await runWithSpawn( - scripts.testE2E(project, e2eArgv, { playwrightArgs: `test ${e2eTarget} --headed` }), + await scripts.testE2EProduction(project, e2eArgv, { playwrightArgs: `test ${e2eTarget} --headed` }), project, argv ); @@ -162,7 +161,7 @@ export async function test( } case 'headed-dev': { await runWithSpawn( - scripts.testE2EDev(project, e2eArgv, { playwrightArgs: `test ${e2eTarget} --headed` }), + await scripts.testE2EDev(project, e2eArgv, { playwrightArgs: `test ${e2eTarget} --headed` }), project, argv ); @@ -170,16 +169,17 @@ export async function test( } case 'debug': { await runWithSpawn( - scripts.testE2E(project, e2eArgv, { playwrightArgs: `test ${e2eTarget} --debug` }), + await scripts.testE2EProduction(project, e2eArgv, { playwrightArgs: `test ${e2eTarget} --debug` }), project, argv ); break; } case 'generate': { - const port = project.env.PORT || '8080'; await runWithSpawn( - scripts.testE2E(project, e2eArgv, { playwrightArgs: `codegen http://localhost:${port}` }), + await scripts.testE2EProduction(project, e2eArgv, { + playwrightArgs: `codegen http://localhost:${project.env.PORT}`, + }), project, argv ); @@ -200,12 +200,11 @@ async function testOnDocker( scripts: BaseScripts, playwrightArgs?: string ): Promise { - process.env.WB_DOCKER ??= '1'; - await runWithSpawn(`${scripts.buildDocker(project, 'test')}${toDevNull(argv)}`, project, argv); + project.env.WB_DOCKER ||= '1'; + await runWithSpawn(scripts.buildDocker(project, 'test'), project, argv); process.exitCode = await runWithSpawn( - scripts.testE2E(project, argv, { + await scripts.testE2EDocker(project, argv, { playwrightArgs, - startCommand: `${dockerScripts.stopAndStart(project, true)}${toDevNull(argv)}`, }), project, argv, diff --git a/packages/wb/src/commands/testOnCi.ts b/packages/wb/src/commands/testOnCi.ts index d8c49311..76361d9c 100644 --- a/packages/wb/src/commands/testOnCi.ts +++ b/packages/wb/src/commands/testOnCi.ts @@ -72,17 +72,16 @@ export async function testOnCi( await promisePool.promiseAll(); if (fs.existsSync(path.join(project.dirPath, 'test', 'e2e'))) { if (project.hasDockerfile) { - process.env.WB_DOCKER ??= '1'; + project.env.WB_DOCKER ||= '1'; + // Discard Docker build output to reduce log size on CI await runWithSpawn(`${scripts.buildDocker(project, 'test')}${toDevNull(argv)}`, project, argv); } - const options = project.hasDockerfile - ? { - startCommand: dockerScripts.stopAndStart(project, true), - } - : {}; + const script = project.hasDockerfile + ? await scripts.testE2EDocker(project, argv, {}) + : await scripts.testE2EProduction(project, argv, {}); process.exitCode = await runWithSpawn( // CI mode disallows `only` to avoid including debug tests - scripts.testE2E(project, argv, options).replaceAll(' --allowOnly', ''), + script.replaceAll(' --allowOnly', ''), project, argv, { diff --git a/packages/wb/src/scripts/dockerScripts.ts b/packages/wb/src/scripts/dockerScripts.ts index be355649..c81a9f47 100644 --- a/packages/wb/src/scripts/dockerScripts.ts +++ b/packages/wb/src/scripts/dockerScripts.ts @@ -8,7 +8,7 @@ import { spawnSyncOnExit } from '../utils/process.js'; * Note that `YARN zzz` is replaced with `yarn zzz` or `node_modules/.bin/zzz`. */ class DockerScripts { - buildDevImage(project: Project, version: string): string { + buildImage(project: Project, version: string): string { // e.g. coding-booster uses `"docker/build/prepare": "touch drill-users.csv",` const prefix = project.dockerPackageJson.scripts?.['docker/build/prepare'] ? 'YARN run docker/build/prepare && ' diff --git a/packages/wb/src/scripts/execution/baseScripts.ts b/packages/wb/src/scripts/execution/baseScripts.ts index 647dd3f9..07ad8aee 100644 --- a/packages/wb/src/scripts/execution/baseScripts.ts +++ b/packages/wb/src/scripts/execution/baseScripts.ts @@ -1,37 +1,77 @@ import type { TestArgv } from '../../commands/test.js'; import type { Project } from '../../project.js'; +import { SERVER_LOG_FILE } from '../../utils/log.js'; +import { checkAndKillPortProcess } from '../../utils/port.js'; import type { ScriptArgv } from '../builder.js'; import { toDevNull } from '../builder.js'; import { dockerScripts } from '../dockerScripts.js'; +import { prismaScripts } from '../prismaScripts.js'; -export interface TestE2EDevOptions { - // '--e2e generate' calls 'codegen http://localhost:8080' +export interface TestE2EOptions { + /** '--e2e generate' calls 'codegen http://localhost:8080' */ playwrightArgs?: string; - startCommand?: string; } -export type TestE2EOptions = { - prismaDirectory?: string; -} & TestE2EDevOptions; - /** * A collection of scripts for executing an app. * Note that YARN zzz` is replaced with `yarn zzz` or `node_modules/.bin/zzz`. */ export abstract class BaseScripts { - buildDocker(project: Project, version = 'development'): string { - return dockerScripts.buildDevImage(project, version); + private readonly shouldWaitAndOpenApp: boolean; + constructor(shouldWaitAndOpenApp: boolean) { + this.shouldWaitAndOpenApp = shouldWaitAndOpenApp; } - abstract start(project: Project, argv: ScriptArgv): string; + buildDocker(project: Project, version: string): string { + return dockerScripts.buildImage(project, version); + } - abstract startProduction(project: Project, argv: ScriptArgv, port: number): string; + // ------------ START: start commands ------------ + protected abstract startDevProtected(_: Project, argv: ScriptArgv): string; + protected startProductionProtected(project: Project): string { + return [ + project.hasPrisma ? prismaScripts.migrate(project) : '', + project.buildCommand, + `pm2-runtime start ${project.findFile('ecosystem.config.cjs')}`, + ] + .filter(Boolean) + .join(' && '); + } - abstract startTest(project: Project, argv: ScriptArgv): string; + async startDev(project: Project, argv: ScriptArgv): Promise { + await checkAndKillPortProcess(project.env.PORT, project); + if (!this.shouldWaitAndOpenApp) return this.startDevProtected(project, argv); - startDocker(project: Project, argv: ScriptArgv): string { - const port = Number(project.env.PORT) || 8080; - return `${this.buildDocker(project)}${toDevNull(argv)} + return `YARN concurrently --raw --kill-others-on-fail + "${this.startDevProtected(project, argv)}" + "${this.waitAndOpenApp(project)}"`; + } + async startProduction(project: Project, _: ScriptArgv): Promise { + await checkAndKillPortProcess(project.env.PORT, project); + if (!this.shouldWaitAndOpenApp) return this.startProductionProtected(project); + + return `YARN concurrently --raw --kill-others-on-fail + "${this.startProductionProtected(project)}" + "${this.waitAndOpenApp(project)}"`; + } + async startTest(project: Project, argv: ScriptArgv): Promise { + await checkAndKillPortProcess(project.env.PORT, project); + // We configure playwright to discard stdout, so startTest() also discards stdout for the consistency. + return this.startProductionProtected(project).replaceAll('&&', `${toDevNull(argv)} &&`); + } + async startDocker(project: Project, argv: ScriptArgv): Promise { + await checkAndKillPortProcess(project.env.PORT, project); + if (!this.shouldWaitAndOpenApp) { + return `${this.buildDocker(project, 'development')} + && ${dockerScripts.stopAndStart( + project, + false, + argv.normalizedDockerOptionsText ?? '', + argv.normalizedArgsText ?? '' + )}`; + } + + return `${this.buildDocker(project, 'development')} && YARN concurrently --raw --kill-others-on-fail "${dockerScripts.stopAndStart( project, @@ -39,64 +79,63 @@ export abstract class BaseScripts { argv.normalizedDockerOptionsText ?? '', argv.normalizedArgsText ?? '' )}" - "${this.waitAndOpenApp(project, argv, port)}"`; + "${this.waitAndOpenApp(project)}"`; } + // ------------ END: start commands ------------ - testE2E( - project: Project, - argv: TestArgv, - { playwrightArgs = 'test test/e2e/', prismaDirectory, startCommand }: TestE2EOptions - ): string { - const env = project.env.WB_ENV; - const port = project.env.PORT || '8080'; - const suffix = project.packageJson.scripts?.['test/e2e-additional'] ? ' && YARN test/e2e-additional' : ''; - const envPrefix = `WB_ENV=${env} NEXT_PUBLIC_WB_ENV=${env} APP_ENV=${env} PORT=${port}`; - const playwrightCommand = buildPlaywrightCommand(playwrightArgs, argv.targets); - if (project.skipLaunchingServerForPlaywright) { - return `${envPrefix} ${playwrightCommand}${suffix}`; - } - - const resetCommand = prismaDirectory ? `rm -Rf ${prismaDirectory}/mount && ` : ''; - return `${envPrefix} YARN concurrently --kill-others --raw --success first - "${resetCommand}${startCommand} && exit 1" - "wait-on -t 600000 -i 2000 http-get://127.0.0.1:${port} - && ${playwrightCommand}${suffix}"`; + // ------------ START: test (e2e) commands ------------ + testE2EDev(project: Project, argv: TestArgv, options: TestE2EOptions): Promise { + return this.testE2EPrivate(project, argv, this.startDevProtected(project, argv), options); + } + testE2EProduction(project: Project, argv: TestArgv, options: TestE2EOptions): Promise { + return this.testE2EPrivate( + project, + argv, + this.startProductionProtected(project).replaceAll('&&', `${toDevNull(argv)} &&`), + options + ); + } + testE2EDocker(project: Project, argv: TestArgv, options: TestE2EOptions): Promise { + return this.testE2EPrivate(project, argv, dockerScripts.stopAndStart(project, true), options); + } + async testStart(project: Project, argv: ScriptArgv): Promise { + await checkAndKillPortProcess(project.env.PORT, project); + return `YARN concurrently --kill-others --raw --success first "${this.startDevProtected(project, argv)} > ${SERVER_LOG_FILE} 2>&1" "${this.waitApp(project)}"`; } - testE2EDev( + private async testE2EPrivate( project: Project, argv: TestArgv, - { playwrightArgs = 'test test/e2e/', startCommand }: TestE2EDevOptions - ): string { - const env = project.env.WB_ENV; - const port = project.env.PORT || '8080'; + startCommand: string, + { playwrightArgs = 'test test/e2e/' }: TestE2EOptions + ): Promise { + const port = await checkAndKillPortProcess(project.env.PORT, project); const suffix = project.packageJson.scripts?.['test/e2e-additional'] ? ' && YARN test/e2e-additional' : ''; - const envPrefix = `WB_ENV=${env} NEXT_PUBLIC_WB_ENV=${env} APP_ENV=${env} PORT=${port}`; const playwrightCommand = buildPlaywrightCommand(playwrightArgs, argv.targets); if (project.skipLaunchingServerForPlaywright) { - return `${envPrefix} ${playwrightCommand}${suffix}`; + return `${playwrightCommand}${suffix}`; } - return `${envPrefix} YARN concurrently --kill-others --raw --success first + return `YARN concurrently --kill-others --raw --success first "${startCommand} && exit 1" "wait-on -t 600000 -i 2000 http-get://127.0.0.1:${port} && ${playwrightCommand}${suffix}"`; } - - abstract testStart(project: Project, argv: ScriptArgv): Promise; + // ------------ END: test (e2e) commands ------------ testUnit(project: Project, argv: TestArgv): string { const testTarget = argv.targets?.join(' ') || 'test/unit/'; if (project.hasVitest) { // Since this command is referred from other commands, we have to use "vitest run" (non-interactive mode). - return `WB_ENV=${project.env.WB_ENV} YARN vitest run ${testTarget} --color --passWithNoTests --allowOnly`; + return `YARN vitest run ${testTarget} --color --passWithNoTests --allowOnly`; } else if (project.isBunAvailable) { - return `WB_ENV=${project.env.WB_ENV} bun test ${testTarget}`; + return `bun test ${testTarget}`; } return 'echo "No tests."'; } - protected waitApp(project: Project, argv: ScriptArgv, port = project.env.PORT || 3000): string { + protected waitApp(project: Project): string { + const port = project.env.PORT; return `wait-on -t 10000 http-get://127.0.0.1:${port} 2> /dev/null || wait-on -t 10000 -i 500 http-get://127.0.0.1:${port} 2> /dev/null || wait-on -t 10000 -i 1000 http-get://127.0.0.1:${port} 2> /dev/null @@ -106,11 +145,10 @@ export abstract class BaseScripts { || wait-on -t 90000 -i 10000 http-get://127.0.0.1:${port}`; } - protected waitAndOpenApp(project: Project, argv: ScriptArgv, port = project.env.PORT || 3000): string { + protected waitAndOpenApp(project: Project): string { + const port = project.env.PORT; return `${this.waitApp( - project, - argv, - port + project )} || wait-on http-get://127.0.0.1:${port} && open-cli http://\${HOST:-localhost}:${port}`; } } diff --git a/packages/wb/src/scripts/execution/blitzScripts.ts b/packages/wb/src/scripts/execution/blitzScripts.ts index f387e91e..f4bea86a 100644 --- a/packages/wb/src/scripts/execution/blitzScripts.ts +++ b/packages/wb/src/scripts/execution/blitzScripts.ts @@ -1,11 +1,6 @@ -import type { TestArgv } from '../../commands/test.js'; import type { Project } from '../../project.js'; -import { findAvailablePort } from '../../utils/findPort.js'; import type { ScriptArgv } from '../builder.js'; -import { toDevNull } from '../builder.js'; -import { prismaScripts } from '../prismaScripts.js'; -import type { TestE2EDevOptions, TestE2EOptions } from './baseScripts.js'; import { BaseScripts } from './baseScripts.js'; /** @@ -13,60 +8,12 @@ import { BaseScripts } from './baseScripts.js'; * Note that `YARN zzz` is replaced with `yarn zzz` or `node_modules/.bin/zzz`. */ class BlitzScripts extends BaseScripts { - override start(project: Project, argv: ScriptArgv): string { - const appEnv = project.env.WB_ENV ? `APP_ENV=${project.env.WB_ENV} ` : ''; - const port = Number(project.env.PORT) || 3000; - return `${appEnv}YARN concurrently --raw --kill-others-on-fail - "blitz dev -p ${port} ${argv.normalizedArgsText ?? ''}" - "${this.waitAndOpenApp(project, argv, port)}"`; + constructor() { + super(true); } - override startProduction(project: Project, argv: ScriptArgv, port: number): string { - const appEnv = project.env.WB_ENV ? `APP_ENV=${project.env.WB_ENV} ` : ''; - return `${appEnv}NODE_ENV=production YARN concurrently --raw --kill-others-on-fail - "${[ - prismaScripts.migrate(project), - project.buildCommand, - `PORT=${port} pm2-runtime start ${project.findFile('ecosystem.config.cjs')} ${argv.normalizedArgsText ?? ''}`, - ].join(' && ')}" - "${this.waitAndOpenApp(project, argv, port)}"`; - } - - override startTest(project: Project, argv: ScriptArgv): string { - return [ - ...prismaScripts.reset(project).split('&&'), - project.buildCommand, - `pm2-runtime start ${project.findFile('ecosystem.config.cjs')}`, - ] - .map((command) => `${command.trim()}${toDevNull(argv)}`) - .join(' && '); - } - - override testE2E(project: Project, argv: TestArgv, options: TestE2EOptions): string { - return super.testE2E(project, argv, { - playwrightArgs: options.playwrightArgs, - prismaDirectory: 'db', - startCommand: - options.startCommand ?? - [ - ...prismaScripts.reset(project).split('&&'), - project.buildCommand, - `pm2-runtime start ${project.findFile('ecosystem.config.cjs')}`, - ] - .map((c) => `${c.trim()}${toDevNull(argv)}`) - .join(' && '), - }); - } - - override testE2EDev(project: Project, argv: TestArgv, { playwrightArgs, startCommand }: TestE2EDevOptions): string { - const port = project.env.PORT || '8080'; - const defaultStartCommand = `blitz dev -p ${port}${toDevNull(argv)}`; - return super.testE2EDev(project, argv, { playwrightArgs, startCommand: startCommand ?? defaultStartCommand }); - } - - override async testStart(project: Project, argv: ScriptArgv): Promise { - const port = await findAvailablePort(); - return `WB_ENV=${project.env.WB_ENV} YARN concurrently --kill-others --raw --success first "blitz dev -p ${port}${toDevNull(argv)}" "${this.waitApp(project, argv, port)}"`; + protected override startDevProtected(_: Project, argv: ScriptArgv): string { + return `blitz dev ${argv.normalizedArgsText ?? ''}`; } } diff --git a/packages/wb/src/scripts/execution/httpServerScripts.ts b/packages/wb/src/scripts/execution/httpServerScripts.ts index 8fd28761..27f7fa8a 100644 --- a/packages/wb/src/scripts/execution/httpServerScripts.ts +++ b/packages/wb/src/scripts/execution/httpServerScripts.ts @@ -1,12 +1,6 @@ -import type { TestArgv } from '../../commands/test.js'; import type { Project } from '../../project.js'; -import { findAvailablePort } from '../../utils/findPort.js'; -import { runtimeWithArgs } from '../../utils/runtime.js'; import type { ScriptArgv } from '../builder.js'; -import { toDevNull } from '../builder.js'; -import { dockerScripts } from '../dockerScripts.js'; -import type { TestE2EDevOptions, TestE2EOptions } from './baseScripts.js'; import { BaseScripts } from './baseScripts.js'; /** @@ -14,63 +8,12 @@ import { BaseScripts } from './baseScripts.js'; * Note that `YARN zzz` is replaced with `yarn zzz` or `node_modules/.bin/zzz`. */ class HttpServerScripts extends BaseScripts { - override start(project: Project, argv: ScriptArgv): string { - const port = Number(project.env.PORT) || 3000; - return `PORT=${port} YARN build-ts run ${argv.watch ? '--watch' : ''} src/index.ts -- ${argv.normalizedArgsText ?? ''}`; + constructor() { + super(false); } - override startDocker(project: Project, argv: ScriptArgv): string { - return `${this.buildDocker(project)}${toDevNull(argv)} && ${dockerScripts.stopAndStart( - project, - false, - argv.normalizedDockerOptionsText ?? '', - argv.normalizedArgsText ?? '' - )}`; - } - - override startProduction(project: Project, argv: ScriptArgv, port = 8080): string { - return `NODE_ENV=production ${ - project.buildCommand - }${toDevNull(argv)} && NODE_ENV=production PORT=\${PORT:-${port}} ${runtimeWithArgs} dist/index.js ${argv.normalizedArgsText ?? ''}`; - } - - override startTest(project: Project, argv: ScriptArgv): string { - return `${project.hasPrisma ? 'prisma migrate reset --force --skip-generate && ' : ''}(${this.startProduction( - project, - argv - )})`; - } - - override testE2E(project: Project, argv: TestArgv, options: TestE2EOptions = {}): string { - const { - startCommand = `${project.hasPrisma ? 'prisma migrate reset --force --skip-generate && ' : ''}(${this.startProduction( - project, - argv - )})`, - } = options; - const port = project.env.PORT || '8080'; - const suffix = project.packageJson.scripts?.['test/e2e-additional'] ? ' && YARN test/e2e-additional' : ''; - const testTarget = argv.targets && argv.targets.length > 0 ? argv.targets.join(' ') : 'test/e2e/'; - return `NODE_ENV=production WB_ENV=${project.env.WB_ENV} PORT=${port} YARN concurrently --kill-others --raw --success first - "${startCommand} && exit 1" - "wait-on -t 600000 -i 2000 http-get://127.0.0.1:${port} && vitest run ${testTarget} --color --passWithNoTests --allowOnly${suffix}"`; - } - - override testE2EDev(project: Project, argv: TestArgv, options: TestE2EDevOptions = {}): string { - const { startCommand } = options; - const port = project.env.PORT || '8080'; - const suffix = project.packageJson.scripts?.['test/e2e-additional'] ? ' && YARN test/e2e-additional' : ''; - const testTarget = argv.targets && argv.targets.length > 0 ? argv.targets.join(' ') : 'test/e2e/'; - return `NODE_ENV=production WB_ENV=${ - project.env.WB_ENV - } PORT=${port} YARN concurrently --kill-others --raw --success first - "${startCommand ?? this.start(project, argv)} && exit 1" - "wait-on -t 600000 -i 2000 http-get://127.0.0.1:${port} && vitest run ${testTarget} --color --passWithNoTests --allowOnly${suffix}"`; - } - - override async testStart(project: Project, argv: ScriptArgv): Promise { - const port = await findAvailablePort(); - return `WB_ENV=${project.env.WB_ENV} PORT=${port} YARN concurrently --kill-others --raw --success first "${this.start(project, argv)}" "${this.waitApp(project, argv, port)}"`; + protected override startDevProtected(_: Project, argv: ScriptArgv): string { + return `YARN build-ts run ${argv.watch ? '--watch' : ''} src/index.ts -- ${argv.normalizedArgsText ?? ''}`; } } diff --git a/packages/wb/src/scripts/execution/nextScripts.ts b/packages/wb/src/scripts/execution/nextScripts.ts index 8cb36603..e8e85fc2 100644 --- a/packages/wb/src/scripts/execution/nextScripts.ts +++ b/packages/wb/src/scripts/execution/nextScripts.ts @@ -1,11 +1,6 @@ -import type { TestArgv } from '../../commands/test.js'; import type { Project } from '../../project.js'; -import { findAvailablePort } from '../../utils/findPort.js'; import type { ScriptArgv } from '../builder.js'; -import { toDevNull } from '../builder.js'; -import { prismaScripts } from '../prismaScripts.js'; -import type { TestE2EDevOptions, TestE2EOptions } from './baseScripts.js'; import { BaseScripts } from './baseScripts.js'; /** @@ -13,58 +8,12 @@ import { BaseScripts } from './baseScripts.js'; * Note that `YARN zzz` is replaced with `yarn zzz` or `node_modules/.bin/zzz`. */ class NextScripts extends BaseScripts { - override start(project: Project, argv: ScriptArgv): string { - const port = Number(project.env.PORT) || 3000; - return `YARN concurrently --raw --kill-others-on-fail - "next dev --turbopack -p ${port} ${argv.normalizedArgsText ?? ''}" - "${this.waitAndOpenApp(project, argv, port)}"`; + constructor() { + super(true); } - override startProduction(project: Project, argv: ScriptArgv, port: number): string { - return `NODE_ENV=production YARN concurrently --raw --kill-others-on-fail - "${[ - ...(project.hasPrisma ? [prismaScripts.migrate(project)] : []), - project.buildCommand, - `PORT=${port} pm2-runtime start ${project.findFile('ecosystem.config.cjs')} ${argv.normalizedArgsText ?? ''}`, - ].join(' && ')}" - "${this.waitAndOpenApp(project, argv, port)}"`; - } - - override startTest(project: Project, argv: ScriptArgv): string { - return [ - ...(project.hasPrisma ? prismaScripts.reset(project).split('&&') : []), - project.buildCommand, - `pm2-runtime start ${project.findFile('ecosystem.config.cjs')}`, - ] - .map((command) => `${command.trim()}${toDevNull(argv)}`) - .join(' && '); - } - - override testE2E(project: Project, argv: TestArgv, options: TestE2EOptions): string { - return super.testE2E(project, argv, { - playwrightArgs: options.playwrightArgs, - prismaDirectory: 'db', - startCommand: - options.startCommand ?? - [ - ...(project.hasPrisma ? prismaScripts.reset(project).split('&&') : []), - project.buildCommand, - `pm2-runtime start ${project.findFile('ecosystem.config.cjs')}`, - ] - .map((c) => `${c.trim()}${toDevNull(argv)}`) - .join(' && '), - }); - } - - override testE2EDev(project: Project, argv: TestArgv, { startCommand }: TestE2EDevOptions): string { - const port = project.env.PORT || '8080'; - const defaultStartCommand = `next dev --turbopack -p ${port}${toDevNull(argv)}`; - return super.testE2EDev(project, argv, { startCommand: startCommand ?? defaultStartCommand }); - } - - override async testStart(project: Project, argv: ScriptArgv): Promise { - const port = await findAvailablePort(); - return `WB_ENV=${project.env.WB_ENV} YARN concurrently --kill-others --raw --success first "next dev --turbopack -p ${port}${toDevNull(argv)}" "${this.waitApp(project, argv, port)}"`; + protected override startDevProtected(_: Project, argv: ScriptArgv): string { + return `next dev --turbopack ${argv.normalizedArgsText ?? ''}`; } } diff --git a/packages/wb/src/scripts/execution/plainAppScripts.ts b/packages/wb/src/scripts/execution/plainAppScripts.ts index 882a07bd..634e020a 100644 --- a/packages/wb/src/scripts/execution/plainAppScripts.ts +++ b/packages/wb/src/scripts/execution/plainAppScripts.ts @@ -1,7 +1,6 @@ import type { Project } from '../../project.js'; import { runtimeWithArgs } from '../../utils/runtime.js'; import type { ScriptArgv } from '../builder.js'; -import { toDevNull } from '../builder.js'; import { dockerScripts } from '../dockerScripts.js'; import { BaseScripts } from './baseScripts.js'; @@ -11,38 +10,47 @@ import { BaseScripts } from './baseScripts.js'; * Note that `YARN zzz` is replaced with `yarn zzz` or `node_modules/.bin/zzz`. */ class PlainAppScripts extends BaseScripts { - override start(project: Project, argv: ScriptArgv): string { - const port = Number(project.env.PORT) || 3000; - return `PORT=${port} YARN build-ts run ${argv.watch ? '--watch' : ''} src/index.ts -- ${argv.normalizedArgsText ?? ''}`; + constructor() { + super(false); } - override startDocker(project: Project, argv: ScriptArgv): string { - return `${this.buildDocker(project)}${toDevNull(argv)} && ${dockerScripts.stopAndStart( - project, - false, - argv.normalizedDockerOptionsText ?? '', - argv.normalizedArgsText ?? '' - )}`; + protected startDevProtected(_1: Project, _2: ScriptArgv): string { + throw new Error('This method should not be called.'); } - override startProduction(project: Project, argv: ScriptArgv): string { - return `NODE_ENV=production ${project.buildCommand} && NODE_ENV=production ${runtimeWithArgs} dist/index.js ${ - argv.normalizedArgsText ?? '' - }`; + override startDev(_: Project, argv: ScriptArgv): Promise { + return Promise.resolve( + `YARN build-ts run ${argv.watch ? '--watch' : ''} src/index.ts -- ${argv.normalizedArgsText ?? ''}` + ); } - - override startTest(): string { - return `echo 'do nothing.'`; + override startProduction(project: Project, argv: ScriptArgv): Promise { + return Promise.resolve( + `${project.buildCommand} && ${runtimeWithArgs} dist/index.js ${argv.normalizedArgsText ?? ''}` + ); } - - override testE2E(): string { - return `echo 'do nothing.'`; + override startTest(): Promise { + return Promise.resolve(`echo 'do nothing.'`); } - - override testE2EDev(): string { - return `echo 'do nothing.'`; + override startDocker(project: Project, argv: ScriptArgv): Promise { + return Promise.resolve( + `${this.buildDocker(project, 'development')} && ${dockerScripts.stopAndStart( + project, + false, + argv.normalizedDockerOptionsText ?? '', + argv.normalizedArgsText ?? '' + )}` + ); } + override testE2EDev(): Promise { + return Promise.resolve(`echo 'do nothing.'`); + } + override testE2EProduction(): Promise { + return Promise.resolve(`echo 'do nothing.'`); + } + override testE2EDocker(): Promise { + return Promise.resolve(`echo 'do nothing.'`); + } override testStart(): Promise { return Promise.resolve(`echo 'do nothing.'`); } diff --git a/packages/wb/src/scripts/execution/remixScripts.ts b/packages/wb/src/scripts/execution/remixScripts.ts index 047b761d..6a24c62d 100644 --- a/packages/wb/src/scripts/execution/remixScripts.ts +++ b/packages/wb/src/scripts/execution/remixScripts.ts @@ -1,11 +1,6 @@ -import type { TestArgv } from '../../commands/test.js'; import type { Project } from '../../project.js'; -import { findAvailablePort } from '../../utils/findPort.js'; import type { ScriptArgv } from '../builder.js'; -import { toDevNull } from '../builder.js'; -import { prismaScripts } from '../prismaScripts.js'; -import type { TestE2EDevOptions, TestE2EOptions } from './baseScripts.js'; import { BaseScripts } from './baseScripts.js'; /** @@ -13,58 +8,12 @@ import { BaseScripts } from './baseScripts.js'; * Note that `YARN zzz` is replaced with `yarn zzz` or `node_modules/.bin/zzz`. */ class RemixScripts extends BaseScripts { - override start(project: Project, argv: ScriptArgv): string { - const port = Number(project.env.PORT) || 3000; - return `PORT=${port} YARN concurrently --raw --kill-others-on-fail - "remix dev ${argv.normalizedArgsText ?? ''}" - "${this.waitAndOpenApp(project, argv, port)}"`; + constructor() { + super(true); } - override startProduction(project: Project, argv: ScriptArgv, port: number): string { - return `NODE_ENV=production YARN concurrently --raw --kill-others-on-fail - "${prismaScripts.migrate(project)} && ${project.buildCommand} && PORT=${port} pm2-runtime start ${project.findFile( - 'ecosystem.config.cjs' - )} ${argv.normalizedArgsText ?? ''}" - "${this.waitAndOpenApp(project, argv, port)}"`; - } - - override startTest(project: Project, argv: ScriptArgv): string { - return [ - ...prismaScripts.reset(project).split('&&'), - project.buildCommand, - `pm2-runtime start ${project.findFile('ecosystem.config.cjs')}`, - ] - .map((command) => `${command.trim()}${toDevNull(argv)}`) - .join(' && '); - } - - override testE2E(project: Project, argv: TestArgv, options: TestE2EOptions): string { - return super.testE2E(project, argv, { - playwrightArgs: options.playwrightArgs, - prismaDirectory: 'prisma', - startCommand: - options.startCommand ?? - [ - ...prismaScripts.reset(project).split('&&'), - project.buildCommand, - `pm2-runtime start ${project.findFile('ecosystem.config.cjs')}`, - ] - .map((c) => `${c.trim()}${toDevNull(argv)}`) - .join(' && '), - }); - } - - override testE2EDev( - project: Project, - argv: TestArgv, - { playwrightArgs, startCommand = `remix dev${toDevNull(argv)}` }: TestE2EDevOptions - ): string { - return super.testE2EDev(project, argv, { playwrightArgs, startCommand }); - } - - override async testStart(project: Project, argv: ScriptArgv): Promise { - const port = await findAvailablePort(); - return `WB_ENV=${project.env.WB_ENV} PORT=${port} YARN concurrently --kill-others --raw --success first "remix dev${toDevNull(argv)}" "${this.waitApp(project, argv, port)}"`; + protected override startDevProtected(_: Project, argv: ScriptArgv): string { + return `remix dev ${argv.normalizedArgsText ?? ''}`; } } diff --git a/packages/wb/src/scripts/run.ts b/packages/wb/src/scripts/run.ts index 3dafb5f6..4984b14a 100644 --- a/packages/wb/src/scripts/run.ts +++ b/packages/wb/src/scripts/run.ts @@ -4,11 +4,6 @@ import type { ArgumentsCamelCase, InferredOptionTypes } from 'yargs'; import type { Project } from '../project.js'; import type { sharedOptionsBuilder } from '../sharedOptionsBuilder.js'; -import { - killPortProcessImmediatelyAndOnExit, - stopDockerContainerByPort, - stopDockerContainerByImageName, -} from '../utils/process.js'; import { promisePool } from '../utils/promisePool.js'; import { isRunningOnBun, packageManagerWithRun } from '../utils/runtime.js'; @@ -39,14 +34,6 @@ export async function runWithSpawn( return 0; } - const port = /http-get:\/\/127.0.0.1:(\d+)/.exec(runnableScript)?.[1]; - if (runnableScript.includes('wait-on') && port) { - if (runnableScript.includes('docker run')) { - await stopDockerContainerByPort(Number(port), project); - await stopDockerContainerByImageName(project.dockerImageName, project); - } - await killPortProcessImmediatelyAndOnExit(Number(port)); - } const ret = await spawnAsync(runnableScript, undefined, { cwd: project.dirPath, env: configureEnv(project.env, opts), @@ -133,7 +120,7 @@ function normalizeScript(script: string, project: Project): [string, string] { ); // Add cascade option when WB_ENV is defined const cascadeOption = project.env.WB_ENV ? ` --cascade-env=${project.env.WB_ENV || 'development'}` : ''; - return [`dotenv${cascadeOption} ${printableScript}`, runnableScript]; + return [`${packageManagerWithRun} dotenv${cascadeOption} -- ${printableScript}`, runnableScript]; } export function printStart(normalizedScript: string, project: Project, prefix = 'Start', weak = false): void { diff --git a/packages/wb/src/utils/process.ts b/packages/wb/src/utils/process.ts index 142d4e6c..198e72ac 100644 --- a/packages/wb/src/utils/process.ts +++ b/packages/wb/src/utils/process.ts @@ -8,20 +8,22 @@ import { printFinishedAndExitIfNeeded, printStart } from '../scripts/run.js'; const killed = new Set(); -export async function killPortProcessImmediatelyAndOnExit(port: number): Promise { - await killPortProcessHandlingErrors(port); +export async function killPortProcessImmediatelyAndOnExit(port: number, project: Project): Promise { + await killPortContainerAndProcess(port, project); const killFunc = async (): Promise => { if (killed.has(port)) return; killed.add(port); - await killPortProcessHandlingErrors(port); + await killPortContainerAndProcess(port, project); }; for (const signal of ['beforeExit', 'SIGINT', 'SIGTERM', 'SIGQUIT']) { process.on(signal, killFunc); } } -async function killPortProcessHandlingErrors(port: number): Promise { +async function killPortContainerAndProcess(port: number, project: Project): Promise { + // We should stop Docker containers first because `kill-port` may fail to stop Docker containers. + await stopDockerContainerByPort(port, project); try { await killPortProcess(port); } catch { From 3bf038b3e6efd60f9de193f99fe6aa2fcd2a5475 Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Sun, 23 Nov 2025 16:48:29 +0900 Subject: [PATCH 02/15] . --- packages/wb/src/utils/log.ts | 1 + packages/wb/src/utils/port.ts | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 packages/wb/src/utils/log.ts create mode 100644 packages/wb/src/utils/port.ts diff --git a/packages/wb/src/utils/log.ts b/packages/wb/src/utils/log.ts new file mode 100644 index 00000000..a6b2e758 --- /dev/null +++ b/packages/wb/src/utils/log.ts @@ -0,0 +1 @@ +export const SERVER_LOG_FILE = 'server.log'; diff --git a/packages/wb/src/utils/port.ts b/packages/wb/src/utils/port.ts new file mode 100644 index 00000000..03993dbb --- /dev/null +++ b/packages/wb/src/utils/port.ts @@ -0,0 +1,11 @@ +import type { Project } from '../project.js'; + +import { killPortProcessImmediatelyAndOnExit } from './process.js'; + +export async function checkAndKillPortProcess(rawPort: unknown, project: Project): Promise { + const port = Number(rawPort); + if (!port) throw new Error(`The given port (${port}) is invalid.`); + + await killPortProcessImmediatelyAndOnExit(port, project); + return port; +} From 647ff2cebd9c2949dddc2ce320738fd55f38359d Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Sun, 23 Nov 2025 16:54:34 +0900 Subject: [PATCH 03/15] test: update BaseScripts e2e commands Co-authored-by: WillBooster (Codex CLI) --- .../scripts/execution/baseScripts.test.ts | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/packages/wb/test/scripts/execution/baseScripts.test.ts b/packages/wb/test/scripts/execution/baseScripts.test.ts index 769beecb..f041dfa0 100644 --- a/packages/wb/test/scripts/execution/baseScripts.test.ts +++ b/packages/wb/test/scripts/execution/baseScripts.test.ts @@ -1,24 +1,25 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import type { TestArgv } from '../../../src/commands/test.js'; import type { Project } from '../../../src/project.js'; +import type { ScriptArgv } from '../../../src/scripts/builder.js'; import { BaseScripts } from '../../../src/scripts/execution/baseScripts.js'; -class TestScripts extends BaseScripts { - override start(): string { - return ''; - } +vi.mock('../../../src/utils/port.js', () => ({ + checkAndKillPortProcess: vi.fn().mockResolvedValue(3000), +})); - override startProduction(): string { - return ''; +class TestScripts extends BaseScripts { + constructor() { + super(false); } - override startTest(): string { - return ''; + protected startDevProtected(_: Project, _argv: ScriptArgv): string { + return 'start-dev'; } - override testStart(): Promise { - return Promise.resolve(''); + protected override startProductionProtected(_: Project): string { + return 'start-production'; } } @@ -31,18 +32,18 @@ describe('BaseScripts.testE2E', () => { const scripts = new TestScripts(); - it('uses default target when none specified', () => { - const command = scripts.testE2E(project, {} as TestArgv, {}); + it('uses default target when none specified', async () => { + const command = await scripts.testE2EProduction(project, {} as TestArgv, {}); expect(command).toContain('BUN playwright test test/e2e/'); }); - it('passes custom target to playwright', () => { - const command = scripts.testE2E(project, { targets: ['test/e2e/topPage.spec.ts'] } as TestArgv, {}); + it('passes custom target to playwright', async () => { + const command = await scripts.testE2EProduction(project, { targets: ['test/e2e/topPage.spec.ts'] } as TestArgv, {}); expect(command).toContain('BUN playwright test test/e2e/topPage.spec.ts'); }); - it('keeps additional playwright args when replacing target', () => { - const command = scripts.testE2E(project, { targets: ['test/e2e/topPage.spec.ts'] } as TestArgv, { + it('keeps additional playwright args when replacing target', async () => { + const command = await scripts.testE2EProduction(project, { targets: ['test/e2e/topPage.spec.ts'] } as TestArgv, { playwrightArgs: 'test test/e2e/ --headed', }); expect(command).toContain('BUN playwright test test/e2e/topPage.spec.ts --headed'); From 01285a0a891f20172bd28cf94989d48e8252e7fb Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Sun, 23 Nov 2025 16:55:01 +0900 Subject: [PATCH 04/15] . --- packages/wb/src/scripts/execution/plainAppScripts.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/wb/src/scripts/execution/plainAppScripts.ts b/packages/wb/src/scripts/execution/plainAppScripts.ts index 634e020a..a439bdba 100644 --- a/packages/wb/src/scripts/execution/plainAppScripts.ts +++ b/packages/wb/src/scripts/execution/plainAppScripts.ts @@ -1,4 +1,5 @@ import type { Project } from '../../project.js'; +import { SERVER_LOG_FILE } from '../../utils/log.js'; import { runtimeWithArgs } from '../../utils/runtime.js'; import type { ScriptArgv } from '../builder.js'; import { dockerScripts } from '../dockerScripts.js'; @@ -25,7 +26,7 @@ class PlainAppScripts extends BaseScripts { } override startProduction(project: Project, argv: ScriptArgv): Promise { return Promise.resolve( - `${project.buildCommand} && ${runtimeWithArgs} dist/index.js ${argv.normalizedArgsText ?? ''}` + `${project.buildCommand} && ${runtimeWithArgs} dist/index.js ${argv.normalizedArgsText ?? ''} | tee ${SERVER_LOG_FILE}` ); } override startTest(): Promise { From 0c7af3abfa3fcf51e790e1c2f87bd5ba2e5f7eda Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Sun, 23 Nov 2025 18:25:08 +0900 Subject: [PATCH 05/15] test: set ports in env fixtures Co-authored-by: WillBooster (Codex CLI) --- packages/shared-lib-node/test/env.test.ts | 10 +++++----- packages/shared-lib-node/test/fixtures/app1/.env | 1 + .../test/fixtures/app1/.env.development | 1 + .../shared-lib-node/test/fixtures/app1/.env.production | 1 + packages/shared-lib-node/test/fixtures/app1/.env.test | 1 + packages/shared-lib-node/test/fixtures/app2/.env | 1 + .../test/fixtures/app2/.env.development | 1 + .../shared-lib-node/test/fixtures/app2/.env.production | 1 + packages/shared-lib-node/test/fixtures/app2/.env.test | 1 + 9 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/shared-lib-node/test/env.test.ts b/packages/shared-lib-node/test/env.test.ts index e7413e4b..0206c891 100644 --- a/packages/shared-lib-node/test/env.test.ts +++ b/packages/shared-lib-node/test/env.test.ts @@ -15,24 +15,24 @@ describe('readAndApplyEnvironmentVariables()', () => { it('should load env vars with --auto-cascade-env', () => { const envVars = readAndApplyEnvironmentVariables({ autoCascadeEnv: true }, 'test/fixtures/app1'); - expect(envVars).toEqual({ NAME: 'app1', ENV: 'development1' }); + expect(envVars).toEqual({ ENV: 'development1', PORT: '3001', NAME: 'app1' }); }); it('should load env vars with --cascade-env=production', () => { const envVars = readAndApplyEnvironmentVariables({ cascadeEnv: 'production', env: ['.env'] }, 'test/fixtures/app1'); - expect(envVars).toEqual({ NAME: 'app1', ENV: 'production1' }); + expect(envVars).toEqual({ ENV: 'production1', PORT: '3003', NAME: 'app1' }); }); it('should load env vars with --cascade-node-env and NODE_ENV=""', () => { process.env.NODE_ENV = ''; const envVars = readAndApplyEnvironmentVariables({ cascadeNodeEnv: true, env: ['.env'] }, 'test/fixtures/app1'); - expect(envVars).toEqual({ NAME: 'app1', ENV: 'development1' }); + expect(envVars).toEqual({ ENV: 'development1', PORT: '3001', NAME: 'app1' }); }); it('should load env vars with --cascade-node-env and NODE_ENV=test', () => { process.env.NODE_ENV = 'test'; const envVars = readAndApplyEnvironmentVariables({ cascadeNodeEnv: true, env: ['.env'] }, 'test/fixtures/app1'); - expect(envVars).toEqual({ NAME: 'app1', ENV: 'test1' }); + expect(envVars).toEqual({ ENV: 'test1', PORT: '3002', NAME: 'app1' }); }); it('should load env vars with --env=test/fixtures/app2/.env --auto-cascade-env, WB_ENV=test and NODE_ENV=production', () => { @@ -42,6 +42,6 @@ describe('readAndApplyEnvironmentVariables()', () => { { autoCascadeEnv: true, env: ['../app2/.env'] }, 'test/fixtures/app1' ); - expect(envVars).toEqual({ NAME: 'app2', ENV: 'test2' }); + expect(envVars).toEqual({ ENV: 'test2', PORT: '4002', NAME: 'app2' }); }); }); diff --git a/packages/shared-lib-node/test/fixtures/app1/.env b/packages/shared-lib-node/test/fixtures/app1/.env index b9759296..dee2b55e 100644 --- a/packages/shared-lib-node/test/fixtures/app1/.env +++ b/packages/shared-lib-node/test/fixtures/app1/.env @@ -1 +1,2 @@ NAME=app1 +PORT=3000 diff --git a/packages/shared-lib-node/test/fixtures/app1/.env.development b/packages/shared-lib-node/test/fixtures/app1/.env.development index 9fab8e9d..61b494f1 100644 --- a/packages/shared-lib-node/test/fixtures/app1/.env.development +++ b/packages/shared-lib-node/test/fixtures/app1/.env.development @@ -1 +1,2 @@ ENV=development1 +PORT=3001 diff --git a/packages/shared-lib-node/test/fixtures/app1/.env.production b/packages/shared-lib-node/test/fixtures/app1/.env.production index 034ce22c..51c37d74 100644 --- a/packages/shared-lib-node/test/fixtures/app1/.env.production +++ b/packages/shared-lib-node/test/fixtures/app1/.env.production @@ -1 +1,2 @@ ENV=production1 +PORT=3003 diff --git a/packages/shared-lib-node/test/fixtures/app1/.env.test b/packages/shared-lib-node/test/fixtures/app1/.env.test index cb99e73b..0dd339e8 100644 --- a/packages/shared-lib-node/test/fixtures/app1/.env.test +++ b/packages/shared-lib-node/test/fixtures/app1/.env.test @@ -1 +1,2 @@ ENV=test1 +PORT=3002 diff --git a/packages/shared-lib-node/test/fixtures/app2/.env b/packages/shared-lib-node/test/fixtures/app2/.env index 59707862..cef52fc0 100644 --- a/packages/shared-lib-node/test/fixtures/app2/.env +++ b/packages/shared-lib-node/test/fixtures/app2/.env @@ -1 +1,2 @@ NAME=app2 +PORT=4000 diff --git a/packages/shared-lib-node/test/fixtures/app2/.env.development b/packages/shared-lib-node/test/fixtures/app2/.env.development index 4a144328..987829f1 100644 --- a/packages/shared-lib-node/test/fixtures/app2/.env.development +++ b/packages/shared-lib-node/test/fixtures/app2/.env.development @@ -1 +1,2 @@ ENV=development2 +PORT=4001 diff --git a/packages/shared-lib-node/test/fixtures/app2/.env.production b/packages/shared-lib-node/test/fixtures/app2/.env.production index d6b786f0..55098114 100644 --- a/packages/shared-lib-node/test/fixtures/app2/.env.production +++ b/packages/shared-lib-node/test/fixtures/app2/.env.production @@ -1 +1,2 @@ ENV=production2 +PORT=4003 diff --git a/packages/shared-lib-node/test/fixtures/app2/.env.test b/packages/shared-lib-node/test/fixtures/app2/.env.test index c78438d7..dfaff518 100644 --- a/packages/shared-lib-node/test/fixtures/app2/.env.test +++ b/packages/shared-lib-node/test/fixtures/app2/.env.test @@ -1 +1,2 @@ ENV=test2 +PORT=4002 From 3cc8244b4fc1e5a930ac0d209cba3195e5192319 Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Sun, 23 Nov 2025 18:28:14 +0900 Subject: [PATCH 06/15] . --- .../wb/src/scripts/execution/baseScripts.ts | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/packages/wb/src/scripts/execution/baseScripts.ts b/packages/wb/src/scripts/execution/baseScripts.ts index 07ad8aee..992a2b13 100644 --- a/packages/wb/src/scripts/execution/baseScripts.ts +++ b/packages/wb/src/scripts/execution/baseScripts.ts @@ -28,13 +28,14 @@ export abstract class BaseScripts { // ------------ START: start commands ------------ protected abstract startDevProtected(_: Project, argv: ScriptArgv): string; - protected startProductionProtected(project: Project): string { + protected startProductionProtected(project: Project, argv: ScriptArgv): string { return [ - project.hasPrisma ? prismaScripts.migrate(project) : '', + ...(project.hasPrisma ? prismaScripts.migrate(project).split('&&') : []), project.buildCommand, `pm2-runtime start ${project.findFile('ecosystem.config.cjs')}`, ] .filter(Boolean) + .map((cmd) => `${cmd} ${toDevNull(argv)} &&`) .join(' && '); } @@ -46,18 +47,17 @@ export abstract class BaseScripts { "${this.startDevProtected(project, argv)}" "${this.waitAndOpenApp(project)}"`; } - async startProduction(project: Project, _: ScriptArgv): Promise { + async startProduction(project: Project, argv: ScriptArgv): Promise { await checkAndKillPortProcess(project.env.PORT, project); - if (!this.shouldWaitAndOpenApp) return this.startProductionProtected(project); + if (!this.shouldWaitAndOpenApp) return this.startProductionProtected(project, argv); return `YARN concurrently --raw --kill-others-on-fail - "${this.startProductionProtected(project)}" + "${this.startProductionProtected(project, argv)}" "${this.waitAndOpenApp(project)}"`; } async startTest(project: Project, argv: ScriptArgv): Promise { await checkAndKillPortProcess(project.env.PORT, project); - // We configure playwright to discard stdout, so startTest() also discards stdout for the consistency. - return this.startProductionProtected(project).replaceAll('&&', `${toDevNull(argv)} &&`); + return this.startProductionProtected(project, argv); } async startDocker(project: Project, argv: ScriptArgv): Promise { await checkAndKillPortProcess(project.env.PORT, project); @@ -88,12 +88,7 @@ export abstract class BaseScripts { return this.testE2EPrivate(project, argv, this.startDevProtected(project, argv), options); } testE2EProduction(project: Project, argv: TestArgv, options: TestE2EOptions): Promise { - return this.testE2EPrivate( - project, - argv, - this.startProductionProtected(project).replaceAll('&&', `${toDevNull(argv)} &&`), - options - ); + return this.testE2EPrivate(project, argv, this.startProductionProtected(project, argv), options); } testE2EDocker(project: Project, argv: TestArgv, options: TestE2EOptions): Promise { return this.testE2EPrivate(project, argv, dockerScripts.stopAndStart(project, true), options); From 57af75bd03cd9dc2facc27fcef10c4da691612b7 Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Sun, 23 Nov 2025 18:29:29 +0900 Subject: [PATCH 07/15] . --- packages/wb/src/scripts/execution/baseScripts.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/wb/src/scripts/execution/baseScripts.ts b/packages/wb/src/scripts/execution/baseScripts.ts index 992a2b13..db69a703 100644 --- a/packages/wb/src/scripts/execution/baseScripts.ts +++ b/packages/wb/src/scripts/execution/baseScripts.ts @@ -1,6 +1,5 @@ import type { TestArgv } from '../../commands/test.js'; import type { Project } from '../../project.js'; -import { SERVER_LOG_FILE } from '../../utils/log.js'; import { checkAndKillPortProcess } from '../../utils/port.js'; import type { ScriptArgv } from '../builder.js'; import { toDevNull } from '../builder.js'; @@ -95,7 +94,7 @@ export abstract class BaseScripts { } async testStart(project: Project, argv: ScriptArgv): Promise { await checkAndKillPortProcess(project.env.PORT, project); - return `YARN concurrently --kill-others --raw --success first "${this.startDevProtected(project, argv)} > ${SERVER_LOG_FILE} 2>&1" "${this.waitApp(project)}"`; + return `YARN concurrently --kill-others --raw --success first "${this.startDevProtected(project, argv)}" "${this.waitApp(project)}"`; } private async testE2EPrivate( From 8b1d0abaf81019524a9ca8e3cb44fd3f83f559c8 Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Sun, 23 Nov 2025 18:37:00 +0900 Subject: [PATCH 08/15] . --- packages/wb/src/commands/test.ts | 3 ++- packages/wb/src/commands/testOnCi.ts | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/wb/src/commands/test.ts b/packages/wb/src/commands/test.ts index 9c05afdd..7ae121a1 100644 --- a/packages/wb/src/commands/test.ts +++ b/packages/wb/src/commands/test.ts @@ -7,6 +7,7 @@ import type { ArgumentsCamelCase, CommandModule, InferredOptionTypes } from 'yar import type { Project } from '../project.js'; import { findDescendantProjects } from '../project.js'; import type { scriptOptionsBuilder } from '../scripts/builder.js'; +import { toDevNull } from '../scripts/builder.js'; import { dockerScripts } from '../scripts/dockerScripts.js'; import type { BaseScripts } from '../scripts/execution/baseScripts.js'; import { blitzScripts } from '../scripts/execution/blitzScripts.js'; @@ -201,7 +202,7 @@ async function testOnDocker( playwrightArgs?: string ): Promise { project.env.WB_DOCKER ||= '1'; - await runWithSpawn(scripts.buildDocker(project, 'test'), project, argv); + await runWithSpawn(`${scripts.buildDocker(project, 'test')}${toDevNull(argv)}`, project, argv); process.exitCode = await runWithSpawn( await scripts.testE2EDocker(project, argv, { playwrightArgs, diff --git a/packages/wb/src/commands/testOnCi.ts b/packages/wb/src/commands/testOnCi.ts index 76361d9c..ed6af5b1 100644 --- a/packages/wb/src/commands/testOnCi.ts +++ b/packages/wb/src/commands/testOnCi.ts @@ -19,7 +19,12 @@ import { promisePool } from '../utils/promisePool.js'; import { httpServerPackages } from './httpServerPackages.js'; -const testOnCiBuilder = {} as const; +const testOnCiBuilder = { + silent: { + description: 'Reduce redundant outputs', + type: 'boolean', + }, +} as const; export const testOnCiCommand: CommandModule< unknown, InferredOptionTypes @@ -73,7 +78,6 @@ export async function testOnCi( if (fs.existsSync(path.join(project.dirPath, 'test', 'e2e'))) { if (project.hasDockerfile) { project.env.WB_DOCKER ||= '1'; - // Discard Docker build output to reduce log size on CI await runWithSpawn(`${scripts.buildDocker(project, 'test')}${toDevNull(argv)}`, project, argv); } const script = project.hasDockerfile From faad4061d33a583b487ef95ce6ff16ad0ce1ba15 Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Sun, 23 Nov 2025 19:14:37 +0900 Subject: [PATCH 09/15] . --- packages/wb/src/scripts/dockerScripts.ts | 2 +- packages/wb/src/utils/findPort.ts | 43 ------------------------ packages/wb/src/utils/port.ts | 32 ++++++++++++++++++ packages/wb/src/utils/process.ts | 7 +++- 4 files changed, 39 insertions(+), 45 deletions(-) delete mode 100644 packages/wb/src/utils/findPort.ts diff --git a/packages/wb/src/scripts/dockerScripts.ts b/packages/wb/src/scripts/dockerScripts.ts index c81a9f47..8002b23f 100644 --- a/packages/wb/src/scripts/dockerScripts.ts +++ b/packages/wb/src/scripts/dockerScripts.ts @@ -31,7 +31,7 @@ class DockerScripts { start(project: Project, additionalOptions = '', additionalArgs = ''): string { spawnSyncOnExit(this.stop(project), project); - return `docker run --rm -it -p \${PORT:-8080}:8080 --name ${project.dockerImageName} ${additionalOptions} ${project.dockerImageName} ${additionalArgs}`; + return `docker run --rm -it --publish \${PORT:-8080}:8080 --name ${project.dockerImageName} ${additionalOptions} ${project.dockerImageName} ${additionalArgs}`; } stop(project: Project): string { diff --git a/packages/wb/src/utils/findPort.ts b/packages/wb/src/utils/findPort.ts deleted file mode 100644 index edab2edb..00000000 --- a/packages/wb/src/utils/findPort.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { createServer } from 'node:net'; - -/** - * Finds an available port starting from the given port number. - * @param startPort - The port number to start searching from (default: 3000) - * @param maxAttempts - Maximum number of ports to try (default: 100) - * @returns A promise that resolves to an available port number - */ -export async function findAvailablePort(startPort = 3000, maxAttempts = 100): Promise { - for (let port = startPort; port < startPort + maxAttempts; port++) { - if (await isPortAvailable(port)) { - return port; - } - } - throw new Error(`No available port found in range ${startPort}-${startPort + maxAttempts - 1}`); -} - -/** - * Checks if a port is available. - * @param port - The port number to check - * @returns A promise that resolves to true if the port is available, false otherwise - */ -async function isPortAvailable(port: number): Promise { - return new Promise((resolve) => { - const server = createServer(); - - server.once('error', (err: NodeJS.ErrnoException) => { - if (err.code === 'EADDRINUSE') { - resolve(false); - } else { - resolve(false); - } - }); - - server.once('listening', () => { - server.close(() => { - resolve(true); - }); - }); - - server.listen(port, '127.0.0.1'); - }); -} diff --git a/packages/wb/src/utils/port.ts b/packages/wb/src/utils/port.ts index 03993dbb..d7037567 100644 --- a/packages/wb/src/utils/port.ts +++ b/packages/wb/src/utils/port.ts @@ -1,7 +1,12 @@ +import { createServer } from 'node:net'; + import type { Project } from '../project.js'; import { killPortProcessImmediatelyAndOnExit } from './process.js'; +/** + * Checks the given port and kills any process using it. + */ export async function checkAndKillPortProcess(rawPort: unknown, project: Project): Promise { const port = Number(rawPort); if (!port) throw new Error(`The given port (${port}) is invalid.`); @@ -9,3 +14,30 @@ export async function checkAndKillPortProcess(rawPort: unknown, project: Project await killPortProcessImmediatelyAndOnExit(port, project); return port; } + +/** + * Checks if a port is available. + * @param port - The port number to check + * @returns A promise that resolves to true if the port is available, false otherwise + */ +export async function isPortAvailable(port: number): Promise { + return new Promise((resolve) => { + const server = createServer(); + + server.once('error', (err: NodeJS.ErrnoException) => { + if (err.code === 'EADDRINUSE') { + resolve(false); + } else { + resolve(false); + } + }); + + server.once('listening', () => { + server.close(() => { + resolve(true); + }); + }); + + server.listen(port, '127.0.0.1'); + }); +} diff --git a/packages/wb/src/utils/process.ts b/packages/wb/src/utils/process.ts index 198e72ac..a79646bf 100644 --- a/packages/wb/src/utils/process.ts +++ b/packages/wb/src/utils/process.ts @@ -6,10 +6,15 @@ import killPortProcess from 'kill-port'; import type { Project } from '../project.js'; import { printFinishedAndExitIfNeeded, printStart } from '../scripts/run.js'; +import { isPortAvailable } from './port.js'; + const killed = new Set(); export async function killPortProcessImmediatelyAndOnExit(port: number, project: Project): Promise { - await killPortContainerAndProcess(port, project); + if (!(await isPortAvailable(port))) { + await killPortContainerAndProcess(port, project); + } + const killFunc = async (): Promise => { if (killed.has(port)) return; From 76be9f68544bc1b9e763490bdb3584537b526df3 Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Sun, 23 Nov 2025 22:43:58 +0900 Subject: [PATCH 10/15] . --- packages/shared-lib-node/package.json | 1 + packages/shared-lib-node/src/env.ts | 3 ++- packages/wb/src/scripts/dockerScripts.ts | 2 +- yarn.lock | 12 +++++++++++- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/shared-lib-node/package.json b/packages/shared-lib-node/package.json index 8645dc42..8ddb46e8 100644 --- a/packages/shared-lib-node/package.json +++ b/packages/shared-lib-node/package.json @@ -39,6 +39,7 @@ "prettier": "@willbooster/prettier-config", "dependencies": { "dotenv": "17.2.3", + "dotenv-expand": "12.0.3", "tree-kill": "1.2.2" }, "devDependencies": { diff --git a/packages/shared-lib-node/src/env.ts b/packages/shared-lib-node/src/env.ts index 3abd262a..ef5c1c1a 100644 --- a/packages/shared-lib-node/src/env.ts +++ b/packages/shared-lib-node/src/env.ts @@ -2,6 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { config } from 'dotenv'; +import { expand } from 'dotenv-expand'; import type { ArgumentsCamelCase, InferredOptionTypes } from 'yargs'; export const yargsOptionsBuilderForEnv = { @@ -104,7 +105,7 @@ export function readEnvironmentVariables( throw new Error(`Missing environment variables in [${envPaths.join(', ')}]: [${missingKeys.join(', ')}]`); } } - return [envVars, envPathAndEnvVarCountPairs]; + return [expand({ parsed: envVars, processEnv: {} }).parsed ?? envVars, envPathAndEnvVarCountPairs]; } /** diff --git a/packages/wb/src/scripts/dockerScripts.ts b/packages/wb/src/scripts/dockerScripts.ts index 8002b23f..3e9b90d4 100644 --- a/packages/wb/src/scripts/dockerScripts.ts +++ b/packages/wb/src/scripts/dockerScripts.ts @@ -31,7 +31,7 @@ class DockerScripts { start(project: Project, additionalOptions = '', additionalArgs = ''): string { spawnSyncOnExit(this.stop(project), project); - return `docker run --rm -it --publish \${PORT:-8080}:8080 --name ${project.dockerImageName} ${additionalOptions} ${project.dockerImageName} ${additionalArgs}`; + return `docker run --rm -it --publish ${process.env.PORT}:8080 --name ${project.dockerImageName} ${additionalOptions} ${project.dockerImageName} ${additionalArgs}`; } stop(project: Project): string { diff --git a/yarn.lock b/yarn.lock index 66ebc2c4..3727406d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5877,6 +5877,7 @@ __metadata: "@willbooster/prettier-config": "npm:10.2.1" build-ts: "npm:17.0.3" dotenv: "npm:17.2.3" + dotenv-expand: "npm:12.0.3" eslint: "npm:9.39.1" eslint-config-flat-gitignore: "npm:2.1.0" eslint-config-prettier: "npm:10.1.8" @@ -9154,6 +9155,15 @@ __metadata: languageName: node linkType: hard +"dotenv-expand@npm:12.0.3": + version: 12.0.3 + resolution: "dotenv-expand@npm:12.0.3" + dependencies: + dotenv: "npm:^16.4.5" + checksum: 10c0/0824bdc74fc816a28b0744b7853a23e046602e9616c82121dfdae21ebc13c6e89afeb773e794e97c35d48be2be0a990fbca721ee3869a49c04210a58a3cf296f + languageName: node + linkType: hard + "dotenv-expand@npm:8.0.3": version: 8.0.3 resolution: "dotenv-expand@npm:8.0.3" @@ -9182,7 +9192,7 @@ __metadata: languageName: node linkType: hard -"dotenv@npm:^16.6.1": +"dotenv@npm:^16.4.5, dotenv@npm:^16.6.1": version: 16.6.1 resolution: "dotenv@npm:16.6.1" checksum: 10c0/15ce56608326ea0d1d9414a5c8ee6dcf0fffc79d2c16422b4ac2268e7e2d76ff5a572d37ffe747c377de12005f14b3cc22361e79fc7f1061cce81f77d2c973dc From da2fe1ed22a827233e41b27fade190d608f1d6f8 Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Sun, 23 Nov 2025 23:09:18 +0900 Subject: [PATCH 11/15] . --- packages/wb/src/scripts/dockerScripts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wb/src/scripts/dockerScripts.ts b/packages/wb/src/scripts/dockerScripts.ts index 3e9b90d4..ddb774f3 100644 --- a/packages/wb/src/scripts/dockerScripts.ts +++ b/packages/wb/src/scripts/dockerScripts.ts @@ -31,7 +31,7 @@ class DockerScripts { start(project: Project, additionalOptions = '', additionalArgs = ''): string { spawnSyncOnExit(this.stop(project), project); - return `docker run --rm -it --publish ${process.env.PORT}:8080 --name ${project.dockerImageName} ${additionalOptions} ${project.dockerImageName} ${additionalArgs}`; + return `docker run --rm -it --publish ${project.env.PORT}:8080 --name ${project.dockerImageName} ${additionalOptions} ${project.dockerImageName} ${additionalArgs}`; } stop(project: Project): string { From 345f839783e08b6bd28673abcc275823c8b2ced7 Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Sun, 23 Nov 2025 23:27:42 +0900 Subject: [PATCH 12/15] . --- packages/wb/src/scripts/execution/baseScripts.ts | 2 +- packages/wb/src/scripts/run.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/wb/src/scripts/execution/baseScripts.ts b/packages/wb/src/scripts/execution/baseScripts.ts index db69a703..b4a262a3 100644 --- a/packages/wb/src/scripts/execution/baseScripts.ts +++ b/packages/wb/src/scripts/execution/baseScripts.ts @@ -34,7 +34,7 @@ export abstract class BaseScripts { `pm2-runtime start ${project.findFile('ecosystem.config.cjs')}`, ] .filter(Boolean) - .map((cmd) => `${cmd} ${toDevNull(argv)} &&`) + .map((cmd) => `${cmd} ${toDevNull(argv)}`.trim()) .join(' && '); } diff --git a/packages/wb/src/scripts/run.ts b/packages/wb/src/scripts/run.ts index 4984b14a..88aaaa98 100644 --- a/packages/wb/src/scripts/run.ts +++ b/packages/wb/src/scripts/run.ts @@ -96,6 +96,10 @@ function normalizeScript(script: string, project: Project): [string, string] { let newScript = script .replaceAll('\n', '') .replaceAll(/\s\s+/g, ' ') + .replaceAll( + 'PRISMA generate ', + project.packageJson.dependencies?.blitz ? 'PRISMA generate ' : 'PRISMA generate --no-hints ' + ) .replaceAll('PRISMA ', project.packageJson.dependencies?.blitz ? 'YARN blitz prisma ' : 'YARN prisma ') .replaceAll('BUN ', project.isBunAvailable ? 'bun --bun run ' : 'YARN ') // Avoid replacing `YARN run` with `run` by replacing `YARN` with `(yarn|bun --bun) run` From b73ff48477bc0b6f6377ede4795a1a99ce2eedc5 Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Sun, 23 Nov 2025 23:36:26 +0900 Subject: [PATCH 13/15] fix: default port when env is missing Co-authored-by: WillBooster (Codex CLI) --- .../wb/src/scripts/execution/baseScripts.ts | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/wb/src/scripts/execution/baseScripts.ts b/packages/wb/src/scripts/execution/baseScripts.ts index b4a262a3..a7e2e3be 100644 --- a/packages/wb/src/scripts/execution/baseScripts.ts +++ b/packages/wb/src/scripts/execution/baseScripts.ts @@ -39,7 +39,8 @@ export abstract class BaseScripts { } async startDev(project: Project, argv: ScriptArgv): Promise { - await checkAndKillPortProcess(project.env.PORT, project); + const port = this.resolvePort(project, 3000); + await checkAndKillPortProcess(port, project); if (!this.shouldWaitAndOpenApp) return this.startDevProtected(project, argv); return `YARN concurrently --raw --kill-others-on-fail @@ -47,7 +48,8 @@ export abstract class BaseScripts { "${this.waitAndOpenApp(project)}"`; } async startProduction(project: Project, argv: ScriptArgv): Promise { - await checkAndKillPortProcess(project.env.PORT, project); + const port = this.resolvePort(project, 8080); + await checkAndKillPortProcess(port, project); if (!this.shouldWaitAndOpenApp) return this.startProductionProtected(project, argv); return `YARN concurrently --raw --kill-others-on-fail @@ -55,11 +57,13 @@ export abstract class BaseScripts { "${this.waitAndOpenApp(project)}"`; } async startTest(project: Project, argv: ScriptArgv): Promise { - await checkAndKillPortProcess(project.env.PORT, project); + const port = this.resolvePort(project, 8080); + await checkAndKillPortProcess(port, project); return this.startProductionProtected(project, argv); } async startDocker(project: Project, argv: ScriptArgv): Promise { - await checkAndKillPortProcess(project.env.PORT, project); + const port = this.resolvePort(project, 8080); + await checkAndKillPortProcess(port, project); if (!this.shouldWaitAndOpenApp) { return `${this.buildDocker(project, 'development')} && ${dockerScripts.stopAndStart( @@ -93,7 +97,8 @@ export abstract class BaseScripts { return this.testE2EPrivate(project, argv, dockerScripts.stopAndStart(project, true), options); } async testStart(project: Project, argv: ScriptArgv): Promise { - await checkAndKillPortProcess(project.env.PORT, project); + const port = this.resolvePort(project, 3000); + await checkAndKillPortProcess(port, project); return `YARN concurrently --kill-others --raw --success first "${this.startDevProtected(project, argv)}" "${this.waitApp(project)}"`; } @@ -103,7 +108,8 @@ export abstract class BaseScripts { startCommand: string, { playwrightArgs = 'test test/e2e/' }: TestE2EOptions ): Promise { - const port = await checkAndKillPortProcess(project.env.PORT, project); + const port = this.resolvePort(project, 8080); + await checkAndKillPortProcess(port, project); const suffix = project.packageJson.scripts?.['test/e2e-additional'] ? ' && YARN test/e2e-additional' : ''; const playwrightCommand = buildPlaywrightCommand(playwrightArgs, argv.targets); if (project.skipLaunchingServerForPlaywright) { @@ -145,6 +151,15 @@ export abstract class BaseScripts { project )} || wait-on http-get://127.0.0.1:${port} && open-cli http://\${HOST:-localhost}:${port}`; } + + private resolvePort(project: Project, fallbackPort: number): number { + const port = Number(project.env.PORT ?? fallbackPort); + if (!Number.isInteger(port) || port <= 0) { + throw new Error(`The given port (${project.env.PORT}) is invalid.`); + } + project.env.PORT = `${port}`; + return port; + } } function buildPlaywrightCommand(playwrightArgs: string, targets: TestArgv['targets']): string { From be7e5be761dd5dc05d9aebe428276de4fb476bd9 Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Mon, 24 Nov 2025 10:22:37 +0900 Subject: [PATCH 14/15] . --- .../wb/src/scripts/execution/baseScripts.ts | 29 +++++-------------- packages/wb/src/utils/port.ts | 1 + packages/wb/test/setup.test.ts | 2 ++ 3 files changed, 10 insertions(+), 22 deletions(-) diff --git a/packages/wb/src/scripts/execution/baseScripts.ts b/packages/wb/src/scripts/execution/baseScripts.ts index a7e2e3be..3a11cfa9 100644 --- a/packages/wb/src/scripts/execution/baseScripts.ts +++ b/packages/wb/src/scripts/execution/baseScripts.ts @@ -17,7 +17,7 @@ export interface TestE2EOptions { */ export abstract class BaseScripts { private readonly shouldWaitAndOpenApp: boolean; - constructor(shouldWaitAndOpenApp: boolean) { + protected constructor(shouldWaitAndOpenApp: boolean) { this.shouldWaitAndOpenApp = shouldWaitAndOpenApp; } @@ -39,8 +39,7 @@ export abstract class BaseScripts { } async startDev(project: Project, argv: ScriptArgv): Promise { - const port = this.resolvePort(project, 3000); - await checkAndKillPortProcess(port, project); + await checkAndKillPortProcess(project.env.PORT, project); if (!this.shouldWaitAndOpenApp) return this.startDevProtected(project, argv); return `YARN concurrently --raw --kill-others-on-fail @@ -48,8 +47,7 @@ export abstract class BaseScripts { "${this.waitAndOpenApp(project)}"`; } async startProduction(project: Project, argv: ScriptArgv): Promise { - const port = this.resolvePort(project, 8080); - await checkAndKillPortProcess(port, project); + await checkAndKillPortProcess(project.env.PORT, project); if (!this.shouldWaitAndOpenApp) return this.startProductionProtected(project, argv); return `YARN concurrently --raw --kill-others-on-fail @@ -57,13 +55,11 @@ export abstract class BaseScripts { "${this.waitAndOpenApp(project)}"`; } async startTest(project: Project, argv: ScriptArgv): Promise { - const port = this.resolvePort(project, 8080); - await checkAndKillPortProcess(port, project); + await checkAndKillPortProcess(project.env.PORT, project); return this.startProductionProtected(project, argv); } async startDocker(project: Project, argv: ScriptArgv): Promise { - const port = this.resolvePort(project, 8080); - await checkAndKillPortProcess(port, project); + await checkAndKillPortProcess(project.env.PORT, project); if (!this.shouldWaitAndOpenApp) { return `${this.buildDocker(project, 'development')} && ${dockerScripts.stopAndStart( @@ -97,8 +93,7 @@ export abstract class BaseScripts { return this.testE2EPrivate(project, argv, dockerScripts.stopAndStart(project, true), options); } async testStart(project: Project, argv: ScriptArgv): Promise { - const port = this.resolvePort(project, 3000); - await checkAndKillPortProcess(port, project); + await checkAndKillPortProcess(project.env.PORT, project); return `YARN concurrently --kill-others --raw --success first "${this.startDevProtected(project, argv)}" "${this.waitApp(project)}"`; } @@ -108,8 +103,7 @@ export abstract class BaseScripts { startCommand: string, { playwrightArgs = 'test test/e2e/' }: TestE2EOptions ): Promise { - const port = this.resolvePort(project, 8080); - await checkAndKillPortProcess(port, project); + const port = await checkAndKillPortProcess(project.env.PORT, project); const suffix = project.packageJson.scripts?.['test/e2e-additional'] ? ' && YARN test/e2e-additional' : ''; const playwrightCommand = buildPlaywrightCommand(playwrightArgs, argv.targets); if (project.skipLaunchingServerForPlaywright) { @@ -151,15 +145,6 @@ export abstract class BaseScripts { project )} || wait-on http-get://127.0.0.1:${port} && open-cli http://\${HOST:-localhost}:${port}`; } - - private resolvePort(project: Project, fallbackPort: number): number { - const port = Number(project.env.PORT ?? fallbackPort); - if (!Number.isInteger(port) || port <= 0) { - throw new Error(`The given port (${project.env.PORT}) is invalid.`); - } - project.env.PORT = `${port}`; - return port; - } } function buildPlaywrightCommand(playwrightArgs: string, targets: TestArgv['targets']): string { diff --git a/packages/wb/src/utils/port.ts b/packages/wb/src/utils/port.ts index d7037567..d9fab601 100644 --- a/packages/wb/src/utils/port.ts +++ b/packages/wb/src/utils/port.ts @@ -6,6 +6,7 @@ import { killPortProcessImmediatelyAndOnExit } from './process.js'; /** * Checks the given port and kills any process using it. + * Note wb always requires PORT environment variable. */ export async function checkAndKillPortProcess(rawPort: unknown, project: Project): Promise { const port = Number(rawPort); diff --git a/packages/wb/test/setup.test.ts b/packages/wb/test/setup.test.ts index 2af7afb0..0fbfb6d8 100644 --- a/packages/wb/test/setup.test.ts +++ b/packages/wb/test/setup.test.ts @@ -7,6 +7,8 @@ import { setup } from '../src/commands/setup.js'; import { initializeProjectDirectory, tempDir } from './shared.js'; +process.env.PORT ||= '3000'; + describe('setup', () => { it( 'blitz', From 660afa53d9b8614fba62a0bac7d02acae21c6bdf Mon Sep 17 00:00:00 2001 From: "Sakamoto, Kazunori" Date: Mon, 24 Nov 2025 10:27:32 +0900 Subject: [PATCH 15/15] fix: require port env in fixtures Co-authored-by: WillBooster (Codex CLI) --- packages/wb/test/fixtures/app/.env | 1 + packages/wb/test/fixtures/blitz/.env | 1 + packages/wb/test/fixtures/monorepo/.env | 1 + packages/wb/test/fixtures/unusual-monorepo/.env | 1 + packages/wb/test/setup.test.ts | 2 -- 5 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 packages/wb/test/fixtures/app/.env create mode 100644 packages/wb/test/fixtures/blitz/.env create mode 100644 packages/wb/test/fixtures/monorepo/.env create mode 100644 packages/wb/test/fixtures/unusual-monorepo/.env diff --git a/packages/wb/test/fixtures/app/.env b/packages/wb/test/fixtures/app/.env new file mode 100644 index 00000000..2fc80e3a --- /dev/null +++ b/packages/wb/test/fixtures/app/.env @@ -0,0 +1 @@ +PORT=3000 diff --git a/packages/wb/test/fixtures/blitz/.env b/packages/wb/test/fixtures/blitz/.env new file mode 100644 index 00000000..2fc80e3a --- /dev/null +++ b/packages/wb/test/fixtures/blitz/.env @@ -0,0 +1 @@ +PORT=3000 diff --git a/packages/wb/test/fixtures/monorepo/.env b/packages/wb/test/fixtures/monorepo/.env new file mode 100644 index 00000000..2fc80e3a --- /dev/null +++ b/packages/wb/test/fixtures/monorepo/.env @@ -0,0 +1 @@ +PORT=3000 diff --git a/packages/wb/test/fixtures/unusual-monorepo/.env b/packages/wb/test/fixtures/unusual-monorepo/.env new file mode 100644 index 00000000..2fc80e3a --- /dev/null +++ b/packages/wb/test/fixtures/unusual-monorepo/.env @@ -0,0 +1 @@ +PORT=3000 diff --git a/packages/wb/test/setup.test.ts b/packages/wb/test/setup.test.ts index 0fbfb6d8..2af7afb0 100644 --- a/packages/wb/test/setup.test.ts +++ b/packages/wb/test/setup.test.ts @@ -7,8 +7,6 @@ import { setup } from '../src/commands/setup.js'; import { initializeProjectDirectory, tempDir } from './shared.js'; -process.env.PORT ||= '3000'; - describe('setup', () => { it( 'blitz',