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 }, - ], - }), - ] -};