mnfst 0.5.32 → 0.5.34

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/dist/manifest.js CHANGED
@@ -117,10 +117,13 @@
117
117
  return new Promise((resolve, reject) => {
118
118
  const url = getPluginUrl(pluginName, version);
119
119
 
120
- // Check if already loaded
120
+ // Skip if script with same src already in DOM (e.g. prerendered HTML or second loader run)
121
121
  const existing = document.querySelector(`script[src="${url}"]`);
122
- if (existing && existing.complete) {
123
- return resolve();
122
+ if (existing) {
123
+ if (existing.complete) return resolve();
124
+ existing.addEventListener('load', () => resolve());
125
+ existing.addEventListener('error', () => reject(new Error(`Failed to load ${pluginName} from ${url}`)));
126
+ return;
124
127
  }
125
128
 
126
129
  const script = document.createElement('script');
@@ -297,6 +300,11 @@
297
300
  detectAppwriteFromManifest();
298
301
 
299
302
  if (config && config.plugins.length > 0) {
303
+ if (window.__manifestLoaderStarted) {
304
+ return;
305
+ }
306
+ window.__manifestLoaderStarted = true;
307
+
300
308
  const MANIFEST_DEPENDENT_PLUGINS = [
301
309
  'data', 'localization', 'components',
302
310
  'appwrite-auth', 'appwrite-data', 'appwrite-presence'
@@ -163,6 +163,12 @@ window.ManifestRoutingPosition = {
163
163
  let currentRoute = '/';
164
164
  let isInternalNavigation = false;
165
165
 
166
+ function isPrerenderedStaticBuild() {
167
+ // When prerendered HTML is served as static pages, prefer normal browser navigation (MPA)
168
+ // so each URL loads its own prerendered HTML rather than SPA toggling.
169
+ return !!document.querySelector('meta[name="manifest:prerendered"][content="1"]');
170
+ }
171
+
166
172
  function getBasePath() {
167
173
  return (typeof window.getManifestBasePath === 'function' ? window.getManifestBasePath() : '') || '';
168
174
  }
@@ -353,15 +359,18 @@ function initializeNavigation() {
353
359
  // Set initial route (logical path for matching)
354
360
  currentRoute = pathnameToLogical(window.location.pathname);
355
361
 
356
- // Intercept link clicks
357
- interceptLinkClicks();
362
+ // In prerendered/static output, use default browser navigation (no interception)
363
+ if (!isPrerenderedStaticBuild()) {
364
+ // Intercept link clicks
365
+ interceptLinkClicks();
358
366
 
359
- // Listen for popstate events (browser back/forward)
360
- window.addEventListener('popstate', () => {
361
- if (!isInternalNavigation) {
362
- handleRouteChange();
363
- }
364
- });
367
+ // Listen for popstate events (browser back/forward)
368
+ window.addEventListener('popstate', () => {
369
+ if (!isInternalNavigation) {
370
+ handleRouteChange();
371
+ }
372
+ });
373
+ }
365
374
 
366
375
  // Handle initial route
367
376
  handleRouteChange();
@@ -597,6 +606,39 @@ function shouldElementBeVisible(element, normalizedPath) {
597
606
  return true;
598
607
  }
599
608
 
609
+ // Resolve :attr and x-bind:attr whose value is $x.path so injected meta/link have real content (SPA + prerender).
610
+ function resolveDataHeadBindings(element) {
611
+ const x = typeof window !== 'undefined' && window.$x;
612
+ if (!x) return;
613
+ const toResolve = [];
614
+ for (let i = 0; i < element.attributes.length; i++) {
615
+ const attr = element.attributes[i];
616
+ const name = attr.name;
617
+ let bindingAttr = null;
618
+ if (name.startsWith(':')) bindingAttr = name.slice(1);
619
+ else if (name.startsWith('x-bind:')) bindingAttr = name.slice(7);
620
+ if (!bindingAttr) continue;
621
+ const expr = (attr.value || '').trim();
622
+ if (!expr.startsWith('$x.')) continue;
623
+ const path = expr.slice(3).trim();
624
+ if (!path) continue;
625
+ toResolve.push({ bindingName: attr.name, attrName: bindingAttr, path });
626
+ }
627
+ for (const { bindingName, attrName, path } of toResolve) {
628
+ let value;
629
+ try {
630
+ value = path.split('.').reduce(function (obj, key) {
631
+ return obj != null && typeof obj === 'object' ? obj[key] : undefined;
632
+ }, x);
633
+ } catch (e) {
634
+ continue;
635
+ }
636
+ if (value === undefined) continue;
637
+ element.setAttribute(attrName, String(value));
638
+ element.removeAttribute(bindingName);
639
+ }
640
+ }
641
+
600
642
  // Generate unique identifier for head content
601
643
  function generateHeadId(element) {
602
644
  const position = element.getAttribute('data-order');
@@ -632,10 +674,14 @@ function processElementHeadContent(element, normalizedPath) {
632
674
  const isVisible = shouldElementBeVisible(element, normalizedPath);
633
675
 
634
676
  if (isVisible) {
635
- // Check if we've already injected this content
677
+ // Skip if already injected (in-memory) or already present in DOM (e.g. prerendered)
636
678
  if (injectedHeadContent.has(headId)) {
637
679
  return;
638
680
  }
681
+ if (document.head.querySelector(`[data-route-head="${headId}"]`)) {
682
+ injectedHeadContent.add(headId);
683
+ return;
684
+ }
639
685
 
640
686
  // Add new head content
641
687
  Array.from(headTemplate.content.children).forEach(child => {
@@ -646,8 +692,9 @@ function processElementHeadContent(element, normalizedPath) {
646
692
  script.setAttribute('data-route-head', headId);
647
693
  document.head.appendChild(script);
648
694
  } else {
649
- // For other elements, clone and add
695
+ // For other elements, clone and add (resolve $x bindings so meta/link have real values in SPA)
650
696
  const clonedChild = child.cloneNode(true);
697
+ resolveDataHeadBindings(clonedChild);
651
698
  clonedChild.setAttribute('data-route-head', headId);
652
699
  document.head.appendChild(clonedChild);
653
700
  }
@@ -831,6 +878,29 @@ window.ManifestRoutingHead = {
831
878
 
832
879
  // Router anchors
833
880
 
881
+ // Parse pipeline syntax: 'scope | targets' (shared for directive and route-change handler)
882
+ function parseAnchorsExpression(expr) {
883
+ if (!expr || expr.trim() === '') {
884
+ return { scope: '', targets: 'h1, h2, h3, h4, h5, h6' };
885
+ }
886
+ if (expr.includes('|')) {
887
+ const parts = expr.split('|').map(p => p.trim());
888
+ return {
889
+ scope: parts[0] || '',
890
+ targets: parts[1] || 'h1, h2, h3, h4, h5, h6'
891
+ };
892
+ }
893
+ return { scope: '', targets: expr };
894
+ }
895
+
896
+ function isVisible(el) {
897
+ if (!el || !el.getBoundingClientRect) return false;
898
+ const rect = el.getBoundingClientRect();
899
+ if (rect.width === 0 && rect.height === 0) return false;
900
+ const style = window.getComputedStyle(el);
901
+ return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
902
+ }
903
+
834
904
  // Anchors functionality
835
905
  function initializeAnchors() {
836
906
 
@@ -839,24 +909,9 @@ function initializeAnchors() {
839
909
 
840
910
 
841
911
  try {
842
- // Parse pipeline syntax: 'scope | targets'
843
- const parseExpression = (expr) => {
844
- if (!expr || expr.trim() === '') {
845
- return { scope: '', targets: 'h1, h2, h3, h4, h5, h6' };
846
- }
847
-
848
- if (expr.includes('|')) {
849
- const parts = expr.split('|').map(p => p.trim());
850
- return {
851
- scope: parts[0] || '',
852
- targets: parts[1] || 'h1, h2, h3, h4, h5, h6'
853
- };
854
- } else {
855
- return { scope: '', targets: expr };
856
- }
857
- };
912
+ const parseExpression = parseAnchorsExpression;
858
913
 
859
- // Extract anchors function
914
+ // Extract anchors function (only from visible scope containers to avoid prior-route content)
860
915
  const extractAnchors = (expr) => {
861
916
  const parsed = parseExpression(expr);
862
917
 
@@ -864,7 +919,8 @@ function initializeAnchors() {
864
919
  if (!parsed.scope) {
865
920
  containers = [document.body];
866
921
  } else {
867
- containers = Array.from(document.querySelectorAll(parsed.scope));
922
+ const all = Array.from(document.querySelectorAll(parsed.scope));
923
+ containers = all.filter(isVisible);
868
924
  }
869
925
 
870
926
  let elements = [];
@@ -1127,21 +1183,44 @@ document.addEventListener('alpine:init', () => {
1127
1183
  }
1128
1184
  });
1129
1185
 
1130
- // Refresh anchors when route changes
1186
+ // Refresh anchors when route changes — wait for scope DOM to update (e.g. x-markdown) to avoid showing prior page's anchors
1131
1187
  window.addEventListener('manifest:route-change', () => {
1132
- // Immediately clear the store to hide the h5 element
1133
1188
  Alpine.store('anchors', { count: 0 });
1134
1189
 
1135
- // Wait longer for content to load after route change
1136
- setTimeout(() => {
1137
- const anchorElements = document.querySelectorAll('[x-anchors]');
1138
- anchorElements.forEach(el => {
1139
- const expression = el.getAttribute('x-anchors');
1140
- if (expression && el._x_anchorRefresh) {
1141
- el._x_anchorRefresh();
1142
- }
1190
+ const runWhenScopeReady = (el) => {
1191
+ const expression = el.getAttribute('x-anchors');
1192
+ if (!expression || !el._x_anchorRefresh) return;
1193
+ const { scope } = parseAnchorsExpression(expression);
1194
+ if (!scope) {
1195
+ setTimeout(() => el._x_anchorRefresh(), 400);
1196
+ return;
1197
+ }
1198
+ const containers = Array.from(document.querySelectorAll(scope)).filter(isVisible);
1199
+ const container = containers[0];
1200
+ if (!container) {
1201
+ el._x_anchorRefresh();
1202
+ return;
1203
+ }
1204
+ let done = false;
1205
+ const finish = () => {
1206
+ if (done) return;
1207
+ done = true;
1208
+ observer?.disconnect();
1209
+ clearTimeout(fallback);
1210
+ el._x_anchorRefresh();
1211
+ };
1212
+ let t = 0;
1213
+ const observer = new MutationObserver(() => {
1214
+ clearTimeout(t);
1215
+ t = setTimeout(finish, 50);
1143
1216
  });
1144
- }, 200);
1217
+ observer.observe(container, { childList: true, subtree: true });
1218
+ const fallback = setTimeout(finish, 800);
1219
+ };
1220
+
1221
+ requestAnimationFrame(() => {
1222
+ document.querySelectorAll('[x-anchors]').forEach(runWhenScopeReady);
1223
+ });
1145
1224
  });
1146
1225
 
1147
1226
  // Refresh anchors when hash changes (for active state updates)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst",
3
- "version": "0.5.32",
3
+ "version": "0.5.34",
4
4
  "private": false,
5
5
  "workspaces": [
6
6
  "templates/starter"