@salesforce/pwa-kit-create-app 3.11.0 → 3.12.0-dev

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 (24) hide show
  1. package/assets/bootstrap/js/config/default.js.hbs +22 -0
  2. package/assets/bootstrap/js/config/utils.js.hbs +48 -0
  3. package/assets/bootstrap/js/overrides/app/components/_app-config/index.jsx.hbs +28 -7
  4. package/assets/bootstrap/js/overrides/app/constants.js.hbs +3 -3
  5. package/assets/bootstrap/js/overrides/app/main.jsx +2 -1
  6. package/assets/bootstrap/js/overrides/app/ssr.js.hbs +40 -20
  7. package/assets/templates/@salesforce/retail-react-app/app/components/_app-config/index.jsx.hbs +27 -6
  8. package/assets/templates/@salesforce/retail-react-app/app/ssr.js.hbs +40 -19
  9. package/assets/templates/@salesforce/retail-react-app/config/default.js.hbs +18 -0
  10. package/assets/templates/@salesforce/retail-react-app/config/utils.js.hbs +48 -0
  11. package/package.json +4 -4
  12. package/program.json +49 -1
  13. package/scripts/create-mobify-app.js +42 -19
  14. package/templates/express-minimal.tar.gz +0 -0
  15. package/templates/mrt-reference-app.tar.gz +0 -0
  16. package/templates/retail-react-app.tar.gz +0 -0
  17. package/templates/typescript-minimal.tar.gz +0 -0
  18. package/assets/cursor-rules/accessibility-rule.mdc +0 -8
  19. package/assets/cursor-rules/cursor-accessibility-mdc/accessibility-button-name.mdc +0 -26
  20. package/assets/cursor-rules/cursor-accessibility-mdc/accessibility-heading-order.mdc +0 -26
  21. package/assets/cursor-rules/cursor-accessibility-mdc/accessibility-image-alt.mdc +0 -24
  22. package/assets/cursor-rules/cursor-accessibility-mdc/accessibility-input-label.mdc +0 -27
  23. package/assets/cursor-rules/cursor-accessibility-mdc/accessibility-link-name.mdc +0 -26
  24. package/assets/cursor-rules/cursor-rule.mdc +0 -66
@@ -6,8 +6,26 @@
6
6
  */
7
7
  /* eslint-disable @typescript-eslint/no-var-requires */
8
8
  const sites = require('./sites.js')
