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');
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);
});
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;
}
}
//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) { /**
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}
; }) );/**
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); } });
})(); }
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({});
} 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 || {};
} 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');
});
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');
}); };
这里比较关键的两个方法: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的地方。