Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/core/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -414,5 +414,6 @@
"AWS.toolkit.lambda.walkthrough.step1.title": "Iterate locally",
"AWS.toolkit.lambda.walkthrough.step1.description": "Locally test and debug your code.",
"AWS.toolkit.lambda.walkthrough.step2.title": "Deploy to the cloud",
"AWS.toolkit.lambda.walkthrough.step2.description": "Test your application in the cloud from within VS Code. \n\nNote: The AWS CLI and the SAM CLI require AWS Credentials to interact with the cloud. For information on setting up your credentials, see [Authentication and access credentials](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html). \n\n[Configure credentials](command:aws.toolkit.lambda.walkthrough.credential)"
"AWS.toolkit.lambda.walkthrough.step2.description": "Test your application in the cloud from within VS Code. \n\nNote: The AWS CLI and the SAM CLI require AWS Credentials to interact with the cloud. For information on setting up your credentials, see [Authentication and access credentials](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html). \n\n[Configure credentials](command:aws.toolkit.lambda.walkthrough.credential)",
"AWS.toolkit.lambda.serverlessLand.quickpickTitle": "Create Lambda Application from template"
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@
</checkbox>
</div>
</checklist>
<checkbox class="theme-picker-link" when-checked="command:aws.lambda.createNewSamApp" checked-on="false">
<checkbox class="theme-picker-link" when-checked="command:aws.toolkit.lambda.serverlessLand" checked-on="false">
See more application example...
</checkbox>
5 changes: 4 additions & 1 deletion packages/core/src/awsService/appBuilder/activation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { ResourceNode } from './explorer/nodes/resourceNode'
import { getSyncWizard, runSync } from '../../shared/sam/sync'
import { getDeployWizard, runDeploy } from '../../shared/sam/deploy'
import { DeployTypeWizard } from './wizards/deployTypeWizard'

import { createNewServerlessLandProject } from './serverlessLand/serverlessLandMain'
export const templateToOpenAppComposer = 'aws.toolkit.appComposer.templateToOpenOnStart'

