@canopy-iiif/app 1.6.0 → 1.6.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.
@@ -11,22 +11,21 @@ const mdx = require("./mdx");
11
11
  const iiif = require("./iiif");
12
12
  const pages = require("./pages");
13
13
  const searchBuild = require("./search");
14
- const { buildSearchIndex } = require("./search-index");
14
+ const {buildSearchIndex} = require("./search-index");
15
15
  const runtimes = require("./runtimes");
16
- const { writeSitemap } = require("./sitemap");
17
- const {
18
- ensureSearchInitialized,
19
- finalizeSearch,
20
- } = require("./search-workflow");
21
- const { ensureStyles } = require("./styles");
22
- const { copyAssets } = require("./assets");
23
- const { logLine } = require("./log");
16
+ const {writeSitemap} = require("./sitemap");
17
+ const {ensureSearchInitialized, finalizeSearch} = require("./search-workflow");
18
+ const {ensureStyles} = require("./styles");
19
+ const {copyAssets} = require("./assets");
20
+ const {logLine} = require("./log");
24
21
  const navigation = require("../components/navigation");
25
22
  const referenced = require("../components/referenced");
26
23
  const bibliography = require("../components/bibliography");
27
24
 
28
25
  // hold records between builds if skipping IIIF
29
26
  let iiifRecordsCache = [];
27
+ let iiifManifestIdsCache = [];
28
+ let iiifCollectionIdsCache = [];
30
29
  let pageRecords = [];
31
30
 
32
31
  function nowMs() {
@@ -38,7 +37,7 @@ function nowMs() {
38
37
  }
39
38
 
40
39
  function formatDuration(ms) {
41
- if (!Number.isFinite(ms) || ms < 0) return '0ms';
40
+ if (!Number.isFinite(ms) || ms < 0) return "0ms";
42
41
  if (ms >= 1000) return `${(ms / 1000).toFixed(1)}s`;
43
42
  return `${Math.round(ms)}ms`;
44
43
  }
@@ -49,25 +48,33 @@ async function timeStage(label, store, fn) {
49
48
  return await fn();
50
49
  } finally {
51
50
  const durationMs = nowMs() - start;
52
- if (Array.isArray(store)) store.push({ label, durationMs });
51
+ if (Array.isArray(store)) store.push({label, durationMs});
53
52
  try {
54
- logLine(`⏱ ${label} completed in ${formatDuration(durationMs)}`, 'cyan', {
55
- dim: true,
56
- });
53
+ logLine(
54
+ `⏱ ${label} completed in ${formatDuration(durationMs)}`,
55
+ "cyan",
56
+ {
57
+ dim: true,
58
+ },
59
+ );
57
60
  } catch (_) {}
58
61
  }
59
62
  }
60
63
 
61
64
  async function ensureNoJekyllMarker() {
62
65
  try {
63
- await fsp.mkdir(OUT_DIR, { recursive: true });
64
- const markerPath = path.join(OUT_DIR, '.nojekyll');
65
- await fsp.writeFile(markerPath, '', 'utf8');
66
+ await fsp.mkdir(OUT_DIR, {recursive: true});
67
+ const markerPath = path.join(OUT_DIR, ".nojekyll");
68
+ await fsp.writeFile(markerPath, "", "utf8");
66
69
  } catch (_) {}
67
70
  }
68
71
 
