From 44e97cf5cf5346bb7f011f2151f46920428f9349 Mon Sep 17 00:00:00 2001 From: "INFRAGISTICS\\IPetrov" Date: Tue, 8 Sep 2026 14:13:43 +0300 Subject: [PATCH 1/2] fix(schematics): resolve valid project template before upgrading Ignite UI packages --- packages/cli/lib/commands/upgrade.ts | 14 +++---- packages/core/packages/PackageManager.ts | 14 +++---- packages/core/util/ProjectTemplateResolver.ts | 42 +++++++++++++++++++ packages/core/util/index.ts | 1 + .../src/upgrade-packages/index.ts | 13 +++--- 5 files changed, 59 insertions(+), 25 deletions(-) create mode 100644 packages/core/util/ProjectTemplateResolver.ts diff --git a/packages/cli/lib/commands/upgrade.ts b/packages/cli/lib/commands/upgrade.ts index 9205d6f13..5e115b6c2 100644 --- a/packages/cli/lib/commands/upgrade.ts +++ b/packages/cli/lib/commands/upgrade.ts @@ -1,5 +1,5 @@ -import { App, BaseTemplateManager, detectFrameworkFromPackageJson, GoogleAnalytics, ProjectConfig, TEMPLATE_MANAGER, - type ProjectTemplate, Util } from "@igniteui/cli-core"; +import { App, BaseTemplateManager, detectFrameworkFromPackageJson, GoogleAnalytics, ProjectConfig, resolveUpgradeableProject, TEMPLATE_MANAGER, + Util } from "@igniteui/cli-core"; import { PositionalArgs, UpgradeCommandType } from "./types"; import { ArgumentsCamelCase } from "yargs"; @@ -60,12 +60,10 @@ const command: UpgradeCommandType = { if (projectType === "igx-ts" || projectType === "igr-ts" || projectType === "igc-ts") { const templateManager = App.container.get(TEMPLATE_MANAGER); const projectLibrary = templateManager.getProjectLibrary(framework, projectType); - let project: ProjectTemplate; - if (!config.project?.projectTemplate || !projectLibrary.hasProject(config.project.projectTemplate)) { - // in case project template is missing from the config we provide backward. - project = projectLibrary.getProject(projectLibrary.projectIds[0]); - } else { - project = projectLibrary.getProject(config.project.projectTemplate); + const project = resolveUpgradeableProject(projectLibrary, config.project?.projectTemplate); + if (!project) { + Util.error("No valid Ignite UI project template found to upgrade packages.", "red"); + return; } const success = await project.upgradeIgniteUIPackages(process.cwd(), ""); if (success && !argv.skipInstall) { diff --git a/packages/core/packages/PackageManager.ts b/packages/core/packages/PackageManager.ts index 8af24a028..c4f5e5a7e 100644 --- a/packages/core/packages/PackageManager.ts +++ b/packages/core/packages/PackageManager.ts @@ -1,8 +1,8 @@ import { exec } from "child_process"; import * as path from "path"; import { BaseTemplateManager } from "../templates"; -import { Config, FS_TOKEN, IFileSystem, ProjectTemplate } from "../types"; -import { App, ProjectConfig, Util } from "../util"; +import { Config, FS_TOKEN, IFileSystem } from "../types"; +import { App, ProjectConfig, resolveUpgradeableProject, Util } from "../util"; import componentsConfig = require("./components"); @@ -69,14 +69,10 @@ export class PackageManager { const projectLibrary = templateManager.getProjectLibrary(config.project.framework, config.project.projectType); if (projectLibrary) { // TODO multiple projects? - let project: ProjectTemplate; - if (!config.project.projectTemplate) { - // in case project template is missing from the config we provide backward. - project = projectLibrary.getProject(projectLibrary.projectIds[0]); - } else { - project = projectLibrary.getProject(config.project.projectTemplate); + const project = resolveUpgradeableProject(projectLibrary, config.project.projectTemplate); + if (project) { + await project.upgradeIgniteUIPackages(process.cwd(), `./node_modules/${this.fullPackage}/en`); } - await project.upgradeIgniteUIPackages(process.cwd(), `./node_modules/${this.fullPackage}/en`); } } } else { diff --git a/packages/core/util/ProjectTemplateResolver.ts b/packages/core/util/ProjectTemplateResolver.ts new file mode 100644 index 000000000..d341f698a --- /dev/null +++ b/packages/core/util/ProjectTemplateResolver.ts @@ -0,0 +1,42 @@ +import { ProjectLibrary, ProjectTemplate } from "../types"; + +/** Returns true if the given project template is safe to use for upgrade operations, + * i.e. it's not hidden/partial-only and actually implements `upgradeIgniteUIPackages`. */ +function isUpgradeableProject(project: ProjectTemplate): boolean { + return !!project && !project.isHidden && typeof project.upgradeIgniteUIPackages === "function"; +} + +/** + * Resolves the project template to use for Ignite UI package upgrade operations. + * + * Resolution order: + * 1. The configured `projectTemplate` id, if it exists in the library and is a valid, + * non-hidden project that implements `upgradeIgniteUIPackages`. + * 2. The first project in `library.projectIds` (declared order) that is valid, + * non-hidden, and implements `upgradeIgniteUIPackages`. + * + * This avoids blindly falling back to `projectIds[0]`, which can select hidden/partial + * project templates (e.g. `ai-config`) that don't support upgrading. + * + * @returns the resolved project template, or `null` if none could be found. + */ +export function resolveUpgradeableProject( + library: ProjectLibrary, + configuredTemplateId?: string +): ProjectTemplate { + if (configuredTemplateId && library.hasProject(configuredTemplateId)) { + const configuredProject = library.getProject(configuredTemplateId); + if (isUpgradeableProject(configuredProject)) { + return configuredProject; + } + } + + for (const id of library.projectIds) { + const project = library.getProject(id); + if (isUpgradeableProject(project)) { + return project; + } + } + + return null; +} diff --git a/packages/core/util/index.ts b/packages/core/util/index.ts index fefde718d..f2362f4e8 100644 --- a/packages/core/util/index.ts +++ b/packages/core/util/index.ts @@ -5,6 +5,7 @@ export * from './mcp-config'; export * from './Util'; export * from './DotnetTemplateManager'; export * from './ProjectConfig'; +export * from './ProjectTemplateResolver'; export * from './Schematics'; export * from './App'; export * from './FileSystem'; diff --git a/packages/ng-schematics/src/upgrade-packages/index.ts b/packages/ng-schematics/src/upgrade-packages/index.ts index 79ade3511..19e6b5411 100644 --- a/packages/ng-schematics/src/upgrade-packages/index.ts +++ b/packages/ng-schematics/src/upgrade-packages/index.ts @@ -1,6 +1,6 @@ -import { Rule, SchematicContext, Tree } from "@angular-devkit/schematics"; +import { Rule, SchematicContext, SchematicsException, Tree } from "@angular-devkit/schematics"; import { NodePackageInstallTask } from "@angular-devkit/schematics/tasks"; -import { App, GoogleAnalytics, ProjectConfig, ProjectTemplate } from "@igniteui/cli-core"; +import { App, GoogleAnalytics, ProjectConfig, resolveUpgradeableProject } from "@igniteui/cli-core"; import { defer } from "rxjs"; import { SchematicsTemplateManager } from "../SchematicsTemplateManager"; import { setVirtual } from "../utils/NgFileSystem"; @@ -19,12 +19,9 @@ export default function(options: UpgradeOptions): Rule { const templateManager = new SchematicsTemplateManager(); const config = ProjectConfig.getConfig(); const library = templateManager.getProjectLibrary('angular', config.project?.projectType || 'igx-ts'); - let project: ProjectTemplate; - if (!config.project?.projectTemplate || !library.hasProject(config.project?.projectTemplate)) { - // in case project template is missing from the config we provide backward. - project = library.getProject(library.projectIds[0]); - } else { - project = library.getProject(config.project.projectTemplate); + const project = resolveUpgradeableProject(library, config.project?.projectTemplate); + if (!project) { + throw new SchematicsException("No valid Ignite UI project template found to upgrade packages."); } setVirtual(tree); return defer(async () => { From a5001acf1b6efeef5e7f3c9106813792be5feb61 Mon Sep 17 00:00:00 2001 From: "INFRAGISTICS\\IPetrov" Date: Tue, 8 Sep 2026 14:43:05 +0300 Subject: [PATCH 2/2] fix(schematics): addressed copilot comments and added test --- packages/core/packages/PackageManager.ts | 4 +- packages/core/util/ProjectTemplateResolver.ts | 2 +- spec/unit/ProjectTemplateResolver-spec.ts | 87 +++++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 spec/unit/ProjectTemplateResolver-spec.ts diff --git a/packages/core/packages/PackageManager.ts b/packages/core/packages/PackageManager.ts index c4f5e5a7e..ba7e2b651 100644 --- a/packages/core/packages/PackageManager.ts +++ b/packages/core/packages/PackageManager.ts @@ -70,7 +70,9 @@ export class PackageManager { if (projectLibrary) { // TODO multiple projects? const project = resolveUpgradeableProject(projectLibrary, config.project.projectTemplate); - if (project) { + if (!project) { + Util.warn("No valid Ignite UI project template found; skipping upgrade step.", "yellow"); + } else { await project.upgradeIgniteUIPackages(process.cwd(), `./node_modules/${this.fullPackage}/en`); } } diff --git a/packages/core/util/ProjectTemplateResolver.ts b/packages/core/util/ProjectTemplateResolver.ts index d341f698a..4fdd7ac4e 100644 --- a/packages/core/util/ProjectTemplateResolver.ts +++ b/packages/core/util/ProjectTemplateResolver.ts @@ -23,7 +23,7 @@ function isUpgradeableProject(project: ProjectTemplate): boolean { export function resolveUpgradeableProject( library: ProjectLibrary, configuredTemplateId?: string -): ProjectTemplate { +): ProjectTemplate | null { if (configuredTemplateId && library.hasProject(configuredTemplateId)) { const configuredProject = library.getProject(configuredTemplateId); if (isUpgradeableProject(configuredProject)) { diff --git a/spec/unit/ProjectTemplateResolver-spec.ts b/spec/unit/ProjectTemplateResolver-spec.ts new file mode 100644 index 000000000..f3bccf563 --- /dev/null +++ b/spec/unit/ProjectTemplateResolver-spec.ts @@ -0,0 +1,87 @@ +import { ProjectLibrary, ProjectTemplate, resolveUpgradeableProject } from "@igniteui/cli-core"; + +describe("Unit - resolveUpgradeableProject", () => { + + function mockProject(id: string, options: { isHidden?: boolean; upgradeable?: boolean } = {}): ProjectTemplate { + return { + id, + isHidden: !!options.isHidden, + upgradeIgniteUIPackages: options.upgradeable === false ? undefined : jasmine.createSpy().and.returnValue(Promise.resolve(true)) + } as unknown as ProjectTemplate; + } + + function mockLibrary(projects: { [id: string]: ProjectTemplate }, projectIds: string[]): ProjectLibrary { + return { + projectIds, + hasProject: jasmine.createSpy().and.callFake((id: string) => projectIds.indexOf(id) > -1), + getProject: jasmine.createSpy().and.callFake((id: string) => projects[id] || null) + } as unknown as ProjectLibrary; + } + + it("returns the configured project template when it exists and is upgradeable", () => { + const emptyProject = mockProject("empty"); + const library = mockLibrary({ empty: emptyProject }, ["ai-config", "empty"]); + + const result = resolveUpgradeableProject(library, "empty"); + + expect(result).toBe(emptyProject); + expect(library.getProject).toHaveBeenCalledWith("empty"); + }); + + it("skips the configured project template when it is hidden and falls back to the first upgradeable project", () => { + const hiddenConfigured = mockProject("hidden-configured", { isHidden: true }); + const emptyProject = mockProject("empty"); + const library = mockLibrary( + { "hidden-configured": hiddenConfigured, empty: emptyProject }, + ["ai-config", "empty"] + ); + + const result = resolveUpgradeableProject(library, "hidden-configured"); + + expect(result).toBe(emptyProject); + }); + + it("skips hidden projects like ai-config and falls back to the first non-hidden upgradeable project", () => { + const aiConfig = mockProject("ai-config", { isHidden: true }); + const base = mockProject("base", { isHidden: true }); + const empty = mockProject("empty"); + const library = mockLibrary( + { "ai-config": aiConfig, base, empty }, + ["ai-config", "base", "empty"] + ); + + const result = resolveUpgradeableProject(library, "invalid-configured-template"); + + expect(result).toBe(empty); + }); + + it("skips projects that don't implement upgradeIgniteUIPackages", () => { + const notUpgradeable = mockProject("not-upgradeable", { upgradeable: false }); + const empty = mockProject("empty"); + const library = mockLibrary( + { "not-upgradeable": notUpgradeable, empty }, + ["not-upgradeable", "empty"] + ); + + const result = resolveUpgradeableProject(library); + + expect(result).toBe(empty); + }); + + it("returns null when the configured template is invalid and no project in the library is upgradeable", () => { + const aiConfig = mockProject("ai-config", { isHidden: true }); + const library = mockLibrary({ "ai-config": aiConfig }, ["ai-config"]); + + const result = resolveUpgradeableProject(library, "invalid-configured-template"); + + expect(result).toBeNull(); + }); + + it("returns null when the library has no projects at all", () => { + const library = mockLibrary({}, []); + + const result = resolveUpgradeableProject(library); + + expect(result).toBeNull(); + }); +});