diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 0e5d22de5d..215aafba63 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -56,6 +56,7 @@ const config = { ], rules: { '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/ban-ts-comment': 0, }, }, { diff --git a/.gitignore b/.gitignore index 99907cc0fd..bf0baa3966 100644 --- a/.gitignore +++ b/.gitignore @@ -72,6 +72,4 @@ docs/_build/ /.tool-versions docs/source/news -.turbo -.parcel-cache/ tsconfig.tsbuildinfo diff --git a/.npmrc b/.npmrc index 71c684383d..bc0f657128 100644 --- a/.npmrc +++ b/.npmrc @@ -3,4 +3,3 @@ public-hoist-pattern[]=*prettier* public-hoist-pattern[]=*stylelint* public-hoist-pattern[]=*cypress* public-hoist-pattern[]=*process* -public-hoist-pattern[]=*parcel* diff --git a/PACKAGES.md b/PACKAGES.md index 12193c73fe..5e12ae1955 100644 --- a/PACKAGES.md +++ b/PACKAGES.md @@ -17,9 +17,9 @@ It's published "as is", so you can import the type definitions from anywhere in ## Core packages -- `@plone/registry` - `@plone/client` - `@plone/components` +- `@plone/registry` ### Rules @@ -28,26 +28,37 @@ Core packages must not depend on any other `@plone/*` package, with only one exc They must be published and bundled in a traditional (transpiled) way. The bundle of these packages must work on both CommonJS and ECMAScript Module (ESM) environments. -## Feature packages - -- `@plone/contents` - ## Utility packages -- `@plone/blocks` -- `@plone/helpers` - `@plone/drivers` +- `@plone/helpers` +- `@plone/providers` - `@plone/rsc` ### Rules Utility packages can depend on core packages and other utility packages. -They must be published in a traditional way, bundled. +They must be published in the traditional way, as a bundle. This bundle must work on both CommonJS and ESM environments. +## Feature packages + +- `@plone/blocks` +- `@plone/contents` +- `@plone/slots` + + +### Rules + +Feature packages, or add-on packages, can depend on any other package. +You must distribute them as source code, and not transpile them. +They must provide a default configuration registry loader as the default main entry point export. +They must be loadable as any other add-on. + + ## Development utility packages These are packages that are not bundled, and they are used in conjunction with Volto core or Volto projects. @@ -55,7 +66,7 @@ They contain utilities that are useful for the development of a Volto project. Some of them are released: - `@plone/scripts` -- `@plone/generator-volto` +- `@plone/generator-volto` (deprecated) Some of them are used by the build, and separated in packages for convenience. diff --git a/RELEASING.md b/RELEASING.md index 55a6def6e7..44e2dac6a0 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -44,13 +44,15 @@ The release process calls `towncrier`. It is a Python library that uses the Python utility `pipx`. This utility allows you to call and execute Python modules without installing them as a prerequisite in your system. It works similar to the NodeJS `npx` utility. -On macOS, you can install `pipx` into your system: + +Install {term}`pipx` for your active Python, and ensure it is on your `$PATH`. +Carefully read the console output for further instructions, if needed. ```shell -brew install pipx +python3 -m pip install pipx +pipx ensurepath ``` -Or follow detailed instructions in the `pipx` documentation for [Installation](https://pypa.github.io/pipx/installation/). ## Running the release process diff --git a/apps/rr7/.eslintrc.cjs b/apps/rr7/.eslintrc.cjs new file mode 100644 index 0000000000..b4a6a65b4d --- /dev/null +++ b/apps/rr7/.eslintrc.cjs @@ -0,0 +1,80 @@ +/** + * 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', + }, + 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/apps/rr7/.gitignore b/apps/rr7/.gitignore new file mode 100644 index 0000000000..f1eb112b25 --- /dev/null +++ b/apps/rr7/.gitignore @@ -0,0 +1,7 @@ +node_modules + +/.cache +/build +.env +.react-router +.registry.loader.js diff --git a/apps/rr7/README.md b/apps/rr7/README.md new file mode 100644 index 0000000000..d6b9daf12c --- /dev/null +++ b/apps/rr7/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/apps/rr7/app/client.ts b/apps/rr7/app/client.ts new file mode 100644 index 0000000000..0eec9cd62e --- /dev/null +++ b/apps/rr7/app/client.ts @@ -0,0 +1,8 @@ +import ploneClient from '@plone/client'; +import config from '@plone/registry'; + +const cli = ploneClient.initialize({ + apiPath: config.settings.apiPath, +}); + +export { cli as ploneClient }; diff --git a/apps/rr7/app/config.ts b/apps/rr7/app/config.ts new file mode 100644 index 0000000000..e7133efdce --- /dev/null +++ b/apps/rr7/app/config.ts @@ -0,0 +1,15 @@ +import config from '@plone/registry'; +import { blocksConfig, slate } from '@plone/blocks'; + +const settings = { + apiPath: 'http://localhost:3000', + slate, +}; + +// @ts-expect-error We need to fix typing +config.set('settings', settings); + +// @ts-expect-error We need to fix typing +config.set('blocks', { blocksConfig }); + +export default config; diff --git a/apps/rr7/app/root.tsx b/apps/rr7/app/root.tsx new file mode 100644 index 0000000000..50808c5b17 --- /dev/null +++ b/apps/rr7/app/root.tsx @@ -0,0 +1,98 @@ +import { useState } from 'react'; +import { + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, + useHref, + useLocation, + useNavigate, + useParams, +} from 'react-router'; +import type { LinksFunction } 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 './config'; + +import '@plone/components/dist/basic.css'; + +function useHrefLocal(to: string) { + return useHref(flattenToAppURL(to)); +} + +export const links: LinksFunction = () => [ + { 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=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap', + }, +]; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {children} + + + + + ); +} + +export default function App() { + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { + // With SSR, we usually want to set some default staleTime + // above 0 to avoid refetching immediately on the client + staleTime: 60 * 1000, + }, + }, + }), + ); + + const [ploneClient] = useState(() => + PloneClient.initialize({ + apiPath: config.settings.apiPath, + }), + ); + + const RRNavigate = useNavigate(); + const navigate = (to: string) => { + return RRNavigate(flattenToAppURL(to)); + }; + + return ( + + + + + ); +} diff --git a/apps/rr7/app/routes.ts b/apps/rr7/app/routes.ts new file mode 100644 index 0000000000..579d64cd84 --- /dev/null +++ b/apps/rr7/app/routes.ts @@ -0,0 +1,7 @@ +import type { RouteConfig } from '@react-router/dev/routes'; +import { index, route } from '@react-router/dev/routes'; + +export const routes: RouteConfig = [ + index('routes/home.tsx'), + route('*', 'routes/$.tsx'), +]; diff --git a/apps/rr7/app/routes/$.tsx b/apps/rr7/app/routes/$.tsx new file mode 100644 index 0000000000..5216c4d188 --- /dev/null +++ b/apps/rr7/app/routes/$.tsx @@ -0,0 +1,2 @@ +import Content, { loader } from './home'; +export { loader, Content as default }; diff --git a/apps/rr7/app/routes/home.tsx b/apps/rr7/app/routes/home.tsx new file mode 100644 index 0000000000..c470ad9f42 --- /dev/null +++ b/apps/rr7/app/routes/home.tsx @@ -0,0 +1,79 @@ +import type { LoaderArgs } from '../routes/+types.home'; +import { + dehydrate, + QueryClient, + HydrationBoundary, + useQuery, + useQueryClient, +} from '@tanstack/react-query'; +import { flattenToAppURL } from '../utils'; +import { useLoaderData, useLocation } from 'react-router'; +import { usePloneClient } from '@plone/providers'; +import { Breadcrumbs, RenderBlocks } from '@plone/components'; +import config from '@plone/registry'; +import { ploneClient } from '../client'; + +import type { MetaFunction } from 'react-router'; + +export const meta: MetaFunction = () => { + return [ + { title: 'Plone on React Router 7' }, + { name: 'description', content: 'Welcome to Plone!' }, + ]; +}; + +const expand = ['breadcrumbs', 'navigation']; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export async function loader({ params, request }: LoaderArgs) { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + // With SSR, we usually want to set some default staleTime + // above 0 to avoid refetching immediately on the client + staleTime: 60 * 1000, + }, + }, + }); + + const { getContentQuery } = ploneClient; + + await queryClient.prefetchQuery( + getContentQuery({ path: flattenToAppURL(request.url), expand }), + ); + + return { dehydratedState: dehydrate(queryClient) }; +} + +function Page() { + const { getContentQuery } = usePloneClient(); + const pathname = useLocation().pathname; + const { data } = useQuery(getContentQuery({ path: pathname, expand })); + + if (!data) return 'Loading...'; + return ( + <> + + + + ); +} + +export default function Content() { + const { dehydratedState } = useLoaderData(); + const queryClient = useQueryClient(); + + return ( + + + + ); +} diff --git a/apps/rr7/app/utils.ts b/apps/rr7/app/utils.ts new file mode 100644 index 0000000000..c297613f90 --- /dev/null +++ b/apps/rr7/app/utils.ts @@ -0,0 +1,17 @@ +import config from './config'; + +/** + * Flatten to app server URL - Given a URL if it starts with the API server URL + * this method flattens it (removes) the server part + * TODO: Update it when implementing non-root based app location (on a + * directory other than /, eg. /myapp) + * @method flattenToAppURL + */ +export function flattenToAppURL(url: string) { + const { settings } = config; + + return ( + url && + url.replace(settings.apiPath, '').replace('http://localhost:3000', '') + ); +} diff --git a/apps/rr7/package.json b/apps/rr7/package.json new file mode 100644 index 0000000000..fa204ea503 --- /dev/null +++ b/apps/rr7/package.json @@ -0,0 +1,39 @@ +{ + "name": "plone-rr7", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev": "react-router dev", + "build": "react-router build", + "start:prod": "react-router-serve ./build/server/index.js", + "typecheck": "react-router typegen && tsc", + "typegen": "react-router typegen" + }, + "dependencies": { + "@react-router/node": "7.0.0-pre.4", + "@react-router/serve": "7.0.0-pre.4", + "@tanstack/react-query": "^5.59.0", + "@tanstack/react-query-devtools": "^5.59.0", + "isbot": "^5.1.17", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router": "7.0.0-pre.4" + }, + "devDependencies": { + "@plone/blocks": "workspace:*", + "@plone/client": "workspace:*", + "@plone/components": "workspace:*", + "@plone/providers": "workspace:*", + "@plone/registry": "workspace:*", + "@react-router/dev": "7.0.0-pre.4", + "@types/react": "^18.3.9", + "@types/react-dom": "^18.3.0", + "typescript": "^5.6.3", + "vite": "^5.4.9", + "vite-tsconfig-paths": "^5.0.1" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/apps/rr7/public/favicon.ico b/apps/rr7/public/favicon.ico new file mode 100644 index 0000000000..5dbdfcddcb Binary files /dev/null and b/apps/rr7/public/favicon.ico differ diff --git a/apps/rr7/tsconfig.json b/apps/rr7/tsconfig.json new file mode 100644 index 0000000000..29b2316386 --- /dev/null +++ b/apps/rr7/tsconfig.json @@ -0,0 +1,33 @@ +{ + "include": [ + "**/*.ts", + "**/*.tsx", + "**/.server/**/*.ts", + "**/.server/**/*.tsx", + "**/.client/**/*.ts", + "**/.client/**/*.tsx", + ".react-router/types/**/*" + ], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["@react-router/node", "vite/client"], + "isolatedModules": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "target": "ES2022", + "strict": true, + "allowJs": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./app/*"] + }, + "noEmit": true, + "rootDirs": [".", "./.react-router/types"], + "plugins": [{ "name": "@react-router/dev" }] + } +} diff --git a/apps/rr7/vite.config.ts b/apps/rr7/vite.config.ts new file mode 100644 index 0000000000..723e0323af --- /dev/null +++ b/apps/rr7/vite.config.ts @@ -0,0 +1,28 @@ +import { reactRouter } from '@react-router/dev/vite'; +import tsconfigPaths from 'vite-tsconfig-paths'; +import { defineConfig } from 'vite'; +import { PloneRegistryVitePlugin } from '@plone/registry/vite-plugin'; + +export default defineConfig({ + plugins: [ + reactRouter({ + // Server-side render by default, to enable SPA mode set this to `false` + ssr: true, + }), + tsconfigPaths(), + PloneRegistryVitePlugin(), + ], + server: { + port: 3000, + proxy: { + '^/\\+\\+api\\+\\+($$|/.*)': { + target: + 'http://localhost:8080/VirtualHostBase/http/localhost:3000/Plone/++api++/VirtualHostRoot', + rewrite: (path) => { + console.log(path); + return path.replace('/++api++', ''); + }, + }, + }, + }, +}); diff --git a/docs/source/contributing/branch-policy.md b/docs/source/_inc/_branch-policy.md similarity index 100% rename from docs/source/contributing/branch-policy.md rename to docs/source/_inc/_branch-policy.md diff --git a/docs/source/contributing/install-docker.md b/docs/source/_inc/_install-docker.md similarity index 100% rename from docs/source/contributing/install-docker.md rename to docs/source/_inc/_install-docker.md diff --git a/docs/source/contributing/install-git.md b/docs/source/_inc/_install-git.md similarity index 100% rename from docs/source/contributing/install-git.md rename to docs/source/_inc/_install-git.md diff --git a/docs/source/contributing/install-make.md b/docs/source/_inc/_install-make.md similarity index 100% rename from docs/source/contributing/install-make.md rename to docs/source/_inc/_install-make.md diff --git a/docs/source/contributing/install-nodejs.md b/docs/source/_inc/_install-nodejs.md similarity index 100% rename from docs/source/contributing/install-nodejs.md rename to docs/source/_inc/_install-nodejs.md diff --git a/docs/source/contributing/install-nvm.md b/docs/source/_inc/_install-nvm.md similarity index 100% rename from docs/source/contributing/install-nvm.md rename to docs/source/_inc/_install-nvm.md diff --git a/docs/source/contributing/install-operating-system.md b/docs/source/_inc/_install-operating-system.md similarity index 100% rename from docs/source/contributing/install-operating-system.md rename to docs/source/_inc/_install-operating-system.md diff --git a/docs/source/_static/plone-home-page.png b/docs/source/_static/plone-home-page.png new file mode 100644 index 0000000000..b2321e0eaf Binary files /dev/null and b/docs/source/_static/plone-home-page.png differ diff --git a/docs/source/_static/searchtools.js b/docs/source/_static/searchtools.js deleted file mode 100644 index 23ed8bf5a2..0000000000 --- a/docs/source/_static/searchtools.js +++ /dev/null @@ -1,553 +0,0 @@ -/* - * searchtools.js - * ~~~~~~~~~~~~~~~~ - * - * Sphinx JavaScript utilities for the full-text search. - * - * :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - - -if (!Scorer) { - /** - * Simple result scoring code. - */ - var Scorer = { - // Implement the following function to further tweak the score for each result - // The function takes a result array [filename, title, anchor, descr, score] - // and returns the new score. - /* - score: function(result) { - return result[4]; - }, - */ - - // query matches the full name of an object - objNameMatch: 11, - // or matches in the last dotted part of the object name - objPartialMatch: 6, - // Additive scores depending on the priority of the object - objPrio: {0: 15, // used to be importantResults - 1: 5, // used to be objectResults - 2: -5}, // used to be unimportantResults - // Used when the priority is not in the mapping. - objPrioDefault: 0, - - // query found in title - title: 15, - partialTitle: 7, - // query found in terms - term: 5, - partialTerm: 2 - }; -} - -if (!splitQuery) { - function splitQuery(query) { - return query.split(/\s+/); - } -} - -/** - * Search Module - */ -var Search = { - - _index : null, - _queued_query : null, - _pulse_status : -1, - - htmlToText : function(htmlString) { - var virtualDocument = document.implementation.createHTMLDocument('virtual'); - var htmlElement = $(htmlString, virtualDocument); - htmlElement.find('.headerlink').remove(); - docContent = htmlElement.find('[role=main]')[0]; - if(docContent === undefined) { - console.warn("Content block not found. Sphinx search tries to obtain it " + - "via '[role=main]'. Could you check your theme or template."); - return ""; - } - return docContent.textContent || docContent.innerText; - }, - - init : function() { - var params = $.getQueryParameters(); - if (params.q) { - var query = params.q[0]; - $('input[name="q"]')[0].value = query; - $('input[name="q"]')[1].value = query; - if (params.doc_section) { - var doc_section = params.doc_section[0]; - $('select[name="doc_section"]')[0].value = doc_section; - } - this.performSearch(query, doc_section); - } - }, - - loadIndex : function(url) { - $.ajax({type: "GET", url: url, data: null, - dataType: "script", cache: true, - complete: function(jqxhr, textstatus) { - if (textstatus != "success") { - document.getElementById("searchindexloader").src = url; - } - }}); - }, - - setIndex : function(index) { - var q; - this._index = index; - if ((q = this._queued_query) !== null) { - this._queued_query = null; - Search.query(q); - } - }, - - hasIndex : function() { - return this._index !== null; - }, - - deferQuery : function(query) { - this._queued_query = query; - }, - - stopPulse : function() { - this._pulse_status = 0; - }, - - startPulse : function() { - if (this._pulse_status >= 0) - return; - function pulse() { - var i; - Search._pulse_status = (Search._pulse_status + 1) % 4; - var dotString = ''; - for (i = 0; i < Search._pulse_status; i++) - dotString += '.'; - Search.dots.text(dotString); - if (Search._pulse_status > -1) - window.setTimeout(pulse, 500); - } - pulse(); - }, - - /** - * perform a search for something (or wait until index is loaded) - */ - performSearch : function(query, doc_section) { - // create the required interface elements - this.out = $('#search-results'); - this.title = $('').appendTo(this.out); - this.dots = $('').appendTo(this.title); - this.status = $('

 

').appendTo(this.out); - this.output = $('