Update RNW CLI to preserve and re-use `init-windows` arguments (#13988)

## Description

This PR updates the RNW CLI to preserve and re-use the arguments passed to `init-windows` when the project was created.

### Type of Change
- Bug fix (non-breaking change which fixes an issue)
- New feature (non-breaking change which adds functionality)
- This change requires a documentation update

### Why
The RNW CLI should be smart enough to change its command's behaviors based on the template used by the project.

Closes #13927 
Closes #13926 
Closes #13937 
Closes #13174 

### What

As per #13927:
1. The `init-windows` command will now stamp the `template`, `name`, and `namespace` arguments to the project's `package.json` file as follows:
    ```json
    {
      "react-native-windows": {
        "init-windows": {
          "name": "testcli",
          "namespace": "testcli",
          "template": "cpp-app",
        },
      },
    }
    ```
2. The `config` command (and therefore all other CLI commands) will now contain the contents of this new `react-native-windows` section in a new `rnwConfig` property.

Then, as per #13937:
1. The `init-windows` command will now use the previously saved arguments if they're available (and not otherwise overwritten by passed-in arguments)
2. If the template arg is missing (as it will be for all previously created projects) the `config` command will try to determine which template was used and inject it

Additionally, as per #13926:
1. The `autolink-windows` command will now use the template information to use the correct files when autolinking

Bonus, as per #13174:
1. The `autolink-windows` command will now include both the CPP and H file (the H file wasn't being regenerated before)

## Screenshots
N/A

## Testing
Verified new projects contain the new information.

## Changelog
Should this change be included in the release notes: _yes_

Update RNW CLI to preserve and re-use `init-windows` arguments
This commit is contained in:
Jon Thysell 2024-10-31 10:03:41 -07:00 коммит произвёл GitHub
Родитель 46d8c0a5bd
Коммит a9afb5fdda
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
13 изменённых файлов: 279 добавлений и 115 удалений

Просмотреть файл

@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "Update RNW CLI to preserve and re-use `init-windows` arguments",
"packageName": "@react-native-windows/cli",
"email": "jthysell@microsoft.com",
"dependentChangeType": "patch"
}

Просмотреть файл

@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "Update RNW CLI to preserve and re-use `init-windows` arguments",
"packageName": "react-native-windows",
"email": "jthysell@microsoft.com",
"dependentChangeType": "patch"
}

Просмотреть файл

