feat: Cli 优化

This commit is contained in:
陶林 2024-12-16 21:30:38 +08:00
parent c64c7e0ea0
commit cb0e9763f0
9 changed files with 379 additions and 65 deletions

View File

@ -1,8 +1,13 @@
{
"name": "测试",
"version": "1.1.1",
"description": "cordova apk build",
"author": {
"name": "taolin",
"email": "taolin@taoya.art"
},
"homeUrl": "https://www.baidu.com",
"appId": "asd",
"appName": "asd",
"appVersion": "1.0.0",
"appId": "com.taoya.test",
"packType": "debug",
"pushEmail": "asd@qq.com"
"pushEmail": "taolin@taoya.art"
}

View File

@ -1,33 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<widget xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0" id="com.example.tao" version="1.0.0">
<name>APP</name>
<description>app</description>
<author email="dev@cordova.apache.org" href="https://cordova.apache.org"></author>
<content src="http://test.taoya.art/" />
<access origin="*" />
<allow-intent href="http://*/*"/>
<allow-intent href="https://*/*"/>
<allow-intent href="tel:*"/>
<allow-intent href="sms:*"/>
<allow-intent href="mailto:*"/>
<allow-intent href="geo:*"/>
<allow-intent href="market:*"/>
<widget xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0" id="com.taoya.test" version="1.1.1">
<name>测试</name>
<description>cordova apk build</description>
<author email="taolin@taoya.art">taolin</author>
<content src="https://www.baidu.com"></content>
<access origin="*"></access>
<allow-intent href="http://*/*"></allow-intent>
<allow-intent href="https://*/*"></allow-intent>
<allow-intent href="tel:*"></allow-intent>
<allow-intent href="sms:*"></allow-intent>
<allow-intent href="mailto:*"></allow-intent>
<allow-intent href="geo:*"></allow-intent>
<allow-intent href="market:*"></allow-intent>
<platform name="android">
<preference name="Fullscreen" value="true"/>
<preference name="Fullscreen"></preference>
<icon src="logo.png"></icon>
</platform>
<!-- 设置Java和Gradle版本 -->
<preference name="android-minSdkVersion" value="24" />
<preference name="android-targetSdkVersion" value="35" />
<preference name="android-compileSdkVersion" value="35" />
<preference name="GradleVersion" value="8.7" />
<preference name="JavaVersion" value="17" />
<preference name="AndroidXEnabled" value="true" />
<!-- Kotlin -->
<preference name="GradlePluginKotlinEnabled" value="true" />
<preference name="GradlePluginKotlinCodeStyle" value="official" />
<preference name="GradlePluginKotlinVersion" value="1.9.24" />
<preference name="android-minSdkVersion" value="24"></preference>
<preference name="android-targetSdkVersion" value="35"></preference>
<preference name="android-compileSdkVersion" value="35"></preference>
<preference name="GradleVersion" value="8.7"></preference>
<preference name="JavaVersion" value="17"></preference>
<preference name="AndroidXEnabled"></preference>
<preference name="GradlePluginKotlinEnabled"></preference>
<preference name="GradlePluginKotlinCodeStyle" value="official"></preference>
<preference name="GradlePluginKotlinVersion" value="1.9.24"></preference>
</widget>

31
package-lock.json generated
View File

