diff --git a/apps/cms/package.json b/apps/cms/package.json index 88c4769..00e0f73 100644 --- a/apps/cms/package.json +++ b/apps/cms/package.json @@ -3,6 +3,7 @@ "description": "Payload CMS instance", "version": "1.0.0", "license": "MIT", + "type": "module", "types": "./src/index.ts", "exports": { ".": { @@ -26,30 +27,35 @@ "scripts": { "clean": "rm -rf node_modules dist build .turbo", "build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/config.ts payload build", - "build:server": "rm -rf dist/* && tsc", + "build:server": "rm -rf dist/* && tsc && tsc-alias", "build": "pnpm build:payload && pnpm build:server && pnpm copyfiles", "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/ && copyfiles -u 1 \"build/**/*\" ../server/build", "generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/config.ts payload generate:types", "generate:graphQLSchema": "PAYLOAD_CONFIG_PATH=src/config.ts payload generate:graphQLSchema", - "lint": "eslint --ext .ts,.tsx,.js,.jsx ./src" + "lint": "eslint --ext .ts,.tsx,.js,.jsx ./src", + "payload": "cross-env PAYLOAD_CONFIG_PATH=src/config.ts payload" }, "dependencies": { "@org/ui": "workspace:*", - "payload": "^1.6.9" + "@payloadcms/bundler-vite": "^0.1.6", + "@payloadcms/db-mongodb": "^1.4.3", + "@payloadcms/richtext-lexical": "^0.7.0", + "payload": "^2.11.2" }, "devDependencies": { "@org/shared": "workspace:*", - "@types/express": "^4.17.17", - "@types/node": "^18.13.0", - "@types/react": "^18.0.27", + "@types/express": "^4.17.21", + "@types/node": "^20.11.20", + "@types/react": "^18.2.59", "@types/react-router-dom": "^5.3.3", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", - "nodemon": "^2.0.20", + "nodemon": "^3.1.0", "react": "^18.2.0", "react-router-dom": "^5.3.4", - "ts-node": "^10.9.1", - "typescript": "^4.9.5" + "ts-node": "^10.9.2", + "tsc-alias": "^1.8.8", + "typescript": "^5.3.3" }, "peerDependencies": { "@org/shared": "workspace:*", diff --git a/apps/cms/src/blocks/Image.ts b/apps/cms/src/blocks/Image.ts index b62700f..5294c77 100644 --- a/apps/cms/src/blocks/Image.ts +++ b/apps/cms/src/blocks/Image.ts @@ -39,7 +39,7 @@ export const Image: Block = { label: 'Caption', type: 'richText', admin: { - elements: ['link'], + // elements: ['link'], }, }, ], diff --git a/apps/cms/src/config.ts b/apps/cms/src/config.ts index 4eaad76..470afb4 100644 --- a/apps/cms/src/config.ts +++ b/apps/cms/src/config.ts @@ -1,15 +1,27 @@ import path from 'path'; -import { buildConfig } from 'payload/config'; -import Users from './collections/Users'; + import { Payload } from 'payload'; +import { buildConfig } from 'payload/config'; + +import { mongooseAdapter } from '@payloadcms/db-mongodb'; +import { viteBundler } from '@payloadcms/bundler-vite'; +import { lexicalEditor } from '@payloadcms/richtext-lexical'; + import { seedPages, seedUsers } from './seed/index'; + import Media from './collections/Media'; import Pages from './collections/Pages'; +import Users from './collections/Users'; const config = buildConfig({ admin: { user: Users.slug, + bundler: viteBundler(), }, + db: mongooseAdapter({ + url: process.env.MONGODB_URL ?? 'mongodb://localhost/remix-server', + }), + editor: lexicalEditor(), collections: [Users, Media, Pages], typescript: { outputFile: path.resolve(__dirname, 'payload-types.ts'), diff --git a/apps/cms/src/index.ts b/apps/cms/src/index.ts index 41d51fa..70a8729 100644 --- a/apps/cms/src/index.ts +++ b/apps/cms/src/index.ts @@ -1,3 +1,3 @@ export * from 'payload'; export { default as payload } from 'payload'; -export * from './payload-types'; \ No newline at end of file +export * from './payload-types'; diff --git a/apps/cms/src/seed/home-page.json b/apps/cms/src/seed/home-page.json index 14afab5..b2101c6 100644 --- a/apps/cms/src/seed/home-page.json +++ b/apps/cms/src/seed/home-page.json @@ -3,93 +3,125 @@ { "blockType": "content", "blockName": "Intro - Rich Text Demo", - "content": [ - { - "children": [ - { - "text": "Here is an H2 to introduce the article" - } - ], - "type": "h3" - }, - { + "content": { + "root": { + "type": "root", "children": [ { - "text": "Here is some content that will be rendered as an HTML paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam." - } - ], - "type": "p" - }, - { - "children": [ + "children": [ + { + "text": "Here is an H2 to introduce the article", + "type": "text" + } + ], + "type": "heading", + "tag": "h3" + }, { - "text": "Lorem Ipsum", - "bold": true + "children": [ + { + "text": "Here is some content that will be rendered as an HTML paragraph. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam.", + "type": "text" + } + ], + "type": "paragraph" }, { - "text": " is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book." + "children": [ + { + "text": "Lorem Ipsum", + "format": 1, + "type": "text" + }, + { + "text": " is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.", + "type": "text" + } + ], + "type": "paragraph" } - ], - "type": "p" + ] } - ] + } }, { "type": "feature", "blockType": "image", "blockName": "Wide Image Demo", "image": "{{IMAGE_ID}}", - "caption": [ - { + "caption": { + "root": { + "type": "root", "children": [ { - "text": "Here is an image caption. It's got a " - }, - { - "type": "link", - "url": "https://payloadcms.com", - "newTab": true, "children": [ { - "text": "link embedded in it" + "text": "Here is an image caption. It's got a ", + "type": "text" + }, + { + "children": [ + { + "text": "link embedded in it", + "type": "text" + } + ], + "type": "link", + "fields": { + "linkType": "custom", + "newTab": true, + "url": "https://payloadcms.com" + } } - ] - }, - { - "text": "" + ], + "type": "paragraph" } ] } - ] + } }, { "type": "card", "blockType": "image", "blockName": "Normal Width Image Demo", "image": "{{IMAGE_ID}}", - "caption": [ - { + "caption": { + "root": { + "type": "root", "children": [ { - "text": "This is a caption for an image." + "children": [ + { + "text": "This is a caption for an image.", + "type": "text" + } + ], + "type": "paragraph" } ] } - ] + } }, { "blockType": "cta", "blockName": "Calls to Action", - "content": [ - { + "content": { + "root": { + "type": "root", "children": [ { - "text": "Here is a Call to Action block" + "children": [ + { + "text": "Here is a Call to Action block", + "type": "text" + } + ], + "type": "heading", + "tag": "h4" } - ], - "type": "h4" + ] } - ], + }, "buttons": [ { "type": "custom", diff --git a/apps/cms/src/seed/index.ts b/apps/cms/src/seed/index.ts index 53d63a6..646d2e0 100644 --- a/apps/cms/src/seed/index.ts +++ b/apps/cms/src/seed/index.ts @@ -15,9 +15,7 @@ export const seedPages = async (payload: Payload) => { collection: mediaSlug, data: { alt: 'Payload', - // Payloads incorrectly expects a 'sizes' object here, which should be optional since they are created during upload - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any, + }, filePath: path.resolve(__dirname, './payload.jpg'), }); @@ -27,8 +25,8 @@ export const seedPages = async (payload: Payload) => { }); const publicString = JSON.stringify(home) - .replace(/{{IMAGE_ID}}/g, createdMedia.id) - .replace(/{{SAMPLE_PAGE_ID}}/g, createdPostsPage.id); + .replace(/{{IMAGE_ID}}/g, createdMedia.id.toString()) + .replace(/{{SAMPLE_PAGE_ID}}/g, createdPostsPage.id.toString()); await payload.create({ collection: pagesSlug, diff --git a/apps/cms/src/seed/posts-page.ts b/apps/cms/src/seed/posts-page.ts index 06ae894..9b27328 100644 --- a/apps/cms/src/seed/posts-page.ts +++ b/apps/cms/src/seed/posts-page.ts @@ -1,43 +1,48 @@ export default { - "layout": [ + layout: [ { - "blockType": "content" as const, - "blockName": "Page Content", - "content": [ - { - "children": [ + blockType: 'content' as const, + blockName: 'Page Content', + content: { + root: { + type: 'root', + children: [ { - "text": "This is a sample page which is only visible to authenticated users." - } - ], - "type": "h3" - }, - { - "children": [ - { - "text": "" + children: [ + { + text: 'This is a sample page which is only visible to authenticated users.', + type: 'text', + }, + ], + type: 'heading', + tag: 'h3', }, { - "type": "link", - "url": "/", - "newTab": false, - "children": [ + children: [ { - "text": "Go back home" - } - ] + children: [ + { + text: 'Go back home', + type: 'text', + }, + ], + type: 'link', + fields: { + linkType: 'custom', + newTab: false, + url: '/', + }, + }, + ], + type: 'paragraph', }, - { - "text": "" - } ], - "type": "p" - } - ] - } + }, + }, + }, ], - "title": "Posts", - "public": false, - "slug": "posts" as const, - "meta": {} -} + title: 'Posts', + public: false, + slug: 'posts' as const, + meta: {}, +}; diff --git a/apps/cms/tsconfig.json b/apps/cms/tsconfig.json index f5abc9e..76f7adb 100644 --- a/apps/cms/tsconfig.json +++ b/apps/cms/tsconfig.json @@ -5,6 +5,12 @@ "paths": { "payload/generated-types": ["./src/payload-types.ts"] }, + "module": "ESNext", + "moduleResolution": "Bundler", + }, + "tsc-alias": { + "resolveFullPaths": true, + "verbose": false }, "include": ["src/**/*"] } diff --git a/apps/server/nodemon.json b/apps/server/nodemon.json index 184dc49..e6ca7eb 100644 --- a/apps/server/nodemon.json +++ b/apps/server/nodemon.json @@ -1,6 +1,6 @@ { "verbose": true, "ext": "ts", - "exec": "node -r ts-node/register src/index.ts", + "exec": "tsx src/index.ts", "watch": ["./src/index.ts", "../cms/src"] } diff --git a/apps/server/package.json b/apps/server/package.json index c15bd9e..4a94307 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -3,28 +3,39 @@ "description": "Server for running several web applications", "version": "1.0.0", "license": "MIT", + "type": "module", "scripts": { "clean": "rm -rf node_modules build dist .turbo", - "dev": "cross-env PAYLOAD_CONFIG_PATH=../cms/src/config.ts NODE_ENV=development nodemon", + "dev": "cross-env PAYLOAD_CONFIG_PATH=../cms/src/config.ts NODE_ENV=development REMIX_ROOT=../web nodemon", "build:server": "rm -rf dist/* && tsc", "build": "pnpm build:server", - "serve": "cross-env PAYLOAD_CONFIG_PATH=../cms/dist/config.js NODE_ENV=production node --conditions=serve dist/index.js", + "serve": "cross-env PAYLOAD_CONFIG_PATH=../cms/dist/config.js NODE_ENV=production node --trace-warnings --conditions=serve dist/index.js", "lint": "eslint --ext .ts,.js ./src/index.ts" }, "dependencies": { "@org/cms": "workspace:*", "@org/shared": "workspace:*", "@org/web": "workspace:*", - "express": "^4.18.2" + "compression": "^1.7.4", + "express": "^4.18.2", + "morgan": "^1.10.0", + "source-map-support": "^0.5.21" }, "devDependencies": { - "@types/express": "^4.17.17", + "@remix-run/dev": "^2.7.2", + "@types/compression": "^1.7.5", + "@types/express": "^4.17.21", + "@types/morgan": "^1.9.9", "@types/node": "^18.13.0", + "@types/source-map-support": "^0.5.10", + "chokidar": "^3.6.0", "copyfiles": "^2.4.1", "cross-env": "^7.0.3", - "nodemon": "^2.0.20", - "ts-node": "^10.9.1", - "typescript": "^4.9.5" + "nodemon": "^3.1.0", + "ts-node": "^10.9.2", + "tsx": "^4.7.1", + "typescript": "^5.3.3", + "vite": "^5.1.4" }, "engines": { "node": ">=16.0.0" diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 30442b2..1d68a95 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -1,14 +1,14 @@ -import path = require('path'); -import fs = require('fs'); +import fs from 'node:fs'; +import path from 'node:path'; -import express = require('express'); -import cms = require('@org/cms'); -import shared = require('@org/shared'); -import webExpressAdapter = require('@org/web/express'); +import { createRequestHandler, installGlobals } from '@org/web/express'; -const { payload } = cms; -const { dotenv } = shared; -const { createRequestHandler } = webExpressAdapter; +import compression from 'compression'; +import express from 'express'; +import morgan from 'morgan'; + +import { dotenv } from '@org/shared'; +import { payload } from '@org/cms'; // Loading environment variables, .env > .env.local const config = dotenv.config(); @@ -28,95 +28,81 @@ if (fs.existsSync(localEnvFilePath)) { } } -const MONGODB_URL = process.env.MONGODB_URL ?? ""; -const PAYLOADCMS_SECRET = process.env.PAYLOADCMS_SECRET ?? ""; +const PAYLOADCMS_SECRET = process.env.PAYLOADCMS_SECRET ?? ''; const ENVIRONMENT = process.env.NODE_ENV; // During development this is fine. Conditionalize this for production as needed. -const WEB_BUILD_DIR = path.join(process.cwd(), '../web/build'); -const WEB_PUBLIC_DIR = path.join(process.cwd(), '../web/public/web'); -const WEB_PUBLIC_BUILD_DIR = path.join( - process.cwd(), - '../web/public/web/build' -); +const WEB_DIR = path.join(process.cwd(), '../web'); +const WEB_BUILD_CLIENT = path.join(WEB_DIR, 'build/client'); +const WEB_BUILD_CLIENT_ASSETS = path.join(WEB_BUILD_CLIENT, 'assets'); + +installGlobals(); + +const viteDevServer = + process.env.NODE_ENV === 'production' + ? undefined + : await import('vite').then((vite) => + vite.createServer({ + root: WEB_DIR, + server: { middlewareMode: true }, + }), + ); + +const remixHandler = createRequestHandler({ + build: viteDevServer + ? () => viteDevServer.ssrLoadModule('virtual:remix/server-build') + : await import(path.join(WEB_DIR, 'build/server/index.js')), + mode: ENVIRONMENT, + getLoadContext(req, res) { + return { + payload: req.payload, + user: req?.user, + res, + }; + }, +}); const app = express(); -app.disable('x-powered-by'); -// Serving the web static files with different caching strategies -app.use( - '/web/build', - express.static(WEB_PUBLIC_BUILD_DIR, { - immutable: true, - maxAge: '1y', - redirect: false, - }) -); -app.use( - '/web', - express.static(WEB_PUBLIC_DIR, { maxAge: '1h', redirect: false }) -); +app.use(compression()); + +// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header +app.disable('x-powered-by'); -payload.init({ +await payload.init({ express: app, - mongoURL: MONGODB_URL, secret: PAYLOADCMS_SECRET, onInit: () => { payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`); }, -}).then(() => { - - app.use(payload.authenticate); - - app.all( - '*', - ENVIRONMENT === 'development' - ? (req, res, next) => { - purgeRequireCache(); - - return createRequestHandler({ - build: require(WEB_BUILD_DIR), - mode: ENVIRONMENT, - getLoadContext(req, res) { - return { - payload: req.payload, - user: req?.user, - res, - }; - }, - })(req, res, next); - } - : createRequestHandler({ - build: require(WEB_BUILD_DIR), - mode: ENVIRONMENT, - getLoadContext(req, res) { - return { - payload: req.payload, - user: req?.user, - res, - }; - }, - }) - ); - - const port = process.env.PORT || 3000; - - app.listen(port, () => { - console.log(`Express server listening on port ${port}`); - }); }); +app.use(payload.authenticate); -function purgeRequireCache() { - // purge require cache on requests for "server side HMR" this won't let - // you have in-memory objects between requests in development, - // alternatively you can set up nodemon/pm2-dev to restart the server on - // file changes, but then you'll have to reconnect to databases/etc on each - // change. We prefer the DX of this, so we've included it for you by default - - for (const key in require.cache) { - if (key.startsWith(WEB_BUILD_DIR)) { - delete require.cache[key]; - } - } +// handle asset requests +if (viteDevServer) { + app.use(viteDevServer.middlewares); +} else { + // Vite fingerprints its assets so we can cache forever. + app.use( + '/assets', + express.static(WEB_BUILD_CLIENT_ASSETS, { + immutable: true, + maxAge: '1y', + }), + ); } + +// Everything else (like favicon.ico) is cached for an hour. You may want to be +// more aggressive with this caching. +app.use(express.static(WEB_BUILD_CLIENT, { maxAge: '1h' })); + +app.use(morgan('tiny')); + +// handle SSR requests +app.all('*', remixHandler); + +const port = process.env.PORT || 3000; +app.listen(port, () => + console.log(`Express server listening at http://localhost:${port}`), +); diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json index 3206d19..2d6250c 100644 --- a/apps/server/tsconfig.json +++ b/apps/server/tsconfig.json @@ -2,7 +2,9 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist", - "baseUrl": "." + "baseUrl": ".", + "module": "ESNext", + "moduleResolution": "Bundler" }, "ts-node": { "transpileOnly": true diff --git a/apps/web/app/components/Blocks/RichText.tsx b/apps/web/app/components/Blocks/RichText.tsx deleted file mode 100644 index 325ec59..0000000 --- a/apps/web/app/components/Blocks/RichText.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import React, { Fragment } from 'react'; -import escapeHTML from 'escape-html'; -import { Text } from 'slate'; - -type RichTextProps = JSX.IntrinsicElements['div'] & { - content: any; -}; - -const RichText = ({ className, content }: RichTextProps) => { - if (!content) { - return null; - } - - return
{text}
;
- }
-
- if (node.italic) {
- text = {text};
- }
-
- if (node.underline) {
- text = (
-
- {text}
-
- );
- }
-
- if (node.strikethrough) {
- text = (
-
- {text}
-
- );
- }
-
- return {serialize(node.children)}- ); - case 'ul': - return
{serialize(node.children)}
; - } - }); - -export { RichText }; diff --git a/apps/web/app/components/Blocks/sections/CallToAction.tsx b/apps/web/app/components/Blocks/sections/CallToAction.tsx index 7187c0f..831f12a 100644 --- a/apps/web/app/components/Blocks/sections/CallToAction.tsx +++ b/apps/web/app/components/Blocks/sections/CallToAction.tsx @@ -1,6 +1,6 @@ import type { Page } from '@org/cms'; import { Link } from '@remix-run/react'; -import { RichText } from '../RichText'; +import { RichText } from '@org/ui'; type CallToActionProps = Page['layout'][0]; @@ -18,7 +18,7 @@ export const CallToAction: React.FC