@canopy-iiif/app 0.7.0 → 0.7.1

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/lib/build/dev.js CHANGED
@@ -1,19 +1,35 @@
1
- const fs = require('fs');
1
+ const fs = require("fs");
2
2
  const fsp = fs.promises;
3
- const path = require('path');
4
- const { spawn } = require('child_process');
5
- const { build } = require('../build/build');
6
- const http = require('http');
7
- const url = require('url');
8
- const { CONTENT_DIR, OUT_DIR, ASSETS_DIR, ensureDirSync } = require('../common');
9
- const twHelper = (() => { try { return require('../../helpers/build-tailwind'); } catch (_) { return null; } })();
3
+ const path = require("path");
4
+ const { spawn } = require("child_process");
5
+ const { build } = require("../build/build");
6
+ const http = require("http");
7
+ const url = require("url");
8
+ const {
9
+ CONTENT_DIR,
10
+ OUT_DIR,
11
+ ASSETS_DIR,
12
+ ensureDirSync,
13
+ } = require("../common");
14
+ const twHelper = (() => {
15
+ try {
16
+ return require("../../helpers/build-tailwind");
17
+ } catch (_) {
18
+ return null;
19
+ }
20
+ })();
10
21
  function resolveTailwindCli() {
11
22
  try {
12
- const cliJs = require.resolve('tailwindcss/lib/cli.js');
23
+ const cliJs = require.resolve("tailwindcss/lib/cli.js");
13
24
  return { cmd: process.execPath, args: [cliJs] };
14
25
  } catch (_) {}
15
26
  try {
16
- const bin = path.join(process.cwd(), 'node_modules', '.bin', process.platform === 'win32' ? 'tailwindcss.cmd' : 'tailwindcss');
27
+ const bin = path.join(
28
+ process.cwd(),
29
+ "node_modules",
30
+ ".bin",
31
+ process.platform === "win32" ? "tailwindcss.cmd" : "tailwindcss"
32
+ );
17
33
  if (fs.existsSync(bin)) return { cmd: bin, args: [] };
18
34
  } catch (_) {}
19
35
  return null;
@@ -23,16 +39,18 @@ let onBuildSuccess = () => {};
23
39
  let onBuildStart = () => {};
24
40
  let onCssChange = () => {};
25
41
  let nextBuildSkipIiif = false; // hint set by watchers
26
- const UI_DIST_DIR = path.resolve(path.join(__dirname, '../../ui/dist'));
42
+ const UI_DIST_DIR = path.resolve(path.join(__dirname, "../../ui/dist"));
27
43
 
28
44
  function prettyPath(p) {
29
45
  try {
30
46
  let rel = path.relative(process.cwd(), p);
31
- if (!rel) rel = '.';
32
- rel = rel.split(path.sep).join('/');
33
- if (!rel.startsWith('./') && !rel.startsWith('../')) rel = './' + rel;
47
+ if (!rel) rel = ".";
48
+ rel = rel.split(path.sep).join("/");
49
+ if (!rel.startsWith("./") && !rel.startsWith("../")) rel = "./" + rel;
34
50
  return rel;
35
- } catch (_) { return p; }
51
+ } catch (_) {
52
+ return p;
53
+ }
36
54
  }
37
55
 
38
56
  async function runBuild() {
@@ -40,22 +58,38 @@ async function runBuild() {
40
58
  const hint = { skipIiif: !!nextBuildSkipIiif };
41
59
  nextBuildSkipIiif = false;
42
60
  await build(hint);
43
- try { onBuildSuccess(); } catch (_) {}
61
+ try {
62
+ onBuildSuccess();
63
+ } catch (_) {}
44
64
  } catch (e) {
45
- console.error('Build failed:', e && e.message ? e.message : e);
65
+ console.error("Build failed:", e && e.message ? e.message : e);
46
66
  }
47
67
  }
48
68
 
