@freeappstore/compliance 0.1.0 → 0.3.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/dist/checks/no-brand-overrides.d.ts +24 -0
  3. package/dist/checks/no-brand-overrides.d.ts.map +1 -0
  4. package/dist/checks/no-brand-overrides.js +164 -0
  5. package/dist/checks/no-brand-overrides.js.map +1 -0
  6. package/dist/checks/no-brand-overrides.test.d.ts +2 -0
  7. package/dist/checks/no-brand-overrides.test.d.ts.map +1 -0
  8. package/dist/checks/no-brand-overrides.test.js +102 -0
  9. package/dist/checks/no-brand-overrides.test.js.map +1 -0
  10. package/dist/checks/no-scroll.d.ts +24 -0
  11. package/dist/checks/no-scroll.d.ts.map +1 -0
  12. package/dist/checks/no-scroll.js +143 -0
  13. package/dist/checks/no-scroll.js.map +1 -0
  14. package/dist/checks/no-scroll.test.d.ts +2 -0
  15. package/dist/checks/no-scroll.test.d.ts.map +1 -0
  16. package/dist/checks/no-scroll.test.js +105 -0
  17. package/dist/checks/no-scroll.test.js.map +1 -0
  18. package/dist/checks/unsafe-vh.d.ts +24 -0
  19. package/dist/checks/unsafe-vh.d.ts.map +1 -0
  20. package/dist/checks/unsafe-vh.js +110 -0
  21. package/dist/checks/unsafe-vh.js.map +1 -0
  22. package/dist/checks/unsafe-vh.test.d.ts +2 -0
  23. package/dist/checks/unsafe-vh.test.d.ts.map +1 -0
  24. package/dist/checks/unsafe-vh.test.js +115 -0
  25. package/dist/checks/unsafe-vh.test.js.map +1 -0
  26. package/dist/checks/viewport-support.d.ts +23 -0
  27. package/dist/checks/viewport-support.d.ts.map +1 -0
  28. package/dist/checks/viewport-support.js +87 -0
  29. package/dist/checks/viewport-support.js.map +1 -0
  30. package/dist/checks/viewport-support.test.d.ts +2 -0
  31. package/dist/checks/viewport-support.test.d.ts.map +1 -0
  32. package/dist/checks/viewport-support.test.js +63 -0
  33. package/dist/checks/viewport-support.test.js.map +1 -0
  34. package/dist/index.d.ts +7 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +13 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/live/index.d.ts +50 -0
  39. package/dist/live/index.d.ts.map +1 -0
  40. package/dist/live/index.js +311 -0
  41. package/dist/live/index.js.map +1 -0
  42. package/dist/live/index.test.d.ts +2 -0
  43. package/dist/live/index.test.d.ts.map +1 -0
  44. package/dist/live/index.test.js +259 -0
  45. package/dist/live/index.test.js.map +1 -0
  46. package/package.json +7 -7
