diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f2ce66b..220186c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,13 +11,12 @@ permissions: jobs: create-release: runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 5 steps: - uses: taiki-e/checkout-action@v1 - uses: taiki-e/create-gh-release-action@v1 with: changelog: CHANGELOG.md - draft: true title: $tag branch: 'main|v[0-9]+' token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9dc66f4..860db6b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,7 +21,7 @@ jobs: runner: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.runner }} - timeout-minutes: 15 + timeout-minutes: 10 steps: - uses: actions/checkout@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 661929f..9cd343e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,42 @@ # Changelog -[`twg`](https://github.com/hoangnhan2ka3/twg) adheres to [Semantic Versioning](http://semver.org/). +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Undocumented APIs should be considered internal and may change without warning. +## [Unreleased] + +### Core change + +- Use `@babel` AST to parse all conditional classes or conditional objects. + - **Pros:** + - More accurate, more trust in processing. + - Reduce several complex regex use to parse condition. + - Lighter bundle. + - **Cons:** + - Currently work on (.jsx, .tsx file) only. + - A bit slower, especially on the first time, when nothing is cached. + - 4 more dependencies. + +## [1.2.3] - 2024-08-24 + +### Core change + +- Collapses `reducer()` into `parser()` function. +- Improves `reducer()` function, does not need to handle Array anymore. + +### Refactor + +- Refactor lite `parser()` function. + +### Chore + +- Test also lite `replacer()` and `twg()` function. +- Add more test cases. + ## [1.2.2] - 2024-08-24 ### Chore @@ -19,7 +52,7 @@ Undocumented APIs should be considered internal and may change without warning. ## [1.2.0] - 2024-08-23 -### Core changes +### Core change - Change the behavior of `extractor()` to scan also callee function and outer object(s) inside it. - Doesn't need regex to match `callee` function anymore. diff --git a/package.json b/package.json index 52e5a4e..03cc88d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "twg", - "version": "1.2.2", + "version": "1.2.3", "description": "A utility function for grouping TailwindCSS variants.", "author": "Nguyễn Hoàng Nhân (https://github.com/hoangnhan2ka3)", "homepage": "https://github.com/hoangnhan2ka3/twg", diff --git a/src/lite/index.ts b/src/lite/index.ts index e256289..90deb33 100644 --- a/src/lite/index.ts +++ b/src/lite/index.ts @@ -20,7 +20,7 @@ function toVal(mix: ClassValue): string { } } } else { - str += parser(mix) + str += parser()(mix) } } return str diff --git a/src/lite/lite-processor/parser.ts b/src/lite/lite-processor/parser.ts index 366e496..3acbf7d 100644 --- a/src/lite/lite-processor/parser.ts +++ b/src/lite/lite-processor/parser.ts @@ -1,38 +1,32 @@ import { type ClassValue } from "src/lite" -function reducer(args: ClassValue[]) { - return ( - args.reduce((acc: string[], cur: ClassValue) => { - if (cur === undefined || cur === null || cur === false) return acc - if (Array.isArray(cur)) { - acc.push(...cur.filter(Boolean).map(String)) - } else if (typeof cur === "object") { +export function parser() { + function reducer(args: ClassValue[]): string[] { + return args.reduce((acc, cur) => { + if (cur && typeof cur === "object") { Object.entries(cur).forEach(([key, values]) => { - const func = parser[key] as (...args: ClassValue[]) => string - if (Array.isArray(values)) { - values.flat(Infinity).forEach((value: ClassValue) => { - acc.push(func(value)) - }) - } else { - acc.push(func(values as ClassValue)) - } + (Array.isArray(values) ? values.flat(Infinity) : [values]).forEach( + value => acc.push( + (parser()[key] as (...args: ClassValue[]) => string)(value as ClassValue) + ) + ) }) } else { acc.push(...String(cur).split(" ")) } return acc - }, []) - ).flat() -} - -export const parser = new Proxy((...args: ClassValue[]) => { - return reducer(args).join(" ") -}, { - get: function (obj, key: string) { - return key ? ( - ...args: ClassValue[] - ) => reducer(args).filter(values => values.trim() !== "").map((values) => ( - `${key}:${values}`.trim() - )) : obj + }, []).flat() } -}) as Record string> & ((...args: ClassValue[]) => string) + + return new Proxy((...args: ClassValue[]) => { + return reducer(args).join(" ") + }, { + get: function (obj, key: string) { + return key ? ( + ...args: ClassValue[] + ) => reducer(args).filter(values => values.trim() !== "").map((values) => ( + `${key}:${values}`.trim() + )) : obj + } + }) as Record string> & ((...args: ClassValue[]) => string) +} diff --git a/src/lite/replacer/index.ts b/src/lite/replacer/index.ts index 864c9a3..aacc665 100644 --- a/src/lite/replacer/index.ts +++ b/src/lite/replacer/index.ts @@ -10,13 +10,13 @@ const replaceTernaryClasses = /(?:!*\(*)*\w+[)\s]*(?:[=!]==?[^&|?]+)?\?\s*(['"`] const replaceAndOrConsequent = /(?:!*\(*)*\w+[)\s]*(?:[=!]==?[^&|?]+)?(?:&&|\|\||\?\?|\?)\s*/g const replaceAlternative = /\}\s*:\s*\{/gs -export default function replacer({ callee = "twg" }: ReplacerLiteOption = {}) { +export function replacer({ callee = "twg" }: ReplacerLiteOption = {}) { return (content: string) => { if (callee.length === 0) callee = "twg" try { extractor(content, callee).forEach(largestObject => { - const filteredObject = largestObject + const filteredObject = (/['"`]/).test(largestObject) ? largestObject .replace(replaceTernaryClasses, '"$2 $3"') .replace(replaceAndOrConsequent, "") @@ -24,7 +24,7 @@ export default function replacer({ callee = "twg" }: ReplacerLiteOption = {}) { : "" try { - const parsedObject = parser( + const parsedObject = parser()( ...new Function(`return [${filteredObject}]`)() as ClassValue[] ) content = content.replace(largestObject, `"${parsedObject}"`) diff --git a/src/processor/parser.ts b/src/processor/parser.ts index bea726e..2153758 100644 --- a/src/processor/parser.ts +++ b/src/processor/parser.ts @@ -1,43 +1,10 @@ import { type ClassValue, type TWGOptions } from "src/index" -/** - * Focusing on handling arrays and objects, looping them until all are flattened. - * @param args The inputs class values - * @param separator The separator used to join the classes - * @returns string[] - * @author `easy-tailwind` [Noriller] see <[reference](https://github.com/Noriller/easy-tailwind/blob/master/src/index.ts#L65C1-L89C2)> - */ -function reducer(args: ClassValue[], options?: TWGOptions) { - return ( - args.reduce((acc: string[], cur: ClassValue) => { - if (cur === undefined || cur === null || cur === false) return acc - if (Array.isArray(cur)) { - acc.push(...cur.filter(Boolean).map(String)) - } else if (typeof cur === "object") { - Object.entries(cur).forEach(([key, values]) => { - const func = parser(options)[key] as (...args: ClassValue[]) => string - if (Array.isArray(values)) { - values.flat(Infinity).forEach((value: ClassValue) => { - acc.push(func(value)) - }) - } else { - acc.push(func(values as ClassValue)) - } - }) - } else { - acc.push(...String(cur).split(" ")) - } - return acc - }, []) - ).flat() -} - /** * Transforms the inputs. Map key to each values inside the Object zones. * @param options see [docs](https://github.com/hoangnhan2ka3/twg?tab=readme-ov-file#twg-options). * @param args The inputs class values * @returns string - * @author `easy-tailwind` [Noriller] see <[reference](https://github.com/Noriller/easy-tailwind/blob/master/src/index.ts#L57C1-L63C4)> */ export function parser(options?: TWGOptions) { const divider = (options?.separator !== undefined) @@ -45,18 +12,40 @@ export function parser(options?: TWGOptions) { ? options.separator : "" : ":" + + /** + * Focusing on handling arrays and objects, looping them until all are flattened. + * @param args The inputs class values + * @param separator The separator used to join the classes + * @returns string[] + */ + function reducer(args: ClassValue[]): string[] { + return args.reduce((acc, cur) => { + if (cur && typeof cur === "object") { + Object.entries(cur).forEach(([key, values]) => { + (Array.isArray(values) ? values.flat(Infinity) : [values]).forEach( + value => acc.push( + (parser(options)[key] as (...args: ClassValue[]) => string)(value as ClassValue) + ) + ) + }) + } else { + acc.push(...String(cur).split(" ")) + } + return acc + }, []).flat() + } + return new Proxy((...args: ClassValue[]) => { - return reducer(args, options).join(" ") + return reducer(args).join(" ") }, { get: function (obj, key: string) { return key ? ( ...args: ClassValue[] // filter out empty strings/spaces - ) => reducer(args, options).filter(values => values.trim() !== "").map((values) => ( + ) => reducer(args).filter(values => values.trim() !== "").map((values) => ( `${key}${divider}${values}`.trim() )) : obj } }) as Record string> & ((...args: ClassValue[]) => string) } - -export default parser diff --git a/src/replacer/index.ts b/src/replacer/index.ts index e894d17..ada5378 100644 --- a/src/replacer/index.ts +++ b/src/replacer/index.ts @@ -10,7 +10,7 @@ export interface ReplacerOptions { const replaceTernaryClasses = /(?:!*\(*)*\w+[)\s]*(?:[=!]==?[^&|?]+)?\?\s*(['"`])(.*?)\1\s*:\s*\1(.*?)\1/gs // cond (=== prop) ? $2 : $3 const replaceAndOrConsequent = /(?:!*\(*)*\w+[)\s]*(?:[=!]==?[^&|?]+)?(?:&&|\|\||\?\?|\?)\s*/g // cond (=== prop) &&, ||, ??, ? -const replaceAlternative = /\}\s*:\s*\{/gs // } : { +const replaceAlternative = /\}\s*:\s*\{/g // } : { /** * Transforms the content before Tailwind scans/extracting its classes. diff --git a/tests/dev/extractor.test.ts b/tests/dev/extractor.test.ts index 62896ad..1b46740 100644 --- a/tests/dev/extractor.test.ts +++ b/tests/dev/extractor.test.ts @@ -5,45 +5,90 @@ describe("extractOuterObjects()", () => { it.each([ { contents: ` - twg("multiple classes") +
`, expected: [] }, { contents: ` - twg({ +
`, expected: [`{ mod1: ["class", "other classes"], mod2: ["multiple classes"] }`] }, { contents: ` - twg( +
`, expected: [`{ mod1: ["class", "other classes"], mod2: ["multiple classes"] }`] }, { contents: ` - twg( +
`, expected: [`{ mod1: ["class", "other classes"], mod2: ["class", { "additional-mod": "other classes" }] }`] + }, + { + contents: ` +
+ `, + expected: ["{ var1: `multiple classes`, var2: [ \"multiple classes\", { var3: `other class` }] }"] + } + ])('"$expected"', ({ contents, expected }) => { + expect(extractor(contents.replace(/\s\s+/g, " "))).toStrictEqual(expected) + }) + }) + + describe("Multiple callee functions:", () => { + it.each([ + { + contents: ` +
+ Hello World +
+ `, + expected: [ + `{ mod1: ["class", "other classes"], mod2: ["multiple classes"] }`, + `{ mod3: ["class", "other multiple classes"], mod4: ["multiple classes"] }` + ] } ])('"$expected"', ({ contents, expected }) => { - expect(extractor(contents.replace(/\s\s+/g, " "), "twg")).toStrictEqual(expected) + expect(extractor(contents.replace(/\s\s+/g, " "), "cn")).toStrictEqual(expected) }) }) }) diff --git a/tests/dev/replacer.test.ts b/tests/dev/replacer.test.ts index 1fa2f54..3967643 100644 --- a/tests/dev/replacer.test.ts +++ b/tests/dev/replacer.test.ts @@ -1,3 +1,4 @@ +import { replacer as liteReplacer } from "src/lite/replacer" import { replacer } from "src/replacer" describe("replacer()", () => { @@ -93,6 +94,7 @@ describe("replacer()", () => { } ])('"$expected"', ({ contents, expected }) => { expect(replacer()(contents)).toBe(expected) + expect(liteReplacer()(contents)).toBe(expected) }) }) @@ -137,6 +139,84 @@ describe("replacer()", () => { } ])('"$expected"', ({ contents, expected }) => { expect(replacer({ callee: "cn" })(contents)).toBe(expected) + expect(liteReplacer({ callee: "cn" })(contents)).toBe(expected) + }) + + it.each([ + { + contents: ` +
+ `, + expected: ` +
+ ` + }, + { + contents: ` +
+ `, + expected: ` +
+ ` + }, + { + contents: ` +
+ `, + expected: ` +
+ ` + }, + { + contents: ` +
+ `, + expected: ` +
+ ` + } + ])('"$expected"', ({ contents, expected }) => { + expect(replacer({ callee: ["cn", "twg", "clsx"] })(contents)).toBe(expected) + expect(liteReplacer({ callee: ["cn", "twg", "clsx"] })(contents)).toBe(expected) }) it.each([ @@ -179,6 +259,7 @@ describe("replacer()", () => { } ])('"$expected"', ({ contents, expected }) => { expect(replacer({ callee: "" })(contents)).toBe(expected) + expect(liteReplacer({ callee: "" })(contents)).toBe(expected) }) }) @@ -407,6 +488,7 @@ describe("replacer()", () => { } ])('"$expected"', ({ contents, expected }) => { expect(replacer({ callee: "cn" })(contents)).toBe(expected) + expect(liteReplacer({ callee: "cn" })(contents)).toBe(expected) }) }) @@ -460,6 +542,7 @@ describe("replacer()", () => { } ])('"$expected"', ({ contents, expected }) => { expect(replacer()(contents)).toBe(expected) + expect(liteReplacer()(contents)).toBe(expected) }) }) @@ -519,6 +602,7 @@ describe("replacer()", () => { } ])('"$expected"', ({ contents, expected }) => { expect(replacer()(contents)).toBe(expected) + expect(liteReplacer()(contents)).toBe(expected) }) }) @@ -699,6 +783,7 @@ describe("replacer()", () => { } ])('"$expected"', ({ contents, expected }) => { expect(replacer()(contents)).toBe(expected) + expect(liteReplacer()(contents)).toBe(expected) }) }) @@ -730,6 +815,7 @@ describe("replacer()", () => { } ])('"$expected"', ({ contents, expected }) => { expect(replacer()(contents)).toBe(expected) + expect(liteReplacer()(contents)).toBe(expected) }) }) @@ -793,6 +879,7 @@ describe("replacer()", () => { } ])('"$expected"', ({ contents, expected }) => { expect(replacer({ callee: "cn" })(contents)).toBe(expected) + expect(liteReplacer({ callee: "cn" })(contents)).toBe(expected) }) }) @@ -838,6 +925,7 @@ describe("replacer()", () => { } ])('"$expected"', ({ contents, expected }) => { expect(replacer()(contents)).toBe(expected) + expect(liteReplacer()(contents)).toBe(expected) }) }) @@ -921,6 +1009,7 @@ describe("replacer()", () => { } ])('"$expected"', ({ contents, expected }) => { expect(replacer()(contents)).toBe(expected) + expect(liteReplacer()(contents)).toBe(expected) }) }) @@ -1128,6 +1217,7 @@ describe("replacer()", () => { } ])('"$expected"', ({ contents, expected }) => { expect(replacer()(contents)).toBe(expected) + expect(liteReplacer()(contents)).toBe(expected) }) }) @@ -1137,6 +1227,7 @@ describe("replacer()", () => { { contents: "anything", expected: "anything" } ])('"$expected"', ({ contents, expected }) => { expect(replacer()(contents)).toBe(expected) + expect(liteReplacer()(contents)).toBe(expected) }) }) }) diff --git a/tests/dev/index.test.ts b/tests/dev/twg.test.ts similarity index 91% rename from tests/dev/index.test.ts rename to tests/dev/twg.test.ts index 260bec7..d607a18 100644 --- a/tests/dev/index.test.ts +++ b/tests/dev/twg.test.ts @@ -2,6 +2,7 @@ /* eslint-disable no-constant-binary-expression */ import { twg } from "src" +import { twg as liteTwg } from "src/lite" describe("twg()", () => { describe("General cases:", () => { @@ -40,6 +41,7 @@ describe("twg()", () => { } ])('"$expected"', ({ args, expected }) => { expect(twg({}, ...args)).toBe(expected) + expect(liteTwg(...args)).toBe(expected) }) }) @@ -71,6 +73,7 @@ describe("twg()", () => { } ])('"$expected"', ({ args, expected }) => { expect(twg(...args)).toBe(expected) + expect(liteTwg(...args)).toBe(expected) }) }) @@ -90,6 +93,7 @@ describe("twg()", () => { } ])('"$expected"', ({ args, expected }) => { expect(twg(...args)).toBe(expected) + expect(liteTwg(...args)).toBe(expected) }) }) @@ -109,6 +113,7 @@ describe("twg()", () => { } ])('"$expected"', ({ args, expected }) => { expect(twg(...args)).toBe(expected) + expect(liteTwg(...args)).toBe(expected) }) }) @@ -128,6 +133,7 @@ describe("twg()", () => { } ])('"$expected"', ({ args, expected }) => { expect(twg(...args)).toBe(expected) + expect(liteTwg(...args)).toBe(expected) }) }) @@ -147,6 +153,7 @@ describe("twg()", () => { } ])('"$expected"', ({ args, expected }) => { expect(twg(...args)).toBe(expected) + expect(liteTwg(...args)).toBe(expected) }) }) @@ -166,6 +173,7 @@ describe("twg()", () => { } ])('"$expected"', ({ args, expected }) => { expect(twg(...args)).toBe(expected) + expect(liteTwg(...args)).toBe(expected) }) }) @@ -185,6 +193,7 @@ describe("twg()", () => { } ])('"$expected"', ({ args, expected }) => { expect(twg(...args)).toBe(expected) + expect(liteTwg(...args)).toBe(expected) }) }) @@ -204,6 +213,7 @@ describe("twg()", () => { } ])('"$expected"', ({ args, expected }) => { expect(twg(...args)).toBe(expected) + expect(liteTwg(...args)).toBe(expected) }) }) @@ -223,6 +233,7 @@ describe("twg()", () => { } ])('"$expected"', ({ args, expected }) => { expect(twg(...args)).toBe(expected) + expect(liteTwg(...args)).toBe(expected) }) }) @@ -250,6 +261,7 @@ describe("twg()", () => { } ])('"$expected"', ({ args, expected }) => { expect(twg(...args)).toBe(expected) + expect(liteTwg(...args)).toBe(expected) }) }) @@ -281,6 +293,7 @@ describe("twg()", () => { } ])('"$expected"', ({ args, expected }) => { expect(twg(...args)).toBe(expected) + expect(liteTwg(...args)).toBe(expected) }) }) @@ -311,6 +324,7 @@ describe("twg()", () => { } ])('"$expected"', ({ args, expected }) => { expect(twg(...args)).toBe(expected) + expect(liteTwg(...args)).toBe(expected) }) }) @@ -378,9 +392,9 @@ describe("twg()", () => { */ describe("Complex cases:", () => { /* cSpell:disable */ - it("Handles a really complex object:", () => { - expect( - twg( + it.each([ + { + args: [ "Lorem ipsum", "dolor sit", ["amet", "consectetur adipiscing elit"], @@ -414,10 +428,12 @@ describe("twg()", () => { } ] } - ) - ).toBe( - "Lorem ipsum dolor sit amet consectetur adipiscing elit Sed sit amet ligula ex Ut var1:in var1:suscipit var1:metus var2:vel var2:accumsan var2:orci var2:Vivamus var2:sapien var2:neque var2:dictum var2:vel var2:felis var2:maximus var3:luctus var3:var4:lorem var5:Fusce var5:malesuada var5:massa var5:eu var5:turpis var5:finibus var5:var6:mollis var5:var6:var7:In var5:var6:var7:augue var5:var6:var7:tortor var5:var6:var7:var8:porta var5:var6:var7:var8:eu var5:var6:var7:var8:erat var5:var6:var7:var8:sit var5:var6:var7:var8:amet var5:var6:var7:var8:tristique var5:var6:var7:var8:ullamcorper var5:var6:var7:var8:arcu" - ) + ], + expected: "Lorem ipsum dolor sit amet consectetur adipiscing elit Sed sit amet ligula ex Ut var1:in var1:suscipit var1:metus var2:vel var2:accumsan var2:orci var2:Vivamus var2:sapien var2:neque var2:dictum var2:vel var2:felis var2:maximus var3:luctus var3:var4:lorem var5:Fusce var5:malesuada var5:massa var5:eu var5:turpis var5:finibus var5:var6:mollis var5:var6:var7:In var5:var6:var7:augue var5:var6:var7:tortor var5:var6:var7:var8:porta var5:var6:var7:var8:eu var5:var6:var7:var8:erat var5:var6:var7:var8:sit var5:var6:var7:var8:amet var5:var6:var7:var8:tristique var5:var6:var7:var8:ullamcorper var5:var6:var7:var8:arcu" + } + ])("Handles a really complex object:", ({ args, expected }) => { + expect(twg(...args)).toBe(expected) + expect(liteTwg(...args)).toBe(expected) }) /* cSpell:enable */ @@ -431,6 +447,7 @@ describe("twg()", () => { "Handles falsy values", (falsy) => { expect(twg(falsy)).toBe("") + expect(liteTwg(falsy)).toBe("") } ) })