@salesforce/webapp-template-feature-react-authentication-experimental 1.21.0 → 1.22.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.
@@ -3,85 +3,378 @@
3
3
  Instructs agents to use the established GraphQL utilities for Salesforce data access.
4
4
 
5
5
  ## Targets
6
- - `force-app/main/default/webapplications/*/**/*.ts`
7
- - `force-app/main/default/webapplications/*/**/*.tsx`
6
+ - `force-app/main/default/webApplications/static-app/**/*.ts`
7
+ - `force-app/main/default/webApplications/static-app/**/*.tsx`
8
8
 
9
- ## Required: Use executeGraphQL Function
9
+ ## TypeScript Types & Code Generation
10
10
 
11
- When fetching data via GraphQL in the web app, **always** use the `executeGraphQL` function from `force-app/main/default/webapplications/<appName>/src/api/graphql.ts`.
11
+ ### Generated Types File
12
+ Types are auto-generated at: `force-app/main/default/webApplications/static-app/src/api/graphql-operations-types.ts`
13
+
14
+ ### Generation Command
15
+ ```bash
16
+ cd force-app/main/default/webApplications/static-app && npm run graphql:codegen
17
+ ```
18
+
19
+ The codegen configuration is located at `scripts/graphql/codegen.js` and generates types from:
20
+ - Schema: `schema.graphql` (root level)
21
+ - Documents: `force-app/main/default/webApplications/static-app/src/**/*.{graphql,ts,tsx}`
22
+
23
+ ### Type Naming Convention
24
+ For a GraphQL operation named `GetHighRevenueAccounts`:
25
+ - **Query/Mutation Response Type**: `GetHighRevenueAccountsQuery` or `GetHighRevenueAccountsMutation`
26
+ - **Input Variables Type**: `GetHighRevenueAccountsQueryVariables` or `GetHighRevenueAccountsMutationVariables`
27
+
28
+ ## Core Types & Function Signatures
29
+
30
+ ### executeGraphQL Function
31
+ Located in `force-app/main/default/webApplications/static-app/src/api/graphql.ts`:
12
32
 
13
- ### Import Pattern
14
33
  ```typescript
15
- import { executeGraphQL } from '../api/graphql';
16
- // or adjust relative path as needed based on file location
34
+ function executeGraphQL<T, InputVariables = Record<string, unknown>>(
35
+ query: string,
36
+ variables?: InputVariables
37
+ ): Promise<T>
38
+ ```
39
+
40
+ - `T` - The response type (e.g., `GetHighRevenueAccountsQuery`)
41
+ - `InputVariables` - The variables type (e.g., `GetHighRevenueAccountsQueryVariables`)
42
+
43
+ ### gql Template Tag
44
+ Also exported from `graphql.ts` for inline query definitions:
45
+
46
+ ```typescript
47
+ import { gql } from '../api/graphql';
48
+
49
+ const MY_QUERY = gql`
50
+ query MyQuery {
51
+ uiapi {
52
+ ...
53
+ }
54
+ }
55
+ `;
17
56
  ```
18
57
 
