Skip to content

Commit ffd5899

Browse files
committed
Try new build
1 parent 869968b commit ffd5899

File tree

4 files changed

+311
-13
lines changed

4 files changed

+311
-13
lines changed

CONTRIBUTING.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,9 @@ The short summary is:
9797

9898
Scripts can be executed via `npm run [script]` or `yarn [script]` respectively.
9999

100-
- `build` - compiles all packages ready for publishing to npm
101-
- `build:core` - builds just Preact itself
102-
- `build:debug` - builds the debug addon only
103-
- `build:hooks` - builds the hook addon only
104-
- `build:test-utils` - builds the test-utils addon only
100+
- `build` - compiles all packages (core + addons) via unified script (`scripts/build-packages.cjs`)
101+
- `build:core` - builds just Preact itself (alias for `node scripts/build-packages.cjs core`)
102+
- (Deprecated) Previous per-package commands like `build:hooks`, `build:debug`, etc. now proxy to the unified build and will be removed in a future major.
105103
- `test:ts` - Run all tests for TypeScript definitions
106104
- `test:karma` - Run all unit/integration tests.
107105
- `test:karma:watch` - Same as above, but it will automatically re-run the test suite if a code change was detected.

package.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -114,14 +114,14 @@
114114
"types": "src/index.d.ts",
115115
"scripts": {
116116
"prepare": "husky && npm run test:install && run-s build",
117-
"build": "npm-run-all --parallel 'build:*'",
118-
"build:core": "microbundle build --raw --no-generateTypes -f cjs,esm,umd",
119-
"build:debug": "microbundle build --raw --no-generateTypes -f cjs,esm,umd --cwd debug",
120-
"build:devtools": "microbundle build --raw --no-generateTypes -f cjs,esm,umd --cwd devtools",
121-
"build:hooks": "microbundle build --raw --no-generateTypes -f cjs,esm,umd --cwd hooks",
122-
"build:test-utils": "microbundle build --raw --no-generateTypes -f cjs,esm,umd --cwd test-utils",
123-
"build:compat": "microbundle build --raw --no-generateTypes -f cjs,esm,umd --cwd compat --globals 'preact/hooks=preactHooks'",
124-
"build:jsx": "microbundle build --raw --no-generateTypes -f cjs,esm,umd --cwd jsx-runtime",
117+
"build": "node scripts/build-packages.cjs all",
118+
"build:core:legacy": "microbundle build --raw --no-generateTypes -f cjs,esm,umd",
119+
"build:core": "node scripts/build-packages.cjs core",
120+
"build:devtools": "echo 'Use build script: npm run build (devtools included)'",
121+
"build:hooks": "echo 'Use build script: npm run build (hooks included)'",
122+
"build:test-utils": "echo 'Use build script: npm run build (test-utils included)'",
123+
"build:compat": "echo 'Use build script: npm run build (compat included)'",
124+
"build:jsx": "echo 'Use build script: npm run build (jsx-runtime included)'",
125125
"postbuild": "node ./config/compat-entries.js",
126126
"dev": "microbundle watch --raw --no-generateTypes --format cjs",
127127
"dev:hooks": "microbundle watch --raw --no-generateTypes --format cjs --cwd hooks",

scripts/build-packages.cjs

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Generalized build script replacing former microbundle-based per-package builds.
4+
*
5+
* Usage:
6+
* npm run build # build all packages
7+
* node scripts/build-packages.cjs core hooks # build subset
8+
*
9+
* Pipeline per package:
10+
* esbuild (ESM) -> manual CJS transform -> esbuild IIFE (UMD) -> terser minification (in-place)
11+
* Property rename (internal) via babel-plugin-transform-rename-properties using root mangle.json
12+
*/
13+
const path = require('node:path');
14+
const fs = require('node:fs/promises');
15+
const { build } = require('esbuild');
16+
const babel = require('@babel/core');
17+
const { minify } = require('terser');
18+
const zlib = require('node:zlib');
19+
20+
async function main() {
21+
const root = path.join(__dirname, '..');
22+
const pkgRoot = JSON.parse(
23+
String(await fs.readFile(path.join(root, 'package.json')))
24+
);
25+
const mangleConfig = JSON.parse(
26+
String(await fs.readFile(path.join(root, 'mangle.json')))
27+
);
28+
29+
const packages = [
30+
{
31+
id: 'core',
32+
dir: '.',
33+
entry: 'src/index.js',
34+
base: 'preact',
35+
globalName: pkgRoot.amdName || 'preact'
36+
},
37+
{
38+
id: 'hooks',
39+
dir: 'hooks',
40+
entry: 'src/index.js',
41+
base: 'hooks',
42+
globalName: 'preactHooks'
43+
},
44+
{
45+
id: 'compat',
46+
dir: 'compat',
47+
entry: 'src/index.js',
48+
base: 'compat',
49+
globalName: 'preactCompat'
50+
},
51+
{
52+
id: 'debug',
53+
dir: 'debug',
54+
entry: 'src/index.js',
55+
base: 'debug',
56+
globalName: 'preactDebug'
57+
},
58+
{
59+
id: 'devtools',
60+
dir: 'devtools',
61+
entry: 'src/index.js',
62+
base: 'devtools',
63+
globalName: 'preactDevtools'
64+
},
65+
{
66+
id: 'test-utils',
67+
dir: 'test-utils',
68+
entry: 'src/index.js',
69+
base: 'testUtils',
70+
globalName: 'preactTestUtils'
71+
},
72+
{
73+
id: 'jsx-runtime',
74+
dir: 'jsx-runtime',
75+
entry: 'src/index.js',
76+
base: 'jsxRuntime',
77+
globalName: 'preactJsxRuntime'
78+
}
79+
];
80+
81+
const args = process.argv.slice(2).filter(Boolean);
82+
let selected = packages;
83+
if (args.length && !args.includes('all')) {
84+
const wanted = new Set(args);
85+
selected = packages.filter(p => wanted.has(p.id));
86+
const missing = Array.from(wanted).filter(
87+
x => !selected.some(p => p.id === x)
88+
);
89+
if (missing.length) {
90+
console.error('[build] Unknown package id(s):', missing.join(', '));
91+
process.exit(1);
92+
}
93+
}
94+
95+
const rename = {};
96+
for (const original in mangleConfig.props.props) {
97+
let name = original;
98+
if (name.startsWith('$')) name = name.slice(1);
99+
rename[name] = mangleConfig.props.props[original];
100+
}
101+
102+
const babelCache = new Map();
103+
function babelRenamePlugin() {
104+
return {
105+
name: 'babel-rename-properties',
106+
setup(buildApi) {
107+
buildApi.onLoad({ filter: /\/src\/.*\.js$/ }, async args => {
108+
const code = String(await fs.readFile(args.path));
109+
const cacheKey = args.path + '::' + code;
110+
const cached = babelCache.get(cacheKey);
111+
if (cached) return cached;
112+
const result = await babel.transformAsync(code, {
113+
filename: args.path,
114+
babelrc: false,
115+
configFile: false,
116+
presets: [],
117+
plugins: [['babel-plugin-transform-rename-properties', { rename }]]
118+
});
119+
const out = { contents: result.code, loader: 'js' };
120+
babelCache.set(cacheKey, out);
121+
return out;
122+
});
123+
}
124+
};
125+
}
126+
127+
const reserved = mangleConfig.minify.mangle.properties.reserved;
128+
Object.values(mangleConfig.props.props).forEach(n => reserved.push(n));
129+
const terserBase = {
130+
toplevel: true,
131+
compress: {
132+
...mangleConfig.minify.compress,
133+
unsafe: true,
134+
pure_getters: true,
135+
keep_infinity: true,
136+
unsafe_proto: true,
137+
// Fewer passes tends to slightly favor gzip size vs many inlining passes:
138+
passes: 3,
139+
toplevel: true
140+
},
141+
mangle: {
142+
toplevel: true,
143+
properties: { ...mangleConfig.minify.mangle.properties, reserved }
144+
},
145+
format: {
146+
shebang: true,
147+
shorthand: true,
148+
wrap_func_args: false,
149+
comments: /^\s*([@#]__[A-Z]+__\s*$|@cc_on)/,
150+
preserve_annotations: true
151+
},
152+
module: true,
153+
sourceMap: true
154+
};
155+
156+
async function minifyFile(inputPath, outputPath, { module }) {
157+
const code = String(await fs.readFile(inputPath));
158+
const result = await minify(code, {
159+
...terserBase,
160+
module,
161+
sourceMap: {
162+
filename: path.basename(outputPath),
163+
url: path.basename(outputPath) + '.map'
164+
}
165+
});
166+
await fs.writeFile(outputPath, result.code + '\n');
167+
if (result.map)
168+
await fs.writeFile(
169+
outputPath + '.map',
170+
typeof result.map === 'string' ? result.map : JSON.stringify(result.map)
171+
);
172+
}
173+
174+
const sizeRows = [];
175+
176+
for (const pkg of selected) {
177+
const absDir = path.join(root, pkg.dir);
178+
const distDir = path.join(absDir, 'dist');
179+
const entryAbs = path.join(absDir, pkg.entry);
180+
await fs.mkdir(distDir, { recursive: true });
181+
182+
const shared = {
183+
entryPoints: [entryAbs],
184+
external: [
185+
'preact',
186+
'preact/jsx-runtime',
187+
'preact/compat',
188+
'preact/hooks',
189+
'preact/debug',
190+
'preact/test-utils',
191+
'preact/devtools',
192+
'preact-render-to-string'
193+
],
194+
bundle: true,
195+
sourcemap: true,
196+
sourcesContent: true,
197+
plugins: [babelRenamePlugin()],
198+
target: ['es2020'],
199+
define: { 'process.env.NODE_ENV': '"production"' }
200+
};
201+
202+
await build({
203+
...shared,
204+
format: 'esm',
205+
outfile: path.join(distDir, pkg.base + '.mjs'),
206+
minify: false
207+
});
208+
209+
let cjsCode = String(await fs.readFile(path.join(distDir, pkg.base + '.js')));
210+
cjsCode = cjsCode
211+
.replace(/export function (\w+)/g, 'function $1')
212+
.replace(/export const (\w+)/g, 'const $1')
213+
.replace(/export class (\w+)/g, 'class $1');
214+
const exportLines = [];
215+
const exported = new Set();
216+
cjsCode = cjsCode.replace(/export \{([^}]+)\};?/g, (m, names) => {
217+
const parts = names.split(',');
218+
for (let raw of parts) {
219+
const s = raw.trim();
220+
if (!s) continue;
221+
let local = s;
222+
let alias = s;
223+
if (s.includes(' as ')) {
224+
[local, alias] = s.split(/\s+as\s+/);
225+
}
226+
if (!exported.has(alias)) {
227+
exportLines.push(`exports.${alias} = ${local};`);
228+
exported.add(alias);
229+
}
230+
}
231+
return '';
232+
});
233+
cjsCode = cjsCode.replace(/export default ([^;]+);?/g, (m, expr) => {
234+
if (!exported.has('default')) {
235+
exported.add('default');
236+
return `module.exports = ${expr};`;
237+
}
238+
return '';
239+
});
240+
if (exportLines.length) cjsCode += '\n' + exportLines.join('\n') + '\n';
241+
await fs.writeFile(path.join(distDir, pkg.base + '.js'), cjsCode);
242+
243+
await build({
244+
...shared,
245+
format: 'iife',
246+
globalName: pkg.globalName,
247+
outfile: path.join(distDir, pkg.base + '.umd.js'),
248+
minify: false
249+
});
250+
251+
await Promise.all([
252+
minifyFile(
253+
path.join(distDir, pkg.base + '.js'),
254+
path.join(distDir, pkg.base + '.js'),
255+
{ module: true }
256+
),
257+
minifyFile(
258+
path.join(distDir, pkg.base + '.mjs'),
259+
path.join(distDir, pkg.base + '.mjs'),
260+
{ module: true }
261+
),
262+
minifyFile(
263+
path.join(distDir, pkg.base + '.umd.js'),
264+
path.join(distDir, pkg.base + '.umd.js'),
265+
{ module: false }
266+
)
267+
]);
268+
269+
for (const ext of ['.js', '.mjs', '.umd.js']) {
270+
const file = pkg.base + ext;
271+
const abs = path.join(distDir, file);
272+
const code = await fs.readFile(abs);
273+
const raw = code.length;
274+
const gz = zlib.gzipSync(code).length;
275+
const br = zlib.brotliCompressSync(code, {
276+
params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 11 }
277+
}).length;
278+
sizeRows.push({
279+
pkg: pkg.id,
280+
file: path.relative(root, abs),
281+
raw,
282+
gz,
283+
br
284+
});
285+
}
286+
}
287+
288+
console.log('\n[build] Artifact sizes (bytes):');
289+
console.log(['Package', 'File', 'Raw', 'Gzip', 'Brotli'].join('\t'));
290+
for (const row of sizeRows)
291+
console.log([row.pkg, row.file, row.raw, row.gz, row.br].join('\t'));
292+
console.log('\nDone.');
293+
}
294+
295+
main().catch(err => {
296+
console.error('[build] Failed:', err);
297+
process.exit(1);
298+
});

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ export { cloneElement } from './clone-element';
1111
export { createContext } from './create-context';
1212
export { toChildArray } from './diff/children';
1313
export { default as options } from './options';
14+
15+
// test

0 commit comments

Comments
 (0)