@playcraft/build 0.0.41 → 0.0.42

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.
@@ -86,6 +86,13 @@ export declare class PlayableScriptsAdapter {
86
86
  * 从参数列表中移除指定参数及其值
87
87
  */
88
88
  private removeArg;
89
+ /**
90
+ * 统一构造 pnpm CLI 参数:
91
+ * - 透传通用 flag(dangerouslyAllowAllBuilds / strict-peer-dependencies / reporter)
92
+ * - 按 NPM_REGISTRY 透传 --registry(与 helm values.yaml 的 mirrors.tencent.com 对齐)
93
+ * 让 ensureLocalDevkit 的 install 与 ensureBuildLoaders 的 add 共享同一套基础约束。
94
+ */
95
+ private buildPnpmArgs;
89
96
  /**
90
97
  * 查找项目本地的 playable-scripts 可执行文件
91
98
  * 优先查找项目 node_modules 中的版本,确保 webpack 能正确解析 loader
@@ -97,8 +104,15 @@ export declare class PlayableScriptsAdapter {
97
104
  */
98
105
  private ensureLocalDevkit;
99
106
  /**
100
- * 检查构建所需 loader 是否存在(babel-loader)
101
- * 注意:babel-loader 是必须的,esbuild-loader/esbuild 是 devkit 内部可选依赖
107
+ * 检查构建所需 loader 是否存在,缺失则尝试在 worker 容器内现场兜底安装。
108
+ *
109
+ * 分两类:
110
+ * 1) required:必须存在(babel-loader)。缺失先尝试 pnpm add 兜底,仍找不到才告警。
111
+ * 2) optional:devkit 在某些路径(例如 obfuscateLevel >= 4 时走 esbuild minify)
112
+ * 会 require('esbuild-loader'),但游戏工程模板的 package.json 通常没有显式声明,
113
+ * pnpm 严格模式下也不会被提升到顶层 node_modules。这里如果本地确认缺失,
114
+ * 就以 --save=false 现场补装到 node_modules/,不写回业务方的 package.json,
115
+ * 避免污染 lockfile。
102
116
  */
103
117
  private ensureBuildLoaders;
104
118
  /**
@@ -520,6 +520,26 @@ export class PlayableScriptsAdapter {
520
520
  }
521
521
  return result;
522
522
  }
523
+ /**
524
+ * 统一构造 pnpm CLI 参数:
525
+ * - 透传通用 flag(dangerouslyAllowAllBuilds / strict-peer-dependencies / reporter)
526
+ * - 按 NPM_REGISTRY 透传 --registry(与 helm values.yaml 的 mirrors.tencent.com 对齐)
527
+ * 让 ensureLocalDevkit 的 install 与 ensureBuildLoaders 的 add 共享同一套基础约束。
528
+ */
529
+ buildPnpmArgs(subcommand, extra = []) {
530
+ const args = [
531
+ subcommand,
532
+ ...extra,
533
+ '--config.dangerouslyAllowAllBuilds=true',
534
+ '--config.strict-peer-dependencies=false',
535
+ '--reporter=append-only',
536
+ ];
537
+ const reg = process.env.NPM_REGISTRY;
538
+ if (reg && reg !== 'https://registry.npmjs.org') {
539
+ args.push(`--registry=${reg}`);
540
+ }
541
+ return args;
542
+ }
523
543
  /**
524
544
  * 查找项目本地的 playable-scripts 可执行文件
525
545
  * 优先查找项目 node_modules 中的版本,确保 webpack 能正确解析 loader
@@ -581,94 +601,267 @@ export class PlayableScriptsAdapter {
581
601
  return found;
582
602
  }
583
603
  console.log('[PlayableScriptsAdapter] 未找到本地 playable-scripts,执行 pnpm install 安装项目依赖');
604
+ // 创建 .npmrc 文件,允许所有构建脚本执行
605
+ // 这样可以让 esbuild 等 native 模块正确编译
606
+ //
607
+ // 注意:新版 pnpm(>=9)引入了 onlyBuiltDependencies 安全策略,
608
+ // 未批准的包(esbuild/sharp 等)的 lifecycle 脚本会被忽略,
609
+ // 并以 exit code 1 退出(即使包本身已经全部解压成功),
610
+ // 日志会出现 [ERR_PNPM_IGNORED_BUILDS]。
611
+ // 这里通过 dangerously-allow-all-builds=true 关掉这个保护,
612
+ // 保证 install 在 PlayCraft 构建场景里以 0 退出。
613
+ const npmrcPath = path.join(this.projectDir, '.npmrc');
614
+ // 关于 hoist 配置:
615
+ // shamefully-hoist=true 在新版 pnpm 上对间接依赖的 hoist 行为并不可靠(实测
616
+ // esbuild-loader / json5-loader / style-loader 这类作为 @playcraft/devkit 的
617
+ // 间接依赖仍然停留在 .pnpm 隔离目录,不会被提到顶层 node_modules,导致
618
+ // webpack 的 enhanced-resolve 在 cwd=projectDir 时找不到这些 loader,
619
+ // 报 "Can't resolve 'xxx-loader'")。
620
+ // 因此显式追加 public-hoist-pattern,把所有 *-loader 以及 esbuild 系列
621
+ // 主动 hoist 到顶层;相比 shamefully-hoist=true 更显式、更稳定。
622
+ // 即使 hoist 仍然失败,下面的 ensureBuildLoaders() 会再用 .pnpm 软链兜底。
623
+ const npmrcContent = [
624
+ '# Temporary config for PlayCraft build',
625
+ 'enable-pre-post-scripts=true',
626
+ 'shamefully-hoist=true',
627
+ 'public-hoist-pattern[]=*-loader',
628
+ 'public-hoist-pattern[]=esbuild',
629
+ 'public-hoist-pattern[]=esbuild-*',
630
+ 'public-hoist-pattern[]=@esbuild/*',
631
+ 'ignore-scripts=false',
632
+ 'strict-peer-dependencies=false',
633
+ 'dangerously-allow-all-builds=true',
634
+ ].join('\n');
635
+ try {
636
+ await fs.writeFile(npmrcPath, npmrcContent, 'utf-8');
637
+ console.log('[PlayableScriptsAdapter] 已创建 .npmrc 配置(允许构建脚本)');
638
+ }
639
+ catch (error) {
640
+ console.log('[PlayableScriptsAdapter] ⚠️ 无法创建 .npmrc:', error.message);
641
+ }
642
+ // pnpm install 命令兜底:CLI flag 与 .npmrc 双保险,并透传 NPM_REGISTRY
643
+ // (helm values.yaml 配置了 https://mirrors.tencent.com/npm,避免外网慢/不通)
644
+ const installArgs = this.buildPnpmArgs('install', ['--config.confirm-modules-purge=false']);
645
+ let installError;
584
646
  try {
585
647
  // 执行 pnpm install 安装 package.json 中定义的所有依赖
586
648
  // 包括 @playcraft/devkit 和 babel-loader 等构建工具
587
649
  // 注意:不要设置 NODE_ENV=production,否则会跳过 devDependencies
588
- // 创建 .npmrc 文件,允许所有构建脚本执行
589
- // 这样可以让 esbuild 等 native 模块正确编译
590
- const npmrcPath = path.join(this.projectDir, '.npmrc');
591
- const npmrcContent = [
592
- '# Temporary config for PlayCraft build',
593
- 'enable-pre-post-scripts=true',
594
- 'shamefully-hoist=true',
595
- ].join('\n');
596
- try {
597
- await fs.writeFile(npmrcPath, npmrcContent, 'utf-8');
598
- console.log('[PlayableScriptsAdapter] 已创建 .npmrc 配置(允许构建脚本)');
599
- }
600
- catch (error) {
601
- console.log('[PlayableScriptsAdapter] ⚠️ 无法创建 .npmrc:', error.message);
602
- }
603
- await this.runCommandWithTimeout('pnpm', ['install'], {
650
+ await this.runCommandWithTimeout('pnpm', installArgs, {
604
651
  cwd: this.projectDir,
605
652
  timeout: 5 * 60 * 1000,
606
653
  env: process.env,
607
654
  });
608
- // 清理临时 .npmrc
609
- try {
610
- await fs.unlink(npmrcPath);
611
- console.log('[PlayableScriptsAdapter] 已清理临时 .npmrc');
612
- }
613
- catch {
614
- // 忽略删除错误
615
- }
616
- // 安装完成后,尝试通过 require.resolve 查找 devkit 路径
617
- try {
618
- const devkitPath = require.resolve('@playcraft/devkit/package.json', {
619
- paths: [this.projectDir],
620
- });
621
- const devkitDir = path.dirname(devkitPath);
622
- // 尝试常见的 CLI 路径
623
- const possiblePaths = [
624
- path.join(devkitDir, 'cli', 'bin', 'playable-scripts.js'),
625
- path.join(devkitDir, 'dist', 'cli.js'),
626
- path.join(devkitDir, 'bin', 'playable-scripts.js'),
627
- ];
628
- for (const binPath of possiblePaths) {
629
- try {
630
- await fs.access(binPath);
631
- console.log(`[PlayableScriptsAdapter] 安装后找到 playable-scripts: ${binPath}`);
632
- return binPath;
633
- }
634
- catch {
635
- // 继续尝试下一个
655
+ }
656
+ catch (error) {
657
+ // 不立即放弃:新版 pnpm 即使 install 实际成功,也可能因 ERR_PNPM_IGNORED_BUILDS
658
+ // 等告警退出非零;devkit 本身不需要 native build,只要 node_modules 里能定位到
659
+ // CLI 入口,构建即可继续。先记下错误,后面 verify 再决定是否真正失败。
660
+ installError = error;
661
+ console.log(`[PlayableScriptsAdapter] ⚠️ pnpm install 退出非零,将再次校验 node_modules 实际状态: ${error.message}`);
662
+ }
663
+ // 清理临时 .npmrc
664
+ try {
665
+ await fs.unlink(npmrcPath);
666
+ console.log('[PlayableScriptsAdapter] 已清理临时 .npmrc');
667
+ }
668
+ catch {
669
+ // 忽略删除错误
670
+ }
671
+ // 安装后无论 pnpm 是否报错,都尝试在 node_modules 中定位 devkit CLI
672
+ // 1) 通过 require.resolve 找到 devkit 包目录
673
+ try {
674
+ const devkitPath = require.resolve('@playcraft/devkit/package.json', {
675
+ paths: [this.projectDir],
676
+ });
677
+ const devkitDir = path.dirname(devkitPath);
678
+ // 尝试常见的 CLI 路径
679
+ const possiblePaths = [
680
+ path.join(devkitDir, 'cli', 'bin', 'playable-scripts.js'),
681
+ path.join(devkitDir, 'dist', 'cli.js'),
682
+ path.join(devkitDir, 'bin', 'playable-scripts.js'),
683
+ ];
684
+ for (const binPath of possiblePaths) {
685
+ try {
686
+ await fs.access(binPath);
687
+ console.log(`[PlayableScriptsAdapter] 安装后找到 playable-scripts: ${binPath}`);
688
+ if (installError) {
689
+ console.log('[PlayableScriptsAdapter] ✅ pnpm install 虽然退出非零,但 devkit 已就绪,忽略告警继续构建');
636
690
  }
691
+ return binPath;
692
+ }
693
+ catch {
694
+ // 继续尝试下一个
637
695
  }
638
- console.error(`[PlayableScriptsAdapter] ❌ 找到 devkit 但无法定位 CLI: ${devkitDir}`);
639
- return null;
640
696
  }
641
- catch {
642
- // 如果 require.resolve 失败,回退到 findLocalPlayableScripts
697
+ console.error(`[PlayableScriptsAdapter] ❌ 找到 devkit 但无法定位 CLI: ${devkitDir}`);
698
+ }
699
+ catch {
700
+ // require.resolve 失败,回退到 findLocalPlayableScripts
701
+ }
702
+ // 2) 回退:直接查 node_modules/.bin/playable-scripts
703
+ const fallback = await this.findLocalPlayableScripts();
704
+ if (fallback) {
705
+ if (installError) {
706
+ console.log('[PlayableScriptsAdapter] ✅ pnpm install 虽然退出非零,但 .bin 中已存在 playable-scripts,忽略告警继续构建');
643
707
  }
644
- // 再次尝试查找
645
- return await this.findLocalPlayableScripts();
708
+ return fallback;
646
709
  }
647
- catch (error) {
648
- console.log(`[PlayableScriptsAdapter] ⚠️ npm install 失败: ${error.message}`);
649
- return null;
710
+ // 真的没找到才报错
711
+ if (installError) {
712
+ console.log(`[PlayableScriptsAdapter] ⚠️ npm install 失败: ${installError.message}`);
650
713
  }
714
+ return null;
651
715
  }
652
716
  /**
653
- * 检查构建所需 loader 是否存在(babel-loader)
654
- * 注意:babel-loader 是必须的,esbuild-loader/esbuild 是 devkit 内部可选依赖
717
+ * 检查构建所需 loader 是否存在,缺失则尝试在 worker 容器内现场兜底安装。
718
+ *
719
+ * 分两类:
720
+ * 1) required:必须存在(babel-loader)。缺失先尝试 pnpm add 兜底,仍找不到才告警。
721
+ * 2) optional:devkit 在某些路径(例如 obfuscateLevel >= 4 时走 esbuild minify)
722
+ * 会 require('esbuild-loader'),但游戏工程模板的 package.json 通常没有显式声明,
723
+ * pnpm 严格模式下也不会被提升到顶层 node_modules。这里如果本地确认缺失,
724
+ * 就以 --save=false 现场补装到 node_modules/,不写回业务方的 package.json,
725
+ * 避免污染 lockfile。
655
726
  */
656
727
  async ensureBuildLoaders() {
657
728
  const required = ['babel-loader'];
658
- const missing = [];
659
- for (const name of required) {
660
- const modulePath = path.join(this.projectDir, 'node_modules', name);
729
+ // optional 列表覆盖 @playcraft/devkit 的 webpack.build.js 实际会 require 的全部 loader:
730
+ // - esbuild-loader / esbuild:obfuscate-level=4 esbuild minify 路径
731
+ // - json5-loader:解析 .json5 / 主题数据
732
+ // - style-loader / css-loader:处理样式资源
733
+ // 这些都是业务工程 package.json 里没显式声明的间接依赖,pnpm 默认不会
734
+ // 提到顶层 node_modules,但实际都已存在于 .pnpm 隔离目录里,可以走
735
+ // linkFromPnpmStore 毫秒级软链兜底(零网络、不动 lockfile)。
736
+ const optional = ['esbuild-loader', 'esbuild', 'json5-loader', 'style-loader', 'css-loader'];
737
+ // ⚠️ 这里的"存在"必须与 webpack 的 enhanced-resolve 行为对齐:
738
+ // 只看 projectDir/node_modules/{name}/package.json 是否可达。
739
+ //
740
+ // 不能用 require.resolve(name, { paths }):
741
+ // pnpm 严格模式(默认 isolated)下,间接依赖不会被提到顶层 node_modules,
742
+ // 但会作为 .pnpm/{pkg}@{ver}/node_modules/{pkg} 存在,require.resolve
743
+ // 会顺着这条隔离图找到它(true positive 对 Node 来说),
744
+ // 而 webpack 的 resolve 在 cwd=source 下只看 source/node_modules/{name},
745
+ // 会得到 "Can't resolve 'esbuild-loader'"(false negative 对它来说)。
746
+ // 这种不一致正是当前测试环境构建失败的根因。
747
+ const checkExists = async (name) => {
748
+ try {
749
+ await fs.access(path.join(this.projectDir, 'node_modules', name, 'package.json'));
750
+ return true;
751
+ }
752
+ catch {
753
+ return false;
754
+ }
755
+ };
756
+ // 优先策略:直接从 .pnpm/<name>@*/node_modules/<name> 软链到顶层。
757
+ // 命中场景:作为 @playcraft/devkit 的间接依赖,esbuild-loader/esbuild
758
+ // 已经被 pnpm 下载到 .pnpm 隔离目录,但没提到顶层。
759
+ // 比 pnpm add 快得多(毫秒级、零网络),并且不动 lockfile。
760
+ const linkFromPnpmStore = async (name) => {
761
+ const pnpmDir = path.join(this.projectDir, 'node_modules', '.pnpm');
762
+ let entries;
661
763
  try {
662
- await fs.access(modulePath);
764
+ entries = await fs.readdir(pnpmDir);
663
765
  }
664
766
  catch {
665
- missing.push(name);
767
+ return false;
768
+ }
769
+ // 形如 esbuild-loader@4.4.3_webpack@5.106.2 / esbuild@0.21.5
770
+ // 注意:name 后面紧跟 '@' 才是同名包,避免 'esbuild' 误匹配 'esbuild-loader'
771
+ const prefix = `${name}@`;
772
+ const candidates = entries.filter((e) => e.startsWith(prefix));
773
+ if (candidates.length === 0)
774
+ return false;
775
+ // 多个版本时挑最大的(用 numeric collator,避免 0.21.5 误大于 0.10.0 这类字典序坑)
776
+ candidates.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
777
+ const chosen = candidates[candidates.length - 1];
778
+ const realPkgDir = path.join(pnpmDir, chosen, 'node_modules', name);
779
+ try {
780
+ await fs.access(path.join(realPkgDir, 'package.json'));
666
781
  }
782
+ catch {
783
+ return false;
784
+ }
785
+ const linkPath = path.join(this.projectDir, 'node_modules', name);
786
+ try {
787
+ // 先清理可能存在的同名残留(损坏的软链等)
788
+ try {
789
+ await fs.rm(linkPath, { recursive: true, force: true });
790
+ }
791
+ catch {
792
+ // ignore
793
+ }
794
+ // 用相对软链,路径更短、镜像迁移友好
795
+ const rel = path.relative(path.dirname(linkPath), realPkgDir);
796
+ await fs.symlink(rel, linkPath, 'dir');
797
+ console.log(`[PlayableScriptsAdapter] 🔗 从 .pnpm 软链 ${name} → ${rel}`);
798
+ return true;
799
+ }
800
+ catch (error) {
801
+ console.log(`[PlayableScriptsAdapter] ⚠️ 软链 ${name} 失败: ${error.message}`);
802
+ return false;
803
+ }
804
+ };
805
+ const ensureOne = async (name) => {
806
+ if (await checkExists(name))
807
+ return true;
808
+ if (await linkFromPnpmStore(name)) {
809
+ return await checkExists(name);
810
+ }
811
+ return false;
812
+ };
813
+ // 过滤出 ensureOne 之后仍缺失的包;首轮检测和 verify 阶段都复用此 helper。
814
+ const filterMissing = async (names) => {
815
+ const missing = [];
816
+ for (const n of names) {
817
+ if (!(await ensureOne(n)))
818
+ missing.push(n);
819
+ }
820
+ return missing;
821
+ };
822
+ const missingRequired = await filterMissing(required);
823
+ const missingOptional = await filterMissing(optional);
824
+ const toInstall = [...missingRequired, ...missingOptional];
825
+ if (toInstall.length === 0)
826
+ return;
827
+ console.log(`[PlayableScriptsAdapter] 以下构建依赖缺失(.pnpm 也没有),尝试现场兜底安装: ${toInstall.join(', ')}`);
828
+ // 用 pnpm add -D --save=false 兜底安装:
829
+ // - --save=false 不写回 package.json,避免污染业务工程;
830
+ // - --config.dangerouslyAllowAllBuilds=true 跳过新版 pnpm 的 ignored-builds 退码(buildPnpmArgs 已带);
831
+ // - 透传 NPM_REGISTRY,与 ensureLocalDevkit 保持一致(走 mirrors.tencent.com)。
832
+ const installArgs = this.buildPnpmArgs('add', ['-D', '--save=false', ...toInstall]);
833
+ let addError;
834
+ try {
835
+ await this.runCommandWithTimeout('pnpm', installArgs, {
836
+ cwd: this.projectDir,
837
+ timeout: 3 * 60 * 1000,
838
+ env: process.env,
839
+ });
840
+ }
841
+ catch (error) {
842
+ addError = error;
843
+ console.log(`[PlayableScriptsAdapter] ⚠️ pnpm add 退出非零,将再次校验是否实际安装成功: ${error.message}`);
667
844
  }
668
- if (missing.length > 0) {
669
- console.log(`[PlayableScriptsAdapter] ⚠️ 以下构建依赖缺失: ${missing.join(', ')}`);
845
+ // 安装后再 verify 一遍:先 checkExists,未命中再尝试一次软链兜底
846
+ // (pnpm add 默认也是 isolated,可能依然没提到顶层)
847
+ const stillMissingRequired = await filterMissing(missingRequired);
848
+ const stillMissingOptional = await filterMissing(missingOptional);
849
+ if (stillMissingRequired.length > 0) {
850
+ console.log(`[PlayableScriptsAdapter] ❌ 必需的构建依赖仍缺失: ${stillMissingRequired.join(', ')}`);
670
851
  console.log(`[PlayableScriptsAdapter] 请确保这些依赖已在 package.json 的 devDependencies 中定义`);
671
852
  }
853
+ else if (missingRequired.length > 0) {
854
+ console.log(`[PlayableScriptsAdapter] ✅ 必需依赖已就绪: ${missingRequired.join(', ')}`);
855
+ }
856
+ if (stillMissingOptional.length > 0) {
857
+ console.log(`[PlayableScriptsAdapter] ⚠️ 可选构建依赖仍缺失: ${stillMissingOptional.join(', ')}(如果 devkit 在当前混淆等级下不需要它们,构建仍可能成功)`);
858
+ }
859
+ else if (missingOptional.length > 0) {
860
+ console.log(`[PlayableScriptsAdapter] ✅ 可选依赖已就绪: ${missingOptional.join(', ')}`);
861
+ }
862
+ if (addError && stillMissingRequired.length === 0 && stillMissingOptional.length === 0) {
863
+ console.log('[PlayableScriptsAdapter] pnpm add 虽然退出非零,但所有依赖已就绪,忽略告警继续构建');
864
+ }
672
865
  }
673
866
  /**
674
867
  * 确保 package.json 中有 build/builds 脚本
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcraft/build",
3
- "version": "0.0.41",
3
+ "version": "0.0.42",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",