@freeappstore/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 (54) hide show
  1. package/README.md +27 -0
  2. package/dist/checks/brand-fonts.d.ts +9 -0
  3. package/dist/checks/brand-fonts.d.ts.map +1 -0
  4. package/dist/checks/brand-fonts.js +43 -0
  5. package/dist/checks/brand-fonts.js.map +1 -0
  6. package/dist/checks/brand-fonts.test.d.ts +2 -0
  7. package/dist/checks/brand-fonts.test.d.ts.map +1 -0
  8. package/dist/checks/brand-fonts.test.js +45 -0
  9. package/dist/checks/brand-fonts.test.js.map +1 -0
  10. package/dist/checks/bundle-size.d.ts +8 -0
  11. package/dist/checks/bundle-size.d.ts.map +1 -0
  12. package/dist/checks/bundle-size.js +64 -0
  13. package/dist/checks/bundle-size.js.map +1 -0
  14. package/dist/checks/bundle-size.test.d.ts +2 -0
  15. package/dist/checks/bundle-size.test.d.ts.map +1 -0
  16. package/dist/checks/bundle-size.test.js +57 -0
  17. package/dist/checks/bundle-size.test.js.map +1 -0
  18. package/dist/checks/manifest.d.ts +8 -0
  19. package/dist/checks/manifest.d.ts.map +1 -0
  20. package/dist/checks/manifest.js +48 -0
  21. package/dist/checks/manifest.js.map +1 -0
  22. package/dist/checks/manifest.test.d.ts +2 -0
  23. package/dist/checks/manifest.test.d.ts.map +1 -0
  24. package/dist/checks/manifest.test.js +53 -0
  25. package/dist/checks/manifest.test.js.map +1 -0
  26. package/dist/checks/no-placeholders.d.ts +9 -0
  27. package/dist/checks/no-placeholders.d.ts.map +1 -0
  28. package/dist/checks/no-placeholders.js +52 -0
  29. package/dist/checks/no-placeholders.js.map +1 -0
  30. package/dist/checks/no-placeholders.test.d.ts +2 -0
  31. package/dist/checks/no-placeholders.test.d.ts.map +1 -0
  32. package/dist/checks/no-placeholders.test.js +46 -0
  33. package/dist/checks/no-placeholders.test.js.map +1 -0
  34. package/dist/checks/no-tracking.d.ts +3 -0
  35. package/dist/checks/no-tracking.d.ts.map +1 -0
  36. package/dist/checks/no-tracking.js +48 -0
  37. package/dist/checks/no-tracking.js.map +1 -0
  38. package/dist/checks/no-tracking.test.d.ts +2 -0
  39. package/dist/checks/no-tracking.test.d.ts.map +1 -0
  40. package/dist/checks/no-tracking.test.js +42 -0
  41. package/dist/checks/no-tracking.test.js.map +1 -0
  42. package/dist/index.d.ts +15 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +21 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/lib/walk.d.ts +6 -0
  47. package/dist/lib/walk.d.ts.map +1 -0
  48. package/dist/lib/walk.js +35 -0
  49. package/dist/lib/walk.js.map +1 -0
  50. package/dist/types.d.ts +12 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +2 -0
  53. package/dist/types.js.map +1 -0
  54. package/package.json +47 -0
