create-pika-minigame 1.0.0 → 1.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/bin/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const { execSync, spawn } = require('child_process');
5
6
 
6
7
  // Colors for terminal output
7
8
  const colors = {
@@ -35,13 +36,12 @@ ${colors.bright}Options:${colors.reset}
35
36
  ${colors.bright}Examples:${colors.reset}
36
37
  npx create-pika-minigame my-awesome-game
37
38
  npx create-pika-minigame pronunciation-game
38
- npx create-pika-minigame quiz-game
39
39
 
40
40
  ${colors.bright}After creation:${colors.reset}
41
41
  cd <project-name>
42
- yarn install
43
- yarn dev # Run with mock host (standalone)
44
- yarn build:ios # Build for production
42
+ npm install
43
+ cd ios && pod install && cd ..
44
+ npm run ios # Run on iOS simulator
45
45
  `);
46
46
  }
47
47
 
@@ -50,13 +50,6 @@ function showVersion() {
50
50
  console.log(pkg.version);
51
51
  }
52
52
 
53
- function toKebabCase(str) {
54
- return str
55
- .replace(/([a-z])([A-Z])/g, '$1-$2')
56
- .replace(/[\s_]+/g, '-')
57
- .toLowerCase();
58
- }
59
-
60
53
  function toPascalCase(str) {
61
54
  return str
62
55
  .split(/[-_\s]+/)
@@ -64,11 +57,13 @@ function toPascalCase(str) {
64
57
  .join('');
65
58
  }
66
59
 
67
- function toSnakeCase(str) {
68
- return str
69
- .replace(/([a-z])([A-Z])/g, '$1_$2')
70
- .replace(/[\s-]+/g, '_')
71
- .toLowerCase();
60
+ function runCommand(command, cwd = process.cwd()) {
61
+ try {
62
+ execSync(command, { cwd, stdio: 'inherit' });
63
+ return true;
64
+ } catch (error) {
65
+ return false;
66
+ }
72
67
  }
73
68
 
74
69
  function copyDir(src, dest, replacements = {}) {
@@ -98,10 +93,56 @@ function copyDir(src, dest, replacements = {}) {
98
93
  }
99
94
  }
100
95
 
96
+ function patchPodfile(projectDir) {
97
+ const podfilePath = path.join(projectDir, 'ios', 'Podfile');
98
+
99
+ if (!fs.existsSync(podfilePath)) {
100
+ log.warn('Podfile not found, skipping patch');
101
+ return;
102
+ }
103
+
104
+ let content = fs.readFileSync(podfilePath, 'utf8');
105
+
106
+ // Add fmt fix to post_install
107
+ const fmtFix = `
108
+ # Fix fmt library compatibility with newer Xcode
109
+ installer.pods_project.targets.each do |target|
110
+ target.build_configurations.each do |config|
111
+ config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)']
112
+ config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'FMT_USE_CONSTEVAL=0'
113
+ end
114
+ end`;
115
+
116
+ // Insert before the last 'end' in post_install
117
+ if (content.includes('react_native_post_install')) {
118
+ content = content.replace(
119
+ /(react_native_post_install\([^)]+\))/,
120
+ `$1\n${fmtFix}`
121
+ );
122
+ fs.writeFileSync(podfilePath, content);
123
+ log.success('Patched Podfile with fmt fix');
124
+ }
125
+ }
126
+
127
+ function createPostInstallScript(projectDir) {
128
+ const scriptContent = `#!/bin/bash
129
+ # Fix fmt consteval issue for Xcode compatibility
130
+ FMT_BASE_H="ios/Pods/fmt/include/fmt/base.h"
131
+ if [ -f "$FMT_BASE_H" ]; then
132
+ sed -i '' 's/__apple_build_version__ < 14000029L/__apple_build_version__/' "$FMT_BASE_H"
133
+ echo "Patched fmt base.h for Xcode compatibility"
134
+ fi
135
+ `;
136
+
137
+ const scriptPath = path.join(projectDir, 'scripts', 'fix-fmt.sh');
138
+ fs.mkdirSync(path.join(projectDir, 'scripts'), { recursive: true });
139
+ fs.writeFileSync(scriptPath, scriptContent);
140
+ fs.chmodSync(scriptPath, '755');
141
+ }
142
+
101
143
  function main() {
102
144
  const args = process.argv.slice(2);
103
145
 
104
- // Handle flags
105
146
  if (args.includes('--help') || args.includes('-h') || args.length === 0) {
106
147
  showHelp();
107
148
  process.exit(0);
@@ -127,6 +168,7 @@ function main() {
127
168
  }
128
169
 
129
170
  const targetDir = path.resolve(process.cwd(), projectName);
171
+ const pascalName = toPascalCase(projectName);
130
172
 
131
173
  if (fs.existsSync(targetDir)) {
132
174
  log.error(`Directory "${projectName}" already exists.`);
@@ -143,44 +185,93 @@ ${colors.bright}${colors.cyan}
143
185
  ${colors.reset}
144
186
  `);
