diff --git a/bin/dev.js b/bin/dev.js index dd748048..f0b61954 100755 --- a/bin/dev.js +++ b/bin/dev.js @@ -1,6 +1,5 @@ #!/usr/bin/env -S node --loader ts-node/esm --disable-warning=ExperimentalWarning -// eslint-disable-next-line n/shebang -import {execute} from '@oclif/core' +import { execute } from '@oclif/core'; -await execute({development: true, dir: import.meta.url}) +await execute({ development: true, dir: import.meta.url }); diff --git a/bin/run.js b/bin/run.js index dd50271f..176d2af5 100755 --- a/bin/run.js +++ b/bin/run.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -import {execute} from '@oclif/core' +import { execute } from '@oclif/core'; -await execute({dir: import.meta.url}) +await execute({ dir: import.meta.url }); diff --git a/package.json b/package.json index 6202108a..1b054c73 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "lint": "eslint --cache src test --ext .ts", "pretest": "npm run clean && npm run lint -- --fix && npm run prepublishOnly", "test": "npm run test-local", - "test-local": "node bin/dev.js test", + "test-local": "node bin/run.js test", "preci": "npm run clean && npm run lint && npm run prepublishOnly", "cov": "c8 -r lcov -r text-summary -x 'test/**' npm run test-local -- --timeout 120000", "ci": "npm run cov", diff --git a/src/baseCommand.ts b/src/baseCommand.ts index 00498be3..0cb4ecb3 100644 --- a/src/baseCommand.ts +++ b/src/baseCommand.ts @@ -1,5 +1,6 @@ import { debuglog } from 'node:util'; import { pathToFileURL } from 'node:url'; +import path from 'node:path'; import { fork, ForkOptions, ChildProcess } from 'node:child_process'; import { Command, Flags, Interfaces } from '@oclif/core'; import { importResolve } from '@eggjs/utils'; @@ -9,7 +10,6 @@ import { getSourceDirname, readPackageJSON, hasTsConfig, } from './utils.js'; -import path from 'node:path'; import { PackageEgg } from './types.js'; const debug = debuglog('@eggjs/bin/baseCommand'); @@ -135,7 +135,7 @@ export abstract class BaseCommand extends Command { public async init(): Promise { await super.init(); - debug('raw args: %o', this.argv); + debug('[init] raw args: %o, NODE_ENV: %o', this.argv, this.env.NODE_ENV); const { args, flags } = await this.parse({ flags: this.ctor.flags, baseFlags: (super.ctor as typeof BaseCommand).baseFlags, @@ -295,7 +295,7 @@ export abstract class BaseCommand extends Command { debug('set NODE_OPTIONS: %o', this.env.NODE_OPTIONS); debug('after: args: %o, flags: %o', args, flags); - debug('enter real command'); + debug('enter real command: %o', this.id); } protected async catch(err: Error & {exitCode?: number}): Promise { diff --git a/src/commands/dev.ts b/src/commands/dev.ts index 874d63a0..ebad3d44 100644 --- a/src/commands/dev.ts +++ b/src/commands/dev.ts @@ -1,30 +1,107 @@ -import { Args, Command, Flags } from '@oclif/core'; +import { debuglog } from 'node:util'; +import { Flags } from '@oclif/core'; +import { pathToFileURL } from 'node:url'; +import { getConfig, getFrameworkPath } from '@eggjs/utils'; +import { detect } from 'detect-port'; +import { getSourceFilename } from '../utils.js'; +import { BaseCommand } from '../baseCommand.js'; -export default class Dev extends Command { - static override args = { - file: Args.string({ description: 'file to read' }), - }; +const debug = debuglog('@eggjs/bin/commands/dev'); - static override description = 'describe the command here'; +export default class Dev extends BaseCommand { + static override description = 'Start server at local dev mode'; static override examples = [ '<%= config.bin %> <%= command.id %>', ]; static override flags = { - // flag with no value (-f, --force) - force: Flags.boolean({ char: 'f' }), - // flag with a value (-n, --name=VALUE) - name: Flags.string({ char: 'n', description: 'name to print' }), + port: Flags.integer({ + description: 'listening port, default to 7001', + char: 'p', + }), + workers: Flags.integer({ + char: 'c', + aliases: [ 'cluster' ], + description: 'numbers of app workers', + default: 1, + }), + framework: Flags.string({ + description: 'specify framework that can be absolute path or npm package, default is "egg"', + }), + sticky: Flags.boolean({ + description: 'start a sticky cluster server', + }), + env: Flags.string({ + description: 'specify the NODE_ENV', + }), }; public async run(): Promise { - const { args, flags } = await this.parse(Dev); + debug('NODE_ENV: %o', this.env); + this.env.NODE_ENV = this.flags.env ?? this.env.NODE_ENV ?? 'development'; + this.env.EGG_MASTER_CLOSE_TIMEOUT = '1000'; + const serverBin = getSourceFilename('../scripts/start-cluster.mjs'); + const eggStartOptions = await this.formatEggStartOptions(); + const args = [ JSON.stringify(eggStartOptions) ]; + const requires = await this.formatRequires(); + const execArgv: string[] = []; + for (const r of requires) { + if (this.isESM) { + execArgv.push('--import'); + execArgv.push(pathToFileURL(r).href); + } else { + execArgv.push('--require'); + execArgv.push(r); + } + } + await this.forkNode(serverBin, args, { execArgv }); + } - const name = flags.name ?? 'world'; - this.log(`hello ${name} from /Users/fengmk2/git/github.com/eggjs/bin/src/commands/dev.ts`); - if (args.file && flags.force) { - this.log(`you input --force and --file: ${args.file}`); + protected async formatEggStartOptions() { + const { flags } = this; + flags.framework = getFrameworkPath({ + framework: flags.framework, + baseDir: flags.base, + }); + + if (!flags.port) { + let configuredPort: number | undefined; + try { + const configuration = await getConfig({ + framework: flags.framework, + baseDir: flags.base, + env: 'local', + }); + configuredPort = configuration?.cluster?.listen?.port; + } catch (err) { + /** skip when failing to read the configuration */ + debug('getConfig error: %s, framework: %o, baseDir: %o, env: local', + err, flags.framework, flags.base); + } + if (configuredPort) { + flags.port = configuredPort; + debug(`use port ${flags.port} from configuration file`); + } else { + const defaultPort = parseInt(process.env.EGG_BIN_DEFAULT_PORT ?? '7001'); + debug('detect available port'); + flags.port = await detect(defaultPort); + if (flags.port !== defaultPort) { + console.warn('[@eggjs/bin] server port %o is unavailable, now using port %o', + defaultPort, flags.port); + } + debug(`use available port ${flags.port}`); + } } + + return { + baseDir: flags.base, + workers: flags.workers, + port: flags.port, + framework: flags.framework, + typescript: flags.typescript, + tscompiler: flags.tscompiler, + sticky: flags.sticky, + }; } } diff --git a/test/cmd/cov.test.ts b/test/cmd/cov.test.ts index ef6e5e05..6c80d342 100644 --- a/test/cmd/cov.test.ts +++ b/test/cmd/cov.test.ts @@ -9,7 +9,7 @@ import { getFixtures, getRootDirname } from '../helper.js'; const version = Number(process.version.substring(1, 3)); describe('test/cmd/cov.test.ts', () => { - const eggBin = path.join(getRootDirname(), 'bin/dev.js'); + const eggBin = path.join(getRootDirname(), 'bin/run.js'); const cwd = getFixtures('test-files'); async function assertCoverage(baseDir: string) { diff --git a/test/cmd/debug.test.ts b/test/cmd/debug.test.ts index ef27a45c..2cbfd788 100644 --- a/test/cmd/debug.test.ts +++ b/test/cmd/debug.test.ts @@ -3,7 +3,7 @@ import coffee from '../coffee.js'; import { getFixtures, getRootDirname } from '../helper.js'; describe('test/cmd/debug.test.ts', () => { - const eggBin = path.join(getRootDirname(), 'bin/dev.js'); + const eggBin = path.join(getRootDirname(), 'bin/run.js'); const cwd = getFixtures('demo-app'); it('should startCluster success', () => { diff --git a/test/cmd/dev.test.ts b/test/cmd/dev.test.ts index e1581da2..3ae42dff 100644 --- a/test/cmd/dev.test.ts +++ b/test/cmd/dev.test.ts @@ -8,7 +8,7 @@ import { getRootDirname, getFixtures } from '../helper.js'; const version = Number(process.version.substring(1, 3)); describe('test/cmd/dev.test.ts', () => { - const eggBin = path.join(getRootDirname(), 'bin/dev.js'); + const eggBin = path.join(getRootDirname(), 'bin/run.js'); const cwd = getFixtures('demo-app'); it('should startCluster success', () => { @@ -25,9 +25,20 @@ describe('test/cmd/dev.test.ts', () => { .end(); }); - it('should dev start with custom NODE_ENV', () => { + it.only('should dev start with custom NODE_ENV', () => { return coffee.fork(eggBin, [ 'dev' ], { cwd, env: { NODE_ENV: 'prod' } }) - // .debug() + .debug() + .expect('stdout', /"workers":1/) + .expect('stdout', /"baseDir":".*?demo-app"/) + .expect('stdout', /"framework":".*?aliyun-egg"/) + .expect('stdout', /NODE_ENV: prod/) + .expect('code', 0) + .end(); + }); + + it('should dev start with --env prod', () => { + return coffee.fork(eggBin, [ 'dev', '--env', 'prod' ], { cwd }) + .debug() .expect('stdout', /"workers":1/) .expect('stdout', /"baseDir":".*?demo-app"/) .expect('stdout', /"framework":".*?aliyun-egg"/) diff --git a/test/cmd/test.test.ts b/test/cmd/test.test.ts index 7ba65cb8..b7e5de77 100644 --- a/test/cmd/test.test.ts +++ b/test/cmd/test.test.ts @@ -5,7 +5,7 @@ import { getFixtures, getRootDirname } from '../helper.js'; const version = Number(process.version.substring(1, 3)); describe('test/cmd/test.test.ts', () => { - const eggBin = path.join(getRootDirname(), 'bin/dev.js'); + const eggBin = path.join(getRootDirname(), 'bin/run.js'); const cwd = getFixtures('test-files'); describe('egg-bin test', () => { diff --git a/test/coffee.ts b/test/coffee.ts index 3a2dd923..628293cd 100644 --- a/test/coffee.ts +++ b/test/coffee.ts @@ -15,6 +15,7 @@ export default { PATH: process.env.PATH, ...options.env, }; + // console.error('fork env: %o', options.env); return coffee.fork(modulePath, args, options); }, }; diff --git a/test/egg-bin.test.ts b/test/egg-bin.test.ts index 10e6958a..64c63932 100644 --- a/test/egg-bin.test.ts +++ b/test/egg-bin.test.ts @@ -3,7 +3,7 @@ import coffee from './coffee.js'; import { getRootDirname, getFixtures } from './helper.js'; describe('test/egg-bin.test.ts', () => { - const eggBin = path.join(getRootDirname(), 'bin/dev.js'); + const eggBin = path.join(getRootDirname(), 'bin/run.js'); const cwd = getFixtures('test-files'); describe('global options', () => {