huenchao / questions

每天想到的问题,都放在issue中。
6 stars 2 forks source link

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

Open huenchao opened 2 years ago

huenchao commented 2 years ago

//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) { /**

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的地方。