Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve workflows for extensions that have related extensions #5108

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions packages/app/src/cli/commands/app/generate/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import {Args, Flags} from '@oclif/core'
import {globalFlags} from '@shopify/cli-kit/node/cli'
import {renderWarning} from '@shopify/cli-kit/node/ui'
import { workflowFlags} from '../../../services/generate/workflows/registry.js'

Check failure on line 11 in packages/app/src/cli/commands/app/generate/extension.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app/src/cli/commands/app/generate/extension.ts#L11

[import/order] `../../../services/generate/workflows/registry.js` import should occur before import of `@oclif/core`

Check failure on line 11 in packages/app/src/cli/commands/app/generate/extension.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app/src/cli/commands/app/generate/extension.ts#L11

[prettier/prettier] Delete `·`

export default class AppGenerateExtension extends AppCommand {
static summary = 'Generate a new app Extension.'
Expand All @@ -23,6 +24,7 @@
static flags = {
...globalFlags,
...appFlags,
...workflowFlags(),
type: Flags.string({
char: 't',
hidden: false,
Expand Down
24 changes: 24 additions & 0 deletions packages/app/src/cli/models/app/app.test-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1084,6 +1084,13 @@ export const testRemoteExtensionTemplates: ExtensionTemplate[] = [
path: 'discounts/rust/product-discounts/default',
},
],
relatedExtensions: [
{
type: 'ui_extension',
name: 'discount_function_settings',
directory: 'discount_ui/discount_ui',
},
],
},
{
identifier: 'order_discounts',
Expand Down Expand Up @@ -1112,6 +1119,23 @@ export const testRemoteExtensionTemplates: ExtensionTemplate[] = [
},
],
},
{
identifier: 'discount_function_settings',
name: 'Function - Discount settings',
defaultName: 'discount-function-settings',
group: 'Discounts and checkout',
supportLinks: [],
type: 'function',
url: 'https://github.com/Shopify/function-examples',
extensionPoints: [],
supportedFlavors: [
{
name: 'JavaScript',
value: 'vanilla-js',
path: 'discounts/javascript/discount-function-settings/default',
},
],
},
productSubscriptionUIExtensionTemplate,
themeAppExtensionTemplate,
]
Expand Down
5 changes: 5 additions & 0 deletions packages/app/src/cli/models/app/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,9 @@ export interface ExtensionTemplate {
extensionPoints: string[]
supportedFlavors: ExtensionFlavor[]
url: string
relatedExtensions?: {
type: string
name: string
directory: string
}[]
}
42 changes: 33 additions & 9 deletions packages/app/src/cli/prompts/generate/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
import {ExtensionFlavorValue} from '../../services/generate/extension.js'
import {ExtensionTemplate} from '../../models/app/template.js'
import {fileExistsSync} from '@shopify/cli-kit/node/fs'
import {renderAutocompletePrompt, renderSelectPrompt, renderTextPrompt} from '@shopify/cli-kit/node/ui'
import {
renderAutocompletePrompt,
renderConfirmationPrompt,
renderSelectPrompt,
renderTextPrompt,
} from '@shopify/cli-kit/node/ui'
import {AbortError} from '@shopify/cli-kit/node/error'
import {joinPath} from '@shopify/cli-kit/node/path'
import {slugify} from '@shopify/cli-kit/common/string'
Expand All @@ -26,6 +31,11 @@
export interface GenerateExtensionContentOutput {
name: string
flavor?: ExtensionFlavorValue
relatedExtensions?: {
type: string
name: string
directory: string
}[]
}

export function buildChoices(extensionTemplates: ExtensionTemplate[], unavailableExtensions: ExtensionTemplate[] = []) {
Expand Down Expand Up @@ -63,13 +73,12 @@
return templateSpecChoices.sort(compareChoices)
}

