@logseq/libs
Advanced tools
| #!/usr/bin/env node | ||
| /** | ||
| * Extracts metadata about the Logseq JS SDK from the generated *.d.ts files. | ||
| * | ||
| * This script uses ts-morph so we can rely on the TypeScript compiler's view of | ||
| * the declarations. We intentionally read the emitted declaration files in | ||
| * dist/ so that consumers do not need to depend on the source layout. | ||
| * | ||
| * The resulting schema is written to dist/logseq-sdk-schema.json and contains | ||
| * a simplified representation that downstream tooling (Babashka) can consume. | ||
| */ | ||
| const fs = require('node:fs'); | ||
| const path = require('node:path'); | ||
| const { Project, Node } = require('ts-morph'); | ||
| const ROOT = path.resolve(__dirname, '..'); | ||
| const DIST_DIR = path.join(ROOT, 'dist'); | ||
| const OUTPUT_FILE = path.join(DIST_DIR, 'logseq-sdk-schema.json'); | ||
| const DECL_FILES = [ | ||
| 'LSPlugin.d.ts', | ||
| 'LSPlugin.user.d.ts', | ||
| ]; | ||
| /** | ||
| * Interfaces whose methods will be turned into CLJS wrappers at runtime. | ||
| * These correspond to `logseq.<Namespace>` targets in the JS SDK. | ||
| */ | ||
| const TARGET_INTERFACES = [ | ||
| 'IAppProxy', | ||
| 'IEditorProxy', | ||
| 'IDBProxy', | ||
| 'IUIProxy', | ||
| 'IUtilsProxy', | ||
| 'IGitProxy', | ||
| 'IAssetsProxy', | ||
| ]; | ||
| /** | ||
| * Simple heuristics to determine whether a parameter should be converted via | ||
| * cljs-bean when crossing the JS <-> CLJS boundary. | ||
| */ | ||
| const BEAN_TO_JS_REGEX = | ||
| /(Record<|Array<|Partial<|UIOptions|UIContainerAttrs|StyleString|StyleOptions|object|any|unknown|IHookEvent|BlockEntity|PageEntity|Promise<\s*Record)/i; | ||
| const project = new Project({ | ||
| compilerOptions: { allowJs: true }, | ||
| }); | ||
| DECL_FILES.forEach((file) => { | ||
| const full = path.join(DIST_DIR, file); | ||
| if (fs.existsSync(full)) { | ||
| project.addSourceFileAtPath(full); | ||
| } | ||
| }); | ||
| const schema = { | ||
| generatedAt: new Date().toISOString(), | ||
| interfaces: {}, | ||
| classes: {}, | ||
| }; | ||
| const serializeDoc = (symbol) => { | ||
| if (!symbol) return undefined; | ||
| const decl = symbol.getDeclarations()[0]; | ||
| if (!decl) return undefined; | ||
| const docs = decl | ||
| .getJsDocs() | ||
| .map((doc) => doc.getComment()) | ||
| .filter(Boolean); | ||
| return docs.length ? docs.join('\n\n') : undefined; | ||
| }; | ||
| const serializeParameter = (signature, symbol, memberNode) => { | ||
| const name = symbol.getName(); | ||
| const declaration = symbol.getDeclarations()[0]; | ||
| let typeText; | ||
| let optional = symbol.isOptional?.() ?? false; | ||
| let rest = symbol.isRestParameter?.() ?? false; | ||
| if (declaration && Node.isParameterDeclaration(declaration)) { | ||
| typeText = declaration.getType().getText(); | ||
| optional = declaration.hasQuestionToken?.() ?? false; | ||
| rest = declaration.isRestParameter?.() ?? false; | ||
| } else { | ||
| const location = | ||
| signature.getDeclaration?.() ?? | ||
| memberNode ?? | ||
| declaration ?? | ||
| symbol.getDeclarations()[0]; | ||
| typeText = symbol.getTypeAtLocation(location).getText(); | ||
| } | ||
| const convertToJs = BEAN_TO_JS_REGEX.test(typeText); | ||
| return { | ||
| name, | ||
| type: typeText, | ||
| optional, | ||
| rest, | ||
| beanToJs: convertToJs, | ||
| }; | ||
| }; | ||
| const serializeSignature = (sig, memberNode) => { | ||
| const params = sig.getParameters().map((paramSymbol) => | ||
| serializeParameter(sig, paramSymbol, memberNode) | ||
| ); | ||
| const returnType = sig.getReturnType().getText(); | ||
| return { | ||
| parameters: params, | ||
| returnType, | ||
| }; | ||
| }; | ||
| const serializeCallable = (symbol, member) => { | ||
| if (!symbol) return null; | ||
| const type = symbol.getTypeAtLocation(member); | ||
| const callSignatures = type.getCallSignatures(); | ||
| if (!callSignatures.length) { | ||
| return null; | ||
| } | ||
| return { | ||
| name: symbol.getName(), | ||
| documentation: serializeDoc(symbol), | ||
| signatures: callSignatures.map((sig) => serializeSignature(sig, member)), | ||
| }; | ||
| }; | ||
| const sourceFiles = project.getSourceFiles(); | ||
| sourceFiles.forEach((source) => { | ||
| source.getInterfaces().forEach((iface) => { | ||
| const name = iface.getName(); | ||
| if (!TARGET_INTERFACES.includes(name)) { | ||
| return; | ||
| } | ||
| const interfaceSymbol = iface.getType().getSymbol(); | ||
| const doc = serializeDoc(interfaceSymbol); | ||
| const methods = iface | ||
| .getMembers() | ||
| .map((member) => serializeCallable(member.getSymbol(), member)) | ||
| .filter(Boolean); | ||
| schema.interfaces[name] = { | ||
| documentation: doc, | ||
| methods, | ||
| }; | ||
| }); | ||
| source.getClasses().forEach((cls) => { | ||
| const name = cls.getName(); | ||
| if (name !== 'LSPluginUser') { | ||
| return; | ||
| } | ||
| const classSymbol = cls.getType().getSymbol(); | ||
| const doc = serializeDoc(classSymbol); | ||
| const methods = cls | ||
| .getInstanceMethods() | ||
| .filter((method) => method.getName() !== 'constructor') | ||
| .map((method) => serializeCallable(method.getSymbol(), method)) | ||
| .filter(Boolean); | ||
| const getters = cls.getGetAccessors().map((accessor) => ({ | ||
| name: accessor.getName(), | ||
| documentation: serializeDoc(accessor.getSymbol()), | ||
| returnType: accessor.getReturnType().getText(), | ||
| })); | ||
| schema.classes[name] = { | ||
| documentation: doc, | ||
| methods, | ||
| getters, | ||
| }; | ||
| }); | ||
| }); | ||
| fs.mkdirSync(DIST_DIR, { recursive: true }); | ||
| fs.writeFileSync(OUTPUT_FILE, JSON.stringify(schema, null, 2)); | ||
| console.log(`Wrote ${OUTPUT_FILE}`); |
@@ -49,2 +49,3 @@ import EventEmitter from 'eventemitter3'; | ||
| version: string; | ||
| runtime: string; | ||
| mode: 'shadow' | 'iframe'; | ||
@@ -60,2 +61,3 @@ webPkg?: any; | ||
| version: string; | ||
| runtime: string; | ||
| [key: string]: any; | ||
@@ -130,2 +132,3 @@ } | ||
| get isWebPlugin(): boolean; | ||
| get installedFromUserWebUrl(): any; | ||
| get layoutCore(): any; | ||
@@ -132,0 +135,0 @@ get isInstalledInLocalDotRoot(): boolean; |
+13
-10
@@ -160,2 +160,3 @@ import * as CSS from 'csstype'; | ||
| title: string; | ||
| fullTitle: string; | ||
| content?: string; | ||
@@ -165,5 +166,5 @@ page: IEntityID; | ||
| updatedAt: number; | ||
| ident?: string; | ||
| properties?: Record<string, any>; | ||
| 'collapsed?': boolean; | ||
| left?: IEntityID; | ||
| anchor?: string; | ||
@@ -238,3 +239,3 @@ body?: any; | ||
| export declare type ExternalCommandType = 'logseq.command/run' | 'logseq.editor/cycle-todo' | 'logseq.editor/down' | 'logseq.editor/up' | 'logseq.editor/expand-block-children' | 'logseq.editor/collapse-block-children' | 'logseq.editor/open-file-in-default-app' | 'logseq.editor/open-file-in-directory' | 'logseq.editor/select-all-blocks' | 'logseq.editor/toggle-open-blocks' | 'logseq.editor/zoom-in' | 'logseq.editor/zoom-out' | 'logseq.editor/indent' | 'logseq.editor/outdent' | 'logseq.editor/copy' | 'logseq.editor/cut' | 'logseq.go/home' | 'logseq.go/journals' | 'logseq.go/keyboard-shortcuts' | 'logseq.go/next-journal' | 'logseq.go/prev-journal' | 'logseq.go/search' | 'logseq.go/tomorrow' | 'logseq.go/backward' | 'logseq.go/forward' | 'logseq.search/re-index' | 'logseq.sidebar/clear' | 'logseq.sidebar/open-today-page' | 'logseq.ui/goto-plugins' | 'logseq.ui/select-theme-color' | 'logseq.ui/toggle-brackets' | 'logseq.ui/toggle-contents' | 'logseq.ui/toggle-document-mode' | 'logseq.ui/toggle-help' | 'logseq.ui/toggle-left-sidebar' | 'logseq.ui/toggle-right-sidebar' | 'logseq.ui/toggle-settings' | 'logseq.ui/toggle-theme' | 'logseq.ui/toggle-wide-mode'; | ||
| export declare type UserProxyTags = 'app' | 'editor' | 'db' | 'git' | 'ui' | 'assets'; | ||
| export declare type UserProxyNSTags = 'app' | 'editor' | 'db' | 'git' | 'ui' | 'assets' | 'utils'; | ||
| export declare type SearchIndiceInitStatus = boolean; | ||
@@ -526,2 +527,3 @@ export declare type SearchBlockItem = { | ||
| newBlockUUID: () => Promise<string>; | ||
| isPageBlock: (block: BlockEntity | PageEntity) => Boolean; | ||
| /** | ||
@@ -537,2 +539,4 @@ * @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-reddit-hot-news | ||
| sibling: boolean; | ||
| start: boolean; | ||
| end: boolean; | ||
| isPageBlock: boolean; | ||
@@ -604,10 +608,4 @@ focus: boolean; | ||
| getProperty: (key: string) => Promise<BlockEntity | null>; | ||
| /** | ||
| * insert or update property entity | ||
| * @param key | ||
| * @param schema | ||
| * @param opts | ||
| */ | ||
| upsertProperty: (key: string, schema?: Partial<{ | ||
| type: 'default' | 'map' | 'number' | 'keyword' | 'node' | 'date' | 'checkbox' | string; | ||
| type: 'default' | 'number' | 'node' | 'date' | 'checkbox' | 'url' | string; | ||
| cardinality: 'many' | 'one'; | ||
@@ -619,6 +617,8 @@ hide: boolean; | ||
| }) => Promise<IEntityID>; | ||
| removeProperty: (key: string) => Promise<void>; | ||
| upsertBlockProperty: (block: BlockIdentity, key: string, value: any) => Promise<void>; | ||
| removeBlockProperty: (block: BlockIdentity, key: string) => Promise<void>; | ||
| getBlockProperty: (block: BlockIdentity, key: string) => Promise<BlockEntity | string | null>; | ||
| getBlockProperty: (block: BlockIdentity, key: string) => Promise<BlockEntity | unknown>; | ||
| getBlockProperties: (block: BlockIdentity) => Promise<Record<string, any> | null>; | ||
| getPageProperties: (page: PageIdentity) => Promise<Record<string, any> | null>; | ||
| scrollToBlockInPage: (pageName: BlockPageName, blockId: BlockIdentity, opts?: { | ||
@@ -708,2 +708,5 @@ replaceState: boolean; | ||
| } | ||
| export interface IUtilsProxy { | ||
| toJs: <R = unknown>(obj: {}) => Promise<R>; | ||
| } | ||
| /** | ||
@@ -710,0 +713,0 @@ * Assets related APIs |
| import { PluginLogger } from './helpers'; | ||
| import { LSPluginCaller } from './LSPlugin.caller'; | ||
| import * as callableAPIs from './callable.apis'; | ||
| import { IAppProxy, IDBProxy, IEditorProxy, ILSPluginUser, LSPluginBaseInfo, LSPluginUserEvents, StyleString, Theme, UIOptions, UIContainerAttrs, SettingSchemaDesc, IUserOffHook, IGitProxy, IUIProxy, UserProxyTags, IAssetsProxy } from './LSPlugin'; | ||
| import { IAppProxy, IDBProxy, IEditorProxy, ILSPluginUser, LSPluginBaseInfo, LSPluginUserEvents, StyleString, Theme, UIOptions, UIContainerAttrs, SettingSchemaDesc, IUserOffHook, IGitProxy, IUIProxy, UserProxyNSTags, IAssetsProxy, IUtilsProxy } from './LSPlugin'; | ||
| import * as CSS from 'csstype'; | ||
@@ -86,14 +86,12 @@ import EventEmitter from 'eventemitter3'; | ||
| */ | ||
| _makeUserProxy(target: any, tag?: UserProxyTags): any; | ||
| _makeUserProxy(target: any, nstag?: UserProxyNSTags): any; | ||
| _execCallableAPIAsync(method: callableMethods, ...args: any[]): Promise<any>; | ||
| _execCallableAPI(method: callableMethods, ...args: any[]): void; | ||
| _callWin(...args: any[]): Promise<any>; | ||
| /** | ||
| * The interface methods of {@link IAppProxy} | ||
| */ | ||
| get App(): IAppProxy; | ||
| get Editor(): IEditorProxy; | ||
| get DB(): IDBProxy; | ||
| get UI(): IUIProxy; | ||
| get Utils(): IUtilsProxy; | ||
| get Git(): IGitProxy; | ||
| get UI(): IUIProxy; | ||
| get Assets(): IAssetsProxy; | ||
@@ -100,0 +98,0 @@ get FileStorage(): LSPluginFileStorage; |
@@ -18,2 +18,9 @@ import { LSPluginUser } from '../LSPlugin.user'; | ||
| }; | ||
| get Utils(): { | ||
| toClj: (input: any) => any; | ||
| jsxToClj: (input: any) => any; | ||
| toJs: (input: any) => any; | ||
| toKeyword: (input: any) => any; | ||
| toSymbol: (input: any) => any; | ||
| }; | ||
| get pluginLocal(): PluginLocal; | ||
@@ -20,0 +27,0 @@ invokeExperMethod(type: string, ...args: Array<any>): any; |
@@ -24,2 +24,13 @@ import { safeSnakeCase } from '../helpers'; | ||
| } | ||
| get Utils() { | ||
| const utils = this.ensureHostScope().logseq.sdk.utils; | ||
| const withCall = (name) => utils[safeSnakeCase(name)]; | ||
| return { | ||
| toClj: withCall('toClj'), | ||
| jsxToClj: withCall('jsxToClj'), | ||
| toJs: withCall('toJs'), | ||
| toKeyword: withCall('toKeyword'), | ||
| toSymbol: withCall('toSymbol') | ||
| }; | ||
| } | ||
| get pluginLocal() { | ||
@@ -66,8 +77,10 @@ return this.ensureHostScope().LSPluginCore.ensurePlugin(this.ctx.baseInfo.id); | ||
| ensureHostScope() { | ||
| if (window === top) { | ||
| try { | ||
| const _ = window.top?.document; | ||
| } | ||
| catch (_e) { | ||
| console.error('Can not access host scope!'); | ||
| return {}; | ||
| } | ||
| return top; | ||
| return window.top; | ||
| } | ||
| } |
+3
-1
| { | ||
| "name": "@logseq/libs", | ||
| "version": "0.2.1", | ||
| "version": "0.2.2", | ||
| "description": "Logseq SDK libraries", | ||
@@ -13,2 +13,3 @@ "main": "dist/lsplugin.user.js", | ||
| "dev:core": "npm run build:core -- --mode development --watch", | ||
| "generate:schema": "node scripts/extract-sdk-schema.js", | ||
| "build": "tsc && rm dist/*.js && npm run build:user", | ||
@@ -31,2 +32,3 @@ "lint": "prettier --check \"src/**/*.{ts, js}\"", | ||
| "devDependencies": { | ||
| "ts-morph": "^22.0.0", | ||
| "@babel/core": "^7.20.2", | ||
@@ -33,0 +35,0 @@ "@babel/preset-env": "^7.20.2", |
+13
-0
@@ -33,1 +33,14 @@ ## @logseq/libs | ||
| https://discord.gg/KpN4eHY | ||
| #### Generate CLJS SDK wrappers | ||
| To regenerate the ClojureScript facade from the JS SDK declarations (keeping the same argument shapes as the JS APIs while auto-converting to/from CLJS data): | ||
| ```bash | ||
| yarn run generate:schema # emits dist/logseq-sdk-schema.json | ||
| bb libs:generate-cljs-sdk # emits logseq/core.cljs and per-proxy files under target/generated-cljs | ||
| ``` | ||
| Non-proxy methods (those defined on `ILSPluginUser`, e.g. `ready`, `provide-ui`) land in `logseq.core`. Each proxy (`IAppProxy`, `IEditorProxy`, ...) is emitted to its own namespace such as `logseq.app` or `logseq.editor`, preserving the original JS argument ordering while automatically bean-converting CLJS data. | ||
| Pass `--out-dir` to change the output location or `--ns-prefix` to pick a different namespace root. |
Sorry, the diff of this file is too big to display
191555
4.13%27
3.85%2593
7.59%46
39.39%17
6.25%