@@ -0,0 +1,24 @@
1
+ import type { CheckResult } from '../types.js';
2
+ /**
3
+ * Flag `100vh` (and Tailwind shortcuts that compile to it) in source.
4
+ *
5
+ * Why: on iOS Safari with the URL bar visible, `100vh` resolves to the
6
+ * *layout* viewport (e.g. 852px on iPhone 15) but the actual visible
7
+ * area is the *visual* viewport (~750px). An element sized at `100vh`
8
+ * is then ~100px taller than the screen, forcing the page to scroll on
9
+ * first load. This is invisible to headless Playwright (no dynamic URL
10
+ * bar) and to the static audit Worker (no rendering), so this fast
11
+ * static rule is the cheapest place to catch it.
12
+ *
13
+ * Fix: use `100svh` (small viewport height — accounts for visible
14
+ * browser UI) for "as tall as the screen" intent, or `100dvh` (dynamic
15
+ * viewport height — recomputes as the URL bar shows/hides) for full-
16
+ * height containers that should grow when chrome retracts.
17
+ *
18
+ * Scope: warn-by-default, not fail. There ARE legitimate uses of
19
+ * `100vh` (intentionally taking layout-viewport height for hero
20
+ * sections that should stay constant as URL bar moves). Creators can
21
+ * acknowledge by leaving an `// allow-100vh` comment on the same line.
22
+ */
23
+ export declare function checkUnsafeVh(repoDir: string): Promise<CheckResult>;
24
+ //# sourceMappingURL=unsafe-vh.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unsafe-vh.d.ts","sourceRoot":"","sources":["../../src/checks/unsafe-vh.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG/C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA2CzE"}
@@ -0,0 +1,110 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { extname, relative } from 'node:path';
3
+ import { walk } from '../lib/walk.js';
4
+ /**
5
+ * Flag `100vh` (and Tailwind shortcuts that compile to it) in source.
6
+ *
7
+ * Why: on iOS Safari with the URL bar visible, `100vh` resolves to the
8
+ * *layout* viewport (e.g. 852px on iPhone 15) but the actual visible
9
+ * area is the *visual* viewport (~750px). An element sized at `100vh`
10
+ * is then ~100px taller than the screen, forcing the page to scroll on
11
+ * first load. This is invisible to headless Playwright (no dynamic URL
12
+ * bar) and to the static audit Worker (no rendering), so this fast
13
+ * static rule is the cheapest place to catch it.
14
+ *
15
+ * Fix: use `100svh` (small viewport height — accounts for visible
16
+ * browser UI) for "as tall as the screen" intent, or `100dvh` (dynamic
17
+ * viewport height — recomputes as the URL bar shows/hides) for full-
18
+ * height containers that should grow when chrome retracts.
19
+ *
20
+ * Scope: warn-by-default, not fail. There ARE legitimate uses of
21
+ * `100vh` (intentionally taking layout-viewport height for hero
22
+ * sections that should stay constant as URL bar moves). Creators can
23
+ * acknowledge by leaving an `// allow-100vh` comment on the same line.
24
+ */
25
+ export async function checkUnsafeVh(repoDir) {
26
+ const issues = [];
27
+ for await (const file of walk(repoDir)) {
28
+ const ext = extname(file).toLowerCase();
29
+ if (!SCAN_EXTS.has(ext))
30
+ continue;
31
+ const content = await readFile(file, 'utf8').catch(() => '');
32
+ if (!content)
33
+ continue;
34
+ const rel = relative(repoDir, file);
35
+ for (const { re, label } of PATTERNS) {
36
+ // Reset lastIndex defensively; we use `g` to find all matches.
37
+ re.lastIndex = 0;
38
+ let m;
39
+ while ((m = re.exec(content)) !== null) {
40
+ const line = lineNumberAt(content, m.index);
41
+ // Allow opt-out via an inline `allow-100vh` comment on the same line.
42
+ if (lineHasAllowComment(content, m.index))
43
+ continue;
44
+ issues.push(`${rel}:${line} ${label} — use 100svh or 100dvh`);
45
+ }
46
+ }
47
+ }
48
+ if (issues.length === 0) {
49
+ return {
50
+ name: 'No unsafe 100vh',
51
+ status: 'pass',
52
+ detail: 'no 100vh / h-screen patterns found',
53
+ };
54
+ }
55
+ const head = issues.slice(0, MAX_REPORTED);
56
+ const tail = issues.length > MAX_REPORTED ? [`...and ${issues.length - MAX_REPORTED} more`] : [];
57
+ return {
58
+ name: 'No unsafe 100vh',
59
+ status: 'warn',
60
+ detail: `${issues.length} occurrence${issues.length === 1 ? '' : 's'} of 100vh / h-screen — breaks on iOS Safari with URL bar visible`,
61
+ suggestions: [
62
+ ...head,
63
+ ...tail,
64
+ 'Replace with 100svh (preferred) or 100dvh. Add `// allow-100vh` on the line if intentional.',
65
+ ],
66
+ };
67
+ }
68
+ const SCAN_EXTS = new Set(['.css', '.scss', '.tsx', '.ts', '.jsx', '.js', '.html']);
69
+ const MAX_REPORTED = 5;
70
+ /**
71
+ * Patterns to flag. Each is global (`g`) so we find every occurrence.
72
+ *
73
+ * Boundary handling:
74
+ * - `100vh` uses `\b` since CSS values are tokenised normally.
75
+ * - Tailwind classes use a custom hyphen-aware boundary
76
+ * `(?<![\w-])foo(?![\w-])` so `h-screen` doesn't match inside
77
+ * `min-h-screen` (and we don't double-count `min-h-screen` as both
78
+ * itself and its `h-screen` substring).
79
+ */
80
+ const PATTERNS = [
81
+ { re: /\b100vh\b/g, label: '100vh' },
82
+ // Tailwind: h-screen → height: 100vh; same for min-/max-.
83
+ { re: /(?<![\w-])h-screen(?![\w-])/g, label: 'h-screen (Tailwind: → 100vh)' },
84
+ { re: /(?<![\w-])min-h-screen(?![\w-])/g, label: 'min-h-screen (Tailwind: → min-height: 100vh)' },
85
+ { re: /(?<![\w-])max-h-screen(?![\w-])/g, label: 'max-h-screen (Tailwind: → max-height: 100vh)' },
86
+ ];
87
+ function lineNumberAt(content, index) {
88
+ let n = 1;
89
+ for (let i = 0; i < index; i++)
90
+ if (content.charCodeAt(i) === 10)
91
+ n++;
92
+ return n;
93
+ }
94
+ /**
95
+ * True if the rest of the line containing `index` includes the literal
96
+ * string "allow-100vh" (typically inside a comment). Cheap and cheerful
97
+ * — doesn't validate comment syntax, just trusts the marker.
98
+ */
99
+ function lineHasAllowComment(content, index) {
100
+ // Find end of the current line.
101
+ let end = content.indexOf('\n', index);
102
+ if (end === -1)
103
+ end = content.length;
104
+ // Walk back to the start of the line for full context.
105
+ let start = content.lastIndexOf('\n', index);
106
+ if (start === -1)
107
+ start = 0;
108
+ return content.slice(start, end).includes('allow-100vh');
109
+ }
110
+ //# sourceMappingURL=unsafe-vh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unsafe-vh.js","sourceRoot":"","sources":["../../src/checks/unsafe-vh.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=unsafe-vh.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unsafe-vh.test.d.ts","sourceRoot":"","sources":["../../src/checks/unsafe-vh.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,115 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mkdtemp, mkdir, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { checkUnsafeVh } from './unsafe-vh.js';
6
+ async function fixture(files) {
7
+ const dir = await mkdtemp(join(tmpdir(), 'fas-unsafe-vh-'));
8
+ for (const [path, content] of Object.entries(files)) {
9
+ const full = join(dir, path);
10
+ await mkdir(join(full, '..'), { recursive: true });
11
+ await writeFile(full, content);
12
+ }
13
+ return dir;
14
+ }
15
+ describe('checkUnsafeVh', () => {
16
+ it('passes when no 100vh appears anywhere', async () => {
17
+ const dir = await fixture({
18
+ 'web/src/index.css': 'body { height: 100svh; }',
19
+ 'web/src/App.tsx': 'export default () => <div className="h-screen-not-really" />;',
20
+ });
21
+ const r = await checkUnsafeVh(dir);
22
+ expect(r.status).toBe('pass');
23
+ expect(r.detail).toMatch(/no 100vh/);
24
+ });
25
+ it('warns on a literal 100vh in CSS', async () => {
26
+ const dir = await fixture({
27
+ 'web/src/index.css': '.full { height: 100vh; }',
28
+ });
29
+ const r = await checkUnsafeVh(dir);
30
+ expect(r.status).toBe('warn');
31
+ expect(r.detail).toMatch(/1 occurrence/);
32
+ expect(r.suggestions?.join('\n')).toMatch(/web\/src\/index\.css:1.*100vh/);
33
+ expect(r.suggestions?.join('\n')).toMatch(/100svh.*100dvh/);
34
+ });
35
+ it('warns on Tailwind h-screen / min-h-screen / max-h-screen', async () => {
36
+ const dir = await fixture({
37
+ 'web/src/App.tsx': `
38
+ export default () => (
39
+ <div className="h-screen">
40
+ <header className="min-h-screen" />
41
+ <main className="max-h-screen" />
42
+ </div>
43
+ );
44
+ `,
45
+ });
46
+ const r = await checkUnsafeVh(dir);
47
+ expect(r.status).toBe('warn');
48
+ expect(r.detail).toMatch(/3 occurrences/);
49
+ const suggestionText = r.suggestions?.join('\n') ?? '';
50
+ expect(suggestionText).toMatch(/h-screen/);
51
+ expect(suggestionText).toMatch(/min-h-screen/);
52
+ expect(suggestionText).toMatch(/max-h-screen/);
53
+ });
54
+ it('respects the `allow-100vh` opt-out comment on the same line', async () => {
55
+ const dir = await fixture({
56
+ 'web/src/index.css': `
57
+ .hero { height: 100vh; /* allow-100vh — intentional, hero stays constant under URL bar */ }
58
+ `,
59
+ });
60
+ const r = await checkUnsafeVh(dir);
61
+ expect(r.status).toBe('pass');
62
+ });
63
+ it('still flags a different line even when one line has the opt-out', async () => {
64
+ const dir = await fixture({
65
+ 'web/src/index.css': `.a { height: 100vh; /* allow-100vh */ }
66
+ .b { height: 100vh; }
67
+ `,
68
+ });
69
+ const r = await checkUnsafeVh(dir);
70
+ expect(r.status).toBe('warn');
71
+ expect(r.detail).toMatch(/1 occurrence/); // only the un-opt-out line
72
+ });
73
+ it('does not flag the strings "100svh" / "100dvh" (the recommended fixes)', async () => {
74
+ const dir = await fixture({
75
+ 'web/src/index.css': `
76
+ .a { height: 100svh; }
77
+ .b { height: 100dvh; }
78
+ `,
79
+ });
80
+ const r = await checkUnsafeVh(dir);
81
+ expect(r.status).toBe('pass');
82
+ });
83
+ it('does not match identifiers that contain "100vh" as a substring', async () => {
84
+ const dir = await fixture({
85
+ // `var100vh` and `--my100vhvar` aren't actual unit usage; word
86
+ // boundaries must keep us from flagging them.
87
+ 'web/src/foo.ts': 'const var100vh = 1; const x = "--my100vhvar";',
88
+ });
89
+ const r = await checkUnsafeVh(dir);
90
+ expect(r.status).toBe('pass');
91
+ });
92
+ it('caps reported issues at 5 and notes the rest', async () => {
93
+ const lines = Array.from({ length: 8 }, (_, i) => `.x${i} { height: 100vh; }`).join('\n');
94
+ const dir = await fixture({ 'web/src/index.css': lines });
95
+ const r = await checkUnsafeVh(dir);
96
+ expect(r.status).toBe('warn');
97
+ expect(r.detail).toMatch(/8 occurrences/);
98
+ // 5 file:line entries + 1 "...and N more" + 1 final "Replace with..." advice = 7
99
+ expect(r.suggestions?.length).toBe(7);
100
+ expect(r.suggestions?.[5]).toMatch(/and 3 more/);
101
+ });
102
+ it('scans CSS, JS, TS, JSX, TSX, HTML — but skips other extensions', async () => {
103
+ const dir = await fixture({
104
+ 'web/src/index.css': '.a { height: 100vh; }',
105
+ 'web/src/App.jsx': 'const x = "h-screen";', // string with the class — still flagged (we can't know intent)
106
+ 'docs/notes.md': 'Use 100vh somewhere', // Markdown — should NOT be scanned
107
+ 'web/src/foo.scss': '$h: 100vh;',
108
+ });
109
+ const r = await checkUnsafeVh(dir);
110
+ expect(r.status).toBe('warn');
111
+ // 3 hits: index.css, App.jsx, foo.scss. notes.md ignored.
112
+ expect(r.detail).toMatch(/3 occurrences/);
113
+ });
114
+ });
115
+ //# sourceMappingURL=unsafe-vh.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unsafe-vh.test.js","sourceRoot":"","sources":["../../src/checks/unsafe-vh.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAE/C,KAAK,UAAU,OAAO,CAAC,KAA6B;IAClD,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,gBAAgB,CAAC,CAAC,CAAC;IAC5D,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACpD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAC7B,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACnD,MAAM,SAAS,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACjC,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,EAAE,CAAC,uCAAuC,EAAE,KAAK,IAAI,EAAE;QACrD,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC;YACxB,mBAAmB,EAAE,0BAA0B;YAC/C,iBAAiB,EAAE,+DAA+D;SACnF,CAAC,CAAC;QACH,MAAM,CAAC,GAAG,MAAM,aAAa,CAAC,GAAG,CAAC,CAAC;QACnC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC9B,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iCAAiC,EAAE,KAAK,IAAI,EAAE;QAC/C,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC;YACxB,mBAAmB,EAAE,0BAA0B;SAChD,CAAC,CAAC;QACH,MAAM,CAAC,GAAG,MAAM,aAAa,CAAC,GAAG,CAAC,CAAC;QACnC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC9B,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;QACzC,MAAM,CAAC,CAAC,CAAC,WAAW,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,+BAA+B,CAAC,CAAC;QAC3E,MAAM,CAAC,CAAC,CAAC,WAAW,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0DAA0D,EAAE,KAAK,IAAI,EAAE;QACxE,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC;YACxB,iBAAiB,EAAE;;;;;;;OAOlB;SACF,CAAC,CAAC;QACH,MAAM,CAAC,GAAG,MAAM,aAAa,CAAC,GAAG,CAAC,CAAC;QACnC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC9B,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;QAC1C,MAAM,cAAc,GAAG,CAAC,CAAC,WAAW,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACvD,MAAM,CAAC,cAAc,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QAC3C,MAAM,CAAC,cAAc,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;QAC/C,MAAM,CAAC,cAAc,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6DAA6D,EAAE,KAAK,IAAI,EAAE;QAC3E,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC;YACxB,mBAAmB,EAAE;;OAEpB;SACF,CAAC,CAAC;QACH,MAAM,CAAC,GAAG,MAAM,aAAa,CAAC,GAAG,CAAC,CAAC;QACnC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iEAAiE,EAAE,KAAK,IAAI,EAAE;QAC/E,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC;YACxB,mBAAmB,EAAE;;CAE1B;SACI,CAAC,CAAC;QACH,MAAM,CAAC,GAAG,MAAM,aAAa,CAAC,GAAG,CAAC,CAAC;QACnC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC9B,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,2BAA2B;IACvE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uEAAuE,EAAE,KAAK,IAAI,EAAE;QACrF,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC;YACxB,mBAAmB,EAAE;;;OAGpB;SACF,CAAC,CAAC;QACH,MAAM,CAAC,GAAG,MAAM,aAAa,CAAC,GAAG,CAAC,CAAC;QACnC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gEAAgE,EAAE,KAAK,IAAI,EAAE;QAC9E,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC;YACxB,+DAA+D;YAC/D,8CAA8C;YAC9C,gBAAgB,EAAE,+CAA+C;SAClE,CAAC,CAAC;QACH,MAAM,CAAC,GAAG,MAAM,aAAa,CAAC,GAAG,CAAC,CAAC;QACnC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8CAA8C,EAAE,KAAK,IAAI,EAAE;QAC5D,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1F,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,EAAE,mBAAmB,EAAE,KAAK,EAAE,CAAC,CAAC;QAC1D,MAAM,CAAC,GAAG,MAAM,aAAa,CAAC,GAAG,CAAC,CAAC;QACnC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC9B,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;QAC1C,iFAAiF;QACjF,MAAM,CAAC,CAAC,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACtC,MAAM,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;IACnD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gEAAgE,EAAE,KAAK,IAAI,EAAE;QAC9E,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC;YACxB,mBAAmB,EAAE,uBAAuB;YAC5C,iBAAiB,EAAE,uBAAuB,EAAE,+DAA+D;YAC3G,eAAe,EAAE,qBAAqB,EAAM,mCAAmC;YAC/E,kBAAkB,EAAE,YAAY;SACjC,CAAC,CAAC;QACH,MAAM,CAAC,GAAG,MAAM,aAAa,CAAC,GAAG,CAAC,CAAC;QACnC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC9B,0DAA0D;QAC1D,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,23 @@
1
+ import type { CheckResult } from '../types.js';
2
+ /**
3
+ * Apps and games must declare which screens and orientations they
4
+ * support, so the storefront can render a coverage badge ("works on
5
+ * 92% of devices · portrait + landscape") and visitors know what to
6
+ * expect before they tap.
7
+ *
8
+ * Two declarations are required, both in `web/public/manifest.json`:
9
+ *
10
+ * "orientation": "any" | "portrait" | "landscape" |
11
+ * "portrait-primary" | "landscape-primary"
12
+ * "min_viewport_width": 320 | 360 | 414 | 600 | 768 | 1024
13
+ *
14
+ * `orientation` is a standard PWA manifest field. `min_viewport_width`
15
+ * is a custom platform field — the smallest screen width (in CSS px)
16
+ * the app renders correctly at. The storefront maps that to a global
17
+ * device-share percentile to render coverage.
18
+ *
19
+ * If you genuinely don't care about orientation, set `"any"`. Don't
20
+ * leave it off — that's the only value that returns a fail.
21
+ */
22
+ export declare function checkViewportSupport(repoDir: string): Promise<CheckResult>;
23
+ //# sourceMappingURL=viewport-support.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewport-support.d.ts","sourceRoot":"","sources":["../../src/checks/viewport-support.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA+DhF"}
@@ -0,0 +1,87 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ /**
4
+ * Apps and games must declare which screens and orientations they
5
+ * support, so the storefront can render a coverage badge ("works on
6
+ * 92% of devices · portrait + landscape") and visitors know what to
7
+ * expect before they tap.
8
+ *
9
+ * Two declarations are required, both in `web/public/manifest.json`:
10
+ *
11
+ * "orientation": "any" | "portrait" | "landscape" |
12
+ * "portrait-primary" | "landscape-primary"
13
+ * "min_viewport_width": 320 | 360 | 414 | 600 | 768 | 1024
14
+ *
15
+ * `orientation` is a standard PWA manifest field. `min_viewport_width`
16
+ * is a custom platform field — the smallest screen width (in CSS px)
17
+ * the app renders correctly at. The storefront maps that to a global
18
+ * device-share percentile to render coverage.
19
+ *
20
+ * If you genuinely don't care about orientation, set `"any"`. Don't
21
+ * leave it off — that's the only value that returns a fail.
22
+ */
23
+ export async function checkViewportSupport(repoDir) {
24
+ const manifestPath = join(repoDir, 'web', 'public', 'manifest.json');
25
+ let manifest;
26
+ try {
27
+ const raw = await readFile(manifestPath, 'utf8');
28
+ manifest = JSON.parse(raw);
29
+ }
30
+ catch {
31
+ return {
32
+ name: 'Viewport support',
33
+ status: 'fail',
34
+ detail: 'web/public/manifest.json not found or unparseable',
35
+ };
36
+ }
37
+ const orientation = manifest['orientation'];
38
+ const minWidth = manifest['min_viewport_width'];
39
+ const issues = [];
40
+ const warnings = [];
41
+ if (typeof orientation !== 'string' || !VALID_ORIENTATIONS.has(orientation)) {
42
+ issues.push(`manifest.orientation is "${String(orientation)}" — must be one of: ${[...VALID_ORIENTATIONS].join(', ')}`);
43
+ }
44
+ if (typeof minWidth !== 'number') {
45
+ warnings.push('manifest.min_viewport_width is missing — assuming 320 (strictest). ' +
46
+ 'Set explicitly to e.g. 360 / 414 / 600 / 768 / 1024 to declare which devices the app supports.');
47
+ }
48
+ else if (minWidth < 320) {
49
+ issues.push(`manifest.min_viewport_width=${minWidth} is below the 320px minimum`);
50
+ }
51
+ else if (!RECOMMENDED_WIDTHS.includes(minWidth)) {
52
+ warnings.push(`manifest.min_viewport_width=${minWidth} is unusual — recommended values: ${RECOMMENDED_WIDTHS.join(', ')}`);
53
+ }
54
+ if (issues.length > 0) {
55
+ return {
56
+ name: 'Viewport support',
57
+ status: 'fail',
58
+ detail: `${issues.length} viewport-declaration issue${issues.length === 1 ? '' : 's'}`,
59
+ suggestions: [
60
+ ...issues,
61
+ 'Edit web/public/manifest.json: add "orientation": "any" (or portrait/landscape) and "min_viewport_width": 360.',
62
+ ],
63
+ };
64
+ }
65
+ if (warnings.length > 0) {
66
+ return {
67
+ name: 'Viewport support',
68
+ status: 'warn',
69
+ detail: warnings[0],
70
+ suggestions: warnings,
71
+ };
72
+ }
73
+ return {
74
+ name: 'Viewport support',
75
+ status: 'pass',
76
+ detail: `orientation=${orientation} · min ${minWidth}px`,
77
+ };
78
+ }
79
+ const VALID_ORIENTATIONS = new Set([
80
+ 'any',
81
+ 'portrait',
82
+ 'landscape',
83
+ 'portrait-primary',
84
+ 'landscape-primary',
85
+ ]);
86
+ const RECOMMENDED_WIDTHS = [320, 360, 414, 600, 768, 1024];
87
+ //# sourceMappingURL=viewport-support.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewport-support.js","sourceRoot":"","sources":["../../src/checks/viewport-support.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=viewport-support.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewport-support.test.d.ts","sourceRoot":"","sources":["../../src/checks/viewport-support.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,63 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mkdtemp, mkdir, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { checkViewportSupport } from './viewport-support.js';
6
+ async function fixture(manifest) {
7
+ const dir = await mkdtemp(join(tmpdir(), 'fas-viewport-'));
8
+ if (manifest !== null) {
9
+ await mkdir(join(dir, 'web', 'public'), { recursive: true });
10
+ await writeFile(join(dir, 'web', 'public', 'manifest.json'), JSON.stringify(manifest));
11
+ }
12
+ return dir;
13
+ }
14
+ describe('checkViewportSupport', () => {
15
+ it('fails when manifest.json is missing', async () => {
16
+ const dir = await fixture(null);
17
+ const r = await checkViewportSupport(dir);
18
+ expect(r.status).toBe('fail');
19
+ expect(r.detail).toMatch(/manifest\.json/);
20
+ });
21
+ it('fails when orientation is missing', async () => {
22
+ const dir = await fixture({ name: 'x' });
23
+ const r = await checkViewportSupport(dir);
24
+ expect(r.status).toBe('fail');
25
+ expect(r.suggestions?.join(' ')).toMatch(/orientation/);
26
+ });
27
+ it('fails when orientation is invalid', async () => {
28
+ const dir = await fixture({ name: 'x', orientation: 'rotate' });
29
+ const r = await checkViewportSupport(dir);
30
+ expect(r.status).toBe('fail');
31
+ });
32
+ it('warns when min_viewport_width is missing but orientation is valid', async () => {
33
+ const dir = await fixture({ name: 'x', orientation: 'any' });
34
+ const r = await checkViewportSupport(dir);
35
+ expect(r.status).toBe('warn');
36
+ expect(r.detail).toMatch(/min_viewport_width/);
37
+ });
38
+ it('fails when min_viewport_width is below 320', async () => {
39
+ const dir = await fixture({ name: 'x', orientation: 'any', min_viewport_width: 280 });
40
+ const r = await checkViewportSupport(dir);
41
+ expect(r.status).toBe('fail');
42
+ });
43
+ it('warns on unusual but accepted widths', async () => {
44
+ const dir = await fixture({ name: 'x', orientation: 'any', min_viewport_width: 387 });
45
+ const r = await checkViewportSupport(dir);
46
+ expect(r.status).toBe('warn');
47
+ expect(r.detail).toMatch(/unusual/);
48
+ });
49
+ it('passes for valid orientation + recommended width', async () => {
50
+ const dir = await fixture({ name: 'x', orientation: 'any', min_viewport_width: 360 });
51
+ const r = await checkViewportSupport(dir);
52
+ expect(r.status).toBe('pass');
53
+ expect(r.detail).toMatch(/any/);
54
+ expect(r.detail).toMatch(/360/);
55
+ });
56
+ it('passes for portrait-only landscape-only too', async () => {
57
+ const dir = await fixture({ name: 'x', orientation: 'portrait', min_viewport_width: 414 });
58
+ expect((await checkViewportSupport(dir)).status).toBe('pass');
59
+ const dir2 = await fixture({ name: 'x', orientation: 'landscape-primary', min_viewport_width: 768 });
60
+ expect((await checkViewportSupport(dir2)).status).toBe('pass');
61
+ });
62
+ });
63
+ //# sourceMappingURL=viewport-support.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewport-support.test.js","sourceRoot":"","sources":["../../src/checks/viewport-support.test.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,11 +1,17 @@
1
1
  import { checkNoPlaceholders } from './checks/no-placeholders.js';
