conjure-js 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist-cli/conjure-js.mjs +9336 -5028
  2. package/dist-vite-plugin/index.mjs +10455 -0
  3. package/package.json +9 -2
  4. package/src/bin/cli.ts +2 -2
  5. package/src/bin/nrepl-symbol.ts +150 -0
  6. package/src/bin/nrepl.ts +301 -157
  7. package/src/bin/version.ts +1 -1
  8. package/src/clojure/core.clj +764 -29
  9. package/src/clojure/core.clj.d.ts +76 -4
  10. package/src/clojure/demo/math.clj +5 -1
  11. package/src/clojure/generated/builtin-namespace-registry.ts +4 -0
  12. package/src/clojure/generated/clojure-core-source.ts +765 -29
  13. package/src/clojure/generated/clojure-set-source.ts +136 -0
  14. package/src/clojure/generated/clojure-walk-source.ts +72 -0
  15. package/src/clojure/set.clj +132 -0
  16. package/src/clojure/set.clj.d.ts +20 -0
  17. package/src/clojure/string.clj.d.ts +14 -0
  18. package/src/clojure/walk.clj +68 -0
  19. package/src/clojure/walk.clj.d.ts +7 -0
  20. package/src/core/assertions.ts +114 -6
  21. package/src/core/bootstrap.ts +337 -0
  22. package/src/core/conversions.ts +48 -31
  23. package/src/core/core-module.ts +303 -0
  24. package/src/core/env.ts +42 -7
  25. package/src/core/errors.ts +8 -0
  26. package/src/core/evaluator/apply.ts +40 -25
  27. package/src/core/evaluator/arity.ts +8 -8
  28. package/src/core/evaluator/async-evaluator.ts +565 -0
  29. package/src/core/evaluator/collections.ts +30 -4
  30. package/src/core/evaluator/destructure.ts +180 -69
  31. package/src/core/evaluator/dispatch.ts +24 -14
  32. package/src/core/evaluator/evaluate.ts +22 -20
  33. package/src/core/evaluator/expand.ts +45 -15
  34. package/src/core/evaluator/form-parsers.ts +178 -0
  35. package/src/core/evaluator/index.ts +7 -9
  36. package/src/core/evaluator/js-interop.ts +189 -0
  37. package/src/core/evaluator/quasiquote.ts +14 -8
  38. package/src/core/evaluator/recur-check.ts +6 -6
  39. package/src/core/evaluator/special-forms.ts +380 -173
  40. package/src/core/factories.ts +182 -3
  41. package/src/core/index.ts +55 -5
  42. package/src/core/module.ts +136 -0
  43. package/src/core/ns-forms.ts +107 -0
  44. package/src/core/positions.ts +9 -2
  45. package/src/core/printer.ts +371 -11
  46. package/src/core/reader.ts +127 -29
  47. package/src/core/registry.ts +209 -0
  48. package/src/core/runtime.ts +376 -0
  49. package/src/core/session.ts +263 -478
  50. package/src/core/stdlib/arithmetic.ts +516 -215
  51. package/src/core/stdlib/async-fns.ts +132 -0
  52. package/src/core/stdlib/atoms.ts +286 -63
  53. package/src/core/stdlib/errors.ts +54 -50
  54. package/src/core/stdlib/hof.ts +74 -173
  55. package/src/core/stdlib/js-namespace.ts +344 -0
  56. package/src/core/stdlib/lazy.ts +34 -0
  57. package/src/core/stdlib/maps-sets.ts +322 -0
  58. package/src/core/stdlib/meta.ts +109 -28
  59. package/src/core/stdlib/predicates.ts +322 -196
  60. package/src/core/stdlib/regex.ts +126 -98
  61. package/src/core/stdlib/seq.ts +564 -0
  62. package/src/core/stdlib/strings.ts +164 -135
  63. package/src/core/stdlib/transducers.ts +95 -100
  64. package/src/core/stdlib/utils.ts +283 -147
  65. package/src/core/stdlib/vars.ts +27 -27
  66. package/src/core/stdlib/vectors.ts +122 -0
  67. package/src/core/tokenizer.ts +13 -3
  68. package/src/core/transformations.ts +117 -9
  69. package/src/core/types.ts +118 -6
  70. package/src/host/node-host-module.ts +74 -0
  71. package/src/nrepl/relay.ts +432 -0
  72. package/src/vite-plugin-clj/codegen.ts +87 -95
  73. package/src/vite-plugin-clj/index.ts +242 -18
  74. package/src/vite-plugin-clj/namespace-utils.ts +39 -0
  75. package/src/vite-plugin-clj/static-analysis.ts +211 -0
  76. package/src/clojure/demo.clj +0 -63
  77. package/src/clojure/demo.clj.d.ts +0 -0
  78. package/src/core/core-env.ts +0 -60
  79. package/src/core/stdlib/collections.ts +0 -784
  80. package/src/host/node.ts +0 -55
