newcandies 0.1.13 → 0.1.14

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.
Files changed (59) hide show
  1. package/dist/index.js +117 -395
  2. package/package.json +1 -1
  3. package/templates/_base/app.json +40 -0
  4. package/templates/_base/babel.config.js +11 -0
  5. package/templates/_base/eas.json +10 -0
  6. package/templates/_base/expo-env.d.ts +3 -0
  7. package/templates/_base/global.css +18 -0
  8. package/templates/_base/metro.config.js +10 -0
  9. package/templates/_base/package.json +44 -0
  10. package/templates/_base/src/app/(tabs)/_layout.tsx +28 -0
  11. package/templates/_base/src/app/(tabs)/index.tsx +10 -0
  12. package/templates/_base/src/app/(tabs)/two.tsx +10 -0
  13. package/templates/_base/src/app/_layout.tsx +23 -0
  14. package/templates/_base/src/app/modal.tsx +10 -0
  15. package/templates/_base/src/components/core/text.tsx +42 -0
  16. package/templates/_base/tsconfig.json +17 -0
  17. package/templates/app-briefs/react-query/sproutsy/README.md +4 -0
  18. package/templates/app-briefs/react-query/sproutsy/assets/fonts/SpaceMono-Regular.ttf +0 -0
  19. package/templates/app-briefs/react-query/sproutsy/assets/images/adaptive-icon.png +0 -0
  20. package/templates/app-briefs/react-query/sproutsy/assets/images/favicon.png +0 -0
  21. package/templates/app-briefs/react-query/sproutsy/assets/images/icon.png +0 -0
  22. package/templates/app-briefs/react-query/sproutsy/assets/images/splash-icon.png +0 -0
  23. package/templates/app-briefs/react-query/sproutsy/assets/images/sproutsies-logo.png +0 -0
  24. package/templates/app-briefs/react-query/sproutsy/assets/images/sproutsy-hallmark.png +0 -0
  25. package/templates/app-briefs/react-query/sproutsy/assets/images/sproutsy-logo.png +0 -0
  26. package/templates/app-briefs/react-query/sproutsy/assets/images/sproutsy-logo.svg +1 -0
  27. package/templates/app-briefs/react-query/sproutsy/assets/images/sproutsy-main-logo.png +0 -0
  28. package/templates/app-briefs/react-query/sproutsy/babel.config.js +7 -0
  29. package/templates/app-briefs/react-query/sproutsy/env +3 -0
  30. package/templates/app-briefs/react-query/sproutsy/expo-env.d.ts +3 -0
  31. package/templates/app-briefs/react-query/sproutsy/global.css +29 -0
  32. package/templates/app-briefs/react-query/sproutsy/metro.config.js +14 -0
  33. package/templates/app-briefs/react-query/sproutsy/package.json +55 -0
  34. package/templates/app-briefs/react-query/sproutsy/src/lib/constants/index.ts +253 -0
  35. package/templates/app-briefs/react-query/sproutsy/src/types/plant.d.ts +50 -0
  36. package/templates/app-briefs/react-query/sproutsy/tsconfig.json +17 -0
  37. package/templates/boilerplate/holidia/babel.config.js +13 -0
  38. package/templates/boilerplate/holidia/global.css +4 -0
  39. package/templates/boilerplate/holidia/metro.config.js +7 -0
  40. package/templates/boilerplate/holidia/package.json +33 -0
  41. package/templates/boilerplate/holidia/src/app/_layout.tsx +16 -0
  42. package/templates/boilerplate/holidia/src/app/index.tsx +34 -0
  43. package/templates/boilerplate/holidia/tailwind.config.js +15 -0
  44. package/templates/boilerplate/supersimplenotes/babel.config.js +13 -0
  45. package/templates/boilerplate/supersimplenotes/global.css +4 -0
  46. package/templates/boilerplate/supersimplenotes/metro.config.js +7 -0
  47. package/templates/boilerplate/supersimplenotes/package.json +32 -0
  48. package/templates/boilerplate/supersimplenotes/src/app/_layout.tsx +11 -0
  49. package/templates/boilerplate/supersimplenotes/src/app/index.tsx +28 -0
  50. package/templates/boilerplate/supersimplenotes/tailwind.config.js +15 -0
  51. package/templates/registry.json +54 -0
  52. package/templates/default/README.md +0 -1
  53. /package/templates/{default → _base}/assets/fonts/SpaceMono-Regular.ttf +0 -0
  54. /package/templates/{default → _base}/assets/images/adaptive-icon.png +0 -0
  55. /package/templates/{default → _base}/assets/images/favicon.png +0 -0
  56. /package/templates/{default → _base}/assets/images/icon.png +0 -0
  57. /package/templates/{default → _base}/assets/images/splash-icon.png +0 -0
  58. /package/templates/boilerplate/holidia/{app → src/app}/holi.tsx +0 -0
  59. /package/templates/boilerplate/supersimplenotes/{app → src/app}/notes.tsx +0 -0
