@playcraft/devkit 1.0.3 → 1.0.5
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");const script=scriptIndex===-1?args[0]:args[scriptIndex];const nodeArgs=scriptIndex>0?args.slice(0,scriptIndex):[];if(["build","dev","builds"].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==="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+"\".")}
|
package/cli/commands/build.js
CHANGED
|
@@ -1 +1,11 @@
|
|
|
1
|
-
"use strict";process.env.BABEL_ENV="production";process.env.NODE_ENV="production";const{runBuild}=require("../../core/webpack.build.js");
|
|
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{validateThemeData}=require("../../core/utils/validateThemeData");/**
|
|
2
|
+
* Detect the current theme name from the theme entry file.
|
|
3
|
+
* Supports patterns like:
|
|
4
|
+
* export { default } from './themeName';
|
|
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);if(themeName){const themeBaseDir=path.join(projectRoot,themeConfig.sourceDir||"src/theme");const schemaPath=path.join(themeBaseDir,"theme.schema.json5");if(fs.existsSync(schemaPath)){console.log(`\x1b[36m[build] 校验主题数据: ${themeName}\x1b[0m`);const{merged,errors}=validateThemeData(themeName,projectRoot,themeConfig);if(errors){console.error(`\x1b[31m[build] 主题 "${themeName}" 数据校验失败:\x1b[0m`);errors.forEach(e=>console.error(`\x1b[31m - ${e}\x1b[0m`));process.exit(1)}console.log(`\x1b[32m[build] 主题 "${themeName}" 数据校验通过`)}}runBuild();
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";const fs=require("fs");const path=require("path");const yazl=require("yazl");// ==================== Configuration ====================
|
|
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
|
|
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
|
|
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
|
|
12
|
+
return path.basename(projectRoot)}/**
|
|
13
|
+
* Resolve the output directory from CLI args.
|
|
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 ====================
|
|
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: remove trailing slashes for directory matching
|
|
23
|
+
const normalized=line.replace(/\/+$/,"");patterns.push(normalized)}return patterns}/**
|
|
24
|
+
* Parse .gitignore and .playcraftignore files, merge and deduplicate patterns.
|
|
25
|
+
* .gitignore provides base ignore rules, .playcraftignore provides additional ones.
|
|
26
|
+
* @returns {{ patterns: string[], sources: { gitignore: number, playcraftignore: number } }}
|
|
27
|
+
*/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)
|
|
28
|
+
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}}}/**
|
|
29
|
+
* Convert a gitignore-like pattern to a RegExp.
|
|
30
|
+
* @param {string} pattern
|
|
31
|
+
* @returns {RegExp}
|
|
32
|
+
*/function patternToRegex(pattern){// Escape regex special chars except * and ?
|
|
33
|
+
let regexStr=pattern.replace(/[.+^${}()|[\]\\]/g,"\\$&");// Handle ** (match any path segment including nested)
|
|
34
|
+
regexStr=regexStr.replace(/\*\*/g,"{{GLOBSTAR}}");// Handle * (match anything except path separator)
|
|
35
|
+
regexStr=regexStr.replace(/\*/g,"[^/]*");// Handle ?
|
|
36
|
+
regexStr=regexStr.replace(/\?/g,"[^/]");// Restore globstar
|
|
37
|
+
regexStr=regexStr.replace(/\{\{GLOBSTAR\}\}/g,".*");// If pattern doesn't contain /, match against basename or full path
|
|
38
|
+
if(!pattern.includes("/")){// Match as a path segment anywhere: either at start or after /
|
|
39
|
+
return new RegExp(`(^|/)${regexStr}(/|$)`)}// Pattern with / — match from the start of the relative path
|
|
40
|
+
return new RegExp(`^${regexStr}(/|$)`)}/**
|
|
41
|
+
* Check if a relative path should be ignored.
|
|
42
|
+
* @param {string} relativePath - Forward-slash normalized relative path
|
|
43
|
+
* @param {RegExp[]} regexPatterns
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/function shouldIgnore(relativePath,regexPatterns){for(const regex of regexPatterns){if(regex.test(relativePath))return true}return false}// ==================== File Collection ====================
|
|
46
|
+
/**
|
|
47
|
+
* Recursively collect all files under a directory, respecting ignore patterns.
|
|
48
|
+
* @param {string} dir - Absolute directory path
|
|
49
|
+
* @param {string} baseDir - Project root for computing relative paths
|
|
50
|
+
* @param {RegExp[]} ignoreRegexes
|
|
51
|
+
* @returns {string[]} Array of absolute file paths
|
|
52
|
+
*/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
|
|
53
|
+
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 ====================
|
|
54
|
+
/**
|
|
55
|
+
* Create a zip file from the collected files.
|
|
56
|
+
* @param {string[]} files - Absolute file paths
|
|
57
|
+
* @param {string} rootFolderName - Root folder name inside the zip
|
|
58
|
+
* @param {string} outputPath - Absolute path for the output zip file
|
|
59
|
+
* @returns {Promise<{fileCount: number, zipSize: number}>}
|
|
60
|
+
*/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
|
|
61
|
+
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 ====================
|
|
62
|
+
/**
|
|
63
|
+
* Format bytes to human-readable string.
|
|
64
|
+
* @param {number} bytes
|
|
65
|
+
* @returns {string}
|
|
66
|
+
*/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 ====================
|
|
67
|
+
async function main(){console.log("\x1B[36m\uD83D\uDCE6 playable-scripts pack\x1B[0m\n");// 1. Resolve appName
|
|
68
|
+
const appName=resolveAppName();console.log(` App Name: \x1b[1m${appName}\x1b[0m`);// 2. Parse ignore patterns (merge .gitignore + .playcraftignore)
|
|
69
|
+
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
|
|
70
|
+
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
|
|
71
|
+
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();
|
package/core/batch-build.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 2. 每个构建任务使用独立的临时目录(dist-temp-{taskId})
|
|
8
8
|
* 3. 构建完成后自动移动到目标目录并清理临时目录
|
|
9
9
|
* 4. 支持灵活的配置系统
|
|
10
|
-
*/const{execSync,exec}=require("child_process");const fs=require("fs");const path=require("path");const os=require("os");const{promisify}=require("util");const{createCOSClient,uploadDirectoryToCOS,uploadFileToCOS,purgeCDNCache}=require("./cos-uploader");const execAsync=promisify(exec);/**
|
|
10
|
+
*/const{execSync,exec}=require("child_process");const fs=require("fs");const path=require("path");const os=require("os");const{promisify}=require("util");const{createCOSClient,uploadDirectoryToCOS,uploadFileToCOS,purgeCDNCache}=require("./cos-uploader");const{validateThemeData}=require("./utils/validateThemeData");const execAsync=promisify(exec);/**
|
|
11
11
|
* 更新 preview.json 文件
|
|
12
12
|
*/async function updatePreviewJson(cosConfig,cosPrefix,currentTheme,previewLinks){const previewJsonKey=cosPrefix?`${cosPrefix}/preview.json`:"preview.json";// 读取现有的 preview.json 文件
|
|
13
13
|
let existingPreviewData={};try{const cos=createCOSClient(cosConfig);const getObjectResult=await new Promise((resolve,reject)=>{cos.getObject({Bucket:cosConfig.Bucket,Region:cosConfig.Region,Key:previewJsonKey},(err,data)=>{if(err){if(err.statusCode===404){// 文件不存在,使用空对象
|
|
@@ -154,7 +154,9 @@ if(config.themes.enabled){backupEntryFile(config,projectRoot)}// 批量构建
|
|
|
154
154
|
const outputDir=path.join(projectRoot,config.build.outputDir);const allResults=[];const previewLinks=[];// 存储预览链接
|
|
155
155
|
// 批量构建前,清理一下目录
|
|
156
156
|
cleanTempDir(outputDir);for(const theme of themes){// 切换主题
|
|
157
|
-
if(config.themes.enabled){switchTheme(config,theme,projectRoot)}//
|
|
157
|
+
if(config.themes.enabled){switchTheme(config,theme,projectRoot)}// Validate and merge theme data (schema + default + theme override)
|
|
158
|
+
const themeBaseDir=path.join(projectRoot,config.themes.sourceDir||"src/theme");const schemaPath=path.join(themeBaseDir,"theme.schema.json5");if(fs.existsSync(schemaPath)){log(`校验主题数据: ${theme}`,"info");const{merged,errors}=validateThemeData(theme,projectRoot,config.themes);if(errors){log(`主题 "${theme}" 数据校验失败:`,"error");errors.forEach(e=>log(` - ${e}`,"error"));if(!config.build.continueOnError){throw new Error(`Theme data validation failed for "${theme}"`)}// Record failures for all channels of this theme and skip building
|
|
159
|
+
channels.forEach(channel=>{allResults.push({success:false,theme,channel,error:"Theme data validation failed"})});continue}log(`主题 "${theme}" 数据校验通过`,"success")}// 并发构建该主题的所有渠道
|
|
158
160
|
const themeResults=await buildChannelsInParallel(config,theme,channels,outputDir,projectRoot,isChannelFold2Zip);allResults.push(...themeResults);// 如果需要预览链接,上传 preview 渠道产物到 COS
|
|
159
161
|
if(needPreview){const previewResult=themeResults.find(r=>r.channel==="preview"&&r.success);if(previewResult){try{const previewDir=path.join(outputDir,theme,"preview");// 去掉 theme 这一级目录,直接使用 PathPrefix
|
|
160
162
|
const cosPrefix=config.cosConfig.PathPrefix||"";log(`开始上传 ${theme} 预览文件到 COS...`,"info");const uploadResults=await uploadDirectoryToCOS(config.cosConfig,previewDir,cosPrefix);// 保存预览链接
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";const fs=require("fs");const path=require("path");const JSON5=require("json5");const Ajv=require("ajv");/**
|
|
2
|
+
* Validate and merge theme data for a given theme.
|
|
3
|
+
*
|
|
4
|
+
* File layout expected under the project:
|
|
5
|
+
* src/theme/theme.schema.json5 – JSON Schema (JSON5 format)
|
|
6
|
+
* src/theme/theme.default.json5 – Default values
|
|
7
|
+
* src/theme/{themeName}/theme.data.json5 – Per-theme overrides
|
|
8
|
+
*
|
|
9
|
+
* Merge strategy: deep merge, theme data takes priority over defaults.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} themeName - Name of the theme directory
|
|
12
|
+
* @param {string} projectRoot - Absolute path to the project root
|
|
13
|
+
* @param {object} [themeConfig] - Optional theme config (themes section from builds.config.js)
|
|
14
|
+
* @returns {{ merged: object, errors: string[] | null }}
|
|
15
|
+
* merged – The merged data object (default + theme override)
|
|
16
|
+
* errors – null if validation passed, otherwise an array of error messages
|
|
17
|
+
*/function validateThemeData(themeName,projectRoot,themeConfig){const sourceDir=themeConfig&&themeConfig.sourceDir||"src/theme";const themeBaseDir=path.join(projectRoot,sourceDir);// --- Resolve file paths ---
|
|
18
|
+
const schemaPath=path.join(themeBaseDir,"theme.schema.json5");const defaultPath=path.join(themeBaseDir,"theme.default.json5");const dataPath=path.join(themeBaseDir,themeName,"theme.data.json5");// --- Read & parse files ---
|
|
19
|
+
if(!fs.existsSync(schemaPath)){return{merged:null,errors:[`Schema file not found: ${schemaPath}`]}}const schema=JSON5.parse(fs.readFileSync(schemaPath,"utf8"));let defaultData={};if(fs.existsSync(defaultPath)){defaultData=JSON5.parse(fs.readFileSync(defaultPath,"utf8"))}let themeData={};if(fs.existsSync(dataPath)){themeData=JSON5.parse(fs.readFileSync(dataPath,"utf8"))}// --- Deep merge (theme overrides default) ---
|
|
20
|
+
const merged=deepMerge(defaultData,themeData);// --- Validate against schema ---
|
|
21
|
+
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{merged,errors}}return{merged,errors:null}}/**
|
|
22
|
+
* Deep merge two plain objects. Values in `override` take priority.
|
|
23
|
+
* Arrays are replaced entirely (not concatenated).
|
|
24
|
+
*
|
|
25
|
+
* @param {object} base
|
|
26
|
+
* @param {object} override
|
|
27
|
+
* @returns {object}
|
|
28
|
+
*/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}module.exports={validateThemeData,deepMerge};
|
package/core/webpack.build.js
CHANGED
|
@@ -48,7 +48,7 @@ if(adNetwork==="adikteev"){const cssRuleIndex=webpackConfig.module.rules.findInd
|
|
|
48
48
|
if(adNetwork!=="mintegral"&&adNetwork!=="adikteev"&&adNetwork!=="bigabid"&&!(isZipOutputFinal&&adNetwork==="facebook"))webpackConfig.plugins.push(new HtmlInlineScriptPlugin);// 注入渠道 SDK 插件(公共逻辑)
|
|
49
49
|
injectSDKPlugins(webpackConfig,{network:adNetwork,protocol:adProtocol,orientation:buildOptions.orientation});if(isZipOutputFinal){if(adNetwork==="tiktok"){webpackConfig.plugins.push(new CopyWebpackPlugin({patterns:[{from:path.join(__dirname,"resources","tiktok-config.json"),to:"config.json"}]}))}else if(adNetwork==="snapchat"){const tmpConfigPath=path.join(__dirname,"resources","snapchat-config.json");webpackConfig.plugins.push(new CopyWebpackPlugin({patterns:[{from:tmpConfigPath,to:"config.json"}]}))}else if(adNetwork==="bigoads"){// BigoAds 要求输出 ZIP 包,包含 index.html 和 config.json
|
|
50
50
|
// SDK 注入已在 injectSDKPlugins 中处理
|
|
51
|
-
webpackConfig.plugins.push(new CopyWebpackPlugin({patterns:[{from:path.join(__dirname,"resources","bigoads-config.json"),to:"config.json"}]}))}webpackConfig.plugins.push(new ZipPlugin({filename:isChannelFold2Zip?adNetwork:getFileName(),path:path.resolve(outDir),cleanTempFolder:
|
|
51
|
+
webpackConfig.plugins.push(new CopyWebpackPlugin({patterns:[{from:path.join(__dirname,"resources","bigoads-config.json"),to:"config.json"}]}))}webpackConfig.plugins.push(new ZipPlugin({filename:isChannelFold2Zip?adNetwork:getFileName(),path:path.resolve(outDir),cleanTempFolder:true// 🗑️ 生成 zip 后删除临时文件夹
|
|
52
52
|
}));webpackConfig.output.path=path.resolve(outDir,adNetwork);if(adNetwork==="adikteev"){webpackConfig.output.filename="creative.js";// Adikteev 需要将 CSS 单独提取为 style.css 文件
|
|
53
53
|
webpackConfig.plugins.push(new MiniCssExtractPlugin({filename:"style.css"}))}else if(adNetwork==="bigabid")webpackConfig.output.filename="main.js";else if(adNetwork==="inmobi")webpackConfig.output.filename="main.js"}// 添加 fflate 压缩插件(仅在打正式包时生效)
|
|
54
54
|
if(buildOptions.fflateCompression){console.log("\u2705 fflate compression enabled (production build only)");webpackConfig.plugins.push(new FflateCompressionPlugin({enabled:true,level:9,keepOriginal:false,logStats:true,googlePlayUrl:buildOptions.googlePlayUrl,appStoreUrl:buildOptions.appStoreUrl}))}return webpackConfig}/**
|
package/core/webpack.common.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";const path=require("path");const fs=require("fs");const TsconfigPathsPlugin=require("tsconfig-paths-webpack-plugin");const{VueLoaderPlugin}=require("vue-loader");const{options}=require("./options");const tsConfigPath=path.resolve(options.tsConfig);const jsConfigPath=path.resolve(options.jsConfig);let jsTsConfigPath=null;if(fs.existsSync(tsConfigPath)){jsTsConfigPath=tsConfigPath}else if(fs.existsSync(jsConfigPath)){jsTsConfigPath=jsConfigPath}/** @type {string[]} List of file extensions that webpack will resolve */const allowedExtensions=[".vue",".ts",".tsx",".js",".json",".png",".glb",".fbx",".obj",".jpg",".webp",".mp3",".svg",".xml",".atlas",".fnt",".css",".gif",".mp4",".woff",".woff2",".ttf",".otf"];/** @type {import('webpack').Configuration} Base webpack configuration used by both development and build configs */const webpackConfig={entry:path.resolve("src/index"),resolve:{extensions:allowedExtensions,alias:{assets:path.resolve("assets"),vue:"vue/dist/vue.esm-bundler.js","@":path.resolve("src")},plugins:jsTsConfigPath?[new TsconfigPathsPlugin({configFile:jsTsConfigPath,extensions:allowedExtensions})]:[]},output:{filename:"build.js",path:path.resolve("dist")},module:{rules:[// Vue 单文件组件
|
|
1
|
+
"use strict";const path=require("path");const fs=require("fs");const TsconfigPathsPlugin=require("tsconfig-paths-webpack-plugin");const{VueLoaderPlugin}=require("vue-loader");const{options}=require("./options");const tsConfigPath=path.resolve(options.tsConfig);const jsConfigPath=path.resolve(options.jsConfig);let jsTsConfigPath=null;if(fs.existsSync(tsConfigPath)){jsTsConfigPath=tsConfigPath}else if(fs.existsSync(jsConfigPath)){jsTsConfigPath=jsConfigPath}/** @type {string[]} List of file extensions that webpack will resolve */const allowedExtensions=[".vue",".ts",".tsx",".js",".json",".json5",".png",".glb",".fbx",".obj",".jpg",".webp",".mp3",".svg",".xml",".atlas",".fnt",".css",".gif",".mp4",".woff",".woff2",".ttf",".otf"];/** @type {import('webpack').Configuration} Base webpack configuration used by both development and build configs */const webpackConfig={entry:path.resolve("src/index"),resolve:{extensions:allowedExtensions,alias:{assets:path.resolve("assets"),vue:"vue/dist/vue.esm-bundler.js","@":path.resolve("src")},plugins:jsTsConfigPath?[new TsconfigPathsPlugin({configFile:jsTsConfigPath,extensions:allowedExtensions})]:[]},output:{filename:"build.js",path:path.resolve("dist")},module:{rules:[// Vue 单文件组件
|
|
2
2
|
{test:/\.vue$/,loader:"vue-loader"},// {
|
|
3
3
|
// test: /\.tsx?$/,
|
|
4
4
|
// use: 'ts-loader',
|
|
@@ -7,4 +7,4 @@
|
|
|
7
7
|
{test:/\.ts?$/,exclude:/node_modules/,use:[{loader:"babel-loader",options:{// presets: ['@babel/preset-env'],
|
|
8
8
|
plugins:options.compilation.allowTemplateLiterals===false?["@babel/plugin-transform-template-literals"]:[]}},{loader:"esbuild-loader",options:{loader:"ts",target:"es2015"}}]},{test:/\.css$/,use:["style-loader","css-loader"]},{test:/\.(js|mjs)$/,// exclude: /node_modules/,
|
|
9
9
|
use:{loader:"babel-loader",options:{// presets: ['@babel/preset-env'],
|
|
10
|
-
plugins:options.compilation.allowTemplateLiterals===false?["@babel/plugin-transform-template-literals"]:[]}}},{test:/\.(gltf)$/,loader:path.join(__dirname,"loaders/gltf-loader.js")},{test:/\.(gif|png|jpe?g|webp|mp3|mp4|m4a|ogg|wav|glb|fbx|obj)$/i,type:"asset/inline"},{test:/\.(svg|xml|json)$/i,type:"asset/source"},{test:/\.atlas$/,type:"asset/inline",generator:{dataUrl:content=>{return`data:text/atlas;base64,${content.toString("base64")}`}}},{test:/\.fnt$/i,type:"asset/inline",generator:{dataUrl:content=>{return`data:text/plain;base64,${content.toString("base64")}`}}},{test:/\.(woff|woff2|eot|ttf|otf)$/i,type:"asset/inline"}]},plugins:[new VueLoaderPlugin]};exports.allowedExtensions=allowedExtensions;exports.webpackCommonConfig=webpackConfig;
|
|
10
|
+
plugins:options.compilation.allowTemplateLiterals===false?["@babel/plugin-transform-template-literals"]:[]}}},{test:/\.(gltf)$/,loader:path.join(__dirname,"loaders/gltf-loader.js")},{test:/\.(gif|png|jpe?g|webp|mp3|mp4|m4a|ogg|wav|glb|fbx|obj)$/i,type:"asset/inline"},{test:/\.json5$/i,type:"javascript/auto",use:{loader:"json5-loader",options:{esModule:false}}},{test:/\.(svg|xml|json)$/i,type:"asset/source"},{test:/\.atlas$/,type:"asset/inline",generator:{dataUrl:content=>{return`data:text/atlas;base64,${content.toString("base64")}`}}},{test:/\.fnt$/i,type:"asset/inline",generator:{dataUrl:content=>{return`data:text/plain;base64,${content.toString("base64")}`}}},{test:/\.(woff|woff2|eot|ttf|otf)$/i,type:"asset/inline"}]},plugins:[new VueLoaderPlugin]};exports.allowedExtensions=allowedExtensions;exports.webpackCommonConfig=webpackConfig;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcraft/devkit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "HTML5 Playable Ads 构建工具,支持多广告渠道打包",
|
|
5
5
|
"main": "core/index.js",
|
|
6
6
|
"module": "core/index.js",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"@babel/core": "^7.26.0",
|
|
24
24
|
"@babel/preset-env": "^7.26.0",
|
|
25
25
|
"@vue/compiler-sfc": "^3.4.0",
|
|
26
|
+
"ajv": "^8.18.0",
|
|
26
27
|
"babel-loader": "^9.2.1",
|
|
27
28
|
"copy-webpack-plugin": "^13.0.0",
|
|
28
29
|
"cos-nodejs-sdk-v5": "^2.14.4",
|
|
@@ -32,6 +33,8 @@
|
|
|
32
33
|
"fflate": "^0.8.2",
|
|
33
34
|
"html-inline-script-webpack-plugin": "^3.2.1",
|
|
34
35
|
"html-webpack-plugin": "^5.6.0",
|
|
36
|
+
"json5": "^2.2.3",
|
|
37
|
+
"json5-loader": "^4.0.0",
|
|
35
38
|
"mini-css-extract-plugin": "^2.7.6",
|
|
36
39
|
"prettyjson": "^1.2.5",
|
|
37
40
|
"style-loader": "^4.0.0",
|