145
187
 
146
- log.info(`Creating a new Pika mini-game in ${colors.bright}${targetDir}${colors.reset}`);
188
+ log.info(`Creating a new Pika mini-game: ${colors.bright}${pascalName}${colors.reset}`);
147
189
 
148
- const kebabName = toKebabCase(projectName);
149
- const pascalName = toPascalCase(projectName);
150
- const snakeName = toSnakeCase(projectName);
190
+ // Step 1: Create React Native project
191
+ log.step(1, 'Creating React Native 0.77 project...');
192
+
193
+ const initSuccess = runCommand(
194
+ `npx @react-native-community/cli init ${pascalName} --version 0.77.0 --directory "${projectName}" --skip-install`,
195
+ process.cwd()
196
+ );
197
+
198
+ if (!initSuccess) {
199
+ log.error('Failed to create React Native project');
200
+ process.exit(1);
201
+ }
202
+
203
+ // Step 2: Copy Pika mini-game source files
204
+ log.step(2, 'Adding Pika mini-game template files...');
205
+
206
+ const templatesDir = path.join(__dirname, '..', 'templates');
207
+ const srcTemplateDir = path.join(templatesDir, 'src');
208
+ const devTemplateDir = path.join(templatesDir, 'dev');
151
209
 
152
210
  const replacements = {
153
211
  PROJECT_NAME: projectName,
154
- PROJECT_NAME_KEBAB: kebabName,
155
212
  PROJECT_NAME_PASCAL: pascalName,
156
- PROJECT_NAME_SNAKE: snakeName,
157
- YEAR: new Date().getFullYear().toString(),
158
213
  };
159
214
 
160
- log.step(1, 'Creating project structure...');
215
+ // Copy src folder
216
+ if (fs.existsSync(srcTemplateDir)) {
217
+ copyDir(srcTemplateDir, path.join(targetDir, 'src'), replacements);
218
+ }
161
219
 
162
- const templatesDir = path.join(__dirname, '..', 'templates');
163
- copyDir(templatesDir, targetDir, replacements);
220
+ // Copy dev folder
221
+ if (fs.existsSync(devTemplateDir)) {
222
+ copyDir(devTemplateDir, path.join(targetDir, 'dev'), replacements);
223
+ }
224
+
225
+ // Step 3: Patch Podfile for fmt compatibility
226
+ log.step(3, 'Patching for Xcode compatibility...');
227
+ patchPodfile(targetDir);
228
+ createPostInstallScript(targetDir);
164
229
 
165
- log.step(2, 'Project created successfully!');
230
+ // Step 4: Update package.json with additional dependencies
231
+ log.step(4, 'Updating package.json...');
232
+
233
+ const pkgPath = path.join(targetDir, 'package.json');
234
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
235
+
236
+ pkg.dependencies = {
237
+ ...pkg.dependencies,
238
+ 'react-native-gesture-handler': '~2.20.0',
239
+ 'react-native-reanimated': '~3.16.0',
240
+ 'react-native-safe-area-context': '4.12.0',
241
+ };
242
+
243
+ pkg.scripts = {
244
+ ...pkg.scripts,
245
+ 'fix-fmt': './scripts/fix-fmt.sh',
246
+ 'postinstall:ios': 'cd ios && pod install && cd .. && npm run fix-fmt',
247
+ };
248
+
249
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
250
+
251
+ log.step(5, 'Project created successfully!');
166
252
 
