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 Aug 7, 2024
1 parent 53ccd31 commit d378aca
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/angry-seas-march.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bigcommerce/create-catalyst": minor
---

add `integration` command to help with developing native Catalyst integrations
44 changes: 44 additions & 0 deletions packages/create-catalyst/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,47 @@ 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`.

As a note, all integrations should be based off the latest `@bigcommerce/catalyst-core` release tag. This is because new Catalyst projects are created based off that tag, and if your integration is compatible with the latest tag, then it will be compatible with all Catalyst projects created from that tag.

Next, create a branch based off the latest `@bigcommerce/catalyst-core` release tag and name it following the convention `integrations/your-integration-name`. This branch is where you'll be building your integration into Catalyst, and the naming convention helps us organize all integration branches inside the Catalyst monorepo.

```bash
git checkout -b integrations/your-integration-name @bigcommerce/catalyst-core@X.X.X
```

> [!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="Your Integration Name" --source-branch=integrations/your-integration-name
```

The command above will create a new folder in your working tree called `integrations/your-integration-name/` 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/your-integration-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.
120 changes: 120 additions & 0 deletions packages/create-catalyst/src/commands/integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { Command } from '@commander-js/extra-typings';
import { exec as execCb } from 'child_process';
import { outputFileSync, writeJsonSync } from 'fs-extra/esm';
import kebabCase from 'lodash.kebabcase';
import { coerce, compare } from 'semver';
import { promisify } from 'util';
import { z } from 'zod';

const exec = promisify(execCb);

interface Manifest {
name: string;
dependencies: {
add: string[];
};
devDependencies: {
add: string[];
};
environmentVariables: string[];
}

export const integration = new Command('integration')
.argument('<integration-name>', 'Formatted name of the integration')
.option('--commit-hash <hash>', 'Override integration source branch with a specific commit hash')
.action(async (integrationNameRaw, options) => {
// @todo check for integration name conflicts
const integrationName = z.string().transform(kebabCase).parse(integrationNameRaw);

const manifest: Manifest = {
name: integrationName,
dependencies: { add: [] },
devDependencies: { add: [] },
environmentVariables: [],
};

await exec('git fetch --tags');

const { stdout: headRefStdOut } = await exec('git rev-parse --abbrev-ref HEAD');
let [sourceRef] = headRefStdOut.split('\n');

if (options.commitHash) {
sourceRef = options.commitHash;
}

const { stdout: catalystTags } = await exec('git tag --list @bigcommerce/catalyst-core@\\*');
const [latestCoreTag] = catalystTags
.split('\n')
.filter(Boolean)
.sort((a, b) => {
const versionA = coerce(a.replace('@bigcommerce/catalyst-core@', ''));
const versionB = coerce(b.replace('@bigcommerce/catalyst-core@', ''));

if (versionA && versionB) {
return compare(versionA, versionB);
}

return 0;
})
.reverse();

const PackageDependenciesSchema = z.object({
dependencies: z.object({}).passthrough(),
devDependencies: z.object({}).passthrough(),
});

const getPackageDeps = async (ref: string) => {
const { stdout } = await exec(`git show ${ref}:core/package.json`);

return PackageDependenciesSchema.parse(JSON.parse(stdout));
};

const integrationJson = await getPackageDeps(sourceRef);
const latestCoreTagJson = await getPackageDeps(latestCoreTag);

const diffObjectKeys = (a: Record<string, unknown>, b: Record<string, unknown>) => {
return Object.keys(a).filter((key) => !Object.keys(b).includes(key));
};

manifest.dependencies.add = diffObjectKeys(
integrationJson.dependencies,
latestCoreTagJson.dependencies,
);
manifest.devDependencies.add = diffObjectKeys(
integrationJson.devDependencies,
latestCoreTagJson.devDependencies,
);

const { stdout: envVarDiff } = await exec(
`git diff ${latestCoreTag}...${sourceRef} -- core/.env.example`,
);

if (envVarDiff.length > 0) {
const envVars: string[] = [];
const lines = envVarDiff.split('\n');
const addedEnvVarPattern = /^\+([A-Z_]+)=/;

lines.forEach((line) => {
const match = line.match(addedEnvVarPattern);

if (match) {
envVars.push(match[1]);
}
});

if (envVars.length > 0) {
manifest.environmentVariables = envVars;
}
}

const { stdout: integrationDiff } = await exec(
`git diff ${latestCoreTag}...${sourceRef} -- ':(exclude)core/package.json' ':(exclude)pnpm-lock.yaml'`,
);

outputFileSync(`integrations/${integrationName}/integration.patch`, integrationDiff);
writeJsonSync(`integrations/${integrationName}/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 d378aca

Please sign in to comment.