clojurific 0.12.2 → 0.12.3

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/cljf/cli.js CHANGED
@@ -42,9 +42,10 @@ function parse(argv) {
42
42
 
43
43
  function run(command, args) {
44
44
  const child = spawn(command, args, { stdio: 'inherit' });
45
+ // forwarded: only a terminal signals the JVM too (SIGINT), not kill, an IDE
46
+ // or a process supervisor; a second SIGINT is harmless
45
47
  for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
46
- // the terminal sends SIGINT to the JVM too, other signals are forwarded
47
- process.on(signal, () => signal !== 'SIGINT' && child.kill(signal));
48
+ process.on(signal, () => child.kill(signal));
48
49
  }
49
50
  child.on('error', e => {
50
51
  console.error(`cljf: ${command}: ${e.message}`);
package/cljf/java.js CHANGED
@@ -203,7 +203,8 @@ export async function setupJava({
203
203
  await fs.mkdir(dir, { recursive: true });
204
204
  const tmp = await fs.mkdtemp(path.join(dir, '.setup-'));
205
205
  try {
206
- const archive = path.join(tmp, pkg.name);
206
+ // a name from the API, not a path
207
+ const archive = path.join(tmp, path.basename(pkg.name));
207
208
  try {
208
209
  await download(pkg.link, archive, { sha256: pkg.checksum, fetch });
209
210
  } catch (e) {
package/cljf/launcher.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // Classpaths are cached in the project's .cljf/cpcache, like the Clojure
5
5
  // CLI's .cpcache, until a deps.edn they were resolved from changes.
6
6
  import { spawn } from 'node:child_process';
7
- import { createHash } from 'node:crypto';
7
+ import { createHash, randomBytes } from 'node:crypto';
8
8
  import { existsSync, readFileSync } from 'node:fs';
9
9
  import fs from 'node:fs/promises';
10
10
  import path from 'node:path';
@@ -78,8 +78,10 @@ async function resolve({ project, aliases, java, cache, env, log }) {
78
78
  repository: env.CLJF_MAVEN_REPO || undefined,
79
79
  log,
80
80
  });
81
- const input = `${cache}.resolve.edn`;
82
- const output = `${cache}.resolve.json`;
81
+ // per process: another one may resolve the same classpath at the same time
82
+ const tmp = `${cache}.${process.pid}.${randomBytes(4).toString('hex')}`;
83
+ const input = `${tmp}.resolve.edn`;
84
+ const output = `${tmp}.resolve.json`;
83
85
  await fs.writeFile(input, edn({
84
86
  ':aliases': aliases,
85
87
  ':compiler': compiler,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clojurific",
3
- "version": "0.12.2",
3
+ "version": "0.12.3",
4
4
  "description": "ClojureScript compiled to ES modules, bundled by Vite",
5
5
  "license": "EPL-1.0",
6
6
  "repository": {
@@ -7,7 +7,7 @@
7
7
  // TypeScript files they import.
8
8
  import { spawn } from 'node:child_process';
9
9
  import fs from 'node:fs/promises';
10
- import { existsSync } from 'node:fs';
10
+ import { existsSync, statSync } from 'node:fs';
11
11
  import path from 'node:path';
12
12
  import { createRequire } from 'node:module';
13
13
  import { pathToFileURL } from 'node:url';
@@ -31,16 +31,51 @@ async function sourceNamespace(file) {
31
31
  return ns;
32
32
  }
33
33
 
34
- // The ClojureScript sources pages load with module scripts
35
- async function pageSources(pages, root) {
36
- const sources = [];
37
- for (const page of pages) {
38
- const html = await fs.readFile(page, 'utf8');
39
- for (const [, src] of html.matchAll(/<script\b[^>]*\bsrc\s*=\s*["']([^"'?#]+\.clj[sc])["'][^>]*>/gi)) {
40
- sources.push(src.startsWith('/') ? path.join(root, src) : path.resolve(path.dirname(page), src));
34
+ const SCRIPT_SRC = /<script\b[^>]*\bsrc\s*=\s*["']([^"']+)["']/gi;
35
+ // import "x", import x from "x", export … from "x", import("x")
36
+ const IMPORT = /(?:\bimport\s*(?:[\w$*{}\s,]+\bfrom\s*)?|\bexport\s*[\w$*{}\s,]+\bfrom\s*|\bimport\s*\(\s*)["']([^"']+)["']/g;
37
+ const SCRIPT_FILE = /\.(?:[cm]?[jt]sx?)$/;
38
+ // extensionless imports, as TypeScript projects write them
39
+ const SCRIPT_SUFFIXES = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs', '/index.ts', '/index.js'];
40
+
41
+ // The file a page's script or a module's import names: root-absolute or
42
+ // relative paths, not packages or aliases, which the bundler resolves
43
+ function importedFile(spec, from, root) {
44
+ const file = spec.replace(/[?#].*$/, '');
45
+ if (!file.startsWith('/') && !file.startsWith('.')) return null;
46
+ return file.startsWith('/') ? path.join(root, file) : path.resolve(path.dirname(from), file);
47
+ }
48
+
49
+ // The ClojureScript sources the entries (pages and JavaScript or TypeScript
50
+ // modules) load: the pages' scripts and the modules' imports, followed through
51
+ // the project's modules
52
+ async function entrySources(entries, root) {
53
+ const sources = new Set();
54
+ const seen = new Set();
55
+ const queue = [...entries];
56
+ while (queue.length) {
57
+ const file = queue.shift();
58
+ if (seen.has(file) || file.includes(`${path.sep}node_modules${path.sep}`)) continue;
59
+ seen.add(file);
60
+ let code;
61
+ try {
62
+ code = await fs.readFile(file, 'utf8');
63
+ } catch (e) {
64
+ continue;
65
+ }
66
+ for (const [, spec] of code.matchAll(file.endsWith('.html') ? SCRIPT_SRC : IMPORT)) {
67
+ const target = importedFile(spec, file, root);
68
+ if (!target) continue;
69
+ if (SOURCE_FILE.test(target)) {
70
+ if (existsSync(target)) sources.add(target);
71
+ } else {
72
+ const module = SCRIPT_SUFFIXES.map(suffix => target + suffix)
73
+ .find(f => SCRIPT_FILE.test(f) && existsSync(f) && !statSync(f).isDirectory());
74
+ if (module) queue.push(module);
75
+ }
41
76
  }
42
77
  }
43
- return [...new Set(sources)];
78
+ return [...sources];
44
79
  }
45
80
 
46
81
  // Whether address is this machine's
@@ -73,14 +108,20 @@ async function commonJSPackage(root) {
73
108
  }
74
109
  }
75
110
 
76
- // A Rollup input (a file, files or {name: file}) as {name: absolute file}
111
+ // A Rollup input (a file, files or {name: file}) as {name: absolute file},
112
+ // files named by their path in the root without the extension, so pages with
113
+ // the same file name in different directories (about/index.html) keep apart
77
114
  function inputObject(input, root) {
78
115
  if (!input) return {};
79
116
  if (typeof input === 'object' && !Array.isArray(input)) {
80
117
  return Object.fromEntries(Object.entries(input).map(([name, file]) => [name, path.resolve(root, file)]));
81
118
  }
82
- return Object.fromEntries([].concat(input).map(file =>
83
- [path.basename(file, path.extname(file)), path.resolve(root, file)]));
119
+ return Object.fromEntries([].concat(input).map(file => {
120
+ const absolute = path.resolve(root, file);
121
+ const relative = path.relative(root, absolute);
122
+ const name = relative.startsWith('..') || path.isAbsolute(relative) ? path.basename(absolute) : relative;
123
+ return [name.slice(0, name.length - path.extname(name).length).split(path.sep).join('/'), absolute];
124
+ }));
84
125
  }
85
126
 
86
127
  function ednString(s) {
@@ -315,6 +356,13 @@ export default function cljs(options) {
315
356
  return (compiled ? path.relative(outputDir, file) : path.relative(cwd, file)).split(path.sep).join('/');
316
357
  }
317
358
 
359
+ // Whether a ClojureScript source may be served: in Vite's root or
360
+ // server.fs.allow
361
+ function servedSource(file) {
362
+ const dirs = [config.root, ...(config.server?.fs?.allow ?? [])].map(dir => path.resolve(dir));
363
+ return SOURCE_FILE.test(file) && dirs.some(dir => file.startsWith(dir + path.sep));
364
+ }
365
+
318
366
  // The compiled module of a ClojureScript source file: its namespace's in the
319
367
  // build info, or its munged name
320
368
  async function compiledModule(file) {
@@ -325,7 +373,8 @@ export default function cljs(options) {
325
373
  `${ns.split('.').map(part => part.replace(/-/g, '_')).join('/')}.js`;
326
374
  const module = path.join(outputDir, relative);
327
375
  if (!existsSync(module)) {
328
- throw new Error(`${ns} (${file}) isn't compiled: load it with a page's script, or add it to :main`);
376
+ throw new Error(`${ns} (${file}) isn't compiled: load it from a page's script or a module the pages ` +
377
+ 'import (through relative or root-absolute imports, restarting Vite after adding it), or add it to :main');
329
378
  }
330
379
  return module;
331
380
  }
@@ -372,8 +421,8 @@ export default function cljs(options) {
372
421
  // the project's page, an input of builds and the dev server's /
373
422
  const indexHtml = existsSync(path.join(root, 'index.html')) ? path.join(root, 'index.html') : null;
374
423
  const userInput = userConfig.build?.rolldownOptions?.input ?? userConfig.build?.rollupOptions?.input;
375
- const pages = Object.values(inputObject(userInput ?? indexHtml, root)).filter(f => f.endsWith('.html'));
376
- pageNamespaces = await Promise.all((await pageSources(pages, root)).map(sourceNamespace));
424
+ const entries = Object.values(inputObject(userInput ?? indexHtml, root));
425
+ pageNamespaces = await Promise.all((await entrySources(entries, root)).map(sourceNamespace));
377
426
  if (env.command === 'build' && !env.isPreview) {
378
427
  const start = Date.now();
379
428
  if (userConfig.build?.watch) {
@@ -452,14 +501,17 @@ export default function cljs(options) {
452
501
  // A ClojureScript source: its compiled module, as a module importing it
453
502
  // when serving, which a page loading it by its source's URL and the
454
503
  // modules importing it by its own share
455
- const file = source.split('?')[0];
504
+ const file = source.split(/[?#]/)[0];
456
505
  if (SOURCE_FILE.test(file) && !source.startsWith('\0')) {
506
+ // URLs are root-relative, files are only served from the project
457
507
  const candidates = [
458
- ...(path.isAbsolute(file) ? [file] : []),
459
508
  ...(file.startsWith('/') ? [path.join(root, file)] : []),
460
- ...(importer && !importer.startsWith('\0') ? [path.resolve(path.dirname(importer.split('?')[0]), file)] : []),
509
+ ...(importer && !importer.startsWith('\0') && !file.startsWith('/')
510
+ ? [path.resolve(path.dirname(importer.split('?')[0]), file)]
511
+ : []),
512
+ ...(path.isAbsolute(file) ? [file] : []),
461
513
  ];
462
- const found = candidates.find(f => existsSync(f));
514
+ const found = candidates.find(f => servedSource(f) && existsSync(f));
463
515
  if (!found) return null;
464
516
  return config.command === 'build' ? compiledModule(found) : '\0' + SOURCE + found;
465
517
  }
@@ -479,7 +531,9 @@ export default function cljs(options) {
479
531
 
480
532
  async load(id) {
481
533
  if (id.startsWith('\0' + SOURCE)) {
482
- const module = await compiledModule(id.slice(SOURCE.length + 1));
534
+ const file = id.slice(SOURCE.length + 1);
535
+ if (!servedSource(file)) this.error(`${file} is outside the project`);
536
+ const module = await compiledModule(file);
483
537
  return `import ${JSON.stringify(module)};\nexport * from ${JSON.stringify(module)};\n`;
484
538
  }
485
539
  // compiled namespaces with their source maps, to the ClojureScript sources
@@ -604,9 +658,10 @@ export default function cljs(options) {
604
658
  async handler(html, { filename }) {
605
659
  if (config.command !== 'serve') return html;
606
660
  const replacements = await Promise.all(
607
- [...html.matchAll(/(<script\b[^>]*\bsrc\s*=\s*["'])([^"'?#]+\.clj[sc])(["'])/gi)].map(async match => {
661
+ // with a query or fragment, which the compiled module's URL keeps
662
+ [...html.matchAll(/(<script\b[^>]*\bsrc\s*=\s*["'])([^"'?#]+\.clj[sc])([?#][^"']*)?(["'])/gi)].map(async match => {
608
663
  const file = match[2].startsWith('/') ? path.join(config.root, match[2]) : path.resolve(path.dirname(filename), match[2]);
609
- return [match[0], match[1] + devUrl(await compiledModule(file)) + match[3]];
664
+ return [match[0], match[1] + devUrl(await compiledModule(file)) + (match[3] ?? '') + match[4]];
610
665
  }));
611
666
  return replacements.reduce((result, [from, to]) => result.replace(from, to), html);
612
667
  },