const generateExtensionPrompts = async (
export const generateExtensionPrompts = async (

Check failure on line 76 in packages/app/src/cli/prompts/generate/extension.ts

View workflow job for this annotation

GitHub Actions / knip-reporter-annotations-check

packages/app/src/cli/prompts/generate/extension.ts#L76

'generateExtensionPrompts' is a duplicate of 'default'
options: GenerateExtensionPromptOptions,
): Promise<GenerateExtensionPromptOutput> => {
let extensionTemplates = options.extensionTemplates
let templateType = options.templateType
const extensionFlavor = options.extensionFlavor

if (!templateType) {
if (extensionFlavor) {
extensionTemplates = extensionTemplates.filter((template) =>
Expand All @@ -81,23 +90,37 @@
throw new AbortError('You have reached the limit for the number of extensions you can create.')
}

// eslint-disable-next-line require-atomic-updates
templateType = await renderAutocompletePrompt({
message: 'Type of extension?',
choices: buildChoices(extensionTemplates, options.unavailableExtensions),
})
if (extensionTemplates.length === 1) {
templateType = extensionTemplates[0]?.identifier
} else {
// eslint-disable-next-line require-atomic-updates
templateType = await renderAutocompletePrompt({
message: 'Type of extension?',
choices: buildChoices(extensionTemplates, options.unavailableExtensions),
})
}
}

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const extensionTemplate = extensionTemplates.find((template) => template.identifier === templateType)!

const name = options.name || (await promptName(options.directory, extensionTemplate.defaultName))

Check warning on line 107 in packages/app/src/cli/prompts/generate/extension.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app/src/cli/prompts/generate/extension.ts#L107

[@typescript-eslint/prefer-nullish-coalescing] Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.
const flavor = options.extensionFlavor ?? (await promptFlavor(extensionTemplate))
const extensionContent = {name, flavor}
const extensionContent = {
name,
flavor,
}

return {extensionTemplate, extensionContent}
}

const promptAddExtensionConfirmation = (): Promise<boolean> => {
return renderConfirmationPrompt({
message: 'Would you like to create the function extension?',
defaultValue: true,
})
}

async function promptName(directory: string, defaultName: string, number = 1): Promise<string> {
const separator = defaultName.includes(' ') ? ' ' : '-'
const name = number <= 1 ? defaultName : `${defaultName}${separator}${number}`
Expand Down Expand Up @@ -133,4 +156,5 @@
})
}

export default generateExtensionPrompts

Check failure on line 159 in packages/app/src/cli/prompts/generate/extension.ts

View workflow job for this annotation

GitHub Actions / knip-reporter-annotations-check

packages/app/src/cli/prompts/generate/extension.ts#L159

'default' is a duplicate of 'generateExtensionPrompts'
export {promptAddExtensionConfirmation}

Check failure on line 160 in packages/app/src/cli/prompts/generate/extension.ts

View workflow job for this annotation

GitHub Actions / knip-reporter-annotations-check

packages/app/src/cli/prompts/generate/extension.ts#L160

'promptAddExtensionConfirmation' is an unused export
67 changes: 66 additions & 1 deletion packages/app/src/cli/services/generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ describe('generate', () => {
│ • To preview this extension along with the rest of the project, run │
│ \`yarn shopify app dev\` │
│ │
╰─────────────────────────────────────────────────────────────────────────────╯
╰─────────────────────────────────────���────────────────────────────────────────╯
"
`)
})
Expand Down Expand Up @@ -207,6 +207,71 @@ describe('generate', () => {
// Then
await expect(got).rejects.toThrow(/Invalid template for extension type/)
})

test.only('creates related extensions in a shared directory with common configuration', async () => {
const outputInfo = await mockSuccessfulCommandExecution('product_discounts')

const productDiscountsTemplate = testRemoteExtensionTemplates.find(
(spec) => spec.identifier === 'product_discounts',
)
if (!productDiscountsTemplate) throw new Error('Product discounts template not found')

vi.mocked(generateExtensionPrompts).mockResolvedValue({
extensionTemplate: productDiscountsTemplate,
extensionContent: {
name: 'product_discounts',
flavor: 'vanilla-js',
relatedExtensions: [
{
type: 'function',
name: 'product_discounts',
directory: 'product_discounts_function/product_discounts_function',
},
{
type: 'ui_extension',
name: 'discount_function_settings',
directory: 'discount_ui/discount_ui',
},
],
},
})

// When
await generate({
directory: '/',
reset: false,
app,
remoteApp,
specifications,
developerPlatformClient,
})

// Then
expect(generateExtensionTemplate).toHaveBeenCalledTimes(2)

// Verify first extension (UI)
expect(generateExtensionTemplate).toHaveBeenCalledWith(
expect.objectContaining({
extensionContent: expect.objectContaining({
name: 'discount_function_settings',
type: 'ui_extension',
}),
}),
)

// Verify second extension (function)
expect(generateExtensionTemplate).toHaveBeenCalledWith(
expect.objectContaining({
extensionContent: expect.objectContaining({
name: 'product_discounts',
type: 'function',
}),
}),
)

// Verify shared configuration file was created
expect(outputInfo.info()).toContain('extensions/google-maps-validation')
})
})

