mnfst 0.5.205 → 0.5.207

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  (function () {
4
4
 
5
- const MANIFEST_BUILD_VERSION = '0.5.205';
5
+ const MANIFEST_BUILD_VERSION = '0.5.207';
6
6
 
7
7
  /* Manifest Data Sources - Appwrite Integration */
8
8
 
package/lib/manifest.d.ts CHANGED
@@ -510,6 +510,17 @@ declare global {
510
510
  Alpine?: unknown;
511
511
  __manifestLoaded?: unknown;
512
512
  }
513
+
514
+ /**
515
+ * Fired when the utilities runtime sees a class the baked sheet (and
516
+ * `manifest.json`'s `utilities.safelist`/`patterns`) doesn't cover, after
517
+ * publish stamped that sheet as covering every class — see
518
+ * manifest.utilities.static.js. `engineLoaded` is true once the lazily
519
+ * loaded Tailwind engine has taken over, false if that load failed.
520
+ */
521
+ interface WindowEventMap {
522
+ 'manifest:utilities-uncovered': CustomEvent<{ classes: string[]; engineLoaded: boolean }>;
523
+ }
513
524
  }
514
525
 
515
526
  // ---------------------------------------------------------------------------
@@ -2,7 +2,7 @@
2
2
 
3
3
  (function () {
4
4
 
5
- const MANIFEST_BUILD_VERSION = '0.5.205';
5
+ const MANIFEST_BUILD_VERSION = '0.5.207';
6
6
 
7
7
  /* Manifest Data Sources - Configuration */
8
8
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifest.appwrite.auth.js": "sha384-HMJmRXnvdDBWyuteXL+DTxWM2llSCdjl4EDamEI7iHNAVlqeOCE4GDTm8ZHipPRj",
3
- "manifest.appwrite.data.js": "sha384-SNLZhDdZFodPsMINThHOZ/rPyLqV/hRyvI/lMxsHCctKB+TI4HdlLt9USnaKFKnG",
3
+ "manifest.appwrite.data.js": "sha384-/1T3gJYo2B7Ct4s24VFKgle5LpTdnZ2kWrMReqamsSzUg1nCqwOjS7T8Z/qgJSoV",
4
4
  "manifest.appwrite.presences.js": "sha384-JyVDWFT69b7Svq8J8p9tNWe2v9PZNQBY5UyhtOl1vnuHFAX0CRQ3MA9SiKq5wzNh",
5
5
  "manifest.bindings.js": "sha384-lPtMf8Ykaic9gNVZKq8Scia1881ZeMAyxer3WCIkUNwwrgzGBWto1OXADUwjvORF",
6
6
  "manifest.charts.js": "sha384-qQrjhWkIDi4oU5uQrpy8DdlMjY+gfiivBuX6mssNiFdkawgkVBvKneGgp+nd3PYJ",
@@ -11,7 +11,7 @@
11
11
  "manifest.combobox.js": "sha384-SN3XHumoebxcduFQqx5zt65/Cd10y8nuf9Gkco6UnsZ42sadRd+gvVLfWPdkkuPH",
12
12
  "manifest.components.js": "sha384-DLGSqCbnGkS87AWJ8GpKoAFP4nCosgC5SFSg6C10hH/nWu/MxI+0ntHlnFEfs++u",
13
13
  "manifest.computed.js": "sha384-GYML/e81gQfN+iDbqhPUgny8bINCyL+SrbTofoGk4hlCnLLjv51mCv42JMqq+ku5",
14
- "manifest.data.js": "sha384-pCz9N7amtpL1KG/Z/3ZbGRPqof5K/pXwjK4NgB7MxBPg4efzFp0MlsQtJpjRnoNd",
14
+ "manifest.data.js": "sha384-dq1Hp751qrHoMVX1rDGjj09KkiKb3jzrbW6rCMdwzPWKjzoIQT22InmU6QVP0JhO",
15
15
  "manifest.datepicker.js": "sha384-8vTJYkLqqvgXgm1w1becuaRR/jUEpiLBjBJ6RsRbUr+ZkypvspD6EsssuDrX4SQq",
16
16
  "manifest.defer.js": "sha384-wXa60M2OHigi5rBurXQ7Oa8eyr5WTKs7EUn2GOru62KMdfs/LDyNegxCc9MNJFSe",
17
17
  "manifest.device.js": "sha384-g+txmrEszYtuOvRelTp/c1wSNR2AtllVI9rx5NPPD7DzhVUnMNughonZykv2xlBW",
@@ -28,14 +28,14 @@
28
28
  "manifest.slides.js": "sha384-3uRTkyK9XPLmnxI2+igZlpi4EyPlU/7IHj5j3BZJJ2KN455vXyk99fiXV3feO/XY",
29
29
  "manifest.status.js": "sha384-nwvUSbT+gpRMqhYx6MeBYswUMH72rY92mjgxv0vrSG7Ht7l83XYpEB6fCTbXjjfK",
30
30
  "manifest.svg.js": "sha384-qr/8H9+2cIhSXNbRWRtIdrDlTOxKlNJqwEefHvCYoiBNW/kKhPTFa109Orv/EBnb",
31
- "manifest.sw.js": "sha384-f+wBpbAvoE4WwJXkMiFJ4pN/UDVdsmEBX7Uav3SoycmUXzgZDz+U/39UWXtLaItm",
31
+ "manifest.sw.js": "sha384-a4rb/5qNtjrIQEP6oMusHWeJVtWZ9LIXR/uIpBholtH1808et2NibzgtM1GTEpgq",
32
32
  "manifest.tabs.js": "sha384-OIIcyaQnGS0eNYk4zgAFEDboNYect7THvUd9D0V6nqCeDOxQcoiqLrBEmmWVTTet",
33
33
  "manifest.tailwind.js": "sha384-r/Jvqb+Piz21jUts10BbVAJoKkShOdJRVFpywyi4HPIi/OfOJq5CtPyWAPbdkMgt",
34
34
  "manifest.text.edit.js": "sha384-pfhc4IPKNJokj6ldlgD+KwGLN4UoM21CNeVf2sVq/34X1Un2PUhEuyTg5itmtsXS",
35
35
  "manifest.toasts.js": "sha384-/OV32LLMJl0Iw8NoNQJ+5lgntLxjIJq/YNj5JAKS/+nUhVx4MMB9h1FOqP7zuj2M",
36
36
  "manifest.tooltips.js": "sha384-uXVVQADl6pY2Pj9i/UaSclZN7dzbKhLkmevv7Ad0+wXR8LgqodALNFrc5jZJjlwm",
37
37
  "manifest.url.parameters.js": "sha384-J2aSVBOPsoqsbafHndrWLHEDpKb4AKJAoQnVmKA4gZh5P/8HHjFWHO/lkusmG84l",
38
- "manifest.utilities.js": "sha384-Dtpf9qskRUYB4bLuJaCBGWYFENVypV5AxlLFPdi7S7+meaGeupzD2zkDr7CYhvXf",
38
+ "manifest.utilities.js": "sha384-pIKcTvk9lrlXZK0OXld6SA6wrmlaFTCmTpLn67nOCE+XhNw4fuGSDX4Z3C2eR6CJ",
39
39
  "manifest.virtual.js": "sha384-YwwmAsZNddajnAkLhBWtrK8edHMJOhD7cJkQi3qmm5o1Thg0wbtDU0dsfrCHKTIQ",
40
- "manifest.js": "sha384-jH0veyUdnBMoGwHxgUQ9Yjs0UneDMI/fYIsg1U58YPZwriKJ6Rv/aoA7RbLepoGa"
40
+ "manifest.js": "sha384-fyktZSCaKJ267Hj5ORpbS/Dk11vtT+J8BN1XkA8zG+kXG1wPDwcA1hmB/40fY5Pq"
41
41
  }
package/lib/manifest.js CHANGED
@@ -686,7 +686,11 @@
686
686
  // Expose API
687
687
  window.Manifest = {
688
688
  loadPlugin: function (pluginName, version = RESOLVED_VERSION) {
689
- const allPlugins = [...AVAILABLE_PLUGINS, ...APPWRITE_PLUGINS, ...AUTHORING_PLUGINS, 'payments', 'chat', 'device'];
689
+ // 'tailwind' isn't a core/Appwrite/authoring plugin — it's the Play-CDN
690
+ // browser engine (lib/manifest.tailwind.js) — but the utilities plugin's
691
+ // uncovered-class watcher lazily loads it this way (see
692
+ // manifest.utilities.static.js setupUncoveredClassWatcher).
693
+ const allPlugins = [...AVAILABLE_PLUGINS, ...APPWRITE_PLUGINS, ...AUTHORING_PLUGINS, 'payments', 'chat', 'device', 'tailwind'];
690
694
  if (!allPlugins.includes(pluginName)) {
691
695
  console.warn(`[Manifest Loader] Unknown plugin: ${pluginName}`);
692
696
  return Promise.reject(new Error(`Unknown plugin: ${pluginName}`));
@@ -431,6 +431,23 @@
431
431
  "type": "object",
432
432
  "description": "Deprecated alias of `device` (renamed in 0.5.199); still accepted.",
433
433
  "additionalProperties": true
434
+ },
435
+ "utilities": {
436
+ "type": "object",
437
+ "description": "Utilities-plugin overrides for classes a plain HTML/component scan can't find on its own — e.g. built from a runtime value (`:class=\"ok ? 'bg-green-500' : 'bg-amber-500'\"`). `safelist` classes are baked into the static utilities sheet (scanClasses/compileUtilities) and treated as covered by the runtime; `patterns` classes are trusted as covered by the runtime only (a regex can't be baked).",
438
+ "properties": {
439
+ "safelist": {
440
+ "type": "array",
441
+ "items": { "type": "string" },
442
+ "description": "Exact utility class names, e.g. [\"bg-amber-500\", \"bg-green-500\"]."
443
+ },
444
+ "patterns": {
445
+ "type": "array",
446
+ "items": { "type": "string" },
447
+ "description": "Regex source strings matched against each class token, e.g. \"^bg-(red|green|blue)-[0-9]+$\"."
448
+ }
449
+ },
450
+ "additionalProperties": false
434
451
  }
435
452
  },
436
453
  "definitions": {
@@ -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.205';
19
+ const BUILD_VERSION = '0.5.207';
20
20
  const CACHE_PREFIX = 'mnfst-sw:';
21
21
  const META_CACHE = CACHE_PREFIX + 'meta';
22
22
  const SWR_CAP = 200;
@@ -556,12 +556,19 @@ class TailwindCompiler {
556
556
  // Cache for parsed class names (must be before addCriticalBlockingStylesSync)
557
557
  this.classCache = new Map();
558
558
 
559
- // Read any publish/render-provided static utilities sheet before the
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).
559
+ // Read any publish/render-provided static utilities sheet, plus
560
+ // manifest.json's utilities.safelist/patterns, before the first
561
+ // compile — those classes are skipped rather than regenerated. Fails
562
+ // open (never "everything covered") until this resolves — compile()'s
563
+ // first run awaits it, capped at 2s (see static.js). Once settled,
564
+ // arm the uncovered-class watcher (a no-op unless the loader actually
565
+ // skipped fetching the Tailwind engine for this page).
563
566
  this.staticUtilitiesCoveredClasses = null;
564
- this.staticUtilitiesReady = this.detectStaticUtilitiesSheet();
567
+ this.staticUtilitiesReady = Promise.all([
568
+ this.detectStaticUtilitiesSheet(),
569
+ this.loadUtilitiesSafelist()
570
+ ]);
571
+ this.staticUtilitiesReady.then(() => this.setupUncoveredClassWatcher());
565
572
 
566
573
  // Add critical styles IMMEDIATELY - don't wait for anything
567
574
  this.addCriticalBlockingStylesSync();
@@ -1493,13 +1500,17 @@ TailwindCompiler.prototype.extractClassesFromHTML = function (html, classSet) {
1493
1500
  // Literal class="..."/class='...' only — lookbehind excludes `:class=`
1494
1501
  // (Alpine binding), whose "class=" substring would otherwise match and
1495
1502
  // truncate at the literal's first internal quote, leaking a stray '{'.
1503
+ // Backreference to the opening quote (rather than stopping at either
1504
+ // quote char) so an arbitrary Tailwind value containing the OTHER quote
1505
+ // character — `content-['*']`, `content-['']` — survives instead of
1506
+ // truncating the whole class list at its first `'`.
1496
1507
  // Whitespace-only split below keeps every token Tailwind would accept
1497
1508
  // (variants, arbitrary values, leading '-', '!'); only x-/$ tokens drop.
1498
- const classRegex = /(?<![:\w])class=["']([^"']+)["']/g;
1509
+ const classRegex = /(?<![:\w])class=(["'])((?:(?!\1)[\s\S])*)\1/g;
1499
1510
  let match;
1500
1511
 
1501
1512
  while ((match = classRegex.exec(html)) !== null) {
1502
- const classString = match[1];
1513
+ const classString = match[2];
1503
1514
  const classes = classString.split(/\s+/).filter(Boolean);
1504
1515
  for (const cls of classes) {
1505
1516
  if (cls && !cls.startsWith('x-') && !cls.startsWith('$')) {
@@ -1517,7 +1528,11 @@ TailwindCompiler.prototype.extractClassesFromHTML = function (html, classSet) {
1517
1528
  if (classMatches) {
1518
1529
  for (const classMatch of classMatches) {
1519
1530
  const cls = classMatch.replace(/['"`]/g, '');
1520
- if (cls && !cls.startsWith('$') && !cls.includes('(')) {
1531
+ // x-data is arbitrary JS — a plain string literal in there
1532
+ // (e.g. `location.pathname.split('/')`) matches this quoted-
1533
+ // token scan too, so require a letter/digit; a punctuation-only
1534
+ // "token" like a bare '/' is skipped (seen on a live page).
1535
+ if (cls && !cls.startsWith('$') && !cls.includes('(') && /[a-zA-Z0-9]/.test(cls)) {
1521
1536
  classSet.add(cls);
1522
1537
  }
1523
1538
  }
@@ -3390,6 +3405,7 @@ TailwindCompiler.prototype.extractSelectorsFromCssText = function (cssText) {
3390
3405
  const len = cssText.length;
3391
3406
  let i = 0;
3392
3407
  while (i < len) {
3408
+ if (cssText[i] === ' ' || cssText[i] === '\n' || cssText[i] === '\r' || cssText[i] === '\t') { i++; continue; } // else an @-rule after a newline reads as a selector
3393
3409
  // Skip inter-rule whitespace first — without this, whitespace before an
3394
3410
  // `@layer`/`@media` (e.g. after a preceding `@layer base, ...;` statement
3395
3411
  // or sibling rule) leaves `i` off the '@', so the block below never fires
@@ -3434,17 +3450,30 @@ TailwindCompiler.prototype.extractSelectorsFromCssText = function (cssText) {
3434
3450
  return selectors;
3435
3451
  };
3436
3452
 
3453
+ // Undo escapeClassName's per-char escaping, including the CSS hex-escape
3454
+ // form used for a class whose first character can't appear bare at the start
3455
+ // of an identifier — a leading digit. Real Tailwind/CSS.escape emit `\32 `
3456
+ // (backslash + hex codepoint + one trailing space) for the '2' in `2xl:*`;
3457
+ // a naive `\X` → `X` unescape corrupts this (leaves a stray "2" + space and
3458
+ // drops the rest of the selector), so `2xl:` variants silently stopped being
3459
+ // recognized as covered — caught by the corpus test (utilities-corpus.test.js).
3460
+ TailwindCompiler.prototype.unescapeClassToken = function (token) {
3461
+ return token.replace(/\\([0-9a-fA-F]{1,6}) ?|\\(.)/g, (m, hex, ch) => hex ? String.fromCodePoint(parseInt(hex, 16)) : ch);
3462
+ };
3463
+
3437
3464
  // Selectors escape every non-alphanumeric/hyphen char (see escapeClassName),
3438
3465
  // so `.hover\:bg-brand` unescapes back to the `hover:bg-brand` token form
3439
- // used everywhere else (class attributes, usedClasses, parseClassName).
3466
+ // used everywhere else (class attributes, usedClasses, parseClassName). The
3467
+ // hex-escape alternative is tried first so `\32 ` is consumed as one unit
3468
+ // rather than stopping at the literal digits.
3440
3469
  TailwindCompiler.prototype.classNamesFromCssText = function (cssText) {
3441
3470
  const classSet = new Set();
3442
- const classRe = /\.((?:\\.|[a-zA-Z0-9_-])+)/g;
3471
+ const classRe = /\.((?:\\[0-9a-fA-F]{1,6} ?|\\.|[a-zA-Z0-9_-])+)/g;
3443
3472
  for (const selector of this.extractSelectorsFromCssText(cssText)) {
3444
3473
  let m;
3445
3474
  classRe.lastIndex = 0;
3446
3475
  while ((m = classRe.exec(selector)) !== null) {
3447
- classSet.add(m[1].replace(/\\(.)/g, '$1'));
3476
+ classSet.add(this.unescapeClassToken(m[1]));
3448
3477
  }
3449
3478
  }
3450
3479
  return classSet;
@@ -3516,12 +3545,63 @@ TailwindCompiler.prototype.detectStaticUtilitiesSheet = function () {
3516
3545
  });
3517
3546
  };
3518
3547
 
3519
- // Drop classes already covered by the static sheet before generating rules
3520
- // for them again — the JIT should only ever patch what's left uncovered.
3548
+ // manifest.json's `utilities.safelist` (exact class names) / `utilities.patterns`
3549
+ // (regex source strings) — classes a project knows are already baked (or
3550
+ // intentionally safe to leave unbaked) even though a plain HTML/component
3551
+ // scan can't find them, e.g. built from a runtime value
3552
+ // (`:class="ok ? 'bg-green-500' : 'bg-amber-500'"`). Read once from the
3553
+ // shared manifest.json fetch (already in flight for other plugins); fails
3554
+ // open to "no safelist" so a bad/missing config never blocks anything.
3555
+ TailwindCompiler.prototype.loadUtilitiesSafelist = async function () {
3556
+ this.safelistClasses = new Set();
3557
+ this.safelistPatterns = [];
3558
+ try {
3559
+ let manifest = window.__manifestLoaded || null;
3560
+ if (!manifest && window.__manifestPromise) manifest = await window.__manifestPromise;
3561
+ const cfg = manifest && typeof manifest === 'object' ? manifest.utilities : null;
3562
+ if (cfg && Array.isArray(cfg.safelist)) {
3563
+ for (const c of cfg.safelist) if (typeof c === 'string' && c) this.safelistClasses.add(c);
3564
+ }
3565
+ if (cfg && Array.isArray(cfg.patterns)) {
3566
+ for (const p of cfg.patterns) {
3567
+ if (typeof p !== 'string') continue;
3568
+ try { this.safelistPatterns.push(new RegExp(p)); } catch (e) { /* invalid pattern, ignore */ }
3569
+ }
3570
+ }
3571
+ } catch (e) {
3572
+ // Fail open: no manifest.json, or it couldn't be read.
3573
+ }
3574
+ };
3575
+
3576
+ TailwindCompiler.prototype.isClassSafelisted = function (cls) {
3577
+ if (this.safelistClasses && this.safelistClasses.has(cls)) return true;
3578
+ if (this.safelistPatterns) {
3579
+ for (const re of this.safelistPatterns) {
3580
+ if (re.test(cls)) return true;
3581
+ }
3582
+ }
3583
+ return false;
3584
+ };
3585
+
3586
+ // Single source of truth for "covered" everywhere that concept is used: the
3587
+ // static sheet's own classes, or the safelist. Both mean "don't regenerate,
3588
+ // and don't treat as a signal that the Tailwind engine needs to load".
3589
+ TailwindCompiler.prototype.isClassCovered = function (cls) {
3590
+ const covered = this.staticUtilitiesCoveredClasses;
3591
+ if (covered && covered.has(cls)) return true;
3592
+ return this.isClassSafelisted(cls);
3593
+ };
3594
+
3595
+ TailwindCompiler.prototype.hasSafelistEntries = function () {
3596
+ return !!((this.safelistClasses && this.safelistClasses.size) || (this.safelistPatterns && this.safelistPatterns.length));
3597
+ };
3598
+
3599
+ // Drop classes already covered by the static sheet (or the safelist) before
3600
+ // generating rules for them again — the JIT should only ever patch what's left.
3521
3601
  TailwindCompiler.prototype.filterStaticallyCoveredClasses = function (classes) {
3522
3602
  const covered = this.staticUtilitiesCoveredClasses;
3523
- if (!covered || covered.size === 0) return classes;
3524
- return classes.filter(c => !covered.has(c));
3603
+ if ((!covered || covered.size === 0) && !this.hasSafelistEntries()) return classes;
3604
+ return classes.filter(c => !this.isClassCovered(c));
3525
3605
  };
3526
3606
 
3527
3607
  // The localStorage cache (manifest.utilities.cache.js) stores a full compiled
@@ -3530,13 +3610,14 @@ TailwindCompiler.prototype.filterStaticallyCoveredClasses = function (classes) {
3530
3610
  // rule the static sheet already covers, so strip those rules out first.
3531
3611
  TailwindCompiler.prototype.stripCoveredRulesFromCss = function (cssText) {
3532
3612
  const covered = this.staticUtilitiesCoveredClasses;
3533
- if (!covered || covered.size === 0 || !cssText) return cssText;
3613
+ if (!cssText || ((!covered || covered.size === 0) && !this.hasSafelistEntries())) return cssText;
3534
3614
 
3535
3615
  const strip = (text) => {
3536
3616
  const out = [];
3537
3617
  const len = text.length;
3538
3618
  let i = 0;
3539
3619
  while (i < len) {
3620
+ if (text[i] === ' ' || text[i] === '\n' || text[i] === '\r' || text[i] === '\t') { out.push(text[i]); i++; continue; }
3540
3621
  // Skip (but preserve) inter-rule whitespace before checking for an
3541
3622
  // at-rule — see extractSelectorsFromCssText for why this matters.
3542
3623
  const wsStart = i;
@@ -3582,14 +3663,14 @@ TailwindCompiler.prototype.stripCoveredRulesFromCss = function (cssText) {
3582
3663
  // Match tokens directly against the bare selector text — it has no
3583
3664
  // rule body for classNamesFromCssText's selector-then-`{` scan to key off.
3584
3665
  const classes = [];
3585
- const classRe = /\.((?:\\.|[a-zA-Z0-9_-])+)/g;
3666
+ const classRe = /\.((?:\\[0-9a-fA-F]{1,6} ?|\\.|[a-zA-Z0-9_-])+)/g;
3586
3667
  let cm;
3587
3668
  while ((cm = classRe.exec(selector)) !== null) {
3588
- classes.push(cm[1].replace(/\\(.)/g, '$1'));
3669
+ classes.push(this.unescapeClassToken(cm[1]));
3589
3670
  }
3590
3671
  // Only drop a rule once every class it references is covered — a
3591
3672
  // mixed selector (`:where(.row, .col)`) stays if either is new.
3592
- const isFullyCovered = classes.length > 0 && classes.every(c => covered.has(c));
3673
+ const isFullyCovered = classes.length > 0 && classes.every(c => this.isClassCovered(c));
3593
3674
  if (!isFullyCovered) out.push(fullRule);
3594
3675
  }
3595
3676
  return out.join('\n');
@@ -3598,6 +3679,96 @@ TailwindCompiler.prototype.stripCoveredRulesFromCss = function (cssText) {
3598
3679
  return strip(cssText);
3599
3680
  };
3600
3681
 
3682
+ // Runtime safety net for the loader's Tailwind-engine skip (manifest.js
3683
+ // staticUtilitiesFullyCovered): true only when this page (a) asked for the
3684
+ // Tailwind engine at all (`data-tailwind`), and (b) publish stamped the
3685
+ // static utilities sheet complete, so the loader chose not to fetch it
3686
+ // eagerly. Checked against the fuller link-or-style read
3687
+ // (findStaticUtilitiesElement) rather than manifest.js's synchronous
3688
+ // style-only check — this runs well after boot, so it isn't limited by that
3689
+ // decision's timing constraint.
3690
+ TailwindCompiler.prototype.tailwindEngineWasSkipped = function () {
3691
+ try {
3692
+ if (!document.querySelector('script[src*="manifest.js"][data-tailwind]')) return false;
3693
+ const el = this.findStaticUtilitiesElement();
3694
+ return !!(el && el.hasAttribute('data-mnfst-utilities-complete'));
3695
+ } catch (e) {
3696
+ return false;
3697
+ }
3698
+ };
3699
+
3700
+ // Arms once staticUtilitiesReady (sheet + safelist) has settled. Watches for
3701
+ // any class token — already on the page, or added/changed after — that the
3702
+ // bake doesn't cover and the safelist doesn't excuse: the signal that a
3703
+ // "complete" stamp was wrong and the page would otherwise render unstyled.
3704
+ // A burst of nodes collapses into one lazy load; the observer disconnects
3705
+ // the moment that load succeeds, handing off to the real engine exactly as
3706
+ // if it had loaded eagerly.
3707
+ TailwindCompiler.prototype.setupUncoveredClassWatcher = function () {
3708
+ if (this.usesStaticPrerenderUtilities) return;
3709
+ if (!this.tailwindEngineWasSkipped()) return;
3710
+ if (window.__mnfstTailwindWatcherArmed) return; // one watcher per page load
3711
+ window.__mnfstTailwindWatcherArmed = true;
3712
+
3713
+ let pending = new Set();
3714
+ let loading = false;
3715
+ let observer = null;
3716
+
3717
+ const collectFrom = (el) => {
3718
+ const cls = el.getAttribute && el.getAttribute('class');
3719
+ if (!cls) return;
3720
+ for (const tok of cls.split(/\s+/)) {
3721
+ if (tok && !this.isClassCovered(tok)) pending.add(tok);
3722
+ }
3723
+ };
3724
+
3725
+ const flush = this.debounce(() => {
3726
+ if (loading || pending.size === 0) return;
3727
+ const classes = Array.from(pending).sort();
3728
+ pending = new Set();
3729
+ loading = true;
3730
+ console.warn('[Manifest Utilities] Uncovered utility class(es) — loading the Tailwind engine:', classes.join(', '));
3731
+ window.Manifest.loadPlugin('tailwind').then(() => {
3732
+ if (observer) observer.disconnect();
3733
+ window.dispatchEvent(new CustomEvent('manifest:utilities-uncovered', { detail: { classes, engineLoaded: true } }));
3734
+ }).catch((err) => {
3735
+ loading = false; // let a further uncovered class try again
3736
+ console.error('[Manifest Utilities] Failed to load the Tailwind engine:', err);
3737
+ window.dispatchEvent(new CustomEvent('manifest:utilities-uncovered', { detail: { classes, engineLoaded: false } }));
3738
+ });
3739
+ }, this.options.debounceTime || 50);
3740
+
3741
+ observer = new MutationObserver((mutations) => {
3742
+ for (const mutation of mutations) {
3743
+ if (mutation.type === 'attributes') {
3744
+ if (mutation.target.nodeType === Node.ELEMENT_NODE) collectFrom(mutation.target);
3745
+ } else if (mutation.type === 'childList') {
3746
+ for (const node of mutation.addedNodes) {
3747
+ if (node.nodeType !== Node.ELEMENT_NODE) continue;
3748
+ collectFrom(node);
3749
+ if (node.querySelectorAll) {
3750
+ for (const desc of node.querySelectorAll('[class]')) collectFrom(desc);
3751
+ }
3752
+ }
3753
+ }
3754
+ }
3755
+ if (pending.size > 0) flush();
3756
+ });
3757
+
3758
+ observer.observe(document.documentElement, {
3759
+ childList: true,
3760
+ subtree: true,
3761
+ attributes: true,
3762
+ attributeFilter: ['class']
3763
+ });
3764
+ this.uncoveredClassObserver = observer; // exposed for tests/diagnostics
3765
+
3766
+ // A wrongly-stamped "complete" sheet may already have uncovered classes
3767
+ // in the initial markup, before any mutation ever fires — check once now.
3768
+ for (const el of document.querySelectorAll('[class]')) collectFrom(el);
3769
+ if (pending.size > 0) flush();
3770
+ };
3771
+
3601
3772
 
3602
3773
  // DOM observation: watch for changes and trigger recompilation
3603
3774
 
@@ -574,12 +574,19 @@ class TailwindCompiler {
574
574
  // Cache for parsed class names (must be before addCriticalBlockingStylesSync)
575
575
  this.classCache = new Map();
576
576
 
577
- // Read any publish/render-provided static utilities sheet before the
578
- // first compile — its classes are skipped rather than regenerated.
579
- // Fails open (never "everything covered") until this resolves —
580
- // compile()'s first run awaits it, capped at 2s (see static.js).
577
+ // Read any publish/render-provided static utilities sheet, plus
578
+ // manifest.json's utilities.safelist/patterns, before the first
579
+ // compile — those classes are skipped rather than regenerated. Fails
580
+ // open (never "everything covered") until this resolves — compile()'s
581
+ // first run awaits it, capped at 2s (see static.js). Once settled,
582
+ // arm the uncovered-class watcher (a no-op unless the loader actually
583
+ // skipped fetching the Tailwind engine for this page).
581
584
  this.staticUtilitiesCoveredClasses = null;
582
- this.staticUtilitiesReady = this.detectStaticUtilitiesSheet();
585
+ this.staticUtilitiesReady = Promise.all([
586
+ this.detectStaticUtilitiesSheet(),
587
+ this.loadUtilitiesSafelist()
588
+ ]);
589
+ this.staticUtilitiesReady.then(() => this.setupUncoveredClassWatcher());
583
590
 
584
591
  // Add critical styles IMMEDIATELY - don't wait for anything
585
592
  this.addCriticalBlockingStylesSync();
@@ -927,13 +934,17 @@ TailwindCompiler.prototype.extractClassesFromHTML = function (html, classSet) {
927
934
  // Literal class="..."/class='...' only — lookbehind excludes `:class=`
928
935
  // (Alpine binding), whose "class=" substring would otherwise match and
929
936
  // truncate at the literal's first internal quote, leaking a stray '{'.
937
+ // Backreference to the opening quote (rather than stopping at either
938
+ // quote char) so an arbitrary Tailwind value containing the OTHER quote
939
+ // character — `content-['*']`, `content-['']` — survives instead of
940
+ // truncating the whole class list at its first `'`.
930
941
  // Whitespace-only split below keeps every token Tailwind would accept
931
942
  // (variants, arbitrary values, leading '-', '!'); only x-/$ tokens drop.
932
- const classRegex = /(?<![:\w])class=["']([^"']+)["']/g;
943
+ const classRegex = /(?<![:\w])class=(["'])((?:(?!\1)[\s\S])*)\1/g;
933
944
  let match;
934
945
 
935
946
  while ((match = classRegex.exec(html)) !== null) {
936
- const classString = match[1];
947
+ const classString = match[2];
937
948
  const classes = classString.split(/\s+/).filter(Boolean);
938
949
  for (const cls of classes) {
939
950
  if (cls && !cls.startsWith('x-') && !cls.startsWith('$')) {
@@ -951,7 +962,11 @@ TailwindCompiler.prototype.extractClassesFromHTML = function (html, classSet) {
951
962
  if (classMatches) {
952
963
  for (const classMatch of classMatches) {
953
964
  const cls = classMatch.replace(/['"`]/g, '');
954
- if (cls && !cls.startsWith('$') && !cls.includes('(')) {
965
+ // x-data is arbitrary JS — a plain string literal in there
966
+ // (e.g. `location.pathname.split('/')`) matches this quoted-
967
+ // token scan too, so require a letter/digit; a punctuation-only
968
+ // "token" like a bare '/' is skipped (seen on a live page).
969
+ if (cls && !cls.startsWith('$') && !cls.includes('(') && /[a-zA-Z0-9]/.test(cls)) {
955
970
  classSet.add(cls);
956
971
  }
957
972
  }
@@ -3010,13 +3025,22 @@ async function compileTailwindPass(classes, injected) {
3010
3025
  * isolate) — see loadTailwindEngine. Omit it (the default) to keep using the
3011
3026
  * Node/CLI fs-based load.
3012
3027
  *
3013
- * @param {{classes?: string[], themeCss?: string, baseCss?: string, tailwind?: {engine: object, themeCss: string, utilitiesCss: string}}} options
3028
+ * `safelist` adds literal class names as extra candidates alongside `classes`
3029
+ * — manifest.json's `utilities.safelist` (see scanClasses below), for classes
3030
+ * a project knows it needs baked even though they never turn up in a plain
3031
+ * HTML scan (e.g. built from a runtime value: `:class="ok ? 'bg-green-500' :
3032
+ * 'bg-amber-500'"`). `utilities.patterns` (regex source strings) has no
3033
+ * equivalent here — a pattern can't be enumerated into concrete candidates,
3034
+ * so it only ever excuses a class from the runtime's uncovered-class watcher
3035
+ * (manifest.utilities.static.js), never bakes one.
3036
+ *
3037
+ * @param {{classes?: string[], themeCss?: string, baseCss?: string, safelist?: string[], tailwind?: {engine: object, themeCss: string, utilitiesCss: string}}} options
3014
3038
  * @returns {Promise<string>}
3015
3039
  */
3016
- export async function compileUtilities({ classes = [], themeCss = '', baseCss = '', tailwind = null } = {}) {
3040
+ export async function compileUtilities({ classes = [], themeCss = '', baseCss = '', safelist = [], tailwind = null } = {}) {
3017
3041
  const compiler = createNodeCompiler();
3018
3042
  const cssText = [baseCss, themeCss].filter(Boolean).join('\n');
3019
- const uniqueClasses = Array.from(new Set(classes));
3043
+ const uniqueClasses = Array.from(new Set([...classes, ...safelist]));
3020
3044
 
3021
3045
  const discovered = compiler.extractCustomUtilities(cssText);
3022
3046
  for (const [name, value] of discovered) compiler.customUtilities.set(name, value);
@@ -3052,12 +3076,17 @@ const SHORTHAND_CLASS_BINDING_RE = /\B:class=(["'])((?:(?!\1)[\s\S])*)\1/g;
3052
3076
  * plugin uses (extractClassesFromHTML), plus `class:`/`:class` static tokens.
3053
3077
  * Skips `x-`/`$` tokens. Returns a sorted, deduplicated array.
3054
3078
  *
3079
+ * `safelist` (manifest.json's `utilities.safelist`) is folded into the result
3080
+ * so a caller that bakes per-file (scripts/utilities-static.mjs, or a
3081
+ * publish pipeline) doesn't need a separate union step — see compileUtilities.
3082
+ *
3055
3083
  * @param {string} html
3084
+ * @param {{safelist?: string[]}} [options]
3056
3085
  * @returns {string[]}
3057
3086
  */
3058
- export function scanClasses(html) {
3087
+ export function scanClasses(html, { safelist = [] } = {}) {
3059
3088
  const compiler = createNodeCompiler();
3060
- const set = new Set();
3089
+ const set = new Set(safelist.filter(c => typeof c === 'string' && c));
3061
3090
  compiler.extractClassesFromHTML(html, set);
3062
3091
 
3063
3092
  let m;
@@ -3074,7 +3103,9 @@ export function scanClasses(html) {
3074
3103
  if (quoted) {
3075
3104
  for (const q of quoted) {
3076
3105
  const cls = q.replace(/['"`]/g, '');
3077
- if (cls && !cls.startsWith('$') && !cls.startsWith('x-') && !cls.includes('(')) set.add(cls);
3106
+ // Reject punctuation-only "tokens" (e.g. a bare '/' from an
3107
+ // unrelated string literal in the expression) — see helpers.js.
3108
+ if (cls && !cls.startsWith('$') && !cls.startsWith('x-') && !cls.includes('(') && /[a-zA-Z0-9]/.test(cls)) set.add(cls);
3078
3109
  }
3079
3110
  }
3080
3111
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst",
3
- "version": "0.5.205",
3
+ "version": "0.5.207",
4
4
  "private": false,
5
5
  "workspaces": [
6
6
  "templates/starter",