create-pika-minigame 1.0.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/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # create-pika-minigame
2
+
3
+ CLI tool to scaffold Pika mini-game projects with standalone development environment.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npx create-pika-minigame my-awesome-game
9
+ cd my-awesome-game
10
+ yarn install
11
+ yarn dev
12
+ ```
13
+
14
+ ## What's Included
15
+
16
+ - **Design System Tokens** - Colors, spacing, typography aligned with Pika brand
17
+ - **UI Components** - Button, Card, TopBar, Spinner
18
+ - **Mock Host** - Test native features without the main app
19
+ - **Re.Pack Config** - Module Federation setup for production
20
+ - **TypeScript** - Full type safety
21
+
22
+ ## Publishing
23
+
24
+ ```bash
25
+ cd tools/create-pika-minigame
26
+ npm publish
27
+ ```
28
+
29
+ ## Development
30
+
31
+ ```bash
32
+ # Test the CLI locally
33
+ node bin/cli.js test-game
34
+
35
+ # Or link globally
36
+ npm link
37
+ create-pika-minigame test-game
38
+ ```
39
+
40
+ ## License
41
+
42
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ // Colors for terminal output
7
+ const colors = {
8
+ reset: '\x1b[0m',
9
+ bright: '\x1b[1m',
10
+ green: '\x1b[32m',
11
+ cyan: '\x1b[36m',
12
+ yellow: '\x1b[33m',
13
+ red: '\x1b[31m',
14
+ };
15
+
16
+ const log = {
17
+ info: (msg) => console.log(`${colors.cyan}info${colors.reset} ${msg}`),
18
+ success: (msg) => console.log(`${colors.green}success${colors.reset} ${msg}`),
19
+ warn: (msg) => console.log(`${colors.yellow}warn${colors.reset} ${msg}`),
20
+ error: (msg) => console.log(`${colors.red}error${colors.reset} ${msg}`),
21
+ step: (num, msg) => console.log(`\n${colors.bright}[${num}]${colors.reset} ${msg}`),
22
+ };
23
+
24
+ function showHelp() {
25
+ console.log(`
26
+ ${colors.bright}create-pika-minigame${colors.reset} - Scaffold a new Pika mini-game project
27
+
28
+ ${colors.bright}Usage:${colors.reset}
29
+ npx create-pika-minigame <project-name> [options]
30
+
31
+ ${colors.bright}Options:${colors.reset}
32
+ --help, -h Show this help message
33
+ --version, -v Show version
34
+
35
+ ${colors.bright}Examples:${colors.reset}
36
+ npx create-pika-minigame my-awesome-game
37
+ npx create-pika-minigame pronunciation-game
38
+ npx create-pika-minigame quiz-game
39
+
40
+ ${colors.bright}After creation:${colors.reset}
41
+ cd <project-name>
42
+ yarn install
43
+ yarn dev # Run with mock host (standalone)
44
+ yarn build:ios # Build for production
45
+ `);
46
+ }
47
+
48
+ function showVersion() {
49
+ const pkg = require('../package.json');
50
+ console.log(pkg.version);
51
+ }
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
+ function toPascalCase(str) {
61
+ return str
62
+ .split(/[-_\s]+/)
63
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
64
+ .join('');
65
+ }
66
+
67
+ function toSnakeCase(str) {
68
+ return str
69
+ .replace(/([a-z])([A-Z])/g, '$1_$2')
70
+ .replace(/[\s-]+/g, '_')
71
+ .toLowerCase();
72
+ }
73
+
74
+ function copyDir(src, dest, replacements = {}) {
75
+ if (!fs.existsSync(dest)) {
76
+ fs.mkdirSync(dest, { recursive: true });
77
+ }
78
+
79
+ const entries = fs.readdirSync(src, { withFileTypes: true });
80
+
81
+ for (const entry of entries) {
82
+ const srcPath = path.join(src, entry.name);
83
+ let destName = entry.name.replace('.template', '');
84
+ const destPath = path.join(dest, destName);
85
+
86
+ if (entry.isDirectory()) {
87
+ copyDir(srcPath, destPath, replacements);
88
+ } else {
89
+ let content = fs.readFileSync(srcPath, 'utf8');
90
+
91
+ // Apply replacements
92
+ for (const [key, value] of Object.entries(replacements)) {
93
+ content = content.replace(new RegExp(`{{${key}}}`, 'g'), value);
94
+ }
95
+
96
+ fs.writeFileSync(destPath, content);
97
+ }
98
+ }
99
+ }
100
+
101
+ function main() {
102
+ const args = process.argv.slice(2);
103
+
104
+ // Handle flags
105
+ if (args.includes('--help') || args.includes('-h') || args.length === 0) {
106
+ showHelp();
107
+ process.exit(0);
108
+ }
109
+
110
+ if (args.includes('--version') || args.includes('-v')) {
111
+ showVersion();
112
+ process.exit(0);
113
+ }
114
+
115
+ const projectName = args[0];
116
+
117
+ if (!projectName) {
118
+ log.error('Please specify a project name:');
119
+ console.log(' npx create-pika-minigame my-awesome-game');
120
+ process.exit(1);
121
+ }
122
+
123
+ // Validate project name
124
+ if (!/^[a-zA-Z][a-zA-Z0-9-_]*$/.test(projectName)) {
125
+ log.error('Project name must start with a letter and contain only letters, numbers, hyphens, and underscores.');
126
+ process.exit(1);
127
+ }
128
+
129
+ const targetDir = path.resolve(process.cwd(), projectName);
130
+
131
+ if (fs.existsSync(targetDir)) {
132
+ log.error(`Directory "${projectName}" already exists.`);
133
+ process.exit(1);
134
+ }
135
+
136
+ console.log(`
137
+ ${colors.bright}${colors.cyan}
138
+ ____ _ _ __ __ _ _ ____
139
+ | _ \\(_) | ____ _| \\/ (_)_ __ (_) / ___| __ _ _ __ ___ ___
140
+ | |_) | | |/ / _\` | |\\/| | | '_ \\| | | | _ / _\` | '_ \` _ \\ / _ \\
141
+ | __/| | < (_| | | | | | | | | | | |_| | (_| | | | | | | __/
142
+ |_| |_|_|\\_\\__,_|_| |_|_|_| |_|_| \\____|\\__,_|_| |_| |_|\\___|
143
+ ${colors.reset}
144
+ `);
145
+
146
+ log.info(`Creating a new Pika mini-game in ${colors.bright}${targetDir}${colors.reset}`);
147
+
148
+ const kebabName = toKebabCase(projectName);
149
+ const pascalName = toPascalCase(projectName);
150
+ const snakeName = toSnakeCase(projectName);
151
+
152
+ const replacements = {
153
+ PROJECT_NAME: projectName,
154
+ PROJECT_NAME_KEBAB: kebabName,
155
+ PROJECT_NAME_PASCAL: pascalName,
156
+ PROJECT_NAME_SNAKE: snakeName,
157
+ YEAR: new Date().getFullYear().toString(),
158
+ };
159
+
160
+ log.step(1, 'Creating project structure...');
161
+
162
+ const templatesDir = path.join(__dirname, '..', 'templates');
163
+ copyDir(templatesDir, targetDir, replacements);
164
+
165
+ log.step(2, 'Project created successfully!');
166
+
167
+ console.log(`
168
+ ${colors.green}Success!${colors.reset} Created ${colors.bright}${projectName}${colors.reset} at ${targetDir}
169
+
170
+ ${colors.bright}Next steps:${colors.reset}
171
+
172
+ ${colors.cyan}cd ${projectName}${colors.reset}
173
+ ${colors.cyan}yarn install${colors.reset}
174
+
175
+ ${colors.bright}Available commands:${colors.reset}
176
+
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)
181
+
182
+ ${colors.bright}Documentation:${colors.reset}
183
+ See README.md in your project for detailed instructions.
184
+
185
+ Happy coding! ${colors.yellow}🎮${colors.reset}
186
+ `);
187
+ }
188
+
189
+ main();
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "create-pika-minigame",
3
+ "version": "1.0.0",
4
+ "description": "CLI to scaffold Pika mini-game projects with standalone dev environment",
5
+ "author": "Pika Team",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "create-pika-minigame": "./bin/cli.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "templates"
13
+ ],
14
+ "keywords": [
15
+ "pika",
16
+ "mini-game",
17
+ "react-native",
18
+ "module-federation",
19
+ "repack"
20
+ ],
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/pika/create-pika-minigame"
27
+ }
28
+ }
@@ -0,0 +1,196 @@
1
+ # {{PROJECT_NAME_PASCAL}}
2
+
3
+ A Pika mini-game built with React Native and Module Federation.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # Install dependencies
9
+ yarn install
10
+
11
+ # Run in development mode (standalone with mock host)
12
+ yarn dev
13
+
14
+ # Build for production
15
+ yarn build:ios
16
+ yarn build:android
17
+ ```
18
+
19
+ ## Project Structure
20
+
21
+ ```
22
+ {{PROJECT_NAME_KEBAB}}/
23
+ ├── src/ # Your game source code
24
+ │ ├── App.tsx # Main entry component
25
+ │ ├── tokens.ts # Design system tokens
26
+ │ ├── types.ts # TypeScript types
27
+ │ └── components/ # Reusable UI components
28
+ │ ├── Button.tsx
29
+ │ ├── Card.tsx
30
+ │ ├── TopBar.tsx
31
+ │ └── Spinner.tsx
32
+
33
+ ├── dev/ # Standalone development
34
+ │ ├── index.js # Dev entry point
35
+ │ ├── DevApp.tsx # Wrapper with mock host
36
+ │ └── MockHost.ts # Mock native capabilities
37
+
38
+ ├── index.js # Production entry (Module Federation)
39
+ ├── rspack.config.mjs # Re.Pack bundler config
40
+ ├── app.json # Expo config (for dev mode)
41
+ └── package.json
42
+ ```
43
+
44
+ ## Development
45
+
46
+ ### Running Standalone
47
+
48
+ The `yarn dev` command runs your mini-game as a standalone Expo app with a mocked host bridge. This lets you:
49
+
50
+ - Develop without the main Pika app
51
+ - Test UI and game logic independently
52
+ - See console logs for mock API calls
53
+
54
+ ### Mock Host
55
+
56
+ Edit `dev/MockHost.ts` to customize mock behavior:
57
+
58
+ ```typescript
59
+ // Return specific scores for testing
60
+ stopAndCheck: async (word) => ({
61
+ totalScore: 95, // Always high score
62
+ words: [{ word, score: 95, letters: [...] }]
63
+ })
64
+
65
+ // Simulate errors
66
+ startRecording: async () => {
67
+ throw new Error('Microphone permission denied');
68
+ }
69
+
70
+ // Add delays for loading state testing
71
+ await delay(3000);
72
+ ```
73
+
74
+ ## Design System
75
+
76
+ ### Tokens
77
+
78
+ Use tokens from `src/tokens.ts` for consistent styling:
79
+
80
+ ```typescript
81
+ import { color, semantic, space, radius, typography } from './tokens';
82
+
83
+ const styles = StyleSheet.create({
84
+ container: {
85
+ backgroundColor: semantic.bg, // Dark background
86
+ padding: space[4], // 16px
87
+ borderRadius: radius.lg, // 16px
88
+ },
89
+ title: {
90
+ color: semantic.text, // White
91
+ fontSize: typography.title1.size, // 28
92
+ fontWeight: typography.title1.fontWeight,
93
+ },
94
+ });
95
+ ```
96
+
97
+ ### Components
98
+
99
+ Pre-built components in `src/components/`:
100
+
101
+ ```typescript
102
+ import { Button, Card, TopBar, Spinner } from './components';
103
+
104
+ // Button variants: primary, secondary, danger, ghost
105
+ <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
113
+ <TopBar left="back" onLeftPress={goBack} title="Game" />
114
+
115
+ // Loading spinner
116
+ <Spinner size="lg" color={color.cyan[400]} />
117
+ ```
118
+
119
+ ## Host Bridge
120
+
121
+ The `host` prop provides native capabilities from the main app:
122
+
123
+ ```typescript
124
+ interface HostBridge {
125
+ pronunciation: {
126
+ startRecording: () => Promise<void>;
127
+ stopAndCheck: (word: string) => Promise<PronunciationResult>;
128
+ cancel: () => Promise<void>;
129
+ };
130
+ }
131
+ ```
132
+
133
+ Usage in your game:
134
+
135
+ ```typescript
136
+ const App: React.FC<GameProps> = ({ host }) => {
137
+ const startRecording = async () => {
138
+ await host?.pronunciation.startRecording();
139
+ };
140
+
141
+ const checkPronunciation = async (word: string) => {
142
+ const result = await host?.pronunciation.stopAndCheck(word);
143
+ console.log('Score:', result.totalScore);
144
+ };
145
+ };
146
+ ```
147
+
148
+ ## Building for Production
149
+
150
+ ### 1. Build the bundle
151
+
152
+ ```bash
153
+ # iOS
154
+ yarn build:ios
155
+
156
+ # Android
157
+ yarn build:android
158
+ ```
159
+
160
+ ### 2. Serve locally for testing
161
+
162
+ ```bash
163
+ yarn serve:ios # Serves on http://localhost:9000
164
+ ```
165
+
166
+ ### 3. Deploy to CDN
167
+
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
+ }
176
+ ```
177
+
178
+ ## Integration with Pika App
179
+
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
193
+
194
+ ## License
195
+
196
+ MIT © {{YEAR}} Pika Team
@@ -0,0 +1,29 @@
1
+ {
2
+ "expo": {
3
+ "name": "{{PROJECT_NAME_PASCAL}}",
4
+ "slug": "{{PROJECT_NAME_KEBAB}}",
5
+ "version": "1.0.0",
6
+ "orientation": "portrait",
7
+ "icon": "./assets/icon.png",
8
+ "userInterfaceStyle": "dark",
9
+ "splash": {
10
+ "image": "./assets/splash.png",
11
+ "resizeMode": "contain",
12
+ "backgroundColor": "#0B0B0B"
13
+ },
14
+ "ios": {
15
+ "supportsTablet": true,
16
+ "bundleIdentifier": "com.pika.minigame.{{PROJECT_NAME_KEBAB}}"
17
+ },
18
+ "android": {
19
+ "adaptiveIcon": {
20
+ "foregroundImage": "./assets/adaptive-icon.png",
21
+ "backgroundColor": "#0B0B0B"
22
+ },
23
+ "package": "com.pika.minigame.{{PROJECT_NAME_SNAKE}}"
24
+ },
25
+ "plugins": [
26
+ "expo-router"
27
+ ]
28
+ }
29
+ }
@@ -0,0 +1,7 @@
1
+ module.exports = function (api) {
2
+ api.cache(true);
3
+ return {
4
+ presets: ['babel-preset-expo'],
5
+ plugins: ['react-native-reanimated/plugin'],
6
+ };
7
+ };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Development wrapper for standalone mini-game testing.
3
+ *
4
+ * This wraps your App component with a mock host bridge
5
+ * so you can develop and test without the main Pika app.
6
+ *
7
+ * Run with: yarn dev
8
+ */
9
+
10
+ import React from 'react';
11
+ import { Alert } from 'react-native';
12
+ import { GestureHandlerRootView } from 'react-native-gesture-handler';
13
+ import { SafeAreaProvider } from 'react-native-safe-area-context';
14
+
15
+ import App from '../src/App';
16
+ import mockHost from './MockHost';
17
+
18
+ export default function DevApp() {
19
+ const handleExit = () => {
20
+ Alert.alert(
21
+ 'Exit Pressed',
22
+ 'In the real app, this would close the mini-game and return to the main app.',
23
+ [{ text: 'OK' }]
24
+ );
25
+ };
26
+
27
+ const handleGameOver = (result: { score: number }) => {
28
+ console.log('[DevApp] Game over with score:', result.score);
29
+ Alert.alert(
30
+ 'Game Over!',
31
+ `Your score: ${result.score}\n\nIn the real app, this score would be saved.`,
32
+ [{ text: 'OK' }]
33
+ );
34
+ };
35
+
36
+ return (
37
+ <GestureHandlerRootView style={{ flex: 1 }}>
38
+ <SafeAreaProvider>
39
+ <App
40
+ host={mockHost}
41
+ onExit={handleExit}
42
+ onGameOver={handleGameOver}
43
+ />
44
+ </SafeAreaProvider>
45
+ </GestureHandlerRootView>
46
+ );
47
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Mock Host Bridge for standalone development.
3
+ *
4
+ * This simulates the host app's native capabilities so you can
5
+ * develop and test your mini-game without the main app.
6
+ */
7
+
8
+ import { HostBridge, PronunciationResult } from '../src/types';
9
+
10
+ // Simulate recording state
11
+ let isRecording = false;
12
+
13
+ /**
14
+ * Generate a random score for testing different UI states.
15
+ */
16
+ const randomScore = (min = 40, max = 100): number =>
17
+ Math.floor(Math.random() * (max - min + 1)) + min;
18
+
19
+ /**
20
+ * Generate letter-by-letter scores for a word.
21
+ */
22
+ const generateLetterScores = (word: string) =>
23
+ [...word].map((letter) => ({
24
+ letter,
25
+ score: randomScore(50, 100),
26
+ }));
27
+
28
+ /**
29
+ * Simulate async delay (like network/native calls).
30
+ */
31
+ const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
32
+
33
+ /**
34
+ * Mock implementation of the host bridge.
35
+ * Edit these to test different scenarios.
36
+ */
37
+ export const mockHost: HostBridge = {
38
+ pronunciation: {
39
+ startRecording: async () => {
40
+ if (isRecording) {
41
+ console.warn('[MockHost] Already recording');
42
+ return;
43
+ }
44
+ console.log('[MockHost] 🎤 Recording started...');
45
+ isRecording = true;
46
+ // Simulate recording initialization delay
47
+ await delay(100);
48
+ },
49
+
50
+ stopAndCheck: async (targetWord: string): Promise<PronunciationResult> => {
51
+ if (!isRecording) {
52
+ console.warn('[MockHost] Not recording');
53
+ throw new Error('Not recording');
54
+ }
55
+
56
+ console.log(`[MockHost] 🛑 Recording stopped. Checking: "${targetWord}"`);
57
+ isRecording = false;
58
+
59
+ // Simulate processing delay (like API call)
60
+ await delay(800 + Math.random() * 500);
61
+
62
+ // Generate mock result
63
+ const wordScore = randomScore(50, 100);
64
+ const letters = generateLetterScores(targetWord);
65
+ const totalScore = Math.round(
66
+ letters.reduce((sum, l) => sum + l.score, 0) / letters.length
67
+ );
68
+
69
+ console.log(`[MockHost] ✅ Score: ${totalScore}`);
70
+
71
+ return {
72
+ totalScore,
73
+ words: [
74
+ {
75
+ word: targetWord,
76
+ score: wordScore,
77
+ letters,
78
+ },
79
+ ],
80
+ };
81
+ },
82
+
83
+ cancel: async () => {
84
+ console.log('[MockHost] ❌ Recording cancelled');
85
+ isRecording = false;
86
+ },
87
+ },
88
+
89
+ // Add more mock capabilities here as needed:
90
+ // audio: {
91
+ // play: async (url) => console.log(`[MockHost] Playing: ${url}`),
92
+ // stop: () => console.log('[MockHost] Audio stopped'),
93
+ // },
94
+ // haptics: {
95
+ // impact: (style) => console.log(`[MockHost] Haptic: ${style}`),
96
+ // },
97
+ };
98
+
99
+ /**
100
+ * Mock host that always fails - useful for testing error states.
101
+ */
102
+ export const mockHostWithErrors: HostBridge = {
103
+ pronunciation: {
104
+ startRecording: async () => {
105
+ throw new Error('Microphone permission denied');
106
+ },
107
+ stopAndCheck: async () => {
108
+ throw new Error('Speech recognition failed');
109
+ },
110
+ cancel: async () => {},
111
+ },
112
+ };
113
+
114
+ /**
115
+ * Mock host with slow responses - useful for testing loading states.
116
+ */
117
+ export const mockHostSlow: HostBridge = {
118
+ pronunciation: {
119
+ startRecording: async () => {
120
+ await delay(2000);
121
+ console.log('[MockHost Slow] Recording started after delay');
122
+ },
123
+ stopAndCheck: async (word) => {
124
+ await delay(3000);
125
+ return mockHost.pronunciation.stopAndCheck(word);
126
+ },
127
+ cancel: async () => {},
128
+ },
129
+ };
130
+
131
+ export default mockHost;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Development entry point.
3
+ * This is used by Expo to run the mini-game standalone.
4
+ */
5
+
6
+ import { registerRootComponent } from 'expo';
7
+ import DevApp from './DevApp';
8
+
9
+ registerRootComponent(DevApp);
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Production entry point for Module Federation.
3
+ * This is bundled by Re.Pack and loaded by the host app.
4
+ */
5
+
6
+ import App from './src/App';
7
+
8
+ export default App;