1+ /**
2+ * @fileoverview Fast build runner using esbuild for smaller bundles and faster builds.
3+ */
4+
5+ import { existsSync } from 'node:fs'
6+ import path from 'node:path'
7+ import { parseArgs } from 'node:util'
8+
9+ import { build } from 'esbuild'
10+
11+ import {
12+ getRootPath ,
13+ isQuiet ,
14+ log ,
15+ printFooter ,
16+ printHeader ,
17+ printHelpHeader
18+ } from './utils/common.mjs'
19+ import { runSequence } from './utils/run-command.mjs'
20+ import { analyzeMetafile , buildConfig , watchConfig } from '../.config/esbuild.config.mjs'
21+
22+ const rootPath = getRootPath ( import . meta. url )
23+
24+ /**
25+ * Build source code with esbuild.
26+ */
27+ async function buildSource ( options = { } ) {
28+ const { analyze = false , quiet = false , skipClean = false , verbose = false } = options
29+
30+ if ( ! quiet ) {
31+ log . progress ( 'Building source code' )
32+ }
33+
34+ // Clean dist directory if needed
35+ if ( ! skipClean ) {
36+ const exitCode = await runSequence ( [
37+ { args : [ 'exec' , 'node' , 'scripts/clean.mjs' , '--dist' , '--quiet' ] , command : 'pnpm' }
38+ ] )
39+ if ( exitCode !== 0 ) {
40+ if ( ! quiet ) {
41+ log . failed ( 'Clean failed' )
42+ }
43+ return exitCode
44+ }
45+ }
46+
47+ try {
48+ const startTime = Date . now ( )
49+ // Determine log level based on verbosity
50+ const logLevel = quiet ? 'silent' : verbose ? 'info' : 'warning'
51+ const result = await build ( {
52+ ...buildConfig ,
53+ logLevel
54+ } )
55+ const buildTime = Date . now ( ) - startTime
56+
57+ if ( ! quiet ) {
58+ log . done ( `Source build complete in ${ buildTime } ms` )
59+
60+ if ( analyze && result . metafile ) {
61+ const analysis = analyzeMetafile ( result . metafile )
62+ log . info ( 'Build output:' )
63+ for ( const file of analysis . files ) {
64+ log . substep ( `${ file . name } : ${ file . size } ` )
65+ }
66+ log . step ( `Total bundle size: ${ analysis . totalSize } ` )
67+ }
68+ }
69+
70+ return 0
71+ } catch ( error ) {
72+ if ( ! quiet ) {
73+ log . failed ( 'Source build failed' )
74+ console . error ( error )
75+ }
76+ return 1
77+ }
78+ }
79+
80+ /**
81+ * Build TypeScript declarations.
82+ */
83+ async function buildTypes ( options = { } ) {
84+ const { quiet = false , skipClean = false , verbose : _verbose = false } = options
85+
86+ if ( ! quiet ) {
87+ log . progress ( 'Building TypeScript declarations' )
88+ }
89+
90+ const commands = [ ]
91+
92+ if ( ! skipClean ) {
93+ commands . push ( { args : [ 'exec' , 'node' , 'scripts/clean.mjs' , '--types' , '--quiet' ] , command : 'pnpm' } )
94+ }
95+
96+ commands . push ( {
97+ args : [ 'exec' , 'tsgo' , '--project' , '.config/tsconfig.dts.json' ] ,
98+ command : 'pnpm' ,
99+ } )
100+
101+ const exitCode = await runSequence ( commands )
102+
103+ if ( exitCode !== 0 ) {
104+ if ( ! quiet ) {
105+ log . failed ( 'Type declarations build failed' )
106+ }
107+ return exitCode
108+ }
109+
110+ if ( ! quiet ) {
111+ log . done ( 'Type declarations built' )
112+ }
113+
114+ return 0
115+ }
116+
117+ /**
118+ * Watch mode for development.
119+ */
120+ async function watchBuild ( options = { } ) {
121+ const { quiet = false , verbose = false } = options
122+
123+ if ( ! quiet ) {
124+ log . step ( 'Starting watch mode' )
125+ log . substep ( 'Watching for file changes...' )
126+ }
127+
128+ try {
129+ // Determine log level based on verbosity
130+ const logLevel = quiet ? 'silent' : verbose ? 'debug' : 'warning'
131+ const ctx = await build ( {
132+ ...watchConfig ,
133+ logLevel
134+ } )
135+
136+ // Keep the process alive
137+ process . on ( 'SIGINT' , ( ) => {
138+ ctx . stop ( )
139+ process . exitCode = 0
140+ throw new Error ( 'Watch mode interrupted' )
141+ } )
142+
143+ // Wait indefinitely
144+ await new Promise ( ( ) => { } )
145+ } catch ( error ) {
146+ if ( ! quiet ) {
147+ log . error ( 'Watch mode failed:' , error )
148+ }
149+ return 1
150+ }
151+ }
152+
153+ /**
154+ * Check if build is needed.
155+ */
156+ function isBuildNeeded ( ) {
157+ const distPath = path . join ( rootPath , 'dist' , 'index.js' )
158+ const distTypesPath = path . join ( rootPath , 'dist' , 'types' , 'index.d.ts' )
159+
160+ return ! existsSync ( distPath ) || ! existsSync ( distTypesPath )
161+ }
162+
163+ async function main ( ) {
164+ try {
165+ // Parse arguments
166+ const { values } = parseArgs ( {
167+ options : {
168+ help : {
169+ type : 'boolean' ,
170+ default : false ,
171+ } ,
172+ src : {
173+ type : 'boolean' ,
174+ default : false ,
175+ } ,
176+ types : {
177+ type : 'boolean' ,
178+ default : false ,
179+ } ,
180+ watch : {
181+ type : 'boolean' ,
182+ default : false ,
183+ } ,
184+ needed : {
185+ type : 'boolean' ,
186+ default : false ,
187+ } ,
188+ analyze : {
189+ type : 'boolean' ,
190+ default : false ,
191+ } ,
192+ silent : {
193+ type : 'boolean' ,
194+ default : false ,
195+ } ,
196+ quiet : {
197+ type : 'boolean' ,
198+ default : false ,
199+ } ,
200+ verbose : {
201+ type : 'boolean' ,
202+ default : false ,
203+ } ,
204+ } ,
205+ allowPositionals : false ,
206+ strict : false ,
207+ } )
208+
209+ // Show help if requested
210+ if ( values . help ) {
211+ printHelpHeader ( 'Build Runner' )
212+ console . log ( '\nUsage: pnpm build [options]' )
213+ console . log ( '\nOptions:' )
214+ console . log ( ' --help Show this help message' )
215+ console . log ( ' --src Build source code only' )
216+ console . log ( ' --types Build TypeScript declarations only' )
217+ console . log ( ' --watch Watch mode for development' )
218+ console . log ( ' --needed Only build if dist files are missing' )
219+ console . log ( ' --analyze Show bundle size analysis' )
220+ console . log ( ' --quiet, --silent Suppress progress messages' )
221+ console . log ( ' --verbose Show detailed build output' )
222+ console . log ( '\nExamples:' )
223+ console . log ( ' pnpm build # Full build (source + types)' )
224+ console . log ( ' pnpm build --src # Build source only' )
225+ console . log ( ' pnpm build --types # Build types only' )
226+ console . log ( ' pnpm build --watch # Watch mode' )
227+ console . log ( ' pnpm build --analyze # Build with size analysis' )
228+ process . exitCode = 0
229+ return
230+ }
231+
232+ const quiet = isQuiet ( values )
233+ const verbose = values . verbose
234+
235+ // Check if build is needed
236+ if ( values . needed && ! isBuildNeeded ( ) ) {
237+ if ( ! quiet ) {
238+ log . info ( 'Build artifacts exist, skipping build' )
239+ }
240+ process . exitCode = 0
241+ return
242+ }
243+
244+ if ( ! quiet ) {
245+ printHeader ( 'Build Runner' )
246+ }
247+
248+ let exitCode = 0
249+
250+ // Handle watch mode
251+ if ( values . watch ) {
252+ exitCode = await watchBuild ( { quiet, verbose } )
253+ }
254+ // Build types only
255+ else if ( values . types && ! values . src ) {
256+ if ( ! quiet ) {
257+ log . step ( 'Building TypeScript declarations only' )
258+ }
259+ exitCode = await buildTypes ( { quiet, verbose } )
260+ }
261+ // Build source only
262+ else if ( values . src && ! values . types ) {
263+ if ( ! quiet ) {
264+ log . step ( 'Building source only' )
265+ }
266+ exitCode = await buildSource ( { quiet, verbose, analyze : values . analyze } )
267+ }
268+ // Build everything (default)
269+ else {
270+ if ( ! quiet ) {
271+ log . step ( 'Building package (source + types)' )
272+ }
273+
274+ // Clean all directories first (once)
275+ if ( ! quiet ) {
276+ log . progress ( 'Cleaning build directories' )
277+ }
278+ exitCode = await runSequence ( [
279+ { args : [ 'exec' , 'node' , 'scripts/clean.mjs' , '--dist' , '--types' , '--quiet' ] , command : 'pnpm' }
280+ ] )
281+ if ( exitCode !== 0 ) {
282+ if ( ! quiet ) {
283+ log . failed ( 'Clean failed' )
284+ }
285+ process . exitCode = exitCode
286+ return
287+ }
288+
289+ // Run source and types builds in parallel
290+ const buildPromises = [
291+ buildSource ( { quiet, verbose, skipClean : true , analyze : values . analyze } ) ,
292+ buildTypes ( { quiet, verbose, skipClean : true } )
293+ ]
294+
295+ const results = await Promise . all ( buildPromises )
296+ exitCode = results . find ( code => code !== 0 ) || 0
297+ }
298+
299+ if ( exitCode !== 0 ) {
300+ if ( ! quiet ) {
301+ log . error ( 'Build failed' )
302+ }
303+ process . exitCode = exitCode
304+ } else {
305+ if ( ! quiet ) {
306+ printFooter ( 'Build completed successfully!' )
307+ }
308+ }
309+ } catch ( error ) {
310+ log . error ( `Build runner failed: ${ error . message } ` )
311+ process . exitCode = 1
312+ }
313+ }
314+
315+ main ( ) . catch ( console . error )
0 commit comments