@cap-js/cds-typer 0.28.1 → 0.30.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 CHANGED
@@ -4,6 +4,24 @@ 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.31.0 - TBD
8
+
9
+ ## Version 0.30.0 - 2024-12-02
10
+
11
+ ### Changed
12
+ - [breaking] when running cds-typer in a CAP project, the default for the `outputDirectory` option will be `./@cds-models` instead of `./`. This default takes the lowest precedence after setting it in the project's `cds.env`, or explicitly as CLI argument.
13
+
14
+ ### Fixed
15
+ - cds-typer no longer ignores the selected `outputDirectory`, which would also cause an issue during build
16
+
17
+
18
+ ## Version 0.29.0 - 2024-11-20
19
+ ### Added
20
+ - [breaking] cds-typer now tries to automatically detect whether it has to generate ESM or CommonJS in the emitted _index.js_ files. This behaviour can be overridden via the `--targetModuleType` option. _If you rely on these generated index.js files to be CJS despite your project being of ESM type, you need to manually tell cds-typer to generate CJS files!_
21
+
22
+ ### Fixed
23
+ - The static `.keys` property now properly reels in key types from inherited classes.
24
+
7
25
  ## Version 0.28.1 - 2024-11-07
8
26
  ### Fixed
9
27
  - `cds build` no longer fails on Windows with an `EINVAL` error.
package/lib/cli.js CHANGED
@@ -142,6 +142,7 @@ const addCLIParamsToConfig = params => {
142
142
  }
143
143
  }
144
144
 
