Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(scaffold): create ve template on scaffold #532

Merged
merged 5 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions packages/pages/src/common/src/parsers/puckConfigParser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { describe, it, expect } from "vitest";
import fs from "node:fs";
import { addDataToPuckConfig } from "./puckConfigParser.js";

describe("addDataToPuckConfig", () => {
it("should throw an error if the filepath is invalid", () => {
expect(() => addDataToPuckConfig("fileName", "invalid/filepath")).toThrow(
'Filepath "invalid/filepath" is invalid.'
);
});

it("correctly adds new config to the puck config file", () => {
try {
fs.writeFileSync(
"test.tsx",
`export const puckConfigs = new Map<string, Config<any>>([
["location", locationConfig],
]);`
);
addDataToPuckConfig("foo", "test.tsx");
const modifiedContent = fs.readFileSync("test.tsx", "utf-8");
expect(modifiedContent).toContain('["foo", fooConfig]');
expect(modifiedContent).toContain(
`export const fooConfig: Config<FooProps>`
);
} finally {
if (fs.existsSync("test.tsx")) {
fs.unlinkSync("test.tsx");
}
}
});
});
43 changes: 43 additions & 0 deletions packages/pages/src/common/src/parsers/puckConfigParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import fs from "node:fs";
import SourceFileParser, { createTsMorphProject } from "./sourceFileParser.js";
import { newConfig } from "../../../scaffold/template/sampleTemplates.js";
import { SyntaxKind } from "ts-morph";

/**
* Adds variables to the puck config file and adds the new config to
* the exported map.
* @param fileName template name with invalid chars and spaces removed
* @param filepath /src/puck/ve.config.ts
*/
export function addDataToPuckConfig(fileName: string, filepath: string) {
if (!fs.existsSync(filepath)) {
throw new Error(`Filepath "${filepath}" is invalid.`);
}
const parser = new SourceFileParser(filepath, createTsMorphProject());

const puckConfigsStatement = parser.getVariableStatement("puckConfigs");

const formattedTemplateName =
fileName.charAt(0).toUpperCase() + fileName.slice(1);

const puckConfigsStartLocation = puckConfigsStatement.getStart();
parser.insertStatement(
newConfig(formattedTemplateName, fileName),
puckConfigsStartLocation
);

const puckConfigsDeclaration = parser.getVariableDeclaration("puckConfigs");
const puckConfigsInitializer = puckConfigsDeclaration.getInitializer();
if (
puckConfigsInitializer &&
puckConfigsInitializer.getKind() === SyntaxKind.NewExpression
) {
const newExpression = puckConfigsInitializer;
const puckConfigsArray = newExpression.getFirstChildByKindOrThrow(
SyntaxKind.ArrayLiteralExpression
);
puckConfigsArray.addElement(`["${fileName}", ${fileName}Config]`);
}
parser.format();
parser.save();
}
12 changes: 12 additions & 0 deletions packages/pages/src/common/src/parsers/sourceFileParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,4 +277,16 @@ export default class SourceFileParser {
removeUnusedImports() {
this.sourceFile.fixUnusedIdentifiers();
}

format() {
this.sourceFile.formatText();
}

getVariableStatement(variableName: string) {
return this.sourceFile.getVariableStatementOrThrow(variableName);
}

getVariableDeclaration(variableName: string) {
return this.sourceFile.getVariableDeclarationOrThrow(variableName);
}
}
64 changes: 59 additions & 5 deletions packages/pages/src/scaffold/template/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ import prompts, { PromptObject } from "prompts";
import { ProjectStructure } from "../../common/src/project/structure.js";
import path from "node:path";
import fs from "node:fs";

/* eslint-disable @typescript-eslint/no-unused-vars */
// TODO: Remove after using adding generation code for templates
import { newConfigFile, visualEditorTemplateCode } from "./sampleTemplates.js";
import { addDataToPuckConfig } from "../../common/src/parsers/puckConfigParser.js";
import {
installDependencies,
updatePackageDependency,
} from "../../upgrade/pagesUpdater.js";
import { logErrorAndExit } from "../../util/logError.js";

export const generateTemplate = async (
projectStructure: ProjectStructure
Expand Down Expand Up @@ -71,8 +75,11 @@ export const generateTemplate = async (

const response = await prompts(questions);

// TODO (SUMO-5251): handle generating VE templates
// TODO (SUMO-5252): handle generating non-VE templates
if (response.isVisualEditor) {
await generateVETemplate(response, projectStructure);
} else {
// TODO (SUMO-5252): handle generating non-VE templates
}
};

// Returns true if templateName can be formatted into valid filename and that filename isn't being used.
Expand Down Expand Up @@ -114,3 +121,50 @@ const formatFileName = (templateName: string): string => {

return fileName;
};

// Creates a src/templates/ file with a basic template based on provided user responses
// and adds the new VE template and config to src/ve.config.ts
const generateVETemplate = async (
response: any,
projectStructure: ProjectStructure
) => {
const templatePath = projectStructure.getTemplatePaths()[0].path;
const templateFileName = formatFileName(response.templateName);

fs.writeFileSync(
path.join(templatePath, `${templateFileName}.tsx`),
visualEditorTemplateCode(
templateFileName,
response.entityScope,
response.filter
)
);
addVETemplateToConfig(templateFileName, projectStructure);

try {
await addVEDependencies();
} catch (error) {
logErrorAndExit(error);
}
};

const addVETemplateToConfig = (
fileName: string,
projectStructure: ProjectStructure
) => {
const configPath = path.join(
projectStructure.config.rootFolders.source,
"ve.config.ts"
);
if (fs.existsSync(configPath)) {
addDataToPuckConfig(fileName, configPath);
} else {
fs.writeFileSync(configPath, newConfigFile(fileName));
}
};

const addVEDependencies = async () => {
await updatePackageDependency("@yext/visual-editor", null, true);
await updatePackageDependency("@measured/puck", null, true);
await installDependencies();
};
60 changes: 60 additions & 0 deletions packages/pages/src/scaffold/template/sampleTemplates.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, expect, it } from "vitest";
import { newConfigFile, visualEditorTemplateCode } from "./sampleTemplates.js";
import fs from "node:fs";
import { Project } from "ts-morph";

