mnfst 0.5.31 → 0.5.33

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'
@@ -169,7 +169,12 @@ function getBasePath() {
169
169
 
170
170
  function pathnameToLogical(pathname) {
171
171
  const base = getBasePath();
172
- if (!base || pathname === base || pathname === base + '/') return '/';
172
+ if (!base) {
173
+ const p = (pathname || '/').replace(/\/$/, '') || '/';
174
+ if (p === '/' || p === '/index.html' || p === '/index') return '/';
175
+ return p.startsWith('/') ? p : '/' + p;
176
+ }
177
+ if (pathname === base || pathname === base + '/') return '/';
173
178
  if (pathname.startsWith(base + '/')) {
174
179
  let logical = pathname.slice(base.length) || '/';
175
180
  if (logical === '/index.html' || logical === '/index') logical = '/';
@@ -592,6 +597,39 @@ function shouldElementBeVisible(element, normalizedPath) {
592
597
  return true;
593
598
  }
594
599
 
600
+ // Resolve :attr and x-bind:attr whose value is $x.path so injected meta/link have real content (SPA + prerender).
601
+ function resolveDataHeadBindings(element) {
602
+ const x = typeof window !== 'undefined' && window.$x;
603
+ if (!x) return;
604
+ const toResolve = [];
605
+ for (let i = 0; i < element.attributes.length; i++) {
606
+ const attr = element.attributes[i];
607
+ const name = attr.name;
608
+ let bindingAttr = null;
609
+ if (name.startsWith(':')) bindingAttr = name.slice(1);
610
+ else if (name.startsWith('x-bind:')) bindingAttr = name.slice(7);
611
+ if (!bindingAttr) continue;
612
+ const expr = (attr.value || '').trim();
613
+ if (!expr.startsWith('$x.')) continue;
614
+ const path = expr.slice(3).trim();
615
+ if (!path) continue;
616
+ toResolve.push({ bindingName: attr.name, attrName: bindingAttr, path });
617
+ }
618
+ for (const { bindingName, attrName, path } of toResolve) {
619
+ let value;
620
+ try {
621
+ value = path.split('.').reduce(function (obj, key) {
622
+ return obj != null && typeof obj === 'object' ? obj[key] : undefined;
623
+ }, x);
624
+ } catch (e) {
625
+ continue;
626
+ }
627
+ if (value === undefined) continue;
628
+ element.setAttribute(attrName, String(value));
629
+ element.removeAttribute(bindingName);
630
+ }
631
+ }
632
+
595
633
  // Generate unique identifier for head content
596
634
  function generateHeadId(element) {
597
635
  const position = element.getAttribute('data-order');
@@ -627,10 +665,14 @@ function processElementHeadContent(element, normalizedPath) {
627
665
  const isVisible = shouldElementBeVisible(element, normalizedPath);
628
666
 
629
667
  if (isVisible) {
630
- // Check if we've already injected this content
668
+ // Skip if already injected (in-memory) or already present in DOM (e.g. prerendered)
631
669
  if (injectedHeadContent.has(headId)) {
632
670
  return;
633
671
  }
672
+ if (document.head.querySelector(`[data-route-head="${headId}"]`)) {
673
+ injectedHeadContent.add(headId);
674
+ return;
675
+ }
634
676
 
635
677
  // Add new head content
636
678
  Array.from(headTemplate.content.children).forEach(child => {
@@ -641,8 +683,9 @@ function processElementHeadContent(element, normalizedPath) {
641
683
  script.setAttribute('data-route-head', headId);
642
684
  document.head.appendChild(script);
643
685
  } else {
644
- // For other elements, clone and add
686
+ // For other elements, clone and add (resolve $x bindings so meta/link have real values in SPA)
645
687
  const clonedChild = child.cloneNode(true);
688
+ resolveDataHeadBindings(clonedChild);
646
689
  clonedChild.setAttribute('data-route-head', headId);
647
690
  document.head.appendChild(clonedChild);
648
691
  }
@@ -826,6 +869,29 @@ window.ManifestRoutingHead = {
826
869
 
827
870
  // Router anchors
828
871
 
872
+ // Parse pipeline syntax: 'scope | targets' (shared for directive and route-change handler)
873
+ function parseAnchorsExpression(expr) {
874
+ if (!expr || expr.trim() === '') {
875
+ return { scope: '', targets: 'h1, h2, h3, h4, h5, h6' };
876
+ }
877
+ if (expr.includes('|')) {
878
+ const parts = expr.split('|').map(p => p.trim());
879
+ return {
880
+ scope: parts[0] || '',
881
+ targets: parts[1] || 'h1, h2, h3, h4, h5, h6'
882
+ };
883
+ }
884
+ return { scope: '', targets: expr };
885
+ }
886
+
887
+ function isVisible(el) {
888
+ if (!el || !el.getBoundingClientRect) return false;
889
+ const rect = el.getBoundingClientRect();
890
+ if (rect.width === 0 && rect.height === 0) return false;
891
+ const style = window.getComputedStyle(el);
892
+ return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
893
+ }
894
+
829
895
  // Anchors functionality
830
896
  function initializeAnchors() {
831
897
 
@@ -834,24 +900,9 @@ function initializeAnchors() {
834
900
 
835
901
 
836
902
  try {
837
- // Parse pipeline syntax: 'scope | targets'
838
- const parseExpression = (expr) => {
839
- if (!expr || expr.trim() === '') {
840
- return { scope: '', targets: 'h1, h2, h3, h4, h5, h6' };
841
- }
903
+ const parseExpression = parseAnchorsExpression;
842
904
 
843
- if (expr.includes('|')) {
844
- const parts = expr.split('|').map(p => p.trim());
845
- return {
846
- scope: parts[0] || '',
847
- targets: parts[1] || 'h1, h2, h3, h4, h5, h6'
848
- };
849
- } else {
850
- return { scope: '', targets: expr };
851
- }
852
- };
853
-
854
- // Extract anchors function
905
+ // Extract anchors function (only from visible scope containers to avoid prior-route content)
855
906
  const extractAnchors = (expr) => {
856
907
  const parsed = parseExpression(expr);
857
908
 
@@ -859,7 +910,8 @@ function initializeAnchors() {
859
910
  if (!parsed.scope) {
860
911
  containers = [document.body];
861
912
  } else {
862
- containers = Array.from(document.querySelectorAll(parsed.scope));
913
+ const all = Array.from(document.querySelectorAll(parsed.scope));
914
+ containers = all.filter(isVisible);
863
915
  }
864
916
 
865
917
  let elements = [];
@@ -1122,21 +1174,44 @@ document.addEventListener('alpine:init', () => {
1122
1174
  }
1123
1175
  });
1124
1176
 
1125
- // Refresh anchors when route changes
1177
+ // Refresh anchors when route changes — wait for scope DOM to update (e.g. x-markdown) to avoid showing prior page's anchors
1126
1178
  window.addEventListener('manifest:route-change', () => {
1127
- // Immediately clear the store to hide the h5 element
1128
1179
  Alpine.store('anchors', { count: 0 });
1129
1180
 
1130
- // Wait longer for content to load after route change
1131
- setTimeout(() => {
1132
- const anchorElements = document.querySelectorAll('[x-anchors]');
1133
- anchorElements.forEach(el => {
1134
- const expression = el.getAttribute('x-anchors');
1135
- if (expression && el._x_anchorRefresh) {
1136
- el._x_anchorRefresh();
1137
- }
1181
+ const runWhenScopeReady = (el) => {
1182
+ const expression = el.getAttribute('x-anchors');
1183
+ if (!expression || !el._x_anchorRefresh) return;
1184
+ const { scope } = parseAnchorsExpression(expression);
1185
+ if (!scope) {
1186
+ setTimeout(() => el._x_anchorRefresh(), 400);
1187
+ return;
1188
+ }
1189
+ const containers = Array.from(document.querySelectorAll(scope)).filter(isVisible);
1190
+ const container = containers[0];
1191
+ if (!container) {
1192
+ el._x_anchorRefresh();
1193
+ return;
1194
+ }
1195
+ let done = false;
1196
+ const finish = () => {
1197
+ if (done) return;
1198
+ done = true;
1199
+ observer?.disconnect();
1200
+ clearTimeout(fallback);
1201
+ el._x_anchorRefresh();
1202
+ };
1203
+ let t = 0;
1204
+ const observer = new MutationObserver(() => {
1205
+ clearTimeout(t);
1206
+ t = setTimeout(finish, 50);
1138
1207
  });
1139
- }, 200);
1208
+ observer.observe(container, { childList: true, subtree: true });
1209
+ const fallback = setTimeout(finish, 800);
1210
+ };
1211
+
1212
+ requestAnimationFrame(() => {
1213
+ document.querySelectorAll('[x-anchors]').forEach(runWhenScopeReady);
1214
+ });
1140
1215
  });
1141
1216
 
1142
1217
  // 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.31",
3
+ "version": "0.5.33",
4
4
  "private": false,
5
5
  "workspaces": [
6
6
  "templates/starter"