Skip to content

Scratch Design

Ferdinand Beyer edited this page Oct 16, 2022 · 5 revisions

Problem Statement

ClojureScript programs often need to consume JavaScript APIs, e.g. Node's APIs when writing Node scripts or libraries or the VS-Code API when writing extensions.

Currently there is little editor support, as Clojure tools such as Clojure-LSP know nothing about the JavaScript APIs.

Some interop pain can be lessened with libraries such as cljs-bean or js-interop, but still they don't provide any help with finding out how to use an API.

However, almost every JavaScript API out there provides TypeScript type information in form of .d.ts files.

Dots aims to make use of this information source to provide API support for ClojureScript programmers.

Solution

We could integrate with tools such as Calva or Clojure-LSP to look for TypeScript declarations for required JavaScript modules, and provide features like completion, jump to definition, etc.

We might also be able write our own Language Server that augments Clojure-LSP and provides the missing pieces.

However, this would still require programmers to take care of JavaScript interop for every call, and limit Dots' use to users of these tools.

Alternatively, we build Dots as a code generator for ClojureScript code that wraps the JavaScript API and provides docstrings and type hints from TypeScript. As this is plain old ClojureScript, it should work in all existing tooling, including API doc generators.

How to parse TypeScript

  • Write our own parser
  • Use some third-party grammar such as the one for ANTLR
  • Use the TypeScript compiler directly
    • Using the Compiler API from ClojureScript
    • Using the Language Service API from ClojureScript
    • Using the TypeScript server

We could also create a thin abstraction layer that uses the TypeScript server for Clojure and the API directly for ClojureScript.

General operation mode

  • Set up a node environment, e.g. by installing @types modules for the API we want to process.
  • Create a TypeScript program (or just the Compiler Host), optionally configuring or hooking in options and resolution strategies.
    • This is a fake program as we don't plan on providing our own typescript sources, but we want to use it to resolve modules.
    • We might support tsconfig.json here.
    • We could provide config and/or options from Clojure via clj->js, as they should be JSON-serializable and thus don't contain exotic types?
  • Ask the host to resolve the module we are interested in
  • Walk the AST and track exported symbols (see below)
  • Use the TypeChecker to obtain polished information about symbols
  • Create a Clojure representation of the artifacts to create for this symbol
  • Emit ClojureScript namespaces and vars from the internal representation

What to generate

We create ClojureScript files representing namespaces and containing vars that correspond to TypeScript modules/namespaces and symbols.

Option to follow types.

Variable

Getter, setter

export const version: string;
(defn version [] vscode/version)

Function

  • Just a def with metadata?
export function getSession(providerId: string, scopes: readonly string[], options: AuthenticationGetSessionOptions & { createIfNone: true }): Thenable<AuthenticationSession>;
(def get-session
  "Get an authentication session matching ..."
  {:arglists ([provider-id scopes options])}
  (.-getSession vscode/authentication))

Interface

  • Functions to read/write properties?
  • For properties with callable type: functions
  • Need to watch out: Methods can be nullable! methodName?(<params>)
export interface Event<T> {
    (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]): Disposable;
}

export interface FileSystemWatcher extends Disposable {
    // ...
    readonly ignoreChangeEvents: boolean;
    // ...
    readonly onDidChange: Event<Uri>;
    // ...
}
(ns file-system-watcher)

(defn ignore-change-events? ^boolean [^js file-system-watcher]
  (.-ignoreChangeEvents file-system-watcher))

(defn on-did-change
  (^js [^js file-system-watcher listener]
   (.onDidChange file-system-watcher listener))
  (^js [^js file-system-watcher listener this-args]
   (.onDidChange file-system-watcher listener this-args))
  (^js [^js file-system-watcher listener this-args disposables]
   (.onDidChange file-system-watcher listener this-args disposables)))
  • Constructor
export interface Command {
    title: string;
    command: string;
    tooltip?: string;
    arguments?: any[];
}
(defn ->Command
  ([title command]
   #js {:title title, :command command})
  ([title command tooltip]
   #js {,,,})
  ([title command tooltip arguments]
   #js {,,,}))

(defn map->Command [{:keys [title command ?tooltip ?arguments]}]
  (let [ret #js {:title title, :command command}]
    (when (some? ?tooltip) (set! (.-tooltip ret) ?tooltip)
    (when (some? ?arguments) (set! (.-arguments ret) ?arguments))))
    ret)

Class

Like interface, but with class constructor.

export class Position {
    readonly line: number;
    readonly character: number;

    constructor(line: number, character: number);
}
(defn line [^js position]
  (.-line position))

(defn ->Position [line character]
  (vscode/Position. line character))

Enum

export enum TextEditorSelectionChangeKind {
    Keyboard = 1,
    Mouse = 2,
    Command = 3
}
(def text-editor-selection-change-kind-keyboard 1)
(def text-editor-selection-change-kind-mouse 2)
(def text-editor-selection-change-kind-command 3)

Namespace (Module)

Merge into current or create separate namespace.

Type Alias

Create constructors.

export type GlobPattern = string | RelativePattern;
(def ->GlobPattern
  [string-or-relative-pattern]
  identity)

For object types (type Foo = {...}), create the same stuff as for interfaces.

Alias

Just create a (def alias referenced)? Or generate whatever we would if the target would be defined inline?

Emitting

  • Produce rewrite-clj nodes
  • Run cljfmt.core/reformat-form on the file AST to apply idiomatic formatting