bsene/skills

clojurescript

Write, review, debug, and configure ClojureScript code and projects.

查看源码
仓库原始内容

按源仓库内容呈现,保留标题、案例、代码、表格、链接以及原文引用的演示图片。

ClojureScript

ClojureScript (CLJS) is Clojure that compiles to JavaScript via the Google Closure Compiler. It shares Clojure's syntax, immutable data structures, and REPL-driven workflow, but runs on JS engines (browser, Node.js) and interoperates directly with JS objects, functions, and npm/Closure libraries.

This skill covers writing idiomatic CLJS, JS interop, dependency/build configuration, and the newer native async/await support. When in doubt about compiler flags or interop mechanics, check references/ rather than guessing — CLJS has a lot of build-tooling surface area that's easy to get subtly wrong.

Not for nbb. Babashka/nbb runs CLJS through an SCI interpreter (no Google Closure Compiler, no :optimizations/:advanced, no externs) for fast-starting Node.js scripts — a different language surface from what this skill covers. nbb is out of scope here — consult nbb's docs and examples for nbb.edn projects, anything run via nbb script.cljs/npx nbb.

Gotchas

  • `:optimizations` defaults to `:none`, not :advanced — a project with no explicit :optimizations setting is running unoptimized dev output.
  • Symbol renaming under `:advanced` breaks untyped JS interop. "Works in dev, breaks in prod" is almost always this — fix with an externs file or string-keyed access (goog.object/get), not by changing app logic.
  • `:main` under `:optimizations :none` only loads what's actually `:require`d. A side-effect-only namespace not reached by the entry namespace's transitive requires silently doesn't run — add an explicit :require or put it in :preloads.
  • `:output-to` and `:modules` are mutually exclusive, not layered — :modules needs a per-module :output-to inside each module map, not a top-level one.
  • No arbitrary-precision numbers. CLJS numbers are all JS number (a double) — no JVM-style BigDecimal/BigInt/ratios. Watch for precision loss on large integers.
  • `^:async` metadata goes on the `fn`/name only, never the arg vector, and await only works inside a function actually marked :async — a nested fn doesn't inherit it and needs its own ^:async (see references/async-functions.md).
  • An `:async` function always returns a `Promise`, even when the body looks like it returns a plain value.
  • Truthiness differs from JS. Only false and nil are falsy — 0, "", js/NaN, [], and (array) are all truthy. (if 0 "yes" "no")"yes", unlike JS. Easy to get backwards when translating JS/TS conditionals.
  • No automatic tail-call optimization, same as JVM Clojure. CLJS functions run as ordinary JS function calls, and most JS engines don't reliably implement proper tail calls either — deep non-tail self-recursion will blow the JS call stack on large/unbounded input, even though it "works" for small test inputs. Reach for recur (self-recursion, tail position only — see below), trampoline (mutual recursion), or wrap the recursive case in lazy-seq (sequence-producing recursion) rather than plain recursive calls whenever the input size isn't small and fixed.

Core language cheat sheet

CLJS syntax is essentially Clojure's, so standard Clojure knowledge (immutable persistent data structures, let/fn/defn, ->/->> threading, map/filter/reduce, multimethods, protocols, core.async) applies directly. For a categorized index of core-library functions when you need the exact name of something less common, see references/core-function-index.md (based on https://cljs.info/cheatsheet/). The CLJS-specific things to keep front of mind:

  • Numbers: CLJS has only one numeric type backed by JS number (a double). No arbitrary-precision integers/ratios like JVM Clojure — watch for precision issues with large integers.
  • Namespaces map to JS modules/Closure namespaces. (ns my.app.core (:require [my.app.util :as u])):require for CLJS/Closure namespaces, :import for Closure classes/enums (e.g. (:import [goog Timer])).
  • `defprotocol`/`deftype`/`defrecord` work like Clojure and compile to real JS constructor functions/prototypes.
  • `js/` prefix accesses JS global objects: js/console, js/document, js/Math, js/window.
  • No JVM interop — anything JVM-specific (Thread, java.* classes, blocking IO) doesn't exist. Concurrency is single-threaded/event-loop based; use core.async channels or promises/async functions instead of threads or locks.
  • `.cljc` files hold code shared between Clojure and ClojureScript, guarded with reader conditionals: #?(:clj (do-jvm-thing) :cljs (do-js-thing)).

JavaScript interop

  • Method call: (.methodName obj arg1 arg2)obj.methodName(arg1, arg2)
  • Property access: (.-propName obj)obj.propName
  • Property set: (set! (.-propName obj) val)
  • Constructor: (SomeClass. arg1 arg2)new SomeClass(arg1, arg2)
  • Static/global access: js/JSON.stringify, js/Object.keys
  • Chained calls read top-down with ..: (.. js/document (getElementById "app") -innerHTML)
  • JS↔CLJS data conversion: (clj->js m) and (js->clj o :keywordize-keys true)
  • Destructuring JS objects: (let [{:keys [a b]} (js->clj obj :keywordize-keys true)] ...) or interop-style ^js type hints with .- accessors when you don't want a full conversion.

