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
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import * as net from 'node:net'
|
|
2
|
+
import { writeFileSync, unlinkSync, existsSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { BDecoderStream, BEncoderStream } from '../bin/bencode'
|
|
5
|
+
import type { Session } from '../core'
|
|
6
|
+
import type { WebSocketServer } from 'vite'
|
|
7
|
+
import { VERSION } from '../bin/version'
|
|
8
|
+
import { resolveSymbol, extractMeta } from '../bin/nrepl-symbol'
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Types
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
type NreplMessage = Record<string, unknown>
|
|
15
|
+
|
|
16
|
+
/** Lightweight per-Calva-session state — no Clojure session, just bookkeeping */
|
|
17
|
+
type RelaySession = {
|
|
18
|
+
id: string
|
|
19
|
+
currentNs: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type BrowserResult = {
|
|
23
|
+
id: string
|
|
24
|
+
value?: string
|
|
25
|
+
error?: string
|
|
26
|
+
ns?: string
|
|
27
|
+
out?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type PendingResolver = (result: BrowserResult) => void
|
|
31
|
+
|
|
32
|
+
export type BrowserNreplRelayOptions = {
|
|
33
|
+
port?: number
|
|
34
|
+
host?: string
|
|
35
|
+
cwd: string
|
|
36
|
+
ws: WebSocketServer
|
|
37
|
+
serverSession: Session
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Protocol helpers
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
function makeId(): string {
|
|
45
|
+
return crypto.randomUUID()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function send(encoder: BEncoderStream, msg: NreplMessage) {
|
|
49
|
+
encoder.write(msg)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function done(
|
|
53
|
+
encoder: BEncoderStream,
|
|
54
|
+
id: string,
|
|
55
|
+
sessionId: string | undefined,
|
|
56
|
+
extra: NreplMessage = {}
|
|
57
|
+
) {
|
|
58
|
+
send(encoder, {
|
|
59
|
+
id,
|
|
60
|
+
...(sessionId ? { session: sessionId } : {}),
|
|
61
|
+
status: ['done'],
|
|
62
|
+
...extra,
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Browser forwarding
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
async function forwardToB(
|
|
71
|
+
event: string,
|
|
72
|
+
data: Record<string, unknown>,
|
|
73
|
+
ws: WebSocketServer,
|
|
74
|
+
pending: Map<string, PendingResolver>,
|
|
75
|
+
timeoutMs = 15000
|
|
76
|
+
): Promise<BrowserResult> {
|
|
77
|
+
const correlationId = makeId()
|
|
78
|
+
|
|
79
|
+
if (ws.clients.size === 0) {
|
|
80
|
+
return { id: correlationId, error: 'No browser tab connected to Vite dev server' }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return new Promise<BrowserResult>((resolve) => {
|
|
84
|
+
const timer = setTimeout(() => {
|
|
85
|
+
if (pending.has(correlationId)) {
|
|
86
|
+
pending.delete(correlationId)
|
|
87
|
+
resolve({ id: correlationId, error: 'Timed out — no response from browser (15s)' })
|
|
88
|
+
}
|
|
89
|
+
}, timeoutMs)
|
|
90
|
+
|
|
91
|
+
pending.set(correlationId, (result) => {
|
|
92
|
+
clearTimeout(timer)
|
|
93
|
+
resolve(result)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
ws.send({ type: 'custom', event, data: { ...data, id: correlationId } })
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Op handlers
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
function handleClone(
|
|
105
|
+
msg: NreplMessage,
|
|
106
|
+
sessions: Map<string, RelaySession>,
|
|
107
|
+
encoder: BEncoderStream
|
|
108
|
+
) {
|
|
109
|
+
const id = (msg['id'] as string) ?? ''
|
|
110
|
+
const newId = makeId()
|
|
111
|
+
sessions.set(newId, { id: newId, currentNs: 'user' })
|
|
112
|
+
done(encoder, id, undefined, { 'new-session': newId })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function handleDescribe(msg: NreplMessage, encoder: BEncoderStream) {
|
|
116
|
+
const id = (msg['id'] as string) ?? ''
|
|
117
|
+
const sessionId = msg['session'] as string | undefined
|
|
118
|
+
done(encoder, id, sessionId, {
|
|
119
|
+
ops: {
|
|
120
|
+
eval: {},
|
|
121
|
+
clone: {},
|
|
122
|
+
close: {},
|
|
123
|
+
complete: {},
|
|
124
|
+
describe: {},
|
|
125
|
+
eldoc: {},
|
|
126
|
+
info: {},
|
|
127
|
+
lookup: {},
|
|
128
|
+
'load-file': {},
|
|
129
|
+
},
|
|
130
|
+
versions: { conjure: { 'version-string': VERSION } },
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function handleComplete(
|
|
135
|
+
msg: NreplMessage,
|
|
136
|
+
session: RelaySession,
|
|
137
|
+
encoder: BEncoderStream,
|
|
138
|
+
serverSession: Session
|
|
139
|
+
) {
|
|
140
|
+
const id = (msg['id'] as string) ?? ''
|
|
141
|
+
const prefix = (msg['prefix'] as string) ?? ''
|
|
142
|
+
const nsName = (msg['ns'] as string) ?? session.currentNs
|
|
143
|
+
const names = serverSession.getCompletions(prefix, nsName)
|
|
144
|
+
const completions = names.map((c) => ({
|
|
145
|
+
candidate: c,
|
|
146
|
+
type: 'var',
|
|
147
|
+
ns: session.currentNs,
|
|
148
|
+
}))
|
|
149
|
+
done(encoder, id, session.id, { completions })
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function handleClose(
|
|
153
|
+
msg: NreplMessage,
|
|
154
|
+
sessions: Map<string, RelaySession>,
|
|
155
|
+
encoder: BEncoderStream
|
|
156
|
+
) {
|
|
157
|
+
const id = (msg['id'] as string) ?? ''
|
|
158
|
+
const sessionId = (msg['session'] as string) ?? ''
|
|
159
|
+
sessions.delete(sessionId)
|
|
160
|
+
send(encoder, { id, session: sessionId, status: ['done'] })
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function handleInfo(
|
|
164
|
+
msg: NreplMessage,
|
|
165
|
+
session: RelaySession,
|
|
166
|
+
encoder: BEncoderStream,
|
|
167
|
+
serverSession: Session
|
|
168
|
+
) {
|
|
169
|
+
const id = (msg['id'] as string) ?? ''
|
|
170
|
+
const sym = msg['sym'] as string | undefined
|
|
171
|
+
const nsOverride = msg['ns'] as string | undefined
|
|
172
|
+
|
|
173
|
+
if (!sym) {
|
|
174
|
+
done(encoder, id, session.id, { status: ['no-info', 'done'] })
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const resolved = resolveSymbol(sym, serverSession, nsOverride ?? session.currentNs)
|
|
179
|
+
if (!resolved) {
|
|
180
|
+
done(encoder, id, session.id, { status: ['no-info', 'done'] })
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const meta = extractMeta(resolved.value, resolved.varObj?.meta)
|
|
185
|
+
done(encoder, id, session.id, {
|
|
186
|
+
ns: resolved.resolvedNs,
|
|
187
|
+
name: resolved.localName,
|
|
188
|
+
doc: meta.doc,
|
|
189
|
+
'arglists-str': meta.arglistsStr,
|
|
190
|
+
type: meta.type,
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function handleEldoc(
|
|
195
|
+
msg: NreplMessage,
|
|
196
|
+
session: RelaySession,
|
|
197
|
+
encoder: BEncoderStream,
|
|
198
|
+
serverSession: Session
|
|
199
|
+
) {
|
|
200
|
+
const id = (msg['id'] as string) ?? ''
|
|
201
|
+
const sym = msg['sym'] as string | undefined
|
|
202
|
+
const nsOverride = msg['ns'] as string | undefined
|
|
203
|
+
|
|
204
|
+
if (!sym) {
|
|
205
|
+
done(encoder, id, session.id, { status: ['no-eldoc', 'done'] })
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const resolved = resolveSymbol(sym, serverSession, nsOverride ?? session.currentNs)
|
|
210
|
+
if (!resolved) {
|
|
211
|
+
done(encoder, id, session.id, { status: ['no-eldoc', 'done'] })
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const meta = extractMeta(resolved.value, resolved.varObj?.meta)
|
|
216
|
+
if (!meta.eldocArgs) {
|
|
217
|
+
done(encoder, id, session.id, { status: ['no-eldoc', 'done'] })
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
done(encoder, id, session.id, {
|
|
222
|
+
name: resolved.localName,
|
|
223
|
+
ns: resolved.resolvedNs,
|
|
224
|
+
type: meta.type,
|
|
225
|
+
eldoc: meta.eldocArgs,
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function handleUnknown(msg: NreplMessage, encoder: BEncoderStream) {
|
|
230
|
+
const id = (msg['id'] as string) ?? ''
|
|
231
|
+
send(encoder, { id, status: ['unknown-op', 'done'] })
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function handleEval(
|
|
235
|
+
msg: NreplMessage,
|
|
236
|
+
session: RelaySession,
|
|
237
|
+
encoder: BEncoderStream,
|
|
238
|
+
ws: WebSocketServer,
|
|
239
|
+
pending: Map<string, PendingResolver>
|
|
240
|
+
) {
|
|
241
|
+
const id = (msg['id'] as string) ?? ''
|
|
242
|
+
const code = (msg['code'] as string) ?? ''
|
|
243
|
+
|
|
244
|
+
const result = await forwardToB(
|
|
245
|
+
'conjure:eval',
|
|
246
|
+
{ code, ns: session.currentNs },
|
|
247
|
+
ws,
|
|
248
|
+
pending
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if (result.ns) session.currentNs = result.ns
|
|
252
|
+
if (result.out) send(encoder, { id, session: session.id, out: result.out })
|
|
253
|
+
|
|
254
|
+
if (result.error) {
|
|
255
|
+
done(encoder, id, session.id, {
|
|
256
|
+
ex: result.error,
|
|
257
|
+
err: result.error + '\n',
|
|
258
|
+
ns: session.currentNs,
|
|
259
|
+
status: ['eval-error', 'done'],
|
|
260
|
+
})
|
|
261
|
+
} else {
|
|
262
|
+
done(encoder, id, session.id, { value: result.value ?? 'nil', ns: session.currentNs })
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function handleLoadFile(
|
|
267
|
+
msg: NreplMessage,
|
|
268
|
+
session: RelaySession,
|
|
269
|
+
encoder: BEncoderStream,
|
|
270
|
+
ws: WebSocketServer,
|
|
271
|
+
pending: Map<string, PendingResolver>
|
|
272
|
+
) {
|
|
273
|
+
const id = (msg['id'] as string) ?? ''
|
|
274
|
+
const source = (msg['file'] as string) ?? ''
|
|
275
|
+
const fileName = (msg['file-name'] as string) ?? ''
|
|
276
|
+
const filePath = (msg['file-path'] as string) ?? ''
|
|
277
|
+
const nsHint = fileName.replace(/\.clj$/, '').replace(/\//g, '.') || undefined
|
|
278
|
+
|
|
279
|
+
const result = await forwardToB(
|
|
280
|
+
'conjure:load-file',
|
|
281
|
+
{ source, nsHint, filePath },
|
|
282
|
+
ws,
|
|
283
|
+
pending
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
if (result.ns) session.currentNs = result.ns
|
|
287
|
+
if (result.out) send(encoder, { id, session: session.id, out: result.out })
|
|
288
|
+
|
|
289
|
+
if (result.error) {
|
|
290
|
+
done(encoder, id, session.id, {
|
|
291
|
+
ex: result.error,
|
|
292
|
+
err: result.error + '\n',
|
|
293
|
+
ns: session.currentNs,
|
|
294
|
+
status: ['eval-error', 'done'],
|
|
295
|
+
})
|
|
296
|
+
} else {
|
|
297
|
+
done(encoder, id, session.id, { value: result.value ?? 'nil', ns: session.currentNs })
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
// Per-connection dispatcher
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
async function handleMessage(
|
|
306
|
+
msg: NreplMessage,
|
|
307
|
+
sessions: Map<string, RelaySession>,
|
|
308
|
+
defaultSession: RelaySession,
|
|
309
|
+
encoder: BEncoderStream,
|
|
310
|
+
ws: WebSocketServer,
|
|
311
|
+
pending: Map<string, PendingResolver>,
|
|
312
|
+
serverSession: Session
|
|
313
|
+
) {
|
|
314
|
+
const op = msg['op'] as string
|
|
315
|
+
const sessionId = msg['session'] as string | undefined
|
|
316
|
+
const session = sessionId ? (sessions.get(sessionId) ?? defaultSession) : defaultSession
|
|
317
|
+
|
|
318
|
+
switch (op) {
|
|
319
|
+
case 'clone':
|
|
320
|
+
handleClone(msg, sessions, encoder)
|
|
321
|
+
break
|
|
322
|
+
case 'describe':
|
|
323
|
+
handleDescribe(msg, encoder)
|
|
324
|
+
break
|
|
325
|
+
case 'eval':
|
|
326
|
+
await handleEval(msg, session, encoder, ws, pending)
|
|
327
|
+
break
|
|
328
|
+
case 'load-file':
|
|
329
|
+
await handleLoadFile(msg, session, encoder, ws, pending)
|
|
330
|
+
break
|
|
331
|
+
case 'complete':
|
|
332
|
+
handleComplete(msg, session, encoder, serverSession)
|
|
333
|
+
break
|
|
334
|
+
case 'close':
|
|
335
|
+
handleClose(msg, sessions, encoder)
|
|
336
|
+
break
|
|
337
|
+
case 'info':
|
|
338
|
+
case 'lookup':
|
|
339
|
+
handleInfo(msg, session, encoder, serverSession)
|
|
340
|
+
break
|
|
341
|
+
case 'eldoc':
|
|
342
|
+
handleEldoc(msg, session, encoder, serverSession)
|
|
343
|
+
break
|
|
344
|
+
default:
|
|
345
|
+
handleUnknown(msg, encoder)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
// Startup
|
|
351
|
+
// ---------------------------------------------------------------------------
|
|
352
|
+
|
|
353
|
+
export function startBrowserNreplRelay(options: BrowserNreplRelayOptions): net.Server {
|
|
354
|
+
const port = options.port ?? 7888
|
|
355
|
+
const host = options.host ?? '127.0.0.1'
|
|
356
|
+
const { ws, serverSession, cwd } = options
|
|
357
|
+
|
|
358
|
+
// Shared pending map — keyed by correlation ID, resolved when browser responds
|
|
359
|
+
const pending = new Map<string, PendingResolver>()
|
|
360
|
+
|
|
361
|
+
// Wire up browser result listeners once at server level
|
|
362
|
+
ws.on('conjure:eval-result', (data: BrowserResult) => {
|
|
363
|
+
const resolve = pending.get(data.id)
|
|
364
|
+
if (resolve) {
|
|
365
|
+
pending.delete(data.id)
|
|
366
|
+
resolve(data)
|
|
367
|
+
}
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
ws.on('conjure:load-file-result', (data: BrowserResult) => {
|
|
371
|
+
const resolve = pending.get(data.id)
|
|
372
|
+
if (resolve) {
|
|
373
|
+
pending.delete(data.id)
|
|
374
|
+
resolve(data)
|
|
375
|
+
}
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
const server = net.createServer((socket) => {
|
|
379
|
+
const encoder = new BEncoderStream()
|
|
380
|
+
const decoder = new BDecoderStream()
|
|
381
|
+
|
|
382
|
+
encoder.pipe(socket)
|
|
383
|
+
socket.pipe(decoder)
|
|
384
|
+
|
|
385
|
+
const sessions = new Map<string, RelaySession>()
|
|
386
|
+
const defaultId = makeId()
|
|
387
|
+
const defaultSession: RelaySession = { id: defaultId, currentNs: 'user' }
|
|
388
|
+
sessions.set(defaultId, defaultSession)
|
|
389
|
+
|
|
390
|
+
decoder.on('data', (msg: NreplMessage) => {
|
|
391
|
+
handleMessage(msg, sessions, defaultSession, encoder, ws, pending, serverSession).catch(
|
|
392
|
+
(err) => {
|
|
393
|
+
console.error('[conjure] relay error:', err)
|
|
394
|
+
}
|
|
395
|
+
)
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
socket.on('error', () => {
|
|
399
|
+
// connection dropped
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
socket.on('close', () => {
|
|
403
|
+
sessions.clear()
|
|
404
|
+
})
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
const portFile = join(cwd, '.nrepl-port')
|
|
408
|
+
|
|
409
|
+
server.on('error', (err: NodeJS.ErrnoException) => {
|
|
410
|
+
if (err.code === 'EADDRINUSE') {
|
|
411
|
+
console.warn(
|
|
412
|
+
`[conjure] Port ${port} already in use — browser nREPL relay not started. ` +
|
|
413
|
+
`Kill the process holding the port or set a different nreplPort.`
|
|
414
|
+
)
|
|
415
|
+
} else {
|
|
416
|
+
console.error('[conjure] nREPL relay error:', err.message)
|
|
417
|
+
}
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
server.listen(port, host, () => {
|
|
421
|
+
writeFileSync(portFile, String(port), 'utf8')
|
|
422
|
+
console.log(`[conjure] Browser nREPL relay started on port ${port}`)
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
const cleanup = () => {
|
|
426
|
+
if (existsSync(portFile)) unlinkSync(portFile)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
server.on('close', cleanup)
|
|
430
|
+
|
|
431
|
+
return server
|
|
432
|
+
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import { extractNsName, extractNsRequires } from './namespace-utils'
|
|
1
|
+
import type { Arity, DestructurePattern } from '../core/types'
|
|
2
|
+
import { extractNsName, extractNsRequires, extractStringRequires } from './namespace-utils'
|
|
3
|
+
import { readNamespaceVars } from './static-analysis'
|
|
5
4
|
|
|
6
5
|
export interface CodegenContext {
|
|
7
|
-
session: Session
|
|
8
6
|
sourceRoots: string[]
|
|
9
7
|
coreIndexPath: string
|
|
10
8
|
virtualSessionId: string
|
|
@@ -14,11 +12,13 @@ export interface CodegenContext {
|
|
|
14
12
|
export function generateModuleCode(
|
|
15
13
|
ctx: CodegenContext,
|
|
16
14
|
nsNameFromPath: string,
|
|
17
|
-
source: string
|
|
15
|
+
source: string,
|
|
16
|
+
filePath?: string
|
|
18
17
|
): string {
|
|
19
18
|
const nsName = extractNsName(source) ?? nsNameFromPath
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
// Detect string requires from AST — determines sync vs async load call.
|
|
21
|
+
const hasStringRequires = extractStringRequires(source, filePath).length > 0
|
|
22
22
|
|
|
23
23
|
const requires = extractNsRequires(source)
|
|
24
24
|
const depImports = requires
|
|
@@ -30,91 +30,120 @@ export function generateModuleCode(
|
|
|
30
30
|
.filter(Boolean)
|
|
31
31
|
.join('\n')
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return `throw new Error('Namespace ${nsName} failed to load');`
|
|
36
|
-
}
|
|
37
|
-
|
|
33
|
+
// Static analysis: pure AST walk, no execution.
|
|
34
|
+
const vars = readNamespaceVars(source)
|
|
38
35
|
const exportLines: string[] = []
|
|
39
|
-
for (const [name, v] of nsData.vars) {
|
|
40
|
-
const value = v.value
|
|
41
|
-
if (isMacro(value)) continue
|
|
42
36
|
|
|
43
|
-
|
|
37
|
+
for (const descriptor of vars) {
|
|
38
|
+
if (descriptor.isMacro) continue
|
|
39
|
+
if (descriptor.isPrivate) continue
|
|
40
|
+
|
|
41
|
+
const safeName = safeJsIdentifier(descriptor.name)
|
|
44
42
|
// At runtime, vars.get() returns a CljVar; deref with .value
|
|
45
|
-
const deref = `__ns.vars.get(${JSON.stringify(name)}).value`
|
|
46
|
-
|
|
43
|
+
const deref = `__ns.vars.get(${JSON.stringify(descriptor.name)}).value`
|
|
44
|
+
|
|
45
|
+
if (descriptor.kind === 'fn') {
|
|
47
46
|
exportLines.push(
|
|
48
47
|
`export function ${safeName}(...args) {` +
|
|
49
48
|
` const fn = ${deref};` +
|
|
50
49
|
` const cljArgs = args.map(jsToClj);` +
|
|
51
|
-
` const result = applyFunction(fn, cljArgs);` +
|
|
52
|
-
` return cljToJs(result);` +
|
|
50
|
+
` const result = __session.applyFunction(fn, cljArgs);` +
|
|
51
|
+
` return cljToJs(result, __session);` +
|
|
53
52
|
`}`
|
|
54
53
|
)
|
|
55
54
|
} else {
|
|
56
55
|
exportLines.push(
|
|
57
|
-
`export const ${safeName} = cljToJs(${deref});`
|
|
56
|
+
`export const ${safeName} = cljToJs(${deref}, __session);`
|
|
58
57
|
)
|
|
59
58
|
}
|
|
60
59
|
}
|
|
61
60
|
|
|
62
61
|
const escapedSource = JSON.stringify(source)
|
|
62
|
+
// Files with string requires need async loading (top-level await, requires target: esnext).
|
|
63
|
+
// Files without string requires use the sync path — no top-level await overhead.
|
|
64
|
+
const loadCall = hasStringRequires
|
|
65
|
+
? `await __session.loadFileAsync(${escapedSource}, ${JSON.stringify(nsName)});`
|
|
66
|
+
: `__session.loadFile(${escapedSource}, ${JSON.stringify(nsName)});`
|
|
67
|
+
|
|
68
|
+
if (exportLines.length === 0) {
|
|
69
|
+
// No public exports — emit a minimal module that loads the namespace at runtime.
|
|
70
|
+
// Namespace will be available in the session even without JS-side exports.
|
|
71
|
+
return [
|
|
72
|
+
`import { getSession } from ${JSON.stringify(ctx.virtualSessionId)};`,
|
|
73
|
+
depImports,
|
|
74
|
+
``,
|
|
75
|
+
`const __session = getSession();`,
|
|
76
|
+
loadCall,
|
|
77
|
+
``,
|
|
78
|
+
`if (import.meta.hot) { import.meta.hot.accept() }`,
|
|
79
|
+
].join('\n')
|
|
80
|
+
}
|
|
63
81
|
|
|
64
82
|
return [
|
|
65
83
|
`import { getSession } from ${JSON.stringify(ctx.virtualSessionId)};`,
|
|
66
|
-
`import { cljToJs, jsToClj
|
|
84
|
+
`import { cljToJs, jsToClj } from ${JSON.stringify(ctx.coreIndexPath)};`,
|
|
67
85
|
depImports,
|
|
68
86
|
``,
|
|
69
87
|
`const __session = getSession();`,
|
|
70
|
-
|
|
88
|
+
loadCall,
|
|
71
89
|
`const __ns = __session.getNs(${JSON.stringify(nsName)});`,
|
|
72
90
|
``,
|
|
73
91
|
...exportLines,
|
|
92
|
+
``,
|
|
93
|
+
`// Self-accept HMR: re-execute this module on save (updates browser session)`,
|
|
94
|
+
`// without propagating to parent modules — prevents full page reload.`,
|
|
95
|
+
`if (import.meta.hot) { import.meta.hot.accept() }`,
|
|
74
96
|
].join('\n')
|
|
75
97
|
}
|
|
76
98
|
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
|
|
99
|
+
export function generateDts(
|
|
100
|
+
_ctx: CodegenContext,
|
|
101
|
+
nsNameFromPath: string,
|
|
102
|
+
source: string
|
|
103
|
+
): string {
|
|
104
|
+
const nsName = extractNsName(source) ?? nsNameFromPath
|
|
105
|
+
const vars = readNamespaceVars(source)
|
|
80
106
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
case 'native-function':
|
|
102
|
-
return '(...args: unknown[]) => unknown'
|
|
103
|
-
case 'macro':
|
|
104
|
-
return 'never'
|
|
105
|
-
case 'var':
|
|
106
|
-
return 'unknown'
|
|
107
|
-
default:
|
|
108
|
-
throw new Error(`Unknown CljValue kind: ${value.kind}`)
|
|
107
|
+
const declarations: string[] = []
|
|
108
|
+
for (const descriptor of vars) {
|
|
109
|
+
if (descriptor.isMacro) continue
|
|
110
|
+
if (descriptor.isPrivate) continue
|
|
111
|
+
|
|
112
|
+
const safeName = safeJsIdentifier(descriptor.name)
|
|
113
|
+
|
|
114
|
+
if (descriptor.kind === 'fn') {
|
|
115
|
+
if (descriptor.arities && descriptor.arities.length > 0) {
|
|
116
|
+
for (const arity of descriptor.arities) {
|
|
117
|
+
declarations.push(`export function ${safeName}${arityToSignature(arity)};`)
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
declarations.push(`export function ${safeName}(...args: unknown[]): unknown;`)
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
// 'const' with inferred type, or 'unknown'
|
|
124
|
+
const tsType = descriptor.tsType ?? 'unknown'
|
|
125
|
+
declarations.push(`export const ${safeName}: ${tsType};`)
|
|
126
|
+
}
|
|
109
127
|
}
|
|
128
|
+
|
|
129
|
+
// Suppress the unused-variable warning — nsName is used for documentation only here
|
|
130
|
+
void nsName
|
|
131
|
+
|
|
132
|
+
return declarations.join('\n')
|
|
110
133
|
}
|
|
111
134
|
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Signature helpers
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
type ArityShape = { params: DestructurePattern[]; restParam: DestructurePattern | null }
|
|
140
|
+
|
|
112
141
|
function patternName(p: Arity['params'][number], index: number): string {
|
|
113
142
|
if (p.kind === 'symbol') return safeJsIdentifier(p.name)
|
|
114
143
|
return `arg${index}`
|
|
115
144
|
}
|
|
116
145
|
|
|
117
|
-
function arityToSignature(arity:
|
|
146
|
+
function arityToSignature(arity: ArityShape): string {
|
|
118
147
|
const fixedParams = arity.params
|
|
119
148
|
.map((p, i) => `${patternName(p, i)}: unknown`)
|
|
120
149
|
.join(', ')
|
|
@@ -133,46 +162,9 @@ function arityToSignature(arity: Arity): string {
|
|
|
133
162
|
return `(${fixedParams}): unknown`
|
|
134
163
|
}
|
|
135
164
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
source: string
|
|
140
|
-
): string {
|
|
141
|
-
const nsName = extractNsName(source) ?? nsNameFromPath
|
|
142
|
-
|
|
143
|
-
try {
|
|
144
|
-
ctx.session.loadFile(source, nsName)
|
|
145
|
-
} catch {
|
|
146
|
-
return ''
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const nsData = ctx.session.getNs(nsName)
|
|
150
|
-
if (!nsData) return ''
|
|
151
|
-
|
|
152
|
-
const declarations: string[] = []
|
|
153
|
-
for (const [name, v] of nsData.vars) {
|
|
154
|
-
const value = v.value
|
|
155
|
-
if (isMacro(value)) continue
|
|
156
|
-
|
|
157
|
-
const safeName = safeJsIdentifier(name)
|
|
158
|
-
|
|
159
|
-
if (value.kind === 'function') {
|
|
160
|
-
for (const arity of value.arities) {
|
|
161
|
-
declarations.push(
|
|
162
|
-
`export function ${safeName}${arityToSignature(arity)};`
|
|
163
|
-
)
|
|
164
|
-
}
|
|
165
|
-
} else if (value.kind === 'native-function') {
|
|
166
|
-
declarations.push(
|
|
167
|
-
`export function ${safeName}(...args: unknown[]): unknown;`
|
|
168
|
-
)
|
|
169
|
-
} else {
|
|
170
|
-
declarations.push(`export const ${safeName}: ${cljValueToTsType(value)};`)
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return declarations.join('\n')
|
|
175
|
-
}
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Identifier sanitization
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
176
168
|
|
|
177
169
|
const JS_RESERVED_WORDS = new Set([
|
|
178
170
|
'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger',
|