@react-spa-scaffold/mcp 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +1 -1
  2. package/dist/tools/get-example.d.ts +4 -6
  3. package/dist/tools/get-example.d.ts.map +1 -1
  4. package/dist/tools/get-example.js +1 -1
  5. package/dist/tools/get-example.js.map +1 -1
  6. package/dist/tools/get-scaffold.d.ts +2 -10
  7. package/dist/tools/get-scaffold.d.ts.map +1 -1
  8. package/dist/tools/get-scaffold.js +11 -3
  9. package/dist/tools/get-scaffold.js.map +1 -1
  10. package/package.json +2 -2
  11. package/templates/.env.example +21 -0
  12. package/templates/.github/PULL_REQUEST_TEMPLATE.md +24 -0
  13. package/templates/.github/actions/setup-node-deps/action.yml +32 -0
  14. package/templates/.github/dependabot.yml +104 -0
  15. package/templates/.github/workflows/ci.yml +156 -0
  16. package/templates/.husky/commit-msg +1 -0
  17. package/templates/.husky/pre-commit +2 -0
  18. package/templates/.nvmrc +1 -0
  19. package/templates/commitlint.config.js +1 -0
  20. package/templates/components.json +21 -0
  21. package/templates/e2e/fixtures/index.ts +10 -0
  22. package/templates/e2e/tests/home.spec.ts +42 -0
  23. package/templates/e2e/tests/language.spec.ts +42 -0
  24. package/templates/e2e/tests/navigation.spec.ts +18 -0
  25. package/templates/e2e/tests/theme.spec.ts +35 -0
  26. package/templates/eslint.config.js +42 -0
  27. package/templates/index.html +13 -0
  28. package/templates/lighthouse-budget.json +17 -0
  29. package/templates/lighthouserc.json +23 -0
  30. package/templates/lingui.config.js +18 -0
  31. package/templates/package.json +125 -0
  32. package/templates/playwright.config.ts +30 -0
  33. package/templates/prettier.config.js +1 -0
  34. package/templates/public/favicon.svg +4 -0
  35. package/templates/tsconfig.app.json +10 -0
  36. package/templates/tsconfig.json +11 -0
  37. package/templates/tsconfig.node.json +4 -0
  38. package/templates/vite.config.ts +54 -0
  39. package/templates/vitest.config.ts +38 -0
package/README.md CHANGED
@@ -42,7 +42,7 @@ Add to your MCP client config:
42
42
  "mcpServers": {
43
43
  "react-spa-scaffold": {
44
44
  "command": "npx",
45
- "args": ["@react-spa-scaffold/mcp"]
45
+ "args": ["-y", "@react-spa-scaffold/mcp@latest"]
46
46
  }
47
47
  }
48
48
  }
@@ -7,12 +7,10 @@
7
7
  */
8
8
  import { z } from 'zod';
9
9
  export declare const getExampleSchema: z.ZodObject<{
10
- pattern: z.ZodEnum<[string, ...string[]]>;
11
- }, "strip", z.ZodTypeAny, {
12
- pattern: string;
13
- }, {
14
- pattern: string;
15
- }>;
10
+ pattern: z.ZodEnum<{
11
+ [x: string]: string;
12
+ }>;
13
+ }, z.core.$strip>;
16
14
  export type GetExampleInput = z.infer<typeof getExampleSchema>;
