diff --git a/packages/pwa-kit-create-app/CHANGELOG.md b/packages/pwa-kit-create-app/CHANGELOG.md
index 11988beb16..73d174a315 100644
--- a/packages/pwa-kit-create-app/CHANGELOG.md
+++ b/packages/pwa-kit-create-app/CHANGELOG.md
@@ -1,4 +1,5 @@
## v3.11.0-dev.0 (May 23, 2025)
+- Add `program.json` + Support for Agent-Friendly CLI Input via stdio [#2662](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2662)
- Change the default ECOM instance in the generated application [#2610](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2610)
- Load active data scripts on demand only [#2623](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/2623)
diff --git a/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/constants.js.hbs b/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/constants.js.hbs
index 5514b49051..24adb416a7 100644
--- a/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/constants.js.hbs
+++ b/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/constants.js.hbs
@@ -16,7 +16,7 @@
import {
DEFAULT_LIMIT_VALUES,
DEFAULT_SEARCH_PARAMS
-} from '{{preset.templateSource.id}}/app/constants'
+} from '{{template.source.name}}/app/constants'
// original value is 25
DEFAULT_LIMIT_VALUES[0] = 3
@@ -26,4 +26,4 @@ export const CUSTOM_HOME_TITLE = '🎉 Hello Extensible React Template!'
export {DEFAULT_LIMIT_VALUES, DEFAULT_SEARCH_PARAMS}
-export * from '{{preset.templateSource.id}}/app/constants'
+export * from '{{template.source.name}}/app/constants'
diff --git a/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/pages/home/index.jsx.hbs b/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/pages/home/index.jsx.hbs
index c8a31056f1..8ccb6b2e11 100644
--- a/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/pages/home/index.jsx.hbs
+++ b/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/pages/home/index.jsx.hbs
@@ -9,19 +9,19 @@ import {useIntl, FormattedMessage} from 'react-intl'
import {useLocation} from 'react-router-dom'
// Components
-import {Box, Button, Stack, Link} from '{{preset.templateSource.id}}/app/components/shared/ui'
+import {Box, Button, Stack, Link} from '{{template.source.name}}/app/components/shared/ui'
// Project Components
-import Hero from '{{preset.templateSource.id}}/app/components/hero'
-import Seo from '{{preset.templateSource.id}}/app/components/seo'
-import Section from '{{preset.templateSource.id}}/app/components/section'
-import ProductScroller from '{{preset.templateSource.id}}/app/components/product-scroller'
+import Hero from '{{template.source.name}}/app/components/hero'
+import Seo from '{{template.source.name}}/app/components/seo'
+import Section from '{{template.source.name}}/app/components/section'
+import ProductScroller from '{{template.source.name}}/app/components/product-scroller'
// Others
import {getAssetUrl} from '@salesforce/pwa-kit-react-sdk/ssr/universal/utils'
//Hooks
-import useEinstein from '{{preset.templateSource.id}}/app/hooks/use-einstein'
+import useEinstein from '{{template.source.name}}/app/hooks/use-einstein'
// Constants
import {
diff --git a/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/routes.jsx.hbs b/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/routes.jsx.hbs
index 0133cefdf3..280603f69b 100644
--- a/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/routes.jsx.hbs
+++ b/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/routes.jsx.hbs
@@ -15,9 +15,9 @@ import loadable from '@loadable/component'
import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
// Components
-import {Skeleton} from '{{preset.templateSource.id}}/app/components/shared/ui'
-import {configureRoutes} from '{{preset.templateSource.id}}/app/utils/routes-utils'
-import {routes as _routes} from '{{preset.templateSource.id}}/app/routes'
+import {Skeleton} from '{{template.source.name}}/app/components/shared/ui'
+import {configureRoutes} from '{{template.source.name}}/app/utils/routes-utils'
+import {routes as _routes} from '{{template.source.name}}/app/routes'
const fallback =
diff --git a/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/static/manifest.json.hbs b/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/static/manifest.json.hbs
index 7b639dcb2d..947eaef433 100644
--- a/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/static/manifest.json.hbs
+++ b/packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/static/manifest.json.hbs
@@ -1,6 +1,6 @@
{
- "name": "{{preset.id}}",
- "short_name": "{{preset.id}}",
+ "name": "{{general.presetOrTemplateId}}",
+ "short_name": "{{general.presetOrTemplateId}}",
"start_url": "/?homescreen=1",
"background_color": "#fff",
"theme_color": "#4e439b",
diff --git a/packages/pwa-kit-create-app/assets/bootstrap/js/package.json.hbs b/packages/pwa-kit-create-app/assets/bootstrap/js/package.json.hbs
index 796d3b65a8..3bd9708a12 100644
--- a/packages/pwa-kit-create-app/assets/bootstrap/js/package.json.hbs
+++ b/packages/pwa-kit-create-app/assets/bootstrap/js/package.json.hbs
@@ -7,11 +7,11 @@
"npm": "^9.0.0 || ^10.0.0 || ^11.0.0"
},
"ccExtensibility": {
- "extends": "{{preset.templateSource.id}}",
+ "extends": "{{template.source.name}}",
"overridesDir": "overrides"
},
"devDependencies": {
- "{{preset.templateSource.id}}": "{{answers.general.packageJSON.version}}"
+ "{{template.source.name}}": "{{answers.general.packageJSON.version}}"
},
"scripts": {
{{#each answers.general.packageJSON.scripts}}
diff --git a/packages/pwa-kit-create-app/package.json b/packages/pwa-kit-create-app/package.json
index 9d0d55119c..bd8bcb08a2 100644
--- a/packages/pwa-kit-create-app/package.json
+++ b/packages/pwa-kit-create-app/package.json
@@ -17,7 +17,8 @@
},
"files": [
"assets",
- "templates"
+ "templates",
+ "program.json"
],
"scripts": {
"format": "internal-lib-build format \"**/*.{js,jsx}\"",
diff --git a/packages/pwa-kit-create-app/program.json b/packages/pwa-kit-create-app/program.json
new file mode 100644
index 0000000000..1a62a6419c
--- /dev/null
+++ b/packages/pwa-kit-create-app/program.json
@@ -0,0 +1,695 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "metadata": {
+ "name": "pwa-kit-create-app",
+ "description": "This file contains the schema for the pwa-kit-create-app CLI tool. Includes the schemas for presets, templates, validators, and questions. It also includes the data for those presets, templates, and validators."
+ },
+ "schemas": {
+ "answers": {
+ "type": "object",
+ "_ai": {
+ "instruction": "Use this during project creation when piping in answers from stdin. It's very important that you include the template id keyed as `general.presetOrTemplateId` in the answers object. This can be derived from the `templateId` property of the preset object or the `id` property of the template object.",
+ "expectedUse": "This data object is used to store the answers to the template questions asked by the program and answered by the user."
+ }
+ },
+ "questions": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "name",
+ "message"
+ ],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ },
+ "choices": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "name",
+ "value"
+ ],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "value": {}
+ }
+ }
+ }
+ },
+ "additionalProperties": true
+ }
+ },
+ "presets": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "id",
+ "name",
+ "description",
+ "answers",
+ "private",
+ "templateId"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "shortDescription": {
+ "type": "string"
+ },
+ "templateId": {
+ "type": "string"
+ },
+ "answers": {
+ "type": "object",
+ "additionalProperties": {}
+ },
+ "private": {
+ "type": "boolean",
+ "_ai": {
+ "instruction": "This property is used to filter what presets are shown to the user. If the preset is private, should NOT be shown to the user in a list of selectable presets. This equates to the 'when' property Inquirer question schema.",
+ "expectedUse": "Hiding presets from the user."
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "_ai": {
+ "instruction": "This property is used as a source of all available presets. When an AI agent is asked to select a preset, the selected preset should be returned as the 'general.presetOrTemplateId' property including all the presets answers under the 'answers' property.",
+ "expectedUse": "List of all available presets."
+ }
+ },
+ "templates": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "id",
+ "name",
+ "description",
+ "source",
+ "questions",
+ "private"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "_ai": {
+ "instruction": "When creating a new project using a template, this property should be renamed to `presetOrTemplateId` and is used to identify the template.",
+ "expectedUse": "Used as a unique identifier for the template."
+ }
+ },
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "shortDescription": {
+ "type": "string"
+ },
+ "source": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "npm",
+ "name"
+ ]
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "type"
+ ]
+ },
+ "questions": {
+ "$ref": "questions.json",
+ "_ai": {
+ "instruction": "This property references the Inquirer Question interface defined here https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/inquirer/index.d.ts",
+ "expectedUse": "Instruct agent on how to ask the user questions."
+ }
+ },
+ "assets": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "answers": {
+ "type": "object",
+ "additionalProperties": {}
+ },
+ "private": {
+ "type": "boolean",
+ "_ai": {
+ "instruction": "This property is used to filter what templates are shown to the user.",
+ "expectedUse": "Hiding templates from the user."
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "_ai": {
+ "instruction": "Do not display templates that are private. Answer is a key/value pair where the key is `project.template` and the value is the template id.",
+ "expectedUse": "Asking the user which template they want to use."
+ }
+ },
+ "validators": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "The unique identifier for the validator."
+ },
+ "name": {
+ "type": "string",
+ "description": "The name of the validator."
+ },
+ "description": {
+ "type": "string",
+ "description": "A brief description of what the validator does."
+ },
+ "message": {
+ "type": "string",
+ "description": "The error message to display if validation fails.",
+ "_ai": {
+ "instruction": "When validation fails, use this message to inform the user what went wrong.",
+ "expectedUse": "For validation error feedback."
+ }
+ },
+ "regex": {
+ "type": "string",
+ "description": "The validation logic, often a regular expression or a reference to a function.",
+ "_ai": {
+ "instruction": "Use this regex to validate the user input.",
+ "expectedUse": "For validation of user input."
+ }
+ }
+ },
+ "required": [
+ "id",
+ "name",
+ "description",
+ "message",
+ "regex"
+ ]
+ }
+ },
+ "options": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "defaultValue": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name",
+ "description"
+ ]
+ }
+ },
+ "examples": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string"
+ },
+ "command": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "description",
+ "command"
+ ]
+ }
+ }
+ },
+ "data": {
+ "presets": [
+ {
+ "id": "retail-react-app-demo",
+ "name": "Retail React App Demo",
+ "description": "Generate a project using the settings for a special B2C Commerce instance that is used for demo purposes. No questions are asked. Use this preset to try out PWA Kit.",
+ "shortDescription": "The Retail app with demo Commerce Cloud instance",
+ "templateId": "retail-react-app",
+ "answers": {
+ "project.extend": true,
+ "project.hybrid": false,
+ "project.name": "demo-storefront",
+ "project.commerce.instanceUrl": "https://zzte-053.dx.commercecloud.salesforce.com",
+ "project.commerce.clientId": "1d763261-6522-4913-9d52-5d947d3b94c4",
+ "project.commerce.siteId": "RefArch",
+ "project.commerce.organizationId": "f_ecom_zzte_053",
+ "project.commerce.shortCode": "kv7kzm78",
+ "project.commerce.isSlasPrivate": false,
+ "project.einstein.clientId": "1ea06c6e-c936-4324-bcf0-fada93f83bb1",
+ "project.einstein.siteId": "aaij-MobileFirst",
+ "project.dataCloud.appSourceId": "f22ae831-ac03-4bf6-afc1-3a0b19f1ea8e",
+ "project.dataCloud.tenantId": "mmydmztgh04dczjzmnsw0zd0g8.pc-rnd",
+ "project.demo.enableDemoSettings": false
+ },
+ "private": false
+ },
+ {
+ "id": "retail-react-app-demo-site-internal",
+ "name": "Retail React App Demo Store",
+ "description": "Generates a project using the settings for a special B2C Commerce instance that is used for demo purposes. The demo site is accessible at https://pwa-kit.mobify-storefront.com/ This environment uses a SLAS private client and has social and passwordless login enabled. This environment is set up to use multiple locales. Future features that are enabled for the demo environment may be added to this preset.",
+ "shortDescription": "The Retail app with demo Commerce Cloud instance and a private SLAS client",
+ "templateId": "retail-react-app",
+ "answers": {
+ "project.extend": false,
+ "project.hybrid": false,
+ "project.name": "demo-storefront",
+ "project.commerce.instanceUrl": "https://zzrf-001.dx.commercecloud.salesforce.com",
+ "project.commerce.clientId": "083859f2-5d93-4209-b999-a112266d63a0",
+ "project.commerce.siteId": "RefArchGlobal",
+ "project.commerce.organizationId": "f_ecom_zzrf_001",
+ "project.commerce.shortCode": "kv7kzm78",
+ "project.commerce.isSlasPrivate": true,
+ "project.einstein.clientId": "1ea06c6e-c936-4324-bcf0-fada93f83bb1",
+ "project.einstein.siteId": "aaij-MobileFirst",
+ "project.dataCloud.appSourceId": "f22ae831-ac03-4bf6-afc1-3a0b19f1ea8e",
+ "project.dataCloud.tenantId": "mmydmztgh04dczjzmnsw0zd0g8.pc-rnd",
+ "project.demo.enableDemoSettings": true
+ },
+ "private": true
+ },
+ {
+ "id": "retail-react-app-test-project",
+ "name": "Retail React App Test Project",
+ "description": "",
+ "templateId": "retail-react-app",
+ "answers": {
+ "project.extend": true,
+ "project.hybrid": false,
+ "project.name": "retail-react-app",
+ "project.commerce.instanceUrl": "https://zzrf-001.dx.commercecloud.salesforce.com",
+ "project.commerce.clientId": "c9c45bfd-0ed3-4aa2-9971-40f88962b836",
+ "project.commerce.siteId": "RefArch",
+ "project.commerce.organizationId": "f_ecom_zzrf_001",
+ "project.commerce.shortCode": "kv7kzm78",
+ "project.commerce.isSlasPrivate": false,
+ "project.einstein.clientId": "1ea06c6e-c936-4324-bcf0-fada93f83bb1",
+ "project.einstein.siteId": "aaij-MobileFirst",
+ "project.dataCloud.appSourceId": "f22ae831-ac03-4bf6-afc1-3a0b19f1ea8e",
+ "project.dataCloud.tenantId": "mmydmztgh04dczjzmnsw0zd0g8.pc-rnd",
+ "project.demo.enableDemoSettings": false
+ },
+ "private": true
+ },
+ {
+ "id": "retail-react-app-private-slas-client",
+ "name": "Retail React App Private SLAS client project",
+ "description": "",
+ "templateId": "retail-react-app",
+ "answers": {
+ "project.extend": true,
+ "project.hybrid": false,
+ "project.name": "retail-react-app",
+ "project.commerce.instanceUrl": "https://zzrf-002.dx.commercecloud.salesforce.com",
+ "project.commerce.clientId": "89655706-9a0d-49ba-a1e5-18bb2d616374",
+ "project.commerce.siteId": "RefArch",
+ "project.commerce.organizationId": "f_ecom_zzrf_002",
+ "project.commerce.shortCode": "kv7kzm78",
+ "project.commerce.isSlasPrivate": true,
+ "project.einstein.clientId": "1ea06c6e-c936-4324-bcf0-fada93f83bb1",
+ "project.einstein.siteId": "aaij-MobileFirst",
+ "project.dataCloud.appSourceId": "f22ae831-ac03-4bf6-afc1-3a0b19f1ea8e",
+ "project.dataCloud.tenantId": "mmydmztgh04dczjzmnsw0zd0g8.pc-rnd",
+ "project.demo.enableDemoSettings": false
+ },
+ "private": true
+ },
+ {
+ "id": "retail-react-app-bug-bounty",
+ "name": "Retail React App Bug Bounty Project",
+ "description": "",
+ "templateId": "retail-react-app",
+ "answers": {
+ "project.extend": true,
+ "project.hybrid": false,
+ "project.name": "retail-react-app",
+ "project.commerce.instanceUrl": "https://zzec-006.dx.commercecloud.salesforce.com",
+ "project.commerce.clientId": "b56e7ad3-2237-42c9-8f55-41e63ebca420",
+ "project.commerce.siteId": "RefArch",
+ "project.commerce.organizationId": "f_ecom_zzec_006",
+ "project.commerce.shortCode": "staging-001",
+ "project.commerce.isSlasPrivate": true,
+ "project.einstein.clientId": "1ea06c6e-c936-4324-bcf0-fada93f83bb1",
+ "project.einstein.siteId": "aaij-MobileFirst",
+ "project.dataCloud.appSourceId": "f22ae831-ac03-4bf6-afc1-3a0b19f1ea8e",
+ "project.dataCloud.tenantId": "mmydmztgh04dczjzmnsw0zd0g8.pc-rnd",
+ "project.demo.enableDemoSettings": false
+ },
+ "private": true
+ },
+ {
+ "id": "retail-react-app-hybrid-test-project",
+ "name": "Retail React App Hybrid Test Private SLAS Project",
+ "description": "",
+ "templateId": "retail-react-app",
+ "answers": {
+ "project.extend": true,
+ "project.hybrid": true,
+ "project.name": "retail-react-app",
+ "project.commerce.instanceUrl": "https://test.phased-launch-testing.com/",
+ "project.commerce.clientId": "99b4e081-00cf-454a-95b0-26ac2b824931",
+ "project.commerce.siteId": "RefArch",
+ "project.commerce.organizationId": "f_ecom_bdpx_dev",
+ "project.commerce.shortCode": "xitgmcd3",
+ "project.commerce.isSlasPrivate": true,
+ "project.einstein.clientId": "1ea06c6e-c936-4324-bcf0-fada93f83bb1",
+ "project.einstein.siteId": "aaij-MobileFirst",
+ "project.dataCloud.appSourceId": "f22ae831-ac03-4bf6-afc1-3a0b19f1ea8e",
+ "project.dataCloud.tenantId": "mmydmztgh04dczjzmnsw0zd0g8.pc-rnd",
+ "project.demo.enableDemoSettings": false
+ },
+ "private": true
+ },
+ {
+ "id": "retail-react-app-hybrid-public-client-test-project",
+ "name": "Retail React App Hybrid Test Public SLAS client project",
+ "description": "",
+ "templateId": "retail-react-app",
+ "answers": {
+ "project.extend": true,
+ "project.hybrid": true,
+ "project.name": "retail-react-app",
+ "project.commerce.instanceUrl": "https://www.phased-launch-testing.com/",
+ "project.commerce.clientId": "e7e22b7f-a904-4f3a-8022-49dbee696485",
+ "project.commerce.siteId": "RefArch",
+ "project.commerce.organizationId": "f_ecom_bjnl_prd",
+ "project.commerce.shortCode": "performance-001",
+ "project.commerce.isSlasPrivate": false,
+ "project.einstein.clientId": "1ea06c6e-c936-4324-bcf0-fada93f83bb1",
+ "project.einstein.siteId": "aaij-MobileFirst",
+ "project.dataCloud.appSourceId": "f22ae831-ac03-4bf6-afc1-3a0b19f1ea8e",
+ "project.dataCloud.tenantId": "mmydmztgh04dczjzmnsw0zd0g8.pc-rnd",
+ "project.demo.enableDemoSettings": false
+ },
+ "private": true
+ },
+ {
+ "id": "typescript-minimal-test-project",
+ "name": "Template Minimal Test Project",
+ "description": "",
+ "templateId": "typescript-minimal",
+ "answers": {
+ "project.name": "typescript-minimal"
+ },
+ "private": true
+ },
+ {
+ "id": "express-minimal-test-project",
+ "name": "Express Minimal Test Project",
+ "description": "",
+ "templateId": "typescript-minimal",
+ "answers": {
+ "project.name": "express-minimal"
+ },
+ "private": true
+ }
+ ],
+ "templates": [
+ {
+ "id": "retail-react-app",
+ "name": "Retail React App",
+ "description": "Generate a project using custom settings by answering questions about a B2C Commerce instance. Use this preset to connect to an existing instance, such as a sandbox.",
+ "shortDescription": "The Retail app using your own Commerce Cloud instance",
+ "source": {
+ "type": "npm",
+ "name": "@salesforce/retail-react-app"
+ },
+ "questions": [
+ {
+ "name": "project.extend",
+ "message": "Do you wish to use template extensibility?",
+ "type": "list",
+ "choices": [
+ {
+ "name": "Yes",
+ "value": true
+ },
+ {
+ "name": "No",
+ "value": false
+ }
+ ]
+ },
+ {
+ "name": "project.name",
+ "message": "What is the name of your Project?",
+ "validator": "validProjectName",
+ "_ai": {
+ "instruction": "Should validate the user input using the 'validateProjectName' validator.",
+ "expectedUse": "Instruct agent on how to validate user input."
+ }
+ },
+ {
+ "name": "project.commerce.instanceUrl",
+ "message": "What is the URL for your Commerce Cloud instance?",
+ "validator": "validUrl",
+ "_ai": {
+ "instruction": "Should validate the user input using the 'validUrl' validator.",
+ "expectedUse": "Instruct agent on how to validate user input."
+ }
+ },
+ {
+ "name": "project.commerce.clientId",
+ "message": "What is your SLAS Client ID?",
+ "validator": "validClientId",
+ "_ai": {
+ "instruction": "Should validate the user input using the 'validClientId' validator.",
+ "expectedUse": "Instruct agent on how to validate user input."
+ }
+ },
+ {
+ "name": "project.commerce.isSlasPrivate",
+ "message": "Is your SLAS client private?",
+ "type": "list",
+ "choices": [
+ {
+ "name": "Yes",
+ "value": true
+ },
+ {
+ "name": "No",
+ "value": false
+ }
+ ]
+ },
+ {
+ "name": "project.commerce.siteId",
+ "message": "What is your Site ID in Business Manager?",
+ "validator": "validSiteId",
+ "_ai": {
+ "instruction": "Should validate the user input using the 'validSiteId' validator.",
+ "expectedUse": "Instruct agent on how to validate user input."
+ }
+ },
+ {
+ "name": "project.commerce.organizationId",
+ "message": "What is your Commerce API organization ID in Business Manager?",
+ "validator": "validOrganizationId",
+ "_ai": {
+ "instruction": "Should validate the user input using the 'validOrganizationId' validator.",
+ "expectedUse": "Instruct agent on how to validate user input."
+ }
+ },
+ {
+ "name": "project.commerce.shortCode",
+ "message": "What is your Commerce API short code in Business Manager?",
+ "validator": "validShortCode",
+ "_ai": {
+ "instruction": "Should validate the user input using the 'validShortCode' validator.",
+ "expectedUse": "Instruct agent on how to validate user input."
+ }
+ }
+ ],
+ "assets": [
+ "translations"
+ ],
+ "private": false
+ },
+ {
+ "id": "typescript-minimal",
+ "name": "Template Minimal Project",
+ "description": "Generate a project using a bare-bones TypeScript app template. Use this as a TypeScript starting point or as a base on top of which to build new TypeScript project templates for Managed Runtime.",
+ "source": {
+ "type": "bundle"
+ },
+ "questions": [
+ {
+ "name": "project.name",
+ "message": "What is the name of your Project?",
+ "validator": "validateProjectName",
+ "_ai": {
+ "instruction": "Should validate the user input using the 'validateProjectName' validator.",
+ "expectedUse": "Instruct agent on how to validate user input."
+ }
+ }
+ ],
+ "private": true
+ },
+ {
+ "id": "express-minimal",
+ "name": "Express Minimal Project",
+ "description": "Generate a project using a bare-bones express app template. Use this as a starting point for APIs or as a base on top of which to build new project templates for Managed Runtime.",
+ "source": {
+ "type": "bundle"
+ },
+ "questions": [
+ {
+ "name": "project.name",
+ "message": "What is the name of your Project?",
+ "validator": "validateProjectName",
+ "_ai": {
+ "instruction": "Should validate the user input using the 'validateProjectName' validator.",
+ "expectedUse": "Instruct agent on how to validate user input."
+ }
+ }
+ ],
+ "private": true
+ },
+ {
+ "id": "mrt-reference-app",
+ "name": "Managed Runtime Reference App",
+ "description": "",
+ "source": {
+ "type": "bundle"
+ },
+ "questions": [
+ {
+ "name": "project.name",
+ "message": "What is the name of your Project?",
+ "validator": "validateProjectName",
+ "_ai": {
+ "instruction": "Should validate the user input using the 'validateProjectName' validator.",
+ "expectedUse": "Instruct agent on how to validate user input."
+ }
+ }
+ ],
+ "answers": {
+ "project.name": "mrt-reference-app"
+ },
+ "private": true
+ }
+ ],
+ "validators": [
+ {
+ "id": "validProjectName",
+ "name": "Valid Project Name",
+ "description": "Ensures the project name is valid (letters, numbers, space, hyphens, 1-20 characters).",
+ "message": "Value can only contain letters, numbers, space and hyphens; with a maximum length is 20 characters.",
+ "regex": "^[a-zA-Z0-9-\\\\s]{1,20}$"
+ },
+ {
+ "id": "validUrl",
+ "name": "Valid URL",
+ "description": "Ensures the value is an absolute URL.",
+ "message": "Value must be an absolute URL",
+ "regex": "^(https?):\\/\\/[^\\s/$.?#].[^\\s]*$"
+ },
+ {
+ "id": "validSiteId",
+ "name": "Valid Site ID",
+ "description": "Ensures the Site ID is valid.",
+ "message": "Valid characters are alphanumeric, hyphen, or underscore",
+ "regex": "^[a-z0-9_-]+$"
+ },
+ {
+ "id": "validShortCode",
+ "name": "Valid Short Code",
+ "description": "Ensures the short code is valid.",
+ "message": "Invalid format. Use docs to find more information about valid configurations: https://developer.salesforce.com/docs/commerce/commerce-api/guide/commerce-api-configuration-values",
+ "regex": "^([0-9A-Z]{8})$"
+ },
+ {
+ "id": "validClientId",
+ "name": "Valid Client ID",
+ "description": "Ensures the client ID is valid.",
+ "message": "Invalid format. Use docs to find more information about valid configurations: https://developer.salesforce.com/docs/commerce/commerce-api/guide/base-url.html",
+ "regex": "^([0-9A-Z]{8}-[0-9A-Z]{4}-[0-9A-Z]{4}-[0-9A-Z]{4}-[0-9A-Z]{12}|aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa)$"
+ },
+ {
+ "id": "validOrganizationId",
+ "name": "Valid Organization ID",
+ "description": "Ensures the organization ID is valid.",
+ "message": "Invalid format. Use docs to find more information about valid configurations: https://developer.salesforce.com/docs/commerce/commerce-api/guide/base-url.html",
+ "regex": "^(f_ecom)_([A-Z]{4})_(prd|stg|dev|[0-9]{3}|s[0-9]{2})$"
+ }
+ ],
+ "options": [
+ {
+ "name": "--outputDir ",
+ "description": "Path to the output directory for the new project"
+ },
+ {
+ "name": "--preset ",
+ "description": "The name of a project preset to use."
+ },
+ {
+ "name": "--templateVersion ",
+ "description": "The version of the template to be generated when it's source is NPM.",
+ "defaultValue": "latest"
+ },
+ {
+ "name": "--verbose",
+ "description": "Print additional logging information to the console."
+ },
+ {
+ "name": "--stdio",
+ "description": "Accept project generation answers from stdin as JSON"
+ },
+ {
+ "name": "--displayProgram",
+ "description": "Display the program.json file detailing the program schema/data and exit"
+ }
+ ],
+ "examples": [
+ {
+ "description": "Generate a project using a preset",
+ "command": "npx @salesforce/pwa-kit-create-app --preset retail-react-app-demo"
+ },
+ {
+ "description": "Generate a project using answers from stdin",
+ "command": "echo {\"project.name\":\"MyProject\", ...} | npx @salesforce/pwa-kit-create-app --stdio"
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/packages/pwa-kit-create-app/scripts/create-mobify-app.js b/packages/pwa-kit-create-app/scripts/create-mobify-app.js
index 72b4be97a7..5602302c12 100755
--- a/packages/pwa-kit-create-app/scripts/create-mobify-app.js
+++ b/packages/pwa-kit-create-app/scripts/create-mobify-app.js
@@ -48,6 +48,38 @@ const semver = require('semver')
const slugify = require('slugify')
const generatorPkg = require('../package.json')
const Handlebars = require('handlebars')
+const PROGRAM = require('../program.json')
+
+// Presets, Templates and Validators
+const {
+ examples: EXAMPLES,
+ options: OPTIONS,
+ presets: PRESETS,
+ templates: TEMPLATES,
+ validators: VALIDATORS
+} = PROGRAM.data
+
+// Questions composed of public presets and public templates.
+// NOTE: We have to do some weird stuff to determine if the thing we are selecting is a preset or a template.
+// There might be a better way to do this.
+// NOTE: Id's between presets and templates are unique. We should not break this contract.
+const INITIAL_QUESTIONS = [
+ {
+ name: 'general.presetOrTemplateId',
+ message: 'Choose a project preset to get started:',
+ type: 'list',
+ choices: [
+ ...PRESETS.filter(({private}) => !private).map(({shortDescription, id}) => ({
+ name: shortDescription,
+ value: id
+ })),
+ ...TEMPLATES.filter(({private}) => !private).map(({shortDescription, id}) => ({
+ name: shortDescription,
+ value: id
+ }))
+ ].sort((a, b) => (a.name || '').localeCompare(b.name))
+ }
+]
const program = new Command()
@@ -55,7 +87,7 @@ sh.set('-e')
// Handlebars helpers
-// Our eslint script uses exscaped double quotes to have windows compatibility. This helper
+// Our eslint script uses escaped double quotes to have windows compatibility. This helper
// will ensure those escaped double quotes are still escaped after processing the template.
Handlebars.registerHelper('script', (object) => object.replaceAll('"', '\\"'))
@@ -64,44 +96,11 @@ const validPreset = (preset) => {
return ALL_PRESET_NAMES.includes(preset)
}
-const validProjectName = (s) => {
- if (s.length > PROJECT_ID_MAX_LENGTH) {
- return `Maximum length is ${PROJECT_ID_MAX_LENGTH} characters.`
- }
- const regex = new RegExp(`^[a-zA-Z0-9-\\s]{1,${PROJECT_ID_MAX_LENGTH}}$`)
- return regex.test(s) || 'Value can only contain letters, numbers, space and hyphens.'
-}
-
-const validUrl = (s) => {
- try {
- new URL(s)
- return true
- } catch (err) {
- return 'Value must be an absolute URL'
- }
-}
-
-const validSiteId = (s) =>
- /^[a-z0-9_-]+$/i.test(s) || 'Valid characters are alphanumeric, hyphen, or underscore'
-
-// To see definitions for Commerce API configuration values, go to
-// https://developer.salesforce.com/docs/commerce/commerce-api/guide/commerce-api-configuration-values.
-const defaultCommerceAPIError =
- 'Invalid format. Use docs to find more information about valid configurations: https://developer.salesforce.com/docs/commerce/commerce-api/guide/commerce-api-configuration-values'
-const validShortCode = (s) => /(^[0-9A-Z]{8}$)/i.test(s) || defaultCommerceAPIError
-
-const validClientId = (s) =>
- /(^[0-9A-Z]{8}-[0-9A-Z]{4}-[0-9A-Z]{4}-[0-9A-Z]{4}-[0-9A-Z]{12}$)/i.test(s) ||
- s === 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' ||
- defaultCommerceAPIError
-const validOrganizationId = (s) =>
- /^(f_ecom)_([A-Z]{4})_(prd|stg|dev|[0-9]{3}|s[0-9]{2})$/i.test(s) || defaultCommerceAPIError
-
// Globals
const GENERATED_PROJECT_VERSION = '0.0.1'
const INITIAL_CONTEXT = {
- preset: undefined,
+ template: undefined,
answers: {
general: {},
project: {}
@@ -109,441 +108,12 @@ const INITIAL_CONTEXT = {
}
const TEMPLATE_SOURCE_NPM = 'npm'
const TEMPLATE_SOURCE_BUNDLE = 'bundle'
-const DEFAULT_TEMPLATE_VERSION = 'latest'
-
-const EXTENSIBILITY_QUESTIONS = [
- {
- name: 'project.extend',
- message: 'Do you wish to use template extensibility?',
- type: 'list',
- choices: [
- {
- name: 'No',
- value: false
- },
- {
- name: 'Yes',
- value: true
- }
- ]
- }
-]
-
-const HYBRID_QUESTIONS = [
- {
- name: 'project.hybrid',
- message: 'Do you wish to set up a phased headless rollout?',
- type: 'list',
- choices: [
- {
- name: 'No',
- value: false
- },
- {
- name: 'Yes',
- value: true
- }
- ]
- }
-]
-
-const MRT_REFERENCE_QUESTIONS = [
- {
- name: 'project.name',
- validate: validProjectName,
- message: 'What is the name of your Project?'
- }
-]
-
-const EXPRESS_MINIMAL_QUESTIONS = [
- {
- name: 'project.name',
- validate: validProjectName,
- message: 'What is the name of your Project?'
- }
-]
-
-const TYPESCRIPT_MINIMAL_QUESTIONS = [
- {
- name: 'project.name',
- validate: validProjectName,
- message: 'What is the name of your Project?'
- }
-]
-
-const RETAIL_REACT_APP_QUESTIONS = [
- {
- name: 'project.name',
- validate: validProjectName,
- message: 'What is the name of your Project?'
- },
- {
- name: 'project.commerce.instanceUrl',
- message: 'What is the URL for your Commerce Cloud instance?',
- validate: validUrl
- },
- {
- name: 'project.commerce.clientId',
- message: 'What is your SLAS Client ID?',
- validate: validClientId
- },
- {
- name: 'project.commerce.isSlasPrivate',
- message: 'Is your SLAS client private?',
- type: 'list',
- choices: [
- {
- name: 'Yes',
- value: true
- },
- {
- name: 'No',
- value: false
- }
- ]
- },
- {
- name: 'project.commerce.siteId',
- message: 'What is your Site ID in Business Manager?',
- validate: validSiteId
- },
- {
- name: 'project.commerce.organizationId',
- message: 'What is your Commerce API organization ID in Business Manager?',
- validate: validOrganizationId
- },
- {
- name: 'project.commerce.shortCode',
- message: 'What is your Commerce API short code in Business Manager?',
- validate: validShortCode
- }
-]
-
-// Project dictionary describing details and how the generator should ask questions etc.
-const PRESETS = [
- {
- id: 'retail-react-app',
- name: 'Retail React App',
- description: `
- Generate a project using custom settings by answering questions about a
- B2C Commerce instance.
-
- Use this preset to connect to an existing instance, such as a sandbox.
- `,
- shortDescription: 'The Retail app using your own Commerce Cloud instance',
- templateSource: {
- type: TEMPLATE_SOURCE_NPM,
- id: '@salesforce/retail-react-app'
- },
- questions: [...EXTENSIBILITY_QUESTIONS, ...RETAIL_REACT_APP_QUESTIONS],
- assets: ['translations'],
- private: false
- },
- {
- id: 'retail-react-app-demo',
- name: 'Retail React App Demo',
- description: `
- Generate a project using the settings for a special B2C Commerce
- instance that is used for demo purposes. No questions are asked.
-
- Use this preset to try out PWA Kit.
- `,
- shortDescription: 'The Retail app with demo Commerce Cloud instance',
- templateSource: {
- type: TEMPLATE_SOURCE_NPM,
- id: '@salesforce/retail-react-app'
- },
- questions: [...EXTENSIBILITY_QUESTIONS, ...RETAIL_REACT_APP_QUESTIONS],
- answers: {
- ['project.extend']: true,
- ['project.hybrid']: false,
- ['project.name']: 'demo-storefront',
- ['project.commerce.instanceUrl']: 'https://production-sitegenesis-dw.demandware.net',
- ['project.commerce.clientId']: '44cfcf31-d64d-4227-9cce-1d9b0716c321',
- ['project.commerce.siteId']: 'RefArch',
- ['project.commerce.organizationId']: 'f_ecom_aaia_prd',
- ['project.commerce.shortCode']: 'xfdy2axw',
- ['project.commerce.isSlasPrivate']: false,
- ['project.einstein.clientId']: '1ea06c6e-c936-4324-bcf0-fada93f83bb1',
- ['project.einstein.siteId']: 'aaij-MobileFirst',
- ['project.dataCloud.appSourceId']: 'f22ae831-ac03-4bf6-afc1-3a0b19f1ea8e',
- ['project.dataCloud.tenantId']: 'mmydmztgh04dczjzmnsw0zd0g8.pc-rnd',
- ['project.demo.enableDemoSettings']: false
- },
- assets: ['translations'],
- private: false
- },
- {
- id: 'retail-react-app-demo-site-internal',
- name: 'Retail React App Demo Store',
- description: `
- Generates a project using the settings for a special B2C Commerce instance that is used
- for demo purposes. The demo site is accessible at https://pwa-kit.mobify-storefront.com/
-
- This environment uses a SLAS private client and has social and passwordless login enabled.
- This environment is set up to use multiple locales.
- Future features that are enabled for the demo environment may be added to this preset.
- `,
- shortDescription:
- 'The Retail app with demo Commerce Cloud instance and a private SLAS client',
- templateSource: {
- type: TEMPLATE_SOURCE_NPM,
- id: '@salesforce/retail-react-app'
- },
- questions: [...EXTENSIBILITY_QUESTIONS, ...RETAIL_REACT_APP_QUESTIONS],
- answers: {
- ['project.extend']: false, // Intentionally not an extensible project so that the correct logos appear on demo site
- ['project.hybrid']: false,
- ['project.name']: 'demo-storefront',
- ['project.commerce.instanceUrl']: 'https://zzrf-001.dx.commercecloud.salesforce.com',
- ['project.commerce.clientId']: '083859f2-5d93-4209-b999-a112266d63a0',
- ['project.commerce.siteId']: 'RefArchGlobal',
- ['project.commerce.organizationId']: 'f_ecom_zzrf_001',
- ['project.commerce.shortCode']: 'kv7kzm78',
- ['project.commerce.isSlasPrivate']: true,
- ['project.einstein.clientId']: '1ea06c6e-c936-4324-bcf0-fada93f83bb1',
- ['project.einstein.siteId']: 'aaij-MobileFirst',
- ['project.dataCloud.appSourceId']: 'f22ae831-ac03-4bf6-afc1-3a0b19f1ea8e',
- ['project.dataCloud.tenantId']: 'mmydmztgh04dczjzmnsw0zd0g8.pc-rnd',
- ['project.demo.enableDemoSettings']: true // True only for presets deployed to demo environments like pwa-kit.mobify-storefront.com
- },
- assets: ['translations'],
- private: true
- },
- {
- id: 'retail-react-app-test-project',
- name: 'Retail React App Test Project',
- description: '',
- templateSource: {
- type: TEMPLATE_SOURCE_NPM,
- id: '@salesforce/retail-react-app'
- },
- questions: [...EXTENSIBILITY_QUESTIONS, ...RETAIL_REACT_APP_QUESTIONS],
- answers: {
- ['project.extend']: true,
- ['project.hybrid']: false,
- ['project.name']: 'retail-react-app',
- ['project.commerce.instanceUrl']: 'https://zzrf-001.dx.commercecloud.salesforce.com',
- ['project.commerce.clientId']: 'c9c45bfd-0ed3-4aa2-9971-40f88962b836',
- ['project.commerce.siteId']: 'RefArch',
- ['project.commerce.organizationId']: 'f_ecom_zzrf_001',
- ['project.commerce.shortCode']: 'kv7kzm78',
- ['project.commerce.isSlasPrivate']: false,
- ['project.einstein.clientId']: '1ea06c6e-c936-4324-bcf0-fada93f83bb1',
- ['project.einstein.siteId']: 'aaij-MobileFirst',
- ['project.dataCloud.appSourceId']: 'f22ae831-ac03-4bf6-afc1-3a0b19f1ea8e',
- ['project.dataCloud.tenantId']: 'mmydmztgh04dczjzmnsw0zd0g8.pc-rnd',
- ['project.demo.enableDemoSettings']: false
- },
- assets: ['translations'],
- private: true
- },
- {
- id: 'retail-react-app-private-slas-client',
- name: 'Retail React App Private SLAS client project',
- description: '',
- templateSource: {
- type: TEMPLATE_SOURCE_NPM,
- id: '@salesforce/retail-react-app'
- },
- questions: [...EXTENSIBILITY_QUESTIONS, ...RETAIL_REACT_APP_QUESTIONS],
- answers: {
- ['project.extend']: true,
- ['project.hybrid']: false,
- ['project.name']: 'retail-react-app',
- ['project.commerce.instanceUrl']: 'https://zzrf-002.dx.commercecloud.salesforce.com',
- ['project.commerce.clientId']: '89655706-9a0d-49ba-a1e5-18bb2d616374',
- ['project.commerce.siteId']: 'RefArch',
- ['project.commerce.organizationId']: 'f_ecom_zzrf_002',
- ['project.commerce.shortCode']: 'kv7kzm78',
- ['project.commerce.isSlasPrivate']: true,
- ['project.einstein.clientId']: '1ea06c6e-c936-4324-bcf0-fada93f83bb1',
- ['project.einstein.siteId']: 'aaij-MobileFirst',
- ['project.dataCloud.appSourceId']: 'f22ae831-ac03-4bf6-afc1-3a0b19f1ea8e',
- ['project.dataCloud.tenantId']: 'mmydmztgh04dczjzmnsw0zd0g8.pc-rnd',
- ['project.demo.enableDemoSettings']: false
- },
- assets: ['translations'],
- private: true
- },
- {
- id: 'retail-react-app-bug-bounty',
- name: 'Retail React App Bug Bounty Project',
- description: '',
- templateSource: {
- type: TEMPLATE_SOURCE_NPM,
- id: '@salesforce/retail-react-app'
- },
- questions: [...EXTENSIBILITY_QUESTIONS, ...RETAIL_REACT_APP_QUESTIONS],
- answers: {
- ['project.extend']: true,
- ['project.hybrid']: false,
- ['project.name']: 'retail-react-app',
- ['project.commerce.instanceUrl']: 'https://zzec-006.dx.commercecloud.salesforce.com',
- ['project.commerce.clientId']: 'b56e7ad3-2237-42c9-8f55-41e63ebca420',
- ['project.commerce.siteId']: 'RefArch',
- ['project.commerce.organizationId']: 'f_ecom_zzec_006',
- ['project.commerce.shortCode']: 'staging-001',
- ['project.einstein.clientId']: '1ea06c6e-c936-4324-bcf0-fada93f83bb1',
- ['project.einstein.siteId']: 'aaij-MobileFirst',
- ['project.dataCloud.appSourceId']: 'f22ae831-ac03-4bf6-afc1-3a0b19f1ea8e',
- ['project.dataCloud.tenantId']: 'mmydmztgh04dczjzmnsw0zd0g8.pc-rnd',
- ['project.commerce.isSlasPrivate']: true,
- ['project.demo.enableDemoSettings']: false
- },
- assets: ['translations'],
- private: true
- },
- {
- id: 'retail-react-app-hybrid-test-project',
- name: 'Retail React App Hybrid Test Private SLAS Project',
- description: '',
- templateSource: {
- type: TEMPLATE_SOURCE_NPM,
- id: '@salesforce/retail-react-app'
- },
- questions: [...EXTENSIBILITY_QUESTIONS, ...HYBRID_QUESTIONS, ...RETAIL_REACT_APP_QUESTIONS],
- answers: {
- ['project.extend']: true,
- ['project.hybrid']: true,
- ['project.name']: 'retail-react-app',
- ['project.commerce.instanceUrl']: 'https://test.phased-launch-testing.com/',
- ['project.commerce.clientId']: '99b4e081-00cf-454a-95b0-26ac2b824931',
- ['project.commerce.siteId']: 'RefArch',
- ['project.commerce.organizationId']: 'f_ecom_bdpx_dev',
- ['project.commerce.shortCode']: 'xitgmcd3',
- ['project.einstein.clientId']: '1ea06c6e-c936-4324-bcf0-fada93f83bb1',
- ['project.einstein.siteId']: 'aaij-MobileFirst',
- ['project.commerce.isSlasPrivate']: true,
- ['project.dataCloud.appSourceId']: 'f22ae831-ac03-4bf6-afc1-3a0b19f1ea8e',
- ['project.dataCloud.tenantId']: 'mmydmztgh04dczjzmnsw0zd0g8.pc-rnd',
- ['project.demo.enableDemoSettings']: false
- },
- assets: ['translations'],
- private: true
- },
- {
- id: 'retail-react-app-hybrid-public-client-test-project',
- name: 'Retail React App Hybrid Test Public SLAS client project',
- description: '',
- templateSource: {
- type: TEMPLATE_SOURCE_NPM,
- id: '@salesforce/retail-react-app'
- },
- questions: [...EXTENSIBILITY_QUESTIONS, ...HYBRID_QUESTIONS, ...RETAIL_REACT_APP_QUESTIONS],
- answers: {
- ['project.extend']: true,
- ['project.hybrid']: true,
- ['project.name']: 'retail-react-app',
- ['project.commerce.instanceUrl']: 'https://www.phased-launch-testing.com/',
- ['project.commerce.clientId']: 'e7e22b7f-a904-4f3a-8022-49dbee696485',
- ['project.commerce.siteId']: 'RefArch',
- ['project.commerce.organizationId']: 'f_ecom_bjnl_prd',
- ['project.commerce.shortCode']: 'performance-001',
- ['project.einstein.clientId']: '1ea06c6e-c936-4324-bcf0-fada93f83bb1',
- ['project.einstein.siteId']: 'aaij-MobileFirst',
- ['project.commerce.isSlasPrivate']: false,
- ['project.dataCloud.appSourceId']: 'f22ae831-ac03-4bf6-afc1-3a0b19f1ea8e',
- ['project.dataCloud.tenantId']: 'mmydmztgh04dczjzmnsw0zd0g8.pc-rnd',
- ['project.demo.enableDemoSettings']: false
- },
- assets: ['translations'],
- private: true
- },
- {
- id: 'typescript-minimal-test-project',
- name: 'Template Minimal Test Project',
- description: '',
- templateSource: {
- type: TEMPLATE_SOURCE_BUNDLE,
- id: 'typescript-minimal'
- },
- private: true
- },
- {
- id: 'typescript-minimal',
- name: 'Template Minimal Project',
- description: `
- Generate a project using a bare-bones TypeScript app template.
-
- Use this as a TypeScript starting point or as a base on top of
- which to build new TypeScript project templates for Managed Runtime.
- `,
- templateSource: {
- type: TEMPLATE_SOURCE_BUNDLE,
- id: 'typescript-minimal'
- },
- questions: TYPESCRIPT_MINIMAL_QUESTIONS,
- private: true
- },
- {
- id: 'express-minimal-test-project',
- name: 'Express Minimal Test Project',
- description: '',
- templateSource: {
- type: TEMPLATE_SOURCE_BUNDLE,
- id: 'express-minimal'
- },
- questions: EXPRESS_MINIMAL_QUESTIONS,
- answers: {
- ['project.name']: 'express-minimal'
- },
- private: true
- },
- {
- id: 'express-minimal',
- name: 'Express Minimal Project',
- description: `
- Generate a project using a bare-bones express app template.
-
- Use this as a starting point for APIs or as a base on top of
- which to build new project templates for Managed Runtime.
- `,
- templateSource: {
- type: TEMPLATE_SOURCE_BUNDLE,
- id: 'express-minimal'
- },
- questions: EXPRESS_MINIMAL_QUESTIONS,
- private: true
- },
- {
- id: 'mrt-reference-app',
- name: 'Managed Runtime Reference App',
- description: '',
- templateSource: {
- type: TEMPLATE_SOURCE_BUNDLE,
- id: 'mrt-reference-app'
- },
- questions: MRT_REFERENCE_QUESTIONS,
- answers: {
- ['project.name']: 'mrt-reference-app'
- },
- private: true
- }
-]
-
-const PRESET_QUESTIONS = [
- {
- name: 'general.presetId',
- message: 'Choose a project preset to get started:',
- type: 'list',
- choices: PRESETS.filter(({private}) => !private).map(({shortDescription, id}) => ({
- name: shortDescription,
- value: id
- }))
- }
-]
const BOOTSTRAP_DIR = p.join(__dirname, '..', 'assets', 'bootstrap', 'js')
-
const ASSETS_TEMPLATES_DIR = p.join(__dirname, '..', 'assets', 'templates')
-
const PRIVATE_PRESET_NAMES = PRESETS.filter(({private}) => !!private).map(({id}) => id)
-
const PUBLIC_PRESET_NAMES = PRESETS.filter(({private}) => !private).map(({id}) => id)
-
const ALL_PRESET_NAMES = PRIVATE_PRESET_NAMES.concat(PUBLIC_PRESET_NAMES)
-
const PROJECT_ID_MAX_LENGTH = 20
// Utilities
@@ -728,8 +298,8 @@ const processTemplate = (relFile, inputDir, outputDir, context) => {
* @param {*} param2
*/
const runGenerator = (context, {outputDir, templateVersion, verbose}) => {
- const {answers, preset} = context
- const {templateSource} = preset
+ const {answers, template} = context
+ const {id, source} = template
const {extend = false} = answers.project
// Check if the output directory doesn't already exist.
@@ -739,13 +309,12 @@ const runGenerator = (context, {outputDir, templateVersion, verbose}) => {
// downloading from NPM or copying from the template bundle folder.
const tmp = fs.mkdtempSync(p.resolve(os.tmpdir(), 'extract-template'))
const packagePath = p.join(tmp, 'package')
- const {id, type} = templateSource
let tarPath
- switch (type) {
+ switch (source.type) {
case TEMPLATE_SOURCE_NPM: {
const tarFile = sh
- .exec(`npm pack ${id}@${templateVersion} --pack-destination="${tmp}"`, {
+ .exec(`npm pack ${source.name}@${templateVersion} --pack-destination="${tmp}"`, {
silent: true
})
.stdout.trim()
@@ -753,10 +322,10 @@ const runGenerator = (context, {outputDir, templateVersion, verbose}) => {
break
}
case TEMPLATE_SOURCE_BUNDLE:
- tarPath = p.join(__dirname, '..', 'templates', `${id}.tar.gz`)
+ tarPath = p.join(__dirname, '..', 'templates', `${source?.name || id}.tar.gz`)
break
default: {
- const msg = `Error: Cannot handle template source type ${type}.`
+ const msg = `Error: Cannot handle template source type ${source.type}.`
console.error(msg)
process.exit(1)
}
@@ -777,12 +346,13 @@ const runGenerator = (context, {outputDir, templateVersion, verbose}) => {
processTemplate(relFilePath, BOOTSTRAP_DIR, outputDir, context)
)
- // Copy required assets defind on the preset level.
- const {assets = []} = preset
+ // Copy required assets defined on the preset level.
+ const {assets = []} = template
assets.forEach((asset) => {
sh.cp('-rf', p.join(packagePath, asset), outputDir)
})
} else {
+ console.log('Copying base template from package or npm: ', packagePath, outputDir)
// Copy the base template either from the package or npm.
sh.cp('-rf', packagePath, outputDir)
@@ -802,7 +372,7 @@ const runGenerator = (context, {outputDir, templateVersion, verbose}) => {
const pkgJsonPath = p.resolve(outputDir, 'package.json')
const pkgJSON = readJson(pkgJsonPath)
const finalPkgData = merge(pkgJSON, {
- name: slugifyName(context.answers.project.name || context.preset.id),
+ name: slugifyName(context.answers.project.name || context.template.id),
version: GENERATED_PROJECT_VERSION
})
writeJson(pkgJsonPath, finalPkgData)
@@ -819,6 +389,82 @@ const foundNode = process.versions.node
const requiredNode = generatorPkg.engines.node
const isUsingCompatibleNode = semver.satisfies(foundNode, new semver.Range(requiredNode))
+/**
+ * Reads all data from standard input (stdin) asynchronously and resolves with the complete input as a string.
+ * Useful for accepting piped or redirected input, such as JSON answers for non-interactive CLI usage.
+ *
+ * @returns {Promise} A promise that resolves with the full stdin input as a string.
+ */
+const readStdin = async () => {
+ return new Promise((resolve, reject) => {
+ let input = ''
+ process.stdin.setEncoding('utf8')
+
+ process.stdin.on('data', (chunk) => {
+ input += chunk
+ })
+
+ process.stdin.on('end', () => {
+ resolve(input)
+ })
+
+ process.stdin.on('error', (err) => {
+ reject(err)
+ })
+ })
+}
+
+/**
+ * Validates the parsed answers object for required fields and structure.
+ * Currently only checks for 'general.presetOrTemplateId', but can be extended for more robust validation.
+ * Throws an error if validation fails.
+ *
+ * @param {Object} answers - The parsed answers object from stdin.
+ */
+const validateAnswers = (answers) => {
+ if (!answers['general.presetOrTemplateId']) {
+ throw new Error('Missing required field: "general.presetOrTemplateId"')
+ }
+
+ // Future enhancement: Add validation for template specific answers.
+}
+
+/**
+ * Reads and parses JSON input from stdin for non-interactive CLI usage.
+ * Exits the process with an error message if input is invalid or missing required fields.
+ *
+ * @returns {Promise