create-react-starter-kit 1.0.6 → 1.0.7

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 (76) hide show
  1. package/dist/cli-args.d.ts.map +1 -1
  2. package/dist/cli-args.js +2 -1
  3. package/dist/cli-args.js.map +1 -1
  4. package/dist/generator/index.d.ts.map +1 -1
  5. package/dist/generator/index.js +1 -0
  6. package/dist/generator/index.js.map +1 -1
  7. package/dist/prompts/index.d.ts.map +1 -1
  8. package/dist/prompts/index.js +8 -0
  9. package/dist/prompts/index.js.map +1 -1
  10. package/dist/templates/README.md.ejs +1 -1
  11. package/dist/templates/__when_apiLayer_axios__/src/services/api.ts.ejs +23 -0
  12. package/dist/templates/__when_apiLayer_axios__/src/services/http/client.ts.ejs +174 -0
  13. package/dist/templates/__when_apiLayer_axios__/src/services/http/errors.ts.ejs +81 -0
  14. package/dist/templates/__when_apiLayer_axios__/src/services/http/index.ts.ejs +5 -0
  15. package/dist/templates/__when_apiLayer_fetch__/src/services/api.ts.ejs +22 -0
  16. package/dist/templates/__when_apiLayer_fetch__/src/services/http/client.ts.ejs +195 -0
  17. package/dist/templates/__when_apiLayer_fetch__/src/services/http/errors.ts.ejs +81 -0
  18. package/dist/templates/__when_apiLayer_fetch__/src/services/http/index.ts.ejs +5 -0
  19. package/dist/templates/__when_buildTool_webpack__/webpack.config.js.ejs +1 -1
  20. package/dist/templates/__when_e2eTesting_cypress__/cypress.config.ts.ejs +2 -1
  21. package/dist/templates/__when_e2eTesting_playwright__/playwright.config.ts.ejs +4 -3
  22. package/dist/templates/__when_serverState_rtk-query__/src/store/postsApi.ts.ejs +28 -5
  23. package/dist/templates/__when_unitTesting_vitest__/tests/feedback.test.tsx.ejs +16 -0
  24. package/dist/templates/__when_unitTesting_vitest__/tests/posts.test.tsx.ejs +1 -3
  25. package/dist/templates/docs/architecture.md.ejs +44 -25
  26. package/dist/templates/docs/best-practices.md.ejs +39 -3
  27. package/dist/templates/docs/examples/README.md.ejs +10 -10
  28. package/dist/templates/docs/examples/api.md.ejs +165 -8
  29. package/dist/templates/docs/examples/data-fetching.md.ejs +107 -6
  30. package/dist/templates/docs/examples/forms.md.ejs +66 -6
  31. package/dist/templates/docs/examples/state.md.ejs +50 -6
  32. package/dist/templates/docs/examples/styling.md.ejs +56 -8
  33. package/dist/templates/docs/examples/testing.md.ejs +63 -9
  34. package/dist/templates/docs/getting-started.md.ejs +63 -15
  35. package/dist/templates/docs/selected-stack.md.ejs +110 -8
  36. package/dist/templates/src/features/feedback/FeedbackForm.tsx.ejs +10 -0
  37. package/dist/templates/src/features/posts/PostsPanel.tsx.ejs +1 -1
  38. package/dist/templates/src/routes/PostsPage.tsx.ejs +7 -2
  39. package/dist/templates/src/types/index.ts.ejs +1 -1
  40. package/dist/templates/src/types/models.ts.ejs +18 -0
  41. package/dist/types.d.ts +1 -1
  42. package/dist/types.d.ts.map +1 -1
  43. package/package.json +1 -1
  44. package/templates/README.md.ejs +1 -1
  45. package/templates/__when_apiLayer_axios__/src/services/api.ts.ejs +23 -0
  46. package/templates/__when_apiLayer_axios__/src/services/http/client.ts.ejs +174 -0
  47. package/templates/__when_apiLayer_axios__/src/services/http/errors.ts.ejs +81 -0
  48. package/templates/__when_apiLayer_axios__/src/services/http/index.ts.ejs +5 -0
  49. package/templates/__when_apiLayer_fetch__/src/services/api.ts.ejs +22 -0
  50. package/templates/__when_apiLayer_fetch__/src/services/http/client.ts.ejs +195 -0
  51. package/templates/__when_apiLayer_fetch__/src/services/http/errors.ts.ejs +81 -0
  52. package/templates/__when_apiLayer_fetch__/src/services/http/index.ts.ejs +5 -0
  53. package/templates/__when_buildTool_webpack__/webpack.config.js.ejs +1 -1
  54. package/templates/__when_e2eTesting_cypress__/cypress.config.ts.ejs +2 -1
  55. package/templates/__when_e2eTesting_playwright__/playwright.config.ts.ejs +4 -3
  56. package/templates/__when_serverState_rtk-query__/src/store/postsApi.ts.ejs +28 -5
  57. package/templates/__when_unitTesting_vitest__/tests/feedback.test.tsx.ejs +16 -0
  58. package/templates/__when_unitTesting_vitest__/tests/posts.test.tsx.ejs +1 -3
  59. package/templates/docs/architecture.md.ejs +44 -25
  60. package/templates/docs/best-practices.md.ejs +39 -3
  61. package/templates/docs/examples/README.md.ejs +10 -10
  62. package/templates/docs/examples/api.md.ejs +165 -8
  63. package/templates/docs/examples/data-fetching.md.ejs +107 -6
  64. package/templates/docs/examples/forms.md.ejs +66 -6
  65. package/templates/docs/examples/state.md.ejs +50 -6
  66. package/templates/docs/examples/styling.md.ejs +56 -8
  67. package/templates/docs/examples/testing.md.ejs +63 -9
  68. package/templates/docs/getting-started.md.ejs +63 -15
  69. package/templates/docs/selected-stack.md.ejs +110 -8
  70. package/templates/src/features/feedback/FeedbackForm.tsx.ejs +10 -0
  71. package/templates/src/features/posts/PostsPanel.tsx.ejs +1 -1
  72. package/templates/src/routes/PostsPage.tsx.ejs +7 -2
  73. package/templates/src/types/index.ts.ejs +1 -1
  74. package/templates/src/types/models.ts.ejs +18 -0
  75. package/dist/templates/src/services/api.ts.ejs +0 -84
  76. package/templates/src/services/api.ts.ejs +0 -84
