@cap-js/cds-typer 0.2.5-beta.1 → 0.4.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 +14 -1
- package/README.md +12 -21
- package/lib/compile.js +2 -859
- package/lib/components/inline.js +13 -12
- package/lib/components/resolver.js +471 -0
- package/lib/components/wrappers.js +66 -0
- package/lib/file.js +38 -9
- package/lib/logging.js +5 -4
- package/lib/util.js +45 -2
- package/lib/visitor.js +353 -0
- package/package.json +1 -1
- package/lib/compile.d.ts +0 -273
- package/lib/file.d.ts +0 -208
- package/lib/logging.d.ts +0 -50
- package/lib/util.d.ts +0 -87
package/lib/compile.js
CHANGED
|
@@ -4,865 +4,9 @@ const fs = require('fs')
|
|
|
4
4
|
const { normalize } = require('path')
|
|
5
5
|
const cds = require('@sap/cds')
|
|
6
6
|
const util = require('./util')
|
|
7
|
-
|
|
8
|
-
const { SourceFile, Path, Library, writeout, baseDefinitions, Buffer } = require('./file')
|
|
9
|
-
const { FlatInlineDeclarationResolver, StructuredInlineDeclarationResolver } = require('./components/inline')
|
|
7
|
+
const { writeout } = require('./file')
|
|
10
8
|
const { Logger } = require('./logging')
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
/** @typedef {import('./file').Buffer} Buffer */
|
|
15
|
-
/** @typedef {import('./file').File} File */
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* @typedef {{
|
|
19
|
-
* cardinality?: { max?: '*' | number }
|
|
20
|
-
* }} EntityCSN
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* @typedef {{
|
|
25
|
-
* definitions?: Object<string, EntityCSN>
|
|
26
|
-
* }} CSN
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* @typedef { {
|
|
31
|
-
* rootDirectory: string,
|
|
32
|
-
* logLevel: number,
|
|
33
|
-
* jsConfigPath?: string
|
|
34
|
-
* }} CompileParameters
|
|
35
|
-
*/
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* `propertiesOptional = true` -> all properties are generated as optional ?:. (standard CAP behaviour, where properties be unavailable)
|
|
39
|
-
* `inlineDeclarations = 'structured'` -> @see inline.StructuredInlineDeclarationResolver
|
|
40
|
-
* `inlineDeclarations = 'flat'` -> @see inline.FlatInlineDeclarationResolver
|
|
41
|
-
* @typedef { {
|
|
42
|
-
* propertiesOptional: boolean,
|
|
43
|
-
* inlineDeclarations: 'flat' | 'structured',
|
|
44
|
-
* }} VisitorOptions
|
|
45
|
-
*/
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* @typedef {{
|
|
49
|
-
* typeName: string,
|
|
50
|
-
* singular: string,
|
|
51
|
-
* plural: string
|
|
52
|
-
* }} Inflection
|
|
53
|
-
*/
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* When nested inline types require additional imports. E.g.:
|
|
57
|
-
* ```cds
|
|
58
|
-
* // mymodel.cds
|
|
59
|
-
* Foo {
|
|
60
|
-
* bar: {
|
|
61
|
-
* baz: a.b.c.Baz // need to require a.b.c in mymodel.cds!
|
|
62
|
-
* }
|
|
63
|
-
* }
|
|
64
|
-
* ```
|
|
65
|
-
* @typedef {{
|
|
66
|
-
* isBuiltin: boolean,
|
|
67
|
-
* isInlineDeclaration: boolean,
|
|
68
|
-
* isForeignKeyReference: boolean,
|
|
69
|
-
* type: string,
|
|
70
|
-
* path?: Path,
|
|
71
|
-
* csn?: CSN,
|
|
72
|
-
* imports: Path[]
|
|
73
|
-
* inner: TypeResolveInfo
|
|
74
|
-
* }} TypeResolveInfo
|
|
75
|
-
*/
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* @typedef {{
|
|
79
|
-
* entity: String
|
|
80
|
-
* }} Context
|
|
81
|
-
*/
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Builtin types defined by CDS.
|
|
85
|
-
*/
|
|
86
|
-
const Builtins = {
|
|
87
|
-
UUID: 'string',
|
|
88
|
-
String: 'string',
|
|
89
|
-
Binary: 'string',
|
|
90
|
-
LargeString: 'string',
|
|
91
|
-
LargeBinary: 'string',
|
|
92
|
-
Integer: 'number',
|
|
93
|
-
UInt8: 'number',
|
|
94
|
-
Int16: 'number',
|
|
95
|
-
Int32: 'number',
|
|
96
|
-
Int64: 'number',
|
|
97
|
-
Integer64: 'number',
|
|
98
|
-
Decimal: 'number',
|
|
99
|
-
DecimalFloat: 'number',
|
|
100
|
-
Float: 'number',
|
|
101
|
-
Double: 'number',
|
|
102
|
-
Boolean: 'boolean',
|
|
103
|
-
Date: 'Date',
|
|
104
|
-
DateTime: 'Date',
|
|
105
|
-
Time: 'Date',
|
|
106
|
-
Timestamp: 'Date',
|
|
107
|
-
//
|
|
108
|
-
Composition: 'Array',
|
|
109
|
-
Association: 'Array'
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const defaults = {
|
|
113
|
-
// FIXME: add defaults for remaining parameters
|
|
114
|
-
propertiesOptional: true,
|
|
115
|
-
inlineDeclarations: 'flat'
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
class Visitor {
|
|
120
|
-
/**
|
|
121
|
-
* Generates singular and plural inflection for the passed type.
|
|
122
|
-
* Several cases are covered here:
|
|
123
|
-
* - explicit annotation by the user in the CSN
|
|
124
|
-
* - implicitly derived inflection based on simple grammar rules
|
|
125
|
-
* - collisions between singular and plural name (resolved by appending a '_' suffix)
|
|
126
|
-
* - inline type definitions, which don't really have a linguistic plural,
|
|
127
|
-
* but need to expressed as array type to be consumable by the likes of Composition.of.many<T>
|
|
128
|
-
* @param {TypeResolveInfo} typeInfo information about the type gathered so far.
|
|
129
|
-
* @param {string} [namespace] namespace the type occurs in. If passed, will be shaved off from the name
|
|
130
|
-
* @returns {Inflection}
|
|
131
|
-
*/
|
|
132
|
-
inflect (typeInfo, namespace) {
|
|
133
|
-
let typeName
|
|
134
|
-
let singular
|
|
135
|
-
let plural
|
|
136
|
-
|
|
137
|
-
if (typeInfo.isInlineDeclaration) {
|
|
138
|
-
// if we detected an inline declaration, we take a quick detour via an InlineDeclarationResolver
|
|
139
|
-
// to rectify the typeName (which would be just '{' elsewise).
|
|
140
|
-
// The correct typename in string form is required in stringifyLambda(...)
|
|
141
|
-
// Note that whenever the typeName is relevant, it is assumed to be in structured form
|
|
142
|
-
// (i.e. a struct), so we always use a StructuredInlineDeclarationResolver here, regardless of
|
|
143
|
-
// what is configured for nested declarations in the visitor.
|
|
144
|
-
// FIXME: in most other places where we have an inline declaration, we actually don't need the typeName.
|
|
145
|
-
// If stringifyLambda(...) is the only place where we need this, we should have stringifyLambda call this
|
|
146
|
-
// piece of code instead to reduce overhead.
|
|
147
|
-
const into = new Buffer()
|
|
148
|
-
new StructuredInlineDeclarationResolver(this).printInlineType(undefined, {typeInfo}, into, '')
|
|
149
|
-
typeName = into.join(' ')
|
|
150
|
-
singular = typeName
|
|
151
|
-
plural = `Array<${typeName}>`
|
|
152
|
-
} else {
|
|
153
|
-
// TODO: make sure the resolution still works. Currently, we only cut off the namespace!
|
|
154
|
-
singular = util.singular4(typeInfo.csn)
|
|
155
|
-
plural = util.getPluralAnnotation(typeInfo.csn)
|
|
156
|
-
? util.plural4(typeInfo.csn)
|
|
157
|
-
: typeInfo.plainName
|
|
158
|
-
|
|
159
|
-
// don't slice off namespace if it isn't part of the inflected name.
|
|
160
|
-
// This happens when the user adds an annotation and singular4 therefore
|
|
161
|
-
// already returns an identifier without namespace
|
|
162
|
-
if (namespace && singular.startsWith(namespace)) {
|
|
163
|
-
// TODO: not totally sure why plural doesn't have to be sliced
|
|
164
|
-
singular = singular.slice(namespace.length + 1)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (singular === plural) {
|
|
168
|
-
// same as when creating the entity
|
|
169
|
-
plural += '_'
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
if (!singular || !plural) {
|
|
173
|
-
this.logger.error(`Singular ('${singular}') or plural ('${plural}') for '${typeName}' is empty.`)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return { typeName, singular, plural }
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Gathers all files that are supposed to be written to
|
|
181
|
-
* the type directory. Including generated source files,
|
|
182
|
-
* as well as library files.
|
|
183
|
-
* @returns {File[]} a full list of files to be written
|
|
184
|
-
*/
|
|
185
|
-
getWriteoutFiles() {
|
|
186
|
-
return Object.values(this.files).concat(this.libraries.filter(lib => lib.referenced))
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* @param csn root CSN
|
|
191
|
-
* @param {VisitorOptions} options
|
|
192
|
-
*/
|
|
193
|
-
constructor(csn, options = {}, logger = new Logger()) {
|
|
194
|
-
this.options = { ...defaults, ...options }
|
|
195
|
-
this.logger = logger
|
|
196
|
-
this.csn = csn
|
|
197
|
-
|
|
198
|
-
/** @type {Context[]} **/
|
|
199
|
-
this.contexts = []
|
|
200
|
-
|
|
201
|
-
/** @type {Library[]} */
|
|
202
|
-
this.libraries = [new Library(require.resolve('../library/cds.hana.ts'))]
|
|
203
|
-
|
|
204
|
-
/** @type {Object<string, File>} */
|
|
205
|
-
this.files = {}
|
|
206
|
-
this.files[baseDefinitions.path.asIdentifier()] = baseDefinitions
|
|
207
|
-
this.inlineDeclarationResolver = this.options.inlineDeclarations === 'structured'
|
|
208
|
-
? new StructuredInlineDeclarationResolver(this)
|
|
209
|
-
: new FlatInlineDeclarationResolver(this)
|
|
210
|
-
|
|
211
|
-
// Entities inherit their ancestors annotations:
|
|
212
|
-
// https://cap.cloud.sap/docs/cds/cdl#annotation-propagation
|
|
213
|
-
// This is a problem if we annotate @singular/ @plural to an entity A,
|
|
214
|
-
// as we don't want all descendents B, C, ... to share the ancestor's
|
|
215
|
-
// annotated inflexion
|
|
216
|
-
// -> remove all such annotations that appear in a parent as well.
|
|
217
|
-
// BUT: we can't just delete the attributes. Imagine three classes
|
|
218
|
-
// A <- B <- C
|
|
219
|
-
// where A contains a @singular annotation.
|
|
220
|
-
// If we erase the annotation from B, C will still contain it and
|
|
221
|
-
// can not detect that its own annotation was inherited without
|
|
222
|
-
// travelling up the entire inheritance chain up to A.
|
|
223
|
-
// So instead, we monkey patch and maintain a dictionary "erased"
|
|
224
|
-
// when removing an annotation which we also check.
|
|
225
|
-
const erase = (entity, parent, attr) => {
|
|
226
|
-
if (attr in entity) {
|
|
227
|
-
const ea = entity[attr]
|
|
228
|
-
if (parent[attr] === ea || (parent.erased && parent.erased[attr] === ea)) {
|
|
229
|
-
if (!('erased' in entity)) {
|
|
230
|
-
entity.erased = {}
|
|
231
|
-
}
|
|
232
|
-
entity.erased[attr] = ea
|
|
233
|
-
delete entity[attr]
|
|
234
|
-
this.logger.info(`Removing inherited attribute ${attr} from ${entity.name}.`)
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
for (const entity of Object.values(csn.definitions)) {
|
|
240
|
-
let i = 0
|
|
241
|
-
if (entity.includes) {
|
|
242
|
-
while (
|
|
243
|
-
(util.getSingularAnnotation(entity) || util.getPluralAnnotation(entity)) &&
|
|
244
|
-
i < entity.includes.length
|
|
245
|
-
) {
|
|
246
|
-
const parent = this.csn.definitions[entity.includes[i]]
|
|
247
|
-
Object.values(util.annotations).flat().forEach(an => erase(entity, parent, an))
|
|
248
|
-
i++
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
this.visitDefinitions()
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Determines the file corresponding to the namespace.
|
|
258
|
-
* If no such file exists yet, it is created first.
|
|
259
|
-
* @param {string} path the name of the namespace (foo.bar.baz)
|
|
260
|
-
* @returns {SourceFile} the file corresponding to that namespace name
|
|
261
|
-
*/
|
|
262
|
-
getNamespaceFile(path) {
|
|
263
|
-
if (!(path in this.files)) {
|
|
264
|
-
this.files[path] = new SourceFile(path)
|
|
265
|
-
}
|
|
266
|
-
return this.files[path]
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Visits all definitions within the CSN definitions.
|
|
271
|
-
*/
|
|
272
|
-
visitDefinitions() {
|
|
273
|
-
for (const [name, entity] of Object.entries(this.csn.definitions)) {
|
|
274
|
-
this.visitEntity(name, entity)
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Wraps type into association to scalar.
|
|
280
|
-
* @param {string} t the singular type name.
|
|
281
|
-
* @returns {string}
|
|
282
|
-
*/
|
|
283
|
-
_createToOneAssociation(t) {
|
|
284
|
-
return `${baseDefinitions.path.asIdentifier()}.Association.to<${t}>`
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Wraps type into association to vector.
|
|
289
|
-
* @param {string} t the singular type name.
|
|
290
|
-
* @returns {string}
|
|
291
|
-
*/
|
|
292
|
-
_createToManyAssociation(t) {
|
|
293
|
-
return `${baseDefinitions.path.asIdentifier()}.Association.to.many<${t}>`
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Wraps type into composition of scalar.
|
|
298
|
-
* @param {string} t the singular type name.
|
|
299
|
-
* @returns {string}
|
|
300
|
-
*/
|
|
301
|
-
_createCompositionOfOne(t) {
|
|
302
|
-
return `${baseDefinitions.path.asIdentifier()}.Composition.of<${t}>`
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Wraps type into composition of vector.
|
|
307
|
-
* @param {string} t the singular type name.
|
|
308
|
-
* @returns {string}
|
|
309
|
-
*/
|
|
310
|
-
_createCompositionOfMany(t) {
|
|
311
|
-
return `${baseDefinitions.path.asIdentifier()}.Composition.of.many<${t}>`
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Puts a passed string in docstring format.
|
|
316
|
-
* @param {string} doc raw string to docify. May contain linebreaks.
|
|
317
|
-
* @returns {string[]} an array of lines wrapped in doc format. The result is not
|
|
318
|
-
* concatenated to be properly indented by `buffer.add(...)`.
|
|
319
|
-
*/
|
|
320
|
-
_docify(doc) {
|
|
321
|
-
return doc ? ['/**'].concat(doc.split('\n').map((line) => `* ${line}`)).concat(['*/']) : []
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Transforms an entity or CDS aspect into a JS aspect (aka mixin).
|
|
326
|
-
* That is, for an element A we get:
|
|
327
|
-
* - the function A(B) to mix the aspect into another class B
|
|
328
|
-
* - the const AXtended which represents the entity A with all of its aspects mixed in (this const is not exported)
|
|
329
|
-
* - the type A to use for external typing and is derived from AXtended.
|
|
330
|
-
* @param {string} name the name of the entity
|
|
331
|
-
* @param {CSN} element the pointer into the CSN to extract the elements from
|
|
332
|
-
* @param {Buffer} buffer the buffer to write the resulting definitions into
|
|
333
|
-
* @param {string?} cleanName the clean name to use. If not passed, it is derived from the passed name instead.
|
|
334
|
-
*/
|
|
335
|
-
_aspectify(name, entity, buffer, cleanName = undefined) {
|
|
336
|
-
const clean = cleanName ?? this._trimNamespace(name)
|
|
337
|
-
const ns = this._resolveNamespace(name.split('.'))
|
|
338
|
-
const file = this.getNamespaceFile(ns)
|
|
339
|
-
|
|
340
|
-
const identSingular = name => name
|
|
341
|
-
const identAspect = name => `_${name}Aspect`
|
|
342
|
-
|
|
343
|
-
this.contexts.push({
|
|
344
|
-
entity: name
|
|
345
|
-
})
|
|
346
|
-
|
|
347
|
-
// CLASS ASPECT
|
|
348
|
-
// while I would personally prefer arrow-function style, using function syntax spares us troubles
|
|
349
|
-
// with hoisting:
|
|
350
|
-
//
|
|
351
|
-
// # does not work
|
|
352
|
-
// const A_fn = () => class ...
|
|
353
|
-
// const A = B_fn(A_fn(Entity)) // error: B called before declaration
|
|
354
|
-
// const B_fn = () => class ...
|
|
355
|
-
//
|
|
356
|
-
// # works
|
|
357
|
-
// function A_fn () { return class ... }
|
|
358
|
-
// const A = B_fn(A_fn(Entity))
|
|
359
|
-
// function B_fn () { return class ... }
|
|
360
|
-
|
|
361
|
-
buffer.add(`export function ${identAspect(clean)}<TBase extends new (...args: any[]) => any>(Base: TBase) {`)
|
|
362
|
-
buffer.indent()
|
|
363
|
-
buffer.add(`return class ${clean} extends Base {`)
|
|
364
|
-
buffer.indent()
|
|
365
|
-
for (const [ename, element] of Object.entries(entity.elements ?? {})) {
|
|
366
|
-
this.visitElement(ename, element, file, buffer)
|
|
367
|
-
}
|
|
368
|
-
for (const [aname, action] of Object.entries(entity.actions ?? {})) {
|
|
369
|
-
buffer.add(SourceFile.stringifyLambda(
|
|
370
|
-
aname,
|
|
371
|
-
Object.entries(action.params ?? {}).map(([n,t]) => [n, this.resolveAndRequire(t, file).typeName]),
|
|
372
|
-
action.returns ? this.resolveAndRequire(action.returns, file).typeName : 'any'
|
|
373
|
-
))
|
|
374
|
-
//this.visitEntity(aname, action, file, buffer)
|
|
375
|
-
}
|
|
376
|
-
buffer.outdent()
|
|
377
|
-
buffer.add('};')
|
|
378
|
-
buffer.outdent()
|
|
379
|
-
buffer.add('}')
|
|
380
|
-
|
|
381
|
-
// CLASS WITH ADDED ASPECTS
|
|
382
|
-
file.addImport(baseDefinitions.path)
|
|
383
|
-
const rhs = (entity.includes ?? [])
|
|
384
|
-
.map((parent) => {
|
|
385
|
-
const [ns, n] = this.untangle(parent)
|
|
386
|
-
file.addImport(ns)
|
|
387
|
-
return [ns, n]
|
|
388
|
-
})
|
|
389
|
-
.concat([[undefined, clean]]) // add own aspect without namespace AFTER imports were created
|
|
390
|
-
.reverse() // reverse so that own aspect A is applied before extensions B,C: B(C(A(Entity)))
|
|
391
|
-
.reduce(
|
|
392
|
-
(wrapped, [ns, n]) =>
|
|
393
|
-
!ns || ns.isCwd(file.path.asDirectory()) ? `${identAspect(n)}(${wrapped})` : `${ns.asIdentifier()}.${identAspect(n)}(${wrapped})`,
|
|
394
|
-
`${baseDefinitions.path.asIdentifier()}.Entity`
|
|
395
|
-
)
|
|
396
|
-
|
|
397
|
-
buffer.add(`export class ${identSingular(clean)} extends ${rhs} {}`)
|
|
398
|
-
//buffer.add(`export type ${clean} = InstanceType<typeof ${identSingular(clean)}>`)
|
|
399
|
-
this.contexts.pop()
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
_printEntity(name, entity) {
|
|
403
|
-
const clean = this._trimNamespace(name)
|
|
404
|
-
const ns = this._resolveNamespace(name.split('.'))
|
|
405
|
-
const file = this.getNamespaceFile(ns)
|
|
406
|
-
// entities are expected to be in plural anyway, so we would favour the regular name.
|
|
407
|
-
// If the user decides to pass a @plural annotation, that gets precedence over the regular name.
|
|
408
|
-
let plural = util.unlocalize(this._trimNamespace(util.getPluralAnnotation(entity) ? util.plural4(entity, false) : name))
|
|
409
|
-
const singular = util.unlocalize(util.singular4(entity, true))
|
|
410
|
-
if (singular === plural) {
|
|
411
|
-
plural += '_'
|
|
412
|
-
this.logger.warning(
|
|
413
|
-
`Derived singular and plural forms for '${singular}' are the same. This usually happens when your CDS entities are named following singular flexion. Consider naming your entities in plural or providing '@singular:'/ '@plural:' annotations to have a clear distinction between the two. Plural form will be renamed to '${plural}' to avoid compilation errors within the output.`
|
|
414
|
-
)
|
|
415
|
-
}
|
|
416
|
-
if (singular in this.csn.definitions) {
|
|
417
|
-
this.logger.error(
|
|
418
|
-
`Derived singular '${singular}' for your entity '${name}', already exists. The resulting types will be erronous. Please consider using '@singular:'/ '@plural:' annotations in your model to resolve this collision.`
|
|
419
|
-
)
|
|
420
|
-
}
|
|
421
|
-
file.addClass(singular, name)
|
|
422
|
-
file.addClass(plural, name)
|
|
423
|
-
|
|
424
|
-
const parent = this._resolveParent(entity.name)
|
|
425
|
-
const buffer =
|
|
426
|
-
parent && parent.kind === 'entity' ? file.getSubNamespace(this._trimNamespace(parent.name)) : file.classes
|
|
427
|
-
|
|
428
|
-
// we can't just use "singular" here, as it may have the subnamespace removed:
|
|
429
|
-
// "Books.text" is just "text" in "singular". Within the inflected exports we need
|
|
430
|
-
// to have Books.texts = Books.text, so we derive the singular once more without cutting off the ns.
|
|
431
|
-
// Directly deriving it from the plural makes sure we retain any parent namespaces of kind "entity",
|
|
432
|
-
// which would not be possible while already in singular form, as "Book.text" could not be resolved in CSN.
|
|
433
|
-
file.addInflection(util.singular4(plural), plural, clean)
|
|
434
|
-
if ('doc' in entity) {
|
|
435
|
-
this._docify(entity.doc).forEach((d) => buffer.add(d))
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
this._aspectify(name, entity, file.classes, singular)
|
|
439
|
-
|
|
440
|
-
// PLURAL
|
|
441
|
-
if (plural.includes('.')) {
|
|
442
|
-
// Foo.text -> namespace Foo { class text { ... }}
|
|
443
|
-
plural = plural.split('.').pop()
|
|
444
|
-
}
|
|
445
|
-
// plural can not be a type alias to $singular[] but needs to be a proper class instead,
|
|
446
|
-
// so it can get passed as value to CQL functions.
|
|
447
|
-
buffer.add(`export class ${plural} extends Array<${singular}> {}`)
|
|
448
|
-
buffer.add('')
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
_printAction(name, action) {
|
|
452
|
-
this.logger.debug(`Printing action ${name}:\n${JSON.stringify(action, null, 2)}`)
|
|
453
|
-
const ns = this._resolveNamespace(name.split('.'))
|
|
454
|
-
const file = this.getNamespaceFile(ns)
|
|
455
|
-
const params = action.params
|
|
456
|
-
? Object.entries(action.params).map(([pname, ptype]) => [
|
|
457
|
-
pname,
|
|
458
|
-
this.resolveAndRequire(ptype, file).typeName,
|
|
459
|
-
])
|
|
460
|
-
: []
|
|
461
|
-
const returns = this.resolveAndRequire(action.returns, file).typeName
|
|
462
|
-
file.addAction(name.split('.').at(-1), params, returns)
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
_printType(name, type) {
|
|
466
|
-
this.logger.debug(`Printing type ${name}:\n${JSON.stringify(type, null, 2)}`)
|
|
467
|
-
const clean = this._trimNamespace(name)
|
|
468
|
-
const ns = this._resolveNamespace(name.split('.'))
|
|
469
|
-
const file = this.getNamespaceFile(ns)
|
|
470
|
-
if ('enum' in type) {
|
|
471
|
-
file.addEnum(
|
|
472
|
-
name,
|
|
473
|
-
clean,
|
|
474
|
-
Object.entries(type.enum).map(([k, v]) => [k, v.val])
|
|
475
|
-
)
|
|
476
|
-
} else {
|
|
477
|
-
// alias
|
|
478
|
-
file.addType(name, clean, this.resolveAndRequire(type, file).typeName)
|
|
479
|
-
}
|
|
480
|
-
// TODO: annotations not handled yet
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
_printAspect(name, aspect) {
|
|
484
|
-
this.logger.debug(`Printing aspect ${name}`)
|
|
485
|
-
const clean = this._trimNamespace(name)
|
|
486
|
-
const ns = this._resolveNamespace(name.split('.'))
|
|
487
|
-
const file = this.getNamespaceFile(ns)
|
|
488
|
-
// aspects are technically classes and can therefore be added to the list of defined classes.
|
|
489
|
-
// Still, when using them as mixins for a class, they need to already be defined.
|
|
490
|
-
// So we separate them into another buffer which is printed before the classes.
|
|
491
|
-
file.addClass(clean, name)
|
|
492
|
-
file.aspects.add(`// the following represents the CDS aspect '${clean}'`)
|
|
493
|
-
this._aspectify(name, aspect, file.aspects, clean)
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
/**
|
|
497
|
-
* Visits a single entity from the CSN's definition field.
|
|
498
|
-
* Will call _printEntity or _printAction based on the entity's kind.
|
|
499
|
-
* @param {string} name name of the entity, fully qualified as is used in the definition field.
|
|
500
|
-
* @param {CSN} entity CSN data belonging to the entity to perform lookups in.
|
|
501
|
-
*/
|
|
502
|
-
visitEntity(name, entity) {
|
|
503
|
-
switch (entity.kind) {
|
|
504
|
-
case 'entity':
|
|
505
|
-
this._printEntity(name, entity)
|
|
506
|
-
break
|
|
507
|
-
case 'action':
|
|
508
|
-
this._printAction(name, entity)
|
|
509
|
-
break
|
|
510
|
-
case 'type':
|
|
511
|
-
this._printType(name, entity)
|
|
512
|
-
break
|
|
513
|
-
case 'aspect':
|
|
514
|
-
this._printAspect(name, entity)
|
|
515
|
-
break
|
|
516
|
-
default:
|
|
517
|
-
this.logger.error(`Unhandled entity kind '${entity.kind}'.`)
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
/**
|
|
522
|
-
* Attempts to retrieve the max cardinality of a CSN for an entity.
|
|
523
|
-
* @param {EntityCSN} element csn of entity to retrieve cardinality for
|
|
524
|
-
* @returns {number} max cardinality of the element.
|
|
525
|
-
* If no cardinality is attached to the element, cardinality is 1.
|
|
526
|
-
* If it is set to '*', result is Infinity.
|
|
527
|
-
*/
|
|
528
|
-
getMaxCardinality(element) {
|
|
529
|
-
const cardinality = element && element.cardinality && element.cardinality.max ? element.cardinality.max : 1
|
|
530
|
-
return cardinality === '*' ? Infinity : parseInt(cardinality)
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
/**
|
|
534
|
-
* Conveniently combines _resolveNamespace and _trimNamespace
|
|
535
|
-
* to end up with both the resolved Path of the namespace,
|
|
536
|
-
* and the clean name of the class.
|
|
537
|
-
* @param {string} fq the fully qualified name of an entity.
|
|
538
|
-
* @returns {[Path, string]} a tuple, [0] holding the path to the namespace, [1] holding the clean name of the entity.
|
|
539
|
-
*/
|
|
540
|
-
untangle(fq) {
|
|
541
|
-
const ns = this._resolveNamespace(fq.split('.'))
|
|
542
|
-
const name = this._trimNamespace(fq)
|
|
543
|
-
return [new Path(ns.split('.')), name]
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
/**
|
|
547
|
-
* A self reference is a property that references the class it appears in.
|
|
548
|
-
* They need to be detected on CDS level, as the emitted TS types will try to
|
|
549
|
-
* refer to refer to types via their alias that hides the aspectification.
|
|
550
|
-
* If we attempt to directly refer to this alias while it has not been fully created,
|
|
551
|
-
* that will result in a TS error.
|
|
552
|
-
* @param {String} entityName
|
|
553
|
-
* @returns {boolean} true, if `entityName` refers to the surrounding class
|
|
554
|
-
* @example
|
|
555
|
-
* ```ts
|
|
556
|
-
* class TreeNode {
|
|
557
|
-
* value: number
|
|
558
|
-
* parent: TreeNode // <- self reference
|
|
559
|
-
* }
|
|
560
|
-
* ```
|
|
561
|
-
*/
|
|
562
|
-
isSelfReference(entityName) {
|
|
563
|
-
return entityName === this.contexts.at(-1)?.entity
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
/**
|
|
567
|
-
* Convenient API to consume resolveType.
|
|
568
|
-
* Internally calls resolveType, determines how it has to be imported,
|
|
569
|
-
* used, etc. relative to file and just returns the name under
|
|
570
|
-
* which it will finally be known within file.
|
|
571
|
-
*
|
|
572
|
-
* For example:
|
|
573
|
-
* model1.cds contains entity Foo
|
|
574
|
-
* model2.cds references Foo
|
|
575
|
-
*
|
|
576
|
-
* calling resolveAndRequire({... Foo}, model2.d.ts) would then:
|
|
577
|
-
* 1. add an import of model1 to model2 with proper path resolution and alias, e.g. "import * as m1 from './model1'"
|
|
578
|
-
* 2. resolve any singular/ plural issues and association/ composition around it
|
|
579
|
-
* 3. return a properly prefixed name to use within model2.d.ts, e.g. "m1.Foo"
|
|
580
|
-
*
|
|
581
|
-
* @param {CSN} element the CSN element to resolve the type for.
|
|
582
|
-
* @param {SourceFile} file source file for context.
|
|
583
|
-
* @returns {{typeName: string, typeInfo: TypeResolveInfo & { inflection: Inflection } }} info about the resolved type
|
|
584
|
-
*/
|
|
585
|
-
resolveAndRequire(element, file) {
|
|
586
|
-
const typeInfo = this.resolveType(element, file)
|
|
587
|
-
const cardinality = this.getMaxCardinality(element)
|
|
588
|
-
|
|
589
|
-
let typeName = typeInfo.plainName ?? typeInfo.type
|
|
590
|
-
|
|
591
|
-
if (typeInfo.isBuiltin === true) {
|
|
592
|
-
const [toOne, toMany] = {
|
|
593
|
-
'Association': [this._createToOneAssociation, this._createToManyAssociation],
|
|
594
|
-
'Composition': [this._createCompositionOfOne, this._createCompositionOfMany]
|
|
595
|
-
}[element.constructor.name] ?? []
|
|
596
|
-
|
|
597
|
-
if (toOne && toMany) {
|
|
598
|
-
const target = typeof element.target === 'string' ? { type: element.target } : element.target
|
|
599
|
-
const { singular, plural } = this.resolveAndRequire(target, file).typeInfo.inflection
|
|
600
|
-
typeName = cardinality > 1
|
|
601
|
-
? toMany.bind(this)(plural)
|
|
602
|
-
: toOne.bind(this)(this.isSelfReference(element.target) ? 'this' : singular)
|
|
603
|
-
file.addImport(baseDefinitions.path)
|
|
604
|
-
}
|
|
605
|
-
} else {
|
|
606
|
-
// TODO: this could go into resolve type
|
|
607
|
-
// resolve and maybe generate an import.
|
|
608
|
-
// Inline declarations don't have a corresponding path, etc., so skip those.
|
|
609
|
-
if (typeInfo.isInlineDeclaration === false) {
|
|
610
|
-
const namespace = this._resolveNamespace(typeInfo.path.parts)
|
|
611
|
-
const parent = new Path(namespace.split('.')) //t.path.getParent()
|
|
612
|
-
typeInfo.inflection = this.inflect(typeInfo, namespace)
|
|
613
|
-
|
|
614
|
-
if (!parent.isCwd(file.path.asDirectory())) {
|
|
615
|
-
file.addImport(parent)
|
|
616
|
-
// prepend namespace
|
|
617
|
-
typeName = `${parent.asIdentifier()}.${typeName}`
|
|
618
|
-
typeInfo.inflection.singular = `${parent.asIdentifier()}.${typeInfo.inflection.singular}`
|
|
619
|
-
typeInfo.inflection.plural = `${parent.asIdentifier()}.${typeInfo.inflection.plural}`
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
if (element.type.ref?.length > 1) {
|
|
623
|
-
const [entity, ...members] = element.type.ref
|
|
624
|
-
const lookup = this.inlineDeclarationResolver.getTypeLookup(members)
|
|
625
|
-
typeName = `__.DeepRequired<${typeInfo.inflection.singular}>${lookup}`
|
|
626
|
-
file.addImport(baseDefinitions.path)
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
// FIXME NOW: inline declarations, aka structs go here!
|
|
630
|
-
|
|
631
|
-
for (const imp of typeInfo.imports ?? []) {
|
|
632
|
-
if (!imp.isCwd(file.path.asDirectory())) {
|
|
633
|
-
file.addImport(imp)
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
if (typeInfo.isInlineDeclaration === true) {
|
|
639
|
-
typeInfo.inflection = this.inflect(typeInfo)
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
return {typeName, typeInfo}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
/**
|
|
646
|
-
* Visits a single element in an entity.
|
|
647
|
-
* @param {string} name name of the element
|
|
648
|
-
* @param {CSN} element CSN data belonging to the the element.
|
|
649
|
-
* @param {SourceFile} file the namespace file the surrounding entity is being printed into.
|
|
650
|
-
* @param {Buffer} buffer buffer to add the definition to. If no buffer is passed, the passed file's class buffer is used instead.
|
|
651
|
-
* @returns @see InlineDeclarationResolver.visitElement
|
|
652
|
-
*/
|
|
653
|
-
visitElement(name, element, file, buffer) {
|
|
654
|
-
return this.inlineDeclarationResolver.visitElement(name, element, file, buffer)
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
/**
|
|
658
|
-
* Resolves the fully qualified name of an entity to its parent entity.
|
|
659
|
-
* _resolveParent(a.b.c.D) -> CSN {a.b.c}
|
|
660
|
-
* @param {string} name fully qualified name of the entity to resolve the parent of.
|
|
661
|
-
* @returns {CSN} the resolved parent CSN.
|
|
662
|
-
*/
|
|
663
|
-
_resolveParent(name) {
|
|
664
|
-
return this.csn.definitions[name.split('.').slice(0, -1).join('.')]
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
/**
|
|
668
|
-
* Resolves a fully qualified identifier to a namespace.
|
|
669
|
-
* In an identifier 'a.b.c.D.E', the namespace is the part of the identifier
|
|
670
|
-
* read from left to right which does not contain a kind 'context' or 'service'.
|
|
671
|
-
* That is, if in the above example 'D' is a context and 'E' is a service,
|
|
672
|
-
* the resulting namespace is 'a.b.c'.
|
|
673
|
-
* @param {string[]} pathParts the distinct parts of the namespace, i.e. ['a','b','c','D','E']
|
|
674
|
-
* @returns {string} the namespace's name, i.e. 'a.b.c'.
|
|
675
|
-
*/
|
|
676
|
-
_resolveNamespace(pathParts) {
|
|
677
|
-
let result
|
|
678
|
-
while (result === undefined) {
|
|
679
|
-
const path = pathParts.join('.')
|
|
680
|
-
const def = this.csn.definitions[path]
|
|
681
|
-
if (!def) {
|
|
682
|
-
result = path
|
|
683
|
-
} else if (['context', 'service'].includes(def.kind)) {
|
|
684
|
-
result = path
|
|
685
|
-
} else {
|
|
686
|
-
pathParts = pathParts.slice(0, -1)
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
return result
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
/**
|
|
693
|
-
* Resolves an element's type to either a builtin or a user defined type.
|
|
694
|
-
* Enriched with additional information for improved printout (see return type).
|
|
695
|
-
* @param {CSN} element the CSN element to resolve the type for.
|
|
696
|
-
* @param {SourceFile} file source file for context.
|
|
697
|
-
* @returns {TypeResolveInfo} description of the resolved type
|
|
698
|
-
*/
|
|
699
|
-
resolveType(element, file) {
|
|
700
|
-
// while resolving inline declarations, it can happen that we land here
|
|
701
|
-
// with an already resolved type. In that case, just return the type we have.
|
|
702
|
-
if (element && Object.hasOwn(element, 'isBuiltin')) { return element }
|
|
703
|
-
|
|
704
|
-
const result = {
|
|
705
|
-
isBuiltin: false, // will be rectified in the corresponding handlers, if needed
|
|
706
|
-
isInlineDeclaration: false,
|
|
707
|
-
isForeignKeyReference: false,
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
// FIXME: switch case
|
|
711
|
-
if (element?.type === undefined) {
|
|
712
|
-
// "fallback" type "empty object". May be overriden via _resolveInlineDeclarationType
|
|
713
|
-
// later on with an inline declaration
|
|
714
|
-
result.type = '{}'
|
|
715
|
-
result.isInlineDeclaration = true
|
|
716
|
-
} else {
|
|
717
|
-
this._resolvePotentialReferenceType(element.type, result, file)
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
// objects and arrays
|
|
721
|
-
if (element?.items) {
|
|
722
|
-
this._resolveInlineDeclarationType(element.items, result, file)
|
|
723
|
-
} else if (element?.elements) {
|
|
724
|
-
this._resolveInlineDeclarationType(element.elements, result, file)
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
if (result.isBuiltin === false && result.isInlineDeclaration === false && !result.plainName) {
|
|
728
|
-
this.logger.warning(`Plain name is empty for ${element?.type ?? '<empty>'}. This will probably cause issues.`)
|
|
729
|
-
}
|
|
730
|
-
return result
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
/**
|
|
735
|
-
* Resolves an inline declaration of a type.
|
|
736
|
-
* We can encounter declarations like:
|
|
737
|
-
*
|
|
738
|
-
* record : array of {
|
|
739
|
-
* column : String;
|
|
740
|
-
* data : String;
|
|
741
|
-
* }
|
|
742
|
-
*
|
|
743
|
-
* These have to be resolved to a new type.
|
|
744
|
-
*
|
|
745
|
-
* @param {any[]} items the properties of the inline declaration.
|
|
746
|
-
* @param {TypeResolveInfo} into @see resolveType()
|
|
747
|
-
* @param {SourceFile} relativeToindent the sourcefile in which we have found the reference to the type.
|
|
748
|
-
* This is important to correctly detect when a field in the inline declaration is referencing
|
|
749
|
-
* types from the CWD. In that case, we will not add an import for that type and not add a namespace-prefix.
|
|
750
|
-
*/
|
|
751
|
-
_resolveInlineDeclarationType(items, into, relativeTo) {
|
|
752
|
-
return this.inlineDeclarationResolver.resolveInlineDeclaration(items, into, relativeTo)
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
/**
|
|
756
|
-
* Attempts to resolve a type that could reference another type.
|
|
757
|
-
* @param {?} val
|
|
758
|
-
* @param {TypeResolveInfo} into see resolveType()
|
|
759
|
-
* @param {SourceFile} file only needed as we may call _resolveInlineDeclarationType from here. Will be expelled at some point.
|
|
760
|
-
*/
|
|
761
|
-
_resolvePotentialReferenceType(val, into, file) {
|
|
762
|
-
// FIXME: get rid of file parameter! it is only used to pass to _resolveInlineDeclarationType
|
|
763
|
-
if (val.elements) {
|
|
764
|
-
this._resolveInlineDeclarationType(val, into, file) // FIXME INDENT!
|
|
765
|
-
} else if (val.constructor === Object && 'ref' in val) {
|
|
766
|
-
this._resolveTypeName(val.ref[0], into)
|
|
767
|
-
into.isForeignKeyReference = true
|
|
768
|
-
} else {
|
|
769
|
-
// val is string
|
|
770
|
-
this._resolveTypeName(val, into)
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
/**
|
|
775
|
-
* Convenience method to shave off the namespace of a fully qualified path.
|
|
776
|
-
* More specifically, only the parts (reading from right to left) that are of
|
|
777
|
-
* kind "entity" are retained.
|
|
778
|
-
* a.b.c.Foo -> Foo
|
|
779
|
-
* Bar -> Bar
|
|
780
|
-
* sap.cap.Book.text -> Book.text (assuming Book and text are both of kind "entity")
|
|
781
|
-
* @param {string} p path
|
|
782
|
-
* @returns {string} the entity name without leading namespace.
|
|
783
|
-
*/
|
|
784
|
-
_trimNamespace(p) {
|
|
785
|
-
// start on right side, go up while we have an entity at hand
|
|
786
|
-
// we cant start on left side, as that clashes with undefined entities like "sap"
|
|
787
|
-
const parts = p.split('.')
|
|
788
|
-
if (parts.length <= 1) {
|
|
789
|
-
return p
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
let qualifier = parts.join('.')
|
|
793
|
-
while (
|
|
794
|
-
this.csn.definitions[qualifier] &&
|
|
795
|
-
['entity', 'type', 'aspect'].includes(this.csn.definitions[qualifier].kind)
|
|
796
|
-
) {
|
|
797
|
-
parts.pop()
|
|
798
|
-
qualifier = parts.join('.')
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
return qualifier ? p.substring(qualifier.length + 1) : p
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
/**
|
|
805
|
-
* Attempts to resolve a string to a type.
|
|
806
|
-
* String is supposed to refer to either a builtin type
|
|
807
|
-
* or any type defined in CSN.
|
|
808
|
-
* @param {string} t fully qualified type, like cds.String, or a.b.c.d.Foo
|
|
809
|
-
* @param {TypeResolveInfo} into optional dictionary to fill by reference, see resolveType()
|
|
810
|
-
* @returns @see resolveType
|
|
811
|
-
*/
|
|
812
|
-
_resolveTypeName(t, into) {
|
|
813
|
-
const result = into || {}
|
|
814
|
-
const path = t.split('.')
|
|
815
|
-
if (path.length === 2 && path[0] === 'cds') {
|
|
816
|
-
// builtin type
|
|
817
|
-
const resolvedBuiltin = Builtins[path[1]]
|
|
818
|
-
if (!resolvedBuiltin) {
|
|
819
|
-
throw new Error(`Can not resolve apparent builtin type '${t}' to any CDS type.`)
|
|
820
|
-
}
|
|
821
|
-
result.type = resolvedBuiltin
|
|
822
|
-
result.isBuiltin = true
|
|
823
|
-
} else if (t in this.csn.definitions) {
|
|
824
|
-
// user-defined type
|
|
825
|
-
result.type = this._trimNamespace(util.singular4(this.csn.definitions[t])) //(path[path.length - 1])
|
|
826
|
-
result.isBuiltin = false
|
|
827
|
-
result.path = new Path(path) // FIXME: relative to current file
|
|
828
|
-
result.csn = this.csn.definitions[t]
|
|
829
|
-
result.plainName = this._trimNamespace(t)
|
|
830
|
-
} else {
|
|
831
|
-
// type offered by some library
|
|
832
|
-
const lib = this.libraries.find(lib => lib.offers(t))
|
|
833
|
-
if (lib) {
|
|
834
|
-
// only use the last name of the (fully qualified) type name in this case.
|
|
835
|
-
// We can not use _trimNamespace, as that actually does a semantic lookup within the CSN.
|
|
836
|
-
// But entities that are found in libraries are not part of that CSN and have therefore be
|
|
837
|
-
// separated from their namespace in a more barbarian way.
|
|
838
|
-
// Luckily, this is not an issue, as libraries are supposed to be flat. So we can assume the
|
|
839
|
-
// last portion of the type to refer to the entity.
|
|
840
|
-
// We use this plain name as type name because consider:
|
|
841
|
-
//
|
|
842
|
-
// ```cds
|
|
843
|
-
// entity Book { title: hana.VARCHAR }
|
|
844
|
-
// ```
|
|
845
|
-
//
|
|
846
|
-
// ```ts
|
|
847
|
-
// import * as _cds_hana from '../../cds/hana'
|
|
848
|
-
// class Book { title: _cds_hana.cds.hana.VARCHAR } // <- how it would be without discarding the namespace
|
|
849
|
-
// class Book { title: _cds_hana.VARCHAR } // <- how we want it to look
|
|
850
|
-
// ```
|
|
851
|
-
const plain = t.split('.').at(-1)
|
|
852
|
-
lib.referenced = true
|
|
853
|
-
result.type = plain
|
|
854
|
-
result.isBuiltin = false
|
|
855
|
-
result.path = lib.path
|
|
856
|
-
result.csn = { name: t }
|
|
857
|
-
result.plainName = plain
|
|
858
|
-
} else {
|
|
859
|
-
throw new Error(`Can not resolve '${t}' to any builtin, library-, or user defined type.`)
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
return result
|
|
864
|
-
}
|
|
865
|
-
}
|
|
9
|
+
const { Visitor } = require('./visitor')
|
|
866
10
|
|
|
867
11
|
/**
|
|
868
12
|
* Writes the accompanying jsconfig.json file to the specified paths.
|
|
@@ -920,7 +64,6 @@ const compileFromCSN = async (csn, parameters) => {
|
|
|
920
64
|
}
|
|
921
65
|
|
|
922
66
|
module.exports = {
|
|
923
|
-
Visitor,
|
|
924
67
|
compileFromFile,
|
|
925
68
|
compileFromCSN,
|
|
926
69
|
}
|