diff --git a/.gitignore b/.gitignore index b2df8b8..d2ab412 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ coverage # Build Outputs .next/ out/ +my-project/ build dist /.cache diff --git a/AGENTS.md b/AGENTS.md index c7164eb..4284a0c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,19 +3,19 @@ Refer to the README.md for information about this repository. ## Project Structure & Module Organization -`create/` contains the Node-based scaffold (`index.js` normalizes CLI options, `main.js` mutates the template). `template/` ships the starter monorepo with workspaces such as `api/` for backend endpoints, `mock-backend/` and `mock-data/` for fixtures, `prototype/` for the prototype app, `ui/` for shared components, and `docs/` for the Antora documentation site. Shared lint and TS configs live in `template/config/`, and bootstrap assets for generated apps sit under `template`. +`create/` contains the Node-based scaffold (`index.js` normalizes CLI options, `main.js` mutates the template). `template/` ships the default starter monorepo with workspaces such as `api/`, `mocks/`, `msw/`, `prototype/`, `ui/`, and `docs/`. `example-templates/` contains optional independent examples that can be copied into generated projects under `examples//`. Shared lint and TS configs live in `template/config/`. ## Build, Test & Development Commands Use Node 22+. Run `npm install` at the repo root before touching the CLI. Template work happens inside `template/`: `npm install` for dependencies, `npm run dev` to launch Turbo-powered development, `npm run build` for a production check, `npm run typecheck` for repository-wide TypeScript validation, `npm run test` to execute Vitest suites, and `npm run format` to apply Prettier across Markdown and TypeScript files. ## Coding Style & Naming Conventions -Prettier enforces two-space indentation, single quotes, and trailing commas (`npm run format`). Keep imports auto-organized by the Prettier organize-imports plugin. Use `camelCase` for variables and functions, `PascalCase` for React components and types, and kebab-case for file names (e.g., `generate-data.mjs`). ESLint rules from `template/config/eslint/` run in every workspace—resolve warnings or document exceptions in-code. +Prettier enforces two-space indentation, single quotes, and trailing commas (`npm run format`). Keep imports auto-organized by the Prettier organize-imports plugin. Use `camelCase` for variables and functions, `PascalCase` for React components and types, and kebab-case for file names (e.g. `generate-data.mjs`). ESLint rules from `template/config/eslint/` run in every workspace, so resolve warnings or document exceptions in-code. ## Testing Guidelines -Vitest handles unit and integration coverage; colocate specs as `*.test.ts` or `*.spec.ts`. Run `npm run test` for the full suite, or target a package with `npm run test -- --filter=@repo/ui`. Always follow tests with `npm run typecheck` before opening a PR, and extend coverage around generators (`mock-data/cli`) and UI behavior when adding features. +Vitest handles unit and integration coverage; colocate specs as `*.test.ts` or `*.spec.ts`. Run `npm run test` for the full suite, or target a package with `npm run test -- --filter=@repo/ui`. Always follow tests with `npm run typecheck` before opening a PR. When changing the scaffold, cover both the default starter and any affected example templates. ## Commit & Pull Request Guidelines Mirror the existing history by writing concise, imperative subjects (`add build verification for Github`). Group logically related changes per commit. Pull requests must include a summary, testing notes (`npm run build`, `npm run test`, etc.), linked issues when applicable, and screenshots for UI updates. ## Setup & Configuration Notes -The scaffold copies `.env.example` to `.env` and swaps `planning-stack-template` tokens; keep those references in sync when editing template assets. Honor the `SKIP_SETUP` and `SKIP_FORMAT` flags in `create/main.js` so automated flows remain consistent. Update every consumer (e.g., `template/gitlab-ci.yml`) when adjusting CI scaffolding. +The scaffold copies `.env.example` to `.env` and swaps `planning-stack-template` tokens in the base template. Honor the `SKIP_SETUP` and `SKIP_FORMAT` flags in `create/main.js` so automated flows remain consistent. Keep example templates independent from the base workspace so they can be deleted safely and are not built automatically by the root Turborepo configuration. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..eef4bd2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b908e72..8164503 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,10 +1,34 @@ - ## Local testing To verify the generator before publishing: -1. Run `npm pack` in this repository to produce a tarball (for example `create-tinker-stack-0.2.6.tgz`). -2. In a temporary directory, execute `npx --yes create-tinker-stack@file://$(pwd)/create-tinker-stack-0.2.6.tgz`. - - Replace the path with the absolute path to the tarball from step 1 (npm `create` does not currently support `file:` specifiers). +1. Run `npm pack` in this repository to produce a tarball (for example `create-tinker-stack-0.2.8.tgz`). +2. Run the generator from this repository, passing a target directory as a positional argument: + +```bash +npx --yes create-tinker-stack@file://$(pwd)/create-tinker-stack-0.2.8.tgz my-project +``` + +This scaffolds a clean starter into `./my-project`. + +### With the example bundle + +Pass `--with-example` to also generate the `react-router` example, or name a specific example with `--example`: + +```bash +# Default example (react-router) +npx --yes create-tinker-stack@file://$(pwd)/create-tinker-stack-0.2.8.tgz my-project --with-example + +# Named example +npx --yes create-tinker-stack@file://$(pwd)/create-tinker-stack-0.2.8.tgz my-project --example react-router +``` + +The example is placed in `my-project/examples/react-router/` and is independent of the root workspace. + +### Notes + +- `$(pwd)` must be run from the repository root where the tarball was produced. +- `npm create` does not currently support `file:` specifiers, so `npx` must be used. +- Omit the target directory argument to scaffold into the current working directory. diff --git a/README.md b/README.md index 573b7c5..fd8a702 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,41 @@ -# Frontend Monorepo for Rapid Prototyping +# Tinker Stack -This is a npm Template for a Frontend Monorepo. +Tinker Stack is a project scaffold for frontend monorepos focused on rapid prototyping. -It puts emphasis on rapid prototyping and a prototype-driven development -([Pixar Planning](https://www.youtube.com/watch?v=gbuWJ48T0bE&t=1294s)). +By default it generates a clean starter workspace with: -The prototypes are using a virtual and ephemeral backend, so changes can be made very quickly without any need for -data migrations on schema changes. +- a minimal React Router prototype shell +- shared `api`, `ui`, `msw`, and `mocks` packages +- Turborepo, TypeScript, ESLint, Prettier, and Antora wiring +- every example template copied into `examples//` -Stakeholders can test a fully working UI before a single line of backend code has been written. +The scaffold no longer ships a demo application in the base workspace. -Test UI concepts on day 1, iterate on them with the designer on day 2 and present them to the customer on day 3. +## Create a project -The API to the backend has usually had several major iterations before a backend developer even gets involved. - -## What is included? - -This monorepo contains the following packages/apps: - -### Apps and Packages +```bash +npm create tinker-stack@latest +``` -- A prototype for the application, based on React Router SPA. -- A package that provides the domain types and enums that can be shared with the backend. -- A documentation in [Antora](https://antora.org/) (AsciiDoc) format. -- A package that provides synthetic data for the applications using - [Faker.js](https://fakerjs.dev/). -- A package that provides a mock API via service workers using [MSW](https://mswjs.io/). -- A component library that is shared by the main application and the prototype (still empty). -- ESLint and TypeScript configurations that are shared throughout the monorepo. +To generate the base workspace without any example folders: -### Features +```bash +npm create tinker-stack@latest -- --no-examples +``` -- [TypeScript](https://www.typescriptlang.org/) Monorepo based on ESM standards. -- Turborepo setup for building and running the monorepo. -- Code formatting with [Prettier](https://prettier.io/) and linting with - [ESLint](https://eslint.org/). +To generate only specific example templates: -## Documentation +```bash +npm create tinker-stack@latest -- --example react-router +``` -The project documentation is written in [AsciiDoc](https://asciidoctor.org/) and is generated using -[Antora](https://antora.org/). +By default, all available examples are copied into `examples//` inside the generated project. +They are self-contained and can be deleted without affecting the main workspace. -## Getting Started +## Example templates -To get started, run the following command: +The generator is designed for multiple named examples. Today the repository includes: -```bash -npm create tinker-stack@latest -``` +- `react-router` -This will create a new project based on the Tinker Stack. +Each example is installed and run separately from its own directory. The root project does not add example folders to its npm workspaces, so Turborepo ignores them by default. diff --git a/create/index.js b/create/index.js index 5713a21..91e2cc8 100755 --- a/create/index.js +++ b/create/index.js @@ -8,30 +8,126 @@ import { fileURLToPath } from 'node:url'; const __filename = fileURLToPath(import.meta.url); const packageRoot = path.dirname(path.dirname(__filename)); const templateDir = path.join(packageRoot, 'template'); +const exampleTemplatesDir = path.join(packageRoot, 'example-templates'); +const DEFAULT_EXAMPLE = 'react-router'; -function targetFromArgv() { - const argv = process.argv.slice(2); - // prefer a last non-flag positional argument as the target folder - for (let i = argv.length - 1; i >= 0; i--) { - if (!argv[i].startsWith('-')) return argv[i]; +function parseCliArgs(argv = process.argv.slice(2)) { + const options = { + cwd: undefined, + debug: undefined, + install: undefined, + examples: [], + noExamples: false, + withExample: false + }; + const positionals = []; + + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + + if (arg === '--example' || arg === '--examples') { + const value = argv[i + 1]; + if (value && !value.startsWith('-')) { + options.examples.push(...splitExampleNames(value)); + i += 1; + } + continue; + } + + if (arg.startsWith('--example=')) { + options.examples.push(...splitExampleNames(arg.slice('--example='.length))); + continue; + } + + if (arg.startsWith('--examples=')) { + options.examples.push(...splitExampleNames(arg.slice('--examples='.length))); + continue; + } + + if (arg === '--with-example') { + options.withExample = true; + continue; + } + + if (arg === '--no-examples') { + options.noExamples = true; + continue; + } + + if (arg === '--cwd') { + const value = argv[i + 1]; + if (value && !value.startsWith('-')) { + options.cwd = value; + i += 1; + } + continue; + } + + if (arg.startsWith('--cwd=')) { + options.cwd = arg.slice('--cwd='.length); + continue; + } + + if (arg === '--debug') { + options.debug = true; + continue; + } + + if (arg === '--install') { + options.install = true; + continue; + } + + if (arg === '--no-install') { + options.install = false; + continue; + } + + if (!arg.startsWith('-')) { + positionals.push(arg); + } + } + + return { + ...options, + positionals + }; +} + +function splitExampleNames(value) { + return value + .split(',') + .map(example => example.trim()) + .filter(Boolean); +} + +function normalizeExampleNames(value) { + if (Array.isArray(value)) { + return value.flatMap(example => normalizeExampleNames(example)); } - return undefined; + + if (typeof value === 'string') { + return splitExampleNames(value); + } + + return []; } function normalizeOptions(input = {}) { // input may be an options object passed by npm init or undefined let opts = {}; if (typeof input === 'object' && input !== null) opts = input; + const cli = parseCliArgs(); const targetDir = opts.targetDirectory || opts.name || // common field name for npm init opts.project || - targetFromArgv(); + cli.positionals.at(-1); const resolvedCwd = - typeof opts.cwd === 'string' && opts.cwd.length > 0 - ? path.resolve(process.cwd(), opts.cwd) + typeof (opts.cwd ?? cli.cwd) === 'string' && (opts.cwd ?? cli.cwd).length > 0 + ? path.resolve(process.cwd(), opts.cwd ?? cli.cwd) : process.cwd(); const resolvedTargetDir = @@ -39,16 +135,46 @@ function normalizeOptions(input = {}) { ? path.resolve(resolvedCwd, targetDir) : undefined; + const explicitExamples = [ + ...normalizeExampleNames(opts.examples), + ...normalizeExampleNames(opts.example), + ...normalizeExampleNames(cli.examples) + ]; + + if (opts.withExample === true || cli.withExample) { + explicitExamples.push(DEFAULT_EXAMPLE); + } + + const noExamples = Boolean(opts.noExamples ?? cli.noExamples); + const hasExplicitExamples = explicitExamples.length > 0; + const examples = hasExplicitExamples ? explicitExamples : noExamples ? [] : getDefaultExamples(); + const normalizedExamples = [...new Set(examples)]; + return { ...opts, targetDir: resolvedTargetDir, - debug: !!opts.debug, - install: opts.install ?? true, + debug: Boolean(opts.debug ?? cli.debug), + install: opts.install ?? cli.install ?? true, cwd: resolvedCwd, - templateDir // always use the built-in template + templateDir, // always use the built-in template + exampleTemplatesDir, + noExamples, + examples: normalizedExamples }; } +function getDefaultExamples() { + try { + return fs + .readdirSync(exampleTemplatesDir, { withFileTypes: true }) + .filter(entry => entry.isDirectory()) + .map(entry => entry.name) + .sort(); + } catch { + return []; + } +} + async function main(...args) { const opts = normalizeOptions(args[0]); @@ -62,6 +188,17 @@ async function main(...args) { throw new Error(`Template directory not found: ${templateDir}\n${err.message}`); } + if (opts.examples.length > 0) { + try { + const stat = fs.statSync(exampleTemplatesDir); + if (!stat.isDirectory()) { + throw new Error(`Example templates path is not a directory: ${exampleTemplatesDir}`); + } + } catch (err) { + throw new Error(`Example templates directory not found: ${exampleTemplatesDir}\n${err.message}`); + } + } + // call the implementation with simple, normalized options await createMain(opts); } diff --git a/create/main.js b/create/main.js index 346acef..4c74160 100644 --- a/create/main.js +++ b/create/main.js @@ -1,13 +1,10 @@ import inquirer from 'inquirer'; import { execSync } from 'node:child_process'; -import crypto from 'node:crypto'; import fs from 'node:fs/promises'; import path from 'node:path'; -const getRandomString = length => crypto.randomBytes(length).toString('hex'); - -export async function main({ cwd, templateDir, targetDir }) { - const APP_TITLE = await getTitle(); +export async function main({ cwd, templateDir, exampleTemplatesDir, targetDir, examples = [], title }) { + const APP_TITLE = await getTitle(title); const APP_NAME = (APP_TITLE) // get rid of anything that's not allowed in an app name @@ -19,93 +16,102 @@ export async function main({ cwd, templateDir, targetDir }) { console.log({ cwd, templateDir, targetDir, APP_NAME, APP_TITLE }); - await fs.cp(templateDir, targetDir, { recursive: true, force: false, errorOnExist: true }, (err) => { - console.warn(err); + await fs.cp(templateDir, targetDir, { + recursive: true, + force: false, + errorOnExist: true, }); + if (examples.length > 0) { + const examplesDir = path.join(targetDir, 'examples'); + await fs.mkdir(examplesDir, { recursive: true }); + + for (const example of examples) { + const sourceDir = path.join(exampleTemplatesDir, example); + + try { + const stat = await fs.stat(sourceDir); + if (!stat.isDirectory()) { + throw new Error(`Example template path is not a directory: ${sourceDir}`); + } + } catch (error) { + throw new Error(`Unknown example template "${example}"`); + } + + await fs.cp(sourceDir, path.join(examplesDir, example), { + recursive: true, + force: false, + errorOnExist: true, + }); + } + } + const EXAMPLE_ENV_PATH = path.join(targetDir, '.env.example'); - const ENV_PATH = path.join(targetDir, '.env'); - const PKG_PATH = path.join(targetDir, 'package.json'); - - const appNameRegex = /planning-stack-template/g; - const appTitleRegex = /PLANNING STACK TEMPLATE/g; - - const [env, packageJsonString] = await Promise.all([ - fs.readFile(EXAMPLE_ENV_PATH, 'utf-8'), - fs.readFile(PKG_PATH, 'utf-8'), - ]); - - const filesWithAppName = await Promise.all([ - PKG_PATH, - path.join(targetDir, 'README.md'), - path.join(targetDir, 'docs', 'antora-playbook.yml'), - path.join(targetDir, 'docs', 'antora.yml'), - path.join(targetDir, 'docs', 'modules/ROOT/pages/architecture.adoc'), - path.join(targetDir, 'docs', 'modules/ROOT/pages/documentation.adoc'), - path.join(targetDir, 'docs', 'modules/ROOT/pages/getting-started.adoc'), - path.join(targetDir, 'mocks', 'cli', 'generate-data.mjs'), - path.join(targetDir, 'prototype', 'README.md'), - path.join(targetDir, 'ui', 'README.md'), - ]); - - const filesWithAppTitle = await Promise.all([ - path.join(targetDir, 'README.md'), - path.join(targetDir, 'docs', 'antora-playbook.yml'), - path.join(targetDir, 'docs', 'antora.yml'), - path.join(targetDir, 'docs', 'modules', 'ROOT', 'pages', 'architecture.adoc'), - path.join(targetDir, 'docs', 'modules', 'ROOT', 'pages', 'index.adoc'), - path.join(targetDir, 'docs', 'modules', 'ROOT', 'pages', 'monorepo.adoc'), - path.join(targetDir, 'docs', 'modules', 'ROOT', 'pages', 'prototype.adoc'), - path.join(targetDir, 'docs', 'modules', 'ROOT', 'partials', 'monorepo.puml'), - path.join(targetDir, 'docs', 'modules', 'ROOT', 'nav.adoc'), - path.join(targetDir, 'docs', 'antora-playbook.yml'), - path.join(targetDir, 'docs', 'package.json'), - path.join(targetDir, 'prototype', 'app', 'root.tsx'), - path.join(targetDir, 'prototype', 'app', 'routes', '_index.tsx'), - path.join(targetDir, 'mocks', 'cli', 'generate-data.mjs'), - ]); - - // Replace all instances of the app title - for (const file of filesWithAppTitle) { - const fileContent = await fs.readFile(file, 'utf-8'); - const newFile = fileContent.replaceAll(appTitleRegex, APP_TITLE); - await fs.writeFile(file, newFile); - } - // Replace all instances of the app name - for (const file of filesWithAppName) { - const fileContent = await fs.readFile(file, 'utf-8'); - const newFile = fileContent.replaceAll(appNameRegex, APP_NAME); - await fs.writeFile(file, newFile); - } - - const packageJson = JSON.parse(packageJsonString); - - packageJson.name = APP_NAME; - delete packageJson.author; - delete packageJson.license; - - const fileOperationPromises = [ - fs.copyFile(EXAMPLE_ENV_PATH, ENV_PATH), - fs.writeFile(PKG_PATH, JSON.stringify(packageJson, null, 2)), - ]; - - await Promise.all(fileOperationPromises); - - if (!process.env.SKIP_SETUP) { - execSync('npm install', { cwd: targetDir, stdio: 'inherit' }); - execSync('npm run typecheck', { cwd: targetDir, stdio: 'inherit' }); - execSync('npm run build:data', { cwd: targetDir, stdio: 'inherit' }); - } - - if (!process.env.SKIP_FORMAT) { - execSync('npm run format -- --log-level warn', { - cwd: targetDir, - stdio: 'inherit', - }); - } - - console.log( - ` + const ENV_PATH = path.join(targetDir, '.env'); + const PKG_PATH = path.join(targetDir, 'package.json'); + + const appNameRegex = /planning-stack-template/g; + const appTitleRegex = /PLANNING STACK TEMPLATE/g; + + const packageJsonString = await fs.readFile(PKG_PATH, 'utf-8'); + + const filesWithAppName = [ + PKG_PATH, + path.join(targetDir, 'README.md'), + path.join(targetDir, 'docs', 'antora-playbook.yml'), + path.join(targetDir, 'docs', 'antora.yml'), + path.join(targetDir, 'docs', 'modules/ROOT/pages/architecture.adoc'), + path.join(targetDir, 'docs', 'modules/ROOT/pages/documentation.adoc'), + path.join(targetDir, 'docs', 'modules/ROOT/pages/getting-started.adoc'), + path.join(targetDir, 'prototype', 'README.md'), + path.join(targetDir, 'ui', 'README.md'), + ]; + + const filesWithAppTitle = [ + path.join(targetDir, 'README.md'), + path.join(targetDir, 'docs', 'antora-playbook.yml'), + path.join(targetDir, 'docs', 'antora.yml'), + path.join(targetDir, 'docs', 'modules', 'ROOT', 'pages', 'architecture.adoc'), + path.join(targetDir, 'docs', 'modules', 'ROOT', 'pages', 'index.adoc'), + path.join(targetDir, 'docs', 'modules', 'ROOT', 'pages', 'monorepo.adoc'), + path.join(targetDir, 'docs', 'modules', 'ROOT', 'pages', 'prototype.adoc'), + path.join(targetDir, 'docs', 'modules', 'ROOT', 'partials', 'monorepo.puml'), + path.join(targetDir, 'docs', 'modules', 'ROOT', 'nav.adoc'), + path.join(targetDir, 'docs', 'package.json'), + path.join(targetDir, 'prototype', 'app', 'root.tsx'), + path.join(targetDir, 'prototype', 'app', 'routes', '_index.tsx'), + path.join(targetDir, 'prototype', 'README.md'), + ]; + + await replaceTokensInFiles(filesWithAppTitle, appTitleRegex, APP_TITLE); + await replaceTokensInFiles(filesWithAppName, appNameRegex, APP_NAME); + + const packageJson = JSON.parse(packageJsonString); + + packageJson.name = APP_NAME; + delete packageJson.author; + delete packageJson.license; + + await Promise.all([ + fs.copyFile(EXAMPLE_ENV_PATH, ENV_PATH), + fs.writeFile(PKG_PATH, JSON.stringify(packageJson, null, 2)), + ]); + + if (!process.env.SKIP_SETUP) { + execSync('npm install', { cwd: targetDir, stdio: 'inherit' }); + execSync('npm run typecheck', { cwd: targetDir, stdio: 'inherit' }); + execSync('npm run build:data', { cwd: targetDir, stdio: 'inherit' }); + } + + if (!process.env.SKIP_FORMAT) { + execSync('npm run format -- --log-level warn', { + cwd: targetDir, + stdio: 'inherit', + }); + } + + console.log( + ` Setup is complete. What's next? @@ -114,23 +120,40 @@ What's next? - Build your mock API - Build your prototype - Iterate - `.trim(), - ); + `.trim(), + ); +} + +async function replaceTokensInFiles(files, pattern, replacement) { + for (const file of files) { + try { + const fileContent = await fs.readFile(file, 'utf-8'); + await fs.writeFile(file, fileContent.replaceAll(pattern, replacement)); + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + } + } } -async function getTitle() { - // Check if we are in interactive mode - if (process.env.CI) { - return 'Demo Title'; - } - - const { title } = await inquirer.prompt([ - { - type: 'input', - name: 'title', - message: 'Enter the title of the app', - }, - ]); - - return title; +async function getTitle(providedTitle) { + if (typeof providedTitle === 'string' && providedTitle.trim().length > 0) { + return providedTitle.trim(); + } + + // Check if we are in interactive mode + if (process.env.CI) { + return 'Demo Title'; + } + + const { title } = await inquirer.prompt([ + { + type: 'input', + name: 'title', + message: 'Enter the title of the app', + }, + ]); + + return title; } diff --git a/example-templates/react-router/.cursor/rules/accessibility.mdc b/example-templates/react-router/.cursor/rules/accessibility.mdc new file mode 100644 index 0000000..9774ae0 --- /dev/null +++ b/example-templates/react-router/.cursor/rules/accessibility.mdc @@ -0,0 +1,24 @@ +--- +description: All tasks that are related to improving accessibility (a11y) +globs: +alwaysApply: false +--- + +# Accessibility Rules + +All UI components in must comply with the WCAG 2.1 AA standard. + +- For all user interface components (including but not limited to: form elements, links and components generated by scripts), the name and role can be programmatically determined; states, properties, and values that can be set by the user can be programmatically set; and notification of changes to these items is available to user agents, including assistive technologies. +- Non-native user interactive components need their `role` attribute set. Pay attention to nested components and also take the role of parent components into account. +- Non-native user interactive components which can be in different UI states, need to be annotated with WAI-ARIA state and property attributes where possible. +- Interactive components need to be labelled either via label elements or through `aria-label` or `aria-labelledby`. +- Do not add noise and information that is not relevant to a user. +- Avoid redundancy. Verify that there are no duplicated annotations. Also check parent and child components. +- Keyboard navigation needs to work in all components. +- User focus needs to be properly guided. +- New titles need to be localized. +- Interactive elements cannot contain other interactive elements. + +Do not forget to also update the tests for a component. + +Use the class `sr-only` for elements that should only be visible to screenreaders. \ No newline at end of file diff --git a/example-templates/react-router/.cursor/rules/react.mdc b/example-templates/react-router/.cursor/rules/react.mdc new file mode 100644 index 0000000..eab9da8 --- /dev/null +++ b/example-templates/react-router/.cursor/rules/react.mdc @@ -0,0 +1,18 @@ +--- +description: +globs: *.tsx +alwaysApply: false +--- + +# Rules for authoring React components + +- React is not just a UI framework but also a concept of how a UI is composed. Information should only flow downwards (One-Way dataflow). Never break this concept. +- Only trigger updates through events / actions or handlers. +- Avoid Refs to a child component. Use Props / Context instead and have the child component react to a change in props. Never trigger changes on a different component explicitly. +- React uses functional concepts instead of OOP concepts. +- Components and Hooks must be pure. +- Prefer useReducer over multiple useStates in the same component. +- Document the code in a way that you still understand it a year later. + + + diff --git a/example-templates/react-router/.cursor/rules/tests.mdc b/example-templates/react-router/.cursor/rules/tests.mdc new file mode 100644 index 0000000..0cf4039 --- /dev/null +++ b/example-templates/react-router/.cursor/rules/tests.mdc @@ -0,0 +1,11 @@ +--- +description: +globs: *.spec.tsx +alwaysApply: false +--- + +# Tests + +- This project is using Testing-Library +- Use semantic selectors from testing-library over querySelector(). +- Prefer user-event over fireEvent. diff --git a/example-templates/react-router/.env.example b/example-templates/react-router/.env.example new file mode 100644 index 0000000..0827589 --- /dev/null +++ b/example-templates/react-router/.env.example @@ -0,0 +1,2 @@ +VITE_BUILD_NUMBER=Vite +VITE_VERSION=dev \ No newline at end of file diff --git a/example-templates/react-router/.gitignore b/example-templates/react-router/.gitignore new file mode 100644 index 0000000..afda6bb --- /dev/null +++ b/example-templates/react-router/.gitignore @@ -0,0 +1,47 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# Dependencies +node_modules +.pnp +.pnp.js + +# Local env files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Testing +coverage + +# Turbo +.turbo + +# Vercel +.vercel + +# Build Outputs +.next/ +out/ +build +dist +/.cache +.react-router/ +/public/build +tsconfig.tsbuildinfo +data-mocks/ + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +vite.config.ts.timestamp-* + +# Misc +.DS_Store +*.pem +.idea/ + + +catalyst-ui-kit/ \ No newline at end of file diff --git a/example-templates/react-router/.gitlab-ci.yml b/example-templates/react-router/.gitlab-ci.yml new file mode 100644 index 0000000..ce76e08 --- /dev/null +++ b/example-templates/react-router/.gitlab-ci.yml @@ -0,0 +1,188 @@ +# Root CI File + +include: + - component: $CI_SERVER_HOST/$CI_COMPONENT_DIR/gitlab-environments/preview@1.0 + inputs: + build_job: 'build prototype' + stage: publish + build_folder: 'prototype/build/client' + +default: + image: node:22 + interruptible: true + # Usage of global_cache key to override the default push-pull policy + # https://docs.gitlab.com/ee/ci/caching/index.html + cache: &npm_cache + key: + files: + - package-lock.json + policy: pull + paths: + - 'node_modules/' + - './*/node_modules/' + - 'config/*/node_modules/' + timeout: 2 minutes + +stages: + - install + - verify + - build + - publish + +variables: + TURBO_TELEMETRY_DISABLED: 1 + VITE_BUILD_NUMBER: ${CI_PIPELINE_IID} + VITE_VERSION: ${CI_COMMIT_SHORT_SHA} + +workflow: + rules: + - if: $CI_MERGE_REQUEST_IID + - if: $CI_COMMIT_TAG + when: always + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_COMMIT_TAG == null + - if: $CI_PIPELINE_SOURCE == "schedule" + +############################################################ +# +# Install stage +# +############################################################ + +# Install dependencies +install dependencies: + stage: install + interruptible: false + cache: + - <<: *npm_cache + policy: pull-push + script: + - node -v + - echo "Installing Dependencies" + - 'find . -name "node_modules" -type d -prune -exec rm -rf "{}" +' + - time npm clean-install + # We need to install the linux version of rollup here. https://github.com/npm/cli/issues/4828 + - npm i -O @rollup/rollup-linux-x64-gnu + rules: + - if: $CI_COMMIT_REF_PROTECTED == "true" + - if: $CI_MERGE_REQUEST_IID + changes: + - 'package-lock.json' + allow_failure: false + - if: $CI_PIPELINE_SOURCE == "schedule" + +force install dependencies: + extends: install dependencies + rules: + - when: manual + allow_failure: true + script: + - node -v + - 'find . -name "node_modules" -type d -prune -exec rm -rf "{}" +' + - npm install --frozen-lockfile --no-progress --non-interactive + - npm i -O @rollup/rollup-linux-x64-gnu + +############################################################ +# +# Verify stage +# +############################################################ + +# Run Type Checks on all changed modules. +verify: + stage: verify + interruptible: true + variables: + ESLINT_CODE_QUALITY_REPORT: 'gl-code-quality-report.json' + cache: + - <<: *npm_cache + - key: verify-$CI_COMMIT_REF_SLUG + paths: + - '.turbo/' + - '**/.turbo/' + needs: + - job: install dependencies + optional: true + script: + - node --version + - npm --version + - npm ci + - ./node_modules/.bin/turbo run typecheck + - ./node_modules/.bin/turbo run lint + artifacts: + when: always + expire_in: 5 days + paths: + - gl-code-quality-report.json + reports: + codequality: 'gl-code-quality-report.json' + +test: + stage: verify + interruptible: true + needs: + - job: install dependencies + optional: true + script: + - ./node_modules/.bin/turbo run test + +############################################################ +# +# Build stage +# +############################################################ + +build prototype: + interruptible: true + stage: build + script: + - npm ci + - npm run build:prototype + artifacts: + paths: + - prototype/build + +############################################################ +# +# Publish stage +# +############################################################ + +pages: + stage: publish + needs: + - job: install dependencies + optional: true + allow_failure: true + script: + - npm ci + - npm run docs + artifacts: + paths: + - docs/build + publish: docs/build + rules: + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + changes: + - 'docs/**/*' + +publish prototype continuous: + extends: create preview + variables: + CI_MERGE_REQUEST_ID: p${CI_PROJECT_ID}-cont + rules: + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + environment: + name: 'prototype/${CI_PROJECT_PATH_SLUG}/continuous' + url: $CI_MERGE_REQUEST_REVIEW_URL + on_stop: 'stop prototype continuous' + +stop prototype continuous: + extends: stop preview + variables: + CI_MERGE_REQUEST_ID: p${CI_PROJECT_ID}-cont + environment: + name: 'prototype/${CI_PROJECT_PATH_SLUG}/continuous' + action: stop + rules: + - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH + when: manual diff --git a/example-templates/react-router/.licenses/isc.md b/example-templates/react-router/.licenses/isc.md new file mode 100644 index 0000000..31b673a --- /dev/null +++ b/example-templates/react-router/.licenses/isc.md @@ -0,0 +1,17 @@ +# ISC License + +- Lucide + +Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All +other copyright (c) for Lucide are held by Lucide Contributors 2022. + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee +is hereby granted, provided that the above copyright notice and this permission notice appear in all +copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS +SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS +SOFTWARE. diff --git a/example-templates/react-router/.lintstagedrc.js b/example-templates/react-router/.lintstagedrc.js new file mode 100644 index 0000000..90e682a --- /dev/null +++ b/example-templates/react-router/.lintstagedrc.js @@ -0,0 +1,3 @@ +export default { + '*.{js,jsx,ts,tsx,json,css,scss,md}': ['prettier --write'], +} diff --git a/example-templates/react-router/.nvmrc b/example-templates/react-router/.nvmrc new file mode 100644 index 0000000..e4f846d --- /dev/null +++ b/example-templates/react-router/.nvmrc @@ -0,0 +1 @@ +v22.10.0 diff --git a/example-templates/react-router/.prettierignore b/example-templates/react-router/.prettierignore new file mode 100644 index 0000000..c7e2c6b --- /dev/null +++ b/example-templates/react-router/.prettierignore @@ -0,0 +1,6 @@ +/node_modules +package-lock.json +.gitignore +.cursorrules.md +**/public +.htaccess \ No newline at end of file diff --git a/example-templates/react-router/.prettierrc.js b/example-templates/react-router/.prettierrc.js new file mode 100644 index 0000000..5af14a2 --- /dev/null +++ b/example-templates/react-router/.prettierrc.js @@ -0,0 +1,30 @@ +/** @type {import("prettier").Options} */ +export default { + arrowParens: 'avoid', + bracketSameLine: false, + bracketSpacing: true, + embeddedLanguageFormatting: 'auto', + endOfLine: 'lf', + htmlWhitespaceSensitivity: 'css', + insertPragma: false, + jsxSingleQuote: false, + printWidth: 100, + proseWrap: 'always', + quoteProps: 'consistent', + requirePragma: false, + semi: true, + singleAttributePerLine: false, + singleQuote: true, + tabWidth: 2, + trailingComma: 'all', + useTabs: true, + overrides: [ + { + files: ['**/*.json', '**/*.yaml', '**/*.yml', '/**/*.adoc', '**/*.md'], + options: { + useTabs: false, + }, + }, + ], + plugins: ['prettier-plugin-organize-imports'], +}; diff --git a/example-templates/react-router/.vscode/settings.json b/example-templates/react-router/.vscode/settings.json new file mode 100644 index 0000000..335f886 --- /dev/null +++ b/example-templates/react-router/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode" +} \ No newline at end of file diff --git a/example-templates/react-router/LICENSE.md b/example-templates/react-router/LICENSE.md new file mode 100644 index 0000000..55654ce --- /dev/null +++ b/example-templates/react-router/LICENSE.md @@ -0,0 +1,18 @@ +MIT License + +Copyright (c) 2025 ti&m + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES +OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/example-templates/react-router/README.md b/example-templates/react-router/README.md new file mode 100644 index 0000000..cd950c2 --- /dev/null +++ b/example-templates/react-router/README.md @@ -0,0 +1,33 @@ +# React Router Example + +This is the self-contained React Router example for Tinker Stack. + +It preserves the original demo-oriented workspace, including: + +- a richer prototype flow +- generated mock data +- MSW handlers wired to that dataset +- docs that describe the demo setup + +## Run the example + +Work inside this directory, not from the generated project root: + +```bash +npm install +npm run build:data +npm run dev +``` + +## How to use it + +This example is meant to be mined for patterns, not coupled to the main workspace. + +Copy ideas selectively from: + +- `prototype/` for routing, layouts, and UI flows +- `mocks/` for seeded dataset generation +- `msw/` for handler composition +- `api/` for shared response types + +You can delete the entire example directory without affecting the generated root project. diff --git a/example-templates/react-router/api/commons/enums.ts b/example-templates/react-router/api/commons/enums.ts new file mode 100644 index 0000000..3984b3c --- /dev/null +++ b/example-templates/react-router/api/commons/enums.ts @@ -0,0 +1,12 @@ +/** + * Granular Permissions. + * The UI only uses these permissions which are derived from the roles in the JWT. + * The key is the permission name and the value is the permission description. + */ +export const permissions = { + 'reports-create': 'Create reports', + 'reports-approve': 'Approve reports', + 'admin': 'Administrative privileges', +}; + +export type Permission = keyof typeof permissions; diff --git a/example-templates/react-router/api/commons/types.ts b/example-templates/react-router/api/commons/types.ts new file mode 100644 index 0000000..3df1886 --- /dev/null +++ b/example-templates/react-router/api/commons/types.ts @@ -0,0 +1,42 @@ +/** + * Image Object + */ +export type Image = { + url: string; + alt?: string; + width?: number; + height?: number; +}; + +/** + * Calendar Date String (YYYY-MM-DD) + */ +export type IsoDateString = string; +/** + * ISO 8601 Timestamp String + */ +export type IsoTimestampString = string; + +/** + * Spring Boot Paginated Response + */ +export type PaginatedResponse = { + content: T[]; + pageable: { + pageNumber: number; + pageSize: number; + }; + totalPages: number; + totalElements: number; + first: boolean; + last: boolean; + size: number; + number: number; +}; + +export type File = { + readonly type: string; + readonly name: string; + readonly size: number; + readonly createdAt: IsoTimestampString; +}; diff --git a/template/api/demo/types.ts b/example-templates/react-router/api/demo/types.ts similarity index 100% rename from template/api/demo/types.ts rename to example-templates/react-router/api/demo/types.ts diff --git a/example-templates/react-router/api/eslint.config.js b/example-templates/react-router/api/eslint.config.js new file mode 100644 index 0000000..75d444b --- /dev/null +++ b/example-templates/react-router/api/eslint.config.js @@ -0,0 +1,15 @@ +import baseConfig from '@repo/eslint-config/base'; + +export default [ + { + ignores: ['**/node_modules/**', '**/dist/**', '**/.turbo/**'], + }, + ...baseConfig, + { + languageOptions: { + parserOptions: { + tsconfigRootDir: import.meta.dirname, + }, + }, + }, +]; diff --git a/example-templates/react-router/api/package.json b/example-templates/react-router/api/package.json new file mode 100644 index 0000000..c297c84 --- /dev/null +++ b/example-templates/react-router/api/package.json @@ -0,0 +1,43 @@ +{ + "name": "@repo/api", + "description": "Domain Types und Enums", + "version": "0.1.0", + "sideEffects": false, + "type": "module", + "scripts": { + "build": "rimraf dist && tsc && tsc-alias", + "dev": "tsc && (concurrently \"tsc -w\" \"tsc-alias -w\")", + "clean": "rimraf dist && rimraf .turbo", + "format": "prettier --write .", + "typecheck": "tsc && tsc-alias", + "lint": "eslint .", + "lint-staged": "lint-staged" + }, + "devDependencies": { + "@repo/eslint-config": "*", + "@repo/typescript-config": "*" + }, + "engines": { + "node": ">=22.0.0" + }, + "exports": { + "./commons/types": { + "types": "./dist/commons/types.d.ts" + }, + "./commons/enums": { + "types": "./dist/commons/enums.d.ts", + "import": "./dist/commons/enums.js" + }, + "./demo/types": { + "types": "./dist/demo/types.d.ts" + } + }, + "lint-staged": { + "*.{js,ts,tsx}": [ + "prettier --write" + ], + "*.{json,md}": [ + "prettier --write" + ] + } +} \ No newline at end of file diff --git a/example-templates/react-router/api/tsconfig.json b/example-templates/react-router/api/tsconfig.json new file mode 100644 index 0000000..1c4452b --- /dev/null +++ b/example-templates/react-router/api/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@repo/typescript-config/node-library.json", + "include": ["./**/*.ts"], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "#/*": ["./*"] + }, + "outDir": "dist" + }, + "exclude": ["node_modules", "dist", ".turbo"] +} diff --git a/example-templates/react-router/config/eslint/README.md b/example-templates/react-router/config/eslint/README.md new file mode 100644 index 0000000..8b42d90 --- /dev/null +++ b/example-templates/react-router/config/eslint/README.md @@ -0,0 +1,3 @@ +# `@turbo/eslint-config` + +Collection of internal eslint configurations. diff --git a/example-templates/react-router/config/eslint/base.js b/example-templates/react-router/config/eslint/base.js new file mode 100644 index 0000000..d14dd65 --- /dev/null +++ b/example-templates/react-router/config/eslint/base.js @@ -0,0 +1,61 @@ +import eslint from '@eslint/js'; +import eslintConfigPrettier from 'eslint-config-prettier'; +import importPlugin from 'eslint-plugin-import'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +/** + * This is the basic rule set for all projects. + * Make sure to add the languageOptions.parserOptions.tsconfigRootDir to the project you want to lint. + * @returns { import('typescript-eslint').FlatConfig.ConfigArray } + */ +export default tseslint.config( + eslint.configs.recommended, + importPlugin.flatConfigs.recommended, + importPlugin.flatConfigs.typescript, + ...tseslint.configs.recommendedTypeChecked, + ...tseslint.configs.stylisticTypeChecked, + { + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + parserOptions: { + projectService: { + allowDefaultProject: ['*.js'], + defaultProject: 'tsconfig.json', + }, + ecmaFeatures: { + jsx: true, + }, + }, + globals: { + // ...globals.browser, + ...globals.serviceworker, + }, + }, + settings: { + 'import/internal-regex': '^#/', + }, + rules: { + '@typescript-eslint/no-empty-object-type': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/consistent-type-definitions': 'off', + '@typescript-eslint/only-throw-error': 'off', + '@typescript-eslint/no-duplicate-type-constituents': [ + 'error', + { + ignoreUnions: true, + ignoreIntersections: true, + }, + ], + '@typescript-eslint/consistent-type-imports': [ + 'error', + { fixStyle: 'separate-type-imports', prefer: 'type-imports' }, + ], + 'import/no-unresolved': 'off', // does not properly work with nodenext yet. + 'import/no-relative-packages': 'error', + 'import/no-relative-parent-imports': 'error', + }, + }, + eslintConfigPrettier, +); diff --git a/example-templates/react-router/config/eslint/package.json b/example-templates/react-router/config/eslint/package.json new file mode 100644 index 0000000..7bf38de --- /dev/null +++ b/example-templates/react-router/config/eslint/package.json @@ -0,0 +1,25 @@ +{ + "name": "@repo/eslint-config", + "version": "0.0.0", + "private": true, + "files": [ + "base.js", + "react.js" + ], + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^8.49.0", + "@typescript-eslint/parser": "^8.49.0", + "eslint-config-prettier": "^10.1.8", + "eslint-config-turbo": "^2.6.3", + "eslint-plugin-only-warn": "^1.1.0", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.0.1", + "typescript": "^5.9.3", + "typescript-eslint": "^8.49.0" + }, + "type": "module", + "exports": { + "./base": "./base.js", + "./react": "./react.js" + } +} diff --git a/example-templates/react-router/config/eslint/react.js b/example-templates/react-router/config/eslint/react.js new file mode 100644 index 0000000..d163295 --- /dev/null +++ b/example-templates/react-router/config/eslint/react.js @@ -0,0 +1,42 @@ +import baseConfig from '@repo/eslint-config/base'; +import reactPlugin from 'eslint-plugin-react'; +import reactHooksPlugin from 'eslint-plugin-react-hooks'; +import tseslint from 'typescript-eslint'; + +/** + * This config adds additional rules for React and JSX. + * @returns { import('typescript-eslint').FlatConfig.ConfigArray } + */ +export default tseslint.config( + ...baseConfig, + { + settings: { + react: { + version: 'detect', + }, + }, + }, + reactPlugin.configs.flat.recommended, + reactPlugin.configs.flat['jsx-runtime'], + // enable a11y rules for the frontend and UI only + // jsxA11yPlugin.flatConfigs.recommended, + // React + { + files: ['**/*.{tsx}'], + settings: { + formComponents: ['Form'], + linkComponents: [ + { name: 'Link', linkAttribute: 'to' }, + { name: 'NavLink', linkAttribute: 'to' }, + ], + }, + plugins: { + 'react-hooks': reactHooksPlugin, + }, + rules: { + 'react/react-in-jsx-scope': 'off', + ...reactHooksPlugin.configs.recommended.rules, + 'react-hooks/rules-of-hooks': 'error', + }, + }, +); diff --git a/example-templates/react-router/config/typescript/base.json b/example-templates/react-router/config/typescript/base.json new file mode 100644 index 0000000..20d7e23 --- /dev/null +++ b/example-templates/react-router/config/typescript/base.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Default", + "compilerOptions": { + "composite": true, + "esModuleInterop": true, + "incremental": true, + "isolatedModules": true, + "lib": ["ESNext"], + "forceConsistentCasingInFileNames": true, + "module": "NodeNext", + "moduleDetection": "force", + "moduleResolution": "NodeNext", + "noUncheckedIndexedAccess": true, + "resolveJsonModule": true, + "allowImportingTsExtensions": false, + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "target": "ESNext", + "noEmit": true + } +} diff --git a/example-templates/react-router/config/typescript/node-library.json b/example-templates/react-router/config/typescript/node-library.json new file mode 100644 index 0000000..9fb6019 --- /dev/null +++ b/example-templates/react-router/config/typescript/node-library.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Node Library", + "extends": "./base.json", + "compilerOptions": { + "noEmit": false, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "target": "ESNext", + } + } \ No newline at end of file diff --git a/example-templates/react-router/config/typescript/package.json b/example-templates/react-router/config/typescript/package.json new file mode 100644 index 0000000..27c0e60 --- /dev/null +++ b/example-templates/react-router/config/typescript/package.json @@ -0,0 +1,9 @@ +{ + "name": "@repo/typescript-config", + "version": "0.0.0", + "private": true, + "license": "MIT", + "publishConfig": { + "access": "public" + } +} diff --git a/example-templates/react-router/config/typescript/react-app.json b/example-templates/react-router/config/typescript/react-app.json new file mode 100644 index 0000000..37e1448 --- /dev/null +++ b/example-templates/react-router/config/typescript/react-app.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "React App", + "extends": "./base.json", + "compilerOptions": { + "jsx": "react-jsx", + "lib": ["ESNext", "DOM", "DOM.Iterable", "WebWorker"], + "noEmit": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true + } +} diff --git a/example-templates/react-router/config/typescript/react-library.json b/example-templates/react-router/config/typescript/react-library.json new file mode 100644 index 0000000..2543a7f --- /dev/null +++ b/example-templates/react-router/config/typescript/react-library.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "React Library", + "extends": "./base.json", + "compilerOptions": { + "jsx": "react-jsx", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "allowImportingTsExtensions": true, + } +} diff --git a/example-templates/react-router/docs/antora-playbook.yml b/example-templates/react-router/docs/antora-playbook.yml new file mode 100644 index 0000000..dfc9cb1 --- /dev/null +++ b/example-templates/react-router/docs/antora-playbook.yml @@ -0,0 +1,22 @@ +site: + title: PLANNING STACK TEMPLATE Dokumentation + start_page: planning-stack-template-frontend::index.adoc + url: https://frontend-6dd0b0.pages.ti8m.ch +content: + sources: + - url: ../ + branches: HEAD + start_path: docs +ui: + bundle: + url: https://gitlab.com/antora/antora-ui-default/-/jobs/artifacts/HEAD/raw/build/ui-bundle.zip?job=bundle-stable + snapshot: true +output: + dir: ./build +asciidoc: + attributes: + # makes the extension download and store diagrams in Antora's _images folder (in output dir) + kroki-fetch-diagram: true + product: PLANNING STACK TEMPLATE + extensions: + - asciidoctor-kroki diff --git a/example-templates/react-router/docs/antora.yml b/example-templates/react-router/docs/antora.yml new file mode 100644 index 0000000..18fdf82 --- /dev/null +++ b/example-templates/react-router/docs/antora.yml @@ -0,0 +1,5 @@ +name: 'planning-stack-template-frontend' +title: 'PLANNING STACK TEMPLATE Frontend' +version: true +nav: + - modules/ROOT/nav.adoc diff --git a/example-templates/react-router/docs/modules/ROOT/images/Prototyp-Repo.drawio.png b/example-templates/react-router/docs/modules/ROOT/images/Prototyp-Repo.drawio.png new file mode 100644 index 0000000..3e73d5a Binary files /dev/null and b/example-templates/react-router/docs/modules/ROOT/images/Prototyp-Repo.drawio.png differ diff --git a/example-templates/react-router/docs/modules/ROOT/nav.adoc b/example-templates/react-router/docs/modules/ROOT/nav.adoc new file mode 100644 index 0000000..f8c4bf7 --- /dev/null +++ b/example-templates/react-router/docs/modules/ROOT/nav.adoc @@ -0,0 +1,11 @@ +:product: PLANNING STACK TEMPLATE + +* xref:index.adoc[Übersicht] +** xref:getting-started.adoc[Erste Schritte] +** xref:architecture.adoc[Architektur] +** xref:monorepo.adoc[Monorepo] +** xref:prototype.adoc[Prototyp] +** xref:mocks.adoc[Testdaten] +** xref:design-system.adoc[Design System] +** xref:login.adoc[Login Flow] +** xref:tailwind.adoc[Tailwind] \ No newline at end of file diff --git a/example-templates/react-router/docs/modules/ROOT/pages/architecture.adoc b/example-templates/react-router/docs/modules/ROOT/pages/architecture.adoc new file mode 100644 index 0000000..a940995 --- /dev/null +++ b/example-templates/react-router/docs/modules/ROOT/pages/architecture.adoc @@ -0,0 +1,145 @@ += Architektur +:experimental: +:plantuml-server-url: http://www.plantuml.com/plantuml +:source-highlighter: highlight.js + +== Überblick + +Das PLANNING STACK TEMPLATE Frontend ist als Monorepo mit mehreren Packages aufgebaut. Der Fokus liegt auf: + +* Schnelle Iteration durch Prototyping +* Wiederverwendbare Komponenten +* Typsicherheit durch TypeScript +* Realistische Testdaten + +== Architekturprinzipien + +=== Domain-Driven Design + + + + +=== Prototypen-getriebene Entwicklung + +Der Entwicklungsprozess folgt einem iterativen Ansatz: + +1. Features werden zuerst im Prototyp implementiert +2. Designer-, Stakeholder- und Benutzer-Feedback wird eingeholt +3. Validierte Features werden in die produktive Anwendung übernommen + +Dies ermöglicht: + +* Reduzierung von Entwicklungsrisiken +* Validierung von UX-Konzepten +* Schnelles Feedback von Benutzern + +=== Typsicherheit + +* Strikte TypeScript Konfiguration +* Zentrale Definition von Domain Types +* Shared Types zwischen Packages + +=== Komponenten-Bibliothek + +Die UI-Komponenten sind in einem separaten Package (`@repo/ui`) organisiert: + +* Basis auf shadcn/ui, Tailwind UI und Tailwind +* Wiederverwendbar zwischen Prototyp und Produktion +* Einheitliches Design-System basierend auf Tailwind UI und dem Figma Design System + +== Technische Architektur + +Siehe auch xref:monorepo.adoc#aufbau[Monorepo]. + +=== Frontend Stack + +[cols="1,2,2"] +|=== +|Technologie |Verwendung |Begründung + +|React +|UI Framework +|Etabliert, grosses Ökosystem + +|React Router +|Full-Stack Framework +|Gute DX, Server Components + +|Tailwind +|CSS Framework +|Utility-First, Wartbar + +|shadcn/ui +|UI-Komponenten +|Zugänglich, Anpassbar + +|TypeScript +|Programmiersprache +|Typsicherheit, DX +|=== + +=== Mock Backend + +[plantuml,msw,svg] +---- +@startuml +actor Browser +participant "React Router App" as App +participant "MSW" as MSW +database "Mock Data" as Data + +Browser -> App: Request +App -> MSW: API Call +MSW -> Data: Load Data +Data --> MSW: Data +MSW --> App: Response +App --> Browser: Render +@enduml +---- + +* MSW (Mock Service Worker) für API-Simulation +* Realistische HTTP-Requests +* Einfacher Wechsel zu echtem Backend + +== Entwicklungsprozess + +=== Feature Entwicklung + +1. Feature-Anforderung +2. Prototyp Implementation +3. User Testing +4. Feedback Integration +5. Production Implementation + +=== Code Organisation + +* Feature-basierte Ordnerstruktur +* Shared Logic in Utils +* Typed API Boundaries + +== Deployment + +=== Prototype + +* Automatisches Deployment bei Push auf `main` +** URL: +** Aktuelle Entwicklungsversion +* Deployment bei Tag-Commit +** URL: +** Stabile Version für User-Testing + +=== Documentation + +* Antora Site Generator +* Deployed zusammen mit Prototype +* Automatische Updates + +== Supporting Subdomain + + +== Zukünftige Entwicklung + +* Migration zu Production App +* API Integration +* E2E Tests +* Performance Monitoring \ No newline at end of file diff --git a/example-templates/react-router/docs/modules/ROOT/pages/design-system.adoc b/example-templates/react-router/docs/modules/ROOT/pages/design-system.adoc new file mode 100644 index 0000000..ffa094c --- /dev/null +++ b/example-templates/react-router/docs/modules/ROOT/pages/design-system.adoc @@ -0,0 +1,57 @@ += Design System +:experimental: + +== Überblick + + +== Komponenten + + +=== Verwendung + +[source,tsx] +---- +import { Button } from '@repo/ui/components/button' +import { LucideIcon } from 'lucide-react' + +export function IconButton({ icon: Icon }) { + return ( + + ) +} +---- + +== Theming + +=== Figma Integration + +* Designs aus Figma Projekt +* Farben und Typography synchron halten +* Komponenten-Patterns übernehmen + +=== Tailwind Konfiguration + +* Custom Colors aus CI +* Spacing und Typography nach Design System +* Erweiterbare Struktur + +Siehe auch xref:tailwind.adoc[Tailwind Dokumentation] für Best Practices. + +== Komponenten entwickeln + +=== Workflow + +1. Figma Design reviewen +2. shadcn/ui Komponente als Basis nehmen +3. Mit Tailwind UI Patterns abgleichen +4. An Design System anpassen +5. Dokumentation erstellen + +=== Guidelines + +* Accessibility first +* Responsive Design +* Konsistente Props API +* Dokumentation in Storybook diff --git a/example-templates/react-router/docs/modules/ROOT/pages/documentation.adoc b/example-templates/react-router/docs/modules/ROOT/pages/documentation.adoc new file mode 100644 index 0000000..4478161 --- /dev/null +++ b/example-templates/react-router/docs/modules/ROOT/pages/documentation.adoc @@ -0,0 +1,93 @@ += Dokumentation +:experimental: +:url-antora-docs: https://docs.antora.org/antora/latest/ +:url-kroki-docs: https://docs.kroki.io/kroki/ + +== Überblick + +Die Entwicklerdokumentation wird mit {url-antora-docs}[Antora] erstellt und über GitLab Pages bereitgestellt. + +* URL: +* Automatisches Deployment bei Push auf `main` +* Versionierung über Git + +== Struktur + +[source] +---- +docs/ +├── antora.yml # Antora Projekt-Konfiguration +├── antora-playbook.yml # Site Generator Konfiguration +└── modules/ + └── ROOT/ + ├── nav.adoc # Navigation + ├── pages/ # Dokumentationsseiten + └── partials/ # Wiederverwendbare Inhalte +---- + +== Diagramme + +Die Dokumentation unterstützt verschiedene Diagrammtypen über {url-kroki-docs}[Kroki]: + +=== PlantUML + +[source,asciidoc] +---- +[plantuml,format=svg] +---- +@startuml +package "Frontend" { + [Components] + [Routes] +} +[Components] --> [Routes] +@enduml +---- +---- + +=== Weitere Diagrammtypen + +* Mermaid +* C4 (mit PlantUML) +* GraphViz +* ... + +Siehe {url-kroki-docs}#diagram-types[Kroki Diagram Types] für eine vollständige Liste. + +== Lokale Entwicklung + +=== Voraussetzungen + +* Node.js >= 22.0.0 +* npm >= 10.0.0 + +=== Dokumentation lokal generieren + +[source,bash] +---- +# Im docs Verzeichnis +npm run docs +---- + +Die generierte Dokumentation ist dann unter `docs/build` verfügbar. + +== Best Practices + +* Neue Features parallel zur Implementierung dokumentieren +* Diagramme für komplexe Zusammenhänge nutzen +* Wiederverwendbare Inhalte in `partials` auslagern +* Interne Links mit `xref:` erstellen + +== Deployment + +Das Deployment erfolgt automatisch über die GitLab CI/CD Pipeline: + +* Trigger: Push auf `main` +* Output: Statische Seiten +* Hosting: GitLab Pages + +== Nützliche Links + +* {url-antora-docs}[Antora Dokumentation] +* {url-antora-docs}asciidoc/page/[AsciiDoc Syntax Guide] +* {url-kroki-docs}[Kroki Dokumentation] \ No newline at end of file diff --git a/example-templates/react-router/docs/modules/ROOT/pages/getting-started.adoc b/example-templates/react-router/docs/modules/ROOT/pages/getting-started.adoc new file mode 100644 index 0000000..7229b2a --- /dev/null +++ b/example-templates/react-router/docs/modules/ROOT/pages/getting-started.adoc @@ -0,0 +1,139 @@ += Erste Schritte +:experimental: +:icons: font + + + +== Voraussetzungen + +Für die Entwicklung werden folgende Tools benötigt: + +* Node.js >= 22 +* npm >= 10 +* Git + +== Repository klonen + +[source,bash] +---- +git clone TODO +cd planning-stack-template +---- + +== Installation + +Das Projekt verwendet npm Workspaces. Alle Abhängigkeiten können mit einem Befehl installiert werden: + +[source,bash] +---- +npm install +---- + +== Entwicklungsumgebung einrichten + +=== Mock-Daten generieren + +Vor dem ersten Start müssen die Mock-Daten generiert werden: + +[source,bash] +---- +npm run build:data +---- + +=== Entwicklungsserver starten + +Der Entwicklungsserver kann mit folgendem Befehl im Root-Verzeichnis des Repos gestartet werden: + +[source,bash] +---- +npm run dev +---- + +Dies startet: + +* Den https://reactrouter.com/home[React Router] Entwicklungsserver (Prototyp) +* Einen Watcher für die Mock-Daten +* Einen Watcher für die UI-Komponenten + +Die Anwendung ist dann unter http://localhost:5173 erreichbar. + +== Projektstruktur + +[source] +---- +. +├── api/ # Domain Types & Enums +├── config/ +│ ├── eslint/ # ESLint Konfigurationen +│ ├── typescript/ # Typescript Konfigurationen +├── docs/ # Dokumentation +├── frontend/ # Produktives Frontend (noch nicht eingerichtet) +├── mocks/ # Testdaten Generator +├── mock-api/ # Mock Backend +├── prototype/ # Klickbarer UX-Prototyp +└── ui/ # Design-System Komponenten +---- + + +== IDE Konfiguration + +=== VS Code + +Empfohlene Extensions: + +* ESLint +* Prettier +* Tailwind CSS IntelliSense +* AsciiDoc + +Die Projekteinstellungen für VS Code sind bereits im Repository enthalten. + +=== WebStorm + +Für WebStorm müssen folgende Plugins installiert werden: + +* Tailwind CSS +* AsciiDoc + +== Typische Entwicklungsabläufe + +=== Neue Feature im Prototyp entwickeln + +1. Feature-Branch erstellen +2. Route in `prototype/app/routes` anlegen +3. UI mit shadcn/ui Komponenten aufbauen +4. Mock-Daten in `mocks` erweitern falls nötig +5. Feature testen und iterieren +6. Pull Request erstellen + +=== UI-Komponente hinzufügen + +1. Komponente in `packages/ui/src/components` erstellen +2. Storybook Story schreiben +3. Tests hinzufügen +4. Komponente exportieren und im Prototyp verwenden + +== Nächste Schritte + +* xref:architecture.adoc[Architektur] verstehen +* xref:prototype.adoc[Prototyp] kennenlernen +* xref:design-system.adoc[Design System] erkunden + +== Troubleshooting + +=== Bekannte Probleme + +[qanda] +Typescript Fehler nach npm install:: + Führen Sie `npm run build` aus, um alle Packages zu bauen. + +Mock-Daten werden nicht aktualisiert:: + Löschen Sie den `.cache` Ordner und führen Sie `npm run build:data` erneut aus. + +=== Support + +Bei Problemen: + +1. Prüfen Sie die bekannten Probleme +2. Suchen Sie in den GitHub Issues +3. Erstellen Sie ein neues Issue \ No newline at end of file diff --git a/example-templates/react-router/docs/modules/ROOT/pages/index.adoc b/example-templates/react-router/docs/modules/ROOT/pages/index.adoc new file mode 100644 index 0000000..4d0ccaa --- /dev/null +++ b/example-templates/react-router/docs/modules/ROOT/pages/index.adoc @@ -0,0 +1,78 @@ += PLANNING STACK TEMPLATE Frontend Dokumentation +:description: Entwicklerdokumentation für das PLANNING STACK TEMPLATE Frontend +:experimental: + +== Über dieses Projekt + +Beschreibung des Projekts + +== Module + +.Frontend-Struktur +image::Prototyp-Repo.drawio.png[Repository-Struktur] + +Das Projekt ist als Monorepo mit folgenden Hauptmodulen aufgebaut: + +[cols="1,4"] +|=== +|Modul |Beschreibung + +|@repo/api +|Domain-Typen und Enums + +|@repo/docs +|Diese Dokumentation + +|@repo/e2e +|End-to-End und Integrations-Tests (Browser-Tests) + +|@repo/frontend +|Frontend-Anwendung + +|@repo/msw +|Mock-Backend mit MSW + +|@repo/mocks +|Synthetische Testdaten-Generator + +|@repo/prototype +|Klickbarer UX-Prototyp für schnelles Feedback + +|@repo/ui +|Design-System und Komponenten-Bibliothek + +|=== + +Das Mock-API muss mit dem Backend synchron gehalten werden. Somit ist es möglich, die Frontend-Anwendung lokal zu entwickeln und gleichzeitig das Backend zu testen. + +Ausserdem ist es dadurch möglich, mit dem Prototypen gegen das echte Backend zu testen. + + +== Technologie-Stack + +Das Frontend basiert auf folgenden Haupttechnologien: + +* *React* mit TypeScript für die UI-Entwicklung +* *Tailwind CSS* mit shadcn/ui für das Design-System +* *Turborepo* für das Monorepo-Management +* *MSW* (Mock Service Worker) für das Mock-Backend +* *Antora* für die Dokumentation + +Die Technologie für das Frontend ist noch offen. Empfohlen wird +React Router v7 als Framework (Isomorph). + +Der Prototyp verwendet React Router SPA, damit er auf einem Shared-Host deployed werden kann. + +== Erste Schritte + +Siehe xref:getting-started.adoc[Erste Schritte] für Setup-Anweisungen und einen Überblick über die Entwicklungsumgebung. + +== Architektur + +Die Anwendung folgt einer prototypen-getriebenen Entwicklung: + +1. Schnelle Iteration über Features im Prototyp +2. Feedback von Benutzern einholen +3. Validierte Features in die produktive Anwendung übernehmen + +Mehr Details zur Architektur unter xref:architecture.adoc[Architektur]. \ No newline at end of file diff --git a/example-templates/react-router/docs/modules/ROOT/pages/login.adoc b/example-templates/react-router/docs/modules/ROOT/pages/login.adoc new file mode 100644 index 0000000..627d3a5 --- /dev/null +++ b/example-templates/react-router/docs/modules/ROOT/pages/login.adoc @@ -0,0 +1,62 @@ += Login Flow (Prototyp) +:experimental: + +== Überblick + +Im Prototyp wird ein vereinfachtes Login-System verwendet: + +* Cookie-basierte Authentifizierung +* Mock Implementation mit MSW +* Vordefinierte Test-Benutzer + +== Implementation + +=== Mock Service Worker + +[plantuml,mock-auth,svg] +---- +@startuml +participant "Browser" as Browser +participant "MSW Handler" as MSW +database "Mock Data" as Data + +Browser -> MSW: POST /auth/login +note right: Form Data mit Username +MSW -> Data: Token lookup +Data --> MSW: JWT Payload +MSW -> MSW: Permissions aus\nRollen ermitteln +MSW --> Browser: Set-Cookie: authToken +@enduml +---- + +=== Test-Benutzer + +[cols="1,2,1"] +|=== +|Persona |Organisation |Permissions + +|Administrator +|Primär-Organisation +|Alle Org-Berechtigungen + +|=== + +== Authentifizierung + +Die Auth-Prüfung erfolgt in den MSW Handlers: + +[source,typescript] +---- +// Beispiel eines geschützten msw Endpoints +http.post('/reports/approve', async ({ cookies }) => { + const { sub, orgId, permissions } = auth(cookies); + // ... Handler Implementation +}); +---- + +== Hinweise + +* Diese Implementation ist nur für den Prototyp, sollte aber ähnlich sein zu der späteren Produktiv-Implementation +* Keine echte Sicherheit +* Dient nur der Feature-Entwicklung +* Produktive Implementation erfolgt später mit IAM \ No newline at end of file diff --git a/example-templates/react-router/docs/modules/ROOT/pages/mock-data.adoc b/example-templates/react-router/docs/modules/ROOT/pages/mock-data.adoc new file mode 100644 index 0000000..e871f62 --- /dev/null +++ b/example-templates/react-router/docs/modules/ROOT/pages/mock-data.adoc @@ -0,0 +1,46 @@ += Testdaten +:experimental: + +== Überblick + +Das `@repo/mocks` Package generiert synthetische Testdaten für die Entwicklung. Die Daten sind: + +* Realistisch und konsistent +* Typsicher durch TypeScript +* Reproduzierbar durch feste Seeds + +Die Daten werden im Entwicklungs-Modus On-The-Fly beim Reload der Applikation generiert. + +== Generator + +=== Technologie + +* https://fakerjs.dev/[Faker.js] für realistische Daten +* TypeScript für Typsicherheit +* 3 vorgegebene Konfigurationen für unterschiedliche Datenmengen + ** `small`: Für manuelle Prüfung der erstellten Daten. + ** `medium`: Für automatisches Testen und Entwicklung. + ** `full`: Alle Organisationen, für vollständige Tests. + + +=== Verwendung + +Siehe `@repo/mocks/README.md` + +Der Ordner enthält Generator-Klassen für die einzelnen Entitäten. + +In der Datei `dataset.ts` werden die einzelnen Generatoren zu einem Datensatz zusammengefasst. + +== Daten erweitern + +1. Domain-Types in `@repo/api` definieren und falls nötig im lokalen Repository anpassen. +2. Generator in `@repo/mocks/src/generators` erstellen +3. In `dataset.ts` integrieren +4. Build mit `npm run build:data` + +== Best Practices + +* Realistische Wertebereiche verwenden +* Beziehungen zwischen Entitäten beachten +* Lokale Besonderheiten berücksichtigen (z.B. Schweizer Adressen) +* Reproduzierbarkeit durch Seeds sicherstellen \ No newline at end of file diff --git a/example-templates/react-router/docs/modules/ROOT/pages/monorepo.adoc b/example-templates/react-router/docs/modules/ROOT/pages/monorepo.adoc new file mode 100644 index 0000000..f31d30c --- /dev/null +++ b/example-templates/react-router/docs/modules/ROOT/pages/monorepo.adoc @@ -0,0 +1,64 @@ += Monorepo + +Siehe auch README.md. + +Wir verwenden Turborepo mit der https://turbo.build/repo/docs/core-concepts/internal-packages#compiled-packages[Compiled Packages] Strategie. + +Alle Pakete sind ESM-Module und verwenden den `@repo` Scope. + +Dieses Monorepo enthält die folgenden Pakete/Apps: + +== Apps und Pakete + +- `@repo/frontend`: das Frontend für die PLANNING STACK TEMPLATE Anwendung (noch zu erstellen). +- `@repo/prototype`: ein Prototyp für die PLANNING STACK TEMPLATE Anwendung. +- `@repo/api`: ein Paket, das die Domain-Typen und -Enums bereitstellt. +- `@repo/docs`: Dokumentationen für die PLANNING STACK TEMPLATE Anwendung im Antora-Format. +- `@repo/mocks`: ein Paket, das synthetische Daten für die Anwendungen bereitstellt. +- `@repo/mock-api`: ein Paket, das eine Mock-API über Service-Worker bereitstellt. +- `@repo/ui`: eine Basis-React-Komponentenbibliothek, die von den Anwendungen `FAS` und `prototype` + gemeinsam genutzt wird +- `@repo/eslint-config`: `eslint`-Konfigurationen +- `@repo/typescript-config`: `tsconfig.json`s, die im gesamten Monorepo verwendet werden + +== Build + +Die Builds sind alle von https://turbo.build[Turborepo] gesteuert und sollten aus dem +Root-Verzeichnis heraus aufgerufen werden. werden. + +Um alle Apps und Pakete zu bauen, führe den folgenden Befehl aus: + +``` +npm run build +``` + +Es ist auch möglich, nur ein bestimmtes Paket oder eine bestimmte App zu bauen: + +``` +npm run build:data + +npm run build:msw + +npm run build:prototype +``` + +### Entwicklung + +Aktuell ist standardmässig der Prototyp aktiviert. Um die Entwicklung zu starten, führe den +folgenden Befehl aus. Dieser startet einen lokalen Server und watcher für die Abhängigen Pakete. + +``` +npm run dev +``` + + + +== Aufbau + +[plantuml,monorepo,svg,role=component] +---- +include::partial$monorepo.puml[] +---- + +Das Monorepo verwendet die export und import Direktiven von node ESM Modulen. +Alle exportierten Namespaces müssen in der package.json Datei aufgelistet werden. \ No newline at end of file diff --git a/example-templates/react-router/docs/modules/ROOT/pages/prototype.adoc b/example-templates/react-router/docs/modules/ROOT/pages/prototype.adoc new file mode 100644 index 0000000..a0d1223 --- /dev/null +++ b/example-templates/react-router/docs/modules/ROOT/pages/prototype.adoc @@ -0,0 +1,105 @@ += Prototyp +:experimental: + +== Zweck + +Der PLANNING STACK TEMPLATE Prototyp dient als: + +* Testumgebung für neue Features und UX-Konzepte +* Basis für Benutzerfeedback und Iterationen +* Proof-of-Concept für technische Lösungen + +== Struktur + +Die Ordnerstruktur des Prototypen orientiert sich an den Modulen des Produktionssystems. +Zusätzlich gibt es den Ordner 'components' für Modulübergreifende UI-Komponenten und 'hooks' für wiederverwendbare Logik. +Der Ordner 'lib' enthält Hilfsfunktionen. Der Ordner 'mocks' die Logik für die Mock-Daten. + +Die Routen sind über eine Route-Konfiguration in app/routes.ts definiert. +Die Route-Komponenten sind in den entsprechenden Modul-Ordnern. + +== API + +Der Prototyp verwendet das `ky` Paket für HTTP-Anfragen. + + +=== Technischer Aufbau + +* React Router SPA mit TypeScript +* Mock-Backend via MSW +* Synthetische Testdaten +* shadcn/ui Komponenten + +== Entwicklungsprozess + +=== Feature-Entwicklung + +1. Feature-Anforderung erfassen +2. Schnelle Implementierung im Prototyp +3. Review mit Stakeholdern +4. Feedback einarbeiten +5. User-Stories updaten +6. Feature für Produktion freigeben + +=== Guidelines + +* Fokus auf Benutzerinteraktion, nicht auf Datenvalidierung +* TODOs für fehlende Produktionsanforderungen +* Schnelle Iteration über perfekte Implementation +* Realistische Testdaten verwenden + +== Deployment + +Das Deployment erfolgt automatisch über die CI/CD Pipeline: + +=== Umgebungen + +[cols="1,3"] +|=== +|Umgebung |Beschreibung + +|Continuous Integration +|* Deployment bei Push auf `main` +* URL: +* Aktuelle Entwicklungsversion + +|Acceptance Test +|* Deployment bei Tag-Commit +* URL: +* Stabile Version für User-Testing + + +|Preview +|* Preview-Umgebung, z.B. für Merge-Requests +* URL: +* Alternative Version für A/B Tests. +|=== + +Die nötigen Zugangsdaten müssen in den Umgebungsvariablen gesetzt werden. Siehe `.env.example`. + +Das Deployment auf Preview kann bei einem Merge-Request manuell ausgelöst werden. + +=== Zugriff + +* Die Hosts sind über einen IP-Filter geschützt. Die Konfiguration ist in der .htaccess Datei im Ordner `prototype/app/public`. + +* Der Host für die Dokumentation kann nur vom internen Netzwerk aus erreicht werden. + + +== Limitationen + +* Keine echte Backend-Anbindung +* Eingeschränkte Validierung +* Vereinfachte Berechtigungen +* Keine Performance-Optimierung + +== Nächste Schritte + +* Integration von Benutzer-Feedback +* Priorisierung der Features für Produktion +* Identifikation technischer Herausforderungen + +== Bekannte Probleme + +* msw in einer Version höher als 2.6.3 (getestet bis 2.6.6) funktioniert nicht mit dem dev server. Das CSS kann nicht geladen werden. +Aktuell ist die Version auf 2.6.3 gepinnt. Allenfalls später nochmals testen und ggf. einen Github-Issue erstellen. \ No newline at end of file diff --git a/example-templates/react-router/docs/modules/ROOT/pages/tailwind.adoc b/example-templates/react-router/docs/modules/ROOT/pages/tailwind.adoc new file mode 100644 index 0000000..2359bef --- /dev/null +++ b/example-templates/react-router/docs/modules/ROOT/pages/tailwind.adoc @@ -0,0 +1,22 @@ += Tailwind Tipps und Tricks + +Tailwind Docs: https://tailwindcss.com/docs/ + +== State +Die Seite soll barrierefrei sein, daher bietet es sich an, die States falls möglich +auf den jeweiligen Aria-States zu basieren. + +.Beispiel Button +```tsx + + + + + {loaderData.error &&
{loaderData.error.message}
} + + + ); + } + + const { me } = loaderData; + const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen); + + return ( + + + +
+
+
+ + + {me.firstName.charAt(0)} + +
+ {me.firstName} {me.lastName} +
+
+ + +
+
+
+ + +
+ + +
+
+
+
+
+ ); +} + +function NavItem({ + href, + icon, + label, + isOpen, +}: { + href: string; + icon: React.ReactNode; + label: string; + isOpen: boolean; +}) { + return ( + + cn('flex flex-none items-center px-4 py-2 hover:bg-accent', { + 'justify-start': isOpen, + 'justify-center w-12 px-0': !isOpen, + 'rounded-sm': !isOpen, + 'bg-accent': isActive, + 'font-bold': isActive, + }) + } + > + {icon} + { + + {label} + + } + + ); +} + +export function HydrateFallback() { + return ( + +
+
+
+
+ {/* TODO: md Klassen hinzufügen*/} + +
+
+

Lädt...

+
+
+
+
+ ); +} diff --git a/example-templates/react-router/prototype/app/routes.ts b/example-templates/react-router/prototype/app/routes.ts new file mode 100644 index 0000000..6c8e094 --- /dev/null +++ b/example-templates/react-router/prototype/app/routes.ts @@ -0,0 +1,11 @@ +// app/routes.ts +import { type RouteConfig, index, route } from '@react-router/dev/routes'; + +const routes = [ + route('dashboard', './demo/dashboard.tsx'), + route('employees', './demo/employees.tsx'), + route('admin', './demo/admin.tsx'), + index('./routes/_index.tsx'), +] satisfies RouteConfig; + +export default routes; diff --git a/example-templates/react-router/prototype/app/routes/_index.tsx b/example-templates/react-router/prototype/app/routes/_index.tsx new file mode 100644 index 0000000..259a1e7 --- /dev/null +++ b/example-templates/react-router/prototype/app/routes/_index.tsx @@ -0,0 +1,16 @@ +import type { MetaFunction } from 'react-router'; + +export const meta: MetaFunction = () => { + return [{ title: 'PLANNING STACK TEMPLATE Prototype' }]; +}; + +export default function Index() { + return ( +
+

