From 311495689c2bc65ab19ee9deca55a4cb4f6dfb69 Mon Sep 17 00:00:00 2001 From: Matthew Volk Date: Tue, 30 Jul 2024 12:16:26 -0500 Subject: [PATCH] feat: command to help generate integration patches --- packages/create-catalyst/README.md | 43 +++++++ .../src/commands/integration.ts | 105 ++++++++++++++++++ packages/create-catalyst/src/index.ts | 4 +- 3 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 packages/create-catalyst/src/commands/integration.ts diff --git a/packages/create-catalyst/README.md b/packages/create-catalyst/README.md index 097e04d5c..c2b7c2f11 100644 --- a/packages/create-catalyst/README.md +++ b/packages/create-catalyst/README.md @@ -34,3 +34,46 @@ pnpm create @bigcommerce/catalyst@latest init ```sh yarn create @bigcommerce/catalyst@latest init ``` + +### Develop a native integration for a new Catalyst project + +> **Note:** Currently there aren't any anticipated use cases that require the usage of the `integration` command outside of the Catalyst monorepo. Therefore, `pnpm` is assumed to be the most common package manager that this command will be executed with. + +```sh +pnpm create @bigcommerce/catalyst@latest integration +``` + +#### How to develop a native integration for new Catalyst projects + +If you are interested in developing a native integration for Catalyst, you can use the `integration` command documented above. + +First, you'll need to write the code that makes your integration work with Catalyst. Begin by forking the [`bigcommerce/catalyst` repository](https://github.com/bigcommerce/catalyst), clone the fork locally, and [follow the steps to get started with local monorepo development](https://www.catalyst.dev/docs/monorepo). Be sure to add the original `bigcommerce/catalyst` repository as a remote (often named `upstream`) so that you can pull in latest updates pushed to `bigcommerce/catalyst:main`. + +Next, create a branch off of `main` and name it whatever you'd like. This branch is where you'll be building your integration into Catalyst. + +```bash +git checkout main && +git checkout -b integrations/name +``` + +> [!IMPORTANT] +> +> #### Things to consider when building integrations: +> +> - In order to ensure your integration applies cleanly to new Catalyst projects, your integration should be 100% contained within the `core` folder of the monorepo. With the exception of installing packages inside of `core` (which in turn modifies the root `pnpm-lock.yaml` file), none of your integration code should live outside of the `core` folder. +> - If your integration requires environment variables to work, be sure to add those environment variables to `core/.env.example`. This allows the `integrate` CLI to track which environment variables are required for the integration to work. + +When you've finished building your integration, commit your changes to the branch, and then run the following command: + +```bash +pnpm create @bigcommerce/catalyst@latest integration --integration-name="My Integration" --source-branch=integrations/name +``` + +The command above will create a new folder in your working tree called `integrations//` with two files: `integration.patch` and `manifest.json`. You'll want to create a PR to merge just this created folder into `main`: + +```bash +git checkout main && +git checkout -b integrations/name-patch +``` + +Once that branch is created, commit your changes, push it to your fork, and open a pull request from your remote branch into `bigcommerce/catalyst:main`. Once your branch is merged into main, the CLI will register your new integration for users to choose from when creating a new Catalyst project. diff --git a/packages/create-catalyst/src/commands/integration.ts b/packages/create-catalyst/src/commands/integration.ts new file mode 100644 index 000000000..1a15fd4e3 --- /dev/null +++ b/packages/create-catalyst/src/commands/integration.ts @@ -0,0 +1,105 @@ +import { Command } from '@commander-js/extra-typings'; +import { exec as execCb } from 'child_process'; +import { outputFileSync, writeJsonSync } from 'fs-extra/esm'; +import { promisify } from 'util'; +import * as z from 'zod'; + +const exec = promisify(execCb); + +interface Manifest { + name: string; + dependencies: string[]; + devDependencies: string[]; + environmentVariables: string[]; +} + +export const integration = new Command('integration') + .requiredOption('--integration-name ', 'Formatted name of the integration') + .requiredOption('--source-branch ', 'The branch containing your integration source code') + .action(async (options) => { + const manifest: Manifest = { + name: options.integrationName, + dependencies: [], + devDependencies: [], + environmentVariables: [], + }; + + const { stdout: gitBranchStdOut } = await exec('git branch --format="%(refname:short)"'); + const localBranches = gitBranchStdOut.split('\n').filter(Boolean); + + if (!localBranches.includes(options.sourceBranch)) { + console.error(`Branch "${options.sourceBranch}" does not exist in your local repository.`); + process.exit(1); + } + + const { stdout: packagesDiffStdOut } = await exec( + `git diff main...${options.sourceBranch} -- core/package.json`, + ); + + if (packagesDiffStdOut.length > 0) { + const packages: string[] = []; + const lines = packagesDiffStdOut.split('\n'); + const packagePattern = /^\+ {4}"([^"]+)":/; + + lines.forEach((line) => { + const match = line.match(packagePattern); + + if (match) { + packages.push(match[1]); + } + }); + + if (packages.length > 0) { + const { stdout: integrationPackageJsonRaw } = await exec( + `git show ${options.sourceBranch}:core/package.json`, + ); + + const integrationPackageJson = z + .object({ + dependencies: z.object({}).passthrough(), + devDependencies: z.object({}).passthrough(), + }) + .parse(JSON.parse(integrationPackageJsonRaw)); + + manifest.dependencies = packages.filter((pkg) => integrationPackageJson.dependencies[pkg]); + manifest.devDependencies = packages.filter( + (pkg) => integrationPackageJson.devDependencies[pkg], + ); + } + } + + const { stdout: envVarDiff } = await exec( + `git diff main...${options.sourceBranch} -- core/.env.example`, + ); + + if (envVarDiff.length > 0) { + const envVars: string[] = []; + const lines = envVarDiff.split('\n'); + const envPattern = /^\+([A-Z_]+)=/; + + lines.forEach((line) => { + const match = line.match(envPattern); + + if (match) { + envVars.push(match[1]); + } + }); + + if (envVars.length > 0) { + manifest.environmentVariables = envVars; + } + } + + const integrationNameNormalized = options.integrationName.toLowerCase().replace(/\s/g, '-'); + + const { stdout: integrationDiff } = await exec( + `git diff main...${options.sourceBranch} -- ':(exclude)core/package.json' ':(exclude)pnpm-lock.yaml'`, + ); + + outputFileSync(`integrations/${integrationNameNormalized}/integration.patch`, integrationDiff); + writeJsonSync(`integrations/${integrationNameNormalized}/manifest.json`, manifest, { + spaces: 2, + }); + + console.log('Integration created successfully.'); + }); diff --git a/packages/create-catalyst/src/index.ts b/packages/create-catalyst/src/index.ts index aa80f15df..16b63ba00 100644 --- a/packages/create-catalyst/src/index.ts +++ b/packages/create-catalyst/src/index.ts @@ -7,6 +7,7 @@ import PACKAGE_INFO from '../package.json'; import { create } from './commands/create'; import { init } from './commands/init'; +import { integration } from './commands/integration'; console.log(chalk.cyanBright(`\n◢ ${PACKAGE_INFO.name} v${PACKAGE_INFO.version}\n`)); @@ -15,6 +16,7 @@ program .version(PACKAGE_INFO.version) .description('A command line tool to create a new Catalyst project.') .addCommand(create, { isDefault: true }) - .addCommand(init); + .addCommand(init) + .addCommand(integration); program.parse(process.argv);