@highbeek/create-rnstarterkit 1.0.2-beta.15 → 1.0.2-beta.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -36,6 +36,27 @@ npx @highbeek/create-rnstarterkit myApp --preset indie
36
36
  npx @highbeek/create-rnstarterkit myApp --preset minimal
37
37
  ```
38
38
 
39
+ ### After scaffolding — React Native CLI projects
40
+
41
+ Native dependencies need linking via CocoaPods before you can run on iOS:
42
+
43
+ ```bash
44
+ cd myApp
45
+ npm install # already run by the generator, skip if done
46
+ cd ios && pod install && cd ..
47
+
48
+ # Then run
49
+ npx react-native run-ios
50
+ npx react-native run-android
51
+ ```
52
+
53
+ ### After scaffolding — Expo projects
54
+
55
+ ```bash
56
+ cd myApp
57
+ npx expo start
58
+ ```
59
+
39
60
  ---
40
61
 
41
62
  ## Presets
@@ -69,6 +90,7 @@ When run without a preset, the CLI walks you through your stack:
69
90
  ? Sentry: Yes / No
70
91
  ? i18n (i18next): Yes / No
71
92
  ? Maestro E2E flows: Yes / No
93
+ ? NativeWind (Tailwind): Yes / No
72
94
  ? CI (GitHub Actions): Yes / No
73
95
  ? Husky pre-commit hooks: Yes / No
74
96
  ```
@@ -161,9 +183,23 @@ Three implementations — all include Login/Register screens, protected routes,
161
183
  | AsyncStorage | Default — async token persistence |
162
184
  | MMKV | Synchronous, 10x faster, drop-in replacement |
163
185
 
186
+ ### NativeWind (Tailwind CSS)
187
+
188
+ Adds `nativewind` + `tailwindcss` and wires everything up for your platform:
189
+
190
+ | Step | Expo | React Native CLI |
191
+ |---|---|---|
192
+ | `tailwind.config.js` | content globs for `app/` + `src/` | content globs for `App.tsx` + `src/` |
193
+ | `global.css` | created, imported in `app/_layout.tsx` | created, imported in `index.js` |
194
+ | `metro.config.js` | wrapped with `withNativeWind` (expo/metro-config) | wrapped with `withNativeWind` (@react-native/metro-config) |
195
+ | `babel.config.js` | `jsxImportSource: 'nativewind'` in babel-preset-expo | `nativewind/babel` plugin added |
196
+ | `tsconfig.json` | `"nativewind/types"` added to types | same |
197
+
198
+ After generation, use `className` props directly on React Native components — no extra setup needed.
199
+
164
200
  ### Sentry
165
201
 
166
- Adds `@sentry/react-native`, initialises in the app entry point, and exports `captureException` + `addBreadcrumb` helpers. Replace `__YOUR_DSN__` in `src/utils/sentry.ts` with your project DSN.
202
+ Adds `@sentry/react-native`, initialises in the app entry point, and exports `captureException` + `addBreadcrumb` helpers. The DSN is read from your environment variables — set `EXPO_PUBLIC_SENTRY_DSN` (Expo) or `SENTRY_DSN` (RN CLI) in your `.env` file.
167
203
 
168
204
  ### i18n
169
205
 
