@varunindiit/create-rn-starter 1.0.1
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/LICENSE +21 -0
- package/README.md +93 -0
- package/bin/index.js +270 -0
- package/lib/prompt.js +63 -0
- package/lib/rename.js +239 -0
- package/lib/scaffold.js +110 -0
- package/lib/utils.js +122 -0
- package/package.json +38 -0
- package/template/.eslintrc.js +4 -0
- package/template/.prettierrc.js +5 -0
- package/template/.watchmanconfig +1 -0
- package/template/App.tsx +100 -0
- package/template/Gemfile +17 -0
- package/template/README.md +97 -0
- package/template/__tests__/App.test.tsx +13 -0
- package/template/_gitignore +75 -0
- package/template/android/app/build.gradle +119 -0
- package/template/android/app/debug.keystore +0 -0
- package/template/android/app/proguard-rules.pro +10 -0
- package/template/android/app/src/main/AndroidManifest.xml +45 -0
- package/template/android/app/src/main/assets/fonts/MonaSans-Black.ttf +0 -0
- package/template/android/app/src/main/assets/fonts/MonaSans-BlackItalic.ttf +0 -0
- package/template/android/app/src/main/assets/fonts/MonaSans-Bold.ttf +0 -0
- package/template/android/app/src/main/assets/fonts/MonaSans-BoldItalic.ttf +0 -0
- package/template/android/app/src/main/assets/fonts/MonaSans-ExtraBold.ttf +0 -0
- package/template/android/app/src/main/assets/fonts/MonaSans-ExtraBoldItalic.ttf +0 -0
- package/template/android/app/src/main/assets/fonts/MonaSans-ExtraLight.ttf +0 -0
- package/template/android/app/src/main/assets/fonts/MonaSans-ExtraLightItalic.ttf +0 -0
- package/template/android/app/src/main/assets/fonts/MonaSans-Italic.ttf +0 -0
- package/template/android/app/src/main/assets/fonts/MonaSans-Light.ttf +0 -0
- package/template/android/app/src/main/assets/fonts/MonaSans-LightItalic.ttf +0 -0
- package/template/android/app/src/main/assets/fonts/MonaSans-Medium.ttf +0 -0
- package/template/android/app/src/main/assets/fonts/MonaSans-MediumItalic.ttf +0 -0
- package/template/android/app/src/main/assets/fonts/MonaSans-Regular.ttf +0 -0
- package/template/android/app/src/main/assets/fonts/MonaSans-SemiBold.ttf +0 -0
- package/template/android/app/src/main/assets/fonts/MonaSans-SemiBoldItalic.ttf +0 -0
- package/template/android/app/src/main/java/com/awesomeproject/MainActivity.kt +22 -0
- package/template/android/app/src/main/java/com/awesomeproject/MainApplication.kt +27 -0
- package/template/android/app/src/main/res/drawable/launch_screen.png +0 -0
- package/template/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
- package/template/android/app/src/main/res/layout/launch_screen.xml +12 -0
- package/template/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/template/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/template/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/template/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/template/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/template/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/template/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/template/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/template/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/template/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/template/android/app/src/main/res/values/colors.xml +3 -0
- package/template/android/app/src/main/res/values/strings.xml +3 -0
- package/template/android/app/src/main/res/values/styles.xml +11 -0
- package/template/android/build.gradle +21 -0
- package/template/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/template/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/template/android/gradle.properties +44 -0
- package/template/android/gradlew +248 -0
- package/template/android/gradlew.bat +98 -0
- package/template/android/link-assets-manifest.json +69 -0
- package/template/android/settings.gradle +6 -0
- package/template/app.json +4 -0
- package/template/babel.config.js +4 -0
- package/template/declarations.d.ts +6 -0
- package/template/env.example +20 -0
- package/template/index.js +10 -0
- package/template/ios/.xcode.env +11 -0
- package/template/ios/.xcode.env.local +1 -0
- package/template/ios/AwesomeProject/AppDelegate.swift +60 -0
- package/template/ios/AwesomeProject/Images.xcassets/AppIcon.appiconset/Contents.json +53 -0
- package/template/ios/AwesomeProject/Images.xcassets/Contents.json +6 -0
- package/template/ios/AwesomeProject/Images.xcassets/Splash.imageset/Contents.json +23 -0
- package/template/ios/AwesomeProject/Images.xcassets/Splash.imageset/Splash@1x.png +0 -0
- package/template/ios/AwesomeProject/Images.xcassets/Splash.imageset/Splash@2x.png +0 -0
- package/template/ios/AwesomeProject/Images.xcassets/Splash.imageset/Splash@3x.png +0 -0
- package/template/ios/AwesomeProject/Info.plist +89 -0
- package/template/ios/AwesomeProject/LaunchScreen.storyboard +40 -0
- package/template/ios/AwesomeProject/PrivacyInfo.xcprivacy +38 -0
- package/template/ios/AwesomeProject.xcodeproj/project.pbxproj +576 -0
- package/template/ios/AwesomeProject.xcodeproj/xcshareddata/xcschemes/AwesomeProject.xcscheme +88 -0
- package/template/ios/AwesomeProject.xcworkspace/contents.xcworkspacedata +10 -0
- package/template/ios/Podfile +68 -0
- package/template/ios/link-assets-manifest.json +69 -0
- package/template/jest.config.js +3 -0
- package/template/metro.config.js +24 -0
- package/template/package.json +68 -0
- package/template/react-native.config.js +7 -0
- package/template/src/assets/fonts/MonaSans-Black.ttf +0 -0
- package/template/src/assets/fonts/MonaSans-BlackItalic.ttf +0 -0
- package/template/src/assets/fonts/MonaSans-Bold.ttf +0 -0
- package/template/src/assets/fonts/MonaSans-BoldItalic.ttf +0 -0
- package/template/src/assets/fonts/MonaSans-ExtraBold.ttf +0 -0
- package/template/src/assets/fonts/MonaSans-ExtraBoldItalic.ttf +0 -0
- package/template/src/assets/fonts/MonaSans-ExtraLight.ttf +0 -0
- package/template/src/assets/fonts/MonaSans-ExtraLightItalic.ttf +0 -0
- package/template/src/assets/fonts/MonaSans-Italic.ttf +0 -0
- package/template/src/assets/fonts/MonaSans-Light.ttf +0 -0
- package/template/src/assets/fonts/MonaSans-LightItalic.ttf +0 -0
- package/template/src/assets/fonts/MonaSans-Medium.ttf +0 -0
- package/template/src/assets/fonts/MonaSans-MediumItalic.ttf +0 -0
- package/template/src/assets/fonts/MonaSans-Regular.ttf +0 -0
- package/template/src/assets/fonts/MonaSans-SemiBold.ttf +0 -0
- package/template/src/assets/fonts/MonaSans-SemiBoldItalic.ttf +0 -0
- package/template/src/assets/image/BackGroundAuth.png +0 -0
- package/template/src/assets/image/BackgroundVerification.png +0 -0
- package/template/src/assets/image/logo.png +0 -0
- package/template/src/assets/svg/add-circle.svg +5 -0
- package/template/src/assets/svg/airConditioning.svg +12 -0
- package/template/src/assets/svg/apple.svg +3 -0
- package/template/src/assets/svg/arrowDown.svg +3 -0
- package/template/src/assets/svg/back.svg +10 -0
- package/template/src/assets/svg/bag.svg +11 -0
- package/template/src/assets/svg/calender.svg +5 -0
- package/template/src/assets/svg/car.svg +10 -0
- package/template/src/assets/svg/carConfirm.svg +60 -0
- package/template/src/assets/svg/chatActive.svg +3 -0
- package/template/src/assets/svg/chatUnActive.svg +3 -0
- package/template/src/assets/svg/document-text.svg +6 -0
- package/template/src/assets/svg/gender.svg +11 -0
- package/template/src/assets/svg/google.svg +6 -0
- package/template/src/assets/svg/headphone.svg +3 -0
- package/template/src/assets/svg/homeActive.svg +3 -0
- package/template/src/assets/svg/homeUnActive.svg +3 -0
- package/template/src/assets/svg/logo.svg +18 -0
- package/template/src/assets/svg/logout.svg +5 -0
- package/template/src/assets/svg/maxBack.svg +4 -0
- package/template/src/assets/svg/message-text.svg +7 -0
- package/template/src/assets/svg/music.svg +5 -0
- package/template/src/assets/svg/noSmoking.svg +10 -0
- package/template/src/assets/svg/notification.svg +5 -0
- package/template/src/assets/svg/passenger.svg +4 -0
- package/template/src/assets/svg/phone.svg +3 -0
- package/template/src/assets/svg/rightArrow.svg +3 -0
- package/template/src/assets/svg/security-user.svg +5 -0
- package/template/src/assets/svg/star.svg +3 -0
- package/template/src/assets/svg/tick-circle.svg +4 -0
- package/template/src/assets/svg/trafficLight.svg +41 -0
- package/template/src/assets/svg/tripActive.svg +10 -0
- package/template/src/assets/svg/tripUnActive.svg +10 -0
- package/template/src/assets/svg/usbChargers.svg +3 -0
- package/template/src/assets/svg/user.svg +4 -0
- package/template/src/assets/svg/userActive.svg +3 -0
- package/template/src/assets/svg/userPlaceholder.svg +3 -0
- package/template/src/assets/svg/userUnActive.svg +3 -0
- package/template/src/components/AuthLayout/AuthLayout.tsx +170 -0
- package/template/src/components/AuthLayout/index.ts +1 -0
- package/template/src/components/BottomSheet/BottomSheet.tsx +73 -0
- package/template/src/components/BottomSheet/BottomSheetAlert.tsx +100 -0
- package/template/src/components/BottomSheet/CenterAlert.tsx +153 -0
- package/template/src/components/BottomSheet/index.ts +2 -0
- package/template/src/components/BottomTabBar/index.tsx +145 -0
- package/template/src/components/Button/RNButton.tsx +152 -0
- package/template/src/components/Button/index.ts +2 -0
- package/template/src/components/Common/Avatar.tsx +80 -0
- package/template/src/components/Common/Card.tsx +49 -0
- package/template/src/components/Common/CardBrandLogo.tsx +66 -0
- package/template/src/components/Common/Checkbox.tsx +65 -0
- package/template/src/components/Common/Chip.tsx +79 -0
- package/template/src/components/Common/CommonStyles.tsx +594 -0
- package/template/src/components/Common/Divider.tsx +33 -0
- package/template/src/components/Common/DriverTripCard.tsx +308 -0
- package/template/src/components/Common/Dropdown.tsx +161 -0
- package/template/src/components/Common/EmptyState.tsx +52 -0
- package/template/src/components/Common/FAB.tsx +68 -0
- package/template/src/components/Common/HeaderLocation.tsx +108 -0
- package/template/src/components/Common/Loader.tsx +23 -0
- package/template/src/components/Common/RatingStars.tsx +103 -0
- package/template/src/components/Common/RouteDots.tsx +98 -0
- package/template/src/components/Common/SegmentedControl.tsx +126 -0
- package/template/src/components/Common/SosButton.tsx +80 -0
- package/template/src/components/Common/SosSheet.tsx +344 -0
- package/template/src/components/Common/StarRating.tsx +58 -0
- package/template/src/components/Common/StatusBadge.tsx +56 -0
- package/template/src/components/Common/Toggle.tsx +66 -0
- package/template/src/components/Common/TripCard.tsx +247 -0
- package/template/src/components/Common/UploadBox.tsx +106 -0
- package/template/src/components/Container/MainContainer.tsx +76 -0
- package/template/src/components/Container/index.ts +1 -0
- package/template/src/components/Header/index.tsx +143 -0
- package/template/src/components/Icon/SvgIcons.tsx +1991 -0
- package/template/src/components/ImagePickerSheet/ImagePickerSheet.tsx +233 -0
- package/template/src/components/ImagePickerSheet/index.ts +2 -0
- package/template/src/components/Input/CountryDropdown.tsx +71 -0
- package/template/src/components/Input/OtpInput.tsx +117 -0
- package/template/src/components/Input/RNInput.tsx +138 -0
- package/template/src/components/Input/index.ts +4 -0
- package/template/src/components/Picker/DatePickerSheet.tsx +393 -0
- package/template/src/components/Picker/PassengerPickerSheet.tsx +237 -0
- package/template/src/components/Text/RNText.tsx +62 -0
- package/template/src/components/Text/index.ts +1 -0
- package/template/src/components/index.ts +44 -0
- package/template/src/hooks/useCurrentLocation.ts +72 -0
- package/template/src/localization/i18n.ts +29 -0
- package/template/src/localization/i18next.d.ts +11 -0
- package/template/src/localization/index.ts +4 -0
- package/template/src/localization/languageStorage.ts +27 -0
- package/template/src/localization/languages.ts +62 -0
- package/template/src/localization/resources/en.ts +703 -0
- package/template/src/localization/resources/fr.ts +703 -0
- package/template/src/localization/useLanguage.ts +42 -0
- package/template/src/navigation/AuthNavigation.tsx +23 -0
- package/template/src/navigation/BottomTabs.tsx +24 -0
- package/template/src/navigation/RootNavigation.tsx +27 -0
- package/template/src/navigation/RouteKey.ts +22 -0
- package/template/src/navigation/StackNavigation.tsx +52 -0
- package/template/src/navigation/paramLists.ts +25 -0
- package/template/src/redux/slice/app.ts +66 -0
- package/template/src/redux/slice/auth.ts +40 -0
- package/template/src/redux/slice/userProfile.ts +124 -0
- package/template/src/redux/store.ts +17 -0
- package/template/src/screen/auth/Login.tsx +69 -0
- package/template/src/screen/onboarding/LanguageSelection.tsx +231 -0
- package/template/src/screen/root/home/index.tsx +36 -0
- package/template/src/screen/root/profile/index.tsx +69 -0
- package/template/src/screen/shared/Chat.tsx +308 -0
- package/template/src/screen/shared/EditProfile.tsx +407 -0
- package/template/src/screen/shared/HelpSupport.tsx +678 -0
- package/template/src/screen/shared/LocationSearch.tsx +362 -0
- package/template/src/screen/shared/Messages.tsx +115 -0
- package/template/src/screen/shared/Notifications.tsx +86 -0
- package/template/src/screen/shared/PrivacyPolicy.tsx +297 -0
- package/template/src/screen/shared/Profile.tsx +118 -0
- package/template/src/screen/shared/Ratings.tsx +170 -0
- package/template/src/screen/shared/TermsConditions.tsx +315 -0
- package/template/src/screen/shared/profile/DriverProfile.tsx +262 -0
- package/template/src/screen/shared/profile/PassengerProfile.tsx +123 -0
- package/template/src/screen/shared/profile/ProfileParts.tsx +219 -0
- package/template/src/services/Config.ts +37 -0
- package/template/src/services/api.ts +37 -0
- package/template/src/services/index.ts +4 -0
- package/template/src/services/places.ts +320 -0
- package/template/src/services/storage.ts +33 -0
- package/template/src/theme/fonts.ts +30 -0
- package/template/src/theme/index.ts +3 -0
- package/template/src/theme/spacing.ts +66 -0
- package/template/src/theme/theme.ts +58 -0
- package/template/src/types/env.d.ts +8 -0
- package/template/src/types/index.ts +3 -0
- package/template/src/utils/card.ts +101 -0
- package/template/src/utils/constants.ts +39 -0
- package/template/src/utils/functions.ts +24 -0
- package/template/tsconfig.json +8 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 varunindiit
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# create-rn-starter
|
|
2
|
+
|
|
3
|
+
> Scaffold a **production-ready React Native CLI** app — fully renamed for Android & iOS — in seconds.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx @varunindiit/create-rn-starter
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The CLI asks for a **project name** and a **bundle/package identifier**, then
|
|
10
|
+
generates a working project: it copies the template, renames every reference
|
|
11
|
+
across JS / Android / iOS, sets up the environment files, installs
|
|
12
|
+
dependencies (and CocoaPods on macOS) and initialises git.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# interactive
|
|
16
|
+
npx @varunindiit/create-rn-starter
|
|
17
|
+
|
|
18
|
+
# non-interactive
|
|
19
|
+
npx @varunindiit/create-rn-starter awesome-app --bundle-id com.acme.awesome -y
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## What you get
|
|
23
|
+
|
|
24
|
+
A real, runnable RN **0.85** project (New Architecture) with:
|
|
25
|
+
|
|
26
|
+
- **Navigation** — React Navigation native-stack + bottom tabs, with a
|
|
27
|
+
declarative auth guard (`StackNavigation`) that swaps the Auth and App trees
|
|
28
|
+
off a single redux flag.
|
|
29
|
+
- **State** — Redux Toolkit store with `auth` / `app` / `userProfile` slices.
|
|
30
|
+
- **Dummy auth flow** — Login → Bottom Tabs (**Home** · **Profile**) → Logout,
|
|
31
|
+
persisted with **MMKV** so the session survives restarts.
|
|
32
|
+
- **Theming** — centralised `theme` (colours, spacing, fonts) + `react-native-size-matters`.
|
|
33
|
+
- **i18n** — `i18next` / `react-i18next` with `en` / `fr` resources and language storage.
|
|
34
|
+
- **SVG** — `react-native-svg` + `react-native-svg-transformer` wired in Metro.
|
|
35
|
+
- **API/services** — `axios` client, `Config` service, storage and places helpers.
|
|
36
|
+
- **Components** — a sizeable reusable library (buttons, inputs, sheets, headers,
|
|
37
|
+
pickers, common UI…).
|
|
38
|
+
- **Native setup** — splash screen, fonts/assets linking, permissions, and a
|
|
39
|
+
fully configured Android + iOS project.
|
|
40
|
+
|
|
41
|
+
## CLI options
|
|
42
|
+
|
|
43
|
+
| Flag | Description |
|
|
44
|
+
| ------------------- | ---------------------------------------------------- |
|
|
45
|
+
| `[project-name]` | Positional. Skips the name prompt. |
|
|
46
|
+
| `--bundle-id <id>` | Reverse-DNS identifier, e.g. `com.acme.myapp`. |
|
|
47
|
+
| `--no-install` | Skip JS dependency installation. |
|
|
48
|
+
| `--no-pods` | Skip iOS CocoaPods (macOS only). |
|
|
49
|
+
| `--no-git` | Skip git initialisation. |
|
|
50
|
+
| `-y, --yes` | Accept all defaults, no prompts. |
|
|
51
|
+
| `-h, --help` | Show help. |
|
|
52
|
+
| `-v, --version` | Show version. |
|
|
53
|
+
|
|
54
|
+
## How the rename works
|
|
55
|
+
|
|
56
|
+
The bundled `template/` is a genuine working app, so every name appears as a
|
|
57
|
+
concrete literal. On scaffold the CLI:
|
|
58
|
+
|
|
59
|
+
1. **Token pass** — rewrites file contents, swapping the source identifiers
|
|
60
|
+
(`AwesomeProject`, `com.awesomeproject`, the default Xcode bundle id …) for
|
|
61
|
+
your values across all text files (TS/JS, gradle, Kotlin, Swift, plist,
|
|
62
|
+
pbxproj, schemes, Podfile…).
|
|
63
|
+
2. **Native folder moves** — renames `ios/<App>`, `ios/<App>.xcodeproj`,
|
|
64
|
+
`ios/<App>.xcworkspace` and the shared scheme, and moves the Android
|
|
65
|
+
`java/com/awesomeproject` package directory to your bundle's path.
|
|
66
|
+
3. **Targeted edits** — sets `package.json` name (npm slug), `app.json`,
|
|
67
|
+
Android `strings.xml` `app_name` and iOS `CFBundleDisplayName` to the
|
|
68
|
+
human display name.
|
|
69
|
+
|
|
70
|
+
## Architecture
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
bin/index.js CLI entry: arg parsing, prompts, orchestration
|
|
74
|
+
lib/utils.js logging + name normalisation (pascal/slug/lower/display/bundle)
|
|
75
|
+
lib/prompt.js zero-dependency readline prompts
|
|
76
|
+
lib/rename.js RN rename engine: token pass + native folder moves + targeted edits
|
|
77
|
+
lib/scaffold.js copy tree, env files, dependency/pod install, git init
|
|
78
|
+
template/ a full, working RN CLI app (the thing that gets cloned)
|
|
79
|
+
scripts/smoke.js end-to-end test: scaffold to a temp dir and assert the rename
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The package has **zero runtime dependencies** — only Node's standard library.
|
|
83
|
+
|
|
84
|
+
## Development
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npm run lint # node --check every script
|
|
88
|
+
npm test # scaffold into a temp dir and assert JS/Android/iOS rename
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
c,
|
|
9
|
+
log,
|
|
10
|
+
step,
|
|
11
|
+
ok,
|
|
12
|
+
warn,
|
|
13
|
+
err,
|
|
14
|
+
toPascalName,
|
|
15
|
+
toSlug,
|
|
16
|
+
toLowerName,
|
|
17
|
+
toDisplayName,
|
|
18
|
+
defaultBundleId,
|
|
19
|
+
isValidBundleId,
|
|
20
|
+
isValidSlug,
|
|
21
|
+
resolveTarget,
|
|
22
|
+
} = require("../lib/utils");
|
|
23
|
+
const { createPrompter } = require("../lib/prompt");
|
|
24
|
+
const {
|
|
25
|
+
copyDir,
|
|
26
|
+
restoreDotfiles,
|
|
27
|
+
prepareEnv,
|
|
28
|
+
detectPackageManager,
|
|
29
|
+
installDeps,
|
|
30
|
+
installPods,
|
|
31
|
+
gitInit,
|
|
32
|
+
} = require("../lib/scaffold");
|
|
33
|
+
const { renameProject } = require("../lib/rename");
|
|
34
|
+
|
|
35
|
+
const TEMPLATE_DIR = path.join(__dirname, "..", "template");
|
|
36
|
+
|
|
37
|
+
// ── arg parsing ──────────────────────────────────────────────────────────────
|
|
38
|
+
function parseArgs(argv) {
|
|
39
|
+
const opts = {
|
|
40
|
+
name: undefined,
|
|
41
|
+
bundleId: undefined,
|
|
42
|
+
install: undefined, // undefined = ask
|
|
43
|
+
pods: undefined,
|
|
44
|
+
git: undefined,
|
|
45
|
+
yes: false,
|
|
46
|
+
};
|
|
47
|
+
const positional = [];
|
|
48
|
+
for (let i = 0; i < argv.length; i++) {
|
|
49
|
+
const a = argv[i];
|
|
50
|
+
if (a === "--help" || a === "-h") opts.help = true;
|
|
51
|
+
else if (a === "--version" || a === "-v") opts.version = true;
|
|
52
|
+
else if (a === "--yes" || a === "-y") opts.yes = true;
|
|
53
|
+
else if (a === "--no-install") opts.install = false;
|
|
54
|
+
else if (a === "--install") opts.install = true;
|
|
55
|
+
else if (a === "--no-pods") opts.pods = false;
|
|
56
|
+
else if (a === "--pods") opts.pods = true;
|
|
57
|
+
else if (a === "--no-git") opts.git = false;
|
|
58
|
+
else if (a === "--git") opts.git = true;
|
|
59
|
+
else if (a === "--bundle-id") opts.bundleId = argv[++i];
|
|
60
|
+
else if (a.startsWith("--bundle-id=")) opts.bundleId = a.split("=")[1];
|
|
61
|
+
else if (a.startsWith("--")) warn(`Ignoring unknown flag: ${a}`);
|
|
62
|
+
else positional.push(a);
|
|
63
|
+
}
|
|
64
|
+
if (positional.length) opts.name = positional[0];
|
|
65
|
+
return opts;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function printHelp() {
|
|
69
|
+
log(`
|
|
70
|
+
${c.bold("create-rn-starter")} — scaffold a production-ready React Native CLI app
|
|
71
|
+
|
|
72
|
+
${c.bold("Usage")}
|
|
73
|
+
npx create-rn-starter ${c.dim("[project-name] [options]")}
|
|
74
|
+
|
|
75
|
+
${c.bold("Options")}
|
|
76
|
+
--bundle-id <id> Reverse-DNS app identifier (e.g. com.acme.myapp)
|
|
77
|
+
--no-install Skip JS dependency installation
|
|
78
|
+
--no-pods Skip iOS CocoaPods installation (macOS only)
|
|
79
|
+
--no-git Skip git repository initialisation
|
|
80
|
+
-y, --yes Accept all defaults, no prompts
|
|
81
|
+
-h, --help Show this help
|
|
82
|
+
-v, --version Show version
|
|
83
|
+
|
|
84
|
+
${c.bold("Example")}
|
|
85
|
+
npx create-rn-starter awesome-app --bundle-id com.acme.awesome
|
|
86
|
+
`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function main() {
|
|
90
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
91
|
+
|
|
92
|
+
if (opts.help) return printHelp();
|
|
93
|
+
if (opts.version) {
|
|
94
|
+
const pkg = require("../package.json");
|
|
95
|
+
return log(pkg.version);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
log("");
|
|
99
|
+
log(`${c.magenta(c.bold("◆ create-rn-starter"))}`);
|
|
100
|
+
log(c.dim(" React Native CLI · React Navigation · Redux Toolkit · TypeScript\n"));
|
|
101
|
+
|
|
102
|
+
if (!fs.existsSync(TEMPLATE_DIR)) {
|
|
103
|
+
err("Bundled template/ directory is missing — the package is corrupt.");
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const prompter = createPrompter();
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// ── project name ──────────────────────────────────────────────────────
|
|
111
|
+
const projectName =
|
|
112
|
+
opts.name ||
|
|
113
|
+
(opts.yes
|
|
114
|
+
? "My RN App"
|
|
115
|
+
: await prompter.ask("Project name", {
|
|
116
|
+
defaultValue: "My RN App",
|
|
117
|
+
validate: (v) =>
|
|
118
|
+
isValidSlug(toSlug(v))
|
|
119
|
+
? true
|
|
120
|
+
: "Use letters, numbers, spaces or hyphens.",
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
const slug = toSlug(projectName);
|
|
124
|
+
if (!isValidSlug(slug)) {
|
|
125
|
+
err(`Could not derive a valid project slug from "${projectName}".`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const target = resolveTarget(process.cwd(), slug);
|
|
130
|
+
if (fs.existsSync(target) && fs.readdirSync(target).length > 0) {
|
|
131
|
+
err(`Directory "${slug}" already exists and is not empty.`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── bundle id ─────────────────────────────────────────────────────────
|
|
136
|
+
const bundleDefault = defaultBundleId(projectName);
|
|
137
|
+
const bundleId =
|
|
138
|
+
opts.bundleId ||
|
|
139
|
+
(opts.yes
|
|
140
|
+
? bundleDefault
|
|
141
|
+
: await prompter.ask("Bundle / package identifier", {
|
|
142
|
+
defaultValue: bundleDefault,
|
|
143
|
+
validate: (v) =>
|
|
144
|
+
isValidBundleId(v)
|
|
145
|
+
? true
|
|
146
|
+
: "Must be reverse-DNS, e.g. com.acme.myapp",
|
|
147
|
+
}));
|
|
148
|
+
|
|
149
|
+
if (!isValidBundleId(bundleId)) {
|
|
150
|
+
err(`Invalid bundle identifier: ${bundleId}`);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const names = {
|
|
155
|
+
pascalName: toPascalName(projectName),
|
|
156
|
+
slug,
|
|
157
|
+
lowerName: toLowerName(projectName),
|
|
158
|
+
displayName: toDisplayName(projectName),
|
|
159
|
+
bundleId,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// ── install / pods / git decisions ────────────────────────────────────
|
|
163
|
+
const wantInstall =
|
|
164
|
+
opts.install !== undefined
|
|
165
|
+
? opts.install
|
|
166
|
+
: opts.yes
|
|
167
|
+
? true
|
|
168
|
+
: await prompter.confirm("Install JS dependencies now?", true);
|
|
169
|
+
|
|
170
|
+
const canPods = process.platform === "darwin";
|
|
171
|
+
const wantPods =
|
|
172
|
+
opts.pods !== undefined
|
|
173
|
+
? opts.pods
|
|
174
|
+
: !canPods || !wantInstall
|
|
175
|
+
? false
|
|
176
|
+
: opts.yes
|
|
177
|
+
? true
|
|
178
|
+
: await prompter.confirm("Install iOS CocoaPods now?", true);
|
|
179
|
+
|
|
180
|
+
const wantGit =
|
|
181
|
+
opts.git !== undefined
|
|
182
|
+
? opts.git
|
|
183
|
+
: opts.yes
|
|
184
|
+
? true
|
|
185
|
+
: await prompter.confirm("Initialise a git repository?", true);
|
|
186
|
+
|
|
187
|
+
prompter.close();
|
|
188
|
+
|
|
189
|
+
// ── summary ───────────────────────────────────────────────────────────
|
|
190
|
+
log("");
|
|
191
|
+
log(c.bold(" Creating project with:"));
|
|
192
|
+
log(` ${c.dim("display name")} ${names.displayName}`);
|
|
193
|
+
log(` ${c.dim("app name ")} ${names.pascalName}`);
|
|
194
|
+
log(` ${c.dim("slug ")} ${names.slug}`);
|
|
195
|
+
log(` ${c.dim("bundle id ")} ${names.bundleId}`);
|
|
196
|
+
log(
|
|
197
|
+
` ${c.dim("location ")} ${
|
|
198
|
+
path.relative(process.cwd(), target) || "."
|
|
199
|
+
}`
|
|
200
|
+
);
|
|
201
|
+
log("");
|
|
202
|
+
|
|
203
|
+
// ── scaffold ──────────────────────────────────────────────────────────
|
|
204
|
+
step("Copying template files…");
|
|
205
|
+
copyDir(TEMPLATE_DIR, target);
|
|
206
|
+
|
|
207
|
+
step("Renaming project everywhere (JS, Android & iOS)…");
|
|
208
|
+
const changed = renameProject(target, names);
|
|
209
|
+
restoreDotfiles(target);
|
|
210
|
+
ok(`Updated ${changed} file${changed === 1 ? "" : "s"} and native folders.`);
|
|
211
|
+
|
|
212
|
+
step("Setting up environment files…");
|
|
213
|
+
prepareEnv(target);
|
|
214
|
+
ok("Created .env from .env.example.");
|
|
215
|
+
|
|
216
|
+
// ── deps ──────────────────────────────────────────────────────────────
|
|
217
|
+
const pm = detectPackageManager();
|
|
218
|
+
if (wantInstall) {
|
|
219
|
+
step(`Installing dependencies with ${pm}… (this can take a minute)`);
|
|
220
|
+
if (installDeps(target, pm)) ok("Dependencies installed.");
|
|
221
|
+
else warn(`"${pm} install" failed — run it manually after.`);
|
|
222
|
+
} else {
|
|
223
|
+
warn("Skipped dependency installation.");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (wantPods) {
|
|
227
|
+
step("Installing iOS CocoaPods…");
|
|
228
|
+
const podResult = installPods(target);
|
|
229
|
+
if (podResult === "ok") ok("CocoaPods installed.");
|
|
230
|
+
else if (podResult === "skipped")
|
|
231
|
+
warn("CocoaPods skipped (not macOS or pod not found).");
|
|
232
|
+
else warn("pod install failed — run it manually in ios/.");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── git ───────────────────────────────────────────────────────────────
|
|
236
|
+
if (wantGit) {
|
|
237
|
+
step("Initialising git repository…");
|
|
238
|
+
if (gitInit(target)) ok("Git repository initialised.");
|
|
239
|
+
else warn("git init failed or git is not installed — skipped.");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ── next steps ────────────────────────────────────────────────────────
|
|
243
|
+
const rel = path.relative(process.cwd(), target) || ".";
|
|
244
|
+
log("");
|
|
245
|
+
log(c.green(c.bold(" ✔ Done! Your React Native app is ready.\n")));
|
|
246
|
+
log(c.bold(" Next steps:"));
|
|
247
|
+
log(` ${c.cyan("cd")} ${rel}`);
|
|
248
|
+
if (!wantInstall) log(` ${c.cyan(pm)} install`);
|
|
249
|
+
if (!wantPods && canPods)
|
|
250
|
+
log(` ${c.cyan("cd ios && ")}${c.cyan("pod install && cd ..")}`);
|
|
251
|
+
log(` ${c.dim("# edit .env / src/services/Config.ts with your API URL")}`);
|
|
252
|
+
log(` ${c.cyan(pm === "npm" ? "npm run" : pm)} android`);
|
|
253
|
+
log(` ${c.cyan(pm === "npm" ? "npm run" : pm)} ios`);
|
|
254
|
+
log("");
|
|
255
|
+
log(
|
|
256
|
+
c.dim(
|
|
257
|
+
" Dummy auth flow: Login → Bottom Tabs (Home · Profile) → Logout, persisted via MMKV."
|
|
258
|
+
)
|
|
259
|
+
);
|
|
260
|
+
log("");
|
|
261
|
+
} catch (e) {
|
|
262
|
+
try {
|
|
263
|
+
prompter.close();
|
|
264
|
+
} catch {}
|
|
265
|
+
err(e && e.message ? e.message : String(e));
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
main();
|
package/lib/prompt.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const readline = require("readline");
|
|
4
|
+
const { c } = require("./utils");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Minimal zero-dependency prompt helpers built on Node's readline.
|
|
8
|
+
* Each returns a Promise; the shared interface is created lazily and closed
|
|
9
|
+
* by the caller via `close()`.
|
|
10
|
+
*/
|
|
11
|
+
function createPrompter() {
|
|
12
|
+
const rl = readline.createInterface({
|
|
13
|
+
input: process.stdin,
|
|
14
|
+
output: process.stdout,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const question = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Ask for free text.
|
|
21
|
+
* @param {string} message
|
|
22
|
+
* @param {object} [opts] { defaultValue, validate(value) => true | string }
|
|
23
|
+
*/
|
|
24
|
+
async function ask(message, opts = {}) {
|
|
25
|
+
const { defaultValue, validate } = opts;
|
|
26
|
+
const hint = defaultValue ? c.dim(` (${defaultValue})`) : "";
|
|
27
|
+
// eslint-disable-next-line no-constant-condition
|
|
28
|
+
while (true) {
|
|
29
|
+
const raw = (await question(`${c.cyan("?")} ${message}${hint}: `)).trim();
|
|
30
|
+
const value = raw || defaultValue || "";
|
|
31
|
+
if (!value) {
|
|
32
|
+
console.log(c.red(" Please enter a value."));
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (validate) {
|
|
36
|
+
const result = validate(value);
|
|
37
|
+
if (result !== true) {
|
|
38
|
+
console.log(c.red(` ${result}`));
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Yes/no confirmation. */
|
|
47
|
+
async function confirm(message, defaultYes = true) {
|
|
48
|
+
const hint = defaultYes ? c.dim(" (Y/n)") : c.dim(" (y/N)");
|
|
49
|
+
const raw = (await question(`${c.cyan("?")} ${message}${hint}: `))
|
|
50
|
+
.trim()
|
|
51
|
+
.toLowerCase();
|
|
52
|
+
if (!raw) return defaultYes;
|
|
53
|
+
return raw === "y" || raw === "yes";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function close() {
|
|
57
|
+
rl.close();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { ask, confirm, close };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { createPrompter };
|
package/lib/rename.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { bundleIdToPath } = require("./utils");
|
|
6
|
+
|
|
7
|
+
// ── what the bundled template currently calls itself ─────────────────────────
|
|
8
|
+
// The template is a real, working RN CLI app, so every identifier appears as a
|
|
9
|
+
// concrete literal. Renaming = swapping these source literals for the user's.
|
|
10
|
+
const SOURCE = {
|
|
11
|
+
pascalName: "AwesomeProject",
|
|
12
|
+
lowerName: "awesomeproject",
|
|
13
|
+
bundleId: "com.awesomeproject",
|
|
14
|
+
// The default identifier `react-native init` bakes into the Xcode project.
|
|
15
|
+
iosDefaultBundleId: "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// File extensions / names we treat as text and run token replacement on.
|
|
19
|
+
// Everything else (fonts, png, keystores, jars …) is copied verbatim.
|
|
20
|
+
const TEXT_EXT = new Set([
|
|
21
|
+
".js", ".jsx", ".ts", ".tsx", ".json", ".md", ".txt", ".html", ".css",
|
|
22
|
+
".yml", ".yaml", ".env", ".xml", ".plist", ".pbxproj", ".storyboard",
|
|
23
|
+
".xcscheme", ".xcworkspacedata", ".gradle", ".properties", ".kt", ".java",
|
|
24
|
+
".swift", ".h", ".m", ".mm", ".rb", ".pro", ".cfg", ".podspec", ".d.ts",
|
|
25
|
+
".xcprivacy",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const TEXT_BASENAMES = new Set([
|
|
29
|
+
"_gitignore", ".gitignore", "Podfile", "Gemfile", "gradlew", "gradlew.bat",
|
|
30
|
+
".watchmanconfig", "app.json",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
function isTextFile(filePath) {
|
|
34
|
+
const base = path.basename(filePath);
|
|
35
|
+
if (TEXT_BASENAMES.has(base)) return true;
|
|
36
|
+
return TEXT_EXT.has(path.extname(filePath).toLowerCase());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function escapeRegExp(s) {
|
|
40
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Walk every file under `dir`, yielding absolute paths. */
|
|
44
|
+
function walk(dir, out = []) {
|
|
45
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
46
|
+
const full = path.join(dir, entry.name);
|
|
47
|
+
if (entry.isDirectory()) walk(full, out);
|
|
48
|
+
else out.push(full);
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Replace every source identifier with the user's values across all text
|
|
55
|
+
* files. Longer keys are applied first so `org.reactjs.native…` and
|
|
56
|
+
* `com.awesomeproject` are handled before the bare `awesomeproject`.
|
|
57
|
+
*/
|
|
58
|
+
function applyTokenReplacements(root, names) {
|
|
59
|
+
const tokens = {
|
|
60
|
+
[SOURCE.iosDefaultBundleId]: names.bundleId,
|
|
61
|
+
[SOURCE.bundleId]: names.bundleId,
|
|
62
|
+
[SOURCE.pascalName]: names.pascalName,
|
|
63
|
+
[SOURCE.lowerName]: names.lowerName,
|
|
64
|
+
};
|
|
65
|
+
const ordered = Object.keys(tokens).sort((a, b) => b.length - a.length);
|
|
66
|
+
const patterns = ordered.map((key) => ({
|
|
67
|
+
re: new RegExp(escapeRegExp(key), "g"),
|
|
68
|
+
value: tokens[key],
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
let changed = 0;
|
|
72
|
+
for (const file of walk(root)) {
|
|
73
|
+
if (!isTextFile(file)) continue;
|
|
74
|
+
let content;
|
|
75
|
+
try {
|
|
76
|
+
content = fs.readFileSync(file, "utf8");
|
|
77
|
+
} catch {
|
|
78
|
+
continue; // unreadable / binary disguised as text — skip safely
|
|
79
|
+
}
|
|
80
|
+
let next = content;
|
|
81
|
+
for (const { re, value } of patterns) next = next.replace(re, value);
|
|
82
|
+
if (next !== content) {
|
|
83
|
+
fs.writeFileSync(file, next);
|
|
84
|
+
changed += 1;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return changed;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function renameIfExists(from, to) {
|
|
91
|
+
if (fs.existsSync(from) && from !== to) {
|
|
92
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
93
|
+
fs.renameSync(from, to);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Rename the iOS folder, Xcode project, workspace and shared scheme so they
|
|
101
|
+
* match the new app name. The token pass above already rewrote every internal
|
|
102
|
+
* reference inside their files, so the names line up after this move.
|
|
103
|
+
*/
|
|
104
|
+
function renameIosDirs(root, names) {
|
|
105
|
+
const ios = path.join(root, "ios");
|
|
106
|
+
if (!fs.existsSync(ios)) return;
|
|
107
|
+
const P = names.pascalName;
|
|
108
|
+
|
|
109
|
+
renameIfExists(path.join(ios, SOURCE.pascalName), path.join(ios, P));
|
|
110
|
+
renameIfExists(
|
|
111
|
+
path.join(ios, `${SOURCE.pascalName}.xcodeproj`),
|
|
112
|
+
path.join(ios, `${P}.xcodeproj`)
|
|
113
|
+
);
|
|
114
|
+
renameIfExists(
|
|
115
|
+
path.join(ios, `${SOURCE.pascalName}.xcworkspace`),
|
|
116
|
+
path.join(ios, `${P}.xcworkspace`)
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// The shared scheme lives inside the (now renamed) .xcodeproj.
|
|
120
|
+
const schemeDir = path.join(
|
|
121
|
+
ios,
|
|
122
|
+
`${P}.xcodeproj`,
|
|
123
|
+
"xcshareddata",
|
|
124
|
+
"xcschemes"
|
|
125
|
+
);
|
|
126
|
+
renameIfExists(
|
|
127
|
+
path.join(schemeDir, `${SOURCE.pascalName}.xcscheme`),
|
|
128
|
+
path.join(schemeDir, `${P}.xcscheme`)
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Recursively remove a directory and any parents that become empty, up to `stopAt`. */
|
|
133
|
+
function pruneEmptyDirs(dir, stopAt) {
|
|
134
|
+
let cur = dir;
|
|
135
|
+
while (cur.startsWith(stopAt) && cur !== stopAt) {
|
|
136
|
+
if (fs.existsSync(cur) && fs.readdirSync(cur).length === 0) {
|
|
137
|
+
fs.rmdirSync(cur);
|
|
138
|
+
cur = path.dirname(cur);
|
|
139
|
+
} else {
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Move the Android source from `…/java/com/awesomeproject` to the package path
|
|
147
|
+
* implied by the new bundle id (e.g. `…/java/com/acme/myapp`). The `package`
|
|
148
|
+
* declaration inside MainActivity/MainApplication was already rewritten by the
|
|
149
|
+
* token pass, so the folder layout just needs to follow.
|
|
150
|
+
*/
|
|
151
|
+
function moveAndroidPackage(root, names) {
|
|
152
|
+
const javaRoot = path.join(root, "android", "app", "src", "main", "java");
|
|
153
|
+
const oldDir = path.join(javaRoot, ...bundleIdToPath(SOURCE.bundleId));
|
|
154
|
+
if (!fs.existsSync(oldDir)) return;
|
|
155
|
+
|
|
156
|
+
const newDir = path.join(javaRoot, ...bundleIdToPath(names.bundleId));
|
|
157
|
+
if (path.resolve(oldDir) === path.resolve(newDir)) return;
|
|
158
|
+
|
|
159
|
+
fs.mkdirSync(newDir, { recursive: true });
|
|
160
|
+
for (const entry of fs.readdirSync(oldDir)) {
|
|
161
|
+
fs.renameSync(path.join(oldDir, entry), path.join(newDir, entry));
|
|
162
|
+
}
|
|
163
|
+
pruneEmptyDirs(oldDir, javaRoot);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── targeted edits the token pass can't safely express ───────────────────────
|
|
167
|
+
|
|
168
|
+
function setJsonField(file, mutate) {
|
|
169
|
+
if (!fs.existsSync(file)) return;
|
|
170
|
+
const json = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
171
|
+
mutate(json);
|
|
172
|
+
fs.writeFileSync(file, JSON.stringify(json, null, 2) + "\n");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function replaceInFile(file, re, value) {
|
|
176
|
+
if (!fs.existsSync(file)) return;
|
|
177
|
+
const content = fs.readFileSync(file, "utf8");
|
|
178
|
+
const next = content.replace(re, value);
|
|
179
|
+
if (next !== content) fs.writeFileSync(file, next);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Apply the human-facing values that differ from the canonical PascalCase name:
|
|
184
|
+
* • package.json `name` → npm slug (lowercase, hyphenated)
|
|
185
|
+
* • app.json name/displayName
|
|
186
|
+
* • Android strings.xml app_name → display name
|
|
187
|
+
* • iOS Info.plist CFBundleDisplayName → display name
|
|
188
|
+
*/
|
|
189
|
+
function applyTargetedEdits(root, names) {
|
|
190
|
+
// package.json — npm names must be lowercase.
|
|
191
|
+
setJsonField(path.join(root, "package.json"), (pkg) => {
|
|
192
|
+
pkg.name = names.slug;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// app.json — `name` is the AppRegistry key (must equal getMainComponentName).
|
|
196
|
+
setJsonField(path.join(root, "app.json"), (app) => {
|
|
197
|
+
app.name = names.pascalName;
|
|
198
|
+
app.displayName = names.displayName;
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Android home-screen label.
|
|
202
|
+
replaceInFile(
|
|
203
|
+
path.join(root, "android/app/src/main/res/values/strings.xml"),
|
|
204
|
+
/(<string name="app_name">)[^<]*(<\/string>)/,
|
|
205
|
+
`$1${names.displayName}$2`
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// iOS home-screen label.
|
|
209
|
+
replaceInFile(
|
|
210
|
+
path.join(root, "ios", names.pascalName, "Info.plist"),
|
|
211
|
+
/(<key>CFBundleDisplayName<\/key>\s*<string>)[^<]*(<\/string>)/,
|
|
212
|
+
`$1${names.displayName}$2`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Full rename pipeline. Order matters: rewrite file *contents* first (while the
|
|
218
|
+
* folders still sit at their source paths), then move the native folders, then
|
|
219
|
+
* apply the few targeted human-facing values.
|
|
220
|
+
*
|
|
221
|
+
* @returns {number} number of files whose contents changed in the token pass.
|
|
222
|
+
*/
|
|
223
|
+
function renameProject(root, names) {
|
|
224
|
+
const changed = applyTokenReplacements(root, names);
|
|
225
|
+
renameIosDirs(root, names);
|
|
226
|
+
moveAndroidPackage(root, names);
|
|
227
|
+
applyTargetedEdits(root, names);
|
|
228
|
+
return changed;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
module.exports = {
|
|
232
|
+
SOURCE,
|
|
233
|
+
isTextFile,
|
|
234
|
+
applyTokenReplacements,
|
|
235
|
+
renameIosDirs,
|
|
236
|
+
moveAndroidPackage,
|
|
237
|
+
applyTargetedEdits,
|
|
238
|
+
renameProject,
|
|
239
|
+
};
|