squint-cljs / squint

Light-weight ClojureScript dialect
https://squint-cljs.github.io/squint
587 stars 35 forks source link

Resolve symbolic namespaces via API #494

Open martinklepsch opened 3 months ago

martinklepsch commented 3 months ago

To upvote this issue, give it a thumbs up. See this list for the most upvoted issues.

Is your feature request related to a problem? Please describe.

I'm using squint with vite as a build tool and I'd like to explore how I could integrate library dependencies like promesa into squint. Currently when requiring a namespace it just shows up as a verbatim string which makes it difficult to implement custom resolver logic.

Describe the solution you'd like

Something like this would be great, the prefix could be configurable but also a static prefix would be enough.

(require '[foo.bar :as foo])
import foo from "cljs-ns:foo.bar";

Additional context

Here's a basic Rollup plugin using resolveId

Rollup is the bundler behind vite and supports emitting code for the browser, node etc.

The below can be used as an entry in the plugin array in vite's config.

    {
      name: 'resolve-cljs-ns',
      resolveId(source) {
        if (source.startsWith("cljs-ns:")) {
          console.log("id", source)
          // find ns in node_modules or other places
          return {
            id: "src/foo/bar.cljs"
          }
        }
      }
    }
borkdude commented 3 months ago

Although we've discussed this, I think this also falls into the category of proposing a solution rather than stating a problem. I think the issue is: when squint compiles a file, should compileString already resolve symbolic namespaces to files (e.g. foo.bar becomes ../foo/bar.cljs or ../foo/bar.cljc etc such that the plugin won't have to implement extra logic to resolve files or should this logic be in the plugin (to which the above proposed solution is but one detail).

I think I prefer to have this logic as much as possible in one place such that efforts shouldn't have to be duplicated. This logic already exists for the squint CLI + squint.edn approach and maybe it would be interesting to see if we could lift this logic into compileString somehow such that the plugin can also benefit from this.

martinklepsch commented 3 months ago

Fair point, let me try to get more at the underlying problem:

A bit of context

These days many tools operate on ESM module graphs, they use them to create bundles, transpile files as they are imported etc. Tools like Vite and Rollup allow you to tell them how to resolve the right side of an import statement. For example vite-plugin-svgr can turn SVGs into React components:

import Logo from "./logo.svg?react";

In this case the plugin can tell Vite that it knows how to resolve modules ending with .svg?react and automatically compiles them to JS modules containing a React component on the fly.

Why does this matter for Squint?

Currently to resolve namespaces we have squint.edn which tells the compiler where to look for files. But in the case of tools like Vite it's not actually squint that does the lookup and instead it is Vite. It would be great to be able to teach Vite how to look up Squint namespaces.

Right now a Vite plugin could intercept module resolution but with namespaces just being compiled to plain strings we'd need to employ heuristics to determine if a given string is in fact a namespace.

(require [foo.bar :as foo])
import foo from "foo.bar";

The JS doesn't give a clear indication that foo.bar is a namespace making it difficult for Vite to actually find the respective .cljs file and compile it.

Other notes

  1. The Vite plugin could internally still use :paths from squint.edn to know about directories it should scan for namespaces.
  2. The prefix wouldn't actually need to be configurable, anything that just clearly says "this is a namespace" would be fine.
  3. It would be fine if this is an undocumented API for now as regular users don't need to care about this.

it would be interesting to see if we could lift this logic into compileString somehow such that the plugin can also benefit from this.

I guess compileString could also transform symbolic namespaces to filepaths so the above example would become something like

import foo from "./src/foo/bar.cljs";

but then that isn't really runnable JS since the cljs file still needs to be compiled / the import needs to point at JS. But for the purpose of the Vite plugin this could work.

martinklepsch commented 3 months ago

Another thought: Maybe the API could also be to pass the contents of squint.edn to compileString. That way the APIs would be reachable from both the CLI (with squint.edn implicitly read) and JS, where the data could either be read from squint.edn or provided raw.

borkdude commented 3 months ago

But in the case of tools like Vite it's not actually squint that does the lookup and instead it is Vite. It would be great to be able to teach Vite how to look up Squint namespaces.

squint could still resolve those symbols to e.g. "./foo.cljs" using the same mechanism that is now used in the squint CLI and then vite would work exactly the same as when you would manually write that.

borkdude commented 3 months ago

This is more in line with your second post I think, but the API could also just read squint.edn itself. Basically we need programmatic access to compileString not from the lower level API but what is used by the CLI and hook that up to the plugin, then we're all set I think.

borkdude commented 3 months ago

That would also then enable using macros

borkdude commented 3 months ago

Similar discussion as #500

I think if we would expose this function: https://github.com/squint-cljs/squint/blob/9d2415394be87832658f4a278761b19be1f520e0/src/squint/internal/cli.cljs#L66-L104

or this one:

https://github.com/squint-cljs/squint/blob/9d2415394be87832658f4a278761b19be1f520e0/src/squint/compiler/node.cljs#L86

or perhaps a slight variation on both

then we would have both macro and symbolic namespace support