@cap-js/cds-typer 0.24.0 → 0.25.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/lib/visitor.js CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  const util = require('./util')
4
4
 
5
- const { amendCSN, isView, isUnresolved, propagateForeignKeys, isDraftEnabled, isType, isProjection, getMaxCardinality } = require('./csn')
5
+ const { amendCSN, isView, isUnresolved, propagateForeignKeys, isDraftEnabled, isType, isProjection, getMaxCardinality, isViewOrProjection, isEnum } = require('./csn')
6
6
  // eslint-disable-next-line no-unused-vars
7
- const { SourceFile, FileRepository, Buffer } = require('./file')
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')
@@ -13,14 +13,18 @@ const { csnToEnumPairs, propertyToInlineEnumName, isInlineEnumType, stringifyEnu
13
13
  const { isReferenceType } = require('./components/reference')
14
14
  const { empty } = require('./components/typescript')
15
15
  const { baseDefinitions } = require('./components/basedefs')
16
- const { EntityRepository } = require('./resolution/entity')
16
+ const { EntityRepository, asIdentifier } = require('./resolution/entity')
17
17
  const { last } = require('./components/identifier')
18
+ const { getPropertyModifiers } = require('./components/property')
18
19
 
19
20
  /** @typedef {import('./file').File} File */
20
21
  /** @typedef {import('./typedefs').visitor.Context} Context */
21
22
  /** @typedef {import('./typedefs').visitor.CompileParameters} CompileParameters */
22
23
  /** @typedef {import('./typedefs').visitor.VisitorOptions} VisitorOptions */
23
24
  /** @typedef {import('./typedefs').visitor.Inflection} Inflection */
25
+ /** @typedef {import('./typedefs').resolver.CSN} CSN */
26
+ /** @typedef {import('./typedefs').resolver.EntityCSN} EntityCSN */
27
+ /** @typedef {import('./typedefs').resolver.EnumCSN} EnumCSN */
24
28
 
25
29
  const defaults = {
26
30
  // FIXME: add defaults for remaining parameters
@@ -36,12 +40,12 @@ class Visitor {
36
40
  * @returns {File[]} a full list of files to be written
37
41
  */
38
42
  getWriteoutFiles() {
39
- return this.fileRepository.getFiles().concat(this.resolver.getUsedLibraries())
43
+ return [...this.fileRepository.getFiles(), ...this.resolver.getUsedLibraries()]
40
44
  }
41
45
 
42
46
  /**
43
47
  * @param {{xtended: CSN, inferred: CSN}} csn - root CSN
44
- * @param {VisitorOptions} options - the options
48
+ * @param {VisitorOptions | {}} options - the options
45
49
  */
46
50
  constructor(csn, options = {}) {
47
51
  amendCSN(csn.xtended)
@@ -124,6 +128,34 @@ class Visitor {
124
128
  })
125
129
  }
126
130
 
131
+ /**
132
+ * @param {EntityCSN} entity - the entity to print the actions for
133
+ * @param {Buffer} buffer - the buffer to write the actions into
134
+ * @param {import('./typedefs').resolver.EntityInfo[]} ancestors - the fully qualified names of the ancestors of the entity
135
+ * @param {SourceFile} file - the file the entity is being printed into
136
+ */
137
+ #printStaticActions(entity, buffer, ancestors, file) {
138
+ // TODO: refactor away! All these printing functionalities need to go
139
+ const actions = Object.entries(entity.actions ?? {})
140
+ const inherited = ancestors.length
141
+ ? ancestors.map(a => `typeof ${asIdentifier({info: a, relative: file.path})}.actions`).join(' & ') + ' & '
142
+ : ''
143
+ if (actions.length) {
144
+ buffer.addIndentedBlock(`declare static readonly actions: ${inherited}{`,
145
+ actions.map(([aname, action]) => SourceFile.stringifyLambda({
146
+ name: aname,
147
+ parameters: this.#stringifyFunctionParams(action.params, file),
148
+ returns: action.returns
149
+ ? this.resolver.resolveAndRequire(action.returns, file).typeName
150
+ : 'any',
151
+ kind: action.kind
152
+ })), '}'
153
+ ) // end of actions
154
+ } else {
155
+ buffer.add(`declare static readonly actions: ${inherited}${empty}`)
156
+ }
157
+ }
158
+
127
159
  /**
128
160
  * Transforms an entity or CDS aspect into a JS aspect (aka mixin).
129
161
  * That is, for an element A we get:
@@ -131,42 +163,66 @@ class Visitor {
131
163
  * - the const AXtended which represents the entity A with all of its aspects mixed in (this const is not exported)
132
164
  * - the type A to use for external typing and is derived from AXtended.
133
165
  * @param {string} fq - the name of the entity
134
- * @param {CSN} entity - the pointer into the CSN to extract the elements from
166
+ * @param {EntityCSN} entity - the pointer into the CSN to extract the elements from
135
167
  * @param {Buffer} buffer - the buffer to write the resulting definitions into
136
168
  * @param {{cleanName?: string}} options - additional options
137
169
  */
138
170
  #aspectify(fq, entity, buffer, options = {}) {
139
171
  const info = this.entityRepository.getByFq(fq)
172
+ if (!info) throw new Error(`could not resolve entity ${fq}`)
140
173
  const clean = options?.cleanName ?? info.withoutNamespace
141
174
  const { namespace } = info
142
175
  const file = this.fileRepository.getNamespaceFile(namespace)
143
- const identSingular = name => name
176
+ const identSingular = (/** @type {string} */name) => name // FIXME: remove
177
+ //const identAspect = name => `_${name}Aspect`
178
+ /** @param {string} name - the name */
144
179
  const identAspect = name => `_${name}Aspect`
145
- const toAspectIdent = (wrapped, [ns, n, fq]) => {
180
+ /**
181
+ * @param {object} options - options
182
+ * @param {Path} [options.ns] - namespace
183
+ * @param {string} options.clean - the clean name of the entity
184
+ * @param {string} options.fq - fully qualified name
185
+ * @returns {string} the local identifier
186
+ */
187
+ // FIXME: replace with resolution/entity::asIdentifier
188
+ const toLocalIdent = ({ns, clean, fq}) => {
146
189
  // types are not inflected, so don't change those to singular
147
- const refersToType = isType(this.csn.inferred.definitions[fq])
148
- const ident = identAspect(refersToType
149
- ? n
150
- : this.resolver.inflect({csn: this.csn.inferred.definitions[fq], plainName: n}).singular
151
- )
190
+ const csn = this.csn.inferred.definitions[fq]
191
+ const ident = isType(csn)
192
+ ? clean
193
+ : this.resolver.inflect({csn, plainName: clean}).singular
152
194
  return !ns || ns.isCwd(file.path.asDirectory())
153
- ? `${ident}(${wrapped})`
154
- : `${ns.asIdentifier()}.${ident}(${wrapped})`
195
+ ? ident
196
+ : `${ns.asIdentifier()}.${ident}`
155
197
  }
156
- const ancestors = (entity.includes ?? [])
157
- .map(parent => {
158
- const { namespace, entityName } = this.entityRepository.getByFq(parent)
159
- file.addImport(namespace)
160
- return [namespace, entityName, parent]
198
+ // remove the ancestry of projections/ views.
199
+ // They explicitly define their properties.
200
+ // But at the same time they also carry their .includes clause, which can
201
+ // clash when the user aliases a selected property with the name of
202
+ // a properties of the entities they project on:
203
+ // entity foo as SELECT bar.baz AS ID FROM bar
204
+ // will produce an error if bar also has a property ID which now clashes with foo.ID
205
+ // WARNING: annotations from entities without properties should actually be propagated this way!
206
+ // So once we start caring about annotations, we have to revisit this part.
207
+ /** @type {import('./typedefs').resolver.EntityInfo[]} */
208
+ const ancestorInfos = ((!isViewOrProjection(entity) ? entity.includes : []) ?? [])
209
+ .map(ancestor => {
210
+ const info = this.entityRepository.getByFq(ancestor)
211
+ if (!info) throw new Error(`could not resolve ancestor ${ancestor} for ${fq}`)
212
+ file.addImport(info.namespace)
213
+ return info
161
214
  })
215
+
216
+ const ancestorsAspects = ancestorInfos
162
217
  .reverse() // reverse so that own aspect A is applied before extensions B,C: B(C(A(Entity)))
163
- .reduce(toAspectIdent, 'Base')
218
+ .reduce((wrapped, ancestor) => `${asIdentifier({info: ancestor, wrapper: name => `_${name}Aspect`, relative: file.path})}(${wrapped})`, 'Base')
164
219
 
165
220
  this.contexts.push({ entity: fq })
166
221
 
167
222
  // CLASS ASPECT
168
223
  buffer.addIndentedBlock(`export function ${identAspect(clean)}<TBase extends new (...args: any[]) => object>(Base: TBase) {`, () => {
169
- buffer.addIndentedBlock(`return class ${clean} extends ${ancestors} {`, () => {
224
+ buffer.addIndentedBlock(`return class ${clean} extends ${ancestorsAspects} {`, () => {
225
+ /** @type {import('./typedefs').resolver.EnumCSN[]} */
170
226
  const enums = []
171
227
  for (let [ename, element] of Object.entries(entity.elements ?? {})) {
172
228
  if (element.target && /\.texts?/.test(element.target)) {
@@ -176,14 +232,14 @@ class Visitor {
176
232
  this.visitElement(ename, element, file, buffer)
177
233
 
178
234
  // make foreign keys explicit
179
- if ('target' in element) {
235
+ if (element.target) {
180
236
  // lookup in cds.definitions can fail for inline structs.
181
237
  // We don't really have to care for this case, as keys from such structs are _not_ propagated to
182
238
  // the containing entity.
183
239
  for (const [kname, originalKeyElement] of this.#keys(element.target)) {
184
240
  if (getMaxCardinality(element) === 1 && typeof element.on !== 'object') { // FIXME: kelement?
185
241
  const foreignKey = `${ename}_${kname}`
186
- if (Object.hasOwn(entity.elements, foreignKey)) {
242
+ if (entity.elements && Object.hasOwn(entity.elements, foreignKey)) {
187
243
  LOG.error(`Attempting to generate a foreign key reference called '${foreignKey}' in type definition for entity ${fq}. But a property of that name is already defined explicitly. Consider renaming that property.`)
188
244
  } else {
189
245
  const kelement = Object.assign(Object.create(originalKeyElement), {
@@ -206,37 +262,39 @@ class Visitor {
206
262
  buffer.add(`static ${e.name} = ${propertyToInlineEnumName(clean, e.name)}`)
207
263
  file.addInlineEnum(clean, fq, e.name, csnToEnumPairs(e, {unwrapVals: true}))
208
264
  }
209
- const actions = Object.entries(entity.actions ?? {})
210
- if (actions.length) {
211
- buffer.addIndentedBlock('static readonly actions: {',
212
- actions.map(([aname, action]) => SourceFile.stringifyLambda({
213
- name: aname,
214
- parameters: this.#stringifyFunctionParams(action.params, file),
215
- returns: action.returns ? this.resolver.resolveAndRequire(action.returns, file).typeName : 'any',
216
- kind: action.kind
217
- }))
218
- , '}') // end of actions
219
- } else {
220
- buffer.add(`static readonly actions: ${empty}`)
221
- }
265
+ this.#printStaticActions(entity, buffer, ancestorInfos, file)
222
266
  })
223
267
  }, '};') // end of generated class
224
268
  }, '}') // end of aspect
225
269
 
226
270
  // CLASS WITH ADDED ASPECTS
227
271
  file.addImport(baseDefinitions.path)
228
- buffer.add(`export class ${identSingular(clean)} extends ${toAspectIdent(`${baseDefinitions.path.asIdentifier()}.Entity`, [undefined, clean, fq])} {${this.#staticClassContents(clean, entity).join('\n')}}`)
272
+ buffer.add(`export class ${identSingular(clean)} extends ${identAspect(toLocalIdent({clean, fq}))}(${baseDefinitions.path.asIdentifier()}.Entity) {${this.#staticClassContents(clean, entity).join('\n')}}`)
229
273
  this.contexts.pop()
230
274
  }
231
275
 
276
+ /**
277
+ * @param {string} clean - the clean name of the entity
278
+ * @param {EntityCSN} entity - the entity to generate the static contents for
279
+ */
232
280
  #staticClassContents(clean, entity) {
233
281
  return isDraftEnabled(entity) ? [`static drafts: typeof ${clean}`] : []
234
282
  }
235
283
 
284
+ /**
285
+ * @param {string} fq - fully qualified name of the entity
286
+ * @param {EntityCSN} entity - the entity to print
287
+ */
236
288
  #printEntity(fq, entity) {
237
289
  // static .name has to be defined more forcefully: https://github.com/microsoft/TypeScript/issues/442
290
+ /**
291
+ * @param {string} clazz - the class to override the name property for
292
+ * @param {string} content - the content to set the name property to
293
+ */
238
294
  const overrideNameProperty = (clazz, content) => `Object.defineProperty(${clazz}, 'name', { value: '${content}' })`
239
- const { namespace: ns, entityName: clean, inflection } = this.entityRepository.getByFq(fq)
295
+ const info = this.entityRepository.getByFq(fq)
296
+ if (!info) throw new Error(`could not resolve entity ${fq}`)
297
+ const { namespace: ns, entityName: clean, inflection } = info
240
298
  const file = this.fileRepository.getNamespaceFile(ns)
241
299
  let { singular, plural } = inflection
242
300
 
@@ -309,15 +367,15 @@ class Visitor {
309
367
  * Stringifies function parameters in preparation of passing them to {@link SourceFile.stringifyLambda}.
310
368
  * Resolves all parameters to a pair of parameter name and name of the resolved type.
311
369
  * Also filters out parameters that indicate a binding parameter ({@link https://cap.cloud.sap/docs/releases/jan23#simplified-syntax-for-binding-parameters}).
312
- * @param {[string, object][]} params - parameter list as found in CSN.
313
- * @param {File} file - source file relative to which the parameter types should be resolved.
370
+ * @param {{[key:string]: EntityCSN}} params - parameter list as found in CSN.
371
+ * @param {SourceFile} file - source file relative to which the parameter types should be resolved.
314
372
  * @returns {[string, string][]} pair of names and types.
315
373
  */
316
374
  #stringifyFunctionParams(params, file) {
317
375
  return params
318
376
  ? Object.entries(params)
319
377
  // filter params of type '[many] $self', as they are not to be part of the implementation
320
- .filter(([, type]) => type?.type !== '$self' && !(type.items?.type === '$self'))
378
+ .filter(([, type]) => type?.type !== '$self' && type.items?.type !== '$self')
321
379
  .map(([name, type]) => [
322
380
  name,
323
381
  this.#stringifyFunctionParamType(type, file)
@@ -325,56 +383,80 @@ class Visitor {
325
383
  : []
326
384
  }
327
385
 
386
+ /**
387
+ * @param {EntityCSN | EnumCSN} type - type
388
+ * @param {SourceFile} file - the file to resolve types into
389
+ */
328
390
  #stringifyFunctionParamType(type, file) {
329
391
  // if type.type is not 'cds.String', 'cds.Integer', ..., then we are actually looking
330
392
  // at a named enum type. In that case also resolve that type name
331
- if (type.enum && this.resolver.builtinResolver.resolveBuiltin(type.type)) return stringifyEnumType(csnToEnumPairs(type))
393
+ const isNamedEnumType = isEnum(type) && this.resolver.builtinResolver.resolveBuiltin(type.type)
394
+ if (isNamedEnumType) return stringifyEnumType(csnToEnumPairs(type))
332
395
  const paramType = this.resolver.resolveAndRequire(type, file)
333
396
  return this.inlineDeclarationResolver.getPropertyDatatype(
334
397
  paramType,
335
- paramType.typeInfo.isArray || paramType.typeInfo.isDeepRequire ? paramType.typeName : paramType.typeInfo.inflection.singular
398
+ paramType.typeInfo.isArray || paramType.typeInfo.isDeepRequire
399
+ ? paramType.typeName
400
+ : paramType.typeInfo.inflection.singular
336
401
  )
337
402
  }
338
403
 
339
404
  /**
340
405
  * @param {string} fq - fully qualified name of the operation
341
- * @param {object} operation - CSN
406
+ * @param {import('./typedefs').resolver.OperationCSN} operation - CSN
342
407
  * @param {'function' | 'action'} kind - kind of operation
343
408
  */
344
409
  #printOperation(fq, operation, kind) {
345
410
  LOG.debug(`Printing operation ${fq}:\n${JSON.stringify(operation, null, 2)}`)
346
- const { namespace } = this.entityRepository.getByFq(fq)
411
+ const info = this.entityRepository.getByFq(fq)
412
+ if (!info) throw new Error(`could not resolve operation ${fq}`)
413
+ const { namespace } = info
347
414
  const file = this.fileRepository.getNamespaceFile(namespace)
348
415
  const params = this.#stringifyFunctionParams(operation.params, file)
349
416
  const returnType = operation.returns
350
417
  ? this.resolver.resolveAndRequire(operation.returns, file)
351
- : { typeName: 'void', typeInfo: { isArray: false, inflection: { singular: 'void', plural: 'void' } } }
418
+ : { typeName: 'void', typeInfo: { plainName: 'void', isArray: false, inflection: { singular: 'void', plural: 'void' } } }
352
419
  const returns = this.inlineDeclarationResolver.getPropertyDatatype(
353
420
  returnType,
354
- returnType.typeInfo.isArray ? returnType.typeName : returnType.typeInfo.inflection.singular
421
+ returnType.typeInfo.isArray
422
+ ? returnType.typeName
423
+ : returnType.typeInfo.inflection.singular
355
424
  )
356
425
  file.addOperation(last(fq), params, returns, kind)
357
426
  }
358
427
 
428
+ /**
429
+ * @param {string} fq - fully qualified name of the type
430
+ * @param {EntityCSN} type - CSN
431
+ */
359
432
  #printType(fq, type) {
360
433
  LOG.debug(`Printing type ${fq}:\n${JSON.stringify(type, null, 2)}`)
361
- const { namespace, entityName } = this.entityRepository.getByFq(fq)
434
+ const info = this.entityRepository.getByFq(fq)
435
+ if (!info) throw new Error(`could not resolve type ${fq}`)
436
+ const { namespace, entityName } = info
362
437
  const file = this.fileRepository.getNamespaceFile(namespace)
363
438
  // skip references to enums.
364
439
  // "Base" enums will always have a builtin type (don't skip those).
365
440
  // A type referencing an enum E will be considered an enum itself and have .type === E (skip).
366
- if ('enum' in type && !isReferenceType(type) && this.resolver.builtinResolver.resolveBuiltin(type.type)) {
441
+ if (isEnum(type) && !isReferenceType(type) && this.resolver.builtinResolver.resolveBuiltin(type.type)) {
367
442
  file.addEnum(fq, entityName, csnToEnumPairs(type))
368
443
  } else {
444
+ const isEnumReference = typeof type.type === 'string' && isEnum(this.csn.inferred.definitions[type?.type])
369
445
  // alias
370
- file.addType(fq, entityName, this.resolver.resolveAndRequire(type, file).typeName)
446
+ file.addType(fq, entityName, this.resolver.resolveAndRequire(type, file).typeName, isEnumReference)
371
447
  }
372
448
  // TODO: annotations not handled yet
373
449
  }
374
450
 
451
+ /**
452
+ * @param {string} fq - fully qualified name of the aspect
453
+ * @param {EntityCSN} aspect - CSN
454
+ */
375
455
  #printAspect(fq, aspect) {
376
456
  LOG.debug(`Printing aspect ${fq}`)
377
- const { namespace, entityName, inflection } = this.entityRepository.getByFq(fq)
457
+ const info = this.entityRepository.getByFq(fq)
458
+ if (!info) throw new Error(`could not resolve aspect ${fq}`)
459
+ const { namespace, entityName, inflection } = info
378
460
  const file = this.fileRepository.getNamespaceFile(namespace)
379
461
  // aspects are technically classes and can therefore be added to the list of defined classes.
380
462
  // Still, when using them as mixins for a class, they need to already be defined.
@@ -384,9 +466,15 @@ class Visitor {
384
466
  this.#aspectify(fq, aspect, file.aspects, { cleanName: inflection.singular })
385
467
  }
386
468
 
469
+ /**
470
+ * @param {string} fq - fully qualified name of the event
471
+ * @param {EntityCSN} event - CSN
472
+ */
387
473
  #printEvent(fq, event) {
388
474
  LOG.debug(`Printing event ${fq}`)
389
- const { namespace, entityName } = this.entityRepository.getByFq(fq)
475
+ const info = this.entityRepository.getByFq(fq)
476
+ if (!info) throw new Error(`could not resolve event ${fq}`)
477
+ const { namespace, entityName } = info
390
478
  const file = this.fileRepository.getNamespaceFile(namespace)
391
479
  file.addEvent(entityName, fq)
392
480
  const buffer = file.events.buffer
@@ -402,9 +490,15 @@ class Visitor {
402
490
  }, '}')
403
491
  }
404
492
 
493
+ /**
494
+ * @param {string} fq - fully qualified name of the service
495
+ * @param {EntityCSN} service - CSN
496
+ */
405
497
  #printService(fq, service) {
406
498
  LOG.debug(`Printing service ${fq}:\n${JSON.stringify(service, null, 2)}`)
407
- const { namespace } = this.entityRepository.getByFq(fq)
499
+ const info = this.entityRepository.getByFq(fq)
500
+ if (!info) throw new Error(`could not resolve service ${fq}`)
501
+ const { namespace } = info
408
502
  const file = this.fileRepository.getNamespaceFile(namespace)
409
503
  // service.name is clean of namespace
410
504
  file.services.buffer.add(`export default { name: '${service.name}' }`)
@@ -415,7 +509,7 @@ class Visitor {
415
509
  * Visits a single entity from the CSN's definition field.
416
510
  * Will call #printEntity or #printAction based on the entity's kind.
417
511
  * @param {string} fq - name of the entity, fully qualified as is used in the definition field.
418
- * @param {CSN} entity - CSN data belonging to the entity to perform lookups in.
512
+ * @param {EntityCSN} entity - CSN data belonging to the entity to perform lookups in.
419
513
  */
420
514
  visitEntity(fq, entity) {
421
515
  switch (entity.kind) {
@@ -424,6 +518,7 @@ class Visitor {
424
518
  break
425
519
  case 'action':
426
520
  case 'function':
521
+ // @ts-expect-error - we know entity is actually an OperationCSN
427
522
  this.#printOperation(fq, entity, entity.kind)
428
523
  break
429
524
  case 'aspect':
@@ -470,16 +565,25 @@ class Visitor {
470
565
  /**
471
566
  * Visits a single element in an entity.
472
567
  * @param {string} name - name of the element
473
- * @param {import('./resolution/resolver').CSN} element - CSN data belonging to the the element.
568
+ * @param {EntityCSN} element - CSN data belonging to the the element.
474
569
  * @param {SourceFile} file - the namespace file the surrounding entity is being printed into.
475
- * @param {Buffer} buffer - buffer to add the definition to. If no buffer is passed, the passed file's class buffer is used instead.
570
+ * @param {Buffer} [buffer] - buffer to add the definition to. If no buffer is passed, the passed file's class buffer is used instead.
476
571
  * @returns @see InlineDeclarationResolver.visitElement
477
572
  */
478
- visitElement(name, element, file, buffer) {
479
- return this.inlineDeclarationResolver.visitElement(name, element, file, buffer)
573
+ visitElement(name, element, file, buffer = file.classes) {
574
+ return this.inlineDeclarationResolver.visitElement({
575
+ name,
576
+ element,
577
+ file,
578
+ buffer,
579
+ // we explicitly pass the "declare" modifier here to avoid problems with noImplicitOverride and useDefineForClassFields in strict tsconfigs
580
+ // but not inside type defs (e.g. parameter types) where this would be a syntax error
581
+ modifiers: getPropertyModifiers(element)
582
+ })
480
583
  }
481
584
  }
482
585
 
483
586
  module.exports = {
484
587
  Visitor
485
588
  }
589
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/cds-typer",
3
- "version": "0.24.0",
3
+ "version": "0.25.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",
@@ -24,7 +24,8 @@
24
24
  "doc:clean": "rm -rf ./doc",
25
25
  "doc:prepare": "npm run doc:clean && mkdir -p doc/types",
26
26
  "doc:typegen": "./node_modules/.bin/tsc ./lib/*.js --skipLibCheck --declaration --allowJs --emitDeclarationOnly --outDir doc/types && cd doc/types && tsc --init",
27
- "doc:cli": "npm run cli -- --help > ./doc/cli.txt"
27
+ "doc:cli": "npm run cli -- --help > ./doc/cli.txt",
28
+ "jsdoc:check": "tsc --noEmit --project jsconfig.json"
28
29
  },
29
30
  "files": [
30
31
  "lib/",
@@ -44,6 +45,7 @@
44
45
  },
45
46
  "devDependencies": {
46
47
  "@stylistic/eslint-plugin-js": "^1.6.3",
48
+ "@cap-js/cds-types": ">=0.6",
47
49
  "acorn": "^8.10.0",
48
50
  "eslint": "^9",
49
51
  "eslint-plugin-jsdoc": "^48.2.7",