@@ -1,15 +1,47 @@
1
1
  import { execFileSync } from 'node:child_process'
2
2
  import { readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs'
3
- import { resolve, relative, join } from 'node:path'
4
- import type { Plugin, ResolvedConfig } from 'vite'
3
+ import { resolve, relative, join, dirname } from 'node:path'
4
+ import { fileURLToPath, pathToFileURL } from 'node:url'
5
+ import { createRequire } from 'node:module'
6
+ import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite'
5
7
  import { createSession } from '../core/session'
6
8
  import type { Session } from '../core/session'
7
- import { nsToPath, pathToNs } from './namespace-utils'
9
+ import { nsToPath, pathToNs, extractStringRequires } from './namespace-utils'
8
10
  import { generateModuleCode, generateDts, safeJsIdentifier } from './codegen'
9
11
  import type { CodegenContext } from './codegen'
12
+ import { startBrowserNreplRelay } from '../nrepl/relay'
10
13
 
11
14
  interface CljPluginOptions {
12
15
  sourceRoots?: string[]
16
+ nreplPort?: number
17
+ /**
18
+ * Path to a user-defined session factory (relative to project root).
19
+ * The factory must be the default export with signature:
20
+ * (importMap: Record<string, unknown>) => SessionOptions | null | undefined
21
+ *
22
+ * The plugin automatically injects `importModule` (wired to the import map) and
23
+ * `output` (wired to nREPL output capture + console.log). Do NOT provide these.
24
+ * Return only what you need: hostBindings, modules, entries, stderr, etc.
25
+ *
26
+ * Example: entrypoint: 'src/conjure.ts'
27
+ */
28
+ entrypoint?: string
29
+ }
30
+
31
+ // Resolve the conjure-js core index path regardless of whether this plugin
32
+ // is running from TypeScript source (src/vite-plugin-clj/) or the pre-built
33
+ // output (dist-vite-plugin/). Never use the consuming project's root.
34
+ function resolveCoreIndexPath(): string {
35
+ const thisDir = dirname(fileURLToPath(import.meta.url))
36
+ // Source layout: packages/conjure-js/src/vite-plugin-clj/index.ts
37
+ const fromSource = resolve(thisDir, '../core/index.ts')
38
+ try {
39
+ statSync(fromSource)
40
+ return fromSource
41
+ } catch {
42
+ // Built layout: packages/conjure-js/dist-vite-plugin/index.mjs
43
+ return resolve(thisDir, '../src/core/index.ts')
44
+ }
13
45
  }
14
46
 
15
47
  const VIRTUAL_SESSION_ID = 'virtual:clj-session'
@@ -22,6 +54,13 @@ export function cljPlugin(options?: CljPluginOptions): Plugin {
22
54
  let coreIndexPath: string
23
55
  let codegenCtx: CodegenContext
24
56
  let generatorScriptPath: string
57
+ let serveMode = false
58
+ // Collected during configResolved: original CLJ source string → resolved path/package name.
59
+ // Original string = what the CLJ runtime passes to importModule(s).
60
+ // Resolved = what Vite should actually import (absolute path for relative, unchanged for pkgs).
61
+ let stringRequires: Array<{ original: string; resolved: string }> = []
62
+ // Resolved absolute path to the user-defined session entrypoint (Mode 2), or null (Mode 1).
63
+ let entrypointPath: string | null = null
25
64
 
26
65
  function writeFileIfChanged(path: string, content: string) {
27
66
  try {
@@ -68,7 +107,7 @@ export function cljPlugin(options?: CljPluginOptions): Plugin {
68
107
  const source = readFileSync(filePath, 'utf-8')
69
108
  const nsNameFromPath = pathToNs(relative(projectRoot, filePath), sourceRoots)
70
109
  const dts = generateDts(codegenCtx, nsNameFromPath, source)
71
- writeFileIfChanged(filePath + '.d.ts', dts)
110
+ if (dts) writeFileIfChanged(filePath + '.d.ts', dts)
72
111
  } catch {
73
112
  continue
74
113
  }
@@ -77,14 +116,45 @@ export function cljPlugin(options?: CljPluginOptions): Plugin {
77
116
  }
78
117
 
79
118
  function initServerSession() {
119
+ // Use a require resolver anchored to the project root so that the server session
120
+ // can import npm packages from the consuming project's node_modules (e.g. date-fns),
121
+ // not just from the plugin's own node_modules.
122
+ const projectRequire = createRequire(resolve(projectRoot, 'package.json'))
123
+
80
124
  serverSession = createSession({
81
125
  sourceRoots,
82
126
  readFile: (filePath: string) =>
83
127
  readFileSync(resolve(projectRoot, filePath), 'utf-8'),
84
128
  output: () => {},
129
+ // Node dynamic import — used only during server-side code generation (DTS inference).
130
+ // In the browser bundle, importModule is a synchronous import map lookup instead.
131
+ importModule: async (s: string) => {
132
+ if (!s.startsWith('.') && !s.startsWith('/')) {
133
+ // Package import: resolve from project root context so the consuming project's
134
+ // node_modules are searched instead of (or in addition to) the plugin's.
135
+ try {
136
+ const resolved = projectRequire.resolve(s)
137
+ return import(pathToFileURL(resolved).href)
138
+ } catch {
139
+ try {
140
+ return await import(s)
141
+ } catch {
142
+ // Package not importable in Node context (browser-only) — return stub
143
+ // so that namespace loading succeeds and codegen can infer exported vars.
144
+ return {}
145
+ }
146
+ }
147
+ }
148
+ // Local/absolute path: may be a browser-only file (e.g. local .ts with DOM deps).
149
+ // Return a stub so the CLJ namespace still loads for server-side var inference.
150
+ try {
151
+ return await import(s)
152
+ } catch {
153
+ return {}
154
+ }
155
+ },
85
156
  })
86
157
  codegenCtx = {
87
- session: serverSession,
88
158
  sourceRoots,
89
159
  coreIndexPath,
90
160
  virtualSessionId: VIRTUAL_SESSION_ID,
@@ -103,7 +173,40 @@ export function cljPlugin(options?: CljPluginOptions): Plugin {
103
173
  }
104
174
  }
105
175
 
176
+ function scanStringRequires() {
177
+ // original string (what CLJ runtime passes to importModule) → resolved path/pkg
178
+ const seen = new Map<string, string>()
179
+ for (const root of sourceRoots) {
180
+ const rootPath = resolve(projectRoot, root)
181
+ for (const filePath of collectCljFiles(rootPath)) {
182
+ try {
183
+ const source = readFileSync(filePath, 'utf-8')
184
+ // Call twice: without filePath to get original CLJ source strings,
185
+ // with filePath to get resolved absolute paths for relative specifiers.
186
+ const originals = extractStringRequires(source)
187
+ const resolved = extractStringRequires(source, filePath)
188
+ for (let i = 0; i < originals.length; i++) {
189
+ seen.set(originals[i], resolved[i])
190
+ }
191
+ } catch {
192
+ continue
193
+ }
194
+ }
195
+ }
196
+ stringRequires = [...seen.entries()].map(([original, resolved]) => ({
197
+ original,
198
+ resolved,
199
+ }))
200
+ }
201
+
106
202
  function regenerateBuiltInNamespaceSources() {
203
+ try {
204
+ statSync(generatorScriptPath)
205
+ } catch {
206
+ // Script doesn't exist in this project — pre-built sources are already
207
+ // included in the conjure-js package, no regeneration needed.
208
+ return
209
+ }
107
210
  try {
108
211
  execFileSync(process.execPath, [generatorScriptPath], {
109
212
  cwd: projectRoot,
@@ -115,18 +218,61 @@ export function cljPlugin(options?: CljPluginOptions): Plugin {
115
218
  }
116
219
  }
117
220
 
221
+ /**
222
+ * Generate the static import table lines for the virtual session module.
223
+ * Each specifier gets a unique variable name. Returns { importLines, mapEntries }.
224
+ */
225
+ function buildImportTable(): { importLines: string[]; mapEntries: string[] } {
226
+ const importLines: string[] = []
227
+ const mapEntries: string[] = []
228
+ stringRequires.forEach(({ original, resolved }, i) => {
229
+ const varName = `_imp_${i}`
230
+ // Import statement uses the resolved path (absolute for local files, pkg name for packages).
231
+ // Map key uses the original CLJ source string — this is what importModule(s) receives at runtime.
232
+ importLines.push(`import * as ${varName} from ${JSON.stringify(resolved)};`)
233
+ mapEntries.push(` ${JSON.stringify(original)}: ${varName},`)
234
+ })
235
+ return { importLines, mapEntries }
236
+ }
237
+
118
238
  return {
119
239
  name: 'vite-plugin-clj',
120
240
 
121
241
  configResolved(config: ResolvedConfig) {
122
242
  projectRoot = config.root
243
+ serveMode = config.command === 'serve'
123
244
  generatorScriptPath = resolve(projectRoot, 'scripts/gen-core-source.mjs')
124
245
  regenerateBuiltInNamespaceSources()
125
- coreIndexPath = resolve(projectRoot, 'src/core/index.ts')
246
+ coreIndexPath = resolveCoreIndexPath()
126
247
  initServerSession()
248
+
249
+ // Detect Mode 2: explicit user entrypoint
250
+ if (options?.entrypoint) {
251
+ const ep = resolve(projectRoot, options.entrypoint)
252
+ try {
253
+ statSync(ep)
254
+ entrypointPath = ep
255
+ } catch {
256
+ // Configured entrypoint doesn't exist — fall through to Mode 1 with a warning
257
+ console.warn(
258
+ `[vite-plugin-clj] entrypoint not found: ${options.entrypoint} — falling back to auto-generated session`
259
+ )
260
+ }
261
+ }
262
+
263
+ scanStringRequires()
127
264
  eagerlyGenerateDts()
128
265
  },
129
266
 
267
+ configureServer(server: ViteDevServer) {
268
+ startBrowserNreplRelay({
269
+ port: options?.nreplPort,
270
+ cwd: projectRoot,
271
+ ws: server.ws,
272
+ serverSession,
273
+ })
274
+ },
275
+
130
276
  resolveId(source: string) {
131
277
  if (source === VIRTUAL_SESSION_ID) {
132
278
  return RESOLVED_VIRTUAL_SESSION_ID
@@ -139,25 +285,103 @@ export function cljPlugin(options?: CljPluginOptions): Plugin {
139
285
 
140
286
  load(id: string) {
141
287
  if (id === RESOLVED_VIRTUAL_SESSION_ID) {
142
- return [
143
- `import { createSession } from ${JSON.stringify(coreIndexPath)};`,
288
+ const { importLines, mapEntries } = buildImportTable()
289
+
290
+ // All static imports must be at the top of the module (ESM hoist semantics)
291
+ const lines: string[] = [
292
+ `import { createSession, printString } from ${JSON.stringify(coreIndexPath)};`,
293
+ ...importLines,
294
+ ...(entrypointPath
295
+ ? [`import __conjureFactory from ${JSON.stringify(entrypointPath)};`]
296
+ : []),
297
+ ``,
298
+ `const __importMap = {`,
299
+ ...mapEntries,
300
+ `};`,
144
301
  ``,
145
302
  `let _session = null;`,
146
- `export function getSession() {`,
147
- ` if (!_session) {`,
148
- ` _session = createSession();`,
149
- ` }`,
150
- ` return _session;`,
151
- `}`,
152
- ].join('\n')
303
+ `let _outputLines = [];`,
304
+ ]
305
+
306
+ if (entrypointPath) {
307
+ // Mode 2: user-defined factory returns SessionOptions (without output/importModule).
308
+ // The plugin owns output capture (for nREPL relay) and importModule (import map wiring).
309
+ // User factory controls hostBindings, modules, entries, etc.
310
+ lines.push(
311
+ `export function getSession() {`,
312
+ ` if (!_session) {`,
313
+ ` const __userOpts = __conjureFactory(__importMap) ?? {};`,
314
+ ` _session = createSession({`,
315
+ ` ...(__userOpts),`,
316
+ ` importModule: (s) => __importMap[s],`,
317
+ ` output: (text) => { _outputLines.push(text); console.log(text.replace(/\\n$/, '')); },`,
318
+ ` });`,
319
+ ` }`,
320
+ ` return _session;`,
321
+ `}`,
322
+ )
323
+ } else {
324
+ // Mode 1: auto-generate session with import map wired in
325
+ lines.push(
326
+ `export function getSession() {`,
327
+ ` if (!_session) {`,
328
+ ` _session = createSession({`,
329
+ ` importModule: (s) => __importMap[s],`,
330
+ ` output: (text) => { _outputLines.push(text); console.log(text.replace(/\\n$/, '')); },`,
331
+ ` });`,
332
+ ` }`,
333
+ ` return _session;`,
334
+ `}`,
335
+ )
336
+ }
337
+
338
+ if (serveMode) {
339
+ lines.push(
340
+ ``,
341
+ `// Browser nREPL relay — active only in Vite dev server`,
342
+ `if (import.meta.hot) {`,
343
+ ` import.meta.hot.on('conjure:eval', async ({ id, code, ns }) => {`,
344
+ ` const session = getSession();`,
345
+ ` _outputLines = [];`,
346
+ ` try {`,
347
+ ` if (ns && ns !== session.currentNs) session.setNs(ns);`,
348
+ ` const result = await session.evaluateAsync(code);`,
349
+ ` const out = _outputLines.join('');`,
350
+ ` import.meta.hot.send('conjure:eval-result', { id, value: printString(result), ns: session.currentNs, ...(out ? { out } : {}) });`,
351
+ ` } catch (err) {`,
352
+ ` console.error(err);`,
353
+ ` const out = _outputLines.join('');`,
354
+ ` import.meta.hot.send('conjure:eval-result', { id, error: err instanceof Error ? err.message : String(err), ns: session.currentNs, ...(out ? { out } : {}) });`,
355
+ ` }`,
356
+ ` });`,
357
+ ``,
358
+ ` import.meta.hot.on('conjure:load-file', async ({ id, source, nsHint, filePath }) => {`,
359
+ ` const session = getSession();`,
360
+ ` _outputLines = [];`,
361
+ ` try {`,
362
+ ` const loadedNs = await session.loadFileAsync(source, nsHint, filePath || undefined);`,
363
+ ` if (loadedNs) session.setNs(loadedNs);`,
364
+ ` const out = _outputLines.join('');`,
365
+ ` import.meta.hot.send('conjure:load-file-result', { id, value: 'nil', ns: session.currentNs, ...(out ? { out } : {}) });`,
366
+ ` } catch (err) {`,
367
+ ` console.error(err);`,
368
+ ` const out = _outputLines.join('');`,
369
+ ` import.meta.hot.send('conjure:load-file-result', { id, error: err instanceof Error ? err.message : String(err), ns: session.currentNs, ...(out ? { out } : {}) });`,
370
+ ` }`,
371
+ ` });`,
372
+ `}`,
373
+ )
374
+ }
375
+
376
+ return lines.join('\n')
153
377
  }
154
378
 
155
379
  if (id.endsWith('.clj') && !id.includes('?')) {
156
380
  const source = readFileSync(id, 'utf-8')
157
381
  const nsNameFromPath = pathToNs(relative(projectRoot, id), sourceRoots)
158
- const code = generateModuleCode(codegenCtx, nsNameFromPath, source)
382
+ const code = generateModuleCode(codegenCtx, nsNameFromPath, source, id)
159
383
  const dts = generateDts(codegenCtx, nsNameFromPath, source)
160
- writeFileIfChanged(id + '.d.ts', dts)
384
+ if (dts) writeFileIfChanged(id + '.d.ts', dts)
161
385
  return code
162
386
  }
163
387
  },
@@ -172,7 +396,7 @@ export function cljPlugin(options?: CljPluginOptions): Plugin {
172
396
  const source = await read()
173
397
  try {
174
398
  const nsNameFromPath = pathToNs(relative(projectRoot, file), sourceRoots)
175
- serverSession.loadFile(source, nsNameFromPath)
399
+ await serverSession.loadFileAsync(source, nsNameFromPath)
176
400
  const dts = generateDts(codegenCtx, nsNameFromPath, source)
177
401
  writeFileIfChanged(file + '.d.ts', dts)
178
402
  } catch {
@@ -1,3 +1,4 @@
1
+ import { resolve, dirname } from 'node:path'
1
2
  import { isKeyword, isList, isSymbol, isVector } from '../core/assertions'
2
3
  import { readForms } from '../core/reader'
3
4
  import { tokenize } from '../core/tokenizer'
@@ -63,6 +64,44 @@ export function extractNsRequires(source: string): string[] {
63
64
  return requires
64
65
  }
65
66
 
67
+ /**
68
+ * Parse Clojure source and extract all string module specifiers from (:require ...) clauses.
69
+ * These are the JS/npm imports written as string literals: (:require ["react" :as React]).
70
+ * Returns an array of resolved specifier strings (deduplicated).
71
+ *
72
+ * If filePath is provided, relative specifiers (starting with ./ or ../) are resolved
73
+ * to absolute paths using the file's directory. Package specifiers are returned as-is.
74
+ */
75
+ export function extractStringRequires(source: string, filePath?: string): string[] {
76
+ const forms = readForms(tokenize(source))
77
+ const nsForm = forms.find(
78
+ (f) => isList(f) && isSymbol(f.value[0]) && f.value[0].name === 'ns'
79
+ )
80
+ if (!nsForm || !isList(nsForm)) return []
81
+
82
+ const specifiers: string[] = []
83
+ for (let i = 2; i < nsForm.value.length; i++) {
84
+ const clause = nsForm.value[i]
85
+ if (
86
+ isList(clause) &&
87
+ isKeyword(clause.value[0]) &&
88
+ clause.value[0].name === ':require'
89
+ ) {
90
+ for (let j = 1; j < clause.value.length; j++) {
91
+ const spec = clause.value[j]
92
+ const first = isVector(spec) && spec.value.length > 0 ? spec.value[0] : null
93
+ if (!first || first.kind !== 'string') continue
94
+ let specifier = first.value
95
+ if (filePath && (specifier.startsWith('./') || specifier.startsWith('../'))) {
96
+ specifier = resolve(dirname(filePath), specifier)
97
+ }
98
+ specifiers.push(specifier)
99
+ }
100
+ }
101
+ }
102
+ return [...new Set(specifiers)]
103
+ }
104
+
66
105
  /**
67
106
  * Extract the namespace name from the (ns ...) form in a Clojure source string.
68
107
  * Returns null if no ns form is found.
@@ -0,0 +1,211 @@
1
+ import { isList, isSymbol, isVector } from '../core/assertions'
2
+ import { readForms } from '../core/reader'
3
+ import { tokenize } from '../core/tokenizer'
4
+ import type { Arity, CljList, CljMap, CljValue, DestructurePattern } from '../core/types'
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Public types
8
+ // ---------------------------------------------------------------------------
9
+
10
+ export interface VarDescriptor {
11
+ name: string
12
+ kind: 'fn' | 'const' | 'unknown'
13
+ arities?: Arity[] // present when kind === 'fn'
14
+ tsType?: string // present when kind === 'const' and value type is inferrable
15
+ isPrivate: boolean
16
+ isMacro: boolean
17
+ }
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Public API
21
+ // ---------------------------------------------------------------------------
22
+
23
+ /**
24
+ * Parse Clojure source and return descriptors for all top-level var definitions.
25
+ * Pure function — no session, no execution, no side effects.
26
+ *
27
+ * Handles: defn, defn-, defmacro, def, defonce, declare.
28
+ * Skips: ns, and any other top-level form that is not a definition.
29
+ *
30
+ * Private vars (defn-) are included with isPrivate: true so callers can decide
31
+ * whether to export them or not.
32
+ */
33
+ export function readNamespaceVars(source: string): VarDescriptor[] {
34
+ const forms = readForms(tokenize(source))
35
+ const descriptors: VarDescriptor[] = []
36
+
37
+ for (const form of forms) {
38
+ if (!isList(form) || form.value.length < 2) continue
39
+ const head = form.value[0]
40
+ if (!isSymbol(head)) continue
41
+
42
+ const descriptor = parseTopLevelDef(form, head.name)
43
+ if (descriptor) descriptors.push(descriptor)
44
+ }
45
+
46
+ return descriptors
47
+ }
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Metadata helpers
51
+ // ---------------------------------------------------------------------------
52
+
53
+ function hasPrivateMeta(meta: CljMap | undefined): boolean {
54
+ return (meta?.entries ?? []).some(
55
+ ([k, val]) =>
56
+ k.kind === 'keyword' && k.name === ':private' &&
57
+ val.kind === 'boolean' && val.value === true
58
+ )
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Per-form dispatch
63
+ // ---------------------------------------------------------------------------
64
+
65
+ function parseTopLevelDef(form: CljList, op: string): VarDescriptor | null {
66
+ switch (op) {
67
+ case 'defn':
68
+ return parseDefn(form, false, false)
69
+ case 'defn-':
70
+ return parseDefn(form, true, false)
71
+ case 'defmacro':
72
+ return parseDefn(form, false, true)
73
+ case 'def':
74
+ case 'defonce':
75
+ return parseDef(form)
76
+ case 'declare':
77
+ return parseDeclare(form)
78
+ default:
79
+ return null
80
+ }
81
+ }
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // defn / defn- / defmacro
85
+ // ---------------------------------------------------------------------------
86
+
87
+ function parseDefn(form: CljList, isPrivate: boolean, isMacro: boolean): VarDescriptor | null {
88
+ const nameSym = form.value[1]
89
+ if (!isSymbol(nameSym)) return null
90
+
91
+ const private_ = isPrivate || hasPrivateMeta(nameSym.meta)
92
+
93
+ // Elements after the name: optional docstring, then params or arity clauses
94
+ const rest = form.value.slice(2)
95
+ // Skip optional docstring
96
+ const start = rest.length > 0 && rest[0].kind === 'string' ? 1 : 0
97
+ const bodyForms = rest.slice(start)
98
+
99
+ if (bodyForms.length === 0) {
100
+ // Bare defn with no param list — treat as unknown function
101
+ return { name: nameSym.name, kind: 'fn', arities: [], isPrivate: private_, isMacro }
102
+ }
103
+
104
+ const arities: Arity[] = isList(bodyForms[0])
105
+ ? // Multi-arity: each clause is a list whose first element is the params vector
106
+ bodyForms.filter(isList).map(parseArityClause)
107
+ : // Single-arity: bodyForms[0] is the params vector
108
+ isVector(bodyForms[0]) ? [vectorToArity(bodyForms[0])] : []
109
+
110
+ return { name: nameSym.name, kind: 'fn', arities, isPrivate: private_, isMacro }
111
+ }
112
+
113
+ function parseArityClause(clause: CljList): Arity {
114
+ const paramVec = clause.value[0]
115
+ return isVector(paramVec) ? vectorToArity(paramVec) : { params: [], restParam: null, body: [] }
116
+ }
117
+
118
+ function vectorToArity(paramVec: { value: CljValue[] }): Arity {
119
+ const params: DestructurePattern[] = []
120
+ let restParam: DestructurePattern | null = null
121
+
122
+ for (let i = 0; i < paramVec.value.length; i++) {
123
+ const p = paramVec.value[i]
124
+ if (isSymbol(p) && p.name === '&') {
125
+ const next = paramVec.value[i + 1]
126
+ if (next) restParam = next as DestructurePattern
127
+ break
128
+ }
129
+ params.push(p as DestructurePattern)
130
+ }
131
+
132
+ return { params, restParam, body: [] }
133
+ }
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // def / defonce
137
+ // ---------------------------------------------------------------------------
138
+
139
+ function parseDef(form: CljList): VarDescriptor | null {
140
+ const nameSym = form.value[1]
141
+ if (!isSymbol(nameSym)) return null
142
+
143
+ const isPrivate = hasPrivateMeta(nameSym.meta)
144
+ const value = form.value[2] // may be undefined for bare (def name)
145
+
146
+ if (!value) {
147
+ return { name: nameSym.name, kind: 'unknown', isPrivate, isMacro: false }
148
+ }
149
+
150
+ // Inline function literal
151
+ const fnArities = tryExtractFnArities(value)
152
+ if (fnArities !== null) {
153
+ return { name: nameSym.name, kind: 'fn', arities: fnArities, isPrivate, isMacro: false }
154
+ }
155
+
156
+ // Literal values with inferrable TypeScript types
157
+ const tsType = inferLiteralTsType(value)
158
+ if (tsType !== null) {
159
+ return { name: nameSym.name, kind: 'const', tsType, isPrivate, isMacro: false }
160
+ }
161
+
162
+ return { name: nameSym.name, kind: 'unknown', isPrivate, isMacro: false }
163
+ }
164
+
165
+ function inferLiteralTsType(value: CljValue): string | null {
166
+ switch (value.kind) {
167
+ case 'number': return 'number'
168
+ case 'string': return 'string'
169
+ case 'boolean': return 'boolean'
170
+ case 'nil': return 'null'
171
+ case 'keyword': return 'string'
172
+ case 'vector':
173
+ case 'set': return 'unknown[]'
174
+ case 'map': return 'Record<string, unknown>'
175
+ default: return null
176
+ }
177
+ }
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // declare
181
+ // ---------------------------------------------------------------------------
182
+
183
+ function parseDeclare(form: CljList): VarDescriptor | null {
184
+ const nameSym = form.value[1]
185
+ if (!isSymbol(nameSym)) return null
186
+ return { name: nameSym.name, kind: 'unknown', isPrivate: false, isMacro: false }
187
+ }
188
+
189
+ // ---------------------------------------------------------------------------
190
+ // (fn ...) extraction for def with inline function
191
+ // ---------------------------------------------------------------------------
192
+
193
+ function tryExtractFnArities(value: CljValue): Arity[] | null {
194
+ if (!isList(value)) return null
195
+ const head = value.value[0]
196
+ if (!isSymbol(head) || head.name !== 'fn') return null
197
+
198
+ // After fn: optional name symbol, then params-vector or arity-clauses
199
+ let rest = value.value.slice(1)
200
+ if (rest.length > 0 && isSymbol(rest[0])) rest = rest.slice(1) // skip optional fn name
201
+
202
+ if (rest.length === 0) return []
203
+
204
+ if (isVector(rest[0])) {
205
+ // Single arity: (fn [params] body)
206
+ return [vectorToArity(rest[0])]
207
+ }
208
+
209
+ // Multi arity: (fn ([params] body) ([params] body))
210
+ return rest.filter(isList).map(parseArityClause)
211
+ }
@@ -1,63 +0,0 @@
1
- (ns demo
2
- (:require [demo.math :refer [pi square factorial] :as m]))
3
-
4
- (def greeting "Hello from Clojure!")
5
-
6
-
7
- greeting
8
- (def add m/add)
9
-
10
- pi
11
-
12
- (add 1 2)
13
-
14
- (square 3)
15
-
16
- (def x 42)
17
-
18
- (doc -)
19
-
20
- (var? #'x)
21
-
22
- #'x
23
-
24
- (+ 1 2)
25
-
26
- pi
27
-
28
- (slurp "test.txt")
29
- (spit "test.txt" "hello from the runtime")
30
-
31
- (doc reduce)
32
-
33
- (defn greet [name]
34
- (str greeting " Welcome, " name "!"))
35
-
36
- (greet "Regibyte")
37
-
38
- (defn fibonacci
39
- "This is the fibonacci function"
40
- [n]
41
- (loop [i 0 a 0 b 1]
42
- (if (= i n)
43
- a
44
- (recur (inc i) b (+ a b)))))
45
-
46
- (map fibonacci [1 2 3 4 5 6])
47
-
48
- m/factorial
49
-
50
- (+ 1 2)
51
-
52
- (println "Hello World!" "From Calva!!!!!!")
53
-
54
- (type {:this-is :awesome!})
55
-
56
- (when true
57
- (println "Yes\n")
58
- (println "This\n")
59
- (println "Is working!")
60
- 42)
61
-
62
-
63
- (map inc [2 3 4 5 8 2])
File without changes