Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rax-app怎么依赖create-cli-utils实现自己的cli? #38

Open
huenchao opened this issue Mar 22, 2022 · 0 comments
Open

rax-app怎么依赖create-cli-utils实现自己的cli? #38

huenchao opened this issue Mar 22, 2022 · 0 comments

Comments

@huenchao
Copy link
Owner

//rax-app
#!/usr/bin/env node
const utils = require('create-cli-utils');
const packageInfo = require('../package.json');
const getBuiltInPlugins = require('../lib');

const forkChildProcessPath = require.resolve('./child-process-start');

(async () => {
//packageInfo rax-app pkg.json
//getBuiltInPlugins rax-app 自己需要的webpack配置
//forkChildProcessPath 和getBuiltInPlugins绑定
await utils.createCli(getBuiltInPlugins, forkChildProcessPath, packageInfo );
})();

#!/usr/bin/env node
const { childProcessStart } = require('create-cli-utils');
const getBuiltInPlugins = require('../lib');

(async () => {
await childProcessStart(getBuiltInPlugins);
})();
import { IGetBuiltInPlugins, IPluginList, Json, IUserConfig } from 'build-scripts';
import * as miniappBuilderShared from 'miniapp-builder-shared';
import { init } from '@builder/pack/deps/webpack/webpack';
import { hijackWebpack } from './require-hook';

const { constants: { MINIAPP, WECHAT_MINIPROGRAM, BYTEDANCE_MICROAPP, BAIDU_SMARTPROGRAM, KUAISHOU_MINIPROGRAM } } = miniappBuilderShared;
const miniappPlatforms = [MINIAPP, WECHAT_MINIPROGRAM, BYTEDANCE_MICROAPP, BAIDU_SMARTPROGRAM, KUAISHOU_MINIPROGRAM];

interface IRaxAppUserConfig extends IUserConfig {
targets: string[];
store?: boolean;
web?: any;
experiments?: {
minifyCSSModules?: boolean;
};

webpack5?: boolean;

router?: boolean;
}

const getBuiltInPlugins: IGetBuiltInPlugins = (userConfig: IRaxAppUserConfig) => {
const { targets = ['web'], store = true, router = true, webpack5, experiments = {} } = userConfig;
const coreOptions: Json = {
framework: 'rax',
alias: 'rax-app',
};

init(webpack5);
hijackWebpack(webpack5);

// built-in plugins for rax app
const builtInPlugins: IPluginList = [
['build-plugin-app-core', coreOptions],
'build-plugin-rax-app',
'build-plugin-ice-config',
];

if (store) {
builtInPlugins.push('build-plugin-rax-store');
}

if (targets.includes('web')) {
builtInPlugins.push('build-plugin-rax-web');
}

if (targets.includes('weex')) {
builtInPlugins.push('build-plugin-rax-weex');
}

if (targets.includes('kraken')) {
builtInPlugins.push('build-plugin-rax-kraken');
}

const isMiniAppTargeted = targets.some((target) => miniappPlatforms.includes(target));

if (isMiniAppTargeted) {
builtInPlugins.push('build-plugin-rax-miniapp');
}

if (userConfig.web) {
if (userConfig.web.pha) {
builtInPlugins.push('build-plugin-rax-pha');
}
// Make ssr plugin after base plugin which need registerTask, the action will override the devServer config
if (userConfig.web.ssr) {
builtInPlugins.push('build-plugin-ssr');
}
}

if (router) {
builtInPlugins.push('build-plugin-rax-router');
}

builtInPlugins.push('build-plugin-ice-logger');

if (experiments.minifyCSSModules === true) {
builtInPlugins.push('build-plugin-minify-classname');
}

return builtInPlugins;
};

export = getBuiltInPlugins;

//回到ice-scripts
#!/usr/bin/env node
const program = require('commander');
const checkNodeVersion = require('./checkNodeVersion');
const start = require('./start');
const build = require('./build');
const test = require('./test');

