mirror of
https://github.com/zhigang1992/nativewind.git
synced 2026-06-19 01:36:46 +08:00
refactor: style serialization
This commit is contained in:
@@ -17,10 +17,10 @@ tailwindRunner(
|
||||
{
|
||||
border: [
|
||||
{
|
||||
borderBottomWidth: "hairlineWidth",
|
||||
borderTopWidth: "hairlineWidth",
|
||||
borderLeftWidth: "hairlineWidth",
|
||||
borderRightWidth: "hairlineWidth",
|
||||
borderBottomWidth: "styleSheet(hairlineWidth)",
|
||||
borderTopWidth: "styleSheet(hairlineWidth)",
|
||||
borderLeftWidth: "styleSheet(hairlineWidth)",
|
||||
borderRightWidth: "styleSheet(hairlineWidth)",
|
||||
} as Style,
|
||||
],
|
||||
},
|
||||
@@ -30,8 +30,8 @@ tailwindRunner(
|
||||
{
|
||||
"border-x": [
|
||||
{
|
||||
borderLeftWidth: "hairlineWidth",
|
||||
borderRightWidth: "hairlineWidth",
|
||||
borderLeftWidth: "styleSheet(hairlineWidth)",
|
||||
borderRightWidth: "styleSheet(hairlineWidth)",
|
||||
} as Style,
|
||||
],
|
||||
},
|
||||
@@ -41,27 +41,39 @@ tailwindRunner(
|
||||
{
|
||||
"border-y": [
|
||||
{
|
||||
borderTopWidth: "hairlineWidth",
|
||||
borderBottomWidth: "hairlineWidth",
|
||||
borderTopWidth: "styleSheet(hairlineWidth)",
|
||||
borderBottomWidth: "styleSheet(hairlineWidth)",
|
||||
} as Style,
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
"border-t",
|
||||
{ "border-t": [{ borderTopWidth: "hairlineWidth" } as Style] },
|
||||
{
|
||||
"border-t": [{ borderTopWidth: "styleSheet(hairlineWidth)" } as Style],
|
||||
},
|
||||
],
|
||||
[
|
||||
"border-b",
|
||||
{ "border-b": [{ borderBottomWidth: "hairlineWidth" } as Style] },
|
||||
{
|
||||
"border-b": [
|
||||
{ borderBottomWidth: "styleSheet(hairlineWidth)" } as Style,
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
"border-l",
|
||||
{ "border-l": [{ borderLeftWidth: "hairlineWidth" } as Style] },
|
||||
{
|
||||
"border-l": [{ borderLeftWidth: "styleSheet(hairlineWidth)" } as Style],
|
||||
},
|
||||
],
|
||||
[
|
||||
"border-r",
|
||||
{ "border-r": [{ borderRightWidth: "hairlineWidth" } as Style] },
|
||||
{
|
||||
"border-r": [
|
||||
{ borderRightWidth: "styleSheet(hairlineWidth)" } as Style,
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
createTests("border", scenarios, (n) => ({
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { TailwindConfig } from "tailwindcss/tailwind-config";
|
||||
import { extractStyles } from "../../../src/postcss/extract-styles";
|
||||
import { StyleError, StyleRecord } from "../../../src/types/common";
|
||||
import { testStyleSerializer } from "../../../src/utils/serialize-styles";
|
||||
|
||||
import plugin from "../../../src/tailwind";
|
||||
import { nativePlugin } from "../../../src/tailwind/native";
|
||||
import { TailwindProvider, TailwindProviderProps } from "../../../src";
|
||||
import { PropsWithChildren } from "react";
|
||||
import { serializeStyles } from "../../../src/utils/serialize-styles";
|
||||
|
||||
export type Test = [string, StyleRecord] | [string, StyleRecord, true];
|
||||
|
||||
@@ -40,30 +40,31 @@ export function assertStyles(
|
||||
{ raw: "", extension: "html" },
|
||||
] as unknown as TailwindConfig["content"],
|
||||
safelist: [css],
|
||||
serializer: (styles) => styles,
|
||||
});
|
||||
|
||||
expect(output).toEqual({ styles });
|
||||
if (shouldError) {
|
||||
errors.push(...outputErrors);
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
expect([...errors, ...outputErrors].length).toBeGreaterThan(0);
|
||||
} else {
|
||||
expect(outputErrors.length).toBe(0);
|
||||
}
|
||||
|
||||
expect(output.output).toEqual(styles);
|
||||
}
|
||||
|
||||
export function TestProvider({
|
||||
css,
|
||||
...props
|
||||
}: PropsWithChildren<TailwindProviderProps & { css: string }>) {
|
||||
globalThis.hairlineWidthValue = 1;
|
||||
|
||||
const { styles } = extractStyles({
|
||||
const { output } = extractStyles({
|
||||
theme: {},
|
||||
plugins: [plugin, nativePlugin()],
|
||||
content: [
|
||||
{ raw: "", extension: "html" },
|
||||
] as unknown as TailwindConfig["content"],
|
||||
safelist: [css],
|
||||
serializer: testStyleSerializer,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return <TailwindProvider {...serializeStyles(styles)} {...props} />;
|
||||
return <TailwindProvider {...output} {...props} />;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { visitor, VisitorState } from "./visitor";
|
||||
import { getAllowedOptions, isAllowedProgramPath } from "./utils/allowed-paths";
|
||||
import { getTailwindConfig } from "./utils/get-tailwind-config";
|
||||
import { StyleError } from "../types/common";
|
||||
import { babelStyleSerializer } from "../utils/serialize-styles";
|
||||
|
||||
export default function rootVisitor(
|
||||
options: TailwindcssReactNativeBabelOptions,
|
||||
@@ -98,12 +99,13 @@ export default function rootVisitor(
|
||||
/**
|
||||
* Override tailwind to only process the classnames in this file
|
||||
*/
|
||||
const { styles } = extractStyles({
|
||||
const { output } = extractStyles({
|
||||
...tailwindConfig,
|
||||
content: [filename],
|
||||
// If the file doesn't have any Tailwind styles, it will print a warning
|
||||
// We force an empty style to prevent this
|
||||
safelist: [".native-hmr-empty"],
|
||||
serializer: babelStyleSerializer,
|
||||
});
|
||||
|
||||
const bodyNode = path.node.body;
|
||||
@@ -117,9 +119,9 @@ export default function rootVisitor(
|
||||
}
|
||||
|
||||
// If there are no styles, early exit
|
||||
if (Object.keys(styles).length === 0) return;
|
||||
if (Object.keys(output.styles).length === 0) return;
|
||||
|
||||
appendVariables(bodyNode, styles);
|
||||
appendVariables(bodyNode, output);
|
||||
|
||||
if (!hasStyleSheetImport) {
|
||||
prependImport(
|
||||
@@ -136,13 +138,16 @@ export default function rootVisitor(
|
||||
/**
|
||||
* Override tailwind to only process the classnames in this file
|
||||
*/
|
||||
const { styles } = extractStyles(tailwindConfig);
|
||||
const { output } = extractStyles({
|
||||
...tailwindConfig,
|
||||
serializer: babelStyleSerializer,
|
||||
});
|
||||
|
||||
// If there are no styles, early exit
|
||||
if (Object.keys(styles).length === 0) return;
|
||||
if (Object.keys(output.styles).length === 0) return;
|
||||
|
||||
const bodyNode = path.node.body;
|
||||
appendVariables(bodyNode, styles);
|
||||
appendVariables(bodyNode, output);
|
||||
|
||||
if (!hasStyleSheetImport) {
|
||||
prependImport(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { DefaultSerializedStyles } from "../../utils/serialize-styles";
|
||||
|
||||
import {
|
||||
arrayExpression,
|
||||
assignmentExpression,
|
||||
@@ -6,6 +8,7 @@ import {
|
||||
Expression,
|
||||
expressionStatement,
|
||||
identifier,
|
||||
isExpression,
|
||||
logicalExpression,
|
||||
memberExpression,
|
||||
nullLiteral,
|
||||
@@ -17,12 +20,10 @@ import {
|
||||
unaryExpression,
|
||||
} from "@babel/types";
|
||||
|
||||
import { StyleRecord } from "../../types/common";
|
||||
import { serializeStyles } from "../../utils/serialize-styles";
|
||||
|
||||
export function appendVariables(body: Statement[], styleRecord: StyleRecord) {
|
||||
const { styles, media } = serializeStyles(styleRecord);
|
||||
|
||||
export function appendVariables(
|
||||
body: Statement[],
|
||||
{ styles, media }: DefaultSerializedStyles
|
||||
) {
|
||||
body.push(
|
||||
expressionStatement(
|
||||
assignmentExpression(
|
||||
@@ -47,7 +48,7 @@ export function appendVariables(body: Statement[], styleRecord: StyleRecord) {
|
||||
identifier("RNStyleSheet"),
|
||||
identifier("create")
|
||||
),
|
||||
[serialize(styles)]
|
||||
[babelSerializeObject(styles)]
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -71,7 +72,7 @@ export function appendVariables(body: Statement[], styleRecord: StyleRecord) {
|
||||
),
|
||||
identifier("{}")
|
||||
),
|
||||
serialize(media),
|
||||
babelSerializeObject(media),
|
||||
]
|
||||
)
|
||||
)
|
||||
@@ -79,21 +80,20 @@ export function appendVariables(body: Statement[], styleRecord: StyleRecord) {
|
||||
);
|
||||
}
|
||||
|
||||
function serialize(literal: unknown): Expression {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function babelSerializeObject(literal: any): Expression {
|
||||
if (isExpression(literal)) {
|
||||
return literal;
|
||||
}
|
||||
|
||||
if (literal === null) {
|
||||
return nullLiteral();
|
||||
}
|
||||
|
||||
switch (typeof literal) {
|
||||
case "number":
|
||||
return numericLiteral(literal);
|
||||
case "string":
|
||||
if (literal === "hairlineWidth") {
|
||||
return memberExpression(
|
||||
identifier("RNStyleSheet"),
|
||||
identifier("hairlineWidth")
|
||||
);
|
||||
}
|
||||
|
||||
return stringLiteral(literal);
|
||||
case "boolean":
|
||||
return booleanLiteral(literal);
|
||||
@@ -101,7 +101,7 @@ function serialize(literal: unknown): Expression {
|
||||
return unaryExpression("void", numericLiteral(0), true);
|
||||
default:
|
||||
if (Array.isArray(literal)) {
|
||||
return arrayExpression(literal.map((n) => serialize(n)));
|
||||
return arrayExpression(literal.map((n) => babelSerializeObject(n)));
|
||||
}
|
||||
|
||||
if (isObject(literal)) {
|
||||
@@ -111,7 +111,10 @@ function serialize(literal: unknown): Expression {
|
||||
return typeof literal[k] !== "undefined";
|
||||
})
|
||||
.map((k) => {
|
||||
return objectProperty(stringLiteral(k), serialize(literal[k]));
|
||||
return objectProperty(
|
||||
stringLiteral(k),
|
||||
babelSerializeObject(literal[k])
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,10 +6,19 @@ import plugin from "../postcss";
|
||||
|
||||
import { StyleError, StyleRecord } from "../types/common";
|
||||
|
||||
export function extractStyles(
|
||||
tailwindConfig: TailwindConfig,
|
||||
export interface ExtractStylesOptions<T> extends TailwindConfig {
|
||||
serializer: (styleRecord: StyleRecord) => T;
|
||||
}
|
||||
|
||||
export interface ExtractStyles<T> {
|
||||
output: T;
|
||||
errors: StyleError[];
|
||||
}
|
||||
|
||||
export function extractStyles<T>(
|
||||
{ serializer, ...tailwindConfig }: ExtractStylesOptions<T>,
|
||||
cssInput = "@tailwind components;@tailwind utilities;"
|
||||
) {
|
||||
): ExtractStyles<T> {
|
||||
let styles: StyleRecord = {};
|
||||
let errors: StyleError[] = [];
|
||||
|
||||
@@ -27,7 +36,7 @@ export function extractStyles(
|
||||
postcss(plugins).process(cssInput).css;
|
||||
|
||||
return {
|
||||
styles,
|
||||
output: serializer(styles),
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
16
src/postcss/output-formatter.ts
Normal file
16
src/postcss/output-formatter.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { StyleRecord } from "../types/common";
|
||||
import { postCSSSerializer } from "../utils/serialize-styles";
|
||||
|
||||
export function outputFormatter(
|
||||
styles: StyleRecord,
|
||||
platform?: string
|
||||
): string {
|
||||
const { style, media } = postCSSSerializer(styles);
|
||||
|
||||
return `// This file was generated by tailwindcss-react-native. Do not edit!
|
||||
const { StyleSheet } = require("react-native")
|
||||
module.exports = { ${platform ? `\nplatform: '${platform},` : ""}
|
||||
styles: ${style},
|
||||
media: ${media},
|
||||
}`;
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { Plugin, PluginCreator } from "postcss";
|
||||
import { normalizeSelector } from "../shared/selector";
|
||||
import { toReactNative } from "./to-react-native";
|
||||
import { StyleRecord, Style, StyleError, AtRuleTuple } from "../types/common";
|
||||
import { serializeStyles } from "../utils/serialize-styles";
|
||||
import { outputFormatter } from "./output-formatter";
|
||||
|
||||
const atRuleSymbol = Symbol("media");
|
||||
|
||||
@@ -73,31 +73,10 @@ export const plugin: PluginCreator<PostcssPluginOptions> = ({
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* Why not serialize styles here?
|
||||
* Because this is used by the tests and its cleaner to write tests
|
||||
* for the non-serialized version (the at rules are next to the declarations)
|
||||
*
|
||||
* If please create an issue if you want a done function
|
||||
* that serializes the styles
|
||||
*/
|
||||
if (done) done({ styles, errors });
|
||||
|
||||
if (output) {
|
||||
const serialized = serializeStyles(styles);
|
||||
|
||||
writeFileSync(
|
||||
output,
|
||||
`// This file was generated by tailwindcss-react-native. Do not edit!
|
||||
const { StyleSheet } = require("react-native")
|
||||
module.exports = { ${platform ? `\nplatform: '${platform},` : ""}
|
||||
styles: ${JSON.stringify(serialized.styles).replace(
|
||||
new RegExp('"hairlineWidth"', "g"),
|
||||
"StyleSheet.hairlineWidth"
|
||||
)},
|
||||
media: ${JSON.stringify(serialized.media)},
|
||||
}`
|
||||
);
|
||||
writeFileSync(output, outputFormatter(styles, platform));
|
||||
}
|
||||
},
|
||||
} as Plugin;
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { getStylesForProperty, Style } from "css-to-react-native";
|
||||
import { ImageStyle, TextStyle, ViewStyle } from "react-native";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var hairlineWidthValue: number | undefined;
|
||||
}
|
||||
|
||||
export type PropertyGuard<T extends string> = (
|
||||
value: string,
|
||||
name: string
|
||||
@@ -56,16 +51,37 @@ export function only<
|
||||
const callback = (value: string, name: string) => {
|
||||
const isNaN = Number.isNaN(Number.parseInt(value));
|
||||
|
||||
if (number) {
|
||||
if (value === "hairlineWidth") {
|
||||
return JSON.parse(
|
||||
JSON.stringify(getStylesForProperty(name, "1px")).replace(
|
||||
new RegExp("1", "g"),
|
||||
globalThis.hairlineWidthValue?.toString() ?? '"hairlineWidth"'
|
||||
)
|
||||
);
|
||||
}
|
||||
if (isFunctionValue(value)) {
|
||||
/**
|
||||
* This is a hack to support platform values: styleSheet(hairlineWidth)
|
||||
*
|
||||
* We need to preserve this value all the way to the style serialization
|
||||
* where they are outputted as runtime values: StyleSheet.hairlineWidth
|
||||
*
|
||||
* But we also need to convert shorthand css property names to their long form
|
||||
*
|
||||
* so { borderWidth: styleSheet(hairlineWidth) } needs to be turned into
|
||||
*
|
||||
* {
|
||||
* "borderBottomWidth": "styleSheet(hairlineWidth)",
|
||||
* "borderLeftWidth": "styleSheet(hairlineWidth)",
|
||||
* "borderRightWidth": "styleSheet(hairlineWidth)",
|
||||
* "borderTopWidth": "styleSheet(hairlineWidth)",
|
||||
* }
|
||||
*
|
||||
* We achieve this by generating a fake style object and replacing its values.
|
||||
*/
|
||||
const fakePropertyStyles =
|
||||
number || units
|
||||
? getStylesForProperty(name, "1px")
|
||||
: getStylesForProperty(name, "transparent");
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(fakePropertyStyles).map(([key]) => [key, value])
|
||||
);
|
||||
}
|
||||
|
||||
if (number) {
|
||||
if (isNaN) {
|
||||
throw new Error(name);
|
||||
}
|
||||
@@ -96,3 +112,7 @@ export function only<
|
||||
|
||||
return callback;
|
||||
}
|
||||
|
||||
function isFunctionValue(value: string) {
|
||||
return value.startsWith("styleSheet(") || value.startsWith("platformColor(");
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ export const nativePlugin = plugin.withOptions<NativePluginOptions | undefined>(
|
||||
widest: "1px",
|
||||
},
|
||||
borderWidth: {
|
||||
DEFAULT: "hairlineWidth",
|
||||
DEFAULT: "styleSheet(hairlineWidth)",
|
||||
0: "0px",
|
||||
2: "2px",
|
||||
4: "4px",
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
import { MediaRecord, StyleRecord } from "../types/common";
|
||||
import {
|
||||
callExpression,
|
||||
identifier,
|
||||
memberExpression,
|
||||
stringLiteral,
|
||||
} from "@babel/types";
|
||||
import { MediaRecord, Style, StyleRecord } from "../types/common";
|
||||
|
||||
export function serializeStyles(styleRecord: StyleRecord) {
|
||||
export interface DefaultSerializedStyles {
|
||||
styles: {
|
||||
[k: string]: Style;
|
||||
};
|
||||
media: MediaRecord;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Default Serializer which separates the styles into { style, media }
|
||||
*/
|
||||
export function serializeStyles(
|
||||
styleRecord: StyleRecord,
|
||||
replacer?: (key: string, value: string) => [string, unknown]
|
||||
): DefaultSerializedStyles {
|
||||
const media: MediaRecord = {};
|
||||
|
||||
const styles = Object.fromEntries(
|
||||
@@ -14,6 +33,13 @@ export function serializeStyles(styleRecord: StyleRecord) {
|
||||
media[key].push(atRules);
|
||||
style = rest;
|
||||
}
|
||||
|
||||
if (replacer) {
|
||||
style = Object.fromEntries(
|
||||
Object.entries(style).map((v) => replacer(...v))
|
||||
);
|
||||
}
|
||||
|
||||
return [newKey, style];
|
||||
});
|
||||
})
|
||||
@@ -21,3 +47,103 @@ export function serializeStyles(styleRecord: StyleRecord) {
|
||||
|
||||
return { styles, media };
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by Jest
|
||||
*
|
||||
* Replaces platform function strings with known values
|
||||
*/
|
||||
export function testStyleSerializer(styleRecord: StyleRecord) {
|
||||
return serializeStyles(styleRecord, (key, value) => {
|
||||
if (value === "styleSheet(hairlineWidth)") {
|
||||
return [key, 1];
|
||||
}
|
||||
|
||||
return [key, value];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by Babel
|
||||
*
|
||||
* Replaces platform function strings with actual platform functions
|
||||
*/
|
||||
export function babelStyleSerializer(styleRecord: StyleRecord) {
|
||||
return serializeStyles(styleRecord, (key, value) => {
|
||||
if (typeof value !== "string") {
|
||||
return [key, value];
|
||||
}
|
||||
|
||||
if (value === "styleSheet(hairlineWidth)") {
|
||||
return [
|
||||
key,
|
||||
memberExpression(
|
||||
identifier("RNStyleSheet"),
|
||||
identifier("hairlineWidth")
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
if (value.startsWith("platformColor(")) {
|
||||
const result = /platformColor\((.+?)\)/.exec(value);
|
||||
|
||||
if (!result) return [key, identifier("undefined")];
|
||||
|
||||
const variables = result[1]
|
||||
.split(/[ ,]+/)
|
||||
.filter(Boolean)
|
||||
.map((v: string) => stringLiteral(v));
|
||||
|
||||
return [key, callExpression(identifier("RNPlatformColor"), variables)];
|
||||
}
|
||||
|
||||
return [key, value];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by Post CSS
|
||||
*
|
||||
* Replaces platform function strings with actual platform functions
|
||||
*/
|
||||
export function postCSSSerializer(styleRecord: StyleRecord) {
|
||||
const serialized = serializeStyles(styleRecord);
|
||||
|
||||
const styleString = JSON.stringify(serialized.styles)
|
||||
.replace(...styleSheetReplacer)
|
||||
.replace(...platformColorReplacer);
|
||||
|
||||
const mediaString = JSON.stringify(serialized.media);
|
||||
|
||||
return {
|
||||
style: styleString,
|
||||
media: mediaString,
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
const styleSheetReplacer: Parameters<String["replace"]> = [
|
||||
// '"styleSheet(hairlineWidth)"' will be captured as ["styleSheet", "hairlineWidth"]
|
||||
new RegExp(`"(styleSheet)\\((.+?)\\)"`, "g"),
|
||||
// [styleSheet, hairlineWidth] => StyleSheet.hairlineWidth
|
||||
(_: string, s: string, attribute: string) => {
|
||||
const styleSheet = s.charAt(0).toUpperCase() + s.slice(1);
|
||||
return `${styleSheet}.${attribute}`;
|
||||
},
|
||||
];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
const platformColorReplacer: Parameters<String["replace"]> = [
|
||||
// '"platformColor(color1, color2)"' will be captured as ["platformColor", "color1, color2"]
|
||||
new RegExp(`"(platformColor)\\((.+?)\\)"`, "g"),
|
||||
// [platformColor, "color1, color2"] => PlatformColor("color1","color2")
|
||||
(_: string, s: string, values: string) => {
|
||||
const platformColor = s.charAt(0).toUpperCase() + s.slice(1);
|
||||
const variables = values
|
||||
.split(/[ ,]+/)
|
||||
.filter(Boolean)
|
||||
.map((v) => `"${v}"`)
|
||||
.join(",");
|
||||
return `${platformColor}(${variables})`;
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user