From 7174fc300dcbaac2a3d5cc9832d1381173dba1d4 Mon Sep 17 00:00:00 2001 From: mstoyanova Date: Wed, 21 Oct 2026 15:29:33 +0300 Subject: [PATCH 01/10] feat: regenerate Angular base project and update dependencies --- .agents/README.md | 3 +- .../igniteui-cli-template-anatomy/SKILL.md | 22 +- .../SKILL.md | 2 +- AGENTS.md | 1 + .../projects/_base/files/__dot__gitignore | 1 + .../_base/files/__dot__vscode/tasks.json | 8 +- .../igx-ts/projects/_base/files/angular.json | 39 ++- .../igx-ts/projects/_base/files/package.json | 10 +- .../igx-ts/projects/_base/files/src/main.ts | 2 +- .../projects/_base/files/tsconfig.app.json | 1 - .../igx-ts/projects/_base/files/tsconfig.json | 7 +- .../projects/_base/files/tsconfig.spec.json | 1 - .../_base_with_home/files/src/favicon.ico | Bin 15086 -> 0 bytes .../projects/side-nav-auth/files/package.json | 13 +- scripts/generate-angular-base.mjs | 296 ++++++++++++++++++ scripts/generate-angular-base.upstream.json | 13 + 16 files changed, 380 insertions(+), 39 deletions(-) delete mode 100644 packages/igx-templates/igx-ts/projects/_base_with_home/files/src/favicon.ico create mode 100644 scripts/generate-angular-base.mjs create mode 100644 scripts/generate-angular-base.upstream.json diff --git a/.agents/README.md b/.agents/README.md index ab7298bcfe..cb0e4d0aec 100644 --- a/.agents/README.md +++ b/.agents/README.md @@ -24,7 +24,7 @@ Each procedure is written once, as a skill in `.agents/skills/` ([Agent Skills]( | [`igniteui-cli-ai-config-skills-refresh`](./skills/igniteui-cli-ai-config-skills-refresh/SKILL.md) | Workflow | Bundled `ai-config` skills | Mirror the framework repositories' `skills/` into the `ai-config` templates. In `dry-run` mode: what changed upstream and content issues | | [`igniteui-cli-mcp-tool`](./skills/igniteui-cli-mcp-tool/SKILL.md) | Workflow | MCP server `src/`, tests, tool docs | Add an MCP server tool or change an existing one. Its `scripts/call-tool.mjs` calls a tool on the built server over stdio | | [`igniteui-cli-smoke-test`](./skills/igniteui-cli-smoke-test/SKILL.md) | Reference | — | Build, running one spec file, specs that guard templates, running the smoke test, checking kept projects, triaging failures ("Triaging a Run"), editing the script | -| [`igniteui-cli-template-anatomy`](./skills/igniteui-cli-template-anatomy/SKILL.md) | Reference | — | Template layout per framework, model templates, every place versions are pinned, parity auditing ("Auditing Parity Across Frameworks") | +| [`igniteui-cli-template-anatomy`](./skills/igniteui-cli-template-anatomy/SKILL.md) | Reference | — | Template layout per framework, model templates, the generated Angular base project, every place versions are pinned, parity auditing ("Auditing Parity Across Frameworks") | The workflows triage their own smoke runs; use the smoke-test skill directly for a run you started by hand. @@ -55,6 +55,7 @@ Each workflow runs on its own and does not call the others. You chain them. | See which pins are stale or conflicting | "dry-run template version update, all frameworks" | | Add a template | "add a Tile Manager component template for angular" | | See which templates are missing | "which component templates are missing per framework?" | +| Move the Angular base project to a new Angular version | "regenerate the Angular base project for Angular ~22.3.0" | | Make sense of a failed smoke run | "triage output/smoke" | | Release notes for 15.9.0 | "draft the changelog for 15.9.0, 2026-10-15" | | Refresh the skills `ig ai-config` bundles | "refresh ai-config skills for all frameworks" | diff --git a/.agents/skills/igniteui-cli-template-anatomy/SKILL.md b/.agents/skills/igniteui-cli-template-anatomy/SKILL.md index 9d420e1030..77f0e392e0 100644 --- a/.agents/skills/igniteui-cli-template-anatomy/SKILL.md +++ b/.agents/skills/igniteui-cli-template-anatomy/SKILL.md @@ -1,6 +1,6 @@ --- name: igniteui-cli-template-anatomy -description: "Template anatomy for Angular (igx-ts), React (igr-ts) and Web Components (igc-ts): folder layout, index.ts fields, placeholders, test files, groups, and every place an Ignite UI version is pinned. WHEN TO USE: adding or changing a component template, locating version pins, auditing template parity across frameworks. WHEN NOT TO USE: running builds, template specs or the smoke test (igniteui-cli-smoke-test)." +description: "Template anatomy for Angular (igx-ts), React (igr-ts) and Web Components (igc-ts): folder layout, index.ts fields, placeholders, test files, groups, the generated Angular base project, and every place an Ignite UI version is pinned. WHEN TO USE: adding or changing a component template, moving the Angular base to a new Angular version, locating version pins, auditing template parity across frameworks. WHEN NOT TO USE: running builds, template specs or the smoke test (igniteui-cli-smoke-test)." license: MIT --- @@ -83,6 +83,26 @@ Angular has two styles. Pick the one the component library dictates: React imports components from `igniteui-react` (or `-grids`, `-charts`, `-gauges`) as `Igr*`. Web Components imports `Igc*Component` and calls `defineComponents(...)`. +## Generated Angular Base Project + +`scripts/generate-angular-base.mjs` rebuilds the Angular-owned files of `packages/igx-templates/igx-ts/projects/_base/files` from `ng new`, followed by `ng add angular-eslint` (at the Angular major) and `ng add igniteui-angular`. It needs network access and runs `npm install` in a temp folder, a few minutes. + +| Goal | Command | +|---|---| +| New Angular minor | `node scripts/generate-angular-base.mjs --angular ~22.3.0` | +| New Angular major (`igniteui-angular` moves with it) | `node scripts/generate-angular-base.mjs --angular ~23.0.0 --igniteui ~23.0.0` | +| Is the base out of date? Writes nothing, exits 1 if a run would change something | `node scripts/generate-angular-base.mjs --check` | + +Without `--angular` / `--igniteui` it uses the `@angular/cli` and `igniteui-angular` pins in `_base/files/package.json`. A run at unchanged versions changes nothing. + +- **Generated**: the files in the script's `COPIED` list, `_base/files/angular.json`, `_base/files/package.json` and `side-nav-auth/files/package.json` (the `_base` manifest plus the packages only `side-nav-auth` uses). Our deviations from `ng new` are patches in the script, so a change to them goes into the script; a hand edit is lost on the next run. +- **Hand-maintained**: every other file in `_base/files` (`README.md`, `src/app/*`, `src/index.html`, `src/styles.scss`, CI workflows, eslint config, environments) and the overlays (`_base_with_home`, `empty`, `side-nav`, `side-nav-auth` tsconfigs, ...). +- **Upstream changes**: the `ng new`/`ng add` versions of the hand-maintained files are fingerprinted in `scripts/generate-angular-base.upstream.json`. When one changed since the last run, the script lists it with a kept upstream copy: port what applies to our file by hand. The updated fingerprint file belongs in the same change as the run's other output. +- **Pins**: `ng new` and `ng add` decide which packages exist. A package already in `_base` keeps its pin while that admits the version installed in the generated project, so a run moves only the pins the new project needs moved (never loosening the others), and the template version update workflow stays the owner of Ignite UI pins. +- **`--igniteui`** sets `igniteui-angular` in the generated manifests only. The other `igniteui-angular` pins (`IGNITEUI_ANGULAR_PACKAGE` and the rest, "Where Ignite UI Versions Are Pinned") must move to the same range; `spec/templates/angular-spec.ts` checks it. + +After a run, validate with the smoke test: `-f angular --all-projects`, plus `-p base`, `-p side-nav-auth` and `-p side-nav-mini-auth` (smoke-test skill). + ## Where Ignite UI Versions Are Pinned Pins live in three kinds of places. **Always regenerate the list with a search; never trust a remembered list.** diff --git a/.agents/skills/igniteui-cli-template-version-update/SKILL.md b/.agents/skills/igniteui-cli-template-version-update/SKILL.md index ab9b0d2951..91f3517c4c 100644 --- a/.agents/skills/igniteui-cli-template-version-update/SKILL.md +++ b/.agents/skills/igniteui-cli-template-version-update/SKILL.md @@ -98,7 +98,7 @@ For each distinct package: - **major** (or `0.x` minor change): **stop for that package**. Leave all of its pins unchanged. - **already current** (the existing range already resolves to the target): no change. This includes `^` pins whose range admits a newer minor (e.g. `^2.1.1` admits 2.2.1). Keep their minimum unless the user asks; list them under Observations as "minimum could be raised to `^`". 4. For packages that proceed, check peers: `npm view @ peerDependencies`. - - If a peer is a **non-Ignite UI** package (e.g. a new `igniteui-angular` needing a newer `@angular/core`) and the project templates don't satisfy it, **stop for that package** and report the required change. + - If a peer is a **non-Ignite UI** package (e.g. a new `igniteui-angular` needing a newer `@angular/core`) and the project templates don't satisfy it, **stop for that package** and report the required change. For Angular, report it as the generator command that makes the change, with `--igniteui ` when `igniteui-angular` is the package moving (anatomy skill, "Generated Angular Base Project"). - If a peer is another **Ignite UI** package pinned in templates (e.g. `igniteui-angular` → `igniteui-grid-lite ~0.11.0`), that pin must satisfy the peer range after your update. Treat bringing it into range as part of this update, subject to the same patch/minor/major rule; if that would be a major (or `0.x` minor) change, report it as a stop item with the peer range as the reason, so a human can approve it explicitly. Do this check also for the framework's **main package even when it is not changing**: existing pins can already be out of range. diff --git a/AGENTS.md b/AGENTS.md index 3336cce1ec..5345d3990f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,6 +57,7 @@ Adding or changing an MCP tool (schema, handler, description, tests, every doc t - Placeholders are substituted by the CLI, not a template engine: `<%=ClassName%>` style in Angular templates, `$(ClassName)` style in React and Web Components templates - Templates in `packages/cli/templates/**/files/` and `packages/igx-templates/` +- Angular `_base` manifests and configs are generated by `scripts/generate-angular-base.mjs`: change its patches, not its output (`igniteui-cli-template-anatomy` skill) - Component/template IDs (e.g., "grid", "combo") used by `ig add` command - Default framework is **jQuery** when not specified diff --git a/packages/igx-templates/igx-ts/projects/_base/files/__dot__gitignore b/packages/igx-templates/igx-ts/projects/_base/files/__dot__gitignore index b1d225e26e..854acd5fc0 100644 --- a/packages/igx-templates/igx-ts/projects/_base/files/__dot__gitignore +++ b/packages/igx-templates/igx-ts/projects/_base/files/__dot__gitignore @@ -26,6 +26,7 @@ yarn-error.log !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json +!.vscode/mcp.json .history/* # Miscellaneous diff --git a/packages/igx-templates/igx-ts/projects/_base/files/__dot__vscode/tasks.json b/packages/igx-templates/igx-ts/projects/_base/files/__dot__vscode/tasks.json index a298b5bd87..244306f98a 100644 --- a/packages/igx-templates/igx-ts/projects/_base/files/__dot__vscode/tasks.json +++ b/packages/igx-templates/igx-ts/projects/_base/files/__dot__vscode/tasks.json @@ -12,10 +12,10 @@ "background": { "activeOnStart": true, "beginsPattern": { - "regexp": "(.*?)" + "regexp": "Changes detected" }, "endsPattern": { - "regexp": "bundle generation complete" + "regexp": "bundle generation (complete|failed)" } } } @@ -30,10 +30,10 @@ "background": { "activeOnStart": true, "beginsPattern": { - "regexp": "(.*?)" + "regexp": "Changes detected" }, "endsPattern": { - "regexp": "bundle generation complete" + "regexp": "bundle generation (complete|failed)" } } } diff --git a/packages/igx-templates/igx-ts/projects/_base/files/angular.json b/packages/igx-templates/igx-ts/projects/_base/files/angular.json index 1e135753a1..d4c782922a 100644 --- a/packages/igx-templates/igx-ts/projects/_base/files/angular.json +++ b/packages/igx-templates/igx-ts/projects/_base/files/angular.json @@ -3,7 +3,9 @@ "version": 1, "cli": { "packageManager": "npm", - "schematicCollections": ["angular-eslint"] + "schematicCollections": [ + "angular-eslint" + ] }, "newProjectRoot": "projects", "projects": { @@ -21,18 +23,26 @@ "build": { "builder": "@angular/build:application", "options": { - "outputPath": "dist/<%=dashName%>", - "index": "src/index.html", "browser": "src/main.ts", - "polyfills": ["zone.js"], + "polyfills": [ + "zone.js" + ], "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", - "assets": ["src/favicon.ico", "src/assets"], + "assets": [ + { + "glob": "**/*", + "input": "public" + }, + "src/assets" + ], "styles": [ "src/styles.scss"<%=DefaultTheme%> ], "stylePreprocessorOptions": { - "includePaths": ["node_modules"] + "includePaths": [ + "node_modules" + ] } }, "configurations": { @@ -77,12 +87,6 @@ }, "defaultConfiguration": "development" }, - "extract-i18n": { - "builder": "@angular/build:extract-i18n", - "options": { - "buildTarget": "<%=dashName%>:build" - } - }, "test": { "builder": "@angular/build:unit-test", "options": { @@ -92,7 +96,16 @@ "lint": { "builder": "@angular-eslint/builder:lint", "options": { - "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"] + "lintFilePatterns": [ + "src/**/*.ts", + "src/**/*.html" + ] + } + }, + "extract-i18n": { + "builder": "@angular/build:extract-i18n", + "options": { + "buildTarget": "<%=dashName%>:build" } } } diff --git a/packages/igx-templates/igx-ts/projects/_base/files/package.json b/packages/igx-templates/igx-ts/projects/_base/files/package.json index eb28a50449..cc4bac66db 100644 --- a/packages/igx-templates/igx-ts/projects/_base/files/package.json +++ b/packages/igx-templates/igx-ts/projects/_base/files/package.json @@ -17,10 +17,10 @@ "@angular/core": "~22.2.0", "@angular/forms": "~22.2.0", "@angular/platform-browser": "~22.2.0", - "@angular/platform-browser-dynamic": "~22.2.0", "@angular/router": "~22.2.0", "@igniteui/material-icons-extended": "^3.0.2", "igniteui-angular": "~22.2.0", + "igniteui-i18n-core": "^1.0.5", "minireset.css": "~0.0.7", "rxjs": "~7.8.1", "tslib": "~2.8.1", @@ -31,14 +31,14 @@ "@angular/cli": "~22.2.0", "@angular/compiler-cli": "~22.2.0", "@eslint/js": "^10.0.1", - "@vitest/browser-playwright": "^4.1.0", - "angular-eslint": "~22.0.0", + "@vitest/browser-playwright": "^5.0.0", + "angular-eslint": "~22.6.0", "eslint": "^10.3.0", "igniteui-cli": "~<%=cliVersion%>", "playwright": "^1.58.2", "prettier": "^3.8.1", "typescript": "~6.0.2", - "typescript-eslint": "8.60.1", - "vitest": "^4.1.0" + "typescript-eslint": "8.71.0", + "vitest": "^5.0.0" } } diff --git a/packages/igx-templates/igx-ts/projects/_base/files/src/main.ts b/packages/igx-templates/igx-ts/projects/_base/files/src/main.ts index 40016cf7ca..5df75f9c83 100644 --- a/packages/igx-templates/igx-ts/projects/_base/files/src/main.ts +++ b/packages/igx-templates/igx-ts/projects/_base/files/src/main.ts @@ -1,6 +1,6 @@ import { bootstrapApplication } from '@angular/platform-browser'; -import { App } from './app/app'; import { appConfig } from './app/app.config'; +import { App } from './app/app'; bootstrapApplication(App, appConfig) .catch((err) => console.error(err)); diff --git a/packages/igx-templates/igx-ts/projects/_base/files/tsconfig.app.json b/packages/igx-templates/igx-ts/projects/_base/files/tsconfig.app.json index 264f459bf8..cb151e1016 100644 --- a/packages/igx-templates/igx-ts/projects/_base/files/tsconfig.app.json +++ b/packages/igx-templates/igx-ts/projects/_base/files/tsconfig.app.json @@ -3,7 +3,6 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "./out-tsc/app", "types": [] }, "include": [ diff --git a/packages/igx-templates/igx-ts/projects/_base/files/tsconfig.json b/packages/igx-templates/igx-ts/projects/_base/files/tsconfig.json index 07c56f68a5..d2fbb9c2b0 100644 --- a/packages/igx-templates/igx-ts/projects/_base/files/tsconfig.json +++ b/packages/igx-templates/igx-ts/projects/_base/files/tsconfig.json @@ -3,7 +3,6 @@ { "compileOnSave": false, "compilerOptions": { - "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, @@ -13,14 +12,12 @@ "experimentalDecorators": true, "importHelpers": true, "target": "ES2022", - "module": "preserve", - "moduleResolution": "Bundler" + "module": "preserve" }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, - "strictInputAccessModifiers": true, - "strictTemplates": true + "strictInputAccessModifiers": true }, "files": [], "references": [ diff --git a/packages/igx-templates/igx-ts/projects/_base/files/tsconfig.spec.json b/packages/igx-templates/igx-ts/projects/_base/files/tsconfig.spec.json index d38370633f..9c8efb9b70 100644 --- a/packages/igx-templates/igx-ts/projects/_base/files/tsconfig.spec.json +++ b/packages/igx-templates/igx-ts/projects/_base/files/tsconfig.spec.json @@ -3,7 +3,6 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "outDir": "./out-tsc/spec", "types": [ "vitest/globals" ] diff --git a/packages/igx-templates/igx-ts/projects/_base_with_home/files/src/favicon.ico b/packages/igx-templates/igx-ts/projects/_base_with_home/files/src/favicon.ico deleted file mode 100644 index 57614f9c967596fad0a3989bec2b1deff33034f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15086 zcmd^G33O9Omi+`8$@{|M-I6TH3wzF-p5CV8o}7f~KxR60LK+ApEFB<$bcciv%@SmA zV{n>g85YMFFeU*Uvl=i4v)C*qgnb;$GQ=3XTe9{Y%c`mO%su)noNCCQ*@t1WXn|B(hQ7i~ zrUK8|pUkD6#lNo!bt$6)jR!&C?`P5G(`e((P($RaLeq+o0Vd~f11;qB05kdbAOm?r zXv~GYr_sibQO9NGTCdT;+G(!{4Xs@4fPak8#L8PjgJwcs-Mm#nR_Z0s&u?nDX5^~@ z+A6?}g0|=4e_LoE69pPFO`yCD@BCjgKpzMH0O4Xs{Ahc?K3HC5;l=f zg>}alhBXX&);z$E-wai+9TTRtBX-bWYY@cl$@YN#gMd~tM_5lj6W%8ah4;uZ;jP@Q zVbuel1rPA?2@x9Y+u?e`l{Z4ngfG5q5BLH5QsEu4GVpt{KIp1?U)=3+KQ;%7ec8l* zdV=zZgN5>O3G(3L2fqj3;oBbZZw$Ij@`Juz@?+yy#OPw)>#wsTewVgTK9BGt5AbZ&?K&B3GVF&yu?@(Xj3fR3n+ZP0%+wo)D9_xp>Z$`A4 zfV>}NWjO#3lqumR0`gvnffd9Ka}JJMuHS&|55-*mCD#8e^anA<+sFZVaJe7{=p*oX zE_Uv?1>e~ga=seYzh{9P+n5<+7&9}&(kwqSaz;1aD|YM3HBiy<))4~QJSIryyqp| z8nGc(8>3(_nEI4n)n7j(&d4idW1tVLjZ7QbNLXg;LB ziHsS5pXHEjGJZb59KcvS~wv;uZR-+4qEqow`;JCfB*+b^UL^3!?;-^F%yt=VjU|v z39SSqKcRu_NVvz!zJzL0CceJaS6%!(eMshPv_0U5G`~!a#I$qI5Ic(>IONej@aH=f z)($TAT#1I{iCS4f{D2+ApS=$3E7}5=+y(rA9mM#;Cky%b*Gi0KfFA`ofKTzu`AV-9 znW|y@19rrZ*!N2AvDi<_ZeR3O2R{#dh1#3-d%$k${Rx42h+i&GZo5!C^dSL34*AKp z27mTd>k>?V&X;Nl%GZ(>0s`1UN~Hfyj>KPjtnc|)xM@{H_B9rNr~LuH`Gr5_am&Ep zTjZA8hljNj5H1Ipm-uD9rC}U{-vR!eay5&6x6FkfupdpT*84MVwGpdd(}ib)zZ3Ky z7C$pnjc82(W_y_F{PhYj?o!@3__UUvpX)v69aBSzYj3 zdi}YQkKs^SyXyFG2LTRz9{(w}y~!`{EuAaUr6G1M{*%c+kP1olW9z23dSH!G4_HSK zzae-DF$OGR{ofP*!$a(r^5Go>I3SObVI6FLY)N@o<*gl0&kLo-OT{Tl*7nCz>Iq=? zcigIDHtj|H;6sR?or8Wd_a4996GI*CXGU}o;D9`^FM!AT1pBY~?|4h^61BY#_yIfO zKO?E0 zJ{Pc`9rVEI&$xxXu`<5E)&+m(7zX^v0rqofLs&bnQT(1baQkAr^kEsk)15vlzAZ-l z@OO9RF<+IiJ*O@HE256gCt!bF=NM*vh|WVWmjVawcNoksRTMvR03H{p@cjwKh(CL4 z7_PB(dM=kO)!s4fW!1p0f93YN@?ZSG` z$B!JaAJCtW$B97}HNO9(x-t30&E}Mo1UPi@Av%uHj~?T|!4JLwV;KCx8xO#b9IlUW zI6+{a@Wj|<2Y=U;a@vXbxqZNngH8^}LleE_4*0&O7#3iGxfJ%Id>+sb;7{L=aIic8 z|EW|{{S)J-wr@;3PmlxRXU8!e2gm_%s|ReH!reFcY8%$Hl4M5>;6^UDUUae?kOy#h zk~6Ee_@ZAn48Bab__^bNmQ~+k=02jz)e0d9Z3>G?RGG!65?d1>9}7iG17?P*=GUV-#SbLRw)Hu{zx*azHxWkGNTWl@HeWjA?39Ia|sCi{e;!^`1Oec zb>Z|b65OM*;eC=ZLSy?_fg$&^2xI>qSLA2G*$nA3GEnp3$N-)46`|36m*sc#4%C|h zBN<2U;7k>&G_wL4=Ve5z`ubVD&*Hxi)r@{4RCDw7U_D`lbC(9&pG5C*z#W>8>HU)h z!h3g?2UL&sS!oY5$3?VlA0Me9W5e~V;2jds*fz^updz#AJ%G8w2V}AEE?E^=MK%Xt z__Bx1cr7+DQmuHmzn*|hh%~eEc9@m05@clWfpEFcr+06%0&dZJH&@8^&@*$qR@}o3 z@Tuuh2FsLz^zH+dN&T&?0G3I?MpmYJ;GP$J!EzjeM#YLJ!W$}MVNb0^HfOA>5Fe~UNn%Zk(PT@~9}1dt)1UQ zU*B5K?Dl#G74qmg|2>^>0WtLX#Jz{lO4NT`NYB*(L#D|5IpXr9v&7a@YsGp3vLR7L zHYGHZg7{ie6n~2p$6Yz>=^cEg7tEgk-1YRl%-s7^cbqFb(U7&Dp78+&ut5!Tn(hER z|Gp4Ed@CnOPeAe|N>U(dB;SZ?NU^AzoD^UAH_vamp6Ws}{|mSq`^+VP1g~2B{%N-!mWz<`)G)>V-<`9`L4?3dM%Qh6<@kba+m`JS{Ya@9Fq*m6$$ zA1%Ogc~VRH33|S9l%CNb4zM%k^EIpqY}@h{w(aBcJ9c05oiZx#SK9t->5lSI`=&l~ z+-Ic)a{FbBhXV$Xt!WRd`R#Jk-$+_Z52rS>?Vpt2IK<84|E-SBEoIw>cs=a{BlQ7O z-?{Fy_M&84&9|KM5wt~)*!~i~E=(6m8(uCO)I=)M?)&sRbzH$9Rovzd?ZEY}GqX+~ zFbEbLz`BZ49=2Yh-|<`waK-_4!7`ro@zlC|r&I4fc4oyb+m=|c8)8%tZ-z5FwhzDt zL5kB@u53`d@%nHl0Sp)Dw`(QU&>vujEn?GPEXUW!Wi<+4e%BORl&BIH+SwRcbS}X@ z01Pk|vA%OdJKAs17zSXtO55k!;%m9>1eW9LnyAX4uj7@${O6cfii`49qTNItzny5J zH&Gj`e}o}?xjQ}r?LrI%FjUd@xflT3|7LA|ka%Q3i}a8gVm<`HIWoJGH=$EGClX^C0lysQJ>UO(q&;`T#8txuoQ_{l^kEV9CAdXuU1Ghg8 zN_6hHFuy&1x24q5-(Z7;!poYdt*`UTdrQOIQ!2O7_+AHV2hgXaEz7)>$LEdG z<8vE^Tw$|YwZHZDPM!SNOAWG$?J)MdmEk{U!!$M#fp7*Wo}jJ$Q(=8>R`Ats?e|VU?Zt7Cdh%AdnfyN3MBWw{ z$OnREvPf7%z6`#2##_7id|H%Y{vV^vWXb?5d5?a_y&t3@p9t$ncHj-NBdo&X{wrfJ zamN)VMYROYh_SvjJ=Xd!Ga?PY_$;*L=SxFte!4O6%0HEh%iZ4=gvns7IWIyJHa|hT z2;1+e)`TvbNb3-0z&DD_)Jomsg-7p_Uh`wjGnU1urmv1_oVqRg#=C?e?!7DgtqojU zWoAB($&53;TsXu^@2;8M`#z{=rPy?JqgYM0CDf4v@z=ZD|ItJ&8%_7A#K?S{wjxgd z?xA6JdJojrWpB7fr2p_MSsU4(R7=XGS0+Eg#xR=j>`H@R9{XjwBmqAiOxOL` zt?XK-iTEOWV}f>Pz3H-s*>W z4~8C&Xq25UQ^xH6H9kY_RM1$ch+%YLF72AA7^b{~VNTG}Tj#qZltz5Q=qxR`&oIlW Nr__JTFzvMr^FKp4S3v*( diff --git a/packages/igx-templates/igx-ts/projects/side-nav-auth/files/package.json b/packages/igx-templates/igx-ts/projects/side-nav-auth/files/package.json index a45da6dbc4..8230742783 100644 --- a/packages/igx-templates/igx-ts/projects/side-nav-auth/files/package.json +++ b/packages/igx-templates/igx-ts/projects/side-nav-auth/files/package.json @@ -17,10 +17,11 @@ "@angular/core": "~22.2.0", "@angular/forms": "~22.2.0", "@angular/platform-browser": "~22.2.0", - "@angular/platform-browser-dynamic": "~22.2.0", "@angular/router": "~22.2.0", - "angular-auth-oidc-client": "~22.2.0", + "@igniteui/material-icons-extended": "^3.0.2", + "angular-auth-oidc-client": "~22.0.2", "igniteui-angular": "~22.2.0", + "igniteui-i18n-core": "^1.0.5", "minireset.css": "~0.0.7", "rxjs": "~7.8.1", "tslib": "~2.8.1", @@ -33,14 +34,14 @@ "@angular/language-service": "~22.2.0", "@eslint/js": "^10.0.1", "@types/facebook-js-sdk": "~3.3.6", - "@vitest/browser-playwright": "^4.1.0", - "angular-eslint": "~22.0.0", + "@vitest/browser-playwright": "^5.0.0", + "angular-eslint": "~22.6.0", "eslint": "^10.3.0", "igniteui-cli": "~<%=cliVersion%>", "playwright": "^1.58.2", "prettier": "^3.8.1", "typescript": "~6.0.2", - "typescript-eslint": "8.60.1", - "vitest": "^4.1.0" + "typescript-eslint": "8.71.0", + "vitest": "^5.0.0" } } diff --git a/scripts/generate-angular-base.mjs b/scripts/generate-angular-base.mjs new file mode 100644 index 0000000000..a22fa94910 --- /dev/null +++ b/scripts/generate-angular-base.mjs @@ -0,0 +1,296 @@ +// Regenerates the Angular-owned files of the igx-ts project templates from a fresh `ng new`, +// followed by `ng add angular-eslint` and `ng add igniteui-angular`. +// +// node scripts/generate-angular-base.mjs [--angular ] [--igniteui ] [--check] +// +// --angular @angular/cli range for `ng new`; defaults to the `@angular/cli` pin in `_base/files/package.json` +// --igniteui igniteui-angular range for `ng add`; defaults to the `igniteui-angular` pin in `_base/files/package.json` +// --check writes nothing; lists what a run would change and exits with 1 if anything would +// +// COPIED files are taken as they are; angular.json and package.json are patched with our deviations, +// and side-nav-auth's package.json is the `_base` one plus the packages only that project uses. +// The other generated files have hand-maintained counterparts. Their upstream content is fingerprinted +// in UPSTREAM_HASHES, so a run lists the ones `ng new` or `ng add` changed since the last run. + +import { execSync } from "node:child_process"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import { createRequire } from "node:module"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const ROOT = path.resolve(fileURLToPath(import.meta.url), "../.."); +const PROJECTS = path.join(ROOT, "packages/igx-templates/igx-ts/projects"); +const BASE = path.join(PROJECTS, "_base/files"); +const SIDE_NAV_AUTH = path.join(PROJECTS, "side-nav-auth/files"); +const UPSTREAM_HASHES = path.join(ROOT, "scripts/generate-angular-base.upstream.json"); +const NAME = "igx-base-placeholder"; +const DASH_NAME = "<%=dashName%>"; +const USAGE = "node scripts/generate-angular-base.mjs [--angular ] [--igniteui ] [--check]"; + +const COPIED = [ + ".editorconfig", + ".gitignore", + ".prettierrc", + ".vscode/extensions.json", + ".vscode/launch.json", + ".vscode/tasks.json", + "public/favicon.ico", + "src/main.ts", + "tsconfig.json", + "tsconfig.app.json", + "tsconfig.spec.json" +]; +const PATCHED = ["angular.json", "package.json"]; +const IGNORED = [ + "node_modules", ".angular", "package-lock.json", + // AI config from @igniteui/angular-schematics; `ig ai-config` owns it in our projects + ".agents", ".claude", ".mcp.json", "AGENTS.md" +]; + +// Packages we add on top of `ng new` and `ng add`, at their version in the current `_base` package.json +const EXTRAS = { + dependencies: ["@angular/animations"], // provideAnimations in the app configs + devDependencies: ["igniteui-cli", "playwright"] // playwright: the browser for @vitest/browser-playwright +}; +const DEPENDENCY_KEYS = ["dependencies", "devDependencies"]; + +function patchAngularJson(json) { + const architect = json.projects[DASH_NAME].architect; + const build = architect.build; + // `public/` from `ng new` is served at the root; overlays keep their files in src/assets + build.options.assets.push("src/assets"); + build.configurations.production.budgets = [ + { type: "initial", maximumWarning: "5mb", maximumError: "40mb" }, + { type: "anyComponentStyle", maximumWarning: "8kB", maximumError: "2mb" } + ]; + build.configurations.development.fileReplacements = [{ + replace: "src/environments/environment.ts", + with: "src/environments/environment.development.ts" + }]; + architect.test.options = { ...architect.test.options, runnerConfig: true }; + architect["extract-i18n"] = { + builder: "@angular/build:extract-i18n", + options: { buildTarget: `${DASH_NAME}:build` } + }; + + // the default theme is appended to the styles array by the template engine + const styles = `"src/styles.scss"`; + const text = toJson(json); + if (text.split(styles).length !== 2) { + throw new Error(`Expected exactly one ${styles} entry in angular.json`); + } + return text.replace(styles, `${styles}<%=DefaultTheme%>`); +} + +function patchPackageJson(pkg, { current, igniteuiVersion, installedVersion, semver }) { + const { dependencies: deps, devDependencies: devDeps } = pkg; + delete pkg.packageManager; // records the npm version of the machine that ran `ng new` + Object.assign(pkg.scripts, { start: "ng serve -o", test: "ng test --browsers=chromium", lint: "ng lint" }); + delete devDeps.jsdom; // tests run in a real browser through @vitest/browser-playwright + // `ig new` projects use igniteui-cli instead; the schematics flow swaps it back (app-projects) + delete devDeps["@igniteui/angular-schematics"]; + + // a package already in `_base` keeps its pin while that admits the version installed in the generated + // project, so a run moves only the pins the new project needs moved and never loosens the others + for (const key of DEPENDENCY_KEYS) { + for (const name of Object.keys(pkg[key])) { + const pin = current[key][name]; + if (pin && semver.satisfies(installedVersion(name), pin)) { + pkg[key][name] = pin; + } + } + } + deps["igniteui-angular"] = igniteuiVersion; + + for (const [key, names] of Object.entries(EXTRAS)) { + for (const name of names) { + if (!current[key][name]) { + throw new Error(`${name} is missing from the current _base package.json`); + } + pkg[key][name] = current[key][name]; + } + } + + // all @angular/* packages share the @angular/core version, on a ~ range + const angularVersion = deps["@angular/core"].replace(/^\^/, "~"); + for (const key of DEPENDENCY_KEYS) { + for (const name of Object.keys(pkg[key]).filter(n => n.startsWith("@angular/"))) { + pkg[key][name] = angularVersion; + } + } + // `ng add angular-eslint` writes its exact version; it follows Angular minors + const eslint = semver.minVersion(devDeps["angular-eslint"]); + devDeps["angular-eslint"] = `~${eslint.major}.${eslint.minor}.0`; + // the browser provider must match vitest exactly + devDeps["@vitest/browser-playwright"] = devDeps.vitest; + return sortDependencies(pkg); +} + +// side-nav-auth carries its own copy of the manifest: the generated `_base` one plus the packages only it uses +function sideNavAuthPackageJson(base, currentBase, currentOwn) { + const pkg = structuredClone(base); + for (const key of DEPENDENCY_KEYS) { + for (const [name, range] of Object.entries(currentOwn[key])) { + if (!(name in currentBase[key])) { + pkg[key][name] = name.startsWith("@angular/") ? base.dependencies["@angular/core"] : range; + } + } + } + return sortDependencies(pkg); +} + +function sortDependencies(pkg) { + for (const key of DEPENDENCY_KEYS) { + pkg[key] = Object.fromEntries(Object.entries(pkg[key]).sort(([a], [b]) => a.localeCompare(b))); + } + return pkg; +} + +function toJson(value) { + return JSON.stringify(value, null, 2) + "\n"; +} + +function readJson(file) { + return JSON.parse(fs.readFileSync(file, "utf8")); +} + +// dotfiles are stored as `__dot__name`, except those already kept under their own name +function templatePath(root, file) { + const literal = path.join(root, file); + return fs.existsSync(literal) + ? literal + : path.join(root, ...file.split("/").map(part => part.replace(/^\./, "__dot__"))); +} + +// writes the outputs that differ from the template files (none with `check`) and returns their paths; +// text keeps the line endings of the file it replaces, so only real changes show in the diff +function writeOutputs(outputs, check) { + const changed = []; + for (const [target, content] of outputs) { + const existing = fs.existsSync(target) ? fs.readFileSync(target) : null; + const crlf = typeof content === "string" && existing?.includes("\r\n"); + const data = Buffer.from(crlf ? content.replace(/\n/g, "\r\n") : content); + if (existing?.equals(data)) { + continue; + } + changed.push(path.relative(ROOT, target)); + if (!check) { + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.writeFileSync(target, data); + } + } + return changed; +} + +function listFiles(dir, prefix = "") { + return fs.readdirSync(dir, { withFileTypes: true }) + .filter(entry => !IGNORED.includes(entry.name)) + .flatMap(entry => entry.isDirectory() + ? listFiles(path.join(dir, entry.name), `${prefix}${entry.name}/`) + : [`${prefix}${entry.name}`]); +} + +// lists the upstream changes, with a kept copy of each new upstream version to compare against ours +function reportUpstreamChanges(files, hashes, read) { + const kept = fs.mkdtempSync(path.join(os.tmpdir(), "igx-base-upstream-")); + console.log("\nChanged upstream since the last run; review our hand-maintained counterparts:"); + for (const file of files) { + if (!(file in hashes)) { + console.log(` ${file}: no longer generated`); + continue; + } + const copy = path.join(kept, file); + fs.mkdirSync(path.dirname(copy), { recursive: true }); + fs.writeFileSync(copy, read(file)); + const ours = templatePath(BASE, file); + console.log(` ${file}: upstream ${copy}, ours ${fs.existsSync(ours) ? path.relative(ROOT, ours) : "(none in _base)"}`); + } +} + +function parseArgs(argv) { + const args = { check: false }; + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + const value = argv[i + 1]; + if (arg === "--check") { + args.check = true; + } else if ((arg === "--angular" || arg === "--igniteui") && value && !value.startsWith("-")) { + args[arg.slice(2)] = value; + i++; + } else { + throw new Error(`Unknown argument or missing value: ${arg}\n${USAGE}`); + } + } + return args; +} + +const args = parseArgs(process.argv.slice(2)); +const currentPackage = readJson(templatePath(BASE, "package.json")); +const angularVersion = args.angular || currentPackage.devDependencies["@angular/cli"]; +const igniteuiVersion = args.igniteui || currentPackage.dependencies["igniteui-angular"]; +const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "igx-base-")); + +try { + const generated = path.join(tmp, NAME); + const read = file => fs.readFileSync(path.join(generated, file), "utf8").replace(/\r\n/g, "\n").split(NAME).join(DASH_NAME); + const ng = (command, cwd) => execSync(`npx -y "@angular/cli@${angularVersion}" ${command} --defaults --interactive=false`, + { cwd, stdio: "inherit" }); + + console.log(`Running ng new with @angular/cli@${angularVersion} in ${tmp}`); + // zone.js stays until the templates are verified zoneless + ng(`new ${NAME} --style=scss --routing --ssr=false --zoneless=false --ai-config=none --skip-install --skip-git`, tmp); + const ngNewPackage = JSON.parse(read("package.json")); + // angular-eslint majors follow Angular's; its ng-add writes the newest release of that major + const angularMajor = ngNewPackage.dependencies["@angular/core"].match(/\d+/)[0]; + for (const pkg of [`angular-eslint@${angularMajor}`, `igniteui-angular@${igniteuiVersion}`]) { + console.log(`Running ng add ${pkg}`); + ng(`add "${pkg}" --skip-confirmation`, generated); + } + + const basePackage = patchPackageJson(JSON.parse(read("package.json")), { + current: currentPackage, + igniteuiVersion, + installedVersion: name => readJson(path.join(generated, "node_modules", name, "package.json")).version, + semver: createRequire(path.join(generated, "package.json"))("semver") // comes with the Angular CLI + }); + const sideNavAuthPackage = sideNavAuthPackageJson(basePackage, currentPackage, + readJson(templatePath(SIDE_NAV_AUTH, "package.json"))); + const changed = writeOutputs(new Map([ + ...COPIED.map(file => [ + templatePath(BASE, file), + file.endsWith(".ico") ? fs.readFileSync(path.join(generated, file)) : read(file) + ]), + [templatePath(BASE, "angular.json"), patchAngularJson(JSON.parse(read("angular.json")))], + [templatePath(BASE, "package.json"), toJson(basePackage)], + [templatePath(SIDE_NAV_AUTH, "package.json"), toJson(sideNavAuthPackage)] + ]), args.check); + + // the other generated files are kept by hand: fingerprint them to spot upstream changes + const hashes = Object.fromEntries(listFiles(generated) + .filter(file => !COPIED.includes(file) && !PATCHED.includes(file)) + .sort() + .map(file => [file, crypto.createHash("sha256").update(read(file)).digest("hex")])); + const previous = fs.existsSync(UPSTREAM_HASHES) ? readJson(UPSTREAM_HASHES) : null; + const upstreamChanged = previous + ? [...new Set([...Object.keys(previous), ...Object.keys(hashes)])].filter(file => previous[file] !== hashes[file]) + : []; + + console.log(changed.length + ? `\n${args.check ? "Would change" : "Updated"}:\n${changed.map(file => ` ${file}`).join("\n")}` + : "\nGenerated files are up to date."); + if (!previous) { + console.log(`\nNo ${path.relative(ROOT, UPSTREAM_HASHES)} yet; this run records the upstream baseline.`); + } else if (upstreamChanged.length) { + reportUpstreamChanges(upstreamChanged, hashes, read); + } + + if (args.check) { + process.exitCode = changed.length || upstreamChanged.length ? 1 : 0; + } else { + fs.writeFileSync(UPSTREAM_HASHES, toJson(hashes)); + } +} finally { + fs.rmSync(tmp, { recursive: true, force: true }); +} diff --git a/scripts/generate-angular-base.upstream.json b/scripts/generate-angular-base.upstream.json new file mode 100644 index 0000000000..aabba60626 --- /dev/null +++ b/scripts/generate-angular-base.upstream.json @@ -0,0 +1,13 @@ +{ + "README.md": "65b7d752f4e2c89781200157db5ddca8914bf8c41cc63d293bb6a5975939a746", + "eslint.config.js": "c92a49968fab95fd7ea538ae9e7501567682fd3cb4c202775ef5a7ed2e49a4a7", + "ignite-ui-cli.json": "457cc9ac4b75c5af8d2c76e4f01305eda83c28fc69dc7bae4a8c4570a461d1af", + "src/app/app.config.ts": "c2292f722fbf7d2c85b56115e1d5d3c21eb8e0587704b0994ae8ed394250187a", + "src/app/app.html": "4d90c16abd4ac0ea5c45e5b3a2835d966ac5c2e4cc6b60131003452e6e4fc328", + "src/app/app.routes.ts": "e2ef74b4c1f7f52a30a417c6808f01b2a29e69bb0d20302527ae5001607f7180", + "src/app/app.scss": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "src/app/app.spec.ts": "e7a2cfdab258441ef2a2811b96acb881fadbd1107b510eddbd566ab86cc4e832", + "src/app/app.ts": "fb4d23cefe9231aff51d721cbfbf0b6a169c6176a7f79405bb1a492b5078f263", + "src/index.html": "f9ac5fbba4459f93c098aee211803f0fd925f926ee92bbb19d06db07e883e5fe", + "src/styles.scss": "584decca418c77fc010012616fa56c76d3db2ee17dc8d67fd16e197f859c7ce8" +} From 383e34913106272907d070730b2dce48b90fc8bd Mon Sep 17 00:00:00 2001 From: Marina Stoyanova Date: Wed, 21 Oct 2026 15:55:07 +0300 Subject: [PATCH 02/10] Update exit code logic in generate-angular-base.mjs Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- scripts/generate-angular-base.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/generate-angular-base.mjs b/scripts/generate-angular-base.mjs index a22fa94910..569ccacf23 100644 --- a/scripts/generate-angular-base.mjs +++ b/scripts/generate-angular-base.mjs @@ -287,7 +287,7 @@ try { } if (args.check) { - process.exitCode = changed.length || upstreamChanged.length ? 1 : 0; + process.exitCode = !previous || changed.length || upstreamChanged.length ? 1 : 0; } else { fs.writeFileSync(UPSTREAM_HASHES, toJson(hashes)); } From 3284206c123e0c3c648d8d65e9aa3c475e7db347 Mon Sep 17 00:00:00 2001 From: mstoyanova Date: Wed, 21 Oct 2026 16:14:30 +0300 Subject: [PATCH 03/10] fix: update fingerprinting logic and improve output handling in Angular base generation --- scripts/generate-angular-base.mjs | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/scripts/generate-angular-base.mjs b/scripts/generate-angular-base.mjs index a22fa94910..29726effa5 100644 --- a/scripts/generate-angular-base.mjs +++ b/scripts/generate-angular-base.mjs @@ -257,15 +257,6 @@ try { }); const sideNavAuthPackage = sideNavAuthPackageJson(basePackage, currentPackage, readJson(templatePath(SIDE_NAV_AUTH, "package.json"))); - const changed = writeOutputs(new Map([ - ...COPIED.map(file => [ - templatePath(BASE, file), - file.endsWith(".ico") ? fs.readFileSync(path.join(generated, file)) : read(file) - ]), - [templatePath(BASE, "angular.json"), patchAngularJson(JSON.parse(read("angular.json")))], - [templatePath(BASE, "package.json"), toJson(basePackage)], - [templatePath(SIDE_NAV_AUTH, "package.json"), toJson(sideNavAuthPackage)] - ]), args.check); // the other generated files are kept by hand: fingerprint them to spot upstream changes const hashes = Object.fromEntries(listFiles(generated) @@ -277,19 +268,29 @@ try { ? [...new Set([...Object.keys(previous), ...Object.keys(hashes)])].filter(file => previous[file] !== hashes[file]) : []; + // the fingerprints are an output like the templates, so a missing or outdated baseline fails --check + const changed = writeOutputs(new Map([ + ...COPIED.map(file => [ + templatePath(BASE, file), + file.endsWith(".ico") ? fs.readFileSync(path.join(generated, file)) : read(file) + ]), + [templatePath(BASE, "angular.json"), patchAngularJson(JSON.parse(read("angular.json")))], + [templatePath(BASE, "package.json"), toJson(basePackage)], + [templatePath(SIDE_NAV_AUTH, "package.json"), toJson(sideNavAuthPackage)], + [UPSTREAM_HASHES, toJson(hashes)] + ]), args.check); + console.log(changed.length ? `\n${args.check ? "Would change" : "Updated"}:\n${changed.map(file => ` ${file}`).join("\n")}` : "\nGenerated files are up to date."); if (!previous) { - console.log(`\nNo ${path.relative(ROOT, UPSTREAM_HASHES)} yet; this run records the upstream baseline.`); + console.log(`\nNo ${path.relative(ROOT, UPSTREAM_HASHES)} yet; a run ${args.check ? "would record" : "records"} the upstream baseline.`); } else if (upstreamChanged.length) { reportUpstreamChanges(upstreamChanged, hashes, read); } - if (args.check) { - process.exitCode = changed.length || upstreamChanged.length ? 1 : 0; - } else { - fs.writeFileSync(UPSTREAM_HASHES, toJson(hashes)); + if (args.check && changed.length) { + process.exitCode = 1; } } finally { fs.rmSync(tmp, { recursive: true, force: true }); From 47bed8a3b7148a2d5b61ed6ecd14362afaf0c3f1 Mon Sep 17 00:00:00 2001 From: mstoyanova Date: Wed, 21 Oct 2026 16:57:19 +0300 Subject: [PATCH 04/10] fix: normalize angular-eslint version in package.json patching --- scripts/generate-angular-base.mjs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/generate-angular-base.mjs b/scripts/generate-angular-base.mjs index 11a5e6d53d..1c27c9eccc 100644 --- a/scripts/generate-angular-base.mjs +++ b/scripts/generate-angular-base.mjs @@ -91,6 +91,10 @@ function patchPackageJson(pkg, { current, igniteuiVersion, installedVersion, sem delete devDeps.jsdom; // tests run in a real browser through @vitest/browser-playwright // `ig new` projects use igniteui-cli instead; the schematics flow swaps it back (app-projects) delete devDeps["@igniteui/angular-schematics"]; + // `ng add angular-eslint` writes its exact version; it follows Angular minors. + // Normalized before the pins below are kept, so a retained pin is never rewritten + const eslint = semver.minVersion(devDeps["angular-eslint"]); + devDeps["angular-eslint"] = `~${eslint.major}.${eslint.minor}.0`; // a package already in `_base` keeps its pin while that admits the version installed in the generated // project, so a run moves only the pins the new project needs moved and never loosens the others @@ -120,9 +124,6 @@ function patchPackageJson(pkg, { current, igniteuiVersion, installedVersion, sem pkg[key][name] = angularVersion; } } - // `ng add angular-eslint` writes its exact version; it follows Angular minors - const eslint = semver.minVersion(devDeps["angular-eslint"]); - devDeps["angular-eslint"] = `~${eslint.major}.${eslint.minor}.0`; // the browser provider must match vitest exactly devDeps["@vitest/browser-playwright"] = devDeps.vitest; return sortDependencies(pkg); From 5e9e6aeb918cc41b3b5b1191e7959892da6345d9 Mon Sep 17 00:00:00 2001 From: Marina Stoyanova Date: Thu, 22 Oct 2026 10:13:20 +0300 Subject: [PATCH 05/10] Change package manager to npm in ng new command Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- scripts/generate-angular-base.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/generate-angular-base.mjs b/scripts/generate-angular-base.mjs index 1c27c9eccc..452f285b43 100644 --- a/scripts/generate-angular-base.mjs +++ b/scripts/generate-angular-base.mjs @@ -241,7 +241,7 @@ try { console.log(`Running ng new with @angular/cli@${angularVersion} in ${tmp}`); // zone.js stays until the templates are verified zoneless - ng(`new ${NAME} --style=scss --routing --ssr=false --zoneless=false --ai-config=none --skip-install --skip-git`, tmp); + ng(`new ${NAME} --style=scss --routing --ssr=false --zoneless=false --ai-config=none --package-manager=npm --skip-install --skip-git`, tmp); const ngNewPackage = JSON.parse(read("package.json")); // angular-eslint majors follow Angular's; its ng-add writes the newest release of that major const angularMajor = ngNewPackage.dependencies["@angular/core"].match(/\d+/)[0]; From a158758b9bf844202eca2811708490297fa388f7 Mon Sep 17 00:00:00 2001 From: mstoyanova Date: Fri, 23 Oct 2026 15:46:55 +0300 Subject: [PATCH 06/10] refactor: remove side-nav-auth project files and update related templates - Deleted package.json, tsconfig.app.json, and tsconfig.spec.json from side-nav-auth project. - Updated index.ts files in side-nav-auth and side-nav-mini-auth to share authentication extras. - Removed ng-schematics app-projects related files and schema. - Updated ng-new schematic to streamline project creation and remove unnecessary complexity. - Removed generate-angular-base script and upstream tracking for Angular base files. - Updated versionScript to utilize constants for igniteui-angular package versioning. --- .agents/README.md | 3 +- .../skills/igniteui-cli-smoke-test/SKILL.md | 11 +- .../igniteui-cli-template-anatomy/SKILL.md | 30 +- .../SKILL.md | 9 +- AGENTS.md | 4 +- packages/cli/lib/commands/new.ts | 35 +- .../src/tools/setup_angular.md | 2 +- .../igx-ts/projects/_base/files/.prettierrc | 12 - .../projects/_base/files/__dot__editorconfig | 17 - .../projects/_base/files/__dot__gitignore | 44 --- .../_base/files/__dot__vscode/extensions.json | 4 - .../_base/files/__dot__vscode/launch.json | 20 -- .../_base/files/__dot__vscode/tasks.json | 42 --- .../igx-ts/projects/_base/files/angular.json | 114 ------- .../igx-ts/projects/_base/files/package.json | 44 --- .../projects/_base/files/public/favicon.ico | Bin 15086 -> 0 bytes .../projects/_base/files/src/index.html | 14 - .../igx-ts/projects/_base/files/src/main.ts | 6 - .../projects/_base/files/tsconfig.app.json | 14 - .../igx-ts/projects/_base/files/tsconfig.json | 31 -- .../projects/_base/files/tsconfig.spec.json | 14 - .../igx-ts/projects/_base/index.ts | 76 ++++- .../igx-ts/projects/_base/scaffold.ts | 123 +++++++ .../projects/side-nav-auth/files/package.json | 47 --- .../side-nav-auth/files/tsconfig.app.json | 24 -- .../side-nav-auth/files/tsconfig.spec.json | 18 -- .../igx-ts/projects/side-nav-auth/index.ts | 10 + .../projects/side-nav-mini-auth/index.ts | 2 + .../ng-schematics/src/app-projects/index.ts | 43 --- .../src/app-projects/index_spec.ts | 25 -- .../ng-schematics/src/app-projects/schema.ts | 10 - packages/ng-schematics/src/collection.json | 5 - packages/ng-schematics/src/ng-new/index.ts | 94 +++--- .../ng-schematics/src/ng-new/index_spec.ts | 94 +++--- packages/ng-schematics/src/ng-new/schema.json | 2 +- scripts/generate-angular-base.mjs | 299 ------------------ scripts/generate-angular-base.upstream.json | 13 - scripts/versionScript.ts | 5 +- 38 files changed, 352 insertions(+), 1008 deletions(-) delete mode 100644 packages/igx-templates/igx-ts/projects/_base/files/.prettierrc delete mode 100644 packages/igx-templates/igx-ts/projects/_base/files/__dot__editorconfig delete mode 100644 packages/igx-templates/igx-ts/projects/_base/files/__dot__gitignore delete mode 100644 packages/igx-templates/igx-ts/projects/_base/files/__dot__vscode/extensions.json delete mode 100644 packages/igx-templates/igx-ts/projects/_base/files/__dot__vscode/launch.json delete mode 100644 packages/igx-templates/igx-ts/projects/_base/files/__dot__vscode/tasks.json delete mode 100644 packages/igx-templates/igx-ts/projects/_base/files/angular.json delete mode 100644 packages/igx-templates/igx-ts/projects/_base/files/package.json delete mode 100644 packages/igx-templates/igx-ts/projects/_base/files/public/favicon.ico delete mode 100644 packages/igx-templates/igx-ts/projects/_base/files/src/index.html delete mode 100644 packages/igx-templates/igx-ts/projects/_base/files/src/main.ts delete mode 100644 packages/igx-templates/igx-ts/projects/_base/files/tsconfig.app.json delete mode 100644 packages/igx-templates/igx-ts/projects/_base/files/tsconfig.json delete mode 100644 packages/igx-templates/igx-ts/projects/_base/files/tsconfig.spec.json create mode 100644 packages/igx-templates/igx-ts/projects/_base/scaffold.ts delete mode 100644 packages/igx-templates/igx-ts/projects/side-nav-auth/files/package.json delete mode 100644 packages/igx-templates/igx-ts/projects/side-nav-auth/files/tsconfig.app.json delete mode 100644 packages/igx-templates/igx-ts/projects/side-nav-auth/files/tsconfig.spec.json delete mode 100644 packages/ng-schematics/src/app-projects/index.ts delete mode 100644 packages/ng-schematics/src/app-projects/index_spec.ts delete mode 100644 packages/ng-schematics/src/app-projects/schema.ts delete mode 100644 scripts/generate-angular-base.mjs delete mode 100644 scripts/generate-angular-base.upstream.json diff --git a/.agents/README.md b/.agents/README.md index cb0e4d0aec..338b50d175 100644 --- a/.agents/README.md +++ b/.agents/README.md @@ -24,7 +24,7 @@ Each procedure is written once, as a skill in `.agents/skills/` ([Agent Skills]( | [`igniteui-cli-ai-config-skills-refresh`](./skills/igniteui-cli-ai-config-skills-refresh/SKILL.md) | Workflow | Bundled `ai-config` skills | Mirror the framework repositories' `skills/` into the `ai-config` templates. In `dry-run` mode: what changed upstream and content issues | | [`igniteui-cli-mcp-tool`](./skills/igniteui-cli-mcp-tool/SKILL.md) | Workflow | MCP server `src/`, tests, tool docs | Add an MCP server tool or change an existing one. Its `scripts/call-tool.mjs` calls a tool on the built server over stdio | | [`igniteui-cli-smoke-test`](./skills/igniteui-cli-smoke-test/SKILL.md) | Reference | — | Build, running one spec file, specs that guard templates, running the smoke test, checking kept projects, triaging failures ("Triaging a Run"), editing the script | -| [`igniteui-cli-template-anatomy`](./skills/igniteui-cli-template-anatomy/SKILL.md) | Reference | — | Template layout per framework, model templates, the generated Angular base project, every place versions are pinned, parity auditing ("Auditing Parity Across Frameworks") | +| [`igniteui-cli-template-anatomy`](./skills/igniteui-cli-template-anatomy/SKILL.md) | Reference | — | Template layout per framework, model templates, how Angular projects are created from `ng new`, every place versions are pinned, parity auditing ("Auditing Parity Across Frameworks") | The workflows triage their own smoke runs; use the smoke-test skill directly for a run you started by hand. @@ -55,7 +55,6 @@ Each workflow runs on its own and does not call the others. You chain them. | See which pins are stale or conflicting | "dry-run template version update, all frameworks" | | Add a template | "add a Tile Manager component template for angular" | | See which templates are missing | "which component templates are missing per framework?" | -| Move the Angular base project to a new Angular version | "regenerate the Angular base project for Angular ~22.3.0" | | Make sense of a failed smoke run | "triage output/smoke" | | Release notes for 15.9.0 | "draft the changelog for 15.9.0, 2026-10-15" | | Refresh the skills `ig ai-config` bundles | "refresh ai-config skills for all frameworks" | diff --git a/.agents/skills/igniteui-cli-smoke-test/SKILL.md b/.agents/skills/igniteui-cli-smoke-test/SKILL.md index fa90053887..3d8a9698ea 100644 --- a/.agents/skills/igniteui-cli-smoke-test/SKILL.md +++ b/.agents/skills/igniteui-cli-smoke-test/SKILL.md @@ -6,7 +6,7 @@ license: MIT # Ignite UI CLI: Build, Template Specs, Smoke Test -`scripts/smoke-test.sh` is the **only** check that a generated project actually installs and builds. Jasmine stubs `PackageManager.installPackages`, so a green Jasmine run says nothing about real npm resolution or TypeScript compilation of generated code. Run the build and the template specs first: they are fast and catch id and pin mistakes before a slow smoke run. +`scripts/smoke-test.sh` is the **only** check that a generated project actually installs and builds. Jasmine stubs `PackageManager.installPackages` and the Angular CLI (`ng new` / `ng add`, which create every Angular project), so a green Jasmine run says nothing about real npm resolution, what the current Angular CLI generates, or TypeScript compilation of generated code. Run the build and the template specs first: they are fast and catch id and pin mistakes before a slow smoke run. ## Build and Template Specs @@ -20,7 +20,8 @@ license: MIT | Spec | Guards | |---|---| -| `spec/templates/angular-spec.ts` | Template ids exist; no file collisions between templates; every `igniteui-angular` pin in templates and project `package.json` equals `IGNITEUI_ANGULAR_PACKAGE` | +| `spec/templates/angular-spec.ts` | Template ids exist; no file collisions between templates; every `igniteui-angular` pin in templates equals `IGNITEUI_ANGULAR_PACKAGE`; project templates carry no `package.json` (the Angular CLI generates it) | +| `spec/templates/angular-scaffold-spec.ts` | The Angular project scaffold, against the stubbed Angular CLI: command sequence, every patch to the generated `angular.json`, `package.json` and tsconfigs, the projects' `extras`, cleanup on failure | | `spec/templates/react-spec.ts` | Same, for all four `IGNITEUI_REACT_*` constants | | `spec/templates/webcomponents-spec.ts` | Ids and collisions only. It does **not** check version pins against the constants | | `spec/templates/template-contracts-spec.ts` | Every jQuery, React, Web Components and Blazor project and component template: matches its framework and project type, its template paths exist, it generates a config | @@ -30,7 +31,7 @@ license: MIT The template specs for a framework run in about 15 seconds, after `npm run build`: ```bash -npx jasmine spec/templates/angular-spec.js +npx jasmine spec/templates/angular-spec.js spec/templates/angular-scaffold-spec.js npx jasmine spec/templates/react-spec.js spec/templates/template-contracts-spec.js npx jasmine spec/templates/webcomponents-spec.js spec/templates/template-contracts-spec.js ``` @@ -51,7 +52,7 @@ Preconditions: `npm run build` has run (the script refuses otherwise); `node` an Defaults: angular, react, webcomponents; default project template; all component templates. jQuery and Blazor are opt-in (`-f jquery`, `-f blazor`): jQuery needs ProGet credentials for 13 templates and has no build; Blazor needs the .NET SDK and has no component templates. -`--all-projects` takes its list from `ig list`, so **hidden project templates** (`isHidden = true` in the project's `index.ts`, e.g. `base`, `side-nav-auth`, `ai-config`) are not scaffolded. Most reuse the `_base` / `_base_with_home` files; a hidden project with its own `files/package.json` (e.g. Angular `side-nav-auth`) is covered only by `scripts/smoke-test.sh -f -p `. Hidden projects can also reuse another project's `files/` through `templatePaths` (Angular `side-nav-mini-auth` includes `side-nav-auth/files`, manifest included), so a change there needs a `-p` run for each project that pulls it in. +`--all-projects` takes its list from `ig list`, so **hidden project templates** (`isHidden = true` in the project's `index.ts`, e.g. `base`, `side-nav-auth`, `ai-config`) are not scaffolded. Most reuse the `_base` / `_base_with_home` files; a hidden project with its own packages (a `files/package.json`, or Angular `extras` as in `side-nav-auth`) is covered only by `scripts/smoke-test.sh -f -p `. Hidden projects can also reuse another project's `files/` through `templatePaths` (Angular `side-nav-mini-auth` includes `side-nav-auth/files` and its packages), so a change there needs a `-p` run for each project that pulls it in. Runs longer than a few minutes: use a long command timeout, or run it in the background with output redirected to a file and wait for the process to exit. The `Results:` line is not the end: when nothing failed and `--keep` is not set, the script deletes the generated projects after printing it. The script **wipes its `--out` directory at start**, so give each run its own directory (e.g. `--out output/smoke-react`, `--out output/smoke-react-projects`) when you need more than one run's results, and never run two at once into the same directory. @@ -115,7 +116,7 @@ The test runners use browser mode with Playwright Chromium. If one fails because ## Mapping an Install Error Back to Source -A pin in an install error comes from one of: the template's variant `index.ts` (`this.packages`), the framework's `constants.ts`, or a project template's `files/package.json`. See the template-anatomy skill. +A pin in an install error comes from one of: the template's variant `index.ts` (`this.packages`), the framework's `constants.ts`, or a project template's `files/package.json`. An Angular project's own packages follow `IGNITEUI_ANGULAR_PACKAGE`: `igniteui-angular` at that range, the rest at whatever `ng add` and the project's `extras` resolve, so an install error there is an upstream peer conflict (template-anatomy skill, "Angular Projects"). ## Mapping a Build Error Back to Template Source diff --git a/.agents/skills/igniteui-cli-template-anatomy/SKILL.md b/.agents/skills/igniteui-cli-template-anatomy/SKILL.md index 77f0e392e0..2a0b7298e8 100644 --- a/.agents/skills/igniteui-cli-template-anatomy/SKILL.md +++ b/.agents/skills/igniteui-cli-template-anatomy/SKILL.md @@ -1,6 +1,6 @@ --- name: igniteui-cli-template-anatomy -description: "Template anatomy for Angular (igx-ts), React (igr-ts) and Web Components (igc-ts): folder layout, index.ts fields, placeholders, test files, groups, the generated Angular base project, and every place an Ignite UI version is pinned. WHEN TO USE: adding or changing a component template, moving the Angular base to a new Angular version, locating version pins, auditing template parity across frameworks. WHEN NOT TO USE: running builds, template specs or the smoke test (igniteui-cli-smoke-test)." +description: "Template anatomy for Angular (igx-ts), React (igr-ts) and Web Components (igc-ts): folder layout, index.ts fields, placeholders, test files, groups, how Angular projects are created from `ng new`, and every place an Ignite UI version is pinned. WHEN TO USE: adding or changing a component template, changing what new Angular projects contain, locating version pins, auditing template parity across frameworks. WHEN NOT TO USE: running builds, template specs or the smoke test (igniteui-cli-smoke-test)." license: MIT --- @@ -83,32 +83,30 @@ Angular has two styles. Pick the one the component library dictates: React imports components from `igniteui-react` (or `-grids`, `-charts`, `-gauges`) as `Igr*`. Web Components imports `Igc*Component` and calls `defineComponents(...)`. -## Generated Angular Base Project +## Angular Projects -`scripts/generate-angular-base.mjs` rebuilds the Angular-owned files of `packages/igx-templates/igx-ts/projects/_base/files` from `ng new`, followed by `ng add angular-eslint` (at the Angular major) and `ng add igniteui-angular`. It needs network access and runs `npm install` in a temp folder, a few minutes. +`ig new` and the `ng-new` schematic create an Angular project with the Angular CLI, through `scaffold()` on `BaseIgxProject` (`packages/igx-templates/igx-ts/projects/_base/index.ts`; the steps live in `_base/scaffold.ts`): -| Goal | Command | -|---|---| -| New Angular minor | `node scripts/generate-angular-base.mjs --angular ~22.3.0` | -| New Angular major (`igniteui-angular` moves with it) | `node scripts/generate-angular-base.mjs --angular ~23.0.0 --igniteui ~23.0.0` | -| Is the base out of date? Writes nothing, exits 1 if a run would change something | `node scripts/generate-angular-base.mjs --check` | +1. `ng new` (scss, routing, no SSR, zone.js), `ng add angular-eslint@`, `ng add igniteui-angular@`. `ng add` installs packages, so `--skip-install` only prints a warning. The AI config `ng add` writes is deleted; `ig new` and the schematic run their own. When a step fails, the project folder is removed. +2. The project's `templatePaths` (`_base`, `_base_with_home`, the project's own `files/`) are laid over the generated project, placeholders substituted. +3. `angular.json` and `package.json` get our deviations as patches (`patchAngularJson`, `patchPackageJson`, which also writes the `IGNITEUI_ANGULAR_PACKAGE` range over the `^` that `ng add` saves); the tsconfigs get the types in the project's `extras`. +4. `npm install` adds `playwright` and the packages in the project's `extras`: at their latest versions, or at the version an entry names. -Without `--angular` / `--igniteui` it uses the `@angular/cli` and `igniteui-angular` pins in `_base/files/package.json`. A run at unchanged versions changes nothing. +Project `files/` are an **overlay**: they hold only files we add or replace wholesale (app code, `README.md`, `ignite-ui-cli.json`, CI workflows, eslint config, `vitest-base.config.ts`, environments, `src/index.html`, `src/styles.scss`). The rest follows from that: -- **Generated**: the files in the script's `COPIED` list, `_base/files/angular.json`, `_base/files/package.json` and `side-nav-auth/files/package.json` (the `_base` manifest plus the packages only `side-nav-auth` uses). Our deviations from `ng new` are patches in the script, so a change to them goes into the script; a hand edit is lost on the next run. -- **Hand-maintained**: every other file in `_base/files` (`README.md`, `src/app/*`, `src/index.html`, `src/styles.scss`, CI workflows, eslint config, environments) and the overlays (`_base_with_home`, `empty`, `side-nav`, `side-nav-auth` tsconfigs, ...). -- **Upstream changes**: the `ng new`/`ng add` versions of the hand-maintained files are fingerprinted in `scripts/generate-angular-base.upstream.json`. When one changed since the last run, the script lists it with a kept upstream copy: port what applies to our file by hand. The updated fingerprint file belongs in the same change as the run's other output. -- **Pins**: `ng new` and `ng add` decide which packages exist. A package already in `_base` keeps its pin while that admits the version installed in the generated project, so a run moves only the pins the new project needs moved (never loosening the others), and the template version update workflow stays the owner of Ignite UI pins. -- **`--igniteui`** sets `igniteui-angular` in the generated manifests only. The other `igniteui-angular` pins (`IGNITEUI_ANGULAR_PACKAGE` and the rest, "Where Ignite UI Versions Are Pinned") must move to the same range; `spec/templates/angular-spec.ts` checks it. +- A change to a file `ng new` generates (`angular.json`, `package.json`, tsconfigs, `src/main.ts`) is a patch in `_base/scaffold.ts`, so the generated file keeps up with Angular. `spec/templates/angular-spec.ts` fails on a project `files/package.json`. +- A package or global type a project's code needs goes in its `extras` (`side-nav-auth` declares `authExtras`; `side-nav-mini-auth` reuses it). `extras` receives the project's Angular major: a package whose majors follow Angular's (`@angular/*`, `angular-auth-oidc-client`) is pinned to it, since its latest version would outrun the project's Angular once a new major ships. +- Packages the overlay files use without an `extras` entry (`minireset.css`, `@igniteui/material-icons-extended`) come from `ng add igniteui-angular`; the smoke build fails if it stops adding them. +- `IGNITEUI_ANGULAR_PACKAGE` in `constants.ts` sets the versions: `igniteui-angular` at that range, Angular at the newest major that range's `peerDependencies` allow (resolved with `npm view` when the project is created). Minors and patches arrive on their own; moving to a new major is a change to that one constant, after which the smoke test below must pass. -After a run, validate with the smoke test: `-f angular --all-projects`, plus `-p base`, `-p side-nav-auth` and `-p side-nav-mini-auth` (smoke-test skill). +Jasmine never runs the real CLI: `spec/helpers/angular-cli-stub.ts` stands in for `ng` and `npm` and writes the project a real `ng new` + `ng add` produce, and `spec/templates/angular-scaffold-spec.ts` checks the command sequence and every patch. When the real output changes shape (a patch fails in a smoke run), update the stub with it. Only the smoke test runs the real CLI, with network access; a change here needs a run over every Angular project template, the hidden `base`, `side-nav-auth` and `side-nav-mini-auth` included (smoke-test skill). ## Where Ignite UI Versions Are Pinned Pins live in three kinds of places. **Always regenerate the list with a search; never trust a remembered list.** 1. `constants.ts` per framework (exported `name@range` strings). -2. Project-template manifests: `/projects/*/files/package.json`. Several projects carry their own copy (e.g. Angular `_base` and `side-nav-auth`; Web Components `_base` and `_base_with_home`). +2. Project-template manifests: `/projects/*/files/package.json`, e.g. Web Components `_base` and `_base_with_home`. Angular projects have none: their versions follow `IGNITEUI_ANGULAR_PACKAGE` ("Angular Projects"). 3. Hard-coded `this.packages = ["name@range", ...]` in variant `index.ts` files, used where no constant exists (Angular charts/gauges/maps family, `igniteui-grid-lite`, wrapped `igniteui-webcomponents`; Web Components `igniteui-dockmanager`). Some build the name at runtime, e.g. Angular dock-manager: `` `${resolvePackage(NPM_DOCK_MANAGER)}@~2.1.1` ``. Run **both** search patterns (ripgrep) over `packages/cli/templates` and `packages/igx-templates`, `*.ts` and `*.json`: diff --git a/.agents/skills/igniteui-cli-template-version-update/SKILL.md b/.agents/skills/igniteui-cli-template-version-update/SKILL.md index 91f3517c4c..372a7ecaeb 100644 --- a/.agents/skills/igniteui-cli-template-version-update/SKILL.md +++ b/.agents/skills/igniteui-cli-template-version-update/SKILL.md @@ -22,7 +22,8 @@ If the scope is missing, or a scope or target is ambiguous, stop and ask. In `up Scope boundaries: - A framework scope covers only that framework's root. With scope `webcomponents`, the `igniteui-webcomponents` pins in Angular's wrapped-component templates (`packages/igx-templates/.../chat`, `rating`) are **out of scope**; report them as observations. -- Packages shared by all frameworks' project templates (e.g. `@igniteui/material-icons-extended`) are updated only when the user names them or the scope covers all three frameworks, so the frameworks don't diverge. Otherwise list them as observations. +- Angular project templates pin nothing: new projects take `igniteui-angular` at the `IGNITEUI_ANGULAR_PACKAGE` range and Angular at the newest major that range supports (anatomy skill, "Angular Projects"). Updating that constant therefore moves new projects too, including their Angular major when the new `igniteui-angular` requires one; Angular pins live only in `constants.ts` and component templates. +- Packages shared by the React and Web Components project templates (e.g. `@igniteui/material-icons-extended`) are updated only when the user names them or the scope covers both frameworks, so the frameworks don't diverge. Otherwise list them as observations. --- @@ -33,7 +34,7 @@ Your changes are Ignite UI version strings, applied as patch or minor updates. E - **Major** updates (see Step 3) are stop items, never applied. - Each pin keeps its range prefix (`~` or `^`); report inconsistent prefixes as observations. - Pins stay where they are: hard-coded pins stay hard-coded unless the user asked to move them into constants. -- Non-Ignite UI dependencies in project templates (Angular, React, Vite, TypeScript, etc.) stay as they are. If a new Ignite UI version requires them to change, that is a stop item (Step 3). +- Non-Ignite UI dependencies in project templates (React, Vite, TypeScript, etc.) stay as they are. If a new Ignite UI version requires them to change, that is a stop item (Step 3). - jQuery and Blazor templates are out of scope unless explicitly named. - `CHANGELOG.md` is written at release time: propose a line in your report. @@ -98,7 +99,7 @@ For each distinct package: - **major** (or `0.x` minor change): **stop for that package**. Leave all of its pins unchanged. - **already current** (the existing range already resolves to the target): no change. This includes `^` pins whose range admits a newer minor (e.g. `^2.1.1` admits 2.2.1). Keep their minimum unless the user asks; list them under Observations as "minimum could be raised to `^`". 4. For packages that proceed, check peers: `npm view @ peerDependencies`. - - If a peer is a **non-Ignite UI** package (e.g. a new `igniteui-angular` needing a newer `@angular/core`) and the project templates don't satisfy it, **stop for that package** and report the required change. For Angular, report it as the generator command that makes the change, with `--igniteui ` when `igniteui-angular` is the package moving (anatomy skill, "Generated Angular Base Project"). + - If a peer is a **non-Ignite UI** package (e.g. a new `igniteui-react` needing a newer `react`) and the project templates don't satisfy it, **stop for that package** and report the required change. - If a peer is another **Ignite UI** package pinned in templates (e.g. `igniteui-angular` → `igniteui-grid-lite ~0.11.0`), that pin must satisfy the peer range after your update. Treat bringing it into range as part of this update, subject to the same patch/minor/major rule; if that would be a major (or `0.x` minor) change, report it as a stop item with the peer range as the reason, so a human can approve it explicitly. Do this check also for the framework's **main package even when it is not changing**: existing pins can already be out of range. @@ -130,7 +131,7 @@ Use the [`igniteui-cli-smoke-test`](../igniteui-cli-smoke-test/SKILL.md) skill f 3. Smoke test, per framework in scope, each into its **own output directory** (a run wipes its `--out` at start, so a shared directory loses the earlier run's results). Scope the run to what changed: - The framework's main package changed (`igniteui-angular`, `igniteui-react`, `igniteui-webcomponents`), which nearly every template pulls in: `scripts/smoke-test.sh -f --out output/smoke-` (default project + every component template). - Otherwise, only the templates that pull in a changed package: search the framework's variant `index.ts` files for the package name or its constant, read each match's `this.id`, and run `scripts/smoke-test.sh -f --templates , --out output/smoke-`. For example, an `igniteui-dockmanager` update runs `--templates dock-manager`. The default project is always scaffolded, so a pin in its `package.json` is covered too. - - Additionally, when any `projects/*/files/package.json` changed: `scripts/smoke-test.sh -f --all-projects --out output/smoke--projects`. `--all-projects` skips hidden projects, so for each changed manifest under a hidden project (`isHidden = true` in its `index.ts`, e.g. Angular `side-nav-auth`) also run `scripts/smoke-test.sh -f -p --out output/smoke--`, and the same for every other hidden project whose `templatePaths` includes that `files/` folder (Angular `side-nav-mini-auth` reuses `side-nav-auth/files`). If a changed package appears only in project manifests (no template pulls it in, e.g. `@igniteui/material-icons-extended`), these runs alone cover it; skip the `--templates` run. + - Additionally, when any `projects/*/files/package.json` changed, or for Angular when `IGNITEUI_ANGULAR_PACKAGE` changed (it sets the versions of every Angular project, hidden ones included): `scripts/smoke-test.sh -f --all-projects --out output/smoke--projects`. `--all-projects` skips hidden projects (`isHidden = true` in their `index.ts`), so also run `scripts/smoke-test.sh -f -p --out output/smoke--` for each hidden project the change reaches: every hidden Angular project when `IGNITEUI_ANGULAR_PACKAGE` changed; for a changed manifest, the hidden project that owns it and every other hidden project whose `templatePaths` includes that `files/` folder. If a changed package appears only in project manifests (no template pulls it in, e.g. `@igniteui/material-icons-extended`), these runs alone cover it; skip the `--templates` run. A scoped run takes about a minute; a full run takes several. Use a long timeout or background execution and wait for completion. Run them one at a time. Classify any failure with the smoke-test skill. Environment failures (network, credentials) are re-run once, then reported; versions stay as they are for those. diff --git a/AGENTS.md b/AGENTS.md index 5345d3990f..2cc16922d1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,7 +15,7 @@ scripts/smoke-test.sh # scaffold + add every template + install + buil Tests are Jasmine specs under `spec/` (unit + acceptance) and within individual packages. -`scripts/smoke-test.sh` is the only check that a generated project actually installs and builds — the Jasmine specs stub `PackageManager.installPackages`. Run it after touching anything under `packages/cli/templates/` or `packages/igx-templates/`. Running one spec file, the specs that guard templates, running the smoke test, reading its results and editing the script: `igniteui-cli-smoke-test` skill. +`scripts/smoke-test.sh` is the only check that a generated project actually installs and builds — the Jasmine specs stub `PackageManager.installPackages` and the Angular CLI. Run it after touching anything under `packages/cli/templates/` or `packages/igx-templates/`. Running one spec file, the specs that guard templates, running the smoke test, reading its results and editing the script: `igniteui-cli-smoke-test` skill. ### MCP Server (`packages/igniteui-mcp/igniteui-doc-mcp`) @@ -57,7 +57,7 @@ Adding or changing an MCP tool (schema, handler, description, tests, every doc t - Placeholders are substituted by the CLI, not a template engine: `<%=ClassName%>` style in Angular templates, `$(ClassName)` style in React and Web Components templates - Templates in `packages/cli/templates/**/files/` and `packages/igx-templates/` -- Angular `_base` manifests and configs are generated by `scripts/generate-angular-base.mjs`: change its patches, not its output (`igniteui-cli-template-anatomy` skill) +- Angular projects are created by `ng new` + `ng add igniteui-angular` at the `IGNITEUI_ANGULAR_PACKAGE` range (`ig new` and the `ng-new` schematic alike); igx-ts project `files/` are an overlay on that, and changes to generated files are patches in `packages/igx-templates/igx-ts/projects/_base/scaffold.ts` (`igniteui-cli-template-anatomy` skill) - Component/template IDs (e.g., "grid", "combo") used by `ig add` command - Default framework is **jQuery** when not specified diff --git a/packages/cli/lib/commands/new.ts b/packages/cli/lib/commands/new.ts index f0dbbdc7bc..29c43be4d9 100644 --- a/packages/cli/lib/commands/new.ts +++ b/packages/cli/lib/commands/new.ts @@ -20,6 +20,18 @@ const _framework: { choices: [] }; +const IG_NEXT_STEPS = [ + "ig add start guided mode for adding views to the app", + "ig start starts a web server and opens the app in the default browser" +]; + +function logNextSteps(name: string, steps: string[]) { + Util.log(""); + Util.log("Next Steps:"); + Util.log(` cd ${Util.quoteIfNeeded(name)}`); + steps.forEach(step => Util.log(` ${step}`)); +} + const command: NewCommandType = { command: "new [name]", describe: "Creates a project", @@ -62,7 +74,7 @@ const command: NewCommandType = { }) .option("skip-install", { alias: "si", - describe: "Do not install packages after scaffolding", + describe: "Do not install packages after scaffolding (Angular projects install them anyway: `ng add` needs them)", type: "boolean" }) .option("template", { @@ -166,7 +178,8 @@ const command: NewCommandType = { }); if (typeof projTemplate.scaffold === "function") { - if (!/^[a-zA-Z][a-zA-Z0-9-]*$/.test(argv.name)) { + const isBlazor = projTemplate.framework === "blazor"; + if (isBlazor && !/^[a-zA-Z][a-zA-Z0-9-]*$/.test(argv.name)) { Util.warn(`The project namespace will be derived from the name '${argv.name}'. ` + "Use only letters, digits and dashes for a clean identifier.", "yellow"); } @@ -191,6 +204,12 @@ const command: NewCommandType = { } process.chdir(argv.name); + if (!isBlazor) { + // the scaffold installed the packages, so `ig add` installs the ones it adds right away + const config = ProjectConfig.localConfig(); + config.packagesInstalled = true; + ProjectConfig.setConfig(config); + } await configure(argv.framework, { agents: argv.agents as (AIAgentTarget | "none")[], assistants: argv.assistants as (AiCodingAssistant | "none")[] @@ -201,11 +220,7 @@ const command: NewCommandType = { Util.gitInit(process.cwd(), argv.name); } - const quotedName = Util.quoteIfNeeded(argv.name); - Util.log(""); - Util.log("Next Steps:"); - Util.log(` cd ${quotedName}`); - Util.log(` dotnet run --project ${quotedName}`); + logNextSteps(argv.name, isBlazor ? [`dotnet run --project ${Util.quoteIfNeeded(argv.name)}`] : IG_NEXT_STEPS); return; } @@ -234,11 +249,7 @@ const command: NewCommandType = { Util.gitInit(process.cwd(), argv.name); } - Util.log(""); - Util.log("Next Steps:"); - Util.log(` cd ${Util.quoteIfNeeded(argv.name)}`); - Util.log(" ig add start guided mode for adding views to the app"); - Util.log(" ig start starts a web server and opens the app in the default browser"); + logNextSteps(argv.name, IG_NEXT_STEPS); }, addChoices(choices: string[]): void { _framework.choices = choices; diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/src/tools/setup_angular.md b/packages/igniteui-mcp/igniteui-doc-mcp/src/tools/setup_angular.md index 2be20aaebf..4449cad48e 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/src/tools/setup_angular.md +++ b/packages/igniteui-mcp/igniteui-doc-mcp/src/tools/setup_angular.md @@ -66,7 +66,7 @@ ig new MyApp --framework=angular --type=igx-ts --template=side-nav | Flag | Description | |---|---| | `--skip-git` | Skip git repository initialization | -| `--skip-install` | Skip npm package installation (useful in CI) | +| `--skip-install` | Has no effect for Angular projects: `ng add igniteui-angular` installs the packages while the project is created | *For theming use igniteui-theming MCP* diff --git a/packages/igx-templates/igx-ts/projects/_base/files/.prettierrc b/packages/igx-templates/igx-ts/projects/_base/files/.prettierrc deleted file mode 100644 index d6c16d7ee7..0000000000 --- a/packages/igx-templates/igx-ts/projects/_base/files/.prettierrc +++ /dev/null @@ -1,12 +0,0 @@ -{ - "printWidth": 100, - "singleQuote": true, - "overrides": [ - { - "files": "*.html", - "options": { - "parser": "angular" - } - } - ] -} diff --git a/packages/igx-templates/igx-ts/projects/_base/files/__dot__editorconfig b/packages/igx-templates/igx-ts/projects/_base/files/__dot__editorconfig deleted file mode 100644 index f166060da1..0000000000 --- a/packages/igx-templates/igx-ts/projects/_base/files/__dot__editorconfig +++ /dev/null @@ -1,17 +0,0 @@ -# Editor configuration, see https://editorconfig.org -root = true - -[*] -charset = utf-8 -indent_style = space -indent_size = 2 -insert_final_newline = true -trim_trailing_whitespace = true - -[*.ts] -quote_type = single -ij_typescript_use_double_quotes = false - -[*.md] -max_line_length = off -trim_trailing_whitespace = false diff --git a/packages/igx-templates/igx-ts/projects/_base/files/__dot__gitignore b/packages/igx-templates/igx-ts/projects/_base/files/__dot__gitignore deleted file mode 100644 index 854acd5fc0..0000000000 --- a/packages/igx-templates/igx-ts/projects/_base/files/__dot__gitignore +++ /dev/null @@ -1,44 +0,0 @@ -# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. - -# Compiled output -/dist -/tmp -/out-tsc -/bazel-out - -# Node -/node_modules -npm-debug.log -yarn-error.log - -# IDEs and editors -.idea/ -.project -.classpath -.c9/ -*.launch -.settings/ -*.sublime-workspace - -# Visual Studio Code -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -!.vscode/mcp.json -.history/* - -# Miscellaneous -/.angular/cache -.sass-cache/ -/connect.lock -/coverage -/libpeerconnection.log -testem.log -/typings -__screenshots__/ - -# System files -.DS_Store -Thumbs.db diff --git a/packages/igx-templates/igx-ts/projects/_base/files/__dot__vscode/extensions.json b/packages/igx-templates/igx-ts/projects/_base/files/__dot__vscode/extensions.json deleted file mode 100644 index 77b374577d..0000000000 --- a/packages/igx-templates/igx-ts/projects/_base/files/__dot__vscode/extensions.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 - "recommendations": ["angular.ng-template"] -} diff --git a/packages/igx-templates/igx-ts/projects/_base/files/__dot__vscode/launch.json b/packages/igx-templates/igx-ts/projects/_base/files/__dot__vscode/launch.json deleted file mode 100644 index 925af83705..0000000000 --- a/packages/igx-templates/igx-ts/projects/_base/files/__dot__vscode/launch.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "ng serve", - "type": "chrome", - "request": "launch", - "preLaunchTask": "npm: start", - "url": "http://localhost:4200/" - }, - { - "name": "ng test", - "type": "chrome", - "request": "launch", - "preLaunchTask": "npm: test", - "url": "http://localhost:9876/debug.html" - } - ] -} diff --git a/packages/igx-templates/igx-ts/projects/_base/files/__dot__vscode/tasks.json b/packages/igx-templates/igx-ts/projects/_base/files/__dot__vscode/tasks.json deleted file mode 100644 index 244306f98a..0000000000 --- a/packages/igx-templates/igx-ts/projects/_base/files/__dot__vscode/tasks.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 - "version": "2.0.0", - "tasks": [ - { - "type": "npm", - "script": "start", - "isBackground": true, - "problemMatcher": { - "owner": "typescript", - "pattern": "$tsc", - "background": { - "activeOnStart": true, - "beginsPattern": { - "regexp": "Changes detected" - }, - "endsPattern": { - "regexp": "bundle generation (complete|failed)" - } - } - } - }, - { - "type": "npm", - "script": "test", - "isBackground": true, - "problemMatcher": { - "owner": "typescript", - "pattern": "$tsc", - "background": { - "activeOnStart": true, - "beginsPattern": { - "regexp": "Changes detected" - }, - "endsPattern": { - "regexp": "bundle generation (complete|failed)" - } - } - } - } - ] -} diff --git a/packages/igx-templates/igx-ts/projects/_base/files/angular.json b/packages/igx-templates/igx-ts/projects/_base/files/angular.json deleted file mode 100644 index d4c782922a..0000000000 --- a/packages/igx-templates/igx-ts/projects/_base/files/angular.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "$schema": "./node_modules/@angular/cli/lib/config/schema.json", - "version": 1, - "cli": { - "packageManager": "npm", - "schematicCollections": [ - "angular-eslint" - ] - }, - "newProjectRoot": "projects", - "projects": { - "<%=dashName%>": { - "projectType": "application", - "schematics": { - "@schematics/angular:component": { - "style": "scss" - } - }, - "root": "", - "sourceRoot": "src", - "prefix": "app", - "architect": { - "build": { - "builder": "@angular/build:application", - "options": { - "browser": "src/main.ts", - "polyfills": [ - "zone.js" - ], - "tsConfig": "tsconfig.app.json", - "inlineStyleLanguage": "scss", - "assets": [ - { - "glob": "**/*", - "input": "public" - }, - "src/assets" - ], - "styles": [ - "src/styles.scss"<%=DefaultTheme%> - ], - "stylePreprocessorOptions": { - "includePaths": [ - "node_modules" - ] - } - }, - "configurations": { - "production": { - "budgets": [ - { - "type": "initial", - "maximumWarning": "5mb", - "maximumError": "40mb" - }, - { - "type": "anyComponentStyle", - "maximumWarning": "8kB", - "maximumError": "2mb" - } - ], - "outputHashing": "all" - }, - "development": { - "optimization": false, - "extractLicenses": false, - "sourceMap": true, - "fileReplacements": [ - { - "replace": "src/environments/environment.ts", - "with": "src/environments/environment.development.ts" - } - ] - } - }, - "defaultConfiguration": "production" - }, - "serve": { - "builder": "@angular/build:dev-server", - "configurations": { - "production": { - "buildTarget": "<%=dashName%>:build:production" - }, - "development": { - "buildTarget": "<%=dashName%>:build:development" - } - }, - "defaultConfiguration": "development" - }, - "test": { - "builder": "@angular/build:unit-test", - "options": { - "runnerConfig": true - } - }, - "lint": { - "builder": "@angular-eslint/builder:lint", - "options": { - "lintFilePatterns": [ - "src/**/*.ts", - "src/**/*.html" - ] - } - }, - "extract-i18n": { - "builder": "@angular/build:extract-i18n", - "options": { - "buildTarget": "<%=dashName%>:build" - } - } - } - } - } -} diff --git a/packages/igx-templates/igx-ts/projects/_base/files/package.json b/packages/igx-templates/igx-ts/projects/_base/files/package.json deleted file mode 100644 index cc4bac66db..0000000000 --- a/packages/igx-templates/igx-ts/projects/_base/files/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "<%=dashName%>", - "version": "0.0.0", - "scripts": { - "ng": "ng", - "start": "ng serve -o", - "build": "ng build", - "watch": "ng build --watch --configuration development", - "test": "ng test --browsers=chromium", - "lint": "ng lint" - }, - "private": true, - "dependencies": { - "@angular/animations": "~22.2.0", - "@angular/common": "~22.2.0", - "@angular/compiler": "~22.2.0", - "@angular/core": "~22.2.0", - "@angular/forms": "~22.2.0", - "@angular/platform-browser": "~22.2.0", - "@angular/router": "~22.2.0", - "@igniteui/material-icons-extended": "^3.0.2", - "igniteui-angular": "~22.2.0", - "igniteui-i18n-core": "^1.0.5", - "minireset.css": "~0.0.7", - "rxjs": "~7.8.1", - "tslib": "~2.8.1", - "zone.js": "~0.16.0" - }, - "devDependencies": { - "@angular/build": "~22.2.0", - "@angular/cli": "~22.2.0", - "@angular/compiler-cli": "~22.2.0", - "@eslint/js": "^10.0.1", - "@vitest/browser-playwright": "^5.0.0", - "angular-eslint": "~22.6.0", - "eslint": "^10.3.0", - "igniteui-cli": "~<%=cliVersion%>", - "playwright": "^1.58.2", - "prettier": "^3.8.1", - "typescript": "~6.0.2", - "typescript-eslint": "8.71.0", - "vitest": "^5.0.0" - } -} diff --git a/packages/igx-templates/igx-ts/projects/_base/files/public/favicon.ico b/packages/igx-templates/igx-ts/projects/_base/files/public/favicon.ico deleted file mode 100644 index 57614f9c967596fad0a3989bec2b1deff33034f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15086 zcmd^G33O9Omi+`8$@{|M-I6TH3wzF-p5CV8o}7f~KxR60LK+ApEFB<$bcciv%@SmA zV{n>g85YMFFeU*Uvl=i4v)C*qgnb;$GQ=3XTe9{Y%c`mO%su)noNCCQ*@t1WXn|B(hQ7i~ zrUK8|pUkD6#lNo!bt$6)jR!&C?`P5G(`e((P($RaLeq+o0Vd~f11;qB05kdbAOm?r zXv~GYr_sibQO9NGTCdT;+G(!{4Xs@4fPak8#L8PjgJwcs-Mm#nR_Z0s&u?nDX5^~@ z+A6?}g0|=4e_LoE69pPFO`yCD@BCjgKpzMH0O4Xs{Ahc?K3HC5;l=f zg>}alhBXX&);z$E-wai+9TTRtBX-bWYY@cl$@YN#gMd~tM_5lj6W%8ah4;uZ;jP@Q zVbuel1rPA?2@x9Y+u?e`l{Z4ngfG5q5BLH5QsEu4GVpt{KIp1?U)=3+KQ;%7ec8l* zdV=zZgN5>O3G(3L2fqj3;oBbZZw$Ij@`Juz@?+yy#OPw)>#wsTewVgTK9BGt5AbZ&?K&B3GVF&yu?@(Xj3fR3n+ZP0%+wo)D9_xp>Z$`A4 zfV>}NWjO#3lqumR0`gvnffd9Ka}JJMuHS&|55-*mCD#8e^anA<+sFZVaJe7{=p*oX zE_Uv?1>e~ga=seYzh{9P+n5<+7&9}&(kwqSaz;1aD|YM3HBiy<))4~QJSIryyqp| z8nGc(8>3(_nEI4n)n7j(&d4idW1tVLjZ7QbNLXg;LB ziHsS5pXHEjGJZb59KcvS~wv;uZR-+4qEqow`;JCfB*+b^UL^3!?;-^F%yt=VjU|v z39SSqKcRu_NVvz!zJzL0CceJaS6%!(eMshPv_0U5G`~!a#I$qI5Ic(>IONej@aH=f z)($TAT#1I{iCS4f{D2+ApS=$3E7}5=+y(rA9mM#;Cky%b*Gi0KfFA`ofKTzu`AV-9 znW|y@19rrZ*!N2AvDi<_ZeR3O2R{#dh1#3-d%$k${Rx42h+i&GZo5!C^dSL34*AKp z27mTd>k>?V&X;Nl%GZ(>0s`1UN~Hfyj>KPjtnc|)xM@{H_B9rNr~LuH`Gr5_am&Ep zTjZA8hljNj5H1Ipm-uD9rC}U{-vR!eay5&6x6FkfupdpT*84MVwGpdd(}ib)zZ3Ky z7C$pnjc82(W_y_F{PhYj?o!@3__UUvpX)v69aBSzYj3 zdi}YQkKs^SyXyFG2LTRz9{(w}y~!`{EuAaUr6G1M{*%c+kP1olW9z23dSH!G4_HSK zzae-DF$OGR{ofP*!$a(r^5Go>I3SObVI6FLY)N@o<*gl0&kLo-OT{Tl*7nCz>Iq=? zcigIDHtj|H;6sR?or8Wd_a4996GI*CXGU}o;D9`^FM!AT1pBY~?|4h^61BY#_yIfO zKO?E0 zJ{Pc`9rVEI&$xxXu`<5E)&+m(7zX^v0rqofLs&bnQT(1baQkAr^kEsk)15vlzAZ-l z@OO9RF<+IiJ*O@HE256gCt!bF=NM*vh|WVWmjVawcNoksRTMvR03H{p@cjwKh(CL4 z7_PB(dM=kO)!s4fW!1p0f93YN@?ZSG` z$B!JaAJCtW$B97}HNO9(x-t30&E}Mo1UPi@Av%uHj~?T|!4JLwV;KCx8xO#b9IlUW zI6+{a@Wj|<2Y=U;a@vXbxqZNngH8^}LleE_4*0&O7#3iGxfJ%Id>+sb;7{L=aIic8 z|EW|{{S)J-wr@;3PmlxRXU8!e2gm_%s|ReH!reFcY8%$Hl4M5>;6^UDUUae?kOy#h zk~6Ee_@ZAn48Bab__^bNmQ~+k=02jz)e0d9Z3>G?RGG!65?d1>9}7iG17?P*=GUV-#SbLRw)Hu{zx*azHxWkGNTWl@HeWjA?39Ia|sCi{e;!^`1Oec zb>Z|b65OM*;eC=ZLSy?_fg$&^2xI>qSLA2G*$nA3GEnp3$N-)46`|36m*sc#4%C|h zBN<2U;7k>&G_wL4=Ve5z`ubVD&*Hxi)r@{4RCDw7U_D`lbC(9&pG5C*z#W>8>HU)h z!h3g?2UL&sS!oY5$3?VlA0Me9W5e~V;2jds*fz^updz#AJ%G8w2V}AEE?E^=MK%Xt z__Bx1cr7+DQmuHmzn*|hh%~eEc9@m05@clWfpEFcr+06%0&dZJH&@8^&@*$qR@}o3 z@Tuuh2FsLz^zH+dN&T&?0G3I?MpmYJ;GP$J!EzjeM#YLJ!W$}MVNb0^HfOA>5Fe~UNn%Zk(PT@~9}1dt)1UQ zU*B5K?Dl#G74qmg|2>^>0WtLX#Jz{lO4NT`NYB*(L#D|5IpXr9v&7a@YsGp3vLR7L zHYGHZg7{ie6n~2p$6Yz>=^cEg7tEgk-1YRl%-s7^cbqFb(U7&Dp78+&ut5!Tn(hER z|Gp4Ed@CnOPeAe|N>U(dB;SZ?NU^AzoD^UAH_vamp6Ws}{|mSq`^+VP1g~2B{%N-!mWz<`)G)>V-<`9`L4?3dM%Qh6<@kba+m`JS{Ya@9Fq*m6$$ zA1%Ogc~VRH33|S9l%CNb4zM%k^EIpqY}@h{w(aBcJ9c05oiZx#SK9t->5lSI`=&l~ z+-Ic)a{FbBhXV$Xt!WRd`R#Jk-$+_Z52rS>?Vpt2IK<84|E-SBEoIw>cs=a{BlQ7O z-?{Fy_M&84&9|KM5wt~)*!~i~E=(6m8(uCO)I=)M?)&sRbzH$9Rovzd?ZEY}GqX+~ zFbEbLz`BZ49=2Yh-|<`waK-_4!7`ro@zlC|r&I4fc4oyb+m=|c8)8%tZ-z5FwhzDt zL5kB@u53`d@%nHl0Sp)Dw`(QU&>vujEn?GPEXUW!Wi<+4e%BORl&BIH+SwRcbS}X@ z01Pk|vA%OdJKAs17zSXtO55k!;%m9>1eW9LnyAX4uj7@${O6cfii`49qTNItzny5J zH&Gj`e}o}?xjQ}r?LrI%FjUd@xflT3|7LA|ka%Q3i}a8gVm<`HIWoJGH=$EGClX^C0lysQJ>UO(q&;`T#8txuoQ_{l^kEV9CAdXuU1Ghg8 zN_6hHFuy&1x24q5-(Z7;!poYdt*`UTdrQOIQ!2O7_+AHV2hgXaEz7)>$LEdG z<8vE^Tw$|YwZHZDPM!SNOAWG$?J)MdmEk{U!!$M#fp7*Wo}jJ$Q(=8>R`Ats?e|VU?Zt7Cdh%AdnfyN3MBWw{ z$OnREvPf7%z6`#2##_7id|H%Y{vV^vWXb?5d5?a_y&t3@p9t$ncHj-NBdo&X{wrfJ zamN)VMYROYh_SvjJ=Xd!Ga?PY_$;*L=SxFte!4O6%0HEh%iZ4=gvns7IWIyJHa|hT z2;1+e)`TvbNb3-0z&DD_)Jomsg-7p_Uh`wjGnU1urmv1_oVqRg#=C?e?!7DgtqojU zWoAB($&53;TsXu^@2;8M`#z{=rPy?JqgYM0CDf4v@z=ZD|ItJ&8%_7A#K?S{wjxgd z?xA6JdJojrWpB7fr2p_MSsU4(R7=XGS0+Eg#xR=j>`H@R9{XjwBmqAiOxOL` zt?XK-iTEOWV}f>Pz3H-s*>W z4~8C&Xq25UQ^xH6H9kY_RM1$ch+%YLF72AA7^b{~VNTG}Tj#qZltz5Q=qxR`&oIlW Nr__JTFzvMr^FKp4S3v*( diff --git a/packages/igx-templates/igx-ts/projects/_base/files/src/index.html b/packages/igx-templates/igx-ts/projects/_base/files/src/index.html deleted file mode 100644 index 469f8ebcb1..0000000000 --- a/packages/igx-templates/igx-ts/projects/_base/files/src/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - Ignite UI for Angular - - - - - - - - - diff --git a/packages/igx-templates/igx-ts/projects/_base/files/src/main.ts b/packages/igx-templates/igx-ts/projects/_base/files/src/main.ts deleted file mode 100644 index 5df75f9c83..0000000000 --- a/packages/igx-templates/igx-ts/projects/_base/files/src/main.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { bootstrapApplication } from '@angular/platform-browser'; -import { appConfig } from './app/app.config'; -import { App } from './app/app'; - -bootstrapApplication(App, appConfig) - .catch((err) => console.error(err)); diff --git a/packages/igx-templates/igx-ts/projects/_base/files/tsconfig.app.json b/packages/igx-templates/igx-ts/projects/_base/files/tsconfig.app.json deleted file mode 100644 index cb151e1016..0000000000 --- a/packages/igx-templates/igx-ts/projects/_base/files/tsconfig.app.json +++ /dev/null @@ -1,14 +0,0 @@ -/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ -/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "types": [] - }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "src/**/*.spec.ts" - ] -} diff --git a/packages/igx-templates/igx-ts/projects/_base/files/tsconfig.json b/packages/igx-templates/igx-ts/projects/_base/files/tsconfig.json deleted file mode 100644 index d2fbb9c2b0..0000000000 --- a/packages/igx-templates/igx-ts/projects/_base/files/tsconfig.json +++ /dev/null @@ -1,31 +0,0 @@ -/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ -/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ -{ - "compileOnSave": false, - "compilerOptions": { - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "skipLibCheck": true, - "isolatedModules": true, - "experimentalDecorators": true, - "importHelpers": true, - "target": "ES2022", - "module": "preserve" - }, - "angularCompilerOptions": { - "enableI18nLegacyMessageIdFormat": false, - "strictInjectionParameters": true, - "strictInputAccessModifiers": true - }, - "files": [], - "references": [ - { - "path": "./tsconfig.app.json" - }, - { - "path": "./tsconfig.spec.json" - } - ] -} diff --git a/packages/igx-templates/igx-ts/projects/_base/files/tsconfig.spec.json b/packages/igx-templates/igx-ts/projects/_base/files/tsconfig.spec.json deleted file mode 100644 index 9c8efb9b70..0000000000 --- a/packages/igx-templates/igx-ts/projects/_base/files/tsconfig.spec.json +++ /dev/null @@ -1,14 +0,0 @@ -/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ -/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "types": [ - "vitest/globals" - ] - }, - "include": [ - "src/**/*.d.ts", - "src/**/*.spec.ts" - ] -} diff --git a/packages/igx-templates/igx-ts/projects/_base/index.ts b/packages/igx-templates/igx-ts/projects/_base/index.ts index b54bff4474..c055428111 100644 --- a/packages/igx-templates/igx-ts/projects/_base/index.ts +++ b/packages/igx-templates/igx-ts/projects/_base/index.ts @@ -1,6 +1,23 @@ -import { ControlExtraConfiguration, NPM_ANGULAR, ProjectTemplate, Util, updateWorkspace } from "@igniteui/cli-core"; +import { + ControlExtraConfiguration, NPM_ANGULAR, ProjectTemplate, ScaffoldOptions, Util, updateWorkspace +} from "@igniteui/cli-core"; +import * as fs from "fs"; import * as path from "path"; +import { CLI_PACKAGE, generateProject, npmInstall, patchAngularJson, patchPackageJson, updateJson } from "./scaffold"; +/** What a project's own files need on top of the generated project */ +export interface ProjectExtras { + /** Packages installed at their latest version, or at the version or range an entry names */ + dependencies: string[]; + devDependencies: string[]; + /** Global types added to tsconfig.app.json and tsconfig.spec.json */ + types: string[]; +} + +/** + * Projects are created by the Angular CLI (`ng new`, `ng add igniteui-angular`). `templatePaths` hold only + * the files laid over that; `scaffold` patches the generated angular.json, package.json and tsconfigs. + */ export class BaseIgxProject implements ProjectTemplate { public id: string = "base"; public name = "base"; @@ -37,13 +54,64 @@ $app-palette: palette($primary, $secondary, $surface); @include typography($font-family: $material-typeface, $type-scale: $material-type-scale); @include theme($app-palette); `; - private DEFAULT_THEME = `, - "node_modules/igniteui-angular/styles/igniteui-angular.css"`; + + /** The extras for a project created with Angular `angularMajor`, which packages that follow Angular's majors match */ + protected extras: (angularMajor: string) => ProjectExtras = () => ({ dependencies: [], devDependencies: [], types: [] }); public get templatePaths(): string[] { return [path.join(__dirname, "files")]; } + /** + * Creates the project with the Angular CLI and lays `templatePaths` over it. + * `options.extraConfig` overrides template variables; its `cliPackage` names the package providing the + * CLI commands (default `igniteui-cli`). + */ + public async scaffold(options: ScaffoldOptions): Promise { + const { cliPackage = CLI_PACKAGE, ...variables } = options.extraConfig || {}; + const config = { ...this.generateConfig(options.name, options.theme), ...variables }; + const projectPath = path.join(process.cwd(), options.name); + if (fs.existsSync(projectPath)) { + Util.error(`Folder "${options.name}" already exists!`, "red"); + return false; + } + if (options.skipInstall) { + Util.warn("--skip-install is ignored for Ignite UI for Angular projects: `ng add` installs the packages it adds.", + "yellow"); + } + + try { + const angularMajor = generateProject(config.dashName, projectPath); + const extras = this.extras(angularMajor); + for (const templatePath of this.templatePaths) { + await Util.processTemplates(templatePath, projectPath, config, this.delimiters, false); + } + updateJson(path.join(projectPath, "angular.json"), json => patchAngularJson(json, config)); + updateJson(path.join(projectPath, "package.json"), pkg => patchPackageJson(pkg, cliPackage, config.cliVersion)); + if (extras.types.length) { + for (const file of ["tsconfig.app.json", "tsconfig.spec.json"]) { + updateJson(path.join(projectPath, file), json => { + json.compilerOptions.types = [...(json.compilerOptions.types || []), ...extras.types]; + return json; + }); + } + } + // also installs the package.json changes above; playwright is the browser for @vitest/browser-playwright + npmInstall(projectPath, ["playwright", ...extras.devDependencies], true); + if (extras.dependencies.length) { + npmInstall(projectPath, extras.dependencies); + } + } catch (error) { + // the folder is this run's own (checked above), so a retry starts clean + fs.rmSync(projectPath, { recursive: true, force: true }); + Util.error(`Project creation failed: ${error.message}`, "red"); + return false; + } + + Util.log(Util.greenCheck() + " Project Created"); + return true; + } + public installModules(): void { throw new Error("Method not implemented."); } @@ -62,7 +130,6 @@ $app-palette: palette($primary, $secondary, $surface); cliVersion: Util.version(), CustomTheme: "", dashName: Util.lowerDashed(name), - DefaultTheme: "", dot: ".", path: name, projectTemplate: this.id, @@ -78,7 +145,6 @@ $app-palette: palette($primary, $secondary, $surface); break; case "Default": default: - config["DefaultTheme"] = this.DEFAULT_THEME; config["themePath"] = "node_modules/igniteui-angular/styles/igniteui-angular.css"; break; } diff --git a/packages/igx-templates/igx-ts/projects/_base/scaffold.ts b/packages/igx-templates/igx-ts/projects/_base/scaffold.ts new file mode 100644 index 0000000000..97fcf8a698 --- /dev/null +++ b/packages/igx-templates/igx-ts/projects/_base/scaffold.ts @@ -0,0 +1,123 @@ +import { Util } from "@igniteui/cli-core"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; +import * as ts from "typescript"; +import { IGNITEUI_ANGULAR_PACKAGE } from "../../../constants"; + +/** Package that provides the `ig` commands in projects created by `ig new` */ +export const CLI_PACKAGE = "igniteui-cli"; +/** Added by `ng add igniteui-angular`; projects created through the schematics keep it instead of igniteui-cli */ +const SCHEMATICS_PACKAGE = "@igniteui/angular-schematics"; + +/** AI config the ai-config step of `ng add igniteui-angular` writes; `ig new` and the ng-new schematic run their own */ +const NG_ADD_AI_CONFIG = [".agents", ".claude", "AGENTS.md", ".mcp.json"]; + +/** Reads a field of the newest published version that matches `spec` (`name@range`) */ +function npmView(spec: string, field: string) { + const result = JSON.parse(Util.execSync(`npm view "${spec}" ${field} --json`, { encoding: "utf8" }) as string); + return Array.isArray(result) ? result[result.length - 1] : result; +} + +/** + * Creates the project at `projectPath` with `ng new`, followed by `ng add angular-eslint` and `ng add igniteui-angular`, + * which also installs the packages. `igniteui-angular` comes from the templates' `IGNITEUI_ANGULAR_PACKAGE` range and + * Angular at the newest major that supports, so the generated project matches what the component templates use. + * Returns that Angular major. + */ +export function generateProject(dashName: string, projectPath: string): string { + const angularMajor = npmView(`@angular/core@${npmView(IGNITEUI_ANGULAR_PACKAGE, "peerDependencies")["@angular/core"]}`, + "version").split(".")[0]; + const ng = (command: string, cwd: string) => + Util.execSync(`npx -y @angular/cli@${angularMajor} ${command} --defaults --interactive=false`, { + cwd, + stdio: "inherit", + env: { ...process.env, NG_CLI_ANALYTICS: "false" } + }); + + // `ng new` refuses to run inside an Angular workspace, so it runs in a temp folder outside any + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "igx-new-")); + try { + ng(`new ${dashName} --directory ${dashName} --style=scss --routing --ssr=false ` + + // zone.js stays until the templates are verified zoneless + "--zoneless=false --ai-config=none --skip-git --skip-install --package-manager=npm", tmp); + fs.cpSync(path.join(tmp, dashName), projectPath, { recursive: true }); + } finally { + fs.rmSync(tmp, { recursive: true, force: true }); + } + + // angular-eslint majors follow Angular's + ng(`add angular-eslint@${angularMajor} --skip-confirmation`, projectPath); + ng(`add ${IGNITEUI_ANGULAR_PACKAGE} --skip-confirmation`, projectPath); + + for (const entry of NG_ADD_AI_CONFIG) { + fs.rmSync(path.join(projectPath, entry), { recursive: true, force: true }); + } + return angularMajor; +} + +/** + * Installs packages at their latest version, or at the version or range an entry names, + * which also brings the lock file and node_modules in line with package.json + */ +export function npmInstall(projectPath: string, packages: string[], dev = false) { + Util.execSync(`npm install ${dev ? "--save-dev " : ""}${packages.join(" ")} --no-audit --no-fund`, { + cwd: projectPath, + stdio: "inherit" + }); +} + +/** Applies our deviations from the angular.json `ng new` generates */ +export function patchAngularJson(json: any, config: { [key: string]: any }) { + const architect = json.projects[config.dashName].architect; + const build = architect.build; + // `public/` from `ng new` is served at the root; overlays keep their files in src/assets + build.options.assets.push("src/assets"); + if (!config.CustomTheme) { + build.options.styles.push(config.themePath); + } + build.configurations.production.budgets = [ + { type: "initial", maximumWarning: "5mb", maximumError: "40mb" }, + { type: "anyComponentStyle", maximumWarning: "8kB", maximumError: "2mb" } + ]; + build.configurations.development.fileReplacements = [{ + replace: "src/environments/environment.ts", + with: "src/environments/environment.development.ts" + }]; + architect.test.options = { ...architect.test.options, runnerConfig: true }; + architect["extract-i18n"] = { + builder: "@angular/build:extract-i18n", + options: { buildTarget: `${config.dashName}:build` } + }; + return json; +} + +/** Applies our deviations from the package.json `ng new` and `ng add` generate; `cliPackage` provides the CLI commands */ +export function patchPackageJson(pkg: any, cliPackage: string, cliVersion: string) { + const { dependencies: deps, devDependencies: devDeps } = pkg; + Object.assign(pkg.scripts, { start: "ng serve -o", test: "ng test --browsers=chromium" }); + // `ng add` saves `^`; the component templates are written for the IGNITEUI_ANGULAR_PACKAGE range + const [igxPackage, igxRange] = IGNITEUI_ANGULAR_PACKAGE.split(/@(?=[^\/]+$)/); + deps[igxPackage] = igxRange; + // provideAnimations in the app configs + deps["@angular/animations"] = deps["@angular/core"]; + // tests run in a real browser through @vitest/browser-playwright, which must match vitest exactly + delete devDeps.jsdom; + devDeps["@vitest/browser-playwright"] = devDeps.vitest; + if (cliPackage !== SCHEMATICS_PACKAGE) { + delete devDeps[SCHEMATICS_PACKAGE]; + } + devDeps[cliPackage] = `~${cliVersion}`; + return pkg; +} + +/** Updates a JSON file in place; tsconfig files may have comments, of which the leading ones are kept */ +export function updateJson(file: string, update: (json: any) => any) { + const text = fs.readFileSync(file, "utf8"); + const { config, error } = ts.parseConfigFileTextToJson(file, text); + if (error) { + throw new Error(`Could not parse ${file}: ${ts.flattenDiagnosticMessageText(error.messageText, "\n")}`); + } + const header = (ts.getLeadingCommentRanges(text, 0) || []).map(range => text.slice(range.pos, range.end) + "\n").join(""); + fs.writeFileSync(file, header + JSON.stringify(update(config), null, 2) + "\n"); +} diff --git a/packages/igx-templates/igx-ts/projects/side-nav-auth/files/package.json b/packages/igx-templates/igx-ts/projects/side-nav-auth/files/package.json deleted file mode 100644 index 8230742783..0000000000 --- a/packages/igx-templates/igx-ts/projects/side-nav-auth/files/package.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "name": "<%=dashName%>", - "version": "0.0.0", - "scripts": { - "ng": "ng", - "start": "ng serve -o", - "build": "ng build", - "watch": "ng build --watch --configuration development", - "test": "ng test --browsers=chromium", - "lint": "ng lint" - }, - "private": true, - "dependencies": { - "@angular/animations": "~22.2.0", - "@angular/common": "~22.2.0", - "@angular/compiler": "~22.2.0", - "@angular/core": "~22.2.0", - "@angular/forms": "~22.2.0", - "@angular/platform-browser": "~22.2.0", - "@angular/router": "~22.2.0", - "@igniteui/material-icons-extended": "^3.0.2", - "angular-auth-oidc-client": "~22.0.2", - "igniteui-angular": "~22.2.0", - "igniteui-i18n-core": "^1.0.5", - "minireset.css": "~0.0.7", - "rxjs": "~7.8.1", - "tslib": "~2.8.1", - "zone.js": "~0.16.0" - }, - "devDependencies": { - "@angular/build": "~22.2.0", - "@angular/cli": "~22.2.0", - "@angular/compiler-cli": "~22.2.0", - "@angular/language-service": "~22.2.0", - "@eslint/js": "^10.0.1", - "@types/facebook-js-sdk": "~3.3.6", - "@vitest/browser-playwright": "^5.0.0", - "angular-eslint": "~22.6.0", - "eslint": "^10.3.0", - "igniteui-cli": "~<%=cliVersion%>", - "playwright": "^1.58.2", - "prettier": "^3.8.1", - "typescript": "~6.0.2", - "typescript-eslint": "8.71.0", - "vitest": "^5.0.0" - } -} diff --git a/packages/igx-templates/igx-ts/projects/side-nav-auth/files/tsconfig.app.json b/packages/igx-templates/igx-ts/projects/side-nav-auth/files/tsconfig.app.json deleted file mode 100644 index 8fea7d1ab5..0000000000 --- a/packages/igx-templates/igx-ts/projects/side-nav-auth/files/tsconfig.app.json +++ /dev/null @@ -1,24 +0,0 @@ -/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ -/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "lib": [ - "es2019", - "dom" - ], - "outDir": "./out-tsc/app", - "types": [ - "facebook-js-sdk" - ] - }, - "files": [ - "src/main.ts" - ], - "include": [ - "src/**/*.d.ts" - ], - "exclude": [ - "src/**/*.spec.ts" - ] -} diff --git a/packages/igx-templates/igx-ts/projects/side-nav-auth/files/tsconfig.spec.json b/packages/igx-templates/igx-ts/projects/side-nav-auth/files/tsconfig.spec.json deleted file mode 100644 index f35045b962..0000000000 --- a/packages/igx-templates/igx-ts/projects/side-nav-auth/files/tsconfig.spec.json +++ /dev/null @@ -1,18 +0,0 @@ -/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ -/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "./out-tsc/spec", - "types": [ - "vitest/globals", - "node", - "facebook-js-sdk" - ], - "module": "esnext" - }, - "include": [ - "src/**/*.d.ts", - "src/**/*.spec.ts" - ] -} diff --git a/packages/igx-templates/igx-ts/projects/side-nav-auth/index.ts b/packages/igx-templates/igx-ts/projects/side-nav-auth/index.ts index a99790d89c..dedbc95279 100644 --- a/packages/igx-templates/igx-ts/projects/side-nav-auth/index.ts +++ b/packages/igx-templates/igx-ts/projects/side-nav-auth/index.ts @@ -1,7 +1,16 @@ import { ProjectTemplate } from "@igniteui/cli-core"; import * as path from "path"; +import { ProjectExtras } from "../_base"; import { SideNavProject } from "../side-nav"; +/** What the authentication files need, shared with side-nav-mini-auth */ +export const authExtras = (angularMajor: string): ProjectExtras => ({ + // both follow Angular's majors; at their latest they would outrun the project's Angular once a new major ships + dependencies: [`angular-auth-oidc-client@${angularMajor}`], + devDependencies: [`@angular/language-service@${angularMajor}`, "@types/facebook-js-sdk"], + types: ["facebook-js-sdk"] +}); + export class AuthSideProject extends SideNavProject implements ProjectTemplate { public id: string = "side-nav-auth"; public name = "Side navigation + login"; @@ -11,6 +20,7 @@ export class AuthSideProject extends SideNavProject implements ProjectTemplate { public projectType: string = "igx-ts"; public hasExtraConfiguration = false; public isHidden = true; + protected extras = authExtras; public get templatePaths() { return [...super.templatePaths, path.join(__dirname, "files")]; diff --git a/packages/igx-templates/igx-ts/projects/side-nav-mini-auth/index.ts b/packages/igx-templates/igx-ts/projects/side-nav-mini-auth/index.ts index 2d5388151a..a256b7d4fc 100644 --- a/packages/igx-templates/igx-ts/projects/side-nav-mini-auth/index.ts +++ b/packages/igx-templates/igx-ts/projects/side-nav-mini-auth/index.ts @@ -1,6 +1,7 @@ import { ProjectTemplate } from "@igniteui/cli-core"; import * as path from "path"; import { SideNavMiniProject } from "../side-nav-mini"; +import { authExtras } from "../side-nav-auth"; export class SideNavMiniAuthProject extends SideNavMiniProject implements ProjectTemplate { public id: string = "side-nav-mini-auth"; @@ -11,6 +12,7 @@ export class SideNavMiniAuthProject extends SideNavMiniProject implements Projec public projectType: string = "igx-ts"; public hasExtraConfiguration = false; public isHidden = true; + protected extras = authExtras; public get templatePaths() { return [ diff --git a/packages/ng-schematics/src/app-projects/index.ts b/packages/ng-schematics/src/app-projects/index.ts deleted file mode 100644 index 09486255f9..0000000000 --- a/packages/ng-schematics/src/app-projects/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { - apply, chain, MergeStrategy, mergeWith, Rule, - SchematicContext, template, Tree, url } from "@angular-devkit/schematics"; -import { Util } from "@igniteui/cli-core"; -import { join } from "path"; -import { NewProjectOptions } from "./schema"; - -export default function(options: NewProjectOptions): Rule { - return (tree: Tree, _context: SchematicContext) => { - let config: { [key: string]: any }; - let templateId = options.templateId; - let templatePaths = options.templatePaths || []; - - if (options.projTemplate) { - config = options.projTemplate.generateConfig(options.name, options.theme); - templateId = options.projTemplate.id; - templatePaths = options.projTemplate.templatePaths; - } else { - if (!options.config || !templatePaths.length) { - throw new Error("Missing app-projects template data."); - } - config = options.config; - } - // update version with schematics one - config.cliVersion = Util.version(join(__dirname, "../../package.json")); - return chain([ - (_tree: Tree, context: SchematicContext) => { - context.logger.debug(`Project template: ${templateId}`); - }, - ...templatePaths.map(templatePath => - mergeWith( - apply(url(Util.relativePath(__filename, templatePath, true)), [ - template(config) - ] - ), MergeStrategy.Overwrite) - ), - (host: Tree, _hostContext: SchematicContext) => { - const content = host.read("./package.json")!.toString(); - host.overwrite("./package.json", content.replace(`"igniteui-cli":`, `"@igniteui/angular-schematics":`)); - } - ]); - }; -} diff --git a/packages/ng-schematics/src/app-projects/index_spec.ts b/packages/ng-schematics/src/app-projects/index_spec.ts deleted file mode 100644 index 3285f7025f..0000000000 --- a/packages/ng-schematics/src/app-projects/index_spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Tree } from "@angular-devkit/schematics"; -import { SchematicTestRunner } from "@angular-devkit/schematics/testing"; -import * as path from "path"; - -const collectionPath = path.join(__dirname, "../collection.json"); - -describe("app-projects", () => { - it("works", async () => { - const runner = new SchematicTestRunner("schematics", collectionPath); - const tree = Tree.empty(); - const mockOptions = { - name: "mock-name", - config: {}, - templateId: "mock-id", - templatePaths: ["mock-template"], - theme: "mock-theme" - }; - spyOn(tree, "read").and.returnValue(Buffer.from(`Mock package content "igniteui-cli":`)); - spyOn(tree, "overwrite"); - await runner.runSchematic("app-projects", mockOptions, tree); - - expect(tree.read).toHaveBeenCalledWith("./package.json"); - expect(tree.overwrite).toHaveBeenCalledWith("./package.json", `Mock package content "@igniteui/angular-schematics":`); - }); -}); diff --git a/packages/ng-schematics/src/app-projects/schema.ts b/packages/ng-schematics/src/app-projects/schema.ts deleted file mode 100644 index dd9322e214..0000000000 --- a/packages/ng-schematics/src/app-projects/schema.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ProjectTemplate } from "@igniteui/cli-core"; - -export abstract class NewProjectOptions { - public projTemplate?: ProjectTemplate; - public templateId?: string; - public templatePaths?: string[]; - public config?: { [key: string]: any }; - public theme: string; - public name: string; -} diff --git a/packages/ng-schematics/src/collection.json b/packages/ng-schematics/src/collection.json index 6dfaebdaf5..216954a925 100644 --- a/packages/ng-schematics/src/collection.json +++ b/packages/ng-schematics/src/collection.json @@ -7,11 +7,6 @@ "schema": "./ng-new/schema.json", "hidden": true }, - "app-projects": { - "description": "A schematic using a source and a schema to validate options.", - "factory": "./app-projects/index", - "hidden": true - }, "start": { "description": "Run npm start", "factory": "./start/index", diff --git a/packages/ng-schematics/src/ng-new/index.ts b/packages/ng-schematics/src/ng-new/index.ts index 786df3b23f..e26b575528 100644 --- a/packages/ng-schematics/src/ng-new/index.ts +++ b/packages/ng-schematics/src/ng-new/index.ts @@ -1,10 +1,6 @@ import { - apply, + callRule, chain, - empty, - MergeStrategy, - mergeWith, - move, Rule, schematic, SchematicContext, @@ -12,9 +8,10 @@ import { TaskId, Tree } from "@angular-devkit/schematics"; import { NodePackageInstallTask, RepositoryInitializerTask, RunSchematicTask } from "@angular-devkit/schematics/tasks"; +// not exported from the package root; start/index.ts imports it the same way +import { ScopedTree } from "@angular-devkit/schematics/src/tree/scoped"; import { App, GoogleAnalytics, ProjectLibrary, ProjectTemplate, TEMPLATE_MANAGER, Util } from "@igniteui/cli-core"; -import { defer, Observable } from "rxjs"; -import { NewProjectOptions } from "../app-projects/schema"; +import { defer, map, Observable } from "rxjs"; import { SchematicsPromptSession } from "../prompt/SchematicsPromptSession"; import { SchematicsTemplateManager } from "../SchematicsTemplateManager"; import { setVirtual } from "../utils/NgFileSystem"; @@ -25,6 +22,12 @@ interface IgxSchematicContext extends SchematicContext { theme: string; } +/** Runs a rule with the project folder as the root of the tree */ +function inProject(getName: () => string, rule: Rule): Rule { + return (tree: Tree, context: SchematicContext) => + callRule(rule, new ScopedTree(tree, getName()), context).pipe(map(() => tree)); +} + export function newProject(options: OptionsSchema): Rule { return (_host: Tree, _hostContext: IgxSchematicContext) => { App.initialize("angular-cli"); @@ -33,7 +36,7 @@ export function newProject(options: OptionsSchema): Rule { cd: "New" }); let projLibrary: ProjectLibrary; - let projectOptions: NewProjectOptions; + let theme: string; const templateManager = new SchematicsTemplateManager(); App.container.set(TEMPLATE_MANAGER, templateManager); const prompt = new SchematicsPromptSession(); @@ -94,22 +97,13 @@ export function newProject(options: OptionsSchema): Rule { throw new SchematicsException(`template with id '${options.template}' not found`); } } - const theme = projLibrary.themes[themeIndex]; + theme = projLibrary.themes[themeIndex]; Util.log(`Project Name: ${options.name}, theme ${theme}`); - // project options: // cache available views and components, same as in component Schematic const components = projLibrary.components; const views = (projLibrary as any).customTemplates; - projectOptions = { - config: projTemplate.generateConfig(options.name, theme), - templateId: projTemplate.id, - templatePaths: [...projTemplate.templatePaths], - theme, - name: options.name - }; - GoogleAnalytics.post({ t: "event", ec: "$ng new", @@ -125,42 +119,36 @@ export function newProject(options: OptionsSchema): Rule { return tree; }); }, - mergeWith( - apply(empty(), [ - // TODO: Task chain based on @schematics/angular ng-new schematic - // externalSchematic("@schematics/angular", "workspace", { name: options.name }), - // externalSchematic("@schematics/angular", "application", { - // projectRoot: "", name: options.name, skipInstall: true, routing: true, style: "scss" - // }), - (tree: Tree, _context: IgxSchematicContext) => { - setVirtual(tree); - // extend project entry point: - return schematic("app-projects", projectOptions); - }, - (tree: Tree, _context: IgxSchematicContext) => { - // extend project entry point: - // tree.create("ignite-ui-cli.json", JSON.stringify({ theme: context.theme })); - if (tree.exists("gitignore")) { - tree.rename("gitignore", ".gitignore"); - } - }, - // run full schematic for args/prompts - schematic("ai-config", {}), - (tree: Tree, context: IgxSchematicContext) => { - if (!nameProvided) { - return defer(async () => { - prompt.setContext(context, tree, options.name as string); - await prompt.chooseActionLoop(projLibrary); - return tree; - }); - } - }, - (_tree: Tree, _context: IgxSchematicContext) => { - // move late so name can be resolved before use - return move(options.name!); + (tree: Tree, _context: IgxSchematicContext): Observable => { + // the same scaffold as `ig new`: the Angular CLI creates the project on disk, the tree reads it from there + return defer(async () => { + const schematicsPackage = require("../../package.json"); + const success = await projTemplate.scaffold!({ + name: options.name!, + theme, + skipInstall: options.skipInstall, + skipGit: true, + // `ng add igniteui-angular` adds this package to the project + extraConfig: { cliPackage: schematicsPackage.name, cliVersion: schematicsPackage.version } + }); + if (!success) { + throw new SchematicsException("Project creation failed."); } - ]), MergeStrategy.Overwrite - ), + return tree; + }); + }, + // run full schematic for args/prompts + inProject(() => options.name!, schematic("ai-config", {})), + inProject(() => options.name!, (projectTree: Tree, context: SchematicContext) => { + if (!nameProvided) { + return defer(async () => { + setVirtual(projectTree); + prompt.setContext(context, projectTree, options.name as string); + await prompt.chooseActionLoop(projLibrary); + return projectTree; + }); + } + }), (tree: Tree, _context: IgxSchematicContext) => { if (prompt.userAnswers && prompt.userAnswers.get("upgradePackages")) { return defer(async () => { diff --git a/packages/ng-schematics/src/ng-new/index_spec.ts b/packages/ng-schematics/src/ng-new/index_spec.ts index b288b24b78..f754727f84 100644 --- a/packages/ng-schematics/src/ng-new/index_spec.ts +++ b/packages/ng-schematics/src/ng-new/index_spec.ts @@ -1,15 +1,52 @@ -import { SchematicContext, Tree } from "@angular-devkit/schematics"; +import { Tree } from "@angular-devkit/schematics"; import { NodePackageTaskOptions } from "@angular-devkit/schematics/tasks/package-manager/options"; import { RepositoryInitializerTaskOptions } from "@angular-devkit/schematics/tasks/repo-init/options"; import { RunSchematicTaskOptions } from "@angular-devkit/schematics/tasks/run-schematic/options"; import { SchematicTestRunner, UnitTestTree } from "@angular-devkit/schematics/testing"; -import { BaseTemplate, GoogleAnalytics, ProjectLibrary, ProjectTemplate, Template } from "@igniteui/cli-core"; +import { BaseTemplate, GoogleAnalytics, ProjectLibrary, ProjectTemplate, ScaffoldOptions, Template } from "@igniteui/cli-core"; import * as path from "path"; -import * as AppProjectSchematic from "../app-projects/index"; import { SchematicsPromptSession } from "../prompt/SchematicsPromptSession"; const collectionPath = path.join(__dirname, "../collection.json"); +/** Serves a library with a single project template, whose scaffold succeeds without creating anything */ +function mockProjectInLibrary(): Partial { + const mockProject: Partial = { + id: "mock-template-id", + framework: "angular", + projectType: "ts", + templatePaths: ["/path/to/template"], + generateConfig: jasmine.createSpy().and.returnValue({}), + upgradeIgniteUIPackages: jasmine.createSpy().and.returnValue(Promise.resolve(true)), + scaffold: jasmine.createSpy().and.returnValue(Promise.resolve(true)) + }; + + const mockLibrary: Partial = { + name: "mock-library", + themes: ["mock-theme"], + components: [], + projectIds: ["another-mock"], + projects: [mockProject as ProjectTemplate], + templates: [], + projectType: "ts" + }; + + spyOn(SchematicsPromptSession.prototype, "getProjectLibraryByType") + .and.returnValue(Promise.resolve(mockLibrary as ProjectLibrary)); + return mockProject; +} + +/** The scaffold options the schematic passes for a project created through the schematics */ +function expectedScaffoldOptions(name: string, theme: string, skipInstall: boolean): ScaffoldOptions { + return { + name, + theme, + skipInstall, + skipGit: true, + extraConfig: { cliPackage: "@igniteui/angular-schematics", cliVersion: jasmine.any(String) } + }; +} + describe("Schematics ng-new", () => { beforeAll(() => { @@ -43,7 +80,8 @@ describe("Schematics ng-new", () => { const mockProjectTemplate: ProjectTemplate = { ...mockBaseTemplate, installModules: jasmine.createSpy().and.callFake(() => {}), - upgradeIgniteUIPackages: jasmine.createSpy().and.returnValue(Promise.resolve(true)) + upgradeIgniteUIPackages: jasmine.createSpy().and.returnValue(Promise.resolve(true)), + scaffold: jasmine.createSpy().and.returnValue(Promise.resolve(true)) }; const mockTemplate: Template = { @@ -103,8 +141,7 @@ describe("Schematics ng-new", () => { const mockSession = { // regression: AI config (ai-config schematic) must run before the interactive // setup/action loop, otherwise "Next Steps" prints before the AI-agents prompt. - // The project isn't moved into its named subdirectory until after the loop runs, - // so the file still sits at the tree root at this point. + // Both run with the project folder as the root of the tree. chooseActionLoop: spyOn(SchematicsPromptSession.prototype, "chooseActionLoop") .and.callFake(function (this: SchematicsPromptSession) { expect(this.tree.exists(".mcp.json")).toBeTruthy(); @@ -128,18 +165,13 @@ describe("Schematics ng-new", () => { set: () => void 0 }); - spyOn(AppProjectSchematic, "default").and.returnValue((currentTree: Tree, _context: SchematicContext) => { - currentTree.create("gitignore", ""); - return currentTree; - }); - const tree = await runner.runSchematic("ng-new", { version: "8.0.3" }, myTree); for (const mockFunc of Object.entries(mockSession)) { expect(mockFunc[1]).toHaveBeenCalled(); } - expect(AppProjectSchematic.default).toHaveBeenCalled(); + expect(mockProjectTemplate.scaffold).toHaveBeenCalledOnceWith(expectedScaffoldOptions(workingDirectory, "mock-theme", false)); expect(tree.files.length).toBeGreaterThanOrEqual(2); - expect(tree.exists(`${workingDirectory}/.gitignore`)).toBeTruthy(); + expect(tree.files.every(file => file.startsWith(`/${workingDirectory}/`))).toBeTrue(); const taskOptions = runner.tasks.map(task => task.options); const expectedInstall: NodePackageTaskOptions = { command: "install", @@ -174,27 +206,7 @@ describe("Schematics ng-new", () => { const myTree = Tree.empty(); const workingDirectory = "my-test-project"; - const mockProject: Partial = { - id: "mock-template-id", - framework: "angular", - projectType: "ts", - templatePaths: ["/path/to/template"], - generateConfig: jasmine.createSpy().and.returnValue({}), - upgradeIgniteUIPackages: jasmine.createSpy().and.returnValue(Promise.resolve(true)) - }; - - const mockLibrary: Partial = { - name: "mock-library", - themes: ["mock-theme"], - components: [], - projectIds: ["another-mock"], - projects: [mockProject as ProjectTemplate], - templates: [], - projectType: "ts" - }; - - spyOn(SchematicsPromptSession.prototype, "getProjectLibraryByType") - .and.returnValue((Promise.resolve(mockLibrary as ProjectLibrary))); + const mockProject = mockProjectInLibrary(); const userAnswers = new Map(); userAnswers.set("upgradePackages", true); @@ -204,15 +216,10 @@ describe("Schematics ng-new", () => { set: () => void 0 }); - spyOn(AppProjectSchematic, "default").and.returnValue((currentTree: Tree, _context: SchematicContext) => { - currentTree.create("gitignore", ""); - return currentTree; - }); - const tree = await runner.runSchematic("ng-new", { version: "8.0.3", name: workingDirectory }, myTree); - expect(AppProjectSchematic.default).toHaveBeenCalled(); + expect(mockProject.scaffold).toHaveBeenCalledOnceWith(expectedScaffoldOptions(workingDirectory, "mock-theme", false)); expect(tree.files.length).toBeGreaterThanOrEqual(2); - expect(tree.exists(`${workingDirectory}/.gitignore`)).toBeTruthy(); + expect(tree.files.every(file => file.startsWith(`/${workingDirectory}/`))).toBeTrue(); const taskOptions = runner.tasks.map(task => task.options); const expectedInstall: NodePackageTaskOptions = { command: "install", @@ -239,10 +246,7 @@ describe("Schematics ng-new", () => { const mcpFilePath = `${workingDirectory}/.mcp.json`; function setupAndRun(runner: SchematicTestRunner, myTree: Tree): Promise { - spyOn(AppProjectSchematic, "default").and.returnValue((currentTree: Tree, _context: SchematicContext) => { - currentTree.create("gitignore", ""); - return currentTree; - }); + mockProjectInLibrary(); const userAnswers = new Map(); userAnswers.set("upgradePackages", false); diff --git a/packages/ng-schematics/src/ng-new/schema.json b/packages/ng-schematics/src/ng-new/schema.json index 805148d4b1..7ecb1b95f0 100644 --- a/packages/ng-schematics/src/ng-new/schema.json +++ b/packages/ng-schematics/src/ng-new/schema.json @@ -21,7 +21,7 @@ } }, "skipInstall": { - "description": "Do not automatically install packages.", + "description": "Do not install packages after the project is set up. `ng add igniteui-angular` still installs the packages the new project starts with.", "type": "boolean", "default": false, "alias": "si" diff --git a/scripts/generate-angular-base.mjs b/scripts/generate-angular-base.mjs deleted file mode 100644 index 1c27c9eccc..0000000000 --- a/scripts/generate-angular-base.mjs +++ /dev/null @@ -1,299 +0,0 @@ -// Regenerates the Angular-owned files of the igx-ts project templates from a fresh `ng new`, -// followed by `ng add angular-eslint` and `ng add igniteui-angular`. -// -// node scripts/generate-angular-base.mjs [--angular ] [--igniteui ] [--check] -// -// --angular @angular/cli range for `ng new`; defaults to the `@angular/cli` pin in `_base/files/package.json` -// --igniteui igniteui-angular range for `ng add`; defaults to the `igniteui-angular` pin in `_base/files/package.json` -// --check writes nothing in the repository; lists what a run would change and exits with 1 if anything would -// -// COPIED files are taken as they are; angular.json and package.json are patched with our deviations, -// and side-nav-auth's package.json is the `_base` one plus the packages only that project uses. -// The other generated files have hand-maintained counterparts. Their upstream content is fingerprinted -// in UPSTREAM_HASHES, so a run lists the ones `ng new` or `ng add` changed since the last run. - -import { execSync } from "node:child_process"; -import crypto from "node:crypto"; -import fs from "node:fs"; -import { createRequire } from "node:module"; -import os from "node:os"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const ROOT = path.resolve(fileURLToPath(import.meta.url), "../.."); -const PROJECTS = path.join(ROOT, "packages/igx-templates/igx-ts/projects"); -const BASE = path.join(PROJECTS, "_base/files"); -const SIDE_NAV_AUTH = path.join(PROJECTS, "side-nav-auth/files"); -const UPSTREAM_HASHES = path.join(ROOT, "scripts/generate-angular-base.upstream.json"); -const NAME = "igx-base-placeholder"; -const DASH_NAME = "<%=dashName%>"; -const USAGE = "node scripts/generate-angular-base.mjs [--angular ] [--igniteui ] [--check]"; - -const COPIED = [ - ".editorconfig", - ".gitignore", - ".prettierrc", - ".vscode/extensions.json", - ".vscode/launch.json", - ".vscode/tasks.json", - "public/favicon.ico", - "src/main.ts", - "tsconfig.json", - "tsconfig.app.json", - "tsconfig.spec.json" -]; -const PATCHED = ["angular.json", "package.json"]; -const IGNORED = [ - "node_modules", ".angular", "package-lock.json", - // AI config from @igniteui/angular-schematics; `ig ai-config` owns it in our projects - ".agents", ".claude", ".mcp.json", "AGENTS.md" -]; - -// Packages we add on top of `ng new` and `ng add`, at their version in the current `_base` package.json -const EXTRAS = { - dependencies: ["@angular/animations"], // provideAnimations in the app configs - devDependencies: ["igniteui-cli", "playwright"] // playwright: the browser for @vitest/browser-playwright -}; -const DEPENDENCY_KEYS = ["dependencies", "devDependencies"]; - -function patchAngularJson(json) { - const architect = json.projects[DASH_NAME].architect; - const build = architect.build; - // `public/` from `ng new` is served at the root; overlays keep their files in src/assets - build.options.assets.push("src/assets"); - build.configurations.production.budgets = [ - { type: "initial", maximumWarning: "5mb", maximumError: "40mb" }, - { type: "anyComponentStyle", maximumWarning: "8kB", maximumError: "2mb" } - ]; - build.configurations.development.fileReplacements = [{ - replace: "src/environments/environment.ts", - with: "src/environments/environment.development.ts" - }]; - architect.test.options = { ...architect.test.options, runnerConfig: true }; - architect["extract-i18n"] = { - builder: "@angular/build:extract-i18n", - options: { buildTarget: `${DASH_NAME}:build` } - }; - - // the default theme is appended to the styles array by the template engine - const styles = `"src/styles.scss"`; - const text = toJson(json); - if (text.split(styles).length !== 2) { - throw new Error(`Expected exactly one ${styles} entry in angular.json`); - } - return text.replace(styles, `${styles}<%=DefaultTheme%>`); -} - -function patchPackageJson(pkg, { current, igniteuiVersion, installedVersion, semver }) { - const { dependencies: deps, devDependencies: devDeps } = pkg; - delete pkg.packageManager; // records the npm version of the machine that ran `ng new` - Object.assign(pkg.scripts, { start: "ng serve -o", test: "ng test --browsers=chromium", lint: "ng lint" }); - delete devDeps.jsdom; // tests run in a real browser through @vitest/browser-playwright - // `ig new` projects use igniteui-cli instead; the schematics flow swaps it back (app-projects) - delete devDeps["@igniteui/angular-schematics"]; - // `ng add angular-eslint` writes its exact version; it follows Angular minors. - // Normalized before the pins below are kept, so a retained pin is never rewritten - const eslint = semver.minVersion(devDeps["angular-eslint"]); - devDeps["angular-eslint"] = `~${eslint.major}.${eslint.minor}.0`; - - // a package already in `_base` keeps its pin while that admits the version installed in the generated - // project, so a run moves only the pins the new project needs moved and never loosens the others - for (const key of DEPENDENCY_KEYS) { - for (const name of Object.keys(pkg[key])) { - const pin = current[key][name]; - if (pin && semver.satisfies(installedVersion(name), pin)) { - pkg[key][name] = pin; - } - } - } - deps["igniteui-angular"] = igniteuiVersion; - - for (const [key, names] of Object.entries(EXTRAS)) { - for (const name of names) { - if (!current[key][name]) { - throw new Error(`${name} is missing from the current _base package.json`); - } - pkg[key][name] = current[key][name]; - } - } - - // all @angular/* packages share the @angular/core version, on a ~ range - const angularVersion = deps["@angular/core"].replace(/^\^/, "~"); - for (const key of DEPENDENCY_KEYS) { - for (const name of Object.keys(pkg[key]).filter(n => n.startsWith("@angular/"))) { - pkg[key][name] = angularVersion; - } - } - // the browser provider must match vitest exactly - devDeps["@vitest/browser-playwright"] = devDeps.vitest; - return sortDependencies(pkg); -} - -// side-nav-auth carries its own copy of the manifest: the generated `_base` one plus the packages only it uses -function sideNavAuthPackageJson(base, currentBase, currentOwn) { - const pkg = structuredClone(base); - for (const key of DEPENDENCY_KEYS) { - for (const [name, range] of Object.entries(currentOwn[key])) { - if (!(name in currentBase[key])) { - pkg[key][name] = name.startsWith("@angular/") ? base.dependencies["@angular/core"] : range; - } - } - } - return sortDependencies(pkg); -} - -function sortDependencies(pkg) { - for (const key of DEPENDENCY_KEYS) { - pkg[key] = Object.fromEntries(Object.entries(pkg[key]).sort(([a], [b]) => a.localeCompare(b))); - } - return pkg; -} - -function toJson(value) { - return JSON.stringify(value, null, 2) + "\n"; -} - -function readJson(file) { - return JSON.parse(fs.readFileSync(file, "utf8")); -} - -// dotfiles are stored as `__dot__name`, except those already kept under their own name -function templatePath(root, file) { - const literal = path.join(root, file); - return fs.existsSync(literal) - ? literal - : path.join(root, ...file.split("/").map(part => part.replace(/^\./, "__dot__"))); -} - -// writes the outputs that differ from the template files (none with `check`) and returns their paths; -// text keeps the line endings of the file it replaces, so only real changes show in the diff -function writeOutputs(outputs, check) { - const changed = []; - for (const [target, content] of outputs) { - const existing = fs.existsSync(target) ? fs.readFileSync(target) : null; - const crlf = typeof content === "string" && existing?.includes("\r\n"); - const data = Buffer.from(crlf ? content.replace(/\n/g, "\r\n") : content); - if (existing?.equals(data)) { - continue; - } - changed.push(path.relative(ROOT, target)); - if (!check) { - fs.mkdirSync(path.dirname(target), { recursive: true }); - fs.writeFileSync(target, data); - } - } - return changed; -} - -function listFiles(dir, prefix = "") { - return fs.readdirSync(dir, { withFileTypes: true }) - .filter(entry => !IGNORED.includes(entry.name)) - .flatMap(entry => entry.isDirectory() - ? listFiles(path.join(dir, entry.name), `${prefix}${entry.name}/`) - : [`${prefix}${entry.name}`]); -} - -// lists the upstream changes, with a kept copy of each new upstream version to compare against ours -function reportUpstreamChanges(files, hashes, read) { - const kept = fs.mkdtempSync(path.join(os.tmpdir(), "igx-base-upstream-")); - console.log("\nChanged upstream since the last run; review our hand-maintained counterparts:"); - for (const file of files) { - if (!(file in hashes)) { - console.log(` ${file}: no longer generated`); - continue; - } - const copy = path.join(kept, file); - fs.mkdirSync(path.dirname(copy), { recursive: true }); - fs.writeFileSync(copy, read(file)); - const ours = templatePath(BASE, file); - console.log(` ${file}: upstream ${copy}, ours ${fs.existsSync(ours) ? path.relative(ROOT, ours) : "(none in _base)"}`); - } -} - -function parseArgs(argv) { - const args = { check: false }; - for (let i = 0; i < argv.length; i++) { - const arg = argv[i]; - const value = argv[i + 1]; - if (arg === "--check") { - args.check = true; - } else if ((arg === "--angular" || arg === "--igniteui") && value && !value.startsWith("-")) { - args[arg.slice(2)] = value; - i++; - } else { - throw new Error(`Unknown argument or missing value: ${arg}\n${USAGE}`); - } - } - return args; -} - -const args = parseArgs(process.argv.slice(2)); -const currentPackage = readJson(templatePath(BASE, "package.json")); -const angularVersion = args.angular || currentPackage.devDependencies["@angular/cli"]; -const igniteuiVersion = args.igniteui || currentPackage.dependencies["igniteui-angular"]; -const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "igx-base-")); - -try { - const generated = path.join(tmp, NAME); - const read = file => fs.readFileSync(path.join(generated, file), "utf8").replace(/\r\n/g, "\n").split(NAME).join(DASH_NAME); - const ng = (command, cwd) => execSync(`npx -y "@angular/cli@${angularVersion}" ${command} --defaults --interactive=false`, - { cwd, stdio: "inherit" }); - - console.log(`Running ng new with @angular/cli@${angularVersion} in ${tmp}`); - // zone.js stays until the templates are verified zoneless - ng(`new ${NAME} --style=scss --routing --ssr=false --zoneless=false --ai-config=none --skip-install --skip-git`, tmp); - const ngNewPackage = JSON.parse(read("package.json")); - // angular-eslint majors follow Angular's; its ng-add writes the newest release of that major - const angularMajor = ngNewPackage.dependencies["@angular/core"].match(/\d+/)[0]; - for (const pkg of [`angular-eslint@${angularMajor}`, `igniteui-angular@${igniteuiVersion}`]) { - console.log(`Running ng add ${pkg}`); - ng(`add "${pkg}" --skip-confirmation`, generated); - } - - const basePackage = patchPackageJson(JSON.parse(read("package.json")), { - current: currentPackage, - igniteuiVersion, - installedVersion: name => readJson(path.join(generated, "node_modules", name, "package.json")).version, - semver: createRequire(path.join(generated, "package.json"))("semver") // comes with the Angular CLI - }); - const sideNavAuthPackage = sideNavAuthPackageJson(basePackage, currentPackage, - readJson(templatePath(SIDE_NAV_AUTH, "package.json"))); - - // the other generated files are kept by hand: fingerprint them to spot upstream changes - const hashes = Object.fromEntries(listFiles(generated) - .filter(file => !COPIED.includes(file) && !PATCHED.includes(file)) - .sort() - .map(file => [file, crypto.createHash("sha256").update(read(file)).digest("hex")])); - const previous = fs.existsSync(UPSTREAM_HASHES) ? readJson(UPSTREAM_HASHES) : null; - const upstreamChanged = previous - ? [...new Set([...Object.keys(previous), ...Object.keys(hashes)])].filter(file => previous[file] !== hashes[file]) - : []; - - // the fingerprints are an output like the templates, so a missing or outdated baseline fails --check - const changed = writeOutputs(new Map([ - ...COPIED.map(file => [ - templatePath(BASE, file), - file.endsWith(".ico") ? fs.readFileSync(path.join(generated, file)) : read(file) - ]), - [templatePath(BASE, "angular.json"), patchAngularJson(JSON.parse(read("angular.json")))], - [templatePath(BASE, "package.json"), toJson(basePackage)], - [templatePath(SIDE_NAV_AUTH, "package.json"), toJson(sideNavAuthPackage)], - [UPSTREAM_HASHES, toJson(hashes)] - ]), args.check); - - console.log(changed.length - ? `\n${args.check ? "Would change" : "Updated"}:\n${changed.map(file => ` ${file}`).join("\n")}` - : "\nGenerated files are up to date."); - if (!previous) { - const records = args.check ? "would record" : "records"; - console.log(`\nNo ${path.relative(ROOT, UPSTREAM_HASHES)} yet; a run ${records} the upstream baseline.`); - } else if (upstreamChanged.length) { - reportUpstreamChanges(upstreamChanged, hashes, read); - } - - if (args.check && changed.length) { - process.exitCode = 1; - } -} finally { - fs.rmSync(tmp, { recursive: true, force: true }); -} diff --git a/scripts/generate-angular-base.upstream.json b/scripts/generate-angular-base.upstream.json deleted file mode 100644 index aabba60626..0000000000 --- a/scripts/generate-angular-base.upstream.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "README.md": "65b7d752f4e2c89781200157db5ddca8914bf8c41cc63d293bb6a5975939a746", - "eslint.config.js": "c92a49968fab95fd7ea538ae9e7501567682fd3cb4c202775ef5a7ed2e49a4a7", - "ignite-ui-cli.json": "457cc9ac4b75c5af8d2c76e4f01305eda83c28fc69dc7bae4a8c4570a461d1af", - "src/app/app.config.ts": "c2292f722fbf7d2c85b56115e1d5d3c21eb8e0587704b0994ae8ed394250187a", - "src/app/app.html": "4d90c16abd4ac0ea5c45e5b3a2835d966ac5c2e4cc6b60131003452e6e4fc328", - "src/app/app.routes.ts": "e2ef74b4c1f7f52a30a417c6808f01b2a29e69bb0d20302527ae5001607f7180", - "src/app/app.scss": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "src/app/app.spec.ts": "e7a2cfdab258441ef2a2811b96acb881fadbd1107b510eddbd566ab86cc4e832", - "src/app/app.ts": "fb4d23cefe9231aff51d721cbfbf0b6a169c6176a7f79405bb1a492b5078f263", - "src/index.html": "f9ac5fbba4459f93c098aee211803f0fd925f926ee92bbb19d06db07e883e5fe", - "src/styles.scss": "584decca418c77fc010012616fa56c76d3db2ee17dc8d67fd16e197f859c7ce8" -} diff --git a/scripts/versionScript.ts b/scripts/versionScript.ts index f2eb514c70..916462ab18 100644 --- a/scripts/versionScript.ts +++ b/scripts/versionScript.ts @@ -1,6 +1,7 @@ import { readFileSync, writeFileSync } from "fs"; import * as path from "path"; +import { IGNITEUI_ANGULAR_PACKAGE } from "../packages/igx-templates/constants"; import { generateChangelog } from "./changelogScript"; let version: string; @@ -11,8 +12,8 @@ const coreVersionedPackages = ["@igniteui/cli-core", "@igniteui/mcp-server"]; const DEP_RANGE_PREFIX = "~"; function getVersion() { - const igxPackageJson = JSON.parse(readFileSync("packages/igx-templates/igx-ts/projects/_base/files/package.json", "utf-8")); - const versionIG = igxPackageJson.dependencies["igniteui-angular"].replace(/^[^0-9]+/, ""); + // the igniteui-angular range the templates use, e.g. "igniteui-angular@~22.2.0" + const versionIG = IGNITEUI_ANGULAR_PACKAGE.split("@").pop()!.replace(/^[^0-9]+/, ""); const majorMinor = versionIG.split(".").slice(0, 2).join("."); const versionCLI = JSON.parse(readFileSync("packages/core/package.json").toString()); coreVersion = versionCLI.version; From 32fafe249633393501457633d372da7f3e23552c Mon Sep 17 00:00:00 2001 From: mstoyanova Date: Fri, 23 Oct 2026 16:42:23 +0300 Subject: [PATCH 07/10] feat: reject dry runs in ng-new schematic and add corresponding test --- packages/ng-schematics/src/ng-new/index.ts | 12 +++++++++++- packages/ng-schematics/src/ng-new/index_spec.ts | 16 ++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/ng-schematics/src/ng-new/index.ts b/packages/ng-schematics/src/ng-new/index.ts index e26b575528..f04519502e 100644 --- a/packages/ng-schematics/src/ng-new/index.ts +++ b/packages/ng-schematics/src/ng-new/index.ts @@ -28,8 +28,18 @@ function inProject(getName: () => string, rule: Rule): Rule { callRule(rule, new ScopedTree(tree, getName()), context).pipe(map(() => tree)); } +/** Whether the workflow runs with `--dry-run`, which workflows keep to themselves: BaseWorkflow, which the Angular CLI's extends, in `_dryRun` */ +function isDryRun(context: SchematicContext): boolean { + return !!(context.engine.workflow as any)?._dryRun; +} + export function newProject(options: OptionsSchema): Rule { - return (_host: Tree, _hostContext: IgxSchematicContext) => { + return (_host: Tree, hostContext: IgxSchematicContext) => { + // the scaffold writes the project and installs its packages on disk, past the workflow's dry-run sink and task skipping + if (isDryRun(hostContext)) { + throw new SchematicsException("--dry-run is not supported: `ng new` and `ng add igniteui-angular` " + + "create the project and install its packages on disk."); + } App.initialize("angular-cli"); GoogleAnalytics.post({ t: "screenview", diff --git a/packages/ng-schematics/src/ng-new/index_spec.ts b/packages/ng-schematics/src/ng-new/index_spec.ts index a152881517..4e24a8471c 100644 --- a/packages/ng-schematics/src/ng-new/index_spec.ts +++ b/packages/ng-schematics/src/ng-new/index_spec.ts @@ -1,10 +1,13 @@ +import { virtualFs } from "@angular-devkit/core"; import { Tree } from "@angular-devkit/schematics"; import { NodePackageTaskOptions } from "@angular-devkit/schematics/tasks/package-manager/options"; import { RepositoryInitializerTaskOptions } from "@angular-devkit/schematics/tasks/repo-init/options"; import { RunSchematicTaskOptions } from "@angular-devkit/schematics/tasks/run-schematic/options"; import { SchematicTestRunner, UnitTestTree } from "@angular-devkit/schematics/testing"; +import { NodeWorkflow } from "@angular-devkit/schematics/tools"; import { BaseTemplate, GoogleAnalytics, ProjectLibrary, ProjectTemplate, ScaffoldOptions, Template } from "@igniteui/cli-core"; import * as path from "path"; +import { lastValueFrom } from "rxjs"; import { SchematicsPromptSession } from "../prompt/SchematicsPromptSession"; const collectionPath = path.join(__dirname, "../collection.json"); @@ -251,6 +254,19 @@ describe("Schematics ng-new", () => { expect(runner.tasks).withContext("no install, git or start tasks").toEqual([]); }); + it("rejects dry runs before creating the project", async () => { + const mockProject = mockProjectInLibrary(); + // how `ng new --dry-run` runs the schematic + const workflow = new NodeWorkflow(new virtualFs.SimpleMemoryHost(), { dryRun: true }); + + await expectAsync(lastValueFrom(workflow.execute({ + collection: collectionPath, + schematic: "ng-new", + options: { version: "8.0.3", name: "my-test-project" } + }), { defaultValue: undefined })).toBeRejectedWithError(/--dry-run is not supported/); + expect(mockProject.scaffold).not.toHaveBeenCalled(); + }); + describe("addAIConfig via ng-new", () => { const workingDirectory = "my-test-project"; const mcpFilePath = `${workingDirectory}/.mcp.json`; From ce7c0424920080663ab2825cdf1cd201d9983f7c Mon Sep 17 00:00:00 2001 From: mstoyanova Date: Mon, 26 Oct 2026 10:14:19 +0200 Subject: [PATCH 08/10] applying code review --- .../skills/igniteui-cli-smoke-test/SKILL.md | 2 +- scripts/smoke-test.sh | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.agents/skills/igniteui-cli-smoke-test/SKILL.md b/.agents/skills/igniteui-cli-smoke-test/SKILL.md index 1971642016..9a6857137a 100644 --- a/.agents/skills/igniteui-cli-smoke-test/SKILL.md +++ b/.agents/skills/igniteui-cli-smoke-test/SKILL.md @@ -151,7 +151,7 @@ End with the PASS/FAIL/SKIP totals, whether the run as a whole is trustworthy (n ## Constraints When Editing the Script -- Every `ig` call must happen **before** `npm install` in a generated project. After install, `bin/execute.js` delegates to the project-local *published* CLI, so later calls would test the published CLI, not your local build. +- Call the CLI through `cli` (which loads `packages/cli/lib/cli.js`), never `bin/execute.js`. The wrapper hands off to a project-local `igniteui-cli` whenever one is installed, and Angular projects install one during `ig new`, so calls through it would test the published CLI, not this checkout. - Keep the three-way step judgement (exit code, log scan, artifact) intact. ## Related Skills diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index 912a24203d..f1006e03ae 100644 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -8,7 +8,7 @@ set -uo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -CLI_ENTRY="$ROOT/packages/cli/bin/execute.js" +CLI_LIB="$ROOT/packages/cli/lib/cli.js" FRAMEWORKS="angular,react,webcomponents" PROJECT="" @@ -100,21 +100,28 @@ count_src_files() { [ -d "$1/src" ] && find "$1/src" -type f | wc -l | tr -d ' ' || echo 0 } +# Runs this checkout's CLI. bin/execute.js is skipped on purpose: it hands off to a +# project-local igniteui-cli when one is installed, and Angular projects get one at `new`. +cli() { + node -e 'require(process.argv[1]).run(process.argv.slice(2)).catch(e => { console.error("Error: " + (e.message || e)); process.exit(1); })' \ + "$CLI_LIB" "$@" +} + # run_cli run_cli() { local cwd="$1" log="$2"; shift 2 - (cd "$cwd" && node "$CLI_ENTRY" "$@") >"$log" 2>&1 + (cd "$cwd" && cli "$@") >"$log" 2>&1 } list_component_ids() { local dir="$1" # a row is one tab then the id; wrapped description lines are indented deeper - (cd "$dir" && node "$CLI_ENTRY" list 2>/dev/null) | strip_ansi | awk '/^\t[^\t]/ {print $1}' + (cd "$dir" && cli list 2>/dev/null) | strip_ansi | awk '/^\t[^\t]/ {print $1}' } list_project_ids() { local fw="$1" - (cd "$OUT" && node "$CLI_ENTRY" list 2>/dev/null) | strip_ansi | awk -v fw="($fw)" ' + (cd "$OUT" && cli list 2>/dev/null) | strip_ansi | awk -v fw="($fw)" ' $0 ~ /^[A-Za-z]/ && index($0, fw) { grab = 1; next } /^[A-Za-z]/ { grab = 0 } grab && /^\t\t/ { sub(/\.\.+.*$/, "", $1); print $1 } @@ -276,7 +283,7 @@ run_framework() { # --- preflight --------------------------------------------------------------- -if [ ! -f "$ROOT/packages/cli/lib/cli.js" ]; then +if [ ! -f "$CLI_LIB" ]; then echo "CLI is not built. Run 'npm run build' in $ROOT first." >&2 exit 1 fi @@ -318,7 +325,7 @@ mkdir -p "$LOGS" : >"$SENTINEL" printf 'framework\tstep\ttemplate\tstatus\tseconds\n' >"$RESULTS" -echo "CLI: $CLI_ENTRY" +echo "CLI: $CLI_LIB" echo "Out: $OUT" # --- run --------------------------------------------------------------------- From 58d23d0cd1285686eb64867572a49b7dba007997 Mon Sep 17 00:00:00 2001 From: mstoyanova Date: Wed, 28 Oct 2026 14:43:22 +0200 Subject: [PATCH 09/10] update: updating schematics workflow so it won't add ai tooling twice --- .../igniteui-cli-template-anatomy/SKILL.md | 2 +- README.md | 14 +- packages/cli/README.md | 1 + packages/core/util/ai-skills.ts | 10 +- .../src/tools/setup_angular.md | 2 + packages/igx-templates/constants.ts | 2 + .../igx-ts/projects/_base/scaffold.ts | 9 +- packages/igx-templates/index.ts | 1 + packages/ng-schematics/README.md | 4 + .../ng-schematics/src/cli-config/index.ts | 8 +- .../src/cli-config/index_spec.ts | 13 + spec/templates/angular-scaffold-spec.ts | 10 +- spec/unit/ai-config-spec.ts | 63 ++--- spec/unit/ai-skills-spec.ts | 247 ++++++++---------- 14 files changed, 185 insertions(+), 201 deletions(-) diff --git a/.agents/skills/igniteui-cli-template-anatomy/SKILL.md b/.agents/skills/igniteui-cli-template-anatomy/SKILL.md index 2c8041d61c..4898f01159 100644 --- a/.agents/skills/igniteui-cli-template-anatomy/SKILL.md +++ b/.agents/skills/igniteui-cli-template-anatomy/SKILL.md @@ -87,7 +87,7 @@ React imports components from `igniteui-react` (or `-grids`, `-charts`, `-gauges `ig new` and the `ng-new` schematic create an Angular project with the Angular CLI, through `scaffold()` on `BaseIgxProject` (`packages/igx-templates/igx-ts/projects/_base/index.ts`; the steps live in `_base/scaffold.ts`): -1. `ng new` (scss, routing, no SSR, zone.js), `ng add angular-eslint@`, `ng add igniteui-angular@`. `ng add` installs packages, so `--skip-install` only prints a warning. The AI config `ng add` writes is deleted; `ig new` and the schematic run their own. When a step fails, the project folder is removed. +1. `ng new` (scss, routing, no SSR, zone.js), `ng add angular-eslint@`, `ng add igniteui-angular@`. `ng add` installs packages, so `--skip-install` only prints a warning. For the same reason the `ng-new` schematic rejects `--dry-run` (`isDryRun` in `packages/ng-schematics/src/ng-new/index.ts`, which reads the workflow's private `_dryRun`; re-check it after an Angular major). The AI config `ng add` writes is deleted; `ig new` and the schematic run their own. When a step fails, the project folder is removed. 2. The project's `templatePaths` (`_base`, `_base_with_home`, the project's own `files/`) are laid over the generated project, placeholders substituted. 3. `angular.json` and `package.json` get our deviations as patches (`patchAngularJson`, `patchPackageJson`, which also writes the `IGNITEUI_ANGULAR_PACKAGE` range over the `^` that `ng add` saves); the tsconfigs get the types in the project's `extras`. 4. `npm install` adds `playwright` and the packages in the project's `extras`: at their latest versions, or at the version an entry names. diff --git a/README.md b/README.md index 7048a85dc3..f953ae4c7a 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,8 @@ To create a new project with Ignite UI for Angular use [ig new](https://github.c ```bash ig new "IG Project" --framework=angular --type=igx-ts --template=side-nav ``` +The project is created with the Angular CLI (`ng new`, then `ng add igniteui-angular`), so this needs network access and always installs the project's packages: `--skip-install` has no effect for Angular projects. + #### Generate Ignite UI for React project To create a new project with Ignite UI for React use [ig new](https://github.com/IgniteUI/igniteui-cli/wiki/New#creating-ignite-ui-for-react-applications-v400) by specifying `react` as framework and `igr-ts` as project type: @@ -287,12 +289,14 @@ yourself, if you hit that. Two things to know if you modify it: -* **All `ig` calls must happen before `npm install`.** `packages/cli/bin/execute.js` delegates to +* **Call the CLI through `cli`, never `packages/cli/bin/execute.js`.** The wrapper delegates to `node_modules/igniteui-cli` whenever one resolves inside the current directory, and every project - template lists `igniteui-cli` as a devDependency — so once a generated project has been installed, - `ig add` silently switches to the *published* CLI and its bundled templates. Scaffolding with - `--skip-install` keeps `PackageManager.queuePackage` from spawning installs, so dependencies are - written to `package.json` and installed once at the end. + template lists `igniteui-cli` as a devDependency. Angular projects install theirs during `ig new` + (`ng add` needs the packages), so through the wrapper `ig add` would silently switch to the + *published* CLI and its bundled templates. `cli` loads this checkout's `packages/cli/lib/cli.js` + instead. For the other frameworks, scaffolding with `--skip-install` still keeps + `PackageManager.queuePackage` from spawning installs, so dependencies are written to + `package.json` and installed once at the end. * **Exit codes are not sufficient.** `Util.error` logs and returns without setting one, so `ig add does-not-exist x` exits 0. Each step is judged on its exit code *and* an error scan of its log *and* an artifact check. diff --git a/packages/cli/README.md b/packages/cli/README.md index 891ab66b4b..7063a7fccc 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -74,6 +74,7 @@ To create a new project with Ignite UI for Angular use [ig new](https://github.c ```bash ig new "IG Project" --framework=angular --type=igx-ts --template=side-nav ``` +The project is created with the Angular CLI (`ng new`, then `ng add igniteui-angular`), so this needs network access and always installs the project's packages: `--skip-install` has no effect for Angular projects. The `igniteui-cli` makes full use of the component definitions provided by [`igniteui/angular-templates`](../ng-schematics) package. You can find all of the available component templates in the [official documentation](https://www.infragistics.com/products/ignite-ui-angular/angular/components/general/cli/component-templates). diff --git a/packages/core/util/ai-skills.ts b/packages/core/util/ai-skills.ts index b9afde8fc1..35a2cff843 100644 --- a/packages/core/util/ai-skills.ts +++ b/packages/core/util/ai-skills.ts @@ -116,14 +116,13 @@ function resolveSkillsRoots(framework: string): SkillsSource[] { candidates = new Set([...allPkgKeys, NPM_REACT, NPM_WEBCOMPONENTS]); } - const srcFs = new FsFileSystem(); for (const pkg of candidates) { const resolved = resolvePackage(pkg as keyof typeof UPGRADEABLE_PACKAGES); const skillsRoot = `node_modules/${resolved}/${AI_SKILLS_DIR_NAME}`; if (fs.directoryExists(skillsRoot) && !roots.some(r => r.path === skillsRoot)) { let version = "unknown"; try { - const pkgJson = JSON.parse(srcFs.readFile(`node_modules/${resolved}/package.json`)); + const pkgJson = JSON.parse(fs.readFile(`node_modules/${resolved}/package.json`)); version = pkgJson.version ?? "unknown"; } catch { /* version stays unknown */ } roots.push({ type: "package", packageName: resolved, packageVersion: version, path: skillsRoot }); @@ -148,11 +147,10 @@ function resolveSkillsRoots(framework: string): SkillsSource[] { */ export function copyAISkillsToProject(agents: AIAgentTarget[], framework: string): AISkillsCopyResult { const result: AISkillsCopyResult = { found: 0, skipped: 0, failed: 0, details: [], sources: [] }; - // Source reads (glob + readFile) always use physical FS - skill files can - // come from sources outside the project virtual tree (external/global package): - const srcFs = new FsFileSystem(); // Destination writes respect the App FS (which may be virtual): const destFs = App.container.get(FS_TOKEN); + // Bundled skills are template files outside the project, read from the physical FS: + const physicalFs = new FsFileSystem(); const skillsSources = resolveSkillsRoots(framework); if (!skillsSources.length) { @@ -166,6 +164,8 @@ export function copyAISkillsToProject(agents: AIAgentTarget[], framework: string const outputDir = getSkillsDir(agent); for (const source of skillsSources) { + // package skills are read through the App FS that found them, whose root may not be the cwd + const srcFs = source.type === "package" ? destFs : physicalFs; const skillsRoot = source.path; const rawPaths = srcFs.glob(skillsRoot, "**/*"); const pkgDirName = multiRoot ? path.basename(path.dirname(skillsRoot)) : ""; diff --git a/packages/igniteui-mcp/igniteui-doc-mcp/src/tools/setup_angular.md b/packages/igniteui-mcp/igniteui-doc-mcp/src/tools/setup_angular.md index 4449cad48e..3fc21cea42 100644 --- a/packages/igniteui-mcp/igniteui-doc-mcp/src/tools/setup_angular.md +++ b/packages/igniteui-mcp/igniteui-doc-mcp/src/tools/setup_angular.md @@ -43,6 +43,8 @@ ig start ```bash ng new --collection="@igniteui/angular-schematics" ``` + +Both options run `ng new` and `ng add igniteui-angular`, which install packages while the project is created. Do not pass `--dry-run` to Option B: it is not supported and stops with an error. Or to add Ignite UI to an existing Angular project: diff --git a/packages/igx-templates/constants.ts b/packages/igx-templates/constants.ts index 4a55269e3a..f0132cc835 100644 --- a/packages/igx-templates/constants.ts +++ b/packages/igx-templates/constants.ts @@ -1 +1,3 @@ export const IGNITEUI_ANGULAR_PACKAGE = "igniteui-angular@~22.2.0"; +/** Set by the project scaffold for `ng add igniteui-angular`, whose cli-config then skips ai-config; the caller runs its own with prompts */ +export const SKIP_AI_CONFIG_ENV = "IGNITEUI_SKIP_AI_CONFIG"; diff --git a/packages/igx-templates/igx-ts/projects/_base/scaffold.ts b/packages/igx-templates/igx-ts/projects/_base/scaffold.ts index 97fcf8a698..a6f9e97cef 100644 --- a/packages/igx-templates/igx-ts/projects/_base/scaffold.ts +++ b/packages/igx-templates/igx-ts/projects/_base/scaffold.ts @@ -3,14 +3,17 @@ import * as fs from "fs"; import * as os from "os"; import * as path from "path"; import * as ts from "typescript"; -import { IGNITEUI_ANGULAR_PACKAGE } from "../../../constants"; +import { IGNITEUI_ANGULAR_PACKAGE, SKIP_AI_CONFIG_ENV } from "../../../constants"; /** Package that provides the `ig` commands in projects created by `ig new` */ export const CLI_PACKAGE = "igniteui-cli"; /** Added by `ng add igniteui-angular`; projects created through the schematics keep it instead of igniteui-cli */ const SCHEMATICS_PACKAGE = "@igniteui/angular-schematics"; -/** AI config the ai-config step of `ng add igniteui-angular` writes; `ig new` and the ng-new schematic run their own */ +/** + * AI config the ai-config step of `ng add igniteui-angular` writes; `ig new` and the ng-new schematic run their own. + * `SKIP_AI_CONFIG_ENV` skips that step; these are removed for @igniteui/angular-schematics versions without the check. + */ const NG_ADD_AI_CONFIG = [".agents", ".claude", "AGENTS.md", ".mcp.json"]; /** Reads a field of the newest published version that matches `spec` (`name@range`) */ @@ -32,7 +35,7 @@ export function generateProject(dashName: string, projectPath: string): string { Util.execSync(`npx -y @angular/cli@${angularMajor} ${command} --defaults --interactive=false`, { cwd, stdio: "inherit", - env: { ...process.env, NG_CLI_ANALYTICS: "false" } + env: { ...process.env, NG_CLI_ANALYTICS: "false", [SKIP_AI_CONFIG_ENV]: "true" } }); // `ng new` refuses to run inside an Angular workspace, so it runs in a temp folder outside any diff --git a/packages/igx-templates/index.ts b/packages/igx-templates/index.ts index 0d675346bb..f4da21df5b 100644 --- a/packages/igx-templates/index.ts +++ b/packages/igx-templates/index.ts @@ -3,5 +3,6 @@ import standalone from './igx-ts'; export * from './IgniteUIForAngularTemplate'; export * from './AngularTypeScriptFileUpdate'; export * from './types'; +export { SKIP_AI_CONFIG_ENV } from './constants'; export default [standalone]; diff --git a/packages/ng-schematics/README.md b/packages/ng-schematics/README.md index 77ce5227d3..48c8618653 100644 --- a/packages/ng-schematics/README.md +++ b/packages/ng-schematics/README.md @@ -18,6 +18,10 @@ If you want to use Ignite UI for Angular controls right off the bat, you can gen ng new --collection="@igniteui/angular-schematics" ``` +The project is created with the Angular CLI's own `ng new` and `ng add igniteui-angular`, so this needs network access and always installs the packages the project starts with. `--skip-install` only skips the final install, which picks up packages the guided setup adds afterwards, and starting the app. + +`--dry-run` is not supported: the project is created and installed on disk, which a dry run cannot hold back, so the command stops with an error before creating anything. + ### Add to existing project You can add the `@igniteui/angular-schematics` collection to an existing Angular project by running: diff --git a/packages/ng-schematics/src/cli-config/index.ts b/packages/ng-schematics/src/cli-config/index.ts index c55bdc66d3..615da26a65 100644 --- a/packages/ng-schematics/src/cli-config/index.ts +++ b/packages/ng-schematics/src/cli-config/index.ts @@ -3,7 +3,7 @@ import { DependencyNotFoundException } from "@angular-devkit/core"; import { chain, FileDoesNotExistException, Rule, SchematicContext, Tree } from "@angular-devkit/schematics"; import { RunSchematicTask } from "@angular-devkit/schematics/tasks"; import { addClassToBody, addMcpServers, AIAgentTarget, AiCodingAssistant, App, copyAgentInstructionFiles, copyAISkillsToProject, FormatSettings, McpServerEntry, NPM_ANGULAR, resolvePackage, TEMPLATE_MANAGER, TypeScriptAstTransformer, TypeScriptUtils } from "@igniteui/cli-core"; -import { AngularTypeScriptFileUpdate } from "@igniteui/angular-templates"; +import { AngularTypeScriptFileUpdate, SKIP_AI_CONFIG_ENV } from "@igniteui/angular-templates"; import { createCliConfig } from "../utils/cli-config"; import { setVirtual } from "../utils/NgFileSystem"; import { addFontsToIndexHtml, getProjects, importDefaultTheme } from "../utils/theme-import"; @@ -153,8 +153,10 @@ export function addAIConfig(options: { agents?: AIAgentTarget[]; assistants?: st export default function (): Rule { return (tree: Tree, context: SchematicContext) => { appInit(tree); - // queue ai-config with prompts: - context.addTask(new RunSchematicTask("ai-config", {})); + // queue ai-config with prompts, unless the project scaffold runs its own after `ng add`: + if (!process.env[SKIP_AI_CONFIG_ENV]) { + context.addTask(new RunSchematicTask("ai-config", {})); + } return chain([ importStyles(), diff --git a/packages/ng-schematics/src/cli-config/index_spec.ts b/packages/ng-schematics/src/cli-config/index_spec.ts index f30b34358e..a1858f9d4f 100644 --- a/packages/ng-schematics/src/cli-config/index_spec.ts +++ b/packages/ng-schematics/src/cli-config/index_spec.ts @@ -4,6 +4,7 @@ import { DependencyNotFoundException } from "@angular-devkit/core"; import { EmptyTree } from "@angular-devkit/schematics"; import { SchematicTestRunner, UnitTestTree } from "@angular-devkit/schematics/testing"; import { App, FEED_ANGULAR, NPM_ANGULAR, TEMPLATE_MANAGER } from "@igniteui/cli-core"; +import { SKIP_AI_CONFIG_ENV } from "@igniteui/angular-templates"; import { SchematicsTemplateManager } from "../SchematicsTemplateManager"; describe("cli-config schematic", () => { @@ -446,6 +447,18 @@ export const appConfig: ApplicationConfig = { expect(taskOptions).toContain(jasmine.objectContaining({ name: "ai-config" })); }); + it("should not schedule the ai-config schematic task when the project scaffold runs its own", async () => { + process.env[SKIP_AI_CONFIG_ENV] = "true"; + try { + await runner.runSchematic("cli-config", {}, tree); + } finally { + delete process.env[SKIP_AI_CONFIG_ENV]; + } + + const taskOptions = runner.tasks.map(task => task.options); + expect(taskOptions).not.toContain(jasmine.objectContaining({ name: "ai-config" })); + }); + describe("ai-config schematic", () => { const mcpFilePath = "/.vscode/mcp.json"; diff --git a/spec/templates/angular-scaffold-spec.ts b/spec/templates/angular-scaffold-spec.ts index fbcab8844b..5cf88c5289 100644 --- a/spec/templates/angular-scaffold-spec.ts +++ b/spec/templates/angular-scaffold-spec.ts @@ -2,7 +2,7 @@ import { GoogleAnalytics, Util } from "@igniteui/cli-core"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; -import { IGNITEUI_ANGULAR_PACKAGE } from "../../packages/igx-templates/constants"; +import { IGNITEUI_ANGULAR_PACKAGE, SKIP_AI_CONFIG_ENV } from "../../packages/igx-templates/constants"; import emptyProject from "../../packages/igx-templates/igx-ts/projects/empty"; import authProject from "../../packages/igx-templates/igx-ts/projects/side-nav-auth"; import { stubAngularCli } from "../helpers/angular-cli-stub"; @@ -144,6 +144,14 @@ describe("Angular project scaffold", () => { expect(commands()).toContain("npm install --save-dev playwright --no-audit --no-fund"); }); + it("tells the Angular CLI calls to skip ng add's ai-config, which ig new and the ng-new schematic run themselves", async () => { + await emptyProject.scaffold({ name, theme: "Default" }); + + for (const [command, options] of execSync.calls.allArgs().filter(([command]) => command.startsWith("npx"))) { + expect(options.env[SKIP_AI_CONFIG_ENV]).withContext(command).toBe("true"); + } + }); + it("runs ng new outside the current folder, which may be inside an Angular workspace", async () => { await emptyProject.scaffold({ name, theme: "Default" }); diff --git a/spec/unit/ai-config-spec.ts b/spec/unit/ai-config-spec.ts index 050aea2097..b9b6873191 100644 --- a/spec/unit/ai-config-spec.ts +++ b/spec/unit/ai-config-spec.ts @@ -548,11 +548,6 @@ describe("Unit - ai-config command", () => { } as unknown as IFileSystem; spyOn(App.container, "get").and.returnValue(mockFs); - // srcFs reads (FsFileSystem.prototype) for source content - spyOn(FsFileSystem.prototype, "glob").and.callFake((d: string) => - d === angularSkillsDir ? [skillFile] : [] - ); - spyOn(FsFileSystem.prototype, "readFile").and.returnValue("skill content"); configureSkills(["claude"], "angular"); @@ -581,11 +576,6 @@ describe("Unit - ai-config command", () => { } as unknown as IFileSystem; spyOn(App.container, "get").and.returnValue(mockFs); - // srcFs reads (FsFileSystem.prototype) for source content - spyOn(FsFileSystem.prototype, "glob").and.callFake((d: string) => - d === angularSkillsDir ? [skillFile] : [] - ); - spyOn(FsFileSystem.prototype, "readFile").and.returnValue(content); configureSkills(["claude"], "angular"); @@ -610,11 +600,6 @@ describe("Unit - ai-config command", () => { } as unknown as IFileSystem; spyOn(App.container, "get").and.returnValue(mockFs); - // srcFs reads (FsFileSystem.prototype) for source content - spyOn(FsFileSystem.prototype, "glob").and.callFake((d: string) => - d === angularSkillsDir ? [skillFile] : [] - ); - spyOn(FsFileSystem.prototype, "readFile").and.returnValue("skill content"); configureSkills(["claude"], "angular"); @@ -639,10 +624,6 @@ describe("Unit - ai-config command", () => { } as unknown as IFileSystem; spyOn(App.container, "get").and.returnValue(mockFs); - spyOn(FsFileSystem.prototype, "glob").and.callFake((d: string) => - d === angularSkillsDir ? [skillFile] : [] - ); - spyOn(FsFileSystem.prototype, "readFile").and.returnValue("skill content"); configureSkills(["claude"], "angular"); @@ -656,7 +637,9 @@ describe("Unit - ai-config command", () => { fileExists: jasmine.createSpy("fileExists").and.callFake((p: string) => p === "ignite-ui-cli.json" || p === destFile ), - readFile: jasmine.createSpy("readFile").and.returnValue("old content"), + readFile: jasmine.createSpy("readFile").and.callFake((p: string) => + p === skillFile ? "new content" : "old content" + ), writeFile: jasmine.createSpy("writeFile"), directoryExists: jasmine.createSpy("directoryExists").and.callFake((p: string) => p === angularSkillsDir @@ -667,10 +650,6 @@ describe("Unit - ai-config command", () => { } as unknown as IFileSystem; spyOn(App.container, "get").and.returnValue(mockFs); - spyOn(FsFileSystem.prototype, "glob").and.callFake((d: string) => - d === angularSkillsDir ? [skillFile] : [] - ); - spyOn(FsFileSystem.prototype, "readFile").and.returnValue("new content"); configureSkills(["claude"], "angular"); @@ -696,10 +675,6 @@ describe("Unit - ai-config command", () => { } as unknown as IFileSystem; spyOn(App.container, "get").and.returnValue(mockFs); - spyOn(FsFileSystem.prototype, "glob").and.callFake((d: string) => - d === angularSkillsDir ? [skillFile] : [] - ); - spyOn(FsFileSystem.prototype, "readFile").and.returnValue(content); configureSkills(["claude"], "angular"); @@ -714,7 +689,12 @@ describe("Unit - ai-config command", () => { fileExists: jasmine.createSpy("fileExists").and.callFake((p: string) => p === "ignite-ui-cli.json" ), - readFile: jasmine.createSpy("readFile").and.returnValue("skill content"), + readFile: jasmine.createSpy("readFile").and.callFake((p: string) => { + if (p === "node_modules/igniteui-angular/package.json") { + return JSON.stringify({ version: "18.2.0" }); + } + return "skill content"; + }), writeFile: jasmine.createSpy("writeFile"), directoryExists: jasmine.createSpy("directoryExists").and.callFake((p: string) => p === angularSkillsDir @@ -725,15 +705,6 @@ describe("Unit - ai-config command", () => { } as unknown as IFileSystem; spyOn(App.container, "get").and.returnValue(mockFs); - spyOn(FsFileSystem.prototype, "glob").and.callFake((d: string) => - d === angularSkillsDir ? [skillFile] : [] - ); - spyOn(FsFileSystem.prototype, "readFile").and.callFake((p: string) => { - if (p === "node_modules/igniteui-angular/package.json") { - return JSON.stringify({ version: "18.2.0" }); - } - return "skill content"; - }); configureSkills(["claude"], "angular"); @@ -775,7 +746,12 @@ describe("Unit - ai-config command", () => { fileExists: jasmine.createSpy("fileExists").and.callFake((p: string) => p === "ignite-ui-cli.json" ), - readFile: jasmine.createSpy("readFile").and.returnValue("skill content"), + readFile: jasmine.createSpy("readFile").and.callFake((p: string) => { + if (p === "node_modules/igniteui-angular/package.json") { + return JSON.stringify({ version: "18.2.0" }); + } + return "skill content"; + }), writeFile: jasmine.createSpy("writeFile"), directoryExists: jasmine.createSpy("directoryExists").and.callFake((p: string) => p === angularSkillsDir @@ -786,15 +762,6 @@ describe("Unit - ai-config command", () => { } as unknown as IFileSystem; spyOn(App.container, "get").and.returnValue(mockFs); - spyOn(FsFileSystem.prototype, "glob").and.callFake((d: string) => - d === angularSkillsDir ? [skillFile] : [] - ); - spyOn(FsFileSystem.prototype, "readFile").and.callFake((p: string) => { - if (p === "node_modules/igniteui-angular/package.json") { - return JSON.stringify({ version: "18.2.0" }); - } - return "skill content"; - }); configureSkills(["claude"], "angular", false); diff --git a/spec/unit/ai-skills-spec.ts b/spec/unit/ai-skills-spec.ts index 1b17377c68..aa76f8a6ae 100644 --- a/spec/unit/ai-skills-spec.ts +++ b/spec/unit/ai-skills-spec.ts @@ -24,7 +24,7 @@ function mockTemplateManager(templatePaths: string[]) { return mockTm; } -/** Creates a mock for the destination FS (injected via container, may be virtual Tree) */ +/** Creates a mock for the container FS (may be virtual Tree): the destination, and the source of package skills */ function makeDestFs(overrides: Partial = {}): IFileSystem { return { fileExists: jasmine.createSpy("destFs.fileExists").and.returnValue(false), @@ -37,7 +37,7 @@ function makeDestFs(overrides: Partial = {}): IFileSystem { } /** - * Spies on FsFileSystem.prototype methods to mock the source FS (always real disk). + * Spies on FsFileSystem.prototype methods to mock the real disk, the source of bundled template skills. * Returns spies dict so tests can configure callFake / returnValue. */ function spySrcFs(overrides: { @@ -71,27 +71,51 @@ describe("Unit - copyAISkillsToProject", () => { const skillFilePath = skillFile("igniteui-angular", "angular.md"); const mockSkillContent = "# Ignite UI for Angular skills"; - spySrcFs({ - glob: spyOn(FsFileSystem.prototype, "glob").and.callFake((dir: string) => + const destFs = makeDestFs({ + directoryExists: jasmine.createSpy("destFs.directoryExists").and.callFake((p: string) => + p === angularSkillsDir + ), + glob: jasmine.createSpy("destFs.glob").and.callFake((dir: string) => dir === angularSkillsDir ? [skillFilePath] : [] ), - readFile: spyOn(FsFileSystem.prototype, "readFile").and.callFake((p: string) => { + readFile: jasmine.createSpy("destFs.readFile").and.callFake((p: string) => { if (p === skillFilePath) return mockSkillContent; return ""; }) }); + App.container.set(FS_TOKEN, destFs); + + const result = copyAISkillsToProject(["claude"], "angular"); + + expect(destFs.writeFile).toHaveBeenCalledWith(".claude/skills/angular.md", mockSkillContent); + expect(result.details).toEqual([{ path: ".claude/skills/angular.md", action: "created" }]); + }); + + it("should read package skills through the container FS, which may not be rooted at the cwd", () => { + // Simulates `ng new`: the tree is scoped to the new project, the cwd is the folder it is created in + const angularSkillsDir = skillsDir("igniteui-angular"); + const treeSkillFilePath = `/${skillFile("igniteui-angular", "angular.md")}`; + const content = "# Angular skills"; + const srcSpies = spySrcFs(); const destFs = makeDestFs({ directoryExists: jasmine.createSpy("destFs.directoryExists").and.callFake((p: string) => p === angularSkillsDir + ), + glob: jasmine.createSpy("destFs.glob").and.callFake((dir: string) => + dir === angularSkillsDir ? [treeSkillFilePath] : [] + ), + readFile: jasmine.createSpy("destFs.readFile").and.callFake((p: string) => + p === treeSkillFilePath ? content : "" ) }); App.container.set(FS_TOKEN, destFs); - const result = copyAISkillsToProject(["claude"], "angular"); + copyAISkillsToProject(["claude"], "angular"); - expect(destFs.writeFile).toHaveBeenCalledWith(".claude/skills/angular.md", mockSkillContent); - expect(result.details).toEqual([{ path: ".claude/skills/angular.md", action: "created" }]); + expect(destFs.writeFile).toHaveBeenCalledWith(".claude/skills/angular.md", content); + expect(srcSpies.glob).not.toHaveBeenCalled(); + expect(srcSpies.readFile).not.toHaveBeenCalled(); }); it("should prefer the licensed @infragistics/igniteui-angular package if installed", () => { @@ -99,14 +123,7 @@ describe("Unit - copyAISkillsToProject", () => { const angularSkillsDir = skillsDir(licensedPkg); const skillFilePath = skillFile(licensedPkg, "angular.md"); - spySrcFs({ - glob: spyOn(FsFileSystem.prototype, "glob").and.callFake((dir: string) => - dir === angularSkillsDir ? [skillFilePath] : [] - ), - readFile: spyOn(FsFileSystem.prototype, "readFile").and.returnValue("skill content") - }); - - // resolvePackage + directoryExists use the container FS (destFs) + // resolvePackage, directoryExists and the package skill reads use the container FS (destFs) const destFs = makeDestFs({ directoryExists: jasmine.createSpy("destFs.directoryExists").and.callFake((p: string) => p === angularSkillsDir @@ -114,8 +131,12 @@ describe("Unit - copyAISkillsToProject", () => { fileExists: jasmine.createSpy("destFs.fileExists").and.callFake((p: string) => p === "./package.json" ), + glob: jasmine.createSpy("destFs.glob").and.callFake((dir: string) => + dir === angularSkillsDir ? [skillFilePath] : [] + ), readFile: jasmine.createSpy("destFs.readFile").and.callFake((p: string) => { if (p === "./package.json") return JSON.stringify({ dependencies: { [licensedPkg]: "^18.0.0" } }); + if (p === skillFilePath) return "skill content"; return ""; }) }); @@ -131,11 +152,6 @@ describe("Unit - copyAISkillsToProject", () => { const skillFilePath = skillFile("igniteui-angular", "angular.md"); const newContent = "# Updated Angular skills"; - spySrcFs({ - glob: spyOn(FsFileSystem.prototype, "glob").and.returnValue([skillFilePath]), - readFile: spyOn(FsFileSystem.prototype, "readFile").and.returnValue(newContent) - }); - const destFs = makeDestFs({ directoryExists: jasmine.createSpy("destFs.directoryExists").and.callFake((p: string) => p === angularSkillsDir @@ -143,7 +159,10 @@ describe("Unit - copyAISkillsToProject", () => { fileExists: jasmine.createSpy("destFs.fileExists").and.callFake((p: string) => p === ".claude/skills/angular.md" ), - readFile: jasmine.createSpy("destFs.readFile").and.returnValue("") // older content + glob: jasmine.createSpy("destFs.glob").and.returnValue([skillFilePath]), + readFile: jasmine.createSpy("destFs.readFile").and.callFake((p: string) => + p === skillFilePath ? newContent : "" // older content + ) }); App.container.set(FS_TOKEN, destFs); @@ -158,13 +177,6 @@ describe("Unit - copyAISkillsToProject", () => { const skillFilePath = skillFile("igniteui-angular", "angular.md"); const existingContent = "# Ignite UI for Angular skills"; - spySrcFs({ - glob: spyOn(FsFileSystem.prototype, "glob").and.callFake((dir: string) => - dir === angularSkillsDir ? [skillFilePath] : [] - ), - readFile: spyOn(FsFileSystem.prototype, "readFile").and.returnValue(existingContent) - }); - const destFs = makeDestFs({ directoryExists: jasmine.createSpy("destFs.directoryExists").and.callFake((p: string) => p === angularSkillsDir @@ -172,6 +184,9 @@ describe("Unit - copyAISkillsToProject", () => { fileExists: jasmine.createSpy("destFs.fileExists").and.callFake((p: string) => p === ".claude/skills/angular.md" ), + glob: jasmine.createSpy("destFs.glob").and.callFake((dir: string) => + dir === angularSkillsDir ? [skillFilePath] : [] + ), readFile: jasmine.createSpy("destFs.readFile").and.returnValue(existingContent) }); App.container.set(FS_TOKEN, destFs); @@ -194,21 +209,18 @@ describe("Unit - copyAISkillsToProject", () => { const file = skillFile(reactPkg, "overview.md"); const content = "# React overview"; - spySrcFs({ - glob: spyOn(FsFileSystem.prototype, "glob").and.callFake((d: string) => + const destFs = makeDestFs({ + directoryExists: jasmine.createSpy("destFs.directoryExists").and.callFake((p: string) => + p === dir + ), + glob: jasmine.createSpy("destFs.glob").and.callFake((d: string) => d === dir ? [file] : [] ), - readFile: spyOn(FsFileSystem.prototype, "readFile").and.callFake((p: string) => { + readFile: jasmine.createSpy("destFs.readFile").and.callFake((p: string) => { if (p === file) return content; return ""; }) }); - - const destFs = makeDestFs({ - directoryExists: jasmine.createSpy("destFs.directoryExists").and.callFake((p: string) => - p === dir - ) - }); App.container.set(FS_TOKEN, destFs); copyAISkillsToProject(["claude"], "react"); @@ -224,21 +236,18 @@ describe("Unit - copyAISkillsToProject", () => { const file = skillFile(wcPkg, "webcomponents.md"); const content = "# Ignite UI WebComponents skills"; - spySrcFs({ - glob: spyOn(FsFileSystem.prototype, "glob").and.callFake((d: string) => + const destFs = makeDestFs({ + directoryExists: jasmine.createSpy("destFs.directoryExists").and.callFake((p: string) => + p === dir + ), + glob: jasmine.createSpy("destFs.glob").and.callFake((d: string) => d === dir ? [file] : [] ), - readFile: spyOn(FsFileSystem.prototype, "readFile").and.callFake((p: string) => { + readFile: jasmine.createSpy("destFs.readFile").and.callFake((p: string) => { if (p === file) return content; return ""; }) }); - - const destFs = makeDestFs({ - directoryExists: jasmine.createSpy("destFs.directoryExists").and.callFake((p: string) => - p === dir - ) - }); App.container.set(FS_TOKEN, destFs); copyAISkillsToProject(["claude"], "webcomponents"); @@ -270,14 +279,11 @@ describe("Unit - copyAISkillsToProject", () => { it("should silently return when skills directory exists but is empty", () => { const dir = skillsDir("igniteui-angular"); - spySrcFs({ - glob: spyOn(FsFileSystem.prototype, "glob").and.returnValue([]) - }); - const destFs = makeDestFs({ directoryExists: jasmine.createSpy("destFs.directoryExists").and.callFake((p: string) => p === dir - ) + ), + glob: jasmine.createSpy("destFs.glob").and.returnValue([]) }); App.container.set(FS_TOKEN, destFs); @@ -293,17 +299,14 @@ describe("Unit - copyAISkillsToProject", () => { const dir = skillsDir(pkg); const file = skillFile(pkg, "angular.md"); - spySrcFs({ - glob: spyOn(FsFileSystem.prototype, "glob").and.callFake((d: string) => - d === dir ? [file] : [] - ), - readFile: spyOn(FsFileSystem.prototype, "readFile").and.returnValue("skill content") - }); - const destFs = makeDestFs({ directoryExists: jasmine.createSpy("destFs.directoryExists").and.callFake((p: string) => p === dir ), + glob: jasmine.createSpy("destFs.glob").and.callFake((d: string) => + d === dir ? [file] : [] + ), + readFile: jasmine.createSpy("destFs.readFile").and.returnValue("skill content"), writeFile: jasmine.createSpy("destFs.writeFile").and.throwError("EACCES: permission denied") }); App.container.set(FS_TOKEN, destFs); @@ -322,13 +325,6 @@ describe("Unit - copyAISkillsToProject", () => { const file = skillFile(pkg, "angular.md"); const destFile = ".claude/skills/angular.md"; - spySrcFs({ - glob: spyOn(FsFileSystem.prototype, "glob").and.callFake((d: string) => - d === dir ? [file] : [] - ), - readFile: spyOn(FsFileSystem.prototype, "readFile").and.returnValue("new content") - }); - const destFs = makeDestFs({ directoryExists: jasmine.createSpy("destFs.directoryExists").and.callFake((p: string) => p === dir @@ -336,7 +332,12 @@ describe("Unit - copyAISkillsToProject", () => { fileExists: jasmine.createSpy("destFs.fileExists").and.callFake((p: string) => p === destFile ), - readFile: jasmine.createSpy("destFs.readFile").and.returnValue("old content"), + glob: jasmine.createSpy("destFs.glob").and.callFake((d: string) => + d === dir ? [file] : [] + ), + readFile: jasmine.createSpy("destFs.readFile").and.callFake((p: string) => + p === file ? "new content" : "old content" + ), writeFile: jasmine.createSpy("destFs.writeFile").and.throwError("EACCES: permission denied") }); App.container.set(FS_TOKEN, destFs); @@ -355,18 +356,15 @@ describe("Unit - copyAISkillsToProject", () => { const file1 = skillFile(pkg, "angular.md"); const file2 = skillFile(pkg, "components.md"); - spySrcFs({ - glob: spyOn(FsFileSystem.prototype, "glob").and.callFake((d: string) => - d === dir ? [file1, file2] : [] - ), - readFile: spyOn(FsFileSystem.prototype, "readFile").and.returnValue("skill content") - }); - let writeCallCount = 0; const destFs = makeDestFs({ directoryExists: jasmine.createSpy("destFs.directoryExists").and.callFake((p: string) => p === dir ), + glob: jasmine.createSpy("destFs.glob").and.callFake((d: string) => + d === dir ? [file1, file2] : [] + ), + readFile: jasmine.createSpy("destFs.readFile").and.returnValue("skill content"), writeFile: jasmine.createSpy("destFs.writeFile").and.callFake(() => { writeCallCount++; if (writeCallCount === 2) { @@ -457,8 +455,8 @@ describe("Unit - copyAISkillsToProject", () => { expect(mockTm.getFrameworkById).not.toHaveBeenCalledWith("angular"); }); - it("should read skill sources from real disk FS even when destFs is virtual", () => { - // Simulates the schematics scenario: srcFs (FsFileSystem) reads from disk, + it("should read bundled skill sources from real disk FS even when destFs is virtual", () => { + // Simulates the schematics scenario: template files outside the project are read from disk, // destFs (NgTreeFileSystem) writes into the virtual Tree. const ABS_TEMPLATE_PATH = path.resolve("/usr/lib/node_modules/fake-templates/base/files"); const SKILLS_ROOT = path.join(ABS_TEMPLATE_PATH, "skills"); @@ -483,7 +481,7 @@ describe("Unit - copyAISkillsToProject", () => { expect(srcSpies.readFile).toHaveBeenCalledWith(skillFilePath); // Dest writes go to the injected FS (virtual Tree) expect(destFs.writeFile).toHaveBeenCalledWith(".claude/skills/angular.md", content); - // destFs.glob should NOT have been called — source ops use srcFs + // destFs.glob should NOT have been called — bundled source ops use srcFs expect(destFs.glob).not.toHaveBeenCalled(); }); }); @@ -495,17 +493,14 @@ describe("Unit - copyAISkillsToProject", () => { const nestedFile = skillFile(pkg, "grids/grid.md"); const content = "# Grid skills"; - spySrcFs({ - glob: spyOn(FsFileSystem.prototype, "glob").and.callFake((d: string) => - d === dir ? [nestedFile] : [] - ), - readFile: spyOn(FsFileSystem.prototype, "readFile").and.returnValue(content) - }); - const destFs = makeDestFs({ directoryExists: jasmine.createSpy("destFs.directoryExists").and.callFake((p: string) => p === dir - ) + ), + glob: jasmine.createSpy("destFs.glob").and.callFake((d: string) => + d === dir ? [nestedFile] : [] + ), + readFile: jasmine.createSpy("destFs.readFile").and.returnValue(content) }); App.container.set(FS_TOKEN, destFs); @@ -521,17 +516,14 @@ describe("Unit - copyAISkillsToProject", () => { const skillFilePath = skillFile("igniteui-angular", "angular.md"); const content = "# Angular skills"; - spySrcFs({ - glob: spyOn(FsFileSystem.prototype, "glob").and.callFake((dir: string) => - dir === angularSkillsDir ? [skillFilePath] : [] - ), - readFile: spyOn(FsFileSystem.prototype, "readFile").and.returnValue(content) - }); - const destFs = makeDestFs({ directoryExists: jasmine.createSpy("destFs.directoryExists").and.callFake((p: string) => p === angularSkillsDir - ) + ), + glob: jasmine.createSpy("destFs.glob").and.callFake((dir: string) => + dir === angularSkillsDir ? [skillFilePath] : [] + ), + readFile: jasmine.createSpy("destFs.readFile").and.returnValue(content) }); App.container.set(FS_TOKEN, destFs); @@ -545,17 +537,14 @@ describe("Unit - copyAISkillsToProject", () => { const skillFilePath = skillFile("igniteui-angular", "angular.md"); const content = "# Angular skills"; - spySrcFs({ - glob: spyOn(FsFileSystem.prototype, "glob").and.callFake((dir: string) => - dir === angularSkillsDir ? [skillFilePath] : [] - ), - readFile: spyOn(FsFileSystem.prototype, "readFile").and.returnValue(content) - }); - const destFs = makeDestFs({ directoryExists: jasmine.createSpy("destFs.directoryExists").and.callFake((p: string) => p === angularSkillsDir - ) + ), + glob: jasmine.createSpy("destFs.glob").and.callFake((dir: string) => + dir === angularSkillsDir ? [skillFilePath] : [] + ), + readFile: jasmine.createSpy("destFs.readFile").and.returnValue(content) }); App.container.set(FS_TOKEN, destFs); @@ -570,17 +559,14 @@ describe("Unit - copyAISkillsToProject", () => { const file = skillFile(reactPkg, "overview.md"); const content = "# React overview"; - spySrcFs({ - glob: spyOn(FsFileSystem.prototype, "glob").and.callFake((d: string) => - d === dir ? [file] : [] - ), - readFile: spyOn(FsFileSystem.prototype, "readFile").and.returnValue(content) - }); - const destFs = makeDestFs({ directoryExists: jasmine.createSpy("destFs.directoryExists").and.callFake((p: string) => p === dir - ) + ), + glob: jasmine.createSpy("destFs.glob").and.callFake((d: string) => + d === dir ? [file] : [] + ), + readFile: jasmine.createSpy("destFs.readFile").and.returnValue(content) }); App.container.set(FS_TOKEN, destFs); @@ -594,17 +580,14 @@ describe("Unit - copyAISkillsToProject", () => { const skillFilePath = skillFile("igniteui-angular", "angular.md"); const content = "# Angular skills"; - spySrcFs({ - glob: spyOn(FsFileSystem.prototype, "glob").and.callFake((dir: string) => - dir === angularSkillsDir ? [skillFilePath] : [] - ), - readFile: spyOn(FsFileSystem.prototype, "readFile").and.returnValue(content) - }); - const destFs = makeDestFs({ directoryExists: jasmine.createSpy("destFs.directoryExists").and.callFake((p: string) => p === angularSkillsDir - ) + ), + glob: jasmine.createSpy("destFs.glob").and.callFake((dir: string) => + dir === angularSkillsDir ? [skillFilePath] : [] + ), + readFile: jasmine.createSpy("destFs.readFile").and.returnValue(content) }); App.container.set(FS_TOKEN, destFs); @@ -634,23 +617,20 @@ describe("Unit - copyAISkillsToProject sources metadata", () => { const angularSkillsDir = skillsDir("igniteui-angular"); const skillFilePath = skillFile("igniteui-angular", "angular.md"); - spySrcFs({ - glob: spyOn(FsFileSystem.prototype, "glob").and.callFake((dir: string) => + const destFs = makeDestFs({ + directoryExists: jasmine.createSpy("destFs.directoryExists").and.callFake((p: string) => + p === angularSkillsDir + ), + glob: jasmine.createSpy("destFs.glob").and.callFake((dir: string) => dir === angularSkillsDir ? [skillFilePath] : [] ), - readFile: spyOn(FsFileSystem.prototype, "readFile").and.callFake((p: string) => { + readFile: jasmine.createSpy("destFs.readFile").and.callFake((p: string) => { if (p === "node_modules/igniteui-angular/package.json") { return JSON.stringify({ name: "igniteui-angular", version: "18.2.0" }); } return "# skill content"; }) }); - - const destFs = makeDestFs({ - directoryExists: jasmine.createSpy("destFs.directoryExists").and.callFake((p: string) => - p === angularSkillsDir - ) - }); App.container.set(FS_TOKEN, destFs); const result = copyAISkillsToProject(["claude"], "angular"); @@ -689,23 +669,20 @@ describe("Unit - copyAISkillsToProject sources metadata", () => { const angularSkillsDir = skillsDir("igniteui-angular"); const skillFilePath = skillFile("igniteui-angular", "angular.md"); - spySrcFs({ - glob: spyOn(FsFileSystem.prototype, "glob").and.callFake((dir: string) => + const destFs = makeDestFs({ + directoryExists: jasmine.createSpy("destFs.directoryExists").and.callFake((p: string) => + p === angularSkillsDir + ), + glob: jasmine.createSpy("destFs.glob").and.callFake((dir: string) => dir === angularSkillsDir ? [skillFilePath] : [] ), - readFile: spyOn(FsFileSystem.prototype, "readFile").and.callFake((p: string) => { + readFile: jasmine.createSpy("destFs.readFile").and.callFake((p: string) => { if (p === "node_modules/igniteui-angular/package.json") { throw new Error("ENOENT"); } return "# skill content"; }) }); - - const destFs = makeDestFs({ - directoryExists: jasmine.createSpy("destFs.directoryExists").and.callFake((p: string) => - p === angularSkillsDir - ) - }); App.container.set(FS_TOKEN, destFs); const result = copyAISkillsToProject(["claude"], "angular"); From b3c63781d65b668e0d7520824741b619d0416402 Mon Sep 17 00:00:00 2001 From: mstoyanova Date: Thu, 29 Oct 2026 14:47:06 +0200 Subject: [PATCH 10/10] Apply review comments --- .../igx-ts/projects/_base/index.ts | 33 +++++++++++---- .../igx-ts/projects/_base/scaffold.ts | 7 +++- spec/acceptance/new-spec.ts | 2 +- spec/templates/angular-scaffold-spec.ts | 42 +++++++++++++++++-- 4 files changed, 70 insertions(+), 14 deletions(-) diff --git a/packages/igx-templates/igx-ts/projects/_base/index.ts b/packages/igx-templates/igx-ts/projects/_base/index.ts index c055428111..d72f1d7ed5 100644 --- a/packages/igx-templates/igx-ts/projects/_base/index.ts +++ b/packages/igx-templates/igx-ts/projects/_base/index.ts @@ -3,7 +3,9 @@ import { } from "@igniteui/cli-core"; import * as fs from "fs"; import * as path from "path"; -import { CLI_PACKAGE, generateProject, npmInstall, patchAngularJson, patchPackageJson, updateJson } from "./scaffold"; +import { + CLI_PACKAGE, generateProject, npmInstall, npmInstallCommand, patchAngularJson, patchPackageJson, updateJson +} from "./scaffold"; /** What a project's own files need on top of the generated project */ export interface ProjectExtras { @@ -76,10 +78,12 @@ $app-palette: palette($primary, $secondary, $surface); return false; } if (options.skipInstall) { - Util.warn("--skip-install is ignored for Ignite UI for Angular projects: `ng add` installs the packages it adds.", + Util.warn("--skip-install does not apply to the initial setup: `ng add` installs the packages the project starts with.", "yellow"); } + /** The installs not yet done, as `npmInstall` arguments; set once the project files are complete */ + let installs: [string[], boolean][] = []; try { const angularMajor = generateProject(config.dashName, projectPath); const extras = this.extras(angularMajor); @@ -96,14 +100,27 @@ $app-palette: palette($primary, $secondary, $surface); }); } } - // also installs the package.json changes above; playwright is the browser for @vitest/browser-playwright - npmInstall(projectPath, ["playwright", ...extras.devDependencies], true); - if (extras.dependencies.length) { - npmInstall(projectPath, extras.dependencies); + // the first also installs the package.json changes above; playwright is the browser for @vitest/browser-playwright + installs = [[["playwright", ...extras.devDependencies], true], [extras.dependencies, false]]; + installs = installs.filter(([packages]) => packages.length); + while (installs.length) { + npmInstall(projectPath, ...installs[0]); + installs.shift(); } } catch (error) { - // the folder is this run's own (checked above), so a retry starts clean - fs.rmSync(projectPath, { recursive: true, force: true }); + if (installs.length) { + // the project is complete but for its packages (a network error, an unpublished CLI version), so it is kept + const commands = installs.map(([packages, dev]) => npmInstallCommand(packages, dev)).join(" && "); + Util.warn(`"${options.name}" is kept; finish installing its packages there with: ${commands}`, "yellow"); + } else { + // the folder is this run's own (checked above), so a retry starts clean + try { + fs.rmSync(projectPath, { recursive: true, force: true }); + } catch { + // e.g. EBUSY/EPERM on Windows while npm still holds native binaries + Util.warn(`Could not remove "${options.name}"; delete it before retrying.`, "yellow"); + } + } Util.error(`Project creation failed: ${error.message}`, "red"); return false; } diff --git a/packages/igx-templates/igx-ts/projects/_base/scaffold.ts b/packages/igx-templates/igx-ts/projects/_base/scaffold.ts index a6f9e97cef..19f3fbdcb6 100644 --- a/packages/igx-templates/igx-ts/projects/_base/scaffold.ts +++ b/packages/igx-templates/igx-ts/projects/_base/scaffold.ts @@ -59,12 +59,17 @@ export function generateProject(dashName: string, projectPath: string): string { return angularMajor; } +/** The `npm install` command `npmInstall` runs */ +export function npmInstallCommand(packages: string[], dev = false) { + return `npm install ${dev ? "--save-dev " : ""}${packages.join(" ")} --no-audit --no-fund`; +} + /** * Installs packages at their latest version, or at the version or range an entry names, * which also brings the lock file and node_modules in line with package.json */ export function npmInstall(projectPath: string, packages: string[], dev = false) { - Util.execSync(`npm install ${dev ? "--save-dev " : ""}${packages.join(" ")} --no-audit --no-fund`, { + Util.execSync(npmInstallCommand(packages, dev), { cwd: projectPath, stdio: "inherit" }); diff --git a/spec/acceptance/new-spec.ts b/spec/acceptance/new-spec.ts index 8d39108152..24bd7e3e16 100644 --- a/spec/acceptance/new-spec.ts +++ b/spec/acceptance/new-spec.ts @@ -156,7 +156,7 @@ describe("New command", () => { await cli.run(["new", projectName, "--framework=angular", "--type=igx-ts", "--skip-git", "--skip-install", "--theme=default"]); expect(Util.gitInit).not.toHaveBeenCalled(); expect(fs.existsSync("./" + projectName + "/.git")).not.toBeTruthy(); - expect(console.warn).toHaveBeenCalledWith(jasmine.stringMatching(/--skip-install is ignored/)); + expect(console.warn).toHaveBeenCalledWith(jasmine.stringMatching(/--skip-install does not apply to the initial setup/)); expect(Util.execSync).toHaveBeenCalledWith(jasmine.stringMatching(/^npm install /), jasmine.anything()); testFolder = projectName; diff --git a/spec/templates/angular-scaffold-spec.ts b/spec/templates/angular-scaffold-spec.ts index 5cf88c5289..8a8a5b12c4 100644 --- a/spec/templates/angular-scaffold-spec.ts +++ b/spec/templates/angular-scaffold-spec.ts @@ -140,7 +140,7 @@ describe("Angular project scaffold", () => { it("installs packages even with skipInstall, and says so", async () => { expect(await emptyProject.scaffold({ name, theme: "Default", skipInstall: true })).toBeTrue(); - expect(console.warn).toHaveBeenCalledWith(jasmine.stringMatching(/--skip-install is ignored/)); + expect(console.warn).toHaveBeenCalledWith(jasmine.stringMatching(/--skip-install does not apply to the initial setup/)); expect(commands()).toContain("npm install --save-dev playwright --no-audit --no-fund"); }); @@ -161,14 +161,48 @@ describe("Angular project scaffold", () => { expect(fs.existsSync(filePath("angular.json"))).toBeTrue(); }); - it("reports a failed command and removes the project folder", async () => { - execSync.withArgs(jasmine.stringMatching(/^npm install/), jasmine.anything()).and.throwError(new Error("Command failed: npm")); + it("reports a failed Angular CLI command and removes the project folder", async () => { + execSync.withArgs(jasmine.stringMatching(/ add igniteui-angular/), jasmine.anything()) + .and.throwError(new Error("Command failed: ng add")); expect(await emptyProject.scaffold({ name, theme: "Default" })).toBeFalse(); - expect(console.error).toHaveBeenCalledWith(jasmine.stringMatching(/Project creation failed: Command failed: npm/)); + expect(console.error).toHaveBeenCalledWith(jasmine.stringMatching(/Project creation failed: Command failed: ng add/)); expect(fs.existsSync(path.join(tmp, name))).withContext("a retry starts clean").toBeFalse(); }); + it("reports a project folder it could not remove instead of throwing", async () => { + execSync.withArgs(jasmine.stringMatching(/ add igniteui-angular/), jasmine.anything()) + .and.throwError(new Error("Command failed: ng add")); + // the module itself: `import * as fs` gives a read-only namespace + spyOn(require("fs"), "rmSync").and.callThrough() + .withArgs(path.join(process.cwd(), name), jasmine.anything()).and.throwError(new Error("EBUSY")); + + expect(await emptyProject.scaffold({ name, theme: "Default" })).toBeFalse(); + expect(console.warn).toHaveBeenCalledWith(jasmine.stringMatching(/Could not remove "Test Project"/)); + expect(console.error).toHaveBeenCalledWith(jasmine.stringMatching(/Project creation failed: Command failed: ng add/)); + }); + + it("keeps a project whose packages failed to install, with the installs left to run", async () => { + execSync.withArgs(jasmine.stringMatching(/^npm install --save-dev/), jasmine.anything()) + .and.throwError(new Error("Command failed: npm")); + + expect(await authProject.scaffold({ name, theme: "Default" })).toBeFalse(); + expect(console.error).toHaveBeenCalledWith(jasmine.stringMatching(/Project creation failed: Command failed: npm/)); + expect(fs.existsSync(filePath("angular.json"))).toBeTrue(); + expect(console.warn).toHaveBeenCalledWith(jasmine.stringMatching( + /"Test Project" is kept; .*npm install --save-dev playwright .* && npm install angular-auth-oidc-client@22 /)); + }); + + it("leaves only the installs that failed or did not run to the user", async () => { + execSync.withArgs(jasmine.stringMatching(/^npm install angular-auth-oidc-client/), jasmine.anything()) + .and.throwError(new Error("Command failed: npm")); + + expect(await authProject.scaffold({ name, theme: "Default" })).toBeFalse(); + const [message] = (console.warn as jasmine.Spy).calls.mostRecent().args; + expect(message).toContain("npm install angular-auth-oidc-client@22"); + expect(message).not.toContain("--save-dev"); + }); + it("leaves an existing folder alone", async () => { fs.mkdirSync(path.join(tmp, name)); fs.writeFileSync(filePath("keep.txt"), "");