@@ -230,7 +266,7 @@ Templates live in `src/templates/`. Generator logic is in `src/generators/appGen
230
266
 
231
267
  ## Status
232
268
 
233
- Current version: `1.0.2-beta.15`
269
+ Current version: `1.0.2-beta.17`
234
270
 
235
271
  Beta release — core scaffolding, presets, and generators are working. Feedback and contributions welcome.
236
272
 
@@ -26,6 +26,7 @@ const PRESETS = {
26
26
  sentry: false,
27
27
  i18n: false,
28
28
  maestro: false,
29
+ nativewind: false,
29
30
  },
30
31
  fintech: {
31
32
  platform: "React Native CLI",
@@ -43,6 +44,7 @@ const PRESETS = {
43
44
  sentry: true,
44
45
  i18n: true,
45
46
  maestro: true,
47
+ nativewind: false,
46
48
  },
47
49
  social: {
48
50
  platform: "Expo",
@@ -60,6 +62,7 @@ const PRESETS = {
60
62
  sentry: true,
61
63
  i18n: true,
62
64
  maestro: false,
65
+ nativewind: false,
63
66
  },
64
67
  indie: {
65
68
  typescript: true,
@@ -76,6 +79,7 @@ const PRESETS = {
76
79
  sentry: false,
77
80
  i18n: false,
78
81
  maestro: false,
82
+ nativewind: false,
79
83
  },
80
84
  };
81
85
  // ---------------------------------------------------------------------------
@@ -194,6 +198,13 @@ async function runInteractivePrompt(prefilledName, presetDefaults) {
194
198
  default: false,
195
199
  when: () => presetDefaults?.maestro === undefined,
196
200
  },
201
+ {
202
+ type: "confirm",
203
+ name: "nativewind",
204
+ message: "Use NativeWind (Tailwind CSS for React Native)?",
205
+ default: false,
206
+ when: () => presetDefaults?.nativewind === undefined,
207
+ },
197
208
  {
198
209
  type: "confirm",
199
210
  name: "ci",
@@ -224,6 +235,7 @@ async function runInteractivePrompt(prefilledName, presetDefaults) {
224
235
  sentry: answers.sentry ?? presetDefaults?.sentry ?? false,
225
236
  i18n: answers.i18n ?? presetDefaults?.i18n ?? false,
226
237
  maestro: answers.maestro ?? presetDefaults?.maestro ?? false,
238
+ nativewind: answers.nativewind ?? presetDefaults?.nativewind ?? false,
227
239
  ci: answers.ci ?? presetDefaults?.ci ?? false,
228
240
  husky: answers.husky ?? presetDefaults?.husky ?? true,
229
241
  };
@@ -235,7 +247,7 @@ const program = new commander_1.Command();
235
247
  program
236
248
  .name("create-rnstarterkit")
237
249
  .description("Scaffold a production-ready React Native app")
238
- .version("1.0.2-beta.15")
250
+ .version("1.0.2-beta.18")
239
251
  .argument("[projectName]", "Name of the project (alphanumeric)")
240
252
  .option("--preset <name>", "Skip prompts with a preset: minimal | fintech | social | indie")
241
253
  .action(async (projectName, cmdOptions) => {
@@ -263,6 +275,7 @@ program
263
275
  sentry: presetDefaults.sentry ?? false,
264
276
  i18n: presetDefaults.i18n ?? false,
265
277
  maestro: presetDefaults.maestro ?? false,
278
+ nativewind: presetDefaults.nativewind ?? false,
266
279
  ci: presetDefaults.ci ?? false,
267
280
  husky: presetDefaults.husky ?? true,
268
281
  };
@@ -7,6 +7,10 @@ exports.generateApp = generateApp;
7
7
  const path_1 = __importDefault(require("path"));
8
8
  const fs_extra_1 = __importDefault(require("fs-extra"));
9
9
  const execa_1 = require("execa");
10
+ const CLI_REANIMATED_VERSION = "^4.2.0";
11
+ const CLI_WORKLETS_VERSION = "^0.7.0";
12
+ const EXPO_REANIMATED_VERSION = "~4.1.1";
13
+ const EXPO_WORKLETS_VERSION = "0.5.1";
10
14
  async function generateApp(options) {
11
15
  const { platform, projectName, auth, apiClient, absoluteImports, state, dataFetching, validation, storage, typescript, apiClientType, husky, sentry, i18n, maestro, } = options;
12
16
  const templateRoot = await resolveTemplateRoot();
@@ -80,6 +84,8 @@ async function generateApp(options) {
80
84
  await configureI18n(targetPath, platform);
81
85
  if (maestro)
82
86
  await configureMaestro(targetPath, { auth, ci: options.ci });
87
+ if (options.nativewind)
88
+ await configureNativeWind(targetPath, platform);
83
89
  await stampVersion(targetPath, options);
84
90
  if (options.ci)
85
91
  await configureCi(targetPath);
@@ -96,6 +102,7 @@ async function generateApp(options) {
96
102
  await fs_extra_1.default.rename(appJs, appTsx);
97
103
  }
98
104
  }
105
+ await validateGeneratedRuntimeCompatibility(targetPath);
99
106
  await installDependencies(targetPath);
100
107
  console.log("✅ Project created successfully!");
101
108
  }
@@ -512,11 +519,13 @@ async function configureStateAndAuthDependencies(targetPath, options) {
512
519
  dependencies["@react-navigation/bottom-tabs"] = "^7.4.0";
513
520
  dependencies["react-native-screens"] = "^4.16.0";
514
521
  dependencies["react-native-gesture-handler"] = "^2.28.0";
515
- dependencies["react-native-reanimated"] = "^3.17.4";
522
+ dependencies["react-native-worklets"] = CLI_WORKLETS_VERSION;
523
+ dependencies["react-native-reanimated"] = CLI_REANIMATED_VERSION;
516
524
  }
517
525
  // Expo with auth uses expo-router tabs which require reanimated
518
526
  if (options.auth && options.platform === "Expo") {
519
- dependencies["react-native-reanimated"] = "^3.17.4";
527
+ dependencies["react-native-worklets"] = EXPO_WORKLETS_VERSION;
528
+ dependencies["react-native-reanimated"] = EXPO_REANIMATED_VERSION;
520
529
  }
521
530
  // AsyncStorage: needed when explicitly selected OR when auth is enabled
522
531
  // (auth modules use AsyncStorage for welcome screen persistence)
@@ -2055,6 +2064,8 @@ async function writeCliBabelConfig(targetPath, options) {
2055
2064
  if (options.useAbsoluteImports) {
2056
2065
  plugins.push("['module-resolver', { root: ['./src'], alias: { '@': './src' } }]");
2057
2066
  }
2067
+ // Must stay last when present.
2068
+ plugins.push("'react-native-worklets/plugin'");
2058
2069
  const pluginsLine = plugins.length > 0 ? `,\n plugins: [${plugins.join(", ")}]` : "";
2059
2070
  const content = `module.exports = {\n presets: ['module:@react-native/babel-preset']${pluginsLine},\n};\n`;
2060
2071
  await fs_extra_1.default.writeFile(babelConfigPath, content, "utf8");
@@ -2086,6 +2097,31 @@ async function ensureDependencies(targetPath, patch) {
2086
2097
  await fs_extra_1.default.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2087
2098
  }
2088
2099
  }
2100
+ function parseMajorMinor(version) {
2101
+ const match = version.match(/(\d+)\.(\d+)/);
2102
+ if (!match)
2103
+ return null;
2104
+ return { major: Number(match[1]), minor: Number(match[2]) };
2105
+ }
2106
+ async function validateGeneratedRuntimeCompatibility(targetPath) {
2107
+ const packageJsonPath = path_1.default.join(targetPath, "package.json");
2108
+ if (!(await fs_extra_1.default.pathExists(packageJsonPath)))
2109
+ return;
2110
+ const pkg = await fs_extra_1.default.readJson(packageJsonPath);
2111
+ const rnVersion = pkg.dependencies?.["react-native"];
2112
+ const reanimatedVersion = pkg.dependencies?.["react-native-reanimated"];
2113
+ if (!rnVersion || !reanimatedVersion)
2114
+ return;
2115
+ const rn = parseMajorMinor(String(rnVersion));
2116
+ const reanimated = parseMajorMinor(String(reanimatedVersion));
2117
+ if (!rn || !reanimated)
2118
+ return;
2119
+ // Reanimated 3 is not compatible with RN >= 0.82.
2120
+ if (rn.major === 0 && rn.minor >= 82 && reanimated.major < 4) {
2121
+ throw new Error(`❌ Invalid dependency combination detected: react-native@${rnVersion} with react-native-reanimated@${reanimatedVersion}. ` +
2122
+ `Use react-native-reanimated@4+ (and react-native-worklets) for React Native 0.${rn.minor}.x.`);
2123
+ }
2124
+ }
2089
2125
  async function writeIfMissing(filePath, content) {
2090
2126
  await fs_extra_1.default.ensureDir(path_1.default.dirname(filePath));
2091
2127
  if (!(await fs_extra_1.default.pathExists(filePath))) {
@@ -2264,6 +2300,112 @@ async function configureTesting(targetPath) {
2264
2300
  }
2265
2301
  }
2266
2302
  }
2303
+ // ---------------------------------------------------------------------------
2304
+ // NativeWind (Tailwind CSS for React Native)
2305
+ // ---------------------------------------------------------------------------
2306
+ async function configureNativeWind(targetPath, platform) {
2307
+ // 1. Install dependencies
2308
+ await ensureDependencies(targetPath, {
2309
+ dependencies: {
2310
+ nativewind: "^4.1.23",
2311
+ tailwindcss: "^3.4.17",
2312
+ },
2313
+ });
2314
+ // 2. tailwind.config.js
2315
+ const contentGlobs = platform === "Expo"
2316
+ ? `['./app/**/*.{js,jsx,ts,tsx}', './src/**/*.{js,jsx,ts,tsx}', './components/**/*.{js,jsx,ts,tsx}']`
2317
+ : `['./App.{js,jsx,ts,tsx}', './src/**/*.{js,jsx,ts,tsx}']`;
2318
+ const tailwindConfig = `/** @type {import('tailwindcss').Config} */
2319
+ module.exports = {
2320
+ content: ${contentGlobs},
2321
+ presets: [require('nativewind/preset')],
2322
+ theme: {
2323
+ extend: {},
2324
+ },
2325
+ plugins: [],
2326
+ };
2327
+ `;
2328
+ await writeIfMissing(path_1.default.join(targetPath, "tailwind.config.js"), tailwindConfig);
2329
+ // Both platforms need global.css
2330
+ const globalCss = `@tailwind base;\n@tailwind components;\n@tailwind utilities;\n`;
2331
+ await writeIfMissing(path_1.default.join(targetPath, "global.css"), globalCss);
2332
+ const metroPath = path_1.default.join(targetPath, "metro.config.js");
2333
+ if (platform === "Expo") {
2334
+ // Expo: metro.config.js — wrap with withNativeWind using expo/metro-config
2335
+ const expoMetroContent = `const { getDefaultConfig } = require('expo/metro-config');
2336
+ const { withNativeWind } = require('nativewind/metro');
2337
+
2338
+ const config = getDefaultConfig(__dirname);
2339
+
2340
+ module.exports = withNativeWind(config, { input: './global.css' });
2341
+ `;
2342
+ await fs_extra_1.default.writeFile(metroPath, expoMetroContent, "utf8");
2343
+ // Expo: babel.config.js — jsxImportSource switches JSX transform to nativewind
2344
+ const expoBabelContent = `module.exports = function (api) {
2345
+ api.cache(true);
2346
+ return {
2347
+ presets: [
2348
+ ['babel-preset-expo', { jsxImportSource: 'nativewind' }],
2349
+ ],
2350
+ };
2351
+ };
2352
+ `;
2353
+ await fs_extra_1.default.writeFile(path_1.default.join(targetPath, "babel.config.js"), expoBabelContent, "utf8");
2354
+ // Expo: inject global.css import at the top of app/_layout.tsx
2355
+ const layoutPath = path_1.default.join(targetPath, "app", "_layout.tsx");
2356
+ if (await fs_extra_1.default.pathExists(layoutPath)) {
2357
+ const layoutContent = await fs_extra_1.default.readFile(layoutPath, "utf8");
2358
+ if (!layoutContent.includes("global.css")) {
2359
+ await fs_extra_1.default.writeFile(layoutPath, `import '../global.css';\n${layoutContent}`, "utf8");
2360
+ }
2361
+ }
2362
+ }
2363
+ else {
2364
+ // CLI: metro.config.js — replace with withNativeWind using @react-native/metro-config
2365
+ const cliMetroContent = `const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
2366
+ const { withNativeWind } = require('nativewind/metro');
2367
+
2368
+ const config = mergeConfig(getDefaultConfig(__dirname), {});
2369
+
2370
+ module.exports = withNativeWind(config, { input: './global.css' });
2371
+ `;
2372
+ await fs_extra_1.default.writeFile(metroPath, cliMetroContent, "utf8");
2373
+ // CLI: babel.config.js — add nativewind/babel plugin
2374
+ const babelPath = path_1.default.join(targetPath, "babel.config.js");
2375
+ if (await fs_extra_1.default.pathExists(babelPath)) {
2376
+ let babelContent = await fs_extra_1.default.readFile(babelPath, "utf8");
2377
+ if (!babelContent.includes("nativewind/babel")) {
2378
+ babelContent = babelContent.replace(/plugins:\s*\[([^\]]*)\]/, (match, inner) => inner.trim()
2379
+ ? match.replace(inner, `${inner.trimEnd()}, 'nativewind/babel'`)
2380
+ : `plugins: ['nativewind/babel']`);
2381
+ if (!babelContent.includes("nativewind/babel")) {
2382
+ babelContent = babelContent.replace(/module\.exports\s*=\s*\{/, `module.exports = {\n plugins: ['nativewind/babel'],`);
2383
+ }
2384
+ await fs_extra_1.default.writeFile(babelPath, babelContent, "utf8");
2385
+ }
2386
+ }
2387
+ // CLI: inject global.css import at the top of index.js
2388
+ const indexPath = path_1.default.join(targetPath, "index.js");
2389
+ if (await fs_extra_1.default.pathExists(indexPath)) {
2390
+ const indexContent = await fs_extra_1.default.readFile(indexPath, "utf8");
2391
+ if (!indexContent.includes("global.css")) {
2392
+ await fs_extra_1.default.writeFile(indexPath, `import './global.css';\n${indexContent}`, "utf8");
2393
+ }
2394
+ }
2395
+ }
2396
+ // 4. Add nativewind types to tsconfig
2397
+ const tsconfigPath = path_1.default.join(targetPath, "tsconfig.json");
2398
+ if (await fs_extra_1.default.pathExists(tsconfigPath)) {
2399
+ const tsconfig = await fs_extra_1.default.readJson(tsconfigPath);
2400
+ tsconfig.compilerOptions = tsconfig.compilerOptions || {};
2401
+ const types = tsconfig.compilerOptions.types || [];
2402
+ if (!types.includes("nativewind/types")) {
2403
+ tsconfig.compilerOptions.types = [...types, "nativewind/types"];
2404
+ }
2405
+ await fs_extra_1.default.writeJson(tsconfigPath, tsconfig, { spaces: 2 });
2406
+ }
2407
+ console.log("🎨 NativeWind configured — use className props on your React Native components.");
2408
+ }
2267
2409
  async function stampVersion(targetPath, options) {
2268
2410
  const packageJsonPath = path_1.default.join(targetPath, "package.json");
2269
2411
  if (!(await fs_extra_1.default.pathExists(packageJsonPath)))
@@ -2297,6 +2439,7 @@ async function stampVersion(targetPath, options) {
2297
2439
  sentry: options.sentry,
2298
2440
  i18n: options.i18n,
2299
2441
  maestro: options.maestro,
2442
+ nativewind: options.nativewind,
2300
2443
  };
2301
2444
  await fs_extra_1.default.writeJson(packageJsonPath, projectPkg, { spaces: 2 });
2302
2445
  }
@@ -2672,6 +2815,8 @@ function isTextFile(filePath) {
2672
2815
  ".lock",
2673
2816
  ".xcprivacy",
2674
2817
  ".storyboard",
2818
+ ".xcworkspacedata",
2819
+ ".xcscheme",
2675
2820
  ]);
2676
2821
  const base = path_1.default.basename(filePath).toLowerCase();
2677
2822
  return textExtensions.has(extension) || base === "podfile" || base === "gemfile";
@@ -1,3 +1,4 @@
1
1
  module.exports = {
2
2
  presets: ['module:@react-native/babel-preset'],
3
+ plugins: ['react-native-worklets/plugin'],
3
4
  };
@@ -15,7 +15,8 @@
15
15
  "react": "19.2.3",
16
16
  "react-native": "0.84.0",
17
17
  "react-native-safe-area-context": "^5.5.2",
18
- "react-native-reanimated": "^3.17.4"
18
+ "react-native-worklets": "^0.7.0",
19
+ "react-native-reanimated": "^4.2.0"
19
20
  },
20
21
  "devDependencies": {
21
22
  "@babel/core": "^7.25.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@highbeek/create-rnstarterkit",
3
- "version": "1.0.2-beta.15",
3
+ "version": "1.0.2-beta.18",
4
4
  "description": "CLI to scaffold production-ready React Native app structures.",
5
5
  "main": "dist/src/generators/appGenerator.js",
6
6
  "bin": {