@canopy-iiif/app 0.7.17 → 0.7.18

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
@@ -2,7 +2,6 @@ const fs = require("fs");
2
2
  const fsp = fs.promises;
3
3
  const path = require("path");
4
4
  const { spawn, spawnSync } = require("child_process");
5
- const { build } = require("../build/build");
6
5
  const http = require("http");
7
6
  const url = require("url");
8
7
  const {
@@ -22,11 +21,29 @@ function resolveTailwindCli() {
22
21
  return { cmd: 'tailwindcss', args: [] };
23
22
  }
24
23
  const PORT = Number(process.env.PORT || 5001);
24
+ const BUILD_MODULE_PATH = path.resolve(__dirname, "build.js");
25
25
  let onBuildSuccess = () => {};
26
26
  let onBuildStart = () => {};
27
27
  let onCssChange = () => {};
28
28
  let nextBuildSkipIiif = false; // hint set by watchers
29
29
  const UI_DIST_DIR = path.resolve(path.join(__dirname, "../../ui/dist"));
30
+ const APP_PACKAGE_ROOT = path.resolve(path.join(__dirname, "..", ".."));
31
+ const APP_LIB_DIR = path.join(APP_PACKAGE_ROOT, "lib");
32
+ const APP_UI_DIR = path.join(APP_PACKAGE_ROOT, "ui");
33
+ const APP_WATCH_TARGETS = [
34
+ { dir: APP_LIB_DIR, label: "@canopy-iiif/app/lib" },
35
+ { dir: APP_UI_DIR, label: "@canopy-iiif/app/ui" },
36
+ ];
37
+ const HAS_APP_WORKSPACE = (() => {
38
+ try {
39
+ return fs.existsSync(path.join(APP_PACKAGE_ROOT, "package.json"));
40
+ } catch (_) {
41
+ return false;
42
+ }
43
+ })();
44
+ let pendingModuleReload = false;
45
+ let building = false;
46
+ let buildAgain = false;
30
47
 
31
48
  function prettyPath(p) {
32
49
  try {
@@ -40,16 +57,71 @@ function prettyPath(p) {
40
57
  }
41
58
  }
42
59
 
60
+ function loadBuildFunction() {
61
+ let mod = null;
62
+ try {
63
+ mod = require(BUILD_MODULE_PATH);
64
+ } catch (error) {
65
+ throw new Error(
66
+ `[watch] Failed to load build module (${BUILD_MODULE_PATH}): ${
67
+ error && error.message ? error.message : error
68
+ }`
69
+ );
70
+ }
71
+ const fn =
72
+ mod && typeof mod.build === "function"
73
+ ? mod.build
74
+ : mod && mod.default && typeof mod.default.build === "function"
75
+ ? mod.default.build
76
+ : null;
77
+ if (typeof fn !== "function") {
78
+ throw new Error("[watch] Invalid build module export: expected build() function");
79
+ }
80
+ return fn;
81
+ }
82
+
83
+ function clearAppModuleCache() {
84
+ try {
85
+ const prefix = APP_PACKAGE_ROOT.endsWith(path.sep)
86
+ ? APP_PACKAGE_ROOT
87
+ : APP_PACKAGE_ROOT + path.sep;
88
+ for (const key of Object.keys(require.cache || {})) {
89
+ if (!key) continue;
90
+ try {
91
+ if (key === APP_PACKAGE_ROOT || key.startsWith(prefix)) {
92
+ delete require.cache[key];
93
+ }
94
+ } catch (_) {}
95
+ }
96
+ } catch (_) {}
97
+ }
98
+
43
99
  async function runBuild() {
100
+ if (building) {
101
+ buildAgain = true;
102
+ return;
103
+ }
104
+ building = true;
105
+ const hint = { skipIiif: !!nextBuildSkipIiif };
106
+ nextBuildSkipIiif = false;
44
107
  try {
45
- const hint = { skipIiif: !!nextBuildSkipIiif };
46
- nextBuildSkipIiif = false;
47
- await build(hint);
108
+ if (pendingModuleReload) {
109
+ clearAppModuleCache();
110
+ pendingModuleReload = false;
111
+ }
112
+ const buildFn = loadBuildFunction();
113
+ await buildFn(hint);
48
114
  try {
49
115
  onBuildSuccess();
50
116
  } catch (_) {}
51
117
  } catch (e) {
52
118
  console.error("Build failed:", e && e.message ? e.message : e);
119
+ } finally {
120
+ building = false;
121
+ if (buildAgain) {
122
+ buildAgain = false;
123
+ debounceBuild();
124
+ }
53
125
  }
54
126
  }
55
127
 
@@ -334,6 +406,131 @@ function watchUiDistPerDir() {
334
406
  };
335
407
  }
336
408
 
409
+ const APP_WATCH_EXTENSIONS = new Set([".js", ".jsx", ".scss"]);
410
+
411
+ function shouldIgnoreAppSourcePath(p) {
412
+ try {
413
+ const resolved = path.resolve(p);
414
+ const rel = path.relative(APP_PACKAGE_ROOT, resolved);
415
+ if (!rel || rel === "") return false;
416
+ if (rel.startsWith("..")) return true;
417
+ const segments = rel.split(path.sep).filter(Boolean);
418
+ if (!segments.length) return false;
419
+ if (segments.includes("node_modules")) return true;
420
+ if (segments.includes(".git")) return true;
421
+ if (segments[0] === "ui" && segments[1] === "dist") return true;
422
+ return false;
423
+ } catch (_) {
424
+ return true;
425
+ }
426
+ }
427
+
428
+ function handleAppSourceChange(baseDir, eventType, filename, label) {
429
+ if (!filename) return;
430
+ const full = path.resolve(baseDir, filename);
431
+ if (shouldIgnoreAppSourcePath(full)) return;
432
+ const ext = path.extname(full).toLowerCase();
433
+ if (!APP_WATCH_EXTENSIONS.has(ext)) return;
434
+ try {
435
+ const relLib = path.relative(APP_LIB_DIR, full);
436
+ if (!relLib.startsWith("..") && !path.isAbsolute(relLib)) {
437
+ pendingModuleReload = true;
438
+ }
439
+ } catch (_) {}
440
+ try {
441
+ console.log(
442
+ `[pkg] ${eventType}: ${prettyPath(full)}${label ? ` (${label})` : ""}`
443
+ );
444
+ } catch (_) {}
445
+ nextBuildSkipIiif = true;
446
+ try {
447
+ onBuildStart();
448
+ } catch (_) {}
449
+ debounceBuild();
450
+ }
451
+
452
+ function tryRecursiveWatchAppDir(dir, label) {
453
+ try {
454
+ return fs.watch(dir, { recursive: true }, (eventType, filename) => {
455
+ handleAppSourceChange(dir, eventType, filename, label);
456
+ });
457
+ } catch (_) {
458
+ return null;
459
+ }
460
+ }
461
+
462
+ function watchAppDirPerDir(dir, label) {
463
+ const watchers = new Map();
464
+
465
+ function watchDir(target) {
466
+ if (watchers.has(target)) return;
467
+ if (shouldIgnoreAppSourcePath(target)) return;
468
+ try {
469
+ const w = fs.watch(target, (eventType, filename) => {
470
+ if (filename) {
471
+ handleAppSourceChange(target, eventType, filename, label);
472
+ }
473
+ scan(target);
474
+ });
475
+ watchers.set(target, w);
476
+ } catch (_) {}
477
+ }
478
+
479
+ function scan(target) {
480
+ let entries;
481
+ try {
482
+ entries = fs.readdirSync(target, { withFileTypes: true });
483
+ } catch (_) {
484
+ return;
485
+ }
486
+ for (const entry of entries) {
487
+ if (!entry.isDirectory()) continue;
488
+ const sub = path.join(target, entry.name);
489
+ if (shouldIgnoreAppSourcePath(sub)) continue;
490
+ watchDir(sub);
491
+ scan(sub);
492
+ }
493
+ }
494
+
495
+ watchDir(dir);
496
+ scan(dir);
497
+
498
+ return () => {
499
+ for (const w of watchers.values()) {
500
+ try {
501
+ w.close();
502
+ } catch (_) {}
503
+ }
504
+ };
505
+ }
506
+
507
+ function watchAppSources() {
508
+ if (!HAS_APP_WORKSPACE) return () => {};
509
+ const stops = [];
510
+ for (const target of APP_WATCH_TARGETS) {
511
+ const { dir, label } = target;
512
+ if (!dir || !fs.existsSync(dir)) continue;
513
+ console.log(`[Watching] ${prettyPath(dir)} (${label})`);
514
+ const watcher = tryRecursiveWatchAppDir(dir, label);
515
+ if (!watcher) {
516
+ stops.push(watchAppDirPerDir(dir, label));
517
+ } else {
518
+ stops.push(() => {
519
+ try {
520
+ watcher.close();
521
+ } catch (_) {}
522
+ });
523
+ }
524
+ }
525
+ return () => {
526
+ for (const stop of stops) {
527
+ try {
528
+ if (typeof stop === "function") stop();
529
+ } catch (_) {}
530
+ }
531
+ };
532
+ }
533
+
337
534
  const MIME = {
338
535
  ".html": "text/html; charset=utf-8",
339
536
  ".css": "text/css; charset=utf-8",
@@ -900,6 +1097,9 @@ async function dev() {
900
1097
  const urw = tryRecursiveWatchUiDist();
901
1098
  if (!urw) watchUiDistPerDir();
902
1099
  }
1100
+ if (HAS_APP_WORKSPACE) {
1101
+ watchAppSources();
1102
+ }
903
1103
  }
904
1104
 
905
1105
  module.exports = { dev };
@@ -0,0 +1,203 @@
1
+ const { spawn } = require('child_process');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ const log = (msg) => console.log(`[canopy] ${msg}`);
6
+ const warn = (msg) => console.warn(`[canopy][warn] ${msg}`);
7
+ const err = (msg) => console.error(`[canopy][error] ${msg}`);
8
+
9
+ let uiWatcherChild = null;
10
+
11
+ const workspacePackageJsonPath = path.resolve(process.cwd(), 'packages/app/package.json');
12
+ const hasAppWorkspace = fs.existsSync(workspacePackageJsonPath);
13
+
14
+ function getMode(argv = process.argv.slice(2), env = process.env) {
15
+ const cli = new Set(argv);
16
+ if (cli.has('--dev')) return 'dev';
17
+ if (cli.has('--build')) return 'build';
18
+
19
+ if (env.CANOPY_MODE === 'dev') return 'dev';
20
+ if (env.CANOPY_MODE === 'build') return 'build';
21
+
22
+ const npmScript = env.npm_lifecycle_event;
23
+ if (npmScript === 'dev') return 'dev';
24
+ if (npmScript === 'build') return 'build';
25
+
26
+ return 'build';
27
+ }
28
+
29
+ function runOnce(cmd, args, opts = {}) {
30
+ return new Promise((resolve, reject) => {
31
+ const child = spawn(cmd, args, { stdio: 'inherit', shell: false, ...opts });
32
+ child.on('error', reject);
33
+ child.on('exit', (code) => {
34
+ if (code === 0) {
35
+ resolve();
36
+ } else {
37
+ reject(new Error(`${cmd} ${args.join(' ')} exited with code ${code}`));
38
+ }
39
+ });
40
+ });
41
+ }
42
+
43
+ function start(cmd, args, opts = {}) {
44
+ const child = spawn(cmd, args, { stdio: 'inherit', shell: false, ...opts });
45
+ child.on('error', (error) => {
46
+ const message = error && error.message ? error.message : String(error);
47
+ warn(`Subprocess error (${cmd}): ${message}`);
48
+ });
49
+ return child;
50
+ }
51
+
52
+ async function prepareUi(mode, env = process.env) {
53
+ if (!hasAppWorkspace) {
54
+ log('Using bundled UI assets from @canopy-iiif/app (workspace not detected)');
55
+ return null;
56
+ }
57
+
58
+ if (mode === 'build') {
59
+ log('Building UI assets (@canopy-iiif/app/ui)');
60
+ try {
61
+ await runOnce('npm', ['-w', '@canopy-iiif/app', 'run', 'ui:build'], { env });
62
+ log('UI assets built');
63
+ } catch (error) {
64
+ warn(`UI build skipped: ${(error && error.message) || String(error)}`);
65
+ }
66
+ return null;
67
+ }
68
+
69
+ try {
70
+ log('Prebuilding UI assets (@canopy-iiif/app/ui)');
71
+ await runOnce('npm', ['-w', '@canopy-iiif/app', 'run', 'ui:build'], { env });
72
+ } catch (error) {
73
+ warn(`UI prebuild skipped: ${(error && error.message) || String(error)}`);
74
+ }
75
+
76
+ log('Starting UI watcher (@canopy-iiif/app/ui)');
77
+ try {
78
+ uiWatcherChild = start('npm', ['-w', '@canopy-iiif/app', 'run', 'ui:watch'], { env });
79
+ } catch (error) {
80
+ warn(`UI watch skipped: ${(error && error.message) || String(error)}`);
81
+ uiWatcherChild = null;
82
+ }
83
+ return uiWatcherChild;
84
+ }
85
+
86
+ function loadLibraryApi() {
87
+ let lib;
88
+ try {
89
+ lib = require('./index.js');
90
+ } catch (e) {
91
+ const hint = [
92
+ 'Unable to load @canopy-iiif/app.',
93
+ 'Ensure dependencies are installed (npm install)',
94
+ "and that peer deps like 'react' are present.",
95
+ ].join(' ');
96
+ const detail = e && e.message ? `\nCaused by: ${e.message}` : '';
97
+ throw new Error(`${hint}${detail}`);
98
+ }
99
+
100
+ const api = lib && (typeof lib.build === 'function' || typeof lib.dev === 'function')
101
+ ? lib
102
+ : lib && lib.default
103
+ ? lib.default
104
+ : lib;
105
+
106
+ if (!api || (typeof api.build !== 'function' && typeof api.dev !== 'function')) {
107
+ throw new TypeError('Invalid @canopy-iiif/app export: expected functions build() and/or dev().');
108
+ }
109
+
110
+ return api;
111
+ }
112
+
113
+ function attachSignalHandlers() {
114
+ const clean = () => {
115
+ if (uiWatcherChild && !uiWatcherChild.killed) {
116
+ try { uiWatcherChild.kill(); } catch (_) {}
117
+ }
118
+ };
119
+
120
+ process.on('SIGINT', () => {
121
+ clean();
122
+ process.exit(130);
123
+ });
124
+ process.on('SIGTERM', () => {
125
+ clean();
126
+ process.exit(143);
127
+ });
128
+ process.on('exit', clean);
129
+ }
130
+
131
+ function verifyBuildOutput(outDir = 'site') {
132
+ const root = path.resolve(outDir);
133
+ function walk(dir) {
134
+ let count = 0;
135
+ if (!fs.existsSync(dir)) return 0;
136
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
137
+ for (const entry of entries) {
138
+ const p = path.join(dir, entry.name);
139
+ if (entry.isDirectory()) count += walk(p);
140
+ else if (entry.isFile() && p.toLowerCase().endsWith('.html')) count += 1;
141
+ }
142
+ return count;
143
+ }
144
+ const pages = walk(root);
145
+ if (!pages) {
146
+ throw new Error('CI check failed: no HTML pages generated in "site/".');
147
+ }
148
+ log(`CI check: found ${pages} HTML page(s) in ${root}.`);
149
+ }
150
+
151
+ async function orchestrate(options = {}) {
152
+ const argv = options.argv || process.argv.slice(2);
153
+ const env = options.env || process.env;
154
+
155
+ process.title = 'canopy-app';
156
+ const mode = getMode(argv, env);
157
+ log(`Mode: ${mode}`);
158
+
159
+ const cli = new Set(argv);
160
+ if (cli.has('--verify')) {
161
+ verifyBuildOutput(env.CANOPY_OUT_DIR || 'site');
162
+ return;
163
+ }
164
+
165
+ await prepareUi(mode, env);
166
+
167
+ const api = loadLibraryApi();
168
+ try {
169
+ if (mode === 'dev') {
170
+ attachSignalHandlers();
171
+ log('Starting dev server...');
172
+ await (typeof api.dev === 'function' ? api.dev() : Promise.resolve());
173
+ } else {
174
+ log('Building site...');
175
+ if (typeof api.build === 'function') {
176
+ await api.build();
177
+ }
178
+ log('Build complete');
179
+ if (env.CANOPY_VERIFY === '1' || env.CANOPY_VERIFY === 'true') {
180
+ verifyBuildOutput(env.CANOPY_OUT_DIR || 'site');
181
+ }
182
+ }
183
+ } finally {
184
+ if (uiWatcherChild && !uiWatcherChild.killed) {
185
+ try { uiWatcherChild.kill(); } catch (_) {}
186
+ }
187
+ }
188
+ }
189
+
190
+ module.exports = {
191
+ orchestrate,
192
+ verifyBuildOutput,
193
+ _internals: {
194
+ getMode,
195
+ prepareUi,
196
+ loadLibraryApi,
197
+ runOnce,
198
+ start,
199
+ },
200
+ log,
201
+ warn,
202
+ err,
203
+ };
@@ -346,10 +346,11 @@ async function attachCommand(host) {
346
346
 
347
347
  host.addEventListener('click', (event) => {
348
348
  const trigger = event.target && event.target.closest && event.target.closest('[data-canopy-command-trigger]');
349
- if (trigger) {
350
- event.preventDefault();
351
- openPanel();
352
- }
349
+ if (!trigger) return;
350
+ const mode = (trigger.dataset && trigger.dataset.canopyCommandTrigger) || '';
351
+ if (mode === 'submit' || mode === 'form') return;
352
+ event.preventDefault();
353
+ openPanel();
353
354
  });
354
355
 
355
356
  try {