@aloma.io/integration-sdk 3.8.68 → 3.8.69

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.
Files changed (34) hide show
  1. package/build/transform/index.d.mts +2 -2
  2. package/build/transform/index.mjs +493 -78
  3. package/package.json +2 -2
  4. package/src/transform/index.mts +530 -96
  5. package/test/fixtures/transform/basic/expected.json +9 -0
  6. package/test/fixtures/transform/basic/src/controller/abstract.mts +5 -0
  7. package/test/fixtures/transform/basic/src/controller/index.mts +30 -0
  8. package/test/fixtures/transform/basic/tsconfig.json +1 -0
  9. package/test/fixtures/transform/interface-param/src/controller/abstract.mts +5 -0
  10. package/test/fixtures/transform/interface-param/src/controller/index.mts +12 -0
  11. package/test/fixtures/transform/interface-param/tsconfig.json +1 -0
  12. package/test/fixtures/transform/overloads/expected.json +6 -0
  13. package/test/fixtures/transform/overloads/src/controller/abstract.mts +5 -0
  14. package/test/fixtures/transform/overloads/src/controller/index.mts +11 -0
  15. package/test/fixtures/transform/overloads/tsconfig.json +1 -0
  16. package/test/fixtures/transform/tdd-stub/expected.json +8 -0
  17. package/test/fixtures/transform/tdd-stub/src/controller/abstract.mts +6 -0
  18. package/test/fixtures/transform/tdd-stub/src/controller/index.mts +204 -0
  19. package/test/fixtures/transform/tdd-stub/tsconfig.json +1 -0
  20. package/test/fixtures/transform/two-exports/src/controller/abstract.mts +5 -0
  21. package/test/fixtures/transform/two-exports/src/controller/index.mts +9 -0
  22. package/test/fixtures/transform/two-exports/tsconfig.json +1 -0
  23. package/test/fixtures/transform/type-error/src/controller/abstract.mts +5 -0
  24. package/test/fixtures/transform/type-error/src/controller/index.mts +7 -0
  25. package/test/fixtures/transform/type-error/tsconfig.json +1 -0
  26. package/test/fixtures/transform/typed/expected.json +8 -0
  27. package/test/fixtures/transform/typed/src/controller/abstract.mts +5 -0
  28. package/test/fixtures/transform/typed/src/controller/index.mts +13 -0
  29. package/test/fixtures/transform/typed/tsconfig.json +1 -0
  30. package/test/fixtures/transform/visibility/expected.json +9 -0
  31. package/test/fixtures/transform/visibility/src/controller/abstract.mts +5 -0
  32. package/test/fixtures/transform/visibility/src/controller/index.mts +19 -0
  33. package/test/fixtures/transform/visibility/tsconfig.json +1 -0
  34. package/test/transform.test.mjs +68 -0