@@ -0,0 +1,5 @@
1
+ export { apiClient, apiGet, apiPost, apiPut, apiPatch, apiDelete, ApiError } from './client';
2
+ <% if (isTypeScript) { -%>
3
+ export type { ApiErrorCode } from './errors';
4
+ export type { RequestOptions } from './client';
5
+ <% } -%>
@@ -0,0 +1,22 @@
1
+ import { apiGet, apiPost } from '@/services/http';
2
+ <% if (isTypeScript) { -%>
3
+ import type { FeedbackPayload, Post } from '@/types/models';
4
+ <% } -%>
5
+
6
+ /**
7
+ * Domain API methods — keep React components free of raw URLs and HTTP details.
8
+ */
9
+ export async function fetchPosts(signal<% if (isTypeScript) { %>?: AbortSignal<% } %>) {
10
+ return apiGet<% if (isTypeScript) { %><Post[]><% } %>('/posts?_limit=5', { signal });
11
+ }
12
+
13
+ export async function createFeedback(
14
+ payload<% if (isTypeScript) { %>: FeedbackPayload<% } %>,
15
+ signal<% if (isTypeScript) { %>?: AbortSignal<% } %>,
16
+ ) {
17
+ return apiPost('/posts', payload, { signal });
18
+ }
19
+
20
+ <% if (isTypeScript) { -%>
21
+ export type { FeedbackPayload, Post } from '@/types/models';
22
+ <% } -%>
@@ -0,0 +1,195 @@
1
+ import { env } from '@/config/env';
2
+ import { ApiError, mapStatusToCode } from './errors';
3
+
4
+ const REQUEST_ID_HEADER = 'X-Request-Id';
5
+ const DEFAULT_TIMEOUT_MS = 15_000;
6
+
7
+ <% if (isTypeScript) { -%>
8
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
9
+
10
+ export interface RequestOptions extends Omit<RequestInit, 'method' | 'body'> {
11
+ method?: HttpMethod;
12
+ body?: unknown;
13
+ timeoutMs?: number;
14
+ /** Skip JSON Content-Type / body serialization when sending FormData, etc. */
15
+ rawBody?: boolean;
16
+ }
17
+ <% } -%>
18
+
19
+ /**
20
+ * Swap this for your real auth storage (cookie, memory store, secure storage).
21
+ */
22
+ function getAccessToken()<% if (isTypeScript) { %>: string | null<% } %> {
23
+ if (typeof window === 'undefined') return null;
24
+ return window.localStorage.getItem('access_token');
25
+ }
26
+
27
+ function createRequestId()<% if (isTypeScript) { %>: string<% } %> {
28
+ if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {
29
+ return crypto.randomUUID();
30
+ }
31
+ return `req_${Date.now()}_${Math.random().toString(16).slice(2)}`;
32
+ }
33
+
34
+ async function parseErrorBody(response<% if (isTypeScript) { %>: Response<% } %>)<% if (isTypeScript) { %>: Promise<unknown><% } %> {
35
+ const contentType = response.headers.get('content-type') ?? '';
36
+ if (contentType.includes('application/json')) {
37
+ try {
38
+ return await response.json();
39
+ } catch {
40
+ return undefined;
41
+ }
42
+ }
43
+ try {
44
+ return await response.text();
45
+ } catch {
46
+ return undefined;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Shared fetch wrapper with auth header, request IDs, timeout, and normalized errors.
52
+ * Prefer domain helpers in `services/api` over calling this from components.
53
+ */
54
+ export async function request<% if (isTypeScript) { %><T = unknown><% } %>(
55
+ path<% if (isTypeScript) { %>: string<% } %>,
56
+ options<% if (isTypeScript) { %>: RequestOptions = {}<% } else { %> = {}<% } %>,
57
+ )<% if (isTypeScript) { %>: Promise<T><% } %> {
58
+ const {
59
+ method = 'GET',
60
+ body,
61
+ headers: initHeaders,
62
+ timeoutMs = DEFAULT_TIMEOUT_MS,
63
+ rawBody = false,
64
+ signal: userSignal,
65
+ ...rest
66
+ } = options;
67
+
68
+ const requestId = createRequestId();
69
+ const headers = new Headers(initHeaders);
70
+ headers.set(REQUEST_ID_HEADER, requestId);
71
+ headers.set('Accept', 'application/json');
72
+
73
+ const token = getAccessToken();
74
+ if (token) {
75
+ headers.set('Authorization', `Bearer ${token}`);
76
+ }
77
+
78
+ let payload<% if (isTypeScript) { %>: BodyInit | undefined<% } %>;
79
+ if (body !== undefined && body !== null) {
80
+ if (rawBody) {
81
+ payload = body<% if (isTypeScript) { %> as BodyInit<% } %>;
82
+ } else {
83
+ headers.set('Content-Type', 'application/json');
84
+ payload = JSON.stringify(body);
85
+ }
86
+ }
87
+
88
+ const controller = new AbortController();
89
+ const onAbort = () => controller.abort();
90
+ if (userSignal) {
91
+ if (userSignal.aborted) controller.abort();
92
+ else userSignal.addEventListener('abort', onAbort, { once: true });
93
+ }
94
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
95
+
96
+ try {
97
+ const response = await fetch(`${env.apiBaseUrl}${path}`, {
98
+ ...rest,
99
+ method,
100
+ headers,
101
+ body: payload,
102
+ signal: controller.signal,
103
+ });
104
+
105
+ if (!response.ok) {
106
+ const details = await parseErrorBody(response);
107
+ const message =
108
+ details && typeof details === 'object' && 'message' in details && typeof details.message === 'string'
109
+ ? details.message
110
+ : `Request failed with status ${response.status}`;
111
+
112
+ const apiError = new ApiError({
113
+ message,
114
+ status: response.status,
115
+ code: mapStatusToCode(response.status),
116
+ details,
117
+ requestId,
118
+ });
119
+
120
+ if (apiError.isUnauthorized && typeof window !== 'undefined') {
121
+ window.dispatchEvent(new CustomEvent('auth:unauthorized', { detail: apiError }));
122
+ }
123
+
124
+ throw apiError;
125
+ }
126
+
127
+ if (response.status === 204) {
128
+ return undefined<% if (isTypeScript) { %> as T<% } %>;
129
+ }
130
+
131
+ return (await response.json())<% if (isTypeScript) { %> as T<% } %>;
132
+ } catch (error) {
133
+ if (error instanceof ApiError) throw error;
134
+
135
+ if (error instanceof DOMException && error.name === 'AbortError') {
136
+ const timedOut = !userSignal?.aborted;
137
+ throw new ApiError({
138
+ message: timedOut ? 'Request timed out. Please try again.' : 'Request was cancelled.',
139
+ code: timedOut ? 'TIMEOUT' : 'NETWORK',
140
+ requestId,
141
+ cause: error,
142
+ });
143
+ }
144
+
145
+ throw new ApiError({
146
+ message: error instanceof Error ? error.message : 'Network error. Check your connection and try again.',
147
+ code: 'NETWORK',
148
+ requestId,
149
+ cause: error,
150
+ });
151
+ } finally {
152
+ clearTimeout(timeoutId);
153
+ userSignal?.removeEventListener('abort', onAbort);
154
+ }
155
+ }
156
+
157
+ export async function apiGet<% if (isTypeScript) { %><T = unknown><% } %>(
158
+ path<% if (isTypeScript) { %>: string<% } %>,
159
+ options<% if (isTypeScript) { %>?: Omit<RequestOptions, 'method' | 'body'><% } %>,
160
+ )<% if (isTypeScript) { %>: Promise<T><% } %> {
161
+ return request<% if (isTypeScript) { %><T><% } %>(path, { ...options, method: 'GET' });
162
+ }
163
+
164
+ export async function apiPost<% if (isTypeScript) { %><T = unknown, B = unknown><% } %>(
165
+ path<% if (isTypeScript) { %>: string<% } %>,
166
+ body<% if (isTypeScript) { %>?: B<% } %>,
167
+ options<% if (isTypeScript) { %>?: Omit<RequestOptions, 'method' | 'body'><% } %>,
168
+ )<% if (isTypeScript) { %>: Promise<T><% } %> {
169
+ return request<% if (isTypeScript) { %><T><% } %>(path, { ...options, method: 'POST', body });
170
+ }
171
+
172
+ export async function apiPut<% if (isTypeScript) { %><T = unknown, B = unknown><% } %>(
173
+ path<% if (isTypeScript) { %>: string<% } %>,
174
+ body<% if (isTypeScript) { %>?: B<% } %>,
175
+ options<% if (isTypeScript) { %>?: Omit<RequestOptions, 'method' | 'body'><% } %>,
176
+ )<% if (isTypeScript) { %>: Promise<T><% } %> {
177
+ return request<% if (isTypeScript) { %><T><% } %>(path, { ...options, method: 'PUT', body });
178
+ }
179
+
180
+ export async function apiPatch<% if (isTypeScript) { %><T = unknown, B = unknown><% } %>(
181
+ path<% if (isTypeScript) { %>: string<% } %>,
182
+ body<% if (isTypeScript) { %>?: B<% } %>,
183
+ options<% if (isTypeScript) { %>?: Omit<RequestOptions, 'method' | 'body'><% } %>,
184
+ )<% if (isTypeScript) { %>: Promise<T><% } %> {
185
+ return request<% if (isTypeScript) { %><T><% } %>(path, { ...options, method: 'PATCH', body });
186
+ }
187
+
188
+ export async function apiDelete<% if (isTypeScript) { %><T = unknown><% } %>(
189
+ path<% if (isTypeScript) { %>: string<% } %>,
190
+ options<% if (isTypeScript) { %>?: Omit<RequestOptions, 'method' | 'body'><% } %>,
191
+ )<% if (isTypeScript) { %>: Promise<T><% } %> {
192
+ return request<% if (isTypeScript) { %><T><% } %>(path, { ...options, method: 'DELETE' });
193
+ }
194
+
195
+ export { ApiError };
@@ -0,0 +1,81 @@
1
+ <% if (isTypeScript) { -%>
2
+ export type ApiErrorCode =
3
+ | 'NETWORK'
4
+ | 'TIMEOUT'
5
+ | 'UNAUTHORIZED'
6
+ | 'FORBIDDEN'
7
+ | 'NOT_FOUND'
8
+ | 'VALIDATION'
9
+ | 'SERVER'
10
+ | 'UNKNOWN';
11
+
12
+ export class ApiError extends Error {
13
+ readonly status?: number;
14
+ readonly code: ApiErrorCode;
15
+ readonly details?: unknown;
16
+ readonly requestId?: string;
17
+
18
+ constructor(options: {
19
+ message: string;
20
+ status?: number;
21
+ code?: ApiErrorCode;
22
+ details?: unknown;
23
+ requestId?: string;
24
+ cause?: unknown;
25
+ }) {
26
+ super(options.message, options.cause ? { cause: options.cause } : undefined);
27
+ this.name = 'ApiError';
28
+ this.status = options.status;
29
+ this.code = options.code ?? 'UNKNOWN';
30
+ this.details = options.details;
31
+ this.requestId = options.requestId;
32
+ }
33
+
34
+ get isUnauthorized() {
35
+ return this.status === 401 || this.code === 'UNAUTHORIZED';
36
+ }
37
+
38
+ get isRetryable() {
39
+ return this.code === 'NETWORK' || this.code === 'TIMEOUT' || this.code === 'SERVER';
40
+ }
41
+ }
42
+
43
+ export function mapStatusToCode(status?: number): ApiErrorCode {
44
+ if (!status) return 'NETWORK';
45
+ if (status === 401) return 'UNAUTHORIZED';
46
+ if (status === 403) return 'FORBIDDEN';
47
+ if (status === 404) return 'NOT_FOUND';
48
+ if (status === 422 || status === 400) return 'VALIDATION';
49
+ if (status >= 500) return 'SERVER';
50
+ return 'UNKNOWN';
51
+ }
52
+ <% } else { -%>
53
+ export class ApiError extends Error {
54
+ constructor(options) {
55
+ super(options.message, options.cause ? { cause: options.cause } : undefined);
56
+ this.name = 'ApiError';
57
+ this.status = options.status;
58
+ this.code = options.code ?? 'UNKNOWN';
59
+ this.details = options.details;
60
+ this.requestId = options.requestId;
61
+ }
62
+
63
+ get isUnauthorized() {
64
+ return this.status === 401 || this.code === 'UNAUTHORIZED';
65
+ }
66
+
67
+ get isRetryable() {
68
+ return this.code === 'NETWORK' || this.code === 'TIMEOUT' || this.code === 'SERVER';
69
+ }
70
+ }
71
+
72
+ export function mapStatusToCode(status) {
73
+ if (!status) return 'NETWORK';
74
+ if (status === 401) return 'UNAUTHORIZED';
75
+ if (status === 403) return 'FORBIDDEN';
76
+ if (status === 404) return 'NOT_FOUND';
77
+ if (status === 422 || status === 400) return 'VALIDATION';
78
+ if (status >= 500) return 'SERVER';
79
+ return 'UNKNOWN';
80
+ }
81
+ <% } -%>
@@ -0,0 +1,5 @@
1
+ export { request, apiGet, apiPost, apiPut, apiPatch, apiDelete, ApiError } from './client';
2
+ <% if (isTypeScript) { -%>
3
+ export type { ApiErrorCode } from './errors';
4
+ export type { HttpMethod, RequestOptions } from './client';
5
+ <% } -%>
@@ -105,7 +105,7 @@ export default (_env, argv) => {
105
105
  }),
106
106
  ],
