extract-sdk-schema.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  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. ];
  22. /**
  23. * Interfaces whose methods will be turned into CLJS wrappers at runtime.
  24. * These correspond to `logseq.<Namespace>` targets in the JS SDK.
  25. */
  26. const TARGET_INTERFACES = [
  27. 'IAppProxy',
  28. 'IEditorProxy',
  29. 'IDBProxy',
  30. 'IUIProxy',
  31. 'IUtilsProxy',
  32. 'IGitProxy',
  33. 'IAssetsProxy',
  34. ];
  35. /**
  36. * Simple heuristics to determine whether a parameter should be converted via
  37. * cljs-bean when crossing the JS <-> CLJS boundary.
  38. */
  39. const BEAN_TO_JS_REGEX =
  40. /(Record<|Array<|UIOptions|UIContainerAttrs|StyleString|StyleOptions|object|any|unknown|IHookEvent|BlockEntity|PageEntity|Promise<\s*Record)/i;
  41. const project = new Project({
  42. compilerOptions: { allowJs: true },
  43. });
  44. DECL_FILES.forEach((file) => {
  45. const full = path.join(DIST_DIR, file);
  46. if (fs.existsSync(full)) {
  47. project.addSourceFileAtPath(full);
  48. }
  49. });
  50. const schema = {
  51. generatedAt: new Date().toISOString(),
  52. interfaces: {},
  53. };
  54. const serializeDoc = (symbol) => {
  55. if (!symbol) return undefined;
  56. const decl = symbol.getDeclarations()[0];
  57. if (!decl) return undefined;
  58. const docs = decl
  59. .getJsDocs()
  60. .map((doc) => doc.getComment())
  61. .filter(Boolean);
  62. return docs.length ? docs.join('\n\n') : undefined;
  63. };
  64. const serializeParameter = (signature, symbol, memberNode) => {
  65. const name = symbol.getName();
  66. const declaration = symbol.getDeclarations()[0];
  67. let typeText;
  68. let optional = symbol.isOptional?.() ?? false;
  69. let rest = symbol.isRestParameter?.() ?? false;
  70. if (declaration && Node.isParameterDeclaration(declaration)) {
  71. typeText = declaration.getType().getText();
  72. optional = declaration.hasQuestionToken?.() ?? false;
  73. rest = declaration.isRestParameter?.() ?? false;
  74. } else {
  75. const location =
  76. signature.getDeclaration?.() ??
  77. memberNode ??
  78. declaration ??
  79. symbol.getDeclarations()[0];
  80. typeText = symbol.getTypeAtLocation(location).getText();
  81. }
  82. const convertToJs = BEAN_TO_JS_REGEX.test(typeText);
  83. return {
  84. name,
  85. type: typeText,
  86. optional,
  87. rest,
  88. beanToJs: convertToJs,
  89. };
  90. };
  91. const serializeSignature = (sig, memberNode) => {
  92. const params = sig.getParameters().map((paramSymbol) =>
  93. serializeParameter(sig, paramSymbol, memberNode)
  94. );
  95. const returnType = sig.getReturnType().getText();
  96. return {
  97. parameters: params,
  98. returnType,
  99. };
  100. };
  101. const sourceFiles = project.getSourceFiles();
  102. sourceFiles.forEach((source) => {
  103. source.getInterfaces().forEach((iface) => {
  104. const name = iface.getName();
  105. if (!TARGET_INTERFACES.includes(name)) {
  106. return;
  107. }
  108. const interfaceSymbol = iface.getType().getSymbol();
  109. const doc = serializeDoc(interfaceSymbol);
  110. const methods = iface.getMembers().map((member) => {
  111. const symbol = member.getSymbol();
  112. if (!symbol) return null;
  113. const type = symbol.getTypeAtLocation(member);
  114. const callSignatures = type.getCallSignatures();
  115. if (!callSignatures.length) {
  116. return null;
  117. }
  118. return {
  119. name: symbol.getName(),
  120. documentation: serializeDoc(symbol),
  121. signatures: callSignatures.map((sig) => serializeSignature(sig, member)),
  122. };
  123. }).filter(Boolean);
  124. schema.interfaces[name] = {
  125. documentation: doc,
  126. methods,
  127. };
  128. });
  129. });
  130. fs.mkdirSync(DIST_DIR, { recursive: true });
  131. fs.writeFileSync(OUTPUT_FILE, JSON.stringify(schema, null, 2));
  132. console.log(`Wrote ${OUTPUT_FILE}`);