import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { upgradeSectionRenderer, writeImportedLibShims } from "./phase-cleanup"; import type { MigrationContext } from "./types"; /** * Build a minimal `MigrationContext` for integration tests of * `writeImportedLibShims`. Only the fields that function reads are * non-default; everything else is a placeholder with the right shape. */ function makeCtx(sourceDir: string, dryRun = false): MigrationContext { return { sourceDir, siteName: "test-site", platform: "vtex", vtexAccount: null, gtmId: null, importMap: {}, discoveredNpmDeps: {}, themeColors: {}, tailwindConfig: { colors: {}, fontFamily: {}, screens: {}, safelist: [], safelistPatterns: [], plugins: [], reviewItems: [] }, fontFamily: null, files: [], sectionMetas: [], islandClassifications: [], islandWrapperTargets: new Map(), loaderInventory: [], scaffoldedFiles: [], transformedFiles: [], deletedFiles: [], movedFiles: [], manualReviewItems: [], frameworkFindings: [], dryRun, verbose: false, }; } describe("writeImportedLibShims (integration)", () => { let tmpDir: string; beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "lazy-shim-test-")); fs.mkdirSync(path.join(tmpDir, "src", "loaders"), { recursive: true }); }); afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); it("writes nothing when no ~/lib imports are found", () => { fs.writeFileSync( path.join(tmpDir, "src", "loaders", "products.ts"), `import { something } from "@decocms/apps/vtex";\nexport const x = 1;\n`, ); writeImportedLibShims(makeCtx(tmpDir)); expect(fs.existsSync(path.join(tmpDir, "src", "lib"))).toBe(false); }); it("writes only the shim files matching imports actually present in src/", () => { fs.writeFileSync( path.join(tmpDir, "src", "loaders", "search.ts"), `import { getSegmentFromBag } from "~/lib/vtex-segment";\n` + `import { toFilterSearchString } from "~/lib/filter-navigate";\n`, ); writeImportedLibShims(makeCtx(tmpDir)); const libDir = path.join(tmpDir, "src", "lib"); expect(fs.existsSync(libDir)).toBe(true); const written = fs.readdirSync(libDir).sort(); expect(written).toEqual(["filter-navigate.ts", "vtex-segment.ts"]); }); it("writes nothing in dry-run mode (but does not throw)", () => { fs.writeFileSync( path.join(tmpDir, "src", "loaders", "search.ts"), `import { getSegmentFromBag } from "~/lib/vtex-segment";\n`, ); writeImportedLibShims(makeCtx(tmpDir, /* dryRun */ true)); expect(fs.existsSync(path.join(tmpDir, "src", "lib"))).toBe(false); }); it("strips trailing .ts from the import specifier when scanning", () => { // Some Fresh sites use explicit .ts extensions in their imports. fs.writeFileSync( path.join(tmpDir, "src", "loaders", "search.ts"), `import { fn } from "~/lib/vtex-transform.ts";\n`, ); writeImportedLibShims(makeCtx(tmpDir)); expect( fs.existsSync(path.join(tmpDir, "src", "lib", "vtex-transform.ts")), ).toBe(true); }); it("ignores imports inside the lib dir itself (no self-amplification)", () => { fs.mkdirSync(path.join(tmpDir, "src", "lib")); fs.writeFileSync( path.join(tmpDir, "src", "lib", "existing.ts"), `import { x } from "~/lib/should-not-be-generated";\nexport const y = 1;\n`, ); writeImportedLibShims(makeCtx(tmpDir)); // Only the existing file should remain; nothing new generated. const files = fs.readdirSync(path.join(tmpDir, "src", "lib")); expect(files).toEqual(["existing.ts"]); }); it("scans .tsx files too, not just .ts", () => { fs.mkdirSync(path.join(tmpDir, "src", "components"), { recursive: true }); fs.writeFileSync( path.join(tmpDir, "src", "components", "Filter.tsx"), `import { toFilterSearchString } from "~/lib/filter-navigate";\n` + `export const C = () => null;\n`, ); writeImportedLibShims(makeCtx(tmpDir)); expect( fs.existsSync(path.join(tmpDir, "src", "lib", "filter-navigate.ts")), ).toBe(true); }); it("does nothing when src/ does not exist", () => { const empty = fs.mkdtempSync(path.join(os.tmpdir(), "no-src-")); try { writeImportedLibShims(makeCtx(empty)); expect(fs.readdirSync(empty)).toEqual([]); } finally { fs.rmSync(empty, { recursive: true, force: true }); } }); }); describe("upgradeSectionRenderer (integration)", () => { let tmpDir: string; beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "render-section-test-")); fs.mkdirSync(path.join(tmpDir, "src", "components"), { recursive: true }); }); afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); it( "adds the missing RenderSection import when a .Component call is " + "converted to JSX usage and no import exists yet (regression: the " + "inner check used to always be false)", () => { const file = path.join(tmpDir, "src", "components", "Nested.tsx"); fs.writeFileSync( file, `export function Nested({ block }: { block: any }) {\n` + ` return ;\n` + `}\n`, ); upgradeSectionRenderer(makeCtx(tmpDir)); const result = fs.readFileSync(file, "utf-8"); expect(result).toContain( `import { RenderSection } from "@decocms/blocks/hooks";`, ); expect(result).toContain(``); // Import must appear before its first usage. expect(result.indexOf("import { RenderSection }")).toBeLessThan( result.indexOf(" { const file = path.join(tmpDir, "src", "components", "Nested.tsx"); fs.writeFileSync( file, `import { RenderSection } from "@decocms/blocks/hooks";\n\n` + `export function Nested({ block }: { block: any }) {\n` + ` return ;\n` + `}\n`, ); upgradeSectionRenderer(makeCtx(tmpDir)); const result = fs.readFileSync(file, "utf-8"); const importCount = ( result.match(/import\s*\{\s*RenderSection\s*\}\s*from/g) ?? [] ).length; expect(importCount).toBe(1); }); it("does not touch files with no SectionRenderer/.Component usage", () => { const file = path.join(tmpDir, "src", "components", "Plain.tsx"); const original = `export const Plain = () =>
hi
;\n`; fs.writeFileSync(file, original); upgradeSectionRenderer(makeCtx(tmpDir)); expect(fs.readFileSync(file, "utf-8")).toBe(original); }); });