package/dist/index.js CHANGED
@@ -8,38 +8,23 @@ import fs from "fs-extra";
8
8
  import path from "path";
9
9
  import { fileURLToPath } from "url";
10
10
  import { execa } from "execa";
11
- var TEMPLATE_MAP = {
12
- "boilerplate/holidia": {
13
- extraDeps: ["@tanstack/react-query", "@react-native-async-storage/async-storage"]
14
- },
15
- "boilerplate/supersimplenotes": {
16
- extraDeps: ["@react-native-community/netinfo"]
17
- },
18
- "app-brief/react-query/sproutsy": {
19
- extraDeps: ["@tanstack/react-query"]
20
- }
21
- };
22
- var TEMPLATES = {
23
- "boilerplate": [
24
- { value: "holidia", label: "holidia" },
25
- { value: "supersimplenotes", label: "supersimplenotes" }
26
- ],
27
- "mini-boilerplate": [
28
- { value: "auth-mini", label: "Auth Mini" }
29
- ],
30
- "app-brief": {
31
- categories: [
32
- { value: "react-query", label: "react-query", variants: [{ value: "sproutsy", label: "sproutsy" }] }
33
- ]
34
- },
35
- "mini-app-brief": { categories: [{ value: "forms", label: "forms", variants: [{ value: "lite", label: "lite" }] }] },
36
- "default": [{ value: "default", label: "Default (Expo Router + Uniwind)" }]
37
- };
11
+ var __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+ var REGISTRY_PATH = path.join(__dirname, "..", "templates", "registry.json");
38
13
  var program = new Command().name("newcandies").option("-t, --type <type>").option("--template <name>").option("--category <name>").option("--variant <name>").option("-y, --yes").option("--pm <pm>", "npm|pnpm|yarn|bun").option("--no-install").option("--no-prebuild").parse(process.argv);
39
14
  var opts = program.opts();