107
107
  devServer: {
108
- port: 5173,
108
+ port: 3000,
109
109
  historyApiFallback: true,
110
110
  hot: true,
111
111
  open: false,
@@ -1,8 +1,9 @@
1
1
  import { defineConfig } from 'cypress';
2
2
 
3
+ <% const e2ePort = buildTool === 'webpack' ? 3000 : 5173; -%>
3
4
  export default defineConfig({
4
5
  e2e: {
5
- baseUrl: 'http://127.0.0.1:5173',
6
+ baseUrl: 'http://127.0.0.1:<%= e2ePort %>',
6
7
  supportFile: false,
7
8
  },
8
9
  });
@@ -3,6 +3,7 @@ import { defineConfig, devices } from '@playwright/test';
3
3
  /**
4
4
  * Run once before first e2e: `<%= packageManager === 'npm' ? 'npm run' : packageManager %> test:e2e:install`
5
5
  */
6
+ <% const e2ePort = buildTool === 'webpack' ? 3000 : 5173; -%>
6
7
  export default defineConfig({
7
8
  testDir: './tests/e2e',
8
9
  fullyParallel: true,
@@ -10,7 +11,7 @@ export default defineConfig({
10
11
  retries: process.env.CI ? 1 : 0,
11
12
  reporter: process.env.CI ? 'github' : 'list',
12
13
  use: {
13
- baseURL: 'http://127.0.0.1:5173',
14
+ baseURL: 'http://127.0.0.1:<%= e2ePort %>',
14
15
  trace: 'on-first-retry',
15
16
  screenshot: 'only-on-failure',
16
17
  },
@@ -19,13 +20,13 @@ export default defineConfig({
19
20
  '<%= buildTool === 'vite'
20
21
  ? 'npx vite --host 127.0.0.1 --port 5173'
21
22
  : buildTool === 'webpack'
22
- ? 'npx webpack serve --mode development --port 5173'
23
+ ? 'npx webpack serve --mode development --port 3000'
23
24
  : packageManager === 'npm'
24
25
  ? 'npm run dev'
25
26
  : packageManager === 'yarn'
26
27
  ? 'yarn dev'
27
28
  : 'pnpm dev' %>',
28
- url: 'http://127.0.0.1:5173',
29
+ url: 'http://127.0.0.1:<%= e2ePort %>',
29
30
  reuseExistingServer: !process.env.CI,
30
31
  timeout: 120_000,
31
32
  },
@@ -1,19 +1,42 @@
1
1
  import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
2
2
  <% if (isTypeScript) { -%>
3
- import type { Post } from '@/services/api';
3
+ import type { FeedbackPayload, Post } from '@/types/models';
4
4
  <% } -%>
5
5
  import { env } from '@/config/env';
6
6
 
7
- const baseUrl = env.apiBaseUrl;
8
-
7
+ /**
8
+ * RTK Query owns HTTP for this stack — no separate Axios/Fetch client is generated.
9
+ * Use `prepareHeaders` for auth and `tagTypes` for cache invalidation.
10
+ */
9
11
  export const postsApi = createApi({
10
12
  reducerPath: 'postsApi',
11
- baseQuery: fetchBaseQuery({ baseUrl }),
13
+ baseQuery: fetchBaseQuery({
14
+ baseUrl: env.apiBaseUrl,
15
+ prepareHeaders: (headers) => {
16
+ const token =
17
+ typeof window !== 'undefined' ? window.localStorage.getItem('access_token') : null;
18
+ if (token) {
19
+ headers.set('Authorization', `Bearer ${token}`);
20
+ }
21
+ headers.set('Accept', 'application/json');
22
+ return headers;
23
+ },
24
+ }),
25
+ tagTypes: ['Posts'],
12
26
  endpoints: (builder) => ({
13
27
  getPosts: builder.query<% if (isTypeScript) { %><Post[], void><% } %>({
14
28
  query: () => '/posts?_limit=5',
29
+ providesTags: ['Posts'],
30
+ }),
31
+ createFeedback: builder.mutation<% if (isTypeScript) { %><{ id: number }, FeedbackPayload><% } %>({
32
+ query: (body) => ({
33
+ url: '/posts',
34
+ method: 'POST',
35
+ body,
36
+ }),
37
+ invalidatesTags: ['Posts'],
15
38
  }),
16
39
  }),
17
40
  });
18
41
 
19
- export const { useGetPostsQuery } = postsApi;
42
+ export const { useGetPostsQuery, useCreateFeedbackMutation } = postsApi;
@@ -4,6 +4,19 @@ import userEvent from '@testing-library/user-event';
4
4
  import { FeedbackForm } from '@/features/feedback/FeedbackForm';
5
5
  import { renderWithProviders } from './test-utils';
6
6
 
7
+ <% if (serverState === 'rtk-query') { -%>
8
+ const createFeedback = vi.fn(() => ({
9
+ unwrap: vi.fn(async () => ({ id: 101 })),
10
+ }));
11
+
12
+ vi.mock('@/store/postsApi', async (importOriginal) => {
13
+ const actual = await importOriginal<% if (isTypeScript) { %><typeof import('@/store/postsApi')><% } %>();
14
+ return {
15
+ ...actual,
16
+ useCreateFeedbackMutation: () => [createFeedback, { isLoading: false }],
17
+ };
18
+ });
19
+ <% } else { -%>
7
20
  vi.mock('@/services/api', async (importOriginal) => {
8
21
  const actual = await importOriginal<% if (isTypeScript) { %><typeof import('@/services/api')><% } %>();
9
22
  return {
@@ -11,6 +24,7 @@ vi.mock('@/services/api', async (importOriginal) => {
11
24
  createFeedback: vi.fn(async () => ({ id: 101 })),
12
25
  };
13
26
  });
27
+ <% } -%>
14
28
 
15
29
  describe('FeedbackForm', () => {
16
30
  beforeEach(() => {
@@ -30,7 +44,9 @@ describe('FeedbackForm', () => {
30
44
 
31
45
  it('submits valid feedback and shows success status', async () => {
32
46
  const user = userEvent.setup();
47
+ <% if (serverState !== 'rtk-query') { -%>
33
48
  const { createFeedback } = await import('@/services/api');
49
+ <% } -%>
34
50
  renderWithProviders(<FeedbackForm />);
35
51
 
36
52
  await user.type(screen.getByPlaceholderText('Ada Lovelace'), 'Ada Lovelace');
@@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event';
4
4
  import { PostsPanel } from '@/features/posts/PostsPanel';
5
5
  import { renderWithProviders } from './test-utils';
6
6
 
7
+ <% if (serverState !== 'rtk-query') { -%>
7
8
  vi.mock('@/services/api', async (importOriginal) => {
8
9
  const actual = await importOriginal<% if (isTypeScript) { %><typeof import('@/services/api')><% } %>();
9
10
  return {
@@ -14,9 +15,6 @@ vi.mock('@/services/api', async (importOriginal) => {
14
15
  ]),
15
16
  };
16
17
  });
17
-
18
- <% if (serverState === 'rtk-query') { -%>
19
- // RTK Query uses its own endpoint; mock still covers the transport used by other modes.
20
18
  <% } -%>
21
19
 
22
20
  describe('PostsPanel', () => {
@@ -21,52 +21,71 @@ You selected a coherent default-friendly stack during scaffolding. The generator
21
21
 
22
22
  ```text
23
23
  src/
24
- main.<%= jsxExt %> # bootstrap + render
25
- providers.<%= jsxExt %>
26
- App.<%= jsxExt %> # route tree
24
+ main.<%= jsxExt %> # bootstrap + render
25
+ providers.<%= jsxExt %> # global providers (store, query, theme)
26
+ App.<%= jsxExt %> # route tree
27
27
  assets/
28
- components/ # shared UI (layout/, ui/)
29
- config/
30
- features/ # domain modules
28
+ components/ # shared UI (layout/, ui/)
29
+ config/ # env + app config
30
+ features/ # domain modules (counter, posts, feedback)
31
31
  hooks/
32
- lib/
33
- routes/ # / /counter /posts /feedback /about
34
- services/
35
- store/
32
+ lib/ # validation, monitoring helpers
33
+ routes/ # page-level route components
34
+ services/ # HTTP client + domain API (when selected)
35
+ store/ # client state + RTK Query APIs (when selected)
36
36
  styles/
37
- types/
37
+ types/ # shared domain models
38
38
  utils/
39
39
  ```
40
40
 
41
41
  **Rules of thumb**
42
42
 
43
43
  - Put new product work in `src/features/<domain>/`.
44
- - Keep `components/` dumb and reusable.
44
+ - Keep `components/` presentational and reusable.
45
45
  - Import with the `@/` alias.
46
46
  - Prefer a feature's public surface over deep cross-feature imports.
47
+ - Keep providers thin — wiring belongs in `providers.<%= jsxExt %>`, business logic does not.
47
48
 
48
49
  ## Routes
49
50
 
50
- | Path | Purpose |
51
- |------|---------|
52
- | `/` | Home overview |
53
- | `/counter` | Client state demo |
54
- | `/posts` | Server state / data fetching |
55
- | `/feedback` | Forms + validation |
56
- | `/about` | Orientation |
51
+ | Path | Purpose | Primary demo |
52
+ |------|---------|--------------|
53
+ | `/` | Home overview | Stack orientation |
54
+ | `/counter` | Client state | <%= stackLabels.stateManagement %> |
55
+ | `/posts` | Server state | <%= stackLabels.serverState %> |
56
+ | `/feedback` | Forms + validation | <%= stackLabels.forms %> |
57
+ | `/about` | Orientation | Architecture notes |
57
58
 
58
59
  ## Path aliases
59
60
 
60
- `@/*` maps to `src/*` in TypeScript/JS config and in your bundler.
61
+ `@/*` maps to `src/*` in TypeScript/JS config and in your bundler. Prefer `@/features/...` over long relative paths.
61
62
 
62
63
  ## Runtime composition
63
64
 
64
- `src/main.<%= jsxExt %>` → `AppProviders` → `App` (router) → layout → pages.
65
+ ```text
66
+ main → AppProviders → App (router) → layout → page → feature panels
67
+ ```
65
68
 
66
- Providers are ordered: client state → server cache → UI theme.
69
+ Provider order matters: client store → server cache → UI theme.
67
70
 
68
71
  ## Boundaries
69
72
 
70
- - **UI** should call HTTP through `services/`.
71
- - **Schemas** live in `lib/validation`.
72
- - **Env access** stays in `config/`, `services/`, and `lib/monitoring`.
73
+ <% if (serverState === 'rtk-query') { -%>
74
+ - **HTTP** for remote resources lives in RTK Query APIs (`src/store/*Api.<%= ext %>`), not in components.
75
+ - Use `fetchBaseQuery` + `prepareHeaders` for auth, base URL, and shared headers.
76
+ - Do **not** add a parallel Axios/Fetch client unless you have a clear non-RTK integration (uploads to a different host, third-party SDK, etc.).
77
+ <% } else { -%>
78
+ - **UI** should call domain helpers in `src/services/api.<%= ext %>`.
79
+ - **Transport** details (interceptors, tokens, timeouts) stay in `src/services/http/`.
80
+ - Components should not call `axios` / `fetch` directly.
81
+ <% } -%>
82
+ - **Schemas** live in `lib/validation` and are reused by forms and API payloads.
83
+ - **Env access** stays in `config/`, services/store, and `lib/monitoring`.
84
+ - **Client state** holds UI/ephemeral state; **server state** caches remote entities.
85
+
86
+ ## Scaling tips
87
+
88
+ 1. Split large features into `components/`, `hooks/`, and `api/` subfolders inside the feature.
89
+ 2. Promote shared types into `src/types/` once two features need them.
90
+ 3. Keep route files thin — they compose features, they should not own data logic.
91
+ 4. Prefer explicit query keys / endpoint names over anonymous fetch calls.
@@ -2,9 +2,24 @@
2
2
 
3
3
  ## Code organization
4
4
 
5
- - Colocate tests next to units or under `tests/` for cross-cutting suites.
5
+ - Colocate feature UI, hooks, and tests under `src/features/<domain>/`.
6
6
  - Prefer small modules with clear exports over “utils dump” files.
7
7
  - Name features after domains (`billing`, `auth`), not technical junk drawers.
8
+ - Keep route files as composers — they should not own fetching or form schemas.
9
+
10
+ ## HTTP & data
11
+
12
+ <% if (serverState === 'rtk-query') { -%>
13
+ - Define one API slice per domain (or a carefully shared base API).
14
+ - Use `tagTypes` for cache invalidation instead of manual refetches when possible.
15
+ - Put auth in `prepareHeaders`.
16
+ - Prefer generated hooks in components; avoid dispatching raw endpoint actions unless needed.
17
+ <% } else { -%>
18
+ - Never call `axios` / `fetch` from React components — use `src/services/api.<%= ext %>`.
19
+ - Keep interceptors and error normalization in `src/services/http/`.
20
+ - Pass `AbortSignal` from query libraries into HTTP helpers.
21
+ <% } -%>
22
+ - Treat server cache and client UI state as separate concerns.
8
23
 
9
24
  ## Linting & formatting
10
25
 
@@ -13,16 +28,27 @@
13
28
  - Husky + lint-staged formats staged files so diffs stay clean.
14
29
  - commitlint enforces Conventional Commits on `git commit`.
15
30
 
31
+ **Commit style examples**
32
+
33
+ ```text
34
+ feat(posts): add pagination to posts query
35
+ fix(feedback): surface API validation errors
36
+ chore(deps): bump testing-library
37
+ ```
38
+
16
39
  ## Environment variables
17
40
 
18
41
  - Never commit `.env`.
19
- - Prefix client-visible vars for your bundler.
42
+ - Prefix client-visible vars for your bundler (`VITE_*`).
20
43
  - Validate required env at boot for production deploys.
44
+ - Document every variable in `.env.example` with a one-line purpose.
21
45
 
22
46
  ## Testing strategy
23
47
 
24
48
  <% if (unitTesting === 'vitest' || unitTesting === 'jest') { -%>
25
49
  - Unit/integration with **<%= stackLabels.unitTesting %>** + Testing Library for components.
50
+ - Prefer `getByRole` / label text over test IDs.
51
+ - Mock network at the domain boundary (`services/api` or RTK endpoints), not inside random UI helpers.
26
52
  <% } else if (unitTesting === 'rtl') { -%>
27
53
  - React Testing Library is installed — pair it with a runner of your choice.
28
54
  <% } else { -%>
@@ -30,19 +56,27 @@
30
56
  <% } -%>
31
57
  <% if (e2eTesting !== 'none') { -%>
32
58
  - E2E with **<%= stackLabels.e2eTesting %>** for critical user journeys (load home, submit form).
59
+ - Keep e2e thin: happy paths + one failure path per critical flow.
33
60
  <% } -%>
34
61
  <% if (visualTesting === 'storybook') { -%>
35
62
  - Storybook for isolated UI development and visual review.
63
+ - Write stories for states (loading, empty, error, success), not only the happy path.
36
64
  <% } else if (visualTesting === 'chromatic') { -%>
37
65
  - Chromatic for visual regression against baselines.
38
66
  <% } -%>
39
67
 
40
68
  Aim for: fast unit tests on logic, fewer integration tests on features, thin e2e on happy paths.
41
69
 
70
+ ## Accessibility
71
+
72
+ - Prefer semantic HTML (`button`, `label`, `nav`) before ARIA.
73
+ - Ensure keyboard focus order matches visual order.
74
+ - Pair every input with a visible label; surface errors with `aria-live` / `role="status"` where appropriate.
75
+
42
76
  ## Deployment notes
43
77
 
44
78
  1. `<%= packageManager === 'npm' ? 'npm run' : packageManager %> build`
45
- 2. Serve `dist/` (or your bundler's output) behind HTTPS.
79
+ 2. Serve the bundler output (`dist/` / `build/`) behind HTTPS.
46
80
  3. Configure SPA fallback to `index.html` for client routes.
47
81
  4. Inject production env vars in the host (Vercel, Netlify, Cloudflare, nginx, etc.).
48
82
  5. Enable error tracking DSN in production only.
@@ -54,3 +88,5 @@ Aim for: fast unit tests on logic, fewer integration tests on features, thin e2e
54
88
  - [ ] Bundle analyzed for accidental secrets
55
89
  - [ ] Monitoring verified with a test event
56
90
  - [ ] Accessibility pass on primary flows
91
+ - [ ] Error states designed for `/posts` and `/feedback`
92
+ - [ ] Auth expiry path handled (401 → re-login / token refresh)