Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CommonJS Support #93

Merged
merged 7 commits into from Nov 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion .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
2 changes: 2 additions & 0 deletions .npmrc
@@ -0,0 +1,2 @@
scripts-prepend-node-path=true

23 changes: 21 additions & 2 deletions package.json
Expand Up @@ -3,7 +3,24 @@
"version": "2.0.7",
"description": "simple, expressive API for tailwindcss + react-native",
"author": "Jared Henderson <jared@netrivet.com>",
"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",
Expand All @@ -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"
},
Expand Down
13 changes: 10 additions & 3 deletions 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';
Expand Down Expand Up @@ -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(`:`);
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion src/__tests__/prefix-match.spec.ts
Expand Up @@ -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);
});
Expand All @@ -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);
Expand Down
209 changes: 209 additions & 0 deletions 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<string, string> = {};
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<string, Cache> = {};

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;