babel-preset-umi
提供 umi 的 babel preset,其中大部分presets 跟 plugins 都是依赖 bundler utils 内编译好的,同时也有自己编写的三个babel 插件
- autoCSSModules 解决 css 自动引入 module 问题, 通过 babel 插件编译,在路径上添加
?modules告诉打包工具需要使用module loader
const CSS_EXT_NAMES = ['.css', '.less', '.sass', '.scss', '.stylus', '.styl'];
export default function () {
return {
visitor: {
ImportDeclaration(path: Babel.NodePath<t.ImportDeclaration>) {
const {
specifiers,
source,
source: { value },
} = path.node;
if (specifiers.length && CSS_EXT_NAMES.includes(extname(value))) {
source.value = `${value}?modules`;
}
},
// e.g.
// const styles = await import('./index.less');
VariableDeclarator(path: Babel.NodePath<t.VariableDeclarator>) {
const { node } = path;
if (
t.isAwaitExpression(node.init) &&
t.isCallExpression(node.init.argument) &&
t.isImport(node.init.argument.callee) &&
node.init.argument.arguments.length === 1 &&
t.isStringLiteral(node.init.argument.arguments[0]) &&
CSS_EXT_NAMES.includes(extname(node.init.argument.arguments[0].value))
) {
node.init.argument.arguments[0].value = `${node.init.argument.arguments[0].value}?modules`;
}
},
},
};
}
2.dynamicImportNode 修改import 语句,改成动态import ,同时会移除import path 前面的注释,import('a');改成 Promise.resolve().then(() => _interopRequireWildcard(require('a')));
const builders = {
static: template('Promise.resolve().then(() => INTEROP(require(SOURCE)))'),
dynamic: template('Promise.resolve(SOURCE).then(s => INTEROP(require(s)))'),
};
function isString(node: t.Node) {
return (
t.isStringLiteral(node) ||
(t.isTemplateLiteral(node) && node.expressions.length === 0)
);
}
export default (): Babel.PluginObj => {
const visited = new WeakSet();
return {
visitor: {
Import(path: Babel.NodePath<t.Import>) {
if (visited) {
if (visited.has(path)) {
return;
}
visited.add(path);
}
const SOURCE = getImportSource(path.parent);
const builder = isString(SOURCE) ? builders.static : builders.dynamic;
const newImport = builder({
SOURCE,
INTEROP: path.hub.addHelper('interopRequireWildcard'),
});
// @ts-ignore
path.parentPath.replaceWith(newImport);
},
},
};
};
3.lockCorejs 改写 lockCorejs 路径
bundler-esbuild
- 使用 esbuild 编译代码,只支持 build,不支持 dev,
if (command === 'build') {
(async () => {
process.env.NODE_ENV = 'production';
assert(entry, `Build failed: entry not found.`);
try {
await build({
config,
cwd,
entry: {
[getEntryKey(entry)]: entry,
},
});
} catch (e) {
console.error(e);
}
})();
} else {
error(`Unsupported command ${command}.`);
}
- 从四个默认的地址寻找entry
const entry = tryPaths([
join(cwd, 'src/index.tsx'),
join(cwd, 'src/index.ts'),
join(cwd, 'index.tsx'),
join(cwd, 'index.ts'),
]);
3.利用 pirates 去 hack require hook,让 esbuild 去解析配置文件
esbuild 插件部分
- external, 把options 中 key 的值暴露给 module.exports
export default (options?: Record<string, string>): Plugin => {
return {
name: 'externals',
setup({ onLoad, onResolve }) {
if (!options || Object.keys(options).length === 0) {
return;
}
Object.keys(options).forEach((key) => {
onResolve({ filter: new RegExp(`^${key}$`) }, (args) => ({
path: args.path,
namespace: key,
}));
onLoad({ filter: /.*/, namespace: key }, () => ({
contents: `module.export=${options[key]}`,
loader: 'js',
}));
});
},
};
};
- style, 主要是寻找、解析 css ,如果是inline,还添加辅助方法,创建style标签插入 body,而且loader 为text,否则不是inline,loader 为css,
injectStyle这个方法很通用, less inline 编译后插入也是复用这个方法
if (inlineStyle) {
onResolve({ filter: /.css$/, namespace: 'style-stub' }, (args) => {
return { path: args.path, namespace: 'style-content' };
});
onResolve(
{ filter: /^__style_helper__$/, namespace: 'style-stub' },
(args) => ({
path: args.path,
namespace: 'style-helper',
sideEffects: false,
}),
);
onLoad({ filter: /.*/, namespace: 'style-helper' }, async () => ({
contents: `
export function injectStyle(text) {
if (typeof document !== 'undefined') {
var style = document.createElement('style')
var node = document.createTextNode(text)
style.appendChild(node)
document.head.appendChild(style)
}
}
`,
}));
onLoad({ filter: /.*/, namespace: 'style-stub' }, async (args) => ({
contents: `
import { injectStyle } from "__style_helper__"
import css from ${JSON.stringify(args.path)}
injectStyle(css)
`,
}));
}
- alias 在 resolve 的时候寻找alias对应的路径解析回去,同时还支持文件夹
export default (options: Record<string, string> = {}): Plugin => {
return {
name: 'alias',
setup({ onResolve }) {
const keys = sortByAffix({ arr: Object.keys(options), affix: '$' });
keys.forEach((key) => {
let value = options[key];
let filter: RegExp;
if (key.endsWith('$')) {
filter = new RegExp(`^${key}`);
} else {
filter = new RegExp(`^${key}$`);
}
onResolve({ filter: filter }, async (args) => {
const path = await resolve(
args.importer,
args.path.replace(filter, value),
);
return {
path,
};
});
if (
!key.endsWith('/') &&
existsSync(value) &&
statSync(value).isDirectory()
) {
const filter = new RegExp(`^${addSlashAffix(key)}`);
onResolve({ filter }, async (args) => {
const path = await resolve(
args.importer,
args.path.replace(filter, addSlashAffix(value)),
);
return {
path,
};
});
}
});
},
};
};
function addSlashAffix(key: string) {
if (key.endsWith('/')) {
return key;
}
return `${key}/`;
}
bundler-vite
提供vite 的dev 和 build 配置,
bundler-utils
- https 主要用于读取 https 证书创建 https 服务器,没有证书就自己创建
export async function resolveHttpsConfig(httpsConfig: HttpsServerOptions) {
// Check if mkcert is installed
try {
await execa.execa('mkcert', ['--version']);
} catch (e) {
logger.error('[HTTPS] The mkcert has not been installed.');
logger.info('[HTTPS] Please follow the guide to install manually.');
switch (process.platform) {
case 'darwin':
console.log(chalk.green('$ brew install mkcert'));
console.log(chalk.gray('# If you use firefox, please install nss.'));
console.log(chalk.green('$ brew install nss'));
console.log(chalk.green('$ mkcert -install'));
break;
case 'win32':
console.log(
chalk.green('Checkout https://github.com/FiloSottile/mkcert#windows'),
);
break;
case 'linux':
console.log(
chalk.green('Checkout https://github.com/FiloSottile/mkcert#linux'),
);
break;
default:
break;
}
throw new Error(`[HTTPS] mkcert not found.`);
}
let { key, cert, hosts } = httpsConfig;
hosts = hosts || defaultHttpsHosts;
if (!key || !cert) {
key = join(__dirname, 'umi.key.pem');
cert = join(__dirname, 'umi.pem');
}
// Generate cert and key files if they are not exist.
if (!existsSync(key) || !existsSync(cert)) {
logger.wait('[HTTPS] Generating cert and key files...');
await execa.execa('mkcert', [
'-cert-file',
cert,
'-key-file',
key,
...hosts!,
]);
}
return {
key,
cert,
};
}
- 提供 利用esbuild 跟
es-module-lexer提供代码解析功能,可以看到利用提前编译好的库,防止 semver版本问题以及break changes
import { init, parse } from '@umijs/bundler-utils/compiled/es-module-lexer';
import { Loader, transformSync } from '@umijs/bundler-utils/compiled/esbuild';
- 对内提供monorepo需要的编译好的依赖库
msfu
主要利用 webpack 5 module fedoration 的remote 以及 webpack virtrual module ,分析依赖,让依赖指向编译好的包,对于 webpack 一些内置依赖,还利用 require hook 进行 hack,(待添加详细笔记)
server
暂时只看到生成 spa 的 markup html 以及生成路由配置, 1.下面是生成 markup html
export async function getMarkup(
opts: Omit<IOpts, 'routes'> & {
path?: string;
},
) {
// TODO: use real component
let markup = ReactDOMServer.renderToString(
React.createElement('div', { id: opts.mountElementId || 'root' }),
);
function propsToString(opts: {
props: Record<string, any>;
filters?: string[];
}) {
return Object.keys(opts.props)
.filter((key) => !(opts.filters || []).includes(key))
.map((key) => `${key}=${JSON.stringify(opts.props[key])}`)
.join(' ');
}
function getScriptContent(script: { src?: string; content?: string }) {
const attrs = propsToString({
props: script,
filters: ['src', 'content'],
});
return script.src
? `<script${opts.esmScript ? ' type="module"' : ''} ${attrs} src="${
script.src
}"></script>`
: `<script${opts.esmScript ? ' type="module"' : ''} ${attrs}>${
script.content
}</script>`;
}
function getStyleContent(style: { src?: string; content?: string }) {
const attrs = propsToString({
props: style,
filters: ['src', 'content'],
});
return style.src
? `<link rel="stylesheet" ${attrs} href="${style.src}" />`
: `<style ${attrs}>${style.content}</style>`;
}
function getTagContent(opts: {
attrs: Record<string, string>;
tagName: string;
}) {
const attrs = propsToString({
props: opts.attrs,
});
return `<${opts.tagName} ${attrs} />`;
}
const favicons: string[] = [];
if (Array.isArray(opts.favicons)) {
opts.favicons.forEach((e) => {
favicons.push(`<link rel="shortcut icon" href="${e}">`);
});
}
const title = opts.title ? `<title>${opts.title}</title>` : '';
const metas = (opts.metas || []).map((meta) =>
getTagContent({ attrs: meta, tagName: 'meta' }),
);
const links = (opts.links || []).map((link) =>
getTagContent({ attrs: link, tagName: 'link' }),
);
const styles = normalizeStyles(opts.styles || []).map(getStyleContent);
const headScripts = normalizeScripts(opts.headScripts || []).map(
getScriptContent,
);
const scripts = normalizeScripts(opts.scripts || []).map(getScriptContent);
markup = [
`<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />`,
metas.join('\n'),
favicons.join('\n'),
title,
links.join('\n'),
styles.join('\n'),
headScripts.join('\n'),
`</head>
<body>`,
markup,
scripts.join('\n'),
`</body>
</html>`,
]
.filter(Boolean)
.join('\n');
if (opts.modifyHTML) {
markup = await opts.modifyHTML(markup, { path: opts.path });
}
return markup;
}
- dfs 优先,生成路由配置,把数组转换成树形结构
export function createServerRoutes(opts: {
routesById: IRoutesById;
parentId?: string;
}) {
const { routesById, parentId } = opts;
return Object.keys(routesById)
.filter((id) => routesById[id].parentId === parentId)
.map((id) => {
const route = createServerRoute({
route: routesById[id],
});
const children = createServerRoutes({
routesById,
parentId: route.id,
});
if (children.length > 0) {
// @ts-ignore
route.children = children;
}
return route;
});
}
export function createServerRoute(opts: { route: IRoute }) {
const { route } = opts;
return {
id: route.id,
path: route.path,
index: route.index,
};
}
umi
- 提供cli,走 core 包的 插件机制,
- cli 解析命令,同时会进行一系列检测,如node版本,设置 proces 的标题等
- 经典 node 中断监听
// kill(2) Ctrl-C
process.once('SIGINT', () => onSignal('SIGINT'));
// kill(3) Ctrl-\
process.once('SIGQUIT', () => onSignal('SIGQUIT'));
// kill(15) default
process.once('SIGTERM', () => onSignal('SIGTERM'));
- dev 的命令加一层监听之后,底层还是调用 core 的service command,其他命令都是直接调用core 的service command
- dev 调用 fork, fork 调用 fork 出 forkdev,fork 提供了重启功能,说白了,就是监听到重启命令,杀掉child,重新 fork出新的forkdev,fork还提供了断点功能,就是
inspect-brk
- 提供 client 层的插件机制
- 支持三种类型的机制,说白了就是三种机制对存储的同个key的hook怎么去调用,或者compose,或者reduce,或者干脆顺序执行,即event。同时如果hook是async方法,但是调用的时候不指明async 的options,将不await 方法执行完毕。
- 提供 core 的service 层
- 定义配置文件
- 对外暴露内置各种功能,如 pluginUtils,eslint配置,stylelint噢诶之等