Bladeren bron

expose fn args

Tienson Qin 2 weken geleden
bovenliggende
commit
1d2bdfca21
3 gewijzigde bestanden met toevoegingen van 213 en 96 verwijderingen
  1. 5 4
      libs/README.md
  2. 47 17
      libs/scripts/extract-sdk-schema.js
  3. 161 75
      scripts/src/logseq/libs/sdk_generator.clj

+ 5 - 4
libs/README.md

@@ -33,12 +33,13 @@ https://discord.gg/KpN4eHY
 
 #### Generate CLJS SDK wrappers
 
-To regenerate the ClojureScript facade from the JS SDK declarations:
+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            # writes per-proxy CLJS under target/generated-cljs
+bb libs:generate-cljs-sdk            # emits logseq/core.cljs and per-proxy files under target/generated-cljs
 ```
 
-Each interface is emitted to its own namespace (e.g. `logseq.app`, `logseq.editor`).
-Pass `--out-dir` to change the output directory or `--ns-prefix` to use a different namespace root.
+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.

+ 47 - 17
libs/scripts/extract-sdk-schema.js

@@ -41,7 +41,7 @@ const TARGET_INTERFACES = [
  * cljs-bean when crossing the JS <-> CLJS boundary.
  */
 const BEAN_TO_JS_REGEX =
-  /(Record<|Array<|UIOptions|UIContainerAttrs|StyleString|StyleOptions|object|any|unknown|IHookEvent|BlockEntity|PageEntity|Promise<\s*Record)/i;
+  /(Record<|Array<|Partial<|UIOptions|UIContainerAttrs|StyleString|StyleOptions|object|any|unknown|IHookEvent|BlockEntity|PageEntity|Promise<\s*Record)/i;
 
 const project = new Project({
   compilerOptions: { allowJs: true },
@@ -57,6 +57,7 @@ DECL_FILES.forEach((file) => {
 const schema = {
   generatedAt: new Date().toISOString(),
   interfaces: {},
+  classes: {},
 };
 
 const serializeDoc = (symbol) => {
@@ -114,6 +115,21 @@ const serializeSignature = (sig, memberNode) => {
   };
 };
 
+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) => {
@@ -124,28 +140,42 @@ sourceFiles.forEach((source) => {
 
     const interfaceSymbol = iface.getType().getSymbol();
     const doc = serializeDoc(interfaceSymbol);
-    const methods = iface.getMembers().map((member) => {
-      const symbol = member.getSymbol();
-      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)),
-      };
-    }).filter(Boolean);
+    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 });

+ 161 - 75
scripts/src/logseq/libs/sdk_generator.clj

@@ -6,6 +6,7 @@
 (def default-schema "libs/dist/logseq-sdk-schema.json")
 (def default-output-dir "target/generated-cljs")
 (def default-ns-prefix "logseq")
+(def core-namespace "core")
 
 (defn parse-args
   [args]
@@ -16,8 +17,8 @@
       (let [[flag value & more] tokens]
         (case flag
           "--schema" (recur (assoc opts :schema value) more)
-          "--out" (recur (assoc opts :out-dir value) more)
           "--out-dir" (recur (assoc opts :out-dir value) more)
+          "--out" (recur (assoc opts :out-dir value) more)
           "--ns-prefix" (recur (assoc opts :ns-prefix value) more)
           (throw (ex-info (str "Unknown flag: " flag) {:flag flag})))))))
 
@@ -34,84 +35,160 @@
       (string/replace #"^I" "")
       (string/replace #"Proxy$" "")))
 
-(defn interface->suffix [iface-name]
-  (-> iface-name
-      interface->target
-      camel->kebab))
+(defn interface->namespace [ns-prefix iface-name]
+  (str ns-prefix "." (camel->kebab (interface->target iface-name))))
 
-(defn interface->namespace [iface-name ns-prefix]
-  (prn :debug :iface-name iface-name
-       :ns-prefix ns-prefix
-       :suffix (interface->suffix iface-name))
-  (str ns-prefix "." (interface->suffix iface-name)))
+(defn getter->interface-name [return-type]
+  (some->> (re-find #"\.(I[A-Za-z0-9]+)" return-type)
+           second))
+
+(defn iface-key->string [k]
+  (cond
+    (string? k) k
+    (keyword? k) (name k)
+    :else (str k)))
 
 (defn format-docstring [doc]
   (when (and doc (not (string/blank? doc)))
     (str "  " (pr-str doc) "\n")))
 
-(defn bean-specs [params]
-  (when (seq params)
-    (vec
-     (map (fn [{:keys [beanToJs rest]}]
-            (let [spec (cond-> {}
-                         beanToJs (assoc :bean-to-js true)
-                         rest (assoc :rest true))]
-              spec))
-          params))))
-
-(defn rest-index [params]
-  (some (fn [[idx {:keys [rest]}]]
-          (when rest idx))
-        (map-indexed vector params)))
-
-(defn select-primary-signature [signatures]
-  (when (seq signatures)
-    (apply max-key #(count (:parameters %)) signatures)))
+(defn param->info
+  [{:keys [name optional rest beanToJs rest?]}]
+  (let [sym (camel->kebab name)
+        spec (cond-> {}
+               beanToJs (assoc :bean-to-js true))]
+    {:name name
+     :sym sym
+     :optional (boolean optional)
+     :rest (boolean (or rest rest?))
+     :spec (when (seq spec) spec)}))
+
+(defn emit-convert-binding [convert-sym {:keys [sym spec]}]
+  (if spec
+    (str "        arg-" sym " (" convert-sym " " (pr-str spec) " " sym ")\n")
+    (str "        arg-" sym " " sym "\n")))
+
+(defn emit-rest-binding [convert-sym {:keys [sym spec]}]
+  (let [rest-var (str "rest-" sym)
+        line (if spec
+               (str "        " rest-var " (map #(" convert-sym " " (pr-str spec) " %) " sym "))\n")
+               (str "        " rest-var " (vec " sym ")\n"))]
+    {:binding line
+     :var rest-var}))
+
+(defn format-param-vector [params]
+  (str "[" (string/join " " params) "]"))
+
+(defn emit-method-body
+  [owner-expr method-name params {:keys [convert call]}]
+  (let [rest-param (some #(when (:rest %) %) params)
+        fixed-params (if rest-param (vec (remove :rest params)) params)
+        convert-lines (map #(emit-convert-binding convert %) fixed-params)
+        {:keys [binding var]} (when rest-param (emit-rest-binding convert rest-param))
+        rest-lines (if binding [binding] [])
+        arg-syms (map #(str "arg-" (:sym %)) fixed-params)
+        args-expr (if rest-param
+                    (str "(into [" (string/join " " arg-syms) "] " var ")")
+                    (str "[" (string/join " " arg-syms) "]"))]
+    (str "  (let [owner " owner-expr "\n"
+         "        method (.-" method-name " owner)\n"
+         (apply str convert-lines)
+         (apply str rest-lines)
+         "        args " args-expr "]\n"
+         "    (" call " owner method args)))\n")))
+
+(defn emit-optional-def
+  [fn-name doc-str params impl-name owner-expr helpers method-name]
+  (let [required (take-while (complement :optional) params)
+        total (count params)
+        param-syms (map :sym params)
+        arities (range (count required) (inc total))
+        header (str "\n(defn " fn-name "\n"
+                    (or doc-str ""))]
+    (str "\n(defn- " impl-name "\n"
+         "  " (format-param-vector param-syms) "\n"
+         (emit-method-body owner-expr method-name params helpers)
+         header
+         (apply str
+                (for [arity arities
+                      :let [provided (take arity param-syms)
+                            missing (- total arity)
+                            call-args (concat provided (repeat missing "nil"))
+                            param-vector (format-param-vector provided)
+                            call-arg-str (string/join " " call-args)
+                            call-arg-str (if (string/blank? call-arg-str) "" (str " " call-arg-str))]]
+                  (str "  (" param-vector "\n"
+                       "  (" impl-name call-arg-str "))")))
+         ")\n")))
 
 (defn emit-method
   [{:keys [name documentation signatures]}
-   iface-name]
-  (let [{:keys [parameters]} (select-primary-signature signatures)
-        specs (bean-specs parameters)
-        rest-idx (rest-index parameters)
+   owner-expr helpers]
+  (let [{:keys [parameters]} (apply max-key #(count (:parameters %)) signatures)
+        params (map param->info parameters)
         fn-name (camel->kebab name)
-        owner-prop (interface->target iface-name)
-        js-prop (str ".-" name)]
-    (str "\n"
-         "(defn " fn-name "\n"
-         (or (format-docstring documentation) "")
-         "  [& args]\n"
-         "  (let [owner  (.-" owner-prop " js/logseq)\n"
-         "        method (" js-prop " owner)\n"
-         "        specs  " (pr-str specs) "\n"
-         "        rest-idx " (if (number? rest-idx) rest-idx "nil") "]\n"
-         "    (call-proxy owner method specs rest-idx args)))\n")))
-
-(defn emit-namespace
-  [iface-name iface ns-prefix]
-  (let [ns (interface->namespace iface-name ns-prefix)
+        doc-str (format-docstring documentation)
+        rest-param (some #(when (:rest %) %) params)
+        optional-params (filter :optional params)
+        impl-name (str fn-name "-impl")
+        method-body (emit-method-body owner-expr name params helpers)]
+    (cond
+      rest-param
+      (let [fixed-syms (map :sym (vec (remove :rest params)))
+            param-vector (format-param-vector (concat fixed-syms ["&" (:sym rest-param)]))]
+        (str "\n(defn " fn-name "\n"
+             (or doc-str "")
+             "  " param-vector "\n"
+             method-body))
+
+      (seq optional-params)
+      (emit-optional-def fn-name doc-str params impl-name owner-expr helpers name)
+
+      :else
+      (let [param-vector (format-param-vector (map :sym params))]
+        (str "\n(defn " fn-name "\n"
+             (or doc-str "")
+             "  " param-vector "\n"
+             method-body)))))
+
+(defn emit-core-namespace
+  [ns-prefix {:keys [methods]}]
+  (let [ns (str ns-prefix "." core-namespace)
         header (str ";; Auto-generated via `bb libs:generate-cljs-sdk`\n"
                     "(ns " ns "\n"
                     "  (:require [cljs-bean.core :as bean]))\n\n"
-                    "(defn- convert-args [specs rest-idx args]\n"
-                    "  (if (seq specs)\n"
-                    "    (map-indexed\n"
-                    "      (fn [idx arg]\n"
-                    "        (let [spec (if (and rest-idx (>= idx rest-idx))\n"
-                    "                       (nth specs rest-idx nil)\n"
-                    "                       (nth specs idx nil))]\n"
-                    "          (if (and spec (:bean-to-js spec))\n"
-                    "            (bean/->js arg)\n"
-                    "            arg)))\n"
-                    "      args)\n"
-                    "    args))\n\n"
-                    "(defn- call-proxy [owner method specs rest-idx args]\n"
+                    "(defn convert-arg [spec value]\n"
+                    "  (cond\n"
+                    "    (nil? spec) value\n"
+                    "    (identical? value js/undefined) value\n"
+                    "    (:bean-to-js spec) (bean/->js value)\n"
+                    "    :else value))\n\n"
+                    "(defn- normalize-result [result]\n"
+                    "  (if (instance? js/Promise result)\n"
+                    "    (.then result (fn [value] (normalize-result value)))\n"
+                    "    (bean/->clj result)))\n\n"
+                    "(defn call-method [owner method args]\n"
                     "  (when-not method\n"
                     "    (throw (js/Error. \"Missing method on logseq namespace\")))\n"
-                    "  (let [converted (convert-args specs rest-idx args)]\n"
-                    "    (.apply method owner (to-array (vec converted)))))\n")
+                    "  (normalize-result (.apply method owner (to-array args))))\n")
+        helpers {:convert "convert-arg"
+                 :call "call-method"}
+        methods-str (->> methods
+                         (map #(emit-method % "js/logseq" helpers))
+                         (apply str))]
+    [ns (str header methods-str)]))
+
+(defn emit-proxy-namespace
+  [ns-prefix iface-name iface]
+  (let [ns (interface->namespace ns-prefix iface-name)
+        owner-expr (str "(.-" (interface->target iface-name) " js/logseq)")
+        header (str ";; Auto-generated via `bb libs:generate-cljs-sdk`\n"
+                    "(ns " ns "\n"
+                    "  (:require [logseq.core :as core]))\n")
+        helpers {:convert "core/convert-arg"
+                 :call "core/call-method"}
         methods-str (->> (:methods iface)
-                         (map #(emit-method % iface-name))
+                         (map #(emit-method % owner-expr helpers))
                          (apply str))]
     [ns (str header methods-str)]))
 
@@ -122,8 +199,7 @@
         file-name (str (last parts) ".cljs")]
     (apply fs/path out-dir (concat dir-parts [file-name]))))
 
-(defn ensure-schema!
-  [schema-path]
+(defn ensure-schema! [schema-path]
   (when-not (fs/exists? schema-path)
     (throw (ex-info (str "Schema not found, run `yarn --cwd libs generate:schema` first: " schema-path)
                     {:schema schema-path}))))
@@ -136,22 +212,32 @@
       (spit (str file) content)
       (println "Generated" (str file)))))
 
-(defn generate!
-  ([] (generate! {}))
+(defn run!
+  ([] (run! {}))
   ([opts]
    (let [schema-path (fs/absolutize (or (:schema opts) default-schema))
-         out-dir     (fs/absolutize (or (:out-dir opts) default-output-dir))
-         ns-prefix   (or (:ns-prefix opts) default-ns-prefix)]
+         out-dir (fs/absolutize (or (:out-dir opts) default-output-dir))
+         ns-prefix (or (:ns-prefix opts) default-ns-prefix)]
      (ensure-schema! schema-path)
      (let [schema (json/parse-string (slurp (str schema-path)) true)
-           namespaces (map (fn [[iface-name iface]]
-                             (emit-namespace (name iface-name) iface ns-prefix))
-                           (:interfaces schema))]
+           interfaces (:interfaces schema)
+           ls-user (get-in schema [:classes :LSPluginUser])
+           _ (when-not ls-user
+               (throw (ex-info "Missing LSPluginUser metadata in schema" {:schema schema-path})))
+           getters (:getters ls-user)
+           proxy-names (->> getters
+                            (keep #(some-> (getter->interface-name (:returnType %)) keyword))
+                            distinct)
+           proxies (for [iface-key proxy-names
+                         :let [iface (get interfaces iface-key)]
+                         :when iface]
+                     (emit-proxy-namespace ns-prefix (iface-key->string iface-key) iface))
+           core (emit-core-namespace ns-prefix ls-user)
+           namespaces (cons core proxies)]
        (fs/create-dirs out-dir)
        (write-namespaces! out-dir namespaces)
        out-dir))))
 
-(defn -main
-  [& args]
+(defn -main [& args]
   (let [opts (parse-args args)]
-    (generate! opts)))
+    (run! opts)))