@playcraft/devkit 1.0.4 → 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+"\".")}
@@ -1 +1,11 @@
1
- "use strict";process.env.BABEL_ENV="production";process.env.NODE_ENV="production";const{runBuild}=require("../../core/webpack.build.js");runBuild();
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();
@@ -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};
@@ -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|json5)$/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.4",
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",