@cbs-consulting/generator-btp 1.1.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,27 @@
1
+ import assert from "yeoman-assert";
2
+
3
+ /**
4
+ * Validates that devcontainer.json is correctly templated with user inputs
5
+ * @param {string} customerName - Original customer name
6
+ * @param {string} projectName - Original project name
7
+ * @param {string} expectedCustomerNormalized - Expected normalized customer name
8
+ * @param {string} expectedProjectNormalized - Expected normalized project name
9
+ */
10
+ export function assertDevcontainerTemplating(
11
+ customerName,
12
+ projectName,
13
+ expectedCustomerNormalized,
14
+ expectedProjectNormalized,
15
+ ) {
16
+ // Verify container name field uses original names
17
+ const containerNameRegex = new RegExp(
18
+ `"name":\\s*"${customerName.replace(/\s+/g, "\\s*")}\\s*-\\s*${projectName.replace(/\s+/g, "\\s*")}"`,
19
+ );
20
+ assert.fileContent(".devcontainer/devcontainer.json", containerNameRegex);
21
+
22
+ // Verify docker container name uses normalized names
23
+ const dockerNameRegex = new RegExp(
24
+ `"--name=${expectedCustomerNormalized}\\.${expectedProjectNormalized}"`,
25
+ );
26
+ assert.fileContent(".devcontainer/devcontainer.json", dockerNameRegex);
27
+ }
@@ -0,0 +1,129 @@
1
+ import { dirname, join } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import * as helpers from "yeoman-test";
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+
8
+ describe("generator-btp:app", () => {
9
+ // ========================================
10
+ // Generator: CAP
11
+ // ========================================
12
+ describe("when user selects CAP", () => {
13
+ let runResult;
14
+
15
+ // Run the generator with CAP selection
16
+ // Mock the spawn to prevent actual command execution during tests
17
+ beforeAll(async () => {
18
+ runResult = await helpers
19
+ .run(join(__dirname, "../index.js"))
20
+ .withPrompts({
21
+ kind: "cap",
22
+ })
23
+ .withOptions({
24
+ skipInstall: true,
25
+ })
26
+ .withSpawnMock((command, args) => {
27
+ // Mock all external commands
28
+ return;
29
+ });
30
+ });
31
+
32
+ // Restore the environment after tests
33
+ afterAll(() => {
34
+ if (runResult) {
35
+ runResult.restore();
36
+ }
37
+ });
38
+
39
+ // Test that the correct generator is composed based on user selection
40
+ it("should compose with cap generator", () => {
41
+ expect(runResult.generator.kind).toBe("cap");
42
+ });
43
+ });
44
+
45
+ // ========================================
46
+ // Generator: UI5
47
+ // ========================================
48
+ describe("when user selects UI5", () => {
49
+ let runResult;
50
+
51
+ // Run the generator with UI5 selection
52
+ beforeAll(async () => {
53
+ runResult = await helpers
54
+ .run(join(__dirname, "../index.js"))
55
+ .withPrompts({
56
+ kind: "ui5",
57
+ });
58
+ });
59
+
60
+ // Restore the environment after tests
61
+ afterAll(() => {
62
+ if (runResult) {
63
+ runResult.restore();
64
+ }
65
+ });
66
+
67
+ // Test that the correct generator is composed based on user selection
68
+ it("should compose with ui5 generator", () => {
69
+ expect(runResult.generator.kind).toBe("ui5");
70
+ });
71
+ });
72
+
73
+ // ========================================
74
+ // Generator: dev container
75
+ // ========================================
76
+ describe("when user selects devcontainer", () => {
77
+ let runResult;
78
+
79
+ // Run the generator with devcontainer selection
80
+ beforeAll(async () => {
81
+ runResult = await helpers
82
+ .run(join(__dirname, "../index.js"))
83
+ .withPrompts({
84
+ kind: "devcontainer",
85
+ });
86
+ });
87
+
88
+ // Restore the environment after tests
89
+ afterAll(() => {
90
+ if (runResult) {
91
+ runResult.restore();
92
+ }
93
+ });
94
+
95
+ // Test that the correct generator is composed based on user selection
96
+ it("should compose with devcontainer generator", () => {
97
+ expect(runResult.generator.kind).toBe("devcontainer");
98
+ });
99
+ });
100
+
101
+ // ========================================
102
+ // Test all prompt choices
103
+ // ========================================
104
+ describe("prompting", () => {
105
+ it.each(["cap", "ui5", "devcontainer"])(
106
+ "should accept '%s' as valid choice",
107
+ async (choice) => {
108
+ const runResult = await helpers
109
+ .run(join(__dirname, "../index.js"))
110
+ .withPrompts({
111
+ kind: choice,
112
+ })
113
+ .withOptions({
114
+ skipInstall: true,
115
+ })
116
+ .withSpawnMock((command, args) => {
117
+ // Mock all external commands
118
+ return;
119
+ });
120
+
121
+ // Verify the choice was stored correctly
122
+ expect(runResult.generator.kind).toBeDefined();
123
+ expect(runResult.generator.kind).toBe(choice);
124
+
125
+ runResult.restore();
126
+ },
127
+ );
128
+ });
129
+ });
@@ -1,9 +1,13 @@
1
- const Generator = require("yeoman-generator");
2
- const chalk = require("chalk");
3
- const yosay = require("yosay");
4
- const { resolve } = require("node:path");
1
+ import Generator from "yeoman-generator";
2
+ import chalk from "chalk";
3
+ import yosay from "yosay";
4
+ import { resolve, dirname } from "node:path";
5
+ import { fileURLToPath } from "node:url";
5
6
 
