Skip to content

Commit e03fd11

Browse files
committed
Simplify build process
1 parent 9b78b44 commit e03fd11

3 files changed

Lines changed: 288 additions & 1 deletion

File tree

build.mjs

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
#!/usr/bin/env node
2+
3+
import * as esbuild from 'esbuild';
4+
import { transformAsync } from '@babel/core';
5+
import { minify } from 'terser';
6+
import fs from 'node:fs/promises';
7+
import { readFileSync, existsSync } from 'node:fs';
8+
import path from 'node:path';
9+
10+
// Load mangle configuration
11+
const mangleConfig = JSON.parse(readFileSync('./mangle.json', 'utf8'));
12+
13+
// Create rename mapping for Babel plugin
14+
const rename = {};
15+
for (let prop in mangleConfig.props.props) {
16+
let name = prop;
17+
if (name[0] === '$') {
18+
name = name.slice(1);
19+
}
20+
rename[name] = mangleConfig.props.props[prop];
21+
}
22+
23+
// Build configurations for different packages
24+
const buildConfigs = [
25+
{
26+
name: 'preact',
27+
entry: 'src/index.js',
28+
outDir: 'dist',
29+
filename: 'preact',
30+
globalName: 'preact'
31+
},
32+
{
33+
name: 'preact/debug',
34+
entry: 'debug/src/index.js',
35+
outDir: 'debug/dist',
36+
external: ['preact'],
37+
filename: 'debug',
38+
globalName: 'preactDebug'
39+
},
40+
{
41+
name: 'preact/devtools',
42+
entry: 'devtools/src/index.js',
43+
outDir: 'devtools/dist',
44+
external: ['preact'],
45+
filename: 'devtools',
46+
globalName: 'preactDevtools'
47+
},
48+
{
49+
name: 'preact/hooks',
50+
entry: 'hooks/src/index.js',
51+
outDir: 'hooks/dist',
52+
external: ['preact'],
53+
filename: 'hooks',
54+
globalName: 'preactHooks'
55+
},
56+
{
57+
name: 'preact/test-utils',
58+
entry: 'test-utils/src/index.js',
59+
external: ['preact'],
60+
outDir: 'test-utils/dist',
61+
filename: 'testUtils',
62+
globalName: 'preactTestUtils'
63+
},
64+
{
65+
name: 'preact/compat',
66+
entry: 'compat/src/index.js',
67+
outDir: 'compat/dist',
68+
filename: 'compat',
69+
globalName: 'preactCompat',
70+
external: ['preact/hooks', 'preact'],
71+
globals: { 'preact/hooks': 'preactHooks' }
72+
},
73+
{
74+
name: 'preact/jsx-runtime',
75+
entry: 'jsx-runtime/src/index.js',
76+
outDir: 'jsx-runtime/dist',
77+
filename: 'jsxRuntime',
78+
external: ['preact'],
79+
globalName: 'preactJsxRuntime'
80+
}
81+
];
82+
83+
// Formats to build for each package
84+
const formats = ['cjs', 'esm', 'umd'];
85+
86+
// Function to apply Babel transformations
87+
async function applyBabelTransform(code, filename) {
88+
try {
89+
const result = await transformAsync(code, {
90+
filename,
91+
configFile: false,
92+
babelrc: false,
93+
presets: [],
94+
plugins: [['babel-plugin-transform-rename-properties', { rename }]]
95+
});
96+
return result.code;
97+
} catch (error) {
98+
console.error(`Babel transform failed for ${filename}:`, error);
99+
throw error;
100+
}
101+
}
102+
103+
// Function to minify with Terser
104+
async function minifyWithTerser(code, filename) {
105+
try {
106+
const result = await minify(code, {
107+
...mangleConfig.minify,
108+
mangle: {
109+
properties: {
110+
regex: mangleConfig.props.regex,
111+
keep_quoted: mangleConfig.props.keep_quoted,
112+
reserved: Object.values(rename)
113+
}
114+
},
115+
compress: {
116+
keep_infinity: true,
117+
pure_getters: true,
118+
unsafe: true,
119+
unsafe_proto: true,
120+
passes: 10
121+
},
122+
format: {
123+
wrap_func_args: false,
124+
comments: false,
125+
shorthand: true,
126+
preserve_annotations: true
127+
},
128+
ecma: 2020,
129+
sourceMap: true,
130+
safari10: true,
131+
module: filename.endsWith('.mjs'),
132+
toplevel: true
133+
});
134+
return result;
135+
} catch (error) {
136+
console.error(`Terser minification failed for ${filename}:`, error);
137+
throw error;
138+
}
139+
}
140+
141+
// Function to build a single package in a specific format
142+
async function buildPackage(config, format) {
143+
const {
144+
name,
145+
entry,
146+
outDir,
147+
filename,
148+
globalName,
149+
external = [],
150+
globals = {}
151+
} = config;
152+
153+
if (!existsSync(entry)) {
154+
console.warn(`⚠️ Entry file ${entry} not found, skipping ${name}`);
155+
return;
156+
}
157+
158+
console.log(`📦 Building ${name} (${format})...`);
159+
160+
// Determine output extension and format
161+
const extensions = {
162+
cjs: '.js',
163+
esm: '.mjs',
164+
umd: '.umd.js'
165+
};
166+
167+
const outputFile = path.join(outDir, `${filename}${extensions[format]}`);
168+
const mapFile = path.join(outDir, `${filename}${extensions[format]}.map`);
169+
170+
// ESBuild configuration
171+
const esbuildConfig = /** @type {import('esbuild').BuildOptions} */ {
172+
entryPoints: [entry],
173+
bundle: true,
174+
format: format === 'cjs' ? 'cjs' : format === 'esm' ? 'esm' : 'iife',
175+
platform: 'browser',
176+
target: ['es2020'],
177+
jsx: 'transform',
178+
jsxFactory: 'h',
179+
jsxFragment: 'Fragment',
180+
jsxSideEffects: false,
181+
color: true,
182+
metafile: true,
183+
external,
184+
sourcemap: true,
185+
globalName: format === 'umd' ? globalName : undefined,
186+
plugins: [],
187+
write: false, // We'll handle writing ourselves
188+
minify: false, // We'll handle minification with Terser
189+
treeShaking: true
190+
};
191+
192+
// Add globals for UMD builds
193+
if (format === 'umd' && Object.keys(globals).length > 0) {
194+
esbuildConfig.globalName = globalName;
195+
// ESBuild doesn't support globals directly, we'll need to handle externals differently
196+
}
197+
198+
try {
199+
// Build with ESBuild
200+
// @ts-expect-error
201+
const result = await esbuild.build(esbuildConfig);
202+
let code = result.outputFiles[0].text;
203+
204+
// Apply Babel transformations
205+
code = await applyBabelTransform(code, outputFile);
206+
207+
// Ensure output directory exists
208+
await fs.mkdir(path.dirname(outputFile), { recursive: true });
209+
210+
// Write unminified version
211+
const minified = await minifyWithTerser(code, outputFile);
212+
if (typeof minified.map === 'string') {
213+
minified.code = `${minified.code}\n//# sourceMappingURL=${path.basename(mapFile)}`;
214+
await fs.writeFile(mapFile, minified.map);
215+
} else if (minified.map) {
216+
minified.code = `${minified.code}\n//# sourceMappingURL=${path.basename(mapFile)}`;
217+
minified.map = JSON.stringify(minified.map);
218+
await fs.writeFile(mapFile, minified.map);
219+
}
220+
await fs.writeFile(outputFile, minified.code);
221+
console.log(`✅ Built ${outputFile}`);
222+
223+
// Create minified version for UMD builds
224+
if (format === 'umd') {
225+
const minifiedCode = await minifyWithTerser(code, outputFile);
226+
const minifiedFile = path.join(outDir, `${filename}.min.js`);
227+
await fs.writeFile(minifiedFile, minifiedCode.code);
228+
console.log(`✅ Minified ${minifiedFile}`);
229+
}
230+
} catch (error) {
231+
console.error(`❌ Failed to build ${name} (${format}):`, error);
232+
throw error;
233+
}
234+
}
235+
236+
// Main build function
237+
async function build() {
238+
console.log('🚀 Starting Preact build with ESBuild + Babel + Terser...\n');
239+
240+
const startTime = Date.now();
241+
let successful = 0;
242+
let failed = 0;
243+
244+
const promises = [];
245+
// Build each package in each format
246+
for (const config of buildConfigs) {
247+
for (const format of formats) {
248+
promises.push(
249+
buildPackage(config, format)
250+
.then(() => {
251+
successful++;
252+
})
253+
.catch(error => {
254+
console.error(`❌ Build failed for ${config.name} (${format})`);
255+
failed++;
256+
})
257+
);
258+
}
259+
}
260+
261+
await Promise.all(promises);
262+
console.log(); // Empty line between packages
263+
264+
const endTime = Date.now();
265+
const duration = ((endTime - startTime) / 1000).toFixed(2);
266+
267+
console.log(`\n🎉 Build completed in ${duration}s`);
268+
console.log(`✅ Successful: ${successful}`);
269+
if (failed > 0) {
270+
console.log(`❌ Failed: ${failed}`);
271+
process.exit(1);
272+
}
273+
}
274+
275+
// Run the build if this script is executed directly
276+
// @ts-expect-error
277+
if (import.meta.url === `file://${process.argv[1]}`) {
278+
build().catch(error => {
279+
console.error('❌ Build failed:', error);
280+
process.exit(1);
281+
});
282+
}
283+
284+
export { build, buildPackage };

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,8 @@
121121
"types": "src/index.d.ts",
122122
"scripts": {
123123
"prepare": "husky && run-s build",
124-
"build": "npm-run-all --parallel 'build:*'",
124+
"build": "node build.mjs && node ./config/compat-entries.js",
125+
"build:microbundle": "npm-run-all --parallel 'build:*'",
125126
"build:core": "microbundle build --raw --no-generateTypes -f cjs,esm,umd",
126127
"build:debug": "microbundle build --raw --no-generateTypes -f cjs,esm,umd --cwd debug",
127128
"build:devtools": "microbundle build --raw --no-generateTypes -f cjs,esm,umd --cwd devtools",

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+
// Build

0 commit comments

Comments
 (0)