module.exports = async (getBuiltInPlugins, forkChildProcessPath, packageInfo, extendCli) => {
if (packageInfo.ICEJS_INFO) {
console.log(
${packageInfo.name} ${packageInfo.version},
(${packageInfo.__ICEJS_INFO__.name} ${packageInfo.__ICEJS_INFO__.version})
);
} else {
console.log(packageInfo.name, packageInfo.version);
}
// finish check before run command
checkNodeVersion(packageInfo.engines.node, packageInfo.name);

program
.version(packageInfo.version)
.usage(' [options]');

program
.command('build')
.description('build project')
.allowUnknownOption()
.option('--config ', 'use custom config')
.option('--rootDir ', 'project root directory')
.action(async function() {
await build(getBuiltInPlugins);
});

program
.command('start')
.description('start server')
.allowUnknownOption()
.option('--config ', 'use custom config')
.option('-h, --host ', 'dev server host', '0.0.0.0')
.option('-p, --port ', 'dev server port')
.option('--rootDir ', 'project root directory')
.action(async function() {
await start(getBuiltInPlugins, forkChildProcessPath);
});

program
.command('test')
.description('run tests with jest')
.allowUnknownOption() // allow jest config
.option('--config ', 'use custom config')
.action(async function() {
await test(getBuiltInPlugins);
});
//rax这边过来是没有的
if (typeof extendCli === 'function') {
extendCli(program);
}

program.parse(process.argv);

const proc = program.runningCommand;

if (proc) {
proc.on('close', process.exit.bind(process));
proc.on('error', () => {
process.exit(1);
});
}

const subCmd = program.args[0];
if (!subCmd) {
program.help();
}
};

//主要是处理监听文件、restart的时候inspector的端口confilct,然后重点就是fork forkChildProcessPath程序文件进程。
#!/usr/bin/env node
const { fork } = require('child_process');
const parse = require('yargs-parser');
const chokidar = require('chokidar');
const detect = require('detect-port');
const path = require('path');
const log = require('build-scripts/lib/utils/log');

let child = null;
const rawArgv = parse(process.argv.slice(2));
const configPath = path.resolve(rawArgv.config || 'build.json');

const inspectRegExp = /^--(inspect(?:-brk)?)(?:=(?:([^:]+):)?(\d+))?$/;

async function modifyInspectArgv(execArgv, processArgv) {
/**

  • Enable debugger by exec argv, eg. node --inspect node_modules/.bin/build-scripts start
  • By this way, there will be two inspector, because start.js is run as a child process.
  • So need to handle the conflict of port.
    */
    const result = await Promise.all(
    execArgv.map(async item => {
    const matchResult = inspectRegExp.exec(item);
    if (!matchResult) {
    return item;
    }
    // eslint-disable-next-line
    const [_, command, ip, port = 9229] = matchResult;
    const nPort = +port;
    const newPort = await detect(nPort);
    return --${command}=${ip ? ${ip}: : ''}${newPort};
    })
    );

/**

  • Enable debugger by process argv, eg. npm run start --inspect
  • Need to change it as an exec argv.
    */
    if (processArgv.inspect) {
    const matchResult = /(?:([^:]+):)?(\d+)/.exec(rawArgv.inspect);
    // eslint-disable-next-line
    const [_, ip, port = 9229] = matchResult || [];
    const newPort = await detect(port);
    result.push(--inspect-brk=${ip ? ${ip}: : ''}${newPort});
    }

return result;
}

function restartProcess(forkChildProcessPath) {
(async () => {
// remove the inspect related argv when passing to child process to avoid port-in-use error
const argv = await modifyInspectArgv(process.execArgv, rawArgv);
const nProcessArgv = process.argv.slice(2).filter((arg) => arg.indexOf('--inspect') === -1);
child = fork(forkChildProcessPath, nProcessArgv, { execArgv: argv });
child.on('message', data => {
if (data && data.type === 'RESTART_DEV') {
child.kill();
restartProcess(forkChildProcessPath);
}
if (process.send) {
process.send(data);
}
});

child.on('exit', code => {
  if (code) {
    process.exit(code);
  }
});

})();
}

