From f8bb934b021a778c695afa076851fe27ab4597a3 Mon Sep 17 00:00:00 2001 From: Tommy Nguyen <4123478+tido64@users.noreply.github.com> Date: Mon, 23 Jun 2025 13:24:11 +0200 Subject: [PATCH] fix(windows): allow specifying MSBuild properties --- .github/workflows/build.yml | 4 +- package.json | 2 +- scripts/types.ts | 61 +++++++++++---------- test/pack.test.ts | 2 +- test/windows/copyAndReplace.test.ts | 2 +- test/windows/findUserProjects.test.ts | 2 +- test/windows/generateSolution.test.ts | 2 +- test/windows/parseMSBuildProperties.test.ts | 28 ++++++++++ test/windows/replaceContent.test.ts | 2 +- windows/ExperimentalFeatures.props | 2 + windows/{test-app.mjs => app.mjs} | 40 +++++++++++++- yarn.lock | 2 +- 12 files changed, 107 insertions(+), 42 deletions(-) create mode 100644 test/windows/parseMSBuildProperties.test.ts rename windows/{test-app.mjs => app.mjs} (91%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cc5612fc9..137beed59 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -564,7 +564,7 @@ jobs: yarn build:windows - name: Generate Visual Studio solution run: | - yarn install-windows-test-app --use-nuget + yarn install-windows-test-app --use-nuget --msbuildprops WindowsTargetPlatformVersion=10.0.26100.0 working-directory: example - name: Test `react-native config` run: | @@ -616,7 +616,7 @@ jobs: yarn build:windows - name: Generate Visual Studio solution run: | - yarn install-windows-test-app --use-nuget + yarn install-windows-test-app --use-nuget --msbuildprops WindowsTargetPlatformVersion=10.0.26100.0 working-directory: template-example - name: Determine whether the Windows app needs to be built id: affected diff --git a/package.json b/package.json index 13a5506e0..ee6ce2fd0 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "init": "scripts/init.mjs", "init-test-app": "scripts/init.mjs", "configure-test-app": "scripts/configure.mjs", - "install-windows-test-app": "windows/test-app.mjs" + "install-windows-test-app": "windows/app.mjs" }, "repository": { "type": "git", diff --git a/scripts/types.ts b/scripts/types.ts index a55d8bf86..b3c016d6d 100644 --- a/scripts/types.ts +++ b/scripts/types.ts @@ -177,6 +177,36 @@ type InferredOptionTypes = { [key in keyof O]: InferredOptionType }; export type Args = InferredOptionTypes & { _: string[] }; +/************************ + * windows/app.mjs * + ************************/ + +type Resources = string[] | { windows?: string[] }; + +export type AssetItems = { + assetItems: string[]; + assetItemFilters: string[]; + assetFilters: string[]; +}; + +export type Assets = { + assetItems: string; + assetItemFilters: string; + assetFilters: string; +}; + +export type AppManifest = { + name?: string; + singleApp?: string; + resources?: Resources; + windows?: { + appxManifest?: string; + certificateKeyFile?: string; + certificatePassword?: string; + certificateThumbprint?: string; + }; +}; + /*********************** * windows/project.mjs * ***********************/ @@ -193,6 +223,7 @@ export type AppxBundle = { export type MSBuildProjectOptions = { autolink: boolean; + msbuildprops?: string; useFabric?: boolean; useHermes?: boolean; useNuGet: boolean; @@ -218,36 +249,6 @@ export type MSBuildProjectConfigurator = ( info: ProjectInfo ) => MSBuildProjectParams; -/************************ - * windows/test-app.mjs * - ************************/ - -type Resources = string[] | { windows?: string[] }; - -export type AssetItems = { - assetItems: string[]; - assetItemFilters: string[]; - assetFilters: string[]; -}; - -export type Assets = { - assetItems: string; - assetItemFilters: string; - assetFilters: string; -}; - -export type AppManifest = { - name?: string; - singleApp?: string; - resources?: Resources; - windows?: { - appxManifest?: string; - certificateKeyFile?: string; - certificatePassword?: string; - certificateThumbprint?: string; - }; -}; - /************** * schema.mjs * **************/ diff --git a/test/pack.test.ts b/test/pack.test.ts index ba223d1e0..1923737f3 100644 --- a/test/pack.test.ts +++ b/test/pack.test.ts @@ -322,8 +322,8 @@ describe("npm pack", () => { "windows/Win32/pch.h", "windows/Win32/resource.h", "windows/Win32/targetver.h", + "windows/app.mjs", "windows/project.mjs", - "windows/test-app.mjs", "windows/uwp.mjs", "windows/win32.mjs", ]); diff --git a/test/windows/copyAndReplace.test.ts b/test/windows/copyAndReplace.test.ts index 71138bbcc..8763273c5 100644 --- a/test/windows/copyAndReplace.test.ts +++ b/test/windows/copyAndReplace.test.ts @@ -1,7 +1,7 @@ import { equal, fail, match, rejects } from "node:assert/strict"; import { afterEach, describe, it } from "node:test"; import { readTextFile as readTextFileActual } from "../../scripts/helpers.js"; -import { copyAndReplace as copyAndReplaceActual } from "../../windows/test-app.mjs"; +import { copyAndReplace as copyAndReplaceActual } from "../../windows/app.mjs"; import { fs, setMockFiles } from "../fs.mock.ts"; describe("copyAndReplace()", () => { diff --git a/test/windows/findUserProjects.test.ts b/test/windows/findUserProjects.test.ts index ca44bb0db..e361da3b0 100644 --- a/test/windows/findUserProjects.test.ts +++ b/test/windows/findUserProjects.test.ts @@ -2,7 +2,7 @@ import { deepEqual, equal } from "node:assert/strict"; import * as os from "node:os"; import * as path from "node:path"; import { describe, it } from "node:test"; -import { findUserProjects, toProjectEntry } from "../../windows/test-app.mjs"; +import { findUserProjects, toProjectEntry } from "../../windows/app.mjs"; describe("findUserProjects()", () => { it("finds all user projects, ignoring android/ios/macos/node_modules", () => { diff --git a/test/windows/generateSolution.test.ts b/test/windows/generateSolution.test.ts index 36c0ae87a..75da4d83e 100644 --- a/test/windows/generateSolution.test.ts +++ b/test/windows/generateSolution.test.ts @@ -1,7 +1,7 @@ import { equal } from "node:assert/strict"; import * as path from "node:path"; import { afterEach, beforeEach, describe, it } from "node:test"; -import { generateSolution as generateSolutionActual } from "../../windows/test-app.mjs"; +import { generateSolution as generateSolutionActual } from "../../windows/app.mjs"; import { fs, setMockFiles } from "../fs.mock.ts"; describe("generateSolution()", () => { diff --git a/test/windows/parseMSBuildProperties.test.ts b/test/windows/parseMSBuildProperties.test.ts new file mode 100644 index 000000000..a29c3c7e8 --- /dev/null +++ b/test/windows/parseMSBuildProperties.test.ts @@ -0,0 +1,28 @@ +import { equal, ok, throws } from "node:assert/strict"; +import { describe, it } from "node:test"; +import { parseMSBuildProperties } from "../../windows/app.mjs"; + +describe("parseMSBuildProperties()", () => { + it("handles empty string", () => { + ok(!parseMSBuildProperties(undefined), "should return undefined"); + ok(!parseMSBuildProperties(""), "should return undefined"); + }); + + it("parses single property", () => { + equal(parseMSBuildProperties("Prop=Value"), "Value"); + }); + + it("parses multiple properties", () => { + equal( + parseMSBuildProperties("Prop1=Value1,Prop2=Value2"), + "Value1\nValue2" + ); + }); + + it("throws on invalid input", () => { + throws(() => parseMSBuildProperties("NULL")); + throws(() => parseMSBuildProperties("Prop=")); + throws(() => parseMSBuildProperties("=Value")); + throws(() => parseMSBuildProperties("Prop=Value,")); + }); +}); diff --git a/test/windows/replaceContent.test.ts b/test/windows/replaceContent.test.ts index e954c654f..4195bb09e 100644 --- a/test/windows/replaceContent.test.ts +++ b/test/windows/replaceContent.test.ts @@ -1,6 +1,6 @@ import { equal } from "node:assert/strict"; import { describe, it } from "node:test"; -import { replaceContent } from "../../windows/test-app.mjs"; +import { replaceContent } from "../../windows/app.mjs"; describe("replaceContent()", () => { it("returns same string with no replacements", () => { diff --git a/windows/ExperimentalFeatures.props b/windows/ExperimentalFeatures.props index cf924e110..26b727e29 100644 --- a/windows/ExperimentalFeatures.props +++ b/windows/ExperimentalFeatures.props @@ -33,6 +33,8 @@ --> false + + true diff --git a/windows/test-app.mjs b/windows/app.mjs similarity index 91% rename from windows/test-app.mjs rename to windows/app.mjs index 687ca952f..d719e045a 100755 --- a/windows/test-app.mjs +++ b/windows/app.mjs @@ -62,6 +62,29 @@ export function findUserProjects(projectDir, projects = [], fs = nodefs) { }, projects); } +/** + * @param {string | undefined} msbuildprops + * @returns {string | undefined} XML elements for additional MSBuild properties. + */ +export function parseMSBuildProperties(msbuildprops) { + if (!msbuildprops) { + return undefined; + } + + return msbuildprops + .split(",") + .map((prop) => { + const [name, value] = prop.split("="); + if (!name || !value) { + throw new Error( + `Invalid MSBuild property: "${prop}"; expected format: "Name=Value".` + ); + } + return `<${name}>${value}`; + }) + .join("\n"); +} + /** * Replaces parts in specified content. * @param {string} content Content to be replaced. @@ -232,7 +255,7 @@ export async function generateSolution(destPath, options, fs = nodefs) { const props = path.relative(process.cwd(), experimentalFeaturesPropsPath); console.log(colors.cyan(colors.bold("info")), `'${props}' already exists`); } else { - const { useHermes } = options; + const { msbuildprops, useHermes } = options; const { useExperimentalNuGet, useFabric, versionNumber } = info; const url = new URL(experimentalFeaturesPropsFilename, import.meta.url); copyAndReplaceAsync(fileURLToPath(url), experimentalFeaturesPropsPath, { @@ -241,6 +264,7 @@ export async function generateSolution(destPath, options, fs = nodefs) { "true": `${useHermes == null ? versionNumber >= v(0, 73, 0) : useHermes}`, "false": `${useFabric}`, "false": `${useExperimentalNuGet}`, + "": msbuildprops ?? "", }); } @@ -322,6 +346,10 @@ if (isMain(import.meta.url)) { type: "boolean", default: os.platform() === "win32", }, + msbuildprops: { + description: `Comma-separated properties passed to MSBuild e.g., UseWinUI3=true,WindowsTargetPlatformVersion=10.0.26100.0`, + type: "string", + }, "use-fabric": { description: "Use New Architecture [experimental] (supported on 0.73+)", type: "boolean", @@ -340,13 +368,19 @@ if (isMain(import.meta.url)) { async ({ "project-directory": projectDirectory, autolink, + msbuildprops, "use-fabric": useFabric, "use-hermes": useHermes, "use-nuget": useNuGet, }) => { - const options = { autolink, useFabric, useHermes, useNuGet }; const destPath = path.resolve(projectDirectory); - const error = await generateSolution(destPath, options); + const error = await generateSolution(destPath, { + autolink, + msbuildprops: parseMSBuildProperties(msbuildprops), + useFabric, + useHermes, + useNuGet, + }); if (error) { console.error(error); process.exitCode = 1; diff --git a/yarn.lock b/yarn.lock index a8d9f9449..08eeb975d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12178,7 +12178,7 @@ __metadata: configure-test-app: scripts/configure.mjs init: scripts/init.mjs init-test-app: scripts/init.mjs - install-windows-test-app: windows/test-app.mjs + install-windows-test-app: windows/app.mjs languageName: unknown linkType: soft