For consuming Closure Library, npm packages, foreign (non-Closure-compatible) JS, and CLJS libraries — including externs, advanced-compilation pitfalls, and CLJSJS — see references/dependencies-and-interop.md before guessing at a :require/:import shape or writing an externs file from scratch.

Recursion

Because CLJS has no automatic TCO (see Gotchas), which recursion tool to reach for depends on the shape of the problem:

  • `recur` — self-recursion only (a function calling itself, directly, not through an intermediate function), and only from the tail position. Compiles to a real loop, so it's stack-safe for arbitrarily large input. This is the default choice for producing a scalar or a small, fixed-size result — e.g. summing a collection with loop/recur rather than a plain recursive call.
  • `trampoline` — for mutual recursion (function A calls B calls A), where recur doesn't apply because it only optimizes self-calls. Wrap each recursive tail call in #(...) and drive the whole thing with (trampoline f args...); trampoline keeps calling the returned function until it gets back a non-function value. Don't reach for trampoline on a self-recursion — recur is simpler and equally stack-safe there.
  • `lazy-seq` — for recursion that produces a sequence, especially an unbounded/large one. Wrapping the recursive call in lazy-seq breaks the recursion into on-demand steps, so callers only pay for the elements they actually realize (take, first, etc.). This is usually the better fit over loop/recur whenever the output size varies or could be large — let take/drop on the caller side decide how much to compute.
  • `memoize` — trades space for time by caching by argument, useful for expensive recursive/mutually-recursive definitions with overlapping subproblems. Caveat: memoizing alone doesn't fix a stack-overflow risk — a cold cache still has to recurse all the way down on the first large call. If the recursion is deep, build the cache bottom-up by mapping the memoized function over a lazy range/seq ((map f (range))) rather than calling it directly on a large n.

Async / promises

CLJS added native ^:async functions with an await macro (since v1.12.145) as a lighter-weight alternative to core.async for promise-based code. This is a newer feature that's easy to get wrong or not know about at all — read `references/async-functions.md` before writing or reviewing any CLJS code that deals with `Promise`s, `fetch`, or `async`/`await`-shaped logic (see Gotchas above for the two most common mistakes).

The single most common typo — ^:async sits on the function name, never the argument vector:

`clojure
(defn ^:async load-user [id] (await (fetch-user id)))  ; ✅ name
(defn load-user ^:async [id] ...)                      ; ❌ arg vector — never

If the codebase already uses core.async (go, <!, channels) for async control flow, stay consistent with that style rather than mixing in ^:async/await unless asked to migrate.

External resources

For idiomatic-Clojure background/mindset reading (design patterns translated to Clojure idioms, core.async worked examples, FP mindset shifts) beyond this skill's own reference material, plus community-curated library lists and real-world example codebases, see references/external-resources.md. Use it as supplementary color/perspective and pointers, not as the source of truth for CLJS-specific mechanics — those stay in this skill's other reference files.

Build & compiler configuration

CLJS projects are almost always built with shadow-cljs (npm/JS-ecosystem friendly, handles npm deps automatically) or the Clojure CLI (`deps.edn`) + `cljs.main`/`cljs-build-api` combo; some older projects use Leiningen + figwheel-main. Don't assume the tool — check for shadow-cljs.edn, deps.edn, or project.clj in the project before giving setup instructions.

For the full list of compiler options (:optimizations, :target, :main, :npm-deps, :modules, :source-map, :closure-defines, warnings, etc.) with exact semantics and defaults, see references/compiler-options.md. Don't guess at option names or defaults — this is a large, easy-to-misremember surface (e.g. :static-fns defaults to false except under :advanced, :npm-deps defaults to false). See Gotchas above for the most common failure patterns tied to these options.

Working style

Default to idiomatic Clojure(Script), not a literal JS translation. This is the top priority whenever writing or rewriting CLJS, and it should win over "closest to what the JS/TS looked like" unless the user's existing codebase clearly does otherwise. Concretely, that means:

  • Immutability and pure functions first. Reach for persistent data structures (maps, vectors, sets) and pure transformations over mutable state, atoms, or set!. Only use an atom/volatile! for genuine local mutable state (e.g. component-local UI state), not as a default variable substitute.
  • Data over classes. Model domain data as plain maps/records rather than JS-style classes with methods, unless polymorphism genuinely calls for defprotocol/deftype/defrecord or a JS class is required at an interop boundary (e.g. a React component).
  • Sequence/threading idioms over loops. Prefer map/filter/reduce/keep/for and ->/->> threading over for/while-style imperative loops, index juggling, or manual accumulation. This is a style preference, not a stack-safety rule: loop/recur is the correct (not just a perf-justified) choice for genuinely tail-recursive, scalar-producing logic — see Recursion above — reach for a transient only when profiling justifies it.
  • Destructuring over manual accessors. Use let/fn-arg destructuring ({:keys [...]}, [a b & rest]) instead of repeated (get m :k) or positional indexing.
  • Small composed functions over long procedural bodies. Break logic into small, named, testable functions and compose them, rather than one large function with sequential mutation-heavy steps.
  • Idiomatic control flow. Prefer cond/case/condp and when/if-let/when-let over nested ifs or JS-style early-return chains; prefer ^:async/await or core.async (per the async section above) over manual .then chains.
  • Namespaced keywords and data-driven design where they fit naturally (e.g. :person/name in shared/library code) rather than plain strings for map keys.

When converting JS/TS to CLJS specifically: re-derive the idiomatic CLJS shape of the logic rather than transliterating line-by-line — e.g. turn .then chains into ^:async/await or core.async, turn classes into defrecord/protocols or plain maps + functions, turn for-loops with accumulator variables into reduce or into.

If the surrounding codebase has an established (even if less idiomatic) convention, match it for consistency rather than unilaterally "fixing" the whole file — but say so, and offer the idiomatic alternative as a suggestion rather than silently picking one or the other.

Other working-style notes:

  • When debugging, ask whether the error is happening under :none (dev) or :advanced/:simple (prod) optimizations — the failure modes are very different (the former is usually a logic/require bug, the latter is very often a renaming/externs issue).

Benchmark

Scenario: .benchmarks/scenarios/clojurescript-001-async-await.md · Run: 2026-08-31 (salience re-run wf_9a5588bc) · Log: .benchmarks/runs/2026-08-31/clojurescript-001-async-await.json

ModelWithoutWithDelta
claude-opus-4-867%100%+33%
claude-sonnet-4-683%100%+17%
claude-haiku-4-5100%100%+0%
PASS (run 2026-08-31). Salience re-run (✅/❌ ^:async-placement pair added to the Async section, wf_9a5588bc): sonnet's arg-vector typo cleared (+17); gains on all models. The prior single-run sonnet −17 was a salience gap, not noise. Gate per .agents/skills/skill-optimizer/rules/release-gates.md.
`
来自同一仓库