module.exports = (getBuiltInPlugins, forkChildProcessPath) => {
restartProcess(forkChildProcessPath);

const watcher = chokidar.watch(configPath, {
ignoreInitial: true,
});

watcher.on('change', function() {
console.log('\n');
log.info('build.json has been changed');
log.info('restart dev server');
// add process env for mark restart dev process
process.env.RESTART_DEV = true;
child.kill();
restartProcess(forkChildProcessPath);
});

watcher.on('error', error => {
log.error('fail to watch file', error);
process.exit(1);
});
};

fork启动的代码:
#!/usr/bin/env node
const { childProcessStart } = require('create-cli-utils');
const getBuiltInPlugins = require('../lib');

(async () => {
await childProcessStart(getBuiltInPlugins);
})();
#!/usr/bin/env node
const detect = require('detect-port');
const inquirer = require('inquirer');
const parse = require('yargs-parser');
const log = require('build-scripts/lib/utils/log');
const { isAbsolute, join } = require('path');
const BuildService = require('./buildService');

const rawArgv = parse(process.argv.slice(2), {
configuration: { 'strip-dashed': true }
});

const DEFAULT_PORT = rawArgv.port || process.env.PORT || 3333;
const defaultPort = parseInt(DEFAULT_PORT, 10);

module.exports = async (getBuiltInPlugins) => {
let newPort = await detect(defaultPort);
if (newPort !== defaultPort) {
const question = {
type: 'confirm',
name: 'shouldChangePort',
message: ${defaultPort} 端口已被占用,是否使用 ${newPort} 端口启动?,
default: true
};
const answer = await inquirer.prompt(question);
if (!answer.shouldChangePort) {
newPort = null;
}
}
if (newPort === null) {
process.exit(1);
}

process.env.NODE_ENV = 'development';
rawArgv.port = parseInt(newPort, 10);

const { rootDir = process.cwd() } = rawArgv;

delete rawArgv.rootDir;
// ignore _ in rawArgv
delete rawArgv._;
try {
const service = new BuildService({
command: 'start',
args: { ...rawArgv },
getBuiltInPlugins,
rootDir: isAbsolute(rootDir) ? rootDir : join(process.cwd(), rootDir),
});
const devServer = await service.run({});

['SIGINT', 'SIGTERM'].forEach(function (sig) {
  process.on(sig, function () {
    if (devServer) {
      devServer.close();
    }
    process.exit(0);
  });
});

} catch (err) {
log.error(err.message);
console.error(err);
process.exit(1);
}
};
//走service 和 context,主要是context

constructor(options: IContextOptions) {
const {
command,
rootDir = process.cwd(),
args = {},
} = options || {};

this.options = options;
this.command = command;
this.commandArgs = args;
this.rootDir = rootDir;
/**
 * config array
 * {
 *   name,
 *   chainConfig,
 *   webpackFunctions,
 * }
 */
this.configArr = [];
this.modifyConfigFns = [];
this.modifyJestConfig = [];
this.modifyConfigRegistrationCallbacks = [];
this.modifyCliRegistrationCallbacks = [];
this.eventHooks = {}; // lifecycle functions
this.internalValue = {}; // internal value shared between plugins
this.userConfigRegistration = {};
this.cliOptionRegistration = {};
this.methodRegistration = {};
this.cancelTaskNames = [];

this.pkg = this.getProjectFile(PKG_FILE);
// register builtin options
this.registerCliOption(BUILTIN_CLI_OPTIONS);

}
new BuildService 实例结束后,去执行await service.run({});
public run = async <T, P>(options?: T): Promise

