Skip to content
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/cli/json-output-command-exceptions.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
const commandExceptions = [
// Existing finite commands awaiting migration. Remove entries as they adopt typed JSON output.
// Do not add new finite commands to this section.
'packages/cli/src/commands/hydrogen/customer-account-push.ts',
'packages/cli/src/commands/hydrogen/deploy.ts',
'packages/cli/src/commands/hydrogen/env/list.ts',
'packages/cli/src/commands/hydrogen/env/pull.ts',
Expand Down
12 changes: 11 additions & 1 deletion packages/cli/oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@
"hydrogen:customer-account-push": {
"aliases": [],
"args": {},
"description": "Push project configuration to admin",
"description": "Push project configuration to admin\n\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `HydrogenCustomerAccountPushResult` schema.\n\n```json\n{\n \"type\": \"object\",\n \"properties\": {\n \"storefrontId\": {\n \"type\": \"string\"\n },\n \"redirectUri\": {\n \"type\": \"string\"\n },\n \"javascriptOrigin\": {\n \"type\": \"string\"\n },\n \"logoutUri\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"storefrontId\",\n \"redirectUri\",\n \"javascriptOrigin\",\n \"logoutUri\"\n ],\n \"additionalProperties\": false,\n \"title\": \"HydrogenCustomerAccountPushResult\",\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```",
"flags": {
"json-schema": {
"description": "Print the command's JSON schemas.",
Expand All @@ -252,6 +252,15 @@
"allowNo": false,
"type": "boolean"
},
"json": {
"char": "j",
"description": "Output the result as JSON. Automatically disables color output.",
"env": "SHOPIFY_FLAG_JSON",
"hidden": false,
"name": "json",
"allowNo": false,
"type": "boolean"
},
"path": {
"description": "The path to the directory of the Hydrogen storefront. Defaults to the current directory where the command is run.",
"env": "SHOPIFY_HYDROGEN_FLAG_PATH",
Expand Down Expand Up @@ -305,6 +314,7 @@
"pluginType": "core",
"strict": true,
"enableJsonFlag": false,
"descriptionWithMarkdown": "Push project configuration to admin",
"isESM": true,
"relativePath": [
"dist",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
import {type AdminSession, login} from '../../lib/auth.js';
import {replaceCustomerApplicationUrls} from '../../lib/graphql/admin/customer-application-update.js';
import {setCustomerAccountConfig} from '../../lib/shopify-config.js';
import {runCustomerAccountPush} from './customer-account-push.js';
import CustomerAccountPush, {
runCustomerAccountPush,
pushCustomerAccountConfig,
} from './customer-account-push.js';
import {captureJsonOutput} from '../../../tests/output.js';

vi.mock('../../lib/auth.js');
vi.mock('../../lib/graphql/admin/customer-application-update.js');
Expand Down Expand Up @@ -47,6 +51,71 @@ describe('runCustomerAccountPush', () => {
vi.clearAllMocks();
});

it('writes configured URLs as one JSON document through the command', async () => {
const command = new CustomerAccountPush([], {} as any);
const parse = vi.spyOn(command as any, 'parse').mockResolvedValue({
flags: {
json: true,
'dev-origin': DEV_ORIGIN,
'javascript-origin': JAVASCRIPT_ORIGIN,
'relative-redirect-uri': '/custom/callback',
'relative-logout-uri': '/logout',
},
});
try {
const {stdout, stderr} = await captureJsonOutput(() => command.run());
expect(JSON.parse(stdout)).toEqual({
storefrontId: STOREFRONT_ID,
redirectUri: `${DEV_ORIGIN}/custom/callback`,
javascriptOrigin: JAVASCRIPT_ORIGIN,
logoutUri: `${DEV_ORIGIN}/logout`,
});
expect(stderr).toBe('');
} finally {
parse.mockRestore();
}
});

it('retains the dev server cleanup callback', async () => {
const cleanup = await runCustomerAccountPush({devOrigin: DEV_ORIGIN});
vi.mocked(replaceCustomerApplicationUrls).mockClear();
await cleanup?.();
expect(replaceCustomerApplicationUrls).toHaveBeenCalledWith(
ADMIN_SESSION,
STOREFRONT_ID,
{
redirectUri: {removeRegex: `${DEV_ORIGIN}/account/authorize`},
javascriptOrigin: {removeRegex: DEV_ORIGIN},
logoutUris: {removeRegex: DEV_ORIGIN},
},
);
});

it('propagates mutation errors without producing a success result', async () => {
vi.mocked(replaceCustomerApplicationUrls).mockResolvedValue({
success: false,
userErrors: [],
});
await expect(
pushCustomerAccountConfig({devOrigin: DEV_ORIGIN}),
).rejects.toThrow('setup update fail');
});

it('validates the public schema and advertises it in help', () => {
expect(CustomerAccountPush.flags.json).toBeDefined();
expect(CustomerAccountPush.description).toContain(
CustomerAccountPush.jsonOutputSchema.name,
);
expect(() =>
CustomerAccountPush.jsonOutputSchema.encode({
storefrontId: 1,
redirectUri: DEV_ORIGIN,
javascriptOrigin: DEV_ORIGIN,
logoutUri: DEV_ORIGIN,
} as any),
).toThrow();
});

it('defaults the JavaScript origin to the development origin', async () => {
await runCustomerAccountPush({devOrigin: DEV_ORIGIN});

Expand Down
50 changes: 40 additions & 10 deletions packages/cli/src/commands/hydrogen/customer-account-push.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import {writeJsonResult} from '../../lib/json-output.js';
import {jsonFlag} from '@shopify/cli-kit/node/cli';
import {customerAccountPushJsonOutputSchema} from '../../lib/customer-account/types.js';
import Command from '@shopify/cli-kit/node/base-command';
import {Flags} from '@oclif/core';
import {AbortError} from '@shopify/cli-kit/node/error';
Expand All @@ -14,9 +17,16 @@ import {
import {replaceCustomerApplicationUrls} from '../../lib/graphql/admin/customer-application-update.js';

export default class CustomerAccountPush extends Command {
static description = 'Push project configuration to admin';
static get jsonOutputSchema(): typeof customerAccountPushJsonOutputSchema {
return customerAccountPushJsonOutputSchema;
}

static descriptionWithMarkdown = 'Push project configuration to admin';

static description = this.descriptionForHelp();

static flags = {
...jsonFlag,
...commonFlags.path,
'storefront-id': Flags.string({
description:
Expand All @@ -42,11 +52,27 @@ export default class CustomerAccountPush extends Command {

async run(): Promise<void> {
const {flags} = await this.parse(CustomerAccountPush);
await runCustomerAccountPush({...flagsToCamelObject(flags)});
const outcome = await pushCustomerAccountConfig({
...flagsToCamelObject(flags),
redirectUriRelativeUrl: flags['relative-redirect-uri'],
logoutUriRelativeUrl: flags['relative-logout-uri'],
});
writeJsonResult(
customerAccountPushJsonOutputSchema,
outcome.result,
flags.json,
);
}
}

export async function runCustomerAccountPush({
/** Compatibility entry point used by the dev server to remove temporary URLs. */
export async function runCustomerAccountPush(
options: Parameters<typeof pushCustomerAccountConfig>[0],
) {
return (await pushCustomerAccountConfig(options)).cleanup;
}

export async function pushCustomerAccountConfig({
path: root = process.cwd(),
storefrontId: storefrontIdFromFlag,
devOrigin,
Expand Down Expand Up @@ -79,10 +105,6 @@ export async function runCustomerAccountPush({
? new URL(logoutUriRelativeUrl, devOrigin).toString()
: devOrigin;

if (!redirectUri && !javascriptOrigin && !logoutUri) {
return;
}

const {session, config} = await login(root);
const customerAccountConfig = config?.storefront?.customerAccountConfig;
const {success, userErrors} = await replaceCustomerApplicationUrls(
Expand Down Expand Up @@ -118,12 +140,20 @@ export async function runCustomerAccountPush({
logoutUri,
});

return () =>
cleanupCustomerApplicationUrls(session, storefrontId, {
return {
result: {
storefrontId,
redirectUri,
javascriptOrigin,
logoutUri,
});
} satisfies import('../../lib/customer-account/types.js').CustomerAccountPushResult,
cleanup: () =>
cleanupCustomerApplicationUrls(session, storefrontId, {
redirectUri,
javascriptOrigin,
logoutUri,
}),
};
} catch (error: any) {
let confidentialAccessFound = false;

Expand Down
18 changes: 18 additions & 0 deletions packages/cli/src/lib/customer-account/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {
defineJsonOutputSchema,
type InferJsonOutputSchema,
} from '@shopify/cli-kit/node/json-output-schema';
import {zod} from '@shopify/cli-kit/node/schema';

export const customerAccountPushJsonOutputSchema = defineJsonOutputSchema({
name: 'HydrogenCustomerAccountPushResult',
schema: zod.object({
storefrontId: zod.string(),
redirectUri: zod.string(),
javascriptOrigin: zod.string(),
logoutUri: zod.string(),
}),
});
export type CustomerAccountPushResult = InferJsonOutputSchema<
typeof customerAccountPushJsonOutputSchema
>;
Loading