更多 Skills

全部 Skills
bsene
社区

c4-diagram

Generates C4 model architecture diagrams using Structurizr DSL (primary) or Mermaid C4 (fallback). Use when: user asks to "draw a diagram", "create a C4 diagram", "show architecture as a diagram", "generate architecture diagram", "document the system". DO NOT USE for code explanation or walkthroughs (use explain-code), class/sequence/ER diagrams (C4 is system-architecture only), or when a diagram is incidental to a code change — only when a C4 diagram is the primary deliverable.

安装量
1
GitHub Stars
5
最近更新
9月21日
bsene
社区

clean-code

Use when writing new code, naming modules/files/functions/classes/variables, reviewing or refactoring code for readability, assessing/reducing complexity, deciding whether an abstraction belongs, or reviewing with CUPID. For behavior-preserving refactors within a function, class, or file, route to the refactoring/ sub-skill. Trigger on requests like "name this function", "is this a good variable name", "review this for clean code", "reduce complexity", "should I add a comment here", "should I abstract this", "review with CUPID", "code smell", "extract method", or "refactor this class". Covers human-scale design, naming, complexity budgets, comments, CUPID, and in-place refactoring. Pairs with language-specific skills (typescript, go, clojurescript) for syntax/idiom concerns.

安装量
1
GitHub Stars
5
最近更新
9月21日
bsene
社区

communication

Analyze a real communication artifact (Slack/Teams message, email, meeting transcript, pitch or presentation draft, CV, or resume) and coach it sharper using seven rhetoric + structure techniques: ethos/logos/pathos, reframing tough questions, centering the other person, the Pyramid Principle, pattern interrupt, making ideas feel safe, and the cognitive-load through-line. Diagnose what works, what's missing, then rewrite. TRIGGER when: user wants to improve communication, "review my email/Slack/CV", "analyze this transcript", prep for a meeting/pitch/presentation, "how do I say this", be more persuasive/assertive, handle a tough question, talk to execs, "is this too long-winded". Also trigger for author style presets under references/authors/: DHH (blunt/direct), Uncle Bob (defend a quality/testing standard), Jessitron (reflective systems-thinking essay), Martin Fowler (measured mixed-feelings stance). DO NOT USE for raw text generation with no analysis, marketing/copywriting, translation, or code/docs.

安装量
1
GitHub Stars
5
最近更新
9月21日
bsene
社区

dantotsu

Use this skill when the user wants to investigate a bug, defect, incident, or quality issue and find its root cause — including requests to run a "5 whys" analysis, write a postmortem, do a root-cause analysis (RCA), figure out why a bug reached production or slipped past code review/QA, set up a recurring defect-analysis routine, or run a daily/weekly quality-improvement (Kaizen) triage. Also known as the Dantotsu method (Sadao Nomura's Toyota-derived defect-analysis approach, adapted for software). Trigger even if the user doesn't name the method explicitly — e.g. "why did this bug get through code review", "a client reported a bug, help me analyze it", "I want to understand why this keeps happening", "write up a root cause report for this defect", or "help me prioritize which fixes to actually ship this week".

安装量
1
GitHub Stars
5
最近更新
9月21日