Skip to content

Document the limitations / caveats #1

@danielweck

Description

@danielweck

Hello :)

@twind/cli is a utility that can greatly help in some cases, however in practice it can also:

  • yield "false positives" (i.e. detect TailwindCSS classes / selectors that are not actually used, due to broad string-based / regular expression search)
  • give false impressions (e.g. class names generated by Twind at runtime via css() apply() and style() cannot possibly be computed via static code analysis, so the CLI-generated stylesheet is likely incomplete in most realistic use-cases).

These known limitations / caveats are absolutely acceptable, but I think the scope of this CLI utility should be documented in the README (i.e. stating goals and non-goals) so that users don't get caught off-guard / waste their time when considering their options.

There are probably several different use-cases for this CLI utility, such as producing useful development-time information, or pre-populating static stylesheets in production builds. On this latter point, if there are indeed benefits in using a pre-rendered stylesheet in combination with the Twind runtime in a live HTML page, then perhaps the README should mention it? (based on my experience, it is my understanding that there is zero advantage in doing this, in fact in my view this would just add unnecessary bytes to the total size of prerendered resources, as the Twind runtime already contains all the non-trivial logic to produce class names and CSS rules just-in-time ... maybe I am not correct, and I am always happy to be proven wrong / to learn :)

Code reference:

const cleanCandidate = (candidate: string): string => {
// 1. remove leading class: (svelte)
return candidate.replace(/^class:/, '')
}
const COMMON_INVALID_CANDIDATES = new Set([
'!DOCTYPE',
'true',
'false',
'null',
'undefined',
'class',
'className',
'currentColor',
])
const removeInvalidCandidate = (candidate: string): boolean => {
return !(
COMMON_INVALID_CANDIDATES.has(candidate) ||
// Remove candiate if it matches the following rules
// - no lower case char
!/[a-z]/.test(candidate) ||
// - containing uppercase letters
// - non number fractions and decimals
// - ending with -, /, @, $, &
// - white space only
/[A-Z]|\D[/.]\D|[-/@$&]$|^\s*$/.test(candidate) ||
// Either of the following two must match
// support @sm:..., >sm:..., <sm:...
/^[@<>][^:]+:/.test(candidate) !=
// - starts with <:#.,;?\d[\]%/$&@_
// - v-*: (vue)
// - aria-*
// - url like
/^-?[<:#.,;?\d[\]%/$&@_]|^v-[^:]+:|^aria-|^https?:\/\/|^mailto:|^tel:/.test(candidate)
)
}
export const extractRulesFromString = (content: string): string[] => {
return (content.match(/[^>"'`\s(){}[\]=][^<>"'`\s(){}=]*[^<>"'`\s(){}=:#.,;?]/g) || [])
.map(cleanCandidate)
.filter(removeInvalidCandidate)
}

Example:

twind.html
=>

<div data-test="font-italic italic">test</div>

<script>
    tw(apply('font-bold'));

    tw(css({
	    ':root body': 'bg-yellow-100',
    }));

    const func = style({
        base: 'underline',
        variants: {
            state: {
                def: 'text-red-500',
                pressed: 'text-black',
            },
            outlined: {
                true: 'ring-4 ring-red-200',
            },
        },
        defaults: {
            state: 'def',
        },
        matches: [
            {
                state: 'rounded',
                outlined: true,
                use: 'border-blue-400',
            },
        ],
    });
    tw(func({ state: 'rounded', outlined: true }));
</script>

twind.config.js
=>

export default {
	hash: false,
	preflight: false,
};

npx @twind/cli ./**/*.html -b -c twind.config.js
=>

* {
  --tw-ring-inset:var(--tw-empty, );
  --tw-ring-offset-width:0px;
  --tw-ring-offset-color:#fff;
  --tw-ring-color:rgba(59,130,246,var(--tw-ring-opacity,0.5));
  --tw-ring-offset-shadow:0 0 transparent;
  --tw-ring-shadow:0 0 transparent;
}
.ring-4 {
  --tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
  --tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);
  box-shadow:
    var(--tw-ring-offset-shadow),
    var(--tw-ring-shadow),
    var(--tw-shadow,0 0 transparent);
}
.text-black {
  --tw-text-opacity:1;
  color: #000;
  color: rgba(0, 0, 0, var(--tw-text-opacity));
}
.text-red-500 {
  --tw-text-opacity:1;
  color: #ef4444;
  color: rgba(239, 68, 68, var(--tw-text-opacity));
}
.bg-yellow-100 {
  --tw-bg-opacity:1;
  background-color: #fef3c7;
  background-color: rgba(254, 243, 199, var(--tw-bg-opacity));
}
.border-blue-400 {
  --tw-border-opacity:1;
  border-color: #60a5fa;
  border-color: rgba(96, 165, 250, var(--tw-border-opacity));
}
.ring-red-200 {
  --tw-ring-opacity:1;
  --tw-ring-color:rgba(254,202,202,var(--tw-ring-opacity));
}
.font-bold {
  font-weight: 700;
}
.font-italic {
  font-style: italic;
}
.italic {
  font-style: italic;
}
.underline {
  -webkit-text-decoration: underline;
  text-decoration: underline;
}
.rounded {
  border-radius: 0.25rem;
}

Issues:

  • rounded is not actually a used class, it is just a "word" found in a string
  • :root body { --tw-bg-opacity:1; background-color: #fef3c7; background-color: rgba(254, 243, 199, var(--tw-bg-opacity)); } is missing from the generated stylesheet, which is completely understandable, as it would normally be generated by the Twind runtime via css(). Instead, the .bg-yellow-100 { } class CSS selector is generated, but this is in fact not used in the content (and therefore wastes bytes).
  • similar remark with css() and apply()

PS: it would be awesome if support for CSS Modules composes was available more broadly, wouldn't it :)
https://github.com/css-modules/css-modules#composition

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions