@playcraft/devkit 1.0.31-beta.1 → 1.0.31-beta.11

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.
Files changed (94) hide show
  1. package/README.md +262 -0
  2. package/cli/bin/playable-scripts.js +53 -6
  3. package/cli/commands/build.js +241 -32
  4. package/cli/commands/builds.js +13 -2
  5. package/cli/commands/dev.js +22 -1
  6. package/cli/commands/pack.js +323 -41
  7. package/cli/commands/vite-dev.js +7 -1
  8. package/core/artifacts/emit.js +98 -1
  9. package/core/artifacts/fflate.js +31 -1
  10. package/core/artifacts/zip.js +39 -1
  11. package/core/batch-build.js +1261 -149
  12. package/core/constants.js +20 -2
  13. package/core/cos-uploader.js +289 -23
  14. package/core/engines/cocos/clean-cache.js +188 -0
  15. package/core/engines/cocos/clean-cache.test.js +75 -0
  16. package/core/engines/cocos/cli-build.js +116 -2
  17. package/core/engines/cocos/cli.js +38 -1
  18. package/core/engines/cocos/constants.js +21 -1
  19. package/core/engines/cocos/creator-cli.js +108 -1
  20. package/core/engines/cocos/dev-server.js +412 -2
  21. package/core/engines/cocos/fs.js +105 -1
  22. package/core/engines/cocos/injects/3x/init.js +438 -19
  23. package/core/engines/cocos/injects/3x/main.js +52 -2
  24. package/core/engines/cocos/injects/libs/pako.js +2 -1
  25. package/core/engines/cocos/mcp-refresh.js +76 -2
  26. package/core/engines/cocos/merge.js +147 -2
  27. package/core/engines/cocos/pack.js +187 -1
  28. package/core/engines/cocos/preview-cache.js +51 -0
  29. package/core/engines/cocos/preview-cache.test.js +32 -0
  30. package/core/engines/cocos/preview-concat.js +192 -0
  31. package/core/engines/cocos/preview-concat.test.js +73 -0
  32. package/core/engines/cocos/preview-json-hook.js +200 -0
  33. package/core/engines/cocos/preview-json-pack.js +379 -0
  34. package/core/engines/cocos/preview-json-pack.test.js +226 -0
  35. package/core/engines/cocos/preview-proxy.js +132 -0
  36. package/core/engines/cocos/rebundle-preview.js +56 -0
  37. package/core/engines/cocos/strip-social-share.js +115 -3
  38. package/core/engines/cocos/theme-assets.js +161 -2
  39. package/core/engines/cocos/writable-cli.js +250 -0
  40. package/core/engines/cocos/writable-cli.test.js +112 -0
  41. package/core/engines/web/index.js +20 -1
  42. package/core/engines/web/vite.dev.js +322 -17
  43. package/core/engines/web/webpack.build.js +522 -65
  44. package/core/engines/web/webpack.common.js +177 -11
  45. package/core/engines/web/webpack.dev.js +88 -6
  46. package/core/index.d.ts +3 -0
  47. package/core/index.js +101 -7
  48. package/core/injectors/apply.js +94 -1
  49. package/core/injectors/check-channel-matrix.js +130 -2
  50. package/core/injectors/spec.js +217 -9
  51. package/core/loaders/gltf-loader.js +74 -28
  52. package/core/options.js +353 -16
  53. package/core/plugins/AdikteevInjectorPlugin.js +26 -4
  54. package/core/plugins/BigoAdsInjectorPlugin.js +21 -4
  55. package/core/plugins/DAPIInjectorPlugin.js +25 -2
  56. package/core/plugins/DebuggerInjectionPlugin.js +31 -3
  57. package/core/plugins/ExitAPIInjectorPlugin.js +56 -3
  58. package/core/plugins/FflateCompressionPlugin.js +231 -40
  59. package/core/plugins/FontInjectionWebpackPlugin.js +46 -4
  60. package/core/plugins/LiftoffInjectorPlugin.js +26 -4
  61. package/core/plugins/MRAIDInjectorPlugin.js +24 -2
  62. package/core/plugins/MintegralInjectorPlugin.js +25 -2
  63. package/core/plugins/PangleInjectorPlugin.js +24 -2
  64. package/core/plugins/PlaycraftOrientationInjectorPlugin.js +29 -3
  65. package/core/plugins/SnapchatInjectorPlugin.js +26 -4
  66. package/core/plugins/SocialMediaInjectorPlugin.js +119 -7
  67. package/core/plugins/TikTokInjectorPlugin.js +24 -2
  68. package/core/plugins/UnityInjectorPlugin.js +37 -8
  69. package/core/plugins/ZipPlugin.js +154 -20
  70. package/core/plugins/socialMediaVitePlugin.js +108 -2
  71. package/core/utils/buildDefines.js +30 -2
  72. package/core/utils/buildTemplateString.js +39 -5
  73. package/core/utils/date.js +16 -1
  74. package/core/utils/fontRegistry.js +81 -13
  75. package/core/utils/fontResolver.js +335 -37
  76. package/core/utils/generateAdikteevHtmlWebpackPluginConfig.js +42 -7
  77. package/core/utils/generateBigabidHtmlWebpackPluginConfig.js +30 -3
  78. package/core/utils/generateInMobiHtmlWebpackPluginConfig.js +43 -7
  79. package/core/utils/getPlaycraftOrientationSuffix.js +9 -2
  80. package/core/utils/injectSDKPlugins.js +68 -13
  81. package/core/utils/logOptions.js +46 -5
  82. package/core/utils/mergeOptions.js +21 -2
  83. package/core/utils/parseArgvOptions.js +125 -5
  84. package/core/utils/resolveChannelFold2Zip.js +15 -3
  85. package/core/utils/validateThemeData.js +227 -26
  86. package/core/validators/index.d.ts +4 -0
  87. package/core/validators/pre-build-checker.js +287 -25
  88. package/core/validators/tracking-validator.js +444 -43
  89. package/core/validators/tracking-validator.test.js +92 -0
  90. package/core/vite.dev.js +1 -1
  91. package/core/webpack.build.js +1 -1
  92. package/core/webpack.common.js +1 -1
  93. package/core/webpack.dev.js +1 -1
  94. package/package.json +3 -2
