Skip to content

Commit 76b0446

Browse files
committed
Add versions file and dynamic ng versioning
1 parent 0f48aaa commit 76b0446

10 files changed

Lines changed: 126 additions & 28 deletions

File tree

src/steps/addAnimations.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import fs from "node:fs";
22
import path from "node:path";
3-
import { npmInstall } from "../utils/shell.js";
3+
import { addAnimationsCompat, npmInstall } from "../utils/shell.js";
44
import type { WizardContext } from "../utils/types.js";
55

66
/**
@@ -9,8 +9,7 @@ import type { WizardContext } from "../utils/types.js";
99
* @param ctx The wizard context.
1010
*/
1111
export async function addAnimations(ctx: WizardContext): Promise<void> {
12-
console.log("Installing @angular/animations...");
13-
await npmInstall(["@angular/animations"], ctx.targetDir);
12+
await addAnimationsCompat(ctx.targetDir);
1413

1514
const appConfigPath = path.join(ctx.targetDir, "src", "app", "app.config.ts");
1615
if (!fs.existsSync(appConfigPath)) {

src/steps/addHusky.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import { npmInstall } from "../utils/shell.js";
55
import { readJsonLoose, writeJson } from "../utils/json.js";
66
import { execa } from "execa";
77
import { mergeScript } from "../utils/scripts.js";
8+
import { VERSIONS } from "../utils/versions.js";
89

910
export async function addHusky(ctx: WizardContext): Promise<void> {
10-
console.log("Adding Husky pre-commit hooks...");
11-
12-
await npmInstall(["-D", "husky@^9.1.7", "is-ci@^3.0.1"], ctx.targetDir);
11+
await npmInstall(
12+
["-D", `husky@${VERSIONS.husky}`, `is-ci@${VERSIONS["is-ci"]}`],
13+
ctx.targetDir
14+
);
1315

1416
const pkgPath = path.join(ctx.targetDir, "package.json");
1517
const pkg = readJsonLoose<Record<string, any>>(pkgPath) ?? {};
@@ -24,8 +26,8 @@ export async function addHusky(ctx: WizardContext): Promise<void> {
2426
pkg.scripts = scripts;
2527
pkg.devDependencies = {
2628
...(pkg.devDependencies ?? {}),
27-
husky: pkg.devDependencies?.husky ?? "^9.1.7",
28-
"is-ci": pkg.devDependencies?.["is-ci"] ?? "^3.0.1",
29+
husky: pkg.devDependencies?.husky ?? VERSIONS.husky,
30+
"is-ci": pkg.devDependencies?.["is-ci"] ?? VERSIONS["is-ci"],
2931
};
3032
writeJson(pkgPath, pkg);
3133

src/steps/addLint.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { npmInstall } from "../utils/shell.js";
66
import { readJsonLoose, writeJson } from "../utils/json.js";
77
import { copyTemplateAndReplace } from "../utils/files.js";
88
import { toKebab } from "../utils/name.js";
9+
import { VERSIONS } from "../utils/versions.js";
910

1011
/**
1112
* Adds ESLint (flat config) to the Angular project.
@@ -21,13 +22,12 @@ export async function addLint(ctx: WizardContext): Promise<void> {
2122
initial: defaultPrefix,
2223
});
2324

24-
console.log("Adding ESLint (flat config)...");
2525
await npmInstall(
2626
[
2727
"-D",
28-
"angular-eslint@20.1.1",
29-
"eslint@^9.33.0",
30-
"typescript-eslint@8.39.1",
28+
`angular-eslint@${VERSIONS["angular-eslint"]}`,
29+
`eslint@${VERSIONS.eslint}`,
30+
`typescript-eslint@${VERSIONS["typescript-eslint"]}`,
3131
],
3232
ctx.targetDir
3333
);

src/steps/addPrettier.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,21 @@ import { npmInstall } from "../utils/shell.js";
44
import type { WizardContext } from "../utils/types.js";
55
import { copyFileFromTemplates, filesRoot } from "../utils/files.js";
66
import { readJsonLoose, writeJson } from "../utils/json.js";
7+
import { VERSIONS } from "../utils/versions.js";
78

89
/**
910
* Adds Prettier formatting to the Angular project.
1011
*
1112
* @param ctx The wizard context.
1213
*/
1314
export async function addPrettier(ctx: WizardContext): Promise<void> {
14-
console.log("Adding Prettier formatting...");
15-
1615
// Install dev deps
1716
await npmInstall(
18-
["-D", "prettier", "@trivago/prettier-plugin-sort-imports"],
17+
[
18+
"-D",
19+
`prettier@${VERSIONS.prettier}`,
20+
`@trivago/prettier-plugin-sort-imports@${VERSIONS["@trivago/prettier-plugin-sort-imports"]}`,
21+
],
1922
ctx.targetDir
2023
);
2124

@@ -36,10 +39,6 @@ export async function addPrettier(ctx: WizardContext): Promise<void> {
3639
const extPath = path.join(vscodeDir, "extensions.json");
3740
const settingsPath = path.join(vscodeDir, "settings.json");
3841

39-
const extTemplate = readJsonLoose<{ recommendations?: string[] }>(
40-
path.join(path.dirname(vscodeDir), "files/vscode/extensions.json")
41-
);
42-
4342
// Merge or create extensions
4443
const existingExt =
4544
readJsonLoose<{ recommendations?: string[] }>(extPath) ?? {};
@@ -61,10 +60,10 @@ export async function addPrettier(ctx: WizardContext): Promise<void> {
6160
const pkg = readJsonLoose<Record<string, any>>(pkgPath) ?? {};
6261
pkg.devDependencies = {
6362
...(pkg.devDependencies ?? {}),
64-
prettier: pkg.devDependencies?.prettier ?? "^3.6.0",
63+
prettier: pkg.devDependencies?.prettier ?? VERSIONS.prettier,
6564
"@trivago/prettier-plugin-sort-imports":
6665
pkg.devDependencies?.["@trivago/prettier-plugin-sort-imports"] ??
67-
"^5.2.2",
66+
VERSIONS["@trivago/prettier-plugin-sort-imports"],
6867
};
6968
pkg.scripts = {
7069
...(pkg.scripts ?? {}),

src/steps/postCreatePackages.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import prompts from "prompts";
2-
import { runNgAdd } from "../utils/shell.js";
2+
import { addCdkCompat, addMaterialCompat, runNgAdd } from "../utils/shell.js";
33
import type { WizardContext } from "../utils/types.js";
44
import { addAnimations } from "./addAnimations.js";
55
import { addPrettier } from "./addPrettier.js";
@@ -59,28 +59,32 @@ export async function postCreatePackages(
5959
);
6060

6161
if (post.prettier) {
62+
console.log("Adding Prettier formatting...");
6263
await addPrettier(ctx);
6364
}
6465

6566
if (post.lint) {
67+
console.log("Adding ESLint (flat config)...");
6668
await addLint(ctx);
6769
}
6870

6971
if (post.husky) {
72+
console.log("Adding Husky pre-commit hooks...");
7073
await addHusky(ctx);
7174
}
7275

7376
if (post.animations) {
77+
console.log("Installing @angular/animations...");
7478
await addAnimations(ctx);
7579
}
7680

7781
if (post.cdk) {
7882
console.log("Adding @angular/cdk...");
79-
await runNgAdd("@angular/cdk", ctx.targetDir);
83+
await addCdkCompat(ctx.targetDir);
8084
}
8185

8286
if (post.material) {
8387
console.log("Adding @angular/material...");
84-
await runNgAdd("@angular/material", ctx.targetDir);
88+
await addMaterialCompat(ctx.targetDir);
8589
}
8690
}

src/utils/angular-version.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import path from "node:path";
2+
import { readJsonLoose } from "./json.js";
3+
import { VERSIONS } from "./versions.js";
4+
5+
/**
6+
* Returns the @angular/core version from the created workspace if available.
7+
* Falls back to VERSIONS["@angular/cli"] major if not resolvable.
8+
*
9+
* @param targetDir The target directory of the Angular workspace.
10+
* @returns The Angular version string.
11+
*/
12+
export function getWorkspaceAngularVersion(targetDir: string): string {
13+
const pkgPath = path.join(targetDir, "package.json");
14+
const pkg = readJsonLoose<Record<string, any>>(pkgPath);
15+
const core = pkg?.dependencies?.["@angular/core"] as string | undefined;
16+
if (core) return core;
17+
18+
// Fallback: use the CLI pin as a caret range of the same major
19+
const cli = VERSIONS["@angular/cli"];
20+
21+
// e.g. "^20.1.7" -> "^20"
22+
const major = cli.replace(/^\^?(\d+).*/, "^$1");
23+
return major || "^20";
24+
}

src/utils/files.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ const __dirname = path.dirname(__filename);
1111
* @param relPath The relative path within the 'files' directory
1212
* @param targetAbsPath The absolute path where the file should be copied to
1313
*/
14-
export function copyFileFromTemplates(relPath: string, targetAbsPath: string) {
14+
export function copyFileFromTemplates(
15+
relPath: string,
16+
targetAbsPath: string
17+
): void {
1518
const from = path.join(filesRoot(), relPath);
1619
fs.mkdirSync(path.dirname(targetAbsPath), { recursive: true });
1720
fs.copyFileSync(from, targetAbsPath);
@@ -29,7 +32,7 @@ export function copyTemplateAndReplace(
2932
relPath: string,
3033
targetAbsPath: string,
3134
replacements: Record<string, string>
32-
) {
35+
): void {
3336
const from = path.join(filesRoot(), relPath);
3437
const raw = fs.readFileSync(from, "utf8");
3538
const out = Object.entries(replacements).reduce(

src/utils/json.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import fs from "node:fs";
22

3+
/**
4+
* Reads a JSON file leniently, ignoring comments and trailing commas.
5+
*
6+
* @param file The path to the JSON file.
7+
* @returns The parsed JSON object or null if not found or invalid.
8+
*/
39
export function readJsonLoose<T = any>(file: string): T | null {
410
if (!fs.existsSync(file)) return null;
511
const raw = fs.readFileSync(file, "utf8");
@@ -17,7 +23,13 @@ export function readJsonLoose<T = any>(file: string): T | null {
1723
}
1824
}
1925

20-
export function writeJson(file: string, data: unknown) {
26+
/**
27+
* Writes an object to a JSON file with pretty formatting.
28+
*
29+
* @param file The path to the JSON file.
30+
* @param data The data to write to the file.
31+
*/
32+
export function writeJson(file: string, data: unknown): void {
2133
const out = JSON.stringify(data, null, 2) + "\n";
2234
fs.writeFileSync(file, out, "utf8");
2335
}

src/utils/shell.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,40 @@
11
import { execa } from "execa";
2+
import { getWorkspaceAngularVersion } from "./angular-version.js";
3+
import { VERSIONS } from "./versions.js";
4+
5+
/**
6+
* Adds @angular/animations with a version compatible with the workspace Angular version.
7+
*
8+
* @param targetDir The target directory of the Angular workspace.
9+
*/
10+
export async function addAnimationsCompat(targetDir: string): Promise<void> {
11+
const v = getWorkspaceAngularVersion(targetDir);
12+
const majorCaret = v.replace(/^\^?(\d+).*/, "^$1");
13+
await npmInstall([`@angular/animations@${majorCaret}`], targetDir);
14+
}
15+
16+
/**
17+
* Adds @angular/cdk with a version compatible with the workspace Angular version.
18+
*
19+
* @param targetDir The target directory of the Angular workspace.
20+
*/
21+
export async function addCdkCompat(targetDir: string): Promise<void> {
22+
const v = getWorkspaceAngularVersion(targetDir);
23+
const majorCaret = v.replace(/^\^?(\d+).*/, "^$1");
24+
await runNgAdd(`@angular/cdk@${majorCaret}`, targetDir);
25+
}
26+
27+
/**
28+
* Adds @angular/material with a version compatible with the workspace Angular version.
29+
*
30+
* @param targetDir The target directory of the Angular workspace.
31+
*/
32+
export async function addMaterialCompat(targetDir: string): Promise<void> {
33+
const v = getWorkspaceAngularVersion(targetDir); // e.g. "^20.1.6"
34+
// convert to caret major to be resilient across patches
35+
const majorCaret = v.replace(/^\^?(\d+).*/, "^$1"); // "^20"
36+
await runNgAdd(`@angular/material@${majorCaret}`, targetDir);
37+
}
238

339
/**
440
* Checks if a command exists by attempting to run it with the --version flag.
@@ -20,7 +56,13 @@ export async function cmdExists(cmd: string): Promise<boolean> {
2056
*/
2157
export async function installGlobalAngularCli(): Promise<void> {
2258
// Windows may need elevated PowerShell. We simply run npm and let npm prompt as needed.
23-
await execa("npm", ["install", "-g", "@angular/cli"], { stdio: "inherit" });
59+
await execa(
60+
"npm",
61+
["install", "-g", `@angular/cli@${VERSIONS["@angular/cli"]}`],
62+
{
63+
stdio: "inherit",
64+
}
65+
);
2466
}
2567

2668
/**

src/utils/versions.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* The last known versions of various packages used in the wizard.
3+
*/
4+
export const VERSIONS = {
5+
"@angular/cli": "^20.1.7",
6+
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
7+
"angular-eslint": "^20.1.1",
8+
"is-ci": "^3.0.1",
9+
"typescript-eslint": "^8.39.1",
10+
eslint: "^9.33.0",
11+
husky: "^9.1.7",
12+
prettier: "^3.6.0",
13+
} as const;

0 commit comments

Comments
 (0)