From 9e3572cb07eb9e96f87ca46374ba15be78a32c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isaac=20Rold=C3=A1n?= Date: Wed, 9 Oct 2024 12:09:38 +0200 Subject: [PATCH 1/5] Use app-context in function commands --- .../src/cli/commands/app/function/build.ts | 3 +- .../src/cli/commands/app/function/replay.ts | 4 +- .../app/src/cli/commands/app/function/run.ts | 4 +- .../src/cli/commands/app/function/schema.ts | 5 +- .../src/cli/commands/app/function/typegen.ts | 2 +- .../app/src/cli/models/app/app.test-data.ts | 20 ++- packages/app/src/cli/models/app/app.ts | 9 +- .../app/src/cli/services/app-context.test.ts | 22 ++- packages/app/src/cli/services/app-context.ts | 21 +-- .../src/cli/services/function/common.test.ts | 32 ++-- .../app/src/cli/services/function/common.ts | 31 ++-- .../src/cli/services/function/replay.test.ts | 27 ++-- .../app/src/cli/services/function/replay.ts | 8 +- .../src/cli/services/generate-schema.test.ts | 153 +----------------- .../app/src/cli/services/generate-schema.ts | 37 +---- packages/app/src/cli/utilities/app-command.ts | 10 +- .../rules/required-fields-when-loading-app.js | 2 +- 17 files changed, 138 insertions(+), 252 deletions(-) diff --git a/packages/app/src/cli/commands/app/function/build.ts b/packages/app/src/cli/commands/app/function/build.ts index e90f2a1ce8..d720443b62 100644 --- a/packages/app/src/cli/commands/app/function/build.ts +++ b/packages/app/src/cli/commands/app/function/build.ts @@ -20,10 +20,11 @@ export default class FunctionBuild extends AppCommand { public async run(): Promise { const {flags} = await this.parse(FunctionBuild) + const app = await inFunctionContext({ path: flags.path, userProvidedConfigName: flags.config, - callback: async (app, ourFunction) => { + callback: async (app, _, ourFunction) => { await buildFunctionExtension(ourFunction, { app, stdout: process.stdout, diff --git a/packages/app/src/cli/commands/app/function/replay.ts b/packages/app/src/cli/commands/app/function/replay.ts index 5fb1441fac..8352d9a71a 100644 --- a/packages/app/src/cli/commands/app/function/replay.ts +++ b/packages/app/src/cli/commands/app/function/replay.ts @@ -60,12 +60,12 @@ export default class FunctionReplay extends AppCommand { const app = await inFunctionContext({ path: flags.path, + apiKey, userProvidedConfigName: flags.config, - callback: async (app, ourFunction) => { + callback: async (app, _, ourFunction) => { await replay({ app, extension: ourFunction, - apiKey, path: flags.path, log: flags.log, json: flags.json, diff --git a/packages/app/src/cli/commands/app/function/run.ts b/packages/app/src/cli/commands/app/function/run.ts index 43fd3829b6..2c8d352f4d 100644 --- a/packages/app/src/cli/commands/app/function/run.ts +++ b/packages/app/src/cli/commands/app/function/run.ts @@ -44,7 +44,7 @@ export default class FunctionRun extends AppCommand { const app = await inFunctionContext({ path: flags.path, userProvidedConfigName: flags.config, - callback: async (app, ourFunction) => { + callback: async (app, developerPlatformClient, ourFunction) => { let functionExport = DEFAULT_FUNCTION_EXPORT if (flags.export !== undefined) { @@ -80,7 +80,7 @@ export default class FunctionRun extends AppCommand { const inputQueryPath = ourFunction?.configuration.targeting?.[0]?.input_query const queryPath = inputQueryPath && `${ourFunction?.directory}/${inputQueryPath}` - const schemaPath = await getOrGenerateSchemaPath(ourFunction, app) + const schemaPath = await getOrGenerateSchemaPath(ourFunction, app, developerPlatformClient) await runFunction({ functionExtension: ourFunction, diff --git a/packages/app/src/cli/commands/app/function/schema.ts b/packages/app/src/cli/commands/app/function/schema.ts index dc9424a83a..3b6ab78639 100644 --- a/packages/app/src/cli/commands/app/function/schema.ts +++ b/packages/app/src/cli/commands/app/function/schema.ts @@ -49,12 +49,13 @@ export default class FetchSchema extends AppCommand { const app = await inFunctionContext({ path: flags.path, + apiKey, userProvidedConfigName: flags.config, - callback: async (app, ourFunction) => { + callback: async (app, developerPlatformClient, ourFunction) => { await generateSchemaService({ app, extension: ourFunction, - apiKey, + developerPlatformClient, stdout: flags.stdout, path: flags.path, }) diff --git a/packages/app/src/cli/commands/app/function/typegen.ts b/packages/app/src/cli/commands/app/function/typegen.ts index 0bdef4f74d..db99a7b78e 100644 --- a/packages/app/src/cli/commands/app/function/typegen.ts +++ b/packages/app/src/cli/commands/app/function/typegen.ts @@ -23,7 +23,7 @@ export default class FunctionTypegen extends AppCommand { const app = await inFunctionContext({ path: flags.path, userProvidedConfigName: flags.config, - callback: async (app, ourFunction) => { + callback: async (app, _, ourFunction) => { await buildGraphqlTypes(ourFunction, {stdout: process.stdout, stderr: process.stderr, app}) renderSuccess({headline: 'GraphQL types generated successfully.'}) return app diff --git a/packages/app/src/cli/models/app/app.test-data.ts b/packages/app/src/cli/models/app/app.test-data.ts index 8ee2f63184..0629873125 100644 --- a/packages/app/src/cli/models/app/app.test-data.ts +++ b/packages/app/src/cli/models/app/app.test-data.ts @@ -4,6 +4,7 @@ import { AppConfigurationSchema, AppConfigurationWithoutPath, AppInterface, + AppLinkedInterface, CurrentAppConfiguration, LegacyAppConfiguration, WebType, @@ -122,6 +123,10 @@ export function testApp(app: Partial = {}, schemaType: 'current' | return newApp } +export function testAppLinked(app: Partial = {}): AppLinkedInterface { + return testApp(app, 'current') as AppLinkedInterface +} + interface TestAppWithConfigOptions { app?: Partial config: object @@ -569,7 +574,7 @@ export function testOrganizationStore({shopId, shopDomain}: {shopId?: string; sh } } -const testRemoteSpecifications: RemoteSpecification[] = [ +export const testRemoteSpecifications: RemoteSpecification[] = [ { name: 'Checkout Post Purchase', externalName: 'Post-purchase UI', @@ -1058,6 +1063,18 @@ const appVersionsDiffResponse: AppVersionsDiffSchema = { }, } +const functionUploadUrlResponse = { + functionUploadUrlGenerate: { + generatedUrlDetails: { + headers: {}, + maxSize: '200 kb', + url: 'https://example.com/upload-url', + moduleId: 'module-id', + maxBytes: 200, + }, + }, +} + export const extensionCreateResponse: ExtensionCreateSchema = { extensionCreate: { extensionRegistration: { @@ -1223,6 +1240,7 @@ export function testDeveloperPlatformClient(stubs: Partial Promise.resolve(emptyActiveAppVersion), appVersionByTag: (_input: AppVersionByTagVariables) => Promise.resolve(appVersionByTagResponse), appVersionsDiff: (_input: AppVersionsDiffVariables) => Promise.resolve(appVersionsDiffResponse), + functionUploadUrl: () => Promise.resolve(functionUploadUrlResponse), createExtension: (_input: ExtensionCreateVariables) => Promise.resolve(extensionCreateResponse), updateExtension: (_input: ExtensionUpdateDraftMutationVariables) => Promise.resolve(extensionUpdateResponse), deploy: (_input: AppDeployVariables) => Promise.resolve(deployResponse), diff --git a/packages/app/src/cli/models/app/app.ts b/packages/app/src/cli/models/app/app.ts index e213759bb3..f1388606e7 100644 --- a/packages/app/src/cli/models/app/app.ts +++ b/packages/app/src/cli/models/app/app.ts @@ -3,7 +3,7 @@ import {ensurePathStartsWithSlash} from './validation/common.js' import {ExtensionInstance} from '../extensions/extension-instance.js' import {isType} from '../../utilities/types.js' import {FunctionConfigType} from '../extensions/specifications/function.js' -import {ExtensionSpecification} from '../extensions/specification.js' +import {ExtensionSpecification, RemoteAwareExtensionSpecification} from '../extensions/specification.js' import {AppConfigurationUsedByCli} from '../extensions/specifications/types/app_config.js' import {EditorExtensionCollectionType} from '../extensions/specifications/editor_extension_collection.js' import {UIExtensionSchema} from '../extensions/specifications/ui_extension.js' @@ -226,6 +226,8 @@ export interface AppConfigurationInterface< remoteFlags: Flag[] } +export type AppLinkedInterface = AppInterface + export interface AppInterface< TConfig extends AppConfiguration = AppConfiguration, TModuleSpec extends ExtensionSpecification = ExtensionSpecification, @@ -345,10 +347,7 @@ export class App< async manifest(): Promise { const modules = await Promise.all( this.realExtensions.map(async (module) => { - const config = await module.deployConfig({ - apiKey: String(this.configuration.client_id ?? ''), - appConfiguration: this.configuration, - }) + const config = await module.commonDeployConfig('', this.configuration) return { type: module.externalType, handle: module.handle, diff --git a/packages/app/src/cli/services/app-context.test.ts b/packages/app/src/cli/services/app-context.test.ts index 9ffda8f72d..ff412b3891 100644 --- a/packages/app/src/cli/services/app-context.test.ts +++ b/packages/app/src/cli/services/app-context.test.ts @@ -44,8 +44,9 @@ describe('linkedAppContext', () => { const result = await linkedAppContext({ directory: tmp, forceRelink: false, - configName: undefined, + userProvidedConfigName: undefined, clientId: undefined, + mode: 'report', }) // Then @@ -58,6 +59,7 @@ describe('linkedAppContext', () => { }), remoteApp: mockRemoteApp, developerPlatformClient: expect.any(Object), + specifications: [], }) expect(link).not.toHaveBeenCalled() }) @@ -91,8 +93,9 @@ describe('linkedAppContext', () => { const result = await linkedAppContext({ directory: tmp, forceRelink: false, - configName: undefined, + userProvidedConfigName: undefined, clientId: undefined, + mode: 'report', }) // Then @@ -100,6 +103,7 @@ describe('linkedAppContext', () => { app: expect.any(Object), remoteApp: mockRemoteApp, developerPlatformClient: expect.any(Object), + specifications: [], }) expect(link).toHaveBeenCalledWith({directory: tmp, apiKey: undefined, configName: undefined}) }) @@ -119,7 +123,13 @@ describe('linkedAppContext', () => { }) // When - await linkedAppContext({directory: tmp, forceRelink: false, configName: undefined, clientId: undefined}) + await linkedAppContext({ + directory: tmp, + forceRelink: false, + userProvidedConfigName: undefined, + clientId: undefined, + mode: 'report', + }) const result = localStorage.getCachedAppInfo(tmp) // Then @@ -148,7 +158,8 @@ describe('linkedAppContext', () => { directory: tmp, clientId: newClientId, forceRelink: false, - configName: undefined, + userProvidedConfigName: undefined, + mode: 'report', }) // Then @@ -187,8 +198,9 @@ describe('linkedAppContext', () => { await linkedAppContext({ directory: tmp, forceRelink: true, - configName: undefined, + userProvidedConfigName: undefined, clientId: undefined, + mode: 'report', }) // Then diff --git a/packages/app/src/cli/services/app-context.ts b/packages/app/src/cli/services/app-context.ts index 91628b0602..e4f0d5e716 100644 --- a/packages/app/src/cli/services/app-context.ts +++ b/packages/app/src/cli/services/app-context.ts @@ -2,16 +2,17 @@ import {appFromId} from './context.js' import {getCachedAppInfo, setCachedAppInfo} from './local-storage.js' import {fetchSpecifications} from './generate/fetch-extension-specifications.js' import link from './app/config/link.js' -import {AppInterface, CurrentAppConfiguration} from '../models/app/app.js' import {OrganizationApp} from '../models/organization.js' import {DeveloperPlatformClient, selectDeveloperPlatformClient} from '../utilities/developer-platform-client.js' -import {getAppConfigurationState, loadAppUsingConfigurationState} from '../models/app/loader.js' +import {AppLoaderMode, getAppConfigurationState, loadAppUsingConfigurationState} from '../models/app/loader.js' import {RemoteAwareExtensionSpecification} from '../models/extensions/specification.js' +import {AppLinkedInterface} from '../models/app/app.js' interface LoadedAppContextOutput { - app: AppInterface + app: AppLinkedInterface remoteApp: OrganizationApp developerPlatformClient: DeveloperPlatformClient + specifications: RemoteAwareExtensionSpecification[] } /** @@ -26,7 +27,8 @@ interface LoadedAppContextOptions { directory: string forceRelink: boolean clientId: string | undefined - configName: string | undefined + userProvidedConfigName: string | undefined + mode: AppLoaderMode } /** @@ -41,15 +43,16 @@ export async function linkedAppContext({ directory, clientId, forceRelink, - configName, + userProvidedConfigName, + mode, }: LoadedAppContextOptions): Promise { // Get current app configuration state - let configState = await getAppConfigurationState(directory, configName) + let configState = await getAppConfigurationState(directory, userProvidedConfigName) let remoteApp: OrganizationApp | undefined // If the app is not linked, force a link. if (configState.state === 'template-only' || forceRelink) { - const result = await link({directory, apiKey: clientId, configName}) + const result = await link({directory, apiKey: clientId, configName: userProvidedConfigName}) remoteApp = result.remoteApp configState = result.state } @@ -77,7 +80,7 @@ export async function linkedAppContext({ const localApp = await loadAppUsingConfigurationState(configState, { specifications, remoteFlags: remoteApp.flags, - mode: 'strict', + mode, }) // If the remoteApp is the same as the linked one, update the cached info. @@ -87,5 +90,5 @@ export async function linkedAppContext({ setCachedAppInfo({appId: remoteApp.apiKey, title: remoteApp.title, directory, orgId: remoteApp.organizationId}) } - return {app: localApp, remoteApp, developerPlatformClient} + return {app: localApp, remoteApp, developerPlatformClient, specifications} } diff --git a/packages/app/src/cli/services/function/common.test.ts b/packages/app/src/cli/services/function/common.test.ts index 7c97592e97..4376b24f05 100644 --- a/packages/app/src/cli/services/function/common.test.ts +++ b/packages/app/src/cli/services/function/common.test.ts @@ -1,17 +1,23 @@ import {getOrGenerateSchemaPath, inFunctionContext} from './common.js' -import {loadApp} from '../../models/app/loader.js' -import {testApp, testFunctionExtension} from '../../models/app/app.test-data.js' -import {AppInterface} from '../../models/app/app.js' +import { + testApp, + testDeveloperPlatformClient, + testFunctionExtension, + testOrganizationApp, +} from '../../models/app/app.test-data.js' +import {AppInterface, AppLinkedInterface} from '../../models/app/app.js' import {ExtensionInstance} from '../../models/extensions/extension-instance.js' import {FunctionConfigType} from '../../models/extensions/specifications/function.js' import {generateSchemaService} from '../generate-schema.js' +import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js' +import {linkedAppContext} from '../app-context.js' import {describe, vi, expect, beforeEach, test} from 'vitest' import {renderAutocompletePrompt, renderFatalError} from '@shopify/cli-kit/node/ui' import {joinPath} from '@shopify/cli-kit/node/path' import {isTerminalInteractive} from '@shopify/cli-kit/node/context/local' import {fileExists} from '@shopify/cli-kit/node/fs' -vi.mock('../../models/app/loader.js') +vi.mock('../app-context.js') vi.mock('@shopify/cli-kit/node/ui') vi.mock('@shopify/cli-kit/node/context/local') vi.mock('@shopify/cli-kit/node/fs') @@ -23,7 +29,12 @@ let ourFunction: ExtensionInstance beforeEach(async () => { ourFunction = await testFunctionExtension() app = testApp({allExtensions: [ourFunction]}) - vi.mocked(loadApp).mockResolvedValue(app) + vi.mocked(linkedAppContext).mockResolvedValue({ + app: app as AppLinkedInterface, + remoteApp: testOrganizationApp(), + developerPlatformClient: testDeveloperPlatformClient(), + specifications: [], + }) vi.mocked(renderFatalError).mockReturnValue('') vi.mocked(renderAutocompletePrompt).mockResolvedValue(ourFunction) vi.mocked(isTerminalInteractive).mockReturnValue(true) @@ -87,15 +98,16 @@ describe('ensure we are within a function context', () => { describe('getOrGenerateSchemaPath', () => { let extension: ExtensionInstance - let app: AppInterface - + let app: AppLinkedInterface + let developerPlatformClient: DeveloperPlatformClient beforeEach(() => { extension = { directory: '/path/to/function', configuration: {}, } as ExtensionInstance - app = {} as AppInterface + app = {} as AppLinkedInterface + developerPlatformClient = testDeveloperPlatformClient() }) test('returns the path if the schema file exists', async () => { @@ -104,7 +116,7 @@ describe('getOrGenerateSchemaPath', () => { vi.mocked(fileExists).mockResolvedValue(true) // When - const result = await getOrGenerateSchemaPath(extension, app) + const result = await getOrGenerateSchemaPath(extension, app, developerPlatformClient) // Then expect(result).toBe(expectedPath) @@ -119,7 +131,7 @@ describe('getOrGenerateSchemaPath', () => { vi.mocked(fileExists).mockResolvedValueOnce(true) // When - const result = await getOrGenerateSchemaPath(extension, app) + const result = await getOrGenerateSchemaPath(extension, app, developerPlatformClient) // Then expect(result).toBe(expectedPath) diff --git a/packages/app/src/cli/services/function/common.ts b/packages/app/src/cli/services/function/common.ts index 330c0bed19..d72de83d7f 100644 --- a/packages/app/src/cli/services/function/common.ts +++ b/packages/app/src/cli/services/function/common.ts @@ -1,9 +1,9 @@ -import {AppInterface} from '../../models/app/app.js' -import {loadApp} from '../../models/app/loader.js' -import {loadLocalExtensionsSpecifications} from '../../models/extensions/load-specifications.js' +import {AppLinkedInterface} from '../../models/app/app.js' import {ExtensionInstance} from '../../models/extensions/extension-instance.js' import {FunctionConfigType} from '../../models/extensions/specifications/function.js' import {generateSchemaService} from '../generate-schema.js' +import {linkedAppContext} from '../app-context.js' +import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js' import {resolvePath, cwd, joinPath} from '@shopify/cli-kit/node/path' import {AbortError} from '@shopify/cli-kit/node/error' import {Flags} from '@oclif/core' @@ -25,14 +25,25 @@ export const functionFlags = { export async function inFunctionContext({ path, userProvidedConfigName, + apiKey, callback, }: { path: string userProvidedConfigName?: string - callback: (app: AppInterface, ourFunction: ExtensionInstance) => Promise + apiKey?: string + callback: ( + app: AppLinkedInterface, + developerPlatformClient: DeveloperPlatformClient, + ourFunction: ExtensionInstance, + ) => Promise }) { - const specifications = await loadLocalExtensionsSpecifications() - const app: AppInterface = await loadApp({specifications, directory: path, userProvidedConfigName}) + const {app, developerPlatformClient} = await linkedAppContext({ + directory: path, + clientId: apiKey, + forceRelink: false, + userProvidedConfigName, + mode: 'strict', + }) const allFunctions = app.allExtensions.filter( (ext) => ext.isFunctionExtension, @@ -40,14 +51,14 @@ export async function inFunctionContext({ const ourFunction = allFunctions.find((fun) => fun.directory === path) if (ourFunction) { - return callback(app, ourFunction) + return callback(app, developerPlatformClient, ourFunction) } else if (isTerminalInteractive()) { const selectedFunction = await renderAutocompletePrompt({ message: 'Which function?', choices: allFunctions.map((shopifyFunction) => ({label: shopifyFunction.handle, value: shopifyFunction})), }) - return callback(app, selectedFunction) + return callback(app, developerPlatformClient, selectedFunction) } else { throw new AbortError('Run this command from a function directory or use `--path` to specify a function directory.') } @@ -55,7 +66,8 @@ export async function inFunctionContext({ export async function getOrGenerateSchemaPath( extension: ExtensionInstance, - app: AppInterface, + app: AppLinkedInterface, + developerPlatformClient: DeveloperPlatformClient, ): Promise { const path = joinPath(extension.directory, 'schema.graphql') if (await fileExists(path)) { @@ -64,6 +76,7 @@ export async function getOrGenerateSchemaPath( await generateSchemaService({ app, + developerPlatformClient, extension, stdout: false, path: extension.directory, diff --git a/packages/app/src/cli/services/function/replay.test.ts b/packages/app/src/cli/services/function/replay.test.ts index 87cef6d5e3..6228a9fbb0 100644 --- a/packages/app/src/cli/services/function/replay.test.ts +++ b/packages/app/src/cli/services/function/replay.test.ts @@ -1,10 +1,9 @@ import {FunctionRunData, replay} from './replay.js' import {renderReplay} from './ui.js' import {runFunction} from './runner.js' -import {testApp, testDeveloperPlatformClient, testFunctionExtension} from '../../models/app/app.test-data.js' +import {testAppLinked, testFunctionExtension} from '../../models/app/app.test-data.js' import {ExtensionInstance} from '../../models/extensions/extension-instance.js' import {FunctionConfigType} from '../../models/extensions/specifications/function.js' -import {ensureConnectedAppFunctionContext} from '../generate-schema.js' import {selectFunctionRunPrompt} from '../../prompts/function/replay.js' import {randomUUID} from '@shopify/cli-kit/node/crypto' import {readFile} from '@shopify/cli-kit/node/fs' @@ -24,8 +23,6 @@ vi.mock('./ui.js') vi.mock('./runner.js') describe('replay', () => { - const developerPlatformClient = testDeveloperPlatformClient() - const apiKey = 'apiKey' const defaultConfig = { name: 'MyFunction', type: 'product_discounts', @@ -45,10 +42,6 @@ describe('replay', () => { extension = await testFunctionExtension({config: defaultConfig}) }) - beforeEach(() => { - vi.mocked(ensureConnectedAppFunctionContext).mockResolvedValue({apiKey, developerPlatformClient}) - }) - test('runs selected function', async () => { // Given const file1 = createFunctionRunFile({handle: extension.handle}) @@ -59,7 +52,7 @@ describe('replay', () => { // When await replay({ - app: testApp(), + app: testAppLinked(), extension, stdout: false, path: 'test-path', @@ -81,7 +74,7 @@ describe('replay', () => { // When await replay({ - app: testApp(), + app: testAppLinked(), extension, stdout: false, path: 'test-path', @@ -103,7 +96,7 @@ describe('replay', () => { // When await replay({ - app: testApp(), + app: testAppLinked(), extension, stdout: false, path: 'test-path', @@ -121,7 +114,7 @@ describe('replay', () => { // When/Then await expect(async () => { await replay({ - app: testApp(), + app: testAppLinked(), extension, stdout: false, path: 'test-path', @@ -141,7 +134,7 @@ describe('replay', () => { // When await replay({ - app: testApp(), + app: testAppLinked(), extension, stdout: false, path: 'test-path', @@ -163,7 +156,7 @@ describe('replay', () => { // When await expect(async () => replay({ - app: testApp(), + app: testAppLinked(), extension, stdout: false, path: 'test-path', @@ -189,7 +182,7 @@ describe('replay', () => { // When await replay({ - app: testApp(), + app: testAppLinked(), extension, stdout: false, path: 'test-path', @@ -212,7 +205,7 @@ describe('replay', () => { // When await expect(async () => replay({ - app: testApp(), + app: testAppLinked(), extension, stdout: false, path: 'test-path', @@ -235,7 +228,7 @@ describe('replay', () => { // When await replay({ - app: testApp(), + app: testAppLinked(), extension, stdout: false, path: 'test-path', diff --git a/packages/app/src/cli/services/function/replay.ts b/packages/app/src/cli/services/function/replay.ts index 2b5bfbc6b3..c77f0f5d4c 100644 --- a/packages/app/src/cli/services/function/replay.ts +++ b/packages/app/src/cli/services/function/replay.ts @@ -1,7 +1,6 @@ import {renderReplay} from './ui.js' import {runFunction} from './runner.js' -import {ensureConnectedAppFunctionContext} from '../generate-schema.js' -import {AppInterface} from '../../models/app/app.js' +import {AppLinkedInterface} from '../../models/app/app.js' import {ExtensionInstance} from '../../models/extensions/extension-instance.js' import {FunctionConfigType} from '../../models/extensions/specifications/function.js' import {selectFunctionRunPrompt} from '../../prompts/function/replay.js' @@ -17,9 +16,8 @@ import {readdirSync} from 'fs' const LOG_SELECTOR_LIMIT = 100 interface ReplayOptions { - app: AppInterface + app: AppLinkedInterface extension: ExtensionInstance - apiKey?: string stdout?: boolean path: string json: boolean @@ -54,7 +52,7 @@ export async function replay(options: ReplayOptions) { const abortController = new AbortController() try { - const {apiKey} = await ensureConnectedAppFunctionContext(options) + const apiKey = options.app.configuration.client_id const functionRunsDir = joinPath(getLogsDir(), apiKey) const selectedRun = options.log diff --git a/packages/app/src/cli/services/generate-schema.test.ts b/packages/app/src/cli/services/generate-schema.test.ts index ed8f93c68a..7a20343e8b 100644 --- a/packages/app/src/cli/services/generate-schema.test.ts +++ b/packages/app/src/cli/services/generate-schema.test.ts @@ -1,15 +1,7 @@ import {generateSchemaService} from './generate-schema.js' -import * as localEnvironment from './context.js' -import * as identifiers from '../models/app/identifiers.js' -import { - testApp, - testDeveloperPlatformClient, - testFunctionExtension, - testOrganizationApp, -} from '../models/app/app.test-data.js' +import {testAppLinked, testDeveloperPlatformClient, testFunctionExtension} from '../models/app/app.test-data.js' import {ApiSchemaDefinitionQueryVariables} from '../api/graphql/functions/api_schema_definition.js' -import {beforeEach, describe, expect, MockedFunction, vi, test} from 'vitest' -import {isTerminalInteractive} from '@shopify/cli-kit/node/context/local' +import {describe, expect, vi, test} from 'vitest' import {AbortError} from '@shopify/cli-kit/node/error' import {inTemporaryDirectory, readFile} from '@shopify/cli-kit/node/fs' import {joinPath} from '@shopify/cli-kit/node/path' @@ -36,7 +28,7 @@ describe('generateSchemaService', () => { test('Save the latest GraphQL schema to ./[extension]/schema.graphql when stdout flag is ABSENT', async () => { await inTemporaryDirectory(async (tmpDir) => { // Given - const app = testApp() + const app = testAppLinked() const extension = await testFunctionExtension({}) const apiKey = 'api-key' const path = tmpDir @@ -45,7 +37,6 @@ describe('generateSchemaService', () => { await generateSchemaService({ app, extension, - apiKey, path, stdout: false, developerPlatformClient: testDeveloperPlatformClient(), @@ -60,9 +51,8 @@ describe('generateSchemaService', () => { test('Print the latest GraphQL schema to stdout when stdout flag is PRESENT', async () => { await inTemporaryDirectory(async (tmpDir) => { // Given - const app = testApp() + const app = testAppLinked() const extension = await testFunctionExtension() - const apiKey = 'api-key' const path = tmpDir const stdout = true const mockOutput = vi.fn() @@ -72,7 +62,6 @@ describe('generateSchemaService', () => { await generateSchemaService({ app, extension, - apiKey, path, stdout, developerPlatformClient: testDeveloperPlatformClient(), @@ -87,7 +76,7 @@ describe('generateSchemaService', () => { test('Uses ApiSchemaDefinitionQuery when not using targets', async () => { await inTemporaryDirectory(async (tmpDir) => { // Given - const app = testApp() + const app = testAppLinked() const extension = await testFunctionExtension({ config: { name: 'test function extension', @@ -113,7 +102,6 @@ describe('generateSchemaService', () => { await generateSchemaService({ app, extension, - apiKey, path, stdout: false, developerPlatformClient, @@ -131,7 +119,7 @@ describe('generateSchemaService', () => { test('Uses TargetSchemaDefinitionQuery when targets present', async () => { await inTemporaryDirectory(async (tmpDir) => { // Given - const app = testApp() + const app = testAppLinked() const extension = await testFunctionExtension({ config: { name: 'test function extension', @@ -163,7 +151,6 @@ describe('generateSchemaService', () => { await generateSchemaService({ app, extension, - apiKey, path, stdout: false, developerPlatformClient, @@ -181,7 +168,7 @@ describe('generateSchemaService', () => { test('aborts if a schema could not be generated', async () => { // Given - const app = testApp() + const app = testAppLinked() const extension = await testFunctionExtension() const apiKey = 'api-key' const developerPlatformClient = testDeveloperPlatformClient({ @@ -192,7 +179,6 @@ describe('generateSchemaService', () => { const result = generateSchemaService({ app, extension, - apiKey, path: '', stdout: true, developerPlatformClient, @@ -201,129 +187,4 @@ describe('generateSchemaService', () => { // Then await expect(result).rejects.toThrow(AbortError) }) - - describe('API key', () => { - const apiKey = 'api-key' - const identifiersApiKey = 'identifier-api-key' - const promptApiKey = 'prompt-api-key' - - const getAppIdentifiers = identifiers.getAppIdentifiers as MockedFunction - const fetchOrCreateOrganizationApp = localEnvironment.fetchOrCreateOrganizationApp as MockedFunction< - typeof localEnvironment.fetchOrCreateOrganizationApp - > - - beforeEach(async () => { - getAppIdentifiers.mockReturnValue({app: identifiersApiKey}) - fetchOrCreateOrganizationApp.mockResolvedValue( - testOrganizationApp({ - apiKey: promptApiKey, - }), - ) - vi.mocked(isTerminalInteractive).mockReturnValue(true) - }) - - test('uses options API key if provided', async () => { - // Given - const app = testApp() - const extension = await testFunctionExtension() - const { - configuration: {api_version: version}, - type, - } = extension - const developerPlatformClient = testDeveloperPlatformClient() - - // When - await generateSchemaService({ - app, - extension, - apiKey, - path: '', - stdout: true, - developerPlatformClient, - }) - - // Then - expect(developerPlatformClient.apiSchemaDefinition).toHaveBeenCalledWith({ - apiKey, - version, - type, - }) - }) - - test('uses app identifier API key, if options API key is not provided', async () => { - // Given - const app = testApp() - const extension = await testFunctionExtension() - const { - configuration: {api_version: version}, - type, - } = extension - const developerPlatformClient = testDeveloperPlatformClient() - - // When - await generateSchemaService({ - app, - extension, - path: '', - stdout: true, - developerPlatformClient, - }) - - // Then - expect(developerPlatformClient.apiSchemaDefinition).toHaveBeenCalledWith({ - apiKey: identifiersApiKey, - version, - type, - }) - }) - - test('prompts for app if no API key is provided in interactive mode', async () => { - // Given - const app = testApp() - const extension = await testFunctionExtension() - const { - configuration: {api_version: version}, - type, - } = extension - getAppIdentifiers.mockReturnValue({app: undefined}) - const developerPlatformClient = testDeveloperPlatformClient() - - // When - await generateSchemaService({ - app, - extension, - path: '', - stdout: true, - developerPlatformClient, - }) - - // Then - expect(developerPlatformClient.apiSchemaDefinition).toHaveBeenCalledWith({ - apiKey: promptApiKey, - version, - type, - }) - }) - - test('aborts if no API key is provided in non-interactive mode', async () => { - // Given - const app = testApp() - const extension = await testFunctionExtension() - getAppIdentifiers.mockReturnValue({app: undefined}) - vi.mocked(isTerminalInteractive).mockReturnValue(false) - const developerPlatformClient = testDeveloperPlatformClient() - - // When - const result = generateSchemaService({ - app, - extension, - path: '', - stdout: true, - developerPlatformClient: testDeveloperPlatformClient(), - }) - - await expect(result).rejects.toThrow() - expect(developerPlatformClient.apiSchemaDefinition).not.toHaveBeenCalled() - }) - }) }) diff --git a/packages/app/src/cli/services/generate-schema.ts b/packages/app/src/cli/services/generate-schema.ts index 686b09772c..9be564ace4 100644 --- a/packages/app/src/cli/services/generate-schema.ts +++ b/packages/app/src/cli/services/generate-schema.ts @@ -1,50 +1,25 @@ -import {fetchOrCreateOrganizationApp} from './context.js' -import {DeveloperPlatformClient, selectDeveloperPlatformClient} from '../utilities/developer-platform-client.js' -import {AppInterface} from '../models/app/app.js' -import {getAppIdentifiers} from '../models/app/identifiers.js' +import {DeveloperPlatformClient} from '../utilities/developer-platform-client.js' import {ApiSchemaDefinitionQueryVariables} from '../api/graphql/functions/api_schema_definition.js' import {TargetSchemaDefinitionQueryVariables} from '../api/graphql/functions/target_schema_definition.js' import {ExtensionInstance} from '../models/extensions/extension-instance.js' import {FunctionConfigType} from '../models/extensions/specifications/function.js' -import {isTerminalInteractive} from '@shopify/cli-kit/node/context/local' +import {AppLinkedInterface} from '../models/app/app.js' import {AbortError} from '@shopify/cli-kit/node/error' import {outputContent, outputInfo} from '@shopify/cli-kit/node/output' import {writeFile} from '@shopify/cli-kit/node/fs' import {joinPath} from '@shopify/cli-kit/node/path' interface GenerateSchemaOptions { - app: AppInterface + app: AppLinkedInterface extension: ExtensionInstance - apiKey?: string stdout: boolean path: string - developerPlatformClient?: DeveloperPlatformClient -} - -export async function ensureConnectedAppFunctionContext( - options: Pick, -): Promise<{apiKey: string; developerPlatformClient: DeveloperPlatformClient}> { - const {app} = options - const developerPlatformClient = - options.developerPlatformClient ?? selectDeveloperPlatformClient({configuration: options.app.configuration}) - let apiKey = options.apiKey || getAppIdentifiers({app}, developerPlatformClient).app - - if (!apiKey) { - if (!isTerminalInteractive()) { - throw new AbortError( - outputContent`No Client ID was provided.`, - outputContent`Provide a Client ID with the --client-id flag.`, - ) - } - - apiKey = (await fetchOrCreateOrganizationApp(app.creationDefaultOptions())).apiKey - } - return {apiKey, developerPlatformClient} + developerPlatformClient: DeveloperPlatformClient } export async function generateSchemaService(options: GenerateSchemaOptions) { - const {extension, stdout} = options - const {apiKey, developerPlatformClient} = await ensureConnectedAppFunctionContext(options) + const {extension, stdout, developerPlatformClient, app} = options + const apiKey = app.configuration.client_id const {api_version: version, type, targeting} = extension.configuration const usingTargets = Boolean(targeting?.length) diff --git a/packages/app/src/cli/utilities/app-command.ts b/packages/app/src/cli/utilities/app-command.ts index 007d5e3e86..f52ac4eb11 100644 --- a/packages/app/src/cli/utilities/app-command.ts +++ b/packages/app/src/cli/utilities/app-command.ts @@ -1,15 +1,15 @@ import {configurationFileNames} from '../constants.js' -import {AppInterface} from '../models/app/app.js' +import {AppLinkedInterface} from '../models/app/app.js' import BaseCommand from '@shopify/cli-kit/node/base-command' /** * By forcing all commands to return `AppCommandOutput` we can be sure that during the run of each command we: - * - Authenticate the user (PENDING) - * - Load an app + * - Have an app that is correctly linked and loaded + * - The user is authenticated + * - A remoteApp is fetched */ export interface AppCommandOutput { - // session: PartnersSession (PENDING) - app: AppInterface + app: AppLinkedInterface } export default abstract class AppCommand extends BaseCommand { diff --git a/packages/eslint-plugin-cli/rules/required-fields-when-loading-app.js b/packages/eslint-plugin-cli/rules/required-fields-when-loading-app.js index d3557b61c8..d35a411049 100644 --- a/packages/eslint-plugin-cli/rules/required-fields-when-loading-app.js +++ b/packages/eslint-plugin-cli/rules/required-fields-when-loading-app.js @@ -12,7 +12,7 @@ module.exports = { }, create: function (context) { - const loadFunctions = ['loadApp', 'loadAppConfiguration', 'inFunctionContext'] + const loadFunctions = ['loadApp', 'loadAppConfiguration', 'inFunctionContext', 'linkedAppContext'] return { CallExpression: function (node) { From d863ab4cc51d8da6c9de59f3fb12d223ebcff384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isaac=20Rold=C3=A1n?= Date: Wed, 9 Oct 2024 12:17:24 +0200 Subject: [PATCH 2/5] Clean up some conflicts --- packages/app/src/cli/commands/app/build.ts | 3 ++- packages/app/src/cli/models/app/app.test-data.ts | 15 +-------------- packages/app/src/cli/models/app/app.ts | 5 ++++- packages/app/src/cli/utilities/app-command.ts | 5 +++-- 4 files changed, 10 insertions(+), 18 deletions(-) diff --git a/packages/app/src/cli/commands/app/build.ts b/packages/app/src/cli/commands/app/build.ts index d33c75a919..f320a75a16 100644 --- a/packages/app/src/cli/commands/app/build.ts +++ b/packages/app/src/cli/commands/app/build.ts @@ -54,7 +54,8 @@ export default class Build extends AppCommand { directory: flags.path, clientId: flags['client-id'], forceRelink: false, - configName: flags.config, + userProvidedConfigName: flags.config, + mode: 'report', }) await build({app, skipDependenciesInstallation: flags['skip-dependencies-installation'], apiKey}) diff --git a/packages/app/src/cli/models/app/app.test-data.ts b/packages/app/src/cli/models/app/app.test-data.ts index 0629873125..7e0b9036f8 100644 --- a/packages/app/src/cli/models/app/app.test-data.ts +++ b/packages/app/src/cli/models/app/app.test-data.ts @@ -574,7 +574,7 @@ export function testOrganizationStore({shopId, shopDomain}: {shopId?: string; sh } } -export const testRemoteSpecifications: RemoteSpecification[] = [ +const testRemoteSpecifications: RemoteSpecification[] = [ { name: 'Checkout Post Purchase', externalName: 'Post-purchase UI', @@ -1063,18 +1063,6 @@ const appVersionsDiffResponse: AppVersionsDiffSchema = { }, } -const functionUploadUrlResponse = { - functionUploadUrlGenerate: { - generatedUrlDetails: { - headers: {}, - maxSize: '200 kb', - url: 'https://example.com/upload-url', - moduleId: 'module-id', - maxBytes: 200, - }, - }, -} - export const extensionCreateResponse: ExtensionCreateSchema = { extensionCreate: { extensionRegistration: { @@ -1240,7 +1228,6 @@ export function testDeveloperPlatformClient(stubs: Partial Promise.resolve(emptyActiveAppVersion), appVersionByTag: (_input: AppVersionByTagVariables) => Promise.resolve(appVersionByTagResponse), appVersionsDiff: (_input: AppVersionsDiffVariables) => Promise.resolve(appVersionsDiffResponse), - functionUploadUrl: () => Promise.resolve(functionUploadUrlResponse), createExtension: (_input: ExtensionCreateVariables) => Promise.resolve(extensionCreateResponse), updateExtension: (_input: ExtensionUpdateDraftMutationVariables) => Promise.resolve(extensionUpdateResponse), deploy: (_input: AppDeployVariables) => Promise.resolve(deployResponse), diff --git a/packages/app/src/cli/models/app/app.ts b/packages/app/src/cli/models/app/app.ts index f1388606e7..115459cf31 100644 --- a/packages/app/src/cli/models/app/app.ts +++ b/packages/app/src/cli/models/app/app.ts @@ -347,7 +347,10 @@ export class App< async manifest(): Promise { const modules = await Promise.all( this.realExtensions.map(async (module) => { - const config = await module.commonDeployConfig('', this.configuration) + const config = await module.deployConfig({ + apiKey: String(this.configuration.client_id ?? ''), + appConfiguration: this.configuration, + }) return { type: module.externalType, handle: module.handle, diff --git a/packages/app/src/cli/utilities/app-command.ts b/packages/app/src/cli/utilities/app-command.ts index f52ac4eb11..4d74428c65 100644 --- a/packages/app/src/cli/utilities/app-command.ts +++ b/packages/app/src/cli/utilities/app-command.ts @@ -1,5 +1,5 @@ import {configurationFileNames} from '../constants.js' -import {AppLinkedInterface} from '../models/app/app.js' +import {AppInterface} from '../models/app/app.js' import BaseCommand from '@shopify/cli-kit/node/base-command' /** @@ -9,7 +9,8 @@ import BaseCommand from '@shopify/cli-kit/node/base-command' * - A remoteApp is fetched */ export interface AppCommandOutput { - app: AppLinkedInterface + // PENDING: Use AppLinkedInterface + app: AppInterface } export default abstract class AppCommand extends BaseCommand { From 445483c9e550e0d241ee0dabc9f2b1a04ef287bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isaac=20Rold=C3=A1n?= Date: Wed, 9 Oct 2024 12:24:49 +0200 Subject: [PATCH 3/5] Some cleanup in tests --- .../app/src/cli/services/function/common.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/app/src/cli/services/function/common.test.ts b/packages/app/src/cli/services/function/common.test.ts index 4376b24f05..98a9ed2795 100644 --- a/packages/app/src/cli/services/function/common.test.ts +++ b/packages/app/src/cli/services/function/common.test.ts @@ -1,11 +1,11 @@ import {getOrGenerateSchemaPath, inFunctionContext} from './common.js' import { - testApp, + testAppLinked, testDeveloperPlatformClient, testFunctionExtension, testOrganizationApp, } from '../../models/app/app.test-data.js' -import {AppInterface, AppLinkedInterface} from '../../models/app/app.js' +import {AppLinkedInterface} from '../../models/app/app.js' import {ExtensionInstance} from '../../models/extensions/extension-instance.js' import {FunctionConfigType} from '../../models/extensions/specifications/function.js' import {generateSchemaService} from '../generate-schema.js' @@ -23,14 +23,14 @@ vi.mock('@shopify/cli-kit/node/context/local') vi.mock('@shopify/cli-kit/node/fs') vi.mock('../generate-schema.js') -let app: AppInterface +let app: AppLinkedInterface let ourFunction: ExtensionInstance beforeEach(async () => { ourFunction = await testFunctionExtension() - app = testApp({allExtensions: [ourFunction]}) + app = testAppLinked({allExtensions: [ourFunction]}) vi.mocked(linkedAppContext).mockResolvedValue({ - app: app as AppLinkedInterface, + app, remoteApp: testOrganizationApp(), developerPlatformClient: testDeveloperPlatformClient(), specifications: [], @@ -106,7 +106,7 @@ describe('getOrGenerateSchemaPath', () => { configuration: {}, } as ExtensionInstance - app = {} as AppLinkedInterface + app = testAppLinked() developerPlatformClient = testDeveloperPlatformClient() }) From f0392ac1a9ffe8f574f020462233c495dd584896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isaac=20Rold=C3=A1n?= Date: Wed, 9 Oct 2024 12:40:39 +0200 Subject: [PATCH 4/5] Fix lint --- packages/app/src/cli/services/function/replay.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/cli/services/function/replay.test.ts b/packages/app/src/cli/services/function/replay.test.ts index 6228a9fbb0..0da743c056 100644 --- a/packages/app/src/cli/services/function/replay.test.ts +++ b/packages/app/src/cli/services/function/replay.test.ts @@ -7,7 +7,7 @@ import {FunctionConfigType} from '../../models/extensions/specifications/functio import {selectFunctionRunPrompt} from '../../prompts/function/replay.js' import {randomUUID} from '@shopify/cli-kit/node/crypto' import {readFile} from '@shopify/cli-kit/node/fs' -import {describe, expect, beforeAll, beforeEach, test, vi} from 'vitest' +import {describe, expect, beforeAll, test, vi} from 'vitest' import {AbortError} from '@shopify/cli-kit/node/error' import {outputInfo} from '@shopify/cli-kit/node/output' import {readdirSync} from 'fs' From 14c8b0817795b7164f8f31682051a5a1f6c07a4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isaac=20Rold=C3=A1n?= Date: Wed, 9 Oct 2024 14:00:26 +0200 Subject: [PATCH 5/5] Update interface docs --- packages/app/src/cli/services/app-context.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/app/src/cli/services/app-context.ts b/packages/app/src/cli/services/app-context.ts index e4f0d5e716..3e93ba4eb6 100644 --- a/packages/app/src/cli/services/app-context.ts +++ b/packages/app/src/cli/services/app-context.ts @@ -19,9 +19,11 @@ interface LoadedAppContextOutput { * Input options for the `linkedAppContext` function. * * @param directory - The directory containing the app. - * @param clientId - The client ID to use when linking the app or when fetching the remote app. * @param forceRelink - Whether to force a relink of the app, this includes re-selecting the remote org and app. - * @param configName - The name of an existing config file in the app, if not provided, the cached/default one will be used. + * @param clientId - The client ID to use when linking the app or when fetching the remote app. + * @param userProvidedConfigName - The name of an existing config file in the app, if not provided, the cached/default one will be used. + * @param mode - The mode of the app loader, it can be 'strict' or 'report'. 'report' will not throw an error when the app/extension configuration is invalid. + * It is recommended to always use 'strict' mode unless the command can work with invalid configurations (like app info). */ interface LoadedAppContextOptions { directory: string @@ -37,7 +39,7 @@ interface LoadedAppContextOptions { * You can use a custom configName to load a specific config file. * In any case, if the selected config file is not linked, this function will force a link. * - * @returns The local app, the remote app, and the developer platform client. + * @returns The local app, the remote app, the correct developer platform client, and the remote specifications list. */ export async function linkedAppContext({ directory,