Skip to content

Commit

Permalink
use oclif
Browse files Browse the repository at this point in the history
  • Loading branch information
fengmk2 committed Dec 25, 2024
1 parent e0ccbc7 commit afb8d34
Show file tree
Hide file tree
Showing 14 changed files with 487 additions and 9 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ jobs:
uses: node-modules/github-actions/.github/workflows/node-test.yml@master
with:
os: 'ubuntu-latest, macos-latest, windows-latest'
version: '18.19.0, 18, 20, 22'
version: '18.19.0, 18, 20, 22, 23'
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,6 @@ yarn.lock
.c8_output
.idea

bin/*.js
cmd/*.js
!bin/postinstall.js
.eslintcache
dist
test/fixtures/example-declarations/typings/
Expand Down
3 changes: 3 additions & 0 deletions bin/dev.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@echo off

node --loader ts-node/esm --no-warnings=ExperimentalWarning "%~dp0\dev" %*
6 changes: 6 additions & 0 deletions bin/dev.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env -S node --loader ts-node/esm --disable-warning=ExperimentalWarning

// eslint-disable-next-line n/shebang
import {execute} from '@oclif/core'

await execute({development: true, dir: import.meta.url})
3 changes: 3 additions & 0 deletions bin/run.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@echo off

node "%~dp0\run" %*
5 changes: 5 additions & 0 deletions bin/run.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env node

import {execute} from '@oclif/core'

await execute({dir: import.meta.url})
17 changes: 14 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"@artus-cli/plugin-autocomplete": "^0.1.1",
"@artus-cli/plugin-version": "^1.0.2",
"@eggjs/utils": "^4.1.2",
"@oclif/core": "^4.2.0",
"c8": "^10.0.0",
"detect-port": "^2.0.0",
"egg-ts-helper": "^2.1.0",
Expand Down Expand Up @@ -85,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": "cross-env NODE_DEBUG=* node dist/esm/bin/cli.js test",
"test-local": "cross-env NODE_DEBUG=* node bin/dev.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",
Expand Down Expand Up @@ -114,14 +115,24 @@
"./package.json": "./package.json"
},
"files": [
"bin",
"dist",
"src",
"scripts"
],
"bin": {
"egg-bin": "./dist/esm/bin/cli.js"
"egg-bin": "./bin/run.js"
},
"types": "./dist/commonjs/index.d.ts",
"main": "./dist/commonjs/index.js",
"module": "./dist/esm/index.js"
"module": "./dist/esm/index.js",
"oclif": {
"bin": "egg-bin",
"commands": "./dist/esm/commands",
"dirname": "egg-bin",
"topicSeparator": " ",
"hooks": {
"init": "./dist/esm/hooks/init/options"
}
}
}
181 changes: 181 additions & 0 deletions src/baseCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { debuglog } from 'node:util';
// import path from 'node:path';
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 {
addNodeOptionsToEnv,
getSourceDirname,
// readPackageJSON, hasTsConfig, getSourceFilename,
} from './utils.js';

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

// only hook once and only when ever start any child.
const children = new Set<ChildProcess>();
let hadHook = false;
function graceful(proc: ChildProcess) {
// save child ref
children.add(proc);

// only hook once
/* c8 ignore else */
if (!hadHook) {
hadHook = true;
let signal: NodeJS.Signals;
[ 'SIGINT', 'SIGQUIT', 'SIGTERM' ].forEach(event => {
process.once(event, () => {
signal = event as NodeJS.Signals;
process.exit(0);
});
});

process.once('exit', (code: number) => {
for (const child of children) {
debug('process exit code: %o, kill child %o with %o', code, child.pid, signal);
child.kill(signal);
}
});
}
}

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

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']>;

export abstract class BaseCommand<T extends typeof Command> extends Command {
// add the --json flag
static enableJsonFlag = false;

// define flags that can be inherited by any command that extends BaseCommand
static baseFlags = {
// 'log-level': Flags.option({
// default: 'info',
// helpGroup: 'GLOBAL',
// options: ['debug', 'warn', 'error', 'info', 'trace'] as const,
// summary: 'Specify level for logging.',
// })(),
dryRun: Flags.boolean({
default: false,
helpGroup: 'GLOBAL',
summary: 'whether show full command script only',
char: 'd',
}),
require: Flags.string({
helpGroup: 'GLOBAL',
summary: 'require the given module',
char: 'r',
multiple: true,
}),
base: Flags.string({
helpGroup: 'GLOBAL',
summary: 'directory of application, default to `process.cwd()`',
aliases: [ 'baseDir' ],
default: process.cwd(),
}),
};

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

protected env = process.env;

public async init(): Promise<void> {
await super.init();
const { args, flags } = await this.parse({
flags: this.ctor.flags,
baseFlags: (super.ctor as typeof BaseCommand).baseFlags,
enableJsonFlag: this.ctor.enableJsonFlag,
args: this.ctor.args,
strict: this.ctor.strict,
});
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);
}

protected async catch(err: Error & {exitCode?: number}): Promise<any> {
// add any custom logic to handle errors from the command
// or simply return the parent class error handling
return super.catch(err);
}

protected async finally(_: Error | undefined): Promise<any> {
// called after run and catch regardless of whether or not the command errored
return super.finally(_);
}

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);
// }
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(' ')}`);
return;
}
const forkExecArgv = [
// ...this.ctx.args.execArgv || [],
...options.execArgv || [],
];

options = {
stdio: 'inherit',
env: this.env,
cwd: args.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}' ` : '',
process.execPath,
modulePath, forkArgs.map(a => `'${a}'`).join(' '));
graceful(proc);

return new Promise<void>((resolve, reject) => {
proc.once('exit', code => {
debug('fork pid: %o exit code %o', proc.pid, code);
children.delete(proc);
if (code !== 0) {
const err = new ForkError(modulePath + ' ' + forkArgs.join(' ') + ' exit with code ' + code, code);
reject(err);
} else {
resolve();
}
});
});
}
}

30 changes: 30 additions & 0 deletions src/commands/cov.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Args, Command, Flags } from '@oclif/core';

export default class Cov extends Command {
static override args = {
file: Args.string({ description: 'file to read' }),
};

static override description = 'describe the command here';

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' }),
};

public async run(): Promise<void> {
const { args, flags } = await this.parse(Cov);

const name = flags.name ?? 'world';
this.log(`hello ${name} from /Users/fengmk2/git/github.com/eggjs/bin/src/commands/cov.ts`);
if (args.file && flags.force) {
this.log(`you input --force and --file: ${args.file}`);
}
}
}
30 changes: 30 additions & 0 deletions src/commands/debug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Args, Command, Flags } from '@oclif/core';

export default class Debug extends Command {
static override args = {
file: Args.string({ description: 'file to read' }),
};

static override description = 'describe the command here';

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' }),
};

public async run(): Promise<void> {
const { args, flags } = await this.parse(Debug);

const name = flags.name ?? 'world';
this.log(`hello ${name} from /Users/fengmk2/git/github.com/eggjs/bin/src/commands/debug.ts`);
if (args.file && flags.force) {
this.log(`you input --force and --file: ${args.file}`);
}
}
}
30 changes: 30 additions & 0 deletions src/commands/dev.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Args, Command, Flags } from '@oclif/core';

export default class Dev extends Command {
static override args = {
file: Args.string({ description: 'file to read' }),
};

static override description = 'describe the command here';

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' }),
};

public async run(): Promise<void> {
const { args, flags } = await this.parse(Dev);

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}`);
}
}
}
Loading

0 comments on commit afb8d34

Please sign in to comment.