Skip to content

Commit

Permalink
fix(scaffold): update VE template generation (#563)
Browse files Browse the repository at this point in the history
Realigns the VE template generation output against the latest changes
we've made in the starter. This includes updating the main.tsx template,
adding the theme.config.ts, adding utils/buildSchema.ts, updating
tailwind.config.ts to use the themeConfig, and adding to
.template-manifest.json.

Even though this updates the command, it also removes the VE template
generation option until hybrid mode is better supported.
  • Loading branch information
mkilpatrick authored Jan 21, 2025
1 parent 4fe7b8e commit 79a3d6b
Show file tree
Hide file tree
Showing 8 changed files with 806 additions and 103 deletions.
24 changes: 21 additions & 3 deletions packages/pages/src/common/src/parsers/sourceFileParser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ describe("getDefaultExport", () => {
const parser = createParser(
`export const no = false; export default function test() {}`
);
const defaultExport = parser.getDefaultExport();
const defaultExport = parser.getDefaultExportName();
expect(defaultExport).toBe("test");
});

it("correctly gets default export's name when variable", () => {
const parser = createParser(`const test = 5; export default test`);
const defaultExport = parser.getDefaultExport();
const defaultExport = parser.getDefaultExportName();
expect(defaultExport).toBe("test");
});
});
Expand All @@ -23,7 +23,7 @@ describe("addDefaultExport", () => {
it("correctly adds default export to file", () => {
const parser = createParser(``);
parser.addDefaultExport("test");
expect(parser.getDefaultExport()).toBe("test");
expect(parser.getDefaultExportName()).toBe("test");
});
});

Expand Down Expand Up @@ -216,6 +216,24 @@ describe("removeUnusedImports", () => {
});
});

describe("ensureNamedImport", () => {
it("correctly adds a new named import and module", () => {
const parser = createParser("");
parser.ensureNamedImport("@testModule", "testFunction");
expect(parser.getAllText()).toContain(
'import { testFunction } from "@testModule";'
);
});

it("correctly adds a second named import to an existing module", () => {
const parser = createParser('import { testFunction } from "@testModule";');
parser.ensureNamedImport("@testModule", "testFunction2");
expect(parser.getAllText()).toContain(
'import { testFunction, testFunction2 } from "@testModule";'
);
});
});

function createParser(sourceCode: string) {
const filepath = path.resolve(__dirname, "test.tsx");
const { project } = createTestSourceFile(sourceCode, filepath);
Expand Down
36 changes: 35 additions & 1 deletion packages/pages/src/common/src/parsers/sourceFileParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
OptionalKind,
ImportAttributeStructure,
VariableDeclaration,
ts,
} from "ts-morph";
import typescript from "typescript";

Expand Down Expand Up @@ -102,7 +103,7 @@ export default class SourceFileParser {
* getDefaultExport parses the source file for a default export.
* @returns the default export's name
*/
getDefaultExport(): string {
getDefaultExportName(): string {
const defaultExportSymbol = this.sourceFile.getDefaultExportSymbol();
if (!defaultExportSymbol) {
return "";
Expand Down Expand Up @@ -132,6 +133,10 @@ export default class SourceFileParser {
});
}

getSourceFile() {
return this.sourceFile;
}

/**
* @returns all imports from source file
*/
Expand Down Expand Up @@ -289,4 +294,33 @@ export default class SourceFileParser {
getVariableDeclaration(variableName: string) {
return this.sourceFile.getVariableDeclarationOrThrow(variableName);
}

/**
* Checks if a named import exists in the source file and adds it if it doesn't.
*/
ensureNamedImport(moduleName: string, importName: string) {
const imports = this.sourceFile.getImportDeclarations();

// Check if there's already an import for the specified module
const targetImport = imports.find(
(imp) => imp.getModuleSpecifierValue() === moduleName
);

if (targetImport) {
// Check if the named import already exists
const namedImports = targetImport.getNamedImports();
const hasImport = namedImports.some((ni) => ni.getName() === importName);

if (!hasImport) {
// Add the named import if it doesn't exist
targetImport.addNamedImport(importName);
}
} else {
// Add a new import declaration if the module is not imported
this.sourceFile.addImportDeclaration({
moduleSpecifier: moduleName,
namedImports: [importName],
});
}
}
}
47 changes: 47 additions & 0 deletions packages/pages/src/common/src/parsers/tailwindConfigParser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, it, expect } from "vitest";
import fs from "node:fs";
import {
addThemeConfigToTailwind,
tailwindConfigFilename,
} from "./tailwindConfigParser.js";

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

it("correctly adds the theme config to the tailwind config", () => {
try {
fs.writeFileSync(
tailwindConfigFilename,
`import type { Config } from "tailwindcss";
export default {
content: [
"./src/**/*.{html,js,jsx,ts,tsx}",
"./node_modules/@yext/visual-editor/dist/**/*.js",
],
plugins: [],
} satisfies Config;
`
);
addThemeConfigToTailwind(tailwindConfigFilename);
const modifiedContent = fs.readFileSync(tailwindConfigFilename, "utf-8");
expect(modifiedContent).toContain(`theme: {
extend: themeResolver({}, themeConfig)
}`);
expect(modifiedContent).toContain(
`import { themeConfig } from "./theme.config";`
);
expect(modifiedContent).toContain(
`import { themeResolver } from "@yext/visual-editor";`
);
} finally {
if (fs.existsSync(tailwindConfigFilename)) {
fs.unlinkSync(tailwindConfigFilename);
}
}
});
});
117 changes: 117 additions & 0 deletions packages/pages/src/common/src/parsers/tailwindConfigParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import fs from "node:fs";
import SourceFileParser, { createTsMorphProject } from "./sourceFileParser.js";
import {
ObjectLiteralExpression,
PropertyAssignment,
SyntaxKind,
} from "ts-morph";