/**
Expand Down Expand Up @@ -200,6 +200,9 @@ async function registerAppBuilderCommands(context: ExtContext): Promise<void> {
await runSync('infra', arg, undefined, choices.syncParam)
}
}
}),
Commands.register({ id: 'aws.toolkit.lambda.serverlessLand', autoconnect: false }, async () => {
await createNewServerlessLandProject(context)
})
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import * as nls from 'vscode-nls'
const localize = nls.loadMessageBundle()
import * as path from 'path'
import { getTelemetryReason, getTelemetryResult } from '../../../shared/errors'
import { RegionProvider } from '../../../shared/regions/regionProvider'
import { getLogger } from '../../../shared/logger/logger'
import globals from '../../../shared/extensionGlobals'
import { checklogs } from '../../../shared/localizedText'
import { Result, telemetry } from '../../../shared/telemetry/telemetry'
import { CreateServerlessLandWizard } from './serverlessLandWizard'
import { ExtContext } from '../../../shared/extensions'
import { addFolderToWorkspace } from '../../../shared/utilities/workspaceUtils'

export const readmeFile: string = 'README.md'

/**
* Creates a new Serverless Land project using the provided extension context
* @param extContext Extension context containing AWS credentials and region information
* @returns Promise that resolves when the project creation is complete
*
* This function:
* 1. Validates AWS credentials and regions
* 2. Launches the Serverless Land project creation wizard
* 3. Creates the project structure
* 4. Adds the project folder to the workspace
* 5. Opens the README.md file if available
* 6. Handles errors and emits telemetry
*/
export async function createNewServerlessLandProject(extContext: ExtContext): Promise<void> {
const awsContext = extContext.awsContext
const regionProvider: RegionProvider = extContext.regionProvider
let createResult: Result = 'Succeeded'
let reason: string | undefined

try {
const credentials = await awsContext.getCredentials()
const schemaRegions = regionProvider
.getRegions()
.filter((r) => regionProvider.isServiceInRegion('schemas', r.id))
const defaultRegion = awsContext.getCredentialDefaultRegion()

// Launch the project creation wizard
const config = await new CreateServerlessLandWizard({
credentials,
schemaRegions,
defaultRegion,
}).run()
if (!config) {
createResult = 'Cancelled'
reason = 'userCancelled'
return
}

// Add the project folder to the workspace
await addFolderToWorkspace(
{
uri: config.location,
name: path.basename(config.location.fsPath),
},
true
)
} catch (err) {
createResult = getTelemetryResult(err)
reason = getTelemetryReason(err)

globals.outputChannel.show(true)
getLogger().error(
localize(
'AWS.serverlessland.initWizard.general.error',
'Error creating new SAM Application. {0}',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine we'll change this message later to not say "SAM"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'll handle error and metrics together for the whole project separately

checklogs()
)
)
getLogger().error('Error creating new Serverless Land Application: %O', err as Error)
} finally {
// add telemetry
// TODO: Will add telemetry once the implementation gets completed
telemetry.sam_init.emit({
result: createResult,
reason: reason,
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
import * as AWS from '@aws-sdk/types'
import * as vscode from 'vscode'
import { Wizard } from '../../../shared/wizards/wizard'
import * as path from 'path'
import { createInputBox } from '../../../shared/ui/inputPrompter'
import { createCommonButtons } from '../../../shared/ui/buttons'
import { createQuickPick } from '../../../shared/ui/pickerPrompter'
import { createFolderPrompt } from '../../../shared/ui/common/location'
import { Region } from '../../../shared/regions/endpoints'
import { createExitPrompter } from '../../../shared/ui/common/exitPrompter'
import { MetadataManager } from './serverlesslandMetadataManager'
import { ToolkitError } from '../../../shared/errors'

export interface CreateServerlessLandWizardForm {
name: string
location: vscode.Uri
pattern: string
runtime: string
iac: string
region?: string
schemaName?: string
}

/**
* Wizard for creating Serverless Land applications
* Guides users through the project creation process
*/
export class CreateServerlessLandWizard extends Wizard<CreateServerlessLandWizardForm> {
private metadataManager: MetadataManager

public constructor(context: { schemaRegions: Region[]; defaultRegion?: string; credentials?: AWS.Credentials }) {
super({
exitPrompterProvider: createExitPrompter,
})
this.metadataManager = MetadataManager.getInstance()
}

public override async run(): Promise<CreateServerlessLandWizardForm | undefined> {
try {
// Load metadata from JSON file
const projectRoot = path.resolve(__dirname, '../../../../../')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the best way to get the root? I'd rather have an absolute path rather than this relative way

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path.resolve() function does return an absolute file path.

Previously, when executing the below line of code, an additional /dist directory was being prepended to the path before the src directory, resulting in a metadata not found error.

const metadataPath = path.resolve(__dirname, './serverlessland-metadata.json')

const metadataPath = path.join(
projectRoot,
'src',
'awsService',
'appBuilder',
'serverlessLand',
'serverlessland-metadata.json'
)
await this.metadataManager.loadMetadata(metadataPath)

// Initialize and display pattern selection
const patterns = this.metadataManager.getPatterns()
if (patterns.length === 0) {
throw new Error('No patterns found in metadata')
}

const patternPicker = createQuickPick<string>(
patterns.map((p) => ({
label: p.label,
detail: p.description,
data: p.label,
buttons: [
{
iconPath: new vscode.ThemeIcon('github'),
tooltip: 'GitHub Button',
},
{
iconPath: new vscode.ThemeIcon('open-preview'),
tooltip: 'Serverless Land Button',
},
],
})),
{
title: 'Select a Pattern for your application',
placeholder: 'Choose a pattern for your project',
buttons: createCommonButtons(),
matchOnDescription: true,
matchOnDetail: true,
}
)

const patternResult = await patternPicker.prompt()
if (!patternResult || typeof patternResult !== 'string') {
return undefined // User cancelled or invalid result
}
const selectedPattern = patternResult

// Show runtime options based on selected pattern
const runtimes = this.metadataManager.getRuntimes(selectedPattern)
if (runtimes.length === 0) {
throw new Error('No runtimes found for the selected pattern')
}

const runtimePicker = createQuickPick<string>(
runtimes.map((r) => ({
label: r.label,
data: r.label,
})),
{
title: 'Select Runtime',
placeholder: 'Choose a runtime for your project',
buttons: createCommonButtons(),
}
)
const runtimeResult = await runtimePicker.prompt()
if (!runtimeResult || typeof runtimeResult !== 'string') {
return undefined // User cancelled or invalid result
}
const selectedRuntime = runtimeResult

// Show IAC options based on selected pattern
const iacOptions = this.metadataManager.getIacOptions(selectedPattern)
if (iacOptions.length === 0) {
throw new Error('No IAC options found for the selected pattern')
}

const iacPicker = createQuickPick<string>(
iacOptions.map((i) => ({
label: i.label,
data: i.label,
})),
{
title: 'Select IaC',
placeholder: 'Choose an IaC option for your project',
buttons: createCommonButtons(),
}
)
const iacResult = await iacPicker.prompt()
if (!iacResult || typeof iacResult !== 'string') {
return undefined // User cancelled or invalid result
}
const selectedIac = iacResult

// Create and show location picker
const locationPicker = createFolderPrompt(vscode.workspace.workspaceFolders ?? [], {
title: 'Select Project Location',
buttons: createCommonButtons(),
browseFolderDetail: 'Select a folder for your project',
})

const selectedLocation = await locationPicker.prompt()
if (!selectedLocation || !(selectedLocation instanceof vscode.Uri)) {
return undefined // User cancelled or invalid result
}

// Create and show project name input
const nameInput = createInputBox({
title: 'Enter Project Name',
placeholder: 'Enter a name for your new application',
buttons: createCommonButtons(),
validateInput: (value: string): string | undefined => {
if (!value) {
return 'Application name cannot be empty'
}
if (value.includes(path.sep)) {
return `The path separator (${path.sep}) is not allowed in application names`
}
return undefined
},
})

const projectName = await nameInput.prompt()
if (!projectName || typeof projectName !== 'string') {
return undefined // User cancelled
}

// Return the form with all collected values
return {
name: projectName,
location: selectedLocation,
pattern: selectedPattern,
runtime: selectedRuntime,
iac: selectedIac,
}
} catch (err) {
throw new ToolkitError(`Failed to run wizard: ${err instanceof Error ? err.message : String(err)}`)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"patterns": {
"s3-lambda-resizing-sam": {
"name": "Resizing image",
"description": "Lambda, S3 • Python, Javascript, Java, .NET • SAM",
"runtimes": [
{
"id": "python",
"name": "Python"
},
{
"id": "javascript",
"name": "Javascript"
},
{
"id": "dotnet",
"name": "Dotnet"
},
{
"id": "java",
"name": "Java"
}
],
"iac": [
{
"id": "sam",
"name": "SAM"
}
]
},
"apigw-rest-api-lambda-sam": {
"name": "Rest API",
"description": "Lambda, API Gateway • Python, Javascript, Java, .NET • SAM",
"runtimes": [
{
"id": "python",
"name": "Python"
},
{
"id": "javascript",
"name": "Javascript"
},
{
"id": "dotnet",
"name": "Dotnet"
},
{
"id": "java",
"name": "Java"
}
],
"iac": [
{
"id": "sam",
"name": "AWS SAM"
}
]
}
}
}
Loading
Loading