@rexezuge/tooling 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +297 -2
- package/dist/eslint.d.ts +120 -0
- package/dist/eslint.d.ts.map +1 -0
- package/dist/eslint.js +551 -0
- package/dist/eslint.js.map +1 -0
- package/dist/functions/pages-proxy.d.ts +113 -0
- package/dist/functions/pages-proxy.d.ts.map +1 -0
- package/dist/functions/pages-proxy.js +131 -0
- package/dist/functions/pages-proxy.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/scripts/backup/d1-target.d.ts +35 -0
- package/dist/scripts/backup/d1-target.d.ts.map +1 -0
- package/dist/scripts/backup/d1-target.js +31 -0
- package/dist/scripts/backup/d1-target.js.map +1 -0
- package/dist/scripts/backup/destination-config.d.ts +61 -0
- package/dist/scripts/backup/destination-config.d.ts.map +1 -0
- package/dist/scripts/backup/destination-config.js +57 -0
- package/dist/scripts/backup/destination-config.js.map +1 -0
- package/dist/scripts/backup/encrypt-backup.d.ts +35 -0
- package/dist/scripts/backup/encrypt-backup.d.ts.map +1 -0
- package/dist/scripts/backup/encrypt-backup.js +98 -0
- package/dist/scripts/backup/encrypt-backup.js.map +1 -0
- package/dist/scripts/backup/naming.d.ts +44 -0
- package/dist/scripts/backup/naming.d.ts.map +1 -0
- package/dist/scripts/backup/naming.js +58 -0
- package/dist/scripts/backup/naming.js.map +1 -0
- package/dist/scripts/check-god-files.d.ts +164 -0
- package/dist/scripts/check-god-files.d.ts.map +1 -0
- package/dist/scripts/check-god-files.js +271 -0
- package/dist/scripts/check-god-files.js.map +1 -0
- package/dist/scripts/ensure-spa-shell-stub.d.ts +3 -0
- package/dist/scripts/ensure-spa-shell-stub.d.ts.map +1 -0
- package/dist/scripts/ensure-spa-shell-stub.js +30 -0
- package/dist/scripts/ensure-spa-shell-stub.js.map +1 -0
- package/dist/scripts/init-secrets.d.ts +63 -0
- package/dist/scripts/init-secrets.d.ts.map +1 -0
- package/dist/scripts/init-secrets.js +240 -0
- package/dist/scripts/init-secrets.js.map +1 -0
- package/dist/scripts/lib/cli-args.d.ts +78 -0
- package/dist/scripts/lib/cli-args.d.ts.map +1 -0
- package/dist/scripts/lib/cli-args.js +116 -0
- package/dist/scripts/lib/cli-args.js.map +1 -0
- package/dist/scripts/lib/github-actions.d.ts +26 -0
- package/dist/scripts/lib/github-actions.d.ts.map +1 -0
- package/dist/scripts/lib/github-actions.js +38 -0
- package/dist/scripts/lib/github-actions.js.map +1 -0
- package/dist/scripts/lib/wrangler-table.d.ts +46 -0
- package/dist/scripts/lib/wrangler-table.d.ts.map +1 -0
- package/dist/scripts/lib/wrangler-table.js +99 -0
- package/dist/scripts/lib/wrangler-table.js.map +1 -0
- package/dist/scripts/migrations-lock.d.ts +3 -0
- package/dist/scripts/migrations-lock.d.ts.map +1 -0
- package/dist/scripts/migrations-lock.js +46 -0
- package/dist/scripts/migrations-lock.js.map +1 -0
- package/dist/scripts/prepare-wrangler-config.d.ts +3 -0
- package/dist/scripts/prepare-wrangler-config.d.ts.map +1 -0
- package/dist/scripts/prepare-wrangler-config.js +50 -0
- package/dist/scripts/prepare-wrangler-config.js.map +1 -0
- package/dist/scripts/spa-shell.d.ts +41 -0
- package/dist/scripts/spa-shell.d.ts.map +1 -0
- package/dist/scripts/spa-shell.js +155 -0
- package/dist/scripts/spa-shell.js.map +1 -0
- package/dist/scripts/validate-locales.d.ts +26 -0
- package/dist/scripts/validate-locales.d.ts.map +1 -0
- package/dist/scripts/validate-locales.js +350 -0
- package/dist/scripts/validate-locales.js.map +1 -0
- package/dist/scripts/verify-migrations.d.ts +62 -0
- package/dist/scripts/verify-migrations.d.ts.map +1 -0
- package/dist/scripts/verify-migrations.js +302 -0
- package/dist/scripts/verify-migrations.js.map +1 -0
- package/dist/scripts/verify-spa-shell.d.ts +3 -0
- package/dist/scripts/verify-spa-shell.d.ts.map +1 -0
- package/dist/scripts/verify-spa-shell.js +53 -0
- package/dist/scripts/verify-spa-shell.js.map +1 -0
- package/dist/scripts/wrangler-config/cli.d.ts +22 -0
- package/dist/scripts/wrangler-config/cli.d.ts.map +1 -0
- package/dist/scripts/wrangler-config/cli.js +51 -0
- package/dist/scripts/wrangler-config/cli.js.map +1 -0
- package/dist/scripts/wrangler-config/patches.d.ts +51 -0
- package/dist/scripts/wrangler-config/patches.d.ts.map +1 -0
- package/dist/scripts/wrangler-config/patches.js +140 -0
- package/dist/scripts/wrangler-config/patches.js.map +1 -0
- package/dist/scripts/wrangler-config/resources.d.ts +70 -0
- package/dist/scripts/wrangler-config/resources.d.ts.map +1 -0
- package/dist/scripts/wrangler-config/resources.js +290 -0
- package/dist/scripts/wrangler-config/resources.js.map +1 -0
- package/dist/scripts/wrangler-config/types.d.ts +103 -0
- package/dist/scripts/wrangler-config/types.d.ts.map +1 -0
- package/dist/scripts/wrangler-config/types.js +49 -0
- package/dist/scripts/wrangler-config/types.js.map +1 -0
- package/dist/test/integration-migrations.d.ts +167 -0
- package/dist/test/integration-migrations.d.ts.map +1 -0
- package/dist/test/integration-migrations.js +171 -0
- package/dist/test/integration-migrations.js.map +1 -0
- package/dist/test/mocks/cloudflare-workers.d.ts +106 -0
- package/dist/test/mocks/cloudflare-workers.d.ts.map +1 -0
- package/dist/test/mocks/cloudflare-workers.js +90 -0
- package/dist/test/mocks/cloudflare-workers.js.map +1 -0
- package/dist/vite.d.ts +117 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +125 -0
- package/dist/vite.js.map +1 -0
- package/dist/vitest-web.d.ts +73 -0
- package/dist/vitest-web.d.ts.map +1 -0
- package/dist/vitest-web.js +72 -0
- package/dist/vitest-web.js.map +1 -0
- package/dist/vitest.d.ts +92 -0
- package/dist/vitest.d.ts.map +1 -0
- package/dist/vitest.js +128 -0
- package/dist/vitest.js.map +1 -0
- package/package.json +58 -3
- package/src/eslint.test.ts +175 -0
- package/src/eslint.ts +640 -0
- package/src/functions/pages-proxy.test.ts +72 -0
- package/src/functions/pages-proxy.ts +187 -0
- package/src/github/actions/retry-step/action.yml +39 -0
- package/src/github/actions/setup-env/action.yml +20 -0
- package/src/github/dependabot.yml +30 -0
- package/src/github/workflows/backup-main.yml +46 -0
- package/src/github/workflows/continuous-deployment.yml +188 -0
- package/src/github/workflows/continuous-integration.yml +259 -0
- package/src/github/workflows/scheduled-version-update.yml +38 -0
- package/src/github/workflows/upstream-sync.yml +56 -0
- package/src/index.ts +42 -0
- package/src/scripts/backup/backup-rules.test.ts +105 -0
- package/src/scripts/backup/d1-target.ts +54 -0
- package/src/scripts/backup/destination-config.ts +92 -0
- package/src/scripts/backup/encrypt-backup.ts +107 -0
- package/src/scripts/backup/naming.ts +62 -0
- package/src/scripts/check-god-files.test.ts +131 -0
- package/src/scripts/check-god-files.ts +327 -0
- package/src/scripts/ensure-spa-shell-stub.ts +34 -0
- package/src/scripts/init-secrets.ts +265 -0
- package/src/scripts/lib/cli-args.ts +154 -0
- package/src/scripts/lib/github-actions.ts +41 -0
- package/src/scripts/lib/wrangler-table.ts +105 -0
- package/src/scripts/migrations-lock.ts +52 -0
- package/src/scripts/prepare-wrangler-config.ts +51 -0
- package/src/scripts/spa-shell.test.ts +91 -0
- package/src/scripts/spa-shell.ts +179 -0
- package/src/scripts/validate-locales.test.ts +89 -0
- package/src/scripts/validate-locales.ts +380 -0
- package/src/scripts/verify-migrations.test.ts +71 -0
- package/src/scripts/verify-migrations.ts +364 -0
- package/src/scripts/verify-spa-shell.ts +56 -0
- package/src/scripts/wrangler-config/cli.ts +51 -0
- package/src/scripts/wrangler-config/patches.ts +157 -0
- package/src/scripts/wrangler-config/resources.ts +330 -0
- package/src/scripts/wrangler-config/types.ts +113 -0
- package/src/test/integration-migrations.test.ts +169 -0
- package/src/test/integration-migrations.ts +267 -0
- package/src/test/mocks/cloudflare-workers.ts +115 -0
- package/src/vite.test.ts +83 -0
- package/src/vite.ts +202 -0
- package/src/vitest-web.ts +109 -0
- package/src/vitest.ts +185 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { ensureSpaShellStub, spaShellCli, verifySpaShell } from './spa-shell';
|
|
6
|
+
|
|
7
|
+
function tempTree(): string {
|
|
8
|
+
return mkdtempSync(join(tmpdir(), 'spa-shell-'));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function writeBuild(root: string, html: string): void {
|
|
12
|
+
const shell = join(root, 'apps/api/src/generated');
|
|
13
|
+
mkdirSync(shell, { recursive: true });
|
|
14
|
+
writeFileSync(join(shell, 'spa-shell.ts'), `export const SPA_HTML: string = ${JSON.stringify(html)};\n`, 'utf8');
|
|
15
|
+
const distAssets = join(root, 'apps/web/dist/assets');
|
|
16
|
+
mkdirSync(distAssets, { recursive: true });
|
|
17
|
+
writeFileSync(join(root, 'apps/web/dist/index.html'), html, 'utf8');
|
|
18
|
+
for (const asset of html.matchAll(/\/assets\/([^"]+)/g)) {
|
|
19
|
+
writeFileSync(join(distAssets, asset[1] as string), 'console.log(1)', 'utf8');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const REAL_HTML = `<!doctype html><html><head><meta charset="utf-8" /><title>Spa Shell Fixture</title><link rel="stylesheet" href="/assets/index-def456.css" /><script type="module" src="/assets/index-abc123.js"></script></head><body><div id="root"></div><!-- fixture padding so the shell exceeds the 200-char minimum --></body></html>`;
|
|
24
|
+
|
|
25
|
+
describe('ensureSpaShellStub', () => {
|
|
26
|
+
it('creates the stub when absent', () => {
|
|
27
|
+
const root = tempTree();
|
|
28
|
+
const result = ensureSpaShellStub({ root });
|
|
29
|
+
expect(result.created).toBe(true);
|
|
30
|
+
expect(existsSync(join(root, 'apps/api/src/generated/spa-shell.ts'))).toBe(true);
|
|
31
|
+
expect(readFileSync(result.path, 'utf8')).toContain("SPA_HTML: string = ''");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('never overwrites an existing file', () => {
|
|
35
|
+
const root = tempTree();
|
|
36
|
+
const path = join(root, 'apps/api/src/generated', 'spa-shell.ts');
|
|
37
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
38
|
+
writeFileSync(path, 'real content', 'utf8');
|
|
39
|
+
const result = ensureSpaShellStub({ root });
|
|
40
|
+
expect(result.created).toBe(false);
|
|
41
|
+
expect(readFileSync(path, 'utf8')).toBe('real content');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('verifySpaShell', () => {
|
|
46
|
+
it('passes a self-consistent build', () => {
|
|
47
|
+
const root = tempTree();
|
|
48
|
+
writeBuild(root, REAL_HTML);
|
|
49
|
+
expect(verifySpaShell({ root }).problems).toStrictEqual([]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('reports a missing shell', () => {
|
|
53
|
+
const root = tempTree();
|
|
54
|
+
const { problems } = verifySpaShell({ root });
|
|
55
|
+
expect(problems.join('\n')).toContain('is missing');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('reports the postinstall stub', () => {
|
|
59
|
+
const root = tempTree();
|
|
60
|
+
const path = join(root, 'apps/api/src/generated', 'spa-shell.ts');
|
|
61
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
62
|
+
writeFileSync(path, "export const SPA_HTML: string = '';\n", 'utf8');
|
|
63
|
+
expect(verifySpaShell({ root }).problems.join('\n')).toContain('postinstall stub');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('reports referenced-but-missing assets', () => {
|
|
67
|
+
const root = tempTree();
|
|
68
|
+
writeBuild(root, REAL_HTML);
|
|
69
|
+
rmSync(join(root, 'apps/web/dist/assets'), { recursive: true, force: true });
|
|
70
|
+
expect(verifySpaShell({ root }).problems.join('\n')).toContain('/assets/index-abc123.js');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('reports a shell that disagrees with dist/index.html', () => {
|
|
74
|
+
const root = tempTree();
|
|
75
|
+
writeBuild(root, REAL_HTML);
|
|
76
|
+
writeFileSync(join(root, 'apps/web/dist/index.html'), '<!doctype html><html>different</html>', 'utf8');
|
|
77
|
+
expect(verifySpaShell({ root }).problems.join('\n')).toContain('does not match');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('spaShellCli', () => {
|
|
82
|
+
it('verifies and reports per mode', () => {
|
|
83
|
+
const root = tempTree();
|
|
84
|
+
expect(spaShellCli(['ensure', root])).toBe(0);
|
|
85
|
+
expect(spaShellCli(['ensure', root])).toBe(0);
|
|
86
|
+
expect(spaShellCli(['verify', root])).toBe(1); // still the stub
|
|
87
|
+
writeBuild(root, REAL_HTML);
|
|
88
|
+
expect(spaShellCli(['verify', root])).toBe(0);
|
|
89
|
+
expect(spaShellCli(['bogus', root])).toBe(1);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SPA shell pair: `ensure-spa-shell-stub` + `verify-spa-shell`.
|
|
3
|
+
*
|
|
4
|
+
* Provenance: the `scripts/ensure-spa-shell-stub.mjs` +
|
|
5
|
+
* `scripts/verify-spa-shell.mjs` pair (Durable-DAV) and Edge-Sonic's typed
|
|
6
|
+
* `scripts/build/{ensure-spa-shell-stub,spa-shell-checks,verify-spa-shell}.ts`.
|
|
7
|
+
* The generate step itself is a Vite plugin (`spaShellEmbedPlugin` in
|
|
8
|
+
* `vite.ts`) because it must run on every build; this module holds the
|
|
9
|
+
* freshness contract around the generated file.
|
|
10
|
+
*
|
|
11
|
+
* Why the pair exists: `apps/api/src/generated/spa-shell.ts` and
|
|
12
|
+
* `apps/web/dist/` are both gitignored build artifacts. A fresh clone gets the
|
|
13
|
+
* empty postinstall stub and every browser navigation returns a blank page,
|
|
14
|
+
* with nothing in the source tree saying so.
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { pathToFileURL } from 'node:url';
|
|
18
|
+
import { dirname, join, resolve } from 'node:path';
|
|
19
|
+
|
|
20
|
+
interface SpaShellPaths {
|
|
21
|
+
/** Repo root. */
|
|
22
|
+
root: string;
|
|
23
|
+
/** Generated TS module path, relative to root. */
|
|
24
|
+
shellPath?: string;
|
|
25
|
+
/** Built SPA index.html path, relative to root. */
|
|
26
|
+
distIndexPath?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface VerifyResult {
|
|
30
|
+
problems: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const DEFAULT_SHELL = 'apps/api/src/generated/spa-shell.ts';
|
|
34
|
+
const DEFAULT_DIST_INDEX = 'apps/web/dist/index.html';
|
|
35
|
+
const STUB_CONTENT =
|
|
36
|
+
"// Auto-generated stub - real content is produced by the build step via the Vite plugin.\nexport const SPA_HTML: string = '';\n";
|
|
37
|
+
|
|
38
|
+
/** A built shell is a full Vite index.html; any small threshold rejects the stub. */
|
|
39
|
+
const MIN_HTML_CHARS = 200;
|
|
40
|
+
|
|
41
|
+
function resolvePaths(options: SpaShellPaths): { shell: string; distIndex: string; distAssets: string } {
|
|
42
|
+
const root = resolve(options.root);
|
|
43
|
+
const shell = join(root, options.shellPath ?? DEFAULT_SHELL);
|
|
44
|
+
const distIndex = join(root, options.distIndexPath ?? DEFAULT_DIST_INDEX);
|
|
45
|
+
return { shell, distIndex, distAssets: join(dirname(distIndex), 'assets') };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create the empty stub when (and only when) nothing is there yet, so a fresh
|
|
50
|
+
* clone typechecks and `pnpm run build` regenerates the real thing. Never
|
|
51
|
+
* overwrites an existing file — overwriting would erase a real build with a
|
|
52
|
+
* stub on the next `pnpm install`.
|
|
53
|
+
*/
|
|
54
|
+
function ensureSpaShellStub(options: SpaShellPaths): { created: boolean; path: string } {
|
|
55
|
+
const { shell } = resolvePaths(options);
|
|
56
|
+
if (existsSync(shell)) {
|
|
57
|
+
return { created: false, path: shell };
|
|
58
|
+
}
|
|
59
|
+
mkdirSync(dirname(shell), { recursive: true });
|
|
60
|
+
writeFileSync(shell, STUB_CONTENT, 'utf8');
|
|
61
|
+
return { created: true, path: shell };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The `SPA_HTML` string literal, decoded from the generated TS module. */
|
|
65
|
+
function readEmbeddedHtml(shell: string, problems: string[]): string | null {
|
|
66
|
+
if (!existsSync(shell)) {
|
|
67
|
+
problems.push(
|
|
68
|
+
`${shell} is missing — run the build (only apps/web has a build step).`,
|
|
69
|
+
);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const source = readFileSync(shell, 'utf8');
|
|
73
|
+
const marker = 'export const SPA_HTML: string = ';
|
|
74
|
+
const at = source.indexOf(marker);
|
|
75
|
+
if (at === -1) {
|
|
76
|
+
problems.push(`${shell} does not export SPA_HTML — regenerate it with the build.`);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const literal = source.slice(at + marker.length).replace(/;\s*$/, '');
|
|
80
|
+
if (literal === "''" || literal === '""') {
|
|
81
|
+
problems.push(
|
|
82
|
+
`${shell} still holds the empty postinstall stub — run the build. Until then every browser navigation served by the API worker is a blank page.`,
|
|
83
|
+
);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
let html: unknown;
|
|
87
|
+
try {
|
|
88
|
+
html = JSON.parse(literal);
|
|
89
|
+
} catch {
|
|
90
|
+
problems.push(`${shell} is not the generated shape — delete it and run the build.`);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
if (typeof html !== 'string' || html.length < MIN_HTML_CHARS) {
|
|
94
|
+
problems.push(`${shell} holds no usable HTML — run the build. Until then every browser navigation is a blank page.`);
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return html;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Fail when the shell the API worker serves is missing or self-inconsistent:
|
|
102
|
+
*
|
|
103
|
+
* - the postinstall stub (or no build at all) still in place;
|
|
104
|
+
* - `/assets/...` bundles referenced but not emitted (a half-cleaned dist,
|
|
105
|
+
* which serves a shell whose JS 404s);
|
|
106
|
+
* - `spa-shell.ts` and `dist/index.html` disagreeing — only one half of the
|
|
107
|
+
* build was refreshed.
|
|
108
|
+
*
|
|
109
|
+
* Known limit, stated rather than papered over: a *stale but self-consistent*
|
|
110
|
+
* pair passes. That class is covered at the source level by each repo's SPA
|
|
111
|
+
* parser integration test; the remaining duty is operational — run the build
|
|
112
|
+
* after touching `apps/web`, before deploy.
|
|
113
|
+
*/
|
|
114
|
+
function verifySpaShell(options: SpaShellPaths): VerifyResult {
|
|
115
|
+
const { shell, distIndex, distAssets } = resolvePaths(options);
|
|
116
|
+
const problems: string[] = [];
|
|
117
|
+
const html = readEmbeddedHtml(shell, problems);
|
|
118
|
+
if (html === null) {
|
|
119
|
+
return { problems };
|
|
120
|
+
}
|
|
121
|
+
const referenced = [...html.matchAll(/(?:src|href)="\/assets\/([^"]+)"/g)].map((match) => match[1] as string);
|
|
122
|
+
if (referenced.length === 0) {
|
|
123
|
+
problems.push('The embedded shell references no /assets/ bundles — it is not a real build output.');
|
|
124
|
+
}
|
|
125
|
+
for (const asset of new Set(referenced)) {
|
|
126
|
+
if (!existsSync(join(distAssets, asset))) {
|
|
127
|
+
problems.push(`The embedded shell references /assets/${asset}, which is not in ${distAssets} — run the build.`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (!existsSync(distIndex)) {
|
|
131
|
+
problems.push(`${distIndex} is missing — run the build.`);
|
|
132
|
+
} else if (readFileSync(distIndex, 'utf8') !== html) {
|
|
133
|
+
problems.push(`${shell} does not match ${distIndex} — run the build.`);
|
|
134
|
+
}
|
|
135
|
+
return { problems };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const CLI_SHAPES = 'verify|ensure';
|
|
139
|
+
|
|
140
|
+
/** CLI entry: `spaShellCli(['verify'|'ensure', 'repo-root?'])`. */
|
|
141
|
+
function spaShellCli(argv: readonly string[] = process.argv.slice(2)): number {
|
|
142
|
+
const mode = argv[0] ?? 'verify';
|
|
143
|
+
if (!CLI_SHAPES.split('|').includes(mode)) {
|
|
144
|
+
console.error(`Usage: tsx spa-shell.ts <${CLI_SHAPES}> [repo-root]`);
|
|
145
|
+
return 1;
|
|
146
|
+
}
|
|
147
|
+
const options: SpaShellPaths = { root: argv[1] ?? process.cwd() };
|
|
148
|
+
if (mode === 'ensure') {
|
|
149
|
+
const result = ensureSpaShellStub(options);
|
|
150
|
+
if (result.created) {
|
|
151
|
+
console.log(`ensure-spa-shell-stub: created stub at ${result.path}`);
|
|
152
|
+
} else {
|
|
153
|
+
console.log(`ensure-spa-shell-stub: ${result.path} already present, left untouched`);
|
|
154
|
+
}
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
const { problems } = verifySpaShell(options);
|
|
158
|
+
if (problems.length > 0) {
|
|
159
|
+
console.error('SPA shell check failed:');
|
|
160
|
+
for (const problem of problems) console.error(` - ${problem}`);
|
|
161
|
+
return 1;
|
|
162
|
+
}
|
|
163
|
+
console.log('SPA shell check passed.');
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export { ensureSpaShellStub, spaShellCli, verifySpaShell };
|
|
168
|
+
export type { SpaShellPaths, VerifyResult };
|
|
169
|
+
|
|
170
|
+
// Run only when this file is the entrypoint.
|
|
171
|
+
//
|
|
172
|
+
// The rules above are pure and the tests import them directly, so a module-level
|
|
173
|
+
// call would shell out (and exit) during a test run. The URL comparison rather than
|
|
174
|
+
// an unconditional call is the same guard Durable-DAV's init-secrets.ts uses, and
|
|
175
|
+
// it is what keeps the rule modules importable — which is the whole reason they are
|
|
176
|
+
// separate modules in the first place.
|
|
177
|
+
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
178
|
+
process.exit(spaShellCli(process.argv.slice(2)));
|
|
179
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { parseObjectLiteral, validateLocales, validateLocalesCli } from './validate-locales';
|
|
6
|
+
|
|
7
|
+
function writeBundles(root: string, locale: string, bundle: Record<string, unknown>): string {
|
|
8
|
+
const dir = join(root, locale);
|
|
9
|
+
mkdirSync(dir, { recursive: true });
|
|
10
|
+
writeFileSync(join(dir, 'translation.json'), JSON.stringify(bundle), 'utf8');
|
|
11
|
+
return root;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const EN = { common: { unauthorized: 'Unauthorized', forbidden: 'Forbidden' } };
|
|
15
|
+
|
|
16
|
+
describe('validateLocales', () => {
|
|
17
|
+
it('passes when every locale mirrors the base keys', () => {
|
|
18
|
+
const root = mkdtempSync(join(tmpdir(), 'locales-'));
|
|
19
|
+
writeBundles(root, 'en', EN);
|
|
20
|
+
writeBundles(root, 'de', { common: { unauthorized: 'Nicht autorisiert', forbidden: 'Verboten' } });
|
|
21
|
+
const report = validateLocales(root);
|
|
22
|
+
expect(report.locales).toStrictEqual(['de', 'en']);
|
|
23
|
+
expect(report.keys).toBe(2);
|
|
24
|
+
expect(report.problems).toStrictEqual([]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('reports both missing and extra keys', () => {
|
|
28
|
+
const root = mkdtempSync(join(tmpdir(), 'locales-'));
|
|
29
|
+
writeBundles(root, 'en', EN);
|
|
30
|
+
writeBundles(root, 'de', { common: { unauthorized: 'x', oops: 'y' } });
|
|
31
|
+
const problems = validateLocales(root).problems;
|
|
32
|
+
expect(problems).toStrictEqual([
|
|
33
|
+
{ locale: 'de', problem: 'missing key: common.forbidden' },
|
|
34
|
+
{ locale: 'de', problem: 'extra key not in en: common.oops' },
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('reports a missing directory and a missing base locale', () => {
|
|
39
|
+
expect(validateLocales('/nonexistent-xyz').problems.length).toBeGreaterThan(0);
|
|
40
|
+
const root = mkdtempSync(join(tmpdir(), 'locales-'));
|
|
41
|
+
writeBundles(root, 'de', EN);
|
|
42
|
+
expect(validateLocales(root).problems[0]?.problem).toContain('base locale');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('supports flat <locale>.json bundles', () => {
|
|
46
|
+
const root = mkdtempSync(join(tmpdir(), 'locales-'));
|
|
47
|
+
writeFileSync(join(root, 'en.json'), JSON.stringify(EN), 'utf8');
|
|
48
|
+
writeFileSync(join(root, 'de.json'), JSON.stringify({ common: { unauthorized: 'x', forbidden: 'y' } }), 'utf8');
|
|
49
|
+
expect(validateLocales(root).problems).toStrictEqual([]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('CLI returns 1 with problems and 0 without', () => {
|
|
53
|
+
const root = mkdtempSync(join(tmpdir(), 'locales-'));
|
|
54
|
+
writeBundles(root, 'en', EN);
|
|
55
|
+
writeBundles(root, 'de', { common: { unauthorized: 'x' } });
|
|
56
|
+
expect(validateLocalesCli([root])).toBe(1);
|
|
57
|
+
writeFileSync(join(root, 'de', 'translation.json'), JSON.stringify(EN), 'utf8');
|
|
58
|
+
expect(validateLocalesCli([root])).toBe(0);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('reads the module shape the kit i18n bundles use', () => {
|
|
62
|
+
// `@rexezuge/i18n` ships `const enStrings: BackendStrings = { … }` per file, so
|
|
63
|
+
// the same gate covers the backend catalogs and the SPA JSON bundles.
|
|
64
|
+
const root = mkdtempSync(join(tmpdir(), 'locales-'));
|
|
65
|
+
writeFileSync(join(root, 'en.ts'), "import type { BackendStrings } from '../BackendStrings';\n\nconst enStrings: BackendStrings = {\n common: {\n unauthorized: 'Authentication Required.',\n forbidden: 'Access Denied.',\n },\n};\n\nexport { enStrings };\n", 'utf8');
|
|
66
|
+
writeFileSync(join(root, 'de.ts'), "const deStrings = {\n common: {\n unauthorized: 'Authentifizierung Erforderlich.',\n forbidden: 'Zugriff Verweigert.',\n },\n};\n", 'utf8');
|
|
67
|
+
expect(validateLocales(root).problems).toStrictEqual([]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('extracts an object literal, ignoring the type annotation and the import', () => {
|
|
71
|
+
const source = [
|
|
72
|
+
"import type { BackendStrings } from '../BackendStrings';",
|
|
73
|
+
'',
|
|
74
|
+
'const enStrings: BackendStrings = {',
|
|
75
|
+
" common: { unauthorized: 'It\\'s fine', },",
|
|
76
|
+
'};',
|
|
77
|
+
'',
|
|
78
|
+
'export { enStrings };',
|
|
79
|
+
].join('\n');
|
|
80
|
+
expect(parseObjectLiteral(source, 'en.ts')).toStrictEqual({ common: { unauthorized: "It's fine" } });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('reports an unparseable module rather than passing it silently', () => {
|
|
84
|
+
const root = mkdtempSync(join(tmpdir(), 'locales-'));
|
|
85
|
+
writeFileSync(join(root, 'en.ts'), 'const enStrings = {};\n', 'utf8');
|
|
86
|
+
const problems = validateLocales(root).problems;
|
|
87
|
+
expect(problems[0]?.problem).toMatch(/no keys|object literal/);
|
|
88
|
+
});
|
|
89
|
+
});
|