Skip to content

Commit 40e229f

Browse files
committed
refactor: consolidate and improve scripts
- Extract scripts from package.json to .mjs files - Replace IIFEs with named main functions - Add common utilities and logger - Add claude.mjs for comprehensive operations - Improve lint, test, and build scripts - Add fix, bump, update, and publish scripts - Use @socketsecurity/registry utilities - Standardize script headers and error handling - Update husky pre-commit hook
1 parent 1da42fe commit 40e229f

25 files changed

+4335
-63
lines changed

.husky/pre-commit

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
if [ -z "${DISABLE_PRECOMMIT_LINT}" ]; then
2-
pnpm run lint-staged
2+
pnpm run precommit
33
else
44
echo "Skipping lint due to DISABLE_PRECOMMIT_LINT env var"
55
fi
66

77
if [ -z "${DISABLE_PRECOMMIT_TEST}" ]; then
8-
pnpm run test-pre-commit
8+
dotenvx -q run -f .env.precommit -- pnpm test --staged
99
else
1010
echo "Skipping testing due to DISABLE_PRECOMMIT_TEST env var"
1111
fi

scripts/build.mjs

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
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

Comments
 (0)