167
253
  console.log(`
168
- ${colors.green}Success!${colors.reset} Created ${colors.bright}${projectName}${colors.reset} at ${targetDir}
254
+ ${colors.green}Success!${colors.reset} Created ${colors.bright}${pascalName}${colors.reset} at ${targetDir}
169
255
 
170
256
  ${colors.bright}Next steps:${colors.reset}
171
257
 
172
258
  ${colors.cyan}cd ${projectName}${colors.reset}
173
- ${colors.cyan}yarn install${colors.reset}
259
+ ${colors.cyan}npm install --legacy-peer-deps${colors.reset}
260
+ ${colors.cyan}cd ios && pod install && cd ..${colors.reset}
261
+ ${colors.cyan}./scripts/fix-fmt.sh${colors.reset} ${colors.yellow}# Fix Xcode compatibility${colors.reset}
262
+ ${colors.cyan}npm run ios${colors.reset}
174
263
 
175
264
  ${colors.bright}Available commands:${colors.reset}
176
265
 
177
- ${colors.cyan}yarn dev${colors.reset} Start development with mock host (standalone)
178
- ${colors.cyan}yarn build:ios${colors.reset} Build for iOS production
179
- ${colors.cyan}yarn build:android${colors.reset} Build for Android production
180
- ${colors.cyan}yarn deploy${colors.reset} Deploy to CDN (requires setup)
266
+ ${colors.cyan}npm run ios${colors.reset} Run on iOS simulator
267
+ ${colors.cyan}npm run android${colors.reset} Run on Android emulator
268
+ ${colors.cyan}npm start${colors.reset} Start Metro bundler
269
+
270
+ ${colors.bright}Project structure:${colors.reset}
181
271
 
182
- ${colors.bright}Documentation:${colors.reset}
183
- See README.md in your project for detailed instructions.
272
+ src/ Game source code (edit this!)
273
+ dev/ Development helpers (MockHost)
274
+ scripts/ Build scripts
184
275
 
185
276
  Happy coding! ${colors.yellow}🎮${colors.reset}
186
277
  `);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-pika-minigame",
3
- "version": "1.0.0",
4
- "description": "CLI to scaffold Pika mini-game projects with standalone dev environment",
3
+ "version": "1.1.0",
4
+ "description": "CLI to scaffold Pika mini-game projects (bare React Native 0.77)",
5
5
  "author": "Pika Team",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -1,25 +1,27 @@
1
1
  # {{PROJECT_NAME_PASCAL}}
2
2
 
3
- A Pika mini-game built with React Native and Module Federation.
3
+ Pika Mini-Game built with React Native 0.77
4
4
 
5
5
  ## Quick Start
6
6
 
7
7
  ```bash
8
8
  # Install dependencies
9
- yarn install
9
+ npm install --legacy-peer-deps
10
10
 
11
- # Run in development mode (standalone with mock host)
12
- yarn dev
11
+ # iOS setup
12
+ cd ios && pod install && cd ..
13
13
 
14
- # Build for production
15
- yarn build:ios
16
- yarn build:android
14
+ # Fix Xcode compatibility (IMPORTANT!)
15
+ ./scripts/fix-fmt.sh
16
+
17
+ # Run
18
+ npm run ios
17
19
  ```
18
20
 
19
21
  ## Project Structure
20
22
 
21
23
  ```
22
- {{PROJECT_NAME_KEBAB}}/
24
+ {{PROJECT_NAME}}/
23
25
  ├── src/ # Your game source code
24
26
  │ ├── App.tsx # Main entry component
25
27
  │ ├── tokens.ts # Design system tokens
@@ -30,26 +32,34 @@ yarn build:android
30
32
  │ ├── TopBar.tsx
31
33
  │ └── Spinner.tsx
32
34
 
33
- ├── dev/ # Standalone development
34
- │ ├── index.js # Dev entry point
35
- │ ├── DevApp.tsx # Wrapper with mock host
35
+ ├── dev/ # Development helpers
36
36
  │ └── MockHost.ts # Mock native capabilities
37
37
 
38
- ├── index.js # Production entry (Module Federation)
39
- ├── rspack.config.mjs # Re.Pack bundler config
40
- ├── app.json # Expo config (for dev mode)
38
+ ├── scripts/ # Build scripts
39
+ │ └── fix-fmt.sh # Xcode compatibility fix
40
+
41
+ ├── ios/ # iOS native code
42
+ ├── android/ # Android native code
41
43
  └── package.json
42
44
  ```