=> {
const { command, commandArgs } = this;
log.verbose(
'OPTIONS',
${command} cliOptions: ${JSON.stringify(commandArgs, null, 2)},
);
try {
await this.setUp();
} catch (err) {
log.error('CONFIG', chalk.red('Failed to get config.'));
await this.applyHook(error, { err });
throw err;
}
const commandModule = this.getCommandModule({ command, commandArgs, userConfig: this.userConfig });
return commandModule(this, options);
}

public setUp = async (): Promise<ITaskConfig[]> => {
await this.resolveConfig();
await this.runPlugins();
await this.runConfigModification();
await this.runUserConfig();
await this.runWebpackFunctions();
await this.runCliOption();
// filter webpack config by cancelTaskNames
this.configArr = this.configArr.filter(
config => !this.cancelTaskNames.includes(config.name),
);
return this.configArr;
};
//关键的是走runPlugins,其实就是遍历执行build.json里注册的,以及getBuiltInPlugins带过来的。
我们以plugin-rax-kraken为例:
module.exports = (api) => {
const { getValue, context, registerTask, onGetWebpackConfig, applyMethod } = api;
const { userConfig = {}, webpack } = context;
const { RawSource } = webpack.sources || webpackSources;
const getWebpackBase = getValue(GET_RAX_APP_WEBPACK_CONFIG);
const tempDir = getValue('TEMP_PATH');
const chainConfig = getWebpackBase(api, {
target,
babelConfigOptions: { styleSheet: userConfig.inlineStyle },
progressOptions: {
name: 'Kraken',
},
});
chainConfig.name(target);
chainConfig.taskName = target;

setEntry(chainConfig, context);

registerTask(target, chainConfig);

onGetWebpackConfig(target, (config) => {
const { command } = context;
const krakenConfig = userConfig.kraken || {};
const staticConfig = getValue('staticConfig');

if (krakenConfig.mpa) {
  setMPAConfig.default(api, config, {
    type: 'kraken',
    targetDir: tempDir,
    entries: getMpaEntries(api, {
      target,
      appJsonContent: staticConfig,
    }),
  });
}

if (command === 'start') {
  applyMethod('rax.injectHotReloadEntries', config);
}

});

onGetWebpackConfig(target, (config) => {
config
.plugin('BuildKBCPlugin')
.use(class BuildKBCPlugin {
apply(compiler) {
const qjsc = new Qjsc();
processAssets({
pluginName: 'BuildKBCPlugin',
compiler,
}, ({ compilation, assets, callback }) => {
const injected = applyMethod('rax.getInjectedHTML');

        Object.keys(assets).forEach((chunkFile) => {
          if (/\.js$/i.test(chunkFile)) {
            const kbcFilename = chunkFile.replace(/(\.js)$/i, '.kbc1');
            const cssFilename = chunkFile.replace(/(\.js)$/i, '.css');

            let injectCode = '';

            const appendCode = (code) => {
              injectCode += code;
            };

            if (injected.metas.length > 0) {
              appendCode(getInjectContent(injected.metas.join(''), 'document.head'));
            }

            if (injected.links.length > 0) {
              appendCode(getInjectContent(injected.links.join(''), 'document.head'));
            }

            if (injected.scripts.length > 0) {
              appendCode(getInjectContent(injected.scripts.join('')));
            }

            if (injected.comboScripts.length > 0) {
              const comboUrl = `https://g.alicdn.com/??${injected.comboScripts.map((s) => s.src).join(',')}`;
              appendCode(getInjectJS(comboUrl));
            }

            if (cssFilename in assets) {
              // Inject to load non-inlined css file.
              const css = assets[cssFilename];
              appendCode(getInjectStyle(css.source()));
            }

            const jsContent = assets[chunkFile].source();
            const bytecode = qjsc.compile(`${injectCode}\n${jsContent}`);
            emitAsset(compilation, kbcFilename, new RawSource(bytecode));
          }
        });
        callback();
      });
    }
  });

});
};

这里比较关键的两个方法:registerTask和onGetWebpackConfig。他们就是为每种构建任务,修改webpack配置。
// 通过registerTask注册,存放初始的webpack-chain配置
private configArr: ITaskConfig[];

