208 lines
5.8 KiB
TypeScript
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`,
|
|
);
|