strata-css
Advanced tools
+12
-0
@@ -5,2 +5,14 @@ # Changelog | ||
| ## [1.6.14] — 2026-08-05 | ||
| ### Fixed | ||
| - **Relative `content` globs resolved against the wrong directory, producing an empty stylesheet.** `scanFiles()` passed patterns to `glob.sync()` without a `cwd`, so a glob like `./src/**/*.jsx` always resolved against `process.cwd()` rather than the Strata project root. Whenever a build ran from a different directory — monorepo package builds, bundlers invoked from a parent directory — **zero files matched and the output contained no utility CSS at all**, with no error. The config itself was located correctly (it is resolved from `opts.cwd`), which made the setup look entirely correct. Globs now resolve against `opts.cwd`, and matched paths are absolute so cache keys, PostCSS dependency messages and watcher paths stay consistent regardless of where the build runs. | ||
| - **Files matched by a `content` glob were silently discarded unless their extension was one of eight.** The scanner gated on an allowlist (`.html .jsx .tsx .vue .astro .svelte .js .ts`); anything else matched by the glob was read and thrown away with no warning. This silently broke every non-JS ecosystem — `.php` and `.blade.php` (Laravel, WordPress), `.mdx`/`.md` (Next.js, Astro content), `.erb` (Rails), `.hbs`, `.twig` — and even `.mjs`/`.cjs`. A Laravel project pointing `content` at `./resources/**/*.blade.php` received an empty stylesheet and no diagnostic. The allowlist is replaced by a denylist of binary/media formats: the content glob is the filter, so a file you explicitly asked for is now always scanned. `.svg` is deliberately scanned, as SVG markup can carry class attributes. | ||
| ### Tests | ||
| - 12 new assertions in `test/scanner.js` covering both bugs — `.php`, `.blade.php`, `.mdx`, `.md`, `.erb`, `.hbs`, `.twig`, `.mjs`, `.cjs`, `.svg` are scanned while `.png` is skipped, and a build run from a directory other than the project root resolves its relative globs correctly and emits non-empty CSS. All 12 verified to fail against 1.6.13 and pass with the fix. | ||
| - Differential run over 195 repository files confirms zero tokens lost versus the 1.6.13 scanner. | ||
| --- | ||
| ## [1.6.13] — 2026-08-04 | ||
@@ -7,0 +19,0 @@ |
+1
-1
| { | ||
| "name": "strata-css", | ||
| "version": "1.6.13", | ||
| "version": "1.6.14", | ||
| "_versioningNote": "Stable: 1.0.0 / 1.1.0 / 2.0.0 | Beta: 1.1.0-beta.1 / 1.1.0-beta.2", | ||
@@ -5,0 +5,0 @@ "description": "A modern CSS framework combining Bootstrap components with Tailwind JIT processing", |
+3
-3
@@ -115,3 +115,3 @@ /** | ||
| const classNames = scanFiles(contentGlobs) | ||
| const classNames = scanFiles(contentGlobs, cwd) | ||
| const { componentCSS, utilityCSS } = generate(classNames, config) | ||
@@ -124,3 +124,3 @@ | ||
| // when a .tsx file added/changed a utility class. | ||
| const watchFiles = getWatchFiles(contentGlobs) | ||
| const watchFiles = getWatchFiles(contentGlobs, cwd) | ||
| for (let i = 0; i < watchFiles.length; i++) { | ||
@@ -194,3 +194,3 @@ result.messages.push({ | ||
| const classNames = scanFiles(contentGlobs) | ||
| const classNames = scanFiles(contentGlobs, cwd) | ||
| const { componentCSS, utilityCSS } = generate(classNames, config) | ||
@@ -197,0 +197,0 @@ |
+37
-12
@@ -12,4 +12,19 @@ /** | ||
| const ALLOWED_EXTENSIONS = new Set([ | ||
| '.html','.jsx','.tsx','.vue','.astro','.svelte','.js','.ts' | ||
| // Previously an allowlist of 8 extensions, which silently discarded any other | ||
| // file a content glob matched — .php, .blade.php, .mdx, .erb, .hbs, .twig, | ||
| // .mjs and .cjs all produced no classes and no warning. The content glob is | ||
| // the user's filter; if they asked for a file, scan it. Only binary/media | ||
| // formats that cannot carry class attributes are skipped. .svg is deliberately | ||
| // absent from this list — SVG markup can carry class attributes. | ||
| const SKIP_EXTENSIONS = new Set([ | ||
| // images | ||
| '.png','.jpg','.jpeg','.gif','.bmp','.ico','.webp','.avif','.tiff','.tif','.psd', | ||
| // fonts | ||
| '.woff','.woff2','.ttf','.eot','.otf', | ||
| // audio / video | ||
| '.mp3','.wav','.ogg','.flac','.mp4','.webm','.avi','.mov','.mkv', | ||
| // archives | ||
| '.zip','.gz','.tgz','.bz2','.7z','.rar','.tar', | ||
| // binaries / build artefacts | ||
| '.exe','.dll','.so','.dylib','.bin','.wasm','.pdf','.map','.lock', | ||
| ]) | ||
@@ -135,7 +150,13 @@ | ||
| function getFiles(contentGlobs) { | ||
| function getFiles(contentGlobs, cwd) { | ||
| // Relative content globs must resolve against the Strata project root, not | ||
| // whatever directory the build happens to run from. Without this, a bundler | ||
| // invoked from a parent directory (monorepos, some Next/webpack setups) | ||
| // matched zero files and emitted a completely empty stylesheet, silently. | ||
| const base = cwd || process.cwd() | ||
| const allFiles = [] | ||
| for (let g = 0; g < contentGlobs.length; g++) { | ||
| const pattern = contentGlobs[g] | ||
| const cached = globCache.get(pattern) | ||
| const pattern = contentGlobs[g] | ||
| const cacheKey = `${base}::${pattern}` | ||
| const cached = globCache.get(cacheKey) | ||
@@ -148,6 +169,8 @@ // Glob results rarely change — cache for 500ms | ||
| const files = glob.sync(pattern, { nodir: true }) | ||
| // absolute:true keeps cache keys, PostCSS dependency messages and watcher | ||
| // paths consistent regardless of the caller's working directory. | ||
| const files = glob.sync(pattern, { nodir: true, cwd: base, absolute: true }) | ||
| .filter(f => path.extname(f).toLowerCase() !== '.css') | ||
| globCache.set(pattern, { files, time: Date.now() }) | ||
| globCache.set(cacheKey, { files, time: Date.now() }) | ||
| files.forEach(f => allFiles.push(f)) | ||
@@ -160,3 +183,3 @@ } | ||
| const ext = path.extname(filePath).toLowerCase() | ||
| if (!ALLOWED_EXTENSIONS.has(ext)) return null | ||
| if (SKIP_EXTENSIONS.has(ext)) return null | ||
@@ -215,5 +238,5 @@ let mtime | ||
| function scanFiles(contentGlobs) { | ||
| function scanFiles(contentGlobs, cwd) { | ||
| const allClasses = new Set() | ||
| const files = getFiles(contentGlobs) | ||
| const files = getFiles(contentGlobs, cwd) | ||
@@ -228,4 +251,4 @@ for (let i = 0; i < files.length; i++) { | ||
| function getWatchFiles(contentGlobs) { | ||
| return getFiles(contentGlobs) | ||
| function getWatchFiles(contentGlobs, cwd) { | ||
| return getFiles(contentGlobs, cwd) | ||
| } | ||
@@ -235,3 +258,5 @@ | ||
| if (filePath) { | ||
| // Cache keys are absolute; accept either form from callers and watchers. | ||
| fileCache.delete(filePath) | ||
| fileCache.delete(path.resolve(filePath)) | ||
| globCache.clear() // a specific file change may mean add/delete — re-glob | ||
@@ -238,0 +263,0 @@ } else { |
316579
1.17%5947
0.42%