Skip to content

Commit

Permalink
feat(plugin): create and bundle client templates (#420)
Browse files Browse the repository at this point in the history
Co-authored-by: Matt Kilpatrick <mkilpatrick@yext.com>
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Nov 10, 2023
1 parent 827c8db commit ba4647a
Show file tree
Hide file tree
Showing 19 changed files with 314 additions and 133 deletions.
6 changes: 6 additions & 0 deletions packages/pages/src/build/build.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { Command } from "commander";
import { build } from "vite";
import { ProjectStructure } from "../common/src/project/structure.js";
import { makeClientFiles } from "../common/src/template/client.js";

const handler = async ({ scope }: { scope: string }) => {
// Pass CLI arguments as env variables to use in vite-plugin
if (scope) {
process.env.YEXT_PAGES_SCOPE = scope;
}
const projectStructure = await ProjectStructure.init({
scope: scope,
});
await makeClientFiles(projectStructure);
await build();
};

Expand Down
6 changes: 5 additions & 1 deletion packages/pages/src/common/src/project/structure.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import pathLib from "path";
import pathLib from "node:path";
import merge from "lodash/merge.js";
import { Path } from "./path.js";
import { determineAssetsFilepath } from "../assets/getAssetsFilepath.js";
Expand Down Expand Up @@ -27,6 +27,8 @@ export interface Subfolders {
serverlessFunctions: string; // Node functions
/** Where to output the bundled static assets */
assets: string;
/** Where to output the client bundles */
clientBundle: string;
/** Where to output the server bundles */
serverBundle: string;
/** Where to output the render bundles */
Expand Down Expand Up @@ -114,6 +116,7 @@ export interface ProjectStructureConfig {
rootFiles: RootFiles;
/** Defines how environment variables will be declared and processed */
envVarConfig: EnvVar;

/**
* This is used for the case of multibrand setup within a single repo.
*
Expand All @@ -136,6 +139,7 @@ const defaultProjectStructureConfig: ProjectStructureConfig = {
templates: "templates",
serverlessFunctions: "functions",
assets: DEFAULT_ASSETS_DIR,
clientBundle: "client",
serverBundle: "server",
renderBundle: "render",
renderer: "renderer",
Expand Down
100 changes: 100 additions & 0 deletions packages/pages/src/common/src/template/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import fs from "node:fs";
import path from "node:path";
import SourceFileParser, {
createTsMorphProject,
} from "../parsers/sourceFileParser.js";
import TemplateParser from "../parsers/templateParser.js";
import { ProjectStructure } from "../project/structure.js";
import { readdir } from "node:fs/promises";
import { logErrorAndExit } from "../../../util/logError.js";

/**
* Creates a corresponding template.client.tsx for each template.tsx.
* @param projectStructure
*/
export const makeClientFiles = async (projectStructure: ProjectStructure) => {
try {
const templatePaths = projectStructure.getTemplatePaths();
for (const templatePath of templatePaths) {
(await readdir(templatePath.getAbsolutePath(), { withFileTypes: true }))
.filter((dirent) => !dirent.isDirectory())
.map((file) => file.name)
.filter(
(f) =>
f !== "_client17.tsx" &&
f !== "_client.tsx" &&
f !== "_server.tsx" &&
!f.includes(".client")
)
.forEach(async (template) => {
const templeFilePath = path.join(templatePath.path, template);
generateAndSaveClientHydrationTemplates(templeFilePath);
});
}
} catch (err) {
logErrorAndExit("Failed to make client templates.");
await removeHydrationClientFiles(projectStructure);
}
};

/**
* Reads template file and parses code for the hydration client file.
* Saves parsed code into file at path.
* @param path src/templates/<templateName>.tsx
*/
const generateAndSaveClientHydrationTemplates = (templatePath: string) => {
const clientTemplatePath = getClientPath(templatePath);
fs.writeFileSync(clientTemplatePath, "");
const sfp = new SourceFileParser(templatePath, createTsMorphProject());
const newSfp = new SourceFileParser(
clientTemplatePath,
createTsMorphProject()
);
const templateParser = new TemplateParser(sfp).makeClientTemplateFromSfp(
newSfp
);
templateParser.sourceFile.save();
};

/**
* @param templatePath /src/templates/location.tsx
* @returns clientPath /src/templates/location.client.tsx
*/
const getClientPath = (templatePath: string): string => {
const parsedPath = path.parse(templatePath);
parsedPath.name = parsedPath.name + ".client";
parsedPath.base = parsedPath.name + parsedPath.ext;
return path.format(parsedPath);
};

/**
* Removes any generated [template].client files.
* @param projectStructure
*/
export const removeHydrationClientFiles = async (
projectStructure: ProjectStructure
) => {
const templatePaths = projectStructure.getTemplatePaths();
for (const templatePath of templatePaths) {
(await readdir(templatePath.getAbsolutePath(), { withFileTypes: true }))
.filter((dirent) => !dirent.isDirectory())
.map((file) => file.name)
.filter(
(f) =>
f !== "_client17.tsx" &&
f !== "_client.tsx" &&
f !== "_server.tsx" &&
f.includes(".client")
)
.forEach(async (template) => {
const clientPath = path.join(
projectStructure.config.rootFolders.source,
projectStructure.config.subfolders.templates,
template
);
if (fs.existsSync(clientPath)) {
fs.rmSync(clientPath);
}
});
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export const getTemplateFilepaths = (paths: Path[]): string[] => {
.filter(
(f) =>
f.indexOf(globalClientRenderFilename) === -1 &&
f.indexOf(globalServerRenderFilename) === -1
f.indexOf(globalServerRenderFilename) === -1 &&
f.indexOf(globalHydrationClientFilename) === -1
)
.forEach((f) => {
const fileName = path.basename(f);
Expand All @@ -51,6 +52,7 @@ export const getTemplateFilepathsFromProjectStructure = (
const globalClientRenderFilename17 = "_client17.tsx";
const globalClientRenderFilename = "_client.tsx";
const globalServerRenderFilename = "_server.tsx";
const globalHydrationClientFilename = ".client";

/**
* Determines the client and server rendering templates to use. It first looks for a _client/server.tsx file in the scoped
Expand Down
8 changes: 6 additions & 2 deletions packages/pages/src/common/src/template/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,12 @@ export interface Stream {
* @public
*/
export type Manifest = {
/** A map of feature name to the bundle path of the feature */
bundlePaths: {
/** A map of feature name to the server path of the feature */
serverPaths: {
[key: string]: string;
};
/** A map of feature name to the client path of the feature */
clientPaths: {
[key: string]: string;
};
/** A map of render template to its bundle path */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe("createArtifactsJson - getArtifactsConfig", () => {
{
root: "dist",
pattern:
"assets/{server,static,renderer,render}/**/*{.js,.css}",
"assets/{server,static,renderer,render,client}/**/*{.js,.css}",
},
],
event: "ON_PAGE_GENERATE",
Expand Down
3 changes: 2 additions & 1 deletion packages/pages/src/generate/artifacts/createArtifactsJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ const getGeneratorPlugin = (projectStructure: ProjectStructure): Plugin => {
const {
assets,
renderer,
clientBundle,
serverBundle,
static: _static,
renderBundle,
Expand All @@ -119,7 +120,7 @@ const getGeneratorPlugin = (projectStructure: ProjectStructure): Plugin => {
},
{
root: `${rootFolders.dist}`,
pattern: `${assets}/{${serverBundle},${_static},${renderer},${renderBundle}}/**/*{.js,.css}`,
pattern: `${assets}/{${serverBundle},${_static},${renderer},${renderBundle},${clientBundle}}/**/*{.js,.css}`,
},
],
event: "ON_PAGE_GENERATE",
Expand Down
4 changes: 2 additions & 2 deletions packages/pages/src/generate/ci/ci.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ describe("ci - getUpdatedCiConfig", () => {
{
root: "dist",
pattern:
"assets/{server,static,renderer,render}/**/*{.js,.css}",
"assets/{server,static,renderer,render,client}/**/*{.js,.css}",
},
],
event: "ON_PAGE_GENERATE",
Expand Down Expand Up @@ -142,7 +142,7 @@ describe("ci - getUpdatedCiConfig", () => {
{
root: "dist",
pattern:
"assets/{server,static,renderer,render}/**/*{.js,.css}",
"assets/{server,static,renderer,render,client}/**/*{.js,.css}",
},
],
event: "ON_PAGE_GENERATE",
Expand Down
3 changes: 2 additions & 1 deletion packages/pages/src/generate/ci/ci.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ const getGeneratorPlugin = (projectStructure: ProjectStructure): Plugin => {
const {
assets,
renderer,
clientBundle,
serverBundle,
static: _static,
renderBundle,
Expand All @@ -181,7 +182,7 @@ const getGeneratorPlugin = (projectStructure: ProjectStructure): Plugin => {
},
{
root: `${rootFolders.dist}`,
pattern: `${assets}/{${serverBundle},${_static},${renderer},${renderBundle}}/**/*{.js,.css}`,
pattern: `${assets}/{${serverBundle},${_static},${renderer},${renderBundle},${clientBundle}}/**/*{.js,.css}`,
},
],
event: "ON_PAGE_GENERATE",
Expand Down
10 changes: 10 additions & 0 deletions packages/pages/src/util/logError.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import colors from "picocolors";
import { ProjectStructure } from "../common/src/project/structure.js";
import { removeHydrationClientFiles } from "../common/src/template/client.js";

/**
* Logs the provided error and exits the program.
Expand All @@ -15,3 +17,11 @@ export const logErrorAndExit = (error: string | any) => {
export const logWarning = (warning: string) => {
console.warn(colors.yellow(`WARNING: ${warning}`));
};

export const logErrorAndClean = async (
error: string | any,
projectStructure: ProjectStructure
) => {
await removeHydrationClientFiles(projectStructure);
logErrorAndExit(error);
};
83 changes: 3 additions & 80 deletions packages/pages/src/vite-plugin/build/build.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import { Plugin, UserConfig } from "vite";
import buildStart from "./buildStart/buildStart.js";
import closeBundle from "./closeBundle/closeBundle.js";
import path, { parse } from "path";
import { InputOption } from "rollup";
import { ProjectStructure } from "../../common/src/project/structure.js";
import { readdir } from "fs/promises";
import { processEnvVariables } from "../../util/processEnvVariables.js";
import { getGlobalClientServerRenderTemplates } from "../../common/src/template/internal/getTemplateFilepaths.js";
import { Path } from "../../common/src/project/path.js";

const intro = `
var global = globalThis;
Expand All @@ -18,7 +13,9 @@ var global = globalThis;
* assets, copies Yext plugin files that execute the bundled assets in a Deno
* environment, and puts them all in an output directory.
*/
export const build = (projectStructure: ProjectStructure): Plugin => {
export const build = async (
projectStructure: ProjectStructure
): Promise<Plugin> => {
const { envVarConfig, subfolders } = projectStructure.config;

return {
Expand All @@ -36,10 +33,6 @@ export const build = (projectStructure: ProjectStructure): Plugin => {
manifest: true,
rollupOptions: {
preserveEntrySignatures: "strict",
input: await discoverInputs(
projectStructure.getTemplatePaths(),
projectStructure
),
output: {
intro,
assetFileNames: `${subfolders.assets}/${subfolders.static}/[name]-[hash][extname]`,
Expand All @@ -64,73 +57,3 @@ export const build = (projectStructure: ProjectStructure): Plugin => {
closeBundle: closeBundle(projectStructure),
};
};

/**
* Produces a {@link InputOption} by adding all templates at {@link rootTemplateDir} and
* {@link scopedTemplateDir} to be output at {@code server/}. If there are two files
* that share the same name between the two provided template folders, only the file
* in scoped template folder path is included. Also adds an additional entry-point
* for all templates ending in tsx to be used to hydrate the bundle.
*
* @param rootTemplateDir the directory where all templates are stored.
* @param scopedTemplateDir the directory where a subset of templates use for the build are stored.
* @param projectStructure
* @returns
*/
const discoverInputs = async (
templatePaths: Path[],
projectStructure: ProjectStructure
): Promise<InputOption> => {
const entryPoints: Record<string, string> = {};
const updateEntryPoints = async (dir: string) =>
(await readdir(dir, { withFileTypes: true }))
.filter((dirent) => !dirent.isDirectory())
.map((file) => file.name)
.filter(
(f) =>
f !== "_client17.tsx" && f !== "_client.tsx" && f !== "_server.tsx"
)
.forEach((template) => {
const parsedPath = parse(template);
const outputPath = `${projectStructure.config.subfolders.serverBundle}/${parsedPath.name}`;
if (entryPoints[outputPath]) {
return;
}
entryPoints[outputPath] = path.join(dir, template);
});

for (const templatePath of templatePaths) {
await updateEntryPoints(templatePath.getAbsolutePath());
}

return { ...entryPoints, ...discoverRenderTemplates(projectStructure) };
};

/**
* Produces the entry points for the client and server render templates to be output at
* {@code render/}.
*
* @param projectStructure
*/
const discoverRenderTemplates = (
projectStructure: ProjectStructure
): Record<string, string> => {
const entryPoints: Record<string, string> = {};

// Move the [compiled] _server.ts and _client.ts render template to /assets/render
const clientServerRenderTemplates = getGlobalClientServerRenderTemplates(
projectStructure.getTemplatePaths()
);

const { renderBundle } = projectStructure.config.subfolders;

// server
entryPoints[`${renderBundle}/_server`] =
clientServerRenderTemplates.serverRenderTemplatePath;

// client
entryPoints[`${renderBundle}/_client`] =
clientServerRenderTemplates.clientRenderTemplatePath;

return entryPoints;
};
Loading

0 comments on commit ba4647a

Please sign in to comment.