19
- ### Usage Pattern
58
+ The `gql` tag is a template literal that allows defining GraphQL queries inline while maintaining syntax highlighting in most editors.
59
+
60
+ ### GraphQLResponse Shape
61
+ The raw response wrapper (handled internally by `executeGraphQL`):
62
+
20
63
  ```typescript
21
- interface MyQueryResponse {
64
+ interface GraphQLResponse<T> {
65
+ data: T;
66
+ errors?: Array<{
67
+ message: string;
68
+ locations?: Array<{ line: number; column: number }>;
69
+ path?: string[];
70
+ }>;
71
+ }
72
+ ```
73
+
74
+ ### NodeOfConnection Utility Type
75
+ Extract the node type from a connection (edges/node pattern):
76
+
77
+ ```typescript
78
+ import { type NodeOfConnection } from '../api/graphql';
79
+
80
+ // Extract Account node type from the query response
81
+ type AccountNode = NodeOfConnection<
82
+ GetHighRevenueAccountsQuery['uiapi']['query']['Account']
83
+ >;
84
+ ```
85
+
86
+ ### UIAPI Response Shape
87
+ All Salesforce GraphQL queries follow this structure:
88
+
89
+ ```typescript
90
+ interface UIAPIQueryResponse {
22
91
  uiapi: {
23
92
  query: {
24
- MyObject: {
25
- edges: Array<{
26
- node: {
93
+ [ObjectName]: {
94
+ edges?: Array<{
95
+ node?: {
27
96
  Id: string;
28
- Name: { value: string };
29
- // ... other fields
30
- };
31
- }>;
32
- };
97
+ [FieldName]?: { value?: FieldType | null } | null;
98
+ // Reference fields include the related record
99
+ [ReferenceField]?: {
100
+ value?: string | null; // The ID
101
+ [RelatedField]?: { value?: RelatedType | null } | null;
102
+ } | null;
103
+ } | null;
104
+ } | null> | null;
105
+ } | null;
33
106
  };
34
107
  };
35
108
  }
109
+ ```
36
110
 
37
- const MY_QUERY = `
38
- query GetMyData {
39
- uiapi {
40
- query {
41
- MyObject(first: 10) {
42
- edges {
43
- node {
44
- Id
45
- Name { value }
46
- }
111
+ ## Required Workflow
112
+
113
+ There are **two acceptable patterns** for defining GraphQL queries:
114
+
115
+ ### Pattern 1: External .graphql File (Recommended for complex queries)
116
+
117
+ #### Step 1: Create .graphql File
118
+ Store queries in `.graphql` files for codegen to process:
119
+
120
+ ```graphql
121
+ # force-app/main/default/webApplications/static-app/src/api/utils/query/myQuery.graphql
122
+ query GetMyData($myVariable: String) {
123
+ uiapi {
124
+ query {
125
+ MyObject(first: 10, where: { Field: { eq: $myVariable } }) {
126
+ edges {
127
+ node {
128
+ Id
129
+ Name { value }
47
130
  }
48
131
  }
49
132
  }
50
133
  }
51
134
  }
135
+ }
136
+ ```
137
+
138
+ #### Step 2: Run Code Generation
139
+ ```bash
140
+ cd force-app/main/default/webApplications/static-app && npm run graphql:codegen
141
+ ```
142
+
143
+ This generates types in `graphql-operations-types.ts`:
144
+ - `GetMyDataQuery` - response type
145
+ - `GetMyDataQueryVariables` - variables type
146
+
147
+ #### Step 3: Import and Use
148
+ ```typescript
149
+ import { executeGraphQL, type NodeOfConnection } from '../api/graphql';
150
+ import MY_QUERY from './query/myQuery.graphql?raw';
151
+ import type {
152
+ GetMyDataQuery,
153
+ GetMyDataQueryVariables,
154
+ } from '../graphql-operations-types';
155
+
156
+ // Extract node type for cleaner return types
157
+ type MyNode = NodeOfConnection<GetMyDataQuery['uiapi']['query']['MyObject']>;
158
+
159
+ export async function getMyData(
160
+ variables: GetMyDataQueryVariables
161
+ ): Promise<MyNode[]> {
162
+ const response = await executeGraphQL<GetMyDataQuery>(MY_QUERY, variables);
163
+ return response.uiapi?.query?.MyObject?.edges?.map(edge => edge?.node) || [];
164
+ }
165
+ ```
166
+
167
+ **Key imports for Pattern 1:**
168
+ - `executeGraphQL` - Execute the query
169
+ - `NodeOfConnection` - Extract node types from connection responses
170
+ - Query from `.graphql` file with `?raw` suffix
171
+ - Generated types from `graphql-operations-types.ts`
172
+
173
+ **Pattern 1 Benefits:**
174
+ - Full codegen support with automatic type generation
175
+ - Syntax highlighting and validation in `.graphql` files
176
+ - Easier to share queries across multiple files/components
177
+ - Better for complex queries with fragments and multiple variables
178
+ - IDE support for GraphQL (autocomplete, validation)
179
+ - Queries can be tested independently
180
+ - Clear separation of concerns between query definition and usage
181
+
182
+ **Pattern 1 Limitations:**
183
+ - Requires separate file management
184
+ - Extra step to run codegen after query changes
185
+ - More boilerplate (file import with `?raw`, separate file to maintain)
186
+ - Slight overhead for very simple queries
187
+ - Need to navigate between files during development
188
+ - Doesn't support dynamic queries (e.g., the set of fields changes based on runtime conditions and cannot be predetermined)
189
+
190
+ ### Pattern 2: Inline Query with gql Tag (Recommended for simple queries)
191
+
192
+ For simpler queries without variables or when colocation is preferred:
193
+
194
+ ```typescript
195
+ import { executeGraphQL, gql } from '../api/graphql';
196
+ import { type CurrentUserQuery } from '../graphql-operations-types';
197
+
198
+ const CURRENT_USER_QUERY = gql`
199
+ query CurrentUser {
200
+ uiapi {
201
+ currentUser {
202
+ Id
203
+ Name {
204
+ value
205
+ }
206
+ }
207
+ }
208
+ }
52
209
  `;
