Skip to content

Commit 9537643

Browse files
Refine build helpers and CI cache docs
Document BUILD_RESOLVE_SYMLINKS and cache compression behavior, and tighten related parsing in the build script. Ensure watch-once packaging failures exit non-zero and simplify error logging. Key CI webpack cache by full Node version for safety.
1 parent 727b578 commit 9537643

File tree

3 files changed

+33
-30
lines changed

3 files changed

+33
-30
lines changed

.github/workflows/pre-release-build.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,18 @@ jobs:
2626
node-version: 20
2727
cache: 'npm'
2828
cache-dependency-path: '**/package-lock.json'
29-
- name: Detect Node major version
30-
run: echo "NODE_MAJOR=$(node -p 'process.versions.node.split(".")[0]')" >> $GITHUB_ENV
29+
- name: Detect Node version
30+
run: echo "NODE_VERSION=$(node -p 'process.versions.node')" >> $GITHUB_ENV
3131
- run: npm ci
3232
- name: Cache Webpack filesystem cache
3333
uses: actions/cache@v4
3434
with:
3535
path: |
3636
.cache/webpack
3737
node_modules/.cache/webpack
38-
key: ${{ runner.os }}-node${{ env.NODE_MAJOR }}-webpack-${{ hashFiles('**/package-lock.json', 'build.mjs') }}
38+
key: ${{ runner.os }}-node${{ env.NODE_VERSION }}-webpack-${{ hashFiles('**/package-lock.json', 'build.mjs') }}
3939
restore-keys: |
40-
${{ runner.os }}-node${{ env.NODE_MAJOR }}-webpack-
40+
${{ runner.os }}-node${{ env.NODE_VERSION }}-webpack-
4141
- run: npm run build
4242

4343
- uses: josStorer/get-current-time@v2

AGENTS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,12 @@ Always reference these instructions first and fall back to search or bash comman
2828
- Default: `0` (no compression) for faster warm builds on CPU-bound SSD machines
2929
- Options: `0|false|none`, `gzip` (or `brotli` if explicitly desired)
3030
- Affects only `.cache/webpack` size/speed; does not change final artifacts
31+
- Note: Babel loader cache uses its own compression setting (currently disabled for speed) and is independent of BUILD_CACHE_COMPRESSION
3132
- BUILD_WATCH_ONCE (dev): When set, `npm run dev` runs a single build and exits (useful for timing)
3233
- BUILD_POOL_TIMEOUT: Override thread-loader production pool timeout (ms)
3334
- Default: `2000`. Increase if workers recycle too aggressively on slow machines/CI
35+
- BUILD_RESOLVE_SYMLINKS: When set to `1`/`true`, re-enable Webpack symlink resolution for `npm link`/pnpm workspace development. Default is `false` to improve performance and ensure consistent module identity (avoids duplicate module instances)
3436
- Source maps (dev): Dev builds emit external `.map` files next to JS bundles for CSP-safe debugging; production builds disable source maps
35-
- Symlinks: Webpack uses `resolve.symlinks: false` to improve performance and ensure consistent module identity; if you rely on `npm link`/pnpm workspaces, temporarily enable symlink resolution while developing linked packages
3637

3738
Performance defaults: esbuild handles JS/CSS minification. In development, CSS is injected via style-loader; in production, CSS is extracted via MiniCssExtractPlugin. Thread-loader is enabled by default in both dev and prod.
3839

