diff --git a/packages/nativewind/package.json b/packages/nativewind/package.json index eaa44f8..e271952 100644 --- a/packages/nativewind/package.json +++ b/packages/nativewind/package.json @@ -1,5 +1,5 @@ { - "version": "3.0.0-next.1", + "version": "0.0.0-experiment.1", "name": "nativewind", "description": "Use Tailwindcss in your cross-platform React Native applications", "main": "dist/index.js", @@ -35,7 +35,8 @@ "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand", "lint": "eslint .", "prepublishOnly": "npm run build", - "build": "rm -rf dist && tsc -p tsconfig.build.json" + "build": "rm -rf dist && tsc -p tsconfig.build.json", + "experiment": "npm config set git-tag-version=false;npm version $(git describe --tags);npm publish" }, "files": [ "dist/", diff --git a/packages/nativewind/src/babel/index.ts b/packages/nativewind/src/babel/index.ts index cc7c77a..24099e1 100644 --- a/packages/nativewind/src/babel/index.ts +++ b/packages/nativewind/src/babel/index.ts @@ -1,14 +1,12 @@ import { readFileSync, writeFileSync } from "node:fs"; -import { resolve, sep, posix, join, dirname, relative } from "node:path"; - -import type { ConfigAPI, NodePath, PluginPass, Visitor } from "@babel/core"; -import { parseExpression } from "@babel/parser"; +import { resolve, sep, posix, join, dirname } from "node:path"; import findCacheDir from "find-cache-dir"; import chokidar from "chokidar"; import micromatch from "micromatch"; import { addNamed, addSideEffect } from "@babel/helper-module-imports"; +import type { ConfigAPI, NodePath, PluginPass, Visitor } from "@babel/core"; import { Expression, @@ -38,7 +36,15 @@ import { validateConfig } from "tailwindcss/lib/util/validateConfig"; // import { getImportBlockedComponents } from "./get-import-blocked-components"; import { extractStyles } from "../postcss/extract"; +import { AtomRecord } from "../postcss/types"; +import { createHash } from "node:crypto"; +/** + * The Babel plugin has 3 functions + * - component transformation + * - style compilation + * - handling .css imports + */ export interface TailwindcssReactNativeBabelOptions { isInContent?: boolean; didTransform?: boolean; @@ -50,11 +56,12 @@ export interface TailwindcssReactNativeBabelOptions { tailwindConfig?: Config | undefined; } -const cacheDirectory = findCacheDir({ name: "nativewind", create: true }); +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 nativewindStylesFile = require.resolve("nativewind/dist/styles"); const watcher = process.env.NODE_ENV === "development" @@ -66,92 +73,9 @@ export default function ( options: TailwindcssReactNativeBabelOptions, cwd: string ) { - /** - * Get the users config - */ - const userConfigPath = resolveConfigPath( - options.tailwindConfig || options.tailwindConfigPath - ); - - let tailwindConfig: Config; - - 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; - - if ("platform" in caller) { - return caller["platform"]; - } else if (bundler === "webpack") { - return "web"; - } - }); - - if (platform) { - process.env.NATIVEWIND_PLATFORM = platform; - } - - if (userConfigPath === null) { - tailwindConfig = resolveConfig(options.tailwindConfig); - } else { - delete require.cache[require.resolve(userConfigPath)]; - const newConfig = resolveConfig(require(userConfigPath)); - tailwindConfig = validateConfig(newConfig); - } - - const hasPreset = tailwindConfig.presets?.some((preset) => { - return ( - preset && - ("nativewind" in preset || - ("default" in preset && "nativewind" in preset["default"])) - ); - }); - - if (!hasPreset) { - throw new Error("NativeWind preset was not included"); - } - - const safelist = - tailwindConfig.safelist && tailwindConfig.safelist.length > 0 - ? tailwindConfig.safelist - : ["babel-empty"]; - - /** - * Resolve their content paths - */ - const contentFilePaths = ( - Array.isArray(tailwindConfig.content) - ? tailwindConfig.content.filter( - (filePath): filePath is string => typeof filePath === "string" - ) - : tailwindConfig.content.files.filter( - (filePath): filePath is string => typeof filePath === "string" - ) - ).map((contentFilePath) => normalizePath(resolve(cwd, contentFilePath))); - - // const allowModuleTransform = Array.isArray(options.allowModuleTransform) - // ? ["react-native", "react-native-web", ...options.allowModuleTransform] - // : "*"; + const tailwindConfig = resolveTailwindConfig(options); + const platform = resolvePlatform(api); + const isDevelopment = api.env("development"); let canCompile = true; let canTransform = true; @@ -160,43 +84,78 @@ export default function ( canTransform = false; } else if (options.mode === "transformOnly") { canCompile = false; + } else if (platform === "web") { + canCompile = false; } - let cssCache: string | undefined; + const safelist = + tailwindConfig.safelist && tailwindConfig.safelist.length > 0 + ? tailwindConfig.safelist + : ["babel-empty"]; + + const content = Array.isArray(tailwindConfig.content) + ? tailwindConfig.content.filter( + (filePath): filePath is string => typeof filePath === "string" + ) + : tailwindConfig.content.files.filter( + (filePath): filePath is string => typeof filePath === "string" + ); + + const contentFilePaths = content.map((contentFilePath) => + normalizePath(resolve(cwd, contentFilePath)) + ); + + // const allowModuleTransform = Array.isArray(options.allowModuleTransform) + // ? ["react-native", "react-native-web", ...options.allowModuleTransform] + // : "*"; + + if (canCompile) { + writeFileSync( + nativewindStylesFile, + `try { require("${stylesFile}") } catch {} // ${Date.now()}` + ); + } + + let cssCache = `@tailwind components;@tailwind utilities;`; watcher?.on("change", (path) => { if (path.endsWith(".css")) { cssCache = readFileSync(path, "utf8"); } }); - const isDevelopment = api.env("development"); + function fullCompile() { + writeFileSync( + stylesFile, + `import { NativeWindStyleSheet } from "nativewind";\nNativeWindStyleSheet.create(${JSON.stringify( + extractStyles(tailwindConfig, cssCache) + )});` + ); + } - function replaceCssImport( - path: NodePath, - source: string, - currentDirectory: string - ) { + function handleCssImport(source: string) { const css = readFileSync(source, "utf8"); if (css.includes("@tailwind")) { - // Start watching this file as well - // watcher.add(filename); - // Write the css to disk, this will cause chokidar watchers to fire on all processes - writeFileSync(cssCacheFile, css); - // Write the new styles to disk - writeFileSync( - stylesFile, - `import { NativeWindStyleSheet } from "nativewind";\nNativeWindStyleSheet.create(${JSON.stringify( - extractStyles(tailwindConfig, css) - )});` - ); - // Replace the .css import with the stylesFile - addSideEffect(path, `./${relative(currentDirectory, stylesFile)}`); - path.remove(); - // After this has been completed, Babel will reevaluate the stylesFile, reloading the styles + cssCache = css; + writeFileSync(cssCacheFile, cssCache); + fullCompile(); } } + function hotReloadStyles(filename: string, styles: AtomRecord) { + const hash = createHash("sha1").update(filename).digest("hex"); + const cacheFilename = join(cacheDirectory, hash, ".js"); + writeFileSync( + cacheFilename, + `import { NativeWindStyleSheet } from "nativewind";\nNativeWindStyleSheet.create(${JSON.stringify( + styles + )}` + ); + writeFileSync(stylesFile, `try { require("./${filename}"); } catch {}`, { + flag: "a", + }); + } + const programVisitor: Visitor< PluginPass & { opts: TailwindcssReactNativeBabelOptions; @@ -206,7 +165,6 @@ export default function ( enter(path, state) { const filename = state.filename; if (!filename) return; - const currentDirectory = dirname(filename); state.blockList = new Set(); state.isInContent = micromatch.isMatch( @@ -217,10 +175,13 @@ export default function ( if (canCompile && state.isInContent) { path.traverse({ ImportDeclaration(path) { - const source = resolve(currentDirectory, path.node.source.value); - - if (source.endsWith(".css")) { - replaceCssImport(path, source, currentDirectory); + 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) { @@ -234,10 +195,11 @@ export default function ( return; } - const source = argument.node.value; - - if (source.endsWith(".css")) { - replaceCssImport(path, source, currentDirectory); + if (argument.node.value.endsWith(".css")) { + const currentDirectory = dirname(filename); + handleCssImport(resolve(currentDirectory, argument.node.value)); + addSideEffect(path, `nativewind/styles`); + path.remove(); } }, }); @@ -248,7 +210,9 @@ export default function ( addNamed(path, "StyledComponent", "nativewind"); } - if ( + if (state.filename === nativewindStylesFile) { + fullCompile(); + } else if ( isDevelopment && canCompile && state.filename && @@ -262,13 +226,7 @@ export default function ( }, cssCache ); - path.pushContainer( - "body", - parseExpression( - `_NativeWindStyleSheet.create(${JSON.stringify(styles)});` - ) - ); - addNamed(path, "NativeWindStyleSheet", "nativewind"); + hotReloadStyles(state.filename, styles); } }, }, @@ -369,3 +327,73 @@ function someAttributes(path: NodePath, names: string[]) { }); }); } + +function resolveTailwindConfig( + options: TailwindcssReactNativeBabelOptions +): Config { + let tailwindConfig: Config; + + const userConfigPath = resolveConfigPath( + options.tailwindConfig || options.tailwindConfigPath + ); + + if (userConfigPath === null) { + tailwindConfig = resolveConfig(options.tailwindConfig); + } else { + delete require.cache[require.resolve(userConfigPath)]; + const newConfig = resolveConfig(require(userConfigPath)); + tailwindConfig = validateConfig(newConfig); + } + + const hasPreset = tailwindConfig.presets?.some((preset) => { + return ( + preset && + ("nativewind" in preset || + ("default" in preset && "nativewind" in preset["default"])) + ); + }); + + if (!hasPreset) { + throw new Error("NativeWind preset was not included"); + } + + return tailwindConfig; +} + +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; + + if ("platform" in caller) { + return caller["platform"]; + } else if (bundler === "webpack") { + return "web"; + } + }); + + process.env.NATIVEWIND_PLATFORM = platform; + + return platform; +} diff --git a/packages/nativewind/src/index.ts b/packages/nativewind/src/index.ts index 21c55af..4bdbe10 100644 --- a/packages/nativewind/src/index.ts +++ b/packages/nativewind/src/index.ts @@ -1,3 +1,4 @@ +import "./styles"; export * from "./styled"; export * from "./theme-functions"; export { NativeWindStyleSheet } from "./style-sheet"; diff --git a/packages/nativewind/src/styles.ts b/packages/nativewind/src/styles.ts new file mode 100644 index 0000000..cb0ff5c --- /dev/null +++ b/packages/nativewind/src/styles.ts @@ -0,0 +1 @@ +export {};