create-pika-minigame 1.2.0 → 1.4.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/package.json +1 -1
- package/templates/README.md +38 -2
- package/templates/dev/MockHost.ts +49 -21
- package/templates/package.json.template +12 -17
- package/templates/scripts/deploy-vercel.sh +202 -0
package/package.json
CHANGED
package/templates/README.md
CHANGED
|
@@ -161,10 +161,46 @@ pod install
|
|
|
161
161
|
cd ..
|
|
162
162
|
```
|
|
163
163
|
|
|
164
|
+
## Deployment
|
|
165
|
+
|
|
166
|
+
### Deploy to Vercel
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
# Install Vercel CLI (first time only)
|
|
170
|
+
npm i -g vercel
|
|
171
|
+
vercel login
|
|
172
|
+
|
|
173
|
+
# Deploy preview
|
|
174
|
+
npm run deploy
|
|
175
|
+
|
|
176
|
+
# Deploy production
|
|
177
|
+
npm run deploy:prod
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
After deployment, you'll get URLs like:
|
|
181
|
+
```
|
|
182
|
+
https://your-game.vercel.app/ios/mf-manifest.json
|
|
183
|
+
https://your-game.vercel.app/android/mf-manifest.json
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Register with Backend
|
|
187
|
+
|
|
188
|
+
```json
|
|
189
|
+
{
|
|
190
|
+
"id": "{{PROJECT_NAME_KEBAB}}",
|
|
191
|
+
"name": "{{PROJECT_NAME_PASCAL}}",
|
|
192
|
+
"scope": "{{PROJECT_NAME_SNAKE}}",
|
|
193
|
+
"module": "./App",
|
|
194
|
+
"container_url": "https://your-game.vercel.app/ios/mf-manifest.json",
|
|
195
|
+
"version": "1.0.0",
|
|
196
|
+
"min_host_version": "1.0.0"
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
164
200
|
## Integration with Pika App
|
|
165
201
|
|
|
166
|
-
Once
|
|
202
|
+
Once deployed and registered, the game will appear in the Pika app's Games hub and can be loaded via Module Federation.
|
|
167
203
|
|
|
168
204
|
---
|
|
169
205
|
|
|
170
|
-
Created with `create-pika-minigame`
|
|
206
|
+
Created with `create-pika-minigame`
|
|
@@ -9,6 +9,19 @@ import { HostBridge, PronunciationResult } from '../src/types';
|
|
|
9
9
|
|
|
10
10
|
// Simulate recording state
|
|
11
11
|
let isRecording = false;
|
|
12
|
+
let recordingStartTime = 0;
|
|
13
|
+
|
|
14
|
+
// Configure mock behavior (edit these for testing)
|
|
15
|
+
const MOCK_CONFIG = {
|
|
16
|
+
// Set to a number (0-100) to always return this score, or null for random
|
|
17
|
+
fixedScore: null as number | null,
|
|
18
|
+
// Minimum recording time in ms (like real API)
|
|
19
|
+
minRecordingMs: 700,
|
|
20
|
+
// Simulate network delay range [min, max] ms
|
|
21
|
+
networkDelay: [800, 1300] as [number, number],
|
|
22
|
+
// Chance to throw error (0-1), set > 0 to test error handling
|
|
23
|
+
errorChance: 0,
|
|
24
|
+
};
|
|
12
25
|
|
|
13
26
|
/**
|
|
14
27
|
* Generate a random score for testing different UI states.
|
|
@@ -43,41 +56,56 @@ export const mockHost: HostBridge = {
|
|
|
43
56
|
}
|
|
44
57
|
console.log('[MockHost] 🎤 Recording started...');
|
|
45
58
|
isRecording = true;
|
|
59
|
+
recordingStartTime = Date.now();
|
|
46
60
|
// Simulate recording initialization delay
|
|
47
61
|
await delay(100);
|
|
48
62
|
},
|
|
49
63
|
|
|
50
|
-
stopAndCheck: async (
|
|
64
|
+
stopAndCheck: async (targetText: string): Promise<PronunciationResult> => {
|
|
51
65
|
if (!isRecording) {
|
|
52
66
|
console.warn('[MockHost] Not recording');
|
|
53
67
|
throw new Error('Not recording');
|
|
54
68
|
}
|
|
55
69
|
|
|
56
|
-
|
|
70
|
+
const elapsed = Date.now() - recordingStartTime;
|
|
71
|
+
console.log(`[MockHost] 🛑 Recording stopped (${elapsed}ms). Checking: "${targetText}"`);
|
|
57
72
|
isRecording = false;
|
|
58
73
|
|
|
59
|
-
//
|
|
60
|
-
|
|
74
|
+
// Check minimum recording time (like real API)
|
|
75
|
+
if (elapsed < MOCK_CONFIG.minRecordingMs) {
|
|
76
|
+
throw new Error('Giữ nút lâu hơn (1–2 giây) rồi nói to, rõ nhé.');
|
|
77
|
+
}
|
|
61
78
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
79
|
+
// Simulate random errors for testing
|
|
80
|
+
if (MOCK_CONFIG.errorChance > 0 && Math.random() < MOCK_CONFIG.errorChance) {
|
|
81
|
+
throw new Error('Chưa nghe rõ, thử lại nhé');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Simulate network delay
|
|
85
|
+
const [minDelay, maxDelay] = MOCK_CONFIG.networkDelay;
|
|
86
|
+
await delay(minDelay + Math.random() * (maxDelay - minDelay));
|
|
87
|
+
|
|
88
|
+
// Split into words (real API does this)
|
|
89
|
+
const wordList = targetText.trim().split(/\s+/);
|
|
90
|
+
|
|
91
|
+
// Generate scores for each word
|
|
92
|
+
const words = wordList.map((word) => {
|
|
93
|
+
const letters = generateLetterScores(word);
|
|
94
|
+
// Use fixed score if configured, otherwise use average of letters
|
|
95
|
+
const wordScore = MOCK_CONFIG.fixedScore ?? Math.round(
|
|
96
|
+
letters.reduce((sum, l) => sum + l.score, 0) / letters.length
|
|
97
|
+
);
|
|
98
|
+
return { word, score: wordScore, letters };
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Total score is average of all words
|
|
102
|
+
const totalScore = MOCK_CONFIG.fixedScore ?? Math.round(
|
|
103
|
+
words.reduce((sum, w) => sum + w.score, 0) / words.length
|
|
67
104
|
);
|
|
68
105
|
|
|
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
|
-
};
|
|
106
|
+
console.log(`[MockHost] ✅ Score: ${totalScore} (${words.length} word${words.length > 1 ? 's' : ''})`);
|
|
107
|
+
|
|
108
|
+
return { totalScore, words };
|
|
81
109
|
},
|
|
82
110
|
|
|
83
111
|
cancel: async () => {
|
|
@@ -3,22 +3,22 @@
|
|
|
3
3
|
"version": "1.0.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Pika mini-game: {{PROJECT_NAME_PASCAL}}",
|
|
6
|
-
"main": "
|
|
6
|
+
"main": "index.js",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"build:ios": "npx react-native webpack-bundle --platform ios --entry-file index.js --dev false --bundle-output build/outputs/ios/
|
|
12
|
-
"build:android": "npx react-native webpack-bundle --platform android --entry-file index.js --dev false --bundle-output build/outputs/android/
|
|
8
|
+
"start": "react-native start",
|
|
9
|
+
"ios": "react-native run-ios",
|
|
10
|
+
"android": "react-native run-android",
|
|
11
|
+
"build:ios": "npx react-native webpack-bundle --platform ios --entry-file index.js --dev false --bundle-output build/outputs/ios/remotes/{{PROJECT_NAME_SNAKE}}.container.js.bundle",
|
|
12
|
+
"build:android": "npx react-native webpack-bundle --platform android --entry-file index.js --dev false --bundle-output build/outputs/android/remotes/{{PROJECT_NAME_SNAKE}}.container.js.bundle",
|
|
13
|
+
"build:all": "npm run build:ios && npm run build:android",
|
|
13
14
|
"serve:ios": "npx serve build/outputs/ios/remotes -l 9000 --cors",
|
|
14
15
|
"serve:android": "npx serve build/outputs/android/remotes -l 9000 --cors",
|
|
15
|
-
"deploy": "
|
|
16
|
-
"
|
|
16
|
+
"deploy": "./scripts/deploy-vercel.sh",
|
|
17
|
+
"deploy:prod": "./scripts/deploy-vercel.sh --prod",
|
|
18
|
+
"lint": "eslint src/",
|
|
17
19
|
"typecheck": "tsc --noEmit"
|
|
18
20
|
},
|
|
19
21
|
"dependencies": {
|
|
20
|
-
"expo": "~52.0.0",
|
|
21
|
-
"expo-status-bar": "~2.0.0",
|
|
22
22
|
"react": "18.3.1",
|
|
23
23
|
"react-native": "0.77.0",
|
|
24
24
|
"react-native-gesture-handler": "~2.20.0",
|
|
@@ -29,13 +29,8 @@
|
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@babel/core": "^7.25.0",
|
|
31
31
|
"@callstack/repack": "^5.0.0",
|
|
32
|
-
"@callstack/repack-plugin-reanimated": "^1.0.0",
|
|
33
32
|
"@types/react": "~18.3.0",
|
|
34
|
-
"typescript": "~5.3.0"
|
|
35
|
-
|
|
36
|
-
"peerDependencies": {
|
|
37
|
-
"react": "18.3.1",
|
|
38
|
-
"react-native": "0.77.0",
|
|
39
|
-
"react-native-reanimated": "~3.16.0"
|
|
33
|
+
"typescript": "~5.3.0",
|
|
34
|
+
"serve": "^14.2.0"
|
|
40
35
|
}
|
|
41
36
|
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# Deploy Pika Mini-Game to Vercel
|
|
6
|
+
# =============================================================================
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# ./scripts/deploy-vercel.sh [--prod]
|
|
10
|
+
#
|
|
11
|
+
# Prerequisites:
|
|
12
|
+
# - Vercel CLI: npm i -g vercel
|
|
13
|
+
# - Logged in: vercel login
|
|
14
|
+
#
|
|
15
|
+
# =============================================================================
|
|
16
|
+
|
|
17
|
+
# Colors
|
|
18
|
+
RED='\033[0;31m'
|
|
19
|
+
GREEN='\033[0;32m'
|
|
20
|
+
YELLOW='\033[1;33m'
|
|
21
|
+
CYAN='\033[0;36m'
|
|
22
|
+
NC='\033[0m' # No Color
|
|
23
|
+
|
|
24
|
+
log_info() { echo -e "${CYAN}[INFO]${NC} $1"; }
|
|
25
|
+
log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
|
26
|
+
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
|
27
|
+
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
|
28
|
+
|
|
29
|
+
# Check if vercel CLI is installed
|
|
30
|
+
if ! command -v vercel &> /dev/null; then
|
|
31
|
+
log_error "Vercel CLI not found. Install with: npm i -g vercel"
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# Parse arguments
|
|
36
|
+
PROD_FLAG=""
|
|
37
|
+
if [[ "$1" == "--prod" ]]; then
|
|
38
|
+
PROD_FLAG="--prod"
|
|
39
|
+
log_info "Deploying to PRODUCTION"
|
|
40
|
+
else
|
|
41
|
+
log_info "Deploying to PREVIEW (add --prod for production)"
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Get project info from package.json
|
|
45
|
+
PROJECT_NAME=$(node -p "require('./package.json').name")
|
|
46
|
+
PROJECT_VERSION=$(node -p "require('./package.json').version")
|
|
47
|
+
|
|
48
|
+
log_info "Project: $PROJECT_NAME v$PROJECT_VERSION"
|
|
49
|
+
|
|
50
|
+
# =============================================================================
|
|
51
|
+
# Step 1: Build for all platforms
|
|
52
|
+
# =============================================================================
|
|
53
|
+
log_info "Building mini-game bundles..."
|
|
54
|
+
|
|
55
|
+
# Create build output directory
|
|
56
|
+
BUILD_DIR="build/deploy"
|
|
57
|
+
rm -rf "$BUILD_DIR"
|
|
58
|
+
mkdir -p "$BUILD_DIR/ios" "$BUILD_DIR/android"
|
|
59
|
+
|
|
60
|
+
# We'll set MF_PUBLIC_PATH after we know the Vercel URL
|
|
61
|
+
# For now, build with a placeholder that we'll update
|
|
62
|
+
|
|
63
|
+
log_info "Building iOS bundle..."
|
|
64
|
+
npx react-native webpack-bundle \
|
|
65
|
+
--platform ios \
|
|
66
|
+
--entry-file index.js \
|
|
67
|
+
--dev false \
|
|
68
|
+
--bundle-output build/outputs/ios/remotes/${PROJECT_NAME//-/_}.container.js.bundle \
|
|
69
|
+
2>&1 | tail -5
|
|
70
|
+
|
|
71
|
+
log_info "Building Android bundle..."
|
|
72
|
+
npx react-native webpack-bundle \
|
|
73
|
+
--platform android \
|
|
74
|
+
--entry-file index.js \
|
|
75
|
+
--dev false \
|
|
76
|
+
--bundle-output build/outputs/android/remotes/${PROJECT_NAME//-/_}.container.js.bundle \
|
|
77
|
+
2>&1 | tail -5
|
|
78
|
+
|
|
79
|
+
# Copy to deploy directory with platform subdirs
|
|
80
|
+
cp -r build/outputs/ios/remotes/* "$BUILD_DIR/ios/"
|
|
81
|
+
cp -r build/outputs/android/remotes/* "$BUILD_DIR/android/"
|
|
82
|
+
|
|
83
|
+
# Create index.html for Vercel (shows deployment info)
|
|
84
|
+
cat > "$BUILD_DIR/index.html" << EOF
|
|
85
|
+
<!DOCTYPE html>
|
|
86
|
+
<html>
|
|
87
|
+
<head>
|
|
88
|
+
<title>${PROJECT_NAME} - Pika Mini-Game</title>
|
|
89
|
+
<style>
|
|
90
|
+
body { font-family: -apple-system, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
|
|
91
|
+
h1 { color: #0B0B0B; }
|
|
92
|
+
code { background: #f4f4f4; padding: 2px 8px; border-radius: 4px; }
|
|
93
|
+
.platform { margin: 20px 0; padding: 15px; background: #f9f9f9; border-radius: 8px; }
|
|
94
|
+
.platform h3 { margin-top: 0; }
|
|
95
|
+
a { color: #46C4D7; }
|
|
96
|
+
</style>
|
|
97
|
+
</head>
|
|
98
|
+
<body>
|
|
99
|
+
<h1>🎮 ${PROJECT_NAME}</h1>
|
|
100
|
+
<p>Version: <code>${PROJECT_VERSION}</code></p>
|
|
101
|
+
<p>Deployed: <code>$(date -u +"%Y-%m-%d %H:%M:%S UTC")</code></p>
|
|
102
|
+
|
|
103
|
+
<div class="platform">
|
|
104
|
+
<h3>📱 iOS</h3>
|
|
105
|
+
<p>Container: <a href="./ios/${PROJECT_NAME//-/_}.container.js.bundle">ios/${PROJECT_NAME//-/_}.container.js.bundle</a></p>
|
|
106
|
+
<p>Manifest: <a href="./ios/mf-manifest.json">ios/mf-manifest.json</a></p>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<div class="platform">
|
|
110
|
+
<h3>🤖 Android</h3>
|
|
111
|
+
<p>Container: <a href="./android/${PROJECT_NAME//-/_}.container.js.bundle">android/${PROJECT_NAME//-/_}.container.js.bundle</a></p>
|
|
112
|
+
<p>Manifest: <a href="./android/mf-manifest.json">android/mf-manifest.json</a></p>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<h2>Integration</h2>
|
|
116
|
+
<p>Register this game with your backend:</p>
|
|
117
|
+
<pre><code>{
|
|
118
|
+
"scope": "${PROJECT_NAME//-/_}",
|
|
119
|
+
"module": "./App",
|
|
120
|
+
"container_url": "[VERCEL_URL]/ios/mf-manifest.json",
|
|
121
|
+
"version": "${PROJECT_VERSION}"
|
|
122
|
+
}</code></pre>
|
|
123
|
+
</body>
|
|
124
|
+
</html>
|
|
125
|
+
EOF
|
|
126
|
+
|
|
127
|
+
# Create vercel.json for proper routing
|
|
128
|
+
cat > "$BUILD_DIR/vercel.json" << EOF
|
|
129
|
+
{
|
|
130
|
+
"headers": [
|
|
131
|
+
{
|
|
132
|
+
"source": "/(.*)",
|
|
133
|
+
"headers": [
|
|
134
|
+
{ "key": "Access-Control-Allow-Origin", "value": "*" },
|
|
135
|
+
{ "key": "Access-Control-Allow-Methods", "value": "GET, OPTIONS" },
|
|
136
|
+
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
EOF
|
|
142
|
+
|
|
143
|
+
log_success "Build complete: $BUILD_DIR"
|
|
144
|
+
|
|
145
|
+
# =============================================================================
|
|
146
|
+
# Step 2: Deploy to Vercel
|
|
147
|
+
# =============================================================================
|
|
148
|
+
log_info "Deploying to Vercel..."
|
|
149
|
+
|
|
150
|
+
# Deploy and capture the URL
|
|
151
|
+
DEPLOY_OUTPUT=$(vercel "$BUILD_DIR" $PROD_FLAG --yes 2>&1)
|
|
152
|
+
DEPLOY_URL=$(echo "$DEPLOY_OUTPUT" | grep -E "https://" | tail -1 | tr -d ' ')
|
|
153
|
+
|
|
154
|
+
if [[ -z "$DEPLOY_URL" ]]; then
|
|
155
|
+
log_error "Failed to get deployment URL"
|
|
156
|
+
echo "$DEPLOY_OUTPUT"
|
|
157
|
+
exit 1
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
log_success "Deployed to: $DEPLOY_URL"
|
|
161
|
+
|
|
162
|
+
# =============================================================================
|
|
163
|
+
# Step 3: Show integration info
|
|
164
|
+
# =============================================================================
|
|
165
|
+
echo ""
|
|
166
|
+
echo "=============================================="
|
|
167
|
+
echo -e "${GREEN}🎉 Deployment Complete!${NC}"
|
|
168
|
+
echo "=============================================="
|
|
169
|
+
echo ""
|
|
170
|
+
echo -e "URL: ${CYAN}$DEPLOY_URL${NC}"
|
|
171
|
+
echo ""
|
|
172
|
+
echo "iOS manifest: $DEPLOY_URL/ios/mf-manifest.json"
|
|
173
|
+
echo "Android manifest: $DEPLOY_URL/android/mf-manifest.json"
|
|
174
|
+
echo ""
|
|
175
|
+
echo "Register with backend API:"
|
|
176
|
+
echo ""
|
|
177
|
+
cat << EOF
|
|
178
|
+
{
|
|
179
|
+
"id": "${PROJECT_NAME}",
|
|
180
|
+
"name": "${PROJECT_NAME}",
|
|
181
|
+
"scope": "${PROJECT_NAME//-/_}",
|
|
182
|
+
"module": "./App",
|
|
183
|
+
"container_url": "$DEPLOY_URL/ios/mf-manifest.json",
|
|
184
|
+
"version": "${PROJECT_VERSION}",
|
|
185
|
+
"min_host_version": "1.0.0"
|
|
186
|
+
}
|
|
187
|
+
EOF
|
|
188
|
+
echo ""
|
|
189
|
+
|
|
190
|
+
# Save deployment info
|
|
191
|
+
cat > "deploy-info.json" << EOF
|
|
192
|
+
{
|
|
193
|
+
"url": "$DEPLOY_URL",
|
|
194
|
+
"project": "$PROJECT_NAME",
|
|
195
|
+
"version": "$PROJECT_VERSION",
|
|
196
|
+
"deployed_at": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
|
|
197
|
+
"ios_manifest": "$DEPLOY_URL/ios/mf-manifest.json",
|
|
198
|
+
"android_manifest": "$DEPLOY_URL/android/mf-manifest.json"
|
|
199
|
+
}
|
|
200
|
+
EOF
|
|
201
|
+
|
|
202
|
+
log_success "Deployment info saved to deploy-info.json"
|