extract-sdk-schema.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. #!/usr/bin/env node
  2. /**
  3. * Extracts metadata about the Logseq JS SDK from the generated *.d.ts files.
  4. *
  5. * This script uses ts-morph so we can rely on the TypeScript compiler's view of
  6. * the declarations. We intentionally read the emitted declaration files in
  7. * dist/ so that consumers do not need to depend on the source layout.
  8. *
  9. * The resulting schema is written to dist/logseq-sdk-schema.json and contains
  10. * a simplified representation that downstream tooling (Babashka) can consume.
  11. */
  12. const fs = require('node:fs');
  13. const path = require('node:path');
  14. const { Project, Node } = require('ts-morph');
  15. const ROOT = path.resolve(__dirname, '..');
  16. const DIST_DIR = path.join(ROOT, 'dist');
  17. const OUTPUT_FILE = path.join(DIST_DIR, 'logseq-sdk-schema.json');
  18. const DECL_FILES = [
  19. 'LSPlugin.d.ts',
  20. 'LSPlugin.user.d.ts',
  21. 'modules/LSPlugin.Experiments.d.ts',
  22. ];
  23. /**
  24. * Interfaces whose methods will be turned into CLJS wrappers at runtime.
  25. * These correspond to `logseq.<Namespace>` targets in the JS SDK.
  26. */
  27. const TARGET_INTERFACES = [
  28. 'IAppProxy',
  29. 'IEditorProxy',
  30. 'IDBProxy',
  31. 'IUIProxy',
  32. 'IUtilsProxy',
  33. 'IGitProxy',
  34. 'IAssetsProxy',
  35. ];
  36. const TARGET_CLASSES = [
  37. 'LSPluginUser',
  38. 'LSPluginExperiments',
  39. ];
  40. /**
  41. * Simple heuristics to determine whether a parameter should be converted via
  42. * cljs-bean when crossing the JS <-> CLJS boundary.
  43. */
  44. const BEAN_TO_JS_REGEX =
  45. /(Record<|Array<|Partial<|UIOptions|UIContainerAttrs|StyleString|StyleOptions|object|any|unknown|IHookEvent|BlockEntity|PageEntity|Promise<\s*Record)/i;
  46. const project = new Project({
  47. compilerOptions: { allowJs: true },
  48. });
  49. DECL_FILES.forEach((file) => {
  50. const full = path.join(DIST_DIR, file);
  51. if (fs.existsSync(full)) {
  52. project.addSourceFileAtPath(full);
  53. }
  54. });
  55. const schema = {
  56. generatedAt: new Date().toISOString(),
  57. interfaces: {},
  58. classes: {},
  59. };
  60. const serializeDoc = (symbol) => {
  61. if (!symbol) return undefined;
  62. const decl = symbol.getDeclarations()[0];
  63. if (!decl) return undefined;
  64. const docs = decl
  65. .getJsDocs()
  66. .map((doc) => doc.getComment())
  67. .filter(Boolean);
  68. return docs.length ? docs.join('\n\n') : undefined;
  69. };
  70. const serializeParameter = (signature, symbol, memberNode) => {
  71. const name = symbol.getName();
  72. const declaration = symbol.getDeclarations()[0];
  73. let typeText;
  74. let optional = symbol.isOptional?.() ?? false;
  75. let rest = symbol.isRestParameter?.() ?? false;
  76. if (declaration && Node.isParameterDeclaration(declaration)) {
  77. typeText = declaration.getType().getText();
  78. optional = declaration.hasQuestionToken?.() ?? false;
  79. rest = declaration.isRestParameter?.() ?? false;
  80. } else {
  81. const location =
  82. signature.getDeclaration?.() ??
  83. memberNode ??
  84. declaration ??
  85. symbol.getDeclarations()[0];
  86. typeText = symbol.getTypeAtLocation(location).getText();
  87. }
  88. const convertToJs = BEAN_TO_JS_REGEX.test(typeText);
  89. return {
  90. name,
  91. type: typeText,
  92. optional,
  93. rest,
  94. beanToJs: convertToJs,
  95. };
  96. };
  97. const serializeSignature = (sig, memberNode) => {
  98. const params = sig.getParameters().map((paramSymbol) =>
  99. serializeParameter(sig, paramSymbol, memberNode)
  100. );
  101. const returnType = sig.getReturnType().getText();
  102. return {
  103. parameters: params,
  104. returnType,
  105. };
  106. };
  107. const serializeCallable = (symbol, member) => {
  108. if (!symbol) return null;
  109. const type = symbol.getTypeAtLocation(member);
  110. const callSignatures = type.getCallSignatures();
  111. if (!callSignatures.length) {
  112. return null;
  113. }
  114. return {
  115. name: symbol.getName(),
  116. documentation: serializeDoc(symbol),
  117. signatures: callSignatures.map((sig) => serializeSignature(sig, member)),
  118. };
  119. };
  120. const sourceFiles = project.getSourceFiles();
  121. sourceFiles.forEach((source) => {
  122. source.getInterfaces().forEach((iface) => {
  123. const name = iface.getName();
  124. if (!TARGET_INTERFACES.includes(name)) {
  125. return;
  126. }
  127. const interfaceSymbol = iface.getType().getSymbol();
  128. const doc = serializeDoc(interfaceSymbol);
  129. const methods = iface
  130. .getMembers()
  131. .map((member) => serializeCallable(member.getSymbol(), member))
  132. .filter(Boolean);
  133. schema.interfaces[name] = {
  134. documentation: doc,
  135. methods,
  136. };
  137. });
  138. source.getClasses().forEach((cls) => {
  139. const name = cls.getName();
  140. if (!TARGET_CLASSES.includes(name)) {
  141. return;
  142. }
  143. const classSymbol = cls.getType().getSymbol();
  144. const doc = serializeDoc(classSymbol);
  145. const methods = cls
  146. .getInstanceMethods()
  147. .filter((method) => method.getName() !== 'constructor')
  148. .map((method) => serializeCallable(method.getSymbol(), method))
  149. .filter(Boolean);
  150. const getters = cls.getGetAccessors().map((accessor) => ({
  151. name: accessor.getName(),
  152. documentation: serializeDoc(accessor.getSymbol()),
  153. returnType: accessor.getReturnType().getText(),
  154. }));
  155. schema.classes[name] = {
  156. documentation: doc,
  157. methods,
  158. getters,
  159. };
  160. });
  161. });
  162. fs.mkdirSync(DIST_DIR, { recursive: true });
  163. fs.writeFileSync(OUTPUT_FILE, JSON.stringify(schema, null, 2));
  164. console.log(`Wrote ${OUTPUT_FILE}`);