From 34ece1be977c1f2eb80fb757f64f6b6bf3602317 Mon Sep 17 00:00:00 2001 From: ClarkXia Date: Thu, 15 Jul 2021 11:04:36 +0800 Subject: [PATCH 01/21] fix: ts types --- packages/build-scripts/package.json | 6 +- packages/build-scripts/src/commands/start.ts | 7 +- packages/build-scripts/src/core/Context.ts | 4 +- .../src/utils/formatWebpackMessages.ts | 121 ++++++++++++++++++ .../build-scripts/src/utils/webpackStats.ts | 15 +-- 5 files changed, 135 insertions(+), 18 deletions(-) create mode 100644 packages/build-scripts/src/utils/formatWebpackMessages.ts diff --git a/packages/build-scripts/package.json b/packages/build-scripts/package.json index 6b0d888..76e7703 100644 --- a/packages/build-scripts/package.json +++ b/packages/build-scripts/package.json @@ -34,7 +34,6 @@ "json5": "^2.1.3", "lodash": "^4.17.15", "npmlog": "^4.1.2", - "react-dev-utils": "^9.0.4", "semver": "^7.3.2", "yargs-parser": "^14.0.0" }, @@ -44,12 +43,11 @@ "@types/json5": "^0.0.30", "@types/lodash": "^4.14.147", "@types/npmlog": "^4.1.2", - "@types/react-dev-utils": "^9.0.1", "@types/semver": "^6.2.0", - "@types/webpack": "^4.39.8", + "@types/webpack": "^4.41.30", "@types/webpack-chain": "^5.2.0", + "jest": "^27.0.6", "package-json": "^6.5.0", - "ts-jest": "^26.4.4", "typescript": "^3.7.2", "webpack": "^5.0.0", "webpack-dev-server": "^3.7.2" diff --git a/packages/build-scripts/src/commands/start.ts b/packages/build-scripts/src/commands/start.ts index a7e53b6..dc4833b 100644 --- a/packages/build-scripts/src/commands/start.ts +++ b/packages/build-scripts/src/commands/start.ts @@ -1,4 +1,5 @@ import chalk from 'chalk'; +import { WebpackOptionsNormalized } from 'webpack'; import Context, { CommandArgs, IGetBuiltInPlugins, IPluginList, ITaskConfig } from '../core/Context'; import webpackStats from '../utils/webpackStats'; @@ -7,6 +8,8 @@ import WebpackDevServer = require('webpack-dev-server') import prepareURLs = require('../utils/prepareURLs') import log = require('../utils/log') +type DevServer = Record; + export = async function({ args, rootDir, @@ -58,7 +61,7 @@ export = async function({ return; } - let devServerConfig = { + let devServerConfig: DevServer = { port: args.port || 3333, host: args.host || '0.0.0.0', https: args.https || false, @@ -66,7 +69,7 @@ export = async function({ for (const item of configArr) { const { chainConfig } = item; - const config = chainConfig.toConfig(); + const config = chainConfig.toConfig() as WebpackOptionsNormalized; if (config.devServer) { devServerConfig = deepmerge(devServerConfig, config.devServer); } diff --git a/packages/build-scripts/src/core/Context.ts b/packages/build-scripts/src/core/Context.ts index 7356710..156aee3 100644 --- a/packages/build-scripts/src/core/Context.ts +++ b/packages/build-scripts/src/core/Context.ts @@ -1,5 +1,6 @@ import { AggregatedResult } from '@jest/test-result'; import { GlobalConfig } from '@jest/types/build/Config'; +import webpack, { MultiStats } from 'webpack'; import { Logger } from 'npmlog'; import { IHash, Json, JsonValue, MaybeArray, MaybePromise, JsonArray } from '../types'; import hijackWebpackResolve from '../utils/hijackWebpack'; @@ -9,7 +10,6 @@ import assert = require('assert') import fs = require('fs-extra') import _ = require('lodash') import camelCase = require('camelcase') -import webpack = require('webpack') import WebpackChain = require('webpack-chain') import WebpackDevServer = require('webpack-dev-server') import log = require('../utils/log') @@ -59,7 +59,7 @@ export interface IJestResult { export interface IOnHookCallbackArg { err?: Error; args?: CommandArgs; - stats?: webpack.compilation.MultiStats; + stats?: MultiStats; url?: string; devServer?: WebpackDevServer; config?: any; diff --git a/packages/build-scripts/src/utils/formatWebpackMessages.ts b/packages/build-scripts/src/utils/formatWebpackMessages.ts new file mode 100644 index 0000000..6047019 --- /dev/null +++ b/packages/build-scripts/src/utils/formatWebpackMessages.ts @@ -0,0 +1,121 @@ +// fork from https://github.com/facebook/create-react-app/blob/main/packages/react-dev-utils/formatWebpackMessages.js +import { StatsCompilation } from 'webpack'; + +const friendlySyntaxErrorLabel = 'Syntax error:'; + +type IsLikelyASyntaxError = (message: string) => boolean +type Message = string | { message: string } | { message: string }[] +type FormatMessage = (message: Message) => string +type FormatWebpackMessages = (json: StatsCompilation) => { errors: string[]; warnings: string[]} + +const isLikelyASyntaxError: IsLikelyASyntaxError = (message) => { + return message.indexOf(friendlySyntaxErrorLabel) !== -1; +}; + +// Cleans up webpack error messages. +const formatMessage: FormatMessage = (message) => { + let formattedMessage = message; + let lines: string[] = []; + + if (typeof formattedMessage === 'string') { + lines = formattedMessage.split('\n'); + } else if ('message' in formattedMessage) { + lines = formattedMessage.message?.split('\n'); + } else if (Array.isArray(formattedMessage)) { + formattedMessage.forEach(messageData => { + if ('message' in messageData) { + lines = messageData.message?.split('\n'); + } + }); + } + + // Strip webpack-added headers off errors/warnings + // https://github.com/webpack/webpack/blob/master/lib/ModuleError.js + lines = lines.filter(line => !/Module [A-z ]+\(from/.test(line)); + + // Transform parsing error into syntax error + // TODO: move this to our ESLint formatter? + lines = lines.map(line => { + const parsingError = /Line (\d+):(?:(\d+):)?\s*Parsing error: (.+)$/.exec(line); + if (!parsingError) { + return line; + } + const [, errorLine, errorColumn, errorMessage] = parsingError; + return `${friendlySyntaxErrorLabel} ${errorMessage} (${errorLine}:${errorColumn})`; + }); + + formattedMessage = lines.join('\n'); + // Smoosh syntax errors (commonly found in CSS) + formattedMessage = formattedMessage.replace( + /SyntaxError\s+\((\d+):(\d+)\)\s*(.+?)\n/g, + `${friendlySyntaxErrorLabel} $3 ($1:$2)\n`, + ); + // Clean up export errors + formattedMessage = formattedMessage.replace( + /^.*export '(.+?)' was not found in '(.+?)'.*$/gm, + `Attempted import error: '$1' is not exported from '$2'.`, + ); + formattedMessage = formattedMessage.replace( + /^.*export 'default' \(imported as '(.+?)'\) was not found in '(.+?)'.*$/gm, + `Attempted import error: '$2' does not contain a default export (imported as '$1').`, + ); + formattedMessage = formattedMessage.replace( + /^.*export '(.+?)' \(imported as '(.+?)'\) was not found in '(.+?)'.*$/gm, + `Attempted import error: '$1' is not exported from '$3' (imported as '$2').`, + ); + lines = formattedMessage.split('\n'); + + // Remove leading newline + if (lines.length > 2 && lines[1].trim() === '') { + lines.splice(1, 1); + } + // Clean up file name + lines[0] = lines[0].replace(/^(.*) \d+:\d+-\d+$/, '$1'); + + // Cleans up verbose "module not found" messages for files and packages. + if (lines[1] && lines[1].indexOf('Module not found: ') === 0) { + lines = [ + lines[0], + lines[1] + .replace('Error: ', '') + .replace('Module not found: Cannot find file:', 'Cannot find file:'), + ]; + } + + // Add helpful message for users trying to use Sass for the first time + if (lines[1] && lines[1].match(/Cannot find module.+sass/)) { + lines[1] = 'To import Sass files, you first need to install sass.\n'; + lines[1] += + 'Run `npm install sass` or `yarn add sass` inside your workspace.'; + } + + formattedMessage = lines.join('\n'); + // Internal stacks are generally useless so we strip them... with the + // exception of stacks containing `webpack:` because they're normally + // from user code generated by webpack. For more information see + // https://github.com/facebook/create-react-app/pull/1050 + formattedMessage = formattedMessage.replace(/^\s*at\s((?!webpack:).)*:\d+:\d+[\s)]*(\n|$)/gm,''); + // at ... ...:x:y + formattedMessage = formattedMessage.replace(/^\s*at\s(\n|$)/gm, ''); // at + lines = formattedMessage.split('\n'); + + // Remove duplicated newlines + lines = lines.filter((line, index, arr) => index === 0 || line.trim() !== '' || line.trim() !== arr[index - 1].trim()); + + // Reassemble the message + formattedMessage = lines.join('\n'); + return formattedMessage.trim(); +}; + +const formatWebpackMessages: FormatWebpackMessages = (json) => { + const formattedErrors = json.errors.map(formatMessage); + const formattedWarnings = json.warnings.map(formatMessage); + const result = { errors: formattedErrors, warnings: formattedWarnings }; + if (result.errors.some(isLikelyASyntaxError)) { + // If there are any syntax errors, show just them. + result.errors = result.errors.filter(isLikelyASyntaxError); + } + return result; +}; + +export default formatWebpackMessages; \ No newline at end of file diff --git a/packages/build-scripts/src/utils/webpackStats.ts b/packages/build-scripts/src/utils/webpackStats.ts index 1c59374..ec16338 100644 --- a/packages/build-scripts/src/utils/webpackStats.ts +++ b/packages/build-scripts/src/utils/webpackStats.ts @@ -1,7 +1,6 @@ import chalk from 'chalk'; - -import webpack = require('webpack') -import formatWebpackMessages = require('react-dev-utils/formatWebpackMessages') +import { MultiStats, Stats } from 'webpack'; +import formatWebpackMessages from './formatWebpackMessages'; import log = require('./log') interface IUrls { @@ -12,8 +11,8 @@ interface IUrls { } interface IWebpackStatsParams { - stats: webpack.Stats | webpack.compilation.MultiStats; - statsOptions?: webpack.Stats.ToJsonOptions; + stats: Stats | MultiStats; + statsOptions?: Record; urls?: IUrls; isFirstCompile?: boolean; } @@ -38,16 +37,12 @@ const defaultOptions = { }; const webpackStats: IWebpackStats = ({ urls, stats, statsOptions = defaultOptions, isFirstCompile }) => { - const statsJson = (stats as webpack.Stats).toJson({ + const statsJson = stats.toJson({ all: false, errors: true, warnings: true, timings: true, }); - // compatible with webpack 5 - ['errors', 'warnings'].forEach((jsonKey: string) => { - (statsJson as any)[jsonKey] = ((statsJson as any)[jsonKey] || []).map((item: string | IJsonItem ) => ((item as IJsonItem).message || item)); - }); const messages = formatWebpackMessages(statsJson); const isSuccessful = !messages.errors.length; if (!process.env.DISABLE_STATS) { From 8b9a816dee66fbfa532b5970f268f96b05ca5ffb Mon Sep 17 00:00:00 2001 From: ClarkXia Date: Thu, 15 Jul 2021 11:11:46 +0800 Subject: [PATCH 02/21] fix: test case --- packages/build-scripts/package.json | 1 + packages/build-scripts/src/core/Context.ts | 2 +- packages/build-scripts/test/pluginAPI.test.ts | 10 ++++++++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/build-scripts/package.json b/packages/build-scripts/package.json index 76e7703..3be0e25 100644 --- a/packages/build-scripts/package.json +++ b/packages/build-scripts/package.json @@ -48,6 +48,7 @@ "@types/webpack-chain": "^5.2.0", "jest": "^27.0.6", "package-json": "^6.5.0", + "ts-jest": "^27.0.3", "typescript": "^3.7.2", "webpack": "^5.0.0", "webpack-dev-server": "^3.7.2" diff --git a/packages/build-scripts/src/core/Context.ts b/packages/build-scripts/src/core/Context.ts index 156aee3..3d68931 100644 --- a/packages/build-scripts/src/core/Context.ts +++ b/packages/build-scripts/src/core/Context.ts @@ -136,7 +136,7 @@ export interface IRegsiterMethod { (name: string, fn: IMethodFunction, options?: IMethodOptions): void; } -type IMethod = [string, string]; +type IMethod = [string, string]|string; export interface IApplyMethod { (config: IMethod, ...args: any[]): any; diff --git a/packages/build-scripts/test/pluginAPI.test.ts b/packages/build-scripts/test/pluginAPI.test.ts index aca642d..aecb8e7 100644 --- a/packages/build-scripts/test/pluginAPI.test.ts +++ b/packages/build-scripts/test/pluginAPI.test.ts @@ -23,8 +23,14 @@ describe('api regsiterMethod/applyMethod', () => { }) it('api applyMethod unregistered', () => { - const err: any = context.applyMethod('unregistered') - expect(err instanceof Error).toBe(true) + let error = false; + try { + const err: any = context.applyMethod('unregistered') + } catch (err) { + error = true; + } + + expect(error).toBe(true) }) }); From 3d6c1fcde48a7bc340334ef0f9ed7d2f23f6586e Mon Sep 17 00:00:00 2001 From: ClarkXia Date: Thu, 15 Jul 2021 11:28:30 +0800 Subject: [PATCH 03/21] fix: prettier code --- packages/build-scripts/src/commands/build.ts | 26 +- packages/build-scripts/src/commands/start.ts | 63 ++- packages/build-scripts/src/commands/test.ts | 59 +-- packages/build-scripts/src/core/Context.ts | 437 +++++++++++------- packages/build-scripts/src/index.ts | 12 +- packages/build-scripts/src/types/index.ts | 12 +- packages/build-scripts/src/types/module.d.ts | 2 +- .../src/utils/checkNodeVersion.ts | 9 +- .../src/utils/formatWebpackMessages.ts | 41 +- .../build-scripts/src/utils/getCliOptions.ts | 8 +- .../build-scripts/src/utils/hijackWebpack.ts | 26 +- packages/build-scripts/src/utils/log.ts | 7 +- .../build-scripts/src/utils/prepareURLs.ts | 16 +- .../build-scripts/src/utils/webpackStats.ts | 19 +- 14 files changed, 465 insertions(+), 272 deletions(-) diff --git a/packages/build-scripts/src/commands/build.ts b/packages/build-scripts/src/commands/build.ts index 660e059..b1c5c62 100644 --- a/packages/build-scripts/src/commands/build.ts +++ b/packages/build-scripts/src/commands/build.ts @@ -1,11 +1,16 @@ import chalk from 'chalk'; -import Context, { CommandArgs, IPluginList, IGetBuiltInPlugins, ITaskConfig } from '../core/Context'; +import Context, { + CommandArgs, + IPluginList, + IGetBuiltInPlugins, + ITaskConfig, +} from '../core/Context'; import webpackStats from '../utils/webpackStats'; -import webpack = require('webpack') -import fs = require('fs-extra') -import path = require('path') -import log = require('../utils/log') +import webpack = require('webpack'); +import fs = require('fs-extra'); +import path = require('path'); +import log = require('../utils/log'); export = async function({ args, @@ -19,7 +24,7 @@ export = async function({ eject?: boolean; plugins?: IPluginList; getBuiltInPlugins?: IGetBuiltInPlugins; -}): Promise { +}): Promise { const command = 'build'; const context = new Context({ @@ -30,7 +35,10 @@ export = async function({ getBuiltInPlugins, }); - log.verbose('OPTIONS', `${command} cliOptions: ${JSON.stringify(args, null, 2)}`); + log.verbose( + 'OPTIONS', + `${command} cliOptions: ${JSON.stringify(args, null, 2)}`, + ); const { applyHook, rootDir: ctxRoot, webpack: webpackInstance } = context; let configArr = []; @@ -86,7 +94,7 @@ export = async function({ // typeof(stats) is webpack.compilation.MultiStats compiler.run((err, stats) => { if (err) { - log.error('WEBPACK', (err.stack || err.toString())); + log.error('WEBPACK', err.stack || err.toString()); reject(err); return; } @@ -105,4 +113,4 @@ export = async function({ }); await applyHook(`after.${command}.compile`, result); -} +}; diff --git a/packages/build-scripts/src/commands/start.ts b/packages/build-scripts/src/commands/start.ts index dc4833b..1fde48a 100644 --- a/packages/build-scripts/src/commands/start.ts +++ b/packages/build-scripts/src/commands/start.ts @@ -1,12 +1,17 @@ import chalk from 'chalk'; import { WebpackOptionsNormalized } from 'webpack'; -import Context, { CommandArgs, IGetBuiltInPlugins, IPluginList, ITaskConfig } from '../core/Context'; +import Context, { + CommandArgs, + IGetBuiltInPlugins, + IPluginList, + ITaskConfig, +} from '../core/Context'; import webpackStats from '../utils/webpackStats'; -import deepmerge = require('deepmerge') -import WebpackDevServer = require('webpack-dev-server') -import prepareURLs = require('../utils/prepareURLs') -import log = require('../utils/log') +import deepmerge = require('deepmerge'); +import WebpackDevServer = require('webpack-dev-server'); +import prepareURLs = require('../utils/prepareURLs'); +import log = require('../utils/log'); type DevServer = Record; @@ -33,7 +38,10 @@ export = async function({ getBuiltInPlugins, }); - log.verbose('OPTIONS', `${command} cliOptions: ${JSON.stringify(args, null, 2)}`); + log.verbose( + 'OPTIONS', + `${command} cliOptions: ${JSON.stringify(args, null, 2)}`, + ); let serverUrl = ''; const { applyHook, webpack } = context; @@ -91,12 +99,16 @@ export = async function({ throw err; } const protocol = devServerConfig.https ? 'https' : 'http'; - const urls = prepareURLs(protocol, devServerConfig.host, devServerConfig.port); + 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) => { + compiler.hooks.done.tap('compileHook', async stats => { const isSuccessful = webpackStats({ urls, stats, @@ -124,19 +136,26 @@ export = async function({ devServer, }); - 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, - }); - }); + 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; -} +}; diff --git a/packages/build-scripts/src/commands/test.ts b/packages/build-scripts/src/commands/test.ts index 2ff9156..fc3f849 100644 --- a/packages/build-scripts/src/commands/test.ts +++ b/packages/build-scripts/src/commands/test.ts @@ -2,9 +2,9 @@ import { runCLI } from 'jest'; import chalk from 'chalk'; import Context, { IContextOptions } from '../core/Context'; -import fs = require('fs-extra') -import path = require('path') -import log = require('../utils/log') +import fs = require('fs-extra'); +import path = require('path'); +import log = require('../utils/log'); export = async function({ args, @@ -41,24 +41,27 @@ export = async function({ let userJestConfig = { moduleNameMapper: {} }; if (fs.existsSync(jestConfigPath)) { - userJestConfig = require(jestConfigPath) // eslint-disable-line + userJestConfig = require(jestConfigPath); // eslint-disable-line } // get webpack.resolve.alias - const alias: { [key: string]: string } = configArr.reduce((acc, {chainConfig}) => { - const webpackConfig = chainConfig.toConfig(); - if (webpackConfig.resolve && webpackConfig.resolve.alias) { - return { - ...acc, - ...webpackConfig.resolve.alias, - }; - } else { - return acc; - } - }, {}); + const alias: { [key: string]: string } = configArr.reduce( + (acc, { chainConfig }) => { + const webpackConfig = chainConfig.toConfig(); + if (webpackConfig.resolve && webpackConfig.resolve.alias) { + return { + ...acc, + ...webpackConfig.resolve.alias, + }; + } else { + return acc; + } + }, + {}, + ); const aliasModuleNameMapper: { [key: string]: string } = {}; - Object.keys(alias || {}).forEach((key) => { + Object.keys(alias || {}).forEach(key => { const aliasPath = alias[key]; // check path if it is a directory if (fs.existsSync(aliasPath) && fs.statSync(aliasPath).isDirectory()) { @@ -89,19 +92,21 @@ export = async function({ config: JSON.stringify(jestConfig), }, [ctxRoot], - ).then((data) => { - const { results } = data; - if (results.success) { - resolve(data); - } else { - reject(new Error('Jest failed')); - } - }).catch((err: Error) => { - log.error('JEST', (err.stack || err.toString())); - }); + ) + .then(data => { + const { results } = data; + if (results.success) { + resolve(data); + } else { + reject(new Error('Jest failed')); + } + }) + .catch((err: Error) => { + log.error('JEST', err.stack || err.toString()); + }); }); await applyHook(`after.${command}`, { result }); return result; -} +}; diff --git a/packages/build-scripts/src/core/Context.ts b/packages/build-scripts/src/core/Context.ts index 3d68931..b663533 100644 --- a/packages/build-scripts/src/core/Context.ts +++ b/packages/build-scripts/src/core/Context.ts @@ -1,18 +1,25 @@ -import { AggregatedResult } from '@jest/test-result'; -import { GlobalConfig } from '@jest/types/build/Config'; +import * as path from 'path'; +import * as fs from 'fs'; +import _ from 'lodash'; +import camelCase from 'camelCase'; import webpack, { MultiStats } from 'webpack'; import { Logger } from 'npmlog'; -import { IHash, Json, JsonValue, MaybeArray, MaybePromise, JsonArray } from '../types'; +import { AggregatedResult } from '@jest/test-result'; +import { GlobalConfig } from '@jest/types/build/Config'; +import { + IHash, + Json, + JsonValue, + MaybeArray, + MaybePromise, + JsonArray, +} from '../types'; import hijackWebpackResolve from '../utils/hijackWebpack'; -import path = require('path') -import assert = require('assert') -import fs = require('fs-extra') -import _ = require('lodash') -import camelCase = require('camelcase') -import WebpackChain = require('webpack-chain') -import WebpackDevServer = require('webpack-dev-server') -import log = require('../utils/log') +import assert = require('assert'); +import WebpackChain = require('webpack-chain'); +import WebpackDevServer = require('webpack-dev-server'); +import log = require('../utils/log'); import JSON5 = require('json5'); const PKG_FILE = 'package.json'; @@ -41,15 +48,15 @@ const BUILTIN_CLI_OPTIONS = [ { name: 'config', commands: ['start', 'build', 'test'] }, ]; -export type IWebpack = typeof webpack +export type IWebpack = typeof webpack; -export type PluginContext = Pick +export type PluginContext = Pick; export type UserConfigContext = PluginContext & { taskName: string; -} +}; -export type ValidationKey = keyof typeof VALIDATION_MAP +export type ValidationKey = keyof typeof VALIDATION_MAP; export interface IJestResult { results: AggregatedResult; @@ -136,7 +143,7 @@ export interface IRegsiterMethod { (name: string, fn: IMethodFunction, options?: IMethodOptions): void; } -type IMethod = [string, string]|string; +type IMethod = [string, string] | string; export interface IApplyMethod { (config: IMethod, ...args: any[]): any; @@ -155,7 +162,7 @@ export interface IModifyConfig { } export interface IModifyUserConfig { - (configKey: string|IModifyConfig, value?: any): void; + (configKey: string | IModifyConfig, value?: any): void; } export interface IGetAllPlugin { @@ -187,17 +194,17 @@ export interface IPluginInfo { options: IPluginOptions; } -export type IPluginOptions = Json | JsonArray +export type IPluginOptions = Json | JsonArray; export interface IPlugin { (api: IPluginAPI, options?: IPluginOptions): MaybePromise; } -export type CommandName = 'start' | 'build' | 'test' +export type CommandName = 'start' | 'build' | 'test'; -export type CommandArgs = IHash +export type CommandArgs = IHash; -export type IPluginList = (string | [string, Json])[] +export type IPluginList = (string | [string, Json])[]; export type IGetBuiltInPlugins = (userConfig: IUserConfig) => IPluginList; @@ -241,65 +248,79 @@ export interface ICliOptionRegistration { export interface IModifyConfigRegistration { (configFunc: IModifyRegisteredConfigCallbacks): void; - (configName: string, configFunc: IModifyRegisteredConfigCallbacks): void; -}; + ( + configName: string, + configFunc: IModifyRegisteredConfigCallbacks, + ): void; +} export interface IModifyCliRegistration { (configFunc: IModifyRegisteredConfigCallbacks): void; - (configName: string, configFunc: IModifyRegisteredConfigCallbacks): void; -}; + ( + configName: string, + configFunc: IModifyRegisteredConfigCallbacks, + ): void; +} -export type IModifyRegisteredConfigArgs = [string, IModifyRegisteredConfigCallbacks] | [IModifyRegisteredConfigCallbacks] -export type IModifyRegisteredCliArgs = [string, IModifyRegisteredConfigCallbacks] | [IModifyRegisteredConfigCallbacks] +export type IModifyRegisteredConfigArgs = + | [string, IModifyRegisteredConfigCallbacks] + | [IModifyRegisteredConfigCallbacks]; +export type IModifyRegisteredCliArgs = + | [string, IModifyRegisteredConfigCallbacks] + | [IModifyRegisteredConfigCallbacks]; -export type IOnGetWebpackConfigArgs = [string, IPluginConfigWebpack] | [IPluginConfigWebpack] +export type IOnGetWebpackConfigArgs = + | [string, IPluginConfigWebpack] + | [IPluginConfigWebpack]; -export type IRegistrationKey = 'modifyConfigRegistrationCallbacks' | 'modifyCliRegistrationCallbacks'; +export type IRegistrationKey = + | 'modifyConfigRegistrationCallbacks' + | 'modifyCliRegistrationCallbacks'; class Context { - public command: CommandName + public command: CommandName; - public commandArgs: CommandArgs + public commandArgs: CommandArgs; - public rootDir: string + public rootDir: string; - public webpack: IWebpack + public webpack: IWebpack; // 通过registerTask注册,存放初始的webpack-chain配置 - private configArr: ITaskConfig[] + private configArr: ITaskConfig[]; - private modifyConfigFns: IOnGetWebpackConfigArgs[] + private modifyConfigFns: IOnGetWebpackConfigArgs[]; - private modifyJestConfig: IJestConfigFunction[] + private modifyJestConfig: IJestConfigFunction[]; - private modifyConfigRegistrationCallbacks: IModifyRegisteredConfigArgs[] + private modifyConfigRegistrationCallbacks: IModifyRegisteredConfigArgs[]; - private modifyCliRegistrationCallbacks: IModifyRegisteredConfigArgs[] + private modifyCliRegistrationCallbacks: IModifyRegisteredConfigArgs[]; private eventHooks: { [name: string]: IOnHookCallback[]; - } + }; - private internalValue: IHash + private internalValue: IHash; - private userConfigRegistration: IUserConfigRegistration + private userConfigRegistration: IUserConfigRegistration; - private cliOptionRegistration: ICliOptionRegistration + private cliOptionRegistration: ICliOptionRegistration; - private methodRegistration: {[name: string]: [IMethodFunction, any]} + private methodRegistration: { [name: string]: [IMethodFunction, any] }; - private cancelTaskNames: string[] + private cancelTaskNames: string[]; - public pkg: Json + public pkg: Json; - public userConfig: IUserConfig + public userConfig: IUserConfig; - public plugins: IPluginInfo[] + public plugins: IPluginInfo[]; constructor({ command, rootDir = process.cwd(), - args = {} , + args = {}, plugins = [], getBuiltInPlugins = () => [], }: IContextOptions) { @@ -329,9 +350,14 @@ class Context { this.pkg = this.getProjectFile(PKG_FILE); this.userConfig = this.getUserConfig(); // run getBuiltInPlugins before resolve webpack while getBuiltInPlugins may add require hook for webpack - const builtInPlugins: IPluginList = [...plugins, ...getBuiltInPlugins(this.userConfig)]; + const builtInPlugins: IPluginList = [ + ...plugins, + ...getBuiltInPlugins(this.userConfig), + ]; // custom webpack - const webpackInstancePath = this.userConfig.customWebpack ? require.resolve('webpack', { paths: [this.rootDir] }) : 'webpack'; + const webpackInstancePath = this.userConfig.customWebpack + ? require.resolve('webpack', { paths: [this.rootDir] }) + : 'webpack'; this.webpack = require(webpackInstancePath); if (this.userConfig.customWebpack) { hijackWebpackResolve(this.webpack, this.rootDir); @@ -342,10 +368,18 @@ class Context { this.plugins = this.resolvePlugins(builtInPlugins); } - private registerConfig = (type: string, args: MaybeArray | MaybeArray, parseName?: (name: string) => string): void => { - const registerKey = `${type}Registration` as 'userConfigRegistration' | 'cliOptionRegistration'; + private registerConfig = ( + type: string, + args: MaybeArray | MaybeArray, + parseName?: (name: string) => string, + ): void => { + const registerKey = `${type}Registration` as + | 'userConfigRegistration' + | 'cliOptionRegistration'; if (!this[registerKey]) { - throw new Error(`unknown register type: ${type}, use available types (userConfig or cliOption) instead`); + throw new Error( + `unknown register type: ${type}, use available types (userConfig or cliOption) instead`, + ); } const configArr = _.isArray(args) ? args : [args]; configArr.forEach((conf): void => { @@ -357,20 +391,28 @@ class Context { this[registerKey][confName] = conf; // set default userConfig - if (type === 'userConfig' - && _.isUndefined(this.userConfig[confName]) - && Object.prototype.hasOwnProperty.call(conf, 'defaultValue')) { + if ( + type === 'userConfig' && + _.isUndefined(this.userConfig[confName]) && + Object.prototype.hasOwnProperty.call(conf, 'defaultValue') + ) { this.userConfig[confName] = (conf as IUserConfigArgs).defaultValue; } }); - } + }; - private async runConfigWebpack(fn: IUserConfigWebpack, configValue: JsonValue, ignoreTasks: string[]|null): Promise { + private async runConfigWebpack( + fn: IUserConfigWebpack, + configValue: JsonValue, + ignoreTasks: string[] | null, + ): Promise { for (const webpackConfigInfo of this.configArr) { const taskName = webpackConfigInfo.name; let ignoreConfig = false; if (Array.isArray(ignoreTasks)) { - ignoreConfig = ignoreTasks.some((ignoreTask) => new RegExp(ignoreTask).exec(taskName)); + ignoreConfig = ignoreTasks.some(ignoreTask => + new RegExp(ignoreTask).exec(taskName), + ); } if (!ignoreConfig) { const userConfigContext: UserConfigContext = { @@ -391,18 +433,23 @@ class Context { try { config = fs.readJsonSync(configPath); } catch (err) { - log.info('CONFIG', `Fail to load config file ${configPath}, use empty object`); + log.info( + 'CONFIG', + `Fail to load config file ${configPath}, use empty object`, + ); } } return config; - } + }; private getUserConfig = (): IUserConfig => { const { config } = this.commandArgs; let configPath = ''; if (config) { - configPath = path.isAbsolute(config) ? config : path.resolve(this.rootDir, config); + configPath = path.isAbsolute(config) + ? config + : path.resolve(this.rootDir, config); } else { configPath = path.resolve(this.rootDir, USER_CONFIG_FILE); } @@ -412,29 +459,43 @@ class Context { const isJsFile = path.extname(configPath) === '.js'; if (fs.existsSync(configPath)) { try { - userConfig = isJsFile ? require(configPath) : JSON5.parse(fs.readFileSync(configPath, 'utf-8')); // read build.json + userConfig = isJsFile + ? require(configPath) + : JSON5.parse(fs.readFileSync(configPath, 'utf-8')); // read build.json } catch (err) { - log.info('CONFIG', `Fail to load config file ${configPath}, use default config instead`); - log.error('CONFIG', (err.stack || err.toString())); + log.info( + 'CONFIG', + `Fail to load config file ${configPath}, use default config instead`, + ); + log.error('CONFIG', err.stack || err.toString()); process.exit(1); } } return this.mergeModeConfig(userConfig); - } + }; private mergeModeConfig = (userConfig: IUserConfig): IUserConfig => { const { mode } = this.commandArgs; // modify userConfig by userConfig.modeConfig - if (userConfig.modeConfig && mode && (userConfig.modeConfig as IModeConfig)[mode]) { - const { plugins, ...basicConfig } = (userConfig.modeConfig as IModeConfig)[mode] as IUserConfig; + if ( + userConfig.modeConfig && + mode && + (userConfig.modeConfig as IModeConfig)[mode] + ) { + const { + plugins, + ...basicConfig + } = (userConfig.modeConfig as IModeConfig)[mode] as IUserConfig; const userPlugins = [...userConfig.plugins]; if (Array.isArray(plugins)) { - const pluginKeys = userPlugins.map((pluginInfo) => { + const pluginKeys = userPlugins.map(pluginInfo => { return Array.isArray(pluginInfo) ? pluginInfo[0] : pluginInfo; }); - plugins.forEach((pluginInfo) => { - const [pluginName] = Array.isArray(pluginInfo) ? pluginInfo : [pluginInfo]; + plugins.forEach(pluginInfo => { + const [pluginName] = Array.isArray(pluginInfo) + ? pluginInfo + : [pluginInfo]; const pluginIndex = pluginKeys.indexOf(pluginName); if (pluginIndex > -1) { // overwrite plugin info by modeConfig @@ -445,50 +506,65 @@ class Context { } }); } - return { ...userConfig, ...basicConfig, plugins: userPlugins}; + return { ...userConfig, ...basicConfig, plugins: userPlugins }; } return userConfig; - } + }; private resolvePlugins = (builtInPlugins: IPluginList): IPluginInfo[] => { - const userPlugins = [...builtInPlugins, ...(this.userConfig.plugins || [])].map((pluginInfo): IPluginInfo => { - let fn; - if (_.isFunction(pluginInfo)) { + const userPlugins = [ + ...builtInPlugins, + ...(this.userConfig.plugins || []), + ].map( + (pluginInfo): IPluginInfo => { + let fn; + if (_.isFunction(pluginInfo)) { + return { + fn: pluginInfo, + options: {}, + }; + } + const plugins: [string, IPluginOptions] = Array.isArray(pluginInfo) + ? pluginInfo + : [pluginInfo, undefined]; + const pluginResolveDir = process.env.EXTRA_PLUGIN_DIR + ? [process.env.EXTRA_PLUGIN_DIR, this.rootDir] + : [this.rootDir]; + const pluginPath = path.isAbsolute(plugins[0]) + ? plugins[0] + : require.resolve(plugins[0], { paths: pluginResolveDir }); + const options = plugins[1]; + + try { + fn = require(pluginPath); // eslint-disable-line + } catch (err) { + log.error('CONFIG', `Fail to load plugin ${pluginPath}`); + log.error('CONFIG', err.stack || err.toString()); + process.exit(1); + } + return { - fn: pluginInfo, - options: {}, + name: plugins[0], + pluginPath, + fn: fn.default || fn || ((): void => {}), + options, }; - } - const plugins: [string, IPluginOptions] = Array.isArray(pluginInfo) ? pluginInfo : [pluginInfo, undefined]; - const pluginResolveDir = process.env.EXTRA_PLUGIN_DIR ? [process.env.EXTRA_PLUGIN_DIR, this.rootDir] : [this.rootDir]; - const pluginPath = path.isAbsolute(plugins[0]) ? plugins[0] : require.resolve(plugins[0], { paths: pluginResolveDir }); - const options = plugins[1]; - - try { - fn = require(pluginPath) // eslint-disable-line - } catch (err) { - log.error('CONFIG', `Fail to load plugin ${pluginPath}`); - log.error('CONFIG', (err.stack || err.toString())); - process.exit(1); - } - - return { - name: plugins[0], - pluginPath, - fn: fn.default || fn || ((): void => {}), - options, - }; - }); + }, + ); return userPlugins; - } - - public getAllPlugin: IGetAllPlugin = (dataKeys = ['pluginPath', 'options', 'name']) => { - return this.plugins.map((pluginInfo): Partial => { - // filter fn to avoid loop - return _.pick(pluginInfo, dataKeys); - }); - } + }; + + public getAllPlugin: IGetAllPlugin = ( + dataKeys = ['pluginPath', 'options', 'name'], + ) => { + return this.plugins.map( + (pluginInfo): Partial => { + // filter fn to avoid loop + return _.pick(pluginInfo, dataKeys); + }, + ); + }; public registerTask: IRegisterTask = (name, chainConfig) => { const exist = this.configArr.find((v): boolean => v.name === name); @@ -501,15 +577,15 @@ class Context { } else { throw new Error(`[Error] config '${name}' already exists!`); } - } + }; - public cancelTask: ICancelTask = (name) => { + public cancelTask: ICancelTask = name => { if (this.cancelTaskNames.includes(name)) { log.info('TASK', `task ${name} has already been canceled`); } else { this.cancelTaskNames.push(name); } - } + }; public registerMethod: IRegsiterMethod = (name, fn, options) => { if (this.methodRegistration[name]) { @@ -518,12 +594,14 @@ class Context { const registration = [fn, options] as [IMethodFunction, IMethodOptions]; this.methodRegistration[name] = registration; } - } + }; - public applyMethod: IApplyMethod = (config, ...args) => { + public applyMethod: IApplyMethod = (config, ...args) => { const [methodName, pluginName] = Array.isArray(config) ? config : [config]; if (this.methodRegistration[methodName]) { - const [registerMethod, methodOptions] = this.methodRegistration[methodName]; + const [registerMethod, methodOptions] = this.methodRegistration[ + methodName + ]; if (methodOptions?.pluginName) { return (registerMethod as IMethodCurry)(pluginName)(...args); } else { @@ -532,11 +610,11 @@ class Context { } else { throw new Error(`apply unknown method ${methodName}`); } - } + }; - public hasMethod: IHasMethod = (name) => { + public hasMethod: IHasMethod = name => { return !!this.methodRegistration[name]; - } + }; public modifyUserConfig: IModifyUserConfig = (configKey, value) => { const errorMsg = 'config plugins is not support to be modified'; @@ -552,34 +630,40 @@ class Context { log.warn('[waring]', errorMsg); } delete modifiedValue.plugins; - Object.keys(modifiedValue).forEach((configKey) => { + Object.keys(modifiedValue).forEach(configKey => { this.userConfig[configKey] = modifiedValue[configKey]; }); } else { throw new Error(`modifyUserConfig must return a plain object`); } } - } + }; - public modifyConfigRegistration: IModifyConfigRegistration = (...args: IModifyRegisteredConfigArgs) => { + public modifyConfigRegistration: IModifyConfigRegistration = ( + ...args: IModifyRegisteredConfigArgs + ) => { this.modifyConfigRegistrationCallbacks.push(args); - } + }; - public modifyCliRegistration: IModifyCliRegistration = (...args: IModifyRegisteredCliArgs) => { + public modifyCliRegistration: IModifyCliRegistration = ( + ...args: IModifyRegisteredCliArgs + ) => { this.modifyCliRegistrationCallbacks.push(args); - } + }; public getAllTask = (): string[] => { return this.configArr.map(v => v.name); - } + }; - public onGetWebpackConfig: IOnGetWebpackConfig = (...args: IOnGetWebpackConfigArgs) => { + public onGetWebpackConfig: IOnGetWebpackConfig = ( + ...args: IOnGetWebpackConfigArgs + ) => { this.modifyConfigFns.push(args); - } + }; public onGetJestConfig: IOnGetJestConfig = (fn: IJestConfigFunction) => { this.modifyJestConfig.push(fn); - } + }; public runJestConfig = (jestConfig: Json): Json => { let result = jestConfig; @@ -587,14 +671,14 @@ class Context { result = fn(result); } return result; - } + }; public onHook: IOnHook = (key, fn) => { if (!Array.isArray(this.eventHooks[key])) { this.eventHooks[key] = []; } this.eventHooks[key].push(fn); - } + }; public applyHook = async (key: string, opts = {}): Promise => { const hooks = this.eventHooks[key] || []; @@ -603,25 +687,25 @@ class Context { // eslint-disable-next-line no-await-in-loop await fn(opts); } - } + }; public setValue = (key: string | number, value: any): void => { this.internalValue[key] = value; - } + }; public getValue = (key: string | number): any => { return this.internalValue[key]; - } + }; public registerUserConfig = (args: MaybeArray): void => { this.registerConfig('userConfig', args); - } + }; public registerCliOption = (args: MaybeArray): void => { - this.registerConfig('cliOption', args, (name) => { + this.registerConfig('cliOption', args, name => { return camelCase(name, { pascalCase: false }); }); - } + }; private runPlugins = async (): Promise => { for (const pluginInfo of this.plugins) { @@ -655,11 +739,11 @@ class Context { // eslint-disable-next-line no-await-in-loop await fn(pluginAPI, options); } - } + }; private checkPluginValue = (plugins: IPluginList): void => { let flag; - if(!_.isArray(plugins)) { + if (!_.isArray(plugins)) { flag = false; } else { flag = plugins.every(v => { @@ -672,22 +756,32 @@ class Context { }); } - if(!flag) { + if (!flag) { throw new Error('plugins did not pass validation'); } - } + }; private runConfigModification = async (): Promise => { - const callbackRegistrations = ['modifyConfigRegistrationCallbacks', 'modifyCliRegistrationCallbacks']; - callbackRegistrations.forEach((registrationKey) => { - const registrations = this[registrationKey as IRegistrationKey] as (IModifyRegisteredConfigArgs | IModifyRegisteredConfigArgs)[]; + const callbackRegistrations = [ + 'modifyConfigRegistrationCallbacks', + 'modifyCliRegistrationCallbacks', + ]; + callbackRegistrations.forEach(registrationKey => { + const registrations = this[registrationKey as IRegistrationKey] as ( + | IModifyRegisteredConfigArgs + | IModifyRegisteredConfigArgs + )[]; registrations.forEach(([name, callback]) => { const modifyAll = _.isFunction(name); - const configRegistrations = this[registrationKey === 'modifyConfigRegistrationCallbacks' ? 'userConfigRegistration' : 'cliOptionRegistration']; + const configRegistrations = this[ + registrationKey === 'modifyConfigRegistrationCallbacks' + ? 'userConfigRegistration' + : 'cliOptionRegistration' + ]; if (modifyAll) { const modifyFunction = name as IModifyRegisteredConfigCallbacks; const modifiedResult = modifyFunction(configRegistrations); - Object.keys(modifiedResult).forEach((configKey) => { + Object.keys(modifiedResult).forEach(configKey => { configRegistrations[configKey] = { ...(configRegistrations[configKey] || {}), ...modifiedResult[configKey], @@ -700,12 +794,12 @@ class Context { const configRegistration = configRegistrations[name]; configRegistrations[name] = { ...configRegistration, - ...(callback(configRegistration)), + ...callback(configRegistration), }; } }); }); - } + }; private runUserConfig = async (): Promise => { for (const configInfoKey in this.userConfig) { @@ -713,7 +807,9 @@ class Context { const configInfo = this.userConfigRegistration[configInfoKey]; if (!configInfo) { - throw new Error(`[Config File] Config key '${configInfoKey}' is not supported`); + throw new Error( + `[Config File] Config key '${configInfoKey}' is not supported`, + ); } const { name, validation, ignoreTasks } = configInfo; @@ -724,53 +820,72 @@ class Context { if (_.isString(validation)) { // split validation string const supportTypes = validation.split('|') as ValidationKey[]; - const validateResult = supportTypes.some((supportType) => { + const validateResult = supportTypes.some(supportType => { const fnName = VALIDATION_MAP[supportType]; if (!fnName) { throw new Error(`validation does not support ${supportType}`); } return _[fnName](configValue); }); - assert(validateResult, `Config ${name} should be ${validation}, but got ${configValue}`); + assert( + validateResult, + `Config ${name} should be ${validation}, but got ${configValue}`, + ); } else { // eslint-disable-next-line no-await-in-loop validationInfo = await validation(configValue); - assert(validationInfo, `${name} did not pass validation, result: ${validationInfo}`); + assert( + validationInfo, + `${name} did not pass validation, result: ${validationInfo}`, + ); } } if (configInfo.configWebpack) { // eslint-disable-next-line no-await-in-loop - await this.runConfigWebpack(configInfo.configWebpack, configValue, ignoreTasks); + await this.runConfigWebpack( + configInfo.configWebpack, + configValue, + ignoreTasks, + ); } } } - } + }; private runCliOption = async (): Promise => { for (const cliOpt in this.commandArgs) { // allow all jest option when run command test if (this.command !== 'test' || cliOpt !== 'jestArgv') { - const { commands, name, configWebpack, ignoreTasks } = this.cliOptionRegistration[cliOpt] || {}; + const { commands, name, configWebpack, ignoreTasks } = + this.cliOptionRegistration[cliOpt] || {}; if (!name || !(commands || []).includes(this.command)) { - throw new Error(`cli option '${cliOpt}' is not supported when run command '${this.command}'`); + throw new Error( + `cli option '${cliOpt}' is not supported when run command '${this.command}'`, + ); } if (configWebpack) { // eslint-disable-next-line no-await-in-loop - await this.runConfigWebpack(configWebpack, this.commandArgs[cliOpt], ignoreTasks); + await this.runConfigWebpack( + configWebpack, + this.commandArgs[cliOpt], + ignoreTasks, + ); } } } - } + }; private runWebpackFunctions = async (): Promise => { this.modifyConfigFns.forEach(([name, func]) => { const isAll = _.isFunction(name); - if (isAll) { // modify all + if (isAll) { + // modify all this.configArr.forEach(config => { config.modifyFunctions.push(name as IPluginConfigWebpack); }); - } else { // modify named config + } else { + // modify named config this.configArr.forEach(config => { if (config.name === name) { config.modifyFunctions.push(func); @@ -785,7 +900,7 @@ class Context { await func(configInfo.chainConfig); } } - } + }; public setUp = async (): Promise => { await this.runPlugins(); @@ -794,9 +909,11 @@ class Context { await this.runWebpackFunctions(); await this.runCliOption(); // filter webpack config by cancelTaskNames - this.configArr = this.configArr.filter((config) => !this.cancelTaskNames.includes(config.name)); + this.configArr = this.configArr.filter( + config => !this.cancelTaskNames.includes(config.name), + ); return this.configArr; - } + }; } export default Context; diff --git a/packages/build-scripts/src/index.ts b/packages/build-scripts/src/index.ts index d151f9e..bef7725 100644 --- a/packages/build-scripts/src/index.ts +++ b/packages/build-scripts/src/index.ts @@ -1,11 +1,7 @@ -import build = require('./commands/build') -import start = require('./commands/start') -import test = require('./commands/test') +import build = require('./commands/build'); +import start = require('./commands/start'); +import test = require('./commands/test'); export * from './core/Context'; export * from './types'; -export { - build, - start, - test, -}; +export { build, start, test }; diff --git a/packages/build-scripts/src/types/index.ts b/packages/build-scripts/src/types/index.ts index d4d96e6..47b1596 100644 --- a/packages/build-scripts/src/types/index.ts +++ b/packages/build-scripts/src/types/index.ts @@ -2,12 +2,12 @@ export interface IHash { [name: string]: T; } -export type Json = IHash - -export type JsonArray = (string|number|boolean|Date|Json|JsonArray)[] +export type Json = IHash; -export type JsonValue = Json[keyof Json] +export type JsonArray = (string | number | boolean | Date | Json | JsonArray)[]; -export type MaybeArray = T | T[] +export type JsonValue = Json[keyof Json]; -export type MaybePromise = T | Promise +export type MaybeArray = T | T[]; + +export type MaybePromise = T | Promise; diff --git a/packages/build-scripts/src/types/module.d.ts b/packages/build-scripts/src/types/module.d.ts index f9646d9..6509ec4 100644 --- a/packages/build-scripts/src/types/module.d.ts +++ b/packages/build-scripts/src/types/module.d.ts @@ -7,4 +7,4 @@ declare module '@alifd/fusion-collector' { cmdType?: string; }): void; export { collectDetail }; -} \ No newline at end of file +} diff --git a/packages/build-scripts/src/utils/checkNodeVersion.ts b/packages/build-scripts/src/utils/checkNodeVersion.ts index 312fd42..0cb35a3 100644 --- a/packages/build-scripts/src/utils/checkNodeVersion.ts +++ b/packages/build-scripts/src/utils/checkNodeVersion.ts @@ -1,10 +1,13 @@ -import semver = require('semver') -import log = require('./log') +import semver = require('semver'); +import log = require('./log'); export = function checkNodeVersion(requireNodeVersion: string): void { if (!semver.satisfies(process.version, requireNodeVersion)) { log.error('ENV', `You are using Node ${process.version}`); - log.error('ENV', `build-scripts requires Node ${requireNodeVersion}, please update Node.`); + log.error( + 'ENV', + `build-scripts requires Node ${requireNodeVersion}, please update Node.`, + ); process.exit(1); } }; diff --git a/packages/build-scripts/src/utils/formatWebpackMessages.ts b/packages/build-scripts/src/utils/formatWebpackMessages.ts index 6047019..157b348 100644 --- a/packages/build-scripts/src/utils/formatWebpackMessages.ts +++ b/packages/build-scripts/src/utils/formatWebpackMessages.ts @@ -3,17 +3,19 @@ import { StatsCompilation } from 'webpack'; const friendlySyntaxErrorLabel = 'Syntax error:'; -type IsLikelyASyntaxError = (message: string) => boolean -type Message = string | { message: string } | { message: string }[] -type FormatMessage = (message: Message) => string -type FormatWebpackMessages = (json: StatsCompilation) => { errors: string[]; warnings: string[]} - -const isLikelyASyntaxError: IsLikelyASyntaxError = (message) => { +type IsLikelyASyntaxError = (message: string) => boolean; +type Message = string | { message: string } | { message: string }[]; +type FormatMessage = (message: Message) => string; +type FormatWebpackMessages = ( + json: StatsCompilation, +) => { errors: string[]; warnings: string[] }; + +const isLikelyASyntaxError: IsLikelyASyntaxError = message => { return message.indexOf(friendlySyntaxErrorLabel) !== -1; }; // Cleans up webpack error messages. -const formatMessage: FormatMessage = (message) => { +const formatMessage: FormatMessage = message => { let formattedMessage = message; let lines: string[] = []; @@ -36,7 +38,9 @@ const formatMessage: FormatMessage = (message) => { // Transform parsing error into syntax error // TODO: move this to our ESLint formatter? lines = lines.map(line => { - const parsingError = /Line (\d+):(?:(\d+):)?\s*Parsing error: (.+)$/.exec(line); + const parsingError = /Line (\d+):(?:(\d+):)?\s*Parsing error: (.+)$/.exec( + line, + ); if (!parsingError) { return line; } @@ -94,20 +98,31 @@ const formatMessage: FormatMessage = (message) => { // exception of stacks containing `webpack:` because they're normally // from user code generated by webpack. For more information see // https://github.com/facebook/create-react-app/pull/1050 - formattedMessage = formattedMessage.replace(/^\s*at\s((?!webpack:).)*:\d+:\d+[\s)]*(\n|$)/gm,''); + formattedMessage = formattedMessage.replace( + /^\s*at\s((?!webpack:).)*:\d+:\d+[\s)]*(\n|$)/gm, + '', + ); // at ... ...:x:y - formattedMessage = formattedMessage.replace(/^\s*at\s(\n|$)/gm, ''); // at + formattedMessage = formattedMessage.replace( + /^\s*at\s(\n|$)/gm, + '', + ); // at lines = formattedMessage.split('\n'); // Remove duplicated newlines - lines = lines.filter((line, index, arr) => index === 0 || line.trim() !== '' || line.trim() !== arr[index - 1].trim()); + lines = lines.filter( + (line, index, arr) => + index === 0 || + line.trim() !== '' || + line.trim() !== arr[index - 1].trim(), + ); // Reassemble the message formattedMessage = lines.join('\n'); return formattedMessage.trim(); }; -const formatWebpackMessages: FormatWebpackMessages = (json) => { +const formatWebpackMessages: FormatWebpackMessages = json => { const formattedErrors = json.errors.map(formatMessage); const formattedWarnings = json.warnings.map(formatMessage); const result = { errors: formattedErrors, warnings: formattedWarnings }; @@ -118,4 +133,4 @@ const formatWebpackMessages: FormatWebpackMessages = (json) => { return result; }; -export default formatWebpackMessages; \ No newline at end of file +export default formatWebpackMessages; diff --git a/packages/build-scripts/src/utils/getCliOptions.ts b/packages/build-scripts/src/utils/getCliOptions.ts index 8c70b3a..711bd39 100644 --- a/packages/build-scripts/src/utils/getCliOptions.ts +++ b/packages/build-scripts/src/utils/getCliOptions.ts @@ -1,10 +1,10 @@ /** * get cli options by program */ -import {Command, Option} from 'commander'; -import {IHash, JsonValue} from '../types'; +import { Command, Option } from 'commander'; +import { IHash, JsonValue } from '../types'; -import camelcase = require('camelcase') +import camelcase = require('camelcase'); module.exports = (program: Command): IHash => { const cliOptions: IHash = {}; @@ -15,7 +15,7 @@ module.exports = (program: Command): IHash => { // 不传参数时是 undefined,这里不判断的话,lib/build 里跟 default 参数 merge 会有问题 // version等参数的类型为function,需要过滤掉 - if ((program[key] !== undefined) && (typeof program[key] !== 'function')) { + if (program[key] !== undefined && typeof program[key] !== 'function') { cliOptions[key] = program[key]; } }); diff --git a/packages/build-scripts/src/utils/hijackWebpack.ts b/packages/build-scripts/src/utils/hijackWebpack.ts index cc37949..ec7f704 100644 --- a/packages/build-scripts/src/utils/hijackWebpack.ts +++ b/packages/build-scripts/src/utils/hijackWebpack.ts @@ -10,22 +10,34 @@ function hijackWebpackResolve(webpack: any, rootDir: string): void { const originalResolver = (Module as any)._resolveFilename; // eslint-disable-next-line no-underscore-dangle - (Module as any)._resolveFilename = function (request: string, parent: string, isMain: boolean, options: IOptions): void { + (Module as any)._resolveFilename = function( + request: string, + parent: string, + isMain: boolean, + options: IOptions, + ): void { if (request.match(webpackRegex)) { const newOptions: IOptions = { paths: [] }; if (options?.paths) { - newOptions.paths = options.paths?.includes(rootDir) ? options.paths : options.paths?.concat(rootDir); + newOptions.paths = options.paths?.includes(rootDir) + ? options.paths + : options.paths?.concat(rootDir); } else { newOptions.paths.push(rootDir); } - return originalResolver.apply(this, [request, parent, isMain, newOptions]); + return originalResolver.apply(this, [ + request, + parent, + isMain, + newOptions, + ]); } return originalResolver.apply(this, [request, parent, isMain, options]); }; // eslint-disable-next-line no-underscore-dangle const originalLoader = (Module as any)._load; // eslint-disable-next-line no-underscore-dangle - (Module as any)._load = function (request: string, parent: object) { + (Module as any)._load = function(request: string, parent: object) { let moduleRequest = request; // ignore case which pass parent if (parent) { @@ -33,8 +45,8 @@ function hijackWebpackResolve(webpack: any, rootDir: string): void { return webpack; } else if (request.match(webpackRegex)) { try { - moduleRequest = require.resolve(request, {paths: [rootDir]}); - } catch(e) { + moduleRequest = require.resolve(request, { paths: [rootDir] }); + } catch (e) { // ignore error } } @@ -43,4 +55,4 @@ function hijackWebpackResolve(webpack: any, rootDir: string): void { }; } -export default hijackWebpackResolve; \ No newline at end of file +export default hijackWebpackResolve; diff --git a/packages/build-scripts/src/utils/log.ts b/packages/build-scripts/src/utils/log.ts index 4664769..b771177 100644 --- a/packages/build-scripts/src/utils/log.ts +++ b/packages/build-scripts/src/utils/log.ts @@ -1,7 +1,8 @@ -import npmlog = require('npmlog') +import npmlog = require('npmlog'); const envs = ['verbose', 'info', 'error', 'warn']; -const logLevel = envs.indexOf(process.env.LOG_LEVEL) !== -1 ? process.env.LOG_LEVEL : 'info'; +const logLevel = + envs.indexOf(process.env.LOG_LEVEL) !== -1 ? process.env.LOG_LEVEL : 'info'; npmlog.level = logLevel; @@ -9,4 +10,4 @@ npmlog.level = logLevel; // log.verbose // log.info // log.error -export = npmlog +export = npmlog; diff --git a/packages/build-scripts/src/utils/prepareURLs.ts b/packages/build-scripts/src/utils/prepareURLs.ts index 7d66435..30da9da 100644 --- a/packages/build-scripts/src/utils/prepareURLs.ts +++ b/packages/build-scripts/src/utils/prepareURLs.ts @@ -8,8 +8,8 @@ import chalk from 'chalk'; -import url = require('url') -import address = require('address') +import url = require('url'); +import address = require('address'); interface IPrepareUrls { lanUrlForConfig: any; @@ -19,7 +19,12 @@ interface IPrepareUrls { localUrlForBrowser: string; } -export = function prepareUrls(protocol: string, host: string, port: number, pathname = '/'): IPrepareUrls { +export = function prepareUrls( + protocol: string, + host: string, + port: number, + pathname = '/', +): IPrepareUrls { const formatUrl = (hostname: string): string => url.format({ protocol, @@ -51,7 +56,8 @@ export = function prepareUrls(protocol: string, host: string, port: number, path if ( /^10[.]|^30[.]|^172[.](1[6-9]|2[0-9]|3[0-1])[.]|^192[.]168[.]/.test( lanUrlForConfig, - ) || process.env.USE_PUBLIC_IP + ) || + process.env.USE_PUBLIC_IP ) { // Address is private, format it for later use lanUrlForTerminal = prettyPrintUrl(lanUrlForConfig); @@ -76,4 +82,4 @@ export = function prepareUrls(protocol: string, host: string, port: number, path localUrlForTerminal, localUrlForBrowser, }; -} +}; diff --git a/packages/build-scripts/src/utils/webpackStats.ts b/packages/build-scripts/src/utils/webpackStats.ts index ec16338..433e837 100644 --- a/packages/build-scripts/src/utils/webpackStats.ts +++ b/packages/build-scripts/src/utils/webpackStats.ts @@ -1,7 +1,7 @@ import chalk from 'chalk'; import { MultiStats, Stats } from 'webpack'; import formatWebpackMessages from './formatWebpackMessages'; -import log = require('./log') +import log = require('./log'); interface IUrls { lanUrlForTerminal: string; @@ -36,7 +36,12 @@ const defaultOptions = { modules: false, }; -const webpackStats: IWebpackStats = ({ urls, stats, statsOptions = defaultOptions, isFirstCompile }) => { +const webpackStats: IWebpackStats = ({ + urls, + stats, + statsOptions = defaultOptions, + isFirstCompile, +}) => { const statsJson = stats.toJson({ all: false, errors: true, @@ -60,8 +65,14 @@ const webpackStats: IWebpackStats = ({ urls, stats, statsOptions = defaultOption if (isFirstCompile && urls) { console.log(); log.info('WEBPACK', chalk.green('Starting the development server at:')); - log.info(' - Local : ', chalk.underline.white(urls.localUrlForBrowser)); - log.info(' - Network: ', chalk.underline.white(urls.lanUrlForTerminal)); + log.info( + ' - Local : ', + chalk.underline.white(urls.localUrlForBrowser), + ); + log.info( + ' - Network: ', + chalk.underline.white(urls.lanUrlForTerminal), + ); console.log(); } } else if (messages.errors.length) { From b84147ef45548d2190547a794365071edef65f91 Mon Sep 17 00:00:00 2001 From: ClarkXia Date: Thu, 15 Jul 2021 15:47:45 +0800 Subject: [PATCH 04/21] feat: refactor context --- README.md | 164 ++++++++---------- packages/build-scripts/bin/build-scripts.js | 4 +- .../build-scripts/bin/child-process-start.js | 2 +- packages/build-scripts/bin/start.js | 10 +- packages/build-scripts/jest.config.js | 2 +- packages/build-scripts/src/commands/build.ts | 105 +---------- packages/build-scripts/src/commands/start.ts | 149 +--------------- packages/build-scripts/src/commands/test.ts | 101 +---------- packages/build-scripts/src/core/Context.ts | 38 ++-- .../build-scripts/src/service/JestService.ts | 18 ++ .../src/service/WebpackService.ts | 62 +++++++ packages/build-scripts/src/service/build.ts | 75 ++++++++ packages/build-scripts/src/service/start.ts | 112 ++++++++++++ packages/build-scripts/src/service/test.ts | 106 +++++++++++ 14 files changed, 496 insertions(+), 452 deletions(-) create mode 100644 packages/build-scripts/src/service/JestService.ts create mode 100644 packages/build-scripts/src/service/WebpackService.ts create mode 100644 packages/build-scripts/src/service/build.ts create mode 100644 packages/build-scripts/src/service/start.ts create mode 100644 packages/build-scripts/src/service/test.ts diff --git a/README.md b/README.md index aa93162..eed8a82 100644 --- a/README.md +++ b/README.md @@ -78,10 +78,7 @@ build-scripts 本身不耦合具体的工程构建逻辑,所以如果希望上 "externals": { "react": "React" }, - "plugins": [ - "build-plugin-component", - "./build.plugin.js" - ] + "plugins": ["build-plugin-component", "./build.plugin.js"] } ``` @@ -100,9 +97,7 @@ build.json 中核心包括两部分内容: ```json { - "plugins": [ - "build-plugin-component" - ] + "plugins": ["build-plugin-component"] } ``` @@ -112,9 +107,12 @@ build.json 中核心包括两部分内容: { "plugins": [ // 数组第一项为插件名,第二项为插件参数 - ["build-plugin-fusion", { - "themePackage": "@icedesign/theme" - }] + [ + "build-plugin-fusion", + { + "themePackage": "@icedesign/theme" + } + ] ] } ``` @@ -126,20 +124,15 @@ build.json 中核心包括两部分内容: ```js module.exports = ({ context, onGetWebpackConfig }) => { // 这里面可以写哪些,具体请查看插件开发章节 - onGetWebpackConfig((config) => { - }); -} + onGetWebpackConfig(config => {}); +}; ``` 最后在 `build.json` 里引入自定义插件即可: ```json - { - "plugins": [ - "build-plugin-component", - "./build.plugin.js" - ] + "plugins": ["build-plugin-component", "./build.plugin.js"] } ``` @@ -187,7 +180,7 @@ module.exports = ({ context }) => { const { userConfig, command, webpack } = context; console.log('userConfig', userConfig); console.log('command', command); -} +}; ``` #### onGetWebpackConfig @@ -206,11 +199,11 @@ module.exports = ({ onGetWebpackConfig }) => { module.exports = ({onGetWebpackConfig, registerTask}) => { registerTask('web', webpackConfigWeb); registerTask('weex', webpackConfigWeex); - + onGetWebpackConfig('web',(config) => { config.entry('src/index'); }); - + onGetWebpackConfig('weex',(config) => { config.entry('src/app'); }); @@ -223,43 +216,42 @@ module.exports = ({onGetWebpackConfig, registerTask}) => { ```js module.exports = ({ onHook }) => { - onHook('before.start.load', () => { - // do something before dev - }); - onHook('after.build.compile', (stats) => { - // do something after build - }); -} + onHook('before.start.load', () => { + // do something before dev + }); + onHook('after.build.compile', stats => { + // do something after build + }); +}; ``` 目前的命令执行生命周期如下: start 命令: -| 生命周期 | 参数 | 调用时机 | -| ---- | ---- | ---- | -| before.start.load | { args: CommandArgs; webpackConfig: WebpackConfig[] } | 获取 webpack 配置之前 | -| before.start.run | { args: CommandArgs; webpackConfig: WebpackConfig[] } | webpack 执行构建之前 | -| after.start.compile | { url: string; stats: WebpackAssets; isFirstCompile: boolean } | 编译结束,每次重新编译都会执行 | -| before.start.devServer | { url: string; devServer: WebpackDevServer } | server 中间件加载后,webpack devServer 启动前 | -| after.start.devServer | { url: string; devServer: WebpackDevServer; err: Error } | webpack devServer 启动后 | +| 生命周期 | 参数 | 调用时机 | +| ---------------------- | -------------------------------------------------------------- | --------------------------------------------- | +| before.start.load | { args: CommandArgs; webpackConfig: WebpackConfig[] } | 获取 webpack 配置之前 | +| before.start.run | { args: CommandArgs; webpackConfig: WebpackConfig[] } | webpack 执行构建之前 | +| after.start.compile | { url: string; stats: WebpackAssets; isFirstCompile: boolean } | 编译结束,每次重新编译都会执行 | +| before.start.devServer | { url: string; devServer: WebpackDevServer } | server 中间件加载后,webpack devServer 启动前 | +| after.start.devServer | { url: string; devServer: WebpackDevServer; err: Error } | webpack devServer 启动后 | build 命令: -| 生命周期 | 参数 | 调用时机 | -| ---- | ---- | ---- | -| before.build.load | { args: CommandArgs; webpackConfig: WebpackConfig[] } | 获取 webpack 配置之前 | -| before.build.run | { args: CommandArgs; webpackConfig: WebpackConfig[] } | webpack 执行构建之前 | -| after.build.compile | { url: string; stats: WebpackAssets; isFirstCompile } | 编译结束 | +| 生命周期 | 参数 | 调用时机 | +| ------------------- | ----------------------------------------------------- | --------------------- | +| before.build.load | { args: CommandArgs; webpackConfig: WebpackConfig[] } | 获取 webpack 配置之前 | +| before.build.run | { args: CommandArgs; webpackConfig: WebpackConfig[] } | webpack 执行构建之前 | +| after.build.compile | { url: string; stats: WebpackAssets; isFirstCompile } | 编译结束 | test 命令: -| 生命周期 | 参数 | 调用时机 | -| ---- | ---- | ---- | -| before.test.load | { args: CommandArgs; webpackConfig: WebpackConfig[] } | 获取 jest 配置之前 | -| before.test.run | { args: CommandArgs; config: JestConfig } | jest 执行构建之前 | -| after.test | { result: JestResult } | 测试结束 | - +| 生命周期 | 参数 | 调用时机 | +| ---------------- | ----------------------------------------------------- | ------------------ | +| before.test.load | { args: CommandArgs; webpackConfig: WebpackConfig[] } | 获取 jest 配置之前 | +| before.test.run | { args: CommandArgs; config: JestConfig } | jest 执行构建之前 | +| after.test | { result: JestResult } | 测试结束 | #### log @@ -271,7 +263,7 @@ module.exports = ({ log }) => { log.info('info'); log.error('error'); log.warn('warn'); -} +}; ``` ### 进阶 API @@ -287,7 +279,7 @@ module.exports = ({ log }) => { module.exports = ({ registerTask }) => { registerTask('web', webpackConfigWeb); registerTask('component', webpackConfigComponent); -} +}; ``` #### cancelTask @@ -297,7 +289,7 @@ module.exports = ({ registerTask }) => { ```js module.exports = ({ cancelTask }) => { cancelTask('web'); -} +}; ``` #### registerUserConfig @@ -315,7 +307,7 @@ module.exports = ({ cancelTask }) => { - validation(string|function) -字段校验,支持string快速校验,string|boolean|number,也可以自定义函数,根据return值判断校验结果 +字段校验,支持 string 快速校验,string|boolean|number,也可以自定义函数,根据 return 值判断校验结果 - ignoreTasks(string[]) @@ -325,23 +317,23 @@ module.exports = ({ cancelTask }) => { 字段效果,具体作用到 webpack 配置上,接收参数: - - config:webpack-chain 形式的配置 - - value: build.json 中的字段值 - - context:与外部 context 相同,新增字段 taskName 表现当前正在修改的task +- config:webpack-chain 形式的配置 +- value: build.json 中的字段值 +- context:与外部 context 相同,新增字段 taskName 表现当前正在修改的 task ```js module.exports = ({ registerUserConfig }) => { registerUserConfig({ name: 'entry', // validation: 'string', - validation: (value) => { - return typeof value === 'string' + validation: value => { + return typeof value === 'string'; }, configWebpack: (config, value, context) => { - config.mode(value) + config.mode(value); }, }); -} +}; ``` #### modifyConfigRegistration @@ -350,14 +342,14 @@ module.exports = ({ registerUserConfig }) => { ```js module.exports = ({ modifyConfigRegistration }) => { - modifyConfigRegistration('name', (configRegistration) => { + modifyConfigRegistration('name', configRegistration => { return { ...configRegistration, // 修正验证字段 validation: 'string', - } + }; }); -} +}; ``` #### modifyUserConfig @@ -366,11 +358,11 @@ module.exports = ({ modifyConfigRegistration }) => { ```js module.exports = ({ modifyUserConfig }) => { - modifyUserConfig((originConfig) => { + modifyUserConfig(originConfig => { // 通过函数返回批量修改 - return { ...originConfig, define: { target: 'xxxx'}}; + return { ...originConfig, define: { target: 'xxxx' } }; }); -} +}; ``` > API 执行的生命周期:所有插件对于修改配置函数将保存至 modifyConfigRegistration 中,在 runUserConfig 执行前完成对当前 userConfig 内容的修改 @@ -383,12 +375,12 @@ module.exports = ({ modifyUserConfig }) => { module.exports = ({ registerCliOption }) => { registerCliOption({ name: 'https', // 注册的 cli 参数名称, - commands: ['start'], // 支持的命令,如果为空默认任何命令都将执行注册方法 + commands: ['start'], // 支持的命令,如果为空默认任何命令都将执行注册方法 configWebpack: (config, value, context) => { // 对应命令链路上的需要执行的相关操作 - } - }) -} + }, + }); +}; ``` > 注册函数执行周期,在 userConfig 相关注册函数执行之后。 @@ -399,14 +391,14 @@ module.exports = ({ registerCliOption }) => { ```js module.exports = ({ modifyConfigRegistration }) => { - modifyConfigRegistration('https', (cliRegistration) => { + modifyConfigRegistration('https', cliRegistration => { return { ...cliRegistration, // 修正 commands 字段 commands: ['start'], - } + }; }); -} +}; ``` #### getAllTask @@ -414,11 +406,10 @@ module.exports = ({ modifyConfigRegistration }) => { 用于获取所有注入任务的名称: ```js - module.exports = ({ getAllTask }) => { const taskNames = getAllTask(); // ['web', 'miniapp'] -} +}; ``` ### 插件通信 @@ -433,22 +424,22 @@ module.exports = ({ getAllTask }) => { #### setValue -用来在context中注册变量,以供插件之间的通信。 +用来在 context 中注册变量,以供插件之间的通信。 ```js module.exports = ({ setValue }) => { setValue('key', 123); -} +}; ``` #### getValue -用来获取context中注册的变量。 +用来获取 context 中注册的变量。 ```js -module.exports = ({getValue}) => { +module.exports = ({ getValue }) => { const value = getValue('key'); // 123 -} +}; ``` #### registerMethod @@ -458,11 +449,11 @@ module.exports = ({getValue}) => { ```js module.exports = ({ registerMethod }) => { // 注册方法 - registerMethod('pipeAppRouterBefore', (content) => { + registerMethod('pipeAppRouterBefore', content => { // 执行相关注册逻辑,可以返回相应的值 return true; }); -} +}; ``` #### applyMethod @@ -474,7 +465,7 @@ module.exports = ({ applyMethod }) => { // 使用其他差价注册方法的方式,如果插件未注册,将返回一个 error 类型的错误 // 类似 new Error(`apply unkown method ${name}`) const result = applyMethod('pipeAppRouterBefore', 'content'); -} +}; ``` ## 升级到 1.x @@ -499,23 +490,20 @@ build-scripts 1.x 中不再耦合具体的 webpack 和 jest 版本,建议在 ## 工程生态 -| Project | Version | Docs | Description | -|----------------|-----------------------------------------|--------------|-----------| -| [icejs] | [![icejs-status]][icejs-package] | [docs][icejs-docs] |A universal framework based on react| -| [rax-scripts] | [![rax-scripts-status]][rax-scripts-package] | [docs][rax-scripts-docs] |Rax official engineering tools use @alib/build-scripts| +| Project | Version | Docs | Description | +| ------------- | -------------------------------------------- | ------------------------ | ------------------------------------------------------ | +| [icejs] | [![icejs-status]][icejs-package] | [docs][icejs-docs] | A universal framework based on react | +| [rax-scripts] | [![rax-scripts-status]][rax-scripts-package] | [docs][rax-scripts-docs] | Rax official engineering tools use @alib/build-scripts | [icejs]: https://github.com/alibaba/ice [rax-scripts]: https://github.com/raxjs/rax-scripts - [icejs-status]: https://img.shields.io/npm/v/ice.js.svg [rax-scripts-status]: https://img.shields.io/npm/v/build-plugin-rax-app.svg - [icejs-package]: https://npmjs.com/package/ice.js [rax-scripts-package]: https://npmjs.com/package/build-plugin-rax-app - [icejs-docs]: https://ice.work/docs/guide/intro [rax-scripts-docs]: https://rax.js.org/ ## License -[MIT](LICENSE) \ No newline at end of file +[MIT](LICENSE) diff --git a/packages/build-scripts/bin/build-scripts.js b/packages/build-scripts/bin/build-scripts.js index 4a4a68b..9722f56 100755 --- a/packages/build-scripts/bin/build-scripts.js +++ b/packages/build-scripts/bin/build-scripts.js @@ -11,9 +11,7 @@ const test = require('./test'); // finish check before run command checkNodeVersion(packageInfo.engines.node); - program - .version(packageInfo.version) - .usage(' [options]'); + program.version(packageInfo.version).usage(' [options]'); program .command('build') diff --git a/packages/build-scripts/bin/child-process-start.js b/packages/build-scripts/bin/child-process-start.js index 37efcbf..5e36bd5 100644 --- a/packages/build-scripts/bin/child-process-start.js +++ b/packages/build-scripts/bin/child-process-start.js @@ -39,7 +39,7 @@ const defaultPort = parseInt(DEFAULT_PORT || 3333, 10); process.env.NODE_ENV = 'development'; rawArgv.port = parseInt(newPort, 10); - + // ignore _ in rawArgv delete rawArgv._; try { diff --git a/packages/build-scripts/bin/start.js b/packages/build-scripts/bin/start.js index 9c5c81a..35669d9 100755 --- a/packages/build-scripts/bin/start.js +++ b/packages/build-scripts/bin/start.js @@ -28,8 +28,8 @@ async function modifyInspectArgv(execArgv, processArgv) { const [_, command, ip, port = 9229] = matchResult; const nPort = +port; const newPort = await detect(nPort); - return `--${command}=${ip ? `${ip}:` : ''}${newPort}` - }) + return `--${command}=${ip ? `${ip}:` : ''}${newPort}`; + }), ); /** @@ -51,8 +51,10 @@ function restartProcess() { // remove the inspect related argv when passing to child process to avoid port-in-use error const execArgv = await modifyInspectArgv(process.execArgv, rawArgv); - // filter inspect in process argv, it has been comsumed - const nProcessArgv = process.argv.slice(2).filter((arg) => arg.indexOf('--inspect') === -1); + // filter inspect in process argv, it has been comsumed + const nProcessArgv = process.argv + .slice(2) + .filter(arg => arg.indexOf('--inspect') === -1); child = fork(scriptPath, nProcessArgv, { execArgv }); child.on('message', data => { if (process.send) { diff --git a/packages/build-scripts/jest.config.js b/packages/build-scripts/jest.config.js index 91a66bd..b873966 100644 --- a/packages/build-scripts/jest.config.js +++ b/packages/build-scripts/jest.config.js @@ -4,4 +4,4 @@ module.exports = { transform: { '^.+\\.tsx?$': 'ts-jest', }, -}; \ No newline at end of file +}; diff --git a/packages/build-scripts/src/commands/build.ts b/packages/build-scripts/src/commands/build.ts index b1c5c62..96ccf74 100644 --- a/packages/build-scripts/src/commands/build.ts +++ b/packages/build-scripts/src/commands/build.ts @@ -1,16 +1,7 @@ -import chalk from 'chalk'; -import Context, { - CommandArgs, - IPluginList, - IGetBuiltInPlugins, - ITaskConfig, -} from '../core/Context'; -import webpackStats from '../utils/webpackStats'; +import WebpackService from '../service/WebpackService'; +import { IContextOptions, ITaskConfig } from '../core/Context'; -import webpack = require('webpack'); -import fs = require('fs-extra'); -import path = require('path'); -import log = require('../utils/log'); +type BuildResult = void | ITaskConfig[]; export = async function({ args, @@ -18,99 +9,15 @@ export = async function({ eject, plugins, getBuiltInPlugins, -}: { - args: CommandArgs; - rootDir: string; - eject?: boolean; - plugins?: IPluginList; - getBuiltInPlugins?: IGetBuiltInPlugins; -}): Promise { +}: IContextOptions & { eject?: boolean }): Promise { const command = 'build'; - const context = new Context({ + const service = new WebpackService({ args, command, rootDir, plugins, getBuiltInPlugins, }); - - log.verbose( - 'OPTIONS', - `${command} cliOptions: ${JSON.stringify(args, null, 2)}`, - ); - - const { applyHook, rootDir: ctxRoot, webpack: webpackInstance } = context; - let configArr = []; - try { - configArr = await context.setUp(); - } catch (err) { - log.error('CONFIG', chalk.red('Failed to get config.')); - await applyHook(`error`, { err }); - throw err; - } - - await applyHook(`before.${command}.load`, { args, 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; - } - - // clear build directory - const defaultPath = path.resolve(ctxRoot, 'build'); - configArr.forEach(v => { - try { - const userBuildPath = v.chainConfig.output.get('path'); - const buildPath = path.resolve(ctxRoot, userBuildPath); - fs.emptyDirSync(buildPath); - } catch (e) { - if (fs.existsSync(defaultPath)) { - fs.emptyDirSync(defaultPath); - } - } - }); - - const webpackConfig = configArr.map(v => v.chainConfig.toConfig()); - await applyHook(`before.${command}.run`, { args, config: webpackConfig }); - - let compiler: webpack.MultiCompiler; - try { - compiler = webpackInstance(webpackConfig); - } catch (err) { - log.error('CONFIG', chalk.red('Failed to load webpack config.')); - await applyHook(`error`, { err }); - throw err; - } - - const result = await new Promise((resolve, reject): void => { - // typeof(stats) is webpack.compilation.MultiStats - compiler.run((err, stats) => { - if (err) { - log.error('WEBPACK', err.stack || err.toString()); - reject(err); - return; - } - - const isSuccessful = webpackStats({ - stats, - }); - if (isSuccessful) { - resolve({ - stats, - }); - } else { - reject(new Error('webpack compile error')); - } - }); - }); - - await applyHook(`after.${command}.compile`, result); + return (await service.run({ eject })) as BuildResult; }; diff --git a/packages/build-scripts/src/commands/start.ts b/packages/build-scripts/src/commands/start.ts index 1fde48a..81b5f9e 100644 --- a/packages/build-scripts/src/commands/start.ts +++ b/packages/build-scripts/src/commands/start.ts @@ -1,19 +1,8 @@ -import chalk from 'chalk'; -import { WebpackOptionsNormalized } from 'webpack'; -import Context, { - CommandArgs, - IGetBuiltInPlugins, - IPluginList, - ITaskConfig, -} from '../core/Context'; -import webpackStats from '../utils/webpackStats'; - -import deepmerge = require('deepmerge'); +import WebpackService from '../service/WebpackService'; +import { IContextOptions, ITaskConfig } from '../core/Context'; import WebpackDevServer = require('webpack-dev-server'); -import prepareURLs = require('../utils/prepareURLs'); -import log = require('../utils/log'); -type DevServer = Record; +type StartResult = void | ITaskConfig[] | WebpackDevServer; export = async function({ args, @@ -21,141 +10,15 @@ export = async function({ eject, plugins, getBuiltInPlugins, -}: { - args: CommandArgs; - rootDir: string; - eject?: boolean; - plugins?: IPluginList; - getBuiltInPlugins?: IGetBuiltInPlugins; -}): Promise { +}: IContextOptions & { eject?: boolean }): Promise { const command = 'start'; - const context = new Context({ + const service = new WebpackService({ args, command, rootDir, plugins, getBuiltInPlugins, }); - - log.verbose( - 'OPTIONS', - `${command} cliOptions: ${JSON.stringify(args, null, 2)}`, - ); - let serverUrl = ''; - - const { applyHook, webpack } = context; - - let configArr = []; - try { - configArr = await context.setUp(); - } catch (err) { - log.error('CONFIG', chalk.red('Failed to get config.')); - await applyHook(`error`, { err }); - throw err; - } - - await applyHook(`before.${command}.load`, { args, 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 devServerConfig: DevServer = { - port: args.port || 3333, - host: args.host || '0.0.0.0', - https: args.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 = args.port; - } - } - - const webpackConfig = configArr.map(v => v.chainConfig.toConfig()); - await applyHook(`before.${command}.run`, { args, 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, - }); - }); - // 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'); - const devServer = new DevServer(compiler, devServerConfig); - - await applyHook(`before.${command}.devServer`, { - url: serverUrl, - urls, - devServer, - }); - - 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; + return (await service.run({ eject })) as StartResult; }; diff --git a/packages/build-scripts/src/commands/test.ts b/packages/build-scripts/src/commands/test.ts index fc3f849..90c92ac 100644 --- a/packages/build-scripts/src/commands/test.ts +++ b/packages/build-scripts/src/commands/test.ts @@ -1,112 +1,21 @@ -import { runCLI } from 'jest'; -import chalk from 'chalk'; -import Context, { IContextOptions } from '../core/Context'; - -import fs = require('fs-extra'); -import path = require('path'); -import log = require('../utils/log'); +import JestService from '../service/JestService'; +import { IContextOptions, IJestResult } from '../core/Context'; export = async function({ args, rootDir, plugins, getBuiltInPlugins, -}: IContextOptions): Promise { +}: IContextOptions): Promise { const command = 'test'; - const context = new Context({ + const service = new JestService({ args, command, rootDir, plugins, getBuiltInPlugins, }); - const { jestArgv = {} } = args || {}; - const { config, regexForTestFiles, ...restArgv } = jestArgv; - - const { applyHook, rootDir: ctxRoot } = context; - await applyHook(`before.${command}.load`, { args }); - - let configArr = []; - try { - configArr = await context.setUp(); - } catch (err) { - log.error('CONFIG', chalk.red('Failed to get config.')); - await applyHook(`error`, { err }); - throw err; - } - - // get user jest config - const jestConfigPath = path.join(ctxRoot, config || 'jest.config.js'); - - let userJestConfig = { moduleNameMapper: {} }; - if (fs.existsSync(jestConfigPath)) { - userJestConfig = require(jestConfigPath); // eslint-disable-line - } - - // get webpack.resolve.alias - const alias: { [key: string]: string } = configArr.reduce( - (acc, { chainConfig }) => { - const webpackConfig = chainConfig.toConfig(); - if (webpackConfig.resolve && webpackConfig.resolve.alias) { - return { - ...acc, - ...webpackConfig.resolve.alias, - }; - } else { - return acc; - } - }, - {}, - ); - - const aliasModuleNameMapper: { [key: string]: string } = {}; - Object.keys(alias || {}).forEach(key => { - const aliasPath = alias[key]; - // check path if it is a directory - if (fs.existsSync(aliasPath) && fs.statSync(aliasPath).isDirectory()) { - aliasModuleNameMapper[`^${key}/(.*)$`] = `${aliasPath}/$1`; - } - aliasModuleNameMapper[`^${key}$`] = aliasPath; - }); - - // generate default jest config - const jestConfig = context.runJestConfig({ - rootDir: ctxRoot, - ...userJestConfig, - moduleNameMapper: { - ...userJestConfig.moduleNameMapper, - ...aliasModuleNameMapper, - }, - ...(regexForTestFiles ? { testMatch: regexForTestFiles } : {}), - }); - - // disallow users to modify jest config - Object.freeze(jestConfig); - await applyHook(`before.${command}.run`, { args, config: jestConfig }); - - const result = await new Promise((resolve, reject): void => { - runCLI( - { - ...restArgv, - config: JSON.stringify(jestConfig), - }, - [ctxRoot], - ) - .then(data => { - const { results } = data; - if (results.success) { - resolve(data); - } else { - reject(new Error('Jest failed')); - } - }) - .catch((err: Error) => { - log.error('JEST', err.stack || err.toString()); - }); - }); - - await applyHook(`after.${command}`, { result }); - return result; + return await service.run(); }; diff --git a/packages/build-scripts/src/core/Context.ts b/packages/build-scripts/src/core/Context.ts index b663533..d68141f 100644 --- a/packages/build-scripts/src/core/Context.ts +++ b/packages/build-scripts/src/core/Context.ts @@ -1,6 +1,5 @@ import * as path from 'path'; -import * as fs from 'fs'; -import _ from 'lodash'; +import * as fs from 'fs-extra'; import camelCase from 'camelCase'; import webpack, { MultiStats } from 'webpack'; import { Logger } from 'npmlog'; @@ -17,6 +16,7 @@ import { import hijackWebpackResolve from '../utils/hijackWebpack'; import assert = require('assert'); +import _ = require('lodash'); import WebpackChain = require('webpack-chain'); import WebpackDevServer = require('webpack-dev-server'); import log = require('../utils/log'); @@ -82,7 +82,7 @@ export interface IOnHook { } export interface IPluginConfigWebpack { - (config: WebpackChain): void; + (config: WebpackChain): Promise; } export interface IUserConfigWebpack { @@ -139,7 +139,7 @@ export interface IMethodOptions { pluginName?: boolean; } -export interface IRegsiterMethod { +export interface IRegisterMethod { (name: string, fn: IMethodFunction, options?: IMethodOptions): void; } @@ -158,7 +158,7 @@ export interface IHasMethod { } export interface IModifyConfig { - (userConfig: IUserConfig): IHash; + (userConfig: IUserConfig): Omit; } export interface IModifyUserConfig { @@ -178,11 +178,11 @@ export interface IPluginAPI { onGetWebpackConfig: IOnGetWebpackConfig; onGetJestConfig: IOnGetJestConfig; onHook: IOnHook; - setValue: (name: string, value: any) => void; - getValue: (name: string) => any; + setValue: (name: string, value: T) => void; + getValue: (name: string) => T; registerUserConfig: (args: MaybeArray) => void; registerCliOption: (args: MaybeArray) => void; - registerMethod: IRegsiterMethod; + registerMethod: IRegisterMethod; applyMethod: IApplyMethodAPI; modifyUserConfig: IModifyUserConfig; } @@ -286,6 +286,12 @@ class Context { public webpack: IWebpack; + public pkg: Json; + + public userConfig: IUserConfig; + + public plugins: IPluginInfo[]; + // 通过registerTask注册,存放初始的webpack-chain配置 private configArr: ITaskConfig[]; @@ -311,12 +317,6 @@ class Context { private cancelTaskNames: string[]; - public pkg: Json; - - public userConfig: IUserConfig; - - public plugins: IPluginInfo[]; - constructor({ command, rootDir = process.cwd(), @@ -587,7 +587,7 @@ class Context { } }; - public registerMethod: IRegsiterMethod = (name, fn, options) => { + public registerMethod: IRegisterMethod = (name, fn, options) => { if (this.methodRegistration[name]) { throw new Error(`[Error] method '${name}' already registered`); } else { @@ -630,8 +630,8 @@ class Context { log.warn('[waring]', errorMsg); } delete modifiedValue.plugins; - Object.keys(modifiedValue).forEach(configKey => { - this.userConfig[configKey] = modifiedValue[configKey]; + Object.keys(modifiedValue).forEach(modifiedConfigKey => { + this.userConfig[modifiedConfigKey] = modifiedValue[modifiedConfigKey]; }); } else { throw new Error(`modifyUserConfig must return a plain object`); @@ -914,6 +914,10 @@ class Context { ); return this.configArr; }; + + public getWebpackConfig = (): ITaskConfig[] => { + return this.configArr; + }; } export default Context; diff --git a/packages/build-scripts/src/service/JestService.ts b/packages/build-scripts/src/service/JestService.ts new file mode 100644 index 0000000..8c2ae43 --- /dev/null +++ b/packages/build-scripts/src/service/JestService.ts @@ -0,0 +1,18 @@ +import Context, { IContextOptions, IJestResult } from '../core/Context'; +import test = require('./test'); + +type ServiceOptions = Omit; + +class JestService { + private context: Context; + + constructor(props: ServiceOptions) { + this.context = new Context(props); + } + + public async run(): Promise { + return await test({ context: this.context }); + } +} + +export default JestService; diff --git a/packages/build-scripts/src/service/WebpackService.ts b/packages/build-scripts/src/service/WebpackService.ts new file mode 100644 index 0000000..3601079 --- /dev/null +++ b/packages/build-scripts/src/service/WebpackService.ts @@ -0,0 +1,62 @@ +import chalk from 'chalk'; +import Context, { IContextOptions, ITaskConfig } from '../core/Context'; +import WebpackDevServer = require('webpack-dev-server'); +import log = require('../utils/log'); +import start = require('./start'); +import build = require('./build'); + +type ServiceOptions = Omit; +interface IRunOptions { + eject?: boolean; +} + +class WebpackService { + private context: Context; + + constructor(props: ServiceOptions) { + this.context = new Context(props); + } + + public async run( + options: IRunOptions, + ): Promise { + const { eject } = options; + const { applyHook, command, commandArgs } = this.context; + log.verbose( + 'OPTIONS', + `${command} cliOptions: ${JSON.stringify(commandArgs, null, 2)}`, + ); + let configArr: ITaskConfig[] = []; + try { + configArr = await this.context.setUp(); + } catch (err) { + log.error('CONFIG', chalk.red('Failed to get config.')); + await applyHook(`error`, { err }); + throw err; + } + 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; + } + const commandOptions = { context: this.context }; + if (command === 'start') { + return start(commandOptions); + } else if (command === 'build') { + return build(commandOptions); + } + } +} + +export default WebpackService; diff --git a/packages/build-scripts/src/service/build.ts b/packages/build-scripts/src/service/build.ts new file mode 100644 index 0000000..2b9dab9 --- /dev/null +++ b/packages/build-scripts/src/service/build.ts @@ -0,0 +1,75 @@ +import chalk from 'chalk'; +import Context, { ITaskConfig } from '../core/Context'; +import webpackStats from '../utils/webpackStats'; + +import webpack = require('webpack'); +import fs = require('fs-extra'); +import path = require('path'); +import log = require('../utils/log'); + +export = async function({ + context, +}: { + context: Context; +}): Promise { + const { + rootDir, + applyHook, + command, + commandArgs, + webpack: webpackInstance, + } = context; + const configArr = context.getWebpackConfig(); + // clear build directory + const defaultPath = path.resolve(rootDir, 'build'); + configArr.forEach(v => { + try { + const userBuildPath = v.chainConfig.output.get('path'); + const buildPath = path.resolve(rootDir, userBuildPath); + fs.emptyDirSync(buildPath); + } catch (e) { + if (fs.existsSync(defaultPath)) { + fs.emptyDirSync(defaultPath); + } + } + }); + + const webpackConfig = configArr.map(v => v.chainConfig.toConfig()); + await applyHook(`before.${command}.run`, { + args: commandArgs, + config: webpackConfig, + }); + + let compiler: webpack.MultiCompiler; + try { + compiler = webpackInstance(webpackConfig); + } catch (err) { + log.error('CONFIG', chalk.red('Failed to load webpack config.')); + await applyHook(`error`, { err }); + throw err; + } + + const result = await new Promise((resolve, reject): void => { + // typeof(stats) is webpack.compilation.MultiStats + compiler.run((err, stats) => { + if (err) { + log.error('WEBPACK', err.stack || err.toString()); + reject(err); + return; + } + + const isSuccessful = webpackStats({ + stats, + }); + if (isSuccessful) { + resolve({ + stats, + }); + } else { + reject(new Error('webpack compile error')); + } + }); + }); + + await applyHook(`after.${command}.compile`, result); +}; diff --git a/packages/build-scripts/src/service/start.ts b/packages/build-scripts/src/service/start.ts new file mode 100644 index 0000000..43de00d --- /dev/null +++ b/packages/build-scripts/src/service/start.ts @@ -0,0 +1,112 @@ +import chalk from 'chalk'; +import { WebpackOptionsNormalized } from 'webpack'; +import Context, { ITaskConfig } from '../core/Context'; +import webpackStats from '../utils/webpackStats'; +import deepmerge = require('deepmerge'); +import WebpackDevServer = require('webpack-dev-server'); +import prepareURLs = require('../utils/prepareURLs'); +import log = require('../utils/log'); + +type DevServer = Record; + +export = async function({ + context, +}: { + context: Context; +}): Promise { + const { command, commandArgs, webpack, applyHook } = context; + const configArr = context.getWebpackConfig(); + let serverUrl = ''; + let devServerConfig: DevServer = { + 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, + }); + }); + // 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'); + const devServer = new DevServer(compiler, devServerConfig); + + await applyHook(`before.${command}.devServer`, { + url: serverUrl, + urls, + devServer, + }); + + 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; +}; diff --git a/packages/build-scripts/src/service/test.ts b/packages/build-scripts/src/service/test.ts new file mode 100644 index 0000000..809b726 --- /dev/null +++ b/packages/build-scripts/src/service/test.ts @@ -0,0 +1,106 @@ +import { runCLI } from 'jest'; +import chalk from 'chalk'; +import Context, { IJestResult } from '../core/Context'; + +import fs = require('fs-extra'); +import path = require('path'); +import log = require('../utils/log'); + +export = async function({ + context, +}: { + context: Context; +}): Promise { + const { command, commandArgs } = context; + const { jestArgv = {} } = commandArgs || {}; + const { config, regexForTestFiles, ...restArgv } = jestArgv; + + const { applyHook, rootDir: ctxRoot } = context; + await applyHook(`before.${command}.load`, { args: commandArgs }); + + let configArr = []; + try { + configArr = await context.setUp(); + } catch (err) { + log.error('CONFIG', chalk.red('Failed to get config.')); + await applyHook(`error`, { err }); + throw err; + } + + // get user jest config + const jestConfigPath = path.join(ctxRoot, config || 'jest.config.js'); + + let userJestConfig = { moduleNameMapper: {} }; + if (fs.existsSync(jestConfigPath)) { + userJestConfig = require(jestConfigPath); // eslint-disable-line + } + + // get webpack.resolve.alias + const alias: { [key: string]: string } = configArr.reduce( + (acc, { chainConfig }) => { + const webpackConfig = chainConfig.toConfig(); + if (webpackConfig.resolve && webpackConfig.resolve.alias) { + return { + ...acc, + ...webpackConfig.resolve.alias, + }; + } else { + return acc; + } + }, + {}, + ); + + const aliasModuleNameMapper: { [key: string]: string } = {}; + Object.keys(alias || {}).forEach(key => { + const aliasPath = alias[key]; + // check path if it is a directory + if (fs.existsSync(aliasPath) && fs.statSync(aliasPath).isDirectory()) { + aliasModuleNameMapper[`^${key}/(.*)$`] = `${aliasPath}/$1`; + } + aliasModuleNameMapper[`^${key}$`] = aliasPath; + }); + + // generate default jest config + const jestConfig = context.runJestConfig({ + rootDir: ctxRoot, + ...userJestConfig, + moduleNameMapper: { + ...userJestConfig.moduleNameMapper, + ...aliasModuleNameMapper, + }, + ...(regexForTestFiles ? { testMatch: regexForTestFiles } : {}), + }); + + // disallow users to modify jest config + Object.freeze(jestConfig); + await applyHook(`before.${command}.run`, { + args: commandArgs, + config: jestConfig, + }); + + const result = await new Promise((resolve, reject): void => { + runCLI( + { + ...restArgv, + config: JSON.stringify(jestConfig), + }, + [ctxRoot], + ) + .then(data => { + const { results } = data; + if (results.success) { + resolve(data); + } else { + reject(new Error('Jest failed')); + } + }) + .catch((err: Error) => { + log.error('JEST', err.stack || err.toString()); + }); + }); + + await applyHook(`after.${command}`, { result }); + + return result as IJestResult; +}; From 8a03ea6d2fc6177b7f0ae4e610c12004aa609817 Mon Sep 17 00:00:00 2001 From: ClarkXia Date: Thu, 15 Jul 2021 19:43:55 +0800 Subject: [PATCH 05/21] feat: support class extends --- packages/build-scripts/package.json | 1 + packages/build-scripts/src/commands/build.ts | 3 +- packages/build-scripts/src/commands/start.ts | 3 +- packages/build-scripts/src/core/Context.ts | 48 ++++++++++++++ .../build-scripts/src/service/JestService.ts | 19 ++---- .../src/service/WebpackService.ts | 63 +++---------------- packages/build-scripts/src/service/build.ts | 28 +++++---- packages/build-scripts/src/service/start.ts | 23 +++++-- packages/build-scripts/src/service/test.ts | 16 +---- packages/build-scripts/src/types/index.ts | 4 ++ 10 files changed, 105 insertions(+), 103 deletions(-) diff --git a/packages/build-scripts/package.json b/packages/build-scripts/package.json index 3be0e25..da768b7 100644 --- a/packages/build-scripts/package.json +++ b/packages/build-scripts/package.json @@ -46,6 +46,7 @@ "@types/semver": "^6.2.0", "@types/webpack": "^4.41.30", "@types/webpack-chain": "^5.2.0", + "@types/webpack-dev-server": "^3.11.5", "jest": "^27.0.6", "package-json": "^6.5.0", "ts-jest": "^27.0.3", diff --git a/packages/build-scripts/src/commands/build.ts b/packages/build-scripts/src/commands/build.ts index 96ccf74..4e20a8d 100644 --- a/packages/build-scripts/src/commands/build.ts +++ b/packages/build-scripts/src/commands/build.ts @@ -1,5 +1,6 @@ import WebpackService from '../service/WebpackService'; import { IContextOptions, ITaskConfig } from '../core/Context'; +import { IRunOptions } from '../types'; type BuildResult = void | ITaskConfig[]; @@ -9,7 +10,7 @@ export = async function({ eject, plugins, getBuiltInPlugins, -}: IContextOptions & { eject?: boolean }): Promise { +}: IContextOptions & IRunOptions): Promise { const command = 'build'; const service = new WebpackService({ diff --git a/packages/build-scripts/src/commands/start.ts b/packages/build-scripts/src/commands/start.ts index 81b5f9e..c6da400 100644 --- a/packages/build-scripts/src/commands/start.ts +++ b/packages/build-scripts/src/commands/start.ts @@ -1,6 +1,7 @@ import WebpackService from '../service/WebpackService'; import { IContextOptions, ITaskConfig } from '../core/Context'; import WebpackDevServer = require('webpack-dev-server'); +import { IRunOptions } from '../types'; type StartResult = void | ITaskConfig[] | WebpackDevServer; @@ -10,7 +11,7 @@ export = async function({ eject, plugins, getBuiltInPlugins, -}: IContextOptions & { eject?: boolean }): Promise { +}: IContextOptions & IRunOptions): Promise { const command = 'start'; const service = new WebpackService({ diff --git a/packages/build-scripts/src/core/Context.ts b/packages/build-scripts/src/core/Context.ts index d68141f..a496e25 100644 --- a/packages/build-scripts/src/core/Context.ts +++ b/packages/build-scripts/src/core/Context.ts @@ -1,5 +1,6 @@ import * as path from 'path'; import * as fs from 'fs-extra'; +import chalk from 'chalk'; import camelCase from 'camelCase'; import webpack, { MultiStats } from 'webpack'; import { Logger } from 'npmlog'; @@ -208,12 +209,23 @@ export type IPluginList = (string | [string, Json])[]; export type IGetBuiltInPlugins = (userConfig: IUserConfig) => IPluginList; +export type CommandModule = (context: Context,options: T) => Promise; + +export interface ICommandModules { + [command: string]: CommandModule; +} + +export type GetCommandModule = (options: { command: CommandName; userConfig: IUserConfig }) => CommandModule; + +export type RegisterCommandModules = (key: string, module: CommandModule) => void; + export interface IContextOptions { command: CommandName; rootDir: string; args: CommandArgs; plugins?: IPluginList; getBuiltInPlugins?: IGetBuiltInPlugins; + commandModules?: ICommandModules; } export interface ITaskConfig { @@ -317,6 +329,8 @@ class Context { private cancelTaskNames: string[]; + private commandModules: ICommandModules; + constructor({ command, rootDir = process.cwd(), @@ -324,6 +338,7 @@ class Context { plugins = [], getBuiltInPlugins = () => [], }: IContextOptions) { + this.commandModules = {}; this.command = command; this.commandArgs = args; this.rootDir = rootDir; @@ -902,6 +917,22 @@ class Context { } }; + public registerCommandModules: RegisterCommandModules = (moduleKey, module) => { + if (this.commandModules[moduleKey]) { + log.warn('CONFIG', `command module ${moduleKey} already been registered`); + } + this.commandModules[moduleKey] = module; + } + + private getCommandModule: GetCommandModule = (options) => { + const { command } = options; + if (this.commandModules[command]) { + return this.commandModules[command]; + } else { + throw new Error(`command ${command} is not support`); + } + }; + public setUp = async (): Promise => { await this.runPlugins(); await this.runConfigModification(); @@ -918,6 +949,23 @@ class Context { public getWebpackConfig = (): ITaskConfig[] => { return this.configArr; }; + + public run = async (options?: T): Promise

=> { + const { command, commandArgs } = this; + const commandModule = this.getCommandModule({ command: this.command, userConfig: this.userConfig }); + 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; + } + return commandModule(this, options); + } } export default Context; diff --git a/packages/build-scripts/src/service/JestService.ts b/packages/build-scripts/src/service/JestService.ts index 8c2ae43..54dd08c 100644 --- a/packages/build-scripts/src/service/JestService.ts +++ b/packages/build-scripts/src/service/JestService.ts @@ -1,18 +1,11 @@ -import Context, { IContextOptions, IJestResult } from '../core/Context'; +import Context, { IContextOptions } from '../core/Context'; import test = require('./test'); -type ServiceOptions = Omit; - -class JestService { - private context: Context; - - constructor(props: ServiceOptions) { - this.context = new Context(props); - } - - public async run(): Promise { - return await test({ context: this.context }); +class JestContext extends Context { + constructor(props: IContextOptions) { + super(props); + super.registerCommandModules('test', test); } } -export default JestService; +export default JestContext; diff --git a/packages/build-scripts/src/service/WebpackService.ts b/packages/build-scripts/src/service/WebpackService.ts index 3601079..e6b5e62 100644 --- a/packages/build-scripts/src/service/WebpackService.ts +++ b/packages/build-scripts/src/service/WebpackService.ts @@ -1,62 +1,13 @@ -import chalk from 'chalk'; -import Context, { IContextOptions, ITaskConfig } from '../core/Context'; -import WebpackDevServer = require('webpack-dev-server'); -import log = require('../utils/log'); +import Context, { IContextOptions } from '../core/Context'; import start = require('./start'); import build = require('./build'); -type ServiceOptions = Omit; -interface IRunOptions { - eject?: boolean; -} - -class WebpackService { - private context: Context; - - constructor(props: ServiceOptions) { - this.context = new Context(props); - } - - public async run( - options: IRunOptions, - ): Promise { - const { eject } = options; - const { applyHook, command, commandArgs } = this.context; - log.verbose( - 'OPTIONS', - `${command} cliOptions: ${JSON.stringify(commandArgs, null, 2)}`, - ); - let configArr: ITaskConfig[] = []; - try { - configArr = await this.context.setUp(); - } catch (err) { - log.error('CONFIG', chalk.red('Failed to get config.')); - await applyHook(`error`, { err }); - throw err; - } - 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; - } - const commandOptions = { context: this.context }; - if (command === 'start') { - return start(commandOptions); - } else if (command === 'build') { - return build(commandOptions); - } +class WebpackContext extends Context { + constructor(props: IContextOptions) { + super(props); + super.registerCommandModules('start', start); + super.registerCommandModules('build', build); } } -export default WebpackService; +export default WebpackContext; diff --git a/packages/build-scripts/src/service/build.ts b/packages/build-scripts/src/service/build.ts index 2b9dab9..c3aa04a 100644 --- a/packages/build-scripts/src/service/build.ts +++ b/packages/build-scripts/src/service/build.ts @@ -1,25 +1,29 @@ import chalk from 'chalk'; import Context, { ITaskConfig } from '../core/Context'; import webpackStats from '../utils/webpackStats'; +import { IRunOptions } from '../types'; import webpack = require('webpack'); import fs = require('fs-extra'); import path = require('path'); import log = require('../utils/log'); -export = async function({ - context, -}: { - context: Context; -}): Promise { - const { - rootDir, - applyHook, - command, - commandArgs, - webpack: webpackInstance, - } = context; +export = async function(context: Context, options: IRunOptions): Promise { + const { eject } = options; const configArr = context.getWebpackConfig(); + const { command, commandArgs, applyHook, rootDir, webpack: webpackInstance } = 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; + } // clear build directory const defaultPath = path.resolve(rootDir, 'build'); configArr.forEach(v => { diff --git a/packages/build-scripts/src/service/start.ts b/packages/build-scripts/src/service/start.ts index 43de00d..0a36153 100644 --- a/packages/build-scripts/src/service/start.ts +++ b/packages/build-scripts/src/service/start.ts @@ -2,6 +2,7 @@ import chalk from 'chalk'; import { WebpackOptionsNormalized } from 'webpack'; import Context, { ITaskConfig } from '../core/Context'; import webpackStats from '../utils/webpackStats'; +import { IRunOptions } from '../types'; import deepmerge = require('deepmerge'); import WebpackDevServer = require('webpack-dev-server'); import prepareURLs = require('../utils/prepareURLs'); @@ -9,13 +10,23 @@ import log = require('../utils/log'); type DevServer = Record; -export = async function({ - context, -}: { - context: Context; -}): Promise { - const { command, commandArgs, webpack, applyHook } = context; +export = async function(context: Context, options: IRunOptions): Promise { + 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: DevServer = { port: commandArgs.port || 3333, diff --git a/packages/build-scripts/src/service/test.ts b/packages/build-scripts/src/service/test.ts index 809b726..adc81e7 100644 --- a/packages/build-scripts/src/service/test.ts +++ b/packages/build-scripts/src/service/test.ts @@ -1,16 +1,11 @@ import { runCLI } from 'jest'; -import chalk from 'chalk'; import Context, { IJestResult } from '../core/Context'; import fs = require('fs-extra'); import path = require('path'); import log = require('../utils/log'); -export = async function({ - context, -}: { - context: Context; -}): Promise { +export = async function(context?: Context): Promise { const { command, commandArgs } = context; const { jestArgv = {} } = commandArgs || {}; const { config, regexForTestFiles, ...restArgv } = jestArgv; @@ -18,14 +13,7 @@ export = async function({ const { applyHook, rootDir: ctxRoot } = context; await applyHook(`before.${command}.load`, { args: commandArgs }); - let configArr = []; - try { - configArr = await context.setUp(); - } catch (err) { - log.error('CONFIG', chalk.red('Failed to get config.')); - await applyHook(`error`, { err }); - throw err; - } + const configArr = context.getWebpackConfig(); // get user jest config const jestConfigPath = path.join(ctxRoot, config || 'jest.config.js'); diff --git a/packages/build-scripts/src/types/index.ts b/packages/build-scripts/src/types/index.ts index 47b1596..ed933b6 100644 --- a/packages/build-scripts/src/types/index.ts +++ b/packages/build-scripts/src/types/index.ts @@ -11,3 +11,7 @@ export type JsonValue = Json[keyof Json]; export type MaybeArray = T | T[]; export type MaybePromise = T | Promise; + +export interface IRunOptions { + eject?: boolean; +} From 59861b4bacbc38d9dfd3a96d2f984704f08a141c Mon Sep 17 00:00:00 2001 From: ClarkXia Date: Thu, 15 Jul 2021 19:45:41 +0800 Subject: [PATCH 06/21] fix: named export --- packages/build-scripts/src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/build-scripts/src/index.ts b/packages/build-scripts/src/index.ts index bef7725..c05734e 100644 --- a/packages/build-scripts/src/index.ts +++ b/packages/build-scripts/src/index.ts @@ -1,7 +1,9 @@ import build = require('./commands/build'); import start = require('./commands/start'); import test = require('./commands/test'); +import WebpackContext from './service/WebpackService'; +import JestContext from './service/JestService'; export * from './core/Context'; export * from './types'; -export { build, start, test }; +export { build, start, test, WebpackContext, JestContext }; From 0780847b0ecc42ca8fdabc564e28d8e2bd113bbd Mon Sep 17 00:00:00 2001 From: ClarkXia Date: Thu, 15 Jul 2021 20:57:30 +0800 Subject: [PATCH 07/21] fix: extends class --- packages/build-scripts/src/commands/start.ts | 3 +-- packages/build-scripts/src/core/Context.ts | 9 ++++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/build-scripts/src/commands/start.ts b/packages/build-scripts/src/commands/start.ts index c6da400..81b5f9e 100644 --- a/packages/build-scripts/src/commands/start.ts +++ b/packages/build-scripts/src/commands/start.ts @@ -1,7 +1,6 @@ import WebpackService from '../service/WebpackService'; import { IContextOptions, ITaskConfig } from '../core/Context'; import WebpackDevServer = require('webpack-dev-server'); -import { IRunOptions } from '../types'; type StartResult = void | ITaskConfig[] | WebpackDevServer; @@ -11,7 +10,7 @@ export = async function({ eject, plugins, getBuiltInPlugins, -}: IContextOptions & IRunOptions): Promise { +}: IContextOptions & { eject?: boolean }): Promise { const command = 'start'; const service = new WebpackService({ diff --git a/packages/build-scripts/src/core/Context.ts b/packages/build-scripts/src/core/Context.ts index a496e25..49175fa 100644 --- a/packages/build-scripts/src/core/Context.ts +++ b/packages/build-scripts/src/core/Context.ts @@ -209,7 +209,7 @@ export type IPluginList = (string | [string, Json])[]; export type IGetBuiltInPlugins = (userConfig: IUserConfig) => IPluginList; -export type CommandModule = (context: Context,options: T) => Promise; +export type CommandModule = (context: Context, options: any) => Promise; export interface ICommandModules { [command: string]: CommandModule; @@ -329,7 +329,7 @@ class Context { private cancelTaskNames: string[]; - private commandModules: ICommandModules; + public commandModules: ICommandModules = {}; constructor({ command, @@ -338,7 +338,6 @@ class Context { plugins = [], getBuiltInPlugins = () => [], }: IContextOptions) { - this.commandModules = {}; this.command = command; this.commandArgs = args; this.rootDir = rootDir; @@ -917,14 +916,14 @@ class Context { } }; - public registerCommandModules: RegisterCommandModules = (moduleKey, module) => { + public registerCommandModules (moduleKey: string, module: CommandModule): void { if (this.commandModules[moduleKey]) { log.warn('CONFIG', `command module ${moduleKey} already been registered`); } this.commandModules[moduleKey] = module; } - private getCommandModule: GetCommandModule = (options) => { + public getCommandModule: GetCommandModule = (options) => { const { command } = options; if (this.commandModules[command]) { return this.commandModules[command]; From 3c3bfec6936da196404fa5e799d7aa3e52f259bf Mon Sep 17 00:00:00 2001 From: ClarkXia Date: Fri, 16 Jul 2021 11:34:28 +0800 Subject: [PATCH 08/21] fix: export api --- packages/build-scripts/src/core/Context.ts | 8 +++----- packages/build-scripts/src/index.ts | 6 +++--- packages/build-scripts/src/service/JestService.ts | 4 ++-- packages/build-scripts/src/service/WebpackService.ts | 4 ++-- packages/build-scripts/src/service/build.ts | 4 ++-- packages/build-scripts/src/service/start.ts | 4 ++-- 6 files changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/build-scripts/src/core/Context.ts b/packages/build-scripts/src/core/Context.ts index 49175fa..9157397 100644 --- a/packages/build-scripts/src/core/Context.ts +++ b/packages/build-scripts/src/core/Context.ts @@ -83,7 +83,7 @@ export interface IOnHook { } export interface IPluginConfigWebpack { - (config: WebpackChain): Promise; + (config: WebpackChain): Promise | void; } export interface IUserConfigWebpack { @@ -215,8 +215,6 @@ export interface ICommandModules { [command: string]: CommandModule; } -export type GetCommandModule = (options: { command: CommandName; userConfig: IUserConfig }) => CommandModule; - export type RegisterCommandModules = (key: string, module: CommandModule) => void; export interface IContextOptions { @@ -923,7 +921,7 @@ class Context { this.commandModules[moduleKey] = module; } - public getCommandModule: GetCommandModule = (options) => { + public getCommandModule (options: { command: CommandName; commandArgs: CommandArgs; userConfig: IUserConfig }): CommandModule { const { command } = options; if (this.commandModules[command]) { return this.commandModules[command]; @@ -951,7 +949,7 @@ class Context { public run = async (options?: T): Promise

=> { const { command, commandArgs } = this; - const commandModule = this.getCommandModule({ command: this.command, userConfig: this.userConfig }); + const commandModule = this.getCommandModule({ command, commandArgs, userConfig: this.userConfig }); log.verbose( 'OPTIONS', `${command} cliOptions: ${JSON.stringify(commandArgs, null, 2)}`, diff --git a/packages/build-scripts/src/index.ts b/packages/build-scripts/src/index.ts index c05734e..d2468ea 100644 --- a/packages/build-scripts/src/index.ts +++ b/packages/build-scripts/src/index.ts @@ -1,9 +1,9 @@ import build = require('./commands/build'); import start = require('./commands/start'); import test = require('./commands/test'); -import WebpackContext from './service/WebpackService'; -import JestContext from './service/JestService'; +import WebpackService from './service/WebpackService'; +import JestService from './service/JestService'; export * from './core/Context'; export * from './types'; -export { build, start, test, WebpackContext, JestContext }; +export { build, start, test, WebpackService, JestService }; diff --git a/packages/build-scripts/src/service/JestService.ts b/packages/build-scripts/src/service/JestService.ts index 54dd08c..e4d1f24 100644 --- a/packages/build-scripts/src/service/JestService.ts +++ b/packages/build-scripts/src/service/JestService.ts @@ -1,11 +1,11 @@ import Context, { IContextOptions } from '../core/Context'; import test = require('./test'); -class JestContext extends Context { +class JestService extends Context { constructor(props: IContextOptions) { super(props); super.registerCommandModules('test', test); } } -export default JestContext; +export default JestService; diff --git a/packages/build-scripts/src/service/WebpackService.ts b/packages/build-scripts/src/service/WebpackService.ts index e6b5e62..6a041c2 100644 --- a/packages/build-scripts/src/service/WebpackService.ts +++ b/packages/build-scripts/src/service/WebpackService.ts @@ -2,7 +2,7 @@ import Context, { IContextOptions } from '../core/Context'; import start = require('./start'); import build = require('./build'); -class WebpackContext extends Context { +class WebpackService extends Context { constructor(props: IContextOptions) { super(props); super.registerCommandModules('start', start); @@ -10,4 +10,4 @@ class WebpackContext extends Context { } } -export default WebpackContext; +export default WebpackService; diff --git a/packages/build-scripts/src/service/build.ts b/packages/build-scripts/src/service/build.ts index c3aa04a..b69995c 100644 --- a/packages/build-scripts/src/service/build.ts +++ b/packages/build-scripts/src/service/build.ts @@ -8,8 +8,8 @@ import fs = require('fs-extra'); import path = require('path'); import log = require('../utils/log'); -export = async function(context: Context, options: IRunOptions): Promise { - const { eject } = options; +export = async function(context: Context, options?: IRunOptions): Promise { + const { eject } = options || {}; const configArr = context.getWebpackConfig(); const { command, commandArgs, applyHook, rootDir, webpack: webpackInstance } = context; await applyHook(`before.${command}.load`, { args: commandArgs, webpackConfig: configArr }); diff --git a/packages/build-scripts/src/service/start.ts b/packages/build-scripts/src/service/start.ts index 0a36153..f191d59 100644 --- a/packages/build-scripts/src/service/start.ts +++ b/packages/build-scripts/src/service/start.ts @@ -10,8 +10,8 @@ import log = require('../utils/log'); type DevServer = Record; -export = async function(context: Context, options: IRunOptions): Promise { - const { eject } = options; +export = async function(context: Context, options?: IRunOptions): Promise { + const { eject } = options || {}; const configArr = context.getWebpackConfig(); const { command, commandArgs, webpack, applyHook } = context; await applyHook(`before.${command}.load`, { args: commandArgs, webpackConfig: configArr }); From 1e738b29d9b21c1c06c1d4d09c3e4c0c436794c9 Mon Sep 17 00:00:00 2001 From: ClarkXia Date: Mon, 19 Jul 2021 11:30:11 +0800 Subject: [PATCH 09/21] fix: add export of Context --- packages/build-scripts/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/build-scripts/src/index.ts b/packages/build-scripts/src/index.ts index d2468ea..0b0031c 100644 --- a/packages/build-scripts/src/index.ts +++ b/packages/build-scripts/src/index.ts @@ -3,7 +3,8 @@ import start = require('./commands/start'); import test = require('./commands/test'); import WebpackService from './service/WebpackService'; import JestService from './service/JestService'; +import Context from './core/Context'; export * from './core/Context'; export * from './types'; -export { build, start, test, WebpackService, JestService }; +export { build, start, test, WebpackService, JestService, Context }; From 647bb9eea7a1fb11a2fa5e174ae0f668741a913f Mon Sep 17 00:00:00 2001 From: ClarkXia Date: Mon, 19 Jul 2021 15:34:01 +0800 Subject: [PATCH 10/21] fix: ts type --- packages/build-scripts/src/commands/build.ts | 2 +- packages/build-scripts/src/commands/start.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/build-scripts/src/commands/build.ts b/packages/build-scripts/src/commands/build.ts index 4e20a8d..6530623 100644 --- a/packages/build-scripts/src/commands/build.ts +++ b/packages/build-scripts/src/commands/build.ts @@ -10,7 +10,7 @@ export = async function({ eject, plugins, getBuiltInPlugins, -}: IContextOptions & IRunOptions): Promise { +}: Omit & IRunOptions): Promise { const command = 'build'; const service = new WebpackService({ diff --git a/packages/build-scripts/src/commands/start.ts b/packages/build-scripts/src/commands/start.ts index 81b5f9e..1b52e57 100644 --- a/packages/build-scripts/src/commands/start.ts +++ b/packages/build-scripts/src/commands/start.ts @@ -10,7 +10,7 @@ export = async function({ eject, plugins, getBuiltInPlugins, -}: IContextOptions & { eject?: boolean }): Promise { +}: Omit & { eject?: boolean }): Promise { const command = 'start'; const service = new WebpackService({ From f60b5aaf711b79b1dd5f3fb58de99cc1b58665b1 Mon Sep 17 00:00:00 2001 From: ClarkXia Date: Mon, 19 Jul 2021 15:55:47 +0800 Subject: [PATCH 11/21] fix: compatible with camelCase --- packages/build-scripts/src/core/Context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/build-scripts/src/core/Context.ts b/packages/build-scripts/src/core/Context.ts index 9157397..47ae999 100644 --- a/packages/build-scripts/src/core/Context.ts +++ b/packages/build-scripts/src/core/Context.ts @@ -1,7 +1,6 @@ import * as path from 'path'; import * as fs from 'fs-extra'; import chalk from 'chalk'; -import camelCase from 'camelCase'; import webpack, { MultiStats } from 'webpack'; import { Logger } from 'npmlog'; import { AggregatedResult } from '@jest/test-result'; @@ -18,6 +17,7 @@ import hijackWebpackResolve from '../utils/hijackWebpack'; import assert = require('assert'); import _ = require('lodash'); +import camelCase = require('camelcase'); import WebpackChain = require('webpack-chain'); import WebpackDevServer = require('webpack-dev-server'); import log = require('../utils/log'); From 5da9ac9628ff91a44f7089c7cd4e8501627c059d Mon Sep 17 00:00:00 2001 From: ClarkXia Date: Tue, 20 Jul 2021 17:45:09 +0800 Subject: [PATCH 12/21] fix: export apis --- packages/build-scripts/src/index.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/build-scripts/src/index.ts b/packages/build-scripts/src/index.ts index 0b0031c..f41f786 100644 --- a/packages/build-scripts/src/index.ts +++ b/packages/build-scripts/src/index.ts @@ -4,7 +4,20 @@ import test = require('./commands/test'); import WebpackService from './service/WebpackService'; import JestService from './service/JestService'; import Context from './core/Context'; +import webpackStart from './service/start'; +import webpackBuild from './service/build'; +import jestTest from './service/test'; export * from './core/Context'; export * from './types'; -export { build, start, test, WebpackService, JestService, Context }; +export { + build, + start, + test, + WebpackService, + JestService, + Context, + webpackStart, + webpackBuild, + jestTest, +}; From 1ca544cb09f9efb5cf594b7b686341cdd9dbdd8c Mon Sep 17 00:00:00 2001 From: ClarkXia Date: Wed, 21 Jul 2021 11:12:52 +0800 Subject: [PATCH 13/21] fix: export apis --- packages/build-scripts/src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/build-scripts/src/index.ts b/packages/build-scripts/src/index.ts index f41f786..e97655d 100644 --- a/packages/build-scripts/src/index.ts +++ b/packages/build-scripts/src/index.ts @@ -1,12 +1,12 @@ import build = require('./commands/build'); import start = require('./commands/start'); import test = require('./commands/test'); +import webpackStart = require('./service/start'); +import webpackBuild = require('./service/build'); +import jestTest = require('./service/test'); import WebpackService from './service/WebpackService'; import JestService from './service/JestService'; import Context from './core/Context'; -import webpackStart from './service/start'; -import webpackBuild from './service/build'; -import jestTest from './service/test'; export * from './core/Context'; export * from './types'; From 377bfd8835842cce3b4a3a71e3ac1900c5593a6d Mon Sep 17 00:00:00 2001 From: ClarkXia Date: Tue, 27 Jul 2021 18:26:56 +0800 Subject: [PATCH 14/21] feat: support config write with ts/esm --- packages/build-scripts/package.json | 1 + packages/build-scripts/src/core/Context.ts | 65 +++++++------- .../build-scripts/src/utils/buildConfig.ts | 56 +++++++++++++ .../build-scripts/src/utils/loadConfig.ts | 84 +++++++++++++++++++ .../build-scripts/test/configFile/config.js | 5 ++ .../build-scripts/test/configFile/config.json | 3 + .../build-scripts/test/configFile/config.ts | 11 +++ .../test/configFile/esmConfig.js | 5 ++ packages/build-scripts/test/context.test.ts | 7 +- .../build-scripts/test/loadConfig.test.ts | 29 +++++++ packages/build-scripts/test/pluginAPI.test.ts | 12 ++- 11 files changed, 242 insertions(+), 36 deletions(-) create mode 100644 packages/build-scripts/src/utils/buildConfig.ts create mode 100644 packages/build-scripts/src/utils/loadConfig.ts create mode 100644 packages/build-scripts/test/configFile/config.js create mode 100644 packages/build-scripts/test/configFile/config.json create mode 100644 packages/build-scripts/test/configFile/config.ts create mode 100644 packages/build-scripts/test/configFile/esmConfig.js create mode 100644 packages/build-scripts/test/loadConfig.test.ts diff --git a/packages/build-scripts/package.json b/packages/build-scripts/package.json index da768b7..1c4cd71 100644 --- a/packages/build-scripts/package.json +++ b/packages/build-scripts/package.json @@ -30,6 +30,7 @@ "commander": "^2.19.0", "deepmerge": "^4.0.0", "detect-port": "^1.3.0", + "esbuild": "^0.12.16", "fs-extra": "^8.1.0", "json5": "^2.1.3", "lodash": "^4.17.15", diff --git a/packages/build-scripts/src/core/Context.ts b/packages/build-scripts/src/core/Context.ts index 47ae999..d5f7681 100644 --- a/packages/build-scripts/src/core/Context.ts +++ b/packages/build-scripts/src/core/Context.ts @@ -14,6 +14,7 @@ import { JsonArray, } from '../types'; import hijackWebpackResolve from '../utils/hijackWebpack'; +import loadConfig from '../utils/loadConfig'; import assert = require('assert'); import _ = require('lodash'); @@ -21,7 +22,6 @@ import camelCase = require('camelcase'); import WebpackChain = require('webpack-chain'); import WebpackDevServer = require('webpack-dev-server'); import log = require('../utils/log'); -import JSON5 = require('json5'); const PKG_FILE = 'package.json'; const USER_CONFIG_FILE = 'build.json'; @@ -302,6 +302,8 @@ class Context { public plugins: IPluginInfo[]; + private options: IContextOptions; + // 通过registerTask注册,存放初始的webpack-chain配置 private configArr: ITaskConfig[]; @@ -329,13 +331,14 @@ class Context { public commandModules: ICommandModules = {}; - constructor({ - command, - rootDir = process.cwd(), - args = {}, - plugins = [], - getBuiltInPlugins = () => [], - }: IContextOptions) { + constructor(options: IContextOptions) { + const { + command, + rootDir = process.cwd(), + args = {}, + } = options || {}; + + this.options = options; this.command = command; this.commandArgs = args; this.rootDir = rootDir; @@ -360,24 +363,8 @@ class Context { this.cancelTaskNames = []; this.pkg = this.getProjectFile(PKG_FILE); - this.userConfig = this.getUserConfig(); - // run getBuiltInPlugins before resolve webpack while getBuiltInPlugins may add require hook for webpack - const builtInPlugins: IPluginList = [ - ...plugins, - ...getBuiltInPlugins(this.userConfig), - ]; - // custom webpack - const webpackInstancePath = this.userConfig.customWebpack - ? require.resolve('webpack', { paths: [this.rootDir] }) - : 'webpack'; - this.webpack = require(webpackInstancePath); - if (this.userConfig.customWebpack) { - hijackWebpackResolve(this.webpack, this.rootDir); - } - // register buildin options + // register builtin options this.registerCliOption(BUILTIN_CLI_OPTIONS); - this.checkPluginValue(builtInPlugins); // check plugins property - this.plugins = this.resolvePlugins(builtInPlugins); } private registerConfig = ( @@ -455,7 +442,7 @@ class Context { return config; }; - private getUserConfig = (): IUserConfig => { + private getUserConfig = async (): Promise => { const { config } = this.commandArgs; let configPath = ''; if (config) { @@ -468,12 +455,9 @@ class Context { let userConfig: IUserConfig = { plugins: [], }; - const isJsFile = path.extname(configPath) === '.js'; if (fs.existsSync(configPath)) { try { - userConfig = isJsFile - ? require(configPath) - : JSON5.parse(fs.readFileSync(configPath, 'utf-8')); // read build.json + userConfig = await loadConfig(configPath, log); } catch (err) { log.info( 'CONFIG', @@ -719,6 +703,26 @@ class Context { }); }; + public resolveConfig = async (): Promise => { + this.userConfig = await this.getUserConfig(); + const { plugins = [], getBuiltInPlugins = () => []} = this.options; + // run getBuiltInPlugins before resolve webpack while getBuiltInPlugins may add require hook for webpack + const builtInPlugins: IPluginList = [ + ...plugins, + ...getBuiltInPlugins(this.userConfig), + ]; + // custom webpack + const webpackInstancePath = this.userConfig.customWebpack + ? require.resolve('webpack', { paths: [this.rootDir] }) + : 'webpack'; + this.webpack = require(webpackInstancePath); + if (this.userConfig.customWebpack) { + hijackWebpackResolve(this.webpack, this.rootDir); + } + this.checkPluginValue(builtInPlugins); // check plugins property + this.plugins = this.resolvePlugins(builtInPlugins); + } + private runPlugins = async (): Promise => { for (const pluginInfo of this.plugins) { const { fn, options, name: pluginName } = pluginInfo; @@ -931,6 +935,7 @@ class Context { }; public setUp = async (): Promise => { + await this.resolveConfig(); await this.runPlugins(); await this.runConfigModification(); await this.runUserConfig(); diff --git a/packages/build-scripts/src/utils/buildConfig.ts b/packages/build-scripts/src/utils/buildConfig.ts new file mode 100644 index 0000000..6215ee5 --- /dev/null +++ b/packages/build-scripts/src/utils/buildConfig.ts @@ -0,0 +1,56 @@ +import * as path from 'path'; +import * as fs from 'fs'; +import { build as esbuild, Plugin} from 'esbuild'; + +const buildConfig = async (fileName: string, mjs: boolean): Promise => { + const pluginExternalDeps: Plugin = { + name: 'plugin-external-deps', + setup(build) { + build.onResolve({ filter: /.*/ }, (args) => { + const id = args.path; + if (id[0] !== '.' && !path.isAbsolute(id)) { + return { + external: true, + }; + } + }); + }, + }; + const pluginReplaceImport: Plugin = { + name: 'plugin-replace-import-meta', + setup(build) { + build.onLoad({ filter: /\.[jt]s$/ }, (args) => { + const contents = fs.readFileSync(args.path, 'utf8'); + return { + loader: args.path.endsWith('.ts') ? 'ts' : 'js', + contents: contents + .replace( + /\bimport\.meta\.url\b/g, + JSON.stringify(`file://${args.path}`), + ) + .replace( + /\b__dirname\b/g, + JSON.stringify(path.dirname(args.path)), + ) + .replace(/\b__filename\b/g, JSON.stringify(args.path)), + }; + }); + }, + }; + + const result = await esbuild({ + entryPoints: [fileName], + outfile: 'out.js', + write: false, + platform: 'node', + bundle: true, + format: mjs ? 'esm' : 'cjs', + metafile: true, + plugins: [pluginExternalDeps, pluginReplaceImport], + }); + const { text } = result.outputFiles[0]; + + return text; +}; + +export default buildConfig; diff --git a/packages/build-scripts/src/utils/loadConfig.ts b/packages/build-scripts/src/utils/loadConfig.ts new file mode 100644 index 0000000..59e63de --- /dev/null +++ b/packages/build-scripts/src/utils/loadConfig.ts @@ -0,0 +1,84 @@ +import * as path from 'path'; +import * as fs from 'fs'; +import { Logger } from 'npmlog'; +import buildConfig from './buildConfig'; +import JSON5 = require('json5'); + +interface INodeModuleWithCompile extends NodeModule { + _compile(code: string, filename: string): any; +} + +async function loadConfig(filePath: string, log: Logger): Promise { + const start = Date.now(); + const isJson = filePath.endsWith('.json'); + const isTS = filePath.endsWith('.ts'); + const isMjs = filePath.endsWith('.mjs'); + + let userConfig: T | undefined; + + if (isJson) { + return JSON5.parse(fs.readFileSync(filePath, 'utf8')); + } + + if (isMjs) { + const fileUrl = require('url').pathToFileURL(filePath); + if (isTS) { + // if config file is a typescript file + // transform config first, write it to disk + // load it with native Node ESM + const code = await buildConfig(filePath, true); + const tempFile = `${filePath}.js`; + fs.writeFileSync(tempFile, code); + // eslint-disable-next-line no-eval + userConfig = (await eval(`import(tempFile + '?t=${Date.now()}')`)).default; + // delete the file after eval + fs.unlinkSync(tempFile); + log.verbose('[config]',`TS + native esm module loaded in ${Date.now() - start}ms, ${fileUrl}`); + } else { + // eslint-disable-next-line no-eval + userConfig = (await eval(`import(fileUrl + '?t=${Date.now()}')`)).default; + log.verbose('[config]',`native esm config loaded in ${Date.now() - start}ms, ${fileUrl}`); + } + } + + if (!userConfig && !isTS && !isMjs) { + // try to load config as cjs module + try { + delete require.cache[require.resolve(filePath)]; + userConfig = require(filePath); + log.verbose('[config]', `cjs module loaded in ${Date.now() - start}ms`); + } catch (e) { + const ignored = new RegExp( + [ + `Cannot use import statement`, + `Must use import to load ES Module`, + // #1635, #2050 some Node 12.x versions don't have esm detection + // so it throws normal syntax errors when encountering esm syntax + `Unexpected token`, + `Unexpected identifier`, + ].join('|'), + ); + if (!ignored.test(e.message)) { + throw e; + } + } + } + + if (!userConfig) { + // if cjs module load failed, the config file is ts or using es import syntax + // bundle config with cjs format + const code = await buildConfig(filePath, false); + const tempFile = `${filePath}.js`; + fs.writeFileSync(tempFile, code); + delete require.cache[require.resolve(tempFile)]; + // eslint-disable-next-line @typescript-eslint/no-var-requires + const raw = require(tempFile); + // eslint-disable-next-line no-underscore-dangle + userConfig = raw.__esModule ? raw.default : raw; + fs.unlinkSync(tempFile); + log.verbose('[config]', `bundled module file loaded in ${Date.now() - start}m`); + } + return userConfig; +} + +export default loadConfig; diff --git a/packages/build-scripts/test/configFile/config.js b/packages/build-scripts/test/configFile/config.js new file mode 100644 index 0000000..053ebdc --- /dev/null +++ b/packages/build-scripts/test/configFile/config.js @@ -0,0 +1,5 @@ +const path = require('path'); + +module.exports = { + entry: path.join('src', 'config.js'), +}; \ No newline at end of file diff --git a/packages/build-scripts/test/configFile/config.json b/packages/build-scripts/test/configFile/config.json new file mode 100644 index 0000000..44ac5c7 --- /dev/null +++ b/packages/build-scripts/test/configFile/config.json @@ -0,0 +1,3 @@ +{ + "entry": "src/json.js" +} \ No newline at end of file diff --git a/packages/build-scripts/test/configFile/config.ts b/packages/build-scripts/test/configFile/config.ts new file mode 100644 index 0000000..0db7d5b --- /dev/null +++ b/packages/build-scripts/test/configFile/config.ts @@ -0,0 +1,11 @@ +import * as path from 'path'; + +interface IConfig { + entry: string; +} + +const config: IConfig = { + entry: path.join('src', 'tsFile.ts'), +} + +export default config; \ No newline at end of file diff --git a/packages/build-scripts/test/configFile/esmConfig.js b/packages/build-scripts/test/configFile/esmConfig.js new file mode 100644 index 0000000..fad7c74 --- /dev/null +++ b/packages/build-scripts/test/configFile/esmConfig.js @@ -0,0 +1,5 @@ +const config = { + entry: 'src/mjsFile.mjs', +}; + +export default config; diff --git a/packages/build-scripts/test/context.test.ts b/packages/build-scripts/test/context.test.ts index c97a090..376acb1 100644 --- a/packages/build-scripts/test/context.test.ts +++ b/packages/build-scripts/test/context.test.ts @@ -7,11 +7,14 @@ describe('merge modeConfig', () => { command: 'start', rootDir: path.join(__dirname, 'fixtures/modeConfig/') }); - it('combine basic config', () => { + + it('combine basic config', async () => { + await context.resolveConfig(); expect(context.userConfig.entry).toEqual('src/test'); }); - it('combine plugins', () => { + it('combine plugins', async () => { + await context.resolveConfig(); expect(context.userConfig.plugins).toEqual([['./a.plugin.js', { name: 'test'}], './b.plugin.js']); }); }); diff --git a/packages/build-scripts/test/loadConfig.test.ts b/packages/build-scripts/test/loadConfig.test.ts new file mode 100644 index 0000000..e82e724 --- /dev/null +++ b/packages/build-scripts/test/loadConfig.test.ts @@ -0,0 +1,29 @@ +import * as path from 'path'; +import loadConfig from '../src/utils/loadConfig'; +import log = require('../src/utils/log'); + +interface IUserConfig { + entry: string; +} + +describe('load config file', () => { + it('json file', async () => { + const userConfig = await loadConfig(path.join(__dirname, './configFile/config.json'), log); + expect(userConfig.entry).toBe('src/json.js'); + }) + + it('js file', async () => { + const userConfig = await loadConfig(path.join(__dirname, './configFile/config.js'), log); + expect(userConfig.entry).toBe('src/config.js'); + }) + + it('ts file', async () => { + const userConfig = await loadConfig(path.join(__dirname, './configFile/config.ts'), log); + expect(userConfig.entry).toBe('src/tsFile.ts'); + }) + + it('esm file', async () => { + const userConfig = await loadConfig(path.join(__dirname, './configFile/esmConfig.js'), log); + expect(userConfig.entry).toBe('src/mjsFile.mjs'); + }) +}); \ No newline at end of file diff --git a/packages/build-scripts/test/pluginAPI.test.ts b/packages/build-scripts/test/pluginAPI.test.ts index aecb8e7..ae248d1 100644 --- a/packages/build-scripts/test/pluginAPI.test.ts +++ b/packages/build-scripts/test/pluginAPI.test.ts @@ -65,30 +65,34 @@ describe('api modifyUserConfig', () => { command: 'start', rootDir: path.join(__dirname, 'fixtures/basic/') }) - it('api modifyUserConfig of plugins', () => { + it('api modifyUserConfig of plugins', async () => { let modified = false try { + await context.resolveConfig(); context.modifyUserConfig('plugins', []) modified = true } catch(err) {} expect(modified).toBe(false) }) - it('api config plugins by function', () => { + it('api config plugins by function', async () => { context.modifyUserConfig(() => { return { plugins: ['build-plugin-test'], } }) + await context.resolveConfig(); expect(context.userConfig).toEqual({ plugins: [] }) }) - it('api modifyUserConfig single config', () => { + it('api modifyUserConfig single config', async () => { + await context.resolveConfig(); context.modifyUserConfig('entry', './src/temp') expect(context.userConfig).toEqual({ plugins: [], entry: './src/temp' }) }) - it('api modifyUserConfig by function', () => { + it('api modifyUserConfig by function', async () => { + await context.resolveConfig(); context.modifyUserConfig(() => { return { entry: './src/index.ts', From a807bba30d7c89cc29e685a3d1e6e8231c3795cb Mon Sep 17 00:00:00 2001 From: ClarkXia Date: Tue, 27 Jul 2021 19:51:47 +0800 Subject: [PATCH 15/21] feat: enhance modifyUserConfig by config path --- packages/build-scripts/src/core/Context.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/build-scripts/src/core/Context.ts b/packages/build-scripts/src/core/Context.ts index d5f7681..2ea351b 100644 --- a/packages/build-scripts/src/core/Context.ts +++ b/packages/build-scripts/src/core/Context.ts @@ -164,6 +164,7 @@ export interface IModifyConfig { export interface IModifyUserConfig { (configKey: string | IModifyConfig, value?: any): void; + (configKey: string, value?: any | ((value: T) => T)): void; } export interface IGetAllPlugin { @@ -618,7 +619,9 @@ class Context { if (configKey === 'plugins') { throw new Error(errorMsg); } - this.userConfig[configKey] = value; + const configPath = configKey.split('.'); + const originalValue = _.get(this.userConfig, configPath); + _.set(this.userConfig, configPath, typeof value !== 'function' ? value : value(originalValue)); } else if (typeof configKey === 'function') { const modifiedValue = configKey(this.userConfig); if (_.isPlainObject(modifiedValue)) { From 9fd8d95cf81e50342068e958e7903484f4de5bdf Mon Sep 17 00:00:00 2001 From: ClarkXia Date: Tue, 27 Jul 2021 19:54:18 +0800 Subject: [PATCH 16/21] fix: ts type --- packages/build-scripts/src/core/Context.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/build-scripts/src/core/Context.ts b/packages/build-scripts/src/core/Context.ts index 2ea351b..e2b472e 100644 --- a/packages/build-scripts/src/core/Context.ts +++ b/packages/build-scripts/src/core/Context.ts @@ -164,7 +164,6 @@ export interface IModifyConfig { export interface IModifyUserConfig { (configKey: string | IModifyConfig, value?: any): void; - (configKey: string, value?: any | ((value: T) => T)): void; } export interface IGetAllPlugin { From 6c996d8422501b6b61e2b785d546243f2a0bdf14 Mon Sep 17 00:00:00 2001 From: ClarkXia Date: Wed, 28 Jul 2021 14:02:28 +0800 Subject: [PATCH 17/21] fix: get command module --- packages/build-scripts/src/core/Context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/build-scripts/src/core/Context.ts b/packages/build-scripts/src/core/Context.ts index e2b472e..292463a 100644 --- a/packages/build-scripts/src/core/Context.ts +++ b/packages/build-scripts/src/core/Context.ts @@ -956,7 +956,6 @@ class Context { public run = async (options?: T): Promise

=> { const { command, commandArgs } = this; - const commandModule = this.getCommandModule({ command, commandArgs, userConfig: this.userConfig }); log.verbose( 'OPTIONS', `${command} cliOptions: ${JSON.stringify(commandArgs, null, 2)}`, @@ -968,6 +967,7 @@ class Context { await this.applyHook(`error`, { err }); throw err; } + const commandModule = this.getCommandModule({ command, commandArgs, userConfig: this.userConfig }); return commandModule(this, options); } } From 4b37f4eecbc2a3ddc1cbe4ec770407f5c004ee42 Mon Sep 17 00:00:00 2001 From: ClarkXia Date: Tue, 3 Aug 2021 13:57:08 +0800 Subject: [PATCH 18/21] fix: remove peerDependencies while build without webpack --- packages/build-scripts/package.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/build-scripts/package.json b/packages/build-scripts/package.json index 1c4cd71..238e64d 100644 --- a/packages/build-scripts/package.json +++ b/packages/build-scripts/package.json @@ -54,10 +54,5 @@ "typescript": "^3.7.2", "webpack": "^5.0.0", "webpack-dev-server": "^3.7.2" - }, - "peerDependencies": { - "webpack": ">=4.27.1", - "jest": ">=26.4.2", - "webpack-dev-server": ">=3.7.2" } } From 8985b94c857be88dea9ec814e01b4e6e5dbc797f Mon Sep 17 00:00:00 2001 From: ClarkXia Date: Fri, 6 Aug 2021 15:46:33 +0800 Subject: [PATCH 19/21] fix: delete temp file when error occur --- packages/build-scripts/package.json | 2 +- .../build-scripts/src/utils/loadConfig.ts | 24 +++++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/build-scripts/package.json b/packages/build-scripts/package.json index 238e64d..7a3e352 100644 --- a/packages/build-scripts/package.json +++ b/packages/build-scripts/package.json @@ -1,6 +1,6 @@ { "name": "build-scripts", - "version": "1.1.0", + "version": "1.1.0-10", "license": "MIT", "description": "scripts core", "main": "lib/index.js", diff --git a/packages/build-scripts/src/utils/loadConfig.ts b/packages/build-scripts/src/utils/loadConfig.ts index 59e63de..93bc3c0 100644 --- a/packages/build-scripts/src/utils/loadConfig.ts +++ b/packages/build-scripts/src/utils/loadConfig.ts @@ -29,8 +29,13 @@ async function loadConfig(filePath: string, log: Logger): Promise(filePath: string, log: Logger): Promise Date: Fri, 6 Aug 2021 16:25:57 +0800 Subject: [PATCH 20/21] feat: enhance modifyUserConfig --- packages/build-scripts/src/core/Context.ts | 28 ++++++++++++++++--- packages/build-scripts/test/pluginAPI.test.ts | 22 +++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/packages/build-scripts/src/core/Context.ts b/packages/build-scripts/src/core/Context.ts index 292463a..19e7969 100644 --- a/packages/build-scripts/src/core/Context.ts +++ b/packages/build-scripts/src/core/Context.ts @@ -21,6 +21,7 @@ import _ = require('lodash'); import camelCase = require('camelcase'); import WebpackChain = require('webpack-chain'); import WebpackDevServer = require('webpack-dev-server'); +import deepmerge = require('deepmerge'); import log = require('../utils/log'); const PKG_FILE = 'package.json'; @@ -30,6 +31,7 @@ const PLUGIN_CONTEXT_KEY = [ 'commandArgs' as 'commandArgs', 'rootDir' as 'rootDir', 'userConfig' as 'userConfig', + 'originalUserConfig' as 'originalUserConfig', 'pkg' as 'pkg', 'webpack' as 'webpack', ]; @@ -163,7 +165,7 @@ export interface IModifyConfig { } export interface IModifyUserConfig { - (configKey: string | IModifyConfig, value?: any): void; + (configKey: string | IModifyConfig, value?: any, options?: { deepmerge: boolean }): void; } export interface IGetAllPlugin { @@ -287,6 +289,17 @@ export type IRegistrationKey = | 'modifyConfigRegistrationCallbacks' | 'modifyCliRegistrationCallbacks'; +const mergeConfig = (currentValue: T, newValue: T): T => { + // only merge when currentValue and newValue is object and array + const isBothArray = Array.isArray(currentValue) && Array.isArray(newValue); + const isBothObject = _.isPlainObject(currentValue) && _.isPlainObject(newValue); + if (isBothArray || isBothObject) { + return deepmerge(currentValue, newValue); + } else { + return newValue; + } +}; + class Context { public command: CommandName; @@ -300,6 +313,8 @@ class Context { public userConfig: IUserConfig; + public originalUserConfig: IUserConfig; + public plugins: IPluginInfo[]; private options: IContextOptions; @@ -612,15 +627,17 @@ class Context { return !!this.methodRegistration[name]; }; - public modifyUserConfig: IModifyUserConfig = (configKey, value) => { + public modifyUserConfig: IModifyUserConfig = (configKey, value, options) => { const errorMsg = 'config plugins is not support to be modified'; + const { deepmerge: mergeInDeep } = options || {}; if (typeof configKey === 'string') { if (configKey === 'plugins') { throw new Error(errorMsg); } const configPath = configKey.split('.'); const originalValue = _.get(this.userConfig, configPath); - _.set(this.userConfig, configPath, typeof value !== 'function' ? value : value(originalValue)); + const newValue = typeof value !== 'function' ? value : value(originalValue); + _.set(this.userConfig, configPath, mergeInDeep ? mergeConfig(originalValue, newValue): newValue); } else if (typeof configKey === 'function') { const modifiedValue = configKey(this.userConfig); if (_.isPlainObject(modifiedValue)) { @@ -629,7 +646,8 @@ class Context { } delete modifiedValue.plugins; Object.keys(modifiedValue).forEach(modifiedConfigKey => { - this.userConfig[modifiedConfigKey] = modifiedValue[modifiedConfigKey]; + const originalValue = this.userConfig[modifiedConfigKey]; + this.userConfig[modifiedConfigKey] = mergeInDeep ? mergeConfig(originalValue, modifiedValue[modifiedConfigKey]) : modifiedValue[modifiedConfigKey] ; }); } else { throw new Error(`modifyUserConfig must return a plain object`); @@ -707,6 +725,8 @@ class Context { public resolveConfig = async (): Promise => { this.userConfig = await this.getUserConfig(); + // shallow copy of userConfig while userConfig may be modified + this.originalUserConfig = { ...this.userConfig }; const { plugins = [], getBuiltInPlugins = () => []} = this.options; // run getBuiltInPlugins before resolve webpack while getBuiltInPlugins may add require hook for webpack const builtInPlugins: IPluginList = [ diff --git a/packages/build-scripts/test/pluginAPI.test.ts b/packages/build-scripts/test/pluginAPI.test.ts index ae248d1..f47689b 100644 --- a/packages/build-scripts/test/pluginAPI.test.ts +++ b/packages/build-scripts/test/pluginAPI.test.ts @@ -89,6 +89,28 @@ describe('api modifyUserConfig', () => { await context.resolveConfig(); context.modifyUserConfig('entry', './src/temp') expect(context.userConfig).toEqual({ plugins: [], entry: './src/temp' }) + expect(context.originalUserConfig).toEqual({ plugins: [] }) + }) + + it('api merge config - object', async () => { + await context.resolveConfig(); + context.modifyUserConfig('entry', { index: 'src/index' }); + context.modifyUserConfig('entry', { add: 'src/add' }, { deepmerge: true }); + expect(context.userConfig).toEqual({ plugins: [], entry: { index: 'src/index', add: 'src/add'} }) + }) + + it('api merge config - array', async () => { + await context.resolveConfig(); + context.modifyUserConfig('entry', ['index']); + context.modifyUserConfig('entry', ['add'], { deepmerge: true }); + expect(context.userConfig).toEqual({ plugins: [], entry: ['index', 'add'] }) + }) + + it('api merge config - overwrite', async () => { + await context.resolveConfig(); + context.modifyUserConfig('entry', ['index']); + context.modifyUserConfig('entry', 'add', { deepmerge: true }); + expect(context.userConfig).toEqual({ plugins: [], entry: 'add' }); }) it('api modifyUserConfig by function', async () => { From c98d66e642086e8ec2c15292992b3dcd4094541b Mon Sep 17 00:00:00 2001 From: ClarkXia Date: Fri, 6 Aug 2021 16:30:45 +0800 Subject: [PATCH 21/21] chore: version --- packages/build-scripts/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/build-scripts/package.json b/packages/build-scripts/package.json index 7a3e352..238e64d 100644 --- a/packages/build-scripts/package.json +++ b/packages/build-scripts/package.json @@ -1,6 +1,6 @@ { "name": "build-scripts", - "version": "1.1.0-10", + "version": "1.1.0", "license": "MIT", "description": "scripts core", "main": "lib/index.js",