打包构建
info
- 编写构建打包脚本
- 开发调试
- 相关代码可在 git tag v1.2 查看
打包配置和脚本参考 React
官方仓库的构建代码
这里适当做了一些调整和优化。
新增 build 命令
- 在
scripts/rollup
目录下, 新增文件build.js
。 - 在
package.json
新增build
命令
package.json
{
"private": true,
"workspaces": ["packages/*"],
"type": "module",
"scripts": {
"lint": "eslint --ext .ts,.jsx,.tsx --fix --quiet ./packages",
"build": "node ./scripts/rollup/build.js",
"upgrade": "yarn upgrade-interactive --latest"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@types/node": "^22.10.2",
"eslint": "^9.17.0",
"eslint-plugin-import-x": "^4.5.0",
"globals": "^15.13.0",
"mkdirp": "^3.0.1",
"ncp": "^2.0.0",
"prettier": "^3.4.2",
"rimraf": "^6.0.1",
"rollup": "^4.28.1",
"rollup-plugin-typescript2": "^0.36.0",
"typescript": "^5.6.3",
"typescript-eslint": "^8.18.0"
},
"packageManager": "yarn@1.22.22"
}
安装构建包
yarn add mkdirp rimraf ncp rollup-plugin-typescript2 -D -W
目录结构
scripts/rollup
└── build.js
└── bundles.js
└── modules.js
└── packaging.js
└── utils.js
编写脚本
- bundles.js
- modules.js
- packaging.js
- utils.js
- build.js
scripts/rollup/bundles.js
const bundleTypes = {
NODE_DEV: "NODE_DEV",
NODE_PROD: "NODE_PROD",
};
const { NODE_DEV, NODE_PROD } = bundleTypes;
const bundles = [
/******* Isomorphic *******/
{
bundleTypes: [NODE_DEV, NODE_PROD],
entry: "react",
global: "React",
},
/******* React JSX Runtime *******/
{
bundleTypes: [NODE_DEV, NODE_PROD],
entry: "react/jsx-runtime",
global: "JSXRuntime",
externals: ["react"],
},
/******* React JSX DEV Runtime *******/
{
bundleTypes: [NODE_DEV, NODE_PROD],
entry: "react/jsx-dev-runtime",
global: "JSXDEVRuntime",
externals: ["react"],
},
];
export function getFilename(bundle, bundleType) {
let name = bundle.entry;
// we do this to replace / to -, for react-dom/server
name = name.replace("/index.", ".").replace("/", "-");
switch (bundleType) {
case NODE_DEV:
return `${name}.development.js`;
case NODE_PROD:
return `${name}.production.js`;
}
}
export default {
bundles,
bundleTypes,
};
scripts/rollup/modules.js
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
// For any external that is used in a DEV-only condition, explicitly
// specify whether it has side effects during import or not. This lets
// us know whether we can safely omit them when they are unused.
const HAS_NO_SIDE_EFFECTS_ON_IMPORT = false;
// const HAS_SIDE_EFFECTS_ON_IMPORT = true;
const importSideEffects = Object.freeze({
react: HAS_NO_SIDE_EFFECTS_ON_IMPORT,
"react/jsx-dev-runtime": HAS_NO_SIDE_EFFECTS_ON_IMPORT,
});
// Bundles exporting globals that other modules rely on.
const knownGlobals = Object.freeze({
react: "React",
});
// Determines node_modules packages that are safe to assume will exist.
export function getDependencies(bundleType, entry) {
// Replaces any part of the entry that follow the package name (like
// "/server" in "react-dom/server") by the path to the package settings
const packageJson = require(entry.replace(/(\/.*)?$/, "/package.json"));
// Both deps and peerDeps are assumed as accessible.
return Array.from(
new Set([
...Object.keys(packageJson.dependencies || {}),
...Object.keys(packageJson.peerDependencies || {}),
])
);
}
export function getPeerGlobals(externals) {
const peerGlobals = {};
if (Array.isArray(externals)) {
externals.forEach((name) => {
peerGlobals[name] = knownGlobals[name];
});
}
return peerGlobals;
}
export function getImportSideEffects() {
return importSideEffects;
}
scripts/rollup/packaging.js
import { existsSync, readdirSync } from "node:fs";
import Bundles from "./bundles.js";
import { asyncCopyTo } from "./utils.js";
const { NODE_DEV, NODE_PROD } = Bundles.bundleTypes;
export function getPackageName(name) {
if (name.indexOf("/") !== -1) {
return name.split("/")[0];
}
return name;
}
export function getBundleOutputPath(bundleType, filename, packageName) {
switch (bundleType) {
case NODE_DEV:
case NODE_PROD:
return `build/node_modules/${packageName}/cjs/${filename}`;
}
}
async function prepareNpmPackage(name) {
await Promise.all([
asyncCopyTo("LICENSE", `build/node_modules/${name}/LICENSE`),
asyncCopyTo(
`packages/${name}/package.json`,
`build/node_modules/${name}/package.json`
),
asyncCopyTo(
`packages/${name}/README.md`,
`build/node_modules/${name}/README.md`
),
asyncCopyTo(`packages/${name}/npm`, `build/node_modules/${name}`),
]);
}
export async function prepareNpmPackages() {
if (!existsSync("build/node_modules")) {
// We didn't build any npm packages.
return;
}
const builtPackageFolders = readdirSync("build/node_modules").filter(
(dir) => dir.charAt(0) !== "."
);
await Promise.all(builtPackageFolders.map(prepareNpmPackage));
}
scripts/rollup/utils.js
import path from "node:path";
import { mkdirp } from "mkdirp";
import NCP from "ncp";
const ncp = NCP.ncp;
export function asyncCopyTo(from, to) {
return mkdirp(path.dirname(to)).then(
() =>
new Promise((resolve, reject) => {
ncp(from, to, (error) => {
if (error) {
// Wrap to have a useful stack trace.
reject(new Error(error));
} else {
// Wait for copied files to exist; ncp() sometimes completes prematurely.
// For more detail, see github.com/facebook/react/issues/22323
// Also github.com/AvianFlu/ncp/issues/127
setTimeout(resolve, 10);
}
});
})
);
}
scripts/rollup/build.js
"use strict";
import { createRequire } from "node:module";
import { rimrafSync } from "rimraf";
import { rollup } from "rollup";
import typescript from "rollup-plugin-typescript2";
import {
getBundleOutputPath,
getPackageName,
prepareNpmPackages,
} from "./packaging.js";
import {
getDependencies,
getImportSideEffects,
getPeerGlobals,
} from "./modules.js";
import Bundles, { getFilename } from "./bundles.js";
const { NODE_DEV, NODE_PROD } = Bundles.bundleTypes;
const require = createRequire(import.meta.url);
function getFormat(bundleType) {
switch (bundleType) {
case NODE_DEV:
case NODE_PROD:
return `cjs`;
}
}
function getPlugins() {
return [typescript()];
}
const getRollupInteropValue = () => {
return "esModule";
};
function shouldSkipBundle(bundle, bundleType) {
const shouldSkipBundleType = bundle.bundleTypes.indexOf(bundleType) === -1;
if (shouldSkipBundleType) {
return true;
}
return false;
}
function resolveEntryFork(resolvedEntry) {
return resolvedEntry;
}
function isProductionBundleType(bundleType) {
switch (bundleType) {
case NODE_DEV:
return false;
case NODE_PROD:
return true;
default:
throw new Error(`Unknown type: ${bundleType}`);
}
}
async function createBundle(bundle, bundleType) {
const filename = getFilename(bundle, bundleType);
const format = getFormat(bundleType);
const packageName = getPackageName(bundle.entry);
let resolvedEntry = resolveEntryFork(require.resolve(bundle.entry));
const peerGlobals = getPeerGlobals(bundle.externals, bundleType);
let externals = Object.keys(peerGlobals);
const deps = getDependencies(bundleType, bundle.entry);
externals = externals.concat(deps);
const importSideEffects = getImportSideEffects();
const pureExternalModules = Object.keys(importSideEffects).filter(
(module) => !importSideEffects[module]
);
/**
* @type {import("rollup").RollupOptions}
*/
const rollupConfig = {
input: resolvedEntry,
treeshake: {
treeshake: {
moduleSideEffects: (id, external) =>
!(external && pureExternalModules.includes(id)),
propertyReadSideEffects: false,
},
},
external(id) {
const containsThisModule = (pkg) =>
id === pkg || id.startsWith(pkg + "/");
const isProvidedByDependency = externals.some(containsThisModule);
if (isProvidedByDependency) {
if (id.indexOf("/src/") !== -1) {
throw Error(
"You are trying to import " +
id +
" but " +
externals.find(containsThisModule) +
" is one of npm dependencies, " +
"so it will not contain that source file. You probably want " +
"to create a new bundle entry point for it instead."
);
}
return true;
}
return !!peerGlobals[id];
},
plugins: getPlugins(),
onwarn: handleRollupWarning,
output: {
externalLiveBindings: false,
freeze: false,
interop: getRollupInteropValue,
esModule: false,
},
};
const mainOutputPath = getBundleOutputPath(bundleType, filename, packageName);
const rollupOutputOptions = getRollupOutputOptions(
mainOutputPath,
format,
peerGlobals,
bundle.global,
bundleType
);
try {
const result = await rollup(rollupConfig);
await result.write(rollupOutputOptions);
} catch (error) {
handleRollupError(error);
}
}
function getRollupOutputOptions(
outputPath,
format,
globals,
globalName,
bundleType
) {
const isProduction = isProductionBundleType(bundleType);
/**
* @return {import("rollup").RollupOutput}
*/
return {
file: outputPath,
format,
globals,
freeze: !isProduction,
interop: getRollupInteropValue,
name: globalName,
sourcemap: false,
esModule: false,
exports: "auto",
};
}
async function buildEverything() {
rimrafSync("build");
let bundles = [];
for (const bundle of Bundles.bundles) {
bundles.push([bundle, NODE_DEV], [bundle, NODE_PROD]);
}
bundles = bundles.filter(([bundle, bundleType]) => {
return !shouldSkipBundle(bundle, bundleType);
});
for (const [bundle, bundleType] of bundles) {
await createBundle(bundle, bundleType);
}
await prepareNpmPackages();
}
function handleRollupWarning(warning) {
if (warning.code === "UNUSED_EXTERNAL_IMPORT") {
const match = warning.message.match(/external module '([^']+)'/);
if (!match || typeof match[1] !== "string") {
throw new Error(
"Could not parse a Rollup warning. " + "Fix this method."
);
}
const importSideEffects = getImportSideEffects();
const externalModule = match[1];
if (typeof importSideEffects[externalModule] !== "boolean") {
throw new Error(
'An external module "' +
externalModule +
'" is used in a DEV-only code path ' +
"but we do not know if it is safe to omit an unused require() to it in production. " +
"Please add it to the `importSideEffects` list in `scripts/rollup/modules.js`."
);
}
// Don't warn. We will remove side effectless require() in a later pass.
return;
}
if (warning.code === "CIRCULAR_DEPENDENCY") {
// Ignored
} else if (typeof warning.code === "string") {
// This is a warning coming from Rollup itself.
// These tend to be important (e.g. clashes in namespaced exports)
// so we'll fail the build on any of them.
console.error();
console.error(warning.message || warning);
console.error();
process.exit(1);
} else {
// The warning is from one of the plugins.
// Maybe it's not important, so just print it.
console.warn(warning.message || warning);
}
}
function handleRollupError(error) {
if (!error.code) {
console.error(error);
return;
}
console.error(
`\x1b[31m-- ${error.code}${error.plugin ? ` (${error.plugin})` : ""} --`
);
console.error(error.stack);
if (error.codeFrame) {
// This looks like an error from a plugin (e.g. Babel).
// In this case we'll resort to displaying the provided code frame
// because we can't be sure the reported location is accurate.
console.error(error.codeFrame);
}
}
buildEverything();
输出目录结构
build/node_modules
└── react
└── cjs
└── react-jsx-dev-runtime.development.js
└── react-jsx-dev-runtime.production.js
└── react-jsx-runtime.development.js
└── react-jsx-runtime.production.js
└─ ─ react.development.js
└── react.production.js
└── index.js
└── js-dev-runtime.js
└── js-runtime.js
└── LICENSE
└── package.json
└── README.md
开发调试
使用create-react-app
创建一个新项目。
- pnpm
pnpm link react/build/node_modules/react
// 解除链接后,pnpm 不会自动删除实际的依赖包或目录,只是移除了符号链接,恢复正常的包管理方式。
// 如果需要完全清除包的依赖,可以使用 pnpm install 或者手动删除相关的依赖。
pnpm unlink react/build/node_modules/react