9
+ const {parseCommerceAgentSettings} = require('./utils.js')
10
+
9
11
  module.exports = {
10
12
  app: {
13
+ // Enable the store locator and shop the store feature.
14
+ storeLocatorEnabled: true,
15
+ // Enable the multi-ship feature.
16
+ multishipEnabled: true,
17
+ // Enable partial hydration capabilities via Island component
18
+ {{#if answers.project.partialHydrationEnabled}}
19
+ partialHydrationEnabled: {{answers.project.partialHydrationEnabled}},
20
+ {{else}}
21
+ partialHydrationEnabled: false,
22
+ {{/if}}
23
+ // Commerce shopping agent configuration for embedded messaging service
24
+ // This enables an agentic shopping experience in the application
25
+ // This property accepts either a JSON string or a plain JavaScript object.
26
+ // The value is set from the COMMERCE_AGENT_SETTINGS environment variable.
27
+ // If the COMMERCE_AGENT_SETTINGS environment variable is not set, the feature is disabled.
28
+ commerceAgent: parseCommerceAgentSettings(process.env.COMMERCE_AGENT_SETTINGS),
11
29
  // Customize how your 'site' and 'locale' are displayed in the url.
12
30
  url: {
13
31
  {{#if answers.project.demo.enableDemoSettings}}
@@ -109,6 +127,10 @@ module.exports = {
109
127
  tenantId: '{{answers.project.dataCloud.tenantId}}'
110
128
  }
111
129
  },
130
+ // Experimental: The base path for the app. This is the path that will be prepended to all /mobify routes,
131
+ // callback routes, and Express routes.
132
+ // Setting this to `/` or an empty string will result in the above routes not having a base path.
133
+ envBasePath: '/',
112
134
  // This list contains server-side only libraries that you don't want to be compiled by webpack
113
135
  externals: [],
114
136
  // Page not found url for your app
@@ -0,0 +1,48 @@
1
+ /*
2
+ * Copyright (c) 2021, salesforce.com, inc.
3
+ * All rights reserved.
4
+ * SPDX-License-Identifier: BSD-3-Clause
5
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
+ */
7
+
8
+ /**
9
+ * Safely parses commerce agent settings from either a JSON string or object
10
+ * @param {string|object} settings - The commerce agent settings
11
+ * @returns {object} Parsed commerce agent settings object
12
+ */
13
+ function parseCommerceAgentSettings(settings) {
14
+ // Default configuration when no settings are provided
15
+ const defaultConfig = {
16
+ enabled: 'false',
17
+ askAgentOnSearch: 'false',
18
+ embeddedServiceName: '',
19
+ embeddedServiceEndpoint: '',
20
+ scriptSourceUrl: '',
21
+ scrt2Url: '',
22
+ salesforceOrgId: '',
23
+ commerceOrgId: '',
24
+ siteId: ''
25
+ }
26
+
27
+ // If settings is already an object, return it
28
+ if (typeof settings === 'object' && settings !== null) {
29
+ return settings
30
+ }
31
+
32
+ // If settings is a string, try to parse it
33
+ if (typeof settings === 'string') {
34
+ try {
35
+ return JSON.parse(settings)
36
+ } catch (error) {
37
+ console.warn('Invalid COMMERCE_AGENT_SETTINGS format, using defaults:', error.message)
38
+ return defaultConfig
39
+ }
40
+ }
41
+
42
+ // If settings is undefined/null, return defaults
43
+ return defaultConfig
44
+ }
45
+
46
+ module.exports = {
47
+ parseCommerceAgentSettings
48
+ }
@@ -18,15 +18,20 @@ import {
18
18
  resolveLocaleFromUrl
19
19
  } from '@salesforce/retail-react-app/app/utils/site-utils'
20
20
  import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
21
- import {proxyBasePath} from '@salesforce/pwa-kit-runtime/utils/ssr-namespace-paths'
21
+ import {
22
+ getEnvBasePath,
23
+ slasPrivateProxyPath
24
+ } from '@salesforce/pwa-kit-runtime/utils/ssr-namespace-paths'
22
25
  import {createUrlTemplate} from '@salesforce/retail-react-app/app/utils/url'
23
26
  import createLogger from '@salesforce/pwa-kit-runtime/utils/logger-factory'
27
+ import {isAbsoluteURL} from '@salesforce/retail-react-app/app/page-designer/utils'
24
28
 
25
29
  import {CommerceApiProvider} from '@salesforce/commerce-sdk-react'
26
30
  import {withReactQuery} from '@salesforce/pwa-kit-react-sdk/ssr/universal/components/with-react-query'
27
31
  import {useCorrelationId} from '@salesforce/pwa-kit-react-sdk/ssr/universal/hooks'
28
32
  import {getAppOrigin} from '@salesforce/pwa-kit-react-sdk/utils/url'
29
33
  import {ReactQueryDevtools} from '@tanstack/react-query-devtools'
34
+ import {generateSfdcUserAgent} from '@salesforce/retail-react-app/app/utils/sfdc-user-agent-utils'
30
35
  import {
31
36
  DEFAULT_DNT_STATE,
32
37
  STORE_LOCATOR_RADIUS,
@@ -38,6 +43,8 @@ import {
38
43
  STORE_LOCATOR_SUPPORTED_COUNTRIES
39
44
  } from '@salesforce/retail-react-app/app/constants'
40
45
 
46
+ const sfdcUserAgent = generateSfdcUserAgent()
47
+
41
48
  /**
42
49
  * Use the AppConfig component to inject extra arguments into the getProps
43
50
  * methods for all Route Components in the app – typically you'd want to do this
@@ -49,7 +56,8 @@ import {
49
56
  const AppConfig = ({children, locals = {}}) => {
50
57
  const {correlationId} = useCorrelationId()
51
58
  const headers = {
52
- 'correlation-id': correlationId
59
+ 'correlation-id': correlationId,
60
+ sfdc_user_agent: sfdcUserAgent
53
61
  }
54
62
 
55
63
  const commerceApiConfig = locals.appConfig.commerceAPI
@@ -68,6 +76,14 @@ const AppConfig = ({children, locals = {}}) => {
68
76
  supportedCountries: STORE_LOCATOR_SUPPORTED_COUNTRIES
69
77
  }
70
78
 
79
+ // Set absolute uris for CommerceApiProvider proxies and callbacks
80
+ const redirectURI = `${appOrigin}${getEnvBasePath()}/callback`
81
+ const proxy = `${appOrigin}${getEnvBasePath()}${commerceApiConfig.proxyPath}`
82
+ const slasPrivateClientProxyEndpoint = `${appOrigin}${getEnvBasePath()}${slasPrivateProxyPath}`
83
+ const passwordlessLoginCallbackURI = isAbsoluteURL(passwordlessCallback)
84
+ ? passwordlessCallback
85
+ : `${appOrigin}${getEnvBasePath()}${passwordlessCallback}`
86
+
71
87
  return (
72
88
  <CommerceApiProvider
73
89
  shortCode={commerceApiConfig.parameters.shortCode}
@@ -76,17 +92,22 @@ const AppConfig = ({children, locals = {}}) => {
76
92
  siteId={locals.site?.id}
77
93
  locale={locals.locale?.id}
78
94
  currency={locals.locale?.preferredCurrency}
79
- redirectURI={`${appOrigin}/callback`}
80
- proxy={`${appOrigin}${commerceApiConfig.proxyPath}`}
95
+ redirectURI={redirectURI}
96
+ proxy={proxy}
81
97
  headers={headers}
82
98
  defaultDnt={DEFAULT_DNT_STATE}
83
99
  logger={createLogger({packageName: 'commerce-sdk-react'})}
84
- passwordlessLoginCallbackURI={passwordlessCallback}
85
- {{#if answers.project.commerce.isSlasPrivate}}
86
- // Set 'enablePWAKitPrivateClient' to true use SLAS private client login flows.
100
+ passwordlessLoginCallbackURI={passwordlessLoginCallbackURI}
101
+ // Set 'enablePWAKitPrivateClient' to true to use SLAS private client login flows.
87
102
  // Make sure to also enable useSLASPrivateClient in ssr.js when enabling this setting.
103
+ {{#if answers.project.commerce.isSlasPrivate}}
88
104
  enablePWAKitPrivateClient={true}
105
+ {{else}}
106
+ enablePWAKitPrivateClient={false}
89
107
  {{/if}}
108
+ privateClientProxyEndpoint={slasPrivateClientProxyEndpoint}
109
+ // Uncomment 'hybridAuthEnabled' if the current site has Hybrid Auth enabled. Do NOT set this flag for hybrid storefronts using Plugin SLAS.
110
+ // hybridAuthEnabled={true}
90
111
  >
91
112
  <MultiSiteProvider site={locals.site} locale={locals.locale} buildUrl={locals.buildUrl}>
92
113
  <StoreLocatorProvider config={storeLocatorConfig}>
@@ -5,10 +5,10 @@
5
5
  * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
6
  */
7
7
 
8
- /*
8
+ /*
9
9
  Hello there! This is a demonstration of how to override a file from the base template.
10
-
11
- It's necessary that the module export interface remain consistent,
10
+
11
+ It's necessary that the module export interface remain consistent,
12
12
  as other files in the base template rely on constants.js, thus we
13
13
  import the underlying constants.js, modifies it and re-export it.
14
14
  */
@@ -5,10 +5,11 @@
5
5
  * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
6
  */
7
7
  import {start, registerServiceWorker} from '@salesforce/pwa-kit-react-sdk/ssr/browser/main'
8
+ import {getEnvBasePath} from '@salesforce/pwa-kit-runtime/utils/ssr-namespace-paths'
8
9
 
9
10
  const main = () => {
10
11
  // The path to your service worker should match what is set up in ssr.js
11
- return Promise.all([start(), registerServiceWorker('/worker.js')])
12
+ return Promise.all([start(), registerServiceWorker(`${getEnvBasePath()}/worker.js`)])
12
13
  }
13
14
 
14
15
  main()
@@ -44,6 +44,13 @@ const options = {
44
44
  // environment variable as this endpoint will return HTTP 501 if it is not set
45
45
  useSLASPrivateClient: {{answers.project.commerce.isSlasPrivate}},
46
46
 
47
+ // If you wish to use additional SLAS endpoints that require private clients,
48
+ // customize this regex to include the additional endpoints the custom SLAS
49
+ // private client secret handler will inject an Authorization header.
50
+ // The default regex is defined in this file: https://github.com/SalesforceCommerceCloud/pwa-kit/blob/develop/packages/pwa-kit-runtime/src/ssr/server/build-remote-server.js
51
+ // applySLASPrivateClientToEndpoints:
52
+ // /\/oauth2\/(token|passwordless\/(login|token)|password\/(reset|action))/,
53
+
47
54
  // If this is enabled, any HTTP header that has a non ASCII value will be URI encoded
48
55
  // If there any HTTP headers that have been encoded, an additional header will be
49
56
  // passed, `x-encoded-headers`, containing a comma separated list
@@ -282,32 +289,45 @@ const {handler} = runtime.createHandler(options, (app) => {
282
289
  // Set default HTTP security headers required by PWA Kit
283
290
  app.use(defaultPwaKitSecurityHeaders)
284
291
  // Set custom HTTP security headers
292
+ {{#if answers.project.contentSecurityPolicy}}
293
+ const contentSecurityPolicy = {{{json answers.project.contentSecurityPolicy}}}
294
+ {{else}}
295
+ const contentSecurityPolicy = {
296
+ useDefaults: true,
297
+ directives: {
298
+ 'img-src': [
299
+ // Default source for product images - replace with your CDN
300
+ '*.commercecloud.salesforce.com',
301
+ '*.demandware.net'
302
+ ],
303
+ 'script-src': [
304
+ // Used by the service worker in /worker/main.js
305
+ 'storage.googleapis.com'
306
+ ],
307
+ 'connect-src': [
308
+ // Connect to Einstein APIs
309
+ 'api.cquotient.com',
310
+ // Connect to DataCloud APIs
311
+ '*.c360a.salesforce.com',
312
+ // Connect to SCRT2 URLs
313
+ '*.salesforce-scrt.com'
314
+ ],
315
+ 'frame-src': [
316
+ // Allow frames from Salesforce site.com (Needed for MIAW)
317
+ '*.site.com'
318
+ ]
319
+ }
320
+ }
321
+ {{/if}}
322
+
285
323
  app.use(
286
324
  helmet({
287
- contentSecurityPolicy: {
288
- useDefaults: true,
289
- directives: {
290
- 'img-src': [
291
- // Default source for product images - replace with your CDN
292
- '*.commercecloud.salesforce.com',
293
- '*.demandware.net'
294
- ],
295
- 'script-src': [
296
- // Used by the service worker in /worker/main.js
297
- 'storage.googleapis.com'
298
- ],
299
- 'connect-src': [
300
- // Connect to Einstein APIs
301
- 'api.cquotient.com',
302
- '*.c360a.salesforce.com'
303
- ]
304
- }
305
- }
325
+ contentSecurityPolicy
306
326
  })
307
327
  )
308
328
 
309
329
  // Handle the redirect from SLAS as to avoid error
310
- app.get('/callback?*', (req, res) => {
330
+ app.get('/callback', (req, res) => {
311
331
  // This endpoint does nothing and is not expected to change
312
332
  // Thus we cache it for a year to maximize performance
313
333
  res.set('Cache-Control', `max-age=31536000`)
@@ -19,14 +19,19 @@ import {
19
19
  resolveLocaleFromUrl
20
20
  } from '@salesforce/retail-react-app/app/utils/site-utils'
21
21
  import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
22
- import {proxyBasePath} from '@salesforce/pwa-kit-runtime/utils/ssr-namespace-paths'
22
+ import {
23
+ getEnvBasePath,
24
+ slasPrivateProxyPath
25
+ } from '@salesforce/pwa-kit-runtime/utils/ssr-namespace-paths'
23
26
  import {createUrlTemplate} from '@salesforce/retail-react-app/app/utils/url'
24
27
  import createLogger from '@salesforce/pwa-kit-runtime/utils/logger-factory'
28
+ import {isAbsoluteURL} from '@salesforce/retail-react-app/app/page-designer/utils'
25
29
 
26
30
  import {CommerceApiProvider} from '@salesforce/commerce-sdk-react'
27
31
  import {withReactQuery} from '@salesforce/pwa-kit-react-sdk/ssr/universal/components/with-react-query'
28
32
  import {useCorrelationId} from '@salesforce/pwa-kit-react-sdk/ssr/universal/hooks'
29
33
  import {ReactQueryDevtools} from '@tanstack/react-query-devtools'
34
+ import {generateSfdcUserAgent} from '@salesforce/retail-react-app/app/utils/sfdc-user-agent-utils'
30
35
  import {DEFAULT_DNT_STATE} from '@salesforce/retail-react-app/app/constants'
31
36
  import {
32
37
  STORE_LOCATOR_RADIUS,
@@ -38,6 +43,8 @@ import {
38
43
  STORE_LOCATOR_SUPPORTED_COUNTRIES
39
44
  } from '@salesforce/retail-react-app/app/constants'
40
45
 
46
+ const sfdcUserAgent = generateSfdcUserAgent()
47
+
41
48
  /**
42
49
  * Use the AppConfig component to inject extra arguments into the getProps
43
50
  * methods for all Route Components in the app – typically you'd want to do this
@@ -49,7 +56,8 @@ import {
49
56
  const AppConfig = ({children, locals = {}}) => {
50
57
  const {correlationId} = useCorrelationId()
51
58
  const headers = {
52
- 'correlation-id': correlationId
59
+ 'correlation-id': correlationId,
60
+ sfdc_user_agent: sfdcUserAgent
53
61
  }
54
62
 
55
63
  const commerceApiConfig = locals.appConfig.commerceAPI
@@ -68,6 +76,14 @@ const AppConfig = ({children, locals = {}}) => {
68
76
  supportedCountries: STORE_LOCATOR_SUPPORTED_COUNTRIES
69
77
  }
70
78
 
79
+ // Set absolute uris for CommerceApiProvider proxies and callbacks
80
+ const redirectURI = `${appOrigin}${getEnvBasePath()}/callback`
81
+ const proxy = `${appOrigin}${getEnvBasePath()}${commerceApiConfig.proxyPath}`
82
+ const slasPrivateClientProxyEndpoint = `${appOrigin}${getEnvBasePath()}${slasPrivateProxyPath}`
83
+ const passwordlessLoginCallbackURI = isAbsoluteURL(passwordlessCallback)
84
+ ? passwordlessCallback
85
+ : `${appOrigin}${getEnvBasePath()}${passwordlessCallback}`
86
+
71
87
  return (
72
88
  <CommerceApiProvider
73
89
  shortCode={commerceApiConfig.parameters.shortCode}
@@ -76,17 +92,22 @@ const AppConfig = ({children, locals = {}}) => {
76
92
  siteId={locals.site?.id}
77
93
  locale={locals.locale?.id}
78
94
  currency={locals.locale?.preferredCurrency}
79
- redirectURI={`${appOrigin}/callback`}
80
- proxy={`${appOrigin}${commerceApiConfig.proxyPath}`}
95
+ redirectURI={redirectURI}
96
+ proxy={proxy}
81
97
  headers={headers}
82
98
  defaultDnt={DEFAULT_DNT_STATE}
83
99
  logger={createLogger({packageName: 'commerce-sdk-react'})}
84
100
  passwordlessLoginCallbackURI={passwordlessCallback}
85
- {{#if answers.project.commerce.isSlasPrivate}}
86
- // Set 'enablePWAKitPrivateClient' to true use SLAS private client login flows.
101
+ // Set 'enablePWAKitPrivateClient' to true to use SLAS private client login flows.
87
102
  // Make sure to also enable useSLASPrivateClient in ssr.js when enabling this setting.
103
+ {{#if answers.project.commerce.isSlasPrivate}}
88
104
  enablePWAKitPrivateClient={true}
105
+ {{else}}
106
+ enablePWAKitPrivateClient={false}
89
107
  {{/if}}
108
+ privateClientProxyEndpoint={slasPrivateClientProxyEndpoint}
109
+ // Uncomment 'hybridAuthEnabled' if the current site has Hybrid Auth enabled. Do NOT set this flag for hybrid storefronts using Plugin SLAS.
110
+ // hybridAuthEnabled={true}
90
111
  >
91
112
  <MultiSiteProvider site={locals.site} locale={locals.locale} buildUrl={locals.buildUrl}>
92
113
  <StoreLocatorProvider config={storeLocatorConfig}>
@@ -44,6 +44,13 @@ const options = {
44
44
  // environment variable as this endpoint will return HTTP 501 if it is not set
45
45
  useSLASPrivateClient: {{answers.project.commerce.isSlasPrivate}},
46
46
 
47
+ // If you wish to use additional SLAS endpoints that require private clients,
48
+ // customize this regex to include the additional endpoints the custom SLAS
49
+ // private client secret handler will inject an Authorization header.
50
+ // The default regex is defined in this file: https://github.com/SalesforceCommerceCloud/pwa-kit/blob/develop/packages/pwa-kit-runtime/src/ssr/server/build-remote-server.js
51
+ // applySLASPrivateClientToEndpoints:
52
+ // /\/oauth2\/(token|passwordless\/(login|token)|password\/(reset|action))/,
53
+
47
54
  // If this is enabled, any HTTP header that has a non ASCII value will be URI encoded
48
55
  // If there any HTTP headers that have been encoded, an additional header will be
49
56
  // passed, `x-encoded-headers`, containing a comma separated list
@@ -282,31 +289,45 @@ const {handler} = runtime.createHandler(options, (app) => {
282
289
  // Set default HTTP security headers required by PWA Kit
283
290
  app.use(defaultPwaKitSecurityHeaders)
284
291
  // Set custom HTTP security headers
292
+ {{#if answers.project.contentSecurityPolicy}}
293
+ const contentSecurityPolicy = {{{json answers.project.contentSecurityPolicy}}}
294
+ {{else}}
295
+ const contentSecurityPolicy = {
296
+ useDefaults: true,
297
+ directives: {
298
+ 'img-src': [
299
+ // Default source for product images - replace with your CDN
300
+ '*.commercecloud.salesforce.com',
301
+ '*.demandware.net'
302
+ ],
303
+ 'script-src': [
304
+ // Used by the service worker in /worker/main.js
305
+ 'storage.googleapis.com'
306
+ ],
307
+ 'connect-src': [
308
+ // Connect to Einstein APIs
309
+ 'api.cquotient.com',
310
+ // Connect to DataCloud APIs
311
+ '*.c360a.salesforce.com',
312
+ // Connect to SCRT2 URLs
313
+ '*.salesforce-scrt.com'
314
+ ],
315
+ 'frame-src': [
316
+ // Allow frames from Salesforce site.com (Needed for MIAW)
317
+ '*.site.com'
318
+ ]
319
+ }
320
+ }
321
+ {{/if}}
322
+
285
323
  app.use(
286
324
  helmet({
287
- contentSecurityPolicy: {
288
- useDefaults: true,
289
- directives: {
290
- 'img-src': [
291
- // Default source for product images - replace with your CDN
292
- '*.commercecloud.salesforce.com'
293
- ],
294
- 'script-src': [
295
- // Used by the service worker in /worker/main.js
296
- 'storage.googleapis.com'
297
- ],
298
- 'connect-src': [
299
- // Connect to Einstein APIs
300
- 'api.cquotient.com',
301
- '*.c360a.salesforce.com'
302
- ]
303
- }
304
- }
325
+ contentSecurityPolicy
305
326
  })
306
327
  )
307
328
 
308
329
  // Handle the redirect from SLAS as to avoid error
309
- app.get('/callback?*', (req, res) => {
330
+ app.get('/callback', (req, res) => {
310
331
  // This endpoint does nothing and is not expected to change
311
332
  // Thus we cache it for a year to maximize performance
312
333
  res.set('Cache-Control', `max-age=31536000`)
@@ -6,8 +6,26 @@
6
6
  */
7
7
  /* eslint-disable @typescript-eslint/no-var-requires */
8
8
  const sites = require('./sites.js')
9
+ const {parseCommerceAgentSettings} = require('./utils.js')
10
+
9
11
  module.exports = {
10
12
  app: {
13
+ // Enable the store locator and shop the store feature.
14
+ storeLocatorEnabled: true,
15
+ // Enable the multi-ship feature.
16
+ multishipEnabled: true,
17
+ // Enable partial hydration capabilities via Island component
18
+ {{#if answers.project.partialHydrationEnabled}}
19
+ partialHydrationEnabled: {{answers.project.partialHydrationEnabled}},
20
+ {{else}}
21
+ partialHydrationEnabled: false,
22
+ {{/if}}
23
+ // Commerce shopping agent configuration for embedded messaging service
24
+ // This enables an agentic shopping experience in the application
25
+ // This property accepts either a JSON string or a plain JavaScript object.
26
+ // The value is set from the COMMERCE_AGENT_SETTINGS environment variable.
27
+ // If the COMMERCE_AGENT_SETTINGS environment variable is not set, the feature is disabled.
28
+ commerceAgent: parseCommerceAgentSettings(process.env.COMMERCE_AGENT_SETTINGS),
11
29
  // Customize settings for your url
12
30
  url: {
13
31
  {{#if answers.project.demo.enableDemoSettings}}
@@ -0,0 +1,48 @@
1
+ /*
2
+ * Copyright (c) 2021, salesforce.com, inc.
3
+ * All rights reserved.
4
+ * SPDX-License-Identifier: BSD-3-Clause
5
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6
+ */
7
+
8
+ /**
9
+ * Safely parses commerce agent settings from either a JSON string or object
10
+ * @param {string|object} settings - The commerce agent settings
11
+ * @returns {object} Parsed commerce agent settings object
12
+ */
13
+ function parseCommerceAgentSettings(settings) {
14
+ // Default configuration when no settings are provided
15
+ const defaultConfig = {
16
+ enabled: 'false',
17
+ askAgentOnSearch: 'false',
18
+ embeddedServiceName: '',
19
+ embeddedServiceEndpoint: '',
20
+ scriptSourceUrl: '',
21
+ scrt2Url: '',
22
+ salesforceOrgId: '',
23
+ commerceOrgId: '',
24
+ siteId: ''
25
+ }
26
+
27
+ // If settings is already an object, return it
28
+ if (typeof settings === 'object' && settings !== null) {
29
+ return settings
30
+ }
31
+
32
+ // If settings is a string, try to parse it
33
+ if (typeof settings === 'string') {
34
+ try {
35
+ return JSON.parse(settings)
36
+ } catch (error) {
37
+ console.warn('Invalid COMMERCE_AGENT_SETTINGS format, using defaults:', error.message)
38
+ return defaultConfig
39
+ }
40
+ }
41
+
42
+ // If settings is undefined/null, return defaults
43
+ return defaultConfig
44
+ }
45
+
46
+ module.exports = {
47
+ parseCommerceAgentSettings
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/pwa-kit-create-app",
3
- "version": "3.11.0",
3
+ "version": "3.12.0-dev",
4
4
  "description": "Salesforce's project generator tool",
5
5
  "homepage": "https://github.com/SalesforceCommerceCloud/pwa-kit/tree/develop/packages/pwa-kit-create-app#readme",
6
6
  "bugs": {
@@ -39,13 +39,13 @@
39
39
  "tar": "^6.2.1"
40
40
  },
41
41
  "devDependencies": {
42
- "@salesforce/pwa-kit-dev": "3.11.0",
43
- "internal-lib-build": "3.11.0",
42
+ "@salesforce/pwa-kit-dev": "3.12.0-dev",
43
+ "internal-lib-build": "3.12.0-dev",
44
44
  "verdaccio": "^5.22.1"
45
45
  },
46
46
  "engines": {
47
47
  "node": "^16.11.0 || ^18.0.0 || ^20.0.0 || ^22.0.0",
48
48
  "npm": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0"
49
49
  },
50
- "gitHead": "ce537dc2e48c980ca78607e03125f9cccf8314c5"
50
+ "gitHead": "60d9a47ad71824709be52bbe1110b092f7698244"
51
51
  }
package/program.json CHANGED
@@ -472,6 +472,54 @@
472
472
  },
473
473
  "private": true
474
474
  },
475
+ {
476
+ "id": "retail-react-app-performance-tests",
477
+ "name": "Retail React App for performance tests",
478
+ "description": "",
479
+ "templateId": "retail-react-app",
480
+ "answers": {
481
+ "project.extend": false,
482
+ "project.hybrid": false,
483
+ "project.name": "retail-react-app",
484
+ "project.commerce.instanceUrl": "https://zzrf-001.dx.commercecloud.salesforce.com",
485
+ "project.commerce.clientId": "9629ef22-f8b8-4987-90ac-b815be3940c8",
486
+ "project.commerce.siteId": "SiteNemesis",
487
+ "project.commerce.organizationId": "f_ecom_tbbn_prd",
488
+ "project.commerce.shortCode": "performance-001",
489
+ "project.commerce.isSlasPrivate": true,
490
+ "project.einstein.clientId": "1ea06c6e-c936-4324-bcf0-fada93f83bb1",
491
+ "project.einstein.siteId": "aaij-MobileFirst",
492
+ "project.dataCloud.appSourceId": "7ae070a6-f4ec-4def-a383-d9cacc3f20a1",
493
+ "project.dataCloud.tenantId": "g82wgnrvm-ywk9dggrrw8mtggy.pc-rnd",
494
+ "project.demo.enableDemoSettings": false,
495
+
496
+ "project.partialHydrationEnabled": true,
497
+ "project.contentSecurityPolicy": {
498
+ "useDefaults": true,
499
+ "directives": {
500
+ "img-src": [
501
+ "*.commercecloud.salesforce.com",
502
+ "*.demandware.net",
503
+ "*.sfcc-store-internal.net"
504
+ ],
505
+ "script-src": [
506
+ "storage.googleapis.com",
507
+ "*.sfcc-store-internal.net"
508
+ ],
509
+ "connect-src": [
510
+ "api.cquotient.com",
511
+ "*.c360a.salesforce.com",
512
+ "*.salesforce-scrt.com",
513
+ "*.sfcc-store-internal.net"
514
+ ],
515
+ "frame-src": [
516
+ "*.site.com"
517
+ ]
518
+ }
519
+ }
520
+ },
521
+ "private": true
522
+ },
475
523
  {
476
524
  "id": "typescript-minimal-test-project",
477
525
  "name": "Template Minimal Test Project",
@@ -723,7 +771,7 @@
723
771
  {
724
772
  "name": "--stdio",
725
773
  "description": "Accept project generation answers from stdin as JSON"
726
- },
774
+ },
727
775
  {
728
776
  "name": "--displayProgram",
729
777
  "description": "Display the program.json file detailing the program schema/data and exit"
@@ -91,6 +91,9 @@ sh.set('-e')
91
91
  // will ensure those escaped double quotes are still escaped after processing the template.
92
92
  Handlebars.registerHelper('script', (object) => object.replaceAll('"', '\\"'))
93
93
 
94
+ // Helper to convert JavaScript objects to JSON strings
95
+ Handlebars.registerHelper('json', (object) => JSON.stringify(object, null, 4))
96
+
94
97
  // Validations
95
98
  const validPreset = (preset) => {
96
99
  return ALL_PRESET_NAMES.includes(preset)
@@ -111,7 +114,6 @@ const TEMPLATE_SOURCE_BUNDLE = 'bundle'
111
114
 
112
115
  const BOOTSTRAP_DIR = p.join(__dirname, '..', 'assets', 'bootstrap', 'js')
113
116
  const ASSETS_TEMPLATES_DIR = p.join(__dirname, '..', 'assets', 'templates')
114
- const CURSOR_RULES_FROM_DIR = p.join(__dirname, '..', 'assets', 'cursor-rules')
115
117
  const PRIVATE_PRESET_NAMES = PRESETS.filter(({private}) => !!private).map(({id}) => id)
116
118
  const PUBLIC_PRESET_NAMES = PRESETS.filter(({private}) => !private).map(({id}) => id)
117
119
  const ALL_PRESET_NAMES = PRIVATE_PRESET_NAMES.concat(PUBLIC_PRESET_NAMES)
@@ -234,6 +236,38 @@ const expandKey = (key, value) =>
234
236
  const expandObject = (obj = {}) =>
235
237
  Object.keys(obj).reduce((acc, curr) => merge(acc, expandKey(curr, obj[curr])), {})
236
238
 
239
+ /**
240
+ * If the generated project is based on a template that includes '.cursor/rules',
241
+ * this function copies the rules from the installed node_modules into the
242
+ * top-level of the generated project.
243
+ * @param {string} outputDir - The directory of the generated project
244
+ * @private
245
+ */
246
+ const copyCursorRules = (outputDir) => {
247
+ const cursorRulesFromDir = p.join(
248
+ outputDir,
249
+ 'node_modules',
250
+ '@salesforce',
251
+ 'retail-react-app',
252
+ '.cursor',
253
+ 'rules'
254
+ )
255
+ if (sh.test('-e', cursorRulesFromDir)) {
256
+ const outputCursorRulesDir = p.join(outputDir, '.cursor', 'rules')
257
+
258
+ // Create the directory if it doesn't exist
259
+ if (!sh.test('-e', outputCursorRulesDir)) {
260
+ fs.mkdirSync(outputCursorRulesDir, {recursive: true})
261
+ }
262
+
263
+ // Copy the contents of cursorRulesFromDir to outputCursorRulesDir
264
+ const files = fs.readdirSync(cursorRulesFromDir)
265
+ files.forEach((file) => {
266
+ sh.cp('-rf', p.join(cursorRulesFromDir, file), outputCursorRulesDir)
267
+ })
268
+ }
269
+ }
270
+
237
271
  /**
238
272
  * Envoke the "npm install" command for the provided project directory.
239
273
  *
@@ -352,6 +386,11 @@ const runGenerator = (context, {outputDir, templateVersion, verbose}) => {
352
386
  assets.forEach((asset) => {
353
387
  sh.cp('-rf', p.join(packagePath, asset), outputDir)
354
388
  })
389
+ // Install dependencies for the newly minted project.
390
+ npmInstall(outputDir, {verbose})
391
+
392
+ // Extended project does not contain cursor rules and need explicit copy
393
+ copyCursorRules(outputDir)
355
394
  } else {
356
395
  console.log('Copying base template from package or npm: ', packagePath, outputDir)
357
396
  // Copy the base template either from the package or npm.
@@ -389,26 +428,10 @@ const runGenerator = (context, {outputDir, templateVersion, verbose}) => {
389
428
 
390
429
  // Clean up
391
430
  sh.rm('-rf', tmp)
392
- }
393
431
 
394
- // Copy the .cursor/rules directory if it exists
395
- if (sh.test('-e', CURSOR_RULES_FROM_DIR)) {
396
- const outputCursorRulesDir = p.join(outputDir, '.cursor', 'rules')
397
-
398
- // Create the directory if it doesn't exist
399
- if (!sh.test('-e', outputCursorRulesDir)) {
400
- fs.mkdirSync(outputCursorRulesDir, {recursive: true})
401
- }
402
-
403
- // Copy the contents of CURSOR_RULES_FROM_DIR to outputCursorRulesDir
404
- const files = fs.readdirSync(CURSOR_RULES_FROM_DIR)
405
- files.forEach((file) => {
406
- sh.cp('-rf', p.join(CURSOR_RULES_FROM_DIR, file), outputCursorRulesDir)
407
- })
432
+ // Install dependencies for the newly minted project.
433
+ npmInstall(outputDir, {verbose})
408
434
  }
409
-
410
- // Install dependencies for the newly minted project.
411
- npmInstall(outputDir, {verbose})
412
435
  }
413
436
 
414
437
  const foundNode = process.versions.node
Binary file
Binary file
Binary file
Binary file
@@ -1,8 +0,0 @@
1
- ---
2
- description: Check and verify accessibility on local project
3
- globs:
4
- alwaysApply: false
5
- ---
6
- To check accessibility of a project, use the accessibility rules defined in PROJECT_ROOT/.cursor/rules/cursor-accessibility-mdc/ directory.
7
- Run an automated scan for the entire codebase within the project and check for any violations.
8
- Show the scanned results in summary and detailed violations if any.
@@ -1,26 +0,0 @@
1
- ---
2
- description: When accessibility is checked. Check if Buttons must have a discernible name using text or aria attributes
3
- globs:
4
- alwaysApply: false
5
- ---
6
-
7
- # Rule: accessibility-button-name
8
-
9
- Buttons must have a discernible name using text or aria attributes
10
-
11
- ## 🔍 Pattern
12
-
13
- ```regex
14
- <button(?![^>]*\b(aria-label|aria-labelledby|title|name)=)
15
- ```
16
-
17
- ## 📍 Examples
18
-
19
- ```tsx
20
- // ❌ Bad
21
- <button></button>
22
- // ✅ Good
23
- <button>Submit</button>
24
- // ✅ Good
25
- <button aria-label="Close dialog"><XIcon /></button>
26
- ```
@@ -1,26 +0,0 @@
1
- ---
2
- description: When accessibility is checked. Check if heading levels should increase sequentially for semantic structure
3
- globs:
4
- alwaysApply: false
5
- ---
6
-
7
- # Rule: accessibility-heading-order
8
-
9
- Heading levels should increase sequentially for semantic structure
10
-
11
- ## 🔍 Pattern
12
-
13
- ```regex
14
- <h([3-6])>
15
- ```
16
-
17
- ## 📍 Examples
18
-
19
- ```tsx
20
- // ❌ Bad
21
- <h1>Main Title</h1>
22
- <h4>Subsection</h4>
23
- // ✅ Good
24
- <h1>Main Title</h1>
25
- <h2>Subsection</h2>
26
- ```
@@ -1,24 +0,0 @@
1
- ---
2
- description: When accessibility is checked. Ensure all <img> tags include descriptive alt attributes
3
- globs:
4
- alwaysApply: false
5
- ---
6
-
7
- # Rule: accessibility-image-alt
8
-
9
- Ensure all <img> tags include descriptive alt attributes
10
-
11
- ## 🔍 Pattern
12
-
13
- ```regex
14
- <img(?![^>]*\balt=)
15
- ```
16
-
17
- ## 📍 Examples
18
-
19
- ```tsx
20
- // ❌ Bad
21
- <img src="photo.jpg" />
22
- // ✅ Good
23
- <img src="photo.jpg" alt="Company logo" />
24
- ```
@@ -1,27 +0,0 @@
1
- ---
2
- description: When accessibility is checked. Check if Input fields must have a label or aria-label for screen readers
3
- globs:
4
- alwaysApply: false
5
- ---
6
-
7
- # Rule: accessibility-input-label
8
-
9
- Input fields must have a label or aria-label for screen readers
10
-
11
- ## 🔍 Pattern
12
-
13
- ```regex
14
- <input(?![^>]*(aria-label|aria-labelledby|id=))
15
- ```
16
-
17
- ## 📍 Examples
18
-
19
- ```tsx
20
- // ❌ Bad
21
- <input type="text" />
22
- // ✅ Good
23
- <input aria-label="Search" />
24
- // ✅ Good with label
25
- <label htmlFor="email">Email</label>
26
- <input id="email" type="text" />
27
- ```
@@ -1,26 +0,0 @@
1
- ---
2
- description: When accessibility is checked. Check if Anchor tags must have accessible names
3
- globs:
4
- alwaysApply: false
5
- ---
6
-
7
- # Rule: accessibility-link-name
8
-
9
- Anchor tags must have accessible names
10
-
11
- ## 🔍 Pattern
12
-
13
- ```regex
14
- <a(?![^>]*\b(aria-label|aria-labelledby|title)=)(?![^>]*>\s*\w+\s*</a>)
15
- ```
16
-
17
- ## 📍 Examples
18
-
19
- ```tsx
20
- // ❌ Bad
21
- <a href="/profile"></a>
22
- // ✅ Good
23
- <a href="/profile">Your Profile</a>
24
- // ✅ Good
25
- <a href="/profile" aria-label="Profile page"><UserIcon /></a>
26
- ```
@@ -1,66 +0,0 @@
1
- ---
2
- description: How to add or edit Cursor rules in our project
3
- globs:
4
- alwaysApply: false
5
- ---
6
- # Cursor Rules Location
7
-
8
- How to add new cursor rules to the project
9
-
10
- 1. Always place rule files in PROJECT_ROOT/.cursor/rules/:
11
- ```
12
- .cursor/rules/
13
- ├── your-rule-name.mdc
14
- ├── another-rule.mdc
15
- └── ...
16
- ```
17
-
18
- 2. Follow the naming convention:
19
- - Use kebab-case for filenames
20
- - Always use .mdc extension
21
- - Make names descriptive of the rule's purpose
22
-
23
- 3. Directory structure:
24
- ```
25
- PROJECT_ROOT/
26
- ├── .cursor/
27
- │ └── rules/
28
- │ ├── your-rule-name.mdc
29
- │ └── ...
30
- └── ...
31
- ```
32
-
33
- 4. Never place rule files:
34
- - In the project root
35
- - In subdirectories outside .cursor/rules
36
- - In any other location
37
-
38
- 5. Cursor rules have the following structure:
39
-
40
- ````
41
- ---
42
- description: Short description of the rule's purpose
43
- globs: optional/path/pattern/**/*
44
- alwaysApply: false
45
- ---
46
- # Rule Title
47
-
48
- Main content explaining the rule with markdown formatting.
49
-
50
- 1. Step-by-step instructions
51
- 2. Code examples
52
- 3. Guidelines
53
-
54
- Example:
55
- ```typescript
56
- // Good example
57
- function goodExample() {
58
- // Implementation following guidelines
59
- }
60
-
61
- // Bad example
62
- function badExample() {
63
- // Implementation not following guidelines
64
- }
65
- ```
66
- ````