17
15
  export declare function getExample(input: GetExampleInput): Promise<{
18
16
  error: string;
@@ -1 +1 @@
1
- {"version":3,"file":"get-example.d.ts","sourceRoot":"","sources":["../../src/tools/get-example.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,eAAO,MAAM,gBAAgB;;;;;;EAI3B,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE/D,wBAAsB,UAAU,CAAC,KAAK,EAAE,eAAe;;;;;;;;;;;;;;;;;;GAoBtD;AAuBD,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;CAoCpC,CAAC"}
1
+ {"version":3,"file":"get-example.d.ts","sourceRoot":"","sources":["../../src/tools/get-example.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,eAAO,MAAM,gBAAgB;;;;iBAI3B,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE/D,wBAAsB,UAAU,CAAC,KAAK,EAAE,eAAe;;;;;;;;;;;;;;;;;;GAoBtD;AAuBD,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;CAoCpC,CAAC"}
@@ -10,7 +10,7 @@ import { getCodeExample, getAvailablePatterns } from '../utils/index.js';
10
10
  const AVAILABLE_PATTERNS = getAvailablePatterns();
11
11
  export const getExampleSchema = z.object({
12
12
  pattern: z.enum(AVAILABLE_PATTERNS, {
13
- errorMap: () => ({ message: `Invalid pattern. Available: ${AVAILABLE_PATTERNS.join(', ')}` }),
13
+ error: `Invalid pattern. Available: ${AVAILABLE_PATTERNS.join(', ')}`,
14
14
  }),
15
15
  });
16
16
  export async function getExample(input) {
@@ -1 +1 @@
1
- {"version":3,"file":"get-example.js","sourceRoot":"","sources":["../../src/tools/get-example.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAEzE,MAAM,kBAAkB,GAAG,oBAAoB,EAA2B,CAAC;AAE3E,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,kBAAkB,EAAE;QAClC,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,+BAA+B,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC;KAC9F,CAAC;CACH,CAAC,CAAC;AAIH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,KAAsB;IACrD,MAAM,EAAE,OAAO,EAAE,GAAG,KAAK,CAAC;IAE1B,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,OAAO,CAAC,CAAC;IAE9C,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO;YACL,KAAK,EAAE,uCAAuC,OAAO,EAAE;YACvD,IAAI,EAAE,gEAAgE;SACvE,CAAC;IACJ,CAAC;IAED,OAAO;QACL,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,WAAW,EAAE,OAAO,CAAC,WAAW;QAChC,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,KAAK,EAAE,iBAAiB,CAAC,OAAO,CAAC;KAClC,CAAC;AACJ,CAAC;AAED,SAAS,iBAAiB,CAAC,OAAe;IACxC,MAAM,KAAK,GAA2B;QACpC,cAAc,EAAE,sDAAsD;QACtE,kBAAkB,EAAE,oEAAoE;QACxF,kBAAkB,EAAE,iCAAiC;QACrD,YAAY,EAAE,2CAA2C;QACzD,YAAY,EAAE,mDAAmD;QACjE,WAAW,EAAE,0DAA0D;QACvE,aAAa,EAAE,uCAAuC;QACtD,eAAe,EAAE,8CAA8C;QAC/D,gBAAgB,EAAE,+CAA+C;QACjE,kBAAkB,EAAE,wDAAwD;QAC5E,gBAAgB,EAAE,yDAAyD;QAC3E,WAAW,EAAE,4BAA4B;QACzC,aAAa,EAAE,8BAA8B;QAC7C,YAAY,EAAE,+BAA+B;KAC9C,CAAC;IAEF,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,2CAA2C,CAAC;AACvE,CAAC;AAED,MAAM,CAAC,MAAM,wBAAwB,GAAG;IACtC,IAAI,EAAE,aAAa;IACnB,WAAW,EAAE;;;;;;;;;;;;;;;;;;;;;;sCAsBuB;IACpC,WAAW,EAAE;QACX,IAAI,EAAE,QAAiB;QACvB,UAAU,EAAE;YACV,OAAO,EAAE;gBACP,IAAI,EAAE,QAAiB;gBACvB,WAAW,EAAE,iCAAiC;gBAC9C,IAAI,EAAE,kBAAkB;aACzB;SACF;QACD,QAAQ,EAAE,CAAC,SAAS,CAAC;KACtB;CACF,CAAC"}
1
+ {"version":3,"file":"get-example.js","sourceRoot":"","sources":["../../src/tools/get-example.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAEzE,MAAM,kBAAkB,GAAG,oBAAoB,EAA2B,CAAC;AAE3E,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,kBAAkB,EAAE;QAClC,KAAK,EAAE,+BAA+B,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;KACtE,CAAC;CACH,CAAC,CAAC;AAIH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,KAAsB;IACrD,MAAM,EAAE,OAAO,EAAE,GAAG,KAAK,CAAC;IAE1B,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,OAAO,CAAC,CAAC;IAE9C,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO;YACL,KAAK,EAAE,uCAAuC,OAAO,EAAE;YACvD,IAAI,EAAE,gEAAgE;SACvE,CAAC;IACJ,CAAC;IAED,OAAO;QACL,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,WAAW,EAAE,OAAO,CAAC,WAAW;QAChC,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,KAAK,EAAE,iBAAiB,CAAC,OAAO,CAAC;KAClC,CAAC;AACJ,CAAC;AAED,SAAS,iBAAiB,CAAC,OAAe;IACxC,MAAM,KAAK,GAA2B;QACpC,cAAc,EAAE,sDAAsD;QACtE,kBAAkB,EAAE,oEAAoE;QACxF,kBAAkB,EAAE,iCAAiC;QACrD,YAAY,EAAE,2CAA2C;QACzD,YAAY,EAAE,mDAAmD;QACjE,WAAW,EAAE,0DAA0D;QACvE,aAAa,EAAE,uCAAuC;QACtD,eAAe,EAAE,8CAA8C;QAC/D,gBAAgB,EAAE,+CAA+C;QACjE,kBAAkB,EAAE,wDAAwD;QAC5E,gBAAgB,EAAE,yDAAyD;QAC3E,WAAW,EAAE,4BAA4B;QACzC,aAAa,EAAE,8BAA8B;QAC7C,YAAY,EAAE,+BAA+B;KAC9C,CAAC;IAEF,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,2CAA2C,CAAC;AACvE,CAAC;AAED,MAAM,CAAC,MAAM,wBAAwB,GAAG;IACtC,IAAI,EAAE,aAAa;IACnB,WAAW,EAAE;;;;;;;;;;;;;;;;;;;;;;sCAsBuB;IACpC,WAAW,EAAE;QACX,IAAI,EAAE,QAAiB;QACvB,UAAU,EAAE;YACV,OAAO,EAAE;gBACP,IAAI,EAAE,QAAiB;gBACvB,WAAW,EAAE,iCAAiC;gBAC9C,IAAI,EAAE,kBAAkB;aACzB;SACF;QACD,QAAQ,EAAE,CAAC,SAAS,CAAC;KACtB;CACF,CAAC"}
@@ -8,18 +8,10 @@
8
8
  import { z } from 'zod';
9
9
  import { type CodeExample } from '../utils/index.js';
10
10
  export declare const getScaffoldSchema: z.ZodObject<{
11
- features: z.ZodEffects<z.ZodArray<z.ZodString, "many">, string[], string[]>;
11
+ features: z.ZodArray<z.ZodString>;
12
12
  projectName: z.ZodOptional<z.ZodString>;
13
13
  includeExamples: z.ZodOptional<z.ZodBoolean>;
14
- }, "strip", z.ZodTypeAny, {
15
- features: string[];
16
- projectName?: string | undefined;
17
- includeExamples?: boolean | undefined;
18
- }, {
19
- features: string[];
20
- projectName?: string | undefined;
21
- includeExamples?: boolean | undefined;
22
- }>;
14
+ }, z.core.$strip>;
23
15
  export type GetScaffoldInput = z.infer<typeof getScaffoldSchema>;
24
16
  export declare function getScaffold(input: GetScaffoldInput): Promise<{
25
17
  projectName: string;
@@ -1 +1 @@
1
- {"version":3,"file":"get-scaffold.d.ts","sourceRoot":"","sources":["../../src/tools/get-scaffold.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAmE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEtH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;EAkB5B,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAEjE,wBAAsB,WAAW,CAAC,KAAK,EAAE,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDxD;AA0CD,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;CAyCrC,CAAC"}
1
+ {"version":3,"file":"get-scaffold.d.ts","sourceRoot":"","sources":["../../src/tools/get-scaffold.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAmE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEtH,eAAO,MAAM,iBAAiB;;;;iBAuB5B,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAEjE,wBAAsB,WAAW,CAAC,KAAK,EAAE,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDxD;AA0CD,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;CAyCrC,CAAC"}
@@ -12,9 +12,17 @@ export const getScaffoldSchema = z.object({
12
12
  features: z
13
13
  .array(z.string())
14
14
  .max(15, 'Maximum 15 features allowed')
15
- .refine((features) => features.every((f) => f in FEATURES), (features) => ({
16
- message: `Invalid features: ${features.filter((f) => !(f in FEATURES)).join(', ')}. Valid: ${FEATURE_IDS.join(', ')}`,
17
- }))
15
+ .check((ctx) => {
16
+ const invalidFeatures = ctx.value.filter((f) => !(f in FEATURES));
17
+ if (invalidFeatures.length > 0) {
18
+ ctx.issues.push({
19
+ code: 'custom',
20
+ message: `Invalid features: ${invalidFeatures.join(', ')}. Valid: ${FEATURE_IDS.join(', ')}`,
21
+ input: ctx.value,
22
+ path: [],
23
+ });
24
+ }
25
+ })
18
26
  .describe('List of feature IDs to include (e.g., ["routing", "ui", "forms"])'),
19
27
  projectName: z
20
28
  .string()
@@ -1 +1 @@
1
- {"version":3,"file":"get-scaffold.js","sourceRoot":"","sources":["../../src/tools/get-scaffold.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"get-scaffold.js","sourceRoot":"","sources":["../../src/tools/get-scaffold.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-spa-scaffold/mcp",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "MCP server for scaffolding projects based on react-spa-scaffold template",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@modelcontextprotocol/sdk": "^1.0.0",
43
- "zod": "^3.25.64"
43
+ "zod": "^4.2.1"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/node": "^22.15.0",
@@ -0,0 +1,21 @@
1
+ # Application
2
+ VITE_APP_NAME="My App"
3
+ VITE_APP_URL=http://localhost:5173
4
+
5
+ # Optional: Base URL for deployment subdirectory
6
+ # VITE_BASE_URL=/
7
+
8
+ # ─────────────────────────────────────────────────────────────
9
+ # Sentry Error Tracking (production only)
10
+ # ─────────────────────────────────────────────────────────────
11
+ # Set to 'false' to disable Sentry entirely (opt-out)
12
+ # Enabled by default when DSN is provided
13
+ # VITE_SENTRY_ENABLED=true
14
+
15
+ # Runtime DSN for error reporting (client-side, safe to expose)
16
+ # VITE_SENTRY_DSN=https://xxxxx@o123456.ingest.sentry.io/789
17
+
18
+ # CI/CD secrets for source map upload (set in GitHub Secrets):
19
+ # - SENTRY_AUTH_TOKEN: API token for uploading source maps
20
+ # - SENTRY_ORG: Sentry organization slug
21
+ # - SENTRY_PROJECT: Sentry project slug
@@ -0,0 +1,24 @@
1
+ ## Summary
2
+
3
+ <!-- Brief description of changes -->
4
+
5
+ ## Type of Change
6
+
7
+ - [ ] Bug fix
8
+ - [ ] New feature
9
+ - [ ] Breaking change
10
+ - [ ] Documentation update
11
+ - [ ] Refactoring
12
+
13
+ ## Testing
14
+
15
+ - [ ] Unit tests added/updated
16
+ - [ ] E2E tests added/updated (if applicable)
17
+ - [ ] Manual testing completed
18
+
19
+ ## Checklist
20
+
21
+ - [ ] Code follows project style guidelines
22
+ - [ ] Self-reviewed the code
23
+ - [ ] Added necessary documentation
24
+ - [ ] No new warnings introduced
@@ -0,0 +1,32 @@
1
+ name: Setup Node.js and Dependencies
2
+ description: Sets up Node.js and caches npm dependencies
3
+
4
+ inputs:
5
+ node-version:
6
+ description: Node.js version
7
+ required: false
8
+ default: '22'
9
+
10
+ runs:
11
+ using: composite
12
+ steps:
13
+ - name: Setup Node.js
14
+ uses: actions/setup-node@v4
15
+ with:
16
+ node-version: ${{ inputs.node-version }}
17
+
18
+ - name: Update npm to latest
19
+ shell: bash
20
+ run: npm install -g npm@latest
21
+
22
+ - name: Cache node_modules
23
+ id: cache
24
+ uses: actions/cache@v5
25
+ with:
26
+ path: node_modules
27
+ key: node-modules-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('package-lock.json') }}
28
+
29
+ - name: Install dependencies
30
+ if: steps.cache.outputs.cache-hit != 'true'
31
+ shell: bash
32
+ run: npm ci
@@ -0,0 +1,104 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: 'npm'
4
+ directory: '/'
5
+ schedule:
6
+ interval: 'weekly'
7
+ day: 'monday'
8
+ time: '09:00'
9
+ timezone: 'UTC'
10
+ open-pull-requests-limit: 10
11
+ groups:
12
+ production:
13
+ patterns:
14
+ - 'react*'
15
+ - 'lucide-react'
16
+ - 'sonner'
17
+ - 'radix-ui'
18
+ - '@radix-ui/*'
19
+ - 'class-variance-authority'
20
+ - 'clsx'
21
+ - 'tailwind-merge'
22
+ - 'tw-animate-css'
23
+ - '@fontsource-variable/*'
24
+ - 'zustand'
25
+ update-types:
26
+ - 'minor'
27
+ - 'patch'
28
+ routing:
29
+ patterns:
30
+ - 'react-router*'
31
+ update-types:
32
+ - 'minor'
33
+ - 'patch'
34
+ data-fetching:
35
+ patterns:
36
+ - '@tanstack/*'
37
+ - 'react-hook-form'
38
+ - '@hookform/*'
39
+ - 'zod'
40
+ update-types:
41
+ - 'minor'
42
+ - 'patch'
43
+ development:
44
+ patterns:
45
+ - '@types/*'
46
+ - '@vitejs/*'
47
+ - '@vitest/*'
48
+ - 'vitest*'
49
+ - 'vite'
50
+ - 'typescript*'
51
+ - 'jsdom'
52
+ update-types:
53
+ - 'minor'
54
+ - 'patch'
55
+ linting:
56
+ patterns:
57
+ - 'eslint*'
58
+ - '@eslint/*'
59
+ - 'prettier*'
60
+ - 'husky'
61
+ - 'lint-staged'
62
+ - 'commitlint*'
63
+ - '@commitlint/*'
64
+ update-types:
65
+ - 'minor'
66
+ - 'patch'
67
+ testing:
68
+ patterns:
69
+ - '@testing-library/*'
70
+ - '@playwright/*'
71
+ - 'msw'
72
+ update-types:
73
+ - 'minor'
74
+ - 'patch'
75
+ monitoring:
76
+ patterns:
77
+ - '@sentry/*'
78
+ update-types:
79
+ - 'minor'
80
+ - 'patch'
81
+ styling:
82
+ patterns:
83
+ - 'tailwindcss'
84
+ - '@tailwindcss/*'
85
+ update-types:
86
+ - 'minor'
87
+ - 'patch'
88
+ i18n:
89
+ patterns:
90
+ - '@lingui/*'
91
+ update-types:
92
+ - 'minor'
93
+ - 'patch'
94
+ commit-message:
95
+ prefix: 'chore(deps)'
96
+
97
+ - package-ecosystem: 'github-actions'
98
+ directory: '/'
99
+ schedule:
100
+ interval: 'weekly'
101
+ day: 'monday'
102
+ open-pull-requests-limit: 5
103
+ commit-message:
104
+ prefix: 'ci(deps)'
@@ -0,0 +1,156 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main, master]
6
+ pull_request:
7
+ branches: [main, master]
8
+ release:
9
+ types: [published]
10
+
11
+ concurrency:
12
+ group: ${{ github.workflow }}-${{ github.ref }}
13
+ cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/master' }}
14
+
15
+ jobs:
16
+ lint:
17
+ name: Lint
18
+ runs-on: ubuntu-latest
19
+ timeout-minutes: 10
20
+ steps:
21
+ - uses: actions/checkout@v6
22
+ - uses: ./.github/actions/setup-node-deps
23
+ - run: npm run lint
24
+ - run: npm run format:check
25
+
26
+ typecheck:
27
+ name: Type Check
28
+ runs-on: ubuntu-latest
29
+ timeout-minutes: 10
30
+ steps:
31
+ - uses: actions/checkout@v6
32
+ - uses: ./.github/actions/setup-node-deps
33
+ - run: npm run typecheck
34
+
35
+ security:
36
+ name: Security Audit
37
+ runs-on: ubuntu-latest
38
+ timeout-minutes: 10
39
+ steps:
40
+ - uses: actions/checkout@v6
41
+ - uses: ./.github/actions/setup-node-deps
42
+ - run: npm audit --audit-level=moderate
43
+
44
+ build:
45
+ name: Build
46
+ needs: [lint, typecheck]
47
+ runs-on: ubuntu-latest
48
+ timeout-minutes: 10
49
+ steps:
50
+ - uses: actions/checkout@v6
51
+ - uses: ./.github/actions/setup-node-deps
52
+ - name: Build
53
+ run: npm run build
54
+ env:
55
+ SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
56
+ SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
57
+ SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
58
+ - uses: actions/upload-artifact@v6
59
+ with:
60
+ name: dist
61
+ path: dist/
62
+ retention-days: 7
63
+
64
+ test:
65
+ name: Unit Tests
66
+ needs: [lint, typecheck]
67
+ runs-on: ubuntu-latest
68
+ timeout-minutes: 15
69
+ steps:
70
+ - uses: actions/checkout@v6
71
+ - uses: ./.github/actions/setup-node-deps
72
+ - run: npm run test:coverage
73
+ - uses: actions/upload-artifact@v6
74
+ with:
75
+ name: coverage
76
+ path: coverage/
77
+ retention-days: 14
78
+
79
+ e2e:
80
+ name: E2E Tests
81
+ needs: [lint, typecheck]
82
+ runs-on: ubuntu-latest
83
+ timeout-minutes: 15
84
+ steps:
85
+ - uses: actions/checkout@v6
86
+ - uses: ./.github/actions/setup-node-deps
87
+ - name: Cache Playwright browsers
88
+ uses: actions/cache@v5
89
+ id: playwright-cache
90
+ with:
91
+ path: ~/.cache/ms-playwright
92
+ key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
93
+ - name: Install Playwright browsers
94
+ if: steps.playwright-cache.outputs.cache-hit != 'true'
95
+ run: npx playwright install chromium --with-deps
96
+ - name: Install Playwright deps (cached)
97
+ if: steps.playwright-cache.outputs.cache-hit == 'true'
98
+ run: npx playwright install-deps chromium
99
+ - run: npm run e2e
100
+ - uses: actions/upload-artifact@v6
101
+ if: failure()
102
+ with:
103
+ name: playwright-report
104
+ path: playwright-report/
105
+ retention-days: 7
106
+
107
+ lighthouse:
108
+ name: Lighthouse CI
109
+ needs: [build]
110
+ runs-on: ubuntu-latest
111
+ timeout-minutes: 10
112
+ continue-on-error: true
113
+ steps:
114
+ - uses: actions/checkout@v6
115
+ - uses: ./.github/actions/setup-node-deps
116
+ - uses: actions/download-artifact@v7
117
+ with:
118
+ name: dist
119
+ path: dist/
120
+ - name: Run Lighthouse CI
121
+ uses: treosh/lighthouse-ci-action@v12
122
+ with:
123
+ configPath: ./lighthouserc.json
124
+ uploadArtifacts: true
125
+ temporaryPublicStorage: true
126
+ - uses: actions/upload-artifact@v6
127
+ if: always()
128
+ with:
129
+ name: lighthouse-report
130
+ path: .lighthouseci/
131
+ retention-days: 14
132
+
133
+ publish:
134
+ name: Publish to npm
135
+ needs: [build, test]
136
+ if: github.event_name == 'release'
137
+ runs-on: ubuntu-latest
138
+ timeout-minutes: 15
139
+ permissions:
140
+ contents: read
141
+ id-token: write
142
+ steps:
143
+ - uses: actions/checkout@v6
144
+ - uses: ./.github/actions/setup-node-deps
145
+ - name: Configure npm authentication
146
+ run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
147
+ env:
148
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
149
+ - name: Publish @react-spa-scaffold/tsconfig
150
+ run: npm publish -w @react-spa-scaffold/tsconfig --provenance --access public || echo "Package may already exist at this version"
151
+ - name: Publish @react-spa-scaffold/eslint-config
152
+ run: npm publish -w @react-spa-scaffold/eslint-config --provenance --access public || echo "Package may already exist at this version"
153
+ - name: Publish @react-spa-scaffold/prettier-config
154
+ run: npm publish -w @react-spa-scaffold/prettier-config --provenance --access public || echo "Package may already exist at this version"
155
+ - name: Publish @react-spa-scaffold/mcp
156
+ run: npm publish -w @react-spa-scaffold/mcp --provenance --access public || echo "Package may already exist at this version"
@@ -0,0 +1 @@
1
+ npx commitlint --edit $1
@@ -0,0 +1,2 @@
1
+ npm run typecheck
2
+ npx lint-staged
@@ -0,0 +1 @@
1
+ 22
@@ -0,0 +1 @@
1
+ export default { extends: ['@commitlint/config-conventional'] };
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "radix-nova",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "src/index.css",
9
+ "baseColor": "zinc",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "iconLibrary": "lucide",
14
+ "aliases": {
15
+ "components": "@/components",
16
+ "utils": "@/lib/utils",
17
+ "ui": "@/components/ui",
18
+ "lib": "@/lib",
19
+ "hooks": "@/hooks"
20
+ }
21
+ }
@@ -0,0 +1,10 @@
1
+ import type { Page } from '@playwright/test';
2
+
3
+ /**
4
+ * Navigate to page with clean state (clears localStorage)
5
+ */
6
+ export async function setupPage(page: Page, path = '/') {
7
+ await page.goto(path);
8
+ await page.evaluate(() => localStorage.clear());
9
+ await page.reload();
10
+ }
@@ -0,0 +1,42 @@
1
+ import { expect, test } from '@playwright/test';
2
+
3
+ test.describe('Home Page', () => {
4
+ test.beforeEach(async ({ page }) => {
5
+ await page.goto('/');
6
+ });
7
+
8
+ test('displays welcome heading', async ({ page }) => {
9
+ await expect(page.getByRole('heading', { name: /welcome/i })).toBeVisible();
10
+ });
11
+
12
+ test('has correct page structure', async ({ page }) => {
13
+ // Header present
14
+ await expect(page.getByRole('banner')).toBeVisible();
15
+
16
+ // Main content area
17
+ await expect(page.getByRole('main')).toBeVisible();
18
+
19
+ // App title in header
20
+ await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
21
+ });
22
+
23
+ test('skip link navigates to main content', async ({ page }) => {
24
+ const skipLink = page.getByRole('link', { name: /skip to main content/i });
25
+
26
+ // Ensure skip link exists in DOM
27
+ await expect(skipLink).toBeAttached();
28
+
29
+ // Focus the skip link explicitly (more reliable than Tab in E2E)
30
+ await skipLink.focus();
31
+ await expect(skipLink).toBeFocused();
32
+
33
+ // Verify skip link becomes visible when focused
34
+ await expect(skipLink).toBeVisible();
35
+
36
+ // Click skip link to navigate to main content
37
+ await skipLink.click();
38
+
39
+ // Main should be scrolled into view
40
+ await expect(page.locator('#main')).toBeInViewport();
41
+ });
42
+ });
@@ -0,0 +1,42 @@
1
+ import { expect, test } from '@playwright/test';
2
+
3
+ import { setupPage } from '../fixtures';
4
+
5
+ test.describe('Language Switcher', () => {
6
+ test.beforeEach(async ({ page }) => {
7
+ await setupPage(page);
8
+ });
9
+
10
+ test('displays language switcher button', async ({ page }) => {
11
+ await expect(page.getByRole('button', { name: /change language/i })).toBeVisible();
12
+ });
13
+
14
+ test('opens dropdown with language options', async ({ page }) => {
15
+ await page.getByRole('button', { name: /change language/i }).click();
16
+
17
+ await expect(page.getByText('English')).toBeVisible();
18
+ await expect(page.getByText('Español')).toBeVisible();
19
+ await expect(page.getByText('Deutsch')).toBeVisible();
20
+ });
21
+
22
+ test('closes dropdown after selection', async ({ page }) => {
23
+ await page.getByRole('button', { name: /change language/i }).click();
24
+ await page.getByText('Español').click();
25
+
26
+ // Dropdown should close - other options not visible
27
+ await expect(page.getByText('Deutsch')).not.toBeVisible();
28
+ });
29
+
30
+ test('persists language preference across reload', async ({ page }) => {
31
+ // Change to Spanish
32
+ await page.getByRole('button', { name: /change language/i }).click();
33
+ await page.getByText('Español').click();
34
+
35
+ // Wait for language change to apply
36
+ await expect(page.getByRole('heading', { name: /bienvenido/i })).toBeVisible();
37
+
38
+ // Reload and verify Spanish persisted
39
+ await page.reload();
40
+ await expect(page.getByRole('heading', { name: /bienvenido/i })).toBeVisible();
41
+ });
42
+ });
@@ -0,0 +1,18 @@
1
+ import { expect, test } from '@playwright/test';
2
+
3
+ test.describe('Navigation', () => {
4
+ test('shows 404 page for unknown routes', async ({ page }) => {
5
+ await page.goto('/non-existent-page');
6
+ await expect(page.getByRole('heading', { name: '404' })).toBeVisible();
7
+ });
8
+
9
+ test('header is present on all pages', async ({ page }) => {
10
+ // Home page
11
+ await page.goto('/');
12
+ await expect(page.getByRole('banner')).toBeVisible();
13
+
14
+ // 404 page
15
+ await page.goto('/unknown');
16
+ await expect(page.getByRole('banner')).toBeVisible();
17
+ });
18
+ });
@@ -0,0 +1,35 @@
1
+ import { expect, test } from '@playwright/test';
2
+
3
+ import { setupPage } from '../fixtures';
4
+
5
+ test.describe('Theme Toggle', () => {
6
+ test.beforeEach(async ({ page }) => {
7
+ await setupPage(page);
8
+ });
9
+
10
+ test('defaults to light theme', async ({ page }) => {
11
+ await expect(page.locator('html')).not.toHaveClass(/dark/);
12
+ });
13
+
14
+ test('toggles between light and dark theme', async ({ page }) => {
15
+ const html = page.locator('html');
16
+
17
+ // Toggle to dark
18
+ await page.getByRole('button', { name: /switch to dark mode/i }).click();
19
+ await expect(html).toHaveClass(/dark/);
20
+
21
+ // Toggle back to light
22
+ await page.getByRole('button', { name: /switch to light mode/i }).click();
23
+ await expect(html).not.toHaveClass(/dark/);
24
+ });
25
+
26
+ test('persists theme preference across reload', async ({ page }) => {
27
+ // Set dark theme
28
+ await page.getByRole('button', { name: /switch to dark mode/i }).click();
29
+ await expect(page.locator('html')).toHaveClass(/dark/);
30
+
31
+ // Reload and verify
32
+ await page.reload();
33
+ await expect(page.locator('html')).toHaveClass(/dark/);
34
+ });
35
+ });
@@ -0,0 +1,42 @@
1
+ /**
2
+ * ESLint configuration for react-spa-scaffold
3
+ *
4
+ * Uses @react-spa-scaffold/eslint-config with local overrides for monorepo packages.
5
+ */
6
+
7
+ import config from '@react-spa-scaffold/eslint-config';
8
+
9
+ export default [
10
+ // Main app uses React config
11
+ ...config,
12
+
13
+ // Additional ignore for monorepo packages dist
14
+ { ignores: ['packages/**/dist'] },
15
+
16
+ // UI components from shadcn and context/provider files - don't modify
17
+ {
18
+ files: ['**/components/ui/**/*.{ts,tsx}', '**/contexts/**/*.{ts,tsx}', '**/test/**/*.{ts,tsx}'],
19
+ rules: {
20
+ 'react-refresh/only-export-components': 'off',
21
+ },
22
+ },
23
+
24
+ // Packages use Node.js rules (override the React/i18n rules from main config)
25
+ {
26
+ files: ['packages/**/*.ts'],
27
+ rules: {
28
+ // Disable React-specific rules (not a React app)
29
+ 'react-hooks/rules-of-hooks': 'off',
30
+ 'react-hooks/exhaustive-deps': 'off',
31
+ 'react-refresh/only-export-components': 'off',
32
+ // Disable i18n rules (server-side code)
33
+ 'lingui/no-unlocalized-strings': 'off',
34
+ 'lingui/t-call-in-function': 'off',
35
+ 'lingui/no-single-variables-to-translate': 'off',
36
+ 'lingui/no-expression-in-message': 'off',
37
+ 'lingui/no-trans-inside-trans': 'off',
38
+ // Allow console for server logging
39
+ 'no-console': 'off',
40
+ },
41
+ },
42
+ ];
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>My App</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,17 @@
1
+ [
2
+ {
3
+ "path": "/*",
4
+ "resourceSizes": [
5
+ { "resourceType": "script", "budget": 500 },
6
+ { "resourceType": "stylesheet", "budget": 100 },
7
+ { "resourceType": "total", "budget": 1000 }
8
+ ],
9
+ "resourceCounts": [{ "resourceType": "third-party", "budget": 10 }],
10
+ "timings": [
11
+ { "metric": "first-contentful-paint", "budget": 2000 },
12
+ { "metric": "interactive", "budget": 5000 },
13
+ { "metric": "largest-contentful-paint", "budget": 3000 },
14
+ { "metric": "cumulative-layout-shift", "budget": 0.1 }
15
+ ]
16
+ }
17
+ ]
@@ -0,0 +1,23 @@
1
+ {
2
+ "ci": {
3
+ "collect": {
4
+ "staticDistDir": "./dist",
5
+ "numberOfRuns": 3,
6
+ "settings": {
7
+ "preset": "desktop",
8
+ "budgetPath": "./lighthouse-budget.json"
9
+ }
10
+ },
11
+ "assert": {
12
+ "assertions": {
13
+ "categories:performance": ["warn", { "minScore": 0.9 }],
14
+ "categories:accessibility": ["warn", { "minScore": 0.9 }],
15
+ "categories:best-practices": ["warn", { "minScore": 0.9 }],
16
+ "categories:seo": ["warn", { "minScore": 0.9 }]
17
+ }
18
+ },
19
+ "upload": {
20
+ "target": "temporary-public-storage"
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,18 @@
1
+ import { defineConfig } from '@lingui/cli';
2
+
3
+ export default defineConfig({
4
+ locales: ['en', 'es', 'de'],
5
+ sourceLocale: 'en',
6
+ fallbackLocales: {
7
+ default: 'en',
8
+ },
9
+ catalogs: [
10
+ {
11
+ path: '<rootDir>/src/locales/{locale}',
12
+ include: ['src'],
13
+ exclude: ['**/node_modules/**', '**/*.test.{ts,tsx}'],
14
+ },
15
+ ],
16
+ format: 'po',
17
+ compileNamespace: 'es',
18
+ });
@@ -0,0 +1,125 @@
1
+ {
2
+ "name": "react-spa-scaffold",
3
+ "private": true,
4
+ "description": "Production-ready React 19 + TypeScript + Vite 7 starter template",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/mkaczkowski/react-spa-scaffold.git"
9
+ },
10
+ "homepage": "https://github.com/mkaczkowski/react-spa-scaffold#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/mkaczkowski/react-spa-scaffold/issues"
13
+ },
14
+ "keywords": [
15
+ "react",
16
+ "typescript",
17
+ "vite",
18
+ "starter",
19
+ "template",
20
+ "boilerplate",
21
+ "react-19",
22
+ "shadcn",
23
+ "tailwind"
24
+ ],
25
+ "version": "0.4.0",
26
+ "type": "module",
27
+ "workspaces": [
28
+ "packages/*"
29
+ ],
30
+ "scripts": {
31
+ "dev": "vite",
32
+ "build": "npm run typecheck && vite build",
33
+ "preview": "vite preview",
34
+ "typecheck": "tsc -p tsconfig.app.json && tsc -p tsconfig.node.json",
35
+ "lint": "eslint .",
36
+ "lint:fix": "eslint . --fix",
37
+ "format": "prettier --write .",
38
+ "format:check": "prettier --check .",
39
+ "test": "vitest run",
40
+ "test:watch": "vitest",
41
+ "test:coverage": "vitest run --coverage",
42
+ "e2e": "playwright test",
43
+ "e2e:ui": "playwright test --ui",
44
+ "i18n:extract": "lingui extract",
45
+ "prepare": "husky",
46
+ "mcp:build": "npm run build -w @react-spa-scaffold/mcp",
47
+ "mcp:dev": "npm run dev -w @react-spa-scaffold/mcp",
48
+ "mcp:start": "npm run start -w @react-spa-scaffold/mcp",
49
+ "mcp:inspect": "npm run inspect -w @react-spa-scaffold/mcp"
50
+ },
51
+ "dependencies": {
52
+ "@fontsource-variable/inter": "^5.2.5",
53
+ "@hookform/resolvers": "^5.0.1",
54
+ "@lingui/core": "^5.7.0",
55
+ "@lingui/react": "^5.7.0",
56
+ "@radix-ui/react-slot": "^1.2.3",
57
+ "@sentry/react": "^10.32.1",
58
+ "@tanstack/react-query": "^5.90.14",
59
+ "class-variance-authority": "^0.7.1",
60
+ "clsx": "^2.1.1",
61
+ "lucide-react": "^0.562.0",
62
+ "radix-ui": "^1.4.3",
63
+ "react": "^19.1.0",
64
+ "react-dom": "^19.1.0",
65
+ "react-hook-form": "^7.58.0",
66
+ "react-router": "^7.11.0",
67
+ "sonner": "^2.0.7",
68
+ "tailwind-merge": "^3.3.0",
69
+ "tw-animate-css": "^1.2.9",
70
+ "zod": "^4.2.1",
71
+ "zustand": "^5.0.9"
72
+ },
73
+ "devDependencies": {
74
+ "@commitlint/config-conventional": "^20.2.0",
75
+ "@react-spa-scaffold/eslint-config": "*",
76
+ "@react-spa-scaffold/prettier-config": "*",
77
+ "@react-spa-scaffold/tsconfig": "*",
78
+ "shadcn": "^3.6.2",
79
+ "@eslint/js": "^9.28.0",
80
+ "@lingui/babel-plugin-lingui-macro": "^5.7.0",
81
+ "@lingui/cli": "^5.7.0",
82
+ "@lingui/vite-plugin": "^5.7.0",
83
+ "@playwright/test": "^1.52.0",
84
+ "@sentry/vite-plugin": "^4.6.1",
85
+ "@tailwindcss/vite": "^4.1.17",
86
+ "@testing-library/jest-dom": "^6.6.3",
87
+ "@testing-library/react": "^16.3.0",
88
+ "@testing-library/user-event": "^14.6.1",
89
+ "@types/node": "^22.15.0",
90
+ "@types/react": "^19.1.8",
91
+ "@types/react-dom": "^19.1.6",
92
+ "@vitejs/plugin-react": "^5.1.2",
93
+ "@vitest/coverage-v8": "^4.0.16",
94
+ "babel-plugin-macros": "^3.1.0",
95
+ "commitlint": "^20.2.0",
96
+ "eslint": "^9.28.0",
97
+ "eslint-config-prettier": "^10.1.0",
98
+ "eslint-plugin-lingui": "^0.11.0",
99
+ "eslint-plugin-react-hooks": "^7.0.1",
100
+ "eslint-plugin-react-refresh": "^0.4.20",
101
+ "husky": "^9.1.7",
102
+ "jsdom": "^27.4.0",
103
+ "lint-staged": "^16.1.0",
104
+ "msw": "^2.12.7",
105
+ "prettier": "^3.5.3",
106
+ "prettier-plugin-tailwindcss": "^0.7.2",
107
+ "tailwindcss": "^4.1.17",
108
+ "typescript": "~5.9.0",
109
+ "typescript-eslint": "^8.33.0",
110
+ "vite": "^7.0.0",
111
+ "vitest": "^4.0.16"
112
+ },
113
+ "lint-staged": {
114
+ "*.{ts,tsx,js}": [
115
+ "eslint --fix",
116
+ "prettier --write"
117
+ ],
118
+ "*.{json,md,yml,yaml,css}": [
119
+ "prettier --write"
120
+ ]
121
+ },
122
+ "engines": {
123
+ "node": ">=22.0.0"
124
+ }
125
+ }
@@ -0,0 +1,30 @@
1
+ import { defineConfig, devices } from '@playwright/test';
2
+
3
+ export default defineConfig({
4
+ testDir: './e2e/tests',
5
+ fullyParallel: true,
6
+ forbidOnly: !!process.env.CI,
7
+ retries: process.env.CI ? 2 : 0,
8
+ workers: process.env.CI ? 1 : undefined,
9
+ reporter: process.env.CI ? 'github' : [['list'], ['html']],
10
+ use: {
11
+ baseURL: 'http://localhost:5173',
12
+ trace: 'on-first-retry',
13
+ screenshot: 'only-on-failure',
14
+ },
15
+ expect: {
16
+ timeout: 10000,
17
+ },
18
+ projects: [
19
+ {
20
+ name: 'chromium',
21
+ use: { ...devices['Desktop Chrome'] },
22
+ },
23
+ ],
24
+ webServer: {
25
+ command: 'npm run dev',
26
+ url: 'http://localhost:5173',
27
+ reuseExistingServer: !process.env.CI,
28
+ timeout: 120000,
29
+ },
30
+ });
@@ -0,0 +1 @@
1
+ export { default } from '@react-spa-scaffold/prettier-config/tailwind';
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
2
+ <rect width="32" height="32" rx="6" fill="#3B82F6"/>
3
+ <path d="M8 12h16M8 16h12M8 20h8" stroke="white" stroke-width="2" stroke-linecap="round"/>
4
+ </svg>
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "@react-spa-scaffold/tsconfig/react",
3
+ "compilerOptions": {
4
+ "baseUrl": ".",
5
+ "paths": {
6
+ "@/*": ["./src/*"]
7
+ }
8
+ },
9
+ "include": ["src", "tests"]
10
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "@react-spa-scaffold/tsconfig/react",
3
+ "compilerOptions": {
4
+ "baseUrl": ".",
5
+ "paths": {
6
+ "@/*": ["./src/*"]
7
+ }
8
+ },
9
+ "files": [],
10
+ "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
11
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "@react-spa-scaffold/tsconfig/vite",
3
+ "include": ["vite.config.ts", "vitest.config.ts", "playwright.config.ts"]
4
+ }
@@ -0,0 +1,54 @@
1
+ import path from 'path';
2
+
3
+ import { lingui } from '@lingui/vite-plugin';
4
+ import { sentryVitePlugin } from '@sentry/vite-plugin';
5
+ import tailwindcss from '@tailwindcss/vite';
6
+ import react from '@vitejs/plugin-react';
7
+ import { defineConfig } from 'vite';
8
+
9
+ export default defineConfig({
10
+ plugins: [
11
+ react({
12
+ babel: {
13
+ plugins: ['@lingui/babel-plugin-lingui-macro'],
14
+ },
15
+ }),
16
+ lingui(),
17
+ tailwindcss(),
18
+ // Sentry source map upload (CI only, requires SENTRY_AUTH_TOKEN)
19
+ process.env.SENTRY_AUTH_TOKEN
20
+ ? sentryVitePlugin({
21
+ org: process.env.SENTRY_ORG,
22
+ project: process.env.SENTRY_PROJECT,
23
+ authToken: process.env.SENTRY_AUTH_TOKEN,
24
+ sourcemaps: {
25
+ filesToDeleteAfterUpload: ['./dist/**/*.map'],
26
+ },
27
+ telemetry: false,
28
+ })
29
+ : null,
30
+ ].filter(Boolean),
31
+ resolve: {
32
+ alias: {
33
+ '@': path.resolve(__dirname, './src'),
34
+ },
35
+ },
36
+ css: {
37
+ devSourcemap: true,
38
+ },
39
+ build: {
40
+ // Source maps: 'hidden' for Sentry (uploaded then deleted), false otherwise
41
+ // Set to true locally if needed for debugging: VITE_SOURCEMAP=true npm run build
42
+ sourcemap: process.env.SENTRY_AUTH_TOKEN ? 'hidden' : process.env.VITE_SOURCEMAP === 'true',
43
+ chunkSizeWarningLimit: 500,
44
+ rollupOptions: {
45
+ output: {
46
+ manualChunks: {
47
+ vendor: ['react', 'react-dom', 'react-router'],
48
+ i18n: ['@lingui/core', '@lingui/react'],
49
+ ui: ['@radix-ui/react-slot', 'class-variance-authority'],
50
+ },
51
+ },
52
+ },
53
+ },
54
+ });
@@ -0,0 +1,38 @@
1
+ import path from 'path';
2
+
3
+ import react from '@vitejs/plugin-react';
4
+ import { defineConfig } from 'vitest/config';
5
+
6
+ export default defineConfig({
7
+ plugins: [
8
+ react({
9
+ babel: {
10
+ plugins: ['@lingui/babel-plugin-lingui-macro'],
11
+ },
12
+ }),
13
+ ],
14
+ test: {
15
+ globals: true,
16
+ environment: 'jsdom',
17
+ setupFiles: ['./src/test-setup.ts'],
18
+ include: ['src/**/*.test.{ts,tsx}', 'tests/**/*.test.{ts,tsx}'],
19
+ clearMocks: true,
20
+ restoreMocks: true,
21
+ coverage: {
22
+ provider: 'v8',
23
+ reporter: ['text', 'json', 'html', 'lcov'],
24
+ exclude: ['**/*.test.{ts,tsx}', '**/index.ts', 'src/types/**', 'src/components/ui/**'],
25
+ thresholds: {
26
+ lines: 80,
27
+ functions: 80,
28
+ statements: 80,
29
+ branches: 80,
30
+ },
31
+ },
32
+ },
33
+ resolve: {
34
+ alias: {
35
+ '@': path.resolve(__dirname, './src'),
36
+ },
37
+ },
38
+ });