Skip to content

Commit

Permalink
refactor test command
Browse files Browse the repository at this point in the history
  • Loading branch information
fengmk2 committed Dec 26, 2024
1 parent b1a61fc commit 07d15fb
Show file tree
Hide file tree
Showing 16 changed files with 307 additions and 99 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
"ci": "npm run cov",
"clean": "rimraf dist",
"copyScripts": "rimraf dist/scripts && cpy scripts dist",
"prepublishOnly": "tshy && tshy-after && attw --pack && rimraf dist/commonjs && npm run copyScripts"
"prepublishOnly": "tshy && tshy-after && attw --pack && npm run copyScripts"
},
"type": "module",
"tshy": {
Expand Down
245 changes: 210 additions & 35 deletions src/baseCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import { pathToFileURL } from 'node:url';
import { fork, ForkOptions, ChildProcess } from 'node:child_process';
import { Command, Flags, Interfaces } from '@oclif/core';
import { importResolve } from '@eggjs/utils';
import { runScript } from 'runscript';
import {
addNodeOptionsToEnv,
getSourceDirname,
// readPackageJSON, hasTsConfig, getSourceFilename,
readPackageJSON, hasTsConfig,
} from './utils.js';
import path from 'node:path';
import { PackageEgg } from './types.js';

const debug = debuglog('@eggjs/bin/baseCommand');

Expand Down Expand Up @@ -40,14 +43,18 @@ function graceful(proc: ChildProcess) {
}
}

