diff --git a/bin/dot b/bin/dot index f8049bd..8363c90 100755 Binary files a/bin/dot and b/bin/dot differ diff --git a/docs/contributor/authoring-flows.md b/docs/contributor/authoring-flows.md index a35294f..26e3698 100644 --- a/docs/contributor/authoring-flows.md +++ b/docs/contributor/authoring-flows.md @@ -389,6 +389,7 @@ The flow immediately appears in `dot flows` and `dot scaffold`. | `fullstack` | `flows/fullstack.go` | `project_name`, `api_language`, `frontend_framework` | | `microservices` | `flows/microservices.go` | `project_name`, `services` (loop: `service_name`, `service_port`) | | `plugin-template` | `flows/plugin_template.go` | `project_name`, `module_path`, `plugin_description`, `plugin_author`, `plugin_year`, `plugin_include_injection`, `plugin_include_generator` | +| `frontend` | `flows/frontend.go` | `project_name`, `framework`, `frontend-router`, `ui-library`, `frontend-styling`, `frontend-state`, `frontend-formatter`, `frontend-linter`, `include-vitest`, `include-playwright`, `include-storybook`, `include-auth`, `include-theme`, `include-feature-flags`, `include-sentry`, `include-analytics`, `include-seo` | --- diff --git a/docs/contributor/flows/frontend.md b/docs/contributor/flows/frontend.md new file mode 100644 index 0000000..6018ec9 --- /dev/null +++ b/docs/contributor/flows/frontend.md @@ -0,0 +1,173 @@ +# Flow: `frontend` + +Scaffolds a TypeScript frontend project — React+Vite or Next.js — with a full range of optional add-ons: router, UI library, styling, state management, testing, auth, feature flags, Sentry, analytics, and SEO. + +--- + +## Identity + +| Field | Value | +|-------|-------| +| ID | `frontend` | +| Title | Frontend Wizard | +| File | `flows/frontend.go` | +| Root question | `project_name` | + +--- + +## Questions + +| ID | Type | Label | Options / Default | +|----|------|-------|-------------------| +| `project_name` | Text | "Project name" | Default: `"my-app"` | +| `framework` | Option | "Framework" | `react-vite`, `next` | +| `frontend-router` | Option | "Router" | `react-router`, `tanstack-router`, `none` — React+Vite only | +| `ui-library` | Option | "UI library" | `shadcn`, `arkui`, `version14`, `none` | +| `frontend-styling` | Option | "Styling" | `tailwind`, `css-modules`, `panda-css` — skipped when shadcn | +| `frontend-state` | Option | "State management" | `zustand`, `jotai`, `none` | +| `frontend-formatter` | Option | "Formatter" | `biome`, `prettier` | +| `frontend-linter` | Option | "Linter" | `biome`, `prettier` | +| `check-vitest-available` | If | _(silent: fw == react-vite?)_ | Routes to `include-vitest` or `include-playwright` | +| `include-vitest` | Confirm | "Include Vitest + React Testing Library?" | Default: `false` — React+Vite only | +| `include-playwright` | Confirm | "Include Playwright (E2E tests)?" | Default: `false` | +| `include-storybook` | Confirm | "Include Storybook?" | Default: `false` | +| `include-auth` | Confirm | "Include authentication?" | Default: `false` | +| `auth-provider` | Option | "Auth provider" | `clerk`, `better-auth`, `vanilla` — when `include-auth` = true | +| `include-theme` | Confirm | "Include custom theme provider?" | Default: `false` | +| `include-feature-flags` | Confirm | "Include feature flags?" | Default: `false` | +| `feature-flags-provider` | Option | "Feature flags provider" | `posthog`, `vercel`, `local` — when `include-feature-flags` = true | +| `include-sentry` | Confirm | "Include Sentry error tracking?" | Default: `false` | +| `include-analytics` | Confirm | "Include analytics?" | Default: `false` | +| `analytics-provider` | Option | "Analytics provider" | `ga4`, `plausible`, `posthog` — when `include-analytics` = true | +| `include-seo` | Confirm | "Include SEO setup?" | Default: `false` | +| `confirm-generate` | Confirm | "Generate the project now?" | Default: `true` | + +--- + +## Question graph + +``` +project_name + └── framework + ├── [react-vite] → frontend-router + │ └── ui-library + │ ├── [shadcn] → frontend-state (skips styling) + │ └── [other] → frontend-styling + │ └── frontend-state + └── [next] ──────────────── ui-library (same tree as above) + +frontend-state + └── frontend-formatter + └── frontend-linter + └── check-vitest-available (IfQuestion) + ├── [react-vite] → include-vitest + │ └── include-playwright + └── [next] → include-playwright + +include-playwright + └── include-storybook + └── include-auth + ├── [true] → auth-provider + │ └── include-theme + └── [false] → include-theme + +include-theme + └── include-feature-flags + ├── [true] → feature-flags-provider + │ └── include-sentry + └── [false] → include-sentry + +include-sentry + └── include-analytics + ├── [true] → analytics-provider + │ └── include-seo + └── [false] → include-seo + +include-seo + └── confirm-generate + └── (end) +``` + +--- + +## Generator resolution + +| Condition | Generators added | +|-----------|-----------------| +| Always | `base_project`, `typescript_base` | +| `framework` = `react-vite` | `react_app` | +| `framework` = `next` | `nextjs_base` | +| `framework` = `react-vite` AND `frontend-router` = `react-router` | `react_router_v7` | +| `framework` = `react-vite` AND `frontend-router` = `tanstack-router` | `tanstack_router` | +| `ui-library` = `shadcn` | `shadcn_ui` | +| `ui-library` = `arkui` | `ark_ui` | +| `ui-library` = `version14` | `version14_ui` | +| `ui-library` ≠ `shadcn` AND `frontend-styling` = `tailwind` | `tailwind_v4` | +| `ui-library` ≠ `shadcn` AND `frontend-styling` = `css-modules` | `css_modules` | +| `ui-library` ≠ `shadcn` AND `frontend-styling` = `panda-css` | `panda_css` | +| `frontend-state` = `zustand` | `zustand_setup` | +| `frontend-state` = `jotai` | `jotai_setup` | +| `include-vitest` = true AND `framework` = `react-vite` | `vitest_testing_library` | +| `include-playwright` = true | `playwright_setup` | +| `include-storybook` = true | `storybook_setup` | +| `include-auth` = true AND `auth-provider` = `clerk` | `auth_clerk_frontend` | +| `include-auth` = true AND `auth-provider` = `better-auth` | `auth_better_auth_frontend` | +| `include-auth` = true AND `auth-provider` = `vanilla` | `auth_vanilla_frontend` | +| `include-theme` = true | `theme_provider` | +| `include-feature-flags` = true AND `feature-flags-provider` = `posthog` | `feature_flags_posthog` | +| `include-feature-flags` = true AND `feature-flags-provider` = `vercel` | `feature_flags_vercel` | +| `include-feature-flags` = true AND `feature-flags-provider` = `local` | `feature_flags_local` | +| `include-sentry` = true | `sentry_frontend` | +| `include-analytics` = true AND `analytics-provider` = `ga4` | `analytics_ga4` | +| `include-analytics` = true AND `analytics-provider` = `plausible` | `analytics_plausible` | +| `include-analytics` = true AND `analytics-provider` = `posthog` AND NOT already `feature_flags_posthog` | `feature_flags_posthog` (dedup) | +| `frontend-formatter` = `prettier` (last) | `prettier_config`, `prettier_typescript_deps`, `prettier_frontend_rules` | +| `frontend-formatter` = `biome` (last) | `biome_config` | + +--- + +## Fixture examples + +**Minimal React+Vite** (`tools/test-flow/testdata/202605280001_frontend_react_vite_minimal.json`): + +```json +{ + "name": "frontend_react_vite_minimal", + "flow_id": "frontend", + "answers": { + "project_name": "my-app", + "framework": "react-vite", + "frontend-router": "none", + "ui-library": "none", + "frontend-styling": "tailwind", + "frontend-state": "none", + "frontend-formatter": "biome", + "frontend-linter": "biome", + "include-vitest": false, + "include-playwright": false, + "include-storybook": false, + "include-auth": false, + "include-theme": false, + "include-feature-flags": false, + "include-sentry": false, + "include-analytics": false, + "include-seo": false, + "confirm-generate": true + } +} +``` + +**All modules** (`tools/test-flow/testdata/202605280008_frontend_react_vite_all_modules.json`): +shadcn forces Tailwind — the `frontend-styling` question is skipped; PostHog dedup — selecting PostHog for both feature flags and analytics emits `feature_flags_posthog` once. + +--- + +## Source + +`flows/frontend.go` + +## See also + +- [docs/generators/nextjs_base.md](../generators/nextjs_base.md) +- [docs/generators/react_router_v7.md](../generators/react_router_v7.md) +- [docs/generators/shadcn_ui.md](../generators/shadcn_ui.md) diff --git a/docs/contributor/generators/analytics_ga4.md b/docs/contributor/generators/analytics_ga4.md new file mode 100644 index 0000000..e2b4f05 --- /dev/null +++ b/docs/contributor/generators/analytics_ga4.md @@ -0,0 +1,78 @@ +# Generator: `analytics_ga4` + +Adds Google Analytics 4 via `react-ga4` — writes an initializer and a `.env.example` with the measurement ID. + +--- + +## Identity + +| Field | Value | +|-------|-------| +| Name | `analytics_ga4` | +| Version | `0.2.0` | +| Package | `generators/analytics_ga4` | + +--- + +## Dependencies + +| Generator | Why | +|-----------|-----| +| `typescript_base` | Requires package.json to merge dependencies into | + +--- + +## Answers consumed + +None. + +--- + +## Files written + +| Path | Description | +|------|-------------| +| `src/lib/ga4.ts` | GA4 init and `trackEvent` helper | +| `.env.example` | Required env var (`VITE_GA_MEASUREMENT_ID`) | + +Also merges into: + +| Path | Keys added / updated | +|------|---------------------| +| `package.json` | `dependencies`: `react-ga4^2` | + +--- + +## Validators + +| Check | Type | Passes when | +|-------|------|-------------| +| `src/lib/ga4.ts` | `file_exists` | File is present after generation | + +--- + +## Post-generation commands + +| Command | WorkDir | Notes | +|---------|---------|-------| +| `pnpm install --dangerously-allow-all-builds` | project root | Installs react-ga4 | + +## Test commands + +| Command | Background | Ready delay | Notes | +|---------|-----------|-------------|-------| +| `pnpm exec tsc --noEmit` | No | — | Verifies TypeScript compiles | + +--- + +## Conflicts + +| Generator | Reason | +|-----------|--------| +| `analytics_plausible` | Both provide analytics — only one analytics library should be installed | + +--- + +## See also + +- [docs/flows/frontend.md](../flows/frontend.md) diff --git a/docs/contributor/generators/analytics_plausible.md b/docs/contributor/generators/analytics_plausible.md new file mode 100644 index 0000000..c107bc5 --- /dev/null +++ b/docs/contributor/generators/analytics_plausible.md @@ -0,0 +1,78 @@ +# Generator: `analytics_plausible` + +Adds Plausible Analytics via `plausible-tracker` — writes a tracker initializer and a `.env.example` with the domain. + +--- + +## Identity + +| Field | Value | +|-------|-------| +| Name | `analytics_plausible` | +| Version | `0.2.0` | +| Package | `generators/analytics_plausible` | + +--- + +## Dependencies + +| Generator | Why | +|-----------|-----| +| `typescript_base` | Requires package.json to merge dependencies into | + +--- + +## Answers consumed + +None. + +--- + +## Files written + +| Path | Description | +|------|-------------| +| `src/lib/plausible.ts` | Plausible tracker init and `trackEvent` helper | +| `.env.example` | Required env var (`VITE_PLAUSIBLE_DOMAIN`) | + +Also merges into: + +| Path | Keys added / updated | +|------|---------------------| +| `package.json` | `dependencies`: `plausible-tracker^0.3` | + +--- + +## Validators + +| Check | Type | Passes when | +|-------|------|-------------| +| `src/lib/plausible.ts` | `file_exists` | File is present after generation | + +--- + +## Post-generation commands + +| Command | WorkDir | Notes | +|---------|---------|-------| +| `pnpm install --dangerously-allow-all-builds` | project root | Installs plausible-tracker | + +## Test commands + +| Command | Background | Ready delay | Notes | +|---------|-----------|-------------|-------| +| `pnpm exec tsc --noEmit` | No | — | Verifies TypeScript compiles | + +--- + +## Conflicts + +| Generator | Reason | +|-----------|--------| +| `analytics_ga4` | Both provide analytics — only one analytics library should be installed | + +--- + +## See also + +- [docs/flows/frontend.md](../flows/frontend.md) diff --git a/docs/contributor/generators/ark_ui.md b/docs/contributor/generators/ark_ui.md new file mode 100644 index 0000000..da1562b --- /dev/null +++ b/docs/contributor/generators/ark_ui.md @@ -0,0 +1,78 @@ +# Generator: `ark_ui` + +Adds Ark UI headless components — installs `@ark-ui/react` and writes an example button built on Ark's `Button` primitive. + +--- + +## Identity + +| Field | Value | +|-------|-------| +| Name | `ark_ui` | +| Version | `0.2.0` | +| Package | `generators/ark_ui` | + +--- + +## Dependencies + +| Generator | Why | +|-----------|-----| +| `typescript_base` | Requires package.json to merge dependencies into | + +--- + +## Answers consumed + +None. + +--- + +## Files written + +| Path | Description | +|------|-------------| +| `src/components/ui/button.tsx` | Button built on Ark UI's Button primitive | +| `src/components/ui/index.ts` | Barrel export | + +Also merges into: + +| Path | Keys added / updated | +|------|---------------------| +| `package.json` | `dependencies`: `@ark-ui/react` | + +--- + +## Validators + +| Check | Type | Passes when | +|-------|------|-------------| +| `src/components/ui/button.tsx` | `file_exists` | File is present after generation | + +--- + +## Post-generation commands + +| Command | WorkDir | Notes | +|---------|---------|-------| +| `pnpm install --dangerously-allow-all-builds` | project root | Installs Ark UI | + +## Test commands + +| Command | Background | Ready delay | Notes | +|---------|-----------|-------------|-------| +| `pnpm exec tsc --noEmit` | No | — | Verifies TypeScript compiles | + +--- + +## Conflicts + +| Generator | Reason | +|-----------|--------| +| `shadcn_ui` | Both write `src/components/ui/button.tsx` | + +--- + +## See also + +- [docs/flows/frontend.md](../flows/frontend.md) diff --git a/docs/contributor/generators/auth_better_auth_frontend.md b/docs/contributor/generators/auth_better_auth_frontend.md new file mode 100644 index 0000000..9053370 --- /dev/null +++ b/docs/contributor/generators/auth_better_auth_frontend.md @@ -0,0 +1,80 @@ +# Generator: `auth_better_auth_frontend` + +Integrates Better Auth — writes an auth client, a React context provider, and a `useAuth` hook. + +--- + +## Identity + +| Field | Value | +|-------|-------| +| Name | `auth_better_auth_frontend` | +| Version | `0.2.0` | +| Package | `generators/auth_better_auth_frontend` | + +--- + +## Dependencies + +| Generator | Why | +|-----------|-----| +| `typescript_base` | Requires package.json to merge dependencies into | + +--- + +## Answers consumed + +None. + +--- + +## Files written + +| Path | Description | +|------|-------------| +| `src/lib/authClient.ts` | Better Auth client instance | +| `src/providers/AuthProvider.tsx` | React context provider for session state | +| `src/hooks/useAuth.ts` | Hook returning session and sign-in/out helpers | + +Also merges into: + +| Path | Keys added / updated | +|------|---------------------| +| `package.json` | `dependencies`: `better-auth^1` | + +--- + +## Validators + +| Check | Type | Passes when | +|-------|------|-------------| +| `src/hooks/useAuth.ts` | `file_exists` | File is present after generation | + +--- + +## Post-generation commands + +| Command | WorkDir | Notes | +|---------|---------|-------| +| `pnpm install --dangerously-allow-all-builds` | project root | Installs Better Auth | + +## Test commands + +| Command | Background | Ready delay | Notes | +|---------|-----------|-------------|-------| +| `pnpm exec tsc --noEmit` | No | — | Verifies TypeScript compiles | + +--- + +## Conflicts + +| Generator | Reason | +|-----------|--------| +| `auth_clerk_frontend` | Both provide auth provider and useAuth hook | +| `auth_vanilla_frontend` | Both provide auth provider and useAuth hook | + +--- + +## See also + +- [docs/flows/frontend.md](../flows/frontend.md) diff --git a/docs/contributor/generators/auth_clerk_frontend.md b/docs/contributor/generators/auth_clerk_frontend.md new file mode 100644 index 0000000..6a8ae53 --- /dev/null +++ b/docs/contributor/generators/auth_clerk_frontend.md @@ -0,0 +1,82 @@ +# Generator: `auth_clerk_frontend` + +Integrates Clerk authentication — framework-aware: `@clerk/nextjs` for Next.js, `@clerk/clerk-react` for Vite. Writes a provider, a `useAuth` hook, and a `.env.example`. + +--- + +## Identity + +| Field | Value | +|-------|-------| +| Name | `auth_clerk_frontend` | +| Version | `0.2.0` | +| Package | `generators/auth_clerk_frontend` | + +--- + +## Dependencies + +| Generator | Why | +|-----------|-----| +| `typescript_base` | Requires package.json to merge dependencies into | + +--- + +## Answers consumed + +| Key | Type | Required | Notes | +|-----|------|----------|-------| +| `framework` | string | No | `"next"` → installs `@clerk/nextjs`; otherwise `@clerk/clerk-react` | + +--- + +## Files written + +| Path | Description | +|------|-------------| +| `src/providers/ClerkProvider.tsx` | Clerk provider wrapper | +| `src/hooks/useAuth.ts` | Hook re-exporting Clerk's `useUser` / `useAuth` | +| `.env.example` | Required env vars (`VITE_CLERK_PUBLISHABLE_KEY` or `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`) | + +Also merges into: + +| Path | Keys added / updated | +|------|---------------------| +| `package.json` | `dependencies`: `@clerk/nextjs` or `@clerk/clerk-react` | + +--- + +## Validators + +| Check | Type | Passes when | +|-------|------|-------------| +| `src/hooks/useAuth.ts` | `file_exists` | File is present after generation | + +--- + +## Post-generation commands + +| Command | WorkDir | Notes | +|---------|---------|-------| +| `pnpm install --dangerously-allow-all-builds` | project root | Installs Clerk SDK | + +## Test commands + +| Command | Background | Ready delay | Notes | +|---------|-----------|-------------|-------| +| `pnpm exec tsc --noEmit` | No | — | Verifies TypeScript compiles | + +--- + +## Conflicts + +| Generator | Reason | +|-----------|--------| +| `auth_better_auth_frontend` | Both provide auth provider and useAuth hook | +| `auth_vanilla_frontend` | Both provide auth provider and useAuth hook | + +--- + +## See also + +- [docs/flows/frontend.md](../flows/frontend.md) diff --git a/docs/contributor/generators/auth_vanilla_frontend.md b/docs/contributor/generators/auth_vanilla_frontend.md new file mode 100644 index 0000000..c3ba036 --- /dev/null +++ b/docs/contributor/generators/auth_vanilla_frontend.md @@ -0,0 +1,74 @@ +# Generator: `auth_vanilla_frontend` + +Scaffolds a custom JWT auth layer from scratch — in-memory token management, a React context provider, and a `useAuth` hook. No third-party auth library. + +--- + +## Identity + +| Field | Value | +|-------|-------| +| Name | `auth_vanilla_frontend` | +| Version | `0.1.0` | +| Package | `generators/auth_vanilla_frontend` | + +--- + +## Dependencies + +| Generator | Why | +|-----------|-----| +| `typescript_base` | Requires the base project structure | + +--- + +## Answers consumed + +None. + +--- + +## Files written + +| Path | Description | +|------|-------------| +| `src/lib/auth.ts` | Token storage and HTTP helpers (in-memory, no external deps) | +| `src/providers/AuthProvider.tsx` | React context provider for auth state | +| `src/hooks/useAuth.ts` | Hook returning user state and auth actions | + +--- + +## Validators + +| Check | Type | Passes when | +|-------|------|-------------| +| `src/hooks/useAuth.ts` | `file_exists` | File is present after generation | + +--- + +## Post-generation commands + +| Command | WorkDir | Notes | +|---------|---------|-------| +| `pnpm install --dangerously-allow-all-builds` | project root | Installs base deps | + +## Test commands + +| Command | Background | Ready delay | Notes | +|---------|-----------|-------------|-------| +| `pnpm exec tsc --noEmit` | No | — | Verifies TypeScript compiles | + +--- + +## Conflicts + +| Generator | Reason | +|-----------|--------| +| `auth_clerk_frontend` | Both provide auth provider and useAuth hook | +| `auth_better_auth_frontend` | Both provide auth provider and useAuth hook | + +--- + +## See also + +- [docs/flows/frontend.md](../flows/frontend.md) diff --git a/docs/contributor/generators/biome_config.md b/docs/contributor/generators/biome_config.md index fd8fe0e..f9237a5 100644 --- a/docs/contributor/generators/biome_config.md +++ b/docs/contributor/generators/biome_config.md @@ -9,7 +9,7 @@ Biome formatter and linter configuration. Writes `biome.json` and merges `lint`/ | Field | Value | |-------|-------| | Name | `biome_config` | -| Version | `0.2.2` | +| Version | `0.2.4` | | Package | `generators/biome_config` | --- diff --git a/docs/contributor/generators/css_modules.md b/docs/contributor/generators/css_modules.md new file mode 100644 index 0000000..0d1ba0d --- /dev/null +++ b/docs/contributor/generators/css_modules.md @@ -0,0 +1,70 @@ +# Generator: `css_modules` + +Sets up CSS Modules with a global stylesheet and an example module file. + +--- + +## Identity + +| Field | Value | +|-------|-------| +| Name | `css_modules` | +| Version | `0.1.0` | +| Package | `generators/css_modules` | + +--- + +## Dependencies + +| Generator | Why | +|-----------|-----| +| `typescript_base` | Provides the base project structure | + +--- + +## Answers consumed + +None. + +--- + +## Files written + +| Path | Description | +|------|-------------| +| `src/styles/global.css` | Global stylesheet (resets, variables) | +| `src/styles/App.module.css` | Example CSS Module | + +--- + +## Validators + +| Check | Type | Passes when | +|-------|------|-------------| +| `src/styles/App.module.css` | `file_exists` | File is present after generation | + +--- + +## Post-generation commands + +No PostGenerationCommands. + +## Test commands + +No TestCommands. + +--- + +## Conflicts + +| Generator | Reason | +|-----------|--------| +| `tailwind_v4` | Conflicting styling approaches | +| `panda_css` | Conflicting styling approaches | +| `shadcn_ui` | shadcn manages its own CSS setup | + +--- + +## See also + +- [docs/flows/frontend.md](../flows/frontend.md) diff --git a/docs/contributor/generators/feature_flags_local.md b/docs/contributor/generators/feature_flags_local.md new file mode 100644 index 0000000..4518030 --- /dev/null +++ b/docs/contributor/generators/feature_flags_local.md @@ -0,0 +1,67 @@ +# Generator: `feature_flags_local` + +Adds local JSON-based feature flags — a static `public/flags.json` file, a flags reader, and a `useFeatureFlag` hook. No external service required. + +--- + +## Identity + +| Field | Value | +|-------|-------| +| Name | `feature_flags_local` | +| Version | `0.1.0` | +| Package | `generators/feature_flags_local` | + +--- + +## Dependencies + +| Generator | Why | +|-----------|-----| +| `typescript_base` | Requires the base project structure | + +--- + +## Answers consumed + +None. + +--- + +## Files written + +| Path | Description | +|------|-------------| +| `public/flags.json` | Static flag definitions (key → boolean) | +| `src/lib/flags.ts` | Async loader that fetches `/flags.json` at runtime | +| `src/hooks/useFeatureFlag.ts` | Hook for reading a flag value | + +--- + +## Validators + +| Check | Type | Passes when | +|-------|------|-------------| +| `src/lib/flags.ts` | `file_exists` | File is present after generation | + +--- + +## Post-generation commands + +No PostGenerationCommands. + +## Test commands + +No TestCommands. + +--- + +## Conflicts + +None. + +--- + +## See also + +- [docs/flows/frontend.md](../flows/frontend.md) diff --git a/docs/contributor/generators/feature_flags_posthog.md b/docs/contributor/generators/feature_flags_posthog.md new file mode 100644 index 0000000..c2b0fc6 --- /dev/null +++ b/docs/contributor/generators/feature_flags_posthog.md @@ -0,0 +1,78 @@ +# Generator: `feature_flags_posthog` + +Adds PostHog feature flags — installs `posthog-js`, writes a PostHog client initializer, a React provider, and a `.env.example`. + +--- + +## Identity + +| Field | Value | +|-------|-------| +| Name | `feature_flags_posthog` | +| Version | `0.2.0` | +| Package | `generators/feature_flags_posthog` | + +--- + +## Dependencies + +| Generator | Why | +|-----------|-----| +| `typescript_base` | Requires package.json to merge dependencies into | + +--- + +## Answers consumed + +None. + +--- + +## Files written + +| Path | Description | +|------|-------------| +| `src/lib/posthog.ts` | PostHog client initialization | +| `src/providers/PostHogProvider.tsx` | React provider wrapping `PostHogProvider` from posthog-js | +| `.env.example` | Required env vars (`VITE_POSTHOG_KEY`, `VITE_POSTHOG_HOST`) | + +Also merges into: + +| Path | Keys added / updated | +|------|---------------------| +| `package.json` | `dependencies`: `posthog-js^1` | + +--- + +## Validators + +| Check | Type | Passes when | +|-------|------|-------------| +| `src/lib/posthog.ts` | `file_exists` | File is present after generation | + +--- + +## Post-generation commands + +| Command | WorkDir | Notes | +|---------|---------|-------| +| `pnpm install --dangerously-allow-all-builds` | project root | Installs PostHog JS SDK | + +## Test commands + +| Command | Background | Ready delay | Notes | +|---------|-----------|-------------|-------| +| `pnpm exec tsc --noEmit` | No | — | Verifies TypeScript compiles | + +--- + +## Conflicts + +None. + +--- + +## See also + +- [docs/flows/frontend.md](../flows/frontend.md) +- [docs/generators/analytics_plausible.md](analytics_plausible.md) — note dedup: selecting PostHog for both feature flags and analytics emits this generator once diff --git a/docs/contributor/generators/feature_flags_vercel.md b/docs/contributor/generators/feature_flags_vercel.md new file mode 100644 index 0000000..f07f9af --- /dev/null +++ b/docs/contributor/generators/feature_flags_vercel.md @@ -0,0 +1,77 @@ +# Generator: `feature_flags_vercel` + +Adds Vercel Edge Config feature flags — installs `@vercel/edge-config` and `@vercel/flags`, writes a flags client and a `useFeatureFlag` hook. + +--- + +## Identity + +| Field | Value | +|-------|-------| +| Name | `feature_flags_vercel` | +| Version | `0.2.0` | +| Package | `generators/feature_flags_vercel` | + +--- + +## Dependencies + +| Generator | Why | +|-----------|-----| +| `typescript_base` | Requires package.json to merge dependencies into | + +--- + +## Answers consumed + +None. + +--- + +## Files written + +| Path | Description | +|------|-------------| +| `src/lib/flags.ts` | Edge Config client + flag helpers | +| `src/hooks/useFeatureFlag.ts` | Hook for reading a flag value | +| `.env.example` | Required env vars (`EDGE_CONFIG`, `FLAGS_SECRET`) | + +Also merges into: + +| Path | Keys added / updated | +|------|---------------------| +| `package.json` | `dependencies`: `@vercel/edge-config`, `@vercel/flags` | + +--- + +## Validators + +| Check | Type | Passes when | +|-------|------|-------------| +| `src/lib/flags.ts` | `file_exists` | File is present after generation | + +--- + +## Post-generation commands + +| Command | WorkDir | Notes | +|---------|---------|-------| +| `pnpm install --dangerously-allow-all-builds` | project root | Installs Vercel Edge Config packages | + +## Test commands + +| Command | Background | Ready delay | Notes | +|---------|-----------|-------------|-------| +| `pnpm exec tsc --noEmit` | No | — | Verifies TypeScript compiles | + +--- + +## Conflicts + +None. + +--- + +## See also + +- [docs/flows/frontend.md](../flows/frontend.md) diff --git a/docs/contributor/generators/jotai_setup.md b/docs/contributor/generators/jotai_setup.md new file mode 100644 index 0000000..7d3e6d6 --- /dev/null +++ b/docs/contributor/generators/jotai_setup.md @@ -0,0 +1,77 @@ +# Generator: `jotai_setup` + +Adds Jotai v2 atomic state management with a counter atom example. + +--- + +## Identity + +| Field | Value | +|-------|-------| +| Name | `jotai_setup` | +| Version | `0.2.0` | +| Package | `generators/jotai_setup` | + +--- + +## Dependencies + +| Generator | Why | +|-----------|-----| +| `typescript_base` | Requires package.json to merge dependencies into | + +--- + +## Answers consumed + +None. + +--- + +## Files written + +| Path | Description | +|------|-------------| +| `src/atoms/counter.atom.ts` | Counter atom using Jotai's `atom()` | + +Also merges into: + +| Path | Keys added / updated | +|------|---------------------| +| `package.json` | `dependencies`: `jotai^2` | + +--- + +## Validators + +| Check | Type | Passes when | +|-------|------|-------------| +| `src/atoms/counter.atom.ts` | `file_exists` | File is present after generation | + +--- + +## Post-generation commands + +| Command | WorkDir | Notes | +|---------|---------|-------| +| `pnpm install --dangerously-allow-all-builds` | project root | Installs Jotai | + +## Test commands + +| Command | Background | Ready delay | Notes | +|---------|-----------|-------------|-------| +| `pnpm exec tsc --noEmit` | No | — | Verifies TypeScript compiles | + +--- + +## Conflicts + +| Generator | Reason | +|-----------|--------| +| `zustand_setup` | Both provide global state management | + +--- + +## See also + +- [docs/flows/frontend.md](../flows/frontend.md) diff --git a/docs/contributor/generators/nextjs_base.md b/docs/contributor/generators/nextjs_base.md new file mode 100644 index 0000000..bdf8a0b --- /dev/null +++ b/docs/contributor/generators/nextjs_base.md @@ -0,0 +1,77 @@ +# Generator: `nextjs_base` + +Bootstraps a Next.js 15 App Router project with TypeScript, writing the entry layout, home page, global CSS, and next.config.ts. + +--- + +## Identity + +| Field | Value | +|-------|-------| +| Name | `nextjs_base` | +| Version | `0.2.0` | +| Package | `generators/nextjs_base` | + +--- + +## Dependencies + +| Generator | Why | +|-----------|-----| +| `typescript_base` | Requires tsconfig.json to patch with Next.js-specific compiler options | + +--- + +## Answers consumed + +None. + +--- + +## Files written + +| Path | Description | +|------|-------------| +| `next.config.ts` | Minimal Next.js config | +| `src/app/layout.tsx` | Root layout component | +| `src/app/page.tsx` | Home page | +| `src/app/globals.css` | Global stylesheet entry point | + +Also merges into: + +| Path | Keys added / updated | +|------|---------------------| +| `package.json` | `dependencies`: `next`, `react`, `react-dom` | +| `tsconfig.json` | `compilerOptions.jsx`, `compilerOptions.plugins`, `compilerOptions.paths` | + +--- + +## Validators + +None. + +--- + +## Post-generation commands + +| Command | WorkDir | Notes | +|---------|---------|-------| +| `pnpm install --dangerously-allow-all-builds` | project root | Installs Next.js and React | + +## Test commands + +| Command | Background | Ready delay | Notes | +|---------|-----------|-------------|-------| +| `pnpm exec tsc --noEmit` | No | — | Verifies TypeScript compiles | + +--- + +## Conflicts + +None. + +--- + +## See also + +- [docs/flows/frontend.md](../flows/frontend.md) diff --git a/docs/contributor/generators/panda_css.md b/docs/contributor/generators/panda_css.md new file mode 100644 index 0000000..fe78bc3 --- /dev/null +++ b/docs/contributor/generators/panda_css.md @@ -0,0 +1,84 @@ +# Generator: `panda_css` + +Sets up Panda CSS v1 with a config file. Registers `panda codegen` as a `prepare` npm script so the styled-system is regenerated on every `pnpm install`. Also adds `styled-system/` to `.prettierignore` (and to `biome.json` files.ignore when Biome is the linter). + +--- + +## Identity + +| Field | Value | +|-------|-------| +| Name | `panda_css` | +| Version | `0.2.0` | +| Package | `generators/panda_css` | + +--- + +## Dependencies + +| Generator | Why | +|-----------|-----| +| `typescript_base` | Requires package.json to merge dependencies into | + +--- + +## Answers consumed + +| Key | Type | Required | Notes | +|-----|------|----------|-------| +| `linter` | string | No | Init flow linter key — `"biome"` adds `styled-system/**` to biome.json | +| `frontend-linter` | string | No | Frontend flow linter key — same effect as `linter` | + +--- + +## Files written + +| Path | Description | +|------|-------------| +| `panda.config.ts` | Panda CSS configuration | +| `postcss.config.mjs` | PostCSS config pointing to Panda's PostCSS plugin | + +Also merges into: + +| Path | Keys added / updated | +|------|---------------------| +| `package.json` | `devDependencies`: `@pandacss/dev`; `scripts.prepare`: `panda codegen` (appended) | +| `.prettierignore` | Appends `styled-system/` | +| `biome.json` | `files.ignore`: `styled-system/**` (biome only) | + +--- + +## Validators + +| Check | Type | Passes when | +|-------|------|-------------| +| `panda.config.ts` | `file_exists` | File is present after generation | + +--- + +## Post-generation commands + +| Command | WorkDir | Notes | +|---------|---------|-------| +| `pnpm install --dangerously-allow-all-builds` | project root | Installs Panda CSS; triggers `prepare` → `panda codegen` | + +## Test commands + +| Command | Background | Ready delay | Notes | +|---------|-----------|-------------|-------| +| `pnpm exec tsc --noEmit` | No | — | Verifies TypeScript compiles | + +--- + +## Conflicts + +| Generator | Reason | +|-----------|--------| +| `tailwind_v4` | Conflicting utility-first CSS systems | +| `shadcn_ui` | shadcn manages its own CSS setup | + +--- + +## See also + +- [docs/flows/frontend.md](../flows/frontend.md) diff --git a/docs/contributor/generators/playwright_setup.md b/docs/contributor/generators/playwright_setup.md new file mode 100644 index 0000000..cb686c7 --- /dev/null +++ b/docs/contributor/generators/playwright_setup.md @@ -0,0 +1,77 @@ +# Generator: `playwright_setup` + +Adds Playwright E2E testing — framework-aware dev server URL (5173 for Vite, 3000 for Next.js) and an example spec. + +--- + +## Identity + +| Field | Value | +|-------|-------| +| Name | `playwright_setup` | +| Version | `0.2.0` | +| Package | `generators/playwright_setup` | + +--- + +## Dependencies + +| Generator | Why | +|-----------|-----| +| `typescript_base` | Requires package.json to merge dependencies into | + +--- + +## Answers consumed + +| Key | Type | Required | Notes | +|-----|------|----------|-------| +| `framework` | string | No | `"next"` → dev server URL `http://localhost:3000`; otherwise `http://localhost:5173` | + +--- + +## Files written + +| Path | Description | +|------|-------------| +| `playwright.config.ts` | Playwright config with dev server URL | +| `e2e/example.spec.ts` | Example homepage navigation test | + +Also merges into: + +| Path | Keys added / updated | +|------|---------------------| +| `package.json` | `devDependencies`: `@playwright/test` | + +--- + +## Validators + +| Check | Type | Passes when | +|-------|------|-------------| +| `playwright.config.ts` | `file_exists` | File is present after generation | + +--- + +## Post-generation commands + +| Command | WorkDir | Notes | +|---------|---------|-------| +| `pnpm install --dangerously-allow-all-builds` | project root | Installs Playwright package | +| `pnpm exec playwright install --with-deps chromium` | project root | Downloads Chromium browser | + +## Test commands + +No TestCommands. + +--- + +## Conflicts + +None. + +--- + +## See also + +- [docs/flows/frontend.md](../flows/frontend.md) diff --git a/docs/contributor/generators/prettier_config.md b/docs/contributor/generators/prettier_config.md index 77bd72a..548cb39 100644 --- a/docs/contributor/generators/prettier_config.md +++ b/docs/contributor/generators/prettier_config.md @@ -9,7 +9,7 @@ Creates the base Prettier configuration files (`.prettierrc` and `.prettierignor | Field | Value | |-------|-------| | Name | `prettier_config` | -| Version | `0.1.0` | +| Version | `0.1.1` | | Package | `generators/prettier_config` | --- diff --git a/docs/contributor/generators/prettier_frontend_rules.md b/docs/contributor/generators/prettier_frontend_rules.md new file mode 100644 index 0000000..53da4b8 --- /dev/null +++ b/docs/contributor/generators/prettier_frontend_rules.md @@ -0,0 +1,65 @@ +# Generator: `prettier_frontend_rules` + +Merges opinionated frontend Prettier rules into `.prettierrc` — JSX single quotes, 80-column print width, trailing commas, etc. + +--- + +## Identity + +| Field | Value | +|-------|-------| +| Name | `prettier_frontend_rules` | +| Version | `0.1.0` | +| Package | `generators/prettier_frontend_rules` | + +--- + +## Dependencies + +| Generator | Why | +|-----------|-----| +| `prettier_config` | Creates `.prettierrc` that this generator merges into | + +--- + +## Answers consumed + +None. + +--- + +## Files written + +Also merges into: + +| Path | Keys added / updated | +|------|---------------------| +| `.prettierrc` | `semi`, `singleQuote`, `jsxSingleQuote`, `trailingComma`, `printWidth`, `tabWidth`, `bracketSpacing`, `bracketSameLine` | + +--- + +## Validators + +None. + +--- + +## Post-generation commands + +No PostGenerationCommands. + +## Test commands + +No TestCommands. + +--- + +## Conflicts + +None. + +--- + +## See also + +- [docs/flows/frontend.md](../flows/frontend.md) diff --git a/docs/contributor/generators/react_app.md b/docs/contributor/generators/react_app.md index b340aef..c0a10d0 100644 --- a/docs/contributor/generators/react_app.md +++ b/docs/contributor/generators/react_app.md @@ -9,7 +9,7 @@ React + Vite application. Merges React dependencies and JSX compiler options int | Field | Value | | ------- | ---------------------- | | Name | `react_app` | -| Version | `0.5.1` | +| Version | `0.6.0` | | Package | `generators/react_app` | --- diff --git a/docs/contributor/generators/react_router_v7.md b/docs/contributor/generators/react_router_v7.md new file mode 100644 index 0000000..3a395c9 --- /dev/null +++ b/docs/contributor/generators/react_router_v7.md @@ -0,0 +1,79 @@ +# Generator: `react_router_v7` + +Adds React Router v7 to a React+Vite app — wires up a `RouterProvider`, creates the router definition, and adds a Home page. + +--- + +## Identity + +| Field | Value | +|-------|-------| +| Name | `react_router_v7` | +| Version | `0.2.0` | +| Package | `generators/react_router_v7` | + +--- + +## Dependencies + +| Generator | Why | +|-----------|-----| +| `react_app` | Requires the Vite+React base (src/main.tsx to overwrite) | + +--- + +## Answers consumed + +None. + +--- + +## Files written + +| Path | Description | +|------|-------------| +| `src/main.tsx` | Overwritten — mounts `RouterProvider` | +| `src/router.tsx` | Router definition with a single `/` route | +| `src/pages/Home.tsx` | Home page component | + +Also merges into: + +| Path | Keys added / updated | +|------|---------------------| +| `package.json` | `dependencies`: `react-router`, `react-router-dom` | + +--- + +## Validators + +| Check | Type | Passes when | +|-------|------|-------------| +| `src/router.tsx` | `file_exists` | File is present after generation | + +--- + +## Post-generation commands + +| Command | WorkDir | Notes | +|---------|---------|-------| +| `pnpm install --dangerously-allow-all-builds` | project root | Installs React Router packages | + +## Test commands + +| Command | Background | Ready delay | Notes | +|---------|-----------|-------------|-------| +| `pnpm exec tsc --noEmit` | No | — | Verifies TypeScript compiles | + +--- + +## Conflicts + +| Generator | Reason | +|-----------|--------| +| `tanstack_router` | Both rewrite src/main.tsx as the router entry point | + +--- + +## See also + +- [docs/flows/frontend.md](../flows/frontend.md) diff --git a/docs/contributor/generators/sentry_frontend.md b/docs/contributor/generators/sentry_frontend.md new file mode 100644 index 0000000..ed22254 --- /dev/null +++ b/docs/contributor/generators/sentry_frontend.md @@ -0,0 +1,79 @@ +# Generator: `sentry_frontend` + +Adds Sentry error tracking — framework-aware: `@sentry/nextjs` with a client config file for Next.js, `@sentry/react` for Vite. + +--- + +## Identity + +| Field | Value | +|-------|-------| +| Name | `sentry_frontend` | +| Version | `0.2.0` | +| Package | `generators/sentry_frontend` | + +--- + +## Dependencies + +| Generator | Why | +|-----------|-----| +| `typescript_base` | Requires package.json to merge dependencies into | + +--- + +## Answers consumed + +| Key | Type | Required | Notes | +|-----|------|----------|-------| +| `framework` | string | No | `"next"` → installs `@sentry/nextjs`, writes `sentry.client.config.ts`; otherwise `@sentry/react` | + +--- + +## Files written + +| Path | Description | +|------|-------------| +| `src/lib/sentry.ts` | Sentry init helper | +| `sentry.client.config.ts` | Next.js client-side Sentry configuration (Next.js only) | +| `.env.example` | Required env vars (`VITE_SENTRY_DSN` or `NEXT_PUBLIC_SENTRY_DSN`) | + +Also merges into: + +| Path | Keys added / updated | +|------|---------------------| +| `package.json` | `dependencies`: `@sentry/nextjs` or `@sentry/react` | + +--- + +## Validators + +| Check | Type | Passes when | +|-------|------|-------------| +| `src/lib/sentry.ts` | `file_exists` | File is present after generation | + +--- + +## Post-generation commands + +| Command | WorkDir | Notes | +|---------|---------|-------| +| `pnpm install --dangerously-allow-all-builds` | project root | Installs Sentry SDK | + +## Test commands + +| Command | Background | Ready delay | Notes | +|---------|-----------|-------------|-------| +| `pnpm exec tsc --noEmit` | No | — | Verifies TypeScript compiles | + +--- + +## Conflicts + +None. + +--- + +## See also + +- [docs/flows/frontend.md](../flows/frontend.md) diff --git a/docs/contributor/generators/seo_react.md b/docs/contributor/generators/seo_react.md new file mode 100644 index 0000000..51fa339 --- /dev/null +++ b/docs/contributor/generators/seo_react.md @@ -0,0 +1,78 @@ +# Generator: `seo_react` + +Adds `react-helmet-async` SEO for React+Vite — writes a `` wrapper and a reusable `` component. + +--- + +## Identity + +| Field | Value | +|-------|-------| +| Name | `seo_react` | +| Version | `0.2.0` | +| Package | `generators/seo_react` | + +--- + +## Dependencies + +| Generator | Why | +|-----------|-----| +| `react_app` | react-helmet-async is a React+Vite-specific integration | + +--- + +## Answers consumed + +None. + +--- + +## Files written + +| Path | Description | +|------|-------------| +| `src/providers/HelmetProvider.tsx` | Thin wrapper around `react-helmet-async`'s `HelmetProvider` | +| `src/components/SEO.tsx` | `` component for per-page title, description, canonical | + +Also merges into: + +| Path | Keys added / updated | +|------|---------------------| +| `package.json` | `dependencies`: `react-helmet-async^2` | + +--- + +## Validators + +| Check | Type | Passes when | +|-------|------|-------------| +| `src/components/SEO.tsx` | `file_exists` | File is present after generation | + +--- + +## Post-generation commands + +| Command | WorkDir | Notes | +|---------|---------|-------| +| `pnpm install --dangerously-allow-all-builds` | project root | Installs react-helmet-async | + +## Test commands + +| Command | Background | Ready delay | Notes | +|---------|-----------|-------------|-------| +| `pnpm exec tsc --noEmit` | No | — | Verifies TypeScript compiles | + +--- + +## Conflicts + +| Generator | Reason | +|-----------|--------| +| `seo_next` | Both write `src/components/SEO.tsx` | + +--- + +## See also + +- [docs/flows/frontend.md](../flows/frontend.md) diff --git a/docs/contributor/generators/shadcn_ui.md b/docs/contributor/generators/shadcn_ui.md new file mode 100644 index 0000000..8ded97b --- /dev/null +++ b/docs/contributor/generators/shadcn_ui.md @@ -0,0 +1,84 @@ +# Generator: `shadcn_ui` + +Configures shadcn/ui with Tailwind v4 — writes components.json, a button component, and the global CSS entry point. Implicitly replaces the `tailwind_v4` generator (they conflict). + +--- + +## Identity + +| Field | Value | +|-------|-------| +| Name | `shadcn_ui` | +| Version | `0.2.0` | +| Package | `generators/shadcn_ui` | + +--- + +## Dependencies + +| Generator | Why | +|-----------|-----| +| `typescript_base` | Requires package.json to merge dependencies into | + +--- + +## Answers consumed + +| Key | Type | Required | Notes | +|-----|------|----------|-------| +| `framework` | string | No | `"next"` → writes CSS to `src/app/globals.css`; otherwise `src/styles/globals.css` | + +--- + +## Files written + +| Path | Description | +|------|-------------| +| `components.json` | shadcn/ui configuration | +| `src/app/globals.css` or `src/styles/globals.css` | Global CSS with `@import "tailwindcss"` (path depends on framework) | +| `src/lib/utils.ts` | `cn()` helper using `clsx` + `tailwind-merge` | +| `src/components/ui/button.tsx` | Example Button component | + +Also merges into: + +| Path | Keys added / updated | +|------|---------------------| +| `package.json` | `dependencies`: `tailwindcss`, `clsx`, `tailwind-merge`; `devDependencies`: `@tailwindcss/vite` | + +--- + +## Validators + +| Check | Type | Passes when | +|-------|------|-------------| +| `components.json` | `file_exists` | File is present after generation | + +--- + +## Post-generation commands + +| Command | WorkDir | Notes | +|---------|---------|-------| +| `pnpm install --dangerously-allow-all-builds` | project root | Installs Tailwind and shadcn deps | + +## Test commands + +| Command | Background | Ready delay | Notes | +|---------|-----------|-------------|-------| +| `pnpm exec tsc --noEmit` | No | — | Verifies TypeScript compiles | + +--- + +## Conflicts + +| Generator | Reason | +|-----------|--------| +| `tailwind_v4` | Both provide Tailwind CSS setup | +| `panda_css` | Conflicting CSS-in-JS / utility-first systems | +| `css_modules` | Conflicting styling approaches | + +--- + +## See also + +- [docs/flows/frontend.md](../flows/frontend.md) diff --git a/docs/contributor/generators/storybook_setup.md b/docs/contributor/generators/storybook_setup.md new file mode 100644 index 0000000..84bcb5c --- /dev/null +++ b/docs/contributor/generators/storybook_setup.md @@ -0,0 +1,77 @@ +# Generator: `storybook_setup` + +Configures Storybook v8 — framework-aware: uses `@storybook/react-vite` for Vite projects and `@storybook/nextjs` for Next.js. + +--- + +## Identity + +| Field | Value | +|-------|-------| +| Name | `storybook_setup` | +| Version | `0.2.0` | +| Package | `generators/storybook_setup` | + +--- + +## Dependencies + +| Generator | Why | +|-----------|-----| +| `typescript_base` | Requires package.json to merge dependencies into | + +--- + +## Answers consumed + +| Key | Type | Required | Notes | +|-----|------|----------|-------| +| `framework` | string | No | `"next"` → uses `@storybook/nextjs`; otherwise uses `@storybook/react-vite` | + +--- + +## Files written + +| Path | Description | +|------|-------------| +| `.storybook/main.ts` | Storybook main config (framework, addons) | +| `.storybook/preview.ts` | Global decorators and parameters | +| `src/stories/Button.stories.tsx` | Example Button story | + +Also merges into: + +| Path | Keys added / updated | +|------|---------------------| +| `package.json` | `devDependencies`: `storybook`, framework-specific `@storybook/*` adapter, `@storybook/addon-essentials`, `@storybook/addon-interactions` | + +--- + +## Validators + +| Check | Type | Passes when | +|-------|------|-------------| +| `.storybook/main.ts` | `file_exists` | File is present after generation | + +--- + +## Post-generation commands + +| Command | WorkDir | Notes | +|---------|---------|-------| +| `pnpm install --dangerously-allow-all-builds` | project root | Installs Storybook packages | + +## Test commands + +No TestCommands. + +--- + +## Conflicts + +None. + +--- + +## See also + +- [docs/flows/frontend.md](../flows/frontend.md) diff --git a/docs/contributor/generators/tailwind_v4.md b/docs/contributor/generators/tailwind_v4.md new file mode 100644 index 0000000..92d9bba --- /dev/null +++ b/docs/contributor/generators/tailwind_v4.md @@ -0,0 +1,81 @@ +# Generator: `tailwind_v4` + +Configures Tailwind CSS v4 — framework-aware: uses the Vite plugin for React+Vite and the PostCSS adapter for Next.js. + +--- + +## Identity + +| Field | Value | +|-------|-------| +| Name | `tailwind_v4` | +| Version | `0.2.0` | +| Package | `generators/tailwind_v4` | + +--- + +## Dependencies + +| Generator | Why | +|-----------|-----| +| `typescript_base` | Requires package.json to merge dependencies into | + +--- + +## Answers consumed + +| Key | Type | Required | Notes | +|-----|------|----------|-------| +| `framework` | string | No | `"next"` → uses `@tailwindcss/postcss` + `postcss.config.mjs`; otherwise uses `@tailwindcss/vite` Vite plugin | + +--- + +## Files written + +| Path | Description | +|------|-------------| +| `src/styles/globals.css` | Global CSS with `@import "tailwindcss"` | +| `postcss.config.mjs` | PostCSS config (Next.js only) | + +Also merges into: + +| Path | Keys added / updated | +|------|---------------------| +| `package.json` | `devDependencies`: `tailwindcss` + either `@tailwindcss/vite` or `@tailwindcss/postcss` | + +--- + +## Validators + +| Check | Type | Passes when | +|-------|------|-------------| +| `src/styles/globals.css` | `file_exists` | File is present after generation | + +--- + +## Post-generation commands + +| Command | WorkDir | Notes | +|---------|---------|-------| +| `pnpm install --dangerously-allow-all-builds` | project root | Installs Tailwind CSS v4 | + +## Test commands + +| Command | Background | Ready delay | Notes | +|---------|-----------|-------------|-------| +| `pnpm exec tsc --noEmit` | No | — | Verifies TypeScript compiles | + +--- + +## Conflicts + +| Generator | Reason | +|-----------|--------| +| `panda_css` | Conflicting CSS-in-JS / utility-first systems | +| `shadcn_ui` | shadcn manages its own Tailwind setup | + +--- + +## See also + +- [docs/flows/frontend.md](../flows/frontend.md) diff --git a/docs/contributor/generators/tanstack_router.md b/docs/contributor/generators/tanstack_router.md new file mode 100644 index 0000000..6b7b13e --- /dev/null +++ b/docs/contributor/generators/tanstack_router.md @@ -0,0 +1,80 @@ +# Generator: `tanstack_router` + +Adds TanStack Router (file-based routing) to a React+Vite app — installs the Vite plugin, wires the root route, and creates a typed index route. + +--- + +## Identity + +| Field | Value | +|-------|-------| +| Name | `tanstack_router` | +| Version | `0.2.0` | +| Package | `generators/tanstack_router` | + +--- + +## Dependencies + +| Generator | Why | +|-----------|-----| +| `react_app` | Requires the Vite+React base (vite.config.ts and src/main.tsx to overwrite) | + +--- + +## Answers consumed + +None. + +--- + +## Files written + +| Path | Description | +|------|-------------| +| `vite.config.ts` | Overwritten — adds `TanStackRouterVite` plugin | +| `src/main.tsx` | Overwritten — mounts `RouterProvider` with TanStack router | +| `src/routes/__root.tsx` | Root route layout | +| `src/routes/index.tsx` | Index route (renders at `/`) | + +Also merges into: + +| Path | Keys added / updated | +|------|---------------------| +| `package.json` | `dependencies`: `@tanstack/react-router`; `devDependencies`: `@tanstack/router-plugin` | + +--- + +## Validators + +| Check | Type | Passes when | +|-------|------|-------------| +| `src/routes/__root.tsx` | `file_exists` | File is present after generation | + +--- + +## Post-generation commands + +| Command | WorkDir | Notes | +|---------|---------|-------| +| `pnpm install --dangerously-allow-all-builds` | project root | Installs TanStack Router packages | + +## Test commands + +| Command | Background | Ready delay | Notes | +|---------|-----------|-------------|-------| +| `pnpm exec tsc --noEmit` | No | — | Verifies TypeScript compiles | + +--- + +## Conflicts + +| Generator | Reason | +|-----------|--------| +| `react_router_v7` | Both rewrite src/main.tsx as the router entry point | + +--- + +## See also + +- [docs/flows/frontend.md](../flows/frontend.md) diff --git a/docs/contributor/generators/theme_provider.md b/docs/contributor/generators/theme_provider.md new file mode 100644 index 0000000..bd3c1c4 --- /dev/null +++ b/docs/contributor/generators/theme_provider.md @@ -0,0 +1,71 @@ +# Generator: `theme_provider` + +Adds a custom CSS-variable-based theme system with light/dark mode support — a ThemeProvider, a `useTheme` hook, and a global CSS variables file. + +--- + +## Identity + +| Field | Value | +|-------|-------| +| Name | `theme_provider` | +| Version | `0.1.0` | +| Package | `generators/theme_provider` | + +--- + +## Dependencies + +| Generator | Why | +|-----------|-----| +| `typescript_base` | Requires the base project structure | + +--- + +## Answers consumed + +None. + +--- + +## Files written + +| Path | Description | +|------|-------------| +| `src/styles/theme.css` | CSS custom properties for light and dark color schemes | +| `src/providers/ThemeProvider.tsx` | React provider — reads localStorage, listens to `prefers-color-scheme` | +| `src/hooks/useTheme.ts` | Hook returning current theme and a toggle function | + +--- + +## Validators + +| Check | Type | Passes when | +|-------|------|-------------| +| `src/providers/ThemeProvider.tsx` | `file_exists` | File is present after generation | + +--- + +## Post-generation commands + +| Command | WorkDir | Notes | +|---------|---------|-------| +| `pnpm install --dangerously-allow-all-builds` | project root | Installs base deps | + +## Test commands + +| Command | Background | Ready delay | Notes | +|---------|-----------|-------------|-------| +| `pnpm exec tsc --noEmit` | No | — | Verifies TypeScript compiles | + +--- + +## Conflicts + +None. + +--- + +## See also + +- [docs/flows/frontend.md](../flows/frontend.md) diff --git a/docs/contributor/generators/version14_ui.md b/docs/contributor/generators/version14_ui.md new file mode 100644 index 0000000..510de10 --- /dev/null +++ b/docs/contributor/generators/version14_ui.md @@ -0,0 +1,78 @@ +# Generator: `version14_ui` + +Adds `@version14/ui` — the company's WIP component library built with Panda CSS — and writes an example component. + +--- + +## Identity + +| Field | Value | +|-------|-------| +| Name | `version14_ui` | +| Version | `0.1.0` | +| Package | `generators/version14_ui` | + +--- + +## Dependencies + +| Generator | Why | +|-----------|-----| +| `typescript_base` | Requires package.json to merge dependencies into | + +--- + +## Answers consumed + +None. + +--- + +## Files written + +| Path | Description | +|------|-------------| +| `src/components/V14Example.tsx` | Example component using `@version14/ui` | + +Also merges into: + +| Path | Keys added / updated | +|------|---------------------| +| `package.json` | `dependencies`: `@version14/ui@latest` | + +--- + +## Validators + +| Check | Type | Passes when | +|-------|------|-------------| +| `src/components/V14Example.tsx` | `file_exists` | File is present after generation | + +--- + +## Post-generation commands + +| Command | WorkDir | Notes | +|---------|---------|-------| +| `pnpm install --dangerously-allow-all-builds` | project root | Installs @version14/ui from npm | + +## Test commands + +| Command | Background | Ready delay | Notes | +|---------|-----------|-------------|-------| +| `pnpm exec tsc --noEmit` | No | — | Verifies TypeScript compiles | + +--- + +## Conflicts + +| Generator | Reason | +|-----------|--------| +| `shadcn_ui` | Both provide a component library | +| `ark_ui` | Both provide a component library | + +--- + +## See also + +- [docs/flows/frontend.md](../flows/frontend.md) diff --git a/docs/contributor/generators/vitest_testing_library.md b/docs/contributor/generators/vitest_testing_library.md new file mode 100644 index 0000000..d13f9f2 --- /dev/null +++ b/docs/contributor/generators/vitest_testing_library.md @@ -0,0 +1,77 @@ +# Generator: `vitest_testing_library` + +Adds Vitest v2 + React Testing Library for unit and component testing in React+Vite projects. + +--- + +## Identity + +| Field | Value | +|-------|-------| +| Name | `vitest_testing_library` | +| Version | `0.2.0` | +| Package | `generators/vitest_testing_library` | + +--- + +## Dependencies + +| Generator | Why | +|-----------|-----| +| `react_app` | React+Vite-only — requires the Vite setup | + +--- + +## Answers consumed + +None. + +--- + +## Files written + +| Path | Description | +|------|-------------| +| `vitest.config.ts` | Vitest configuration with jsdom environment and setup file | +| `src/test/setup.ts` | Global test setup (imports `@testing-library/jest-dom`) | +| `src/test/App.test.tsx` | Example component test | + +Also merges into: + +| Path | Keys added / updated | +|------|---------------------| +| `package.json` | `devDependencies`: `vitest^2`, `@testing-library/react`, `@testing-library/jest-dom`, `@testing-library/user-event`, `jsdom` | + +--- + +## Validators + +| Check | Type | Passes when | +|-------|------|-------------| +| `vitest.config.ts` | `file_exists` | File is present after generation | + +--- + +## Post-generation commands + +| Command | WorkDir | Notes | +|---------|---------|-------| +| `pnpm install --dangerously-allow-all-builds` | project root | Installs Vitest and Testing Library | + +## Test commands + +| Command | Background | Ready delay | Notes | +|---------|-----------|-------------|-------| +| `pnpm exec vitest run` | No | — | Runs the test suite once (non-watch) | + +--- + +## Conflicts + +None. + +--- + +## See also + +- [docs/flows/frontend.md](../flows/frontend.md) diff --git a/docs/contributor/generators/zustand_setup.md b/docs/contributor/generators/zustand_setup.md new file mode 100644 index 0000000..47dfb84 --- /dev/null +++ b/docs/contributor/generators/zustand_setup.md @@ -0,0 +1,77 @@ +# Generator: `zustand_setup` + +Adds Zustand v5 state management with a typed counter store example. + +--- + +## Identity + +| Field | Value | +|-------|-------| +| Name | `zustand_setup` | +| Version | `0.2.0` | +| Package | `generators/zustand_setup` | + +--- + +## Dependencies + +| Generator | Why | +|-----------|-----| +| `typescript_base` | Requires package.json to merge dependencies into | + +--- + +## Answers consumed + +None. + +--- + +## Files written + +| Path | Description | +|------|-------------| +| `src/stores/counter.store.ts` | Typed counter store with `CounterState` interface | + +Also merges into: + +| Path | Keys added / updated | +|------|---------------------| +| `package.json` | `dependencies`: `zustand^5` | + +--- + +## Validators + +| Check | Type | Passes when | +|-------|------|-------------| +| `src/stores/counter.store.ts` | `file_exists` | File is present after generation | + +--- + +## Post-generation commands + +| Command | WorkDir | Notes | +|---------|---------|-------| +| `pnpm install --dangerously-allow-all-builds` | project root | Installs Zustand | + +## Test commands + +| Command | Background | Ready delay | Notes | +|---------|-----------|-------------|-------| +| `pnpm exec tsc --noEmit` | No | — | Verifies TypeScript compiles | + +--- + +## Conflicts + +| Generator | Reason | +|-----------|--------| +| `jotai_setup` | Both provide global state management | + +--- + +## See also + +- [docs/flows/frontend.md](../flows/frontend.md) diff --git a/flows/frontend.go b/flows/frontend.go new file mode 100644 index 0000000..e084171 --- /dev/null +++ b/flows/frontend.go @@ -0,0 +1,414 @@ +package flows + +import ( + "github.com/version14/dot/internal/flow" + "github.com/version14/dot/internal/spec" +) + +const FRONTEND_REACT_VITE = "react-vite" +const FRONTEND_NEXT = "next" + +const FRONTEND_ROUTER_REACT_ROUTER = "react-router" +const FRONTEND_ROUTER_TANSTACK = "tanstack-router" +const FRONTEND_ROUTER_NONE = "none" + +const FRONTEND_UI_SHADCN = "shadcn" +const FRONTEND_UI_ARKUI = "arkui" +const FRONTEND_UI_VERSION14 = "version14" +const FRONTEND_UI_NONE = "none" + +const FRONTEND_STYLING_TAILWIND = "tailwind" +const FRONTEND_STYLING_CSS_MODULES = "css-modules" +const FRONTEND_STYLING_PANDA = "panda-css" + +const FRONTEND_STATE_ZUSTAND = "zustand" +const FRONTEND_STATE_JOTAI = "jotai" +const FRONTEND_STATE_NONE = "none" + +const FRONTEND_AUTH_CLERK = "clerk" +const FRONTEND_AUTH_BETTER_AUTH = "better-auth" +const FRONTEND_AUTH_VANILLA = "vanilla" + +const FRONTEND_FLAGS_POSTHOG = "posthog" +const FRONTEND_FLAGS_VERCEL = "vercel" +const FRONTEND_FLAGS_LOCAL = "local" + +const FRONTEND_ANALYTICS_GA4 = "ga4" +const FRONTEND_ANALYTICS_PLAUSIBLE = "plausible" +const FRONTEND_ANALYTICS_POSTHOG = "posthog" + +// FrontendFlow is the frontend project wizard. It walks the user through +// framework → router → UI library → styling → state → formatter/linter → +// testing → modules (auth, theme, feature flags, Sentry, analytics, SEO). +// +// Question IDs are stable: re-runs of `dot scaffold` reuse persisted answers. +func FrontendFlow() *FlowDef { + confirmGenerate := &flow.ConfirmQuestion{ + QuestionBase: flow.QuestionBase{ID_: "confirm-generate"}, + Label: "Generate the project now?", + Default: true, + Then: &flow.Next{End: true}, + Else: &flow.Next{End: true}, + } + + includeSEO := &flow.ConfirmQuestion{ + QuestionBase: flow.QuestionBase{ID_: "include-seo"}, + Label: "Include SEO setup?", + Default: false, + Then: &flow.Next{Question: confirmGenerate}, + Else: &flow.Next{Question: confirmGenerate}, + } + + analyticsProvider := &flow.OptionQuestion{ + QuestionBase: flow.QuestionBase{ID_: "analytics-provider"}, + Label: "Analytics provider", + Options: []*flow.Option{ + {Label: "Google Analytics 4", Value: FRONTEND_ANALYTICS_GA4, Next: &flow.Next{Question: includeSEO}}, + {Label: "Plausible", Value: FRONTEND_ANALYTICS_PLAUSIBLE, Next: &flow.Next{Question: includeSEO}}, + {Label: "PostHog", Value: FRONTEND_ANALYTICS_POSTHOG, Next: &flow.Next{Question: includeSEO}}, + }, + } + + includeAnalytics := &flow.ConfirmQuestion{ + QuestionBase: flow.QuestionBase{ID_: "include-analytics"}, + Label: "Include analytics?", + Default: false, + Then: &flow.Next{Question: analyticsProvider}, + Else: &flow.Next{Question: includeSEO}, + } + + includeSentry := &flow.ConfirmQuestion{ + QuestionBase: flow.QuestionBase{ID_: "include-sentry"}, + Label: "Include Sentry error tracking?", + Default: false, + Then: &flow.Next{Question: includeAnalytics}, + Else: &flow.Next{Question: includeAnalytics}, + } + + featureFlagsProvider := &flow.OptionQuestion{ + QuestionBase: flow.QuestionBase{ID_: "feature-flags-provider"}, + Label: "Feature flags provider", + Options: []*flow.Option{ + {Label: "PostHog", Value: FRONTEND_FLAGS_POSTHOG, Next: &flow.Next{Question: includeSentry}}, + {Label: "Vercel Edge Config", Value: FRONTEND_FLAGS_VERCEL, Next: &flow.Next{Question: includeSentry}}, + {Label: "Local JSON", Value: FRONTEND_FLAGS_LOCAL, Next: &flow.Next{Question: includeSentry}}, + }, + } + + includeFeatureFlags := &flow.ConfirmQuestion{ + QuestionBase: flow.QuestionBase{ID_: "include-feature-flags"}, + Label: "Include feature flags?", + Default: false, + Then: &flow.Next{Question: featureFlagsProvider}, + Else: &flow.Next{Question: includeSentry}, + } + + includeTheme := &flow.ConfirmQuestion{ + QuestionBase: flow.QuestionBase{ID_: "include-theme"}, + Label: "Include custom theme provider (CSS variables, light/dark)?", + Default: false, + Then: &flow.Next{Question: includeFeatureFlags}, + Else: &flow.Next{Question: includeFeatureFlags}, + } + + authProvider := &flow.OptionQuestion{ + QuestionBase: flow.QuestionBase{ID_: "auth-provider"}, + Label: "Auth provider", + Options: []*flow.Option{ + {Label: "Clerk", Value: FRONTEND_AUTH_CLERK, Next: &flow.Next{Question: includeTheme}}, + {Label: "Better Auth", Value: FRONTEND_AUTH_BETTER_AUTH, Next: &flow.Next{Question: includeTheme}}, + {Label: "Vanilla (from scratch)", Value: FRONTEND_AUTH_VANILLA, Next: &flow.Next{Question: includeTheme}}, + }, + } + + includeAuth := &flow.ConfirmQuestion{ + QuestionBase: flow.QuestionBase{ID_: "include-auth"}, + Label: "Include authentication?", + Default: false, + Then: &flow.Next{Question: authProvider}, + Else: &flow.Next{Question: includeTheme}, + } + + includeStorybook := &flow.ConfirmQuestion{ + QuestionBase: flow.QuestionBase{ID_: "include-storybook"}, + Label: "Include Storybook?", + Default: false, + Then: &flow.Next{Question: includeAuth}, + Else: &flow.Next{Question: includeAuth}, + } + + includePlaywright := &flow.ConfirmQuestion{ + QuestionBase: flow.QuestionBase{ID_: "include-playwright"}, + Label: "Include Playwright (E2E tests)?", + Default: false, + Then: &flow.Next{Question: includeStorybook}, + Else: &flow.Next{Question: includeStorybook}, + } + + includeVitest := &flow.ConfirmQuestion{ + QuestionBase: flow.QuestionBase{ID_: "include-vitest"}, + Label: "Include Vitest + React Testing Library?", + Default: false, + Then: &flow.Next{Question: includePlaywright}, + Else: &flow.Next{Question: includePlaywright}, + } + + // IfQuestion: Vitest is React+Vite only — skip for Next.js + checkVitest := &flow.IfQuestion{ + QuestionBase: flow.QuestionBase{ID_: "check-vitest-available"}, + Condition: func(ctx *flow.FlowContext) bool { + fw, _ := ctx.Answers["framework"].(string) + return fw == FRONTEND_REACT_VITE + }, + Then: &flow.Next{Question: includeVitest}, + Else: &flow.Next{Question: includePlaywright}, + } + + linter := &flow.OptionQuestion{ + QuestionBase: flow.QuestionBase{ID_: "frontend-linter"}, + Label: "Linter", + Options: []*flow.Option{ + {Label: "Biome", Value: "biome", Next: &flow.Next{Question: checkVitest}}, + {Label: "Prettier", Value: "prettier", Next: &flow.Next{Question: checkVitest}}, + }, + } + + formatter := &flow.OptionQuestion{ + QuestionBase: flow.QuestionBase{ID_: "frontend-formatter"}, + Label: "Formatter", + Options: []*flow.Option{ + {Label: "Biome", Value: "biome", Next: &flow.Next{Question: linter}}, + {Label: "Prettier", Value: "prettier", Next: &flow.Next{Question: linter}}, + }, + } + + state := &flow.OptionQuestion{ + QuestionBase: flow.QuestionBase{ID_: "frontend-state"}, + Label: "State management", + Options: []*flow.Option{ + {Label: "Zustand", Value: FRONTEND_STATE_ZUSTAND, Next: &flow.Next{Question: formatter}}, + {Label: "Jotai", Value: FRONTEND_STATE_JOTAI, Next: &flow.Next{Question: formatter}}, + {Label: "None", Value: FRONTEND_STATE_NONE, Next: &flow.Next{Question: formatter}}, + }, + } + + styling := &flow.OptionQuestion{ + QuestionBase: flow.QuestionBase{ID_: "frontend-styling"}, + Label: "Styling", + Options: []*flow.Option{ + {Label: "Tailwind CSS v4", Value: FRONTEND_STYLING_TAILWIND, Next: &flow.Next{Question: state}}, + {Label: "CSS Modules", Value: FRONTEND_STYLING_CSS_MODULES, Next: &flow.Next{Question: state}}, + {Label: "Panda CSS", Value: FRONTEND_STYLING_PANDA, Next: &flow.Next{Question: state}}, + }, + } + + uiLibrary := &flow.OptionQuestion{ + QuestionBase: flow.QuestionBase{ID_: "ui-library"}, + Label: "UI library", + Options: []*flow.Option{ + // shadcn forces Tailwind — skip styling question + {Label: "shadcn/ui (includes Tailwind v4)", Value: FRONTEND_UI_SHADCN, Next: &flow.Next{Question: state}}, + {Label: "Ark UI", Value: FRONTEND_UI_ARKUI, Next: &flow.Next{Question: styling}}, + {Label: "@version14/ui", Value: FRONTEND_UI_VERSION14, Next: &flow.Next{Question: styling}}, + {Label: "None", Value: FRONTEND_UI_NONE, Next: &flow.Next{Question: styling}}, + }, + } + + router := &flow.OptionQuestion{ + QuestionBase: flow.QuestionBase{ID_: "frontend-router"}, + Label: "Router", + Options: []*flow.Option{ + {Label: "React Router v7", Value: FRONTEND_ROUTER_REACT_ROUTER, Next: &flow.Next{Question: uiLibrary}}, + {Label: "TanStack Router", Value: FRONTEND_ROUTER_TANSTACK, Next: &flow.Next{Question: uiLibrary}}, + {Label: "None", Value: FRONTEND_ROUTER_NONE, Next: &flow.Next{Question: uiLibrary}}, + }, + } + + framework := &flow.OptionQuestion{ + QuestionBase: flow.QuestionBase{ID_: "framework"}, + Label: "Framework", + Options: []*flow.Option{ + // React+Vite: ask router first + {Label: "React + Vite", Value: FRONTEND_REACT_VITE, Next: &flow.Next{Question: router}}, + // Next.js: skip router (built-in) + {Label: "Next.js", Value: FRONTEND_NEXT, Next: &flow.Next{Question: uiLibrary}}, + }, + } + + projectName := &flow.TextQuestion{ + QuestionBase: flow.QuestionBase{ + ID_: "project_name", + Next_: &flow.Next{Question: framework}, + }, + Label: "Project name", + Description: "Used as the package name and root directory.", + Default: "my-app", + Validate: nonEmpty, + } + + return &FlowDef{ + ID: "frontend", + Title: "Frontend Wizard", + Description: "Scaffold a TypeScript frontend project with React+Vite or Next.js.", + Root: projectName, + Generators: resolveFrontendGenerators, + } +} + +func resolveFrontendGenerators(s *spec.ProjectSpec) []Invocation { + if s == nil { + return nil + } + + fw, _ := s.Answers["framework"].(string) + routerChoice, _ := s.Answers["frontend-router"].(string) + uiLib, _ := s.Answers["ui-library"].(string) + stylingChoice, _ := s.Answers["frontend-styling"].(string) + stateChoice, _ := s.Answers["frontend-state"].(string) + formatterChoice, _ := s.Answers["frontend-formatter"].(string) + includeVitest, _ := s.Answers["include-vitest"].(bool) + includePlaywright, _ := s.Answers["include-playwright"].(bool) + includeStorybook, _ := s.Answers["include-storybook"].(bool) + includeAuth, _ := s.Answers["include-auth"].(bool) + authProviderChoice, _ := s.Answers["auth-provider"].(string) + includeTheme, _ := s.Answers["include-theme"].(bool) + includeFeatureFlags, _ := s.Answers["include-feature-flags"].(bool) + flagsProviderChoice, _ := s.Answers["feature-flags-provider"].(string) + includeSentry, _ := s.Answers["include-sentry"].(bool) + includeAnalytics, _ := s.Answers["include-analytics"].(bool) + analyticsProviderChoice, _ := s.Answers["analytics-provider"].(string) + includeSEO, _ := s.Answers["include-seo"].(bool) + + var out []Invocation + + out = append(out, Invocation{Name: "base_project"}) + out = append(out, Invocation{Name: "typescript_base"}) + + // Framework base + switch fw { + case FRONTEND_REACT_VITE: + out = append(out, Invocation{Name: "react_app"}) + case FRONTEND_NEXT: + out = append(out, Invocation{Name: "nextjs_base"}) + } + + // Router (React+Vite only) + if fw == FRONTEND_REACT_VITE { + switch routerChoice { + case FRONTEND_ROUTER_REACT_ROUTER: + out = append(out, Invocation{Name: "react_router_v7"}) + case FRONTEND_ROUTER_TANSTACK: + out = append(out, Invocation{Name: "tanstack_router"}) + } + } + + // UI library (shadcn auto-includes Tailwind) + switch uiLib { + case FRONTEND_UI_SHADCN: + out = append(out, Invocation{Name: "shadcn_ui"}) + case FRONTEND_UI_ARKUI: + out = append(out, Invocation{Name: "ark_ui"}) + case FRONTEND_UI_VERSION14: + out = append(out, Invocation{Name: "version14_ui"}) + } + + // Styling (skip if shadcn — it handles its own CSS) + if uiLib != FRONTEND_UI_SHADCN { + switch stylingChoice { + case FRONTEND_STYLING_TAILWIND: + out = append(out, Invocation{Name: "tailwind_v4"}) + case FRONTEND_STYLING_CSS_MODULES: + out = append(out, Invocation{Name: "css_modules"}) + case FRONTEND_STYLING_PANDA: + out = append(out, Invocation{Name: "panda_css"}) + } + } + + // State management + switch stateChoice { + case FRONTEND_STATE_ZUSTAND: + out = append(out, Invocation{Name: "zustand_setup"}) + case FRONTEND_STATE_JOTAI: + out = append(out, Invocation{Name: "jotai_setup"}) + } + + // Testing + if includeVitest && fw == FRONTEND_REACT_VITE { + out = append(out, Invocation{Name: "vitest_testing_library"}) + } + if includePlaywright { + out = append(out, Invocation{Name: "playwright_setup"}) + } + if includeStorybook { + out = append(out, Invocation{Name: "storybook_setup"}) + } + + // Auth module + if includeAuth { + switch authProviderChoice { + case FRONTEND_AUTH_CLERK: + out = append(out, Invocation{Name: "auth_clerk_frontend"}) + case FRONTEND_AUTH_BETTER_AUTH: + out = append(out, Invocation{Name: "auth_better_auth_frontend"}) + case FRONTEND_AUTH_VANILLA: + out = append(out, Invocation{Name: "auth_vanilla_frontend"}) + } + } + + // Theme module + if includeTheme { + out = append(out, Invocation{Name: "theme_provider"}) + } + + // Feature flags module + if includeFeatureFlags { + switch flagsProviderChoice { + case FRONTEND_FLAGS_POSTHOG: + out = append(out, Invocation{Name: "feature_flags_posthog"}) + case FRONTEND_FLAGS_VERCEL: + out = append(out, Invocation{Name: "feature_flags_vercel"}) + case FRONTEND_FLAGS_LOCAL: + out = append(out, Invocation{Name: "feature_flags_local"}) + } + } + + // Sentry module + if includeSentry { + out = append(out, Invocation{Name: "sentry_frontend"}) + } + + // Analytics module — deduplicate PostHog with feature_flags_posthog + if includeAnalytics { + switch analyticsProviderChoice { + case FRONTEND_ANALYTICS_GA4: + out = append(out, Invocation{Name: "analytics_ga4"}) + case FRONTEND_ANALYTICS_PLAUSIBLE: + out = append(out, Invocation{Name: "analytics_plausible"}) + case FRONTEND_ANALYTICS_POSTHOG: + // Only emit feature_flags_posthog if not already in the list + if !includeFeatureFlags || flagsProviderChoice != FRONTEND_FLAGS_POSTHOG { + out = append(out, Invocation{Name: "feature_flags_posthog"}) + } + } + } + + // SEO module (framework-aware) + if includeSEO { + switch fw { + case FRONTEND_REACT_VITE: + out = append(out, Invocation{Name: "seo_react"}) + } + } + + // Formatters last (DependsOn: ["*"] in their manifests) + switch formatterChoice { + case "prettier": + out = append(out, Invocation{Name: "prettier_config"}) + out = append(out, Invocation{Name: "prettier_typescript_deps"}) + out = append(out, Invocation{Name: "prettier_frontend_rules"}) + case "biome": + out = append(out, Invocation{Name: "biome_config"}) + } + + return out +} diff --git a/flows/registry.go b/flows/registry.go index 77ecf91..4a57dc5 100644 --- a/flows/registry.go +++ b/flows/registry.go @@ -67,6 +67,7 @@ func (r *Registry) All() []*FlowDef { func Default() *Registry { r := NewRegistry() _ = r.Register(InitFlow()) + _ = r.Register(FrontendFlow()) _ = r.Register(PluginTemplateFlow()) return r } diff --git a/generators/analytics_ga4/files/.env.example b/generators/analytics_ga4/files/.env.example new file mode 100644 index 0000000..2194aad --- /dev/null +++ b/generators/analytics_ga4/files/.env.example @@ -0,0 +1,2 @@ +# Google Analytics 4 +VITE_GA_MEASUREMENT_ID=G-XXXXXXXXXX diff --git a/generators/analytics_ga4/files/src/lib/ga4.ts b/generators/analytics_ga4/files/src/lib/ga4.ts new file mode 100644 index 0000000..523764a --- /dev/null +++ b/generators/analytics_ga4/files/src/lib/ga4.ts @@ -0,0 +1,16 @@ +import ReactGA from "react-ga4"; + +const GA_MEASUREMENT_ID = import.meta.env.VITE_GA_MEASUREMENT_ID ?? ""; + +export function initGA() { + if (!GA_MEASUREMENT_ID) return; + ReactGA.initialize(GA_MEASUREMENT_ID); +} + +export function trackPageView(path: string) { + ReactGA.send({ hitType: "pageview", page: path }); +} + +export function trackEvent(action: string, category: string, label?: string) { + ReactGA.event({ action, category, label }); +} diff --git a/generators/analytics_ga4/generator.go b/generators/analytics_ga4/generator.go new file mode 100644 index 0000000..5b2e70f --- /dev/null +++ b/generators/analytics_ga4/generator.go @@ -0,0 +1,52 @@ +package analyticsga4 + +import ( + "embed" + + "github.com/version14/dot/internal/render" + "github.com/version14/dot/internal/state" + "github.com/version14/dot/pkg/dotapi" +) + +//go:embed all:files +var filesFS embed.FS + +//go:embed all:next +var nextFS embed.FS + +type Generator struct{} + +func New() *Generator { return &Generator{} } + +func (g *Generator) Name() string { return Manifest.Name } +func (g *Generator) Version() string { return Manifest.Version } + +func (g *Generator) Generate(ctx *dotapi.Context) error { + framework, _ := ctx.Answers["framework"].(string) + + if err := ctx.State.UpdateJSON("package.json", func(d *state.JSONDoc) error { + d.Merge(map[string]interface{}{ + "dependencies": map[string]interface{}{ + "react-ga4": "^3.0.1", + }, + }) + return nil + }); err != nil { + return err + } + + if err := render.NewLocalFolderRenderer(ctx.State).Render(filesFS, nil); err != nil { + return err + } + + if framework == "next" { + ga4, _ := nextFS.ReadFile("next/ga4.ts") + ctx.State.WriteFile("src/lib/ga4.ts", ga4, state.ContentRaw) + env, _ := nextFS.ReadFile("next/.env.example") + ctx.State.AppendFile(".env.example", env) + } else { + ctx.State.AppendFile(".env.example", []byte("# Google Analytics 4\nVITE_GA_MEASUREMENT_ID=G-XXXXXXXXXX\n")) + } + + return nil +} diff --git a/generators/analytics_ga4/manifest.go b/generators/analytics_ga4/manifest.go new file mode 100644 index 0000000..086be8d --- /dev/null +++ b/generators/analytics_ga4/manifest.go @@ -0,0 +1,28 @@ +package analyticsga4 + +import "github.com/version14/dot/pkg/dotapi" + +var Manifest = dotapi.Manifest{ + Name: "analytics_ga4", + Version: "0.2.0", + Description: "Google Analytics 4 (GA4) with pageview tracking", + DependsOn: []string{"typescript_base"}, + ConflictsWith: []string{"analytics_plausible"}, + Outputs: []string{ + "src/lib/ga4.ts", + }, + PostGenerationCommands: []dotapi.Command{ + {Cmd: "pnpm install --dangerously-allow-all-builds"}, + }, + TestCommands: []dotapi.Command{ + {Cmd: "pnpm exec tsc --noEmit"}, + }, + Validators: []dotapi.Validator{ + { + Name: "analytics-ga4-files", + Checks: []dotapi.Check{ + {Type: dotapi.CheckFileExists, Path: "src/lib/ga4.ts"}, + }, + }, + }, +} diff --git a/generators/analytics_ga4/next/.env.example b/generators/analytics_ga4/next/.env.example new file mode 100644 index 0000000..23fbd6e --- /dev/null +++ b/generators/analytics_ga4/next/.env.example @@ -0,0 +1,2 @@ +# Google Analytics 4 +NEXT_PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX diff --git a/generators/analytics_ga4/next/ga4.ts b/generators/analytics_ga4/next/ga4.ts new file mode 100644 index 0000000..00a2fdb --- /dev/null +++ b/generators/analytics_ga4/next/ga4.ts @@ -0,0 +1,16 @@ +import ReactGA from "react-ga4"; + +const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID ?? ""; + +export function initGA() { + if (!GA_MEASUREMENT_ID) return; + ReactGA.initialize(GA_MEASUREMENT_ID); +} + +export function trackPageView(path: string) { + ReactGA.send({ hitType: "pageview", page: path }); +} + +export function trackEvent(action: string, category: string, label?: string) { + ReactGA.event({ action, category, label }); +} diff --git a/generators/analytics_plausible/files/.env.example b/generators/analytics_plausible/files/.env.example new file mode 100644 index 0000000..1ce22c8 --- /dev/null +++ b/generators/analytics_plausible/files/.env.example @@ -0,0 +1,3 @@ +# Plausible Analytics +VITE_PLAUSIBLE_DOMAIN=yourdomain.com +VITE_PLAUSIBLE_API_HOST=https://plausible.io diff --git a/generators/analytics_plausible/files/src/lib/plausible.ts b/generators/analytics_plausible/files/src/lib/plausible.ts new file mode 100644 index 0000000..3768141 --- /dev/null +++ b/generators/analytics_plausible/files/src/lib/plausible.ts @@ -0,0 +1,19 @@ +import Plausible from "plausible-tracker"; + +const DOMAIN = + import.meta.env.VITE_PLAUSIBLE_DOMAIN ?? window.location.hostname; +const API_HOST = + import.meta.env.VITE_PLAUSIBLE_API_HOST ?? "https://plausible.io"; + +const plausible = Plausible({ domain: DOMAIN, apiHost: API_HOST }); + +export function initPlausible() { + plausible.enableAutoPageviews(); +} + +export function trackEvent( + name: string, + props?: Record, +) { + plausible.trackEvent(name, { props }); +} diff --git a/generators/analytics_plausible/generator.go b/generators/analytics_plausible/generator.go new file mode 100644 index 0000000..214359d --- /dev/null +++ b/generators/analytics_plausible/generator.go @@ -0,0 +1,52 @@ +package analyticsplausible + +import ( + "embed" + + "github.com/version14/dot/internal/render" + "github.com/version14/dot/internal/state" + "github.com/version14/dot/pkg/dotapi" +) + +//go:embed all:files +var filesFS embed.FS + +//go:embed all:next +var nextFS embed.FS + +type Generator struct{} + +func New() *Generator { return &Generator{} } + +func (g *Generator) Name() string { return Manifest.Name } +func (g *Generator) Version() string { return Manifest.Version } + +func (g *Generator) Generate(ctx *dotapi.Context) error { + framework, _ := ctx.Answers["framework"].(string) + + if err := ctx.State.UpdateJSON("package.json", func(d *state.JSONDoc) error { + d.Merge(map[string]interface{}{ + "dependencies": map[string]interface{}{ + "plausible-tracker": "^0.3.9", + }, + }) + return nil + }); err != nil { + return err + } + + if err := render.NewLocalFolderRenderer(ctx.State).Render(filesFS, nil); err != nil { + return err + } + + if framework == "next" { + plausible, _ := nextFS.ReadFile("next/plausible.ts") + ctx.State.WriteFile("src/lib/plausible.ts", plausible, state.ContentRaw) + env, _ := nextFS.ReadFile("next/.env.example") + ctx.State.AppendFile(".env.example", env) + } else { + ctx.State.AppendFile(".env.example", []byte("# Plausible Analytics\nVITE_PLAUSIBLE_DOMAIN=yourdomain.com\nVITE_PLAUSIBLE_API_HOST=https://plausible.io\n")) + } + + return nil +} diff --git a/generators/analytics_plausible/manifest.go b/generators/analytics_plausible/manifest.go new file mode 100644 index 0000000..86c3f23 --- /dev/null +++ b/generators/analytics_plausible/manifest.go @@ -0,0 +1,28 @@ +package analyticsplausible + +import "github.com/version14/dot/pkg/dotapi" + +var Manifest = dotapi.Manifest{ + Name: "analytics_plausible", + Version: "0.2.0", + Description: "Plausible Analytics privacy-first tracking", + DependsOn: []string{"typescript_base"}, + ConflictsWith: []string{"analytics_ga4"}, + Outputs: []string{ + "src/lib/plausible.ts", + }, + PostGenerationCommands: []dotapi.Command{ + {Cmd: "pnpm install --dangerously-allow-all-builds"}, + }, + TestCommands: []dotapi.Command{ + {Cmd: "pnpm exec tsc --noEmit"}, + }, + Validators: []dotapi.Validator{ + { + Name: "analytics-plausible-files", + Checks: []dotapi.Check{ + {Type: dotapi.CheckFileExists, Path: "src/lib/plausible.ts"}, + }, + }, + }, +} diff --git a/generators/analytics_plausible/next/.env.example b/generators/analytics_plausible/next/.env.example new file mode 100644 index 0000000..bd850dd --- /dev/null +++ b/generators/analytics_plausible/next/.env.example @@ -0,0 +1,3 @@ +# Plausible Analytics +NEXT_PUBLIC_PLAUSIBLE_DOMAIN=yourdomain.com +NEXT_PUBLIC_PLAUSIBLE_API_HOST=https://plausible.io diff --git a/generators/analytics_plausible/next/plausible.ts b/generators/analytics_plausible/next/plausible.ts new file mode 100644 index 0000000..2d2333a --- /dev/null +++ b/generators/analytics_plausible/next/plausible.ts @@ -0,0 +1,20 @@ +import Plausible from "plausible-tracker"; + +const DOMAIN = + process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN ?? + (typeof window !== "undefined" ? window.location.hostname : ""); +const API_HOST = + process.env.NEXT_PUBLIC_PLAUSIBLE_API_HOST ?? "https://plausible.io"; + +const plausible = Plausible({ domain: DOMAIN, apiHost: API_HOST }); + +export function initPlausible() { + plausible.enableAutoPageviews(); +} + +export function trackEvent( + name: string, + props?: Record, +) { + plausible.trackEvent(name, { props }); +} diff --git a/generators/ark_ui/files/src/components/ui/button.tsx b/generators/ark_ui/files/src/components/ui/button.tsx new file mode 100644 index 0000000..e9514f5 --- /dev/null +++ b/generators/ark_ui/files/src/components/ui/button.tsx @@ -0,0 +1,3 @@ +import { ark } from "@ark-ui/react"; + +export const Button = ark.button; diff --git a/generators/ark_ui/files/src/components/ui/index.ts b/generators/ark_ui/files/src/components/ui/index.ts new file mode 100644 index 0000000..98d55ac --- /dev/null +++ b/generators/ark_ui/files/src/components/ui/index.ts @@ -0,0 +1 @@ +export * from "./button"; diff --git a/generators/ark_ui/generator.go b/generators/ark_ui/generator.go new file mode 100644 index 0000000..90747d9 --- /dev/null +++ b/generators/ark_ui/generator.go @@ -0,0 +1,34 @@ +package arkui + +import ( + "embed" + + "github.com/version14/dot/internal/render" + "github.com/version14/dot/internal/state" + "github.com/version14/dot/pkg/dotapi" +) + +//go:embed all:files +var fs embed.FS + +type Generator struct{} + +func New() *Generator { return &Generator{} } + +func (g *Generator) Name() string { return Manifest.Name } +func (g *Generator) Version() string { return Manifest.Version } + +func (g *Generator) Generate(ctx *dotapi.Context) error { + if err := ctx.State.UpdateJSON("package.json", func(d *state.JSONDoc) error { + d.Merge(map[string]interface{}{ + "dependencies": map[string]interface{}{ + "@ark-ui/react": "^5.37.0", + }, + }) + return nil + }); err != nil { + return err + } + + return render.NewLocalFolderRenderer(ctx.State).Render(fs, nil) +} diff --git a/generators/ark_ui/manifest.go b/generators/ark_ui/manifest.go new file mode 100644 index 0000000..7bc369e --- /dev/null +++ b/generators/ark_ui/manifest.go @@ -0,0 +1,30 @@ +package arkui + +import "github.com/version14/dot/pkg/dotapi" + +var Manifest = dotapi.Manifest{ + Name: "ark_ui", + Version: "0.2.0", + Description: "Ark UI headless component library", + DependsOn: []string{"typescript_base"}, + ConflictsWith: []string{"shadcn_ui"}, + Outputs: []string{ + "src/components/ui/button.tsx", + "src/components/ui/index.ts", + }, + PostGenerationCommands: []dotapi.Command{ + {Cmd: "pnpm install --dangerously-allow-all-builds"}, + }, + TestCommands: []dotapi.Command{ + {Cmd: "pnpm exec tsc --noEmit"}, + }, + Validators: []dotapi.Validator{ + { + Name: "ark-ui-files", + Checks: []dotapi.Check{ + {Type: dotapi.CheckFileExists, Path: "src/components/ui/button.tsx"}, + {Type: dotapi.CheckJSONKeyExists, Path: "package.json", Key: "dependencies.@ark-ui/react"}, + }, + }, + }, +} diff --git a/generators/auth_better_auth_frontend/files/src/hooks/useAuth.ts b/generators/auth_better_auth_frontend/files/src/hooks/useAuth.ts new file mode 100644 index 0000000..6278290 --- /dev/null +++ b/generators/auth_better_auth_frontend/files/src/hooks/useAuth.ts @@ -0,0 +1,2 @@ +export { useSession } from "../lib/authClient"; +export { useAuthContext } from "../providers/AuthProvider"; diff --git a/generators/auth_better_auth_frontend/files/src/lib/authClient.ts.tmpl b/generators/auth_better_auth_frontend/files/src/lib/authClient.ts.tmpl new file mode 100644 index 0000000..2496626 --- /dev/null +++ b/generators/auth_better_auth_frontend/files/src/lib/authClient.ts.tmpl @@ -0,0 +1,7 @@ +import { createAuthClient } from "better-auth/react"; + +export const authClient = createAuthClient({ + baseURL: {{if eq .Framework "next"}}process.env.NEXT_PUBLIC_API_URL{{else}}import.meta.env.VITE_API_URL{{end}} ?? "http://localhost:3000", +}); + +export const { signIn, signOut, signUp, useSession } = authClient; diff --git a/generators/auth_better_auth_frontend/files/src/providers/AuthProvider.tsx b/generators/auth_better_auth_frontend/files/src/providers/AuthProvider.tsx new file mode 100644 index 0000000..8b0794f --- /dev/null +++ b/generators/auth_better_auth_frontend/files/src/providers/AuthProvider.tsx @@ -0,0 +1,21 @@ +import React, { createContext, useContext } from "react"; +import { useSession } from "../lib/authClient"; + +interface AuthContextValue { + session: ReturnType["data"]; +} + +const AuthContext = createContext(null); + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const { data: session } = useSession(); + return ( + {children} + ); +} + +export function useAuthContext() { + const ctx = useContext(AuthContext); + if (!ctx) throw new Error("useAuthContext must be used within AuthProvider"); + return ctx; +} diff --git a/generators/auth_better_auth_frontend/generator.go b/generators/auth_better_auth_frontend/generator.go new file mode 100644 index 0000000..820fa7e --- /dev/null +++ b/generators/auth_better_auth_frontend/generator.go @@ -0,0 +1,40 @@ +package authbetterauthfrontend + +import ( + "embed" + + "github.com/version14/dot/internal/render" + "github.com/version14/dot/internal/state" + "github.com/version14/dot/pkg/dotapi" +) + +//go:embed all:files +var fs embed.FS + +type authBAData struct { + Framework string +} + +type Generator struct{} + +func New() *Generator { return &Generator{} } + +func (g *Generator) Name() string { return Manifest.Name } +func (g *Generator) Version() string { return Manifest.Version } + +func (g *Generator) Generate(ctx *dotapi.Context) error { + framework, _ := ctx.Answers["framework"].(string) + + if err := ctx.State.UpdateJSON("package.json", func(d *state.JSONDoc) error { + d.Merge(map[string]interface{}{ + "dependencies": map[string]interface{}{ + "better-auth": "^1.6.11", + }, + }) + return nil + }); err != nil { + return err + } + + return render.NewLocalFolderRenderer(ctx.State).Render(fs, authBAData{Framework: framework}) +} diff --git a/generators/auth_better_auth_frontend/manifest.go b/generators/auth_better_auth_frontend/manifest.go new file mode 100644 index 0000000..2b50508 --- /dev/null +++ b/generators/auth_better_auth_frontend/manifest.go @@ -0,0 +1,31 @@ +package authbetterauthfrontend + +import "github.com/version14/dot/pkg/dotapi" + +var Manifest = dotapi.Manifest{ + Name: "auth_better_auth_frontend", + Version: "0.2.0", + Description: "Better Auth client setup with AuthProvider and useAuth hook", + DependsOn: []string{"typescript_base"}, + ConflictsWith: []string{"auth_clerk_frontend", "auth_vanilla_frontend"}, + Outputs: []string{ + "src/lib/authClient.ts", + "src/providers/AuthProvider.tsx", + "src/hooks/useAuth.ts", + }, + PostGenerationCommands: []dotapi.Command{ + {Cmd: "pnpm install --dangerously-allow-all-builds"}, + }, + TestCommands: []dotapi.Command{ + {Cmd: "pnpm exec tsc --noEmit"}, + }, + Validators: []dotapi.Validator{ + { + Name: "auth-better-auth-frontend-files", + Checks: []dotapi.Check{ + {Type: dotapi.CheckFileExists, Path: "src/lib/authClient.ts"}, + {Type: dotapi.CheckJSONKeyExists, Path: "package.json", Key: "dependencies.better-auth"}, + }, + }, + }, +} diff --git a/generators/auth_clerk_frontend/files/.env.example b/generators/auth_clerk_frontend/files/.env.example new file mode 100644 index 0000000..4717b79 --- /dev/null +++ b/generators/auth_clerk_frontend/files/.env.example @@ -0,0 +1,2 @@ +# Clerk +VITE_CLERK_PUBLISHABLE_KEY=pk_test_your_publishable_key_here diff --git a/generators/auth_clerk_frontend/files/ClerkProvider.tsx b/generators/auth_clerk_frontend/files/ClerkProvider.tsx new file mode 100644 index 0000000..77dfb5f --- /dev/null +++ b/generators/auth_clerk_frontend/files/ClerkProvider.tsx @@ -0,0 +1,12 @@ +import { ClerkProvider as BaseClerkProvider } from "@clerk/clerk-react"; + +const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY; +if (!PUBLISHABLE_KEY) throw new Error("Missing VITE_CLERK_PUBLISHABLE_KEY"); + +export function ClerkProvider({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/generators/auth_clerk_frontend/files/useAuth.ts b/generators/auth_clerk_frontend/files/useAuth.ts new file mode 100644 index 0000000..f8075b9 --- /dev/null +++ b/generators/auth_clerk_frontend/files/useAuth.ts @@ -0,0 +1,8 @@ +export { + useAuth, + useUser, + useClerk, + SignedIn, + SignedOut, + SignInButton, +} from "@clerk/clerk-react"; diff --git a/generators/auth_clerk_frontend/generator.go b/generators/auth_clerk_frontend/generator.go new file mode 100644 index 0000000..477ab47 --- /dev/null +++ b/generators/auth_clerk_frontend/generator.go @@ -0,0 +1,64 @@ +package authclerkfrontend + +import ( + "embed" + + "github.com/version14/dot/internal/state" + "github.com/version14/dot/pkg/dotapi" +) + +//go:embed all:files +var filesFS embed.FS + +//go:embed all:next +var nextFS embed.FS + +type Generator struct{} + +func New() *Generator { return &Generator{} } + +func (g *Generator) Name() string { return Manifest.Name } +func (g *Generator) Version() string { return Manifest.Version } + +func mustRead(fs embed.FS, path string) []byte { + data, err := fs.ReadFile(path) + if err != nil { + panic(err) + } + return data +} + +func (g *Generator) Generate(ctx *dotapi.Context) error { + framework, _ := ctx.Answers["framework"].(string) + + pkg := "@clerk/clerk-react" + version := "^5.61.3" + src := filesFS + if framework == "next" { + pkg = "@clerk/nextjs" + version = "^7.4.2" + src = nextFS + } + + dir := "files" + if framework == "next" { + dir = "next" + } + + if err := ctx.State.UpdateJSON("package.json", func(d *state.JSONDoc) error { + d.Merge(map[string]interface{}{ + "dependencies": map[string]interface{}{ + pkg: version, + }, + }) + return nil + }); err != nil { + return err + } + + ctx.State.WriteFile("src/providers/ClerkProvider.tsx", mustRead(src, dir+"/ClerkProvider.tsx"), state.ContentRaw) + ctx.State.WriteFile("src/hooks/useAuth.ts", mustRead(src, dir+"/useAuth.ts"), state.ContentRaw) + ctx.State.AppendFile(".env.example", mustRead(src, dir+"/.env.example")) + + return nil +} diff --git a/generators/auth_clerk_frontend/manifest.go b/generators/auth_clerk_frontend/manifest.go new file mode 100644 index 0000000..bbbed37 --- /dev/null +++ b/generators/auth_clerk_frontend/manifest.go @@ -0,0 +1,29 @@ +package authclerkfrontend + +import "github.com/version14/dot/pkg/dotapi" + +var Manifest = dotapi.Manifest{ + Name: "auth_clerk_frontend", + Version: "0.2.0", + Description: "Clerk authentication provider and useAuth hook", + DependsOn: []string{"typescript_base"}, + ConflictsWith: []string{"auth_better_auth_frontend", "auth_vanilla_frontend"}, + Outputs: []string{ + "src/providers/ClerkProvider.tsx", + "src/hooks/useAuth.ts", + }, + PostGenerationCommands: []dotapi.Command{ + {Cmd: "pnpm install --dangerously-allow-all-builds"}, + }, + TestCommands: []dotapi.Command{ + {Cmd: "pnpm exec tsc --noEmit"}, + }, + Validators: []dotapi.Validator{ + { + Name: "auth-clerk-frontend-files", + Checks: []dotapi.Check{ + {Type: dotapi.CheckFileExists, Path: "src/providers/ClerkProvider.tsx"}, + }, + }, + }, +} diff --git a/generators/auth_clerk_frontend/next/.env.example b/generators/auth_clerk_frontend/next/.env.example new file mode 100644 index 0000000..cba342d --- /dev/null +++ b/generators/auth_clerk_frontend/next/.env.example @@ -0,0 +1,2 @@ +# Clerk +NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your_publishable_key_here diff --git a/generators/auth_clerk_frontend/next/ClerkProvider.tsx b/generators/auth_clerk_frontend/next/ClerkProvider.tsx new file mode 100644 index 0000000..cdabfce --- /dev/null +++ b/generators/auth_clerk_frontend/next/ClerkProvider.tsx @@ -0,0 +1,3 @@ +// For Next.js: wrap your root layout with ClerkProvider from @clerk/nextjs +// See: https://clerk.com/docs/quickstarts/nextjs +export { ClerkProvider } from "@clerk/nextjs"; diff --git a/generators/auth_clerk_frontend/next/useAuth.ts b/generators/auth_clerk_frontend/next/useAuth.ts new file mode 100644 index 0000000..a5505ec --- /dev/null +++ b/generators/auth_clerk_frontend/next/useAuth.ts @@ -0,0 +1 @@ +export { useAuth, useUser, useClerk } from "@clerk/nextjs"; diff --git a/generators/auth_vanilla_frontend/files/src/hooks/useAuth.ts b/generators/auth_vanilla_frontend/files/src/hooks/useAuth.ts new file mode 100644 index 0000000..48d7cc1 --- /dev/null +++ b/generators/auth_vanilla_frontend/files/src/hooks/useAuth.ts @@ -0,0 +1 @@ +export { useAuthContext as useAuth } from "../providers/AuthProvider"; diff --git a/generators/auth_vanilla_frontend/files/src/lib/auth.ts b/generators/auth_vanilla_frontend/files/src/lib/auth.ts new file mode 100644 index 0000000..0ddeb5e --- /dev/null +++ b/generators/auth_vanilla_frontend/files/src/lib/auth.ts @@ -0,0 +1,27 @@ +const API_URL = import.meta.env.VITE_API_URL ?? "http://localhost:3000"; + +let accessToken: string | null = null; + +export async function login(email: string, password: string): Promise { + const res = await fetch(`${API_URL}/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }), + }); + if (!res.ok) throw new Error("Login failed"); + const data = await res.json(); + accessToken = data.accessToken; +} + +export async function logout(): Promise { + await fetch(`${API_URL}/auth/logout`, { method: "POST" }); + accessToken = null; +} + +export function getAccessToken(): string | null { + return accessToken; +} + +export function isAuthenticated(): boolean { + return accessToken !== null; +} diff --git a/generators/auth_vanilla_frontend/files/src/providers/AuthProvider.tsx b/generators/auth_vanilla_frontend/files/src/providers/AuthProvider.tsx new file mode 100644 index 0000000..e85439b --- /dev/null +++ b/generators/auth_vanilla_frontend/files/src/providers/AuthProvider.tsx @@ -0,0 +1,36 @@ +import React, { createContext, useContext, useState, useCallback } from "react"; +import { login as apiLogin, logout as apiLogout, isAuthenticated } from "../lib/auth"; + +interface AuthContextValue { + isAuth: boolean; + login: (email: string, password: string) => Promise; + logout: () => Promise; +} + +const AuthContext = createContext(null); + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [isAuth, setIsAuth] = useState(isAuthenticated()); + + const login = useCallback(async (email: string, password: string) => { + await apiLogin(email, password); + setIsAuth(true); + }, []); + + const logout = useCallback(async () => { + await apiLogout(); + setIsAuth(false); + }, []); + + return ( + + {children} + + ); +} + +export function useAuthContext() { + const ctx = useContext(AuthContext); + if (!ctx) throw new Error("useAuthContext must be used within AuthProvider"); + return ctx; +} diff --git a/generators/auth_vanilla_frontend/generator.go b/generators/auth_vanilla_frontend/generator.go new file mode 100644 index 0000000..adde9dd --- /dev/null +++ b/generators/auth_vanilla_frontend/generator.go @@ -0,0 +1,22 @@ +package authvanillafrontend + +import ( + "embed" + + "github.com/version14/dot/internal/render" + "github.com/version14/dot/pkg/dotapi" +) + +//go:embed all:files +var fs embed.FS + +type Generator struct{} + +func New() *Generator { return &Generator{} } + +func (g *Generator) Name() string { return Manifest.Name } +func (g *Generator) Version() string { return Manifest.Version } + +func (g *Generator) Generate(ctx *dotapi.Context) error { + return render.NewLocalFolderRenderer(ctx.State).Render(fs, nil) +} diff --git a/generators/auth_vanilla_frontend/manifest.go b/generators/auth_vanilla_frontend/manifest.go new file mode 100644 index 0000000..236dddd --- /dev/null +++ b/generators/auth_vanilla_frontend/manifest.go @@ -0,0 +1,27 @@ +package authvanillafrontend + +import "github.com/version14/dot/pkg/dotapi" + +var Manifest = dotapi.Manifest{ + Name: "auth_vanilla_frontend", + Version: "0.1.0", + Description: "Custom JWT authentication context and useAuth hook (from scratch)", + DependsOn: []string{"typescript_base"}, + ConflictsWith: []string{"auth_clerk_frontend", "auth_better_auth_frontend"}, + Outputs: []string{ + "src/lib/auth.ts", + "src/providers/AuthProvider.tsx", + "src/hooks/useAuth.ts", + }, + TestCommands: []dotapi.Command{ + {Cmd: "pnpm exec tsc --noEmit"}, + }, + Validators: []dotapi.Validator{ + { + Name: "auth-vanilla-frontend-files", + Checks: []dotapi.Check{ + {Type: dotapi.CheckFileExists, Path: "src/lib/auth.ts"}, + }, + }, + }, +} diff --git a/generators/biome_config/generator.go b/generators/biome_config/generator.go index 067deb9..94935f4 100644 --- a/generators/biome_config/generator.go +++ b/generators/biome_config/generator.go @@ -25,11 +25,45 @@ func (g *Generator) Generate(ctx *dotapi.Context) error { "indentStyle": "space", "indentWidth": 2, }, - "files": map[string]interface{}{ - "experimentalScannerIgnores": []interface{}{".dot/"}, - }, + "files": map[string]interface{}{}, }) - return nil + + // Biome 2.x: files.ignore and experimentalScannerIgnores were removed. + // Use files.includes negation patterns. Per the "Valid Folder Ignore + // Pattern" docs, directory exclusions must use the **/ form — a + // bare name like !dist does not match the directory's contents. + // !! is a hard-exclude (skip scanner) for the dot-owned meta dir. + // "**" MUST be position 0; collect any entries added by earlier + // generators (e.g. panda_css's !**/styled-system) and append them after. + standard := []string{ + "**", + "!!**/.dot", + "!**/.next", + "!**/coverage", + "!**/dist", + "!**/playwright-report", + "!**/storybook-static", + "!src/routeTree.gen.ts", + } + seen := make(map[string]struct{}, len(standard)) + final := make([]interface{}, 0, len(standard)) + for _, s := range standard { + seen[s] = struct{}{} + final = append(final, s) + } + if raw, ok := d.GetNested("files.includes"); ok { + if arr, ok := raw.([]interface{}); ok { + for _, v := range arr { + if s, ok := v.(string); ok { + if _, dup := seen[s]; !dup { + seen[s] = struct{}{} + final = append(final, s) + } + } + } + } + } + return d.SetNested("files.includes", final) }); err != nil { return err } diff --git a/generators/biome_config/manifest.go b/generators/biome_config/manifest.go index d4041d5..e0c4570 100644 --- a/generators/biome_config/manifest.go +++ b/generators/biome_config/manifest.go @@ -8,7 +8,7 @@ const BIOME_FILE = "biome.json" // Depends on typescript_base since it modifies package.json scripts. var Manifest = dotapi.Manifest{ Name: "biome_config", - Version: "0.2.2", + Version: "0.2.4", Description: "Biome lint + format configuration", DependsOn: []string{"typescript_base", "*"}, Outputs: []string{ diff --git a/generators/css_modules/files/src/styles/App.module.css b/generators/css_modules/files/src/styles/App.module.css new file mode 100644 index 0000000..4970840 --- /dev/null +++ b/generators/css_modules/files/src/styles/App.module.css @@ -0,0 +1,10 @@ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; +} + +.title { + font-size: 2rem; + font-weight: bold; +} diff --git a/generators/css_modules/files/src/styles/global.css b/generators/css_modules/files/src/styles/global.css new file mode 100644 index 0000000..3974971 --- /dev/null +++ b/generators/css_modules/files/src/styles/global.css @@ -0,0 +1,10 @@ +*, +*::before, +*::after { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: system-ui, sans-serif; +} diff --git a/generators/css_modules/generator.go b/generators/css_modules/generator.go new file mode 100644 index 0000000..44a1804 --- /dev/null +++ b/generators/css_modules/generator.go @@ -0,0 +1,22 @@ +package cssmodules + +import ( + "embed" + + "github.com/version14/dot/internal/render" + "github.com/version14/dot/pkg/dotapi" +) + +//go:embed all:files +var fs embed.FS + +type Generator struct{} + +func New() *Generator { return &Generator{} } + +func (g *Generator) Name() string { return Manifest.Name } +func (g *Generator) Version() string { return Manifest.Version } + +func (g *Generator) Generate(ctx *dotapi.Context) error { + return render.NewLocalFolderRenderer(ctx.State).Render(fs, nil) +} diff --git a/generators/css_modules/manifest.go b/generators/css_modules/manifest.go new file mode 100644 index 0000000..90676c3 --- /dev/null +++ b/generators/css_modules/manifest.go @@ -0,0 +1,26 @@ +package cssmodules + +import "github.com/version14/dot/pkg/dotapi" + +var Manifest = dotapi.Manifest{ + Name: "css_modules", + Version: "0.1.0", + Description: "CSS Modules setup with global styles and example component stylesheet", + DependsOn: []string{"typescript_base"}, + ConflictsWith: []string{"tailwind_v4", "panda_css", "shadcn_ui"}, + Outputs: []string{ + "src/styles/global.css", + "src/styles/App.module.css", + }, + TestCommands: []dotapi.Command{ + {Cmd: "pnpm exec tsc --noEmit"}, + }, + Validators: []dotapi.Validator{ + { + Name: "css-modules-files", + Checks: []dotapi.Check{ + {Type: dotapi.CheckFileExists, Path: "src/styles/global.css"}, + }, + }, + }, +} diff --git a/generators/feature_flags_local/files/public/flags.json b/generators/feature_flags_local/files/public/flags.json new file mode 100644 index 0000000..f93e9ea --- /dev/null +++ b/generators/feature_flags_local/files/public/flags.json @@ -0,0 +1,4 @@ +{ + "new-dashboard": false, + "beta-features": false +} diff --git a/generators/feature_flags_local/files/src/hooks/useFeatureFlag.ts b/generators/feature_flags_local/files/src/hooks/useFeatureFlag.ts new file mode 100644 index 0000000..7608f9c --- /dev/null +++ b/generators/feature_flags_local/files/src/hooks/useFeatureFlag.ts @@ -0,0 +1,10 @@ +import { useState, useEffect } from "react"; +import { getFlag } from "../lib/flags"; + +export function useFeatureFlag(flag: string): boolean { + const [enabled, setEnabled] = useState(false); + useEffect(() => { + getFlag(flag).then(setEnabled); + }, [flag]); + return enabled; +} diff --git a/generators/feature_flags_local/files/src/lib/flags.ts b/generators/feature_flags_local/files/src/lib/flags.ts new file mode 100644 index 0000000..3f35710 --- /dev/null +++ b/generators/feature_flags_local/files/src/lib/flags.ts @@ -0,0 +1,14 @@ +let flagsCache: Record | null = null; + +async function loadFlags(): Promise> { + if (flagsCache) return flagsCache; + const res = await fetch("/flags.json"); + flagsCache = await res.json(); + return flagsCache ?? {}; + +} + +export async function getFlag(flag: string): Promise { + const flags = await loadFlags(); + return flags[flag] ?? false; +} diff --git a/generators/feature_flags_local/generator.go b/generators/feature_flags_local/generator.go new file mode 100644 index 0000000..7a752b4 --- /dev/null +++ b/generators/feature_flags_local/generator.go @@ -0,0 +1,22 @@ +package featureflagslocal + +import ( + "embed" + + "github.com/version14/dot/internal/render" + "github.com/version14/dot/pkg/dotapi" +) + +//go:embed all:files +var fs embed.FS + +type Generator struct{} + +func New() *Generator { return &Generator{} } + +func (g *Generator) Name() string { return Manifest.Name } +func (g *Generator) Version() string { return Manifest.Version } + +func (g *Generator) Generate(ctx *dotapi.Context) error { + return render.NewLocalFolderRenderer(ctx.State).Render(fs, nil) +} diff --git a/generators/feature_flags_local/manifest.go b/generators/feature_flags_local/manifest.go new file mode 100644 index 0000000..2b1a22a --- /dev/null +++ b/generators/feature_flags_local/manifest.go @@ -0,0 +1,27 @@ +package featureflagslocal + +import "github.com/version14/dot/pkg/dotapi" + +var Manifest = dotapi.Manifest{ + Name: "feature_flags_local", + Version: "0.1.0", + Description: "Local JSON file-based feature flags with typed hook", + DependsOn: []string{"typescript_base"}, + Outputs: []string{ + "public/flags.json", + "src/lib/flags.ts", + "src/hooks/useFeatureFlag.ts", + }, + TestCommands: []dotapi.Command{ + {Cmd: "pnpm exec tsc --noEmit"}, + }, + Validators: []dotapi.Validator{ + { + Name: "feature-flags-local-files", + Checks: []dotapi.Check{ + {Type: dotapi.CheckFileExists, Path: "public/flags.json"}, + {Type: dotapi.CheckFileExists, Path: "src/lib/flags.ts"}, + }, + }, + }, +} diff --git a/generators/feature_flags_posthog/files/.env.example b/generators/feature_flags_posthog/files/.env.example new file mode 100644 index 0000000..c78237b --- /dev/null +++ b/generators/feature_flags_posthog/files/.env.example @@ -0,0 +1,3 @@ +# PostHog +VITE_POSTHOG_KEY=phc_your_project_api_key +VITE_POSTHOG_HOST=https://app.posthog.com diff --git a/generators/feature_flags_posthog/files/src/lib/posthog.ts b/generators/feature_flags_posthog/files/src/lib/posthog.ts new file mode 100644 index 0000000..c57dc9e --- /dev/null +++ b/generators/feature_flags_posthog/files/src/lib/posthog.ts @@ -0,0 +1,12 @@ +import posthog from "posthog-js"; + +export function initPostHog() { + posthog.init(import.meta.env.VITE_POSTHOG_KEY ?? "", { + api_host: import.meta.env.VITE_POSTHOG_HOST ?? "https://app.posthog.com", + loaded: (ph) => { + if (import.meta.env.DEV) ph.opt_out_capturing(); + }, + }); +} + +export { posthog }; diff --git a/generators/feature_flags_posthog/files/src/providers/PostHogProvider.tsx b/generators/feature_flags_posthog/files/src/providers/PostHogProvider.tsx new file mode 100644 index 0000000..664d59d --- /dev/null +++ b/generators/feature_flags_posthog/files/src/providers/PostHogProvider.tsx @@ -0,0 +1,9 @@ +import React, { useEffect } from "react"; +import { initPostHog } from "../lib/posthog"; + +export function PostHogProvider({ children }: { children: React.ReactNode }) { + useEffect(() => { + initPostHog(); + }, []); + return <>{children}; +} diff --git a/generators/feature_flags_posthog/generator.go b/generators/feature_flags_posthog/generator.go new file mode 100644 index 0000000..5d43264 --- /dev/null +++ b/generators/feature_flags_posthog/generator.go @@ -0,0 +1,52 @@ +package featureflagsposthog + +import ( + "embed" + + "github.com/version14/dot/internal/render" + "github.com/version14/dot/internal/state" + "github.com/version14/dot/pkg/dotapi" +) + +//go:embed all:files +var filesFS embed.FS + +//go:embed all:next +var nextFS embed.FS + +type Generator struct{} + +func New() *Generator { return &Generator{} } + +func (g *Generator) Name() string { return Manifest.Name } +func (g *Generator) Version() string { return Manifest.Version } + +func (g *Generator) Generate(ctx *dotapi.Context) error { + framework, _ := ctx.Answers["framework"].(string) + + if err := ctx.State.UpdateJSON("package.json", func(d *state.JSONDoc) error { + d.Merge(map[string]interface{}{ + "dependencies": map[string]interface{}{ + "posthog-js": "^1.376.4", + }, + }) + return nil + }); err != nil { + return err + } + + if err := render.NewLocalFolderRenderer(ctx.State).Render(filesFS, nil); err != nil { + return err + } + + if framework == "next" { + posthog, _ := nextFS.ReadFile("next/posthog.ts") + ctx.State.WriteFile("src/lib/posthog.ts", posthog, state.ContentRaw) + env, _ := nextFS.ReadFile("next/.env.example") + ctx.State.AppendFile(".env.example", env) + } else { + ctx.State.AppendFile(".env.example", []byte("# PostHog\nVITE_POSTHOG_KEY=phc_your_project_api_key\nVITE_POSTHOG_HOST=https://app.posthog.com\n")) + } + + return nil +} diff --git a/generators/feature_flags_posthog/manifest.go b/generators/feature_flags_posthog/manifest.go new file mode 100644 index 0000000..542d09d --- /dev/null +++ b/generators/feature_flags_posthog/manifest.go @@ -0,0 +1,29 @@ +package featureflagsposthog + +import "github.com/version14/dot/pkg/dotapi" + +var Manifest = dotapi.Manifest{ + Name: "feature_flags_posthog", + Version: "0.2.0", + Description: "PostHog feature flags and analytics provider", + DependsOn: []string{"typescript_base"}, + Outputs: []string{ + "src/lib/posthog.ts", + "src/providers/PostHogProvider.tsx", + }, + PostGenerationCommands: []dotapi.Command{ + {Cmd: "pnpm install --dangerously-allow-all-builds"}, + }, + TestCommands: []dotapi.Command{ + {Cmd: "pnpm exec tsc --noEmit"}, + }, + Validators: []dotapi.Validator{ + { + Name: "feature-flags-posthog-files", + Checks: []dotapi.Check{ + {Type: dotapi.CheckFileExists, Path: "src/lib/posthog.ts"}, + {Type: dotapi.CheckJSONKeyExists, Path: "package.json", Key: "dependencies.posthog-js"}, + }, + }, + }, +} diff --git a/generators/feature_flags_posthog/next/.env.example b/generators/feature_flags_posthog/next/.env.example new file mode 100644 index 0000000..9a4ba5a --- /dev/null +++ b/generators/feature_flags_posthog/next/.env.example @@ -0,0 +1,3 @@ +# PostHog +NEXT_PUBLIC_POSTHOG_KEY=phc_your_project_api_key +NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com diff --git a/generators/feature_flags_posthog/next/posthog.ts b/generators/feature_flags_posthog/next/posthog.ts new file mode 100644 index 0000000..bd5804b --- /dev/null +++ b/generators/feature_flags_posthog/next/posthog.ts @@ -0,0 +1,12 @@ +import posthog from "posthog-js"; + +export function initPostHog() { + posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY ?? "", { + api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST ?? "https://app.posthog.com", + loaded: (ph) => { + if (process.env.NODE_ENV === "development") ph.opt_out_capturing(); + }, + }); +} + +export { posthog }; diff --git a/generators/feature_flags_vercel/files/.env.example b/generators/feature_flags_vercel/files/.env.example new file mode 100644 index 0000000..6599016 --- /dev/null +++ b/generators/feature_flags_vercel/files/.env.example @@ -0,0 +1,2 @@ +# Vercel Edge Config +EDGE_CONFIG=your_edge_config_connection_string diff --git a/generators/feature_flags_vercel/files/src/hooks/useFeatureFlag.ts b/generators/feature_flags_vercel/files/src/hooks/useFeatureFlag.ts new file mode 100644 index 0000000..4f7d849 --- /dev/null +++ b/generators/feature_flags_vercel/files/src/hooks/useFeatureFlag.ts @@ -0,0 +1,10 @@ +import { useState, useEffect } from "react"; +import { getFlag, type FeatureFlag } from "../lib/flags"; + +export function useFeatureFlag(flag: FeatureFlag): boolean { + const [enabled, setEnabled] = useState(false); + useEffect(() => { + getFlag(flag).then(setEnabled); + }, [flag]); + return enabled; +} diff --git a/generators/feature_flags_vercel/files/src/lib/flags.ts b/generators/feature_flags_vercel/files/src/lib/flags.ts new file mode 100644 index 0000000..a119e83 --- /dev/null +++ b/generators/feature_flags_vercel/files/src/lib/flags.ts @@ -0,0 +1,13 @@ +// Vercel Edge Config feature flags +// Configure flags in your Vercel dashboard or edge-config.json + +export type FeatureFlag = "new-dashboard" | "beta-features"; + +export async function getFlag(flag: FeatureFlag): Promise { + try { + const { get } = await import("@vercel/edge-config"); + return (await get(flag)) ?? false; + } catch { + return false; + } +} diff --git a/generators/feature_flags_vercel/generator.go b/generators/feature_flags_vercel/generator.go new file mode 100644 index 0000000..61a0ca1 --- /dev/null +++ b/generators/feature_flags_vercel/generator.go @@ -0,0 +1,39 @@ +package featureflagsvercel + +import ( + "embed" + + "github.com/version14/dot/internal/render" + "github.com/version14/dot/internal/state" + "github.com/version14/dot/pkg/dotapi" +) + +//go:embed all:files +var fs embed.FS + +type Generator struct{} + +func New() *Generator { return &Generator{} } + +func (g *Generator) Name() string { return Manifest.Name } +func (g *Generator) Version() string { return Manifest.Version } + +func (g *Generator) Generate(ctx *dotapi.Context) error { + if err := ctx.State.UpdateJSON("package.json", func(d *state.JSONDoc) error { + d.Merge(map[string]interface{}{ + "dependencies": map[string]interface{}{ + "@vercel/edge-config": "^1.4.3", + "@vercel/flags": "^3.1.1", + }, + }) + return nil + }); err != nil { + return err + } + + if err := render.NewLocalFolderRenderer(ctx.State).Render(fs, nil); err != nil { + return err + } + ctx.State.AppendFile(".env.example", []byte("# Vercel Edge Config\nEDGE_CONFIG=your_edge_config_connection_string\n")) + return nil +} diff --git a/generators/feature_flags_vercel/manifest.go b/generators/feature_flags_vercel/manifest.go new file mode 100644 index 0000000..1307234 --- /dev/null +++ b/generators/feature_flags_vercel/manifest.go @@ -0,0 +1,29 @@ +package featureflagsvercel + +import "github.com/version14/dot/pkg/dotapi" + +var Manifest = dotapi.Manifest{ + Name: "feature_flags_vercel", + Version: "0.2.0", + Description: "Vercel Edge Config feature flags with typed hook", + DependsOn: []string{"typescript_base"}, + Outputs: []string{ + "src/lib/flags.ts", + "src/hooks/useFeatureFlag.ts", + }, + PostGenerationCommands: []dotapi.Command{ + {Cmd: "pnpm install --dangerously-allow-all-builds"}, + }, + TestCommands: []dotapi.Command{ + {Cmd: "pnpm exec tsc --noEmit"}, + }, + Validators: []dotapi.Validator{ + { + Name: "feature-flags-vercel-files", + Checks: []dotapi.Check{ + {Type: dotapi.CheckFileExists, Path: "src/lib/flags.ts"}, + {Type: dotapi.CheckJSONKeyExists, Path: "package.json", Key: "dependencies.@vercel/edge-config"}, + }, + }, + }, +} diff --git a/generators/jotai_setup/files/src/atoms/counter.atom.ts b/generators/jotai_setup/files/src/atoms/counter.atom.ts new file mode 100644 index 0000000..d0ac5bf --- /dev/null +++ b/generators/jotai_setup/files/src/atoms/counter.atom.ts @@ -0,0 +1,11 @@ +import { atom } from "jotai"; + +export const countAtom = atom(0); + +export const incrementAtom = atom(null, (get, set) => + set(countAtom, get(countAtom) + 1), +); + +export const decrementAtom = atom(null, (get, set) => + set(countAtom, get(countAtom) - 1), +); diff --git a/generators/jotai_setup/generator.go b/generators/jotai_setup/generator.go new file mode 100644 index 0000000..51c3a69 --- /dev/null +++ b/generators/jotai_setup/generator.go @@ -0,0 +1,34 @@ +package jotaisetup + +import ( + "embed" + + "github.com/version14/dot/internal/render" + "github.com/version14/dot/internal/state" + "github.com/version14/dot/pkg/dotapi" +) + +//go:embed all:files +var fs embed.FS + +type Generator struct{} + +func New() *Generator { return &Generator{} } + +func (g *Generator) Name() string { return Manifest.Name } +func (g *Generator) Version() string { return Manifest.Version } + +func (g *Generator) Generate(ctx *dotapi.Context) error { + if err := ctx.State.UpdateJSON("package.json", func(d *state.JSONDoc) error { + d.Merge(map[string]interface{}{ + "dependencies": map[string]interface{}{ + "jotai": "^2.20.0", + }, + }) + return nil + }); err != nil { + return err + } + + return render.NewLocalFolderRenderer(ctx.State).Render(fs, nil) +} diff --git a/generators/jotai_setup/manifest.go b/generators/jotai_setup/manifest.go new file mode 100644 index 0000000..ae2b112 --- /dev/null +++ b/generators/jotai_setup/manifest.go @@ -0,0 +1,29 @@ +package jotaisetup + +import "github.com/version14/dot/pkg/dotapi" + +var Manifest = dotapi.Manifest{ + Name: "jotai_setup", + Version: "0.2.0", + Description: "Jotai atomic state management with example counter atom", + DependsOn: []string{"typescript_base"}, + ConflictsWith: []string{"zustand_setup"}, + Outputs: []string{ + "src/atoms/counter.atom.ts", + }, + PostGenerationCommands: []dotapi.Command{ + {Cmd: "pnpm install --dangerously-allow-all-builds"}, + }, + TestCommands: []dotapi.Command{ + {Cmd: "pnpm exec tsc --noEmit"}, + }, + Validators: []dotapi.Validator{ + { + Name: "jotai-setup-files", + Checks: []dotapi.Check{ + {Type: dotapi.CheckFileExists, Path: "src/atoms/counter.atom.ts"}, + {Type: dotapi.CheckJSONKeyExists, Path: "package.json", Key: "dependencies.jotai"}, + }, + }, + }, +} diff --git a/generators/nextjs_base/files/next.config.ts b/generators/nextjs_base/files/next.config.ts new file mode 100644 index 0000000..cb651cd --- /dev/null +++ b/generators/nextjs_base/files/next.config.ts @@ -0,0 +1,5 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = {}; + +export default nextConfig; diff --git a/generators/nextjs_base/files/src/app/globals.css b/generators/nextjs_base/files/src/app/globals.css new file mode 100644 index 0000000..366a8b1 --- /dev/null +++ b/generators/nextjs_base/files/src/app/globals.css @@ -0,0 +1,9 @@ +*, +*::before, +*::after { + box-sizing: border-box; +} + +body { + margin: 0; +} diff --git a/generators/nextjs_base/files/src/app/layout.tsx.tmpl b/generators/nextjs_base/files/src/app/layout.tsx.tmpl new file mode 100644 index 0000000..c7cf717 --- /dev/null +++ b/generators/nextjs_base/files/src/app/layout.tsx.tmpl @@ -0,0 +1,19 @@ +import type { Metadata } from "next"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "{{.ProjectName}}", + description: "Generated by dot", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/generators/nextjs_base/files/src/app/page.tsx.tmpl b/generators/nextjs_base/files/src/app/page.tsx.tmpl new file mode 100644 index 0000000..333dbae --- /dev/null +++ b/generators/nextjs_base/files/src/app/page.tsx.tmpl @@ -0,0 +1,7 @@ +export default function Home() { + return ( +
+

