From 42b7b0fc8288f4dcad9f5f9f0f8dd2cd4e2ecef0 Mon Sep 17 00:00:00 2001 From: Bill Fienberg Date: Wed, 14 Jan 2026 14:53:28 -0600 Subject: [PATCH] Fix SHOPIFY_CLI_APP_TEMPLATES_JSON_PATH not working with SHOPIFY_CLI_1P_DEV The template JSON override was only implemented in AppManagementClient, but when SHOPIFY_CLI_1P_DEV is enabled, PartnersClient is used instead. This adds the same override logic to PartnersClient so the env var works consistently regardless of which client is selected. Co-Authored-By: Claude Opus 4.5 --- .changeset/fix-template-json-path-1p-dev.md | 5 + .../partners-client.test.ts | 99 ++++++++++++++++++- .../partners-client.ts | 37 +++++-- 3 files changed, 131 insertions(+), 10 deletions(-) create mode 100644 .changeset/fix-template-json-path-1p-dev.md diff --git a/.changeset/fix-template-json-path-1p-dev.md b/.changeset/fix-template-json-path-1p-dev.md new file mode 100644 index 00000000000..6ce070bf155 --- /dev/null +++ b/.changeset/fix-template-json-path-1p-dev.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': patch +--- + +Fix `SHOPIFY_CLI_APP_TEMPLATES_JSON_PATH` env var not working when `SHOPIFY_CLI_1P_DEV` is enabled. The template override now works consistently regardless of which developer platform client is selected. diff --git a/packages/app/src/cli/utilities/developer-platform-client/partners-client.test.ts b/packages/app/src/cli/utilities/developer-platform-client/partners-client.test.ts index b75607313a4..5f28c4f9b6e 100644 --- a/packages/app/src/cli/utilities/developer-platform-client/partners-client.test.ts +++ b/packages/app/src/cli/utilities/developer-platform-client/partners-client.test.ts @@ -1,5 +1,6 @@ import {PartnersClient} from './partners-client.js' import {CreateAppQuery} from '../../api/graphql/create_app.js' +import {RemoteTemplateSpecificationsQuery} from '../../api/graphql/template_specifications.js' import {AppInterface, WebType} from '../../models/app/app.js' import {Organization, OrganizationSource, OrganizationStore} from '../../models/organization.js' import { @@ -11,7 +12,9 @@ import { import {appNamePrompt} from '../../prompts/dev.js' import {FindOrganizationQuery} from '../../api/graphql/find_org.js' import {partnersRequest} from '@shopify/cli-kit/node/api/partners' -import {describe, expect, vi, test, beforeEach} from 'vitest' +import {inTemporaryDirectory, writeFile} from '@shopify/cli-kit/node/fs' +import {joinPath} from '@shopify/cli-kit/node/path' +import {describe, expect, vi, test, beforeEach, afterEach} from 'vitest' vi.mock('../../prompts/dev.js') vi.mock('@shopify/cli-kit/node/api/partners') @@ -230,3 +233,97 @@ describe('singleton pattern', () => { expect(instance1).not.toBe(instance2) }) }) + +describe('templateSpecifications', () => { + const originalEnv = process.env + + afterEach(() => { + process.env = originalEnv + }) + + test('fetches templates from GraphQL when no override is set', async () => { + // Given + const partnersClient = PartnersClient.getInstance(testPartnersUserSession) + const mockTemplates = { + templateSpecifications: [ + { + identifier: 'test-template', + name: 'Test Template', + defaultName: 'test', + group: 'TestGroup', + sortPriority: 1, + supportLinks: [], + types: [{url: 'https://example.com', type: 'test', extensionPoints: [], supportedFlavors: []}], + }, + ], + } + vi.mocked(partnersRequest).mockResolvedValueOnce(mockTemplates) + + // When + const result = await partnersClient.templateSpecifications({apiKey: 'test-api-key'}) + + // Then + expect(partnersRequest).toHaveBeenCalledWith( + RemoteTemplateSpecificationsQuery, + 'token', + {apiKey: 'test-api-key'}, + undefined, + undefined, + {type: 'token_refresh', handler: expect.any(Function)}, + ) + expect(result.templates).toHaveLength(1) + expect(result.templates[0]!.identifier).toBe('test-template') + }) + + test('loads templates from JSON file when SHOPIFY_CLI_APP_TEMPLATES_JSON_PATH is set', async () => { + await inTemporaryDirectory(async (tmpDir) => { + // Given + const templatesPath = joinPath(tmpDir, 'templates.json') + const templates = [ + { + identifier: 'json-template', + name: 'JSON Template', + defaultName: 'json-test', + group: 'JsonGroup', + sortPriority: 1, + supportLinks: [], + type: 'json-type', + url: 'https://example.com/json', + extensionPoints: [], + supportedFlavors: [], + }, + ] + await writeFile(templatesPath, JSON.stringify(templates)) + process.env = {...originalEnv, SHOPIFY_CLI_APP_TEMPLATES_JSON_PATH: templatesPath} + + const partnersClient = PartnersClient.getInstance(testPartnersUserSession) + + // When + const result = await partnersClient.templateSpecifications({apiKey: 'test-api-key'}) + + // Then + expect(partnersRequest).not.toHaveBeenCalledWith( + RemoteTemplateSpecificationsQuery, + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + ) + expect(result.templates).toHaveLength(1) + expect(result.templates[0]!.identifier).toBe('json-template') + expect(result.templates[0]!.type).toBe('json-type') + }) + }) + + test('throws error when SHOPIFY_CLI_APP_TEMPLATES_JSON_PATH points to non-existent file', async () => { + // Given + process.env = {...originalEnv, SHOPIFY_CLI_APP_TEMPLATES_JSON_PATH: '/non/existent/path.json'} + const partnersClient = PartnersClient.getInstance(testPartnersUserSession) + + // When/Then + await expect(partnersClient.templateSpecifications({apiKey: 'test-api-key'})).rejects.toThrow( + 'There is no file at the path specified for template specifications', + ) + }) +}) diff --git a/packages/app/src/cli/utilities/developer-platform-client/partners-client.ts b/packages/app/src/cli/utilities/developer-platform-client/partners-client.ts index 59a680465da..190700d9845 100644 --- a/packages/app/src/cli/utilities/developer-platform-client/partners-client.ts +++ b/packages/app/src/cli/utilities/developer-platform-client/partners-client.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ +import {environmentVariableNames} from '../../constants.js' import {CreateAppQuery, CreateAppQuerySchema, CreateAppQueryVariables} from '../../api/graphql/create_app.js' import { AppVersion, @@ -157,6 +158,7 @@ import {AppLogsSubscribeMutationVariables} from '../../api/graphql/app-managemen import {TypedDocumentNode} from '@graphql-typed-document-node/core' import {isUnitTest} from '@shopify/cli-kit/node/context/local' import {AbortError} from '@shopify/cli-kit/node/error' +import {fileExists, readFile} from '@shopify/cli-kit/node/fs' import {generateFetchAppLogUrl, partnersRequest, partnersRequestDoc} from '@shopify/cli-kit/node/api/partners' import {CacheOptions, GraphQLVariables, UnauthorizedHandler} from '@shopify/cli-kit/node/api/graphql' import {ensureAuthenticatedPartners, Session} from '@shopify/cli-kit/node/session' @@ -367,18 +369,35 @@ export class PartnersClient implements DeveloperPlatformClient { } async templateSpecifications({apiKey}: MinimalAppIdentifiers): Promise { - const variables: RemoteTemplateSpecificationsVariables = {apiKey} - const result: RemoteTemplateSpecificationsSchema = await this.request(RemoteTemplateSpecificationsQuery, variables) - const templates = result.templateSpecifications.map((template) => { - const {types, ...rest} = template - return { - ...rest, - ...types[0], + const {templatesJsonPath} = environmentVariableNames + const overrideFile = process.env[templatesJsonPath] + + let templates + if (overrideFile) { + if (!(await fileExists(overrideFile))) { + throw new AbortError('There is no file at the path specified for template specifications') } - }) + const templatesJson = await readFile(overrideFile) + // JSON file is already in flattened ExtensionTemplate format + templates = JSON.parse(templatesJson) + } else { + const variables: RemoteTemplateSpecificationsVariables = {apiKey} + const result: RemoteTemplateSpecificationsSchema = await this.request( + RemoteTemplateSpecificationsQuery, + variables, + ) + // GraphQL result needs transformation to flatten the types array + templates = result.templateSpecifications.map((template) => { + const {types, ...rest} = template + return { + ...rest, + ...types[0], + } + }) + } let counter = 0 - const templatesWithPriority = templates.map((template) => ({ + const templatesWithPriority = templates.map((template: {sortPriority?: number; group?: string}) => ({ ...template, sortPriority: template.sortPriority ?? counter++, }))