2
2
  import { checkNoTracking } from './checks/no-tracking.js';
3
3
  import { checkBrandFonts } from './checks/brand-fonts.js';
4
+ import { checkNoBrandOverrides } from './checks/no-brand-overrides.js';
5
+ import { checkNoScroll } from './checks/no-scroll.js';
6
+ import { checkViewportSupport } from './checks/viewport-support.js';
4
7
  import { checkManifest } from './checks/manifest.js';
5
8
  import { checkBundleSize } from './checks/bundle-size.js';
9
+ import { checkUnsafeVh } from './checks/unsafe-vh.js';
6
10
  import type { CheckResult } from './types.js';
7
11
  export type { CheckStatus, CheckResult } from './types.js';
8
- export { checkNoPlaceholders, checkNoTracking, checkBrandFonts, checkManifest, checkBundleSize, };
12
+ export { checkNoPlaceholders, checkNoTracking, checkBrandFonts, checkNoBrandOverrides, checkNoScroll, checkViewportSupport, checkManifest, checkBundleSize, checkUnsafeVh, };
13
+ export { auditLive, checkNoTrackingLive, checkBrandFontsLive, checkManifestLive, checkBundleSizeLive, checkUnsafeVhLive, } from './live/index.js';
14
+ export type { LiveAuditInput, LiveAuditReport } from './live/index.js';
9
15
  /**
10
16
  * Runs every compliance check against `repoDir` (the root of an app, the
11
17
  * directory containing package.json + web/). Returns results in a stable
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,aAAa,EACb,eAAe,GAChB,CAAC;AAEF;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAQvE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AACpE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,aAAa,EACb,oBAAoB,EACpB,aAAa,EACb,eAAe,EACf,aAAa,GACd,CAAC;AAKF,OAAO,EACL,SAAS,EACT,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvE;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAYvE"}
package/dist/index.js CHANGED
@@ -1,9 +1,17 @@
1
1
  import { checkNoPlaceholders } from './checks/no-placeholders.js';
2
2
  import { checkNoTracking } from './checks/no-tracking.js';
3
3
  import { checkBrandFonts } from './checks/brand-fonts.js';
4
+ import { checkNoBrandOverrides } from './checks/no-brand-overrides.js';
5
+ import { checkNoScroll } from './checks/no-scroll.js';
6
+ import { checkViewportSupport } from './checks/viewport-support.js';
4
7
  import { checkManifest } from './checks/manifest.js';
5
8
  import { checkBundleSize } from './checks/bundle-size.js';
6
- export { checkNoPlaceholders, checkNoTracking, checkBrandFonts, checkManifest, checkBundleSize, };
9
+ import { checkUnsafeVh } from './checks/unsafe-vh.js';
10
+ export { checkNoPlaceholders, checkNoTracking, checkBrandFonts, checkNoBrandOverrides, checkNoScroll, checkViewportSupport, checkManifest, checkBundleSize, checkUnsafeVh, };
11
+ // Live-URL audit (used by the compliance audit Worker; runs in
12
+ // browser/Workers env, no filesystem). Separate export path so callers
13
+ // don't accidentally pull node:fs in via the file-walking checks.
14
+ export { auditLive, checkNoTrackingLive, checkBrandFontsLive, checkManifestLive, checkBundleSizeLive, checkUnsafeVhLive, } from './live/index.js';
7
15
  /**
8
16
  * Runs every compliance check against `repoDir` (the root of an app, the
9
17
  * directory containing package.json + web/). Returns results in a stable
@@ -14,6 +22,10 @@ export async function runChecks(repoDir) {
14
22
  checkNoPlaceholders(repoDir),
15
23
  checkNoTracking(repoDir),
16
24
  checkBrandFonts(repoDir),
25
+ checkNoBrandOverrides(repoDir),
26
+ checkNoScroll(repoDir),
27
+ checkViewportSupport(repoDir),
28
+ checkUnsafeVh(repoDir),
17
29
  checkManifest(repoDir),
18
30
  checkBundleSize(repoDir),
19
31
  ]);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAI1D,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,aAAa,EACb,eAAe,GAChB,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,OAAe;IAC7C,OAAO,OAAO,CAAC,GAAG,CAAC;QACjB,mBAAmB,CAAC,OAAO,CAAC;QAC5B,eAAe,CAAC,OAAO,CAAC;QACxB,eAAe,CAAC,OAAO,CAAC;QACxB,aAAa,CAAC,OAAO,CAAC;QACtB,eAAe,CAAC,OAAO,CAAC;KACzB,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AACpE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAItD,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,aAAa,EACb,oBAAoB,EACpB,aAAa,EACb,eAAe,EACf,aAAa,GACd,CAAC;AAEF,+DAA+D;AAC/D,uEAAuE;AACvE,kEAAkE;AAClE,OAAO,EACL,SAAS,EACT,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;AAGzB;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,OAAe;IAC7C,OAAO,OAAO,CAAC,GAAG,CAAC;QACjB,mBAAmB,CAAC,OAAO,CAAC;QAC5B,eAAe,CAAC,OAAO,CAAC;QACxB,eAAe,CAAC,OAAO,CAAC;QACxB,qBAAqB,CAAC,OAAO,CAAC;QAC9B,aAAa,CAAC,OAAO,CAAC;QACtB,oBAAoB,CAAC,OAAO,CAAC;QAC7B,aAAa,CAAC,OAAO,CAAC;QACtB,aAAa,CAAC,OAAO,CAAC;QACtB,eAAe,CAAC,OAAO,CAAC;KACzB,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Live-URL audit checks. Unlike the file-walking checks in ../checks/,
3
+ * these run inside a Cloudflare Worker (no filesystem) against published
4
+ * apps' live URLs to catch post-publish drift.
5
+ *
6
+ * What we can check from a live URL:
7
+ * - Tracking SDK injection (script src, inline scripts, fetched JS)
8
+ * - Brand fonts presence (link href to fonts.googleapis.com)
9
+ * - PWA manifest reachable + valid
10
+ * - Main bundle size (HEAD request)
11
+ *
12
+ * What we CAN'T check post-publish:
13
+ * - APPNAME placeholders (build erases these)
14
+ * - Brand-override CSS variables (sourcemaps not always shipped)
15
+ *
16
+ * The audit Worker runs these on a weekly cron and stores per-app
17
+ * results in D1 so creators (or platform admins) can see drift over
18
+ * time without leaving the storefront.
19
+ */
20
+ import type { CheckResult } from '../types.js';
21
+ export interface LiveAuditInput {
22
+ appId: string;
23
+ liveUrl: string;
24
+ }
25
+ export interface LiveAuditReport {
26
+ appId: string;
27
+ liveUrl: string;
28
+ checkedAt: number;
29
+ reachable: boolean;
30
+ results: CheckResult[];
31
+ }
32
+ export declare function auditLive(input: LiveAuditInput): Promise<LiveAuditReport>;
33
+ export declare function checkNoTrackingLive(html: string): CheckResult;
34
+ export declare function checkBrandFontsLive(html: string): CheckResult;
35
+ export declare function checkManifestLive(html: string, liveUrl: string): Promise<CheckResult>;
36
+ export declare function checkBundleSizeLive(html: string, liveUrl: string): Promise<CheckResult>;
37
+ /**
38
+ * Live counterpart to checkUnsafeVh. Fetches the first bundled
39
+ * stylesheet linked from the HTML and scans it for `100vh` (the
40
+ * unit, not the Tailwind class — by build time those have been
41
+ * compiled to CSS). Catches whiteboard-class iOS Safari URL-bar
42
+ * scroll bugs in apps that have already shipped.
43
+ *
44
+ * Cost: one extra subrequest per app. The audit Worker runs near the
45
+ * free-plan subrequest cap; on cap errors we degrade to `warn`
46
+ * (re-run needed) rather than `fail` (app broken) — the app isn't
47
+ * actually broken, the audit just couldn't check.
48
+ */
49
+ export declare function checkUnsafeVhLive(html: string, liveUrl: string): Promise<CheckResult>;
50
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/live/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAc/C,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,WAAW,EAAE,CAAC;CACxB;AA2BD,wBAAsB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC,CA2D/E;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAa7D;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAe7D;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA8C3F;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAmD7F;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAgD3F"}