145
+ // when changing/ adding anything in here, make sure to adjust scripts/write-cds-typer-schema.js accordingly!
145
146
  const flags = enrichFlagSchema({
146
147
  outputDirectory: {
147
148
  desc: 'Root directory to write the generated files to.',
@@ -190,7 +191,12 @@ const flags = enrichFlagSchema({
190
191
  IEEE754Compatible: parameterTypes.boolean({
191
192
  desc: `If set to true, floating point properties are generated${EOL}as IEEE754 compatible '(number | string)' instead of 'number'.`,
192
193
  default: 'false'
193
- })
194
+ }),
195
+ targetModuleType: {
196
+ desc: `Output format for generated .js files.${EOL}Setting it to auto tries to derive the module type from${EOL}the package.json and falls back to CJS.`,
197
+ allowed: ['esm', 'cjs', 'auto'],
198
+ default: 'auto'
199
+ }
194
200
  })
195
201
 
196
202
  const hint = () => 'Missing or invalid parameter(s). Call with --help for more details.'
@@ -253,6 +259,9 @@ const prepareParameters = (/** @type {any[]} */ argv) => {
253
259
  }
254
260
 
255
261
  const main = async (/** @type {any[]} */ argv) => {
262
+ // when calling from CLI within a CAP project, make sure plugins (this includes cds-typer)
263
+ // are initialised and have their default values injected into cds.env
264
+ await cds.plugins
256
265
  const { positional } = prepareParameters(argv)
257
266
  compileFromFile(positional)
258
267
  }
@@ -1,7 +1,7 @@
1
1
  const { configuration } = require('../config')
2
2
  const { SourceFile, Buffer } = require('../file')
3
3
  const { normalise } = require('./identifier')
4
- const { docify } = require('./wrappers')
4
+ const { docify } = require('../printers/wrappers')
5
5
 
6
6
  /** @typedef {import('../resolution/resolver').TypeResolveInfo} TypeResolveInfo */
7
7
  /** @typedef {import('../resolution/resolver').TypeResolveOptions} TypeResolverOptions */
package/lib/config.js CHANGED
@@ -1,5 +1,8 @@
1
+ const fs = require('node:fs')
2
+ const path = require('node:path')
1
3
  const cds = require('@sap/cds')
2
- const { camelToSnake } = require('./util')
4
+ const { camelToSnake, getProjectTargetType } = require('./util')
5
+ const { LOG } = require('./logging')
3
6
 
4
7
  /**
5
8
  * Makes properties of an object accessible in both camelCase and snake_case.
@@ -31,19 +34,39 @@ const camelSnakeHybrid = target => {
31
34
  return proxy
32
35
  }
33
36
  class Config {
34
- static #defaults = {
35
- propertiesOptional: true,
36
- useEntitiesProxy: false,
37
- inlineDeclarations: 'flat',
38
- outputDirectory: '.'
37
+ /** @type {Record<string, unknown>} */
38
+ static #defaults = {}
39
+ static get defaults () {
40
+ // these are the exact defaults that are used when cds-typer is loaded as cds plugin
41
+ // which will shove the values into cds.env. When executed standalone, without cds environment,
42
+ // we need to load them from package.json ourselves.
43
+ if (Object.keys(Config.#defaults).length) return Config.#defaults
44
+ try {
45
+ const packageJsonPath = path.resolve(__dirname, path.join('..', 'package.json'))
46
+ const pjson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
47
+ Config.#defaults = pjson.cds.typer
48
+ } catch (e) {
49
+ LOG.error(`Failed to load default configuration from package.json: ${e}`)
50
+ }
51
+ return Config.#defaults
39
52
  }
40
53
 
41
54
  values = undefined
42
55
  proxy = undefined
43
56
 
44
57
  init () {
45
- this.values = {...Config.#defaults, ...(cds.env.typer ?? {})}
58
+ this.values = {...Config.defaults, ...(cds.env.typer ?? {})}
46
59
  this.proxy = camelSnakeHybrid(this.values)
60
+ if (this.proxy.targetModuleType === 'auto') {
61
+ const type = getProjectTargetType(cds.root)
62
+ if (type) {
63
+ LOG.info(`automatically detected module type '${type}' in ${cds.root}`)
64
+ this.values.targetModuleType = type
65
+ } else {
66
+ LOG.warn(`target module type was set to 'auto', but could not detect module type in ${cds.root}. Falling back to cjs`)
67
+ this.values.targetModuleType = 'cjs'
68
+ }
69
+ }
47
70
  }
48
71
 
49
72
  constructor() {
package/lib/file.js CHANGED
@@ -7,8 +7,9 @@ const { printEnum, propertyToInlineEnumName, stringifyEnumImplementation } = req
7
7
  const { normalise } = require('./components/identifier')
8
8
  const { empty } = require('./components/typescript')
9
9
  const { proxyAccessFunction } = require('./components/javascript')
10
- const { createObjectOf } = require('./components/wrappers')
10
+ const { createObjectOf, stringIdent } = require('./printers/wrappers')
11
11
  const { configuration } = require('./config')
12
+ const { CJSPrinter, ESMPrinter } = require('./printers/javascript')
12
13
 
13
14
  const AUTO_GEN_NOTE = '// This is an automatically generated file. Please do not change its contents manually!'
14
15
 
@@ -107,6 +108,9 @@ class Library extends File {
107
108
  * Source file containing several buffers.
108
109
  */
109
110
  class SourceFile extends File {
111
+ /** @type {import('../lib/printers/javascript').Printer | undefined} */
112
+ #jsPrinter
113
+
110
114
  /**
111
115
  * @param {string | Path} path - path to the file
112
116
  */
@@ -146,6 +150,12 @@ class SourceFile extends File {
146
150
  this.entityProxies = {}
147
151
  }
148
152
 
153
+ get jsPrinter () {
154
+ return this.#jsPrinter ??= configuration.targetModuleType === 'esm'
155
+ ? new ESMPrinter()
156
+ : new CJSPrinter()
157
+ }
158
+
149
159
  /**
150
160
  * Stringifies a lambda expression.
151
161
  * @param {object} options - options
@@ -428,7 +438,7 @@ class SourceFile extends File {
428
438
  ].filter(Boolean).join('\n')
429
439
  }
430
440
  #getEntityProxyFunctionExport() {
431
- return `module.exports.createEntityProxy = ${proxyAccessFunction}`
441
+ return this.jsPrinter.printExport('createEntityProxy', proxyAccessFunction)
432
442
  }
433
443
  /**
434
444
  * Returns boilerplate code for `index.js` files
@@ -438,18 +448,23 @@ class SourceFile extends File {
438
448
  */
439
449
  #getJSExportBoilerplate() {
440
450
  const namespace = this.path.asNamespace()
441
-
451
+ const jsp = this.jsPrinter
442
452
  const boilerplate = [AUTO_GEN_NOTE]
443
453
  if (configuration.useEntitiesProxy) {
444
454
  if (namespace === '_') {
445
- boilerplate.push('const cds = require(\'@sap/cds\')', this.#getEntityProxyFunctionExport())
455
+ boilerplate.push(jsp.printImport('cds', '@sap/cds'), this.#getEntityProxyFunctionExport())
446
456
  } else {
447
- boilerplate.push(`const { createEntityProxy } = require('${new Path(['_']).asDirectory({relative: this.path.asDirectory()})}')`)
457
+ boilerplate.push(
458
+ jsp.printDeconstructedImport(
459
+ ['createEntityProxy'],
460
+ new Path(['_']).asDirectory({ relative: this.path.asDirectory() })
461
+ )
462
+ )
448
463
  }
449
464
  } else {
450
465
  boilerplate.push(
451
- 'const cds = require(\'@sap/cds\')',
452
- `const csn = cds.entities('${namespace}')`
466
+ jsp.printImport('cds', '@sap/cds'),
467
+ jsp.printConstant('csn', `cds.entities('${namespace}')`)
453
468
  )
454
469
  }
455
470
  return boilerplate
@@ -480,18 +495,32 @@ class SourceFile extends File {
480
495
  }
481
496
  }
482
497
  }
498
+
483
499
  toJSExports() {
500
+ const jsp = this.jsPrinter
484
501
  return this.#getJSExportBoilerplate() // boilerplate
485
502
  .concat(
486
503
  // FIXME: move stringification of service into own module
487
504
  this.services.names.flatMap(name => {
488
- const nameSimple = name.split('.').pop()
489
- return [
490
- '// service',
491
- `const ${nameSimple} = { name: '${name}' }`,
492
- `module.exports = ${nameSimple}`, // there should be only one and must be the first
493
- `module.exports.${nameSimple} = ${nameSimple}`
505
+ const nameSimple = /** @type {string} */(name.split('.').pop())
506
+ const service = [
507
+ jsp.printSingleLineComment('service'),
508
+ jsp.printConstant(nameSimple, `{ name: '${name}' }`),
509
+ jsp.printDefaultExport(nameSimple), // there should be only one and must be the first
494
510
  ]
511
+ if (jsp instanceof CJSPrinter) {
512
+ // unfortunately, we have to add a tiny bit of special logic for CJS, where we get:
513
+ //
514
+ // module.exports = FooService
515
+ // module.exports.FooService = FooService
516
+ //
517
+ // which, in ESM, would amount to:
518
+ //
519
+ // export const FooService
520
+ // default export FooService = FooService // <- duplicate identifier
521
+ service.push(jsp.printExport(nameSimple, nameSimple))
522
+ }
523
+ return service
495
524
  })
496
525
  )
497
526
  .concat(this.inflections
@@ -502,10 +531,13 @@ class SourceFile extends File {
502
531
  .flatMap(([singular, plural, original]) => {
503
532
  const { singularRhs, pluralRhs } = this.#getEntityExportsRhs(singular, original)
504
533
 
505
- const exports = [`// ${original}`, `module.exports.${singular} = ${singularRhs}`]
534
+ const exports = [
535
+ jsp.printSingleLineComment(original),
536
+ jsp.printExport(singular, singularRhs)
537
+ ]
506
538
  if (!/Array<.*>/.test(plural) && plural !== original) {
507
539
  // FIXME: this is a hack to support CDS types that will produce "Array<MyType>" as plural, which we do not want as export in the index.js files
508
- exports.push(`module.exports.${plural} = ${pluralRhs}`)
540
+ exports.push(jsp.printExport(plural, pluralRhs))
509
541
  }
510
542
  // FIXME: we currently produce at most 3 entries.
511
543
  // This could be an issue when the user re-used the original name in a @singular/@plural annotation.
@@ -513,16 +545,16 @@ class SourceFile extends File {
513
545
  if (singular !== original) {
514
546
  // do not do the is_singular spiel if the original name is used for the plural
515
547
  const rhs = plural === original ? pluralRhs : singularRhs
516
- exports.push(`module.exports.${original} = ${rhs}`)
548
+ exports.push(jsp.printExport(original, rhs))
517
549
  }
518
550
  return exports
519
551
  })
520
552
  ) // singular -> plural aliases
521
- .concat(['// events'])
522
- .concat(this.events.fqs.map(({fq, name}) => `module.exports.${name} = '${fq}'`))
523
- .concat(['// actions'])
524
- .concat(this.operations.names.map(name => `module.exports.${name} = '${name}'`))
525
- .concat(['// enums'])
553
+ .concat(jsp.printSingleLineComment('events'))
554
+ .concat(this.events.fqs.map(({fq, name}) => jsp.printExport(name, stringIdent(fq))))
555
+ .concat(jsp.printSingleLineComment('actions'))
556
+ .concat(this.operations.names.map(name => jsp.printExport(name, stringIdent(name))))
557
+ .concat(jsp.printSingleLineComment('enums'))
526
558
  .concat(this.enums.data.map(({name, kvs}) => stringifyEnumImplementation(name, kvs)))
527
559
  .join('\n') + '\n'
528
560
  }
@@ -555,6 +587,15 @@ class Buffer {
555
587
  this.closed = false
556
588
  }
557
589
 
590
+ /**
591
+ * @returns {Buffer} an empty Buffer that starts at this Buffer's current indentation.
592
+ */
593
+ createSubBuffer() {
594
+ const sub = new Buffer(this.indentation)
595
+ sub.currentIndent = this.currentIndent
596
+ return sub
597
+ }
598
+
558
599
  /**
559
600
  * Indents by the predefined spacing.
560
601
  */
@@ -0,0 +1,131 @@
1
+ class JavaScriptPrinter {
2
+ /**
3
+ * @param {string} text - comment text
4
+ */
5
+ printSingleLineComment (text) {
6
+ return `// ${text}`
7
+ }
8
+
9
+ /**
10
+ * @param {string} name - name of the constant
11
+ * @param {string} value - initial assignment to the constant
12
+ */
13
+ printConstant (name, value) {
14
+ return `const ${name} = ${value}`
15
+ }
16
+
17
+ /**
18
+ * @abstract
19
+ * @param {string} alias - what the import should be known as within the importing file
20
+ * @param {string} from - the package/ location to import from
21
+ * @returns {string}
22
+ */
23
+ // eslint-disable-next-line no-unused-vars
24
+ printImport (alias, from) {
25
+ throw Error('not implemented')
26
+ }
27
+
28
+ /**
29
+ * @abstract
30
+ * @param {string[]} imports - the deconstructed elements
31
+ * @param {string} from - the package/ location to import from
32
+ * @returns {string}
33
+ */
34
+ // eslint-disable-next-line no-unused-vars
35
+ printDeconstructedImport (imports, from) {
36
+ throw Error('not implemented')
37
+ }
38
+
39
+ /**
40
+ * @abstract
41
+ * @param {string} name - name the export should be known as
42
+ * @param {string} value - the value of the export
43
+ * @returns {string}
44
+ */
45
+ // eslint-disable-next-line no-unused-vars
46
+ printExport (name, value) {
47
+ throw Error('not implemented')
48
+ }
49
+
50
+ /**
51
+ * @abstract
52
+ * @param {string} value - the value of the default export
53
+ * @returns {string}
54
+ */
55
+ // eslint-disable-next-line no-unused-vars
56
+ printDefaultExport (value) {
57
+ throw Error('not implemented')
58
+ }
59
+
60
+ /**
61
+ * @abstract
62
+ * @param {string} file - the file name without extension
63
+ * @returns {string}
64
+ */
65
+ // eslint-disable-next-line no-unused-vars
66
+ nameFile (file) {
67
+ throw Error('not implemented')
68
+ }
69
+ }
70
+
71
+ class ESMPrinter extends JavaScriptPrinter {
72
+ /** @type {JavaScriptPrinter['printImport']} */
73
+ printImport (alias, from) {
74
+ return `import * as ${alias} from '${from}'`
75
+ }
76
+
77
+ /** @type {JavaScriptPrinter['printDeconstructedImport']} */
78
+ printDeconstructedImport (imports, from) {
79
+ return `import { ${imports.join(', ')} } from '${from}'`
80
+ }
81
+
82
+ /** @type {JavaScriptPrinter['printExport']} */
83
+ printExport (name, value) {
84
+ return `export const ${name} = ${value}`
85
+ }
86
+
87
+ /** @type {JavaScriptPrinter['printDefaultExport']} */
88
+ printDefaultExport (value) {
89
+ return `export default ${value}`
90
+ }
91
+
92
+ /** @type {JavaScriptPrinter['nameFile']} */
93
+ nameFile (file) {
94
+ return `${file}.mjs`
95
+ }
96
+ }
97
+
98
+ class CJSPrinter extends JavaScriptPrinter {
99
+ /** @type {JavaScriptPrinter['printImport']} */
100
+ printImport (alias, from) {
101
+ return `const ${alias} = require('${from}')`
102
+ }
103
+
104
+ /** @type {JavaScriptPrinter['printDeconstructedImport']} */
105
+ printDeconstructedImport (imports, from) {
106
+ return `const { ${imports.join(', ')} } = require('${from}')`
107
+ }
108
+
109
+ /** @type {JavaScriptPrinter['printExport']} */
110
+ printExport (name, value) {
111
+ return `module.exports.${name} = ${value}`
112
+ }
113
+
114
+ /** @type {JavaScriptPrinter['printDefaultExport']} */
115
+ printDefaultExport (value) {
116
+ // not exactly "default", but you catch the drift...
117
+ return `module.exports = ${value}`
118
+ }
119
+
120
+ /** @type {JavaScriptPrinter['nameFile']} */
121
+ nameFile (file) {
122
+ return `${file}.js`
123
+ }
124
+ }
125
+
126
+ /** @typedef {JavaScriptPrinter} Printer */
127
+
128
+ module.exports = {
129
+ CJSPrinter,
130
+ ESMPrinter
131
+ }
@@ -87,6 +87,13 @@ const createObjectOf = t => `{${t}}`
87
87
  */
88
88
  const createUnionOf = (...types) => types.join(' | ')
89
89
 
90
+ /**
91
+ * Wraps types into a intersection type string
92
+ * @param {string[]} types - an array of types
93
+ * @returns {string}
94
+ */
95
+ const createIntersectionOf = (...types) => types.join(' & ')
96
+
90
97
  /**
91
98
  * Wraps type into a promise
92
99
  * @param {string} t - the type to wrap.
@@ -136,6 +143,7 @@ module.exports = {
136
143
  createObjectOf,
137
144
  createPromiseOf,
138
145
  createUnionOf,
146
+ createIntersectionOf,
139
147
  createToOneAssociation,
140
148
  createToManyAssociation,
141
149
  createCompositionOfOne,
@@ -3,7 +3,7 @@
3
3
  const util = require('../util')
4
4
  // eslint-disable-next-line no-unused-vars
5
5
  const { Buffer, SourceFile, Path, Library } = require('../file')
6
- const { deepRequire, createToManyAssociation, createToOneAssociation, createArrayOf, createCompositionOfMany, createCompositionOfOne, createKey } = require('../components/wrappers')
6
+ const { deepRequire, createToManyAssociation, createToOneAssociation, createArrayOf, createCompositionOfMany, createCompositionOfOne, createKey } = require('../printers/wrappers')
7
7
  const { StructuredInlineDeclarationResolver } = require('../components/inline')
8
8
  const { isInlineEnumType, propertyToInlineEnumName } = require('../components/enum')
9
9
  const { isReferenceType } = require('../components/reference')
package/lib/typedefs.d.ts CHANGED
@@ -202,6 +202,7 @@ export module config {
202
202
  * `IEEE754Compatible = true` -> any cds.Decimal will become `number | string`
203
203
  */
204
204
  IEEE754Compatible: boolean
205
+ targetModuleType: 'cjs' | 'esm' | 'auto'
205
206
  }
206
207
  }
207
208
 
package/lib/util.js CHANGED
@@ -1,8 +1,9 @@
1
1
  /** @typedef { import('./typedefs').util.Annotations} Annotations */
2
- /** @typedef { import('./typedefs').util.CommandLineFlags } CommandlineFlag */
3
- /** @typedef { import('./typedefs').util.ParsedFlag } ParsedFlags */
4
2
  /** @typedef { import('./typedefs').resolver.EntityCSN } EntityCSN */
5
3
 
4
+ const fs = require('node:fs')
5
+ const path = require('node:path')
6
+
6
7
  // inflection functions are stolen from github/cap/dev/blob/main/etc/inflect.js
7
8
 
8
9
  // MONKEY PATCH to support Node v14
@@ -108,7 +109,7 @@ const singular4 = (dn, stripped = false) => {
108
109
  const plural4 = (dn, stripped) => {
109
110
  let n = dn.name ?? dn
110
111
  if (stripped) {
111
- n = n.match(last)?.[0]
112
+ n = /** @type {string} */(n.match(last)?.[0])
112
113
  }
113
114
  return (
114
115
  getPluralAnnotation(dn) ??
@@ -125,8 +126,8 @@ const plural4 = (dn, stripped) => {
125
126
  /**
126
127
  * Performs a deep merge of the passed objects into the first object.
127
128
  * See Object.assign(target, source).
128
- * @param {object} target - object to assign into.
129
- * @param {object} source - object to assign from.
129
+ * @param {{[key: string]: any}} target - object to assign into.
130
+ * @param {{[key: string]: any}} source - object to assign from.
130
131
  */
131
132
  const deepMerge = (target, source) => {
132
133
  Object.keys(target)
@@ -135,11 +136,40 @@ const deepMerge = (target, source) => {
135
136
  Object.assign(target, source)
136
137
  }
137
138
 
139
+ /**
140
+ * Tries to determine the project's target type (ESM or CJS).
141
+ * @param {string} root - the root of the project
142
+ * @returns {'esm' | 'cjs' | undefined}
143
+ */
144
+ const getProjectTargetType = root => {
145
+ const isFsRoot = (/** @type {string} **/ dir) => dir === path.dirname(dir)
146
+ let dir = root
147
+ let packageJson
148
+
149
+ do {
150
+ packageJson = fs.readdirSync(dir).find(f => f.endsWith('package.json'))
151
+ if (packageJson) {
152
+ packageJson = path.join(dir, packageJson)
153
+ }
154
+ dir = path.dirname(dir)
155
+ } while (!packageJson && !isFsRoot(dir))
156
+
157
+ if (packageJson) {
158
+ const json = JSON.parse(fs.readFileSync(packageJson, 'utf-8'))
159
+ return json.type === 'module'
160
+ ? 'esm'
161
+ : 'cjs'
162
+ }
163
+ // no package.json found -> inconclusive
164
+ return undefined
165
+ }
166
+
138
167
  module.exports = {
139
168
  annotations,
140
169
  camelToSnake,
141
170
  getSingularAnnotation,
142
171
  getPluralAnnotation,
172
+ getProjectTargetType,
143
173
  unlocalize,
144
174
  singular4,
145
175
  plural4,
package/lib/visitor.js CHANGED
@@ -8,7 +8,7 @@ const { SourceFile, FileRepository, Buffer, Path } = require('./file')
8
8
  const { FlatInlineDeclarationResolver, StructuredInlineDeclarationResolver } = require('./components/inline')
9
9
  const { Resolver } = require('./resolution/resolver')
10
10
  const { LOG } = require('./logging')
11
- const { docify, createPromiseOf, createUnionOf, createKeysOf, createElementsOf, stringIdent, createDraftsOf, createDraftOf } = require('./components/wrappers')
11
+ const { docify, createPromiseOf, createUnionOf, createKeysOf, createElementsOf, stringIdent, createDraftsOf, createDraftOf, createIntersectionOf } = require('./printers/wrappers')
12
12
  const { csnToEnumPairs, propertyToInlineEnumName, isInlineEnumType, stringifyEnumType } = require('./components/enum')
13
13
  const { isReferenceType } = require('./components/reference')
14
14
  const { empty } = require('./components/typescript')
@@ -138,11 +138,11 @@ class Visitor {
138
138
  #printStaticActions(entity, buffer, ancestors, file) {
139
139
  // TODO: refactor away! All these printing functionalities need to go
140
140
  const actions = Object.entries(entity.actions ?? {})
141
- const inherited = ancestors.length
142
- ? ancestors.map(a => `typeof ${asIdentifier({info: a, relative: file.path})}.actions`).join(' & ') + ' & '
143
- : ''
141
+ const inherited = ancestors.map(a => `typeof ${asIdentifier({info: a, relative: file.path})}.actions`)
142
+
143
+ const typeBuffer = buffer.createSubBuffer()
144
144
  if (actions.length) {
145
- buffer.addIndentedBlock(`declare static readonly actions: ${inherited}{`,
145
+ typeBuffer.addIndentedBlock(createIntersectionOf(...inherited, '{'),
146
146
  () => {
147
147
  for (const [aname, action] of actions) {
148
148
  const [opener, content, closer] = SourceFile.stringifyLambda({
@@ -153,28 +153,34 @@ class Visitor {
153
153
  : 'any',
154
154
  kind: action.kind,
155
155
  doc: docify(action.doc)})
156
- buffer.addIndentedBlock(opener, content, closer)
156
+ typeBuffer.addIndentedBlock(opener, content, closer)
157
157
  }
158
- }, '}'
159
- ) // end of actions
158
+ }, '}')
160
159
  } else {
161
- buffer.add(createMember({
162
- name: 'actions',
163
- type: `${inherited}${empty}`,
164
- isStatic: true,
165
- isReadonly: true
166
- }))
160
+ typeBuffer.add(createIntersectionOf(...inherited, empty))
167
161
  }
162
+ buffer.add(createMember({
163
+ name: 'actions',
164
+ type: typeBuffer.join().trimStart(), // remove leading whitespace from indentation, as type is printed in line
165
+ isStatic: true,
166
+ isReadonly: true,
167
+ isDeclare: true,
168
+ }))
168
169
  }
169
170
 
170
171
  /**
171
172
  * @param {Buffer} buffer - the buffer to write the keys into
172
173
  * @param {string} clean - the clean name of the entity
174
+ * @param {import('./typedefs').resolver.EntityInfo[]} ancestors - ancestors infos to include in they type
175
+ * @param {SourceFile} file - the file the entity is being printed into
173
176
  */
174
- #printStaticKeys(buffer, clean) {
177
+ #printStaticKeys(buffer, clean, ancestors, file) {
178
+ const ancestorKeys = ancestors
179
+ .filter(a => Object.entries(a.csn.keys ?? {}).length)
180
+ .map(a => `typeof ${asIdentifier({info: a, relative: file.path})}.keys`)
175
181
  buffer.add(createMember({
176
182
  name: 'keys',
177
- type: createKeysOf(clean),
183
+ type: createIntersectionOf(createKeysOf(clean), ...ancestorKeys),
178
184
  isDeclare: true,
179
185
  isStatic: true,
180
186
  isReadonly: true,
@@ -312,15 +318,15 @@ class Visitor {
312
318
  if ('kind' in entity) {
313
319
  buffer.add(createMember({
314
320
  name: 'kind',
315
- type: '"entity" | "type" | "aspect"',
321
+ type: createUnionOf(...['entity', 'type', 'aspect'].map(stringIdent)),
316
322
  isStatic: true,
317
323
  isReadonly: true,
318
324
  isDeclare: false,
319
- isOverride: ancestorInfos.some(ancestor => ancestor.csn.kind),
325
+ isOverride: ancestorInfos.some(({csn}) => csn.kind),
320
326
  initialiser: stringIdent(entity.kind)
321
327
  }))
322
328
  }
323
- this.#printStaticKeys(buffer, clean)
329
+ this.#printStaticKeys(buffer, clean, ancestorInfos, file)
324
330
  this.#printStaticElements(buffer, clean)
325
331
  this.#printStaticActions(entity, buffer, ancestorInfos, file)
326
332
  }, '};') // end of generated class
@@ -657,4 +663,3 @@ class Visitor {
657
663
  module.exports = {
658
664
  Visitor
659
665
  }
660
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/cds-typer",
3
- "version": "0.28.1",
3
+ "version": "0.30.0",
4
4
  "description": "Generates .ts files for a CDS model to receive code completion in VS Code",
5
5
  "main": "index.js",
6
6
  "repository": "github:cap-js/cds-typer",
@@ -63,6 +63,13 @@
63
63
  ]
64
64
  },
65
65
  "cds": {
66
+ "typer": {
67
+ "output_directory": "@cds-models",
68
+ "inline_declarations": "flat",
69
+ "target_module_type": "auto",
70
+ "properties_optional": true,
71
+ "use_entities_proxy": false
72
+ },
66
73
  "schema": {
67
74
  "buildTaskType": {
68
75
  "name": "typescript",
@@ -123,6 +130,16 @@
123
130
  "type": "boolean",
124
131
  "description": "If set to true, floating point properties are generated\nas IEEE754 compatible '(number | string)' instead of 'number'.",
125
132
  "default": false
133
+ },
134
+ "target_module_type": {
135
+ "type": "string",
136
+ "description": "Output format for generated .js files.\nSetting it to auto tries to derive the module type from\nthe package.json and falls back to CJS.",
137
+ "enum": [
138
+ "esm",
139
+ "cjs",
140
+ "auto"
141
+ ],
142
+ "default": "auto"
126
143
  }
127
144
  }
128
145
  }