@proappstore/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.
- package/README.md +27 -0
- package/dist/checks/brand-fonts.d.ts +10 -0
- package/dist/checks/brand-fonts.d.ts.map +1 -0
- package/dist/checks/brand-fonts.js +47 -0
- package/dist/checks/brand-fonts.js.map +1 -0
- package/dist/checks/brand-tokens.d.ts +22 -0
- package/dist/checks/brand-tokens.d.ts.map +1 -0
- package/dist/checks/brand-tokens.js +53 -0
- package/dist/checks/brand-tokens.js.map +1 -0
- package/dist/checks/bundle-size.d.ts +14 -0
- package/dist/checks/bundle-size.d.ts.map +1 -0
- package/dist/checks/bundle-size.js +84 -0
- package/dist/checks/bundle-size.js.map +1 -0
- package/dist/checks/claude-md-slim.d.ts +4 -0
- package/dist/checks/claude-md-slim.d.ts.map +1 -0
- package/dist/checks/claude-md-slim.js +89 -0
- package/dist/checks/claude-md-slim.js.map +1 -0
- package/dist/checks/dark-mode.d.ts +20 -0
- package/dist/checks/dark-mode.d.ts.map +1 -0
- package/dist/checks/dark-mode.js +56 -0
- package/dist/checks/dark-mode.js.map +1 -0
- package/dist/checks/html-meta.d.ts +16 -0
- package/dist/checks/html-meta.d.ts.map +1 -0
- package/dist/checks/html-meta.js +51 -0
- package/dist/checks/html-meta.js.map +1 -0
- package/dist/checks/license-mit.d.ts +13 -0
- package/dist/checks/license-mit.d.ts.map +1 -0
- package/dist/checks/license-mit.js +45 -0
- package/dist/checks/license-mit.js.map +1 -0
- package/dist/checks/manifest.d.ts +9 -0
- package/dist/checks/manifest.d.ts.map +1 -0
- package/dist/checks/manifest.js +43 -0
- package/dist/checks/manifest.js.map +1 -0
- package/dist/checks/no-brand-overrides.d.ts +25 -0
- package/dist/checks/no-brand-overrides.d.ts.map +1 -0
- package/dist/checks/no-brand-overrides.js +228 -0
- package/dist/checks/no-brand-overrides.js.map +1 -0
- package/dist/checks/no-env-production.d.ts +16 -0
- package/dist/checks/no-env-production.d.ts.map +1 -0
- package/dist/checks/no-env-production.js +37 -0
- package/dist/checks/no-env-production.js.map +1 -0
- package/dist/checks/no-placeholders.d.ts +10 -0
- package/dist/checks/no-placeholders.d.ts.map +1 -0
- package/dist/checks/no-placeholders.js +54 -0
- package/dist/checks/no-placeholders.js.map +1 -0
- package/dist/checks/no-scroll.d.ts +25 -0
- package/dist/checks/no-scroll.d.ts.map +1 -0
- package/dist/checks/no-scroll.js +109 -0
- package/dist/checks/no-scroll.js.map +1 -0
- package/dist/checks/no-tracking.d.ts +4 -0
- package/dist/checks/no-tracking.d.ts.map +1 -0
- package/dist/checks/no-tracking.js +104 -0
- package/dist/checks/no-tracking.js.map +1 -0
- package/dist/checks/pwa-meta.d.ts +19 -0
- package/dist/checks/pwa-meta.d.ts.map +1 -0
- package/dist/checks/pwa-meta.js +42 -0
- package/dist/checks/pwa-meta.js.map +1 -0
- package/dist/checks/pwa-offline.d.ts +40 -0
- package/dist/checks/pwa-offline.d.ts.map +1 -0
- package/dist/checks/pwa-offline.js +388 -0
- package/dist/checks/pwa-offline.js.map +1 -0
- package/dist/checks/store-link.d.ts +13 -0
- package/dist/checks/store-link.d.ts.map +1 -0
- package/dist/checks/store-link.js +30 -0
- package/dist/checks/store-link.js.map +1 -0
- package/dist/checks/unsafe-vh.d.ts +25 -0
- package/dist/checks/unsafe-vh.d.ts.map +1 -0
- package/dist/checks/unsafe-vh.js +120 -0
- package/dist/checks/unsafe-vh.js.map +1 -0
- package/dist/checks/viewport-support.d.ts +24 -0
- package/dist/checks/viewport-support.d.ts.map +1 -0
- package/dist/checks/viewport-support.js +92 -0
- package/dist/checks/viewport-support.js.map +1 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/file-source.d.ts +52 -0
- package/dist/lib/file-source.d.ts.map +1 -0
- package/dist/lib/file-source.js +156 -0
- package/dist/lib/file-source.js.map +1 -0
- package/dist/lib/gzip.d.ts +8 -0
- package/dist/lib/gzip.d.ts.map +1 -0
- package/dist/lib/gzip.js +26 -0
- package/dist/lib/gzip.js.map +1 -0
- package/dist/lib/project-type.d.ts +18 -0
- package/dist/lib/project-type.d.ts.map +1 -0
- package/dist/lib/project-type.js +34 -0
- package/dist/lib/project-type.js.map +1 -0
- package/dist/lib/strip.d.ts +60 -0
- package/dist/lib/strip.d.ts.map +1 -0
- package/dist/lib/strip.js +169 -0
- package/dist/lib/strip.js.map +1 -0
- package/dist/live/index.d.ts +50 -0
- package/dist/live/index.d.ts.map +1 -0
- package/dist/live/index.js +317 -0
- package/dist/live/index.js.map +1 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FileSource } from '../lib/file-source.js';
|
|
2
|
+
import type { CheckResult } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Verifies the PWA manifest exists at the expected location and parses
|
|
5
|
+
* to JSON. Apps without a manifest can't be "Add to Home Screen"-d on
|
|
6
|
+
* mobile; required by the platform.
|
|
7
|
+
*/
|
|
8
|
+
export declare function checkManifest(source: FileSource): Promise<CheckResult>;
|
|
9
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/checks/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAI/C;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,CAoC5E"}
|
|
@@ -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;AAExD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,CAgDpF;AA2DD,wBAAgB,WAAW,CACzB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,IAAI,GAAE;IAAE,aAAa,CAAC,EAAE,OAAO,CAAA;CAAO,GACrC,MAAM,EAAE,CAmDV"}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { stripCommentsOnly, stripCssComments } from '../lib/strip.js';
|
|
2
|
+
/**
|
|
3
|
+
* Apps and games on the platform must look consistent with the storefront
|
|
4
|
+
* and with each other — brand colors, fonts, and layout rhythm come from
|
|
5
|
+
* the template, not the app. This check fails on the most common forms of
|
|
6
|
+
* brand-override drift:
|
|
7
|
+
*
|
|
8
|
+
* 1. Custom CSS variable overrides for the platform-defined tokens
|
|
9
|
+
* (--paper, --ink, --accent, --line, --panel, --muted). These are
|
|
10
|
+
* defined by the template; an app redefining them changes the brand.
|
|
11
|
+
* 2. Custom font-family declarations beyond the brand stack
|
|
12
|
+
* (Manrope, Fraunces) and the system / monospace fallbacks.
|
|
13
|
+
*
|
|
14
|
+
* What's allowed:
|
|
15
|
+
* - Using the variables (color: var(--accent)) — that's the whole point.
|
|
16
|
+
* - Hardcoded colors for non-brand purposes (highlight states, error
|
|
17
|
+
* red, etc.). We don't try to gate every hex literal — too noisy and
|
|
18
|
+
* too many false positives.
|
|
19
|
+
*/
|
|
20
|
+
export async function checkNoBrandOverrides(source) {
|
|
21
|
+
const issues = [];
|
|
22
|
+
for await (const path of source.list()) {
|
|
23
|
+
const ext = extOf(path);
|
|
24
|
+
if (!SCANNED_EXTS.has(ext))
|
|
25
|
+
continue;
|
|
26
|
+
const raw = await source.read(path);
|
|
27
|
+
if (!raw)
|
|
28
|
+
continue;
|
|
29
|
+
// Strip comment bodies so a `// font-family: "Comic Sans"` note
|
|
30
|
+
// doesn't fire. Preserve string-literal contents — the JSX value
|
|
31
|
+
// `fontFamily: "Comic Sans"` IS a string literal and is the thing
|
|
32
|
+
// we're auditing. For .css/.scss use the css-only stripper (no `//`
|
|
33
|
+
// syntax at the language level).
|
|
34
|
+
const content = ext === '.css' || ext === '.scss'
|
|
35
|
+
? stripCssComments(raw)
|
|
36
|
+
: stripCommentsOnly(raw);
|
|
37
|
+
// Canonical theme file: the platform's CSS variables ARE defined here.
|
|
38
|
+
// Apps own this file post-scaffold (they can technically modify token
|
|
39
|
+
// values, and we accept the imperfection — the loud places where a
|
|
40
|
+
// brand override actually causes inconsistency are inline styles +
|
|
41
|
+
// per-component CSS, which this check still covers).
|
|
42
|
+
const isThemeFile = path === 'web/src/index.css' || path === 'web/src/main.css';
|
|
43
|
+
const fileIssues = scanContent(path, content, { skipVarRedefs: isThemeFile });
|
|
44
|
+
issues.push(...fileIssues);
|
|
45
|
+
// Cap at 10 — beyond that the user has bigger problems and we don't
|
|
46
|
+
// want a 200-line failure report.
|
|
47
|
+
if (issues.length >= 10)
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
if (issues.length === 0) {
|
|
51
|
+
return {
|
|
52
|
+
name: 'No brand overrides',
|
|
53
|
+
status: 'pass',
|
|
54
|
+
detail: 'apps inherit colors + fonts from the template',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
name: 'No brand overrides',
|
|
59
|
+
status: 'fail',
|
|
60
|
+
detail: `${issues.length} apparent override${issues.length === 1 ? '' : 's'}`,
|
|
61
|
+
suggestions: [
|
|
62
|
+
...issues.slice(0, 5),
|
|
63
|
+
...(issues.length > 5 ? [`...and ${issues.length - 5} more`] : []),
|
|
64
|
+
'Use the template tokens (var(--accent), var(--ink), etc.) instead of redefining them.',
|
|
65
|
+
'Brand fonts are Manrope (body) + Fraunces (display). Stick to those + system / monospace.',
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const SCANNED_EXTS = new Set(['.css', '.scss', '.tsx', '.ts', '.jsx', '.js', '.html']);
|
|
70
|
+
const PROTECTED_VARS = new Set([
|
|
71
|
+
'--paper',
|
|
72
|
+
'--ink',
|
|
73
|
+
'--muted',
|
|
74
|
+
'--accent',
|
|
75
|
+
'--line',
|
|
76
|
+
'--line-strong',
|
|
77
|
+
'--panel',
|
|
78
|
+
]);
|
|
79
|
+
/**
|
|
80
|
+
* Whitelist of font-family declarations we accept. Anything outside this
|
|
81
|
+
* set — for ANY font name not in the brand stack and not a generic /
|
|
82
|
+
* system fallback — flags. The check is case-insensitive.
|
|
83
|
+
*/
|
|
84
|
+
const ALLOWED_FONT_TOKENS = new Set([
|
|
85
|
+
// Brand stack
|
|
86
|
+
'manrope',
|
|
87
|
+
'fraunces',
|
|
88
|
+
// Generic/system fallbacks (CSS keywords)
|
|
89
|
+
'serif',
|
|
90
|
+
'sans-serif',
|
|
91
|
+
'monospace',
|
|
92
|
+
'system-ui',
|
|
93
|
+
'ui-monospace',
|
|
94
|
+
'ui-serif',
|
|
95
|
+
'ui-sans-serif',
|
|
96
|
+
'inherit',
|
|
97
|
+
'initial',
|
|
98
|
+
'unset',
|
|
99
|
+
'revert',
|
|
100
|
+
// Common system stack names — these are aliases, not custom fonts.
|
|
101
|
+
'-apple-system',
|
|
102
|
+
'blinkmacsystemfont',
|
|
103
|
+
'segoe ui',
|
|
104
|
+
'roboto',
|
|
105
|
+
'helvetica',
|
|
106
|
+
'helvetica neue',
|
|
107
|
+
'arial',
|
|
108
|
+
'sf mono',
|
|
109
|
+
'sf pro',
|
|
110
|
+
'menlo',
|
|
111
|
+
'monaco',
|
|
112
|
+
'consolas',
|
|
113
|
+
'liberation mono',
|
|
114
|
+
'courier',
|
|
115
|
+
'courier new',
|
|
116
|
+
'georgia',
|
|
117
|
+
'noto sans',
|
|
118
|
+
'noto color emoji',
|
|
119
|
+
'apple color emoji',
|
|
120
|
+
'segoe ui emoji',
|
|
121
|
+
'segoe ui symbol',
|
|
122
|
+
]);
|
|
123
|
+
export function scanContent(filename, content, opts = {}) {
|
|
124
|
+
const out = [];
|
|
125
|
+
// 1. CSS variable overrides for protected tokens.
|
|
126
|
+
if (!opts.skipVarRedefs) {
|
|
127
|
+
const varDeclRe = /(?:^|[\s;{,])(--[a-z-]+)\s*:/gim;
|
|
128
|
+
let m;
|
|
129
|
+
while ((m = varDeclRe.exec(content)) !== null) {
|
|
130
|
+
const name = m[1];
|
|
131
|
+
if (PROTECTED_VARS.has(name)) {
|
|
132
|
+
const line = lineNumberAt(content, m.index);
|
|
133
|
+
out.push(`${filename}:${line} redefines ${name} (template owns this token)`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// 2. font-family overrides.
|
|
138
|
+
// Capture the value of every font-family / fontFamily declaration, then
|
|
139
|
+
// extract candidate font names. JSX values can be ternaries
|
|
140
|
+
// (`fontFamily: isFoo ? "Fraunces" : "Manrope"`), so we pull out every
|
|
141
|
+
// quoted string in the value and check each one separately. If there are
|
|
142
|
+
// no quoted strings, the value is treated as a CSS-style comma list
|
|
143
|
+
// (`font-family: Comic Sans, serif`).
|
|
144
|
+
const fontHeadRe = /(font-family|fontFamily)\s*[:=]/g;
|
|
145
|
+
const quotedRe = /(['"`])([^'"`\n]+)\1/g;
|
|
146
|
+
let m;
|
|
147
|
+
while ((m = fontHeadRe.exec(content)) !== null) {
|
|
148
|
+
const isJsx = m[1] === 'fontFamily';
|
|
149
|
+
const start = m.index + m[0].length;
|
|
150
|
+
const value = sliceFontValue(content, start, isJsx);
|
|
151
|
+
const quotedStrings = [...value.matchAll(quotedRe)].map((x) => x[2]);
|
|
152
|
+
const candidates = quotedStrings.length > 0 ? quotedStrings : [value];
|
|
153
|
+
let flagged = false;
|
|
154
|
+
for (const cand of candidates) {
|
|
155
|
+
const tokens = cand
|
|
156
|
+
.split(',')
|
|
157
|
+
.map((t) => t.trim().replace(/^["']|["']$/g, '').toLowerCase())
|
|
158
|
+
.filter(Boolean);
|
|
159
|
+
for (const t of tokens) {
|
|
160
|
+
if (!ALLOWED_FONT_TOKENS.has(t)) {
|
|
161
|
+
const line = lineNumberAt(content, m.index);
|
|
162
|
+
out.push(`${filename}:${line} non-brand font "${t}"`);
|
|
163
|
+
flagged = true;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (flagged)
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Walk forward from `start` until we hit the end of the font-family value.
|
|
175
|
+
*
|
|
176
|
+
* CSS values end at `;` or `}` or newline.
|
|
177
|
+
* JSX values end at `}` or at a `,` followed by another identifier+colon
|
|
178
|
+
* (the next style property). Commas inside quoted strings or before
|
|
179
|
+
* non-identifiers (e.g. font fallbacks) are part of the value.
|
|
180
|
+
*/
|
|
181
|
+
function sliceFontValue(content, start, isJsx) {
|
|
182
|
+
let i = start;
|
|
183
|
+
let quote = null;
|
|
184
|
+
while (i < content.length) {
|
|
185
|
+
const c = content[i];
|
|
186
|
+
if (quote) {
|
|
187
|
+
if (c === '\\') {
|
|
188
|
+
i += 2;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (c === quote)
|
|
192
|
+
quote = null;
|
|
193
|
+
i++;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
197
|
+
quote = c;
|
|
198
|
+
i++;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (c === '\n' || c === ';' || c === '}')
|
|
202
|
+
break;
|
|
203
|
+
if (isJsx && c === ',') {
|
|
204
|
+
// Lookahead: is this `,` followed by `identifier:`? If yes, end of value.
|
|
205
|
+
let j = i + 1;
|
|
206
|
+
while (j < content.length && (content[j] === ' ' || content[j] === '\t'))
|
|
207
|
+
j++;
|
|
208
|
+
const rest = content.slice(j, j + 64);
|
|
209
|
+
if (/^[A-Za-z_$][\w$]*\s*:/.test(rest))
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
i++;
|
|
213
|
+
}
|
|
214
|
+
return content.slice(start, i);
|
|
215
|
+
}
|
|
216
|
+
function extOf(path) {
|
|
217
|
+
const dot = path.lastIndexOf('.');
|
|
218
|
+
const slash = path.lastIndexOf('/');
|
|
219
|
+
return dot > slash ? path.slice(dot).toLowerCase() : '';
|
|
220
|
+
}
|
|
221
|
+
function lineNumberAt(content, index) {
|
|
222
|
+
let n = 1;
|
|
223
|
+
for (let i = 0; i < index; i++)
|
|
224
|
+
if (content.charCodeAt(i) === 10)
|
|
225
|
+
n++;
|
|
226
|
+
return n;
|
|
227
|
+
}
|
|
228
|
+
//# 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":"AACA,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAGtE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,MAAkB;IAC5D,MAAM,MAAM,GAAa,EAAE,CAAC;IAE5B,IAAI,KAAK,EAAE,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;QACvC,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;QACxB,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,SAAS;QACrC,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,CAAC,GAAG;YAAE,SAAS;QACnB,gEAAgE;QAChE,iEAAiE;QACjE,kEAAkE;QAClE,oEAAoE;QACpE,iCAAiC;QACjC,MAAM,OAAO,GACX,GAAG,KAAK,MAAM,IAAI,GAAG,KAAK,OAAO;YAC/B,CAAC,CAAC,gBAAgB,CAAC,GAAG,CAAC;YACvB,CAAC,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC;QAC7B,uEAAuE;QACvE,sEAAsE;QACtE,mEAAmE;QACnE,mEAAmE;QACnE,qDAAqD;QACrD,MAAM,WAAW,GAAG,IAAI,KAAK,mBAAmB,IAAI,IAAI,KAAK,kBAAkB,CAAC;QAChF,MAAM,UAAU,GAAG,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,aAAa,EAAE,WAAW,EAAE,CAAC,CAAC;QAC9E,MAAM,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,CAAC;QAC3B,oEAAoE;QACpE,kCAAkC;QAClC,IAAI,MAAM,CAAC,MAAM,IAAI,EAAE;YAAE,MAAM;IACjC,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO;YACL,IAAI,EAAE,oBAAoB;YAC1B,MAAM,EAAE,MAAM;YACd,MAAM,EAAE,+CAA+C;SACxD,CAAC;IACJ,CAAC;IACD,OAAO;QACL,IAAI,EAAE,oBAAoB;QAC1B,MAAM,EAAE,MAAM;QACd,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,qBAAqB,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE;QAC7E,WAAW,EAAE;YACX,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC;YACrB,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,MAAM,CAAC,MAAM,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAClE,uFAAuF;YACvF,2FAA2F;SAC5F;KACF,CAAC;AACJ,CAAC;AAED,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC;AAEvF,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC;IAC7B,SAAS;IACT,OAAO;IACP,SAAS;IACT,UAAU;IACV,QAAQ;IACR,eAAe;IACf,SAAS;CACV,CAAC,CAAC;AAEH;;;;GAIG;AACH,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAC;IAClC,cAAc;IACd,SAAS;IACT,UAAU;IACV,0CAA0C;IAC1C,OAAO;IACP,YAAY;IACZ,WAAW;IACX,WAAW;IACX,cAAc;IACd,UAAU;IACV,eAAe;IACf,SAAS;IACT,SAAS;IACT,OAAO;IACP,QAAQ;IACR,mEAAmE;IACnE,eAAe;IACf,oBAAoB;IACpB,UAAU;IACV,QAAQ;IACR,WAAW;IACX,gBAAgB;IAChB,OAAO;IACP,SAAS;IACT,QAAQ;IACR,OAAO;IACP,QAAQ;IACR,UAAU;IACV,iBAAiB;IACjB,SAAS;IACT,aAAa;IACb,SAAS;IACT,WAAW;IACX,kBAAkB;IAClB,mBAAmB;IACnB,gBAAgB;IAChB,iBAAiB;CAClB,CAAC,CAAC;AAEH,MAAM,UAAU,WAAW,CACzB,QAAgB,EAChB,OAAe,EACf,OAAoC,EAAE;IAEtC,MAAM,GAAG,GAAa,EAAE,CAAC;IAEzB,kDAAkD;IAClD,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;QACxB,MAAM,SAAS,GAAG,iCAAiC,CAAC;QACpD,IAAI,CAAyB,CAAC;QAC9B,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YAC9C,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC;YACnB,IAAI,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC7B,MAAM,IAAI,GAAG,YAAY,CAAC,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC;gBAC5C,GAAG,CAAC,IAAI,CAAC,GAAG,QAAQ,IAAI,IAAI,cAAc,IAAI,6BAA6B,CAAC,CAAC;YAC/E,CAAC;QACH,CAAC;IACH,CAAC;IAED,4BAA4B;IAC5B,wEAAwE;IACxE,4DAA4D;IAC5D,uEAAuE;IACvE,yEAAyE;IACzE,oEAAoE;IACpE,sCAAsC;IACtC,MAAM,UAAU,GAAG,kCAAkC,CAAC;IACtD,MAAM,QAAQ,GAAG,uBAAuB,CAAC;IACzC,IAAI,CAAyB,CAAC;IAC9B,OAAO,CAAC,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC/C,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,YAAY,CAAC;QACpC,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;QACpC,MAAM,KAAK,GAAG,cAAc,CAAC,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;QACpD,MAAM,aAAa,GAAG,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC;QACtE,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QACtE,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;YAC9B,MAAM,MAAM,GAAG,IAAI;iBAChB,KAAK,CAAC,GAAG,CAAC;iBACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;iBAC9D,MAAM,CAAC,OAAO,CAAC,CAAC;YACnB,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;gBACvB,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;oBAChC,MAAM,IAAI,GAAG,YAAY,CAAC,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC;oBAC5C,GAAG,CAAC,IAAI,CAAC,GAAG,QAAQ,IAAI,IAAI,oBAAoB,CAAC,GAAG,CAAC,CAAC;oBACtD,OAAO,GAAG,IAAI,CAAC;oBACf,MAAM;gBACR,CAAC;YACH,CAAC;YACD,IAAI,OAAO;gBAAE,MAAM;QACrB,CAAC;IACH,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,cAAc,CAAC,OAAe,EAAE,KAAa,EAAE,KAAc;IACpE,IAAI,CAAC,GAAG,KAAK,CAAC;IACd,IAAI,KAAK,GAAkB,IAAI,CAAC;IAChC,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;QAC1B,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAE,CAAC;QACtB,IAAI,KAAK,EAAE,CAAC;YACV,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;gBACf,CAAC,IAAI,CAAC,CAAC;gBACP,SAAS;YACX,CAAC;YACD,IAAI,CAAC,KAAK,KAAK;gBAAE,KAAK,GAAG,IAAI,CAAC;YAC9B,CAAC,EAAE,CAAC;YACJ,SAAS;QACX,CAAC;QACD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;YACxC,KAAK,GAAG,CAAC,CAAC;YACV,CAAC,EAAE,CAAC;YACJ,SAAS;QACX,CAAC;QACD,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG;YAAE,MAAM;QAChD,IAAI,KAAK,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;YACvB,0EAA0E;YAC1E,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACd,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,GAAG,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;gBAAE,CAAC,EAAE,CAAC;YAC9E,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC;YACtC,IAAI,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC;gBAAE,MAAM;QAChD,CAAC;QACD,CAAC,EAAE,CAAC;IACN,CAAC;IACD,OAAO,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;AACjC,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;AAED,SAAS,YAAY,CAAC,OAAe,EAAE,KAAa;IAClD,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE;QAAE,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,EAAE;YAAE,CAAC,EAAE,CAAC;IACtE,OAAO,CAAC,CAAC;AACX,CAAC"}
|
|
@@ -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,CAsBnF"}
|
|
@@ -0,0 +1,37 @@
|
|
|
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 {
|
|
22
|
+
name: 'No .env.production',
|
|
23
|
+
status: 'pass',
|
|
24
|
+
detail: 'no production env files committed',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
name: 'No .env.production',
|
|
29
|
+
status: 'fail',
|
|
30
|
+
detail: `${offenders.length} .env.production file(s): ${offenders.join(', ')}`,
|
|
31
|
+
suggestions: [
|
|
32
|
+
'Delete the file and add `.env.production` to .gitignore.',
|
|
33
|
+
'Use Cloudflare Pages → Settings → Environment Variables for production secrets instead.',
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
//# 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;YACL,IAAI,EAAE,oBAAoB;YAC1B,MAAM,EAAE,MAAM;YACd,MAAM,EAAE,mCAAmC;SAC5C,CAAC;IACJ,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;AAoB/C;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,CAsBlF"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const TEXT_EXTS = new Set([
|
|
2
|
+
'.md',
|
|
3
|
+
'.txt',
|
|
4
|
+
'.json',
|
|
5
|
+
'.ts',
|
|
6
|
+
'.tsx',
|
|
7
|
+
'.js',
|
|
8
|
+
'.jsx',
|
|
9
|
+
'.html',
|
|
10
|
+
'.htm',
|
|
11
|
+
'.css',
|
|
12
|
+
'.scss',
|
|
13
|
+
'.yaml',
|
|
14
|
+
'.yml',
|
|
15
|
+
'.toml',
|
|
16
|
+
'.svg',
|
|
17
|
+
]);
|
|
18
|
+
/**
|
|
19
|
+
* Catches the case where APPNAME placeholders from the template were
|
|
20
|
+
* never substituted. The CLI's `fas init` substitutes correctly today
|
|
21
|
+
* but if someone clones the template manually and forgets the find/
|
|
22
|
+
* replace step, every page renders with `<title>APPNAME — …</title>`.
|
|
23
|
+
*/
|
|
24
|
+
export async function checkNoPlaceholders(source) {
|
|
25
|
+
const offenders = [];
|
|
26
|
+
for await (const path of source.list()) {
|
|
27
|
+
if (!TEXT_EXTS.has(extOf(path)))
|
|
28
|
+
continue;
|
|
29
|
+
const content = await source.read(path);
|
|
30
|
+
if (content?.includes('APPNAME')) {
|
|
31
|
+
offenders.push(path);
|
|
32
|
+
if (offenders.length >= 5)
|
|
33
|
+
break; // don't spam, summarize
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (offenders.length === 0) {
|
|
37
|
+
return { name: 'No template placeholders', status: 'pass', detail: 'all APPNAME substituted' };
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
name: 'No template placeholders',
|
|
41
|
+
status: 'fail',
|
|
42
|
+
detail: `${offenders.length === 5 ? '5+' : offenders.length} file(s) still contain APPNAME: ${offenders.join(', ')}`,
|
|
43
|
+
suggestions: [
|
|
44
|
+
'Run `fas init <id>` instead of cloning the template manually — it does the substitution.',
|
|
45
|
+
'Or do a project-wide find/replace: APPNAME → your app id (lowercase, hyphenated).',
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function extOf(path) {
|
|
50
|
+
const dot = path.lastIndexOf('.');
|
|
51
|
+
const slash = path.lastIndexOf('/');
|
|
52
|
+
return dot > slash ? path.slice(dot).toLowerCase() : '';
|
|
53
|
+
}
|
|
54
|
+
//# 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;IACL,MAAM;IACN,OAAO;IACP,KAAK;IACL,MAAM;IACN,KAAK;IACL,MAAM;IACN,OAAO;IACP,MAAM;IACN,MAAM;IACN,OAAO;IACP,OAAO;IACP,MAAM;IACN,OAAO;IACP,MAAM;CACP,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,EAAE,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;YACjC,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 ProAppStore 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 (ProAppStore) 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 @proappstore/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,109 @@
|
|
|
1
|
+
import { isGameProject } from '../lib/project-type.js';
|
|
2
|
+
/**
|
|
3
|
+
* Games on ProAppStore 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 (ProAppStore) 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 @proappstore/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 @proappstore/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 @proappstore/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 = [/(?:^|[\s,{])(?:html|body)\s*\{[^}]*min-height\s*:\s*100vh/im];
|
|
96
|
+
const VIEWPORT_LOCK = /(?:height|max-height)\s*:\s*100(?:s?vh)|GameShell|@proappstore\/games/i;
|
|
97
|
+
function extOf(path) {
|
|
98
|
+
const dot = path.lastIndexOf('.');
|
|
99
|
+
const slash = path.lastIndexOf('/');
|
|
100
|
+
return dot > slash ? path.slice(dot).toLowerCase() : '';
|
|
101
|
+
}
|
|
102
|
+
function lineNumberAt(content, index) {
|
|
103
|
+
let n = 1;
|
|
104
|
+
for (let i = 0; i < index; i++)
|
|
105
|
+
if (content.charCodeAt(i) === 10)
|
|
106
|
+
n++;
|
|
107
|
+
return n;
|
|
108
|
+
}
|
|
109
|
+
//# 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-tracking.d.ts","sourceRoot":"","sources":["../../src/checks/no-tracking.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAExD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAuE/C,wBAAsB,eAAe,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,CA6C9E"}
|