clojurific 0.12.3 → 0.12.5

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/README.md CHANGED
@@ -16,6 +16,9 @@ counts Clojurific's releases, starting from 1.
16
16
 
17
17
  ## Usage
18
18
 
19
+ `npm create clojurific@latest` scaffolds a project (Vanilla, Reagent or UIx)
20
+ like the one below, installs it and starts the dev server.
21
+
19
22
  Dependencies and source paths come from the project's `deps.edn`:
20
23
 
21
24
  ```clojure
@@ -50,8 +53,14 @@ or with the command line:
50
53
  npx cljf build '{:main my.app :output-dir "out"}'
51
54
  npx cljf -A:test watch @cljs.edn
52
55
  npx cljf classpath
56
+ npx cljf repl
53
57
  ```
54
58
 
59
+ `cljf repl` is a ClojureScript REPL evaluating in the pages the dev server
60
+ runs, through its nREPL server: `cljs({ outputDir: 'target/cljs',
61
+ compilerOptions: { repl: { 'nrepl-port': 0 } } })` with `nrepl/nrepl` in
62
+ `deps.edn`.
63
+
55
64
  `cljf` resolves `deps.edn` with tools.deps, adds the compiler, and removes
56
65
  stock ClojureScript (`org.clojure/clojurescript`) that libraries depend on.
57
66
  Dependencies are downloaded into `~/.m2/repository`, which the Clojure CLI
package/cljf/cli.js CHANGED
@@ -1,18 +1,22 @@
1
1
  #!/usr/bin/env node
2
- // cljf: builds and watches ClojureScript with the compiler (cljs.esm), on the
2
+ // cljf: builds and watches ClojureScript with the compiler (cljf.esm), on the
3
3
  // project's classpath resolved from its deps.edn. See usage below.
4
4
  import { spawn } from 'node:child_process';
5
5
  import path from 'node:path';
6
6
  import { LauncherError } from './errors.js';
7
7
  import { classpath, compilerCommand, findJava, setupJava, version } from './launcher.js';
8
+ import { repl } from './repl.js';
8
9
 