@@ -1,3 +1,162 @@
1
- "use strict";const fs=require("fs");const path=require("path");const JSON5=require("json5");const{getBase64FromFile,getAllFilesFromDir}=require("./fs");const{THEME_ASSET_EXT}=require("./constants");const{validateThemeData}=require("../../utils/validateThemeData");function detectCurrentTheme(projectRoot,themeConfig){const entryFile=themeConfig&&themeConfig.entryFile||"src/theme/index.ts";const entryPath=path.join(projectRoot,entryFile);if(!fs.existsSync(entryPath))return null;const content=fs.readFileSync(entryPath,"utf8");const match=content.match(/from\s+['"]\.\/([^'"]+)['"]/);return match?match[1]:null}function collectDirAssets(assetsDir,keyPrefix){const map={};if(!fs.existsSync(assetsDir))return map;const root=assetsDir.replace(/\\/g,"/");for(const filePath of getAllFilesFromDir(assetsDir)){if(!THEME_ASSET_EXT.test(filePath))continue;const rel=filePath.replace(/\\/g,"/").slice(root.length).replace(/^\//,"");map[`${keyPrefix}${rel}`]=getBase64FromFile(filePath)}return map}function collectReferencedFiles(projectRoot,sourceDir,themeName,mergedData,assetRecord){if(!mergedData||typeof mergedData!=="object")return;const themeRoot=path.join(projectRoot,sourceDir,themeName);for(const value of Object.values(mergedData)){if(typeof value==="string"){if(assetRecord[value])continue;const rel=value.replace(/^\.\//,"");const candidates=[path.join(themeRoot,rel),path.join(themeRoot,"assets",path.basename(rel)),path.resolve(projectRoot,rel)];const abs=candidates.find(p=>fs.existsSync(p)&&fs.statSync(p).isFile()&&THEME_ASSET_EXT.test(p));if(abs)assetRecord[value]=getBase64FromFile(abs)}else if(value&&typeof value==="object"&&!Array.isArray(value)){collectReferencedFiles(projectRoot,sourceDir,themeName,value,assetRecord)}}}function loadThemeI18n(projectRoot,sourceDir,themeName){const p=path.join(projectRoot,sourceDir,themeName,"theme.i18n.runtime.json5");if(!fs.existsSync(p))return{};try{return JSON5.parse(fs.readFileSync(p,"utf8"))||{}}catch{return{}}}/**
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const JSON5 = require('json5');
6
+ const { getBase64FromFile, getAllFilesFromDir, sniffAudioMime } = require('./fs');
7
+ const { THEME_ASSET_EXT } = require('./constants');
8
+ const { validateThemeData } = require('../../utils/validateThemeData');
9
+
10
+ function detectCurrentTheme(projectRoot, themeConfig) {
11
+ const entryFile = (themeConfig && themeConfig.entryFile) || 'src/theme/index.ts';
12
+ const entryPath = path.join(projectRoot, entryFile);
13
+ if (!fs.existsSync(entryPath)) return null;
14
+ const content = fs.readFileSync(entryPath, 'utf8');
15
+ const match = content.match(/from\s+['"]\.\/([^'"]+)['"]/);
16
+ return match ? match[1] : null;
17
+ }
18
+
19
+ function isInlinableThemeAsset(filePath) {
20
+ if (THEME_ASSET_EXT.test(filePath)) return true;
21
+ try {
22
+ return Boolean(sniffAudioMime(fs.readFileSync(filePath)));
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ function collectDirAssets(assetsDir, keyPrefix) {
29
+ const map = {};
30
+ if (!fs.existsSync(assetsDir)) return map;
31
+ const root = assetsDir.replace(/\\/g, '/');
32
+ for (const filePath of getAllFilesFromDir(assetsDir)) {
33
+ if (!isInlinableThemeAsset(filePath)) continue;
34
+ const rel = filePath.replace(/\\/g, '/').slice(root.length).replace(/^\//, '');
35
+ map[`${keyPrefix}${rel}`] = getBase64FromFile(filePath);
36
+ }
37
+ return map;
38
+ }
39
+
40
+ function collectReferencedFiles(projectRoot, sourceDir, themeName, mergedData, assetRecord) {
41
+ if (!mergedData || typeof mergedData !== 'object') return;
42
+ const themeRoot = path.join(projectRoot, sourceDir, themeName);
43
+ for (const value of Object.values(mergedData)) {
44
+ if (typeof value === 'string') {
45
+ if (assetRecord[value]) continue;
46
+ const rel = value.replace(/^\.\//, '');
47
+ const candidates = [
48
+ path.join(themeRoot, rel),
49
+ path.join(themeRoot, 'assets', path.basename(rel)),
50
+ path.resolve(projectRoot, rel),
51
+ ];
52
+ const abs = candidates.find((p) => fs.existsSync(p) && fs.statSync(p).isFile() && isInlinableThemeAsset(p));
53
+ if (abs) assetRecord[value] = getBase64FromFile(abs);
54
+ } else if (value && typeof value === 'object' && !Array.isArray(value)) {
55
+ collectReferencedFiles(projectRoot, sourceDir, themeName, value, assetRecord);
56
+ }
57
+ }
58
+ }
59
+
60
+ function loadThemeI18n(projectRoot, sourceDir, themeName) {
61
+ const p = path.join(projectRoot, sourceDir, themeName, 'theme.i18n.runtime.json5');
62
+ if (!fs.existsSync(p)) return {};
63
+ try {
64
+ return JSON5.parse(fs.readFileSync(p, 'utf8')) || {};
65
+ } catch {
66
+ return {};
67
+ }
68
+ }
69
+
70
+ /**
2
71
  * Read src/theme json5 + theme/assets the same way Phaser webpack inlines them.
3
- */function loadThemePayload(projectRoot,themeName,themeConfig={}){const sourceDir=themeConfig.sourceDir||"src/theme";const{mergedData,errors}=validateThemeData(themeName,projectRoot,themeConfig);if(errors){throw new Error(`Theme "${themeName}" validation failed:\n${errors.join("\n")}`)}const assetsDir=path.join(projectRoot,sourceDir,themeName,"assets");const assetRecordByPath=collectDirAssets(assetsDir,"./assets/");collectReferencedFiles(projectRoot,sourceDir,themeName,mergedData,assetRecordByPath);return{themeName,mergedData:mergedData||{},i18nOverride:loadThemeI18n(projectRoot,sourceDir,themeName),assetRecordByPath}}function writeGeneratedThemeModule(filePath,payload){const header="/* eslint-disable */\n"+"// Generated from src/theme by playable-scripts. Do not edit.\n";const body=`export const THEME_DATA = ${JSON.stringify(payload.mergedData,null,2)} as Record<string, unknown>;\n`+`export const THEME_I18N_OVERRIDE = ${JSON.stringify(payload.i18nOverride,null,2)} as Record<string, Record<string, string>>;\n`+`export const THEME_ASSETS = ${JSON.stringify(payload.assetRecordByPath)} as Record<string, string>;\n`;const next=header+body;if(fs.existsSync(filePath)&&fs.readFileSync(filePath,"utf8")===next){return false}fs.mkdirSync(path.dirname(filePath),{recursive:true});fs.writeFileSync(filePath,next);return true}function syncGeneratedTheme(projectRoot,options={}){const themeConfig=options.themeConfig||{sourceDir:"src/theme",entryFile:"src/theme/index.ts"};const themeName=options.themeName||detectCurrentTheme(projectRoot,themeConfig);if(!themeName)return null;const payload=loadThemePayload(projectRoot,themeName,themeConfig);const rel=options.generatedRel||"assets/scripts/platform/theme.generated.ts";const changed=writeGeneratedThemeModule(path.join(projectRoot,rel),payload);return{themeName,payload,generatedRel:rel,changed}}module.exports={detectCurrentTheme,loadThemePayload,writeGeneratedThemeModule,syncGeneratedTheme,collectDirAssets};
72
+ */
73
+ function loadThemePayload(projectRoot, themeName, themeConfig = {}) {
74
+ const sourceDir = themeConfig.sourceDir || 'src/theme';
75
+ const { mergedData, errors } = validateThemeData(themeName, projectRoot, themeConfig);
76
+ if (errors) {
77
+ throw new Error(`Theme "${themeName}" validation failed:\n${errors.join('\n')}`);
78
+ }
79
+
80
+ const assetsDir = path.join(projectRoot, sourceDir, themeName, 'assets');
81
+ const assetRecordByPath = collectDirAssets(assetsDir, './assets/');
82
+ collectReferencedFiles(projectRoot, sourceDir, themeName, mergedData, assetRecordByPath);
83
+
84
+ return {
85
+ themeName,
86
+ mergedData: mergedData || {},
87
+ i18nOverride: loadThemeI18n(projectRoot, sourceDir, themeName),
88
+ assetRecordByPath,
89
+ };
90
+ }
91
+
92
+ function copyThemeAssetsToResources(projectRoot, themeName) {
93
+ const srcDir = path.join(projectRoot, 'src/theme', themeName, 'assets');
94
+ const destDir = path.join(projectRoot, 'assets/resources/textures');
95
+ if (!fs.existsSync(srcDir) || !fs.existsSync(destDir)) return false;
96
+ let changed = false;
97
+ for (const name of fs.readdirSync(srcDir)) {
98
+ if (!/\.(png|webp|jpe?g)$/i.test(name)) continue;
99
+ const dest = path.join(destDir, name);
100
+ if (!fs.existsSync(dest)) continue;
101
+ const from = path.join(srcDir, name);
102
+ const next = fs.readFileSync(from);
103
+ const prev = fs.readFileSync(dest);
104
+ if (Buffer.compare(next, prev) === 0) continue;
105
+ fs.writeFileSync(dest, next);
106
+ changed = true;
107
+ }
108
+ return changed;
109
+ }
110
+
111
+ function themeAssetStamp(dir) {
112
+ if (!fs.existsSync(dir)) return '';
113
+ try {
114
+ return fs
115
+ .readdirSync(dir)
116
+ .filter((name) => THEME_ASSET_EXT.test(name))
117
+ .map((name) => {
118
+ const st = fs.statSync(path.join(dir, name));
119
+ return `${name}:${st.size}:${Math.round(st.mtimeMs)}`;
120
+ })
121
+ .join('|');
122
+ } catch {
123
+ return '';
124
+ }
125
+ }
126
+
127
+ function writeGeneratedThemeModule(filePath, payload) {
128
+ const header =
129
+ '/* eslint-disable */\n' +
130
+ '// Generated from src/theme by playable-scripts. Do not edit.\n';
131
+ const body =
132
+ `export const THEME_DATA = ${JSON.stringify(payload.mergedData, null, 2)} as Record<string, unknown>;\n` +
133
+ `export const THEME_I18N_OVERRIDE = ${JSON.stringify(payload.i18nOverride, null, 2)} as Record<string, Record<string, string>>;\n` +
134
+ `export const THEME_ASSETS = ${JSON.stringify(payload.assetRecordByPath)} as Record<string, string>;\n`;
135
+ const next = header + body;
136
+ if (fs.existsSync(filePath) && fs.readFileSync(filePath, 'utf8') === next) {
137
+ return false;
138
+ }
139
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
140
+ fs.writeFileSync(filePath, next);
141
+ return true;
142
+ }
143
+
144
+ function syncGeneratedTheme(projectRoot, options = {}) {
145
+ const themeConfig = options.themeConfig || { sourceDir: 'src/theme', entryFile: 'src/theme/index.ts' };
146
+ const themeName = options.themeName || detectCurrentTheme(projectRoot, themeConfig);
147
+ if (!themeName) return null;
148
+ const payload = loadThemePayload(projectRoot, themeName, themeConfig);
149
+ const rel = options.generatedRel || 'assets/scripts/platform/theme.generated.ts';
150
+ const copied = copyThemeAssetsToResources(projectRoot, themeName);
151
+ const changed = writeGeneratedThemeModule(path.join(projectRoot, rel), payload);
152
+ return { themeName, payload, generatedRel: rel, changed: changed || copied };
153
+ }
154
+
155
+ module.exports = {
156
+ detectCurrentTheme,
157
+ loadThemePayload,
158
+ writeGeneratedThemeModule,
159
+ syncGeneratedTheme,
160
+ collectDirAssets,
161
+ themeAssetStamp,
162
+ };
@@ -0,0 +1,250 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+
7
+ function exists(p) {
8
+ try {
9
+ return Boolean(p) && fs.existsSync(p);
10
+ } catch {
11
+ return false;
12
+ }
13
+ }
14
+
15
+ function isWritableDir(dir) {
16
+ try {
17
+ fs.mkdirSync(dir, { recursive: true });
18
+ const probe = path.join(dir, `.write-probe-${process.pid}`);
19
+ fs.writeFileSync(probe, 'ok');
20
+ fs.unlinkSync(probe);
21
+ return true;
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ function which(bin) {
28
+ if (!bin) return '';
29
+ if (path.isAbsolute(bin) && exists(bin)) return bin;
30
+ const result = spawnSync(process.platform === 'win32' ? 'where' : 'which', [bin], {
31
+ encoding: 'utf8',
32
+ });
33
+ const line = (result.stdout || '').split(/\r?\n/).map((s) => s.trim()).find(Boolean);
34
+ if (!line) return '';
35
+ try {
36
+ return fs.realpathSync(line);
37
+ } catch {
38
+ return line;
39
+ }
40
+ }
41
+
42
+ function climbToCliRoot(start) {
43
+ let dir = start;
44
+ try {
45
+ if (exists(start) && fs.statSync(start).isFile()) dir = path.dirname(start);
46
+ } catch {
47
+ return '';
48
+ }
49
+ for (let i = 0; i < 6; i++) {
50
+ if (exists(path.join(dir, 'packages', 'engine', 'package.json'))
51
+ || exists(path.join(dir, 'packages', 'engine', 'cc.config.json'))
52
+ || exists(path.join(dir, 'packages', 'engine'))) {
53
+ return dir;
54
+ }
55
+ const parent = path.dirname(dir);
56
+ if (parent === dir) break;
57
+ dir = parent;
58
+ }
59
+ return '';
60
+ }
61
+
62
+ function resolveCocosCliRoot(env = process.env) {
63
+ if (env.COCOS_CLI_ROOT && exists(env.COCOS_CLI_ROOT)) {
64
+ return path.resolve(env.COCOS_CLI_ROOT);
65
+ }
66
+ const bin = which(env.COCOS_CLI || 'cocos');
67
+ if (bin) {
68
+ const root = climbToCliRoot(bin);
69
+ if (root) return root;
70
+ }
71
+ const fallbacks = [
72
+ '/opt/cocos-cli',
73
+ '/usr/local/cocos-cli',
74
+ path.join(env.HOME || '', 'cocos-cli'),
75
+ ];
76
+ return fallbacks.find((p) => p && exists(path.join(p, 'packages', 'engine'))) || '';
77
+ }
78
+
79
+ function copyTree(src, dest) {
80
+ if (exists(dest)) return;
81
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
82
+ const al = spawnSync('cp', ['-al', src, dest], { encoding: 'utf8' });
83
+ if (al.status === 0 && exists(dest)) return;
84
+ const a = spawnSync('cp', ['-a', src, dest], { encoding: 'utf8' });
85
+ if (a.status === 0 && exists(dest)) return;
86
+ if (typeof fs.cpSync === 'function') {
87
+ fs.cpSync(src, dest, { recursive: true });
88
+ return;
89
+ }
90
+ throw new Error(`[cocos] failed to copy ${src} → ${dest}: ${(al.stderr || a.stderr || '').trim()}`);
91
+ }
92
+
93
+ function symlinkOrSkip(from, to) {
94
+ if (exists(to)) return;
95
+ fs.symlinkSync(from, to);
96
+ }
97
+
98
+ function overlayEngine(srcEngine, destEngine) {
99
+ fs.mkdirSync(destEngine, { recursive: true });
100
+ for (const name of fs.readdirSync(srcEngine)) {
101
+ if (name === 'bin' || name === 'editor') continue;
102
+ symlinkOrSkip(path.join(srcEngine, name), path.join(destEngine, name));
103
+ }
104
+ const srcBin = path.join(srcEngine, 'bin');
105
+ const srcEditor = path.join(srcEngine, 'editor');
106
+ if (exists(srcBin)) copyTree(srcBin, path.join(destEngine, 'bin'));
107
+ else fs.mkdirSync(path.join(destEngine, 'bin'), { recursive: true });
108
+ if (exists(srcEditor)) copyTree(srcEditor, path.join(destEngine, 'editor'));
109
+ else fs.mkdirSync(path.join(destEngine, 'editor', 'library'), { recursive: true });
110
+ fs.mkdirSync(path.join(destEngine, 'bin', 'temp'), { recursive: true });
111
+ fs.mkdirSync(path.join(destEngine, 'editor', 'library'), { recursive: true });
112
+ }
113
+
114
+ function shimLinuxFbxConverter(cliRoot) {
115
+ if (process.platform !== 'linux') return false;
116
+ const binDir = path.join(cliRoot, 'node_modules', '@cocos', 'fbx-gltf-conv', 'bin');
117
+ const linux = path.join(binDir, 'linux', 'FBX-glTF-conv');
118
+ const darwin = path.join(binDir, 'darwin', 'FBX-glTF-conv');
119
+ if (!exists(linux) || !exists(darwin)) return false;
120
+ try {
121
+ fs.unlinkSync(darwin);
122
+ fs.copyFileSync(linux, darwin);
123
+ fs.chmodSync(darwin, 0o755);
124
+ return true;
125
+ } catch {
126
+ return false;
127
+ }
128
+ }
129
+
130
+ function writeBinWrapper(destRoot) {
131
+ const candidates = [
132
+ path.join(destRoot, 'src', 'cli.js'),
133
+ path.join(destRoot, 'dist', 'cli.js'),
134
+ path.join(destRoot, 'cli.js'),
135
+ ];
136
+ const cliJs = candidates.find(exists);
137
+ if (!cliJs) {
138
+ throw new Error(`[cocos] overlay missing cli entry under ${destRoot}`);
139
+ }
140
+ const binDir = path.join(destRoot, 'bin');
141
+ fs.mkdirSync(binDir, { recursive: true });
142
+ const bin = path.join(binDir, 'cocos');
143
+ fs.writeFileSync(bin, `#!/usr/bin/env node\nrequire(${JSON.stringify(cliJs)});\n`);
144
+ fs.chmodSync(bin, 0o755);
145
+ return bin;
146
+ }
147
+
148
+ function overlayNodeModules(srcNm, destNm) {
149
+ fs.mkdirSync(destNm, { recursive: true });
150
+ for (const name of fs.readdirSync(srcNm)) {
151
+ if (name === '@cocos') continue;
152
+ symlinkOrSkip(path.join(srcNm, name), path.join(destNm, name));
153
+ }
154
+ const srcCocos = path.join(srcNm, '@cocos');
155
+ if (!exists(srcCocos)) return;
156
+ const destCocos = path.join(destNm, '@cocos');
157
+ fs.mkdirSync(destCocos, { recursive: true });
158
+ for (const name of fs.readdirSync(srcCocos)) {
159
+ if (name === 'fbx-gltf-conv') {
160
+ copyTree(path.join(srcCocos, name), path.join(destCocos, name));
161
+ continue;
162
+ }
163
+ symlinkOrSkip(path.join(srcCocos, name), path.join(destCocos, name));
164
+ }
165
+ }
166
+
167
+ function overlayCocosCli(srcRoot, destRoot) {
168
+ fs.mkdirSync(destRoot, { recursive: true });
169
+ const skip = new Set(['src', 'dist', 'packages', 'bin', 'node_modules']);
170
+ for (const name of fs.readdirSync(srcRoot)) {
171
+ if (skip.has(name)) continue;
172
+ symlinkOrSkip(path.join(srcRoot, name), path.join(destRoot, name));
173
+ }
174
+
175
+ if (exists(path.join(srcRoot, 'src'))) copyTree(path.join(srcRoot, 'src'), path.join(destRoot, 'src'));
176
+ if (exists(path.join(srcRoot, 'dist'))) copyTree(path.join(srcRoot, 'dist'), path.join(destRoot, 'dist'));
177
+ if (exists(path.join(srcRoot, 'node_modules'))) {
178
+ overlayNodeModules(path.join(srcRoot, 'node_modules'), path.join(destRoot, 'node_modules'));
179
+ }
180
+
181
+ const srcPkgs = path.join(srcRoot, 'packages');
182
+ const destPkgs = path.join(destRoot, 'packages');
183
+ fs.mkdirSync(destPkgs, { recursive: true });
184
+ if (exists(srcPkgs)) {
185
+ for (const name of fs.readdirSync(srcPkgs)) {
186
+ if (name === 'engine') continue;
187
+ symlinkOrSkip(path.join(srcPkgs, name), path.join(destPkgs, name));
188
+ }
189
+ overlayEngine(path.join(srcPkgs, 'engine'), path.join(destPkgs, 'engine'));
190
+ }
191
+
192
+ const bin = writeBinWrapper(destRoot);
193
+ shimLinuxFbxConverter(destRoot);
194
+ return bin;
195
+ }
196
+
197
+ function engineNeedsOverlay(cliRoot) {
198
+ const engine = path.join(cliRoot, 'packages', 'engine');
199
+ if (!exists(engine)) return false;
200
+ return !isWritableDir(path.join(engine, 'bin', 'temp'))
201
+ || !isWritableDir(path.join(engine, 'editor', 'library'));
202
+ }
203
+
204
+ /**
205
+ * Linux 沙箱里 /opt/cocos-cli/packages/engine 只读时,把 CLI 映到项目 temp,
206
+ * 让 GlobalPaths.enginePath(join(__dirname, '../packages/engine'))落在可写目录。
207
+ */
208
+ function prepareWritableCocosCli(projectRoot, env = process.env) {
209
+ const srcRoot = resolveCocosCliRoot(env);
210
+ if (!srcRoot) {
211
+ return { overlaid: false, cliRoot: '', env: {} };
212
+ }
213
+ if (!engineNeedsOverlay(srcRoot)) {
214
+ return { overlaid: false, cliRoot: srcRoot, env: { COCOS_ENGINE: path.join(srcRoot, 'packages', 'engine') } };
215
+ }
216
+
217
+ const destRoot = path.join(projectRoot, 'temp', 'cocos-cli');
218
+ const destEngine = path.join(destRoot, 'packages', 'engine');
219
+ const destBin = path.join(destRoot, 'bin', 'cocos');
220
+ if (!(exists(destBin) && exists(destEngine) && isWritableDir(path.join(destEngine, 'bin', 'temp')))) {
221
+ if (exists(destRoot)) fs.rmSync(destRoot, { recursive: true, force: true });
222
+ overlayCocosCli(srcRoot, destRoot);
223
+ } else {
224
+ shimLinuxFbxConverter(destRoot);
225
+ }
226
+
227
+ console.log(`[cocos] engine not writable (${srcRoot}); overlay ${path.relative(projectRoot, destRoot)}`);
228
+ return {
229
+ overlaid: true,
230
+ cliRoot: destRoot,
231
+ env: {
232
+ COCOS_CLI: destBin,
233
+ COCOS_CLI_ROOT: destRoot,
234
+ COCOS_ENGINE: destEngine,
235
+ },
236
+ };
237
+ }
238
+
239
+ module.exports = {
240
+ exists,
241
+ isWritableDir,
242
+ which,
243
+ climbToCliRoot,
244
+ resolveCocosCliRoot,
245
+ overlayCocosCli,
246
+ overlayEngine,
247
+ shimLinuxFbxConverter,
248
+ engineNeedsOverlay,
249
+ prepareWritableCocosCli,
250
+ };
@@ -0,0 +1,112 @@
1
+ 'use strict';
2
+
3
+ const { describe, it, before, after, beforeEach } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const path = require('path');
8
+ const {
9
+ overlayCocosCli,
10
+ engineNeedsOverlay,
11
+ prepareWritableCocosCli,
12
+ shimLinuxFbxConverter,
13
+ } = require('./writable-cli.js');
14
+
15
+ function writeFile(file, body) {
16
+ fs.mkdirSync(path.dirname(file), { recursive: true });
17
+ fs.writeFileSync(file, body);
18
+ }
19
+
20
+ function makeCliTree(root) {
21
+ writeFile(path.join(root, 'src', 'cli.js'), 'console.log("cli");\n');
22
+ writeFile(
23
+ path.join(root, 'src', 'global.js'),
24
+ 'const { join } = require("path");\nmodule.exports = { GlobalPaths: { enginePath: join(__dirname, "..", "packages", "engine") } };\n',
25
+ );
26
+ writeFile(path.join(root, 'packages', 'engine', 'cc.config.json'), '{}\n');
27
+ writeFile(path.join(root, 'packages', 'engine', 'bin', 'keep.txt'), 'cache\n');
28
+ writeFile(path.join(root, 'packages', 'engine', 'editor', 'library', '.internal'), 'meta\n');
29
+ writeFile(path.join(root, 'packages', 'other', 'pkg.json'), '{}\n');
30
+ writeFile(path.join(root, 'node_modules', '@cocos', 'fbx-gltf-conv', 'bin', 'linux', 'FBX-glTF-conv'), 'linux-bin');
31
+ writeFile(path.join(root, 'node_modules', '@cocos', 'fbx-gltf-conv', 'bin', 'darwin', 'FBX-glTF-conv'), 'darwin-bin');
32
+ writeFile(path.join(root, 'bin', 'cocos'), '#!/usr/bin/env node\nrequire("../src/cli.js");\n');
33
+ fs.chmodSync(path.join(root, 'bin', 'cocos'), 0o755);
34
+ }
35
+
36
+ describe('writable cocos-cli overlay', () => {
37
+ let root;
38
+
39
+ beforeEach(() => {
40
+ if (root && fs.existsSync(root)) fs.rmSync(root, { recursive: true, force: true });
41
+ root = fs.mkdtempSync(path.join(os.tmpdir(), 'cocos-cli-overlay-'));
42
+ });
43
+
44
+ after(() => {
45
+ if (root && fs.existsSync(root)) fs.rmSync(root, { recursive: true, force: true });
46
+ });
47
+
48
+ it('reports overlay when engine bin/temp is not writable', () => {
49
+ const cli = path.join(root, 'opt-cli');
50
+ makeCliTree(cli);
51
+ fs.chmodSync(path.join(cli, 'packages', 'engine', 'bin'), 0o555);
52
+ assert.equal(engineNeedsOverlay(cli), true);
53
+ fs.chmodSync(path.join(cli, 'packages', 'engine', 'bin'), 0o755);
54
+ });
55
+
56
+ it('maps GlobalPaths.enginePath onto a writable overlay', () => {
57
+ const cli = path.join(root, 'opt-cli');
58
+ makeCliTree(cli);
59
+ const dest = path.join(root, 'overlay');
60
+ const bin = overlayCocosCli(cli, dest);
61
+ assert.equal(fs.existsSync(bin), true);
62
+
63
+ const enginePath = path.join(dest, 'packages', 'engine');
64
+ fs.mkdirSync(path.join(enginePath, 'bin', 'temp'), { recursive: true });
65
+ fs.writeFileSync(path.join(enginePath, 'bin', 'temp', 'ok.txt'), '1');
66
+ assert.equal(fs.readFileSync(path.join(enginePath, 'bin', 'keep.txt'), 'utf8'), 'cache\n');
67
+
68
+ const globalJs = path.join(dest, 'src', 'global.js');
69
+ delete require.cache[require.resolve(globalJs)];
70
+ const resolved = require(globalJs).GlobalPaths.enginePath;
71
+ assert.equal(resolved, enginePath);
72
+
73
+ const srcDarwin = path.join(cli, 'node_modules', '@cocos', 'fbx-gltf-conv', 'bin', 'darwin', 'FBX-glTF-conv');
74
+ const destDarwin = path.join(dest, 'node_modules', '@cocos', 'fbx-gltf-conv', 'bin', 'darwin', 'FBX-glTF-conv');
75
+ assert.equal(fs.readFileSync(srcDarwin, 'utf8'), 'darwin-bin');
76
+ if (process.platform === 'linux') {
77
+ assert.equal(fs.readFileSync(destDarwin, 'utf8'), 'linux-bin');
78
+ }
79
+ });
80
+
81
+ it('replaces darwin FBX converter with the linux binary', () => {
82
+ const cli = path.join(root, 'opt-cli');
83
+ makeCliTree(cli);
84
+ if (process.platform !== 'linux') {
85
+ const original = process.platform;
86
+ Object.defineProperty(process, 'platform', { value: 'linux' });
87
+ try {
88
+ assert.equal(shimLinuxFbxConverter(cli), true);
89
+ } finally {
90
+ Object.defineProperty(process, 'platform', { value: original });
91
+ }
92
+ } else {
93
+ assert.equal(shimLinuxFbxConverter(cli), true);
94
+ }
95
+ const darwin = path.join(cli, 'node_modules', '@cocos', 'fbx-gltf-conv', 'bin', 'darwin', 'FBX-glTF-conv');
96
+ assert.equal(fs.readFileSync(darwin, 'utf8'), 'linux-bin');
97
+ });
98
+
99
+ it('prepareWritableCocosCli overlays a locked engine into project temp', () => {
100
+ const cli = path.join(root, 'opt-cli');
101
+ makeCliTree(cli);
102
+ fs.chmodSync(path.join(cli, 'packages', 'engine', 'bin'), 0o555);
103
+ const project = path.join(root, 'game');
104
+ fs.mkdirSync(project);
105
+ const result = prepareWritableCocosCli(project, { COCOS_CLI_ROOT: cli });
106
+ assert.equal(result.overlaid, true);
107
+ assert.equal(fs.existsSync(result.env.COCOS_CLI), true);
108
+ fs.mkdirSync(path.join(result.env.COCOS_ENGINE, 'bin', 'temp'), { recursive: true });
109
+ fs.writeFileSync(path.join(result.env.COCOS_ENGINE, 'bin', 'temp', 'ok.txt'), '1');
110
+ fs.chmodSync(path.join(cli, 'packages', 'engine', 'bin'), 0o755);
111
+ });
112
+ });
@@ -1 +1,20 @@
1
- "use strict";const{allowedExtensions,webpackCommonConfig}=require("./webpack.common.js");const{runBuild,makeWebpackBuildConfig}=require("./webpack.build.js");const{runDev,makeWebpackDevConfig}=require("./webpack.dev.js");const{runViteDev,makeViteDevConfig,json5Plugin,assetInlinePlugin,fontFamilyPlugin}=require("./vite.dev.js");module.exports={allowedExtensions,webpackCommonConfig,runBuild,makeWebpackBuildConfig,runDev,makeWebpackDevConfig,runViteDev,makeViteDevConfig,json5Plugin,assetInlinePlugin,fontFamilyPlugin};
1
+ 'use strict';
2
+
3
+ const { allowedExtensions, webpackCommonConfig } = require('./webpack.common.js');
4
+ const { runBuild, makeWebpackBuildConfig } = require('./webpack.build.js');
5
+ const { runDev, makeWebpackDevConfig } = require('./webpack.dev.js');
6
+ const { runViteDev, makeViteDevConfig, json5Plugin, assetInlinePlugin, fontFamilyPlugin } = require('./vite.dev.js');
7
+
8
+ module.exports = {
9
+ allowedExtensions,
10
+ webpackCommonConfig,
11
+ runBuild,
12
+ makeWebpackBuildConfig,
13
+ runDev,
14
+ makeWebpackDevConfig,
15
+ runViteDev,
16
+ makeViteDevConfig,
17
+ json5Plugin,
18
+ assetInlinePlugin,
19
+ fontFamilyPlugin,
20
+ };