Skip to content

Commit

Permalink
feat(cli): Add codegen command
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelbromley committed Apr 3, 2024
1 parent 5b32c59 commit de5544c
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 18 deletions.
5 changes: 5 additions & 0 deletions packages/cli/src/commands/add/add.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { cancel, isCancel, log, select } from '@clack/prompts';
import { Command } from 'commander';

import { addCodegen } from './codegen/add-codegen';
import { addEntity } from './entity/add-entity';
import { createNewPlugin } from './plugin/create-new-plugin';
import { addUiExtensions } from './ui-extensions/add-ui-extensions';
Expand All @@ -18,6 +19,7 @@ export function registerAddCommand(program: Command) {
{ value: 'plugin', label: '[Plugin] Add a new plugin' },
{ value: 'entity', label: '[Plugin: Entity] Add a new entity to a plugin' },
{ value: 'uiExtensions', label: '[Plugin: UI] Set up Admin UI extensions' },
{ value: 'codegen', label: '[Project: Codegen] Set up GraphQL code generation' },
],
});
if (isCancel(featureType)) {
Expand All @@ -34,6 +36,9 @@ export function registerAddCommand(program: Command) {
if (featureType === 'entity') {
await addEntity();
}
if (featureType === 'codegen') {
await addCodegen();
}
} catch (e: any) {
log.error(e.message as string);
}
Expand Down
94 changes: 94 additions & 0 deletions packages/cli/src/commands/add/codegen/add-codegen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { log, note, outro, spinner } from '@clack/prompts';
import path from 'path';
import pc from 'picocolors';
import { ClassDeclaration, StructureKind, SyntaxKind } from 'ts-morph';

import { selectMultiplePluginClasses } from '../../../shared/shared-prompts';
import { createFile, getRelativeImportPath, getTsMorphProject } from '../../../utilities/ast-utils';
import { addNpmScriptToPackageJson, installRequiredPackages } from '../../../utilities/package-utils';

export async function addCodegen(providedPluginClass?: ClassDeclaration) {
let pluginClasses = providedPluginClass ? [providedPluginClass] : [];
let project = providedPluginClass?.getProject();
if (!pluginClasses.length || !project) {
const projectSpinner = spinner();
projectSpinner.start('Analyzing project...');
await new Promise(resolve => setTimeout(resolve, 100));
project = getTsMorphProject();
projectSpinner.stop('Project analyzed');
pluginClasses = await selectMultiplePluginClasses(project, 'Add codegen cancelled');
}

const installSpinner = spinner();
installSpinner.start(`Installing dependencies...`);
try {
await installRequiredPackages(project, [
{
pkg: '@graphql-codegen/cli',
isDevDependency: true,
},
{
pkg: '@graphql-codegen/typescript',
isDevDependency: true,
},
]);
} catch (e: any) {
log.error(`Failed to install dependencies: ${e.message as string}.`);
}
installSpinner.stop('Dependencies installed');

const configSpinner = spinner();
configSpinner.start('Configuring codegen file...');
await new Promise(resolve => setTimeout(resolve, 100));

const tempProject = getTsMorphProject({ skipAddingFilesFromTsConfig: true });
const codegenFile = createFile(tempProject, path.join(__dirname, 'templates/codegen.template.ts'));
const codegenConfig = codegenFile
.getVariableDeclaration('config')
?.getChildrenOfKind(SyntaxKind.ObjectLiteralExpression)[0];
if (!codegenConfig) {
throw new Error('Could not find the config variable in the template codegen file');
}
const generatesProp = codegenConfig
.getProperty('generates')
?.getFirstChildByKind(SyntaxKind.ObjectLiteralExpression);
if (!generatesProp) {
throw new Error('Could not find the generates property in the template codegen file');
}
const rootDir = tempProject.getDirectory('.');
if (!rootDir) {
throw new Error('Could not find the root directory of the project');
}
for (const pluginClass of pluginClasses) {
const relativePluginPath = getRelativeImportPath({
from: pluginClass.getSourceFile(),
to: rootDir,
});
const generatedTypesPath = `${path.dirname(relativePluginPath)}/gql/generated.ts`;
generatesProp
.addProperty({
name: `'${generatedTypesPath}'`,
kind: StructureKind.PropertyAssignment,
initializer: `{ plugins: ['typescript'] }`,
})
.formatText();
}
codegenFile.move(path.join(rootDir.getPath(), 'codegen.ts'));

addNpmScriptToPackageJson(tempProject, 'codegen', 'graphql-codegen --config codegen.ts');

configSpinner.stop('Configured codegen file');

project.saveSync();

const nextSteps = [
`You can run codegen by doing the following:`,
`1. Ensure your dev server is running`,
`2. Run "npm run codegen"`,
];
note(nextSteps.join('\n'));

if (!providedPluginClass) {
outro('✅ Codegen setup complete!');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
overwrite: true,
// This assumes your server is running on the standard port
// and with the default admin API path. Adjust accordingly.
schema: 'http://localhost:3000/admin-api',
config: {
// This tells codegen that the `Money` scalar is a number
scalars: { Money: 'number' },
// This ensures generated enums do not conflict with the built-in types.
namingConvention: { enumValues: 'keep' },
},
generates: {},
};

export default config;
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ export async function addUiExtensions(providedPluginClass?: ClassDeclaration) {
const installSpinner = spinner();
installSpinner.start(`Installing dependencies...`);
try {
const version = determineVendureVersion();
await installRequiredPackages([
const version = determineVendureVersion(project);
await installRequiredPackages(project, [
{
pkg: '@vendure/ui-devkit',
isDevDependency: true,
Expand Down
38 changes: 37 additions & 1 deletion packages/cli/src/shared/shared-prompts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cancel, isCancel, select, text } from '@clack/prompts';
import { cancel, isCancel, multiselect, select, text } from '@clack/prompts';
import { pascalCase } from 'change-case';
import { ClassDeclaration, Project } from 'ts-morph';

Expand Down Expand Up @@ -41,3 +41,39 @@ export async function selectPluginClass(project: Project, cancelledMessage: stri
}
return targetPlugin as ClassDeclaration;
}

export async function selectMultiplePluginClasses(project: Project, cancelledMessage: string) {
const pluginClasses = getPluginClasses(project);
const selectAll = await select({
message: 'To which plugin would you like to add the feature?',
options: [
{
value: 'all',
label: 'All plugins',
},
{
value: 'specific',
label: 'Specific plugins (you will be prompted to select the plugins)',
},
],
});
if (isCancel(selectAll)) {
cancel(cancelledMessage);
process.exit(0);
}
if (selectAll === 'all') {
return pluginClasses;
}
const targetPlugins = await multiselect({
message: 'Select one or more plugins (use ↑, ↓, space to select)',
options: pluginClasses.map(c => ({
value: c,
label: c.getName() as string,
})),
});
if (isCancel(targetPlugins)) {
cancel(cancelledMessage);
process.exit(0);
}
return targetPlugins as ClassDeclaration[];
}
16 changes: 9 additions & 7 deletions packages/cli/src/utilities/ast-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { log } from '@clack/prompts';
import fs from 'fs-extra';
import path from 'node:path';
import {
Directory,
Node,
ObjectLiteralExpression,
Project,
Expand Down Expand Up @@ -134,13 +135,14 @@ function getModuleSpecifierString(moduleSpecifier: string | SourceFile, sourceFi
return getRelativeImportPath({ from: moduleSpecifier, to: sourceFile });
}

export function getRelativeImportPath(locations: { from: SourceFile; to: SourceFile }): string {
return convertPathToRelativeImport(
path.relative(
locations.to.getSourceFile().getDirectory().getPath(),
locations.from.getSourceFile().getFilePath(),
),
);
export function getRelativeImportPath(locations: {
from: SourceFile | Directory;
to: SourceFile | Directory;
}): string {
const fromPath =
locations.from instanceof SourceFile ? locations.from.getFilePath() : locations.from.getPath();
const toPath = locations.to instanceof SourceFile ? locations.to.getFilePath() : locations.to.getPath();
return convertPathToRelativeImport(path.relative(toPath, fromPath));
}

export function createFile(project: Project, templatePath: string) {
Expand Down
41 changes: 33 additions & 8 deletions packages/cli/src/utilities/package-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,26 @@ import { note } from '@clack/prompts';
import spawn from 'cross-spawn';
import fs from 'fs-extra';
import path from 'path';
import { Project } from 'ts-morph';

export interface PackageToInstall {
pkg: string;
version?: string;
isDevDependency?: boolean;
}

export function determineVendureVersion(): string | undefined {
const packageJson = getPackageJsonContent();
export function determineVendureVersion(project: Project): string | undefined {
const packageJson = getPackageJsonContent(project);
return packageJson.dependencies['@vendure/core'];
}

export async function installRequiredPackages(requiredPackages: PackageToInstall[]) {
const packageJson = getPackageJsonContent();
/**
* @description
* Installs the packages with the appropriate package manager if the package
* is not already found in the package.json file.
*/
export async function installRequiredPackages(project: Project, requiredPackages: PackageToInstall[]) {
const packageJson = getPackageJsonContent(project);
const packagesToInstall = requiredPackages.filter(({ pkg, version, isDevDependency }) => {
const hasDependency = isDevDependency
? packageJson.devDependencies[pkg]
Expand All @@ -24,10 +30,10 @@ export async function installRequiredPackages(requiredPackages: PackageToInstall
});

const depsToInstall = packagesToInstall
.filter(p => !p.isDevDependency)
.filter(p => !p.isDevDependency && packageJson.dependencies?.[p.pkg] === undefined)
.map(p => `${p.pkg}${p.version ? `@${p.version}` : ''}`);
const devDepsToInstall = packagesToInstall
.filter(p => p.isDevDependency)
.filter(p => p.isDevDependency && packageJson.devDependencies?.[p.pkg] === undefined)
.map(p => `${p.pkg}${p.version ? `@${p.version}` : ''}`);
if (depsToInstall.length) {
await installPackages(depsToInstall, false);
Expand Down Expand Up @@ -72,6 +78,21 @@ export async function installPackages(dependencies: string[], isDev: boolean) {
});
}

export function addNpmScriptToPackageJson(project: Project, scriptName: string, script: string) {
const packageJson = getPackageJsonContent(project);
if (!packageJson) {
return;
}
packageJson.scripts = packageJson.scripts || {};
packageJson.scripts[scriptName] = script;
const rootDir = project.getDirectory('.');
if (!rootDir) {
throw new Error('Could not find the root directory of the project');
}
const packageJsonPath = path.join(rootDir.getPath(), 'package.json');
fs.writeJsonSync(packageJsonPath, packageJson, { spaces: 2 });
}

function determinePackageManagerBasedOnLockFile(): 'yarn' | 'npm' | 'pnpm' {
const yarnLockPath = path.join(process.cwd(), 'yarn.lock');
const npmLockPath = path.join(process.cwd(), 'package-lock.json');
Expand All @@ -88,8 +109,12 @@ function determinePackageManagerBasedOnLockFile(): 'yarn' | 'npm' | 'pnpm' {
return 'npm';
}

function getPackageJsonContent() {
const packageJsonPath = path.join(process.cwd(), 'package.json');
function getPackageJsonContent(project: Project) {
const rootDir = project.getDirectory('.');
if (!rootDir) {
throw new Error('Could not find the root directory of the project');
}
const packageJsonPath = path.join(rootDir.getPath(), 'package.json');
if (!fs.existsSync(packageJsonPath)) {
note(
`Could not find a package.json in the current directory. Please run this command from the root of a Vendure project.`,
Expand Down

0 comments on commit de5544c

Please sign in to comment.