package/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # @freeappstore/compliance
2
+
3
+ Compliance checks for apps published on **freeappstore.online**. Same checks the CLI runs locally and the template's CI runs on every push.
4
+
5
+ ```ts
6
+ import { runChecks } from '@freeappstore/compliance';
7
+
8
+ const results = await runChecks(process.cwd());
9
+ for (const r of results) {
10
+ console.log(`${r.status} ${r.name} ${r.detail}`);
11
+ for (const s of r.suggestions ?? []) console.log(` → ${s}`);
12
+ }
13
+ ```
14
+
15
+ ## Checks (v0.1.0)
16
+
17
+ | Name | What it checks | Status on fail |
18
+ |---|---|---|
19
+ | `No template placeholders` | No file still contains `APPNAME` | **fail** |
20
+ | `No tracking SDKs` | No reference to google-analytics / gtag / amplitude / mixpanel / segment / hotjar / plausible / posthog | **fail** |
21
+ | `Brand fonts present` | Manrope + Fraunces referenced in CSS or HTML | **fail** |
22
+ | `PWA manifest` | `web/public/manifest.json` exists and has name / short_name / start_url / display | warn |
23
+ | `Bundle size` | Largest JS in `web/dist/assets/` is ≤ 300 KB gzipped | **fail** if too big; warn if not built yet |
24
+
25
+ ## License
26
+
27
+ MIT.
@@ -0,0 +1,9 @@
1
+ import type { CheckResult } from '../types.js';
2
+ /**
3
+ * The platform's brand wants Manrope (body) + Fraunces (display) so apps
4
+ * have a consistent typographic feel across the storefront. We accept any
5
+ * CSS file referencing both font names — the actual font-loading mechanism
6
+ * (Google Fonts, self-hosted, etc.) is the app's choice.
7
+ */
8
+ export declare function checkBrandFonts(repoDir: string): Promise<CheckResult>;
9
+ //# sourceMappingURL=brand-fonts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"brand-fonts.d.ts","sourceRoot":"","sources":["../../src/checks/brand-fonts.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAK/C;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA6B3E"}
@@ -0,0 +1,43 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { extname } from 'node:path';
3
+ import { walk } from '../lib/walk.js';
4
+ const REQUIRED_FONTS = ['Manrope', 'Fraunces'];
5
+ /**
6
+ * The platform's brand wants Manrope (body) + Fraunces (display) so apps
7
+ * have a consistent typographic feel across the storefront. We accept any
8
+ * CSS file referencing both font names — the actual font-loading mechanism
9
+ * (Google Fonts, self-hosted, etc.) is the app's choice.
10
+ */
11
+ export async function checkBrandFonts(repoDir) {
12
+ const found = new Set();
13
+ for await (const file of walk(repoDir)) {
14
+ const ext = extname(file).toLowerCase();
15
+ if (ext !== '.css' && ext !== '.scss' && ext !== '.html')
16
+ continue;
17
+ const content = await readFile(file, 'utf8').catch(() => '');
18
+ for (const font of REQUIRED_FONTS) {
19
+ if (content.includes(font))
20
+ found.add(font);
21
+ }
22
+ if (found.size === REQUIRED_FONTS.length)
23
+ break;
24
+ }
25
+ const missing = REQUIRED_FONTS.filter((f) => !found.has(f));
26
+ if (missing.length === 0) {
27
+ return {
28
+ name: 'Brand fonts present',
29
+ status: 'pass',
30
+ detail: REQUIRED_FONTS.join(' + '),
31
+ };
32
+ }
33
+ return {
34
+ name: 'Brand fonts present',
35
+ status: 'fail',
36
+ detail: `missing: ${missing.join(', ')}`,
37
+ suggestions: [
38
+ 'Add the Google Fonts <link> for Manrope + Fraunces to web/index.html.',
39
+ 'Use them as your body / display fonts in CSS so storefront and apps feel consistent.',
40
+ ],
41
+ };
42
+ }
43
+ //# sourceMappingURL=brand-fonts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"brand-fonts.js","sourceRoot":"","sources":["../../src/checks/brand-fonts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAEtC,MAAM,cAAc,GAAG,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;AAE/C;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,OAAe;IACnD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,IAAI,KAAK,EAAE,MAAM,IAAI,IAAI,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACvC,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;QACxC,IAAI,GAAG,KAAK,MAAM,IAAI,GAAG,KAAK,OAAO,IAAI,GAAG,KAAK,OAAO;YAAE,SAAS;QACnE,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QAC7D,KAAK,MAAM,IAAI,IAAI,cAAc,EAAE,CAAC;YAClC,IAAI,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC;gBAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC9C,CAAC;QACD,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc,CAAC,MAAM;YAAE,MAAM;IAClD,CAAC;IAED,MAAM,OAAO,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5D,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO;YACL,IAAI,EAAE,qBAAqB;YAC3B,MAAM,EAAE,MAAM;YACd,MAAM,EAAE,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC;SACnC,CAAC;IACJ,CAAC;IACD,OAAO;QACL,IAAI,EAAE,qBAAqB;QAC3B,MAAM,EAAE,MAAM;QACd,MAAM,EAAE,YAAY,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;QACxC,WAAW,EAAE;YACX,uEAAuE;YACvE,sFAAsF;SACvF;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=brand-fonts.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"brand-fonts.test.d.ts","sourceRoot":"","sources":["../../src/checks/brand-fonts.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,45 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mkdtemp, rm, writeFile, mkdir } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { checkBrandFonts } from './brand-fonts.js';
6
+ let dir;
7
+ beforeEach(async () => {
8
+ dir = await mkdtemp(join(tmpdir(), 'fas-compliance-'));
9
+ await mkdir(join(dir, 'web', 'src'), { recursive: true });
10
+ });
11
+ afterEach(async () => {
12
+ await rm(dir, { recursive: true, force: true });
13
+ });
14
+ describe('checkBrandFonts', () => {
15
+ it('passes when both Manrope and Fraunces are referenced', async () => {
16
+ await writeFile(join(dir, 'web', 'src', 'index.css'), 'body { font-family: Manrope, sans-serif; } h1 { font-family: Fraunces; }');
17
+ const r = await checkBrandFonts(dir);
18
+ expect(r.status).toBe('pass');
19
+ });
20
+ it('passes with fonts split across files', async () => {
21
+ await writeFile(join(dir, 'web', 'src', 'a.css'), '@import "Manrope";');
22
+ await writeFile(join(dir, 'web', 'src', 'b.css'), '@import "Fraunces";');
23
+ const r = await checkBrandFonts(dir);
24
+ expect(r.status).toBe('pass');
25
+ });
26
+ it('passes when fonts are in HTML link tag (Google Fonts)', async () => {
27
+ await writeFile(join(dir, 'web', 'index.html'), '<link href="https://fonts.googleapis.com/css2?family=Manrope&family=Fraunces" rel="stylesheet">');
28
+ const r = await checkBrandFonts(dir);
29
+ expect(r.status).toBe('pass');
30
+ });
31
+ it('fails when only one of the two is present', async () => {
32
+ await writeFile(join(dir, 'web', 'src', 'a.css'), '@import "Manrope";');
33
+ const r = await checkBrandFonts(dir);
34
+ expect(r.status).toBe('fail');
35
+ expect(r.detail).toMatch(/Fraunces/);
36
+ });
37
+ it('fails when neither is present', async () => {
38
+ await writeFile(join(dir, 'web', 'src', 'a.css'), 'body { font: Helvetica; }');
39
+ const r = await checkBrandFonts(dir);
40
+ expect(r.status).toBe('fail');
41
+ expect(r.detail).toMatch(/Manrope/);
42
+ expect(r.detail).toMatch(/Fraunces/);
43
+ });
44
+ });
45
+ //# sourceMappingURL=brand-fonts.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"brand-fonts.test.js","sourceRoot":"","sources":["../../src/checks/brand-fonts.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { CheckResult } from '../types.js';
2
+ /**
3
+ * Checks the largest JS asset under web/dist/assets/ against the 300KB-gzip
4
+ * limit. Returns 'warn' if dist hasn't been built yet (we don't want to
5
+ * silently pass when there's nothing to measure).
6
+ */
7
+ export declare function checkBundleSize(repoDir: string): Promise<CheckResult>;
8
+ //# sourceMappingURL=bundle-size.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundle-size.d.ts","sourceRoot":"","sources":["../../src/checks/bundle-size.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAI/C;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAyD3E"}
@@ -0,0 +1,64 @@
1
+ import { readdir, readFile, stat } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { gzipSync } from 'node:zlib';
4
+ const MAX_GZIP_BYTES = 300 * 1024; // 300 KB — matches the template's compliance.yml
5
+ /**
6
+ * Checks the largest JS asset under web/dist/assets/ against the 300KB-gzip
7
+ * limit. Returns 'warn' if dist hasn't been built yet (we don't want to
8
+ * silently pass when there's nothing to measure).
9
+ */
10
+ export async function checkBundleSize(repoDir) {
11
+ const assetsDir = join(repoDir, 'web', 'dist', 'assets');
12
+ let entries;
13
+ try {
14
+ entries = await readdir(assetsDir);
15
+ }
16
+ catch {
17
+ return {
18
+ name: 'Bundle size',
19
+ status: 'warn',
20
+ detail: 'web/dist not built yet — run `pnpm build` to measure',
21
+ };
22
+ }
23
+ const jsFiles = entries.filter((f) => f.endsWith('.js'));
24
+ if (jsFiles.length === 0) {
25
+ return {
26
+ name: 'Bundle size',
27
+ status: 'warn',
28
+ detail: `no JS files in ${assetsDir}`,
29
+ };
30
+ }
31
+ // Find the largest JS file (the entry chunk) — same behavior as the
32
+ // existing compliance.yml in the templates.
33
+ let largest = '';
34
+ let largestSize = 0;
35
+ for (const f of jsFiles) {
36
+ const s = await stat(join(assetsDir, f));
37
+ if (s.size > largestSize) {
38
+ largest = f;
39
+ largestSize = s.size;
40
+ }
41
+ }
42
+ const content = await readFile(join(assetsDir, largest));
43
+ const gzipped = gzipSync(content).byteLength;
44
+ const kb = (gzipped / 1024).toFixed(1);
45
+ const limitKb = (MAX_GZIP_BYTES / 1024).toFixed(0);
46
+ if (gzipped > MAX_GZIP_BYTES) {
47
+ return {
48
+ name: 'Bundle size',
49
+ status: 'fail',
50
+ detail: `${largest}: ${kb} KB gzipped (limit ${limitKb} KB)`,
51
+ suggestions: [
52
+ 'Find heavy dependencies: `pnpm dlx vite-bundle-visualizer`',
53
+ 'Lazy-load non-critical screens with dynamic import().',
54
+ 'Consider lighter alternatives for the biggest deps.',
55
+ ],
56
+ };
57
+ }
58
+ return {
59
+ name: 'Bundle size',
60
+ status: 'pass',
61
+ detail: `${largest}: ${kb} KB gzipped (limit ${limitKb} KB)`,
62
+ };
63
+ }
64
+ //# sourceMappingURL=bundle-size.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundle-size.js","sourceRoot":"","sources":["../../src/checks/bundle-size.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAGrC,MAAM,cAAc,GAAG,GAAG,GAAG,IAAI,CAAC,CAAC,iDAAiD;AAEpF;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,OAAe;IACnD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;IACzD,IAAI,OAAO,CAAC;IACZ,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,OAAO,CAAC,SAAS,CAAC,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO;YACL,IAAI,EAAE,aAAa;YACnB,MAAM,EAAE,MAAM;YACd,MAAM,EAAE,sDAAsD;SAC/D,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;IACzD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO;YACL,IAAI,EAAE,aAAa;YACnB,MAAM,EAAE,MAAM;YACd,MAAM,EAAE,kBAAkB,SAAS,EAAE;SACtC,CAAC;IACJ,CAAC;IAED,oEAAoE;IACpE,4CAA4C;IAC5C,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,IAAI,WAAW,GAAG,CAAC,CAAC;IACpB,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC;QACzC,IAAI,CAAC,CAAC,IAAI,GAAG,WAAW,EAAE,CAAC;YACzB,OAAO,GAAG,CAAC,CAAC;YACZ,WAAW,GAAG,CAAC,CAAC,IAAI,CAAC;QACvB,CAAC;IACH,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC;IACzD,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC;IAC7C,MAAM,EAAE,GAAG,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACvC,MAAM,OAAO,GAAG,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAEnD,IAAI,OAAO,GAAG,cAAc,EAAE,CAAC;QAC7B,OAAO;YACL,IAAI,EAAE,aAAa;YACnB,MAAM,EAAE,MAAM;YACd,MAAM,EAAE,GAAG,OAAO,KAAK,EAAE,sBAAsB,OAAO,MAAM;YAC5D,WAAW,EAAE;gBACX,4DAA4D;gBAC5D,uDAAuD;gBACvD,qDAAqD;aACtD;SACF,CAAC;IACJ,CAAC;IAED,OAAO;QACL,IAAI,EAAE,aAAa;QACnB,MAAM,EAAE,MAAM;QACd,MAAM,EAAE,GAAG,OAAO,KAAK,EAAE,sBAAsB,OAAO,MAAM;KAC7D,CAAC;AACJ,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=bundle-size.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundle-size.test.d.ts","sourceRoot":"","sources":["../../src/checks/bundle-size.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,57 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mkdtemp, rm, writeFile, mkdir } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { checkBundleSize } from './bundle-size.js';
6
+ let dir;
7
+ beforeEach(async () => {
8
+ dir = await mkdtemp(join(tmpdir(), 'fas-compliance-'));
9
+ });
10
+ afterEach(async () => {
11
+ await rm(dir, { recursive: true, force: true });
12
+ });
13
+ async function writeAsset(name, content) {
14
+ const p = join(dir, 'web', 'dist', 'assets');
15
+ await mkdir(p, { recursive: true });
16
+ await writeFile(join(p, name), content);
17
+ }
18
+ describe('checkBundleSize', () => {
19
+ it('warns when web/dist not built', async () => {
20
+ const r = await checkBundleSize(dir);
21
+ expect(r.status).toBe('warn');
22
+ expect(r.detail).toMatch(/pnpm build/);
23
+ });
24
+ it('warns when no JS files in assets', async () => {
25
+ const p = join(dir, 'web', 'dist', 'assets');
26
+ await mkdir(p, { recursive: true });
27
+ await writeFile(join(p, 'styles.css'), 'body{color:red}');
28
+ const r = await checkBundleSize(dir);
29
+ expect(r.status).toBe('warn');
30
+ expect(r.detail).toMatch(/no JS/);
31
+ });
32
+ it('passes when largest JS is under 300KB gzipped', async () => {
33
+ // ~100KB of repetitive content gzips to a few KB
34
+ await writeAsset('index-abc.js', 'a'.repeat(100_000));
35
+ const r = await checkBundleSize(dir);
36
+ expect(r.status).toBe('pass');
37
+ expect(r.detail).toMatch(/index-abc\.js/);
38
+ expect(r.detail).toMatch(/limit 300 KB/);
39
+ });
40
+ it('picks the largest JS file for measurement', async () => {
41
+ await writeAsset('small.js', 'tiny');
42
+ await writeAsset('big.js', 'a'.repeat(200_000));
43
+ const r = await checkBundleSize(dir);
44
+ expect(r.detail).toMatch(/big\.js/);
45
+ });
46
+ it('fails when largest JS exceeds 300KB gzipped', async () => {
47
+ // Random bytes don't compress, so 400KB of randomness ~= 400KB gzipped.
48
+ const random = Buffer.alloc(400 * 1024);
49
+ for (let i = 0; i < random.length; i++)
50
+ random[i] = Math.floor(Math.random() * 256);
51
+ await writeAsset('huge.js', random);
52
+ const r = await checkBundleSize(dir);
53
+ expect(r.status).toBe('fail');
54
+ expect(r.suggestions?.length ?? 0).toBeGreaterThan(0);
55
+ });
56
+ });
57
+ //# sourceMappingURL=bundle-size.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundle-size.test.js","sourceRoot":"","sources":["../../src/checks/bundle-size.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { CheckResult } from '../types.js';
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 declare function checkManifest(repoDir: string): Promise<CheckResult>;
8
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/checks/manifest.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAuCzE"}
@@ -0,0 +1,48 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
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 async function checkManifest(repoDir) {
9
+ const path = join(repoDir, 'web', 'public', 'manifest.json');
10
+ let raw;
11
+ try {
12
+ raw = await readFile(path, 'utf8');
13
+ }
14
+ catch {
15
+ return {
16
+ name: 'PWA manifest',
17
+ status: 'fail',
18
+ detail: 'web/public/manifest.json missing',
19
+ suggestions: [
20
+ 'Add a manifest.json with at least name, short_name, start_url, display, icons.',
21
+ 'See template-standalone for a working example.',
22
+ ],
23
+ };
24
+ }
25
+ let parsed;
26
+ try {
27
+ parsed = JSON.parse(raw);
28
+ }
29
+ catch {
30
+ return {
31
+ name: 'PWA manifest',
32
+ status: 'fail',
33
+ detail: `web/public/manifest.json is not valid JSON`,
34
+ };
35
+ }
36
+ const required = ['name', 'short_name', 'start_url', 'display'];
37
+ const missing = required.filter((k) => typeof parsed[k] !== 'string' || parsed[k] === '');
38
+ if (missing.length > 0) {
39
+ return {
40
+ name: 'PWA manifest',
41
+ status: 'warn',
42
+ detail: `missing fields: ${missing.join(', ')}`,
43
+ suggestions: [`Add the ${missing.join(', ')} field(s) to manifest.json so installs work.`],
44
+ };
45
+ }
46
+ return { name: 'PWA manifest', status: 'pass', detail: 'web/public/manifest.json' };
47
+ }
48
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../src/checks/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAAe;IACjD,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,eAAe,CAAC,CAAC;IAC7D,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO;YACL,IAAI,EAAE,cAAc;YACpB,MAAM,EAAE,MAAM;YACd,MAAM,EAAE,kCAAkC;YAC1C,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,4CAA4C;SACrD,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,0BAA0B,EAAE,CAAC;AACtF,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=manifest.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.test.d.ts","sourceRoot":"","sources":["../../src/checks/manifest.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,53 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mkdtemp, rm, writeFile, mkdir } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { checkManifest } from './manifest.js';
6
+ let dir;
7
+ beforeEach(async () => {
8
+ dir = await mkdtemp(join(tmpdir(), 'fas-compliance-'));
9
+ });
10
+ afterEach(async () => {
11
+ await rm(dir, { recursive: true, force: true });
12
+ });
13
+ async function withManifest(content) {
14
+ await mkdir(join(dir, 'web', 'public'), { recursive: true });
15
+ await writeFile(join(dir, 'web', 'public', 'manifest.json'), content);
16
+ }
17
+ describe('checkManifest', () => {
18
+ it('fails when manifest.json missing', async () => {
19
+ const r = await checkManifest(dir);
20
+ expect(r.status).toBe('fail');
21
+ expect(r.detail).toMatch(/missing/);
22
+ });
23
+ it('fails when manifest.json is not valid JSON', async () => {
24
+ await withManifest('{ not json');
25
+ const r = await checkManifest(dir);
26
+ expect(r.status).toBe('fail');
27
+ expect(r.detail).toMatch(/JSON/i);
28
+ });
29
+ it('warns when required fields are missing', async () => {
30
+ await withManifest(JSON.stringify({ name: 'My App' }));
31
+ const r = await checkManifest(dir);
32
+ expect(r.status).toBe('warn');
33
+ expect(r.detail).toMatch(/short_name/);
34
+ expect(r.detail).toMatch(/start_url/);
35
+ });
36
+ it('passes with all four required fields present', async () => {
37
+ await withManifest(JSON.stringify({
38
+ name: 'My App',
39
+ short_name: 'MyApp',
40
+ start_url: '/',
41
+ display: 'standalone',
42
+ }));
43
+ const r = await checkManifest(dir);
44
+ expect(r.status).toBe('pass');
45
+ });
46
+ it('treats empty-string values for required fields as missing', async () => {
47
+ await withManifest(JSON.stringify({ name: '', short_name: 'X', start_url: '/', display: 'standalone' }));
48
+ const r = await checkManifest(dir);
49
+ expect(r.status).toBe('warn');
50
+ expect(r.detail).toMatch(/name/);
51
+ });
52
+ });
53
+ //# sourceMappingURL=manifest.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.test.js","sourceRoot":"","sources":["../../src/checks/manifest.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import type { CheckResult } from '../types.js';
2
+ /**
3
+ * Catches the case where APPNAME placeholders from the template were
4
+ * never substituted. The CLI's `fas init` substitutes correctly today
5
+ * but if someone clones the template manually and forgets the find/
6
+ * replace step, every page renders with `<title>APPNAME — …</title>`.
7
+ */
8
+ export declare function checkNoPlaceholders(repoDir: string): Promise<CheckResult>;
9
+ //# 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":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAqB/C;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAsB/E"}
@@ -0,0 +1,52 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { extname } from 'node:path';
3
+ import { walk } from '../lib/walk.js';
4
+ const TEXT_EXTS = new Set([
5
+ '.md',
6
+ '.txt',
7
+ '.json',
8
+ '.ts',
9
+ '.tsx',
10
+ '.js',
11
+ '.jsx',
12
+ '.html',
13
+ '.htm',
14
+ '.css',
15
+ '.scss',
16
+ '.yaml',
17
+ '.yml',
18
+ '.toml',
19
+ '.svg',
20
+ ]);
21
+ /**
22
+ * Catches the case where APPNAME placeholders from the template were
23
+ * never substituted. The CLI's `fas init` substitutes correctly today
24
+ * but if someone clones the template manually and forgets the find/
25
+ * replace step, every page renders with `<title>APPNAME — …</title>`.
26
+ */
27
+ export async function checkNoPlaceholders(repoDir) {
28
+ const offenders = [];
29
+ for await (const file of walk(repoDir)) {
30
+ if (!TEXT_EXTS.has(extname(file).toLowerCase()))
31
+ continue;
32
+ const content = await readFile(file, 'utf8').catch(() => '');
33
+ if (content.includes('APPNAME')) {
34
+ offenders.push(file.replace(repoDir + '/', ''));
35
+ if (offenders.length >= 5)
36
+ break; // don't spam, summarize
37
+ }
38
+ }
39
+ if (offenders.length === 0) {
40
+ return { name: 'No template placeholders', status: 'pass', detail: 'all APPNAME substituted' };
41
+ }
42
+ return {
43
+ name: 'No template placeholders',
44
+ status: 'fail',
45
+ detail: `${offenders.length === 5 ? '5+' : offenders.length} file(s) still contain APPNAME: ${offenders.join(', ')}`,
46
+ suggestions: [
47
+ 'Run `fas init <id>` instead of cloning the template manually — it does the substitution.',
48
+ 'Or do a project-wide find/replace: APPNAME → your app id (lowercase, hyphenated).',
49
+ ],
50
+ };
51
+ }
52
+ //# sourceMappingURL=no-placeholders.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-placeholders.js","sourceRoot":"","sources":["../../src/checks/no-placeholders.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAEtC,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,OAAe;IACvD,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,IAAI,KAAK,EAAE,MAAM,IAAI,IAAI,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACvC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;YAAE,SAAS;QAC1D,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QAC7D,IAAI,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;YAChC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,GAAG,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC;YAChD,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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=no-placeholders.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-placeholders.test.d.ts","sourceRoot":"","sources":["../../src/checks/no-placeholders.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mkdtemp, rm, writeFile, mkdir } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { checkNoPlaceholders } from './no-placeholders.js';
6
+ let dir;
7
+ beforeEach(async () => {
8
+ dir = await mkdtemp(join(tmpdir(), 'fas-compliance-'));
9
+ });
10
+ afterEach(async () => {
11
+ await rm(dir, { recursive: true, force: true });
12
+ });
13
+ describe('checkNoPlaceholders', () => {
14
+ it('passes when no APPNAME anywhere', async () => {
15
+ await writeFile(join(dir, 'README.md'), '# my-app\nNo placeholders here.');
16
+ await writeFile(join(dir, 'package.json'), '{"name":"my-app"}');
17
+ const r = await checkNoPlaceholders(dir);
18
+ expect(r.status).toBe('pass');
19
+ });
20
+ it('fails with file list when APPNAME is left in any text file', async () => {
21
+ await writeFile(join(dir, 'README.md'), '# APPNAME\n');
22
+ await mkdir(join(dir, 'web'));
23
+ await writeFile(join(dir, 'web', 'index.html'), '<title>APPNAME — FreeAppStore</title>');
24
+ const r = await checkNoPlaceholders(dir);
25
+ expect(r.status).toBe('fail');
26
+ expect(r.detail).toMatch(/README\.md/);
27
+ expect(r.detail).toMatch(/index\.html/);
28
+ expect(r.suggestions?.length ?? 0).toBeGreaterThan(0);
29
+ });
30
+ it('skips binary-ish extensions (no false positives on icons etc.)', async () => {
31
+ // .png file with bytes that happen to spell APPNAME — should be skipped
32
+ await writeFile(join(dir, 'icon.png'), Buffer.from('APPNAME'));
33
+ const r = await checkNoPlaceholders(dir);
34
+ expect(r.status).toBe('pass');
35
+ });
36
+ it('caps at 5 reported files when many offend', async () => {
37
+ await mkdir(join(dir, 'web'));
38
+ for (let i = 0; i < 10; i++) {
39
+ await writeFile(join(dir, 'web', `file${i}.ts`), 'const x = "APPNAME";');
40
+ }
41
+ const r = await checkNoPlaceholders(dir);
42
+ expect(r.status).toBe('fail');
43
+ expect(r.detail).toMatch(/5\+/);
44
+ });
45
+ });
46
+ //# sourceMappingURL=no-placeholders.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-placeholders.test.js","sourceRoot":"","sources":["../../src/checks/no-placeholders.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { CheckResult } from '../types.js';
2
+ export declare function checkNoTracking(repoDir: string): Promise<CheckResult>;
3
+ //# 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":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAkB/C,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAgC3E"}
@@ -0,0 +1,48 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { extname } from 'node:path';
3
+ import { walk } from '../lib/walk.js';
4
+ // Same forbidden list the template's compliance.yml enforces — kept in
5
+ // sync because both are downstream consumers of this package long-term.
6
+ const FORBIDDEN = [
7
+ 'google-analytics',
8
+ 'gtag',
9
+ 'amplitude',
10
+ 'mixpanel',
11
+ 'segment',
12
+ 'hotjar',
13
+ 'plausible',
14
+ 'posthog',
15
+ ];
16
+ const SCAN_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.html', '.json']);
17
+ export async function checkNoTracking(repoDir) {
18
+ const hits = [];
19
+ for await (const file of walk(repoDir)) {
20
+ if (!SCAN_EXTS.has(extname(file).toLowerCase()))
21
+ continue;
22
+ const content = await readFile(file, 'utf8').catch(() => '');
23
+ const matches = FORBIDDEN.filter((sdk) => content.includes(sdk));
24
+ if (matches.length > 0) {
25
+ hits.push({ file: file.replace(repoDir + '/', ''), matches });
26
+ }
27
+ }
28
+ if (hits.length === 0) {
29
+ return {
30
+ name: 'No tracking SDKs',
31
+ status: 'pass',
32
+ detail: `scanned for ${FORBIDDEN.length} known trackers`,
33
+ };
34
+ }
35
+ return {
36
+ name: 'No tracking SDKs',
37
+ status: 'fail',
38
+ detail: `${hits.length} file(s) reference trackers: ${hits
39
+ .slice(0, 3)
40
+ .map((h) => `${h.file} (${h.matches.join(', ')})`)
41
+ .join('; ')}${hits.length > 3 ? '…' : ''}`,
42
+ suggestions: [
43
+ 'FreeAppStore apps must be tracking-free. Remove the SDK + any analytics calls.',
44
+ 'For private-by-design metrics, CF edge analytics already counts requests anonymously.',
45
+ ],
46
+ };
47
+ }
48
+ //# sourceMappingURL=no-tracking.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-tracking.js","sourceRoot":"","sources":["../../src/checks/no-tracking.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAEtC,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,OAAe;IACnD,MAAM,IAAI,GAA0C,EAAE,CAAC;IAEvD,IAAI,KAAK,EAAE,MAAM,IAAI,IAAI,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACvC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;YAAE,SAAS;QAC1D,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QAC7D,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,CAAC,OAAO,CAAC,OAAO,GAAG,GAAG,EAAE,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;QAChE,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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=no-tracking.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-tracking.test.d.ts","sourceRoot":"","sources":["../../src/checks/no-tracking.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,42 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mkdtemp, rm, writeFile, mkdir } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { checkNoTracking } from './no-tracking.js';
6
+ let dir;
7
+ beforeEach(async () => {
8
+ dir = await mkdtemp(join(tmpdir(), 'fas-compliance-'));
9
+ await mkdir(join(dir, 'web'));
10
+ });
11
+ afterEach(async () => {
12
+ await rm(dir, { recursive: true, force: true });
13
+ });
14
+ describe('checkNoTracking', () => {
15
+ it('passes when no tracking strings anywhere', async () => {
16
+ await writeFile(join(dir, 'web', 'app.ts'), 'export const x = 1;');
17
+ const r = await checkNoTracking(dir);
18
+ expect(r.status).toBe('pass');
19
+ });
20
+ it('fails on google-analytics in source', async () => {
21
+ await writeFile(join(dir, 'web', 'index.html'), '<script src="https://www.google-analytics.com/analytics.js"></script>');
22
+ const r = await checkNoTracking(dir);
23
+ expect(r.status).toBe('fail');
24
+ expect(r.detail).toMatch(/google-analytics/);
25
+ });
26
+ it('fails on plausible / posthog as deps even though those are privacy-respecting', async () => {
27
+ // FreeAppStore policy is *no* analytics, even the privacy-respecting ones.
28
+ await writeFile(join(dir, 'web', 'package.json'), '{"dependencies":{"posthog-js":"1.0.0"}}');
29
+ const r = await checkNoTracking(dir);
30
+ expect(r.status).toBe('fail');
31
+ expect(r.detail).toMatch(/posthog/);
32
+ });
33
+ it('does not scan dist or node_modules (skipped by walk)', async () => {
34
+ await mkdir(join(dir, 'node_modules', 'posthog-js'), { recursive: true });
35
+ await writeFile(join(dir, 'node_modules', 'posthog-js', 'index.js'), '// posthog stuff');
36
+ await mkdir(join(dir, 'dist'));
37
+ await writeFile(join(dir, 'dist', 'bundle.js'), 'amplitude.track("x")');
38
+ const r = await checkNoTracking(dir);
39
+ expect(r.status).toBe('pass');
40
+ });
41
+ });
42
+ //# sourceMappingURL=no-tracking.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-tracking.test.js","sourceRoot":"","sources":["../../src/checks/no-tracking.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import { checkNoPlaceholders } from './checks/no-placeholders.js';
2
+ import { checkNoTracking } from './checks/no-tracking.js';
3
+ import { checkBrandFonts } from './checks/brand-fonts.js';
4
+ import { checkManifest } from './checks/manifest.js';
5
+ import { checkBundleSize } from './checks/bundle-size.js';
6
+ import type { CheckResult } from './types.js';
7
+ export type { CheckStatus, CheckResult } from './types.js';
8
+ export { checkNoPlaceholders, checkNoTracking, checkBrandFonts, checkManifest, checkBundleSize, };
9
+ /**
10
+ * Runs every compliance check against `repoDir` (the root of an app, the
11
+ * directory containing package.json + web/). Returns results in a stable
12
+ * order so callers can render predictable output.
13
+ */
14
+ export declare function runChecks(repoDir: string): Promise<CheckResult[]>;
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC3D,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,aAAa,EACb,eAAe,GAChB,CAAC;AAEF;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAQvE"}
package/dist/index.js ADDED
@@ -0,0 +1,21 @@
1
+ import { checkNoPlaceholders } from './checks/no-placeholders.js';
2
+ import { checkNoTracking } from './checks/no-tracking.js';
3
+ import { checkBrandFonts } from './checks/brand-fonts.js';
4
+ import { checkManifest } from './checks/manifest.js';
5
+ import { checkBundleSize } from './checks/bundle-size.js';
6
+ export { checkNoPlaceholders, checkNoTracking, checkBrandFonts, checkManifest, checkBundleSize, };
7
+ /**
8
+ * Runs every compliance check against `repoDir` (the root of an app, the
9
+ * directory containing package.json + web/). Returns results in a stable
10
+ * order so callers can render predictable output.
11
+ */
12
+ export async function runChecks(repoDir) {
13
+ return Promise.all([
14
+ checkNoPlaceholders(repoDir),
15
+ checkNoTracking(repoDir),
16
+ checkBrandFonts(repoDir),
17
+ checkManifest(repoDir),
18
+ checkBundleSize(repoDir),
19
+ ]);
20
+ }
21
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAI1D,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,aAAa,EACb,eAAe,GAChB,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,OAAe;IAC7C,OAAO,OAAO,CAAC,GAAG,CAAC;QACjB,mBAAmB,CAAC,OAAO,CAAC;QAC5B,eAAe,CAAC,OAAO,CAAC;QACxB,eAAe,CAAC,OAAO,CAAC;QACxB,aAAa,CAAC,OAAO,CAAC;QACtB,eAAe,CAAC,OAAO,CAAC;KACzB,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Recursive file iteration that skips noise directories. Used by every
3
+ * check that needs to scan source files.
4
+ */
5
+ export declare function walk(dir: string): AsyncGenerator<string>;
6
+ //# sourceMappingURL=walk.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"walk.d.ts","sourceRoot":"","sources":["../../src/lib/walk.ts"],"names":[],"mappings":"AAaA;;;GAGG;AACH,wBAAuB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,CAAC,MAAM,CAAC,CAe/D"}
@@ -0,0 +1,35 @@
1
+ import { readdir } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ const SKIP_DIRS = new Set([
4
+ '.git',
5
+ 'node_modules',
6
+ 'dist',
7
+ '.next',
8
+ '.cache',
9
+ '.wrangler',
10
+ '.turbo',
11
+ ]);
12
+ /**
13
+ * Recursive file iteration that skips noise directories. Used by every
14
+ * check that needs to scan source files.
15
+ */
16
+ export async function* walk(dir) {
17
+ let entries;
18
+ try {
19
+ entries = await readdir(dir, { withFileTypes: true });
20
+ }
21
+ catch {
22
+ return; // missing dir is not an error here — caller decides
23
+ }
24
+ for (const entry of entries) {
25
+ if (entry.isDirectory()) {
26
+ if (SKIP_DIRS.has(entry.name))
27
+ continue;
28
+ yield* walk(join(dir, entry.name));
29
+ }
30
+ else if (entry.isFile()) {
31
+ yield join(dir, entry.name);
32
+ }
33
+ }
34
+ }
35
+ //# sourceMappingURL=walk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"walk.js","sourceRoot":"","sources":["../../src/lib/walk.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC;IACxB,MAAM;IACN,cAAc;IACd,MAAM;IACN,OAAO;IACP,QAAQ;IACR,WAAW;IACX,QAAQ;CACT,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,IAAI,CAAC,GAAW;IACrC,IAAI,OAAO,CAAC;IACZ,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IACxD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,oDAAoD;IAC9D,CAAC;IACD,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;YACxB,IAAI,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;gBAAE,SAAS;YACxC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;QACrC,CAAC;aAAM,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;YAC1B,MAAM,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -0,0 +1,12 @@
1
+ export type CheckStatus = 'pass' | 'warn' | 'fail';
2
+ export interface CheckResult {
3
+ /** Short human-readable name. */
4
+ name: string;
5
+ /** pass | warn | fail. fail is a hard gate; warn is informational. */
6
+ status: CheckStatus;
7
+ /** One-line context (file path, count, current value). */
8
+ detail: string;
9
+ /** Optional actionable advice for fixing a fail/warn. */
10
+ suggestions?: string[];
11
+ }
12
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEnD,MAAM,WAAW,WAAW;IAC1B,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,sEAAsE;IACtE,MAAM,EAAE,WAAW,CAAC;IACpB,0DAA0D;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@freeappstore/compliance",
3
+ "version": "0.1.0",
4
+ "description": "Compliance checks for apps published on freeappstore.online — used by the CLI for local feedback and by CI for gating.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "homepage": "https://github.com/freeappstore-online/sdk#readme",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/freeappstore-online/sdk.git",
19
+ "directory": "packages/compliance"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/freeappstore-online/sdk/issues"
23
+ },
24
+ "keywords": [
25
+ "freeappstore",
26
+ "compliance",
27
+ "lint",
28
+ "audit",
29
+ "ci"
30
+ ],
31
+ "files": [
32
+ "dist",
33
+ "README.md"
34
+ ],
35
+ "engines": {
36
+ "node": ">=22"
37
+ },
38
+ "scripts": {
39
+ "build": "tsc",
40
+ "typecheck": "tsc --noEmit"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^22.0.0",
44
+ "typescript": "^5.7.0"
45
+ },
46
+ "sideEffects": false
47
+ }