15
+ function deepMerge(target, source) {
16
+ for (const key in source) {
17
+ if (source[key] instanceof Object && key in target) {
18
+ Object.assign(source[key], deepMerge(target[key], source[key]));
19
+ }
20
+ }
21
+ Object.assign(target || {}, source);
22
+ return target;
23
+ }
40
24
  async function main() {
41
25
  console.clear();
42
26
  p.intro(pc.bgMagenta(pc.black(" newcandies ")));
27
+ const registry = await fs.readJSON(REGISTRY_PATH);
43
28
  const project = await p.group({
44
29
  name: () => p.text({
45
30
  message: "Project name?",
@@ -51,32 +36,44 @@ async function main() {
51
36
  return p.select({
52
37
  message: "Pick type",
53
38
  options: [
54
- { value: "boilerplate", label: "Boilerplate (main projects)" },
55
- { value: "mini-boilerplate", label: "Mini-boilerplate" },
56
- { value: "app-brief", label: "App-brief (2 levels)" },
57
- { value: "mini-app-brief", label: "Mini-app-brief (2 levels)" },
58
- { value: "default", label: "Default" }
39
+ { value: "boilerplates", label: "Boilerplate (Full Projects)" },
40
+ { value: "mini-boilerplates", label: "Mini-boilerplate" },
41
+ { value: "app-briefs", label: "App-brief (2 levels)" },
42
+ { value: "mini-app-briefs", label: "Mini-app-brief (2 levels)" },
43
+ { value: "default", label: "Default (Base)" }
59
44
  ]
60
45
  });
61
46
  },
62
47
  template: async ({ results }) => {
63
- if (results.type === "app-brief" || results.type === "mini-app-brief") return null;
48
+ const type = results.type;
49
+ if (type === "default" || type === "app-briefs" || type === "mini-app-briefs") return null;
64
50
  if (opts.template) return opts.template;
65
- const list = TEMPLATESafe(results.type);
66
- return p.select({ message: "Pick template", options: list });
51
+ const items = registry[type];
52
+ return p.select({
53
+ message: "Pick template",
54
+ options: items.map((i) => ({ value: i.name, label: `${i.name} - ${i.description}` }))
55
+ });
67
56
  },
68
57
  category: async ({ results }) => {
69
- if (results.type !== "app-brief" && results.type !== "mini-app-brief") return null;
58
+ const type = results.type;
59
+ if (type !== "app-briefs" && type !== "mini-app-briefs") return null;
70
60
  if (opts.category) return opts.category;
71
- const cats = TEMPLATES[results.type].categories;
72
- return p.select({ message: "Pick category", options: cats });
61
+ const cats = registry[type];
62
+ return p.select({
63
+ message: "Pick category",
64
+ options: cats.map((c) => ({ value: c.category, label: c.category }))
65
+ });
73
66
  },
74
67
  variant: async ({ results }) => {
75
- if (results.type !== "app-brief" && results.type !== "mini-app-brief") return null;
68
+ const type = results.type;
69
+ if (type !== "app-briefs" && type !== "mini-app-briefs") return null;
76
70
  if (opts.variant) return opts.variant;
77
- const cats = TEMPLATES[results.type].categories;
78
- const cat = cats.find((c) => c.value === results.category);
79
- return p.select({ message: "Pick variant", options: cat.variants });
71
+ const cats = registry[type];
72
+ const cat = cats.find((c) => c.category === results.category);
73
+ return p.select({
74
+ message: "Pick variant",
75
+ options: cat.variants.map((v) => ({ value: v.name, label: `${v.name} - ${v.description}` }))
76
+ });
80
77
  },
81
78
  install: async () => {
82
79
  if (opts.yes !== void 0) return !!opts.yes;
@@ -103,22 +100,92 @@ async function main() {
103
100
  const dest = path.resolve(process.cwd(), project.name);
104
101
  const s = p.spinner();
105
102
  s.start("Scaffolding");
103
+ let selectedItem = null;
104
+ if (project.type === "default") {
105
+ selectedItem = { name: "default", path: "", description: "Default", strategy: "base" };
106
+ } else if (project.type === "boilerplates" || project.type === "mini-boilerplates") {
107
+ const list = registry[project.type];
108
+ selectedItem = list.find((i) => i.name === project.template) || null;
109
+ } else {
110
+ const type = project.type;
111
+ const cat = registry[type].find((c) => c.category === project.category);
112
+ selectedItem = cat?.variants.find((v) => v.name === project.variant) || null;
113
+ }
114
+ if (!selectedItem) {
115
+ s.stop("Error: Template not found");
116
+ process.exit(1);
117
+ }
106
118
  await fs.ensureDir(dest);
107
- const withQuery = project.type === "app-brief" && project.category === "react-query" && project.variant === "sproutsy";
108
- await writeBaseline({ dest, name: project.name, withQuery });
109
- await applyTemplateOverlay({ dest, type: project.type, template: project.template, category: project.category, variant: project.variant });
119
+ const strategy = selectedItem.strategy || "base";
120
+ if (strategy === "base") {
121
+ const basePath = path.join(__dirname, "..", "templates", "_base");
122
+ await fs.copy(basePath, dest);
123
+ }
124
+ let basePkg = null;
125
+ let baseApp = null;
126
+ if (strategy === "base") {
127
+ const pkgPath2 = path.join(dest, "package.json");
128
+ const appPath = path.join(dest, "app.json");
129
+ if (await fs.pathExists(pkgPath2)) basePkg = await fs.readJSON(pkgPath2);
130
+ if (await fs.pathExists(appPath)) baseApp = await fs.readJSON(appPath);
131
+ }
132
+ if (selectedItem.path) {
133
+ const templatePath = path.join(__dirname, "..", "templates", selectedItem.path);
134
+ if (await fs.pathExists(templatePath)) {
135
+ await fs.copy(templatePath, dest, { overwrite: true });
136
+ }
137
+ }
138
+ if (strategy === "base" && basePkg) {
139
+ const templatePkgPath = path.join(__dirname, "..", "templates", selectedItem.path, "package.json");
140
+ const templateAppPath = path.join(__dirname, "..", "templates", selectedItem.path, "app.json");
141
+ if (await fs.pathExists(templatePkgPath)) {
142
+ const tmplPkg = await fs.readJSON(templatePkgPath);
143
+ basePkg.dependencies = { ...basePkg.dependencies, ...tmplPkg.dependencies };
144
+ basePkg.devDependencies = { ...basePkg.devDependencies, ...tmplPkg.devDependencies };
145
+ basePkg.scripts = { ...basePkg.scripts, ...tmplPkg.scripts };
146
+ await fs.writeJSON(path.join(dest, "package.json"), basePkg, { spaces: 2 });
147
+ }
148
+ if (await fs.pathExists(templateAppPath) && baseApp) {
149
+ const tmplApp = await fs.readJSON(templateAppPath);
150
+ const mergedApp = deepMerge(baseApp, tmplApp);
151
+ await fs.writeJSON(path.join(dest, "app.json"), mergedApp, { spaces: 2 });
152
+ }
153
+ }
154
+ const envPath = path.join(dest, "env");
155
+ if (await fs.pathExists(envPath)) {
156
+ await fs.move(envPath, path.join(dest, ".env"), { overwrite: true });
157
+ }
158
+ const pkgPath = path.join(dest, "package.json");
159
+ if (await fs.pathExists(pkgPath)) {
160
+ const pkg = await fs.readJSON(pkgPath);
161
+ pkg.name = project.name;
162
+ await fs.writeJSON(pkgPath, pkg, { spaces: 2 });
163
+ }
164
+ const appJsonPath = path.join(dest, "app.json");
165
+ if (await fs.pathExists(appJsonPath)) {
166
+ const appJson = await fs.readJSON(appJsonPath);
167
+ if (appJson.expo) {
168
+ appJson.expo.name = project.name;
169
+ appJson.expo.slug = String(project.name).toLowerCase().replace(/\s+/g, "-");
170
+ appJson.expo.scheme = appJson.expo.slug;
171
+ }
172
+ await fs.writeJSON(appJsonPath, appJson, { spaces: 2 });
173
+ }
110
174
  s.stop("Files ready");
111
- const extraDeps = resolveExtraDeps({ type: project.type, template: project.template, category: project.category, variant: project.variant });
112
175
  if (project.install) {
113
176
  const si = p.spinner();
114
177
  si.start("Installing dependencies");
115
- await installDeps(project.pm, dest, extraDeps);
178
+ await installDeps(project.pm, dest);
116
179
  si.stop("Installed");
117
180
  if (opts.prebuild !== false) {
118
181
  const sp = p.spinner();
119
182
  sp.start("Generating ios/android (expo prebuild)");
120
- await execa("npx", ["expo", "prebuild"], { cwd: dest, stdio: "inherit", env: { ...process.env, CI: "1", EXPO_ROUTER_APP_ROOT: "src/app" } });
121
- sp.stop("Native projects generated");
183
+ try {
184
+ await execa("npx", ["expo", "prebuild"], { cwd: dest, stdio: "inherit", env: { ...process.env, CI: "1", EXPO_ROUTER_APP_ROOT: "src/app" } });
185
+ sp.stop("Native projects generated");
186
+ } catch (e) {
187
+ sp.stop("Prebuild failed (check logs)");
188
+ }
122
189
  }
123
190
  }
124
191
  p.outro([
@@ -128,352 +195,7 @@ async function main() {
128
195
  ` ${project.pm} ${project.pm === "npm" ? "run " : ""}start`
129
196
  ].filter(Boolean).join("\n"));
130
197
  }
131
- function TEMPLATESafe(type) {
132
- const t = TEMPLATES[type];
133
- if (Array.isArray(t)) return t;
134
- return [{ value: "default", label: "Default (Expo Router + Uniwind)" }];
135
- }
136
- async function writeBaseline({ dest, name, withQuery }) {
137
- const baseDeps = {
138
- // Expo core + platform libs
139
- expo: "54.0.23",
140
- "expo-constants": "~18.0.10",
141
- "expo-font": "~14.0.9",
142
- "expo-linking": "~8.0.8",
143
- "expo-router": "~6.0.14",
144
- "expo-splash-screen": "~31.0.10",
145
- "expo-status-bar": "~3.0.8",
146
- "expo-web-browser": "~15.0.9",
147
- // React ecosystem
148
- react: "19.1.0",
149
- "react-dom": "19.1.0",
150
- "react-native": "0.81.5",
151
- "react-native-web": "~0.21.0",
152
- // RN libs
153
- "react-native-reanimated": "~4.1.1",
154
- "react-native-gesture-handler": "latest",
155
- "react-native-safe-area-context": "~5.6.0",
156
- "react-native-screens": "~4.16.0",
157
- "react-native-worklets": "0.5.1",
158
- // Styling
159
- tailwindcss: "^4.1.16",
160
- uniwind: "^1.0.0",
161
- "@expo/vector-icons": "latest"
162
- };
163
- if (withQuery) {
164
- baseDeps["@tanstack/react-query"] = "latest";
165
- }
166
- baseDeps["tailwind-merge"] = "latest";
167
- baseDeps["react-native-keyboard-controller"] = "latest";
168
- baseDeps["@gorhom/bottom-sheet"] = "latest";
169
- baseDeps["sonner-native"] = "latest";
170
- const pkg = {
171
- name,
172
- version: "0.0.0",
173
- private: true,
174
- main: "expo-router/entry",
175
- scripts: {
176
- start: "EXPO_ROUTER_APP_ROOT=src/app expo start",
177
- android: "EXPO_ROUTER_APP_ROOT=src/app expo run:android",
178
- ios: "EXPO_ROUTER_APP_ROOT=src/app expo run:ios",
179
- prebuild: "EXPO_ROUTER_APP_ROOT=src/app expo prebuild"
180
- },
181
- dependencies: baseDeps,
182
- devDependencies: {
183
- "@types/react": "~19.1.0",
184
- typescript: "~5.9.2",
185
- "babel-plugin-module-resolver": "latest"
186
- }
187
- };
188
- await fs.writeJSON(path.join(dest, "package.json"), pkg, { spaces: 2 });
189
- const slug = String(name).toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
190
- const pkgNameSegment = slug.replace(/-/g, "") || "app";
191
- const bundleId = `com.example.${pkgNameSegment}`;
192
- const appJson = {
193
- expo: {
194
- name,
195
- slug,
196
- version: "1.0.0",
197
- orientation: "portrait",
198
- icon: "./assets/images/icon.png",
199
- scheme: slug,
200
- userInterfaceStyle: "automatic",
201
- newArchEnabled: true,
202
- splash: {
203
- image: "./assets/images/splash-icon.png",
204
- resizeMode: "contain",
205
- backgroundColor: "#ffffff"
206
- },
207
- ios: {
208
- supportsTablet: true,
209
- bundleIdentifier: bundleId
210
- },
211
- android: {
212
- adaptiveIcon: {
213
- foregroundImage: "./assets/images/adaptive-icon.png",
214
- backgroundColor: "#ffffff"
215
- },
216
- edgeToEdgeEnabled: true,
217
- predictiveBackGestureEnabled: false,
218
- package: bundleId
219
- },
220
- web: {
221
- bundler: "metro",
222
- output: "static",
223
- favicon: "./assets/images/favicon.png"
224
- },
225
- plugins: ["expo-router"],
226
- experiments: {
227
- typedRoutes: true
228
- }
229
- }
230
- };
231
- await fs.writeJSON(path.join(dest, "app.json"), appJson, { spaces: 2 });
232
- const babel = `module.exports = function (api) {
233
- api.cache(true);
234
- return {
235
- presets: ['babel-preset-expo'],
236
- plugins: [
237
- ['module-resolver', { alias: { '~': './' } }],
238
- 'react-native-reanimated/plugin'
239
- ]
240
- };
241
- };
242
- `;
243
- await fs.writeFile(path.join(dest, "babel.config.js"), babel);
244
- const metro = `const { getDefaultConfig } = require('expo/metro-config');
245
- const { withUniwindConfig } = require('uniwind/metro');
246
-
247
- const config = getDefaultConfig(__dirname);
248
-
249
- module.exports = withUniwindConfig(config, {
250
- cssEntryFile: './global.css',
251
- dtsFile: './uniwind-types.d.ts',
252
- });
253
- `;
254
- await fs.writeFile(path.join(dest, "metro.config.js"), metro);
255
- const tsconfig = {
256
- extends: "expo/tsconfig.base",
257
- compilerOptions: {
258
- strict: true,
259
- paths: {
260
- "~/*": ["./*"]
261
- }
262
- },
263
- include: [
264
- "**/*.ts",
265
- "**/*.tsx",
266
- ".expo/types/**/*.ts",
267
- "expo-env.d.ts",
268
- "./uniwind-types.d.ts"
269
- ]
270
- };
271
- await fs.writeJSON(path.join(dest, "tsconfig.json"), tsconfig, { spaces: 2 });
272
- const expoEnv = `/// <reference types="expo" />
273
- /// <reference types="expo-router" />
274
- `;
275
- await fs.writeFile(path.join(dest, "expo-env.d.ts"), expoEnv);
276
- const srcDir = path.join(dest, "src");
277
- const appDir = path.join(srcDir, "app");
278
- await fs.ensureDir(appDir);
279
- await fs.ensureDir(path.join(srcDir, "core"));
280
- await fs.ensureDir(path.join(srcDir, "api"));
281
- await fs.ensureDir(path.join(srcDir, "lib"));
282
- await fs.ensureDir(path.join(srcDir, "types"));
283
- await fs.ensureDir(path.join(srcDir, "components", "screens", "home"));
284
- await fs.ensureDir(path.join(srcDir, "components", "screens", "profile"));
285
- await fs.ensureDir(path.join(srcDir, "components", "core"));
286
- const textCore = `import { Text as RNText, TextProps } from 'react-native';
287
- import { twMerge } from 'tailwind-merge';
288
-
289
- type TypographyVariant =
290
- | 'title'
291
- | 'subtitle'
292
- | 'body'
293
- | 'caption'
294
- | 'button'
295
- | 'display'
296
- | 'caption-primary'
297
- | 'body-primary'
298
- | 'subtitle-primary';
299
-
300
- interface TextComponentProps extends TextProps {
301
- className?: string;
302
- variant?: TypographyVariant;
303
- }
304
-
305
- const variantStyles: Record<TypographyVariant, string> = {
306
- title: 'text-2xl font-bold',
307
- subtitle: 'text-xl font-semibold',
308
- 'subtitle-primary': 'text-xl font-semibold text-primary',
309
- body: 'text-base',
310
- 'body-primary': 'text-base text-primary',
311
- caption: 'text-sm font-medium',
312
- 'caption-primary': 'text-sm text-primary font-medium',
313
- button: 'text-xl text-primary font-semibold text-white text-center',
314
- display: 'text-3x font-bold',
315
- };
316
-
317
- const Text = ({ variant = 'body', children, className, ...props }: TextComponentProps) => {
318
- const textStyle = twMerge('text-black', variantStyles[variant], className);
319
- return (
320
- <RNText className={textStyle} {...props}>
321
- {children}
322
- </RNText>
323
- );
324
- };
325
-
326
- export default Text;
327
- `;
328
- await fs.writeFile(path.join(srcDir, "components", "core", "text.tsx"), textCore);
329
- const easJson = {
330
- cli: { version: ">= 7.0.0" },
331
- build: {
332
- development: { android: { buildType: "apk" }, developmentClient: true, distribution: "internal" },
333
- preview: { distribution: "internal" },
334
- production: {}
335
- },
336
- submit: { production: {} }
337
- };
338
- await fs.writeJSON(path.join(dest, "eas.json"), easJson, { spaces: 2 });
339
- const globalCss = `@import 'tailwindcss';
340
- @import 'uniwind';
341
-
342
- @layer theme {
343
- :root {
344
- @variant light {
345
- --color-background: #ffffff;
346
- --color-foreground: #111827;
347
- --color-primary: #3b82f6;
348
- }
349
- @variant dark {
350
- --color-background: #000000;
351
- --color-foreground: #ffffff;
352
- --color-primary: #60a5fa;
353
- }
354
- }
355
- }
356
- `;
357
- await fs.writeFile(path.join(dest, "global.css"), globalCss);
358
- {
359
- const rootLayout = `import { Stack } from 'expo-router';
360
- import { GestureHandlerRootView } from 'react-native-gesture-handler';
361
- import { KeyboardProvider } from 'react-native-keyboard-controller';
362
- import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
363
- import { Toaster } from 'sonner-native';
364
- import 'react-native-reanimated';
365
- import '../../global.css';
366
-
367
- export default function RootLayout() {
368
- return (
369
- <GestureHandlerRootView style={{ flex: 1 }}>
370
- <KeyboardProvider>
371
- <BottomSheetModalProvider>
372
- <Stack>
373
- <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
374
- </Stack>
375
- <Toaster />
376
- </BottomSheetModalProvider>
377
- </KeyboardProvider>
378
- </GestureHandlerRootView>
379
- );
380
- }
381
- `;
382
- await fs.writeFile(path.join(appDir, "_layout.tsx"), rootLayout);
383
- const tabsDir = path.join(appDir, "(tabs)");
384
- await fs.ensureDir(tabsDir);
385
- const layout = `import FontAwesome from '@expo/vector-icons/FontAwesome';
386
- import { Tabs } from 'expo-router';
387
-
388
- function TabBarIcon(props: { name: React.ComponentProps<typeof FontAwesome>['name']; color: string; }) {
389
- return <FontAwesome size={28} style={{ marginBottom: -3 }} {...props} />;
390
- }
391
-
392
- export default function TabLayout() {
393
- return (
394
- <Tabs screenOptions={{ headerShown: false }}>
395
- <Tabs.Screen
396
- name="index"
397
- options={{
398
- title: 'Home',
399
- tabBarIcon: ({ color }) => <TabBarIcon name="home" color={color} />,
400
- }}
401
- />
402
- <Tabs.Screen
403
- name="two"
404
- options={{
405
- title: 'Explore',
406
- tabBarIcon: ({ color }) => <TabBarIcon name="search" color={color} />,
407
- }}
408
- />
409
- </Tabs>
410
- );
411
- }
412
- `;
413
- await fs.writeFile(path.join(tabsDir, "_layout.tsx"), layout);
414
- const tabOne = `import { View, Text } from 'react-native';
415
-
416
- export default function Index() {
417
- return (
418
- <View className="flex-1 items-center justify-center bg-background">
419
- <Text className="text-foreground text-xl">Tab One</Text>
420
- </View>
421
- );
422
- }
423
- `;
424
- await fs.writeFile(path.join(tabsDir, "index.tsx"), tabOne);
425
- const tabTwo = `import { View, Text } from 'react-native';
426
-
427
- export default function Two() {
428
- return (
429
- <View className="flex-1 items-center justify-center bg-background">
430
- <Text className="text-foreground text-xl">Tab Two</Text>
431
- </View>
432
- );
433
- }
434
- `;
435
- await fs.writeFile(path.join(tabsDir, "two.tsx"), tabTwo);
436
- const modal = `import { View, Text } from 'react-native';
437
-
438
- export default function Modal() {
439
- return (
440
- <View className="flex-1 items-center justify-center bg-background">
441
- <Text className="text-foreground text-xl">Modal screen</Text>
442
- </View>
443
- );
444
- }
445
- `;
446
- await fs.writeFile(path.join(appDir, "modal.tsx"), modal);
447
- }
448
- }
449
- async function applyTemplateOverlay({ dest, type, template, category, variant }) {
450
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
451
- const defaultSrc = path.join(__dirname, "..", "templates", "default");
452
- if (await fs.pathExists(defaultSrc)) {
453
- await fs.copy(defaultSrc, dest, { overwrite: false, errorOnExist: false });
454
- }
455
- let src = null;
456
- if (type === "app-brief" || type === "mini-app-brief") {
457
- if (category && variant) {
458
- src = path.join(__dirname, "..", "templates", type, category, variant);
459
- }
460
- } else if (type && template) {
461
- src = path.join(__dirname, "..", "templates", type, template);
462
- } else if (type === "default") {
463
- src = path.join(__dirname, "..", "templates", "default");
464
- }
465
- if (src && await fs.pathExists(src)) {
466
- await fs.copy(src, dest, { overwrite: true });
467
- }
468
- }
469
- function resolveExtraDeps({ type, template, category, variant }) {
470
- let key = null;
471
- if (type === "boilerplate" && template) key = `${type}/${template}`;
472
- if ((type === "app-brief" || type === "mini-app-brief") && category && variant) key = `${type}/${category}/${variant}`;
473
- const spec = key ? TEMPLATE_MAP[key] : void 0;
474
- return spec?.extraDeps ?? [];
475
- }
476
- async function installDeps(pm, cwd, _extraDeps) {
198
+ async function installDeps(pm, cwd) {
477
199
  const cmd = pm === "bun" ? "bun" : pm;
478
200
  const args = pm === "bun" ? ["install"] : ["install"];
479
201
  await execa(cmd, args, { cwd, stdio: "inherit" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newcandies",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Scaffold Expo Router + Uniwind React Native apps with layered templates.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,40 @@
1
+ {
2
+ "expo": {
3
+ "name": "Newcandies App",
4
+ "slug": "newcandies-app",
5
+ "version": "1.0.0",
6
+ "orientation": "portrait",
7
+ "icon": "./assets/images/icon.png",
8
+ "scheme": "newcandies-app",
9
+ "userInterfaceStyle": "automatic",
10
+ "newArchEnabled": true,
11
+ "splash": {
12
+ "image": "./assets/images/splash-icon.png",
13
+ "resizeMode": "contain",
14
+ "backgroundColor": "#ffffff"
15
+ },
16
+ "ios": {
17
+ "supportsTablet": true,
18
+ "bundleIdentifier": "com.example.newcandiesapp"
19
+ },
20
+ "android": {
21
+ "adaptiveIcon": {
22
+ "foregroundImage": "./assets/images/adaptive-icon.png",
23
+ "backgroundColor": "#ffffff"
24
+ },
25
+ "edgeToEdgeEnabled": true,
26
+ "predictiveBackGestureEnabled": false,
27
+ "package": "com.example.newcandiesapp"
28
+ },
29
+ "web": {
30
+ "bundler": "metro",
31
+ "output": "static",
32
+ "favicon": "./assets/images/favicon.png"
33
+ },
34
+ "plugins": ["expo-router"],
35
+ "experiments": {
36
+ "typedRoutes": true
37
+ }
38
+ }
39
+ }
40
+
@@ -0,0 +1,11 @@
1
+ module.exports = function (api) {
2
+ api.cache(true);
3
+ return {
4
+ presets: ['babel-preset-expo'],
5
+ plugins: [
6
+ ['module-resolver', { alias: { '~': './' } }],
7
+ 'react-native-reanimated/plugin'
8
+ ]
9
+ };
10
+ };
11
+
@@ -0,0 +1,10 @@
1
+ {
2
+ "cli": { "version": ">= 7.0.0" },
3
+ "build": {
4
+ "development": { "android": { "buildType": "apk" }, "developmentClient": true, "distribution": "internal" },
5
+ "preview": { "distribution": "internal" },
6
+ "production": {}
7
+ },
8
+ "submit": { "production": {} }
9
+ }
10
+
@@ -0,0 +1,3 @@
1
+ /// <reference types="expo" />
2
+ /// <reference types="expo-router" />
3
+
@@ -0,0 +1,18 @@
1
+ @import 'tailwindcss';
2
+ @import 'uniwind';
3
+
4
+ @layer theme {
5
+ :root {
6
+ @variant light {
7
+ --color-background: #ffffff;
8
+ --color-foreground: #111827;
9
+ --color-primary: #3b82f6;
10
+ }
11
+ @variant dark {
12
+ --color-background: #000000;
13
+ --color-foreground: #ffffff;
14
+ --color-primary: #60a5fa;
15
+ }
16
+ }
17
+ }
18
+
@@ -0,0 +1,10 @@
1
+ const { getDefaultConfig } = require('expo/metro-config');
2
+ const { withUniwindConfig } = require('uniwind/metro');
3
+
4
+ const config = getDefaultConfig(__dirname);
5
+
6
+ module.exports = withUniwindConfig(config, {
7
+ cssEntryFile: './global.css',
8
+ dtsFile: './uniwind-types.d.ts',
9
+ });
10
+