49
69
  function tryRecursiveWatch() {
50
70
  try {
51
- const watcher = fs.watch(CONTENT_DIR, { recursive: true }, (eventType, filename) => {
52
- if (!filename) return;
53
- try { console.log(`[watch] ${eventType}: ${prettyPath(path.join(CONTENT_DIR, filename))}`); } catch (_) {}
54
- // If an MDX file changed, we can skip IIIF for the next build
55
- try { if (/\.mdx$/i.test(filename)) nextBuildSkipIiif = true; } catch (_) {}
56
- try { onBuildStart(); } catch (_) {}
57
- debounceBuild();
58
- });
71
+ const watcher = fs.watch(
72
+ CONTENT_DIR,
73
+ { recursive: true },
74
+ (eventType, filename) => {
75
+ if (!filename) return;
76
+ try {
77
+ console.log(
78
+ `[watch] ${eventType}: ${prettyPath(
79
+ path.join(CONTENT_DIR, filename)
80
+ )}`
81
+ );
82
+ } catch (_) {}
83
+ // If an MDX file changed, we can skip IIIF for the next build
84
+ try {
85
+ if (/\.mdx$/i.test(filename)) nextBuildSkipIiif = true;
86
+ } catch (_) {}
87
+ try {
88
+ onBuildStart();
89
+ } catch (_) {}
90
+ debounceBuild();
91
+ }
92
+ );
59
93
  return watcher;
60
94
  } catch (e) {
61
95
  return null;
@@ -75,11 +109,21 @@ function watchPerDir() {
75
109
  if (watchers.has(dir)) return;
76
110
  try {
77
111
  const w = fs.watch(dir, (eventType, filename) => {
78
- try { console.log(`[watch] ${eventType}: ${prettyPath(path.join(dir, filename || ''))}`); } catch (_) {}
112
+ try {
113
+ console.log(
114
+ `[watch] ${eventType}: ${prettyPath(
115
+ path.join(dir, filename || "")
116
+ )}`
117
+ );
118
+ } catch (_) {}
79
119
  // If a new directory appears, add a watcher for it on next scan
80
120
  scan(dir);
81
- try { if (filename && /\.mdx$/i.test(filename)) nextBuildSkipIiif = true; } catch (_) {}
82
- try { onBuildStart(); } catch (_) {}
121
+ try {
122
+ if (filename && /\.mdx$/i.test(filename)) nextBuildSkipIiif = true;
123
+ } catch (_) {}
124
+ try {
125
+ onBuildStart();
126
+ } catch (_) {}
83
127
  debounceBuild();
84
128
  });
85
129
  watchers.set(dir, w);
@@ -120,25 +164,46 @@ async function syncAsset(relativePath) {
120
164
  }
121
165
  ensureDirSync(path.dirname(dest));
122
166
  await fsp.copyFile(src, dest);
123
- console.log(`[assets] Copied ${relativePath} -> ${path.relative(process.cwd(), dest)}`);
167
+ console.log(
168
+ `[assets] Copied ${relativePath} -> ${path.relative(
169
+ process.cwd(),
170
+ dest
171
+ )}`
172
+ );
124
173
  } else {
125
174
  // Removed or renamed away: remove dest
126
- try { await fsp.rm(dest, { force: true, recursive: true }); } catch (_) {}
175
+ try {
176
+ await fsp.rm(dest, { force: true, recursive: true });
177
+ } catch (_) {}
127
178
  console.log(`[assets] Removed ${relativePath}`);
128
179
  }
129
180
  } catch (e) {
130
- console.warn('[assets] sync failed:', e && e.message ? e.message : e);
181
+ console.warn("[assets] sync failed:", e && e.message ? e.message : e);
131
182
  }
132
183
  }
133
184
 
134
185
  function tryRecursiveWatchAssets() {
135
186
  try {
136
- const watcher = fs.watch(ASSETS_DIR, { recursive: true }, (eventType, filename) => {
137
- if (!filename) return;
138
- try { console.log(`[assets] ${eventType}: ${prettyPath(path.join(ASSETS_DIR, filename))}`); } catch (_) {}
139
- // Copy just the changed asset and trigger reload
140
- syncAsset(filename).then(() => { try { onBuildSuccess(); } catch (_) {} });
141
- });
187
+ const watcher = fs.watch(
188
+ ASSETS_DIR,
189
+ { recursive: true },
190
+ (eventType, filename) => {
191
+ if (!filename) return;
192
+ try {
193
+ console.log(
194
+ `[assets] ${eventType}: ${prettyPath(
195
+ path.join(ASSETS_DIR, filename)
196
+ )}`
197
+ );
198
+ } catch (_) {}
199
+ // Copy just the changed asset and trigger reload
200
+ syncAsset(filename).then(() => {
201
+ try {
202
+ onBuildSuccess();
203
+ } catch (_) {}
204
+ });
205
+ }
206
+ );
142
207
  return watcher;
143
208
  } catch (e) {
144
209
  return null;
@@ -152,11 +217,23 @@ function watchAssetsPerDir() {
152
217
  if (watchers.has(dir)) return;
153
218
  try {
154
219
  const w = fs.watch(dir, (eventType, filename) => {
155
- const rel = filename ? path.relative(ASSETS_DIR, path.join(dir, filename)) : path.relative(ASSETS_DIR, dir);
156
- try { console.log(`[assets] ${eventType}: ${prettyPath(path.join(dir, filename || ''))}`); } catch (_) {}
220
+ const rel = filename
221
+ ? path.relative(ASSETS_DIR, path.join(dir, filename))
222
+ : path.relative(ASSETS_DIR, dir);
223
+ try {
224
+ console.log(
225
+ `[assets] ${eventType}: ${prettyPath(
226
+ path.join(dir, filename || "")
227
+ )}`
228
+ );
229
+ } catch (_) {}
157
230
  // If a new directory appears, add a watcher for it on next scan
158
231
  scan(dir);
159
- syncAsset(rel).then(() => { try { onBuildSuccess(); } catch (_) {} });
232
+ syncAsset(rel).then(() => {
233
+ try {
234
+ onBuildSuccess();
235
+ } catch (_) {}
236
+ });
160
237
  });
161
238
  watchers.set(dir, w);
162
239
  } catch (_) {
@@ -186,31 +263,43 @@ function watchAssetsPerDir() {
186
263
  // When UI dist changes, rebuild the search runtime bundle and trigger a browser reload.
187
264
  async function rebuildSearchBundle() {
188
265
  try {
189
- const search = require('./search');
190
- if (search && typeof search.ensureSearchRuntime === 'function') {
266
+ const search = require("./search");
267
+ if (search && typeof search.ensureSearchRuntime === "function") {
191
268
  await search.ensureSearchRuntime();
192
269
  }
193
270
  } catch (_) {}
194
- try { onBuildSuccess(); } catch (_) {}
271
+ try {
272
+ onBuildSuccess();
273
+ } catch (_) {}
195
274
  }
196
275
 
197
276
  function tryRecursiveWatchUiDist() {
198
277
  try {
199
278
  if (!fs.existsSync(UI_DIST_DIR)) return null;
200
- const watcher = fs.watch(UI_DIST_DIR, { recursive: true }, (eventType, filename) => {
201
- if (!filename) return;
202
- try { console.log(`[ui] ${eventType}: ${prettyPath(path.join(UI_DIST_DIR, filename))}`); } catch (_) {}
203
- // Lightweight path: rebuild only the search runtime bundle
204
- rebuildSearchBundle();
205
- // If the server-side UI bundle changed, trigger a site rebuild (skip IIIF)
206
- try {
207
- if (/server\.(js|mjs)$/.test(filename)) {
208
- nextBuildSkipIiif = true;
209
- try { onBuildStart(); } catch (_) {}
210
- debounceBuild();
211
- }
212
- } catch (_) {}
213
- });
279
+ const watcher = fs.watch(
280
+ UI_DIST_DIR,
281
+ { recursive: true },
282
+ (eventType, filename) => {
283
+ if (!filename) return;
284
+ try {
285
+ console.log(
286
+ `[ui] ${eventType}: ${prettyPath(path.join(UI_DIST_DIR, filename))}`
287
+ );
288
+ } catch (_) {}
289
+ // Lightweight path: rebuild only the search runtime bundle
290
+ rebuildSearchBundle();
291
+ // If the server-side UI bundle changed, trigger a site rebuild (skip IIIF)
292
+ try {
293
+ if (/server\.(js|mjs)$/.test(filename)) {
294
+ nextBuildSkipIiif = true;
295
+ try {
296
+ onBuildStart();
297
+ } catch (_) {}
298
+ debounceBuild();
299
+ }
300
+ } catch (_) {}
301
+ }
302
+ );
214
303
  return watcher;
215
304
  } catch (_) {
216
305
  return null;
@@ -224,13 +313,19 @@ function watchUiDistPerDir() {
224
313
  if (watchers.has(dir)) return;
225
314
  try {
226
315
  const w = fs.watch(dir, (eventType, filename) => {
227
- try { console.log(`[ui] ${eventType}: ${prettyPath(path.join(dir, filename || ''))}`); } catch (_) {}
316
+ try {
317
+ console.log(
318
+ `[ui] ${eventType}: ${prettyPath(path.join(dir, filename || ""))}`
319
+ );
320
+ } catch (_) {}
228
321
  scan(dir);
229
322
  rebuildSearchBundle();
230
323
  try {
231
- if (/server\.(js|mjs)$/.test(filename || '')) {
324
+ if (/server\.(js|mjs)$/.test(filename || "")) {
232
325
  nextBuildSkipIiif = true;
233
- try { onBuildStart(); } catch (_) {}
326
+ try {
327
+ onBuildStart();
328
+ } catch (_) {}
234
329
  debounceBuild();
235
330
  }
236
331
  } catch (_) {}
@@ -247,44 +342,56 @@ function watchUiDistPerDir() {
247
342
  }
248
343
  watchDir(UI_DIST_DIR);
249
344
  scan(UI_DIST_DIR);
250
- return () => { for (const w of watchers.values()) w.close(); };
345
+ return () => {
346
+ for (const w of watchers.values()) w.close();
347
+ };
251
348
  }
252
349
 
253
350
  const MIME = {
254
- '.html': 'text/html; charset=utf-8',
255
- '.css': 'text/css; charset=utf-8',
256
- '.js': 'application/javascript; charset=utf-8',
257
- '.json': 'application/json; charset=utf-8',
258
- '.png': 'image/png',
259
- '.jpg': 'image/jpeg',
260
- '.jpeg': 'image/jpeg',
261
- '.gif': 'image/gif',
262
- '.svg': 'image/svg+xml',
263
- '.txt': 'text/plain; charset=utf-8'
351
+ ".html": "text/html; charset=utf-8",
352
+ ".css": "text/css; charset=utf-8",
353
+ ".js": "application/javascript; charset=utf-8",
354
+ ".json": "application/json; charset=utf-8",
355
+ ".png": "image/png",
356
+ ".jpg": "image/jpeg",
357
+ ".jpeg": "image/jpeg",
358
+ ".gif": "image/gif",
359
+ ".svg": "image/svg+xml",
360
+ ".txt": "text/plain; charset=utf-8",
264
361
  };
265
362
 
266
363
  function startServer() {
267
364
  const clients = new Set();
268
365
  function broadcast(type) {
269
366
  for (const res of clients) {
270
- try { res.write(`data: ${type}\n\n`); } catch (_) {}
367
+ try {
368
+ res.write(`data: ${type}\n\n`);
369
+ } catch (_) {}
271
370
  }
272
371
  }
273
- onBuildStart = () => broadcast('building');
274
- onBuildSuccess = () => broadcast('reload');
275
- onCssChange = () => broadcast('css');
372
+ onBuildStart = () => broadcast("building");
373
+ onBuildSuccess = () => broadcast("reload");
374
+ onCssChange = () => broadcast("css");
276
375
 
277
376
  const server = http.createServer((req, res) => {
278
- const parsed = url.parse(req.url || '/');
279
- let pathname = decodeURI(parsed.pathname || '/');
377
+ const parsed = url.parse(req.url || "/");
378
+ let pathname = decodeURI(parsed.pathname || "/");
280
379
  // Serve dev toast assets and config
281
- if (pathname === '/__livereload-config') {
282
- res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-cache' });
283
- const cfgPath = path.join(__dirname, 'devtoast.config.json');
284
- let cfg = { buildingText: 'Rebuilding…', reloadedText: 'Reloaded', fadeMs: 800, reloadDelayMs: 200 };
380
+ if (pathname === "/__livereload-config") {
381
+ res.writeHead(200, {
382
+ "Content-Type": "application/json; charset=utf-8",
383
+ "Cache-Control": "no-cache",
384
+ });
385
+ const cfgPath = path.join(__dirname, "devtoast.config.json");
386
+ let cfg = {
387
+ buildingText: "Rebuilding…",
388
+ reloadedText: "Reloaded",
389
+ fadeMs: 800,
390
+ reloadDelayMs: 200,
391
+ };
285
392
  try {
286
393
  if (fs.existsSync(cfgPath)) {
287
- const raw = fs.readFileSync(cfgPath, 'utf8');
394
+ const raw = fs.readFileSync(cfgPath, "utf8");
288
395
  const parsedCfg = JSON.parse(raw);
289
396
  cfg = { ...cfg, ...parsedCfg };
290
397
  }
@@ -292,34 +399,39 @@ function startServer() {
292
399
  res.end(JSON.stringify(cfg));
293
400
  return;
294
401
  }
295
- if (pathname === '/__livereload.css') {
296
- res.writeHead(200, { 'Content-Type': 'text/css; charset=utf-8', 'Cache-Control': 'no-cache' });
297
- const cssPath = path.join(__dirname, 'devtoast.css');
402
+ if (pathname === "/__livereload.css") {
403
+ res.writeHead(200, {
404
+ "Content-Type": "text/css; charset=utf-8",
405
+ "Cache-Control": "no-cache",
406
+ });
407
+ const cssPath = path.join(__dirname, "devtoast.css");
298
408
  let css = `#__lr_toast{position:fixed;bottom:12px;left:12px;background:rgba(0,0,0,.8);color:#fff;padding:6px 10px;border-radius:6px;font:12px/1.2 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;z-index:99999;box-shadow:0 2px 8px rgba(0,0,0,.3);opacity:0;transition:opacity .15s ease}`;
299
409
  try {
300
- if (fs.existsSync(cssPath)) css = fs.readFileSync(cssPath, 'utf8');
410
+ if (fs.existsSync(cssPath)) css = fs.readFileSync(cssPath, "utf8");
301
411
  } catch (_) {}
302
412
  res.end(css);
303
413
  return;
304
414
  }
305
- if (pathname === '/__livereload') {
415
+ if (pathname === "/__livereload") {
306
416
  res.writeHead(200, {
307
- 'Content-Type': 'text/event-stream',
308
- 'Cache-Control': 'no-cache, no-transform',
309
- Connection: 'keep-alive'
417
+ "Content-Type": "text/event-stream",
418
+ "Cache-Control": "no-cache, no-transform",
419
+ Connection: "keep-alive",
310
420
  });
311
- res.write(': connected\n\n');
421
+ res.write(": connected\n\n");
312
422
  clients.add(res);
313
423
  const keepAlive = setInterval(() => {
314
- try { res.write(': ping\n\n'); } catch (_) {}
424
+ try {
425
+ res.write(": ping\n\n");
426
+ } catch (_) {}
315
427
  }, 30000);
316
- req.on('close', () => {
428
+ req.on("close", () => {
317
429
  clearInterval(keepAlive);
318
430
  clients.delete(res);
319
431
  });
320
432
  return;
321
433
  }
322
- if (pathname === '/') pathname = '/index.html';
434
+ if (pathname === "/") pathname = "/index.html";
323
435
 
324
436
  // Resolve candidate paths in order:
325
437
  // 1) as-is
@@ -327,7 +439,7 @@ function startServer() {
327
439
  // 3) if a directory, use its index.html
328
440
  let filePath = null;
329
441
  const candidateA = path.join(OUT_DIR, pathname);
330
- const candidateB = path.join(OUT_DIR, pathname + '.html');
442
+ const candidateB = path.join(OUT_DIR, pathname + ".html");
331
443
  if (fs.existsSync(candidateA)) {
332
444
  filePath = candidateA;
333
445
  } else if (fs.existsSync(candidateB)) {
@@ -340,7 +452,7 @@ function startServer() {
340
452
  try {
341
453
  const st = fs.statSync(maybeDir);
342
454
  if (st.isDirectory()) {
343
- const idx = path.join(maybeDir, 'index.html');
455
+ const idx = path.join(maybeDir, "index.html");
344
456
  if (fs.existsSync(idx)) filePath = idx;
345
457
  }
346
458
  } catch (_) {}
@@ -348,8 +460,8 @@ function startServer() {
348
460
  }
349
461
  if (!filePath) {
350
462
  res.statusCode = 404;
351
- res.setHeader('Content-Type', 'text/plain; charset=utf-8');
352
- res.end('Not Found');
463
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
464
+ res.end("Not Found");
353
465
  return;
354
466
  }
355
467
 
@@ -357,7 +469,7 @@ function startServer() {
357
469
  let resolved = path.resolve(filePath);
358
470
  if (!resolved.startsWith(OUT_DIR)) {
359
471
  res.statusCode = 403;
360
- res.end('Forbidden');
472
+ res.end("Forbidden");
361
473
  return;
362
474
  }
363
475
 
@@ -365,13 +477,13 @@ function startServer() {
365
477
  try {
366
478
  const st = fs.statSync(resolved);
367
479
  if (st.isDirectory()) {
368
- const idx = path.join(resolved, 'index.html');
480
+ const idx = path.join(resolved, "index.html");
369
481
  if (fs.existsSync(idx)) {
370
482
  filePath = idx;
371
483
  resolved = path.resolve(filePath);
372
484
  } else {
373
485
  res.statusCode = 404;
374
- res.end('Not Found');
486
+ res.end("Not Found");
375
487
  return;
376
488
  }
377
489
  }
@@ -381,14 +493,14 @@ function startServer() {
381
493
  resolved = path.resolve(filePath);
382
494
  const ext = path.extname(resolved).toLowerCase();
383
495
  res.statusCode = 200;
384
- res.setHeader('Content-Type', MIME[ext] || 'application/octet-stream');
496
+ res.setHeader("Content-Type", MIME[ext] || "application/octet-stream");
385
497
  // Dev: always disable caching so reloads fetch fresh assets
386
- res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
387
- res.setHeader('Pragma', 'no-cache');
388
- res.setHeader('Expires', '0');
389
- if (ext === '.html') {
498
+ res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
499
+ res.setHeader("Pragma", "no-cache");
500
+ res.setHeader("Expires", "0");
501
+ if (ext === ".html") {
390
502
  try {
391
- let html = fs.readFileSync(resolved, 'utf8');
503
+ let html = fs.readFileSync(resolved, "utf8");
392
504
  const snippet = `
393
505
  <link rel="stylesheet" href="/__livereload.css">
394
506
  <script>(function(){
@@ -433,11 +545,13 @@ function startServer() {
433
545
  };
434
546
  window.addEventListener('beforeunload', function(){ try { es.close(); } catch(e) {} });
435
547
  })();</script>`;
436
- html = html.includes('</body>') ? html.replace('</body>', snippet + '</body>') : html + snippet;
548
+ html = html.includes("</body>")
549
+ ? html.replace("</body>", snippet + "</body>")
550
+ : html + snippet;
437
551
  res.end(html);
438
552
  } catch (e) {
439
553
  res.statusCode = 500;
440
- res.end('Error serving HTML');
554
+ res.end("Error serving HTML");
441
555
  }
442
556
  } else {
443
557
  fs.createReadStream(resolved).pipe(res);
@@ -453,12 +567,12 @@ function startServer() {
453
567
 
454
568
  async function dev() {
455
569
  if (!fs.existsSync(CONTENT_DIR)) {
456
- console.error('No content directory found at', CONTENT_DIR);
570
+ console.error("No content directory found at", CONTENT_DIR);
457
571
  process.exit(1);
458
572
  }
459
573
  // Start server before the initial build so build logs follow server standup
460
574
  startServer();
461
- console.log('Initial build...');
575
+ console.log("Initial build...");
462
576
  // Expose a base URL for builders to construct absolute ids/links
463
577
  if (!process.env.CANOPY_BASE_URL) {
464
578
  process.env.CANOPY_BASE_URL = `http://localhost:${PORT}`;
@@ -466,12 +580,14 @@ async function dev() {
466
580
  // In dev, let the Tailwind watcher own CSS generation to avoid duplicate
467
581
  // one-off builds that print "Rebuilding..." messages. Skip ensureStyles()
468
582
  // within build() by setting an environment flag.
469
- process.env.CANOPY_SKIP_STYLES = process.env.DEV_ONCE ? '' : '1';
583
+ process.env.CANOPY_SKIP_STYLES = process.env.DEV_ONCE ? "" : "1";
470
584
  // Suppress noisy Browserslist old data warning in dev/tailwind
471
- process.env.BROWSERSLIST_IGNORE_OLD_DATA = '1';
585
+ process.env.BROWSERSLIST_IGNORE_OLD_DATA = "1";
472
586
  if (process.env.DEV_ONCE) {
473
587
  // Build once and exit (used for tests/CI)
474
- runBuild().then(() => process.exit(0)).catch(() => process.exit(1));
588
+ runBuild()
589
+ .then(() => process.exit(0))
590
+ .catch(() => process.exit(1));
475
591
  return;
476
592
  }
477
593
  // Run the initial build synchronously now that the server is up
@@ -480,37 +596,68 @@ async function dev() {
480
596
  // Start Tailwind watcher if config + input exist (after initial build)
481
597
  try {
482
598
  const root = process.cwd();
483
- const appStylesDir = path.join(root, 'app', 'styles');
484
- const twConfigsRoot = ['tailwind.config.js','tailwind.config.cjs','tailwind.config.mjs','tailwind.config.ts']
485
- .map((n) => path.join(root, n));
486
- const twConfigsApp = ['tailwind.config.js','tailwind.config.cjs','tailwind.config.mjs','tailwind.config.ts']
487
- .map((n) => path.join(appStylesDir, n));
488
- let configPath = [...twConfigsApp, ...twConfigsRoot].find((p) => { try { return fs.existsSync(p); } catch (_) { return false; } });
489
- const inputCandidates = [path.join(appStylesDir, 'index.css'), path.join(CONTENT_DIR, '_styles.css')];
490
- let inputCss = inputCandidates.find((p) => { try { return fs.existsSync(p); } catch (_) { return false; } });
599
+ const appStylesDir = path.join(root, "app", "styles");
600
+ const twConfigsRoot = [
601
+ "tailwind.config.js",
602
+ "tailwind.config.cjs",
603
+ "tailwind.config.mjs",
604
+ "tailwind.config.ts",
605
+ ].map((n) => path.join(root, n));
606
+ const twConfigsApp = [
607
+ "tailwind.config.js",
608
+ "tailwind.config.cjs",
609
+ "tailwind.config.mjs",
610
+ "tailwind.config.ts",
611
+ ].map((n) => path.join(appStylesDir, n));
612
+ let configPath = [...twConfigsApp, ...twConfigsRoot].find((p) => {
613
+ try {
614
+ return fs.existsSync(p);
615
+ } catch (_) {
616
+ return false;
617
+ }
618
+ });
619
+ const inputCandidates = [
620
+ path.join(appStylesDir, "index.css"),
621
+ path.join(CONTENT_DIR, "_styles.css"),
622
+ ];
623
+ let inputCss = inputCandidates.find((p) => {
624
+ try {
625
+ return fs.existsSync(p);
626
+ } catch (_) {
627
+ return false;
628
+ }
629
+ });
491
630
  // Generate fallback config and input if missing
492
631
  if (!configPath) {
493
632
  try {
494
- const { CACHE_DIR } = require('./common');
495
- const genDir = path.join(CACHE_DIR, 'tailwind');
633
+ const { CACHE_DIR } = require("./common");
634
+ const genDir = path.join(CACHE_DIR, "tailwind");
496
635
  ensureDirSync(genDir);
497
- const genCfg = path.join(genDir, 'tailwind.config.js');
636
+ const genCfg = path.join(genDir, "tailwind.config.js");
498
637
  const cfg = `module.exports = {\n presets: [require('@canopy-iiif/app/ui/canopy-iiif-preset')],\n content: [\n './content/**/*.{mdx,html}',\n './site/**/*.html',\n './site/**/*.js',\n './packages/app/ui/**/*.{js,jsx,ts,tsx}',\n './packages/app/lib/iiif/components/**/*.{js,jsx}',\n ],\n theme: { extend: {} },\n plugins: [require('@canopy-iiif/app/ui/canopy-iiif-plugin')],\n};\n`;
499
- fs.writeFileSync(genCfg, cfg, 'utf8');
638
+ fs.writeFileSync(genCfg, cfg, "utf8");
500
639
  configPath = genCfg;
501
- } catch (_) { configPath = null; }
640
+ } catch (_) {
641
+ configPath = null;
642
+ }
502
643
  }
503
644
  if (!inputCss) {
504
645
  try {
505
- const { CACHE_DIR } = require('./common');
506
- const genDir = path.join(CACHE_DIR, 'tailwind');
646
+ const { CACHE_DIR } = require("./common");
647
+ const genDir = path.join(CACHE_DIR, "tailwind");
507
648
  ensureDirSync(genDir);
508
- const genCss = path.join(genDir, 'index.css');
509
- fs.writeFileSync(genCss, `@tailwind base;\n@tailwind components;\n@tailwind utilities;\n`, 'utf8');
649
+ const genCss = path.join(genDir, "index.css");
650
+ fs.writeFileSync(
651
+ genCss,
652
+ `@tailwind base;\n@tailwind components;\n@tailwind utilities;\n`,
653
+ "utf8"
654
+ );
510
655
  inputCss = genCss;
511
- } catch (_) { inputCss = null; }
656
+ } catch (_) {
657
+ inputCss = null;
658
+ }
512
659
  }
513
- const outputCss = path.join(OUT_DIR, 'styles', 'styles.css');
660
+ const outputCss = path.join(OUT_DIR, "styles", "styles.css");
514
661
  if (configPath && inputCss) {
515
662
  // Ensure output dir exists and start watcher
516
663
  ensureDirSync(path.dirname(outputCss));
@@ -521,35 +668,70 @@ async function dev() {
521
668
  if (!fs.existsSync(outputCss)) {
522
669
  const base = `:root{--max-w:760px;--muted:#6b7280}*{box-sizing:border-box}body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Helvetica,Arial,sans-serif;max-width:var(--max-w);margin:2rem auto;padding:0 1rem;line-height:1.6}a{color:#2563eb;text-decoration:none}a:hover{text-decoration:underline}`;
523
670
  ensureDirSync(path.dirname(outputCss));
524
- fs.writeFileSync(outputCss, base + '\n', 'utf8');
525
- console.log('[tailwind] wrote fallback CSS to', prettyPath(outputCss));
671
+ fs.writeFileSync(outputCss, base + "\n", "utf8");
672
+ console.log(
673
+ "[tailwind] wrote fallback CSS to",
674
+ prettyPath(outputCss)
675
+ );
526
676
  }
527
677
  } catch (_) {}
528
678
  }
529
- function fileSizeKb(p) { try { const st = fs.statSync(p); return st && st.size ? (st.size/1024).toFixed(1) : '0.0'; } catch (_) { return '0.0'; } }
679
+ function fileSizeKb(p) {
680
+ try {
681
+ const st = fs.statSync(p);
682
+ return st && st.size ? (st.size / 1024).toFixed(1) : "0.0";
683
+ } catch (_) {
684
+ return "0.0";
685
+ }
686
+ }
530
687
  // Initial one-off compile so the CSS exists before watcher starts
531
688
  try {
532
689
  const cliOnce = resolveTailwindCli();
533
690
  if (cliOnce) {
534
- const { spawnSync } = require('child_process');
535
- const argsOnce = ['-i', inputCss, '-o', outputCss, '-c', configPath, '--minify'];
536
- const res = spawnSync(cliOnce.cmd, [...cliOnce.args, ...argsOnce], { stdio: ['ignore','pipe','pipe'], env: { ...process.env, BROWSERSLIST_IGNORE_OLD_DATA: '1' } });
691
+ const { spawnSync } = require("child_process");
692
+ const argsOnce = [
693
+ "-i",
694
+ inputCss,
695
+ "-o",
696
+ outputCss,
697
+ "-c",
698
+ configPath,
699
+ "--minify",
700
+ ];
701
+ const res = spawnSync(cliOnce.cmd, [...cliOnce.args, ...argsOnce], {
702
+ stdio: ["ignore", "pipe", "pipe"],
703
+ env: { ...process.env, BROWSERSLIST_IGNORE_OLD_DATA: "1" },
704
+ });
537
705
  if (res && res.status === 0) {
538
- console.log(`[tailwind] initial build ok (${fileSizeKb(outputCss)} KB) →`, prettyPath(outputCss));
706
+ console.log(
707
+ `[tailwind] initial build ok (${fileSizeKb(outputCss)} KB) →`,
708
+ prettyPath(outputCss)
709
+ );
539
710
  } else {
540
- console.warn('[tailwind] initial build failed; using fallback CSS');
541
- try { if (res && res.stderr) process.stderr.write(res.stderr); } catch (_) {}
711
+ console.warn("[tailwind] initial build failed; using fallback CSS");
712
+ try {
713
+ if (res && res.stderr) process.stderr.write(res.stderr);
714
+ } catch (_) {}
542
715
  writeFallbackCssIfMissing();
543
716
  }
544
717
  } else {
545
- console.warn('[tailwind] CLI not found; using fallback CSS');
718
+ console.warn("[tailwind] CLI not found; using fallback CSS");
546
719
  writeFallbackCssIfMissing();
547
720
  }
548
721
  } catch (_) {}
549
722
  // Prefer direct CLI spawn so we can mute initial rebuild logs
550
723
  const cli = resolveTailwindCli();
551
724
  if (cli) {
552
- const args = ['-i', inputCss, '-o', outputCss, '--watch', '-c', configPath, '--minify'];
725
+ const args = [
726
+ "-i",
727
+ inputCss,
728
+ "-o",
729
+ outputCss,
730
+ "--watch",
731
+ "-c",
732
+ configPath,
733
+ "--minify",
734
+ ];
553
735
  let unmuted = false;
554
736
  let cssWatcherAttached = false;
555
737
  function attachCssWatcherOnce() {
@@ -559,48 +741,94 @@ async function dev() {
559
741
  fs.watch(outputCss, { persistent: false }, () => {
560
742
  if (!unmuted) {
561
743
  unmuted = true;
562
- console.log(`[tailwind] watching ${prettyPath(inputCss)} — compiled (${fileSizeKb(outputCss)} KB)`);
744
+ console.log(
745
+ `[tailwind] watching ${prettyPath(
746
+ inputCss
747
+ )} — compiled (${fileSizeKb(outputCss)} KB)`
748
+ );
563
749
  }
564
- try { onCssChange(); } catch (_) {}
750
+ try {
751
+ onCssChange();
752
+ } catch (_) {}
565
753
  });
566
754
  } catch (_) {}
567
755
  }
568
756
  function compileTailwindOnce() {
569
757
  try {
570
- const { spawnSync } = require('child_process');
571
- const res = spawnSync(cli.cmd, [...cli.args, '-i', inputCss, '-o', outputCss, '-c', configPath, '--minify'], { stdio: ['ignore','pipe','pipe'], env: { ...process.env, BROWSERSLIST_IGNORE_OLD_DATA: '1' } });
758
+ const { spawnSync } = require("child_process");
759
+ const res = spawnSync(
760
+ cli.cmd,
761
+ [
762
+ ...cli.args,
763
+ "-i",
764
+ inputCss,
765
+ "-o",
766
+ outputCss,
767
+ "-c",
768
+ configPath,
769
+ "--minify",
770
+ ],
771
+ {
772
+ stdio: ["ignore", "pipe", "pipe"],
773
+ env: { ...process.env, BROWSERSLIST_IGNORE_OLD_DATA: "1" },
774
+ }
775
+ );
572
776
  if (res && res.status === 0) {
573
- console.log(`[tailwind] compiled (${fileSizeKb(outputCss)} KB) →`, prettyPath(outputCss));
574
- try { onCssChange(); } catch (_) {}
777
+ console.log(
778
+ `[tailwind] compiled (${fileSizeKb(outputCss)} KB) →`,
779
+ prettyPath(outputCss)
780
+ );
781
+ try {
782
+ onCssChange();
783
+ } catch (_) {}
575
784
  } else {
576
- console.warn('[tailwind] on-demand compile failed');
577
- try { if (res && res.stderr) process.stderr.write(res.stderr); } catch (_) {}
785
+ console.warn("[tailwind] on-demand compile failed");
786
+ try {
787
+ if (res && res.stderr) process.stderr.write(res.stderr);
788
+ } catch (_) {}
578
789
  }
579
790
  } catch (_) {}
580
791
  }
581
792
  function startTailwindWatcher() {
582
793
  unmuted = false;
583
- const proc = spawn(cli.cmd, [...cli.args, ...args], { stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, BROWSERSLIST_IGNORE_OLD_DATA: '1' } });
584
- if (proc.stdout) proc.stdout.on('data', (d) => {
585
- const s = d ? String(d) : '';
586
- if (!unmuted) {
587
- if (/error/i.test(s)) { try { process.stdout.write('[tailwind] ' + s); } catch (_) {} }
588
- } else {
589
- try { process.stdout.write(s); } catch (_) {}
590
- }
591
- });
592
- if (proc.stderr) proc.stderr.on('data', (d) => {
593
- const s = d ? String(d) : '';
594
- if (!unmuted) {
595
- if (s.trim()) { try { process.stderr.write('[tailwind] ' + s); } catch (_) {} }
596
- } else {
597
- try { process.stderr.write(s); } catch (_) {}
598
- }
794
+ const proc = spawn(cli.cmd, [...cli.args, ...args], {
795
+ stdio: ["ignore", "pipe", "pipe"],
796
+ env: { ...process.env, BROWSERSLIST_IGNORE_OLD_DATA: "1" },
599
797
  });
600
- proc.on('exit', (code) => {
798
+ if (proc.stdout)
799
+ proc.stdout.on("data", (d) => {
800
+ const s = d ? String(d) : "";
801
+ if (!unmuted) {
802
+ if (/error/i.test(s)) {
803
+ try {
804
+ process.stdout.write("[tailwind] " + s);
805
+ } catch (_) {}
806
+ }
807
+ } else {
808
+ try {
809
+ process.stdout.write(s);
810
+ } catch (_) {}
811
+ }
812
+ });
813
+ if (proc.stderr)
814
+ proc.stderr.on("data", (d) => {
815
+ const s = d ? String(d) : "";
816
+ if (!unmuted) {
817
+ if (s.trim()) {
818
+ try {
819
+ process.stderr.write("[tailwind] " + s);
820
+ } catch (_) {}
821
+ }
822
+ } else {
823
+ try {
824
+ process.stderr.write(s);
825
+ } catch (_) {}
826
+ }
827
+ });
828
+ proc.on("exit", (code) => {
601
829
  // Ignore null exits (expected when we intentionally restart the watcher)
602
830
  if (code !== 0 && code !== null) {
603
- console.error('[tailwind] watcher exited with code', code);
831
+ console.error("[tailwind] watcher exited with code", code);
604
832
  }
605
833
  });
606
834
  attachCssWatcherOnce();
@@ -610,55 +838,90 @@ async function dev() {
610
838
  // Unmute Tailwind logs after the first successful CSS write
611
839
  // Watch UI Tailwind plugin/preset files and restart Tailwind to pick up code changes
612
840
  try {
613
- const uiPlugin = path.join(__dirname, '../ui', 'tailwind-canopy-iiif-plugin.js');
614
- const uiPreset = path.join(__dirname, '../ui', 'tailwind-canopy-iiif-preset.js');
615
- const uiStylesDir = path.join(__dirname, '../ui', 'styles');
841
+ const uiPlugin = path.join(
842
+ __dirname,
843
+ "../ui",
844
+ "tailwind-canopy-iiif-plugin.js"
845
+ );
846
+ const uiPreset = path.join(
847
+ __dirname,
848
+ "../ui",
849
+ "tailwind-canopy-iiif-preset.js"
850
+ );
851
+ const uiStylesDir = path.join(__dirname, "../ui", "styles");
616
852
  const files = [uiPlugin, uiPreset].filter((p) => {
617
- try { return fs.existsSync(p); } catch (_) { return false; }
853
+ try {
854
+ return fs.existsSync(p);
855
+ } catch (_) {
856
+ return false;
857
+ }
618
858
  });
619
859
  let restartTimer = null;
620
860
  const restart = () => {
621
861
  clearTimeout(restartTimer);
622
862
  restartTimer = setTimeout(() => {
623
- console.log('[tailwind] detected UI plugin/preset change — restarting Tailwind');
624
- try { if (child && !child.killed) child.kill(); } catch (_) {}
863
+ console.log(
864
+ "[tailwind] detected UI plugin/preset change restarting Tailwind"
865
+ );
866
+ try {
867
+ if (child && !child.killed) child.kill();
868
+ } catch (_) {}
625
869
  // Force a compile immediately so new CSS lands before reload
626
870
  compileTailwindOnce();
627
871
  child = startTailwindWatcher();
628
872
  // Notify clients that a rebuild is in progress; CSS watcher will trigger reload on write
629
- try { onBuildStart(); } catch (_) {}
873
+ try {
874
+ onBuildStart();
875
+ } catch (_) {}
630
876
  }, 50);
631
877
  };
632
878
  for (const f of files) {
633
- try { fs.watch(f, { persistent: false }, restart); } catch (_) {}
879
+ try {
880
+ fs.watch(f, { persistent: false }, restart);
881
+ } catch (_) {}
634
882
  }
635
883
  // Watch UI styles directory (Sass partials used by the plugin); restart Tailwind on Sass changes
636
884
  try {
637
885
  if (fs.existsSync(uiStylesDir)) {
638
886
  try {
639
- fs.watch(uiStylesDir, { persistent: false, recursive: true }, (evt, fn) => {
640
- try {
641
- if (fn && /\.s[ac]ss$/i.test(String(fn))) restart();
642
- } catch (_) {}
643
- });
887
+ fs.watch(
888
+ uiStylesDir,
889
+ { persistent: false, recursive: true },
890
+ (evt, fn) => {
891
+ try {
892
+ if (fn && /\.s[ac]ss$/i.test(String(fn))) restart();
893
+ } catch (_) {}
894
+ }
895
+ );
644
896
  } catch (_) {
645
897
  // Fallback: per-dir watch without recursion
646
898
  const watchers = new Map();
647
899
  const watchDir = (dir) => {
648
900
  if (watchers.has(dir)) return;
649
901
  try {
650
- const w = fs.watch(dir, { persistent: false }, (evt, fn) => {
651
- try { if (fn && /\.s[ac]ss$/i.test(String(fn))) restart(); } catch (_) {}
652
- });
902
+ const w = fs.watch(
903
+ dir,
904
+ { persistent: false },
905
+ (evt, fn) => {
906
+ try {
907
+ if (fn && /\.s[ac]ss$/i.test(String(fn))) restart();
908
+ } catch (_) {}
909
+ }
910
+ );
653
911
  watchers.set(dir, w);
654
912
  } catch (_) {}
655
913
  };
656
914
  const scan = (dir) => {
657
915
  try {
658
- const entries = fs.readdirSync(dir, { withFileTypes: true });
916
+ const entries = fs.readdirSync(dir, {
917
+ withFileTypes: true,
918
+ });
659
919
  for (const e of entries) {
660
920
  const p = path.join(dir, e.name);
661
- if (e.isDirectory()) { watchDir(p); scan(p); }
921
+ if (e.isDirectory()) {
922
+ watchDir(p);
923
+ scan(p);
924
+ }
662
925
  }
663
926
  } catch (_) {}
664
927
  };
@@ -668,49 +931,75 @@ async function dev() {
668
931
  }
669
932
  } catch (_) {}
670
933
  // Also watch the app Tailwind config; restart Tailwind when it changes
671
- try { if (configPath && fs.existsSync(configPath)) fs.watch(configPath, { persistent: false }, () => {
672
- console.log('[tailwind] tailwind.config change — restarting Tailwind');
673
- restart();
674
- }); } catch (_) {}
934
+ try {
935
+ if (configPath && fs.existsSync(configPath))
936
+ fs.watch(configPath, { persistent: false }, () => {
937
+ console.log(
938
+ "[tailwind] tailwind.config change — restarting Tailwind"
939
+ );
940
+ restart();
941
+ });
942
+ } catch (_) {}
675
943
  // If the input CSS lives under app/styles, watch the directory for direct edits to CSS/partials
676
944
  try {
677
- const stylesDir = path.dirname(inputCss || '');
678
- if (stylesDir && stylesDir.includes(path.join('app','styles'))) {
945
+ const stylesDir = path.dirname(inputCss || "");
946
+ if (stylesDir && stylesDir.includes(path.join("app", "styles"))) {
679
947
  let cssDebounce = null;
680
948
  fs.watch(stylesDir, { persistent: false }, (evt, fn) => {
681
949
  clearTimeout(cssDebounce);
682
950
  cssDebounce = setTimeout(() => {
683
- try { onBuildStart(); } catch (_) {}
951
+ try {
952
+ onBuildStart();
953
+ } catch (_) {}
684
954
  // Force a compile so changes in index.css or partials are reflected immediately
685
- try { compileTailwindOnce(); } catch (_) {}
686
- try { onCssChange(); } catch (_) {}
955
+ try {
956
+ compileTailwindOnce();
957
+ } catch (_) {}
958
+ try {
959
+ onCssChange();
960
+ } catch (_) {}
687
961
  }, 50);
688
962
  });
689
963
  }
690
964
  } catch (_) {}
691
965
  } catch (_) {}
692
- } else if (twHelper && typeof twHelper.watchTailwind === 'function') {
966
+ } else if (twHelper && typeof twHelper.watchTailwind === "function") {
693
967
  // Fallback to helper (cannot mute its initial logs)
694
- child = twHelper.watchTailwind({ input: inputCss, output: outputCss, config: configPath, minify: false });
968
+ child = twHelper.watchTailwind({
969
+ input: inputCss,
970
+ output: outputCss,
971
+ config: configPath,
972
+ minify: false,
973
+ });
695
974
  if (child) {
696
- console.log('[tailwind] watching', prettyPath(inputCss));
697
- try { fs.watch(outputCss, { persistent: false }, () => { try { onCssChange(); } catch (_) {} }); } catch (_) {}
975
+ console.log("[tailwind] watching", prettyPath(inputCss));
976
+ try {
977
+ fs.watch(outputCss, { persistent: false }, () => {
978
+ try {
979
+ onCssChange();
980
+ } catch (_) {}
981
+ });
982
+ } catch (_) {}
698
983
  }
699
984
  }
700
985
  }
701
986
  } catch (_) {}
702
- console.log('[Watching]', prettyPath(CONTENT_DIR), '(Ctrl+C to stop)');
987
+ console.log("[Watching]", prettyPath(CONTENT_DIR), "(Ctrl+C to stop)");
703
988
  const rw = tryRecursiveWatch();
704
989
  if (!rw) watchPerDir();
705
990
  // Watch assets for live copy without full rebuild
706
991
  if (fs.existsSync(ASSETS_DIR)) {
707
- console.log('[Watching]', prettyPath(ASSETS_DIR), '(assets live-reload)');
992
+ console.log("[Watching]", prettyPath(ASSETS_DIR), "(assets live-reload)");
708
993
  const arw = tryRecursiveWatchAssets();
709
994
  if (!arw) watchAssetsPerDir();
710
995
  }
711
996
  // Watch UI dist for live-reload and targeted search runtime rebuilds
712
997
  if (fs.existsSync(UI_DIST_DIR)) {
713
- console.log('[Watching]', prettyPath(UI_DIST_DIR), '(@canopy-iiif/app/ui dist)');
998
+ console.log(
999
+ "[Watching]",
1000
+ prettyPath(UI_DIST_DIR),
1001
+ "(@canopy-iiif/app/ui dist)"
1002
+ );
714
1003
  const urw = tryRecursiveWatchUiDist();
715
1004
  if (!urw) watchUiDistPerDir();
716
1005
  }