@canopy-iiif/app 0.7.9 → 0.7.11
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 +20 -1
- package/lib/build/iiif.js +68 -2
- package/lib/build/mdx.js +197 -6
- package/lib/build/pages.js +7 -3
- package/lib/build/runtimes.js +66 -8
- package/lib/build/verify.js +60 -0
- package/lib/components/featured.js +144 -0
- package/lib/search/search-app.jsx +35 -13
- package/lib/search/search.js +15 -44
- package/package.json +1 -1
- package/ui/dist/index.mjs +143 -166
- package/ui/dist/index.mjs.map +4 -4
- package/ui/dist/server.mjs +212 -26
- package/ui/dist/server.mjs.map +4 -4
package/lib/build/build.js
CHANGED
|
@@ -19,6 +19,7 @@ const {
|
|
|
19
19
|
const { ensureStyles } = require("./styles");
|
|
20
20
|
const { copyAssets } = require("./assets");
|
|
21
21
|
const { logLine } = require("./log");
|
|
22
|
+
const { verifyBuildOutput } = require("./verify");
|
|
22
23
|
|
|
23
24
|
// hold records between builds if skipping IIIF
|
|
24
25
|
let iiifRecordsCache = [];
|
|
@@ -57,11 +58,15 @@ async function build(options = {}) {
|
|
|
57
58
|
underscore: true,
|
|
58
59
|
});
|
|
59
60
|
let iiifRecords = [];
|
|
61
|
+
const CONFIG = await iiif.loadConfig();
|
|
60
62
|
if (!skipIiif) {
|
|
61
|
-
const CONFIG = await iiif.loadConfig();
|
|
62
63
|
const results = await iiif.buildIiifCollectionPages(CONFIG);
|
|
63
64
|
iiifRecords = results?.iiifRecords;
|
|
64
65
|
}
|
|
66
|
+
// Ensure any configured featured manifests are cached (and thumbnails computed)
|
|
67
|
+
// so SSR components like <Hero /> can resolve items even if they are not part of
|
|
68
|
+
// the traversed collection or when IIIF build is skipped during incremental rebuilds.
|
|
69
|
+
try { await iiif.ensureFeaturedInCache(CONFIG); } catch (_) {}
|
|
65
70
|
|
|
66
71
|
/**
|
|
67
72
|
* Build contextual MDX content from the content directory.
|
|
@@ -72,6 +77,7 @@ async function build(options = {}) {
|
|
|
72
77
|
bright: true,
|
|
73
78
|
underscore: true,
|
|
74
79
|
});
|
|
80
|
+
// FeaturedHero now reads directly from the local IIIF cache; no API file needed
|
|
75
81
|
pageRecords = await searchBuild.collectMdxPageRecords();
|
|
76
82
|
await pages.buildContentTree(CONTENT_DIR, pageRecords);
|
|
77
83
|
logLine("✓ MDX pages built", "green");
|
|
@@ -94,6 +100,8 @@ async function build(options = {}) {
|
|
|
94
100
|
logLine(" " + String(e), "red");
|
|
95
101
|
}
|
|
96
102
|
|
|
103
|
+
// No-op: Featured API file no longer written (SSR reads from cache directly)
|
|
104
|
+
|
|
97
105
|
/**
|
|
98
106
|
* Prepare client runtimes (e.g. search) by bundling with esbuild.
|
|
99
107
|
* This is done early so that MDX content can reference runtime assets if needed.
|
|
@@ -116,6 +124,17 @@ async function build(options = {}) {
|
|
|
116
124
|
underscore: true,
|
|
117
125
|
});
|
|
118
126
|
await copyAssets();
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Final verification (checklist)
|
|
130
|
+
*/
|
|
131
|
+
try {
|
|
132
|
+
verifyBuildOutput({ outDir: OUT_DIR });
|
|
133
|
+
} catch (e) {
|
|
134
|
+
logLine("✗ Build verification failed", "red", { bright: true });
|
|
135
|
+
logLine(String(e && e.message ? e.message : e), "red");
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
119
138
|
}
|
|
120
139
|
|
|
121
140
|
module.exports = { build };
|
package/lib/build/iiif.js
CHANGED
|
@@ -365,6 +365,51 @@ async function saveCachedManifest(manifest, id, parentId) {
|
|
|
365
365
|
} catch (_) {}
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
+
// Ensure any configured featured manifests are present in the local cache
|
|
369
|
+
// (and have thumbnails computed) so SSR components like <Hero /> can read them.
|
|
370
|
+
async function ensureFeaturedInCache(cfg) {
|
|
371
|
+
try {
|
|
372
|
+
const CONFIG = cfg || (await loadConfig());
|
|
373
|
+
const featured = Array.isArray(CONFIG && CONFIG.featured) ? CONFIG.featured : [];
|
|
374
|
+
if (!featured.length) return;
|
|
375
|
+
const { getThumbnail } = require("../iiif/thumbnail");
|
|
376
|
+
// Thumbnail sizing config
|
|
377
|
+
const thumbSize = CONFIG && CONFIG.iiif && CONFIG.iiif.thumbnails && typeof CONFIG.iiif.thumbnails.preferredSize === 'number' ? CONFIG.iiif.thumbnails.preferredSize : 400;
|
|
378
|
+
const unsafeThumbs = !!(CONFIG && CONFIG.iiif && CONFIG.iiif.thumbnails && (CONFIG.iiif.thumbnails.unsafe === true || CONFIG.iiif.thumbnails.unsafe === 'true'));
|
|
379
|
+
for (const rawId of featured) {
|
|
380
|
+
const id = normalizeIiifId(String(rawId || ''));
|
|
381
|
+
if (!id) continue;
|
|
382
|
+
let manifest = await loadCachedManifestById(id);
|
|
383
|
+
if (!manifest) {
|
|
384
|
+
const m = await readJsonFromUri(id).catch(() => null);
|
|
385
|
+
if (!m) continue;
|
|
386
|
+
const v3 = await normalizeToV3(m);
|
|
387
|
+
if (!v3 || !v3.id) continue;
|
|
388
|
+
await saveCachedManifest(v3, id, '');
|
|
389
|
+
manifest = v3;
|
|
390
|
+
}
|
|
391
|
+
// Ensure thumbnail fields exist in index for this manifest (if computable)
|
|
392
|
+
try {
|
|
393
|
+
const t = await getThumbnail(manifest, thumbSize, unsafeThumbs);
|
|
394
|
+
if (t && t.url) {
|
|
395
|
+
const idx = await loadManifestIndex();
|
|
396
|
+
if (Array.isArray(idx.byId)) {
|
|
397
|
+
const entry = idx.byId.find((e) => e && e.type === 'Manifest' && normalizeIiifId(String(e.id)) === normalizeIiifId(String(manifest.id)));
|
|
398
|
+
if (entry) {
|
|
399
|
+
entry.thumbnail = String(t.url);
|
|
400
|
+
if (typeof t.width === 'number') entry.thumbnailWidth = t.width;
|
|
401
|
+
if (typeof t.height === 'number') entry.thumbnailHeight = t.height;
|
|
402
|
+
await saveManifestIndex(idx);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
} catch (_) {}
|
|
407
|
+
}
|
|
408
|
+
} catch (_) {
|
|
409
|
+
// ignore failures; fallback SSR will still render a minimal hero without content
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
368
413
|
async function flushManifestCache() {
|
|
369
414
|
try {
|
|
370
415
|
await fsp.rm(IIIF_CACHE_MANIFESTS_DIR, { recursive: true, force: true });
|
|
@@ -762,7 +807,11 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
762
807
|
href = withBase(href);
|
|
763
808
|
return React.createElement("a", { href, ...rest }, props.children);
|
|
764
809
|
};
|
|
810
|
+
// Map exported UI components into MDX, with sensible aliases/fallbacks
|
|
765
811
|
const compMap = { ...components, a: Anchor };
|
|
812
|
+
if (!compMap.SearchPanel && compMap.CommandPalette) {
|
|
813
|
+
compMap.SearchPanel = compMap.CommandPalette;
|
|
814
|
+
}
|
|
766
815
|
if (!components.HelloWorld) {
|
|
767
816
|
components.HelloWorld = components.Fallback
|
|
768
817
|
? (props) =>
|
|
@@ -811,6 +860,7 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
811
860
|
.join("/");
|
|
812
861
|
const needsHydrateViewer = body.includes("data-canopy-viewer");
|
|
813
862
|
const needsRelated = body.includes("data-canopy-related-items");
|
|
863
|
+
const needsHero = body.includes("data-canopy-hero");
|
|
814
864
|
const needsCommand = body.includes("data-canopy-command");
|
|
815
865
|
const needsHydrate =
|
|
816
866
|
body.includes("data-canopy-hydrate") ||
|
|
@@ -836,6 +886,15 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
836
886
|
.split(path.sep)
|
|
837
887
|
.join("/")
|
|
838
888
|
: null;
|
|
889
|
+
const heroRel = needsHero
|
|
890
|
+
? path
|
|
891
|
+
.relative(
|
|
892
|
+
path.dirname(outPath),
|
|
893
|
+
path.join(OUT_DIR, "scripts", "canopy-hero.js")
|
|
894
|
+
)
|
|
895
|
+
.split(path.sep)
|
|
896
|
+
.join("/")
|
|
897
|
+
: null;
|
|
839
898
|
const relatedRel = needsRelated
|
|
840
899
|
? path
|
|
841
900
|
.relative(
|
|
@@ -856,11 +915,12 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
856
915
|
: null;
|
|
857
916
|
|
|
858
917
|
let jsRel = null;
|
|
859
|
-
if (
|
|
918
|
+
if (needsHero && heroRel) jsRel = heroRel;
|
|
919
|
+
else if (needsRelated && sliderRel) jsRel = sliderRel;
|
|
860
920
|
else if (viewerRel) jsRel = viewerRel;
|
|
861
921
|
|
|
862
922
|
let headExtra = head;
|
|
863
|
-
const needsReact = !!(needsHydrateViewer || needsRelated);
|
|
923
|
+
const needsReact = !!(needsHydrateViewer || needsRelated || needsHero);
|
|
864
924
|
let vendorTag = "";
|
|
865
925
|
if (needsReact) {
|
|
866
926
|
try {
|
|
@@ -881,6 +941,8 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
881
941
|
} catch (_) {}
|
|
882
942
|
}
|
|
883
943
|
const extraScripts = [];
|
|
944
|
+
if (heroRel && jsRel !== heroRel)
|
|
945
|
+
extraScripts.push(`<script defer src="${heroRel}"></script>`);
|
|
884
946
|
if (relatedRel && jsRel !== relatedRel)
|
|
885
947
|
extraScripts.push(`<script defer src="${relatedRel}"></script>`);
|
|
886
948
|
if (viewerRel && jsRel !== viewerRel)
|
|
@@ -979,6 +1041,10 @@ module.exports = {
|
|
|
979
1041
|
loadConfig,
|
|
980
1042
|
loadManifestIndex,
|
|
981
1043
|
saveManifestIndex,
|
|
1044
|
+
// Expose helpers used by build for cache warming
|
|
1045
|
+
loadCachedManifestById,
|
|
1046
|
+
saveCachedManifest,
|
|
1047
|
+
ensureFeaturedInCache,
|
|
982
1048
|
};
|
|
983
1049
|
|
|
984
1050
|
// Debug: list collections cache after traversal
|
package/lib/build/mdx.js
CHANGED
|
@@ -44,15 +44,131 @@ async function getMdxProvider() {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
// Lazily load UI components from the workspace package and cache them.
|
|
47
|
+
// Re-import when the built UI server bundle changes on disk.
|
|
47
48
|
let UI_COMPONENTS = null;
|
|
49
|
+
let UI_COMPONENTS_PATH = '';
|
|
50
|
+
let UI_COMPONENTS_MTIME = 0;
|
|
51
|
+
const DEBUG = process.env.CANOPY_DEBUG === '1' || process.env.CANOPY_DEBUG === 'true';
|
|
48
52
|
async function loadUiComponents() {
|
|
49
|
-
|
|
53
|
+
// Do not rely on a cached mapping; re-import each time to avoid transient races.
|
|
50
54
|
try {
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
// Prefer the workspace dist path during dev to avoid export-map resolution issues
|
|
56
|
+
let resolved = null;
|
|
57
|
+
try {
|
|
58
|
+
const wsDist = path.join(process.cwd(), 'packages', 'app', 'ui', 'dist', 'server.mjs');
|
|
59
|
+
if (fs.existsSync(wsDist)) resolved = wsDist;
|
|
60
|
+
} catch (_) {}
|
|
61
|
+
// Prefer explicit dist path to avoid export-map issues
|
|
62
|
+
if (!resolved) {
|
|
63
|
+
try { resolved = require.resolve("@canopy-iiif/app/ui/dist/server.mjs"); } catch (_) {
|
|
64
|
+
try { resolved = require.resolve("@canopy-iiif/app/ui/server"); } catch (_) { resolved = null; }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Determine current mtime for change detection
|
|
68
|
+
let currentPath = resolved || '';
|
|
69
|
+
let currentMtime = 0;
|
|
70
|
+
if (currentPath) {
|
|
71
|
+
try { const st = fs.statSync(currentPath); currentMtime = Math.floor(st.mtimeMs || 0); } catch (_) { currentMtime = 0; }
|
|
72
|
+
}
|
|
73
|
+
// If we have a cached module and the path/mtime have not changed, return cached
|
|
74
|
+
if (UI_COMPONENTS && UI_COMPONENTS_PATH === currentPath && UI_COMPONENTS_MTIME === currentMtime) {
|
|
75
|
+
if (DEBUG) {
|
|
76
|
+
try { console.log('[canopy][mdx] UI components cache hit:', { path: UI_COMPONENTS_PATH, mtime: UI_COMPONENTS_MTIME }); } catch(_){}
|
|
77
|
+
}
|
|
78
|
+
return UI_COMPONENTS;
|
|
79
|
+
}
|
|
80
|
+
let mod = null;
|
|
81
|
+
let importErr = null;
|
|
82
|
+
if (resolved) {
|
|
83
|
+
const { pathToFileURL } = require("url");
|
|
84
|
+
const fileUrl = pathToFileURL(resolved).href;
|
|
85
|
+
const attempts = 5;
|
|
86
|
+
for (let i = 0; i < attempts && !mod; i++) {
|
|
87
|
+
const bustVal = currentMtime ? `${currentMtime}-${i}` : `${Date.now()}-${i}`;
|
|
88
|
+
try {
|
|
89
|
+
mod = await import(fileUrl + `?v=${bustVal}`);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
importErr = e;
|
|
92
|
+
if (DEBUG) {
|
|
93
|
+
try { console.warn('[canopy][mdx] ESM import failed for', resolved, '(attempt', i + 1, 'of', attempts + ')\n', e && (e.stack || e.message || String(e))); } catch(_){}
|
|
94
|
+
}
|
|
95
|
+
// Small delay to avoid watch-write race
|
|
96
|
+
await new Promise((r) => setTimeout(r, 60));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (DEBUG) {
|
|
100
|
+
try { console.log('[canopy][mdx] UI components resolved', { path: resolved, mtime: currentMtime, loaded: !!mod }); } catch(_){}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (!mod) {
|
|
104
|
+
// Try package subpath as a secondary resolution path (not a UI component fallback)
|
|
105
|
+
try {
|
|
106
|
+
mod = await import('@canopy-iiif/app/ui/server');
|
|
107
|
+
} catch (e2) {
|
|
108
|
+
const msgA = importErr && (importErr.stack || importErr.message);
|
|
109
|
+
const msgB = e2 && (e2.stack || e2.message);
|
|
110
|
+
throw new Error('Failed to load @canopy-iiif/app/ui/server. Ensure the UI package is built.\nPath import error: ' + (msgA||'') + '\nExport-map import error: ' + (msgB||''));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
let comp = (mod && typeof mod === 'object') ? mod : {};
|
|
114
|
+
// Hard-require core exports; do not inject fallbacks
|
|
115
|
+
const required = ['SearchPanel', 'CommandPalette', 'SearchResults', 'SearchSummary', 'SearchTabs', 'Viewer', 'Slider', 'RelatedItems'];
|
|
116
|
+
const missing = required.filter((k) => !comp || !comp[k]);
|
|
117
|
+
if (missing.length) {
|
|
118
|
+
throw new Error('[canopy][mdx] Missing UI exports: ' + missing.join(', '));
|
|
119
|
+
}
|
|
120
|
+
if (DEBUG) {
|
|
121
|
+
try { console.log('[canopy][mdx] UI component sources', {
|
|
122
|
+
path: currentPath,
|
|
123
|
+
mtime: currentMtime,
|
|
124
|
+
hasServerExport: !!mod,
|
|
125
|
+
hasWorkspace: typeof comp !== 'undefined',
|
|
126
|
+
CommandPalette: !!comp.CommandPalette,
|
|
127
|
+
Viewer: !!comp.Viewer,
|
|
128
|
+
Slider: !!comp.Slider,
|
|
129
|
+
}); } catch(_){}
|
|
130
|
+
}
|
|
131
|
+
// No stub injection beyond this point; UI package must supply these.
|
|
132
|
+
// Ensure a minimal SSR Hero exists
|
|
133
|
+
if (!comp.Hero) {
|
|
134
|
+
comp.Hero = function SimpleHero({ height = 360, item, className = '', style = {}, ...rest }){
|
|
135
|
+
const h = typeof height === 'number' ? `${height}px` : String(height || '').trim() || '360px';
|
|
136
|
+
const base = { position: 'relative', width: '100%', height: h, overflow: 'hidden', backgroundColor: 'var(--color-gray-muted)', ...style };
|
|
137
|
+
const title = (item && item.title) || '';
|
|
138
|
+
const href = (item && item.href) || '#';
|
|
139
|
+
const thumbnail = (item && item.thumbnail) || '';
|
|
140
|
+
return React.createElement('div', { className: ['canopy-hero', className].filter(Boolean).join(' '), style: base, ...rest },
|
|
141
|
+
thumbnail ? React.createElement('img', { src: thumbnail, alt: '', 'aria-hidden': 'true', style: { position:'absolute', inset:0, width:'100%', height:'100%', objectFit:'cover', objectPosition:'center', filter:'none' } }) : null,
|
|
142
|
+
React.createElement('div', { className:'canopy-hero-overlay', style: { position:'absolute', left:0, right:0, bottom:0, padding:'1rem', background:'linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.35) 55%, rgba(0,0,0,0.65) 100%)', color:'white' } },
|
|
143
|
+
React.createElement('h3', { style: { margin:0, fontSize:'1.5rem', fontWeight:600, lineHeight:1.2, textShadow:'0 1px 3px rgba(0,0,0,0.6)' } },
|
|
144
|
+
React.createElement('a', { href, style:{ color:'inherit', textDecoration:'none' }, className:'canopy-hero-link' }, title)
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
// Provide a minimal SSR FeaturedHero fallback if missing
|
|
151
|
+
if (!comp.FeaturedHero) {
|
|
152
|
+
try {
|
|
153
|
+
const helpers = require('../components/featured');
|
|
154
|
+
comp.FeaturedHero = function FeaturedHero(props) {
|
|
155
|
+
try {
|
|
156
|
+
const list = helpers && helpers.readFeaturedFromCacheSync ? helpers.readFeaturedFromCacheSync() : [];
|
|
157
|
+
if (!Array.isArray(list) || list.length === 0) return null;
|
|
158
|
+
const index = (props && typeof props.index === 'number') ? Math.max(0, Math.min(list.length - 1, Math.floor(props.index))) : null;
|
|
159
|
+
const pick = (index != null) ? index : ((props && (props.random === true || props.random === 'true')) ? Math.floor(Math.random() * list.length) : 0);
|
|
160
|
+
const item = list[pick] || list[0];
|
|
161
|
+
return React.createElement(comp.Hero, { ...props, item });
|
|
162
|
+
} catch (_) { return null; }
|
|
163
|
+
};
|
|
164
|
+
} catch (_) { /* ignore */ }
|
|
165
|
+
}
|
|
166
|
+
UI_COMPONENTS = comp;
|
|
167
|
+
UI_COMPONENTS_PATH = currentPath;
|
|
168
|
+
UI_COMPONENTS_MTIME = currentMtime;
|
|
169
|
+
} catch (e) {
|
|
170
|
+
const msg = e && (e.stack || e.message || String(e)) || 'unknown error';
|
|
171
|
+
throw new Error('[canopy][mdx] Failed to load UI components (no fallbacks): ' + msg);
|
|
56
172
|
}
|
|
57
173
|
return UI_COMPONENTS;
|
|
58
174
|
}
|
|
@@ -672,6 +788,79 @@ async function ensureReactGlobals() {
|
|
|
672
788
|
});
|
|
673
789
|
}
|
|
674
790
|
|
|
791
|
+
// Bundle a small runtime to hydrate <Hero /> placeholders from featured items
|
|
792
|
+
async function ensureHeroRuntime() {
|
|
793
|
+
let esbuild = null;
|
|
794
|
+
try { esbuild = require("../../ui/node_modules/esbuild"); } catch (_) { try { esbuild = require("esbuild"); } catch (_) {} }
|
|
795
|
+
if (!esbuild) return;
|
|
796
|
+
const { path } = require("../common");
|
|
797
|
+
ensureDirSync(OUT_DIR);
|
|
798
|
+
const scriptsDir = path.join(OUT_DIR, 'scripts');
|
|
799
|
+
ensureDirSync(scriptsDir);
|
|
800
|
+
const outFile = path.join(scriptsDir, 'canopy-hero.js');
|
|
801
|
+
const entry = `
|
|
802
|
+
import { Hero } from '@canopy-iiif/app/ui';
|
|
803
|
+
function ready(fn){ if(document.readyState==='loading') document.addEventListener('DOMContentLoaded', fn, { once: true }); else fn(); }
|
|
804
|
+
function parseProps(el){ try{ const s=el.querySelector('script[type="application/json"]'); if(s) return JSON.parse(s.textContent||'{}'); }catch(_){ } return {}; }
|
|
805
|
+
function rootBase(){ try { var bp=(window&&window.CANOPY_BASE_PATH)?String(window.CANOPY_BASE_PATH):''; return bp && bp.endsWith('/') ? bp.slice(0,-1) : bp; } catch(_){ return ''; } }
|
|
806
|
+
async function getApiVersion(){ try{ const u=rootBase() + '/api/index.json'; const res=await fetch(u).catch(()=>null); const j=res&&res.ok?await res.json().catch(()=>null):null; return (j && j.version) || ''; }catch(_){ return ''; } }
|
|
807
|
+
async function loadFeatured(){ try { const v = await getApiVersion(); const q = v ? ('?v='+encodeURIComponent(v)) : ''; const res = await fetch(rootBase() + '/api/featured.json' + q).catch(()=>null); const j = res && res.ok ? await res.json().catch(()=>[]) : []; return Array.isArray(j) ? j : (j && j.items) || []; } catch(_){ return []; } }
|
|
808
|
+
function mount(el, rec){ try{ const React=(window&&window.React)||null; const RDC=(window&&window.ReactDOMClient)||null; const createRoot = RDC && RDC.createRoot; if(!React||!createRoot) return; const props = parseProps(el) || {}; const height = props.height || 360; const node = React.createElement(Hero, { height, item: rec }); const root=createRoot(el); root.render(node); } catch(_){} }
|
|
809
|
+
ready(async function(){ const hosts = Array.from(document.querySelectorAll('[data-canopy-hero]')); if(!hosts.length) return; const featured = await loadFeatured(); if(!featured.length) return; hosts.forEach((el, i) => { try { const p = parseProps(el) || {}; let idx = 0; if (p && typeof p.index === 'number') idx = Math.max(0, Math.min(featured.length-1, Math.floor(p.index))); else if (p && (p.random===true || p.random==='true')) idx = Math.floor(Math.random() * featured.length); const rec = featured[idx] || featured[0]; if (rec) mount(el, rec); } catch(_){} }); });
|
|
810
|
+
`;
|
|
811
|
+
const reactShim = `
|
|
812
|
+
const React = (typeof window !== 'undefined' && window.React) || {};
|
|
813
|
+
export default React;
|
|
814
|
+
export const Children = React.Children;
|
|
815
|
+
export const Component = React.Component;
|
|
816
|
+
export const Fragment = React.Fragment;
|
|
817
|
+
export const createElement = React.createElement;
|
|
818
|
+
export const cloneElement = React.cloneElement;
|
|
819
|
+
export const createContext = React.createContext;
|
|
820
|
+
export const forwardRef = React.forwardRef;
|
|
821
|
+
export const memo = React.memo;
|
|
822
|
+
export const startTransition = React.startTransition;
|
|
823
|
+
export const isValidElement = React.isValidElement;
|
|
824
|
+
export const useEffect = React.useEffect;
|
|
825
|
+
export const useLayoutEffect = React.useLayoutEffect;
|
|
826
|
+
export const useMemo = React.useMemo;
|
|
827
|
+
export const useState = React.useState;
|
|
828
|
+
export const useRef = React.useRef;
|
|
829
|
+
export const useCallback = React.useCallback;
|
|
830
|
+
export const useContext = React.useContext;
|
|
831
|
+
export const useReducer = React.useReducer;
|
|
832
|
+
export const useId = React.useId;
|
|
833
|
+
`;
|
|
834
|
+
const rdomClientShim = `
|
|
835
|
+
const RDC = (typeof window !== 'undefined' && window.ReactDOMClient) || {};
|
|
836
|
+
export default RDC;
|
|
837
|
+
export const createRoot = RDC.createRoot;
|
|
838
|
+
export const hydrateRoot = RDC.hydrateRoot;
|
|
839
|
+
`;
|
|
840
|
+
const plugin = {
|
|
841
|
+
name: 'canopy-react-shims-hero',
|
|
842
|
+
setup(build) {
|
|
843
|
+
const ns = 'canopy-shim';
|
|
844
|
+
build.onResolve({ filter: /^react$/ }, () => ({ path: 'react', namespace: ns }));
|
|
845
|
+
build.onResolve({ filter: /^react-dom\/client$/ }, () => ({ path: 'react-dom-client', namespace: ns }));
|
|
846
|
+
build.onLoad({ filter: /^react$/, namespace: ns }, () => ({ contents: reactShim, loader: 'js' }));
|
|
847
|
+
build.onLoad({ filter: /^react-dom-client$/, namespace: ns }, () => ({ contents: rdomClientShim, loader: 'js' }));
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
await esbuild.build({
|
|
851
|
+
stdin: { contents: entry, resolveDir: process.cwd(), sourcefile: 'canopy-hero-entry.js', loader: 'js' },
|
|
852
|
+
outfile: outFile,
|
|
853
|
+
platform: 'browser',
|
|
854
|
+
format: 'iife',
|
|
855
|
+
bundle: true,
|
|
856
|
+
sourcemap: false,
|
|
857
|
+
target: ['es2018'],
|
|
858
|
+
logLevel: 'silent',
|
|
859
|
+
minify: true,
|
|
860
|
+
plugins: [plugin],
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
|
|
675
864
|
module.exports = {
|
|
676
865
|
extractTitle,
|
|
677
866
|
isReservedFile,
|
|
@@ -682,6 +871,7 @@ module.exports = {
|
|
|
682
871
|
loadAppWrapper,
|
|
683
872
|
ensureClientRuntime,
|
|
684
873
|
ensureSliderRuntime,
|
|
874
|
+
ensureHeroRuntime,
|
|
685
875
|
ensureFacetsRuntime,
|
|
686
876
|
ensureReactGlobals,
|
|
687
877
|
resetMdxCaches: function () {
|
|
@@ -689,5 +879,6 @@ module.exports = {
|
|
|
689
879
|
DIR_LAYOUTS.clear();
|
|
690
880
|
} catch (_) {}
|
|
691
881
|
APP_WRAPPER = null;
|
|
882
|
+
UI_COMPONENTS = null;
|
|
692
883
|
},
|
|
693
884
|
};
|
package/lib/build/pages.js
CHANGED
|
@@ -62,9 +62,13 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}) {
|
|
|
62
62
|
const facetsRel = needsFacets
|
|
63
63
|
? path.relative(path.dirname(outPath), path.join(OUT_DIR, 'scripts', 'canopy-related-items.js')).split(path.sep).join('/')
|
|
64
64
|
: null;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
let commandRel = null;
|
|
66
|
+
if (needsCommand) {
|
|
67
|
+
const cmdAbs = path.join(OUT_DIR, 'scripts', 'canopy-command.js');
|
|
68
|
+
let rel = path.relative(path.dirname(outPath), cmdAbs).split(path.sep).join('/');
|
|
69
|
+
try { const st = fs.statSync(cmdAbs); rel += `?v=${Math.floor(st.mtimeMs || Date.now())}`; } catch (_) {}
|
|
70
|
+
commandRel = rel;
|
|
71
|
+
}
|
|
68
72
|
let jsRel = null;
|
|
69
73
|
if (needsFacets && sliderRel) jsRel = sliderRel;
|
|
70
74
|
else if (viewerRel) jsRel = viewerRel;
|
package/lib/build/runtimes.js
CHANGED
|
@@ -6,6 +6,12 @@ async function prepareAllRuntimes() {
|
|
|
6
6
|
const mdx = require('./mdx');
|
|
7
7
|
try { await mdx.ensureClientRuntime(); } catch (_) {}
|
|
8
8
|
try { if (typeof mdx.ensureSliderRuntime === 'function') await mdx.ensureSliderRuntime(); } catch (_) {}
|
|
9
|
+
// Optional: Hero runtime is SSR-only by default; enable explicitly to avoid bundling Node deps in browser
|
|
10
|
+
try {
|
|
11
|
+
if (process.env.CANOPY_ENABLE_HERO_RUNTIME === '1' || process.env.CANOPY_ENABLE_HERO_RUNTIME === 'true') {
|
|
12
|
+
if (typeof mdx.ensureHeroRuntime === 'function') await mdx.ensureHeroRuntime();
|
|
13
|
+
}
|
|
14
|
+
} catch (_) {}
|
|
9
15
|
try { if (typeof mdx.ensureFacetsRuntime === 'function') await mdx.ensureFacetsRuntime(); } catch (_) {}
|
|
10
16
|
try { if (typeof mdx.ensureReactGlobals === 'function') await mdx.ensureReactGlobals(); } catch (_) {}
|
|
11
17
|
try { await ensureCommandFallback(); } catch (_) {}
|
|
@@ -24,15 +30,67 @@ async function ensureCommandFallback() {
|
|
|
24
30
|
function norm(s){ try{ return String(s||'').toLowerCase(); }catch(_){ return ''; } }
|
|
25
31
|
function withBase(href){ try{ var bp = (window && window.CANOPY_BASE_PATH) ? String(window.CANOPY_BASE_PATH) : ''; if(!bp) return href; if(/^https?:/i.test(href)) return href; var clean = href.replace(/^\\/+/, ''); return (bp.endsWith('/') ? bp.slice(0,-1) : bp) + '/' + clean; } catch(_){ return href; } }
|
|
26
32
|
function rootBase(){ try { var bp = (window && window.CANOPY_BASE_PATH) ? String(window.CANOPY_BASE_PATH) : ''; return bp && bp.endsWith('/') ? bp.slice(0,-1) : bp; } catch(_) { return ''; } }
|
|
27
|
-
function
|
|
33
|
+
function isOnSearchPage(){ try{ var base=rootBase(); var p=String(location.pathname||''); if(base && p.startsWith(base)) p=p.slice(base.length); if(p.endsWith('/')) p=p.slice(0,-1); return p==='/search'; }catch(_){ return false; } }
|
|
28
34
|
async function loadRecords(){ try{ var v=''; try{ var m = await fetch(rootBase() + '/api/index.json').then(function(r){return r&&r.ok?r.json():null;}).catch(function(){return null;}); v=(m&&m.version)||''; }catch(_){} var res = await fetch(rootBase() + '/api/search-index.json' + (v?('?v='+encodeURIComponent(v)):'')).catch(function(){return null;}); var j = res && res.ok ? await res.json().catch(function(){return[];}) : []; return Array.isArray(j) ? j : (j && j.records) || []; } catch(_){ return []; } }
|
|
29
|
-
ready(async function(){ var host=document.querySelector('[data-canopy-command]'); if(!host) return; var cfg=parseProps(host)||{}; var maxResults = Number(cfg.maxResults||8)||8; var groupOrder = Array.isArray(cfg.groupOrder)?cfg.groupOrder:['work','page']; var
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
var
|
|
35
|
+
ready(async function(){ var host=document.querySelector('[data-canopy-command]'); if(!host) return; var cfg=parseProps(host)||{}; var maxResults = Number(cfg.maxResults||8)||8; var groupOrder = Array.isArray(cfg.groupOrder)?cfg.groupOrder:['work','page']; var onSearchPage = isOnSearchPage();
|
|
36
|
+
var panel = (function(){ try{ return host.querySelector('[data-canopy-command-panel]') || null; }catch(_){ return null; } })();
|
|
37
|
+
if(!panel) return; // no SSR panel present; do nothing
|
|
38
|
+
try{ var rel=host.querySelector('.relative'); if(rel && !onSearchPage) rel.setAttribute('data-canopy-panel-auto','1'); }catch(_){}
|
|
39
|
+
if(onSearchPage){ panel.style.display='none'; }
|
|
40
|
+
var list=panel.querySelector('#cplist');
|
|
41
|
+
var input = (function(){ try{ return host.querySelector('[data-canopy-command-input]') || null; }catch(_){ return null; } })();
|
|
42
|
+
if(!input) return; // require SSR input; no dynamic creation
|
|
43
|
+
// Populate from ?q= URL param if present
|
|
44
|
+
try {
|
|
45
|
+
var sp = new URLSearchParams(location.search || '');
|
|
46
|
+
var qp = sp.get('q');
|
|
47
|
+
if (qp) input.value = qp;
|
|
48
|
+
} catch(_) {}
|
|
49
|
+
// Do not inject legacy trigger buttons or inputs
|
|
50
|
+
var records = await loadRecords(); function render(items){ list.innerHTML=''; if(!items.length){ panel.style.display='none'; return; } var groups=new Map(); items.forEach(function(r){ var t=String(r.type||'page'); if(!groups.has(t)) groups.set(t, []); groups.get(t).push(r); }); function gl(t){ if(t==='work') return 'Works'; if(t==='page') return 'Pages'; return t.charAt(0).toUpperCase()+t.slice(1);} var ordered=[].concat(groupOrder.filter(function(t){return groups.has(t);})).concat(Array.from(groups.keys()).filter(function(t){return groupOrder.indexOf(t)===-1;})); ordered.forEach(function(t){ var hdr=document.createElement('div'); hdr.textContent=gl(t); hdr.style.cssText='padding:6px 12px;font-weight:600;color:#374151'; list.appendChild(hdr); groups.get(t).forEach(function(r){ var it=document.createElement('div'); it.setAttribute('data-canopy-item',''); it.tabIndex=0; it.style.cssText='display:flex;align-items:center;gap:8px;padding:8px 12px;cursor:pointer;outline:none;'; var thumb=(String(r.type||'')==='work' && r.thumbnail)?r.thumbnail:''; if(thumb){ var img=document.createElement('img'); img.src=thumb; img.alt=''; img.style.cssText='width:40px;height:40px;object-fit:cover;border-radius:4px'; it.appendChild(img);} var span=document.createElement('span'); span.textContent=r.title||r.href; it.appendChild(span); it.onmouseenter=function(){ it.style.background='#f8fafc'; }; it.onmouseleave=function(){ it.style.background='transparent'; }; it.onfocus=function(){ it.style.background='#eef2ff'; try{ it.scrollIntoView({ block: 'nearest' }); }catch(_){} }; it.onblur=function(){ it.style.background='transparent'; }; it.onclick=function(){ try{ window.location.href = withBase(String(r.href||'')); }catch(_){} panel.style.display='none'; }; list.appendChild(it); }); }); }
|
|
51
|
+
function getItems(){ try{ return Array.prototype.slice.call(list.querySelectorAll('[data-canopy-item]')); }catch(_){ return []; } }
|
|
52
|
+
function filterAndShow(q){ try{ var qq=norm(q); if(!qq){ try{ panel.style.display='block'; list.innerHTML=''; }catch(_){} return; } var out=[]; for(var i=0;i<records.length;i++){ var r=records[i]; var title=String((r&&r.title)||''); if(!title) continue; if(norm(title).indexOf(qq)!==-1) out.push(r); if(out.length>=maxResults) break; } render(out); }catch(_){} }
|
|
53
|
+
input.addEventListener('input', function(){ if(onSearchPage){ try{ var ev = new CustomEvent('canopy:search:setQuery', { detail: { query: (input.value||'') } }); window.dispatchEvent(ev); }catch(_){ } return; } filterAndShow(input.value||''); });
|
|
54
|
+
// Keyboard navigation: ArrowDown/ArrowUp to move through items; Enter to select
|
|
55
|
+
input.addEventListener('keydown', function(e){
|
|
56
|
+
if(e.key==='ArrowDown'){ e.preventDefault(); try{ var items=getItems(); if(items.length){ panel.style.display='block'; items[0].focus(); } }catch(_){} }
|
|
57
|
+
else if(e.key==='ArrowUp'){ e.preventDefault(); try{ var items2=getItems(); if(items2.length){ panel.style.display='block'; items2[items2.length-1].focus(); } }catch(_){} }
|
|
58
|
+
});
|
|
59
|
+
list.addEventListener('keydown', function(e){
|
|
60
|
+
var cur = e.target && e.target.closest && e.target.closest('[data-canopy-item]');
|
|
61
|
+
if(!cur) return;
|
|
62
|
+
if(e.key==='ArrowDown'){
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
try{ var arr=getItems(); var i=arr.indexOf(cur); var nxt=arr[Math.min(arr.length-1, i+1)]||cur; nxt.focus(); }catch(_){}
|
|
65
|
+
} else if(e.key==='ArrowUp'){
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
try{ var arr2=getItems(); var i2=arr2.indexOf(cur); if(i2<=0){ input && input.focus && input.focus(); } else { var prv=arr2[i2-1]; (prv||cur).focus(); } }catch(_){}
|
|
68
|
+
} else if(e.key==='Enter'){
|
|
69
|
+
e.preventDefault(); try{ cur.click(); }catch(_){}
|
|
70
|
+
} else if(e.key==='Escape'){
|
|
71
|
+
panel.style.display='none'; try{ input && input.focus && input.focus(); }catch(_){}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
document.addEventListener('keydown', function(e){ if(e.key==='Escape'){ panel.style.display='none'; }});
|
|
75
|
+
document.addEventListener('mousedown', function(e){ try{ if(!panel.contains(e.target) && !host.contains(e.target)){ panel.style.display='none'; } }catch(_){} });
|
|
76
|
+
// Hotkey support (e.g., mod+k)
|
|
77
|
+
document.addEventListener('keydown', function(e){
|
|
78
|
+
try {
|
|
79
|
+
var want = String((cfg && cfg.hotkey) || '').toLowerCase();
|
|
80
|
+
if (!want) return;
|
|
81
|
+
var isMod = e.metaKey || e.ctrlKey;
|
|
82
|
+
if ((want === 'mod+k' || want === 'cmd+k' || want === 'ctrl+k') && isMod && (e.key === 'k' || e.key === 'K')) {
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
if(onSearchPage){ try{ var ev2 = new CustomEvent('canopy:search:setQuery', { detail: { hotkey: true } }); window.dispatchEvent(ev2); }catch(_){ } return; }
|
|
85
|
+
panel.style.display='block';
|
|
86
|
+
(input && input.focus && input.focus());
|
|
87
|
+
filterAndShow(input && input.value || '');
|
|
88
|
+
}
|
|
89
|
+
} catch(_) { }
|
|
90
|
+
});
|
|
91
|
+
function openPanel(){ if(onSearchPage){ try{ var ev3 = new CustomEvent('canopy:search:setQuery', { detail: {} }); window.dispatchEvent(ev3); }catch(_){ } return; } panel.style.display='block'; (input && input.focus && input.focus()); filterAndShow(input && input.value || ''); }
|
|
92
|
+
host.addEventListener('click', function(e){ var trg=e.target && e.target.closest && e.target.closest('[data-canopy-command-trigger]'); if(trg){ e.preventDefault(); openPanel(); }});
|
|
93
|
+
try{ var teaser2 = host.querySelector('[data-canopy-command-input]'); if(teaser2){ teaser2.addEventListener('focus', function(){ openPanel(); }); } }catch(_){}
|
|
36
94
|
});
|
|
37
95
|
})();
|
|
38
96
|
`;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const { fs, path, OUT_DIR } = require('../common');
|
|
2
|
+
const { logLine } = require('./log');
|
|
3
|
+
|
|
4
|
+
function readFileSafe(p) {
|
|
5
|
+
try { return fs.readFileSync(p, 'utf8'); } catch (_) { return ''; }
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function hasHtmlFiles(dir) {
|
|
9
|
+
let count = 0;
|
|
10
|
+
if (!fs.existsSync(dir)) return 0;
|
|
11
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
12
|
+
for (const e of entries) {
|
|
13
|
+
const p = path.join(dir, e.name);
|
|
14
|
+
if (e.isDirectory()) count += hasHtmlFiles(p);
|
|
15
|
+
else if (e.isFile() && p.toLowerCase().endsWith('.html')) count++;
|
|
16
|
+
}
|
|
17
|
+
return count;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function verifyHomepageElements(outDir) {
|
|
21
|
+
const idx = path.join(outDir, 'index.html');
|
|
22
|
+
const html = readFileSafe(idx);
|
|
23
|
+
const okHero = /class=\"[^\"]*canopy-hero/.test(html) || /<div[^>]+canopy-hero/.test(html);
|
|
24
|
+
const okCommand = /data-canopy-command=/.test(html);
|
|
25
|
+
const okCommandTrigger = /data-canopy-command-trigger/.test(html);
|
|
26
|
+
const okCommandScriptRef = /<script[^>]+canopy-command\.js/.test(html);
|
|
27
|
+
return { okHero, okCommand, okCommandTrigger, okCommandScriptRef, htmlPath: idx };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function verifyBuildOutput(options = {}) {
|
|
31
|
+
const outDir = path.resolve(options.outDir || OUT_DIR);
|
|
32
|
+
logLine("\nVerify build output", "magenta", { bright: true, underscore: true });
|
|
33
|
+
const total = hasHtmlFiles(outDir);
|
|
34
|
+
const okAny = total > 0;
|
|
35
|
+
const indexPath = path.join(outDir, 'index.html');
|
|
36
|
+
const hasIndex = fs.existsSync(indexPath) && fs.statSync(indexPath).size > 0;
|
|
37
|
+
const { okHero, okCommand, okCommandTrigger, okCommandScriptRef } = verifyHomepageElements(outDir);
|
|
38
|
+
|
|
39
|
+
const ck = (label, ok, extra) => {
|
|
40
|
+
const status = ok ? '✓' : '✗';
|
|
41
|
+
logLine(`${status} ${label}${extra ? ` ${extra}` : ''}`, ok ? 'green' : 'red');
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
ck('HTML pages exist', okAny, okAny ? `(${total})` : '');
|
|
45
|
+
ck('homepage exists', hasIndex, hasIndex ? `(${indexPath})` : '');
|
|
46
|
+
ck('homepage: Hero present', okHero);
|
|
47
|
+
ck('homepage: Command present', okCommand);
|
|
48
|
+
ck('homepage: Command trigger present', okCommandTrigger);
|
|
49
|
+
ck('homepage: Command script referenced', okCommandScriptRef);
|
|
50
|
+
|
|
51
|
+
// Do not fail build on missing SSR trigger; the client runtime injects a default.
|
|
52
|
+
const ok = okAny && hasIndex && okHero && okCommand && okCommandScriptRef;
|
|
53
|
+
if (!ok) {
|
|
54
|
+
const err = new Error('Build verification failed');
|
|
55
|
+
err.outDir = outDir;
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { verifyBuildOutput };
|