PLANNING STACK TEMPLATE Prototype

+

Dies ist ein klickbarer Prototyp für die PLANNING STACK TEMPLATE-Anwendung.

+

Es dient zum Austesten von Anwendungsfällen und Prozessen.

+

Alle Daten sind synthetisch und es gibt keinen Zugriff auf ein Backend.

+
+ ); +} diff --git a/example-templates/react-router/prototype/app/styles/tailwind.css b/example-templates/react-router/prototype/app/styles/tailwind.css new file mode 100644 index 0000000..f2d7cc2 --- /dev/null +++ b/example-templates/react-router/prototype/app/styles/tailwind.css @@ -0,0 +1,70 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + .lucide { + display: inline-flex; + } + :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + } + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/example-templates/react-router/prototype/app/vite-env.d.ts b/example-templates/react-router/prototype/app/vite-env.d.ts new file mode 100644 index 0000000..e528a04 --- /dev/null +++ b/example-templates/react-router/prototype/app/vite-env.d.ts @@ -0,0 +1,11 @@ +/// + +interface ImportMetaEnv { + readonly VITE_BUILD_NUMBER: string; + readonly VITE_VERSION: string; + // more env variables... +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/example-templates/react-router/prototype/components.json b/example-templates/react-router/prototype/components.json new file mode 100644 index 0000000..b363fb4 --- /dev/null +++ b/example-templates/react-router/prototype/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "app/styles/tailwind.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "#/components", + "utils": "#/lib/utils", + "ui": "#/components/ui", + "lib": "#/lib", + "hooks": "#/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/example-templates/react-router/prototype/eslint.config.mjs b/example-templates/react-router/prototype/eslint.config.mjs new file mode 100644 index 0000000..c95e1ec --- /dev/null +++ b/example-templates/react-router/prototype/eslint.config.mjs @@ -0,0 +1,38 @@ +import baseConfig from '@repo/eslint-config/react'; +import reactCompiler from 'eslint-plugin-react-compiler'; + +export default [ + ...baseConfig, + { + files: ['./app/**/*.tsx'], + plugins: { + 'react-compiler': reactCompiler, + }, + rules: { + 'react-compiler/react-compiler': 'error', + }, + }, + { + languageOptions: { + parserOptions: { + tsconfigRootDir: import.meta.dirname, + }, + }, + // disable some rules for the prototype + rules: { + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/prefer-nullish-coalescing': 'off', + '@typescript-eslint/no-base-to-string': 'off', + 'react/no-unknown-property': 'off', + }, + }, + { + // Rules for shadcn/ui components + files: ['**/components/ui/*.tsx'], + rules: { + 'react/prop-types': 'off', + }, + }, +]; diff --git a/example-templates/react-router/prototype/package.json b/example-templates/react-router/prototype/package.json new file mode 100644 index 0000000..68a6eed --- /dev/null +++ b/example-templates/react-router/prototype/package.json @@ -0,0 +1,58 @@ +{ + "name": "@repo/prototype", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "build": "react-router build", + "dev": "react-router dev", + "clean": "rimraf build && rimraf .turbo", + "lint": "eslint --cache --cache-location ./node_modules/.cache/eslint app/", + "preview": "vite preview", + "typecheck": "react-router typegen && tsc", + "format": "prettier --write ." + }, + "dependencies": { + "@radix-ui/react-avatar": "^1.1.11", + "@radix-ui/react-slot": "^1.2.4", + "@react-router/node": "^7.10.1", + "@repo/api": "*", + "@repo/msw": "*", + "@repo/ui": "*", + "@tinyhttp/cookie": "^2.1.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "isbot": "^5", + "ky": "^1.14.1", + "lodash-es": "^4.17.21", + "lucide-react": "^0.472.0", + "react": "^19.0.2", + "react-dom": "^19.0.2", + "react-router": "^7.10.1", + "tailwind-merge": "^3.4.0", + "tailwindcss-animate": "^1.0.7", + "use-container-queries": "^1.0.0" + }, + "devDependencies": { + "@react-router/dev": "^7.10.1", + "@repo/eslint-config": "*", + "@repo/typescript-config": "*", + "@tailwindcss/typography": "^0.5.19", + "@types/lodash-es": "^4.17.12", + "@types/node": "^22.7.4", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "autoprefixer": "^10.4.22", + "eslint-plugin-react-compiler": "^19.1.0-rc.2", + "msw": "2.12.4", + "postcss": "^8.5.6", + "prettier": "^3.7.4", + "tailwindcss": "^3.4.13" + }, + "msw": { + "workerDirectory": [ + "public" + ] + } +} \ No newline at end of file diff --git a/example-templates/react-router/prototype/postcss.config.js b/example-templates/react-router/prototype/postcss.config.js new file mode 100644 index 0000000..7b75c83 --- /dev/null +++ b/example-templates/react-router/prototype/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/example-templates/react-router/prototype/public/favicon.ico b/example-templates/react-router/prototype/public/favicon.ico new file mode 100644 index 0000000..8830cf6 Binary files /dev/null and b/example-templates/react-router/prototype/public/favicon.ico differ diff --git a/example-templates/react-router/prototype/public/mockServiceWorker.js b/example-templates/react-router/prototype/public/mockServiceWorker.js new file mode 100644 index 0000000..558540f --- /dev/null +++ b/example-templates/react-router/prototype/public/mockServiceWorker.js @@ -0,0 +1,349 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + */ + +const PACKAGE_VERSION = '2.12.4' +const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +addEventListener('install', function () { + self.skipWaiting() +}) + +addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +addEventListener('message', async function (event) { + const clientId = Reflect.get(event.source || {}, 'id') + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +addEventListener('fetch', function (event) { + const requestInterceptedAt = Date.now() + + // Bypass navigation requests. + if (event.request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if ( + event.request.cache === 'only-if-cached' && + event.request.mode !== 'same-origin' + ) { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been terminated (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) +}) + +/** + * @param {FetchEvent} event + * @param {string} requestId + * @param {number} requestInterceptedAt + */ +async function handleRequest(event, requestId, requestInterceptedAt) { + const client = await resolveMainClient(event) + const requestCloneForEvents = event.request.clone() + const response = await getResponse( + event, + client, + requestId, + requestInterceptedAt, + ) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + const serializedRequest = await serializeRequest(requestCloneForEvents) + + // Clone the response so both the client and the library could consume it. + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + isMockedResponse: IS_MOCKED_RESPONSE in response, + request: { + id: requestId, + ...serializedRequest, + }, + response: { + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + headers: Object.fromEntries(responseClone.headers.entries()), + body: responseClone.body, + }, + }, + }, + responseClone.body ? [serializedRequest.body, responseClone.body] : [], + ) + } + + return response +} + +/** + * Resolve the main client for the given event. + * Client that issues a request doesn't necessarily equal the client + * that registered the worker. It's with the latter the worker should + * communicate with during the response resolving phase. + * @param {FetchEvent} event + * @returns {Promise} + */ +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +/** + * @param {FetchEvent} event + * @param {Client | undefined} client + * @param {string} requestId + * @param {number} requestInterceptedAt + * @returns {Promise} + */ +async function getResponse(event, client, requestId, requestInterceptedAt) { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = event.request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter( + (value) => value !== 'msw/passthrough', + ) + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const serializedRequest = await serializeRequest(event.request) + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + interceptedAt: requestInterceptedAt, + ...serializedRequest, + }, + }, + [serializedRequest.body], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +/** + * @param {Client} client + * @param {any} message + * @param {Array} transferrables + * @returns {Promise} + */ +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [ + channel.port2, + ...transferrables.filter(Boolean), + ]) + }) +} + +/** + * @param {Response} response + * @returns {Response} + */ +function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} + +/** + * @param {Request} request + */ +async function serializeRequest(request) { + return { + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.arrayBuffer(), + keepalive: request.keepalive, + } +} diff --git a/example-templates/react-router/prototype/public/robots.txt b/example-templates/react-router/prototype/public/robots.txt new file mode 100644 index 0000000..1f53798 --- /dev/null +++ b/example-templates/react-router/prototype/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/example-templates/react-router/prototype/react-router.config.ts b/example-templates/react-router/prototype/react-router.config.ts new file mode 100644 index 0000000..3591e39 --- /dev/null +++ b/example-templates/react-router/prototype/react-router.config.ts @@ -0,0 +1,6 @@ +import type { Config } from '@react-router/dev/config'; + +export default { + prerender: true, + ssr: false, +} satisfies Config; diff --git a/example-templates/react-router/prototype/tailwind.config.js b/example-templates/react-router/prototype/tailwind.config.js new file mode 100644 index 0000000..21e3faf --- /dev/null +++ b/example-templates/react-router/prototype/tailwind.config.js @@ -0,0 +1,10 @@ +import baseConfig from '@repo/ui/tailwind.config'; + +export default { + ...baseConfig, + darkMode: ['selector'], + content: { + relative: true, + files: ['./app/**/*.tsx', '../ui/src/**/*.tsx'], + }, +}; diff --git a/example-templates/react-router/prototype/tsconfig.json b/example-templates/react-router/prototype/tsconfig.json new file mode 100644 index 0000000..2576016 --- /dev/null +++ b/example-templates/react-router/prototype/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "@repo/typescript-config/react-app.json", + "compilerOptions": { + "baseUrl": ".", + "types": ["@react-router/node", "vite/client"], + "paths": { + "#/*": ["./app/*"] + }, + "outDir": "build", + "rootDirs": ["./", "./.react-router/types"] + }, + "include": [ + ".react-router/types/**/*", + "app/**/*.ts", + "app/**/*.tsx", + "react-router.config.ts", + "vite.config.ts" + ], + "exclude": ["node_modules"] +} diff --git a/example-templates/react-router/prototype/vite.config.ts b/example-templates/react-router/prototype/vite.config.ts new file mode 100644 index 0000000..d89a387 --- /dev/null +++ b/example-templates/react-router/prototype/vite.config.ts @@ -0,0 +1,26 @@ +import { reactRouter } from '@react-router/dev/vite'; +import { defineConfig } from 'vite'; +import tsconfigPaths from 'vite-tsconfig-paths'; + +export default defineConfig({ + plugins: [reactRouter(), tsconfigPaths()], + resolve: { + preserveSymlinks: false, + }, + optimizeDeps: { + exclude: + process.env.NODE_ENV === 'development' + ? [ + '@repo/msw/medium', + '@repo/msw/handlers', + '@repo/api/stammdaten/enums', + '@repo/ui', + ] + : [], + }, + server: { + watch: { + followSymlinks: true, // Vite beobachtet auch verlinkte Dependencies + }, + }, +}); diff --git a/example-templates/react-router/turbo.json b/example-templates/react-router/turbo.json new file mode 100644 index 0000000..835bd45 --- /dev/null +++ b/example-templates/react-router/turbo.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://turbo.build/schema.json", + "ui": "tui", + "tasks": { + "build": { + "dependsOn": ["^build"], + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "outputs": ["dist/**", "build/client/**"], + "env": ["VITE_BUILD_NUMBER", "VITE_VERSION"] + }, + "@repo/mocks#generate:medium": { + "dependsOn": ["^@repo/mocks#build"], + "outputs": ["data-mocks/mocks.medium.json", "data-mocks/hints.medium.json"] + }, + "@repo/mocks#generate:small": { + "dependsOn": ["^@repo/mocks#build"], + "outputs": ["data-mocks/mocks.small.json", "data-mocks/hints.small.json"] + }, + "build:data": { + "dependsOn": ["^@repo/mocks#generate:small", "^@repo/mocks#generate:medium"] + }, + "lint": {}, + "test": {}, + "dev": { + "cache": false, + "persistent": true + }, + "docs": { + "inputs": ["$TURBO_DEFAULT$", "antora-playbook.yml"], + "outputs": ["build/**"] + }, + "clean": { + "cache": false, + "dependsOn": [] + }, + "typecheck": { + "outputs": ["dist/**"], + "dependsOn": ["^typecheck"] + }, + "@repo/prototype#typecheck": { + "outputs": [".react-router/**"], + "dependsOn": ["^typecheck"] + }, + "@repo/prototype#build": { + "inputs": ["public/**", "react-router.config.ts", "vite.config.ts", "taiwind.config.js"], + "dependsOn": ["^@repo/msw#build"], + "outputs": ["build/client/**"], + "env": ["VITE_BUILD_NUMBER", "VITE_VERSION"] + }, + "@repo/frontend#typecheck": { + "outputs": [".react-router/**"], + "dependsOn": ["^typecheck"] + }, + "@repo/ui#typecheck": { + "outputs": [], + "dependsOn": ["^typecheck"] + } + } +} diff --git a/example-templates/react-router/ui/README.md b/example-templates/react-router/ui/README.md new file mode 100644 index 0000000..c07fb5a --- /dev/null +++ b/example-templates/react-router/ui/README.md @@ -0,0 +1,6 @@ +# PLANNING STACK TEMPLATE UI + +Dieses Paket enthält die UI-Komponenten für das PLANNING STACK TEMPLATE. Das UI basiert auf +[Tailwind UI](https://tailwindui.com/) und [Shadcn](https://ui.shadcn.com/). + +Die einzelnen Komponenten werden als Node-Pfad exportiert (im package.json). diff --git a/example-templates/react-router/ui/eslint.config.mjs b/example-templates/react-router/ui/eslint.config.mjs new file mode 100644 index 0000000..82a7c74 --- /dev/null +++ b/example-templates/react-router/ui/eslint.config.mjs @@ -0,0 +1,21 @@ +import baseConfig from '@repo/eslint-config/react'; +import jsxA11yPlugin from 'eslint-plugin-jsx-a11y'; + +export default [ + ...baseConfig, + jsxA11yPlugin.flatConfigs.recommended, + { + languageOptions: { + parserOptions: { + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + // Rules for shadcn/ui components + files: ['**/ui/*.tsx'], + rules: { + 'react/prop-types': 'off', + }, + }, +]; diff --git a/example-templates/react-router/ui/package.json b/example-templates/react-router/ui/package.json new file mode 100644 index 0000000..0145510 --- /dev/null +++ b/example-templates/react-router/ui/package.json @@ -0,0 +1,53 @@ +{ + "name": "@repo/ui", + "version": "0.0.0", + "sideEffects": false, + "private": true, + "exports": { + "./scroll-area": "./src/ui/scroll-area.tsx", + "./tailwind.config": "./tailwind.config.js", + "./empty-state": "./src/ui/empty-state.tsx" + }, + "imports": { + "#utils": "./src/lib/utils.ts", + "#ui/*": "./src/ui/*" + }, + "files": [ + "src" + ], + "scripts": { + "lint": "eslint src --max-warnings 0", + "typecheck": "tsc --noEmit", + "generate:component": "turbo gen react-component" + }, + "devDependencies": { + "@repo/eslint-config": "*", + "@repo/typescript-config": "*", + "@tailwindcss/container-queries": "^0.1.1", + "@turbo/gen": "^2.6.3", + "@types/eslint": "^9.6.1", + "@types/node": "^22.7.4", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "eslint-plugin-jsx-a11y": "^6.10.2", + "react": "^19.0.2", + "react-dom": "^19.0.2", + "typescript": "^5.9.3" + }, + "dependencies": { + "@radix-ui/react-avatar": "^1.1.11", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-slot": "^1.2.4", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.472.0", + "tailwind-merge": "^3.4.0", + "tailwindcss": "^3.4.13", + "tailwindcss-animate": "^1.0.7", + "use-container-queries": "^1.0.0" + }, + "peerDependencies": { + "react": ">=19.0.0" + }, + "type": "module" +} \ No newline at end of file diff --git a/example-templates/react-router/ui/src/lib/utils.ts b/example-templates/react-router/ui/src/lib/utils.ts new file mode 100644 index 0000000..9fc54a4 --- /dev/null +++ b/example-templates/react-router/ui/src/lib/utils.ts @@ -0,0 +1,20 @@ +import { clsx, type ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +/** Class-Name-Utility that supports TailwindCSS merge */ +export function cn(...inputs: ClassValue[]): string { + return twMerge(clsx(inputs)); +} + +const prefersReducedMotion = + typeof window === 'undefined' + ? true + : window.matchMedia('(prefers-reduced-motion: reduce)').matches; + +/** + * View Transition API compatibility layer for non-browser environments. + */ +export const viewTransition = + !prefersReducedMotion && globalThis.document && 'startViewTransition' in document + ? globalThis.document.startViewTransition.bind(globalThis.document) + : (cb: () => void) => cb(); diff --git a/example-templates/react-router/ui/src/ui/empty-state.tsx b/example-templates/react-router/ui/src/ui/empty-state.tsx new file mode 100644 index 0000000..76e0337 --- /dev/null +++ b/example-templates/react-router/ui/src/ui/empty-state.tsx @@ -0,0 +1,19 @@ +export function EmptyState({ + children, + icon, +}: { + children?: React.ReactNode; + icon?: React.ReactNode; +}) { + return ( +
+ {icon && ( +
+ {icon} +
+ )} +
{children}
+
+
+ ); +} diff --git a/example-templates/react-router/ui/src/ui/scroll-area.tsx b/example-templates/react-router/ui/src/ui/scroll-area.tsx new file mode 100644 index 0000000..2ff7011 --- /dev/null +++ b/example-templates/react-router/ui/src/ui/scroll-area.tsx @@ -0,0 +1,44 @@ +import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; +import * as React from 'react'; + +import { cn } from '#utils'; + +const ScrollArea = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + {children} + + + + +)); +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; + +const ScrollBar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, orientation = 'vertical', ...props }, ref) => ( + + + +)); +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; + +export { ScrollArea, ScrollBar }; diff --git a/example-templates/react-router/ui/tailwind.config.js b/example-templates/react-router/ui/tailwind.config.js new file mode 100644 index 0000000..a5dd77b --- /dev/null +++ b/example-templates/react-router/ui/tailwind.config.js @@ -0,0 +1,60 @@ +import containerQueries from '@tailwindcss/container-queries'; // Obsolete in Tailwind 4 +import typography from '@tailwindcss/typography'; +import animate from 'tailwindcss-animate'; + +/** @type {import('tailwindcss').Config} */ +export default { + darkMode: 'selector', + theme: { + extend: { + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + }, + colors: { + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + chart: { + 1: 'hsl(var(--chart-1))', + 2: 'hsl(var(--chart-2))', + 3: 'hsl(var(--chart-3))', + 4: 'hsl(var(--chart-4))', + 5: 'hsl(var(--chart-5))', + }, + }, + }, + }, + plugins: [typography, animate, containerQueries], +}; diff --git a/example-templates/react-router/ui/tsconfig.json b/example-templates/react-router/ui/tsconfig.json new file mode 100644 index 0000000..dcc232f --- /dev/null +++ b/example-templates/react-router/ui/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@repo/typescript-config/react-library.json", + "compilerOptions": { + "baseUrl": "." + }, + "include": ["./src/**/*.ts", "./src/**/*.tsx"], + "exclude": ["node_modules", "dist"] +} diff --git a/example-templates/react-router/ui/turbo/generators/config.ts b/example-templates/react-router/ui/turbo/generators/config.ts new file mode 100644 index 0000000..b5375c2 --- /dev/null +++ b/example-templates/react-router/ui/turbo/generators/config.ts @@ -0,0 +1,30 @@ +import type { PlopTypes } from '@turbo/gen'; + +// Learn more about Turborepo Generators at https://turbo.build/repo/docs/core-concepts/monorepos/code-generation + +export default function generator(plop: PlopTypes.NodePlopAPI): void { + // A simple generator to add a new React component to the internal UI library + plop.setGenerator('react-component', { + description: 'Adds a new react component', + prompts: [ + { + type: 'input', + name: 'name', + message: 'What is the name of the component?', + }, + ], + actions: [ + { + type: 'add', + path: 'src/{{kebabCase name}}.tsx', + templateFile: 'templates/component.hbs', + }, + { + type: 'append', + path: 'package.json', + pattern: /"exports": {(?)/g, + template: ' "./{{kebabCase name}}": "./src/{{kebabCase name}}.tsx",', + }, + ], + }); +} diff --git a/example-templates/react-router/ui/turbo/generators/templates/component.hbs b/example-templates/react-router/ui/turbo/generators/templates/component.hbs new file mode 100644 index 0000000..d968b9e --- /dev/null +++ b/example-templates/react-router/ui/turbo/generators/templates/component.hbs @@ -0,0 +1,8 @@ +export const {{ pascalCase name }} = ({ children }: { children: React.ReactNode }) => { + return ( +
+