build.mjs

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const parallelBuild = getBooleanEnv(process.env.BUILD_PARALLEL, true)
3232
const isWatchOnce = getBooleanEnv(process.env.BUILD_WATCH_ONCE, false)
3333
// Cache compression control: default none; allow override via env
3434
function parseCacheCompressionOption(envVal) {
35-
if (envVal == null) return undefined
35+
if (envVal == null) return false
3636
const v = String(envVal).trim().toLowerCase()
3737
if (v === '' || v === '0' || v === 'false' || v === 'none') return false
3838
if (v === 'gzip' || v === 'brotli') return v
@@ -48,7 +48,7 @@ try {
4848
cpuCount = 1
4949
}
5050
function parseThreadWorkerCount(envValue, cpuCount) {
51-
const maxWorkers = Math.max(1, cpuCount - 1)
51+
const maxWorkers = Math.max(1, cpuCount)
5252
if (envValue !== undefined && envValue !== null) {
5353
const rawStr = String(envValue).trim()
5454
if (/^[1-9]\d*$/.test(rawStr)) {
@@ -66,6 +66,7 @@ function parseThreadWorkerCount(envValue, cpuCount) {
6666
}
6767
const threadWorkers = parseThreadWorkerCount(process.env.BUILD_THREAD_WORKERS, cpuCount)
6868
// Thread-loader pool timeout constants (allow override via env)
69+
// Keep worker pool warm briefly to amortize repeated builds while still exiting quickly in CI
6970
let PRODUCTION_POOL_TIMEOUT_MS = 2000
7071
if (process.env.BUILD_POOL_TIMEOUT) {
7172
const n = parseInt(process.env.BUILD_POOL_TIMEOUT, 10)
@@ -79,6 +80,8 @@ if (process.env.BUILD_POOL_TIMEOUT) {
7980
}
8081
// Enable threads by default; allow disabling via BUILD_THREAD=0/false/no/off
8182
const enableThread = getBooleanEnv(process.env.BUILD_THREAD, true)
83+
// Allow opt-in symlink resolution for linked/workspace development when needed
84+
const resolveSymlinks = getBooleanEnv(process.env.BUILD_RESOLVE_SYMLINKS, false)
8285

8386
// Cache and resolve Sass implementation once per process
8487
let sassImplPromise
@@ -93,8 +96,8 @@ async function getSassImplementation() {
9396
const mod = await import('sass')
9497
return mod.default || mod
9598
} catch (e2) {
96-
console.error('[build] Failed to load sass-embedded:', e1 && e1.message ? e1.message : e1)
97-
console.error('[build] Failed to load sass:', e2 && e2.message ? e2.message : e2)
99+
console.error('[build] Failed to load sass-embedded:', e1)
100+
console.error('[build] Failed to load sass:', e2)
98101
throw new Error("No Sass implementation available. Install 'sass-embedded' or 'sass'.")
99102
}
100103
}
@@ -166,7 +169,7 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, minimal, sourceBuil
166169
// unnecessary cache invalidations across machines/CI runners
167170
version: JSON.stringify({ PROD: isProduction }),
168171
// default none; override via BUILD_CACHE_COMPRESSION=gzip|brotli
169-
compression: cacheCompressionOption ?? false,
172+
compression: cacheCompressionOption,
170173
buildDependencies: {
171174
config: [
172175
path.resolve('build.mjs'),
@@ -224,9 +227,8 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, minimal, sourceBuil
224227
],
225228
resolve: {
226229
extensions: ['.jsx', '.mjs', '.js'],
227-
// Disable symlink resolution for consistent behavior/perf; note this can
228-
// affect `npm link`/pnpm workspaces during local development
229-
symlinks: false,
230+
// Disable symlink resolution for consistent behavior/perf; enable via BUILD_RESOLVE_SYMLINKS=1 when working with linked deps
231+
symlinks: resolveSymlinks,
230232
alias: {
231233
parse5: path.resolve(__dirname, 'node_modules/parse5'),
232234
...(minimal
@@ -296,7 +298,12 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, minimal, sourceBuil
296298
},
297299
{
298300
loader: 'sass-loader',
299-
options: { implementation: sassImpl },
301+
options: {
302+
implementation: sassImpl,
303+
sassOptions: {
304+
quietDeps: true,
305+
},
306+
},
300307
},
301308
],
302309
},
@@ -399,10 +406,7 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, minimal, sourceBuil
399406
if (isProduction) {
400407
// Ensure compiler is properly closed after production runs
401408
compiler.run((err, stats) => {
402-
const hasErrors = !!(
403-
err ||
404-
(stats && typeof stats.hasErrors === 'function' && stats.hasErrors())
405-
)
409+
const hasErrors = !!(err || stats?.hasErrors?.())
406410
let callbackFailed = false
407411
const finishClose = () =>
408412
compiler.close((closeErr) => {
@@ -424,18 +428,15 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, minimal, sourceBuil
424428
} else {
425429
finishClose()
426430
}
427-
} catch (err) {
428-
console.error('[build] Callback error:', err)
431+
} catch (callbackErr) {
432+
console.error('[build] Callback error:', callbackErr)
429433
callbackFailed = true
430434
finishClose()
431435
}
432436
})
433437
} else {
434438
const watching = compiler.watch({}, (err, stats) => {
435-
const hasErrors = !!(
436-
err ||
437-
(stats && typeof stats.hasErrors === 'function' && stats.hasErrors())
438-
)
439+
const hasErrors = !!(err || stats?.hasErrors?.())
439440
// Normalize callback return into a Promise to catch synchronous throws
440441
const ret = Promise.resolve().then(() => callback(err, stats))
441442
if (isWatchOnce) {
@@ -504,7 +505,7 @@ async function copyFiles(entryPoints, targetDir) {
504505
try {
505506
await fs.copy(entryPoint.src, `${targetDir}/${entryPoint.dst}`)
506507
} catch (e) {
507-
const isCss = String(entryPoint.dst).endsWith('.css')
508+
const isCss = typeof entryPoint.dst === 'string' && entryPoint.dst.endsWith('.css')
508509
if (e && e.code === 'ENOENT') {
509510
if (!isProduction && isCss) {
510511
console.log(
@@ -613,7 +614,7 @@ async function build() {
613614
minimal,
614615
tmpDir,
615616
async (err, stats) => {
616-
if (err || (stats && typeof stats.hasErrors === 'function' && stats.hasErrors())) {
617+
if (err || stats?.hasErrors?.()) {
617618
console.error(err || stats.toString())
618619
reject(err || new Error('webpack error'))
619620
return
@@ -656,10 +657,7 @@ async function build() {
656657

657658
await new Promise((resolve, reject) => {
658659
const ret = runWebpack(false, false, false, outdir, async (err, stats) => {
659-
const hasErrors = !!(
660-
err ||
661-
(stats && typeof stats.hasErrors === 'function' && stats.hasErrors())
662-
)
660+
const hasErrors = !!(err || stats?.hasErrors?.())
663661
if (hasErrors) {
664662
console.error(err || stats.toString())
665663
// In normal dev watch, keep process alive on initial errors; only fail when watch-once
@@ -674,6 +672,10 @@ async function build() {
674672
} catch (e) {
675673
// Packaging failure should stop even in dev to avoid silent success
676674
reject(e)
675+
if (isWatchOnce) {
676+
// Re-throw to surface an error and exit non-zero even if rejection isn't awaited
677+
throw e
678+
}
677679
}
678680
})
679681
// Early setup failures (e.g., dynamic imports) should fail fast

0 commit comments

Comments
 (0)