@ -32,6 +32,7 @@
"cordova-plugin-vibration": "^3.1.1",
"es6-promise-plugin": "^4.2.2",
"eslint": "^9.17.0",
"fast-xml-parser": "^4.5.1",
"picocolors": "^1.1.1",
"tsx": "^4.19.2",
"typeorm": "^0.3.20",
@ -3988,6 +3989,29 @@
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/fast-xml-parser": {
"version": "4.5.1",
"resolved": "https://registry.npmmirror.com/fast-xml-parser/-/fast-xml-parser-4.5.1.tgz",
"integrity": "sha512-y655CeyUQ+jj7KBbYMc4FG01V8ZQqjN+gDYGJ50RtfsUB8iG9AmwmwoAgeKLJdmueKKMrH1RJ7yXHTSoczdv5w==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
},
{
"type": "paypal",
"url": "https://paypal.me/naturalintelligence"
}
],
"license": "MIT",
"dependencies": {
"strnum": "^1.0.5"
},
"bin": {
"fxparser": "src/cli/cli.js"
}
},
"node_modules/fastq": {
"version": "1.17.1",
"resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.17.1.tgz",
@ -8562,6 +8586,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strnum": {
"version": "1.0.5",
"resolved": "https://registry.npmmirror.com/strnum/-/strnum-1.0.5.tgz",
"integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==",
"dev": true,
"license": "MIT"
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz",

View File

@ -12,7 +12,8 @@
"build": "cordova build android",
"dev": "node scripts/dev.js",
"logo": "tsx scripts/logo.ts",
"allInOne": "npm run clean && npm i && npm run ad"
"allInOne": "npm run clean && npm i && npm run ad",
"platform": "tsx scripts/platform.ts"
},
"keywords": [],
"author": "taolin taolin@taoya.art",
@ -38,6 +39,7 @@
"cordova-plugin-vibration": "^3.1.1",
"es6-promise-plugin": "^4.2.2",
"eslint": "^9.17.0",
"fast-xml-parser": "^4.5.1",
"picocolors": "^1.1.1",
"tsx": "^4.19.2",
"typeorm": "^0.3.20",
@ -61,5 +63,6 @@
},
"dependencies": {
"commander": "^12.1.0"
}
},
"packageManager": "pnpm@8.10.2+sha1.e0b68270e89c817ff88b7be62466a2128c53af02"
}

104
scripts/build.ts Normal file
View File