{{ pascalCase name }} Component

+ {children} +
+ ); +}; diff --git a/package-lock.json b/package-lock.json index d032fa1..47cd134 100644 --- a/package-lock.json +++ b/package-lock.json @@ -835,9 +835,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", - "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", + "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", "cpu": [ "arm" ], @@ -849,9 +849,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", - "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", + "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", "cpu": [ "arm64" ], @@ -863,9 +863,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", - "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", + "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", "cpu": [ "arm64" ], @@ -877,9 +877,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", - "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", + "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", "cpu": [ "x64" ], @@ -891,9 +891,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", - "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", + "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", "cpu": [ "arm64" ], @@ -905,9 +905,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", - "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", + "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", "cpu": [ "x64" ], @@ -919,13 +919,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", - "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", + "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -933,13 +936,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", - "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", + "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -947,13 +953,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", - "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", + "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -961,13 +970,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", - "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", + "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -975,13 +987,33 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", - "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", + "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", + "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", "cpu": [ "loong64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -989,13 +1021,33 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", - "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", + "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", + "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1003,13 +1055,16 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", - "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", + "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1017,13 +1072,16 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", - "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", + "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1031,13 +1089,16 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", - "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", + "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1045,13 +1106,16 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", - "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", + "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1059,23 +1123,40 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", - "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", + "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", + "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", - "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", + "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", "cpu": [ "arm64" ], @@ -1087,9 +1168,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", - "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", + "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", "cpu": [ "arm64" ], @@ -1101,9 +1182,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", - "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", + "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", "cpu": [ "ia32" ], @@ -1115,9 +1196,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", - "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", + "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", "cpu": [ "x64" ], @@ -1129,9 +1210,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", - "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", + "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", "cpu": [ "x64" ], @@ -1384,9 +1465,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -2241,9 +2322,9 @@ } }, "node_modules/fast-xml-parser": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.6.tgz", - "integrity": "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA==", + "version": "5.5.9", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.9.tgz", + "integrity": "sha512-jldvxr1MC6rtiZKgrFnDSvT8xuH+eJqxqOBThUVjYrxssYTo1avZLGql5l0a0BAERR01CadYzZ83kVEkbyDg+g==", "dev": true, "funding": [ { @@ -2253,12 +2334,30 @@ ], "license": "MIT", "dependencies": { - "strnum": "^2.1.2" + "fast-xml-builder": "^1.1.4", + "path-expression-matcher": "^1.2.0", + "strnum": "^2.2.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, + "node_modules/fast-xml-parser/node_modules/fast-xml-builder": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", + "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.1.3" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2589,9 +2688,9 @@ "license": "ISC" }, "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3189,9 +3288,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -3414,6 +3513,22 @@ "node": ">=8" } }, + "node_modules/path-expression-matcher": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz", + "integrity": "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -3468,9 +3583,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -3744,9 +3859,9 @@ } }, "node_modules/rollup": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", - "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "version": "4.60.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", + "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3760,28 +3875,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.53.3", - "@rollup/rollup-android-arm64": "4.53.3", - "@rollup/rollup-darwin-arm64": "4.53.3", - "@rollup/rollup-darwin-x64": "4.53.3", - "@rollup/rollup-freebsd-arm64": "4.53.3", - "@rollup/rollup-freebsd-x64": "4.53.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", - "@rollup/rollup-linux-arm-musleabihf": "4.53.3", - "@rollup/rollup-linux-arm64-gnu": "4.53.3", - "@rollup/rollup-linux-arm64-musl": "4.53.3", - "@rollup/rollup-linux-loong64-gnu": "4.53.3", - "@rollup/rollup-linux-ppc64-gnu": "4.53.3", - "@rollup/rollup-linux-riscv64-gnu": "4.53.3", - "@rollup/rollup-linux-riscv64-musl": "4.53.3", - "@rollup/rollup-linux-s390x-gnu": "4.53.3", - "@rollup/rollup-linux-x64-gnu": "4.53.3", - "@rollup/rollup-linux-x64-musl": "4.53.3", - "@rollup/rollup-openharmony-arm64": "4.53.3", - "@rollup/rollup-win32-arm64-msvc": "4.53.3", - "@rollup/rollup-win32-ia32-msvc": "4.53.3", - "@rollup/rollup-win32-x64-gnu": "4.53.3", - "@rollup/rollup-win32-x64-msvc": "4.53.3", + "@rollup/rollup-android-arm-eabi": "4.60.0", + "@rollup/rollup-android-arm64": "4.60.0", + "@rollup/rollup-darwin-arm64": "4.60.0", + "@rollup/rollup-darwin-x64": "4.60.0", + "@rollup/rollup-freebsd-arm64": "4.60.0", + "@rollup/rollup-freebsd-x64": "4.60.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", + "@rollup/rollup-linux-arm-musleabihf": "4.60.0", + "@rollup/rollup-linux-arm64-gnu": "4.60.0", + "@rollup/rollup-linux-arm64-musl": "4.60.0", + "@rollup/rollup-linux-loong64-gnu": "4.60.0", + "@rollup/rollup-linux-loong64-musl": "4.60.0", + "@rollup/rollup-linux-ppc64-gnu": "4.60.0", + "@rollup/rollup-linux-ppc64-musl": "4.60.0", + "@rollup/rollup-linux-riscv64-gnu": "4.60.0", + "@rollup/rollup-linux-riscv64-musl": "4.60.0", + "@rollup/rollup-linux-s390x-gnu": "4.60.0", + "@rollup/rollup-linux-x64-gnu": "4.60.0", + "@rollup/rollup-linux-x64-musl": "4.60.0", + "@rollup/rollup-openbsd-x64": "4.60.0", + "@rollup/rollup-openharmony-arm64": "4.60.0", + "@rollup/rollup-win32-arm64-msvc": "4.60.0", + "@rollup/rollup-win32-ia32-msvc": "4.60.0", + "@rollup/rollup-win32-x64-gnu": "4.60.0", + "@rollup/rollup-win32-x64-msvc": "4.60.0", "fsevents": "~2.3.2" } }, @@ -4054,9 +4172,9 @@ } }, "node_modules/strnum": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", - "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz", + "integrity": "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==", "dev": true, "funding": [ { @@ -4526,9 +4644,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "dev": true, "license": "ISC", "bin": { diff --git a/package.json b/package.json index 55860e0..4ac427f 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "type": "module", "files": [ "template", + "example-templates", "create" ], "engines": { @@ -32,12 +33,12 @@ "create-tinker-stack" ], "scripts": { - "prerelease": "find template -name package-lock.json -delete && find template -name tsconfig.tsbuildinfo -delete && find template -name .turbo -type d -prune -exec rm -rf {} +", + "prerelease": "find template example-templates -name package-lock.json -delete && find template example-templates -name tsconfig.tsbuildinfo -delete && find template example-templates -name .turbo -type d -prune -exec rm -rf {} +", "release": "commit-and-tag-version", "test": "npm run test:e2e", "test:e2e": "vitest run --no-file-parallelism", "upgrade:template": "npx npm-check-updates --interactive --format group -c 2 -w --cwd template", - "postupgrade:template": "find template -name package-lock.json -delete && find template -name node_modules -type d -prune -exec rm -rf {} +", + "postupgrade:template": "find template example-templates -name package-lock.json -delete && find template example-templates -name node_modules -type d -prune -exec rm -rf {} +", "upgrade:root": "npx npm-check-updates --interactive --format group -c 2" }, "dependencies": { diff --git a/specs/example-project.md b/specs/example-project.md new file mode 100644 index 0000000..69a19e2 --- /dev/null +++ b/specs/example-project.md @@ -0,0 +1,74 @@ +# Example Project Toggle Spec + +## Background +- The generator `create/main.js` copies the entire `template/` workspace into the target directory and immediately runs setup commands, producing a project pre-populated with demo data and demo UI flows. +- Removing the example today requires deep edits across `prototype`, `mock-backend`, and `mock-data`, plus updates to docs and build scripts. Developers struggle to strip the demo without breaking interconnected references to demo APIs, dataset builders, and MSW handlers. +- Goal: ship a clean, empty project by default while preserving the current demo as an opt-in bundle. + +## Current Template State +- **Prototype (`template/prototype`)** + - Routes are hard-wired to demo screens (`app/routes.ts` routing into `app/demo/*.tsx`, e.g. `dashboard.tsx`, `employees.tsx`) that assume MSW demo endpoints. + - Root loader/action logic (`app/root.tsx`) fetches demo user data (`DemoApi` from `app/demo/api.ts`) and renders demo-specific login buttons, navigation, and layout. + - Local MSW setup (`app/mocks/handlers.ts`) imports `@repo/mock-backend/medium` so the app only works with the demo dataset. +- **Mock backend (`template/mock-backend/src`)** + - Provides dataset-bound handlers (`handlers/demo.ts`) and dataset presets (`medium`, `full`) that generate demo responses by reading demo dataset indices. + - Auth utilities (`auth.ts`) expect JWT payloads populated by the demo dataset. +- **Mock data (`template/mock-data`)** + - Dataset generator (`src/generators/dataset.ts`) creates seeded data for companies, persons, roles, and auth tokens that power demo flows. + - CLI (`cli/generate-data.mjs`) and published JSON fixtures (`data-mocks/*.json`) serialize the demo dataset in small/medium/full sizes. +- **Generator behavior** + - `create/main.js` blindly copies `template/` and replaces tokens, so the default scaffold always contains demo code, fixtures, and run scripts that assume the dataset can build immediately. + - Automated tests (`tests/generator.test.mjs`) assert that the generated project can run `npm run build:data` and `npm run build`, implicitly relying on demo assets. + +## Desired Behavior & Requirements +1. **Empty project by default** + - Generated workspace should have the same package layout but with neutral starter files: prototype is a skeleton shell, mock backend/data contain minimal no-op implementations (or are omitted) so new teams can build from scratch. + - Default `npm run build`, `npm run typecheck`, and other scripts must succeed without demo data. +2. **Opt-in example bundle** + - Introduce a generator flag (e.g. `--with-example`, `withExample` option from `npm init`) that copies the existing demo content into a sibling `example/` directory _inside_ the generated project. + - The `example/` directory should mirror the current template structure (`prototype`, `mock-data`, `mock-backend`, etc.) so teams can browse or copy patterns without polluting the main workspace. + - Example assets should remain runnable in-place (document how to install & build the example). + - It must be possible to delete the example directory without breaking any dependencies. +3. **Flag plumbing** + - Update argument normalization in `create/index.js` so CLI flags / `npm init` options expose `withExample` (boolean). + - Modify `create/main.js` to: + - Copy the slim template into the project root. + - When `withExample` is true, additionally copy the rich demo tree into `targetDir/example`. + - Ensure environment token replacement, dependency installs, and format commands only touch the base project; example content can be left untouched or bootstrapped separately per spec decisions. +4. **Documentation & DX** + - Document the new flag and default-empty behavior in `README.md` (both repository root and generated template readmes as needed). + - Provide guidance inside `example/README.md` (new or repurposed) explaining how to run the demo and what parts can be copied into the main app. +5. **Testing & automation** + - Expand `tests/generator.test.mjs` (or add new cases) to cover: + - Default scaffold produces empty baseline and still passes install/typecheck/build (with updated expectations). + - `--with-example` scaffolds the extra directory and preserves demo assets (spot-check key files or run targeted commands if feasible). + - Ensure CI steps or scripts that assume demo data are updated or gated behind the flag. + +## Implementation Notes & Work Items +- Refactor template content: + - Extract current demo sources (`prototype/app/demo`, associated context providers, MSW handlers, dataset generation, mock backend handlers, `mock-data` presets) into a new source tree destined for `example/`. + - Introduce lightweight placeholders in the base template: e.g., a blank `prototype` with a single welcome page, stubbed mock-data scripts that return empty structures, and no prebuilt JSON fixtures. + - Decide whether base template should keep mock packages at all (empty package with placeholder README) vs. omit them; ensure workspaces remain valid. +- Update generator copy logic: + - Allow copying multiple source roots (base template and example bundle) while respecting existing token replacement (app name/title) without touching example content unless required. + - Consider deduplicating shared config (eslint/tsconfig) so both base and example can reuse it without duplication. + - Guard install/format steps when `withExample` is used (e.g., do not attempt to install dependencies inside `example/` unless explicitly desired). +- Data & auth considerations: + - Empty project should not ship demo JWT payloads or auth stubs; evaluate whether to include a minimal `auth` scaffold or leave instructions for developers to implement. + - When `withExample` is chosen, ensure dataset presets and JSON outputs remain accessible (copy existing `data-mocks/*.json`). +- Migration of docs & scripts: + - Audit references to demo flows in `template/docs` and update to reflect new optional example. + - Update `README` badges, quick-start instructions, and `docs/modules/ROOT/pages` to describe both baseline and example usage. +- Ensure `npm run build:data` and related scripts degrade gracefully when the dataset is empty (either no-op or produce placeholder files). + +## Open Questions +- Should the CLI prompt the user interactively about including the example when running without `--with-example`? +- Should the example directory be completely independent (own package.json/turbo config), or share the monorepo root and rely on workspaces? +- Do we need to ship prebuilt mock JSON for the empty template (e.g., empty arrays) to satisfy downstream tooling? +- How do we handle token replacement inside the example bundle—do we mirror replacements there or leave the original `planning-stack-template` strings? + +## Validation Plan +- Run generator default path with `SKIP_SETUP=1` to ensure scaffolding succeeds and key files contain the expected placeholders. +- Run generator with `--with-example`, install dependencies, and verify both the base project and the `example/` bundle can execute `npm run build:data`, `npm run build`, and `npm run dev` (document which commands should run where). +- Add automated test coverage for the new flag and update snapshots/expectations accordingly. +- Confirm lint/typecheck/build commands in the repository (root) remain green after restructuring template assets. diff --git a/template/.prettierignore b/template/.prettierignore index c7e2c6b..b40141c 100644 --- a/template/.prettierignore +++ b/template/.prettierignore @@ -1,6 +1,7 @@ /node_modules +/examples package-lock.json .gitignore .cursorrules.md **/public -.htaccess \ No newline at end of file +.htaccess diff --git a/template/README.md b/template/README.md index 0b93ae2..35c9cfc 100644 --- a/template/README.md +++ b/template/README.md @@ -1,76 +1,45 @@ # PLANNING STACK TEMPLATE Frontend Monorepo -Dieses Monorepo enthält alle Module und Apps für das PLANNING STACK TEMPLATE Frontend. Inklusive der -Testdaten und des Prototyps. +This monorepo is the default Tinker Stack starter for PLANNING STACK TEMPLATE. -## Was ist enthalten? +It is intentionally lightweight: -Dieses Monorepo enthält die folgenden Pakete/Apps: - -### Apps und Pakete - -- `@repo/prototype`: ein Prototyp für die PLANNING STACK TEMPLATE Anwendung. -- `@repo/api`: ein Paket, das die Domain-Typen und -Enums bereitstellt. -- `@repo/docs`: Dokumentationen für die PLANNING STACK TEMPLATE Anwendung im Antora-Format. -- `@repo/mocks`: ein Paket, das synthetische Daten für die Anwendungen bereitstellt. -- `@repo/mock-api`: ein Paket, das eine Mock-API über Service-Worker bereitstellt. -- `@repo/ui`: eine Basis-React-Komponentenbibliothek, die von den Anwendungen - `PLANNING STACK TEMPLATE` und `prototype` gemeinsam genutzt wird -- `@repo/eslint-config`: `eslint`-Konfigurationen (enthält `eslint-config-next` und - `eslint-config-prettier`) -- `@repo/typescript-config`: `tsconfig.json`s, die im gesamten Monorepo verwendet werden - -Jedes Paket/jede App ist in [TypeScript](https://www.typescriptlang.org/) geschrieben. - -## Dokumentation - -Die Entwickler-Dokumentation befindet sich im Ordner `docs`. Sie wird automatisch mittels -Gitlab-Pages -[hier](https://frontend-6dd0b0.pages.ti8m.ch/planning-stack-template-frontend/HEAD/index.html) -veröffentlicht. - -Die Projekt-Dokumentation ist in [AsciiDoc](https://asciidoctor.org/) verfasst und wird mittels -[Antora](https://antora.org/) generiert. +- `@repo/prototype`: a minimal React Router prototype shell +- `@repo/api`: shared domain types and enums +- `@repo/docs`: Antora-based project documentation +- `@repo/mocks`: starter-safe mock-data package +- `@repo/msw`: starter-safe MSW package +- `@repo/ui`: shared UI primitives ## Build -Die Builds sind alle von [Turborepo](https://turbo.build) gesteuert und sollten aus dem -Root-Verzeichnis heraus aufgerufen werden. werden. - -Um alle Apps und Pakete zu bauen, führe den folgenden Befehl aus: +Run commands from the repository root: -``` +```bash npm run build +npm run typecheck +npm run test ``` -Es ist auch möglich, nur ein bestimmtes Paket oder eine bestimmte App zu bauen: - -``` -npm run build:data - -npm run build:msw +`npm run build:data` is available for the `@repo/mocks` package and succeeds even before you add +real generators. -npm run build:prototype -``` +## Development -### Entwicklung +Start the prototype and package watchers: -Bevor der Dev-Server zum ersten Mal gestartet werden kann, müssen die Mock-Daten gebaut werden. - -``` -npm run build:data +```bash +npm run dev ``` -Aktuell ist standardmässig der Prototyp aktiviert. Um die Entwicklung zu starten, führe den -folgenden Befehl aus. Dieser startet einen lokalen Server und watcher für die Abhängigen Pakete. +## Examples -``` -npm run dev -``` +Optional examples, when generated, live under `examples//`. -### Publish +They are independent from this monorepo: -Der Prototyp sollte automatisch bei einem Push auf die `main`-Branch auf einen Preview-Server -deployed werden. +- they are not listed in the root workspaces +- they are not built by default Turbo commands +- they can be installed, run, or deleted separately -Die Dokumentation wird bei einem Push auf die `main`-Branch mittels Gitlab-Pages deployed. +See the README inside each example directory for its own setup and copyable patterns. diff --git a/template/api/package.json b/template/api/package.json index c297c84..8c3f6fe 100644 --- a/template/api/package.json +++ b/template/api/package.json @@ -27,9 +27,6 @@ "./commons/enums": { "types": "./dist/commons/enums.d.ts", "import": "./dist/commons/enums.js" - }, - "./demo/types": { - "types": "./dist/demo/types.d.ts" } }, "lint-staged": { @@ -40,4 +37,4 @@ "prettier --write" ] } -} \ No newline at end of file +} diff --git a/template/docs/modules/ROOT/nav.adoc b/template/docs/modules/ROOT/nav.adoc index f8c4bf7..fcf89b2 100644 --- a/template/docs/modules/ROOT/nav.adoc +++ b/template/docs/modules/ROOT/nav.adoc @@ -1,11 +1,11 @@ :product: PLANNING STACK TEMPLATE -* xref:index.adoc[Übersicht] -** xref:getting-started.adoc[Erste Schritte] -** xref:architecture.adoc[Architektur] +* xref:index.adoc[Overview] +** xref:getting-started.adoc[Getting Started] +** xref:architecture.adoc[Architecture] ** xref:monorepo.adoc[Monorepo] -** xref:prototype.adoc[Prototyp] -** xref:mocks.adoc[Testdaten] +** xref:prototype.adoc[Prototype] +** xref:mock-data.adoc[Mock Data] ** xref:design-system.adoc[Design System] ** xref:login.adoc[Login Flow] -** xref:tailwind.adoc[Tailwind] \ No newline at end of file +** xref:tailwind.adoc[Tailwind] diff --git a/template/docs/modules/ROOT/pages/architecture.adoc b/template/docs/modules/ROOT/pages/architecture.adoc index a940995..df7e4cb 100644 --- a/template/docs/modules/ROOT/pages/architecture.adoc +++ b/template/docs/modules/ROOT/pages/architecture.adoc @@ -1,145 +1,80 @@ -= Architektur += Architecture :experimental: :plantuml-server-url: http://www.plantuml.com/plantuml :source-highlighter: highlight.js -== Überblick +== Overview -Das PLANNING STACK TEMPLATE Frontend ist als Monorepo mit mehreren Packages aufgebaut. Der Fokus liegt auf: +The PLANNING STACK TEMPLATE frontend is structured as a monorepo. The focus is on: -* Schnelle Iteration durch Prototyping -* Wiederverwendbare Komponenten -* Typsicherheit durch TypeScript -* Realistische Testdaten +* fast iteration +* clearly separated packages +* type safety through TypeScript +* optional, independent reference examples -== Architekturprinzipien +== Architecture principles -=== Domain-Driven Design +=== Starter first +The base project should be immediately installable and buildable without forcing a pre-built demo. +=== Prototype-driven development +The recommended workflow remains: -=== Prototypen-getriebene Entwicklung +1. Model the feature in the prototype first +2. Gather feedback +3. Carry validated patterns into the production application -Der Entwicklungsprozess folgt einem iterativen Ansatz: +=== Independent examples -1. Features werden zuerst im Prototyp implementiert -2. Designer-, Stakeholder- und Benutzer-Feedback wird eingeholt -3. Validierte Features werden in die produktive Anwendung übernommen +Optional examples live outside the root workspaces under `examples//`. -Dies ermöglicht: +This means: -* Reduzierung von Entwicklungsrisiken -* Validierung von UX-Konzepten -* Schnelles Feedback von Benutzern +* no automatic participation in `npm run build` +* no coupling to the root Turbo pipeline +* safe to delete without side effects in the main project -=== Typsicherheit +== Technical architecture -* Strikte TypeScript Konfiguration -* Zentrale Definition von Domain Types -* Shared Types zwischen Packages - -=== Komponenten-Bibliothek - -Die UI-Komponenten sind in einem separaten Package (`@repo/ui`) organisiert: - -* Basis auf shadcn/ui, Tailwind UI und Tailwind -* Wiederverwendbar zwischen Prototyp und Produktion -* Einheitliches Design-System basierend auf Tailwind UI und dem Figma Design System - -== Technische Architektur - -Siehe auch xref:monorepo.adoc#aufbau[Monorepo]. - -=== Frontend Stack +=== Frontend stack [cols="1,2,2"] |=== -|Technologie |Verwendung |Begründung +|Technology |Usage |Rationale |React -|UI Framework -|Etabliert, grosses Ökosystem +|UI framework +|Established, large ecosystem |React Router -|Full-Stack Framework -|Gute DX, Server Components +|Starter prototype +|Fast routing and good DX |Tailwind -|CSS Framework -|Utility-First, Wartbar - -|shadcn/ui -|UI-Komponenten -|Zugänglich, Anpassbar +|CSS framework +|Fast, consistent UI development |TypeScript -|Programmiersprache -|Typsicherheit, DX +|Programming language +|Type safety and refactoring confidence |=== -=== Mock Backend +=== Mocking [plantuml,msw,svg] ---- @startuml actor Browser -participant "React Router App" as App +participant "Prototype" as App participant "MSW" as MSW -database "Mock Data" as Data Browser -> App: Request -App -> MSW: API Call -MSW -> Data: Load Data -Data --> MSW: Data -MSW --> App: Response +App -> MSW: Optional API Call +MSW --> App: Mocked or passthrough response App --> Browser: Render @enduml ---- -* MSW (Mock Service Worker) für API-Simulation -* Realistische HTTP-Requests -* Einfacher Wechsel zu echtem Backend - -== Entwicklungsprozess - -=== Feature Entwicklung - -1. Feature-Anforderung -2. Prototyp Implementation -3. User Testing -4. Feedback Integration -5. Production Implementation - -=== Code Organisation - -* Feature-basierte Ordnerstruktur -* Shared Logic in Utils -* Typed API Boundaries - -== Deployment - -=== Prototype - -* Automatisches Deployment bei Push auf `main` -** URL: -** Aktuelle Entwicklungsversion -* Deployment bei Tag-Commit -** URL: -** Stabile Version für User-Testing - -=== Documentation - -* Antora Site Generator -* Deployed zusammen mit Prototype -* Automatische Updates - -== Supporting Subdomain - - -== Zukünftige Entwicklung - -* Migration zu Production App -* API Integration -* E2E Tests -* Performance Monitoring \ No newline at end of file +The base provides only a starter-safe MSW layer. Project-specific handlers and datasets are added later or copied from an optional example. diff --git a/template/docs/modules/ROOT/pages/design-system.adoc b/template/docs/modules/ROOT/pages/design-system.adoc index ffa094c..4ea71b8 100644 --- a/template/docs/modules/ROOT/pages/design-system.adoc +++ b/template/docs/modules/ROOT/pages/design-system.adoc @@ -1,13 +1,13 @@ = Design System :experimental: -== Überblick +== Overview -== Komponenten +== Components -=== Verwendung +=== Usage [source,tsx] ---- @@ -25,33 +25,33 @@ export function IconButton({ icon: Icon }) { == Theming -=== Figma Integration +=== Figma integration -* Designs aus Figma Projekt -* Farben und Typography synchron halten -* Komponenten-Patterns übernehmen +* Designs from Figma project +* Keep colors and typography in sync +* Adopt component patterns -=== Tailwind Konfiguration +=== Tailwind configuration -* Custom Colors aus CI -* Spacing und Typography nach Design System -* Erweiterbare Struktur +* Custom colors from CI +* Spacing and typography per design system +* Extensible structure -Siehe auch xref:tailwind.adoc[Tailwind Dokumentation] für Best Practices. +See also xref:tailwind.adoc[Tailwind documentation] for best practices. -== Komponenten entwickeln +== Developing components === Workflow -1. Figma Design reviewen -2. shadcn/ui Komponente als Basis nehmen -3. Mit Tailwind UI Patterns abgleichen -4. An Design System anpassen -5. Dokumentation erstellen +1. Review Figma design +2. Use a shadcn/ui component as a base +3. Align with Tailwind UI patterns +4. Adapt to the design system +5. Write documentation === Guidelines * Accessibility first -* Responsive Design -* Konsistente Props API -* Dokumentation in Storybook +* Responsive design +* Consistent props API +* Documentation in Storybook diff --git a/template/docs/modules/ROOT/pages/documentation.adoc b/template/docs/modules/ROOT/pages/documentation.adoc index 4478161..211030c 100644 --- a/template/docs/modules/ROOT/pages/documentation.adoc +++ b/template/docs/modules/ROOT/pages/documentation.adoc @@ -1,33 +1,33 @@ -= Dokumentation += Documentation :experimental: :url-antora-docs: https://docs.antora.org/antora/latest/ :url-kroki-docs: https://docs.kroki.io/kroki/ -== Überblick +== Overview -Die Entwicklerdokumentation wird mit {url-antora-docs}[Antora] erstellt und über GitLab Pages bereitgestellt. +The developer documentation is built with {url-antora-docs}[Antora] and published via GitLab Pages. -* URL: -* Automatisches Deployment bei Push auf `main` -* Versionierung über Git +* URL: +* Automatic deployment on push to `main` +* Versioning via Git -== Struktur +== Structure [source] ---- docs/ -├── antora.yml # Antora Projekt-Konfiguration -├── antora-playbook.yml # Site Generator Konfiguration +├── antora.yml # Antora project configuration +├── antora-playbook.yml # Site generator configuration └── modules/ └── ROOT/ ├── nav.adoc # Navigation - ├── pages/ # Dokumentationsseiten - └── partials/ # Wiederverwendbare Inhalte + ├── pages/ # Documentation pages + └── partials/ # Reusable content ---- -== Diagramme +== Diagrams -Die Dokumentation unterstützt verschiedene Diagrammtypen über {url-kroki-docs}[Kroki]: +The documentation supports various diagram types via {url-kroki-docs}[Kroki]: === PlantUML @@ -45,49 +45,49 @@ package "Frontend" { ---- ---- -=== Weitere Diagrammtypen +=== Other diagram types * Mermaid -* C4 (mit PlantUML) +* C4 (with PlantUML) * GraphViz * ... -Siehe {url-kroki-docs}#diagram-types[Kroki Diagram Types] für eine vollständige Liste. +See {url-kroki-docs}#diagram-types[Kroki Diagram Types] for a complete list. -== Lokale Entwicklung +== Local development -=== Voraussetzungen +=== Prerequisites * Node.js >= 22.0.0 * npm >= 10.0.0 -=== Dokumentation lokal generieren +=== Generate documentation locally [source,bash] ---- -# Im docs Verzeichnis +# Inside the docs directory npm run docs ---- -Die generierte Dokumentation ist dann unter `docs/build` verfügbar. +The generated documentation will be available under `docs/build`. -== Best Practices +== Best practices -* Neue Features parallel zur Implementierung dokumentieren -* Diagramme für komplexe Zusammenhänge nutzen -* Wiederverwendbare Inhalte in `partials` auslagern -* Interne Links mit `xref:` erstellen +* Document new features in parallel with implementation +* Use diagrams for complex relationships +* Extract reusable content into `partials` +* Use `xref:` for internal links == Deployment -Das Deployment erfolgt automatisch über die GitLab CI/CD Pipeline: +Deployment is handled automatically via the GitLab CI/CD pipeline: -* Trigger: Push auf `main` -* Output: Statische Seiten +* Trigger: Push to `main` +* Output: Static pages * Hosting: GitLab Pages -== Nützliche Links +== Useful links -* {url-antora-docs}[Antora Dokumentation] +* {url-antora-docs}[Antora documentation] * {url-antora-docs}asciidoc/page/[AsciiDoc Syntax Guide] -* {url-kroki-docs}[Kroki Dokumentation] \ No newline at end of file +* {url-kroki-docs}[Kroki documentation] diff --git a/template/docs/modules/ROOT/pages/getting-started.adoc b/template/docs/modules/ROOT/pages/getting-started.adoc index 7229b2a..917d18c 100644 --- a/template/docs/modules/ROOT/pages/getting-started.adoc +++ b/template/docs/modules/ROOT/pages/getting-started.adoc @@ -1,18 +1,16 @@ -= Erste Schritte += Getting Started :experimental: :icons: font +== Prerequisites - -== Voraussetzungen - -Für die Entwicklung werden folgende Tools benötigt: +The following tools are required for development: * Node.js >= 22 * npm >= 10 * Git -== Repository klonen +== Clone the repository [source,bash] ---- @@ -22,118 +20,75 @@ cd planning-stack-template == Installation -Das Projekt verwendet npm Workspaces. Alle Abhängigkeiten können mit einem Befehl installiert werden: +The project uses npm Workspaces. All dependencies can be installed with a single command: [source,bash] ---- npm install ---- -== Entwicklungsumgebung einrichten +== Start the development server -=== Mock-Daten generieren - -Vor dem ersten Start müssen die Mock-Daten generiert werden: +The development server can be started directly from the root directory: [source,bash] ---- -npm run build:data +npm run dev ---- -=== Entwicklungsserver starten +This starts the prototype and the watchers for dependent packages. + +The application is available at http://localhost:5173. + +== Mock data -Der Entwicklungsserver kann mit folgendem Befehl im Root-Verzeichnis des Repos gestartet werden: +The starter ships with intentionally empty mock data. Once your own generators are in place, the JSON can be built as usual: [source,bash] ---- -npm run dev +npm run build:data ---- -Dies startet: +== Optional examples -* Den https://reactrouter.com/home[React Router] Entwicklungsserver (Prototyp) -* Einen Watcher für die Mock-Daten -* Einen Watcher für die UI-Komponenten +If the project was generated with an example, it is located under `examples//`. -Die Anwendung ist dann unter http://localhost:5173 erreichbar. +Examples are independent from the main project: -== Projektstruktur +* They are not part of the root workspaces +* They are not automatically built by Turborepo +* They must be installed separately inside their own folder + +== Project structure [source] ---- . -├── api/ # Domain Types & Enums +├── api/ # Domain types & enums ├── config/ -│ ├── eslint/ # ESLint Konfigurationen -│ ├── typescript/ # Typescript Konfigurationen -├── docs/ # Dokumentation -├── frontend/ # Produktives Frontend (noch nicht eingerichtet) -├── mocks/ # Testdaten Generator -├── mock-api/ # Mock Backend -├── prototype/ # Klickbarer UX-Prototyp -└── ui/ # Design-System Komponenten +│ ├── eslint/ # ESLint configurations +│ ├── typescript/ # TypeScript configurations +├── docs/ # Documentation +├── frontend/ # Placeholder for the production frontend +├── mocks/ # Starter for generated mock data +├── msw/ # Starter for MSW handlers +├── prototype/ # Clickable UX prototype +└── ui/ # Design system components ---- +== Typical development workflows -== IDE Konfiguration - -=== VS Code - -Empfohlene Extensions: - -* ESLint -* Prettier -* Tailwind CSS IntelliSense -* AsciiDoc - -Die Projekteinstellungen für VS Code sind bereits im Repository enthalten. - -=== WebStorm - -Für WebStorm müssen folgende Plugins installiert werden: - -* Tailwind CSS -* AsciiDoc - -== Typische Entwicklungsabläufe - -=== Neue Feature im Prototyp entwickeln - -1. Feature-Branch erstellen -2. Route in `prototype/app/routes` anlegen -3. UI mit shadcn/ui Komponenten aufbauen -4. Mock-Daten in `mocks` erweitern falls nötig -5. Feature testen und iterieren -6. Pull Request erstellen - -=== UI-Komponente hinzufügen - -1. Komponente in `packages/ui/src/components` erstellen -2. Storybook Story schreiben -3. Tests hinzufügen -4. Komponente exportieren und im Prototyp verwenden - -== Nächste Schritte - -* xref:architecture.adoc[Architektur] verstehen -* xref:prototype.adoc[Prototyp] kennenlernen -* xref:design-system.adoc[Design System] erkunden - -== Troubleshooting - -=== Bekannte Probleme - -[qanda] -Typescript Fehler nach npm install:: - Führen Sie `npm run build` aus, um alle Packages zu bauen. - -Mock-Daten werden nicht aktualisiert:: - Löschen Sie den `.cache` Ordner und führen Sie `npm run build:data` erneut aus. +=== Build the first product flow in the prototype -=== Support +1. Create a feature branch +2. Add a route in `prototype/app/routes.ts` +3. Build the UI in the prototype +4. Add mock data and MSW handlers as needed +5. Validate and iterate -Bei Problemen: +=== Explore an optional example -1. Prüfen Sie die bekannten Probleme -2. Suchen Sie in den GitHub Issues -3. Erstellen Sie ein neues Issue \ No newline at end of file +1. Navigate into the `examples//` folder +2. Run `npm install` there +3. Start or build the example locally +4. Copy the patterns you need into the main project diff --git a/template/docs/modules/ROOT/pages/index.adoc b/template/docs/modules/ROOT/pages/index.adoc index 4d0ccaa..cd35fd9 100644 --- a/template/docs/modules/ROOT/pages/index.adoc +++ b/template/docs/modules/ROOT/pages/index.adoc @@ -1,78 +1,59 @@ -= PLANNING STACK TEMPLATE Frontend Dokumentation -:description: Entwicklerdokumentation für das PLANNING STACK TEMPLATE Frontend += PLANNING STACK TEMPLATE Frontend Documentation +:description: Developer documentation for the PLANNING STACK TEMPLATE frontend monorepo :experimental: -== Über dieses Projekt +== Overview -Beschreibung des Projekts +This repository is the default Tinker Stack starter for PLANNING STACK TEMPLATE. -== Module +The base workspace is intentionally empty enough to let a team start from first principles instead +of deleting a built-in demo. -.Frontend-Struktur -image::Prototyp-Repo.drawio.png[Repository-Struktur] +== Modules -Das Projekt ist als Monorepo mit folgenden Hauptmodulen aufgebaut: +.Frontend structure +image::Prototyp-Repo.drawio.png[Repository structure] [cols="1,4"] |=== -|Modul |Beschreibung +|Module |Description -|@repo/api -|Domain-Typen und Enums +|@repo/api +|Shared domain types and enums |@repo/docs -|Diese Dokumentation - -|@repo/e2e -|End-to-End und Integrations-Tests (Browser-Tests) +|This documentation site |@repo/frontend -|Frontend-Anwendung +|Reserved for the production frontend |@repo/msw -|Mock-Backend mit MSW +|Starter-safe MSW package |@repo/mocks -|Synthetische Testdaten-Generator +|Starter-safe mock-data package |@repo/prototype -|Klickbarer UX-Prototyp für schnelles Feedback +|Minimal React Router prototype shell |@repo/ui -|Design-System und Komponenten-Bibliothek - +|Shared UI primitives and design-system components |=== -Das Mock-API muss mit dem Backend synchron gehalten werden. Somit ist es möglich, die Frontend-Anwendung lokal zu entwickeln und gleichzeitig das Backend zu testen. - -Ausserdem ist es dadurch möglich, mit dem Prototypen gegen das echte Backend zu testen. - - -== Technologie-Stack - -Das Frontend basiert auf folgenden Haupttechnologien: - -* *React* mit TypeScript für die UI-Entwicklung -* *Tailwind CSS* mit shadcn/ui für das Design-System -* *Turborepo* für das Monorepo-Management -* *MSW* (Mock Service Worker) für das Mock-Backend -* *Antora* für die Dokumentation - -Die Technologie für das Frontend ist noch offen. Empfohlen wird -React Router v7 als Framework (Isomorph). - -Der Prototyp verwendet React Router SPA, damit er auf einem Shared-Host deployed werden kann. - -== Erste Schritte +Optional examples can be generated into `examples//`. These examples are independent from the +base workspace and are not part of the default Turborepo build graph. -Siehe xref:getting-started.adoc[Erste Schritte] für Setup-Anweisungen und einen Überblick über die Entwicklungsumgebung. +== Technology stack -== Architektur +The default stack includes: -Die Anwendung folgt einer prototypen-getriebenen Entwicklung: +* React with TypeScript +* React Router for the starter prototype +* Tailwind CSS and shadcn/ui-style primitives +* Turborepo for workspace orchestration +* MSW for local API mocking +* Antora for docs -1. Schnelle Iteration über Features im Prototyp -2. Feedback von Benutzern einholen -3. Validierte Features in die produktive Anwendung übernehmen +== Getting started -Mehr Details zur Architektur unter xref:architecture.adoc[Architektur]. \ No newline at end of file +See xref:getting-started.adoc[Getting Started] for setup instructions. diff --git a/template/docs/modules/ROOT/pages/login.adoc b/template/docs/modules/ROOT/pages/login.adoc index 627d3a5..a639ec3 100644 --- a/template/docs/modules/ROOT/pages/login.adoc +++ b/template/docs/modules/ROOT/pages/login.adoc @@ -1,62 +1,19 @@ -= Login Flow (Prototyp) += Login Flow (Prototype) :experimental: -== Überblick +== Overview -Im Prototyp wird ein vereinfachtes Login-System verwendet: +The prototype does not include a login system yet. +Implement authentication that fits your project, for example: -* Cookie-basierte Authentifizierung -* Mock Implementation mit MSW -* Vordefinierte Test-Benutzer +* Cookie-based authentication with MSW handlers +* JWT or session tokens +* Integration with an IAM system -== Implementation +== Next steps -=== Mock Service Worker +1. Define test users and roles in `@repo/mocks` +2. Create MSW handlers for login/logout endpoints +3. Implement auth checks in protected handlers -[plantuml,mock-auth,svg] ----- -@startuml -participant "Browser" as Browser -participant "MSW Handler" as MSW -database "Mock Data" as Data - -Browser -> MSW: POST /auth/login -note right: Form Data mit Username -MSW -> Data: Token lookup -Data --> MSW: JWT Payload -MSW -> MSW: Permissions aus\nRollen ermitteln -MSW --> Browser: Set-Cookie: authToken -@enduml ----- - -=== Test-Benutzer - -[cols="1,2,1"] -|=== -|Persona |Organisation |Permissions - -|Administrator -|Primär-Organisation -|Alle Org-Berechtigungen - -|=== - -== Authentifizierung - -Die Auth-Prüfung erfolgt in den MSW Handlers: - -[source,typescript] ----- -// Beispiel eines geschützten msw Endpoints -http.post('/reports/approve', async ({ cookies }) => { - const { sub, orgId, permissions } = auth(cookies); - // ... Handler Implementation -}); ----- - -== Hinweise - -* Diese Implementation ist nur für den Prototyp, sollte aber ähnlich sein zu der späteren Produktiv-Implementation -* Keine echte Sicherheit -* Dient nur der Feature-Entwicklung -* Produktive Implementation erfolgt später mit IAM \ No newline at end of file +TIP: The optional example project (`examples/`) contains a complete login implementation with mock users as a reference. diff --git a/template/docs/modules/ROOT/pages/mock-data.adoc b/template/docs/modules/ROOT/pages/mock-data.adoc index e871f62..14c4461 100644 --- a/template/docs/modules/ROOT/pages/mock-data.adoc +++ b/template/docs/modules/ROOT/pages/mock-data.adoc @@ -1,46 +1,25 @@ -= Testdaten += Mock Data :experimental: -== Überblick +== Overview -Das `@repo/mocks` Package generiert synthetische Testdaten für die Entwicklung. Die Daten sind: +The `@repo/mocks` package is intentionally neutral in the starter. -* Realistisch und konsistent -* Typsicher durch TypeScript -* Reproduzierbar durch feste Seeds +The default implementation produces empty datasets so that `npm run build:data` works immediately, even before any domain models have been defined. -Die Daten werden im Entwicklungs-Modus On-The-Fly beim Reload der Applikation generiert. +== Starter behavior -== Generator +* Presets for `small`, `medium`, and `full` are present +* The generator returns an empty JSON object by default +* The structure can be replaced later with project-specific generators -=== Technologie +== Next steps -* https://fakerjs.dev/[Faker.js] für realistische Daten -* TypeScript für Typsicherheit -* 3 vorgegebene Konfigurationen für unterschiedliche Datenmengen - ** `small`: Für manuelle Prüfung der erstellten Daten. - ** `medium`: Für automatisches Testen und Entwicklung. - ** `full`: Alle Organisationen, für vollständige Tests. +1. Define your own domain types in `@repo/api` +2. Adjust presets in `mocks/src/generators/config.ts` +3. Implement data logic in `mocks/src/generators/dataset.ts` +4. Generate JSON with `npm run build:data` +== Examples -=== Verwendung - -Siehe `@repo/mocks/README.md` - -Der Ordner enthält Generator-Klassen für die einzelnen Entitäten. - -In der Datei `dataset.ts` werden die einzelnen Generatoren zu einem Datensatz zusammengefasst. - -== Daten erweitern - -1. Domain-Types in `@repo/api` definieren und falls nötig im lokalen Repository anpassen. -2. Generator in `@repo/mocks/src/generators` erstellen -3. In `dataset.ts` integrieren -4. Build mit `npm run build:data` - -== Best Practices - -* Realistische Wertebereiche verwenden -* Beziehungen zwischen Entitäten beachten -* Lokale Besonderheiten berücksichtigen (z.B. Schweizer Adressen) -* Reproduzierbarkeit durch Seeds sicherstellen \ No newline at end of file +If an optional example was generated alongside the project, its `mocks/` package shows a complete implementation in an independent context. diff --git a/template/docs/modules/ROOT/pages/monorepo.adoc b/template/docs/modules/ROOT/pages/monorepo.adoc index f31d30c..5ecfcb4 100644 --- a/template/docs/modules/ROOT/pages/monorepo.adoc +++ b/template/docs/modules/ROOT/pages/monorepo.adoc @@ -1,64 +1,60 @@ = Monorepo -Siehe auch README.md. +See also README.md. -Wir verwenden Turborepo mit der https://turbo.build/repo/docs/core-concepts/internal-packages#compiled-packages[Compiled Packages] Strategie. +We use Turborepo with compiled internal packages. -Alle Pakete sind ESM-Module und verwenden den `@repo` Scope. +All packages are ESM modules and use the `@repo` scope. -Dieses Monorepo enthält die folgenden Pakete/Apps: +== Apps and packages -== Apps und Pakete - -- `@repo/frontend`: das Frontend für die PLANNING STACK TEMPLATE Anwendung (noch zu erstellen). -- `@repo/prototype`: ein Prototyp für die PLANNING STACK TEMPLATE Anwendung. -- `@repo/api`: ein Paket, das die Domain-Typen und -Enums bereitstellt. -- `@repo/docs`: Dokumentationen für die PLANNING STACK TEMPLATE Anwendung im Antora-Format. -- `@repo/mocks`: ein Paket, das synthetische Daten für die Anwendungen bereitstellt. -- `@repo/mock-api`: ein Paket, das eine Mock-API über Service-Worker bereitstellt. -- `@repo/ui`: eine Basis-React-Komponentenbibliothek, die von den Anwendungen `FAS` und `prototype` - gemeinsam genutzt wird -- `@repo/eslint-config`: `eslint`-Konfigurationen -- `@repo/typescript-config`: `tsconfig.json`s, die im gesamten Monorepo verwendet werden +- `@repo/frontend`: Placeholder for the production frontend +- `@repo/prototype`: Minimal prototype for early UX validation +- `@repo/api`: Domain types and enums +- `@repo/docs`: Antora documentation +- `@repo/mocks`: Starter for generated mock data +- `@repo/msw`: Starter for MSW handlers +- `@repo/ui`: Shared UI components +- `@repo/eslint-config`: `eslint` configurations +- `@repo/typescript-config`: Shared `tsconfig.json` == Build -Die Builds sind alle von https://turbo.build[Turborepo] gesteuert und sollten aus dem -Root-Verzeichnis heraus aufgerufen werden. werden. - -Um alle Apps und Pakete zu bauen, führe den folgenden Befehl aus: +The base workspaces are built via Turborepo: ``` npm run build ``` -Es ist auch möglich, nur ein bestimmtes Paket oder eine bestimmte App zu bauen: +Other useful commands: ``` npm run build:data - npm run build:msw - npm run build:prototype ``` -### Entwicklung +== Development -Aktuell ist standardmässig der Prototyp aktiviert. Um die Entwicklung zu starten, führe den -folgenden Befehl aus. Dieser startet einen lokalen Server und watcher für die Abhängigen Pakete. +To start base development: ``` npm run dev ``` +== Optional examples +Examples are generated under `examples//` and intentionally excluded from the root workspaces. -== Aufbau +This means: + +* Turborepo ignores them by default +* They can be installed separately +* They can be deleted at any time + +== Structure [plantuml,monorepo,svg,role=component] ---- include::partial$monorepo.puml[] ---- - -Das Monorepo verwendet die export und import Direktiven von node ESM Modulen. -Alle exportierten Namespaces müssen in der package.json Datei aufgelistet werden. \ No newline at end of file diff --git a/template/docs/modules/ROOT/pages/prototype.adoc b/template/docs/modules/ROOT/pages/prototype.adoc index a0d1223..5520722 100644 --- a/template/docs/modules/ROOT/pages/prototype.adoc +++ b/template/docs/modules/ROOT/pages/prototype.adoc @@ -1,105 +1,33 @@ -= Prototyp += Prototype :experimental: -== Zweck +== Purpose -Der PLANNING STACK TEMPLATE Prototyp dient als: +The starter prototype is intentionally minimal. It serves as: -* Testumgebung für neue Features und UX-Konzepte -* Basis für Benutzerfeedback und Iterationen -* Proof-of-Concept für technische Lösungen +* An entry point for the first validatable user flow +* A place for rapid UX iteration +* A shell into which your own API and mock logic can be plugged -== Struktur +== Technical setup -Die Ordnerstruktur des Prototypen orientiert sich an den Modulen des Produktionssystems. -Zusätzlich gibt es den Ordner 'components' für Modulübergreifende UI-Komponenten und 'hooks' für wiederverwendbare Logik. -Der Ordner 'lib' enthält Hilfsfunktionen. Der Ordner 'mocks' die Logik für die Mock-Daten. +* React Router SPA with TypeScript +* Tailwind CSS +* Local MSW bootstrapping +* Starter landing page instead of a demo workflow -Die Routen sind über eine Route-Konfiguration in app/routes.ts definiert. -Die Route-Komponenten sind in den entsprechenden Modul-Ordnern. +== Starting point -== API +The prototype ships with: -Der Prototyp verwendet das `ky` Paket für HTTP-Anfragen. +* A root layout file +* A landing route +* MSW pass-through handlers +From here the desired feature structure can be built up. -=== Technischer Aufbau +== Optional examples -* React Router SPA mit TypeScript -* Mock-Backend via MSW -* Synthetische Testdaten -* shadcn/ui Komponenten +If an example was generated, it contains a complete reference flow in its own directory under `examples//`. -== Entwicklungsprozess - -=== Feature-Entwicklung - -1. Feature-Anforderung erfassen -2. Schnelle Implementierung im Prototyp -3. Review mit Stakeholdern -4. Feedback einarbeiten -5. User-Stories updaten -6. Feature für Produktion freigeben - -=== Guidelines - -* Fokus auf Benutzerinteraktion, nicht auf Datenvalidierung -* TODOs für fehlende Produktionsanforderungen -* Schnelle Iteration über perfekte Implementation -* Realistische Testdaten verwenden - -== Deployment - -Das Deployment erfolgt automatisch über die CI/CD Pipeline: - -=== Umgebungen - -[cols="1,3"] -|=== -|Umgebung |Beschreibung - -|Continuous Integration -|* Deployment bei Push auf `main` -* URL: -* Aktuelle Entwicklungsversion - -|Acceptance Test -|* Deployment bei Tag-Commit -* URL: -* Stabile Version für User-Testing - - -|Preview -|* Preview-Umgebung, z.B. für Merge-Requests -* URL: -* Alternative Version für A/B Tests. -|=== - -Die nötigen Zugangsdaten müssen in den Umgebungsvariablen gesetzt werden. Siehe `.env.example`. - -Das Deployment auf Preview kann bei einem Merge-Request manuell ausgelöst werden. - -=== Zugriff - -* Die Hosts sind über einen IP-Filter geschützt. Die Konfiguration ist in der .htaccess Datei im Ordner `prototype/app/public`. - -* Der Host für die Dokumentation kann nur vom internen Netzwerk aus erreicht werden. - - -== Limitationen - -* Keine echte Backend-Anbindung -* Eingeschränkte Validierung -* Vereinfachte Berechtigungen -* Keine Performance-Optimierung - -== Nächste Schritte - -* Integration von Benutzer-Feedback -* Priorisierung der Features für Produktion -* Identifikation technischer Herausforderungen - -== Bekannte Probleme - -* msw in einer Version höher als 2.6.3 (getestet bis 2.6.6) funktioniert nicht mit dem dev server. Das CSS kann nicht geladen werden. -Aktuell ist die Version auf 2.6.3 gepinnt. Allenfalls später nochmals testen und ggf. einen Github-Issue erstellen. \ No newline at end of file +These examples are not part of the root project's build pipeline and can be started or deleted independently. diff --git a/template/docs/modules/ROOT/pages/tailwind.adoc b/template/docs/modules/ROOT/pages/tailwind.adoc index 2359bef..97adcae 100644 --- a/template/docs/modules/ROOT/pages/tailwind.adoc +++ b/template/docs/modules/ROOT/pages/tailwind.adoc @@ -1,12 +1,12 @@ -= Tailwind Tipps und Tricks += Tailwind Tips and Tricks Tailwind Docs: https://tailwindcss.com/docs/ == State -Die Seite soll barrierefrei sein, daher bietet es sich an, die States falls möglich -auf den jeweiligen Aria-States zu basieren. -.Beispiel Button +The app should be accessible, so where possible it is advisable to base states on the corresponding ARIA attributes. + +.Example button ```tsx - - - - {loaderData.error &&
{loaderData.error.message}
} - - - ); - } - - const { me } = loaderData; - const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen); - - return ( - - - -
-
-
- - - {me.firstName.charAt(0)} - -
- {me.firstName} {me.lastName} -
-
- - -
-
-
- - -
- - -
-
-
-
-
- ); -} - -function NavItem({ - href, - icon, - label, - isOpen, -}: { - href: string; - icon: React.ReactNode; - label: string; - isOpen: boolean; -}) { - return ( - - cn('flex flex-none items-center px-4 py-2 hover:bg-accent', { - 'justify-start': isOpen, - 'justify-center w-12 px-0': !isOpen, - 'rounded-sm': !isOpen, - 'bg-accent': isActive, - 'font-bold': isActive, - }) - } - > - {icon} - { - - {label} - - } - - ); + return ( + +
+
+
+ + +
+
+
+ ); } export function HydrateFallback() { - return ( - -
-
-
-
- {/* TODO: md Klassen hinzufügen*/} - -
-
-

Lädt...

-
-
-
-
- ); + return ( + +
+
+
+
+

Loading prototype shell...

+
+
+
+
+ ); } diff --git a/template/prototype/app/routes.ts b/template/prototype/app/routes.ts index 6c8e094..aad9b67 100644 --- a/template/prototype/app/routes.ts +++ b/template/prototype/app/routes.ts @@ -1,10 +1,7 @@ // app/routes.ts -import { type RouteConfig, index, route } from '@react-router/dev/routes'; +import { type RouteConfig, index } from '@react-router/dev/routes'; const routes = [ - route('dashboard', './demo/dashboard.tsx'), - route('employees', './demo/employees.tsx'), - route('admin', './demo/admin.tsx'), index('./routes/_index.tsx'), ] satisfies RouteConfig; diff --git a/template/prototype/app/routes/_index.tsx b/template/prototype/app/routes/_index.tsx index 259a1e7..c15ceb7 100644 --- a/template/prototype/app/routes/_index.tsx +++ b/template/prototype/app/routes/_index.tsx @@ -1,16 +1,44 @@ import type { MetaFunction } from 'react-router'; export const meta: MetaFunction = () => { - return [{ title: 'PLANNING STACK TEMPLATE Prototype' }]; + return [{ title: 'PLANNING STACK TEMPLATE Prototype' }]; }; export default function Index() { - return ( -
-