async function mockSuccessfulCommandExecution(identifier: string, existingExtensions: ExtensionInstance[] = []) {
Expand Down
46 changes: 38 additions & 8 deletions packages/app/src/cli/services/generate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {fetchExtensionTemplates} from './generate/fetch-template-specifications.js'
import {workflowRegistry, WorkflowResult} from './generate/workflows/registry.js'
import {DeveloperPlatformClient} from '../utilities/developer-platform-client.js'
import {AppInterface, AppLinkedInterface} from '../models/app/app.js'
import generateExtensionPrompts, {
Expand All @@ -23,7 +24,7 @@
import {formatPackageManagerCommand} from '@shopify/cli-kit/node/output'
import {groupBy} from '@shopify/cli-kit/common/collection'

interface GenerateOptions {
export interface GenerateOptions {
app: AppLinkedInterface
specifications: RemoteAwareExtensionSpecification[]
remoteApp: OrganizationApp
Expand All @@ -50,10 +51,27 @@
const generateExtensionOptions = buildGenerateOptions(promptAnswers, app, options, developerPlatformClient)
const generatedExtension = await generateExtensionTemplate(generateExtensionOptions)

renderSuccessMessage(generatedExtension, app.packageManager)
const workflow = workflowRegistry[generatedExtension.extensionTemplate.identifier]
if (!workflow) {
renderSuccessMessage(generatedExtension, app.packageManager)
return
}

const workflowResult = await workflow?.afterGenerate({
generateOptions: options,
generatedExtension,
extensionTemplateOptions: generateExtensionOptions,
extensionTemplates,
})

if (!workflowResult?.success) {
// TODO: Cleanup extension?

Check failure on line 68 in packages/app/src/cli/services/generate.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app/src/cli/services/generate.ts#L68

[no-warning-comments] Unexpected 'todo' comment: 'TODO: Cleanup extension?'.
}

renderSuccessMessage(generatedExtension, app.packageManager, workflowResult)
}

async function buildPromptOptions(
export async function buildPromptOptions(
extensionTemplates: ExtensionTemplate[],
specifications: ExtensionSpecification[],
app: AppInterface,
Expand Down Expand Up @@ -85,6 +103,7 @@
const allValid = !limitReached(app, specifications, template)
return allValid ? 'validTemplates' : 'templatesOverlimit'
}

return groupBy(extensionTemplates, iterateeFunction)
}

Expand All @@ -92,7 +111,7 @@
const type = template.type
const specification = specifications.find((spec) => spec.identifier === type || spec.externalIdentifier === type)
const existingExtensions = app.extensionsForType({identifier: type, externalIdentifier: type})
return existingExtensions.length >= (specification?.registrationLimit || 1)

Check warning on line 114 in packages/app/src/cli/services/generate.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app/src/cli/services/generate.ts#L114

[@typescript-eslint/prefer-nullish-coalescing] Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.
}

async function saveAnalyticsMetadata(promptAnswers: GenerateExtensionPromptOutput, typeFlag: string | undefined) {
Expand All @@ -104,7 +123,7 @@
}))
}

function buildGenerateOptions(
export function buildGenerateOptions(
promptAnswers: GenerateExtensionPromptOutput,
app: AppInterface,
options: GenerateOptions,
Expand All @@ -119,11 +138,16 @@
}
}

