Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions packages/build-scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -53,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"
}
}
97 changes: 62 additions & 35 deletions packages/build-scripts/src/core/Context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ import {
JsonArray,
} from '../types';
import hijackWebpackResolve from '../utils/hijackWebpack';
import loadConfig from '../utils/loadConfig';

import assert = require('assert');
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');
import JSON5 = require('json5');

const PKG_FILE = 'package.json';
const USER_CONFIG_FILE = 'build.json';
Expand All @@ -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',
];
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -287,6 +289,17 @@ export type IRegistrationKey =
| 'modifyConfigRegistrationCallbacks'
| 'modifyCliRegistrationCallbacks';

const mergeConfig = <T>(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;

Expand All @@ -300,8 +313,12 @@ class Context {

public userConfig: IUserConfig;

public originalUserConfig: IUserConfig;

public plugins: IPluginInfo[];

private options: IContextOptions;

// 通过registerTask注册,存放初始的webpack-chain配置
private configArr: ITaskConfig[];

Expand Down Expand Up @@ -329,13 +346,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;
Expand All @@ -360,24 +378,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 = (
Expand Down Expand Up @@ -455,7 +457,7 @@ class Context {
return config;
};

private getUserConfig = (): IUserConfig => {
private getUserConfig = async (): Promise<IUserConfig> => {
const { config } = this.commandArgs;
let configPath = '';
if (config) {
Expand All @@ -468,12 +470,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',
Expand Down Expand Up @@ -628,13 +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);
}
this.userConfig[configKey] = value;
const configPath = configKey.split('.');
const originalValue = _.get(this.userConfig, configPath);
const newValue = typeof value !== 'function' ? value : value(originalValue);
_.set(this.userConfig, configPath, mergeInDeep ? mergeConfig<JsonValue>(originalValue, newValue): newValue);
} else if (typeof configKey === 'function') {
const modifiedValue = configKey(this.userConfig);
if (_.isPlainObject(modifiedValue)) {
Expand All @@ -643,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<JsonValue>(originalValue, modifiedValue[modifiedConfigKey]) : modifiedValue[modifiedConfigKey] ;
});
} else {
throw new Error(`modifyUserConfig must return a plain object`);
Expand Down Expand Up @@ -719,6 +723,28 @@ class Context {
});
};

public resolveConfig = async (): Promise<void> => {
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 = [
...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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

注释是不是加代码上面更好点

this.plugins = this.resolvePlugins(builtInPlugins);
}

private runPlugins = async (): Promise<void> => {
for (const pluginInfo of this.plugins) {
const { fn, options, name: pluginName } = pluginInfo;
Expand Down Expand Up @@ -931,6 +957,7 @@ class Context {
};

public setUp = async (): Promise<ITaskConfig[]> => {
await this.resolveConfig();
await this.runPlugins();
await this.runConfigModification();
await this.runUserConfig();
Expand All @@ -949,7 +976,6 @@ class Context {

public run = async <T, P>(options?: T): Promise<P> => {
const { command, commandArgs } = this;
const commandModule = this.getCommandModule({ command, commandArgs, userConfig: this.userConfig });
log.verbose(
'OPTIONS',
`${command} cliOptions: ${JSON.stringify(commandArgs, null, 2)}`,
Expand All @@ -961,6 +987,7 @@ class Context {
await this.applyHook(`error`, { err });
throw err;
}
const commandModule = this.getCommandModule({ command, commandArgs, userConfig: this.userConfig });
return commandModule(this, options);
}
}
Expand Down
56 changes: 56 additions & 0 deletions packages/build-scripts/src/utils/buildConfig.ts
Original file line number Diff line number Diff line change
@@ -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<string> => {
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;
94 changes: 94 additions & 0 deletions packages/build-scripts/src/utils/loadConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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<T>(filePath: string, log: Logger): Promise<T|undefined> {
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);
try {
// eslint-disable-next-line no-eval
userConfig = (await eval(`import(tempFile + '?t=${Date.now()}')`)).default;
} catch(err) {
fs.unlinkSync(tempFile);
throw err;
}
// 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)];
try {
// 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;
} catch (err) {
fs.unlinkSync(tempFile);
throw err;
}
fs.unlinkSync(tempFile);
log.verbose('[config]', `bundled module file loaded in ${Date.now() - start}m`);
}
return userConfig;
}

export default loadConfig;
5 changes: 5 additions & 0 deletions packages/build-scripts/test/configFile/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const path = require('path');

module.exports = {
entry: path.join('src', 'config.js'),
};
Loading