43
45
 
44
46
  ## Development
45
47
 
46
- ### Running Standalone
48
+ ### Running on iOS
49
+
50
+ ```bash
51
+ # Terminal 1: Start Metro
52
+ npm start
53
+
54
+ # Terminal 2: Run app
55
+ npm run ios
56
+ ```
47
57
 
48
- The `yarn dev` command runs your mini-game as a standalone Expo app with a mocked host bridge. This lets you:
58
+ ### Running on Android
49
59
 
50
- - Develop without the main Pika app
51
- - Test UI and game logic independently
52
- - See console logs for mock API calls
60
+ ```bash
61
+ npm run android
62
+ ```
53
63
 
54
64
  ### Mock Host
55
65
 
@@ -58,7 +68,7 @@ Edit `dev/MockHost.ts` to customize mock behavior:
58
68
  ```typescript
59
69
  // Return specific scores for testing
60
70
  stopAndCheck: async (word) => ({
61
- totalScore: 95, // Always high score
71
+ totalScore: 95,
62
72
  words: [{ word, score: 95, letters: [...] }]
63
73
  })
64
74
 
@@ -66,9 +76,6 @@ stopAndCheck: async (word) => ({
66
76
  startRecording: async () => {
67
77
  throw new Error('Microphone permission denied');
68
78
  }
69
-
70
- // Add delays for loading state testing
71
- await delay(3000);
72
79
  ```
73
80
 
74
81
  ## Design System
@@ -82,13 +89,13 @@ import { color, semantic, space, radius, typography } from './tokens';
82
89
 
83
90
  const styles = StyleSheet.create({
84
91
  container: {
85
- backgroundColor: semantic.bg, // Dark background
86
- padding: space[4], // 16px
87
- borderRadius: radius.lg, // 16px
92
+ backgroundColor: semantic.bg,
93
+ padding: space[4],
94
+ borderRadius: radius.lg,
88
95
  },
89
96
  title: {
90
- color: semantic.text, // White
91
- fontSize: typography.title1.size, // 28
97
+ color: semantic.text,
98
+ fontSize: typography.title1.size,
92
99
  fontWeight: typography.title1.fontWeight,
93
100
  },
94
101
  });
@@ -101,18 +108,9 @@ Pre-built components in `src/components/`:
101
108
  ```typescript
102
109
  import { Button, Card, TopBar, Spinner } from './components';
103
110
 
104
- // Button variants: primary, secondary, danger, ghost
105
111
  <Button label="Start" variant="primary" size="lg" onPress={...} />
106
-
107
- // Card with press interaction
108
- <Card onPress={...}>
109
- <Text>Tap me</Text>
110
- </Card>
111
-
112
- // TopBar with back/close buttons
112
+ <Card onPress={...}><Text>Tap me</Text></Card>
113
113
  <TopBar left="back" onLeftPress={goBack} title="Game" />
114
-
115
- // Loading spinner
116
114
  <Spinner size="lg" color={color.cyan[400]} />
117
115
  ```
118
116
 
@@ -130,67 +128,45 @@ interface HostBridge {
130
128
  }
131
129
  ```
132
130
 
133
- Usage in your game:
131
+ Usage:
134
132
 
135
133
  ```typescript
136
134
  const App: React.FC<GameProps> = ({ host }) => {
137
135
  const startRecording = async () => {
138
136
  await host?.pronunciation.startRecording();
139
137
  };
140
-
141
- const checkPronunciation = async (word: string) => {
142
- const result = await host?.pronunciation.stopAndCheck(word);
143
- console.log('Score:', result.totalScore);
144
- };
145
138
  };
