Skip to content

Commit 65e8ca6

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 d585946 commit 65e8ca6

File tree

3 files changed

+31
-28
lines changed

3 files changed

+31
-28
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: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const parallelBuild = getBooleanEnv(process.env.BUILD_PARALLEL, true)
3939
const isWatchOnce = getBooleanEnv(process.env.BUILD_WATCH_ONCE, false)
4040
// Cache compression control: default none; allow override via env
4141
function parseCacheCompressionOption(envVal) {
42-
if (envVal == null) return undefined
42+
if (envVal == null) return false
4343
const v = String(envVal).trim().toLowerCase()
4444
if (v === '' || v === '0' || v === 'false' || v === 'none') return false
4545
if (v === 'gzip' || v === 'brotli') return v
@@ -55,7 +55,7 @@ try {
5555
cpuCount = 1
5656
}
5757
function parseThreadWorkerCount(envValue, cpuCount) {
58-
const maxWorkers = Math.max(1, cpuCount - 1)
58+
const maxWorkers = Math.max(1, cpuCount)
5959
if (envValue !== undefined && envValue !== null) {
6060
const rawStr = String(envValue).trim()
6161
if (/^[1-9]\d*$/.test(rawStr)) {
@@ -73,6 +73,7 @@ function parseThreadWorkerCount(envValue, cpuCount) {
7373
}
7474
const threadWorkers = parseThreadWorkerCount(process.env.BUILD_THREAD_WORKERS, cpuCount)
7575
// Thread-loader pool timeout constants (allow override via env)
76+
// Keep worker pool warm briefly to amortize repeated builds while still exiting quickly in CI
7677
let PRODUCTION_POOL_TIMEOUT_MS = 2000
7778
if (process.env.BUILD_POOL_TIMEOUT) {
7879
const n = parseInt(process.env.BUILD_POOL_TIMEOUT, 10)
@@ -86,6 +87,8 @@ if (process.env.BUILD_POOL_TIMEOUT) {
8687
}
8788
// Enable threads by default; allow disabling via BUILD_THREAD=0/false/no/off
8889
const enableThread = getBooleanEnv(process.env.BUILD_THREAD, true)
90+
// Allow opt-in symlink resolution for linked/workspace development when needed
91+
const resolveSymlinks = getBooleanEnv(process.env.BUILD_RESOLVE_SYMLINKS, false)
8992

9093
// Cache and resolve Sass implementation once per process
9194
let sassImplPromise
@@ -100,8 +103,8 @@ async function getSassImplementation() {
100103
const mod = await import('sass')
101104
return mod.default || mod
102105
} catch (e2) {
103-
console.error('[build] Failed to load sass-embedded:', e1 && e1.message ? e1.message : e1)
104-
console.error('[build] Failed to load sass:', e2 && e2.message ? e2.message : e2)
106+
console.error('[build] Failed to load sass-embedded:', e1)
107+
console.error('[build] Failed to load sass:', e2)
105108
throw new Error("No Sass implementation available. Install 'sass-embedded' or 'sass'.")
106109
}
107110
}
@@ -173,7 +176,7 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, minimal, sourceBuil
173176
// unnecessary cache invalidations across machines/CI runners
174177
version: JSON.stringify({ PROD: isProduction }),
175178
// default none; override via BUILD_CACHE_COMPRESSION=gzip|brotli
176-
compression: cacheCompressionOption ?? false,
179+
compression: cacheCompressionOption,
177180
buildDependencies: {
178181
config: [
179182
path.resolve('build.mjs'),
@@ -231,9 +234,8 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, minimal, sourceBuil
231234
],
232235
resolve: {
233236
extensions: ['.jsx', '.mjs', '.js'],
234-
// Disable symlink resolution for consistent behavior/perf; note this can
235-
// affect `npm link`/pnpm workspaces during local development
236-
symlinks: false,
237+
// Disable symlink resolution for consistent behavior/perf; enable via BUILD_RESOLVE_SYMLINKS=1 when working with linked deps
238+
symlinks: resolveSymlinks,
237239
alias: {
238240
parse5: path.resolve(__dirname, 'node_modules/parse5'),
239241
...(minimal
@@ -303,7 +305,12 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, minimal, sourceBuil
303305
},
304306
{
305307
loader: 'sass-loader',
306-
options: { implementation: sassImpl },
308+
options: {
309+
implementation: sassImpl,
310+
sassOptions: {
311+
silentDeps: true,
312+
},
313+
},
307314
},
308315
],
309316
},
@@ -406,10 +413,7 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, minimal, sourceBuil
406413
if (isProduction) {
407414
// Ensure compiler is properly closed after production runs
408415
compiler.run((err, stats) => {
409-
const hasErrors = !!(
410-
err ||
411-
(stats && typeof stats.hasErrors === 'function' && stats.hasErrors())
412-
)
416+
const hasErrors = !!(err || stats?.hasErrors?.())
413417
let callbackFailed = false
414418
const finishClose = () =>
415419
compiler.close((closeErr) => {
@@ -438,10 +442,7 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, minimal, sourceBuil
438442
})
439443
} else {
440444
const watching = compiler.watch({}, (err, stats) => {
441-
const hasErrors = !!(
442-
err ||
443-
(stats && typeof stats.hasErrors === 'function' && stats.hasErrors())
444-
)
445+
const hasErrors = !!(err || stats?.hasErrors?.())
445446
// Normalize callback return into a Promise to catch synchronous throws
446447
const ret = Promise.resolve().then(() => callback(err, stats))
447448
if (isWatchOnce) {
@@ -510,7 +511,7 @@ async function copyFiles(entryPoints, targetDir) {
510511
try {
511512
await fs.copy(entryPoint.src, `${targetDir}/${entryPoint.dst}`)
512513
} catch (e) {
513-
const isCss = String(entryPoint.dst).endsWith('.css')
514+
const isCss = typeof entryPoint.dst === 'string' && entryPoint.dst.endsWith('.css')
514515
if (e && e.code === 'ENOENT') {
515516
if (!isProduction && isCss) {
516517
console.log(
@@ -619,7 +620,7 @@ async function build() {
619620
minimal,
620621
tmpDir,
621622
async (err, stats) => {
622-
if (err || (stats && typeof stats.hasErrors === 'function' && stats.hasErrors())) {
623+
if (err || stats?.hasErrors?.()) {
623624
console.error(err || stats.toString())
624625
reject(err || new Error('webpack error'))
625626
return
@@ -662,10 +663,7 @@ async function build() {
662663

663664
await new Promise((resolve, reject) => {
664665
const ret = runWebpack(false, false, false, outdir, async (err, stats) => {
665-
const hasErrors = !!(
666-
err ||
667-
(stats && typeof stats.hasErrors === 'function' && stats.hasErrors())
668-
)
666+
const hasErrors = !!(err || stats?.hasErrors?.())
669667
if (hasErrors) {
670668
console.error(err || stats.toString())
671669
// In normal dev watch, keep process alive on initial errors; only fail when watch-once
@@ -680,6 +678,10 @@ async function build() {
680678
} catch (e) {
681679
// Packaging failure should stop even in dev to avoid silent success
682680
reject(e)
681+
if (isWatchOnce) {
682+
// Re-throw to surface an error and exit non-zero even if rejection isn't awaited
683+
throw e
684+
}
683685
}
684686
})
685687
// Early setup failures (e.g., dynamic imports) should fail fast

0 commit comments

Comments
 (0)