9
10
  const usage = `Usage: cljf [options] <command> [arguments]
10
11
 
11
12
  Commands:
12
- build [options...] compile once, cljs.esm's options: EDN maps, @file.edn
13
+ build [options...] compile once, cljf.esm's options: EDN maps, @file.edn
13
14
  and :profile keywords (defaults to :release)
14
15
  watch [options...] compile, then recompile on changes (defaults to :dev)
15
16
  classpath print the project's classpath
17
+ repl [--port <port>] a ClojureScript REPL into the pages running the dev
18
+ server's (or watch's) build, through its nREPL server
19
+ (:repl {:nrepl-port 0}, port from .nrepl-port)
16
20
  setup-java [version] download Eclipse Temurin (the latest LTS by default)
17
21
  into cljf's cache, for machines without Java
18
22
 
@@ -65,7 +69,7 @@ async function main() {
65
69
  case 'build':
66
70
  case 'watch': {
67
71
  const cmd = await compilerCommand({ ...opts, interactive, log });
68
- run(cmd.command, [...cmd.args, '-m', 'cljs.esm', command, ...args]);
72
+ run(cmd.command, [...cmd.args, '-m', 'cljf.esm', command, ...args]);
69
73
  break;
70
74
  }
71
75
  case 'classpath': {
@@ -73,6 +77,11 @@ async function main() {
73
77
  console.log(cp.classpath.join(path.delimiter));
74
78
  break;
75
79
  }
80
+ case 'repl': {
81
+ const i = args.indexOf('--port');
82
+ process.exit(await repl({ port: i >= 0 ? Number(args[i + 1]) : undefined }));
83
+ break;
84
+ }
76
85
  case 'setup-java': {
77
86
  const { java } = await setupJava({ version: args[0], log });
78
87
  console.log(java);
package/cljf/launcher.js CHANGED
@@ -141,8 +141,8 @@ export async function classpath({ cwd = process.cwd(), aliases = [], force = fal
141
141
 
142
142
  /**
143
143
  * The command starting clojure.main with the compiler on the project's
144
- * classpath: {command, args, java, classpath}, cljs.esm's arguments go after
145
- * args, i.e. [...args, '-m', 'cljs.esm', 'build']. Options: cwd (the
144
+ * classpath: {command, args, java, classpath}, cljf.esm's arguments go after
145
+ * args, i.e. [...args, '-m', 'cljf.esm', 'build']. Options: cwd (the
146
146
  * project, with its deps.edn), aliases, force (resolve the classpath again),
147
147
  * interactive (offer to install Java), env and log.
148
148
  */
package/cljf/repl.js ADDED
@@ -0,0 +1,335 @@
1
+ // cljf repl: a ClojureScript REPL in the terminal, evaluating in the pages
2
+ // running a watched build. It's an nREPL client of the watcher's nREPL server
3
+ // (:repl {:nrepl-port 0}, see cljf.esm.repl.nrepl), whose session it
4
+ // switches to ClojureScript with (cljf.esm.repl/repl).
5
+ import { existsSync } from 'node:fs';
6
+ import fs from 'node:fs/promises';
7
+ import net from 'node:net';
8
+ import path from 'node:path';
9
+ import readline from 'node:readline';
10
+ import { LauncherError } from './errors.js';
11
+
12
+ // bencode, nREPL's encoding: dicts, lists, integers and (UTF-8) strings
13
+
14
+ export function bencode(v) {
15
+ if (typeof v === 'number') return Buffer.from(`i${Math.trunc(v)}e`);
16
+ if (typeof v === 'string' || Buffer.isBuffer(v)) {
17
+ const bytes = Buffer.isBuffer(v) ? v : Buffer.from(v);
18
+ return Buffer.concat([Buffer.from(`${bytes.length}:`), bytes]);
19
+ }
20
+ if (Array.isArray(v)) return Buffer.concat([Buffer.from('l'), ...v.map(bencode), Buffer.from('e')]);
21
+ const keys = Object.keys(v).filter(k => v[k] !== undefined).sort();
22
+ return Buffer.concat([Buffer.from('d'), ...keys.flatMap(k => [bencode(k), bencode(v[k])]), Buffer.from('e')]);
23
+ }
24
+
25
+ const INCOMPLETE = Symbol('incomplete');
26
+
27
+ // The value at buf[i], [value, next index], or INCOMPLETE
28
+ function decodeAt(buf, i) {
29
+ if (i >= buf.length) return INCOMPLETE;
30
+ const c = String.fromCharCode(buf[i]);
31
+ if (c === 'i') {
32
+ const end = buf.indexOf('e', i);
33
+ return end < 0 ? INCOMPLETE : [Number(buf.toString('latin1', i + 1, end)), end + 1];
34
+ }
35
+ if (c === 'l' || c === 'd') {
36
+ const items = [];
37
+ let j = i + 1;
38
+ while (j < buf.length && buf[j] !== 0x65) {
39
+ const item = decodeAt(buf, j);
40
+ if (item === INCOMPLETE) return INCOMPLETE;
41
+ items.push(item[0]);
42
+ j = item[1];
43
+ }
44
+ if (j >= buf.length) return INCOMPLETE;
45
+ if (c === 'l') return [items, j + 1];
46
+ const dict = {};
47
+ for (let k = 0; k < items.length; k += 2) dict[items[k]] = items[k + 1];
48
+ return [dict, j + 1];
49
+ }
50
+ if (c >= '0' && c <= '9') {
51
+ const colon = buf.indexOf(':', i);
52
+ if (colon < 0) return INCOMPLETE;
53
+ const start = colon + 1;
54
+ const end = start + Number(buf.toString('latin1', i, colon));
55
+ return end > buf.length ? INCOMPLETE : [buf.toString('utf8', start, end), end];
56
+ }
57
+ throw new Error(`bencode: unexpected ${JSON.stringify(c)}`);
58
+ }
59
+
60
+ /** Decodes the values of a byte stream, as chunks arrive. */
61
+ export class Decoder {
62
+ #buf = Buffer.alloc(0);
63
+
64
+ /** The values chunk completes. */
65
+ push(chunk) {
66
+ this.#buf = Buffer.concat([this.#buf, chunk]);
67
+ const values = [];
68
+ for (;;) {
69
+ const decoded = decodeAt(this.#buf, 0);
70
+ if (decoded === INCOMPLETE) return values;
71
+ values.push(decoded[0]);
72
+ this.#buf = this.#buf.subarray(decoded[1]);
73
+ }
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Whether code's forms are all closed, so that the REPL evaluates it rather
79
+ * than reading another line: no open (, [ or {, string or regex. Extra
80
+ * closing brackets are the reader's to report.
81
+ */
82
+ export function complete(code) {
83
+ let depth = 0;
84
+ for (let i = 0; i < code.length; i++) {
85
+ const c = code[i];
86
+ if (c === ';') {
87
+ while (i < code.length && code[i] !== '\n') i++;
88
+ } else if (c === '\\') {
89
+ i++;
90
+ } else if (c === '"') {
91
+ for (i++; i < code.length && code[i] !== '"'; i++) if (code[i] === '\\') i++;
92
+ if (i >= code.length) return false;
93
+ } else if ('([{'.includes(c)) {
94
+ depth++;
95
+ } else if (')]}'.includes(c)) {
96
+ depth--;
97
+ }
98
+ }
99
+ return depth <= 0;
100
+ }
101
+
102
+ /** An nREPL connection: requests whose responses end with status done. */
103
+ export class Connection {
104
+ #socket;
105
+ #ids = 0;
106
+ #pending = new Map();
107
+
108
+ constructor(socket) {
109
+ this.#socket = socket;
110
+ const decoder = new Decoder();
111
+ socket.on('data', chunk => {
112
+ for (const msg of decoder.push(chunk)) {
113
+ const request = this.#pending.get(msg.id);
114
+ if (!request) continue;
115
+ request.onResponse(msg);
116
+ if (msg.status?.includes('done')) {
117
+ this.#pending.delete(msg.id);
118
+ request.resolve(msg);
119
+ }
120
+ }
121
+ });
122
+ }
123
+
124
+ /** A message id for request. */
125
+ newId() {
126
+ return String(++this.#ids);
127
+ }
128
+
129
+ /**
130
+ * Sends msg (with an id from newId, or a new one), calls onResponse with
131
+ * its responses; resolves to the last.
132
+ */
133
+ request(msg, onResponse = () => {}) {
134
+ const id = msg.id ?? this.newId();
135
+ return new Promise(resolve => {
136
+ this.#pending.set(id, { onResponse, resolve });
137
+ this.#socket.write(bencode({ ...msg, id }));
138
+ });
139
+ }
140
+
141
+ close() {
142
+ this.#socket.end();
143
+ }
144
+ }
145
+
146
+ function connect(port, host) {
147
+ return new Promise((resolve, reject) => {
148
+ const socket = net.connect(port, host);
149
+ socket.once('connect', () => {
150
+ socket.off('error', reject);
151
+ resolve(socket);
152
+ });
153
+ socket.once('error', reject);
154
+ });
155
+ }
156
+
157
+ /**
158
+ * Connects to the nREPL server on port, or the one in cwd's .nrepl-port,
159
+ * waiting for it to start (the dev server writes the file once it built).
160
+ */
161
+ export async function connectToServer({ cwd = process.cwd(), port, host = '127.0.0.1', log = () => {}, interval = 500 } = {}) {
162
+ const portFile = path.join(cwd, '.nrepl-port');
163
+ let waiting = false;
164
+ for (;;) {
165
+ const p = port ?? (existsSync(portFile) ? Number((await fs.readFile(portFile, 'utf8')).trim()) : null);
166
+ if (p) {
167
+ try {
168
+ return await connect(p, host);
169
+ } catch (e) {
170
+ if (port) throw new LauncherError(`cljf repl: no nREPL server on ${host}:${port}: ${e.message}`);
171
+ }
172
+ }
173
+ if (!waiting) {
174
+ log(`; waiting for the nREPL server of the dev server (npm run dev) or cljf watch, with :repl {:nrepl-port 0},\n; to write ${portFile}`);
175
+ waiting = true;
176
+ }
177
+ await new Promise(resolve => setTimeout(resolve, interval));
178
+ }
179
+ }
180
+
181
+ async function readHistory(file) {
182
+ try {
183
+ return (await fs.readFile(file, 'utf8')).split('\n').filter(Boolean).reverse();
184
+ } catch {
185
+ return [];
186
+ }
187
+ }
188
+
189
+ /**
190
+ * The REPL: reads forms from input, evaluates them in the session switched
191
+ * to ClojureScript, writes their output and values to output (errors and
192
+ * warnings to errorOutput). Resolves to the exit code once input ends or
193
+ * :cljs/quit.
194
+ */
195
+ export async function repl({
196
+ cwd = process.cwd(),
197
+ port,
198
+ host,
199
+ input = process.stdin,
200
+ output = process.stdout,
201
+ errorOutput = process.stderr,
202
+ terminal = Boolean(input.isTTY && output.isTTY),
203
+ historyFile = path.join(cwd, '.cljf', 'repl-history'),
204
+ } = {}) {
205
+ const socket = await connectToServer({ cwd, port, host, log: line => errorOutput.write(`${line}\n`) });
206
+ const conn = new Connection(socket);
207
+ let closing = false;
208
+ const closed = new Promise(resolve => socket.on('close', resolve));
209
+ socket.on('error', () => {});
210
+
211
+ const write = (stream, text) => text && stream.write(text);
212
+ const { 'new-session': session } = await conn.request({ op: 'clone' });
213
+ let failed = false;
214
+ await conn.request({ op: 'eval', session, code: '(cljf.esm.repl/repl)' }, msg => {
215
+ write(output, msg.out);
216
+ write(errorOutput, msg.err);
217
+ if (msg.ex) failed = true;
218
+ });
219
+ if (failed) {
220
+ conn.close();
221
+ throw new LauncherError('cljf repl: the nREPL server has no ClojureScript REPL (cljf.esm.repl), is it the dev server\'s?');
222
+ }
223
+
224
+ const history = terminal ? await readHistory(historyFile) : [];
225
+ const rl = readline.createInterface({ input, output, terminal, history, historySize: 1000, removeHistoryDuplicates: true });
226
+ if (terminal) {
227
+ rl.on('history', lines => {
228
+ fs.mkdir(path.dirname(historyFile), { recursive: true })
229
+ .then(() => fs.writeFile(historyFile, `${[...lines].reverse().join('\n')}\n`))
230
+ .catch(() => {});
231
+ });
232
+ }
233
+
234
+ let ns = 'cljs.user';
235
+ let pending = '';
236
+ let evaluating = null;
237
+ let interrupted = false;
238
+ const prompt = () => {
239
+ if (!terminal) return;
240
+ rl.setPrompt(pending ? `${' '.repeat(Math.max(0, ns.length - 1))}#_=> ` : `${ns}=> `);
241
+ rl.prompt();
242
+ };
243
+
244
+ const done = new Promise(resolve => {
245
+ const quit = code => {
246
+ closing = true;
247
+ rl.close();
248
+ // the server forgets the session, unless it doesn't answer
249
+ const timeout = new Promise(r => setTimeout(r, 1000).unref());
250
+ Promise.race([conn.request({ op: 'close', session }), timeout]).finally(() => {
251
+ conn.close();
252
+ resolve(code);
253
+ });
254
+ };
255
+ closed.then(() => {
256
+ if (closing) return;
257
+ errorOutput.write('\n; the nREPL server closed the connection\n');
258
+ closing = true;
259
+ rl.close();
260
+ resolve(1);
261
+ });
262
+
263
+ rl.on('SIGINT', () => {
264
+ if (evaluating) {
265
+ conn.request({ op: 'interrupt', session, 'interrupt-id': evaluating });
266
+ } else if (pending || rl.line) {
267
+ pending = '';
268
+ interrupted = false;
269
+ rl.write(null, { ctrl: true, name: 'u' });
270
+ output.write('\n');
271
+ prompt();
272
+ } else if (interrupted) {
273
+ quit(0);
274
+ } else {
275
+ interrupted = true;
276
+ output.write('\n; Ctrl-C again, Ctrl-D or :cljs/quit to quit\n');
277
+ prompt();
278
+ }
279
+ });
280
+
281
+ // lines are read one at a time: a form waits for the one before
282
+ const lines = [];
283
+ let busy = false;
284
+ const next = async () => {
285
+ if (busy) return;
286
+ busy = true;
287
+ while (lines.length) {
288
+ const line = lines.shift();
289
+ if (line === null) {
290
+ quit(0);
291
+ return;
292
+ }
293
+ interrupted = false;
294
+ pending += `${line}\n`;
295
+ if (!complete(pending)) {
296
+ prompt();
297
+ continue;
298
+ }
299
+ const code = pending;
300
+ pending = '';
301
+ if (!code.trim()) {
302
+ prompt();
303
+ continue;
304
+ }
305
+ if (code.trim() === ':cljs/quit') {
306
+ quit(0);
307
+ return;
308
+ }
309
+ evaluating = conn.newId();
310
+ await conn.request({ op: 'eval', session, code, ns, id: evaluating }, msg => {
311
+ write(output, msg.out);
312
+ write(errorOutput, msg.err);
313
+ if (msg.value !== undefined) output.write(`${msg.value}\n`);
314
+ if (msg.ns) ns = msg.ns;
315
+ });
316
+ evaluating = null;
317
+ if (closing) return;
318
+ prompt();
319
+ }
320
+ busy = false;
321
+ };
322
+ rl.on('line', line => {
323
+ lines.push(line);
324
+ next();
325
+ });
326
+ rl.on('close', () => {
327
+ if (closing) return;
328
+ lines.push(null);
329
+ next();
330
+ });
331
+ });
332
+
333
+ prompt();
334
+ return done;
335
+ }
@@ -1,12 +1,12 @@
1
- // Karma adapter for a cljs.esm test bundle (an ES module) run by
2
- // cljs.esm.karma/start. Add it to Karma's files, serve the bundle's directory
1
+ // Karma adapter for a cljf.esm test bundle (an ES module) run by
2
+ // cljf.esm.karma/start. Add it to Karma's files, serve the bundle's directory
3
3
  // (included: false), and name its entry module in client.args:
