Skip to content
5 changes: 5 additions & 0 deletions .changeset/gold-students-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@strapi/sdk-plugin': minor
---

feat: add a command for generating boilerplate based on @strapi/generators
5 changes: 5 additions & 0 deletions .changeset/popular-pianos-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@strapi/sdk-plugin': patch
---

feat: add validation for the 'strapi' object in the package json
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,7 @@ Verifies the output of your plugin before publishing it
```sh
yarn run verify
```

### `generate`

Starts an interactive CLI to generate boilerplate code for your plugin
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"watch": "pack-up watch"
},
"dependencies": {
"@strapi/generators": "5.27.0",
"@strapi/pack-up": "^5.0.1",
"@types/prompts": "2.4.9",
"boxen": "5.1.2",
Expand All @@ -67,6 +68,7 @@
"execa": "^9.3.1",
"get-latest-version": "5.1.0",
"git-url-parse": "13.1.1",
"inquirer": "^9.3.8",
"nodemon": "^3.1.0",
"ora": "5.4.1",
"outdent": "0.8.0",
Expand All @@ -85,6 +87,7 @@
"@swc/core": "^1.4.13",
"@swc/jest": "^0.2.36",
"@types/git-url-parse": "9.0.3",
"@types/inquirer": "^9.0.9",
"@types/jest": "^29.5.12",
"@types/node": "^22.5.4",
"@types/prettier": "^2.0.0",
Expand Down
925 changes: 925 additions & 0 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/cli/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { command as buildPluginCommand } from './plugin/build';
import { command as generateCommand } from './plugin/generate';
import { command as initPluginCommand } from './plugin/init';
import { command as linkWatchPluginCommand } from './plugin/link-watch';
import { command as verifyPluginCommand } from './plugin/verify';
Expand All @@ -8,6 +9,7 @@ import type { StrapiCommand } from '../../types';

export const commands: StrapiCommand[] = [
buildPluginCommand,
generateCommand,
initPluginCommand,
linkWatchPluginCommand,
watchPluginCommand,
Expand Down
37 changes: 37 additions & 0 deletions src/cli/commands/plugin/generate/actions/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { generate } from '@strapi/generators';
import validateInput from '@strapi/generators/dist/plops/utils/validate-input';
import inquirer from 'inquirer';

import { loadPkg, validatePkg } from '../../../utils/pkg';

import type { CLIContext } from '../../../../../types';

/**
* api generator for Strapi plugins
*/
const action = async ({ ctx: { cwd, logger } }: { ctx: CLIContext }) => {
const pkg = await loadPkg({ cwd, logger });
const validatedPkg = await validatePkg({ pkg });

const config = await inquirer.prompt([
{
type: 'input',
name: 'id',
message: 'API name',
validate: (input) => validateInput(input),
},
]);

generate(
'api',
{
id: config.id,
isPluginApi: true,
destination: 'root',
plugin: validatedPkg.strapi.name,
},
{ dir: 'server' }
);
};

export default action;
40 changes: 40 additions & 0 deletions src/cli/commands/plugin/generate/actions/content-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { generate } from '@strapi/generators';
import bootstrapApiPrompts from '@strapi/generators/dist/plops/prompts/bootstrap-api-prompts';
import ctNamesPrompts from '@strapi/generators/dist/plops/prompts/ct-names-prompts';
import getAttributesPrompts from '@strapi/generators/dist/plops/prompts/get-attributes-prompts';
import kindPrompts from '@strapi/generators/dist/plops/prompts/kind-prompts';
import inquirer from 'inquirer';

import { loadPkg, validatePkg } from '../../../utils/pkg';

import type { CLIContext } from '../../../../../types';

/**
* content-type generator for Strapi plugins
*/
const action = async ({ ctx: { cwd, logger } }: { ctx: CLIContext }) => {
const pkg = await loadPkg({ cwd, logger });
const validatedPkg = await validatePkg({ pkg });

const nameInfo = await inquirer.prompt([...ctNamesPrompts, ...kindPrompts] as any);
const attributes = await getAttributesPrompts(inquirer);
const bootstrapInfo = await inquirer.prompt([...bootstrapApiPrompts] as any);

generate(
'content-type',
{
kind: nameInfo.kind,
singularName: nameInfo.singularName,
id: nameInfo.displayName,
pluralName: nameInfo.pluralName,
displayName: nameInfo.displayName,
destination: 'root',
bootstrapApi: bootstrapInfo.bootstrapApi,
attributes,
plugin: validatedPkg.strapi.name,
},
{ dir: 'server' }
);
};

export default action;
36 changes: 36 additions & 0 deletions src/cli/commands/plugin/generate/actions/controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { generate } from '@strapi/generators';
import validateInput from '@strapi/generators/dist/plops/utils/validate-input';
import inquirer from 'inquirer';

import { loadPkg, validatePkg } from '../../../utils/pkg';

import type { CLIContext } from '../../../../../types';

/**
* controller generator for Strapi plugins
*/
const action = async ({ ctx: { cwd, logger } }: { ctx: CLIContext }) => {
const pkg = await loadPkg({ cwd, logger });
const validatedPkg = await validatePkg({ pkg });

const config = await inquirer.prompt([
{
type: 'input',
name: 'id',
message: 'Controller name',
validate: (input) => validateInput(input),
},
]);

generate(
'controller',
{
id: config.id,
destination: 'root',
plugin: validatedPkg.strapi.name,
},
{ dir: 'server' }
);
};

export default action;
36 changes: 36 additions & 0 deletions src/cli/commands/plugin/generate/actions/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { generate } from '@strapi/generators';
import validateInput from '@strapi/generators/dist/plops/utils/validate-input';
import inquirer from 'inquirer';

import { loadPkg, validatePkg } from '../../../utils/pkg';

import type { CLIContext } from '../../../../../types';

/**
* middleware generator for Strapi plugins
*/
const action = async ({ ctx: { cwd, logger } }: { ctx: CLIContext }) => {
const pkg = await loadPkg({ cwd, logger });
const validatedPkg = await validatePkg({ pkg });

const config = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'Middleware name',
validate: (input) => validateInput(input),
},
]);