69
72
  async function build(options = {}) {
70
- const skipIiif = !!(options?.skipIiif || process.env.CANOPY_SKIP_IIIF === '1' || process.env.CANOPY_SKIP_IIIF === 'true');
73
+ const skipIiif = !!(
74
+ options?.skipIiif ||
75
+ process.env.CANOPY_SKIP_IIIF === "1" ||
76
+ process.env.CANOPY_SKIP_IIIF === "true"
77
+ );
71
78
  if (!fs.existsSync(CONTENT_DIR)) {
72
79
  console.error("No content directory found at", CONTENT_DIR);
73
80
  process.exit(1);
@@ -83,7 +90,7 @@ async function build(options = {}) {
83
90
  bright: true,
84
91
  underscore: true,
85
92
  });
86
- logLine("• Reset MDX cache", "blue", { dim: true });
93
+ logLine("• Reset MDX cache", "blue", {dim: true});
87
94
  mdx?.resetMdxCaches();
88
95
  navigation?.resetNavigationCache?.();
89
96
  referenced?.resetReferenceIndex?.();
@@ -91,9 +98,9 @@ async function build(options = {}) {
91
98
  if (!skipIiif) {
92
99
  await cleanDir(OUT_DIR);
93
100
  await ensureNoJekyllMarker();
94
- logLine(`• Cleaned output directory`, "blue", { dim: true });
101
+ logLine(`• Cleaned output directory`, "blue", {dim: true});
95
102
  } else {
96
- logLine("• Retaining cache (skipping IIIF rebuild)", "blue", { dim: true });
103
+ logLine("• Retaining cache (skipping IIIF rebuild)", "blue", {dim: true});
97
104
  }
98
105
  if (skipIiif) {
99
106
  await ensureNoJekyllMarker();
@@ -107,29 +114,72 @@ async function build(options = {}) {
107
114
  */
108
115
  let iiifRecords = [];
109
116
  let CONFIG;
117
+ let currentManifestIds = [];
118
+ let currentCollectionIds = [];
110
119
  await timeStage("Build IIIF Collection content", stageTimings, async () => {
111
120
  logLine("\nBuild IIIF Collection content", "magenta", {
112
121
  bright: true,
113
122
  underscore: true,
114
123
  });
115
124
  CONFIG = await iiif.loadConfig();
116
- if (!skipIiif) {
125
+ const sources = iiif.resolveIiifSources(CONFIG);
126
+ const hasIiifSources =
127
+ Array.isArray(sources?.collections) && sources.collections.length
128
+ ? true
129
+ : Array.isArray(sources?.manifests) && sources.manifests.length > 0;
130
+ if (!skipIiif && hasIiifSources) {
117
131
  const results = await iiif.buildIiifCollectionPages(CONFIG);
118
132
  iiifRecords = results?.iiifRecords;
119
133
  iiifRecordsCache = Array.isArray(iiifRecords) ? iiifRecords : [];
134
+ currentManifestIds = Array.isArray(results?.manifestIds)
135
+ ? results.manifestIds
136
+ : [];
137
+ currentCollectionIds = Array.isArray(results?.collectionIds)
138
+ ? results.collectionIds
139
+ : [];
140
+ iiifManifestIdsCache = currentManifestIds;
141
+ iiifCollectionIdsCache = currentCollectionIds;
142
+ logLine(
143
+ `• IIIF records collected: ${Array.isArray(iiifRecords) ? iiifRecords.length : 0}`,
144
+ "blue",
145
+ {dim: true},
146
+ );
147
+ } else if (!skipIiif && !hasIiifSources) {
148
+ iiifRecords = [];
149
+ iiifRecordsCache = [];
150
+ currentManifestIds = [];
151
+ currentCollectionIds = [];
152
+ iiifManifestIdsCache = [];
153
+ iiifCollectionIdsCache = [];
154
+ logLine("• No IIIF sources configured; skipping", "blue", {dim: true});
120
155
  } else {
121
156
  iiifRecords = Array.isArray(iiifRecordsCache) ? iiifRecordsCache : [];
157
+ currentManifestIds = Array.isArray(iiifManifestIdsCache)
158
+ ? iiifManifestIdsCache
159
+ : [];
160
+ currentCollectionIds = Array.isArray(iiifCollectionIdsCache)
161
+ ? iiifCollectionIdsCache
162
+ : [];
122
163
  logLine(
123
164
  `• Reusing cached IIIF search records (${iiifRecords.length})`,
124
165
  "blue",
125
- { dim: true }
166
+ {dim: true},
126
167
  );
127
168
  }
128
169
  // Ensure any configured featured manifests are cached (and thumbnails computed)
129
170
  // so SSR interstitials can resolve items even if they are not part of
130
171
  // the traversed collection or when IIIF build is skipped during incremental rebuilds.
131
- try { await iiif.ensureFeaturedInCache(CONFIG); } catch (_) {}
132
- try { await iiif.rebuildManifestIndexFromCache(); } catch (_) {}
172
+ try {
173
+ await iiif.ensureFeaturedInCache(CONFIG);
174
+ } catch (_) {}
175
+ if (!skipIiif && hasIiifSources && currentManifestIds.length) {
176
+ try {
177
+ await iiif.cleanupIiifCache({
178
+ allowedManifestIds: currentManifestIds,
179
+ allowedCollectionIds: currentCollectionIds,
180
+ });
181
+ } catch (_) {}
182
+ }
133
183
  });
134
184
 
135
185
  /**
@@ -137,25 +187,30 @@ async function build(options = {}) {
137
187
  * This includes collecting page metadata for sitemap and search index,
138
188
  * as well as building all MDX pages to HTML.
139
189
  */
140
- await timeStage("Build contextual content from Markdown pages", stageTimings, async () => {
141
- logLine("\nBuild contextual content from Markdown pages", "magenta", {
142
- bright: true,
143
- underscore: true,
144
- });
145
- // Interstitials read directly from the local IIIF cache; no API file needed
146
- try {
147
- bibliography?.buildBibliographyIndexSync?.();
148
- } catch (err) {
149
- logLine(
150
- "• Failed to build bibliography index: " + String(err && err.message ? err.message : err),
151
- "red",
152
- { dim: true }
153
- );
154
- }
155
- pageRecords = await searchBuild.collectMdxPageRecords();
156
- await pages.buildContentTree(CONTENT_DIR, pageRecords);
157
- logLine("✓ MDX pages built", "green");
158
- });
190
+ await timeStage(
191
+ "Build contextual content from Markdown pages",
192
+ stageTimings,
193
+ async () => {
194
+ logLine("\nBuild contextual content from Markdown pages", "magenta", {
195
+ bright: true,
196
+ underscore: true,
197
+ });
198
+ // Interstitials read directly from the local IIIF cache; no API file needed
199
+ try {
200
+ bibliography?.buildBibliographyIndexSync?.();
201
+ } catch (err) {
202
+ logLine(
203
+ "• Failed to build bibliography index: " +
204
+ String(err && err.message ? err.message : err),
205
+ "red",
206
+ {dim: true},
207
+ );
208
+ }
209
+ pageRecords = await searchBuild.collectMdxPageRecords();
210
+ await pages.buildContentTree(CONTENT_DIR, pageRecords);
211
+ logLine("✓ MDX pages built", "green");
212
+ },
213
+ );
159
214
 
160
215
  /**
161
216
  * Build search index from IIIF and MDX records, then build or update
@@ -172,7 +227,7 @@ async function build(options = {}) {
172
227
  const combined = await buildSearchIndex(iiifRecords, pageRecords);
173
228
  await finalizeSearch(combined);
174
229
  } catch (e) {
175
- logLine("✗ Search index creation failed", "red", { bright: true });
230
+ logLine("✗ Search index creation failed", "red", {bright: true});
176
231
  logLine(" " + String(e), "red");
177
232
  }
178
233
  });
@@ -188,7 +243,7 @@ async function build(options = {}) {
188
243
  try {
189
244
  await writeSitemap(iiifRecords, pageRecords);
190
245
  } catch (e) {
191
- logLine("✗ Failed to write sitemap.xml", "red", { bright: true });
246
+ logLine("✗ Failed to write sitemap.xml", "red", {bright: true});
192
247
  logLine(" " + String(e), "red");
193
248
  }
194
249
  });
@@ -199,23 +254,27 @@ async function build(options = {}) {
199
254
  * Prepare client runtimes (e.g. search) by bundling with esbuild.
200
255
  * This is done early so that MDX content can reference runtime assets if needed.
201
256
  */
202
- await timeStage("Prepare client runtimes and stylesheets", stageTimings, async () => {
203
- logLine("\nPrepare client runtimes and stylesheets", "magenta", {
204
- bright: true,
205
- underscore: true,
206
- });
207
- const tasks = [];
208
- if (!process.env.CANOPY_SKIP_STYLES) {
209
- tasks.push(
210
- (async () => {
211
- await ensureStyles();
212
- logLine("✓ Wrote styles.css", "cyan");
213
- })()
214
- );
215
- }
216
- tasks.push(runtimes.prepareAllRuntimes());
217
- await Promise.all(tasks);
218
- });
257
+ await timeStage(
258
+ "Prepare client runtimes and stylesheets",
259
+ stageTimings,
260
+ async () => {
261
+ logLine("\nPrepare client runtimes and stylesheets", "magenta", {
262
+ bright: true,
263
+ underscore: true,
264
+ });
265
+ const tasks = [];
266
+ if (!process.env.CANOPY_SKIP_STYLES) {
267
+ tasks.push(
268
+ (async () => {
269
+ await ensureStyles();
270
+ logLine("✓ Wrote styles.css", "cyan");
271
+ })(),
272
+ );
273
+ }
274
+ tasks.push(runtimes.prepareAllRuntimes());
275
+ await Promise.all(tasks);
276
+ },
277
+ );
219
278
 
220
279
  /**
221
280
  * Copy static assets from the assets directory to the output directory.
@@ -229,15 +288,16 @@ async function build(options = {}) {
229
288
  });
230
289
 
231
290
  const totalMs = nowMs() - buildStart;
232
- logLine("\nBuild phase timings", "magenta", { bright: true, underscore: true });
233
- for (const { label, durationMs } of stageTimings) {
234
- logLine(`• ${label}: ${formatDuration(durationMs)}`, "blue", { dim: true });
291
+ logLine("\nBuild phase timings", "magenta", {bright: true, underscore: true});
292
+ for (const {label, durationMs} of stageTimings) {
293
+ logLine(`• ${label}: ${formatDuration(durationMs)}`, "blue", {dim: true});
235
294
  }
236
- logLine(`Total build time: ${formatDuration(totalMs)}`, "green", { bright: true });
237
-
295
+ logLine(`Total build time: ${formatDuration(totalMs)}`, "green", {
296
+ bright: true,
297
+ });
238
298
  }
239
299
 
240
- module.exports = { build };
300
+ module.exports = {build};
241
301
 
242
302
  if (require.main === module) {
243
303
  build().catch((e) => {