4
4
  //
5
5
  // files: ['<this file>', {pattern: 'build/ci-test/**/*', included: false}],
6
6
  // client: {args: ['/base/build/ci-test/my.test-runner.js']},
7
7
  //
8
8
  // Karma calls start once the page has loaded, module scripts may not have run
9
- // yet: cljs.esm.karma/start runs the tests in whichever order the two happen.
9
+ // yet: cljf.esm.karma/start runs the tests in whichever order the two happen.
10
10
  window.__karma__.start = function () {
11
11
  window.__karmaStarted = true;
12
12
  if (window.__karmaRun) window.__karmaRun();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clojurific",
3
- "version": "0.12.3",
3
+ "version": "0.12.5",
4
4
  "description": "ClojureScript compiled to ES modules, bundled by Vite",
5
5
  "license": "EPL-1.0",
6
6
  "repository": {
@@ -16,7 +16,7 @@ import { Worker } from 'node:worker_threads';
16
16
  import { compilerCommand } from '../cljf/launcher.js';
17
17
  import { pruneChunks, propertyRenames } from './prune.js';
18
18
 
19
- const EVENT_PREFIX = '[cljs.esm] ';
19
+ const EVENT_PREFIX = '[cljf.esm] ';
20
20
  const NPM_AS = 'cljs-npm-as:';
21
21
  // a ClojureScript source loaded by a page or imported from JavaScript, which
22
22
  // the dev server serves as an import of its compiled module
@@ -227,11 +227,11 @@ export default module.exports;
227
227
  * the compiler added and stock ClojureScript removed, see cljf/launcher.js.
228
228
  * @param {string[]} [options.command] starts Clojure with the compiler on the
229
229
  * classpath instead of the launcher, i.e. ["clojure", "-M:cljs"],
230
- * cljs.esm's arguments follow
230
+ * cljf.esm's arguments follow
231
231
  * @param {string} [options.cwd] the compiler's working directory, with the
232
232
  * project's deps.edn, defaults to Vite's root (the project's directory)
233
233
  * @param {string} [options.config] compiler options file (EDN, see
234
- * cljs.esm/load-options), relative to cwd
234
+ * cljf.esm/load-options), relative to cwd
235
235
  * @param {string} [options.profile] profile of the config file, defaults to
236
236
  * dev when serving and release when building
237
237
  * @param {string} options.outputDir the compiler's output directory, relative
@@ -285,7 +285,7 @@ export default function cljs(options) {
285
285
 
286
286
  function run(args, onEvent) {
287
287
  const [cmd, ...cmdArgs] = compiler;
288
- const child = spawn(cmd, [...cmdArgs, '-m', 'cljs.esm', ...args], {
288
+ const child = spawn(cmd, [...cmdArgs, '-m', 'cljf.esm', ...args], {
289
289
  cwd,
290
290
  stdio: ['pipe', 'pipe', 'pipe'],
291
291
  });
@@ -686,7 +686,7 @@ export default function cljs(options) {
686
686
  server.ws.send(problem);
687
687
  };
688
688
  server.ws.on('connection', socket => problem && socket.send(JSON.stringify(problem)));
689
- // The REPL (cljs.esm.repl): pages running the build (cljs.esm.repl-runtime)
689
+ // The REPL (cljf.esm.repl): pages running the build (cljf.esm.repl-runtime)
690
690
  // say hello over Vite's websocket, each connection gets a runtime id. Their
691
691
  // cljs:repl messages go to the compiler as `repl <json>` lines, the
692
692
  // compiler's repl-send events to the page of their runtime id.