53
210
 
54
- const response = await executeGraphQL<MyQueryResponse>(MY_QUERY, { /* variables */ });
211
+ interface User {
212
+ id: string;
213
+ name: string;
214
+ }
215
+
216
+ export async function getCurrentUser(): Promise<User | null> {
217
+ try {
218
+ const response = await executeGraphQL<CurrentUserQuery>(CURRENT_USER_QUERY);
219
+
220
+ const userData = response.uiapi.currentUser;
221
+
222
+ if (!userData) {
223
+ throw new Error('No user data found');
224
+ }
225
+
226
+ return {
227
+ id: userData.Id,
228
+ name: userData.Name?.value || 'User',
229
+ };
230
+ } catch (error) {
231
+ console.error('Error fetching user data:', error);
232
+ throw error;
233
+ }
234
+ }
55
235
  ```
56
236
 
57
- ## Reference Example
237
+ **Key imports for Pattern 2:**
238
+ - `executeGraphQL` - Execute the query
239
+ - `gql` - Template tag for inline query definition
240
+ - Generated types from `graphql-operations-types.ts`
241
+
242
+ **Pattern 2 Benefits:**
243
+ - Query is colocated with usage code
244
+ - Supports dynamic queries (e.g., the set of fields changes based on runtime conditions and cannot be predetermined)
245
+ - No separate file to maintain
246
+ - Still gets type-checked against generated types
247
+ - Simpler for straightforward queries
248
+
249
+ **Pattern 2 Limitations:**
250
+ - Inline queries without `gql` template tag are not processed by codegen
251
+ - Must manually ensure query name matches generated types
252
+ - Less suitable for complex queries with fragments
253
+
254
+ ## Reference Examples
58
255
 
59
- See `force-app/main/default/webapplications/<appName>/src/api/utils/accounts.ts` for a complete working example that demonstrates:
60
- 1. TypeScript interface definitions for GraphQL response shapes
61
- 2. GraphQL query construction using UIAPI syntax
62
- 3. Proper typing with `executeGraphQL<T>()`
63
- 4. Data extraction from the response
256
+ ### Pattern 1 Example: accounts.ts
257
+ See `force-app/main/default/webApplications/static-app/src/api/utils/accounts.ts` for Pattern 1:
258
+ 1. Importing query from `.graphql` file with `?raw` suffix
259
+ 2. Importing generated types from `graphql-operations-types.ts`
260
+ 3. Using `NodeOfConnection` to extract node types
261
+ 4. Proper typing with `executeGraphQL<ResponseType>(query, variables)`
262
+ 5. Safe data extraction from the nested response
64
263
 
65
- ## Anti-Patterns (FORBIDDEN)
264
+ ### Pattern 2 Example: user.ts
265
+ See `force-app/main/default/webApplications/static-app/src/api/utils/user.ts` for Pattern 2:
266
+ 1. Using `gql` template tag for inline query definition
267
+ 2. Importing generated types from `graphql-operations-types.ts`
268
+ 3. Simple query without variables
269
+ 4. Error handling with try/catch
270
+ 5. Direct access to `uiapi.currentUser` (non-connection response)
271
+
272
+ ## Conditional Field Selection with Directives
273
+
274
+ For dynamic fieldsets with known fields that should be conditionally included, use GraphQL directives instead of building queries dynamically. This preserves type generation while allowing runtime control.
275
+
276
+ ### Directives
277
+ - **`@include(if: $condition)`** - include field/fragment when `$condition` is `true`
278
+ - **`@skip(if: $condition)`** - skip field/fragment when `$condition` is `true`
279
+
280
+ ### Example with Fragments
281
+ ```graphql
282
+ # static-app/src/api/utils/query/getAccountDetails.graphql
283
+ query GetAccountDetails(
284
+ $id: ID!
285
+ $includeFinancials: Boolean!
286
+ $includeContacts: Boolean!
287
+ ) {
288
+ uiapi {
289
+ query {
290
+ Account(where: { Id: { eq: $id } }) {
291
+ edges {
292
+ node {
293
+ Id
294
+ Name { value }
295
+ ...FinancialFields @include(if: $includeFinancials)
296
+ ...ContactFields @include(if: $includeContacts)
297
+ }
298
+ }
299
+ }
300
+ }
301
+ }
302
+ }
303
+
304
+ fragment FinancialFields on Account {
305
+ AnnualRevenue { value }
306
+ NumberOfEmployees { value }
307
+ }
308
+
309
+ fragment ContactFields on Account {
310
+ Phone { value }
311
+ Website { value }
312
+ }
313
+ ```
314
+
315
+ ### Usage
316
+ ```typescript
317
+ import { executeGraphQL } from '../api/graphql';
318
+ import QUERY from './query/getAccountDetails.graphql?raw';
319
+ import type {
320
+ GetAccountDetailsQuery,
321
+ GetAccountDetailsQueryVariables,
322
+ } from '../graphql-operations-types';
323
+
324
+ const data = await executeGraphQL<GetAccountDetailsQuery>(QUERY, {
325
+ id: accountId,
326
+ includeFinancials: userWantsFinancials,
327
+ includeContacts: userWantsContacts,
328
+ });
329
+ ```
330
+
331
+ ### Benefits
332
+ - Query stays in `.graphql` file (codegen works)
333
+ - Generated types include all possible fields as optional
334
+ - Runtime control over which fields are fetched
335
+ - Better performance when fields aren't needed
336
+ - Type safety is preserved
337
+
338
+ ## Anti-Patterns (Not Recommended)
66
339
 
67
340
  ### Direct API Calls
68
341
  ```typescript