class ForkError extends Error {
export class ForkError extends Error {
code: number | null;
constructor(message: string, code: number | null) {
super(message);
this.code = code;
}
}

export interface ForkNodeOptions extends ForkOptions {
dryRun?: boolean;
}

export type Flags<T extends typeof Command> = Interfaces.InferredFlags<typeof BaseCommand['baseFlags'] & T['flags']>;
export type Args<T extends typeof Command> = Interfaces.InferredArgs<T['args']>;

Expand All @@ -63,7 +70,7 @@ export abstract class BaseCommand<T extends typeof Command> extends Command {
// options: ['debug', 'warn', 'error', 'info', 'trace'] as const,
// summary: 'Specify level for logging.',
// })(),
dryRun: Flags.boolean({
'dry-run': Flags.boolean({
default: false,
helpGroup: 'GLOBAL',
summary: 'whether show full command script only',
Expand All @@ -77,16 +84,50 @@ export abstract class BaseCommand<T extends typeof Command> extends Command {
}),
base: Flags.string({
helpGroup: 'GLOBAL',
summary: 'directory of application, default to `process.cwd()`',
summary: 'directory of application',
aliases: [ 'baseDir' ],
default: process.cwd(),
}),
tscompiler: Flags.string({
helpGroup: 'GLOBAL',
summary: 'TypeScript compiler, like ts-node/register',
}),
// flag with no value (--ts, --typescript)
typescript: Flags.boolean({
helpGroup: 'GLOBAL',
description: '[default: true] use TypeScript to run the test',
aliases: [ 'ts' ],
allowNo: true,
}),
javascript: Flags.boolean({
helpGroup: 'GLOBAL',
description: 'use JavaScript to run the test',
aliases: [ 'js' ],
}),
declarations: Flags.boolean({
helpGroup: 'GLOBAL',
description: 'whether create typings, will add `--require egg-ts-helper/register`',
aliases: [ 'dts' ],
}),
// https://nodejs.org/dist/latest-v18.x/docs/api/cli.html#--inspect-brkhostport
inspect: Flags.boolean({
helpGroup: 'GLOBAL',
description: 'Activate inspector',
}),
'inspect-brk': Flags.boolean({
helpGroup: 'GLOBAL',
description: 'Activate inspector and break at start of user script',
}),
};

protected flags!: Flags<T>;
protected args!: Args<T>;

protected env = process.env;
protected env = { ...process.env };
protected pkg: Record<string, any>;
protected isESM: boolean;
protected pkgEgg: PackageEgg;
protected globalExecArgv: string[] = [];

public async init(): Promise<void> {
await super.init();
Expand All @@ -100,18 +141,148 @@ export abstract class BaseCommand<T extends typeof Command> extends Command {
this.flags = flags as Flags<T>;
this.args = args as Args<T>;

// use ts-node/esm loader on esm
let esmLoader = importResolve('ts-node/esm', {
paths: [ getSourceDirname() ],
});
// ES Module loading with absolute path fails on windows
// https://github.com/nodejs/node/issues/31710#issuecomment-583916239
// https://nodejs.org/api/url.html#url_url_pathtofileurl_path
// Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]: Only URLs with a scheme in: file, data, and node are supported by the default ESM loader. On Windows, absolute paths must be valid file:// URLs. Received protocol 'd:'
esmLoader = pathToFileURL(esmLoader).href;
// wait for https://github.com/nodejs/node/issues/40940
addNodeOptionsToEnv('--no-warnings', this.env);
addNodeOptionsToEnv(`--loader ${esmLoader}`, this.env);
await this.#afterInit();
}

async #afterInit() {
const { args, flags } = this;
debug('before: args: %o, flags: %o', args, flags);
if (!path.isAbsolute(flags.base)) {
flags.base = path.join(process.cwd(), flags.base);
}
debug('baseDir: %o', flags.base);
const pkg = await readPackageJSON(flags.base);
this.pkg = pkg;
this.pkgEgg = pkg.egg ?? {};
flags.tscompiler = flags.tscompiler ?? this.env.TS_COMPILER ?? this.pkgEgg.tscompiler;
let typescript = args.typescript;
if (typescript === undefined) {
// try to ready EGG_TYPESCRIPT env first, only accept 'true' or 'false' string
if (this.env.EGG_TYPESCRIPT === 'false') {
typescript = false;
debug('detect typescript=%o from EGG_TYPESCRIPT=%o', false, this.env.EGG_TYPESCRIPT);
} else if (this.env.EGG_TYPESCRIPT === 'true') {
typescript = true;
debug('detect typescript=%o from EGG_TYPESCRIPT=%o', true, this.env.EGG_TYPESCRIPT);
} else if (typeof this.pkgEgg.typescript === 'boolean') {
// read `egg.typescript` from package.json if not pass argv
typescript = this.pkgEgg.typescript;
debug('detect typescript=%o from pkg.egg.typescript=%o', typescript, this.pkgEgg.typescript);
} else if (pkg.dependencies?.typescript) {
// auto detect pkg.dependencies.typescript or pkg.devDependencies.typescript
typescript = true;
debug('detect typescript=%o from pkg.dependencies.typescript=%o', true, pkg.dependencies.typescript);
} else if (pkg.devDependencies?.typescript) {
typescript = true;
debug('detect typescript=%o from pkg.devDependencies.typescript=%o', true, pkg.devDependencies.typescript);
} else if (await hasTsConfig(flags.base)) {
// tsconfig.json exists
typescript = true;
debug('detect typescript=%o cause tsconfig.json exists', true);
} else if (flags.tscompiler) {
typescript = true;
debug('detect typescript=%o from --tscompiler=%o', true, flags.tscompiler);
}
}
flags.typescript = typescript;

this.isESM = pkg.type === 'module';
if (typescript) {
const findPaths: string[] = [ getSourceDirname() ];
if (flags.tscompiler) {
// try app baseDir first on custom tscompiler
// then try to find tscompiler in @eggjs/bin/node_modules
findPaths.unshift(flags.base);
}
flags.tscompiler = flags.tscompiler ?? 'ts-node/register';
let tsNodeRegister = importResolve(flags.tscompiler, {
paths: findPaths,
});
flags.tscompiler = tsNodeRegister;
// should require tsNodeRegister on current process, let it can require *.ts files
// e.g.: dev command will execute egg loader to find configs and plugins
// await importModule(tsNodeRegister);
// let child process auto require ts-node too
if (this.isESM) {
tsNodeRegister = pathToFileURL(tsNodeRegister).href;
addNodeOptionsToEnv(`--import ${tsNodeRegister}`, this.env);
} else {
addNodeOptionsToEnv(`--require ${tsNodeRegister}`, this.env);
}
// tell egg loader to load ts file
// see https://github.com/eggjs/egg-core/blob/master/lib/loader/egg_loader.js#L443
this.env.EGG_TYPESCRIPT = 'true';
// set current process.env.EGG_TYPESCRIPT too
process.env.EGG_TYPESCRIPT = 'true';
// load files from tsconfig on startup
this.env.TS_NODE_FILES = process.env.TS_NODE_FILES ?? 'true';
// keep same logic with egg-core, test cmd load files need it
// see https://github.com/eggjs/egg-core/blob/master/lib/loader/egg_loader.js#L49
// addNodeOptionsToEnv(`--require ${importResolve('tsconfig-paths/register', {
// paths: [ getSourceDirname() ],
// })}`, ctx.env);
}
if (this.isESM) {
// use ts-node/esm loader on esm
let esmLoader = importResolve('ts-node/esm', {
paths: [ getSourceDirname() ],
});
// ES Module loading with absolute path fails on windows
// https://github.com/nodejs/node/issues/31710#issuecomment-583916239
// https://nodejs.org/api/url.html#url_url_pathtofileurl_path
// Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]: Only URLs with a scheme in: file, data, and node are supported by the default ESM loader. On Windows, absolute paths must be valid file:// URLs. Received protocol 'd:'
esmLoader = pathToFileURL(esmLoader).href;
// wait for https://github.com/nodejs/node/issues/40940
addNodeOptionsToEnv('--no-warnings', this.env);
addNodeOptionsToEnv(`--loader ${esmLoader}`, this.env);
}

if (flags.declarations === undefined) {
if (typeof this.pkgEgg.declarations === 'boolean') {
// read `egg.declarations` from package.json if not pass argv
flags.declarations = this.pkgEgg.declarations;
debug('detect declarations from pkg.egg.declarations=%o', this.pkgEgg.declarations);
}
}
if (flags.declarations) {
const etsBin = importResolve('egg-ts-helper/dist/bin', {
paths: [
flags.base,
getSourceDirname(),
],
});
debug('run ets first: %o', etsBin);
await runScript(`node ${etsBin}`);
}

if (this.pkgEgg.revert) {
const reverts = Array.isArray(this.pkgEgg.revert) ? this.pkgEgg.revert : [ this.pkgEgg.revert ];
for (const revert of reverts) {
this.globalExecArgv.push(`--security-revert=${revert}`);
}
}

let hasInspectOption = false;
if (flags.inspect) {
addNodeOptionsToEnv('--inspect', this.env);
hasInspectOption = true;
}
if (flags['inspect-brk']) {
addNodeOptionsToEnv('--inspect-brk', this.env);
hasInspectOption = true;
}
if (hasInspectOption) {
Reflect.set(flags, 'timeout', 0);
debug('set timeout = 0 when inspect enable');
} else if (this.env.JB_DEBUG_FILE) {
// others like WebStorm 2019 will pass NODE_OPTIONS, and @eggjs/bin itself will be debug, so could detect `process.env.JB_DEBUG_FILE`.
Reflect.set(flags, 'timeout', 0);
debug('set timeout = false when process.env.JB_DEBUG_FILE=%o', this.env.JB_DEBUG_FILE);
}

debug('set NODE_OPTIONS: %o', this.env.NODE_OPTIONS);
debug('after: args: %o, flags: %o', args, flags);
debug('enter real command');
}

protected async catch(err: Error & {exitCode?: number}): Promise<any> {
Expand All @@ -126,42 +297,46 @@ export abstract class BaseCommand<T extends typeof Command> extends Command {
}

protected async formatRequires(): Promise<string[]> {
const requires = this.args.require ?? [];
// const eggRequire = this.args.pkgEgg.require;
// if (Array.isArray(eggRequire)) {
// for (const r of eggRequire) {
// requires.push(r);
// }
// } else if (typeof eggRequire === 'string' && eggRequire) {
// requires.push(eggRequire);
// }
const requires = this.flags.require ?? [];
const eggRequire = this.pkgEgg.require;
if (Array.isArray(eggRequire)) {
for (const r of eggRequire) {
requires.push(r);
}
} else if (typeof eggRequire === 'string' && eggRequire) {
requires.push(eggRequire);
}
return requires;
}

protected async forkNode(modulePath: string, forkArgs: string[], options: ForkOptions = {}) {
const { args } = this;
if (args.dryRun) {
console.log('dry run: $ %o', `${process.execPath} ${modulePath} ${args.join(' ')}`);
protected async forkNode(modulePath: string, forkArgs: string[], options: ForkNodeOptions = {}) {
const env = {
...this.env,
...options.env,
};
const NODE_OPTIONS = env.NODE_OPTIONS ? `NODE_OPTIONS='${env.NODE_OPTIONS}' ` : '';
if (options.dryRun) {
console.log('dry run: $ %o', `${NODE_OPTIONS}${process.execPath} ${modulePath} ${forkArgs.join(' ')}`);
return;
}
const forkExecArgv = [
// ...this.ctx.args.execArgv || [],
...this.globalExecArgv,
...options.execArgv || [],
];

options = {
stdio: 'inherit',
env: this.env,
cwd: args.base,
env,
cwd: this.flags.base,
...options,
execArgv: forkExecArgv,
};
const proc = fork(modulePath, forkArgs, options);
debug('Run fork pid: %o\n\n$ %s%s %s %s\n\n',
proc.pid,
options.env?.NODE_OPTIONS ? `NODE_OPTIONS='${options.env.NODE_OPTIONS}' ` : '',
NODE_OPTIONS,
process.execPath,
modulePath, forkArgs.map(a => `'${a}'`).join(' '));
modulePath, forkArgs.map(a => `${a}`).join(' '));
graceful(proc);

return new Promise<void>((resolve, reject) => {
Expand Down
Loading

0 comments on commit 07d15fb

Please sign in to comment.