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.
- package/dist-cli/conjure-js.mjs +9336 -5028
- package/dist-vite-plugin/index.mjs +10455 -0
- package/package.json +9 -2
- package/src/bin/cli.ts +2 -2
- package/src/bin/nrepl-symbol.ts +150 -0
- package/src/bin/nrepl.ts +301 -157
- package/src/bin/version.ts +1 -1
- package/src/clojure/core.clj +764 -29
- package/src/clojure/core.clj.d.ts +76 -4
- package/src/clojure/demo/math.clj +5 -1
- package/src/clojure/generated/builtin-namespace-registry.ts +4 -0
- package/src/clojure/generated/clojure-core-source.ts +765 -29
- package/src/clojure/generated/clojure-set-source.ts +136 -0
- package/src/clojure/generated/clojure-walk-source.ts +72 -0
- package/src/clojure/set.clj +132 -0
- package/src/clojure/set.clj.d.ts +20 -0
- package/src/clojure/string.clj.d.ts +14 -0
- package/src/clojure/walk.clj +68 -0
- package/src/clojure/walk.clj.d.ts +7 -0
- package/src/core/assertions.ts +114 -6
- package/src/core/bootstrap.ts +337 -0
- package/src/core/conversions.ts +48 -31
- package/src/core/core-module.ts +303 -0
- package/src/core/env.ts +42 -7
- package/src/core/errors.ts +8 -0
- package/src/core/evaluator/apply.ts +40 -25
- package/src/core/evaluator/arity.ts +8 -8
- package/src/core/evaluator/async-evaluator.ts +565 -0
- package/src/core/evaluator/collections.ts +30 -4
- package/src/core/evaluator/destructure.ts +180 -69
- package/src/core/evaluator/dispatch.ts +24 -14
- package/src/core/evaluator/evaluate.ts +22 -20
- package/src/core/evaluator/expand.ts +45 -15
- package/src/core/evaluator/form-parsers.ts +178 -0
- package/src/core/evaluator/index.ts +7 -9
- package/src/core/evaluator/js-interop.ts +189 -0
- package/src/core/evaluator/quasiquote.ts +14 -8
- package/src/core/evaluator/recur-check.ts +6 -6
- package/src/core/evaluator/special-forms.ts +380 -173
- package/src/core/factories.ts +182 -3
- package/src/core/index.ts +55 -5
- package/src/core/module.ts +136 -0
- package/src/core/ns-forms.ts +107 -0
- package/src/core/positions.ts +9 -2
- package/src/core/printer.ts +371 -11
- package/src/core/reader.ts +127 -29
- package/src/core/registry.ts +209 -0
- package/src/core/runtime.ts +376 -0
- package/src/core/session.ts +263 -478
- package/src/core/stdlib/arithmetic.ts +516 -215
- package/src/core/stdlib/async-fns.ts +132 -0
- package/src/core/stdlib/atoms.ts +286 -63
- package/src/core/stdlib/errors.ts +54 -50
- package/src/core/stdlib/hof.ts +74 -173
- package/src/core/stdlib/js-namespace.ts +344 -0
- package/src/core/stdlib/lazy.ts +34 -0
- package/src/core/stdlib/maps-sets.ts +322 -0
- package/src/core/stdlib/meta.ts +109 -28
- package/src/core/stdlib/predicates.ts +322 -196
- package/src/core/stdlib/regex.ts +126 -98
- package/src/core/stdlib/seq.ts +564 -0
- package/src/core/stdlib/strings.ts +164 -135
- package/src/core/stdlib/transducers.ts +95 -100
- package/src/core/stdlib/utils.ts +283 -147
- package/src/core/stdlib/vars.ts +27 -27
- package/src/core/stdlib/vectors.ts +122 -0
- package/src/core/tokenizer.ts +13 -3
- package/src/core/transformations.ts +117 -9
- package/src/core/types.ts +118 -6
- package/src/host/node-host-module.ts +74 -0
- package/src/nrepl/relay.ts +432 -0
- package/src/vite-plugin-clj/codegen.ts +87 -95
- package/src/vite-plugin-clj/index.ts +242 -18
- package/src/vite-plugin-clj/namespace-utils.ts +39 -0
- package/src/vite-plugin-clj/static-analysis.ts +211 -0
- package/src/clojure/demo.clj +0 -63
- package/src/clojure/demo.clj.d.ts +0 -0
- package/src/core/core-env.ts +0 -60
- package/src/core/stdlib/collections.ts +0 -784
- 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
|
|
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 =
|
|
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
|
-
|
|
143
|
-
|
|
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
|
-
`
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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.
|
|
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
|
+
}
|
package/src/clojure/demo.clj
DELETED
|
@@ -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
|