69
- // FORBIDDEN: Direct axios/fetch calls for GraphQL
342
+ // NOT RECOMMENDED: Direct axios/fetch calls for GraphQL
70
343
  const response = await axios.post('/graphql', { query });
71
344
 
72
- // REQUIRED: Use executeGraphQL
345
+ // PREFERRED: Use executeGraphQL
73
346
  const data = await executeGraphQL<ResponseType>(query, variables);
74
347
  ```
75
348
 
76
349
  ### Missing Type Definitions
77
350
  ```typescript
78
- // FORBIDDEN: Untyped GraphQL calls
351
+ // NOT RECOMMENDED: Untyped GraphQL calls
352
+ const data = await executeGraphQL(query);
353
+
354
+ // PREFERRED: Provide response type
355
+ const data = await executeGraphQL<GetMyDataQuery>(query);
356
+ ```
357
+
358
+ ### Plain String Queries (Without gql Tag)
359
+ ```typescript
360
+ // NOT RECOMMENDED: Plain string queries without gql tag
361
+ const query = `query { ... }`;
79
362
  const data = await executeGraphQL(query);
80
363
 
81
- // REQUIRED: Provide response type
82
- const data = await executeGraphQL<MyTypedResponse>(query);
364
+ // PREFERRED: Use gql tag for inline queries
365
+ const QUERY = gql`query { ... }`;
366
+ const data = await executeGraphQL<ResponseType>(QUERY);
367
+
368
+ // OR: Use .graphql file for complex queries
369
+ import QUERY from './query/myQuery.graphql?raw';
370
+ const data = await executeGraphQL<ResponseType>(QUERY);
83
371
  ```
84
372
 
373
+ **Why avoid plain strings:**
374
+ - No syntax highlighting or validation
375
+ - Harder to maintain and refactor
376
+ - More error-prone
377
+
85
378
  ## Benefits of executeGraphQL
86
379
  - Centralized error handling for GraphQL errors
87
380
  - Consistent typing with `GraphQLResponse<T>` interface
@@ -90,9 +383,26 @@ const data = await executeGraphQL<MyTypedResponse>(query);
90
383
 
91
384
  ## Quality Checklist
92
385
  Before completing GraphQL data access code:
93
- 1. Import `executeGraphQL` from the correct path
94
- 2. Define TypeScript interfaces for the query response
95
- 3. Use proper generic typing: `executeGraphQL<ResponseType>`
96
- 4. Handle the returned data appropriately
97
- 5. Follow the pattern established in `force-app/main/default/webapplications/<appName>/src/api/utils/accounts.ts`
98
386
 
387
+ ### For Pattern 1 (.graphql files):
388
+ 1. [ ] Create `.graphql` file for the query/mutation
389
+ 2. [ ] Run `npm run graphql:codegen` to generate types
390
+ 3. [ ] Import query with `?raw` suffix
391
+ 4. [ ] Import generated types from `graphql-operations-types.ts`
392
+ 5. [ ] Use `executeGraphQL<ResponseType>()` with proper generic
393
+ 6. [ ] Use `NodeOfConnection` for cleaner node types when needed
394
+ 7. [ ] Handle optional chaining for nested response data
395
+ 8. [ ] Follow the pattern in `accounts.ts`
396
+
397
+ ### For Pattern 2 (inline with gql):
398
+ 1. [ ] Define query using `gql` template tag
399
+ 2. [ ] Ensure query name matches generated types in `graphql-operations-types.ts`
400
+ 3. [ ] Import generated types for the query
401
+ 4. [ ] Use `executeGraphQL<ResponseType>()` with proper generic
402
+ 5. [ ] Handle errors with try/catch when appropriate
403
+ 6. [ ] Handle optional chaining for nested response data
404
+ 7. [ ] Follow the pattern in `user.ts`
405
+
406
+ ### General:
407
+ - [ ] Choose Pattern 1 for complex queries with variables, fragments, or when shared
408
+ - [ ] Choose Pattern 2 for simple, colocated queries without complex requirements
package/dist/CHANGELOG.md CHANGED
@@ -3,6 +3,25 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [1.22.1](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.22.0...v1.22.1) (2026-02-11)
7
+
8
+ **Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
9
+
10
+
11
+
12
+
13
+
14
+ # [1.22.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.21.0...v1.22.0) (2026-02-10)
15
+
16
+
17
+ ### Features
18
+
19
+ * @W-20916499 use DataSDK ([#81](https://github.com/salesforce-experience-platform-emu/webapps/issues/81)) ([fbd88e5](https://github.com/salesforce-experience-platform-emu/webapps/commit/fbd88e58283925f8fe241e99c3f968ba2c614f48))
20
+
21
+
22
+
23
+
24
+
6
25
  # [1.21.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.20.0...v1.21.0) (2026-02-10)
7
26
 
8
27
  **Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
@@ -0,0 +1,2 @@
1
+ declare const _default: import('vite').UserConfigFnObject;
2
+ export default _default;
@@ -0,0 +1,73 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+ import path from 'path';
4
+ import { resolve } from 'path';
5
+ import tailwindcss from '@tailwindcss/vite';
6
+ import salesforce from '@salesforce/vite-plugin-webapp-experimental';
7
+ export default defineConfig(function (_a) {
8
+ var mode = _a.mode;
9
+ return {
10
+ plugins: [tailwindcss(), react(), salesforce()],
11
+ // Build configuration for MPA
12
+ build: {
13
+ outDir: resolve(__dirname, 'dist'),
14
+ assetsDir: 'assets',
15
+ sourcemap: false,
16
+ },
17
+ // Resolve aliases (shared between build and test)
18
+ resolve: {
19
+ alias: {
20
+ '@': path.resolve(__dirname, './src'),
21
+ '@api': path.resolve(__dirname, './src/api'),
22
+ '@components': path.resolve(__dirname, './src/components'),
23
+ '@utils': path.resolve(__dirname, './src/utils'),
24
+ '@styles': path.resolve(__dirname, './src/styles'),
25
+ '@assets': path.resolve(__dirname, './src/assets'),
26
+ },
27
+ },
28
+ // Vitest configuration
29
+ test: {
30
+ // Override root for tests (build uses src/pages as root)
31
+ root: resolve(__dirname),
32
+ // Use jsdom environment for React component testing
33
+ environment: 'jsdom',
34
+ // Setup files to run before each test
35
+ setupFiles: ['./src/test/setup.ts'],
36
+ // Global test patterns
37
+ include: [
38
+ 'src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
39
+ 'src/**/__tests__/**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
40
+ ],
41
+ // Coverage configuration
42
+ coverage: {
43
+ provider: 'v8',
44
+ reporter: ['text', 'html', 'clover', 'json'],
45
+ exclude: [
46
+ 'node_modules/',
47
+ 'src/test/',
48
+ 'src/**/*.d.ts',
49
+ 'src/main.tsx',
50
+ 'src/vite-env.d.ts',
51
+ 'src/components/**/index.ts',
52
+ '**/*.config.ts',
53
+ 'build/',
54
+ 'dist/',
55
+ 'coverage/',
56
+ 'eslint.config.js',
57
+ ],
58
+ thresholds: {
59
+ global: {
60
+ branches: 85,
61
+ functions: 85,
62
+ lines: 85,
63
+ statements: 85,
64
+ },
65
+ },
66
+ },
67
+ // Test timeout
68
+ testTimeout: 10000,
69
+ // Globals for easier testing
70
+ globals: true,
71
+ },
72
+ };
73
+ });
@@ -0,0 +1,24 @@
1
+ import { test, expect } from '@playwright/test';
2
+
3
+ test.describe('base-react-app', () => {
4
+ test('home page loads and shows welcome content', async ({ page }) => {
5
+ await page.goto('/');
6
+ await expect(page.getByRole('heading', { name: 'Home' })).toBeVisible();
7
+ await expect(
8
+ page.getByText('Welcome to your React application.')
9
+ ).toBeVisible();
10
+ });
11
+
12
+ test('about page loads', async ({ page }) => {
13
+ await page.goto('/about');
14
+ await expect(page).toHaveURL(/\/about/);
15
+ await expect(page.getByRole('heading', { name: 'About' })).toBeVisible();
16
+ await expect(page.getByText('This is the about page.')).toBeVisible();
17
+ });
18
+
19
+ test('not found route shows 404', async ({ page }) => {
20
+ await page.goto('/non-existent-route');
21
+ await expect(page.getByRole('heading', { name: '404' })).toBeVisible();
22
+ await expect(page.getByText('Page not found')).toBeVisible();
23
+ });
24
+ });