@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.
- package/README.md +27 -0
- package/dist/checks/brand-fonts.d.ts +9 -0
- package/dist/checks/brand-fonts.d.ts.map +1 -0
- package/dist/checks/brand-fonts.js +43 -0
- package/dist/checks/brand-fonts.js.map +1 -0
- package/dist/checks/brand-fonts.test.d.ts +2 -0
- package/dist/checks/brand-fonts.test.d.ts.map +1 -0
- package/dist/checks/brand-fonts.test.js +45 -0
- package/dist/checks/brand-fonts.test.js.map +1 -0
- package/dist/checks/bundle-size.d.ts +8 -0
- package/dist/checks/bundle-size.d.ts.map +1 -0
- package/dist/checks/bundle-size.js +64 -0
- package/dist/checks/bundle-size.js.map +1 -0
- package/dist/checks/bundle-size.test.d.ts +2 -0
- package/dist/checks/bundle-size.test.d.ts.map +1 -0
- package/dist/checks/bundle-size.test.js +57 -0
- package/dist/checks/bundle-size.test.js.map +1 -0
- package/dist/checks/manifest.d.ts +8 -0
- package/dist/checks/manifest.d.ts.map +1 -0
- package/dist/checks/manifest.js +48 -0
- package/dist/checks/manifest.js.map +1 -0
- package/dist/checks/manifest.test.d.ts +2 -0
- package/dist/checks/manifest.test.d.ts.map +1 -0
- package/dist/checks/manifest.test.js +53 -0
- package/dist/checks/manifest.test.js.map +1 -0
- package/dist/checks/no-placeholders.d.ts +9 -0
- package/dist/checks/no-placeholders.d.ts.map +1 -0
- package/dist/checks/no-placeholders.js +52 -0
- package/dist/checks/no-placeholders.js.map +1 -0
- package/dist/checks/no-placeholders.test.d.ts +2 -0
- package/dist/checks/no-placeholders.test.d.ts.map +1 -0
- package/dist/checks/no-placeholders.test.js +46 -0
- package/dist/checks/no-placeholders.test.js.map +1 -0
- package/dist/checks/no-tracking.d.ts +3 -0
- package/dist/checks/no-tracking.d.ts.map +1 -0
- package/dist/checks/no-tracking.js +48 -0
- package/dist/checks/no-tracking.js.map +1 -0
- package/dist/checks/no-tracking.test.d.ts +2 -0
- package/dist/checks/no-tracking.test.d.ts.map +1 -0
- package/dist/checks/no-tracking.test.js +42 -0
- package/dist/checks/no-tracking.test.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/walk.d.ts +6 -0
- package/dist/lib/walk.d.ts.map +1 -0
- package/dist/lib/walk.js +35 -0
- package/dist/lib/walk.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
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACrE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEnD,IAAI,GAAW,CAAC;AAEhB,UAAU,CAAC,KAAK,IAAI,EAAE;IACpB,GAAG,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,iBAAiB,CAAC,CAAC,CAAC;IACvD,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;AAChC,CAAC,CAAC,CAAC;AACH,SAAS,CAAC,KAAK,IAAI,EAAE;IACnB,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AAClD,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;IAC/B,EAAE,CAAC,0CAA0C,EAAE,KAAK,IAAI,EAAE;QACxD,MAAM,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,QAAQ,CAAC,EAAE,qBAAqB,CAAC,CAAC;QACnE,MAAM,CAAC,GAAG,MAAM,eAAe,CAAC,GAAG,CAAC,CAAC;QACrC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,KAAK,IAAI,EAAE;QACnD,MAAM,SAAS,CACb,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,YAAY,CAAC,EAC9B,uEAAuE,CACxE,CAAC;QACF,MAAM,CAAC,GAAG,MAAM,eAAe,CAAC,GAAG,CAAC,CAAC;QACrC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC9B,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+EAA+E,EAAE,KAAK,IAAI,EAAE;QAC7F,2EAA2E;QAC3E,MAAM,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,cAAc,CAAC,EAAE,yCAAyC,CAAC,CAAC;QAC7F,MAAM,CAAC,GAAG,MAAM,eAAe,CAAC,GAAG,CAAC,CAAC;QACrC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC9B,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sDAAsD,EAAE,KAAK,IAAI,EAAE;QACpE,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,EAAE,YAAY,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1E,MAAM,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,EAAE,YAAY,EAAE,UAAU,CAAC,EAAE,kBAAkB,CAAC,CAAC;QACzF,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC;QAC/B,MAAM,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,sBAAsB,CAAC,CAAC;QACxE,MAAM,CAAC,GAAG,MAAM,eAAe,CAAC,GAAG,CAAC,CAAC;QACrC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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"}
|
package/dist/lib/walk.js
ADDED
|
@@ -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"}
|
package/dist/types.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
+
}
|