├── plugin.js
├── sample
├── shim.d.ts
├── assets
│ ├── icon.png
│ ├── favicon.png
│ ├── splash.png
│ └── adaptive-icon.png
├── tsconfig.json
├── .expo-shared
│ └── assets.json
├── babel.config.js
├── .gitignore
├── Card.tsx
├── scripts
│ ├── transform-app.js
│ └── App.transformed.js
├── app.json
├── webpack.config.js
├── App.tsx
├── package.json
└── metro.config.js
├── .gitignore
├── .prettierrc
├── tsconfig.build.json
├── react-native-dark-Hero.png
├── vitest.config.js
├── .changeset
└── config.json
├── CHANGELOG.md
├── __tests__
├── data
│ ├── no-use.input.js
│ ├── no-use.output.js
│ ├── single-use.input.js
│ ├── stylesheet-import-name.input.js
│ ├── dynamic-hook-injection.input.js
│ ├── multiple-create-calls.input.js
│ ├── single-use.output.js
│ ├── stylesheet-import-name.output.js
│ ├── dynamic-hook-injection.output.js
│ ├── different-function-forms.input.js
│ ├── multiple-create-calls.output.js
│ └── different-function-forms.output.js
└── plugin.test.ts
├── tsconfig.json
├── .github
└── workflows
│ ├── check.yml
│ └── release.yml
├── shim.d.ts
├── src
├── index.tsx
└── plugin.ts
├── LICENSE
├── package.json
├── CONTRIBUTING.md
└── README.md
/plugin.js:
--------------------------------------------------------------------------------
1 | module.exports = require("./dist/plugin").default;
2 |
--------------------------------------------------------------------------------
/sample/shim.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 |
4 | # Packing artifacts
5 | *.tgz
6 | package
7 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": false,
3 | "semi": true,
4 | "trailingComma": "all"
5 | }
6 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["src/**/*.test.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/sample/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FormidableLabs/react-native-dark/HEAD/sample/assets/icon.png
--------------------------------------------------------------------------------
/sample/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FormidableLabs/react-native-dark/HEAD/sample/assets/favicon.png
--------------------------------------------------------------------------------
/sample/assets/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FormidableLabs/react-native-dark/HEAD/sample/assets/splash.png
--------------------------------------------------------------------------------
/react-native-dark-Hero.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FormidableLabs/react-native-dark/HEAD/react-native-dark-Hero.png
--------------------------------------------------------------------------------
/sample/assets/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FormidableLabs/react-native-dark/HEAD/sample/assets/adaptive-icon.png
--------------------------------------------------------------------------------
/sample/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true,
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/vitest.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | test: {
3 | deps: {
4 | inline: ["react-native"],
5 | },
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/sample/.expo-shared/assets.json:
--------------------------------------------------------------------------------
1 | {
2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
4 | }
5 |
--------------------------------------------------------------------------------
/sample/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 | return {
4 | presets: ["babel-preset-expo"],
5 | plugins: ["react-native-dark/plugin"],
6 | };
7 | };
8 |
--------------------------------------------------------------------------------
/sample/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .expo/
3 | dist/
4 | npm-debug.*
5 | *.jks
6 | *.p8
7 | *.p12
8 | *.key
9 | *.mobileprovision
10 | *.orig.*
11 | web-build/
12 |
13 | # macOS
14 | .DS_Store
15 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
3 | "changelog": [
4 | "@svitejs/changesets-changelog-github-compact",
5 | {
6 | "repo": "FormidableLabs/react-native-dark"
7 | }
8 | ],
9 | "access": "public",
10 | "baseBranch": "main"
11 | }
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # react-native-dark
2 |
3 | ## 0.1.4
4 |
5 | ### Patch Changes
6 |
7 | - fixes shim for react-native 0.73+ ([#8](https://github.com/FormidableLabs/react-native-dark/pull/8))
8 |
9 | ## 0.1.3
10 |
11 | ### Patch Changes
12 |
13 | - Adding GitHub Action workflow ([#6](https://github.com/FormidableLabs/react-native-dark/pull/6))
14 |
--------------------------------------------------------------------------------
/__tests__/data/no-use.input.js:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { View, StyleSheet } from "react-native";
3 |
4 | function App() {
5 | return React.createElement(View, {
6 | style: styles.container,
7 | });
8 | }
9 |
10 | const styles = StyleSheet.create({
11 | container: {
12 | flex: 1,
13 | backgroundColor: "white",
14 | },
15 | });
16 |
--------------------------------------------------------------------------------
/__tests__/data/no-use.output.js:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { View, StyleSheet } from "react-native";
3 |
4 | function App() {
5 | return React.createElement(View, {
6 | style: styles.container,
7 | });
8 | }
9 |
10 | const styles = StyleSheet.create({
11 | container: {
12 | flex: 1,
13 | backgroundColor: "white",
14 | },
15 | });
16 |
--------------------------------------------------------------------------------
/__tests__/data/single-use.input.js:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { View, StyleSheet } from "react-native";
3 |
4 | function App() {
5 | return React.createElement(View, { style: styles.container });
6 | }
7 |
8 | const styles = StyleSheet.create({
9 | container: {
10 | flex: 1,
11 | backgroundColor: "white",
12 | $dark: {
13 | backgroundColor: "black"
14 | }
15 | }
16 | });
17 |
--------------------------------------------------------------------------------
/__tests__/data/stylesheet-import-name.input.js:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { View, StyleSheet as MyStyleSheet } from "react-native";
3 |
4 | function App() {
5 | return React.createElement(View, { style: styles.container });
6 | }
7 |
8 | const styles = MyStyleSheet.create({
9 | container: {
10 | flex: 1,
11 | backgroundColor: "white",
12 | $dark: {
13 | backgroundColor: "black",
14 | },
15 | },
16 | });
17 |
--------------------------------------------------------------------------------
/sample/Card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { createStyleSheet } from "../dist";
3 | import { Text, View } from "react-native";
4 |
5 | export const Card = () => {
6 | return (
7 |
8 | A card!
9 |
10 | );
11 | };
12 |
13 | const styles = createStyleSheet({
14 | title: {
15 | backgroundColor: "red",
16 | borderRadius: 8,
17 | padding: 8,
18 | $dark: {
19 | backgroundColor: "pink",
20 | },
21 | },
22 | });
23 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2019",
4 | "module": "commonjs",
5 | "esModuleInterop": true,
6 | "forceConsistentCasingInFileNames": true,
7 | "declaration": true,
8 | "jsx": "react-native",
9 | "outDir": "./dist",
10 | "strict": true,
11 | "baseUrl": "./src",
12 | "rootDirs": ["./src"],
13 | "skipLibCheck": true,
14 | "downlevelIteration": true,
15 | "allowSyntheticDefaultImports": true
16 | },
17 | "include": ["src/**/*.ts", "src/**/*.tsx"]
18 | }
19 |
--------------------------------------------------------------------------------
/__tests__/data/dynamic-hook-injection.input.js:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { View, StyleSheet } from "react-native";
3 |
4 | function App() {
5 | console.log(styles.title);
6 | return React.createElement(Body);
7 | }
8 |
9 | function Body() {
10 | return React.createElement(View, { style: styles.container });
11 | }
12 |
13 | const styles = StyleSheet.create({
14 | container: {
15 | flex: 1,
16 | backgroundColor: "white",
17 | $dark: {
18 | backgroundColor: "black",
19 | },
20 | },
21 | title: {},
22 | });
23 |
--------------------------------------------------------------------------------
/.github/workflows/check.yml:
--------------------------------------------------------------------------------
1 | name: Typecheck and Unit Tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'main'
7 | pull_request:
8 |
9 | jobs:
10 | ci-check:
11 | name: Typecheck and Unit Tests
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v2
15 | - uses: actions/setup-node@v2
16 | with:
17 | node-version: 16
18 | - name: Install dependencies
19 | run: yarn install
20 | - name: Typecheck
21 | run: yarn typecheck
22 | - name: Unit Tests
23 | run: yarn test
24 |
--------------------------------------------------------------------------------
/__tests__/data/multiple-create-calls.input.js:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { View, Text, StyleSheet } from "react-native";
3 |
4 | function App() {
5 | return React.createElement(
6 | View,
7 | { style: styles.container },
8 | React.createElement(Text, { style: otherStyles.title }),
9 | );
10 | }
11 |
12 | const styles = StyleSheet.create({
13 | container: {
14 | flex: 1,
15 | backgroundColor: "white",
16 | $dark: {
17 | backgroundColor: "black",
18 | },
19 | },
20 | });
21 |
22 | const otherStyles = StyleSheet.create({
23 | title: {
24 | color: "black",
25 | $dark: {
26 | color: "white",
27 | },
28 | },
29 | });
30 |
--------------------------------------------------------------------------------
/sample/scripts/transform-app.js:
--------------------------------------------------------------------------------
1 | const fs = require("node:fs/promises");
2 | const path = require("node:path");
3 | const babel = require("@babel/core");
4 | const prettier = require("prettier");
5 |
6 | const main = async () => {
7 | const AppContent = await fs.readFile(
8 | path.join(__dirname, "..", "App.tsx"),
9 | "utf-8",
10 | );
11 |
12 | const transformed = babel.transform(AppContent, {
13 | // presets: ["babel-preset-expo"],
14 | // plugins: ["react-native-dark/babel"],
15 | filename: "App.tsx",
16 | }).code;
17 |
18 | const formatted = prettier.format(transformed);
19 |
20 | await fs.writeFile(path.join(__dirname, "App.transformed.js"), formatted);
21 | };
22 |
23 | main();
24 |
--------------------------------------------------------------------------------
/shim.d.ts:
--------------------------------------------------------------------------------
1 | import "react-native";
2 | import { type ImageStyle, type TextStyle, type ViewStyle } from "react-native";
3 |
4 | module "react-native" {
5 | export * from "react-native";
6 |
7 | export namespace StyleSheet {
8 | type AddDark = T & { $dark?: T };
9 | type NamedStylesWithDark = {
10 | [P in keyof T]:
11 | | AddDark
12 | | AddDark
13 | | AddDark;
14 | };
15 |
16 | /**
17 | * Create a stylesheet with dark-mode support via $dark property on style objects.
18 | */
19 | export function create<
20 | T extends NamedStylesWithDark | NamedStylesWithDark,
21 | >(styles: T & NamedStylesWithDark): T;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/__tests__/data/single-use.output.js:
--------------------------------------------------------------------------------
1 | import { useDarkMode as _useDarkMode } from "react-native-dark";
2 | import * as React from "react";
3 | import { View, StyleSheet } from "react-native";
4 |
5 | function App() {
6 | const _REACT_NATIVE_DARK_isDark = _useDarkMode();
7 |
8 | return React.createElement(View, {
9 | style: _REACT_NATIVE_DARK_isDark
10 | ? __styles__container__$dark
11 | : styles.container,
12 | });
13 | }
14 |
15 | const styles = StyleSheet.create({
16 | container: {
17 | flex: 1,
18 | backgroundColor: "white",
19 | },
20 | __container__$dark: {
21 | backgroundColor: "black",
22 | },
23 | });
24 |
25 | const __styles__container__$dark = StyleSheet.compose(
26 | styles.container,
27 | styles.__container__$dark,
28 | );
29 |
--------------------------------------------------------------------------------
/__tests__/data/stylesheet-import-name.output.js:
--------------------------------------------------------------------------------
1 | import { useDarkMode as _useDarkMode } from "react-native-dark";
2 | import * as React from "react";
3 | import { View, StyleSheet as MyStyleSheet } from "react-native";
4 |
5 | function App() {
6 | const _REACT_NATIVE_DARK_isDark = _useDarkMode();
7 |
8 | return React.createElement(View, {
9 | style: _REACT_NATIVE_DARK_isDark
10 | ? __styles__container__$dark
11 | : styles.container,
12 | });
13 | }
14 |
15 | const styles = MyStyleSheet.create({
16 | container: {
17 | flex: 1,
18 | backgroundColor: "white",
19 | },
20 | __container__$dark: {
21 | backgroundColor: "black",
22 | },
23 | });
24 |
25 | const __styles__container__$dark = MyStyleSheet.compose(
26 | styles.container,
27 | styles.__container__$dark,
28 | );
29 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { useColorScheme } from "react-native";
3 |
4 | export type ColorSchemeMode = "light" | "dark" | "auto";
5 | const DarkModeContext = React.createContext("auto");
6 |
7 | export const useDarkMode = () => {
8 | const colorMode = React.useContext(DarkModeContext) || "auto";
9 | const deviceColorScheme = useColorScheme();
10 |
11 | return (
12 | colorMode === "dark" ||
13 | (colorMode === "auto" && deviceColorScheme === "dark")
14 | );
15 | };
16 |
17 | export const DarkModeProvider = ({
18 | colorMode,
19 | children,
20 | }: React.PropsWithChildren<{ colorMode: ColorSchemeMode }>) => {
21 | return (
22 |
23 | {children}
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/sample/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "sample",
4 | "slug": "sample",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/icon.png",
8 | "userInterfaceStyle": "automatic",
9 | "splash": {
10 | "image": "./assets/splash.png",
11 | "resizeMode": "contain",
12 | "backgroundColor": "#ffffff"
13 | },
14 | "updates": {
15 | "fallbackToCacheTimeout": 0
16 | },
17 | "assetBundlePatterns": [
18 | "**/*"
19 | ],
20 | "ios": {
21 | "supportsTablet": true
22 | },
23 | "android": {
24 | "adaptiveIcon": {
25 | "foregroundImage": "./assets/adaptive-icon.png",
26 | "backgroundColor": "#FFFFFF"
27 | }
28 | },
29 | "web": {
30 | "favicon": "./assets/favicon.png"
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/__tests__/data/dynamic-hook-injection.output.js:
--------------------------------------------------------------------------------
1 | import { useDarkMode as _useDarkMode } from "react-native-dark";
2 | import * as React from "react";
3 | import { View, StyleSheet } from "react-native";
4 |
5 | function App() {
6 | console.log(styles.title);
7 | return React.createElement(Body);
8 | }
9 |
10 | function Body() {
11 | const _REACT_NATIVE_DARK_isDark = _useDarkMode();
12 |
13 | return React.createElement(View, {
14 | style: _REACT_NATIVE_DARK_isDark
15 | ? __styles__container__$dark
16 | : styles.container,
17 | });
18 | }
19 |
20 | const styles = StyleSheet.create({
21 | container: {
22 | flex: 1,
23 | backgroundColor: "white",
24 | },
25 | title: {},
26 | __container__$dark: {
27 | backgroundColor: "black",
28 | },
29 | });
30 |
31 | const __styles__container__$dark = StyleSheet.compose(
32 | styles.container,
33 | styles.__container__$dark,
34 | );
35 |
--------------------------------------------------------------------------------
/sample/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const createExpoWebpackConfigAsync = require('@expo/webpack-config');
3 | const { resolver } = require('./metro.config');
4 |
5 | const root = path.resolve(__dirname, '..');
6 | const node_modules = path.join(__dirname, 'node_modules');
7 |
8 | module.exports = async function (env, argv) {
9 | const config = await createExpoWebpackConfigAsync(env, argv);
10 |
11 | config.module.rules.push({
12 | test: /\.(js|jsx|ts|tsx)$/,
13 | include: path.resolve(root, 'src'),
14 | use: 'babel-loader',
15 | });
16 |
17 | // We need to make sure that only one version is loaded for peerDependencies
18 | // So we alias them to the versions in example's node_modules
19 | Object.assign(config.resolve.alias, {
20 | ...resolver.extraNodeModules,
21 | 'react-native-web': path.join(node_modules, 'react-native-web'),
22 | });
23 |
24 | return config;
25 | };
26 |
--------------------------------------------------------------------------------
/sample/App.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { StyleSheet, Text, View } from "react-native";
3 | import { DarkModeProvider } from "react-native-dark";
4 |
5 | const App = () => {
6 | return (
7 |
8 |
9 |
10 | );
11 | };
12 |
13 | const Body = () => {
14 | return (
15 |
16 | Hello world!
17 |
18 | );
19 | };
20 |
21 | export default App;
22 |
23 | const styles = StyleSheet.create({
24 | container: {
25 | flex: 1,
26 | justifyContent: "center",
27 | alignItems: "center",
28 | backgroundColor: "white",
29 |
30 | $dark: {
31 | backgroundColor: "black",
32 | },
33 | },
34 |
35 | title: {
36 | color: "black",
37 | fontSize: 24,
38 |
39 | $dark: {
40 | color: "white",
41 | fontWeight: "bold",
42 | },
43 | },
44 | });
45 |
--------------------------------------------------------------------------------
/sample/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sample",
3 | "version": "1.0.0",
4 | "main": "node_modules/expo/AppEntry.js",
5 | "scripts": {
6 | "start": "expo start --clear",
7 | "android": "expo start --android",
8 | "ios": "expo start --ios",
9 | "web": "expo start --web"
10 | },
11 | "dependencies": {
12 | "expo": "~46.0.9",
13 | "expo-status-bar": "~1.4.0",
14 | "nativewind": "^2.0.9",
15 | "prettier": "^2.7.1",
16 | "react": "18.0.0",
17 | "react-dom": "18.0.0",
18 | "react-native": "0.69.5",
19 | "react-native-dark": "./rn-dark.tgz",
20 | "react-native-web": "~0.18.7"
21 | },
22 | "devDependencies": {
23 | "@babel/cli": "^7.18.10",
24 | "@babel/core": "^7.12.9",
25 | "@expo/metro-config": "^0.3.22",
26 | "@expo/webpack-config": "^0.17.2",
27 | "@types/react": "~18.0.14",
28 | "@types/react-native": "~0.69.1",
29 | "typescript": "~4.3.5"
30 | },
31 | "private": true
32 | }
33 |
--------------------------------------------------------------------------------
/__tests__/data/different-function-forms.input.js:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { View, StyleSheet } from "react-native";
3 |
4 | function App() {
5 | return React.createElement(View, {}, [
6 | React.createElement(FuncDeclaration),
7 | React.createElement(FE),
8 | React.createElement(ArrowFunc),
9 | React.createElement(ArrowFuncImplicitReturn),
10 | ]);
11 | }
12 |
13 | function FuncDeclaration() {
14 | return React.createElement(View, { style: styles.container });
15 | }
16 |
17 | const FE = function FuncExpression() {
18 | return React.createElement(View, { style: styles.container });
19 | };
20 |
21 | const ArrowFunc = () => {
22 | return React.createElement(View, { style: styles.container });
23 | };
24 |
25 | const ArrowFuncImplicitReturn = () =>
26 | React.createElement(View, { style: styles.container });
27 |
28 | const styles = StyleSheet.create({
29 | container: {
30 | flex: 1,
31 | backgroundColor: "white",
32 | $dark: {
33 | backgroundColor: "black",
34 | },
35 | },
36 | });
37 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | push:
4 | branches:
5 | - main
6 | jobs:
7 | release:
8 | name: Release
9 | runs-on: ubuntu-latest
10 | permissions:
11 | contents: write
12 | id-token: write
13 | issues: write
14 | repository-projects: write
15 | deployments: write
16 | packages: write
17 | pull-requests: write
18 | steps:
19 | - uses: actions/checkout@v2
20 | - uses: actions/setup-node@v3
21 | with:
22 | node-version: 18
23 |
24 | - name: Install dependencies
25 | run: yarn install --frozen-lockfile
26 |
27 | - name: Build Package
28 | run: yarn build
29 |
30 | - name: Unit Tests
31 | run: yarn test
32 |
33 | - name: PR or Publish
34 | id: changesets
35 | uses: changesets/action@v1
36 | with:
37 | version: yarn changeset version
38 | publish: yarn changeset publish
39 | env:
40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
41 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Formidable
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/sample/metro.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const escape = require("escape-string-regexp");
3 | const { getDefaultConfig } = require("@expo/metro-config");
4 | const exclusionList = require("metro-config/src/defaults/exclusionList");
5 | const pak = require("../package.json");
6 |
7 | const root = path.resolve(__dirname, "..");
8 |
9 | const modules = Object.keys({
10 | ...pak.peerDependencies,
11 | });
12 |
13 | const defaultConfig = getDefaultConfig(__dirname);
14 |
15 | module.exports = {
16 | ...defaultConfig,
17 |
18 | projectRoot: __dirname,
19 | watchFolders: [root],
20 |
21 | // We need to make sure that only one version is loaded for peerDependencies
22 | // So we block them at the root, and alias them to the versions in example's node_modules
23 | resolver: {
24 | ...defaultConfig.resolver,
25 |
26 | blacklistRE: exclusionList(
27 | modules.map(
28 | (m) =>
29 | new RegExp(`^${escape(path.join(root, "node_modules", m))}\\/.*$`),
30 | ),
31 | ),
32 |
33 | extraNodeModules: modules.reduce((acc, name) => {
34 | acc[name] = path.join(__dirname, "node_modules", name);
35 | return acc;
36 | }, {}),
37 | },
38 | };
39 |
--------------------------------------------------------------------------------
/__tests__/data/multiple-create-calls.output.js:
--------------------------------------------------------------------------------
1 | import { useDarkMode as _useDarkMode } from "react-native-dark";
2 | import * as React from "react";
3 | import { View, Text, StyleSheet } from "react-native";
4 |
5 | function App() {
6 | const _REACT_NATIVE_DARK_isDark = _useDarkMode();
7 |
8 | return React.createElement(
9 | View,
10 | {
11 | style: _REACT_NATIVE_DARK_isDark
12 | ? __styles__container__$dark
13 | : styles.container,
14 | },
15 | React.createElement(Text, {
16 | style: _REACT_NATIVE_DARK_isDark
17 | ? __otherStyles__title__$dark
18 | : otherStyles.title,
19 | }),
20 | );
21 | }
22 |
23 | const styles = StyleSheet.create({
24 | container: {
25 | flex: 1,
26 | backgroundColor: "white",
27 | },
28 | __container__$dark: {
29 | backgroundColor: "black",
30 | },
31 | });
32 | const otherStyles = StyleSheet.create({
33 | title: {
34 | color: "black",
35 | },
36 | __title__$dark: {
37 | color: "white",
38 | },
39 | });
40 |
41 | const __styles__container__$dark = StyleSheet.compose(
42 | styles.container,
43 | styles.__container__$dark,
44 | );
45 |
46 | const __otherStyles__title__$dark = StyleSheet.compose(
47 | otherStyles.title,
48 | otherStyles.__title__$dark,
49 | );
50 |
--------------------------------------------------------------------------------
/__tests__/plugin.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from "vitest";
2 | import plugin from "../src/plugin";
3 | import babel from "@babel/core";
4 | import * as fs from "node:fs";
5 | import * as path from "node:path";
6 | import { format } from "prettier";
7 |
8 | // Get test names from ./data directory
9 | const testFileNames = Array.from(
10 | new Set(
11 | fs
12 | .readdirSync(path.join(__dirname, "data"))
13 | .map((fn) => fn.replace(/\.(input|output)\.js$/, "")),
14 | ),
15 | );
16 |
17 | // Grab test input/output data
18 | const testData = testFileNames.reduce>(
19 | (acc, name) => {
20 | const input = fs.readFileSync(
21 | path.join(__dirname, "data", `${name}.input.js`),
22 | "utf-8",
23 | );
24 | const output = fs.readFileSync(
25 | path.join(__dirname, "data", `${name}.output.js`),
26 | "utf-8",
27 | );
28 | acc[name] = [input, output];
29 | return acc;
30 | },
31 | {},
32 | );
33 |
34 | /**
35 | * Transform code with plugin
36 | */
37 | const transform = (code: string) => {
38 | const transformed = babel.transform(code, {
39 | filename: "foobar.ts",
40 | plugins: [plugin],
41 | });
42 |
43 | const output = transformed?.code || "";
44 | return output;
45 | };
46 |
47 | describe("babel-plugin", () => {
48 | it.each(Object.keys(testData))("Transforms %s appropriately", (key) => {
49 | expect(prettify(transform(testData[key][0]))).to.equal(
50 | prettify(testData[key][1]),
51 | );
52 | });
53 | });
54 |
55 | const prettify = (str: string) => format(str, { parser: "babel" });
56 |
--------------------------------------------------------------------------------
/__tests__/data/different-function-forms.output.js:
--------------------------------------------------------------------------------
1 | import { useDarkMode as _useDarkMode } from "react-native-dark";
2 | import * as React from "react";
3 | import { View, StyleSheet } from "react-native";
4 |
5 | function App() {
6 | return React.createElement(View, {}, [
7 | React.createElement(FuncDeclaration),
8 | React.createElement(FE),
9 | React.createElement(ArrowFunc),
10 | React.createElement(ArrowFuncImplicitReturn),
11 | ]);
12 | }
13 |
14 | function FuncDeclaration() {
15 | const _REACT_NATIVE_DARK_isDark = _useDarkMode();
16 |
17 | return React.createElement(View, {
18 | style: _REACT_NATIVE_DARK_isDark
19 | ? __styles__container__$dark
20 | : styles.container,
21 | });
22 | }
23 |
24 | const FE = function FuncExpression() {
25 | const _REACT_NATIVE_DARK_isDark2 = _useDarkMode();
26 |
27 | return React.createElement(View, {
28 | style: _REACT_NATIVE_DARK_isDark2
29 | ? __styles__container__$dark
30 | : styles.container,
31 | });
32 | };
33 |
34 | const ArrowFunc = () => {
35 | const _REACT_NATIVE_DARK_isDark3 = _useDarkMode();
36 |
37 | return React.createElement(View, {
38 | style: _REACT_NATIVE_DARK_isDark3
39 | ? __styles__container__$dark
40 | : styles.container,
41 | });
42 | };
43 |
44 | const ArrowFuncImplicitReturn = () => {
45 | const _REACT_NATIVE_DARK_isDark4 = _useDarkMode();
46 |
47 | return React.createElement(View, {
48 | style: _REACT_NATIVE_DARK_isDark4
49 | ? __styles__container__$dark
50 | : styles.container,
51 | });
52 | };
53 |
54 | const styles = StyleSheet.create({
55 | container: {
56 | flex: 1,
57 | backgroundColor: "white",
58 | },
59 | __container__$dark: {
60 | backgroundColor: "black",
61 | },
62 | });
63 |
64 | const __styles__container__$dark = StyleSheet.compose(
65 | styles.container,
66 | styles.__container__$dark,
67 | );
68 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-dark",
3 | "author": "Formidable",
4 | "private": false,
5 | "description": "Tiny wrapper around React Native's StyleSheet.create API to easily support dark mode.",
6 | "keywords": [
7 | "react",
8 | "react-native",
9 | "dark-mode"
10 | ],
11 | "version": "0.1.4",
12 | "main": "dist/index.js",
13 | "types": "dist/index.d.ts",
14 | "files": [
15 | "dist/",
16 | "plugin.js",
17 | "shim.d.ts"
18 | ],
19 | "repository": {
20 | "url": "https://github.com/FormidableLabs/react-native-dark/"
21 | },
22 | "homepage": "https://github.com/FormidableLabs/react-native-dark/",
23 | "scripts": {
24 | "build": "rm -rf dist && tsc -p ./tsconfig.build.json",
25 | "dev": "tsc --watch",
26 | "typecheck": "tsc --noEmit",
27 | "test": "vitest run",
28 | "test:watch": "vitest",
29 | "prepublishOnly": "yarn test && yarn build",
30 | "prepack": "yarn build",
31 | "preversion": "yarn test"
32 | },
33 | "publishConfig": {
34 | "provenance": true
35 | },
36 | "license": "MIT",
37 | "peerDependencies": {
38 | "react": ">=16.0.0",
39 | "react-native": ">=0.66.4"
40 | },
41 | "dependencies": {
42 | "@babel/core": "^7.19.0",
43 | "@babel/helper-module-imports": "^7.18.6"
44 | },
45 | "devDependencies": {
46 | "@babel/code-frame": "^7.18.6",
47 | "@babel/preset-env": "^7.19.0",
48 | "@babel/preset-react": "^7.18.6",
49 | "@babel/types": "^7.19.0",
50 | "@changesets/cli": "^2.26.1",
51 | "@svitejs/changesets-changelog-github-compact": "^0.1.1",
52 | "@testing-library/react-hooks": "^8.0.1",
53 | "@testing-library/react-native": "^10.1.1",
54 | "@types/babel__core": "^7.1.19",
55 | "@types/prettier": "^2.7.0",
56 | "@types/react": "^18.0.15",
57 | "@types/react-native": "^0.69.2",
58 | "babel-preset-expo": "^9.2.0",
59 | "concurrently": "^7.2.2",
60 | "nodemon": "^2.0.19",
61 | "prettier": "^2.7.1",
62 | "react": "^18.2.0",
63 | "react-native": "0.68.2",
64 | "react-test-renderer": "^18.2.0",
65 | "typescript": "~4.3.5",
66 | "vitest": "^0.18.0"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Thanks for contributing to react-native-dark!
4 |
5 | ## Development
6 |
7 | To setup your environment:
8 |
9 | ```sh
10 | $ yarn install
11 | $ yarn typecheck
12 | $ yarn test
13 |
14 | ```
15 |
16 | ### Using changesets
17 |
18 | Our official release path is to use automation to perform the actual publishing of our packages. The steps are to:
19 |
20 | 1. A human developer adds a changeset. Ideally this is as a part of a PR that will have a version impact on a package.
21 | 2. On merge of a PR our automation system opens a "Version Packages" PR.
22 | 3. On merging the "Version Packages" PR, the automation system publishes the packages.
23 |
24 | Here are more details:
25 |
26 | ### Add a changeset
27 |
28 | When you would like to add a changeset (which creates a file indicating the type of change), in your branch/PR issue this command:
29 |
30 | ```sh
31 | $ yarn changeset
32 | ```
33 |
34 | to produce an interactive menu. Navigate the packages with arrow keys and hit `` to select 1+ packages. Hit `` when done. Select semver versions for packages and add appropriate messages. From there, you'll be prompted to enter a summary of the change. Some tips for this summary:
35 |
36 | 1. Aim for a single line, 1+ sentences as appropriate.
37 | 2. Include issue links in GH format (e.g. `#123`).
38 | 3. You don't need to reference the current pull request or whatnot, as that will be added later automatically.
39 |
40 | After this, you'll see a new uncommitted file in `.changesets` like:
41 |
42 | ```sh
43 | $ git status
44 | # ....
45 | Untracked files:
46 | (use "git add ..." to include in what will be committed)
47 | .changeset/flimsy-pandas-marry.md
48 | ```
49 |
50 | Review the file, make any necessary adjustments, and commit it to source. When we eventually do a package release, the changeset notes and version will be incorporated!
51 |
52 | ### Creating versions
53 |
54 | On a merge of a feature PR, the changesets GitHub action will open a new PR titled `"Version Packages"`. This PR is automatically kept up to date with additional PRs with changesets. So, if you're not ready to publish yet, just keep merging feature PRs and then merge the version packages PR later.
55 |
56 | ### Publishing packages
57 |
58 | On the merge of a version packages PR, the changesets GitHub action will publish the packages to npm.
59 |
60 | ### Manually Releasing a new version to NPM
61 |
62 |
63 |
64 | Only for project administrators
65 |
66 |
67 | 1. Update `CHANGELOG.md`, following format for previous versions
68 | 2. Commit as "Changes for version VERSION"
69 | 3. Run `npm version patch` (or `minor|major|VERSION`) to run tests and lint,
70 | build published directories, then update `package.json` + add a git tag.
71 | 4. Run `npm publish` and publish to NPM if all is well.
72 | 5. Run `git push && git push --tags`
73 |
74 |
75 |
--------------------------------------------------------------------------------
/sample/scripts/App.transformed.js:
--------------------------------------------------------------------------------
1 | Object.defineProperty(exports, "__esModule", { value: true });
2 | exports.default = App;
3 | var _reactNativeDark = require("react-native-dark");
4 | var React = _interopRequireWildcard(require("react"));
5 | var _reactNative = require("react-native");
6 | var _jsxRuntime = require("react/jsx-runtime");
7 | function _getRequireWildcardCache(nodeInterop) {
8 | if (typeof WeakMap !== "function") return null;
9 | var cacheBabelInterop = new WeakMap();
10 | var cacheNodeInterop = new WeakMap();
11 | return (_getRequireWildcardCache = function _getRequireWildcardCache(
12 | nodeInterop
13 | ) {
14 | return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
15 | })(nodeInterop);
16 | }
17 | function _interopRequireWildcard(obj, nodeInterop) {
18 | if (!nodeInterop && obj && obj.__esModule) {
19 | return obj;
20 | }
21 | if (obj === null || (typeof obj !== "object" && typeof obj !== "function")) {
22 | return { default: obj };
23 | }
24 | var cache = _getRequireWildcardCache(nodeInterop);
25 | if (cache && cache.has(obj)) {
26 | return cache.get(obj);
27 | }
28 | var newObj = {};
29 | var hasPropertyDescriptor =
30 | Object.defineProperty && Object.getOwnPropertyDescriptor;
31 | for (var key in obj) {
32 | if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
33 | var desc = hasPropertyDescriptor
34 | ? Object.getOwnPropertyDescriptor(obj, key)
35 | : null;
36 | if (desc && (desc.get || desc.set)) {
37 | Object.defineProperty(newObj, key, desc);
38 | } else {
39 | newObj[key] = obj[key];
40 | }
41 | }
42 | }
43 | newObj.default = obj;
44 | if (cache) {
45 | cache.set(obj, newObj);
46 | }
47 | return newObj;
48 | }
49 | function App() {
50 | return (0, _jsxRuntime.jsxs)(_reactNative.View, {
51 | children: [
52 | (0, _jsxRuntime.jsx)(Header, {}),
53 | (0, _jsxRuntime.jsx)(Body, {}),
54 | ],
55 | });
56 | }
57 | var Header = function Header() {
58 | var _REACT_NATIVE_DARK_isDark = (0, _reactNativeDark.useDarkMode)();
59 | return (0, _jsxRuntime.jsx)(_reactNative.View, {
60 | style: _REACT_NATIVE_DARK_isDark ? __styles__banner__$dark : styles.banner,
61 | });
62 | };
63 | var Body = function Body() {
64 | var _REACT_NATIVE_DARK_isDark2 = (0, _reactNativeDark.useDarkMode)();
65 | return (0, _jsxRuntime.jsx)(_reactNative.Text, {
66 | style: _REACT_NATIVE_DARK_isDark2 ? __styles__title__$dark : styles.title,
67 | });
68 | };
69 | var Footer = function Footer() {
70 | var _REACT_NATIVE_DARK_isDark3 = (0, _reactNativeDark.useDarkMode)();
71 | return (0, _jsxRuntime.jsx)(_reactNative.View, {
72 | style: _REACT_NATIVE_DARK_isDark3
73 | ? __otherStyles__foo__$dark
74 | : otherStyles.foo,
75 | });
76 | };
77 | var styles = _reactNative.StyleSheet.create({
78 | container: { backgroundColor: "red" },
79 | title: { fontSize: 24 },
80 | banner: { padding: 8 },
81 | __container__$dark: { fontWeight: "bold" },
82 | __title__$dark: { fontSize: 18 },
83 | __banner__$dark: { padding: 12 },
84 | });
85 | var otherStyles = _reactNative.StyleSheet.create({
86 | foo: { backgroundColor: "red" },
87 | __foo__$dark: { backgroundColor: "blue" },
88 | });
89 | var __styles__container__$dark = _reactNative.StyleSheet.compose(
90 | styles.container,
91 | styles.__container__$dark
92 | );
93 | var __styles__title__$dark = _reactNative.StyleSheet.compose(
94 | styles.title,
95 | styles.__title__$dark
96 | );
97 | var __styles__banner__$dark = _reactNative.StyleSheet.compose(
98 | styles.banner,
99 | styles.__banner__$dark
100 | );
101 | var __otherStyles__foo__$dark = _reactNative.StyleSheet.compose(
102 | otherStyles.foo,
103 | otherStyles.__foo__$dark
104 | );
105 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://formidable.com/open-source/)
2 |
3 | `react-native-dark` is a minor augmentation of `StyleSheet.create` to support dynamic dark-mode styling with little hassle, made possible by babel.
4 |
5 | A little, illustrative example:
6 |
7 | ```tsx
8 | import { StyleSheet, Text, View } from "react-native";
9 |
10 | export default function App() {
11 | return (
12 |
13 | Hello, world!
14 |
15 | );
16 | }
17 |
18 | const styles = createStyleSheet({
19 | container: {
20 | flex: 1,
21 | backgroundColor: "white",
22 | // 🎉 dark mode 🎉
23 | $dark: {
24 | backgroundColor: "black",
25 | },
26 | },
27 |
28 | title: {
29 | color: "black",
30 | // 🎉 dark mode 🎉
31 | $dark: {
32 | color: "white",
33 | },
34 | },
35 | });
36 | ```
37 |
38 | ## Setup
39 |
40 | Setup involves three steps.
41 |
42 | ### Step 1: Installation
43 |
44 | From a React Native (or Expo) project, install `react-native-dark` from npm:
45 |
46 | ```shell
47 | npm install react-native-dark # npm
48 | yarn add react-native-dark # yarn
49 | pnpm add react-native-dark # pnpm
50 | ```
51 |
52 | ### Step 2: Add the babel plugin
53 |
54 | In your babel configuration (in e.g. `babel.config.js`), add the `react-native-dark` babel plugin:
55 |
56 | ```js
57 | module.exports = function() {
58 | return {
59 | // ...
60 | plugins: ["react-native-dark/plugin"], // 👈 add this
61 | };
62 | };
63 | ```
64 |
65 | ### Step 3: Add the TypeScript shim for `StyleSheet.create`
66 |
67 | We'll also "shim" the type for `StyleSheet.create` to enhance the developer experience. Add a declaration file to your project, such as `shim.d.ts` and add the following line:
68 |
69 | ```ts
70 | ///
71 | ```
72 |
73 | ### Step 4: Go to town!
74 |
75 | You're ready to start adding dark-mode styles to your app! See below for more details on usage.
76 |
77 | ## Usage
78 |
79 | The babel plugin and TS shim were built to make adding dark-mode support to your app as easy as just declaring dark-mode styles in your stylesheets. In a standard style declaration, just add a `$dark` field with the styles to be applied in dark mode! These styles will be applied _on top_ of the baseline styles.
80 |
81 | ```ts
82 | import { StyleSheet } from "react-native";
83 |
84 | // ...
85 |
86 | const styles = StyleSheet.create({
87 | card: {
88 | padding: 8,
89 | borderRadius: 8,
90 | backgroundColor: "lightblue",
91 |
92 | // 🪄 magic happens here 🪄
93 | $dark: {
94 | backgroundColor: "blue"
95 | }
96 | }
97 | });
98 | ```
99 |
100 | Now when you call `styles.card` within your function components, the value will be automagically dynamic based on color scheme preference.
101 |
102 | ### Manually setting color mode
103 |
104 | By default, `$dark` styles will be applied when the user's device color scheme preference is set to `dark`. However, you can manually override this behavior by wrapping a component tree in `DarkModeProvider` from `react-native-dark`.
105 |
106 | ```tsx
107 | import { DarkModeProvider } from "react-native-dark";
108 |
109 | // Example of forcing dark mode and ignore user's color scheme preference
110 | const App = () => {
111 | return (
112 |
113 |
114 |
115 | )
116 | }
117 | ```
118 |
119 | The `DarkModeProvider` has a single `colorMode` prop that can accept:
120 |
121 | - `"auto"` (default) to respect user's color scheme preference;
122 | - `"light"` to force light mode;
123 | - `"dark"` to force dark mode.
124 |
125 | ## 🦄 Magical, but not magic
126 |
127 | The babel plugin does the heavy lifting here and will turn code like the following:
128 |
129 | ```tsx
130 | import { StyleSheet, View } from "react-native";
131 |
132 | export const App = () => {
133 | return ;
134 | }
135 |
136 | const styles = StyleSheet.create({
137 | container: {
138 | flex: 1,
139 | backgroundColor: "white",
140 |
141 | $dark: {
142 | backgroundColor: "black"
143 | }
144 | }
145 | });
146 | ```
147 |
148 | into something like this:
149 |
150 | ```tsx
151 | import { StyleSheet, View } from "react-native";
152 | import { useDarkMode } from "react-native-dark";
153 |
154 | export const App = () => {
155 | const isDark = useDarkMode();
156 |
157 | return ;
158 | }
159 |
160 | const styles = StyleSheet.create({
161 | container: {
162 | flex: 1,
163 | backgroundColor: "white",
164 | },
165 | __container__$dark: {
166 | backgroundColor: "black"
167 | }
168 | });
169 |
170 | const __styles__container__$dark = StyleSheet.compose(styles.container, styles.__container__$dark);
171 | ```
172 |
173 | This is a reasonable and performant approach that you might take _by hand_ if you were implementing dark mode by hand. `react-native-dark` just cuts out the extra code for you. This, however, comes with a limitation or two...
174 |
175 | ### Limitations
176 |
177 | 1. Styles should be defined in the same file that they are referenced. E.g., don't import/export your styles object – define them in the same file that they're used.
178 | 1. The dynamic support is handled by the `useColorScheme` hook from React Native, therefore this library only currently supports function components.
179 | 1. Who knows, we'll probably find more limitations as we go 🤷
180 |
181 |
182 |
183 |
184 |
--------------------------------------------------------------------------------
/src/plugin.ts:
--------------------------------------------------------------------------------
1 | import * as babel from "@babel/core";
2 | import type { PluginObj, NodePath } from "@babel/core";
3 | // @ts-ignore
4 | import { addNamed } from "@babel/helper-module-imports";
5 | import {
6 | ArrowFunctionExpression,
7 | BlockStatement,
8 | FunctionDeclaration,
9 | FunctionExpression,
10 | Identifier,
11 | MemberExpression,
12 | ObjectProperty,
13 | } from "@babel/types";
14 |
15 | /**
16 | * We want to transform:
17 | * const styles = StyleSheet.create({foo: {$dark: {}}})
18 | * into
19 | * const styles = StyleSheet.create({ foo: {}, __foo__$dark: {} })
20 | * by extracting out the dark styles.
21 | *
22 | * Then we want to compose base/dark styles by creating a variable like:
23 | * const __styles__foo__$dark = StyleSheet.compose(styles.foo, styles.__foo__$dark);
24 | *
25 | * Then we want to find functions that reference this styles.foo (etc) property and:
26 | * - inject a const __isDark = useDarkMode(); into the top
27 | * - replace styles.foo with (!__isDark ? styles.foo : __styles__foo__$dark)
28 | */
29 |
30 | export default function ({ types: t }: typeof babel): PluginObj {
31 | return {
32 | visitor: {
33 | Program: {
34 | enter(programPath) {
35 | /**
36 | * Step 1: Check if `StyleSheet` is imported from React Native and `StyleSheet.create` is called with a $dark
37 | */
38 | let StyleSheetName: string | null = null;
39 | programPath.traverse({
40 | ImportDeclaration(importPath) {
41 | if (importPath.node.source.value === "react-native") {
42 | for (let spec of importPath.node.specifiers) {
43 | if (
44 | t.isImportSpecifier(spec) &&
45 | t.isIdentifier(spec.imported) &&
46 | spec.imported.name === "StyleSheet" &&
47 | t.isIdentifier(spec.local)
48 | ) {
49 | StyleSheetName = spec.local.name;
50 | }
51 | }
52 | }
53 | },
54 | });
55 | if (!StyleSheetName) return;
56 |
57 | /**
58 | * Step 2: Check for a const [styles] = StyleSheet.create({ [property]: { $dark: {} } }); call, and rearrange accordingly
59 | */
60 | // { styles: ['container', 'title'], ... } type of map to track which style lookups require dynamic adjusting
61 | const dynamicStyleMap = {} as Record;
62 | programPath.traverse({
63 | VariableDeclarator(varDecPath) {
64 | const callPath = varDecPath.get("init");
65 | if (!t.isCallExpression(callPath.node)) return;
66 | if (!t.isIdentifier(varDecPath.node.id)) return;
67 |
68 | let stylesName = varDecPath.node.id.name;
69 | dynamicStyleMap[stylesName] = [];
70 |
71 | if (
72 | t.isMemberExpression(callPath.node.callee) &&
73 | t.isIdentifier(callPath.node.callee.object) &&
74 | callPath.node.callee.object.name === StyleSheetName &&
75 | t.isIdentifier(callPath.node.callee.property) &&
76 | callPath.node.callee.property.name === "create"
77 | ) {
78 | // At this point, callPath.node.callee is a MemberExpression. Grab argument, which is arg to StyleSheet.create
79 | const arg = callPath.node.arguments[0];
80 | if (!arg || !t.isObjectExpression(arg)) return;
81 |
82 | // Loop through properties of the StyleSheet.create argument object
83 | // Check for ones that have a nested $dark property
84 | const nodesToInsert = [] as {
85 | node: ObjectProperty;
86 | ogName: string;
87 | }[];
88 | for (const i in arg.properties) {
89 | const property = arg.properties[i];
90 | if (!t.isProperty(property)) continue;
91 | if (!t.isIdentifier(property.key)) continue;
92 | if (!t.isObjectExpression(property.value)) continue;
93 |
94 | // Loop through fields of the style object (like fields of styles.container), look for $dark
95 |
96 | for (const j in property.value.properties) {
97 | const decProp = property.value.properties[j];
98 | if (!t.isProperty(decProp)) continue;
99 | if (
100 | t.isIdentifier(decProp.key) &&
101 | decProp.key.name === "$dark"
102 | ) {
103 | // Keep track of e.g. `styles.container` as a call that will need dynamic dark mode support
104 | dynamicStyleMap[stylesName].push(property.key.name);
105 |
106 | // Extract path to $dark sub property
107 | const $darkPath = callPath.get(
108 | `arguments.0.properties.${i}.value.properties.${j}`,
109 | ) as NodePath;
110 |
111 | const clonedNode = t.cloneDeepWithoutLoc(
112 | // @ts-ignore
113 | $darkPath.node,
114 | ) as ObjectProperty;
115 | if (t.isIdentifier(clonedNode.key))
116 | clonedNode.key.name = `__${property.key.name}__$dark`;
117 |
118 | nodesToInsert.push({
119 | node: clonedNode,
120 | ogName: property.key.name,
121 | });
122 | $darkPath.remove();
123 | }
124 | }
125 | }
126 |
127 | nodesToInsert.forEach(({ node, ogName }) => {
128 | // Insert __property__$dark style declaration into StyleSheet.create
129 | // @ts-ignore
130 | callPath.node.arguments[0].properties.push(node);
131 |
132 | // Then add StyleSheet.compose call
133 | const newNode = t.variableDeclaration("const", [
134 | t.variableDeclarator(
135 | t.identifier(
136 | `__${stylesName}${(node.key as Identifier).name}`,
137 | ),
138 | t.callExpression(
139 | t.memberExpression(
140 | t.identifier(StyleSheetName!),
141 | t.identifier("compose"),
142 | ),
143 | [
144 | // styles.container
145 | t.memberExpression(
146 | t.identifier(stylesName),
147 | t.identifier(ogName),
148 | ),
149 | // styles.__container__$dark
150 | t.memberExpression(
151 | t.identifier(stylesName),
152 | t.identifier((node.key as Identifier).name),
153 | ),
154 | ],
155 | ),
156 | ),
157 | ]);
158 |
159 | programPath.pushContainer("body", [newNode]);
160 | });
161 | }
162 | },
163 | });
164 |
165 | // Short-circuit if no dynamic styles are found
166 | if (Object.values(dynamicStyleMap).every((arr) => arr.length === 0))
167 | return;
168 |
169 | /**
170 | * Step 3: Import useDarkMode(name?) from react-native-dark.
171 | */
172 | const useDarkModeName = addNamed(
173 | programPath,
174 | "useDarkMode",
175 | "react-native-dark",
176 | ).name as string;
177 |
178 | /**
179 | * Step #: Find all functions that have JSX that access our style props, and inject hook call
180 | */
181 | programPath.traverse({
182 | // @ts-ignore
183 | "FunctionDeclaration|FunctionExpression|ArrowFunctionExpression"(
184 | funcPath:
185 | | babel.NodePath
186 | | babel.NodePath
187 | | babel.NodePath,
188 | ) {
189 | // Before injecting anything, we should find member expressions that need to be modified
190 | const styleCallToMod = [] as {
191 | memPath: NodePath;
192 | darkStyleVarName: string;
193 | }[];
194 | funcPath.traverse({
195 | MemberExpression(memPath) {
196 | if (
197 | t.isIdentifier(memPath.node.object) &&
198 | memPath.node.object.name in dynamicStyleMap &&
199 | t.isIdentifier(memPath.node.property) &&
200 | dynamicStyleMap[memPath.node.object.name].includes(
201 | memPath.node.property.name,
202 | )
203 | ) {
204 | styleCallToMod.push({
205 | memPath,
206 | darkStyleVarName: `__${memPath.node.object.name}__${memPath.node.property.name}__$dark`,
207 | });
208 | }
209 | },
210 | });
211 |
212 | if (styleCallToMod.length === 0) return;
213 |
214 | // Inject useDarkMode into function body
215 | const isDarkNode = funcPath.scope.generateUidIdentifier(
216 | "__REACT_NATIVE_DARK_isDark",
217 | );
218 | const hookDeclaration = t.variableDeclaration("const", [
219 | t.variableDeclarator(
220 | // @ts-ignore babel types are a bit rough
221 | isDarkNode,
222 | t.callExpression(t.identifier(useDarkModeName), []),
223 | ),
224 | ]);
225 |
226 | // Expressions like `const Footer = () => ;` need to be modded
227 | // to add a block statement
228 | const body = funcPath.get("body");
229 | if (t.isBlockStatement(body)) {
230 | (body as babel.NodePath).unshiftContainer(
231 | "body",
232 | hookDeclaration,
233 | );
234 | } else {
235 | (body as NodePath).replaceWith(
236 | t.blockStatement([
237 | hookDeclaration,
238 | // @ts-ignore
239 | t.returnStatement(body.node),
240 | ]),
241 | );
242 | }
243 |
244 | styleCallToMod.forEach(({ memPath, darkStyleVarName }) => {
245 | memPath.replaceWith(
246 | t.conditionalExpression(
247 | // @ts-ignore
248 | isDarkNode,
249 | t.identifier(darkStyleVarName),
250 | // @ts-ignore
251 | memPath.node,
252 | ),
253 | );
254 | });
255 | },
256 | });
257 | },
258 | },
259 | },
260 | };
261 | }
262 |
--------------------------------------------------------------------------------