refactor: testing compiling in metro

This commit is contained in:
Mark Lawlor
2022-10-09 22:11:32 +10:00
parent ec33a290fd
commit f5b435fd61
4 changed files with 78 additions and 253 deletions

View File

@@ -1,5 +1,5 @@
{
"version": "3.0.0-next.1-4-ge0c752f",
"version": "3.0.0-next.1-5-gec33a29",
"name": "nativewind",
"description": "Use Tailwindcss in your cross-platform React Native applications",
"main": "dist/index.js",

View File

@@ -1,26 +1,12 @@
import {
readFileSync,
writeFileSync,
statSync,
openSync,
rmSync,
} from "node:fs";
import process from "node:process";
import { resolve, join } from "node:path";
import findCacheDir from "find-cache-dir";
import chokidar from "chokidar";
import { resolve } from "node:path";
import type { ConfigAPI } from "@babel/core";
import type { Config } from "tailwindcss";
import resolveConfig from "tailwindcss/resolveConfig";
import resolveConfigPath from "tailwindcss/lib/util/resolveConfigPath";
import { validateConfig } from "tailwindcss/lib/util/validateConfig";
// import { getImportBlockedComponents } from "./get-import-blocked-components";
import { extractStyles } from "../postcss/extract";
import { createHash } from "node:crypto";
import { plugin } from "./plugin";
import { normalizePath } from "./normalize-path";
@@ -30,95 +16,16 @@ export interface TailwindcssReactNativeBabelOptions {
allowModuleTransform?: "*" | string[];
blockModuleTransform?: string[];
mode?: "compileAndTransform" | "compileOnly" | "transformOnly";
rem?: number;
tailwindConfigPath?: string;
tailwindConfig?: Config | undefined;
}
const cacheDirectory = findCacheDir({ name: "nativewind", create: true }) ?? "";
if (!cacheDirectory) throw new Error("Unable to secure cache directory");
const stylesFile = join(cacheDirectory, "styles.js");
const cssCacheFile = join(cacheDirectory, "styles.css");
const masterProcessLock = join(cacheDirectory, process.ppid.toString());
const nativewindStylesFile = require.resolve("nativewind/dist/styles");
export default function (
api: ConfigAPI,
options: TailwindcssReactNativeBabelOptions,
cwd: string
) {
const [newTailwindConfig, tailwindConfigPath] = resolveTailwindConfig(
api,
options
);
let tailwindConfig = newTailwindConfig;
const platform = resolvePlatform(api);
const isDevelopment = api.env("development");
let canCompile = true;
let canTransform = true;
if (options.mode === "compileOnly") {
canTransform = false;
} else if (options.mode === "transformOnly") {
canCompile = false;
} else if (platform === "web") {
canCompile = false;
}
api.cache.using(() => {
if (!canCompile) return true;
try {
openSync(masterProcessLock, "wx");
for (const eventType of [
`exit`,
`SIGINT`,
`SIGUSR1`,
`SIGUSR2`,
`uncaughtException`,
`SIGTERM`,
]) {
process.on(eventType, () => {
rmSync(masterProcessLock, { force: true });
});
}
if (process.env.NODE_ENV === "development") {
const watcher = chokidar.watch(cacheDirectory).on("change", (path) => {
if (path.endsWith(".css")) {
cssCache = readFileSync(path, "utf8");
} else if (path === tailwindConfigPath) {
// Reload the Tailwind Config
tailwindConfig = resolveTailwindConfig(api, options)[0];
fullCompile();
}
});
if (tailwindConfigPath) {
watcher.add(tailwindConfigPath);
}
}
writeFileSync(
nativewindStylesFile,
`try { require("${stylesFile}") } catch {} // ${Date.now()}`
);
fullCompile();
} catch {
/* ignore */
}
return true;
});
const safelist =
tailwindConfig.safelist && tailwindConfig.safelist.length > 0
? tailwindConfig.safelist
: ["babel-empty"];
const tailwindConfig = resolveTailwindConfig(api, options);
const content = Array.isArray(tailwindConfig.content)
? tailwindConfig.content.filter(
@@ -132,48 +39,6 @@ export default function (
normalizePath(resolve(cwd, contentFilePath))
);
let cssCache = `@tailwind components;@tailwind utilities;`;
function hotReloadStyles(filename: string) {
const styles = extractStyles(
{
...tailwindConfig,
content: [filename],
safelist,
},
cssCache
);
const hash = createHash("sha1").update(filename).digest("hex");
const cacheFilename = join(cacheDirectory, `${hash}.js`);
const styleString = JSON.stringify(styles);
writeFileSync(
cacheFilename,
`import { NativeWindStyleSheet } from "nativewind";\nNativeWindStyleSheet.create(${styleString})`
);
writeFileSync(stylesFile, `try { require("${cacheFilename}"); } catch {}`, {
flag: "a",
});
}
function fullCompile() {
const styleString = JSON.stringify(extractStyles(tailwindConfig, cssCache));
writeFileSync(
stylesFile,
`import { NativeWindStyleSheet } from "nativewind";\nNativeWindStyleSheet.create(${styleString});`
);
}
function handleCssImport(source: string) {
const css = readFileSync(source, "utf8");
if (css.includes("@tailwind")) {
cssCache = css;
writeFileSync(cssCacheFile, cssCache);
fullCompile();
}
}
// const allowModuleTransform = Array.isArray(options.allowModuleTransform)
// ? ["react-native", "react-native-web", ...options.allowModuleTransform]
// : "*";
@@ -183,13 +48,7 @@ export default function (
[
plugin,
{
canCompile,
canTransform,
contentFilePaths,
fullCompile,
handleCssImport,
hotReloadStyles,
isDevelopment,
},
],
],
@@ -197,9 +56,9 @@ export default function (
}
function resolveTailwindConfig(
api: ConfigAPI,
_: ConfigAPI,
options: TailwindcssReactNativeBabelOptions
): [Config, string | null] {
): Config {
let tailwindConfig: Config;
const userConfigPath = resolveConfigPath(
@@ -209,7 +68,6 @@ function resolveTailwindConfig(
if (userConfigPath === null) {
tailwindConfig = resolveConfig(options.tailwindConfig);
} else {
api.cache.using(() => statSync(userConfigPath).mtimeMs);
delete require.cache[require.resolve(userConfigPath)];
const newConfig = resolveConfig(require(userConfigPath));
tailwindConfig = validateConfig(newConfig);
@@ -227,45 +85,5 @@ function resolveTailwindConfig(
throw new Error("NativeWind preset was not included");
}
return [tailwindConfig, userConfigPath];
}
function resolvePlatform(api: ConfigAPI) {
const bundler = api.caller((caller) => {
if (!caller) return;
if ("bundler" in caller) {
return caller["bundler"];
}
const { name } = caller;
switch (name) {
case "metro": {
return "metro";
}
case "next-babel-turbo-loader": {
return "webpack";
}
case "babel-loader": {
return "webpack";
}
}
});
const platform = api.caller((caller) => {
if (!caller) return "unknown";
if ("platform" in caller) {
return caller["platform"];
} else if (bundler === "webpack") {
return "web";
} else {
return "unknown";
}
});
process.env.NATIVEWIND_PLATFORM = platform;
return platform;
return tailwindConfig;
}

View File

@@ -1,8 +1,6 @@
import { resolve, dirname } from "node:path";
import type { ConfigAPI, NodePath, PluginPass, Visitor } from "@babel/core";
import micromatch from "micromatch";
import { addNamed, addSideEffect } from "@babel/helper-module-imports";
import { addNamed } from "@babel/helper-module-imports";
import { TailwindcssReactNativeBabelOptions } from ".";
@@ -29,29 +27,10 @@ import {
import { normalizePath } from "./normalize-path";
export interface PluginOptions {
canCompile: boolean;
canTransform: boolean;
contentFilePaths: string[];
handleCssImport: (source: string) => void;
fullCompile: () => void;
isDevelopment: boolean;
nativewindStylesFile: string;
hotReloadStyles: (filename: string) => void;
}
export function plugin(
_: ConfigAPI,
{
contentFilePaths,
canCompile,
canTransform,
handleCssImport,
fullCompile,
isDevelopment,
hotReloadStyles,
nativewindStylesFile,
}: PluginOptions
) {
export function plugin(_: ConfigAPI, { contentFilePaths }: PluginOptions) {
const programVisitor: Visitor<
PluginPass & {
opts: TailwindcssReactNativeBabelOptions;
@@ -68,54 +47,33 @@ export function plugin(
contentFilePaths
);
if (canCompile && state.isInContent) {
path.traverse({
ImportDeclaration(path) {
if (path.node.source.value.endsWith(".css")) {
const currentDirectory = dirname(filename);
handleCssImport(
resolve(currentDirectory, path.node.source.value)
);
addSideEffect(path, `nativewind/styles`);
path.remove();
}
},
CallExpression(path) {
const callee = path.get("callee");
if (!callee.isIdentifier() || !callee.equals("name", "require")) {
return;
}
path.traverse({
ImportDeclaration(path) {
if (path.node.source.value.endsWith(".css")) {
path.remove();
}
},
CallExpression(path) {
const callee = path.get("callee");
if (!callee.isIdentifier() || !callee.equals("name", "require")) {
return;
}
const argument = path.get("arguments")[0];
if (!argument || !argument.isStringLiteral()) {
return;
}
const argument = path.get("arguments")[0];
if (!argument || !argument.isStringLiteral()) {
return;
}
if (argument.node.value.endsWith(".css")) {
const currentDirectory = dirname(filename);
handleCssImport(resolve(currentDirectory, argument.node.value));
addSideEffect(path, `nativewind/styles`);
path.remove();
}
},
});
}
if (argument.node.value.endsWith(".css")) {
path.remove();
}
},
});
},
exit(path, state) {
if (state.didTransform) {
addNamed(path, "StyledComponent", "nativewind");
}
if (state.filename === nativewindStylesFile) {
fullCompile();
} else if (
isDevelopment &&
canCompile &&
state.filename &&
state.isInContent
) {
hotReloadStyles(state.filename);
}
},
},
JSXElement(path, state) {
@@ -135,7 +93,6 @@ export function plugin(
name === "_StyledComponent" || name === "StyledComponent";
if (
!canTransform ||
!someAttributes(path, ["className", "tw"]) ||
!name ||
isWrapper ||

View File

@@ -0,0 +1,50 @@
import { writeFileSync } from "node:fs";
import { join } from "node:path";
import findCacheDir from "find-cache-dir";
import { spawn } from "node:child_process";
export interface WithNativeWindOptions {
inputPath?: string;
postcssPath?: string;
}
// We actually don't do anything to the Metro config,
// this is simply here to future proof incase we need to
export default function withNativeWind(
config: unknown,
{ inputPath: input, postcssPath }: WithNativeWindOptions
) {
const cacheDirectory = findCacheDir({ name: "nativewind", create: true });
if (!cacheDirectory) throw new Error("Unable to secure cache directory");
if (!input) {
input = join(cacheDirectory, "input.css");
writeFileSync(input, "@tailwind components;@tailwind utilities;");
}
const spawnCommands = ["tailwind", "-i", input];
if (postcssPath) {
spawnCommands.push("--postcss", postcssPath);
}
if (process.env.NODE_ENV !== "production") {
spawnCommands.push("--watch");
}
const cli = spawn("npx", spawnCommands);
cli.stdout.on("data", (data) => {
console.log(`stdout: ${data}`);
});
cli.stderr.on("data", (data) => {
console.error(`stderr: ${data}`);
});
cli.on("close", (code) => {
console.log(`child process exited with code ${code}`);
});
return config;
}