-
Notifications
You must be signed in to change notification settings - Fork 187
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cli): CATALYST-246 create-catalyst login, create env, scaffold p…
…roject
- Loading branch information
1 parent
16f9b05
commit a78022a
Showing
21 changed files
with
1,109 additions
and
85 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,55 +1 @@ | ||
# packages/create-catalyst | ||
|
||
> [!WARNING] | ||
> The create-catalyst package is in development and not published to the NPM registry | ||
Scaffolding for Catalyst storefront projects. | ||
|
||
## Usage | ||
|
||
**NPM:** | ||
|
||
```sh | ||
npm create @bigcommerce/catalyst@latest my-catalyst-store | ||
``` | ||
|
||
**PNPM:** | ||
|
||
```sh | ||
pnpm create @bigcommerce/catalyst@latest my-catalyst-store | ||
``` | ||
|
||
**Yarn:** | ||
|
||
```sh | ||
yarn create @bigcommerce/catalyst@latest my-catalyst-store | ||
``` | ||
|
||
## Contributing | ||
|
||
**Prerequisites:** | ||
|
||
- Node `>=18.16` | ||
- [Verdaccio](https://verdaccio.org/) `>=5` | ||
|
||
**Developing Locally:** | ||
|
||
While developing `create-catalyst` locally, it's essential to test your changes before publishing them to NPM. To achieve this, we utilize Verdaccio, a lightweight private npm proxy registry that you can run in your local environment. By publishing `create-catalyst` to Verdaccio during local development, we can point `[yarn|npm|pnpm] create @bigcommerce/catalyst` at the Verdaccio registry URL and observe how our changes will behave once they are published to NPM. | ||
|
||
1. Install Verdaccio: https://verdaccio.org/docs/installation | ||
2. Run Verdaccio: `verdaccio --listen 4873` | ||
3. Add an NPM user with `@bigcommerce` scope to Verdaccio: `npm adduser --scope=@bigcommerce --registry=http://localhost:4873` | ||
|
||
> ⚠️ **IMPORTANT:** NPM registry data is immutable, meaning once published, a package cannot change. Be careful to ensure that you do not run commands such as `publish` against the default NPM registry if your work is not ready to be published. Always explicitly pass `--registry=http://localhost:<VERDACCIO_PORT>` with commands that modify the registry (such as `publish`) to ensure you only publish to Verdaccio when working locally. | ||
4. If necessary, run `pnpm build` and `pnpm publish --registry=http://localhost:4873` in each Catalyst-scoped package required by relevant examples listed in `apps/` (e.g., Catalyst `core` examples require `@bigcommerce/reactant`, `@bigcommerce/eslint-config-catalyst`, `@bigcommerce/catalyst-configs`, `@bigcommerce/catalyst-client`) | ||
5. Run `pnpm build` and `pnpm publish --registry=http://localhost:4873` in the `@bigcommerce/create-catalyst` package | ||
6. Confirm published packages are listed in Verdaccio: http://localhost:4873 | ||
|
||
In order to point `npm create`, `pnpm create`, and/or `yarn create` to the Verdaccio registry, run one or more of the following commands against the package manager's global configuration: | ||
|
||
- **NPM:** `npm config set @bigcommerce:registry http://localhost:4873` | ||
- **PNPM:** `pnpm config set @bigcommerce:registry http://localhost:4873` | ||
- **Yarn:** `yarn config set npmScopes.bigcommerce.npmRegistryServer "http://localhost:4873" -H` and then `yarn config set unsafeHttpWhitelist "localhost" -H` | ||
|
||
7. Finally, navigate to the directory in which you'd like to create a new Catalyst storefront, and run `[yarn|npm|pnpm] create @bigcommerce/catalyst name-of-your-catalyst-storefront` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
import chalk from 'chalk'; | ||
import { exec as execCallback } from 'child_process'; | ||
import { copySync, outputFileSync, readJsonSync, removeSync, writeJsonSync } from 'fs-extra/esm'; | ||
import merge from 'lodash.merge'; | ||
import set from 'lodash.set'; | ||
import unset from 'lodash.unset'; | ||
import { installDependencies } from 'nypm'; | ||
import { join } from 'path'; | ||
import { promisify } from 'util'; | ||
import * as z from 'zod'; | ||
|
||
import { cloneTemplate } from '../utils/clone.js'; | ||
import { getPackageManager } from '../utils/pm.js'; | ||
import { promptWithValidation } from '../utils/prompt.js'; | ||
import { PkgJsonSchema, TsConfigSchema } from '../utils/schemas.js'; | ||
import { spinner } from '../utils/spinner.js'; | ||
import { validate } from '../utils/validate.js'; | ||
|
||
import { env } from './env.js'; | ||
import { login } from './login.js'; | ||
|
||
const exec = promisify(execCallback); | ||
|
||
export const create = async () => { | ||
const GITHUB_TOKEN = validate( | ||
process.env.GITHUB_TOKEN, | ||
z | ||
.string({ required_error: 'GITHUB_TOKEN is a required environment variable' }) | ||
.trim() | ||
.startsWith('ghp'), | ||
); | ||
|
||
const REF = validate(process.env.REF, z.string().trim().optional().default('main')); | ||
|
||
console.log(chalk.cyanBright('\n◢ @bigcommerce/create-catalyst v0.1.0\n')); | ||
|
||
const { projectName } = await promptWithValidation( | ||
{ | ||
type: 'text', | ||
name: 'projectName', | ||
message: 'What is the name of your project?', | ||
}, | ||
z.object({ projectName: z.string().min(1) }), | ||
); | ||
|
||
const projectDir = join(process.cwd(), projectName); | ||
|
||
const credentials = await login(); | ||
|
||
if (!credentials) { | ||
console.log(chalk.yellow('\n@todo use sample data\n')); | ||
process.exit(0); | ||
} | ||
|
||
const environment = await env({ ...credentials }); | ||
|
||
console.log(`\nCreating '${projectName}' at '${projectDir}'\n`); | ||
|
||
await spinner( | ||
cloneTemplate(`github:bigcommerce/catalyst/apps/core#${REF}`, { | ||
dest: projectDir, | ||
auth: GITHUB_TOKEN, | ||
}), | ||
{ | ||
text: 'Cloning Catalyst template...', | ||
successText: 'Catalyst template cloned successfully', | ||
}, | ||
); | ||
|
||
const componentsTmpDir = join(projectDir, 'tmp', 'ui'); | ||
|
||
await spinner( | ||
cloneTemplate(`github:bigcommerce/catalyst/packages/reactant#${REF}`, { | ||
dest: componentsTmpDir, | ||
auth: GITHUB_TOKEN, | ||
}), | ||
{ | ||
text: 'Cloning Catalyst components...', | ||
successText: 'Catalyst components cloned successfully', | ||
}, | ||
); | ||
|
||
outputFileSync(join(projectDir, '.env.local'), environment); | ||
|
||
copySync(join(componentsTmpDir, 'src/components'), join(projectDir, 'components', 'ui')); | ||
|
||
const paths = { | ||
catalyst: { | ||
tailwind: join(projectDir, 'tailwind.config.js'), | ||
package: join(projectDir, 'package.json'), | ||
tsconfig: join(projectDir, 'tsconfig.json'), | ||
}, | ||
components: { | ||
tailwind: join(componentsTmpDir, 'tailwind.config.js'), | ||
package: join(componentsTmpDir, 'package.json'), | ||
tsconfig: join(componentsTmpDir, 'tsconfig.json'), | ||
}, | ||
}; | ||
|
||
const catalystPkgJson = validate(readJsonSync(paths.catalyst.package), PkgJsonSchema); | ||
const componentsPkgJson = validate(readJsonSync(paths.components.package), PkgJsonSchema); | ||
|
||
const packageJson = merge({}, catalystPkgJson, { | ||
name: projectName, | ||
description: '', | ||
version: '0.1.0', | ||
dependencies: componentsPkgJson.dependencies, | ||
devDependencies: componentsPkgJson.devDependencies, | ||
}); | ||
|
||
unset(packageJson, 'dependencies.@bigcommerce/reactant'); // keep | ||
unset(packageJson, 'devDependencies.react'); // move to dep/devDep | ||
unset(packageJson, 'devDependencies.react-dom'); // move to dep/devDep | ||
|
||
set(packageJson, 'dependencies.@bigcommerce/catalyst-client', '^0.1.0'); // modify pkg.json | ||
set(packageJson, 'devDependencies.@bigcommerce/catalyst-configs', '^0.1.0'); // modify pkg.json | ||
set(packageJson, 'devDependencies.@bigcommerce/eslint-config-catalyst', '^0.1.0'); // modify pkg.json | ||
|
||
writeJsonSync(join(projectDir, 'package.json'), packageJson, { spaces: 2 }); | ||
|
||
const catalystTsConfig = validate(readJsonSync(paths.catalyst.tsconfig), TsConfigSchema); | ||
const componentsTsConfig = validate(readJsonSync(paths.components.tsconfig), TsConfigSchema); | ||
|
||
const tsConfig = merge({}, componentsTsConfig, catalystTsConfig); | ||
|
||
unset(tsConfig, 'compilerOptions.declaration'); | ||
unset(tsConfig, 'compilerOptions.declarationMap'); | ||
|
||
set(tsConfig, 'compilerOptions.paths.@bigcommerce/reactant/*', ['./components/ui/*']); | ||
|
||
writeJsonSync(join(projectDir, 'tsconfig.json'), tsConfig, { spaces: 2 }); | ||
|
||
copySync(paths.components.tailwind, paths.catalyst.tailwind); | ||
|
||
removeSync(join(projectDir, 'tmp')); | ||
|
||
// @todo yarn doesn't work? | ||
const pm = getPackageManager(); | ||
|
||
console.log(`\nUsing ${chalk.bold(pm)}\n`); | ||
|
||
await spinner(installDependencies({ cwd: projectDir, silent: true, packageManager: pm }), { | ||
text: `Installing dependencies. This could take a minute...`, | ||
successText: `Dependencies installed successfully`, | ||
}); | ||
|
||
await spinner(exec(`${pm} run codegen`, { cwd: projectDir }), { | ||
text: 'Generating GraphQL types...', | ||
successText: 'GraphQL types generated successfully', | ||
}); | ||
|
||
await spinner(exec(`${pm} run lint --fix`, { cwd: projectDir }), { | ||
text: 'Linting to validate generated types...', | ||
successText: 'GraphQL types validated successfully', | ||
}); | ||
|
||
console.log(`\n${chalk.green('Success!')} Created '${projectName}' at '${projectDir}'\n`); | ||
|
||
console.log('Next steps:'); | ||
console.log(chalk.yellow(`\ncd ${projectName} && ${pm} run dev\n`)); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import chalk from 'chalk'; | ||
import { randomBytes } from 'crypto'; | ||
import * as z from 'zod'; | ||
|
||
import { | ||
createCustomerImpersonationToken, | ||
ensureChannelCheckoutSite, | ||
ensureChannelSite, | ||
fetchChannels, | ||
} from '../utils/http.js'; | ||
import { promptWithValidation } from '../utils/prompt.js'; | ||
import { ChannelSchema, CredentialsSchema } from '../utils/schemas.js'; | ||
|
||
export const env = async ({ storeHash, accessToken }: z.infer<typeof CredentialsSchema>) => { | ||
console.log(chalk.yellow('\n@todo validate storefront limit\n')); | ||
|
||
const { create } = await promptWithValidation( | ||
{ | ||
type: 'select', | ||
name: 'create', | ||
message: 'Which would you like?', | ||
choices: [ | ||
{ title: 'Create a new BigCommerce channel', value: true }, | ||
{ title: 'Use an existing BigCommerce channel', value: false }, | ||
], | ||
}, | ||
z.object({ create: z.boolean() }), | ||
); | ||
|
||
if (create) { | ||
console.log(chalk.yellow(`\n@todo create new channel\n`)); | ||
process.exit(0); | ||
} | ||
|
||
const channels = await fetchChannels({ storeHash, accessToken }); | ||
|
||
const channelSortOrder = ['catalyst', 'next', 'bigcommerce']; | ||
|
||
const { channel } = await promptWithValidation( | ||
{ | ||
type: 'select', | ||
name: 'channel', | ||
message: 'Which channel would you like to link?', | ||
choices: channels.data | ||
.sort((a, b) => channelSortOrder.indexOf(a.platform) - channelSortOrder.indexOf(b.platform)) | ||
.flatMap((ch) => ({ | ||
title: ch.name, | ||
value: ch, | ||
description: `Channel Platform: ${ | ||
ch.platform === 'bigcommerce' | ||
? 'Stencil' | ||
: ch.platform.charAt(0).toUpperCase() + ch.platform.slice(1) | ||
}`, | ||
})), | ||
}, | ||
z.object({ channel: ChannelSchema }), | ||
); | ||
|
||
await ensureChannelSite(channel.id, { storeHash, accessToken }); | ||
await ensureChannelCheckoutSite(channel.id, { storeHash, accessToken }); | ||
|
||
const { | ||
data: { token }, | ||
} = await createCustomerImpersonationToken(channel.id, { storeHash, accessToken }); | ||
|
||
const environment = [ | ||
`AUTH_SECRET=${randomBytes(32).toString('hex')}`, | ||
'BIGCOMMERCE_CDN_HOSTNAME=*.bigcommerce.com', | ||
`BIGCOMMERCE_STORE_HASH=${storeHash}`, | ||
`BIGCOMMERCE_CHANNEL_ID=${channel.id}`, | ||
`BIGCOMMERCE_ACCESS_TOKEN=${accessToken}`, | ||
`BIGCOMMERCE_CUSTOMER_IMPERSONATION_TOKEN=${token}`, | ||
].join('\n'); | ||
|
||
console.log(chalk.yellow('\n@todo post channel checkout site routes')); | ||
console.log(chalk.yellow('@todo generate auth secret')); | ||
|
||
return environment; | ||
}; |
Oops, something went wrong.