diff --git a/COMMANDS.md b/COMMANDS.md new file mode 100644 index 0000000..449a485 --- /dev/null +++ b/COMMANDS.md @@ -0,0 +1,83 @@ +# Commands + + + +- [`sf webapp dev`](#sf-webapp-dev) +- [`sf webapp generate`](#sf-webapp-generate) + +## `sf webapp dev` + +Preview a web app locally without needing to deploy + +``` +USAGE + $ sf webapp dev -n [--json] [--flags-dir ] [-t ] [-p ] + +FLAGS + -n, --name= (required) Identifies the Web Application + -p, --port= [default: 5173] Port for the dev server + -t, --target= Selects which Web Application target to use for the preview (e.g., Lightning App, Site) + +GLOBAL FLAGS + --flags-dir= Import flag values from a directory. + --json Format output as json. + +DESCRIPTION + Preview a web app locally without needing to deploy + + Starts a local development server for a Web Application, using the local project files. This enables rapid + development with hot reloading and immediate feedback. + +EXAMPLES + Start the development server: + + $ sf webapp dev --name myWebApp + + Start the development server with a specific target: + + $ sf webapp dev --name myWebApp --target "LightningApp" + + Start the development server on a custom port: + + $ sf webapp dev --name myWebApp --port 8080 +``` + +## `sf webapp generate` + +Create a web app and associated metadata. + +``` +USAGE + $ sf webapp generate -n -l [--json] [--flags-dir ] [-t ] [-w] + +FLAGS + -l, --label= (required) Human readable name of your web app + -n, --name= (required) Name of your web app + -t, --template= [default: empty] Template to use for web app generation (pulls from central solution) + -w, --wizard Run in interactive wizard mode + +GLOBAL FLAGS + --flags-dir= Import flag values from a directory. + --json Format output as json. + +DESCRIPTION + Create a web app and associated metadata. + + This command creates a new web app with the specified configuration, including the basic structure and metadata + files. + +EXAMPLES + Create an empty web app: + + $ sf webapp generate --name "myWebApp" --label "My first Web App" + + Create a web app with a specific template: + + $ sf webapp generate --name "myWebApp" --label "My Web App" --template "React app starter" + + Create a web app using the wizard: + + $ sf webapp generate --name "myWebApp" --label "My Web App" --wizard +``` + + diff --git a/command-snapshot.json b/command-snapshot.json index 6dec3eb..24fb6ab 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -1,10 +1,18 @@ [ { "alias": [], - "command": "hello:world", + "command": "webapp:dev", "flagAliases": [], - "flagChars": ["n"], - "flags": ["flags-dir", "json", "name"], + "flagChars": ["n", "p", "t"], + "flags": ["flags-dir", "json", "name", "port", "target"], + "plugin": "@salesforce/plugin-webapp" + }, + { + "alias": [], + "command": "webapp:generate", + "flagAliases": [], + "flagChars": ["l", "n", "t", "w"], + "flags": ["flags-dir", "json", "label", "name", "template", "wizard"], "plugin": "@salesforce/plugin-webapp" } ] diff --git a/messages/hello.world.md b/messages/hello.world.md deleted file mode 100644 index 804f848..0000000 --- a/messages/hello.world.md +++ /dev/null @@ -1,29 +0,0 @@ -# summary - -Say hello. - -# description - -Say hello either to the world or someone you know. - -# flags.name.summary - -The name of the person you'd like to say hello to. - -# flags.name.description - -This person can be anyone in the world! - -# examples - -- Say hello to the world: - - <%= config.bin %> <%= command.id %> - -- Say hello to someone you know: - - <%= config.bin %> <%= command.id %> --name Astro - -# info.hello - -Hello %s at %s. diff --git a/messages/webapp.dev.md b/messages/webapp.dev.md new file mode 100644 index 0000000..e8f55ce --- /dev/null +++ b/messages/webapp.dev.md @@ -0,0 +1,33 @@ +# summary + +Preview a web app locally without needing to deploy + +# description + +Starts a local development server for a Web Application, using the local project files. This enables rapid development with hot reloading and immediate feedback. + +# flags.name.summary + +Identifies the Web Application + +# flags.target.summary + +Selects which Web Application target to use for the preview (e.g., Lightning App, Site) + +# flags.port.summary + +Port for the dev server + +# examples + +- Start the development server: + + <%= config.bin %> <%= command.id %> --name myWebApp + +- Start the development server with a specific target: + + <%= config.bin %> <%= command.id %> --name myWebApp --target "LightningApp" + +- Start the development server on a custom port: + + <%= config.bin %> <%= command.id %> --name myWebApp --port 8080 diff --git a/messages/webapp.generate.md b/messages/webapp.generate.md new file mode 100644 index 0000000..8aaa4db --- /dev/null +++ b/messages/webapp.generate.md @@ -0,0 +1,37 @@ +# summary + +Create a web app and associated metadata. + +# description + +This command creates a new web app with the specified configuration, including the basic structure and metadata files. + +# flags.name.summary + +Name of your web app + +# flags.label.summary + +Human readable name of your web app + +# flags.template.summary + +Template to use for web app generation (pulls from central solution) + +# flags.wizard.summary + +Run in interactive wizard mode + +# examples + +- Create an empty web app: + + <%= config.bin %> <%= command.id %> --name "myWebApp" --label "My first Web App" + +- Create a web app with a specific template: + + <%= config.bin %> <%= command.id %> --name "myWebApp" --label "My Web App" --template "React app starter" + +- Create a web app using the wizard: + + <%= config.bin %> <%= command.id %> --name "myWebApp" --label "My Web App" --wizard diff --git a/package.json b/package.json index 94dc5f6..e9861cd 100644 --- a/package.json +++ b/package.json @@ -52,8 +52,8 @@ "@salesforce/plugin-command-reference" ], "topics": { - "hello": { - "description": "Commands to say hello." + "webapp": { + "description": "Work with Salesforce Web Apps" } }, "flexibleTaxonomy": true diff --git a/schemas/hello-world.json b/schemas/webapp-dev.json similarity index 60% rename from schemas/hello-world.json rename to schemas/webapp-dev.json index d28ac19..88d6027 100644 --- a/schemas/hello-world.json +++ b/schemas/webapp-dev.json @@ -1,18 +1,21 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/HelloWorldResult", + "$ref": "#/definitions/WebappDevResult", "definitions": { - "HelloWorldResult": { + "WebappDevResult": { "type": "object", "properties": { "name": { "type": "string" }, - "time": { + "target": { + "type": "string" + }, + "url": { "type": "string" } }, - "required": ["name", "time"], + "required": ["name", "url"], "additionalProperties": false } } diff --git a/schemas/webapp-generate.json b/schemas/webapp-generate.json new file mode 100644 index 0000000..1a008e2 --- /dev/null +++ b/schemas/webapp-generate.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/WebappGenerateResult", + "definitions": { + "WebappGenerateResult": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "label": { + "type": "string" + }, + "template": { + "type": "string" + }, + "wizard": { + "type": "boolean" + } + }, + "required": ["name", "label", "template", "wizard"], + "additionalProperties": false + } + } +} diff --git a/src/commands/hello/world.ts b/src/commands/webapp/dev.ts similarity index 54% rename from src/commands/hello/world.ts rename to src/commands/webapp/dev.ts index 4cfa97b..d236885 100644 --- a/src/commands/hello/world.ts +++ b/src/commands/webapp/dev.ts @@ -18,34 +18,60 @@ import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; import { Messages } from '@salesforce/core'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); -const messages = Messages.loadMessages('@salesforce/plugin-webapp', 'hello.world'); +const messages = Messages.loadMessages('@salesforce/plugin-webapp', 'webapp.dev'); -export type HelloWorldResult = { +export type WebappDevResult = { name: string; - time: string; + target?: string; + url: string; }; -export default class World extends SfCommand { +export default class WebappDev extends SfCommand { public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); public static readonly examples = messages.getMessages('examples'); public static readonly flags = { name: Flags.string({ - char: 'n', summary: messages.getMessage('flags.name.summary'), - description: messages.getMessage('flags.name.description'), - default: 'World', + char: 'n', + required: true, + }), + target: Flags.string({ + summary: messages.getMessage('flags.target.summary'), + char: 't', + required: false, + }), + port: Flags.integer({ + summary: messages.getMessage('flags.port.summary'), + char: 'p', + default: 5173, }), }; - public async run(): Promise { - const { flags } = await this.parse(World); - const time = new Date().toDateString(); - this.log(messages.getMessage('info.hello', [flags.name, time])); + public async run(): Promise { + const { flags } = await this.parse(WebappDev); + + this.log(`Starting development server for web app: ${flags.name}`); + if (flags.target) { + this.log(`Using target: ${flags.target}`); + } + + const url = `http://localhost:${flags.port}`; + this.log(`Server running on ${url}`); + this.log('Opening browser...'); + + // TODO: Implement dev server logic + // This would typically involve: + // 1. Starting a local development server + // 2. Watching for file changes + // 3. Hot reloading + // 4. Opening browser + return { name: flags.name, - time, + target: flags.target, + url, }; } } diff --git a/src/commands/webapp/generate.ts b/src/commands/webapp/generate.ts new file mode 100644 index 0000000..e0801c2 --- /dev/null +++ b/src/commands/webapp/generate.ts @@ -0,0 +1,83 @@ +/* + * Copyright 2025, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; +import { Messages } from '@salesforce/core'; + +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +const messages = Messages.loadMessages('@salesforce/plugin-webapp', 'webapp.generate'); + +export type WebappGenerateResult = { + name: string; + label: string; + template: string; + wizard: boolean; +}; + +export default class WebappGenerate extends SfCommand { + public static readonly summary = messages.getMessage('summary'); + public static readonly description = messages.getMessage('description'); + public static readonly examples = messages.getMessages('examples'); + + public static readonly flags = { + name: Flags.string({ + summary: messages.getMessage('flags.name.summary'), + char: 'n', + required: true, + }), + label: Flags.string({ + summary: messages.getMessage('flags.label.summary'), + char: 'l', + required: true, + }), + template: Flags.string({ + summary: messages.getMessage('flags.template.summary'), + char: 't', + default: 'empty', + }), + wizard: Flags.boolean({ + summary: messages.getMessage('flags.wizard.summary'), + char: 'w', + default: false, + }), + }; + + public async run(): Promise { + const { flags } = await this.parse(WebappGenerate); + + this.log('Generating your web app, give us a moment...'); + this.log(`Name: ${flags.name}`); + this.log(`Label: ${flags.label}`); + this.log(`Template: ${flags.template}`); + this.log(`Wizard mode: ${flags.wizard}`); + + // TODO: Implement web app generation logic + // This would typically involve: + // 1. Creating webapp.json configuration + // 2. Setting up SFDX project structure + // 3. Generating metadata files + // 4. Creating necessary bundle structure + + this.log('Your Web App has been created, have fun!'); + + return { + name: flags.name, + label: flags.label, + template: flags.template ?? 'empty', + wizard: flags.wizard, + }; + } +} diff --git a/test/commands/webapp/dev.nut.ts b/test/commands/webapp/dev.nut.ts new file mode 100644 index 0000000..bbf1da6 --- /dev/null +++ b/test/commands/webapp/dev.nut.ts @@ -0,0 +1,55 @@ +/* + * Copyright 2025, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; +import { expect } from 'chai'; + +describe('webapp dev NUTs', () => { + let session: TestSession; + + before(async () => { + session = await TestSession.create({ devhubAuthStrategy: 'NONE' }); + }); + + after(async () => { + await session?.clean(); + }); + + it('should run webapp dev with required flags', () => { + const result = execCmd<{ name: string; url: string }>('webapp dev --name myApp --json', { ensureExitCode: 0 }) + .jsonOutput?.result; + expect(result?.name).to.equal('myApp'); + expect(result?.url).to.equal('http://localhost:5173'); + }); + + it('should run webapp dev with target flag', () => { + const result = execCmd<{ name: string; target?: string; url: string }>( + 'webapp dev --name myApp --target LightningApp --json', + { ensureExitCode: 0 } + ).jsonOutput?.result; + expect(result?.name).to.equal('myApp'); + expect(result?.target).to.equal('LightningApp'); + expect(result?.url).to.equal('http://localhost:5173'); + }); + + it('should run webapp dev with custom port', () => { + const result = execCmd<{ name: string; url: string }>('webapp dev --name myApp --port 8080 --json', { + ensureExitCode: 0, + }).jsonOutput?.result; + expect(result?.name).to.equal('myApp'); + expect(result?.url).to.equal('http://localhost:8080'); + }); +}); diff --git a/test/commands/hello/world.test.ts b/test/commands/webapp/dev.test.ts similarity index 51% rename from test/commands/hello/world.test.ts rename to test/commands/webapp/dev.test.ts index fa5c824..84eaaac 100644 --- a/test/commands/hello/world.test.ts +++ b/test/commands/webapp/dev.test.ts @@ -16,9 +16,9 @@ import { TestContext } from '@salesforce/core/testSetup'; import { expect } from 'chai'; import { stubSfCommandUx } from '@salesforce/sf-plugins-core'; -import World from '../../../src/commands/hello/world.js'; +import WebappDev from '../../../src/commands/webapp/dev.js'; -describe('hello world', () => { +describe('webapp dev', () => { const $$ = new TestContext(); let sfCommandStubs: ReturnType; @@ -30,31 +30,32 @@ describe('hello world', () => { $$.restore(); }); - it('runs hello world', async () => { - await World.run([]); - const output = sfCommandStubs.log - .getCalls() - .flatMap((c) => c.args) - .join('\n'); - expect(output).to.include('Hello World'); + it('runs webapp dev with required flags', async () => { + const result = await WebappDev.run(['--name', 'myApp']); + expect(result.name).to.equal('myApp'); + expect(result.url).to.equal('http://localhost:5173'); }); - it('runs hello world with --json and no provided name', async () => { - const result = await World.run([]); - expect(result.name).to.equal('World'); + it('runs webapp dev with target flag', async () => { + const result = await WebappDev.run(['--name', 'myApp', '--target', 'LightningApp']); + expect(result.name).to.equal('myApp'); + expect(result.target).to.equal('LightningApp'); + expect(result.url).to.equal('http://localhost:5173'); }); - it('runs hello world --name Astro', async () => { - await World.run(['--name', 'Astro']); + it('runs webapp dev with custom port', async () => { + const result = await WebappDev.run(['--name', 'myApp', '--port', '8080']); + expect(result.name).to.equal('myApp'); + expect(result.url).to.equal('http://localhost:8080'); + }); + + it('outputs dev server messages', async () => { + await WebappDev.run(['--name', 'myApp']); const output = sfCommandStubs.log .getCalls() .flatMap((c) => c.args) .join('\n'); - expect(output).to.include('Hello Astro'); - }); - - it('runs hello world --name Astro --json', async () => { - const result = await World.run(['--name', 'Astro', '--json']); - expect(result.name).to.equal('Astro'); + expect(output).to.include('Starting development server'); + expect(output).to.include('Server running on'); }); }); diff --git a/test/commands/hello/world.nut.ts b/test/commands/webapp/generate.nut.ts similarity index 51% rename from test/commands/hello/world.nut.ts rename to test/commands/webapp/generate.nut.ts index 61ac34d..e639f74 100644 --- a/test/commands/hello/world.nut.ts +++ b/test/commands/webapp/generate.nut.ts @@ -13,31 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; import { expect } from 'chai'; -import { HelloWorldResult } from '../../../src/commands/hello/world.js'; -let testSession: TestSession; +describe('webapp generate NUTs', () => { + let session: TestSession; -describe('hello world NUTs', () => { - before('prepare session', async () => { - testSession = await TestSession.create(); + before(async () => { + session = await TestSession.create({ devhubAuthStrategy: 'NONE' }); }); after(async () => { - await testSession?.clean(); - }); - - it('should say hello to the world', () => { - const result = execCmd('hello world --json', { ensureExitCode: 0 }).jsonOutput?.result; - expect(result?.name).to.equal('World'); + await session?.clean(); }); - it('should say hello to a given person', () => { - const result = execCmd('hello world --name Astro --json', { - ensureExitCode: 0, - }).jsonOutput?.result; - expect(result?.name).to.equal('Astro'); + it('should display provided name', () => { + const name = 'TestApp'; + const label = 'Test Application'; + const command = `webapp generate --name ${name} --label "${label}"`; + const output = execCmd(command, { ensureExitCode: 0 }).shellOutput.stdout; + expect(output).to.contain(name); + expect(output).to.contain(label); }); }); diff --git a/test/commands/webapp/generate.test.ts b/test/commands/webapp/generate.test.ts new file mode 100644 index 0000000..835a8ef --- /dev/null +++ b/test/commands/webapp/generate.test.ts @@ -0,0 +1,70 @@ +/* + * Copyright 2025, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { TestContext } from '@salesforce/core/testSetup'; +import { expect } from 'chai'; +import { stubSfCommandUx } from '@salesforce/sf-plugins-core'; +import WebappGenerate from '../../../src/commands/webapp/generate.js'; + +describe('webapp generate', () => { + const $$ = new TestContext(); + let sfCommandStubs: ReturnType; + + beforeEach(() => { + sfCommandStubs = stubSfCommandUx($$.SANDBOX); + }); + + afterEach(() => { + $$.restore(); + }); + + it('generates webapp with required flags', async () => { + const result = await WebappGenerate.run(['--name', 'myWebApp', '--label', 'My Web App']); + expect(result.name).to.equal('myWebApp'); + expect(result.label).to.equal('My Web App'); + expect(result.template).to.equal('empty'); + expect(result.wizard).to.be.false; + }); + + it('generates webapp with template', async () => { + const result = await WebappGenerate.run([ + '--name', + 'testApp', + '--label', + 'Test App', + '--template', + 'React app starter', + ]); + expect(result.name).to.equal('testApp'); + expect(result.label).to.equal('Test App'); + expect(result.template).to.equal('React app starter'); + }); + + it('generates webapp with wizard mode', async () => { + const result = await WebappGenerate.run(['--name', 'wizardApp', '--label', 'Wizard App', '--wizard']); + expect(result.name).to.equal('wizardApp'); + expect(result.wizard).to.be.true; + }); + + it('outputs generation messages', async () => { + await WebappGenerate.run(['--name', 'myApp', '--label', 'My App']); + const output = sfCommandStubs.log + .getCalls() + .flatMap((c) => c.args) + .join('\n'); + expect(output).to.include('Generating your web app'); + expect(output).to.include('Your Web App has been created'); + }); +});