@playcraft/devkit 1.0.7 → 1.0.9
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/cli/bin/playable-scripts.js +1 -1
- package/cli/commands/build.js +8 -9
- package/cli/commands/pack.js +58 -58
- package/cli/commands/vite-dev.js +1 -0
- package/core/index.d.ts +16 -0
- package/core/index.js +1 -1
- package/core/options.js +8 -7
- package/core/utils/parseArgvOptions.js +3 -2
- package/core/utils/validateThemeData.js +53 -53
- package/core/vite.dev.js +25 -0
- package/package.json +2 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";process.on("unhandledRejection",err=>{throw err});const spawn=require("cross-spawn");const args=process.argv.slice(2);const scriptIndex=args.findIndex(x=>x==="build"||x==="dev"||x==="builds"||x==="pack");const script=scriptIndex===-1?args[0]:args[scriptIndex];const nodeArgs=scriptIndex>0?args.slice(0,scriptIndex):[];if(["build","dev","builds","pack"].includes(script)){const result=spawn.sync(process.execPath,nodeArgs.concat(require.resolve("../commands/"+script)).concat(args.slice(scriptIndex+1)),{stdio:"inherit"});if(result.signal){if(result.signal==="SIGKILL"){console.log("The build failed because the process exited too early. "+"This probably means the system ran out of memory or someone called "+"`kill -9` on the process.")}else if(result.signal==="SIGTERM"){console.log("The build failed because the process exited too early. "+"Someone might have called `kill` or `killall`, or the system could "+"be shutting down.")}process.exit(1)}process.exit(result.status)}else{console.log("Unknown script \""+script+"\".")}
|
|
2
|
+
"use strict";process.on("unhandledRejection",err=>{throw err});const spawn=require("cross-spawn");const args=process.argv.slice(2);const scriptIndex=args.findIndex(x=>x==="build"||x==="dev"||x==="vite-dev"||x==="builds"||x==="pack");const script=scriptIndex===-1?args[0]:args[scriptIndex];const nodeArgs=scriptIndex>0?args.slice(0,scriptIndex):[];if(["build","dev","vite-dev","builds","pack"].includes(script)){const result=spawn.sync(process.execPath,nodeArgs.concat(require.resolve("../commands/"+script)).concat(args.slice(scriptIndex+1)),{stdio:"inherit"});if(result.signal){if(result.signal==="SIGKILL"){console.log("The build failed because the process exited too early. "+"This probably means the system ran out of memory or someone called "+"`kill -9` on the process.")}else if(result.signal==="SIGTERM"){console.log("The build failed because the process exited too early. "+"Someone might have called `kill` or `killall`, or the system could "+"be shutting down.")}process.exit(1)}process.exit(result.status)}else{console.log("Unknown script \""+script+"\".")}
|
package/cli/commands/build.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
"use strict";process.env.BABEL_ENV="production";process.env.NODE_ENV="production";const fs=require("fs");const path=require("path");const{runBuild}=require("../../core/webpack.build.js");const{runThemeValidation,restoreSchema}=require("../../core/utils/validateThemeData");/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* 从主题入口文件中检测当前主题名称。
|
|
3
|
+
* 支持以下模式:
|
|
4
4
|
* export { default } from './themeName';
|
|
5
5
|
* import Theme from './themeName';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
if(schemaPathToRestore)restoreSchema(schemaPathToRestore);process.exit(1)}}// Run build, then always restore schema regardless of success/failure
|
|
6
|
+
*/function detectCurrentTheme(projectRoot){// 尝试加载 builds.config.js 获取主题配置
|
|
7
|
+
const buildsConfigPath=path.join(projectRoot,"builds.config.js");let themeConfig={sourceDir:"src/theme",entryFile:"src/theme/index.ts"};if(fs.existsSync(buildsConfigPath)){try{delete require.cache[buildsConfigPath];const config=require(buildsConfigPath);if(config.themes){themeConfig={...themeConfig,...config.themes}}}catch(e){// 忽略错误
|
|
8
|
+
}}const entryPath=path.join(projectRoot,themeConfig.entryFile);if(!fs.existsSync(entryPath)){return{themeName:null,themeConfig}}const content=fs.readFileSync(entryPath,"utf8");// 匹配: export { default } from './themeName' 或 import ... from './themeName'
|
|
9
|
+
const match=content.match(/from\s+['"]\.\/([^'"]+)['"]/);if(match){return{themeName:match[1],themeConfig}}return{themeName:null,themeConfig}}// --- 主流程 ---
|
|
10
|
+
const projectRoot=process.cwd();const{themeName,themeConfig}=detectCurrentTheme(projectRoot);let schemaPathToRestore=null;if(themeName){const buildLog=(msg,level)=>{const colors={info:"\x1B[36m",success:"\x1B[32m",error:"\x1B[31m"};const reset="\x1B[0m";const color=colors[level]||"";console.log(`${color}[build] ${msg}${reset}`)};const{valid,schemaPath}=runThemeValidation(themeName,projectRoot,themeConfig,{log:buildLog});schemaPathToRestore=schemaPath;if(!valid){// 校验失败时恢复 schema(备份可能已创建)
|
|
11
|
+
if(schemaPathToRestore)restoreSchema(schemaPathToRestore);process.exit(1)}}// 执行构建,无论成功或失败都恢复 schema
|
|
13
12
|
runBuild().then(()=>{if(schemaPathToRestore)restoreSchema(schemaPathToRestore)}).catch(err=>{if(schemaPathToRestore)restoreSchema(schemaPathToRestore);console.error(err);process.exit(1)});
|
package/cli/commands/pack.js
CHANGED
|
@@ -1,81 +1,81 @@
|
|
|
1
|
-
"use strict";const fs=require("fs");const path=require("path");const yazl=require("yazl");// ====================
|
|
1
|
+
"use strict";const fs=require("fs");const path=require("path");const yazl=require("yazl");// ==================== 配置 ====================
|
|
2
2
|
const projectRoot=process.cwd();/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* 解析 appName,依次从 builds.config.js、build.json 或 CLI 参数中获取。
|
|
4
|
+
* 优先级: CLI --app-name > builds.config.js > build.json > 文件夹名称
|
|
5
5
|
* @returns {string}
|
|
6
|
-
*/function resolveAppName(){// 1. CLI
|
|
7
|
-
const args=process.argv.slice(2);for(const arg of args){if(arg.startsWith("--app-name=")){return arg.split("=")[1].trim()}}// 2. builds.config.js → appName
|
|
8
|
-
const buildsConfigPath=path.join(projectRoot,"builds.config.js");if(fs.existsSync(buildsConfigPath)){try{delete require.cache[buildsConfigPath];const config=require(buildsConfigPath);if(config.appName)return config.appName;if(config.title)return config.title}catch(e){//
|
|
6
|
+
*/function resolveAppName(){// 1. CLI 参数: --app-name=xxx
|
|
7
|
+
const args=process.argv.slice(2);for(const arg of args){if(arg.startsWith("--app-name=")){return arg.split("=")[1].trim()}}// 2. builds.config.js → appName 或 title
|
|
8
|
+
const buildsConfigPath=path.join(projectRoot,"builds.config.js");if(fs.existsSync(buildsConfigPath)){try{delete require.cache[buildsConfigPath];const config=require(buildsConfigPath);if(config.appName)return config.appName;if(config.title)return config.title}catch(e){// 忽略错误
|
|
9
9
|
}}// 3. build.json → app
|
|
10
|
-
const buildJsonPath=path.join(projectRoot,"build.json");if(fs.existsSync(buildJsonPath)){try{const data=JSON.parse(fs.readFileSync(buildJsonPath,"utf8"));if(data.app)return data.app}catch(e){//
|
|
11
|
-
}}// 4.
|
|
10
|
+
const buildJsonPath=path.join(projectRoot,"build.json");if(fs.existsSync(buildJsonPath)){try{const data=JSON.parse(fs.readFileSync(buildJsonPath,"utf8"));if(data.app)return data.app}catch(e){// 忽略错误
|
|
11
|
+
}}// 4. 兜底: 使用项目文件夹名称
|
|
12
12
|
return path.basename(projectRoot)}/**
|
|
13
|
-
*
|
|
13
|
+
* 从 CLI 参数中解析输出目录。
|
|
14
14
|
* @returns {string}
|
|
15
|
-
*/function resolveOutputDir(){const args=process.argv.slice(2);for(const arg of args){if(arg.startsWith("--out-dir=")){return arg.split("=")[1].trim()}}return"dist"}// ====================
|
|
15
|
+
*/function resolveOutputDir(){const args=process.argv.slice(2);for(const arg of args){if(arg.startsWith("--out-dir=")){return arg.split("=")[1].trim()}}return"dist"}// ==================== 忽略规则解析 ====================
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
* @param {string} filePath -
|
|
19
|
-
* @returns {string[]}
|
|
20
|
-
*/function parseSingleIgnoreFile(filePath){if(!fs.existsSync(filePath)){return[]}const content=fs.readFileSync(filePath,"utf8");const patterns=[];for(const rawLine of content.split(/\r?\n/)){const line=rawLine.trim();//
|
|
21
|
-
if(!line||line.startsWith("#"))continue;//
|
|
22
|
-
if(line.startsWith("!")){console.log(`\x1b[33m⚠️
|
|
23
|
-
// -
|
|
24
|
-
// -
|
|
17
|
+
* 解析单个忽略文件,返回规范化后的模式数组。
|
|
18
|
+
* @param {string} filePath - 忽略文件的绝对路径
|
|
19
|
+
* @returns {string[]} 规范化后的忽略模式数组
|
|
20
|
+
*/function parseSingleIgnoreFile(filePath){if(!fs.existsSync(filePath)){return[]}const content=fs.readFileSync(filePath,"utf8");const patterns=[];for(const rawLine of content.split(/\r?\n/)){const line=rawLine.trim();// 跳过空行和注释
|
|
21
|
+
if(!line||line.startsWith("#"))continue;// 不支持取反模式,给出警告并跳过
|
|
22
|
+
if(line.startsWith("!")){console.log(`\x1b[33m⚠️ 不支持取反模式,已跳过: ${line}\x1b[0m`);continue}// 规范化:
|
|
23
|
+
// - 移除尾部斜杠(用于目录匹配)
|
|
24
|
+
// - 移除前导 ./(gitignore 中 ./foo 等同于 foo)
|
|
25
25
|
let normalized=line.replace(/\/+$/,"");normalized=normalized.replace(/^\.\//g,"");patterns.push(normalized)}return patterns}/**
|
|
26
|
-
*
|
|
27
|
-
* .gitignore
|
|
26
|
+
* 解析 .gitignore 和 .playcraftignore 文件,合并并去重。
|
|
27
|
+
* .gitignore 提供基础忽略规则,.playcraftignore 提供额外补充规则。
|
|
28
28
|
* @returns {{ patterns: string[], sources: { gitignore: number, playcraftignore: number } }}
|
|
29
|
-
*/function parseIgnoreFiles(){const gitignorePath=path.join(projectRoot,".gitignore");const playcraftignorePath=path.join(projectRoot,".playcraftignore");const gitignorePatterns=parseSingleIgnoreFile(gitignorePath);const playcraftPatterns=parseSingleIgnoreFile(playcraftignorePath);if(gitignorePatterns.length===0&&playcraftPatterns.length===0){console.log("\x1B[33m\u26A0\uFE0F
|
|
29
|
+
*/function parseIgnoreFiles(){const gitignorePath=path.join(projectRoot,".gitignore");const playcraftignorePath=path.join(projectRoot,".playcraftignore");const gitignorePatterns=parseSingleIgnoreFile(gitignorePath);const playcraftPatterns=parseSingleIgnoreFile(playcraftignorePath);if(gitignorePatterns.length===0&&playcraftPatterns.length===0){console.log("\x1B[33m\u26A0\uFE0F \u672A\u627E\u5230 .gitignore \u6216 .playcraftignore \u6587\u4EF6\uFF0C\u5C06\u6253\u5305\u6240\u6709\u6587\u4EF6\u3002\x1B[0m");return{patterns:[],sources:{gitignore:0,playcraftignore:0}}}// 合并并去重(保持顺序,.gitignore 优先)
|
|
30
30
|
const seen=new Set;const merged=[];for(const p of[...gitignorePatterns,...playcraftPatterns]){if(!seen.has(p)){seen.add(p);merged.push(p)}}return{patterns:merged,sources:{gitignore:gitignorePatterns.length,playcraftignore:playcraftPatterns.length}}}/**
|
|
31
|
-
*
|
|
31
|
+
* 将 gitignore 风格的模式转换为正则表达式。
|
|
32
32
|
*
|
|
33
|
-
*
|
|
34
|
-
* -
|
|
35
|
-
* -
|
|
36
|
-
* -
|
|
37
|
-
* -
|
|
33
|
+
* gitignore 语义:
|
|
34
|
+
* - 不含 `/` 的模式 → 在目录树中任意位置匹配文件/目录名
|
|
35
|
+
* - 含 `/` 的模式(如 `scripts/foo`)→ 锚定到项目根目录
|
|
36
|
+
* - 前导 `/` 表示"仅匹配根目录",构建正则前会被剥离
|
|
37
|
+
* - 前导 `./` 与 `/` 等效(已在解析阶段被剥离)
|
|
38
38
|
*
|
|
39
39
|
* @param {string} pattern
|
|
40
40
|
* @returns {RegExp}
|
|
41
|
-
*/function patternToRegex(pattern){let p=pattern;let anchored=false;//
|
|
42
|
-
if(p.startsWith("/")){p=p.slice(1);anchored=true}//
|
|
43
|
-
if(p.includes("/")){anchored=true}//
|
|
44
|
-
let regexStr=p.replace(/[.+^${}()|[\]\\]/g,"\\$&");//
|
|
45
|
-
regexStr=regexStr.replace(/\*\*/g,"{{GLOBSTAR}}");//
|
|
46
|
-
regexStr=regexStr.replace(/\*/g,"[^/]*");//
|
|
47
|
-
regexStr=regexStr.replace(/\?/g,"[^/]");//
|
|
48
|
-
regexStr=regexStr.replace(/\{\{GLOBSTAR\}\}/g,".*");if(anchored){//
|
|
49
|
-
return new RegExp(`^${regexStr}(/|$)`)}//
|
|
41
|
+
*/function patternToRegex(pattern){let p=pattern;let anchored=false;// 前导 / 表示锚定到根目录;构建正则前剥离
|
|
42
|
+
if(p.startsWith("/")){p=p.slice(1);anchored=true}// 如果模式中包含 /,同样视为锚定到根目录
|
|
43
|
+
if(p.includes("/")){anchored=true}// 转义正则特殊字符(* 和 ? 除外)
|
|
44
|
+
let regexStr=p.replace(/[.+^${}()|[\]\\]/g,"\\$&");// 处理 **(匹配任意路径层级,包括嵌套目录)
|
|
45
|
+
regexStr=regexStr.replace(/\*\*/g,"{{GLOBSTAR}}");// 处理 *(匹配除路径分隔符外的任意字符)
|
|
46
|
+
regexStr=regexStr.replace(/\*/g,"[^/]*");// 处理 ?(匹配除路径分隔符外的单个字符)
|
|
47
|
+
regexStr=regexStr.replace(/\?/g,"[^/]");// 还原 globstar 占位符
|
|
48
|
+
regexStr=regexStr.replace(/\{\{GLOBSTAR\}\}/g,".*");if(anchored){// 锚定模式:从相对路径的起始位置匹配
|
|
49
|
+
return new RegExp(`^${regexStr}(/|$)`)}// 非锚定模式:在目录树中任意位置匹配路径段
|
|
50
50
|
return new RegExp(`(^|/)${regexStr}(/|$)`)}/**
|
|
51
|
-
*
|
|
52
|
-
* @param {string} relativePath -
|
|
51
|
+
* 检查相对路径是否应被忽略。
|
|
52
|
+
* @param {string} relativePath - 使用正斜杠规范化后的相对路径
|
|
53
53
|
* @param {RegExp[]} regexPatterns
|
|
54
54
|
* @returns {boolean}
|
|
55
|
-
*/function shouldIgnore(relativePath,regexPatterns){for(const regex of regexPatterns){if(regex.test(relativePath))return true}return false}// ====================
|
|
55
|
+
*/function shouldIgnore(relativePath,regexPatterns){for(const regex of regexPatterns){if(regex.test(relativePath))return true}return false}// ==================== 文件收集 ====================
|
|
56
56
|
/**
|
|
57
|
-
*
|
|
58
|
-
* @param {string} dir -
|
|
59
|
-
* @param {string} baseDir -
|
|
57
|
+
* 递归收集目录下的所有文件,遵循忽略规则。
|
|
58
|
+
* @param {string} dir - 目录的绝对路径
|
|
59
|
+
* @param {string} baseDir - 项目根目录,用于计算相对路径
|
|
60
60
|
* @param {RegExp[]} ignoreRegexes
|
|
61
|
-
* @returns {string[]}
|
|
62
|
-
*/function collectFiles(dir,baseDir,ignoreRegexes){const results=[];let entries;try{entries=fs.readdirSync(dir,{withFileTypes:true})}catch(e){console.warn(`\x1b[33m⚠️
|
|
63
|
-
const relativePath=path.relative(baseDir,fullPath).replace(/\\/g,"/");if(shouldIgnore(relativePath,ignoreRegexes)){continue}if(entry.isDirectory()){results.push(...collectFiles(fullPath,baseDir,ignoreRegexes))}else if(entry.isFile()||entry.isSymbolicLink()){results.push(fullPath)}}return results}// ==================== Zip
|
|
61
|
+
* @returns {string[]} 文件绝对路径数组
|
|
62
|
+
*/function collectFiles(dir,baseDir,ignoreRegexes){const results=[];let entries;try{entries=fs.readdirSync(dir,{withFileTypes:true})}catch(e){console.warn(`\x1b[33m⚠️ 无法读取目录: ${dir}\x1b[0m`);return results}for(const entry of entries){const fullPath=path.join(dir,entry.name);// 使用正斜杠以保持匹配一致性
|
|
63
|
+
const relativePath=path.relative(baseDir,fullPath).replace(/\\/g,"/");if(shouldIgnore(relativePath,ignoreRegexes)){continue}if(entry.isDirectory()){results.push(...collectFiles(fullPath,baseDir,ignoreRegexes))}else if(entry.isFile()||entry.isSymbolicLink()){results.push(fullPath)}}return results}// ==================== Zip 创建 ====================
|
|
64
64
|
/**
|
|
65
|
-
*
|
|
66
|
-
* @param {string[]} files -
|
|
67
|
-
* @param {string} rootFolderName -
|
|
68
|
-
* @param {string} outputPath -
|
|
65
|
+
* 根据收集到的文件创建 zip 压缩包。
|
|
66
|
+
* @param {string[]} files - 文件绝对路径数组
|
|
67
|
+
* @param {string} rootFolderName - zip 内的根文件夹名称
|
|
68
|
+
* @param {string} outputPath - 输出 zip 文件的绝对路径
|
|
69
69
|
* @returns {Promise<{fileCount: number, zipSize: number}>}
|
|
70
|
-
*/function createZip(files,rootFolderName,outputPath){return new Promise((resolve,reject)=>{const zipFile=new yazl.ZipFile;let fileCount=0;for(const filePath of files){const relativePath=path.relative(projectRoot,filePath).replace(/\\/g,"/");const zipEntryPath=`${rootFolderName}/${relativePath}`;zipFile.addFile(filePath,zipEntryPath);fileCount++}zipFile.end();//
|
|
71
|
-
const outputDir=path.dirname(outputPath);if(!fs.existsSync(outputDir)){fs.mkdirSync(outputDir,{recursive:true})}const writeStream=fs.createWriteStream(outputPath);writeStream.on("error",err=>{reject(err)});writeStream.on("close",()=>{const stats=fs.statSync(outputPath);resolve({fileCount,zipSize:stats.size})});zipFile.outputStream.pipe(writeStream)})}// ====================
|
|
70
|
+
*/function createZip(files,rootFolderName,outputPath){return new Promise((resolve,reject)=>{const zipFile=new yazl.ZipFile;let fileCount=0;for(const filePath of files){const relativePath=path.relative(projectRoot,filePath).replace(/\\/g,"/");const zipEntryPath=`${rootFolderName}/${relativePath}`;zipFile.addFile(filePath,zipEntryPath);fileCount++}zipFile.end();// 确保输出目录存在
|
|
71
|
+
const outputDir=path.dirname(outputPath);if(!fs.existsSync(outputDir)){fs.mkdirSync(outputDir,{recursive:true})}const writeStream=fs.createWriteStream(outputPath);writeStream.on("error",err=>{reject(err)});writeStream.on("close",()=>{const stats=fs.statSync(outputPath);resolve({fileCount,zipSize:stats.size})});zipFile.outputStream.pipe(writeStream)})}// ==================== 格式化工具 ====================
|
|
72
72
|
/**
|
|
73
|
-
*
|
|
73
|
+
* 将字节数格式化为可读字符串。
|
|
74
74
|
* @param {number} bytes
|
|
75
75
|
* @returns {string}
|
|
76
|
-
*/function formatSize(bytes){if(bytes<1024)return`${bytes} B`;if(bytes<1024*1024)return`${(bytes/1024).toFixed(2)} KB`;return`${(bytes/(1024*1024)).toFixed(2)} MB`}// ====================
|
|
77
|
-
async function main(){console.log("\x1B[36m\uD83D\uDCE6 playable-scripts
|
|
78
|
-
const appName=resolveAppName();console.log(`
|
|
79
|
-
const{patterns,sources}=parseIgnoreFiles();const ignoreRegexes=patterns.map(patternToRegex);if(patterns.length>0){const parts=[];if(sources.gitignore>0)parts.push(`${sources.gitignore}
|
|
80
|
-
console.log(`
|
|
81
|
-
const outDir=resolveOutputDir();const outputPath=path.resolve(projectRoot,outDir,`${appName}.zip`);console.log(`
|
|
76
|
+
*/function formatSize(bytes){if(bytes<1024)return`${bytes} B`;if(bytes<1024*1024)return`${(bytes/1024).toFixed(2)} KB`;return`${(bytes/(1024*1024)).toFixed(2)} MB`}// ==================== 主流程 ====================
|
|
77
|
+
async function main(){console.log("\x1B[36m\uD83D\uDCE6 playable-scripts \u6253\u5305\x1B[0m\n");// 1. 解析 appName
|
|
78
|
+
const appName=resolveAppName();console.log(` 应用名称: \x1b[1m${appName}\x1b[0m`);// 2. 解析忽略规则(合并 .gitignore + .playcraftignore)
|
|
79
|
+
const{patterns,sources}=parseIgnoreFiles();const ignoreRegexes=patterns.map(patternToRegex);if(patterns.length>0){const parts=[];if(sources.gitignore>0)parts.push(`${sources.gitignore} 条来自 .gitignore`);if(sources.playcraftignore>0)parts.push(`${sources.playcraftignore} 条来自 .playcraftignore`);console.log(` 忽略规则: \x1b[33m${patterns.length}\x1b[0m 条模式 (${parts.join(", ")})`)}// 3. 收集文件
|
|
80
|
+
console.log(` 正在扫描项目文件...`);const files=collectFiles(projectRoot,projectRoot,ignoreRegexes);if(files.length===0){console.error("\x1B[31m\u274C \u6CA1\u6709\u53EF\u6253\u5305\u7684\u6587\u4EF6\uFF01\u8BF7\u68C0\u67E5 .playcraftignore \u89C4\u5219\u3002\x1B[0m");process.exit(1)}console.log(` 文件数量: \x1b[32m${files.length}\x1b[0m 个待打包文件`);// 4. 创建 zip 压缩包
|
|
81
|
+
const outDir=resolveOutputDir();const outputPath=path.resolve(projectRoot,outDir,`${appName}.zip`);console.log(` 输出路径: ${outputPath}\n`);console.log(" \u6B63\u5728\u6253\u5305...");try{const{fileCount,zipSize}=await createZip(files,appName,outputPath);console.log(`\n\x1b[32m✅ 打包完成!\x1b[0m`);console.log(` 文件数: ${fileCount}`);console.log(` 大小: ${formatSize(zipSize)}`);console.log(` 输出: ${outputPath}`)}catch(err){console.error(`\n\x1b[31m❌ 打包失败: ${err.message}\x1b[0m`);process.exit(1)}}main();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";process.env.BABEL_ENV="development";process.env.NODE_ENV="development";const{runViteDev}=require("../../core/vite.dev.js");runViteDev();
|
package/core/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Configuration as WebpackConfig } from 'webpack';
|
|
2
2
|
import { WebpackPluginInstance } from 'webpack';
|
|
3
|
+
import { UserConfig as ViteUserConfig } from 'vite';
|
|
3
4
|
|
|
4
5
|
/** Supported file extensions for webpack resolution */
|
|
5
6
|
export declare const allowedExtensions: string[];
|
|
@@ -167,3 +168,18 @@ export declare class DebuggerInjectionPlugin implements WebpackPluginInstance {
|
|
|
167
168
|
export declare class ExitAPIInjectorPlugin implements WebpackPluginInstance {
|
|
168
169
|
apply(compiler: any): void;
|
|
169
170
|
}
|
|
171
|
+
|
|
172
|
+
/** Create Vite config for development */
|
|
173
|
+
export declare function makeViteDevConfig(
|
|
174
|
+
customOptions?: Partial<CLIOptions>,
|
|
175
|
+
customDefines?: Record<string, any>,
|
|
176
|
+
viteCustomConfig?: Partial<ViteUserConfig>
|
|
177
|
+
): ViteUserConfig;
|
|
178
|
+
|
|
179
|
+
/** Start Vite development server */
|
|
180
|
+
export declare function runViteDev(
|
|
181
|
+
viteConfig?: ViteUserConfig,
|
|
182
|
+
customOptions?: Partial<CLIOptions>,
|
|
183
|
+
customDefines?: Record<string, any>,
|
|
184
|
+
viteCustomConfig?: Partial<ViteUserConfig>
|
|
185
|
+
): Promise<void>;
|
package/core/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";const{allowedExtensions,webpackCommonConfig}=require("./webpack.common.js");const{parseArgvOptions}=require("./utils/parseArgvOptions.js");const{mergeOptions}=require("./utils/mergeOptions.js");const{options}=require("./options.js");const{DAPIInjectorPlugin}=require("./plugins/DAPIInjectorPlugin.js");const{DebuggerInjectionPlugin}=require("./plugins/DebuggerInjectionPlugin.js");const{ExitAPIInjectorPlugin}=require("./plugins/ExitAPIInjectorPlugin.js");const{runBuild,makeWebpackBuildConfig}=require("./webpack.build.js");const{runDev,makeWebpackDevConfig}=require("./webpack.dev.js");exports.mergeOptions=mergeOptions;exports.parseArgvOptions=parseArgvOptions;exports.options=options;exports.allowedExtensions=allowedExtensions;exports.webpackCommonConfig=webpackCommonConfig;exports.makeWebpackDevConfig=makeWebpackDevConfig;exports.makeWebpackBuildConfig=makeWebpackBuildConfig;exports.runDev=runDev;exports.runBuild=runBuild;exports.DAPIInjectorPlugin=DAPIInjectorPlugin;exports.ExitAPIInjectorPlugin=ExitAPIInjectorPlugin;exports.DebuggerInjectionPlugin=DebuggerInjectionPlugin;
|
|
1
|
+
"use strict";const{allowedExtensions,webpackCommonConfig}=require("./webpack.common.js");const{parseArgvOptions}=require("./utils/parseArgvOptions.js");const{mergeOptions}=require("./utils/mergeOptions.js");const{options}=require("./options.js");const{DAPIInjectorPlugin}=require("./plugins/DAPIInjectorPlugin.js");const{DebuggerInjectionPlugin}=require("./plugins/DebuggerInjectionPlugin.js");const{ExitAPIInjectorPlugin}=require("./plugins/ExitAPIInjectorPlugin.js");const{runBuild,makeWebpackBuildConfig}=require("./webpack.build.js");const{runDev,makeWebpackDevConfig}=require("./webpack.dev.js");const{runViteDev,makeViteDevConfig}=require("./vite.dev.js");exports.mergeOptions=mergeOptions;exports.parseArgvOptions=parseArgvOptions;exports.options=options;exports.allowedExtensions=allowedExtensions;exports.webpackCommonConfig=webpackCommonConfig;exports.makeWebpackDevConfig=makeWebpackDevConfig;exports.makeWebpackBuildConfig=makeWebpackBuildConfig;exports.runDev=runDev;exports.runBuild=runBuild;exports.DAPIInjectorPlugin=DAPIInjectorPlugin;exports.ExitAPIInjectorPlugin=ExitAPIInjectorPlugin;exports.DebuggerInjectionPlugin=DebuggerInjectionPlugin;exports.makeViteDevConfig=makeViteDevConfig;exports.runViteDev=runViteDev;
|
package/core/options.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";const{parseArgvOptions,allowedAdProtocols,allowedAdNetworks,allowedOrientations,allowedLanguages}=require("./utils/parseArgvOptions");const path=require("path");const fs=require("fs");const{name,version}=require("../package.json");const possibleOptions=[{name:"out-dir",alias:"outDir",defaultValue:"dist",hasValue:true,description:"Output directory for build files"},{name:"build-config",alias:"buildConfig",defaultValue:"build.json",hasValue:true,description:"Path to build.json configuration file"},{name:"ts-config",alias:"tsConfig",defaultValue:"tsconfig.json",hasValue:true,description:"For TypeScript projects, path to tsconfig.json file"},{name:"js-config",alias:"jsConfig",defaultValue:"jsconfig.json",hasValue:true,description:"For JavaScript projects, path to jsconfig.json file"},{name:"port",hasValue:true,defaultValue:3000,description:"Development server port number",parser:function(rawValue){const value=+rawValue;if(isNaN(value))throw new Error("--port should be a number");return value}},{name:"open",defaultValue:false,hasValue:false,description:"Open browser automatically when server starts"},{name:"protocol",hasValue:true,defaultValue:"none",description:"Ad protocol to use (none, mraid, or dapi)",parser:function(rawValue){if(!allowedAdProtocols.includes(rawValue)){throw new Error(`--protocol should have one of the value: ${allowedAdProtocols.join(", ")}`)}return rawValue}},{name:"network",defaultValue:"preview",hasValue:true,description:"Target Ad network",parser:function(rawValue){if(!allowedAdNetworks.includes(rawValue)){throw new Error(`--network should have one of the value: ${allowedAdNetworks.join(", ")}`)}return rawValue}},{name:"zip",defaultValue:false,hasValue:false,description:"Should the build be zipped? (only for some ad networks)"},{name:"is-channel-fold2zip",defaultValue:false,hasValue:false,description:"\u5F53\u542F\u7528\u65F6\uFF0C\u6784\u5EFA\u4EA7\u7269\u683C\u5F0F\u4E3A\u300C\u4E3B\u9898\u6587\u4EF6\u540D/\u6E20\u9053.zip\u300D"},{name:"dev",hasValue:true,description:"Enable development mode (true/false)",parser:function(rawValue){if(!(rawValue==="true"||rawValue==="false"))throw new Error("--dev should have either true or false value");return rawValue==="true"}},{name:"filename",defaultValue:"{app}_{name}_{version}_{date}_{language}_{network}",hasValue:true,description:"Specifies the build filename template"},{name:"app",defaultValue:"AppName",hasValue:true,description:"Specifies the application name used in build filename and APP define"},{name:"name",defaultValue:"ConceptName",hasValue:true,description:"Specifies the concept name used in build filename and NAME define"},{name:"version",defaultValue:"v1",hasValue:true,description:"Specifies the version name used in build filename and VERSION define"},{name:"language",defaultValue:"en",hasValue:true,description:"Specifies the language of the build used in LANGUAGE define",parser:function(rawValue){if(!allowedLanguages.includes(rawValue)){throw new Error(`--platform should have one of the value: ${allowedLanguages.join(", ")}`)}return rawValue}},{name:"orientation",defaultValue:"both",hasValue:true,description:"Specifies the ad orientation used in ORIENTATION define and some network specific configurations",parser:function(rawValue){if(!allowedOrientations.includes(rawValue)){throw new Error(`--orientation should have one of the value: ${allowedOrientations.join(", ")}`)}return rawValue}},{name:"google-play-url",alias:"googlePlayUrl",defaultValue:"https://play.google.com/store/games",hasValue:true,description:"Google Play Store URL for the app"},{name:"app-store-url",alias:"appStoreUrl",defaultValue:"https://www.apple.com/app-store/",hasValue:true,description:"App Store URL for the app"},{name:"skip-recommended-meta",alias:"skipRecommendedMeta",hasValue:false,description:"Don't inject recommended for playable ads META tags"},{name:"debugger",hasValue:true,description:"URL of debugger script to inject into code"},{name:"obfuscate-level",alias:"obfuscateLevel",defaultValue:4,hasValue:true,description:"Code obfuscation level: 1=terser only, 2=obfuscator basic(default), 3=obfuscator+dead code, 4=maximum obfuscation",parser:function(rawValue){const value=+rawValue;if(isNaN(value)||value<1||value>4){throw new Error("--obfuscate-level should be a number between 1 and 4")}return value}},{name:"fflate-compression",alias:"fflateCompression",defaultValue:false,hasValue:true,description:"Enable fflate compression for HTML output (level 9, production only)",parser:function(rawValue){// 支持 true/false 字符串和布尔值
|
|
2
|
-
if(rawValue==="true"||rawValue===true||rawValue==="1")return true;if(rawValue==="false"||rawValue===false||rawValue==="0")return false;return Boolean(rawValue)}},{name:"exclude-compress-dirs",alias:"excludeCompressDirs",defaultValue:[],hasValue:true,description:"Directories to exclude from compression (comma-separated list of paths)",parser:function(rawValue){if(!rawValue)return[];return rawValue.split(",").map(dir=>dir.trim()).filter(Boolean)}}];/** @type {import('./index').CLIOptions} */const options=parseArgvOptions(possibleOptions)
|
|
2
|
+
if(rawValue==="true"||rawValue===true||rawValue==="1")return true;if(rawValue==="false"||rawValue===false||rawValue==="0")return false;return Boolean(rawValue)}},{name:"exclude-compress-dirs",alias:"excludeCompressDirs",defaultValue:[],hasValue:true,description:"Directories to exclude from compression (comma-separated list of paths)",parser:function(rawValue){if(!rawValue)return[];return rawValue.split(",").map(dir=>dir.trim()).filter(Boolean)}}];/** @type {import('./index').CLIOptions} */const options=parseArgvOptions(possibleOptions);// Extract CLI explicit keys set, then remove it from options
|
|
3
|
+
const cliExplicitKeys=options._cliExplicitKeys||new Set;delete options._cliExplicitKeys;options.defines={};options.compilation={};options.adNetworkNames={};// ==================== 配置文件加载 ====================
|
|
3
4
|
//
|
|
4
5
|
// 优先级(从低到高):
|
|
5
6
|
// 1. builds.config.js (新配置文件,优先读取)
|
|
@@ -11,15 +12,15 @@ if(rawValue==="true"||rawValue===true||rawValue==="1")return true;if(rawValue===
|
|
|
11
12
|
// - build.json: google_play_url → googlePlayUrl, app_store_url → appStoreUrl
|
|
12
13
|
// 1️⃣ 先尝试读取 builds.config.js
|
|
13
14
|
const buildsConfigPath=path.resolve("builds.config.js");if(fs.existsSync(buildsConfigPath)){try{// 清除 require 缓存,确保读取最新配置
|
|
14
|
-
delete require.cache[buildsConfigPath];const buildsConfig=require(buildsConfigPath);//
|
|
15
|
-
if(buildsConfig.googlePlayUrl){options.googlePlayUrl=buildsConfig.googlePlayUrl}if(buildsConfig.appStoreUrl){options.appStoreUrl=buildsConfig.appStoreUrl}// 读取其他特殊配置
|
|
15
|
+
delete require.cache[buildsConfigPath];const buildsConfig=require(buildsConfigPath);// 读取商店链接配置(respect CLI args priority)
|
|
16
|
+
if(buildsConfig.googlePlayUrl&&!cliExplicitKeys.has("googlePlayUrl")){options.googlePlayUrl=buildsConfig.googlePlayUrl}if(buildsConfig.appStoreUrl&&!cliExplicitKeys.has("appStoreUrl")){options.appStoreUrl=buildsConfig.appStoreUrl}// 读取其他特殊配置
|
|
16
17
|
if(buildsConfig.defines)Object.assign(options.defines,buildsConfig.defines);if(buildsConfig.compilation)Object.assign(options.compilation,buildsConfig.compilation);if(buildsConfig.adNetworkNames)Object.assign(options.adNetworkNames,buildsConfig.adNetworkNames);// 读取所有通用配置字段(如果在 builds.config.js 中定义了)
|
|
17
18
|
for(const key in buildsConfig){// 跳过已处理的特殊字段和批量构建专用字段
|
|
18
19
|
if(["googlePlayUrl","appStoreUrl","defines","compilation","adNetworkNames","title","appName","projectCode","channels","themes","naming","build","hooks","cosConfig"].includes(key)){continue}// 查找对应的配置项定义
|
|
19
|
-
const possibleOption=possibleOptions.find(e=>e.alias===key||e.name===key);if(possibleOption&&buildsConfig[key]!==undefined){
|
|
20
|
-
|
|
21
|
-
else if(key==="app_store_url")options.appStoreUrl=customOptions.app_store_url
|
|
22
|
-
|
|
20
|
+
const possibleOption=possibleOptions.find(e=>e.alias===key||e.name===key);if(possibleOption&&buildsConfig[key]!==undefined){// Only override if not explicitly set by CLI args
|
|
21
|
+
const optionKey=possibleOption.alias||possibleOption.name;if(!cliExplicitKeys.has(optionKey)){options[optionKey]=buildsConfig[key]}}}}catch(err){console.log("\x1B[33m\u26A0\uFE0F \u8BFB\u53D6 builds.config.js \u5931\u8D25: "+err.message+"\x1B[0m")}}// 2️⃣ 再尝试读取 build.json(会覆盖 builds.config.js 中的相同字段)
|
|
22
|
+
try{const fileData=fs.readFileSync(path.resolve(options["buildConfig"]),"utf8");try{const customOptions=JSON.parse(fileData);for(let key in customOptions){if(key==="defines")Object.assign(options.defines,customOptions[key]);else if(key==="compilation")Object.assign(options.compilation,customOptions[key]);else if(key==="adNetworkNames")Object.assign(options.adNetworkNames,customOptions[key]);else if(key==="google_play_url"&&!cliExplicitKeys.has("googlePlayUrl"))options.googlePlayUrl=customOptions.google_play_url;else if(key==="app_store_url"&&!cliExplicitKeys.has("appStoreUrl"))options.appStoreUrl=customOptions.app_store_url;else{const possibleOption=possibleOptions.find(e=>e.alias===key||e.name===key);if(possibleOption){const optionKey=possibleOption.alias||possibleOption.name;// Only override if not explicitly set by CLI args
|
|
23
|
+
if(!cliExplicitKeys.has(optionKey)){options[optionKey]=customOptions[key]}}}}}catch(err){console.log("\x1B[31mBuild config parsing error: "+err.message+"\x1B[0m")}}catch(err){// build.json 不存在是正常的(使用 builds.config.js)
|
|
23
24
|
}// 环境变量覆盖(用于批量构建时避免竞态条件)
|
|
24
25
|
// 批量构建时通过 PLAYABLE_FILENAME 环境变量传递 filename,优先级最高
|
|
25
26
|
if(process.env.PLAYABLE_FILENAME){options.filename=process.env.PLAYABLE_FILENAME}console.log(`${name} v${version}`);console.log("Options:",options);exports.options=options;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Parses command line arguments based on provided options configuration
|
|
3
3
|
* @param {import('../index').CLIOptionConfig[]} posiibleOptions - Array of possible options to parse
|
|
4
4
|
* @returns {Record<string, any>} Parsed options object with values from command line arguments
|
|
5
|
-
*/exports.parseArgvOptions=function parseArgvOptions(posiibleOptions){const argvOptions={}
|
|
5
|
+
*/exports.parseArgvOptions=function parseArgvOptions(posiibleOptions){const argvOptions={};// Track which options were explicitly set via CLI args
|
|
6
|
+
const cliExplicitKeys=new Set;for(let possibleOption of posiibleOptions){if(possibleOption.defaultValue!==undefined)argvOptions[possibleOption.alias||possibleOption.name]=possibleOption.defaultValue}let adNetwork=null;for(let i=2;i<process.argv.length;i++){let key=process.argv[i];let inlineValue=null;// 支持 --key=value 格式
|
|
6
7
|
if(key.includes("=")){const eqIndex=key.indexOf("=");inlineValue=key.slice(eqIndex+1);key=key.slice(0,eqIndex)}const allowedOption=posiibleOptions.find(e=>"--"+e.name===key);if(allowedOption){let value=true;if(allowedOption.hasValue){// 优先使用内联值(--key=value),否则使用下一个参数
|
|
7
|
-
value=inlineValue!==null?inlineValue:process.argv[++i];if(allowedOption.parser)value=allowedOption.parser(value)}
|
|
8
|
+
value=inlineValue!==null?inlineValue:process.argv[++i];if(allowedOption.parser)value=allowedOption.parser(value)}const optionKey=allowedOption.alias||allowedOption.name;argvOptions[optionKey]=value;cliExplicitKeys.add(optionKey)}else if(!adNetwork)adNetwork=key}if(posiibleOptions.find(e=>e.name==="network")){if(adNetwork&&allowedAdNetworks.includes(adNetwork)){argvOptions["network"]=adNetwork}if(posiibleOptions.find(e=>e.name==="protocol")){if(mraidPartners.includes(argvOptions["network"])&&argvOptions["protocol"]==="none"){argvOptions["protocol"]="mraid"}}}argvOptions._cliExplicitKeys=cliExplicitKeys;return argvOptions};exports.allowedAdNetworks=allowedAdNetworks;exports.allowedAdProtocols=allowedAdProtocols;exports.mraidPartners=mraidPartners;exports.allowedLanguages=allowedLanguages;exports.allowedOrientations=allowedOrientations;
|
|
@@ -1,78 +1,78 @@
|
|
|
1
|
-
"use strict";const fs=require("fs");const path=require("path");const JSON5=require("json5");const Ajv=require("ajv");const SCHEMA_BACKUP_SUFFIX=".backup";const SCHEMA_BACKUP_COMMENT="// [
|
|
2
|
-
*
|
|
1
|
+
"use strict";const fs=require("fs");const path=require("path");const JSON5=require("json5");const Ajv=require("ajv");const SCHEMA_BACKUP_SUFFIX=".backup";const SCHEMA_BACKUP_COMMENT="// [\u81EA\u52A8\u751F\u6210\u7684\u5907\u4EFD] \u6B64\u6587\u4EF6\u7531\u6253\u5305\u6D41\u7A0B\u521B\u5EFA\u3002\n"+"// \u5982\u679C\u6253\u5305\u4E2D\u65AD\uFF0C\u53EF\u4EE5\u79FB\u9664 \".backup\" \u540E\u7F00\uFF0C\n"+"// \u5E76\u7528\u6B64\u6587\u4EF6\u66FF\u6362\u539F\u59CB\u7684 \"theme.schema.json5\" \u6765\u6062\u590D\u3002\n";/**
|
|
2
|
+
* 递归提取 JSON Schema 定义中的 `default` 值。
|
|
3
3
|
*
|
|
4
|
-
* -
|
|
5
|
-
* -
|
|
4
|
+
* - 对于 "object" 类型:递归遍历 `properties` 并构建嵌套对象。
|
|
5
|
+
* - 对于基本类型:如果存在 `default` 值则返回。
|
|
6
6
|
*
|
|
7
|
-
* @param {object} schema -
|
|
8
|
-
* @returns {*}
|
|
9
|
-
*/function extractDefaultsFromSchema(schema){if(!schema||typeof schema!=="object")return undefined;if(schema.type==="object"&&schema.properties){const result={};for(const[key,propSchema]of Object.entries(schema.properties)){const val=extractDefaultsFromSchema(propSchema);if(val!==undefined){result[key]=val}}return Object.keys(result).length>0?result:undefined}//
|
|
7
|
+
* @param {object} schema - JSON Schema(或子 schema)对象
|
|
8
|
+
* @returns {*} 提取的默认值树,如果没有则返回 undefined
|
|
9
|
+
*/function extractDefaultsFromSchema(schema){if(!schema||typeof schema!=="object")return undefined;if(schema.type==="object"&&schema.properties){const result={};for(const[key,propSchema]of Object.entries(schema.properties)){const val=extractDefaultsFromSchema(propSchema);if(val!==undefined){result[key]=val}}return Object.keys(result).length>0?result:undefined}// 基本类型或数组类型,带有默认值
|
|
10
10
|
if("default"in schema){return schema.default}return undefined}/**
|
|
11
|
-
*
|
|
11
|
+
* 根据 schema 校验指定主题的主题数据。
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
* src/theme/theme.schema.json5 – JSON Schema
|
|
15
|
-
* src/theme/{themeName}/theme.data.json5 –
|
|
13
|
+
* 项目中预期的文件结构:
|
|
14
|
+
* src/theme/theme.schema.json5 – JSON Schema(JSON5 格式)
|
|
15
|
+
* src/theme/{themeName}/theme.data.json5 – 各主题的数据
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* 通过 extractDefaultsFromSchema 从 schema 的 `default` 字段提取默认值,
|
|
18
|
+
* 然后与主题数据深度合并(主题数据优先)后再进行校验。
|
|
19
19
|
*
|
|
20
|
-
* @param {string} themeName -
|
|
21
|
-
* @param {string} projectRoot -
|
|
22
|
-
* @param {object} [themeConfig] -
|
|
20
|
+
* @param {string} themeName - 主题目录名称
|
|
21
|
+
* @param {string} projectRoot - 项目根目录的绝对路径
|
|
22
|
+
* @param {object} [themeConfig] - 可选的主题配置(builds.config.js 中的 themes 部分)
|
|
23
23
|
* @returns {{ defaultData: object, errors: string[] | null }}
|
|
24
|
-
* defaultData –
|
|
25
|
-
* errors – null
|
|
26
|
-
*/function validateThemeData(themeName,projectRoot,themeConfig){const sourceDir=themeConfig&&themeConfig.sourceDir||"src/theme";const themeBaseDir=path.join(projectRoot,sourceDir);// ---
|
|
27
|
-
const schemaPath=path.join(themeBaseDir,"theme.schema.json5");const dataPath=path.join(themeBaseDir,themeName,"theme.data.json5");// ---
|
|
28
|
-
if(!fs.existsSync(schemaPath)){return{defaultData:null,errors:[
|
|
29
|
-
const defaultData=extractDefaultsFromSchema(schema)||{};let themeData={};if(fs.existsSync(dataPath)){themeData=JSON5.parse(fs.readFileSync(dataPath,"utf8"))}//
|
|
30
|
-
const merged=deepMerge(defaultData,themeData);// ---
|
|
24
|
+
* defaultData – 从 schema 提取的默认值
|
|
25
|
+
* errors – 校验通过为 null,否则为错误信息数组
|
|
26
|
+
*/function validateThemeData(themeName,projectRoot,themeConfig){const sourceDir=themeConfig&&themeConfig.sourceDir||"src/theme";const themeBaseDir=path.join(projectRoot,sourceDir);// --- 解析文件路径 ---
|
|
27
|
+
const schemaPath=path.join(themeBaseDir,"theme.schema.json5");const dataPath=path.join(themeBaseDir,themeName,"theme.data.json5");// --- 读取并解析文件 ---
|
|
28
|
+
if(!fs.existsSync(schemaPath)){return{defaultData:null,errors:[`未找到 Schema 文件: ${schemaPath}`]}}const schema=JSON5.parse(fs.readFileSync(schemaPath,"utf8"));// 从 schema 提取默认值
|
|
29
|
+
const defaultData=extractDefaultsFromSchema(schema)||{};let themeData={};if(fs.existsSync(dataPath)){themeData=JSON5.parse(fs.readFileSync(dataPath,"utf8"))}// 深度合并(主题数据覆盖 schema 默认值),然后校验
|
|
30
|
+
const merged=deepMerge(defaultData,themeData);// --- 根据 schema 校验合并后的数据 ---
|
|
31
31
|
const ajv=new Ajv({allErrors:true,strict:false});const validate=ajv.compile(schema);const valid=validate(merged);if(!valid){const errors=validate.errors.map(err=>{const field=err.instancePath||"(root)";return`${field} ${err.message}`});return{defaultData,errors}}return{defaultData,errors:null}}/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
32
|
+
* 深度合并两个普通对象。`override` 中的值优先。
|
|
33
|
+
* 数组会被整体替换(不会拼接)。
|
|
34
34
|
*
|
|
35
35
|
* @param {object} base
|
|
36
36
|
* @param {object} override
|
|
37
37
|
* @returns {object}
|
|
38
38
|
*/function deepMerge(base,override){const result={...base};for(const key of Object.keys(override)){const baseVal=base[key];const overVal=override[key];if(overVal!==null&&typeof overVal==="object"&&!Array.isArray(overVal)&&baseVal!==null&&typeof baseVal==="object"&&!Array.isArray(baseVal)){result[key]=deepMerge(baseVal,overVal)}else{result[key]=overVal}}return result}/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
39
|
+
* 备份原始的 theme.schema.json5 文件。
|
|
40
|
+
* 备份文件包含注释头,说明如何手动恢复。
|
|
41
41
|
*
|
|
42
|
-
* @param {string} schemaPath -
|
|
43
|
-
* @returns {string}
|
|
42
|
+
* @param {string} schemaPath - theme.schema.json5 的绝对路径
|
|
43
|
+
* @returns {string} 备份文件路径
|
|
44
44
|
*/function backupSchema(schemaPath){const backupPath=schemaPath+SCHEMA_BACKUP_SUFFIX;const originalContent=fs.readFileSync(schemaPath,"utf8");fs.writeFileSync(backupPath,SCHEMA_BACKUP_COMMENT+originalContent,"utf8");return backupPath}/**
|
|
45
|
-
*
|
|
45
|
+
* 从备份文件恢复 theme.schema.json5 并删除备份。
|
|
46
46
|
*
|
|
47
|
-
* @param {string} schemaPath -
|
|
48
|
-
*/function restoreSchema(schemaPath){const backupPath=schemaPath+SCHEMA_BACKUP_SUFFIX;if(!fs.existsSync(backupPath))return;const backupContent=fs.readFileSync(backupPath,"utf8");//
|
|
47
|
+
* @param {string} schemaPath - theme.schema.json5 的绝对路径
|
|
48
|
+
*/function restoreSchema(schemaPath){const backupPath=schemaPath+SCHEMA_BACKUP_SUFFIX;if(!fs.existsSync(backupPath))return;const backupContent=fs.readFileSync(backupPath,"utf8");// 去除自动生成的注释头(以 "//" 开头的行)
|
|
49
49
|
const lines=backupContent.split("\n");const contentStart=lines.findIndex(line=>!line.startsWith("//"));const originalContent=lines.slice(contentStart>=0?contentStart:0).join("\n");fs.writeFileSync(schemaPath,originalContent,"utf8");fs.unlinkSync(backupPath)}/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
50
|
+
* 用合并后的数据(schema 默认值 + 主题覆盖值)覆写 theme.schema.json5,
|
|
51
|
+
* 以便打包工具在构建时可以直接使用。
|
|
52
52
|
*
|
|
53
|
-
* @param {string} schemaPath -
|
|
54
|
-
* @param {object} mergedData -
|
|
53
|
+
* @param {string} schemaPath - theme.schema.json5 的绝对路径
|
|
54
|
+
* @param {object} mergedData - 要写入的合并后的主题数据
|
|
55
55
|
*/function overwriteSchemaWithMerged(schemaPath,mergedData){const content=JSON.stringify(mergedData,null,2);fs.writeFileSync(schemaPath,content,"utf8")}/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
56
|
+
* 执行主题校验并输出标准日志。
|
|
57
|
+
* 单次构建(build.js)和批量构建(batch-build.js)共用此函数。
|
|
58
58
|
*
|
|
59
|
-
*
|
|
60
|
-
* 1.
|
|
61
|
-
* 2.
|
|
62
|
-
* 3.
|
|
63
|
-
* 4.
|
|
59
|
+
* 流程:
|
|
60
|
+
* 1. 根据 theme.schema.json5 校验 theme.data.json5
|
|
61
|
+
* 2. 备份 theme.schema.json5(包含恢复说明)
|
|
62
|
+
* 3. 通过 extractDefaultsFromSchema 从 schema 提取默认值
|
|
63
|
+
* 4. 用默认数据覆写 theme.schema.json5 以供构建使用
|
|
64
64
|
*
|
|
65
|
-
*
|
|
65
|
+
* 构建完成后(无论成功或失败),调用 restoreSchema() 恢复原始文件。
|
|
66
66
|
*
|
|
67
|
-
* @param {string} themeName -
|
|
68
|
-
* @param {string} projectRoot -
|
|
69
|
-
* @param {object} [themeConfig] -
|
|
67
|
+
* @param {string} themeName - 主题目录名称
|
|
68
|
+
* @param {string} projectRoot - 项目根目录的绝对路径
|
|
69
|
+
* @param {object} [themeConfig] - 可选的主题配置(builds.config.js 中的 themes 部分)
|
|
70
70
|
* @param {object} [options]
|
|
71
|
-
* @param {function} [options.log] -
|
|
71
|
+
* @param {function} [options.log] - 自定义日志函数(message, level)。默认使用 console.log。
|
|
72
72
|
* @returns {{ valid: boolean, errors: string[]|null, schemaPath: string|null }}
|
|
73
|
-
* valid –
|
|
74
|
-
* errors – null
|
|
75
|
-
* schemaPath –
|
|
76
|
-
*/function runThemeValidation(themeName,projectRoot,themeConfig,options){const sourceDir=themeConfig&&themeConfig.sourceDir||"src/theme";const themeBaseDir=path.join(projectRoot,sourceDir);const schemaPath=path.join(themeBaseDir,"theme.schema.json5");//
|
|
77
|
-
if(!fs.existsSync(schemaPath)){return{valid:true,errors:null,schemaPath:null}}const logFn=options&&options.log||function defaultLog(msg,level){const colors={info:"\x1B[36m",success:"\x1B[32m",error:"\x1B[31m"};const reset="\x1B[0m";const color=colors[level]||"";console.log(`${color}${msg}${reset}`)};logFn(`校验主题数据: ${themeName}`,"info");const{defaultData,errors}=validateThemeData(themeName,projectRoot,themeConfig);if(errors){logFn(`主题 "${themeName}" 数据校验失败:`,"error");errors.forEach(e=>logFn(` - ${e}`,"error"));return{valid:false,errors,schemaPath}}logFn(`主题 "${themeName}" 数据校验通过`,"success");//
|
|
73
|
+
* valid – schema 文件不存在(跳过)或校验通过时为 true
|
|
74
|
+
* errors – 校验通过为 null,否则为错误信息数组
|
|
75
|
+
* schemaPath – theme.schema.json5 的绝对路径(用于 restoreSchema 调用),或 null
|
|
76
|
+
*/function runThemeValidation(themeName,projectRoot,themeConfig,options){const sourceDir=themeConfig&&themeConfig.sourceDir||"src/theme";const themeBaseDir=path.join(projectRoot,sourceDir);const schemaPath=path.join(themeBaseDir,"theme.schema.json5");// 没有 schema 文件 → 完全跳过校验
|
|
77
|
+
if(!fs.existsSync(schemaPath)){return{valid:true,errors:null,schemaPath:null}}const logFn=options&&options.log||function defaultLog(msg,level){const colors={info:"\x1B[36m",success:"\x1B[32m",error:"\x1B[31m"};const reset="\x1B[0m";const color=colors[level]||"";console.log(`${color}${msg}${reset}`)};logFn(`校验主题数据: ${themeName}`,"info");const{defaultData,errors}=validateThemeData(themeName,projectRoot,themeConfig);if(errors){logFn(`主题 "${themeName}" 数据校验失败:`,"error");errors.forEach(e=>logFn(` - ${e}`,"error"));return{valid:false,errors,schemaPath}}logFn(`主题 "${themeName}" 数据校验通过`,"success");// 备份原始 schema,然后用默认数据覆写以供构建使用
|
|
78
78
|
backupSchema(schemaPath);overwriteSchemaWithMerged(schemaPath,defaultData);logFn(`已备份 schema 并写入默认数据`,"info");return{valid:true,errors:null,schemaPath}}module.exports={validateThemeData,deepMerge,extractDefaultsFromSchema,runThemeValidation,backupSchema,restoreSchema,overwriteSchemaWithMerged};
|
package/core/vite.dev.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";const path=require("path");const fs=require("fs");const{options}=require("./options.js");const{mergeOptions}=require("./utils/mergeOptions.js");const{buildDefines}=require("./utils/buildDefines.js");const{logOptions}=require("./utils/logOptions.js");/**
|
|
2
|
+
* 创建 Vite 开发配置
|
|
3
|
+
* @param {Partial<import('./index').CLIOptions>} [customOptions] - 自定义选项,会与默认选项合并
|
|
4
|
+
* @param {Record<string, any>} [customDefines] - 额外的 define 常量
|
|
5
|
+
* @param {import('vite').UserConfig} [viteCustomConfig] - 自定义 Vite 配置,会与默认配置合并
|
|
6
|
+
* @returns {import('vite').UserConfig} 最终的 Vite 开发配置
|
|
7
|
+
*/function makeViteDevConfig(customOptions,customDefines,viteCustomConfig){const devOptions=mergeOptions(options,customOptions);logOptions(devOptions);customDefines=customDefines||{};viteCustomConfig=viteCustomConfig||{};// 构建 define 常量(复用 webpack 的 buildDefines 逻辑)
|
|
8
|
+
const defines={...buildDefines(),...devOptions.defines,__DEV__:JSON.stringify(devOptions["dev"]===undefined?true:devOptions["dev"]),...customDefines};// 解析 tsconfig/jsconfig 路径别名
|
|
9
|
+
const tsConfigPath=path.resolve(devOptions.tsConfig||"tsconfig.json");const jsConfigPath=path.resolve(devOptions.jsConfig||"jsconfig.json");let aliasConfig={};// 尝试从 tsconfig/jsconfig 中读取 paths 配置
|
|
10
|
+
let configFilePath=null;if(fs.existsSync(tsConfigPath)){configFilePath=tsConfigPath}else if(fs.existsSync(jsConfigPath)){configFilePath=jsConfigPath}if(configFilePath){try{var _config$compilerOptio;const JSON5=require("json5");const configContent=fs.readFileSync(configFilePath,"utf-8");const config=JSON5.parse(configContent);const paths=((_config$compilerOptio=config.compilerOptions)===null||_config$compilerOptio===void 0?void 0:_config$compilerOptio.paths)||{};for(const[alias,targets]of Object.entries(paths)){var _targets$;// 将 tsconfig paths 格式 "foo/*" -> ["src/foo/*"] 转换为 vite alias
|
|
11
|
+
const cleanAlias=alias.replace("/*","");const cleanTarget=((_targets$=targets[0])===null||_targets$===void 0?void 0:_targets$.replace("/*",""))||"";if(cleanAlias&&cleanTarget){aliasConfig[cleanAlias]=path.resolve(cleanTarget)}}}catch(err){console.log("\x1B[33m\u26A0\uFE0F \u8BFB\u53D6\u8DEF\u5F84\u914D\u7F6E\u5931\u8D25: "+err.message+"\x1B[0m")}}// 默认别名
|
|
12
|
+
aliasConfig={assets:path.resolve("assets"),"@":path.resolve("src"),...aliasConfig};// JSON5 插件
|
|
13
|
+
function json5Plugin(){return{name:"vite-plugin-json5",transform(code,id){if(id.endsWith(".json5")){const JSON5=require("json5");const raw=fs.readFileSync(id,"utf-8");const parsed=JSON5.parse(raw);return{code:`export default ${JSON.stringify(parsed)};`,map:null}}}}}// 资源内联插件(模拟 webpack 的 asset/inline 行为)
|
|
14
|
+
function assetInlinePlugin(){return{name:"vite-plugin-asset-inline",enforce:"pre",transform(code,id){// .atlas 文件 -> base64 data URL
|
|
15
|
+
if(id.endsWith(".atlas")){const content=fs.readFileSync(id);const base64=content.toString("base64");return{code:`export default "data:text/atlas;base64,${base64}";`,map:null}}// .fnt 文件 -> base64 data URL
|
|
16
|
+
if(id.endsWith(".fnt")){const content=fs.readFileSync(id);const base64=content.toString("base64");return{code:`export default "data:text/plain;base64,${base64}";`,map:null}}// .xml 文件 -> 原始文本
|
|
17
|
+
if(id.endsWith(".xml")){const raw=fs.readFileSync(id,"utf-8");return{code:`export default ${JSON.stringify(raw)};`,map:null}}}}}/** @type {import('vite').UserConfig} */const viteConfig={root:".",resolve:{alias:aliasConfig},plugins:[json5Plugin(),assetInlinePlugin()],define:defines,server:{port:devOptions["port"]||3000,open:devOptions["open"]||false},build:{outDir:devOptions["outDir"]||"dist",sourcemap:true}};// 合并自定义配置
|
|
18
|
+
if(viteCustomConfig){// 简单合并:自定义配置中的字段会覆盖默认配置
|
|
19
|
+
const{plugins:customPlugins,define:customDefine,resolve:customResolve,server:customServer,build:customBuild,...rest}=viteCustomConfig;if(customPlugins){viteConfig.plugins=[...viteConfig.plugins,...customPlugins]}if(customDefine){viteConfig.define={...viteConfig.define,...customDefine}}if(customResolve){viteConfig.resolve={...viteConfig.resolve,...customResolve,alias:{...viteConfig.resolve.alias,...(customResolve.alias||{})}}}if(customServer){viteConfig.server={...viteConfig.server,...customServer}}if(customBuild){viteConfig.build={...viteConfig.build,...customBuild}}Object.assign(viteConfig,rest)}return viteConfig}/**
|
|
20
|
+
* 启动 Vite 开发服务器
|
|
21
|
+
* @param {import('vite').UserConfig} [viteConfig] - Vite 配置,不传则自动创建
|
|
22
|
+
* @param {Partial<import('./index').CLIOptions>} [customOptions] - 自定义选项
|
|
23
|
+
* @param {Record<string, any>} [customDefines] - 额外的 define 常量
|
|
24
|
+
* @param {import('vite').UserConfig} [viteCustomConfig] - 自定义 Vite 配置
|
|
25
|
+
*/async function runViteDev(viteConfig,customOptions,customDefines,viteCustomConfig){if(!viteConfig){viteConfig=makeViteDevConfig(customOptions,customDefines,viteCustomConfig)}const{createServer}=require("vite");const server=await createServer(viteConfig);await server.listen();server.printUrls()}exports.makeViteDevConfig=makeViteDevConfig;exports.runViteDev=runViteDev;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcraft/devkit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "HTML5 Playable Ads 构建工具,支持多广告渠道打包",
|
|
5
5
|
"main": "core/index.js",
|
|
6
6
|
"module": "core/index.js",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"tencentcloud-sdk-nodejs-cdn": "^4.1.161",
|
|
43
43
|
"terser-webpack-plugin": "^5.3.10",
|
|
44
44
|
"tsconfig-paths-webpack-plugin": "^4.2.0",
|
|
45
|
+
"vite": "^6.4.1",
|
|
45
46
|
"vue": "^3.4.0",
|
|
46
47
|
"vue-loader": "^17.4.2",
|
|
47
48
|
"webpack": "^5.91.0",
|