| name | miniprogram-ci |
| description | Use when the user wants to automate WeChat mini-program upload or preview via CI/CD, generate deployment scripts, set up miniprogram-ci workflows, or create preview QR codes automatically. Trigger whenever the user mentions "ไธไผ ๅฐ็จๅบ", "้ข่ง", "CI ้จ็ฝฒ", "miniprogram-ci", "่ชๅจๅไธไผ ", "ๅๅธๅฐ็จๅบ็ๆฌ", "็ๆ้ข่งไบ็ปด็ ", or asks to integrate WeChat mini-program with continuous integration pipelines. |
ๅพฎไฟกๅฐ็จๅบ CI ่ชๅจๅ
ๆฆ่ฟฐ
ๆฌ skill ๅธฎๅฉ็ๆๅฏ็ดๆฅ่ฟ่ก็ Node.js ่ๆฌ๏ผ็จไบๅฎ็ฐๅฐ็จๅบไปฃ็ ็่ชๅจ้ข่งใๆๅ
ไพ่ตใไธไผ ็ญๆไฝใ่ๆฌๅบไบ็จๆท้กน็ฎ้
็ฝฎๅๆฐๅ็ๆ๏ผๆฏๆ CI/CD ๆตๆฐด็บฟ้ๆ๏ผGitHub ActionsใGitLab CI ็ญ๏ผใ
ๆ ธๅฟ่่ดฃ๏ผๆ นๆฎ็จๆท้กน็ฎไฟกๆฏ็ๆๅฏ้ๅคๆง่ก็ๅฝไปค่ก่ๆฌ๏ผ็จๆทๆง่ก็ๆ็่ๆฌๅฎๆๅฎ้
็้จ็ฝฒไปปๅกใ
Step 1๏ผๆถ้ๅฟ
่ฆไฟกๆฏ
ๆง่กๅๅ
็กฎ่ฎคไปฅไธไฟกๆฏ๏ผๅฆ็จๆทๆชๆไพๅ้้กน่ฏข้ฎ๏ผ๏ผ
1.1 ๆไฝ็ฑปๅ
่ฏข้ฎ็จๆท๏ผ "ไฝ ้่ฆๅช็ง่ฝๅ๏ผ"
| ๆไฝ | ่ฏดๆ | ้็จๅบๆฏ |
|---|
| ๆๅ
ไพ่ต๏ผpack-npm๏ผ | ๆๅปบ npm ไพ่ต่ณ miniprogram_npm ็ฎๅฝ | ้กน็ฎไฝฟ็จ npm ๆจกๅๆถ้ๅ
ๆง่ก |
| ้ข่ง๏ผpreview๏ผ | ็ๆ้ข่งไบ็ปด็ ๏ผไพๅผๅ/ๆต่ฏๆซ็ ไฝ้ช | ๅผๅ้ถๆฎตๅฟซ้้ช่ฏ |
| ไธไผ ๏ผupload๏ผ | ไธไผ ไปฃ็ ่ณๅพฎไฟกๅๅฐ็ๆฌ็ฎก็ | ๆๆตใๅๅธๆฐ็ๆฌ |
| ๅคไธช็ปๅ | ๅๆถ็ๆๅคไธช่ๆฌ | ๅฎๆด CI ๆต็จ๏ผๅ
pack-npm๏ผๅ preview/upload๏ผ |
1.2 ็ผ่ฏไบง็ฉ็ฎๅฝ
ๅฐ็จๅบ็ผ่ฏๅ็่พๅบ็ฎๅฝ่ทฏๅพ๏ผminiprogram-ci ้่ฆๆๅ่ฏฅ็ฎๅฝใ
ๅธธ่ง่ทฏๅพ๏ผ
- Taro ้กน็ฎ๏ผ
dist/
- ๅ็้กน็ฎ๏ผ้กน็ฎๆ น็ฎๅฝๆ
miniprogram/
- uni-app๏ผ
dist/build/mp-weixin/
่ฏข้ฎ็จๆท๏ผ "่ฏท็กฎ่ฎคๅฐ็จๅบ็ผ่ฏไบง็ฉ็ฎๅฝ๏ผ้ป่ฎค dist/๏ผ๏ผ"
ๆฃๆฅ็ฎๅฝๆฏๅฆๅญๅจ๏ผ
ls <็ผ่ฏไบง็ฉ็ฎๅฝ>/project.config.json 2>/dev/null || echo "็ฎๅฝไธๅญๅจๆ็ผบๅฐ project.config.json"
1.3 ๅ
็ฎก็ๅจ
่ฏข้ฎ็จๆท๏ผ่ฅๆ ๆณๅคๆญ๏ผ๏ผ "้กน็ฎไฝฟ็จๅชไธชๅ
็ฎก็ๅจ๏ผ"
ไนๅฏ้่ฟไปฅไธๆนๅผ่ชๅจๅคๆญ๏ผ
[ -f pnpm-lock.yaml ] && echo "pnpm" || \
[ -f yarn.lock ] && echo "yarn" || \
echo "npm"
1.4 ็ฐๆ่ๆฌๆฃๆฅ
ๆฃๆฅ scripts/ ็ฎๅฝไธๆฏๅฆๅทฒๅญๅจ็ธๅ
ณ่ๆฌ๏ผ
ls scripts/preview.js scripts/upload.js scripts/ci-*.js 2>/dev/null || echo "้่ฆๅๅปบ"
Step 2๏ผๅ็ฝฎๆกไปถๆฃๆฅ
2.1 ๅฎ่ฃ
miniprogram-ci
ls node_modules/miniprogram-ci 2>/dev/null && echo "ๅทฒๅฎ่ฃ
" || echo "ๆชๅฎ่ฃ
"
่ฅๆชๅฎ่ฃ
๏ผๆ นๆฎๅ
็ฎก็ๅจๅฎ่ฃ
๏ผ
pnpm add miniprogram-ci --save-dev
npm install miniprogram-ci --save-dev
yarn add miniprogram-ci --dev
2.2 ่ทๅไธไผ ๅฏ้ฅ
ๅ็ฅ็จๆท่ทๅ่ทฏๅพ๏ผ
- ็ปๅฝ ๅพฎไฟกๅ
ฌไผๅนณๅฐ
- ่ฟๅ
ฅ๏ผๅผๅ็ฎก็ โ ๅผๅ่ฎพ็ฝฎ โ ๅฐ็จๅบไปฃ็ ไธไผ
- ็นๅปใ็ๆใไธ่ฝฝๅฏ้ฅๆไปถ๏ผ
private.*.key๏ผ
ๅฎๅ
จๆ้๏ผ
- โ ๅฏ้ฅๆไปถ็ปๅฏนไธ่ฝๆไบคๅฐไปฃ็ ไปๅบ
- โ
ๅจ
.gitignore ไธญๆทปๅ *.key ๅ private.*.key
- โ
ๅจ CI/CD ไธญไฝฟ็จ secrets ็ฎก็ๅฏ้ฅๅ
ๅฎน
2.3 ้
็ฝฎ IP ็ฝๅๅ
ๅ็ฅ็จๆท๏ผ
- ๅพฎไฟกๅ
ฌไผๅนณๅฐ โ ๅผๅ่ฎพ็ฝฎ โ ๅฐ็จๅบไปฃ็ ไธไผ โ IP ็ฝๅๅ
- ๆทปๅ CI ๆๅกๅจ็ๅบๅฃ IP
- ๆฌๅฐๅผๅๅฏไธดๆถๅ
ณ้ญ็ฝๅๅ๏ผไฝ็ไบง็ฏๅขๅผบ็ๅปบ่ฎฎๅผๅฏ
Step 3๏ผๅๅปบ่ๆฌ
ๆ นๆฎ็จๆท้ๆฉ็ๆไฝ็ฑปๅ๏ผๅๅปบๅฏนๅบ่ๆฌใไปฅไธๆจกๆฟไฝฟ็จ็ฏๅขๅ้่ฏปๅๆๆ้
็ฝฎ๏ผๆฏๆ CI/CD ้ๆใ
3.1 ๆๅ
ไพ่ต่ๆฌๆจกๆฟ
่ฅ้กน็ฎไฝฟ็จ npm ๆจกๅไธ็จๆท้่ฆๆๅ
ไพ่ต่ฝๅ๏ผๅๅปบ scripts/pack-npm.js๏ผ
#!/usr/bin/env node
const ci = require('miniprogram-ci');
const fs = require('fs');
const path = require('path');
const CONFIG = {
appid: process.env.MP_APPID,
projectPath: process.env.MP_PROJECT_PATH,
};
function validateConfig() {
const required = { MP_APPID: CONFIG.appid, MP_PROJECT_PATH: CONFIG.projectPath };
const missing = Object.entries(required).filter(([, v]) => !v).map(([k]) => k);
if (missing.length) {
console.error(`โ ็ผบๅฐ็ฏๅขๅ้: ${missing.join(', ')}`);
process.exit(1);
}
if (!fs.existsSync(path.resolve(CONFIG.projectPath))) {
console.error(`โ ้กน็ฎ่ทฏๅพไธๅญๅจ: ${CONFIG.projectPath}`);
process.exit(1);
}
}
async function main() {
console.log('๐ ๆ ก้ช้
็ฝฎ...');
validateConfig();
console.log('\n๐ NPM ๆๅ
้
็ฝฎ:');
console.log(` AppID: ${CONFIG.appid}`);
console.log(` ้กน็ฎ่ทฏๅพ: ${path.resolve(CONFIG.projectPath)}`);
const project = new ci.Project({
appid: CONFIG.appid,
type: 'miniProgram',
projectPath: path.resolve(CONFIG.projectPath),
ignores: ['node_modules/**/*'],
});
console.log('\n๐ ๆๅ
ไพ่ต...');
try {
const result = await ci.packNpm(project, {
reporter: (msg) => console.log(` ${msg}`),
});
console.log('\nโ
NPM ๆๅ
ๅฎๆ๏ผ');
console.log(`๐ฆ ็ปๆ: ${JSON.stringify(result)}`);
} catch (err) {
console.error(`\nโ NPM ๆๅ
ๅคฑ่ดฅ: ${err.message}`);
process.exit(1);
}
}
main();
3.2 ้ข่ง่ๆฌๆจกๆฟ
่ฅ็จๆท้่ฆ้ข่ง่ฝๅ๏ผๅๅปบ scripts/preview.js๏ผ
#!/usr/bin/env node
const ci = require('miniprogram-ci');
const fs = require('fs');
const path = require('path');
const CONFIG = {
appid: process.env.MP_APPID,
privateKeyPath: process.env.MP_PRIVATE_KEY_PATH,
projectPath: process.env.MP_PROJECT_PATH,
robot: parseInt(process.env.MP_ROBOT, 10) || 1,
pagePath: process.env.MP_PAGE_PATH || '',
searchQuery: process.env.MP_SEARCH_QUERY || '',
outputDir: path.resolve(process.cwd(), 'ci-artifacts/previews'),
};
function validateConfig() {
const required = { MP_APPID: CONFIG.appid, MP_PRIVATE_KEY_PATH: CONFIG.privateKeyPath, MP_PROJECT_PATH: CONFIG.projectPath };
const missing = Object.entries(required).filter(([, v]) => !v).map(([k]) => k);
if (missing.length) {
console.error(`โ ็ผบๅฐ็ฏๅขๅ้: ${missing.join(', ')}`);
process.exit(1);
}
if (CONFIG.robot < 1 || CONFIG.robot > 30) {
console.error('โ MP_ROBOT ๅฟ
้กปๅจ 1-30 ไน้ด');
process.exit(1);
}
if (!fs.existsSync(path.resolve(CONFIG.privateKeyPath))) {
console.error(`โ ๅฏ้ฅๆไปถไธๅญๅจ: ${CONFIG.privateKeyPath}`);
process.exit(1);
}
if (!fs.existsSync(path.resolve(CONFIG.projectPath))) {
console.error(`โ ้กน็ฎ่ทฏๅพไธๅญๅจ: ${CONFIG.projectPath}`);
process.exit(1);
}
}
function ensureDir(dir) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
}
async function main() {
console.log('๐ ๆ ก้ช้
็ฝฎ...');
validateConfig();
ensureDir(CONFIG.outputDir);
const qrcodePath = path.join(CONFIG.outputDir, `preview-${timestamp()}.png`);
console.log('\n๐ ้ข่ง้
็ฝฎ:');
console.log(` AppID: ${CONFIG.appid}`);
console.log(` ้กน็ฎ่ทฏๅพ: ${path.resolve(CONFIG.projectPath)}`);
console.log(` ๆบๅจไบบ็ผๅท: ${CONFIG.robot}`);
if (CONFIG.pagePath) console.log(` ้กต้ข่ทฏๅพ: ${CONFIG.pagePath}`);
if (CONFIG.searchQuery) console.log(` ๆฅ่ฏขๅๆฐ: ${CONFIG.searchQuery}`);
console.log(` ไบ็ปด็ ่พๅบ: ${qrcodePath}`);
const project = new ci.Project({
appid: CONFIG.appid,
type: 'miniProgram',
projectPath: path.resolve(CONFIG.projectPath),
privateKeyPath: path.resolve(CONFIG.privateKeyPath),
ignores: ['node_modules/**/*'],
});
console.log('\n๐ ็ๆ้ข่ง...');
try {
const result = await ci.preview({
project,
desc: `Preview by robot ${CONFIG.robot} at ${new Date().toLocaleString()}`,
setting: { es6: true, es7: true, minify: true, autoPrefixWXSS: true },
qrcodeFormat: 'image',
qrcodeOutputDest: qrcodePath,
robot: CONFIG.robot,
...(CONFIG.pagePath && { pagePath: CONFIG.pagePath }),
...(CONFIG.searchQuery && { searchQuery: CONFIG.searchQuery }),
});
console.log('\nโ
้ข่งๆๅ๏ผ');
console.log(`๐ฑ ไบ็ปด็ : ${qrcodePath}`);
if (result?.subPackageInfo) {
console.log('\n๐ฆ ๅ
ๅคงๅฐ:');
result.subPackageInfo.forEach(p => console.log(` ${p.name || 'ไธปๅ
'}: ${(p.size / 1024 / 1024).toFixed(2)} MB`));
}
} catch (err) {
console.error(`\nโ ้ข่งๅคฑ่ดฅ: ${err.message}`);
if (err.message.includes('invalid ip')) console.error('๐ก ่ฏทๅฐๅฝๅ IP ๆทปๅ ๅฐๅพฎไฟกๅๅฐ็ฝๅๅ');
process.exit(1);
}
}
main();
3.2 ไธไผ ่ๆฌๆจกๆฟ
่ฅ็จๆท้่ฆไธไผ ่ฝๅ๏ผๅๅปบ scripts/upload.js๏ผ
#!/usr/bin/env node
const ci = require('miniprogram-ci');
const fs = require('fs');
const path = require('path');
const CONFIG = {
appid: process.env.MP_APPID,
privateKeyPath: process.env.MP_PRIVATE_KEY_PATH,
projectPath: process.env.MP_PROJECT_PATH,
robot: parseInt(process.env.MP_ROBOT, 10) || 1,
outputDir: path.resolve(process.cwd(), 'ci-artifacts/uploads'),
};
function parseArgs() {
const args = process.argv.slice(2);
const result = { version: null, desc: null, packNpm: false };
for (let i = 0; i < args.length; i++) {
if (args[i] === '--version' && args[i + 1]) result.version = args[++i];
else if (args[i] === '--desc' && args[i + 1]) result.desc = args[++i];
else if (args[i] === '--pack-npm') result.packNpm = true;
else if (args[i] === '--help' || args[i] === '-h') { printHelp(); process.exit(0); }
}
return result;
}
function printHelp() {
console.log(`
็จๆณ: node scripts/upload.js [้้กน]
้้กน:
--version <็ๆฌๅท> ๅฟ
ๅกซ๏ผๅฆ 1.0.0
--desc <ๆ่ฟฐ> ๅฟ
ๅกซ๏ผ็ๆฌๆ่ฟฐ
--pack-npm ไธไผ ๅๆง่ก npm ๆๅปบ
--help ๆพ็คบๅธฎๅฉ
`);
}
function validateConfig() {
const required = { MP_APPID: CONFIG.appid, MP_PRIVATE_KEY_PATH: CONFIG.privateKeyPath, MP_PROJECT_PATH: CONFIG.projectPath };
const missing = Object.entries(required).filter(([, v]) => !v).map(([k]) => k);
if (missing.length) {
console.error(`โ ็ผบๅฐ็ฏๅขๅ้: ${missing.join(', ')}`);
process.exit(1);
}
if (CONFIG.robot < 1 || CONFIG.robot > 30) {
console.error('โ MP_ROBOT ๅฟ
้กปๅจ 1-30 ไน้ด');
process.exit(1);
}
if (!fs.existsSync(path.resolve(CONFIG.privateKeyPath))) {
console.error(`โ ๅฏ้ฅๆไปถไธๅญๅจ: ${CONFIG.privateKeyPath}`);
process.exit(1);
}
if (!fs.existsSync(path.resolve(CONFIG.projectPath))) {
console.error(`โ ้กน็ฎ่ทฏๅพไธๅญๅจ: ${CONFIG.projectPath}`);
process.exit(1);
}
}
function ensureDir(dir) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}
function timestamp() {
return new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
}
function saveResult(result, args) {
ensureDir(CONFIG.outputDir);
const filename = `upload-${args.version}-${timestamp()}.json`;
const filepath = path.join(CONFIG.outputDir, filename);
const data = {
timestamp: new Date().toISOString(),
version: args.version,
desc: args.desc,
robot: CONFIG.robot,
result,
};
fs.writeFileSync(filepath, JSON.stringify(data, null, 2));
console.log(`๐ ็ปๆๅทฒไฟๅญ: ${filepath}`);
}
async function main() {
const args = parseArgs();
if (!args.version) { console.error('โ ๅฟ
้กปๆๅฎ --version'); process.exit(1); }
if (!args.desc) { console.error('โ ๅฟ
้กปๆๅฎ --desc'); process.exit(1); }
console.log('๐ ๆ ก้ช้
็ฝฎ...');
validateConfig();
console.log('\n๐ ไธไผ ้
็ฝฎ:');
console.log(` AppID: ${CONFIG.appid}`);
console.log(` ้กน็ฎ่ทฏๅพ: ${path.resolve(CONFIG.projectPath)}`);
console.log(` ๆบๅจไบบ็ผๅท: ${CONFIG.robot}`);
console.log(` ็ๆฌๅท: ${args.version}`);
console.log(` ็ๆฌๆ่ฟฐ: ${args.desc}`);
console.log(` packNpm: ${args.packNpm ? 'ๆฏ' : 'ๅฆ'}`);
const project = new ci.Project({
appid: CONFIG.appid,
type: 'miniProgram',
projectPath: path.resolve(CONFIG.projectPath),
privateKeyPath: path.resolve(CONFIG.privateKeyPath),
ignores: ['node_modules/**/*'],
});
if (args.packNpm) {
console.log('\n๐ฆ ๆง่ก npm ๆๅปบ...');
try {
await ci.packNpm(project, { reporter: console.log });
console.log('โ
npm ๆๅปบๅฎๆ');
} catch (err) {
console.error(`โ npm ๆๅปบๅคฑ่ดฅ: ${err.message}`);
process.exit(1);
}
}
console.log('\n๐ ไธไผ ไปฃ็ ...');
try {
const result = await ci.upload({
project,
version: args.version,
desc: args.desc,
robot: CONFIG.robot,
setting: { es6: true, es7: true, minify: true, autoPrefixWXSS: true },
onProgressUpdate: console.log,
});
console.log('\nโ
ไธไผ ๆๅ๏ผ');
if (result?.subPackageInfo) {
console.log('\n๐ฆ ๅ
ๅคงๅฐ:');
result.subPackageInfo.forEach(p => console.log(` ${p.name || 'ไธปๅ
'}: ${(p.size / 1024 / 1024).toFixed(2)} MB`));
}
saveResult({ success: true, ...result }, args);
} catch (err) {
console.error(`\nโ ไธไผ ๅคฑ่ดฅ: ${err.message}`);
if (err.message.includes('invalid ip')) console.error('๐ก ่ฏทๅฐๅฝๅ IP ๆทปๅ ๅฐๅพฎไฟกๅๅฐ็ฝๅๅ');
saveResult({ success: false, error: err.message }, args);
process.exit(1);
}
}
main();
Step 4๏ผๆณจๅ npm scripts
ๅจ package.json ็ scripts ไธญๆทปๅ ๏ผๆ นๆฎ็จๆท้ๆฉ็ๆไฝ๏ผ๏ผ
{
"scripts": {
"ci:pack-npm": "node scripts/pack-npm.js",
"ci:preview": "node scripts/preview.js",
"ci:upload": "node scripts/upload.js",
"ci:upload:npm": "node scripts/upload.js --pack-npm"
}
}
Step 5๏ผ็ฏๅขๅ้้
็ฝฎๆๅผ
ๆฌๅฐๅผๅ
ๅๅปบ .env ๆไปถ๏ผ้้
ๅ dotenv ๆ shell source๏ผ๏ผ
MP_APPID=wx1234567890abcdef
MP_PRIVATE_KEY_PATH=./private.wxXXXX.key
MP_PROJECT_PATH=./dist
MP_ROBOT=1
ๆ้็จๆท๏ผ ๅฐ .env ๅ *.key ๆทปๅ ๅฐ .gitignoreใ
CI/CD ้
็ฝฎ๏ผGitHub Actions ็คบไพ๏ผ
name: Deploy Mini Program
on:
push:
tags:
- 'v*'
jobs:
upload:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Write private key
run: echo "${{ secrets.MP_PRIVATE_KEY }}" > private.key
- name: Upload to WeChat
env:
MP_APPID: ${{ secrets.MP_APPID }}
MP_PRIVATE_KEY_PATH: ./private.key
MP_PROJECT_PATH: ./dist
MP_ROBOT: 1
run: |
VERSION=${GITHUB_REF_NAME#v}
npm run ci:upload -- --version "$VERSION" --desc "CI ่ชๅจไธไผ "
- name: Cleanup
if: always()
run: rm -f private.key
ๅธธ่ง้่ฏฏๅค็
| ้่ฏฏ็ฐ่ฑก | ๅๅ | ่งฃๅณๆนๆก |
|---|
invalid ip | IP ไธๅจ็ฝๅๅ | ๅพฎไฟกๅๅฐๆทปๅ IP ๆไธดๆถๅ
ณ้ญ็ฝๅๅ |
permission denied | ๅฏ้ฅๆ ๆๆๆ ๆ้ | ้ๆฐ็ๆๅฏ้ฅ๏ผๆฃๆฅๆฏๅฆๆไธไผ ๆ้ |
project.config.json not found | ้กน็ฎ่ทฏๅพ้่ฏฏ | ็กฎ่ฎค MP_PROJECT_PATH ๆๅ็ผ่ฏไบง็ฉ็ฎๅฝ |
Error: getaddrinfo ENOTFOUND | ็ฝ็ป้ฎ้ข | ๆฃๆฅไปฃ็่ฎพ็ฝฎๆ็ฝ็ป่ฟๆฅ |
| ไธไผ ๅ็ๆฌๆชๅบ็ฐ | robot ็ผๅทๅฒ็ช | ไธๅไปปๅกไฝฟ็จไธๅ robot ็ผๅท |
่ๆฌๅๆฐ้ๆฅ
preview.js
| ็ฏๅขๅ้ | ๅฟ
ๅกซ | ่ฏดๆ |
|---|
MP_APPID | โ
| ๅฐ็จๅบ AppID |
MP_PRIVATE_KEY_PATH | โ
| ๅฏ้ฅๆไปถ่ทฏๅพ |
MP_PROJECT_PATH | โ
| ็ผ่ฏไบง็ฉ็ฎๅฝ |
MP_ROBOT | โ | ๆบๅจไบบ็ผๅท๏ผ้ป่ฎค 1๏ผ |
MP_PAGE_PATH | โ | ้ข่งๆๅผ็้กต้ข |
MP_SEARCH_QUERY | โ | ้กต้ขๆฅ่ฏขๅๆฐ |
upload.js
| ๅๆฐ | ๅฟ
ๅกซ | ่ฏดๆ |
|---|
--version <v> | โ
| ็ๆฌๅท |
--desc <d> | โ
| ็ๆฌๆ่ฟฐ |
--pack-npm | โ | ไธไผ ๅๆง่ก npm ๆๅปบ |
ๅฎๅ
จๆฃๆฅๆธ
ๅ
ๅจไบคไป่ๆฌๅ๏ผๆ้็จๆท็กฎ่ฎค๏ผ