Skip to content

Instantly share code, notes, and snippets.

@rphlmr
Last active September 22, 2022 17:47
Show Gist options
  • Save rphlmr/9708ff3c8d70cc807272330b04353fab to your computer and use it in GitHub Desktop.
Save rphlmr/9708ff3c8d70cc807272330b04353fab to your computer and use it in GitHub Desktop.
Generate React Components with Plop
export function {{pascalCase (parseName name)}}() {
return <></>
}
export function {{camelCase (parseName name)}}() {
return null
}
export function {{pascalCase (parseName name)}}({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
import { createContext, useContext, useMemo } from "react";
const {{pascalCase (parseName name)}}Context = createContext<undefined>(undefined);
export function use{{pascalCase (parseName name)}}() {
const context = useContext({{pascalCase (parseName name)}}Context);
if (!context) {
throw new Error("use{{pascalCase (parseName name)}} must be used within a {{pascalCase (parseName name)}}");
}
return context;
}
export function {{pascalCase (parseName name)}}({
children,
}: {
children: React.ReactNode;
}) {
const value = useMemo(() => ({}), []);
return (
<{{pascalCase (parseName name)}}Context.Provider value={value}>
{children}
</{{pascalCase (parseName name)}}Context.Provider>
);
}
export function {{camelCase (parseName name)}}() {
return null
}
import { createCookieSessionStorage, redirect } from "@remix-run/node";
import { NODE_ENV, safeRedirect, SESSION_SECRET } from "@utils";
type {{pascalCase (parseName name)}} = "IMPLEMENT_ME";
const SESSION_KEY = "{{camelCase (parseName name)}}";
const SESSION_ERROR_KEY = "{{camelCase (parseName name)}}Error";
const SESSION_MAX_AGE = 60 * 60 * 24 * 7; // 7 days;
const sessionStorage = createCookieSessionStorage({
cookie: {
name: "__{{camelCase (parseName name)}}",
httpOnly: true,
path: "/",
sameSite: "lax",
secrets: [SESSION_SECRET],
secure: NODE_ENV === "production",
},
});
export async function create{{pascalCase (parseName name)}}({
request,
{{camelCase (parseName name)}},
redirectTo,
}: {
request: Request;
{{camelCase (parseName name)}}: {{pascalCase (parseName name)}};
redirectTo: string;
}) {
return redirect(safeRedirect(redirectTo), {
headers: {
"Set-Cookie": await commit{{pascalCase (parseName name)}}(request, {
{{camelCase (parseName name)}},
flashErrorMessage: null,
}),
},
});
}
async function getSession(request: Request) {
const cookie = request.headers.get("Cookie");
return sessionStorage.getSession(cookie);
}
export async function get{{pascalCase (parseName name)}}(
request: Request
): Promise<{{pascalCase (parseName name)}} | null> {
const session = await getSession(request);
return session.get(SESSION_KEY);
}
export async function commit{{pascalCase (parseName name)}}(
request: Request,
{
{{camelCase (parseName name)}},
flashErrorMessage,
}: {
{{camelCase (parseName name)}}?: {{pascalCase (parseName name)}} | null;
flashErrorMessage?: string | null;
} = {}
) {
const session = await getSession(request);
// allow session to be null.
// useful you want to clear session and display a message explaining why
if ({{camelCase (parseName name)}} !== undefined) {
session.set(SESSION_KEY, {{camelCase (parseName name)}});
}
session.flash(SESSION_ERROR_KEY, flashErrorMessage);
return sessionStorage.commitSession(session, { maxAge: SESSION_MAX_AGE });
}
export async function destroy{{pascalCase (parseName name)}}(request: Request) {
const session = await getSession(request);
return redirect("/", {
headers: {
"Set-Cookie": await sessionStorage.destroySession(session),
},
});
}

In my Remix Run projects, I want to follow the "barrels" way for my components, layouts, services, hooks, utils, helpers.

components/
| - index.ts // export * from './my-component'
| - my-component/
  | - index.ts // export * from './my-component'
  | - my-component.tsx
| - my-other-component/
  | - index.ts // export * from './my-other-component'
  | - my-other-component.tsx
layouts/
| - index.ts // export * from './my-layout'
| - my-layout/
  | - index.ts // export * from './my-layout'
  | - my-layout.tsx
etc ...

// consume components like that
import { MyComponent, MyOtherComponent } from "~/components"
import { MyLayout } from "~/layouts"

When I create a new {type} (component/layout/service/etc), I want it to be exported in /{type}/index.ts automatically, but not if It's a child {type} 🤯

It can be a pain to write this by hand at every new component, so I use plop (npm i -D plop) with this configuration :

  • plopfile.js in root folder (where package.json is)
  • a templates in root folder with
    • index.hbs This is an Handlebars template for {type}/index.ts and {type}/{{name}}/index.ts, our barrel export
    • index.server.hbs for .server scoped {type} (service and session)
    • component folder containing our component template with
      • {{parseName name}}.tsx.hbs The component template
    • layout folder containing our layout template with
      • {{parseName name}}.tsx.hbs The layout template
    • provider folder containing our react provider template with
      • {{parseName name}}.tsx.hbs The provider template
    • function folder containing our function template (for hooks, services, helpers) with
      • {{parseName name}}.tsx.hbs This is the function template

It's easy to add more template on your need 🚀

Then, enjoy 🦄

npx plop new component my-component

npx plop new component provider my-component/my-provider

npx plop new layout my-layout

npx plop new layout provider my-layout/my-provider

npx plop new hook use-hook

npx plop new service my-service

npx plop new session my-session

PS: if you have a folder you want to "barrel" => npx plop barrel app/my-folder-to-barrel

export * from "./{{parseName name}}";
export * from "./{{parseName name}}.server";
const {
readFileSync,
writeFileSync,
mkdirSync,
existsSync,
readdirSync,
} = require("node:fs");
const path = require("node:path");
/* -------------------------------------------------------------------------- */
/* Cli util to generate components, layout and services */
/* -------------------------------------------------------------------------- */
module.exports = function (/** @type {import('plop').NodePlopAPI} */ plop) {
/* -------------------------------- Helpers; -------------------------------- */
function isChild(text) {
return text.split("/").length >= 2;
}
/* ---------------------------------- Plop Helpers --------------------------------- */
// helper to give me component name from cli input, even handles child component like my-component/sub-component
plop.setHelper("parseName", function (text) {
return text.split("/").pop() || text;
});
/* ------------------------------ Custom action ----------------------------- */
plop.setActionType("sort-index", function (_, { path }) {
const sortedIndex =
Array.from(
readFileSync(path, "utf8")
.split("\n")
.filter((line) => line.trim().length > 0)
.sort()
.reduce((acc, line) => {
return acc.add(line);
}, new Set())
).join("\n") + "\n";
writeFileSync(path, sortedIndex);
});
plop.setActionType("barrel", function ({ destination }, _) {
const generatedIndex =
readdirSync(destination)
.filter(
(file) =>
!(
file.match(".test") ||
file.match(".story") ||
file.match("index.ts") ||
file.match("layouts")
)
)
.map((file) => `export * from "./${path.parse(file).name}";`)
.sort()
.join("\n") + "\n";
writeFileSync(`${destination}/index.ts`, generatedIndex);
});
/* ---------------------------- Create generator ---------------------------- */
const supportedTypes = [
"component",
"function",
"layout",
"provider",
"service",
"session",
];
const serverTypes = ["service", "session"];
function createGenerator(type, destination) {
if (!supportedTypes.includes(type))
throw new Error(`Type ${type} is not supported`);
if (!destination) throw new Error(`Destination is missing`);
// create the initial index if not exists
if (!existsSync(destination)) {
mkdirSync(destination);
}
writeFileSync(`${destination}/index.ts`, "", {
flag: "a",
});
return {
description: `Generate ${type} in ${destination}`,
prompts: [
{
type: "input",
name: "name",
message: `${type} name`,
},
],
actions: [
{
type: "addMany",
destination: `${destination}/{{name}}`,
base: `templates/${type}`,
templateFiles: [`templates/${type}/*`],
},
{
type: "add",
path: `${destination}/{{name}}/index.ts`,
templateFile: `${
serverTypes.includes(type)
? "templates/index.server.hbs"
: "templates/index.hbs"
}`,
},
{
type: "append",
path: `${destination}/index.ts`,
templateFile: `templates/index.hbs`,
separator: "",
skip: ({ name }) =>
isChild(name)
? `skip append in ${destination}/index.ts because it's a child ${type}`
: false,
},
{
type: "sort-index",
path: `${destination}/index.ts`,
skip: ({ name }) =>
isChild(name)
? `skip sorting ${destination}/index.ts because it's a child ${type}`
: false,
},
],
};
}
/* ------------------------------ Plop commands ----------------------------- */
// npx plop new component my-component
plop.setGenerator(
"new component",
createGenerator("component", "app/components")
);
// npx plop new layout provider my-layout/my-provider
plop.setGenerator(
"new component provider",
createGenerator("provider", "app/components")
);
// npx plop new layout my-layout
plop.setGenerator("new layout", createGenerator("layout", "app/components"));
// npx plop new service my-service
plop.setGenerator("new service", createGenerator("service", "app/services"));
// npx plop new hook my-hook
plop.setGenerator("new hook", createGenerator("function", "app/hooks"));
// npx plop new util my-util
plop.setGenerator("new util", createGenerator("function", "app/utils"));
// npx plop new session my-session
plop.setGenerator("new session", createGenerator("session", "app/sessions"));
// npx plop new helper my-helper
plop.setGenerator("new helper", createGenerator("function", "app/helpers"));
// npx plop barrel app/folder-to-migrate
plop.setGenerator("barrel", {
description: `barrel a path`,
prompts: [
{
type: "input",
name: "destination",
message: `destination to barrel`,
},
],
actions: [
{
type: "barrel",
},
],
});
};
@rphlmr
Copy link
Author

rphlmr commented Sep 21, 2022

This is my template needs. Make yours ⚡️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment