@freegamestore/compliance 0.1.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 (94) hide show
  1. package/README.md +27 -0
  2. package/dist/checks/brand-fonts.d.ts +10 -0
  3. package/dist/checks/brand-fonts.d.ts.map +1 -0
  4. package/dist/checks/brand-fonts.js +47 -0
  5. package/dist/checks/brand-fonts.js.map +1 -0
  6. package/dist/checks/brand-tokens.d.ts +22 -0
  7. package/dist/checks/brand-tokens.d.ts.map +1 -0
  8. package/dist/checks/brand-tokens.js +53 -0
  9. package/dist/checks/brand-tokens.js.map +1 -0
  10. package/dist/checks/bundle-size.d.ts +14 -0
  11. package/dist/checks/bundle-size.d.ts.map +1 -0
  12. package/dist/checks/bundle-size.js +84 -0
  13. package/dist/checks/bundle-size.js.map +1 -0
  14. package/dist/checks/claude-md-slim.d.ts +4 -0
  15. package/dist/checks/claude-md-slim.d.ts.map +1 -0
  16. package/dist/checks/claude-md-slim.js +88 -0
  17. package/dist/checks/claude-md-slim.js.map +1 -0
  18. package/dist/checks/dark-mode.d.ts +20 -0
  19. package/dist/checks/dark-mode.d.ts.map +1 -0
  20. package/dist/checks/dark-mode.js +48 -0
  21. package/dist/checks/dark-mode.js.map +1 -0
  22. package/dist/checks/html-meta.d.ts +16 -0
  23. package/dist/checks/html-meta.d.ts.map +1 -0
  24. package/dist/checks/html-meta.js +46 -0
  25. package/dist/checks/html-meta.js.map +1 -0
  26. package/dist/checks/license-mit.d.ts +13 -0
  27. package/dist/checks/license-mit.d.ts.map +1 -0
  28. package/dist/checks/license-mit.js +38 -0
  29. package/dist/checks/license-mit.js.map +1 -0
  30. package/dist/checks/manifest.d.ts +9 -0
  31. package/dist/checks/manifest.d.ts.map +1 -0
  32. package/dist/checks/manifest.js +43 -0
  33. package/dist/checks/manifest.js.map +1 -0
  34. package/dist/checks/no-brand-overrides.d.ts +25 -0
  35. package/dist/checks/no-brand-overrides.d.ts.map +1 -0
  36. package/dist/checks/no-brand-overrides.js +159 -0
  37. package/dist/checks/no-brand-overrides.js.map +1 -0
  38. package/dist/checks/no-env-production.d.ts +16 -0
  39. package/dist/checks/no-env-production.d.ts.map +1 -0
  40. package/dist/checks/no-env-production.js +33 -0
  41. package/dist/checks/no-env-production.js.map +1 -0
  42. package/dist/checks/no-placeholders.d.ts +10 -0
  43. package/dist/checks/no-placeholders.d.ts.map +1 -0
  44. package/dist/checks/no-placeholders.js +41 -0
  45. package/dist/checks/no-placeholders.js.map +1 -0
  46. package/dist/checks/no-scroll.d.ts +25 -0
  47. package/dist/checks/no-scroll.d.ts.map +1 -0
  48. package/dist/checks/no-scroll.js +111 -0
  49. package/dist/checks/no-scroll.js.map +1 -0
  50. package/dist/checks/no-tracking.d.ts +4 -0
  51. package/dist/checks/no-tracking.d.ts.map +1 -0
  52. package/dist/checks/no-tracking.js +52 -0
  53. package/dist/checks/no-tracking.js.map +1 -0
  54. package/dist/checks/pwa-meta.d.ts +19 -0
  55. package/dist/checks/pwa-meta.d.ts.map +1 -0
  56. package/dist/checks/pwa-meta.js +38 -0
  57. package/dist/checks/pwa-meta.js.map +1 -0
  58. package/dist/checks/store-link.d.ts +8 -0
  59. package/dist/checks/store-link.d.ts.map +1 -0
  60. package/dist/checks/store-link.js +25 -0
  61. package/dist/checks/store-link.js.map +1 -0
  62. package/dist/checks/unsafe-vh.d.ts +25 -0
  63. package/dist/checks/unsafe-vh.d.ts.map +1 -0
  64. package/dist/checks/unsafe-vh.js +108 -0
  65. package/dist/checks/unsafe-vh.js.map +1 -0
  66. package/dist/checks/viewport-support.d.ts +24 -0
  67. package/dist/checks/viewport-support.d.ts.map +1 -0
  68. package/dist/checks/viewport-support.js +92 -0
  69. package/dist/checks/viewport-support.js.map +1 -0
  70. package/dist/index.d.ts +38 -0
  71. package/dist/index.d.ts.map +1 -0
  72. package/dist/index.js +62 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/lib/file-source.d.ts +52 -0
  75. package/dist/lib/file-source.d.ts.map +1 -0
  76. package/dist/lib/file-source.js +148 -0
  77. package/dist/lib/file-source.js.map +1 -0
  78. package/dist/lib/gzip.d.ts +8 -0
  79. package/dist/lib/gzip.d.ts.map +1 -0
  80. package/dist/lib/gzip.js +26 -0
  81. package/dist/lib/gzip.js.map +1 -0
  82. package/dist/lib/project-type.d.ts +11 -0
  83. package/dist/lib/project-type.d.ts.map +1 -0
  84. package/dist/lib/project-type.js +27 -0
  85. package/dist/lib/project-type.js.map +1 -0
  86. package/dist/live/index.d.ts +50 -0
  87. package/dist/live/index.d.ts.map +1 -0
  88. package/dist/live/index.js +313 -0
  89. package/dist/live/index.js.map +1 -0
  90. package/dist/types.d.ts +12 -0
  91. package/dist/types.d.ts.map +1 -0
  92. package/dist/types.js +2 -0
  93. package/dist/types.js.map +1 -0
  94. package/package.json +47 -0
