Skip to content

Commit

Permalink
refactor(create-catalyst): enhance CLI commands and update Jest confi…
Browse files Browse the repository at this point in the history
…guration

- Updated Jest configuration to include source maps and module type settings.
- Refactored CLI commands to utilize a dependency injection container for better service management.
- Enhanced the 'create' command with additional options for BigCommerce integration.
- Improved error handling in CLI commands for better user feedback.
- Updated the 'init' command to connect to existing BigCommerce channels with improved environment variable configuration.
- Added telemetry management options to the CLI.

These changes improve the overall structure and usability of the CLI tool, making it more robust and user-friendly.
  • Loading branch information
bookernath committed Dec 27, 2024
1 parent 1c9b880 commit 2bcf3f1
Show file tree
Hide file tree
Showing 21 changed files with 1,149 additions and 400 deletions.
23 changes: 21 additions & 2 deletions packages/create-catalyst/jest.config.cjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
module.exports = {
transformIgnorePatterns: [],
transformIgnorePatterns: [
'node_modules/(?!(fs-extra|@inquirer|chalk|commander|conf|dotenv|giget|lodash.kebabcase|nypm|ora|semver|std-env|zod|zod-validation-error)/)',
],
transform: {
'^.+\\.(t|j)s?$': '@swc/jest',
'^.+\\.(t|j)s?$': ['@swc/jest', {
sourceMaps: true,
module: {
type: 'commonjs'
},
jsc: {
target: 'es2021',
parser: {
syntax: 'typescript',
tsx: false,
decorators: true,
},
},
}]
},
moduleNameMapper: {
'^chalk$': '<rootDir>/src/test-mocks/chalk.ts',
},
setupFiles: ['<rootDir>/src/test/setup.ts'],
};
52 changes: 27 additions & 25 deletions packages/create-catalyst/src/commands/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ import { join } from 'path';
import { z } from 'zod';

import { cloneCatalyst } from '../utils/clone-catalyst';
import { Https } from '../utils/https';
import {
type Channel,
type ChannelsResponse,
type CreateChannelResponse,
type EligibilityResponse,
Https,
} from '../utils/https';
import { installDependencies } from '../utils/install-dependencies';
import { login } from '../utils/login';
import { parse } from '../utils/parse';
Expand Down Expand Up @@ -41,8 +47,8 @@ export const create = new Command('create')
.hideHelp(),
)
.addOption(
new Option('--sample-data-api-url <url>', 'BigCommerce sample data API URL')
.default('https://api.bc-sample.store')
new Option('--cli-api-hostname <hostname>', 'BigCommerce CLI API hostname')
.default('cxm-prd.bigcommerceapp.com')
.hideHelp(),
)
// eslint-disable-next-line complexity
Expand All @@ -65,8 +71,7 @@ export const create = new Command('create')
}

const URLSchema = z.string().url();
const sampleDataApiUrl = parse(options.sampleDataApiUrl, URLSchema);
const bigcommerceApiUrl = parse(`https://api.${options.bigcommerceHostname}`, URLSchema);
const cliApiUrl = parse(`https://${options.cliApiHostname}`, URLSchema);
const bigcommerceAuthUrl = parse(`https://login.${options.bigcommerceHostname}`, URLSchema);
const resetMain = options.resetMain;

Expand Down Expand Up @@ -175,14 +180,15 @@ export const create = new Command('create')
await telemetry.identify(storeHash);

