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 1 commit
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");
}
}
});
});
71 changes: 31 additions & 40 deletions packages/pages/src/common/src/parsers/puckConfigParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,49 +4,40 @@ import { newConfig } from "../../../scaffold/template/sampleTemplates.js";
import { SyntaxKind } from "ts-morph";

/**
* ConfigParser is a class for parsing the puck config file.
* 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 default class ConfigParser {
/**
* Adds variables to the puckConfig file and adds the new config to
* the exported map.
* @param templateName as recieved by user input
* @param formattedTemplateName capitalized first letter of fileName
* @param fileName removed invalid chars from templateName
* @param filepath /src/puck/ve.config.ts
*/
addDataToPuckConfig(
templateName: string,
formattedTemplateName: string,
fileName: string,
filepath: string
) {
if (!fs.existsSync(filepath)) {
throw new Error(`Filepath "${filepath}" is invalid.`);
}
const newSfp = new SourceFileParser(filepath, createTsMorphProject());
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 = newSfp.getVariableStatement("puckConfigs");
const puckConfigsStatement = parser.getVariableStatement("puckConfigs");

const puckConfigsStartLocation = puckConfigsStatement.getStart();
newSfp.insertStatement(
newConfig(formattedTemplateName, fileName),
puckConfigsStartLocation
);
const formattedTemplateName =
fileName.charAt(0).toUpperCase() + fileName.slice(1);

const puckConfigsDeclaration = newSfp.getVariableDeclaration("puckConfigs");
const puckConfigsInitializer = puckConfigsDeclaration.getInitializer();
if (
puckConfigsInitializer &&
puckConfigsInitializer.getKind() === SyntaxKind.NewExpression
) {
const newExpression = puckConfigsInitializer;
const puckConfigsArray = newExpression.getFirstChildByKindOrThrow(
SyntaxKind.ArrayLiteralExpression
);
puckConfigsArray.addElement(`["${templateName}", ${fileName}Config]`);
}
newSfp.format();
newSfp.save();
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();
}
23 changes: 0 additions & 23 deletions packages/pages/src/common/src/project/structure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ export interface Subfolders {
redirects: string;
/** The Node functions folder */
serverlessFunctions: string; // Node functions
/** The puck folder */
puck: string;
/** Where to output the bundled static assets */
assets: string;
/** Where to output the bundled public assets */
Expand Down Expand Up @@ -156,7 +154,6 @@ const defaultProjectStructureConfig: ProjectStructureConfig = {
modules: "modules",
redirects: "redirects",
serverlessFunctions: "functions",
puck: "puck",
assets: DEFAULT_ASSETS_DIR,
public: DEFAULT_PUBLIC_DIR,
clientBundle: "client",
Expand Down Expand Up @@ -331,24 +328,4 @@ export class ProjectStructure {

return [new Path(modulesPath)];
};

/**
* @returns the list of of src/puck, taking scope into account. If a scope is defined and
* the scoped path exists, then both the scoped and non-scoped puck paths are returned.
*/
getPuckPaths = () => {
// src/puck
const puckRoot = pathLib.join(
this.config.rootFolders.source,
this.config.subfolders.puck
);

const scopedPath = pathLib.join(puckRoot, this.config.scope ?? "");

if (this.config.scope && fs.existsSync(scopedPath)) {
return [new Path(scopedPath), new Path(puckRoot)];
}

return [new Path(puckRoot)];
};
}
40 changes: 23 additions & 17 deletions packages/pages/src/scaffold/template/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { ProjectStructure } from "../../common/src/project/structure.js";
import path from "node:path";
import fs from "node:fs";
import { newConfigFile, visualEditorTemplateCode } from "./sampleTemplates.js";
import ConfigParser from "../../common/src/parsers/puckConfigParser.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,7 +76,7 @@ export const generateTemplate = async (
const response = await prompts(questions);

if (response.isVisualEditor) {
generateVETemplate(response, projectStructure);
await generateVETemplate(response, projectStructure);
} else {
// TODO (SUMO-5252): handle generating non-VE templates
}
Expand Down Expand Up @@ -119,7 +124,7 @@ const formatFileName = (templateName: string): string => {

// 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 = (
const generateVETemplate = async (
response: any,
projectStructure: ProjectStructure
) => {
Expand All @@ -129,21 +134,21 @@ const generateVETemplate = (
fs.writeFileSync(
path.join(templatePath, `${templateFileName}.tsx`),
visualEditorTemplateCode(
response.templateName,
templateFileName,
response.entityScope,
response.filter
)
);
addVETemplateToConfig(
response.templateName,
templateFileName,
projectStructure
);
addVETemplateToConfig(templateFileName, projectStructure);

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

const addVETemplateToConfig = (
templateName: string,
fileName: string,
projectStructure: ProjectStructure
) => {
Expand All @@ -152,13 +157,14 @@ const addVETemplateToConfig = (
"ve.config.ts"
);
if (fs.existsSync(configPath)) {
new ConfigParser().addDataToPuckConfig(
templateName,
fileName.charAt(0).toUpperCase() + fileName.slice(1),
fileName,
configPath
);
addDataToPuckConfig(fileName, configPath);
} else {
fs.writeFileSync(configPath, newConfigFile(templateName, fileName));
fs.writeFileSync(configPath, newConfigFile(fileName));
}
};

const getDependencies = async () => {
asanehisa marked this conversation as resolved.
Show resolved Hide resolved
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");
}
}
});
});
45 changes: 16 additions & 29 deletions packages/pages/src/scaffold/template/sampleTemplates.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
// TODO: Use new resolveData (or similar function) in transformProps
export const visualEditorTemplateCode = (
templateName: string,
asanehisa marked this conversation as resolved.
Show resolved Hide resolved
fileName: string,
entityScope: string,
filter: string[]
): string => {
const formattedTemplateName =
fileName.charAt(0).toUpperCase() + fileName.slice(1);
templateName.charAt(0).toUpperCase() + templateName.slice(1);
const filterCode = `${entityScope}: ${JSON.stringify(filter)},`;
const config = `${fileName}Config`;
const config = `${templateName}Config`;

return `import {
Template,
Expand All @@ -22,7 +20,7 @@ export const visualEditorTemplateCode = (
import { Config, Render } from "@measured/puck";
import { ${config} } from "../ve.config";
import { DocumentProvider } from "../hooks/useDocument";
import { getTemplatePuckData } from "../utils/puckDataHelper";
import { resolveVisualEditorData } from "@yext/visual-editor";

export const config: TemplateConfig = {
name: "${templateName}",
Expand Down Expand Up @@ -53,25 +51,14 @@ export const transformProps = async (data) => {
const entityConfigurations = document.c_visualConfigurations ?? [];
const entityLayoutConfigurations = document.c_pages_layouts ?? [];
const siteLayoutConfigurations = document._site?.c_visualLayouts;
try {
const templateData = getTemplatePuckData(
entityConfigurations,
entityLayoutConfigurations,
siteLayoutConfigurations,
config.name,
);
const visualTemplate = JSON.parse(templateData);
return {
...data,
document: {
...document,
visualTemplate,
},
};
} catch (error) {
console.error("Failed to parse visualTemplate: " + error);
return data;
}
const visualTemplate = resolveVisualEditorData(entityConfigurations, entityLayoutConfigurations, siteLayoutConfigurations, ${formattedTemplateName});
return {
...data,
document: {
...document,
visualTemplate,
},
};
};

export const getHeadConfig: GetHeadConfig<TemplateRenderProps> = ({
Expand All @@ -85,7 +72,7 @@ export const getHeadConfig: GetHeadConfig<TemplateRenderProps> = ({
};

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

const ${formattedTemplateName}: Template<TemplateRenderProps> = ({ document }) => {
Expand All @@ -101,14 +88,14 @@ export default ${formattedTemplateName};
`;
};

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

return `import type { Config } from "@measured/puck";
${newConfig(formattedTemplateName, fileName)}
asanehisa marked this conversation as resolved.
Show resolved Hide resolved
${newConfig(formattedTemplateName, templateName)}
export const puckConfigs = new Map<string, Config<any>>([
["${templateName}", ${fileName}Config],
["${templateName}", ${templateName}Config],
]);
`;
};
Expand Down
2 changes: 0 additions & 2 deletions packages/yext-function/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@ export interface Subfolders {
modules: string;
/** The Node functions folder */
serverlessFunctions: string; // Node functions
/** The puck folder */
puck: string;
/** Where to output the bundled static assets */
assets: string;
/** Where to output the client bundles */
Expand Down
Loading