describe("newConfigFile", () => {
it("confirm returned code has no warnings", () => {
const fileContent = newConfigFile("testTemplate");
const filePath = "test.tsx";

try {
fs.writeFileSync(filePath, fileContent);
const project = new Project();
project.addSourceFileAtPath(filePath);
const diagnostics = project
.getPreEmitDiagnostics()
.filter(
(d) =>
!d
.getMessageText()
.toString()
.includes("Cannot find module '@measured/puck'")
);
expect(diagnostics.length).toBe(0);
} finally {
if (fs.existsSync("test.tsx")) {
fs.unlinkSync("test.tsx");
}
}
});
});

describe("visualEditorTemplateCode", () => {
it("confirm returned code has no warnings", () => {
const fileContent = visualEditorTemplateCode(
"testTemplate",
"entityTypes",
["location"]
);
const filePath = "test.tsx";

try {
fs.writeFileSync(filePath, fileContent);
const project = new Project();
project.addSourceFileAtPath(filePath);
const diagnostics = project
.getPreEmitDiagnostics()
.filter(
(d) =>
!d.getMessageText().toString().includes("Cannot find module") &&
!d.getMessageText().toString().includes("Cannot use JSX")
);
expect(diagnostics.length).toBe(0);
} finally {
if (fs.existsSync("test.tsx")) {
fs.unlinkSync("test.tsx");
}
}
});
});
115 changes: 115 additions & 0 deletions packages/pages/src/scaffold/template/sampleTemplates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
export const visualEditorTemplateCode = (
asanehisa marked this conversation as resolved.
Show resolved Hide resolved
templateName: string,
entityScope: string,
filter: string[]
): string => {
const formattedTemplateName =
templateName.charAt(0).toUpperCase() + templateName.slice(1);
const filterCode = `${entityScope}: ${JSON.stringify(filter)},`;
const config = `${templateName}Config`;

return `import {
Template,
GetPath,
TemplateConfig,
TemplateProps,
TemplateRenderProps,
GetHeadConfig,
HeadConfig,
} from "@yext/pages";
import { Config, Render } from "@measured/puck";
import { ${config} } from "../ve.config";
import { DocumentProvider } from "../hooks/useDocument";
import { resolveVisualEditorData } from "@yext/visual-editor";

export const config: TemplateConfig = {
name: "${templateName}",
stream: {
$id: "${templateName}-stream",
filter: {
${filterCode}
},
fields: [
"id",
"name",
"slug",
"c_visualConfigurations",
"c_pages_layouts.c_visualConfiguration",
],
localization: {
locales: ["en"],
},
},
additionalProperties: {
isVETemplate: true,
isDraft: true,
}
};

export const transformProps = async (data) => {
const { document } = data;
const entityConfigurations = document.c_visualConfigurations ?? [];
const entityLayoutConfigurations = document.c_pages_layouts ?? [];
const siteLayoutConfigurations = document._site?.c_visualLayouts;
const visualTemplate = resolveVisualEditorData(entityConfigurations, entityLayoutConfigurations, siteLayoutConfigurations, ${formattedTemplateName});
return {
...data,
document: {
...document,
visualTemplate,
},
};
};

export const getHeadConfig: GetHeadConfig<TemplateRenderProps> = ({
document,
}): HeadConfig => {
return {
title: document.name,
charset: "UTF-8",
viewport: "width=device-width, initial-scale=1"
};
};

export const getPath: GetPath<TemplateProps> = ({ document }) => {
return document.slug ? document.slug : "${templateName}/" + document.id;
};

const ${formattedTemplateName}: Template<TemplateRenderProps> = ({ document }) => {
const { visualTemplate } = document;
return (
<DocumentProvider value={document}>
<Render config={${config} as Config} data={visualTemplate} />
</DocumentProvider>
);
};

export default ${formattedTemplateName};
`;
};

export const newConfigFile = (templateName: string) => {
const formattedTemplateName =
templateName.charAt(0).toUpperCase() + templateName.slice(1);

return `import type { Config } from "@measured/puck";
asanehisa marked this conversation as resolved.
Show resolved Hide resolved
${newConfig(formattedTemplateName, templateName)}
export const puckConfigs = new Map<string, Config<any>>([
["${templateName}", ${templateName}Config],
]);
`;
};

export const newConfig = (formattedTemplateName: string, fileName: string) => {
return `
// eslint-disable-next-line @typescript-eslint/ban-types
type ${formattedTemplateName}Props = {
};

export const ${fileName}Config: Config<${formattedTemplateName}Props> = {
components: { },
root: { },
};

`;
};
Loading