diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94c240a..b2d15c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,8 @@ jobs: run: npm run ts:check - name: compile run: npm run compile + - name: check commonjs node compat + run: node ./dist/cjs/create.js - name: lint run: npm run lint - name: test diff --git a/.npmignore b/.npmignore index f586bd6..f428f92 100644 --- a/.npmignore +++ b/.npmignore @@ -1,7 +1,9 @@ .DS_Store node_modules/ -jest.config.js +jest.config.json +src/ *.swp *.md tsconfig.json +tsconfig.cjs.json babel.config.js diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..075be88 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +scripts-prepend-node-path=true + diff --git a/package.json b/package.json index 57b2475..8fe444b 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,24 @@ "version": "2.0.7", "description": "simple, expressive API for tailwindcss + react-native", "author": "Jared Henderson ", - "main": "dist/index.js", + "module": "dist/esm/index.js", + "types": "dist/esm/index.d.ts", + "main": "dist/cjs/index.js", + "exports": { + ".": { + "import": "./dist/esm/index.js", + "require": "./dist/cjs/index.js" + }, + "./create": { + "import": "./dist/esm/create.js", + "require": "./dist/cjs/create.js" + } + }, + "typesVersions": { + "*": { + "create": ["./dist/esm/create.d.ts"] + } + }, "license": "MIT", "scripts": { "test": "jest", @@ -14,7 +31,9 @@ "ts:check": "fldev ts:check", "format": "fldev format", "format:check": "fldev format --check", - "compile": "fldev ts:compile", + "compile": "npm run compile:esm && npm run compile:cjs", + "compile:esm": "fldev ts:compile", + "compile:cjs": "fldev ts:compile --project tsconfig.cjs.json", "prepublishOnly": "npm run compile", "npub:precheck": "npm run lint && npm run format:check && npm run ts:check && npm run test" }, diff --git a/src/ClassParser.ts b/src/ClassParser.ts index 9a8b8b8..09e0c26 100644 --- a/src/ClassParser.ts +++ b/src/ClassParser.ts @@ -1,10 +1,16 @@ -import { Platform as RnPlatform } from 'react-native'; import fontSize from './resolve/font-size'; import lineHeight from './resolve/line-height'; import spacing from './resolve/spacing'; import screens from './screens'; import { TwConfig } from './tw-config'; -import { StyleIR, isOrientation, isPlatform, DeviceContext, ParseContext } from './types'; +import { + StyleIR, + isOrientation, + isPlatform, + DeviceContext, + ParseContext, + Platform, +} from './types'; import fontFamily from './resolve/font-family'; import { color, colorOpacity } from './resolve/color'; import { border, borderRadius } from './resolve/borders'; @@ -38,6 +44,7 @@ export default class ClassParser { private config: TwConfig = {}, private cache: Cache, device: DeviceContext, + platform: Platform, ) { this.context.device = device; const parts = input.trim().split(`:`); @@ -70,7 +77,7 @@ export default class ClassParser { this.isNull = true; } } else if (isPlatform(prefix)) { - this.isNull = prefix !== RnPlatform.OS; + this.isNull = prefix !== platform; } else if (isOrientation(prefix)) { if (!device.windowDimensions) { this.isNull = true; diff --git a/src/__tests__/prefix-match.spec.ts b/src/__tests__/prefix-match.spec.ts index 91156a0..6199045 100644 --- a/src/__tests__/prefix-match.spec.ts +++ b/src/__tests__/prefix-match.spec.ts @@ -18,10 +18,11 @@ describe(`tw.prefixMatch()`, () => { test(`platform prefixes`, () => { rn.Platform.OS = `ios`; + tw = create(); expect(tw.prefixMatch(`ios`)).toBe(true); expect(tw.prefixMatch(`android`)).toBe(false); - tw = create(); rn.Platform.OS = `android`; + tw = create(); expect(tw.prefixMatch(`ios`)).toBe(false); expect(tw.prefixMatch(`android`)).toBe(true); }); @@ -46,6 +47,7 @@ describe(`tw.prefixMatch()`, () => { test(`multiple prefixes`, () => { rn.Platform.OS = `ios`; + tw = create(); tw.setWindowDimensions({ width: 800, height: 600 }); expect(tw.prefixMatch(`min-w-[500px]`, `max-w-[600px]`)).toBe(false); expect(tw.prefixMatch(`min-w-[500px]`, `max-w-[900px]`)).toBe(true); diff --git a/src/create.ts b/src/create.ts new file mode 100644 index 0000000..636c341 --- /dev/null +++ b/src/create.ts @@ -0,0 +1,209 @@ +import resolveConfig from 'tailwindcss/resolveConfig'; +import { + ClassInput, + DependentStyle, + Style, + TailwindFn, + RnColorScheme, + OrderedStyle, + StyleIR, + DeviceContext, + Platform, +} from './types'; +import { TwConfig } from './tw-config'; +import Cache from './cache'; +import ClassParser from './ClassParser'; +import { parseInputs } from './parse-inputs'; +import { complete, warn } from './helpers'; +import { getAddedUtilities } from './plugin'; +import { removeOpacityHelpers } from './resolve/color'; + +export function create(customConfig: TwConfig, platform: Platform): TailwindFn { + const config = resolveConfig(customConfig as any) as TwConfig; + const device: DeviceContext = {}; + + const pluginUtils = getAddedUtilities(config.plugins); + const customStringUtils: Record = {}; + const customStyleUtils = Object.entries(pluginUtils) + .map(([util, style]): [string, StyleIR] => { + if (typeof style === `string`) { + // mutating while mapping, i know - bad form, but for performance sake... ¯\_(ツ)_/¯ + customStringUtils[util] = style; + return [util, { kind: `null` }]; + } + return [util, complete(style)]; + }) + .filter(([, ir]) => ir.kind !== `null`); + + function deriveCacheGroup(): string { + return ( + [ + device.windowDimensions ? `w${device.windowDimensions.width}` : false, + device.windowDimensions ? `h${device.windowDimensions.height}` : false, + device.fontScale ? `fs${device.fontScale}` : false, + device.colorScheme === `dark` ? `dark` : false, + device.pixelDensity === 2 ? `retina` : false, + ] + .filter(Boolean) + .join(`--`) || `default` + ); + } + + let cacheGroup = deriveCacheGroup(); + const contextCaches: Record = {}; + + function getCache(): Cache { + const existing = contextCaches[cacheGroup]; + if (existing) { + return existing; + } + const cache = new Cache(customStyleUtils); + contextCaches[cacheGroup] = cache; + return cache; + } + + function style(...inputs: ClassInput[]): Style { + const cache = getCache(); + let resolved: Style = {}; + const dependents: DependentStyle[] = []; + const ordered: OrderedStyle[] = []; + const [utilities, userStyle] = parseInputs(inputs); + + // check if we've seen this full set of classes before + // if we have a cached copy, we can skip examining each utility + const joined = utilities.join(` `); + const cached = cache.getStyle(joined); + if (cached) { + return { ...cached, ...(userStyle ? userStyle : {}) }; + } + + for (const utility of utilities) { + let styleIr = cache.getIr(utility); + + if (!styleIr && utility in customStringUtils) { + const customStyle = style(customStringUtils[utility]); + cache.setIr(utility, complete(customStyle)); + resolved = { ...resolved, ...customStyle }; + continue; + } + + const parser = new ClassParser(utility, config, cache, device, platform); + styleIr = parser.parse(); + + switch (styleIr.kind) { + case `complete`: + resolved = { ...resolved, ...styleIr.style }; + cache.setIr(utility, styleIr); + break; + case `dependent`: + dependents.push(styleIr); + break; + case `ordered`: + ordered.push(styleIr); + break; + case `null`: + cache.setIr(utility, styleIr); + break; + } + } + + if (ordered.length > 0) { + ordered.sort((a, b) => a.order - b.order); + for (const orderedStyle of ordered) { + switch (orderedStyle.styleIr.kind) { + case `complete`: + resolved = { ...resolved, ...orderedStyle.styleIr.style }; + break; + case `dependent`: + dependents.push(orderedStyle.styleIr); + break; + } + } + } + + if (dependents.length > 0) { + for (const dependent of dependents) { + const error = dependent.complete(resolved); + if (error) { + warn(error); + } + } + removeOpacityHelpers(resolved); + } + + // cache the full set of classes for future re-renders + // it's important we cache BEFORE merging in userStyle below + if (joined !== ``) { + cache.setStyle(joined, resolved); + } + + if (userStyle) { + resolved = { ...resolved, ...userStyle }; + } + + return resolved; + } + + function color(utils: string): string | undefined { + const styleObj = style( + utils + .split(/\s+/g) + .map((util) => util.replace(/^(bg|text)-/, ``)) + .map((util) => `bg-${util}`) + .join(` `), + ); + return typeof styleObj.backgroundColor === `string` + ? styleObj.backgroundColor + : undefined; + } + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + const tailwindFn = (strings: TemplateStringsArray, ...values: (string | number)[]) => { + let str = ``; + strings.forEach((string, i) => { + str += string + (values[i] || ``); + }); + return style(str); + }; + + tailwindFn.style = style; + tailwindFn.color = color; + + tailwindFn.prefixMatch = (...prefixes: string[]) => { + const joined = prefixes.sort().join(`:`); + const cache = getCache(); + const cached = cache.getPrefixMatch(joined); + if (cached !== undefined) { + return cached; + } + const parser = new ClassParser(`${joined}:flex`, config, cache, device, platform); + const ir = parser.parse(); + const prefixMatches = ir.kind !== `null`; + cache.setPrefixMatch(joined, prefixMatches); + return prefixMatches; + }; + + tailwindFn.setWindowDimensions = (newDimensions: { width: number; height: number }) => { + device.windowDimensions = newDimensions; + cacheGroup = deriveCacheGroup(); + }; + + tailwindFn.setFontScale = (newFontScale: number) => { + device.fontScale = newFontScale; + cacheGroup = deriveCacheGroup(); + }; + + tailwindFn.setPixelDensity = (newPixelDensity: 1 | 2) => { + device.pixelDensity = newPixelDensity; + cacheGroup = deriveCacheGroup(); + }; + + tailwindFn.setColorScheme = (newColorScheme: RnColorScheme) => { + device.colorScheme = newColorScheme; + cacheGroup = deriveCacheGroup(); + }; + + return tailwindFn; +} + +export default create; diff --git a/src/index.ts b/src/index.ts index 6b4bdea..77e5219 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,214 +1,16 @@ -import resolveConfig from 'tailwindcss/resolveConfig'; -import { - ClassInput, - DependentStyle, - Style, - TailwindFn, - RnColorScheme, - OrderedStyle, - StyleIR, - DeviceContext, -} from './types'; +import { Platform } from 'react-native'; +import { TailwindFn } from './types'; import { TwConfig } from './tw-config'; -import Cache from './cache'; -import ClassParser from './ClassParser'; -import { parseInputs } from './parse-inputs'; -import { complete, warn } from './helpers'; -import plugin, { getAddedUtilities } from './plugin'; -import { removeOpacityHelpers } from './resolve/color'; +import plugin from './plugin'; +import rawCreate from './create'; -export { plugin }; +// Apply default config and inject RN Platform +const create = (twConfig: TwConfig = {}): TailwindFn => rawCreate(twConfig, Platform.OS); + +export { create, plugin }; export type { TailwindFn, TwConfig }; export { useDeviceContext } from './hooks'; -export function create(customConfig: TwConfig = {}): TailwindFn { - const config = resolveConfig(customConfig as any) as TwConfig; - const device: DeviceContext = {}; - - const pluginUtils = getAddedUtilities(config.plugins); - const customStringUtils: Record = {}; - const customStyleUtils = Object.entries(pluginUtils) - .map(([util, style]): [string, StyleIR] => { - if (typeof style === `string`) { - // mutating while mapping, i know - bad form, but for performance sake... ¯\_(ツ)_/¯ - customStringUtils[util] = style; - return [util, { kind: `null` }]; - } - return [util, complete(style)]; - }) - .filter(([, ir]) => ir.kind !== `null`); - - function deriveCacheGroup(): string { - return ( - [ - device.windowDimensions ? `w${device.windowDimensions.width}` : false, - device.windowDimensions ? `h${device.windowDimensions.height}` : false, - device.fontScale ? `fs${device.fontScale}` : false, - device.colorScheme === `dark` ? `dark` : false, - device.pixelDensity === 2 ? `retina` : false, - ] - .filter(Boolean) - .join(`--`) || `default` - ); - } - - let cacheGroup = deriveCacheGroup(); - const contextCaches: Record = {}; - - function getCache(): Cache { - const existing = contextCaches[cacheGroup]; - if (existing) { - return existing; - } - const cache = new Cache(customStyleUtils); - contextCaches[cacheGroup] = cache; - return cache; - } - - function style(...inputs: ClassInput[]): Style { - const cache = getCache(); - let resolved: Style = {}; - const dependents: DependentStyle[] = []; - const ordered: OrderedStyle[] = []; - const [utilities, userStyle] = parseInputs(inputs); - - // check if we've seen this full set of classes before - // if we have a cached copy, we can skip examining each utility - const joined = utilities.join(` `); - const cached = cache.getStyle(joined); - if (cached) { - return { ...cached, ...(userStyle ? userStyle : {}) }; - } - - for (const utility of utilities) { - let styleIr = cache.getIr(utility); - - if (!styleIr && utility in customStringUtils) { - const customStyle = style(customStringUtils[utility]); - cache.setIr(utility, complete(customStyle)); - resolved = { ...resolved, ...customStyle }; - continue; - } - - const parser = new ClassParser(utility, config, cache, device); - styleIr = parser.parse(); - - switch (styleIr.kind) { - case `complete`: - resolved = { ...resolved, ...styleIr.style }; - cache.setIr(utility, styleIr); - break; - case `dependent`: - dependents.push(styleIr); - break; - case `ordered`: - ordered.push(styleIr); - break; - case `null`: - cache.setIr(utility, styleIr); - break; - } - } - - if (ordered.length > 0) { - ordered.sort((a, b) => a.order - b.order); - for (const orderedStyle of ordered) { - switch (orderedStyle.styleIr.kind) { - case `complete`: - resolved = { ...resolved, ...orderedStyle.styleIr.style }; - break; - case `dependent`: - dependents.push(orderedStyle.styleIr); - break; - } - } - } - - if (dependents.length > 0) { - for (const dependent of dependents) { - const error = dependent.complete(resolved); - if (error) { - warn(error); - } - } - removeOpacityHelpers(resolved); - } - - // cache the full set of classes for future re-renders - // it's important we cache BEFORE merging in userStyle below - if (joined !== ``) { - cache.setStyle(joined, resolved); - } - - if (userStyle) { - resolved = { ...resolved, ...userStyle }; - } - - return resolved; - } - - function color(utils: string): string | undefined { - const styleObj = style( - utils - .split(/\s+/g) - .map((util) => util.replace(/^(bg|text)-/, ``)) - .map((util) => `bg-${util}`) - .join(` `), - ); - return typeof styleObj.backgroundColor === `string` - ? styleObj.backgroundColor - : undefined; - } - - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - const tailwindFn = (strings: TemplateStringsArray, ...values: (string | number)[]) => { - let str = ``; - strings.forEach((string, i) => { - str += string + (values[i] || ``); - }); - return style(str); - }; - - tailwindFn.style = style; - tailwindFn.color = color; - - tailwindFn.prefixMatch = (...prefixes: string[]) => { - const joined = prefixes.sort().join(`:`); - const cache = getCache(); - const cached = cache.getPrefixMatch(joined); - if (cached !== undefined) { - return cached; - } - const parser = new ClassParser(`${joined}:flex`, config, cache, device); - const ir = parser.parse(); - const prefixMatches = ir.kind !== `null`; - cache.setPrefixMatch(joined, prefixMatches); - return prefixMatches; - }; - - tailwindFn.setWindowDimensions = (newDimensions: { width: number; height: number }) => { - device.windowDimensions = newDimensions; - cacheGroup = deriveCacheGroup(); - }; - - tailwindFn.setFontScale = (newFontScale: number) => { - device.fontScale = newFontScale; - cacheGroup = deriveCacheGroup(); - }; - - tailwindFn.setPixelDensity = (newPixelDensity: 1 | 2) => { - device.pixelDensity = newPixelDensity; - cacheGroup = deriveCacheGroup(); - }; - - tailwindFn.setColorScheme = (newColorScheme: RnColorScheme) => { - device.colorScheme = newColorScheme; - cacheGroup = deriveCacheGroup(); - }; - - return tailwindFn; -} - const tailwind = create(); export default tailwind; diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json new file mode 100644 index 0000000..3b34abb --- /dev/null +++ b/tsconfig.cjs.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "outDir": "./dist/cjs", + "declaration": false + } +} diff --git a/tsconfig.json b/tsconfig.json index 071ea2c..446582b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "noImplicitOverride": true, "skipLibCheck": true, "rootDir": "./src", - "outDir": "./dist", + "outDir": "./dist/esm", "lib": ["ES2019"], "target": "ES2019", "declaration": true,