mnfst 0.5.203 → 0.5.205

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
@@ -187,6 +187,11 @@
187
187
  // Configuration
188
188
  const DEFAULT_VERSION = 'latest';
189
189
 
190
+ // The version the page booted with (data-version). Every later load —
191
+ // Manifest.loadPlugin(), usage sniffing — resolves against it, so a runtime
192
+ // load can't pull a second copy of the framework at another version.
193
+ let RESOLVED_VERSION = DEFAULT_VERSION;
194
+
190
195
  // CDN fallback chain (first-party first). Each origin serves the npm scheme
191
196
  // `<origin>/<pkg>@<version>/<path>`. Override order with `data-cdn`
192
197
  // (comma-separated origins).
@@ -298,7 +303,7 @@
298
303
  // candidate per CDN host, tried in order.
299
304
  let _pluginBase = null;
300
305
  function setPluginBase(b) { _pluginBase = b || null; }
301
- function getPluginUrlCandidates(pluginName, version = DEFAULT_VERSION) {
306
+ function getPluginUrlCandidates(pluginName, version = RESOLVED_VERSION) {
302
307
  // Hyphenated API name → dotted file name (`appwrite-auth` → appwrite.auth).
303
308
  const fileName = pluginName.replace(/-/g, '.');
304
309
  if (_pluginBase) {
@@ -307,7 +312,7 @@
307
312
  }
308
313
  return CDN_HOSTS.map(h => `${getBaseUrl(version, h)}/${hostFile(h, `manifest.${fileName}.min.js`)}`);
309
314
  }
310
- function getPluginUrl(pluginName, version = DEFAULT_VERSION) {
315
+ function getPluginUrl(pluginName, version = RESOLVED_VERSION) {
311
316
  return getPluginUrlCandidates(pluginName, version)[0];
312
317
  }
313
318
 
@@ -388,14 +393,8 @@
388
393
  return false;
389
394
  }
390
395
 
391
- // One fetch per URL: concurrent callers share the first call's promise
392
- const inflightScripts = new Map();
393
-
394
396
  function injectScript(url) {
395
- const shared = inflightScripts.get(url);
396
- if (shared) return shared;
397
-
398
- const promise = new Promise((resolve, reject) => {
397
+ return new Promise((resolve, reject) => {
399
398
  // Same src already in the DOM (author tag, prerendered HTML, second loader run)
400
399
  const existing = document.querySelector(`script[src="${url}"]`);
401
400
  if (existing) {
@@ -418,16 +417,12 @@
418
417
  script.onerror = () => { script.removeAttribute('data-mnfst-loading'); script.remove(); reject(new Error(`Failed to load ${url}`)); };
419
418
  document.head.appendChild(script);
420
419
  });
421
-
422
- inflightScripts.set(url, promise);
423
- promise.then(() => inflightScripts.delete(url), () => inflightScripts.delete(url)); // dedupe only while in flight
424
- return promise;
425
420
  }
426
421
 
427
422
  // Load a plugin, falling through the CDN chain on error. A fallback insert
428
423
  // lands after already-inserted scripts, so strict cross-plugin execution
429
424
  // order is traded for availability in the (already degraded) fallback case.
430
- async function addScript(pluginName, version = DEFAULT_VERSION) {
425
+ async function fetchPluginScript(pluginName, version) {
431
426
  const urls = getPluginUrlCandidates(pluginName, version);
432
427
  let lastErr = null;
433
428
  for (const url of urls) {
@@ -441,6 +436,26 @@
441
436
  throw lastErr || new Error(`Failed to load ${pluginName}`);
442
437
  }
443
438
 
439
+ // One execution per plugin per page. injectScript dedupes by URL, which two
440
+ // versions of the same plugin slip past — and a second copy of a plugin that
441
+ // owns page-level state (utilities' style element and its order observer)
442
+ // fights the first. A failed load is forgotten so a retry can still run.
443
+ const pluginLoads = new Map();
444
+
445
+ async function addScript(pluginName, version = RESOLVED_VERSION) {
446
+ const prior = pluginLoads.get(pluginName);
447
+ if (prior) {
448
+ if (prior.version !== version) {
449
+ console.warn(`[Manifest Loader] ${pluginName} already loaded at ${prior.version} — ignoring request for ${version}`);
450
+ }
451
+ return prior.promise;
452
+ }
453
+ const promise = fetchPluginScript(pluginName, version);
454
+ pluginLoads.set(pluginName, { version, promise });
455
+ promise.catch(() => pluginLoads.delete(pluginName));
456
+ return promise;
457
+ }
458
+
444
459
  // Resolve plugin dependencies (auto-inject required dependencies)
445
460
  function resolveDependencies(pluginList) {
446
461
  const resolved = [];
@@ -635,8 +650,26 @@
635
650
  };
636
651
  }
637
652
 
653
+ // A publish/render bake stamps `data-mnfst-utilities-complete` on its
654
+ // `<style data-mnfst-utilities>` sheet once it has verified every class
655
+ // scanned from the page's own HTML got a rule (Manifest's + the baked
656
+ // Tailwind pass — see compileUtilities in manifest.utilities.node.mjs).
657
+ // Only the inline `<style>` case is checked: it's synchronous, so this can
658
+ // run before deciding whether to fetch the Tailwind engine at all — a
659
+ // `<link>` sheet would need an async load first, so it's left alone
660
+ // (fail open: Tailwind still loads). No flag, no attribute, any read
661
+ // error → false, so this never skips loading the engine speculatively.
662
+ function staticUtilitiesFullyCovered() {
663
+ try {
664
+ const el = document.querySelector('style[data-mnfst-utilities][data-mnfst-utilities-complete]');
665
+ return !!el;
666
+ } catch (e) {
667
+ return false;
668
+ }
669
+ }
670
+
638
671
  // Load custom Tailwind CDN script, falling through the CDN chain on error
639
- async function loadTailwind(version = DEFAULT_VERSION) {
672
+ async function loadTailwind(version = RESOLVED_VERSION) {
640
673
  const urls = CDN_HOSTS.map(h => `${getBaseUrl(version, h)}/${hostFile(h, 'manifest.tailwind.min.js')}`);
641
674
  let lastErr = null;
642
675
  for (const url of urls) {
@@ -652,7 +685,7 @@
652
685
 
653
686
  // Expose API
654
687
  window.Manifest = {
655
- loadPlugin: function (pluginName, version = DEFAULT_VERSION) {
688
+ loadPlugin: function (pluginName, version = RESOLVED_VERSION) {
656
689
  const allPlugins = [...AVAILABLE_PLUGINS, ...APPWRITE_PLUGINS, ...AUTHORING_PLUGINS, 'payments', 'chat', 'device'];
657
690
  if (!allPlugins.includes(pluginName)) {
658
691
  console.warn(`[Manifest Loader] Unknown plugin: ${pluginName}`);
@@ -780,6 +813,7 @@
780
813
 
781
814
  // Parse config and load plugins
782
815
  const config = parseDataAttributes();
816
+ if (config && config.version) RESOLVED_VERSION = config.version;
783
817
  if (config && config.pluginBase) setPluginBase(config.pluginBase);
784
818
  if (config && config.cdn) setCdnHosts(config.cdn);
785
819
  armServiceWorker(config);
@@ -890,7 +924,7 @@
890
924
  console.warn(`[Manifest Loader] Failed to load plugin ${pluginName}:`, error);
891
925
  });
892
926
  });
893
- if (config.tailwind) {
927
+ if (config.tailwind && !staticUtilitiesFullyCovered()) {
894
928
  pluginPromises.push(loadTailwind(config.version).catch(() => { }));
895
929
  }
896
930
  await Promise.all(pluginPromises);
@@ -144,6 +144,20 @@
144
144
  { "type": "array", "items": { "type": "string", "enum": ["user", "team"] } }
145
145
  ]
146
146
  },
147
+ "scopeColumn": {
148
+ "description": "Column name(s) `scope` filters/writes on. Default teamId/userId. A string sets both; an object sets them independently.",
149
+ "oneOf": [
150
+ { "type": "string" },
151
+ {
152
+ "type": "object",
153
+ "properties": {
154
+ "team": { "type": "string" },
155
+ "user": { "type": "string" }
156
+ },
157
+ "additionalProperties": false
158
+ }
159
+ ]
160
+ },
147
161
  "autoInjectUserId": { "type": "boolean" },
148
162
  "autoInjectTeamId": { "type": "boolean" },
149
163
  "queries": {
@@ -16,7 +16,7 @@
16
16
 
17
17
  // Build stamps the published version here; unstamped (dev) falls back to the
18
18
  // registration URL's `v`, then 'dev'.
19
- const BUILD_VERSION = '0.5.203';
19
+ const BUILD_VERSION = '0.5.205';
20
20
  const CACHE_PREFIX = 'mnfst-sw:';
21
21
  const META_CACHE = CACHE_PREFIX + 'meta';
22
22
  const SWR_CAP = 200;
@@ -558,8 +558,10 @@ class TailwindCompiler {
558
558
 
559
559
  // Read any publish/render-provided static utilities sheet before the
560
560
  // first compile — its classes are skipped rather than regenerated.
561
+ // Fails open (never "everything covered") until this resolves —
562
+ // compile()'s first run awaits it, capped at 2s (see static.js).
561
563
  this.staticUtilitiesCoveredClasses = null;
562
- this.detectStaticUtilitiesSheet();
564
+ this.staticUtilitiesReady = this.detectStaticUtilitiesSheet();
563
565
 
564
566
  // Add critical styles IMMEDIATELY - don't wait for anything
565
567
  this.addCriticalBlockingStylesSync();
@@ -1488,9 +1490,12 @@ TailwindCompiler.prototype.scanStaticClasses = async function () {
1488
1490
 
1489
1491
  // Extract classes from HTML content
1490
1492
  TailwindCompiler.prototype.extractClassesFromHTML = function (html, classSet) {
1491
- // Plain class attributes only — the leading boundary excludes Alpine's
1492
- // :class / x-bind:class bindings (JS expressions, not token lists)
1493
- const classRegex = /(?:^|[\s"'`>/])(?:[a-zA-Z][\w.-]*-)?class=["']([^"']+)["']/g;
1493
+ // Literal class="..."/class='...' only — lookbehind excludes `:class=`
1494
+ // (Alpine binding), whose "class=" substring would otherwise match and
1495
+ // truncate at the literal's first internal quote, leaking a stray '{'.
1496
+ // Whitespace-only split below keeps every token Tailwind would accept
1497
+ // (variants, arbitrary values, leading '-', '!'); only x-/$ tokens drop.
1498
+ const classRegex = /(?<![:\w])class=["']([^"']+)["']/g;
1494
1499
  let match;
1495
1500
 
1496
1501
  while ((match = classRegex.exec(html)) !== null) {
@@ -1701,8 +1706,11 @@ TailwindCompiler.prototype.fetchThemeContent = async function () {
1701
1706
  TailwindCompiler.prototype.extractThemeVariables = function (cssText) {
1702
1707
  const variables = new Map();
1703
1708
 
1704
- // Extract ALL CSS custom properties from ANY declaration block
1705
- const varRegex = /--([\w-]+):\s*([^;]+);/g;
1709
+ // Extract ALL CSS custom properties from ANY declaration block. Terminator
1710
+ // is a lookahead (`;` or `}`) rather than a consumed `;` so the last
1711
+ // declaration in a block still matches when the author omits the
1712
+ // trailing semicolon (valid CSS, e.g. `:root{--x:1rem}`).
1713
+ const varRegex = /--([\w-]+):\s*([^;}]+)(?=[;}])/g;
1706
1714
 
1707
1715
  let varMatch;
1708
1716
  while ((varMatch = varRegex.exec(cssText)) !== null) {
@@ -3180,6 +3188,11 @@ TailwindCompiler.prototype.compile = async function () {
3180
3188
  if (!this.hasScannedStatic) {
3181
3189
  await this.scanStaticClasses();
3182
3190
 
3191
+ // Wait for the static utilities sheet's covered-class read to
3192
+ // settle (capped at 2s in detectStaticUtilitiesSheet) so this
3193
+ // first compile decision isn't a guess — see static.js.
3194
+ if (this.staticUtilitiesReady) await this.staticUtilitiesReady;
3195
+
3183
3196
  // Fetch CSS content once for initial compilation
3184
3197
  const themeCss = await this.fetchThemeContent();
3185
3198
  if (themeCss) {
@@ -3377,6 +3390,14 @@ TailwindCompiler.prototype.extractSelectorsFromCssText = function (cssText) {
3377
3390
  const len = cssText.length;
3378
3391
  let i = 0;
3379
3392
  while (i < len) {
3393
+ // Skip inter-rule whitespace first — without this, whitespace before an
3394
+ // `@layer`/`@media` (e.g. after a preceding `@layer base, ...;` statement
3395
+ // or sibling rule) leaves `i` off the '@', so the block below never fires
3396
+ // and the whole nested block is swallowed whole as one bogus selector,
3397
+ // losing every class inside it (real Tailwind/compileUtilities output is
3398
+ // always `@layer theme {...}` followed by `@layer utilities {...}`).
3399
+ while (i < len && /\s/.test(cssText[i])) i++;
3400
+ if (i >= len) break;
3380
3401
  if (cssText[i] === '/' && cssText[i + 1] === '*') {
3381
3402
  const end = cssText.indexOf('*/', i + 2);
3382
3403
  i = end === -1 ? len : end + 2;
@@ -3429,40 +3450,70 @@ TailwindCompiler.prototype.classNamesFromCssText = function (cssText) {
3429
3450
  return classSet;
3430
3451
  };
3431
3452
 
3432
- // Read the static sheet's covered classes once. Same-origin `<link>`/`<style>`
3433
- // rules are read synchronously via CSSOM; if the sheet isn't accessible yet
3434
- // (cross-origin, or not finished loading) this falls back to fetching the
3435
- // `href` as text and re-populating the covered set when it resolves.
3453
+ // Attempt a synchronous CSSOM read of the static link's rules. Returns a Set
3454
+ // of covered classes, or null when nothing could be read — a security error
3455
+ // (cross-origin), an exception, a missing sheet, or zero rules (the link
3456
+ // hasn't finished loading, or its sheet is genuinely empty). null is never
3457
+ // distinguished from "empty": both fail open in filterStaticallyCoveredClasses
3458
+ // / stripCoveredRulesFromCss, and neither is ever treated as "everything
3459
+ // covered".
3460
+ TailwindCompiler.prototype.readStaticUtilitiesRules = function (el) {
3461
+ try {
3462
+ const sheet = Array.from(document.styleSheets).find(s => s.ownerNode === el);
3463
+ const rules = sheet && sheet.cssRules;
3464
+ if (rules && rules.length > 0) {
3465
+ const cssText = Array.from(rules).map(r => r.cssText).join('\n');
3466
+ return this.classNamesFromCssText(cssText);
3467
+ }
3468
+ } catch (e) {
3469
+ // Cross-origin, or not yet parsed.
3470
+ }
3471
+ return null;
3472
+ };
3473
+
3474
+ // Read the static sheet's covered classes once per page load. `<style>` and
3475
+ // an already-loaded `<link>` resolve synchronously. A `<link>` that hasn't
3476
+ // finished loading (or whose sheet can't be read yet) is awaited via its
3477
+ // `load`/`error` event, capped at 2s, then re-read once more; the caller
3478
+ // (compile()'s first run) awaits the returned promise so the very first
3479
+ // compile decision sees an accurate covered set rather than a guess. Fails
3480
+ // open the whole time — staticUtilitiesCoveredClasses stays null (nothing
3481
+ // covered) until a read actually succeeds, never cached as "covered".
3436
3482
  TailwindCompiler.prototype.detectStaticUtilitiesSheet = function () {
3437
3483
  this.staticUtilitiesCoveredClasses = null;
3438
3484
  const el = this.findStaticUtilitiesElement();
3439
- if (!el) return;
3485
+ if (!el) return Promise.resolve();
3440
3486
 
3441
3487
  if (el.tagName === 'STYLE') {
3442
3488
  this.staticUtilitiesCoveredClasses = this.classNamesFromCssText(el.textContent || '');
3443
- return;
3489
+ return Promise.resolve();
3444
3490
  }
3445
3491
 
3446
- try {
3447
- const sheet = Array.from(document.styleSheets).find(s => s.ownerNode === el);
3448
- const rules = sheet && sheet.cssRules;
3449
- if (rules && rules.length > 0) {
3450
- const cssText = Array.from(rules).map(r => r.cssText).join('\n');
3451
- this.staticUtilitiesCoveredClasses = this.classNamesFromCssText(cssText);
3452
- return;
3453
- }
3454
- } catch (e) {
3455
- // Cross-origin (or not yet parsed) — fall through to fetch.
3492
+ const covered = this.readStaticUtilitiesRules(el);
3493
+ if (covered) {
3494
+ this.staticUtilitiesCoveredClasses = covered;
3495
+ return Promise.resolve();
3456
3496
  }
3457
3497
 
3458
- // Placeholder so callers treat "not yet known" as "nothing covered" rather
3459
- // than null, until the fetch below resolves and replaces it.
3460
- this.staticUtilitiesCoveredClasses = new Set();
3461
- const href = el.getAttribute('href');
3462
- if (!href || typeof fetch !== 'function') return;
3463
- fetch(href).then(r => (r.ok ? r.text() : '')).then(text => {
3464
- if (text) this.staticUtilitiesCoveredClasses = this.classNamesFromCssText(text);
3465
- }).catch(() => {});
3498
+ return new Promise((resolve) => {
3499
+ let settled = false;
3500
+ const finish = () => {
3501
+ if (settled) return;
3502
+ settled = true;
3503
+ clearTimeout(timer);
3504
+ el.removeEventListener('load', onLoad);
3505
+ el.removeEventListener('error', onError);
3506
+ // staticUtilitiesCoveredClasses stays null (fail open) if this
3507
+ // still can't be read — e.g. a cross-origin sheet without CORS.
3508
+ this.staticUtilitiesCoveredClasses = this.readStaticUtilitiesRules(el);
3509
+ resolve();
3510
+ };
3511
+ const onLoad = () => finish();
3512
+ const onError = () => finish();
3513
+ const timer = setTimeout(finish, 2000);
3514
+ el.addEventListener('load', onLoad, { once: true });
3515
+ el.addEventListener('error', onError, { once: true });
3516
+ });
3466
3517
  };
3467
3518
 
3468
3519
  // Drop classes already covered by the static sheet before generating rules
@@ -3486,6 +3537,12 @@ TailwindCompiler.prototype.stripCoveredRulesFromCss = function (cssText) {
3486
3537
  const len = text.length;
3487
3538
  let i = 0;
3488
3539
  while (i < len) {
3540
+ // Skip (but preserve) inter-rule whitespace before checking for an
3541
+ // at-rule — see extractSelectorsFromCssText for why this matters.
3542
+ const wsStart = i;
3543
+ while (i < len && /\s/.test(text[i])) i++;
3544
+ if (i > wsStart) out.push(text.slice(wsStart, i));
3545
+ if (i >= len) break;
3489
3546
  if (text[i] === '/' && text[i + 1] === '*') {
3490
3547
  const end = text.indexOf('*/', i + 2);
3491
3548
  const stop = end === -1 ? len : end + 2;
@@ -3831,8 +3888,13 @@ function injectTailwindVariants() {
3831
3888
  }
3832
3889
  injectTailwindVariants();
3833
3890
 
3834
- // Initialize immediately without waiting for DOMContentLoaded
3835
- const compiler = new TailwindCompiler();
3891
+ // One compiler per page: a second copy would own a rival #manifest-styles and
3892
+ // the two order observers would re-append their own style to <head> forever.
3893
+ // A compiler whose style element left the document is stale, so it is rebuilt.
3894
+ const existingCompiler = window.ManifestUtilities;
3895
+ const compiler = (existingCompiler && existingCompiler.styleElement && existingCompiler.styleElement.isConnected)
3896
+ ? existingCompiler
3897
+ : new TailwindCompiler();
3836
3898
 
3837
3899
  // Expose utilities compiler for optional integration
3838
3900
  window.ManifestUtilities = compiler;
@@ -3856,11 +3918,14 @@ if ('PerformanceObserver' in window) {
3856
3918
  }
3857
3919
 
3858
3920
  // Recompile on DOMContentLoaded for late-added elements
3859
- document.addEventListener('DOMContentLoaded', () => {
3860
- if (!compiler.usesStaticPrerenderUtilities && !compiler.isCompiling) {
3861
- compiler.compile();
3862
- }
3863
- });
3921
+ if (!compiler.__mnfstDomReadyBound) {
3922
+ compiler.__mnfstDomReadyBound = true;
3923
+ document.addEventListener('DOMContentLoaded', () => {
3924
+ if (!compiler.usesStaticPrerenderUtilities && !compiler.isCompiling) {
3925
+ compiler.compile();
3926
+ }
3927
+ });
3928
+ }
3864
3929
 
3865
3930
 
3866
3931