@canopy-iiif/app 1.10.5 → 1.10.7
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/build.js +4 -0
- package/lib/build/iiif.js +68 -7
- package/lib/components/featured.js +79 -8
- package/lib/orchestrator.js +10 -0
- package/package.json +1 -1
package/lib/build/build.js
CHANGED
|
@@ -174,9 +174,13 @@ async function build(options = {}) {
|
|
|
174
174
|
} catch (_) {}
|
|
175
175
|
if (!skipIiif && hasIiifSources && currentManifestIds.length) {
|
|
176
176
|
try {
|
|
177
|
+
const featuredKeepIds = Array.isArray(CONFIG?.featured)
|
|
178
|
+
? CONFIG.featured
|
|
179
|
+
: [];
|
|
177
180
|
await iiif.cleanupIiifCache({
|
|
178
181
|
allowedManifestIds: currentManifestIds,
|
|
179
182
|
allowedCollectionIds: currentCollectionIds,
|
|
183
|
+
keepManifestIds: featuredKeepIds,
|
|
180
184
|
});
|
|
181
185
|
} catch (_) {}
|
|
182
186
|
}
|
package/lib/build/iiif.js
CHANGED
|
@@ -82,6 +82,7 @@ const IIIF_CACHE_INDEX_MANIFESTS = path.join(
|
|
|
82
82
|
const DEFAULT_THUMBNAIL_SIZE = 400;
|
|
83
83
|
const DEFAULT_CHUNK_SIZE = 10;
|
|
84
84
|
const DEFAULT_FETCH_CONCURRENCY = 1;
|
|
85
|
+
const DEFAULT_FEATURED_FETCH_TIMEOUT_MS = 15000;
|
|
85
86
|
const HERO_THUMBNAIL_SIZE = 800;
|
|
86
87
|
const HERO_IMAGE_SIZES_ATTR = "(min-width: 1024px) 1280px, 100vw";
|
|
87
88
|
const OG_IMAGE_WIDTH = 1200;
|
|
@@ -873,13 +874,27 @@ function extractCollectionEntries(collection) {
|
|
|
873
874
|
}
|
|
874
875
|
|
|
875
876
|
async function readJsonFromUri(uri, options = {log: false}) {
|
|
877
|
+
const opts = options && typeof options === "object" ? options : {};
|
|
878
|
+
const shouldLog = Boolean(opts.log);
|
|
879
|
+
const fetchSignal = opts.signal;
|
|
876
880
|
try {
|
|
877
881
|
if (/^https?:\/\//i.test(uri)) {
|
|
878
882
|
if (typeof fetch !== "function") return null;
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
+
let res = null;
|
|
884
|
+
try {
|
|
885
|
+
const fetchOptions = {headers: {Accept: "application/json"}};
|
|
886
|
+
if (fetchSignal) fetchOptions.signal = fetchSignal;
|
|
887
|
+
res = await fetch(uri, fetchOptions);
|
|
888
|
+
} catch (error) {
|
|
889
|
+
if (shouldLog) {
|
|
890
|
+
try {
|
|
891
|
+
const code = error && error.name === "AbortError" ? "ABORT" : "ERR";
|
|
892
|
+
logLine(`⊘ ${String(uri)} → ${code}`, "red", {bright: true});
|
|
893
|
+
} catch (_) {}
|
|
894
|
+
}
|
|
895
|
+
return null;
|
|
896
|
+
}
|
|
897
|
+
if (shouldLog) {
|
|
883
898
|
try {
|
|
884
899
|
if (res && res.ok) {
|
|
885
900
|
logLine(`↓ ${String(uri)} → ${res.status}`, "yellow", {
|
|
@@ -1235,14 +1250,53 @@ async function ensureFeaturedInCache(cfg) {
|
|
|
1235
1250
|
if (!featured.length) return;
|
|
1236
1251
|
const {size: thumbSize, unsafe: unsafeThumbs} =
|
|
1237
1252
|
resolveThumbnailPreferences();
|
|
1253
|
+
const fetchTimeoutMs = resolvePositiveInteger(
|
|
1254
|
+
process.env.CANOPY_FEATURED_FETCH_TIMEOUT_MS,
|
|
1255
|
+
DEFAULT_FEATURED_FETCH_TIMEOUT_MS,
|
|
1256
|
+
{allowZero: true},
|
|
1257
|
+
);
|
|
1258
|
+
const canAbortFetches =
|
|
1259
|
+
typeof AbortController === "function" && Number(fetchTimeoutMs) > 0;
|
|
1238
1260
|
for (const rawId of featured) {
|
|
1239
1261
|
const id = normalizeIiifId(String(rawId || ""));
|
|
1240
1262
|
if (!id) continue;
|
|
1241
1263
|
let manifest = await loadCachedManifestById(id);
|
|
1242
1264
|
if (!manifest) {
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1265
|
+
let controller = null;
|
|
1266
|
+
let timeoutId = null;
|
|
1267
|
+
let fetched = null;
|
|
1268
|
+
try {
|
|
1269
|
+
if (canAbortFetches) {
|
|
1270
|
+
controller = new AbortController();
|
|
1271
|
+
timeoutId = setTimeout(
|
|
1272
|
+
() => controller.abort(),
|
|
1273
|
+
Math.max(1, fetchTimeoutMs),
|
|
1274
|
+
);
|
|
1275
|
+
}
|
|
1276
|
+
fetched = await readJsonFromUri(id, {
|
|
1277
|
+
log: true,
|
|
1278
|
+
signal: controller ? controller.signal : undefined,
|
|
1279
|
+
});
|
|
1280
|
+
} catch (_) {
|
|
1281
|
+
fetched = null;
|
|
1282
|
+
} finally {
|
|
1283
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
1284
|
+
}
|
|
1285
|
+
if (!fetched) {
|
|
1286
|
+
if (controller && controller.signal && controller.signal.aborted) {
|
|
1287
|
+
try {
|
|
1288
|
+
logLine(
|
|
1289
|
+
`[iiif] Featured manifest timed out after ${formatDurationMs(
|
|
1290
|
+
fetchTimeoutMs,
|
|
1291
|
+
)}: ${id}`,
|
|
1292
|
+
"red",
|
|
1293
|
+
{dim: true},
|
|
1294
|
+
);
|
|
1295
|
+
} catch (_) {}
|
|
1296
|
+
}
|
|
1297
|
+
continue;
|
|
1298
|
+
}
|
|
1299
|
+
const upgraded = await upgradeIiifResource(fetched);
|
|
1246
1300
|
if (!upgraded || !upgraded.id) continue;
|
|
1247
1301
|
manifest = (await saveCachedManifest(upgraded, id, "")) || upgraded;
|
|
1248
1302
|
manifest = (await loadCachedManifestById(id)) || manifest;
|
|
@@ -1522,11 +1576,18 @@ async function cleanupIiifCache(options = {}) {
|
|
|
1522
1576
|
const allowedCollectionIds = Array.isArray(options.allowedCollectionIds)
|
|
1523
1577
|
? options.allowedCollectionIds
|
|
1524
1578
|
: [];
|
|
1579
|
+
const keepManifestIds = Array.isArray(options.keepManifestIds)
|
|
1580
|
+
? options.keepManifestIds
|
|
1581
|
+
: [];
|
|
1525
1582
|
const manifestSet = new Set(
|
|
1526
1583
|
allowedManifestIds
|
|
1527
1584
|
.map((id) => normalizeIiifId(String(id || "")))
|
|
1528
1585
|
.filter(Boolean),
|
|
1529
1586
|
);
|
|
1587
|
+
for (const keepId of keepManifestIds) {
|
|
1588
|
+
const normalized = normalizeIiifId(String(keepId || ""));
|
|
1589
|
+
if (normalized) manifestSet.add(normalized);
|
|
1590
|
+
}
|
|
1530
1591
|
const collectionSet = new Set(
|
|
1531
1592
|
allowedCollectionIds
|
|
1532
1593
|
.map((id) => normalizeIiifId(String(id || "")))
|
|
@@ -4,6 +4,9 @@ const yaml = require('js-yaml');
|
|
|
4
4
|
const { rootRelativeHref } = require('../common');
|
|
5
5
|
const { resolveCanopyConfigPath } = require('../config-path');
|
|
6
6
|
|
|
7
|
+
const IIIF_MANIFESTS_DIR = path.resolve('.cache/iiif/manifests');
|
|
8
|
+
const SLUG_MEMO = new Map();
|
|
9
|
+
|
|
7
10
|
function firstLabelString(label) {
|
|
8
11
|
if (!label) return 'Untitled';
|
|
9
12
|
if (typeof label === 'string') return label;
|
|
@@ -67,12 +70,11 @@ function readJson(p) {
|
|
|
67
70
|
|
|
68
71
|
function findSlugByIdFromDiskSync(nid) {
|
|
69
72
|
try {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const names = fs.readdirSync(dir);
|
|
73
|
+
if (!fs.existsSync(IIIF_MANIFESTS_DIR)) return null;
|
|
74
|
+
const names = fs.readdirSync(IIIF_MANIFESTS_DIR);
|
|
73
75
|
for (const name of names) {
|
|
74
76
|
if (!name || !name.toLowerCase().endsWith('.json')) continue;
|
|
75
|
-
const fp = path.join(
|
|
77
|
+
const fp = path.join(IIIF_MANIFESTS_DIR, name);
|
|
76
78
|
try {
|
|
77
79
|
const obj = readJson(fp);
|
|
78
80
|
const mid = normalizeIiifId(String((obj && (obj.id || obj['@id'])) || ''));
|
|
@@ -83,6 +85,75 @@ function findSlugByIdFromDiskSync(nid) {
|
|
|
83
85
|
return null;
|
|
84
86
|
}
|
|
85
87
|
|
|
88
|
+
function manifestPathFromSlug(slug) {
|
|
89
|
+
try {
|
|
90
|
+
const normalized = String(slug || '').trim();
|
|
91
|
+
if (!normalized || /[\\/]/.test(normalized)) return '';
|
|
92
|
+
return path.resolve(IIIF_MANIFESTS_DIR, `${normalized}.json`);
|
|
93
|
+
} catch (_) {
|
|
94
|
+
return '';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function manifestExistsForSlug(slug) {
|
|
99
|
+
const fp = manifestPathFromSlug(slug);
|
|
100
|
+
if (!fp) return false;
|
|
101
|
+
try {
|
|
102
|
+
return fs.existsSync(fp);
|
|
103
|
+
} catch (_) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readManifestBySlug(slug) {
|
|
109
|
+
const fp = manifestPathFromSlug(slug);
|
|
110
|
+
if (!fp) return null;
|
|
111
|
+
return readJson(fp);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function resolveSlugForFeaturedEntry(rawId, byId) {
|
|
115
|
+
if (rawId == null) return null;
|
|
116
|
+
const rawValue = String(rawId).trim();
|
|
117
|
+
if (!rawValue) return null;
|
|
118
|
+
if (!/^https?:\/\//i.test(rawValue) && manifestExistsForSlug(rawValue)) {
|
|
119
|
+
const entry = Array.isArray(byId)
|
|
120
|
+
? byId.find((candidate) => candidate && candidate.slug === rawValue)
|
|
121
|
+
: null;
|
|
122
|
+
return {slug: rawValue, entry};
|
|
123
|
+
}
|
|
124
|
+
const normalizedId = normalizeIiifId(rawValue);
|
|
125
|
+
if (!normalizedId) return null;
|
|
126
|
+
const memoSlug = SLUG_MEMO.get(normalizedId);
|
|
127
|
+
if (memoSlug && manifestExistsForSlug(memoSlug)) {
|
|
128
|
+
const memoEntry = Array.isArray(byId)
|
|
129
|
+
? byId.find((candidate) => candidate && candidate.slug === memoSlug)
|
|
130
|
+
: null;
|
|
131
|
+
return {slug: memoSlug, entry: memoEntry};
|
|
132
|
+
}
|
|
133
|
+
const entry = byId.find(
|
|
134
|
+
(candidate) =>
|
|
135
|
+
candidate && candidate.type === 'Manifest' && equalIiifId(candidate.id, normalizedId)
|
|
136
|
+
);
|
|
137
|
+
if (entry && entry.slug && manifestExistsForSlug(entry.slug)) {
|
|
138
|
+
SLUG_MEMO.set(normalizedId, entry.slug);
|
|
139
|
+
return {slug: entry.slug, entry};
|
|
140
|
+
}
|
|
141
|
+
const fallbackSlug = findSlugByIdFromDiskSync(normalizedId);
|
|
142
|
+
if (fallbackSlug && manifestExistsForSlug(fallbackSlug)) {
|
|
143
|
+
SLUG_MEMO.set(normalizedId, fallbackSlug);
|
|
144
|
+
const fallbackEntry = Array.isArray(byId)
|
|
145
|
+
? byId.find(
|
|
146
|
+
(candidate) =>
|
|
147
|
+
candidate &&
|
|
148
|
+
candidate.type === 'Manifest' &&
|
|
149
|
+
(candidate.slug === fallbackSlug || equalIiifId(candidate.id, normalizedId))
|
|
150
|
+
)
|
|
151
|
+
: null;
|
|
152
|
+
return {slug: fallbackSlug, entry: fallbackEntry};
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
86
157
|
function readFeaturedFromCacheSync() {
|
|
87
158
|
try {
|
|
88
159
|
const debug = !!process.env.CANOPY_DEBUG_FEATURED;
|
|
@@ -93,13 +164,13 @@ function readFeaturedFromCacheSync() {
|
|
|
93
164
|
const byId = Array.isArray(idx && idx.byId) ? idx.byId : [];
|
|
94
165
|
const out = [];
|
|
95
166
|
for (const id of featured) {
|
|
96
|
-
const nid = normalizeIiifId(id);
|
|
97
167
|
if (debug) { try { console.log('[featured] id:', id); } catch (_) {} }
|
|
98
|
-
const
|
|
99
|
-
const slug =
|
|
168
|
+
const resolved = resolveSlugForFeaturedEntry(id, byId);
|
|
169
|
+
const slug = resolved && resolved.slug ? resolved.slug : '';
|
|
170
|
+
const entry = resolved && resolved.entry ? resolved.entry : null;
|
|
100
171
|
if (debug) { try { console.log('[featured] - slug:', slug || '(none)'); } catch (_) {} }
|
|
101
172
|
if (!slug) continue;
|
|
102
|
-
const m =
|
|
173
|
+
const m = readManifestBySlug(slug);
|
|
103
174
|
if (!m) continue;
|
|
104
175
|
const rec = {
|
|
105
176
|
title: firstLabelString(m && m.label),
|
package/lib/orchestrator.js
CHANGED
|
@@ -185,6 +185,7 @@ async function orchestrate(options = {}) {
|
|
|
185
185
|
await prepareUi(mode, env);
|
|
186
186
|
|
|
187
187
|
const api = loadLibraryApi();
|
|
188
|
+
let exitAfterBuild = false;
|
|
188
189
|
try {
|
|
189
190
|
if (mode === 'dev') {
|
|
190
191
|
attachSignalHandlers();
|
|
@@ -196,12 +197,21 @@ async function orchestrate(options = {}) {
|
|
|
196
197
|
await api.build();
|
|
197
198
|
}
|
|
198
199
|
log('Build complete');
|
|
200
|
+
exitAfterBuild = true;
|
|
199
201
|
}
|
|
200
202
|
} finally {
|
|
201
203
|
if (uiWatcherChild && !uiWatcherChild.killed) {
|
|
202
204
|
try { uiWatcherChild.kill(); } catch (_) {}
|
|
203
205
|
}
|
|
204
206
|
}
|
|
207
|
+
|
|
208
|
+
if (exitAfterBuild) {
|
|
209
|
+
try {
|
|
210
|
+
if (!process.exitCode || process.exitCode === 0) process.exit(0);
|
|
211
|
+
} catch (_) {
|
|
212
|
+
process.exit(0);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
205
215
|
}
|
|
206
216
|
|
|
207
217
|
module.exports = {
|