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

Add react-router-monorepo template #924

Merged
merged 3 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 3 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ jobs:
- run: |
cd packages/bento-design-system
pnpm version --no-git-tag-version --new-version ${{ github.ref_name }}
new_version=$(cat package.json | jq -r '.version')
cd ../../templates/react-router-monorepo/libs/design-system
jq '.dependencies["@buildo/bento-design-system"] = "'${new_version}'"' package.json > package.json.tmp && mv package.json.tmp package.json

- name: Commit & Push changes
uses: actions-js/push@master
Expand Down
4 changes: 4 additions & 0 deletions templates/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package-lock.json
yarn.lock
pnpm-lock.yaml
pnpm-lock.yml
19 changes: 19 additions & 0 deletions templates/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Bento project templates

This directory contains project templates that can be used to bootstrap new projects using Bento.

## How to use a template

```bash
pnpx degit buildo/bento-design-system/templates/<template-name> [my-new-project]
```

Omitting the project name will clone the template in the current directory.

## Available templates

- [react-router-monorepo](./react-router-monorepo/README.md): sets up a monorepo using pnpm and Nx. The monorepo contains an app and a design system library. The app uses React Router v7 (with SSR enabled) and it comes with i18n pre-configured.

```bash
pnpx degit buildo/bento-design-system/templates/react-router-monorepo my-new-project
```
9 changes: 9 additions & 0 deletions templates/react-router-monorepo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules

/.cache
/build
.env
.react-router

.nx/cache
.nx/workspace-data
4 changes: 4 additions & 0 deletions templates/react-router-monorepo/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"typescript.tsdk": "apps/app/node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
}
40 changes: 40 additions & 0 deletions templates/react-router-monorepo/apps/app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Welcome to React Router!

