@cap-js/cds-typer 0.25.0 → 0.27.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/CHANGELOG.md +26 -2
- package/README.md +19 -0
- package/cds-plugin.js +11 -4
- package/lib/cli.js +174 -36
- package/lib/compile.js +10 -16
- package/lib/components/basedefs.js +6 -0
- package/lib/components/enum.js +4 -2
- package/lib/components/inline.js +2 -1
- package/lib/components/javascript.js +28 -0
- package/lib/components/wrappers.js +42 -3
- package/lib/config.js +117 -0
- package/lib/file.js +115 -26
- package/lib/resolution/resolver.js +17 -4
- package/lib/typedefs.d.ts +54 -27
- package/lib/util.js +17 -64
- package/lib/visitor.js +78 -57
- package/package.json +8 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,30 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
6
6
|
|
|
7
|
-
## Version 0.
|
|
7
|
+
## Version 0.28.0 - TBD
|
|
8
|
+
|
|
9
|
+
## Version 0.27.0 - 2024-10-02
|
|
10
|
+
### Changed
|
|
11
|
+
- Any configuration variable (via CLI or `cds.env`) can now be passed in snake_case in addition to camelCase
|
|
12
|
+
- Action parameters are now generated as optional by default, which is how the runtime treats them. Mandatory parameters have to be marked as `not null` in CDS/CDL, or `notNull` in CSN.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- Fix build task for projects with spaces
|
|
16
|
+
- Fixa bug where cds-typer would produce redundant type declarations when the model contains an associations to another entity's property
|
|
17
|
+
|
|
18
|
+
## Version 0.26.0 - 2024-09-11
|
|
19
|
+
### Added
|
|
20
|
+
- Added a static `.keys` property in all entities. That property is dictionary which holds all properties as keys that are marked as `key` in CDS
|
|
21
|
+
- Added a CLI option `--useEntitiesProxy`. When set to `true`, all entities are wrapped into `Proxy` objects during runtime, allowing top level imports of entity types.
|
|
22
|
+
- Added a static `.kind` property for entities and types, which contains `'entity'` or `'type'` respectively
|
|
23
|
+
- Apps need to provide `@sap/cds` version `8.2` or higher.
|
|
24
|
+
- Apps need to provide `@cap-js/cds-types` version `0.6.4` or higher.
|
|
25
|
+
- Typed methods are now generated for calls of unbound actions. Named and positional call styles are supported, e.g. `service.action({one, two})` and `service.action(one, two)`.
|
|
26
|
+
- Action parameters can be optional in the named call style (`service.action({one:1, ...})`).
|
|
27
|
+
- Actions for ABAP RFC modules cannot be called with positional parameters, but only with named ones. They have 'parameter categories' (import/export/changing/tables) that cannot be called in a flat order.
|
|
28
|
+
- Services now have their own export (named like the service itself). The current default export is not usable in some scenarios from CommonJS modules.
|
|
29
|
+
- Enums and operation parameters can have doc comments
|
|
30
|
+
|
|
8
31
|
|
|
9
32
|
## Version 0.25.0 - 2024-08-13
|
|
10
33
|
### Added
|
|
@@ -34,6 +57,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
|
34
57
|
|
|
35
58
|
|
|
36
59
|
## Version 0.22.0 - 2024-06-20
|
|
60
|
+
|
|
37
61
|
### Fixed
|
|
38
62
|
- Fixed a bug where keys would sometimes inconsistently become nullable
|
|
39
63
|
|
|
@@ -85,7 +109,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
|
85
109
|
- Importing an enum into a service will now generate an alias to the original enum, instead of incorrectly duplicating the definition
|
|
86
110
|
- Returning entities from actions/ functions and using them as parameters will now properly use the singular inflection instead of returning an array thereof
|
|
87
111
|
- Aspects are now consistently named and called in their singular form
|
|
88
|
-
- Only detect inflection clash if singular and plural share the same namespace. This also no longer reports `sap.common` as erroneous during type creation
|
|
112
|
+
- Only detect inflection clash if singular and plural share the same namespace. This also no longer reports `sap.common` as erroneous during type creation
|
|
89
113
|
|
|
90
114
|
## Version 0.19.0 - 2024-03-28
|
|
91
115
|
### Added
|
package/README.md
CHANGED
|
@@ -9,6 +9,25 @@ Generates `.ts` files for a CDS model to receive code completion in VS Code.
|
|
|
9
9
|
|
|
10
10
|
Exhaustive documentation can be found on [CAPire](https://cap.cloud.sap/docs/tools/cds-typer).
|
|
11
11
|
|
|
12
|
+
## Known Restrictions
|
|
13
|
+
|
|
14
|
+
Certain language features of CDS can not be represented in TypeScript.
|
|
15
|
+
Trying to generate types for models using these features will therefore result in incorrect or broken TypeScript code.
|
|
16
|
+
|
|
17
|
+
### Changing Types
|
|
18
|
+
|
|
19
|
+
While the following is valid CDS, there is no TypeScript equivalent that would allow the type of an inherited property to change (TS2416).
|
|
20
|
+
|
|
21
|
+
```cds
|
|
22
|
+
entity A {
|
|
23
|
+
foo: Integer
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
entity B: A {
|
|
27
|
+
foo: String
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
12
31
|
## Support, Feedback, Contributing
|
|
13
32
|
|
|
14
33
|
This project is open to feature requests/suggestions, bug reports etc. via [GitHub issues](https://github.com/cap-js/cds-typer/issues). Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](CONTRIBUTING.md).
|
package/cds-plugin.js
CHANGED
|
@@ -36,7 +36,7 @@ const rmDirIfExists = dir => {
|
|
|
36
36
|
* Remove files with given extensions from a directory recursively.
|
|
37
37
|
* @param {string} dir - The directory to start from.
|
|
38
38
|
* @param {string[]} exts - The extensions to remove.
|
|
39
|
-
* @returns {Promise<
|
|
39
|
+
* @returns {Promise<unknown>}
|
|
40
40
|
*/
|
|
41
41
|
const rmFiles = async (dir, exts) => fs.existsSync(dir)
|
|
42
42
|
? Promise.all(
|
|
@@ -68,10 +68,15 @@ cds.build?.register?.('typescript', class extends cds.build.Plugin {
|
|
|
68
68
|
|
|
69
69
|
get #appFolder () { return cds?.env?.folders?.app ?? 'app' }
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* cds.env > tsconfig.compilerOptions.paths > '@cds-models' (default)
|
|
73
|
+
*/
|
|
71
74
|
get #modelDirectoryName () {
|
|
75
|
+
const outputDirectory = cds.env.typer?.outputDirectory
|
|
76
|
+
if (outputDirectory) return outputDirectory
|
|
72
77
|
try {
|
|
73
78
|
// expected format: { '#cds-models/*': [ './@cds-models/*/index.ts' ] }
|
|
74
|
-
//
|
|
79
|
+
// ^^^^^^^^^^^
|
|
75
80
|
// relevant part - may be changed by user
|
|
76
81
|
const config = JSON.parse(fs.readFileSync ('tsconfig.json', 'utf8'))
|
|
77
82
|
const alias = config.compilerOptions.paths['#cds-models/*'][0]
|
|
@@ -89,7 +94,9 @@ cds.build?.register?.('typescript', class extends cds.build.Plugin {
|
|
|
89
94
|
|
|
90
95
|
async #runCdsTyper () {
|
|
91
96
|
DEBUG?.('running cds-typer')
|
|
92
|
-
|
|
97
|
+
cds.env.typer ??= {}
|
|
98
|
+
cds.env.typer.outputDirectory ??= this.#modelDirectoryName
|
|
99
|
+
await typer.compileFromFile('*')
|
|
93
100
|
}
|
|
94
101
|
|
|
95
102
|
async #buildWithConfig () {
|
|
@@ -103,7 +110,7 @@ cds.build?.register?.('typescript', class extends cds.build.Plugin {
|
|
|
103
110
|
DEBUG?.('building without config')
|
|
104
111
|
// this will include gen/ that was created by the nodejs task
|
|
105
112
|
// _within_ the project directory. So we need to remove it afterwards.
|
|
106
|
-
await exec(`npx tsc --outDir ${this.task.dest}`)
|
|
113
|
+
await exec(`npx tsc --outDir "${this.task.dest}"`)
|
|
107
114
|
rmDirIfExists(path.join(this.task.dest, cds.env.build.target))
|
|
108
115
|
rmDirIfExists(path.join(this.task.dest, this.#appFolder))
|
|
109
116
|
}
|
package/lib/cli.js
CHANGED
|
@@ -2,20 +2,147 @@
|
|
|
2
2
|
/* eslint-disable no-console */
|
|
3
3
|
'use strict'
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {import('./typedefs').config.cli.ParameterSchema[number]} Parameter
|
|
7
|
+
*/
|
|
8
|
+
|
|
5
9
|
const cds = require('@sap/cds')
|
|
6
10
|
const { compileFromFile } = require('./compile')
|
|
7
|
-
const {
|
|
11
|
+
const { camelToSnake } = require('./util')
|
|
8
12
|
const { deprecated, _keyFor } = require('./logging')
|
|
9
13
|
const path = require('path')
|
|
10
14
|
const { EOL } = require('node:os')
|
|
15
|
+
const { camelSnakeHybrid, configuration } = require('./config')
|
|
11
16
|
|
|
12
17
|
const EOL2 = EOL + EOL
|
|
13
18
|
const toolName = 'cds-typer'
|
|
14
|
-
|
|
15
19
|
// @ts-expect-error - nope, it is actually there. Types just seem to be out of sync.
|
|
16
20
|
const lls = cds.log.levels
|
|
21
|
+
const parameterTypes = {
|
|
22
|
+
boolean:
|
|
23
|
+
/**
|
|
24
|
+
* @param {Parameter} props - additional parameter properties
|
|
25
|
+
* @returns {Parameter}
|
|
26
|
+
*/
|
|
27
|
+
props => ({...{
|
|
28
|
+
allowed: ['true', 'false'],
|
|
29
|
+
type: 'boolean',
|
|
30
|
+
postprocess: (/** @type {string} */ value) => value === 'true'
|
|
31
|
+
},
|
|
32
|
+
...props})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Adds additional properties to the CLI parameter schema.
|
|
37
|
+
* @param {import('./typedefs').config.cli.ParameterSchema } flags - The CLI parameter schema.
|
|
38
|
+
* @returns {import('./typedefs').config.cli.ParameterSchema} - The enriched schema.
|
|
39
|
+
*/
|
|
40
|
+
const enrichFlagSchema = flags => {
|
|
41
|
+
const flagKeys = Object.keys(flags)
|
|
42
|
+
for (const [key, value] of Object.entries(flags)) {
|
|
43
|
+
/** @type {Parameter} */(value).camel = key;
|
|
44
|
+
/** @type {Parameter} */(value).snake = camelToSnake(key)
|
|
45
|
+
}
|
|
46
|
+
// non-enumerable utilities
|
|
47
|
+
Object.defineProperties(flags, {
|
|
48
|
+
hasFlag: {
|
|
49
|
+
value: (/** @type {string} **/ flag) => Object.values(flags).some(f => f.snake === flag || f.camel === flag)
|
|
50
|
+
},
|
|
51
|
+
keys: {
|
|
52
|
+
value: flagKeys
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
return camelSnakeHybrid(flags)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parses command line arguments into named and positional parameters.
|
|
60
|
+
* Named parameters are expected to start with a double dash (--).
|
|
61
|
+
* If the next argument `B` after a named parameter `A` is not a named parameter itself,
|
|
62
|
+
* `B` is used as value for `A`.
|
|
63
|
+
* If `A` and `B` are both named parameters, `A` is just treated as a flag (and may receive a default value).
|
|
64
|
+
* Only named parameters that occur in validFlags are allowed. Specifying named flags that are not listed there
|
|
65
|
+
* will cause an error.
|
|
66
|
+
* Named parameters that are either not specified or do not have a value assigned to them may draw a default value
|
|
67
|
+
* from their definition in validFlags.
|
|
68
|
+
* @param {string[]} argv - list of command line arguments
|
|
69
|
+
* @param {import('./typedefs').config.cli.ParameterSchema} schema - allowed flags. May specify default values.
|
|
70
|
+
* @returns {import('./typedefs').config.cli.ParsedParameters} - parsed arguments
|
|
71
|
+
*/
|
|
72
|
+
const parseCommandlineArgs = (argv, schema) => {
|
|
73
|
+
const isFlag = (/** @type {string} */ arg) => arg.startsWith('--')
|
|
74
|
+
const positional = []
|
|
75
|
+
/** @type {import('./typedefs').config.cli.ParsedParameters['named']} */
|
|
76
|
+
const named = {}
|
|
77
|
+
|
|
78
|
+
let i = 0
|
|
79
|
+
while (i < argv.length) {
|
|
80
|
+
const originalArgName = argv[i] // so our feedback to the user is less confusing
|
|
81
|
+
let arg = originalArgName
|
|
82
|
+
if (isFlag(arg)) {
|
|
83
|
+
arg = camelToSnake(arg.slice(2))
|
|
84
|
+
// @ts-expect-error - cba to add hasFlag to the general dictionary
|
|
85
|
+
if (!schema.hasFlag(arg)) {
|
|
86
|
+
throw new Error(`invalid named flag '${originalArgName}'`)
|
|
87
|
+
}
|
|
88
|
+
const next = argv[i + 1]
|
|
89
|
+
if (next && !isFlag(next)) {
|
|
90
|
+
named[arg] = { value: next, isDefault: false }
|
|
91
|
+
i++
|
|
92
|
+
} else {
|
|
93
|
+
named[arg] = { value: schema[arg].default, isDefault: true }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const { allowed, allowedHint } = schema[arg]
|
|
97
|
+
if (allowed && !allowed.includes(named[arg].value)) {
|
|
98
|
+
throw new Error(`invalid value '${named[arg]?.value ?? named[arg]}' for flag ${originalArgName}. Must be one of ${(allowedHint ?? allowed.join(', '))}`)
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
positional.push(arg)
|
|
102
|
+
}
|
|
103
|
+
i++
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// enrich with defaults
|
|
107
|
+
/** @type {import('./typedefs').config.cli.ParameterSchema} */
|
|
108
|
+
const defaults = Object.entries(schema)
|
|
109
|
+
.filter(e => !!e[1].default)
|
|
110
|
+
.reduce((dict, [k, v]) => {
|
|
111
|
+
// @ts-expect-error - can't infer type of initial {}
|
|
112
|
+
dict[camelToSnake(k)] = { value: v.default, isDefault: true }
|
|
113
|
+
return dict
|
|
114
|
+
}, {})
|
|
115
|
+
|
|
116
|
+
const namedWithDefaults = {...defaults, ...named}
|
|
17
117
|
|
|
18
|
-
|
|
118
|
+
// apply postprocessing
|
|
119
|
+
for (const [key, {value}] of Object.entries(namedWithDefaults)) {
|
|
120
|
+
const { postprocess } = schema[key]
|
|
121
|
+
if (typeof postprocess === 'function') {
|
|
122
|
+
namedWithDefaults[key].value = postprocess(value)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
named: namedWithDefaults,
|
|
128
|
+
positional,
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Adds CLI parameters to the configuration object.
|
|
134
|
+
* Precedence: CLI > env > default.
|
|
135
|
+
* @param {ReturnType<parseCommandlineArgs>['named']} params - CLI parameters.
|
|
136
|
+
*/
|
|
137
|
+
const addCLIParamsToConfig = params => {
|
|
138
|
+
for (const [key, value] of Object.entries(params)) {
|
|
139
|
+
if (!value.isDefault || Object.hasOwn(configuration, key)) {
|
|
140
|
+
configuration[key] = value.value
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const flags = enrichFlagSchema({
|
|
19
146
|
outputDirectory: {
|
|
20
147
|
desc: 'Root directory to write the generated files to.',
|
|
21
148
|
default: './',
|
|
@@ -30,11 +157,24 @@ const flags = {
|
|
|
30
157
|
allowedHint: Object.keys(lls).join(' | '), // FIXME: remove once old levels are faded out
|
|
31
158
|
defaultHint: _keyFor(lls.ERROR),
|
|
32
159
|
default: cds?.env?.log?.levels?.['cds-typer'] ?? _keyFor(lls.ERROR),
|
|
160
|
+
postprocess: level => {
|
|
161
|
+
const newLogLevel = deprecated[level]
|
|
162
|
+
if (newLogLevel) {
|
|
163
|
+
console.warn(`deprecated log level '${level}', use '${newLogLevel}' instead (changing this automatically for now).`)
|
|
164
|
+
return newLogLevel
|
|
165
|
+
}
|
|
166
|
+
return level
|
|
167
|
+
}
|
|
33
168
|
},
|
|
34
169
|
jsConfigPath: {
|
|
35
170
|
desc: `Path to where the jsconfig.json should be written.${EOL}If specified, ${toolName} will create a jsconfig.json file and${EOL}set it up to restrict property usage in types entities to${EOL}existing properties only.`,
|
|
36
|
-
type: 'string'
|
|
171
|
+
type: 'string',
|
|
172
|
+
postprocess: file => file && !file.endsWith('jsconfig.json') ? path.join(file, 'jsconfig.json') : path
|
|
37
173
|
},
|
|
174
|
+
useEntitiesProxy: parameterTypes.boolean({
|
|
175
|
+
desc: `If set to true the 'cds.entities' exports in the generated 'index.js'${EOL}files will be wrapped in 'Proxy' objects\nso static import/require calls can be used everywhere.${EOL}${EOL}WARNING: entity properties can still only be accessed after${EOL}'cds.entities' has been loaded`,
|
|
176
|
+
default: 'false'
|
|
177
|
+
}),
|
|
38
178
|
version: {
|
|
39
179
|
desc: 'Prints the version of this tool.'
|
|
40
180
|
},
|
|
@@ -43,17 +183,15 @@ const flags = {
|
|
|
43
183
|
allowed: ['flat', 'structured'],
|
|
44
184
|
default: 'structured'
|
|
45
185
|
},
|
|
46
|
-
propertiesOptional: {
|
|
186
|
+
propertiesOptional: parameterTypes.boolean({
|
|
47
187
|
desc: `If set to true, properties in entities are${EOL}always generated as optional (a?: T).`,
|
|
48
|
-
allowed: ['true', 'false'],
|
|
49
188
|
default: 'true'
|
|
50
|
-
},
|
|
51
|
-
IEEE754Compatible: {
|
|
189
|
+
}),
|
|
190
|
+
IEEE754Compatible: parameterTypes.boolean({
|
|
52
191
|
desc: `If set to true, floating point properties are generated${EOL}as IEEE754 compatible '(number | string)' instead of 'number'.`,
|
|
53
|
-
allowed: ['true', 'false'],
|
|
54
192
|
default: 'false'
|
|
55
|
-
}
|
|
56
|
-
}
|
|
193
|
+
})
|
|
194
|
+
})
|
|
57
195
|
|
|
58
196
|
const hint = () => 'Missing or invalid parameter(s). Call with --help for more details.'
|
|
59
197
|
/**
|
|
@@ -69,19 +207,22 @@ const help = () => `SYNOPSIS${EOL2}` +
|
|
|
69
207
|
indent('cds-typer [cds file | "*"]', ' ') + EOL2 +
|
|
70
208
|
indent(`Generates type information based on a CDS model.${EOL}Call with at least one positional parameter pointing${EOL}to the (root) CDS file you want to compile.`, ' ') + EOL2 +
|
|
71
209
|
`OPTIONS${EOL2}` +
|
|
72
|
-
|
|
210
|
+
flags.keys
|
|
73
211
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
74
|
-
.map(
|
|
212
|
+
.map(key => {
|
|
213
|
+
const value = flags[key]
|
|
75
214
|
let s = indent(`--${key}`, ' ')
|
|
76
|
-
|
|
215
|
+
const snake = camelToSnake(key)
|
|
216
|
+
if (key !== snake) s += EOL + indent(`--${snake}`, ' ')
|
|
217
|
+
// ts-expect-error - not going to check presence of each property. Same for the following expect-errors.
|
|
77
218
|
if (value.allowedHint) s += ` ${value.allowedHint}`
|
|
78
|
-
//
|
|
219
|
+
// ts-expect-error
|
|
79
220
|
else if (value.allowed) s += `: <${value.allowed.join(' | ')}>`
|
|
80
221
|
else if ('type' in value && value.type) s += `: <${value.type}>`
|
|
81
|
-
//
|
|
222
|
+
// ts-expect-error
|
|
82
223
|
if (value.defaultHint || value.default) {
|
|
83
224
|
s += EOL
|
|
84
|
-
//
|
|
225
|
+
// ts-expect-error
|
|
85
226
|
s += indent(`(default: ${value.defaultHint ?? value.default})`, ' ')
|
|
86
227
|
}
|
|
87
228
|
s += `${EOL2}${indent(value.desc, ' ')}`
|
|
@@ -91,7 +232,9 @@ const help = () => `SYNOPSIS${EOL2}` +
|
|
|
91
232
|
|
|
92
233
|
const version = () => require('../package.json').version
|
|
93
234
|
|
|
94
|
-
const
|
|
235
|
+
const prepareParameters = (/** @type {any[]} */ argv) => {
|
|
236
|
+
const args = parseCommandlineArgs(argv, flags)
|
|
237
|
+
|
|
95
238
|
if ('help' in args.named) {
|
|
96
239
|
console.log(help())
|
|
97
240
|
process.exit(0)
|
|
@@ -104,26 +247,21 @@ const main = async (/** @type {any} */ args) => {
|
|
|
104
247
|
console.log(hint())
|
|
105
248
|
process.exit(1)
|
|
106
249
|
}
|
|
107
|
-
if (args.named.jsConfigPath && !args.named.jsConfigPath.endsWith('jsconfig.json')) {
|
|
108
|
-
args.named.jsConfigPath = path.join(args.named.jsConfigPath, 'jsconfig.json')
|
|
109
|
-
}
|
|
110
|
-
const newLogLevel = deprecated[args.named.logLevel]
|
|
111
|
-
if (newLogLevel) {
|
|
112
|
-
console.warn(`deprecated log level '${args.named.logLevel}', use '${newLogLevel}' instead (changing this automatically for now).`)
|
|
113
|
-
args.named.logLevel = newLogLevel
|
|
114
|
-
}
|
|
115
250
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
IEEE754Compatible: args.named.IEEE754Compatible === 'true'
|
|
124
|
-
})
|
|
251
|
+
addCLIParamsToConfig(args.named)
|
|
252
|
+
return args
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const main = async (/** @type {any[]} */ argv) => {
|
|
256
|
+
const { positional } = prepareParameters(argv)
|
|
257
|
+
compileFromFile(positional)
|
|
125
258
|
}
|
|
126
259
|
|
|
127
260
|
if (require.main === module) {
|
|
128
|
-
main(
|
|
261
|
+
main(process.argv.slice(2))
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
module.exports = {
|
|
265
|
+
flags,
|
|
266
|
+
prepareParameters
|
|
129
267
|
}
|
package/lib/compile.js
CHANGED
|
@@ -7,10 +7,7 @@ const util = require('./util')
|
|
|
7
7
|
const { writeout } = require('./file')
|
|
8
8
|
const { Visitor } = require('./visitor')
|
|
9
9
|
const { LOG, setLevel } = require('./logging')
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* @typedef {import('./typedefs').visitor.CompileParameters} CompileParameters
|
|
13
|
-
*/
|
|
10
|
+
const { configuration } = require('./config')
|
|
14
11
|
|
|
15
12
|
/**
|
|
16
13
|
* Writes the accompanying jsconfig.json file to the specified paths.
|
|
@@ -40,31 +37,28 @@ const writeJsConfig = path => {
|
|
|
40
37
|
/**
|
|
41
38
|
* Compiles a CSN object to Typescript types.
|
|
42
39
|
* @param {{xtended: import('./typedefs').resolver.CSN, inferred: import('./typedefs').resolver.CSN}} csn - csn tuple
|
|
43
|
-
* @param {CompileParameters} parameters - path to root directory for all generated files, min log level
|
|
44
40
|
*/
|
|
45
|
-
const compileFromCSN = async
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
writeJsConfig(parameters.jsConfigPath)
|
|
41
|
+
const compileFromCSN = async csn => {
|
|
42
|
+
|
|
43
|
+
setLevel(configuration.logLevel)
|
|
44
|
+
if (configuration.jsConfigPath) {
|
|
45
|
+
writeJsConfig(configuration.jsConfigPath)
|
|
51
46
|
}
|
|
52
47
|
return writeout(
|
|
53
|
-
|
|
54
|
-
Object.values(new Visitor(csn
|
|
48
|
+
configuration.outputDirectory,
|
|
49
|
+
Object.values(new Visitor(csn).getWriteoutFiles())
|
|
55
50
|
)
|
|
56
51
|
}
|
|
57
52
|
|
|
58
53
|
/**
|
|
59
54
|
* Compiles a .cds file to Typescript types.
|
|
60
55
|
* @param {string | string[]} inputFile - path to input .cds file
|
|
61
|
-
* @param {CompileParameters} parameters - path to root directory for all generated files, min log level, etc.
|
|
62
56
|
*/
|
|
63
|
-
const compileFromFile = async
|
|
57
|
+
const compileFromFile = async inputFile => {
|
|
64
58
|
const paths = typeof inputFile === 'string' ? normalize(inputFile) : inputFile.map(f => normalize(f))
|
|
65
59
|
const xtended = await cds.linked(await cds.load(paths, { docs: true, flavor: 'xtended' }))
|
|
66
60
|
const inferred = await cds.linked(await cds.load(paths, { docs: true }))
|
|
67
|
-
return compileFromCSN({xtended, inferred}
|
|
61
|
+
return compileFromCSN({xtended, inferred})
|
|
68
62
|
}
|
|
69
63
|
|
|
70
64
|
module.exports = {
|
|
@@ -42,6 +42,12 @@ export type DeepRequired<T> = {
|
|
|
42
42
|
[K in keyof T]: DeepRequired<T[K]>
|
|
43
43
|
} & Exclude<Required<T>, null>;
|
|
44
44
|
|
|
45
|
+
const key = Symbol('key') // to avoid .key showing up in IDE's auto-completion
|
|
46
|
+
export type Key<T> = T & {[key]?: true}
|
|
47
|
+
|
|
48
|
+
export type KeysOf<T> = {
|
|
49
|
+
[K in keyof T as NonNullable<T[K]> extends Key<unknown> ? K : never]-?: Key<{}> // T[K]
|
|
50
|
+
}
|
|
45
51
|
|
|
46
52
|
/**
|
|
47
53
|
* Dates and timestamps are strings during runtime, so cds-typer represents them as such.
|
package/lib/components/enum.js
CHANGED
|
@@ -22,7 +22,7 @@ const stringifyEnumType = kvs => [...uniqueValues(kvs)].join(' | ')
|
|
|
22
22
|
/**
|
|
23
23
|
* @param {string} key - the key of the enum
|
|
24
24
|
* @param {any} value - the value of the enum
|
|
25
|
-
* @param {string | import('
|
|
25
|
+
* @param {string | import('../typedefs').resolver.ref} enumType - the type of the enum
|
|
26
26
|
*/
|
|
27
27
|
const enumVal = (key, value, enumType) => enumType === 'cds.String' ? JSON.stringify(`${value ?? key}`) : value
|
|
28
28
|
|
|
@@ -50,10 +50,12 @@ const enumVal = (key, value, enumType) => enumType === 'cds.String' ? JSON.strin
|
|
|
50
50
|
* @param {string} name - local name of the enum, i.e. the name under which it should be created in the .ts file
|
|
51
51
|
* @param {[string, string][]} kvs - list of key-value pairs
|
|
52
52
|
* @param {object} options - options for printing the enum
|
|
53
|
+
* @param {string[]} doc - the enum docs
|
|
53
54
|
*/
|
|
54
|
-
function printEnum(buffer, name, kvs, options = {}) {
|
|
55
|
+
function printEnum(buffer, name, kvs, options = {}, doc=[]) {
|
|
55
56
|
const opts = {...{export: true}, ...options}
|
|
56
57
|
buffer.add('// enum')
|
|
58
|
+
if (opts.export) doc.forEach(d => { buffer.add(d) })
|
|
57
59
|
buffer.addIndentedBlock(`${opts.export ? 'export ' : ''}const ${name} = {`, () =>
|
|
58
60
|
kvs.forEach(([k, v]) => { buffer.add(`${normalise(k)}: ${v},`) })
|
|
59
61
|
, '} as const;')
|
package/lib/components/inline.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const { configuration } = require('../config')
|
|
1
2
|
const { SourceFile, Buffer } = require('../file')
|
|
2
3
|
const { normalise } = require('./identifier')
|
|
3
4
|
const { docify } = require('./wrappers')
|
|
@@ -94,7 +95,7 @@ class InlineDeclarationResolver {
|
|
|
94
95
|
* @returns {'?:'|':'}
|
|
95
96
|
*/
|
|
96
97
|
getPropertyTypeSeparator() {
|
|
97
|
-
return
|
|
98
|
+
return configuration.propertiesOptional ? '?:' : ':'
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
/**
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/* eslint-disable no-undef */
|
|
2
|
+
// this function will be stringified as part of the generated types and is not supposed to be used internally
|
|
3
|
+
// @ts-nocheck
|
|
4
|
+
const proxyAccessFunction = function (fqParts, opts = {}) {
|
|
5
|
+
const { target, customProps } = { target: {}, customProps: [], ...opts }
|
|
6
|
+
const fq = fqParts.filter(p => !!p).join('.')
|
|
7
|
+
return new Proxy(target, {
|
|
8
|
+
get: function (target, prop) {
|
|
9
|
+
if (cds.entities) {
|
|
10
|
+
target.__proto__ = cds.entities(fqParts[0])[fqParts[1]]
|
|
11
|
+
// overwrite/simplify getter after cds.entities is accessible
|
|
12
|
+
this.get = (target, prop) => target[prop]
|
|
13
|
+
return target[prop]
|
|
14
|
+
}
|
|
15
|
+
// we already know the name so we skip the cds.entities proxy access
|
|
16
|
+
if (prop === 'name') return fq
|
|
17
|
+
// custom properties access on 'target' as well as cached _entity property access goes here
|
|
18
|
+
if (Object.hasOwn(target, prop)) return target[prop]
|
|
19
|
+
// inline enums have to be caught here for first time access, as they do not exist on the entity
|
|
20
|
+
if (customProps.includes(prop)) return target[prop]
|
|
21
|
+
// last but not least we pass the property access to cds.entities
|
|
22
|
+
throw new Error(`Property ${prop} does not exist on entity '${fq}' or cds.entities is not yet defined. Ensure the CDS runtime is fully booted before accessing properties.`)
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
}.toString()
|
|
26
|
+
/* eslint-enable no-undef */
|
|
27
|
+
|
|
28
|
+
module.exports = { proxyAccessFunction }
|
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
// this was derived from baseDefinitions before, but caused a circular dependency
|
|
4
4
|
const base = '__'
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Wraps type into the Key type.
|
|
8
|
+
* @param {string} t - the type name.
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
const createKey = t => `${base}.Key<${t}>`
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Wraps type into KeysOf type.
|
|
15
|
+
* @param {string} t - the type name.
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
const createKeysOf = t => `${base}.KeysOf<${t}>`
|
|
19
|
+
|
|
6
20
|
/**
|
|
7
21
|
* Wraps type into association to scalar.
|
|
8
22
|
* @param {string} t - the singular type name.
|
|
@@ -45,6 +59,24 @@ const createArrayOf = t => `Array<${t}>`
|
|
|
45
59
|
*/
|
|
46
60
|
const createObjectOf = t => `{${t}}`
|
|
47
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Wraps types into a union type string
|
|
64
|
+
* @param {string[]} types - an array of types
|
|
65
|
+
* @returns {string}
|
|
66
|
+
*/
|
|
67
|
+
const createUnionOf = (...types) => types.join(' | ')
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Wraps type into a promise
|
|
71
|
+
* @param {string} t - the type to wrap.
|
|
72
|
+
* @returns {string}
|
|
73
|
+
* @example
|
|
74
|
+
* ```js
|
|
75
|
+
* createPromiseOf('string') // -> 'Promise<string>'
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
const createPromiseOf = t => `Promise<${t}>`
|
|
79
|
+
|
|
48
80
|
/**
|
|
49
81
|
* Wraps type into a deep require (removes all posibilities of undefined recursively).
|
|
50
82
|
* @param {string} t - the singular type name.
|
|
@@ -59,13 +91,20 @@ const deepRequire = (t, lookup = '') => `${base}.DeepRequired<${t}>${lookup}`
|
|
|
59
91
|
* @returns {string[]} an array of lines wrapped in doc format. The result is not
|
|
60
92
|
* concatenated to be properly indented by `buffer.add(...)`.
|
|
61
93
|
*/
|
|
62
|
-
const docify = doc =>
|
|
63
|
-
|
|
64
|
-
|
|
94
|
+
const docify = doc => {
|
|
95
|
+
if (!doc) return []
|
|
96
|
+
const lines = doc.split(/\r?\n/).map(l => l.trim().replaceAll('*/', '*\\/')) // mask any */ with *\/
|
|
97
|
+
if (lines.length === 1) return [`/** ${lines[0]} */`] // one-line doc
|
|
98
|
+
return ['/**'].concat(lines.map(line => `* ${line}`)).concat(['*/'])
|
|
99
|
+
}
|
|
65
100
|
|
|
66
101
|
module.exports = {
|
|
67
102
|
createArrayOf,
|
|
103
|
+
createKey,
|
|
104
|
+
createKeysOf,
|
|
68
105
|
createObjectOf,
|
|
106
|
+
createPromiseOf,
|
|
107
|
+
createUnionOf,
|
|
69
108
|
createToOneAssociation,
|
|
70
109
|
createToManyAssociation,
|
|
71
110
|
createCompositionOfOne,
|