137 lines
4.5 KiB
JavaScript
137 lines
4.5 KiB
JavaScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import assert from "node:assert/strict";
|
|
import { afterEach, describe, it } from "vitest";
|
|
|
|
import {
|
|
formatDbLegacyMarkupResult,
|
|
listDbLegacyFiles,
|
|
verifyDbLegacyMarkup,
|
|
} from "./verify-db-legacy-markup.mjs";
|
|
|
|
const fixtures = [];
|
|
|
|
function writeFile(rootDir, relativePath, content) {
|
|
const absolutePath = path.join(rootDir, relativePath);
|
|
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
fs.writeFileSync(absolutePath, content);
|
|
}
|
|
|
|
function createFixture(files = {}) {
|
|
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "db-legacy-markup-"));
|
|
fixtures.push(rootDir);
|
|
|
|
writeFile(
|
|
rootDir,
|
|
"apps/web/app/utils/db/users.db.server.ts",
|
|
files["apps/web/app/utils/db/users.db.server.ts"] ?? `type Payload = Promise<{ id: string; ok: boolean }>;
|
|
|
|
export async function listUsers(): Payload {
|
|
return { id: "user_1", ok: true };
|
|
}
|
|
`,
|
|
);
|
|
|
|
for (const [relativePath, content] of Object.entries(files)) {
|
|
writeFile(rootDir, relativePath, content);
|
|
}
|
|
|
|
return rootDir;
|
|
}
|
|
|
|
afterEach(() => {
|
|
while (fixtures.length > 0) {
|
|
fs.rmSync(fixtures.pop(), { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe("verify-db-legacy-markup", () => {
|
|
it("lists DB legacy TypeScript files recursively", () => {
|
|
const rootDir = createFixture({
|
|
"apps/web/app/utils/db/entities/rows.db.server.ts": "export const rows = [];\n",
|
|
"apps/web/app/utils/db/entities/notes.md": "ignored\n",
|
|
});
|
|
|
|
assert.deepEqual(listDbLegacyFiles(rootDir), [
|
|
"apps/web/app/utils/db/entities/rows.db.server.ts",
|
|
"apps/web/app/utils/db/users.db.server.ts",
|
|
]);
|
|
});
|
|
|
|
it("passes clean DB files and generic object type annotations", () => {
|
|
const rootDir = createFixture({
|
|
"apps/web/app/utils/db/users.db.server.ts": `type Payload = Promise<{ id: string; ok: boolean }>;
|
|
type Selection<T extends { id: string }> = Pick<T, "id">;
|
|
type Lookup = Record<string, Array<Partial<{ value: number }>>>;
|
|
|
|
export async function listUsers<T extends { id: string }>(rows: T[]): Promise<Selection<T>[]> {
|
|
return rows.map((row) => ({ id: row.id }));
|
|
}
|
|
|
|
export function compare(left: number, right: number) {
|
|
return left < right && right > 0;
|
|
}
|
|
`,
|
|
});
|
|
|
|
const result = verifyDbLegacyMarkup(rootDir);
|
|
|
|
assert.deepEqual(result.failures, []);
|
|
assert.equal(result.filesScanned, 1);
|
|
assert.match(formatDbLegacyMarkupResult(result), /PASS DB legacy markup verification/);
|
|
});
|
|
|
|
it("fails when DB files import React or React subpaths", () => {
|
|
const rootDir = createFixture({
|
|
"apps/web/app/utils/db/users.db.server.ts": `import type { ReactNode } from "react";
|
|
import { cache } from "react/cache";
|
|
|
|
export type UserNode = ReactNode;
|
|
export const loader = cache(() => null);
|
|
`,
|
|
});
|
|
|
|
const result = verifyDbLegacyMarkup(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /app\/utils\/db\/users\.db\.server\.ts: forbidden React import/);
|
|
assert.match(result.failures.join("\n"), /app\/utils\/db\/users\.db\.server\.ts: forbidden ReactNode type/);
|
|
});
|
|
|
|
it("fails when a TSX file enters the DB legacy boundary", () => {
|
|
const rootDir = createFixture({
|
|
"apps/web/app/utils/db/render.db.server.tsx": "export function Render() { return <div />; }\n",
|
|
});
|
|
|
|
const result = verifyDbLegacyMarkup(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /app\/utils\/db\/render\.db\.server\.tsx: forbidden \.tsx file/);
|
|
});
|
|
|
|
it("fails on dangerouslySetInnerHTML markers without treating HTML strings as markup", () => {
|
|
const rootDir = createFixture({
|
|
"apps/web/app/utils/db/users.db.server.ts": "export const html = { dangerouslySetInnerHTML: { __html: '<b>x</b>' } };\n",
|
|
});
|
|
|
|
const result = verifyDbLegacyMarkup(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /forbidden dangerouslySetInnerHTML marker/);
|
|
assert.doesNotMatch(result.failures.join("\n"), /forbidden JSX-like markup tag/);
|
|
});
|
|
|
|
it("fails on JSX type annotations and JSX-like markup in TypeScript files", () => {
|
|
const rootDir = createFixture({
|
|
"apps/web/app/utils/db/users.db.server.ts": `export type View = JSX.Element;
|
|
export type Fragment = JSXFragment;
|
|
export const element = <span data-id={row.id} />;
|
|
`,
|
|
});
|
|
|
|
const result = verifyDbLegacyMarkup(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /forbidden JSX\.Element type/);
|
|
assert.match(result.failures.join("\n"), /forbidden JSXFragment type/);
|
|
assert.match(result.failures.join("\n"), /forbidden JSX-like markup tag/);
|
|
});
|
|
});
|