Hello from {{.ProjectName}}

+
+ ); +} diff --git a/generators/nextjs_base/generator.go b/generators/nextjs_base/generator.go new file mode 100644 index 0000000..8f1bbb2 --- /dev/null +++ b/generators/nextjs_base/generator.go @@ -0,0 +1,76 @@ +package nextjsbase + +import ( + "embed" + + "github.com/version14/dot/internal/render" + "github.com/version14/dot/internal/state" + "github.com/version14/dot/pkg/dotapi" +) + +//go:embed all:files +var fs embed.FS + +type nextjsData struct { + ProjectName string +} + +type Generator struct{} + +func New() *Generator { return &Generator{} } + +func (g *Generator) Name() string { return Manifest.Name } +func (g *Generator) Version() string { return Manifest.Version } + +func (g *Generator) Generate(ctx *dotapi.Context) error { + projectName, _ := ctx.Answers["project_name"].(string) + if projectName == "" { + projectName = ctx.Spec.Metadata.ProjectName + } + if projectName == "" { + projectName = "app" + } + + if err := ctx.State.UpdateJSON("package.json", func(d *state.JSONDoc) error { + d.Merge(map[string]interface{}{ + "scripts": map[string]interface{}{ + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "next lint", + }, + "dependencies": map[string]interface{}{ + "next": "^16.2.6", + "react": "^19.2.6", + "react-dom": "^19.2.6", + }, + "devDependencies": map[string]interface{}{ + "@types/node": "^25.9.1", + "@types/react": "^19.2.15", + "@types/react-dom": "^19.2.3", + }, + }) + return nil + }); err != nil { + return err + } + + if err := ctx.State.UpdateJSON("tsconfig.json", func(d *state.JSONDoc) error { + _ = d.SetNested("compilerOptions.jsx", "preserve") + _ = d.SetNested("compilerOptions.lib", []interface{}{"DOM", "DOM.Iterable", "ES2022"}) + _ = d.SetNested("compilerOptions.plugins", []interface{}{map[string]interface{}{"name": "next"}}) + _ = d.SetNested("compilerOptions.paths", map[string]interface{}{"@/*": []interface{}{"./src/*"}}) + _ = d.SetNested("compilerOptions.allowJs", true) + _ = d.SetNested("compilerOptions.noEmit", true) + _ = d.SetNested("compilerOptions.incremental", true) + _ = d.SetNested("compilerOptions.resolveJsonModule", true) + _ = d.SetNested("compilerOptions.isolatedModules", true) + _ = d.SetNested("exclude", []interface{}{"node_modules"}) + _ = d.SetNested("include", []interface{}{"src", ".next/types/**/*.ts"}) + return nil + }); err != nil { + return err + } + + return render.NewLocalFolderRenderer(ctx.State).Render(fs, nextjsData{ProjectName: projectName}) +} diff --git a/generators/nextjs_base/manifest.go b/generators/nextjs_base/manifest.go new file mode 100644 index 0000000..436e060 --- /dev/null +++ b/generators/nextjs_base/manifest.go @@ -0,0 +1,34 @@ +package nextjsbase + +import "github.com/version14/dot/pkg/dotapi" + +var Manifest = dotapi.Manifest{ + Name: "nextjs_base", + Version: "0.2.0", + Description: "Next.js 15 App Router project with TypeScript", + DependsOn: []string{"typescript_base"}, + Outputs: []string{ + "next.config.ts", + "src/app/layout.tsx", + "src/app/page.tsx", + "src/app/globals.css", + }, + PostGenerationCommands: []dotapi.Command{ + {Cmd: "pnpm install --dangerously-allow-all-builds"}, + }, + TestCommands: []dotapi.Command{ + {Cmd: "pnpm exec tsc --noEmit"}, + {Cmd: "pnpm exec next build"}, + }, + Validators: []dotapi.Validator{ + { + Name: "nextjs-base-files", + Checks: []dotapi.Check{ + {Type: dotapi.CheckFileExists, Path: "next.config.ts"}, + {Type: dotapi.CheckFileExists, Path: "src/app/layout.tsx"}, + {Type: dotapi.CheckFileExists, Path: "src/app/page.tsx"}, + {Type: dotapi.CheckJSONKeyExists, Path: "package.json", Key: "dependencies.next"}, + }, + }, + }, +} diff --git a/generators/panda_css/files/panda.config.ts b/generators/panda_css/files/panda.config.ts new file mode 100644 index 0000000..7f881d7 --- /dev/null +++ b/generators/panda_css/files/panda.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "@pandacss/dev"; + +export default defineConfig({ + preflight: true, + include: ["./src/**/*.{js,jsx,ts,tsx}"], + exclude: [], + outdir: "styled-system", +}); diff --git a/generators/panda_css/files/postcss.config.mjs b/generators/panda_css/files/postcss.config.mjs new file mode 100644 index 0000000..ff84c7a --- /dev/null +++ b/generators/panda_css/files/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + "@pandacss/dev/postcss": {}, + }, +}; diff --git a/generators/panda_css/generator.go b/generators/panda_css/generator.go new file mode 100644 index 0000000..297d1d2 --- /dev/null +++ b/generators/panda_css/generator.go @@ -0,0 +1,62 @@ +package pandacss + +import ( + "embed" + + "github.com/version14/dot/internal/render" + "github.com/version14/dot/internal/state" + "github.com/version14/dot/pkg/dotapi" +) + +//go:embed all:files +var filesFS embed.FS + +type Generator struct{} + +func New() *Generator { return &Generator{} } + +func (g *Generator) Name() string { return Manifest.Name } +func (g *Generator) Version() string { return Manifest.Version } + +func (g *Generator) Generate(ctx *dotapi.Context) error { + linter, _ := ctx.Answers["linter"].(string) + if linter == "" { + linter, _ = ctx.Answers["frontend-linter"].(string) + } + + if err := ctx.State.UpdateJSON("package.json", func(d *state.JSONDoc) error { + d.Merge(map[string]interface{}{ + "devDependencies": map[string]interface{}{ + "@pandacss/dev": "^1.11.1", + }, + }) + prepare := "panda codegen" + if existing, ok := d.GetNested("scripts.prepare"); ok { + if s, isStr := existing.(string); isStr && s != "" { + prepare = s + " && panda codegen" + } + } + return d.SetNested("scripts.prepare", prepare) + }); err != nil { + return err + } + + existing := "" + if f, ok := ctx.State.GetFile(".prettierignore"); ok { + existing = string(f.Content) + if len(existing) > 0 && existing[len(existing)-1] != '\n' { + existing += "\n" + } + } + ctx.State.WriteFile(".prettierignore", []byte(existing+"styled-system/\n"), state.ContentRaw) + + if linter == "biome" { + if err := ctx.State.UpdateJSON("biome.json", func(d *state.JSONDoc) error { + return d.AppendStringSet("files.includes", "!**/styled-system") + }); err != nil { + return err + } + } + + return render.NewLocalFolderRenderer(ctx.State).Render(filesFS, nil) +} diff --git a/generators/panda_css/manifest.go b/generators/panda_css/manifest.go new file mode 100644 index 0000000..8fc0f72 --- /dev/null +++ b/generators/panda_css/manifest.go @@ -0,0 +1,31 @@ +package pandacss + +import "github.com/version14/dot/pkg/dotapi" + +var Manifest = dotapi.Manifest{ + Name: "panda_css", + Version: "0.2.0", + Description: "Panda CSS v1 with styled-system code generation", + DependsOn: []string{"typescript_base"}, + ConflictsWith: []string{"tailwind_v4", "shadcn_ui"}, + Outputs: []string{ + "panda.config.ts", + "postcss.config.mjs", + }, + PostGenerationCommands: []dotapi.Command{ + // panda codegen runs via the prepare script on pnpm install + {Cmd: "pnpm install --dangerously-allow-all-builds"}, + }, + TestCommands: []dotapi.Command{ + {Cmd: "pnpm exec tsc --noEmit"}, + }, + Validators: []dotapi.Validator{ + { + Name: "panda-css-files", + Checks: []dotapi.Check{ + {Type: dotapi.CheckFileExists, Path: "panda.config.ts"}, + {Type: dotapi.CheckJSONKeyExists, Path: "package.json", Key: "devDependencies.@pandacss/dev"}, + }, + }, + }, +} diff --git a/generators/playwright_setup/files/e2e/example.spec.ts b/generators/playwright_setup/files/e2e/example.spec.ts new file mode 100644 index 0000000..1d55620 --- /dev/null +++ b/generators/playwright_setup/files/e2e/example.spec.ts @@ -0,0 +1,6 @@ +import { test, expect } from "@playwright/test"; + +test("homepage renders", async ({ page }) => { + await page.goto("/"); + await expect(page).toHaveTitle(/.*/); +}); diff --git a/generators/playwright_setup/files/playwright.config.ts.tmpl b/generators/playwright_setup/files/playwright.config.ts.tmpl new file mode 100644 index 0000000..56c9f34 --- /dev/null +++ b/generators/playwright_setup/files/playwright.config.ts.tmpl @@ -0,0 +1,17 @@ +import { defineConfig, devices } from "@playwright/test"; + +export default defineConfig({ + testDir: "./e2e", + fullyParallel: true, + reporter: "html", + use: { baseURL: "{{.DevServerURL}}" }, + webServer: { + command: "{{.DevServerCommand}}", + url: "{{.DevServerURL}}", + reuseExistingServer: true, + timeout: 120_000, + }, + projects: [ + { name: "chromium", use: { ...devices["Desktop Chrome"] } }, + ], +}); diff --git a/generators/playwright_setup/generator.go b/generators/playwright_setup/generator.go new file mode 100644 index 0000000..17c5d8f --- /dev/null +++ b/generators/playwright_setup/generator.go @@ -0,0 +1,54 @@ +package playwrightsetup + +import ( + "embed" + + "github.com/version14/dot/internal/render" + "github.com/version14/dot/internal/state" + "github.com/version14/dot/pkg/dotapi" +) + +//go:embed all:files +var fs embed.FS + +type playwrightData struct { + DevServerURL string + DevServerCommand string +} + +type Generator struct{} + +func New() *Generator { return &Generator{} } + +func (g *Generator) Name() string { return Manifest.Name } +func (g *Generator) Version() string { return Manifest.Version } + +func (g *Generator) Generate(ctx *dotapi.Context) error { + framework, _ := ctx.Answers["framework"].(string) + + devServerURL := "http://localhost:5173" + devServerCommand := "pnpm exec vite --host 127.0.0.1" + if framework == "next" { + devServerURL = "http://localhost:3000" + devServerCommand = "pnpm exec next start" + } + + if err := ctx.State.UpdateJSON("package.json", func(d *state.JSONDoc) error { + d.Merge(map[string]interface{}{ + "scripts": map[string]interface{}{ + "test:e2e": "playwright test", + }, + "devDependencies": map[string]interface{}{ + "@playwright/test": "^1.60.0", + }, + }) + return nil + }); err != nil { + return err + } + + return render.NewLocalFolderRenderer(ctx.State).Render(fs, playwrightData{ + DevServerURL: devServerURL, + DevServerCommand: devServerCommand, + }) +} diff --git a/generators/playwright_setup/manifest.go b/generators/playwright_setup/manifest.go new file mode 100644 index 0000000..47afa1e --- /dev/null +++ b/generators/playwright_setup/manifest.go @@ -0,0 +1,30 @@ +package playwrightsetup + +import "github.com/version14/dot/pkg/dotapi" + +var Manifest = dotapi.Manifest{ + Name: "playwright_setup", + Version: "0.2.0", + Description: "Playwright end-to-end testing setup", + DependsOn: []string{"typescript_base"}, + Outputs: []string{ + "playwright.config.ts", + "e2e/example.spec.ts", + }, + PostGenerationCommands: []dotapi.Command{ + {Cmd: "pnpm install --dangerously-allow-all-builds"}, + {Cmd: "pnpm exec playwright install --with-deps chromium"}, + }, + TestCommands: []dotapi.Command{ + {Cmd: "pnpm exec playwright test"}, + }, + Validators: []dotapi.Validator{ + { + Name: "playwright-setup-files", + Checks: []dotapi.Check{ + {Type: dotapi.CheckFileExists, Path: "playwright.config.ts"}, + {Type: dotapi.CheckJSONKeyExists, Path: "package.json", Key: "devDependencies.@playwright/test"}, + }, + }, + }, +} diff --git a/generators/prettier_config/generator.go b/generators/prettier_config/generator.go index 9a2308c..72ca22d 100644 --- a/generators/prettier_config/generator.go +++ b/generators/prettier_config/generator.go @@ -1,6 +1,8 @@ package prettierconfig import ( + "strings" + "github.com/version14/dot/internal/state" "github.com/version14/dot/pkg/dotapi" ) @@ -12,6 +14,22 @@ func New() *Generator { return &Generator{} } func (g *Generator) Name() string { return Manifest.Name } func (g *Generator) Version() string { return Manifest.Version } +// defaultIgnoreEntries are the standard prettier ignore entries every project +// should have. Earlier generators (e.g. panda_css) may have pre-written entries +// to .prettierignore; we read them and merge so nothing is lost. +var defaultIgnoreEntries = []string{ + "node_modules", + ".dot", + ".next", + "coverage", + "dist", + "build", + "playwright-report", + "storybook-static", + "test-results", + ".env", +} + func (g *Generator) Generate(ctx *dotapi.Context) error { ctx.State.WriteFile(".prettierrc", []byte(`{ "semi": true, @@ -22,11 +40,28 @@ func (g *Generator) Generate(ctx *dotapi.Context) error { } `), state.ContentRaw) - ctx.State.WriteFile(".prettierignore", []byte(`node_modules -dist -build -.env -`), state.ContentRaw) + // Merge existing .prettierignore entries (written by earlier generators) + // with the standard defaults, deduplicating. Order: existing first, then defaults. + existing := "" + if f, ok := ctx.State.GetFile(".prettierignore"); ok { + existing = string(f.Content) + } + seen := map[string]bool{} + var result []string + for _, line := range strings.Split(existing, "\n") { + line = strings.TrimRight(line, "\r") + if line != "" && !seen[line] { + seen[line] = true + result = append(result, line) + } + } + for _, entry := range defaultIgnoreEntries { + if !seen[entry] { + seen[entry] = true + result = append(result, entry) + } + } + ctx.State.WriteFile(".prettierignore", []byte(strings.Join(result, "\n")+"\n"), state.ContentRaw) return nil } diff --git a/generators/prettier_config/manifest.go b/generators/prettier_config/manifest.go index afd34a5..006ecc9 100644 --- a/generators/prettier_config/manifest.go +++ b/generators/prettier_config/manifest.go @@ -4,7 +4,7 @@ import "github.com/version14/dot/pkg/dotapi" var Manifest = dotapi.Manifest{ Name: "prettier_config", - Version: "0.1.0", + Version: "0.1.1", Description: "Base Prettier configuration: .prettierrc and .prettierignore", DependsOn: []string{"typescript_base"}, Outputs: []string{ diff --git a/generators/prettier_frontend_rules/generator.go b/generators/prettier_frontend_rules/generator.go new file mode 100644 index 0000000..1294af5 --- /dev/null +++ b/generators/prettier_frontend_rules/generator.go @@ -0,0 +1,29 @@ +package prettierfrontendrules + +import ( + "github.com/version14/dot/internal/state" + "github.com/version14/dot/pkg/dotapi" +) + +type Generator struct{} + +func New() *Generator { return &Generator{} } + +func (g *Generator) Name() string { return Manifest.Name } +func (g *Generator) Version() string { return Manifest.Version } + +func (g *Generator) Generate(ctx *dotapi.Context) error { + return ctx.State.UpdateJSON(".prettierrc", func(d *state.JSONDoc) error { + d.Merge(map[string]interface{}{ + "semi": true, + "singleQuote": false, + "jsxSingleQuote": false, + "trailingComma": "all", + "printWidth": 80, + "tabWidth": 2, + "bracketSpacing": true, + "bracketSameLine": false, + }) + return nil + }) +} diff --git a/generators/prettier_frontend_rules/manifest.go b/generators/prettier_frontend_rules/manifest.go new file mode 100644 index 0000000..d130e3c --- /dev/null +++ b/generators/prettier_frontend_rules/manifest.go @@ -0,0 +1,12 @@ +package prettierfrontendrules + +import "github.com/version14/dot/pkg/dotapi" + +var Manifest = dotapi.Manifest{ + Name: "prettier_frontend_rules", + Version: "0.1.0", + Description: "Frontend-specific Prettier rules for JSX/TSX projects", + DependsOn: []string{"prettier_config"}, + Outputs: []string{}, + Validators: []dotapi.Validator{}, +} diff --git a/generators/prettier_typescript_deps/manifest.go b/generators/prettier_typescript_deps/manifest.go index 8c3e219..c2e5c53 100644 --- a/generators/prettier_typescript_deps/manifest.go +++ b/generators/prettier_typescript_deps/manifest.go @@ -13,6 +13,7 @@ var Manifest = dotapi.Manifest{ {Cmd: "pnpm format"}, }, TestCommands: []dotapi.Command{ + {Cmd: "pnpm format"}, {Cmd: "pnpm format:check"}, }, Validators: []dotapi.Validator{ diff --git a/generators/react_app/files/index.html.tmpl b/generators/react_app/files/index.html.tmpl new file mode 100644 index 0000000..7258ecb --- /dev/null +++ b/generators/react_app/files/index.html.tmpl @@ -0,0 +1,12 @@ + + + + + + {{.ProjectName}} + + +
+ + + diff --git a/generators/react_app/files/src/App.tsx.tmpl b/generators/react_app/files/src/App.tsx.tmpl new file mode 100644 index 0000000..c2b6ed5 --- /dev/null +++ b/generators/react_app/files/src/App.tsx.tmpl @@ -0,0 +1,3 @@ +export default function App() { + return

Hello from {{.ProjectName}}

; +} diff --git a/generators/react_app/files/src/main.tsx b/generators/react_app/files/src/main.tsx new file mode 100644 index 0000000..0817190 --- /dev/null +++ b/generators/react_app/files/src/main.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; + +const rootElement = document.getElementById("root"); + +if (!rootElement) { + throw new Error("Root element #root not found"); +} + +ReactDOM.createRoot(rootElement).render( + + + , +); diff --git a/generators/react_app/files/src/vite-env.d.ts b/generators/react_app/files/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/generators/react_app/files/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/generators/react_app/files/vite.config.ts b/generators/react_app/files/vite.config.ts new file mode 100644 index 0000000..081c8d9 --- /dev/null +++ b/generators/react_app/files/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], +}); diff --git a/generators/react_app/generator.go b/generators/react_app/generator.go index 09fad39..d624a2e 100644 --- a/generators/react_app/generator.go +++ b/generators/react_app/generator.go @@ -1,11 +1,20 @@ package reactapp import ( + "embed" + "github.com/version14/dot/internal/render" "github.com/version14/dot/internal/state" "github.com/version14/dot/pkg/dotapi" ) +//go:embed all:files +var filesFS embed.FS + +type reactAppData struct { + ProjectName string +} + type Generator struct{} func New() *Generator { return &Generator{} } @@ -22,7 +31,6 @@ func (g *Generator) Generate(ctx *dotapi.Context) error { projectName = "app" } - // Merge React deps + scripts into package.json. if err := ctx.State.UpdateJSON("package.json", func(d *state.JSONDoc) error { d.Merge(map[string]interface{}{ "scripts": map[string]interface{}{ @@ -35,8 +43,8 @@ func (g *Generator) Generate(ctx *dotapi.Context) error { "react-dom": "^19.2.6", }, "devDependencies": map[string]interface{}{ - "@types/react": "^19.2.6", - "@types/react-dom": "^19.2.6", + "@types/react": "^19.2.15", + "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.2", "vite": "^8.0.14", }, @@ -46,7 +54,6 @@ func (g *Generator) Generate(ctx *dotapi.Context) error { return err } - // Patch tsconfig for JSX. if err := ctx.State.UpdateJSON("tsconfig.json", func(d *state.JSONDoc) error { _ = d.SetNested("compilerOptions.jsx", "react-jsx") _ = d.SetNested("compilerOptions.lib", []interface{}{"DOM", "DOM.Iterable", "ES2022"}) @@ -55,61 +62,5 @@ func (g *Generator) Generate(ctx *dotapi.Context) error { return err } - // Source files. - data := map[string]interface{}{"ProjectName": projectName} - - if out, err := render.Render(indexHTMLTmpl, data); err == nil { - ctx.State.WriteFile("index.html", out, state.ContentRaw) - } else { - return err - } - - ctx.State.WriteFile("vite.config.ts", []byte(viteConfig), state.ContentRaw) - ctx.State.WriteFile("src/main.tsx", []byte(mainTSX), state.ContentRaw) - - if out, err := render.Render(appTSX, data); err == nil { - ctx.State.WriteFile("src/App.tsx", out, state.ContentRaw) - } else { - return err - } - - return nil -} - -const indexHTMLTmpl = ` - - - - - {{.ProjectName}} - - -
- - - -` - -const viteConfig = `import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; - -export default defineConfig({ - plugins: [react()], -}); -` - -const mainTSX = `import React from "react"; -import ReactDOM from "react-dom/client"; -import App from "./App"; - -ReactDOM.createRoot(document.getElementById("root")!).render( - - - , -); -` - -const appTSX = `export default function App() { - return

Hello from {{.ProjectName}}

; + return render.NewLocalFolderRenderer(ctx.State).Render(filesFS, reactAppData{ProjectName: projectName}) } -` diff --git a/generators/react_app/manifest.go b/generators/react_app/manifest.go index 3783ebd..50d2e97 100644 --- a/generators/react_app/manifest.go +++ b/generators/react_app/manifest.go @@ -10,12 +10,13 @@ import ( // project needs the TypeScript foundation first. var Manifest = dotapi.Manifest{ Name: "react_app", - Version: "0.5.1", + Version: "0.6.0", Description: "React + Vite application setup", DependsOn: []string{"typescript_base"}, Outputs: []string{ "src/main.tsx", "src/App.tsx", + "src/vite-env.d.ts", "index.html", "vite.config.ts", }, @@ -36,6 +37,7 @@ var Manifest = dotapi.Manifest{ Checks: []dotapi.Check{ {Type: dotapi.CheckFileExists, Path: "src/main.tsx"}, {Type: dotapi.CheckFileExists, Path: "src/App.tsx"}, + {Type: dotapi.CheckFileExists, Path: "src/vite-env.d.ts"}, {Type: dotapi.CheckFileExists, Path: "index.html"}, {Type: dotapi.CheckFileExists, Path: "vite.config.ts"}, {Type: dotapi.CheckJSONKeyExists, Path: "package.json", Key: "dependencies.react"}, diff --git a/generators/react_router_v7/files/src/main.tsx b/generators/react_router_v7/files/src/main.tsx new file mode 100644 index 0000000..a5d225b --- /dev/null +++ b/generators/react_router_v7/files/src/main.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { RouterProvider } from "react-router-dom"; +import { router } from "./router"; + +const rootElement = document.getElementById("root"); + +if (!rootElement) { + throw new Error("Root element #root not found"); +} + +ReactDOM.createRoot(rootElement).render( + + + , +); diff --git a/generators/react_router_v7/files/src/pages/Home.tsx b/generators/react_router_v7/files/src/pages/Home.tsx new file mode 100644 index 0000000..c23c9bd --- /dev/null +++ b/generators/react_router_v7/files/src/pages/Home.tsx @@ -0,0 +1,3 @@ +export default function Home() { + return

Home

; +} diff --git a/generators/react_router_v7/files/src/router.tsx b/generators/react_router_v7/files/src/router.tsx new file mode 100644 index 0000000..9be49b0 --- /dev/null +++ b/generators/react_router_v7/files/src/router.tsx @@ -0,0 +1,6 @@ +import { createBrowserRouter } from "react-router-dom"; +import Home from "./pages/Home"; + +export const router = createBrowserRouter([ + { path: "/", element: }, +]); diff --git a/generators/react_router_v7/generator.go b/generators/react_router_v7/generator.go new file mode 100644 index 0000000..01c6926 --- /dev/null +++ b/generators/react_router_v7/generator.go @@ -0,0 +1,35 @@ +package reactrouterv7 + +import ( + "embed" + + "github.com/version14/dot/internal/render" + "github.com/version14/dot/internal/state" + "github.com/version14/dot/pkg/dotapi" +) + +//go:embed all:files +var fs embed.FS + +type Generator struct{} + +func New() *Generator { return &Generator{} } + +func (g *Generator) Name() string { return Manifest.Name } +func (g *Generator) Version() string { return Manifest.Version } + +func (g *Generator) Generate(ctx *dotapi.Context) error { + if err := ctx.State.UpdateJSON("package.json", func(d *state.JSONDoc) error { + d.Merge(map[string]interface{}{ + "dependencies": map[string]interface{}{ + "react-router": "^7.16.0", + "react-router-dom": "^7.16.0", + }, + }) + return nil + }); err != nil { + return err + } + + return render.NewLocalFolderRenderer(ctx.State).Render(fs, nil) +} diff --git a/generators/react_router_v7/manifest.go b/generators/react_router_v7/manifest.go new file mode 100644 index 0000000..162c7ff --- /dev/null +++ b/generators/react_router_v7/manifest.go @@ -0,0 +1,31 @@ +package reactrouterv7 + +import "github.com/version14/dot/pkg/dotapi" + +var Manifest = dotapi.Manifest{ + Name: "react_router_v7", + Version: "0.2.0", + Description: "React Router v7 client-side routing for React+Vite", + DependsOn: []string{"react_app"}, + ConflictsWith: []string{"tanstack_router"}, + Outputs: []string{ + "src/router.tsx", + "src/pages/Home.tsx", + }, + PostGenerationCommands: []dotapi.Command{ + {Cmd: "pnpm install --dangerously-allow-all-builds"}, + }, + TestCommands: []dotapi.Command{ + {Cmd: "pnpm exec tsc --noEmit"}, + {Cmd: "pnpm exec vite build"}, + }, + Validators: []dotapi.Validator{ + { + Name: "react-router-v7-files", + Checks: []dotapi.Check{ + {Type: dotapi.CheckFileExists, Path: "src/router.tsx"}, + {Type: dotapi.CheckJSONKeyExists, Path: "package.json", Key: "dependencies.react-router"}, + }, + }, + }, +} diff --git a/generators/sentry_frontend/files/.env.example b/generators/sentry_frontend/files/.env.example new file mode 100644 index 0000000..b79b41a --- /dev/null +++ b/generators/sentry_frontend/files/.env.example @@ -0,0 +1,2 @@ +# Sentry +VITE_SENTRY_DSN=https://your_dsn@sentry.io/your_project_id diff --git a/generators/sentry_frontend/files/sentry.ts b/generators/sentry_frontend/files/sentry.ts new file mode 100644 index 0000000..e5c84c5 --- /dev/null +++ b/generators/sentry_frontend/files/sentry.ts @@ -0,0 +1,11 @@ +import * as Sentry from "@sentry/react"; + +export function initSentry() { + Sentry.init({ + dsn: import.meta.env.VITE_SENTRY_DSN, + tracesSampleRate: 0.1, + environment: import.meta.env.MODE, + }); +} + +export { Sentry }; diff --git a/generators/sentry_frontend/generator.go b/generators/sentry_frontend/generator.go new file mode 100644 index 0000000..b5b91ec --- /dev/null +++ b/generators/sentry_frontend/generator.go @@ -0,0 +1,62 @@ +package sentryfrontend + +import ( + "embed" + + "github.com/version14/dot/internal/state" + "github.com/version14/dot/pkg/dotapi" +) + +//go:embed all:files +var filesFS embed.FS + +//go:embed all:next +var nextFS embed.FS + +type Generator struct{} + +func New() *Generator { return &Generator{} } + +func (g *Generator) Name() string { return Manifest.Name } +func (g *Generator) Version() string { return Manifest.Version } + +func mustRead(fs embed.FS, path string) []byte { + data, err := fs.ReadFile(path) + if err != nil { + panic(err) + } + return data +} + +func (g *Generator) Generate(ctx *dotapi.Context) error { + framework, _ := ctx.Answers["framework"].(string) + + sentryPkg := "@sentry/react" + src := filesFS + dir := "files" + if framework == "next" { + sentryPkg = "@sentry/nextjs" + src = nextFS + dir = "next" + } + + if err := ctx.State.UpdateJSON("package.json", func(d *state.JSONDoc) error { + d.Merge(map[string]interface{}{ + "dependencies": map[string]interface{}{ + sentryPkg: "^10.55.0", + }, + }) + return nil + }); err != nil { + return err + } + + ctx.State.WriteFile("src/lib/sentry.ts", mustRead(src, dir+"/sentry.ts"), state.ContentRaw) + ctx.State.AppendFile(".env.example", mustRead(src, dir+"/.env.example")) + + if framework == "next" { + ctx.State.WriteFile("sentry.client.config.ts", mustRead(nextFS, "next/sentry.client.config.ts"), state.ContentRaw) + } + + return nil +} diff --git a/generators/sentry_frontend/manifest.go b/generators/sentry_frontend/manifest.go new file mode 100644 index 0000000..3ea6206 --- /dev/null +++ b/generators/sentry_frontend/manifest.go @@ -0,0 +1,27 @@ +package sentryfrontend + +import "github.com/version14/dot/pkg/dotapi" + +var Manifest = dotapi.Manifest{ + Name: "sentry_frontend", + Version: "0.2.0", + Description: "Sentry error tracking and performance monitoring", + DependsOn: []string{"typescript_base"}, + Outputs: []string{ + "src/lib/sentry.ts", + }, + PostGenerationCommands: []dotapi.Command{ + {Cmd: "pnpm install --dangerously-allow-all-builds"}, + }, + TestCommands: []dotapi.Command{ + {Cmd: "pnpm exec tsc --noEmit"}, + }, + Validators: []dotapi.Validator{ + { + Name: "sentry-frontend-files", + Checks: []dotapi.Check{ + {Type: dotapi.CheckFileExists, Path: "src/lib/sentry.ts"}, + }, + }, + }, +} diff --git a/generators/sentry_frontend/next/.env.example b/generators/sentry_frontend/next/.env.example new file mode 100644 index 0000000..9dd2ec7 --- /dev/null +++ b/generators/sentry_frontend/next/.env.example @@ -0,0 +1,2 @@ +# Sentry +NEXT_PUBLIC_SENTRY_DSN=https://your_dsn@sentry.io/your_project_id diff --git a/generators/sentry_frontend/next/sentry.client.config.ts b/generators/sentry_frontend/next/sentry.client.config.ts new file mode 100644 index 0000000..413ee5f --- /dev/null +++ b/generators/sentry_frontend/next/sentry.client.config.ts @@ -0,0 +1,6 @@ +import * as Sentry from "@sentry/nextjs"; + +Sentry.init({ + dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, + tracesSampleRate: 0.1, +}); diff --git a/generators/sentry_frontend/next/sentry.ts b/generators/sentry_frontend/next/sentry.ts new file mode 100644 index 0000000..6da8bbc --- /dev/null +++ b/generators/sentry_frontend/next/sentry.ts @@ -0,0 +1,3 @@ +// For Next.js, configure Sentry in sentry.client.config.ts and sentry.server.config.ts +// See: https://docs.sentry.io/platforms/javascript/guides/nextjs/ +export * from "@sentry/nextjs"; diff --git a/generators/seo_react/files/src/components/SEO.tsx b/generators/seo_react/files/src/components/SEO.tsx new file mode 100644 index 0000000..43596a7 --- /dev/null +++ b/generators/seo_react/files/src/components/SEO.tsx @@ -0,0 +1,17 @@ +import { Helmet } from "react-helmet-async"; + +interface SEOProps { + title?: string; + description?: string; + canonical?: string; +} + +export function SEO({ title, description, canonical }: SEOProps) { + return ( + + {title && {title}} + {description && } + {canonical && } + + ); +} diff --git a/generators/seo_react/files/src/providers/HelmetProvider.tsx b/generators/seo_react/files/src/providers/HelmetProvider.tsx new file mode 100644 index 0000000..9fe3104 --- /dev/null +++ b/generators/seo_react/files/src/providers/HelmetProvider.tsx @@ -0,0 +1,5 @@ +import { HelmetProvider as BaseHelmetProvider } from "react-helmet-async"; + +export function HelmetProvider({ children }: { children: React.ReactNode }) { + return {children}; +} diff --git a/generators/seo_react/generator.go b/generators/seo_react/generator.go new file mode 100644 index 0000000..fdfa10e --- /dev/null +++ b/generators/seo_react/generator.go @@ -0,0 +1,34 @@ +package seoreact + +import ( + "embed" + + "github.com/version14/dot/internal/render" + "github.com/version14/dot/internal/state" + "github.com/version14/dot/pkg/dotapi" +) + +//go:embed all:files +var fs embed.FS + +type Generator struct{} + +func New() *Generator { return &Generator{} } + +func (g *Generator) Name() string { return Manifest.Name } +func (g *Generator) Version() string { return Manifest.Version } + +func (g *Generator) Generate(ctx *dotapi.Context) error { + if err := ctx.State.UpdateJSON("package.json", func(d *state.JSONDoc) error { + d.Merge(map[string]interface{}{ + "dependencies": map[string]interface{}{ + "react-helmet-async": "^3.0.0", + }, + }) + return nil + }); err != nil { + return err + } + + return render.NewLocalFolderRenderer(ctx.State).Render(fs, nil) +} diff --git a/generators/seo_react/manifest.go b/generators/seo_react/manifest.go new file mode 100644 index 0000000..bdf22e9 --- /dev/null +++ b/generators/seo_react/manifest.go @@ -0,0 +1,29 @@ +package seoreact + +import "github.com/version14/dot/pkg/dotapi" + +var Manifest = dotapi.Manifest{ + Name: "seo_react", + Version: "0.2.0", + Description: "SEO setup for React+Vite using react-helmet-async", + DependsOn: []string{"react_app"}, + ConflictsWith: []string{"seo_next"}, + Outputs: []string{ + "src/providers/HelmetProvider.tsx", + "src/components/SEO.tsx", + }, + PostGenerationCommands: []dotapi.Command{ + {Cmd: "pnpm install --dangerously-allow-all-builds"}, + }, + TestCommands: []dotapi.Command{ + {Cmd: "pnpm exec tsc --noEmit"}, + }, + Validators: []dotapi.Validator{ + { + Name: "seo-react-files", + Checks: []dotapi.Check{ + {Type: dotapi.CheckFileExists, Path: "src/components/SEO.tsx"}, + }, + }, + }, +} diff --git a/generators/shadcn_ui/extra/globals.css b/generators/shadcn_ui/extra/globals.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/generators/shadcn_ui/extra/globals.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/generators/shadcn_ui/files/components.json.tmpl b/generators/shadcn_ui/files/components.json.tmpl new file mode 100644 index 0000000..41ec313 --- /dev/null +++ b/generators/shadcn_ui/files/components.json.tmpl @@ -0,0 +1,15 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": {{.RSC}}, + "tsx": true, + "tailwind": { + "css": "{{.CSSPath}}", + "baseColor": "slate", + "cssVariables": true + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} diff --git a/generators/shadcn_ui/files/src/components/ui/button.tsx b/generators/shadcn_ui/files/src/components/ui/button.tsx new file mode 100644 index 0000000..5119aaf --- /dev/null +++ b/generators/shadcn_ui/files/src/components/ui/button.tsx @@ -0,0 +1,42 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + outline: "border border-input hover:bg-accent hover:text-accent-foreground", + ghost: "hover:bg-accent hover:text-accent-foreground", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 px-3", + lg: "h-11 px-8", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps {} + +const Button = React.forwardRef( + ({ className, variant, size, ...props }, ref) => ( + +); + +const meta = { + title: "Example/Button", + component: ButtonComponent, + tags: ["autodocs"], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { label: "Button" }, +}; diff --git a/generators/storybook_setup/generator.go b/generators/storybook_setup/generator.go new file mode 100644 index 0000000..8cdf581 --- /dev/null +++ b/generators/storybook_setup/generator.go @@ -0,0 +1,51 @@ +package storybooksetup + +import ( + "embed" + + "github.com/version14/dot/internal/render" + "github.com/version14/dot/internal/state" + "github.com/version14/dot/pkg/dotapi" +) + +//go:embed all:files +var fs embed.FS + +type storybookData struct { + FrameworkPkg string +} + +type Generator struct{} + +func New() *Generator { return &Generator{} } + +func (g *Generator) Name() string { return Manifest.Name } +func (g *Generator) Version() string { return Manifest.Version } + +func (g *Generator) Generate(ctx *dotapi.Context) error { + framework, _ := ctx.Answers["framework"].(string) + + frameworkPkg := "@storybook/react-vite" + if framework == "next" { + frameworkPkg = "@storybook/nextjs" + } + + if err := ctx.State.UpdateJSON("package.json", func(d *state.JSONDoc) error { + d.Merge(map[string]interface{}{ + "scripts": map[string]interface{}{ + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build", + }, + "devDependencies": map[string]interface{}{ + "storybook": "^10.4.1", + "@storybook/react": "^10.4.1", + frameworkPkg: "^10.4.1", + }, + }) + return nil + }); err != nil { + return err + } + + return render.NewLocalFolderRenderer(ctx.State).Render(fs, storybookData{FrameworkPkg: frameworkPkg}) +} diff --git a/generators/storybook_setup/manifest.go b/generators/storybook_setup/manifest.go new file mode 100644 index 0000000..0de29e3 --- /dev/null +++ b/generators/storybook_setup/manifest.go @@ -0,0 +1,30 @@ +package storybooksetup + +import "github.com/version14/dot/pkg/dotapi" + +var Manifest = dotapi.Manifest{ + Name: "storybook_setup", + Version: "0.2.0", + Description: "Storybook v8 component development environment", + DependsOn: []string{"typescript_base"}, + Outputs: []string{ + ".storybook/main.ts", + ".storybook/preview.ts", + "src/stories/Button.stories.tsx", + }, + PostGenerationCommands: []dotapi.Command{ + {Cmd: "pnpm install --dangerously-allow-all-builds"}, + }, + TestCommands: []dotapi.Command{ + {Cmd: "pnpm exec storybook build"}, + }, + Validators: []dotapi.Validator{ + { + Name: "storybook-setup-files", + Checks: []dotapi.Check{ + {Type: dotapi.CheckFileExists, Path: ".storybook/main.ts"}, + {Type: dotapi.CheckJSONKeyExists, Path: "package.json", Key: "devDependencies.storybook"}, + }, + }, + }, +} diff --git a/generators/tailwind_v4/extra/vite.config.ts b/generators/tailwind_v4/extra/vite.config.ts new file mode 100644 index 0000000..8a24cd2 --- /dev/null +++ b/generators/tailwind_v4/extra/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; + +export default defineConfig({ + plugins: [tailwindcss(), react()], +}); diff --git a/generators/tailwind_v4/files/postcss.config.mjs b/generators/tailwind_v4/files/postcss.config.mjs new file mode 100644 index 0000000..c2ddf74 --- /dev/null +++ b/generators/tailwind_v4/files/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; diff --git a/generators/tailwind_v4/files/src/styles/globals.css b/generators/tailwind_v4/files/src/styles/globals.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/generators/tailwind_v4/files/src/styles/globals.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/generators/tailwind_v4/generator.go b/generators/tailwind_v4/generator.go new file mode 100644 index 0000000..8be1306 --- /dev/null +++ b/generators/tailwind_v4/generator.go @@ -0,0 +1,53 @@ +package tailwindv4 + +import ( + "embed" + + "github.com/version14/dot/internal/render" + "github.com/version14/dot/internal/state" + "github.com/version14/dot/pkg/dotapi" +) + +//go:embed all:files +var filesFS embed.FS + +//go:embed extra/vite.config.ts +var viteConfigBytes []byte + +type Generator struct{} + +func New() *Generator { return &Generator{} } + +func (g *Generator) Name() string { return Manifest.Name } +func (g *Generator) Version() string { return Manifest.Version } + +func (g *Generator) Generate(ctx *dotapi.Context) error { + framework, _ := ctx.Answers["framework"].(string) + + devDeps := map[string]interface{}{ + "@tailwindcss/vite": "^4.3.0", + "@tailwindcss/postcss": "^4.3.0", + } + + if err := ctx.State.UpdateJSON("package.json", func(d *state.JSONDoc) error { + d.Merge(map[string]interface{}{ + "dependencies": map[string]interface{}{ + "tailwindcss": "^4.3.0", + }, + "devDependencies": devDeps, + }) + return nil + }); err != nil { + return err + } + + if err := render.NewLocalFolderRenderer(ctx.State).Render(filesFS, nil); err != nil { + return err + } + + if framework != "next" { + ctx.State.WriteFile("vite.config.ts", viteConfigBytes, state.ContentRaw) + } + + return nil +} diff --git a/generators/tailwind_v4/manifest.go b/generators/tailwind_v4/manifest.go new file mode 100644 index 0000000..abdcbbf --- /dev/null +++ b/generators/tailwind_v4/manifest.go @@ -0,0 +1,30 @@ +package tailwindv4 + +import "github.com/version14/dot/pkg/dotapi" + +var Manifest = dotapi.Manifest{ + Name: "tailwind_v4", + Version: "0.2.0", + Description: "Tailwind CSS v4 (CSS-first configuration)", + DependsOn: []string{"typescript_base"}, + ConflictsWith: []string{"panda_css", "shadcn_ui"}, + Outputs: []string{ + "src/styles/globals.css", + "postcss.config.mjs", + }, + PostGenerationCommands: []dotapi.Command{ + {Cmd: "pnpm install --dangerously-allow-all-builds"}, + }, + TestCommands: []dotapi.Command{ + {Cmd: "pnpm exec tsc --noEmit"}, + }, + Validators: []dotapi.Validator{ + { + Name: "tailwind-v4-files", + Checks: []dotapi.Check{ + {Type: dotapi.CheckFileExists, Path: "src/styles/globals.css"}, + {Type: dotapi.CheckJSONKeyExists, Path: "package.json", Key: "dependencies.tailwindcss"}, + }, + }, + }, +} diff --git a/generators/tanstack_router/files/src/main.tsx b/generators/tanstack_router/files/src/main.tsx new file mode 100644 index 0000000..41e3ac9 --- /dev/null +++ b/generators/tanstack_router/files/src/main.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { RouterProvider, createRouter } from "@tanstack/react-router"; +import { routeTree } from "./routeTree.gen"; + +const router = createRouter({ routeTree }); + +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } +} + +const rootElement = document.getElementById("root"); + +if (!rootElement) { + throw new Error("Root element #root not found"); +} + +ReactDOM.createRoot(rootElement).render( + + + , +); diff --git a/generators/tanstack_router/files/src/routes/__root.tsx b/generators/tanstack_router/files/src/routes/__root.tsx new file mode 100644 index 0000000..72414ec --- /dev/null +++ b/generators/tanstack_router/files/src/routes/__root.tsx @@ -0,0 +1,5 @@ +import { createRootRoute, Outlet } from "@tanstack/react-router"; + +export const Route = createRootRoute({ + component: () => , +}); diff --git a/generators/tanstack_router/files/src/routes/index.tsx b/generators/tanstack_router/files/src/routes/index.tsx new file mode 100644 index 0000000..bc58c26 --- /dev/null +++ b/generators/tanstack_router/files/src/routes/index.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/")({ + component: () =>

Home

, +}); diff --git a/generators/tanstack_router/files/vite.config.ts b/generators/tanstack_router/files/vite.config.ts new file mode 100644 index 0000000..d32a971 --- /dev/null +++ b/generators/tanstack_router/files/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import { TanStackRouterVite } from "@tanstack/router-plugin/vite"; + +export default defineConfig({ + plugins: [TanStackRouterVite({ autoCodeSplitting: true }), react()], +}); diff --git a/generators/tanstack_router/generator.go b/generators/tanstack_router/generator.go new file mode 100644 index 0000000..9d8494b --- /dev/null +++ b/generators/tanstack_router/generator.go @@ -0,0 +1,37 @@ +package tanstackrouter + +import ( + "embed" + + "github.com/version14/dot/internal/render" + "github.com/version14/dot/internal/state" + "github.com/version14/dot/pkg/dotapi" +) + +//go:embed all:files +var fs embed.FS + +type Generator struct{} + +func New() *Generator { return &Generator{} } + +func (g *Generator) Name() string { return Manifest.Name } +func (g *Generator) Version() string { return Manifest.Version } + +func (g *Generator) Generate(ctx *dotapi.Context) error { + if err := ctx.State.UpdateJSON("package.json", func(d *state.JSONDoc) error { + d.Merge(map[string]interface{}{ + "dependencies": map[string]interface{}{ + "@tanstack/react-router": "^1.170.9", + }, + "devDependencies": map[string]interface{}{ + "@tanstack/router-plugin": "^1.168.12", + }, + }) + return nil + }); err != nil { + return err + } + + return render.NewLocalFolderRenderer(ctx.State).Render(fs, nil) +} diff --git a/generators/tanstack_router/manifest.go b/generators/tanstack_router/manifest.go new file mode 100644 index 0000000..99fcca3 --- /dev/null +++ b/generators/tanstack_router/manifest.go @@ -0,0 +1,32 @@ +package tanstackrouter + +import "github.com/version14/dot/pkg/dotapi" + +var Manifest = dotapi.Manifest{ + Name: "tanstack_router", + Version: "0.2.0", + Description: "TanStack Router type-safe file-based routing for React+Vite", + DependsOn: []string{"react_app"}, + ConflictsWith: []string{"react_router_v7"}, + Outputs: []string{ + "src/routes/__root.tsx", + "src/routes/index.tsx", + }, + PostGenerationCommands: []dotapi.Command{ + {Cmd: "pnpm install --dangerously-allow-all-builds"}, + {Cmd: "pnpm exec vite build"}, + }, + TestCommands: []dotapi.Command{ + {Cmd: "pnpm exec tsc --noEmit"}, + {Cmd: "pnpm exec vite build"}, + }, + Validators: []dotapi.Validator{ + { + Name: "tanstack-router-files", + Checks: []dotapi.Check{ + {Type: dotapi.CheckFileExists, Path: "src/routes/__root.tsx"}, + {Type: dotapi.CheckJSONKeyExists, Path: "package.json", Key: "dependencies.@tanstack/react-router"}, + }, + }, + }, +} diff --git a/generators/theme_provider/files/src/hooks/useTheme.ts b/generators/theme_provider/files/src/hooks/useTheme.ts new file mode 100644 index 0000000..2affa50 --- /dev/null +++ b/generators/theme_provider/files/src/hooks/useTheme.ts @@ -0,0 +1 @@ +export { useThemeContext as useTheme } from "../providers/ThemeProvider"; diff --git a/generators/theme_provider/files/src/providers/ThemeProvider.tsx b/generators/theme_provider/files/src/providers/ThemeProvider.tsx new file mode 100644 index 0000000..faf1453 --- /dev/null +++ b/generators/theme_provider/files/src/providers/ThemeProvider.tsx @@ -0,0 +1,46 @@ +import React, { createContext, useContext, useEffect, useState } from "react"; + +type Theme = "light" | "dark" | "system"; + +interface ThemeContextValue { + theme: Theme; + setTheme: (theme: Theme) => void; +} + +const ThemeContext = createContext(null); + +export function ThemeProvider({ + children, + defaultTheme = "system", +}: { + children: React.ReactNode; + defaultTheme?: Theme; +}) { + const [theme, setThemeState] = useState( + () => (localStorage.getItem("theme") as Theme) ?? defaultTheme, + ); + + useEffect(() => { + const root = document.documentElement; + const resolved = + theme === "system" + ? window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light" + : theme; + root.setAttribute("data-theme", resolved); + localStorage.setItem("theme", theme); + }, [theme]); + + return ( + + {children} + + ); +} + +export function useThemeContext() { + const ctx = useContext(ThemeContext); + if (!ctx) throw new Error("useThemeContext must be used within ThemeProvider"); + return ctx; +} diff --git a/generators/theme_provider/files/src/styles/theme.css b/generators/theme_provider/files/src/styles/theme.css new file mode 100644 index 0000000..5703f82 --- /dev/null +++ b/generators/theme_provider/files/src/styles/theme.css @@ -0,0 +1,19 @@ +:root { + --color-background: #ffffff; + --color-foreground: #0a0a0a; + --color-primary: #3b82f6; + --color-primary-foreground: #ffffff; + --color-muted: #f4f4f5; + --color-muted-foreground: #71717a; + --color-border: #e4e4e7; +} + +[data-theme="dark"] { + --color-background: #0a0a0a; + --color-foreground: #f4f4f5; + --color-primary: #60a5fa; + --color-primary-foreground: #0a0a0a; + --color-muted: #27272a; + --color-muted-foreground: #a1a1aa; + --color-border: #27272a; +} diff --git a/generators/theme_provider/generator.go b/generators/theme_provider/generator.go new file mode 100644 index 0000000..6576c82 --- /dev/null +++ b/generators/theme_provider/generator.go @@ -0,0 +1,22 @@ +package themeprovider + +import ( + "embed" + + "github.com/version14/dot/internal/render" + "github.com/version14/dot/pkg/dotapi" +) + +//go:embed all:files +var fs embed.FS + +type Generator struct{} + +func New() *Generator { return &Generator{} } + +func (g *Generator) Name() string { return Manifest.Name } +func (g *Generator) Version() string { return Manifest.Version } + +func (g *Generator) Generate(ctx *dotapi.Context) error { + return render.NewLocalFolderRenderer(ctx.State).Render(fs, nil) +} diff --git a/generators/theme_provider/manifest.go b/generators/theme_provider/manifest.go new file mode 100644 index 0000000..e4cb4eb --- /dev/null +++ b/generators/theme_provider/manifest.go @@ -0,0 +1,27 @@ +package themeprovider + +import "github.com/version14/dot/pkg/dotapi" + +var Manifest = dotapi.Manifest{ + Name: "theme_provider", + Version: "0.1.0", + Description: "Custom ThemeProvider with CSS variables for light/dark mode", + DependsOn: []string{"typescript_base"}, + Outputs: []string{ + "src/providers/ThemeProvider.tsx", + "src/hooks/useTheme.ts", + "src/styles/theme.css", + }, + TestCommands: []dotapi.Command{ + {Cmd: "pnpm exec tsc --noEmit"}, + }, + Validators: []dotapi.Validator{ + { + Name: "theme-provider-files", + Checks: []dotapi.Check{ + {Type: dotapi.CheckFileExists, Path: "src/providers/ThemeProvider.tsx"}, + {Type: dotapi.CheckFileExists, Path: "src/styles/theme.css"}, + }, + }, + }, +} diff --git a/generators/version14_ui/files/src/components/V14Example.tsx b/generators/version14_ui/files/src/components/V14Example.tsx new file mode 100644 index 0000000..44e7780 --- /dev/null +++ b/generators/version14_ui/files/src/components/V14Example.tsx @@ -0,0 +1,7 @@ +export default function V14Example() { + return ( +
+ @version14/ui is installed. Import components from "@version14/ui". +
+ ); +} diff --git a/generators/version14_ui/generator.go b/generators/version14_ui/generator.go new file mode 100644 index 0000000..ab311ef --- /dev/null +++ b/generators/version14_ui/generator.go @@ -0,0 +1,34 @@ +package version14ui + +import ( + "embed" + + "github.com/version14/dot/internal/render" + "github.com/version14/dot/internal/state" + "github.com/version14/dot/pkg/dotapi" +) + +//go:embed all:files +var fs embed.FS + +type Generator struct{} + +func New() *Generator { return &Generator{} } + +func (g *Generator) Name() string { return Manifest.Name } +func (g *Generator) Version() string { return Manifest.Version } + +func (g *Generator) Generate(ctx *dotapi.Context) error { + if err := ctx.State.UpdateJSON("package.json", func(d *state.JSONDoc) error { + d.Merge(map[string]interface{}{ + "dependencies": map[string]interface{}{ + "@version14/ui": "latest", + }, + }) + return nil + }); err != nil { + return err + } + + return render.NewLocalFolderRenderer(ctx.State).Render(fs, nil) +} diff --git a/generators/version14_ui/manifest.go b/generators/version14_ui/manifest.go new file mode 100644 index 0000000..ac9aa04 --- /dev/null +++ b/generators/version14_ui/manifest.go @@ -0,0 +1,28 @@ +package version14ui + +import "github.com/version14/dot/pkg/dotapi" + +var Manifest = dotapi.Manifest{ + Name: "version14_ui", + Version: "0.1.0", + Description: "@version14/ui component library", + DependsOn: []string{"typescript_base"}, + ConflictsWith: []string{"shadcn_ui", "ark_ui"}, + Outputs: []string{ + "src/components/V14Example.tsx", + }, + PostGenerationCommands: []dotapi.Command{ + {Cmd: "pnpm install --dangerously-allow-all-builds"}, + }, + TestCommands: []dotapi.Command{ + {Cmd: "pnpm exec tsc --noEmit"}, + }, + Validators: []dotapi.Validator{ + { + Name: "version14-ui-files", + Checks: []dotapi.Check{ + {Type: dotapi.CheckJSONKeyExists, Path: "package.json", Key: "dependencies.@version14/ui"}, + }, + }, + }, +} diff --git a/generators/vitest_testing_library/files/src/test/App.test.tsx b/generators/vitest_testing_library/files/src/test/App.test.tsx new file mode 100644 index 0000000..7a396e1 --- /dev/null +++ b/generators/vitest_testing_library/files/src/test/App.test.tsx @@ -0,0 +1,10 @@ +import { render } from "@testing-library/react"; +import { describe, it, expect } from "vitest"; +import App from "../App"; + +describe("App", () => { + it("renders without crashing", () => { + render(); + expect(document.body).toBeDefined(); + }); +}); diff --git a/generators/vitest_testing_library/files/src/test/setup.ts b/generators/vitest_testing_library/files/src/test/setup.ts new file mode 100644 index 0000000..d0de870 --- /dev/null +++ b/generators/vitest_testing_library/files/src/test/setup.ts @@ -0,0 +1 @@ +import "@testing-library/jest-dom"; diff --git a/generators/vitest_testing_library/files/vitest.config.ts b/generators/vitest_testing_library/files/vitest.config.ts new file mode 100644 index 0000000..6a6c106 --- /dev/null +++ b/generators/vitest_testing_library/files/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vitest/config"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + test: { + globals: true, + environment: "jsdom", + exclude: ["**/e2e/**", "**/node_modules/**"], + setupFiles: ["./src/test/setup.ts"], + }, +}); diff --git a/generators/vitest_testing_library/generator.go b/generators/vitest_testing_library/generator.go new file mode 100644 index 0000000..ed8ce20 --- /dev/null +++ b/generators/vitest_testing_library/generator.go @@ -0,0 +1,44 @@ +package vitesttestinglibrary + +import ( + "embed" + + "github.com/version14/dot/internal/render" + "github.com/version14/dot/internal/state" + "github.com/version14/dot/pkg/dotapi" +) + +//go:embed all:files +var fs embed.FS + +type Generator struct{} + +func New() *Generator { return &Generator{} } + +func (g *Generator) Name() string { return Manifest.Name } +func (g *Generator) Version() string { return Manifest.Version } + +func (g *Generator) Generate(ctx *dotapi.Context) error { + if err := ctx.State.UpdateJSON("package.json", func(d *state.JSONDoc) error { + d.Merge(map[string]interface{}{ + "scripts": map[string]interface{}{ + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + }, + "devDependencies": map[string]interface{}{ + "vitest": "^4.1.7", + "@vitest/coverage-v8": "^4.1.7", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@testing-library/jest-dom": "^6.9.1", + "jsdom": "^29.1.1", + }, + }) + return nil + }); err != nil { + return err + } + + return render.NewLocalFolderRenderer(ctx.State).Render(fs, nil) +} diff --git a/generators/vitest_testing_library/manifest.go b/generators/vitest_testing_library/manifest.go new file mode 100644 index 0000000..f2ef1ae --- /dev/null +++ b/generators/vitest_testing_library/manifest.go @@ -0,0 +1,31 @@ +package vitesttestinglibrary + +import "github.com/version14/dot/pkg/dotapi" + +var Manifest = dotapi.Manifest{ + Name: "vitest_testing_library", + Version: "0.2.0", + Description: "Vitest + React Testing Library for React+Vite projects", + DependsOn: []string{"react_app"}, + Outputs: []string{ + "vitest.config.ts", + "src/test/setup.ts", + "src/test/App.test.tsx", + }, + PostGenerationCommands: []dotapi.Command{ + {Cmd: "pnpm install --dangerously-allow-all-builds"}, + }, + TestCommands: []dotapi.Command{ + {Cmd: "pnpm exec vitest run"}, + }, + Validators: []dotapi.Validator{ + { + Name: "vitest-testing-library-files", + Checks: []dotapi.Check{ + {Type: dotapi.CheckFileExists, Path: "vitest.config.ts"}, + {Type: dotapi.CheckJSONKeyExists, Path: "package.json", Key: "devDependencies.vitest"}, + {Type: dotapi.CheckJSONKeyExists, Path: "package.json", Key: "scripts.test"}, + }, + }, + }, +} diff --git a/generators/zustand_setup/files/src/stores/counter.store.ts b/generators/zustand_setup/files/src/stores/counter.store.ts new file mode 100644 index 0000000..9aff78d --- /dev/null +++ b/generators/zustand_setup/files/src/stores/counter.store.ts @@ -0,0 +1,15 @@ +import { create } from "zustand"; + +interface CounterState { + count: number; + increment: () => void; + decrement: () => void; + reset: () => void; +} + +export const useCounterStore = create((set) => ({ + count: 0, + increment: () => set((state) => ({ count: state.count + 1 })), + decrement: () => set((state) => ({ count: state.count - 1 })), + reset: () => set({ count: 0 }), +})); diff --git a/generators/zustand_setup/generator.go b/generators/zustand_setup/generator.go new file mode 100644 index 0000000..a831f5f --- /dev/null +++ b/generators/zustand_setup/generator.go @@ -0,0 +1,34 @@ +package zustandsetup + +import ( + "embed" + + "github.com/version14/dot/internal/render" + "github.com/version14/dot/internal/state" + "github.com/version14/dot/pkg/dotapi" +) + +//go:embed all:files +var fs embed.FS + +type Generator struct{} + +func New() *Generator { return &Generator{} } + +func (g *Generator) Name() string { return Manifest.Name } +func (g *Generator) Version() string { return Manifest.Version } + +func (g *Generator) Generate(ctx *dotapi.Context) error { + if err := ctx.State.UpdateJSON("package.json", func(d *state.JSONDoc) error { + d.Merge(map[string]interface{}{ + "dependencies": map[string]interface{}{ + "zustand": "^5.0.14", + }, + }) + return nil + }); err != nil { + return err + } + + return render.NewLocalFolderRenderer(ctx.State).Render(fs, nil) +} diff --git a/generators/zustand_setup/manifest.go b/generators/zustand_setup/manifest.go new file mode 100644 index 0000000..8163a46 --- /dev/null +++ b/generators/zustand_setup/manifest.go @@ -0,0 +1,29 @@ +package zustandsetup + +import "github.com/version14/dot/pkg/dotapi" + +var Manifest = dotapi.Manifest{ + Name: "zustand_setup", + Version: "0.2.0", + Description: "Zustand global state management with example counter store", + DependsOn: []string{"typescript_base"}, + ConflictsWith: []string{"jotai_setup"}, + Outputs: []string{ + "src/stores/counter.store.ts", + }, + PostGenerationCommands: []dotapi.Command{ + {Cmd: "pnpm install --dangerously-allow-all-builds"}, + }, + TestCommands: []dotapi.Command{ + {Cmd: "pnpm exec tsc --noEmit"}, + }, + Validators: []dotapi.Validator{ + { + Name: "zustand-setup-files", + Checks: []dotapi.Check{ + {Type: dotapi.CheckFileExists, Path: "src/stores/counter.store.ts"}, + {Type: dotapi.CheckJSONKeyExists, Path: "package.json", Key: "dependencies.zustand"}, + }, + }, + }, +} diff --git a/internal/cli/form_walker.go b/internal/cli/form_walker.go index aa2355c..f145533 100644 --- a/internal/cli/form_walker.go +++ b/internal/cli/form_walker.go @@ -207,9 +207,12 @@ func (s *liveStore) partialContext() *flow.FlowContext { // formSlot is one node collected during the pre-walk. It pairs a question with // the set of pathConditions (OR-ed) that determine when it is visible. +// orderDeps are separate: they only constrain display order and never affect +// whether the slot is visible. type formSlot struct { question flow.Question conditions []pathCond + orderDeps []string } // loopBarrier marks a LoopQuestion in the slot list so the runner can find it @@ -219,6 +222,11 @@ type loopBarrier struct { question *flow.LoopQuestion } +type walkVisit struct { + cond pathCond + orderDeps []string +} + // ---------------------------------------------------------------------------- // formWalker — DFS pre-walk of the flow graph // ---------------------------------------------------------------------------- @@ -243,7 +251,11 @@ type formWalker struct { slots []*formSlot visited map[string]int // questionID → slot index - loops []*loopBarrier + // ifVisited tracks walk states already expanded for non-UI IfQuestion nodes. + // Without this, the same IfQuestion can be re-expanded many times from merged + // upstream branches, causing path explosion even when downstream nodes are deduped. + ifVisited map[string][]walkVisit + loops []*loopBarrier } func newFormWalker(hooks *flow.HookRegistry, fragments *flow.FragmentRegistry) *formWalker { @@ -252,23 +264,38 @@ func newFormWalker(hooks *flow.HookRegistry, fragments *flow.FragmentRegistry) * hooks: hooks, fragments: fragments, visited: make(map[string]int), + ifVisited: make(map[string][]walkVisit), } } func (w *formWalker) walk(root flow.Question) { - w.walkQ(root, nil) + w.walkQ(root, nil, nil) + w.orderSlotsByDeps() } -func (w *formWalker) walkQ(q flow.Question, cond pathCond) { +func (w *formWalker) walkQ(q flow.Question, cond pathCond, orderDeps []string) { if q == nil { return } // Apply Replace injection (first registered Replace wins). q = w.applyReplace(q) + id := q.ID() // IfQuestion has no UI: thread its condition into downstream nodes and return. if ifq, ok := q.(*flow.IfQuestion); ok { + // Deduplicate non-UI IfQuestion expansion by (questionID, condition). + // This mirrors slot-level dedupe for visible questions. + for _, existing := range w.ifVisited[id] { + if existing.cond.equals(cond) && stringSlicesEqual(existing.orderDeps, orderDeps) { + return + } + } + w.ifVisited[id] = append(w.ifVisited[id], walkVisit{ + cond: cloneCond(cond), + orderDeps: cloneStringSlice(orderDeps), + }) + thenCond := appendCond(cond, condClause{ kind: clauseIfEq, boolVal: true, @@ -279,28 +306,32 @@ func (w *formWalker) walkQ(q flow.Question, cond pathCond) { boolVal: false, ifCond: ifq.Condition, }) - w.walkNext(ifq.Then, thenCond) - w.walkNext(ifq.Else, elseCond) + w.walkNext(ifq.Then, thenCond, orderDeps) + w.walkNext(ifq.Else, elseCond, orderDeps) return } - id := q.ID() - // Merge condition if already visited. idx, alreadyVisited := w.visited[id] if alreadyVisited { // Avoid redundant re-walks if we already have this exact condition. for _, existing := range w.slots[idx].conditions { if existing.equals(cond) { + w.slots[idx].orderDeps = appendOrderDeps(w.slots[idx].orderDeps, orderDeps...) return } } w.slots[idx].conditions = append(w.slots[idx].conditions, cond) + w.slots[idx].orderDeps = appendOrderDeps(w.slots[idx].orderDeps, orderDeps...) } else { // Register slot. idx = len(w.slots) w.visited[id] = idx - slot := &formSlot{question: q, conditions: []pathCond{cond}} + slot := &formSlot{ + question: q, + conditions: []pathCond{cond}, + orderDeps: cloneStringSlice(orderDeps), + } w.slots = append(w.slots, slot) } @@ -315,37 +346,55 @@ func (w *formWalker) walkQ(q flow.Question, cond pathCond) { case *flow.OptionQuestion: // Walk inserts first (same condition as target — always shown after target). for _, ins := range inserts { - w.walkQ(ins, cloneCond(cond)) + w.walkQ(ins, cloneCond(cond), appendOrderDep(orderDeps, id)) } + nextOrderDeps := appendOrderDep(orderDeps, id) if typed.Multiple { - w.walkNext(typed.Next_, cond) + w.walkNext(typed.Next_, cond, nextOrderDeps) } else { // Merge plugin-added options so we walk their branches too. merged := w.mergeOptions(typed) + // When every option lands on the same next target, downstream reachability + // is independent of the selected value. Walking one branch avoids + // exponential condition growth without changing visibility semantics. + if sameOptionsNextTarget(merged) { + if len(merged) > 0 { + w.walkNext(merged[0].Next, cond, nextOrderDeps) + } + return + } for _, opt := range merged { branchCond := appendCond(cond, condClause{ kind: clauseSelectEq, questionID: id, value: opt.Value, }) - w.walkNext(opt.Next, branchCond) + w.walkNext(opt.Next, branchCond, nextOrderDeps) } } case *flow.ConfirmQuestion: for _, ins := range inserts { - w.walkQ(ins, cloneCond(cond)) + w.walkQ(ins, cloneCond(cond), appendOrderDep(orderDeps, id)) + } + nextOrderDeps := appendOrderDep(orderDeps, id) + // If both branches land on the same next target, branching is + // condition-independent. Walking both sides would duplicate downstream + // conditions exponentially (true/false clauses for the same path). + if sameNextTarget(typed.Then, typed.Else) { + w.walkNext(typed.Then, cond, nextOrderDeps) + return } thenCond := appendCond(cond, condClause{kind: clauseConfirmEq, questionID: id, boolVal: true}) elseCond := appendCond(cond, condClause{kind: clauseConfirmEq, questionID: id, boolVal: false}) - w.walkNext(typed.Then, thenCond) - w.walkNext(typed.Else, elseCond) + w.walkNext(typed.Then, thenCond, nextOrderDeps) + w.walkNext(typed.Else, elseCond, nextOrderDeps) case *flow.TextQuestion: for _, ins := range inserts { - w.walkQ(ins, cloneCond(cond)) + w.walkQ(ins, cloneCond(cond), appendOrderDep(orderDeps, id)) } - w.walkNext(typed.Next_, cond) + w.walkNext(typed.Next_, cond, appendOrderDep(orderDeps, id)) case *flow.LoopQuestion: // Record the barrier only once. @@ -360,12 +409,12 @@ func (w *formWalker) walkQ(q flow.Question, cond pathCond) { } } -func (w *formWalker) walkNext(next *flow.Next, cond pathCond) { +func (w *formWalker) walkNext(next *flow.Next, cond pathCond, orderDeps []string) { if next == nil || next.End { return } if next.Question != nil { - w.walkQ(next.Question, cond) + w.walkQ(next.Question, cond, orderDeps) return } if next.Fragment != "" && w.fragments != nil { @@ -373,7 +422,88 @@ func (w *formWalker) walkNext(next *flow.Next, cond pathCond) { // handle nil gracefully (they cannot branch on live answers here). resolved := w.fragments.Resolve(next.Fragment, nil) if resolved != nil { - w.walkNext(resolved, cond) + w.walkNext(resolved, cond, orderDeps) + } + } +} + +// orderSlotsByDeps keeps the pre-built form in a usable prompt order. +// +// DFS can discover a converged downstream question through an early branch before +// it has discovered questions from later branches. Huh does not naturally jump +// backward to newly visible earlier groups, so each slot carries explicit +// ordering dependencies gathered from the graph edges used to reach it. +func (w *formWalker) orderSlotsByDeps() { + if len(w.slots) < 2 { + return + } + + idToIndex := make(map[string]int, len(w.slots)) + for i, slot := range w.slots { + idToIndex[slot.question.ID()] = i + } + + deps := make([]map[int]bool, len(w.slots)) + for i, slot := range w.slots { + for _, depID := range slot.orderDeps { + depIdx, ok := idToIndex[depID] + if !ok || depIdx == i { + continue + } + if deps[i] == nil { + deps[i] = make(map[int]bool) + } + deps[i][depIdx] = true + } + } + + done := make([]bool, len(w.slots)) + ordered := make([]*formSlot, 0, len(w.slots)) + oldToNew := make(map[int]int, len(w.slots)) + + for len(ordered) < len(w.slots) { + nextIdx := -1 + for i := range w.slots { + if done[i] { + continue + } + ready := true + for depIdx := range deps[i] { + if !done[depIdx] { + ready = false + break + } + } + if ready { + nextIdx = i + break + } + } + + // A cycle should not be possible in a valid question graph, but keep the + // original relative order instead of dropping slots if a plugin creates one. + if nextIdx == -1 { + for i := range w.slots { + if !done[i] { + nextIdx = i + break + } + } + } + + done[nextIdx] = true + oldToNew[nextIdx] = len(ordered) + ordered = append(ordered, w.slots[nextIdx]) + } + + w.slots = ordered + w.visited = make(map[string]int, len(w.slots)) + for i, slot := range w.slots { + w.visited[slot.question.ID()] = i + } + for _, barrier := range w.loops { + if newIdx, ok := oldToNew[barrier.slotIdx]; ok { + barrier.slotIdx = newIdx } } } @@ -424,3 +554,79 @@ func appendCond(c pathCond, clause condClause) pathCond { out[len(c)] = clause return out } + +func cloneStringSlice(in []string) []string { + if len(in) == 0 { + return nil + } + out := make([]string, len(in)) + copy(out, in) + return out +} + +func appendOrderDep(deps []string, dep string) []string { + return appendOrderDeps(deps, dep) +} + +func appendOrderDeps(deps []string, additions ...string) []string { + out := cloneStringSlice(deps) + for _, addition := range additions { + if addition == "" || stringSliceContains(out, addition) { + continue + } + out = append(out, addition) + } + return out +} + +func stringSliceContains(items []string, target string) bool { + for _, item := range items { + if item == target { + return true + } + } + return false +} + +func stringSlicesEqual(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +// sameNextTarget reports whether two Next edges resolve to the same target. +// It is intentionally conservative: if we cannot prove equivalence, it returns false. +func sameNextTarget(a, b *flow.Next) bool { + if a == nil || b == nil { + return a == b + } + if a.End || b.End { + return a.End == b.End + } + if a.Fragment != "" || b.Fragment != "" { + return a.Fragment != "" && b.Fragment != "" && a.Fragment == b.Fragment + } + if a.Question == nil || b.Question == nil { + return a.Question == b.Question + } + return a.Question.ID() == b.Question.ID() +} + +func sameOptionsNextTarget(opts []*flow.Option) bool { + if len(opts) <= 1 { + return true + } + base := opts[0].Next + for i := 1; i < len(opts); i++ { + if !sameNextTarget(base, opts[i].Next) { + return false + } + } + return true +} diff --git a/internal/cli/form_walker_test.go b/internal/cli/form_walker_test.go index b8e7c63..6a007db 100644 --- a/internal/cli/form_walker_test.go +++ b/internal/cli/form_walker_test.go @@ -3,6 +3,7 @@ package cli import ( "testing" + "github.com/version14/dot/flows" "github.com/version14/dot/internal/flow" ) @@ -45,11 +46,35 @@ func TestFormWalkerDiamondConvergence(t *testing.T) { t.Fatal("Question C not found in walker slots") } - // C should be reachable via (A=a1 AND B=b1) OR (A=a2 AND B=b1) - if len(cSlot.conditions) < 2 { - t.Errorf("Expected at least 2 conditions for C, got %d", len(cSlot.conditions)) - for i, cond := range cSlot.conditions { - t.Logf("Condition %d: %v", i, cond) - } + // Both A options and B's single option converge to C, so C remains + // unconditionally visible after same-target branch collapse. + if len(cSlot.conditions) != 1 { + t.Fatalf("Expected 1 collapsed condition for C, got %d", len(cSlot.conditions)) + } + if buildHideFunc(cSlot.conditions, newLiveStore())() { + t.Fatalf("Expected collapsed condition to keep C visible, got %+v", cSlot.conditions[0]) + } +} + +func TestFormWalkerFrontendStylingBeforeState(t *testing.T) { + def := flows.FrontendFlow() + walker := newFormWalker(nil, nil) + walker.walk(def.Root) + + index := make(map[string]int, len(walker.slots)) + for i, slot := range walker.slots { + index[slot.question.ID()] = i + } + + stylingIdx, ok := index["frontend-styling"] + if !ok { + t.Fatal("frontend-styling not found in walker slots") + } + stateIdx, ok := index["frontend-state"] + if !ok { + t.Fatal("frontend-state not found in walker slots") + } + if stylingIdx > stateIdx { + t.Fatalf("frontend-styling should be before frontend-state, got styling=%d state=%d", stylingIdx, stateIdx) } } diff --git a/internal/cli/registry.go b/internal/cli/registry.go index 0da269f..b682aa9 100644 --- a/internal/cli/registry.go +++ b/internal/cli/registry.go @@ -3,17 +3,24 @@ package cli import ( "fmt" + analyticsga4 "github.com/version14/dot/generators/analytics_ga4" + analyticsplausible "github.com/version14/dot/generators/analytics_plausible" + arkui "github.com/version14/dot/generators/ark_ui" authbetterauth "github.com/version14/dot/generators/auth_better_auth" + authbetterauthfrontend "github.com/version14/dot/generators/auth_better_auth_frontend" authbetterauthschema "github.com/version14/dot/generators/auth_better_auth_schema" + authclerkfrontend "github.com/version14/dot/generators/auth_clerk_frontend" authjwtcleanarchmodule "github.com/version14/dot/generators/auth_jwt_clean_arch_module" authjwtmvcroute "github.com/version14/dot/generators/auth_jwt_mvc_route" authjwtusersschema "github.com/version14/dot/generators/auth_jwt_users_schema" authjwtvanilla "github.com/version14/dot/generators/auth_jwt_vanilla" + authvanillafrontend "github.com/version14/dot/generators/auth_vanilla_frontend" backendArchitectureCleanArchitecture "github.com/version14/dot/generators/backend_architecture_clean_architecture" backendArchitectureHexagonal "github.com/version14/dot/generators/backend_architecture_hexagonal_architecture" backendArchitectureMVC "github.com/version14/dot/generators/backend_architecture_mvc_architecture" baseproject "github.com/version14/dot/generators/base_project" biomeconfig "github.com/version14/dot/generators/biome_config" + cssmodules "github.com/version14/dot/generators/css_modules" decoratorscleanarchadapter "github.com/version14/dot/generators/decorators_clean_arch_adapter" decoratorshexagonaladapter "github.com/version14/dot/generators/decorators_hexagonal_adapter" decoratorsmvcadapter "github.com/version14/dot/generators/decorators_mvc_adapter" @@ -31,16 +38,35 @@ import ( expresssharederrors "github.com/version14/dot/generators/express_shared_errors" expressswaggerjsdoc "github.com/version14/dot/generators/express_swagger_jsdoc" expresstestsetup "github.com/version14/dot/generators/express_test_setup" + featureflagslocal "github.com/version14/dot/generators/feature_flags_local" + featureflagsposthog "github.com/version14/dot/generators/feature_flags_posthog" + featureflagsvercel "github.com/version14/dot/generators/feature_flags_vercel" + jotaisetup "github.com/version14/dot/generators/jotai_setup" monorepotsworkspaces "github.com/version14/dot/generators/monorepo_ts_workspaces" + nextjsbase "github.com/version14/dot/generators/nextjs_base" + pandacss "github.com/version14/dot/generators/panda_css" + playwrightsetup "github.com/version14/dot/generators/playwright_setup" pluginreposkeleton "github.com/version14/dot/generators/plugin_repo_skeleton" postgresdockercompose "github.com/version14/dot/generators/postgres_docker_compose" postgresenvexample "github.com/version14/dot/generators/postgres_env_example" prettierconfig "github.com/version14/dot/generators/prettier_config" prettierexpressrules "github.com/version14/dot/generators/prettier_express_rules" + prettierfrontendrules "github.com/version14/dot/generators/prettier_frontend_rules" prettiertypescriptdeps "github.com/version14/dot/generators/prettier_typescript_deps" reactapp "github.com/version14/dot/generators/react_app" + reactrouterv7 "github.com/version14/dot/generators/react_router_v7" + sentryfrontend "github.com/version14/dot/generators/sentry_frontend" + seoreact "github.com/version14/dot/generators/seo_react" + shadcnui "github.com/version14/dot/generators/shadcn_ui" + storybooksetup "github.com/version14/dot/generators/storybook_setup" + tailwindv4 "github.com/version14/dot/generators/tailwind_v4" + tanstackrouter "github.com/version14/dot/generators/tanstack_router" + themeprovider "github.com/version14/dot/generators/theme_provider" typescriptbase "github.com/version14/dot/generators/typescript_base" + version14ui "github.com/version14/dot/generators/version14_ui" + vitesttestinglibrary "github.com/version14/dot/generators/vitest_testing_library" zodvalidationdeps "github.com/version14/dot/generators/zod_validation_deps" + zustandsetup "github.com/version14/dot/generators/zustand_setup" "github.com/version14/dot/internal/generator" ) @@ -53,10 +79,52 @@ func builtinGeneratorEntries() []generator.Entry { {Manifest: baseproject.Manifest, Generator: baseproject.New()}, {Manifest: typescriptbase.Manifest, Generator: typescriptbase.New()}, {Manifest: reactapp.Manifest, Generator: reactapp.New()}, + {Manifest: nextjsbase.Manifest, Generator: nextjsbase.New()}, {Manifest: biomeconfig.Manifest, Generator: biomeconfig.New()}, {Manifest: monorepotsworkspaces.Manifest, Generator: monorepotsworkspaces.New()}, {Manifest: pluginreposkeleton.Manifest, Generator: pluginreposkeleton.New()}, + // Frontend — router + {Manifest: reactrouterv7.Manifest, Generator: reactrouterv7.New()}, + {Manifest: tanstackrouter.Manifest, Generator: tanstackrouter.New()}, + + // Frontend — UI library + {Manifest: shadcnui.Manifest, Generator: shadcnui.New()}, + {Manifest: arkui.Manifest, Generator: arkui.New()}, + {Manifest: version14ui.Manifest, Generator: version14ui.New()}, + + // Frontend — styling + {Manifest: tailwindv4.Manifest, Generator: tailwindv4.New()}, + {Manifest: cssmodules.Manifest, Generator: cssmodules.New()}, + {Manifest: pandacss.Manifest, Generator: pandacss.New()}, + + // Frontend — state + {Manifest: zustandsetup.Manifest, Generator: zustandsetup.New()}, + {Manifest: jotaisetup.Manifest, Generator: jotaisetup.New()}, + + // Frontend — testing + {Manifest: vitesttestinglibrary.Manifest, Generator: vitesttestinglibrary.New()}, + {Manifest: playwrightsetup.Manifest, Generator: playwrightsetup.New()}, + {Manifest: storybooksetup.Manifest, Generator: storybooksetup.New()}, + + // Frontend — auth modules + {Manifest: authclerkfrontend.Manifest, Generator: authclerkfrontend.New()}, + {Manifest: authbetterauthfrontend.Manifest, Generator: authbetterauthfrontend.New()}, + {Manifest: authvanillafrontend.Manifest, Generator: authvanillafrontend.New()}, + + // Frontend — modules + {Manifest: themeprovider.Manifest, Generator: themeprovider.New()}, + {Manifest: featureflagsposthog.Manifest, Generator: featureflagsposthog.New()}, + {Manifest: featureflagsvercel.Manifest, Generator: featureflagsvercel.New()}, + {Manifest: featureflagslocal.Manifest, Generator: featureflagslocal.New()}, + {Manifest: sentryfrontend.Manifest, Generator: sentryfrontend.New()}, + {Manifest: analyticsga4.Manifest, Generator: analyticsga4.New()}, + {Manifest: analyticsplausible.Manifest, Generator: analyticsplausible.New()}, + {Manifest: seoreact.Manifest, Generator: seoreact.New()}, + + // Frontend — formatter rules + {Manifest: prettierfrontendrules.Manifest, Generator: prettierfrontendrules.New()}, + // Backend architecture {Manifest: backendArchitectureCleanArchitecture.Manifest, Generator: backendArchitectureCleanArchitecture.New()}, {Manifest: backendArchitectureMVC.Manifest, Generator: backendArchitectureMVC.New()}, diff --git a/internal/state/virtual.go b/internal/state/virtual.go index 5f8e994..f872534 100644 --- a/internal/state/virtual.go +++ b/internal/state/virtual.go @@ -101,6 +101,22 @@ func (s *VirtualProjectState) WriteFile(path string, content []byte, ct ContentT s.writeRaw(s.np(path), content, ct) } +// AppendFile appends content to a raw file, creating it if absent. +// A newline is added between existing content and new content when needed. +func (s *VirtualProjectState) AppendFile(path string, content []byte) { + full := s.np(path) + existing, ok := s.Files[full] + if !ok || len(existing.Content) == 0 { + s.writeRaw(full, content, ContentRaw) + return + } + combined := append([]byte(nil), existing.Content...) + if combined[len(combined)-1] != '\n' { + combined = append(combined, '\n') + } + s.writeRaw(full, append(combined, content...), ContentRaw) +} + func (s *VirtualProjectState) GetFile(path string) (*FileNode, bool) { f, ok := s.Files[s.np(path)] return f, ok diff --git a/tools/test-flow/testdata/202605270001_frontend_react_vite_minimal.json b/tools/test-flow/testdata/202605270001_frontend_react_vite_minimal.json new file mode 100644 index 0000000..ea4624f --- /dev/null +++ b/tools/test-flow/testdata/202605270001_frontend_react_vite_minimal.json @@ -0,0 +1,47 @@ +{ + "name": "frontend_react_vite_minimal", + "flow_id": "frontend", + "answers": { + "project_name": "frontend-react-vite-minimal", + "framework": "react-vite", + "frontend-router": "none", + "ui-library": "none", + "frontend-styling": "tailwind", + "frontend-state": "none", + "frontend-formatter": "biome", + "frontend-linter": "biome", + "include-vitest": false, + "include-playwright": false, + "include-storybook": false, + "include-auth": false, + "include-theme": false, + "include-feature-flags": false, + "include-sentry": false, + "include-analytics": false, + "include-seo": false, + "confirm-generate": true + }, + "expected_visited": [ + "project_name", + "framework", + "frontend-router", + "ui-library", + "frontend-styling", + "frontend-state", + "frontend-formatter", + "frontend-linter", + "check-vitest-available", + "include-vitest", + "include-playwright", + "include-storybook", + "include-auth", + "include-theme", + "include-feature-flags", + "include-sentry", + "include-analytics", + "include-seo", + "confirm-generate" + ], + "skip_post_commands": false, + "skip_test_commands": false +} diff --git a/tools/test-flow/testdata/202605270002_frontend_react_vite_tanstack_shadcn_modules.json b/tools/test-flow/testdata/202605270002_frontend_react_vite_tanstack_shadcn_modules.json new file mode 100644 index 0000000..e8f5bac --- /dev/null +++ b/tools/test-flow/testdata/202605270002_frontend_react_vite_tanstack_shadcn_modules.json @@ -0,0 +1,51 @@ +{ + "name": "frontend_react_vite_tanstack_shadcn_modules", + "flow_id": "frontend", + "answers": { + "project_name": "frontend-react-vite-tanstack-shadcn-modules", + "framework": "react-vite", + "frontend-router": "tanstack-router", + "ui-library": "shadcn", + "frontend-state": "zustand", + "frontend-formatter": "biome", + "frontend-linter": "biome", + "include-vitest": true, + "include-playwright": true, + "include-storybook": true, + "include-auth": true, + "auth-provider": "clerk", + "include-theme": true, + "include-feature-flags": true, + "feature-flags-provider": "posthog", + "include-sentry": true, + "include-analytics": true, + "analytics-provider": "posthog", + "include-seo": true, + "confirm-generate": true + }, + "expected_visited": [ + "project_name", + "framework", + "frontend-router", + "ui-library", + "frontend-state", + "frontend-formatter", + "frontend-linter", + "check-vitest-available", + "include-vitest", + "include-playwright", + "include-storybook", + "include-auth", + "auth-provider", + "include-theme", + "include-feature-flags", + "feature-flags-provider", + "include-sentry", + "include-analytics", + "analytics-provider", + "include-seo", + "confirm-generate" + ], + "skip_post_commands": false, + "skip_test_commands": false +} diff --git a/tools/test-flow/testdata/202605270003_frontend_next_panda_prettier_modules.json b/tools/test-flow/testdata/202605270003_frontend_next_panda_prettier_modules.json new file mode 100644 index 0000000..c3bb4e6 --- /dev/null +++ b/tools/test-flow/testdata/202605270003_frontend_next_panda_prettier_modules.json @@ -0,0 +1,49 @@ +{ + "name": "frontend_next_panda_prettier_modules", + "flow_id": "frontend", + "answers": { + "project_name": "frontend-next-panda-prettier-modules", + "framework": "next", + "ui-library": "arkui", + "frontend-styling": "panda-css", + "frontend-state": "jotai", + "frontend-formatter": "prettier", + "frontend-linter": "prettier", + "include-playwright": true, + "include-storybook": true, + "include-auth": true, + "auth-provider": "better-auth", + "include-theme": true, + "include-feature-flags": true, + "feature-flags-provider": "vercel", + "include-sentry": true, + "include-analytics": true, + "analytics-provider": "plausible", + "include-seo": true, + "confirm-generate": true + }, + "expected_visited": [ + "project_name", + "framework", + "ui-library", + "frontend-styling", + "frontend-state", + "frontend-formatter", + "frontend-linter", + "check-vitest-available", + "include-playwright", + "include-storybook", + "include-auth", + "auth-provider", + "include-theme", + "include-feature-flags", + "feature-flags-provider", + "include-sentry", + "include-analytics", + "analytics-provider", + "include-seo", + "confirm-generate" + ], + "skip_post_commands": false, + "skip_test_commands": false +} diff --git a/tools/test-flow/testdata/202605270004_frontend_react_router_css_modules_prettier.json b/tools/test-flow/testdata/202605270004_frontend_react_router_css_modules_prettier.json new file mode 100644 index 0000000..750650f --- /dev/null +++ b/tools/test-flow/testdata/202605270004_frontend_react_router_css_modules_prettier.json @@ -0,0 +1,53 @@ +{ + "name": "frontend_react_router_css_modules_prettier", + "flow_id": "frontend", + "answers": { + "project_name": "frontend-react-router-css-modules-prettier", + "framework": "react-vite", + "frontend-router": "react-router", + "ui-library": "version14", + "frontend-styling": "css-modules", + "frontend-state": "none", + "frontend-formatter": "prettier", + "frontend-linter": "prettier", + "include-vitest": false, + "include-playwright": false, + "include-storybook": false, + "include-auth": true, + "auth-provider": "vanilla", + "include-theme": false, + "include-feature-flags": true, + "feature-flags-provider": "local", + "include-sentry": false, + "include-analytics": true, + "analytics-provider": "ga4", + "include-seo": true, + "confirm-generate": true + }, + "expected_visited": [ + "project_name", + "framework", + "frontend-router", + "ui-library", + "frontend-styling", + "frontend-state", + "frontend-formatter", + "frontend-linter", + "check-vitest-available", + "include-vitest", + "include-playwright", + "include-storybook", + "include-auth", + "auth-provider", + "include-theme", + "include-feature-flags", + "feature-flags-provider", + "include-sentry", + "include-analytics", + "analytics-provider", + "include-seo", + "confirm-generate" + ], + "skip_post_commands": false, + "skip_test_commands": false +} diff --git a/tools/test-flow/testdata/202605280001_frontend_react_vite_minimal.json b/tools/test-flow/testdata/202605280001_frontend_react_vite_minimal.json new file mode 100644 index 0000000..0ac76da --- /dev/null +++ b/tools/test-flow/testdata/202605280001_frontend_react_vite_minimal.json @@ -0,0 +1,47 @@ +{ + "name": "frontend_react_vite_minimal", + "flow_id": "frontend", + "answers": { + "project_name": "my-app", + "framework": "react-vite", + "frontend-router": "none", + "ui-library": "none", + "frontend-styling": "tailwind", + "frontend-state": "none", + "frontend-formatter": "biome", + "frontend-linter": "biome", + "include-vitest": false, + "include-playwright": false, + "include-storybook": false, + "include-auth": false, + "include-theme": false, + "include-feature-flags": false, + "include-sentry": false, + "include-analytics": false, + "include-seo": false, + "confirm-generate": true + }, + "expected_visited": [ + "project_name", + "framework", + "frontend-router", + "ui-library", + "frontend-styling", + "frontend-state", + "frontend-formatter", + "frontend-linter", + "check-vitest-available", + "include-vitest", + "include-playwright", + "include-storybook", + "include-auth", + "include-theme", + "include-feature-flags", + "include-sentry", + "include-analytics", + "include-seo", + "confirm-generate" + ], + "skip_post_commands": false, + "skip_test_commands": false +} diff --git a/tools/test-flow/testdata/202605280002_frontend_next_minimal.json b/tools/test-flow/testdata/202605280002_frontend_next_minimal.json new file mode 100644 index 0000000..866ccab --- /dev/null +++ b/tools/test-flow/testdata/202605280002_frontend_next_minimal.json @@ -0,0 +1,43 @@ +{ + "name": "frontend_next_minimal", + "flow_id": "frontend", + "answers": { + "project_name": "my-next-app", + "framework": "next", + "ui-library": "none", + "frontend-styling": "tailwind", + "frontend-state": "none", + "frontend-formatter": "biome", + "frontend-linter": "biome", + "include-playwright": false, + "include-storybook": false, + "include-auth": false, + "include-theme": false, + "include-feature-flags": false, + "include-sentry": false, + "include-analytics": false, + "include-seo": false, + "confirm-generate": true + }, + "expected_visited": [ + "project_name", + "framework", + "ui-library", + "frontend-styling", + "frontend-state", + "frontend-formatter", + "frontend-linter", + "check-vitest-available", + "include-playwright", + "include-storybook", + "include-auth", + "include-theme", + "include-feature-flags", + "include-sentry", + "include-analytics", + "include-seo", + "confirm-generate" + ], + "skip_post_commands": false, + "skip_test_commands": false +} diff --git a/tools/test-flow/testdata/202605280003_frontend_react_vite_shadcn_clerk_all_tests.json b/tools/test-flow/testdata/202605280003_frontend_react_vite_shadcn_clerk_all_tests.json new file mode 100644 index 0000000..bd156af --- /dev/null +++ b/tools/test-flow/testdata/202605280003_frontend_react_vite_shadcn_clerk_all_tests.json @@ -0,0 +1,49 @@ +{ + "name": "frontend_react_vite_shadcn_clerk_all_tests", + "flow_id": "frontend", + "answers": { + "project_name": "my-app", + "framework": "react-vite", + "frontend-router": "react-router", + "ui-library": "shadcn", + "frontend-state": "zustand", + "frontend-formatter": "prettier", + "frontend-linter": "prettier", + "include-vitest": true, + "include-playwright": true, + "include-storybook": true, + "include-auth": true, + "auth-provider": "clerk", + "include-theme": true, + "include-feature-flags": false, + "include-sentry": true, + "include-analytics": true, + "analytics-provider": "ga4", + "include-seo": true, + "confirm-generate": true + }, + "expected_visited": [ + "project_name", + "framework", + "frontend-router", + "ui-library", + "frontend-state", + "frontend-formatter", + "frontend-linter", + "check-vitest-available", + "include-vitest", + "include-playwright", + "include-storybook", + "include-auth", + "auth-provider", + "include-theme", + "include-feature-flags", + "include-sentry", + "include-analytics", + "analytics-provider", + "include-seo", + "confirm-generate" + ], + "skip_post_commands": false, + "skip_test_commands": false +} diff --git a/tools/test-flow/testdata/202605280004_frontend_next_shadcn_better_auth_seo.json b/tools/test-flow/testdata/202605280004_frontend_next_shadcn_better_auth_seo.json new file mode 100644 index 0000000..f5f9e97 --- /dev/null +++ b/tools/test-flow/testdata/202605280004_frontend_next_shadcn_better_auth_seo.json @@ -0,0 +1,45 @@ +{ + "name": "frontend_next_shadcn_better_auth_seo", + "flow_id": "frontend", + "answers": { + "project_name": "my-next-app", + "framework": "next", + "ui-library": "shadcn", + "frontend-state": "none", + "frontend-formatter": "prettier", + "frontend-linter": "prettier", + "include-playwright": true, + "include-storybook": false, + "include-auth": true, + "auth-provider": "better-auth", + "include-theme": false, + "include-feature-flags": false, + "include-sentry": true, + "include-analytics": true, + "analytics-provider": "plausible", + "include-seo": true, + "confirm-generate": true + }, + "expected_visited": [ + "project_name", + "framework", + "ui-library", + "frontend-state", + "frontend-formatter", + "frontend-linter", + "check-vitest-available", + "include-playwright", + "include-storybook", + "include-auth", + "auth-provider", + "include-theme", + "include-feature-flags", + "include-sentry", + "include-analytics", + "analytics-provider", + "include-seo", + "confirm-generate" + ], + "skip_post_commands": false, + "skip_test_commands": false +} diff --git a/tools/test-flow/testdata/202605280005_frontend_react_vite_tanstack_arkui_panda_jotai.json b/tools/test-flow/testdata/202605280005_frontend_react_vite_tanstack_arkui_panda_jotai.json new file mode 100644 index 0000000..7214cc8 --- /dev/null +++ b/tools/test-flow/testdata/202605280005_frontend_react_vite_tanstack_arkui_panda_jotai.json @@ -0,0 +1,49 @@ +{ + "name": "frontend_react_vite_tanstack_arkui_panda_jotai", + "flow_id": "frontend", + "answers": { + "project_name": "my-app", + "framework": "react-vite", + "frontend-router": "tanstack-router", + "ui-library": "arkui", + "frontend-styling": "panda-css", + "frontend-state": "jotai", + "frontend-formatter": "prettier", + "frontend-linter": "prettier", + "include-vitest": false, + "include-playwright": true, + "include-storybook": false, + "include-auth": false, + "include-theme": false, + "include-feature-flags": true, + "feature-flags-provider": "vercel", + "include-sentry": false, + "include-analytics": false, + "include-seo": false, + "confirm-generate": true + }, + "expected_visited": [ + "project_name", + "framework", + "frontend-router", + "ui-library", + "frontend-styling", + "frontend-state", + "frontend-formatter", + "frontend-linter", + "check-vitest-available", + "include-vitest", + "include-playwright", + "include-storybook", + "include-auth", + "include-theme", + "include-feature-flags", + "feature-flags-provider", + "include-sentry", + "include-analytics", + "include-seo", + "confirm-generate" + ], + "skip_post_commands": false, + "skip_test_commands": false +} diff --git a/tools/test-flow/testdata/202605280006_frontend_react_vite_version14_vanilla_auth.json b/tools/test-flow/testdata/202605280006_frontend_react_vite_version14_vanilla_auth.json new file mode 100644 index 0000000..11f2eee --- /dev/null +++ b/tools/test-flow/testdata/202605280006_frontend_react_vite_version14_vanilla_auth.json @@ -0,0 +1,51 @@ +{ + "name": "frontend_react_vite_version14_vanilla_auth", + "flow_id": "frontend", + "answers": { + "project_name": "my-app", + "framework": "react-vite", + "frontend-router": "none", + "ui-library": "version14", + "frontend-styling": "panda-css", + "frontend-state": "none", + "frontend-formatter": "biome", + "frontend-linter": "biome", + "include-vitest": false, + "include-playwright": false, + "include-storybook": false, + "include-auth": true, + "auth-provider": "vanilla", + "include-theme": true, + "include-feature-flags": true, + "feature-flags-provider": "local", + "include-sentry": false, + "include-analytics": false, + "include-seo": false, + "confirm-generate": true + }, + "expected_visited": [ + "project_name", + "framework", + "frontend-router", + "ui-library", + "frontend-styling", + "frontend-state", + "frontend-formatter", + "frontend-linter", + "check-vitest-available", + "include-vitest", + "include-playwright", + "include-storybook", + "include-auth", + "auth-provider", + "include-theme", + "include-feature-flags", + "feature-flags-provider", + "include-sentry", + "include-analytics", + "include-seo", + "confirm-generate" + ], + "skip_post_commands": false, + "skip_test_commands": false +} diff --git a/tools/test-flow/testdata/202605280007_frontend_next_clerk_sentry_seo.json b/tools/test-flow/testdata/202605280007_frontend_next_clerk_sentry_seo.json new file mode 100644 index 0000000..17b0be8 --- /dev/null +++ b/tools/test-flow/testdata/202605280007_frontend_next_clerk_sentry_seo.json @@ -0,0 +1,47 @@ +{ + "name": "frontend_next_clerk_sentry_seo", + "flow_id": "frontend", + "answers": { + "project_name": "my-next-app", + "framework": "next", + "ui-library": "none", + "frontend-styling": "css-modules", + "frontend-state": "zustand", + "frontend-formatter": "prettier", + "frontend-linter": "prettier", + "include-playwright": false, + "include-storybook": false, + "include-auth": true, + "auth-provider": "clerk", + "include-theme": true, + "include-feature-flags": false, + "include-sentry": true, + "include-analytics": true, + "analytics-provider": "ga4", + "include-seo": true, + "confirm-generate": true + }, + "expected_visited": [ + "project_name", + "framework", + "ui-library", + "frontend-styling", + "frontend-state", + "frontend-formatter", + "frontend-linter", + "check-vitest-available", + "include-playwright", + "include-storybook", + "include-auth", + "auth-provider", + "include-theme", + "include-feature-flags", + "include-sentry", + "include-analytics", + "analytics-provider", + "include-seo", + "confirm-generate" + ], + "skip_post_commands": false, + "skip_test_commands": false +} diff --git a/tools/test-flow/testdata/202605280008_frontend_react_vite_all_modules.json b/tools/test-flow/testdata/202605280008_frontend_react_vite_all_modules.json new file mode 100644 index 0000000..ee993c9 --- /dev/null +++ b/tools/test-flow/testdata/202605280008_frontend_react_vite_all_modules.json @@ -0,0 +1,53 @@ +{ + "name": "frontend_react_vite_all_modules", + "flow_id": "frontend", + "answers": { + "project_name": "my-app", + "framework": "react-vite", + "frontend-router": "react-router", + "ui-library": "none", + "frontend-styling": "tailwind", + "frontend-state": "zustand", + "frontend-formatter": "prettier", + "frontend-linter": "prettier", + "include-vitest": true, + "include-playwright": true, + "include-storybook": true, + "include-auth": true, + "auth-provider": "better-auth", + "include-theme": true, + "include-feature-flags": true, + "feature-flags-provider": "local", + "include-sentry": true, + "include-analytics": true, + "analytics-provider": "plausible", + "include-seo": true, + "confirm-generate": true + }, + "expected_visited": [ + "project_name", + "framework", + "frontend-router", + "ui-library", + "frontend-styling", + "frontend-state", + "frontend-formatter", + "frontend-linter", + "check-vitest-available", + "include-vitest", + "include-playwright", + "include-storybook", + "include-auth", + "auth-provider", + "include-theme", + "include-feature-flags", + "feature-flags-provider", + "include-sentry", + "include-analytics", + "analytics-provider", + "include-seo", + "confirm-generate" + ], + "skip_post_commands": false, + "skip_test_commands": false +} diff --git a/tools/test-flow/testdata/202605280009_frontend_react_vite_vitest_storybook.json b/tools/test-flow/testdata/202605280009_frontend_react_vite_vitest_storybook.json new file mode 100644 index 0000000..5f5f9b5 --- /dev/null +++ b/tools/test-flow/testdata/202605280009_frontend_react_vite_vitest_storybook.json @@ -0,0 +1,47 @@ +{ + "name": "frontend_react_vite_vitest_storybook", + "flow_id": "frontend", + "answers": { + "project_name": "my-app", + "framework": "react-vite", + "frontend-router": "none", + "ui-library": "none", + "frontend-styling": "css-modules", + "frontend-state": "none", + "frontend-formatter": "biome", + "frontend-linter": "biome", + "include-vitest": true, + "include-playwright": false, + "include-storybook": true, + "include-auth": false, + "include-theme": false, + "include-feature-flags": false, + "include-sentry": false, + "include-analytics": false, + "include-seo": false, + "confirm-generate": true + }, + "expected_visited": [ + "project_name", + "framework", + "frontend-router", + "ui-library", + "frontend-styling", + "frontend-state", + "frontend-formatter", + "frontend-linter", + "check-vitest-available", + "include-vitest", + "include-playwright", + "include-storybook", + "include-auth", + "include-theme", + "include-feature-flags", + "include-sentry", + "include-analytics", + "include-seo", + "confirm-generate" + ], + "skip_post_commands": false, + "skip_test_commands": false +} diff --git a/tools/test-flow/testdata/202605280010_frontend_analytics_posthog_dedup.json b/tools/test-flow/testdata/202605280010_frontend_analytics_posthog_dedup.json new file mode 100644 index 0000000..b11e058 --- /dev/null +++ b/tools/test-flow/testdata/202605280010_frontend_analytics_posthog_dedup.json @@ -0,0 +1,51 @@ +{ + "name": "frontend_analytics_posthog_dedup", + "flow_id": "frontend", + "answers": { + "project_name": "my-app", + "framework": "react-vite", + "frontend-router": "none", + "ui-library": "none", + "frontend-styling": "tailwind", + "frontend-state": "none", + "frontend-formatter": "biome", + "frontend-linter": "biome", + "include-vitest": false, + "include-playwright": false, + "include-storybook": false, + "include-auth": false, + "include-theme": false, + "include-feature-flags": true, + "feature-flags-provider": "posthog", + "include-sentry": false, + "include-analytics": true, + "analytics-provider": "posthog", + "include-seo": false, + "confirm-generate": true + }, + "expected_visited": [ + "project_name", + "framework", + "frontend-router", + "ui-library", + "frontend-styling", + "frontend-state", + "frontend-formatter", + "frontend-linter", + "check-vitest-available", + "include-vitest", + "include-playwright", + "include-storybook", + "include-auth", + "include-theme", + "include-feature-flags", + "feature-flags-provider", + "include-sentry", + "include-analytics", + "analytics-provider", + "include-seo", + "confirm-generate" + ], + "skip_post_commands": false, + "skip_test_commands": false +}