@@ -1,5 +1,5 @@
1
- declare const _default: (path: string) => Promise<{
2
- text: any;
1
+ declare const _default: (file: string) => Promise<{
2
+ text: string;
3
3
  methods: string[];
4
4
  }>;
5
5
  export default _default;
@@ -1,89 +1,504 @@
1
- import { parseFromFiles } from '@ts-ast-parser/core';
1
+ // Reads a connector's controller source and produces the method declarations (`text`) and the method names
2
+ // (`methods`) that `cli build` writes to build/.controller.json.
3
+ //
4
+ // This used to go through @ts-ast-parser/core. That package has had no release since 2024 and its `engines` field
5
+ // stops at Node 20, so `yarn install` failed on Node 22 and 24. This file does the few things the SDK needs from it
6
+ // directly with the TypeScript compiler API (typescript is already a dependency) and `comment-parser` (the same
7
+ // JSDoc parser the old package used, so JSDoc tags come out the same).
8
+ import { parse as parseComment } from 'comment-parser';
9
+ import path from 'node:path';
10
+ import ts from 'typescript';
11
+ // ---------------------------------------------------------------------------------------------------------------
12
+ // program
13
+ const DEFAULT_COMPILER_OPTIONS = {
14
+ experimentalDecorators: true,
15
+ target: ts.ScriptTarget.ES2020,
16
+ module: ts.ModuleKind.ES2020,
17
+ lib: ['lib.es2020.d.ts', 'lib.dom.d.ts'],
18
+ declaration: true,
19
+ skipLibCheck: true,
20
+ skipDefaultLibCheck: true,
21
+ };
22
+ // the connector's tsconfig.json (found from the working directory upwards), else defaults
23
+ const compilerOptions = () => {
24
+ const configPath = ts.findConfigFile(process.cwd(), ts.sys.fileExists, 'tsconfig.json');
25
+ const config = configPath && ts.readConfigFile(configPath, ts.sys.readFile);
26
+ if (!configPath || !config || config.error)
27
+ return DEFAULT_COMPILER_OPTIONS;
28
+ const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, path.dirname(configPath), {}, configPath);
29
+ return parsed.errors.length ? DEFAULT_COMPILER_OPTIONS : parsed.options;
30
+ };
31
+ const createProgram = (file) => {
32
+ const options = compilerOptions();
33
+ const program = ts.createProgram({ rootNames: [file], options, host: ts.createCompilerHost(options, true) });
34
+ const diagnostics = ts.sortAndDeduplicateDiagnostics([
35
+ ...program.getSemanticDiagnostics(),
36
+ ...program.getSyntacticDiagnostics(),
37
+ ]);
38
+ if (diagnostics.length) {
39
+ const host = {
40
+ getCanonicalFileName: (name) => name,
41
+ getCurrentDirectory: () => process.cwd(),
42
+ getNewLine: () => '\n',
43
+ };
44
+ throw new Error(`${file} has TypeScript errors:\n${ts.formatDiagnostics(diagnostics, host)}`);
45
+ }
46
+ const source = program.getSourceFile(file);
47
+ if (!source)
48
+ throw new Error(`Unable to analyse file ${file}`);
49
+ return { program, checker: program.getTypeChecker(), source };
50
+ };
51
+ // ---------------------------------------------------------------------------------------------------------------
52
+ // JSDoc: the tags of the comment in front of a node, as [{kind, value}] in first-appearance order of the kind
53
+ const trimNewLines = (str = '') => str.replace(/^\s+|\s+$/g, '');
54
+ const tagValue = (tagName, tag) => {
55
+ const result = {};
56
+ const description = trimNewLines(((d) => (d.startsWith('- ') ? d.slice(2) : d))(tag.description ?? ''));
57
+ const name = tag.name ?? '';
58
+ if ((tag.default ?? '') !== '')
59
+ result.default = tag.default;
60
+ if (tag.optional)
61
+ result.optional = true;
62
+ if ((tag.type ?? '') !== '')
63
+ result.type = tag.type;
64
+ if (name !== '')
65
+ result.name = name;
66
+ if (description !== '')
67
+ result.description = description;
68
+ const count = Object.keys(result).length;
69
+ // only one string key: the value is that string
70
+ if (count === 1 && (result.description ?? result.name))
71
+ return name || description;
72
+ // name and description with no "-" between them: one string
73
+ if (count === 2 && result.description && result.name) {
74
+ const line = tag.source.find((s) => s.source.includes(tagName));
75
+ const text = (line?.tokens.name ?? '') + (line?.tokens.description ?? '');
76
+ if (!text.includes('-'))
77
+ return [name, description].join(' ');
78
+ }
79
+ return result;
80
+ };
81
+ const jsDoc = (node) => {
82
+ const text = node.getSourceFile()?.text;
83
+ if (!text)
84
+ return [];
85
+ const comment = (ts.getLeadingCommentRanges(text, node.pos) ?? [])
86
+ .map((range) => text.substring(range.pos, range.end))
87
+ .join('\n');
88
+ const byKind = {};
89
+ const add = (kind, value) => (byKind[kind] ??= []).push(value);
90
+ for (const block of parseComment(comment, { spacing: 'preserve' })) {
91
+ if (block.problems.length)
92
+ continue;
93
+ const description = trimNewLines(block.description ?? '');
94
+ if (description !== '')
95
+ add('description', description);
96
+ for (const tag of block.tags)
97
+ add(tag.tag ?? '', tagValue(tag.tag ?? '', tag));
98
+ }
99
+ return Object.entries(byKind).flatMap(([kind, values]) => values.map((value) => ({ kind, value })));
100
+ };
101
+ const isIgnored = (docs) => docs.some(({ kind }) => ['ignore', 'internal', 'private'].includes(kind));
102
+ // ---------------------------------------------------------------------------------------------------------------
103
+ // expressions and types
104
+ const isThirdParty = (path) => path.length < 1000 && (/.*node_modules\/.+/.test(path) || /^https?:\/\/.+/.test(path));
105
+ // the default value of a parameter or binding element, as text/number/boolean/null
106
+ const resolveExpression = (checker, expression) => {
107
+ let expr = expression;
108
+ if (expr == null)
109
+ return undefined;
110
+ if (ts.isAsExpression(expr) || ts.isTypeAssertionExpression(expr) || ts.isParenthesizedExpression(expr) || ts.isComputedPropertyName(expr)) {
111
+ expr = expr.expression;
112
+ }
113
+ if (ts.isPrefixUnaryExpression(expr)) {
114
+ const value = resolveExpression(checker, expr.operand);
115
+ if (typeof value !== 'number' && typeof value !== 'string' && typeof value !== 'boolean')
116
+ return value;
117
+ if (expr.operator === ts.SyntaxKind.MinusToken)
118
+ return -value;
119
+ if (expr.operator === ts.SyntaxKind.ExclamationToken)
120
+ return !value;
121
+ if (expr.operator === ts.SyntaxKind.PlusToken)
122
+ return +value;
123
+ return value;
124
+ }
125
+ if (expr == null)
126
+ return undefined;
127
+ const text = expr.getText() ?? '';
128
+ if (ts.isStringLiteralLike(expr) || ts.isArrayLiteralExpression(expr) || ts.isObjectLiteralExpression(expr))
129
+ return text;
130
+ if (ts.isNumericLiteral(expr)) {
131
+ const parsed = parseFloat(text);
132
+ return isNaN(parsed) ? text : parsed;
133
+ }
134
+ if (expr.kind === ts.SyntaxKind.FalseKeyword || expr.kind === ts.SyntaxKind.TrueKeyword)
135
+ return text === 'true';
136
+ if (expr.kind === ts.SyntaxKind.NullKeyword)
137
+ return null;
138
+ if (expr.kind === ts.SyntaxKind.UndefinedKeyword)
139
+ return undefined;
140
+ if (ts.isIdentifier(expr) || ts.isPropertyAccessExpression(expr)) {
141
+ let symbol = checker.getSymbolAtLocation(expr);
142
+ while (symbol && (symbol.flags & ts.SymbolFlags.Alias) !== 0)
143
+ symbol = checker.getAliasedSymbol(symbol);
144
+ const declaration = (symbol ?? expr.symbol)?.declarations?.[0];
145
+ if (declaration == null || isThirdParty(declaration.getSourceFile().fileName))
146
+ return text;
147
+ if (ts.isVariableDeclaration(declaration) || ts.isPropertyDeclaration(declaration)) {
148
+ return resolveExpression(checker, declaration.initializer);
149
+ }
150
+ return text;
151
+ }
152
+ return text;
153
+ };
154
+ const KEYWORD_TEXT = {
155
+ [ts.SyntaxKind.NumberKeyword]: 'number',
156
+ [ts.SyntaxKind.BooleanKeyword]: 'boolean',
157
+ [ts.SyntaxKind.StringKeyword]: 'string',
158
+ [ts.SyntaxKind.UndefinedKeyword]: 'undefined',
159
+ [ts.SyntaxKind.NullKeyword]: 'null',
160
+ [ts.SyntaxKind.ObjectKeyword]: 'object',
161
+ [ts.SyntaxKind.SymbolKeyword]: 'symbol',
162
+ [ts.SyntaxKind.BigIntKeyword]: 'bigint',
163
+ [ts.SyntaxKind.VoidKeyword]: 'void',
164
+ };
165
+ const UNKNOWN_KEYWORD_TEXT = {
166
+ [ts.SyntaxKind.AnyKeyword]: 'any',
167
+ [ts.SyntaxKind.NeverKeyword]: 'never',
168
+ [ts.SyntaxKind.ObjectKeyword]: 'object',
169
+ [ts.SyntaxKind.UnknownKeyword]: 'unknown',
170
+ [ts.SyntaxKind.IntrinsicKeyword]: 'intrinsic',
171
+ };
172
+ // the text of a type, given a type node (from source) or a type (turned into a node by the checker)
173
+ const typeText = (checker, nodeOrType) => {
174
+ let node;
175
+ let type;
176
+ if ('kind' in nodeOrType) {
177
+ node = nodeOrType;
178
+ type = checker.getTypeFromTypeNode(nodeOrType);
179
+ }
180
+ else {
181
+ type = nodeOrType;
182
+ const made = checker.typeToTypeNode(type, undefined, ts.NodeBuilderFlags.IgnoreErrors);
183
+ if (!made)
184
+ return checker.typeToString(type);
185
+ node = made;
186
+ }
187
+ // parentheses carry no information for the reader
188
+ if (ts.isParenthesizedTypeNode(node)) {
189
+ node = node.type;
190
+ type = checker.getTypeFromTypeNode(node);
191
+ }
192
+ const nodeAtStart = node;
193
+ const asString = () => checker.typeToString(type) ?? '';
194
+ // a node written in the source has text; a node made by the checker does not
195
+ const sourceText = () => {
196
+ try {
197
+ return nodeAtStart.getText() ?? '';
198
+ }
199
+ catch {
200
+ return asString();
201
+ }
202
+ };
203
+ const child = (n) => typeText(checker, n);
204
+ switch (node.kind) {
205
+ case ts.SyntaxKind.TypeLiteral:
206
+ case ts.SyntaxKind.ConditionalType:
207
+ case ts.SyntaxKind.UnionType:
208
+ case ts.SyntaxKind.IntersectionType:
209
+ case ts.SyntaxKind.TypeOperator:
210
+ return sourceText();
211
+ case ts.SyntaxKind.ArrayType:
212
+ return `${child(node.elementType)}[]`;
213
+ case ts.SyntaxKind.TupleType:
214
+ return `[${node.elements.map(child).join(', ')}]`;
215
+ case ts.SyntaxKind.NamedTupleMember:
216
+ return `${node.name.getText() ?? ''}: ${child(node.type)}`;
217
+ case ts.SyntaxKind.OptionalType:
218
+ return `${child(node.type)}?`;
219
+ case ts.SyntaxKind.RestType:
220
+ return `...${child(node.type)}`;
221
+ case ts.SyntaxKind.IndexedAccessType:
222
+ return `${child(node.objectType)}[${child(node.indexType)}]`;
223
+ case ts.SyntaxKind.TypeReference: {
224
+ if ((type.flags & ts.TypeFlags.Any) === 0)
225
+ return asString();
226
+ const ref = node;
227
+ const name = ts.isIdentifier(ref.typeName) ? ref.typeName.text : ref.typeName.right.text;
228
+ const args = (ref.typeArguments ?? []).map(child);
229
+ return args.length > 0 ? `${name}<${args.join(', ')}>` : name;
230
+ }
231
+ case ts.SyntaxKind.LiteralType: {
232
+ const literal = node.literal;
233
+ switch (literal.kind) {
234
+ case ts.SyntaxKind.TrueKeyword:
235
+ return 'true';
236
+ case ts.SyntaxKind.FalseKeyword:
237
+ return 'false';
238
+ case ts.SyntaxKind.NullKeyword:
239
+ return 'null';
240
+ case ts.SyntaxKind.PrefixUnaryExpression:
241
+ if (literal.operand.kind === ts.SyntaxKind.NumericLiteral)
242
+ return literal.getText();
243
+ if (literal.operand.kind === ts.SyntaxKind.BigIntLiteral)
244
+ return literal.getText().replace('n', '');
245
+ return '';
246
+ case ts.SyntaxKind.NumericLiteral:
247
+ case ts.SyntaxKind.BigIntLiteral:
248
+ case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
249
+ return literal.text;
250
+ case ts.SyntaxKind.StringLiteral:
251
+ return `"${literal.text}"`;
252
+ default:
253
+ return '';
254
+ }
255
+ }
256
+ case ts.SyntaxKind.InferType: {
257
+ const parameter = node.typeParameter;
258
+ return parameter.constraint ? `infer ${parameter.name.text} extends ${child(parameter.constraint)}` : `infer ${parameter.name.text}`;
259
+ }
260
+ case ts.SyntaxKind.TypePredicate: {
261
+ const predicate = node;
262
+ const name = ts.isThisTypeNode(predicate.parameterName) ? 'this' : predicate.parameterName.getText();
263
+ const out = predicate.assertsModifier ? ['asserts', name] : [name];
264
+ if (predicate.type)
265
+ out.push('is', child(predicate.type));
266
+ return out.join(' ');
267
+ }
268
+ case ts.SyntaxKind.FunctionType:
269
+ case ts.SyntaxKind.TypeQuery:
270
+ case ts.SyntaxKind.MappedType:
271
+ case ts.SyntaxKind.TemplateLiteralType:
272
+ return asString();
273
+ case ts.SyntaxKind.AnyKeyword:
274
+ case ts.SyntaxKind.NeverKeyword:
275
+ case ts.SyntaxKind.UnknownKeyword:
276
+ case ts.SyntaxKind.IntrinsicKeyword:
277
+ return UNKNOWN_KEYWORD_TEXT[node.kind];
278
+ default:
279
+ // intrinsic keywords (string, number, ...), else whatever the checker prints
280
+ return KEYWORD_TEXT[node.kind] ?? asString();
281
+ }
282
+ };
283
+ // type of a declaration with no checker symbol (the declared type, literals widened)
284
+ const typeOfDeclaration = (checker, node) => {
285
+ const fromDoc = ts.getJSDocType(node);
286
+ if (fromDoc)
287
+ return fromDoc;
288
+ const type = checker.getTypeAtLocation(node);
289
+ const assertion = ts.hasOnlyExpressionInitializer(node) &&
290
+ !!node.initializer &&
291
+ (ts.isAsExpression(node.initializer) || ts.isTypeAssertionExpression(node));
292
+ return assertion ? type : checker.getBaseTypeOfLiteralType(type);
293
+ };
294
+ // ---------------------------------------------------------------------------------------------------------------
295
+ // class and methods
296
+ const hasFlag = (flags, flag) => (flags & flag) === flag;
297
+ const isFunctionProperty = (decl) => ts.isPropertyDeclaration(decl) && !!decl.initializer && (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer));
298
+ const isPublic = (member) => {
299
+ const flags = ts.getCombinedModifierFlags(member);
300
+ const name = member.name;
301
+ return !(hasFlag(flags, ts.ModifierFlags.Private) || (name && ts.isPrivateIdentifier(name)) || hasFlag(flags, ts.ModifierFlags.Protected));
302
+ };
303
+ const modifierFlag = (node, flag) => !!node && ts.canHaveModifiers(node) && !ts.isVariableStatement(node) && hasFlag(ts.getCombinedModifierFlags(node), flag);
304
+ // is the member declared in the class's base class
305
+ const isInherited = (checker, classNode, member) => {
306
+ const base = checker.getTypeAtLocation(classNode).getBaseTypes?.()?.[0];
307
+ if (!base)
308
+ return false;
309
+ const parents = (base.getSymbol()?.getDeclarations() ?? []).slice();
310
+ parents.push(...parents.flatMap((parent) => (ts.isClassDeclaration(parent) ? parent.members.filter(ts.isConstructorDeclaration) : [])));
311
+ return parents.some((parent) => member.getDeclarations()?.some((decl) => decl.parent === parent));
312
+ };
313
+ // the class declared by a statement: `class X {}` or `const X = class {}`
314
+ const classOf = (node) => {
315
+ if (ts.isClassDeclaration(node))
316
+ return node;
317
+ if (!ts.isVariableStatement(node))
318
+ return null;
319
+ const initializer = node.declarationList.declarations[0]?.initializer;
320
+ return initializer && ts.isClassExpression(initializer) ? initializer : null;
321
+ };
322
+ // public instance methods the connector class declares itself (not inherited, not from node_modules)
323
+ const methodsOf = (checker, classNode) => {
324
+ const symbol = checker.getTypeAtLocation(classNode).getSymbol();
325
+ const declared = symbol && checker.getDeclaredTypeOfSymbol(symbol);
326
+ const methods = [];
327
+ for (const prop of declared?.getProperties() ?? []) {
328
+ if (hasFlag(prop.flags, ts.SymbolFlags.ModuleMember) || hasFlag(prop.flags, ts.SymbolFlags.Prototype))
329
+ continue;
330
+ const decl = prop.getDeclarations()?.[0];
331
+ if (!decl)
332
+ continue;
333
+ if (isThirdParty(decl.getSourceFile()?.fileName ?? ''))
334
+ continue;
335
+ if (!ts.isMethodDeclaration(decl) && !isFunctionProperty(decl))
336
+ continue;
337
+ const overrides = modifierFlag(decl, ts.ModifierFlags.Override);
338
+ if (!overrides && isInherited(checker, classNode, prop))
339
+ continue;
340
+ if (modifierFlag(decl, ts.ModifierFlags.Static) || !isPublic(decl))
341
+ continue;
342
+ methods.push({
343
+ name: decl.name?.getText() ?? '',
344
+ decl,
345
+ type: checker.getTypeOfSymbolAtLocation(prop, classNode),
346
+ });
347
+ }
348
+ return methods;
349
+ };
350
+ // the single class a connector file exports (`export default class ...`)
351
+ const exportedClass = (source) => {
352
+ // names exported by `export {A}`, `export {A as B}` and `export default A`
353
+ const exportedNames = new Set();
354
+ for (const statement of source.statements) {
355
+ if (ts.isExportAssignment(statement) && ts.isIdentifier(statement.expression))
356
+ exportedNames.add(statement.expression.text);
357
+ if (ts.isExportDeclaration(statement) && !statement.moduleSpecifier && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
358
+ for (const element of statement.exportClause.elements)
359
+ exportedNames.add((element.propertyName ?? element.name).text);
360
+ }
361
+ }
362
+ const declarations = [];
363
+ const consider = (name, node, exportedHere) => {
364
+ if (!exportedHere && !exportedNames.has(name))
365
+ return;
366
+ if (isIgnored(jsDoc(node)))
367
+ return;
368
+ if (!declarations.some((d) => d.name === name))
369
+ declarations.push({ name, node });
370
+ };
371
+ for (const statement of source.statements) {
372
+ const exportedHere = ts.canHaveModifiers(statement) && (ts.getModifiers(statement) ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
373
+ if (ts.isVariableStatement(statement)) {
374
+ for (const declaration of statement.declarationList.declarations)
375
+ consider(declaration.name.getText(), statement, exportedHere);
376
+ }
377
+ else if (ts.isFunctionDeclaration(statement) ||
378
+ ts.isClassDeclaration(statement) ||
379
+ ts.isInterfaceDeclaration(statement) ||
380
+ ts.isEnumDeclaration(statement) ||
381
+ ts.isTypeAliasDeclaration(statement)) {
382
+ consider(statement.name?.getText() ?? 'default', statement, exportedHere);
383
+ }
384
+ }
385
+ return declarations.length === 1 ? classOf(declarations[0].node) : null;
386
+ };
387
+ // ---------------------------------------------------------------------------------------------------------------
388
+ // declarations
2
389
  function stripQuotes(name) {
3
390
  if ((name.startsWith("'") && name.endsWith("'")) || (name.startsWith('"') && name.endsWith('"'))) {
4
391
  return name.slice(1, -1);
5
392
  }
6
393
  return name;
7
394
  }
8
- const transform = (meta) => {
9
- if (!meta?.length)
10
- throw new Error('metadata is empty');
11
- meta = meta[0];
12
- if (meta.getDeclarations()?.length !== 1) {
13
- throw new Error('connector file needs to export default class');
395
+ const symbolOf = (checker, node) => {
396
+ let symbol;
397
+ try {
398
+ symbol = checker.getSymbolAtLocation(node) ?? (node.name ? checker.getSymbolAtLocation(node.name) : undefined);
14
399
  }
15
- const methods = {};
16
- const decl = meta.getDeclarations()[0];
17
- const members = decl.getMethods().filter((member) => {
18
- return !(member.isStatic() ||
19
- member.isInherited() ||
20
- member.getKind() !== 'Method' ||
21
- member.getModifier() !== 'public' ||
22
- stripQuotes(member.getName()).startsWith('_'));
23
- });
24
- const text = members
25
- .map((member) => {
26
- const methodName = stripQuotes(member.getName());
27
- methods[methodName] = true;
28
- return member
29
- .getSignatures()
30
- .map((sig) => {
31
- const docs = sig.getJSDoc().serialize() || [];
32
- const desc = docs.find((what) => what.kind === 'description')?.value;
33
- const example = docs.find((what) => what.kind === 'example')?.value;
34
- const ns = docs.find((what) => what.kind === 'namespace')?.value;
35
- let space = ns ? `@namespace ${ns}` : '';
36
- let eg;
37
- if (example) {
38
- const parts = example.split(/```/);
39
- const backticks = '```';
40
- eg = `@example ${parts[0] || 'usage'}\n${backticks}${parts[1]}${backticks}`;
41
- }
42
- const paramDocs = docs.filter((what) => what.kind === 'param');
43
- const params = sig
44
- .getParameters()
45
- .filter((param) => param.isNamed())
46
- .map((param) => {
47
- const serialized = param.serialize();
48
- const prefix = param
49
- .getNamedElements()
50
- .map((p) => {
51
- const defaultVal = p.getDefault() != null ? ' = ' + p.getDefault() : '';
52
- return `${p.getName()}${defaultVal}`;
53
- })
54
- .join('; ');
55
- const suffix = serialized.type.properties
56
- .map((p) => {
57
- const comment = paramDocs.find((what) => what.value.name === p.name);
58
- const desc = (comment?.value.description || '').replace(/\\@/gi, '@');
59
- return `\n/**\n${desc}\n */\n ${p.name}: ${p.type.text}`;
60
- })
61
- .join('; ');
62
- return `{${prefix}}: {${suffix}}`;
400
+ catch {
401
+ symbol = undefined;
402
+ }
403
+ while (symbol && (symbol.flags & ts.SymbolFlags.Alias) !== 0)
404
+ symbol = checker.getAliasedSymbol(symbol);
405
+ return symbol ?? node.symbol ?? null;
406
+ };
407
+ // the properties of the object type of a destructured parameter: [name, type text]
408
+ //
409
+ // The type is turned into a type literal node and each property is read from that node, the way the previous
410
+ // implementation did, so the declarations stay identical (a property written as `email: string` in a
411
+ // `{email}: {email: string}` parameter is printed as `any`: a node the checker made cannot be resolved again).
412
+ const propertiesOf = (checker, parameter, symbol) => {
413
+ const type = symbol ? checker.getTypeOfSymbolAtLocation(symbol, parameter) : typeOfDeclaration(checker, parameter);
414
+ let node = 'kind' in type ? type : checker.typeToTypeNode(type, undefined, ts.NodeBuilderFlags.IgnoreErrors);
415
+ if (node && ts.isParenthesizedTypeNode(node))
416
+ node = node.type;
417
+ if (node && ts.isTypeLiteralNode(node)) {
418
+ const literal = node;
419
+ return literal.members.filter(ts.isPropertySignature).map((member) => {
420
+ const name = ts.isIdentifier(member.name) ? String(member.name.escapedText) : member.name.getText();
421
+ const memberSymbol = symbolOf(checker, member);
422
+ const memberType = memberSymbol && checker.getTypeOfSymbolAtLocation(memberSymbol, literal);
423
+ const fromDoc = ts.getJSDocType(member);
424
+ if (fromDoc)
425
+ return [name, typeText(checker, fromDoc)];
426
+ if (memberType)
427
+ return [name, typeText(checker, memberType)];
428
+ if (member.type)
429
+ return [name, typeText(checker, member.type)];
430
+ return [name, typeText(checker, typeOfDeclaration(checker, member))];
431
+ });
432
+ }
433
+ // not a type literal (an interface or a type alias, say): list its properties with the checker's types
434
+ const realType = 'kind' in type ? checker.getTypeFromTypeNode(type) : type;
435
+ return checker.getPropertiesOfType(realType).map((prop) => [prop.getName(), typeText(checker, checker.getTypeOfSymbolAtLocation(prop, parameter))]);
436
+ };
437
+ const declare = (checker, method) => {
438
+ const methodName = stripQuotes(method.name);
439
+ return method.type
440
+ .getNonNullableType()
441
+ .getCallSignatures()
442
+ .map((sig) => {
443
+ const declaration = sig.getDeclaration();
444
+ const docs = jsDoc(declaration);
445
+ const desc = docs.find((what) => what.kind === 'description')?.value;
446
+ const example = docs.find((what) => what.kind === 'example')?.value;
447
+ const ns = docs.find((what) => what.kind === 'namespace')?.value;
448
+ const space = ns ? `@namespace ${ns}` : '';
449
+ let eg;
450
+ if (example) {
451
+ const parts = example.split(/```/);
452
+ const backticks = '```';
453
+ eg = `@example ${parts[0] || 'usage'}\n${backticks}${parts[1]}${backticks}`;
454
+ }
455
+ const paramDocs = docs.filter((what) => what.kind === 'param');
456
+ // only destructured parameters ({a, b}: {...}) are part of the declaration
457
+ const params = declaration.parameters
458
+ .map((parameter, index) => ({ parameter, symbol: sig.parameters[index] }))
459
+ .filter(({ parameter }) => !isIgnored(jsDoc(parameter)) && ts.isObjectBindingPattern(parameter.name))
460
+ .map(({ parameter, symbol }) => {
461
+ const prefix = parameter.name.elements
462
+ .map((p) => {
463
+ const defaultVal = resolveExpression(checker, p.initializer);
464
+ return `${p.name.getText()}${defaultVal != null ? ' = ' + defaultVal : ''}`;
465
+ })
466
+ .join('; ');
467
+ const suffix = propertiesOf(checker, parameter, symbol)
468
+ .map(([name, type]) => {
469
+ const comment = paramDocs.find((what) => what.value.name === name);
470
+ const description = (comment?.value.description || '').replace(/\\@/gi, '@');
471
+ return `\n/**\n${description}\n */\n ${name}: ${type}`;
63
472
  })
64
- .join(', ');
65
- const retVal = sig
66
- .serialize()
67
- .return.type.text.replace(/^Promise</, '')
68
- .replace(/>$/, '');
69
- return `
70
- /**
71
- * ${desc || ''}
72
- *
73
- * ${space || ''}
74
- * ${eg || ''}
75
- **/
76
- declare function ${methodName}(${params}): ${retVal};
77
- `;
473
+ .join('; ');
474
+ return `{${prefix}}: {${suffix}}`;
78
475
  })
79
- .join('\n');
476
+ .join(', ');
477
+ const fromDoc = ts.getJSDocReturnType(declaration);
478
+ const retVal = typeText(checker, fromDoc ?? checker.getReturnTypeOfSignature(sig)).replace(/^Promise</, '').replace(/>$/, '');
479
+ // the exact whitespace (including the spaces after "**/") is part of the output other tools read
480
+ return [
481
+ '',
482
+ '/**',
483
+ ` * ${desc || ''}`,
484
+ ' *',
485
+ ` * ${space || ''}`,
486
+ ` * ${eg || ''}`,
487
+ ' **/ ',
488
+ `declare function ${methodName}(${params}): ${retVal};`,
489
+ ' ',
490
+ ].join('\n');
80
491
  })
81
- .join('');
82
- return { text, methods: Object.keys(methods) };
83
- };
84
- export default async (path) => {
85
- const parsed = await parseFromFiles([path]);
86
- if (parsed.errors?.length)
87
- throw new Error(path + ' ' + JSON.stringify(parsed.errors));
88
- return transform(parsed.project?.getModules() || []);
492
+ .join('\n');
493
+ };
494
+ export default async (file) => {
495
+ const { checker, source } = createProgram(file);
496
+ const classNode = exportedClass(source);
497
+ if (!classNode)
498
+ throw new Error('connector file needs to export default class');
499
+ const methods = methodsOf(checker, classNode).filter((method) => !stripQuotes(method.name).startsWith('_'));
500
+ return {
501
+ text: methods.map((method) => declare(checker, method)).join(''),
502
+ methods: [...new Set(methods.map((method) => stripQuotes(method.name)))],
503
+ };
89
504
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aloma.io/integration-sdk",
3
- "version": "3.8.68",
3
+ "version": "3.8.69",
4
4
  "description": "",
5
5
  "author": "aloma.io",
6
6
  "license": "Apache-2.0",
@@ -31,9 +31,9 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@paralleldrive/cuid2": "^2",
34
- "@ts-ast-parser/core": "^0.5",
35
34
  "@types/node": "^18",
36
35
  "commander": "^12",
36
+ "comment-parser": "^1.4.1",
37
37
  "dotenv": "^16",
38
38
  "express": "^4",
39
39
  "jose": "^5",