export const tailwindConfigFilename = "tailwind.config.ts";

/**
* Adds the themeConfig to tailwind.config.ts if it's not there.
*/
export const addThemeConfigToTailwind = (tailwindConfigPath: string) => {
if (!fs.existsSync(tailwindConfigPath)) {
throw new Error(`Filepath "${tailwindConfigPath}" is invalid.`);
}

const parser = new SourceFileParser(
tailwindConfigPath,
createTsMorphProject()
);

const defaultExport = parser
.getSourceFile()
.getFirstDescendantByKind(SyntaxKind.ExportAssignment);

if (!defaultExport) {
throw new Error("Default export not found in the file.");
}

// Get the initializer of the default export
const exportInitializer = defaultExport.getExpression();

if (!exportInitializer) {
throw new Error("No initializer found for the default export.");
}

// If the initializer includes a `satisfies` clause, extract the object literal
let configObject: ObjectLiteralExpression | undefined;
if (
exportInitializer.getKind() === SyntaxKind.AsExpression ||
exportInitializer.getKind() === SyntaxKind.SatisfiesExpression
) {
const innerExpression = exportInitializer.getChildAtIndex(0); // Extract left-hand side
if (innerExpression.getKind() === SyntaxKind.ObjectLiteralExpression) {
configObject = innerExpression.asKind(SyntaxKind.ObjectLiteralExpression);
}
} else if (
exportInitializer.getKind() === SyntaxKind.ObjectLiteralExpression
) {
configObject = exportInitializer.asKind(SyntaxKind.ObjectLiteralExpression);
}

if (!configObject) {
throw new Error("Config object not found in the default export.");
}

// Locate the "theme" property
let themeProperty = configObject.getProperty("theme") as PropertyAssignment;

if (
!themeProperty ||
themeProperty.getKind() !== SyntaxKind.PropertyAssignment
) {
// Add the "theme" property if it doesn't exist
themeProperty = configObject.addPropertyAssignment({
name: "theme",
initializer: `{
extend: themeResolver({}, themeConfig)
}`,
});
}

// Get the value of the theme property
const themeValue = (themeProperty as PropertyAssignment).getInitializer();

if (!themeValue) {
throw new Error("Unable to determine theme initializer.");
}

// Check if the theme value is a function call or object literal
if (themeValue.getKind() === SyntaxKind.CallExpression) {
// The theme is resolved using a function call
(themeProperty as PropertyAssignment).setInitializer(`{
extend: themeResolver({}, themeConfig)
}`);
} else if (themeValue.getKind() === SyntaxKind.ObjectLiteralExpression) {
// The theme is a regular object literal
const extendProperty = (themeValue as ObjectLiteralExpression).getProperty(
"extend"
);
if (
extendProperty &&
extendProperty.getKind() === SyntaxKind.PropertyAssignment
) {
// Modify or replace the "extend" property
(extendProperty as PropertyAssignment).setInitializer(
`themeResolver({}, themeConfig)`
);
} else {
// Add "extend" if it doesn't exist
(themeValue as ObjectLiteralExpression).addPropertyAssignment({
name: "extend",
initializer: `themeResolver({}, themeConfig)`,
});
}
} else {
throw new Error("Unsupported initializer type for the theme property.");
}

parser.ensureNamedImport("./theme.config", "themeConfig");
parser.ensureNamedImport("@yext/visual-editor", "themeResolver");

parser.format();
parser.save();
};
2 changes: 1 addition & 1 deletion packages/pages/src/common/src/parsers/templateParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default class TemplateParser {
* @return output and newSfp.
*/
makeClientTemplateFromSfp(newSfp: SourceFileParser) {
const defaultExportName = this.originalSfp.getDefaultExport();
const defaultExportName = this.originalSfp.getDefaultExportName();
const childExpressionNames: string[] = [defaultExportName];
this.originalSfp.getChildExpressions(
defaultExportName,
Expand Down
Loading

0 comments on commit 79a3d6b

Please sign in to comment.