From 46094aee5d5ae03b78d5c7efce07e57e4884e0c6 Mon Sep 17 00:00:00 2001 From: Nidhi Manu Date: Fri, 6 Oct 2023 18:51:36 -0400 Subject: [PATCH] Fix imports for commonly-named components --- .../studio-plugin/src/sourcefiles/PageFile.ts | 9 +- .../src/writers/FileSystemWriter.ts | 3 +- .../src/writers/ReactComponentFileWriter.ts | 43 +- .../tests/FileSystemManager.test.ts | 5 + .../FileSystemManager/components/Banner.tsx | 3 + .../FileSystemManager/pages/UpdatedPage.tsx | 2 + .../PageWithContainerAndText.tsx | 11 + .../tests/__fixtures__/componentStates.ts | 6 +- .../PageFile.updatePageFile.test.ts | 592 ++++++++++-------- .../tests/writers/FileSystemWriter.test.ts | 5 + .../writers/ReactComponentFileWriter.test.ts | 102 ++- 11 files changed, 488 insertions(+), 293 deletions(-) create mode 100644 packages/studio-plugin/tests/__fixtures__/FileSystemManager/components/Banner.tsx create mode 100644 packages/studio-plugin/tests/__fixtures__/PageFile/updatePageFile/PageWithContainerAndText.tsx diff --git a/packages/studio-plugin/src/sourcefiles/PageFile.ts b/packages/studio-plugin/src/sourcefiles/PageFile.ts index 537d65ce6..dfdc1228c 100644 --- a/packages/studio-plugin/src/sourcefiles/PageFile.ts +++ b/packages/studio-plugin/src/sourcefiles/PageFile.ts @@ -1,6 +1,6 @@ import { ArrowFunction, FunctionDeclaration, Project } from "ts-morph"; import { Result } from "true-myth"; -import { PageState } from "../types"; +import { FileMetadata, PageState } from "../types"; import TemplateConfigWriter from "../writers/TemplateConfigWriter"; import ReactComponentFileWriter from "../writers/ReactComponentFileWriter"; import upath from "upath"; @@ -110,8 +110,12 @@ export default class PageFile { * source file will be mutated to update the template configuration. * * @param updatedPageState - the updated state for the page file + * @param UUIDToFileMetadata - mapping of metadataUUID to FileMetadata */ - updatePageFile(updatedPageState: PageState): void { + updatePageFile( + updatedPageState: PageState, + UUIDToFileMetadata: Record + ): void { const onFileUpdate = ( pageComponent: FunctionDeclaration | ArrowFunction ) => { @@ -134,6 +138,7 @@ export default class PageFile { componentTree: updatedPageState.componentTree, cssImports: updatedPageState.cssImports, onFileUpdate, + UUIDToFileMetadata, }); } } diff --git a/packages/studio-plugin/src/writers/FileSystemWriter.ts b/packages/studio-plugin/src/writers/FileSystemWriter.ts index 3fa0bb0cd..662afd672 100644 --- a/packages/studio-plugin/src/writers/FileSystemWriter.ts +++ b/packages/studio-plugin/src/writers/FileSystemWriter.ts @@ -22,7 +22,8 @@ export class FileSystemWriter { */ writeToPageFile(pageName: string, pageState: PageState): void { const pageFile = this.orchestrator.getOrCreatePageFile(pageName); - pageFile.updatePageFile(pageState); + const UUIDToFileMetadata = this.orchestrator.getUUIDToFileMetadata(); + pageFile.updatePageFile(pageState, UUIDToFileMetadata); } writeToSiteSettings(siteSettingsValues: SiteSettingsValues): void { diff --git a/packages/studio-plugin/src/writers/ReactComponentFileWriter.ts b/packages/studio-plugin/src/writers/ReactComponentFileWriter.ts index a7129f9c1..285620a6f 100644 --- a/packages/studio-plugin/src/writers/ReactComponentFileWriter.ts +++ b/packages/studio-plugin/src/writers/ReactComponentFileWriter.ts @@ -14,15 +14,19 @@ import StudioSourceFileParser from "../parsers/StudioSourceFileParser"; import { ComponentState, ComponentStateKind, + ErrorComponentState, + FileMetadata, PropVal, PropValueKind, PropValues, PropValueType, + StandardComponentState, } from "../types"; import StudioSourceFileWriter from "./StudioSourceFileWriter"; import ComponentTreeHelpers from "../utils/ComponentTreeHelpers"; import camelCase from "camelcase"; import { CustomTags } from "../parsers/helpers/TypeNodeParsingHelper"; +import getImportSpecifier from "../utils/getImportSpecifier"; /** * ReactComponentFileWriter is a class for housing data @@ -161,6 +165,39 @@ export default class ReactComponentFileWriter { } } + /** + * Gets the import data for each component in the component tree that is + * defined in the user's repo (e.g. in `src/components`). Note, for now, we + * only allow default imports for components. + */ + private getComponentImports( + componentTree: ComponentState[], + UUIDToFileMetadata: Record + ): { name: string; moduleSpecifier: string }[] { + return componentTree + .filter( + (c): c is StandardComponentState | ErrorComponentState => + c.kind === ComponentStateKind.Standard || + c.kind === ComponentStateKind.Error + ) + .filter((c) => { + if (UUIDToFileMetadata[c.metadataUUID]) { + return true; + } + console.error( + `Error adding import in ${this.componentName}: unable to find metadata for ${c.componentName}.` + ); + return false; + }) + .map((c) => { + const moduleSpecifier = getImportSpecifier( + this.studioSourceFileParser.getFilepath(), + UUIDToFileMetadata[c.metadataUUID].filepath + ); + return { name: c.componentName, moduleSpecifier }; + }); + } + /** * Update a React component file, which include: * - file imports @@ -170,16 +207,16 @@ export default class ReactComponentFileWriter { */ updateFile({ componentTree, + UUIDToFileMetadata, cssImports, onFileUpdate, - defaultImports, }: { componentTree: ComponentState[]; + UUIDToFileMetadata: Record; cssImports?: string[]; onFileUpdate?: ( functionComponent: FunctionDeclaration | ArrowFunction ) => void; - defaultImports?: { name: string; moduleSpecifier: string }[]; }): void { let defaultExport: VariableDeclaration | FunctionDeclaration; try { @@ -207,7 +244,7 @@ export default class ReactComponentFileWriter { this.studioSourceFileWriter.updateFileImports( {}, cssImports, - defaultImports + this.getComponentImports(componentTree, UUIDToFileMetadata) ); this.studioSourceFileWriter.writeToFile(); } diff --git a/packages/studio-plugin/tests/FileSystemManager.test.ts b/packages/studio-plugin/tests/FileSystemManager.test.ts index 93e0291bf..60d29c4bc 100644 --- a/packages/studio-plugin/tests/FileSystemManager.test.ts +++ b/packages/studio-plugin/tests/FileSystemManager.test.ts @@ -12,6 +12,10 @@ import { PageState, } from "../src/types"; import { createTestProject } from "./__utils__/createTestSourceFile"; +import * as uuidUtils from "uuid"; + +jest.mock("uuid"); +jest.spyOn(uuidUtils, "v4").mockReturnValue("mock-metadata-uuid"); const bannerComponentState: ComponentState = { kind: ComponentStateKind.Standard, @@ -34,6 +38,7 @@ const projectRoot = upath.resolve( const tsMorphProject: Project = createTestProject(); const paths = getUserPaths(projectRoot); paths.pages = upath.join(projectRoot, "pages"); +paths.components = upath.join(projectRoot, "components"); const orchestrator = new ParsingOrchestrator(tsMorphProject, { paths, diff --git a/packages/studio-plugin/tests/__fixtures__/FileSystemManager/components/Banner.tsx b/packages/studio-plugin/tests/__fixtures__/FileSystemManager/components/Banner.tsx new file mode 100644 index 000000000..d0c706a6e --- /dev/null +++ b/packages/studio-plugin/tests/__fixtures__/FileSystemManager/components/Banner.tsx @@ -0,0 +1,3 @@ +export default function Banner() { + return <>; +} diff --git a/packages/studio-plugin/tests/__fixtures__/FileSystemManager/pages/UpdatedPage.tsx b/packages/studio-plugin/tests/__fixtures__/FileSystemManager/pages/UpdatedPage.tsx index fd7297883..054111a1a 100644 --- a/packages/studio-plugin/tests/__fixtures__/FileSystemManager/pages/UpdatedPage.tsx +++ b/packages/studio-plugin/tests/__fixtures__/FileSystemManager/pages/UpdatedPage.tsx @@ -1,3 +1,5 @@ +import Banner from "../components/Banner"; + export default function NewPage() { return ; } diff --git a/packages/studio-plugin/tests/__fixtures__/PageFile/updatePageFile/PageWithContainerAndText.tsx b/packages/studio-plugin/tests/__fixtures__/PageFile/updatePageFile/PageWithContainerAndText.tsx new file mode 100644 index 000000000..2ae674e84 --- /dev/null +++ b/packages/studio-plugin/tests/__fixtures__/PageFile/updatePageFile/PageWithContainerAndText.tsx @@ -0,0 +1,11 @@ +import Container from "../../ComponentFile/Container"; +import Text from "../../ComponentFile/Text"; + +export default function IndexPage() { + return ( + <> + + + + ); +} diff --git a/packages/studio-plugin/tests/__fixtures__/componentStates.ts b/packages/studio-plugin/tests/__fixtures__/componentStates.ts index 34fd40b55..3400e99db 100644 --- a/packages/studio-plugin/tests/__fixtures__/componentStates.ts +++ b/packages/studio-plugin/tests/__fixtures__/componentStates.ts @@ -105,7 +105,7 @@ export const streamConfigMultipleFieldsComponentTree: ComponentState[] = [ componentName: "ComplexBanner", parentUUID: "mock-uuid-0", uuid: "mock-uuid-1", - metadataUUID: "banner-metadata", + metadataUUID: "mock-metadata-uuid", props: { title: { kind: PropValueKind.Expression, @@ -151,7 +151,7 @@ export const streamConfigMultipleFieldsComponentTree: ComponentState[] = [ componentName: "ComplexBanner", parentUUID: "mock-uuid-0", uuid: "mock-uuid-3", - metadataUUID: "banner-metadata", + metadataUUID: "mock-metadata-uuid", props: { title: { kind: PropValueKind.Expression, @@ -165,7 +165,7 @@ export const streamConfigMultipleFieldsComponentTree: ComponentState[] = [ componentName: "ComplexBanner", parentUUID: "mock-uuid-0", uuid: "mock-uuid-4", - metadataUUID: "banner-metadata", + metadataUUID: "mock-metadata-uuid", props: { title: { kind: PropValueKind.Literal, diff --git a/packages/studio-plugin/tests/sourcefiles/PageFile.updatePageFile.test.ts b/packages/studio-plugin/tests/sourcefiles/PageFile.updatePageFile.test.ts index ac4c64dd1..54cef1c24 100644 --- a/packages/studio-plugin/tests/sourcefiles/PageFile.updatePageFile.test.ts +++ b/packages/studio-plugin/tests/sourcefiles/PageFile.updatePageFile.test.ts @@ -6,11 +6,11 @@ import * as uuidUtils from "uuid"; import fs from "fs"; import { Project } from "ts-morph"; import { streamConfigMultipleFieldsComponentTree } from "../__fixtures__/componentStates"; -import { addFilesToProject } from "../__utils__/addFilesToProject"; import { throwIfCalled } from "../__utils__/throwIfCalled"; import { PageState } from "../../src/types/PageState"; import upath from "upath"; import { createTestProject } from "../__utils__/createTestSourceFile"; +import { FileMetadata, FileMetadataKind } from "../../src/types"; jest.mock("uuid"); @@ -28,6 +28,14 @@ const entityPageState: PageState = { }, }; +const UUIDToFileMetadata: Record = { + "mock-metadata-uuid": { + kind: FileMetadataKind.Component, + metadataUUID: "mock-metadata-uuid", + filepath: getComponentPath("ComplexBanner"), + }, +}; + describe("updatePageFile", () => { let tsMorphProject: Project; beforeEach(() => { @@ -36,20 +44,22 @@ describe("updatePageFile", () => { }); it("updates page component based on PageState's component tree", () => { - addFilesToProject(tsMorphProject, [getComponentPath("ComplexBanner")]); const pageFile = createPageFile("EmptyPage", tsMorphProject); - pageFile.updatePageFile({ - ...basicPageState, - componentTree: [ - { - kind: ComponentStateKind.Standard, - componentName: "ComplexBanner", - props: {}, - uuid: "mock-uuid-0", - metadataUUID: "mock-metadata-uuid", - }, - ], - }); + pageFile.updatePageFile( + { + ...basicPageState, + componentTree: [ + { + kind: ComponentStateKind.Standard, + componentName: "ComplexBanner", + props: {}, + uuid: "mock-uuid-0", + metadataUUID: "mock-metadata-uuid", + }, + ], + }, + UUIDToFileMetadata + ); expect(fs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining("EmptyPage.tsx"), fs.readFileSync(getPagePath("updatePageFile/PageWithAComponent"), "utf-8") @@ -63,32 +73,34 @@ describe("updatePageFile", () => { }); it("adds template config variable when it is not already defined", () => { - addFilesToProject(tsMorphProject, [getComponentPath("ComplexBanner")]); const pageFile = createPageFile("PageWithAComponent", tsMorphProject); - pageFile.updatePageFile({ - ...entityPageState, - componentTree: [ - { - kind: ComponentStateKind.Standard, - componentName: "ComplexBanner", - uuid: "mock-uuid-0", - props: { - title: { - kind: PropValueKind.Expression, - value: "document.title", - valueType: PropValueType.string, + pageFile.updatePageFile( + { + ...entityPageState, + componentTree: [ + { + kind: ComponentStateKind.Standard, + componentName: "ComplexBanner", + uuid: "mock-uuid-0", + props: { + title: { + kind: PropValueKind.Expression, + value: "document.title", + valueType: PropValueType.string, + }, }, + metadataUUID: "mock-metadata-uuid", + }, + ], + pagesJS: { + getPathValue: undefined, + streamScope: { + entityTypes: ["location"], }, - metadataUUID: "mock-metadata-uuid", - }, - ], - pagesJS: { - getPathValue: undefined, - streamScope: { - entityTypes: ["location"], }, }, - }); + UUIDToFileMetadata + ); expect(fs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining("PageWithAComponent.tsx"), fs.readFileSync( @@ -99,30 +111,32 @@ describe("updatePageFile", () => { }); it("does not add template config if no stream scope is defined", () => { - addFilesToProject(tsMorphProject, [getComponentPath("ComplexBanner")]); const pageFile = createPageFile("PageWithAComponent", tsMorphProject); - pageFile.updatePageFile({ - ...entityPageState, - componentTree: [ - { - kind: ComponentStateKind.Standard, - componentName: "ComplexBanner", - uuid: "mock-uuid-0", - props: { - title: { - kind: PropValueKind.Expression, - value: "document.title", - valueType: PropValueType.string, + pageFile.updatePageFile( + { + ...entityPageState, + componentTree: [ + { + kind: ComponentStateKind.Standard, + componentName: "ComplexBanner", + uuid: "mock-uuid-0", + props: { + title: { + kind: PropValueKind.Expression, + value: "document.title", + valueType: PropValueType.string, + }, }, + metadataUUID: "mock-metadata-uuid", }, - metadataUUID: "mock-metadata-uuid", + ], + pagesJS: { + getPathValue: undefined, + streamScope: undefined, }, - ], - pagesJS: { - getPathValue: undefined, - streamScope: undefined, }, - }); + UUIDToFileMetadata + ); expect(fs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining("PageWithAComponent.tsx"), fs.readFileSync( @@ -133,12 +147,14 @@ describe("updatePageFile", () => { }); it("adds new stream document paths used in new page state", () => { - addFilesToProject(tsMorphProject, [getComponentPath("ComplexBanner")]); const pageFile = createPageFile("EmptyPage", tsMorphProject); - pageFile.updatePageFile({ - ...entityPageState, - componentTree: streamConfigMultipleFieldsComponentTree, - }); + pageFile.updatePageFile( + { + ...entityPageState, + componentTree: streamConfigMultipleFieldsComponentTree, + }, + UUIDToFileMetadata + ); expect(fs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining("EmptyPage.tsx"), fs.readFileSync( @@ -149,35 +165,37 @@ describe("updatePageFile", () => { }); it("removes unused stream document paths", () => { - addFilesToProject(tsMorphProject, [getComponentPath("ComplexBanner")]); const pageFile = createPageFile( "PageWithStreamConfigMultipleFields", tsMorphProject ); - pageFile.updatePageFile({ - ...entityPageState, - componentTree: [ - { - kind: ComponentStateKind.Standard, - componentName: "ComplexBanner", - uuid: "mock-uuid-0", - props: { - title: { - kind: PropValueKind.Expression, - value: "document.title", - valueType: PropValueType.string, + pageFile.updatePageFile( + { + ...entityPageState, + componentTree: [ + { + kind: ComponentStateKind.Standard, + componentName: "ComplexBanner", + uuid: "mock-uuid-0", + props: { + title: { + kind: PropValueKind.Expression, + value: "document.title", + valueType: PropValueType.string, + }, }, + metadataUUID: "mock-metadata-uuid", + }, + ], + pagesJS: { + getPathValue: undefined, + streamScope: { + entityTypes: ["location"], }, - metadataUUID: "mock-metadata-uuid", - }, - ], - pagesJS: { - getPathValue: undefined, - streamScope: { - entityTypes: ["location"], }, }, - }); + UUIDToFileMetadata + ); expect(fs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining("PageWithStreamConfigMultipleFields.tsx"), fs.readFileSync( @@ -188,33 +206,35 @@ describe("updatePageFile", () => { }); it("adds stream document paths in getPath", () => { - addFilesToProject(tsMorphProject, [getComponentPath("ComplexBanner")]); const pageFile = createPageFile("PageWithStreamConfig", tsMorphProject); - pageFile.updatePageFile({ - ...entityPageState, - componentTree: [ - { - kind: ComponentStateKind.Standard, - componentName: "ComplexBanner", - uuid: "mock-uuid-0", - props: { - title: { - kind: PropValueKind.Expression, - value: "document.title", - valueType: PropValueType.string, + pageFile.updatePageFile( + { + ...entityPageState, + componentTree: [ + { + kind: ComponentStateKind.Standard, + componentName: "ComplexBanner", + uuid: "mock-uuid-0", + props: { + title: { + kind: PropValueKind.Expression, + value: "document.title", + valueType: PropValueType.string, + }, }, + metadataUUID: "mock-metadata-uuid", + }, + ], + pagesJS: { + ...entityPageState.pagesJS, + getPathValue: { + kind: PropValueKind.Expression, + value: "`${document.city}-${document.services[0]}`", }, - metadataUUID: "mock-metadata-uuid", - }, - ], - pagesJS: { - ...entityPageState.pagesJS, - getPathValue: { - kind: PropValueKind.Expression, - value: "`${document.city}-${document.services[0]}`", }, }, - }); + UUIDToFileMetadata + ); expect(fs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining("PageWithStreamConfig.tsx"), fs.readFileSync( @@ -225,33 +245,35 @@ describe("updatePageFile", () => { }); it("does not add template props param if no stream paths in tree", () => { - addFilesToProject(tsMorphProject, [getComponentPath("ComplexBanner")]); const pageFile = createPageFile("EmptyPage", tsMorphProject); - pageFile.updatePageFile({ - ...entityPageState, - componentTree: [ - { - kind: ComponentStateKind.Standard, - componentName: "ComplexBanner", - uuid: "mock-uuid-0", - props: { - title: { - kind: PropValueKind.Literal, - value: "title", - valueType: PropValueType.string, + pageFile.updatePageFile( + { + ...entityPageState, + componentTree: [ + { + kind: ComponentStateKind.Standard, + componentName: "ComplexBanner", + uuid: "mock-uuid-0", + props: { + title: { + kind: PropValueKind.Literal, + value: "title", + valueType: PropValueType.string, + }, }, + metadataUUID: "mock-metadata-uuid", + }, + ], + pagesJS: { + ...entityPageState.pagesJS, + getPathValue: { + kind: PropValueKind.Expression, + value: "document.slug", }, - metadataUUID: "mock-metadata-uuid", - }, - ], - pagesJS: { - ...entityPageState.pagesJS, - getPathValue: { - kind: PropValueKind.Expression, - value: "document.slug", }, }, - }); + UUIDToFileMetadata + ); expect(fs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining("EmptyPage.tsx"), fs.readFileSync( @@ -262,68 +284,70 @@ describe("updatePageFile", () => { }); it("dedupes stream document paths", () => { - addFilesToProject(tsMorphProject, [getComponentPath("ComplexBanner")]); const pageFile = createPageFile( "PageWithStreamConfigMultipleFields", tsMorphProject ); - pageFile.updatePageFile({ - ...entityPageState, - componentTree: [ - { - kind: ComponentStateKind.Standard, - componentName: "ComplexBanner", - uuid: "mock-uuid-0", - props: { - title: { - kind: PropValueKind.Expression, - value: "document.arrayIndex[0]", - valueType: PropValueType.string, + pageFile.updatePageFile( + { + ...entityPageState, + componentTree: [ + { + kind: ComponentStateKind.Standard, + componentName: "ComplexBanner", + uuid: "mock-uuid-0", + props: { + title: { + kind: PropValueKind.Expression, + value: "document.arrayIndex[0]", + valueType: PropValueType.string, + }, }, + metadataUUID: "mock-metadata-uuid", }, - metadataUUID: "mock-metadata-uuid-0", - }, - { - kind: ComponentStateKind.Standard, - componentName: "ComplexBanner", - uuid: "mock-uuid-1", - props: { - title: { - kind: PropValueKind.Expression, - value: "document.arrayIndex[1]", - valueType: PropValueType.string, + { + kind: ComponentStateKind.Standard, + componentName: "ComplexBanner", + uuid: "mock-uuid-1", + props: { + title: { + kind: PropValueKind.Expression, + value: "document.arrayIndex[1]", + valueType: PropValueType.string, + }, }, + metadataUUID: "mock-metadata-uuid", }, - metadataUUID: "mock-metadata-uuid-1", - }, - { - kind: ComponentStateKind.Standard, - componentName: "ComplexBanner", - uuid: "mock-uuid-2", - props: { - title: { - kind: PropValueKind.Expression, - value: "document.objectField.attr1", - valueType: PropValueType.string, + { + kind: ComponentStateKind.Standard, + componentName: "ComplexBanner", + uuid: "mock-uuid-2", + props: { + title: { + kind: PropValueKind.Expression, + value: "document.objectField.attr1", + valueType: PropValueType.string, + }, }, + metadataUUID: "mock-metadata-uuid", }, - metadataUUID: "mock-metadata-uuid-2", - }, - { - kind: ComponentStateKind.Standard, - componentName: "ComplexBanner", - uuid: "mock-uuid-3", - props: { - title: { - kind: PropValueKind.Expression, - value: "document.objectField.attr2", - valueType: PropValueType.string, + { + kind: ComponentStateKind.Standard, + componentName: "ComplexBanner", + uuid: "mock-uuid-3", + props: { + title: { + kind: PropValueKind.Expression, + value: "document.objectField.attr2", + valueType: PropValueType.string, + }, }, + metadataUUID: "mock-metadata-uuid", }, - metadataUUID: "mock-metadata-uuid-3", - }, - ], - }); + ], + }, + UUIDToFileMetadata + ); expect(fs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining("PageWithStreamConfigMultipleFields.tsx"), fs.readFileSync( @@ -333,30 +357,36 @@ describe("updatePageFile", () => { ); }); - it("handles streams document usages even in ErrorComponentStates", () => { + it("gracefully handles imports and streams document usages even in ErrorComponentStates", () => { + const consoleErrorSpy = jest + .spyOn(global.console, "error") + .mockImplementation(); const pageFile = createPageFile( "PageWithErrorComponentState", tsMorphProject ); - pageFile.updatePageFile({ - ...entityPageState, - componentTree: [ - { - kind: ComponentStateKind.Error, - props: { - title: { - kind: PropValueKind.Expression, - value: "document.name", + pageFile.updatePageFile( + { + ...entityPageState, + componentTree: [ + { + kind: ComponentStateKind.Error, + props: { + title: { + kind: PropValueKind.Expression, + value: "document.name", + }, }, + componentName: "Banner", + fullText: ``, + uuid: "error-banner-uuid", + metadataUUID: "error-banner-metadataUUID", + message: "could not parse banner", }, - componentName: "Banner", - fullText: ``, - uuid: "error-banner-uuid", - metadataUUID: "error-banner-metadataUUID", - message: "could not parse banner", - }, - ], - }); + ], + }, + UUIDToFileMetadata + ); expect(fs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining("PageWithErrorComponentState.tsx"), fs.readFileSync( @@ -364,32 +394,38 @@ describe("updatePageFile", () => { "utf-8" ) ); + expect(consoleErrorSpy).toHaveBeenCalledTimes(1); + expect(consoleErrorSpy).toHaveBeenCalledWith( + "Error adding import in test: unable to find metadata for Banner." + ); }); it("preserves 'slug' field for PagesJS PageFile", () => { - addFilesToProject(tsMorphProject, [getComponentPath("ComplexBanner")]); const pageFile = createPageFile( "EmptyPageWithStreamConfigSlugField", tsMorphProject ); - pageFile.updatePageFile({ - ...entityPageState, - componentTree: [ - { - kind: ComponentStateKind.Standard, - componentName: "ComplexBanner", - uuid: "mock-uuid-0", - props: { - title: { - kind: PropValueKind.Expression, - value: "document.title", - valueType: PropValueType.string, + pageFile.updatePageFile( + { + ...entityPageState, + componentTree: [ + { + kind: ComponentStateKind.Standard, + componentName: "ComplexBanner", + uuid: "mock-uuid-0", + props: { + title: { + kind: PropValueKind.Expression, + value: "document.title", + valueType: PropValueType.string, + }, }, + metadataUUID: "mock-metadata-uuid", }, - metadataUUID: "mock-metadata-uuid", - }, - ], - }); + ], + }, + UUIDToFileMetadata + ); expect(fs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining("PageWithStreamConfigSlugField.tsx"), fs.readFileSync( @@ -400,32 +436,34 @@ describe("updatePageFile", () => { }); it("updates stream scope", () => { - addFilesToProject(tsMorphProject, [getComponentPath("ComplexBanner")]); const pageFile = createPageFile("PageWithStreamConfig", tsMorphProject); - pageFile.updatePageFile({ - ...entityPageState, - componentTree: [ - { - kind: ComponentStateKind.Standard, - componentName: "ComplexBanner", - uuid: "mock-uuid-0", - props: { - title: { - kind: PropValueKind.Expression, - value: "document.title", - valueType: PropValueType.string, + pageFile.updatePageFile( + { + ...entityPageState, + componentTree: [ + { + kind: ComponentStateKind.Standard, + componentName: "ComplexBanner", + uuid: "mock-uuid-0", + props: { + title: { + kind: PropValueKind.Expression, + value: "document.title", + valueType: PropValueType.string, + }, }, + metadataUUID: "mock-metadata-uuid", + }, + ], + pagesJS: { + getPathValue: undefined, + streamScope: { + entityTypes: ["product"], }, - metadataUUID: "mock-metadata-uuid", - }, - ], - pagesJS: { - getPathValue: undefined, - streamScope: { - entityTypes: ["product"], }, }, - }); + UUIDToFileMetadata + ); expect(fs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining("PageWithStreamConfig.tsx"), fs.readFileSync( @@ -439,12 +477,15 @@ describe("updatePageFile", () => { describe("getPath", () => { it("adds new getPath function when there is none", () => { const pageFile = createPageFile("EmptyPage", tsMorphProject); - pageFile.updatePageFile({ - ...basicPageState, - pagesJS: { - getPathValue: { kind: PropValueKind.Literal, value: "index.html" }, + pageFile.updatePageFile( + { + ...basicPageState, + pagesJS: { + getPathValue: { kind: PropValueKind.Literal, value: "index.html" }, + }, }, - }); + UUIDToFileMetadata + ); expect(fs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining("EmptyPage.tsx"), fs.readFileSync(getPagePath("updatePageFile/PageWithGetPath"), "utf-8") @@ -454,12 +495,18 @@ describe("updatePageFile", () => { describe("updates existing getPath function", () => { it("updates to modified string literal value", () => { const pageFile = createPageFile("PageWithGetPath", tsMorphProject); - pageFile.updatePageFile({ - ...basicPageState, - pagesJS: { - getPathValue: { kind: PropValueKind.Literal, value: "static.html" }, + pageFile.updatePageFile( + { + ...basicPageState, + pagesJS: { + getPathValue: { + kind: PropValueKind.Literal, + value: "static.html", + }, + }, }, - }); + UUIDToFileMetadata + ); expect(fs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining("PageWithGetPath.tsx"), fs.readFileSync( @@ -471,15 +518,18 @@ describe("updatePageFile", () => { it("updates to template expression", () => { const pageFile = createPageFile("PageWithGetPath", tsMorphProject); - pageFile.updatePageFile({ - ...basicPageState, - pagesJS: { - getPathValue: { - kind: PropValueKind.Expression, - value: "`test-${document.slug}`", + pageFile.updatePageFile( + { + ...basicPageState, + pagesJS: { + getPathValue: { + kind: PropValueKind.Expression, + value: "`test-${document.slug}`", + }, }, }, - }); + UUIDToFileMetadata + ); expect(fs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining("PageWithGetPath.tsx"), fs.readFileSync( @@ -491,15 +541,18 @@ describe("updatePageFile", () => { it("updates to property access expression", () => { const pageFile = createPageFile("PageWithGetPath", tsMorphProject); - pageFile.updatePageFile({ - ...basicPageState, - pagesJS: { - getPathValue: { - kind: PropValueKind.Expression, - value: "document.slug", + pageFile.updatePageFile( + { + ...basicPageState, + pagesJS: { + getPathValue: { + kind: PropValueKind.Expression, + value: "document.slug", + }, }, }, - }); + UUIDToFileMetadata + ); expect(fs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining("PageWithGetPath.tsx"), fs.readFileSync( @@ -514,12 +567,18 @@ describe("updatePageFile", () => { "PageWithNoReturnGetPath", tsMorphProject ); - pageFile.updatePageFile({ - ...basicPageState, - pagesJS: { - getPathValue: { kind: PropValueKind.Literal, value: "static.html" }, + pageFile.updatePageFile( + { + ...basicPageState, + pagesJS: { + getPathValue: { + kind: PropValueKind.Literal, + value: "static.html", + }, + }, }, - }); + UUIDToFileMetadata + ); expect(fs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining("PageWithNoReturnGetPath.tsx"), fs.readFileSync( @@ -532,13 +591,16 @@ describe("updatePageFile", () => { it("does not modify getPath if getPathValue is undefined", () => { const pageFile = createPageFile("PageWithComplexGetPath", tsMorphProject); - pageFile.updatePageFile({ - ...basicPageState, - pagesJS: { - entityFiles: ["mock-entity-file"], - getPathValue: undefined, + pageFile.updatePageFile( + { + ...basicPageState, + pagesJS: { + entityFiles: ["mock-entity-file"], + getPathValue: undefined, + }, }, - }); + UUIDToFileMetadata + ); expect(fs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining("PageWithComplexGetPath.tsx"), fs.readFileSync( diff --git a/packages/studio-plugin/tests/writers/FileSystemWriter.test.ts b/packages/studio-plugin/tests/writers/FileSystemWriter.test.ts index becae9f77..df6084bfc 100644 --- a/packages/studio-plugin/tests/writers/FileSystemWriter.test.ts +++ b/packages/studio-plugin/tests/writers/FileSystemWriter.test.ts @@ -13,6 +13,10 @@ import { SiteSettingsValues, } from "../../src/types"; import { createTestProject } from "../__utils__/createTestSourceFile"; +import * as uuidUtils from "uuid"; + +jest.mock("uuid"); +jest.spyOn(uuidUtils, "v4").mockReturnValue("mock-metadata-uuid"); jest.mock("fs", () => { const actualFs = jest.requireActual("fs"); @@ -27,6 +31,7 @@ const projectRoot = upath.resolve( ); const paths = getUserPaths(projectRoot); paths.pages = upath.join(projectRoot, "pages"); +paths.components = upath.join(projectRoot, "components"); paths.siteSettings = upath.join(projectRoot, "siteSettings.ts"); const bannerComponentState: ComponentState = { diff --git a/packages/studio-plugin/tests/writers/ReactComponentFileWriter.test.ts b/packages/studio-plugin/tests/writers/ReactComponentFileWriter.test.ts index 0b7426579..cc1e88567 100644 --- a/packages/studio-plugin/tests/writers/ReactComponentFileWriter.test.ts +++ b/packages/studio-plugin/tests/writers/ReactComponentFileWriter.test.ts @@ -10,12 +10,25 @@ import { nestedBannerComponentTree, } from "../__fixtures__/componentStates"; import ReactComponentFileWriter from "../../src/writers/ReactComponentFileWriter"; -import { addFilesToProject } from "../__utils__/addFilesToProject"; -import { PropValueKind, PropValueType } from "../../src/types"; +import { + FileMetadata, + FileMetadataKind, + PropValueKind, + PropValueType, +} from "../../src/types"; import StudioSourceFileWriter from "../../src/writers/StudioSourceFileWriter"; import StudioSourceFileParser from "../../src/parsers/StudioSourceFileParser"; import { createTestProject } from "../__utils__/createTestSourceFile"; +const UUIDToFileMetadata = computeUUIDToFileMetadata({ + "mock-metadata-uuid": "ComplexBanner", + [getComponentPath("BannerUsingObject")]: "BannerUsingObject", + [getComponentPath("BannerUsingArrays")]: "BannerUsingArrays", + "mock-standard-metadata-uuid": "SimpleBanner", + "mock-container-metadata-uuid": "Container", + "mock-text-metadata-uuid": "Text", +}); + function createReactComponentFileWriter( tsMorphProject: Project, filepath: string, @@ -41,7 +54,6 @@ describe("updateFile", () => { describe("React component return statement", () => { it("adds top-level sibling component", () => { - addFilesToProject(tsMorphProject, [getComponentPath("ComplexBanner")]); const filepath = getPagePath("updatePageFile/PageWithAComponent"); const commonComplexBannerState: Omit = { kind: ComponentStateKind.Standard, @@ -59,6 +71,7 @@ describe("updateFile", () => { { ...commonComplexBannerState, uuid: "mock-uuid-1" }, ], cssImports: [], + UUIDToFileMetadata, }); expect(fs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining("PageWithAComponent.tsx"), @@ -70,11 +83,6 @@ describe("updateFile", () => { }); it("nests component with new sibling and children components", () => { - addFilesToProject(tsMorphProject, [ - getComponentPath("ComplexBanner"), - getComponentPath("NestedBanner"), - ]); - const filepath = getPagePath("updatePageFile/PageWithAComponent"); createReactComponentFileWriter( tsMorphProject, @@ -83,6 +91,10 @@ describe("updateFile", () => { ).updateFile({ componentTree: nestedBannerComponentTree, cssImports: [], + UUIDToFileMetadata: computeUUIDToFileMetadata({ + [getComponentPath("ComplexBanner")]: "ComplexBanner", + [getComponentPath("NestedBanner")]: "NestedBanner", + }), }); expect(fs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining("PageWithAComponent.tsx"), @@ -94,10 +106,6 @@ describe("updateFile", () => { }); it("can write object props", () => { - addFilesToProject(tsMorphProject, [ - getComponentPath("BannerUsingObject"), - ]); - const filepath = getPagePath("updatePageFile/PageWithObjectProp"); createReactComponentFileWriter( tsMorphProject, @@ -141,6 +149,7 @@ describe("updateFile", () => { }, ], cssImports: [], + UUIDToFileMetadata, }); expect(fs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining("PageWithObjectProp.tsx"), @@ -152,10 +161,6 @@ describe("updateFile", () => { }); it("can write array props", () => { - addFilesToProject(tsMorphProject, [ - getComponentPath("BannerUsingArrays"), - ]); - const filepath = getPagePath("updatePageFile/PageWithArrayProps"); createReactComponentFileWriter( tsMorphProject, @@ -199,6 +204,7 @@ describe("updateFile", () => { }, ], cssImports: [], + UUIDToFileMetadata, }); expect(fs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining("PageWithArrayProps.tsx"), @@ -210,7 +216,6 @@ describe("updateFile", () => { }); it("remove components that are not part of the new component tree", () => { - addFilesToProject(tsMorphProject, [getComponentPath("ComplexBanner")]); const filepath = getPagePath("updatePageFile/PageWithNestedComponents"); createReactComponentFileWriter( tsMorphProject, @@ -227,6 +232,7 @@ describe("updateFile", () => { }, ], cssImports: [], + UUIDToFileMetadata, }); expect(fs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining("PageWithNestedComponents.tsx"), @@ -248,6 +254,7 @@ describe("updateFile", () => { ).updateFile({ componentTree: [fragmentComponent], cssImports: ["../index.css", "./App.css"], + UUIDToFileMetadata, }); expect(fs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining("EmptyPage.tsx"), @@ -259,7 +266,6 @@ describe("updateFile", () => { }); it("adds missing imports", () => { - addFilesToProject(tsMorphProject, [getComponentPath("SimpleBanner")]); const filepath = getPagePath("updatePageFile/EmptyPage"); createReactComponentFileWriter( tsMorphProject, @@ -278,6 +284,7 @@ describe("updateFile", () => { }, ], cssImports: [], + UUIDToFileMetadata, }); expect(fs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining("EmptyPage.tsx"), @@ -297,12 +304,51 @@ describe("updateFile", () => { ).updateFile({ componentTree: [fragmentComponent], cssImports: [], + UUIDToFileMetadata, }); expect(fs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining("PageWithUnusedImports.tsx"), fs.readFileSync(getPagePath("updatePageFile/EmptyPage"), "utf-8") ); }); + + it("correctly imports Container and Text components", () => { + const filepath = getPagePath("updatePageFile/EmptyPage"); + createReactComponentFileWriter( + tsMorphProject, + filepath, + "IndexPage" + ).updateFile({ + componentTree: [ + fragmentComponent, + { + kind: ComponentStateKind.Standard, + componentName: "Container", + props: {}, + uuid: "mock-uuid-1", + parentUUID: "mock-uuid-0", + metadataUUID: "mock-container-metadata-uuid", + }, + { + kind: ComponentStateKind.Standard, + componentName: "Text", + props: {}, + uuid: "mock-uuid-2", + parentUUID: "mock-uuid-0", + metadataUUID: "mock-text-metadata-uuid", + }, + ], + cssImports: [], + UUIDToFileMetadata, + }); + expect(fs.writeFileSync).toHaveBeenCalledWith( + expect.stringContaining("EmptyPage.tsx"), + fs.readFileSync( + getPagePath("updatePageFile/PageWithContainerAndText"), + "utf-8" + ) + ); + }); }); describe("reactComponentNameSanitizer", () => { @@ -315,7 +361,7 @@ describe("updateFile", () => { tsMorphProject, filepath, inputName - ).updateFile({ componentTree: [] }); + ).updateFile({ componentTree: [], UUIDToFileMetadata }); expect(fs.writeFileSync).toHaveBeenCalledWith( expect.stringContaining("EmptyFile"), expect.stringContaining(`export default function ${outputName}() {}`) @@ -341,3 +387,21 @@ describe("updateFile", () => { }); }); }); + +/** + * Takes a mapping of metadataUUID to component name and outputs + * UUIDToFileMetadata. + */ +function computeUUIDToFileMetadata(components: Record) { + return Object.entries(components).reduce( + (UUIDToFileMetadata, [metadataUUID, componentName]) => { + UUIDToFileMetadata[metadataUUID] = { + kind: FileMetadataKind.Component, + metadataUUID, + filepath: getComponentPath(componentName), + }; + return UUIDToFileMetadata; + }, + {} as Record + ); +}