From 0eef05c6848cc728d6d5913b996d7a8a3e1be611 Mon Sep 17 00:00:00 2001 From: Aliaksandr Samuseu Date: Tue, 15 Sep 2026 16:41:12 +0200 Subject: [PATCH] EPBDS-16211 fix watch-lib: same config as the published build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `watch-lib` had its own webpack config, and it had drifted. Measured against `build-lib` rather than read: dist/*.d.ts build-lib: 2 watch-lib: 0 stylesheet build-lib: dist/static/all.css watch-lib: dist/static/ui-render.css font.css, the semantic stub, the root-static re-exports: build-lib only All three matter for the one thing `watch-lib` exists for — a consumer `yalc`-linking the package to try a pre-release. They would get a stylesheet at a path the README and the published package do not use, no types at all, and a root `static/` left over from an earlier `build-lib` that watch never refreshes: correct-looking and silently stale. THE TYPES WERE THE SUBTLE ONE. Both configs set `output.clean: true`; `build-lib` gets away with it because `gen-ts` runs immediately after webpack. In watch mode nothing regenerates them, so every rebuild wiped the declarations a previous `build-lib` had produced. FIXED BY DELETING THE DUPLICATE, not by syncing it. `webpack.watch.config.mjs` is gone and `watch-lib` uses `webpack.library.config.mjs`, which was already watch-aware — it taps `watchRun` as well as `beforeRun`. Two supporting changes: `output.clean` becomes `{ keep: /\.d\.ts$/ }` so declarations survive a rebuild (a no-op for `build-lib`, where `gen-ts` rewrites them anyway), and `watch-lib` runs `gen-ts` once before starting the watcher. Verified the way the divergence was found: after the change, `watch-lib` produces an IDENTICAL file list to `build-lib` — 8 files, 2 `.d.ts`, the stylesheet at `dist/static/all.css` re-exporting the root copy — and the published CSS keeps its sha256. ALSO FIXES A READ-AFTER-CHECK RACE that has been producing intermittent failures all day. `css.pipeline.parity.test.js` tested `existsSync` at module load and `readFileSync` in a `beforeAll`; something empties the root `static/` directory between those moments in a full-suite run, so the describe was not skipped and three tests died on ENOENT — invisible in an isolated run, and it reads like a broken assertion rather than a missing file. The file is now read once, at load, so the suite either has the bytes it checked for or skips. What is established about the emptying, and what is not, is written into that file rather than guessed: it is reliably triggered by the LESS render in `css.semantic-parity.test.js` (bisected suite by suite, then by disabling that suite's compile), and it is NOT done through `fs` in the jest process — probes around `rmSync`/`unlinkSync`/`rmdirSync`/`renameSync` and their async and `fs.promises` forms, installed both in `setupFiles` and inside the suite, never fire, and no watcher process is running. The mechanism is unexplained. CI is unaffected either way: it runs jest before `build-lib`, so those three always skip there. Gates: jsdom 167 suites / 2540 tests on React 16.14, 17 and 18.3; Chrome 41/41; `lint:js`, `lint:css`, `css:fixture:check`, both docs checks, `build-lib` and `build-css` green; published CSS byte-identical. --- CLAUDE.md | 2 +- package.json | 2 +- .../__tests__/css.pipeline.parity.test.js | 32 +++++-- webpack.library.config.mjs | 7 +- webpack.watch.config.mjs | 86 ------------------- 5 files changed, 33 insertions(+), 96 deletions(-) delete mode 100644 webpack.watch.config.mjs diff --git a/CLAUDE.md b/CLAUDE.md index 8a4197d0..aac2d680 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,7 +13,7 @@ The modernization roadmap (React 17/18 upgrade, `semantic-ui-react` exit, projec - `npm start` — Run demo app in dev mode (webpack-dev-server) - `npm run build` — Build the demo app for GitHub Pages deployment - `npm run build-lib` — Build the publishable library to `dist/` (webpack + tsc) -- `npm run watch-lib` — Watch mode for library build +- `npm run watch-lib` — Watch mode for the library build. Uses the SAME webpack config as `build-lib`, deliberately: it had its own parallel config until 2026-09-15, and it had drifted into emitting the stylesheet under a different name and producing no type declarations - `npm run yalc-publish` — Build lib and publish locally via yalc (for testing in consuming apps) - `npm run yalc-watch` — Auto-rebuild and yalc-publish on src changes - `npm run deploy` — Deploy demo to GitHub Pages (run `build` first) diff --git a/package.json b/package.json index 5c42cc34..ec249af8 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "start": "webpack serve --mode development --config webpack.demo.config.mjs", "build": "webpack --mode production --config webpack.demo.config.mjs && cp build/index.html build/404.html", "build-lib": "webpack --mode production --config webpack.library.config.mjs && npm run gen-ts", - "watch-lib": "webpack --watch --mode production --config webpack.watch.config.mjs", + "watch-lib": "npm run gen-ts && webpack --watch --mode production --config webpack.library.config.mjs", "yalc-publish": "npm run build-lib && yalc publish --push", "yalc-watch": "nodemon --watch src --ext js,jsx,ts,tsx --exec 'npm run yalc-publish'", "gen-ts": "tsc --project tsconfig.build.json", diff --git a/src/style/__tests__/css.pipeline.parity.test.js b/src/style/__tests__/css.pipeline.parity.test.js index 935a0207..c6549964 100644 --- a/src/style/__tests__/css.pipeline.parity.test.js +++ b/src/style/__tests__/css.pipeline.parity.test.js @@ -348,15 +348,33 @@ describe('CSS pipeline parity — final CSS, post-PostCSS (§9.5)', () => { * Selector granularity only: the published file is minified, and cssnano may merge or split declaration * blocks without changing scoping. */ -const hasPublishedCss = fs.existsSync(PUBLISHED_CSS); -const describeIfBuilt = hasPublishedCss ? describe : describe.skip; +/** + * READ AT MODULE LOAD, NOT IN `beforeAll`, and the difference is a real failure this suite produced + * repeatedly before it was understood. + * + * It used to test `existsSync` here and `readFileSync` in a `beforeAll`. Something in a full-suite + * run EMPTIES the root `static/` directory between those two moments, so the describe was not + * skipped — the file existed when the decision was made — and then three tests died on ENOENT. + * Intermittent, invisible in an isolated run, and it reads like a broken assertion rather than a + * missing file. + * + * What is established about the cause, and what is not: the emptying is reliably triggered by the + * LESS render in `css.semantic-parity.test.js` (bisected suite by suite, then by disabling that + * suite's compile — with it off the directory survives, with it on the directory is emptied every + * time). It is NOT done through `fs` in the jest process: probes wrapping `rmSync`, `unlinkSync`, + * `rmdirSync`, `renameSync` and their async and `fs.promises` forms, installed both in `setupFiles` + * and at the top of the suite itself, never fire. No stray watcher process is running. The + * mechanism is unexplained, and is written down here rather than guessed at. + * + * Reading once, at load, makes this suite correct regardless: it either has the bytes it checked + * for, or it skips. CI is unaffected either way — it runs jest before `build-lib`, so these three + * always skip there, which is what the describe name says. + */ +const publishedCssAtLoad = fs.existsSync(PUBLISHED_CSS) ? fs.readFileSync(PUBLISHED_CSS, 'utf8') : null; +const describeIfBuilt = publishedCssAtLoad !== null ? describe : describe.skip; describeIfBuilt('published static/all.css (needs `npm run build-lib` — skipped when absent)', () => { - let publishedCss; - - beforeAll(() => { - publishedCss = fs.readFileSync(PUBLISHED_CSS, 'utf8'); - }); + const publishedCss = publishedCssAtLoad; it('leaks the same global selectors as the in-process webpack pipeline', () => { expect(occurrenceCounts(globalRuleInventory(publishedCss))).toEqual(H8_LEAK_OCCURRENCES); diff --git a/webpack.library.config.mjs b/webpack.library.config.mjs index 27054b8c..00ed5800 100644 --- a/webpack.library.config.mjs +++ b/webpack.library.config.mjs @@ -36,7 +36,12 @@ export default { export: 'default', }, globalObject: 'this', - clean: true, + // `keep` rather than a bare `true`, and it is what makes `watch-lib` usable: webpack owns + // `dist/index.js`, but `dist/*.d.ts` come from `tsc` in a separate step. A plain clean + // wipes them on every rebuild, and in watch mode nothing regenerates them — measured, a + // watch session left `dist` with ZERO declaration files after `build-lib` had produced two. + // For `build-lib` this changes nothing: `gen-ts` runs immediately after and rewrites them. + clean: { keep: /\.d\.ts$/ }, }, externals:{ moment: 'moment', diff --git a/webpack.watch.config.mjs b/webpack.watch.config.mjs deleted file mode 100644 index 20017257..00000000 --- a/webpack.watch.config.mjs +++ /dev/null @@ -1,86 +0,0 @@ -import path from 'path'; -import CopyPlugin from 'copy-webpack-plugin'; -import MiniCssExtractPlugin from 'mini-css-extract-plugin'; -import { fileURLToPath } from 'url'; -import webpack from 'webpack'; -import lessOptionsModule from './scripts/less-options.js'; -const { lessOptions } = lessOptionsModule; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -export default { - mode: 'production', - devtool: 'source-map', - entry: './src/library/index.js', - output: { - path: path.resolve(__dirname, 'dist'), - filename: 'index.js', - library: { - name: 'UIRender', - type: 'umd', - export: 'default', - }, - globalObject: 'this', - clean: true, - }, - externals:{ - moment: 'moment', - react: 'react', - 'react-dom': 'react-dom', - }, - module: { - rules: [ - { - test: /\.(ts|tsx|js|jsx)$/, - loader: 'babel-loader', - exclude: /node_modules/, - }, - { - test: /\.css$/, - use: [MiniCssExtractPlugin.loader, 'css-loader'], - }, - { - test: /\.less$/, - use: [ - MiniCssExtractPlugin.loader, - 'css-loader', - 'postcss-loader', - { - loader: 'less-loader', - options: { - lessOptions: lessOptions({ relativeUrls: false }), - }, - }, - ], - }, - { - test: /\.(woff|woff2|eot|ttf|svg)$/, - type: 'asset/resource', - generator: { - filename: 'static/fonts/[name][ext]', - }, - }, - ], - }, - resolve: { - extensions: ['.js', '.jsx', '.ts', '.tsx'], - alias: { - }, - }, - plugins: [ - new webpack.DefinePlugin({ - 'process.env.NODE_ENV': JSON.stringify('production'), - }), - new MiniCssExtractPlugin({ - filename: 'static/ui-render.css', - }), - new CopyPlugin({ - patterns: [ - { from: 'src/style/fonts/icons/fonts', to: './static/fonts/icons/fonts', noErrorOnMissing: true }, - { from: 'public/static/images', to: './static/images', noErrorOnMissing: true }, - { from: 'src/style/fonts/icons/fonts', to: '../static/fonts/icons/fonts', noErrorOnMissing: true }, - { from: 'public/static/images', to: '../static/images', noErrorOnMissing: true }, - ], - }), - ] -};