@freeappstore/compliance 0.2.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.
- package/dist/checks/no-scroll.d.ts +24 -0
- package/dist/checks/no-scroll.d.ts.map +1 -0
- package/dist/checks/no-scroll.js +143 -0
- package/dist/checks/no-scroll.js.map +1 -0
- package/dist/checks/no-scroll.test.d.ts +2 -0
- package/dist/checks/no-scroll.test.d.ts.map +1 -0
- package/dist/checks/no-scroll.test.js +105 -0
- package/dist/checks/no-scroll.test.js.map +1 -0
- package/dist/checks/unsafe-vh.d.ts +24 -0
- package/dist/checks/unsafe-vh.d.ts.map +1 -0
- package/dist/checks/unsafe-vh.js +110 -0
- package/dist/checks/unsafe-vh.js.map +1 -0
- package/dist/checks/unsafe-vh.test.d.ts +2 -0
- package/dist/checks/unsafe-vh.test.d.ts.map +1 -0
- package/dist/checks/unsafe-vh.test.js +115 -0
- package/dist/checks/unsafe-vh.test.js.map +1 -0
- package/dist/checks/viewport-support.d.ts +23 -0
- package/dist/checks/viewport-support.d.ts.map +1 -0
- package/dist/checks/viewport-support.js +87 -0
- package/dist/checks/viewport-support.js.map +1 -0
- package/dist/checks/viewport-support.test.d.ts +2 -0
- package/dist/checks/viewport-support.test.d.ts.map +1 -0
- package/dist/checks/viewport-support.test.js +63 -0
- package/dist/checks/viewport-support.test.js.map +1 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/live/index.d.ts +50 -0
- package/dist/live/index.d.ts.map +1 -0
- package/dist/live/index.js +311 -0
- package/dist/live/index.js.map +1 -0
- package/dist/live/index.test.d.ts +2 -0
- package/dist/live/index.test.d.ts.map +1 -0
- package/dist/live/index.test.js +259 -0
- package/dist/live/index.test.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CheckResult } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Games on FreeGameStore must fit the viewport — no horizontal or
|
|
4
|
+
* vertical scroll at the document level. Static analysis catches the
|
|
5
|
+
* common CSS / inline-style anti-patterns:
|
|
6
|
+
*
|
|
7
|
+
* - `overflow: scroll` or `overflow: auto` on `html` or `body`.
|
|
8
|
+
* - `min-height: 100vh` on `html` / `body` (creates page that's at
|
|
9
|
+
* least viewport-tall, but content + topbar = > 100vh = scroll).
|
|
10
|
+
* - Missing the `100svh` / `100vh` height constraint on the layout
|
|
11
|
+
* wrapper (loose check — "at least one root element should hard-cap
|
|
12
|
+
* to viewport height").
|
|
13
|
+
*
|
|
14
|
+
* Runtime guarantees (real DOM measurement) live in the e2e Playwright
|
|
15
|
+
* suite; this check exists as a fast pre-publish gate so creators
|
|
16
|
+
* don't ship trivially-scrolling games.
|
|
17
|
+
*
|
|
18
|
+
* Apps (FreeAppStore) are not subject to this check — the apps Shell
|
|
19
|
+
* wraps them in a sidebar+main layout that legitimately scrolls. We
|
|
20
|
+
* detect "is this a game project" by looking for the @freeappstore/games
|
|
21
|
+
* dep, the canonical signal that the app uses GameShell.
|
|
22
|
+
*/
|
|
23
|
+
export declare function checkNoScroll(repoDir: string): Promise<CheckResult>;
|
|
24
|
+
//# sourceMappingURL=no-scroll.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-scroll.d.ts","sourceRoot":"","sources":["../../src/checks/no-scroll.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,CA6EzE"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { extname, relative } from 'node:path';
|
|
3
|
+
import { walk } from '../lib/walk.js';
|
|
4
|
+
/**
|
|
5
|
+
* Games on FreeGameStore must fit the viewport — no horizontal or
|
|
6
|
+
* vertical scroll at the document level. Static analysis catches the
|
|
7
|
+
* common CSS / inline-style anti-patterns:
|
|
8
|
+
*
|
|
9
|
+
* - `overflow: scroll` or `overflow: auto` on `html` or `body`.
|
|
10
|
+
* - `min-height: 100vh` on `html` / `body` (creates page that's at
|
|
11
|
+
* least viewport-tall, but content + topbar = > 100vh = scroll).
|
|
12
|
+
* - Missing the `100svh` / `100vh` height constraint on the layout
|
|
13
|
+
* wrapper (loose check — "at least one root element should hard-cap
|
|
14
|
+
* to viewport height").
|
|
15
|
+
*
|
|
16
|
+
* Runtime guarantees (real DOM measurement) live in the e2e Playwright
|
|
17
|
+
* suite; this check exists as a fast pre-publish gate so creators
|
|
18
|
+
* don't ship trivially-scrolling games.
|
|
19
|
+
*
|
|
20
|
+
* Apps (FreeAppStore) are not subject to this check — the apps Shell
|
|
21
|
+
* wraps them in a sidebar+main layout that legitimately scrolls. We
|
|
22
|
+
* detect "is this a game project" by looking for the @freeappstore/games
|
|
23
|
+
* dep, the canonical signal that the app uses GameShell.
|
|
24
|
+
*/
|
|
25
|
+
export async function checkNoScroll(repoDir) {
|
|
26
|
+
// Skip apps — only games are subject to no-scroll.
|
|
27
|
+
if (!(await isGame(repoDir))) {
|
|
28
|
+
return {
|
|
29
|
+
name: 'No scroll (games only)',
|
|
30
|
+
status: 'pass',
|
|
31
|
+
detail: 'not a game project — check skipped',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const issues = [];
|
|
35
|
+
let sawViewportLock = false;
|
|
36
|
+
for await (const file of walk(repoDir)) {
|
|
37
|
+
const ext = extname(file).toLowerCase();
|
|
38
|
+
if (!SCAN_EXTS.has(ext))
|
|
39
|
+
continue;
|
|
40
|
+
const content = await readFile(file, 'utf8').catch(() => '');
|
|
41
|
+
if (!content)
|
|
42
|
+
continue;
|
|
43
|
+
const rel = relative(repoDir, file);
|
|
44
|
+
// 1. Forbidden overflow declarations on root elements.
|
|
45
|
+
for (const re of FORBIDDEN_OVERFLOW) {
|
|
46
|
+
const m = re.exec(content);
|
|
47
|
+
if (m) {
|
|
48
|
+
const line = lineNumberAt(content, m.index);
|
|
49
|
+
issues.push(`${rel}:${line} ${m[0]} — root scrolling not allowed in games`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// 2. min-height on root elements (creates pages taller than viewport).
|
|
53
|
+
for (const re of FORBIDDEN_MIN_HEIGHT) {
|
|
54
|
+
const m = re.exec(content);
|
|
55
|
+
if (m) {
|
|
56
|
+
const line = lineNumberAt(content, m.index);
|
|
57
|
+
issues.push(`${rel}:${line} ${m[0]} — use exact viewport height (100svh) instead`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// 3. Look for at least one viewport-lock pattern. GameShell sets
|
|
61
|
+
// `height: 100svh` on a fixed wrapper; we accept any 100svh / 100vh
|
|
62
|
+
// declaration as evidence the game considered viewport sizing.
|
|
63
|
+
if (VIEWPORT_LOCK.test(content))
|
|
64
|
+
sawViewportLock = true;
|
|
65
|
+
if (issues.length >= 8)
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
if (issues.length > 0) {
|
|
69
|
+
return {
|
|
70
|
+
name: 'No scroll (games only)',
|
|
71
|
+
status: 'fail',
|
|
72
|
+
detail: `${issues.length} scroll-enabling pattern${issues.length === 1 ? '' : 's'}`,
|
|
73
|
+
suggestions: [
|
|
74
|
+
...issues.slice(0, 5),
|
|
75
|
+
...(issues.length > 5 ? [`...and ${issues.length - 5} more`] : []),
|
|
76
|
+
'Use <GameShell> from @freeappstore/games — it locks layout to 100svh.',
|
|
77
|
+
'Inside the play area, use overflow: hidden instead of overflow: auto.',
|
|
78
|
+
],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (!sawViewportLock) {
|
|
82
|
+
return {
|
|
83
|
+
name: 'No scroll (games only)',
|
|
84
|
+
status: 'warn',
|
|
85
|
+
detail: 'no 100svh/100vh viewport lock detected — game may scroll on small viewports',
|
|
86
|
+
suggestions: [
|
|
87
|
+
'Wrap your game in <GameShell> from @freeappstore/games, or',
|
|
88
|
+
'Add `height: 100svh` to your root container.',
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
name: 'No scroll (games only)',
|
|
94
|
+
status: 'pass',
|
|
95
|
+
detail: 'viewport-locked layout detected',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const SCAN_EXTS = new Set(['.css', '.scss', '.tsx', '.ts', '.jsx', '.js', '.html']);
|
|
99
|
+
// Match `overflow: scroll` and `overflow: auto` on html/body selectors,
|
|
100
|
+
// or as bare global rules in CSS. Conservative: only flags when paired
|
|
101
|
+
// with html/body — avoids false positives for component-scoped overflow.
|
|
102
|
+
//
|
|
103
|
+
// IMPORTANT: no `g` flag. Module-scoped regexes with `g` share lastIndex
|
|
104
|
+
// across parallel test runs and produce flaky matches; we only need
|
|
105
|
+
// first-match-per-file anyway.
|
|
106
|
+
const FORBIDDEN_OVERFLOW = [
|
|
107
|
+
/(?:^|[\s,{])(?:html|body)\s*\{[^}]*overflow\s*:\s*(?:scroll|auto)/im,
|
|
108
|
+
/(?:^|[\s,{])(?:html|body)\s*\{[^}]*overflow-(?:x|y)\s*:\s*(?:scroll|auto)/im,
|
|
109
|
+
];
|
|
110
|
+
const FORBIDDEN_MIN_HEIGHT = [
|
|
111
|
+
/(?:^|[\s,{])(?:html|body)\s*\{[^}]*min-height\s*:\s*100vh/im,
|
|
112
|
+
];
|
|
113
|
+
const VIEWPORT_LOCK = /(?:height|max-height)\s*:\s*100(?:s?vh)|GameShell|@freeappstore\/games/i;
|
|
114
|
+
async function isGame(repoDir) {
|
|
115
|
+
// Fast path: look for the explicit games-sdk dep in any package.json.
|
|
116
|
+
// Falls back to scanning JSX for GameShell import — catches cases
|
|
117
|
+
// where the dep is hoisted to a workspace root.
|
|
118
|
+
for await (const file of walk(repoDir)) {
|
|
119
|
+
const base = file.split('/').pop() ?? '';
|
|
120
|
+
if (base !== 'package.json')
|
|
121
|
+
continue;
|
|
122
|
+
const content = await readFile(file, 'utf8').catch(() => '');
|
|
123
|
+
if (/@freeappstore\/games/.test(content))
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
for await (const file of walk(repoDir)) {
|
|
127
|
+
const ext = extname(file).toLowerCase();
|
|
128
|
+
if (ext !== '.tsx' && ext !== '.ts')
|
|
129
|
+
continue;
|
|
130
|
+
const content = await readFile(file, 'utf8').catch(() => '');
|
|
131
|
+
if (/from\s+['"]@freeappstore\/games['"]/.test(content))
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
function lineNumberAt(content, index) {
|
|
137
|
+
let n = 1;
|
|
138
|
+
for (let i = 0; i < index; i++)
|
|
139
|
+
if (content.charCodeAt(i) === 10)
|
|
140
|
+
n++;
|
|
141
|
+
return n;
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=no-scroll.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-scroll.js","sourceRoot":"","sources":["../../src/checks/no-scroll.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-scroll.test.d.ts","sourceRoot":"","sources":["../../src/checks/no-scroll.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,105 @@
|
|
|
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 { checkNoScroll } from './no-scroll.js';
|
|
6
|
+
async function fixture(files) {
|
|
7
|
+
const dir = await mkdtemp(join(tmpdir(), 'fas-noscroll-'));
|
|
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
|
+
const GAME_DEPS = JSON.stringify({
|
|
16
|
+
name: 'my-game',
|
|
17
|
+
dependencies: { '@freeappstore/games': '^0.1.0' },
|
|
18
|
+
});
|
|
19
|
+
describe('checkNoScroll', () => {
|
|
20
|
+
it('passes (skipped) when the project is not a game', async () => {
|
|
21
|
+
const dir = await fixture({
|
|
22
|
+
'package.json': JSON.stringify({ name: 'my-app' }),
|
|
23
|
+
'web/src/index.css': 'body { overflow: scroll; }',
|
|
24
|
+
});
|
|
25
|
+
const r = await checkNoScroll(dir);
|
|
26
|
+
expect(r.status).toBe('pass');
|
|
27
|
+
expect(r.detail).toMatch(/not a game/);
|
|
28
|
+
});
|
|
29
|
+
it('detects @freeappstore/games dep as the game signal', async () => {
|
|
30
|
+
const dir = await fixture({
|
|
31
|
+
'package.json': GAME_DEPS,
|
|
32
|
+
'web/src/index.css': 'body { height: 100svh; }',
|
|
33
|
+
'web/src/App.tsx': 'import { GameShell } from "@freeappstore/games";',
|
|
34
|
+
});
|
|
35
|
+
const r = await checkNoScroll(dir);
|
|
36
|
+
// Should at least run the check, not skip.
|
|
37
|
+
expect(r.detail).not.toMatch(/not a game/);
|
|
38
|
+
});
|
|
39
|
+
it('fails when html/body declares overflow: scroll', async () => {
|
|
40
|
+
const dir = await fixture({
|
|
41
|
+
'package.json': GAME_DEPS,
|
|
42
|
+
'web/src/index.css': 'html { overflow: scroll; }',
|
|
43
|
+
});
|
|
44
|
+
const r = await checkNoScroll(dir);
|
|
45
|
+
expect(r.status).toBe('fail');
|
|
46
|
+
expect(r.suggestions?.join(' ')).toMatch(/GameShell/);
|
|
47
|
+
});
|
|
48
|
+
it('fails when html/body declares overflow: auto', async () => {
|
|
49
|
+
const dir = await fixture({
|
|
50
|
+
'package.json': GAME_DEPS,
|
|
51
|
+
'web/src/index.css': 'body { overflow: auto; }',
|
|
52
|
+
});
|
|
53
|
+
const r = await checkNoScroll(dir);
|
|
54
|
+
expect(r.status).toBe('fail');
|
|
55
|
+
});
|
|
56
|
+
it('fails on overflow-y: scroll on body', async () => {
|
|
57
|
+
const dir = await fixture({
|
|
58
|
+
'package.json': GAME_DEPS,
|
|
59
|
+
'web/src/index.css': 'body { overflow-y: scroll; padding: 0; }',
|
|
60
|
+
});
|
|
61
|
+
const r = await checkNoScroll(dir);
|
|
62
|
+
expect(r.status).toBe('fail');
|
|
63
|
+
});
|
|
64
|
+
it('fails on min-height: 100vh on body (creates page taller than viewport)', async () => {
|
|
65
|
+
const dir = await fixture({
|
|
66
|
+
'package.json': GAME_DEPS,
|
|
67
|
+
'web/src/index.css': 'body { min-height: 100vh; }',
|
|
68
|
+
});
|
|
69
|
+
const r = await checkNoScroll(dir);
|
|
70
|
+
expect(r.status).toBe('fail');
|
|
71
|
+
expect(r.suggestions?.join(' ')).toMatch(/100svh/);
|
|
72
|
+
});
|
|
73
|
+
it('warns when there is no viewport-lock evidence', async () => {
|
|
74
|
+
const dir = await fixture({
|
|
75
|
+
'package.json': GAME_DEPS,
|
|
76
|
+
'web/src/index.css': 'body { padding: 0; }',
|
|
77
|
+
'web/src/App.tsx': 'export default function App() { return <div>game</div>; }',
|
|
78
|
+
});
|
|
79
|
+
const r = await checkNoScroll(dir);
|
|
80
|
+
expect(r.status).toBe('warn');
|
|
81
|
+
expect(r.detail).toMatch(/viewport lock/);
|
|
82
|
+
});
|
|
83
|
+
it('passes when GameShell is imported (the canonical viewport lock)', async () => {
|
|
84
|
+
const dir = await fixture({
|
|
85
|
+
'package.json': GAME_DEPS,
|
|
86
|
+
'web/src/App.tsx': `
|
|
87
|
+
import { GameShell, GameTopbar } from "@freeappstore/games";
|
|
88
|
+
export default function App() {
|
|
89
|
+
return <GameShell topbar={<GameTopbar score={0} />}>game</GameShell>;
|
|
90
|
+
}
|
|
91
|
+
`,
|
|
92
|
+
});
|
|
93
|
+
const r = await checkNoScroll(dir);
|
|
94
|
+
expect(r.status).toBe('pass');
|
|
95
|
+
});
|
|
96
|
+
it('passes when CSS has explicit 100svh height lock', async () => {
|
|
97
|
+
const dir = await fixture({
|
|
98
|
+
'package.json': GAME_DEPS,
|
|
99
|
+
'web/src/index.css': '.game-root { height: 100svh; overflow: hidden; }',
|
|
100
|
+
});
|
|
101
|
+
const r = await checkNoScroll(dir);
|
|
102
|
+
expect(r.status).toBe('pass');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
//# sourceMappingURL=no-scroll.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-scroll.test.js","sourceRoot":"","sources":["../../src/checks/no-scroll.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -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 @@
|
|
|
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":"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"}
|
|
@@ -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"}
|