newcandies 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
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,20 @@
1
+ # newcandies
2
+
3
+ A CLI to scaffold Expo Router + Uniwind React Native apps with layered templates.
4
+
5
+ Highlights
6
+ - Always Expo Router
7
+ - ES6 arrow functions for root components
8
+ - Pre-configured: react-native-reanimated, react-native-gesture-handler, react-native-safe-area-context, react-native-screens
9
+ - Uniwind (Tailwind v4) configured out of the box
10
+ - Choice of navigation style via Expo Router: Stack or Tabs
11
+ - Template-driven extra dependencies per boilerplate
12
+
13
+ Usage (after publish)
14
+ - npx newcandies@latest
15
+
16
+ Local dev
17
+ - npm i
18
+ - npm run build
19
+ - npm link
20
+ - newcandies
package/dist/index.js ADDED
@@ -0,0 +1,335 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import * as p from "@clack/prompts";
5
+ import pc from "picocolors";
6
+ import { Command } from "commander";
7
+ import fs from "fs-extra";
8
+ import path from "path";
9
+ import { fileURLToPath } from "url";
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
+ };
19
+ var TEMPLATES = {
20
+ "boilerplate": [
21
+ { value: "holidia", label: "holidia" },
22
+ { value: "supersimplenotes", label: "supersimplenotes" }
23
+ ],
24
+ "mini-boilerplate": [
25
+ { value: "auth-mini", label: "Auth Mini" }
26
+ ],
27
+ "app-brief": {
28
+ categories: [
29
+ { value: "react-query", label: "react-query", variants: [{ value: "sproutsy", label: "sproutsy" }] }
30
+ ]
31
+ },
32
+ "mini-app-brief": { categories: [{ value: "forms", label: "forms", variants: [{ value: "lite", label: "lite" }] }] },
33
+ "default": [{ value: "default", label: "Default (Expo Router + Uniwind)" }]
34
+ };
35
+ var program = new Command().name("newcandies").option("-t, --type <type>").option("--template <name>").option("--category <name>").option("--variant <name>").option("-n, --navigator <navigator>", "stack|tabs", "tabs").option("-y, --yes").option("--pm <pm>", "npm|pnpm|yarn|bun").option("--no-install").parse(process.argv);
36
+ var opts = program.opts();
37
+ async function main() {
38
+ console.clear();
39
+ p.intro(pc.bgMagenta(pc.black(" newcandies ")));
40
+ const project = await p.group({
41
+ name: () => p.text({
42
+ message: "Project name?",
43
+ placeholder: "my-newcandies-app",
44
+ validate: (v) => !v ? "Enter a name" : /[^a-zA-Z0-9_-]/.test(v) ? "Use letters, numbers, _ or -" : void 0
45
+ }),
46
+ type: async () => {
47
+ if (opts.type) return opts.type;
48
+ return p.select({
49
+ message: "Pick type",
50
+ options: [
51
+ { value: "boilerplate", label: "Boilerplate (main projects)" },
52
+ { value: "mini-boilerplate", label: "Mini-boilerplate" },
53
+ { value: "app-brief", label: "App-brief (2 levels)" },
54
+ { value: "mini-app-brief", label: "Mini-app-brief (2 levels)" },
55
+ { value: "default", label: "Default" }
56
+ ]
57
+ });
58
+ },
59
+ template: async ({ results }) => {
60
+ if (results.type === "app-brief" || results.type === "mini-app-brief") return null;
61
+ if (opts.template) return opts.template;
62
+ const list = TEMPLATESafe(results.type);
63
+ return p.select({ message: "Pick template", options: list });
64
+ },
65
+ category: async ({ results }) => {
66
+ if (results.type !== "app-brief" && results.type !== "mini-app-brief") return null;
67
+ if (opts.category) return opts.category;
68
+ const cats = TEMPLATES[results.type].categories;
69
+ return p.select({ message: "Pick category", options: cats });
70
+ },
71
+ variant: async ({ results }) => {
72
+ if (results.type !== "app-brief" && results.type !== "mini-app-brief") return null;
73
+ if (opts.variant) return opts.variant;
74
+ const cats = TEMPLATES[results.type].categories;
75
+ const cat = cats.find((c) => c.value === results.category);
76
+ return p.select({ message: "Pick variant", options: cat.variants });
77
+ },
78
+ navigator: async () => {
79
+ if (opts.navigator) return opts.navigator;
80
+ return p.select({
81
+ message: "Navigator",
82
+ options: [
83
+ { value: "stack", label: "Stack" },
84
+ { value: "tabs", label: "Tabs" }
85
+ ],
86
+ initialValue: "tabs"
87
+ });
88
+ },
89
+ install: async () => {
90
+ if (opts.yes !== void 0) return !!opts.yes;
91
+ if (opts.install === false) return false;
92
+ return p.confirm({ message: "Install dependencies?", initialValue: true });
93
+ },
94
+ pm: async () => {
95
+ if (opts.pm) return opts.pm;
96
+ return p.select({
97
+ message: "Package manager?",
98
+ options: [
99
+ { value: "pnpm", label: "pnpm" },
100
+ { value: "npm", label: "npm" },
101
+ { value: "yarn", label: "yarn" },
102
+ { value: "bun", label: "bun" }
103
+ ],
104
+ initialValue: "pnpm"
105
+ });
106
+ }
107
+ }, { onCancel: () => {
108
+ p.cancel("Aborted");
109
+ process.exit(0);
110
+ } });
111
+ const dest = path.resolve(process.cwd(), project.name);
112
+ const s = p.spinner();
113
+ s.start("Scaffolding");
114
+ await fs.ensureDir(dest);
115
+ await writeBaseline({ dest, name: project.name, navigator: project.navigator });
116
+ await applyTemplateOverlay({ dest, type: project.type, template: project.template, category: project.category, variant: project.variant });
117
+ s.stop("Files ready");
118
+ const extraDeps = resolveExtraDeps({ type: project.type, template: project.template, category: project.category, variant: project.variant });
119
+ if (project.install) {
120
+ const si = p.spinner();
121
+ si.start("Installing dependencies (via Expo)");
122
+ await installDeps(project.pm, dest, extraDeps);
123
+ si.stop("Installed");
124
+ }
125
+ p.outro([
126
+ pc.green("Next steps:"),
127
+ ` cd ${project.name}`,
128
+ project.install ? "" : ` ${project.pm} install`,
129
+ ` ${project.pm} ${project.pm === "npm" ? "run " : ""}start`
130
+ ].filter(Boolean).join("\n"));
131
+ }
132
+ function TEMPLATESafe(type) {
133
+ const t = TEMPLATES[type];
134
+ if (Array.isArray(t)) return t;
135
+ return [{ value: "default", label: "Default (Expo Router + Uniwind)" }];
136
+ }
137
+ async function writeBaseline({ dest, name, navigator }) {
138
+ const pkg = {
139
+ name,
140
+ version: "0.0.0",
141
+ private: true,
142
+ main: "expo-router/entry",
143
+ scripts: {
144
+ start: "expo start",
145
+ android: "expo run:android",
146
+ ios: "expo run:ios"
147
+ }
148
+ };
149
+ await fs.writeJSON(path.join(dest, "package.json"), pkg, { spaces: 2 });
150
+ const appJson = {
151
+ expo: {
152
+ name,
153
+ slug: name,
154
+ scheme: "newcandies",
155
+ plugins: ["expo-router"],
156
+ experiments: { typedRoutes: true }
157
+ }
158
+ };
159
+ await fs.writeJSON(path.join(dest, "app.json"), appJson, { spaces: 2 });
160
+ const babel = `module.exports = function (api) {
161
+ api.cache(true);
162
+ return {
163
+ presets: ['babel-preset-expo'],
164
+ plugins: [
165
+ 'expo-router/babel',
166
+ 'react-native-reanimated/plugin'
167
+ ]
168
+ };
169
+ };
170
+ `;
171
+ await fs.writeFile(path.join(dest, "babel.config.js"), babel);
172
+ const metro = `const { getDefaultConfig } = require('expo/metro-config');
173
+ const { withUniwindConfig } = require('uniwind/metro');
174
+
175
+ const config = getDefaultConfig(__dirname);
176
+
177
+ module.exports = withUniwindConfig(config, {
178
+ cssEntryFile: './app/global.css',
179
+ dtsFile: './app/uniwind-types.d.ts',
180
+ });
181
+ `;
182
+ await fs.writeFile(path.join(dest, "metro.config.js"), metro);
183
+ const tsconfig = {
184
+ compilerOptions: {
185
+ strict: true,
186
+ jsx: "react-native",
187
+ target: "ES2020",
188
+ moduleResolution: "node",
189
+ types: ["react", "react-native", "expo-router"]
190
+ },
191
+ include: ["app", "./app/uniwind-types.d.ts"]
192
+ };
193
+ await fs.writeJSON(path.join(dest, "tsconfig.json"), tsconfig, { spaces: 2 });
194
+ const appDir = path.join(dest, "app");
195
+ await fs.ensureDir(appDir);
196
+ const globalCss = `@import 'tailwindcss';
197
+ @import 'uniwind';
198
+
199
+ @layer theme {
200
+ :root {
201
+ @variant light {
202
+ --color-background: #ffffff;
203
+ --color-foreground: #111827;
204
+ --color-primary: #3b82f6;
205
+ }
206
+ @variant dark {
207
+ --color-background: #000000;
208
+ --color-foreground: #ffffff;
209
+ --color-primary: #60a5fa;
210
+ }
211
+ }
212
+ }
213
+ `;
214
+ await fs.writeFile(path.join(appDir, "global.css"), globalCss);
215
+ if (navigator === "tabs") {
216
+ const tabsDir = path.join(appDir, "(tabs)");
217
+ await fs.ensureDir(tabsDir);
218
+ const layout = `import 'react-native-gesture-handler';
219
+ import '../global.css';
220
+ import { Tabs } from 'expo-router';
221
+ import { GestureHandlerRootView } from 'react-native-gesture-handler';
222
+ import { SafeAreaProvider } from 'react-native-safe-area-context';
223
+
224
+ const TabsLayout = () => {
225
+ return (
226
+ <GestureHandlerRootView style={{ flex: 1 }}>
227
+ <SafeAreaProvider>
228
+ <Tabs screenOptions={{ headerShown: false }} />
229
+ </SafeAreaProvider>
230
+ </GestureHandlerRootView>
231
+ );
232
+ };
233
+
234
+ export default TabsLayout;
235
+ `;
236
+ await fs.writeFile(path.join(tabsDir, "_layout.tsx"), layout);
237
+ const home = `import { View, Text } from 'react-native';
238
+
239
+ const Home = () => (
240
+ <View className="flex-1 items-center justify-center bg-background">
241
+ <Text className="text-foreground text-xl">Home Tab</Text>
242
+ </View>
243
+ );
244
+
245
+ export default Home;
246
+ `;
247
+ await fs.writeFile(path.join(tabsDir, "index.tsx"), home);
248
+ const settings = `import { View, Text } from 'react-native';
249
+
250
+ const Settings = () => (
251
+ <View className="flex-1 items-center justify-center bg-background">
252
+ <Text className="text-foreground text-xl">Settings Tab</Text>
253
+ </View>
254
+ );
255
+
256
+ export default Settings;
257
+ `;
258
+ await fs.writeFile(path.join(tabsDir, "settings.tsx"), settings);
259
+ } else {
260
+ const layout = `import 'react-native-gesture-handler';
261
+ import './global.css';
262
+ import { Stack } from 'expo-router';
263
+ import { GestureHandlerRootView } from 'react-native-gesture-handler';
264
+ import { SafeAreaProvider } from 'react-native-safe-area-context';
265
+
266
+ const RootLayout = () => {
267
+ return (
268
+ <GestureHandlerRootView style={{ flex: 1 }}>
269
+ <SafeAreaProvider>
270
+ <Stack screenOptions={{ headerShown: false }} />
271
+ </SafeAreaProvider>
272
+ </GestureHandlerRootView>
273
+ );
274
+ };
275
+
276
+ export default RootLayout;
277
+ `;
278
+ await fs.writeFile(path.join(appDir, "_layout.tsx"), layout);
279
+ const index = `import { View, Text } from 'react-native';
280
+
281
+ const Home = () => {
282
+ return (
283
+ <View className="flex-1 items-center justify-center bg-background">
284
+ <Text className="text-foreground text-xl">Hello from Stack + Uniwind + Router</Text>
285
+ </View>
286
+ );
287
+ };
288
+
289
+ export default Home;
290
+ `;
291
+ await fs.writeFile(path.join(appDir, "index.tsx"), index);
292
+ }
293
+ }
294
+ async function applyTemplateOverlay({ dest, type, template, category, variant }) {
295
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
296
+ let src = null;
297
+ if (type === "app-brief" || type === "mini-app-brief") {
298
+ if (category && variant) {
299
+ src = path.join(__dirname, "..", "templates", type, category, variant);
300
+ }
301
+ } else if (type && template) {
302
+ src = path.join(__dirname, "..", "templates", type, template);
303
+ } else if (type === "default") {
304
+ src = path.join(__dirname, "..", "templates", "default");
305
+ }
306
+ if (src && await fs.pathExists(src)) {
307
+ await fs.copy(src, dest, { overwrite: true });
308
+ }
309
+ }
310
+ function resolveExtraDeps({ type, template, category, variant }) {
311
+ let key = null;
312
+ if (type === "boilerplate" && template) key = `${type}/${template}`;
313
+ if ((type === "app-brief" || type === "mini-app-brief") && category && variant) key = `${type}/${category}/${variant}`;
314
+ const spec = key ? TEMPLATE_MAP[key] : void 0;
315
+ return spec?.extraDeps ?? [];
316
+ }
317
+ async function installDeps(pm, cwd, extraDeps) {
318
+ const base = [
319
+ "expo",
320
+ "expo-router",
321
+ "react",
322
+ "react-native",
323
+ "react-native-reanimated",
324
+ "react-native-gesture-handler",
325
+ "react-native-safe-area-context",
326
+ "react-native-screens",
327
+ "tailwindcss",
328
+ "uniwind"
329
+ ];
330
+ await execa("npx", ["expo", "install", ...base, ...extraDeps], { cwd, stdio: "inherit" });
331
+ }
332
+ main().catch((e) => {
333
+ p.log.error(e?.message ?? String(e));
334
+ process.exit(1);
335
+ });
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "newcandies",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold Expo Router + Uniwind React Native apps with layered templates.",
5
+ "type": "module",
6
+ "bin": {
7
+ "newcandies": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "templates",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "keywords": ["expo", "react-native", "expo-router", "tailwind", "uniwind", "cli", "scaffold"],
16
+ "license": "MIT",
17
+ "engines": { "node": ">=18" },
18
+ "scripts": {
19
+ "build": "tsup src/index.ts --format esm --clean",
20
+ "dev": "tsup src/index.ts --format esm --watch --clean=false",
21
+ "start": "node dist/index.js",
22
+ "prepublishOnly": "npm run build"
23
+ },
24
+ "dependencies": {
25
+ "@clack/prompts": "^0.7.0",
26
+ "commander": "^12.1.0",
27
+ "execa": "^8.0.1",
28
+ "fs-extra": "^11.2.0",
29
+ "giget": "^1.2.1",
30
+ "picocolors": "^1.0.0",
31
+ "validate-npm-package-name": "^5.0.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/fs-extra": "^11.0.4",
35
+ "@types/node": "^22.7.5",
36
+ "tsup": "^8.2.4",
37
+ "typescript": "^5.6.3"
38
+ }
39
+ }
@@ -0,0 +1,3 @@
1
+ # Holidia template
2
+
3
+ Adds a sample screen at `app/holi.tsx`. Extra deps will include React Query and AsyncStorage.
@@ -0,0 +1,9 @@
1
+ import { View, Text } from 'react-native'
2
+
3
+ const Holi = () => (
4
+ <View className="flex-1 items-center justify-center bg-background">
5
+ <Text className="text-foreground text-xl">Holidia overlay screen</Text>
6
+ </View>
7
+ )
8
+
9
+ export default Holi
@@ -0,0 +1,3 @@
1
+ # SuperSimpleNotes template
2
+
3
+ Adds `app/notes.tsx`. Extra deps include `@react-native-community/netinfo`.
@@ -0,0 +1,9 @@
1
+ import { View, Text } from 'react-native'
2
+
3
+ const Notes = () => (
4
+ <View className="flex-1 items-center justify-center bg-background">
5
+ <Text className="text-foreground text-xl">SuperSimpleNotes overlay</Text>
6
+ </View>
7
+ )
8
+
9
+ export default Notes
@@ -0,0 +1 @@
1
+ Default overlay (empty).