conjure-js 0.0.12 → 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 +9360 -5298
- package/dist-vite-plugin/index.mjs +9463 -5185
- package/package.json +3 -1
- package/src/bin/cli.ts +2 -2
- package/src/bin/nrepl-symbol.ts +150 -0
- package/src/bin/nrepl.ts +289 -167
- package/src/bin/version.ts +1 -1
- package/src/clojure/core.clj +757 -29
- package/src/clojure/core.clj.d.ts +75 -131
- package/src/clojure/generated/builtin-namespace-registry.ts +4 -0
- package/src/clojure/generated/clojure-core-source.ts +758 -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 +20 -6
- 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 +28 -5
- package/src/core/evaluator/destructure.ts +180 -69
- package/src/core/evaluator/dispatch.ts +12 -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 +234 -191
- package/src/core/factories.ts +182 -3
- package/src/core/index.ts +54 -4
- package/src/core/module.ts +136 -0
- package/src/core/ns-forms.ts +107 -0
- package/src/core/printer.ts +371 -11
- package/src/core/reader.ts +84 -33
- package/src/core/registry.ts +209 -0
- package/src/core/runtime.ts +376 -0
- package/src/core/session.ts +253 -487
- package/src/core/stdlib/arithmetic.ts +528 -194
- package/src/core/stdlib/async-fns.ts +132 -0
- package/src/core/stdlib/atoms.ts +291 -56
- package/src/core/stdlib/errors.ts +54 -50
- package/src/core/stdlib/hof.ts +82 -166
- 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 +61 -30
- package/src/core/stdlib/predicates.ts +325 -187
- 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 +292 -130
- package/src/core/stdlib/vars.ts +27 -27
- package/src/core/stdlib/vectors.ts +122 -0
- package/src/core/tokenizer.ts +2 -2
- package/src/core/transformations.ts +117 -9
- package/src/core/types.ts +98 -2
- package/src/host/node-host-module.ts +74 -0
- package/src/{vite-plugin-clj/nrepl-relay.ts → nrepl/relay.ts} +72 -11
- package/src/vite-plugin-clj/codegen.ts +87 -95
- package/src/vite-plugin-clj/index.ts +178 -23
- 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 -72
- package/src/clojure/demo.clj.d.ts +0 -0
- package/src/core/core-env.ts +0 -61
- package/src/core/stdlib/collections.ts +0 -739
- 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,11 +164,12 @@ 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
|
|
|
@@ -148,13 +178,134 @@ function handleEval(
|
|
|
148
178
|
// Calva sends 1-based :line and :column for the start of the evaluated form
|
|
149
179
|
// in the original file. Convert to 0-based offsets so evaluateDef can add
|
|
150
180
|
// them to the relative positions the reader computes from the snippet.
|
|
151
|
-
const lineOffset =
|
|
152
|
-
|
|
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
|
+
}
|
|
153
282
|
|
|
154
283
|
try {
|
|
155
|
-
|
|
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
|
+
)
|
|
156
307
|
done(encoder, id, managed.id, {
|
|
157
|
-
value:
|
|
308
|
+
value: resultStr,
|
|
158
309
|
ns: managed.session.currentNs,
|
|
159
310
|
})
|
|
160
311
|
} catch (error) {
|
|
@@ -168,11 +319,11 @@ function handleEval(
|
|
|
168
319
|
}
|
|
169
320
|
}
|
|
170
321
|
|
|
171
|
-
function handleLoadFile(
|
|
322
|
+
async function handleLoadFile(
|
|
172
323
|
msg: NreplMessage,
|
|
173
324
|
managed: ManagedSession,
|
|
174
325
|
encoder: BEncoderStream
|
|
175
|
-
) {
|
|
326
|
+
): Promise<void> {
|
|
176
327
|
const id = (msg['id'] as string) ?? ''
|
|
177
328
|
const source = (msg['file'] as string) ?? ''
|
|
178
329
|
const fileName = (msg['file-name'] as string) ?? ''
|
|
@@ -193,7 +344,14 @@ function handleLoadFile(
|
|
|
193
344
|
|
|
194
345
|
const nsHint =
|
|
195
346
|
fileName.replace(/\.clj$/, '').replace(/\//g, '.') || undefined
|
|
196
|
-
|
|
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
|
+
)
|
|
197
355
|
|
|
198
356
|
// Track the file path for this namespace so info/lookup can return :file
|
|
199
357
|
// for go-to-definition support.
|
|
@@ -254,120 +412,15 @@ function resolveSymbol(
|
|
|
254
412
|
sym: string,
|
|
255
413
|
managed: ManagedSession,
|
|
256
414
|
contextNs?: string
|
|
257
|
-
)
|
|
258
|
-
|
|
259
|
-
const slashIdx = sym.indexOf('/')
|
|
260
|
-
|
|
261
|
-
if (slashIdx > 0) {
|
|
262
|
-
const qualifier = sym.slice(0, slashIdx)
|
|
263
|
-
const localName = sym.slice(slashIdx + 1)
|
|
264
|
-
|
|
265
|
-
// 1. Try as full namespace name (use registry Env for chain-based lookup)
|
|
266
|
-
const nsEnvFull = managed.session.registry.get(qualifier)
|
|
267
|
-
if (nsEnvFull) {
|
|
268
|
-
const value = tryLookup(localName, nsEnvFull)
|
|
269
|
-
if (value !== undefined) return { value, resolvedNs: qualifier, localName }
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// 2. Try as alias (:as str → clojure.string)
|
|
273
|
-
const currentNsData = managed.session.getNs(ns)
|
|
274
|
-
const aliasedNs = currentNsData?.aliases.get(qualifier)
|
|
275
|
-
if (aliasedNs) {
|
|
276
|
-
const v = aliasedNs.vars.get(localName)
|
|
277
|
-
if (v !== undefined)
|
|
278
|
-
return { value: v.value, resolvedNs: aliasedNs.name, localName }
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
return null
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Unqualified symbol
|
|
285
|
-
const localName = sym
|
|
286
|
-
const nsEnvFull = managed.session.registry.get(ns)
|
|
287
|
-
if (!nsEnvFull) return null
|
|
288
|
-
const value = tryLookup(sym, nsEnvFull)
|
|
289
|
-
if (value === undefined) return null
|
|
290
|
-
|
|
291
|
-
// Determine the namespace where this symbol is defined.
|
|
292
|
-
// CljVar carries the authoritative ns; fall back to other heuristics.
|
|
293
|
-
const varObj = lookupVar(sym, nsEnvFull)
|
|
294
|
-
let resolvedNs: string
|
|
295
|
-
if (varObj) {
|
|
296
|
-
resolvedNs = varObj.ns
|
|
297
|
-
} else if (value.kind === 'function' || value.kind === 'macro') {
|
|
298
|
-
resolvedNs = getNamespaceEnv(value.env).ns?.name ?? ns
|
|
299
|
-
} else if (value.kind === 'native-function') {
|
|
300
|
-
const i = value.name.indexOf('/')
|
|
301
|
-
resolvedNs = i > 0 ? value.name.slice(0, i) : ns
|
|
302
|
-
} else {
|
|
303
|
-
resolvedNs = ns
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
return { value, resolvedNs, localName }
|
|
415
|
+
) {
|
|
416
|
+
return resolveSymbolShared(sym, managed.session, contextNs)
|
|
307
417
|
}
|
|
308
418
|
|
|
309
|
-
function extractMeta(
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
} {
|
|
315
|
-
const type =
|
|
316
|
-
value.kind === 'macro'
|
|
317
|
-
? 'macro'
|
|
318
|
-
: value.kind === 'function' || value.kind === 'native-function'
|
|
319
|
-
? 'function'
|
|
320
|
-
: 'var'
|
|
321
|
-
|
|
322
|
-
const meta: CljMap | undefined =
|
|
323
|
-
value.kind === 'function'
|
|
324
|
-
? value.meta
|
|
325
|
-
: value.kind === 'native-function'
|
|
326
|
-
? value.meta
|
|
327
|
-
: undefined
|
|
328
|
-
|
|
329
|
-
let doc = ''
|
|
330
|
-
let arglistsStr = ''
|
|
331
|
-
let eldocArgs: string[][] | null = null
|
|
332
|
-
|
|
333
|
-
if (meta) {
|
|
334
|
-
const docEntry = meta.entries.find(
|
|
335
|
-
([k]) => k.kind === 'keyword' && k.name === ':doc'
|
|
336
|
-
)
|
|
337
|
-
if (docEntry && docEntry[1].kind === 'string') doc = docEntry[1].value
|
|
338
|
-
|
|
339
|
-
const argsEntry = meta.entries.find(
|
|
340
|
-
([k]) => k.kind === 'keyword' && k.name === ':arglists'
|
|
341
|
-
)
|
|
342
|
-
if (argsEntry && argsEntry[1].kind === 'vector') {
|
|
343
|
-
const arglists = argsEntry[1]
|
|
344
|
-
arglistsStr = '(' + arglists.value.map((al) => printString(al)).join(' ') + ')'
|
|
345
|
-
eldocArgs = arglists.value.map((al) => {
|
|
346
|
-
if (al.kind !== 'vector') return [printString(al)]
|
|
347
|
-
return al.value.map((p) => (p.kind === 'symbol' ? p.name : printString(p)))
|
|
348
|
-
})
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Fallback: derive arglists from structural arities (fn/macro without meta)
|
|
353
|
-
if (
|
|
354
|
-
arglistsStr === '' &&
|
|
355
|
-
(value.kind === 'function' || value.kind === 'macro')
|
|
356
|
-
) {
|
|
357
|
-
const arityStrs = 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.join(' ') + ']'
|
|
361
|
-
})
|
|
362
|
-
arglistsStr = '(' + arityStrs.join(' ') + ')'
|
|
363
|
-
eldocArgs = value.arities.map((arity) => {
|
|
364
|
-
const params = arity.params.map((p) => printString(p))
|
|
365
|
-
if (arity.restParam) params.push('&', printString(arity.restParam))
|
|
366
|
-
return params
|
|
367
|
-
})
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
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)
|
|
371
424
|
}
|
|
372
425
|
|
|
373
426
|
function handleInfo(
|
|
@@ -401,20 +454,19 @@ function handleInfo(
|
|
|
401
454
|
return
|
|
402
455
|
}
|
|
403
456
|
|
|
404
|
-
const meta = extractMeta(resolved
|
|
457
|
+
const meta = extractMeta(resolved)
|
|
405
458
|
const file = managed.nsToFile.get(resolved.resolvedNs)
|
|
406
459
|
|
|
407
460
|
// Extract :line/:column/:file from var meta if present (stamped by evaluateDef).
|
|
408
461
|
let varLine: number | undefined
|
|
409
462
|
let varColumn: number | undefined
|
|
410
463
|
let varFile: string | undefined
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
}
|
|
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
|
|
418
470
|
}
|
|
419
471
|
|
|
420
472
|
done(encoder, id, managed.id, {
|
|
@@ -423,8 +475,8 @@ function handleInfo(
|
|
|
423
475
|
doc: meta.doc,
|
|
424
476
|
'arglists-str': meta.arglistsStr,
|
|
425
477
|
type: meta.type,
|
|
426
|
-
...(varFile ?? file ? { file: varFile ?? file } : {}),
|
|
427
|
-
...(varLine
|
|
478
|
+
...((varFile ?? file) ? { file: varFile ?? file } : {}),
|
|
479
|
+
...(varLine !== undefined ? { line: varLine } : {}),
|
|
428
480
|
...(varColumn !== undefined ? { column: varColumn } : {}),
|
|
429
481
|
})
|
|
430
482
|
}
|
|
@@ -457,7 +509,7 @@ function handleEldoc(
|
|
|
457
509
|
return
|
|
458
510
|
}
|
|
459
511
|
|
|
460
|
-
const meta = extractMeta(resolved
|
|
512
|
+
const meta = extractMeta(resolved)
|
|
461
513
|
if (!meta.eldocArgs) {
|
|
462
514
|
done(encoder, id, managed.id, { status: ['no-eldoc', 'done'] })
|
|
463
515
|
return
|
|
@@ -486,7 +538,10 @@ function handleMessage(
|
|
|
486
538
|
snapshot: SessionSnapshot,
|
|
487
539
|
encoder: BEncoderStream,
|
|
488
540
|
defaultSession: ManagedSession,
|
|
489
|
-
sourceRoots?: string[]
|
|
541
|
+
sourceRoots?: string[],
|
|
542
|
+
meshNode?: RemoteEvalNode,
|
|
543
|
+
onOutput?: (text: string) => void,
|
|
544
|
+
importModule?: (specifier: string) => unknown | Promise<unknown>
|
|
490
545
|
) {
|
|
491
546
|
const op = msg['op'] as string
|
|
492
547
|
const sessionId = msg['session'] as string | undefined
|
|
@@ -496,16 +551,32 @@ function handleMessage(
|
|
|
496
551
|
|
|
497
552
|
switch (op) {
|
|
498
553
|
case 'clone':
|
|
499
|
-
handleClone(msg, sessions, snapshot, encoder, sourceRoots)
|
|
554
|
+
handleClone(msg, sessions, snapshot, encoder, sourceRoots, onOutput, importModule)
|
|
500
555
|
break
|
|
501
556
|
case 'describe':
|
|
502
557
|
handleDescribe(msg, encoder)
|
|
503
558
|
break
|
|
504
559
|
case 'eval':
|
|
505
|
-
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
|
+
})
|
|
506
569
|
break
|
|
507
570
|
case 'load-file':
|
|
508
|
-
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
|
+
})
|
|
509
580
|
break
|
|
510
581
|
case 'complete':
|
|
511
582
|
handleComplete(msg, managed, encoder)
|
|
@@ -535,6 +606,26 @@ export type NreplServerOptions = {
|
|
|
535
606
|
port?: number
|
|
536
607
|
host?: string
|
|
537
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>
|
|
538
629
|
}
|
|
539
630
|
|
|
540
631
|
export function startNreplServer(options: NreplServerOptions = {}): net.Server {
|
|
@@ -542,12 +633,19 @@ export function startNreplServer(options: NreplServerOptions = {}): net.Server {
|
|
|
542
633
|
const host = options.host ?? '127.0.0.1'
|
|
543
634
|
|
|
544
635
|
// Build a warm snapshot once — all clones skip the core bootstrap.
|
|
545
|
-
//
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
|
551
649
|
|
|
552
650
|
const server = net.createServer((socket) => {
|
|
553
651
|
const encoder = new BEncoderStream()
|
|
@@ -560,11 +658,28 @@ export function startNreplServer(options: NreplServerOptions = {}): net.Server {
|
|
|
560
658
|
|
|
561
659
|
// A default session for session-less messages (e.g. Calva's initial eval)
|
|
562
660
|
const defaultId = makeSessionId()
|
|
563
|
-
const defaultSession = createManagedSession(
|
|
661
|
+
const defaultSession = createManagedSession(
|
|
662
|
+
defaultId,
|
|
663
|
+
snapshot,
|
|
664
|
+
encoder,
|
|
665
|
+
options.sourceRoots,
|
|
666
|
+
onOutput,
|
|
667
|
+
importModule
|
|
668
|
+
)
|
|
564
669
|
sessions.set(defaultId, defaultSession)
|
|
565
670
|
|
|
566
671
|
decoder.on('data', (msg: NreplMessage) => {
|
|
567
|
-
handleMessage(
|
|
672
|
+
handleMessage(
|
|
673
|
+
msg,
|
|
674
|
+
sessions,
|
|
675
|
+
snapshot,
|
|
676
|
+
encoder,
|
|
677
|
+
defaultSession,
|
|
678
|
+
options.sourceRoots,
|
|
679
|
+
meshNode,
|
|
680
|
+
onOutput,
|
|
681
|
+
importModule
|
|
682
|
+
)
|
|
568
683
|
})
|
|
569
684
|
|
|
570
685
|
socket.on('error', () => {
|
|
@@ -577,25 +692,32 @@ export function startNreplServer(options: NreplServerOptions = {}): net.Server {
|
|
|
577
692
|
})
|
|
578
693
|
|
|
579
694
|
const portFile = join(process.cwd(), '.nrepl-port')
|
|
580
|
-
|
|
581
|
-
server.listen(port, host, () => {
|
|
582
|
-
writeFileSync(portFile, String(port), 'utf8')
|
|
583
|
-
process.stdout.write(`Conjure nREPL server v${VERSION} started on port ${port}\n`)
|
|
584
|
-
})
|
|
695
|
+
const writePortFile = options.writePortFile ?? true
|
|
585
696
|
|
|
586
697
|
const cleanup = () => {
|
|
587
698
|
if (existsSync(portFile)) unlinkSync(portFile)
|
|
588
699
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
+
}
|
|
599
721
|
|
|
600
722
|
return server
|
|
601
723
|
}
|
package/src/bin/version.ts
CHANGED