create-river-miniapp 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/README.md +38 -0
- package/bin/create-river-miniapp.js +9 -0
- package/lib/create.js +259 -0
- package/package.json +30 -0
- package/templates/html/README.md +21 -0
- package/templates/html/_gitignore +4 -0
- package/templates/html/index.html +13 -0
- package/templates/html/miniapp.config.json +15 -0
- package/templates/html/package.json +18 -0
- package/templates/html/public/icon.svg +11 -0
- package/templates/html/scripts/check-miniapp.mjs +28 -0
- package/templates/html/scripts/package-miniapp.mjs +78 -0
- package/templates/html/src/main.js +44 -0
- package/templates/html/src/style.css +74 -0
- package/templates/html/vite.config.js +5 -0
- package/templates/react/README.md +21 -0
- package/templates/react/_gitignore +4 -0
- package/templates/react/index.html +13 -0
- package/templates/react/miniapp.config.json +15 -0
- package/templates/react/package.json +23 -0
- package/templates/react/public/icon.svg +13 -0
- package/templates/react/scripts/check-miniapp.mjs +28 -0
- package/templates/react/scripts/package-miniapp.mjs +78 -0
- package/templates/react/src/App.jsx +48 -0
- package/templates/react/src/main.jsx +10 -0
- package/templates/react/src/style.css +74 -0
- package/templates/react/vite.config.js +7 -0
- package/templates/vue/README.md +21 -0
- package/templates/vue/_gitignore +4 -0
- package/templates/vue/index.html +13 -0
- package/templates/vue/miniapp.config.json +15 -0
- package/templates/vue/package.json +22 -0
- package/templates/vue/public/icon.svg +11 -0
- package/templates/vue/scripts/check-miniapp.mjs +28 -0
- package/templates/vue/scripts/package-miniapp.mjs +78 -0
- package/templates/vue/src/App.vue +39 -0
- package/templates/vue/src/main.js +5 -0
- package/templates/vue/src/style.css +74 -0
- package/templates/vue/vite.config.js +7 -0
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# create-river-miniapp
|
|
2
|
+
|
|
3
|
+
River 小程序脚手架,支持 `html` / `vue` / `react` 三种模板,并内置构建与打包脚本。
|
|
4
|
+
|
|
5
|
+
## 使用
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx create-river-miniapp my-miniapp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
或指定模板:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx create-river-miniapp my-miniapp --template vue
|
|
15
|
+
npx create-river-miniapp my-miniapp --template react
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
无交互模式:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx create-river-miniapp my-miniapp --template html --id local.my_miniapp --name "我的小程序" --yes
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 生成项目常用命令
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install
|
|
28
|
+
npm run dev
|
|
29
|
+
npm run build
|
|
30
|
+
npm run package:miniapp
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`npm run package:miniapp` 会输出:
|
|
34
|
+
|
|
35
|
+
- `miniapp_output/packages/<app_id>.zip`
|
|
36
|
+
- `miniapp_output/miniapps.partial.json`
|
|
37
|
+
|
|
38
|
+
可将 `miniapps.partial.json` 的条目合并到你的 `miniapps.json` 中。
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { run } = require('../lib/create');
|
|
4
|
+
|
|
5
|
+
run(process.argv.slice(2)).catch((error) => {
|
|
6
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7
|
+
console.error(`\n[create-river-miniapp] ${message}`);
|
|
8
|
+
process.exit(1);
|
|
9
|
+
});
|
package/lib/create.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const readline = require('node:readline/promises');
|
|
4
|
+
const { stdin, stdout } = require('node:process');
|
|
5
|
+
|
|
6
|
+
const TEMPLATE_NAMES = ['html', 'vue', 'react'];
|
|
7
|
+
const TEXT_EXTENSIONS = new Set([
|
|
8
|
+
'.json',
|
|
9
|
+
'.js',
|
|
10
|
+
'.mjs',
|
|
11
|
+
'.ts',
|
|
12
|
+
'.tsx',
|
|
13
|
+
'.vue',
|
|
14
|
+
'.html',
|
|
15
|
+
'.css',
|
|
16
|
+
'.md',
|
|
17
|
+
'.txt',
|
|
18
|
+
'.yml',
|
|
19
|
+
'.yaml',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
function printHelp() {
|
|
23
|
+
console.log(`
|
|
24
|
+
create-river-miniapp
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
create-river-miniapp [target-dir] [options]
|
|
28
|
+
|
|
29
|
+
Options:
|
|
30
|
+
-t, --template <html|vue|react> Template type
|
|
31
|
+
--id <miniapp-id> Mini-app id (default: local.<project_name>)
|
|
32
|
+
--name <display-name> Mini-app display name
|
|
33
|
+
-y, --yes Skip all prompts
|
|
34
|
+
-h, --help Show help
|
|
35
|
+
`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseArgs(args) {
|
|
39
|
+
const parsed = {
|
|
40
|
+
targetDir: '',
|
|
41
|
+
template: '',
|
|
42
|
+
appId: '',
|
|
43
|
+
appName: '',
|
|
44
|
+
yes: false,
|
|
45
|
+
help: false,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
49
|
+
const arg = args[i];
|
|
50
|
+
if (arg === '-h' || arg === '--help') {
|
|
51
|
+
parsed.help = true;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (arg === '-y' || arg === '--yes') {
|
|
55
|
+
parsed.yes = true;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (arg === '-t' || arg === '--template') {
|
|
59
|
+
parsed.template = (args[i + 1] || '').trim();
|
|
60
|
+
i += 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (arg === '--id') {
|
|
64
|
+
parsed.appId = (args[i + 1] || '').trim();
|
|
65
|
+
i += 1;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (arg === '--name') {
|
|
69
|
+
parsed.appName = (args[i + 1] || '').trim();
|
|
70
|
+
i += 1;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (!arg.startsWith('-') && !parsed.targetDir) {
|
|
74
|
+
parsed.targetDir = arg.trim();
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
78
|
+
}
|
|
79
|
+
return parsed;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function sanitizeName(value) {
|
|
83
|
+
return value
|
|
84
|
+
.trim()
|
|
85
|
+
.replace(/[^\w.-]+/g, '-')
|
|
86
|
+
.replace(/^-+|-+$/g, '')
|
|
87
|
+
.toLowerCase();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function defaultAppId(projectName) {
|
|
91
|
+
const safe = projectName.replace(/[^\w.-]+/g, '_');
|
|
92
|
+
return `local.${safe || 'miniapp'}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function askQuestion(rl, question, fallback = '') {
|
|
96
|
+
const answer = await rl.question(question);
|
|
97
|
+
const text = answer.trim();
|
|
98
|
+
if (text) {
|
|
99
|
+
return text;
|
|
100
|
+
}
|
|
101
|
+
return fallback;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function askTemplate(rl, fallback) {
|
|
105
|
+
console.log('\n请选择模板:');
|
|
106
|
+
TEMPLATE_NAMES.forEach((name, index) => {
|
|
107
|
+
console.log(` ${index + 1}. ${name}`);
|
|
108
|
+
});
|
|
109
|
+
const raw = await askQuestion(rl, `模板 [${fallback}]: `, fallback);
|
|
110
|
+
const lowered = raw.toLowerCase();
|
|
111
|
+
if (TEMPLATE_NAMES.includes(lowered)) {
|
|
112
|
+
return lowered;
|
|
113
|
+
}
|
|
114
|
+
const index = Number.parseInt(raw, 10);
|
|
115
|
+
if (!Number.isNaN(index) && index >= 1 && index <= TEMPLATE_NAMES.length) {
|
|
116
|
+
return TEMPLATE_NAMES[index - 1];
|
|
117
|
+
}
|
|
118
|
+
throw new Error(`Unsupported template: ${raw}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function ensureDirEmpty(targetDir) {
|
|
122
|
+
if (!fs.existsSync(targetDir)) {
|
|
123
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const files = fs.readdirSync(targetDir);
|
|
127
|
+
if (files.length > 0) {
|
|
128
|
+
throw new Error(`Target directory is not empty: ${targetDir}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function copyDir(src, dst) {
|
|
133
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
134
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
135
|
+
for (const entry of entries) {
|
|
136
|
+
const srcPath = path.join(src, entry.name);
|
|
137
|
+
const outName = entry.name === '_gitignore' ? '.gitignore' : entry.name;
|
|
138
|
+
const dstPath = path.join(dst, outName);
|
|
139
|
+
if (entry.isDirectory()) {
|
|
140
|
+
copyDir(srcPath, dstPath);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
fs.copyFileSync(srcPath, dstPath);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function applyTemplateVariables(targetDir, variables) {
|
|
148
|
+
const walk = (dir) => {
|
|
149
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
150
|
+
for (const entry of entries) {
|
|
151
|
+
const filePath = path.join(dir, entry.name);
|
|
152
|
+
if (entry.isDirectory()) {
|
|
153
|
+
walk(filePath);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
157
|
+
if (!TEXT_EXTENSIONS.has(ext)) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
let text = fs.readFileSync(filePath, 'utf-8');
|
|
161
|
+
Object.entries(variables).forEach(([key, value]) => {
|
|
162
|
+
text = text.replaceAll(`{{${key}}}`, value);
|
|
163
|
+
});
|
|
164
|
+
fs.writeFileSync(filePath, text, 'utf-8');
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
walk(targetDir);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function run(rawArgs) {
|
|
171
|
+
const args = parseArgs(rawArgs);
|
|
172
|
+
if (args.help) {
|
|
173
|
+
printHelp();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const cwd = process.cwd();
|
|
178
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
179
|
+
try {
|
|
180
|
+
let targetName = args.targetDir.trim();
|
|
181
|
+
if (!targetName && args.yes) {
|
|
182
|
+
throw new Error('Target directory is required in --yes mode.');
|
|
183
|
+
}
|
|
184
|
+
if (!targetName) {
|
|
185
|
+
targetName = await askQuestion(rl, '项目目录名: ');
|
|
186
|
+
}
|
|
187
|
+
if (!targetName) {
|
|
188
|
+
throw new Error('Project directory name is required.');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const normalizedTarget = targetName.replace(/[\\/]+/g, path.sep);
|
|
192
|
+
const projectDir = path.resolve(cwd, normalizedTarget);
|
|
193
|
+
const projectName = sanitizeName(path.basename(projectDir));
|
|
194
|
+
if (!projectName) {
|
|
195
|
+
throw new Error(`Invalid project name: ${targetName}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
let template = args.template.trim().toLowerCase();
|
|
199
|
+
if (template && !TEMPLATE_NAMES.includes(template)) {
|
|
200
|
+
throw new Error(`Unsupported template: ${template}`);
|
|
201
|
+
}
|
|
202
|
+
if (!template && args.yes) {
|
|
203
|
+
template = 'html';
|
|
204
|
+
}
|
|
205
|
+
if (!template) {
|
|
206
|
+
template = await askTemplate(rl, 'html');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let appId = args.appId.trim();
|
|
210
|
+
if (!appId && args.yes) {
|
|
211
|
+
appId = defaultAppId(projectName);
|
|
212
|
+
}
|
|
213
|
+
if (!appId) {
|
|
214
|
+
appId = await askQuestion(rl, `小程序 ID [${defaultAppId(projectName)}]: `, defaultAppId(projectName));
|
|
215
|
+
}
|
|
216
|
+
if (!appId) {
|
|
217
|
+
throw new Error('Mini-app id is required.');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let appName = args.appName.trim();
|
|
221
|
+
const defaultName = projectName;
|
|
222
|
+
if (!appName && args.yes) {
|
|
223
|
+
appName = defaultName;
|
|
224
|
+
}
|
|
225
|
+
if (!appName) {
|
|
226
|
+
appName = await askQuestion(rl, `小程序名称 [${defaultName}]: `, defaultName);
|
|
227
|
+
}
|
|
228
|
+
if (!appName) {
|
|
229
|
+
throw new Error('Mini-app display name is required.');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
ensureDirEmpty(projectDir);
|
|
233
|
+
|
|
234
|
+
const templateDir = path.resolve(__dirname, '..', 'templates', template);
|
|
235
|
+
if (!fs.existsSync(templateDir)) {
|
|
236
|
+
throw new Error(`Template directory not found: ${templateDir}`);
|
|
237
|
+
}
|
|
238
|
+
copyDir(templateDir, projectDir);
|
|
239
|
+
|
|
240
|
+
applyTemplateVariables(projectDir, {
|
|
241
|
+
PROJECT_NAME: projectName,
|
|
242
|
+
APP_ID: appId,
|
|
243
|
+
APP_NAME: appName,
|
|
244
|
+
TEMPLATE: template,
|
|
245
|
+
YEAR: String(new Date().getFullYear()),
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
console.log(`\n项目已创建: ${projectDir}`);
|
|
249
|
+
console.log('\n下一步:');
|
|
250
|
+
console.log(` cd ${path.relative(cwd, projectDir) || '.'}`);
|
|
251
|
+
console.log(' npm install');
|
|
252
|
+
console.log(' npm run dev');
|
|
253
|
+
console.log(' npm run package:miniapp');
|
|
254
|
+
} finally {
|
|
255
|
+
rl.close();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = { run };
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-river-miniapp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffold River mini-app projects with html/vue/react templates",
|
|
5
|
+
"bin": {
|
|
6
|
+
"create-river-miniapp": "./bin/create-river-miniapp.js"
|
|
7
|
+
},
|
|
8
|
+
"type": "commonjs",
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"lib",
|
|
15
|
+
"templates",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"start": "node ./bin/create-river-miniapp.js --help"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"miniapp",
|
|
23
|
+
"river",
|
|
24
|
+
"scaffold",
|
|
25
|
+
"vue",
|
|
26
|
+
"react",
|
|
27
|
+
"html"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT"
|
|
30
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# {{APP_NAME}}
|
|
2
|
+
|
|
3
|
+
由 `create-river-miniapp` 生成(HTML 模板)。
|
|
4
|
+
|
|
5
|
+
## 开发
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## 打包
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm run package:miniapp
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
产物:
|
|
19
|
+
|
|
20
|
+
- `miniapp_output/packages/{{APP_ID}}.zip`
|
|
21
|
+
- `miniapp_output/miniapps.partial.json`
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<link rel="icon" type="image/svg+xml" href="./icon.svg" />
|
|
7
|
+
<title>{{APP_NAME}}</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="app"></div>
|
|
11
|
+
<script type="module" src="./src/main.js"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "{{APP_ID}}",
|
|
3
|
+
"name": "{{APP_NAME}}",
|
|
4
|
+
"slug": "{{PROJECT_NAME}}",
|
|
5
|
+
"description": "Generated by create-river-miniapp (html template)",
|
|
6
|
+
"entry": "index.html",
|
|
7
|
+
"icon": "icon.svg",
|
|
8
|
+
"requires_auth": false,
|
|
9
|
+
"enabled": true,
|
|
10
|
+
"bridge_version": "1.0.0",
|
|
11
|
+
"tags": [
|
|
12
|
+
"html",
|
|
13
|
+
"template"
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"check": "node scripts/check-miniapp.mjs",
|
|
11
|
+
"package:miniapp": "npm run build && node scripts/package-miniapp.mjs",
|
|
12
|
+
"package:miniapp:no-build": "node scripts/package-miniapp.mjs"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"archiver": "^7.0.1",
|
|
16
|
+
"vite": "^7.3.1"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#12457A"/>
|
|
5
|
+
<stop offset="1" stop-color="#2174F1"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect width="96" height="96" rx="22" fill="url(#g)"/>
|
|
9
|
+
<path d="M23 66V30h17.8c7.7 0 12.8 4.4 12.8 11.3 0 4.3-2.1 7.4-5.8 9.2l8.6 15.5H45l-7.2-13.4h-4.4V66H23zm10.4-21.6h6.6c2.9 0 4.9-1.8 4.9-4.4 0-2.7-2-4.4-4.9-4.4h-6.6v8.8z" fill="#fff"/>
|
|
10
|
+
<circle cx="70" cy="27" r="8" fill="#fff" fill-opacity=".86"/>
|
|
11
|
+
</svg>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const root = process.cwd();
|
|
5
|
+
const configPath = path.join(root, 'miniapp.config.json');
|
|
6
|
+
const distIndex = path.join(root, 'dist', 'index.html');
|
|
7
|
+
|
|
8
|
+
if (!fs.existsSync(configPath)) {
|
|
9
|
+
console.error('Missing miniapp.config.json');
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
14
|
+
const required = ['id', 'name', 'slug', 'entry', 'icon', 'bridge_version'];
|
|
15
|
+
for (const key of required) {
|
|
16
|
+
const value = String(config[key] ?? '').trim();
|
|
17
|
+
if (!value) {
|
|
18
|
+
console.error(`miniapp.config.json missing field: ${key}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!fs.existsSync(distIndex)) {
|
|
24
|
+
console.error('Missing dist/index.html. Please run `npm run build` first.');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log('Mini-app check passed.');
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import archiver from 'archiver';
|
|
5
|
+
|
|
6
|
+
const root = process.cwd();
|
|
7
|
+
const configPath = path.join(root, 'miniapp.config.json');
|
|
8
|
+
const packageJsonPath = path.join(root, 'package.json');
|
|
9
|
+
const distDir = path.join(root, 'dist');
|
|
10
|
+
|
|
11
|
+
if (!fs.existsSync(configPath)) {
|
|
12
|
+
throw new Error('Missing miniapp.config.json');
|
|
13
|
+
}
|
|
14
|
+
if (!fs.existsSync(distDir)) {
|
|
15
|
+
throw new Error('Missing dist directory. Please run `npm run build` first.');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
19
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
20
|
+
|
|
21
|
+
const safeId = String(config.id).replace(/[^\w.-]+/g, '_');
|
|
22
|
+
const outputRoot = path.join(root, 'miniapp_output');
|
|
23
|
+
const packageDir = path.join(outputRoot, 'packages');
|
|
24
|
+
const zipName = `${safeId}.zip`;
|
|
25
|
+
const zipPath = path.join(packageDir, zipName);
|
|
26
|
+
|
|
27
|
+
fs.mkdirSync(packageDir, { recursive: true });
|
|
28
|
+
|
|
29
|
+
await new Promise((resolve, reject) => {
|
|
30
|
+
const output = fs.createWriteStream(zipPath);
|
|
31
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
32
|
+
output.on('close', resolve);
|
|
33
|
+
output.on('error', reject);
|
|
34
|
+
archive.on('error', reject);
|
|
35
|
+
archive.pipe(output);
|
|
36
|
+
archive.directory(distDir, false);
|
|
37
|
+
archive.finalize();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const sha256 = crypto
|
|
41
|
+
.createHash('sha256')
|
|
42
|
+
.update(fs.readFileSync(zipPath))
|
|
43
|
+
.digest('hex');
|
|
44
|
+
|
|
45
|
+
const slug = String(config.slug || safeId);
|
|
46
|
+
const iconPath = `./miniapps/${slug}/${config.icon}`;
|
|
47
|
+
const manifest = {
|
|
48
|
+
version: '1.0.0',
|
|
49
|
+
updated_at: new Date().toISOString(),
|
|
50
|
+
apps: [
|
|
51
|
+
{
|
|
52
|
+
id: String(config.id),
|
|
53
|
+
name: String(config.name),
|
|
54
|
+
version: String(packageJson.version || '1.0.0'),
|
|
55
|
+
url: `./miniapps/${slug}/${config.entry}`,
|
|
56
|
+
icon: iconPath,
|
|
57
|
+
package_url: `./packages/${zipName}`,
|
|
58
|
+
package_sha256: sha256,
|
|
59
|
+
description: String(config.description || ''),
|
|
60
|
+
requires_auth: Boolean(config.requires_auth),
|
|
61
|
+
enabled: Boolean(config.enabled ?? true),
|
|
62
|
+
order: Number(config.order ?? 0),
|
|
63
|
+
bridge_version: String(config.bridge_version || '1.0.0'),
|
|
64
|
+
tags: Array.isArray(config.tags) ? config.tags : [],
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
fs.mkdirSync(outputRoot, { recursive: true });
|
|
70
|
+
fs.writeFileSync(
|
|
71
|
+
path.join(outputRoot, 'miniapps.partial.json'),
|
|
72
|
+
`${JSON.stringify(manifest, null, 2)}\n`,
|
|
73
|
+
'utf-8',
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
console.log(`Packed: ${zipPath}`);
|
|
77
|
+
console.log(`SHA256: ${sha256}`);
|
|
78
|
+
console.log('Manifest snippet: miniapp_output/miniapps.partial.json');
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import './style.css';
|
|
2
|
+
|
|
3
|
+
const app = document.querySelector('#app');
|
|
4
|
+
|
|
5
|
+
const callBridge = async (action, payload = {}) => {
|
|
6
|
+
if (!window.RiverMiniApp || typeof window.RiverMiniApp.call !== 'function') {
|
|
7
|
+
return { ok: false, message: 'Bridge 未注入,当前为普通浏览器模式。' };
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
const result = await window.RiverMiniApp.call(action, payload);
|
|
11
|
+
return { ok: true, result };
|
|
12
|
+
} catch (error) {
|
|
13
|
+
return { ok: false, message: String(error) };
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
app.innerHTML = `
|
|
18
|
+
<main class="container">
|
|
19
|
+
<section class="card">
|
|
20
|
+
<h1>{{APP_NAME}}</h1>
|
|
21
|
+
<p>模板:HTML · 项目:{{PROJECT_NAME}}</p>
|
|
22
|
+
<div class="actions">
|
|
23
|
+
<button id="btn-context">获取上下文</button>
|
|
24
|
+
<button id="btn-title">修改标题</button>
|
|
25
|
+
<button id="btn-close">关闭小程序</button>
|
|
26
|
+
</div>
|
|
27
|
+
<pre id="output">等待操作...</pre>
|
|
28
|
+
</section>
|
|
29
|
+
</main>
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
const output = document.querySelector('#output');
|
|
33
|
+
document.querySelector('#btn-context').addEventListener('click', async () => {
|
|
34
|
+
const data = await callBridge('getContext');
|
|
35
|
+
output.textContent = JSON.stringify(data, null, 2);
|
|
36
|
+
});
|
|
37
|
+
document.querySelector('#btn-title').addEventListener('click', async () => {
|
|
38
|
+
const data = await callBridge('setTitle', { title: '{{APP_NAME}} 已连接' });
|
|
39
|
+
output.textContent = JSON.stringify(data, null, 2);
|
|
40
|
+
});
|
|
41
|
+
document.querySelector('#btn-close').addEventListener('click', async () => {
|
|
42
|
+
const data = await callBridge('close');
|
|
43
|
+
output.textContent = JSON.stringify(data, null, 2);
|
|
44
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light;
|
|
3
|
+
font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
* {
|
|
7
|
+
box-sizing: border-box;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
body {
|
|
11
|
+
margin: 0;
|
|
12
|
+
background: linear-gradient(160deg, #f6f8fc 0%, #eef3ff 100%);
|
|
13
|
+
color: #172033;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.container {
|
|
17
|
+
min-height: 100dvh;
|
|
18
|
+
display: grid;
|
|
19
|
+
place-items: center;
|
|
20
|
+
padding: 24px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.card {
|
|
24
|
+
width: min(680px, 100%);
|
|
25
|
+
border-radius: 20px;
|
|
26
|
+
background: #ffffffcc;
|
|
27
|
+
backdrop-filter: blur(10px);
|
|
28
|
+
border: 1px solid #dde6fa;
|
|
29
|
+
box-shadow: 0 20px 50px #12457a1a;
|
|
30
|
+
padding: 20px;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
h1 {
|
|
34
|
+
margin: 0 0 8px;
|
|
35
|
+
font-size: 28px;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
p {
|
|
39
|
+
margin: 0 0 16px;
|
|
40
|
+
color: #4b607e;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.actions {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-wrap: wrap;
|
|
46
|
+
gap: 10px;
|
|
47
|
+
margin-bottom: 16px;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
button {
|
|
51
|
+
border: 0;
|
|
52
|
+
border-radius: 12px;
|
|
53
|
+
padding: 10px 14px;
|
|
54
|
+
background: #12457a;
|
|
55
|
+
color: white;
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
font-weight: 600;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
button:hover {
|
|
61
|
+
background: #0e3863;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
pre {
|
|
65
|
+
margin: 0;
|
|
66
|
+
border-radius: 12px;
|
|
67
|
+
padding: 12px;
|
|
68
|
+
min-height: 120px;
|
|
69
|
+
overflow: auto;
|
|
70
|
+
background: #eff4ff;
|
|
71
|
+
border: 1px solid #d5e2fb;
|
|
72
|
+
font-size: 12px;
|
|
73
|
+
line-height: 1.45;
|
|
74
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# {{APP_NAME}}
|
|
2
|
+
|
|
3
|
+
由 `create-river-miniapp` 生成(React 模板)。
|
|
4
|
+
|
|
5
|
+
## 开发
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## 打包
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm run package:miniapp
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
产物:
|
|
19
|
+
|
|
20
|
+
- `miniapp_output/packages/{{APP_ID}}.zip`
|
|
21
|
+
- `miniapp_output/miniapps.partial.json`
|