diff --git a/packages/revolto/.eslintrc.cjs b/packages/revolto/.eslintrc.cjs new file mode 100644 index 0000000000..d1480fca9f --- /dev/null +++ b/packages/revolto/.eslintrc.cjs @@ -0,0 +1,81 @@ +/** + * This is intended to be a basic starting point for linting in your app. + * It relies on recommended configs out of the box for simplicity, but you can + * and should modify this configuration to best suit your team's needs. + */ + +/** @type {import('eslint').Linter.Config} */ +module.exports = { + root: true, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + }, + env: { + browser: true, + commonjs: true, + es6: true, + }, + + // Base config + extends: ['eslint:recommended'], + + overrides: [ + // React + { + files: ['**/*.{js,jsx,ts,tsx}'], + plugins: ['react', 'jsx-a11y'], + extends: [ + 'plugin:react/recommended', + 'plugin:react/jsx-runtime', + 'plugin:react-hooks/recommended', + 'plugin:jsx-a11y/recommended', + ], + settings: { + react: { + version: 'detect', + }, + 'import/core-modules': ['@plone/registry/addons-loader'], + formComponents: ['Form'], + linkComponents: [ + { name: 'Link', linkAttribute: 'to' }, + { name: 'NavLink', linkAttribute: 'to' }, + ], + }, + }, + + // Typescript + { + files: ['**/*.{ts,tsx}'], + plugins: ['@typescript-eslint', 'import'], + parser: '@typescript-eslint/parser', + settings: { + 'import/internal-regex': '^~/', + 'import/resolver': { + node: { + extensions: ['.ts', '.tsx'], + }, + typescript: { + alwaysTryTypes: true, + }, + }, + }, + extends: [ + 'plugin:@typescript-eslint/recommended', + 'plugin:import/recommended', + 'plugin:import/typescript', + ], + }, + + // Node + { + files: ['.eslintrc.js'], + env: { + node: true, + }, + }, + ], +}; diff --git a/packages/revolto/.gitignore b/packages/revolto/.gitignore new file mode 100644 index 0000000000..f1eb112b25 --- /dev/null +++ b/packages/revolto/.gitignore @@ -0,0 +1,7 @@ +node_modules + +/.cache +/build +.env +.react-router +.registry.loader.js diff --git a/packages/revolto/README.md b/packages/revolto/README.md new file mode 100644 index 0000000000..d6b9daf12c --- /dev/null +++ b/packages/revolto/README.md @@ -0,0 +1,30 @@ +# Plone on React Router 7 + +This is a proof of concept of a [React Router](https://reactrouter.com/dev/docs) app, using the `@plone/*` libraries. +This is intended to serve as both a playground for the development of both packages and as a demo of Plone using Remix. + +> [!WARNING] +> This package or app is experimental. +> The community offers no support whatsoever for it. +> Breaking changes may occur without notice. + +## Development + +To start, from the root of the monorepo, issue the following commands. + +```shell +pnpm install +pnpm --filter plone-remix run dev +``` + +Then start the Plone backend. + +% TODO MAKEFILE +```shell +make backend-docker-start +``` + + +## About this app + +- [Remix Docs](https://remix.run/docs/en/main) diff --git a/packages/revolto/app/config.server.ts b/packages/revolto/app/config.server.ts new file mode 100644 index 0000000000..2d7c951f7d --- /dev/null +++ b/packages/revolto/app/config.server.ts @@ -0,0 +1,32 @@ +/** + * This is the server side config entry point + */ +import config from '@plone/registry'; +import ploneClient from '@plone/client'; +import applyAddonConfiguration from '@plone/registry/addons-loader'; + +export default function install() { + applyAddonConfiguration(config); + + config.settings.apiPath = + process.env.PLONE_API_PATH || 'http://localhost:3000'; + config.settings.internalApiPath = + process.env.PLONE_INTERNAL_API_PATH || undefined; + + const cli = ploneClient.initialize({ + apiPath: config.settings.internalApiPath || config.settings.apiPath, + }); + + config.registerUtility({ + name: 'ploneClient', + type: 'client', + method: () => cli, + }); + + console.log('API_PATH is:', config.settings.apiPath); + console.log( + 'INTERNAL_API_PATH is:', + config.settings.internalApiPath || 'not set', + ); + return config; +} diff --git a/packages/revolto/app/config.ts b/packages/revolto/app/config.ts new file mode 100644 index 0000000000..7f2026e538 --- /dev/null +++ b/packages/revolto/app/config.ts @@ -0,0 +1,11 @@ +/** + * This is the client side config entry point + */ +import config from '@plone/registry'; +import applyAddonConfiguration from '@plone/registry/addons-loader'; + +export default function install() { + applyAddonConfiguration(config); + config.settings.apiPath = 'http://localhost:3000'; + return config; +} diff --git a/packages/revolto/app/content.tsx b/packages/revolto/app/content.tsx new file mode 100644 index 0000000000..cfc6134824 --- /dev/null +++ b/packages/revolto/app/content.tsx @@ -0,0 +1,57 @@ +import type { Route } from './+types/content'; +import { data, useLoaderData, useLocation } from 'react-router'; +import PloneClient from '@plone/client'; +import App from '@plone/slots/components/App'; +import config from '@plone/registry'; + +export const meta: Route.MetaFunction = ({ data }) => { + return [ + { title: data?.title }, + { name: 'description', content: data?.description }, + ]; +}; + +const expand = ['navroot', 'breadcrumbs', 'navigation']; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export async function loader({ params, request }: Route.LoaderArgs) { + const ploneClient = config + .getUtility({ + name: 'ploneClient', + type: 'client', + }) + .method(); + + const { getContent } = ploneClient as PloneClient; + + const path = new URL(request.url).pathname; + + if ( + !( + /^https?:\/\//.test(path) || + /^favicon.ico\/\//.test(path) || + /expand/.test(path) || + /\/@@images\//.test(path) || + /\/@@download\//.test(path) || + /^\/assets/.test(path) || + /\.(css|css\.map)$/.test(path) + ) + ) { + console.log('prefetching', path); + try { + return await getContent({ path, expand }); + } catch (error) { + throw data('Content Not Found', { status: 404 }); + } + } else { + console.log('path not prefetched', path); + throw data('Content Not Found', { status: 404 }); + } +} + +export default function Content() { + const data = useLoaderData(); + const pathname = useLocation().pathname; + + return ; +} diff --git a/packages/revolto/app/okroute.tsx b/packages/revolto/app/okroute.tsx new file mode 100644 index 0000000000..56472e1bb6 --- /dev/null +++ b/packages/revolto/app/okroute.tsx @@ -0,0 +1,5 @@ +export async function loader() { + return new Response(null, { + status: 200, + }); +} diff --git a/packages/revolto/app/root.tsx b/packages/revolto/app/root.tsx new file mode 100644 index 0000000000..7340c7627a --- /dev/null +++ b/packages/revolto/app/root.tsx @@ -0,0 +1,161 @@ +import type { LinksFunction } from 'react-router'; +import type { Route } from './+types/root'; +import { useState } from 'react'; +import { + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, + useHref, + useLocation, + useNavigate as useRRNavigate, + useParams, + useLoaderData, + isRouteErrorResponse, +} from 'react-router'; + +import { QueryClient } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import PloneClient from '@plone/client'; +import { PloneProvider } from '@plone/providers'; +import { flattenToAppURL } from './utils'; +import config from '@plone/registry'; +import install from './config'; +import installSSR from './config.server'; + +install(); + +import themingMain from '@plone/theming/styles/main.css?url'; +import slotsMain from '@plone/slots/main.css?url'; + +function useNavigate() { + const navigate = useRRNavigate(); + return (to: string) => navigate(flattenToAppURL(to)); +} + +function useHrefLocal(to: string) { + return useHref(flattenToAppURL(to)); +} + +export const links: LinksFunction = () => [ + { rel: 'stylesheet', href: themingMain }, + { rel: 'stylesheet', href: slotsMain }, + { rel: 'preconnect', href: 'https://fonts.googleapis.com' }, + { + rel: 'preconnect', + href: 'https://fonts.gstatic.com', + crossOrigin: 'anonymous', + }, + { + rel: 'stylesheet', + href: 'https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap', + }, +]; + +export async function loader() { + const ssrConfig = installSSR(); + + return { + env: { + PLONE_API_PATH: ssrConfig.settings.apiPath, + PLONE_INTERNAL_API_PATH: ssrConfig.settings.internalApiPath, + }, + }; +} + +export function Layout({ children }: { children: React.ReactNode }) { + const data = useLoaderData(); + + return ( + + + + + + + + + {children} + +