create-pika-minigame 1.0.0 → 1.2.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 +111 -36
- package/package.json +2 -2
- package/templates/README.md +51 -77
- package/templates/app.json.template +1 -11
- package/templates/metro.config.js +15 -0
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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,47 @@ 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 auto-patch to post_install
|
|
107
|
+
const fmtFix = `
|
|
108
|
+
# Fix fmt consteval issue with newer Xcode (auto-patch)
|
|
109
|
+
fmt_base_h = File.join(__dir__, 'Pods/fmt/include/fmt/base.h')
|
|
110
|
+
if File.exist?(fmt_base_h)
|
|
111
|
+
content = File.read(fmt_base_h)
|
|
112
|
+
patched = content.gsub(
|
|
113
|
+
/#elif defined\\(__apple_build_version__\\) && __apple_build_version__ < \\d+L/,
|
|
114
|
+
'#elif defined(__apple_build_version__)'
|
|
115
|
+
)
|
|
116
|
+
if content != patched
|
|
117
|
+
File.write(fmt_base_h, patched)
|
|
118
|
+
Pod::UI.puts "Patched fmt/base.h for Xcode compatibility".green
|
|
119
|
+
end
|
|
120
|
+
end`;
|
|
121
|
+
|
|
122
|
+
// Insert after react_native_post_install
|
|
123
|
+
if (content.includes('react_native_post_install')) {
|
|
124
|
+
content = content.replace(
|
|
125
|
+
/(react_native_post_install\([^)]+\))/,
|
|
126
|
+
`$1\n${fmtFix}`
|
|
127
|
+
);
|
|
128
|
+
fs.writeFileSync(podfilePath, content);
|
|
129
|
+
log.success('Patched Podfile with auto fmt fix');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
101
134
|
function main() {
|
|
102
135
|
const args = process.argv.slice(2);
|
|
103
136
|
|
|
104
|
-
// Handle flags
|
|
105
137
|
if (args.includes('--help') || args.includes('-h') || args.length === 0) {
|
|
106
138
|
showHelp();
|
|
107
139
|
process.exit(0);
|
|
@@ -127,6 +159,7 @@ function main() {
|
|
|
127
159
|
}
|
|
128
160
|
|
|
129
161
|
const targetDir = path.resolve(process.cwd(), projectName);
|
|
162
|
+
const pascalName = toPascalCase(projectName);
|
|
130
163
|
|
|
131
164
|
if (fs.existsSync(targetDir)) {
|
|
132
165
|
log.error(`Directory "${projectName}" already exists.`);
|
|
@@ -143,44 +176,86 @@ ${colors.bright}${colors.cyan}
|
|
|
143
176
|
${colors.reset}
|
|
144
177
|
`);
|
|
145
178
|
|
|
146
|
-
log.info(`Creating a new Pika mini-game
|
|
179
|
+
log.info(`Creating a new Pika mini-game: ${colors.bright}${pascalName}${colors.reset}`);
|
|
147
180
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
181
|
+
// Step 1: Create React Native project
|
|
182
|
+
log.step(1, 'Creating React Native 0.77 project...');
|
|
183
|
+
|
|
184
|
+
const initSuccess = runCommand(
|
|
185
|
+
`npx @react-native-community/cli init ${pascalName} --version 0.77.0 --directory "${projectName}" --skip-install`,
|
|
186
|
+
process.cwd()
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
if (!initSuccess) {
|
|
190
|
+
log.error('Failed to create React Native project');
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Step 2: Copy Pika mini-game source files
|
|
195
|
+
log.step(2, 'Adding Pika mini-game template files...');
|
|
196
|
+
|
|
197
|
+
const templatesDir = path.join(__dirname, '..', 'templates');
|
|
198
|
+
const srcTemplateDir = path.join(templatesDir, 'src');
|
|
199
|
+
const devTemplateDir = path.join(templatesDir, 'dev');
|
|
151
200
|
|
|
152
201
|
const replacements = {
|
|
153
202
|
PROJECT_NAME: projectName,
|
|
154
|
-
PROJECT_NAME_KEBAB: kebabName,
|
|
155
203
|
PROJECT_NAME_PASCAL: pascalName,
|
|
156
|
-
PROJECT_NAME_SNAKE: snakeName,
|
|
157
|
-
YEAR: new Date().getFullYear().toString(),
|
|
158
204
|
};
|
|
159
205
|
|
|
160
|
-
|
|
206
|
+
// Copy src folder
|
|
207
|
+
if (fs.existsSync(srcTemplateDir)) {
|
|
208
|
+
copyDir(srcTemplateDir, path.join(targetDir, 'src'), replacements);
|
|
209
|
+
}
|
|
161
210
|
|
|
162
|
-
|
|
163
|
-
|
|
211
|
+
// Copy dev folder
|
|
212
|
+
if (fs.existsSync(devTemplateDir)) {
|
|
213
|
+
copyDir(devTemplateDir, path.join(targetDir, 'dev'), replacements);
|
|
214
|
+
}
|
|
164
215
|
|
|
165
|
-
|
|
216
|
+
// Step 3: Patch Podfile for fmt compatibility
|
|
217
|
+
log.step(3, 'Patching for Xcode compatibility...');
|
|
218
|
+
patchPodfile(targetDir);
|
|
219
|
+
|
|
220
|
+
// Step 4: Update package.json with additional dependencies
|
|
221
|
+
log.step(4, 'Updating package.json...');
|
|
222
|
+
|
|
223
|
+
const pkgPath = path.join(targetDir, 'package.json');
|
|
224
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
225
|
+
|
|
226
|
+
pkg.dependencies = {
|
|
227
|
+
...pkg.dependencies,
|
|
228
|
+
'react-native-gesture-handler': '~2.20.0',
|
|
229
|
+
'react-native-reanimated': '~3.16.0',
|
|
230
|
+
'react-native-safe-area-context': '4.12.0',
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// No additional scripts needed - fmt fix is automatic in Podfile
|
|
234
|
+
|
|
235
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
|
|
236
|
+
|
|
237
|
+
log.step(5, 'Project created successfully!');
|
|
166
238
|
|
|
167
239
|
console.log(`
|
|
168
|
-
${colors.green}Success!${colors.reset} Created ${colors.bright}${
|
|
240
|
+
${colors.green}Success!${colors.reset} Created ${colors.bright}${pascalName}${colors.reset} at ${targetDir}
|
|
169
241
|
|
|
170
242
|
${colors.bright}Next steps:${colors.reset}
|
|
171
243
|
|
|
172
244
|
${colors.cyan}cd ${projectName}${colors.reset}
|
|
173
|
-
${colors.cyan}
|
|
245
|
+
${colors.cyan}npm install --legacy-peer-deps${colors.reset}
|
|
246
|
+
${colors.cyan}cd ios && pod install && cd ..${colors.reset}
|
|
247
|
+
${colors.cyan}npm run ios${colors.reset}
|
|
174
248
|
|
|
175
249
|
${colors.bright}Available commands:${colors.reset}
|
|
176
250
|
|
|
177
|
-
${colors.cyan}
|
|
178
|
-
${colors.cyan}
|
|
179
|
-
${colors.cyan}
|
|
180
|
-
|
|
251
|
+
${colors.cyan}npm run ios${colors.reset} Run on iOS simulator
|
|
252
|
+
${colors.cyan}npm run android${colors.reset} Run on Android emulator
|
|
253
|
+
${colors.cyan}npm start${colors.reset} Start Metro bundler
|
|
254
|
+
|
|
255
|
+
${colors.bright}Project structure:${colors.reset}
|
|
181
256
|
|
|
182
|
-
|
|
183
|
-
|
|
257
|
+
src/ Game source code (edit this!)
|
|
258
|
+
dev/ Development helpers (MockHost)
|
|
184
259
|
|
|
185
260
|
Happy coding! ${colors.yellow}🎮${colors.reset}
|
|
186
261
|
`);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pika-minigame",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "CLI to scaffold Pika mini-game projects
|
|
3
|
+
"version": "1.2.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": {
|
package/templates/README.md
CHANGED
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
# {{PROJECT_NAME_PASCAL}}
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
9
|
+
npm install --legacy-peer-deps
|
|
10
10
|
|
|
11
|
-
#
|
|
12
|
-
|
|
11
|
+
# iOS setup (auto-patches Xcode compatibility)
|
|
12
|
+
cd ios && pod install && cd ..
|
|
13
13
|
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
yarn build:android
|
|
14
|
+
# Run
|
|
15
|
+
npm run ios
|
|
17
16
|
```
|
|
18
17
|
|
|
19
18
|
## Project Structure
|
|
20
19
|
|
|
21
20
|
```
|
|
22
|
-
{{
|
|
21
|
+
{{PROJECT_NAME}}/
|
|
23
22
|
├── src/ # Your game source code
|
|
24
23
|
│ ├── App.tsx # Main entry component
|
|
25
24
|
│ ├── tokens.ts # Design system tokens
|
|
@@ -30,26 +29,34 @@ yarn build:android
|
|
|
30
29
|
│ ├── TopBar.tsx
|
|
31
30
|
│ └── Spinner.tsx
|
|
32
31
|
│
|
|
33
|
-
├── dev/ #
|
|
34
|
-
│ ├── index.js # Dev entry point
|
|
35
|
-
│ ├── DevApp.tsx # Wrapper with mock host
|
|
32
|
+
├── dev/ # Development helpers
|
|
36
33
|
│ └── MockHost.ts # Mock native capabilities
|
|
37
34
|
│
|
|
38
|
-
├──
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
├── scripts/ # Build scripts
|
|
36
|
+
│ └── fix-fmt.sh # Xcode compatibility fix
|
|
37
|
+
│
|
|
38
|
+
├── ios/ # iOS native code
|
|
39
|
+
├── android/ # Android native code
|
|
41
40
|
└── package.json
|
|
42
41
|
```
|
|
43
42
|
|
|
44
43
|
## Development
|
|
45
44
|
|
|
46
|
-
### Running
|
|
45
|
+
### Running on iOS
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Terminal 1: Start Metro
|
|
49
|
+
npm start
|
|
47
50
|
|
|
48
|
-
|
|
51
|
+
# Terminal 2: Run app
|
|
52
|
+
npm run ios
|
|
53
|
+
```
|
|
49
54
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
### Running on Android
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm run android
|
|
59
|
+
```
|
|
53
60
|
|
|
54
61
|
### Mock Host
|
|
55
62
|
|
|
@@ -58,7 +65,7 @@ Edit `dev/MockHost.ts` to customize mock behavior:
|
|
|
58
65
|
```typescript
|
|
59
66
|
// Return specific scores for testing
|
|
60
67
|
stopAndCheck: async (word) => ({
|
|
61
|
-
totalScore: 95,
|
|
68
|
+
totalScore: 95,
|
|
62
69
|
words: [{ word, score: 95, letters: [...] }]
|
|
63
70
|
})
|
|
64
71
|
|
|
@@ -66,9 +73,6 @@ stopAndCheck: async (word) => ({
|
|
|
66
73
|
startRecording: async () => {
|
|
67
74
|
throw new Error('Microphone permission denied');
|
|
68
75
|
}
|
|
69
|
-
|
|
70
|
-
// Add delays for loading state testing
|
|
71
|
-
await delay(3000);
|
|
72
76
|
```
|
|
73
77
|
|
|
74
78
|
## Design System
|
|
@@ -82,13 +86,13 @@ import { color, semantic, space, radius, typography } from './tokens';
|
|
|
82
86
|
|
|
83
87
|
const styles = StyleSheet.create({
|
|
84
88
|
container: {
|
|
85
|
-
backgroundColor: semantic.bg,
|
|
86
|
-
padding: space[4],
|
|
87
|
-
borderRadius: radius.lg,
|
|
89
|
+
backgroundColor: semantic.bg,
|
|
90
|
+
padding: space[4],
|
|
91
|
+
borderRadius: radius.lg,
|
|
88
92
|
},
|
|
89
93
|
title: {
|
|
90
|
-
color: semantic.text,
|
|
91
|
-
fontSize: typography.title1.size,
|
|
94
|
+
color: semantic.text,
|
|
95
|
+
fontSize: typography.title1.size,
|
|
92
96
|
fontWeight: typography.title1.fontWeight,
|
|
93
97
|
},
|
|
94
98
|
});
|
|
@@ -101,18 +105,9 @@ Pre-built components in `src/components/`:
|
|
|
101
105
|
```typescript
|
|
102
106
|
import { Button, Card, TopBar, Spinner } from './components';
|
|
103
107
|
|
|
104
|
-
// Button variants: primary, secondary, danger, ghost
|
|
105
108
|
<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
|
|
109
|
+
<Card onPress={...}><Text>Tap me</Text></Card>
|
|
113
110
|
<TopBar left="back" onLeftPress={goBack} title="Game" />
|
|
114
|
-
|
|
115
|
-
// Loading spinner
|
|
116
111
|
<Spinner size="lg" color={color.cyan[400]} />
|
|
117
112
|
```
|
|
118
113
|
|
|
@@ -130,67 +125,46 @@ interface HostBridge {
|
|
|
130
125
|
}
|
|
131
126
|
```
|
|
132
127
|
|
|
133
|
-
Usage
|
|
128
|
+
Usage:
|
|
134
129
|
|
|
135
130
|
```typescript
|
|
136
131
|
const App: React.FC<GameProps> = ({ host }) => {
|
|
137
132
|
const startRecording = async () => {
|
|
138
133
|
await host?.pronunciation.startRecording();
|
|
139
134
|
};
|
|
140
|
-
|
|
141
|
-
const checkPronunciation = async (word: string) => {
|
|
142
|
-
const result = await host?.pronunciation.stopAndCheck(word);
|
|
143
|
-
console.log('Score:', result.totalScore);
|
|
144
|
-
};
|
|
145
135
|
};
|
|
146
136
|
```
|
|
147
137
|
|
|
148
|
-
##
|
|
138
|
+
## Troubleshooting
|
|
149
139
|
|
|
150
|
-
###
|
|
140
|
+
### Xcode fmt errors
|
|
151
141
|
|
|
142
|
+
Re-run pod install (it auto-patches):
|
|
152
143
|
```bash
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
# Android
|
|
157
|
-
yarn build:android
|
|
144
|
+
cd ios && rm -rf Pods Podfile.lock && pod install && cd ..
|
|
145
|
+
# Then in Xcode: Cmd+Shift+K (Clean), Cmd+R (Run)
|
|
158
146
|
```
|
|
159
147
|
|
|
160
|
-
###
|
|
148
|
+
### Metro port in use
|
|
161
149
|
|
|
162
150
|
```bash
|
|
163
|
-
|
|
151
|
+
lsof -ti:8081 | xargs kill -9
|
|
152
|
+
npm start
|
|
164
153
|
```
|
|
165
154
|
|
|
166
|
-
###
|
|
155
|
+
### Pod install fails
|
|
167
156
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
"deploy:ios": "MF_PUBLIC_PATH=https://your-cdn.com/games/{{PROJECT_NAME_KEBAB}}/ yarn build:ios && <your-deploy-command>"
|
|
174
|
-
}
|
|
175
|
-
}
|
|
157
|
+
```bash
|
|
158
|
+
cd ios
|
|
159
|
+
rm -rf Pods Podfile.lock
|
|
160
|
+
pod install
|
|
161
|
+
cd ..
|
|
176
162
|
```
|
|
177
163
|
|
|
178
164
|
## Integration with Pika App
|
|
179
165
|
|
|
180
|
-
Once
|
|
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
|
|
166
|
+
Once ready, this mini-game can be loaded via Module Federation in the main Pika app.
|
|
193
167
|
|
|
194
|
-
|
|
168
|
+
---
|
|
195
169
|
|
|
196
|
-
|
|
170
|
+
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;
|