import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { collectRemovedModuleSourceFiles, findRemovedModuleMatches, renderRemovedModuleScanResult, scanRemovedModules, } from "./scan-removed-modules.ts"; const tempRoots: string[] = []; async function createFixtureRoot() { const root = await mkdtemp(path.join(os.tmpdir(), "removed-modules-")); tempRoots.push(root); return root; } async function writeFixtureFile(root: string, relativePath: string, content: string) { const absolutePath = path.join(root, relativePath); await mkdir(path.dirname(absolutePath), { recursive: true }); await writeFile(absolutePath, content, "utf8"); return absolutePath; } afterEach(async () => { await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true }))); }); describe("removed-module scanner", () => { it("passes for clean runtime roots and reports observability counters", async () => { const root = await createFixtureRoot(); await writeFixtureFile(root, "apps/web/app/root.tsx", "export const Root = () => null;\n"); await writeFixtureFile(root, "apps/worker/index.mjs", "export const worker = true;\n"); await writeFixtureFile(root, "packages/core/manifest.json", "{\"name\":\"core\"}\n"); const result = await scanRemovedModules(root); const output = renderRemovedModuleScanResult(result); expect(result.roots).toEqual(["apps", "packages"]); expect(result.missingRoots).toEqual([]); expect(result.files).toEqual(["apps/web/app/root.tsx", "apps/worker/index.mjs", "packages/core/manifest.json"]); expect(result.diagnostics).toEqual([]); expect(output).toContain("PASS removed-module scan"); expect(output).toContain("files scanned: 3"); expect(output).toContain("matches found: 0"); }); it("detects each forbidden runtime group with path, line, and excerpt", async () => { const root = await createFixtureRoot(); await writeFixtureFile( root, "app/runtime.ts", [ "const mailer = 'postmark';", "const referral = 'Rewardful';", "const block = 'PageBlocks';", "const demo = 'my-site-demo';", ].join("\n"), ); const result = await scanRemovedModules(root, ["app"]); expect(result.diagnostics).toEqual([ { id: "postmark", file: "app/runtime.ts", line: 1, excerpt: "const mailer = 'postmark';" }, { id: "rewardful", file: "app/runtime.ts", line: 2, excerpt: "const referral = 'Rewardful';" }, { id: "pageblocks", file: "app/runtime.ts", line: 3, excerpt: "const block = 'PageBlocks';" }, { id: "site-demo", file: "app/runtime.ts", line: 4, excerpt: "const demo = 'my-site-demo';" }, ]); expect(renderRemovedModuleScanResult(result)).toContain("app/runtime.ts:4: [site-demo] const demo = 'my-site-demo';"); }); it("detects supported site-demo spellings and singular PageBlock", () => { const diagnostics = findRemovedModuleMatches("Site-Demo\nsite demo\ndemo site\nSiteDemo\nPageBlock\n", "app/demo.ts"); expect(diagnostics.map((diagnostic) => diagnostic.id)).toEqual(["site-demo", "site-demo", "site-demo", "site-demo", "pageblocks"]); }); it("ignores dependency, build, generated, cache, TC, optional plugin, test, fixture, story, and mock paths", async () => { const root = await createFixtureRoot(); await writeFixtureFile(root, "app/entry.ts", "export const entry = true;\n"); await writeFixtureFile(root, "app/node_modules/pkg/index.ts", "postmark\n"); await writeFixtureFile(root, "app/build/server.js", "rewardful\n"); await writeFixtureFile(root, "app/.cache/cache.ts", "PageBlocks\n"); await writeFixtureFile(root, "app/.tc/plan.ts", "SiteDemo\n"); await writeFixtureFile(root, "app/generated/client.ts", "postmark\n"); await writeFixtureFile(root, "app/component.test.ts", "rewardful\n"); await writeFixtureFile(root, "app/component.stories.tsx", "PageBlocks\n"); await writeFixtureFile(root, "app/fixtures/data.ts", "demo site\n"); await writeFixtureFile(root, "app/__mocks__/mail.ts", "postmark\n"); await writeFixtureFile(root, "packages/plugins/index.ts", "rewardful\n"); await writeFixtureFile(root, "packages/core/types.d.ts", "declare const SiteDemo: string;\n"); await writeFixtureFile(root, "packages/core/bundle.min.js", "postmark\n"); await writeFixtureFile(root, "packages/core/source.js.map", "rewardful\n"); const result = await scanRemovedModules(root, ["app", "packages"]); expect(result.files).toEqual(["app/entry.ts"]); expect(result.diagnostics).toEqual([]); }); it("records absent roots without failing collection", async () => { const root = await createFixtureRoot(); await writeFixtureFile(root, "packages/core/index.ts", "export const core = true;\n"); const result = await collectRemovedModuleSourceFiles(root, ["app", "apps", "packages"]); expect(result.roots).toEqual(["packages"]); expect(result.missingRoots).toEqual(["app", "apps"]); expect(result.files.map((file) => path.relative(root, file).split(path.sep).join("/"))).toEqual(["packages/core/index.ts"]); }); it("scans markdown and mdx files inside runtime roots", async () => { const root = await createFixtureRoot(); await writeFixtureFile(root, "packages/docs/runtime.md", "Legacy PageBlocks reference\n"); await writeFixtureFile(root, "packages/docs/runtime.mdx", "Legacy site demo reference\n"); const result = await scanRemovedModules(root, ["packages"]); expect(result.diagnostics).toEqual([ { id: "pageblocks", file: "packages/docs/runtime.md", line: 1, excerpt: "Legacy PageBlocks reference" }, { id: "site-demo", file: "packages/docs/runtime.mdx", line: 1, excerpt: "Legacy site demo reference" }, ]); }); });