codingpixel-expo-app 0.1.0
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 +112 -0
- package/bin/cli.js +9 -0
- package/dist/babel.js +198 -0
- package/dist/bootstrap.js +54 -0
- package/dist/fonts.js +98 -0
- package/dist/index.js +120 -0
- package/dist/install.js +243 -0
- package/dist/overlay.js +141 -0
- package/dist/patch.js +212 -0
- package/dist/prompts.js +115 -0
- package/dist/scaffold.js +43 -0
- package/dist/sdkNotes.js +19 -0
- package/dist/util.js +48 -0
- package/package.json +56 -0
- package/templates/base/assets/fonts/.gitkeep +0 -0
- package/templates/base/assets/images/.gitkeep +0 -0
- package/templates/base/assets/index.ts +8 -0
- package/templates/base/src/app/_layout.tsx +39 -0
- package/templates/base/src/app/index.tsx +13 -0
- package/templates/base/src/app/routes.tsx +9 -0
- package/templates/base/src/core/hooks/.gitkeep +0 -0
- package/templates/base/src/core/redux/hooks.ts +6 -0
- package/templates/base/src/core/redux/mmkvStorage.ts +18 -0
- package/templates/base/src/core/redux/reducers.ts +20 -0
- package/templates/base/src/core/redux/slices/userSlice.ts +31 -0
- package/templates/base/src/core/redux/store.ts +28 -0
- package/templates/base/src/core/services/.gitkeep +0 -0
- package/templates/base/src/core/tanstack/index.tsx +93 -0
- package/templates/base/src/core/tanstack/tanstack-keys.ts +42 -0
- package/templates/base/src/core/utils/config.ts +50 -0
- package/templates/base/src/core/utils/constants.ts +11 -0
- package/templates/base/src/core/utils/endpoints.ts +29 -0
- package/templates/base/src/core/utils/types.ts +47 -0
- package/templates/base/src/core/utils/validation.ts +10 -0
- package/templates/base/src/features/.gitkeep +0 -0
- package/templates/base/src/ui/appComponents/AppRefreshControl/index.tsx +39 -0
- package/templates/base/src/ui/appComponents/appButton/index.tsx +122 -0
- package/templates/base/src/ui/appComponents/appColumnView/index.tsx +320 -0
- package/templates/base/src/ui/appComponents/appFlashList/index.tsx +191 -0
- package/templates/base/src/ui/appComponents/appFlatList/index.tsx +172 -0
- package/templates/base/src/ui/appComponents/appIcon/index.tsx +105 -0
- package/templates/base/src/ui/appComponents/appInput/index.tsx +226 -0
- package/templates/base/src/ui/appComponents/appKeyboardAvoidingView/index.tsx +168 -0
- package/templates/base/src/ui/appComponents/appKeyboardAwareScrollView/index.tsx +188 -0
- package/templates/base/src/ui/appComponents/appLogger/index.tsx +22 -0
- package/templates/base/src/ui/appComponents/appPressable/index.tsx +220 -0
- package/templates/base/src/ui/appComponents/appRowView/index.tsx +320 -0
- package/templates/base/src/ui/appComponents/appSafeAreaInsets/index.tsx +24 -0
- package/templates/base/src/ui/appComponents/appScrollView/index.tsx +166 -0
- package/templates/base/src/ui/appComponents/appSkeleton/index.tsx +22 -0
- package/templates/base/src/ui/appComponents/appStatusBar/index.tsx +8 -0
- package/templates/base/src/ui/appComponents/appTabHeader/index.tsx +111 -0
- package/templates/base/src/ui/appComponents/appText/index.tsx +258 -0
- package/templates/base/src/ui/appComponents/appTextWrapper/index.tsx +84 -0
- package/templates/base/src/ui/appComponents/appWrapper/index.tsx +56 -0
- package/templates/base/src/ui/appComponents/customActivityIndicator/index.tsx +34 -0
- package/templates/base/src/ui/appComponents/customGetPermissionModal/index.tsx +62 -0
- package/templates/base/src/ui/appComponents/customModal/index.tsx +26 -0
- package/templates/base/src/ui/appComponents/customTextInput/index.tsx +143 -0
- package/templates/base/src/ui/components/.gitkeep +0 -0
- package/templates/base/src/ui/components/avatarBlock/index.tsx +42 -0
- package/templates/base/src/ui/components/backgroundGradient/index.tsx +24 -0
- package/templates/base/src/ui/components/errorFallback/index.tsx +40 -0
- package/templates/base/src/ui/iconComponents/IconFontAwesome6/index.tsx +22 -0
- package/templates/base/src/ui/iconComponents/IoniconsIcons/index.tsx +33 -0
- package/templates/base/src/ui/iconComponents/antDesignicons/index.tsx +22 -0
- package/templates/base/src/ui/iconComponents/featherIcons/index.tsx +22 -0
- package/templates/base/src/ui/iconComponents/materialCommunityIcons/index.tsx +24 -0
- package/templates/base/src/ui/iconComponents/materialIcons/index.tsx +23 -0
- package/templates/base/src/ui/iconComponents/octiconsIcons/index.tsx +23 -0
- package/templates/base/src/ui/theme/allFileStyles.ts +39 -0
- package/templates/base/src/ui/theme/colors.ts +54 -0
- package/templates/base/src/ui/theme/fonts.ts +3 -0
- package/templates/base/src/ui/theme/responsive.ts +27 -0
- package/templates/bottom-sheet/src/ui/appComponents/BottomSheetKeyboardAwareScrollView/index.tsx +111 -0
- package/templates/bottom-sheet/src/ui/appComponents/appBottomSheetBackdrop/index.tsx +27 -0
- package/templates/bottom-sheet/src/ui/appComponents/appBottomSheetScrollView/index.tsx +99 -0
- package/templates/bottom-sheet/src/ui/appComponents/appBottomSheetView/index.tsx +173 -0
- package/templates/bottom-sheet/src/ui/appComponents/customBottomSheetModal/index.tsx +17 -0
- package/templates/claude-command/init-app.md +41 -0
- package/templates/image-picker/media-constants.snippet.ts +18 -0
- package/templates/image-picker/src/core/services/PermissionService.ts +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 codingpixel
|
|
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,112 @@
|
|
|
1
|
+
# `codingpixel-expo-app`
|
|
2
|
+
|
|
3
|
+
Opinionated Expo SDK 54+ app scaffolder. Mirrors the [MyRoster](https://github.com/) project's component / redux / theme conventions; layers them on top of `create-expo-app`'s `blank-typescript` template; runs all native-dep installs through `expo install` so versions match the target SDK; ships a single lockfile (yarn OR npm — never both).
|
|
4
|
+
|
|
5
|
+
## Quickstart
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx codingpixel-expo-app my-app
|
|
9
|
+
cd my-app
|
|
10
|
+
npx expo prebuild
|
|
11
|
+
yarn ios # or: npm run ios / yarn android / npm run android
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The bin name is `codingpixel-expo` (used after global install). Invoke through `npx codingpixel-expo-app` (the package name) for one-shot runs.
|
|
15
|
+
|
|
16
|
+
## Not Expo Go-compatible
|
|
17
|
+
|
|
18
|
+
This template ships native modules that Expo Go can't load:
|
|
19
|
+
|
|
20
|
+
- `react-native-mmkv` (redux-persist storage)
|
|
21
|
+
- `react-native-gesture-handler`
|
|
22
|
+
- `react-native-reanimated` (+ `react-native-worklets`)
|
|
23
|
+
- `react-native-keyboard-controller`
|
|
24
|
+
- `@gorhom/bottom-sheet` (when bottom-sheet support enabled)
|
|
25
|
+
|
|
26
|
+
You **must** prebuild + run a custom dev-client:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx expo prebuild
|
|
30
|
+
yarn ios # or yarn android / npm run ios / npm run android
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Subsequent runs use the dev-client + bundler (`yarn start`).
|
|
34
|
+
|
|
35
|
+
## Fonts
|
|
36
|
+
|
|
37
|
+
Fonts are intentionally disabled in v0.1.x — generated `src/ui/theme/fonts.ts` exports `Fonts = {} as const` + `FontKey = never`, and `_layout.tsx` ships without `useFonts`. Apps that need custom fonts wire `expo-font` themselves (drop `.ttf`s into `assets/fonts/`, populate `Fonts`, add `useFonts(...)` + a loading guard in `_layout.tsx`).
|
|
38
|
+
|
|
39
|
+
`expo-font` is already in `dependencies`, so no extra install is needed.
|
|
40
|
+
|
|
41
|
+
## `@/*` alias
|
|
42
|
+
|
|
43
|
+
The template overrides `expo/tsconfig.base`'s default by setting `@/*` → `src/*`. All MyRoster-style aliases (`@theme/*`, `@utils/*`, `@redux/*`, `@core/*`, `@services/*`, `@hooks/*`, `@appComponents/*`, `@components/*`, `@icons/*`, `@features/*`, `@assets`) resolve to their concrete dirs in `src/`. See `tsconfig.json` `compilerOptions.paths`.
|
|
44
|
+
|
|
45
|
+
If your generated app's `tsconfig.json` already has a `@/*` mapping, the CLI preserves it + warns.
|
|
46
|
+
|
|
47
|
+
## First-time dev-client build
|
|
48
|
+
|
|
49
|
+
iOS:
|
|
50
|
+
- Xcode + CocoaPods installed (`sudo gem install cocoapods` if missing).
|
|
51
|
+
- `npx expo prebuild` then `yarn ios` (or `npm run ios`) — opens the iOS simulator with your custom dev-client.
|
|
52
|
+
|
|
53
|
+
Android:
|
|
54
|
+
- Android SDK + emulator running (or a USB-attached device with debugging enabled).
|
|
55
|
+
- `JAVA_HOME` pointing at JDK 17.
|
|
56
|
+
- `npx expo prebuild` then `yarn android` (or `npm run android`).
|
|
57
|
+
|
|
58
|
+
## Expo SDK compatibility
|
|
59
|
+
|
|
60
|
+
Built + tested against Expo SDK 54.x (probed via `docs/SDK_NOTES.md` at template-author time). Native dep versions are inherited at scaffold time via `expo install`, so subsequent SDK bumps update the deps without a CLI release.
|
|
61
|
+
|
|
62
|
+
If `expo install` reports version-resolution failures, the CLI runs an isolation retry (`retryWithIsolation`) — drops the failing dep, retries the rest, and surfaces the failed name + verbatim Expo stderr at the end.
|
|
63
|
+
|
|
64
|
+
## Environment variables (non-interactive runs)
|
|
65
|
+
|
|
66
|
+
Required when stdin is not a TTY (e.g. slash-command flows).
|
|
67
|
+
|
|
68
|
+
| Var | Type | Notes |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| `EXPO_INCLUDE_BOTTOM_SHEET` | `"0"` or `"1"` | Other values throw before any fs mutation. |
|
|
71
|
+
| `EXPO_INCLUDE_IMAGE_PICKER` | `"0"` or `"1"` | Same. |
|
|
72
|
+
| `EXPO_PACKAGE_MANAGER` | `"yarn"` or `"npm"` | Optional override; auto-detect otherwise. Other values throw. |
|
|
73
|
+
|
|
74
|
+
`EXPO_PRIMARY_FONT` / `EXPO_SECONDARY_FONT` are silently ignored (fonts disabled — see "Fonts" above).
|
|
75
|
+
|
|
76
|
+
## Bin name
|
|
77
|
+
|
|
78
|
+
- npm package: `codingpixel-expo-app` (unscoped, public).
|
|
79
|
+
- Bin: `codingpixel-expo`.
|
|
80
|
+
- Run as `npx codingpixel-expo-app <dir>` (the package name) — `npx codingpixel-expo` only resolves after a `yarn global add` / `npm i -g` of this package.
|
|
81
|
+
|
|
82
|
+
## Recovery from mid-run failures
|
|
83
|
+
|
|
84
|
+
The CLI mutates `<target>` in place. If a patch throws partway through:
|
|
85
|
+
|
|
86
|
+
1. **Patches are idempotent.** Fix the root cause (e.g. install missing PATH entry, retry network) and re-run the same command. The CLI converges.
|
|
87
|
+
2. **Or:** `rm -rf <target>` and start fresh.
|
|
88
|
+
|
|
89
|
+
The CLI **never** auto-deletes the target dir.
|
|
90
|
+
|
|
91
|
+
## Contributing / publish
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
yarn install
|
|
95
|
+
yarn build
|
|
96
|
+
yarn test
|
|
97
|
+
yarn audit:templates # also runs as prepublishOnly
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`prepublishOnly` chains `npm run build && npm run test && npm run audit:templates` — `audit:templates` greps mirrored files for unmapped MyRoster prefixes + Fonts-as-type misuse, blocking publish on stale audits. See `docs/MIRROR_NOTES.md` for the deviation log.
|
|
101
|
+
|
|
102
|
+
Publish:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
npm publish
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
(Unscoped packages publish public by default — no `--access` flag needed.)
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT.
|
package/bin/cli.js
ADDED
package/dist/babel.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// Phase 7 step 4 — `babel.config.js` AST merge.
|
|
2
|
+
//
|
|
3
|
+
// Why AST + not regex:
|
|
4
|
+
// create-expo-app's default `babel.config.js` is a `module.exports = function`
|
|
5
|
+
// returning an object literal. We need to insert/dedup `plugins[]` entries
|
|
6
|
+
// while preserving comments, presets, env-conditional branches, etc. Regex
|
|
7
|
+
// gets fragile fast; AST is the only safe path.
|
|
8
|
+
//
|
|
9
|
+
// Why plugin order matters:
|
|
10
|
+
// reanimated/worklets plugin MUST be the LAST entry in `plugins[]` per
|
|
11
|
+
// upstream docs. `module-resolver` (when we insert it) goes at the FRONT
|
|
12
|
+
// so its presence can never displace the worklets last-slot reservation.
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { parse } from "@babel/parser";
|
|
16
|
+
import * as _traverseNS from "@babel/traverse";
|
|
17
|
+
import * as _generateNS from "@babel/generator";
|
|
18
|
+
import * as t from "@babel/types";
|
|
19
|
+
import { fileExists, log } from "./util.js";
|
|
20
|
+
// ESM/CJS interop — Node's ESM loader wraps CJS modules so @babel/traverse +
|
|
21
|
+
// @babel/generator surface as `{ default: { default: <fn>, ...other } }` from
|
|
22
|
+
// `import * as ns`. Unwrap up to two levels so the callable is reached
|
|
23
|
+
// regardless of how the underlying CJS module is shaped (some Babel versions
|
|
24
|
+
// expose the function directly on .default; newer ones nest one level deeper).
|
|
25
|
+
function unwrap(ns) {
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
27
|
+
let cur = ns;
|
|
28
|
+
for (let i = 0; i < 3; i++) {
|
|
29
|
+
if (typeof cur === "function")
|
|
30
|
+
return cur;
|
|
31
|
+
if (cur && typeof cur.default !== "undefined") {
|
|
32
|
+
cur = cur.default;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
return cur;
|
|
38
|
+
}
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
40
|
+
const traverse = unwrap(_traverseNS);
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
const generate = unwrap(_generateNS);
|
|
43
|
+
const isWorklets = (entry) => {
|
|
44
|
+
const name = nameOfEntry(entry);
|
|
45
|
+
return /^react-native-worklets(-core)?\/plugin$/.test(name);
|
|
46
|
+
};
|
|
47
|
+
const isModuleResolver = (entry) => nameOfEntry(entry) === "module-resolver";
|
|
48
|
+
function nameOfEntry(entry) {
|
|
49
|
+
if (Array.isArray(entry))
|
|
50
|
+
return String(entry[0]);
|
|
51
|
+
return String(entry);
|
|
52
|
+
}
|
|
53
|
+
/** Get the string name from an AST plugin entry (string literal or array literal whose first elem is a string). */
|
|
54
|
+
function pluginEntryName(node) {
|
|
55
|
+
if (t.isStringLiteral(node))
|
|
56
|
+
return node.value;
|
|
57
|
+
if (t.isArrayExpression(node)) {
|
|
58
|
+
const first = node.elements[0];
|
|
59
|
+
if (first && t.isStringLiteral(first))
|
|
60
|
+
return first.value;
|
|
61
|
+
}
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
64
|
+
/** Build an AST node for `["module-resolver", { alias: <map> }]`. */
|
|
65
|
+
function buildModuleResolverEntry(aliasMap) {
|
|
66
|
+
const aliasObject = t.objectExpression(Object.entries(aliasMap).map(([from, to]) => t.objectProperty(t.stringLiteral(from), t.stringLiteral(to))));
|
|
67
|
+
return t.arrayExpression([
|
|
68
|
+
t.stringLiteral("module-resolver"),
|
|
69
|
+
t.objectExpression([t.objectProperty(t.identifier("alias"), aliasObject)]),
|
|
70
|
+
]);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Patch `<target>/babel.config.js`. Idempotent. Throws on parse failure or on
|
|
74
|
+
* unsupported config form (e.g. .ts/.cjs/.mjs only).
|
|
75
|
+
*/
|
|
76
|
+
export function patchBabel(target, opts) {
|
|
77
|
+
// File-extension detection: .ts/.cjs/.mjs without .js → unsupported.
|
|
78
|
+
const jsPath = path.join(target, "babel.config.js");
|
|
79
|
+
for (const alt of ["babel.config.ts", "babel.config.cjs", "babel.config.mjs"]) {
|
|
80
|
+
if (fileExists(path.join(target, alt)) && !fileExists(jsPath)) {
|
|
81
|
+
throw new Error(`manual babel config (${alt}) not yet supported — patchBabel handles babel.config.js only.`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Deviation #8 (docs/MIRROR_NOTES.md): SDK 54 blank-typescript no longer
|
|
85
|
+
// ships `babel.config.js` — preset-only via expo-router auto-config.
|
|
86
|
+
// Create the minimal stub here; patcher AST below then layers our entries on top.
|
|
87
|
+
if (!fileExists(jsPath)) {
|
|
88
|
+
const stub = `module.exports = function (api) {
|
|
89
|
+
api.cache(true);
|
|
90
|
+
return {
|
|
91
|
+
presets: ['babel-preset-expo'],
|
|
92
|
+
plugins: [],
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
`;
|
|
96
|
+
fs.writeFileSync(jsPath, stub);
|
|
97
|
+
log.step("babel.config.js missing — wrote default stub before patching.");
|
|
98
|
+
}
|
|
99
|
+
const source = fs.readFileSync(jsPath, "utf8");
|
|
100
|
+
const ast = parse(source, {
|
|
101
|
+
sourceType: "script", // create-expo-app default is CJS module.exports
|
|
102
|
+
plugins: [],
|
|
103
|
+
});
|
|
104
|
+
// Locate the returned ObjectExpression (return value of the exported function).
|
|
105
|
+
// `let configObj: any` so traverse-callback assignments don't trip TS narrowing.
|
|
106
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
107
|
+
let configObj = null;
|
|
108
|
+
traverse(ast, {
|
|
109
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
110
|
+
ReturnStatement(p) {
|
|
111
|
+
if (configObj)
|
|
112
|
+
return;
|
|
113
|
+
const arg = p.node.argument;
|
|
114
|
+
if (arg && t.isObjectExpression(arg)) {
|
|
115
|
+
configObj = arg;
|
|
116
|
+
p.stop();
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
if (!configObj) {
|
|
121
|
+
// Fallback: top-level `module.exports = { ... }` form (no function wrapper).
|
|
122
|
+
traverse(ast, {
|
|
123
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
124
|
+
AssignmentExpression(p) {
|
|
125
|
+
if (configObj)
|
|
126
|
+
return;
|
|
127
|
+
if (t.isMemberExpression(p.node.left) &&
|
|
128
|
+
t.isIdentifier(p.node.left.object, { name: "module" }) &&
|
|
129
|
+
t.isIdentifier(p.node.left.property, { name: "exports" }) &&
|
|
130
|
+
t.isObjectExpression(p.node.right)) {
|
|
131
|
+
configObj = p.node.right;
|
|
132
|
+
p.stop();
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
if (!configObj) {
|
|
138
|
+
throw new Error("patchBabel: could not locate config ObjectExpression in babel.config.js");
|
|
139
|
+
}
|
|
140
|
+
const config = configObj;
|
|
141
|
+
// Find or create `plugins` property.
|
|
142
|
+
let pluginsProp = config.properties.find((p) => t.isObjectProperty(p) &&
|
|
143
|
+
((t.isIdentifier(p.key) && p.key.name === "plugins") ||
|
|
144
|
+
(t.isStringLiteral(p.key) && p.key.value === "plugins")));
|
|
145
|
+
if (!pluginsProp) {
|
|
146
|
+
pluginsProp = t.objectProperty(t.identifier("plugins"), t.arrayExpression([]));
|
|
147
|
+
config.properties.push(pluginsProp);
|
|
148
|
+
}
|
|
149
|
+
if (!t.isArrayExpression(pluginsProp.value)) {
|
|
150
|
+
throw new Error("patchBabel: plugins is not an array literal — cannot patch.");
|
|
151
|
+
}
|
|
152
|
+
const plugins = pluginsProp.value;
|
|
153
|
+
// ----- Step 0: decision prelude -----
|
|
154
|
+
const namesOnly = plugins.elements
|
|
155
|
+
.filter((e) => e !== null)
|
|
156
|
+
.map(pluginEntryName);
|
|
157
|
+
const moduleResolverPresent = namesOnly.includes("module-resolver");
|
|
158
|
+
const moduleResolverIsLast = namesOnly.length > 0 && namesOnly[namesOnly.length - 1] === "module-resolver";
|
|
159
|
+
const workletsPresent = namesOnly.some((n) => /^react-native-worklets(-core)?\/plugin$/.test(n));
|
|
160
|
+
const workletsNeedsInsertion = !opts.workletsAutoIncluded && !workletsPresent;
|
|
161
|
+
// ----- Step A: module-resolver -----
|
|
162
|
+
if (moduleResolverIsLast && workletsNeedsInsertion) {
|
|
163
|
+
throw new Error("module-resolver cannot occupy the final plugin slot when worklets plugin is required; " +
|
|
164
|
+
"please reorder babel.config.js plugins manually.");
|
|
165
|
+
}
|
|
166
|
+
if (!moduleResolverPresent) {
|
|
167
|
+
plugins.elements.unshift(buildModuleResolverEntry(opts.aliasMap));
|
|
168
|
+
}
|
|
169
|
+
// ----- Step B: worklets -----
|
|
170
|
+
if (workletsNeedsInsertion) {
|
|
171
|
+
plugins.elements.push(t.stringLiteral(`${opts.workletsPkg}/plugin`));
|
|
172
|
+
}
|
|
173
|
+
// ----- Step C: post-patch invariant assert with user-mutation tolerance -----
|
|
174
|
+
const finalNames = plugins.elements
|
|
175
|
+
.filter((e) => e !== null)
|
|
176
|
+
.map(pluginEntryName);
|
|
177
|
+
const workletsIdx = finalNames.findIndex((n) => /^react-native-worklets(-core)?\/plugin$/.test(n));
|
|
178
|
+
if (workletsIdx !== -1 && workletsIdx !== finalNames.length - 1) {
|
|
179
|
+
const trailing = finalNames.slice(workletsIdx + 1);
|
|
180
|
+
const OURS = new Set(["module-resolver"]);
|
|
181
|
+
const oursAfter = trailing.filter((n) => OURS.has(n));
|
|
182
|
+
const userAfter = trailing.filter((n) => !OURS.has(n));
|
|
183
|
+
if (oursAfter.length > 0) {
|
|
184
|
+
throw new Error(`babel.config.js: our patch corrupted plugin order — worklets must be last. ` +
|
|
185
|
+
`Offending trailing entries from our patch: ${oursAfter.join(", ")}`);
|
|
186
|
+
}
|
|
187
|
+
if (userAfter.length > 0) {
|
|
188
|
+
log.warn(`babel.config.js: plugin(s) [${userAfter.join(", ")}] appear after worklets — ` +
|
|
189
|
+
`reanimated requires worklets last; please reorder.`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Regenerate.
|
|
193
|
+
const out = generate(ast, { retainLines: false, comments: true }, source).code;
|
|
194
|
+
fs.writeFileSync(jsPath, out + (out.endsWith("\n") ? "" : "\n"));
|
|
195
|
+
// sanity: also verify the runtime predicates still match what we computed.
|
|
196
|
+
void isWorklets;
|
|
197
|
+
void isModuleResolver;
|
|
198
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import prompts from "prompts";
|
|
3
|
+
import { dirExists, ensureDir, fileExists, isDirEmpty } from "./util.js";
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the target directory + app name from a positional CLI arg.
|
|
6
|
+
*
|
|
7
|
+
* Modes (per PLAN_V5.md Phase 1):
|
|
8
|
+
* - `arg === undefined | ""` → prompt for name; recurse with answer.
|
|
9
|
+
* - `arg === "."` → use cwd. Throws if `package.json` already present.
|
|
10
|
+
* - `arg` non-empty → join with cwd, mkdir -p; throws if non-empty.
|
|
11
|
+
*
|
|
12
|
+
* Hard rejects (security + clarity):
|
|
13
|
+
* - Absolute paths (Phase 1 step 2).
|
|
14
|
+
* - `..`-traversal (Phase 1 step 2).
|
|
15
|
+
*/
|
|
16
|
+
export async function resolveTargetDir(arg) {
|
|
17
|
+
// Empty-arg branch — prompt then recurse.
|
|
18
|
+
if (arg === undefined || arg === "") {
|
|
19
|
+
if (!process.stdin.isTTY) {
|
|
20
|
+
throw new Error('No app name provided and stdin is not a TTY. Pass a directory: `codingpixel-expo my-app`.');
|
|
21
|
+
}
|
|
22
|
+
const ans = await prompts({
|
|
23
|
+
type: "text",
|
|
24
|
+
name: "name",
|
|
25
|
+
message: "App name?",
|
|
26
|
+
validate: (v) => (v.trim() === "" ? "Required" : true),
|
|
27
|
+
});
|
|
28
|
+
if (!ans.name)
|
|
29
|
+
throw new Error("Aborted.");
|
|
30
|
+
return resolveTargetDir(String(ans.name).trim());
|
|
31
|
+
}
|
|
32
|
+
// Reject absolute paths + ..-traversal BEFORE any path math.
|
|
33
|
+
if (path.isAbsolute(arg)) {
|
|
34
|
+
throw new Error(`Absolute paths not allowed: "${arg}". Use a directory name relative to cwd.`);
|
|
35
|
+
}
|
|
36
|
+
if (arg.split(/[/\\]/).some((seg) => seg === "..")) {
|
|
37
|
+
throw new Error(`Path traversal not allowed: "${arg}". Use a name relative to cwd.`);
|
|
38
|
+
}
|
|
39
|
+
// Current-dir branch.
|
|
40
|
+
if (arg === ".") {
|
|
41
|
+
const cwd = process.cwd();
|
|
42
|
+
if (fileExists(path.join(cwd, "package.json"))) {
|
|
43
|
+
throw new Error(`Refusing to scaffold into "." — package.json already present in ${cwd}.`);
|
|
44
|
+
}
|
|
45
|
+
return { dir: cwd, name: path.basename(cwd) };
|
|
46
|
+
}
|
|
47
|
+
// Named-dir branch.
|
|
48
|
+
const dir = path.join(process.cwd(), arg);
|
|
49
|
+
if (dirExists(dir) && !isDirEmpty(dir)) {
|
|
50
|
+
throw new Error(`Target directory not empty: ${dir}`);
|
|
51
|
+
}
|
|
52
|
+
ensureDir(dir);
|
|
53
|
+
return { dir, name: path.basename(dir) };
|
|
54
|
+
}
|
package/dist/fonts.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Phase 6 generators. All return strings to be spliced into sentinel positions.
|
|
2
|
+
// Decision-locked: fonts type is object literal `as const` + `FontKey`
|
|
3
|
+
// (NOT TS `enum` — Babel-unsafe under Hermes).
|
|
4
|
+
/**
|
|
5
|
+
* Build the `Fonts` object literal + `FontKey` type. Family naming convention:
|
|
6
|
+
* - `<FontName>-Regular` (always)
|
|
7
|
+
* - `<FontName>-Medium`, `-SemiBold`, `-Bold` (always)
|
|
8
|
+
* - if `secondary` non-empty: also `<SecondaryName>-Regular`, etc. as
|
|
9
|
+
* `SECONDARY_REGULAR`, `SECONDARY_MEDIUM`, `SECONDARY_SEMIBOLD`, `SECONDARY_BOLD`
|
|
10
|
+
*
|
|
11
|
+
* Empty `primary` → `Fonts = {}` + `FontKey = never` (apps using `Fonts.X` will
|
|
12
|
+
* fail to typecheck; intended — user opted out).
|
|
13
|
+
*/
|
|
14
|
+
export function generateFontsObject(primary, secondary) {
|
|
15
|
+
const out = [];
|
|
16
|
+
out.push("export const Fonts = {");
|
|
17
|
+
if (primary) {
|
|
18
|
+
out.push(` REGULAR: "${primary}-Regular",`);
|
|
19
|
+
out.push(` MEDIUM: "${primary}-Medium",`);
|
|
20
|
+
out.push(` SEMIBOLD: "${primary}-SemiBold",`);
|
|
21
|
+
out.push(` BOLD: "${primary}-Bold",`);
|
|
22
|
+
}
|
|
23
|
+
if (secondary) {
|
|
24
|
+
out.push(` SECONDARY_REGULAR: "${secondary}-Regular",`);
|
|
25
|
+
out.push(` SECONDARY_MEDIUM: "${secondary}-Medium",`);
|
|
26
|
+
out.push(` SECONDARY_SEMIBOLD: "${secondary}-SemiBold",`);
|
|
27
|
+
out.push(` SECONDARY_BOLD: "${secondary}-Bold",`);
|
|
28
|
+
}
|
|
29
|
+
out.push("} as const;");
|
|
30
|
+
out.push("");
|
|
31
|
+
out.push("export type FontKey = keyof typeof Fonts;");
|
|
32
|
+
return out.join("\n");
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Build the three pieces inserted into `_layout.tsx`:
|
|
36
|
+
* - import line(s) for `useFonts` + the asset .ttf require()s
|
|
37
|
+
* - the `useFonts(...)` hook call (must be inside the component body)
|
|
38
|
+
* - the `if (!loaded) return null;` guard (also inside component body, after
|
|
39
|
+
* the hook so React's rules-of-hooks stay satisfied)
|
|
40
|
+
*
|
|
41
|
+
* Empty `primary` → all three return empty strings → sentinel lines drop.
|
|
42
|
+
*/
|
|
43
|
+
export function generateUseFontsBlocks(primary, secondary) {
|
|
44
|
+
if (!primary) {
|
|
45
|
+
return { importBlock: "", hookBlock: "", guardBlock: "" };
|
|
46
|
+
}
|
|
47
|
+
const fontPairs = [
|
|
48
|
+
[`${primary}-Regular`, `${primary}-Regular.ttf`],
|
|
49
|
+
[`${primary}-Medium`, `${primary}-Medium.ttf`],
|
|
50
|
+
[`${primary}-SemiBold`, `${primary}-SemiBold.ttf`],
|
|
51
|
+
[`${primary}-Bold`, `${primary}-Bold.ttf`],
|
|
52
|
+
];
|
|
53
|
+
if (secondary) {
|
|
54
|
+
fontPairs.push([`${secondary}-Regular`, `${secondary}-Regular.ttf`], [`${secondary}-Medium`, `${secondary}-Medium.ttf`], [`${secondary}-SemiBold`, `${secondary}-SemiBold.ttf`], [`${secondary}-Bold`, `${secondary}-Bold.ttf`]);
|
|
55
|
+
}
|
|
56
|
+
const importBlock = `import { useFonts } from "expo-font";`;
|
|
57
|
+
const mapEntries = fontPairs
|
|
58
|
+
.map(([key, file]) => ` "${key}": require("../../assets/fonts/${file}"),`)
|
|
59
|
+
.join("\n");
|
|
60
|
+
const hookBlock = ` const [loaded] = useFonts({\n${mapEntries}\n });`;
|
|
61
|
+
const guardBlock = ` if (!loaded) return null;`;
|
|
62
|
+
return { importBlock, hookBlock, guardBlock };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Build the three pieces wrapping the routing tree with `BottomSheetModalProvider`:
|
|
66
|
+
* - import line for `BottomSheetModalProvider`
|
|
67
|
+
* - opening JSX
|
|
68
|
+
* - closing JSX
|
|
69
|
+
*
|
|
70
|
+
* `bottomSheet === false` → all three empty → sentinel lines drop.
|
|
71
|
+
*/
|
|
72
|
+
export function generateBottomSheetProviderBlocks(bottomSheet) {
|
|
73
|
+
if (!bottomSheet) {
|
|
74
|
+
return { importBlock: "", openBlock: "", closeBlock: "" };
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
importBlock: `import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";`,
|
|
78
|
+
openBlock: ` <BottomSheetModalProvider>`,
|
|
79
|
+
closeBlock: ` </BottomSheetModalProvider>`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Convenience: build the full sentinel-replacement map for `patchLayout`.
|
|
84
|
+
* Includes both layout sentinels and the fonts.ts sentinel.
|
|
85
|
+
*/
|
|
86
|
+
export function buildLayoutReplacements(answers) {
|
|
87
|
+
const fonts = generateUseFontsBlocks(answers.primaryFont, answers.secondaryFont);
|
|
88
|
+
const bs = generateBottomSheetProviderBlocks(answers.bottomSheet);
|
|
89
|
+
return {
|
|
90
|
+
USE_FONTS_IMPORT: fonts.importBlock,
|
|
91
|
+
USE_FONTS_HOOK: fonts.hookBlock,
|
|
92
|
+
USE_FONTS_GUARD: fonts.guardBlock,
|
|
93
|
+
BOTTOM_SHEET_PROVIDER_IMPORT: bs.importBlock,
|
|
94
|
+
BOTTOM_SHEET_PROVIDER_OPEN: bs.openBlock,
|
|
95
|
+
BOTTOM_SHEET_PROVIDER_CLOSE: bs.closeBlock,
|
|
96
|
+
FONTS_OBJECT: generateFontsObject(answers.primaryFont, answers.secondaryFont),
|
|
97
|
+
};
|
|
98
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Entry point. Order per PLAN_V5.md (consolidated v5-r6):
|
|
2
|
+
// validateEnvVars
|
|
3
|
+
// → resolveTargetDir
|
|
4
|
+
// → runCreateExpoApp + cleanupBlankTemplate
|
|
5
|
+
// → gatherAnswers
|
|
6
|
+
// → applyBase + applyBottomSheet + applyImagePicker
|
|
7
|
+
// → patchAppJson + patchExpoRouterEntry + patchAppJsonPlugins
|
|
8
|
+
// → patchConstants + patchLayout
|
|
9
|
+
// → patchPackageJsonScripts + patchTsconfig + patchBabel
|
|
10
|
+
// → installNativeDeps
|
|
11
|
+
// → ensureLockfile
|
|
12
|
+
// → success message
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { resolveTargetDir } from "./bootstrap.js";
|
|
16
|
+
import { gatherAnswers, validateEnvVars } from "./prompts.js";
|
|
17
|
+
import { cleanupBlankTemplate, runCreateExpoApp } from "./scaffold.js";
|
|
18
|
+
import { applyBase, applyBottomSheet, applyImagePicker } from "./overlay.js";
|
|
19
|
+
import { patchAppJson, patchAppJsonPlugins, patchConstants, patchExpoRouterEntry, patchLayout, patchPackageJsonScripts, patchTsconfig, } from "./patch.js";
|
|
20
|
+
import { patchBabel } from "./babel.js";
|
|
21
|
+
import { buildLayoutReplacements } from "./fonts.js";
|
|
22
|
+
import { ensureLockfile, installNativeDeps } from "./install.js";
|
|
23
|
+
import { log } from "./util.js";
|
|
24
|
+
import { readSDKNotes } from "./sdkNotes.js";
|
|
25
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
26
|
+
const __dirname = path.dirname(__filename);
|
|
27
|
+
/**
|
|
28
|
+
* Locate `templates/` directory. In the published package layout, it sits at
|
|
29
|
+
* `<pkgRoot>/templates/`; this file lives at `<pkgRoot>/dist/index.js`. So one
|
|
30
|
+
* level up from `dist/` reaches the package root.
|
|
31
|
+
*/
|
|
32
|
+
function resolveTemplatesRoot() {
|
|
33
|
+
return path.resolve(__dirname, "..", "templates");
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Locate `docs/SDK_NOTES.md`. Same layout assumption as `resolveTemplatesRoot`.
|
|
37
|
+
*/
|
|
38
|
+
function resolveSdkNotesPath() {
|
|
39
|
+
return path.resolve(__dirname, "..", "docs", "SDK_NOTES.md");
|
|
40
|
+
}
|
|
41
|
+
async function main() {
|
|
42
|
+
validateEnvVars();
|
|
43
|
+
const arg = process.argv[2];
|
|
44
|
+
const target = await resolveTargetDir(arg);
|
|
45
|
+
log.info(`Target dir: ${target.dir}`);
|
|
46
|
+
log.info(`App name: ${target.name}`);
|
|
47
|
+
await runCreateExpoApp(target.dir, target.name);
|
|
48
|
+
cleanupBlankTemplate(target.dir);
|
|
49
|
+
const answers = await gatherAnswers();
|
|
50
|
+
log.info(`Answers: primaryFont="${answers.primaryFont}" secondaryFont="${answers.secondaryFont}" ` +
|
|
51
|
+
`bottomSheet=${answers.bottomSheet} imagePicker=${answers.imagePicker} pm=${answers.packageManager}`);
|
|
52
|
+
// ---- Apply templates ----
|
|
53
|
+
const templatesRoot = resolveTemplatesRoot();
|
|
54
|
+
log.step("Overlaying templates/base/ …");
|
|
55
|
+
applyBase(target.dir, templatesRoot);
|
|
56
|
+
if (answers.bottomSheet) {
|
|
57
|
+
log.step("Overlaying templates/bottom-sheet/ …");
|
|
58
|
+
applyBottomSheet(target.dir, templatesRoot);
|
|
59
|
+
}
|
|
60
|
+
if (answers.imagePicker) {
|
|
61
|
+
log.step("Overlaying templates/image-picker/ …");
|
|
62
|
+
applyImagePicker(target.dir, templatesRoot);
|
|
63
|
+
}
|
|
64
|
+
// ---- Patches ----
|
|
65
|
+
log.step("Patching app.json + expo-router entry …");
|
|
66
|
+
patchAppJson(target.dir, target.name, answers);
|
|
67
|
+
patchExpoRouterEntry(target.dir);
|
|
68
|
+
patchAppJsonPlugins(target.dir, answers);
|
|
69
|
+
log.step("Splicing constants + layout sentinels …");
|
|
70
|
+
patchConstants(target.dir, templatesRoot, answers);
|
|
71
|
+
patchLayout(target.dir, buildLayoutReplacements(answers));
|
|
72
|
+
// ---- Read SDK_NOTES.md probe outcomes ----
|
|
73
|
+
const sdk = readSDKNotes(resolveSdkNotesPath());
|
|
74
|
+
const flagsOk = sdk.get("FLAGS_OK") === "1";
|
|
75
|
+
const probePass = sdk.get("PROBE_PASS") === "1";
|
|
76
|
+
const workletsAutoIncluded = sdk.get("BABEL_PRESET_AUTO_INCLUDES_WORKLETS") === "1";
|
|
77
|
+
const workletsPkg = sdk.get("WORKLETS_PKG") === "react-native-worklets-core"
|
|
78
|
+
? "react-native-worklets-core"
|
|
79
|
+
: "react-native-worklets";
|
|
80
|
+
const expoBaseUrlInherited = sdk.get("EXPO_TSCONFIG_BASEURL") !== "null";
|
|
81
|
+
log.step("Patching package.json scripts + tsconfig + babel.config.js …");
|
|
82
|
+
patchPackageJsonScripts(target.dir);
|
|
83
|
+
patchTsconfig(target.dir, { expoBaseUrlInherited });
|
|
84
|
+
patchBabel(target.dir, {
|
|
85
|
+
workletsAutoIncluded,
|
|
86
|
+
workletsPkg,
|
|
87
|
+
aliasMap: {
|
|
88
|
+
"@": "./src",
|
|
89
|
+
"@theme": "./src/ui/theme",
|
|
90
|
+
"@utils": "./src/core/utils",
|
|
91
|
+
"@redux": "./src/core/redux",
|
|
92
|
+
"@core": "./src/core",
|
|
93
|
+
"@services": "./src/core/services",
|
|
94
|
+
"@hooks": "./src/core/hooks",
|
|
95
|
+
"@appComponents": "./src/ui/appComponents",
|
|
96
|
+
"@components": "./src/ui/components",
|
|
97
|
+
"@icons": "./src/ui/iconComponents",
|
|
98
|
+
"@features": "./src/features",
|
|
99
|
+
"@assets": "./assets",
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
// ---- Install + verify ----
|
|
103
|
+
await installNativeDeps(target.dir, answers, { flagsOk, workletsPkg });
|
|
104
|
+
const lockOutcome = await ensureLockfile(target.dir, answers, { probePass, flagsOk });
|
|
105
|
+
// ---- Success ----
|
|
106
|
+
const cmdPm = lockOutcome.producedPm === "yarn" ? "yarn" : "npm run";
|
|
107
|
+
log.success(`Project ready at ${target.dir}`);
|
|
108
|
+
log.raw("");
|
|
109
|
+
log.raw(` cd ${path.relative(process.cwd(), target.dir) || target.name}`);
|
|
110
|
+
log.raw(` npx expo prebuild`);
|
|
111
|
+
log.raw(` ${cmdPm} ios # or: ${cmdPm} android`);
|
|
112
|
+
log.raw("");
|
|
113
|
+
log.info("First-time dev-client build details: README → 'First-time dev-client build'.");
|
|
114
|
+
}
|
|
115
|
+
main().catch((err) => {
|
|
116
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
117
|
+
log.warn("If the failure is mid-patch, the target dir may be in an inconsistent state. " +
|
|
118
|
+
"Re-run the CLI (patches are idempotent and will converge), or `rm -rf <path>` and start fresh.");
|
|
119
|
+
process.exit(1);
|
|
120
|
+
});
|