@cap-js/cds-typer 0.20.1 → 0.21.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,7 +4,25 @@ 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.21.0 - TBD
7
+ ## Version 0.22.0 - TBD
8
+
9
+ ## Version 0.21.0 - 2024-05-31
10
+ ### Added
11
+ - Added `IEEE754Compatible` flag which, when set to `true`, generates decimal fields as `(number | string)` instead of `number`. This flag will be removed in the long run
12
+ - Added plugin to `cds build` TypeScript projects. Can be explicitly called using `cds build --for typescript`
13
+
14
+ ### Changed
15
+ - Types representing CDS events are now only `declare`d to avoid having to make their properties optional
16
+ - Singular forms in generated _index.js_ files now contain a `.is_singular` property as marker for distinguished handling of singular and plural in the runtime
17
+ - Parameters passed to the CLI now take precedence over configuration contained in the `typer` section of `cds.env`
18
+
19
+ ### Fixed
20
+ - Entities ending with an "s" are no longer incorrectly truncated within `extends`-clauses
21
+ - Entity names prefixed with their own namespace (e.g. `Name.Name`, `Name.NameAttachments`) are not stripped of their name prefix
22
+
23
+ ## Version 0.20.2 - 2024-04-29
24
+ ### Fixed
25
+ - Referring to a property's type in a function/ action parameter no longer refers to the enclosing entity
8
26
 
9
27
  ## Version 0.20.1 - 2024-04-24
10
28
  ### Fixed
@@ -13,7 +31,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
13
31
  ## Version 0.20.0 - 2024-04-23
14
32
  ### Added
15
33
  - Types for actions and functions now expose a `.kind` property which holds the string `'function'` or `'action'` respectively
16
- - Added the CdsDate, CdsDateTime, CdsTime, CdsTimestamp types, which are each represented as a `string`.
34
+ - Added the `CdsDate`, `CdsDateTime`, `CdsTime`, `CdsTimestamp` types, which are each represented as a `string`.
17
35
  - Plural types can now also contain an optional numeric `$count` property
18
36
 
19
37
  ### Changed
package/lib/cli.js CHANGED
@@ -41,6 +41,11 @@ const flags = {
41
41
  desc: `If set to true, properties in entities are${EOL}always generated as optional (a?: T).`,
42
42
  allowed: ['true', 'false'],
43
43
  default: 'true'
44
+ },
45
+ IEEE754Compatible: {
46
+ desc: `If set to true, floating point properties are generated${EOL}as IEEE754 compatible '(number | string)' instead of 'number'.`,
47
+ allowed: ['true', 'false'],
48
+ default: 'false'
44
49
  }
45
50
  }
46
51
 
@@ -93,7 +98,8 @@ const main = async args => {
93
98
  logLevel: Levels[args.named.logLevel] ?? args.named.logLevel,
94
99
  jsConfigPath: args.named.jsConfigPath,
95
100
  inlineDeclarations: args.named.inlineDeclarations,
96
- propertiesOptional: args.named.propertiesOptional === 'true'
101
+ propertiesOptional: args.named.propertiesOptional === 'true',
102
+ IEEE754Compatible: args.named.IEEE754Compatible === 'true'
97
103
  })
98
104
  }
99
105
 
