@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.
- package/README.md +1 -1
- package/dist/tools/get-example.d.ts +4 -6
- package/dist/tools/get-example.d.ts.map +1 -1
- package/dist/tools/get-example.js +1 -1
- package/dist/tools/get-example.js.map +1 -1
- package/dist/tools/get-scaffold.d.ts +2 -10
- package/dist/tools/get-scaffold.d.ts.map +1 -1
- package/dist/tools/get-scaffold.js +11 -3
- package/dist/tools/get-scaffold.js.map +1 -1
- package/package.json +2 -2
- package/templates/.env.example +21 -0
- package/templates/.github/PULL_REQUEST_TEMPLATE.md +24 -0
- package/templates/.github/actions/setup-node-deps/action.yml +32 -0
- package/templates/.github/dependabot.yml +104 -0
- package/templates/.github/workflows/ci.yml +156 -0
- package/templates/.husky/commit-msg +1 -0
- package/templates/.husky/pre-commit +2 -0
- package/templates/.nvmrc +1 -0
- package/templates/commitlint.config.js +1 -0
- package/templates/components.json +21 -0
- package/templates/e2e/fixtures/index.ts +10 -0
- package/templates/e2e/tests/home.spec.ts +42 -0
- package/templates/e2e/tests/language.spec.ts +42 -0
- package/templates/e2e/tests/navigation.spec.ts +18 -0
- package/templates/e2e/tests/theme.spec.ts +35 -0
- package/templates/eslint.config.js +42 -0
- package/templates/index.html +13 -0
- package/templates/lighthouse-budget.json +17 -0
- package/templates/lighthouserc.json +23 -0
- package/templates/lingui.config.js +18 -0
- package/templates/package.json +125 -0
- package/templates/playwright.config.ts +30 -0
- package/templates/prettier.config.js +1 -0
- package/templates/public/favicon.svg +4 -0
- package/templates/tsconfig.app.json +10 -0
- package/templates/tsconfig.json +11 -0
- package/templates/tsconfig.node.json +4 -0
- package/templates/vite.config.ts +54 -0
- package/templates/vitest.config.ts +38 -0
package/README.md
CHANGED
|
@@ -7,12 +7,10 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { z } from 'zod';
|
|
9
9
|
export declare const getExampleSchema: z.ZodObject<{
|
|
10
|
-
pattern: z.ZodEnum<
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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.
|
|
11
|
+
features: z.ZodArray<z.ZodString>;
|
|
12
12
|
projectName: z.ZodOptional<z.ZodString>;
|
|
13
13
|
includeExamples: z.ZodOptional<z.ZodBoolean>;
|
|
14
|
-
},
|
|
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
|
|
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
|
-
.
|
|
16
|
-
|
|
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":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,0BAA0B,EAAE,kBAAkB,EAAoB,MAAM,mBAAmB,CAAC;AAEtH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC;IACxC,QAAQ,EAAE,CAAC;SACR,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;SACjB,GAAG,CAAC,EAAE,EAAE,6BAA6B,CAAC;SACtC,
|
|
1
|
+
{"version":3,"file":"get-scaffold.js","sourceRoot":"","sources":["../../src/tools/get-scaffold.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,0BAA0B,EAAE,kBAAkB,EAAoB,MAAM,mBAAmB,CAAC;AAEtH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC;IACxC,QAAQ,EAAE,CAAC;SACR,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;SACjB,GAAG,CAAC,EAAE,EAAE,6BAA6B,CAAC;SACtC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;QACb,MAAM,eAAe,GAAG,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,CAAC,CAAC;QAC1E,IAAI,eAAe,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC/B,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;gBACd,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,qBAAqB,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;gBAC5F,KAAK,EAAE,GAAG,CAAC,KAAK;gBAChB,IAAI,EAAE,EAAE;aACT,CAAC,CAAC;QACL,CAAC;IACH,CAAC,CAAC;SACD,QAAQ,CAAC,mEAAmE,CAAC;IAChF,WAAW,EAAE,CAAC;SACX,MAAM,EAAE;SACR,GAAG,CAAC,EAAE,EAAE,uBAAuB,CAAC;SAChC,KAAK,CAAC,cAAc,EAAE,mEAAmE,CAAC;SAC1F,QAAQ,EAAE;SACV,QAAQ,CAAC,iDAAiD,CAAC;IAC9D,eAAe,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,iEAAiE,CAAC;CACpH,CAAC,CAAC;AAIH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,KAAuB;IACvD,MAAM,EAAE,QAAQ,EAAE,WAAW,GAAG,QAAQ,EAAE,eAAe,GAAG,KAAK,EAAE,GAAG,KAAK,CAAC;IAE5E,uBAAuB;IACvB,MAAM,gBAAgB,GAAG,0BAA0B,CAAC,QAAQ,CAAC,CAAC;IAE9D,sBAAsB;IACtB,MAAM,QAAQ,GAAG,MAAM,eAAe,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;IAE9D,wBAAwB;IACxB,MAAM,cAAc,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE;QACjD,MAAM,OAAO,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;QAC7B,OAAO;YACL,EAAE;YACF,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,qBAAqB,EAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC5C,eAAe,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YACvC,QAAQ,EAAE,OAAO,CAAC,QAAQ;SAC3B,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,8BAA8B;IAC9B,IAAI,QAAmD,CAAC;IACxD,IAAI,eAAe,EAAE,CAAC;QACpB,QAAQ,GAAG,EAAE,CAAC;QACd,KAAK,MAAM,EAAE,IAAI,gBAAgB,EAAE,CAAC;YAClC,MAAM,OAAO,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;YAC7B,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAChC,QAAQ,CAAC,EAAE,CAAC,GAAG,MAAM,kBAAkB,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;YAC5D,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO;QACL,WAAW;QACX,gBAAgB,EAAE,QAAQ;QAC1B,gBAAgB;QAChB,cAAc;QACd,WAAW,EAAE,QAAQ,CAAC,WAAW;QACjC,aAAa,EAAE,QAAQ,CAAC,SAAS;QACjC,WAAW,EAAE,QAAQ,CAAC,WAAW;QACjC,aAAa,EAAE,QAAQ,CAAC,aAAa;QACrC,QAAQ,EAAE,QAAQ,CAAC,QAAQ;QAC3B,UAAU,EAAE,QAAQ,CAAC,UAAU;QAC/B,KAAK,EAAE,QAAQ,CAAC,KAAK;QACrB,IAAI,EAAE,QAAQ,CAAC,IAAI;QACnB,QAAQ;QACR,YAAY,EAAE,oBAAoB,CAAC,WAAW,EAAE,QAAQ,CAAC,aAAa,CAAC;KACxE,CAAC;AACJ,CAAC;AAED,SAAS,oBAAoB,CAAC,WAAmB,EAAE,aAAuB;IACxE,OAAO;;;;WAIE,WAAW,UAAU,WAAW;;;;;;;;;;;;;;;;;KAiBtC,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC;;;;;;;;;;;;;;;uDAeuB,CAAC;AACxD,CAAC;AAED,MAAM,CAAC,MAAM,yBAAyB,GAAG;IACvC,IAAI,EAAE,cAAc;IACpB,WAAW,EAAE;;;;;;;;;;;;;;;;;;;uDAmBwC;IACrD,WAAW,EAAE;QACX,IAAI,EAAE,QAAiB;QACvB,UAAU,EAAE;YACV,QAAQ,EAAE;gBACR,IAAI,EAAE,OAAgB;gBACtB,KAAK,EAAE,EAAE,IAAI,EAAE,QAAiB,EAAE;gBAClC,WAAW,EAAE,gCAAgC;aAC9C;YACD,WAAW,EAAE;gBACX,IAAI,EAAE,QAAiB;gBACvB,WAAW,EAAE,0BAA0B;aACxC;YACD,eAAe,EAAE;gBACf,IAAI,EAAE,SAAkB;gBACxB,WAAW,EAAE,oCAAoC;aAClD;SACF;QACD,QAAQ,EAAE,CAAC,UAAU,CAAC;KACvB;CACF,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@react-spa-scaffold/mcp",
|
|
3
|
-
"version": "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": "^
|
|
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
|
package/templates/.nvmrc
ADDED
|
@@ -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,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
|
+
});
|