@cap-js/cds-typer 0.2.5-beta.1
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 +43 -0
- package/LICENSE +201 -0
- package/README.md +192 -0
- package/index.js +5 -0
- package/lib/cli.js +100 -0
- package/lib/compile.d.ts +273 -0
- package/lib/compile.js +926 -0
- package/lib/components/inline.js +217 -0
- package/lib/file.d.ts +208 -0
- package/lib/file.js +497 -0
- package/lib/logging.d.ts +50 -0
- package/lib/logging.js +73 -0
- package/lib/util.d.ts +87 -0
- package/lib/util.js +196 -0
- package/library/cds.hana.ts +15 -0
- package/package.json +50 -0
package/lib/file.js
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs').promises
|
|
4
|
+
const { readFileSync } = require('fs')
|
|
5
|
+
const path = require('path')
|
|
6
|
+
|
|
7
|
+
const AUTO_GEN_NOTE = "// This is an automatically generated file. Please do not change its contents manually!"
|
|
8
|
+
|
|
9
|
+
/** @typedef {Object<string, Buffer>} Namespace */
|
|
10
|
+
|
|
11
|
+
class File {
|
|
12
|
+
/**
|
|
13
|
+
* Creates one string from the buffers representing the type definitions.
|
|
14
|
+
* @returns {string} complete file contents.
|
|
15
|
+
*/
|
|
16
|
+
toTypeDefs() { return '' }
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Concats the classnames to an export dictionary
|
|
20
|
+
* to create the accompanying JS file for the typings.
|
|
21
|
+
* @returns {string} a string containing the module.exports for the JS file.
|
|
22
|
+
*/
|
|
23
|
+
toJSExports() { return '' }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Library files that contain predefined types.
|
|
28
|
+
* For example, the cap.hana types are included in a separate predefined library file
|
|
29
|
+
* which is only included if the model that is being compiled references any of these types.
|
|
30
|
+
* A library is uniquely identified by the namespace it represents. That namespace is directly
|
|
31
|
+
* derived from the file's name. i.e. path/to/file/cap.hana.ts will be considered to hold
|
|
32
|
+
* definitions describing the namespace "cap.hana".
|
|
33
|
+
* These files are supposed to contain fully usable types, not CSN or a CDS file, as they are just
|
|
34
|
+
* being copied verbatim when they are being used.
|
|
35
|
+
*/
|
|
36
|
+
class Library extends File {
|
|
37
|
+
toTypeDefs() {
|
|
38
|
+
return this.contents
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
constructor(file) {
|
|
42
|
+
super()
|
|
43
|
+
this.contents = readFileSync(file, 'utf-8')
|
|
44
|
+
/**
|
|
45
|
+
* The path to the file where the lib definitions are stored (some/path/name.space.ts)
|
|
46
|
+
* @type {string}
|
|
47
|
+
* @private
|
|
48
|
+
*/
|
|
49
|
+
this.file = file
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* List of entity names (plain, without namespace)
|
|
53
|
+
* @type {string[]}
|
|
54
|
+
*/
|
|
55
|
+
this.entities = Array.from(this.contents.matchAll(/export class (\w+)/g), ([,m]) => m)
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Namespace (a.b.c.d)
|
|
59
|
+
* @type {string}
|
|
60
|
+
*/
|
|
61
|
+
this.namespace = path.basename(file, '.ts')
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Whether this library was referenced at least once
|
|
65
|
+
* @type {boolean}
|
|
66
|
+
*/
|
|
67
|
+
this.referenced = false
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The Path for this library file, which is constructed from its namespace.
|
|
71
|
+
* @type {Path}
|
|
72
|
+
*/
|
|
73
|
+
this.path = new Path(this.namespace.split('.'))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Whether this library offers an entity of a given type (fully qualified).
|
|
78
|
+
* @param {string} entity the entity's name, e.g. cap.hana.TINYINT
|
|
79
|
+
* @returns {boolean} true, iff the namespace inferred from the passed string matches that of this library
|
|
80
|
+
* and this library contains a class of that name. i.e.:
|
|
81
|
+
* ```js
|
|
82
|
+
* new Library('cap.hana.ts').offers('cap.hana.TINYINT') // -> true`
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
offers(entity) {
|
|
86
|
+
return this.entities.some(e => `${this.namespace}.${e}` === entity)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Source file containing several buffers.
|
|
92
|
+
*/
|
|
93
|
+
class SourceFile extends File {
|
|
94
|
+
constructor(path) {
|
|
95
|
+
super()
|
|
96
|
+
/** @type {Path} */
|
|
97
|
+
this.path = new Path(path.split('.'))
|
|
98
|
+
/** @type {Object} */
|
|
99
|
+
this.imports = {}
|
|
100
|
+
/** @type {Buffer} */
|
|
101
|
+
this.preamble = new Buffer()
|
|
102
|
+
/** @type {Buffer} */
|
|
103
|
+
this.types = new Buffer()
|
|
104
|
+
/** @type {Buffer} */
|
|
105
|
+
this.enums = new Buffer()
|
|
106
|
+
/** @type {Buffer} */
|
|
107
|
+
this.classes = new Buffer()
|
|
108
|
+
/** @type {{ buffer: Buffer, names: string[]} */
|
|
109
|
+
this.actions = { buffer: new Buffer(), names: [] }
|
|
110
|
+
/** @type {Buffer} */
|
|
111
|
+
this.aspects = new Buffer()
|
|
112
|
+
/** @type {Namespace} */
|
|
113
|
+
this.namespaces = {}
|
|
114
|
+
/** @type {Object<string, any>} */
|
|
115
|
+
this.classNames = {} // for .js file
|
|
116
|
+
/** @type {Object<string, any>} */
|
|
117
|
+
this.typeNames = {}
|
|
118
|
+
/** @type {[string, string, string][]} */
|
|
119
|
+
this.inflections = []
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
static stringifyLambda(name, parameters = [], returns = 'any') {
|
|
123
|
+
return `${name}: (${parameters.map(([n, t]) => `${n}: ${t}`).join(', ')}) => ${returns}`
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Adds a pair of singular and plural inflection.
|
|
128
|
+
* These are later used to generate the singular -> plural
|
|
129
|
+
* aliases in the index.js file.
|
|
130
|
+
* @param {string} singular singular type without namespace.
|
|
131
|
+
* @param {string} plural plural type without namespace
|
|
132
|
+
* @param {string} original original entity name without namespace.
|
|
133
|
+
* In many cases this will be the same as plural.
|
|
134
|
+
*/
|
|
135
|
+
addInflection(singular, plural, original) {
|
|
136
|
+
this.inflections.push([singular, plural, original])
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Adds an action definition in form of a arrow function to the file.
|
|
141
|
+
* @param {string} name name of the action
|
|
142
|
+
* @param {{relative: string | undefined, local: boolean, posix: boolean}} params list of parameters, passed as [name, type] pairs
|
|
143
|
+
* @param returns the return type of the action
|
|
144
|
+
*/
|
|
145
|
+
addAction(name, params, returns) {
|
|
146
|
+
//const ps = params.map(([n, t]) => `${n}: ${t}`).join(', ')
|
|
147
|
+
this.actions.buffer.add("// action")
|
|
148
|
+
//this.actions.buffer.add(`export declare const ${name}: ( args: { ${ps} }) => ${returns};`)
|
|
149
|
+
this.actions.buffer.add(`export declare const ${SourceFile.stringifyLambda(name, params, returns)};`)
|
|
150
|
+
this.actions.names.push(name)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Retrieves or creates and retrieves a sub namespace
|
|
155
|
+
* with a given name.
|
|
156
|
+
* @param {string} name of the sub namespace.
|
|
157
|
+
* @returns {Namespace} the sub namespace.
|
|
158
|
+
*/
|
|
159
|
+
getSubNamespace(name) {
|
|
160
|
+
if (!(name in this.namespaces)) {
|
|
161
|
+
const buffer = new Buffer()
|
|
162
|
+
buffer.closed = false
|
|
163
|
+
buffer.add(`export namespace ${name} {`)
|
|
164
|
+
buffer.indent()
|
|
165
|
+
this.namespaces[name] = buffer
|
|
166
|
+
}
|
|
167
|
+
const buffer = this.namespaces[name]
|
|
168
|
+
if (buffer.closed) {
|
|
169
|
+
throw new Error(`Tried to add content to namespace buffer '${name}' that was already closed.`)
|
|
170
|
+
}
|
|
171
|
+
return this.namespaces[name]
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Adds an enum to this file.
|
|
176
|
+
* @param {string} fq fully qualified name of the enum
|
|
177
|
+
* @param {string} name local name of the enum
|
|
178
|
+
* @param {[string, string][]} kvs list of key-value pairs
|
|
179
|
+
*/
|
|
180
|
+
addEnum(fq, name, kvs) {
|
|
181
|
+
this.enums.add(`export enum ${name} {`)
|
|
182
|
+
this.enums.indent()
|
|
183
|
+
for (const [k, v] of kvs) {
|
|
184
|
+
this.enums.add(`${k} = ${v},`)
|
|
185
|
+
}
|
|
186
|
+
this.enums.outdent()
|
|
187
|
+
this.enums.add('}')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Adds a class to this file.
|
|
192
|
+
* This differs from writing to the classes buffer,
|
|
193
|
+
* as it is just a cache to collect all classes that
|
|
194
|
+
* are supposed to be present in this file.
|
|
195
|
+
* @param {string} clean cleaned name of the class
|
|
196
|
+
* @param {string} fq fully qualified name, including the namespace
|
|
197
|
+
*/
|
|
198
|
+
addClass(clean, fq) {
|
|
199
|
+
this.classNames[clean] = fq
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Adds an import if it does not exist yet.
|
|
204
|
+
* @param {Path} imp qualifier for the namespace to import.
|
|
205
|
+
*/
|
|
206
|
+
addImport(imp) {
|
|
207
|
+
const dir = imp.asDirectory({relative: this.path.asDirectory()})
|
|
208
|
+
if (!(dir in this.imports)) {
|
|
209
|
+
this.imports[dir] = imp
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Adds an arbitrary piece of code that is added
|
|
215
|
+
* right after the imports.
|
|
216
|
+
* @param {string} code the preamble code.
|
|
217
|
+
*/
|
|
218
|
+
addPreamble(code) {
|
|
219
|
+
this.preamble.add(code)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Adds a type alias to this file.
|
|
224
|
+
* @param {string} fq fully qualified name of the enum
|
|
225
|
+
* @param {string} name local name of the enum
|
|
226
|
+
* @param {string} rhs the right hand side of the assignment
|
|
227
|
+
*/
|
|
228
|
+
addType(fq, clean, rhs) {
|
|
229
|
+
this.typeNames[clean] = fq
|
|
230
|
+
this.types.add(`export type ${clean} = ${rhs};`)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Writes all imports to a buffer, relative to the current file.
|
|
235
|
+
* Creates a new buffer on each call, as concatenating import strings directly
|
|
236
|
+
* upon discovering them would complicate filtering out duplicate entries.
|
|
237
|
+
* @returns {Buffer} all imports written to a buffer.
|
|
238
|
+
*/
|
|
239
|
+
getImports() {
|
|
240
|
+
const buffer = new Buffer()
|
|
241
|
+
for (const imp of Object.values(this.imports)) {
|
|
242
|
+
if (!imp.isCwd(this.path.asDirectory())) {
|
|
243
|
+
buffer.add(`import * as ${imp.asIdentifier()} from '${imp.asDirectory({relative: this.path.asDirectory()})}';`)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return buffer
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
toTypeDefs() {
|
|
250
|
+
const namespaces = new Buffer()
|
|
251
|
+
for (const ns of Object.values(this.namespaces)) {
|
|
252
|
+
ns.outdent()
|
|
253
|
+
ns.add('}')
|
|
254
|
+
ns.closed = true
|
|
255
|
+
namespaces.add(ns.join())
|
|
256
|
+
}
|
|
257
|
+
return [
|
|
258
|
+
AUTO_GEN_NOTE,
|
|
259
|
+
this.getImports().join(),
|
|
260
|
+
this.preamble.join(),
|
|
261
|
+
this.types.join(),
|
|
262
|
+
this.enums.join(),
|
|
263
|
+
namespaces.join(),
|
|
264
|
+
this.aspects.join(), // needs to be before classes
|
|
265
|
+
this.classes.join(),
|
|
266
|
+
this.actions.buffer.join(),
|
|
267
|
+
].filter(Boolean).join('\n')
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
toJSExports() {
|
|
271
|
+
return [AUTO_GEN_NOTE, "const cds = require('@sap/cds')", `const csn = cds.entities('${this.path.asNamespace()}')`] // boilerplate
|
|
272
|
+
.concat(
|
|
273
|
+
this.inflections
|
|
274
|
+
// sorting the entries based on the number of dots in their singular.
|
|
275
|
+
// that makes sure we have defined all parent namespaces before adding subclasses to them e.g.:
|
|
276
|
+
// "module.exports.Books" is defined before "module.exports.Books.text"
|
|
277
|
+
.sort(([a], [b]) => a.split('.').length - b.split('.').length)
|
|
278
|
+
// by using a temporary Set we make sure to catch cases where
|
|
279
|
+
// (1) plural is the same as original (default case) and
|
|
280
|
+
// (2) plural differs from original, i.e. when a @plural annotation is present
|
|
281
|
+
// or when plural4 produced weird inflection.
|
|
282
|
+
.flatMap(([singular, plural, original]) => Array.from(new Set([
|
|
283
|
+
`module.exports.${singular} = csn.${original}`,
|
|
284
|
+
`module.exports.${plural} = csn.${original}`,
|
|
285
|
+
`module.exports.${original} = csn.${original}`
|
|
286
|
+
])))
|
|
287
|
+
) // singular -> plural aliases
|
|
288
|
+
.concat(['// actions'])
|
|
289
|
+
.concat(this.actions.names.map(name => `module.exports.${name} = '${name}'`))
|
|
290
|
+
.join('\n') + '\n'
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* String buffer to conveniently append strings to.
|
|
296
|
+
*/
|
|
297
|
+
class Buffer {
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* @param {string} indentation
|
|
301
|
+
*/
|
|
302
|
+
constructor(indentation = ' ') {
|
|
303
|
+
/**
|
|
304
|
+
* @type {string[]}
|
|
305
|
+
*/
|
|
306
|
+
this.parts = []
|
|
307
|
+
/**
|
|
308
|
+
* @type {string}
|
|
309
|
+
*/
|
|
310
|
+
this.indentation = indentation
|
|
311
|
+
/**
|
|
312
|
+
* @type {string}
|
|
313
|
+
*/
|
|
314
|
+
this.currentIndent = ''
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Indents by the predefined spacing.
|
|
319
|
+
*/
|
|
320
|
+
indent() {
|
|
321
|
+
this.currentIndent += this.indentation
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Removes one level of indentation.
|
|
326
|
+
*/
|
|
327
|
+
outdent() {
|
|
328
|
+
if (this.currentIndent.length === 0) {
|
|
329
|
+
throw new Error('Can not outdent buffer further. Probably mismatched indent.')
|
|
330
|
+
}
|
|
331
|
+
this.currentIndent = this.currentIndent.slice(0, -this.indentation.length)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Concats all elements in the buffer into a single string.
|
|
336
|
+
* @param {string} glue string to intersperse all buffer contents with
|
|
337
|
+
* @returns {string} string spilled buffer contents.
|
|
338
|
+
*/
|
|
339
|
+
join(glue = '\n') {
|
|
340
|
+
return this.parts.join(glue)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Clears the buffer.
|
|
345
|
+
*/
|
|
346
|
+
clear() {
|
|
347
|
+
this.parts = []
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Adds an element to the buffer with the current indentation level.
|
|
352
|
+
* @param {string} part
|
|
353
|
+
*/
|
|
354
|
+
add(part) {
|
|
355
|
+
this.parts.push(this.currentIndent + part)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Convenience class to handle path qualifiers.
|
|
361
|
+
*/
|
|
362
|
+
class Path {
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* @param {string[]} parts parts of the path. 'a.b.c' -> ['a', 'b', 'c']
|
|
366
|
+
* @param kind FIXME: currently unused
|
|
367
|
+
*/
|
|
368
|
+
constructor(parts, kind) {
|
|
369
|
+
this.parts = parts
|
|
370
|
+
this.kind = kind
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* @returns {Path} the path to the parent directory. 'a.b.c'.getParent() -> 'a.b'
|
|
375
|
+
*/
|
|
376
|
+
getParent() {
|
|
377
|
+
return new Path(this.parts.slice(0, -1))
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Transfoms the Path into a directory path.
|
|
382
|
+
* @param {string?} params.relative if defined, the path is constructed relative to this directory
|
|
383
|
+
* @param {boolean} params.local if set to true, './' is prefixed to the directory
|
|
384
|
+
* @param {boolean} params.posix if set to true, all slashes will be forward slashes on every OS. Useful for require/ import
|
|
385
|
+
* @returns {string} directory 'a.b.c'.asDirectory() -> 'a/b/c' (or a\b\c when on Windows without passing posix = true)
|
|
386
|
+
*/
|
|
387
|
+
asDirectory(params = {}) {
|
|
388
|
+
const { relative, local, posix } = {relative: undefined, local: true, posix: true, ...params}
|
|
389
|
+
const sep = posix ? path.posix.sep : path.sep
|
|
390
|
+
const prefix = local ? `.${sep}` : ''
|
|
391
|
+
const absolute = path.join(...this.parts)
|
|
392
|
+
let p = relative ? path.relative(relative, absolute) : absolute
|
|
393
|
+
if (posix) {
|
|
394
|
+
// NOTE: this could fail for absolute paths (D:\\...) or network drives on windows
|
|
395
|
+
p = p.split(path.sep).join(path.posix.sep)
|
|
396
|
+
}
|
|
397
|
+
// path.join removes leading ./, so we have to concat manually here
|
|
398
|
+
return prefix + p
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Transforms the Path into a namespace qualifier.
|
|
403
|
+
* @returns {string} namespace qualifier 'a.b.c'.asNamespace() -> 'a.b.c'
|
|
404
|
+
*/
|
|
405
|
+
asNamespace() {
|
|
406
|
+
return this.parts.join('.')
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Transforms the Path into an identifier that can be used as variable name.
|
|
411
|
+
* @returns {string} identifier 'a.b.c'.asIdentifier() -> '_a_b_c', ''.asIdentifier() -> '_'
|
|
412
|
+
*/
|
|
413
|
+
asIdentifier() {
|
|
414
|
+
return `_${this.parts.join('_')}`
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* @returns {boolean} true, iff the Path refers to the current working directory, aka './'
|
|
419
|
+
*/
|
|
420
|
+
isCwd(relative = undefined) {
|
|
421
|
+
return (!relative && this.parts.length === 0) || (!!relative && this.asDirectory({relative}) === './')
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Base definitions used throughout the typing process,
|
|
427
|
+
* such as Associations and Compositions.
|
|
428
|
+
* @type {SourceFile}
|
|
429
|
+
*/
|
|
430
|
+
const baseDefinitions = new SourceFile('_')
|
|
431
|
+
baseDefinitions.addPreamble(`
|
|
432
|
+
export namespace Association {
|
|
433
|
+
export type to <T> = T;
|
|
434
|
+
export namespace to {
|
|
435
|
+
export type many <T extends readonly any[]> = T;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export namespace Composition {
|
|
440
|
+
export type of <T> = T;
|
|
441
|
+
export namespace of {
|
|
442
|
+
export type many <T extends readonly any[]> = T;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export class Entity {
|
|
447
|
+
static data<T extends Entity> (this:T, input:Object) : T {
|
|
448
|
+
return {} as T // mock
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export type EntitySet<T> = T[] & {
|
|
453
|
+
data (input:object[]) : T[]
|
|
454
|
+
data (input:object) : T
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
export type DeepRequired<T> = {
|
|
458
|
+
[K in keyof T]: DeepRequired<T[K]>
|
|
459
|
+
} & Required<T>;
|
|
460
|
+
`)
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Writes the files to disk. For each source, a index.d.ts holding the type definitions
|
|
464
|
+
* and a index.js holding implementation stubs is generated at the appropriate directory.
|
|
465
|
+
* Missing directories are created automatically and asynchronously.
|
|
466
|
+
* @param {string} root root directory to prefix all directories with
|
|
467
|
+
* @param {File[]} sources source files to write to disk
|
|
468
|
+
* @returns {Promise<string[]>} Promise that resolves to a list of all directory paths pointing to generated files.
|
|
469
|
+
*/
|
|
470
|
+
const writeout = async (root, sources) =>
|
|
471
|
+
Promise.all(
|
|
472
|
+
sources.map(async (source) => {
|
|
473
|
+
const dir = path.join(root, source.path.asDirectory({local: false, posix: false}))
|
|
474
|
+
try {
|
|
475
|
+
await fs.mkdir(dir, { recursive: true })
|
|
476
|
+
await Promise.all([
|
|
477
|
+
fs.writeFile(path.join(dir, 'index.ts'), source.toTypeDefs()),
|
|
478
|
+
fs.writeFile(path.join(dir, 'index.js'), source.toJSExports()),
|
|
479
|
+
])
|
|
480
|
+
|
|
481
|
+
} catch (err) {
|
|
482
|
+
// eslint-disable-next-line no-console
|
|
483
|
+
console.error(`Could not create parent directory ${dir}: ${err}.`)
|
|
484
|
+
}
|
|
485
|
+
return dir
|
|
486
|
+
})
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
module.exports = {
|
|
490
|
+
Library,
|
|
491
|
+
Buffer,
|
|
492
|
+
File,
|
|
493
|
+
SourceFile,
|
|
494
|
+
Path,
|
|
495
|
+
writeout,
|
|
496
|
+
baseDefinitions,
|
|
497
|
+
}
|
package/lib/logging.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export enum Levels {
|
|
2
|
+
TRACE = 1,
|
|
3
|
+
DEBUG = 2,
|
|
4
|
+
INFO = 3,
|
|
5
|
+
WARNING = 4,
|
|
6
|
+
ERROR = 8,
|
|
7
|
+
CRITICAL = 16,
|
|
8
|
+
NONE = 32
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class Logger {
|
|
12
|
+
private mask: number;
|
|
13
|
+
|
|
14
|
+
public constructor();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Add all log levels starting at level.
|
|
18
|
+
* @param level level to start from.
|
|
19
|
+
*/
|
|
20
|
+
public addFrom(level: number): void;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Adds a log level to react to.
|
|
24
|
+
* @param level the level to react to.
|
|
25
|
+
*/
|
|
26
|
+
public add(level: number): void;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Ignores a log level.
|
|
30
|
+
* @param level the level to ignore.
|
|
31
|
+
*/
|
|
32
|
+
public ignore(level: number): void;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Attempts to log a message.
|
|
36
|
+
* Only iff levelName is a valid log level
|
|
37
|
+
* and the corresponding number if part of mask,
|
|
38
|
+
* the message gets logged.
|
|
39
|
+
* @param levelName name of the log level.
|
|
40
|
+
* @param message message to log.
|
|
41
|
+
*/
|
|
42
|
+
private _log(levelName: Levels, message: string);
|
|
43
|
+
|
|
44
|
+
public trace(message: string);
|
|
45
|
+
public debug(message: string);
|
|
46
|
+
public info(message: string);
|
|
47
|
+
public warning(message: string);
|
|
48
|
+
public error(message: string);
|
|
49
|
+
public critical(message: string);
|
|
50
|
+
}
|
package/lib/logging.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** @enum {number} */
|
|
2
|
+
const Levels = {
|
|
3
|
+
TRACE: 1,
|
|
4
|
+
DEBUG: 2,
|
|
5
|
+
INFO: 3,
|
|
6
|
+
WARNING: 4,
|
|
7
|
+
ERROR: 8,
|
|
8
|
+
CRITICAL: 16,
|
|
9
|
+
NONE: 32,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class Logger {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.mask = 0
|
|
15
|
+
const lvls = Object.keys(Levels)
|
|
16
|
+
for (let i = 0; i < lvls.length - 1; i++) {
|
|
17
|
+
// -1 to ignore NONE
|
|
18
|
+
const level = lvls[i]
|
|
19
|
+
this[level.toLowerCase()] = function (message) {
|
|
20
|
+
this._log(level, message)
|
|
21
|
+
}.bind(this)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Add all log levels starting at level.
|
|
27
|
+
* @param {number} level level to start from.
|
|
28
|
+
*/
|
|
29
|
+
addFrom(baseLevel) {
|
|
30
|
+
const vals = Object.values(Levels)
|
|
31
|
+
const highest = vals[vals.length - 1]
|
|
32
|
+
for (let l = Math.log2(baseLevel); Math.pow(2, l) <= highest; l++) {
|
|
33
|
+
this.add(Math.pow(2, l))
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Adds a log level to react to.
|
|
39
|
+
* @param {number} level the level to react to.
|
|
40
|
+
*/
|
|
41
|
+
add(level) {
|
|
42
|
+
this.mask = this.mask | level
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Ignores a log level.
|
|
47
|
+
* @param {number} level the level to ignore.
|
|
48
|
+
*/
|
|
49
|
+
ignore(level) {
|
|
50
|
+
this.mask = this.mask ^ level
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Attempts to log a message.
|
|
55
|
+
* Only iff levelName is a valid log level
|
|
56
|
+
* and the corresponding number if part of mask,
|
|
57
|
+
* the message gets logged.
|
|
58
|
+
* @param {Levels} levelName name of the log level.
|
|
59
|
+
* @param {string} message message to log.
|
|
60
|
+
*/
|
|
61
|
+
_log(levelName, message) {
|
|
62
|
+
const level = Levels[levelName]
|
|
63
|
+
if (level && (this.mask & level) === level) {
|
|
64
|
+
// eslint-disable-next-line no-console
|
|
65
|
+
console.log(`[${levelName}]`, message)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = {
|
|
71
|
+
Levels,
|
|
72
|
+
Logger,
|
|
73
|
+
}
|
package/lib/util.d.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
interface Annotations {
|
|
2
|
+
name?: string,
|
|
3
|
+
'@singular'?: string,
|
|
4
|
+
'@plural'?: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface CommandlineFlag {
|
|
8
|
+
desc: string,
|
|
9
|
+
default?: any
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ParsedFlags {
|
|
13
|
+
positional: string[],
|
|
14
|
+
named: {[key: string]: any}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Tries to retrieve an annotation that specifies the singular name
|
|
19
|
+
* from a CSN. Valid annotations are listed in util.annotations
|
|
20
|
+
* and their precedence is in order of definition.
|
|
21
|
+
* If no singular is specified at all, undefined is returned.
|
|
22
|
+
* @param csn the CSN of an entity to check
|
|
23
|
+
* @returns the singular annotation or undefined
|
|
24
|
+
*/
|
|
25
|
+
export function getSingularAnnotation(csn: {}): string | undefined;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Tries to retrieve an annotation that specifies the plural name
|
|
29
|
+
* from a CSN. Valid annotations are listed in util.annotations
|
|
30
|
+
* and their precedence is in order of definition.
|
|
31
|
+
* If no plural is specified at all, undefined is returned.
|
|
32
|
+
* @param csn the CSN of an entity to check
|
|
33
|
+
* @returns the plural annotation or undefined
|
|
34
|
+
*/
|
|
35
|
+
export function getPluralAnnotation(csn: {}): string | undefined;
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Users can specify that they want to refer to localisation
|
|
40
|
+
* using the syntax {i18n>Foo}, where Foo is the name of the
|
|
41
|
+
* entity as found in the .cds file
|
|
42
|
+
* (see: https://pages.github.tools.sap/cap/docs/guides/i18n)
|
|
43
|
+
* As this throws off the naming, we remove this wrapper
|
|
44
|
+
* unlocalize("{i18n>Foo}") -> "Foo"
|
|
45
|
+
* @param name the entity name (singular or plural).
|
|
46
|
+
* @returns the name without localisation syntax or untouched.
|
|
47
|
+
*/
|
|
48
|
+
export function unlocalize(name: string): string;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Attempts to derive the singular form of an English noun.
|
|
52
|
+
* If '@singular' is passed as annotation, that is preferred.
|
|
53
|
+
* @param dn annotations
|
|
54
|
+
* @param stripped if true, leading namespace will be stripped
|
|
55
|
+
*/
|
|
56
|
+
export function singular4(dn: Annotations | string, stripped: boolean): string;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Attempts to derive the plural form of an English noun.
|
|
60
|
+
* If '@plural' is passed as annotation, that is preferred.
|
|
61
|
+
* @param dn annotations
|
|
62
|
+
* @param stripped if true, leading namespace will be stripped
|
|
63
|
+
*/
|
|
64
|
+
export function plural4(dn: Annotations | string, stripped: boolean): string;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Parses command line arguments into named and positional parameters.
|
|
68
|
+
* Named parameters are expected to start with a double dash (--).
|
|
69
|
+
* If the next argument `B` after a named parameter `A` is not a named parameter itself,
|
|
70
|
+
* `B` is used as value for `A`.
|
|
71
|
+
* If `A` and `B` are both named parameters, `A` is just treated as a flag (and may receive a default value).
|
|
72
|
+
* Only named parameters that occur in validFlags are allowed. Specifying named flags that are not listed there
|
|
73
|
+
* will cause an error.
|
|
74
|
+
* Named parameters that are either not specified or do not have a value assigned to them may draw a default value
|
|
75
|
+
* from their definition in validFlags.
|
|
76
|
+
* @param argv list of command line arguments
|
|
77
|
+
* @param validFlags allowed flags. May specify default values.
|
|
78
|
+
*/
|
|
79
|
+
export function parseCommandlineArgs(argv: string[], validFlags: {[key: string]: CommandlineFlag}): ParsedFlags;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Performs a deep merge of the passed objects into the first object.
|
|
83
|
+
* See Object.assign(target, source).
|
|
84
|
+
* @param target object to assign into.
|
|
85
|
+
* @param source object to assign from.
|
|
86
|
+
*/
|
|
87
|
+
export function deepMerge(target: {}, source: {}): void;
|