diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml index 82480849c7..f4637cded8 100644 --- a/.github/workflows/acceptance.yml +++ b/.github/workflows/acceptance.yml @@ -691,3 +691,51 @@ jobs: with: name: cypress-videos path: packages/volto/cypress/videos + + seven: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + runs-on: ubuntu-latest + name: Seven + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + node-version: [22.x] + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js environment + uses: ./.github/actions/node_env_setup + with: + node-version: ${{ matrix.node-version }} + + - name: Cypress acceptance tests + uses: cypress-io/github-action@v6 + env: + BABEL_ENV: production + CYPRESS_RETRIES: 2 + # Recommended: pass the GitHub token lets this action correctly + # determine the unique run id necessary to re-run the checks + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + install: false + working-directory: packages/seven + browser: chrome + spec: cypress/tests/core/basic/**/*.cy.ts + start: | + make ci-acceptance-backend-start + make acceptance-frontend-prod-start + wait-on: 'npx wait-on --httpTimeout 20000 http-get://127.0.0.1:55001/plone http://127.0.0.1:3000' + + # Upload Cypress screenshots + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-screenshots + path: packages/seven/cypress/screenshots + # Upload Cypress videos + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-videos + path: packages/seven/cypress/videos diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index b9616ac57d..db5a5e8d93 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -48,6 +48,8 @@ jobs: - 'packages/slots/**' theming: - 'packages/theming/**' + seven: + - 'packages/seven/**' wrongNews: - added|modified: 'news/**' @@ -150,6 +152,14 @@ jobs: env: BASE_BRANCH: ${{ github.base_ref }} + - name: seven changelog check + if: steps.filter.outputs.seven == 'true' + run: | + git fetch --no-tags origin main + towncrier check --compare-with origin/main --dir packages/seven + env: + BASE_BRANCH: ${{ github.base_ref }} + - name: Wrong location of news changelog check if: steps.filter.outputs.wrongNews == 'true' run: echo "News items should be moved from the repository root to the appropriate package root in `packages/package-name`." && exit 1 diff --git a/.github/workflows/cookieplone.yml b/.github/workflows/cookieplone.yml new file mode 100644 index 0000000000..a9d52a957c --- /dev/null +++ b/.github/workflows/cookieplone.yml @@ -0,0 +1,66 @@ +name: Cookieplone +on: [push, pull_request] + +env: + PYTHON_VERSION: "3.13" + +jobs: + seven: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + runs-on: ubuntu-latest + name: Seven Cookieplone + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + node-version: [22.x] + steps: + - uses: actions/checkout@v4 + - run: echo "Current branch is ${GITHUB_REF#refs/heads/}" + - name: Set up Node.js environment + uses: ./.github/actions/node_env_setup + with: + node-version: ${{ matrix.node-version }} + + - name: Generate Cookieplone-based frontend addon + run: | + pipx install cookieplone + COOKIEPLONE_REPOSITORY_TAG=seventemplate pipx run --no-cache cookieplone seven_addon --no-input + + - name: Install generated package + working-directory: seven-add-on + run: | + pnpm dlx mrs-developer missdev --no-config --fetch-https + (cd core && git fetch --depth 1 origin ${GITHUB_REF#refs/heads/}:${GITHUB_REF#refs/heads/} && git checkout ${GITHUB_REF#refs/heads/}) + pnpm i + + - name: Cypress acceptance tests + uses: cypress-io/github-action@v6 + env: + BABEL_ENV: production + CYPRESS_RETRIES: 2 + # Recommended: pass the GitHub token lets this action correctly + # determine the unique run id necessary to re-run the checks + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + install: false + working-directory: seven-add-on/core/packages/seven + browser: chrome + spec: cypress/tests/core/basic/**/*.cy.ts + start: | + make ci-acceptance-backend-start + make project-acceptance-frontend-prod-start + wait-on: 'npx wait-on --httpTimeout 20000 http-get://127.0.0.1:55001/plone http://127.0.0.1:3000' + + # Upload Cypress screenshots + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-screenshots + path: packages/seven/cypress/screenshots + # Upload Cypress videos + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: cypress-videos + path: packages/seven/cypress/videos diff --git a/.lintstagedrc b/.lintstagedrc deleted file mode 100644 index e2d1cf3190..0000000000 --- a/.lintstagedrc +++ /dev/null @@ -1,18 +0,0 @@ -{ - "packages/!(volto)/**/*.{js,jsx,ts,tsx}": [ - "pnpm eslint --max-warnings=0 --fix", - "pnpm prettier --single-quote --write" - ], - "packages/volto/**/*.{js,jsx,ts,tsx}": [ - "pnpm --filter @plone/volto lint:husky", - "pnpm --filter @plone/volto prettier:husky" - ], - "packages/volto/src/**/*.{jsx, tsx}": ["pnpm --filter @plone/volto i18n"], - "packages/!(volto)/**/*.{css,less,scss}": ["pnpm stylelint --fix"], - "packages/volto/**/*.{css,less,scss}": [ - "pnpm --filter @plone/volto stylelint --fix" - ], - "packages/volto/**/*.overrides": [ - "pnpm --filter @plone/volto stylelint --fix" - ] -} diff --git a/.readthedocs.yaml b/.readthedocs.yaml index e2295290f3..2f8365e7cf 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -11,27 +11,25 @@ build: tools: python: "3.12" nodejs: "20" - jobs: - post_checkout: - # Cancel building pull requests when there aren't changes in the docs directory or YAML file. - # You can add any other files or directories that you'd like here as well, - # like your docs requirements file, or other files that will change your docs build. - # - # If there are no changes (git diff exits with 0) we force the command to return with 183. - # This is a special exit code on Read the Docs that will cancel the build immediately. - - | - if [ "$READTHEDOCS_VERSION_TYPE" = "external" ] && git diff --quiet origin/main -- docs/ .readthedocs.yaml requirements-docs.txt packages/volto/.storybook; - then - exit 183; - fi - post_install: - # Install dependencies defined in your ``package.json`` - # - npm ci - # Install any other extra dependencies to build the docs - - corepack enable - - corepack prepare pnpm@* --activate - - asdf reshim nodejs - - pnpm install - - pnpm build:registry - - (cd packages/volto && pnpm build-storybook -o ${READTHEDOCS_OUTPUT}/html/storybook) - - make docs-rtd-pr-preview + commands: + # Cancel building pull requests when there aren't changes in the docs directory or YAML file. + # You can add any other files or directories that you'd like here as well, + # like your docs requirements file, or other files that will change your docs build. + # + # If there are no changes (git diff exits with 0) we force the command to return with 183. + # This is a special exit code on Read the Docs that will cancel the build immediately. + - | + if [ "$READTHEDOCS_VERSION_TYPE" = "external" ] && git diff --quiet origin/main -- docs/ .readthedocs.yaml requirements-docs.txt packages/volto/.storybook; + then + exit 183; + fi + # Install dependencies defined in your ``package.json`` + # - npm ci + # Install any other extra dependencies to build the docs + - corepack enable + - corepack prepare pnpm@* --activate + - asdf reshim nodejs + - pnpm install + - pnpm build:registry + - (cd packages/volto && pnpm build-storybook -o ${READTHEDOCS_OUTPUT}/html/storybook) + - make docs-rtd-pr-preview diff --git a/Makefile b/Makefile index 72656906b6..b689fac12c 100644 --- a/Makefile +++ b/Makefile @@ -145,9 +145,24 @@ packages/registry/dist: $(shell find packages/registry/src -type f) packages/components/dist: $(shell find packages/components/src -type f) pnpm build:components +packages/client/dist: $(shell find packages/client/src -type f) + pnpm build:client + +packages/providers/dist: $(shell find packages/providers/src -type f) + pnpm build:providers + +packages/helpers/dist: $(shell find packages/helpers/src -type f) + pnpm build:helpers + +packages/react-router/dist: $(shell find packages/react-router/src -type f) + pnpm build:react-router + .PHONY: build-deps build-deps: packages/registry/dist ## Build dependencies +.PHONY: build-all-deps +build-all-deps: packages/registry/dist packages/components/dist packages/client/dist packages/providers/dist packages/react-router/dist packages/helpers/dist ## Build all dependencies + .PHONY: i18n i18n: ## Converts your po files into json to translate volto frontend $(MAKE) -C "./packages/volto/" i18n @@ -378,6 +393,28 @@ acceptance-server-detached-start: ## Starts test acceptance server main fixture acceptance-server-detached-stop: ## Stop test acceptance server main fixture in detached mode (daemon) docker kill plone-client-acceptance-server +######### Seven acceptance tests + +.PHONY: seven-acceptance-frontend-dev-start +seven-acceptance-frontend-dev-start: ## Start acceptance frontend in development mode for Seven + $(MAKE) -C "./packages/seven/" acceptance-frontend-dev-start + +.PHONY: seven-acceptance-frontend-prod-start +seven-acceptance-frontend-prod-start:: ## Start acceptance frontend in production mode for Seven + $(MAKE) -C "./packages/seven/" acceptance-frontend-prod-start + +.PHONY: seven-acceptance-test +seven-acceptance-test: ## Start Cypress in interactive mode for Seven + $(MAKE) -C "./packages/seven/" acceptance-test + +.PHONY: seven-ci-acceptance-test +seven-ci-acceptance-test: ## Run cypress tests in headless mode for CI for Seven + $(MAKE) -C "./packages/seven/" ci-acceptance-test + +.PHONY: seven-ci-acceptance-test-run-all +seven-ci-acceptance-test-run-all: ## With a single command, start both the acceptance frontend and backend acceptance server, and run Cypress tests in headless mode for Seven + $(MAKE) -C "./packages/seven/" ci-acceptance-test-run-all + # include local overrides if present -include Makefile.local -include ../../../Makefile.local diff --git a/PACKAGES.md b/PACKAGES.md index 5f72ef477c..51de2c3eac 100644 --- a/PACKAGES.md +++ b/PACKAGES.md @@ -15,9 +15,8 @@ and as a development dependency: Plone 6.0.x (Volto 17 and below) does not use any of them. These packages are expected to be used and become part of Plone 7. -Some of them might become part of Plone 6.1.x minor versions. -The packages are divided into three categories or types: +These packages are divided into three categories or types: - core - utilities @@ -53,6 +52,7 @@ The bundle of these packages must work on both CommonJS and ECMAScript Module (E - `@plone/providers` - `@plone/helpers` +- `@plone/react-router` ### Rules @@ -67,6 +67,7 @@ This bundle must work on both CommonJS and ESM environments. - `@plone/blocks` - `@plone/slots` - `@plone/theming` +- `@plone/cmsui` - `@plone/contents` @@ -94,7 +95,7 @@ Some of them are used by the build, and separated in packages for convenience. - `tsconfig` -## Volto add-ons packages +## Volto add-on packages These Volto add-ons are packages used by Volto core. They are always loaded, so they are also called "core packages". diff --git a/docs/source/release-notes/index.md b/docs/source/release-notes/index.md index 3eab10e849..5b1b29e49e 100644 --- a/docs/source/release-notes/index.md +++ b/docs/source/release-notes/index.md @@ -17,6 +17,29 @@ myst: +## 18.7.0 (2025-01-24) + +### Feature + +- - Fixed handling of the site logo preview to appear after upload. @Shyam-Raghuwanshi [#6591](https://github.com/plone/volto/issues/6591) +- Provide language alternate links @erral [#6602](https://github.com/plone/volto/issues/6602) +- feat(cypress):Add custom check Accessibility command @Tishasoumya-02 [#6606](https://github.com/plone/volto/issues/6606) + +### Bugfix + +- Improve the usability of the `ObjectBrowser` when inputting a manual value, checking it on blur, and adding a local validator. @sneridagh [#6576](https://github.com/plone/volto/issues/6576) +- fix(useClipboard): Do not have a pending promise in a boolean state @nileshgulia1 [#6585](https://github.com/plone/volto/issues/6585) + +### Internal + +- Add Seven convenience Makefile commands. @sneridagh [#6599](https://github.com/plone/volto/issues/6599) +- Restore pull request previews on Read the Docs. @stevepiercy [#6612](https://github.com/plone/volto/issues/6612) +- Fix lint-staged throwing warnings when a file is checked-in and ignored. @sneridagh [#6614](https://github.com/plone/volto/issues/6614) + +### Documentation + +- Enhancements of the upgrade guide for Volto 18, since we detected some inconsistencies. @sneridagh [#6609](https://github.com/plone/volto/issues/6609) + ## 18.6.0 (2025-01-11) ### Feature diff --git a/lint-staged.config.js b/lint-staged.config.js new file mode 100644 index 0000000000..1b1d2ec7ae --- /dev/null +++ b/lint-staged.config.js @@ -0,0 +1,31 @@ +const { ESLint } = require('eslint'); + +const removeIgnoredFiles = async (files) => { + const eslint = new ESLint(); + const ignoredFiles = await Promise.all( + files.map((file) => eslint.isPathIgnored(file)), + ); + const filteredFiles = files.filter((_, i) => !ignoredFiles[i]); + return filteredFiles.join(' '); +}; +module.exports = { + 'packages/!(volto)/**/*.{js,jsx,ts,tsx}': async (files) => { + const filesToLint = await removeIgnoredFiles(files); + return [ + `eslint --max-warnings=0 ${filesToLint}`, + 'pnpm prettier --single-quote --write', + ]; + }, + 'packages/volto/**/*.{js,jsx,ts,tsx}': [ + 'pnpm --filter @plone/volto lint:husky', + 'pnpm --filter @plone/volto prettier:husky', + ], + 'packages/volto/src/**/*.{jsx, tsx}': ['pnpm --filter @plone/volto i18n'], + 'packages/!(volto)/**/*.{css,less,scss}': ['pnpm stylelint --fix'], + 'packages/volto/**/*.{css,less,scss}': [ + 'pnpm --filter @plone/volto stylelint --fix', + ], + 'packages/volto/**/*.overrides': [ + 'pnpm --filter @plone/volto stylelint --fix', + ], +}; diff --git a/package.json b/package.json index 1cf8ebd0e3..756e6d20a0 100644 --- a/package.json +++ b/package.json @@ -5,14 +5,18 @@ "preinstall": "npx only-allow pnpm", "watch": "pnpm --filter @plone/registry --filter @plone/client --filter @plone/components --filter @plone/providers watch", "build:deps": "pnpm --filter @plone/registry build", - "build:all": "pnpm --filter @plone/registry --filter @plone/client --filter @plone/components --filter @plone/providers build", - "build:all:force": "pnpm --filter @plone/registry --filter @plone/client --filter @plone/components --filter @plone/providers build:force", + "build:all": "pnpm --filter @plone/registry --filter @plone/client --filter @plone/components --filter @plone/providers --filter @plone/react-router build", + "build:all:force": "pnpm --filter @plone/registry --filter @plone/client --filter @plone/components --filter @plone/providers --filter @plone/react-router build:force", "build:registry": "pnpm --filter @plone/registry run build", "build:components": "pnpm --filter @plone/components run build", + "build:client": "pnpm --filter @plone/client run build", + "build:providers": "pnpm --filter @plone/providers run build", + "build:helpers": "pnpm --filter @plone/helpers run build", + "build:react-router": "pnpm --filter @plone/react-router run build", "build": "pnpm --filter @plone/volto build", "start": "pnpm --filter @plone/volto start", "start:project": "pnpm --filter plone run start", - "lint": "pnpm build:all && eslint --max-warnings=0 '{apps,packages}/**/*.{js,jsx,ts,tsx}'", + "lint": "make build-all-deps && eslint --max-warnings=0 '{apps,packages}/**/*.{js,jsx,ts,tsx}'", "lint:volto": "pnpm --filter @plone/volto run lint", "test": "pnpm --filter @plone/volto run test", "test:ci": "pnpm --filter @plone/volto run test:ci", diff --git a/packages/blocks/.eslintrc.cjs b/packages/blocks/.eslintrc.cjs index 42cb63d1af..18bae08069 100644 --- a/packages/blocks/.eslintrc.cjs +++ b/packages/blocks/.eslintrc.cjs @@ -1,5 +1,5 @@ /** @type {import('eslint').Linter.Config} */ module.exports = { - extends: ['../../.eslintrc.cjs', 'plugin:react/jsx-runtime'], - ignorePatterns: ['storybook-static', 'dist'], + extends: ['../../.eslintrc.cjs', '../eslintconfig/addons.js'], + ignorePatterns: ['vitest.config.ts'], }; diff --git a/packages/blocks/CHANGELOG.md b/packages/blocks/CHANGELOG.md index 89cd5d8368..190740f454 100644 --- a/packages/blocks/CHANGELOG.md +++ b/packages/blocks/CHANGELOG.md @@ -8,6 +8,20 @@ +## 1.0.0-alpha.2 (2025-01-24) + +### Feature + +- Added more blocks. @sneridagh [#6409](https://github.com/plone/volto/pull/6409) + +### Bugfix + +- Fixed several typing errors and a map without key. @sneridagh [#6599](https://github.com/plone/volto/pull/6599) + +### Internal + +- Centralize `tsconfig`. @sneridagh [#6536](https://github.com/plone/volto/pull/6536) + ## 1.0.0-alpha.1 (2024-07-26) ### Internal diff --git a/packages/blocks/RenderBlocks/BlockWrapper.tsx b/packages/blocks/RenderBlocks/BlockWrapper.tsx index e94b555d47..d55f8ec8d5 100644 --- a/packages/blocks/RenderBlocks/BlockWrapper.tsx +++ b/packages/blocks/RenderBlocks/BlockWrapper.tsx @@ -11,8 +11,8 @@ const BlockWrapper = (props: BlockWrapperProps) => { const data = content.blocks?.[block]; const category = blocksConfig?.[data['@type']]?.category; // TODO: Bring in the StyleWrapper helpers for calculating styles and classes - const classNames = null; - const style = null; + const classNames = undefined; + const style = undefined; return (
{ const Block = blocksConfig[blockType]?.view || DefaultBlockView; return Block ? ( - + {/* @ts-ignore It's ok to pass the blockData as is */} +## 1.0.0-alpha.22 (2025-01-24) + +### Documentation + +- Fix typo in README. @sneridagh [#6599](https://github.com/plone/volto/pull/6599) + ## 1.0.0-alpha.21 (2025-01-15) ### Feature diff --git a/packages/client/README.md b/packages/client/README.md index 139648e30e..3e36a69cfb 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -37,7 +37,7 @@ The main artifact that the client provides is the `ploneClient` entry point. Once imported, you should call `initialize` to setup its basic parameters, like `apiPath`, headers or authentication options. -After initialization, you can import all the prorvided query options factories. +After initialization, you can import all the provided query options factories. ```ts import ploneClient from '@plone/client'; diff --git a/packages/client/package.json b/packages/client/package.json index e4bce7a30d..c87e16e7ab 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -8,7 +8,7 @@ } ], "license": "MIT", - "version": "1.0.0-alpha.21", + "version": "1.0.0-alpha.22", "repository": { "type": "git", "url": "git@github.com:plone/volto.git" diff --git a/packages/eslintconfig/README.md b/packages/eslintconfig/README.md new file mode 100644 index 0000000000..551281bc53 --- /dev/null +++ b/packages/eslintconfig/README.md @@ -0,0 +1,34 @@ +# `eslintconfig` + +Base configurations for projects. + +## Usage + +In `package.json`: + +```json + "devDependencies": { + "eslintconfig": "workspace:*", + } +``` + +```js +{ + "extends": "tsconfig/react-library.json", + "include": ["src", "src/**/*.js"], + "exclude": [ + "node_modules", + "build", + "public", + "coverage", + "src/**/*.test.{js,jsx,ts,tsx}", + "src/**/*.spec.{js,jsx,ts,tsx}", + "src/**/*.stories.{js,jsx,ts,tsx}" + ] +} +``` + +> [!WARNING] +> This package or app is experimental. +> The community offers no support whatsoever for it. +> Breaking changes may occur without notice. diff --git a/packages/eslintconfig/addons.js b/packages/eslintconfig/addons.js new file mode 100644 index 0000000000..cf72e9fa30 --- /dev/null +++ b/packages/eslintconfig/addons.js @@ -0,0 +1,17 @@ +/** @type {import('eslint').Linter.Config} */ +module.exports = { + ignorePatterns: ['storybook-static', 'dist'], + overrides: [ + { + files: ['**/*.{ts,tsx, js, jsx}'], + extends: [ + 'plugin:react/jsx-runtime', // We only want this for non-library code (eg. volto add-ons) + ], + rules: { + 'import/no-unresolved': 1, + 'import/named': 'error', + 'react/jsx-key': [2, { checkFragmentShorthand: true }], + }, + }, + ], +}; diff --git a/packages/eslintconfig/package.json b/packages/eslintconfig/package.json new file mode 100644 index 0000000000..5abc62b4fb --- /dev/null +++ b/packages/eslintconfig/package.json @@ -0,0 +1,9 @@ +{ + "name": "eslintconfig", + "version": "0.0.0", + "private": true, + "license": "MIT", + "publishConfig": { + "access": "public" + } +} diff --git a/packages/helpers/CHANGELOG.md b/packages/helpers/CHANGELOG.md index bd9e85c701..9f6747d5ff 100644 --- a/packages/helpers/CHANGELOG.md +++ b/packages/helpers/CHANGELOG.md @@ -8,6 +8,12 @@ +## 1.0.1 (2025-01-24) + +### Internal + +- Centralize `tsconfig`. @sneridagh [#6536](https://github.com/plone/volto/issues/6536) + ## 1.0.0 (2024-12-12) ### Internal diff --git a/packages/helpers/news/6536.internal b/packages/helpers/news/6536.internal deleted file mode 100644 index 27c8bbd143..0000000000 --- a/packages/helpers/news/6536.internal +++ /dev/null @@ -1 +0,0 @@ -Centralize `tsconfig`. @sneridagh diff --git a/packages/helpers/package.json b/packages/helpers/package.json index 01a81fc5f7..e9ee170c59 100644 --- a/packages/helpers/package.json +++ b/packages/helpers/package.json @@ -9,7 +9,7 @@ ], "funding": "https://github.com/sponsors/plone", "license": "MIT", - "version": "1.0.0", + "version": "1.0.1", "repository": { "type": "git", "url": "https://github.com/plone/volto.git" diff --git a/packages/providers/CHANGELOG.md b/packages/providers/CHANGELOG.md index 96a8e9b472..fec0df254a 100644 --- a/packages/providers/CHANGELOG.md +++ b/packages/providers/CHANGELOG.md @@ -8,6 +8,12 @@ +## 1.0.0-alpha.7 (2025-01-24) + +### Internal + +- Centralize `tsconfig`. @sneridagh [#6536](https://github.com/plone/volto/issues/6536) + ## 1.0.0-alpha.6 (2024-11-21) ### Feature diff --git a/packages/providers/news/6536.internal b/packages/providers/news/6536.internal deleted file mode 100644 index 27c8bbd143..0000000000 --- a/packages/providers/news/6536.internal +++ /dev/null @@ -1 +0,0 @@ -Centralize `tsconfig`. @sneridagh diff --git a/packages/providers/package.json b/packages/providers/package.json index 8a12623112..9e3772638b 100644 --- a/packages/providers/package.json +++ b/packages/providers/package.json @@ -9,7 +9,7 @@ ], "funding": "https://github.com/sponsors/plone", "license": "MIT", - "version": "1.0.0-alpha.6", + "version": "1.0.0-alpha.7", "repository": { "type": "git", "url": "https://github.com/plone/volto.git" diff --git a/packages/react-router/.gitignore b/packages/react-router/.gitignore new file mode 100644 index 0000000000..b947077876 --- /dev/null +++ b/packages/react-router/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/packages/react-router/.npmignore b/packages/react-router/.npmignore new file mode 100644 index 0000000000..a6d10baa1e --- /dev/null +++ b/packages/react-router/.npmignore @@ -0,0 +1,6 @@ +news +towncrier.toml +.changelog.draft +node_modules/ +.release-it.json +.eslintrc.js diff --git a/packages/react-router/.release-it.json b/packages/react-router/.release-it.json new file mode 100644 index 0000000000..bcb50ec97b --- /dev/null +++ b/packages/react-router/.release-it.json @@ -0,0 +1,28 @@ +{ + "plugins": { + "../scripts/prepublish.js": {} + }, + "hooks": { + "after:bump": [ + "pipx run towncrier build --draft --yes --version ${version} > .changelog.draft && pipx run towncrier build --yes --version ${version}", + "pnpm build:force" + ], + "after:release": "rm .changelog.draft" + }, + "npm": { + "publish": false + }, + "git": { + "changelog": "pipx run towncrier build --draft --yes --version 0.0.0", + "requireUpstream": false, + "requireCleanWorkingDir": false, + "commitMessage": "Release @plone/react-router ${version}", + "tagName": "plone-react-router-${version}", + "tagAnnotation": "Release @plone/react-router ${version}" + }, + "github": { + "release": true, + "releaseName": "@plone/react-router ${version}", + "releaseNotes": "cat .changelog.draft" + } +} diff --git a/packages/react-router/CHANGELOG.md b/packages/react-router/CHANGELOG.md new file mode 100644 index 0000000000..97a8deaaa6 --- /dev/null +++ b/packages/react-router/CHANGELOG.md @@ -0,0 +1,66 @@ +# @plone/providers Release Notes + + + + + +## 1.0.0 (2025-01-24) + +### Bugfix + +- Initial implementation. + Added `getAddonRoutesConfig` for configuring routes in add-ons. @sneridagh [#6599](https://github.com/plone/volto/issues/6599) + +## 1.0.0-alpha.6 (2024-11-21) + +### Feature + +- Update RAC to 1.5.0 @sneridagh [#6498](https://github.com/plone/volto/issues/6498) + +## 1.0.0-alpha.5 (2024-11-05) + +### Internal + +- Improve packaging. @sneridagh + +## 1.0.0-alpha.4 (2024-11-05) + +### Internal + +- Bump local `typescript` version. @sneridagh [#6461](https://github.com/plone/volto/issues/6461) +- Replace `parcel` with `tsup`. Better types, better tsconfig. Move to ESM. @sneridagh [#6468](https://github.com/plone/volto/issues/6468) + +## 1.0.0-alpha.3 (2024-10-18) + +## 1.0.0-alpha.2 (2024-10-18) + +### Breaking + +- Improve and group providers. @sneridagh + Breaking: + - The interface of the providers has changed. Please check the new one, and adapt your apps accordingly. [#6069](https://github.com/plone/volto/issues/6069) + +### Internal + +- Update typescript and vitest everywhere @sneridagh [#6407](https://github.com/plone/volto/issues/6407) + +## 1.0.0-alpha.1 (2024-05-23) + +### Internal + +- Cleanup imports in RouterLocation provider @pnicolli [#6029](https://github.com/plone/volto/issues/6029) + +## 1.0.0-alpha.0 (2024-05-13) + +### Feature + +- Initial implementation @sneridagh [#5887](https://github.com/plone/volto/issues/5887) + +### Internal + +- Improvements to the monorepo setup with utilities, especially ESLint. Build cached option to speedup operations. @sneridagh [#5969](https://github.com/plone/volto/issues/5969) +- Saner defaults for building deps, switch default to cached, add `build:force` command @sneridagh [#5980](https://github.com/plone/volto/issues/5980) diff --git a/packages/react-router/README.md b/packages/react-router/README.md new file mode 100644 index 0000000000..75ba6475a9 --- /dev/null +++ b/packages/react-router/README.md @@ -0,0 +1,188 @@ +# `@plone/providers` + +This package contains utility providers for Plone React components. +The main purpose is to provide dependency injection of common required artifacts needed by any app. +These artifacts include: +- Router related +- Plone Client +- URL handling methods + +> [!WARNING] +> This package or app is experimental. +> The community offers no support whatsoever for it. +> Breaking changes may occur without notice. + +## `PloneProvider` + +It provides all the necessary artifacts that an app can need grouped in a single provider. + +```ts +interface PloneProvider { + ploneClient: InstanceType; + queryClient: QueryClient; + useLocation: () => Location | undefined; + useParams: (opts?: any) => Record; + navigate: (path: string) => void; + useHref: (to: string, options?: any) => string; + flattenToAppURL: (path: string | undefined) => string | undefined; +} +``` + +It should be instantiated at the top of your app. +You have to provide the required props depending on the framework and the router used. +This is the example for a Next.js app. +Please refer to the {file}`apps` folder of the Volto repository for more examples of the usage of `PloneProvider` in different React frameworks. + +```tsx +'use client'; +import React from 'react'; +import { + useRouter, + usePathname, + useSearchParams, + useParams, +} from 'next/navigation'; +import { QueryClient } from '@tanstack/react-query'; +import { PloneProvider } from '@plone/providers'; +import PloneClient from '@plone/client'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { flattenToAppURL } from './utils'; +import config from './config'; + +// Custom hook to unify the location object between NextJS and Plone +function useLocation() { + let pathname = usePathname(); + let search = useSearchParams(); + + return { + pathname, + search, + searchStr: '', + hash: (typeof window !== 'undefined' && window.location.hash) || '', + href: (typeof window !== 'undefined' && window.location.href) || '', + }; +} + +const Providers: React.FC<{ + children?: React.ReactNode; +}> = ({ children }) => { + // Creating the clients at the file root level makes the cache shared + // between all requests and means _all_ data gets passed to _all_ users. + // Besides being bad for performance, this also leaks any sensitive data. + // We use this pattern to ensure that every client gets its own clients + const [queryClient] = React.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] = React.useState(() => + PloneClient.initialize({ + apiPath: config.settings.apiPath, + }), + ); + + const router = useRouter(); + + return ( + { + router.push(to); + }} + useParams={useParams} + useHref={(to) => flattenToAppURL(to)} + flattenToAppURL={flattenToAppURL} + > + {children} + + + ); +}; + +export default Providers; + +``` + +You can use it anywhere in your app by using the hook `usePloneProvider`. + +```tsx +import { usePloneProvider } from '@plone/providers'; + +const { ploneClient } = usePloneProvider() +``` + +Alternatively, you can use it in any other context property. + +```tsx +const { navigate } = usePloneProvider() +``` + +## `PloneClientProvider` + +`PloneProvider` in a group of other smaller providers. +You can also instantiate and use them as standalone providers. +However, you should do this only if the framework has some limitation on using the bulk `PloneClientProvider`. + +The following snippets show its usage. +First, instantiate the provider. + +```ts +export type PloneClientProviderProps = { + client: InstanceType; + queryClient: QueryClient; + children?: React.ReactNode; +}; +``` + +Second, use its related hook through either of the following examples. + +```tsx +import { usePloneClient } from '@plone/providers'; + +const client = usePloneClient() +``` + +or + +```tsx +const { getContentQuery } = usePloneClient() +``` + +## `AppRouterProvider` + +This provider is included also in `PloneProvider`. +You can also instantiate and use it as a standalone provider. +However, you should do this only if the framework has some limitation on using the bulk `PloneClientProvider`. + +The following code example shows its usage. + +```ts +interface AppRouterProps { + useLocation: () => Location | undefined; + useParams: (opts?: any) => Record; + navigate: (path: string) => void; + useHref?: (to: string, options?: any) => string; + flattenToAppURL: (path: string | undefined) => string | undefined; + children: ReactNode; +} +``` + +The following code sample shows its related hook. + +```tsx +import { useAppRouter } from '@plone/providers'; + +const { useLocation } = useAppRouter() +``` diff --git a/packages/react-router/news/.gitkeep b/packages/react-router/news/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/react-router/package.json b/packages/react-router/package.json new file mode 100644 index 0000000000..0b6477b40c --- /dev/null +++ b/packages/react-router/package.json @@ -0,0 +1,77 @@ +{ + "name": "@plone/react-router", + "description": "Plone React Router integration package", + "maintainers": [ + { + "name": "Plone Foundation", + "url": "https://plone.org" + } + ], + "funding": "https://github.com/sponsors/plone", + "license": "MIT", + "version": "1.0.0", + "repository": { + "type": "git", + "url": "https://github.com/plone/volto.git" + }, + "bugs": { + "url": "https://github.com/plone/volto/issues" + }, + "homepage": "https://plone.org", + "keywords": [ + "volto", + "plone", + "plone6", + "react", + "helpers" + ], + "publishConfig": { + "access": "public" + }, + "type": "module", + "files": [ + "dist", + "README.md" + ], + "main": "./dist/index.js", + "exports": { + "./package.json": "./package.json", + ".": { + "import": "./dist/index.js", + "default": "./dist/index.cjs" + } + }, + "scripts": { + "build": "tsup", + "build:force": "tsup", + "check:exports": "attw --pack .", + "test": "vitest", + "dry-release": "release-it --dry-run", + "release": "release-it", + "release-major-alpha": "release-it major --preRelease=alpha", + "release-alpha": "release-it --preRelease=alpha" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + }, + "dependencies": {}, + "devDependencies": { + "@arethetypeswrong/cli": "^0.16.4", + "@plone/types": "workspace:*", + "@react-router/dev": "7.1.2", + "@types/node": "22.10.7", + "@types/react": "^18", + "@types/react-dom": "^18", + "release-it": "17.1.1", + "tsconfig": "workspace:*", + "tsup": "^8.3.5", + "typescript": "^5.6.3", + "vitest": "^2.1.3" + } +} diff --git a/packages/react-router/src/index.test.ts b/packages/react-router/src/index.test.ts new file mode 100644 index 0000000000..7f20b57247 --- /dev/null +++ b/packages/react-router/src/index.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it, afterEach, beforeEach } from 'vitest'; +import { getAddonRoutesConfig } from './index'; +import type { ReactRouterRouteEntry } from '@plone/types'; + +describe('getAddonRoutesConfig', () => { + const addonsInfo = [ + { + name: '@plone/components', + modulePath: '/my/path/to/plone/components', + }, + ]; + + it('route - basic', () => { + const routesConfig: Array = [ + { + type: 'route', + path: '/login', + file: './login.tsx', + }, + ]; + expect(getAddonRoutesConfig(routesConfig, addonsInfo)).toEqual([ + { children: undefined, file: './login.tsx', path: '/login' }, + ]); + }); + + it('route - basic with addon name', () => { + const routesConfig: Array = [ + { + type: 'route', + path: '/login', + file: '@plone/components/login.tsx', + }, + ]; + expect(getAddonRoutesConfig(routesConfig, addonsInfo)).toEqual([ + { + children: undefined, + file: '/my/path/to/plone/components/login.tsx', + path: '/login', + }, + ]); + }); + + it('route - with options', () => { + const routesConfig: Array = [ + { + type: 'route', + path: '/login', + file: './login.tsx', + options: { + id: 'login', + }, + }, + ]; + expect(getAddonRoutesConfig(routesConfig, addonsInfo)).toEqual([ + { + children: undefined, + file: './login.tsx', + path: '/login', + id: 'login', + }, + ]); + }); + + it('route - nested', () => { + const routesConfig: Array = [ + { + type: 'route', + path: '/login', + file: './login.tsx', + children: [ + { + type: 'route', + path: '/login/ok', + file: './login/ok.tsx', + }, + ], + }, + ]; + expect(getAddonRoutesConfig(routesConfig, addonsInfo)).toEqual([ + { + file: './login.tsx', + path: '/login', + children: [ + { + children: undefined, + file: './login/ok.tsx', + path: '/login/ok', + }, + ], + }, + ]); + }); +}); diff --git a/packages/react-router/src/index.ts b/packages/react-router/src/index.ts new file mode 100644 index 0000000000..59788606ce --- /dev/null +++ b/packages/react-router/src/index.ts @@ -0,0 +1,77 @@ +import { route, index, layout, prefix } from '@react-router/dev/routes'; +import type { RouteConfig, RouteConfigEntry } from '@react-router/dev/routes'; +import type { ReactRouterRouteEntry } from '@plone/types'; +import path from 'node:path'; + +export function getAddonRoutesConfig( + routesConfig: Array, + addonsInfo: Array, +): Array { + let resultRoutesConfig: RouteConfig = []; + + for (const routeConfig of routesConfig) { + const containsAddonModule = addonsInfo.find((addon) => + routeConfig.file.includes(addon.name), + ); + if (containsAddonModule) { + routeConfig.file = path.join( + containsAddonModule.modulePath, + routeConfig.file.replace(containsAddonModule.name, ''), + ); + } + switch (routeConfig.type) { + case 'route': + if (routeConfig.options) { + resultRoutesConfig.push( + route(routeConfig.path, routeConfig.file, routeConfig.options), + ); + } else if (routeConfig.children) { + resultRoutesConfig.push( + route( + routeConfig.path, + routeConfig.file, + routeConfig.options || {}, + getAddonRoutesConfig( + routeConfig.children, + addonsInfo, + ) as Array, + ), + ); + } else { + resultRoutesConfig.push(route(routeConfig.path, routeConfig.file)); + } + break; + + case 'index': + resultRoutesConfig.push(index(routeConfig.file, routeConfig.options)); + break; + + case 'layout': + if (routeConfig.options) { + resultRoutesConfig.push( + layout(routeConfig.file, routeConfig.options), + ); + } + if (routeConfig.children) { + resultRoutesConfig.push( + layout( + routeConfig.file, + routeConfig.options || {}, + getAddonRoutesConfig( + routeConfig.children, + addonsInfo, + ) as Array, + ), + ); + } + break; + + case 'prefix': + console.log('prefix not implemented yet'); + break; + default: + break; + } + } + return resultRoutesConfig; +} diff --git a/packages/react-router/towncrier.toml b/packages/react-router/towncrier.toml new file mode 100644 index 0000000000..3ef721f378 --- /dev/null +++ b/packages/react-router/towncrier.toml @@ -0,0 +1,33 @@ +[tool.towncrier] +filename = "CHANGELOG.md" +directory = "news/" +title_format = "## {version} ({project_date})" +underlines = ["", "", ""] +template = "../scripts/templates/towncrier_template.jinja" +start_string = "\n" +issue_format = "[#{issue}](https://github.com/plone/volto/issues/{issue})" + +[[tool.towncrier.type]] +directory = "breaking" +name = "Breaking" +showcontent = true + +[[tool.towncrier.type]] +directory = "feature" +name = "Feature" +showcontent = true + +[[tool.towncrier.type]] +directory = "bugfix" +name = "Bugfix" +showcontent = true + +[[tool.towncrier.type]] +directory = "internal" +name = "Internal" +showcontent = true + +[[tool.towncrier.type]] +directory = "documentation" +name = "Documentation" +showcontent = true diff --git a/packages/react-router/tsconfig.json b/packages/react-router/tsconfig.json new file mode 100644 index 0000000000..d6fbf62fe0 --- /dev/null +++ b/packages/react-router/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "tsconfig/react-library.json", + "include": ["src"], + "exclude": [ + "node_modules", + "build", + "public", + "coverage", + "src/**/*.test.{js,jsx,ts,tsx}", + "src/**/*.spec.{js,jsx,ts,tsx}", + "src/**/*.stories.{js,jsx,ts,tsx}" + ] +} diff --git a/packages/react-router/tsup.config.ts b/packages/react-router/tsup.config.ts new file mode 100644 index 0000000000..82b88a425c --- /dev/null +++ b/packages/react-router/tsup.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entryPoints: ['src/index.ts'], + format: ['cjs', 'esm'], + dts: true, + outDir: 'dist', + clean: true, +}); diff --git a/packages/registry/CHANGELOG.md b/packages/registry/CHANGELOG.md index 549b62a537..5aed2ac3e9 100644 --- a/packages/registry/CHANGELOG.md +++ b/packages/registry/CHANGELOG.md @@ -8,6 +8,16 @@ +## 2.3.0 (2025-01-24) + +### Feature + +- Added route registry. @sneridagh [#6600](https://github.com/plone/volto/issues/6600) + +### Documentation + +- Document the route API. @sneridagh [#6604](https://github.com/plone/volto/issues/6604) + ## 2.2.0 (2024-12-12) ### Feature diff --git a/packages/registry/news/6600.feature b/packages/registry/news/6600.feature deleted file mode 100644 index 946395b56f..0000000000 --- a/packages/registry/news/6600.feature +++ /dev/null @@ -1 +0,0 @@ -Added route registry. @sneridagh diff --git a/packages/registry/news/6604.documentation b/packages/registry/news/6604.documentation deleted file mode 100644 index bd10235aba..0000000000 --- a/packages/registry/news/6604.documentation +++ /dev/null @@ -1 +0,0 @@ -Document the route API. @sneridagh diff --git a/packages/registry/package.json b/packages/registry/package.json index f05ac0d9bd..1f0f9047e0 100644 --- a/packages/registry/package.json +++ b/packages/registry/package.json @@ -9,7 +9,7 @@ ], "funding": "https://github.com/sponsors/plone", "license": "MIT", - "version": "2.2.0", + "version": "2.3.0", "repository": { "type": "git", "url": "https://github.com/plone/volto.git" diff --git a/packages/seven/.eslintrc.cjs b/packages/seven/.eslintrc.cjs new file mode 100644 index 0000000000..21d4f1322c --- /dev/null +++ b/packages/seven/.eslintrc.cjs @@ -0,0 +1,88 @@ +/** + * 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'], + + // Ignore Cypress folder + ignorePatterns: [ + 'cypress/', + '.react-router/**/*', + 'tests/registry.config.ts', + ], + + 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/seven/.gitignore b/packages/seven/.gitignore new file mode 100644 index 0000000000..f1eb112b25 --- /dev/null +++ b/packages/seven/.gitignore @@ -0,0 +1,7 @@ +node_modules + +/.cache +/build +.env +.react-router +.registry.loader.js diff --git a/packages/seven/.release-it.json b/packages/seven/.release-it.json new file mode 100644 index 0000000000..d91788c67c --- /dev/null +++ b/packages/seven/.release-it.json @@ -0,0 +1,26 @@ +{ + "hooks": { + "after:bump": [ + "pipx run towncrier build --draft --yes --version ${version} > .changelog.draft", + "pipx run towncrier build --yes --version ${version}" + ], + "after:release": "rm .changelog.draft" + }, + "npm": { + "publish": false + }, + "git": { + "commitArgs": ["--no-verify"], + "changelog": "pipx run towncrier build --draft --yes --version 0.0.0", + "requireUpstream": false, + "requireCleanWorkingDir": false, + "commitMessage": "Release Plone7 ${version}", + "tagName": "plone7-${version}", + "tagAnnotation": "Release Plone7 ${version}" + }, + "github": { + "release": true, + "releaseName": "Plone7 ${version}", + "releaseNotes": "cat .changelog.draft" + } +} diff --git a/packages/seven/CHANGELOG.md b/packages/seven/CHANGELOG.md new file mode 100644 index 0000000000..a90be32e57 --- /dev/null +++ b/packages/seven/CHANGELOG.md @@ -0,0 +1,11 @@ +# Plone 7 Release Notes + + + + + +## 1.0.0 (unreleased) diff --git a/packages/seven/Makefile b/packages/seven/Makefile new file mode 100644 index 0000000000..b012ee8899 --- /dev/null +++ b/packages/seven/Makefile @@ -0,0 +1,274 @@ +# Volto development + +### Defensive settings for make: +# https://tech.davis-hansson.com/p/make/ +SHELL:=bash +.ONESHELL: +.SHELLFLAGS:=-eu -o pipefail -c +.SILENT: +.DELETE_ON_ERROR: +MAKEFLAGS+=--warn-undefined-variables +MAKEFLAGS+=--no-builtin-rules + +# Project settings (read from repo root) +include ../../variables.mk + +# Allow setting the language for backend-docker-start. Default is `en`. +LANGUAGE ?=en + +# Recipe snippets for reuse + +CHECKOUT_BASENAME="$(shell basename $(shell realpath ./))" +CHECKOUT_BRANCH=$(shell git branch --show-current) +CHECKOUT_TMP=../$(CHECKOUT_BASENAME).tmp +CHECKOUT_TMP_ABS="$(shell realpath $(CHECKOUT_TMP))" + +CURRENT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) + +# We like colors +# From: https://coderwall.com/p/izxssa/colored-makefile-for-golang-projects +RED=`tput setaf 1` +GREEN=`tput setaf 2` +RESET=`tput sgr0` +YELLOW=`tput setaf 3` + + +# Top-level targets + +.PHONY: all +all: help + +# Add the following 'help' target to your Makefile +# and add help text after each target name starting with ' ##' +# to return a pretty list of targets and their descriptions. +.PHONY: help +help: ## This help message + @echo -e "$$(grep -hE '^\S+:.*##' $(MAKEFILE_LIST) | sed -e 's/:.*##\s*/:/' -e 's/^\(.\+\):\(.*\)/\\x1b[36m\1\\x1b[m:\2/' | column -c2 -t -s :)" + +.PHONY: start +start: ## Starts Plone 7 in development mode + pnpm start + +.PHONY: build +build: ## Build a production bundle for distribution + pnpm build + +.PHONY: test +test: ## Run unit tests + pnpm test + +.PHONY: clean +clean: ## Clean development environment + rm -rf node_modules + +##### Build + +.PHONY: cypress-install +cypress-install: ## Install Cypress for acceptance tests + $(NODEBIN)/cypress install + +../registry/dist: $(shell find ../registry/src -type f) + (cd ../../ && pnpm build:registry) + +../components/dist: $(shell find ../components/src -type f) + (cd ../../ && pnpm build:components) + +../client/dist: $(shell find ../client/src -type f) + (cd ../../ && pnpm build:client) + +../providers/dist: $(shell find ../providers/src -type f) + (cd ../../ && pnpm build:providers) + +../helpers/dist: $(shell find ../helpers/src -type f) + (cd ../../ && pnpm build:helpers) + +../react-router/dist: $(shell find ../react-router/src -type f) + (cd ../../ && pnpm build:react-router) + +.PHONY: build-deps +build-deps: ../registry/dist ../components/dist ../client/dist ../providers/dist ../react-router/dist ../helpers/dist ## Build dependencies + +.PHONY: i18n +i18n: ## Extract and compile translations + pnpm i18n + +## Storybook + +.PHONY: storybook-start +storybook-start: ## Start Storybook server on port 6006 + @echo "$(GREEN)==> Start Storybook$(RESET)" + pnpm run storybook + +.PHONY: storybook-build +storybook-build: build-deps ## Build Storybook + pnpm build-storybook -o ../../docs/_build/html/storybook + +##### Release (it runs the one inside) + +.PHONY: release-notes-copy-to-docs +release-notes-copy-to-docs: ## Copy release notes into documentation + cp CHANGELOG.md ../../docs/source/release-notes/index.md + git add ../../docs/source/release-notes/index.md + +##### Docker containers + +.PHONY: backend-docker-start +backend-docker-start: ## Starts a Docker-based backend for development + docker run -it --rm --name=backend -p 8080:8080 -v volto-backend-data:/data -e SITE=Plone -e ADDONS='$(KGS)' -e LANGUAGE='$(LANGUAGE)' $(DOCKER_IMAGE) + +.PHONY: frontend-docker-start +frontend-docker-start: ## Starts a Docker-based frontend for development + docker run -it --rm --name=volto --link backend -p 3000:3000 -e RAZZLE_INTERNAL_API_PATH=http://backend:8080/Plone -e RAZZLE_DEV_PROXY_API_PATH=http://backend:8080/Plone plone/plone-frontend:latest + +##### Acceptance tests (Cypress) +######### Dev mode Acceptance tests + +.PHONY: acceptance-frontend-dev-start +acceptance-frontend-dev-start: ## Start acceptance frontend in development mode + PLONE_API_PATH=http://127.0.0.1:55001/plone pnpm start + +######### Core Acceptance tests + +.PHONY: acceptance-backend-start +acceptance-backend-start: ## Start backend acceptance server + #docker run -it --rm -p 55001:55001 $(DOCKER_IMAGE_ACCEPTANCE) + # Uncomment next line and comment line above to use a custom image with the acceptance server (in case you need to use an specific backend add-on or version) + docker run -it --rm -e ZSERVER_HOST=0.0.0.0 -e ZSERVER_PORT=55001 -p 55001:55001 -e ADDONS='$(KGS) $(TESTING_ADDONS)' -e APPLY_PROFILES=plone.app.contenttypes:plone-content,plone.restapi:default,plone.volto:default-homepage -e CONFIGURE_PACKAGES=plone.app.contenttypes,plone.restapi,plone.volto,plone.volto.cors $(DOCKER_IMAGE) ./bin/robot-server plone.app.robotframework.testing.VOLTO_ROBOT_TESTING + +.PHONY: ci-acceptance-backend-start +ci-acceptance-backend-start: ## Start backend acceptance server in headless mode for CI + docker run -i --rm -p 55001:55001 $(DOCKER_IMAGE_ACCEPTANCE) + # Uncomment next line and comment line above to use a custom image with the acceptance server (in case you need to use an specific backend add-on or version) + # docker run -i --rm -e ZSERVER_HOST=0.0.0.0 -e ZSERVER_PORT=55001 -p 55001:55001 -e ADDONS='$(KGS) $(TESTING_ADDONS)' -e APPLY_PROFILES=plone.app.contenttypes:plone-content,plone.restapi:default,plone.volto:default-homepage -e CONFIGURE_PACKAGES=plone.app.contenttypes,plone.restapi,plone.volto,plone.volto.cors $(DOCKER_IMAGE) ./bin/robot-server plone.app.robotframework.testing.VOLTO_ROBOT_TESTING + +.PHONY: acceptance-frontend-prod-start +acceptance-frontend-prod-start: build-deps ## Start acceptance frontend in production mode + pnpm build && PLONE_API_PATH=http://127.0.0.1:55001/plone pnpm start:prod + +.PHONY: acceptance-test +acceptance-test: ## Start Cypress in interactive mode + NODE_ENV=production CYPRESS_API=plone $(NODEBIN)/cypress open + +.PHONY: ci-acceptance-test +ci-acceptance-test: ## Run cypress tests in headless mode for CI + NODE_ENV=production CYPRESS_API=plone $(NODEBIN)/cypress run --config specPattern='cypress/tests/core/**/*.{ts,tsx}' + +.PHONY: ci-acceptance-test-run-all +ci-acceptance-test-run-all: ## With a single command, start both the acceptance frontend and backend acceptance server, and run Cypress tests in headless mode + $(NODEBIN)/start-test "make ci-acceptance-backend-start" http-get://127.0.0.1:55001/plone "make acceptance-frontend-prod-start" http://127.0.0.1:3000 "make ci-acceptance-test" + +######### Deployment Core Acceptance tests + +.PHONY: deployment-acceptance-frontend-prod-start +deployment-acceptance-frontend-prod-start: build-deps ## Start acceptance frontend in production mode for deployment + pnpm build && pnpm start:prod + +.PHONY: deployment-acceptance-test +deployment-acceptance-test: ## Start Cypress in interactive mode for tests in deployment + NODE_ENV=production CYPRESS_API=plone $(NODEBIN)/cypress open --config baseUrl='http://localhost' + +.PHONY: deployment-acceptance-web-server-start +deployment-acceptance-web-server-start: ## Start the reverse proxy (Traefik) in port 80 for deployment + cd cypress/docker && docker compose -f seamless.yml up + +.PHONY: deployment-ci-acceptance-test-run-all +deployment-ci-acceptance-test-run-all: ## With a single command, run the backend, frontend, and the Cypress tests in headless mode for CI for deployment tests + $(NODEBIN)/start-test "make acceptance-backend-start" http-get://127.0.0.1:55001/plone "make deployment-acceptance-frontend-prod-start" http://127.0.0.1:3000 "make ci-acceptance-test" + +######### Project Acceptance tests + +.PHONY: project-acceptance-frontend-prod-start +project-acceptance-frontend-prod-start: build-deps ## Start acceptance frontend in production mode for project tests + (cd ../../.. && pnpm build && PLONE_API_PATH=http://127.0.0.1:55001/plone pnpm start:prod) + +######### Core Sandbox Acceptance tests + +.PHONY: coresandbox-acceptance-backend-start +coresandbox-acceptance-backend-start: ## Start backend acceptance server for core sandbox tests + docker run -i --rm -p 55001:55001 -e APPLY_PROFILES=plone.app.contenttypes:plone-content,plone.restapi:default,plone.volto:default-homepage,plone.volto:coresandbox -e CONFIGURE_PACKAGES=plone.app.contenttypes,plone.restapi,plone.volto,plone.volto.cors,plone.volto.coresandbox $(DOCKER_IMAGE_ACCEPTANCE) + +.PHONY: coresandbox-acceptance-frontend-prod-start +coresandbox-acceptance-frontend-prod-start: build-deps ## Start acceptance frontend in production mode for core sandbox tests + ADDONS=@plone/volto-coresandbox PLONE_API_PATH=http://127.0.0.1:55001/plone pnpm build && pnpm start:prod + +.PHONY: coresandbox-acceptance-frontend-dev-start +coresandbox-acceptance-frontend-dev-start: build-deps ## Start acceptance frontend in development mode for core sandbox tests + ADDONS=@plone/volto-coresandbox PLONE_API_PATH=http://127.0.0.1:55001/plone pnpm start + +.PHONY: coresandbox-acceptance-test +coresandbox-acceptance-test: ## Start Cypress in interactive mode for core sandbox tests + NODE_ENV=production CYPRESS_API=plone $(NODEBIN)/cypress open --config specPattern='cypress/tests/coresandbox/**/*.{js,jsx,ts,tsx}' + +.PHONY: coresandbox-ci-acceptance-test +coresandbox-ci-acceptance-test: ## Run Cypress tests in headless mode for CI for core sandbox tests + NODE_ENV=production CYPRESS_API=plone $(NODEBIN)/cypress run --config specPattern='cypress/tests/coresandbox/**/*.{js,jsx,ts,tsx}' + +.PHONY: coresandbox-ci-acceptance-test-run-all +coresandbox-ci-acceptance-test-run-all: ## With a single command, run the backend, frontend, and the Cypress tests in headless mode for CI for core sandbox tests + $(NODEBIN)/start-test "make coresandbox-acceptance-backend-start" http-get://127.0.0.1:55001/plone "make coresandbox-acceptance-frontend-prod-start" http://127.0.0.1:3000 "make coresandbox-ci-acceptance-test" + +######### Multilingual Acceptance tests + +.PHONY: multilingual-acceptance-backend-start +multilingual-acceptance-backend-start: ## Start backend acceptance server for multilingual tests + docker run -i --rm -p 55001:55001 -e APPLY_PROFILES=plone.app.contenttypes:plone-content,plone.restapi:default,plone.volto:multilingual $(DOCKER_IMAGE_ACCEPTANCE) + +.PHONY: multilingual-acceptance-frontend-prod-start +multilingual-acceptance-frontend-prod-start: build-deps ## Start acceptance frontend in production mode for multilingual tests + ADDONS=@plone/volto-coresandbox:multilingualFixture PLONE_API_PATH=http://127.0.0.1:55001/plone pnpm build && pnpm start:prod + +.PHONY: multilingual-acceptance-test +multilingual-acceptance-test: ## Start Cypress in interactive mode for multilingual tests + NODE_ENV=production CYPRESS_API=plone $(NODEBIN)/cypress open --config specPattern='cypress/tests/multilingual/**/*.{js,jsx,ts,tsx}' + +.PHONY: multilingual-ci-acceptance-test +multilingual-ci-acceptance-test: ## Run Cypress tests in headless mode for CI for multilingual tests + NODE_ENV=production CYPRESS_API=plone $(NODEBIN)/cypress run --config specPattern='cypress/tests/multilingual/**/*.{js,jsx,ts,tsx}' + +.PHONY: multilingual-ci-acceptance-test-run-all +multilingual-ci-acceptance-test-run-all: ## With a single command, run the backend, frontend, and the Cypress tests in headless mode for CI for multilingual tests + $(NODEBIN)/start-test "make multilingual-acceptance-backend-start" http-get://127.0.0.1:55001/plone "make multilingual-acceptance-frontend-prod-start" http://127.0.0.1:3000 "make multilingual-ci-acceptance-test" + +######### Deployment Multilingual Acceptance tests + +.PHONY: deployment-multilingual-acceptance-backend-start +deployment-multilingual-acceptance-backend-start: ## Start backend acceptance server for multilingual tests for deployment + docker run -i --rm -p 55001:55001 -e APPLY_PROFILES=plone.app.contenttypes:plone-content,plone.restapi:default,plone.volto:multilingual $(DOCKER_IMAGE_ACCEPTANCE) + +.PHONY: deployment-multilingual-acceptance-frontend-prod-start +deployment-multilingual-acceptance-frontend-prod-start: build-deps ##Start acceptance frontend in production mode for multilingual tests for deployment + ADDONS=@plone/volto-coresandbox:multilingualFixture pnpm build && pnpm start:prod + +.PHONY: deployment-multilingual-acceptance-test +deployment-multilingual-acceptance-test: ## Start Cypress in interactive mode for multilingual tests for deployment + NODE_ENV=production CYPRESS_API=plone $(NODEBIN)/cypress open --config baseUrl='http://localhost',specPattern='cypress/tests/multilingual/**/*.{js,jsx,ts,tsx}' + +.PHONY: deployment-multilingual-ci-acceptance-test +deployment-multilingual-ci-acceptance-test: ## Run Cypress tests in headless mode for CI for multilingual tests for deployment + NODE_ENV=production CYPRESS_API=plone $(NODEBIN)/cypress run --config specPattern='cypress/tests/multilingual/**/*.{js,jsx,ts,tsx}' + +.PHONY: deployment-multilingual-ci-acceptance-test-run-all +deployment-multilingual-ci-acceptance-test-run-all: ## With a single command, run the backend, frontend, and the Cypress tests in headless mode for CI for multilingual tests for deployment + $(NODEBIN)/start-test "make deployment-multilingual-acceptance-backend-start" http-get://127.0.0.1:55001/plone "make deployment-multilingual-acceptance-frontend-prod-start" http://127.0.0.1:3000 "make deployment-multilingual-ci-acceptance-test" + +######### Working Copy Acceptance tests + +.PHONY: working-copy-acceptance-backend-start +working-copy-acceptance-backend-start: ## Start backend acceptance server for working copy tests + docker run -i --rm -p 55001:55001 -e APPLY_PROFILES=plone.app.contenttypes:plone-content,plone.restapi:default,plone.app.iterate:default,plone.volto:default-homepage $(DOCKER_IMAGE_ACCEPTANCE) + +.PHONY: working-copy-acceptance-frontend-prod-start +working-copy-acceptance-frontend-prod-start: build-deps ## Start acceptance frontend in production mode for working copy tests + ADDONS=@plone/volto-coresandbox:workingCopyFixture PLONE_API_PATH=http://127.0.0.1:55001/plone pnpm build && pnpm start:prod + +.PHONY: working-copy-acceptance-test +working-copy-acceptance-test: ## Start Cypress in interactive mode for working copy tests + NODE_ENV=production CYPRESS_API=plone $(NODEBIN)/cypress open --config specPattern='cypress/tests/workingCopy/**/*.{js,jsx,ts,tsx}' + +.PHONY: working-copy-ci-acceptance-test +working-copy-ci-acceptance-test: ## Run Cypress tests in headless mode for CI for working copy tests + NODE_ENV=production CYPRESS_API=plone $(NODEBIN)/cypress run --config specPattern='cypress/tests/workingCopy/**/*.{js,jsx,ts,tsx}' + +.PHONY: working-copy-ci-acceptance-test-run-all +working-copy-ci-acceptance-test-run-all: ## With a single command, run the backend, frontend, and the Cypress tests in headless mode for CI for working copy tests + $(NODEBIN)/start-test "make working-copy-acceptance-backend-start" http-get://127.0.0.1:55001/plone "make working-copy-acceptance-frontend-prod-start" http://127.0.0.1:3000 "make working-copy-ci-acceptance-test" diff --git a/packages/seven/README.md b/packages/seven/README.md new file mode 100644 index 0000000000..9377c8fc62 --- /dev/null +++ b/packages/seven/README.md @@ -0,0 +1,33 @@ +# Plone 7 + +> [!WARNING] +> This package and all the efforts around it are not even in an alpha state and are experimental. +> The community offers no support whatsoever for it. +> Breaking changes may occur without notice. + +This is the initial (and very early) implementation of Plone 7. +After the design and first implementations of all the required pieces (the `@plone/*` libraries) that will compose Plone 7, this package will concentrate all the development during the next years. + +It is based on [React Router](https://reactrouter.com/dev/docs) 7, using the `@plone/*` libraries. + +The name of this package and its folder name in `packages` may also change since it's undecided yet. + +## Releases + +Even in experimental phase, this package will be soft released periodically, under a tag. +This will provide a way to try it out in real development and deploy scenarios. + +## Development + +To start, from the root of the monorepo, issue the following commands. + +```shell +pnpm install +pnpm --filter plone7 run dev +``` + +Then start the Plone backend. + +```shell +make backend-docker-start +``` diff --git a/packages/seven/app/config.server.ts b/packages/seven/app/config.server.ts new file mode 100644 index 0000000000..2d7c951f7d --- /dev/null +++ b/packages/seven/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/seven/app/config.ts b/packages/seven/app/config.ts new file mode 100644 index 0000000000..7f2026e538 --- /dev/null +++ b/packages/seven/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/seven/app/content.tsx b/packages/seven/app/content.tsx new file mode 100644 index 0000000000..8358a163e0 --- /dev/null +++ b/packages/seven/app/content.tsx @@ -0,0 +1,56 @@ +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) + ) + ) { + try { + return await getContent({ path, expand }); + } catch (error) { + throw data('Content Not Found', { status: 404 }); + } + } else { + console.log('matched path not fetched', path); + throw data('Content Not Found', { status: 404 }); + } +} + +export default function Content() { + const data = useLoaderData(); + const pathname = useLocation().pathname; + + return ; +} diff --git a/packages/seven/app/okroute.tsx b/packages/seven/app/okroute.tsx new file mode 100644 index 0000000000..56472e1bb6 --- /dev/null +++ b/packages/seven/app/okroute.tsx @@ -0,0 +1,5 @@ +export async function loader() { + return new Response(null, { + status: 200, + }); +} diff --git a/packages/seven/app/root.tsx b/packages/seven/app/root.tsx new file mode 100644 index 0000000000..ad5ae56a73 --- /dev/null +++ b/packages/seven/app/root.tsx @@ -0,0 +1,191 @@ +import { useState } from 'react'; +import { + isRouteErrorResponse, + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, + useHref, + useLocation, + useNavigate as useRRNavigate, + useParams, + useLoaderData, + useRouteLoaderData, +} from 'react-router'; +import type { Route } from './+types/root'; + +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 meta: Route.MetaFunction = () => [ + { name: 'generator', content: 'Plone 7 - https://plone.org' }, +]; + +export const links: Route.LinksFunction = () => [ + { + rel: 'icon', + href: '/favicon.png', + type: 'image/png', + sizes: 'any', + }, + { + rel: 'icon', + href: '/icon.svg', + type: 'image/svg+xml', + }, + { 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(); + const indexLoaderData = useRouteLoaderData('index'); + const contentLoaderData = useRouteLoaderData('content'); + const contentData = indexLoaderData || contentLoaderData; + + return ( + + + + + + + + + +