if (!channelId || !storefrontToken) {
const bc = new Https({ bigCommerceApiUrl: bigcommerceApiUrl, storeHash, accessToken });
const sampleDataApi = new Https({
sampleDataApiUrl,
const cliApi = new Https({
bigCommerceApiUrl: `${cliApiUrl}/stores/${storeHash}/cli-api/v3`,
storeHash,
accessToken,
});

const eligibilityResponse = await sampleDataApi.checkEligibility();
const eligibilityResponse = await cliApi.get<EligibilityResponse>(
'/channels/catalyst/eligibility',
);

if (!eligibilityResponse.data.eligible) {
console.warn(chalk.yellow(eligibilityResponse.data.message));
Expand All @@ -207,30 +213,31 @@ export const create = new Command('create')

const {
data: { id: createdChannelId, storefront_api_token: storefrontApiToken },
} = await sampleDataApi.createChannel(newChannelName);

await bc.createChannelMenus(createdChannelId);
} = await cliApi.post<CreateChannelResponse>('/channels/catalyst', {
name: newChannelName,
initialData: { type: 'sample', scenario: 1 },
deployStorefront: false,
});

channelId = createdChannelId;
storefrontToken = storefrontApiToken;

/**
* @todo prompt sample data API
*/
}

if (!shouldCreateChannel) {
const channelSortOrder = ['catalyst', 'next', 'bigcommerce'];

const availableChannels = await bc.channels('?available=true&type=storefront');
const availableChannels = await cliApi.get<ChannelsResponse>(
'/channels?available=true&type=storefront',
);

const existingChannel = await select({
message: 'Which channel would you like to use?',
choices: availableChannels.data
.sort(
(a, b) => channelSortOrder.indexOf(a.platform) - channelSortOrder.indexOf(b.platform),
(a: Channel, b: Channel) =>
channelSortOrder.indexOf(a.platform) - channelSortOrder.indexOf(b.platform),
)
.map((ch) => ({
.map((ch: Channel) => ({
name: ch.name,
value: ch,
description: `Channel Platform: ${
Expand All @@ -242,12 +249,7 @@ export const create = new Command('create')
});

channelId = existingChannel.id;

const {
data: { token: sfToken },
} = await bc.storefrontToken();

storefrontToken = sfToken;
storefrontToken = existingChannel.storefront_api_token;
}
}

Expand Down
120 changes: 58 additions & 62 deletions packages/create-catalyst/src/commands/init.ts
Original file line number Diff line number Diff line change
@@ -1,114 +1,97 @@
import { Command, Option } from '@commander-js/extra-typings';
import { input, select } from '@inquirer/prompts';
import { select } from '@inquirer/prompts';
import chalk from 'chalk';
import * as z from 'zod';

import { Https } from '../utils/https';
import { type Channel, type ChannelsResponse, Https, type InitResponse } from '../utils/https';
import { login } from '../utils/login';
import { parse } from '../utils/parse';
import { Telemetry } from '../utils/telemetry/telemetry';
import { writeEnv } from '../utils/write-env';

const telemetry = new Telemetry();

/**
* The `init` command connects your local Catalyst project to an existing BigCommerce channel.
* It helps you:
* 1. Connect to a BigCommerce store (via CLI args or interactive login)
* 2. Select an existing channel (via CLI args or interactive selection)
* 3. Configure your local environment variables with:
* - Channel ID
* - Store Hash
* - Storefront Token
* - Makeswift API Key (if available)
*
* To create a new channel, use the `create` command instead.
*
* Usage:
* - Interactive: `create-catalyst init`
* - With args: `create-catalyst init --store-hash <hash> --access-token <token> --channel-id <id>`
*/
export const init = new Command('init')
.description('Connect a BigCommerce store with an existing Catalyst project')
.description('Connect your Catalyst project to an existing BigCommerce channel')
.option('--store-hash <hash>', 'BigCommerce store hash')
.option('--access-token <token>', 'BigCommerce access token')
.option('--channel-id <id>', 'Existing BigCommerce channel ID to connect to')
.addOption(
new Option('--bigcommerce-hostname <hostname>', 'BigCommerce hostname')
.default('bigcommerce.com')
.hideHelp(),
)
.addOption(
new Option('--sample-data-api-url <url>', 'BigCommerce sample data API URL')
.default('https://api.bc-sample.store')
new Option('--cli-api-hostname <hostname>', 'BigCommerce CLI API hostname')
.default('cxm-prd.bigcommerceapp.com')
.hideHelp(),
)
.action(async (options) => {
const projectDir = process.cwd();

const URLSchema = z.string().url();
const sampleDataApiUrl = parse(options.sampleDataApiUrl, URLSchema);
const bigCommerceApiUrl = `https://api.${options.bigcommerceHostname}`;
const bigCommerceAuthUrl = `https://login.${options.bigcommerceHostname}`;
const cliApiUrl = parse(`https://${options.cliApiHostname}`, URLSchema);
const bigcommerceAuthUrl = parse(`https://login.${options.bigcommerceHostname}`, URLSchema);

let storeHash = options.storeHash;
let accessToken = options.accessToken;
let channelId;
let channelId = options.channelId ? parseInt(options.channelId, 10) : undefined;
let storefrontToken;
let makeswiftApiKey;

if (!options.storeHash || !options.accessToken) {
const credentials = await login(bigCommerceAuthUrl);
if (!storeHash || !accessToken) {
const credentials = await login(bigcommerceAuthUrl);

storeHash = credentials.storeHash;
accessToken = credentials.accessToken;
}

if (!storeHash || !accessToken) {
console.log(
chalk.yellow('\nYou must authenticate with a store to overwrite your local environment.\n'),
chalk.yellow('\nYou must authenticate with a store to configure your local environment.\n'),
);

process.exit(1);
}

await telemetry.identify(storeHash);

const bc = new Https({ bigCommerceApiUrl, storeHash, accessToken });
const sampleDataApi = new Https({
sampleDataApiUrl,
const cliApi = new Https({
bigCommerceApiUrl: `${cliApiUrl}/stores/${storeHash}/cli-api/v3`,
storeHash,
accessToken,
});

const eligibilityResponse = await sampleDataApi.checkEligibility();

if (!eligibilityResponse.data.eligible) {
console.warn(chalk.yellow(eligibilityResponse.data.message));
}

let shouldCreateChannel;

if (eligibilityResponse.data.eligible) {
shouldCreateChannel = await select({
message: 'Would you like to create a new channel?',
choices: [
{ name: 'Yes', value: true },
{ name: 'No', value: false },
],
});
}

if (shouldCreateChannel) {
const newChannelName = await input({
message: 'What would you like to name your new channel?',
});

const {
data: { id: createdChannelId, storefront_api_token: storefrontApiToken },
} = await sampleDataApi.createChannel(newChannelName);

channelId = createdChannelId;
storefrontToken = storefrontApiToken;

/**
* @todo prompt sample data API
*/
}

if (!shouldCreateChannel) {
if (!channelId) {
const channelSortOrder = ['catalyst', 'next', 'bigcommerce'];

const availableChannels = await bc.channels('?available=true&type=storefront');
const availableChannels = await cliApi.get<ChannelsResponse>(
'/channels?available=true&type=storefront',
);

const existingChannel = await select({
message: 'Which channel would you like to use?',
choices: availableChannels.data
.sort(
(a, b) => channelSortOrder.indexOf(a.platform) - channelSortOrder.indexOf(b.platform),
(a: Channel, b: Channel) =>
channelSortOrder.indexOf(a.platform) - channelSortOrder.indexOf(b.platform),
)
.map((ch) => ({
.map((ch: Channel) => ({
name: ch.name,
value: ch,
description: `Channel Platform: ${
Expand All @@ -120,20 +103,33 @@ export const init = new Command('init')
});

channelId = existingChannel.id;

const {
data: { token: sfToken },
} = await bc.storefrontToken();

storefrontToken = sfToken;
storefrontToken = existingChannel.storefront_api_token;
}

if (!channelId) throw new Error('Something went wrong, channelId is not defined');
if (!storefrontToken) throw new Error('Something went wrong, storefrontToken is not defined');

try {
const initResponse = await cliApi.get<InitResponse>(`/channels/${channelId}/init`);

makeswiftApiKey = initResponse.data.makeswift_dev_api_key;
} catch {
console.warn(
chalk.yellow(
'\nWarning: Could not fetch Makeswift API key. If you need Makeswift integration, please configure it manually.\n',
),
);
}

writeEnv(projectDir, {
channelId: channelId.toString(),
storeHash,
storefrontToken,
...(makeswiftApiKey && { MAKESWIFT_SITE_API_KEY: makeswiftApiKey }),
});

console.log(
chalk.green('\nSuccess!'),
'Your local environment has been configured with the selected channel.\n',
);
});
25 changes: 25 additions & 0 deletions packages/create-catalyst/src/container.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { BigCommerceServiceImpl } from './services/bigcommerce';
import { GitServiceImpl } from './services/git';
import { ProjectServiceImpl } from './services/project';

interface Config {
bigCommerceHostname: string;
cliApiHostname: string;
}

interface Container {
getProjectService: () => ProjectServiceImpl;
}

export function createContainer(config: Config): Container {
const gitService = new GitServiceImpl();
const bigCommerceService = new BigCommerceServiceImpl({
bigCommerceApiUrl: `https://api.${config.bigCommerceHostname}`,
bigCommerceAuthUrl: `https://login.${config.bigCommerceHostname}`,
cliApiUrl: `https://${config.cliApiHostname}`,
});

return {
getProjectService: () => new ProjectServiceImpl(gitService, bigCommerceService),
};
}
8 changes: 8 additions & 0 deletions packages/create-catalyst/src/errors/authentication-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { CatalystError } from './catalyst-error';

export class AuthenticationError extends CatalystError {
constructor(message: string) {
super(message, 'AUTHENTICATION_ERROR');
this.name = 'AuthenticationError';
}
}
9 changes: 9 additions & 0 deletions packages/create-catalyst/src/errors/catalyst-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export class CatalystError extends Error {
constructor(
message: string,
public code: string,
) {
super(message);
this.name = 'CatalystError';
}
}
8 changes: 8 additions & 0 deletions packages/create-catalyst/src/errors/dependency-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { CatalystError } from './catalyst-error';

export class DependencyError extends CatalystError {
constructor(message: string) {
super(message, 'DEPENDENCY_ERROR');
this.name = 'DependencyError';
}
}
7 changes: 7 additions & 0 deletions packages/create-catalyst/src/errors/validation-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class ValidationError extends Error {
constructor(message: string) {
super(message);
Object.setPrototypeOf(this, ValidationError.prototype);
this.name = 'ValidationError';
}
}
Loading

0 comments on commit 2bcf3f1

Please sign in to comment.