Files
clean-architecture-frontend…/scripts/check-browser-file-storage-boundaries.ts
T

208 lines
5.8 KiB
TypeScript

import { spawnSync } from "node:child_process";
import { readFile, readdir } from "node:fs/promises";
import path from "node:path";
function requiredPnpmCli(): string {
const executable = process.env.npm_execpath;
if (!executable) {
throw new Error(
"check:browser-file-storage-boundaries must run through pnpm",
);
}
return executable;
}
function runEslint(path: string) {
return spawnSync(
process.execPath,
[
requiredPnpmCli(),
"exec",
"eslint",
path,
"--no-ignore",
"--max-warnings=0",
],
{ encoding: "utf8" },
);
}
function runEslintSource(source: string, virtualPath: string) {
return spawnSync(
process.execPath,
[
requiredPnpmCli(),
"exec",
"eslint",
"--stdin",
"--stdin-filename",
virtualPath,
"--no-ignore",
"--max-warnings=0",
],
{ encoding: "utf8", input: source },
);
}
const allowed = runEslint(
"tests/fixtures/browser-file-storage-boundaries/allowed",
);
const forbiddenRoot =
"tests/fixtures/browser-file-storage-boundaries/forbidden";
const forbiddenFiles = (
await readdir(forbiddenRoot, { withFileTypes: true })
)
.filter((entry) => entry.isFile() && /\.tsx?$/u.test(entry.name))
.map((entry) => path.join(forbiddenRoot, entry.name));
const forbiddenResults = forbiddenFiles.map((file) => ({
file,
result: runEslint(file),
}));
const acceptedForbidden = forbiddenResults.filter(
({ result }) => result.status === 0,
);
const indexedDbBypassSource = await readFile(
path.join(forbiddenRoot, "direct-indexeddb.ts"),
"utf8",
);
const objectUrlBypassSource = await readFile(
path.join(forbiddenRoot, "object-url.ts"),
"utf8",
);
const blobBypassSource = await readFile(
path.join(forbiddenRoot, "direct-blob.ts"),
"utf8",
);
const crossContextAdapterSource = await readFile(
"tests/fixtures/browser-file-storage-boundaries/allowed/cross-context-host.ts",
"utf8",
);
const browserStorageAdapterSource = await readFile(
"tests/fixtures/browser-file-storage-boundaries/allowed/browser-storage-host.ts",
"utf8",
);
const protectedSourceFixtureNames = [
"aliased-globalthis.ts",
"class-field-alias.ts",
"constructor-root-escape.ts",
"default-parameter-alias.ts",
"direct-blob.ts",
"dynamic-capability-key.ts",
"global-object-container.ts",
"identity-wrapped-global.ts",
"instance-property-alias.ts",
"property-descriptor-access.ts",
] as const;
const protectedSourceFixtures = await Promise.all(
protectedSourceFixtureNames.map(async (file) => ({
file,
source: await readFile(path.join(forbiddenRoot, file), "utf8"),
})),
);
const approvedAdapterResults = [
{
file: "src/adapters/storage/indexeddb/boundary-fixture.ts",
result: runEslintSource(
indexedDbBypassSource,
"src/adapters/storage/indexeddb/boundary-fixture.ts",
),
},
{
file: "src/adapters/cross-context-invalidation/boundary-fixture.ts",
result: runEslintSource(
crossContextAdapterSource,
"src/adapters/cross-context-invalidation/boundary-fixture.ts",
),
},
{
file: "src/adapters/storage/boundary-fixture.ts",
result: runEslintSource(
browserStorageAdapterSource,
"src/adapters/storage/boundary-fixture.ts",
),
},
{
file: "src/adapters/browser-transfer/boundary-fixture.ts",
result: runEslintSource(
blobBypassSource,
"src/adapters/browser-transfer/boundary-fixture.ts",
),
},
];
const rejectedApprovedAdapters = approvedAdapterResults.filter(
({ result }) => result.status !== 0,
);
const misplacedAdapterResults = [
{
file: "src/adapters/http/boundary-fixture.ts",
result: runEslintSource(
indexedDbBypassSource,
"src/adapters/http/boundary-fixture.ts",
),
},
{
file: "src/features/reference-feature/adapters/boundary-fixture.ts",
result: runEslintSource(
objectUrlBypassSource,
"src/features/reference-feature/adapters/boundary-fixture.ts",
),
},
{
file: "src/adapters/http/cross-context-boundary-fixture.ts",
result: runEslintSource(
crossContextAdapterSource,
"src/adapters/http/cross-context-boundary-fixture.ts",
),
},
{
file: "src/adapters/http/browser-storage-boundary-fixture.ts",
result: runEslintSource(
browserStorageAdapterSource,
"src/adapters/http/browser-storage-boundary-fixture.ts",
),
},
...protectedSourceFixtures.map(({ file, source }) => {
const virtualPath = `src/presentation/${file}`;
return {
file: virtualPath,
result: runEslintSource(source, virtualPath),
};
}),
];
const acceptedMisplacedAdapters = misplacedAdapterResults.filter(
({ result }) => result.status === 0,
);
if (
allowed.status !== 0 ||
rejectedApprovedAdapters.length > 0 ||
forbiddenFiles.length === 0 ||
acceptedForbidden.length > 0 ||
acceptedMisplacedAdapters.length > 0
) {
process.stderr.write(allowed.stderr || allowed.stdout);
for (const { file, result } of rejectedApprovedAdapters) {
process.stderr.write(
`${file}: owned browser capability adapter was rejected\n`,
);
process.stderr.write(result.stderr || result.stdout);
}
for (const { file, result } of acceptedForbidden) {
process.stderr.write(
`${file}: forbidden browser API fixture was accepted\n`,
);
process.stderr.write(result.stderr || result.stdout);
}
for (const { file, result } of acceptedMisplacedAdapters) {
process.stderr.write(
`${file}: native browser storage access outside its owned adapter was accepted\n`,
);
process.stderr.write(result.stderr || result.stdout);
}
process.exit(1);
}
process.stdout.write(
`Browser file/storage boundaries: PASS (owned adapter allowed, ${forbiddenFiles.length + misplacedAdapterResults.length} direct or misplaced native access cases rejected)\n`,
);