@ -0,0 +1,104 @@
import { readFile, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { XMLParser, XMLBuilder } from 'fast-xml-parser'
import consola from 'consola'
// 获取当前文件的目录路径
const __dirname = fileURLToPath(new URL('.', import.meta.url))
// 获取项目根目录路径
const root = join(__dirname, '..')
// 定义配置文件的类型接口
interface AppConfig {
name: string // 应用名称
version: string // 应用版本号
description: string // 应用描述
author: { // 作者信息
name: string // 作者名称
email: string // 作者邮箱
}
homeUrl: string // 应用主页 URL
appId: string // 应用 ID
packType: 'debug' | 'release' // 打包类型
}
/**
* config.xml
* @param config
*/
async function updateConfigXml(config: AppConfig) {
// 构建 config.xml 的完整路径
const xmlPath = join(root, 'config.xml')
try {
// 读取现有的 config.xml 文件内容
const xmlContent = await readFile(xmlPath, 'utf-8')
// 创建 XML 解析器实例,配置属性处理选项
const parser = new XMLParser({
ignoreAttributes: false, // 不忽略 XML 属性
attributeNamePrefix: '@_' // 属性名称前缀
})
// 解析 XML 内容为 JavaScript 对象
const xmlObj = parser.parse(xmlContent)
// 更新配置信息
xmlObj.widget['@_version'] = config.version // 更新版本号
xmlObj.widget['@_id'] = config.appId // 更新应用 ID
xmlObj.widget.name = config.name // 更新应用名称
xmlObj.widget.description = config.description // 更新应用描述
xmlObj.widget.author = {
'#text': config.author.name, // 作者名称作为节点文本
'@_email': config.author.email // 邮箱作为属性
}
// 更新或添加 content 标签的 src 属性
if (!xmlObj.widget.content) {
xmlObj.widget.content = { '@_src': config.homeUrl }
} else {
xmlObj.widget.content['@_src'] = config.homeUrl
}
// 创建 XML 构建器实例
const builder = new XMLBuilder({
ignoreAttributes: false,
attributeNamePrefix: '@_',
format: true // 启用格式化输出
})
// 将对象转换回 XML 字符串
const newXmlContent = builder.build(xmlObj)
// 将更新后的内容写回文件
await writeFile(xmlPath, newXmlContent, 'utf-8')
consola.success('config.xml has been updated')
} catch (err) {
consola.error('Failed to update config.xml:', err)
process.exit(1)
}
}
/**
*
*/
async function main() {
try {
// 读取 config.json 配置文件
const configPath = join(root, 'config.json')
const configContent = await readFile(configPath, 'utf-8')
const config: AppConfig = JSON.parse(configContent)
// 使用配置更新 config.xml
await updateConfigXml(config)
// TODO: 这里可以添加其他构建步骤
consola.success('Build completed successfully')
} catch (err) {
consola.error('Build failed:', err)
process.exit(1)
}
}
// 执行主函数
main()

View File

@ -2,9 +2,25 @@ import { z } from 'zod';
import fs from 'fs/promises';
import path from 'path';
import { intro, outro, text, select, isCancel, cancel, group } from '@clack/prompts';
import { bgRed } from 'picocolors';
const ConfigSchema = z.object({
name: z.string().min(1, {
message: "应用名称不能为空"
}),
version: z.string().regex(/^\d+\.\d+\.\d+$/, {
message: "版本号格式必须为 x.x.x"
}),
description: z.string().min(1, {
message: "应用描述不能为空"
}),
author: z.object({
name: z.string().min(1, {
message: "作者名称不能为空"
}),
email: z.string().email({
message: "作者邮箱格式不正确"
})
}),
homeUrl: z.string().url({
message: "URL格式不正确"
}),
@ -13,12 +29,6 @@ const ConfigSchema = z.object({
}).regex(/^[a-zA-Z0-9._-]+$/, {
message: "应用ID只能包含字母、数字、点、下划线和横线"
}),
appName: z.string().min(1, {
message: "应用名称不能为空"
}),
appVersion: z.string().regex(/^\d+\.\d+\.\d+$/, {
message: "版本号格式必须为 x.x.x"
}),
packType: z.enum(['release', 'debug'], {
errorMap: () => ({ message: "打包类型必须是 release 或 debug" })
}),
@ -34,6 +44,45 @@ async function main() {
const config = await group(
{
name: () => text({
message: '请输入应用名称',
validate(value) {
if (!value.length) return '应用名称不能为空';
},
}),
version: () => text({
message: '请输入应用版本号',
validate(value) {
if (!/^\d+\.\d+\.\d+$/.test(value)) {
return '请输入正确的版本号,例如: 1.0.0';
}
},
}),
description: () => text({
message: '请输入应用描述',
validate(value) {
if (!value.length) return '应用描述不能为空';
},
}),
authorName: () => text({
message: '请输入作者名称',
validate(value) {
if (!value.length) return '作者名称不能为空';
},
}),
authorEmail: () => text({
message: '请输入作者邮箱',
validate(value) {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
return '请输入正确的邮箱地址';
}
},
}),
homeUrl: () => text({
message: '请输入主页URL',
validate(value) {
@ -55,22 +104,6 @@ async function main() {
},
}),
appName: () => text({
message: '请输入应用名称',
validate(value) {
if (!value.length) return '应用名称不能为空';
},
}),
appVersion: () => text({
message: '请输入应用版本号',
validate(value) {
if (!/^\d+\.\d+\.\d+$/.test(value)) {
return '请输入正确的版本号,例如: 1.0.0';
}
},
}),
packType: () => select({
message: '请选择打包类型',
options: [
@ -104,14 +137,29 @@ async function main() {
}
}
// 重构配置对象以匹配所需格式
const formattedConfig = {
name: config.name,
version: config.version,
description: config.description,
author: {
name: config.authorName,
email: config.authorEmail
},
homeUrl: config.homeUrl,
appId: config.appId,
packType: config.packType,
pushEmail: config.pushEmail
};
try {
// 使用zod验证配置
const result = ConfigSchema.safeParse(config);
const result = ConfigSchema.safeParse(formattedConfig);
if (!result.success) {
cancel('配置验证失败');
result.error.errors.forEach(error => {
console.error(bgRed(` ${error.path.join('.')}: ${error.message} `));
console.error((` ${error.path.join('.')}: ${error.message} `));
});
process.exit(1);
}
@ -125,7 +173,7 @@ async function main() {
} catch (error) {
cancel('保存配置文件时出错');
if (error instanceof Error) {
console.error(bgRed(` ${error.message} `));
console.error((` ${error.message} `));
}
process.exit(1);
}
@ -134,7 +182,7 @@ async function main() {
main().catch(error => {
cancel('发生意外错误');
if (error instanceof Error) {
console.error(bgRed(` ${error.message} `));
console.error((` ${error.message} `));
}
process.exit(1);
});

53
scripts/dist.ts Normal file
View File

@ -0,0 +1,53 @@
import { mkdir, copyFile, access } from 'node:fs/promises'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import consola from 'consola'
const __dirname = fileURLToPath(new URL('.', import.meta.url))
const root = join(__dirname, '..')
// 检查文件是否存在的辅助函数
async function fileExists(path: string): Promise<boolean> {
try {
await access(path)
return true
} catch {
return false
}
}
async function main() {
// 确保 dist 目录存在
try {
await mkdir(join(root, 'dist'))
consola.info('dist directory created')
} catch (err) {
consola.info('dist directory already exists')
}
// 定义 APK 文件路径
const apkPaths = {
debug: {
source: join(root, 'platforms/android/app/build/outputs/apk/debug/app-debug.apk'),
target: join(root, 'dist/app-debug.apk')
},
release: {
source: join(root, 'platforms/android/app/build/outputs/apk/release/app-release.apk'),
target: join(root, 'dist/app-release.apk')
}
}
// 尝试复制 debug 和 release 版本的 APK
for (const [type, paths] of Object.entries(apkPaths)) {
if (await fileExists(paths.source)) {
try {
await copyFile(paths.source, paths.target)
consola.success(`${type} APK copied to dist/app-${type}.apk`)
} catch (err) {
consola.warn(`Failed to copy ${type} APK:`, err)
}
}
}
}
main()

2
scripts/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './dist' // 导出 dist 脚本
export * from './logo' // 替换 Logo

73
scripts/platform.ts Normal file
View File

@ -0,0 +1,73 @@
import { exec } from 'node:child_process'
import { promisify } from 'node:util'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { access } from 'node:fs/promises'
import consola from 'consola'
const execAsync = promisify(exec)
const __dirname = fileURLToPath(new URL('.', import.meta.url))
const root = join(__dirname, '..')
async function checkPlatformExists(platform: string): Promise<boolean> {
try {
await access(join(root, 'platforms', platform))
return true
} catch {
return false
}
}
async function addPlatform(platform: string) {
try {
// 检查平台是否已存在
const exists = await checkPlatformExists(platform)
if (exists) {
consola.info(`Platform ${platform} already exists`)
return
}
// 添加平台
consola.info(`Adding ${platform} platform...`)
const { stdout, stderr } = await execAsync(`cordova platform add ${platform}`, {
cwd: root,
env: {
...process.env,
NODE_ENV: 'development'
}
})
if (stderr) {
consola.warn(stderr)
}
if (stdout) {
consola.info(stdout)
}
consola.success(`Platform ${platform} added successfully`)
} catch (error) {
if (error instanceof Error) {
consola.error('Failed to add platform:', error.message)
} else {
consola.error('An unknown error occurred while adding platform')
}
process.exit(1)
}
}
async function main() {
try {
// 添加 Android 平台
await addPlatform('android')
} catch (error) {
if (error instanceof Error) {
consola.error('Platform addition failed:', error.message)
} else {
consola.error('An unknown error occurred')
}
process.exit(1)
}
}
main()