Skip to content

About

Merges a Clojure function and its transitive dependencies into one standalone, single-segment namespace with no :require and no (require ...) — a single file you can load-file anywhere bare Clojure runs

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

no-require

Merges a Clojure function and its transitive dependencies into one standalone, single-segment namespace with no :require and no (require ...) — a single file you can load-file anywhere bare Clojure runs.

clojure -M:run -d /path/to/project my.app.core/process
# => out/process.clj  with (ns process) and everything `process` needs, inlined

Usage

clojure -M:run [opts] <ns/fn>

  -o, --out FILE            Output file (default out/<fn-name>.clj)
  -n, --out-ns NAME         Output namespace (single segment; default fn name)
  -p, --path PATH           Source path relative to project dir (repeatable; default src)
  -d, --project-dir DIR     Project root of the entry fn (default .)
      --full-classpath      Lint the project's full classpath — inlines library fns too
      --inline-preloaded    Inline clojure.string/walk/edn/java.io instead of relying on boot preload
      --force-include VAR   Extra ns/var to include (repeatable; for dynamic loading the
                            static analysis can't see)
      --[no-]verify         Sandbox load check (default on)
  -l, --llm                 LLM-assisted mode (see below)
  -m, --llm-model MODEL     Model passed to `llm -m`
      --max-fix-iters N     Max LLM iterations per loop (default 4)

Exit 0 when nothing is unresolved and the sandbox load check passes. The report (EDN) lists renames, dynamic rewrites, unresolved vars, and warnings.

How it works

  1. Analysis — clj-kondo over the project paths plus the Clojure jar (--full-classpath to cover third-party libraries; their sources are read straight out of the jars).
  2. Closure — transitive var-usage walk from the entry fn. defmethod and extend-protocol/extend-type forms ride along with their defmulti/defprotocol. Only vars actually reachable are emitted — unused siblings never appear.
  3. Dynamic loading — literal (requiring-resolve 'ns/fn) / (resolve 'ns/fn) calls are detected: the target is pulled into the closure and the call site is rewritten to a direct #'fn reference. Non-literal dynamic loads produce a warning; combine --force-include (static) or --llm (sandbox probing) to resolve them.
  4. Rewrite — collisions renamed (<ns-segment>-<name>; names that clash with clojure.core are always renamed), aliased/refered symbols fully qualified, ::kw / ::alias/kw auto-resolved keywords frozen to their original namespace, :import list rebuilt from classes actually used.
  5. Order — topological sort of forms; mutual recursion gets a (declare ...) block, macros are ordered before their uses.
  6. Verify — the bundle is load-filed in .no-require/sandbox/ against bare Clojure (-Srepro, no deps). That check is the no-require guarantee.

Namespaces preloaded by clojure.main boot (clojure.string, clojure.walk, clojure.edn, clojure.java.io, verified against 1.12) stay fully-qualified instead of inlined — shorter output, still loads without :require. Use --inline-preloaded if the bundle must survive exotic boot paths (AOT, Graal).

LLM mode (--llm)

Drives the llm CLI with a restricted toolbox (read_file, write_file, read_project_file, list_project_files, run_clojure — all confined to the sandbox / read-only project) via llm --functions:

  1. Fix loop — if the sandbox load fails (e.g. input-dependent dynamic requires), the LLM reads the error, inlines what's missing from the original sources, and re-runs until the bundle loads. Successful fixes are synced back to the output file.
  2. Tests — writes bundle_test.clj in the sandbox: clojure.test unit tests for the entry fn, reusing existing project tests when found; a clojure.alpha.spec (spec-alpha2) fdef; and, when the specs are generatable, a test.check defspec property — the automatically derivable one being equivalence: (bundled x) = (original x) for generated inputs (the original project is on the sandbox classpath under the :orig alias).
  3. Test failures loop back through llm -c until green or --max-fix-iters.

Limitations

  • .clj / JVM only (no cljs, no reader conditionals).
  • AOT-only dependencies (no source on the classpath) and Java class dependencies from libraries can't be inlined — they surface as :unresolved.
  • (require ...) with computed arguments, eval, intern are reported, not solved statically; use --force-include or --llm. A surviving non-literal resolve/requiring-resolve also fails the :forbidden artifact scan.
  • Renaming a colliding defrecord/deftype constructor is not supported (the record keeps its name; collisions between record names abort at load).
  • defmethod/extend-* on clojure.core or preloaded targets (print-method, Datafiable, ...) ride along only when the form mentions an inlined record/type; keyword-dispatch or java-class-only forms are dropped silently.
  • Top-level side effects other than var definitions, defmethod, and extend-* forms (e.g. bare (swap! registry ...) calls, s/def registrations) are not carried over — they are reported as :side-effect-form-skipped warnings.
  • "Preloaded" namespaces assume the bundle is loaded via clojure.main boot.

Dev

clojure -M:test        # unit + fixture E2E (generates, then runs the bundle against bare Clojure)

About

Merges a Clojure function and its transitive dependencies into one standalone, single-segment namespace with no :require and no (require ...) — a single file you can load-file anywhere bare Clojure runs

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages