From 48cff10d1096cf549731b54c8b4b2bf6643995fd Mon Sep 17 00:00:00 2001 From: tangkai <1944876319@qq.com> Date: Tue, 23 Jun 2026 20:31:57 +0800 Subject: [PATCH 1/4] fix(builder): support injected stage package options Parse executeBuildStageTask runtime packages when they are passed as a JSON string from CLI injection. Merge the normalized package options over cocos.compile.config.json so values such as wechatgame.wechatToolsPath override persisted build settings while preserving unrelated fields. Add coverage for the wechatgame stage override case. --- src/core/builder/@types/protected/options.ts | 2 +- src/core/builder/index.ts | 32 ++++++++++++- .../test/execute-build-stage-task.spec.ts | 47 +++++++++++++++++++ 3 files changed, 78 insertions(+), 3 deletions(-) diff --git a/src/core/builder/@types/protected/options.ts b/src/core/builder/@types/protected/options.ts index 3f9a5dc5b..826278bcd 100644 --- a/src/core/builder/@types/protected/options.ts +++ b/src/core/builder/@types/protected/options.ts @@ -369,7 +369,7 @@ export interface IBuildStageOptions { platform: Platform | string; taskName?: string; logDest?: string; - packages?: Record>; + packages?: Record> | string; } export type BuildStageProgressCallback = (message: string, progress: number) => void; diff --git a/src/core/builder/index.ts b/src/core/builder/index.ts index 6965408d4..4f7450605 100644 --- a/src/core/builder/index.ts +++ b/src/core/builder/index.ts @@ -210,11 +210,12 @@ function tryReadBuildOptionsForBuildStage(options: IBuildStageOptions) { } function mergeBuildStageRuntimeOptions(buildOptions: IBuildTaskOption, options: IBuildStageOptions) { - if (!options.packages) { + const runtimePackages = normalizeBuildStageRuntimePackages(options.packages); + if (!runtimePackages) { return; } buildOptions.packages = buildOptions.packages || {}; - for (const [platform, packageOptions] of Object.entries(options.packages)) { + for (const [platform, packageOptions] of Object.entries(runtimePackages)) { buildOptions.packages[platform] = { ...(buildOptions.packages[platform] || {}), ...packageOptions, @@ -222,6 +223,33 @@ function mergeBuildStageRuntimeOptions(buildOptions: IBuildTaskOption, opti } } +function normalizeBuildStageRuntimePackages(packages: IBuildStageOptions['packages']) { + if (!packages) { + return undefined; + } + + let parsedPackages: unknown = packages; + if (typeof packages === 'string') { + try { + parsedPackages = JSON.parse(packages); + } catch (error) { + throw new Error(`Invalid build stage packages JSON: ${error instanceof Error ? error.message : String(error)}`); + } + } + + if (!parsedPackages || typeof parsedPackages !== 'object' || Array.isArray(parsedPackages)) { + throw new Error('Build stage packages must be an object.'); + } + + for (const [platform, packageOptions] of Object.entries(parsedPackages)) { + if (!packageOptions || typeof packageOptions !== 'object' || Array.isArray(packageOptions)) { + throw new Error(`Build stage package options for ${platform} must be an object.`); + } + } + + return parsedPackages as Record>; +} + export async function executeBuildStageTask(taskId: string, stageName: string, options: IBuildStageOptions, onProgress?: BuildStageProgressCallback): Promise { if (!options.taskName) { options.taskName = stageName + ' build'; diff --git a/src/core/builder/test/execute-build-stage-task.spec.ts b/src/core/builder/test/execute-build-stage-task.spec.ts index b57e55001..04c49408a 100644 --- a/src/core/builder/test/execute-build-stage-task.spec.ts +++ b/src/core/builder/test/execute-build-stage-task.spec.ts @@ -231,6 +231,53 @@ describe('executeBuildStageTask', () => { }); }); + it('parses injected packages JSON and overrides compile options for non-web stages', async () => { + const { executeBuildStageTask } = await import('../index'); + let receivedOptions: any; + const runHookModule = { + throwError: true, + run: jest.fn(async (_root: string, options: any) => { + receivedOptions = options; + }), + }; + mockReadJSONSync.mockReturnValue({ + platform: 'wechatgame', + packages: { + wechatgame: { + wechatToolsPath: 'old-tools-path', + appid: 'persisted-appid', + }, + }, + }); + mockGetHooksInfo.mockReturnValue({ + pkgNameOrder: ['wechatgame'], + infos: { + wechatgame: { + path: 'wechatgame/hooks', + internal: true, + }, + }, + }); + mockGetBuildStageWithHookTasks.mockReturnValue({ + name: 'run', + hook: 'run', + displayName: 'Run', + parallelism: 'all', + }); + mockRequireFile.mockReturnValue(runHookModule); + + await executeBuildStageTask('task-id', 'run', { + dest: 'build/wechatgame', + platform: 'wechatgame', + packages: '{"wechatgame":{"wechatToolsPath":"c:\\\\Program Files (x86)\\\\Tencent\\\\微信web开发者工具\\\\微信开发者工具.exe"}}', + }); + + expect(receivedOptions.packages.wechatgame).toEqual({ + wechatToolsPath: 'c:\\Program Files (x86)\\Tencent\\微信web开发者工具\\微信开发者工具.exe', + appid: 'persisted-appid', + }); + }); + it('uses persisted build log destination for non-web stages by default', async () => { const { executeBuildStageTask } = await import('../index'); const { newConsole } = await import('../../base/console'); From 53428862313f5adfa796444acf99deff3f2a25ce Mon Sep 17 00:00:00 2001 From: tangkai <1944876319@qq.com> Date: Wed, 24 Jun 2026 10:14:12 +0800 Subject: [PATCH 2/4] =?UTF-8?q?options.packages=20=E6=AF=94=20cocos.compil?= =?UTF-8?q?e.config.json=20=E4=B8=AD=E7=9A=84=20packages=20=E4=BC=98?= =?UTF-8?q?=E5=85=88=E7=BA=A7=E6=9C=80=E9=AB=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/builder/@types/protected/options.ts | 2 +- src/core/builder/index.ts | 32 ++----------------- .../test/execute-build-stage-task.spec.ts | 18 +++++++++-- 3 files changed, 19 insertions(+), 33 deletions(-) diff --git a/src/core/builder/@types/protected/options.ts b/src/core/builder/@types/protected/options.ts index 826278bcd..3f9a5dc5b 100644 --- a/src/core/builder/@types/protected/options.ts +++ b/src/core/builder/@types/protected/options.ts @@ -369,7 +369,7 @@ export interface IBuildStageOptions { platform: Platform | string; taskName?: string; logDest?: string; - packages?: Record> | string; + packages?: Record>; } export type BuildStageProgressCallback = (message: string, progress: number) => void; diff --git a/src/core/builder/index.ts b/src/core/builder/index.ts index 4f7450605..6965408d4 100644 --- a/src/core/builder/index.ts +++ b/src/core/builder/index.ts @@ -210,12 +210,11 @@ function tryReadBuildOptionsForBuildStage(options: IBuildStageOptions) { } function mergeBuildStageRuntimeOptions(buildOptions: IBuildTaskOption, options: IBuildStageOptions) { - const runtimePackages = normalizeBuildStageRuntimePackages(options.packages); - if (!runtimePackages) { + if (!options.packages) { return; } buildOptions.packages = buildOptions.packages || {}; - for (const [platform, packageOptions] of Object.entries(runtimePackages)) { + for (const [platform, packageOptions] of Object.entries(options.packages)) { buildOptions.packages[platform] = { ...(buildOptions.packages[platform] || {}), ...packageOptions, @@ -223,33 +222,6 @@ function mergeBuildStageRuntimeOptions(buildOptions: IBuildTaskOption, opti } } -function normalizeBuildStageRuntimePackages(packages: IBuildStageOptions['packages']) { - if (!packages) { - return undefined; - } - - let parsedPackages: unknown = packages; - if (typeof packages === 'string') { - try { - parsedPackages = JSON.parse(packages); - } catch (error) { - throw new Error(`Invalid build stage packages JSON: ${error instanceof Error ? error.message : String(error)}`); - } - } - - if (!parsedPackages || typeof parsedPackages !== 'object' || Array.isArray(parsedPackages)) { - throw new Error('Build stage packages must be an object.'); - } - - for (const [platform, packageOptions] of Object.entries(parsedPackages)) { - if (!packageOptions || typeof packageOptions !== 'object' || Array.isArray(packageOptions)) { - throw new Error(`Build stage package options for ${platform} must be an object.`); - } - } - - return parsedPackages as Record>; -} - export async function executeBuildStageTask(taskId: string, stageName: string, options: IBuildStageOptions, onProgress?: BuildStageProgressCallback): Promise { if (!options.taskName) { options.taskName = stageName + ' build'; diff --git a/src/core/builder/test/execute-build-stage-task.spec.ts b/src/core/builder/test/execute-build-stage-task.spec.ts index 04c49408a..da550bf60 100644 --- a/src/core/builder/test/execute-build-stage-task.spec.ts +++ b/src/core/builder/test/execute-build-stage-task.spec.ts @@ -231,7 +231,7 @@ describe('executeBuildStageTask', () => { }); }); - it('parses injected packages JSON and overrides compile options for non-web stages', async () => { + it('overrides compile options with injected package objects for non-web stages', async () => { const { executeBuildStageTask } = await import('../index'); let receivedOptions: any; const runHookModule = { @@ -246,6 +246,10 @@ describe('executeBuildStageTask', () => { wechatgame: { wechatToolsPath: 'old-tools-path', appid: 'persisted-appid', + nestedConfig: { + mode: 'persisted', + keepMe: true, + }, }, }, }); @@ -269,12 +273,22 @@ describe('executeBuildStageTask', () => { await executeBuildStageTask('task-id', 'run', { dest: 'build/wechatgame', platform: 'wechatgame', - packages: '{"wechatgame":{"wechatToolsPath":"c:\\\\Program Files (x86)\\\\Tencent\\\\微信web开发者工具\\\\微信开发者工具.exe"}}', + packages: { + wechatgame: { + wechatToolsPath: 'c:\\Program Files (x86)\\Tencent\\微信web开发者工具\\微信开发者工具.exe', + nestedConfig: { + mode: 'runtime', + }, + }, + }, }); expect(receivedOptions.packages.wechatgame).toEqual({ wechatToolsPath: 'c:\\Program Files (x86)\\Tencent\\微信web开发者工具\\微信开发者工具.exe', appid: 'persisted-appid', + nestedConfig: { + mode: 'runtime', + }, }); }); From 4610cf036f667e6e4ddebaa398383bfed755dd07 Mon Sep 17 00:00:00 2001 From: tangkai <1944876319@qq.com> Date: Wed, 24 Jun 2026 10:30:04 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E4=BD=BF=E7=94=A8options.logDest=E4=BD=9C?= =?UTF-8?q?=E4=B8=BA=E4=BC=98=E5=85=88=E7=BA=A7=E6=9C=80=E9=AB=98=E7=9A=84?= =?UTF-8?q?=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/builder/index.ts | 27 +++++----------- .../test/execute-build-stage-task.spec.ts | 32 +++++++++++++------ 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/src/core/builder/index.ts b/src/core/builder/index.ts index 6965408d4..129be5d6e 100644 --- a/src/core/builder/index.ts +++ b/src/core/builder/index.ts @@ -200,16 +200,13 @@ function readBuildOptionsForBuildStage(options: IBuildStageOptions) { return buildOptions; } -function tryReadBuildOptionsForBuildStage(options: IBuildStageOptions) { - options.dest = utils.Path.resolveToRaw(options.dest); - try { - return readBuildTaskOptions(options.dest); - } catch { - return undefined; +function mergeBuildStageRuntimeOptions(buildOptions: IBuildTaskOption, options: IBuildStageOptions) { + buildOptions.platform = options.platform; + (buildOptions as IBuildTaskOption & { dest?: string }).dest = options.dest; + if (options.logDest) { + buildOptions.logDest = options.logDest; } -} -function mergeBuildStageRuntimeOptions(buildOptions: IBuildTaskOption, options: IBuildStageOptions) { if (!options.packages) { return; } @@ -227,27 +224,19 @@ export async function executeBuildStageTask(taskId: string, stageName: string, o options.taskName = stageName + ' build'; } const restoreLogSink = newConsole.createLogSinkRestorer(); - const explicitLogDest = options.logDest; - ensureBuildLogSink(options, options.taskName, explicitLogDest); + ensureBuildLogSink(options, options.taskName, options.logDest); let buildStageTask: Awaited> | undefined; try { let buildOptions: IBuildTaskOption | undefined; - let savedBuildOptions: IBuildTaskOption | undefined; - if (options.platform.startsWith('web')) { - savedBuildOptions = tryReadBuildOptionsForBuildStage(options); - } else { + if (!options.platform.startsWith('web')) { options.dest = utils.Path.resolveToRaw(options.dest); - savedBuildOptions = readBuildTaskOptions(options.dest); - buildOptions = savedBuildOptions; + buildOptions = readBuildTaskOptions(options.dest); if (!buildOptions) { throw new Error('Build options is not exist!'); } mergeBuildStageRuntimeOptions(buildOptions, options); } - if (!explicitLogDest && savedBuildOptions?.logDest) { - ensureBuildLogSink(options, options.taskName, savedBuildOptions.logDest); - } buildStageTask = await createBuildStageTaskWithBuildOptions(taskId, stageName, options, buildOptions); if (onProgress) { buildStageTask.on('update', onProgress); diff --git a/src/core/builder/test/execute-build-stage-task.spec.ts b/src/core/builder/test/execute-build-stage-task.spec.ts index da550bf60..494d4089f 100644 --- a/src/core/builder/test/execute-build-stage-task.spec.ts +++ b/src/core/builder/test/execute-build-stage-task.spec.ts @@ -191,7 +191,9 @@ describe('executeBuildStageTask', () => { }), }; mockReadJSONSync.mockReturnValue({ - platform: 'openpaas', + platform: 'persisted-openpaas', + dest: 'persisted-dest', + logDest: 'persisted-log', packages: { openpaas: { versionName: '1.0.0', @@ -218,6 +220,7 @@ describe('executeBuildStageTask', () => { await executeBuildStageTask('task-id', 'upload', { dest: 'build/openpaas', platform: 'openpaas', + logDest: 'runtime-log', packages: { openpaas: { accessToken: 'token-1', @@ -229,6 +232,9 @@ describe('executeBuildStageTask', () => { versionName: '1.0.0', accessToken: 'token-1', }); + expect(receivedOptions.platform).toBe('openpaas'); + expect(receivedOptions.dest).toBe('build/openpaas'); + expect(receivedOptions.logDest).toBe(join('project-root', 'runtime-log.log')); }); it('overrides compile options with injected package objects for non-web stages', async () => { @@ -292,12 +298,15 @@ describe('executeBuildStageTask', () => { }); }); - it('uses persisted build log destination for non-web stages by default', async () => { + it('uses current stage log destination for non-web stages by default', async () => { const { executeBuildStageTask } = await import('../index'); const { newConsole } = await import('../../base/console'); + let receivedOptions: any; const uploadHookModule = { throwError: true, - upload: jest.fn(), + upload: jest.fn(async (_root: string, options: any) => { + receivedOptions = options; + }), }; mockReadJSONSync.mockReturnValue({ platform: 'openpaas', @@ -328,23 +337,26 @@ describe('executeBuildStageTask', () => { platform: 'openpaas', }); - expect(newConsole.record).toHaveBeenCalledWith(join('project-root', 'temp/builder/log/build-log.log')); + expect(newConsole.record).toHaveBeenCalledTimes(1); + const logDest = (newConsole.record as jest.Mock).mock.calls[0][0]; + expect(logDest).toMatch(/temp[\\/]builder[\\/]log[\\/]upload build-/); + expect(logDest).toMatch(/\.log$/); + expect(receivedOptions.logDest).toBe(logDest); }); - it('uses persisted build log destination for web stages without changing hook options', async () => { + it('uses current stage log destination for web stages without changing hook options', async () => { const { executeBuildStageTask } = await import('../index'); const { newConsole } = await import('../../base/console'); - mockReadJSONSync.mockReturnValue({ - platform: 'web-desktop', - logDest: 'temp/builder/log/web-build-log.log', - }); await executeBuildStageTask('task-id', 'run', { dest: 'build/web-desktop', platform: 'web-desktop', }); - expect(newConsole.record).toHaveBeenCalledWith(join('project-root', 'temp/builder/log/web-build-log.log')); + expect(newConsole.record).toHaveBeenCalledTimes(1); + const logDest = (newConsole.record as jest.Mock).mock.calls[0][0]; + expect(logDest).toMatch(/temp[\\/]builder[\\/]log[\\/]run build-/); + expect(logDest).toMatch(/\.log$/); expect(hookModule.run).toHaveBeenCalledWith('build/web-desktop', undefined); }); From eb5c9ddc716e3771c9aff55771df15783e436857 Mon Sep 17 00:00:00 2001 From: tangkai <1944876319@qq.com> Date: Wed, 24 Jun 2026 11:18:19 +0800 Subject: [PATCH 4/4] update create-build-stage-task.spec.ts --- src/core/builder/test/create-build-stage-task.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/builder/test/create-build-stage-task.spec.ts b/src/core/builder/test/create-build-stage-task.spec.ts index 4249c3dde..03537a0b6 100644 --- a/src/core/builder/test/create-build-stage-task.spec.ts +++ b/src/core/builder/test/create-build-stage-task.spec.ts @@ -165,7 +165,7 @@ describe('createBuildStageTask', () => { expect(task.buildExitRes.dest).toBe('raw:build/openpaas'); }); - it('does not treat stage logDest as build hook options during task creation', async () => { + it('merges stage logDest into build hook options without opening a log sink during task creation', async () => { const { createBuildStageTask } = await import('../index'); const buildOptions = { platform: 'openpaas', @@ -191,7 +191,7 @@ describe('createBuildStageTask', () => { }); expect(mockRecord).not.toHaveBeenCalled(); - expect(task.options.logDest).toBe('temp/builder/log/build-log.log'); + expect(task.options.logDest).toBe('custom-stage-log'); expect((task.options as any).packages.openpaas.logDest).toBeUndefined(); });