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

Nw hackdays37 extension workflow registry #5087

Draft
wants to merge 8 commits into
base: mathiusj/create-extensions-grouped
Choose a base branch
from
Draft
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
1 change: 0 additions & 1 deletion packages/app/src/cli/prompts/generate/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@
let extensionTemplates = options.extensionTemplates
let templateType = options.templateType
const extensionFlavor = options.extensionFlavor
console.log('EXTENSION TEMPLATES', extensionTemplates)
if (!templateType) {
if (extensionFlavor) {
extensionTemplates = extensionTemplates.filter((template) =>
Expand All @@ -101,7 +100,7 @@
// 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 103 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#L103

[@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,
Expand All @@ -109,7 +108,7 @@
// extensionChildren [identifiers]
extensionChildren: extensionTemplate.relatedExtensions?.map((elem) => elem.name) ?? [],
}
// TODO: here perhaps return a flag, that lets us know that there are possible next steps

Check failure on line 111 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#L111

[no-warning-comments] Unexpected 'todo' comment: 'TODO: here perhaps return a flag, that...'.

return {extensionTemplate, extensionContent}
}
Expand Down Expand Up @@ -157,4 +156,4 @@
}

export default generateExtensionPrompts
export {promptAddExtensionConfirmation}

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

'promptAddExtensionConfirmation' is an unused export
75 changes: 24 additions & 51 deletions packages/app/src/cli/services/generate.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
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, {
GenerateExtensionPromptOptions,
GenerateExtensionPromptOutput,
promptAddExtensionConfirmation,
} from '../prompts/generate/extension.js'
import metadata from '../metadata.js'
import {
Expand All @@ -24,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 @@ -40,62 +40,35 @@
async function generate(options: GenerateOptions) {
const {app, developerPlatformClient, remoteApp, specifications, template} = options

// console.log('GENERATE', options)

const availableSpecifications = specifications.map((spec) => spec.identifier)
const extensionTemplates = await fetchExtensionTemplates(developerPlatformClient, remoteApp, availableSpecifications)

const promptOptions = await buildPromptOptions(extensionTemplates, specifications, app, options)
const promptAnswers = await generateExtensionPrompts(promptOptions)

// Add related extension children for product discounts
if (promptAnswers.extensionTemplate.identifier === 'product_discounts') {
const settingsTemplate = extensionTemplates.find((template) => template.identifier === 'discount_function_settings')
if (settingsTemplate) {
const mockSettingsPromptAnswers: GenerateExtensionPromptOutput = {
extensionTemplate: settingsTemplate,
extensionContent: {
name: `${promptAnswers.extensionContent.name}-settings`,
flavor: 'react',
relatedExtensions: [],
},
}

// Generate the settings extension
const settingsGenerateOptions = buildGenerateOptions(
mockSettingsPromptAnswers,
app,
options,
developerPlatformClient,
)
await generateExtensionTemplate(settingsGenerateOptions)
}
}

console.log('PROMPT ANSWERS', promptAnswers)
// Call module related to that child extension
await saveAnalyticsMetadata(promptAnswers, template)
// Here we could check if the user wants to add a related extension
const addPromptConfirmation = await promptAddExtensionConfirmation()

// We can add an extra generated extension here.
const generateExtensionOptions = buildGenerateOptions(promptAnswers, app, options, developerPlatformClient)
const generatedExtension = await generateExtensionTemplate(generateExtensionOptions)

// 1. Generate ui extension
// 2. We check the identifier
// 3. If the identifier has related extensions then the team who owns can add logic to add related extensions
// 4. This allows new prompts
// 4.1. Generate function settings
// 4.2. Function settings is meant to be used with a discount function, would you like us to generate one for you?
// 4.3. What is the name of your function?
// Generates both extensions
if (addPromptConfirmation) {
// Generate related extensions
const generateExtensionOptions = buildGenerateOptions(promptAnswers, app, options, developerPlatformClient)
const generatedExtension = await generateExtensionTemplate(generateExtensionOptions)
const workflow = workflowRegistry[generatedExtension.extensionTemplate.identifier]
if (!workflow) {
renderSuccessMessage(generatedExtension, app.packageManager)
}

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

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?'.

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

[spaced-comment] Expected space or tab after '//' in comment.
}

renderSuccessMessage(generatedExtension, app.packageManager)
renderSuccessMessage(generatedExtension, app.packageManager, workflowResult)
}

async function buildPromptOptions(
Expand All @@ -104,7 +77,6 @@
app: AppInterface,
options: GenerateOptions,
): Promise<GenerateExtensionPromptOptions> {
// console.log('BUILDING PROMPT OPTIONS', arguments)
const extensionTemplate = await handleTypeParameter(options.template, app, extensionTemplates, specifications)
validateExtensionFlavor(extensionTemplate, options.flavor)

Expand Down Expand Up @@ -136,16 +108,14 @@
}

function limitReached(app: AppInterface, specifications: ExtensionSpecification[], template: ExtensionTemplate) {
// console.log('CHECKING LIMITS', specifications)
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) {
const {extensionContent} = promptAnswers
console.log('SAVING METADATA', promptAnswers)
return metadata.addPublicMetadata(() => ({
cmd_scaffold_template_flavor: extensionContent.flavor,
cmd_scaffold_type: promptAnswers.extensionTemplate.identifier,
Expand All @@ -168,11 +138,12 @@
}
}

function renderSuccessMessage(extension: GeneratedExtension, packageManager: AppInterface['packageManager']) {
function renderSuccessMessage(extension: GeneratedExtension, packageManager: AppInterface['packageManager'], workflowResult?: WorkflowResult) {

Check failure on line 141 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#L141

[prettier/prettier] Replace `extension:·GeneratedExtension,·packageManager:·AppInterface['packageManager'],·workflowResult?:·WorkflowResult` with `⏎··extension:·GeneratedExtension,⏎··packageManager:·AppInterface['packageManager'],⏎··workflowResult?:·WorkflowResult,⏎`
const formattedSuccessfulMessage = formatSuccessfulRunMessage(
extension.extensionTemplate,
extension.directory,
packageManager,
workflowResult,
)
renderSuccess(formattedSuccessfulMessage)
}
Expand All @@ -194,11 +165,13 @@
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 || ['Your extension was created in', {filePath: extensionDirectory}, {char: '.'}],

Check warning on line 172 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#L172

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

Check failure on line 172 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#L172

[prettier/prettier] Replace `'Your·extension·was·created·in',·{filePath:·extensionDirectory},·{char:·'.'}` with `⏎······'Your·extension·was·created·in',⏎······{filePath:·extensionDirectory},⏎······{char:·'.'},⏎····`
nextSteps: workflowMessage?.nextSteps || [],

Check warning on line 173 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#L173

[@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 174 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#L174

[@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
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {Workflow} from './registry.js'
import {joinPath} from '@shopify/cli-kit/node/path'
import {renderTextPrompt} from '@shopify/cli-kit/node/ui'
import {patchConfigurationFile} from './patch-configuration-file.js'

Check failure on line 4 in packages/app/src/cli/services/generate/workflows/editor-extension-collection.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app/src/cli/services/generate/workflows/editor-extension-collection.ts#L4

[import/order] `./patch-configuration-file.js` import should occur before import of `@shopify/cli-kit/node/path`

export const editorExtensionCollection: Workflow = {
afterGenerate: async (options) => {
const existingExtensions = options.generateOptions.app.extensionsForType({
identifier: 'ui_extension',
externalIdentifier: 'ui_extension',
})
const availableExtensions = existingExtensions.map((extension) => extension.handle).join(' ')
const extensions = await renderTextPrompt({
message: `The extension handles to include in the collection, comma separated. Options: ${availableExtensions}`,
})
await patchConfigurationFile({
path: joinPath(options.generatedExtension.directory, 'shopify.extension.toml'),
patch: {
extensions: [{

Check failure on line 19 in packages/app/src/cli/services/generate/workflows/editor-extension-collection.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app/src/cli/services/generate/workflows/editor-extension-collection.ts#L19

[prettier/prettier] Insert `⏎··········`
includes: extensions.split(',').map((handle) => handle.trim())

Check failure on line 20 in packages/app/src/cli/services/generate/workflows/editor-extension-collection.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app/src/cli/services/generate/workflows/editor-extension-collection.ts#L20

[prettier/prettier] Replace `··········includes:·extensions.split(',').map((handle)·=>·handle.trim())` with `············includes:·extensions.split(',').map((handle)·=>·handle.trim()),`
}],

Check failure on line 21 in packages/app/src/cli/services/generate/workflows/editor-extension-collection.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app/src/cli/services/generate/workflows/editor-extension-collection.ts#L21

[prettier/prettier] Replace `········}` with `··········},⏎········`
},
})
return {
success: true,
}
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {deepMergeObjects} from '@shopify/cli-kit/common/object'
import {readFile, writeFile} from '@shopify/cli-kit/node/fs'
import {zod} from '@shopify/cli-kit/node/schema'
import {decodeToml, encodeToml} from '@shopify/cli-kit/node/toml'

export interface PatchTomlOptions {

Check failure on line 6 in packages/app/src/cli/services/generate/workflows/patch-configuration-file.ts

View workflow job for this annotation

GitHub Actions / knip-reporter-annotations-check

packages/app/src/cli/services/generate/workflows/patch-configuration-file.ts#L6

'PatchTomlOptions' is an unused type
path: string
patch: {[key: string]: unknown}
schema?: zod.AnyZodObject
}

function mergeArrayStrategy(existingArray: unknown[], newArray: unknown[]): unknown[] {
if (
existingArray.length > 0 &&
existingArray[0] &&
typeof existingArray[0] === 'object' &&
newArray[0] &&
typeof newArray[0] === 'object'
) {
return [{...(existingArray[0] as object), ...(newArray[0] as object)}]

Check failure on line 20 in packages/app/src/cli/services/generate/workflows/patch-configuration-file.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app/src/cli/services/generate/workflows/patch-configuration-file.ts#L20

[@typescript-eslint/no-unnecessary-type-assertion] This assertion is unnecessary since it does not change the type of the expression.

Check failure on line 20 in packages/app/src/cli/services/generate/workflows/patch-configuration-file.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app/src/cli/services/generate/workflows/patch-configuration-file.ts#L20

[@typescript-eslint/no-unnecessary-type-assertion] This assertion is unnecessary since it does not change the type of the expression.
}
return newArray
}

export async function patchConfigurationFile({path, patch}: PatchTomlOptions) {
const tomlContents = await readFile(path)
const configuration = decodeToml(tomlContents)

// Deep merge with custom array strategy
const updatedConfig = deepMergeObjects(configuration, patch, mergeArrayStrategy)

const encodedString = encodeToml(updatedConfig)
await writeFile(path, encodedString)
}
27 changes: 27 additions & 0 deletions packages/app/src/cli/services/generate/workflows/registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {editorExtensionCollection} from './editor-extension-collection.js'
import {GenerateOptions} from '../../generate.js'
import {GeneratedExtension, GenerateExtensionTemplateOptions} from '../../generate/extension.js'
import { RenderAlertOptions } from '@shopify/cli-kit/node/ui'

Check failure on line 4 in packages/app/src/cli/services/generate/workflows/registry.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/app/src/cli/services/generate/workflows/registry.ts#L4

[prettier/prettier] Replace `·RenderAlertOptions·` with `RenderAlertOptions`

interface AfterGenerateOptions {
generateOptions: GenerateOptions
extensionTemplateOptions: GenerateExtensionTemplateOptions
generatedExtension: GeneratedExtension
}

export interface WorkflowResult {
success: boolean
message?: RenderAlertOptions
}

export interface Workflow {
afterGenerate: (options: AfterGenerateOptions) => Promise<WorkflowResult>
}

interface WorkflowRegistry {
[key: string]: Workflow
}

export const workflowRegistry: WorkflowRegistry = {
editor_extension_collection: editorExtensionCollection,
}
Loading