Skip to content

Commit f2526df

Browse files
Merge pull request #81 from nicoschoenteich/main
feat: cap subgenerator
2 parents a3415a1 + 3a91a7d commit f2526df

File tree

13 files changed

+370
-47
lines changed

13 files changed

+370
-47
lines changed

README.md

+12-1
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,21 @@ This subgenerator adds a OPA5 journey (integration test) and page object to one
118118
119119
</details>
120120
121+
<details>
122+
<summary>cap</summary>
123+
124+
<br>
125+
126+
```bash
127+
yo easy-ui5 project cap
128+
```
129+
This subgenerator adds an SAP Cloud Application Programming Model (CAP) server to the project and connects one of the existing uimodules to it (via the [cds-plugin-ui5](https://www.npmjs.com/package/cds-plugin-ui5) and a dev dependency). This subgenerator is basically a convenvience wrapper around the [cds init](https://cap.cloud.sap/docs/get-started/in-a-nutshell#jumpstart) command - with the added benefit of constructing a deployment-ready `mta.yaml` file on root level of the project, that includes all necessary artifacts in line with the selected [deployment target](#deployment).
130+
131+
</details>
121132
122133
## Deployment
123134
124-
Projects created with this generator use the [Multitarget Application](https://sap.github.io/cloud-mta-build-tool/) approach can be built and deployed out of the box:
135+
Projects created with this generator use the [Multitarget Application](https://sap.github.io/cloud-mta-build-tool/) approach and can be built and deployed out of the box:
125136
126137
> Make sure you have the [Cloud Foundry CLI installed](https://developers.sap.com/tutorials/cp-cf-download-cli.html) and are logged in to your Cloud Foundry environment via the `cf login` command.
127138

generators/cap/index.js

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import chalk from "chalk"
2+
import fs from "fs"
3+
import Generator from "yeoman-generator"
4+
import prompts from "./prompts.js"
5+
import {
6+
lookForParentUI5ProjectAndPrompt,
7+
addModuleToNPMWorkspaces
8+
} from "../helpers.js"
9+
import dependencies from "../dependencies.js"
10+
import yaml from "yaml"
11+
12+
import ModelGenerator from "../model/index.js"
13+
import { createRequire } from "node:module"
14+
const require = createRequire(import.meta.url)
15+
16+
export default class extends Generator {
17+
static displayName = "Create a new SAP CAP module within an existing OpenUI5/SAPUI5 project"
18+
19+
async prompting() {
20+
if (this.config.get("platform") === "SAP NetWeaver") {
21+
this.log(chalk.red("This subgenerator cannot be run if your target deployment platform is SAP NetWeaver."))
22+
this.cancelCancellableTasks()
23+
return
24+
}
25+
await lookForParentUI5ProjectAndPrompt.call(this, prompts, true, "Which existing uimodule would you like to connect with your new SAP CAP module?")
26+
}
27+
28+
async writing() {
29+
this.log(chalk.green(`✨ creating new SAP CAP module for ${this.options.config.projectName}`))
30+
31+
// TO-DO: check for typescript and configure cap project accordingly
32+
this.spawnCommandSync("npx", ["-p", "@sap/cds-dk", "cds", "init", `${this.options.config.capName}`, "--add", "tiny-sample, data, xsuaa, mta, postgres"],
33+
this.destinationPath()
34+
)
35+
36+
addModuleToNPMWorkspaces.call(this, this.options.config.capName)
37+
}
38+
39+
async install() {
40+
fs.rmdirSync(this.destinationPath(`${this.options.config.capName}/app`))
41+
42+
const packageJson = JSON.parse(fs.readFileSync(this.destinationPath(`${this.options.config.capName}/package.json`)))
43+
delete packageJson["cds"] // go with the cds defaults (no auth required at dev time)
44+
if (!packageJson["devDependencies"]) packageJson["devDependencies"] = {}
45+
packageJson["devDependencies"]["@sap/cds-dk"] = dependencies["@sap/cds-dk"]
46+
packageJson["devDependencies"]["cds-plugin-ui5"] = dependencies["cds-plugin-ui5"]
47+
packageJson["devDependencies"][this.options.config.uimoduleName] = `../${this.options.config.uimoduleName}`
48+
packageJson["scripts"]["dev"] = "cds watch"
49+
packageJson["scripts"]["build"] = "cds build --production"
50+
fs.writeFileSync(this.destinationPath(`${this.options.config.capName}/package.json`), JSON.stringify(packageJson, null, 4))
51+
52+
// use parts of generated mta.yaml from "cds init" to enrich root mta.yaml
53+
const capMtaYaml = yaml.parse(fs.readFileSync(this.destinationPath(`${this.options.config.capName}/mta.yaml`)).toString())
54+
const rootMtaYaml = yaml.parse(fs.readFileSync(this.destinationPath("mta.yaml")).toString())
55+
if (!rootMtaYaml.resources) rootMtaYaml.resources = []
56+
57+
const authName = `${this.options.config.projectId}-auth`
58+
// use auth and xs-security.json from cap module
59+
if (["Static webserver", "Application Router"].includes(this.options.config.platform)) {
60+
const capAuth = capMtaYaml.resources.find(resource => resource.name === `${this.options.config.capName}-auth`)
61+
capAuth.name = authName
62+
capAuth.parameters.path = `${this.options.config.capName}/xs-security.json`
63+
if (this.options.config.platform === "Application Router") {
64+
capAuth.parameters.config["oauth2-configuration"] = {
65+
"redirect-uris": ["~{approuter/callback-url}"]
66+
}
67+
if (!capAuth.requires) capAuth.requires = []
68+
capAuth.requires.push({ name: "approuter" })
69+
const approuter = rootMtaYaml.modules.find(module => module.name === `${this.options.config.projectId}-approuter`)
70+
if (!approuter.requires) approuter.requires = []
71+
approuter.requires.push({ name: capAuth.name })
72+
}
73+
rootMtaYaml.resources.push(capAuth)
74+
}
75+
// use auth and xs-security.json from root
76+
else if (["SAP HTML5 Application Repository Service", "SAP Build Work Zone, standard edition"].includes(this.options.config.platform)) {
77+
fs.rename(this.destinationPath("xs-security.json"), `${this.options.config.capName}/xs-security.json`, err => { })
78+
const rootAuth = rootMtaYaml.resources.find(resource => resource.name === authName)
79+
rootAuth.parameters.path = `${this.options.config.capName}/xs-security.json`
80+
}
81+
82+
const capPostgres = capMtaYaml.resources.find(resource => resource.name === `${this.options.config.capName}-postgres`)
83+
capPostgres.name = `${this.options.config.projectId}-${capPostgres.name}`
84+
rootMtaYaml.resources.push(capPostgres)
85+
86+
const capDeployer = capMtaYaml.modules.find(module => module.name === `${this.options.config.capName}-postgres-deployer`)
87+
capDeployer.path = this.options.config.capName + "/" + capDeployer.path
88+
capDeployer.name = `${this.options.config.projectId}-${capDeployer.name}`
89+
capDeployer.requires = [
90+
{ name: capPostgres.name }
91+
]
92+
rootMtaYaml.modules.push(capDeployer)
93+
94+
const capSrv = capMtaYaml.modules.find(module => module.name === `${this.options.config.capName}-srv`)
95+
capSrv.path = this.options.config.capName + "/" + capSrv.path
96+
capSrv.name = `${this.options.config.projectId}-${capSrv.name}`
97+
capSrv.requires = [
98+
{ name: capPostgres.name },
99+
{ name: authName }
100+
]
101+
rootMtaYaml.modules.push(capSrv)
102+
103+
fs.unlinkSync(this.destinationPath(`${this.options.config.capName}/mta.yaml`))
104+
this.writeDestination(this.destinationPath("mta.yaml"), yaml.stringify(rootMtaYaml))
105+
106+
if (this.options.config.runModelSubgenerator) {
107+
this.composeWith(
108+
{
109+
Generator: ModelGenerator,
110+
path: require.resolve("../model")
111+
},
112+
{
113+
config: {
114+
projectId: this.options.config.projectId,
115+
uimoduleName: this.options.config.uimoduleName,
116+
platform: this.options.config.platform,
117+
modelName: "",
118+
modelType: "OData v4",
119+
modelUrl: "http://localhost:4004/odata/v4/catalog",
120+
setupProxy: true,
121+
setupRouteAndDest: ["Application Router", "SAP HTML5 Application Repository Service", "SAP Build Work Zone, standard edition"].includes(this.options.config.platform),
122+
destName: this.options.config.capName
123+
}
124+
125+
}
126+
)
127+
}
128+
}
129+
}

generators/cap/prompts.js

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {
2+
validateAlphaNumericStartingWithLetterNonEmpty
3+
} from "../helpers.js"
4+
5+
export default async function prompts() {
6+
7+
this.options.config.capName = (await this.prompt({
8+
type: "input",
9+
name: "capName",
10+
message: "How do you want to name your new SAP CAP server module?",
11+
default: "server",
12+
validate: validateAlphaNumericStartingWithLetterNonEmpty
13+
})).capName
14+
15+
this.options.config.runModelSubgenerator = (await this.prompt({
16+
type: "confirm",
17+
name: "runModelSubgenerator",
18+
message: "Do you want to add the SAP CAP service as the default model to your uimodule?",
19+
default: true
20+
})).runModelSubgenerator
21+
}

generators/dependencies.js

+2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
// dependencies for generated applications
22
// TO-DO: maybe add as peerDependencies for automated security checks via GitHub, then read programmatically from package.json
33
export default {
4+
"@sap/cds-dk": "^7",
45
"@sap-ux/eslint-plugin-fiori-tools": "^0.2",
56
"@sap/approuter": "latest",
67
"@ui5/linter": "latest",
8+
"cds-plugin-ui5": "latest",
79
"mbt": "^1",
810
"rimraf": "latest",
911
"OpenUI5": "1.120.13",

generators/helpers.js

+10-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import chalk from "chalk"
22
import fs from "fs"
33
import yaml from "yaml"
44

5-
export async function lookForParentUI5ProjectAndPrompt(prompts, uimodulePrompt = true) {
5+
export async function lookForParentUI5ProjectAndPrompt(prompts, uimodulePrompt = true, alternativePromptMessage) {
66
const readConfig = () => this.readDestinationJSON(".yo-rc.json")?.["generator-ui5-project"] || {}
77
this.options.config = readConfig()
88

@@ -24,7 +24,7 @@ export async function lookForParentUI5ProjectAndPrompt(prompts, uimodulePrompt =
2424
this.options.config.uimoduleName = (await this.prompt({
2525
type: "list",
2626
name: "uimoduleName",
27-
message: "For which uimodule would you like to call this subgenerator?",
27+
message: alternativePromptMessage || "For which uimodule would you like to call this subgenerator?",
2828
choices: this.options.config.uimodules
2929
})).uimoduleName
3030
}
@@ -33,6 +33,14 @@ export async function lookForParentUI5ProjectAndPrompt(prompts, uimodulePrompt =
3333
await prompts.call(this)
3434
}
3535

36+
export function addModuleToNPMWorkspaces(moduleName) {
37+
const rootPackageJson = JSON.parse(fs.readFileSync(this.destinationPath("package.json")))
38+
rootPackageJson.workspaces.push(moduleName)
39+
rootPackageJson.scripts[`start:${moduleName}`] = `npm start --workspace ${moduleName}`
40+
fs.writeFileSync(this.destinationPath("package.json"), JSON.stringify(rootPackageJson, null, 4))
41+
42+
}
43+
3644
export async function ensureCorrectDestinationPath() {
3745
// required when called from fpmpage subgenerator
3846
if (!this.destinationPath().endsWith(this.options.config.uimoduleName)) {

generators/model/index.js

+51-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ export default class extends Generator {
99
static displayName = "Create a new model for an existing uimodule."
1010

1111
async prompting() {
12-
await lookForParentUI5ProjectAndPrompt.call(this, prompts)
12+
if (!this.options.config) {
13+
this.standaloneCall = true
14+
await lookForParentUI5ProjectAndPrompt.call(this, prompts)
15+
}
1316
}
1417

1518
async writing() {
@@ -119,6 +122,52 @@ export default class extends Generator {
119122
this.writeDestination(this.destinationPath(ui5YamlPath), yaml.stringify(ui5Yaml))
120123
}
121124
}
122-
}
123125

126+
// set up route and destination
127+
if (this.options.config.setupRouteAndDest) {
128+
const rootMtaYaml = yaml.parse(fs.readFileSync(this.destinationPath("mta.yaml")).toString())
129+
const destination = rootMtaYaml.resources.find(resource => resource.name === `${this.options.config.projectId}-destination-service`)
130+
if (!destination.parameters.config) destination.parameters.config = {}
131+
if (!destination.parameters.config.init_data) destination.parameters.config.init_data = {}
132+
if (!destination.parameters.config.init_data.instance) destination.parameters.config.init_data.instance = {
133+
existing_destinations_policy: "update",
134+
destinations: []
135+
}
136+
destination.parameters.config.init_data.instance.destinations.push({
137+
Name: this.options.config.destName,
138+
Authentication: "NoAuthentication",
139+
ProxyType: "Internet",
140+
Type: "HTTP",
141+
URL: this.standaloneCall ? serviceUrl.origin : "~{srv-api/srv-url}",
142+
"HTML5.DynamicDestination": true,
143+
"HTML5.ForwardAuthToken": true
144+
})
145+
if (!this.standaloneCall) {
146+
if (!destination.requires) destination.requires = []
147+
destination.requires.push({ name: "srv-api" })
148+
}
149+
this.writeDestination(this.destinationPath("mta.yaml"), yaml.stringify(rootMtaYaml))
150+
151+
let xsappJsonPath
152+
switch (this.options.config.platform) {
153+
case "Application Router":
154+
xsappJsonPath = this.destinationPath("approuter/xs-app.json")
155+
break
156+
157+
case "SAP HTML5 Application Repository Service":
158+
case "SAP Build Work Zone, standard edition":
159+
xsappJsonPath = this.destinationPath(`${this.options.config.uimoduleName}/webapp/xs-app.json`)
160+
break
161+
}
162+
const xsappJson = JSON.parse(fs.readFileSync(xsappJsonPath))
163+
xsappJson.routes.unshift({
164+
source: `${serviceUrl.pathname}(.*)`,
165+
destination: this.options.config.destName,
166+
authenticationType: "none"
167+
})
168+
const wildcardRoute = xsappJson.routes.find(route => route.source === "^(.*)$")
169+
wildcardRoute.authenticationType = "xsuaa"
170+
this.writeDestinationJSON(xsappJsonPath, xsappJson, null, 4)
171+
}
172+
}
124173
}

generators/model/prompts.js

+44-20
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,51 @@ export default async function prompts() {
1313
default: ""
1414
})).modelName
1515

16-
this.options.config.modelType = (await this.prompt({
17-
type: "list",
18-
name: "modelType",
19-
message: "Which type of model do you want to add?",
20-
choices: ["OData v4", "OData v2", "JSON"],
21-
default: "OData v4"
22-
})).modelType
16+
if (!this.options.config.modelType) {
17+
this.options.config.modelType = (await this.prompt({
18+
type: "list",
19+
name: "modelType",
20+
message: "Which type of model do you want to add?",
21+
choices: ["OData v4", "OData v2", "JSON"],
22+
default: "OData v4"
23+
})).modelType
24+
}
2325

24-
this.options.config.modelUrl = (await this.prompt({
25-
type: "input",
26-
name: "modelUrl",
27-
message: "What is the data source url of your service?",
28-
when: this.options.config.modelType.includes("OData"),
29-
validate: validateUrl
30-
})).modelUrl
26+
if (!this.options.config.modelUrl) {
27+
this.options.config.modelUrl = (await this.prompt({
28+
type: "input",
29+
name: "modelUrl",
30+
message: "What is the data source url of your service?",
31+
when: this.options.config.modelType.includes("OData"),
32+
validate: validateUrl
33+
})).modelUrl
34+
}
35+
36+
if (!this.options.config.setupProxy) {
37+
this.options.config.setupProxy = (await this.prompt({
38+
type: "confirm",
39+
name: "setupProxy",
40+
message: "Do you want to set up a proxy for the new model?",
41+
when: this.options.config.modelType.includes("OData")
42+
})).setupProxy
43+
}
3144

32-
this.options.config.setupProxy = (await this.prompt({
33-
type: "confirm",
34-
name: "setupProxy",
35-
message: "Do you want to set up a proxy for the new model?",
36-
when: this.options.config.modelType.includes("OData")
37-
})).setupProxy
45+
if (this.options.config.setupProxy && !this.options.config.setupRouteAndDest) {
46+
if (["Application Router", "SAP HTML5 Application Repository Service", "SAP Build Work Zone, standard edition"].includes(this.options.config.platform)) {
47+
this.options.config.setupRouteAndDest = (await this.prompt({
48+
type: "confirm",
49+
name: "setupRouteAndDest",
50+
message: "Do you want to set up a route (xs-app.json) and destination for your new model?",
51+
})).setupRouteAndDest
52+
}
53+
}
3854

55+
if (this.options.config.setupRouteAndDest && !this.options.config.destName) {
56+
this.options.config.destName = (await this.prompt({
57+
type: "input",
58+
name: "destName",
59+
message: "How do you want to name your new destination?",
60+
validate: validateAlphaNumeric
61+
})).destName
62+
}
3963
}

generators/project/templates/managed-approuter/mta.yaml

+6-6
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ modules:
1414
- name: <%= projectId %>-destination-content
1515
type: com.sap.application.content
1616
requires:
17-
- name: <%= projectId %>-uaa
17+
- name: <%= projectId %>-auth
1818
- name: <%= projectId %>-html-repo-host
1919
- name: <%= projectId %>-destination-service
2020
parameters:
@@ -28,9 +28,9 @@ modules:
2828
ServiceInstanceName: <%= projectId %>-html-repo-host
2929
ServiceKeyName: <%= projectId %>-html-repo-host-key
3030
sap.cloud.service: basic.service
31-
- Name: <%= projectId %>-uaa
32-
ServiceInstanceName: <%= projectId %>-uaa
33-
ServiceKeyName: <%= projectId %>-uaa-key
31+
- Name: <%= projectId %>-auth
32+
ServiceInstanceName: <%= projectId %>-auth
33+
ServiceKeyName: <%= projectId %>-auth-key
3434
sap.cloud.service: basic.service
3535
Authentication: OAuth2UserTokenExchange
3636
build-parameters:
@@ -69,11 +69,11 @@ resources:
6969
service-keys:
7070
- name: <%= projectId %>-html-repo-host-key
7171

72-
- name: <%= projectId %>-uaa
72+
- name: <%= projectId %>-auth
7373
type: org.cloudfoundry.managed-service
7474
parameters:
7575
path: ./xs-security.json
7676
service: xsuaa
7777
service-plan: application
7878
service-keys:
79-
- name: <%= projectId %>-uaa-key
79+
- name: <%= projectId %>-auth-key

0 commit comments

Comments
 (0)