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
|
@@ -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',
|
|
@@ -1,18 +1,31 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process'
|
|
2
2
|
import { readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs'
|
|
3
3
|
import { resolve, relative, join, dirname } from 'node:path'
|
|
4
|
-
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
5
|
+
import { createRequire } from 'node:module'
|
|
5
6
|
import type { Plugin, ResolvedConfig, ViteDevServer } from 'vite'
|
|
6
7
|
import { createSession } from '../core/session'
|
|
7
8
|
import type { Session } from '../core/session'
|
|
8
|
-
import { nsToPath, pathToNs } from './namespace-utils'
|
|
9
|
+
import { nsToPath, pathToNs, extractStringRequires } from './namespace-utils'
|
|
9
10
|
import { generateModuleCode, generateDts, safeJsIdentifier } from './codegen'
|
|
10
11
|
import type { CodegenContext } from './codegen'
|
|
11
|
-
import { startBrowserNreplRelay } from '
|
|
12
|
+
import { startBrowserNreplRelay } from '../nrepl/relay'
|
|
12
13
|
|
|
13
14
|
interface CljPluginOptions {
|
|
14
15
|
sourceRoots?: string[]
|
|
15
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
|
|
16
29
|
}
|
|
17
30
|
|
|
18
31
|
// Resolve the conjure-js core index path regardless of whether this plugin
|
|
@@ -42,6 +55,12 @@ export function cljPlugin(options?: CljPluginOptions): Plugin {
|
|
|
42
55
|
let codegenCtx: CodegenContext
|
|
43
56
|
let generatorScriptPath: string
|
|
44
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
|
|
45
64
|
|
|
46
65
|
function writeFileIfChanged(path: string, content: string) {
|
|
47
66
|
try {
|
|
@@ -88,7 +107,7 @@ export function cljPlugin(options?: CljPluginOptions): Plugin {
|
|
|
88
107
|
const source = readFileSync(filePath, 'utf-8')
|
|
89
108
|
const nsNameFromPath = pathToNs(relative(projectRoot, filePath), sourceRoots)
|
|
90
109
|
const dts = generateDts(codegenCtx, nsNameFromPath, source)
|
|
91
|
-
writeFileIfChanged(filePath + '.d.ts', dts)
|
|
110
|
+
if (dts) writeFileIfChanged(filePath + '.d.ts', dts)
|
|
92
111
|
} catch {
|
|
93
112
|
continue
|
|
94
113
|
}
|
|
@@ -97,14 +116,45 @@ export function cljPlugin(options?: CljPluginOptions): Plugin {
|
|
|
97
116
|
}
|
|
98
117
|
|
|
99
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
|
+
|
|
100
124
|
serverSession = createSession({
|
|
101
125
|
sourceRoots,
|
|
102
126
|
readFile: (filePath: string) =>
|
|
103
127
|
readFileSync(resolve(projectRoot, filePath), 'utf-8'),
|
|
104
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
|
+
},
|
|
105
156
|
})
|
|
106
157
|
codegenCtx = {
|
|
107
|
-
session: serverSession,
|
|
108
158
|
sourceRoots,
|
|
109
159
|
coreIndexPath,
|
|
110
160
|
virtualSessionId: VIRTUAL_SESSION_ID,
|
|
@@ -123,6 +173,32 @@ export function cljPlugin(options?: CljPluginOptions): Plugin {
|
|
|
123
173
|
}
|
|
124
174
|
}
|
|
125
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
|
+
|
|
126
202
|
function regenerateBuiltInNamespaceSources() {
|
|
127
203
|
try {
|
|
128
204
|
statSync(generatorScriptPath)
|
|
@@ -142,6 +218,23 @@ export function cljPlugin(options?: CljPluginOptions): Plugin {
|
|
|
142
218
|
}
|
|
143
219
|
}
|
|
144
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
|
+
|
|
145
238
|
return {
|
|
146
239
|
name: 'vite-plugin-clj',
|
|
147
240
|
|
|
@@ -152,6 +245,22 @@ export function cljPlugin(options?: CljPluginOptions): Plugin {
|
|
|
152
245
|
regenerateBuiltInNamespaceSources()
|
|
153
246
|
coreIndexPath = resolveCoreIndexPath()
|
|
154
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()
|
|
155
264
|
eagerlyGenerateDts()
|
|
156
265
|
},
|
|
157
266
|
|
|
@@ -176,42 +285,88 @@ export function cljPlugin(options?: CljPluginOptions): Plugin {
|
|
|
176
285
|
|
|
177
286
|
load(id: string) {
|
|
178
287
|
if (id === RESOLVED_VIRTUAL_SESSION_ID) {
|
|
179
|
-
const
|
|
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[] = [
|
|
180
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
|
+
`};`,
|
|
181
301
|
``,
|
|
182
302
|
`let _session = null;`,
|
|
183
|
-
`
|
|
184
|
-
` if (!_session) {`,
|
|
185
|
-
` _session = createSession();`,
|
|
186
|
-
` }`,
|
|
187
|
-
` return _session;`,
|
|
188
|
-
`}`,
|
|
303
|
+
`let _outputLines = [];`,
|
|
189
304
|
]
|
|
190
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
|
+
|
|
191
338
|
if (serveMode) {
|
|
192
339
|
lines.push(
|
|
193
340
|
``,
|
|
194
341
|
`// Browser nREPL relay — active only in Vite dev server`,
|
|
195
342
|
`if (import.meta.hot) {`,
|
|
196
|
-
` import.meta.hot.on('conjure:eval', ({ id, code, ns }) => {`,
|
|
343
|
+
` import.meta.hot.on('conjure:eval', async ({ id, code, ns }) => {`,
|
|
197
344
|
` const session = getSession();`,
|
|
345
|
+
` _outputLines = [];`,
|
|
198
346
|
` try {`,
|
|
199
347
|
` if (ns && ns !== session.currentNs) session.setNs(ns);`,
|
|
200
|
-
` const result = session.
|
|
201
|
-
`
|
|
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 } : {}) });`,
|
|
202
351
|
` } catch (err) {`,
|
|
203
|
-
`
|
|
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 } : {}) });`,
|
|
204
355
|
` }`,
|
|
205
356
|
` });`,
|
|
206
357
|
``,
|
|
207
|
-
` import.meta.hot.on('conjure:load-file', ({ id, source, nsHint, filePath }) => {`,
|
|
358
|
+
` import.meta.hot.on('conjure:load-file', async ({ id, source, nsHint, filePath }) => {`,
|
|
208
359
|
` const session = getSession();`,
|
|
360
|
+
` _outputLines = [];`,
|
|
209
361
|
` try {`,
|
|
210
|
-
` const loadedNs = session.
|
|
362
|
+
` const loadedNs = await session.loadFileAsync(source, nsHint, filePath || undefined);`,
|
|
211
363
|
` if (loadedNs) session.setNs(loadedNs);`,
|
|
212
|
-
`
|
|
364
|
+
` const out = _outputLines.join('');`,
|
|
365
|
+
` import.meta.hot.send('conjure:load-file-result', { id, value: 'nil', ns: session.currentNs, ...(out ? { out } : {}) });`,
|
|
213
366
|
` } catch (err) {`,
|
|
214
|
-
`
|
|
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 } : {}) });`,
|
|
215
370
|
` }`,
|
|
216
371
|
` });`,
|
|
217
372
|
`}`,
|
|
@@ -224,9 +379,9 @@ export function cljPlugin(options?: CljPluginOptions): Plugin {
|
|
|
224
379
|
if (id.endsWith('.clj') && !id.includes('?')) {
|
|
225
380
|
const source = readFileSync(id, 'utf-8')
|
|
226
381
|
const nsNameFromPath = pathToNs(relative(projectRoot, id), sourceRoots)
|
|
227
|
-
const code = generateModuleCode(codegenCtx, nsNameFromPath, source)
|
|
382
|
+
const code = generateModuleCode(codegenCtx, nsNameFromPath, source, id)
|
|
228
383
|
const dts = generateDts(codegenCtx, nsNameFromPath, source)
|
|
229
|
-
writeFileIfChanged(id + '.d.ts', dts)
|
|
384
|
+
if (dts) writeFileIfChanged(id + '.d.ts', dts)
|
|
230
385
|
return code
|
|
231
386
|
}
|
|
232
387
|
},
|
|
@@ -241,7 +396,7 @@ export function cljPlugin(options?: CljPluginOptions): Plugin {
|
|
|
241
396
|
const source = await read()
|
|
242
397
|
try {
|
|
243
398
|
const nsNameFromPath = pathToNs(relative(projectRoot, file), sourceRoots)
|
|
244
|
-
serverSession.
|
|
399
|
+
await serverSession.loadFileAsync(source, nsNameFromPath)
|
|
245
400
|
const dts = generateDts(codegenCtx, nsNameFromPath, source)
|
|
246
401
|
writeFileIfChanged(file + '.d.ts', dts)
|
|
247
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.
|