package/lib/compile.js CHANGED
@@ -45,7 +45,7 @@ const writeJsConfig = (path, logger) => {
45
45
  */
46
46
  const compileFromCSN = async (csn, parameters) => {
47
47
  const envSettings = cds.env?.typer ?? {}
48
- parameters = { ...parameters, ...envSettings }
48
+ parameters = { ...envSettings, ...parameters }
49
49
  const logger = new Logger()
50
50
  logger.addFrom(parameters.logLevel)
51
51
  if (parameters.jsConfigPath) {
@@ -39,48 +39,61 @@ const { baseDefinitions } = require('./basedefs')
39
39
  * }} TypeResolveInfo
40
40
  */
41
41
 
42
- /**
43
- * Builtin types defined by CDS.
44
- */
45
- const Builtins = {
46
- UUID: 'string',
47
- String: 'string',
48
- Binary: 'string',
49
- LargeString: 'string',
50
- LargeBinary: 'Buffer | string | {value: import("stream").Readable, $mediaContentType: string, $mediaContentDispositionFilename?: string, $mediaContentDispositionType?: string}',
51
- Vector: 'string',
52
- Integer: 'number',
53
- UInt8: 'number',
54
- Int16: 'number',
55
- Int32: 'number',
56
- Int64: 'number',
57
- Integer64: 'number',
58
- Decimal: 'number',
59
- DecimalFloat: 'number',
60
- Float: 'number',
61
- Double: 'number',
62
- Boolean: 'boolean',
63
- // note: the date-related types are strings on purpose, which reflects their runtime behaviour
64
- Date: '__.CdsDate', // yyyy-mm-dd
65
- DateTime: '__.CdsDateTime', // yyyy-mm-dd + time + TZ (precision: seconds)
66
- Time: '__.CdsTime', // hh:mm:ss
67
- Timestamp: '__.CdsTimestamp', // yyy-mm-dd + time + TZ (ms precision)
68
- //
69
- Composition: 'Array',
70
- Association: 'Array'
71
- }
42
+ class BuiltinResolver {
43
+ /**
44
+ * Builtin types defined by CDS.
45
+ */
46
+ #builtins = {
47
+ UUID: 'string',
48
+ String: 'string',
49
+ Binary: 'string',
50
+ LargeString: 'string',
51
+ LargeBinary: 'Buffer | string | {value: import("stream").Readable, $mediaContentType: string, $mediaContentDispositionFilename?: string, $mediaContentDispositionType?: string}',
52
+ Vector: 'string',
53
+ Integer: 'number',
54
+ UInt8: 'number',
55
+ Int16: 'number',
56
+ Int32: 'number',
57
+ Int64: 'number',
58
+ Integer64: 'number',
59
+ Decimal: 'number',
60
+ DecimalFloat: 'number',
61
+ Float: 'number',
62
+ Double: 'number',
63
+ Boolean: 'boolean',
64
+ // note: the date-related types are strings on purpose, which reflects their runtime behaviour
65
+ Date: '__.CdsDate', // yyyy-mm-dd
66
+ DateTime: '__.CdsDateTime', // yyyy-mm-dd + time + TZ (precision: seconds)
67
+ Time: '__.CdsTime', // hh:mm:ss
68
+ Timestamp: '__.CdsTimestamp', // yyy-mm-dd + time + TZ (ms precision)
69
+ //
70
+ Composition: 'Array',
71
+ Association: 'Array'
72
+ }
72
73
 
73
- /**
74
- * @param {string | string[]} type name or parts of the type name split on dots
75
- * @returns {string | undefined | false} if t refers to a builtin, the name of the corresponding TS type is returned.
76
- * If t _looks like_ a builtin (`cds.X`), undefined is returned.
77
- * If t is obviously not a builtin, false is returned.
78
- */
79
- function resolveBuiltin (t) {
80
- const path = Array.isArray(t) ? t : t.split('.')
81
- return path.length === 2 && path[0] === 'cds'
82
- ? Builtins[path[1]]
83
- : false
74
+ constructor ({ IEEE754Compatible } = {}) {
75
+ if (IEEE754Compatible) {
76
+ this.#builtins.Decimal = '(number | string)'
77
+ this.#builtins.DecimalFloat = '(number | string)'
78
+ this.#builtins.Float = '(number | string)'
79
+ this.#builtins.Double = '(number | string)'
80
+ }
81
+ this.#builtins = Object.freeze(this.#builtins)
82
+ }
83
+
84
+ /**
85
+ * @param {string | string[]} type name or parts of the type name split on dots
86
+ * @returns {string | undefined | false} if t refers to a builtin, the name of the corresponding TS type is returned.
87
+ * If t _looks like_ a builtin (`cds.X`), undefined is returned.
88
+ * If t is obviously not a builtin, false is returned.
89
+ */
90
+ resolveBuiltin (t) {
91
+ if (!Array.isArray(t) && typeof t !== 'string') return false
92
+ const path = Array.isArray(t) ? t : t.split('.')
93
+ return path.length === 2 && path[0] === 'cds'
94
+ ? this.#builtins[path[1]]
95
+ : false
96
+ }
84
97
  }
85
98
 
86
99
  class Resolver {
@@ -135,6 +148,9 @@ class Resolver {
135
148
  /** @type {Visitor} */
136
149
  this.visitor = visitor
137
150
 
151
+ /** @type {BuiltinResolver} */
152
+ this.builtinResolver = new BuiltinResolver(visitor.options)
153
+
138
154
  /** @type {Library[]} */
139
155
  this.libraries = [new Library(require.resolve('../../library/cds.hana.ts'))]
140
156
 
@@ -171,7 +187,7 @@ class Resolver {
171
187
  * @returns {Untangled} untangled qualifier
172
188
  */
173
189
  untangle(fq) {
174
- const builtin = resolveBuiltin(fq)
190
+ const builtin = this.builtinResolver.resolveBuiltin(fq)
175
191
  if (builtin) return { namespace: new Path([]), name: builtin, property: [], scope: [] }
176
192
 
177
193
  const ns = this.resolveNamespace(fq)
@@ -318,7 +334,7 @@ class Resolver {
318
334
  // don't slice off namespace if it isn't part of the inflected name.
319
335
  // This happens when the user adds an annotation and singular4 therefore
320
336
  // already returns an identifier without namespace. Plural has ns already sliced off.
321
- if (namespace && singular.startsWith(namespace)) {
337
+ if (namespace && singular.startsWith(namespace+'.')) {
322
338
  singular = singular.slice(namespace.length + 1)
323
339
  }
324
340
 
@@ -641,7 +657,7 @@ class Resolver {
641
657
  #resolveTypeName(t, into) {
642
658
  const result = into || {}
643
659
  const path = t.split('.')
644
- const builtin = resolveBuiltin(path)
660
+ const builtin = this.builtinResolver.resolveBuiltin(path)
645
661
  if (builtin === undefined) {
646
662
  // looks like builtin, but isn't
647
663
  throw new Error(`Can not resolve apparent builtin type '${t}' to any CDS type.`)
@@ -698,6 +714,5 @@ class Resolver {
698
714
  }
699
715
 
700
716
  module.exports = {
701
- Resolver,
702
- resolveBuiltin
717
+ Resolver
703
718
  }
package/lib/file.js CHANGED
@@ -391,18 +391,20 @@ class SourceFile extends File {
391
391
  // that makes sure we have defined all parent namespaces before adding subclasses to them e.g.:
392
392
  // "module.exports.Books" is defined before "module.exports.Books.text"
393
393
  .sort(([a], [b]) => a.split('.').length - b.split('.').length)
394
- // by using a temporary Set we make sure to catch cases where
395
- // (1) plural is the same as original (default case) and
396
- // (2) plural differs from original, i.e. when a @plural annotation is present
397
- // or when plural4 produced weird inflection.
398
- .flatMap(([singular, plural, original]) => Array.from(new Set([
399
- `module.exports.${singular} = csn.${original}`,
400
- /Array<.*>/.test(plural) ? undefined : `module.exports.${plural} = csn.${original}`,
394
+ .flatMap(([singular, plural, original]) => {
395
+ const exports = [`module.exports.${singular} = { is_singular: true, __proto__: csn.${original} }`]
396
+ if (!/Array<.*>/.test(plural) && plural !== original) {
397
+ // 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
398
+ exports.push(`module.exports.${plural} = csn.${original}`)
399
+ }
401
400
  // FIXME: we currently produce at most 3 entries.
402
401
  // This could be an issue when the user re-used the original name in a @singular/@plural annotation.
403
402
  // Seems unlikely, but we have to eliminate the original entry if users start running into this.
404
- `module.exports.${original} = csn.${original}`
405
- ].filter(Boolean)))) // 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
403
+ if (singular !== original) {
404
+ exports.push(`module.exports.${original} = { is_singular: true, __proto__: csn.${original} }`)
405
+ }
406
+ return exports
407
+ })
406
408
  ) // singular -> plural aliases
407
409
  .concat(['// events'])
408
410
  .concat(this.events.fqs.map(({fq, name}) => `module.exports.${name} = '${fq}'`))
package/lib/visitor.js CHANGED
@@ -6,7 +6,7 @@ const { amendCSN, isView, isUnresolved, propagateForeignKeys, isDraftEnabled, is
6
6
  // eslint-disable-next-line no-unused-vars
7
7
  const { SourceFile, FileRepository, Buffer } = require('./file')
8
8
  const { FlatInlineDeclarationResolver, StructuredInlineDeclarationResolver } = require('./components/inline')
9
- const { Resolver, resolveBuiltin } = require('./components/resolver')
9
+ const { Resolver } = require('./components/resolver')
10
10
  const { Logger } = require('./logging')
11
11
  const { docify } = require('./components/wrappers')
12
12
  const { csnToEnumPairs, propertyToInlineEnumName, isInlineEnumType, stringifyEnumType } = require('./components/enum')
@@ -19,7 +19,7 @@ const { baseDefinitions } = require('./components/basedefs')
19
19
 
20
20
  /**
21
21
  * @typedef { {
22
- * rootDirectory: string,
22
+ * outputDirectory: string,
23
23
  * logLevel: number,
24
24
  * jsConfigPath?: string
25
25
  * }} CompileParameters
@@ -227,13 +227,16 @@ class Visitor {
227
227
  file.addImport(namespace)
228
228
  return [namespace, name, parent]
229
229
  })
230
- .concat([[undefined, clean, [namespace, clean].filter(Boolean).join('.')]]) // add own aspect without namespace AFTER imports were created
230
+ .concat([[undefined, clean, name]]) // add own aspect without namespace AFTER imports were created
231
+ //.concat([[undefined, clean, [namespace, clean].filter(Boolean).join('.')]]) // add own aspect without namespace AFTER imports were created
231
232
  .reverse() // reverse so that own aspect A is applied before extensions B,C: B(C(A(Entity)))
232
233
  .reduce((wrapped, [ns, n, fq]) => {
233
234
  // types are not inflected, so don't change those to singular
234
235
  const refersToType = isType(this.csn.inferred.definitions[fq])
235
- const ident = identAspect(refersToType ? n : util.singular4(n))
236
-
236
+ const ident = identAspect(refersToType
237
+ ? n
238
+ : this.resolver.inflect({csn: this.csn.inferred.definitions[fq], plainName: n}).singular
239
+ )
237
240
  return !ns || ns.isCwd(file.path.asDirectory())
238
241
  ? `${ident}(${wrapped})`
239
242
  : `${ns.asIdentifier()}.${ident}(${wrapped})`
@@ -307,6 +310,7 @@ class Visitor {
307
310
  this.#aspectify(name, target, buffer, { cleanName: singular })
308
311
 
309
312
  buffer.add(overrideNameProperty(singular, entity.name))
313
+ buffer.add(`Object.defineProperty(${singular}, 'is_singular', { value: true })`)
310
314
 
311
315
  // PLURAL
312
316
 
@@ -349,11 +353,11 @@ class Visitor {
349
353
  #stringifyFunctionParamType(type, file) {
350
354
  // if type.type is not 'cds.String', 'cds.Integer', ..., then we are actually looking
351
355
  // at a named enum type. In that case also resolve that type name
352
- if (type.enum && resolveBuiltin(type.type)) return stringifyEnumType(csnToEnumPairs(type))
356
+ if (type.enum && this.resolver.builtinResolver.resolveBuiltin(type.type)) return stringifyEnumType(csnToEnumPairs(type))
353
357
  const paramType = this.resolver.resolveAndRequire(type, file)
354
358
  return this.inlineDeclarationResolver.getPropertyDatatype(
355
359
  paramType,
356
- paramType.typeInfo.isArray ? paramType.typeName : paramType.typeInfo.inflection.singular
360
+ paramType.typeInfo.isArray || paramType.typeInfo.isDeepRequire ? paramType.typeName : paramType.typeInfo.inflection.singular
357
361
  )
358
362
  }
359
363
 
@@ -384,7 +388,7 @@ class Visitor {
384
388
  // skip references to enums.
385
389
  // "Base" enums will always have a builtin type (don't skip those).
386
390
  // A type referencing an enum E will be considered an enum itself and have .type === E (skip).
387
- if ('enum' in type && !isReferenceType(type) && resolveBuiltin(type.type)) {
391
+ if ('enum' in type && !isReferenceType(type) && this.resolver.builtinResolver.resolveBuiltin(type.type)) {
388
392
  file.addEnum(name, clean, csnToEnumPairs(type))
389
393
  } else {
390
394
  // alias
@@ -412,7 +416,8 @@ class Visitor {
412
416
  file.addEvent(clean, name)
413
417
  const buffer = file.events.buffer
414
418
  buffer.add('// event')
415
- buffer.addIndentedBlock(`export class ${clean} {`, function() {
419
+ // only declare classes, as their properties are not optional, so we don't have to do awkward initialisation thereof.
420
+ buffer.addIndentedBlock(`export declare class ${clean} {`, function() {
416
421
  const propOpt = this.options.propertiesOptional
417
422
  this.options.propertiesOptional = false
418
423
  for (const [ename, element] of Object.entries(event.elements ?? {})) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/cds-typer",
3
- "version": "0.20.1",
3
+ "version": "0.21.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",
@@ -15,8 +15,9 @@
15
15
  "scripts": {
16
16
  "test:unit": "jest --projects test/unit.jest.config.js",
17
17
  "test:integration": "jest --projects test/int.jest.config.js",
18
+ "test:smoke": "jest --projects test/smoke.jest.config.js",
18
19
  "test:all": "jest",
19
- "test": "npm run test:unit",
20
+ "test": "npm run test:smoke && npm run test:unit",
20
21
  "lint": "npx eslint .",
21
22
  "cli": "node lib/cli.js",
22
23
  "doc:clean": "rm -rf ./doc",
@@ -49,6 +50,7 @@
49
50
  },
50
51
  "jest": {
51
52
  "projects": [
53
+ "test/smoke.jest.config.js",
52
54
  "test/unit.jest.config.js"
53
55
  ]
54
56
  }