146
139
  ```
147
140
 
148
- ## Building for Production
141
+ ## Troubleshooting
149
142
 
150
- ### 1. Build the bundle
143
+ ### Xcode fmt errors
151
144
 
152
145
  ```bash
153
- # iOS
154
- yarn build:ios
155
-
156
- # Android
157
- yarn build:android
146
+ ./scripts/fix-fmt.sh
147
+ # Then in Xcode: Cmd+Shift+K (Clean), Cmd+R (Run)
158
148
  ```
159
149
 
160
- ### 2. Serve locally for testing
150
+ ### Metro port in use
161
151
 
162
152
  ```bash
163
- yarn serve:ios # Serves on http://localhost:9000
153
+ lsof -ti:8081 | xargs kill -9
154
+ npm start
164
155
  ```
165
156
 
166
- ### 3. Deploy to CDN
157
+ ### Pod install fails
167
158
 
168
- Edit `package.json` to add your deployment command:
169
-
170
- ```json
171
- {
172
- "scripts": {
173
- "deploy:ios": "MF_PUBLIC_PATH=https://your-cdn.com/games/{{PROJECT_NAME_KEBAB}}/ yarn build:ios && <your-deploy-command>"
174
- }
175
- }
159
+ ```bash
160
+ cd ios
161
+ rm -rf Pods Podfile.lock
162
+ pod install
163
+ cd ..
176
164
  ```
177
165
 
178
166
  ## Integration with Pika App
179
167
 
180
- Once deployed, the main Pika app can load your mini-game:
181
-
182
- ```typescript
183
- // In the host app
184
- const game = await loadRemote('{{PROJECT_NAME_SNAKE}}/App');
185
- ```
186
-
187
- ## Tips
188
-
189
- 1. **Test on real devices** - Mock host simulates behavior, but test on device for real performance
190
- 2. **Keep it fast** - Mini-games should load instantly; minimize bundle size
191
- 3. **Handle errors** - Always check if `host` is defined before using
192
- 4. **Dark theme** - Default tokens are dark-themed for immersive game feel
168
+ Once ready, this mini-game can be loaded via Module Federation in the main Pika app.
193
169
 
194
- ## License
170
+ ---
195
171
 
196
- MIT © {{YEAR}} Pika Team
172
+ Created with `create-pika-minigame` v1.1.0
@@ -4,11 +4,8 @@
4
4
  "slug": "{{PROJECT_NAME_KEBAB}}",
5
5
  "version": "1.0.0",
6
6
  "orientation": "portrait",
7
- "icon": "./assets/icon.png",
8
7
  "userInterfaceStyle": "dark",
9
8
  "splash": {
10
- "image": "./assets/splash.png",
11
- "resizeMode": "contain",
12
9
  "backgroundColor": "#0B0B0B"
13
10
  },
14
11
  "ios": {
@@ -16,14 +13,7 @@
16
13
  "bundleIdentifier": "com.pika.minigame.{{PROJECT_NAME_KEBAB}}"
17
14
  },
18
15
  "android": {
19
- "adaptiveIcon": {
20
- "foregroundImage": "./assets/adaptive-icon.png",
21
- "backgroundColor": "#0B0B0B"
22
- },
23
16
  "package": "com.pika.minigame.{{PROJECT_NAME_SNAKE}}"
24
- },
25
- "plugins": [
26
- "expo-router"
27
- ]
17
+ }
28
18
  }
29
19
  }
@@ -0,0 +1,15 @@
1
+ const { getDefaultConfig } = require('expo/metro-config');
2
+ const path = require('path');
3
+
4
+ const projectRoot = __dirname;
5
+
6
+ const config = getDefaultConfig(projectRoot);
7
+
8
+ // Isolate this project - don't resolve from parent directories
9
+ config.watchFolders = [projectRoot];
10
+ config.resolver.nodeModulesPaths = [path.resolve(projectRoot, 'node_modules')];
11
+
12
+ // Disable looking up in parent directories
13
+ config.resolver.disableHierarchicalLookup = true;
14
+
15
+ module.exports = config;