@weerachai06/tw-scanner 1.1.2 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/validator.js +25 -47
  2. package/package.json +1 -1
package/dist/validator.js CHANGED
@@ -1,6 +1,27 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { compile } from '@tailwindcss/node';
4
+ // ─── Compile with fallback for unknown @apply utilities ──────────────────────
5
+ // Tailwind v4 throws when @apply references a utility it doesn't know about.
6
+ // This can happen with custom utilities defined via PostCSS plugins or external
7
+ // font/design systems. We recover by injecting empty @utility stubs and retrying.
8
+ async function compileWithFallback(css, base, stubs = []) {
9
+ const injected = stubs.map((u) => `@utility ${u} { --tw-stub: 0; }`).join('\n');
10
+ const input = injected ? injected + '\n' + css : css;
11
+ try {
12
+ return await compile(input, { base, onDependency: () => { } });
13
+ }
14
+ catch (err) {
15
+ const msg = err.message ?? '';
16
+ const match = msg.match(/Cannot apply unknown utility class [`']?([^\s`']+)[`']?/);
17
+ if (match && stubs.length < 50) {
18
+ const unknown = match[1];
19
+ console.warn(`⚠ Unknown utility in @apply: "${unknown}" — stub injected for scanning. Add "@utility ${unknown} {}" to your CSS to silence this.`);
20
+ return compileWithFallback(css, base, [...stubs, unknown]);
21
+ }
22
+ throw err;
23
+ }
24
+ }
4
25
  // ─── Cache: compile result per CSS file ──────────────────────────────────────
5
26
  const compileCache = new Map();
6
27
  export async function loadTailwindContext(cssFile) {
@@ -11,58 +32,15 @@ export async function loadTailwindContext(cssFile) {
11
32
  throw new Error(`Tailwind CSS file not found: ${abs}`);
12
33
  }
13
34
  const css = fs.readFileSync(abs, 'utf8');
14
- let result;
15
- try {
16
- result = await compile(css, {
17
- base: path.dirname(abs),
18
- onDependency: () => { },
19
- });
20
- }
21
- catch (err) {
22
- const msg = err.message ?? '';
23
- const match = msg.match(/Cannot apply unknown utility class [`']?([^\s`']+)[`']?/);
24
- if (match) {
25
- // Strip all @apply lines from the entry CSS and retry so scanning can continue.
26
- // Unknown utilities are likely defined via PostCSS plugins or external font systems
27
- // that @tailwindcss/node's compile() doesn't have access to.
28
- const strippedCss = css.replace(/^\s*@apply\b[^;]+;/gm, '');
29
- console.warn(`⚠ Skipping @apply in "${path.relative(process.cwd(), abs)}" — ` +
30
- `unknown utility: "${match[1]}". ` +
31
- `Add "@utility ${match[1]} {}" to register it if you want it validated.`);
32
- result = await compile(strippedCss, {
33
- base: path.dirname(abs),
34
- onDependency: () => { },
35
- });
36
- }
37
- else {
38
- throw err;
39
- }
40
- }
35
+ const result = await compileWithFallback(css, path.dirname(abs));
41
36
  compileCache.set(abs, result);
42
37
  return result;
43
38
  }
44
39
  // ─── Build CSS selector for string matching ──────────────────────────────────
45
40
  function cssSelector(cls) {
46
- // Produces the escaped selector as it appears in Tailwind's CSS output
47
- return '.' + cls
48
- .replace(/\//g, '\\/')
49
- .replace(/\./g, '\\.')
50
- .replace(/:/g, '\\:')
51
- .replace(/\[/g, '\\[')
52
- .replace(/\]/g, '\\]')
53
- .replace(/\(/g, '\\(')
54
- .replace(/\)/g, '\\)')
55
- .replace(/=/g, '\\=')
56
- .replace(/>/g, '\\>')
57
- .replace(/&/g, '\\&')
58
- .replace(/~/g, '\\~')
59
- .replace(/\+/g, '\\+')
60
- .replace(/#/g, '\\#')
61
- .replace(/%/g, '\\%')
62
- .replace(/!/g, '\\!')
63
- .replace(/,/g, '\\,')
64
- .replace(/'/g, "\\'")
65
- .replace(/"/g, '\\"');
41
+ // Produces the escaped selector as it appears in Tailwind's CSS output.
42
+ // Single-pass replacement avoids any interaction between sequential replacements.
43
+ return '.' + cls.replace(/[\\/.:[\]()=>&~+#%!,'"]/g, '\\$&');
66
44
  }
67
45
  function selectorInOutput(selector, output) {
68
46
  const idx = output.indexOf(selector);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weerachai06/tw-scanner",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "AST-based Tailwind v4 class validator for React projects",
5
5
  "type": "module",
6
6
  "bin": {