@playcraft/devkit 1.0.7 → 1.0.8

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.
@@ -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+"\".")}
@@ -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
- * Detect the current theme name from the theme entry file.
3
- * Supports patterns like:
2
+ * 从主题入口文件中检测当前主题名称。
3
+ * 支持以下模式:
4
4
  * export { default } from './themeName';
5
5
  * import Theme from './themeName';
6
- * export { default } from "./themeName";
7
- */function detectCurrentTheme(projectRoot){// Try to load builds.config.js for theme config
8
- 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){// ignore
9
- }}const entryPath=path.join(projectRoot,themeConfig.entryFile);if(!fs.existsSync(entryPath)){return{themeName:null,themeConfig}}const content=fs.readFileSync(entryPath,"utf8");// Match: export { default } from './themeName' or import ... from './themeName'
10
- const match=content.match(/from\s+['"]\.\/([^'"]+)['"]/);if(match){return{themeName:match[1],themeConfig}}return{themeName:null,themeConfig}}// --- Main ---
11
- 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){// Restore schema on validation failure (backup may have been created)
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)});
@@ -1,81 +1,81 @@
1
- "use strict";const fs=require("fs");const path=require("path");const yazl=require("yazl");// ==================== Configuration ====================
1
+ "use strict";const fs=require("fs");const path=require("path");const yazl=require("yazl");// ==================== 配置 ====================
2
2
  const projectRoot=process.cwd();/**
3
- * Resolve the appName from builds.config.js, build.json, or CLI args.
4
- * Priority: CLI --app-name > builds.config.js > build.json > folder name
3
+ * 解析 appName,依次从 builds.config.jsbuild.json CLI 参数中获取。
4
+ * 优先级: CLI --app-name > builds.config.js > build.json > 文件夹名称
5
5
  * @returns {string}
6
- */function resolveAppName(){// 1. CLI argument: --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 or 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){// ignore
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){// ignore
11
- }}// 4. Fallback: project folder name
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
- * Resolve the output directory from CLI args.
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"}// ==================== Ignore Pattern Parsing ====================
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
- * Parse a single ignore file and return an array of normalized patterns.
18
- * @param {string} filePath - Absolute path to the ignore file
19
- * @returns {string[]} Array of normalized ignore patterns
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();// Skip empty lines and comments
21
- if(!line||line.startsWith("#"))continue;// Warn about negation patterns (not supported)
22
- if(line.startsWith("!")){console.log(`\x1b[33m⚠️ Negation pattern not supported, skipping: ${line}\x1b[0m`);continue}// Normalize:
23
- // - remove trailing slashes for directory matching
24
- // - remove leading ./ (gitignore treats ./foo same as foo)
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
- * Parse .gitignore and .playcraftignore files, merge and deduplicate patterns.
27
- * .gitignore provides base ignore rules, .playcraftignore provides additional ones.
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 No .gitignore or .playcraftignore file found, packing all files.\x1B[0m");return{patterns:[],sources:{gitignore:0,playcraftignore:0}}}// Merge and deduplicate (preserve order, .gitignore first)
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
- * Convert a gitignore-like pattern to a RegExp.
31
+ * gitignore 风格的模式转换为正则表达式。
32
32
  *
33
- * Gitignore semantics:
34
- * - Pattern without `/` → matches against the basename anywhere in the tree
35
- * - Pattern with `/` (e.g. `scripts/foo`) → anchored to project root
36
- * - Leading `/` means "root only" and is stripped before regex conversion
37
- * - Leading `./` is treated the same as `/` (already stripped in parse phase)
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;// Leading / means root-anchored; strip it for regex building
42
- if(p.startsWith("/")){p=p.slice(1);anchored=true}// If the pattern contains a /, it is also root-anchored
43
- if(p.includes("/")){anchored=true}// Escape regex special chars except * and ?
44
- let regexStr=p.replace(/[.+^${}()|[\]\\]/g,"\\$&");// Handle ** (match any path segment including nested)
45
- regexStr=regexStr.replace(/\*\*/g,"{{GLOBSTAR}}");// Handle * (match anything except path separator)
46
- regexStr=regexStr.replace(/\*/g,"[^/]*");// Handle ?
47
- regexStr=regexStr.replace(/\?/g,"[^/]");// Restore globstar
48
- regexStr=regexStr.replace(/\{\{GLOBSTAR\}\}/g,".*");if(anchored){// Anchored: match from the start of the relative path
49
- return new RegExp(`^${regexStr}(/|$)`)}// Unanchored: match as a path segment anywhere in the tree
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
- * Check if a relative path should be ignored.
52
- * @param {string} relativePath - Forward-slash normalized relative path
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}// ==================== File Collection ====================
55
+ */function shouldIgnore(relativePath,regexPatterns){for(const regex of regexPatterns){if(regex.test(relativePath))return true}return false}// ==================== 文件收集 ====================
56
56
  /**
57
- * Recursively collect all files under a directory, respecting ignore patterns.
58
- * @param {string} dir - Absolute directory path
59
- * @param {string} baseDir - Project root for computing relative paths
57
+ * 递归收集目录下的所有文件,遵循忽略规则。
58
+ * @param {string} dir - 目录的绝对路径
59
+ * @param {string} baseDir - 项目根目录,用于计算相对路径
60
60
  * @param {RegExp[]} ignoreRegexes
61
- * @returns {string[]} Array of absolute file paths
62
- */function collectFiles(dir,baseDir,ignoreRegexes){const results=[];let entries;try{entries=fs.readdirSync(dir,{withFileTypes:true})}catch(e){console.warn(`\x1b[33m⚠️ Cannot read directory: ${dir}\x1b[0m`);return results}for(const entry of entries){const fullPath=path.join(dir,entry.name);// Use forward slashes for consistent matching
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 Creation ====================
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
- * Create a zip file from the collected files.
66
- * @param {string[]} files - Absolute file paths
67
- * @param {string} rootFolderName - Root folder name inside the zip
68
- * @param {string} outputPath - Absolute path for the output zip file
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();// Ensure output directory exists
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)})}// ==================== Formatting Helpers ====================
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
- * Format bytes to human-readable string.
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`}// ==================== Main ====================
77
- async function main(){console.log("\x1B[36m\uD83D\uDCE6 playable-scripts pack\x1B[0m\n");// 1. Resolve appName
78
- const appName=resolveAppName();console.log(` App Name: \x1b[1m${appName}\x1b[0m`);// 2. Parse ignore patterns (merge .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} from .gitignore`);if(sources.playcraftignore>0)parts.push(`${sources.playcraftignore} from .playcraftignore`);console.log(` Ignore rules: \x1b[33m${patterns.length}\x1b[0m patterns (${parts.join(", ")})`)}// 3. Collect files
80
- console.log(` Scanning project files...`);const files=collectFiles(projectRoot,projectRoot,ignoreRegexes);if(files.length===0){console.error("\x1B[31m\u274C No files to pack! Check your .playcraftignore rules.\x1B[0m");process.exit(1)}console.log(` Found: \x1b[32m${files.length}\x1b[0m files to pack`);// 4. Create zip
81
- const outDir=resolveOutputDir();const outputPath=path.resolve(projectRoot,outDir,`${appName}.zip`);console.log(` Output: ${outputPath}\n`);console.log(" Packing...");try{const{fileCount,zipSize}=await createZip(files,appName,outputPath);console.log(`\n\x1b[32m✅ Pack complete!\x1b[0m`);console.log(` Files: ${fileCount}`);console.log(` Size: ${formatSize(zipSize)}`);console.log(` Output: ${outputPath}`)}catch(err){console.error(`\n\x1b[31m❌ Pack failed: ${err.message}\x1b[0m`);process.exit(1)}}main();
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;
@@ -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="// [AUTO-GENERATED BACKUP] This file was created by the build process.\n"+"// If the build was interrupted, you can restore it by removing the \".backup\" suffix\n"+"// and replacing the original \"theme.schema.json5\" with this file.\n";/**
2
- * Recursively extract `default` values from a JSON Schema definition.
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
- * - For "object" types: recurse into `properties` and build a nested object.
5
- * - For primitive types: return the `default` value if present.
4
+ * - 对于 "object" 类型:递归遍历 `properties` 并构建嵌套对象。
5
+ * - 对于基本类型:如果存在 `default` 值则返回。
6
6
  *
7
- * @param {object} schema - A JSON Schema (or sub-schema) object
8
- * @returns {*} The extracted default value tree, or undefined if none
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}// Primitive or array type with a default value
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
- * Validate theme data for a given theme against the schema.
11
+ * 根据 schema 校验指定主题的主题数据。
12
12
  *
13
- * File layout expected under the project:
14
- * src/theme/theme.schema.json5 – JSON Schema (JSON5 format)
15
- * src/theme/{themeName}/theme.data.json5 – Per-theme data
13
+ * 项目中预期的文件结构:
14
+ * src/theme/theme.schema.json5 – JSON SchemaJSON5 格式)
15
+ * src/theme/{themeName}/theme.data.json5 – 各主题的数据
16
16
  *
17
- * Default values are extracted from the schema's `default` fields via extractDefaultsFromSchema,
18
- * then deep-merged with theme data (theme data takes priority) before validation.
17
+ * 通过 extractDefaultsFromSchema schema `default` 字段提取默认值,
18
+ * 然后与主题数据深度合并(主题数据优先)后再进行校验。
19
19
  *
20
- * @param {string} themeName - Name of the theme directory
21
- * @param {string} projectRoot - Absolute path to the project root
22
- * @param {object} [themeConfig] - Optional theme config (themes section from builds.config.js)
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 – The extracted default values from schema
25
- * errors – null if validation passed, otherwise an array of error messages
26
- */function validateThemeData(themeName,projectRoot,themeConfig){const sourceDir=themeConfig&&themeConfig.sourceDir||"src/theme";const themeBaseDir=path.join(projectRoot,sourceDir);// --- Resolve file paths ---
27
- const schemaPath=path.join(themeBaseDir,"theme.schema.json5");const dataPath=path.join(themeBaseDir,themeName,"theme.data.json5");// --- Read & parse files ---
28
- if(!fs.existsSync(schemaPath)){return{defaultData:null,errors:[`Schema file not found: ${schemaPath}`]}}const schema=JSON5.parse(fs.readFileSync(schemaPath,"utf8"));// Extract default values from schema
29
- const defaultData=extractDefaultsFromSchema(schema)||{};let themeData={};if(fs.existsSync(dataPath)){themeData=JSON5.parse(fs.readFileSync(dataPath,"utf8"))}// Deep merge (theme data overrides schema defaults), then validate
30
- const merged=deepMerge(defaultData,themeData);// --- Validate merged data against schema ---
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
- * Deep merge two plain objects. Values in `override` take priority.
33
- * Arrays are replaced entirely (not concatenated).
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
- * Backup the original theme.schema.json5 file.
40
- * The backup file includes a comment header explaining how to restore it manually.
39
+ * 备份原始的 theme.schema.json5 文件。
40
+ * 备份文件包含注释头,说明如何手动恢复。
41
41
  *
42
- * @param {string} schemaPath - Absolute path to theme.schema.json5
43
- * @returns {string} The backup file path
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
- * Restore theme.schema.json5 from its backup file and remove the backup.
45
+ * 从备份文件恢复 theme.schema.json5 并删除备份。
46
46
  *
47
- * @param {string} schemaPath - Absolute path to theme.schema.json5
48
- */function restoreSchema(schemaPath){const backupPath=schemaPath+SCHEMA_BACKUP_SUFFIX;if(!fs.existsSync(backupPath))return;const backupContent=fs.readFileSync(backupPath,"utf8");// Strip the auto-generated comment header (lines starting with "//")
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
- * Overwrite theme.schema.json5 with the merged data (schema defaults + theme overrides)
51
- * so that the bundler can consume it directly during the build.
50
+ * 用合并后的数据(schema 默认值 + 主题覆盖值)覆写 theme.schema.json5
51
+ * 以便打包工具在构建时可以直接使用。
52
52
  *
53
- * @param {string} schemaPath - Absolute path to theme.schema.json5
54
- * @param {object} mergedData - The merged theme data to write
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
- * Run theme validation with standard logging.
57
- * Shared by both single build (build.js) and batch build (batch-build.js).
56
+ * 执行主题校验并输出标准日志。
57
+ * 单次构建(build.js)和批量构建(batch-build.js)共用此函数。
58
58
  *
59
- * Flow:
60
- * 1. Validate theme.data.json5 against theme.schema.json5
61
- * 2. Backup theme.schema.json5 (with restore instructions)
62
- * 3. Extract defaults from schema via extractDefaultsFromSchema
63
- * 4. Overwrite theme.schema.json5 with default data for the build
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
- * After the build (success or failure), call restoreSchema() to restore the original.
65
+ * 构建完成后(无论成功或失败),调用 restoreSchema() 恢复原始文件。
66
66
  *
67
- * @param {string} themeName - Name of the theme directory
68
- * @param {string} projectRoot - Absolute path to the project root
69
- * @param {object} [themeConfig] - Optional theme config (themes section from builds.config.js)
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] - Custom log function(message, level). Defaults to console.log.
71
+ * @param {function} [options.log] - 自定义日志函数(message, level)。默认使用 console.log
72
72
  * @returns {{ valid: boolean, errors: string[]|null, schemaPath: string|null }}
73
- * valid – true if schema file doesn't exist (skip) or validation passed
74
- * errors – null if validation passed, otherwise an array of error messages
75
- * schemaPath – Absolute path to theme.schema.json5 (for restoreSchema calls), or 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");// No schema fileskip validation entirely
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");// Backup original schema, then overwrite with default data for the build
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};
@@ -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.7",
3
+ "version": "1.0.8",
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",