From 58c870060e13e95ac50bcd8b98de441126dafb05 Mon Sep 17 00:00:00 2001 From: Sascha Tandel Date: Mon, 12 Dec 2022 16:55:36 +0100 Subject: [PATCH] automatically add `content: ''` to `before` and `after` variant styles (closes #405, related to #414) --- .changeset/five-pots-give.md | 6 ++ documentation/examples.md | 6 +- examples/README.md | 6 +- packages/cdn/package.json | 2 +- packages/core/package.json | 4 +- packages/core/src/define-config.ts | 15 ++--- packages/core/src/internal/context.ts | 39 +++++++------ packages/core/src/tests/twind.test.ts | 1 + packages/core/src/twind.ts | 6 +- packages/core/src/types.ts | 24 +++++++- packages/preset-ext/src/preset-ext.test.json | 6 +- packages/preset-tailwind/src/index.ts | 8 +++ .../preset-tailwind/src/preflight.test.ts | 55 +++++++++++++++++++ packages/preset-tailwind/src/rules.test.json | 9 +-- packages/preset-tailwind/src/rules.test.ts | 17 ++++++ 15 files changed, 157 insertions(+), 47 deletions(-) create mode 100644 .changeset/five-pots-give.md diff --git a/.changeset/five-pots-give.md b/.changeset/five-pots-give.md new file mode 100644 index 000000000..b2eed2eb4 --- /dev/null +++ b/.changeset/five-pots-give.md @@ -0,0 +1,6 @@ +--- +'@twind/core': minor +'@twind/preset-tailwind': minor +--- + +automatically add `content: ''` to `before` and `after` variant styles (closes #405, related to #414) diff --git a/documentation/examples.md b/documentation/examples.md index 63cd248b6..4080e391f 100644 --- a/documentation/examples.md +++ b/documentation/examples.md @@ -13,10 +13,10 @@ We have created a few [examples](https://github.com/tw-in-js/twind/tree/main/exa ## Presets -| Example | Try it live at | Description | -| ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Example | Try it live at | Description | +| ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Tailwind Forms](https://github.com/tw-in-js/twind/tree/main/examples/using-tailwind-forms) | [Stackblitz](https://stackblitz.com/fork/github/tw-in-js/twind/tree/main/examples/using-tailwind-forms) • [Codesandbox](https://githubbox.com/tw-in-js/twind/tree/main/examples/using-tailwind-forms) | using [@twind/preset-autoprefix](./preset-autoprefix) and [@twind/preset-tailwind](./preset-tailwind) and [@twind/preset-tailwind-forms](./preset-tailwind-forms) | -| [Twind CDN](https://github.com/tw-in-js/twind/tree/main/examples/using-twind-cdn) | [Stackblitz](https://stackblitz.com/fork/github/tw-in-js/twind/tree/main/examples/using-twind-cdn) • [Codesandbox](https://githubbox.com/tw-in-js/twind/tree/main/examples/using-twind-cdn) | using [@twind/cdn](./installation#twind-cdn) | +| [Twind CDN](https://github.com/tw-in-js/twind/tree/main/examples/using-twind-cdn) | [Stackblitz](https://stackblitz.com/fork/github/tw-in-js/twind/tree/main/examples/using-twind-cdn) • [Codesandbox](https://githubbox.com/tw-in-js/twind/tree/main/examples/using-twind-cdn) | using [@twind/cdn](./installation#twind-cdn) | ## Integrations diff --git a/examples/README.md b/examples/README.md index ea415381a..748969b95 100644 --- a/examples/README.md +++ b/examples/README.md @@ -9,10 +9,10 @@ Some examples of how to use the Twind. These are great for reproductions of issu ## Packages -| Example | Try it live at | Description | -| ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Example | Try it live at | Description | +| ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Tailwind Forms](https://github.com/tw-in-js/twind/tree/main/examples/using-tailwind-forms) | [Stackblitz](https://stackblitz.com/fork/github/tw-in-js/twind/tree/main/examples/using-tailwind-forms) • [Codesandbox](https://githubbox.com/tw-in-js/twind/tree/main/examples/using-tailwind-forms) | using [@twind/preset-autoprefix](https://github.com/tw-in-js/twind/tree/main/packages/preset-autoprefix) and [@twind/preset-tailwind](https://github.com/tw-in-js/twind/tree/main/packages/preset-tailwind) and [@twind/preset-tailwind-forms](https://github.com/tw-in-js/twind/tree/main/packages/preset-tailwind-forms) | -| [Twind CDN](https://github.com/tw-in-js/twind/tree/main/examples/using-twind-cdn) | [Stackblitz](https://stackblitz.com/fork/github/tw-in-js/twind/tree/main/examples/using-twind-cdn) • [Codesandbox](https://githubbox.com/tw-in-js/twind/tree/main/examples/using-twind-cdn) | using [@twind/cdn](https://github.com/tw-in-js/twind/tree/main/packages/cdn) | +| [Twind CDN](https://github.com/tw-in-js/twind/tree/main/examples/using-twind-cdn) | [Stackblitz](https://stackblitz.com/fork/github/tw-in-js/twind/tree/main/examples/using-twind-cdn) • [Codesandbox](https://githubbox.com/tw-in-js/twind/tree/main/examples/using-twind-cdn) | using [@twind/cdn](https://github.com/tw-in-js/twind/tree/main/packages/cdn) | ## Frameworks diff --git a/packages/cdn/package.json b/packages/cdn/package.json index 66a75433f..45f896364 100644 --- a/packages/cdn/package.json +++ b/packages/cdn/package.json @@ -27,7 +27,7 @@ "name": "@twind/cdn", "path": "dist/cdn.esnext.js", "brotli": true, - "limit": "16kb" + "limit": "16.2kb" } ], "dependencies": { diff --git a/packages/core/package.json b/packages/core/package.json index fd85bcfdb..a12a25925 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -32,14 +32,14 @@ "path": "dist/core.esnext.js", "import": "{ setup }", "brotli": true, - "limit": "5.55kb" + "limit": "5.6kb" }, { "name": "@twind/core (twind + cssom)", "path": "dist/core.esnext.js", "import": "{ twind, cssom }", "brotli": true, - "limit": "4.9kb" + "limit": "5kb" } ], "dependencies": { diff --git a/packages/core/src/define-config.ts b/packages/core/src/define-config.ts index 5f9749f51..1c3f7a379 100644 --- a/packages/core/src/define-config.ts +++ b/packages/core/src/define-config.ts @@ -21,15 +21,16 @@ export function defineConfig[] = // most user config values go first to have precendence over preset config // only `preflight` and `theme` are applied as last preset to override all presets let config: TwindConfig> = { - preflight: userConfig.preflight !== false && [], darkMode: undefined, darkColor: undefined, + preflight: userConfig.preflight !== false && [], theme: {}, variants: asArray(userConfig.variants), rules: asArray(userConfig.rules), ignorelist: asArray(userConfig.ignorelist), - hash: userConfig.hash, - stringify: userConfig.stringify || noprefix, + hash: undefined, + stringify: (property, value) => property + ':' + value, + finalize: [], } for (const preset of asArray([ @@ -41,6 +42,7 @@ export function defineConfig[] = theme: userConfig.theme as TwindConfig>['theme'], hash: userConfig.hash, stringify: userConfig.stringify, + finalize: userConfig.finalize, } as TwindPresetConfig, ])) { const { @@ -53,6 +55,7 @@ export function defineConfig[] = ignorelist, hash = config.hash, stringify = config.stringify, + finalize, } = typeof preset == 'function' ? preset(config) : (preset as TwindPresetConfig) config = { @@ -78,12 +81,10 @@ export function defineConfig[] = hash, stringify, + + finalize: [...config.finalize, ...asArray(finalize)], } as TwindConfig> } return config } - -function noprefix(property: string, value: string): string { - return property + ':' + value -} diff --git a/packages/core/src/internal/context.ts b/packages/core/src/internal/context.ts index 6d92dc376..f07de6fd4 100644 --- a/packages/core/src/internal/context.ts +++ b/packages/core/src/internal/context.ts @@ -19,7 +19,7 @@ import type { import { DEV } from 'distilt/env' import { makeThemeFunction } from './theme' -import { asArray, escape, hash as defaultHash, identity } from '../utils' +import { asArray, escape, hash as defaultHash, identity, noop } from '../utils' import { fromMatch } from '../rules' import { warn } from './warn' @@ -37,12 +37,13 @@ type VariantFunction = ( export function createContext({ theme, darkMode, - darkColor, + darkColor = noop, variants, rules, hash, stringify, ignorelist, + finalize, }: TwindConfig): Context { // Used to cache resolved rule values const variantCache = new Map>() @@ -58,7 +59,7 @@ export function createContext({ const ignored = createRegExpExecutor(ignorelist, (value, condition) => condition.test(value)) - const reportedUnknownClasses = new Set() + const reportedUnknownClasses = /* #__PURE__ */ new Set() // add dark as last variant to allow user to override it // we can modify variants as it has been passed through defineConfig which already made a copy @@ -78,6 +79,17 @@ export function createContext({ ? defaultHash : identity + if (h !== identity) { + finalize.push((rule) => ({ + ...rule, + n: rule.n && h(rule.n), + d: rule.d?.replace( + /--(tw(?:-[\w-]+)?)\b/g, + (_: string, property: string) => '--' + h(property).replace('#', ''), + ), + })) + } + return { theme: makeThemeFunction(theme), @@ -86,12 +98,11 @@ export function createContext({ h, s(property, value) { - // Hash/Tag tailwind custom properties during serialization - return stringify(hashVars(property, h), hashVars(value, h), this) + return stringify(property, value, this) }, d(section, key, color) { - return darkColor?.(section, key, this, color) + return darkColor(section, key, this, color) }, v(value) { @@ -131,6 +142,10 @@ export function createContext({ return ruleCache.get(key) }, + + f(rule) { + return finalize.reduce((rule, p) => p(rule, this), rule) + }, } } @@ -229,15 +244,3 @@ export function toCondition(value: string | RegExp): RegExp { ? new RegExp('^' + value + (value.includes('$') || value.slice(-1) == '-' ? '' : '$')) : value } - -function hashVars(value: string, h: Context['h']): string { - // PERF: check for --tw before running the regexp - // if (value.includes('--tw')) { - return value.replace( - /--(tw(?:-[\w-]+)?)\b/g, - (_: string, property: string) => '--' + h(property).replace('#', ''), - ) - // } - - // return value -} diff --git a/packages/core/src/tests/twind.test.ts b/packages/core/src/tests/twind.test.ts index 8296b7f5d..f1feb02d3 100644 --- a/packages/core/src/tests/twind.test.ts +++ b/packages/core/src/tests/twind.test.ts @@ -23,5 +23,6 @@ test('the config can accessed', () => { ignorelist: [], hash: undefined, stringify: tw.config.stringify, + finalize: [], }) }) diff --git a/packages/core/src/twind.ts b/packages/core/src/twind.ts index f5bd1e6bd..bc8a9f433 100644 --- a/packages/core/src/twind.ts +++ b/packages/core/src/twind.ts @@ -65,9 +65,9 @@ export function twind(userConfig: TwindConfig | TwindUserConfig, sheet ) function insert(rule: TwindRule): string | undefined { - const name = rule.n && context.h(rule.n) + const finalRule = context.f(rule) - const cssText = stringify(name ? { ...rule, n: name } : rule) + const cssText = stringify(finalRule) // If not already inserted if (cssText && !insertedRules.has(cssText)) { @@ -84,7 +84,7 @@ export function twind(userConfig: TwindConfig | TwindUserConfig, sheet sortedPrecedences.splice(index, 0, rule) } - return name + return finalRule.n } return Object.defineProperties( diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 2a7c9bf7d..84da74690 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -108,21 +108,28 @@ export interface Context { * * @private */ - v: (value: string) => MaybeArray + v(value: string): MaybeArray /** * resolves a rule * * @private */ - r: (value: string, isDark?: boolean) => RuleResult + r(value: string, isDark?: boolean): RuleResult /** * stringifies a CSS property and value to a declaration * * @private */ - s: (property: string, value: string) => string + s(property: string, value: string): string + + /** + * called right before the rule is stringified and inserted into the sheet + * + * @private + */ + f(rule: TwindRule): TwindRule } // Get the leaf theme value and omit nested records like for colors @@ -336,6 +343,11 @@ export type DarkColor = ( color: ColorValue, ) => ColorValue | Falsey +export type Finalize = ( + rule: TwindRule, + context: Context, +) => TwindRule + export interface TwindConfig { /** Allows to change how the `dark` variant is used (default: `"media"`) */ darkMode?: DarkModeConfig @@ -350,6 +362,8 @@ export interface TwindConfig { hash?: boolean | undefined | HashFunction stringify: StringifyDeclaration ignorelist: (string | RegExp)[] + + finalize: Finalize[] } export type ArrayType = T extends (infer Item)[] ? Item : T @@ -389,6 +403,8 @@ export interface TwindPresetConfig { hash?: boolean | undefined | HashFunction stringify?: StringifyDeclaration ignorelist?: MaybeArray + + finalize?: MaybeArray> } export interface TwindUserConfig[] = Preset[]> { @@ -427,6 +443,8 @@ export interface TwindUserConfig[ stringify?: StringifyDeclaration> ignorelist?: MaybeArray + + finalize?: MaybeArray> } export interface BaseTheme { diff --git a/packages/preset-ext/src/preset-ext.test.json b/packages/preset-ext/src/preset-ext.test.json index fd5f8c570..0084a60cb 100644 --- a/packages/preset-ext/src/preset-ext.test.json +++ b/packages/preset-ext/src/preset-ext.test.json @@ -1,11 +1,11 @@ { "background-color[#1da1f1]": ".background-color\\[\\#1da1f1\\]{background-color:#1da1f1}", - "after::block": ".after\\:\\:block::after{display:block}", + "after::block": ".after\\:\\:block::after{content:var(--tw-content);display:block}", "before::(block inset-0)": [ "before::inset-0 before::block", [ - ".before\\:\\:inset-0::before{top:0px;right:0px;bottom:0px;left:0px}", - ".before\\:\\:block::before{display:block}" + ".before\\:\\:inset-0::before{content:var(--tw-content);top:0px;right:0px;bottom:0px;left:0px}", + ".before\\:\\:block::before{content:var(--tw-content);display:block}" ] ], "children:underline": ".children\\:underline>*{text-decoration-line:underline}", diff --git a/packages/preset-tailwind/src/index.ts b/packages/preset-tailwind/src/index.ts index d55e8f78d..723a1662d 100644 --- a/packages/preset-tailwind/src/index.ts +++ b/packages/preset-tailwind/src/index.ts @@ -22,5 +22,13 @@ export default function presetTailwind({ theme, variants, rules, + finalize(rule) { + // automatically add `content: ''` to before and after so you don’t have to specify it unless you want a different value + if (rule.r.some((r) => /^&::(before|after)$/.test(r)) && !rule.d?.includes('content:')) { + return { ...rule, d: ['content:var(--tw-content)', rule.d].filter(Boolean).join(';') } + } + + return rule + }, } } diff --git a/packages/preset-tailwind/src/preflight.test.ts b/packages/preset-tailwind/src/preflight.test.ts index 78c0bcc0c..8a634723c 100644 --- a/packages/preset-tailwind/src/preflight.test.ts +++ b/packages/preset-tailwind/src/preflight.test.ts @@ -137,3 +137,58 @@ test('custom preflight', () => { '.underline{text-decoration-line:underline}', ]) }) + +test('preflight with hashed vars', () => { + const tw = twind( + { + presets: [tailwind()], + hash: true, + }, + virtual(), + ) + + assert.deepEqual(tw.target, []) + + assert.strictEqual(tw('underline'), '#1utjbpi') + + assert.deepEqual(tw.target, [ + '*,::before,::after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}', + "::before,::after{--328t5w:''}", + 'html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"}', + 'body{margin:0;line-height:inherit}', + 'hr{height:0;color:inherit;border-top-width:1px}', + 'abbr:where([title]){text-decoration:underline dotted}', + 'h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}', + 'a{color:inherit;text-decoration:inherit}', + 'b,strong{font-weight:bolder}', + 'code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}', + 'small{font-size:80%}', + 'sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}', + 'sub{bottom:-0.25em}', + 'sup{top:-0.5em}', + 'table{text-indent:0;border-color:inherit;border-collapse:collapse}', + 'button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:inherit;color:inherit;margin:0;padding:0}', + 'button,select{text-transform:none}', + "button,[type='button'],[type='reset'],[type='submit']{-webkit-appearance:button;background-color:transparent;background-image:none}", + ':-moz-focusring{outline:auto}', + ':-moz-ui-invalid{box-shadow:none}', + 'progress{vertical-align:baseline}', + '::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}', + "[type='search']{-webkit-appearance:textfield;outline-offset:-2px}", + '::-webkit-search-decoration{-webkit-appearance:none}', + '::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}', + 'summary{display:list-item}', + 'blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}', + 'fieldset{margin:0;padding:0}', + 'legend{padding:0}', + 'ol,ul,menu{list-style:none;margin:0;padding:0}', + 'textarea{resize:vertical}', + 'input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}', + 'button,[role="button"]{cursor:pointer}', + ':disabled{cursor:default}', + 'img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}', + 'img,video{max-width:100%;height:auto}', + '[hidden]{display:none}', + '.\\#1utjbpi{text-decoration-line:underline}', + ]) +}) diff --git a/packages/preset-tailwind/src/rules.test.json b/packages/preset-tailwind/src/rules.test.json index a66d7d376..472edcfc6 100644 --- a/packages/preset-tailwind/src/rules.test.json +++ b/packages/preset-tailwind/src/rules.test.json @@ -2018,9 +2018,9 @@ "after:content-['*'] after:ml-0.5 after:text-red-500": [ "after:content-['*'] after:text-red-500 after:ml-0.5", [ - ".after\\:content-\\[\\'\\*\\'\\]:after{--tw-content:'*';content:var(--tw-content)}", - ".after\\:text-red-500:after{--tw-text-opacity:1;color:rgba(239,68,68,var(--tw-text-opacity))}", - ".after\\:ml-0\\.5:after{margin-left:0.125rem}" + ".after\\:content-\\[\\'\\*\\'\\]::after{--tw-content:'*';content:var(--tw-content)}", + ".after\\:text-red-500::after{content:var(--tw-content);--tw-text-opacity:1;color:rgba(239,68,68,var(--tw-text-opacity))}", + ".after\\:ml-0\\.5::after{content:var(--tw-content);margin-left:0.125rem}" ] ], "h-[14px]": ".h-\\[14px\\]{height:14px}", @@ -2117,5 +2117,6 @@ "bg-[length:200px_100px]": ".bg-\\[length\\:200px_100px\\]{background-size:200px 100px}", "bg-[size:200px_100px]": ".bg-\\[size\\:200px_100px\\]{background-size:200px 100px}", "bg-[center_top_1rem]": ".bg-\\[center_top_1rem\\]{background-position:center top 1rem}", - "bg-[position:center_top_1rem]": ".bg-\\[position\\:center_top_1rem\\]{background-position:center top 1rem}" + "bg-[position:center_top_1rem]": ".bg-\\[position\\:center_top_1rem\\]{background-position:center top 1rem}", + "before:absolute": ".before\\:absolute::before{content:var(--tw-content);position:absolute}" } diff --git a/packages/preset-tailwind/src/rules.test.ts b/packages/preset-tailwind/src/rules.test.ts index 04c477829..eb03b1dfd 100644 --- a/packages/preset-tailwind/src/rules.test.ts +++ b/packages/preset-tailwind/src/rules.test.ts @@ -365,3 +365,20 @@ test('font-size utilities can include a font-weight', () => { '.text-sm{font-size:12px}', ]) }) + +test('after and before content hashed', () => { + const tw = twind( + { + presets: [tailwind({ disablePreflight: true })], + hash: true, + }, + virtual(), + ) + + assert.strictEqual(tw('before:block'), '#1r4qyix') + assert.strictEqual(tw('after:block'), '#qwvwoi') + assert.deepEqual(tw.target, [ + '.\\#qwvwoi::after{content:var(--328t5w);display:block}', + '.\\#1r4qyix::before{content:var(--328t5w);display:block}', + ]) +})