@@ -0,0 +1,43 @@
1
+ const MANIFEST_PATH = 'web/public/manifest.json';
2
+ /**
3
+ * Verifies the PWA manifest exists at the expected location and parses
4
+ * to JSON. Apps without a manifest can't be "Add to Home Screen"-d on
5
+ * mobile; required by the platform.
6
+ */
7
+ export async function checkManifest(source) {
8
+ const raw = await source.read(MANIFEST_PATH);
9
+ if (raw === null) {
10
+ return {
11
+ name: 'PWA manifest',
12
+ status: 'fail',
13
+ detail: `${MANIFEST_PATH} missing`,
14
+ suggestions: [
15
+ 'Add a manifest.json with at least name, short_name, start_url, display, icons.',
16
+ 'See template-standalone for a working example.',
17
+ ],
18
+ };
19
+ }
20
+ let parsed;
21
+ try {
22
+ parsed = JSON.parse(raw);
23
+ }
24
+ catch {
25
+ return {
26
+ name: 'PWA manifest',
27
+ status: 'fail',
28
+ detail: `${MANIFEST_PATH} is not valid JSON`,
29
+ };
30
+ }
31
+ const required = ['name', 'short_name', 'start_url', 'display'];
32
+ const missing = required.filter((k) => typeof parsed[k] !== 'string' || parsed[k] === '');
33
+ if (missing.length > 0) {
34
+ return {
35
+ name: 'PWA manifest',
36
+ status: 'warn',
37
+ detail: `missing fields: ${missing.join(', ')}`,
38
+ suggestions: [`Add the ${missing.join(', ')} field(s) to manifest.json so installs work.`],
39
+ };
40
+ }
41
+ return { name: 'PWA manifest', status: 'pass', detail: MANIFEST_PATH };
42
+ }
43
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../src/checks/manifest.ts"],"names":[],"mappings":"AAGA,MAAM,aAAa,GAAG,0BAA0B,CAAC;AAEjD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,MAAkB;IACpD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC7C,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QACjB,OAAO;YACL,IAAI,EAAE,cAAc;YACpB,MAAM,EAAE,MAAM;YACd,MAAM,EAAE,GAAG,aAAa,UAAU;YAClC,WAAW,EAAE;gBACX,gFAAgF;gBAChF,gDAAgD;aACjD;SACF,CAAC;IACJ,CAAC;IAED,IAAI,MAAwF,CAAC;IAC7F,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO;YACL,IAAI,EAAE,cAAc;YACpB,MAAM,EAAE,MAAM;YACd,MAAM,EAAE,GAAG,aAAa,oBAAoB;SAC7C,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,CAAC,MAAM,EAAE,YAAY,EAAE,WAAW,EAAE,SAAS,CAAU,CAAC;IACzE,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,MAAM,CAAC,CAAC,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;IAC1F,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,OAAO;YACL,IAAI,EAAE,cAAc;YACpB,MAAM,EAAE,MAAM;YACd,MAAM,EAAE,mBAAmB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;YAC/C,WAAW,EAAE,CAAC,WAAW,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,8CAA8C,CAAC;SAC3F,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC;AACzE,CAAC"}
@@ -0,0 +1,25 @@
1
+ import type { FileSource } from '../lib/file-source.js';
2
+ import type { CheckResult } from '../types.js';
3
+ /**
4
+ * Apps and games on the platform must look consistent with the storefront
5
+ * and with each other — brand colors, fonts, and layout rhythm come from
6
+ * the template, not the app. This check fails on the most common forms of
7
+ * brand-override drift:
8
+ *
9
+ * 1. Custom CSS variable overrides for the platform-defined tokens
10
+ * (--paper, --ink, --accent, --line, --panel, --muted). These are
11
+ * defined by the template; an app redefining them changes the brand.
12
+ * 2. Custom font-family declarations beyond the brand stack
13
+ * (Manrope, Fraunces) and the system / monospace fallbacks.
14
+ *
15
+ * What's allowed:
16
+ * - Using the variables (color: var(--accent)) — that's the whole point.
17
+ * - Hardcoded colors for non-brand purposes (highlight states, error
18
+ * red, etc.). We don't try to gate every hex literal — too noisy and
19
+ * too many false positives.
20
+ */
21
+ export declare function checkNoBrandOverrides(source: FileSource): Promise<CheckResult>;
22
+ export declare function scanContent(filename: string, content: string, opts?: {
23
+ skipVarRedefs?: boolean;
24
+ }): string[];
25
+ //# sourceMappingURL=no-brand-overrides.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-brand-overrides.d.ts","sourceRoot":"","sources":["../../src/checks/no-brand-overrides.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,CAuCpF;AA2DD,wBAAgB,WAAW,CACzB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,IAAI,GAAE;IAAE,aAAa,CAAC,EAAE,OAAO,CAAA;CAAO,GACrC,MAAM,EAAE,CAmCV"}
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Apps and games on the platform must look consistent with the storefront
3
+ * and with each other — brand colors, fonts, and layout rhythm come from
4
+ * the template, not the app. This check fails on the most common forms of
5
+ * brand-override drift:
6
+ *
7
+ * 1. Custom CSS variable overrides for the platform-defined tokens
8
+ * (--paper, --ink, --accent, --line, --panel, --muted). These are
9
+ * defined by the template; an app redefining them changes the brand.
10
+ * 2. Custom font-family declarations beyond the brand stack
11
+ * (Manrope, Fraunces) and the system / monospace fallbacks.
12
+ *
13
+ * What's allowed:
14
+ * - Using the variables (color: var(--accent)) — that's the whole point.
15
+ * - Hardcoded colors for non-brand purposes (highlight states, error
16
+ * red, etc.). We don't try to gate every hex literal — too noisy and
17
+ * too many false positives.
18
+ */
19
+ export async function checkNoBrandOverrides(source) {
20
+ const issues = [];
21
+ for await (const path of source.list()) {
22
+ const ext = extOf(path);
23
+ if (!SCANNED_EXTS.has(ext))
24
+ continue;
25
+ const content = await source.read(path);
26
+ if (!content)
27
+ continue;
28
+ // Canonical theme file: the platform's CSS variables ARE defined here.
29
+ // Apps own this file post-scaffold (they can technically modify token
30
+ // values, and we accept the imperfection — the loud places where a
31
+ // brand override actually causes inconsistency are inline styles +
32
+ // per-component CSS, which this check still covers).
33
+ const isThemeFile = path === 'web/src/index.css' || path === 'web/src/main.css';
34
+ const fileIssues = scanContent(path, content, { skipVarRedefs: isThemeFile });
35
+ issues.push(...fileIssues);
36
+ // Cap at 10 — beyond that the user has bigger problems and we don't
37
+ // want a 200-line failure report.
38
+ if (issues.length >= 10)
39
+ break;
40
+ }
41
+ if (issues.length === 0) {
42
+ return {
43
+ name: 'No brand overrides',
44
+ status: 'pass',
45
+ detail: 'apps inherit colors + fonts from the template',
46
+ };
47
+ }
48
+ return {
49
+ name: 'No brand overrides',
50
+ status: 'fail',
51
+ detail: `${issues.length} apparent override${issues.length === 1 ? '' : 's'}`,
52
+ suggestions: [
53
+ ...issues.slice(0, 5),
54
+ ...(issues.length > 5 ? [`...and ${issues.length - 5} more`] : []),
55
+ 'Use the template tokens (var(--accent), var(--ink), etc.) instead of redefining them.',
56
+ 'Brand fonts are Manrope (body) + Fraunces (display). Stick to those + system / monospace.',
57
+ ],
58
+ };
59
+ }
60
+ const SCANNED_EXTS = new Set(['.css', '.scss', '.tsx', '.ts', '.jsx', '.js', '.html']);
61
+ const PROTECTED_VARS = new Set([
62
+ '--paper',
63
+ '--ink',
64
+ '--muted',
65
+ '--accent',
66
+ '--line',
67
+ '--line-strong',
68
+ '--panel',
69
+ ]);
70
+ /**
71
+ * Whitelist of font-family declarations we accept. Anything outside this
72
+ * set — for ANY font name not in the brand stack and not a generic /
73
+ * system fallback — flags. The check is case-insensitive.
74
+ */
75
+ const ALLOWED_FONT_TOKENS = new Set([
76
+ // Brand stack
77
+ 'manrope',
78
+ 'fraunces',
79
+ // Generic/system fallbacks (CSS keywords)
80
+ 'serif',
81
+ 'sans-serif',
82
+ 'monospace',
83
+ 'system-ui',
84
+ 'ui-monospace',
85
+ 'ui-serif',
86
+ 'ui-sans-serif',
87
+ 'inherit',
88
+ 'initial',
89
+ 'unset',
90
+ 'revert',
91
+ // Common system stack names — these are aliases, not custom fonts.
92
+ '-apple-system',
93
+ 'blinkmacsystemfont',
94
+ 'segoe ui',
95
+ 'roboto',
96
+ 'helvetica',
97
+ 'helvetica neue',
98
+ 'arial',
99
+ 'sf mono',
100
+ 'sf pro',
101
+ 'menlo',
102
+ 'monaco',
103
+ 'consolas',
104
+ 'liberation mono',
105
+ 'courier',
106
+ 'courier new',
107
+ 'georgia',
108
+ 'noto sans',
109
+ 'noto color emoji',
110
+ 'apple color emoji',
111
+ 'segoe ui emoji',
112
+ 'segoe ui symbol',
113
+ ]);
114
+ export function scanContent(filename, content, opts = {}) {
115
+ const out = [];
116
+ // 1. CSS variable overrides for protected tokens.
117
+ if (!opts.skipVarRedefs) {
118
+ const varDeclRe = /(?:^|[\s;{,])(--[a-z-]+)\s*:/gim;
119
+ let m;
120
+ while ((m = varDeclRe.exec(content)) !== null) {
121
+ const name = m[1];
122
+ if (PROTECTED_VARS.has(name)) {
123
+ const line = lineNumberAt(content, m.index);
124
+ out.push(`${filename}:${line} redefines ${name} (template owns this token)`);
125
+ }
126
+ }
127
+ }
128
+ // 2. font-family overrides.
129
+ const fontRe = /(?:font-family|fontFamily)\s*[:=]\s*["'`]?([^;"'`}\n]+)["'`]?/g;
130
+ let m;
131
+ while ((m = fontRe.exec(content)) !== null) {
132
+ const list = m[1];
133
+ const tokens = list
134
+ .split(',')
135
+ .map((t) => t.trim().replace(/^["']|["']$/g, '').toLowerCase())
136
+ .filter(Boolean);
137
+ for (const t of tokens) {
138
+ if (!ALLOWED_FONT_TOKENS.has(t)) {
139
+ const line = lineNumberAt(content, m.index);
140
+ out.push(`${filename}:${line} non-brand font "${t}"`);
141
+ break;
142
+ }
143
+ }
144
+ }
145
+ return out;
146
+ }
147
+ function extOf(path) {
148
+ const dot = path.lastIndexOf('.');
149
+ const slash = path.lastIndexOf('/');
150
+ return dot > slash ? path.slice(dot).toLowerCase() : '';
151
+ }
152
+ function lineNumberAt(content, index) {
153
+ let n = 1;
154
+ for (let i = 0; i < index; i++)
155
+ if (content.charCodeAt(i) === 10)
156
+ n++;
157
+ return n;
158
+ }
159
+ //# sourceMappingURL=no-brand-overrides.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-brand-overrides.js","sourceRoot":"","sources":["../../src/checks/no-brand-overrides.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { FileSource } from '../lib/file-source.js';
2
+ import type { CheckResult } from '../types.js';
3
+ /**
4
+ * `.env.production` is a frequent accidental commit — Vite reads it at
5
+ * build time, and it's the file most likely to contain real API keys
6
+ * for the production deploy. The platform's templates instruct creators
7
+ * to use Cloudflare Pages env vars or Wrangler secrets instead. This
8
+ * check is a safety net: even if the creator's gitignore misses it, we
9
+ * fail compliance before publish.
10
+ *
11
+ * Scope: any `.env.production` file anywhere in the repo. We don't try
12
+ * to differentiate "this one's empty" from "this one has secrets" —
13
+ * the file shouldn't exist at all.
14
+ */
15
+ export declare function checkNoEnvProduction(source: FileSource): Promise<CheckResult>;
16
+ //# sourceMappingURL=no-env-production.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-env-production.d.ts","sourceRoot":"","sources":["../../src/checks/no-env-production.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C;;;;;;;;;;;GAWG;AACH,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,CAkBnF"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * `.env.production` is a frequent accidental commit — Vite reads it at
3
+ * build time, and it's the file most likely to contain real API keys
4
+ * for the production deploy. The platform's templates instruct creators
5
+ * to use Cloudflare Pages env vars or Wrangler secrets instead. This
6
+ * check is a safety net: even if the creator's gitignore misses it, we
7
+ * fail compliance before publish.
8
+ *
9
+ * Scope: any `.env.production` file anywhere in the repo. We don't try
10
+ * to differentiate "this one's empty" from "this one has secrets" —
11
+ * the file shouldn't exist at all.
12
+ */
13
+ export async function checkNoEnvProduction(source) {
14
+ const offenders = [];
15
+ for await (const path of source.list()) {
16
+ const base = path.split('/').pop() ?? '';
17
+ if (base === '.env.production')
18
+ offenders.push(path);
19
+ }
20
+ if (offenders.length === 0) {
21
+ return { name: 'No .env.production', status: 'pass', detail: 'no production env files committed' };
22
+ }
23
+ return {
24
+ name: 'No .env.production',
25
+ status: 'fail',
26
+ detail: `${offenders.length} .env.production file(s): ${offenders.join(', ')}`,
27
+ suggestions: [
28
+ 'Delete the file and add `.env.production` to .gitignore.',
29
+ 'Use Cloudflare Pages → Settings → Environment Variables for production secrets instead.',
30
+ ],
31
+ };
32
+ }
33
+ //# sourceMappingURL=no-env-production.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-env-production.js","sourceRoot":"","sources":["../../src/checks/no-env-production.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,MAAkB;IAC3D,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,IAAI,KAAK,EAAE,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;QACvC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;QACzC,IAAI,IAAI,KAAK,iBAAiB;YAAE,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvD,CAAC;IACD,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO,EAAE,IAAI,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,mCAAmC,EAAE,CAAC;IACrG,CAAC;IACD,OAAO;QACL,IAAI,EAAE,oBAAoB;QAC1B,MAAM,EAAE,MAAM;QACd,MAAM,EAAE,GAAG,SAAS,CAAC,MAAM,6BAA6B,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;QAC9E,WAAW,EAAE;YACX,0DAA0D;YAC1D,yFAAyF;SAC1F;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { FileSource } from '../lib/file-source.js';
2
+ import type { CheckResult } from '../types.js';
3
+ /**
4
+ * Catches the case where APPNAME placeholders from the template were
5
+ * never substituted. The CLI's `fas init` substitutes correctly today
6
+ * but if someone clones the template manually and forgets the find/
7
+ * replace step, every page renders with `<title>APPNAME — …</title>`.
8
+ */
9
+ export declare function checkNoPlaceholders(source: FileSource): Promise<CheckResult>;
10
+ //# sourceMappingURL=no-placeholders.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-placeholders.d.ts","sourceRoot":"","sources":["../../src/checks/no-placeholders.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAO/C;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,CAsBlF"}
@@ -0,0 +1,41 @@
1
+ const TEXT_EXTS = new Set([
2
+ '.md', '.txt', '.json', '.ts', '.tsx', '.js', '.jsx',
3
+ '.html', '.htm', '.css', '.scss', '.yaml', '.yml', '.toml', '.svg',
4
+ ]);
5
+ /**
6
+ * Catches the case where APPNAME placeholders from the template were
7
+ * never substituted. The CLI's `fas init` substitutes correctly today
8
+ * but if someone clones the template manually and forgets the find/
9
+ * replace step, every page renders with `<title>APPNAME — …</title>`.
10
+ */
11
+ export async function checkNoPlaceholders(source) {
12
+ const offenders = [];
13
+ for await (const path of source.list()) {
14
+ if (!TEXT_EXTS.has(extOf(path)))
15
+ continue;
16
+ const content = await source.read(path);
17
+ if (content && content.includes('APPNAME')) {
18
+ offenders.push(path);
19
+ if (offenders.length >= 5)
20
+ break; // don't spam, summarize
21
+ }
22
+ }
23
+ if (offenders.length === 0) {
24
+ return { name: 'No template placeholders', status: 'pass', detail: 'all APPNAME substituted' };
25
+ }
26
+ return {
27
+ name: 'No template placeholders',
28
+ status: 'fail',
29
+ detail: `${offenders.length === 5 ? '5+' : offenders.length} file(s) still contain APPNAME: ${offenders.join(', ')}`,
30
+ suggestions: [
31
+ 'Run `fas init <id>` instead of cloning the template manually — it does the substitution.',
32
+ 'Or do a project-wide find/replace: APPNAME → your app id (lowercase, hyphenated).',
33
+ ],
34
+ };
35
+ }
36
+ function extOf(path) {
37
+ const dot = path.lastIndexOf('.');
38
+ const slash = path.lastIndexOf('/');
39
+ return dot > slash ? path.slice(dot).toLowerCase() : '';
40
+ }
41
+ //# sourceMappingURL=no-placeholders.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-placeholders.js","sourceRoot":"","sources":["../../src/checks/no-placeholders.ts"],"names":[],"mappings":"AAGA,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC;IACxB,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IACpD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CACnE,CAAC,CAAC;AAEH;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,MAAkB;IAC1D,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,IAAI,KAAK,EAAE,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;QACvC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAAE,SAAS;QAC1C,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxC,IAAI,OAAO,IAAI,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;YAC3C,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACrB,IAAI,SAAS,CAAC,MAAM,IAAI,CAAC;gBAAE,MAAM,CAAC,wBAAwB;QAC5D,CAAC;IACH,CAAC;IACD,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO,EAAE,IAAI,EAAE,0BAA0B,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,yBAAyB,EAAE,CAAC;IACjG,CAAC;IACD,OAAO;QACL,IAAI,EAAE,0BAA0B;QAChC,MAAM,EAAE,MAAM;QACd,MAAM,EAAE,GAAG,SAAS,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,mCAAmC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;QACpH,WAAW,EAAE;YACX,0FAA0F;YAC1F,mFAAmF;SACpF;KACF,CAAC;AACJ,CAAC;AAED,SAAS,KAAK,CAAC,IAAY;IACzB,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAClC,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACpC,OAAO,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AAC1D,CAAC"}
@@ -0,0 +1,25 @@
1
+ import type { FileSource } from '../lib/file-source.js';
2
+ import type { CheckResult } from '../types.js';
3
+ /**
4
+ * Games on FreeGameStore must fit the viewport — no horizontal or
5
+ * vertical scroll at the document level. Static analysis catches the
6
+ * common CSS / inline-style anti-patterns:
7
+ *
8
+ * - `overflow: scroll` or `overflow: auto` on `html` or `body`.
9
+ * - `min-height: 100vh` on `html` / `body` (creates page that's at
10
+ * least viewport-tall, but content + topbar = > 100vh = scroll).
11
+ * - Missing the `100svh` / `100vh` height constraint on the layout
12
+ * wrapper (loose check — "at least one root element should hard-cap
13
+ * to viewport height").
14
+ *
15
+ * Runtime guarantees (real DOM measurement) live in the e2e Playwright
16
+ * suite; this check exists as a fast pre-publish gate so creators
17
+ * don't ship trivially-scrolling games.
18
+ *
19
+ * Apps (FreeAppStore) are not subject to this check — the apps Shell
20
+ * wraps them in a sidebar+main layout that legitimately scrolls. We
21
+ * detect "is this a game project" by looking for the @freegamestore/games
22
+ * dep, the canonical signal that the app uses GameShell.
23
+ */
24
+ export declare function checkNoScroll(source: FileSource): Promise<CheckResult>;
25
+ //# 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":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAExD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,CAuE5E"}
@@ -0,0 +1,111 @@
1
+ import { isGameProject } from '../lib/project-type.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 @freegamestore/games
21
+ * dep, the canonical signal that the app uses GameShell.
22
+ */
23
+ export async function checkNoScroll(source) {
24
+ // Skip apps — only games are subject to no-scroll.
25
+ if (!(await isGameProject(source))) {
26
+ return {
27
+ name: 'No scroll (games only)',
28
+ status: 'pass',
29
+ detail: 'not a game project — check skipped',
30
+ };
31
+ }
32
+ const issues = [];
33
+ let sawViewportLock = false;
34
+ for await (const path of source.list()) {
35
+ const ext = extOf(path);
36
+ if (!SCAN_EXTS.has(ext))
37
+ continue;
38
+ const content = await source.read(path);
39
+ if (!content)
40
+ continue;
41
+ for (const re of FORBIDDEN_OVERFLOW) {
42
+ const m = re.exec(content);
43
+ if (m) {
44
+ const line = lineNumberAt(content, m.index);
45
+ issues.push(`${path}:${line} ${m[0]} — root scrolling not allowed in games`);
46
+ }
47
+ }
48
+ for (const re of FORBIDDEN_MIN_HEIGHT) {
49
+ const m = re.exec(content);
50
+ if (m) {
51
+ const line = lineNumberAt(content, m.index);
52
+ issues.push(`${path}:${line} ${m[0]} — use exact viewport height (100svh) instead`);
53
+ }
54
+ }
55
+ if (VIEWPORT_LOCK.test(content))
56
+ sawViewportLock = true;
57
+ if (issues.length >= 8)
58
+ break;
59
+ }
60
+ if (issues.length > 0) {
61
+ return {
62
+ name: 'No scroll (games only)',
63
+ status: 'fail',
64
+ detail: `${issues.length} scroll-enabling pattern${issues.length === 1 ? '' : 's'}`,
65
+ suggestions: [
66
+ ...issues.slice(0, 5),
67
+ ...(issues.length > 5 ? [`...and ${issues.length - 5} more`] : []),
68
+ 'Use <GameShell> from @freegamestore/games — it locks layout to 100svh.',
69
+ 'Inside the play area, use overflow: hidden instead of overflow: auto.',
70
+ ],
71
+ };
72
+ }
73
+ if (!sawViewportLock) {
74
+ return {
75
+ name: 'No scroll (games only)',
76
+ status: 'warn',
77
+ detail: 'no 100svh/100vh viewport lock detected — game may scroll on small viewports',
78
+ suggestions: [
79
+ 'Wrap your game in <GameShell> from @freegamestore/games, or',
80
+ 'Add `height: 100svh` to your root container.',
81
+ ],
82
+ };
83
+ }
84
+ return {
85
+ name: 'No scroll (games only)',
86
+ status: 'pass',
87
+ detail: 'viewport-locked layout detected',
88
+ };
89
+ }
90
+ const SCAN_EXTS = new Set(['.css', '.scss', '.tsx', '.ts', '.jsx', '.js', '.html']);
91
+ const FORBIDDEN_OVERFLOW = [
92
+ /(?:^|[\s,{])(?:html|body)\s*\{[^}]*overflow\s*:\s*(?:scroll|auto)/im,
93
+ /(?:^|[\s,{])(?:html|body)\s*\{[^}]*overflow-(?:x|y)\s*:\s*(?:scroll|auto)/im,
94
+ ];
95
+ const FORBIDDEN_MIN_HEIGHT = [
96
+ /(?:^|[\s,{])(?:html|body)\s*\{[^}]*min-height\s*:\s*100vh/im,
97
+ ];
98
+ const VIEWPORT_LOCK = /(?:height|max-height)\s*:\s*100(?:s?vh)|GameShell|@freeappstore\/games/i;
99
+ function extOf(path) {
100
+ const dot = path.lastIndexOf('.');
101
+ const slash = path.lastIndexOf('/');
102
+ return dot > slash ? path.slice(dot).toLowerCase() : '';
103
+ }
104
+ function lineNumberAt(content, index) {
105
+ let n = 1;
106
+ for (let i = 0; i < index; i++)
107
+ if (content.charCodeAt(i) === 10)
108
+ n++;
109
+ return n;
110
+ }
111
+ //# 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,4 @@
1
+ import type { FileSource } from '../lib/file-source.js';
2
+ import type { CheckResult } from '../types.js';
3
+ export declare function checkNoTracking(source: FileSource): Promise<CheckResult>;
4
+ //# sourceMappingURL=no-tracking.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-tracking.d.ts","sourceRoot":"","sources":["../../src/checks/no-tracking.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAiB/C,wBAAsB,eAAe,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,CAiC9E"}
@@ -0,0 +1,52 @@
1
+ // Same forbidden list the template's compliance.yml enforces — kept in
2
+ // sync because both are downstream consumers of this package long-term.
3
+ const FORBIDDEN = [
4
+ 'google-analytics',
5
+ 'gtag',
6
+ 'amplitude',
7
+ 'mixpanel',
8
+ 'segment',
9
+ 'hotjar',
10
+ 'plausible',
11
+ 'posthog',
12
+ ];
13
+ const SCAN_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.html', '.json']);
14
+ export async function checkNoTracking(source) {
15
+ const hits = [];
16
+ for await (const path of source.list()) {
17
+ if (!SCAN_EXTS.has(extOf(path)))
18
+ continue;
19
+ const content = await source.read(path);
20
+ if (!content)
21
+ continue;
22
+ const matches = FORBIDDEN.filter((sdk) => content.includes(sdk));
23
+ if (matches.length > 0) {
24
+ hits.push({ file: path, matches });
25
+ }
26
+ }
27
+ if (hits.length === 0) {
28
+ return {
29
+ name: 'No tracking SDKs',
30
+ status: 'pass',
31
+ detail: `scanned for ${FORBIDDEN.length} known trackers`,
32
+ };
33
+ }
34
+ return {
35
+ name: 'No tracking SDKs',
36
+ status: 'fail',
37
+ detail: `${hits.length} file(s) reference trackers: ${hits
38
+ .slice(0, 3)
39
+ .map((h) => `${h.file} (${h.matches.join(', ')})`)
40
+ .join('; ')}${hits.length > 3 ? '…' : ''}`,
41
+ suggestions: [
42
+ 'FreeAppStore apps must be tracking-free. Remove the SDK + any analytics calls.',
43
+ 'For private-by-design metrics, CF edge analytics already counts requests anonymously.',
44
+ ],
45
+ };
46
+ }
47
+ function extOf(path) {
48
+ const dot = path.lastIndexOf('.');
49
+ const slash = path.lastIndexOf('/');
50
+ return dot > slash ? path.slice(dot).toLowerCase() : '';
51
+ }
52
+ //# sourceMappingURL=no-tracking.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-tracking.js","sourceRoot":"","sources":["../../src/checks/no-tracking.ts"],"names":[],"mappings":"AAGA,uEAAuE;AACvE,wEAAwE;AACxE,MAAM,SAAS,GAAG;IAChB,kBAAkB;IAClB,MAAM;IACN,WAAW;IACX,UAAU;IACV,SAAS;IACT,QAAQ;IACR,WAAW;IACX,SAAS;CACV,CAAC;AAEF,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;AAE5E,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,MAAkB;IACtD,MAAM,IAAI,GAA0C,EAAE,CAAC;IAEvD,IAAI,KAAK,EAAE,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;QACvC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAAE,SAAS;QAC1C,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxC,IAAI,CAAC,OAAO;YAAE,SAAS;QACvB,MAAM,OAAO,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;QACjE,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QACrC,CAAC;IACH,CAAC;IAED,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO;YACL,IAAI,EAAE,kBAAkB;YACxB,MAAM,EAAE,MAAM;YACd,MAAM,EAAE,eAAe,SAAS,CAAC,MAAM,iBAAiB;SACzD,CAAC;IACJ,CAAC;IAED,OAAO;QACL,IAAI,EAAE,kBAAkB;QACxB,MAAM,EAAE,MAAM;QACd,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,gCAAgC,IAAI;aACvD,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;aACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;aACjD,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QAC5C,WAAW,EAAE;YACX,gFAAgF;YAChF,uFAAuF;SACxF;KACF,CAAC;AACJ,CAAC;AAED,SAAS,KAAK,CAAC,IAAY;IACzB,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAClC,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACpC,OAAO,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;AAC1D,CAAC"}
@@ -0,0 +1,19 @@
1
+ import type { FileSource } from '../lib/file-source.js';
2
+ import type { CheckResult } from '../types.js';
3
+ /**
4
+ * For an "Add to Home Screen" install on iOS Safari to launch the app
5
+ * standalone (no browser chrome), `web/index.html` needs at least one
6
+ * of:
7
+ *
8
+ * <meta name="apple-mobile-web-app-capable" content="yes" />
9
+ * <meta name="mobile-web-app-capable" content="yes" />
10
+ *
11
+ * Without this, iOS opens the installed icon as a normal Safari tab,
12
+ * defeating the PWA install. Android Chrome reads the manifest's
13
+ * `display: standalone` instead, but iOS still requires the meta tag.
14
+ *
15
+ * The manifest itself is a separate check (`checkManifest`); this one
16
+ * is specifically the iOS install hint that lives in the HTML head.
17
+ */
18
+ export declare function checkPwaMeta(source: FileSource): Promise<CheckResult>;
19
+ //# sourceMappingURL=pwa-meta.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pwa-meta.d.ts","sourceRoot":"","sources":["../../src/checks/pwa-meta.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAI/C;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,CAoB3E"}