mnfst 0.5.162 → 0.5.164

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/manifest.js CHANGED
@@ -2,10 +2,8 @@
2
2
  /* By Andrew Matlock under MIT license
3
3
  /* https://manifestx.dev
4
4
  /*
5
- /* Lightweight loader that dynamically loads Alpine.js and Manifest plugins
6
- /* from jsDelivr CDN. Loads all plugins by default, or a subset if specified.
7
- /*
8
- /* Some plugins use Manifest CSS styles.
5
+ /* Loader: pulls Alpine.js and Manifest plugins from the jsDelivr CDN — all
6
+ /* plugins by default, or a subset if specified.
9
7
  */
10
8
 
11
9
  (function () {
@@ -14,23 +12,16 @@
14
12
  /*
15
13
  * Hydration contract runtime
16
14
  * --------------------------
17
- * Prerendered MPA pages carry a `<script type="application/json"
18
- * id="__manifest_hydrate__">` blob containing the source-authored
19
- * attributes (and, for explicit `data-hydrate` subtrees, the source
20
- * innerHTML) of every element that needs runtime hydration. This
21
- * function runs once on page load BEFORE any plugin or Alpine starts —
22
- * it walks the contract, restores source state, and removes its own
23
- * markers. Every downstream plugin (colors, router, data, markdown,
24
- * icons, …) then sees exactly the DOM the user authored, exactly as it
25
- * would in a live SPA. No plugin needs a "prerender mode" branch.
15
+ * Prerendered MPA pages carry a `#__manifest_hydrate__` JSON blob of the
16
+ * source-authored attributes (and, for `data-hydrate` subtrees, innerHTML)
17
+ * of each element needing hydration. Runs once BEFORE any plugin or Alpine
18
+ * so every downstream plugin sees the authored DOM, as in a live SPA — no
19
+ * plugin needs a "prerender mode" branch.
26
20
  *
27
- * Implementation notes:
28
- * - We use a temp-div HTML parse to set attributes because `setAttribute`
29
- * throws InvalidCharacterError on Alpine special names like `@click`.
30
- * The HTML parser is lenient and accepts them.
31
- * - The contract is a compact diff: only attributes whose values drifted
32
- * from source during prerender appear. An entry's `attrs` object maps
33
- * attribute name -> source value, or null to mean "remove".
21
+ * - Attributes are set via a temp-div HTML parse, not setAttribute, which
22
+ * throws on Alpine special names like `@click`; the parser accepts them.
23
+ * - The contract is a compact diff: `attrs` maps name -> source value, or
24
+ * null to remove; only values that drifted during prerender appear.
34
25
  */
35
26
  function hydratePrerenderedPage() {
36
27
  if (typeof document === 'undefined' || !document.querySelector) return;
@@ -55,9 +46,7 @@
55
46
  .replace(/"/g, '&quot;');
56
47
  const voidEls = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
57
48
 
58
- // Restore deepest-first so that when an ancestor rebuilds its innerHTML,
59
- // its children have already been restored and their source state is what
60
- // the ancestor captures.
49
+ // Deepest-first: a rebuilt ancestor then captures already-restored children.
61
50
  const items = [];
62
51
  for (const entry of entries) {
63
52
  const el = document.querySelector('[data-hydrate-id="' + entry.id + '"]');
@@ -72,13 +61,12 @@
72
61
  const el = document.querySelector('[data-hydrate-id="' + entry.id + '"]') || initialEl;
73
62
  if (!el || !el.parentNode) continue;
74
63
 
75
- // Case 1: explicit subtree restoration (entry.html present).
76
- // Rebuild the element from scratch via outerHTML replacement so the
77
- // entire subtree mirrors the authored source.
64
+ // Case 1: explicit subtree restoration (entry.html present)
65
+ // rebuild via outerHTML so the whole subtree mirrors source.
78
66
  if (typeof entry.html === 'string') {
79
67
  const tag = el.tagName.toLowerCase();
80
68
  const finalAttrs = {};
81
- // Start from current attrs, then apply the contract diff.
69
+ // Current attrs, then the contract diff.
82
70
  const cur = el.attributes;
83
71
  for (let i = 0; i < cur.length; i++) {
84
72
  if (cur[i].name !== 'data-hydrate-id') finalAttrs[cur[i].name] = cur[i].value;
@@ -106,9 +94,8 @@
106
94
  continue;
107
95
  }
108
96
 
109
- // Case 2: attribute-only diff. Reparse the element with the merged
110
- // attribute set (current attrs overlaid by source diff) so that
111
- // special-name attributes like @click work. Preserve innerHTML.
97
+ // Case 2: attribute-only diff. Reparse with merged attrs (so
98
+ // special names like @click work); innerHTML preserved.
112
99
  if (!entry.attrs) continue;
113
100
  const tag = el.tagName.toLowerCase();
114
101
  const finalAttrs = {};
@@ -141,25 +128,19 @@
141
128
  }
142
129
 
143
130
  /*
144
- * Reconcile baked x-for/x-if clones the prerender kept for crawlers. Their
145
- * <template> is still live, so without intervention Alpine renders a second
146
- * copy on boot (a duplicate). data-hydrate islands keep their baked DOM.
131
+ * Reconcile baked x-for/x-if clones the prerender kept for crawlers. Their
132
+ * <template> is still live, so Alpine would render a duplicate on boot.
147
133
  *
148
- * - x-if clones are ADOPTED: we point the template's Alpine x-if state
149
- * (`_x_currentIfEl`) at the baked element, so Alpine treats the
150
- * condition as already-rendered and leaves the baked DOM in place. Heavy
151
- * x-if content (e.g. an x-markdown article) would otherwise be torn down
152
- * and re-rendered — flashing raw text and re-fetching $x data — even
153
- * though the prerendered output is already correct. A genuine
134
+ * - x-if clones are ADOPTED: point the template's `_x_currentIfEl` at the
135
+ * baked element so Alpine treats the condition as already-rendered and
136
+ * leaves it in place avoids tearing down heavy content (e.g. an
137
+ * x-markdown article) only to re-render identical output. A real
154
138
  * route/condition change later still re-renders normally.
155
- * - x-for clones are REMOVED: lists are light, often interactive, and
156
- * Alpine re-renders them from the live template, so the baked copies are
157
- * dropped to avoid a duplicate.
139
+ * - x-for clones are REMOVED: lists are light and re-render from the live
140
+ * template, so the baked copies are dropped to avoid a duplicate.
158
141
  *
159
- * Deliberately NOT part of hydratePrerenderedPage(): it runs at the last
160
- * safe moment — `alpine:init`, which Alpine dispatches after its script has
161
- * executed but BEFORE it walks the DOM. If Alpine never arrives (CDN
162
- * failure, offline), the listener never fires and the page keeps its
142
+ * Runs on `alpine:init` (after Alpine's script, before it walks the DOM),
143
+ * not in hydratePrerenderedPage: if Alpine never arrives the page keeps its
163
144
  * complete baked content.
164
145
  */
165
146
  function reconcilePrerenderClones() {
@@ -169,8 +150,7 @@
169
150
  el.removeAttribute('data-mnfst-prerender-clone');
170
151
  const tpl = el.previousElementSibling;
171
152
  if (tpl && tpl.tagName === 'TEMPLATE' && tpl.hasAttribute('x-if')) {
172
- // Adopt: Alpine's x-if `show()` returns early when _x_currentIfEl
173
- // is set, so the baked element stays and is not re-rendered.
153
+ // Alpine's x-if show() returns early when _x_currentIfEl is set.
174
154
  tpl._x_currentIfEl = el;
175
155
  } else {
176
156
  el.remove();
@@ -181,25 +161,18 @@
181
161
  document.addEventListener('alpine:init', reconcilePrerenderClones, { once: true });
182
162
  }
183
163
 
184
- // Run hydration BEFORE Alpine's deferred script executes.
185
- //
186
- // Timing: `<script defer>` runs AFTER HTML parsing finishes but BEFORE
187
- // `DOMContentLoaded` fires. So listening for DOMContentLoaded is too late
188
- // Alpine has already walked the tree and attached directives by then, and
189
- // our `replaceChild`-based restore would destroy the Alpine-bound nodes.
190
- //
191
- // The only earlier hook is `readystatechange → 'interactive'`, which is
192
- // dispatched the moment the parser finishes and BEFORE deferred scripts run.
193
- // We also run synchronously if readyState is already 'interactive' or later
194
- // (e.g. if manifest.js was injected dynamically after page load).
164
+ // Run hydration BEFORE Alpine's deferred script executes: DOMContentLoaded
165
+ // is too late (Alpine has already bound the nodes our replaceChild would
166
+ // destroy). The earlier `readystatechange 'interactive'` fires the moment
167
+ // parsing finishes, before deferred scripts. Run synchronously if already
168
+ // past 'loading' (e.g. manifest.js injected after load).
195
169
  function tryHydrate() {
196
170
  try { hydratePrerenderedPage(); } catch (e) { /* graceful */ }
197
171
  }
198
172
  if (typeof document !== 'undefined') {
199
173
  if (document.readyState === 'loading') {
200
- // We're still parsing. Listen for 'interactive' via readystatechange
201
- // this is the earliest moment document.body is guaranteed to exist
202
- // but deferred scripts haven't run yet.
174
+ // Still parsing: 'interactive' is the earliest hook where body exists
175
+ // but deferred scripts haven't run.
203
176
  let hydrated = false;
204
177
  document.addEventListener('readystatechange', () => {
205
178
  if (!hydrated && document.readyState !== 'loading') {
@@ -208,7 +181,7 @@
208
181
  }
209
182
  });
210
183
  } else {
211
- // Parser already done (interactive or complete). Hydrate immediately.
184
+ // Parser already done hydrate immediately.
212
185
  tryHydrate();
213
186
  }
214
187
  }
@@ -274,7 +247,7 @@
274
247
  // Plugin dependencies: plugins that require other plugins to be loaded first
275
248
  const PLUGIN_DEPENDENCIES = {
276
249
  'appwrite-data': ['data'],
277
- 'appwrite-presence': ['data']
250
+ 'appwrite-presence': ['data', 'appwrite-auth']
278
251
  };
279
252
 
280
253
  // Derive default plugin list from manifest (only load data/localization/components when manifest needs them)
@@ -306,16 +279,12 @@
306
279
  });
307
280
  }
308
281
 
309
- // Get plugin URL from CDN or the `data-plugin-base` override. When the
310
- // loader's <script> tag carries `data-plugin-base="/scripts"` (or an
311
- // absolute URL), plugins are loaded from that base as unminified `.js`
312
- // files. Otherwise they come from the jsDelivr CDN as `.min.js`.
282
+ // Plugin URL: `data-plugin-base` override → unminified `.js`; else the
283
+ // jsDelivr CDN as `.min.js`.
313
284
  let _pluginBase = null;
314
285
  function setPluginBase(b) { _pluginBase = b || null; }
315
286
  function getPluginUrl(pluginName, version = DEFAULT_VERSION) {
316
- // Map hyphenated plugin API names to their dotted file names.
317
- // `appwrite-auth` → `manifest.appwrite.auth.js`
318
- // `url-parameters` → `manifest.url.parameters.js`
287
+ // Hyphenated API name dotted file name (`appwrite-auth` → appwrite.auth).
319
288
  const fileName = pluginName.replace(/-/g, '.');
320
289
  if (_pluginBase) {
321
290
  const base = _pluginBase.replace(/\/$/, '');
@@ -332,10 +301,8 @@
332
301
  return `https://cdn.jsdelivr.net/npm/alpinejs@${dataAlpine}/dist/cdn.min.js`;
333
302
  }
334
303
 
335
- // Has DOMContentLoaded already fired? readyState alone can't tell:
336
- // 'interactive' covers both "deferred scripts still running" (DCL pending)
337
- // and "DCL done, subresources still loading". Disambiguate via the
338
- // navigation timing entry, which records the event the moment it runs.
304
+ // Has DOMContentLoaded fired? readyState can't tell ('interactive' spans
305
+ // both DCL-pending and DCL-done); the navigation timing entry disambiguates.
339
306
  function domContentLoadedFired() {
340
307
  if (document.readyState === 'complete') return true;
341
308
  if (document.readyState === 'loading') return false;
@@ -346,9 +313,8 @@
346
313
  return false;
347
314
  }
348
315
 
349
- // Run fn once the document's deferred scripts have all executed (i.e. at
350
- // or after DOMContentLoaded). The window 'load' listener is a belt-and-
351
- // braces fallback for environments where the navigation entry is missing.
316
+ // Run fn at or after DOMContentLoaded; 'load' is a fallback when the
317
+ // navigation entry is missing.
352
318
  function whenDomReady(fn) {
353
319
  if (domContentLoadedFired()) {
354
320
  fn();
@@ -360,28 +326,18 @@
360
326
  window.addEventListener('load', run, { once: true });
361
327
  }
362
328
 
363
- // Load Alpine.js from CDN. Called by the loader AFTER all plugin scripts
364
- // have finished loading and registered their directives/magics.
365
- //
366
- // Gated on DOMContentLoaded: the page's own deferred scripts register
367
- // x-data components and magics via `alpine:init`, and the defer queue
368
- // spins the event loop while a script is still in flight — so on a warm
369
- // cache an injected Alpine script can load and EXECUTE between two
370
- // deferred scripts, firing `alpine:init` before the page's registrations
371
- // exist. Waiting for DCL (which fires only after every deferred script
372
- // has run) makes the ordering deterministic. In the common cold-cache
373
- // case DCL has long passed by the time the plugin loads settle, so the
374
- // gate adds no delay.
329
+ // Load Alpine, called after all plugins have registered. Gated on DCL: a
330
+ // warm-cache Alpine could otherwise execute between two deferred scripts and
331
+ // fire `alpine:init` before the page's registrations exist. DCL fires only
332
+ // after every deferred script runs, making the order deterministic (and cold
333
+ // cache is already past DCL, so no added delay).
375
334
  function loadAlpine(alpineUrl = ALPINE_CDN_URL) {
376
335
  whenDomReady(() => {
377
- // Fast check: Alpine already initialized
378
336
  if (window.Alpine) {
379
337
  return;
380
338
  }
381
339
 
382
- // Fallback: if an existing Alpine <script> tag is already in the DOM
383
- // (e.g. the fixture explicitly added one), wait for it — don't inject
384
- // a second copy.
340
+ // Don't inject a second copy if an Alpine <script> is already present.
385
341
  const existingAlpine = document.querySelector('script[src*="alpinejs"]');
386
342
  if (existingAlpine) {
387
343
  return;
@@ -389,8 +345,7 @@
389
345
 
390
346
  const script = document.createElement('script');
391
347
  script.src = alpineUrl;
392
- // No `defer` — we're past plugin registration and past DCL, so
393
- // Alpine should load and execute as soon as it arrives.
348
+ // No `defer` — past DCL, so execute as soon as it arrives.
394
349
  document.head.appendChild(script);
395
350
  });
396
351
  }
@@ -423,11 +378,9 @@
423
378
  const resolved = [];
424
379
  const added = new Set();
425
380
 
426
- // Helper to add a plugin and its dependencies in correct order
427
381
  function addPluginWithDeps(plugin) {
428
382
  if (added.has(plugin)) return;
429
383
 
430
- // First, add all dependencies
431
384
  const deps = PLUGIN_DEPENDENCIES[plugin];
432
385
  if (deps) {
433
386
  for (const dep of deps) {
@@ -437,12 +390,10 @@
437
390
  }
438
391
  }
439
392
 
440
- // Then add the plugin itself
441
393
  resolved.push(plugin);
442
394
  added.add(plugin);
443
395
  }
444
396
 
445
- // Process all plugins in order, ensuring dependencies come first
446
397
  for (const plugin of pluginList) {
447
398
  addPluginWithDeps(plugin);
448
399
  }
@@ -470,7 +421,7 @@
470
421
  ))) {
471
422
  plugins.push('appwrite-data');
472
423
  }
473
- if (manifest.data?.presence?.appwriteTableId) {
424
+ if (manifest.appwrite?.presence) {
474
425
  plugins.push('appwrite-presence');
475
426
  }
476
427
  return plugins;
@@ -501,11 +452,8 @@
501
452
  const tailwind = script.getAttribute('data-tailwind') !== null;
502
453
  const version = script.getAttribute('data-version') || DEFAULT_VERSION;
503
454
  const alpine = script.getAttribute('data-alpine');
504
- // Optional override: when present, plugin URLs are resolved against
505
- // this base instead of the CDN. Useful for self-hosted deployments
506
- // and for the e2e harness which needs to load locally-built plugins.
507
- // The base should point at a directory that serves `manifest.<name>.js`
508
- // files. It can be relative (e.g. "/scripts") or absolute.
455
+ // Override: resolve plugin URLs against this base (dir serving
456
+ // `manifest.<name>.js`, relative or absolute) instead of the CDN.
509
457
  const pluginBase = script.getAttribute('data-plugin-base');
510
458
 
511
459
  let pluginList = [];
@@ -597,21 +545,12 @@
597
545
  ];
598
546
  const manifestUrl = (document.querySelector('link[rel="manifest"]')?.getAttribute('href')) || '/manifest.json';
599
547
 
600
- // Substitute ${VAR} placeholders against window.env in every string
601
- // value of the parsed manifest, in place. Called once before the
602
- // manifest is cached on window so every downstream consumer
603
- // (auth, data, components, etc.) sees resolved values. Inlined in
604
- // the loader rather than borrowed from the data plugin because the
605
- // data plugin's script may not have finished executing yet at the
606
- // point we cache the manifest. window.env is populated by either
607
- // the mnfst-run dev server (which reads PUBLIC_-prefixed vars from
608
- // .env at startup) or a developer-supplied
609
- // <script>window.env = {…}</script> block.
610
- //
611
- // Misses are warned, not silently dropped: a missing var almost
612
- // always means the dev forgot the PUBLIC_ prefix or hasn't set the
613
- // var at all, and an empty substitution downstream (e.g. an empty
614
- // API URL) tends to fail far from the cause.
548
+ // Substitute ${VAR} placeholders against window.env in-place, once,
549
+ // before caching the manifest so every consumer sees resolved values.
550
+ // Inlined (not borrowed from the data plugin, which may not have run
551
+ // yet). window.env comes from mnfst-run (.env PUBLIC_ vars) or an
552
+ // author <script>window.env = {…}</script>. Misses are warned, not
553
+ // dropped an empty substitution tends to fail far from the cause.
615
554
  const warnedMissingEnv = new Set();
616
555
  const interpolateManifestEnv = (obj) => {
617
556
  if (obj === null || typeof obj !== 'object') return;
@@ -687,11 +626,8 @@
687
626
  manifest = await manifestPromise;
688
627
  }
689
628
  if (manifest && typeof window !== 'undefined') {
690
- // Resolve ${VAR} placeholders once, here, before any
691
- // downstream plugin reads the cached manifest. Plugins like
692
- // appwrite-auth read window.__manifestLoaded directly and
693
- // would otherwise see literal `${APPWRITE_DEV_KEY}` strings
694
- // even when window.env is populated.
629
+ // Resolve ${VAR} before any plugin reads the cached manifest —
630
+ // appwrite-auth etc. read window.__manifestLoaded directly.
695
631
  interpolateManifestEnv(manifest);
696
632
  window.__manifestLoaded = manifest;
697
633
  if (window.ManifestComponentsRegistry) {