@tonycodes-ui/mortar 0.12.0 → 0.13.0
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/CHANGELOG.md +16 -39
- package/DESIGN.md +1 -1
- package/catalog.json +413 -377
- package/cli/audit.mjs +152 -0
- package/dist/agent-data.json +1 -1
- package/dist/application.d.ts +30 -5
- package/dist/application.js +20 -8
- package/dist/index.d.ts +1 -1
- package/dist/inspection.js +17 -1
- package/dist/internal.d.ts +15 -1
- package/dist/internal.js +12 -1
- package/dist/marketing.d.ts +3 -2
- package/dist/marketing.js +2 -6
- package/dist/primitives.js +20 -11
- package/docs/agents.md +21 -18
- package/docs/theming.md +72 -1
- package/package.json +1 -1
- package/skills/mortar/SKILL.md +1 -1
- package/src/application.css +95 -1
- package/src/application.tsx +88 -19
- package/src/data.css +58 -2
- package/src/foundation.css +10 -0
- package/src/index.ts +3 -0
- package/src/inspection.tsx +15 -0
- package/src/internal.tsx +93 -2
- package/src/marketing.tsx +4 -74
- package/src/primitives.tsx +38 -19
package/cli/audit.mjs
CHANGED
|
@@ -44,6 +44,8 @@ export const RULES = {
|
|
|
44
44
|
"missing-theme-provider":
|
|
45
45
|
"Mortar components are rendered but ThemeProvider is never rendered.",
|
|
46
46
|
"local-copy": "A local copy of Mortar source.",
|
|
47
|
+
"raw-element":
|
|
48
|
+
"A raw HTML element (such as <table> or <select>) that a Mortar component replaces, in an app that uses Mortar.",
|
|
47
49
|
"limited-data": "The audit could not use the installed version's own data.",
|
|
48
50
|
};
|
|
49
51
|
|
|
@@ -292,6 +294,110 @@ export function jsxAttributes(src, i) {
|
|
|
292
294
|
return attrs;
|
|
293
295
|
}
|
|
294
296
|
|
|
297
|
+
/**
|
|
298
|
+
* The text with string and template-literal contents blanked, so HTML inside
|
|
299
|
+
* strings (an email template, say) is not read as JSX. Lengths and newlines
|
|
300
|
+
* are kept, so offsets still map to lines.
|
|
301
|
+
*
|
|
302
|
+
* A quote starts a string when it follows an operator, a bracket, `=>`, or
|
|
303
|
+
* `return`, `case`, `throw`, `from` or `import`. An apostrophe in JSX text
|
|
304
|
+
* (`Don't`) does not. Template literals recurse into `${…}`: strings and
|
|
305
|
+
* nested templates there are blanked, and a backtick quoted inside the
|
|
306
|
+
* interpolation does not end the outer template or blank the lines after it.
|
|
307
|
+
* JSX in an interpolation is left in place.
|
|
308
|
+
*/
|
|
309
|
+
export function blankStrings(src) {
|
|
310
|
+
const out = src.split("");
|
|
311
|
+
const n = src.length;
|
|
312
|
+
// A quote in JSX text (Don't) is not a string.
|
|
313
|
+
const starts = (i) => {
|
|
314
|
+
const q = src[i];
|
|
315
|
+
if (q !== '"' && q !== "'" && q !== "`") return false;
|
|
316
|
+
if (q === "`") return true;
|
|
317
|
+
const before = src.slice(0, i).trimEnd();
|
|
318
|
+
return /(?:^|=>|[=(,:?&|[{}+;!]|\b(?:return|case|throw|from|import))$/.test(
|
|
319
|
+
before,
|
|
320
|
+
);
|
|
321
|
+
};
|
|
322
|
+
const blank = (from, to) => {
|
|
323
|
+
for (let k = from; k < Math.min(to, n); k++)
|
|
324
|
+
if (out[k] !== "\n") out[k] = " ";
|
|
325
|
+
};
|
|
326
|
+
// Blank the string whose opening quote is at `i`. Returns the index of the
|
|
327
|
+
// closing quote, the newline that ends a non-template, or `end`.
|
|
328
|
+
const blankOne = (i, end) => {
|
|
329
|
+
const q = src[i];
|
|
330
|
+
let j = i + 1;
|
|
331
|
+
let literal = j;
|
|
332
|
+
while (j < end) {
|
|
333
|
+
if (src[j] === "\\") {
|
|
334
|
+
j += 2;
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (q === "`" && src[j] === "$" && src[j + 1] === "{") {
|
|
338
|
+
blank(literal, j);
|
|
339
|
+
const close = blankCode(j + 2, end, true);
|
|
340
|
+
j = close < end ? close + 1 : end;
|
|
341
|
+
literal = j;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (src[j] === q) break;
|
|
345
|
+
if (q !== "`" && src[j] === "\n") break;
|
|
346
|
+
j++;
|
|
347
|
+
}
|
|
348
|
+
blank(literal, j);
|
|
349
|
+
return j;
|
|
350
|
+
};
|
|
351
|
+
// Walk code in [start, end). With `expr`, the `{` of `${` is already
|
|
352
|
+
// counted and the walk stops at the matching `}`.
|
|
353
|
+
const blankCode = (start, end, expr) => {
|
|
354
|
+
let depth = expr ? 1 : 0;
|
|
355
|
+
let i = start;
|
|
356
|
+
while (i < end) {
|
|
357
|
+
const c = src[i];
|
|
358
|
+
if (expr && c === "{") depth++;
|
|
359
|
+
else if (expr && c === "}") {
|
|
360
|
+
if (--depth === 0) return i;
|
|
361
|
+
} else if ((c === '"' || c === "'" || c === "`") && starts(i)) {
|
|
362
|
+
i = blankOne(i, end);
|
|
363
|
+
if (i < end) i++;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
i++;
|
|
367
|
+
}
|
|
368
|
+
return i;
|
|
369
|
+
};
|
|
370
|
+
blankCode(0, n, false);
|
|
371
|
+
return out.join("");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Raw element → the Mortar components that replace it, from each contract's
|
|
376
|
+
* `replaces` (an element name, optionally with `[type=…]` for inputs).
|
|
377
|
+
*/
|
|
378
|
+
function replacements(data) {
|
|
379
|
+
const map = new Map();
|
|
380
|
+
for (const c of data.components ?? [])
|
|
381
|
+
for (const element of c.contract?.replaces ?? [])
|
|
382
|
+
map.set(element, [...(map.get(element) ?? []), c.export]);
|
|
383
|
+
return map;
|
|
384
|
+
}
|
|
385
|
+
// Inputs with no Mortar equivalent (hidden values, file pickers, sliders).
|
|
386
|
+
const NATIVE_ONLY = new Set(["hidden", "file", "range", "color", "image"]);
|
|
387
|
+
const TEXT_LIKE = new Set([
|
|
388
|
+
"text",
|
|
389
|
+
"email",
|
|
390
|
+
"search",
|
|
391
|
+
"url",
|
|
392
|
+
"tel",
|
|
393
|
+
"number",
|
|
394
|
+
"date",
|
|
395
|
+
"time",
|
|
396
|
+
"datetime-local",
|
|
397
|
+
"month",
|
|
398
|
+
"week",
|
|
399
|
+
]);
|
|
400
|
+
|
|
295
401
|
function lineAt(text) {
|
|
296
402
|
const starts = [0];
|
|
297
403
|
for (let i = 0; i < text.length; i++)
|
|
@@ -568,6 +674,13 @@ export async function auditApp(path = ".", { data: bundled } = {}) {
|
|
|
568
674
|
);
|
|
569
675
|
}
|
|
570
676
|
const classes = new Set(data.agent.classes);
|
|
677
|
+
// An installed release older than the `replaces` field falls back to this
|
|
678
|
+
// tool's mapping; every component it names has existed since 0.1.
|
|
679
|
+
let rawElements = replacements(data);
|
|
680
|
+
if (!rawElements.size) rawElements = replacements(bundled);
|
|
681
|
+
// Raw elements are collected from every JSX file and reported once the
|
|
682
|
+
// whole app is known to use Mortar.
|
|
683
|
+
const rawHits = [];
|
|
571
684
|
|
|
572
685
|
const files = walk(root);
|
|
573
686
|
const hashes = data.agent.sourceHashes ?? {};
|
|
@@ -644,6 +757,30 @@ export async function auditApp(path = ".", { data: bundled } = {}) {
|
|
|
644
757
|
}
|
|
645
758
|
|
|
646
759
|
const src = blankComments(text);
|
|
760
|
+
if (JSX.has(ext)) {
|
|
761
|
+
const jsx = blankStrings(src);
|
|
762
|
+
for (const m of jsx.matchAll(/<([a-z][a-z0-9]*)(?=[\s/>])/g)) {
|
|
763
|
+
const tagName = m[1];
|
|
764
|
+
let element = tagName;
|
|
765
|
+
if (tagName === "input") {
|
|
766
|
+
const type = jsxAttributes(jsx, m.index + m[0].length).find(
|
|
767
|
+
(a) => a.name === "type",
|
|
768
|
+
);
|
|
769
|
+
// A dynamic type is read as a text-like input.
|
|
770
|
+
const value = type
|
|
771
|
+
? src
|
|
772
|
+
.slice(type.index)
|
|
773
|
+
.match(/^type\s*=\s*(?:\{\s*)?(["'`])([^"'`]*)\1/)?.[2]
|
|
774
|
+
: undefined;
|
|
775
|
+
if (value && NATIVE_ONLY.has(value)) continue;
|
|
776
|
+
element =
|
|
777
|
+
value && !TEXT_LIKE.has(value) ? `input[type=${value}]` : "input";
|
|
778
|
+
}
|
|
779
|
+
const components = rawElements.get(element);
|
|
780
|
+
if (!components) continue;
|
|
781
|
+
rawHits.push({ file, line: line(m.index), tag: element, components });
|
|
782
|
+
}
|
|
783
|
+
}
|
|
647
784
|
for (const m of src.matchAll(DEEP)) {
|
|
648
785
|
usesMortar = true;
|
|
649
786
|
add(
|
|
@@ -860,6 +997,21 @@ export async function auditApp(path = ".", { data: bundled } = {}) {
|
|
|
860
997
|
}
|
|
861
998
|
}
|
|
862
999
|
|
|
1000
|
+
if (usesMortar)
|
|
1001
|
+
for (const hit of rawHits) {
|
|
1002
|
+
const names = hit.components.map((c) => `<${c}>`);
|
|
1003
|
+
add(
|
|
1004
|
+
"raw-element",
|
|
1005
|
+
// A raw <button> is often a deliberate custom trigger; the rest are
|
|
1006
|
+
// contract breaks that ship unthemed, untested markup.
|
|
1007
|
+
hit.tag === "button" ? "warning" : "error",
|
|
1008
|
+
hit.file,
|
|
1009
|
+
hit.line,
|
|
1010
|
+
`Raw <${hit.tag.replace(/\[type=(.*)\]/, ' type="$1"')}> in an app that uses Mortar.`,
|
|
1011
|
+
`Use ${names.length > 1 ? `${names.slice(0, -1).join(", ")} or ${names.at(-1)}` : names[0]} from "${PKG}" (mortar component ${hit.components[0]}).`,
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
863
1015
|
// Package-level rules.
|
|
864
1016
|
let pkgJson;
|
|
865
1017
|
const pkgFile = join(root, "package.json");
|