diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a8c60c7..50dc2fb 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -33,6 +33,7 @@ jobs: - cppjswasm - uitk-svelte - uitk-webawesome + - site-react - site-sveltekit - site-webawesome os: @@ -43,6 +44,8 @@ jobs: os: macos-latest - template: uitk-webawesome os: macos-latest + - template: site-react + os: macos-latest - template: site-sveltekit os: macos-latest - template: site-webawesome @@ -92,7 +95,7 @@ jobs: version: 22.x pnpm_version: 11.11.0 js_folder: "." - if: matrix.template == 'js' || matrix.template == 'jupyter' || matrix.template == 'cppjswasm' || matrix.template == 'rustjswasm' || matrix.template == 'uitk-svelte' || matrix.template == 'uitk-webawesome' || matrix.template == 'site-sveltekit' || matrix.template == 'site-webawesome' + if: matrix.template == 'js' || matrix.template == 'jupyter' || matrix.template == 'cppjswasm' || matrix.template == 'rustjswasm' || matrix.template == 'uitk-svelte' || matrix.template == 'uitk-webawesome' || matrix.template == 'site-react' || matrix.template == 'site-sveltekit' || matrix.template == 'site-webawesome' - name: Setup Rust uses: actions-ext/rust/setup@2db8e3e4c56beb7320c7d730bd2c2d6a6447bf6b @@ -114,7 +117,7 @@ jobs: cd ../javascript-template-${{ matrix.template }} pnpm install pnpm exec playwright install --with-deps chromium - if: matrix.template == 'uitk-svelte' || matrix.template == 'uitk-webawesome' || matrix.template == 'site-sveltekit' || matrix.template == 'site-webawesome' + if: matrix.template == 'uitk-svelte' || matrix.template == 'uitk-webawesome' || matrix.template == 'site-react' || matrix.template == 'site-sveltekit' || matrix.template == 'site-webawesome' - name: Test template run: make test-${{ matrix.template }} diff --git a/Makefile b/Makefile index 762c921..4ceb72b 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ fix: ## fix formatting in this repo format: fix test: ## run tests for this repo -.PHONY: gen-python gen-cpp gen-js gen-jupyter gen-rust gen-rustjswasm gen-cppjswasm gen-uitk-svelte gen-uitk-webawesome gen-site-sveltekit gen-site-webawesome +.PHONY: gen-python gen-cpp gen-js gen-jupyter gen-rust gen-rustjswasm gen-cppjswasm gen-uitk-svelte gen-uitk-webawesome gen-site-react gen-site-sveltekit gen-site-webawesome gen-python: ## regenerate the python template from scratch mkdir -p ../python-template && cd ../python-template && rm -rf ./* && rm -rf .copier-answers.yaml .gitignore .github .gitattributes copier copy -w . ../python-template --data-file examples/python.yaml @@ -68,6 +68,10 @@ gen-uitk-webawesome: ## regenerate the Lit and Web Awesome UI toolkit template mkdir -p ../javascript-template-uitk-webawesome && cd ../javascript-template-uitk-webawesome && rm -rf ./* && rm -rf .copier-answers.yaml .github .gitignore .prettierignore .prettierrc .oxfmtrc.json .oxlintrc.json copier copy -w . ../javascript-template-uitk-webawesome --data-file examples/uitk-webawesome.yaml +gen-site-react: ## regenerate the React site template from scratch + mkdir -p ../javascript-template-site-react && cd ../javascript-template-site-react && rm -rf ./* && rm -rf .copier-answers.yaml .github .gitignore .oxfmtrc.json .oxlintrc.json + copier copy -w . ../javascript-template-site-react --data-file examples/site-react.yaml + gen-site-sveltekit: ## regenerate the SvelteKit site template from scratch mkdir -p ../javascript-template-site-sveltekit && cd ../javascript-template-site-sveltekit && rm -rf ./* && rm -rf .copier-answers.yaml .github .gitignore .prettierignore .prettierrc .oxfmtrc.json .oxlintrc.json copier copy -w . ../javascript-template-site-sveltekit --data-file examples/site-sveltekit.yaml @@ -76,7 +80,7 @@ gen-site-webawesome: ## regenerate the Web Awesome site template from scratch mkdir -p ../javascript-template-site-webawesome && cd ../javascript-template-site-webawesome && rm -rf ./* && rm -rf .copier-answers.yaml .github .gitignore .prettierignore .prettierrc .oxfmtrc.json .oxlintrc.json copier copy -w . ../javascript-template-site-webawesome --data-file examples/site-webawesome.yaml -.PHONY: test-python test-cpp test-js test-jupyter test-rust test-rustjswasm test-cppjswasm test-uitk-svelte test-uitk-webawesome test-site-sveltekit test-site-webawesome +.PHONY: test-python test-cpp test-js test-jupyter test-rust test-rustjswasm test-cppjswasm test-uitk-svelte test-uitk-webawesome test-site-react test-site-sveltekit test-site-webawesome test-python: cd ../python-template && git config --global user.name "github-actions" && git config --global user.email "41898282+github-actions[bot]@users.noreply.github.c@example.com" && git init && git add . && git commit -m "initial commit" cd ../python-template && make develop @@ -150,6 +154,15 @@ test-uitk-webawesome: cd ../javascript-template-uitk-webawesome && pnpm test:unit cd ../javascript-template-uitk-webawesome && CI=1 pnpm test:e2e +test-site-react: + cd ../javascript-template-site-react && pnpm install + cd ../javascript-template-site-react && pnpm exec playwright install chromium + cd ../javascript-template-site-react && pnpm lint + cd ../javascript-template-site-react && pnpm check + cd ../javascript-template-site-react && pnpm build + cd ../javascript-template-site-react && pnpm test:unit + cd ../javascript-template-site-react && CI=1 pnpm test:e2e + test-site-sveltekit: cd ../javascript-template-site-sveltekit && pnpm install cd ../javascript-template-site-sveltekit && pnpm exec playwright install chromium diff --git a/README.md b/README.md index f010c51..513d45c 100644 --- a/README.md +++ b/README.md @@ -19,5 +19,6 @@ One selector chooses a template: - `Python / …` options generate pure Python, C++, JavaScript, Jupyter, Rust, and WebAssembly-backed Python packages from `python/`. - `JavaScript / UI Toolkit / Svelte` generates a Svelte package and showcase from `javascript/uitk-svelte/`. - `JavaScript / UI Toolkit / Lit + Web Awesome` generates custom elements and a showcase from `javascript/uitk-webawesome/`. +- `JavaScript / Site / React` generates a React site with Vite, Vitest, and Playwright from `javascript/site-react/`. - `JavaScript / Site / SvelteKit` generates a SvelteKit site with static or Node deployment from `javascript/site-sveltekit/`. - `JavaScript / Site / Web Awesome` generates a Vite site built with Web Awesome components from `javascript/site-webawesome/`. diff --git a/copier.yaml b/copier.yaml index 582c6b3..5cd5885 100644 --- a/copier.yaml +++ b/copier.yaml @@ -29,12 +29,12 @@ github: team: type: str help: What is your name or your team's name? - when: "{{ add_extension not in ['uitk-svelte', 'uitk-webawesome', 'site-sveltekit', 'site-webawesome'] }}" + when: "{{ add_extension not in ['uitk-svelte', 'uitk-webawesome', 'site-react', 'site-sveltekit', 'site-webawesome'] }}" email: type: str help: What is your email? - when: "{{ add_extension not in ['uitk-svelte', 'uitk-webawesome', 'site-sveltekit', 'site-webawesome'] }}" + when: "{{ add_extension not in ['uitk-svelte', 'uitk-webawesome', 'site-react', 'site-sveltekit', 'site-webawesome'] }}" add_extension: type: str @@ -60,10 +60,12 @@ add_extension: value: uitk-webawesome JavaScript / Site / SvelteKit: value: site-sveltekit + JavaScript / Site / React: + value: site-react JavaScript / Site / Web Awesome: value: site-webawesome -_subdirectory: "{% if add_extension == 'python' %}python/pure{% elif add_extension in ['uitk-svelte', 'uitk-webawesome', 'site-sveltekit', 'site-webawesome'] %}javascript/{{ add_extension }}{% else %}python/{{ add_extension }}{% endif %}" +_subdirectory: "{% if add_extension == 'python' %}python/pure{% elif add_extension in ['uitk-svelte', 'uitk-webawesome', 'site-react', 'site-sveltekit', 'site-webawesome'] %}javascript/{{ add_extension }}{% else %}python/{{ add_extension }}{% endif %}" add_vcpkg: type: bool @@ -90,19 +92,19 @@ python_version_primary: "3.13": value: "3.13" help: Primary version of Python to use in CI/CD - when: "{{ add_extension not in ['uitk-svelte', 'uitk-webawesome', 'site-sveltekit', 'site-webawesome'] }}" + when: "{{ add_extension not in ['uitk-svelte', 'uitk-webawesome', 'site-react', 'site-sveltekit', 'site-webawesome'] }}" add_docs: type: bool default: false help: Add documentation using Yardang/Sphinx - when: "{{ add_extension not in ['uitk-svelte', 'uitk-webawesome', 'site-sveltekit', 'site-webawesome'] }}" + when: "{{ add_extension not in ['uitk-svelte', 'uitk-webawesome', 'site-react', 'site-sveltekit', 'site-webawesome'] }}" add_wiki: type: bool default: false help: Add documentation using GitHub wiki - when: "{{ add_extension not in ['uitk-svelte', 'uitk-webawesome', 'site-sveltekit', 'site-webawesome'] }}" + when: "{{ add_extension not in ['uitk-svelte', 'uitk-webawesome', 'site-react', 'site-sveltekit', 'site-webawesome'] }}" package_name: type: str @@ -114,7 +116,7 @@ brand_name: type: str help: What is the human-readable brand name? default: "{{ project_name }}" - when: "{{ add_extension in ['uitk-svelte', 'uitk-webawesome', 'site-sveltekit', 'site-webawesome'] }}" + when: "{{ add_extension in ['uitk-svelte', 'uitk-webawesome', 'site-react', 'site-sveltekit', 'site-webawesome'] }}" css_prefix: type: str @@ -132,7 +134,7 @@ site_url: type: str help: What is the site's canonical URL? default: https://example.com - when: "{{ add_extension in ['site-sveltekit', 'site-webawesome'] }}" + when: "{{ add_extension in ['site-react', 'site-sveltekit', 'site-webawesome'] }}" site_adapter: type: str diff --git a/examples/site-react.yaml b/examples/site-react.yaml new file mode 100644 index 0000000..4c19ce5 --- /dev/null +++ b/examples/site-react.yaml @@ -0,0 +1,7 @@ +--- +add_extension: site-react +brand_name: Example +github: example +project_description: A fast, accessible React website. +project_name: example-site +site_url: https://example.com diff --git a/javascript/site-react/.github/dependabot.yaml.jinja b/javascript/site-react/.github/dependabot.yaml.jinja new file mode 100644 index 0000000..fb56892 --- /dev/null +++ b/javascript/site-react/.github/dependabot.yaml.jinja @@ -0,0 +1,18 @@ +version: 2 +updates: + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'monthly' + labels: + - 'part: github_actions' + + - package-ecosystem: 'npm' + directory: '/' + schedule: + interval: 'monthly' + cooldown: + default-days: 7 + labels: + - 'lang: javascript' + - 'part: dependencies' diff --git a/javascript/site-react/.github/workflows/build.yaml.jinja b/javascript/site-react/.github/workflows/build.yaml.jinja new file mode 100644 index 0000000..52b282f --- /dev/null +++ b/javascript/site-react/.github/workflows/build.yaml.jinja @@ -0,0 +1,35 @@ +name: Build Status + +on: + push: + branches: [main] + tags: [v*] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions-ext/node/setup@b0536d87c5e92c924bc8667f566f620758280825 + with: + version: 22.x + js_folder: . + - run: pnpm install + - run: pnpm build + - run: pnpm check + - run: pnpm lint + - run: pnpm test:unit + - run: pnpm exec playwright install --with-deps chromium + - run: pnpm test:e2e + env: + CI: '1' diff --git a/javascript/site-react/.gitignore.jinja b/javascript/site-react/.gitignore.jinja new file mode 100644 index 0000000..4221f7a --- /dev/null +++ b/javascript/site-react/.gitignore.jinja @@ -0,0 +1,9 @@ +node_modules +/dist +/coverage +/playwright-report +/test-results +.env +.env.* +!.env.example +.DS_Store diff --git a/javascript/site-react/.oxfmtrc.json.jinja b/javascript/site-react/.oxfmtrc.json.jinja new file mode 100644 index 0000000..1eab9a6 --- /dev/null +++ b/javascript/site-react/.oxfmtrc.json.jinja @@ -0,0 +1,10 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "printWidth": 120, + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "none", + "sortPackageJson": false, + "ignorePatterns": [".copier-answers.yaml", "coverage/**", "dist/**", "playwright-report/**", "test-results/**"] +} diff --git a/javascript/site-react/.oxlintrc.json.jinja b/javascript/site-react/.oxlintrc.json.jinja new file mode 100644 index 0000000..b521a88 --- /dev/null +++ b/javascript/site-react/.oxlintrc.json.jinja @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "env": { + "browser": true, + "es6": true, + "node": true + } +} diff --git a/javascript/site-react/README.md.jinja b/javascript/site-react/README.md.jinja new file mode 100644 index 0000000..8497b4b --- /dev/null +++ b/javascript/site-react/README.md.jinja @@ -0,0 +1,22 @@ +# {{ brand_name }} + +{{ project_description }} + +## Develop + +```bash +pnpm install +pnpm dev +``` + +## Verify + +```bash +pnpm check +pnpm lint +pnpm test +pnpm build +``` + +The application uses React with Vite and TypeScript. Vitest covers component +behavior, while Playwright and axe cover browser behavior and accessibility. diff --git a/javascript/site-react/index.html.jinja b/javascript/site-react/index.html.jinja new file mode 100644 index 0000000..e0e1582 --- /dev/null +++ b/javascript/site-react/index.html.jinja @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + {{ brand_name }} + + +
+ + + diff --git a/javascript/site-react/package.json.jinja b/javascript/site-react/package.json.jinja new file mode 100644 index 0000000..9c908f7 --- /dev/null +++ b/javascript/site-react/package.json.jinja @@ -0,0 +1,43 @@ +{ + "name": "{{ project_name_formatted }}", + "version": "0.1.0", + "description": "{{ project_description }}", + "private": true, + "packageManager": "pnpm@11.9.0", + "engines": { + "node": ">=22" + }, + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "check": "tsc --noEmit", + "lint": "oxlint . && oxfmt --check .", + "fix": "oxlint --fix . && oxfmt --write .", + "test": "pnpm test:unit && pnpm test:e2e", + "test:unit": "vitest run", + "test:e2e": "playwright test", + "test:e2e:update": "playwright test --update-snapshots" + }, + "dependencies": { + "react": "^19.2.8", + "react-dom": "^19.2.8" + }, + "devDependencies": { + "@axe-core/playwright": "^4.12.1", + "@playwright/test": "^1.60.0", + "@testing-library/jest-dom": "^7.0.1", + "@testing-library/react": "16.3.2", + "@types/node": "^24.0.0", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.5", + "@vitejs/plugin-react": "^6.1.0", + "jsdom": "^30.0.1", + "oxfmt": "^0.63.0", + "oxlint": "^1.78.0", + "typescript": "^5.9.3", + "vite": "^8.1.5", + "vitest": "^4.1.8" + } +} diff --git a/javascript/site-react/playwright.config.ts.jinja b/javascript/site-react/playwright.config.ts.jinja new file mode 100644 index 0000000..8165435 --- /dev/null +++ b/javascript/site-react/playwright.config.ts.jinja @@ -0,0 +1,26 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: 'tests', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + reporter: process.env.CI ? 'github' : 'list', + use: { + baseURL: 'http://127.0.0.1:4177', + trace: 'retain-on-failure', + screenshot: 'only-on-failure' + }, + webServer: { + command: 'vite build && vite preview --host 127.0.0.1 --port 4177 --strictPort', + port: 4177, + reuseExistingServer: !process.env.CI, + timeout: 120_000 + }, + projects: [ + { + name: 'desktop', + use: { ...devices['Desktop Chrome'], viewport: { width: 1280, height: 800 } } + } + ] +}); diff --git a/javascript/site-react/src/App.test.tsx.jinja b/javascript/site-react/src/App.test.tsx.jinja new file mode 100644 index 0000000..1f69e2e --- /dev/null +++ b/javascript/site-react/src/App.test.tsx.jinja @@ -0,0 +1,12 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import App from './App'; + +describe('App', () => { + it('introduces the site', () => { + render(); + + expect(screen.getByRole('heading', { name: '{{ brand_name }}' })).toBeInTheDocument(); + expect(screen.getByText('{{ project_description }}')).toBeInTheDocument(); + }); +}); diff --git a/javascript/site-react/src/App.tsx.jinja b/javascript/site-react/src/App.tsx.jinja new file mode 100644 index 0000000..29e82d4 --- /dev/null +++ b/javascript/site-react/src/App.tsx.jinja @@ -0,0 +1,12 @@ +export default function App() { + return ( +
+

{{ brand_name }}

+

{{ brand_name }}

+

{{ project_description }}

+ + Get started + +
+ ); +} diff --git a/javascript/site-react/src/main.tsx.jinja b/javascript/site-react/src/main.tsx.jinja new file mode 100644 index 0000000..5b585d3 --- /dev/null +++ b/javascript/site-react/src/main.tsx.jinja @@ -0,0 +1,10 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; +import './styles.css'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/javascript/site-react/src/styles.css.jinja b/javascript/site-react/src/styles.css.jinja new file mode 100644 index 0000000..64aced2 --- /dev/null +++ b/javascript/site-react/src/styles.css.jinja @@ -0,0 +1,44 @@ +:root { + color: #171717; + background: #ffffff; + font-family: system-ui, sans-serif; +} + +body { + margin: 0; +} + +main { + width: min(calc(100% - 3rem), 64rem); + margin: 0 auto; + padding: 6rem 0; +} + +.eyebrow { + font-size: 0.75rem; + letter-spacing: 0.14em; + text-transform: uppercase; +} + +h1 { + margin: 2rem 0 1rem; + font-size: clamp(3rem, 10vw, 8rem); + line-height: 0.9; + letter-spacing: -0.06em; +} + +main > p:not(.eyebrow) { + max-width: 42rem; + margin-bottom: 2rem; + font-size: 1.25rem; + line-height: 1.6; +} + +.button { + display: inline-block; + padding: 0.75rem 1rem; + color: #ffffff; + background: #171717; + border-radius: 0.35rem; + text-decoration: none; +} diff --git a/javascript/site-react/tests/a11y/audit.spec.ts.jinja b/javascript/site-react/tests/a11y/audit.spec.ts.jinja new file mode 100644 index 0000000..26877bd --- /dev/null +++ b/javascript/site-react/tests/a11y/audit.spec.ts.jinja @@ -0,0 +1,10 @@ +import AxeBuilder from '@axe-core/playwright'; +import { expect, test } from '@playwright/test'; + +test('home page has no automatically detectable accessibility violations', async ({ page }) => { + await page.goto('/'); + await expect(page).toHaveTitle('{{ brand_name }}'); + + const results = await new AxeBuilder({ page }).analyze(); + expect(results.violations).toEqual([]); +}); diff --git a/javascript/site-react/tsconfig.json.jinja b/javascript/site-react/tsconfig.json.jinja new file mode 100644 index 0000000..929384e --- /dev/null +++ b/javascript/site-react/tsconfig.json.jinja @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "jsx": "react-jsx" + }, + "include": ["src", "tests", "playwright.config.ts", "vite.config.ts", "vitest.config.ts", "vitest.setup.ts"] +} diff --git a/javascript/site-react/vite.config.ts.jinja b/javascript/site-react/vite.config.ts.jinja new file mode 100644 index 0000000..6ff59fc --- /dev/null +++ b/javascript/site-react/vite.config.ts.jinja @@ -0,0 +1,6 @@ +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [react()] +}); diff --git a/javascript/site-react/vitest.config.ts.jinja b/javascript/site-react/vitest.config.ts.jinja new file mode 100644 index 0000000..986af83 --- /dev/null +++ b/javascript/site-react/vitest.config.ts.jinja @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['src/**/*.test.ts', 'src/**/*.test.tsx'], + environment: 'jsdom', + setupFiles: ['./vitest.setup.ts'] + } +}); diff --git a/javascript/site-react/vitest.setup.ts.jinja b/javascript/site-react/vitest.setup.ts.jinja new file mode 100644 index 0000000..bb02c60 --- /dev/null +++ b/javascript/site-react/vitest.setup.ts.jinja @@ -0,0 +1 @@ +import '@testing-library/jest-dom/vitest'; diff --git a/javascript/site-react/{{_copier_conf.answers_file}}.jinja b/javascript/site-react/{{_copier_conf.answers_file}}.jinja new file mode 100644 index 0000000..a96840d --- /dev/null +++ b/javascript/site-react/{{_copier_conf.answers_file}}.jinja @@ -0,0 +1,2 @@ +# Changes here will be overwritten by Copier +{{ _copier_answers|to_nice_yaml -}}