function renderSuccessMessage(extension: GeneratedExtension, packageManager: AppInterface['packageManager']) {
function renderSuccessMessage(
extension: GeneratedExtension,
packageManager: AppInterface['packageManager'],
workflowResult?: WorkflowResult,
) {
const formattedSuccessfulMessage = formatSuccessfulRunMessage(
extension.extensionTemplate,
extension.directory,
packageManager,
workflowResult,
)
renderSuccess(formattedSuccessfulMessage)
}
Expand All @@ -145,11 +169,17 @@
extensionTemplate: ExtensionTemplate,
extensionDirectory: string,
depndencyManager: PackageManager,
workflowResult?: WorkflowResult,
): RenderAlertOptions {
const workflowMessage = workflowResult?.message
const options: RenderAlertOptions = {
headline: ['Your extension was created in', {filePath: extensionDirectory}, {char: '.'}],
nextSteps: [],
reference: [],
headline: workflowMessage?.headline || [

Check warning on line 176 in packages/app/src/cli/services/generate.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app/src/cli/services/generate.ts#L176

[@typescript-eslint/prefer-nullish-coalescing] Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.
'Your extension was created in',
{filePath: extensionDirectory},
{char: '.'},
],
nextSteps: workflowMessage?.nextSteps || [],

Check warning on line 181 in packages/app/src/cli/services/generate.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app/src/cli/services/generate.ts#L181

[@typescript-eslint/prefer-nullish-coalescing] Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.
reference: workflowMessage?.reference || [],

Check warning on line 182 in packages/app/src/cli/services/generate.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app/src/cli/services/generate.ts#L182

[@typescript-eslint/prefer-nullish-coalescing] Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.
}

if (extensionTemplate.type !== 'function') {
Expand Down
13 changes: 10 additions & 3 deletions packages/app/src/cli/services/generate/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
export interface GeneratedExtension {
directory: string
extensionTemplate: ExtensionTemplate
handle: string
}

interface ExtensionInitOptions {
Expand All @@ -72,7 +73,7 @@
uid: string | undefined
onGetTemplateRepository: (url: string, destination: string) => Promise<void>
}

// This might become plural?
export async function generateExtensionTemplate(
options: GenerateExtensionTemplateOptions,
): Promise<GeneratedExtension> {
Expand All @@ -82,7 +83,7 @@
(flavor) => flavor.value === extensionFlavorValue,
)
const directory = await ensureExtensionDirectoryExists({app: options.app, name: extensionName})
const url = options.cloneUrl || options.extensionTemplate.url

Check warning on line 86 in packages/app/src/cli/services/generate/extension.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app/src/cli/services/generate/extension.ts#L86

[@typescript-eslint/prefer-nullish-coalescing] Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.
const uid = options.developerPlatformClient.supportsAtomicDeployments ? randomUUID() : undefined
const initOptions: ExtensionInitOptions = {
directory,
Expand All @@ -99,7 +100,11 @@
}),
}
await extensionInit(initOptions)
return {directory: relativizePath(directory), extensionTemplate: options.extensionTemplate}
return {
directory: relativizePath(directory),
extensionTemplate: options.extensionTemplate,
handle: slugify(extensionName),
}
}

async function extensionInit(options: ExtensionInitOptions) {
Expand Down Expand Up @@ -152,6 +157,7 @@
uid,
onGetTemplateRepository,
}: ExtensionInitOptions) {
// Here we'd want to write the ui extension handle in the function toml
const templateLanguage = getTemplateLanguage(extensionFlavor?.value)
const taskList = []

Expand All @@ -168,13 +174,15 @@
await recursiveLiquidTemplateCopy(templateDirectory, directory, {
name,
handle: slugify(name),
// TODO: This is where we'd want to write the ui extension handle in the function toml

Check failure on line 177 in packages/app/src/cli/services/generate/extension.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app/src/cli/services/generate/extension.ts#L177

[no-warning-comments] Unexpected 'todo' comment: 'TODO: This is where we'd want to write...'.
uiExtensionHandle: slugify(name),
flavor: extensionFlavor?.value,
uid,
})
})

if (templateLanguage === 'javascript') {
const srcFileExtension = getSrcFileExtension(extensionFlavor?.value || 'rust')

Check warning on line 185 in packages/app/src/cli/services/generate/extension.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app/src/cli/services/generate/extension.ts#L185

[@typescript-eslint/prefer-nullish-coalescing] Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.
await changeIndexFileExtension(directory, srcFileExtension, '!(*.graphql)')
}
},
Expand Down Expand Up @@ -221,7 +229,6 @@
onGetTemplateRepository,
}: ExtensionInitOptions) {
const templateLanguage = getTemplateLanguage(extensionFlavor?.value)

const tasks = [
{
title: `Generating extension`,
Expand Down
Loading
Loading