6
- module.exports = class extends Generator {
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+
10
+ export default class extends Generator {
7
11
  async prompting() {
8
12
  this.log(
9
13
  yosay(
@@ -51,4 +55,4 @@ module.exports = class extends Generator {
51
55
  end() {
52
56
  this.log(chalk.green("Done."));
53
57
  }
54
- };
58
+ }
@@ -0,0 +1,209 @@
1
+ import { dirname, join } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import assert from "yeoman-assert";
4
+ import * as helpers from "yeoman-test";
5
+ import { assertDevcontainerTemplating } from "../../__tests__/helpers.js";
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+
10
+ describe("generator-btp:cap", () => {
11
+ describe("with default values and npm install", () => {
12
+ let runResult;
13
+
14
+ beforeAll(async () => {
15
+ runResult = await helpers
16
+ .run(join(__dirname, "../index.js"))
17
+ .withPrompts({
18
+ projectName: "Test CAP Project",
19
+ customerName: "Test Customer",
20
+ dbSchema: "TEST_SCHEMA",
21
+ useAzureDevOps: true,
22
+ });
23
+ }, 120000);
24
+
25
+ afterAll(() => {
26
+ if (runResult) {
27
+ runResult.restore();
28
+ }
29
+ });
30
+
31
+ it("should not cause install errors", () => {
32
+ expect(runResult.generator).toBeDefined();
33
+ });
34
+ });
35
+
36
+ describe.each([
37
+ {
38
+ scenario: "with Azure DevOps enabled",
39
+ projectName: "Test CAP Project",
40
+ customerName: "Test Customer",
41
+ dbSchema: "TEST_SCHEMA",
42
+ useAzureDevOps: true,
43
+ expectedProjectNormalized: "test-cap-project",
44
+ expectedCustomerNormalized: "test-customer",
45
+ },
46
+ {
47
+ scenario: "without Azure DevOps",
48
+ projectName: "No Azure Project",
49
+ customerName: "My Customer",
50
+ dbSchema: "MY_SCHEMA",
51
+ useAzureDevOps: false,
52
+ expectedProjectNormalized: "no-azure-project",
53
+ expectedCustomerNormalized: "my-customer",
54
+ },
55
+ {
56
+ scenario: "with uppercase names",
57
+ projectName: "UPPERCASE PROJECT",
58
+ customerName: "UPPERCASE CUSTOMER",
59
+ dbSchema: "UPPER_SCHEMA",
60
+ useAzureDevOps: false,
61
+ expectedProjectNormalized: "uppercase-project",
62
+ expectedCustomerNormalized: "uppercase-customer",
63
+ },
64
+ {
65
+ scenario: "with multiple spaces",
66
+ projectName: "Multi Space Project",
67
+ customerName: "Multi Space Customer",
68
+ dbSchema: "MULTI_SCHEMA",
69
+ useAzureDevOps: true,
70
+ expectedProjectNormalized: "multi-space-project",
71
+ expectedCustomerNormalized: "multi-space-customer",
72
+ },
73
+ ])(
74
+ "$scenario",
75
+ ({
76
+ projectName,
77
+ customerName,
78
+ dbSchema,
79
+ useAzureDevOps,
80
+ expectedProjectNormalized,
81
+ expectedCustomerNormalized,
82
+ }) => {
83
+ let runResult;
84
+
85
+ beforeAll(async () => {
86
+ runResult = await helpers
87
+ .run(join(__dirname, "../index.js"))
88
+ .withPrompts({
89
+ projectName,
90
+ customerName,
91
+ dbSchema,
92
+ useAzureDevOps,
93
+ })
94
+ .withOptions({
95
+ skipInstall: true,
96
+ })
97
+ .withSpawnMock((command, args) => {
98
+ // Mock external commands
99
+ if (command === "git" && args[0] === "init") {
100
+ return;
101
+ }
102
+ if (command === "cds" && args[0] === "init") {
103
+ return;
104
+ }
105
+ if (command === "npm") {
106
+ return;
107
+ }
108
+ if (command === "npx" && args[0] === "simple-git-hooks") {
109
+ return;
110
+ }
111
+ });
112
+ }, 30000);
113
+
114
+ afterAll(() => {
115
+ if (runResult) {
116
+ runResult.restore();
117
+ }
118
+ });
119
+
120
+ it("should store and normalize inputs correctly", () => {
121
+ expect(runResult.generator.answers.projectName).toBe(projectName);
122
+ expect(runResult.generator.answers.customerName).toBe(customerName);
123
+ expect(runResult.generator.answers.dbSchema).toBe(dbSchema);
124
+ expect(runResult.generator.answers.useAzureDevOps).toBe(useAzureDevOps);
125
+ expect(runResult.generator.answers.projectNameNormalized).toBe(
126
+ expectedProjectNormalized,
127
+ );
128
+ expect(runResult.generator.answers.customerNameNormalized).toBe(
129
+ expectedCustomerNormalized,
130
+ );
131
+ });
132
+
133
+ it("should create base configuration files", () => {
134
+ assert.file([
135
+ ".gitignore",
136
+ ".npmrc",
137
+ ".gitattributes",
138
+ ".gitconfig.aliases",
139
+ "base.tsconfig.json",
140
+ "eslint.config.mjs",
141
+ "prettier.config.mjs",
142
+ "mta.yaml",
143
+ ]);
144
+ });
145
+
146
+ it("should create devcontainer configuration", () => {
147
+ assert.file([
148
+ ".devcontainer/devcontainer.json",
149
+ ".devcontainer/Dockerfile",
150
+ ".devcontainer/post-create.sh",
151
+ ]);
152
+ });
153
+
154
+ it("should create MTA extension files", () => {
155
+ assert.file([
156
+ "mta-ext/dev.mtaext",
157
+ "mta-ext/test.mtaext",
158
+ "mta-ext/prod.mtaext",
159
+ ]);
160
+ });
161
+
162
+ it("should conditionally create Azure DevOps pipeline file", () => {
163
+ if (useAzureDevOps) {
164
+ assert.file(["azure-pipelines.yaml"]);
165
+ } else {
166
+ assert.noFile(["azure-pipelines.yaml"]);
167
+ }
168
+ });
169
+
170
+ it("should not create undeploy.json", () => {
171
+ assert.noFile(["db/undeploy.json"]);
172
+ });
173
+
174
+ it("should template mta.yaml with correct values", () => {
175
+ // Check MTA ID uses normalized project name
176
+ const mtaIdRegex = new RegExp(`ID:\\s*${expectedProjectNormalized}`);
177
+ assert.fileContent("mta.yaml", mtaIdRegex);
178
+
179
+ // Check MTA description uses original project name
180
+ const mtaDescRegex = new RegExp(
181
+ `description:\\s*MTA for ${projectName.replace(/\s+/g, "\\s+")}`,
182
+ );
183
+ assert.fileContent("mta.yaml", mtaDescRegex);
184
+
185
+ // Check module name uses normalized name
186
+ const moduleNameRegex = new RegExp(
187
+ `name:\\s*${expectedProjectNormalized}-srv`,
188
+ );
189
+ assert.fileContent("mta.yaml", moduleNameRegex);
190
+ });
191
+
192
+ it("should template devcontainer with correct values", () => {
193
+ assertDevcontainerTemplating(
194
+ customerName,
195
+ projectName,
196
+ expectedCustomerNormalized,
197
+ expectedProjectNormalized,
198
+ );
199
+ });
200
+
201
+ it("should template package.json with normalized name", () => {
202
+ assert.fileContent(
203
+ "package.json",
204
+ new RegExp(`"name":\\s*"${expectedProjectNormalized}"`),
205
+ );
206
+ });
207
+ },
208
+ );
209
+ });
@@ -1,17 +1,17 @@
1
- const chalk = require("chalk");
2
- const glob = require("glob");
3
- const mergewith = require("lodash.mergewith");
4
- const fs = require("node:fs");
5
- const { join, resolve } = require("node:path");
6
- const Generator = require("yeoman-generator");
7
- const {
8
- mergeArray,
9
- readJsonC,
10
- readJsonCSafe,
11
- } = require("../../utils/jsonFile");
1
+ import chalk from "chalk";
2
+ import glob from "glob";
3
+ import mergewith from "lodash.mergewith";
4
+ import fs from "node:fs";
5
+ import { join, resolve, dirname } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import Generator from "yeoman-generator";
8
+ import { mergeArray, readJsonC, readJsonCSafe } from "../../utils/jsonFile.js";
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
12
  const globSync = glob.globSync || glob.sync;
13
13
 
14
- module.exports = class extends Generator {
14
+ export default class extends Generator {
15
15
  _tplRoot = resolve(__dirname, "./templates");
16
16
  _addRoot = resolve(__dirname, "./additions");
17
17
  _capRoot = __dirname;
@@ -215,4 +215,4 @@ module.exports = class extends Generator {
215
215
  this.log("Next steps:");
216
216
  this.log("1) Review/commit the changes.");
217
217
  }
218
- };
218
+ }
@@ -0,0 +1,104 @@
1
+ import { dirname, join } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import assert from "yeoman-assert";
4
+ import * as helpers from "yeoman-test";
5
+ import { assertDevcontainerTemplating } from "../../__tests__/helpers.js";
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+
10
+ describe("generator-btp:devcontainer", () => {
11
+ describe.each([
12
+ {
13
+ scenario: "default values",
14
+ projectName: "My Project",
15
+ customerName: "My Customer",
16
+ expectedProjectNormalized: "my-project",
17
+ expectedCustomerNormalized: "my-customer",
18
+ },
19
+ {
20
+ scenario: "simple names",
21
+ projectName: "MyProject",
22
+ customerName: "MyCustomer",
23
+ expectedProjectNormalized: "myproject",
24
+ expectedCustomerNormalized: "mycustomer",
25
+ },
26
+ {
27
+ scenario: "names with spaces",
28
+ projectName: "My Test Project",
29
+ customerName: "My Test Customer",
30
+ expectedProjectNormalized: "my-test-project",
31
+ expectedCustomerNormalized: "my-test-customer",
32
+ },
33
+ {
34
+ scenario: "uppercase names",
35
+ projectName: "UPPERCASE PROJECT",
36
+ customerName: "UPPERCASE CUSTOMER",
37
+ expectedProjectNormalized: "uppercase-project",
38
+ expectedCustomerNormalized: "uppercase-customer",
39
+ },
40
+ {
41
+ scenario: "mixed case with multiple spaces",
42
+ projectName: "Multi Word Project",
43
+ customerName: "Multi Word Customer",
44
+ expectedProjectNormalized: "multi-word-project",
45
+ expectedCustomerNormalized: "multi-word-customer",
46
+ },
47
+ ])(
48
+ "with $scenario",
49
+ ({
50
+ projectName,
51
+ customerName,
52
+ expectedProjectNormalized,
53
+ expectedCustomerNormalized,
54
+ }) => {
55
+ let runResult;
56
+
57
+ beforeAll(async () => {
58
+ runResult = await helpers
59
+ .run(join(__dirname, "../index.js"))
60
+ .withPrompts({
61
+ projectName,
62
+ customerName,
63
+ })
64
+ .withOptions({
65
+ skipInstall: true,
66
+ });
67
+ });
68
+
69
+ afterAll(() => {
70
+ if (runResult) {
71
+ runResult.restore();
72
+ }
73
+ });
74
+
75
+ it("should create all devcontainer files", () => {
76
+ assert.file([
77
+ ".devcontainer/devcontainer.json",
78
+ ".devcontainer/Dockerfile",
79
+ ".devcontainer/post-create.sh",
80
+ ]);
81
+ });
82
+
83
+ it("should store and normalize input correctly", () => {
84
+ expect(runResult.generator.answers.projectName).toBe(projectName);
85
+ expect(runResult.generator.answers.customerName).toBe(customerName);
86
+ expect(runResult.generator.answers.projectNameNormalized).toBe(
87
+ expectedProjectNormalized,
88
+ );
89
+ expect(runResult.generator.answers.customerNameNormalized).toBe(
90
+ expectedCustomerNormalized,
91
+ );
92
+ });
93
+
94
+ it("should apply templating correctly", () => {
95
+ assertDevcontainerTemplating(
96
+ customerName,
97
+ projectName,
98
+ expectedCustomerNormalized,
99
+ expectedProjectNormalized,
100
+ );
101
+ });
102
+ },
103
+ );
104
+ });
@@ -1,11 +1,15 @@
1
- const chalk = require("chalk");
2
- const glob = require("glob");
3
- const fs = require("node:fs");
4
- const { join, resolve } = require("node:path");
5
- const Generator = require("yeoman-generator");
1
+ import chalk from "chalk";
2
+ import glob from "glob";
3
+ import fs from "node:fs";
4
+ import { join, resolve, dirname } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import Generator from "yeoman-generator";
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
6
10
  const globSync = glob.globSync || glob.sync;
7
11
 
8
- module.exports = class extends Generator {
12
+ export default class extends Generator {
9
13
  _tplRoot = resolve(__dirname, "./templates");
10
14
 
11
15
  async initializing() {
@@ -75,4 +79,4 @@ module.exports = class extends Generator {
75
79
  this.log("1) Review the .devcontainer folder.");
76
80
  this.log("2) Customize the Dockerfile and devcontainer.json as needed.");
77
81
  }
78
- };
82
+ }
@@ -0,0 +1,249 @@
1
+ import { dirname, join } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import assert from "yeoman-assert";
4
+ import * as helpers from "yeoman-test";
5
+ import fs from "node:fs";
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+
10
+ describe("generator-btp:ui5", () => {
11
+ describe("with existing UI5 apps", () => {
12
+ let runResult;
13
+
14
+ beforeAll(async () => {
15
+ runResult = await helpers
16
+ .run(join(__dirname, "../index.js"))
17
+ .withOptions({
18
+ skipInstall: true,
19
+ })
20
+ .inTmpDir((dir) => {
21
+ // Create mock UI5 app structure
22
+ const appDir = join(dir, "app", "myui5app");
23
+ fs.mkdirSync(appDir, { recursive: true });
24
+
25
+ // Create a minimal ui5.yaml to mark it as a UI5 app
26
+ fs.writeFileSync(
27
+ join(appDir, "ui5.yaml"),
28
+ `specVersion: "3.0"
29
+ metadata:
30
+ name: myui5app
31
+ type: application
32
+ `,
33
+ );
34
+
35
+ // Create initial package.json
36
+ fs.writeFileSync(
37
+ join(appDir, "package.json"),
38
+ JSON.stringify(
39
+ {
40
+ name: "myui5app",
41
+ version: "1.0.0",
42
+ scripts: {},
43
+ },
44
+ null,
45
+ 2,
46
+ ),
47
+ );
48
+
49
+ // Create initial tsconfig.json
50
+ fs.writeFileSync(
51
+ join(appDir, "tsconfig.json"),
52
+ JSON.stringify(
53
+ {
54
+ compilerOptions: {
55
+ target: "es2020",
56
+ },
57
+ },
58
+ null,
59
+ 2,
60
+ ),
61
+ );
62
+ });
63
+ }, 120000);
64
+
65
+ afterAll(() => {
66
+ if (runResult) {
67
+ runResult.restore();
68
+ }
69
+ });
70
+
71
+ it("should detect UI5 apps", () => {
72
+ expect(runResult.generator._detectedUi5Apps).toBeDefined();
73
+ expect(runResult.generator._detectedUi5Apps.length).toBeGreaterThan(0);
74
+ });
75
+
76
+ it("should create eslint.config.mjs in UI5 app", () => {
77
+ assert.file(["app/myui5app/eslint.config.mjs"]);
78
+ });
79
+
80
+ it("should create ui5lint.config.mjs in UI5 app", () => {
81
+ assert.file(["app/myui5app/ui5lint.config.mjs"]);
82
+ });
83
+
84
+ it("should merge package.json with additions", () => {
85
+ assert.file(["app/myui5app/package.json"]);
86
+ const pkgPath = join(runResult.cwd, "app", "myui5app", "package.json");
87
+ const pkgContent = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
88
+
89
+ // Original fields should be preserved
90
+ expect(pkgContent.name).toBe("myui5app");
91
+ expect(pkgContent.version).toBe("1.0.0");
92
+
93
+ // New scripts should be merged
94
+ expect(pkgContent.scripts).toBeDefined();
95
+ expect(pkgContent.scripts.lint).toBe("eslint .");
96
+ expect(pkgContent.scripts["lint-fix"]).toBe("eslint . --fix");
97
+ expect(pkgContent.scripts.ui5lint).toBe("ui5lint");
98
+
99
+ // CDS configuration should be added
100
+ expect(pkgContent.cds).toBeDefined();
101
+ expect(pkgContent.cds.typer).toBeDefined();
102
+
103
+ // Dev dependencies should be added
104
+ expect(pkgContent.devDependencies).toBeDefined();
105
+ expect(pkgContent.devDependencies.eslint).toBeDefined();
106
+ expect(pkgContent.devDependencies["typescript-eslint"]).toBeDefined();
107
+ expect(
108
+ pkgContent.devDependencies["eslint-config-prettier"],
109
+ ).toBeDefined();
110
+ expect(pkgContent.devDependencies["cbs-tools"]).toBeDefined();
111
+ expect(pkgContent.devDependencies["@ui5/linter"]).toBeDefined();
112
+ });
113
+
114
+ it("should update tsconfig.json with rootDirs", () => {
115
+ assert.file(["app/myui5app/tsconfig.json"]);
116
+ const tsPath = join(runResult.cwd, "app", "myui5app", "tsconfig.json");
117
+ const tsContent = JSON.parse(fs.readFileSync(tsPath, "utf-8"));
118
+
119
+ expect(tsContent.compilerOptions).toBeDefined();
120
+
121
+ // Original compilerOptions should be preserved
122
+ expect(tsContent.compilerOptions.target).toBe("es2020");
123
+
124
+ // rootDirs should be added/merged
125
+ expect(tsContent.compilerOptions.rootDirs).toBeDefined();
126
+ expect(Array.isArray(tsContent.compilerOptions.rootDirs)).toBe(true);
127
+ expect(tsContent.compilerOptions.rootDirs).toContain("./webapp");
128
+ expect(tsContent.compilerOptions.rootDirs).toContain("../../@cds-models");
129
+ });
130
+ });
131
+
132
+ describe("with multiple UI5 apps", () => {
133
+ let runResult;
134
+
135
+ beforeAll(async () => {
136
+ runResult = await helpers
137
+ .run(join(__dirname, "../index.js"))
138
+ .withOptions({
139
+ skipInstall: true,
140
+ })
141
+ .inTmpDir((dir) => {
142
+ // Create two mock UI5 apps
143
+ const app1Dir = join(dir, "app", "app1");
144
+ const app2Dir = join(dir, "app", "app2");
145
+
146
+ fs.mkdirSync(app1Dir, { recursive: true });
147
+ fs.mkdirSync(app2Dir, { recursive: true });
148
+
149
+ // Create ui5.yaml for both apps
150
+ fs.writeFileSync(
151
+ join(app1Dir, "ui5.yaml"),
152
+ `specVersion: "3.0"
153
+ metadata:
154
+ name: app1
155
+ type: application
156
+ `,
157
+ );
158
+ fs.writeFileSync(
159
+ join(app2Dir, "ui5.yaml"),
160
+ `specVersion: "3.0"
161
+ metadata:
162
+ name: app2
163
+ type: application
164
+ `,
165
+ );
166
+
167
+ // Create package.json for both apps
168
+ fs.writeFileSync(
169
+ join(app1Dir, "package.json"),
170
+ JSON.stringify({ name: "app1" }, null, 2),
171
+ );
172
+ fs.writeFileSync(
173
+ join(app2Dir, "package.json"),
174
+ JSON.stringify({ name: "app2" }, null, 2),
175
+ );
176
+
177
+ // Create tsconfig.json for both apps
178
+ fs.writeFileSync(
179
+ join(app1Dir, "tsconfig.json"),
180
+ JSON.stringify({ compilerOptions: {} }, null, 2),
181
+ );
182
+ fs.writeFileSync(
183
+ join(app2Dir, "tsconfig.json"),
184
+ JSON.stringify({ compilerOptions: {} }, null, 2),
185
+ );
186
+ })
187
+ .withSpawnMock((command, args) => {
188
+ if (command === "npm") {
189
+ return;
190
+ }
191
+ });
192
+ });
193
+
194
+ afterAll(() => {
195
+ if (runResult) {
196
+ runResult.restore();
197
+ }
198
+ });
199
+
200
+ it("should detect multiple UI5 apps", () => {
201
+ expect(runResult.generator._detectedUi5Apps.length).toBe(2);
202
+ });
203
+
204
+ it("should create config files for app1", () => {
205
+ assert.file([
206
+ "app/app1/eslint.config.mjs",
207
+ "app/app1/ui5lint.config.mjs",
208
+ ]);
209
+ });
210
+
211
+ it("should create config files for app2", () => {
212
+ assert.file([
213
+ "app/app2/eslint.config.mjs",
214
+ "app/app2/ui5lint.config.mjs",
215
+ ]);
216
+ });
217
+ });
218
+
219
+ describe("without UI5 apps", () => {
220
+ let runResult;
221
+
222
+ beforeAll(async () => {
223
+ runResult = await helpers
224
+ .run(join(__dirname, "../index.js"))
225
+ .withOptions({
226
+ skipInstall: true,
227
+ })
228
+ .inTmpDir((dir) => {
229
+ // Create app directory but no UI5 apps
230
+ fs.mkdirSync(join(dir, "app"), { recursive: true });
231
+ })
232
+ .withSpawnMock((command, args) => {
233
+ if (command === "npm") {
234
+ return;
235
+ }
236
+ });
237
+ });
238
+
239
+ afterAll(() => {
240
+ if (runResult) {
241
+ runResult.restore();
242
+ }
243
+ });
244
+
245
+ it("should detect no UI5 apps", () => {
246
+ expect(runResult.generator._detectedUi5Apps.length).toBe(0);
247
+ });
248
+ });
249
+ });
@@ -1,15 +1,15 @@
1
- const Generator = require("yeoman-generator");
2
- const fs = require("node:fs");
3
- const { join } = require("node:path");
4
- const chalk = require("chalk");
5
- const mergewith = require("lodash.mergewith");
6
- const {
7
- readJsonC,
8
- readJsonCSafe,
9
- mergeArray,
10
- } = require("../../utils/jsonFile");
1
+ import Generator from "yeoman-generator";
2
+ import fs from "node:fs";
3
+ import { join, dirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import chalk from "chalk";
6
+ import mergewith from "lodash.mergewith";
7
+ import { readJsonC, readJsonCSafe, mergeArray } from "../../utils/jsonFile.js";
11
8
 
12
- module.exports = class extends Generator {
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+
12
+ export default class extends Generator {
13
13
  _tplRoot = join(__dirname, "templates");
14
14
  _addRoot = join(__dirname, "additions");
15
15
  _ui5Root = __dirname;
@@ -117,4 +117,4 @@ module.exports = class extends Generator {
117
117
  this.log("Next steps:");
118
118
  this.log("1) Review/commit the changes.");
119
119
  }
120
- };
120
+ }
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@cbs-consulting/generator-btp",
3
- "version": "1.1.3",
3
+ "version": "1.2.0",
4
+ "type": "module",
4
5
  "description": "Yeoman generator for bootstrapping CAP/UI5 projects with TypeScript, ESLint, and other essential configurations",
5
6
  "main": "generators/app/index.js",
6
7
  "scripts": {
@@ -8,33 +9,39 @@
8
9
  "unlink": "npm unlink @cbs-consulting/generator-btp",
9
10
  "check-deps": "ncu",
10
11
  "upgrade-deps": "ncu -u --target minor",
11
- "commit": "git add . && npx cz"
12
+ "commit": "git add . && npx cz",
13
+ "test": "vitest run",
14
+ "test:watch": "vitest",
15
+ "test:coverage": "vitest run --coverage"
12
16
  },
13
17
  "files": [
14
18
  "generators",
15
19
  "utils"
16
20
  ],
17
21
  "dependencies": {
18
- "chalk": "4.1.2",
22
+ "chalk": "5.6.2",
19
23
  "comment-json": "4.5.1",
20
24
  "lodash.mergewith": "4.6.2",
21
25
  "lodash.union": "4.6.0",
22
- "yeoman-generator": "5.10.0",
23
- "yosay": "2.0.2"
26
+ "yeoman-generator": "7.5.1",
27
+ "yosay": "3.0.0"
24
28
  },
25
29
  "devDependencies": {
26
30
  "@commitlint/cli": "20.4.1",
27
31
  "@commitlint/config-conventional": "20.4.1",
28
32
  "@semantic-release/changelog": "6.0.3",
29
33
  "@semantic-release/git": "10.0.1",
34
+ "@vitest/coverage-v8": "4.0.18",
35
+ "@yeoman/adapter": "4.0.1",
30
36
  "commitizen": "4.3.1",
31
37
  "cz-conventional-changelog": "3.3.0",
32
- "jest": "30.2.0",
33
38
  "npm-check-updates": "19.3.2",
34
39
  "prettier": "3.8.1",
35
40
  "semantic-release": "25.0.3",
41
+ "vitest": "4.0.18",
36
42
  "yeoman-assert": "3.1.1",
37
- "yeoman-test": "7.4.0"
43
+ "yeoman-environment": "5.1.3",
44
+ "yeoman-test": "11.2.0"
38
45
  },
39
46
  "publishConfig": {
40
47
  "access": "public"
package/utils/jsonFile.js CHANGED
@@ -1,6 +1,6 @@
1
- const fs = require("node:fs");
2
- const { parse } = require("comment-json");
3
- const union = require("lodash.union");
1
+ import fs from "node:fs";
2
+ import { parse } from "comment-json";
3
+ import union from "lodash.union";
4
4
 
5
5
  // Read JSON (with comments) from generator source path
6
6
  function readJsonC(filePath, defaults) {
@@ -34,4 +34,4 @@ function mergeArray(objValue, srcValue) {
34
34
  }
35
35
  }
36
36
 
37
- module.exports = { readJsonC, readJsonCSafe, mergeArray };
37
+ export { readJsonC, readJsonCSafe, mergeArray };