From 773e68eec1bf4d7fed37a1728ef8ce1e4ed897de Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Mon, 15 Dec 2025 18:39:38 -0800 Subject: [PATCH 1/2] feat: legacy-peer option on ns create --- lib/bun-package-manager.ts | 19 +++++----- lib/commands/create-project.ts | 25 +++++++------ lib/declarations.d.ts | 65 ++++++++++++++++++--------------- lib/definitions/project.d.ts | 5 +++ lib/node-package-manager.ts | 24 +++++++----- lib/options.ts | 21 ++++++----- lib/pnpm-package-manager.ts | 21 ++++++----- lib/services/project-service.ts | 56 ++++++++++++++-------------- lib/yarn-package-manager.ts | 23 ++++++------ lib/yarn2-package-manager.ts | 25 +++++++------ test/node-package-manager.ts | 29 ++++++++++++--- 11 files changed, 175 insertions(+), 138 deletions(-) diff --git a/lib/bun-package-manager.ts b/lib/bun-package-manager.ts index cfd5ffc057..e4ef1bbba7 100644 --- a/lib/bun-package-manager.ts +++ b/lib/bun-package-manager.ts @@ -25,7 +25,7 @@ export class BunPackageManager extends BasePackageManager { $hostInfo: IHostInfo, private $logger: ILogger, private $httpClient: Server.IHttpClient, - $pacoteService: IPacoteService + $pacoteService: IPacoteService, ) { super($childProcess, $fs, $hostInfo, $pacoteService, "bun"); } @@ -34,11 +34,12 @@ export class BunPackageManager extends BasePackageManager { public async install( packageName: string, pathToSave: string, - config: INodePackageManagerInstallOptions + config: INodePackageManagerInstallOptions, ): Promise { if (config.disableNpmInstall) { return; } + delete (config as any).legacyPeers; if (config.ignoreScripts) { config["ignore-scripts"] = true; } @@ -60,7 +61,7 @@ export class BunPackageManager extends BasePackageManager { const result = await this.processPackageManagerInstall( packageName, params, - { cwd, isInstallingAllDependencies } + { cwd, isInstallingAllDependencies }, ); return result; } catch (err) { @@ -74,7 +75,7 @@ export class BunPackageManager extends BasePackageManager { public async uninstall( packageName: string, config?: any, - cwd?: string + cwd?: string, ): Promise { const flags = this.getFlagsString(config, false); return this.$childProcess.exec(`bun remove ${packageName} ${flags}`, { @@ -91,7 +92,7 @@ export class BunPackageManager extends BasePackageManager { let viewResult: any; try { viewResult = await this.$childProcess.exec( - `npm view ${packageName} ${flags}` + `npm view ${packageName} ${flags}`, ); } catch (e) { this.$errors.fail(e.message); @@ -119,7 +120,7 @@ export class BunPackageManager extends BasePackageManager { // https://github.com/npms-io/npms-api/issues/112. Better to switch to // https://registry.npmjs.org/ const httpRequestResult = await this.$httpClient.httpRequest( - `https://api.npms.io/v2/search?q=keywords:${keyword}` + `https://api.npms.io/v2/search?q=keywords:${keyword}`, ); const result: INpmsResult = JSON.parse(httpRequestResult.body); return result; @@ -132,15 +133,15 @@ export class BunPackageManager extends BasePackageManager { const registry = await this.$childProcess.exec(`npm config get registry`); const url = registry.trim() + packageName; this.$logger.trace( - `Trying to get data from npm registry for package ${packageName}, url is: ${url}` + `Trying to get data from npm registry for package ${packageName}, url is: ${url}`, ); const responseData = (await this.$httpClient.httpRequest(url)).body; this.$logger.trace( - `Successfully received data from npm registry for package ${packageName}. Response data is: ${responseData}` + `Successfully received data from npm registry for package ${packageName}. Response data is: ${responseData}`, ); const jsonData = JSON.parse(responseData); this.$logger.trace( - `Successfully parsed data from npm registry for package ${packageName}.` + `Successfully parsed data from npm registry for package ${packageName}.`, ); return jsonData; } diff --git a/lib/commands/create-project.ts b/lib/commands/create-project.ts index c817d1e188..b27b4c19c8 100644 --- a/lib/commands/create-project.ts +++ b/lib/commands/create-project.ts @@ -35,7 +35,7 @@ export class CreateProjectCommand implements ICommand { private $errors: IErrors, private $options: IOptions, private $prompter: IPrompter, - private $stringParameter: ICommandParameter + private $stringParameter: ICommandParameter, ) {} public async execute(args: string[]): Promise { @@ -55,7 +55,7 @@ export class CreateProjectCommand implements ICommand { this.$options.template ) { this.$errors.failWithHelp( - "You cannot use a flavor option like --ng, --vue, --react, --solid, --svelte, --tsc and --js together with --template." + "You cannot use a flavor option like --ng, --vue, --react, --solid, --svelte, --tsc and --js together with --template.", ); } @@ -115,7 +115,7 @@ export class CreateProjectCommand implements ICommand { this.printInteractiveCreationIntroIfNeeded(); projectName = await this.$prompter.getString( `${getNextInteractiveAdverb()}, what will be the name of your app?`, - { allowEmpty: false } + { allowEmpty: false }, ); this.$logger.info(); } @@ -130,7 +130,7 @@ export class CreateProjectCommand implements ICommand { this.printInteractiveCreationIntroIfNeeded(); selectedTemplate = await this.interactiveFlavorAndTemplateSelection( getNextInteractiveAdverb(), - getNextInteractiveAdverb() + getNextInteractiveAdverb(), ); } @@ -142,17 +142,18 @@ export class CreateProjectCommand implements ICommand { // its already validated above force: true, ignoreScripts: this.$options.ignoreScripts, + legacyPeerDeps: this.$options.legacyPeerDeps, }); } private async interactiveFlavorAndTemplateSelection( flavorAdverb: string, - templateAdverb: string + templateAdverb: string, ) { const selectedFlavor = await this.interactiveFlavorSelection(flavorAdverb); const selectedTemplate: string = await this.interactiveTemplateSelection( selectedFlavor, - templateAdverb + templateAdverb, ); return selectedTemplate; @@ -191,7 +192,7 @@ export class CreateProjectCommand implements ICommand { key: constants.JsFlavorName, description: "Use NativeScript without any framework", }, - ] + ], ); return flavorSelection; } @@ -210,7 +211,7 @@ can skip this prompt next time using the --template option, or using --ng, --rea private async interactiveTemplateSelection( flavorSelection: string, - adverb: string + adverb: string, ) { const selectedFlavorTemplates: { key?: string; @@ -255,10 +256,10 @@ can skip this prompt next time using the --template option, or using --ng, --rea }); const selectedTemplateKey = await this.$prompter.promptForDetailedChoice( `${adverb}, which template would you like to start from:`, - templateChoices + templateChoices, ); selectedTemplate = selectedFlavorTemplates.find( - (t) => t.key === selectedTemplateKey + (t) => t.key === selectedTemplateKey, ).value; } else { selectedTemplate = selectedFlavorTemplates[0].value; @@ -472,14 +473,14 @@ can skip this prompt next time using the --template option, or using --ng, --rea ].join(" "), "", `Now you can navigate to your project with ${color.cyan( - `cd ${relativePath}` + `cd ${relativePath}`, )} and then:`, "", ...runDebugNotes, ``, `For more options consult the docs or run ${color.green("ns --help")}`, "", - ].join("\n") + ].join("\n"), ); // todo: add back ns preview // this.$logger.printMarkdown( diff --git a/lib/declarations.d.ts b/lib/declarations.d.ts index edb18142ed..7a00be85a7 100644 --- a/lib/declarations.d.ts +++ b/lib/declarations.d.ts @@ -31,7 +31,7 @@ interface INodePackageManager { install( packageName: string, pathToSave: string, - config: INodePackageManagerInstallOptions + config: INodePackageManagerInstallOptions, ): Promise; /** @@ -44,7 +44,7 @@ interface INodePackageManager { uninstall( packageName: string, config?: IDictionary, - path?: string + path?: string, ): Promise; /** @@ -84,7 +84,7 @@ interface INodePackageManager { */ search( filter: string[], - config: IDictionary + config: IDictionary, ): Promise; /** @@ -130,7 +130,7 @@ interface IPerformanceService { methodInfo: string, startTime: number, endTime: number, - args: any[] + args: any[], ): void; // Will return a reference time in milliseconds @@ -141,39 +141,39 @@ interface IPackageInstallationManager { install( packageName: string, packageDir: string, - options?: INpmInstallOptions + options?: INpmInstallOptions, ): Promise; uninstall( packageName: string, packageDir: string, - options?: IDictionary + options?: IDictionary, ): Promise; getLatestVersion(packageName: string): Promise; getNextVersion(packageName: string): Promise; getLatestCompatibleVersion( packageName: string, - referenceVersion?: string + referenceVersion?: string, ): Promise; getMaxSatisfyingVersion( packageName: string, - versionRange: string + versionRange: string, ): Promise; getLatestCompatibleVersionSafe( packageName: string, - referenceVersion?: string + referenceVersion?: string, ): Promise; getInspectorFromCache( inspectorNpmPackageName: string, - projectDir: string + projectDir: string, ): Promise; clearInspectorCache(): void; getInstalledDependencyVersion( packageName: string, - projectDir?: string + projectDir?: string, ): Promise; getMaxSatisfyingVersionSafe( packageName: string, - versionIdentifier: string + versionIdentifier: string, ): Promise; } @@ -183,6 +183,12 @@ interface IPackageInstallationManager { interface INodePackageManagerInstallOptions extends INpmInstallConfigurationOptions, IDictionary { + /** + * When true and the active package manager is npm, execute installs with `--legacy-peer-deps`. + * Other package managers should ignore this option. + */ + legacyPeers?: boolean; + /** * Destination of the installation. * @type {string} @@ -266,7 +272,7 @@ interface INpmPeerDependencyInfo { * @type {string} */ requires: string; - } + }, ]; /** * Dependencies of the dependency. @@ -548,6 +554,7 @@ interface IAndroidReleaseOptions { interface INpmInstallConfigurationOptionsBase { frameworkPath: string; ignoreScripts: boolean; //npm flag + legacyPeerDeps?: boolean; //npm flag (--legacy-peer-deps) } interface INpmInstallConfigurationOptions @@ -622,7 +629,7 @@ interface IOptions argv: IYargArgv; validateOptions( commandSpecificDashedOptions?: IDictionary, - projectData?: IProjectData + projectData?: IProjectData, ): void; options: IDictionary; shorthands: string[]; @@ -834,7 +841,7 @@ interface IAndroidToolsInfo { */ validateJavacVersion( installedJavaVersion: string, - options?: IAndroidToolsInfoOptions + options?: IAndroidToolsInfoOptions, ): boolean; /** @@ -913,14 +920,14 @@ interface IAppDebugSocketProxyFactory extends NodeJS.EventEmitter { device: Mobile.IiOSDevice, appId: string, projectName: string, - projectDir: string + projectDir: string, ): Promise; ensureWebSocketProxy( device: Mobile.IiOSDevice, appId: string, projectName: string, - projectDir: string + projectDir: string, ): Promise; removeAllProxies(): void; @@ -939,12 +946,12 @@ interface IiOSSocketRequestExecutor { executeAttachRequest( device: Mobile.IiOSDevice, timeout: number, - projectId: string + projectId: string, ): Promise; executeRefreshRequest( device: Mobile.IiOSDevice, timeout: number, - appId: string + appId: string, ): Promise; } @@ -995,7 +1002,7 @@ interface IProjectNameService { */ ensureValidName( projectName: string, - validateOptions?: { force: boolean } + validateOptions?: { force: boolean }, ): Promise; } @@ -1089,7 +1096,7 @@ interface IBundleValidatorHelper { */ getBundlerDependencyVersion( projectData: IProjectData, - bundlerName?: string + bundlerName?: string, ): string; } @@ -1171,7 +1178,7 @@ interface IAssetsGenerationService { * @returns {Promise} */ generateSplashScreens( - splashesGenerationData: IResourceGenerationData + splashesGenerationData: IResourceGenerationData, ): Promise; } @@ -1207,7 +1214,7 @@ interface IPlatformValidationService { provision: true | string, teamId: true | string, projectData: IProjectData, - platform?: string + platform?: string, ): Promise; validatePlatformInstalled(platform: string, projectData: IProjectData): void; @@ -1220,7 +1227,7 @@ interface IPlatformValidationService { */ isPlatformSupportedForOS( platform: string, - projectData: IProjectData + projectData: IProjectData, ): boolean; } @@ -1228,27 +1235,27 @@ interface IPlatformCommandHelper { addPlatforms( platforms: string[], projectData: IProjectData, - frameworkPath?: string + frameworkPath?: string, ): Promise; cleanPlatforms( platforms: string[], projectData: IProjectData, - frameworkPath: string + frameworkPath: string, ): Promise; removePlatforms( platforms: string[], - projectData: IProjectData + projectData: IProjectData, ): Promise; updatePlatforms( platforms: string[], - projectData: IProjectData + projectData: IProjectData, ): Promise; getInstalledPlatforms(projectData: IProjectData): string[]; getAvailablePlatforms(projectData: IProjectData): string[]; getPreparedPlatforms(projectData: IProjectData): string[]; getCurrentPlatformVersion( platform: string, - projectData: IProjectData + projectData: IProjectData, ): string; } diff --git a/lib/definitions/project.d.ts b/lib/definitions/project.d.ts index c8046b2ed4..8f83882048 100644 --- a/lib/definitions/project.d.ts +++ b/lib/definitions/project.d.ts @@ -33,6 +33,11 @@ interface IProjectSettingsBase extends IProjectName { */ ignoreScripts?: boolean; + /** + * Defines whether `npm install` should be executed with `--legacy-peer-deps`. + */ + legacyPeerDeps?: boolean; + /** * Selected template from which to create the project. If not specified, defaults to hello-world template. * Template can be any npm package, local dir, github url, .tgz file. diff --git a/lib/node-package-manager.ts b/lib/node-package-manager.ts index bf63cae523..c5a86d8a2b 100644 --- a/lib/node-package-manager.ts +++ b/lib/node-package-manager.ts @@ -25,7 +25,7 @@ export class NodePackageManager extends BasePackageManager { $hostInfo: IHostInfo, private $logger: ILogger, private $httpClient: Server.IHttpClient, - $pacoteService: IPacoteService + $pacoteService: IPacoteService, ) { super($childProcess, $fs, $hostInfo, $pacoteService, "npm"); } @@ -34,11 +34,15 @@ export class NodePackageManager extends BasePackageManager { public async install( packageName: string, pathToSave: string, - config: INodePackageManagerInstallOptions + config: INodePackageManagerInstallOptions, ): Promise { if (config.disableNpmInstall) { return; } + if (config.legacyPeers) { + config["legacy-peer-deps"] = true; + } + delete (config as any).legacyPeers; if (config.ignoreScripts) { config["ignore-scripts"] = true; } @@ -67,7 +71,7 @@ export class NodePackageManager extends BasePackageManager { if (config.frameworkPath) { relativePathFromCwdToSource = relative( config.frameworkPath, - pathToSave + pathToSave, ); if (this.$fs.exists(relativePathFromCwdToSource)) { packageName = relativePathFromCwdToSource; @@ -79,7 +83,7 @@ export class NodePackageManager extends BasePackageManager { const result = await this.processPackageManagerInstall( packageName, params, - { cwd, isInstallingAllDependencies } + { cwd, isInstallingAllDependencies }, ); return result; } catch (err) { @@ -104,7 +108,7 @@ export class NodePackageManager extends BasePackageManager { public async uninstall( packageName: string, config?: any, - path?: string + path?: string, ): Promise { const flags = this.getFlagsString(config, false); return this.$childProcess.exec(`npm uninstall ${packageName} ${flags}`, { @@ -126,7 +130,7 @@ export class NodePackageManager extends BasePackageManager { let viewResult: any; try { viewResult = await this.$childProcess.exec( - `npm view ${packageName} ${flags}` + `npm view ${packageName} ${flags}`, ); } catch (e) { this.$errors.fail(e.message); @@ -142,7 +146,7 @@ export class NodePackageManager extends BasePackageManager { public async searchNpms(keyword: string): Promise { // TODO: Fix the generation of url - in case it contains @ or / , the call may fail. const httpRequestResult = await this.$httpClient.httpRequest( - `https://api.npms.io/v2/search?q=keywords:${keyword}` + `https://api.npms.io/v2/search?q=keywords:${keyword}`, ); const result: INpmsResult = JSON.parse(httpRequestResult.body); return result; @@ -152,15 +156,15 @@ export class NodePackageManager extends BasePackageManager { const registry = await this.$childProcess.exec(`npm config get registry`); const url = registry.trim() + packageName; this.$logger.trace( - `Trying to get data from npm registry for package ${packageName}, url is: ${url}` + `Trying to get data from npm registry for package ${packageName}, url is: ${url}`, ); const responseData = (await this.$httpClient.httpRequest(url)).body; this.$logger.trace( - `Successfully received data from npm registry for package ${packageName}. Response data is: ${responseData}` + `Successfully received data from npm registry for package ${packageName}. Response data is: ${responseData}`, ); const jsonData = JSON.parse(responseData); this.$logger.trace( - `Successfully parsed data from npm registry for package ${packageName}.` + `Successfully parsed data from npm registry for package ${packageName}.`, ); return jsonData; } diff --git a/lib/options.ts b/lib/options.ts index d5dcd1b509..65b16a32bb 100644 --- a/lib/options.ts +++ b/lib/options.ts @@ -42,7 +42,7 @@ export class Options { public options: IDictionary; public setupOptions( - commandSpecificDashedOptions?: IDictionary + commandSpecificDashedOptions?: IDictionary, ): void { if (commandSpecificDashedOptions) { _.extend(this.options, commandSpecificDashedOptions); @@ -54,7 +54,7 @@ export class Options { // Check if the user has explicitly provide --hmr and --release options from command line if (this.initialArgv.release && this.initialArgv.hmr) { this.$errors.fail( - "The options --release and --hmr cannot be used simultaneously." + "The options --release and --hmr cannot be used simultaneously.", ); } @@ -75,7 +75,7 @@ export class Options { constructor( private $errors: IErrors, - private $settingsService: ISettingsService + private $settingsService: ISettingsService, ) { this.options = _.extend({}, this.commonOptions, this.globalOptions); this.setArgv(); @@ -120,6 +120,7 @@ export class Options { hasSensitiveValue: true, }, ignoreScripts: { type: OptionType.Boolean, hasSensitiveValue: false }, + legacyPeerDeps: { type: OptionType.Boolean, hasSensitiveValue: false }, disableNpmInstall: { type: OptionType.Boolean, hasSensitiveValue: false }, compileSdk: { type: OptionType.Number, hasSensitiveValue: false }, port: { type: OptionType.Number, hasSensitiveValue: false }, @@ -264,7 +265,7 @@ export class Options { } public validateOptions( - commandSpecificDashedOptions?: IDictionary + commandSpecificDashedOptions?: IDictionary, ): void { this.setupOptions(commandSpecificDashedOptions); const parsed: any = {}; @@ -284,7 +285,7 @@ export class Options { if (!_.includes(this.optionsWhiteList, optionName)) { if (!this.isOptionSupported(optionName)) { this.$errors.failWithHelp( - `The option '${originalOptionName}' is not supported.` + `The option '${originalOptionName}' is not supported.`, ); } @@ -294,7 +295,7 @@ export class Options { if (_.isArray(optionValue) && optionType !== OptionType.Array) { this.$errors.failWithHelp( "The '%s' option requires a single value.", - originalOptionName + originalOptionName, ); } else if ( optionType === OptionType.String && @@ -302,14 +303,14 @@ export class Options { ) { this.$errors.failWithHelp( "The option '%s' requires non-empty value.", - originalOptionName + originalOptionName, ); } else if ( optionType === OptionType.Array && optionValue.length === 0 ) { this.$errors.failWithHelp( - `The option '${originalOptionName}' requires one or more values, separated by a space.` + `The option '${originalOptionName}' requires one or more values, separated by a space.`, ); } } @@ -350,7 +351,7 @@ export class Options { // This way your code will work in case "$ emulate android --profile-dir" or "$ emulate android --profileDir" is used by user. private getNonDashedOptionName(optionName: string): string { const matchUpperCaseLetters = optionName.match( - Options.NONDASHED_OPTION_REGEX + Options.NONDASHED_OPTION_REGEX, ); if (matchUpperCaseLetters) { // get here if option with upperCase letter is specified, for example profileDir @@ -410,7 +411,7 @@ export class Options { .map((match) => { return match[currentDepth]; }) - .filter(Boolean) + .filter(Boolean), ), ]; diff --git a/lib/pnpm-package-manager.ts b/lib/pnpm-package-manager.ts index f2de683552..fd9340f4f6 100644 --- a/lib/pnpm-package-manager.ts +++ b/lib/pnpm-package-manager.ts @@ -26,7 +26,7 @@ export class PnpmPackageManager extends BasePackageManager { $hostInfo: IHostInfo, private $httpClient: Server.IHttpClient, private $logger: ILogger, - $pacoteService: IPacoteService + $pacoteService: IPacoteService, ) { super($childProcess, $fs, $hostInfo, $pacoteService, "pnpm"); } @@ -35,11 +35,12 @@ export class PnpmPackageManager extends BasePackageManager { public async install( packageName: string, pathToSave: string, - config: INodePackageManagerInstallOptions + config: INodePackageManagerInstallOptions, ): Promise { if (config.disableNpmInstall) { return; } + delete (config as any).legacyPeers; delete config.dev; // temporary fix for unsupported yarn flag if (config.ignoreScripts) { config["ignore-scripts"] = true; @@ -63,7 +64,7 @@ export class PnpmPackageManager extends BasePackageManager { const result = await this.processPackageManagerInstall( packageName, params, - { cwd, isInstallingAllDependencies } + { cwd, isInstallingAllDependencies }, ); return result; } catch (e) { @@ -76,7 +77,7 @@ export class PnpmPackageManager extends BasePackageManager { public uninstall( packageName: string, config?: IDictionary, - cwd?: string + cwd?: string, ): Promise { // pnpm does not want save option in remove. It saves it by default delete config["save"]; @@ -94,7 +95,7 @@ export class PnpmPackageManager extends BasePackageManager { let viewResult: any; try { viewResult = await this.$childProcess.exec( - `pnpm info ${packageName} ${flags}` + `pnpm info ${packageName} ${flags}`, ); } catch (e) { this.$errors.fail(e.message); @@ -110,7 +111,7 @@ export class PnpmPackageManager extends BasePackageManager { @exported("pnpm") public search( filter: string[], - config: IDictionary + config: IDictionary, ): Promise { const flags = this.getFlagsString(config, false); return this.$childProcess.exec(`pnpm search ${filter.join(" ")} ${flags}`); @@ -118,7 +119,7 @@ export class PnpmPackageManager extends BasePackageManager { public async searchNpms(keyword: string): Promise { const httpRequestResult = await this.$httpClient.httpRequest( - `https://api.npms.io/v2/search?q=keywords:${keyword}` + `https://api.npms.io/v2/search?q=keywords:${keyword}`, ); const result: INpmsResult = JSON.parse(httpRequestResult.body); return result; @@ -129,15 +130,15 @@ export class PnpmPackageManager extends BasePackageManager { const registry = await this.$childProcess.exec(`pnpm config get registry`); const url = `${registry.trim()}/${packageName}`; this.$logger.trace( - `Trying to get data from pnpm registry for package ${packageName}, url is: ${url}` + `Trying to get data from pnpm registry for package ${packageName}, url is: ${url}`, ); const responseData = (await this.$httpClient.httpRequest(url)).body; this.$logger.trace( - `Successfully received data from pnpm registry for package ${packageName}. Response data is: ${responseData}` + `Successfully received data from pnpm registry for package ${packageName}. Response data is: ${responseData}`, ); const jsonData = JSON.parse(responseData); this.$logger.trace( - `Successfully parsed data from pnpm registry for package ${packageName}.` + `Successfully parsed data from pnpm registry for package ${packageName}.`, ); return jsonData; } diff --git a/lib/services/project-service.ts b/lib/services/project-service.ts index 91bc4bce84..cef58d4916 100644 --- a/lib/services/project-service.ts +++ b/lib/services/project-service.ts @@ -47,7 +47,7 @@ export class ProjectService implements IProjectService { private $projectTemplatesService: IProjectTemplatesService, private $tempService: ITempService, private $staticConfig: IStaticConfig, - private $childProcess: IChildProcess + private $childProcess: IChildProcess, ) {} public async validateProjectName(opts: { @@ -58,7 +58,7 @@ export class ProjectService implements IProjectService { let projectName = opts.projectName; if (!projectName) { this.$errors.failWithHelp( - "You must specify when creating a new project." + "You must specify when creating a new project.", ); } @@ -76,7 +76,7 @@ export class ProjectService implements IProjectService { @exported("projectService") @performanceLog() public async createProject( - projectOptions: IProjectSettings + projectOptions: IProjectSettings, ): Promise { const projectName = await this.validateProjectName({ projectName: projectOptions.projectName, @@ -85,7 +85,7 @@ export class ProjectService implements IProjectService { }); const projectDir = this.getValidProjectDir( projectOptions.pathToProject, - projectName + projectName, ); this.$fs.createDirectory(projectDir); @@ -94,10 +94,10 @@ export class ProjectService implements IProjectService { projectOptions.appId || this.$projectHelper.generateDefaultAppId( projectName, - constants.DEFAULT_APP_IDENTIFIER_PREFIX + constants.DEFAULT_APP_IDENTIFIER_PREFIX, ); this.$logger.trace( - `Creating a new NativeScript project with name ${projectName} and id ${appId} at location ${projectDir}` + `Creating a new NativeScript project with name ${projectName} and id ${appId} at location ${projectDir}`, ); const projectCreationData = await this.createProjectCore({ @@ -123,12 +123,12 @@ export class ProjectService implements IProjectService { await this.$childProcess.exec(`git init ${projectDir}`); await this.$childProcess.exec(`git -C ${projectDir} add --all`); await this.$childProcess.exec( - `git -C ${projectDir} commit --no-verify -m "init"` + `git -C ${projectDir} commit --no-verify -m "init"`, ); } catch (err) { this.$logger.trace( "Unable to initialize git repository. Error is: ", - err + err, ); } } @@ -141,9 +141,8 @@ export class ProjectService implements IProjectService { @exported("projectService") public isValidNativeScriptProject(pathToProject?: string): boolean { try { - const projectData = this.$projectDataService.getProjectData( - pathToProject - ); + const projectData = + this.$projectDataService.getProjectData(pathToProject); return ( !!projectData && @@ -160,7 +159,7 @@ export class ProjectService implements IProjectService { private getValidProjectDir( pathToProject: string, - projectName: string + projectName: string, ): string { const selectedPath = path.resolve(pathToProject || "."); const projectDir = path.join(selectedPath, projectName); @@ -169,7 +168,7 @@ export class ProjectService implements IProjectService { } private async createProjectCore( - projectCreationSettings: IProjectCreationSettings + projectCreationSettings: IProjectCreationSettings, ): Promise { const { template, @@ -177,12 +176,13 @@ export class ProjectService implements IProjectService { appId, projectName, ignoreScripts, + legacyPeerDeps, } = projectCreationSettings; try { const templateData = await this.$projectTemplatesService.prepareTemplate( template, - projectDir + projectDir, ); await this.extractTemplate(projectDir, templateData); @@ -197,6 +197,7 @@ export class ProjectService implements IProjectService { disableNpmInstall: false, frameworkPath: null, ignoreScripts, + legacyPeers: legacyPeerDeps, }); } catch (err) { this.$fs.deleteDirectory(projectDir); @@ -213,7 +214,7 @@ export class ProjectService implements IProjectService { @performanceLog() private async extractTemplate( projectDir: string, - templateData: ITemplateData + templateData: ITemplateData, ): Promise { this.$fs.ensureDirectoryExists(projectDir); @@ -226,42 +227,39 @@ export class ProjectService implements IProjectService { @performanceLog() public async ensureAppResourcesExist(projectDir: string): Promise { const projectData = this.$projectDataService.getProjectData(projectDir); - const appResourcesDestinationPath = projectData.getAppResourcesDirectoryPath( - projectDir - ); + const appResourcesDestinationPath = + projectData.getAppResourcesDirectoryPath(projectDir); if (!this.$fs.exists(appResourcesDestinationPath)) { this.$logger.trace( - "Project does not have App_Resources - fetching from default template." + "Project does not have App_Resources - fetching from default template.", ); this.$fs.createDirectory(appResourcesDestinationPath); const tempDir = await this.$tempService.mkdirSync("ns-default-template"); // the template installed doesn't have App_Resources -> get from a default template await this.$pacoteService.extractPackage( constants.RESERVED_TEMPLATE_NAMES["default"], - tempDir - ); - const templateProjectData = this.$projectDataService.getProjectData( - tempDir - ); - const templateAppResourcesDir = templateProjectData.getAppResourcesDirectoryPath( - tempDir + tempDir, ); + const templateProjectData = + this.$projectDataService.getProjectData(tempDir); + const templateAppResourcesDir = + templateProjectData.getAppResourcesDirectoryPath(tempDir); this.$fs.copyFile( path.join(templateAppResourcesDir, "*"), - appResourcesDestinationPath + appResourcesDestinationPath, ); } } @performanceLog() private alterPackageJsonData( - projectCreationSettings: IProjectCreationSettings + projectCreationSettings: IProjectCreationSettings, ): void { const { projectDir, projectName } = projectCreationSettings; const projectFilePath = path.join( projectDir, - this.$staticConfig.PROJECT_FILE_NAME + this.$staticConfig.PROJECT_FILE_NAME, ); let packageJsonData = this.$fs.readJson(projectFilePath); diff --git a/lib/yarn-package-manager.ts b/lib/yarn-package-manager.ts index d4d08ad7f0..09f4d9e902 100644 --- a/lib/yarn-package-manager.ts +++ b/lib/yarn-package-manager.ts @@ -25,7 +25,7 @@ export class YarnPackageManager extends BasePackageManager { $hostInfo: IHostInfo, private $httpClient: Server.IHttpClient, private $logger: ILogger, - $pacoteService: IPacoteService + $pacoteService: IPacoteService, ) { super($childProcess, $fs, $hostInfo, $pacoteService, "yarn"); } @@ -34,11 +34,12 @@ export class YarnPackageManager extends BasePackageManager { public async install( packageName: string, pathToSave: string, - config: INodePackageManagerInstallOptions + config: INodePackageManagerInstallOptions, ): Promise { if (config.disableNpmInstall) { return; } + delete (config as any).legacyPeers; if (config.ignoreScripts) { config["ignore-scripts"] = true; } @@ -60,7 +61,7 @@ export class YarnPackageManager extends BasePackageManager { const result = await this.processPackageManagerInstall( packageName, params, - { cwd, isInstallingAllDependencies } + { cwd, isInstallingAllDependencies }, ); return result; } catch (e) { @@ -73,7 +74,7 @@ export class YarnPackageManager extends BasePackageManager { public uninstall( packageName: string, config?: IDictionary, - cwd?: string + cwd?: string, ): Promise { const flags = this.getFlagsString(config, false); return this.$childProcess.exec(`yarn remove ${packageName} ${flags}`, { @@ -89,7 +90,7 @@ export class YarnPackageManager extends BasePackageManager { let viewResult: any; try { viewResult = await this.$childProcess.exec( - `yarn info ${packageName} ${flags}` + `yarn info ${packageName} ${flags}`, ); } catch (e) { this.$errors.fail(e.message); @@ -106,17 +107,17 @@ export class YarnPackageManager extends BasePackageManager { @exported("yarn") public search( filter: string[], - config: IDictionary + config: IDictionary, ): Promise { this.$errors.fail( - "Method not implemented. Yarn does not support searching for packages in the registry." + "Method not implemented. Yarn does not support searching for packages in the registry.", ); return null; } public async searchNpms(keyword: string): Promise { const httpRequestResult = await this.$httpClient.httpRequest( - `https://api.npms.io/v2/search?q=keywords:${keyword}` + `https://api.npms.io/v2/search?q=keywords:${keyword}`, ); const result: INpmsResult = JSON.parse(httpRequestResult.body); return result; @@ -127,15 +128,15 @@ export class YarnPackageManager extends BasePackageManager { const registry = await this.$childProcess.exec(`yarn config get registry`); const url = `${registry.trim()}/${packageName}`; this.$logger.trace( - `Trying to get data from yarn registry for package ${packageName}, url is: ${url}` + `Trying to get data from yarn registry for package ${packageName}, url is: ${url}`, ); const responseData = (await this.$httpClient.httpRequest(url)).body; this.$logger.trace( - `Successfully received data from yarn registry for package ${packageName}. Response data is: ${responseData}` + `Successfully received data from yarn registry for package ${packageName}. Response data is: ${responseData}`, ); const jsonData = JSON.parse(responseData); this.$logger.trace( - `Successfully parsed data from yarn registry for package ${packageName}.` + `Successfully parsed data from yarn registry for package ${packageName}.`, ); return jsonData; } diff --git a/lib/yarn2-package-manager.ts b/lib/yarn2-package-manager.ts index a8312abff3..28711e92e3 100644 --- a/lib/yarn2-package-manager.ts +++ b/lib/yarn2-package-manager.ts @@ -26,7 +26,7 @@ export class Yarn2PackageManager extends BasePackageManager { $hostInfo: IHostInfo, private $httpClient: Server.IHttpClient, private $logger: ILogger, - $pacoteService: IPacoteService + $pacoteService: IPacoteService, ) { super($childProcess, $fs, $hostInfo, $pacoteService, "yarn2"); this.$hostInfo_ = $hostInfo; @@ -46,11 +46,12 @@ export class Yarn2PackageManager extends BasePackageManager { public async install( packageName: string, pathToSave: string, - config: INodePackageManagerInstallOptions + config: INodePackageManagerInstallOptions, ): Promise { if (config.disableNpmInstall) { return; } + delete (config as any).legacyPeers; if (config.ignoreScripts) { config["ignore-scripts"] = true; } @@ -76,7 +77,7 @@ export class Yarn2PackageManager extends BasePackageManager { const result = await this.processPackageManagerInstall( packageName, params, - { cwd, isInstallingAllDependencies } + { cwd, isInstallingAllDependencies }, ); return result; } catch (e) { @@ -89,7 +90,7 @@ export class Yarn2PackageManager extends BasePackageManager { public uninstall( packageName: string, config?: IDictionary, - cwd?: string + cwd?: string, ): Promise { const flags = this.getFlagsString(config, false); return this.$childProcess.exec(`yarn remove ${packageName} ${flags}`, { @@ -105,7 +106,7 @@ export class Yarn2PackageManager extends BasePackageManager { let viewResult: any; try { viewResult = await this.$childProcess.exec( - `yarn npm info ${packageName} ${flags}` + `yarn npm info ${packageName} ${flags}`, ); } catch (e) { this.$errors.fail(e.message); @@ -122,17 +123,17 @@ export class Yarn2PackageManager extends BasePackageManager { @exported("yarn2") public search( filter: string[], - config: IDictionary + config: IDictionary, ): Promise { this.$errors.fail( - "Method not implemented. Yarn does not support searching for packages in the registry." + "Method not implemented. Yarn does not support searching for packages in the registry.", ); return null; } public async searchNpms(keyword: string): Promise { const httpRequestResult = await this.$httpClient.httpRequest( - `https://api.npms.io/v2/search?q=keywords:${keyword}` + `https://api.npms.io/v2/search?q=keywords:${keyword}`, ); const result: INpmsResult = JSON.parse(httpRequestResult.body); return result; @@ -141,19 +142,19 @@ export class Yarn2PackageManager extends BasePackageManager { @exported("yarn2") public async getRegistryPackageData(packageName: string): Promise { const registry = await this.$childProcess.exec( - `yarn config get npmRegistryServer` + `yarn config get npmRegistryServer`, ); const url = `${registry.trim()}/${packageName}`; this.$logger.trace( - `Trying to get data from yarn registry for package ${packageName}, url is: ${url}` + `Trying to get data from yarn registry for package ${packageName}, url is: ${url}`, ); const responseData = (await this.$httpClient.httpRequest(url)).body; this.$logger.trace( - `Successfully received data from yarn registry for package ${packageName}. Response data is: ${responseData}` + `Successfully received data from yarn registry for package ${packageName}. Response data is: ${responseData}`, ); const jsonData = JSON.parse(responseData); this.$logger.trace( - `Successfully parsed data from yarn registry for package ${packageName}.` + `Successfully parsed data from yarn registry for package ${packageName}.`, ); return jsonData; } diff --git a/test/node-package-manager.ts b/test/node-package-manager.ts index 27efb270f3..f74b46ba7f 100644 --- a/test/node-package-manager.ts +++ b/test/node-package-manager.ts @@ -6,7 +6,7 @@ import { IInjector } from "../lib/common/definitions/yok"; function createTestInjector(configuration: {} = {}): IInjector { const injector = new Yok(); - injector.register("hostInfo", {}); + injector.register("hostInfo", { isWindows: false }); injector.register("errors", stubs.ErrorsStub); injector.register("logger", stubs.LoggerStub); injector.register("childProcess", stubs.ChildProcessStub); @@ -30,15 +30,13 @@ describe("node-package-manager", () => { expectedName: "some-template", }, { - name: - "should return both name and version when valid fullName with scope passed", + name: "should return both name and version when valid fullName with scope passed", templateFullName: "@nativescript/some-template@1.0.0", expectedVersion: "1.0.0", expectedName: "@nativescript/some-template", }, { - name: - "should return only name when version is not specified and the template is scoped", + name: "should return only name when version is not specified and the template is scoped", templateFullName: "@nativescript/some-template", expectedVersion: "", expectedName: "@nativescript/some-template", @@ -54,7 +52,7 @@ describe("node-package-manager", () => { const testInjector = createTestInjector(); const npm = testInjector.resolve("npm"); const templateNameParts = await npm.getPackageNameParts( - testCase.templateFullName + testCase.templateFullName, ); assert.strictEqual(templateNameParts.name, testCase.expectedName); assert.strictEqual(templateNameParts.version, testCase.expectedVersion); @@ -96,4 +94,23 @@ describe("node-package-manager", () => { }); }); }); + + describe("install", () => { + it("passes --legacy-peer-deps when legacyPeers is set", async () => { + const testInjector = createTestInjector(); + const npm = testInjector.resolve("npm"); + const childProcess = + testInjector.resolve("childProcess"); + + await npm.install("/tmp/project", "/tmp/project", { + disableNpmInstall: false, + frameworkPath: null, + ignoreScripts: false, + legacyPeers: true, + }); + + assert.include(childProcess.lastCommandArgs, "--legacy-peer-deps"); + assert.notInclude(childProcess.lastCommandArgs, "--legacyPeers"); + }); + }); }); From 083272ba24fb8366b52aa3def484ae4611066383 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Mon, 15 Dec 2025 19:13:57 -0800 Subject: [PATCH 2/2] feat: legacy flag --- lib/commands/create-project.ts | 5 +- lib/options.ts | 6 ++- lib/services/project-service.ts | 1 + test/project-service.ts | 83 ++++++++++++++++++++++----------- 4 files changed, 67 insertions(+), 28 deletions(-) diff --git a/lib/commands/create-project.ts b/lib/commands/create-project.ts index b27b4c19c8..f53b1c9777 100644 --- a/lib/commands/create-project.ts +++ b/lib/commands/create-project.ts @@ -134,6 +134,9 @@ export class CreateProjectCommand implements ICommand { ); } + const legacyPeerDeps = + this.$options.legacyPeerDeps || (this.$options as any).legacyPeers; + this.createdProjectData = await this.$projectService.createProject({ projectName: projectName, template: selectedTemplate, @@ -142,7 +145,7 @@ export class CreateProjectCommand implements ICommand { // its already validated above force: true, ignoreScripts: this.$options.ignoreScripts, - legacyPeerDeps: this.$options.legacyPeerDeps, + legacyPeerDeps, }); } diff --git a/lib/options.ts b/lib/options.ts index 65b16a32bb..624a705484 100644 --- a/lib/options.ts +++ b/lib/options.ts @@ -120,7 +120,11 @@ export class Options { hasSensitiveValue: true, }, ignoreScripts: { type: OptionType.Boolean, hasSensitiveValue: false }, - legacyPeerDeps: { type: OptionType.Boolean, hasSensitiveValue: false }, + legacyPeerDeps: { + type: OptionType.Boolean, + alias: "legacyPeers", + hasSensitiveValue: false, + }, disableNpmInstall: { type: OptionType.Boolean, hasSensitiveValue: false }, compileSdk: { type: OptionType.Number, hasSensitiveValue: false }, port: { type: OptionType.Number, hasSensitiveValue: false }, diff --git a/lib/services/project-service.ts b/lib/services/project-service.ts index cef58d4916..daef9c783c 100644 --- a/lib/services/project-service.ts +++ b/lib/services/project-service.ts @@ -104,6 +104,7 @@ export class ProjectService implements IProjectService { template: projectOptions.template, projectDir, ignoreScripts: projectOptions.ignoreScripts, + legacyPeerDeps: projectOptions.legacyPeerDeps, appId: appId, projectName, }); diff --git a/test/project-service.ts b/test/project-service.ts index 21d1e5f1dc..a1196c2616 100644 --- a/test/project-service.ts +++ b/test/project-service.ts @@ -24,8 +24,16 @@ describe("projectService", () => { /* tslint:disable:no-empty */ const getTestInjector = (opts: { projectName: string }): IInjector => { const testInjector = new yok.Yok(); + let lastInstallConfig: any = null; testInjector.register("packageManager", { - install: async () => {}, + install: async ( + _packageName: string, + _pathToSave: string, + config: any, + ) => { + lastInstallConfig = config; + }, + _getLastInstallConfig: () => lastInstallConfig, }); testInjector.register("errors", ErrorsStub); testInjector.register("fs", { @@ -79,7 +87,7 @@ describe("projectService", () => { testInjector.register("hooksService", { executeAfterHooks: async ( commandName: string, - hookArguments?: IDictionary + hookArguments?: IDictionary, ): Promise => undefined, }); testInjector.register("pacoteService", { @@ -104,7 +112,7 @@ describe("projectService", () => { const projectName = invalidProjectName; const testInjector = getTestInjector({ projectName }); const projectService = testInjector.resolve( - ProjectServiceLib.ProjectService + ProjectServiceLib.ProjectService, ); const projectDir = path.join(dirToCreateProject, projectName); const projectCreationData = await projectService.createProject({ @@ -114,6 +122,31 @@ describe("projectService", () => { template: constants.RESERVED_TEMPLATE_NAMES["default"], }); + it("passes legacyPeerDeps to package manager install", async () => { + const projectName = invalidProjectName; + const testInjector = getTestInjector({ projectName }); + const projectService = testInjector.resolve( + ProjectServiceLib.ProjectService, + ); + + await projectService.createProject({ + projectName: projectName, + pathToProject: dirToCreateProject, + force: true, + template: constants.RESERVED_TEMPLATE_NAMES["default"], + legacyPeerDeps: true, + }); + + const installConfig = testInjector + .resolve("packageManager") + ._getLastInstallConfig(); + assert.isOk( + installConfig, + "Expected package manager install to be called", + ); + assert.isTrue(installConfig.legacyPeers); + }); + assert.deepStrictEqual(projectCreationData, { projectName, projectDir, @@ -125,7 +158,7 @@ describe("projectService", () => { const testInjector = getTestInjector({ projectName }); const options = testInjector.resolve("options"); const projectService = testInjector.resolve( - ProjectServiceLib.ProjectService + ProjectServiceLib.ProjectService, ); const projectDir = path.join(dirToCreateProject, projectName); @@ -145,7 +178,7 @@ describe("projectService", () => { `git init ${projectDir}`, `git -C ${projectDir} add --all`, `git -C ${projectDir} commit --no-verify -m "init"`, - ] + ], ); }); @@ -154,7 +187,7 @@ describe("projectService", () => { const testInjector = getTestInjector({ projectName }); const options = testInjector.resolve("options"); const projectService = testInjector.resolve( - ProjectServiceLib.ProjectService + ProjectServiceLib.ProjectService, ); // simulate --no-git @@ -169,22 +202,21 @@ describe("projectService", () => { assert.deepEqual( testInjector.resolve("childProcess")._getExecutedCommands(), - [] + [], ); }); it("fails when invalid name is passed when projectNameService fails", async () => { const projectName = invalidProjectName; const testInjector = getTestInjector({ projectName }); - const projectNameService = testInjector.resolve( - "projectNameService" - ); + const projectNameService = + testInjector.resolve("projectNameService"); const err = new Error("Invalid name"); projectNameService.ensureValidName = (name: string) => { throw err; }; const projectService = testInjector.resolve( - ProjectServiceLib.ProjectService + ProjectServiceLib.ProjectService, ); await assert.isRejected( projectService.createProject({ @@ -192,7 +224,7 @@ describe("projectService", () => { pathToProject: dirToCreateProject, template: constants.RESERVED_TEMPLATE_NAMES["default"], }), - err.message + err.message, ); }); @@ -202,7 +234,7 @@ describe("projectService", () => { const fs = testInjector.resolve("fs"); fs.isEmptyDir = (name: string) => false; const projectService = testInjector.resolve( - ProjectServiceLib.ProjectService + ProjectServiceLib.ProjectService, ); await assert.isRejected( projectService.createProject({ @@ -212,8 +244,8 @@ describe("projectService", () => { }), `Path already exists and is not empty ${path.join( dirToCreateProject, - projectName - )}` + projectName, + )}`, ); }); }); @@ -243,7 +275,7 @@ describe("projectService", () => { testInjector.register("hooksService", { executeAfterHooks: async ( commandName: string, - hookArguments?: IDictionary + hookArguments?: IDictionary, ): Promise => undefined, }); testInjector.register("pacoteService", { @@ -264,16 +296,15 @@ describe("projectService", () => { }); const projectService: IProjectService = testInjector.resolve( - ProjectServiceLib.ProjectService + ProjectServiceLib.ProjectService, ); assert.isTrue(projectService.isValidNativeScriptProject("some-dir")); }); it("returns correct data when multiple calls are executed", () => { const testInjector = getTestInjector(); - const projectDataService = testInjector.resolve( - "projectDataService" - ); + const projectDataService = + testInjector.resolve("projectDataService"); const projectData: any = { projectDir: "projectDir", projectId: "projectId", @@ -282,7 +313,7 @@ describe("projectService", () => { let returnedProjectData: any = null; projectDataService.getProjectData = ( - projectDir?: string + projectDir?: string, ): IProjectData => { projectData.projectDir = projectDir; returnedProjectData = projectData; @@ -290,7 +321,7 @@ describe("projectService", () => { }; const projectService: IProjectService = testInjector.resolve( - ProjectServiceLib.ProjectService + ProjectServiceLib.ProjectService, ); assert.isTrue(projectService.isValidNativeScriptProject("some-dir")); assert.equal(returnedProjectData.projectDir, "some-dir"); @@ -298,7 +329,7 @@ describe("projectService", () => { assert.equal(returnedProjectData.projectDir, "some-dir-2"); projectDataService.getProjectData = ( - projectDir?: string + projectDir?: string, ): IProjectData => { throw new Error("Err"); }; @@ -315,7 +346,7 @@ describe("projectService", () => { }); const projectService: IProjectService = testInjector.resolve( - ProjectServiceLib.ProjectService + ProjectServiceLib.ProjectService, ); assert.isFalse(projectService.isValidNativeScriptProject("some-dir")); }); @@ -326,7 +357,7 @@ describe("projectService", () => { }); const projectService: IProjectService = testInjector.resolve( - ProjectServiceLib.ProjectService + ProjectServiceLib.ProjectService, ); assert.isFalse(projectService.isValidNativeScriptProject("some-dir")); }); @@ -337,7 +368,7 @@ describe("projectService", () => { }); const projectService: IProjectService = testInjector.resolve( - ProjectServiceLib.ProjectService + ProjectServiceLib.ProjectService, ); assert.isFalse(projectService.isValidNativeScriptProject("some-dir")); });