@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.
- package/generators/__tests__/helpers.js +27 -0
- package/generators/app/__tests__/index.test.js +129 -0
- package/generators/app/index.js +10 -6
- package/generators/cap/__tests__/index.test.js +209 -0
- package/generators/cap/index.js +13 -13
- package/generators/devcontainer/__tests__/index.test.js +104 -0
- package/generators/devcontainer/index.js +11 -7
- package/generators/ui5/__tests__/index.test.js +249 -0
- package/generators/ui5/index.js +12 -12
- package/package.json +14 -7
- package/utils/jsonFile.js +4 -4
|
@@ -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
|
+
});
|
package/generators/app/index.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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
|
+
});
|
package/generators/cap/index.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
+
});
|
package/generators/ui5/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
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.
|
|
23
|
-
"yosay": "
|
|
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-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
37
|
+
export { readJsonC, readJsonCSafe, mergeArray };
|