- 📖 [React Router docs](https://reactrouter.com/dev)

## Development

Run the dev server:

```shellscript
npm run dev
```

## Deployment

First, build your app for production:

```sh
npm run build
```

Then run the app in production mode:

```sh
npm start
```

Now you'll need to pick a host to deploy it to.

### DIY

If you're familiar with deploying Node applications, the built-in app server is production-ready.

Make sure to deploy the output of `npm run build`

- `build/server`
- `build/client`

## Styling

This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.
56 changes: 56 additions & 0 deletions templates/react-router-monorepo/apps/app/app/defaultMessages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { ComponentProps } from "react";
import { BentoProvider } from "design-system";
import { useTranslation } from "react-i18next";

export const useDefaultMessages = (): ComponentProps<typeof BentoProvider>["defaultMessages"] => {
const { t } = useTranslation();

return {
Chip: {
dismissButtonLabel: t("common.chip.dismissButtonLabel", "Remove"),
},
Banner: {
dismissButtonLabel: t("common.banner.dismissButtonLabel", "Close"),
},
Modal: {
closeButtonLabel: t("common.modal.closeButtonLabel", "Close"),
},
SelectField: {
noOptionsMessage: t("common.selectField.noOptionsMessage", "No options"),
multiOptionsSelected: (n) => {
const options =
n > 1
? t("common.selectField.optionsPlural", "options")
: t("common.selectField.optionsSingular", "option");
return t("common.selectField.multiOptionsSelected", "{{n}} {{options}} selected", {
n,
options,
});
},
selectAllButtonLabel: t("common.selectField.selectAllButtonLabel", "Select all"),
clearAllButtonLabel: t("common.selectField.clearAllButtonLabel", "Clear all"),
},
SearchBar: {
clearButtonLabel: t("common.searchBar.clearButtonLabel", "Clear"),
},
Table: {
noResultsTitle: t("common.table.noResultsTitle", "No results found"),
noResultsDescription: t(
"common.table.noResultsDescription",
"Try adjusting your search filters to find what you're looking for."
),
missingValue: t("common.table.missingValue", "-"),
},
Loader: {
loadingMessage: t("common.loader.loadingMessage", "Loading..."),
},
DateField: {
previousMonthLabel: t("common.dateField.previousMonthLabel", "Prev month"),
nextMonthLabel: t("common.dateField.nextMonthLabel", "Next month"),
},
TextField: {
showPasswordLabel: t("common.textField.showPasswordLabel", "Show password"),
hidePasswordLabel: t("common.textField.hidePasswordLabel", "Hide password"),
},
};
};
41 changes: 41 additions & 0 deletions templates/react-router-monorepo/apps/app/app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { HydratedRouter } from "react-router/dom";
import i18n, { registerCustomFormats } from "./i18n";
import i18next from "i18next";
import { I18nextProvider, initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import Backend from "i18next-http-backend";

await i18next
.use(initReactI18next) // Tell i18next to use the react-i18next plugin
.use(LanguageDetector) // Setup a client-side language detector
.use(Backend) // Setup your backend
.init({
...i18n, // spread the configuration
// This function detects the namespaces your routes rendered while SSR use
ns: [],
backend: { loadPath: "/locales/{{lng}}.json" },
detection: {
// Here only enable htmlTag detection, we'll detect the language only
// server-side with remix-i18next, by using the `<html lang>` attribute
// we can communicate to the client the language detected server-side
order: ["htmlTag"],
// Because we only use htmlTag, there's no reason to cache the language
// on the browser, so we disable it
caches: [],
},
});

registerCustomFormats(i18next);

startTransition(() => {
hydrateRoot(
document,
<I18nextProvider i18n={i18next}>
<StrictMode>
<HydratedRouter />
</StrictMode>
</I18nextProvider>
);
});
103 changes: 103 additions & 0 deletions templates/react-router-monorepo/apps/app/app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { PassThrough } from "node:stream";

import type { AppLoadContext, EntryContext } from "react-router";
import { createReadableStreamFromReadable } from "@react-router/node";
import { ServerRouter } from "react-router";
import { isbot } from "isbot";
import type { RenderToPipeableStreamOptions } from "react-dom/server";
import { renderToPipeableStream } from "react-dom/server";
import { createInstance } from "i18next";
import i18next from "./i18next.server";
import { I18nextProvider, initReactI18next } from "react-i18next";
import Backend from "i18next-fs-backend";
import i18n, { registerCustomFormats } from "./i18n"; // your i18n configuration file
import * as path from "node:path";

const ABORT_DELAY = 5_000;

// Override console.erro to suppress specific warnings
const originalConsoleError = console.error;
console.error = (msg, ...args) => {
if (typeof msg === "string" && msg.includes("useLayoutEffect")) {
return;
}
if (typeof msg === "string" && msg.includes("A props object containing")) {
return;
}
originalConsoleError(msg, ...args);
};

export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
routerContext: EntryContext,
loadContext: AppLoadContext
) {
return new Promise(async (resolve, reject) => {
let shellRendered = false;
let userAgent = request.headers.get("user-agent");

// Ensure requests from bots and SPA Mode renders wait for all content to load before responding
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
let readyOption: keyof RenderToPipeableStreamOptions =
(userAgent && isbot(userAgent)) || routerContext.isSpaMode ? "onAllReady" : "onShellReady";

let instance = createInstance();
let lng = await i18next.getLocale(request);
// let ns = i18next.getRouteNamespaces(routerContext);
let ns = ["translation"] as string[];

await instance
.use(initReactI18next) // Tell our instance to use react-i18next
.use(Backend) // Setup our backend
.init({
...i18n, // spread the configuration
lng, // The locale we detected above
ns, // The namespaces the routes about to render wants to use
backend: {
loadPath: path.resolve("./public/locales/{{lng}}.json"),
},
});

registerCustomFormats(instance);

const { pipe, abort } = renderToPipeableStream(
<I18nextProvider i18n={instance}>
<ServerRouter context={routerContext} url={request.url} abortDelay={ABORT_DELAY} />,
</I18nextProvider>,
{
[readyOption]() {
shellRendered = true;
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);

responseHeaders.set("Content-Type", "text/html");

resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
})
);

pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
}
);

setTimeout(abort, ABORT_DELAY);
});
}
22 changes: 22 additions & 0 deletions templates/react-router-monorepo/apps/app/app/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { i18n, InitOptions } from "i18next";
import type en from "../public/locales/en.json";

export default {
supportedLngs: ["en", "it"],
fallbackLng: "en",
} satisfies InitOptions;

export function registerCustomFormats(i18n: i18n) {
i18n.services.formatter?.add("capitalize", (value: string) => {
return value.charAt(0).toUpperCase() + value.slice(1);
});
}

declare module "i18next" {
interface CustomTypeOptions {
defaultNS: "translation";
resources: {
translation: typeof en;
};
}
}
25 changes: 25 additions & 0 deletions templates/react-router-monorepo/apps/app/app/i18next.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Backend from "i18next-fs-backend";
import { resolve } from "node:path";
import { RemixI18Next } from "remix-i18next/server";
import i18n from "./i18n";

const i18next = new RemixI18Next({
detection: {
supportedLanguages: i18n.supportedLngs,
fallbackLanguage: i18n.fallbackLng,
},
// This is the configuration for i18next used
// when translating messages server-side only
i18next: {
...i18n,
backend: {
loadPath: resolve("./public/locales/{{lng}}.json"),
},
},
// The i18next plugins you want RemixI18next to use for `i18n.getFixedT` inside loaders and actions.
// E.g. The Backend plugin for loading translations from the file system
// Tip: You could pass `resources` to the `i18next` configuration and avoid a backend here
plugins: [Backend],
});

export default i18next;
3 changes: 3 additions & 0 deletions templates/react-router-monorepo/apps/app/app/root.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
body {
height: 100vh;
}
Loading
Loading