Skip to content

Commit

Permalink
feat: command to help generate integration patches
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewvolk committed Jul 30, 2024
1 parent 5c77f41 commit 3114956
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 1 deletion.
43 changes: 43 additions & 0 deletions packages/create-catalyst/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<integration-name-normalized>/` 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.
105 changes: 105 additions & 0 deletions packages/create-catalyst/src/commands/integration.ts
Original file line number Diff line number Diff line change
@@ -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 <name>', 'Formatted name of the integration')
.requiredOption('--source-branch <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.');
});
4 changes: 3 additions & 1 deletion packages/create-catalyst/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`));

Expand All @@ -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);

0 comments on commit 3114956

Please sign in to comment.