Skip to content

Commit

Permalink
add themeConfig, TWConfig, buildSchema
Browse files Browse the repository at this point in the history
  • Loading branch information
mkilpatrick committed Jan 17, 2025
1 parent bbdf72a commit 84f2822
Show file tree
Hide file tree
Showing 7 changed files with 681 additions and 9 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
39 changes: 39 additions & 0 deletions packages/pages/src/scaffold/template/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ import { ProjectStructure } from "../../common/src/project/structure.js";
import path from "node:path";
import fs from "node:fs";
import {
buildSchemaUtil,
dynamicTemplate,
newConfigFile,
staticTemplate,
tailwindConfig,
veThemeConfig,
visualEditorTemplateCode,
} from "./sampleTemplates.js";
import { addDataToPuckConfig } from "../../common/src/parsers/puckConfigParser.js";
Expand All @@ -14,6 +17,7 @@ import {
updatePackageDependency,
} from "../../upgrade/pagesUpdater.js";
import { logErrorAndExit } from "../../util/logError.js";
import { addThemeConfigToTailwind } from "../../common/src/parsers/tailwindConfigParser.js";

export const generateTemplate = async (
projectStructure: ProjectStructure
Expand Down Expand Up @@ -154,6 +158,9 @@ const generateVETemplate = async (
visualEditorTemplateCode(templateFileName)
);
addVETemplateToConfig(templateFileName, projectStructure);
addVEThemeConfig();
addTailwindConfig();
addBuildSchemaUtil(projectStructure);

try {
await addVEDependencies();
Expand All @@ -177,6 +184,38 @@ const addVETemplateToConfig = (
}
};

const addVEThemeConfig = () => {
const themeConfigPath = path.join("ve.config.tsx");
if (fs.existsSync(themeConfigPath)) {
return;
}

fs.writeFileSync(themeConfigPath, veThemeConfig);
};

const addTailwindConfig = () => {
const tailwindConfigPath = path.join("tailwind.config.ts");
if (fs.existsSync(tailwindConfigPath)) {
addThemeConfigToTailwind(tailwindConfigPath);
return;
}

fs.writeFileSync(tailwindConfigPath, tailwindConfig);
};

const addBuildSchemaUtil = (projectStructure: ProjectStructure) => {
const buildSchemaUtilPath = path.join(
projectStructure.config.rootFolders.source,
"utils",
"buildSchema.ts"
);
if (fs.existsSync(buildSchemaUtilPath)) {
return;
}

fs.writeFileSync(buildSchemaUtilPath, buildSchemaUtil);
};

const addVEDependencies = async () => {
await updatePackageDependency("@yext/visual-editor", null, true);
await updatePackageDependency(
Expand Down
Loading

0 comments on commit 84f2822

Please sign in to comment.