diff --git a/.github/workflows/sub_frontend_project.yml b/.github/workflows/sub_frontend_project.yml new file mode 100644 index 0000000..494b3fd --- /dev/null +++ b/.github/workflows/sub_frontend_project.yml @@ -0,0 +1,48 @@ +name: Plone Frontend Project CI +on: + push: + paths: + - "sub/frontend_project/**" + - ".github/workflows/sub_frontend_project.yml" + workflow_dispatch: + +env: + NODE_VERSION: 20.x + PYTHON_VERSION: "3.10" + +jobs: + + generation: + + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + python-version: + - "3.10" + - "3.11" + - "3.12" + + steps: + # git checkout + - name: Checkout codebase + uses: actions/checkout@v4 + + # python setup + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + + # python install + - name: Install dependencies + run: | + pip install -r requirements.txt + + # Test + - name: Run tests + run: | + cd sub/frontend_project + python -m pytest tests diff --git a/Makefile b/Makefile index a6becd0..67c0257 100644 --- a/Makefile +++ b/Makefile @@ -35,12 +35,14 @@ format: bin/cookieplone ## Format code bin/isort hooks .scripts $(MAKE) -C "./backend_addon/" format $(MAKE) -C "./frontend_addon/" format + $(MAKE) -C "./sub/frontend_project/" format .PHONY: test test: bin/cookieplone ## Test all cookiecutters @echo "$(GREEN)==> Test all cookiecutters$(RESET)" $(MAKE) -C "./backend_addon/" test $(MAKE) -C "./frontend_addon/" test + $(MAKE) -C "./sub/frontend_project/" test .PHONY: report-context report-context: bin/cookieplone ## Generate a report of all context options diff --git a/frontend_addon/hooks/pre_gen_project.py b/frontend_addon/hooks/pre_gen_project.py index 4597964..53fe3ad 100644 --- a/frontend_addon/hooks/pre_gen_project.py +++ b/frontend_addon/hooks/pre_gen_project.py @@ -23,9 +23,7 @@ def check_errors(context: dict) -> data.ContextValidatorResult: """Check for errors in the provided data.""" validations = [ - data.ItemValidator( - "frontend_addon_name", validators.validate_volto_addon_name - ), + data.ItemValidator("frontend_addon_name", validators.validate_volto_addon_name), data.ItemValidator("npm_package_name", validators.validate_npm_package_name), ] result = validators.run_context_validations(context, validations) diff --git a/frontend_addon/tests/conftest.py b/frontend_addon/tests/conftest.py index 07ade32..ac35b64 100644 --- a/frontend_addon/tests/conftest.py +++ b/frontend_addon/tests/conftest.py @@ -24,7 +24,7 @@ def context() -> dict: "email": "collective@plone.org", "github_organization": "collective", "npm_package_name": "@plone-collective/volto-addon", - "volto_version": "18.0.0-alpha.31" + "volto_version": "18.0.0-alpha.31", } diff --git a/sub/frontend_project/Makefile b/sub/frontend_project/Makefile new file mode 100644 index 0000000..49f5636 --- /dev/null +++ b/sub/frontend_project/Makefile @@ -0,0 +1,44 @@ +SHELL := /bin/bash +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` + +.PHONY: all +all: build + + +# Add the following 'help' target to your Makefile +# And add help text after each target name starting with '\#\#' +.PHONY: help +help: ## This help message + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +.PHONY: clean +clean: ## Clean + rm -rf volto-addon + +../../bin/cookieplone: ## cookieplone installation + $(MAKE) -C ".." bin/cookieplone + +.PHONY: format +format: ../../bin/cookieplone ## Format code + @echo "$(GREEN)==> Formatting codebase $(RESET)" + ../../bin/black hooks tests + ../../bin/isort hooks tests + +.PHONY: generate +generate: ../../bin/cookieplone ## Create a sample package + @echo "$(GREEN)==> Creating new test package$(RESET)" + rm -rf volto-addon + ../../bin/cookiecutter . --no-input + +.PHONY: test +test: ../../bin/cookieplone ## Create a sample package and tests it + @echo "$(GREEN)==> Creating new test package$(RESET)" + ../../bin/python -m pytest tests diff --git a/sub/frontend_project/cookiecutter.json b/sub/frontend_project/cookiecutter.json index da66518..b4d1e36 100644 --- a/sub/frontend_project/cookiecutter.json +++ b/sub/frontend_project/cookiecutter.json @@ -1,6 +1,8 @@ { "title": "Frontend Project", "volto_version": "{{ 'Yes' | latest_volto }}", + "author": "Plone Community", + "email": "collective@plone.org", "__folder_name": "app", "__gha_enable": true, "__version_plone_volto": "{{ cookiecutter.volto_version }}", @@ -11,7 +13,9 @@ "title": "Project name", "volto_version": "Volto version" }, - "_copy_without_render": [], + "_copy_without_render": [ + "_project_files" + ], "_extensions": [ "cookieplone.filters.latest_volto" ] diff --git a/sub/frontend_project/hooks/post_gen_project.py b/sub/frontend_project/hooks/post_gen_project.py index f718ef3..c242e58 100644 --- a/sub/frontend_project/hooks/post_gen_project.py +++ b/sub/frontend_project/hooks/post_gen_project.py @@ -9,10 +9,10 @@ context = {{cookiecutter}} -TO_REMOVE = [ - ".github", - "packages/volto-addon" -] +LOCAL_FILES_FOLDER_NAME = "_project_files" + + +TO_REMOVE = [".github", "packages/volto-addon"] def generate_addon(context, output_dir): @@ -21,12 +21,22 @@ def generate_addon(context, output_dir): output_dir = output_dir.parent context["frontend_addon_name"] = "volto-addon" generator.generate_subtemplate( - "volto_addon", output_dir, folder_name, context, TO_REMOVE + "../../frontend_addon", output_dir, folder_name, context, TO_REMOVE ) + def cleanup(context, output_dir): """Remove references to volto-addon.""" - pass + project_files_folder = output_dir / LOCAL_FILES_FOLDER_NAME + project_files: list[Path] = [path for path in project_files_folder.glob("*")] + filenames = [path.name for path in project_files] + # Remove old files + files.remove_files(output_dir, filenames) + for path in project_files: + name = path.name + path.rename(output_dir / name) + # Remove templates folder + files.remove_files(output_dir, [LOCAL_FILES_FOLDER_NAME]) def main(): @@ -51,5 +61,6 @@ def main(): url="https://plone.org/", ) + if __name__ == "__main__": main() diff --git a/sub/frontend_project/hooks/pre_gen_project.py b/sub/frontend_project/hooks/pre_gen_project.py index 573dea8..ef1e248 100644 --- a/sub/frontend_project/hooks/pre_gen_project.py +++ b/sub/frontend_project/hooks/pre_gen_project.py @@ -1,9 +1,10 @@ """Pre generation hook.""" import sys +from collections import OrderedDict # noQA from pathlib import Path from textwrap import dedent -from collections import OrderedDict # noQA + from cookieplone import data from cookieplone.utils import console, validators diff --git a/sub/frontend_project/{{ cookiecutter.__folder_name }}/.gitkeep b/sub/frontend_project/tests/__init__.py similarity index 100% rename from sub/frontend_project/{{ cookiecutter.__folder_name }}/.gitkeep rename to sub/frontend_project/tests/__init__.py diff --git a/sub/frontend_project/tests/conftest.py b/sub/frontend_project/tests/conftest.py new file mode 100644 index 0000000..9324280 --- /dev/null +++ b/sub/frontend_project/tests/conftest.py @@ -0,0 +1,50 @@ +"""Pytest configuration.""" + +import re +from copy import deepcopy +from pathlib import Path +from typing import List + +import pytest + + +@pytest.fixture(scope="session") +def variable_pattern(): + return re.compile("{{( ?cookiecutter)[.](.*?)}}") + + +@pytest.fixture(scope="session") +def context() -> dict: + """Cookiecutter context.""" + return { + "title": "Frontend project", + "author": "Plone Collective", + "email": "collective@plone.org", + "volto_version": "18.0.0-alpha.31", + } + + +@pytest.fixture(scope="session") +def bad_context() -> dict: + """Cookiecutter context with invalid data.""" + return { + "title": "Frontend project", + "author": "Plone Collective", + "email": "collective@plone.org", + "volto_version": "---", + } + + +@pytest.fixture +def build_files_list(): + def func(root_dir: Path) -> List[Path]: + """Build a list containing absolute paths to the generated files.""" + return [path for path in Path(root_dir).glob("*") if path.is_file()] + + return func + + +@pytest.fixture(scope="session") +def cutter_result(cookies_session, context): + """Cookiecutter result.""" + return cookies_session.bake(extra_context=context) diff --git a/sub/frontend_project/tests/test_cutter.py b/sub/frontend_project/tests/test_cutter.py new file mode 100644 index 0000000..38cf93c --- /dev/null +++ b/sub/frontend_project/tests/test_cutter.py @@ -0,0 +1,44 @@ +"""Test cookiecutter generation with all features enabled.""" + +from pathlib import Path + +import pytest + + +def test_creation(cookies, context: dict): + """Generated project should match provided value.""" + result = cookies.bake(extra_context=context) + assert result.exception is None + assert result.exit_code == 0 + assert result.project_path.name == "app" + assert result.project_path.is_dir() + + +def test_variable_substitution(build_files_list, variable_pattern, cutter_result): + """Check if no file was unprocessed.""" + paths = build_files_list(cutter_result.project_path) + for path in paths: + for line in open(path): + match = variable_pattern.search(line) + msg = f"cookiecutter variable not replaced in {path}" + assert match is None, msg + + +@pytest.mark.parametrize( + "file_path,text,expected", + [ + [".eslintrc.js", "volto-addon", False], + ["Makefile", "volto-addon", False], + ["package.json", "volto-addon-dev", False], + ["package.json", "project-dev", True], + ["volto.config.js", "volto-addon", False], + ], +) +def test_root_files_do_not_mention_addon( + cutter_result, file_path: Path, text: str, expected: bool +): + """Check if root files were generated and have no reference to the addon.""" + path = cutter_result.project_path / file_path + assert path.exists() + assert path.is_file() + assert (text in path.read_text()) is expected diff --git a/sub/frontend_project/{{ cookiecutter.__folder_name }}/_project_files/.eslintrc.js b/sub/frontend_project/{{ cookiecutter.__folder_name }}/_project_files/.eslintrc.js new file mode 100644 index 0000000..e9b8316 --- /dev/null +++ b/sub/frontend_project/{{ cookiecutter.__folder_name }}/_project_files/.eslintrc.js @@ -0,0 +1,30 @@ +const fs = require('fs'); +const projectRootPath = __dirname; + +let coreLocation; +if (fs.existsSync(`${projectRootPath}/core`)) + coreLocation = `${projectRootPath}/core`; +else if (fs.existsSync(`${projectRootPath}/../../core`)) + coreLocation = `${projectRootPath}/../../core`; + +module.exports = { + extends: `${coreLocation}/packages/volto/.eslintrc`, + rules: { + 'import/no-unresolved': 1, + }, + settings: { + 'import/resolver': { + alias: { + map: [ + ['@plone/volto', `${coreLocation}/packages/volto/src`], + [ + '@plone/volto-slate', + `${coreLocation}/core/packages/volto-slate/src`, + ], + ['@plone/registry', `${coreLocation}/packages/registry/src`], + ], + extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'], + }, + }, + }, +}; diff --git a/sub/frontend_project/{{ cookiecutter.__folder_name }}/_project_files/Makefile b/sub/frontend_project/{{ cookiecutter.__folder_name }}/_project_files/Makefile new file mode 100644 index 0000000..2f3c51c --- /dev/null +++ b/sub/frontend_project/{{ cookiecutter.__folder_name }}/_project_files/Makefile @@ -0,0 +1,114 @@ +### 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 + +CURRENT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) + +# Recipe snippets for reuse + +# 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` + +PLONE_VERSION=6 +DOCKER_IMAGE=plone/server-dev:${PLONE_VERSION} +DOCKER_IMAGE_ACCEPTANCE=plone/server-acceptance:${PLONE_VERSION} + + +.PHONY: help +help: ## Show this help + @echo -e "$$(grep -hE '^\S+:.*##' $(MAKEFILE_LIST) | sed -e 's/:.*##\s*/:/' -e 's/^\(.\+\):\(.*\)/\\x1b[36m\1\\x1b[m:\2/' | column -c2 -t -s :)" + +# Dev Helpers + +.PHONY: install +install: ## Installs the add-on in a development environment + pnpm dlx mrs-developer missdev --no-config --fetch-https + pnpm i + +.PHONY: start +start: ## Starts Volto, allowing reloading of the add-on during development + pnpm start + +.PHONY: build +build: ## Build a production bundle for distribution of the project with the add-on + pnpm build + +.PHONY: format +format: ## Format codebase + pnpm lint:fix + pnpm prettier:fix + pnpm stylelint:fix + +.PHONY: lint +lint: ## Lint, or catch and remove problems, in code base + pnpm lint + pnpm prettier + pnpm stylelint + +.PHONY: release +release: ## Release the add-on on npmjs.org + pnpm release + +.PHONY: release-dry-run +release-dry-run: ## Dry-run the release of the add-on on npmjs.org + pnpm release + +.PHONY: test +test: ## Run unit tests + pnpm test + +.PHONY: test-ci +ci-test: ## Run unit tests in CI + CI=1 RAZZLE_JEST_CONFIG=$(CURRENT_DIR)/jest-addon.config.js pnpm --filter @plone/volto test -- --passWithNoTests + +.PHONY: backend-docker-start +backend-docker-start: ## Starts a Docker-based backend for development + @echo "$(GREEN)==> Start Docker-based Plone Backend$(RESET)" + docker run -it --rm --name=backend -p 8080:8080 -e SITE=Plone $(DOCKER_IMAGE) + +## 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 Storybook + @echo "$(GREEN)==> Build Storybook$(RESET)" + mkdir -p $(CURRENT_DIR)/.storybook-build + pnpm run build-storybook -o $(CURRENT_DIR)/.storybook-build + +## Acceptance +.PHONY: acceptance-frontend-dev-start +acceptance-frontend-dev-start: ## Start acceptance frontend in development mode + RAZZLE_API_PATH=http://127.0.0.1:55001/plone pnpm start + +.PHONY: acceptance-frontend-prod-start +acceptance-frontend-prod-start: ## Start acceptance frontend in production mode + RAZZLE_API_PATH=http://127.0.0.1:55001/plone pnpm build && pnpm start:prod + +.PHONY: acceptance-backend-start +acceptance-backend-start: ## Start backend acceptance server + docker run -it --rm -p 55001:55001 $(DOCKER_IMAGE_ACCEPTANCE) + +.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) + +.PHONY: acceptance-test +acceptance-test: ## Start Cypress in interactive mode + pnpm --filter @plone/volto exec cypress open --config-file $(CURRENT_DIR)/cypress.config.js --config specPattern=$(CURRENT_DIR)'/cypress/tests/**/*.{js,jsx,ts,tsx}' + +.PHONY: ci-acceptance-test +ci-acceptance-test: ## Run cypress tests in headless mode for CI + pnpm --filter @plone/volto exec cypress run --config-file $(CURRENT_DIR)/cypress.config.js --config specPattern=$(CURRENT_DIR)'/cypress/tests/**/*.{js,jsx,ts,tsx}' diff --git a/sub/frontend_project/{{ cookiecutter.__folder_name }}/_project_files/README.md b/sub/frontend_project/{{ cookiecutter.__folder_name }}/_project_files/README.md new file mode 100644 index 0000000..9d3dea6 --- /dev/null +++ b/sub/frontend_project/{{ cookiecutter.__folder_name }}/_project_files/README.md @@ -0,0 +1 @@ +# Frontend Project diff --git a/sub/frontend_project/{{ cookiecutter.__folder_name }}/_project_files/package.json b/sub/frontend_project/{{ cookiecutter.__folder_name }}/_project_files/package.json new file mode 100644 index 0000000..5ecaa99 --- /dev/null +++ b/sub/frontend_project/{{ cookiecutter.__folder_name }}/_project_files/package.json @@ -0,0 +1,43 @@ +{ + "name": "project-dev", + "version": "1.0.0-alpha.0", + "description": "A new project with Plone & Volto", + "author": "Plone Foundation", + "homepage": "https://plone.org", + "license": "MIT", + "keywords": [ + "volto", + "plone", + "react" + ], + "scripts": { + "preinstall": "npx only-allow pnpm", + "start": "pnpm build:deps && VOLTOCONFIG=$(pwd)/volto.config.js pnpm --filter @plone/volto start", + "start:prod": "pnpm --filter @plone/volto start:prod", + "build": "pnpm build:deps && VOLTOCONFIG=$(pwd)/volto.config.js pnpm --filter @plone/volto build", + "build:deps": "pnpm --filter @plone/registry --filter @plone/components build", + "i18n": "pnpm --filter volto-addon i18n", + "test": "RAZZLE_JEST_CONFIG=$(pwd)/jest-addon.config.js pnpm --filter @plone/volto test", + "lint": "eslint --max-warnings=0 'packages/**/src/**/*.{js,jsx,ts,tsx}'", + "lint:fix": "eslint --fix 'packages/**/src/**/*.{js,jsx,ts,tsx}'", + "prettier": "prettier --check 'packages/**/src/**/*.{js,jsx,ts,tsx}'", + "prettier:fix": "prettier --write 'packages/**/src/**/*.{js,jsx,ts,tsx}' ", + "stylelint": "stylelint 'packages/**/src/**/*.{css,scss,less}' --allow-empty-input", + "stylelint:fix": "stylelint 'packages/**/src/**/*.{css,scss,less}' --fix --allow-empty-input", + "dry-release": "pnpm --filter volto-addon dry-release", + "release": "pnpm --filter volto-addon release", + "release-major-alpha": "pnpm --filter volto-addon release-major-alpha", + "release-alpha": "pnpm --filter volto-addon release-alpha", + "storybook": "pnpm build:deps && VOLTOCONFIG=$(pwd)/volto.config.js pnpm --filter @plone/volto storybook dev -p 6006 -c $(pwd)/.storybook", + "build-storybook": "pnpm build:deps && VOLTOCONFIG=$(pwd)/volto.config.js pnpm --filter @plone/volto build-storybook -c $(pwd)/.storybook" + }, + "dependencies": { + "@plone/volto": "workspace:*", + "@plone/registry": "workspace:*", + "volto-addon": "workspace:*" + }, + "devDependencies": { + "mrs-developer": "^2.2.0" + }, + "packageManager": "pnpm@9.1.1" +} diff --git a/sub/frontend_project/{{ cookiecutter.__folder_name }}/_project_files/volto.config.js b/sub/frontend_project/{{ cookiecutter.__folder_name }}/_project_files/volto.config.js new file mode 100644 index 0000000..1f1b16c --- /dev/null +++ b/sub/frontend_project/{{ cookiecutter.__folder_name }}/_project_files/volto.config.js @@ -0,0 +1,7 @@ +const addons = []; +const theme = ''; + +module.exports = { + addons, + theme, +};