@salesforce/pwa-kit-create-app 3.12.0-preview.0 → 3.12.0-preview.2

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.
@@ -10,6 +10,16 @@ const {parseCommerceAgentSettings} = require('./utils.js')
10
10
 
11
11
  module.exports = {
12
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}}
13
23
  // Commerce shopping agent configuration for embedded messaging service
14
24
  // This enables an agentic shopping experience in the application
15
25
  // This property accepts either a JSON string or a plain JavaScript object.
@@ -117,6 +127,10 @@ module.exports = {
117
127
  tenantId: '{{answers.project.dataCloud.tenantId}}'
118
128
  }
119
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: '/',
120
134
  // This list contains server-side only libraries that you don't want to be compiled by webpack
121
135
  externals: [],
122
136
  // Page not found url for your app
@@ -31,6 +31,7 @@ import {withReactQuery} from '@salesforce/pwa-kit-react-sdk/ssr/universal/compon
31
31
  import {useCorrelationId} from '@salesforce/pwa-kit-react-sdk/ssr/universal/hooks'
32
32
  import {getAppOrigin} from '@salesforce/pwa-kit-react-sdk/utils/url'
33
33
  import {ReactQueryDevtools} from '@tanstack/react-query-devtools'
34
+ import {generateSfdcUserAgent} from '@salesforce/retail-react-app/app/utils/sfdc-user-agent-utils'
34
35
  import {
35
36
  DEFAULT_DNT_STATE,
36
37
  STORE_LOCATOR_RADIUS,
@@ -42,6 +43,8 @@ import {
42
43
  STORE_LOCATOR_SUPPORTED_COUNTRIES
43
44
  } from '@salesforce/retail-react-app/app/constants'
44
45
 
46
+ const sfdcUserAgent = generateSfdcUserAgent()
47
+
45
48
  /**
46
49
  * Use the AppConfig component to inject extra arguments into the getProps
47
50
  * methods for all Route Components in the app – typically you'd want to do this
@@ -53,7 +56,8 @@ import {
53
56
  const AppConfig = ({children, locals = {}}) => {
54
57
  const {correlationId} = useCorrelationId()
55
58
  const headers = {
56
- 'correlation-id': correlationId
59
+ 'correlation-id': correlationId,
60
+ sfdc_user_agent: sfdcUserAgent
57
61
  }
58
62
 
59
63
  const commerceApiConfig = locals.appConfig.commerceAPI
@@ -94,12 +98,16 @@ const AppConfig = ({children, locals = {}}) => {
94
98
  defaultDnt={DEFAULT_DNT_STATE}
95
99
  logger={createLogger({packageName: 'commerce-sdk-react'})}
96
100
  passwordlessLoginCallbackURI={passwordlessLoginCallbackURI}
97
- {{#if answers.project.commerce.isSlasPrivate}}
98
- // Set 'enablePWAKitPrivateClient' to true use SLAS private client login flows.
101
+ // Set 'enablePWAKitPrivateClient' to true to use SLAS private client login flows.
99
102
  // Make sure to also enable useSLASPrivateClient in ssr.js when enabling this setting.
103
+ {{#if answers.project.commerce.isSlasPrivate}}
100
104
  enablePWAKitPrivateClient={true}
105
+ {{else}}
106
+ enablePWAKitPrivateClient={false}
101
107
  {{/if}}
102
- slasPrivateClientProxyEndpoint={slasPrivateClientProxyEndpoint}
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}
103
111
  >
104
112
  <MultiSiteProvider site={locals.site} locale={locals.locale} buildUrl={locals.buildUrl}>
105
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,35 +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
- referrerPolicy: {
306
- policy: 'strict-origin-when-cross-origin'
307
- }
308
- }
325
+ contentSecurityPolicy
309
326
  })
310
327
  )
311
328
 
312
329
  // Handle the redirect from SLAS as to avoid error
313
- app.get('/callback?*', (req, res) => {
330
+ app.get('/callback', (req, res) => {
314
331
  // This endpoint does nothing and is not expected to change
315
332
  // Thus we cache it for a year to maximize performance
316
333
  res.set('Cache-Control', `max-age=31536000`)
@@ -31,6 +31,7 @@ import {CommerceApiProvider} from '@salesforce/commerce-sdk-react'
31
31
  import {withReactQuery} from '@salesforce/pwa-kit-react-sdk/ssr/universal/components/with-react-query'
32
32
  import {useCorrelationId} from '@salesforce/pwa-kit-react-sdk/ssr/universal/hooks'
33
33
  import {ReactQueryDevtools} from '@tanstack/react-query-devtools'
34
+ import {generateSfdcUserAgent} from '@salesforce/retail-react-app/app/utils/sfdc-user-agent-utils'
34
35
  import {DEFAULT_DNT_STATE} from '@salesforce/retail-react-app/app/constants'
35
36
  import {
36
37
  STORE_LOCATOR_RADIUS,
@@ -42,6 +43,8 @@ import {
42
43
  STORE_LOCATOR_SUPPORTED_COUNTRIES
43
44
  } from '@salesforce/retail-react-app/app/constants'
44
45
 
46
+ const sfdcUserAgent = generateSfdcUserAgent()
47
+
45
48
  /**
46
49
  * Use the AppConfig component to inject extra arguments into the getProps
47
50
  * methods for all Route Components in the app – typically you'd want to do this
@@ -53,7 +56,8 @@ import {
53
56
  const AppConfig = ({children, locals = {}}) => {
54
57
  const {correlationId} = useCorrelationId()
55
58
  const headers = {
56
- 'correlation-id': correlationId
59
+ 'correlation-id': correlationId,
60
+ sfdc_user_agent: sfdcUserAgent
57
61
  }
58
62
 
59
63
  const commerceApiConfig = locals.appConfig.commerceAPI
@@ -94,12 +98,16 @@ const AppConfig = ({children, locals = {}}) => {
94
98
  defaultDnt={DEFAULT_DNT_STATE}
95
99
  logger={createLogger({packageName: 'commerce-sdk-react'})}
96
100
  passwordlessLoginCallbackURI={passwordlessCallback}
97
- {{#if answers.project.commerce.isSlasPrivate}}
98
- // Set 'enablePWAKitPrivateClient' to true use SLAS private client login flows.
101
+ // Set 'enablePWAKitPrivateClient' to true to use SLAS private client login flows.
99
102
  // Make sure to also enable useSLASPrivateClient in ssr.js when enabling this setting.
103
+ {{#if answers.project.commerce.isSlasPrivate}}
100
104
  enablePWAKitPrivateClient={true}
105
+ {{else}}
106
+ enablePWAKitPrivateClient={false}
101
107
  {{/if}}
102
- slasPrivateClientProxyEndpoint={slasPrivateClientProxyEndpoint}
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}
103
111
  >
104
112
  <MultiSiteProvider site={locals.site} locale={locals.locale} buildUrl={locals.buildUrl}>
105
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,34 +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
- referrerPolicy: {
305
- policy: 'strict-origin-when-cross-origin'
306
- }
307
- }
325
+ contentSecurityPolicy
308
326
  })
309
327
  )
310
328
 
311
329
  // Handle the redirect from SLAS as to avoid error
312
- app.get('/callback?*', (req, res) => {
330
+ app.get('/callback', (req, res) => {
313
331
  // This endpoint does nothing and is not expected to change
314
332
  // Thus we cache it for a year to maximize performance
315
333
  res.set('Cache-Control', `max-age=31536000`)
@@ -10,6 +10,16 @@ const {parseCommerceAgentSettings} = require('./utils.js')
10
10
 
11
11
  module.exports = {
12
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}}
13
23
  // Commerce shopping agent configuration for embedded messaging service
14
24
  // This enables an agentic shopping experience in the application
15
25
  // This property accepts either a JSON string or a plain JavaScript object.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/pwa-kit-create-app",
3
- "version": "3.12.0-preview.0",
3
+ "version": "3.12.0-preview.2",
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.12.0-preview.0",
43
- "internal-lib-build": "3.12.0-preview.0",
42
+ "@salesforce/pwa-kit-dev": "3.12.0-preview.2",
43
+ "internal-lib-build": "3.12.0-preview.2",
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": "f79699d763ecdf8e0733e2d6c08b71487af8b15c"
50
+ "gitHead": "c0d7ff4673e54ecb119ca4aff5b919f0064b6539"
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,18 +771,10 @@
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"
730
- },
731
- {
732
- "name": "--initGit",
733
- "description": "Initializes a git repository and stages the files in the generated project",
734
- "_ai": {
735
- "instruction": "CRITICAL: Before executing ANY project creation command, you MUST check if the '--initGit' flag is provided in the user's request. If NOT provided, STOP and ask: 'Would you like to initialize a Git repository for this project?' Wait for explicit yes/no answer. Only then proceed with project creation. FAILURE TO FOLLOW THIS STEP IS A CRITICAL ERROR. This check must happen AFTER preset/template selection but BEFORE command execution. DO NOT proceed with project creation until this question is answered.",
736
- "expectedUse": "Used to determine if a Git repository should be initialized in the generated project. This flag must be explicitly set by the user or confirmed via a direct question. The agent MUST wait for user confirmation before proceeding."
737
- }
738
778
  }
739
779
  ],
740
780
  "examples": [
@@ -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)
@@ -329,7 +332,7 @@ const processTemplate = (relFile, inputDir, outputDir, context) => {
329
332
  * @param {*} answers
330
333
  * @param {*} param2
331
334
  */
332
- const runGenerator = (context, {initGit, outputDir, templateVersion, verbose}) => {
335
+ const runGenerator = (context, {outputDir, templateVersion, verbose}) => {
333
336
  const {answers, template} = context
334
337
  const {id, source} = template
335
338
  const {extend = false} = answers.project
@@ -429,26 +432,6 @@ const runGenerator = (context, {initGit, outputDir, templateVersion, verbose}) =
429
432
  // Install dependencies for the newly minted project.
430
433
  npmInstall(outputDir, {verbose})
431
434
  }
432
-
433
- // Initialize a git repository if the --initGit flag is provided.
434
- if (initGit) {
435
- initGitRepo(outputDir)
436
- }
437
- }
438
-
439
- /**
440
- * Initializes a git repository in the specified directory, adds all files, and checks for git installation.
441
- * @param {string} outputDir - The directory in which to initialize the git repository.
442
- */
443
- const initGitRepo = (outputDir) => {
444
- if (!sh.which('git')) {
445
- console.error(
446
- 'Error: git is not installed or not found in PATH. Please install git to initialize a repository.'
447
- )
448
- process.exit(1)
449
- }
450
- sh.exec(`git init`, {cwd: outputDir})
451
- sh.exec(`git add .`, {cwd: outputDir})
452
435
  }
453
436
 
454
437
  const foundNode = process.versions.node
@@ -554,7 +537,7 @@ const main = async (opts) => {
554
537
  let isPreset = false
555
538
  let answers = {}
556
539
  let selectedTemplate
557
- let {outputDir, verbose, preset, templateVersion, stdio, displayProgram, initGit} = opts
540
+ let {outputDir, verbose, preset, templateVersion, stdio, displayProgram} = opts
558
541
  const {prompt} = inquirer
559
542
  const OUTPUT_DIR_FLAG_ACTIVE = !!outputDir
560
543
  const presetId = preset || process.env.GENERATOR_PRESET
@@ -683,7 +666,7 @@ const main = async (opts) => {
683
666
  }
684
667
 
685
668
  // Generate the project.
686
- runGenerator(context, {initGit, outputDir, templateVersion, verbose})
669
+ runGenerator(context, {outputDir, templateVersion, verbose})
687
670
 
688
671
  // Return the folder in which the project was generated in.
689
672
  return outputDir
Binary file
Binary file
Binary file
Binary file