generate(
'middleware',
{
name: config.name,
destination: 'root',
plugin: validatedPkg.strapi.name,
},
{ dir: 'server' }
);
};

export default action;
36 changes: 36 additions & 0 deletions src/cli/commands/plugin/generate/actions/policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { generate } from '@strapi/generators';
import validateInput from '@strapi/generators/dist/plops/utils/validate-input';
import inquirer from 'inquirer';

import { loadPkg, validatePkg } from '../../../utils/pkg';

import type { CLIContext } from '../../../../../types';

/**
* policy generator for Strapi plugins
*/
const action = async ({ ctx: { cwd, logger } }: { ctx: CLIContext }) => {
const pkg = await loadPkg({ cwd, logger });
const validatedPkg = await validatePkg({ pkg });

const config = await inquirer.prompt([
{
type: 'input',
name: 'id',
message: 'Policy name',
validate: (input) => validateInput(input),
},
]);

generate(
'policy',
{
id: config.id,
destination: 'root',
plugin: validatedPkg.strapi.name,
},
{ dir: 'server' }
);
};

export default action;
36 changes: 36 additions & 0 deletions src/cli/commands/plugin/generate/actions/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { generate } from '@strapi/generators';
import validateInput from '@strapi/generators/dist/plops/utils/validate-input';
import inquirer from 'inquirer';

import { loadPkg, validatePkg } from '../../../utils/pkg';

import type { CLIContext } from '../../../../../types';

/**
* service generator for Strapi plugins
*/
const action = async ({ ctx: { cwd, logger } }: { ctx: CLIContext }) => {
const pkg = await loadPkg({ cwd, logger });
const validatedPkg = await validatePkg({ pkg });

const config = await inquirer.prompt([
{
type: 'input',
name: 'id',
message: 'Service name',
validate: (input) => validateInput(input),
},
]);

generate(
'service',
{
id: config.id,
destination: 'root',
plugin: validatedPkg.strapi.name,
},
{ dir: 'server' }
);
};

export default action;
65 changes: 65 additions & 0 deletions src/cli/commands/plugin/generate/command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import inquirer from 'inquirer';

import apiAction from './actions/api';
import ctAction from './actions/content-type';
import controllerAction from './actions/controller';
import middlewareAction from './actions/middleware';
import policyAction from './actions/policy';
import serviceAction from './actions/service';

import type { StrapiCommand } from '../../../../types';

/**
* `$ strapi-plugin generate`
*/
const command: StrapiCommand = ({ command: commanderCommand, ctx }) => {
commanderCommand
.command('generate')
.description('Generate some boilerplate code for a Strapi plugin')
.option('-d, --debug', 'Enable debugging mode with verbose logs', false)
.option('--silent', "Don't log anything", false)
.action(async () => {
const options = [
{ name: 'api', value: 'api' },
{ name: 'controller', value: 'controller' },
{ name: 'content-type', value: 'content-type' },
{ name: 'policy', value: 'policy' },
{ name: 'middleware', value: 'middleware' },
{ name: 'service', value: 'service' },
];

const { generator } = await inquirer.prompt([
{
type: 'list',
name: 'generator',
message: 'Strapi Generators',
choices: options,
},
]);

switch (generator) {
case 'api':
apiAction({ ctx });
break;
case 'controller':
controllerAction({ ctx });
break;
case 'content-type':
ctAction({ ctx });
break;
case 'policy':
policyAction({ ctx });
break;
case 'middleware':
middlewareAction({ ctx });
break;
case 'service':
serviceAction({ ctx });
break;
default:
ctx.logger.error('Unknown generator type');
}
});
};

export default command;
1 change: 1 addition & 0 deletions src/cli/commands/plugin/generate/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as command } from './command';
6 changes: 6 additions & 0 deletions src/cli/commands/utils/pkg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ interface Export {

const packageJsonSchema = yup.object({
name: yup.string().required(),
strapi: yup.object({
kind: yup.string().required(),
name: yup.string().required(),
displayName: yup.string().required(),
description: yup.string().optional(),
}),
exports: yup.lazy((value) =>
yup
.object(
Expand Down