public registerTask: IRegisterTask = (name, chainConfig) => {
const exist = this.configArr.find((v): boolean => v.name === name);
if (!exist) {
this.configArr.push({
name,
chainConfig,
modifyFunctions: [],
});
} else {
throw new Error([Error] config '${name}' already exists!);
}
};
public onGetWebpackConfig: IOnGetWebpackConfig = (
...args: IOnGetWebpackConfigArgs
) => {
this.modifyConfigFns.push(args);
};
最后,我们这边的就是start模块。
const commandModule = this.getCommandModule({ command, commandArgs, userConfig: this.userConfig });
return commandModule(this, options);
export = async function(context: Context, options?: IRunOptions): Promise<void | ITaskConfig[] | WebpackDevServer> {
const { eject } = options || {};
const configArr = context.getWebpackConfig();
const { command, commandArgs, webpack, applyHook } = context;
await applyHook(before.${command}.load, { args: commandArgs, webpackConfig: configArr });
// eject config
if (eject) {
return configArr;
}

if (!configArr.length) {
const errorMsg = 'No webpack config found.';
log.warn('CONFIG', errorMsg);
await applyHook(error, { err: new Error(errorMsg) });
return;
}

let serverUrl = '';
let devServerConfig: DevServerConfig = {
port: commandArgs.port || 3333,
host: commandArgs.host || '0.0.0.0',
https: commandArgs.https || false,
};

for (const item of configArr) {
const { chainConfig } = item;
const config = chainConfig.toConfig() as WebpackOptionsNormalized;
if (config.devServer) {
devServerConfig = deepmerge(devServerConfig, config.devServer);
}
// if --port or process.env.PORT has been set, overwrite option port
if (process.env.USE_CLI_PORT) {
devServerConfig.port = commandArgs.port;
}
}

const webpackConfig = configArr.map(v => v.chainConfig.toConfig());
await applyHook(before.${command}.run, {
args: commandArgs,
config: webpackConfig,
});

let compiler;
try {
compiler = webpack(webpackConfig);
} catch (err) {
log.error('CONFIG', chalk.red('Failed to load webpack config.'));
await applyHook(error, { err });
throw err;
}
const protocol = devServerConfig.https ? 'https' : 'http';
const urls = prepareURLs(
protocol,
devServerConfig.host,
devServerConfig.port,
);
serverUrl = urls.localUrlForBrowser;

let isFirstCompile = true;
// typeof(stats) is webpack.compilation.MultiStats
compiler.hooks.done.tap('compileHook', async stats => {
const isSuccessful = webpackStats({
urls,
stats,
isFirstCompile,
});
if (isSuccessful) {
isFirstCompile = false;
}
await applyHook(after.${command}.compile, {
url: serverUrl,
urls,
isFirstCompile,
stats,
});
});

let devServer: WebpackDevServer;
// require webpack-dev-server after context setup
// context may hijack webpack resolve
// eslint-disable-next-line @typescript-eslint/no-var-requires
const DevServer = require('webpack-dev-server');

// static method getFreePort in v4
if (DevServer.getFreePort) {
devServer = new DevServer(devServerConfig, compiler);
} else {
devServer = new DevServer(compiler, devServerConfig);
}

await applyHook(before.${command}.devServer, {
url: serverUrl,
urls,
devServer,
});
if (devServer.startCallback) {
devServer.startCallback(
() => {
applyHook(after.${command}.devServer, {
url: serverUrl,
urls,
devServer,
});
},
);
} else {
devServer.listen(devServerConfig.port, devServerConfig.host, async (err: Error) => {
if (err) {
log.info('WEBPACK',chalk.red('[ERR]: Failed to start webpack dev server'));
log.error('WEBPACK', (err.stack || err.toString()));
}
await applyHook(after.${command}.devServer, {
url: serverUrl,
urls,
devServer,
err,
});
});
}

return devServer;
};
这里真正生成WDS的地方。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant