├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .nvmrc ├── docker ├── .dockerignore ├── docker-compose.yml ├── docker-compose.db.yml └── README.md ├── .github ├── FUNDING.yml └── workflows │ ├── codesee-arch-diagram.yml │ ├── ci-codegen.yml │ └── clean-up-pr-caches.yml ├── apps ├── vite-app │ ├── src │ │ ├── vite-env.d.ts │ │ ├── types.d │ │ │ └── emotion-css.d.ts │ │ ├── main.tsx │ │ ├── index.css │ │ ├── App.css │ │ └── App.tsx │ ├── .gitignore │ ├── index.html │ ├── tsconfig.json │ ├── .eslintrc.cjs │ ├── vite.config.ts │ └── lint-staged.config.js └── nextjs-app │ ├── src │ ├── layouts │ │ ├── main │ │ │ ├── index.ts │ │ │ ├── MainLayout.tsx │ │ │ ├── __tests__ │ │ │ │ └── MainLayout.test.tsx │ │ │ └── MainFooter.tsx │ │ └── admin │ │ │ ├── index.ts │ │ │ └── AdminLayout.tsx │ ├── features │ │ ├── demo │ │ │ ├── pages │ │ │ │ └── index.ts │ │ │ ├── components │ │ │ │ ├── index.ts │ │ │ │ └── PoemGrid.tsx │ │ │ ├── blocks │ │ │ │ ├── index.ts │ │ │ │ └── poetry │ │ │ │ │ └── PoetryBlock.tsx │ │ │ ├── demo.config.ts │ │ │ └── api │ │ │ │ └── fetch-poems.api.ts │ │ ├── home │ │ │ ├── pages │ │ │ │ ├── index.ts │ │ │ │ └── HomePage.tsx │ │ │ ├── blocks │ │ │ │ └── index.ts │ │ │ └── home.config.ts │ │ ├── admin │ │ │ ├── layouts │ │ │ │ ├── index.ts │ │ │ │ └── AdminLayout.tsx │ │ │ ├── pages │ │ │ │ ├── index.ts │ │ │ │ └── AdminMainPage.tsx │ │ │ └── admin.config.ts │ │ ├── system │ │ │ ├── pages │ │ │ │ ├── index.ts │ │ │ │ ├── __tests__ │ │ │ │ │ ├── NotFoundPage.test.tsx │ │ │ │ │ └── ErrorPage.test.tsx │ │ │ │ ├── NotFoundPage.tsx │ │ │ │ └── ErrorPage.tsx │ │ │ └── system.config.ts │ │ └── auth │ │ │ ├── auth.config.ts │ │ │ └── pages │ │ │ └── LoginPage.tsx │ ├── lib │ │ ├── emotion │ │ │ ├── index.ts │ │ │ └── createEmotionCache.ts │ │ ├── auth │ │ │ └── error │ │ │ │ ├── index.ts │ │ │ │ ├── authErrorCodes.ts │ │ │ │ └── createHttpUnauthorized.ts │ │ ├── i18n │ │ │ ├── I18nNamespace.types.ts │ │ │ └── getSortedI18nNamespaces.ts │ │ ├── env │ │ │ ├── index.mjs │ │ │ ├── __tests__ │ │ │ │ └── utils.test.ts │ │ │ └── getValidatedBuildEnv.mjs │ │ └── factory │ │ │ └── ky.factory.ts │ ├── styles │ │ └── global.css │ ├── server │ │ ├── config │ │ │ ├── container.config.ts │ │ │ ├── index.ts │ │ │ ├── cors.config.ts │ │ │ └── __tests__ │ │ │ │ └── cors.config.test.ts │ │ ├── features │ │ │ └── poem │ │ │ │ └── SearchPoems │ │ │ │ ├── index.ts │ │ │ │ └── SearchPoems.types.ts │ │ └── i18n │ │ │ └── getServerTranslations.ts │ ├── pages │ │ ├── api │ │ │ ├── auth │ │ │ │ └── [...nextauth].ts │ │ │ ├── hello.ts │ │ │ ├── _monitor │ │ │ │ ├── sentry.ts │ │ │ │ └── healthcheck.ts │ │ │ └── rest │ │ │ │ └── post │ │ │ │ └── index.ts │ │ ├── _monitor │ │ │ ├── preview │ │ │ │ └── error-page.tsx │ │ │ └── sentry │ │ │ │ ├── csr-page.tsx │ │ │ │ └── ssr-page.tsx │ │ ├── 404.tsx │ │ ├── index.tsx │ │ ├── auth │ │ │ └── login.tsx │ │ ├── home.tsx │ │ ├── admin │ │ │ └── index.tsx │ │ └── _app.tsx │ ├── config │ │ ├── react-query.config.ts │ │ ├── app-cache.config.tsx │ │ └── api-fetcher.config.ts │ ├── themes │ │ ├── tailwind │ │ │ └── tailwind.theme.js │ │ ├── mui │ │ │ └── mui.theme.ts │ │ └── shared │ │ │ ├── __tests__ │ │ │ └── colors.test.ts │ │ │ ├── browser-fonts.js │ │ │ └── colors.js │ ├── types.d │ │ ├── i18next.d.ts │ │ ├── react-svgr.d.ts │ │ ├── env.d.ts │ │ └── next-auth.d.ts │ ├── providers │ │ └── ReactQueryClientProvider.tsx │ ├── components │ │ └── avatar │ │ │ └── TextAvatar.tsx │ └── middleware.ts │ ├── config │ └── tests │ │ ├── setupVitest.ts │ │ ├── ReactSvgrMock.tsx │ │ ├── AppTestProviders.tsx │ │ ├── I18nextTestStubProvider.tsx │ │ └── test-utils.tsx │ ├── .escheckrc │ ├── public │ ├── images │ │ ├── paella.jpg │ │ ├── lizard.webp │ │ ├── nextjs-logo.png │ │ ├── favicon │ │ │ ├── favicon.ico │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── mstile-150x150.png │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── browserconfig.xml │ │ │ ├── site.webmanifest │ │ │ └── safari-pinned-tab.svg │ │ ├── anims │ │ │ └── flow-giphy.webp │ │ └── layout │ │ │ └── footer-waves.svg │ └── assets │ │ └── annie-spratt-unsplash.jpg │ ├── .env.development │ ├── next-env.d.ts │ ├── e2e │ ├── api │ │ ├── _monitor │ │ │ ├── sentry.spec.ts │ │ │ └── healthcheck.spec.ts │ │ └── graphql │ │ │ └── index.spec.ts │ ├── pages │ │ ├── _monitor │ │ │ └── sentry.spec.ts │ │ ├── index │ │ │ └── index.spec.ts │ │ └── system │ │ │ └── 404.spec.ts │ └── README.md │ ├── .gitignore │ ├── turbo.json │ ├── postcss.config.cjs │ ├── next-i18next.config.mjs │ ├── LICENSE │ ├── .size-limit.cjs │ └── lint-staged.config.js ├── packages ├── ts-utils │ ├── src │ │ ├── asserts │ │ │ ├── index.ts │ │ │ └── __tests__ │ │ │ │ └── asserts.test.ts │ │ ├── convert │ │ │ ├── index.ts │ │ │ └── string-convert.ts │ │ ├── typeguards │ │ │ └── index.ts │ │ ├── array │ │ │ ├── index.ts │ │ │ └── ArrayUtils.ts │ │ ├── random │ │ │ ├── index.ts │ │ │ ├── __tests__ │ │ │ │ └── getRandomInt.test.ts │ │ │ └── getRandomInt.ts │ │ ├── types │ │ │ ├── NonEmptyArray.ts │ │ │ ├── Unpromisify.ts │ │ │ └── index.ts │ │ └── index.ts │ ├── .escheckrc │ ├── .size-limit.cjs │ ├── .gitignore │ ├── tsup.config.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── vitest.config.ts │ ├── .eslintrc.cjs │ ├── LICENSE │ ├── README.md │ └── lint-staged.config.js ├── common-i18n │ ├── src │ │ ├── index.ts │ │ ├── locales │ │ │ ├── en │ │ │ │ ├── admin.json │ │ │ │ ├── blog.json │ │ │ │ ├── navigation.json │ │ │ │ ├── demo.json │ │ │ │ ├── system.json │ │ │ │ ├── auth.json │ │ │ │ ├── home.json │ │ │ │ └── common.json │ │ │ └── fr │ │ │ │ ├── admin.json │ │ │ │ ├── blog.json │ │ │ │ ├── navigation.json │ │ │ │ ├── demo.json │ │ │ │ ├── auth.json │ │ │ │ ├── system.json │ │ │ │ ├── home.json │ │ │ │ └── common.json │ │ └── I18nResources.types.ts │ ├── .gitignore │ ├── tsconfig.json │ ├── .eslintrc.cjs │ ├── LICENSE │ ├── package.json │ └── lint-staged.config.js ├── ui-lib │ ├── src │ │ ├── _stories │ │ │ ├── tailwind.css │ │ │ ├── header.css │ │ │ ├── button.css │ │ │ ├── Page.stories.ts │ │ │ ├── Header.stories.ts │ │ │ ├── Button.stories.ts │ │ │ ├── Button.tsx │ │ │ └── assets │ │ │ │ └── direction.svg │ │ ├── base │ │ │ ├── index.ts │ │ │ ├── card │ │ │ │ ├── BasicCard.styles.tsx │ │ │ │ ├── BasicCard.tsx │ │ │ │ └── BasicCard.stories.tsx │ │ │ └── button │ │ │ │ ├── Button.styles.tsx │ │ │ │ ├── Button.stories.tsx │ │ │ │ └── Button.tsx │ │ ├── ux │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── types.d │ │ │ └── emotion-css.d.ts │ │ ├── icons │ │ │ └── social │ │ │ │ └── README.md │ │ ├── message.tsx │ │ └── async-message.tsx │ ├── vite.config.storybook.ts │ ├── config │ │ └── test │ │ │ ├── setupVitest.ts │ │ │ └── test-utils.ts │ ├── postcss.config.cjs │ ├── .gitignore │ ├── README.md │ ├── .storybook │ │ ├── preview.ts │ │ └── main.ts │ ├── tsup.config.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── LICENSE │ ├── vitest.config.ts │ └── lint-staged.config.js ├── db-main-prisma │ ├── .env.development │ ├── src │ │ ├── seeds │ │ │ ├── index.ts │ │ │ └── UserSeeds.ts │ │ ├── index.ts │ │ └── lib │ │ │ └── AbstractSeed.ts │ ├── .env │ ├── prisma │ │ ├── migrations │ │ │ └── migration_lock.toml │ │ └── seed.ts │ ├── turbo.json │ ├── .gitignore │ ├── tsup.config.ts │ ├── tsconfig.json │ ├── tsconfig.build.json │ ├── docker-compose.e2e.yml │ ├── vitest.config.ts │ ├── lint-staged.config.js │ ├── LICENSE │ ├── .eslintrc.cjs │ └── e2e │ │ └── e2e-dsn-services.util.ts ├── eslint-config-bases │ ├── src │ │ ├── index.js │ │ ├── patch │ │ │ └── modern-module-resolution.js │ │ ├── helpers │ │ │ ├── getPrettierConfig.js │ │ │ ├── index.js │ │ │ └── getDefaultIgnorePatterns.js │ │ ├── bases │ │ │ ├── prettier-config.js │ │ │ ├── simple-import-sort.js │ │ │ ├── perfectionist-jsx.js │ │ │ ├── prettier-plugin.js │ │ │ ├── performance.js │ │ │ ├── graphql-schema.js │ │ │ ├── regexp.js │ │ │ ├── mdx.js │ │ │ ├── import-x.js │ │ │ ├── rtl.js │ │ │ ├── react-query.js │ │ │ ├── playwright.js │ │ │ ├── index.js │ │ │ ├── tailwind.js │ │ │ ├── storybook.js │ │ │ └── perfectionist.js │ │ ├── prettier.base.config.js │ │ └── config │ │ │ └── file-patterns.js │ ├── .escheckrc │ ├── .prettierrc.js │ ├── tsconfig.json │ └── .eslintrc.cjs └── core-lib │ ├── src │ ├── hooks │ │ ├── index.ts │ │ ├── use-deep-compare-memoize.ts │ │ └── __tests__ │ │ │ └── use-deep-compare-memoize.test.ts │ ├── index.ts │ └── component │ │ └── info-card.tsx │ ├── config │ └── test │ │ ├── setupVitest.ts │ │ └── test-utils.ts │ ├── README.md │ ├── .gitignore │ ├── tsup.config.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── LICENSE │ ├── vitest.config.ts │ └── lint-staged.config.js ├── .vercelignore ├── .npmrc ├── .gitpod.yml ├── docs ├── docker │ └── multistage-size.png ├── deploy │ └── images │ │ ├── vercel-monorepo-cache.jpg │ │ ├── vercel-monorepo-import.jpg │ │ └── vercel-monorepo-naming.jpg ├── images │ └── changeset │ │ ├── atlassian-changeset-bot-app.jpg │ │ └── atlassian-changeset-bot-perms.jpg ├── dx-atlassian-changeset.md ├── benchmark │ └── jest-vs-vitest.md └── about-lint-staged.md ├── .prettierignore ├── .ncurc.yml ├── .yarnrc.yml ├── .changeset └── config.json ├── scripts └── check-git-pristine.sh ├── .prettierrc.js ├── docker-compose.yml ├── tsconfig.base.json ├── CHANGELOG.md ├── lint-staged.config.js ├── TROUBLESHOOT.md ├── commitlint.config.js ├── cache.config.js ├── LICENSE ├── constraints.pro ├── turbo.json ├── .syncpackrc.cjs └── .gitignore /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | # latest lts version 2 | lts/* -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | yarn commitlint --edit $1 -------------------------------------------------------------------------------- /docker/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/node_modules -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | yarn g:lint-staged-files --debug 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: belgattitude 2 | ko_fi: belgattitude 3 | -------------------------------------------------------------------------------- /apps/vite-app/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/ts-utils/src/asserts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './asserts'; 2 | -------------------------------------------------------------------------------- /packages/ts-utils/src/convert/index.ts: -------------------------------------------------------------------------------- 1 | export * from './string-convert'; 2 | -------------------------------------------------------------------------------- /packages/ts-utils/src/typeguards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './typeguards'; 2 | -------------------------------------------------------------------------------- /packages/ts-utils/src/array/index.ts: -------------------------------------------------------------------------------- 1 | export { ArrayUtils } from './ArrayUtils'; 2 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/layouts/main/index.ts: -------------------------------------------------------------------------------- 1 | export { MainLayout } from './MainLayout'; 2 | -------------------------------------------------------------------------------- /packages/ts-utils/src/random/index.ts: -------------------------------------------------------------------------------- 1 | export { getRandomInt } from './getRandomInt'; 2 | -------------------------------------------------------------------------------- /apps/nextjs-app/config/tests/setupVitest.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/vitest'; 2 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/features/demo/pages/index.ts: -------------------------------------------------------------------------------- 1 | export { DemoPage } from './DemoPage'; 2 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/features/home/pages/index.ts: -------------------------------------------------------------------------------- 1 | export { HomePage } from './HomePage'; 2 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/layouts/admin/index.ts: -------------------------------------------------------------------------------- 1 | export { AdminLayout } from './AdminLayout'; 2 | -------------------------------------------------------------------------------- /apps/vite-app/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | -------------------------------------------------------------------------------- /packages/ts-utils/src/types/NonEmptyArray.ts: -------------------------------------------------------------------------------- 1 | export type NonEmptyArray = [T, ...T[]]; 2 | -------------------------------------------------------------------------------- /.vercelignore: -------------------------------------------------------------------------------- 1 | .github 2 | .changeset 3 | .husky 4 | docker 5 | README.md 6 | CHANGELOG.md 7 | 8 | -------------------------------------------------------------------------------- /packages/common-i18n/src/index.ts: -------------------------------------------------------------------------------- 1 | export type { I18nResources } from './I18nResources.types'; 2 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/features/admin/layouts/index.ts: -------------------------------------------------------------------------------- 1 | export { AdminLayout } from './AdminLayout'; 2 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/features/admin/pages/index.ts: -------------------------------------------------------------------------------- 1 | export { AdminMainPage } from './AdminMainPage'; 2 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/lib/emotion/index.ts: -------------------------------------------------------------------------------- 1 | export { createEmotionCache } from './createEmotionCache'; 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # used in tandem with package.json engines section to only use yarn 2 | engine-strict=true 3 | 4 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/styles/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /packages/common-i18n/src/locales/en/admin.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": { 3 | "title": "Admin" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/common-i18n/src/locales/en/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": { 3 | "title": "Blog" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/common-i18n/src/locales/fr/admin.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": { 3 | "title": "Admin" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/common-i18n/src/locales/fr/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": { 3 | "title": "Blog" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/common-i18n/src/locales/en/navigation.json: -------------------------------------------------------------------------------- 1 | { 2 | "topNav": { 3 | "home": "Home" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/common-i18n/src/locales/fr/navigation.json: -------------------------------------------------------------------------------- 1 | { 2 | "topNav": { 3 | "home": "Home" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/ts-utils/src/types/Unpromisify.ts: -------------------------------------------------------------------------------- 1 | export type UnPromisify = T extends Promise ? U : T; 2 | -------------------------------------------------------------------------------- /packages/ui-lib/src/_stories/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /packages/ts-utils/.escheckrc: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaVersion": "es2022", 3 | "module": true, 4 | "files": "./dist/**/*.js" 5 | } -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: corepack enable && yarn install 3 | command: yarn workspace nextjs-app run dev -p 3000 4 | -------------------------------------------------------------------------------- /packages/db-main-prisma/.env.development: -------------------------------------------------------------------------------- 1 | PRISMA_DATABASE_URL=postgresql://nextjs:!ChangeMe!@localhost:5432/maindb?schema=public -------------------------------------------------------------------------------- /packages/eslint-config-bases/src/index.js: -------------------------------------------------------------------------------- 1 | const { typescript } = require('./bases'); 2 | 3 | module.exports = typescript; 4 | -------------------------------------------------------------------------------- /packages/db-main-prisma/src/seeds/index.ts: -------------------------------------------------------------------------------- 1 | export { UserSeeds } from './UserSeeds'; 2 | export { PoemSeeds } from './PoemSeeds'; 3 | -------------------------------------------------------------------------------- /packages/eslint-config-bases/.escheckrc: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaVersion": "es2020", 3 | "module": false, 4 | "files": "./src/**/*.js" 5 | } -------------------------------------------------------------------------------- /packages/ui-lib/src/base/index.ts: -------------------------------------------------------------------------------- 1 | export { Button } from './button/Button'; 2 | export { BasicCard } from './card/BasicCard'; 3 | -------------------------------------------------------------------------------- /apps/nextjs-app/.escheckrc: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaVersion": "es2022", 3 | "module": false, 4 | "files": "./.next/static/chunks/**/*.js" 5 | } -------------------------------------------------------------------------------- /docs/docker/multistage-size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belgattitude/nextjs-monorepo-example/HEAD/docs/docker/multistage-size.png -------------------------------------------------------------------------------- /packages/ui-lib/src/ux/index.ts: -------------------------------------------------------------------------------- 1 | export { GradientText } from './text/GradientText'; 2 | export { TypedText } from './text/TypedText'; 3 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/features/demo/components/index.ts: -------------------------------------------------------------------------------- 1 | export { PoemCard } from './PoemCard'; 2 | export { PoemGrid } from './PoemGrid'; 3 | -------------------------------------------------------------------------------- /packages/common-i18n/src/locales/en/demo.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": { 3 | "title": "Web-app | Demo | nextjs-monorepo-example" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/common-i18n/src/locales/fr/demo.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": { 3 | "title": "Web-app | Démo | nextjs-monorepo-example" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/features/system/pages/index.ts: -------------------------------------------------------------------------------- 1 | export { NotFoundPage } from './NotFoundPage'; 2 | export { ErrorPage } from './ErrorPage'; 3 | -------------------------------------------------------------------------------- /packages/ts-utils/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export type { UnPromisify } from './Unpromisify'; 2 | export type { NonEmptyArray } from './NonEmptyArray'; 3 | -------------------------------------------------------------------------------- /apps/nextjs-app/public/images/paella.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belgattitude/nextjs-monorepo-example/HEAD/apps/nextjs-app/public/images/paella.jpg -------------------------------------------------------------------------------- /packages/ui-lib/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base'; 2 | export { Message } from './message'; 3 | export { AsyncMessage } from './async-message'; 4 | -------------------------------------------------------------------------------- /apps/nextjs-app/public/images/lizard.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belgattitude/nextjs-monorepo-example/HEAD/apps/nextjs-app/public/images/lizard.webp -------------------------------------------------------------------------------- /apps/vite-app/src/types.d/emotion-css.d.ts: -------------------------------------------------------------------------------- 1 | // Bypass issues with "'css' prop does not exists" 2 | /// 3 | -------------------------------------------------------------------------------- /packages/core-lib/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { usePromise } from './use-promise'; 2 | export { useDeepCompareMemoize } from './use-deep-compare-memoize'; 3 | -------------------------------------------------------------------------------- /packages/ts-utils/.size-limit.cjs: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | name: 'JS', 4 | path: ['dist/index.js'], 5 | limit: '3KB', 6 | }, 7 | ]; 8 | -------------------------------------------------------------------------------- /apps/nextjs-app/public/images/nextjs-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belgattitude/nextjs-monorepo-example/HEAD/apps/nextjs-app/public/images/nextjs-logo.png -------------------------------------------------------------------------------- /docs/deploy/images/vercel-monorepo-cache.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belgattitude/nextjs-monorepo-example/HEAD/docs/deploy/images/vercel-monorepo-cache.jpg -------------------------------------------------------------------------------- /docs/deploy/images/vercel-monorepo-import.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belgattitude/nextjs-monorepo-example/HEAD/docs/deploy/images/vercel-monorepo-import.jpg -------------------------------------------------------------------------------- /docs/deploy/images/vercel-monorepo-naming.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belgattitude/nextjs-monorepo-example/HEAD/docs/deploy/images/vercel-monorepo-naming.jpg -------------------------------------------------------------------------------- /packages/core-lib/src/index.ts: -------------------------------------------------------------------------------- 1 | export const sayHello = (name: string): string => { 2 | return `I'm the @your-org/ui-lib component telling ${name} !`; 3 | }; 4 | -------------------------------------------------------------------------------- /packages/ui-lib/src/types.d/emotion-css.d.ts: -------------------------------------------------------------------------------- 1 | // Bypass issues with "'css' prop does not exists" 2 | /// 3 | -------------------------------------------------------------------------------- /apps/nextjs-app/public/images/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belgattitude/nextjs-monorepo-example/HEAD/apps/nextjs-app/public/images/favicon/favicon.ico -------------------------------------------------------------------------------- /apps/nextjs-app/public/images/anims/flow-giphy.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belgattitude/nextjs-monorepo-example/HEAD/apps/nextjs-app/public/images/anims/flow-giphy.webp -------------------------------------------------------------------------------- /docs/images/changeset/atlassian-changeset-bot-app.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belgattitude/nextjs-monorepo-example/HEAD/docs/images/changeset/atlassian-changeset-bot-app.jpg -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # prettier 3.0 adds .gitignore by default 2 | **/.yarn 3 | **/.next 4 | **/.out 5 | **/dist 6 | **/build 7 | **/.tmp 8 | **/.cache 9 | **/.turbo 10 | 11 | -------------------------------------------------------------------------------- /apps/nextjs-app/public/assets/annie-spratt-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belgattitude/nextjs-monorepo-example/HEAD/apps/nextjs-app/public/assets/annie-spratt-unsplash.jpg -------------------------------------------------------------------------------- /apps/nextjs-app/public/images/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belgattitude/nextjs-monorepo-example/HEAD/apps/nextjs-app/public/images/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /apps/nextjs-app/public/images/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belgattitude/nextjs-monorepo-example/HEAD/apps/nextjs-app/public/images/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /docs/images/changeset/atlassian-changeset-bot-perms.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belgattitude/nextjs-monorepo-example/HEAD/docs/images/changeset/atlassian-changeset-bot-perms.jpg -------------------------------------------------------------------------------- /apps/nextjs-app/public/images/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belgattitude/nextjs-monorepo-example/HEAD/apps/nextjs-app/public/images/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /apps/nextjs-app/public/images/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belgattitude/nextjs-monorepo-example/HEAD/apps/nextjs-app/public/images/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /apps/nextjs-app/src/server/config/container.config.ts: -------------------------------------------------------------------------------- 1 | import { getPrismaClientDbMain } from './prisma/db-main-prisma.config'; 2 | export const prismaClient = getPrismaClientDbMain(); 3 | -------------------------------------------------------------------------------- /packages/ui-lib/src/icons/social/README.md: -------------------------------------------------------------------------------- 1 | ## Credits 2 | 3 | - **github.svg** by https://www.iconfinder.com/Adi_Sinchetru from https://www.iconfinder.com/icons/1390302/github_icon 4 | -------------------------------------------------------------------------------- /packages/common-i18n/src/locales/en/system.json: -------------------------------------------------------------------------------- 1 | { 2 | "notFound": { 3 | "title": "404 - Page Not Found" 4 | }, 5 | "links": { 6 | "backToHome": "Back to home" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/common-i18n/src/locales/en/auth.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": { 3 | "title": "Login" 4 | }, 5 | "button": { 6 | "login": "Login", 7 | "logout": "Logout" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/common-i18n/src/locales/fr/auth.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": { 3 | "title": "Login" 4 | }, 5 | "button": { 6 | "login": "Login", 7 | "logout": "Logout" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/db-main-prisma/.env: -------------------------------------------------------------------------------- 1 | # Default for running this package locally. Apps will override this. 2 | PRISMA_DATABASE_URL=postgresql://nextjs:!ChangeMe!@localhost:5432/maindb?schema=public 3 | 4 | -------------------------------------------------------------------------------- /packages/db-main-prisma/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /apps/nextjs-app/public/images/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belgattitude/nextjs-monorepo-example/HEAD/apps/nextjs-app/public/images/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /apps/nextjs-app/public/images/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/belgattitude/nextjs-monorepo-example/HEAD/apps/nextjs-app/public/images/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /apps/nextjs-app/src/pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | import NextAuth from 'next-auth'; 2 | import { nextAuthConfig } from '@/config/next-auth.config'; 3 | 4 | export default NextAuth(nextAuthConfig); 5 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/server/features/poem/SearchPoems/index.ts: -------------------------------------------------------------------------------- 1 | export type { SearchPoemsParams, SearchPoems } from './SearchPoems.types'; 2 | export { SearchPoemsQuery } from './SearchPoemsQuery'; 3 | -------------------------------------------------------------------------------- /packages/common-i18n/src/locales/fr/system.json: -------------------------------------------------------------------------------- 1 | { 2 | "notFound": { 3 | "title": "404 - Page non trouvée" 4 | }, 5 | "links": { 6 | "backToHome": "Revenir à l'accueil" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/features/home/blocks/index.ts: -------------------------------------------------------------------------------- 1 | export { CtaBlock } from './cta/CtaBlock'; 2 | export { HeroBlock } from './hero/HeroBlock'; 3 | export { FeaturesBlock } from './features/FeaturesBlock'; 4 | -------------------------------------------------------------------------------- /packages/eslint-config-bases/src/patch/modern-module-resolution.js: -------------------------------------------------------------------------------- 1 | // See https://www.npmjs.com/package/@rushstack/eslint-patch 2 | // @ts-ignore 3 | require('@rushstack/eslint-patch/modern-module-resolution'); 4 | -------------------------------------------------------------------------------- /packages/ui-lib/vite.config.storybook.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import tsconfigPaths from 'vite-tsconfig-paths'; 3 | 4 | export default defineConfig({ 5 | plugins: [tsconfigPaths()], 6 | }); 7 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/features/demo/blocks/index.ts: -------------------------------------------------------------------------------- 1 | export { DemoMuiBlock } from './demo-mui/DemoMuiBlock'; 2 | export { PoetryBlock } from './poetry/PoetryBlock'; 3 | export { Jumbotron } from './jumbotron/Jumbotron'; 4 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/lib/auth/error/index.ts: -------------------------------------------------------------------------------- 1 | export { authErrorCodes } from './authErrorCodes'; 2 | export type { AuthErrorCodes } from './authErrorCodes'; 3 | export { createHttpUnauthorized } from './createHttpUnauthorized'; 4 | -------------------------------------------------------------------------------- /packages/ui-lib/src/message.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | 3 | type Props = { 4 | message: string; 5 | children?: never; 6 | }; 7 | 8 | export const Message: FC = ({ message }) => {message}; 9 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/lib/i18n/I18nNamespace.types.ts: -------------------------------------------------------------------------------- 1 | import type { CustomTypeOptions } from 'i18next'; 2 | 3 | export type I18nNamespace = keyof CustomTypeOptions['resources']; 4 | 5 | export type I18nActiveNamespaces = I18nNamespace[]; 6 | -------------------------------------------------------------------------------- /packages/common-i18n/src/locales/fr/home.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": { 3 | "title": "Accueil | Nextjs-app | nextjs-monorepo-example" 4 | }, 5 | "btn": { 6 | "getStarted": "Démarrer", 7 | "liveDemo": "Démo" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/common-i18n/src/locales/en/home.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": { 3 | "title": "Home | nextjs-app | nextjs-monorepo-example" 4 | }, 5 | "btn": { 6 | "getStarted": "Get started", 7 | "liveDemo": "Live demo" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/ui-lib/config/test/setupVitest.ts: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest'; 2 | 3 | const jestCompatOverride = { 4 | fn: vi.fn, 5 | spyOn: vi.spyOn, 6 | }; 7 | 8 | (globalThis as unknown as Record<'jest', unknown>).jest = jestCompatOverride; 9 | -------------------------------------------------------------------------------- /packages/core-lib/config/test/setupVitest.ts: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest'; 2 | 3 | const jestCompatOverride = { 4 | fn: vi.fn, 5 | spyOn: vi.spyOn, 6 | }; 7 | 8 | (globalThis as unknown as Record<'jest', unknown>).jest = jestCompatOverride; 9 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/lib/auth/error/authErrorCodes.ts: -------------------------------------------------------------------------------- 1 | export const authErrorCodes = { 2 | AUTHENTICATION_FAILED: 'AUTHENTICATION_FAILED', 3 | } as const; 4 | 5 | export type AuthErrorCodes = 6 | (typeof authErrorCodes)[keyof typeof authErrorCodes]; 7 | -------------------------------------------------------------------------------- /packages/core-lib/README.md: -------------------------------------------------------------------------------- 1 | # @your-org/core-lib 2 | 3 | ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/belgattitude/nextjs-monorepo-example/ci-packages.yml?style=for-the-badge&label=CI) 4 | 5 | ## Intro 6 | 7 | WIP 8 | -------------------------------------------------------------------------------- /packages/common-i18n/src/locales/en/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": { 3 | "save": "Save", 4 | "update": "Update", 5 | "delete": "Delete", 6 | "cancel": "Cancel", 7 | "zoomIn": "Zoom in", 8 | "zoomOut": "Zoom out" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/ts-utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export type { UnPromisify } from './types'; 2 | 3 | export * from './array/index'; 4 | export * from './typeguards/index'; 5 | export * from './asserts/index'; 6 | export * from './convert/index'; 7 | export * from './random/index'; 8 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/lib/env/index.mjs: -------------------------------------------------------------------------------- 1 | export { getValidatedBuildEnv } from './getValidatedBuildEnv.mjs'; 2 | export { getValidatedServerRuntimeEnv } from './getValidatedServerRuntimeEnv.mjs'; 3 | export { truthyStrEnvValue, zConvertTruthyStrToBool } from './utils.mjs'; 4 | -------------------------------------------------------------------------------- /apps/nextjs-app/.env.development: -------------------------------------------------------------------------------- 1 | NEXTJS_DISABLE_SENTRY=true 2 | PRISMA_DATABASE_URL=postgresql://nextjs:!ChangeMe!@localhost:5432/maindb?schema=public 3 | NEXTAUTH_URL=http://localhost:3000 4 | NEXTAUTH_SECRET=7cbca39437d5fff11efede0920f24d1d29a107e4d49a01e7cd8103adc9e5ce06 5 | -------------------------------------------------------------------------------- /packages/common-i18n/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # build 4 | /dist 5 | 6 | # dependencies 7 | node_modules 8 | 9 | # testing 10 | /coverage 11 | 12 | # misc 13 | .DS_Store 14 | *.pem 15 | -------------------------------------------------------------------------------- /packages/common-i18n/src/locales/fr/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": { 3 | "save": "Sauver", 4 | "update": "Mettre à jour", 5 | "delete": "Effacer", 6 | "cancel": "Annuler", 7 | "zoomIn": "Agrandir", 8 | "zoomOut": "Réduire" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/core-lib/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # build 4 | /dist 5 | 6 | # dependencies 7 | node_modules 8 | 9 | # testing 10 | /coverage 11 | 12 | # misc 13 | .DS_Store 14 | *.pem 15 | -------------------------------------------------------------------------------- /packages/ts-utils/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # build 4 | /dist 5 | 6 | # dependencies 7 | node_modules 8 | 9 | # testing 10 | /coverage 11 | 12 | # misc 13 | .DS_Store 14 | *.pem 15 | -------------------------------------------------------------------------------- /apps/nextjs-app/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /packages/db-main-prisma/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "extends": ["//"], 4 | "tasks": { 5 | "codegen": { 6 | "inputs": ["src/**/*", "prisma/**/*"], 7 | "outputs": ["src/generated/**"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/eslint-config-bases/src/helpers/getPrettierConfig.js: -------------------------------------------------------------------------------- 1 | const prettierBaseConfig = require('../prettier.base.config'); 2 | 3 | const getPrettierConfig = () => { 4 | return prettierBaseConfig; 5 | }; 6 | 7 | module.exports = { 8 | getPrettierConfig, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/eslint-config-bases/src/helpers/index.js: -------------------------------------------------------------------------------- 1 | const { getDefaultIgnorePatterns } = require('./getDefaultIgnorePatterns'); 2 | const { getPrettierConfig } = require('./getPrettierConfig'); 3 | 4 | module.exports = { 5 | getDefaultIgnorePatterns, 6 | getPrettierConfig, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/ui-lib/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Notably for storybook & independent releases 3 | * @link https://storybook.js.org/addons/@storybook/addon-postcss 4 | */ 5 | module.exports = { 6 | plugins: { 7 | tailwindcss: {}, 8 | autoprefixer: {}, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/config/react-query.config.ts: -------------------------------------------------------------------------------- 1 | import type { QueryClientConfig } from '@tanstack/react-query'; 2 | 3 | export const queryClientConfig: QueryClientConfig = { 4 | defaultOptions: { 5 | queries: { 6 | refetchOnWindowFocus: false, 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/server/config/index.ts: -------------------------------------------------------------------------------- 1 | import { getPrismaClientDbMain } from './prisma/db-main-prisma.config'; 2 | export const prismaDbMain = getPrismaClientDbMain(); 3 | export { 4 | corsAllowedOrigins, 5 | getCorsWhitelistOriginRegexp, 6 | corsDefaultOptions, 7 | } from './cors.config'; 8 | -------------------------------------------------------------------------------- /packages/ui-lib/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # build 4 | /dist 5 | /build 6 | /storybook-static 7 | 8 | # dependencies 9 | node_modules 10 | 11 | # testing 12 | /coverage 13 | 14 | # misc 15 | .DS_Store 16 | *.pem 17 | -------------------------------------------------------------------------------- /packages/ui-lib/README.md: -------------------------------------------------------------------------------- 1 | # @your-org/ui-lib 2 | 3 | ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/belgattitude/nextjs-monorepo-example/ci-packages.yml?style=for-the-badge&label=CI) 4 | 5 | ## Intro 6 | 7 | A basic example of a design-system setup with storybook. 8 | -------------------------------------------------------------------------------- /apps/nextjs-app/e2e/api/_monitor/sentry.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test('should return a status of 500', async ({ request }) => { 4 | const resp = await request.get('/api/_monitor/sentry'); 5 | const status = resp.status(); 6 | expect(status).toEqual(500); 7 | }); 8 | -------------------------------------------------------------------------------- /docs/dx-atlassian-changeset.md: -------------------------------------------------------------------------------- 1 | # DX 2 | 3 | ## Atlassian changeset 4 | 5 | Changesets 6 | 7 | The github-app [changeset-bot](https://github.com/apps/changeset-bot) 8 | 9 | 10 | ![](./images/changeset/atlassian-changeset-bot-app.jpg) 11 | ![](./images/changeset/atlassian-changeset-bot-perms.jpg) 12 | 13 | -------------------------------------------------------------------------------- /.ncurc.yml: -------------------------------------------------------------------------------- 1 | # npm-check-updates configuration used by yarn deps:check && yarn deps:update 2 | # convenience scripts. 3 | # @link https://github.com/raineorshine/npm-check-updates 4 | 5 | # Add here exclusions on packages if any 6 | reject: 7 | ['eslint', '@pothos/plugin-prisma', '@pothos/core', '@pothos/plugin-errors'] 8 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/features/admin/layouts/AdminLayout.tsx: -------------------------------------------------------------------------------- 1 | import type { FC, PropsWithChildren } from 'react'; 2 | import { MainLayout } from '@/layouts/main'; 3 | 4 | export const AdminLayout: FC = (props) => { 5 | const { children } = props; 6 | return {children}; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/db-main-prisma/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # generated 4 | 5 | /src/generated 6 | 7 | # build 8 | /dist 9 | 10 | # dependencies 11 | node_modules 12 | 13 | # testing 14 | /coverage 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | -------------------------------------------------------------------------------- /packages/eslint-config-bases/.prettierrc.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { 3 | getPrettierConfig, 4 | } = require("./src/helpers"); 5 | 6 | /** 7 | * @type {import('prettier').Config} 8 | */ 9 | module.exports = { 10 | ...getPrettierConfig(), 11 | overrides: [ 12 | // whatever you need 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | import { sayHello } from '@your-org/core-lib'; 2 | import type { NextApiRequest, NextApiResponse } from 'next'; 3 | 4 | export default function handleApiHelloRoute( 5 | req: NextApiRequest, 6 | res: NextApiResponse 7 | ) { 8 | res.send(sayHello('world loaded from /api/hello')); 9 | } 10 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/themes/tailwind/tailwind.theme.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Keep this file as '.js' as it's included in tailwind.config.js 3 | 4 | import { browserFonts } from '../shared/browser-fonts'; 5 | 6 | export const tailwindTheme = { 7 | fontFamily: { 8 | sans: ['Inter Variable', ...browserFonts.sans], 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /apps/nextjs-app/public/images/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: 0 2 | 3 | defaultSemverRangePrefix: '' 4 | 5 | enableGlobalCache: false 6 | 7 | nmMode: hardlinks-local 8 | 9 | nodeLinker: node-modules 10 | 11 | npmRegistryServer: 'https://registry.npmjs.org/' 12 | 13 | supportedArchitectures: 14 | cpu: 15 | - current 16 | libc: 17 | - current 18 | os: 19 | - current 20 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/features/demo/demo.config.ts: -------------------------------------------------------------------------------- 1 | import { getSortedI18nNamespaces } from '@/lib/i18n/getSortedI18nNamespaces'; 2 | 3 | const i18nNamespaces = getSortedI18nNamespaces(['common', 'demo']); 4 | 5 | export type DemoConfig = { 6 | i18nNamespaces: typeof i18nNamespaces; 7 | }; 8 | 9 | export const demoConfig: DemoConfig = { 10 | i18nNamespaces, 11 | }; 12 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/features/home/home.config.ts: -------------------------------------------------------------------------------- 1 | import { getSortedI18nNamespaces } from '@/lib/i18n/getSortedI18nNamespaces'; 2 | 3 | const i18nNamespaces = getSortedI18nNamespaces(['common', 'home']); 4 | 5 | export type HomeConfig = { 6 | i18nNamespaces: typeof i18nNamespaces; 7 | }; 8 | 9 | export const homeConfig: HomeConfig = { 10 | i18nNamespaces, 11 | }; 12 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/features/system/system.config.ts: -------------------------------------------------------------------------------- 1 | import { getSortedI18nNamespaces } from '@/lib/i18n/getSortedI18nNamespaces'; 2 | 3 | const i18nNamespaces = getSortedI18nNamespaces(['system']); 4 | 5 | export type SystemConfig = { 6 | i18nNamespaces: typeof i18nNamespaces; 7 | }; 8 | 9 | export const systemConfig: SystemConfig = { 10 | i18nNamespaces, 11 | }; 12 | -------------------------------------------------------------------------------- /apps/vite-app/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import App from './App'; 4 | import './index.css'; 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 7 | createRoot(document.querySelector('#root')!).render( 8 | 9 | 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/server/features/poem/SearchPoems/SearchPoems.types.ts: -------------------------------------------------------------------------------- 1 | import type { UnPromisify } from '@your-org/ts-utils'; 2 | import type { SearchPoemsQuery } from './SearchPoemsQuery'; 3 | 4 | export interface SearchPoemsParams { 5 | limit?: number; 6 | offset?: number; 7 | } 8 | 9 | export type SearchPoems = UnPromisify>; 10 | -------------------------------------------------------------------------------- /packages/db-main-prisma/src/index.ts: -------------------------------------------------------------------------------- 1 | export { PrismaClient as PrismaClientDbMain } from './generated/client'; 2 | export type { Prisma as PrismaDbMain } from './generated/client'; 3 | export { PrismaManager } from './prisma-manager'; 4 | export type { PrismaClientOptions } from './prisma-manager'; 5 | export type { default as DbMainPothosTypes } from './generated/pothos/prisma-pothos-types'; 6 | -------------------------------------------------------------------------------- /packages/eslint-config-bases/src/bases/prettier-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom config base for projects using prettier. 3 | * @see https://github.com/belgattitude/shared-dx/tree/main/packages/eslint-config-bases 4 | */ 5 | 6 | module.exports = { 7 | extends: ['prettier'], 8 | rules: { 9 | 'arrow-body-style': 'off', 10 | 'prefer-arrow-callback': 'off', 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /packages/eslint-config-bases/src/prettier.base.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @type {import('prettier').Config} 5 | */ 6 | module.exports = { 7 | bracketSameLine: false, 8 | bracketSpacing: true, 9 | endOfLine: 'lf', 10 | overrides: [], 11 | semi: true, 12 | singleQuote: true, 13 | tabWidth: 2, 14 | trailingComma: 'es5', 15 | useTabs: false, 16 | }; 17 | -------------------------------------------------------------------------------- /packages/ui-lib/.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from '@storybook/react'; 2 | 3 | const preview: Preview = { 4 | parameters: { 5 | actions: { argTypesRegex: '^on[A-Z].*' }, 6 | controls: { 7 | matchers: { 8 | color: /(background|color)$/i, 9 | date: /Date$/, 10 | }, 11 | }, 12 | }, 13 | }; 14 | 15 | export default preview; 16 | -------------------------------------------------------------------------------- /packages/db-main-prisma/src/lib/AbstractSeed.ts: -------------------------------------------------------------------------------- 1 | import type { PrismaClient } from '../generated/client'; 2 | 3 | export abstract class AbstractSeed { 4 | constructor(public prisma: PrismaClient) {} 5 | 6 | abstract execute(): Promise; 7 | 8 | protected log = (operation: 'UPSERT' | 'CREATE' | 'UPDATE', msg: string) => { 9 | console.log(`${operation}: ${msg}`); 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/layouts/main/MainLayout.tsx: -------------------------------------------------------------------------------- 1 | import type { FC, ReactNode } from 'react'; 2 | import { MainFooter } from './MainFooter'; 3 | 4 | export const MainLayout: FC<{ children: ReactNode }> = (props) => { 5 | const { children } = props; 6 | return ( 7 |
8 |
{children}
9 | 10 |
11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/features/admin/admin.config.ts: -------------------------------------------------------------------------------- 1 | import { getSortedI18nNamespaces } from '@/lib/i18n/getSortedI18nNamespaces'; 2 | 3 | const i18nNamespaces = getSortedI18nNamespaces([ 4 | 'admin', 5 | 'common', 6 | 'navigation', 7 | ]); 8 | 9 | export type AdminConfig = { 10 | i18nNamespaces: typeof i18nNamespaces; 11 | }; 12 | export const adminConfig: AdminConfig = { 13 | i18nNamespaces, 14 | }; 15 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/features/auth/auth.config.ts: -------------------------------------------------------------------------------- 1 | import { getSortedI18nNamespaces } from '@/lib/i18n/getSortedI18nNamespaces'; 2 | 3 | const i18nNamespaces = getSortedI18nNamespaces([ 4 | 'auth', 5 | 'common', 6 | 'navigation', 7 | ]); 8 | 9 | export type AuthConfig = { 10 | i18nNamespaces: typeof i18nNamespaces; 11 | }; 12 | 13 | export const authConfig: AuthConfig = { 14 | i18nNamespaces, 15 | }; 16 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | name: nextjs-monorepo-example 2 | services: 3 | nextjs-app: 4 | container_name: 'nextjs-app' 5 | build: 6 | context: ../ 7 | dockerfile: ./docker/Dockerfile 8 | restart: always 9 | networks: 10 | - nextjs-monorepo-example-network 11 | ports: 12 | - 3000:3000 13 | 14 | networks: 15 | nextjs-monorepo-example-network: 16 | driver: bridge 17 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/layouts/admin/AdminLayout.tsx: -------------------------------------------------------------------------------- 1 | import type { FC, ReactNode } from 'react'; 2 | import { AdminSidebar } from '@/layouts/admin/AdminSidebar'; 3 | 4 | export const AdminLayout: FC<{ children: ReactNode }> = (props) => { 5 | const { children } = props; 6 | return ( 7 |
8 | 9 |
{children}
10 |
11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/pages/_monitor/preview/error-page.tsx: -------------------------------------------------------------------------------- 1 | import { ErrorPage } from '@/features/system/pages'; 2 | 3 | const exampleError = new Error('ErrorPage example error'); 4 | 5 | export default function ErrorPageRoute() { 6 | return ( 7 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /apps/vite-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/lib/auth/error/createHttpUnauthorized.ts: -------------------------------------------------------------------------------- 1 | import { HttpUnauthorized } from '@httpx/exception'; 2 | import { authErrorCodes } from './authErrorCodes'; 3 | 4 | export const createHttpUnauthorized = ( 5 | message?: string, 6 | errorCause?: Error 7 | ): HttpUnauthorized => { 8 | return new HttpUnauthorized({ 9 | message, 10 | code: authErrorCodes.AUTHENTICATION_FAILED, 11 | cause: errorCause, 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /apps/vite-app/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /packages/eslint-config-bases/src/bases/simple-import-sort.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | }, 7 | plugins: ['simple-import-sort'], 8 | rules: { 9 | 'linebreak-style': ['error', 'unix'], 10 | 'no-duplicate-imports': 'off', 11 | 'simple-import-sort/imports': 'error', 12 | 'simple-import-sort/exports': 'error', 13 | 'sort-imports': 'off', 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /apps/nextjs-app/public/images/layout/footer-waves.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.3/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { "repo": "belgattitude/nextjs-monorepo-example" } 6 | ], 7 | "privatePackages": { "version": true, "tag": true }, 8 | "commit": true, 9 | "linked": [], 10 | "access": "restricted", 11 | "baseBranch": "main", 12 | "updateInternalDependencies": "patch", 13 | "ignore": [] 14 | } 15 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/features/system/pages/__tests__/NotFoundPage.test.tsx: -------------------------------------------------------------------------------- 1 | import { NotFoundPage } from '@/features/system/pages'; 2 | import { render, screen } from '@/test-utils'; 3 | 4 | describe('notFoundPage test', () => { 5 | it('should contain passed title', () => { 6 | render(); 7 | expect(screen.getByTestId('not-found-title')).toHaveTextContent( 8 | '404 - Not found' 9 | ); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/themes/mui/mui.theme.ts: -------------------------------------------------------------------------------- 1 | import { red } from '@mui/material/colors'; 2 | import { createTheme } from '@mui/material/styles'; 3 | import { tailwindTheme } from '@/themes/tailwind/tailwind.theme'; 4 | 5 | // Create a mui v5 theme instance. 6 | export const muiTheme = createTheme({ 7 | typography: { 8 | fontFamily: tailwindTheme.fontFamily.sans.join(','), 9 | }, 10 | palette: { 11 | error: { 12 | main: red.A400, 13 | }, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /packages/ts-utils/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig((options) => ({ 4 | entry: ['src/index.ts'], 5 | splitting: true, 6 | clean: true, 7 | dts: true, 8 | format: ['esm'], 9 | platform: 'browser', 10 | target: ['es2020', 'chrome80', 'edge18', 'firefox80', 'node18'], 11 | tsconfig: new URL('tsconfig.build.json', import.meta.url).pathname, 12 | sourcemap: !options.watch, 13 | minify: !options.watch, 14 | })); 15 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/features/system/pages/__tests__/ErrorPage.test.tsx: -------------------------------------------------------------------------------- 1 | import { ErrorPage } from '@/features/system/pages'; 2 | import { render, screen } from '@/test-utils'; 3 | 4 | describe('errorPage test', () => { 5 | it('should contain error passed status code', () => { 6 | render(); 7 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call 8 | expect(screen.getByTestId('error-status-code')).toHaveTextContent('500'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/layouts/main/__tests__/MainLayout.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@/test-utils'; 2 | import { MainLayout } from '../MainLayout'; 3 | 4 | describe('main layout tests', () => { 5 | it('should render children', () => { 6 | render( 7 | 8 |
Hello
9 |
10 | ); 11 | const appContent = screen.getByRole('article'); 12 | expect(appContent).toHaveTextContent('Hello'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /packages/core-lib/src/hooks/use-deep-compare-memoize.ts: -------------------------------------------------------------------------------- 1 | import { dequal } from 'dequal'; 2 | import { useRef } from 'react'; 3 | 4 | type SupportedValue = 5 | | Record 6 | | string 7 | | boolean 8 | | number 9 | | null; 10 | 11 | export function useDeepCompareMemoize(value: SupportedValue): SupportedValue { 12 | const ref = useRef(null); 13 | if (!dequal(value, ref.current)) { 14 | ref.current = value; 15 | } 16 | return ref.current; 17 | } 18 | -------------------------------------------------------------------------------- /packages/core-lib/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig((options) => ({ 4 | entry: ['src/index.ts'], 5 | splitting: true, 6 | treeshake: true, 7 | clean: true, 8 | dts: true, 9 | format: ['esm'], 10 | platform: 'browser', 11 | target: ['es2020', 'chrome80', 'edge18', 'firefox70', 'node18'], 12 | tsconfig: new URL('tsconfig.build.json', import.meta.url).pathname, 13 | sourcemap: !options.watch, 14 | minify: !options.watch, 15 | })); 16 | -------------------------------------------------------------------------------- /packages/eslint-config-bases/src/helpers/getDefaultIgnorePatterns.js: -------------------------------------------------------------------------------- 1 | const getDefaultIgnorePatterns = () => { 2 | // Hacky way to silence @yarnpkg/doctor about node_modules detection 3 | return [ 4 | `${'node'}_modules}`, 5 | `**/${'node'}_modules}`, 6 | '**/.cache', 7 | 'build', 8 | 'dist', 9 | 'storybook-static', 10 | '.yarn', 11 | '.turbo', 12 | `**/.turbo`, 13 | '.out', 14 | ]; 15 | }; 16 | 17 | module.exports = { 18 | getDefaultIgnorePatterns, 19 | }; 20 | -------------------------------------------------------------------------------- /packages/ui-lib/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig((options) => ({ 4 | entry: ['src/index.ts'], 5 | splitting: true, 6 | treeshake: true, 7 | clean: true, 8 | dts: true, 9 | format: ['esm'], 10 | platform: 'browser', 11 | target: ['es2020', 'chrome70', 'edge18', 'firefox70', 'node18'], 12 | tsconfig: new URL('tsconfig.build.json', import.meta.url).pathname, 13 | sourcemap: !options.watch, 14 | minify: !options.watch, 15 | })); 16 | -------------------------------------------------------------------------------- /packages/core-lib/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "target": "esnext", 6 | "module": "esnext", 7 | "jsx": "react-jsx", 8 | "incremental": false // dts with tsup requires rootDir if incremental (which does not work well aliases) 9 | }, 10 | "exclude": [ 11 | "**/*.stories.tsx", 12 | "**/*.stories.ts", 13 | "**/*.stories.mdx", 14 | ".storybook/**" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/ui-lib/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "target": "esnext", 6 | "module": "esnext", 7 | "jsx": "react-jsx", 8 | "incremental": false // dts with tsup requires rootDir if incremental (which does not work well aliases) 9 | }, 10 | "exclude": [ 11 | "**/*.stories.tsx", 12 | "**/*.stories.ts", 13 | "**/*.stories.mdx", 14 | ".storybook/**" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/codesee-arch-diagram.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [dev, main] 4 | pull_request_target: 5 | types: [opened, synchronize, reopened] 6 | 7 | name: CodeSee 8 | 9 | permissions: read-all 10 | 11 | jobs: 12 | codesee: 13 | runs-on: ubuntu-latest 14 | continue-on-error: true 15 | name: Analyze the repo with CodeSee 16 | steps: 17 | - uses: Codesee-io/codesee-action@v2 18 | with: 19 | codesee-token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }} 20 | -------------------------------------------------------------------------------- /packages/eslint-config-bases/src/bases/perfectionist-jsx.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom config base for projects using typescript / javascript. 3 | * @see https://github.com/belgattitude/shared-dx/tree/main/packages/eslint-config-bases#belgattitudeeslint-config-bases 4 | */ 5 | 6 | module.exports = { 7 | overrides: [ 8 | { 9 | files: ['*.jsx', '*.tsx'], 10 | plugins: ['perfectionist'], 11 | rules: { 12 | 'perfectionist/sort-jsx-props': 'error', 13 | }, 14 | }, 15 | ], 16 | }; 17 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/types.d/i18next.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Types augmentation for translation keys to allow to typecheck 3 | * and suggesting keys to the t function. In case it's too slow 4 | * you can opt out by commenting the following code. 5 | * @link https://react.i18next.com/latest/typescript 6 | */ 7 | import type { I18nResources } from '@your-org/common-i18n'; 8 | 9 | declare module 'i18next' { 10 | interface CustomTypeOptions { 11 | defaultNS: 'common'; 12 | resources: I18nResources; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /apps/nextjs-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | /e2e/.out 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env.local 30 | .env.*.local 31 | 32 | 33 | # Sentry 34 | .sentryclirc 35 | -------------------------------------------------------------------------------- /packages/core-lib/src/hooks/__tests__/use-deep-compare-memoize.test.ts: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react'; 2 | import { useDeepCompareMemoize } from '../use-deep-compare-memoize'; 3 | 4 | describe('useDeepCompareMemoize', () => { 5 | it('should not mutate references', () => { 6 | const val = { 7 | fn: () => {}, 8 | }; 9 | const { result } = renderHook(() => useDeepCompareMemoize(val)); 10 | const ret = result.current as typeof val; 11 | expect(ret.fn).toStrictEqual(val.fn); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/config/app-cache.config.tsx: -------------------------------------------------------------------------------- 1 | import { MapCacheAdapter } from '@soluble/cache-interop'; 2 | import { 3 | getIoRedisOptionsFromDsn, 4 | IoRedisCacheAdapter, 5 | } from '@soluble/cache-ioredis'; 6 | 7 | const appCacheDsn = process.env.APP_CACHE_DSN ?? null; 8 | 9 | export const appCache = appCacheDsn 10 | ? new IoRedisCacheAdapter({ 11 | connection: getIoRedisOptionsFromDsn(appCacheDsn, { 12 | connectTimeout: 3000, 13 | maxRetriesPerRequest: 2, 14 | }), 15 | }) 16 | : new MapCacheAdapter(); 17 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/themes/shared/__tests__/colors.test.ts: -------------------------------------------------------------------------------- 1 | import { tailwindV3Colors } from '../colors'; 2 | describe('colors', () => { 3 | describe('tailwindV3Colors', () => { 4 | it('should be defined', () => { 5 | expect(tailwindV3Colors).toBeDefined(); 6 | }); 7 | it('should contain current', () => { 8 | expect(tailwindV3Colors?.current).toBeDefined(); 9 | }); 10 | it("shouldn't contain deprecated colors", () => { 11 | expect(tailwindV3Colors?.warmGray).toBeUndefined(); 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /scripts/check-git-pristine.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This script helps to identify unexpected changes during steps 3 | if [ $(git status --porcelain | wc -c) -gt 0 ]; 4 | then 5 | printf "[Error]: The git repository state changed after the previous operation.\n" 6 | printf " Here's the output of git status.\n\n" 7 | git status 8 | printf "\nHINT: Did you forget to run the codegen on changed packages ?\n" 9 | exit 1 10 | fi 11 | printf "[Success]: The git repository state haven't been changed after the previous operation.\n" 12 | exit 0 13 | -------------------------------------------------------------------------------- /packages/eslint-config-bases/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "@belgattitude/eslint-config-bases", 4 | "compilerOptions": { 5 | "strict": true, 6 | "skipLibCheck": true, 7 | "noEmit": true, 8 | "incremental": true, 9 | "allowJs": true, 10 | "checkJs": true 11 | }, 12 | "exclude": ["node_modules", "dist", "build"], 13 | "include": [ 14 | ".eslintrc.*", 15 | "**/*.ts", 16 | "**/*.js", 17 | "**/*.cjs", 18 | "**/*.mjs", 19 | "**/*.json" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/lib/emotion/createEmotionCache.ts: -------------------------------------------------------------------------------- 1 | import createCache, { type EmotionCache } from '@emotion/cache'; 2 | 3 | const isBrowser = typeof document !== 'undefined'; 4 | 5 | export const createEmotionCache = (): EmotionCache => { 6 | let insertionPoint; 7 | if (isBrowser) { 8 | const emotionInsertionPoint = document.querySelector( 9 | 'meta[name="emotion-insertion-point"]' 10 | ); 11 | insertionPoint = emotionInsertionPoint ?? undefined; 12 | } 13 | return createCache({ key: 'css', insertionPoint }); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/ts-utils/src/array/ArrayUtils.ts: -------------------------------------------------------------------------------- 1 | import { getRandomInt } from '../random/index'; 2 | import type { NonEmptyArray } from '../types'; 3 | 4 | export class ArrayUtils { 5 | static getRandom(items: NonEmptyArray): T { 6 | if (items.length === 1) return items[0]; 7 | return items[getRandomInt(0, items.length - 1)] as unknown as T; 8 | } 9 | 10 | static removeItem(arr: T[], item: T): T[] { 11 | const index = arr.indexOf(item); 12 | if (index !== -1) { 13 | arr.splice(index, 1); 14 | } 15 | return arr; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/db-main-prisma/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig((options) => { 4 | return { 5 | entry: ['src/index.ts'], 6 | splitting: true, 7 | clean: true, 8 | dts: true, 9 | format: ['esm'], 10 | platform: 'node', 11 | target: ['node18'], 12 | tsconfig: new URL('tsconfig.build.json', import.meta.url).pathname, 13 | sourcemap: !options.watch, 14 | // Do not minify node only packages to let patching possible by the consumer (ie: patch-package) 15 | minify: false, 16 | }; 17 | }); 18 | -------------------------------------------------------------------------------- /packages/ts-utils/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "baseUrl": "./src", 6 | "target": "ESNext", 7 | "verbatimModuleSyntax": true, 8 | "module": "ESNext", 9 | "moduleResolution": "bundler", 10 | "declarationDir": "./dist", 11 | "incremental": true, 12 | "tsBuildInfoFile": "./tsconfig.build.tsbuildinfo" // for tsup rollup dts tsBuildInfoFile is required when using incremental 13 | }, 14 | "exclude": ["dist", "**/__tests__/*"] 15 | } 16 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/providers/ReactQueryClientProvider.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 2 | import type { FC, PropsWithChildren } from 'react'; 3 | import { queryClientConfig } from '@/config/react-query.config'; 4 | 5 | const queryClient = new QueryClient(queryClientConfig); 6 | 7 | type Props = PropsWithChildren; 8 | 9 | export const ReactQueryClientProvider: FC = (props) => { 10 | const { children } = props; 11 | return ( 12 | {children} 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const { getPrettierConfig } = require('@your-org/eslint-config-bases/helpers'); 4 | 5 | const { overrides = [], ...prettierConfig } = getPrettierConfig(); 6 | 7 | /** 8 | * @type {import('prettier').Config} 9 | */ 10 | const config = { 11 | ...prettierConfig, 12 | overrides: [ 13 | ...overrides, 14 | ...[ 15 | { 16 | files: '*.md', 17 | options: { 18 | singleQuote: false, 19 | quoteProps: 'preserve', 20 | }, 21 | }, 22 | ], 23 | ], 24 | }; 25 | 26 | module.exports = config; 27 | -------------------------------------------------------------------------------- /apps/nextjs-app/public/images/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/images/favicon/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/images/favicon/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /packages/core-lib/src/component/info-card.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | 3 | type Props = { 4 | originatingAppName: string; 5 | children?: never; 6 | }; 7 | export const InfoCard: FC = (props) => { 8 | const { originatingAppName } = props; 9 | return ( 10 |
11 | Avatar 12 |
13 |

14 | John Doe 15 |

16 |

{originatingAppName}

17 |
18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /packages/eslint-config-bases/src/bases/prettier-plugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom config base for projects using prettier. 3 | * @see https://github.com/belgattitude/shared-dx/tree/main/packages/eslint-config-bases 4 | */ 5 | 6 | const { getPrettierConfig } = require('../helpers'); 7 | 8 | const { ...prettierConfig } = getPrettierConfig(); 9 | 10 | module.exports = { 11 | extends: ['prettier'], 12 | plugins: ['prettier'], 13 | rules: { 14 | 'arrow-body-style': 'off', 15 | 'prefer-arrow-callback': 'off', 16 | 'prettier/prettier': ['error', prettierConfig], 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /apps/nextjs-app/turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "extends": ["//"], 4 | "tasks": { 5 | "build": { 6 | "env": ["NEXT_BUILD_ENV_*", "NEXT_PUBLIC_*", "SENTRY_*"], 7 | "outputs": [".next/**", "!.next/cache/**"] 8 | }, 9 | "vercel-build": { 10 | "env": ["NEXT_BUILD_ENV_*", "NEXT_PUBLIC_*", "SENTRY_*"], 11 | "outputs": [".next/**", "!.next/cache/**"] 12 | }, 13 | "typecheck": {}, 14 | "dev": { 15 | "cache": false, 16 | "persistent": true, 17 | "dependsOn": ["^codegen"] 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/pages/api/_monitor/sentry.ts: -------------------------------------------------------------------------------- 1 | import { wrapApiHandlerWithSentry } from '@sentry/nextjs'; 2 | import type { NextApiRequest, NextApiResponse } from 'next'; 3 | 4 | // eslint-disable-next-line @typescript-eslint/require-await 5 | async function sentryMonitorApiRoute( 6 | _req: NextApiRequest, 7 | _res: NextApiResponse 8 | ): Promise { 9 | throw new Error( 10 | 'Error purposely crafted for monitoring sentry (/pages/api/_monitor/sentry.tsx)' 11 | ); 12 | } 13 | export default wrapApiHandlerWithSentry( 14 | sentryMonitorApiRoute, 15 | '/api/_monitor/sentry' 16 | ); 17 | -------------------------------------------------------------------------------- /packages/eslint-config-bases/src/bases/performance.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom config base for projects that wants to enable regexp rules. 3 | * @see https://github.com/belgattitude/shared-dx/tree/main/packages/eslint-config-bases 4 | */ 5 | 6 | const { filePatterns } = require('../config/file-patterns'); 7 | 8 | module.exports = { 9 | overrides: [ 10 | { 11 | plugins: ['unicorn'], 12 | files: filePatterns.typescriptAndJsCodeWithoutJsx, 13 | excludedFiles: filePatterns.nonCodeFile, 14 | rules: { 15 | 'unicorn/prefer-set-has': 'error', 16 | }, 17 | }, 18 | ], 19 | }; 20 | -------------------------------------------------------------------------------- /apps/nextjs-app/config/tests/ReactSvgrMock.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * This mock is useful if you're relying on https://react-svgr.com/. 3 | * 4 | * @link {https://react-svgr.com/docs/jest/|SVGR Jest doc} 5 | * @link {https://github.com/gregberge/svgr/issues/83#issuecomment-785996587|Config that actually works} 6 | */ 7 | 8 | import { type SVGProps, forwardRef } from 'react'; 9 | 10 | const SvgrMock = forwardRef>( 11 | (props, ref) => 12 | ); 13 | 14 | SvgrMock.displayName = 'SvgrMock'; 15 | 16 | export const ReactComponent = SvgrMock; 17 | export default SvgrMock; 18 | -------------------------------------------------------------------------------- /packages/eslint-config-bases/src/config/file-patterns.js: -------------------------------------------------------------------------------- 1 | const filePatterns = { 2 | test: ['**/?(*.)+(test|spec).{js,jsx,ts,tsx}'], 3 | anyCode: ['*.{js,jsx,mjs,jsx,tsx}'], 4 | typescriptCodeWithJsx: ['*.{ts,tsx}'], 5 | typescriptCodeWithoutJsx: ['*.ts'], 6 | typescriptAndJsCodeWithoutJsx: ['*.{js,mjs,ts}'], 7 | typescriptAndJsCodeWithJsx: ['*.{js,mjs,ts,jsx,tsx}'], 8 | storybook: ['**/*.stories.{ts,tsx,mdx}'], 9 | nonCodeFile: [ 10 | '**/?(*.)+(test).{js,jsx,ts,tsx}', 11 | '**/?(*.)+(bench).{js,jsx,ts,tsx}', 12 | '*.stories.{js,ts,jsx,tsx}', 13 | ], 14 | }; 15 | module.exports = { 16 | filePatterns, 17 | }; 18 | -------------------------------------------------------------------------------- /packages/eslint-config-bases/src/bases/graphql-schema.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Opinionated config base for projects using graphql schemas (*.graphql) 3 | * @see https://github.com/belgattitude/shared-dx/tree/main/packages/eslint-config-bases 4 | */ 5 | const graphqlSchemaPatterns = { 6 | files: ['*.graphql'], 7 | }; 8 | 9 | module.exports = { 10 | overrides: [ 11 | { 12 | // @see https://github.com/B2o5T/graphql-eslint 13 | extends: 'plugin:@graphql-eslint/schema-recommended', 14 | files: graphqlSchemaPatterns.files, 15 | rules: { 16 | '@graphql-eslint/known-type-names': 'error', 17 | }, 18 | }, 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /apps/nextjs-app/e2e/pages/_monitor/sentry.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test.describe('Sentry monitor pages', () => { 4 | test.describe('Client-side rendered', () => { 5 | test('should have a title containing error', async ({ page }) => { 6 | await page.goto('/_monitor/sentry/csr-page'); 7 | await expect(page).toHaveTitle(/error/i); 8 | }); 9 | }); 10 | 11 | test.describe('Server-side rendered', () => { 12 | test('should have a title containing error', async ({ page }) => { 13 | await page.goto('/_monitor/sentry/ssr-page'); 14 | await expect(page).toHaveTitle(/error/i); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /apps/nextjs-app/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | // Customized postcss 2 | // @link https://nextjs.org/docs/advanced-features/customizing-postcss-config 3 | // @link https://tailwindcss.com/docs/using-with-preprocessors 4 | 5 | const isProd = process.env.NODE_ENV === 'production'; 6 | 7 | module.exports = { 8 | plugins: { 9 | tailwindcss: {}, 10 | ...(isProd 11 | ? { 12 | 'postcss-preset-env': { 13 | // https://github.com/csstools/postcss-plugins/tree/main/plugin-packs/postcss-preset-env#stability-and-portability 14 | stage: 3, 15 | autoprefixer: { grid: true }, 16 | }, 17 | } 18 | : {}), 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /packages/db-main-prisma/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "../../tsconfig.base.json", 4 | "compilerOptions": { 5 | "baseUrl": "./src", 6 | "target": "esnext", 7 | "lib": ["esnext"], 8 | "module": "esnext", 9 | "noEmit": false, 10 | "incremental": true, 11 | "paths": {}, 12 | "types": ["vitest/globals"] 13 | }, 14 | "exclude": ["**/node_modules", "**/.*/*", "dist"], 15 | "include": [ 16 | ".eslintrc.*", 17 | "**/*.ts", 18 | "**/*.tsx", 19 | "**/*.mts", 20 | "**/*.js", 21 | "**/*.cjs", 22 | "**/*.mjs", 23 | "**/*.jsx", 24 | "**/*.json" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /apps/nextjs-app/e2e/README.md: -------------------------------------------------------------------------------- 1 | # WIP 2 | 3 | ## Launch server on the host 4 | 5 | ``` 6 | cd apps/nextjs-app 7 | yarn build && yarn start 8 | # or 9 | yarn dev 10 | ``` 11 | 12 | ## Launch docker 13 | 14 | ```bash 15 | export PLAYWRIGHT_VERSION=$(npm ls @playwright/test | grep '@playwright/test@' | grep -v 'deduped' | sed 's/.*@//' | uniq -u) 16 | docker run -it --rm --ipc=host --add-host=host.docker.internal:host-gateway -v $PWD:/app -w /app mcr.microsoft.com/playwright:v${PLAYWRIGHT_VERSION}-jammy /bin/bash 17 | ``` 18 | 19 | ## In the docker container 20 | 21 | ``` 22 | yarn install 23 | cd apps/nextjs-app/e2e 24 | npx playwright test --config $PWD/playwright.config.ts 25 | ``` 26 | -------------------------------------------------------------------------------- /packages/common-i18n/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "../../tsconfig.base.json", 4 | "compilerOptions": { 5 | "baseUrl": "./src", 6 | "target": "esnext", 7 | "lib": ["dom", "dom.iterable", "esnext"], 8 | "module": "esnext", 9 | "jsx": "react-jsx", 10 | "noEmit": false, 11 | "incremental": true, 12 | "paths": {} 13 | }, 14 | "exclude": ["**/node_modules", "**/.*/*", "dist"], 15 | "include": [ 16 | ".eslintrc.*", 17 | "**/*.ts", 18 | "**/*.tsx", 19 | "**/*.mts", 20 | "**/*.js", 21 | "**/*.cjs", 22 | "**/*.mjs", 23 | "**/*.jsx", 24 | "**/*.json" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /packages/eslint-config-bases/src/bases/regexp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom config base for projects that wants to enable regexp rules. 3 | * @see https://github.com/belgattitude/shared-dx/tree/main/packages/eslint-config-bases 4 | */ 5 | 6 | const { filePatterns } = require('../config/file-patterns'); 7 | 8 | module.exports = { 9 | // @see https://github.com/ota-meshi/eslint-plugin-regexp 10 | extends: ['plugin:regexp/recommended'], 11 | overrides: [ 12 | { 13 | extends: ['plugin:regexp/recommended'], 14 | files: filePatterns.typescriptAndJsCodeWithJsx, 15 | rules: { 16 | 'regexp/prefer-result-array-groups': 'off', 17 | }, 18 | }, 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/server/config/cors.config.ts: -------------------------------------------------------------------------------- 1 | import type { CorsOptions } from 'cors'; 2 | 3 | export const corsAllowedOrigins: string[] = [ 4 | // previews 5 | String.raw`.+\-belgattitude.vercel.app`, 6 | // for local development 7 | 'localhost', 8 | ]; 9 | 10 | export const getCorsWhitelistOriginRegexp = (allowedOrigins?: string[]) => { 11 | const origins = allowedOrigins ?? corsAllowedOrigins; 12 | return new RegExp( 13 | `^https?://(([^/])+\\.)?(${origins.join('|')})(\\:\\d+)?$`, 14 | 'i' 15 | ); 16 | }; 17 | 18 | type CorsDefaultOptions = Pick; 19 | 20 | export const corsDefaultOptions: CorsDefaultOptions = { 21 | maxAge: 3600, 22 | }; 23 | -------------------------------------------------------------------------------- /packages/ui-lib/src/base/card/BasicCard.styles.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | 3 | export const Ctn = styled.div` 4 | background-color: white; 5 | border-radius: 0.25rem; 6 | max-width: 300px; 7 | box-shadow: 8 | 0 0 #0000, 9 | 0 0 #0000, 10 | 0 4px 6px -1px rgba(0, 0, 0, 0.1), 11 | 0 2px 4px -1px rgba(0, 0, 0, 0.06); 12 | display: flex; 13 | flex-direction: column; 14 | overflow: hidden; 15 | &:hover { 16 | .card__image { 17 | filter: contrast(100%); 18 | } 19 | } 20 | img { 21 | max-height: 200px; 22 | object-fit: cover; 23 | object-position: top; 24 | } 25 | .container { 26 | padding: 15px; 27 | } 28 | `; 29 | -------------------------------------------------------------------------------- /packages/ts-utils/src/random/__tests__/getRandomInt.test.ts: -------------------------------------------------------------------------------- 1 | import { getRandomInt } from '../getRandomInt'; 2 | 3 | describe('getRandomInt tests', () => { 4 | it('should return an integer between min and max', () => { 5 | expect([100, 101]).toContain(getRandomInt(100, 101)); 6 | expect([-101, -100]).toContain(getRandomInt(-101, -100)); 7 | }); 8 | 9 | it('should throw if not a number', () => { 10 | expect(() => getRandomInt(NaN, 100)).toThrow(/min/i); 11 | expect(() => getRandomInt(10, {} as unknown as number)).toThrow(/max/i); 12 | }); 13 | 14 | it('should throw if min > max', () => { 15 | expect(() => getRandomInt(100, 10)).toThrow(/greater/i); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/features/auth/pages/LoginPage.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from 'next-i18next'; 2 | import { NextSeo } from 'next-seo'; 3 | import type { FC } from 'react'; 4 | import { authConfig } from '@/features/auth/auth.config'; 5 | import { LoginForm } from '@/features/auth/components/LoginForm'; 6 | 7 | export const LoginPage: FC = () => { 8 | const { t } = useTranslation(authConfig.i18nNamespaces); 9 | const redirectToPage = '/admin'; 10 | return ( 11 | <> 12 | 13 |
14 | 15 |
16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | name: nextjs-monorepo-example-db 2 | 3 | services: 4 | main-db: 5 | container_name: nextjs-monorepo-example-db 6 | image: postgres:16.3-alpine3.18 7 | ports: 8 | - target: 5432 9 | published: 5432 10 | protocol: tcp 11 | environment: 12 | - POSTGRES_DB=maindb 13 | - POSTGRES_PASSWORD=!ChangeMe! 14 | - POSTGRES_USER=nextjs 15 | volumes: 16 | - db_data:/var/lib/postgresql/data:rw 17 | # you may use a bind-mounted host directory instead, 18 | # so that it is harder to accidentally remove the volume and lose all your data! 19 | # - ./docker/db/data:/var/lib/postgresql/data:rw 20 | 21 | volumes: 22 | db_data: 23 | -------------------------------------------------------------------------------- /packages/db-main-prisma/prisma/seed.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClientDbMain } from '../src'; 2 | import { PoemSeeds, UserSeeds } from '../src/seeds'; 3 | 4 | const prisma = new PrismaClientDbMain(); 5 | 6 | async function main() { 7 | console.log(`Start seeding ...`); 8 | 9 | const userSeeds = new UserSeeds(prisma); 10 | await userSeeds.execute(); 11 | 12 | const companySeeds = new PoemSeeds(prisma); 13 | await companySeeds.execute(); 14 | 15 | console.log(`Seeding finished.`); 16 | } 17 | 18 | try { 19 | await main(); 20 | } catch (e) { 21 | console.error(e); 22 | // eslint-disable-next-line unicorn/no-process-exit 23 | process.exit(1); 24 | } finally { 25 | await prisma.$disconnect(); 26 | } 27 | -------------------------------------------------------------------------------- /packages/ui-lib/src/_stories/header.css: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 4 | padding: 15px 20px; 5 | display: flex; 6 | align-items: center; 7 | justify-content: space-between; 8 | } 9 | 10 | svg { 11 | display: inline-block; 12 | vertical-align: top; 13 | } 14 | 15 | h1 { 16 | font-weight: 700; 17 | font-size: 20px; 18 | line-height: 1; 19 | margin: 6px 0 6px 10px; 20 | display: inline-block; 21 | vertical-align: top; 22 | } 23 | 24 | button + button { 25 | margin-left: 10px; 26 | } 27 | 28 | .welcome { 29 | color: #333; 30 | font-size: 14px; 31 | margin-right: 10px; 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "moduleResolution": "node", 5 | "verbatimModuleSyntax": true, 6 | "strict": true, 7 | "useUnknownInCatchVariables": true, 8 | "noImplicitOverride": true, 9 | "noUncheckedIndexedAccess": true, 10 | "allowUnreachableCode": false, 11 | "noFallthroughCasesInSwitch": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "allowJs": true, 14 | "resolveJsonModule": true, 15 | "skipLibCheck": true, 16 | "noEmit": true, 17 | "esModuleInterop": true, 18 | "incremental": true, 19 | "newLine": "lf" 20 | }, 21 | "exclude": ["**/node_modules", "**/.*/"] 22 | } 23 | -------------------------------------------------------------------------------- /docs/benchmark/jest-vs-vitest.md: -------------------------------------------------------------------------------- 1 | # Unit tests bench 2 | 3 | Vitest: 4 secs 4 | Jest: 29 secs 5 | 6 | ### Vitest 7 | 8 | - Vitest 0.12.6 9 | - Happy-dom 3+ 10 | - Coverage with c8 11 | 12 | ``` 13 | hyperfine "yarn vitest run --coverage" 14 | Benchmark 1: yarn vitest run --coverage 15 | Time (mean ± σ): 4.205 s ± 0.446 s [User: 11.726 s, System: 0.869 s] 16 | Range (min … max): 3.803 s … 5.236 s 10 runs 17 | ``` 18 | 19 | ### Jest 20 | 21 | - Jest 28.1.0 + ts-jest 22 | 23 | ``` 24 | hyperfine "yarn jest --coverage" 25 | Benchmark 1: yarn jest --coverage 26 | Time (mean ± σ): 29.022 s ± 0.897 s [User: 273.365 s, System: 8.163 s] 27 | Range (min … max): 28.096 s … 30.664 s 10 runs 28 | ``` 29 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/features/demo/api/fetch-poems.api.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isJsonApiSuccessResponse, 3 | type JsonApiResponse, 4 | } from '@httpx/json-api'; 5 | import { apiFetcher } from '@/config/api-fetcher.config'; 6 | import type { SearchPoems } from '@/server/features/poem/SearchPoems'; 7 | 8 | export const fetchPoems = async (): Promise => { 9 | return apiFetcher>('/api/rest/poem').then( 10 | (resp) => { 11 | if (!isJsonApiSuccessResponse(resp)) { 12 | throw new Error( 13 | // @todo improve error reporting 14 | `Error fetching poems: ${JSON.stringify(resp?.errors ?? '')}` 15 | ); 16 | } 17 | return resp.data; 18 | } 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /packages/db-main-prisma/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "target": "ES2022", 6 | "module": "ESNext", 7 | "moduleResolution": "Bundler", 8 | "isolatedModules": true, 9 | "verbatimModuleSyntax": false, 10 | "allowSyntheticDefaultImports": true, 11 | "allowJs": false, 12 | "noEmit": false, 13 | "declaration": true, 14 | "declarationMap": false, 15 | "declarationDir": "./dist", 16 | "tsBuildInfoFile": "./tsconfig.build.tsbuildinfo" // for tsup rollup dts tsBuildInfoFile is required when using incremental 17 | }, 18 | "exclude": ["*.test.ts", "**/node_modules", "**/.*/", "./dist"] 19 | } 20 | -------------------------------------------------------------------------------- /apps/nextjs-app/e2e/pages/index/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import homeJsonEn from '@your-org/common-i18n/locales/en/demo.json' assert { type: 'json' }; 3 | import homeJsonFr from '@your-org/common-i18n/locales/fr/demo.json' assert { type: 'json' }; 4 | 5 | test.describe('Demo page', () => { 6 | test('should have the title in english by default', async ({ page }) => { 7 | await page.goto('/'); 8 | const title = await page.title(); 9 | expect(title).toBe(homeJsonEn.page.title); 10 | }); 11 | test('should have the title in french', async ({ page }) => { 12 | await page.goto('/fr'); 13 | const title = await page.title(); 14 | expect(title).toBe(homeJsonFr.page.title); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/common-i18n/src/I18nResources.types.ts: -------------------------------------------------------------------------------- 1 | import type admin from './locales/en/admin.json'; 2 | import type auth from './locales/en/auth.json'; 3 | import type blog from './locales/en/blog.json'; 4 | import type common from './locales/en/common.json'; 5 | import type demo from './locales/en/demo.json'; 6 | import type home from './locales/en/home.json'; 7 | import type navigation from './locales/en/navigation.json'; 8 | import type system from './locales/en/system.json'; 9 | 10 | export interface I18nResources { 11 | admin: typeof admin; 12 | auth: typeof auth; 13 | blog: typeof blog; 14 | common: typeof common; 15 | demo: typeof demo; 16 | home: typeof home; 17 | navigation: typeof navigation; 18 | system: typeof system; 19 | } 20 | -------------------------------------------------------------------------------- /packages/ui-lib/src/base/button/Button.styles.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | 3 | export const cssButtonStyle = css` 4 | font-weight: 700; 5 | border: 0; 6 | border-radius: 3em; 7 | cursor: pointer; 8 | display: inline-block; 9 | line-height: 1; 10 | &.primary { 11 | color: white; 12 | background-color: #1ea7fd; 13 | } 14 | &.secondary { 15 | color: #333; 16 | background-color: transparent; 17 | box-shadow: rgba(0, 0, 0, 0.15) 0 0 0 1px inset; 18 | } 19 | &.small { 20 | font-size: 12px; 21 | padding: 10px 16px; 22 | } 23 | &.medium { 24 | font-size: 14px; 25 | padding: 11px 20px; 26 | } 27 | &.large { 28 | font-size: 16px; 29 | padding: 12px 24px; 30 | } 31 | `; 32 | -------------------------------------------------------------------------------- /packages/ts-utils/src/convert/string-convert.ts: -------------------------------------------------------------------------------- 1 | import { isParsableNumeric, isParsableSafeInteger } from '../typeguards'; 2 | 3 | export function stringToSafeInteger(value: unknown): number | null { 4 | if (!isParsableSafeInteger(value)) { 5 | return null; 6 | } 7 | return typeof value === 'string' ? Number.parseInt(value, 10) : value; 8 | } 9 | 10 | export function stringToFloat(value: unknown): number | null { 11 | if ( 12 | !isParsableNumeric( 13 | typeof value === 'number' ? value.toString(10) : (value ?? '') 14 | ) 15 | ) { 16 | return null; 17 | } 18 | const v = Number.parseFloat( 19 | typeof value === 'string' ? value : (value as number).toString(10) 20 | ); 21 | return Number.isNaN(v) ? null : v; 22 | } 23 | -------------------------------------------------------------------------------- /apps/nextjs-app/config/tests/AppTestProviders.tsx: -------------------------------------------------------------------------------- 1 | import type { Session } from 'next-auth'; 2 | import type { FC, PropsWithChildren } from 'react'; 3 | import { AppProviders } from '../../src/providers/AppProviders'; 4 | import { I18nextTestStubProvider } from './I18nextTestStubProvider'; 5 | 6 | const fakeNextAuthSession: Session = { 7 | user: { 8 | email: 'test@example.com', 9 | role: 'guest', 10 | name: 'AppTestProvider', 11 | }, 12 | expires: '2050-01-01T00:00:00.000Z', 13 | }; 14 | 15 | export const AppTestProviders: FC = ({ children }) => { 16 | return ( 17 | 18 | {children} 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /packages/eslint-config-bases/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | const { getDefaultIgnorePatterns } = require('./src/helpers'); 2 | 3 | module.exports = { 4 | extends: [ 5 | './src/bases/typescript', 6 | './src/bases/simple-import-sort', 7 | './src/bases/import-x', 8 | './src/bases/sonar', 9 | './src/bases/regexp', 10 | './src/bases/perfectionist', 11 | './src/bases/performance', 12 | './src/bases/prettier-plugin', 13 | './src/bases/mdx', 14 | ], 15 | ignorePatterns: [...getDefaultIgnorePatterns()], 16 | parser: '@typescript-eslint/parser', 17 | parserOptions: { 18 | projectService: true, 19 | tsconfigRootDir: __dirname, 20 | }, 21 | root: true, 22 | rules: { 23 | '@typescript-eslint/no-require-imports': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /packages/ui-lib/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/react-vite'; 2 | 3 | const config: StorybookConfig = { 4 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], 5 | addons: [ 6 | '@storybook/addon-links', 7 | '@storybook/addon-essentials', 8 | // Not working in storybook v7 + vite 9 | // '@storybook/addon-interactions', 10 | ], 11 | framework: { 12 | name: '@storybook/react-vite', 13 | options: { 14 | strictMode: true, 15 | builder: { 16 | viteConfigPath: './vite.config.storybook.ts', 17 | }, 18 | }, 19 | }, 20 | docs: { 21 | autodocs: 'tag', 22 | defaultName: 'Docs', // set to change the name of generated docs entries 23 | }, 24 | }; 25 | export default config; 26 | -------------------------------------------------------------------------------- /packages/db-main-prisma/docker-compose.e2e.yml: -------------------------------------------------------------------------------- 1 | name: nextjs-monorepo-example-db-main-prisma-e2e 2 | services: 3 | e2e-postgres: 4 | container_name: db-main-prisma-e2e-postgresql 5 | image: postgres:16.3-alpine3.18 6 | ports: 7 | - target: 5432 8 | published: 5432 9 | protocol: tcp 10 | environment: 11 | POSTGRES_USER: postgres 12 | POSTGRES_PASSWORD: postgres 13 | POSTGRES_DB: e2edb 14 | POSTGRES_INITDB_ARGS: '--locale-provider=icu --icu-locale=en-US --no-sync' 15 | healthcheck: 16 | test: ['CMD-SHELL', "sh -c 'pg_isready -U postgres -d e2edb'"] 17 | interval: 2s 18 | timeout: 5s 19 | retries: 5 20 | volumes: 21 | - e2e_postgres:/var/lib/postgresql/data:rw 22 | volumes: 23 | e2e_postgres: 24 | -------------------------------------------------------------------------------- /packages/eslint-config-bases/src/bases/mdx.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Opinionated config base for https://github.com/mdx-js/eslint-mdx 3 | * @see https://github.com/belgattitude/shared-dx/tree/main/packages/eslint-config-bases 4 | */ 5 | 6 | const mdxPatterns = { 7 | files: ['*.mdx'], 8 | }; 9 | 10 | module.exports = { 11 | overrides: [ 12 | { 13 | extends: [ 14 | 'plugin:mdx/recommended', 15 | 'plugin:@typescript-eslint/disable-type-checked', 16 | ], 17 | // For performance enable this only on mdx files 18 | files: mdxPatterns.files, 19 | parser: 'eslint-mdx', 20 | parserOptions: { 21 | project: null, 22 | }, 23 | rules: { 24 | '@typescript-eslint/consistent-type-exports': 'off', 25 | }, 26 | }, 27 | ], 28 | }; 29 | -------------------------------------------------------------------------------- /packages/ui-lib/src/_stories/button.css: -------------------------------------------------------------------------------- 1 | .storybook-button { 2 | font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | font-weight: 700; 4 | border: 0; 5 | border-radius: 3em; 6 | cursor: pointer; 7 | display: inline-block; 8 | line-height: 1; 9 | } 10 | .storybook-button--primary { 11 | color: white; 12 | background-color: #1ea7fd; 13 | } 14 | .storybook-button--secondary { 15 | color: #333; 16 | background-color: transparent; 17 | box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; 18 | } 19 | .storybook-button--small { 20 | font-size: 12px; 21 | padding: 10px 16px; 22 | } 23 | .storybook-button--medium { 24 | font-size: 14px; 25 | padding: 11px 20px; 26 | } 27 | .storybook-button--large { 28 | font-size: 16px; 29 | padding: 12px 24px; 30 | } 31 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/types.d/react-svgr.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An example declaration for svg if you're relying on https://react-svgr.com/ 3 | * and @svgr/webpack equivalent strategy. 4 | * 5 | * This definition will improve type completion experience. 6 | * 7 | * @link {https://github.com/gregberge/svgr/issues/546|For issue followup} 8 | * @link {https://github.com/gregberge/svgr/pull/573|To follow upcoming improvements} 9 | * 10 | * If you're NOT using @svgr/webpack, be sure the svg definition is equivalent to 11 | * 12 | * ``` 13 | * declare module "*.svg" { 14 | * const svg: string; 15 | * export default svg; 16 | * } 17 | * ``` 18 | */ 19 | 20 | declare module '*.svg' { 21 | import type { FC, SVGProps } from 'react'; 22 | const svg: FC>; 23 | export default svg; 24 | } 25 | -------------------------------------------------------------------------------- /apps/vite-app/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | 40 | button { 41 | font-size: calc(10px + 2vmin); 42 | } 43 | -------------------------------------------------------------------------------- /apps/nextjs-app/e2e/api/graphql/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import { isNonEmptyString, isParsableNumeric } from '@your-org/ts-utils'; 3 | 4 | test('should call the getUser graphql endpoint', async ({ request }) => { 5 | const resp = await request.post('/api/graphql', { 6 | data: { 7 | query: `query { getUser(id: 1) { email, id } }`, 8 | }, 9 | }); 10 | await expect(resp).toBeOK(); 11 | const headers = resp.headers(); 12 | expect(headers['content-type']).toEqual('application/json; charset=utf-8'); 13 | const json = (await resp.json()) as { 14 | data?: { getUser?: { id: string; email: string } }; 15 | }; 16 | const { id, email } = json?.data?.getUser ?? {}; 17 | expect(isNonEmptyString(email)).toBeTruthy(); 18 | expect(isParsableNumeric(id)).toBeTruthy(); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/eslint-config-bases/src/bases/import-x.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom config base for projects using jest. 3 | * @see https://github.com/belgattitude/shared-dx/tree/main/packages/eslint-config-bases 4 | */ 5 | const { filePatterns } = require('../config/file-patterns'); 6 | 7 | module.exports = { 8 | extends: ['plugin:import-x/typescript', 'plugin:import-x/recommended'], 9 | rules: { 10 | 'import-x/no-unused-modules': 'warn', 11 | 'import-x/no-absolute-path': 'error', 12 | 'import-x/no-useless-path-segments': 'error', 13 | }, 14 | overrides: [ 15 | { 16 | files: filePatterns.test, 17 | rules: { 18 | 'import-x/namespace': 'off', 19 | }, 20 | }, 21 | ], 22 | settings: { 23 | 'import-x/resolver': { 24 | typescript: true, 25 | node: true, 26 | }, 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import type { GetStaticPropsContext, InferGetStaticPropsType } from 'next'; 2 | import { NotFoundPage } from '@/features/system/pages'; 3 | import { systemConfig } from '@/features/system/system.config'; 4 | import { getServerTranslations } from '@/server/i18n/getServerTranslations'; 5 | 6 | export const getStaticProps = async (context: GetStaticPropsContext) => { 7 | const { locale = 'en' } = context; 8 | 9 | const inlinedTranslation = await getServerTranslations( 10 | locale, 11 | systemConfig.i18nNamespaces 12 | ); 13 | 14 | return { 15 | props: { 16 | locale: locale, 17 | ...inlinedTranslation, 18 | }, 19 | }; 20 | }; 21 | 22 | export default function Custom404( 23 | _props: InferGetStaticPropsType 24 | ) { 25 | return ; 26 | } 27 | -------------------------------------------------------------------------------- /packages/eslint-config-bases/src/bases/rtl.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Opinionated config base for projects using react-testing-library 3 | * @see https://github.com/belgattitude/shared-dx/tree/main/packages/eslint-config-bases 4 | */ 5 | 6 | const rtlPatterns = { 7 | files: ['**/?(*.)+(test).{js,jsx,ts,tsx}'], 8 | }; 9 | 10 | module.exports = { 11 | env: { 12 | browser: true, 13 | es6: true, 14 | node: true, 15 | }, 16 | overrides: [ 17 | { 18 | extends: ['plugin:testing-library/react'], 19 | // For performance enable react-testing-library only on test files 20 | files: rtlPatterns.files, 21 | }, 22 | { 23 | files: ['**/test-utils.tsx'], 24 | rules: { 25 | '@typescript-eslint/explicit-module-boundary-types': 'off', 26 | 'import/export': 'off', 27 | }, 28 | }, 29 | ], 30 | }; 31 | -------------------------------------------------------------------------------- /apps/nextjs-app/e2e/pages/system/404.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import systemJsonEn from '@your-org/common-i18n/locales/en/system.json' assert { type: 'json' }; 3 | import systemJsonFr from '@your-org/common-i18n/locales/fr/system.json' assert { type: 'json' }; 4 | 5 | const pageSlug = 'this-page-does-not-exist'; 6 | 7 | test.describe('404 not found page', () => { 8 | test('should have the title in english by default', async ({ page }) => { 9 | await page.goto(`/${pageSlug}`); 10 | const title = await page.title(); 11 | expect(title).toBe(systemJsonEn.notFound.title); 12 | }); 13 | test('should have the title in french', async ({ page }) => { 14 | await page.goto(`/fr/${pageSlug}`); 15 | const title = await page.title(); 16 | expect(title).toBe(systemJsonFr.notFound.title); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/lib/i18n/getSortedI18nNamespaces.ts: -------------------------------------------------------------------------------- 1 | import type { I18nActiveNamespaces } from '@/lib/i18n/I18nNamespace.types'; 2 | 3 | /** 4 | * Ensure that the i18nNamespaces are sorted alphabetically when passed in feature configs. 5 | * That should help typescript when compatible groups (ie ['common', 'home'] and ['home', 'common']) 6 | * should not produce a different union type. 7 | * 8 | * @throws Error if the namespaces are not sorted alphabetically. 9 | */ 10 | export const getSortedI18nNamespaces = ( 11 | ns: T 12 | ) => { 13 | if ([...ns].sort((a, b) => a.localeCompare(b)).toString() !== ns.toString()) { 14 | throw new Error( 15 | `Config error: i18nNamespaces should be sorted for best performance ${JSON.stringify( 16 | ns 17 | )}` 18 | ); 19 | } 20 | return ns; 21 | }; 22 | -------------------------------------------------------------------------------- /packages/eslint-config-bases/src/bases/react-query.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Opinionated config base for projects using react. 3 | * @see https://github.com/belgattitude/shared-dx/tree/main/packages/eslint-config-bases 4 | */ 5 | 6 | const reactPatterns = { 7 | files: ['*.{jsx,tsx}'], 8 | }; 9 | 10 | /** 11 | * Fine-tune naming convention react typescript jsx (function components) 12 | * @link https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/naming-convention.md 13 | */ 14 | 15 | module.exports = { 16 | overrides: [ 17 | { 18 | extends: [ 19 | // @see https://tanstack.com/query/v4/docs/react/eslint/eslint-plugin-query 20 | 'plugin:@tanstack/eslint-plugin-query/recommended', 21 | ], 22 | files: [...reactPatterns.files], 23 | // rules: { }, 24 | }, 25 | ], 26 | }; 27 | -------------------------------------------------------------------------------- /docker/docker-compose.db.yml: -------------------------------------------------------------------------------- 1 | name: nextjs-monorepo-example-db 2 | 3 | services: 4 | main-db: 5 | container_name: nextjs-monorepo-example-db 6 | image: postgres:17-alpine3.20 7 | ports: 8 | - target: 5432 9 | published: 5432 10 | protocol: tcp 11 | environment: 12 | - POSTGRES_DB=maindb 13 | - POSTGRES_PASSWORD=!ChangeMe! 14 | - POSTGRES_USER=nextjs 15 | networks: 16 | - nextjs-monorepo-example-network 17 | volumes: 18 | - db_data:/var/lib/postgresql/data:rw 19 | # you may use a bind-mounted host directory instead, 20 | # so that it is harder to accidentally remove the volume and lose all your data! 21 | # - ./docker/db/data:/var/lib/postgresql/data:rw 22 | 23 | volumes: 24 | db_data: 25 | 26 | networks: 27 | nextjs-monorepo-example-network: 28 | driver: bridge 29 | -------------------------------------------------------------------------------- /packages/core-lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "../../tsconfig.base.json", 4 | "compilerOptions": { 5 | "baseUrl": "./src", 6 | "target": "ESNext", 7 | "lib": ["dom", "dom.iterable", "esnext"], 8 | "module": "ESNext", 9 | "jsx": "react-jsx", 10 | "noEmit": false, 11 | "incremental": true, 12 | "paths": { 13 | "@/test-utils": ["../config/test/test-utils"], 14 | "@your-org/ts-utils": ["../../../packages/ts-utils/src/index"] 15 | }, 16 | "types": ["vitest/globals"] 17 | }, 18 | "exclude": ["**/node_modules", "**/.*/*", "dist"], 19 | "include": [ 20 | ".eslintrc.*", 21 | "**/*.ts", 22 | "**/*.tsx", 23 | "**/*.mts", 24 | "**/*.js", 25 | "**/*.cjs", 26 | "**/*.mjs", 27 | "**/*.jsx", 28 | "**/*.json" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/ci-codegen.yml: -------------------------------------------------------------------------------- 1 | name: CI-codegen-sync 2 | 3 | on: 4 | push: 5 | branches: [dev, main] 6 | pull_request: 7 | types: [opened, synchronize, reopened] 8 | 9 | jobs: 10 | codegen: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [20.x] 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | 23 | - name: 📥 Monorepo install 24 | uses: ./.github/actions/yarn-nm-install 25 | 26 | - name: 🏗 Run codegen 27 | run: yarn workspaces foreach -A -tv run codegen 28 | 29 | - name: 🛟 Check if codegen is in sync 30 | shell: bash 31 | run: bash ./scripts/check-git-pristine.sh 32 | -------------------------------------------------------------------------------- /packages/eslint-config-bases/src/bases/playwright.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Opinionated config base for projects using playwright. 3 | * @see https://github.com/belgattitude/shared-dx/tree/main/packages/eslint-config-bases 4 | */ 5 | 6 | const playwrightPatterns = { 7 | files: ['**/e2e/**/*.test.{js,ts}'], 8 | }; 9 | 10 | module.exports = { 11 | overrides: [ 12 | { 13 | // @see https://github.com/playwright-community/eslint-plugin-playwright 14 | extends: ['plugin:playwright/recommended'], 15 | // To ensure best performance enable only on e2e test files 16 | files: playwrightPatterns.files, 17 | rules: { 18 | '@typescript-eslint/no-empty-function': 'off', 19 | '@typescript-eslint/no-non-null-assertion': 'off', 20 | '@typescript-eslint/no-object-literal-type-assertion': 'off', 21 | }, 22 | }, 23 | ], 24 | }; 25 | -------------------------------------------------------------------------------- /packages/ts-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "@belgattitude/ts-utils", 4 | "extends": "../../tsconfig.base.json", 5 | "compilerOptions": { 6 | "module": "ESNext", 7 | "moduleResolution": "node", 8 | "target": "ESNext", 9 | "lib": ["ESNext"], 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "baseUrl": "./src", 13 | "noEmit": false, 14 | "incremental": true, 15 | "resolveJsonModule": true, 16 | "paths": {}, 17 | "types": ["vitest/globals"] 18 | }, 19 | "exclude": ["**/node_modules", "**/.*/*", "dist"], 20 | "include": [ 21 | ".eslintrc.*", 22 | "**/*.ts", 23 | "**/*.tsx", 24 | "**/*.mts", 25 | "**/*.js", 26 | "**/*.cjs", 27 | "**/*.mjs", 28 | "**/*.jsx", 29 | "**/*.json" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /packages/ui-lib/src/base/card/BasicCard.tsx: -------------------------------------------------------------------------------- 1 | // To test out support for emotion-11/styled in storybook 2 | import { isNonEmptyString } from '@your-org/ts-utils'; 3 | import type { FC } from 'react'; 4 | import * as S from './BasicCard.styles'; 5 | 6 | type Props = { 7 | image?: string; 8 | title: string; 9 | description: string; 10 | }; 11 | 12 | export const BasicCard: FC = (props) => { 13 | const { image, title, description } = props; 14 | const imgSrc = isNonEmptyString(image) ? image : undefined; 15 | return ( 16 | 17 | {imgSrc === undefined ? null : ( 18 | something 19 | )} 20 |
21 |

22 | {title} 23 |

24 |

{description}

25 |
26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /packages/ui-lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "../../tsconfig.base.json", 4 | "compilerOptions": { 5 | "baseUrl": "./src", 6 | "target": "esnext", 7 | "lib": ["dom", "dom.iterable", "esnext"], 8 | "module": "esnext", 9 | "jsx": "react-jsx", 10 | "jsxImportSource": "@emotion/react", 11 | "noEmit": false, 12 | "incremental": true, 13 | "paths": { 14 | "@your-org/ts-utils": ["../../../packages/ts-utils/src/index"] 15 | }, 16 | "types": ["@testing-library/jest-dom/vitest"] 17 | }, 18 | "exclude": ["**/node_modules", "**/.*/*", "dist"], 19 | "include": [ 20 | ".eslintrc.*", 21 | "**/*.ts", 22 | "**/*.tsx", 23 | "**/*.mts", 24 | "**/*.js", 25 | "**/*.cjs", 26 | "**/*.mjs", 27 | "**/*.jsx", 28 | "**/*.json" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented 4 | in their respective packages and apps. Changelog generation is 5 | done through [atlassian/changesets](https://github.com/atlassian/changesets). 6 | 7 | Apps: 8 | 9 | - [nextjs-app/CHANGELOG.md](./apps/nextjs-app/CHANGELOG.md) 10 | - [remix-app/CHANGELOG.md](./apps/remix-app/CHANGELOG.md) 11 | - [vite-app/CHANGELOG.md](./apps/vite-app/CHANGELOG.md) 12 | 13 | Packages: 14 | 15 | - [api-gateway/CHANGELOG.md](./packages/api-gateway/CHANGELOG.md) 16 | - [common-i18n/CHANGELOG.md](./packages/common-i18n/CHANGELOG.md) 17 | - [core-lib/CHANGELOG.md](./packages/core-lib/CHANGELOG.md) 18 | - [db-main-prisma/CHANGELOG.md](./packages/db-main-prisma/CHANGELOG.md) 19 | - [ui-lib/CHANGELOG.md](./packages/ui-lib/CHANGELOG.md) 20 | - [graphql-mesh/CHANGELOG.md](./packages/graphql-mesh/CHANGELOG.md) 21 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/features/home/pages/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from 'next-i18next'; 2 | import { NextSeo } from 'next-seo'; 3 | import type { FC } from 'react'; 4 | import { Banner } from '@/components/banner/Banner'; 5 | import { MainLayout } from '@/layouts/main'; 6 | import { CtaBlock, FeaturesBlock, HeroBlock } from '../blocks'; 7 | import { homeConfig } from '../home.config'; 8 | 9 | export const HomePage: FC = () => { 10 | const { t } = useTranslation(homeConfig.i18nNamespaces); 11 | 12 | return ( 13 | <> 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/types.d/env.d.ts: -------------------------------------------------------------------------------- 1 | import { type z } from 'zod'; 2 | import { type serverRuntimeEnvSchema } from '../config/server-runtime-env.config.mjs'; 3 | 4 | /** 5 | * runtime-server-env.mjs does not mutate process.env. So the exact types can't be 6 | * known at process.env level. Best effort is to type the keys as string | undefined. 7 | */ 8 | type ProcessEnvKeyForServerEnv = Record< 9 | keyof z.infer, 10 | string | undefined 11 | >; 12 | 13 | type AugmentedProcessEnv = ProcessEnvKeyForServerEnv & { 14 | // By nextjs convention, these can be used 15 | HOSTNAME: string | undefined; 16 | PORT: string | undefined; 17 | }; 18 | 19 | declare global { 20 | declare namespace NodeJS { 21 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 22 | export interface ProcessEnv extends AugmentedProcessEnv {} 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/ui-lib/src/base/card/BasicCard.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { StoryObj, Meta } from '@storybook/react'; 2 | import { BasicCard } from './BasicCard'; 3 | 4 | // More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 5 | export default { 6 | title: 'Card/BasicCard', 7 | component: BasicCard, 8 | // More on argTypes: https://storybook.js.org/docs/react/api/argtypes 9 | // argTypes: { 10 | // bg: { 11 | // options: ['sky', 'green', 'blue', 'red'], 12 | // }, 13 | // }, 14 | } as Meta; 15 | 16 | export const BasicCardExample: StoryObj = { 17 | render: (_args) => ( 18 |
19 | 24 |
25 | ), 26 | }; 27 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/components/avatar/TextAvatar.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | 3 | type FullName = { 4 | firstName: string; 5 | lastName?: string; 6 | }; 7 | 8 | type Props = { 9 | name: string | FullName; 10 | className?: string; 11 | }; 12 | 13 | const mapToFullName = (nameOrFullName: string | FullName): FullName => { 14 | if (typeof nameOrFullName === 'string') { 15 | const [firstName = 'F', lastName = 'L'] = nameOrFullName.split(' '); 16 | return { 17 | firstName, 18 | lastName, 19 | }; 20 | } 21 | return nameOrFullName; 22 | }; 23 | 24 | export const TextAvatar: FC = (props) => { 25 | const { name, className = '' } = props; 26 | const { firstName, lastName } = mapToFullName(name); 27 | return ( 28 |
29 | {firstName} 30 | {lastName} 31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/eslint-config-bases/src/bases/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | graphqlSchema: require('./graphql-schema'), 3 | 'import-x': require('./import-x'), 4 | jest: require('./jest'), 5 | mdx: require('./mdx'), 6 | perfectionist: require('./perfectionist'), 7 | 'perfectionist-jsx': require('./perfectionist-jsx'), 8 | playwright: require('./playwright'), 9 | 'prettier-config': require('./prettier-config'), 10 | 'prettier-plugin': require('./prettier-plugin'), 11 | performance: require('./performance'), 12 | react: require('./react'), 13 | reactQuery: require('./react-query'), 14 | reactTestingLibrary: require('./rtl'), 15 | regexp: require('./regexp'), 16 | 'simple-import-sort': require('./simple-import-sort'), 17 | sonar: require('./sonar'), 18 | storybook: require('./storybook'), 19 | tailwind: require('./tailwind'), 20 | typescript: require('./typescript'), 21 | }; 22 | -------------------------------------------------------------------------------- /apps/nextjs-app/public/images/favicon/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/core-lib/config/test/test-utils.ts: -------------------------------------------------------------------------------- 1 | import { cleanup, render } from '@testing-library/react'; 2 | import type React from 'react'; 3 | 4 | import { afterEach } from 'vitest'; 5 | 6 | // The condition below is only needed to support both jest and vitest. 7 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 8 | // @ts-ignore 9 | if (afterEach) { 10 | afterEach(() => { 11 | cleanup(); 12 | }); 13 | } 14 | 15 | const customRender = (ui: React.ReactElement, options = {}) => 16 | render(ui, { 17 | // wrap provider(s) here if needed 18 | wrapper: ({ children }) => children, 19 | ...options, 20 | }); 21 | 22 | // eslint-disable-next-line import-x/export 23 | export * from '@testing-library/react'; 24 | export { default as userEvent } from '@testing-library/user-event'; 25 | // override render export 26 | // eslint-disable-next-line import-x/export 27 | export { customRender as render }; 28 | -------------------------------------------------------------------------------- /packages/ui-lib/config/test/test-utils.ts: -------------------------------------------------------------------------------- 1 | import { cleanup, render } from '@testing-library/react'; 2 | import type React from 'react'; 3 | 4 | import { afterEach } from 'vitest'; 5 | 6 | // The condition below is only needed to support both jest and vitest. 7 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 8 | // @ts-ignore 9 | if (afterEach) { 10 | afterEach(() => { 11 | cleanup(); 12 | }); 13 | } 14 | 15 | const customRender = (ui: React.ReactElement, options = {}) => 16 | render(ui, { 17 | // wrap provider(s) here if needed 18 | wrapper: ({ children }) => children, 19 | ...options, 20 | }); 21 | 22 | // eslint-disable-next-line import-x/export 23 | export * from '@testing-library/react'; 24 | export { default as userEvent } from '@testing-library/user-event'; 25 | // override render export 26 | // eslint-disable-next-line import-x/export 27 | export { customRender as render }; 28 | -------------------------------------------------------------------------------- /apps/nextjs-app/e2e/api/_monitor/healthcheck.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import { isIsoDateString } from '@your-org/ts-utils'; 3 | import type { HealthCheckApiPayload } from '@/pages/api/_monitor/healthcheck'; 4 | import packageJson from '../../../package.json' assert { type: 'json' }; 5 | 6 | test('should return a success payload', async ({ request }) => { 7 | const resp = await request.get('/api/_monitor/healthcheck'); 8 | const headers = resp.headers(); 9 | const json = (await resp.json()) as HealthCheckApiPayload; 10 | const { timestamp, ...restJson } = json; 11 | expect(headers['content-type']).toEqual('application/json'); 12 | expect(isIsoDateString(timestamp)).toBeTruthy(); 13 | expect(restJson).toMatchObject({ 14 | status: 'ok', 15 | message: 'Health check successful for API route', 16 | appName: packageJson.name, 17 | appVersion: packageJson.version, 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/pages/api/_monitor/healthcheck.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next'; 2 | 3 | export type HealthCheckApiPayload = { 4 | status: 'ok' | 'error'; 5 | message: string; 6 | appName: string; 7 | appVersion: string; 8 | timestamp: string; 9 | }; 10 | 11 | export default function healthCheckApiRoute( 12 | req: NextApiRequest, 13 | res: NextApiResponse 14 | ) { 15 | if (req.method !== 'GET') { 16 | res.status(400).end(); 17 | return; 18 | } 19 | 20 | res.setHeader('Content-Type', 'application/json'); 21 | 22 | const payload: HealthCheckApiPayload = { 23 | status: 'ok', 24 | message: 'Health check successful for API route', 25 | appName: process.env.APP_NAME ?? 'unknown', 26 | appVersion: process.env.APP_VERSION ?? 'unknown', 27 | timestamp: new Date().toISOString(), 28 | }; 29 | 30 | res.status(200).send(JSON.stringify(payload, undefined, 2)); 31 | res.end(); 32 | } 33 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/server/i18n/getServerTranslations.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Retrieve translations on server-side, wraps next-i18next.serverSideTranslations 3 | * to allow further customizations. 4 | */ 5 | import type { SSRConfig, UserConfig } from 'next-i18next'; 6 | import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; 7 | import type { 8 | I18nActiveNamespaces, 9 | I18nNamespace, 10 | } from '@/lib/i18n/I18nNamespace.types'; 11 | import nextI18nextConfig from '../../../next-i18next.config.mjs'; 12 | 13 | export const getServerTranslations = async ( 14 | locale: string, 15 | namespacesRequired?: I18nActiveNamespaces | I18nNamespace, 16 | configOverride?: UserConfig | null, 17 | extraLocales?: string[] | false 18 | ): Promise => { 19 | const config = configOverride ?? nextI18nextConfig; 20 | 21 | return serverSideTranslations( 22 | locale, 23 | namespacesRequired, 24 | config, 25 | extraLocales 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /packages/ts-utils/src/random/getRandomInt.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns a random integer between min (inclusive) and max (inclusive). 3 | * The value is no lower than min (or the next integer greater than min 4 | * if min isn't an integer) and no greater than max (or the next integer 5 | * lower than max if max isn't an integer). 6 | * @link https://stackoverflow.com/questions/1527803/generating-random-whole-numbers-in-javascript-in-a-specific-range/1527820#1527820 7 | */ 8 | 9 | export function getRandomInt(min: number, max: number): number { 10 | [min, max].forEach((v, idx) => { 11 | if (!Number.isSafeInteger(v)) { 12 | throw new TypeError( 13 | `${idx === 0 ? 'min' : 'max'} is not a valid integer` 14 | ); 15 | } 16 | }); 17 | if (max < min) { 18 | throw new Error('Min cannot be greater than max'); 19 | } 20 | min = Math.ceil(min); 21 | max = Math.floor(max); 22 | return Math.floor(Math.random() * (max - min + 1)) + min; 23 | } 24 | -------------------------------------------------------------------------------- /packages/ts-utils/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import tsconfigPaths from 'vite-tsconfig-paths'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | const testFiles = ['./src/**/*.test.{js,ts}']; 5 | 6 | export default defineConfig({ 7 | plugins: [tsconfigPaths()], 8 | cacheDir: '../../.cache/vitest/ts-utils', 9 | test: { 10 | globals: true, 11 | environment: 'node', 12 | passWithNoTests: false, 13 | coverage: { 14 | provider: 'v8', 15 | reporter: ['text', 'clover'], 16 | extension: ['js', 'jsx', 'ts', 'tsx'], 17 | all: true, 18 | }, 19 | include: testFiles, 20 | // To mimic Jest behaviour regarding mocks. 21 | // @link https://vitest.dev/config/#clearmocks 22 | clearMocks: true, 23 | mockReset: true, 24 | restoreMocks: true, 25 | exclude: [ 26 | '**/node_modules/**', 27 | 'dist/**', 28 | '**/coverage/**', 29 | '**/.{idea,git,cache,output,temp}/**', 30 | ], 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/features/admin/pages/AdminMainPage.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from 'next-i18next'; 2 | import { NextSeo } from 'next-seo'; 3 | import type { FC } from 'react'; 4 | import { adminConfig } from '@/features/admin/admin.config'; 5 | import { AdminLayout } from '@/layouts/admin'; 6 | 7 | export const AdminMainPage: FC = () => { 8 | const { t } = useTranslation(adminConfig.i18nNamespaces); 9 | return ( 10 | <> 11 | 12 | 13 |

This page is protected by Middleware

14 |

Only admin users can see this page.

15 |

16 | To learn more about the NextAuth middleware see  17 | 18 | the docs 19 | 20 | . 21 |

22 |
23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/themes/shared/browser-fonts.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Keep this file as '.js' as it's included in tailwind.config.js 3 | 4 | /** 5 | * @type {{mono: string[], sans: string[], serif: string[]}} 6 | */ 7 | export const browserFonts = { 8 | sans: [ 9 | 'ui-sans-serif', 10 | 'system-ui', 11 | '-apple-system', 12 | 'BlinkMacSystemFont', 13 | 'Segoe UI', 14 | 'Roboto', 15 | 'Helvetica Neue', 16 | 'Arial', 17 | 'Noto Sans', 18 | 'sans-serif', 19 | 'Apple Color Emoji', 20 | 'Segoe UI Emoji', 21 | 'Segoe UI Symbol', 22 | 'Noto Color Emoji', 23 | ], 24 | serif: [ 25 | 'ui-serif', 26 | 'Georgia', 27 | 'Cambria', 28 | 'Times New Roman', 29 | 'Times', 30 | 'serif', 31 | ], 32 | mono: [ 33 | 'ui-monospace', 34 | 'SFMono-Regular', 35 | 'Menlo', 36 | 'Monaco', 37 | 'Consolas', 38 | 'Liberation Mono', 39 | 'Courier New', 40 | 'monospace', 41 | ], 42 | }; 43 | -------------------------------------------------------------------------------- /packages/common-i18n/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Specific eslint rules for this app/package, extends the base rules 3 | * @see https://github.com/belgattitude/nextjs-monorepo-example/blob/main/docs/about-linters.md 4 | */ 5 | 6 | const { 7 | getDefaultIgnorePatterns, 8 | } = require('@your-org/eslint-config-bases/helpers'); 9 | 10 | module.exports = { 11 | root: true, 12 | parser: '@typescript-eslint/parser', 13 | parserOptions: { 14 | tsconfigRootDir: __dirname, 15 | project: 'tsconfig.json', 16 | }, 17 | ignorePatterns: [...getDefaultIgnorePatterns()], 18 | extends: [ 19 | '@your-org/eslint-config-bases/typescript', 20 | '@your-org/eslint-config-bases/import-x', 21 | // Apply prettier and disable incompatible rules 22 | '@your-org/eslint-config-bases/prettier-plugin', 23 | ], 24 | rules: { 25 | // optional overrides per project 26 | }, 27 | overrides: [ 28 | // optional overrides per project file match 29 | ], 30 | }; 31 | -------------------------------------------------------------------------------- /packages/ts-utils/src/asserts/__tests__/asserts.test.ts: -------------------------------------------------------------------------------- 1 | import { assertIncludes, assertNonEmptyString } from '../asserts'; 2 | 3 | describe('Asserts test', () => { 4 | describe('assertNonEmptyString', () => { 5 | it('should work as expected', () => { 6 | expect(() => { 7 | assertNonEmptyString('cool'); 8 | }).not.toThrow(); 9 | expect(() => { 10 | assertNonEmptyString(' ', 'message'); 11 | }).toThrow('message'); 12 | expect(() => { 13 | assertNonEmptyString(true, () => { 14 | return new Error('message2'); 15 | }); 16 | }).toThrow('message2'); 17 | }); 18 | }); 19 | describe('assertIncludes', () => { 20 | it('should work as expected', () => { 21 | expect(() => { 22 | assertIncludes('cool', ['cool']); 23 | }).not.toThrow(); 24 | expect(() => { 25 | assertIncludes('cool', [], 'message'); 26 | }).toThrow('message'); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/eslint-config-bases/src/bases/tailwind.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Opinionated config base for projects using react. 3 | * @see https://github.com/belgattitude/shared-dx/tree/main/packages/eslint-config-bases 4 | */ 5 | 6 | const reactPatterns = { 7 | files: ['*.{jsx,tsx}'], 8 | }; 9 | 10 | /** 11 | * Fine-tune naming convention react typescript jsx (function components) 12 | * @link https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/naming-convention.md 13 | */ 14 | 15 | module.exports = { 16 | env: { 17 | browser: true, 18 | es6: true, 19 | node: true, 20 | }, 21 | overrides: [ 22 | { 23 | extends: [ 24 | // @see https://github.com/francoismassart/eslint-plugin-tailwindcss, 25 | 'plugin:tailwindcss/recommended', 26 | ], 27 | files: [...reactPatterns.files], 28 | rules: { 29 | 'tailwindcss/no-custom-classname': 'off', 30 | }, 31 | }, 32 | ], 33 | }; 34 | -------------------------------------------------------------------------------- /packages/ui-lib/src/_stories/Page.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { within, userEvent } from '@storybook/test'; 3 | 4 | import { Page } from './Page'; 5 | 6 | const meta = { 7 | title: 'Example/Page', 8 | component: Page, 9 | parameters: { 10 | // More on how to position stories at: https://storybook.js.org/docs/7.0/react/configure/story-layout 11 | layout: 'fullscreen', 12 | }, 13 | } satisfies Meta; 14 | 15 | export default meta; 16 | type Story = StoryObj; 17 | 18 | export const LoggedOut: Story = {}; 19 | 20 | // More on interaction testing: https://storybook.js.org/docs/7.0/react/writing-tests/interaction-testing 21 | export const LoggedIn: Story = { 22 | play: async ({ canvasElement }) => { 23 | const canvas = within(canvasElement); 24 | const loginButton = canvas.getByRole('button', { 25 | name: /Log in/i, 26 | }); 27 | await userEvent.click(loginButton); 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/lib/env/__tests__/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { zConvertTruthyStrToBool } from '../utils.mjs'; 3 | 4 | describe('utils', () => { 5 | describe('zConvertTruthyToBool', () => { 6 | it.each([ 7 | [undefined, false, false], 8 | [undefined, true, true], 9 | ['true', true, true], 10 | ['true', false, true], 11 | ['1', true, true], 12 | ['0', true, false], 13 | ] as [value: string | undefined, defaultVal: boolean, expected: boolean][])( 14 | 'when "%s" is given should return "%s"', 15 | (value, defaultVal, expected) => { 16 | const schema = z.object({ 17 | TEST: zConvertTruthyStrToBool(defaultVal), 18 | }); 19 | 20 | const parsed = schema.safeParse({ TEST: value }); 21 | expect(parsed).toStrictEqual({ 22 | data: { 23 | TEST: expected, 24 | }, 25 | success: true, 26 | }); 27 | } 28 | ); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/types.d/next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import type { DefaultSession } from 'next-auth'; 2 | 3 | // @link https://next-auth.js.org/getting-started/typescript#module-augmentation 4 | 5 | declare module 'next-auth' { 6 | /** 7 | * The shape of the user object returned by the OAuth providers' `profile` callback, 8 | * or the second parameter of the `session` callback, when using a database. 9 | */ 10 | interface User { 11 | role: string; 12 | } 13 | /** 14 | * Usually contains information about the provider being used 15 | * and also extends `TokenSet`, which is different tokens returned by OAuth Providers. 16 | */ 17 | // interface Account {} 18 | /** The OAuth profile returned from your provider */ 19 | // interface Profile {} 20 | 21 | interface Session { 22 | user: { 23 | role: string; 24 | } & DefaultSession['user']; 25 | } 26 | } 27 | 28 | declare module 'next-auth/jwt' { 29 | interface JWT { 30 | role: string; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/ui-lib/src/base/button/Button.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta } from '@storybook/react'; 2 | 3 | import { Button } from './Button'; 4 | 5 | // More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 6 | export default { 7 | title: 'Input/Button', 8 | component: Button, 9 | // More on argTypes: https://storybook.js.org/docs/react/api/argtypes 10 | argTypes: { 11 | backgroundColor: { control: 'color' }, 12 | }, 13 | } as Meta; 14 | 15 | export const Primary = { 16 | args: { 17 | primary: true, 18 | label: 'Button', 19 | size: 'medium', 20 | }, 21 | }; 22 | 23 | export const Secondary = { 24 | args: { 25 | label: 'Button', 26 | size: 'medium', 27 | }, 28 | }; 29 | 30 | export const Large = { 31 | args: { 32 | size: 'large', 33 | label: 'Button', 34 | }, 35 | }; 36 | 37 | export const Small = { 38 | args: { 39 | size: 'small', 40 | label: 'Button', 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * This is the base lint-staged rules config and just includes prettier by default. 5 | * A good practice is to override this base configuration in each package and/or application 6 | * where we are able to add customization depending on the nature of the project (eslint...). 7 | * 8 | * {@link https://github.com/okonet/lint-staged#how-to-use-lint-staged-in-a-multi-package-monorepo} 9 | * {@link https://github.com/belgattitude/nextjs-monorepo-example/blob/main/docs/about-lint-staged.md} 10 | */ 11 | 12 | const { concatFilesForPrettier } = require('./lint-staged.common.js'); 13 | 14 | /** 15 | * @type {Record string | string[] | Promise>} 16 | */ 17 | const rules = { 18 | '**/*.{json,md,mdx,css,html,yml,yaml,scss,ts,js,tsx,jsx,mjs}': ( 19 | filenames 20 | ) => { 21 | return [`yarn prettier --write ${concatFilesForPrettier(filenames)}`]; 22 | }, 23 | }; 24 | 25 | module.exports = rules; 26 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/server/config/__tests__/cors.config.test.ts: -------------------------------------------------------------------------------- 1 | import { getCorsWhitelistOriginRegexp } from '../cors.config'; 2 | 3 | describe('cors.config "smoke" test', () => { 4 | describe('ensure validity of regexp in config', () => { 5 | const corsAllowedOrigins: string[] = [ 6 | String.raw`.+\-belgattitude.vercel.app`, 7 | 'failwell.be', 8 | 'localhost', 9 | ]; 10 | const regexp = getCorsWhitelistOriginRegexp(corsAllowedOrigins); 11 | 12 | it('regexp should not fail', () => { 13 | expect(regexp.test('https://nothing')).toBe(false); 14 | expect(regexp.test('localhost')).toBe(false); 15 | expect(regexp.test('http://localhost')).toBe(true); 16 | expect(regexp.test('http://localhost:3000')).toBe(true); 17 | expect(regexp.test('https://preview-belgattitude.vercel.app')).toBe(true); 18 | expect(regexp.test('https://failwell.be')).toBe(true); 19 | expect(regexp.test('https://www.failwell.be')).toBe(true); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/ui-lib/src/async-message.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, type FC } from 'react'; 2 | 3 | type Props = { 4 | apiUrl: string; 5 | children?: never; 6 | }; 7 | 8 | export const AsyncMessage: FC = (props) => { 9 | const [msg, setMsg] = useState(null); 10 | const [isLoading, setIsLoading] = useState(true); 11 | const [error, setError] = useState(null); 12 | 13 | useEffect(() => { 14 | setIsLoading(true); 15 | fetch(props.apiUrl) 16 | .then((res) => res.text()) 17 | .then((data) => { 18 | setMsg(data); 19 | setIsLoading(false); 20 | }) 21 | .catch((err: unknown) => { 22 | setError(err instanceof Error ? err.message : 'Unknown error'); 23 | setIsLoading(false); 24 | }); 25 | }, [props.apiUrl]); 26 | 27 | if (error) { 28 | return Error: {error}; 29 | } 30 | if (isLoading) { 31 | return Loading; 32 | } 33 | 34 | return {msg}; 35 | }; 36 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { HttpBadRequest } from '@httpx/exception'; 2 | import type { GetStaticProps, InferGetStaticPropsType } from 'next'; 3 | import { demoConfig } from '@/features/demo/demo.config'; 4 | import { DemoPage } from '@/features/demo/pages'; 5 | import { getServerTranslations } from '@/server/i18n/getServerTranslations'; 6 | 7 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 8 | type Props = { 9 | /** Add HomeRoute props here */ 10 | }; 11 | 12 | export default function DemoRoute( 13 | _props: InferGetStaticPropsType 14 | ) { 15 | return ; 16 | } 17 | 18 | export const getStaticProps: GetStaticProps = async (context) => { 19 | const { locale } = context; 20 | if (locale === undefined) { 21 | throw new HttpBadRequest('locale is missing'); 22 | } 23 | const { i18nNamespaces } = demoConfig; 24 | return { 25 | props: { 26 | ...(await getServerTranslations(locale, i18nNamespaces)), 27 | }, 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/pages/auth/login.tsx: -------------------------------------------------------------------------------- 1 | import { HttpBadRequest } from '@httpx/exception'; 2 | import type { GetStaticProps, InferGetStaticPropsType } from 'next'; 3 | import { authConfig } from '@/features/auth/auth.config'; 4 | import { LoginPage } from '@/features/auth/pages/LoginPage'; 5 | import { getServerTranslations } from '@/server/i18n/getServerTranslations'; 6 | 7 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 8 | type Props = { 9 | /** Add props here */ 10 | }; 11 | 12 | export default function LoginRoute( 13 | _props: InferGetStaticPropsType 14 | ) { 15 | return ; 16 | } 17 | 18 | export const getStaticProps: GetStaticProps = async (context) => { 19 | const { locale } = context; 20 | if (locale === undefined) { 21 | throw new HttpBadRequest('locale is missing'); 22 | } 23 | const { i18nNamespaces } = authConfig; 24 | return { 25 | props: { 26 | ...(await getServerTranslations(locale, i18nNamespaces)), 27 | }, 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /TROUBLESHOOT.md: -------------------------------------------------------------------------------- 1 | # TROUBLESHOOT 2 | 3 | ## Install fails 4 | 5 | This repo uses the official but experimental [corepack](https://nodejs.org/api/corepack.html) to select the correct 6 | package manager to use. Just enable it, then run install 7 | 8 | ```bash 9 | corepack enable 10 | yarn install 11 | ``` 12 | 13 | > PS: Same applies to docker. For vercel just add ENABLE_EXPERIMENTAL_COREPACK=1 to your vercel env. 14 | 15 | ## Development 16 | 17 | ### Limit of file watchers reached 18 | 19 | Error: ENOSPC: System limit for number of file watchers reached 20 | [Stackoverflow answer](https://stackoverflow.com/questions/55763428/react-native-error-enospc-system-limit-for-number-of-file-watchers-reached) 21 | 22 | ``` 23 | # insert the new value into the system config 24 | echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p 25 | 26 | # check that the new value was applied 27 | cat /proc/sys/fs/inotify/max_user_watches 28 | 29 | # config variable name (not runnable) 30 | fs.inotify.max_user_watches=524288 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/about-lint-staged.md: -------------------------------------------------------------------------------- 1 | # About lint-staged 2 | 3 | [Lint-staged](https://github.com/okonet/lint-staged) and [husky](https://github.com/typicode/husky) are used to automatically 4 | run linters on commit. 5 | 6 | ## Structure 7 | 8 | See [the doc to use lint-staged in a monorepo](https://github.com/okonet/lint-staged#how-to-use-lint-staged-in-a-multi-package-monorepo) 9 | and the [linter docs](./about-linters.md). 10 | 11 | ``` 12 | . 13 | ├── apps 14 | │ ├── remix-app 15 | │ │ ├── .eslintrc.js 16 | │ │ └── lint-staged.config.js (overwrite global lint-staged.config.js, custom eslint) 17 | │ └── nextjs-app 18 | │ ├── .eslintrc.js 19 | │ └── lint-staged.config.js (overwrite global lint-staged.config.js, custom eslint) 20 | ├── packages 21 | │ └── ui-lib 22 | │ ├── .eslintrc.js 23 | │ └── lint-staged.config.js (overwrite global lint-staged.config.js, custom eslint) 24 | │ 25 | ├── lint-staged.common.js (few common utils) 26 | └── lint-staged.config.js (base config to overwrite per apps/packages) 27 | ``` 28 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/themes/shared/colors.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-require-imports 2 | const tailwindColors = require('tailwindcss/colors'); 3 | 4 | /** 5 | * Return tailwind v3 non-deprecated colors 6 | * PS: code is dirty cause tailwind colors have getters on them 7 | * that will log a warning when accessing the object key 8 | * @type {Record>} 9 | */ 10 | export const tailwindV3Colors = Object.entries( 11 | Object.getOwnPropertyDescriptors(tailwindColors) 12 | ) 13 | .filter( 14 | ([, desc]) => 15 | Object.prototype.hasOwnProperty.call(desc, 'value') && 16 | typeof desc.value !== 'function' 17 | ) 18 | // eslint-disable-next-line unicorn/no-array-reduce 19 | .reduce((acc, [key]) => { 20 | if ( 21 | !['coolGray', 'lightBlue', 'warmGray', 'trueGray', 'blueGray'].includes( 22 | key 23 | ) 24 | ) { 25 | acc[key] = tailwindColors[key]; 26 | } 27 | return acc; 28 | }, {}); 29 | 30 | module.exports = { tailwindV3Colors }; 31 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/pages/home.tsx: -------------------------------------------------------------------------------- 1 | import { HttpBadRequest } from '@httpx/exception'; 2 | import type { GetServerSideProps, InferGetServerSidePropsType } from 'next'; 3 | import { homeConfig } from '@/features/home/home.config'; 4 | import { HomePage } from '@/features/home/pages'; 5 | import { getServerTranslations } from '@/server/i18n/getServerTranslations'; 6 | 7 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 8 | type Props = { 9 | /** Add HomeRoute props here */ 10 | }; 11 | 12 | export default function HomeRoute( 13 | _props: InferGetServerSidePropsType 14 | ) { 15 | return ; 16 | } 17 | 18 | export const getServerSideProps: GetServerSideProps = async ( 19 | context 20 | ) => { 21 | const { locale } = context; 22 | if (locale === undefined) { 23 | throw new HttpBadRequest('locale is missing'); 24 | } 25 | const { i18nNamespaces } = homeConfig; 26 | return { 27 | props: { 28 | ...(await getServerTranslations(locale, i18nNamespaces)), 29 | }, 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /packages/ui-lib/src/_stories/Header.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { fn } from '@storybook/test'; 3 | import { Header } from './Header'; 4 | 5 | const meta = { 6 | title: 'Example/Header', 7 | component: Header, 8 | // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/7.0/react/writing-docs/docs-page 9 | tags: ['autodocs'], 10 | parameters: { 11 | // More on how to position stories at: https://storybook.js.org/docs/7.0/react/configure/story-layout 12 | layout: 'fullscreen', 13 | }, 14 | } satisfies Meta; 15 | 16 | export default meta; 17 | type Story = StoryObj; 18 | 19 | export const LoggedIn: Story = { 20 | args: { 21 | onLogout: fn(), 22 | onLogin: fn(), 23 | onCreateAccount: fn(), 24 | user: { 25 | name: 'Jane Doe', 26 | }, 27 | }, 28 | }; 29 | 30 | export const LoggedOut: Story = { 31 | args: { 32 | onLogout: fn(), 33 | onLogin: fn(), 34 | onCreateAccount: fn(), 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/features/system/pages/NotFoundPage.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import { useTranslation } from 'next-i18next'; 3 | import type { FC } from 'react'; 4 | 5 | import { systemConfig } from '@/features/system/system.config'; 6 | 7 | type Props = { 8 | title?: string; 9 | children?: never; 10 | }; 11 | 12 | export const NotFoundPage: FC = (props) => { 13 | const { t } = useTranslation(systemConfig.i18nNamespaces); 14 | const title = props.title ?? t('system:notFound.title'); 15 | return ( 16 | <> 17 | 18 | {title} 19 | 20 |
21 |

25 | {title} 26 |

27 |

28 | {t('system:links.backToHome')} 29 |

30 |
31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /apps/vite-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "../../tsconfig.base.json", 4 | "compilerOptions": { 5 | "baseUrl": "./src", 6 | "target": "esnext", 7 | "lib": ["dom", "dom.iterable", "esnext"], 8 | "module": "esnext", 9 | "jsx": "react-jsx", 10 | "jsxImportSource": "@emotion/react", 11 | "incremental": true, 12 | "paths": { 13 | "@your-org/ui-lib/*": ["../../../packages/ui-lib/src/*"], 14 | "@your-org/ui-lib": ["../../../packages/ui-lib/src/index"], 15 | "@your-org/core-lib/*": ["../../../packages/core-lib/src/*"], 16 | "@your-org/core-lib": ["../../../packages/core-lib/src/index"], 17 | "@your-org/ts-utils": ["../../../packages/ts-utils/src/index"] 18 | }, 19 | "types": ["@testing-library/jest-dom/vitest"] 20 | }, 21 | "exclude": ["**/node_modules", "**/.*/*"], 22 | "include": [ 23 | ".eslintrc.*", 24 | "**/*.ts", 25 | "**/*.tsx", 26 | "**/*.mts", 27 | "**/*.js", 28 | "**/*.cjs", 29 | "**/*.mjs", 30 | "**/*.jsx", 31 | "**/*.json" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'body-leading-blank': [1, 'always'], 5 | 'body-max-line-length': [2, 'always', 100], 6 | 'footer-leading-blank': [1, 'always'], 7 | 'footer-max-line-length': [2, 'always', 100], 8 | 'header-max-length': [2, 'always', 100], 9 | 'scope-case': [2, 'always', 'lower-case'], 10 | 'subject-case': [ 11 | 2, 12 | 'never', 13 | ['sentence-case', 'start-case', 'pascal-case', 'upper-case'], 14 | ], 15 | 'subject-empty': [2, 'never'], 16 | 'subject-full-stop': [2, 'never', '.'], 17 | 'type-case': [2, 'always', 'lower-case'], 18 | 'type-empty': [2, 'never'], 19 | 'type-enum': [ 20 | 2, 21 | 'always', 22 | [ 23 | 'build', 24 | 'chore', 25 | 'ci', 26 | 'docs', 27 | 'feat', 28 | 'fix', 29 | 'perf', 30 | 'refactor', 31 | 'revert', 32 | 'style', 33 | 'test', 34 | 'translation', 35 | 'security', 36 | ], 37 | ], 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /packages/ui-lib/src/_stories/Button.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | 3 | import { Button } from './Button'; 4 | 5 | // More on how to set up stories at: https://storybook.js.org/docs/7.0/react/writing-stories/introduction 6 | const meta = { 7 | title: 'Example/Button', 8 | component: Button, 9 | tags: ['autodocs'], 10 | argTypes: { 11 | backgroundColor: { control: 'color' }, 12 | }, 13 | } satisfies Meta; 14 | 15 | export default meta; 16 | type Story = StoryObj; 17 | 18 | // More on writing stories with args: https://storybook.js.org/docs/7.0/react/writing-stories/args 19 | export const Primary: Story = { 20 | args: { 21 | primary: true, 22 | label: 'Button', 23 | }, 24 | }; 25 | 26 | export const Secondary: Story = { 27 | args: { 28 | label: 'Button', 29 | }, 30 | }; 31 | 32 | export const Large: Story = { 33 | args: { 34 | size: 'large', 35 | label: 'Button', 36 | }, 37 | }; 38 | 39 | export const Small: Story = { 40 | args: { 41 | size: 'small', 42 | label: 'Button', 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/features/demo/components/PoemGrid.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | import type { SearchPoems } from '@/server/features/poem/SearchPoems'; 3 | import { PoemCard } from './PoemCard'; 4 | 5 | export const PoemGrid: FC<{ poems: SearchPoems }> = (props) => { 6 | const { poems } = props; 7 | return ( 8 |
9 | {poems.map((poem) => { 10 | const unsplashImg = `https://source.unsplash.com/random/640x480?${( 11 | poem.keywords ?? [] 12 | ) 13 | .map((keyword) => encodeURIComponent(keyword)) 14 | .join(',')}`; 15 | 16 | return ( 17 |
21 | 28 |
29 | ); 30 | })} 31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/ts-utils/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Specific eslint rules for this workspace, learn how to compose 3 | * @link https://github.com/belgattitude/nextjs-monorepo-example/tree/main/packages/eslint-config-bases 4 | */ 5 | 6 | const { 7 | getDefaultIgnorePatterns, 8 | } = require('@your-org/eslint-config-bases/helpers'); 9 | 10 | module.exports = { 11 | root: true, 12 | parser: '@typescript-eslint/parser', 13 | parserOptions: { 14 | tsconfigRootDir: __dirname, 15 | project: 'tsconfig.json', 16 | }, 17 | ignorePatterns: [...getDefaultIgnorePatterns()], 18 | extends: [ 19 | '@your-org/eslint-config-bases/typescript', 20 | '@your-org/eslint-config-bases/import-x', 21 | '@your-org/eslint-config-bases/sonar', 22 | '@your-org/eslint-config-bases/regexp', 23 | '@your-org/eslint-config-bases/jest', 24 | // Apply prettier and disable incompatible rules 25 | '@your-org/eslint-config-bases/prettier-plugin', 26 | ], 27 | rules: { 28 | // optional overrides per project 29 | }, 30 | overrides: [ 31 | // optional overrides per project file match 32 | ], 33 | }; 34 | -------------------------------------------------------------------------------- /apps/nextjs-app/config/tests/I18nextTestStubProvider.tsx: -------------------------------------------------------------------------------- 1 | import i18next from 'i18next'; 2 | import type { FC, ReactNode } from 'react'; 3 | import { initReactI18next, I18nextProvider } from 'react-i18next'; 4 | import type { I18nNamespace } from '@/lib/i18n/I18nNamespace.types'; 5 | 6 | /** 7 | * Fully wrapped strategy for i18next, you can use stub/mocks as well 8 | * @link {https://react.i18next.com/misc/testing} 9 | */ 10 | void i18next.use(initReactI18next).init({ 11 | lng: 'en', 12 | fallbackLng: 'en', 13 | ns: ['common'], 14 | defaultNS: 'common', 15 | debug: false, 16 | interpolation: { 17 | escapeValue: false, // not needed for react!! 18 | }, 19 | // Let empty so you can test on translation keys rather than translated strings 20 | resources: { 21 | en: { common: {}, demo: {}, home: {}, system: {} } as Record< 22 | I18nNamespace, 23 | Record 24 | >, 25 | }, 26 | }); 27 | 28 | export const I18nextTestStubProvider: FC<{ children: ReactNode }> = ({ 29 | children, 30 | }) => { 31 | return {children}; 32 | }; 33 | -------------------------------------------------------------------------------- /packages/db-main-prisma/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import tsconfigPaths from 'vite-tsconfig-paths'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | const testFiles = ['./e2e/suites/*.test.{js,ts}']; 5 | 6 | export default defineConfig({ 7 | plugins: [tsconfigPaths()], 8 | cacheDir: '../../.cache/vitest/db-main-prisma', 9 | test: { 10 | globals: true, 11 | environment: 'node', 12 | testTimeout: 15_000, 13 | passWithNoTests: false, 14 | // setupFiles: './config/tests/setupVitest.ts', 15 | coverage: { 16 | provider: 'v8', 17 | reporter: ['text', 'clover'], 18 | extension: ['js', 'jsx', 'ts', 'tsx'], 19 | all: true, 20 | include: ['src/**/*.js', 'src/**/*.ts'], 21 | }, 22 | include: testFiles, 23 | // To mimic Jest behaviour regarding mocks. 24 | // @link https://vitest.dev/config/#clearmocks 25 | clearMocks: true, 26 | mockReset: true, 27 | restoreMocks: true, 28 | exclude: [ 29 | '**/node_modules/**', 30 | 'dist/**', 31 | '**/coverage/**', 32 | '**/.{idea,git,cache,output,temp}/**', 33 | ], 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /apps/vite-app/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import { sayHello } from '@your-org/core-lib'; 3 | import { Message } from '@your-org/ui-lib'; 4 | import { GradientText } from '@your-org/ui-lib/ux'; 5 | import { useState } from 'react'; 6 | 7 | import './App.css'; 8 | 9 | function App() { 10 | const [count, setCount] = useState(0); 11 | 12 | return ( 13 |
14 |
15 |

16 | 22 | Hello 23 | 24 |

25 |

{`${sayHello('Hello Vite')} from @your-org/core-lib`}

26 |

27 | 30 |

31 |

32 | 33 |

34 |
35 |
36 | ); 37 | } 38 | 39 | export default App; 40 | -------------------------------------------------------------------------------- /apps/nextjs-app/next-i18next.config.mjs: -------------------------------------------------------------------------------- 1 | const debugI18n = ['true', 1].includes( 2 | process?.env?.NEXTJS_DEBUG_I18N ?? 'false' 3 | ); 4 | 5 | const localePublicFolder = undefined; 6 | 7 | export const defaultLocale = 'en'; 8 | 9 | const getLocalesPath = async () => { 10 | if ('window' in globalThis) { 11 | return localePublicFolder; 12 | } 13 | // eslint-disable-next-line unicorn/prefer-node-protocol,unicorn/import-style 14 | const path = await import('path').then((mod) => mod.default); 15 | return path.resolve('../../packages/common-i18n/src/locales'); 16 | }; 17 | 18 | const localePath = await getLocalesPath(); 19 | 20 | /** 21 | * @type {import('next-i18next').UserConfig} 22 | */ 23 | export default { 24 | i18n: { 25 | defaultLocale, 26 | locales: ['en', 'fr'], 27 | }, 28 | saveMissing: false, 29 | strictMode: true, 30 | serializeConfig: false, 31 | reloadOnPrerender: process?.env?.NODE_ENV === 'development', 32 | react: { 33 | useSuspense: false, 34 | }, 35 | debug: debugI18n, 36 | /* 37 | interpolation: { 38 | escapeValue: false, 39 | }, 40 | */ 41 | localePath, 42 | }; 43 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/config/api-fetcher.config.ts: -------------------------------------------------------------------------------- 1 | import { ofetch } from 'ofetch'; 2 | 3 | /** 4 | * Custom fetcher to be used for ingestion api calls 5 | */ 6 | export const apiFetcher = ofetch.create({ 7 | // baseURL: '', 8 | headers: { 9 | Accept: 'application/json', 10 | 'Cache-Control': 'no-cache', 11 | }, 12 | retry: 2, 13 | /** 14 | * By setting the FETCH_KEEP_ALIVE environment variable to true, an http/https agent will be registered that 15 | * keeps sockets around even when there are no outstanding requests, so they can be used for future requests 16 | * without having to reestablish a TCP connection. 17 | * 18 | * Note: This option can potentially introduce memory leaks. 19 | * Please check https://github.com/node-fetch/node-fetch/pull/1325 for more details. 20 | */ 21 | keepalive: true, 22 | 23 | /* 24 | async onRequest({ request, options }) { 25 | // Nothing yet 26 | }, 27 | 28 | async onRequestError({ request, options, error }) { 29 | // Nothing yet 30 | }, 31 | 32 | async onResponseError({ request, response, options }) { 33 | // Nothing yet 34 | }, */ 35 | }); 36 | -------------------------------------------------------------------------------- /packages/db-main-prisma/lint-staged.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * This files overrides the base lint-staged.config.js present in the root directory. 5 | * It allows to run eslint based the package specific requirements. 6 | * {@link https://github.com/okonet/lint-staged#how-to-use-lint-staged-in-a-multi-package-monorepo} 7 | * {@link https://github.com/belgattitude/nextjs-monorepo-example/blob/main/docs/about-lint-staged.md} 8 | */ 9 | 10 | const { 11 | concatFilesForPrettier, 12 | getEslintFixCmd, 13 | } = require('../../lint-staged.common.js'); 14 | 15 | /** 16 | * @type {Record string | string[] | Promise>} 17 | */ 18 | const rules = { 19 | '**/*.{js,jsx,ts,tsx,mjs,cjs}': (filenames) => { 20 | return getEslintFixCmd({ 21 | cwd: __dirname, 22 | fix: true, 23 | cache: true, 24 | maxWarnings: 25, 25 | files: filenames, 26 | }); 27 | }, 28 | '**/*.{json,md,mdx,css,html,yml,yaml,scss}': (filenames) => { 29 | return [`prettier --write ${concatFilesForPrettier(filenames)}`]; 30 | }, 31 | }; 32 | 33 | module.exports = rules; 34 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/layouts/main/MainFooter.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import GithubIcon from '@your-org/ui-lib/icons/social/github.svg'; 3 | import type { FC } from 'react'; 4 | import FooterWaves from '@/public/images/layout/footer-waves.svg'; 5 | 6 | const BgWaved = styled(FooterWaves)` 7 | //background-size: cover; 8 | object-fit: cover; 9 | position: absolute; 10 | width: 100%; 11 | object-position: center; 12 | display: block; 13 | `; 14 | 15 | const FooterCtn = styled.footer` 16 | display: grid; 17 | .content, 18 | .bgImage { 19 | grid-area: 1 / 1; 20 | } 21 | .bgImage { 22 | z-index: -1; 23 | } 24 | `; 25 | 26 | export const MainFooter: FC = () => { 27 | return ( 28 | 29 |
30 | 31 |
32 |
33 | 38 | 39 | 40 |
41 |
42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/middleware.ts: -------------------------------------------------------------------------------- 1 | // @link https://nextjs.org/docs/app/building-your-application/routing/middleware 2 | 3 | import { withAuth } from 'next-auth/middleware'; 4 | 5 | type MiddlewareEnabledRouteMatchers = (typeof config.matcher)[number]; 6 | 7 | export const config = { 8 | matcher: [ 9 | '/admin/:path*', 10 | // '/profile/:path*' 11 | ], 12 | }; 13 | 14 | const adminAuthRoutes: MiddlewareEnabledRouteMatchers[] = ['/admin/:path*']; 15 | 16 | const isAdminRoute = ( 17 | pathName: string, 18 | adminRoutes: MiddlewareEnabledRouteMatchers[] 19 | ): boolean => { 20 | return adminRoutes.some((routePrefix) => { 21 | const path = pathName.toLowerCase().trim(); 22 | if (routePrefix.endsWith(':path*')) { 23 | return pathName.startsWith(routePrefix.slice(0, -7)); 24 | } 25 | return path === routePrefix; 26 | }); 27 | }; 28 | 29 | export default withAuth({ 30 | callbacks: { 31 | authorized: ({ req, token }) => { 32 | const { pathname } = req.nextUrl; 33 | return ( 34 | !isAdminRoute(pathname, adminAuthRoutes) || token?.role === 'admin' 35 | ); 36 | }, 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /cache.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Convenience script to harmonize cache directories across various 3 | * tooling such as eslint and jest. 4 | * 5 | * Recently more & more tools like babel-loader tend to cache in 6 | * node_modules/.cache (@link https://github.com/avajs/find-cache-dir) 7 | * It's possible too. 8 | */ 9 | // @ts-check 10 | 'use strict'; 11 | 12 | const { resolve } = require('path'); 13 | 14 | const globalCachePath = resolve(`${__dirname}/.cache`); 15 | 16 | /** 17 | * @param {string} packageName 18 | * @returns string 19 | */ 20 | function sanitize(packageName) { 21 | return packageName.replace('/', '.').replace(/[^a-z0-9.@_-]+/gi, '-'); 22 | } 23 | 24 | /** 25 | * @param {string} packageName 26 | * @returns string 27 | */ 28 | function getEslintCachePath(packageName) { 29 | return `${globalCachePath}/${sanitize(packageName)}/eslint`; 30 | } 31 | 32 | /** 33 | * @param {string} packageName 34 | * @returns string 35 | */ 36 | function getJestCachePath(packageName) { 37 | return `${globalCachePath}/${sanitize(packageName)}/jest`; 38 | } 39 | 40 | module.exports = { 41 | getJestCachePath, 42 | getEslintCachePath, 43 | }; 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-current Sébastien Vanvelthem 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apps/nextjs-app/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2021 Sébastien Vanvelthem 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/core-lib/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2021 Sébastien Vanvelthem 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/ts-utils/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2023 Sébastien Vanvelthem 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/ui-lib/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2021 Sébastien Vanvelthem 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apps/nextjs-app/config/tests/test-utils.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Automatically add app-providers 3 | * @see https://testing-library.com/docs/react-testing-library/setup#configuring-jest-with-test-utils 4 | */ 5 | import { render } from '@testing-library/react'; 6 | import type { ReactElement } from 'react'; 7 | import { AppTestProviders } from './AppTestProviders'; 8 | 9 | /** Recommended in vitest only for cleanup 10 | import { cleanup, render } from '@testing-library/react'; 11 | import { afterEach } from 'vitest'; 12 | 13 | afterEach(() => { 14 | cleanup(); 15 | }); */ 16 | 17 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 18 | const customRender = (ui: ReactElement, options?: any) => 19 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 20 | render(ui, { 21 | wrapper: AppTestProviders, 22 | ...options, 23 | }); 24 | 25 | // re-export everything 26 | // eslint-disable-next-line import-x/export 27 | export * from '@testing-library/react'; 28 | export { default as userEvent } from '@testing-library/user-event'; 29 | 30 | // override render method 31 | // eslint-disable-next-line import-x/export 32 | export { customRender as render }; 33 | -------------------------------------------------------------------------------- /constraints.pro: -------------------------------------------------------------------------------- 1 | % Yarn Constraints https://yarnpkg.com/features/constraints 2 | % check with "yarn constraints" (fix w/ "yarn constraints --fix") 3 | % reference for other constraints: https://github.com/babel/babel/blob/main/constraints.pro 4 | 5 | % Enforces the license in all public workspaces while removing it from private workspaces 6 | gen_enforced_field(WorkspaceCwd, 'license', 'MIT') :- 7 | \+ workspace_field(WorkspaceCwd, 'private', true). 8 | 9 | % Enforces that a dependency doesn't appear in both `dependencies` and `devDependencies` 10 | gen_enforced_dependency(WorkspaceCwd, DependencyIdent, null, 'devDependencies') :- 11 | workspace_has_dependency(WorkspaceCwd, DependencyIdent, _, 'devDependencies'), 12 | workspace_has_dependency(WorkspaceCwd, DependencyIdent, _, 'dependencies'). 13 | 14 | % Force all workspace dependencies to be made explicit 15 | % https://yarnpkg.com/features/constraints#force-all-workspace-dependencies-to-be-made-explicit 16 | gen_enforced_dependency(WorkspaceCwd, DependencyIdent, 'workspace:^', DependencyType) :- 17 | workspace_ident(_, DependencyIdent), 18 | workspace_has_dependency(WorkspaceCwd, DependencyIdent, _, DependencyType). 19 | 20 | -------------------------------------------------------------------------------- /packages/common-i18n/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2021 Sébastien Vanvelthem 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/db-main-prisma/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2021 Sébastien Vanvelthem 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/pages/_monitor/sentry/csr-page.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, type FC } from 'react'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/require-await 4 | const getAsyncError = async (): Promise => { 5 | throw new Error( 6 | 'Error purposely crafted for monitoring sentry (/pages/_monitor/sentry/csr-page.tsx)' 7 | ); 8 | }; 9 | 10 | const MonitorSentryCsrRoute: FC = () => { 11 | const [error, setError] = useState(null); 12 | 13 | useEffect(() => { 14 | getAsyncError().catch((err) => setError(err as Error)); 15 | }, []); 16 | 17 | if (error) { 18 | throw error; 19 | } 20 | return ( 21 |
22 |

Unexpected error

23 |

24 | If you see this message, it means that an error thrown in a static 25 | NextJs page wasn't caught by the global error handler 26 | (pages/_error.tsx). This is a bug in the application and may affect the 27 | ability to display error pages and log errors on Sentry. See the 28 | monitoring page in /pages/_monitor/sentry/csr-page.tsx. 29 |

30 |
31 | ); 32 | }; 33 | 34 | export default MonitorSentryCsrRoute; 35 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/features/demo/blocks/poetry/PoetryBlock.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@tanstack/react-query'; 2 | import type { FC } from 'react'; 3 | import { fetchPoems } from '../../api/fetch-poems.api'; 4 | import { PoemGrid } from '../../components/PoemGrid'; 5 | 6 | const PoemGridWithReactQuery: FC = () => { 7 | const { data, isLoading, error } = useQuery({ 8 | queryKey: ['poems'], 9 | queryFn: fetchPoems, 10 | }); 11 | if (isLoading) { 12 | return
Loading...
; 13 | } 14 | if (error) { 15 | const { message } = error; 16 | return
Error {message}
; 17 | } 18 | return <>{data && }; 19 | }; 20 | 21 | export const PoetryBlock: FC = () => { 22 | return ( 23 |
24 |
25 |

Poetry on the wild.

26 |

27 | Client fetch with ofetch / react-query from nextjs api, db in 28 | supabase.io, connection with prisma. Ui with tailwind 29 |

30 | 31 |
32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /packages/ui-lib/src/_stories/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './button.css'; 3 | 4 | interface ButtonProps { 5 | /** 6 | * Is this the principal call to action on the page? 7 | */ 8 | primary?: boolean; 9 | /** 10 | * What background color to use 11 | */ 12 | backgroundColor?: string; 13 | /** 14 | * How large should the button be? 15 | */ 16 | size?: 'small' | 'medium' | 'large'; 17 | /** 18 | * Button contents 19 | */ 20 | label: string; 21 | /** 22 | * Optional click handler 23 | */ 24 | onClick?: () => void; 25 | } 26 | 27 | /** 28 | * Primary UI component for user interaction 29 | */ 30 | export const Button = ({ 31 | primary = false, 32 | size = 'medium', 33 | backgroundColor, 34 | label, 35 | ...props 36 | }: ButtonProps) => { 37 | const mode = primary 38 | ? 'storybook-button--primary' 39 | : 'storybook-button--secondary'; 40 | return ( 41 | 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": ["**/.env.*local", "**/tsconfig*.json"], 4 | "globalPassThroughEnv": ["AWS_SECRET_KEY", "GITHUB_TOKEN"], 5 | "tasks": { 6 | "codegen": { 7 | // Codegen caching might also be disabled and enabled by workspace (see packages/db-main-prisma/turbo.json) 8 | // as generated code location might vary between toolings 9 | "cache": true, 10 | "outputs": ["src/generated/**"] 11 | }, 12 | "build": { 13 | "outputs": ["dist/**"] 14 | // This repo example relies on tsconfig paths to ease starting 15 | // "dependsOn": ["^build"] 16 | }, 17 | "build-force": { 18 | "outputs": ["dist/**"] 19 | }, 20 | "test": {}, 21 | "test-unit": {}, 22 | "lint": { 23 | "env": ["TIMING"] 24 | }, 25 | "lint-styles": {}, 26 | "typecheck": {}, 27 | "build-storybook": {}, 28 | "clean": { 29 | "cache": false 30 | }, 31 | "check-dist": { 32 | /** Note that build-force */ 33 | "dependsOn": ["build-force", "build"] 34 | }, 35 | "check-size": { 36 | "dependsOn": ["build-force", "build"] 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/clean-up-pr-caches.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries 2 | name: Cleanup caches for closed branches 3 | 4 | on: 5 | pull_request: 6 | types: [closed] 7 | workflow_dispatch: 8 | 9 | jobs: 10 | cleanup: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out code 14 | uses: actions/checkout@v4 15 | 16 | - name: 🧹 Cleanup 17 | run: | 18 | gh extension install actions/gh-actions-cache 19 | 20 | REPO=${{ github.repository }} 21 | BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge" 22 | 23 | echo "Fetching list of cache key" 24 | cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 ) 25 | 26 | ## Setting this to not fail the workflow while deleting cache keys. 27 | set +e 28 | echo "Deleting caches..." 29 | for cacheKey in $cacheKeysForPR 30 | do 31 | gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm 32 | done 33 | echo "Done" 34 | env: 35 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | -------------------------------------------------------------------------------- /.syncpackrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "customTypes": { 3 | "engines": { 4 | "path": "engines", 5 | "strategy": "versionsByName" 6 | }, 7 | "packageManager": { 8 | "path": "packageManager", 9 | "strategy": "name@version" 10 | } 11 | }, 12 | "dependencyTypes": ["dev", "prod", "peer"], 13 | "filter": ".", 14 | "indent": " ", 15 | "semverGroups": [ 16 | { 17 | "range": "", 18 | "label": "apps", 19 | "dependencyTypes": ["dev", "prod"], 20 | "dependencies": ["**"], 21 | "packages": ["*-app"] 22 | }, 23 | { 24 | "range": "^", 25 | "label": "publishable-packages", 26 | "dependencyTypes": ["peer", "prod"], 27 | "dependencies": ["**"], 28 | "packages": ["@your-org/*"] 29 | }, 30 | ], 31 | "sortAz": [ 32 | "contributors", 33 | "dependencies", 34 | "devDependencies", 35 | "peerDependencies", 36 | "resolutions" 37 | ], 38 | "sortFirst": ["name", "version", "private", "module", "sideEffects", "type", "main", "types", "exports", "description", "keywords", "author", "license", "homepage", "repository", "scripts"], 39 | "source": ["apps/**", "packages/**", "./package.json"], 40 | "versionGroups": [] 41 | } -------------------------------------------------------------------------------- /apps/nextjs-app/src/lib/factory/ky.factory.ts: -------------------------------------------------------------------------------- 1 | import Ky, { type Options, type NormalizedOptions } from 'ky'; 2 | 3 | type Props = { 4 | baseUrl?: string; 5 | onAuthFailure?: ( 6 | request: Request, 7 | options: NormalizedOptions, 8 | response: Response 9 | ) => Promise; 10 | }; 11 | 12 | export class KyFactory { 13 | constructor(private props: Props) {} 14 | create = (options?: Omit): typeof Ky => { 15 | const hooks: Options['hooks'] = 16 | this.props.onAuthFailure === undefined 17 | ? {} 18 | : { 19 | afterResponse: [ 20 | async (request, options, response): Promise => { 21 | const { status } = response; 22 | if ( 23 | [401, 403].includes(status) && 24 | this.props.onAuthFailure !== undefined 25 | ) { 26 | await this.props.onAuthFailure(request, options, response); 27 | } 28 | return response; 29 | }, 30 | ], 31 | }; 32 | 33 | return Ky.create({ 34 | prefixUrl: this.props.baseUrl, 35 | ...options, 36 | hooks: hooks, 37 | }); 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/pages/_monitor/sentry/ssr-page.tsx: -------------------------------------------------------------------------------- 1 | import type { GetServerSideProps, InferGetServerSidePropsType } from 'next'; 2 | 3 | type Props = { 4 | hasRunOnServer: boolean; 5 | }; 6 | 7 | export default function MonitorSentrySsrRoute( 8 | _props: InferGetServerSidePropsType 9 | ) { 10 | return ( 11 |
12 |

Unexpected error

13 |

14 | If you see this message, it means that the an error thrown in the 15 | `getServerSideProps()` function wasn't caught by the global error 16 | handler (pages/_error.tsx). This is a bug in the application and may 17 | affect the ability to display error pages and log errors on Sentry. See 18 | the monitoring page in /pages/_monitor/sentry/ssr-page.tsx. 19 |

20 |
21 | ); 22 | } 23 | 24 | /** 25 | * Always throws an error on purpose for monitoring 26 | */ 27 | export const getServerSideProps: GetServerSideProps = async ( 28 | _context 29 | // eslint-disable-next-line @typescript-eslint/require-await 30 | ): Promise => { 31 | throw new Error( 32 | 'Error purposely crafted for monitoring sentry (/pages/_monitor/sentry/ssr-page.tsx)' 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | # and https://github.com/github/gitignore for examples 3 | 4 | # local env files (followinf dotenv-flow / nextjs convention) 5 | 6 | .env.local 7 | .env.*.local 8 | 9 | # security: atlassian/changeset 10 | 11 | **/.netrc 12 | 13 | # turbo 14 | .turbo 15 | 16 | # dependencies 17 | node_modules 18 | .yarn/* 19 | !.yarn/patches 20 | !.yarn/releases 21 | !.yarn/plugins 22 | !.yarn/sdks 23 | !.yarn/versions 24 | .pnp.* 25 | 26 | # testing 27 | /coverage 28 | .out/ 29 | 30 | # Debug 31 | 32 | **/.debug 33 | 34 | # Build directories (next.js...) 35 | /.next/ 36 | /out/ 37 | /build 38 | 39 | # Cache 40 | **/tsconfig.tsbuildinfo 41 | **/.eslintcache 42 | .cache/* 43 | 44 | # Misc 45 | .DS_Store 46 | *.pem 47 | 48 | # Debug 49 | npm-debug.log* 50 | yarn-debug.log* 51 | yarn-error.log* 52 | 53 | 54 | # IDE 55 | .idea/* 56 | .project 57 | .classpath 58 | *.launch 59 | *.sublime-workspace 60 | 61 | .vscode/ 62 | !.vscode/settings.json 63 | !.vscode/tasks.json 64 | !.vscode/launch.json 65 | !.vscode/extensions.json 66 | !.vscode/*.code-snippets 67 | 68 | # Docker overrides 69 | 70 | ./docker-compose.override.yml 71 | 72 | # Deployment platforms 73 | 74 | .vercel 75 | -------------------------------------------------------------------------------- /packages/ts-utils/README.md: -------------------------------------------------------------------------------- 1 | # @your-org/ts-utils 2 | 3 | ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/belgattitude/nextjs-monorepo-example/ci-packages.yml?style=for-the-badge&label=CI) 4 | 5 | > **Note** 6 | > This package is part of [belgattitude/nextjs-monorepo-example](https://github.com/belgattitude/nextjs-monorepo-example). 7 | 8 | A package holding some basic typescript utilities: typeguards, assertions... 9 | 10 | - [x] Packaged as ES module (type: module in package.json). 11 | - [x] Can be build with tsup (no need if using tsconfig aliases). 12 | - [x] Simple unit tests demo with either Vitest (`yarn test-unit`) or TS-Jest (`yarn test-unit-jest`). 13 | 14 | ## Install 15 | 16 | From any package or apps: 17 | 18 | ```bash 19 | yarn add @your-org/ts-utils@"workspace:^" 20 | ``` 21 | 22 | ## Enable aliases 23 | 24 | ```json5 25 | { 26 | //"extends": "../../tsconfig.base.json", 27 | "compilerOptions": { 28 | "baseUrl": "./src", 29 | "paths": { 30 | "@your-org/ts-utils": ["../../../packages/ts-utils/src/index"], 31 | }, 32 | }, 33 | } 34 | ``` 35 | 36 | ## Consume 37 | 38 | ```typescript 39 | import { isPlainObject } from "@your-org/ts-utils"; 40 | 41 | isPlainObject(true) === false; 42 | ``` 43 | -------------------------------------------------------------------------------- /apps/vite-app/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Specific eslint rules for this app/package, extends the base rules 3 | * @see https://github.com/belgattitude/nextjs-monorepo-example/blob/main/docs/about-linters.md 4 | */ 5 | 6 | // Workaround for https://github.com/eslint/eslint/issues/3458 (re-export of @rushstack/eslint-patch) 7 | require('@your-org/eslint-config-bases/patch/modern-module-resolution'); 8 | 9 | const { 10 | getDefaultIgnorePatterns, 11 | } = require('@your-org/eslint-config-bases/helpers'); 12 | 13 | module.exports = { 14 | root: true, 15 | parser: '@typescript-eslint/parser', 16 | parserOptions: { 17 | tsconfigRootDir: __dirname, 18 | project: 'tsconfig.json', 19 | }, 20 | ignorePatterns: [...getDefaultIgnorePatterns()], 21 | extends: [ 22 | '@your-org/eslint-config-bases/typescript', 23 | '@your-org/eslint-config-bases/sonar', 24 | '@your-org/eslint-config-bases/regexp', 25 | '@your-org/eslint-config-bases/jest', 26 | '@your-org/eslint-config-bases/react', 27 | '@your-org/eslint-config-bases/rtl', 28 | // Apply prettier and disable incompatible rules 29 | '@your-org/eslint-config-bases/prettier-plugin', 30 | ], 31 | rules: { 32 | 'jsx-a11y/anchor-is-valid': 'off', 33 | }, 34 | overrides: [], 35 | }; 36 | -------------------------------------------------------------------------------- /packages/eslint-config-bases/src/bases/storybook.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Opinionated config base for projects using storybook. 3 | * @see https://github.com/belgattitude/shared-dx/tree/main/packages/eslint-config-bases 4 | */ 5 | 6 | const { filePatterns } = require('../config/file-patterns'); 7 | 8 | module.exports = { 9 | env: { 10 | browser: true, 11 | es6: true, 12 | node: true, 13 | }, 14 | overrides: [ 15 | { 16 | extends: ['plugin:storybook/recommended'], 17 | files: filePatterns.storybook, 18 | rules: { 19 | '@typescript-eslint/ban-ts-comment': 'off', 20 | '@typescript-eslint/no-empty-function': 'off', 21 | '@typescript-eslint/no-explicit-any': 'off', 22 | '@typescript-eslint/no-non-null-assertion': 'off', 23 | '@typescript-eslint/no-object-literal-type-assertion': 'off', 24 | '@typescript-eslint/no-unsafe-assignment': 'off', 25 | '@typescript-eslint/no-unsafe-member-access': 'off', 26 | // Relax rules that are known to be slow and less useful in a test context 27 | 'import-x/namespace': 'off', 28 | 'import-x/no-duplicates': 'off', 29 | // no checks for exports 30 | 'import-x/no-unused-modules': 'warn', 31 | }, 32 | }, 33 | ], 34 | }; 35 | -------------------------------------------------------------------------------- /packages/core-lib/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react'; 2 | import tsconfigPaths from 'vite-tsconfig-paths'; 3 | import { defineConfig } from 'vitest/config'; 4 | 5 | const testFiles = ['./src/**/*.test.{js,jsx,ts,tsx}']; 6 | 7 | export default defineConfig({ 8 | plugins: [ 9 | tsconfigPaths(), 10 | react({ 11 | // fastRefresh: false, 12 | }), 13 | ], 14 | cacheDir: '../../.cache/vitest/core-lib', 15 | test: { 16 | globals: true, 17 | environment: 'happy-dom', 18 | setupFiles: './config/test/setupVitest.ts', 19 | passWithNoTests: true, 20 | /* 21 | deps: { 22 | experimentalOptimizer: { 23 | enabled: true, 24 | }, 25 | }, */ 26 | coverage: { 27 | provider: 'v8', 28 | reporter: ['text', 'clover'], 29 | extension: ['js', 'jsx', 'ts', 'tsx'], 30 | all: true, 31 | }, 32 | // To mimic Jest behaviour regarding mocks. 33 | // @link https://vitest.dev/config/#clearmocks 34 | clearMocks: true, 35 | mockReset: true, 36 | restoreMocks: true, 37 | include: testFiles, 38 | exclude: [ 39 | '**/node_modules/**', 40 | '**/dist/**', 41 | '**/.next/**', 42 | '**/.{idea,git,cache,output,temp}/**', 43 | ], 44 | }, 45 | }); 46 | -------------------------------------------------------------------------------- /packages/ui-lib/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react-swc'; 2 | import tsconfigPaths from 'vite-tsconfig-paths'; 3 | import { defineConfig } from 'vitest/config'; 4 | 5 | const testFiles = ['./src/**/*.test.{js,jsx,ts,tsx}']; 6 | 7 | export default defineConfig({ 8 | plugins: [ 9 | tsconfigPaths(), 10 | react({ 11 | // fastRefresh: false, 12 | }), 13 | ], 14 | cacheDir: '../../.cache/vitest/ui-lib', 15 | test: { 16 | globals: true, 17 | environment: 'happy-dom', 18 | setupFiles: './config/test/setupVitest.ts', 19 | passWithNoTests: true, 20 | /* 21 | deps: { 22 | experimentalOptimizer: { 23 | enabled: true, 24 | }, 25 | }, */ 26 | coverage: { 27 | provider: 'v8', 28 | reporter: ['text', 'clover'], 29 | extension: ['js', 'jsx', 'ts', 'tsx'], 30 | all: true, 31 | }, 32 | // To mimic Jest behaviour regarding mocks. 33 | // @link https://vitest.dev/config/#clearmocks 34 | clearMocks: true, 35 | mockReset: true, 36 | restoreMocks: true, 37 | include: testFiles, 38 | exclude: [ 39 | '**/node_modules/**', 40 | '**/dist/**', 41 | '**/.next/**', 42 | '**/.{idea,git,cache,output,temp}/**', 43 | ], 44 | }, 45 | }); 46 | -------------------------------------------------------------------------------- /apps/vite-app/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react'; 2 | import svgr from 'vite-plugin-svgr'; 3 | import tsconfigPaths from 'vite-tsconfig-paths'; 4 | import { defineConfig } from 'vitest/config'; 5 | 6 | const testFiles = ['./src/**/*.test.{js,jsx,ts,tsx}']; 7 | 8 | export default defineConfig({ 9 | plugins: [ 10 | react({ 11 | jsxImportSource: '@emotion/react', 12 | babel: { 13 | plugins: ['@emotion/babel-plugin'], 14 | }, 15 | }), 16 | tsconfigPaths(), 17 | svgr({ 18 | // svgr options: https://react-svgr.com/docs/options/ 19 | svgrOptions: {}, 20 | }), 21 | ], 22 | test: { 23 | globals: true, 24 | environment: 'happy-dom', 25 | passWithNoTests: true, 26 | coverage: { 27 | provider: 'v8', 28 | reporter: ['text', 'clover'], 29 | extension: ['js', 'jsx', 'ts', 'tsx'], 30 | }, 31 | include: testFiles, 32 | // To mimic Jest behaviour regarding mocks. 33 | // @link https://vitest.dev/config/#clearmocks 34 | clearMocks: true, 35 | mockReset: true, 36 | restoreMocks: true, 37 | exclude: [ 38 | '**/node_modules/**', 39 | '**/dist/**', 40 | '**/.next/**', 41 | '**/.{idea,git,cache,output,temp}/**', 42 | ], 43 | }, 44 | }); 45 | -------------------------------------------------------------------------------- /packages/db-main-prisma/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Specific eslint rules for this workspace, learn how to compose 3 | * @link https://github.com/belgattitude/perso/tree/main/packages/eslint-config-bases 4 | */ 5 | 6 | // Workaround for https://github.com/eslint/eslint/issues/3458 7 | require('@your-org/eslint-config-bases/patch/modern-module-resolution'); 8 | 9 | const { 10 | getDefaultIgnorePatterns, 11 | } = require('@your-org/eslint-config-bases/helpers'); 12 | 13 | module.exports = { 14 | root: true, 15 | parser: '@typescript-eslint/parser', 16 | parserOptions: { 17 | tsconfigRootDir: __dirname, 18 | project: 'tsconfig.json', 19 | }, 20 | ignorePatterns: [...getDefaultIgnorePatterns(), 'src/generated'], 21 | extends: [ 22 | '@your-org/eslint-config-bases/typescript', 23 | '@your-org/eslint-config-bases/import-x', 24 | '@your-org/eslint-config-bases/sonar', 25 | '@your-org/eslint-config-bases/regexp', 26 | // Apply prettier and disable incompatible rules 27 | '@your-org/eslint-config-bases/prettier-plugin', 28 | ], 29 | overrides: [ 30 | // optional overrides per project file match 31 | { 32 | files: ['**/*seed.ts'], 33 | rules: { 34 | 'sonarjs/no-duplicate-string': 'off', 35 | }, 36 | }, 37 | ], 38 | }; 39 | -------------------------------------------------------------------------------- /packages/db-main-prisma/e2e/e2e-dsn-services.util.ts: -------------------------------------------------------------------------------- 1 | import { parseDsn, type ParsedDsn } from '@soluble/dsn-parser'; 2 | import isPortReachable from 'is-port-reachable'; 3 | import pc from 'picocolors'; 4 | 5 | const dsn = process.env.E2E_PRISMA_DATABASE_URL!; 6 | 7 | export const getValidatedDsn = (): { dsn: string } & ParsedDsn => { 8 | const parsedDsn = parseDsn(dsn); 9 | if (!parsedDsn.success) { 10 | throw new Error( 11 | `${pc.bgRed(`[SetupError]: E2E_PRISMA_DATABASE_URL ${parsedDsn.reason}`)}` 12 | ); 13 | } 14 | if (!parsedDsn.value.port) { 15 | throw new Error( 16 | `${pc.bgRed(`[SetupError]: E2E_PRISMA_DATABASE_URL must provide a port`)}` 17 | ); 18 | } 19 | return { 20 | dsn, 21 | ...parsedDsn.value, 22 | }; 23 | }; 24 | 25 | export const getAndCheckDatabaseDsn = async (): Promise => { 26 | const { dsn, port, host } = getValidatedDsn(); 27 | const reachable = await isPortReachable(port as unknown as number, { 28 | host: host, 29 | timeout: 5000, 30 | }); 31 | 32 | if (!reachable) { 33 | throw new Error( 34 | `${pc.bgRed( 35 | `[SetupError]: Unreachable required e2e database ${[host, port].join( 36 | ':' 37 | )}` 38 | )}` 39 | ); 40 | } 41 | return dsn; 42 | }; 43 | -------------------------------------------------------------------------------- /packages/ui-lib/src/_stories/assets/direction.svg: -------------------------------------------------------------------------------- 1 | illustration/direction -------------------------------------------------------------------------------- /packages/ui-lib/src/base/button/Button.tsx: -------------------------------------------------------------------------------- 1 | // To test out support for emotion-11/css prop in storybook 2 | 3 | import type { FC } from 'react'; 4 | import { cssButtonStyle } from './Button.styles'; 5 | 6 | type ButtonProps = { 7 | /** 8 | * Is this the principal call to action on the page? 9 | */ 10 | primary?: boolean; 11 | /** 12 | * What background color to use 13 | */ 14 | backgroundColor?: string; 15 | /** 16 | * How large should the button be? 17 | */ 18 | size?: 'small' | 'medium' | 'large'; 19 | /** 20 | * Button contents 21 | */ 22 | label: string; 23 | /** 24 | * Optional click handler 25 | */ 26 | onClick?: () => void; 27 | children?: never; 28 | }; 29 | 30 | /** 31 | * Primary UI component for user interaction 32 | */ 33 | export const Button: FC = (props) => { 34 | const { 35 | primary = false, 36 | size = 'medium', 37 | backgroundColor, 38 | label, 39 | ...restProps 40 | } = props; 41 | const mode = primary ? 'primary' : 'secondary'; 42 | return ( 43 | 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/pages/admin/index.tsx: -------------------------------------------------------------------------------- 1 | import { HttpBadRequest } from '@httpx/exception'; 2 | import type { GetStaticProps, InferGetStaticPropsType } from 'next'; 3 | import type { ReactElement } from 'react'; 4 | import { adminConfig } from '@/features/admin/admin.config'; 5 | import { AdminLayout } from '@/features/admin/layouts'; 6 | import { AdminMainPage } from '@/features/admin/pages'; 7 | import { getServerTranslations } from '@/server/i18n/getServerTranslations'; 8 | 9 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 10 | type Props = { 11 | /** Add props here */ 12 | }; 13 | 14 | AdminRoute.getLayout = function getLayout(page: ReactElement) { 15 | return {page}; 16 | }; 17 | 18 | export const getStaticProps: GetStaticProps = async (context) => { 19 | const { locale } = context; 20 | if (locale === undefined) { 21 | throw new HttpBadRequest('locale is missing'); 22 | } 23 | const { i18nNamespaces } = adminConfig; 24 | return { 25 | props: { 26 | ...(await getServerTranslations(locale, i18nNamespaces)), 27 | }, 28 | // revalidate: 60, 29 | }; 30 | }; 31 | 32 | export default function AdminRoute( 33 | _props: InferGetStaticPropsType 34 | ) { 35 | return ; 36 | } 37 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | ## Docker 2 | 3 | ### Requirements 4 | 5 | - [x] [docker-engine](https://docs.docker.com/get-docker) >= 23 and [buildkit](https://docs.docker.com/build/buildkit/). 6 | - [x] docker compose v2: [repo](https://github.com/docker/compose) - [docs](https://docs.docker.com/compose/) 7 | - [x] optional: [lazydocker](https://github.com/jesseduffield/lazydocker), a beautiful tui. 8 | - [x] optional: [dive](https://github.com/wagoodman/dive) to debug layer sizes. 9 | 10 | ### Quick run 11 | 12 | ```bash 13 | cd ./docker 14 | docker compose build 15 | docker compose build --progress=plain # More verbose 16 | docker compose build --parallel # Might be faster 17 | docker compose up 18 | docker compose down 19 | ``` 20 | 21 | ### Cheat sheet 22 | 23 | ```bash 24 | export IMAGE=nextjs-monorepo-example-nextjs-app 25 | 26 | # Inspect the image 27 | docker image inspect ${IMAGE} 28 | 29 | # Save te image (gzip) 30 | docker save ${IMAGE} | gip > /tmp/${IMAGE}-app.tar.gz 31 | # +/- 70M ${IMAGE}.tar.gz (slower) 32 | 33 | docker save ${IMAGE} | zstd | pv > /tmp/${IMAGE}.tar.zst 34 | # +/- 60M Jul 20 10:54 ${IMAGE}.tar.zst (faster) 35 | 36 | # if not using k8s/registry, you can load and run from a remote machine. 37 | docker load -i /tmp/${IMAGE}.tar.zst 38 | 39 | # Run the image 40 | docker run ${IMAGE} 41 | ``` 42 | -------------------------------------------------------------------------------- /packages/eslint-config-bases/src/bases/perfectionist.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom config base for projects using typescript / javascript. 3 | * @see https://github.com/belgattitude/shared-dx/tree/main/packages/eslint-config-bases#belgattitudeeslint-config-bases 4 | */ 5 | 6 | module.exports = { 7 | overrides: [ 8 | { 9 | extends: ['plugin:perfectionist/recommended-natural-legacy'], 10 | files: ['*.js', '*.cjs', '*.mjs', '*.ts'], 11 | rules: { 12 | // import/order is used 13 | 'perfectionist/sort-named-imports': 'off', 14 | 'perfectionist/sort-imports': 'off', 15 | // Keep at false as it can create issue when code relies on keys order 16 | 'perfectionist/sort-maps': 'off', 17 | // Keep at false as because it does not work with class properties 18 | 'perfectionist/sort-classes': 'off', 19 | // Keep at false as it can create issue when code relies on keys order 20 | 'perfectionist/sort-objects': 'off', 21 | // Keep at false as it can create issue when code relies on keys order 22 | 'perfectionist/sort-union-types': 'off', 23 | // May introduce performance degradation 24 | 'perfectionist/sort-array-includes': 'off', 25 | 'perfectionist/sort-jsx-props': 'off', 26 | }, 27 | }, 28 | ], 29 | }; 30 | -------------------------------------------------------------------------------- /apps/nextjs-app/.size-limit.cjs: -------------------------------------------------------------------------------- 1 | // Just a basic example for size limit with simple file preset 2 | // @link https://github.com/ai/size-limit 3 | 4 | let manifest; 5 | try { 6 | manifest = require('./.next/build-manifest.json'); 7 | } catch (e) { 8 | throw new Error( 9 | 'Cannot find a NextJs build folder, did you forget to build ?' 10 | ); 11 | } 12 | const pages = manifest.pages; 13 | 14 | const limitCfg = { 15 | defaultSize: '105kb', 16 | pages: { 17 | '/': '140kb', 18 | '/404': '105kb', 19 | '/_app': '175kb', 20 | '/_error': '105kb', 21 | '/_monitor/sentry/csr-page': '105kb', 22 | '/_monitor/sentry/ssr-page': '105kb', 23 | '/_monitor/preview/error-page': '105kb', 24 | '/admin': '115kb', 25 | '/auth/login': '130kb', 26 | '/home': '122kb', 27 | }, 28 | }; 29 | const getPageLimits = () => { 30 | let pageLimits = []; 31 | for (const [uri, paths] of Object.entries(pages)) { 32 | pageLimits.push({ 33 | name: `Page '${uri}'`, 34 | limit: limitCfg.pages?.[uri] ?? limitCfg.defaultSize, 35 | path: paths.map((p) => `.next/${p}`), 36 | }); 37 | } 38 | return pageLimits; 39 | }; 40 | 41 | module.exports = [ 42 | ...getPageLimits(), 43 | { 44 | name: 'CSS', 45 | path: ['.next/static/css/**/*.css'], 46 | limit: '15 kB', 47 | }, 48 | ]; 49 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/features/system/pages/ErrorPage.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import type { FC } from 'react'; 3 | 4 | type Props = { 5 | statusCode?: number | null; 6 | error?: Error; 7 | message?: string; 8 | errorId?: string; 9 | children?: never; 10 | }; 11 | 12 | export const ErrorPage: FC = (props) => { 13 | const { error, errorId, message, statusCode } = props; 14 | 15 | return ( 16 | <> 17 | 18 | Error {statusCode} 19 | 20 |
21 |
22 |

Woops !

23 |

24 | Something went wrong. Please try again later. 25 |

26 |
27 |
28 |

Code: {statusCode}

29 |

Message: {message}

30 |

Error id: {errorId}

31 |

ErrorMessage: {error?.message}

32 |
33 |
34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /packages/common-i18n/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@your-org/common-i18n", 3 | "version": "1.2.2", 4 | "private": true, 5 | "sideEffects": false, 6 | "type": "module", 7 | "exports": { 8 | "./locales/*.json": { 9 | "require": "./src/locales/*.json", 10 | "import": "./src/locales/*.json" 11 | }, 12 | "./package.json": "./package.json" 13 | }, 14 | "author": { 15 | "name": "Vanvelthem Sébastien", 16 | "url": "https://github.com/belgattitude" 17 | }, 18 | "license": "MIT", 19 | "homepage": "https://github.com/belgattitude/nextjs-monorepo-example", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/belgattitude/nextjs-monorepo-example", 23 | "directory": "packages/common-i18n" 24 | }, 25 | "scripts": { 26 | "clean": "rimraf ./dist ./coverage ./tsconfig.tsbuildinfo", 27 | "lint": "eslint . --ext .ts,.tsx,.js,.jsx,.cjs,.mjs --cache --cache-location ../../.cache/eslint/common-i18n.eslintcache", 28 | "typecheck": "tsc --project ./tsconfig.json --noEmit" 29 | }, 30 | "devDependencies": { 31 | "@types/node": "22.13.5", 32 | "@your-org/eslint-config-bases": "workspace:^", 33 | "cross-env": "7.0.3", 34 | "eslint": "8.57.1", 35 | "prettier": "3.5.2", 36 | "rimraf": "6.0.1", 37 | "typescript": "5.7.3" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/lib/env/getValidatedBuildEnv.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { exitOrThrowError, printValidatedEnv } from './utils.mjs'; 4 | 5 | /** 6 | * Return a validated / transformed environment object from a zodSchema. This is to be used to tune 7 | * the build/dev process in next.config.mjs. By convention prefer the `NEXT_BUILD_` prefix if there isn't a 8 | * good reason to do otherwise. 9 | * 10 | * Validated build envs are shown in the console by default. In case of error it will 11 | * exit/die with an error indicating missing requirements. This is particularly helpful in CI, 12 | * multiple deployments (previews, staging...) to give a clear indication a build parameters 13 | * used (or debug). 14 | * 15 | * @template { import('zod').ZodSchema } T 16 | * @param { T } zodSchema 17 | * @param {{ displayConsole?: boolean, env?: Record }} options 18 | * @returns { import('zod').infer } 19 | */ 20 | export const getValidatedBuildEnv = (zodSchema, options = {}) => { 21 | const { env = process.env, displayConsole = true } = options ?? {}; 22 | const parsedEnv = zodSchema.safeParse(env); 23 | if (parsedEnv.success) { 24 | if (displayConsole) { 25 | printValidatedEnv('Build env(s)', parsedEnv); 26 | } 27 | return parsedEnv.data; 28 | } 29 | exitOrThrowError(parsedEnv); 30 | }; 31 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/pages/api/rest/post/index.ts: -------------------------------------------------------------------------------- 1 | import { HttpMethodNotAllowed } from '@httpx/exception'; 2 | import { JsonApiResponseFactory, JsonApiErrorFactory } from '@httpx/json-api'; 3 | import type { NextApiRequest, NextApiResponse } from 'next'; 4 | import { PostRepositorySsr } from '@/server/api/rest/post-repository.ssr'; 5 | import { prismaClient } from '@/server/config/container.config'; 6 | 7 | export default async function handleListPosts( 8 | req: NextApiRequest, 9 | res: NextApiResponse 10 | ) { 11 | if (req.method === 'GET') { 12 | const postRepo = new PostRepositorySsr(prismaClient); 13 | try { 14 | return res.json( 15 | JsonApiResponseFactory.fromSuccess( 16 | await postRepo.getPosts({ 17 | limit: 100, 18 | }) 19 | ) 20 | ); 21 | } catch (e) { 22 | const apiError = JsonApiErrorFactory.fromCatchVariable(e); 23 | return res 24 | .status(apiError.status ?? 500) 25 | .json(JsonApiResponseFactory.fromError(apiError)); 26 | } 27 | } else { 28 | return res 29 | .status(HttpMethodNotAllowed.STATUS) 30 | .json( 31 | JsonApiResponseFactory.fromError( 32 | `The HTTP ${req.method} method is not supported at this route.`, 33 | HttpMethodNotAllowed.STATUS 34 | ) 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /apps/nextjs-app/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { EmotionCache } from '@emotion/cache'; 2 | import { Analytics } from '@vercel/analytics/react'; 3 | import type { AppProps } from 'next/app'; 4 | import Head from 'next/head'; 5 | import { appWithTranslation } from 'next-i18next'; 6 | import nextI18nextConfig from '../../next-i18next.config.mjs'; 7 | import { AppProviders } from '../providers/AppProviders'; 8 | 9 | /** 10 | * Import global styles, global css or polyfills here 11 | * i.e.: import '@/assets/theme/style.scss' 12 | */ 13 | import '../styles/global.css'; 14 | 15 | import '@fontsource-variable/inter'; 16 | 17 | export type MyAppProps = AppProps & { 18 | emotionCache?: EmotionCache; 19 | }; 20 | 21 | /** 22 | * @link https://nextjs.org/docs/advanced-features/custom-app 23 | */ 24 | const MyApp = (appProps: MyAppProps) => { 25 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 26 | const { Component, pageProps, emotionCache } = appProps; 27 | return ( 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ); 36 | }; 37 | 38 | export default appWithTranslation(MyApp, { 39 | ...nextI18nextConfig, 40 | }); 41 | -------------------------------------------------------------------------------- /packages/db-main-prisma/src/seeds/UserSeeds.ts: -------------------------------------------------------------------------------- 1 | import type { PrismaDbMain as Prisma } from '..'; 2 | import { AbstractSeed } from '../lib/AbstractSeed'; 3 | 4 | const userData: Prisma.UserCreateInput[] = [ 5 | { 6 | firstName: 'Sébastien', 7 | lastName: 'Vanvelthem', 8 | username: 'belgattitude', 9 | email: 'belgattitude@gmail.com', 10 | Post: { 11 | create: [ 12 | { 13 | title: 'Nextjs monorepo example', 14 | slug: 'first-post', 15 | link: 'https://github.com/belgattitude/nextjs-monorepo-example', 16 | content: 'Hello world', 17 | image: 18 | 'https://images.unsplash.com/photo-1625904835711-fa25795530e8?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1052&q=80', 19 | publishedAt: new Date(), 20 | }, 21 | ], 22 | }, 23 | }, 24 | ]; 25 | 26 | export class UserSeeds extends AbstractSeed { 27 | execute = async (): Promise => { 28 | for (const u of userData) { 29 | const { email, username, ...userNonUnique } = u; 30 | const user = await this.prisma.user.upsert({ 31 | where: { email }, 32 | update: userNonUnique, 33 | create: u, 34 | }); 35 | this.log('UPSERT', `User ${user.id} - ${user.email} - ${user.password}`); 36 | } 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /packages/ts-utils/lint-staged.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * This files overrides the base lint-staged.config.js present in the root directory. 5 | * It allows to run eslint based the package specific requirements. 6 | * {@link https://github.com/okonet/lint-staged#how-to-use-lint-staged-in-a-multi-package-monorepo} 7 | * {@link https://github.com/belgattitude/nextjs-monorepo-example/blob/main/docs/about-lint-staged.md} 8 | */ 9 | 10 | const { 11 | concatFilesForPrettier, 12 | getEslintFixCmd, 13 | } = require('../../lint-staged.common'); 14 | 15 | /** 16 | * @type {Record string | string[] | Promise>} 17 | */ 18 | const rules = { 19 | '**/*.{js,jsx,ts,tsx}': (filenames) => { 20 | return getEslintFixCmd({ 21 | cwd: __dirname, 22 | fix: true, 23 | cache: true, 24 | // when autofixing staged-files a good tip is to disable react-hooks/exhaustive-deps, cause 25 | // a change here can potentially break things without proper visibility. 26 | rules: ['react-hooks/exhaustive-deps: off'], 27 | maxWarnings: 25, 28 | files: filenames, 29 | }); 30 | }, 31 | '**/*.{json,md,mdx,css,html,yml,yaml,scss}': (filenames) => { 32 | return [`yarn prettier --write ${concatFilesForPrettier(filenames)}`]; 33 | }, 34 | }; 35 | 36 | module.exports = rules; 37 | -------------------------------------------------------------------------------- /apps/nextjs-app/lint-staged.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * This files overrides the base lint-staged.config.js present in the root directory. 5 | * It allows to run eslint based the package specific requirements. 6 | * {@link https://github.com/okonet/lint-staged#how-to-use-lint-staged-in-a-multi-package-monorepo} 7 | * {@link https://github.com/belgattitude/nextjs-monorepo-example/blob/main/docs/about-lint-staged.md} 8 | */ 9 | 10 | const { 11 | concatFilesForPrettier, 12 | getEslintFixCmd, 13 | } = require('../../lint-staged.common.js'); 14 | 15 | /** 16 | * @type {Record string | string[] | Promise>} 17 | */ 18 | const rules = { 19 | '**/*.{js,jsx,ts,tsx,mjs,cjs}': (filenames) => { 20 | return getEslintFixCmd({ 21 | cwd: __dirname, 22 | fix: true, 23 | cache: true, 24 | // when autofixing staged-files a good tip is to disable react-hooks/exhaustive-deps, cause 25 | // a change here can potentially break things without proper visibility. 26 | rules: ['react-hooks/exhaustive-deps: off'], 27 | maxWarnings: 25, 28 | files: filenames, 29 | }); 30 | }, 31 | '**/*.{json,md,mdx,css,html,yml,yaml,scss}': (filenames) => { 32 | return [`prettier --write ${concatFilesForPrettier(filenames)}`]; 33 | }, 34 | }; 35 | 36 | module.exports = rules; 37 | -------------------------------------------------------------------------------- /apps/vite-app/lint-staged.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * This files overrides the base lint-staged.config.js present in the root directory. 5 | * It allows to run eslint based the package specific requirements. 6 | * {@link https://github.com/okonet/lint-staged#how-to-use-lint-staged-in-a-multi-package-monorepo} 7 | * {@link https://github.com/belgattitude/nextjs-monorepo-example/blob/main/docs/about-lint-staged.md} 8 | */ 9 | 10 | const { 11 | concatFilesForPrettier, 12 | getEslintFixCmd, 13 | } = require('../../lint-staged.common.js'); 14 | 15 | /** 16 | * @type {Record string | string[] | Promise>} 17 | */ 18 | const rules = { 19 | '**/*.{js,jsx,ts,tsx,mjs,cjs}': (filenames) => { 20 | return getEslintFixCmd({ 21 | cwd: __dirname, 22 | fix: true, 23 | cache: true, 24 | // when autofixing staged-files a good tip is to disable react-hooks/exhaustive-deps, cause 25 | // a change here can potentially break things without proper visibility. 26 | rules: ['react-hooks/exhaustive-deps: off'], 27 | maxWarnings: 25, 28 | files: filenames, 29 | }); 30 | }, 31 | '**/*.{json,md,mdx,css,html,yml,yaml,scss}': (filenames) => { 32 | return [`prettier --write ${concatFilesForPrettier(filenames)}`]; 33 | }, 34 | }; 35 | 36 | module.exports = rules; 37 | -------------------------------------------------------------------------------- /packages/ui-lib/lint-staged.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * This files overrides the base lint-staged.config.js present in the root directory. 5 | * It allows to run eslint based the package specific requirements. 6 | * {@link https://github.com/okonet/lint-staged#how-to-use-lint-staged-in-a-multi-package-monorepo} 7 | * {@link https://github.com/belgattitude/nextjs-monorepo-example/blob/main/docs/about-lint-staged.md} 8 | */ 9 | 10 | const { 11 | concatFilesForPrettier, 12 | getEslintFixCmd, 13 | } = require('../../lint-staged.common.js'); 14 | 15 | /** 16 | * @type {Record string | string[] | Promise>} 17 | */ 18 | const rules = { 19 | '**/*.{js,jsx,ts,tsx,mjs,cjs}': (filenames) => { 20 | return getEslintFixCmd({ 21 | cwd: __dirname, 22 | fix: true, 23 | cache: true, 24 | // when autofixing staged-files a good tip is to disable react-hooks/exhaustive-deps, cause 25 | // a change here can potentially break things without proper visibility. 26 | rules: ['react-hooks/exhaustive-deps: off'], 27 | maxWarnings: 25, 28 | files: filenames, 29 | }); 30 | }, 31 | '**/*.{json,md,mdx,css,html,yml,yaml,scss}': (filenames) => { 32 | return [`prettier --write ${concatFilesForPrettier(filenames)}`]; 33 | }, 34 | }; 35 | 36 | module.exports = rules; 37 | -------------------------------------------------------------------------------- /packages/common-i18n/lint-staged.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * This files overrides the base lint-staged.config.js present in the root directory. 5 | * It allows to run eslint based the package specific requirements. 6 | * {@link https://github.com/okonet/lint-staged#how-to-use-lint-staged-in-a-multi-package-monorepo} 7 | * {@link https://github.com/belgattitude/nextjs-monorepo-example/blob/main/docs/about-lint-staged.md} 8 | */ 9 | 10 | const { 11 | concatFilesForPrettier, 12 | getEslintFixCmd, 13 | } = require('../../lint-staged.common.js'); 14 | 15 | /** 16 | * @type {Record string | string[] | Promise>} 17 | */ 18 | const rules = { 19 | '**/*.{js,jsx,ts,tsx,mjs,cjs}': (filenames) => { 20 | return getEslintFixCmd({ 21 | cwd: __dirname, 22 | fix: true, 23 | cache: true, 24 | // when autofixing staged-files a good tip is to disable react-hooks/exhaustive-deps, cause 25 | // a change here can potentially break things without proper visibility. 26 | rules: ['react-hooks/exhaustive-deps: off'], 27 | maxWarnings: 25, 28 | files: filenames, 29 | }); 30 | }, 31 | '**/*.{json,md,mdx,css,html,yml,yaml,scss}': (filenames) => { 32 | return [`prettier --write ${concatFilesForPrettier(filenames)}`]; 33 | }, 34 | }; 35 | 36 | module.exports = rules; 37 | -------------------------------------------------------------------------------- /packages/core-lib/lint-staged.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * This files overrides the base lint-staged.config.js present in the root directory. 5 | * It allows to run eslint based the package specific requirements. 6 | * {@link https://github.com/okonet/lint-staged#how-to-use-lint-staged-in-a-multi-package-monorepo} 7 | * {@link https://github.com/belgattitude/nextjs-monorepo-example/blob/main/docs/about-lint-staged.md} 8 | */ 9 | 10 | const { 11 | concatFilesForPrettier, 12 | getEslintFixCmd, 13 | } = require('../../lint-staged.common.js'); 14 | 15 | /** 16 | * @type {Record string | string[] | Promise>} 17 | */ 18 | const rules = { 19 | '**/*.{js,jsx,ts,tsx,mjs,cjs}': (filenames) => { 20 | return getEslintFixCmd({ 21 | cwd: __dirname, 22 | fix: true, 23 | cache: true, 24 | // when autofixing staged-files a good tip is to disable react-hooks/exhaustive-deps, cause 25 | // a change here can potentially break things without proper visibility. 26 | rules: ['react-hooks/exhaustive-deps: off'], 27 | maxWarnings: 25, 28 | files: filenames, 29 | }); 30 | }, 31 | '**/*.{json,md,mdx,css,html,yml,yaml,scss}': (filenames) => { 32 | return [`prettier --write ${concatFilesForPrettier(filenames)}`]; 33 | }, 34 | }; 35 | 36 | module.exports = rules; 37 | --------------------------------------------------------------------------------