@ -119,18 +119,12 @@ export class AutoLinkWindows {
// Generating cs/cpp files for app code consumption
if (projectLang === 'cs') {
this.changesNecessary =
(await this.generateCSAutolinking(
templateRoot,
projectLang,
projectDir,
)) || this.changesNecessary;
(await this.generateCSAutolinking(templateRoot, projectDir)) ||
this.changesNecessary;
} else if (projectLang === 'cpp') {
this.changesNecessary =
(await this.generateCppAutolinking(
templateRoot,
projectLang,
projectDir,
)) || this.changesNecessary;
(await this.generateCppAutolinking(templateRoot, projectDir)) ||
this.changesNecessary;
}
// Generating props for app project consumption
@ -284,23 +278,51 @@ export class AutoLinkWindows {
});
}
private getAutolinkTemplateFile(
templateRoot: string,
targetFile: string,
): string {
if (templateRoot.endsWith('\\template')) {
// Old template split into different folders
switch (path.extname(targetFile)) {
case '.cpp':
case '.h':
templateRoot = path.join(templateRoot, 'cpp-app', 'src');
break;
case '.cs':
templateRoot = path.join(templateRoot, 'cs-app', 'src');
break;
case '.props':
case '.targets':
default:
templateRoot = path.join(templateRoot, 'shared-app', 'src');
break;
}
} else {
// New template with projected layout
templateRoot = path.join(templateRoot, 'windows', 'MyApp');
}
return path.join(templateRoot, targetFile);
}
private async generateCppAutolinking(
templateRoot: string,
projectLang: string,
projectDir: string,
) {
const {cppPackageProviders, cppIncludes} = this.getCppReplacements();
const cppFileName = 'AutolinkedNativeModules.g.cpp';
const headerFileName = 'AutolinkedNativeModules.g.h';
const srcCppFile = path.join(
const srcCppFile = this.getAutolinkTemplateFile(templateRoot, cppFileName);
const srcHeaderFile = this.getAutolinkTemplateFile(
templateRoot,
`${projectLang}-app`,
'src',
cppFileName,
headerFileName,
);
const destCppFile = path.join(projectDir, cppFileName);
const destHeaderFile = path.join(projectDir, headerFileName);
verboseMessage(
`Calculating ${chalk.bold(path.basename(destCppFile))}...`,
@ -313,7 +335,22 @@ export class AutoLinkWindows {
autolinkCppPackageProviders: cppPackageProviders,
});
return await this.updateFile(destCppFile, cppContents);
const cppChanged = await this.updateFile(destCppFile, cppContents);
verboseMessage(
`Calculating ${chalk.bold(path.basename(destHeaderFile))}...`,
this.options.logging,
);
const headerContents = generatorCommon.resolveContents(srcHeaderFile, {
useMustache: true,
autolinkCppIncludes: cppIncludes,
autolinkCppPackageProviders: cppPackageProviders,
});
const headerChanged = await this.updateFile(destHeaderFile, headerContents);
return cppChanged || headerChanged;
}
public getCppReplacements() {
@ -340,14 +377,13 @@ export class AutoLinkWindows {
if (cppPackageProviders === '') {
// There are no windows dependencies, this would result in warning. C4100: 'packageProviders': unreferenced formal parameter.
// therefore add a usage.
cppPackageProviders = '\n UNREFERENCED_PARAMETER(packageProviders);'; // CODESYNC: vnext\local-cli\generator-windows\index.js
cppPackageProviders = '\n UNREFERENCED_PARAMETER(packageProviders);'; // CODESYNC: @react-native-windows\cli\src\generator-windows\index.ts
}
return {cppPackageProviders, cppIncludes};
}
private generateCSAutolinking(
private async generateCSAutolinking(
templateRoot: string,
projectLang: string,
projectDir: string,
) {
const {csUsingNamespaces, csReactPackageProviders} =
@ -355,12 +391,7 @@ export class AutoLinkWindows {
const csFileName = 'AutolinkedNativeModules.g.cs';
const srcCsFile = path.join(
templateRoot,
`${projectLang}-app`,
'src',
csFileName,
);
const srcCsFile = this.getAutolinkTemplateFile(templateRoot, csFileName);
const destCsFile = path.join(projectDir, csFileName);
@ -375,7 +406,7 @@ export class AutoLinkWindows {
autolinkCsReactPackageProviders: csReactPackageProviders,
});
return this.updateFile(destCsFile, csContents);
return await this.updateFile(destCsFile, csContents);
}
public getCsReplacements() {
@ -505,7 +536,10 @@ export class AutoLinkWindows {
return contentsChanged;
}
private generateAutolinkTargets(projectDir: string, templateRoot: string) {
private async generateAutolinkTargets(
projectDir: string,
templateRoot: string,
) {
let projectReferencesForTargets = '';
const windowsDependencies = this.getWindowsDependencies();
@ -534,10 +568,8 @@ export class AutoLinkWindows {
const targetFileName = 'AutolinkedNativeModules.g.targets';
const srcTargetFile = path.join(
const srcTargetFile = this.getAutolinkTemplateFile(
templateRoot,
`shared-app`,
'src',
targetFileName,
);
@ -553,20 +585,18 @@ export class AutoLinkWindows {
autolinkProjectReferencesForTargets: projectReferencesForTargets,
});
return this.updateFile(destTargetFile, targetContents);
return await this.updateFile(destTargetFile, targetContents);
}
private generateAutolinkProps(
private async generateAutolinkProps(
templateRoot: string,
projectDir: string,
propertiesForProps: string,
) {
const propsFileName = 'AutolinkedNativeModules.g.props';
const srcPropsFile = path.join(
const srcPropsFile = this.getAutolinkTemplateFile(
templateRoot,
`shared-app`,
'src',
propsFileName,
);
@ -582,7 +612,7 @@ export class AutoLinkWindows {
autolinkPropertiesForProps: propertiesForProps,
});
return this.updateFile(destPropsFile, propsContents);
return await this.updateFile(destPropsFile, propsContents);
}
private getCSModules() {
@ -863,6 +893,16 @@ function resolveRnwRoot(projectConfig: WindowsProjectConfig) {
*/
function resolveTemplateRoot(projectConfig: WindowsProjectConfig) {
const rnwPackage = resolveRnwRoot(projectConfig);
const template = projectConfig.rnwConfig?.['init-windows']?.template as
| string
| undefined;
// For new templates, return the template's root path
if (template && !template.startsWith('old/')) {
return path.join(rnwPackage, 'templates', template);
}
// For old (unknown templates) fall back to old behavior
return path.join(rnwPackage, 'template');
}

Просмотреть файл

@ -95,38 +95,105 @@ export function findSolutionFiles(winFolder: string): string[] {
return solutionFiles;
}
export interface RawTemplateInfo {
projectLang: 'cpp' | 'cs' | null;
projectType: 'app' | 'lib' | null;
projectArch: 'old' | 'new' | 'mixed' | null;
}
/**
* Determines the RawTemplateInfo of the target project file.
* @param filePath The absolute file path to check.
* @return The RawTemplateInfo for the specific project.
*/
export function getRawTemplateInfo(filePath: string): RawTemplateInfo {
const result: RawTemplateInfo = {
projectLang: null,
projectType: null,
projectArch: null,
};
if (!fs.existsSync(filePath)) {
return result;
}
result.projectLang = getProjectLanguage(filePath);
const projectContents = readProjectFile(filePath);
if (result.projectLang === 'cs') {
if (
importProjectExists(
projectContents,
'Microsoft.ReactNative.Uwp.CSharpApp.targets',
)
) {
result.projectType = 'app';
result.projectArch = 'old';
} else if (
importProjectExists(
projectContents,
'Microsoft.ReactNative.Uwp.CSharpLib.targets',
)
) {
result.projectType = 'lib';
result.projectArch = 'old';
}
} else if (result.projectLang === 'cpp') {
if (
importProjectExists(
projectContents,
'Microsoft.ReactNative.Uwp.CppApp.targets',
)
) {
result.projectType = 'app';
result.projectArch = 'old';
} else if (
importProjectExists(
projectContents,
'Microsoft.ReactNative.Uwp.CppLib.targets',
)
) {
result.projectType = 'lib';
result.projectArch = 'old';
} else if (
importProjectExists(
projectContents,
'Microsoft.ReactNative.Composition.CppApp.targets',
)
) {
result.projectType = 'app';
result.projectArch = 'new';
} else if (
importProjectExists(
projectContents,
'Microsoft.ReactNative.Composition.CppLib.targets',
)
) {
result.projectType = 'lib';
result.projectArch = 'new';
} else if (
importProjectExists(
projectContents,
'Microsoft.ReactNative.CppLib.targets',
)
) {
result.projectType = 'lib';
result.projectArch = 'mixed';
}
}
return result;
}
/**
* Checks if the target file path is a RNW lib project file.
* @param filePath The absolute file path to check.
* @return Whether the path is to a RNW lib project file.
*/
export function isRnwDependencyProject(filePath: string): boolean {
const projectContents = readProjectFile(filePath);
const projectLang = getProjectLanguage(filePath);
if (projectLang === 'cs') {
return importProjectExists(
projectContents,
'Microsoft.ReactNative.Uwp.CSharpLib.targets',
);
} else if (projectLang === 'cpp') {
return (
importProjectExists(
projectContents,
'Microsoft.ReactNative.Uwp.CppLib.targets',
) ||
importProjectExists(
projectContents,
'Microsoft.ReactNative.Composition.CppLib.targets',
) ||
importProjectExists(
projectContents,
'Microsoft.ReactNative.CppLib.targets',
)
);
}
return false;
const rawTemplateInfo = getRawTemplateInfo(filePath);
return rawTemplateInfo.projectType === 'lib';
}
/**
@ -166,56 +233,14 @@ export function findDependencyProjectFiles(winFolder: string): string[] {
return dependencyProjectFiles;
}
type ReactNativeProjectType = 'unknown' | 'App-WinAppSDK';
function getReactNativeProjectType(
value: string | null,
): ReactNativeProjectType {
switch (value) {
case 'App-WinAppSDK':
return value;
default:
return 'unknown';
}
}
/**
* Checks if the target file path is a RNW app project file.
* @param filePath The absolute file path to check.
* @return Whether the path is to a RNW app project file.
*/
function isRnwAppProject(filePath: string): boolean {
const projectContents = readProjectFile(filePath);
const rnProjectType = getReactNativeProjectType(
tryFindPropertyValue(projectContents, 'ReactNativeProjectType'),
);
if (rnProjectType !== 'unknown') {
return true;
}
const projectLang = getProjectLanguage(filePath);
if (projectLang === 'cs') {
return importProjectExists(
projectContents,
'Microsoft.ReactNative.Uwp.CSharpApp.targets',
);
} else if (projectLang === 'cpp') {
return (
importProjectExists(
projectContents,
'Microsoft.ReactNative.Uwp.CppApp.targets',
) ||
importProjectExists(
projectContents,
'Microsoft.ReactNative.Composition.CppApp.targets',
)
);
}
return false;
export function isRnwAppProject(filePath: string): boolean {
const rawTemplateInfo = getRawTemplateInfo(filePath);
return rawTemplateInfo.projectType === 'app';
}
/**
@ -503,3 +528,25 @@ export function getExperimentalFeatures(
}
return result;
}
export function getRnwConfig(
root: string,
projectFile: string,
): Record<string, any> | undefined {
const pkgJson = require(path.join(root, 'package.json'));
const config: Record<string, any> = pkgJson['react-native-windows'] ?? {};
// if init-windows is missing (most existing projects), try to auto-calculate it
config['init-windows'] ??= {};
if (!config['init-windows'].template) {
const info = getRawTemplateInfo(projectFile);
if (info.projectArch && info.projectLang && info.projectType) {
config['init-windows'].template = `${
info.projectArch === 'old' ? 'old/uwp-' : ''
}${info.projectLang}-${info.projectType}`;
}
}
return config;
}

Просмотреть файл

@ -55,6 +55,7 @@ opt - Item is optional. If an override file exists, it MAY provide it. If no ov
csPackageProviders: [], // (req) Array of fully qualified cs IReactPackageProviders, ie: 'NugetModule.ReactPackageProvider'
},
],
rnwConfig: Record<string, any>, // (auto) Object extracted from 'react-native-windows' property in package.json
}
Example react-native.config.js for a 'MyModule':
@ -105,6 +106,7 @@ export interface WindowsDependencyConfig {
solutionFile?: string | null;
projects: ProjectDependency[];
nugetPackages: NuGetPackageDependency[];
rnwConfig?: Record<string, any>;
}
/**
@ -322,6 +324,8 @@ export function dependencyConfigWindows(
csNamespaces,
csPackageProviders,
});
result.rnwConfig ??= configUtils.getRnwConfig(folder, projectFile);
} else {
const projectPath = path.relative(sourceDir, projectFile);
result.projects.push({

Просмотреть файл

@ -39,7 +39,8 @@ opt - Item is optional. If an override file exists, it MAY provide it. If no ov
projectLang: string, // (auto) Language of the project, cpp or cs, determined from projectFile
projectGuid: string, // (auto) Project identifier, determined from projectFile
},
experimentalFeatures: Record<String, string> // (auto) Properties extracted from ExperimentalFeatures.props
experimentalFeatures: Record<string, string>, // (auto) Properties extracted from ExperimentalFeatures.props
rnwConfig: Record<string, any>, // (auto) Object extracted from 'react-native-windows' property in package.json
}
Example react-native.config.js for a 'MyApp':
@ -73,6 +74,7 @@ export interface WindowsProjectConfig {
project: Project;
useWinUI3?: boolean;
experimentalFeatures?: Record<string, string>;
rnwConfig?: Record<string, any>;
}
type DeepPartial<T> = {[P in keyof T]?: DeepPartial<T[P]>};
@ -234,6 +236,18 @@ export function projectConfigWindows(
result.experimentalFeatures = result.experimentalFeatures ?? {};
result.experimentalFeatures.UseExperimentalNuget = useExperimentalNuget;
}
result.rnwConfig = configUtils.getRnwConfig(folder, projectFile);
}
if (!result.rnwConfig) {
// No rnwConfig, maybe it's actually a lib, try to get some info from it
let projectFile = '';
const foundProjects = configUtils.findDependencyProjectFiles(sourceDir);
if (foundProjects.length > 0) {
projectFile = path.join(sourceDir, foundProjects[0]);
}
result.rnwConfig = configUtils.getRnwConfig(folder, projectFile);
}
return result as WindowsProjectConfig;

Просмотреть файл

@ -52,11 +52,13 @@ export interface InitWindowsTemplateConfig {
export class InitWindows {
protected readonly rnwPath: string;
protected readonly rnwConfig?: Record<string, any>;
protected readonly templates: Map<string, InitWindowsTemplateConfig> =
new Map();
constructor(readonly config: Config, readonly options: InitOptions) {
this.rnwPath = pathHelpers.resolveRnwRoot(this.config.root);
this.rnwConfig = this.config.project.windows?.rnwConfig;
}
protected verboseMessage(message: any) {
@ -140,6 +142,7 @@ export class InitWindows {
console.log(`\n`);
}
// eslint-disable-next-line complexity
public async run(spinner: Ora) {
await this.loadTemplates();
@ -150,7 +153,9 @@ export class InitWindows {
return;
}
this.options.template ??= this.getDefaultTemplateName();
this.options.template ??=
(this.rnwConfig?.['init-windows']?.template as string | undefined) ??
this.getDefaultTemplateName();
spinner.info(`Using template '${this.options.template}'...`);
if (!this.templates.has(this.options.template.replace(/[\\]/g, '/'))) {
@ -172,9 +177,11 @@ export class InitWindows {
);
}
// If no project name is provided, calculate the name and clean if necessary
// If no project name is provided, check previously used name or calculate a name and clean if necessary
if (!this.options.name) {
const projectName = this.getReactNativeProjectName(this.config.root);
const projectName =
(this.rnwConfig?.['init-windows']?.name as string | undefined) ??
this.getReactNativeProjectName(this.config.root);
this.options.name = nameHelpers.isValidProjectName(projectName)
? projectName
: nameHelpers.cleanName(projectName);
@ -199,9 +206,11 @@ export class InitWindows {
);
}
// If no project namespace is provided, use the project name and clean if necessary
// If no project namespace is provided, check previously used namespace or use the project name and clean if necessary
if (!this.options.namespace) {
const namespace = this.options.name;
const namespace =
(this.rnwConfig?.['init-windows']?.namespace as string | undefined) ??
this.options.name;
this.options.namespace = nameHelpers.isValidProjectNamespace(namespace)
? namespace
: nameHelpers.cleanNamespace(namespace);

Просмотреть файл

@ -227,7 +227,7 @@ export async function copyProjectTemplateAndReplace(
autolinkCsReactPackageProviders: '',
autolinkCppIncludes: '',
autolinkCppPackageProviders:
'\n UNREFERENCED_PARAMETER(packageProviders);', // CODESYNC: vnext\local-cli\runWindows\utils\autolink.js
'\n UNREFERENCED_PARAMETER(packageProviders);', // CODESYNC: @react-native-windows\cli\src\commands\autolinkWindows\autolinkWindows.ts
};
const commonMappings =

Просмотреть файл

@ -75,6 +75,13 @@ async function getFileMappings(config = {}, options = {}) {
addReactNativePublicAdoFeed: true || isCanary, // Temporary true for all new projects until code-signing is restored, see issue #14030
cppNugetPackages,
// autolinking template variables
autolinkPropertiesForProps: '',
autolinkProjectReferencesForTargets: '',
autolinkCppIncludes: '',
autolinkCppPackageProviders:
'\n UNREFERENCED_PARAMETER(packageProviders);', // CODESYNC: @react-native-windows\cli\src\commands\autolinkWindows\autolinkWindows.ts
};
let fileMappings = [];

Просмотреть файл

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- AutolinkedNativeModules.g.props contents generated by "npx @react-native-community/cli autolink-windows" -->
<PropertyGroup>{{ &autolinkPropertiesForProps }}
</PropertyGroup>
</Project>

Просмотреть файл

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- AutolinkedNativeModules.g.targets contents generated by "npx @react-native-community/cli autolink-windows" -->
<ItemGroup>{{ &autolinkProjectReferencesForTargets }}
</ItemGroup>
</Project>

Просмотреть файл

@ -55,6 +55,8 @@ function makeGenerateWindowsWrapper(
options.namespace,
generateOptions,
);
await templateUtils.updateProjectPackageJson(config, options);
};
return {

Просмотреть файл

@ -110,7 +110,12 @@ async function runNpmInstall(config = {}, options = {}) {
}
}
async function updateProjectPackageJson(config = {}, options = {}, props = {}) {
async function updateProjectPackageJson(
config = {},
options = {},
props = {},
saveOptions = true,
) {
const projectRoot = config?.root ?? process.cwd();
const projectPackage =
await pkgUtils.WritableNpmPackage.fromPath(projectRoot);
@ -121,6 +126,16 @@ async function updateProjectPackageJson(config = {}, options = {}, props = {}) {
);
}
if (saveOptions) {
props['react-native-windows'] = {
'init-windows': {
name: options.name,
namespace: options.namespace,
template: options.template,
},
};
}
if (options?.logging) {
console.log(`Modifying ${path.join(projectRoot, 'package.json')}...`);
}