@webto-id/variant-check 0.1.1 → 0.1.2

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.
Files changed (3) hide show
  1. package/dist/cli.js +109 -13
  2. package/dist/index.js +109 -13
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -176,12 +176,19 @@ var Parser = class {
176
176
  const lower = tag.toLowerCase();
177
177
  if (RAW_TEXT_TAGS.has(lower)) {
178
178
  if (selfClose) this.fail(`<${tag}> cannot be self-closing`);
179
- const close = new RegExp(`</${lower}\\s*>`, "i");
179
+ const boundary = new RegExp(`</${lower}(?=[\\s/>])`, "i");
180
180
  const rest = this.src.slice(this.pos);
181
- const mm = close.exec(rest);
181
+ const mm = boundary.exec(rest);
182
182
  if (!mm) this.fail(`missing </${lower}>`);
183
+ const after = rest.slice(mm.index + mm[0].length);
184
+ const clean = /^\s*>/.exec(after);
185
+ if (!clean) {
186
+ this.fail(
187
+ `malformed </${lower}> \u2014 a raw text block must close with exactly </${lower}>; the browser ends it at '</${lower}' followed by whitespace or '/', so anything else here would execute unchecked`
188
+ );
189
+ }
183
190
  const text = rest.slice(0, mm.index);
184
- this.pos += mm.index + mm[0].length;
191
+ this.pos += mm.index + mm[0].length + clean[0].length;
185
192
  const raw = {};
186
193
  for (const [k, v] of Object.entries(attrs)) raw[k] = typeof v === "string" || v === true ? v : "{expr}";
187
194
  const block = { tag: lower, attrs: raw, text, line: startLine };
@@ -1216,6 +1223,7 @@ function lintCss(css, sectionType2, attrs = {}) {
1216
1223
  if ("is:global" in attrs) err("style-global", "<style is:global> is not allowed \u2014 variant CSS is always scoped");
1217
1224
  if ("define:vars" in attrs) err("style-define-vars", "<style define:vars> is not supported \u2014 use var(--color-*) / var(--radius) tokens");
1218
1225
  if (css.length > 64 * 1024) err("style-size", "style block exceeds 64 KB");
1226
+ if (/<\//.test(css)) err("css-raw-close", "'</' is not allowed inside a <style> block (it can close the block early and inject markup)");
1219
1227
  const stripped = css.replace(/\/\*[\s\S]*?\*\//g, " ");
1220
1228
  if (/@import\b/i.test(stripped)) err("css-import", "@import is not allowed");
1221
1229
  if (/@font-face\b/i.test(stripped)) err("css-font-face", "@font-face is not allowed \u2014 fonts come from the site theme (var(--font-heading|--font-body))");
@@ -1316,7 +1324,49 @@ var FORBIDDEN_MEMBERS = /* @__PURE__ */ new Set([
1316
1324
  "defineProperty",
1317
1325
  "getOwnPropertyDescriptor",
1318
1326
  "setPrototypeOf",
1319
- "getPrototypeOf"
1327
+ "getPrototypeOf",
1328
+ // Routes back to a window/document object, from which a forbidden name could
1329
+ // be reached as a property (see the FORBIDDEN_IDENTS-as-member rule below).
1330
+ "defaultView",
1331
+ "ownerDocument",
1332
+ "getRootNode"
1333
+ ]);
1334
+ var URL_BEARING_STYLE_PROPS = /* @__PURE__ */ new Set([
1335
+ "background",
1336
+ "backgroundImage",
1337
+ "listStyle",
1338
+ "listStyleImage",
1339
+ "borderImage",
1340
+ "borderImageSource",
1341
+ "content",
1342
+ "cursor",
1343
+ "mask",
1344
+ "maskImage",
1345
+ "webkitMaskImage",
1346
+ "webkitMask",
1347
+ "filter",
1348
+ "backdropFilter",
1349
+ "src",
1350
+ "offsetPath",
1351
+ "clipPath",
1352
+ "shapeOutside",
1353
+ "cssText"
1354
+ ]);
1355
+ var URL_BEARING_PROPS = /* @__PURE__ */ new Set([
1356
+ "src",
1357
+ "href",
1358
+ "action",
1359
+ "srcset",
1360
+ "poster",
1361
+ "data",
1362
+ "formAction",
1363
+ "formaction",
1364
+ "background",
1365
+ "ping",
1366
+ "cite",
1367
+ "codeBase",
1368
+ "lowsrc",
1369
+ "xlinkHref"
1320
1370
  ]);
1321
1371
  var DYNAMIC_HTML = /* @__PURE__ */ new Set(["innerHTML", "outerHTML"]);
1322
1372
  var FORBIDDEN_CALLS = /* @__PURE__ */ new Set(["atob", "btoa", "unescape", "decodeURIComponent", "setTimeout", "setInterval"]);
@@ -1335,6 +1385,7 @@ function lintScript(src, attrs = {}) {
1335
1385
  if (/\bwith\s*\(/.test(src)) err("script-with", "`with` is not allowed");
1336
1386
  if (/\bimport\s*\(/.test(src) || /\bimport\s+[\w{*]/.test(src) || /\bexport\s+/.test(src)) err("script-import", "import/export are not allowed in inline scripts");
1337
1387
  if (/\bdebugger\b/.test(src)) err("script-debugger", "`debugger` is not allowed");
1388
+ if (/<\//.test(src)) err("script-raw-close", "'</' is not allowed inside a <script> block (it can close the block early and inject markup)");
1338
1389
  let ast;
1339
1390
  try {
1340
1391
  ast = acorn.parse(src, { ecmaVersion: 2022, sourceType: "script", allowReturnOutsideFunction: true, locations: true });
@@ -1367,6 +1418,7 @@ function lintScript(src, attrs = {}) {
1367
1418
  if (obj.type === "Identifier" && /^(window|document|globalThis|self|Object|Array)$/.test(obj.name)) err("script-forbidden", `computed member access on ${obj.name} is not allowed`, lineOf2(n));
1368
1419
  }
1369
1420
  if (p && FORBIDDEN_MEMBERS.has(p)) err("script-forbidden", `'.${p}' is not allowed`, lineOf2(n));
1421
+ if (p && FORBIDDEN_IDENTS.has(p)) err("script-forbidden", `'.${p}' is not allowed`, lineOf2(n));
1370
1422
  break;
1371
1423
  }
1372
1424
  case "AssignmentExpression": {
@@ -1374,8 +1426,15 @@ function lintScript(src, attrs = {}) {
1374
1426
  if (left.type === "MemberExpression") {
1375
1427
  const p = propName(left);
1376
1428
  if (p && DYNAMIC_HTML.has(p) && n.right.type !== "Literal") err("script-html", `assigning a non-literal to .${p} is not allowed`, lineOf2(n));
1377
- if (p === "src" || p === "href" || p === "action") err("script-forbidden", `assigning .${p} is not allowed`, lineOf2(n));
1429
+ if (p && URL_BEARING_PROPS.has(p)) err("script-forbidden", `assigning .${p} is not allowed (it loads a URL \u2014 an outbound channel for page data)`, lineOf2(n));
1378
1430
  if (p && /^on[a-z]+$/.test(p)) err("script-forbidden", `assigning .${p} handlers is not allowed \u2014 use addEventListener`, lineOf2(n));
1431
+ if (p && URL_BEARING_STYLE_PROPS.has(p)) {
1432
+ const obj = left.object;
1433
+ if (obj.type === "MemberExpression" && propName(obj) === "style") {
1434
+ err("script-forbidden", `assigning .style.${p} is not allowed (it can load a URL)`, lineOf2(n));
1435
+ }
1436
+ }
1437
+ if (p === "style") err("script-forbidden", "assigning .style wholesale is not allowed \u2014 set individual layout properties", lineOf2(n));
1379
1438
  }
1380
1439
  break;
1381
1440
  }
@@ -1621,7 +1680,11 @@ function getCompiler() {
1621
1680
  return compilerPromise;
1622
1681
  }
1623
1682
  __name(getCompiler, "getCompiler");
1624
- var CANDIDATE_RE = /^[!@]?[A-Za-z0-9_][\w:/\[\].%#(),-]*$/;
1683
+ var CANDIDATE_RE = /^[!@]?-?[A-Za-z0-9_][\w:/\[\].%#(),-]*$/;
1684
+ function isCandidate(t) {
1685
+ return !!t && t.length <= 96 && CANDIDATE_RE.test(t) && !/url\(/i.test(t);
1686
+ }
1687
+ __name(isCandidate, "isCandidate");
1625
1688
  var MACRO_CLASSES = [
1626
1689
  "mx-auto w-full px-6",
1627
1690
  "inline-flex items-center justify-center font-medium transition-all duration-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
@@ -1695,7 +1758,7 @@ function deriveSchema(fields, props, base2) {
1695
1758
  __name(deriveSchema, "deriveSchema");
1696
1759
 
1697
1760
  // ../variant-compiler/src/index.ts
1698
- var COMPILER_VERSION = "0.1.0";
1761
+ var COMPILER_VERSION = "0.1.2";
1699
1762
  var IR_MAX_BYTES = 256 * 1024;
1700
1763
  var CSS_MAX_BYTES = 64 * 1024;
1701
1764
  var STRICT_CODES = /* @__PURE__ */ new Set(["dead-text", "hardcoded-color", "hardcoded-radius", "hardcoded-font", "hardcoded-image", "img-alt", "props-optional"]);
@@ -1768,12 +1831,22 @@ async function compile2(source2, opts) {
1768
1831
  if (tpl.scripts[0]) lint.push(...lintScript(script, tpl.scripts[0].attrs).map((m) => ({ ...m, line: m.line != null ? m.line + tpl.scripts[0].line : tpl.scripts[0].line })));
1769
1832
  lint.push(...lintHtml(tpl.root, opts.sectionType));
1770
1833
  const strings = [];
1834
+ const classTokens = /* @__PURE__ */ new Set();
1771
1835
  walkNodes(tpl.root, (e) => collectStrings(e, strings), (attrs) => {
1772
- for (const v of Object.values(attrs)) if (typeof v === "string") strings.push(v);
1836
+ for (const [k, v] of Object.entries(attrs)) {
1837
+ if (typeof v !== "string") continue;
1838
+ strings.push(v);
1839
+ if (k === "class" || k === "class:list") {
1840
+ for (const t of v.split(/\s+/)) if (t) classTokens.add(t);
1841
+ }
1842
+ }
1773
1843
  });
1774
1844
  for (const c of front.consts) collectStrings(c.e, strings);
1775
1845
  for (const p of front.props) if (p.def) collectStrings(p.def, strings);
1776
1846
  const classCandidates = pickCandidates(strings);
1847
+ for (const t of classTokens) {
1848
+ if (!isCandidate(t)) lint.push({ level: "warning", code: "tailwind-skipped", message: `class token '${t}' is not a shape the compiler accepts, so no CSS is generated for it \u2014 rewrite it (e.g. an arbitrary value like mt-[-4rem]) or drop it` });
1849
+ }
1777
1850
  let twCss = "";
1778
1851
  if (!opts.skipTailwind) {
1779
1852
  try {
@@ -2012,8 +2085,15 @@ function collectStrings(e, out) {
2012
2085
  __name(collectStrings, "collectStrings");
2013
2086
 
2014
2087
  // ../variant-compiler/src/runtime/eval.ts
2088
+ var JSX_VALUES = /* @__PURE__ */ new WeakSet();
2089
+ function makeJsxValue(nodes, scope) {
2090
+ const v = { __wvJsx: true, nodes, scope };
2091
+ JSX_VALUES.add(v);
2092
+ return v;
2093
+ }
2094
+ __name(makeJsxValue, "makeJsxValue");
2015
2095
  function isJsxValue(v) {
2016
- return typeof v === "object" && v !== null && v.__wvJsx === true;
2096
+ return typeof v === "object" && v !== null && JSX_VALUES.has(v);
2017
2097
  }
2018
2098
  __name(isJsxValue, "isJsxValue");
2019
2099
  var FORBIDDEN_PROPS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
@@ -2152,7 +2232,7 @@ function evalExpr(e, scope, budget) {
2152
2232
  return evalExpr(e.body, s, budget);
2153
2233
  };
2154
2234
  case "jsx":
2155
- return { __wvJsx: true, nodes: e.n, scope };
2235
+ return makeJsxValue(e.n, scope);
2156
2236
  }
2157
2237
  }
2158
2238
  __name(evalExpr, "evalExpr");
@@ -2335,11 +2415,24 @@ function renderVariant(ir, content, ctx, opts = {}) {
2335
2415
  }
2336
2416
  for (const c of ir.consts) scope[c.name] = evalExpr(c.e, scope, budget);
2337
2417
  const html = renderNodes(ir.root, scope, ctx, budget);
2338
- const css = ir.css ? ir.css.replace(/__WV_ROOT__/g, `[data-wv-inst="${uid}"]`) : "";
2418
+ const css = ir.css ? layerize(ir.css.replace(/__WV_ROOT__/g, `[data-wv-inst="${uid}"]`)) : "";
2339
2419
  const script = ir.script ? wrapScript(ir.script, uid) : "";
2340
2420
  return { html, css, script, uid };
2341
2421
  }
2342
2422
  __name(renderVariant, "renderVariant");
2423
+ var STATEMENT_AT = /^\s*@[a-zA-Z-]+[^;{}]*;/;
2424
+ function layerize(css) {
2425
+ let rest = css;
2426
+ let head = "";
2427
+ for (; ; ) {
2428
+ const m = rest.match(STATEMENT_AT);
2429
+ if (!m) break;
2430
+ head += m[0].trim();
2431
+ rest = rest.slice(m[0].length);
2432
+ }
2433
+ return rest.trim() ? `${head}@layer wvf{${rest}}` : head;
2434
+ }
2435
+ __name(layerize, "layerize");
2343
2436
  function wrapScript(body, uid) {
2344
2437
  return `(function(${SHADOWED_GLOBALS.join(",")}){"use strict";var root=document.querySelector('[data-wv-inst="${uid}"]');if(!root)return;
2345
2438
  ${body}
@@ -2360,10 +2453,13 @@ __name(isPreviewThemeKey, "isPreviewThemeKey");
2360
2453
  function buildPreviewDoc(ir, content, themeKey, opts = {}) {
2361
2454
  const th = PREVIEW_THEMES[themeKey];
2362
2455
  const out = renderVariant(ir, content, { t: /* @__PURE__ */ __name((k) => k, "t"), editChrome: opts.editChrome ?? false }, { uid: "wv-preview" });
2363
- const vars = `--background:${th.background};--foreground:${th.foreground};--card:${th.card};--card-foreground:${th.foreground};--primary:${th.primary};--primary-foreground:${th.primaryFg};--secondary:${th.secondary};--secondary-foreground:#fff;--muted:${th.muted};--muted-foreground:${th.mutedFg};--accent:${th.accent};--accent-foreground:#000;--border:${th.border};--input:${th.border};--ring:${th.primary};--radius:${th.radius};--font-heading:${th.heading};--font-body:${th.body};--gradient-from:${th.primary};--gradient-to:${th.secondary};--spacing:.25rem;--site-max-width:72rem;`;
2456
+ const vars = `--background:${th.background};--foreground:${th.foreground};--card:${th.card};--card-foreground:${th.foreground};--primary:${th.primary};--primary-foreground:${th.primaryFg};--secondary:${th.secondary};--secondary-foreground:#fff;--muted:${th.muted};--muted-foreground:${th.mutedFg};--accent:${th.accent};--accent-foreground:#000;--border:${th.border};--input:${th.border};--ring:${th.primary};--radius:${th.radius};--font-heading:${th.heading};--font-body:${th.body};--gradient-from:${th.primary};--gradient-to:${th.secondary};--spacing:.25rem;--site-max-width:72rem;--font-heading-size:48px;--font-body-size:16px;`;
2364
2457
  const tokens = `--color-background:var(--background);--color-foreground:var(--foreground);--color-card:var(--card);--color-card-foreground:var(--card-foreground);--color-primary:var(--primary);--color-primary-foreground:var(--primary-foreground);--color-secondary:var(--secondary);--color-secondary-foreground:var(--secondary-foreground);--color-muted:var(--muted);--color-muted-foreground:var(--muted-foreground);--color-accent:var(--accent);--color-accent-foreground:var(--accent-foreground);--color-border:var(--border);--color-input:var(--input);--color-ring:var(--ring);--radius-sm:calc(var(--radius) - 4px);--radius-md:calc(var(--radius) - 2px);--radius-lg:var(--radius);--radius-xl:calc(var(--radius) + 4px);`;
2365
2458
  return `<!doctype html><html data-theme="${themeKey === "dark" ? "dark" : "light"}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex">
2366
- <style>*,::before,::after{box-sizing:border-box;border:0 solid var(--color-border)}html{font-size:16px;-webkit-text-size-adjust:100%}body{margin:0;font-family:var(--font-body);color:var(--foreground);background:var(--background);line-height:1.5}h1,h2,h3,h4,p{margin:0}h1{font-size:48px}h2{font-size:36px}h3{font-size:28px}img,video,svg{display:block;max-width:100%;height:auto}a{color:inherit;text-decoration:inherit}button{font:inherit;color:inherit;background:none}ul,ol{margin:0;padding:0;list-style:none}:root{${vars}${tokens}}.edit-url-pill{display:none!important}</style>
2459
+ <style>@layer wvfbase{*,::before,::after{box-sizing:border-box;border:0 solid var(--color-border)}html{font-size:var(--font-body-size,16px);-webkit-text-size-adjust:100%}body{margin:0;font-family:var(--font-body);color:var(--foreground);background:var(--background);line-height:1.5}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}h1,h2,h3,h4,p{margin:0}img,video,svg{display:block;max-width:100%;height:auto}a{color:inherit;text-decoration:inherit}button{font:inherit;color:inherit;background:none}ul,ol{margin:0;padding:0;list-style:none}}
2460
+ :root{${vars}${tokens}}
2461
+ h1{font-size:var(--font-heading-size,48px)}h2{font-size:calc(var(--font-heading-size,48px) * 0.75)}h3{font-size:calc(var(--font-heading-size,48px) * 0.6)}h4{font-size:calc(var(--font-heading-size,48px) * 0.5)}
2462
+ .edit-url-pill{display:none!important}</style>
2367
2463
  <style>${out.css}</style></head><body><div data-wv-root data-wv-inst="${out.uid}">${out.html}</div>${out.script ? `<script>${out.script}</script>` : ""}</body></html>`;
2368
2464
  }
2369
2465
  __name(buildPreviewDoc, "buildPreviewDoc");
package/dist/index.js CHANGED
@@ -173,12 +173,19 @@ var Parser = class {
173
173
  const lower = tag.toLowerCase();
174
174
  if (RAW_TEXT_TAGS.has(lower)) {
175
175
  if (selfClose) this.fail(`<${tag}> cannot be self-closing`);
176
- const close = new RegExp(`</${lower}\\s*>`, "i");
176
+ const boundary = new RegExp(`</${lower}(?=[\\s/>])`, "i");
177
177
  const rest = this.src.slice(this.pos);
178
- const mm = close.exec(rest);
178
+ const mm = boundary.exec(rest);
179
179
  if (!mm) this.fail(`missing </${lower}>`);
180
+ const after = rest.slice(mm.index + mm[0].length);
181
+ const clean = /^\s*>/.exec(after);
182
+ if (!clean) {
183
+ this.fail(
184
+ `malformed </${lower}> \u2014 a raw text block must close with exactly </${lower}>; the browser ends it at '</${lower}' followed by whitespace or '/', so anything else here would execute unchecked`
185
+ );
186
+ }
180
187
  const text = rest.slice(0, mm.index);
181
- this.pos += mm.index + mm[0].length;
188
+ this.pos += mm.index + mm[0].length + clean[0].length;
182
189
  const raw = {};
183
190
  for (const [k, v] of Object.entries(attrs)) raw[k] = typeof v === "string" || v === true ? v : "{expr}";
184
191
  const block = { tag: lower, attrs: raw, text, line: startLine };
@@ -1213,6 +1220,7 @@ function lintCss(css, sectionType, attrs = {}) {
1213
1220
  if ("is:global" in attrs) err("style-global", "<style is:global> is not allowed \u2014 variant CSS is always scoped");
1214
1221
  if ("define:vars" in attrs) err("style-define-vars", "<style define:vars> is not supported \u2014 use var(--color-*) / var(--radius) tokens");
1215
1222
  if (css.length > 64 * 1024) err("style-size", "style block exceeds 64 KB");
1223
+ if (/<\//.test(css)) err("css-raw-close", "'</' is not allowed inside a <style> block (it can close the block early and inject markup)");
1216
1224
  const stripped = css.replace(/\/\*[\s\S]*?\*\//g, " ");
1217
1225
  if (/@import\b/i.test(stripped)) err("css-import", "@import is not allowed");
1218
1226
  if (/@font-face\b/i.test(stripped)) err("css-font-face", "@font-face is not allowed \u2014 fonts come from the site theme (var(--font-heading|--font-body))");
@@ -1313,7 +1321,49 @@ var FORBIDDEN_MEMBERS = /* @__PURE__ */ new Set([
1313
1321
  "defineProperty",
1314
1322
  "getOwnPropertyDescriptor",
1315
1323
  "setPrototypeOf",
1316
- "getPrototypeOf"
1324
+ "getPrototypeOf",
1325
+ // Routes back to a window/document object, from which a forbidden name could
1326
+ // be reached as a property (see the FORBIDDEN_IDENTS-as-member rule below).
1327
+ "defaultView",
1328
+ "ownerDocument",
1329
+ "getRootNode"
1330
+ ]);
1331
+ var URL_BEARING_STYLE_PROPS = /* @__PURE__ */ new Set([
1332
+ "background",
1333
+ "backgroundImage",
1334
+ "listStyle",
1335
+ "listStyleImage",
1336
+ "borderImage",
1337
+ "borderImageSource",
1338
+ "content",
1339
+ "cursor",
1340
+ "mask",
1341
+ "maskImage",
1342
+ "webkitMaskImage",
1343
+ "webkitMask",
1344
+ "filter",
1345
+ "backdropFilter",
1346
+ "src",
1347
+ "offsetPath",
1348
+ "clipPath",
1349
+ "shapeOutside",
1350
+ "cssText"
1351
+ ]);
1352
+ var URL_BEARING_PROPS = /* @__PURE__ */ new Set([
1353
+ "src",
1354
+ "href",
1355
+ "action",
1356
+ "srcset",
1357
+ "poster",
1358
+ "data",
1359
+ "formAction",
1360
+ "formaction",
1361
+ "background",
1362
+ "ping",
1363
+ "cite",
1364
+ "codeBase",
1365
+ "lowsrc",
1366
+ "xlinkHref"
1317
1367
  ]);
1318
1368
  var DYNAMIC_HTML = /* @__PURE__ */ new Set(["innerHTML", "outerHTML"]);
1319
1369
  var FORBIDDEN_CALLS = /* @__PURE__ */ new Set(["atob", "btoa", "unescape", "decodeURIComponent", "setTimeout", "setInterval"]);
@@ -1332,6 +1382,7 @@ function lintScript(src, attrs = {}) {
1332
1382
  if (/\bwith\s*\(/.test(src)) err("script-with", "`with` is not allowed");
1333
1383
  if (/\bimport\s*\(/.test(src) || /\bimport\s+[\w{*]/.test(src) || /\bexport\s+/.test(src)) err("script-import", "import/export are not allowed in inline scripts");
1334
1384
  if (/\bdebugger\b/.test(src)) err("script-debugger", "`debugger` is not allowed");
1385
+ if (/<\//.test(src)) err("script-raw-close", "'</' is not allowed inside a <script> block (it can close the block early and inject markup)");
1335
1386
  let ast;
1336
1387
  try {
1337
1388
  ast = acorn.parse(src, { ecmaVersion: 2022, sourceType: "script", allowReturnOutsideFunction: true, locations: true });
@@ -1364,6 +1415,7 @@ function lintScript(src, attrs = {}) {
1364
1415
  if (obj.type === "Identifier" && /^(window|document|globalThis|self|Object|Array)$/.test(obj.name)) err("script-forbidden", `computed member access on ${obj.name} is not allowed`, lineOf2(n));
1365
1416
  }
1366
1417
  if (p && FORBIDDEN_MEMBERS.has(p)) err("script-forbidden", `'.${p}' is not allowed`, lineOf2(n));
1418
+ if (p && FORBIDDEN_IDENTS.has(p)) err("script-forbidden", `'.${p}' is not allowed`, lineOf2(n));
1367
1419
  break;
1368
1420
  }
1369
1421
  case "AssignmentExpression": {
@@ -1371,8 +1423,15 @@ function lintScript(src, attrs = {}) {
1371
1423
  if (left.type === "MemberExpression") {
1372
1424
  const p = propName(left);
1373
1425
  if (p && DYNAMIC_HTML.has(p) && n.right.type !== "Literal") err("script-html", `assigning a non-literal to .${p} is not allowed`, lineOf2(n));
1374
- if (p === "src" || p === "href" || p === "action") err("script-forbidden", `assigning .${p} is not allowed`, lineOf2(n));
1426
+ if (p && URL_BEARING_PROPS.has(p)) err("script-forbidden", `assigning .${p} is not allowed (it loads a URL \u2014 an outbound channel for page data)`, lineOf2(n));
1375
1427
  if (p && /^on[a-z]+$/.test(p)) err("script-forbidden", `assigning .${p} handlers is not allowed \u2014 use addEventListener`, lineOf2(n));
1428
+ if (p && URL_BEARING_STYLE_PROPS.has(p)) {
1429
+ const obj = left.object;
1430
+ if (obj.type === "MemberExpression" && propName(obj) === "style") {
1431
+ err("script-forbidden", `assigning .style.${p} is not allowed (it can load a URL)`, lineOf2(n));
1432
+ }
1433
+ }
1434
+ if (p === "style") err("script-forbidden", "assigning .style wholesale is not allowed \u2014 set individual layout properties", lineOf2(n));
1376
1435
  }
1377
1436
  break;
1378
1437
  }
@@ -1618,7 +1677,11 @@ function getCompiler() {
1618
1677
  return compilerPromise;
1619
1678
  }
1620
1679
  __name(getCompiler, "getCompiler");
1621
- var CANDIDATE_RE = /^[!@]?[A-Za-z0-9_][\w:/\[\].%#(),-]*$/;
1680
+ var CANDIDATE_RE = /^[!@]?-?[A-Za-z0-9_][\w:/\[\].%#(),-]*$/;
1681
+ function isCandidate(t) {
1682
+ return !!t && t.length <= 96 && CANDIDATE_RE.test(t) && !/url\(/i.test(t);
1683
+ }
1684
+ __name(isCandidate, "isCandidate");
1622
1685
  var MACRO_CLASSES = [
1623
1686
  "mx-auto w-full px-6",
1624
1687
  "inline-flex items-center justify-center font-medium transition-all duration-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
@@ -1692,7 +1755,7 @@ function deriveSchema(fields, props, base) {
1692
1755
  __name(deriveSchema, "deriveSchema");
1693
1756
 
1694
1757
  // ../variant-compiler/src/index.ts
1695
- var COMPILER_VERSION = "0.1.0";
1758
+ var COMPILER_VERSION = "0.1.2";
1696
1759
  var IR_MAX_BYTES = 256 * 1024;
1697
1760
  var CSS_MAX_BYTES = 64 * 1024;
1698
1761
  var STRICT_CODES = /* @__PURE__ */ new Set(["dead-text", "hardcoded-color", "hardcoded-radius", "hardcoded-font", "hardcoded-image", "img-alt", "props-optional"]);
@@ -1765,12 +1828,22 @@ async function compile2(source, opts) {
1765
1828
  if (tpl.scripts[0]) lint.push(...lintScript(script, tpl.scripts[0].attrs).map((m) => ({ ...m, line: m.line != null ? m.line + tpl.scripts[0].line : tpl.scripts[0].line })));
1766
1829
  lint.push(...lintHtml(tpl.root, opts.sectionType));
1767
1830
  const strings = [];
1831
+ const classTokens = /* @__PURE__ */ new Set();
1768
1832
  walkNodes(tpl.root, (e) => collectStrings(e, strings), (attrs) => {
1769
- for (const v of Object.values(attrs)) if (typeof v === "string") strings.push(v);
1833
+ for (const [k, v] of Object.entries(attrs)) {
1834
+ if (typeof v !== "string") continue;
1835
+ strings.push(v);
1836
+ if (k === "class" || k === "class:list") {
1837
+ for (const t of v.split(/\s+/)) if (t) classTokens.add(t);
1838
+ }
1839
+ }
1770
1840
  });
1771
1841
  for (const c of front.consts) collectStrings(c.e, strings);
1772
1842
  for (const p of front.props) if (p.def) collectStrings(p.def, strings);
1773
1843
  const classCandidates = pickCandidates(strings);
1844
+ for (const t of classTokens) {
1845
+ if (!isCandidate(t)) lint.push({ level: "warning", code: "tailwind-skipped", message: `class token '${t}' is not a shape the compiler accepts, so no CSS is generated for it \u2014 rewrite it (e.g. an arbitrary value like mt-[-4rem]) or drop it` });
1846
+ }
1774
1847
  let twCss = "";
1775
1848
  if (!opts.skipTailwind) {
1776
1849
  try {
@@ -2009,8 +2082,15 @@ function collectStrings(e, out) {
2009
2082
  __name(collectStrings, "collectStrings");
2010
2083
 
2011
2084
  // ../variant-compiler/src/runtime/eval.ts
2085
+ var JSX_VALUES = /* @__PURE__ */ new WeakSet();
2086
+ function makeJsxValue(nodes, scope) {
2087
+ const v = { __wvJsx: true, nodes, scope };
2088
+ JSX_VALUES.add(v);
2089
+ return v;
2090
+ }
2091
+ __name(makeJsxValue, "makeJsxValue");
2012
2092
  function isJsxValue(v) {
2013
- return typeof v === "object" && v !== null && v.__wvJsx === true;
2093
+ return typeof v === "object" && v !== null && JSX_VALUES.has(v);
2014
2094
  }
2015
2095
  __name(isJsxValue, "isJsxValue");
2016
2096
  var FORBIDDEN_PROPS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
@@ -2149,7 +2229,7 @@ function evalExpr(e, scope, budget) {
2149
2229
  return evalExpr(e.body, s, budget);
2150
2230
  };
2151
2231
  case "jsx":
2152
- return { __wvJsx: true, nodes: e.n, scope };
2232
+ return makeJsxValue(e.n, scope);
2153
2233
  }
2154
2234
  }
2155
2235
  __name(evalExpr, "evalExpr");
@@ -2332,11 +2412,24 @@ function renderVariant(ir, content, ctx, opts = {}) {
2332
2412
  }
2333
2413
  for (const c of ir.consts) scope[c.name] = evalExpr(c.e, scope, budget);
2334
2414
  const html = renderNodes(ir.root, scope, ctx, budget);
2335
- const css = ir.css ? ir.css.replace(/__WV_ROOT__/g, `[data-wv-inst="${uid}"]`) : "";
2415
+ const css = ir.css ? layerize(ir.css.replace(/__WV_ROOT__/g, `[data-wv-inst="${uid}"]`)) : "";
2336
2416
  const script = ir.script ? wrapScript(ir.script, uid) : "";
2337
2417
  return { html, css, script, uid };
2338
2418
  }
2339
2419
  __name(renderVariant, "renderVariant");
2420
+ var STATEMENT_AT = /^\s*@[a-zA-Z-]+[^;{}]*;/;
2421
+ function layerize(css) {
2422
+ let rest = css;
2423
+ let head = "";
2424
+ for (; ; ) {
2425
+ const m = rest.match(STATEMENT_AT);
2426
+ if (!m) break;
2427
+ head += m[0].trim();
2428
+ rest = rest.slice(m[0].length);
2429
+ }
2430
+ return rest.trim() ? `${head}@layer wvf{${rest}}` : head;
2431
+ }
2432
+ __name(layerize, "layerize");
2340
2433
  function wrapScript(body, uid) {
2341
2434
  return `(function(${SHADOWED_GLOBALS.join(",")}){"use strict";var root=document.querySelector('[data-wv-inst="${uid}"]');if(!root)return;
2342
2435
  ${body}
@@ -2357,10 +2450,13 @@ __name(isPreviewThemeKey, "isPreviewThemeKey");
2357
2450
  function buildPreviewDoc(ir, content, themeKey, opts = {}) {
2358
2451
  const th = PREVIEW_THEMES[themeKey];
2359
2452
  const out = renderVariant(ir, content, { t: /* @__PURE__ */ __name((k) => k, "t"), editChrome: opts.editChrome ?? false }, { uid: "wv-preview" });
2360
- const vars = `--background:${th.background};--foreground:${th.foreground};--card:${th.card};--card-foreground:${th.foreground};--primary:${th.primary};--primary-foreground:${th.primaryFg};--secondary:${th.secondary};--secondary-foreground:#fff;--muted:${th.muted};--muted-foreground:${th.mutedFg};--accent:${th.accent};--accent-foreground:#000;--border:${th.border};--input:${th.border};--ring:${th.primary};--radius:${th.radius};--font-heading:${th.heading};--font-body:${th.body};--gradient-from:${th.primary};--gradient-to:${th.secondary};--spacing:.25rem;--site-max-width:72rem;`;
2453
+ const vars = `--background:${th.background};--foreground:${th.foreground};--card:${th.card};--card-foreground:${th.foreground};--primary:${th.primary};--primary-foreground:${th.primaryFg};--secondary:${th.secondary};--secondary-foreground:#fff;--muted:${th.muted};--muted-foreground:${th.mutedFg};--accent:${th.accent};--accent-foreground:#000;--border:${th.border};--input:${th.border};--ring:${th.primary};--radius:${th.radius};--font-heading:${th.heading};--font-body:${th.body};--gradient-from:${th.primary};--gradient-to:${th.secondary};--spacing:.25rem;--site-max-width:72rem;--font-heading-size:48px;--font-body-size:16px;`;
2361
2454
  const tokens = `--color-background:var(--background);--color-foreground:var(--foreground);--color-card:var(--card);--color-card-foreground:var(--card-foreground);--color-primary:var(--primary);--color-primary-foreground:var(--primary-foreground);--color-secondary:var(--secondary);--color-secondary-foreground:var(--secondary-foreground);--color-muted:var(--muted);--color-muted-foreground:var(--muted-foreground);--color-accent:var(--accent);--color-accent-foreground:var(--accent-foreground);--color-border:var(--border);--color-input:var(--input);--color-ring:var(--ring);--radius-sm:calc(var(--radius) - 4px);--radius-md:calc(var(--radius) - 2px);--radius-lg:var(--radius);--radius-xl:calc(var(--radius) + 4px);`;
2362
2455
  return `<!doctype html><html data-theme="${themeKey === "dark" ? "dark" : "light"}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex">
2363
- <style>*,::before,::after{box-sizing:border-box;border:0 solid var(--color-border)}html{font-size:16px;-webkit-text-size-adjust:100%}body{margin:0;font-family:var(--font-body);color:var(--foreground);background:var(--background);line-height:1.5}h1,h2,h3,h4,p{margin:0}h1{font-size:48px}h2{font-size:36px}h3{font-size:28px}img,video,svg{display:block;max-width:100%;height:auto}a{color:inherit;text-decoration:inherit}button{font:inherit;color:inherit;background:none}ul,ol{margin:0;padding:0;list-style:none}:root{${vars}${tokens}}.edit-url-pill{display:none!important}</style>
2456
+ <style>@layer wvfbase{*,::before,::after{box-sizing:border-box;border:0 solid var(--color-border)}html{font-size:var(--font-body-size,16px);-webkit-text-size-adjust:100%}body{margin:0;font-family:var(--font-body);color:var(--foreground);background:var(--background);line-height:1.5}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}h1,h2,h3,h4,p{margin:0}img,video,svg{display:block;max-width:100%;height:auto}a{color:inherit;text-decoration:inherit}button{font:inherit;color:inherit;background:none}ul,ol{margin:0;padding:0;list-style:none}}
2457
+ :root{${vars}${tokens}}
2458
+ h1{font-size:var(--font-heading-size,48px)}h2{font-size:calc(var(--font-heading-size,48px) * 0.75)}h3{font-size:calc(var(--font-heading-size,48px) * 0.6)}h4{font-size:calc(var(--font-heading-size,48px) * 0.5)}
2459
+ .edit-url-pill{display:none!important}</style>
2364
2460
  <style>${out.css}</style></head><body><div data-wv-root data-wv-inst="${out.uid}">${out.html}</div>${out.script ? `<script>${out.script}</script>` : ""}</body></html>`;
2365
2461
  }
2366
2462
  __name(buildPreviewDoc, "buildPreviewDoc");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webto-id/variant-check",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Lint, compile and preview a Webto Variant Format (.astro subset) section variant with the exact compiler the webto.id marketplace uses.",
5
5
  "license": "MIT",
6
6
  "type": "module",