@canopy-iiif/app 0.7.14 → 0.7.17
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/AGENTS.md +66 -0
- package/lib/build/dev.js +252 -355
- package/lib/build/iiif.js +19 -50
- package/lib/build/mdx.js +42 -76
- package/lib/build/pages.js +1 -5
- package/lib/build/runtimes.js +40 -103
- package/lib/build/search.js +2 -5
- package/lib/build/styles.js +9 -15
- package/lib/common.js +16 -8
- package/lib/head.js +21 -0
- package/lib/index.js +5 -1
- package/lib/search/command-runtime.js +370 -0
- package/lib/search/search-app.jsx +2 -2
- package/lib/search/search.js +12 -191
- package/package.json +10 -3
- package/ui/dist/index.mjs +108 -83
- package/ui/dist/index.mjs.map +4 -4
- package/ui/dist/server.mjs +108 -70
- package/ui/dist/server.mjs.map +4 -4
- package/ui/styles/base/_common.scss +8 -0
- package/ui/styles/base/index.scss +1 -0
- package/ui/styles/components/_command.scss +84 -1
- package/ui/styles/components/_header.scss +0 -0
- package/ui/styles/components/_hero.scss +22 -0
- package/ui/styles/components/index.scss +2 -3
- package/ui/styles/index.css +106 -1
- package/ui/styles/index.scss +3 -2
- package/ui/tailwind-canopy-iiif-plugin.js +7 -12
- package/ui/tailwind-canopy-iiif-preset.js +15 -0
package/lib/build/iiif.js
CHANGED
|
@@ -22,7 +22,7 @@ const IIIF_CACHE_COLLECTIONS_DIR = path.join(IIIF_CACHE_DIR, "collections");
|
|
|
22
22
|
const IIIF_CACHE_COLLECTION = path.join(IIIF_CACHE_DIR, "collection.json");
|
|
23
23
|
// Primary global index location
|
|
24
24
|
const IIIF_CACHE_INDEX = path.join(IIIF_CACHE_DIR, "index.json");
|
|
25
|
-
//
|
|
25
|
+
// Additional legacy locations kept for backward compatibility (read + optional write)
|
|
26
26
|
const IIIF_CACHE_INDEX_LEGACY = path.join(
|
|
27
27
|
IIIF_CACHE_DIR,
|
|
28
28
|
"manifest-index.json"
|
|
@@ -150,7 +150,7 @@ async function loadManifestIndex() {
|
|
|
150
150
|
return { byId, collection: idx.collection || null };
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
|
-
//
|
|
153
|
+
// Legacy index location retained for backward compatibility
|
|
154
154
|
if (fs.existsSync(IIIF_CACHE_INDEX_LEGACY)) {
|
|
155
155
|
const idx = await readJson(IIIF_CACHE_INDEX_LEGACY);
|
|
156
156
|
if (idx && typeof idx === "object") {
|
|
@@ -167,7 +167,7 @@ async function loadManifestIndex() {
|
|
|
167
167
|
return { byId, collection: idx.collection || null };
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
|
-
//
|
|
170
|
+
// Legacy manifests index retained for backward compatibility
|
|
171
171
|
if (fs.existsSync(IIIF_CACHE_INDEX_MANIFESTS)) {
|
|
172
172
|
const idx = await readJson(IIIF_CACHE_INDEX_MANIFESTS);
|
|
173
173
|
if (idx && typeof idx === "object") {
|
|
@@ -406,8 +406,9 @@ async function ensureFeaturedInCache(cfg) {
|
|
|
406
406
|
}
|
|
407
407
|
} catch (_) {}
|
|
408
408
|
}
|
|
409
|
-
} catch (
|
|
410
|
-
|
|
409
|
+
} catch (err) {
|
|
410
|
+
const message = err && err.message ? err.message : err;
|
|
411
|
+
throw new Error(`[iiif] Failed to populate featured cache: ${message}`);
|
|
411
412
|
}
|
|
412
413
|
}
|
|
413
414
|
|
|
@@ -643,33 +644,20 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
643
644
|
1200;
|
|
644
645
|
|
|
645
646
|
// Compile the works layout component once per run
|
|
647
|
+
const worksLayoutPath = path.join(CONTENT_DIR, "works", "_layout.mdx");
|
|
648
|
+
if (!fs.existsSync(worksLayoutPath)) {
|
|
649
|
+
throw new Error(
|
|
650
|
+
"IIIF build requires content/works/_layout.mdx. Create the layout instead of relying on generated output."
|
|
651
|
+
);
|
|
652
|
+
}
|
|
646
653
|
let WorksLayoutComp = null;
|
|
647
654
|
try {
|
|
648
|
-
const worksLayoutPath = path.join(CONTENT_DIR, "works", "_layout.mdx");
|
|
649
655
|
WorksLayoutComp = await mdx.compileMdxToComponent(worksLayoutPath);
|
|
650
|
-
} catch (
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
"div",
|
|
656
|
-
{ className: "content" },
|
|
657
|
-
React.createElement("h1", null, title || "Untitled"),
|
|
658
|
-
// Render viewer placeholder for hydration
|
|
659
|
-
React.createElement(
|
|
660
|
-
"div",
|
|
661
|
-
{ "data-canopy-viewer": "1" },
|
|
662
|
-
React.createElement("script", {
|
|
663
|
-
type: "application/json",
|
|
664
|
-
dangerouslySetInnerHTML: {
|
|
665
|
-
__html: JSON.stringify({
|
|
666
|
-
iiifContent: manifest && (manifest.id || ""),
|
|
667
|
-
}),
|
|
668
|
-
},
|
|
669
|
-
})
|
|
670
|
-
)
|
|
671
|
-
);
|
|
672
|
-
};
|
|
656
|
+
} catch (err) {
|
|
657
|
+
const message = err && err.message ? err.message : err;
|
|
658
|
+
throw new Error(
|
|
659
|
+
`Failed to compile content/works/_layout.mdx: ${message}`
|
|
660
|
+
);
|
|
673
661
|
}
|
|
674
662
|
|
|
675
663
|
for (let ci = 0; ci < chunks; ci++) {
|
|
@@ -808,20 +796,8 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
808
796
|
href = withBase(href);
|
|
809
797
|
return React.createElement("a", { href, ...rest }, props.children);
|
|
810
798
|
};
|
|
811
|
-
// Map exported UI components into MDX
|
|
799
|
+
// Map exported UI components into MDX and add anchor helper
|
|
812
800
|
const compMap = { ...components, a: Anchor };
|
|
813
|
-
if (!compMap.SearchPanel && compMap.CommandPalette) {
|
|
814
|
-
compMap.SearchPanel = compMap.CommandPalette;
|
|
815
|
-
}
|
|
816
|
-
if (!components.HelloWorld) {
|
|
817
|
-
components.HelloWorld = components.Fallback
|
|
818
|
-
? (props) =>
|
|
819
|
-
React.createElement(components.Fallback, {
|
|
820
|
-
name: "HelloWorld",
|
|
821
|
-
...props,
|
|
822
|
-
})
|
|
823
|
-
: () => null;
|
|
824
|
-
}
|
|
825
801
|
let MDXProvider = null;
|
|
826
802
|
try {
|
|
827
803
|
const mod = await import("@mdx-js/react");
|
|
@@ -852,13 +828,6 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
852
828
|
React.createElement(app.Head)
|
|
853
829
|
)
|
|
854
830
|
: "";
|
|
855
|
-
const cssRel = path
|
|
856
|
-
.relative(
|
|
857
|
-
path.dirname(outPath),
|
|
858
|
-
path.join(OUT_DIR, "styles", "styles.css")
|
|
859
|
-
)
|
|
860
|
-
.split(path.sep)
|
|
861
|
-
.join("/");
|
|
862
831
|
const needsHydrateViewer = body.includes("data-canopy-viewer");
|
|
863
832
|
const needsRelated = body.includes("data-canopy-related-items");
|
|
864
833
|
const needsHero = body.includes("data-canopy-hero");
|
|
@@ -966,7 +935,7 @@ async function buildIiifCollectionPages(CONFIG) {
|
|
|
966
935
|
let html = htmlShell({
|
|
967
936
|
title,
|
|
968
937
|
body: pageBody,
|
|
969
|
-
cssHref:
|
|
938
|
+
cssHref: null,
|
|
970
939
|
scriptHref: jsRel,
|
|
971
940
|
headExtra: vendorTag + headExtra,
|
|
972
941
|
});
|
package/lib/build/mdx.js
CHANGED
|
@@ -101,7 +101,7 @@ async function loadUiComponents() {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
if (!mod) {
|
|
104
|
-
// Try package subpath as a secondary resolution path
|
|
104
|
+
// Try package subpath as a secondary resolution path to avoid export-map issues
|
|
105
105
|
try {
|
|
106
106
|
mod = await import('@canopy-iiif/app/ui/server');
|
|
107
107
|
} catch (e2) {
|
|
@@ -112,7 +112,7 @@ async function loadUiComponents() {
|
|
|
112
112
|
}
|
|
113
113
|
let comp = (mod && typeof mod === 'object') ? mod : {};
|
|
114
114
|
// Hard-require core exports; do not inject fallbacks
|
|
115
|
-
const required = ['SearchPanel', 'CommandPalette', 'SearchResults', 'SearchSummary', 'SearchTabs', 'Viewer', 'Slider', 'RelatedItems'];
|
|
115
|
+
const required = ['SearchPanel', 'CommandPalette', 'SearchResults', 'SearchSummary', 'SearchTabs', 'Viewer', 'Slider', 'RelatedItems', 'Hero', 'FeaturedHero'];
|
|
116
116
|
const missing = required.filter((k) => !comp || !comp[k]);
|
|
117
117
|
if (missing.length) {
|
|
118
118
|
throw new Error('[canopy][mdx] Missing UI exports: ' + missing.join(', '));
|
|
@@ -128,41 +128,6 @@ async function loadUiComponents() {
|
|
|
128
128
|
Slider: !!comp.Slider,
|
|
129
129
|
}); } catch(_){}
|
|
130
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
131
|
UI_COMPONENTS = comp;
|
|
167
132
|
UI_COMPONENTS_PATH = currentPath;
|
|
168
133
|
UI_COMPONENTS_MTIME = currentMtime;
|
|
@@ -174,8 +139,11 @@ async function loadUiComponents() {
|
|
|
174
139
|
}
|
|
175
140
|
|
|
176
141
|
function extractTitle(mdxSource) {
|
|
177
|
-
const { content } = parseFrontmatter(String(mdxSource || ""));
|
|
178
|
-
|
|
142
|
+
const { data, content } = parseFrontmatter(String(mdxSource || ""));
|
|
143
|
+
if (data && typeof data.title === "string" && data.title.trim()) {
|
|
144
|
+
return data.title.trim();
|
|
145
|
+
}
|
|
146
|
+
const m = content.match(/^\s*#{1,6}\s+(.+?)\s*$/m);
|
|
179
147
|
return m ? m[1].trim() : "Untitled";
|
|
180
148
|
}
|
|
181
149
|
|
|
@@ -265,26 +233,9 @@ async function loadAppWrapper() {
|
|
|
265
233
|
ok = false;
|
|
266
234
|
}
|
|
267
235
|
if (!ok) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
// Fallback to pass-through wrapper to avoid blocking builds
|
|
273
|
-
if (!App) {
|
|
274
|
-
App = function PassThrough(props) {
|
|
275
|
-
return React.createElement(React.Fragment, null, props.children);
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
try {
|
|
279
|
-
require("./log").log(
|
|
280
|
-
"! Warning: content/_app.mdx did not clearly render {children}; proceeding with best-effort wrapper\n",
|
|
281
|
-
"yellow"
|
|
282
|
-
);
|
|
283
|
-
} catch (_) {
|
|
284
|
-
console.warn(
|
|
285
|
-
"Warning: content/_app.mdx did not clearly render {children}; proceeding."
|
|
286
|
-
);
|
|
287
|
-
}
|
|
236
|
+
throw new Error(
|
|
237
|
+
"content/_app.mdx must render {children}. Update the layout so downstream pages receive their content."
|
|
238
|
+
);
|
|
288
239
|
}
|
|
289
240
|
APP_WRAPPER = { App, Head };
|
|
290
241
|
return APP_WRAPPER;
|
|
@@ -390,7 +341,7 @@ async function ensureClientRuntime() {
|
|
|
390
341
|
esbuild = require("esbuild");
|
|
391
342
|
} catch (_) {}
|
|
392
343
|
}
|
|
393
|
-
if (!esbuild)
|
|
344
|
+
if (!esbuild) throw new Error('Viewer runtime bundling requires esbuild. Install dependencies before building.');
|
|
394
345
|
ensureDirSync(OUT_DIR);
|
|
395
346
|
const scriptsDir = path.join(OUT_DIR, 'scripts');
|
|
396
347
|
ensureDirSync(scriptsDir);
|
|
@@ -509,6 +460,9 @@ async function ensureClientRuntime() {
|
|
|
509
460
|
async function ensureFacetsRuntime() {
|
|
510
461
|
let esbuild = null;
|
|
511
462
|
try { esbuild = require("../../ui/node_modules/esbuild"); } catch (_) { try { esbuild = require("esbuild"); } catch (_) {} }
|
|
463
|
+
if (!esbuild) {
|
|
464
|
+
throw new Error('RelatedItems runtime bundling requires esbuild. Install dependencies before building.');
|
|
465
|
+
}
|
|
512
466
|
ensureDirSync(OUT_DIR);
|
|
513
467
|
const scriptsDir = path.join(OUT_DIR, 'scripts');
|
|
514
468
|
ensureDirSync(scriptsDir);
|
|
@@ -589,18 +543,30 @@ async function ensureFacetsRuntime() {
|
|
|
589
543
|
});
|
|
590
544
|
`;
|
|
591
545
|
const shim = { name: 'facets-vanilla', setup(){} };
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
546
|
+
try {
|
|
547
|
+
await esbuild.build({
|
|
548
|
+
stdin: { contents: entry, resolveDir: process.cwd(), sourcefile: 'canopy-facets-entry.js', loader: 'js' },
|
|
549
|
+
outfile: outFile,
|
|
550
|
+
platform: 'browser',
|
|
551
|
+
format: 'iife',
|
|
552
|
+
bundle: true,
|
|
553
|
+
sourcemap: false,
|
|
554
|
+
target: ['es2018'],
|
|
555
|
+
logLevel: 'silent',
|
|
556
|
+
minify: true,
|
|
557
|
+
plugins: [shim]
|
|
558
|
+
});
|
|
559
|
+
} catch (e) {
|
|
560
|
+
const message = e && e.message ? e.message : e;
|
|
561
|
+
throw new Error(`RelatedItems runtime build failed: ${message}`);
|
|
603
562
|
}
|
|
563
|
+
try {
|
|
564
|
+
const { logLine } = require('./log');
|
|
565
|
+
let size = 0; try { const st = fs.statSync(outFile); size = st && st.size || 0; } catch (_) {}
|
|
566
|
+
const kb = size ? ` (${(size/1024).toFixed(1)} KB)` : '';
|
|
567
|
+
const rel = path.relative(process.cwd(), outFile).split(path.sep).join('/');
|
|
568
|
+
logLine(`✓ Wrote ${rel}${kb}`, 'cyan');
|
|
569
|
+
} catch (_) {}
|
|
604
570
|
}
|
|
605
571
|
|
|
606
572
|
// Bundle a separate client runtime for the Clover Slider to keep payloads split.
|
|
@@ -611,7 +577,7 @@ async function ensureSliderRuntime() {
|
|
|
611
577
|
} catch (_) {
|
|
612
578
|
try { esbuild = require("esbuild"); } catch (_) {}
|
|
613
579
|
}
|
|
614
|
-
if (!esbuild)
|
|
580
|
+
if (!esbuild) throw new Error('Slider runtime bundling requires esbuild. Install dependencies before building.');
|
|
615
581
|
ensureDirSync(OUT_DIR);
|
|
616
582
|
const scriptsDir = path.join(OUT_DIR, 'scripts');
|
|
617
583
|
ensureDirSync(scriptsDir);
|
|
@@ -735,8 +701,8 @@ async function ensureSliderRuntime() {
|
|
|
735
701
|
plugins: [plugin],
|
|
736
702
|
});
|
|
737
703
|
} catch (e) {
|
|
738
|
-
|
|
739
|
-
|
|
704
|
+
const message = e && e.message ? e.message : e;
|
|
705
|
+
throw new Error(`Slider runtime build failed: ${message}`);
|
|
740
706
|
}
|
|
741
707
|
try {
|
|
742
708
|
const { logLine } = require('./log');
|
|
@@ -757,7 +723,7 @@ async function ensureReactGlobals() {
|
|
|
757
723
|
esbuild = require("esbuild");
|
|
758
724
|
} catch (_) {}
|
|
759
725
|
}
|
|
760
|
-
if (!esbuild)
|
|
726
|
+
if (!esbuild) throw new Error('React globals bundling requires esbuild. Install dependencies before building.');
|
|
761
727
|
const { path } = require("../common");
|
|
762
728
|
ensureDirSync(OUT_DIR);
|
|
763
729
|
const scriptsDir = path.join(OUT_DIR, "scripts");
|
|
@@ -792,7 +758,7 @@ async function ensureReactGlobals() {
|
|
|
792
758
|
async function ensureHeroRuntime() {
|
|
793
759
|
let esbuild = null;
|
|
794
760
|
try { esbuild = require("../../ui/node_modules/esbuild"); } catch (_) { try { esbuild = require("esbuild"); } catch (_) {} }
|
|
795
|
-
if (!esbuild)
|
|
761
|
+
if (!esbuild) throw new Error('Hero runtime bundling requires esbuild. Install dependencies before building.');
|
|
796
762
|
const { path } = require("../common");
|
|
797
763
|
ensureDirSync(OUT_DIR);
|
|
798
764
|
const scriptsDir = path.join(OUT_DIR, 'scripts');
|
package/lib/build/pages.js
CHANGED
|
@@ -45,10 +45,6 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}) {
|
|
|
45
45
|
const source = await fsp.readFile(filePath, 'utf8');
|
|
46
46
|
const title = mdx.extractTitle(source);
|
|
47
47
|
const { body, head } = await mdx.compileMdxFile(filePath, outPath, null, extraProps);
|
|
48
|
-
const cssRel = path
|
|
49
|
-
.relative(path.dirname(outPath), path.join(OUT_DIR, 'styles', 'styles.css'))
|
|
50
|
-
.split(path.sep)
|
|
51
|
-
.join('/');
|
|
52
48
|
const needsHydrateViewer = body.includes('data-canopy-viewer');
|
|
53
49
|
const needsHydrateSlider = body.includes('data-canopy-slider');
|
|
54
50
|
const needsCommand = true; // command runtime is global
|
|
@@ -96,7 +92,7 @@ async function renderContentMdxToHtml(filePath, outPath, extraProps = {}) {
|
|
|
96
92
|
if (sliderRel && jsRel !== sliderRel) extraScripts.push(`<script defer src="${sliderRel}"></script>`);
|
|
97
93
|
if (commandRel && jsRel !== commandRel) extraScripts.push(`<script defer src="${commandRel}"></script>`);
|
|
98
94
|
if (extraScripts.length) headExtra = extraScripts.join('') + headExtra;
|
|
99
|
-
const html = htmlShell({ title, body, cssHref:
|
|
95
|
+
const html = htmlShell({ title, body, cssHref: null, scriptHref: jsRel, headExtra: vendorTag + headExtra });
|
|
100
96
|
const { applyBaseToHtml } = require('../common');
|
|
101
97
|
return applyBaseToHtml(html);
|
|
102
98
|
}
|
package/lib/build/runtimes.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
|
|
2
1
|
const { logLine } = require('./log');
|
|
3
|
-
const { fs,
|
|
2
|
+
const { fs, path, OUT_DIR, ensureDirSync } = require('../common');
|
|
4
3
|
|
|
5
4
|
async function prepareAllRuntimes() {
|
|
6
5
|
const mdx = require('./mdx');
|
|
7
6
|
try { await mdx.ensureClientRuntime(); } catch (_) {}
|
|
8
7
|
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
8
|
try {
|
|
11
9
|
if (process.env.CANOPY_ENABLE_HERO_RUNTIME === '1' || process.env.CANOPY_ENABLE_HERO_RUNTIME === 'true') {
|
|
12
10
|
if (typeof mdx.ensureHeroRuntime === 'function') await mdx.ensureHeroRuntime();
|
|
@@ -14,121 +12,60 @@ async function prepareAllRuntimes() {
|
|
|
14
12
|
} catch (_) {}
|
|
15
13
|
try { if (typeof mdx.ensureFacetsRuntime === 'function') await mdx.ensureFacetsRuntime(); } catch (_) {}
|
|
16
14
|
try { if (typeof mdx.ensureReactGlobals === 'function') await mdx.ensureReactGlobals(); } catch (_) {}
|
|
17
|
-
|
|
15
|
+
await prepareCommandRuntime();
|
|
18
16
|
try { logLine('✓ Prepared client hydration runtimes', 'cyan', { dim: true }); } catch (_) {}
|
|
19
17
|
}
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
async function resolveEsbuild() {
|
|
20
|
+
try { return require('../../ui/node_modules/esbuild'); } catch (_) {
|
|
21
|
+
try { return require('esbuild'); } catch (_) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
22
26
|
|
|
23
|
-
async function
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
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 ''; } }
|
|
51
|
-
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; } }
|
|
52
|
-
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 []; } }
|
|
53
|
-
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();
|
|
54
|
-
var panel = (function(){ try{ return host.querySelector('[data-canopy-command-panel]') || null; }catch(_){ return null; } })();
|
|
55
|
-
if(!panel) return; // no SSR panel present; do nothing
|
|
56
|
-
try{ var rel=host.querySelector('.relative'); if(rel && !onSearchPage) rel.setAttribute('data-canopy-panel-auto','1'); }catch(_){}
|
|
57
|
-
if(onSearchPage){ panel.style.display='none'; }
|
|
58
|
-
var list=panel.querySelector('#cplist');
|
|
59
|
-
var input = (function(){ try{ return host.querySelector('[data-canopy-command-input]') || null; }catch(_){ return null; } })();
|
|
60
|
-
if(!input) return; // require SSR input; no dynamic creation
|
|
61
|
-
// Populate from ?q= URL param if present
|
|
62
|
-
try {
|
|
63
|
-
var sp = new URLSearchParams(location.search || '');
|
|
64
|
-
var qp = sp.get('q');
|
|
65
|
-
if (qp) input.value = qp;
|
|
66
|
-
} catch(_) {}
|
|
67
|
-
// Do not inject legacy trigger buttons or inputs
|
|
68
|
-
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); }); }); }
|
|
69
|
-
function getItems(){ try{ return Array.prototype.slice.call(list.querySelectorAll('[data-canopy-item]')); }catch(_){ return []; } }
|
|
70
|
-
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(_){} }
|
|
71
|
-
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||''); });
|
|
72
|
-
// Keyboard navigation: ArrowDown/ArrowUp to move through items; Enter to select
|
|
73
|
-
input.addEventListener('keydown', function(e){
|
|
74
|
-
if(e.key==='ArrowDown'){ e.preventDefault(); try{ var items=getItems(); if(items.length){ panel.style.display='block'; items[0].focus(); } }catch(_){} }
|
|
75
|
-
else if(e.key==='ArrowUp'){ e.preventDefault(); try{ var items2=getItems(); if(items2.length){ panel.style.display='block'; items2[items2.length-1].focus(); } }catch(_){} }
|
|
76
|
-
});
|
|
77
|
-
list.addEventListener('keydown', function(e){
|
|
78
|
-
var cur = e.target && e.target.closest && e.target.closest('[data-canopy-item]');
|
|
79
|
-
if(!cur) return;
|
|
80
|
-
if(e.key==='ArrowDown'){
|
|
81
|
-
e.preventDefault();
|
|
82
|
-
try{ var arr=getItems(); var i=arr.indexOf(cur); var nxt=arr[Math.min(arr.length-1, i+1)]||cur; nxt.focus(); }catch(_){}
|
|
83
|
-
} else if(e.key==='ArrowUp'){
|
|
84
|
-
e.preventDefault();
|
|
85
|
-
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(_){}
|
|
86
|
-
} else if(e.key==='Enter'){
|
|
87
|
-
e.preventDefault(); try{ cur.click(); }catch(_){}
|
|
88
|
-
} else if(e.key==='Escape'){
|
|
89
|
-
panel.style.display='none'; try{ input && input.focus && input.focus(); }catch(_){}
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
document.addEventListener('keydown', function(e){ if(e.key==='Escape'){ panel.style.display='none'; }});
|
|
93
|
-
document.addEventListener('mousedown', function(e){ try{ if(!panel.contains(e.target) && !host.contains(e.target)){ panel.style.display='none'; } }catch(_){} });
|
|
94
|
-
// Hotkey support (e.g., mod+k)
|
|
95
|
-
document.addEventListener('keydown', function(e){
|
|
96
|
-
try {
|
|
97
|
-
var want = String((cfg && cfg.hotkey) || '').toLowerCase();
|
|
98
|
-
if (!want) return;
|
|
99
|
-
var isMod = e.metaKey || e.ctrlKey;
|
|
100
|
-
if ((want === 'mod+k' || want === 'cmd+k' || want === 'ctrl+k') && isMod && (e.key === 'k' || e.key === 'K')) {
|
|
101
|
-
e.preventDefault();
|
|
102
|
-
if(onSearchPage){ try{ var ev2 = new CustomEvent('canopy:search:setQuery', { detail: { hotkey: true } }); window.dispatchEvent(ev2); }catch(_){ } return; }
|
|
103
|
-
panel.style.display='block';
|
|
104
|
-
(input && input.focus && input.focus());
|
|
105
|
-
filterAndShow(input && input.value || '');
|
|
106
|
-
}
|
|
107
|
-
} catch(_) { }
|
|
108
|
-
});
|
|
109
|
-
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 || ''); }
|
|
110
|
-
host.addEventListener('click', function(e){ var trg=e.target && e.target.closest && e.target.closest('[data-canopy-command-trigger]'); if(trg){ e.preventDefault(); openPanel(); }});
|
|
111
|
-
try{ var teaser2 = host.querySelector('[data-canopy-command-input]'); if(teaser2){ teaser2.addEventListener('focus', function(){ openPanel(); }); } }catch(_){}
|
|
112
|
-
});
|
|
113
|
-
})();
|
|
114
|
-
`;
|
|
115
|
-
await fsp.writeFile(cmdOut, fallback, 'utf8');
|
|
116
|
-
try { logLine(`✓ Wrote ${path.relative(process.cwd(), cmdOut)} (fallback)`, 'cyan'); } catch (_) {}
|
|
27
|
+
async function prepareCommandRuntime() {
|
|
28
|
+
const esbuild = await resolveEsbuild();
|
|
29
|
+
if (!esbuild) throw new Error('Command runtime bundling requires esbuild. Install dependencies before building.');
|
|
30
|
+
ensureDirSync(OUT_DIR);
|
|
31
|
+
const scriptsDir = path.join(OUT_DIR, 'scripts');
|
|
32
|
+
ensureDirSync(scriptsDir);
|
|
33
|
+
const entry = path.join(__dirname, '..', 'search', 'command-runtime.js');
|
|
34
|
+
const outFile = path.join(scriptsDir, 'canopy-command.js');
|
|
35
|
+
await esbuild.build({
|
|
36
|
+
entryPoints: [entry],
|
|
37
|
+
outfile: outFile,
|
|
38
|
+
platform: 'browser',
|
|
39
|
+
format: 'iife',
|
|
40
|
+
bundle: true,
|
|
41
|
+
sourcemap: false,
|
|
42
|
+
target: ['es2018'],
|
|
43
|
+
logLevel: 'silent',
|
|
44
|
+
minify: true,
|
|
45
|
+
});
|
|
46
|
+
try {
|
|
47
|
+
let size = 0;
|
|
48
|
+
try { const st = fs.statSync(outFile); size = st.size || 0; } catch (_) {}
|
|
49
|
+
const kb = size ? ` (${(size / 1024).toFixed(1)} KB)` : '';
|
|
50
|
+
const rel = path.relative(process.cwd(), outFile).split(path.sep).join('/');
|
|
51
|
+
logLine(`✓ Wrote ${rel}${kb}`, 'cyan');
|
|
52
|
+
} catch (_) {}
|
|
117
53
|
}
|
|
118
54
|
|
|
119
55
|
async function prepareSearchRuntime(timeoutMs = 10000, label = '') {
|
|
120
56
|
const search = require('../search/search');
|
|
121
|
-
try { logLine(`• Writing search runtime${label ? ' ('+label+')' : ''}...`, 'blue', { bright: true }); } catch (_) {}
|
|
57
|
+
try { logLine(`• Writing search runtime${label ? ' (' + label + ')' : ''}...`, 'blue', { bright: true }); } catch (_) {}
|
|
58
|
+
|
|
122
59
|
let timedOut = false;
|
|
123
60
|
await Promise.race([
|
|
124
61
|
search.ensureSearchRuntime(),
|
|
125
62
|
new Promise((_, reject) => setTimeout(() => { timedOut = true; reject(new Error('timeout')); }, Number(timeoutMs)))
|
|
126
63
|
]).catch(() => {
|
|
127
|
-
try { console.warn(`Search: Bundling runtime timed out${label ? ' ('+label+')' : ''}, skipping`); } catch (_) {}
|
|
64
|
+
try { console.warn(`Search: Bundling runtime timed out${label ? ' (' + label + ')' : ''}, skipping`); } catch (_) {}
|
|
128
65
|
});
|
|
129
66
|
if (timedOut) {
|
|
130
|
-
try { logLine(`! Search runtime not bundled${label ? ' ('+label+')' : ''}\n`, 'yellow'); } catch (_) {}
|
|
67
|
+
try { logLine(`! Search runtime not bundled${label ? ' (' + label + ')' : ''}\n`, 'yellow'); } catch (_) {}
|
|
131
68
|
}
|
|
132
69
|
}
|
|
133
70
|
|
|
134
|
-
module.exports = { prepareAllRuntimes,
|
|
71
|
+
module.exports = { prepareAllRuntimes, prepareCommandRuntime, prepareSearchRuntime };
|
package/lib/build/search.js
CHANGED
|
@@ -165,10 +165,6 @@ async function writeFacetsSearchApi() {
|
|
|
165
165
|
await fsp.writeFile(dest, JSON.stringify(data, null, 2), 'utf8');
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
module.exports = { buildFacetsForWorks, writeFacetCollections, writeFacetsSearchApi };
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
168
|
async function collectMdxPageRecords() {
|
|
173
169
|
const { fs, fsp, path, CONTENT_DIR, rootRelativeHref } = require('../common');
|
|
174
170
|
const mdx = require('./mdx');
|
|
@@ -183,7 +179,8 @@ async function collectMdxPageRecords() {
|
|
|
183
179
|
const base = path.basename(p).toLowerCase();
|
|
184
180
|
const src = await fsp.readFile(p, 'utf8');
|
|
185
181
|
const fm = mdx.parseFrontmatter(src);
|
|
186
|
-
const
|
|
182
|
+
const titleRaw = mdx.extractTitle(src);
|
|
183
|
+
const title = typeof titleRaw === 'string' ? titleRaw.trim() : '';
|
|
187
184
|
const rel = path.relative(CONTENT_DIR, p).replace(/\.mdx$/i, '.html');
|
|
188
185
|
if (base !== 'sitemap.mdx') {
|
|
189
186
|
const href = rootRelativeHref(rel.split(path.sep).join('/'));
|
package/lib/build/styles.js
CHANGED
|
@@ -70,21 +70,15 @@ async function ensureStyles() {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
function resolveTailwindCli() {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
process.platform === "win32" ? "tailwindcss.cmd" : "tailwindcss"
|
|
83
|
-
);
|
|
84
|
-
if (fs.existsSync(localBin)) return { cmd: localBin, args: [] };
|
|
85
|
-
} catch (_) {}
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
73
|
+
const localBin = path.join(
|
|
74
|
+
process.cwd(),
|
|
75
|
+
"node_modules",
|
|
76
|
+
".bin",
|
|
77
|
+
process.platform === "win32" ? "tailwindcss.cmd" : "tailwindcss"
|
|
78
|
+
);
|
|
79
|
+
if (fs.existsSync(localBin)) return { cmd: localBin, args: [] };
|
|
80
|
+
return { cmd: 'tailwindcss', args: [] };
|
|
81
|
+
}
|
|
88
82
|
function buildTailwindCli({ input, output, config, minify = true }) {
|
|
89
83
|
try {
|
|
90
84
|
const cli = resolveTailwindCli();
|
package/lib/common.js
CHANGED
|
@@ -26,13 +26,13 @@ function readYamlConfigBaseUrl() {
|
|
|
26
26
|
// Priority:
|
|
27
27
|
// 1) CANOPY_BASE_URL env
|
|
28
28
|
// 2) canopy.yml → site.baseUrl
|
|
29
|
-
// 3) dev server default http://localhost:PORT (PORT env or
|
|
29
|
+
// 3) dev server default http://localhost:PORT (PORT env or 5001)
|
|
30
30
|
const BASE_ORIGIN = (() => {
|
|
31
31
|
const env = String(process.env.CANOPY_BASE_URL || '').trim();
|
|
32
32
|
if (env) return env.replace(/\/$/, '');
|
|
33
33
|
const cfg = readYamlConfigBaseUrl();
|
|
34
34
|
if (cfg) return cfg.replace(/\/$/, '');
|
|
35
|
-
const port = Number(process.env.PORT ||
|
|
35
|
+
const port = Number(process.env.PORT || 5001);
|
|
36
36
|
return `http://localhost:${port}`;
|
|
37
37
|
})();
|
|
38
38
|
|
|
@@ -50,7 +50,8 @@ async function cleanDir(dir) {
|
|
|
50
50
|
function htmlShell({ title, body, cssHref, scriptHref, headExtra }) {
|
|
51
51
|
const scriptTag = scriptHref ? `<script defer src="${scriptHref}"></script>` : '';
|
|
52
52
|
const extra = headExtra ? String(headExtra) : '';
|
|
53
|
-
|
|
53
|
+
const cssTag = cssHref ? `<link rel="stylesheet" href="${cssHref}">` : '';
|
|
54
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><title>${title}</title>${extra}${cssTag}${scriptTag}</head><body>${body}</body></html>`;
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
function withBase(href) {
|
|
@@ -114,11 +115,18 @@ function absoluteUrl(p) {
|
|
|
114
115
|
function applyBaseToHtml(html) {
|
|
115
116
|
if (!BASE_PATH) return html;
|
|
116
117
|
try {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return out;
|
|
118
|
+
const out = String(html || '');
|
|
119
|
+
const normalizedBase = BASE_PATH.startsWith('/')
|
|
120
|
+
? BASE_PATH.replace(/\/$/, '')
|
|
121
|
+
: `/${BASE_PATH.replace(/\/$/, '')}`;
|
|
122
|
+
if (!normalizedBase || normalizedBase === '/') return out;
|
|
123
|
+
const pattern = /(href|src)=(['"])(\/(?!\/)[^'"\s]*)\2/g;
|
|
124
|
+
return out.replace(pattern, (match, attr, quote, path) => {
|
|
125
|
+
if (path === normalizedBase || path.startsWith(`${normalizedBase}/`)) {
|
|
126
|
+
return match;
|
|
127
|
+
}
|
|
128
|
+
return `${attr}=${quote}${normalizedBase}${path}${quote}`;
|
|
129
|
+
});
|
|
122
130
|
} catch (_) {
|
|
123
131
|
return html;
|
|
124
132
|
}
|