PLANNING STACK TEMPLATE Prototype

-

Dies ist ein klickbarer Prototyp für die PLANNING STACK TEMPLATE-Anwendung.

-

Es dient zum Austesten von Anwendungsfällen und Prozessen.

-

Alle Daten sind synthetisch und es gibt keinen Zugriff auf ein Backend.

-
- ); + return ( +
+
+

Start with a blank product shell

+

+ This prototype is intentionally empty. Add your first route, connect your own API layer, + or use one of the optional examples as reference. +

+
+ +
+
+

Prototype

+

+ Replace this landing page with the first user flow you want to validate. +

+
+ +
+

Mocks

+

+ The base project ships with starter-safe mock data and MSW packages that build without + demo entities. +

+
+ +
+

Examples

+

+ If you generated an example, open the matching folder under examples/ and + run it independently. +

+
+
+
+ ); } diff --git a/template/prototype/vite.config.ts b/template/prototype/vite.config.ts index d89a387..2df0ebb 100644 --- a/template/prototype/vite.config.ts +++ b/template/prototype/vite.config.ts @@ -8,15 +8,7 @@ export default defineConfig({ preserveSymlinks: false, }, optimizeDeps: { - exclude: - process.env.NODE_ENV === 'development' - ? [ - '@repo/msw/medium', - '@repo/msw/handlers', - '@repo/api/stammdaten/enums', - '@repo/ui', - ] - : [], + exclude: process.env.NODE_ENV === 'development' ? ['@repo/ui'] : [], }, server: { watch: { diff --git a/tests/generator.test.mjs b/tests/generator.test.mjs index fb1c8e0..cc1370c 100644 --- a/tests/generator.test.mjs +++ b/tests/generator.test.mjs @@ -22,11 +22,12 @@ async function runCommand(cmd, args, options = {}) { describe('create-tinker-stack generator', () => { test( - 'scaffolds project and passes build checks', + 'scaffolds project with all examples by default and passes build checks', async () => { const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'tinker-stack-')); const appFolder = 'demo-app'; const projectDir = path.join(tempRoot, appFolder); + const exampleDir = path.join(projectDir, 'examples', 'react-router'); try { await runCommand('node', [CLI_ENTRY, appFolder], { @@ -46,6 +47,10 @@ describe('create-tinker-stack generator', () => { const readme = await readFile(path.join(projectDir, 'README.md'), 'utf8'); expect(readme).toContain('Demo Title'); + const examplePackageJson = JSON.parse( + await readFile(path.join(exampleDir, 'package.json'), 'utf8') + ); + expect(examplePackageJson.scripts).toHaveProperty('build'); await runCommand('npm', ['install'], { cwd: projectDir }); await runCommand('npm', ['run', 'typecheck'], { cwd: projectDir }); @@ -58,6 +63,79 @@ describe('create-tinker-stack generator', () => { 5 * 60 * 1000 ); + test( + 'scaffolds an independent named example without adding it to the root workspace graph', + async () => { + const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'tinker-stack-')); + const appFolder = 'demo-app'; + const projectDir = path.join(tempRoot, appFolder); + const exampleDir = path.join(projectDir, 'examples', 'react-router'); + + try { + await runCommand('node', [CLI_ENTRY, '--example', 'react-router', appFolder], { + env: { + ...process.env, + CI: '1', + SKIP_SETUP: '1', + SKIP_FORMAT: '1' + }, + cwd: tempRoot + }); + + const packageJson = JSON.parse(await readFile(path.join(projectDir, 'package.json'), 'utf8')); + expect(packageJson.name).toBe('demo-title'); + expect(packageJson.workspaces).not.toContain('examples'); + expect(packageJson.workspaces).not.toContain('examples/*'); + + const examplePackageJson = JSON.parse( + await readFile(path.join(exampleDir, 'package.json'), 'utf8') + ); + expect(examplePackageJson.scripts).toHaveProperty('build'); + expect(examplePackageJson.scripts).toHaveProperty('build:data'); + + const exampleReadme = await readFile(path.join(exampleDir, 'README.md'), 'utf8'); + expect(exampleReadme).toContain('self-contained React Router example'); + expect(exampleReadme).toContain('delete the entire example directory'); + + await runCommand('npm', ['install'], { cwd: projectDir }); + await runCommand('npm', ['run', 'build'], { cwd: projectDir }); + } finally { + await rm(tempRoot, { recursive: true, force: true }); + } + }, + 5 * 60 * 1000 + ); + + test( + 'skips copying example templates when --no-examples is passed', + async () => { + const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'tinker-stack-')); + const appFolder = 'demo-app'; + const projectDir = path.join(tempRoot, appFolder); + + try { + await runCommand('node', [CLI_ENTRY, '--no-examples', appFolder], { + env: { + ...process.env, + CI: '1', + SKIP_SETUP: '1', + SKIP_FORMAT: '1' + }, + cwd: tempRoot + }); + + const packageJson = JSON.parse(await readFile(path.join(projectDir, 'package.json'), 'utf8')); + expect(packageJson.name).toBe('demo-title'); + await expect(readFile(path.join(projectDir, 'examples', 'react-router', 'package.json'), 'utf8')) + .rejects + .toMatchObject({ code: 'ENOENT' }); + } finally { + await rm(tempRoot, { recursive: true, force: true }); + } + }, + 5 * 60 * 1000 + ); + test( 'scaffolds into the current working directory when no target is provided', async () => {