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
package/src/bin/nrepl.ts
CHANGED
|
@@ -5,17 +5,24 @@ import { BDecoderStream, BEncoderStream } from './bencode'
|
|
|
5
5
|
import {
|
|
6
6
|
createSession,
|
|
7
7
|
createSessionFromSnapshot,
|
|
8
|
+
cljNil,
|
|
8
9
|
printString,
|
|
10
|
+
readString,
|
|
9
11
|
snapshotSession,
|
|
10
|
-
type CljMap,
|
|
11
12
|
type CljValue,
|
|
12
13
|
type Session,
|
|
13
14
|
type SessionSnapshot,
|
|
14
15
|
} from '../core'
|
|
15
|
-
import {
|
|
16
|
+
import { withPrintContext } from '../core/printer'
|
|
17
|
+
import { derefValue } from '../core/env'
|
|
16
18
|
import { inferSourceRoot } from './nrepl-utils'
|
|
19
|
+
import {
|
|
20
|
+
resolveSymbol as resolveSymbolShared,
|
|
21
|
+
extractMeta as extractMetaShared,
|
|
22
|
+
} from './nrepl-symbol'
|
|
17
23
|
import { VERSION } from './version'
|
|
18
|
-
|
|
24
|
+
export { makeNodeHostModule } from '../host/node-host-module'
|
|
25
|
+
import { makeNodeHostModule } from '../host/node-host-module'
|
|
19
26
|
|
|
20
27
|
const CONJURE_VERSION = VERSION
|
|
21
28
|
|
|
@@ -25,6 +32,22 @@ const CONJURE_VERSION = VERSION
|
|
|
25
32
|
|
|
26
33
|
type NreplMessage = Record<string, unknown>
|
|
27
34
|
|
|
35
|
+
/** Streaming output chunk forwarded from a remote eval. */
|
|
36
|
+
type RemoteStreamChunk =
|
|
37
|
+
| { type: 'out'; text: string }
|
|
38
|
+
| { type: 'err'; text: string }
|
|
39
|
+
|
|
40
|
+
/** Minimal interface satisfied by MeshNode (structural — no import from the mesh experiment). */
|
|
41
|
+
export type RemoteEvalNode = {
|
|
42
|
+
evalAt(
|
|
43
|
+
targetId: string,
|
|
44
|
+
source: string,
|
|
45
|
+
ns?: string,
|
|
46
|
+
timeoutMs?: number,
|
|
47
|
+
onChunk?: (chunk: RemoteStreamChunk) => void
|
|
48
|
+
): Promise<{ value?: string; error?: string }>
|
|
49
|
+
}
|
|
50
|
+
|
|
28
51
|
type ManagedSession = {
|
|
29
52
|
id: string
|
|
30
53
|
session: Session
|
|
@@ -46,19 +69,23 @@ function createManagedSession(
|
|
|
46
69
|
id: string,
|
|
47
70
|
snapshot: SessionSnapshot,
|
|
48
71
|
encoder: BEncoderStream,
|
|
49
|
-
sourceRoots?: string[]
|
|
72
|
+
sourceRoots?: string[],
|
|
73
|
+
onOutput?: (text: string) => void,
|
|
74
|
+
importModule?: (specifier: string) => unknown | Promise<unknown>
|
|
50
75
|
): ManagedSession {
|
|
51
76
|
let currentMsgId = ''
|
|
52
77
|
|
|
53
78
|
const session = createSessionFromSnapshot(snapshot, {
|
|
54
79
|
output: (text) => {
|
|
55
80
|
send(encoder, { id: currentMsgId, session: id, out: text })
|
|
81
|
+
onOutput?.(text)
|
|
56
82
|
},
|
|
57
83
|
readFile: (filePath) => readFileSync(filePath, 'utf8'),
|
|
58
84
|
sourceRoots,
|
|
85
|
+
importModule,
|
|
59
86
|
})
|
|
60
87
|
|
|
61
|
-
|
|
88
|
+
session.runtime.installModules([makeNodeHostModule(session)])
|
|
62
89
|
|
|
63
90
|
return {
|
|
64
91
|
id,
|
|
@@ -105,11 +132,13 @@ function handleClone(
|
|
|
105
132
|
sessions: Map<string, ManagedSession>,
|
|
106
133
|
snapshot: SessionSnapshot,
|
|
107
134
|
encoder: BEncoderStream,
|
|
108
|
-
sourceRoots?: string[]
|
|
135
|
+
sourceRoots?: string[],
|
|
136
|
+
onOutput?: (text: string) => void,
|
|
137
|
+
importModule?: (specifier: string) => unknown | Promise<unknown>
|
|
109
138
|
) {
|
|
110
139
|
const id = (msg['id'] as string) ?? ''
|
|
111
140
|
const newId = makeSessionId()
|
|
112
|
-
const managed = createManagedSession(newId, snapshot, encoder, sourceRoots)
|
|
141
|
+
const managed = createManagedSession(newId, snapshot, encoder, sourceRoots, onOutput, importModule)
|
|
113
142
|
sessions.set(newId, managed)
|
|
114
143
|
done(encoder, id, undefined, { 'new-session': newId })
|
|
115
144
|
}
|
|
@@ -135,20 +164,148 @@ function handleDescribe(msg: NreplMessage, encoder: BEncoderStream) {
|
|
|
135
164
|
})
|
|
136
165
|
}
|
|
137
166
|
|
|
138
|
-
function handleEval(
|
|
167
|
+
async function handleEval(
|
|
139
168
|
msg: NreplMessage,
|
|
140
169
|
managed: ManagedSession,
|
|
141
|
-
encoder: BEncoderStream
|
|
142
|
-
|
|
170
|
+
encoder: BEncoderStream,
|
|
171
|
+
meshNode?: RemoteEvalNode
|
|
172
|
+
): Promise<void> {
|
|
143
173
|
const id = (msg['id'] as string) ?? ''
|
|
144
174
|
const code = (msg['code'] as string) ?? ''
|
|
145
175
|
|
|
146
176
|
managed.currentMsgId = id
|
|
147
177
|
|
|
178
|
+
// Calva sends 1-based :line and :column for the start of the evaluated form
|
|
179
|
+
// in the original file. Convert to 0-based offsets so evaluateDef can add
|
|
180
|
+
// them to the relative positions the reader computes from the snippet.
|
|
181
|
+
const lineOffset =
|
|
182
|
+
typeof msg['line'] === 'number' ? (msg['line'] as number) - 1 : 0
|
|
183
|
+
const colOffset =
|
|
184
|
+
typeof msg['column'] === 'number' ? (msg['column'] as number) - 1 : 0
|
|
185
|
+
|
|
186
|
+
// Mesh routing: if mesh/*eval-target* is set and a meshNode is provided, route remotely.
|
|
187
|
+
// *eval-target* stores a CljString (set by set-target!) or CljNil (cleared).
|
|
188
|
+
//
|
|
189
|
+
// IMPORTANT: set-target! must ALWAYS run locally, regardless of the current target.
|
|
190
|
+
// If routed, (mesh/set-target! nil) would run on the remote node and clear ITS var,
|
|
191
|
+
// leaving the local target set — the user would be permanently stuck until reconnect.
|
|
192
|
+
//
|
|
193
|
+
// We detect "is this a set-target! call?" by resolving the head symbol in the local
|
|
194
|
+
// environment, not by regex. This correctly handles namespace aliases (:as m → m/set-target!),
|
|
195
|
+
// refers, and user renames — without any string matching on code.
|
|
196
|
+
// Vars that must always evaluate locally — reading or calling them on a
|
|
197
|
+
// remote node would return that node's state, not the local connection's.
|
|
198
|
+
const MESH_LOCAL_ONLY = new Set(['set-target!', '*eval-target*'])
|
|
199
|
+
const isMeshControl = (() => {
|
|
200
|
+
try {
|
|
201
|
+
const first = readString(code.trim())
|
|
202
|
+
// Direct symbol read: mesh/*eval-target*
|
|
203
|
+
if (first.kind === 'symbol') {
|
|
204
|
+
const resolved = resolveSymbol(first.name, managed)
|
|
205
|
+
return resolved?.resolvedNs === 'mesh' && MESH_LOCAL_ONLY.has(resolved.localName)
|
|
206
|
+
}
|
|
207
|
+
// Function call: (set-target! ...) or (mesh/set-target! ...)
|
|
208
|
+
if (first.kind !== 'list' || first.value.length === 0) return false
|
|
209
|
+
const head = first.value[0]
|
|
210
|
+
if (head.kind !== 'symbol') return false
|
|
211
|
+
const resolved = resolveSymbol(head.name, managed)
|
|
212
|
+
return resolved?.resolvedNs === 'mesh' && MESH_LOCAL_ONLY.has(resolved?.localName ?? '')
|
|
213
|
+
} catch {
|
|
214
|
+
return false
|
|
215
|
+
}
|
|
216
|
+
})()
|
|
217
|
+
|
|
218
|
+
if (!isMeshControl && meshNode) {
|
|
219
|
+
const meshNs = managed.session.getNs('mesh')
|
|
220
|
+
const evalTargetVar = meshNs?.vars.get('*eval-target*')
|
|
221
|
+
const targetVal = evalTargetVar?.value
|
|
222
|
+
if (targetVal?.kind === 'string' && targetVal.value) {
|
|
223
|
+
const targetId = targetVal.value
|
|
224
|
+
try {
|
|
225
|
+
const result = await meshNode.evalAt(
|
|
226
|
+
targetId,
|
|
227
|
+
code,
|
|
228
|
+
managed.session.currentNs,
|
|
229
|
+
undefined,
|
|
230
|
+
(chunk) => {
|
|
231
|
+
// Stream each chunk back to the editor immediately as it arrives —
|
|
232
|
+
// no buffering, works for million-line outputs and long-running evals.
|
|
233
|
+
if (chunk.type === 'out')
|
|
234
|
+
send(encoder, { id, session: managed.id, out: chunk.text })
|
|
235
|
+
else send(encoder, { id, session: managed.id, err: chunk.text })
|
|
236
|
+
}
|
|
237
|
+
)
|
|
238
|
+
if (result.error) {
|
|
239
|
+
done(encoder, id, managed.id, {
|
|
240
|
+
ex: result.error,
|
|
241
|
+
err: result.error + '\n',
|
|
242
|
+
ns: managed.session.currentNs,
|
|
243
|
+
status: ['eval-error', 'done'],
|
|
244
|
+
})
|
|
245
|
+
} else {
|
|
246
|
+
done(encoder, id, managed.id, {
|
|
247
|
+
value: result.value ?? 'nil',
|
|
248
|
+
ns: managed.session.currentNs,
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
} catch (e) {
|
|
252
|
+
const msg2 = e instanceof Error ? e.message : String(e)
|
|
253
|
+
const isUnreachable =
|
|
254
|
+
msg2.includes('not registered') || msg2.includes('Timeout')
|
|
255
|
+
if (isUnreachable) {
|
|
256
|
+
// Clear *eval-target* so the user isn't stuck on subsequent evals.
|
|
257
|
+
// Do NOT fall through to local eval — the code was intended for the
|
|
258
|
+
// remote node; running it locally could be dangerous or wrong.
|
|
259
|
+
const meshNsClear = managed.session.getNs('mesh')
|
|
260
|
+
const evalTargetVarClear = meshNsClear?.vars.get('*eval-target*')
|
|
261
|
+
if (evalTargetVarClear) evalTargetVarClear.value = cljNil()
|
|
262
|
+
|
|
263
|
+
const errMsg = `Node '${targetId}' unreachable — *eval-target* cleared. Eval dropped. Re-send to try on this node or try another node.\n`
|
|
264
|
+
done(encoder, id, managed.id, {
|
|
265
|
+
ex: errMsg,
|
|
266
|
+
err: errMsg,
|
|
267
|
+
ns: managed.session.currentNs,
|
|
268
|
+
status: ['eval-error', 'done'],
|
|
269
|
+
})
|
|
270
|
+
} else {
|
|
271
|
+
done(encoder, id, managed.id, {
|
|
272
|
+
ex: msg2,
|
|
273
|
+
err: msg2 + '\n',
|
|
274
|
+
ns: managed.session.currentNs,
|
|
275
|
+
status: ['eval-error', 'done'],
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
148
283
|
try {
|
|
149
|
-
|
|
284
|
+
// evaluateAsync awaits any CljPending result before returning, so async
|
|
285
|
+
// forms, mesh/set-target!, mesh/list-nodes, etc. all resolve correctly
|
|
286
|
+
// before the nREPL response is sent. Sync evals are unaffected.
|
|
287
|
+
const result = await managed.session.evaluateAsync(code, {
|
|
288
|
+
lineOffset,
|
|
289
|
+
colOffset,
|
|
290
|
+
})
|
|
291
|
+
// Resolve *print-length* / *print-level* via the registry (same rationale
|
|
292
|
+
// as emitToOut in core-module.ts: session.getNs goes through the runtime
|
|
293
|
+
// registry so we always get the session's own freshly-cloned var, not a
|
|
294
|
+
// stale one from a snapshot closure env).
|
|
295
|
+
const coreNs = managed.session.getNs('clojure.core')
|
|
296
|
+
const lenVar = coreNs?.vars.get('*print-length*')
|
|
297
|
+
const lvlVar = coreNs?.vars.get('*print-level*')
|
|
298
|
+
const printLen = lenVar ? derefValue(lenVar) : undefined
|
|
299
|
+
const printLvl = lvlVar ? derefValue(lvlVar) : undefined
|
|
300
|
+
const resultStr = withPrintContext(
|
|
301
|
+
{
|
|
302
|
+
printLength: printLen?.kind === 'number' ? printLen.value : null,
|
|
303
|
+
printLevel: printLvl?.kind === 'number' ? printLvl.value : null,
|
|
304
|
+
},
|
|
305
|
+
() => printString(result)
|
|
306
|
+
)
|
|
150
307
|
done(encoder, id, managed.id, {
|
|
151
|
-
value:
|
|
308
|
+
value: resultStr,
|
|
152
309
|
ns: managed.session.currentNs,
|
|
153
310
|
})
|
|
154
311
|
} catch (error) {
|
|
@@ -162,11 +319,11 @@ function handleEval(
|
|
|
162
319
|
}
|
|
163
320
|
}
|
|
164
321
|
|
|
165
|
-
function handleLoadFile(
|
|
322
|
+
async function handleLoadFile(
|
|
166
323
|
msg: NreplMessage,
|
|
167
324
|
managed: ManagedSession,
|
|
168
325
|
encoder: BEncoderStream
|
|
169
|
-
) {
|
|
326
|
+
): Promise<void> {
|
|
170
327
|
const id = (msg['id'] as string) ?? ''
|
|
171
328
|
const source = (msg['file'] as string) ?? ''
|
|
172
329
|
const fileName = (msg['file-name'] as string) ?? ''
|
|
@@ -187,7 +344,14 @@ function handleLoadFile(
|
|
|
187
344
|
|
|
188
345
|
const nsHint =
|
|
189
346
|
fileName.replace(/\.clj$/, '').replace(/\//g, '.') || undefined
|
|
190
|
-
|
|
347
|
+
|
|
348
|
+
// loadFileAsync handles both sync and string (:require ["pkg" :as X]) forms.
|
|
349
|
+
// It falls back to the sync path internally if no async requires are present.
|
|
350
|
+
const loadedNs = await managed.session.loadFileAsync(
|
|
351
|
+
source,
|
|
352
|
+
nsHint,
|
|
353
|
+
filePath || undefined
|
|
354
|
+
)
|
|
191
355
|
|
|
192
356
|
// Track the file path for this namespace so info/lookup can return :file
|
|
193
357
|
// for go-to-definition support.
|
|
@@ -248,120 +412,15 @@ function resolveSymbol(
|
|
|
248
412
|
sym: string,
|
|
249
413
|
managed: ManagedSession,
|
|
250
414
|
contextNs?: string
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
const slashIdx = sym.indexOf('/')
|
|
254
|
-
|
|
255
|
-
if (slashIdx > 0) {
|
|
256
|
-
const qualifier = sym.slice(0, slashIdx)
|
|
257
|
-
const localName = sym.slice(slashIdx + 1)
|
|
258
|
-
|
|
259
|
-
// 1. Try as full namespace name (use registry Env for chain-based lookup)
|
|
260
|
-
const nsEnvFull = managed.session.registry.get(qualifier)
|
|
261
|
-
if (nsEnvFull) {
|
|
262
|
-
const value = tryLookup(localName, nsEnvFull)
|
|
263
|
-
if (value !== undefined) return { value, resolvedNs: qualifier, localName }
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// 2. Try as alias (:as str → clojure.string)
|
|
267
|
-
const currentNsData = managed.session.getNs(ns)
|
|
268
|
-
const aliasedNs = currentNsData?.aliases.get(qualifier)
|
|
269
|
-
if (aliasedNs) {
|
|
270
|
-
const v = aliasedNs.vars.get(localName)
|
|
271
|
-
if (v !== undefined)
|
|
272
|
-
return { value: v.value, resolvedNs: aliasedNs.name, localName }
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return null
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Unqualified symbol
|
|
279
|
-
const localName = sym
|
|
280
|
-
const nsEnvFull = managed.session.registry.get(ns)
|
|
281
|
-
if (!nsEnvFull) return null
|
|
282
|
-
const value = tryLookup(sym, nsEnvFull)
|
|
283
|
-
if (value === undefined) return null
|
|
284
|
-
|
|
285
|
-
// Determine the namespace where this symbol is defined.
|
|
286
|
-
// CljVar carries the authoritative ns; fall back to other heuristics.
|
|
287
|
-
const varObj = lookupVar(sym, nsEnvFull)
|
|
288
|
-
let resolvedNs: string
|
|
289
|
-
if (varObj) {
|
|
290
|
-
resolvedNs = varObj.ns
|
|
291
|
-
} else if (value.kind === 'function' || value.kind === 'macro') {
|
|
292
|
-
resolvedNs = getNamespaceEnv(value.env).ns?.name ?? ns
|
|
293
|
-
} else if (value.kind === 'native-function') {
|
|
294
|
-
const i = value.name.indexOf('/')
|
|
295
|
-
resolvedNs = i > 0 ? value.name.slice(0, i) : ns
|
|
296
|
-
} else {
|
|
297
|
-
resolvedNs = ns
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
return { value, resolvedNs, localName }
|
|
415
|
+
) {
|
|
416
|
+
return resolveSymbolShared(sym, managed.session, contextNs)
|
|
301
417
|
}
|
|
302
418
|
|
|
303
|
-
function extractMeta(
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
} {
|
|
309
|
-
const type =
|
|
310
|
-
value.kind === 'macro'
|
|
311
|
-
? 'macro'
|
|
312
|
-
: value.kind === 'function' || value.kind === 'native-function'
|
|
313
|
-
? 'function'
|
|
314
|
-
: 'var'
|
|
315
|
-
|
|
316
|
-
const meta: CljMap | undefined =
|
|
317
|
-
value.kind === 'function'
|
|
318
|
-
? value.meta
|
|
319
|
-
: value.kind === 'native-function'
|
|
320
|
-
? value.meta
|
|
321
|
-
: undefined
|
|
322
|
-
|
|
323
|
-
let doc = ''
|
|
324
|
-
let arglistsStr = ''
|
|
325
|
-
let eldocArgs: string[][] | null = null
|
|
326
|
-
|
|
327
|
-
if (meta) {
|
|
328
|
-
const docEntry = meta.entries.find(
|
|
329
|
-
([k]) => k.kind === 'keyword' && k.name === ':doc'
|
|
330
|
-
)
|
|
331
|
-
if (docEntry && docEntry[1].kind === 'string') doc = docEntry[1].value
|
|
332
|
-
|
|
333
|
-
const argsEntry = meta.entries.find(
|
|
334
|
-
([k]) => k.kind === 'keyword' && k.name === ':arglists'
|
|
335
|
-
)
|
|
336
|
-
if (argsEntry && argsEntry[1].kind === 'vector') {
|
|
337
|
-
const arglists = argsEntry[1]
|
|
338
|
-
arglistsStr = '(' + arglists.value.map((al) => printString(al)).join(' ') + ')'
|
|
339
|
-
eldocArgs = arglists.value.map((al) => {
|
|
340
|
-
if (al.kind !== 'vector') return [printString(al)]
|
|
341
|
-
return al.value.map((p) => (p.kind === 'symbol' ? p.name : printString(p)))
|
|
342
|
-
})
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Fallback: derive arglists from structural arities (fn/macro without meta)
|
|
347
|
-
if (
|
|
348
|
-
arglistsStr === '' &&
|
|
349
|
-
(value.kind === 'function' || value.kind === 'macro')
|
|
350
|
-
) {
|
|
351
|
-
const arityStrs = value.arities.map((arity) => {
|
|
352
|
-
const params = arity.params.map((p) => printString(p))
|
|
353
|
-
if (arity.restParam) params.push('&', printString(arity.restParam))
|
|
354
|
-
return '[' + params.join(' ') + ']'
|
|
355
|
-
})
|
|
356
|
-
arglistsStr = '(' + arityStrs.join(' ') + ')'
|
|
357
|
-
eldocArgs = value.arities.map((arity) => {
|
|
358
|
-
const params = arity.params.map((p) => printString(p))
|
|
359
|
-
if (arity.restParam) params.push('&', printString(arity.restParam))
|
|
360
|
-
return params
|
|
361
|
-
})
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
return { doc, arglistsStr, eldocArgs, type }
|
|
419
|
+
function extractMeta(resolved: {
|
|
420
|
+
value: CljValue
|
|
421
|
+
varObj?: import('./nrepl-symbol').ResolvedSymbol['varObj']
|
|
422
|
+
}) {
|
|
423
|
+
return extractMetaShared(resolved.value, resolved.varObj?.meta)
|
|
365
424
|
}
|
|
366
425
|
|
|
367
426
|
function handleInfo(
|
|
@@ -395,15 +454,30 @@ function handleInfo(
|
|
|
395
454
|
return
|
|
396
455
|
}
|
|
397
456
|
|
|
398
|
-
const meta = extractMeta(resolved
|
|
457
|
+
const meta = extractMeta(resolved)
|
|
399
458
|
const file = managed.nsToFile.get(resolved.resolvedNs)
|
|
459
|
+
|
|
460
|
+
// Extract :line/:column/:file from var meta if present (stamped by evaluateDef).
|
|
461
|
+
let varLine: number | undefined
|
|
462
|
+
let varColumn: number | undefined
|
|
463
|
+
let varFile: string | undefined
|
|
464
|
+
const varMetaEntries = resolved.varObj?.meta?.entries ?? []
|
|
465
|
+
for (const [k, v] of varMetaEntries) {
|
|
466
|
+
if (k.kind !== 'keyword') continue
|
|
467
|
+
if (k.name === ':line' && v.kind === 'number') varLine = v.value
|
|
468
|
+
if (k.name === ':column' && v.kind === 'number') varColumn = v.value
|
|
469
|
+
if (k.name === ':file' && v.kind === 'string') varFile = v.value
|
|
470
|
+
}
|
|
471
|
+
|
|
400
472
|
done(encoder, id, managed.id, {
|
|
401
473
|
ns: resolved.resolvedNs,
|
|
402
474
|
name: resolved.localName,
|
|
403
475
|
doc: meta.doc,
|
|
404
476
|
'arglists-str': meta.arglistsStr,
|
|
405
477
|
type: meta.type,
|
|
406
|
-
...(file ? { file } : {}),
|
|
478
|
+
...((varFile ?? file) ? { file: varFile ?? file } : {}),
|
|
479
|
+
...(varLine !== undefined ? { line: varLine } : {}),
|
|
480
|
+
...(varColumn !== undefined ? { column: varColumn } : {}),
|
|
407
481
|
})
|
|
408
482
|
}
|
|
409
483
|
|
|
@@ -435,7 +509,7 @@ function handleEldoc(
|
|
|
435
509
|
return
|
|
436
510
|
}
|
|
437
511
|
|
|
438
|
-
const meta = extractMeta(resolved
|
|
512
|
+
const meta = extractMeta(resolved)
|
|
439
513
|
if (!meta.eldocArgs) {
|
|
440
514
|
done(encoder, id, managed.id, { status: ['no-eldoc', 'done'] })
|
|
441
515
|
return
|
|
@@ -464,7 +538,10 @@ function handleMessage(
|
|
|
464
538
|
snapshot: SessionSnapshot,
|
|
465
539
|
encoder: BEncoderStream,
|
|
466
540
|
defaultSession: ManagedSession,
|
|
467
|
-
sourceRoots?: string[]
|
|
541
|
+
sourceRoots?: string[],
|
|
542
|
+
meshNode?: RemoteEvalNode,
|
|
543
|
+
onOutput?: (text: string) => void,
|
|
544
|
+
importModule?: (specifier: string) => unknown | Promise<unknown>
|
|
468
545
|
) {
|
|
469
546
|
const op = msg['op'] as string
|
|
470
547
|
const sessionId = msg['session'] as string | undefined
|
|
@@ -474,16 +551,32 @@ function handleMessage(
|
|
|
474
551
|
|
|
475
552
|
switch (op) {
|
|
476
553
|
case 'clone':
|
|
477
|
-
handleClone(msg, sessions, snapshot, encoder, sourceRoots)
|
|
554
|
+
handleClone(msg, sessions, snapshot, encoder, sourceRoots, onOutput, importModule)
|
|
478
555
|
break
|
|
479
556
|
case 'describe':
|
|
480
557
|
handleDescribe(msg, encoder)
|
|
481
558
|
break
|
|
482
559
|
case 'eval':
|
|
483
|
-
handleEval(msg, managed, encoder)
|
|
560
|
+
void handleEval(msg, managed, encoder, meshNode).catch((e) => {
|
|
561
|
+
const m = e instanceof Error ? e.message : String(e)
|
|
562
|
+
done(encoder, (msg['id'] as string) ?? '', managed.id, {
|
|
563
|
+
ex: m,
|
|
564
|
+
err: m + '\n',
|
|
565
|
+
ns: managed.session.currentNs,
|
|
566
|
+
status: ['eval-error', 'done'],
|
|
567
|
+
})
|
|
568
|
+
})
|
|
484
569
|
break
|
|
485
570
|
case 'load-file':
|
|
486
|
-
handleLoadFile(msg, managed, encoder)
|
|
571
|
+
void handleLoadFile(msg, managed, encoder).catch((e) => {
|
|
572
|
+
const m = e instanceof Error ? e.message : String(e)
|
|
573
|
+
done(encoder, (msg['id'] as string) ?? '', managed.id, {
|
|
574
|
+
ex: m,
|
|
575
|
+
err: m + '\n',
|
|
576
|
+
ns: managed.session.currentNs,
|
|
577
|
+
status: ['eval-error', 'done'],
|
|
578
|
+
})
|
|
579
|
+
})
|
|
487
580
|
break
|
|
488
581
|
case 'complete':
|
|
489
582
|
handleComplete(msg, managed, encoder)
|
|
@@ -513,6 +606,26 @@ export type NreplServerOptions = {
|
|
|
513
606
|
port?: number
|
|
514
607
|
host?: string
|
|
515
608
|
sourceRoots?: string[]
|
|
609
|
+
/** Pre-created session (with modules already installed). Will be snapshotted internally. */
|
|
610
|
+
session?: Session
|
|
611
|
+
/** Or pass a pre-taken snapshot directly. Takes precedence over `session`. */
|
|
612
|
+
snapshot?: SessionSnapshot
|
|
613
|
+
/** Optional mesh node for routing eval ops when mesh/*eval-target* is set. */
|
|
614
|
+
meshNode?: RemoteEvalNode
|
|
615
|
+
/** Write .nrepl-port to cwd on listen. Default: true. Set false for embedded/server use. */
|
|
616
|
+
writePortFile?: boolean
|
|
617
|
+
/**
|
|
618
|
+
* Called for every stdout chunk from any managed session (i.e. local evals from the editor).
|
|
619
|
+
* Use this to echo output to the server terminal alongside the Calva encoder stream.
|
|
620
|
+
* Example: onOutput: (t) => process.stdout.write(t)
|
|
621
|
+
*/
|
|
622
|
+
onOutput?: (text: string) => void
|
|
623
|
+
/**
|
|
624
|
+
* Called when (:require ["specifier" :as Alias]) is encountered in an eval.
|
|
625
|
+
* Forwarded to every managed session cloned from the snapshot.
|
|
626
|
+
* Example: importModule: (s) => import(s)
|
|
627
|
+
*/
|
|
628
|
+
importModule?: (specifier: string) => unknown | Promise<unknown>
|
|
516
629
|
}
|
|
517
630
|
|
|
518
631
|
export function startNreplServer(options: NreplServerOptions = {}): net.Server {
|
|
@@ -520,12 +633,19 @@ export function startNreplServer(options: NreplServerOptions = {}): net.Server {
|
|
|
520
633
|
const host = options.host ?? '127.0.0.1'
|
|
521
634
|
|
|
522
635
|
// Build a warm snapshot once — all clones skip the core bootstrap.
|
|
523
|
-
//
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
636
|
+
// Callers may provide a pre-created session (with extra modules installed) or snapshot.
|
|
637
|
+
const snapshot: SessionSnapshot =
|
|
638
|
+
options.snapshot ??
|
|
639
|
+
(options.session
|
|
640
|
+
? snapshotSession(options.session)
|
|
641
|
+
: snapshotSession(
|
|
642
|
+
createSession({
|
|
643
|
+
sourceRoots: options.sourceRoots,
|
|
644
|
+
readFile: (filePath) => readFileSync(filePath, 'utf8'),
|
|
645
|
+
})
|
|
646
|
+
))
|
|
647
|
+
|
|
648
|
+
const { meshNode, onOutput, importModule } = options
|
|
529
649
|
|
|
530
650
|
const server = net.createServer((socket) => {
|
|
531
651
|
const encoder = new BEncoderStream()
|
|
@@ -538,11 +658,28 @@ export function startNreplServer(options: NreplServerOptions = {}): net.Server {
|
|
|
538
658
|
|
|
539
659
|
// A default session for session-less messages (e.g. Calva's initial eval)
|
|
540
660
|
const defaultId = makeSessionId()
|
|
541
|
-
const defaultSession = createManagedSession(
|
|
661
|
+
const defaultSession = createManagedSession(
|
|
662
|
+
defaultId,
|
|
663
|
+
snapshot,
|
|
664
|
+
encoder,
|
|
665
|
+
options.sourceRoots,
|
|
666
|
+
onOutput,
|
|
667
|
+
importModule
|
|
668
|
+
)
|
|
542
669
|
sessions.set(defaultId, defaultSession)
|
|
543
670
|
|
|
544
671
|
decoder.on('data', (msg: NreplMessage) => {
|
|
545
|
-
handleMessage(
|
|
672
|
+
handleMessage(
|
|
673
|
+
msg,
|
|
674
|
+
sessions,
|
|
675
|
+
snapshot,
|
|
676
|
+
encoder,
|
|
677
|
+
defaultSession,
|
|
678
|
+
options.sourceRoots,
|
|
679
|
+
meshNode,
|
|
680
|
+
onOutput,
|
|
681
|
+
importModule
|
|
682
|
+
)
|
|
546
683
|
})
|
|
547
684
|
|
|
548
685
|
socket.on('error', () => {
|
|
@@ -555,25 +692,32 @@ export function startNreplServer(options: NreplServerOptions = {}): net.Server {
|
|
|
555
692
|
})
|
|
556
693
|
|
|
557
694
|
const portFile = join(process.cwd(), '.nrepl-port')
|
|
558
|
-
|
|
559
|
-
server.listen(port, host, () => {
|
|
560
|
-
writeFileSync(portFile, String(port), 'utf8')
|
|
561
|
-
process.stdout.write(`Conjure nREPL server v${VERSION} started on port ${port}\n`)
|
|
562
|
-
})
|
|
695
|
+
const writePortFile = options.writePortFile ?? true
|
|
563
696
|
|
|
564
697
|
const cleanup = () => {
|
|
565
698
|
if (existsSync(portFile)) unlinkSync(portFile)
|
|
566
699
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
700
|
+
|
|
701
|
+
if (writePortFile) {
|
|
702
|
+
server.listen(port, host, () => {
|
|
703
|
+
writeFileSync(portFile, String(port), 'utf8')
|
|
704
|
+
process.stdout.write(
|
|
705
|
+
`Conjure nREPL server v${VERSION} started on port ${port}\n`
|
|
706
|
+
)
|
|
707
|
+
})
|
|
708
|
+
server.on('close', cleanup)
|
|
709
|
+
process.on('exit', cleanup)
|
|
710
|
+
process.on('SIGINT', () => {
|
|
711
|
+
cleanup()
|
|
712
|
+
process.exit(0)
|
|
713
|
+
})
|
|
714
|
+
process.on('SIGTERM', () => {
|
|
715
|
+
cleanup()
|
|
716
|
+
process.exit(0)
|
|
717
|
+
})
|
|
718
|
+
} else {
|
|
719
|
+
server.listen(port, host)
|
|
720
|
+
}
|
|
577
721
|
|
|
578
722
|
return server
|
|
579
723
|
}
|
package/src/bin/version.ts
CHANGED