mirror of
https://github.com/preactjs/preact.git
synced 2026-10-28 10:44:53 +08:00
251 lines
6.6 KiB
JavaScript
251 lines
6.6 KiB
JavaScript
/**
|
|
* Build: Rollup → Babel (per module, downlevel + mangle.json property
|
|
* renames) → Terser (minify + property mangle name cache).
|
|
*
|
|
* node ./scripts/build.mjs build everything
|
|
* node ./scripts/build.mjs --watch [pkg...]
|
|
*/
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import {
|
|
gzipSync,
|
|
brotliCompressSync,
|
|
constants as zlibConstants
|
|
} from 'node:zlib';
|
|
import { rollup } from 'rollup';
|
|
import { babel } from '@rollup/plugin-babel';
|
|
import { minify } from 'terser';
|
|
import fastRest from './babel-plugin-fast-rest.mjs';
|
|
|
|
const ROOT = path.resolve(fileURLToPath(import.meta.url), '../..');
|
|
|
|
const PACKAGES = [
|
|
{ cwd: '.' },
|
|
{ cwd: 'compat' },
|
|
{ cwd: 'debug' },
|
|
{ cwd: 'devtools' },
|
|
{ cwd: 'hooks' },
|
|
{ cwd: 'jsx-runtime' },
|
|
{ cwd: 'test-utils' }
|
|
];
|
|
|
|
const BROWSER_TARGETS = [
|
|
'chrome >= 40',
|
|
'safari >= 9',
|
|
'firefox >= 36',
|
|
'edge >= 12',
|
|
'not dead'
|
|
];
|
|
|
|
function readJson(file) {
|
|
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
}
|
|
|
|
function renameMap() {
|
|
const rename = {};
|
|
const mangle = readJson(path.resolve(ROOT, 'mangle.json'));
|
|
for (const prop in mangle.props.props) {
|
|
const name = prop[0] === '$' ? prop.slice(1) : prop;
|
|
rename[name] = mangle.props.props[prop];
|
|
}
|
|
return rename;
|
|
}
|
|
|
|
/** The package's mangle.json doubles as terser's mangle name cache; packages
|
|
* without one (test-utils) use their package.json `mangle` field, uncached. */
|
|
function loadMangle(cwd) {
|
|
const file = path.resolve(cwd, 'mangle.json');
|
|
let nameCache = null;
|
|
let options;
|
|
if (fs.existsSync(file)) {
|
|
nameCache = readJson(file);
|
|
options = nameCache.minify || {};
|
|
} else {
|
|
const meta = readJson(path.resolve(cwd, 'package.json'));
|
|
options = { mangle: { properties: meta.minify || meta.mangle || {} } };
|
|
}
|
|
const properties = (options.mangle && options.mangle.properties) || {};
|
|
return {
|
|
file,
|
|
nameCache,
|
|
knownProps: nameCache ? { ...(nameCache.props || {}).props } : null,
|
|
compress: options.compress || {},
|
|
properties: {
|
|
...properties,
|
|
regex: properties.regex && new RegExp(properties.regex),
|
|
reserved: properties.reserved || []
|
|
}
|
|
};
|
|
}
|
|
|
|
function minifyPlugin(pkg) {
|
|
return {
|
|
name: 'minify',
|
|
async renderChunk(code) {
|
|
const mangle = loadMangle(pkg.cwd);
|
|
|
|
const result = await minify(code, {
|
|
compress: {
|
|
keep_infinity: true,
|
|
pure_getters: true,
|
|
passes: 10,
|
|
...mangle.compress
|
|
},
|
|
format: {
|
|
wrap_func_args: false,
|
|
comments: /^\s*([@#]__[A-Z]+__\s*$|@cc_on)/,
|
|
preserve_annotations: true
|
|
},
|
|
module: false,
|
|
ecma: 5,
|
|
toplevel: true,
|
|
mangle: { properties: mangle.properties },
|
|
nameCache: mangle.nameCache,
|
|
sourceMap: true
|
|
});
|
|
|
|
// unmapped private properties get a bundle-local name; pin them in
|
|
// mangle.json when another package needs to access them
|
|
if (mangle.nameCache) {
|
|
const after = (mangle.nameCache.props || {}).props || {};
|
|
const fresh = Object.keys(after).filter(k => !(k in mangle.knownProps));
|
|
if (fresh.length) {
|
|
console.warn(
|
|
`WARN [${pkg.name}]: properties mangled without a ${path.relative(ROOT, mangle.file)} entry: ` +
|
|
fresh.map(k => k.slice(1)).join(', ')
|
|
);
|
|
}
|
|
}
|
|
|
|
return { code: result.code, map: result.map };
|
|
}
|
|
};
|
|
}
|
|
|
|
function outputOptions(pkg, file) {
|
|
return {
|
|
format: 'es',
|
|
file,
|
|
sourcemap: true,
|
|
strict: false,
|
|
freeze: false,
|
|
plugins: [minifyPlugin(pkg)]
|
|
};
|
|
}
|
|
|
|
function createBundle(pkg) {
|
|
return rollup({
|
|
input: path.resolve(pkg.cwd, pkg.meta.source),
|
|
external: pkg.cwd === ROOT ? () => false : id => /^preact($|\/)/.test(id),
|
|
treeshake: { propertyReadSideEffects: false },
|
|
onwarn(warning, warn) {
|
|
if (warning.code === 'CIRCULAR_DEPENDENCY') return;
|
|
if (warning.code === 'MIXED_EXPORTS') return;
|
|
warn(warning);
|
|
},
|
|
plugins: [
|
|
babel({
|
|
babelHelpers: 'bundled',
|
|
extensions: ['.js', '.jsx', '.mjs'],
|
|
exclude: /\/node_modules\//,
|
|
babelrc: false,
|
|
configFile: false,
|
|
presets: [
|
|
[
|
|
'@babel/preset-env',
|
|
{
|
|
loose: true,
|
|
modules: false,
|
|
useBuiltIns: false,
|
|
targets: { browsers: BROWSER_TARGETS },
|
|
exclude: ['@babel/plugin-transform-typeof-symbol']
|
|
}
|
|
]
|
|
],
|
|
plugins: [
|
|
['babel-plugin-transform-rename-properties', { rename: renameMap() }],
|
|
[fastRest, { helper: false, literal: true }]
|
|
]
|
|
})
|
|
]
|
|
});
|
|
}
|
|
|
|
async function buildPackage(pkg) {
|
|
const bundle = await createBundle(pkg);
|
|
const file = path.resolve(pkg.cwd, pkg.meta.module);
|
|
const { output } = await bundle.write(outputOptions(pkg, file));
|
|
await bundle.close();
|
|
|
|
return { name: pkg.name, written: [{ file, code: output[0].code }] };
|
|
}
|
|
|
|
function printSizes(results) {
|
|
for (const { name, written } of results) {
|
|
console.log(
|
|
`Build "${name}" to ${path.relative(ROOT, path.dirname(written[0].file))}:`
|
|
);
|
|
for (const { file, code } of written) {
|
|
const buf = Buffer.from(
|
|
code.replace(/\n?\/\/# sourceMappingURL=.*\s*$/, ''),
|
|
'utf8'
|
|
);
|
|
const gz = gzipSync(buf, { level: 9 }).length;
|
|
const br = brotliCompressSync(buf, {
|
|
params: { [zlibConstants.BROTLI_PARAM_QUALITY]: 11 }
|
|
}).length;
|
|
console.log(`${String(gz).padStart(11)} B: ${path.basename(file)}.gz`);
|
|
console.log(`${String(br).padStart(11)} B: ${path.basename(file)}.br`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function resolvePackages(names) {
|
|
const packages = PACKAGES.map(pkg => {
|
|
const cwd = path.resolve(ROOT, pkg.cwd);
|
|
const meta = readJson(path.resolve(cwd, 'package.json'));
|
|
return { ...pkg, cwd, meta, name: meta.name };
|
|
});
|
|
if (!names.length) return packages;
|
|
return packages.filter(
|
|
pkg =>
|
|
names.includes(pkg.name) ||
|
|
names.includes(path.relative(ROOT, pkg.cwd) || '.')
|
|
);
|
|
}
|
|
|
|
async function watchMode(packages) {
|
|
for (const pkg of packages) {
|
|
const rebuild = async () => {
|
|
const start = Date.now();
|
|
try {
|
|
const result = await buildPackage(pkg);
|
|
console.log(`${pkg.name} rebuilt in ${Date.now() - start}ms`);
|
|
printSizes([result]);
|
|
} catch (e) {
|
|
console.error(`${pkg.name} failed: ${e.message}`);
|
|
}
|
|
};
|
|
await rebuild();
|
|
fs.watch(path.resolve(pkg.cwd, 'src'), { recursive: true }, () => {
|
|
clearTimeout(pkg._debounce);
|
|
pkg._debounce = setTimeout(rebuild, 50);
|
|
});
|
|
}
|
|
console.log('Watching for changes...');
|
|
}
|
|
|
|
const args = process.argv.slice(2);
|
|
const isWatch = args.includes('--watch');
|
|
const packages = resolvePackages(args.filter(a => a !== '--watch'));
|
|
|
|
if (isWatch) {
|
|
await watchMode(packages);
|
|
} else {
|
|
const start = Date.now();
|
|
const results = await Promise.all(packages.map(buildPackage));
|
|
printSizes(results);
|
|
console.log(`Built ${packages.length} packages in ${Date.now() - start}ms`);
|
|
}
|