feat(strapi): add experimental plugin init command (#18970)

This commit is contained in:
Josh 2023-12-11 09:53:59 +00:00 committed by GitHub
parent 40ef34cbcd
commit 46e6ffbb44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1450 additions and 31 deletions

View File

@ -1,6 +1,6 @@
---
title: plugin:build
description: An in depth look at the plugin:build command of the Strapi CLI
title: plugin:watch
description: An in depth look at the plugin:watch command of the Strapi CLI
tags:
- CLI
- commands

View File

@ -0,0 +1,35 @@
---
title: plugin:init
description: An in depth look at the plugin:init command of the Strapi CLI
tags:
- CLI
- commands
- plugins
- initialization
---
The `plugin:init` command is used to create a plugin, by default in `src/plugins` because this is the strapi CLI we assume we're in a user app by default. This is done by using `pack-up` underneath and a unique template configuration.
## Usage
```bash
strapi plugin:init [path]
```
### Options
```bash
Create a new plugin at a given path.
Options:
-d, --debug Enable debugging mode with verbose logs (default: false)
--silent Don't log anything (default: false)
-h, --help Display help for command
```
## How it works
The command sequence can be visualised as follows:
- Ask the user a series of questions via prompts
- Generate a plugin folder structure based on that template

View File

@ -142,6 +142,8 @@
"dotenv": "14.2.0",
"execa": "5.1.1",
"fs-extra": "10.0.0",
"get-latest-version": "5.1.0",
"git-url-parse": "13.1.0",
"glob": "7.2.3",
"http-errors": "1.8.1",
"https-proxy-agent": "5.0.1",
@ -163,6 +165,7 @@
"node-schedule": "2.1.0",
"open": "8.4.0",
"ora": "5.4.1",
"outdent": "0.8.0",
"package-json": "7.0.0",
"pkg-up": "3.1.0",
"qs": "6.11.1",

View File

@ -1,5 +1,4 @@
import { RenderAdminArgs, renderAdmin, Store } from '@strapi/admin/strapi-admin';
// @ts-expect-error No types, yet.
import contentTypeBuilder from '@strapi/plugin-content-type-builder/strapi-admin';
import email from '@strapi/plugin-email/strapi-admin';
// @ts-expect-error No types, yet.

View File

@ -2,15 +2,18 @@ import boxen from 'boxen';
import chalk from 'chalk';
import { BuildCLIOptions, ConfigBundle, build } from '@strapi/pack-up';
import { notifyExperimentalCommand } from '../../../utils/helpers';
import { createLogger } from '../../../utils/logger';
import { Export, loadPkg, validatePkg } from '../../../utils/pkg';
import { CLIContext } from '../../../types';
interface ActionOptions extends BuildCLIOptions {
force?: boolean;
}
export default async ({ force, ...opts }: ActionOptions) => {
const logger = createLogger({ debug: opts.debug, silent: opts.silent, timestamp: false });
export default async (
{ force, ...opts }: ActionOptions,
_cmd: unknown,
{ logger, cwd }: CLIContext
) => {
try {
/**
* ALWAYS set production for using plugin build CLI.
@ -18,12 +21,10 @@ export default async ({ force, ...opts }: ActionOptions) => {
process.env.NODE_ENV = 'production';
/**
* Notify users this is an experimental command and get them to approve first
* this can be opted out by setting the argument --yes
* this can be opted out by setting the argument --force
*/
await notifyExperimentalCommand('plugin:build', { force });
const cwd = process.cwd();
const pkg = await loadPkg({ cwd, logger });
const pkgJson = await validatePkg({ pkg });

View File

@ -6,7 +6,7 @@ import action from './action';
/**
* `$ strapi plugin:build`
*/
const command: StrapiCommand = ({ command }) => {
const command: StrapiCommand = ({ command, ctx }) => {
command
.command('plugin:build')
.description('Bundle your strapi plugin for publishing.')
@ -15,7 +15,7 @@ const command: StrapiCommand = ({ command }) => {
.option('--silent', "Don't log anything", false)
.option('--sourcemap', 'produce sourcemaps', false)
.option('--minify', 'minify the output', false)
.action(runAction('plugin:build', action));
.action((...args) => runAction('plugin:build', action)(...args, ctx));
};
export default command;

View File

@ -0,0 +1,545 @@
import path from 'node:path';
import boxen from 'boxen';
import chalk from 'chalk';
import getLatestVersion from 'get-latest-version';
import gitUrlParse from 'git-url-parse';
import {
InitOptions,
definePackageFeature,
definePackageOption,
defineTemplate,
init,
TemplateFile,
} from '@strapi/pack-up';
import { outdent } from 'outdent';
import { notifyExperimentalCommand } from '../../../utils/helpers';
import { CLIContext } from '../../../types';
import { gitIgnoreFile } from './files/gitIgnore';
interface ActionOptions extends Pick<InitOptions, 'silent' | 'debug'> {}
export default async (
packagePath: string,
{ silent, debug }: ActionOptions,
_cmd: unknown,
{ logger, cwd }: CLIContext
) => {
try {
/**
* Notify users this is an experimental command. We don't need to get them to approve first.
*/
await notifyExperimentalCommand('plugin:init', { force: true });
/**
* Create the package // plugin
*/
await init({
path: packagePath,
cwd,
silent,
debug,
template: PLUGIN_TEMPLATE,
});
logger.info("Don't forget to enable your plugin in your configuration files.");
} catch (err) {
logger.error(
'There seems to be an unexpected error, try again with --debug for more information \n'
);
if (err instanceof Error && err.stack) {
logger.log(
chalk.red(
boxen(err.stack, {
padding: 1,
align: 'left',
})
)
);
}
process.exit(1);
}
};
const PACKAGE_NAME_REGEXP = /^(?:@(?:[a-z0-9-*~][a-z0-9-*._~]*)\/)?[a-z0-9-~][a-z0-9-._~]*$/i;
interface PackageExport {
types?: string;
require: string;
import: string;
source: string;
default: string;
}
interface PluginPackageJson {
name?: string;
description?: string;
version?: string;
keywords?: string[];
type: 'commonjs';
license?: string;
repository?: {
type: 'git';
url: string;
};
bugs?: {
url: string;
};
homepage?: string;
author?: string;
exports: {
'./strapi-admin'?: PackageExport;
'./strapi-server'?: PackageExport;
'./package.json': `${string}.json`;
};
files: string[];
scripts: Record<string, string>;
dependencies: Record<string, string>;
devDependencies: Record<string, string>;
peerDependencies: Record<string, string>;
strapi: {
name?: string;
displayName?: string;
description?: string;
kind: 'plugin';
};
}
const PLUGIN_TEMPLATE = defineTemplate(async ({ logger, gitConfig, packagePath }) => {
let repo: {
source?: string;
owner?: string;
name?: string;
};
const [packageFolder] = packagePath.split(path.sep).slice(-1);
return {
prompts: [
definePackageOption({
name: 'repo',
type: 'text',
message: 'git url',
validate(v) {
if (!v) {
return true;
}
try {
const result = gitUrlParse(v);
repo = { source: result.source, owner: result.owner, name: result.name };
return true;
} catch (err) {
return 'invalid git url';
}
},
}),
definePackageOption({
name: 'pkgName',
type: 'text',
message: 'plugin name',
initial: () => repo?.name ?? '',
validate(v) {
if (!v) {
return 'package name is required';
}
const match = PACKAGE_NAME_REGEXP.exec(v);
if (!match) {
return 'invalid package name';
}
return true;
},
}),
definePackageOption({
name: 'displayName',
type: 'text',
message: 'plugin display name',
}),
definePackageOption({
name: 'description',
type: 'text',
message: 'plugin description',
}),
definePackageOption({
name: 'authorName',
type: 'text',
message: 'plugin author name',
initial: gitConfig?.user?.name,
}),
definePackageOption({
name: 'authorEmail',
type: 'text',
message: 'plugin author email',
initial: gitConfig?.user?.email,
}),
definePackageOption({
name: 'license',
type: 'text',
message: 'plugin license',
initial: 'MIT',
validate(v) {
if (!v) {
return 'license is required';
}
return true;
},
}),
definePackageOption({
name: 'client-code',
type: 'confirm',
message: 'register with the admin panel?',
initial: true,
}),
definePackageOption({
name: 'server-code',
type: 'confirm',
message: 'register with the server?',
initial: true,
}),
definePackageFeature({
name: 'editorconfig',
initial: true,
optional: true,
}),
definePackageFeature({
name: 'eslint',
initial: true,
optional: true,
}),
definePackageFeature({
name: 'prettier',
initial: true,
optional: true,
}),
definePackageFeature({
name: 'typescript',
initial: true,
optional: true,
}),
],
async getFiles(answers) {
const author: string[] = [];
const files: TemplateFile[] = [];
// package.json
const pkgJson: PluginPackageJson = {
version: '0.0.0',
keywords: [],
type: 'commonjs',
exports: {
'./package.json': './package.json',
},
files: ['dist'],
scripts: {
build: 'strapi plugin:build',
watch: 'strapi plugin:watch',
},
dependencies: {},
devDependencies: {
/**
* We set * as a default version, but further down
* we try to resolve each package to their latest
* version, failing that we leave the fallback of *.
*/
'@strapi/strapi': '*',
prettier: '*',
},
peerDependencies: {
'@strapi/strapi': '^4.0.0',
},
strapi: {
kind: 'plugin',
},
};
if (Array.isArray(answers)) {
for (const ans of answers) {
const { name, answer } = ans;
switch (name) {
case 'pkgName': {
pkgJson.name = String(answer);
pkgJson.strapi.name = String(answer);
break;
}
case 'description': {
pkgJson.description = String(answer) ?? undefined;
pkgJson.strapi.description = String(answer) ?? undefined;
break;
}
case 'displayName': {
pkgJson.strapi.displayName = String(answer) ?? undefined;
break;
}
case 'authorName': {
author.push(String(answer));
break;
}
case 'authorEmail': {
if (answer) {
author.push(`<${answer}>`);
}
break;
}
case 'license': {
pkgJson.license = String(answer);
break;
}
case 'client-code': {
if (answer) {
pkgJson.exports['./strapi-admin'] = {
source: './src/admin/index.js',
import: './dist/admin/index.mjs',
require: './dist/admin/index.js',
default: './dist/admin/index.js',
};
pkgJson.dependencies = {
...pkgJson.dependencies,
'@strapi/helper-plugin': '*',
'@strapi/design-system': '*',
'@strapi/icons': '*',
};
pkgJson.devDependencies = {
...pkgJson.dependencies,
react: '*',
'react-dom': '*',
'react-router-dom': '5.3.4',
'styled-components': '5.3.3',
};
pkgJson.peerDependencies = {
...pkgJson.peerDependencies,
react: '^17.0.0 || ^18.0.0',
'react-dom': '^17.0.0 || ^18.0.0',
'react-router-dom': '5.2.0',
'styled-components': '5.2.1',
};
}
break;
}
case 'server-code': {
if (answer) {
pkgJson.exports['./strapi-server'] = {
source: './src/server/index.js',
import: './dist/server/index.mjs',
require: './dist/server/index.js',
default: './dist/server/index.js',
};
pkgJson.files.push('./strapi-server.js');
files.push({
name: 'strapi-server.js',
contents: outdent`
'use strict';
module.exports = require('./dist/server');
`,
});
}
break;
}
case 'typescript': {
const isTypescript = Boolean(answer);
if (isTypescript) {
if (isRecord(pkgJson.exports['./strapi-admin'])) {
pkgJson.exports['./strapi-admin'].source = './src/admin/index.ts';
pkgJson.exports['./strapi-admin'] = {
types: './dist/admin/src/index.d.ts',
...pkgJson.exports['./strapi-admin'],
};
pkgJson.scripts = {
...pkgJson.scripts,
'test:ts:front': 'run -T tsc -p admin/tsconfig.json',
};
pkgJson.devDependencies = {
...pkgJson.devDependencies,
'@types/react': '*',
'@types/react-dom': '*',
'@types/react-router-dom': '5.3.3',
'@types/styled-components': '5.1.26',
};
const { adminTsconfigFiles } = await import('./files/typescript');
files.push(adminTsconfigFiles.tsconfigBuildFile, adminTsconfigFiles.tsconfigFile);
}
if (isRecord(pkgJson.exports['./strapi-server'])) {
pkgJson.exports['./strapi-server'].source = './src/server/index.ts';
pkgJson.exports['./strapi-server'] = {
types: './dist/server/src/index.d.ts',
...pkgJson.exports['./strapi-server'],
};
pkgJson.scripts = {
...pkgJson.scripts,
'test:ts:back': 'run -T tsc -p server/tsconfig.json',
};
const { serverTsconfigFiles } = await import('./files/typescript');
files.push(
serverTsconfigFiles.tsconfigBuildFile,
serverTsconfigFiles.tsconfigFile
);
}
pkgJson.devDependencies = {
...pkgJson.devDependencies,
'@strapi/typescript-utils': '*',
typescript: '*',
};
}
/**
* This is where we add all the source files regardless
* of whether they are typescript or javascript.
*/
if (isRecord(pkgJson.exports['./strapi-admin'])) {
files.push({
name: isTypescript ? 'admin/src/pluginId.ts' : 'admin/src/pluginId.js',
contents: outdent`
export const PLUGIN_ID = '${pkgJson.name!.replace(/^strapi-plugin-/i, '')}';
`,
});
if (isTypescript) {
const { adminTypescriptFiles } = await import('./files/admin');
files.push(...adminTypescriptFiles);
} else {
const { adminJavascriptFiles } = await import('./files/admin');
files.push(...adminJavascriptFiles);
}
}
if (isRecord(pkgJson.exports['./strapi-server'])) {
if (isTypescript) {
const { serverTypescriptFiles } = await import('./files/server');
files.push(...serverTypescriptFiles(packageFolder));
} else {
const { serverJavascriptFiles } = await import('./files/server');
files.push(...serverJavascriptFiles(packageFolder));
}
}
break;
}
case 'eslint': {
if (answer) {
const { eslintIgnoreFile } = await import('./files/eslint');
files.push(eslintIgnoreFile);
}
break;
}
case 'prettier': {
if (answer) {
const { prettierFile, prettierIgnoreFile } = await import('./files/prettier');
files.push(prettierFile, prettierIgnoreFile);
}
break;
}
case 'editorconfig': {
if (answer) {
const { editorConfigFile } = await import('./files/editorConfig');
files.push(editorConfigFile);
}
break;
}
default:
break;
}
}
}
if (repo) {
pkgJson.repository = {
type: 'git',
url: `git+ssh://git@${repo.source}/${repo.owner}/${repo.name}.git`,
};
pkgJson.bugs = {
url: `https://${repo.source}/${repo.owner}/${repo.name}/issues`,
};
pkgJson.homepage = `https://${repo.source}/${repo.owner}/${repo.name}#readme`;
}
pkgJson.author = author.filter(Boolean).join(' ') ?? undefined;
try {
pkgJson.devDependencies = await resolveLatestVerisonOfDeps(pkgJson.devDependencies);
pkgJson.dependencies = await resolveLatestVerisonOfDeps(pkgJson.dependencies);
pkgJson.peerDependencies = await resolveLatestVerisonOfDeps(pkgJson.peerDependencies);
} catch (err) {
if (err instanceof Error) {
logger.error(err.message);
} else {
logger.error(err);
}
}
files.push({
name: 'package.json',
contents: outdent`
${JSON.stringify(pkgJson, null, 2)}
`,
});
files.push({
name: 'README.md',
contents: outdent`
# ${pkgJson.name}
${pkgJson.description ?? ''}
`,
});
files.push(gitIgnoreFile);
return files;
},
};
});
const isRecord = (value: unknown): value is Record<string, unknown> =>
Boolean(value) && !Array.isArray(value) && typeof value === 'object';
const resolveLatestVerisonOfDeps = async (
deps: Record<string, string>
): Promise<Record<string, string>> => {
const latestDeps: Record<string, string> = {};
for (const [name, version] of Object.entries(deps)) {
try {
const latestVersion = await getLatestVersion(name, version);
latestDeps[name] = latestVersion ? `^${latestVersion}` : '*';
} catch (err) {
latestDeps[name] = '*';
}
}
return latestDeps;
};

View File

@ -0,0 +1,18 @@
import type { StrapiCommand } from '../../../types';
import { runAction } from '../../../utils/helpers';
import action from './action';
/**
* `$ strapi plugin:init`
*/
const command: StrapiCommand = ({ command, ctx }) => {
command
.command('plugin:init')
.description('Create a new plugin at a given path')
.argument('[path]', 'path to the plugin', './src/plugins/my-plugin')
.option('-d, --debug', 'Enable debugging mode with verbose logs', false)
.option('--silent', "Don't log anything", false)
.action((...args) => runAction('plugin:init', action)(...args, ctx));
};
export default command;

View File

@ -0,0 +1,286 @@
import { TemplateFile } from '@strapi/pack-up';
import { outdent } from 'outdent';
const PLUGIN_ICON_CODE = outdent`
import { Puzzle } from '@strapi/icons';
const PluginIcon = () => <Puzzle />;
export { PluginIcon };
`;
const APP_CODE = outdent`
import { AnErrorOccurred } from '@strapi/helper-plugin';
import { Switch, Route } from 'react-router-dom';
import { PLUGIN_ID } from '../pluginId';
import { HomePage } from './HomePage';
const App = () => {
return (
<Switch>
<Route path={\`/plugins/\${PLUGIN_ID}\`} component={HomePage} exact />
<Route component={AnErrorOccurred} />
</Switch>
);
};
export { App };
`;
const HOMEPAGE_CODE = outdent`
import { Main } from '@strapi/design-system';
import { useIntl } from 'react-intl';
import { getTranslation } from '../utils/getTranslation';
const HomePage = () => {
const { formatMessage } = useIntl();
return (
<Main>
<h1>Welcome to {formatMessage({ id: getTranslation("plugin.name") })}</h1>
</Main>
)
}
export { HomePage };
`;
const TYPESCRIPT: TemplateFile[] = [
{
name: 'admin/src/index.ts',
contents: outdent`
import { prefixPluginTranslations } from '@strapi/helper-plugin';
import { PLUGIN_ID } from './pluginId';
import { Initializer } from './components/Initializer';
import { PluginIcon } from './components/PluginIcon';
export default {
register(app: any) {
app.addMenuLink({
to: \`/plugins/\${PluginIcon}\`,
icon: PluginIcon,
intlLabel: {
id: \`\${PLUGIN_ID}.plugin.name\`,
defaultMessage: PLUGIN_ID,
},
Component: async () => {
const { App } = await import('./pages/App');
return App;
},
});
app.registerPlugin({
id: PLUGIN_ID,
initializer: Initializer,
isReady: false,
name: PLUGIN_ID,
});
},
async registerTrads(app: any) {
const { locales } = app;
const importedTranslations = await Promise.all(
(locales as string[]).map((locale) => {
return import(\`./translations/\${locale}.json\`)
.then(({ default: data }) => {
return {
data: prefixPluginTranslations(data, PLUGIN_ID),
locale,
};
})
.catch(() => {
return {
data: {},
locale,
};
});
})
);
return importedTranslations;
},
};
`,
},
{
name: 'admin/src/components/PluginIcon.tsx',
contents: PLUGIN_ICON_CODE,
},
{
name: 'admin/src/components/Initializer.tsx',
contents: outdent`
import { useEffect, useRef } from 'react';
import { PLUGIN_ID } from '../pluginId';
type InitializerProps = {
setPlugin: (id: string) => void;
};
const Initializer = ({ setPlugin }: InitializerProps) => {
const ref = useRef(setPlugin);
useEffect(() => {
ref.current(PLUGIN_ID);
}, []);
return null;
};
export { Initializer };
`,
},
{
name: 'admin/src/pages/App.tsx',
contents: APP_CODE,
},
{
name: 'admin/src/pages/HomePage.tsx',
contents: HOMEPAGE_CODE,
},
{
name: 'admin/src/utils/getTranslation.ts',
contents: outdent`
import { PLUGIN_ID } from '../pluginId';
const getTranslation = (id: string) => \`\${PLUGIN_ID}.\${id}\`;
export { getTranslation };
`,
},
{
name: 'admin/src/translations/en.json',
contents: outdent`
{}
`,
},
{
/**
* TODO: remove this when we release design-system V2
*/
name: 'admin/custom.d.ts',
contents: outdent`
declare module '@strapi/design-system/*';
declare module '@strapi/design-system';
`,
},
];
const JAVASCRIPT: TemplateFile[] = [
{
name: 'admin/src/index.js',
contents: outdent`
import { prefixPluginTranslations } from '@strapi/helper-plugin';
import { PLUGIN_ID } from './pluginId';
import { Initializer } from './components/Initializer';
import { PluginIcon } from './components/PluginIcon';
export default {
register(app) {
app.addMenuLink({
to: \`/plugins/\${PluginIcon}\`,
icon: PluginIcon,
intlLabel: {
id: \`\${PLUGIN_ID}.plugin.name\`,
defaultMessage: PLUGIN_ID,
},
Component: async () => {
const { App } = await import('./pages/App');
return App;
},
});
app.registerPlugin({
id: PLUGIN_ID,
initializer: Initializer,
isReady: false,
name: PLUGIN_ID,
});
},
async registerTrads(app) {
const { locales } = app;
const importedTranslations = await Promise.all(
locales.map((locale) => {
return import(\`./translations/\${locale}.json\`)
.then(({ default: data }) => {
return {
data: prefixPluginTranslations(data, PLUGIN_ID),
locale,
};
})
.catch(() => {
return {
data: {},
locale,
};
});
})
);
return importedTranslations;
},
};
`,
},
{
name: 'admin/src/components/PluginIcon.jsx',
contents: PLUGIN_ICON_CODE,
},
{
name: 'admin/src/components/Initializer.jsx',
contents: outdent`
import { useEffect, useRef } from 'react';
import { PLUGIN_ID } from '../pluginId';
/**
* @type {import('react').FC<{ setPlugin: (id: string) => void }>}
*/
const Initializer = ({ setPlugin }) => {
const ref = useRef(setPlugin);
useEffect(() => {
ref.current(PLUGIN_ID);
}, []);
return null;
};
export { Initializer };
`,
},
{
name: 'admin/src/pages/App.jsx',
contents: APP_CODE,
},
{
name: 'admin/src/pages/HomePage.jsx',
contents: HOMEPAGE_CODE,
},
{
name: 'admin/src/utils/getTranslation.js',
contents: outdent`
import { PLUGIN_ID } from '../pluginId';
const getTranslation = (id) => \`\${PLUGIN_ID}.\${id}\`;
export { getTranslation };
`,
},
{
name: 'admin/src/translations/en.json',
contents: outdent`
{}
`,
},
];
export { TYPESCRIPT as adminTypescriptFiles, JAVASCRIPT as adminJavascriptFiles };

View File

@ -0,0 +1,26 @@
import { TemplateFile } from '@strapi/pack-up';
import { outdent } from 'outdent';
const editorConfigFile: TemplateFile = {
name: '.editorconfig',
contents: outdent`
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[{package.json,*.yml}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
`,
};
export { editorConfigFile };

View File

@ -0,0 +1,11 @@
import { TemplateFile } from '@strapi/pack-up';
import { outdent } from 'outdent';
const eslintIgnoreFile: TemplateFile = {
name: '.eslintignore',
contents: outdent`
dist
`,
};
export { eslintIgnoreFile };

View File

@ -0,0 +1,34 @@
import { TemplateFile } from '@strapi/pack-up';
import { outdent } from 'outdent';
const gitIgnoreFile: TemplateFile = {
name: '.gitignore',
contents: outdent`
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules
.pnp
.pnp.js
# testing
coverage
# production
dist
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env
`,
};
export { gitIgnoreFile };

View File

@ -0,0 +1,25 @@
import { TemplateFile } from '@strapi/pack-up';
import { outdent } from 'outdent';
const prettierFile: TemplateFile = {
name: '.prettierrc',
contents: outdent`
{
"endOfLine": 'lf',
"tabWidth": 2,
"printWidth": 100,
"singleQuote": true,
"trailingComma": 'es5',
}
`,
};
const prettierIgnoreFile: TemplateFile = {
name: '.prettierignore',
contents: outdent`
dist
coverage
`,
};
export { prettierFile, prettierIgnoreFile };

View File

@ -0,0 +1,360 @@
import { TemplateFile } from '@strapi/pack-up';
import { outdent } from 'outdent';
const TYPESCRIPT = (pluginName: string): TemplateFile[] => [
{
name: 'server/src/index.ts',
contents: outdent`
/**
* Application methods
*/
import bootstrap from './bootstrap';
import destroy from './destroy';
import register from './register';
/**
* Plugin server methods
*/
import config from './config';
import contentTypes from './content-types';
import controllers from './controllers';
import middlewares from './middlewares';
import policies from './policies';
import routes from './routes';
import services from './services';
export default {
bootstrap,
destroy,
register,
config,
controllers,
contentTypes,
middlewares,
policies,
routes,
services,
};
`,
},
{
name: 'server/src/bootstrap.ts',
contents: outdent`
import type { Strapi } from '@strapi/strapi';
const bootstrap = ({ strapi }: { strapi: Strapi }) => {
// bootstrap phase
};
export default bootstrap;
`,
},
{
name: 'server/src/destroy.ts',
contents: outdent`
import type { Strapi } from '@strapi/strapi';
const destroy = ({ strapi }: { strapi: Strapi }) => {
// destroy phase
};
export default destroy;
`,
},
{
name: 'server/src/register.ts',
contents: outdent`
import type { Strapi } from '@strapi/strapi';
const register = ({ strapi }: { strapi: Strapi }) => {
// register phase
};
export default register;
`,
},
{
name: 'server/src/config/index.ts',
contents: outdent`
export default {
default: {},
validator() {},
};
`,
},
{
name: 'server/src/content-types/index.ts',
contents: outdent`
export default {};
`,
},
{
name: 'server/src/controllers/index.ts',
contents: outdent`
import controller from './controller';
export default {
controller,
};
`,
},
{
name: 'server/src/controllers/controller.ts',
contents: outdent`
import type { Strapi } from '@strapi/strapi';
const controller = ({ strapi }: { strapi: Strapi }) => ({
index(ctx) {
ctx.body = strapi
.plugin('${pluginName}')
// the name of the service file & the method.
.service('service')
.getWelcomeMessage();
},
});
export default controller
`,
},
{
name: 'server/src/middlewares/index.ts',
contents: outdent`
export default {};
`,
},
{
name: 'server/src/policies/index.ts',
contents: outdent`
export default {};
`,
},
{
name: 'server/src/routes/index.ts',
contents: outdent`
export default [
{
method: 'GET',
path: '/',
// name of the controller file & the method.
handler: 'controller.index',
config: {
policies: [],
},
},
];
`,
},
{
name: 'server/src/services/index.ts',
contents: outdent`
import service from './service';
export default {
service,
};
`,
},
{
name: 'server/src/services/service.ts',
contents: outdent`
import type { Strapi } from '@strapi/strapi';
const service = ({ strapi }: { strapi: Strapi }) => ({
getWelcomeMessage() {
return 'Welcome to Strapi 🚀';
},
});
export default service
`,
},
];
const JAVASCRIPT = (pluginName: string): TemplateFile[] => [
{
name: 'server/src/index.ts',
contents: outdent`
'use strict';
/**
* Application methods
*/
const bootstrap = require('./bootstrap');
const destroy = require('./destroy');
const register = require('./register');
/**
* Plugin server methods
*/
const config = require('./config');
const contentTypes = require('./content-types');
const controllers = require('./controllers');
const middlewares = require('./middlewares');
const policies = require('./policies');
const routes = require('./routes');
const services = require('./services');
module.exports = {
bootstrap,
destroy,
register,
config,
controllers,
contentTypes,
middlewares,
policies,
routes,
services,
};
`,
},
{
name: 'server/src/bootstrap.ts',
contents: outdent`
'use strict';
const bootstrap = ({ strapi }) => {
// bootstrap phase
};
module.exports = bootstrap;
`,
},
{
name: 'server/src/destroy.ts',
contents: outdent`
'use strict';
const destroy = ({ strapi }) => {
// destroy phase
};
module.exports = destroy;
`,
},
{
name: 'server/src/register.ts',
contents: outdent`
'use strict';
const register = ({ strapi }) => {
// register phase
};
module.exports = register;
`,
},
{
name: 'server/src/config/index.ts',
contents: outdent`
'use strict';
module.exports = {
default: {},
validator() {},
};
`,
},
{
name: 'server/src/content-types/index.ts',
contents: outdent`
'use strict';
module.exports = {};
`,
},
{
name: 'server/src/controllers/index.ts',
contents: outdent`
'use strict';
const controller = require('./controller');
module.exports = {
controller,
};
`,
},
{
name: 'server/src/controllers/controller.ts',
contents: outdent`
'use strict';
const controller = ({ strapi }) => ({
index(ctx) {
ctx.body = strapi
.plugin('${pluginName}')
// the name of the service file & the method.
.service('service')
.getWelcomeMessage();
},
});
module.exports = controller
`,
},
{
name: 'server/src/middlewares/index.ts',
contents: outdent`
'use strict';
module.exports = {};
`,
},
{
name: 'server/src/policies/index.ts',
contents: outdent`
'use strict';
module.exports = {};
`,
},
{
name: 'server/src/routes/index.ts',
contents: outdent`
'use strict';
module.exports = [
{
method: 'GET',
path: '/',
// name of the controller file & the method.
handler: 'controller.index',
config: {
policies: [],
},
},
];
`,
},
{
name: 'server/src/services/index.ts',
contents: outdent`
'use strict';
const service = require('./service');
module.exports = {
service,
};
`,
},
{
name: 'server/src/services/service.ts',
contents: outdent`
'use strict';
const service = ({ strapi }) => ({
getWelcomeMessage() {
return 'Welcome to Strapi 🚀';
},
});
module.exports = service
`,
},
];
export { TYPESCRIPT as serverTypescriptFiles, JAVASCRIPT as serverJavascriptFiles };

View File

@ -0,0 +1,71 @@
import { TemplateFile } from '@strapi/pack-up';
import { outdent } from 'outdent';
interface TsConfigFiles {
tsconfigFile: TemplateFile;
tsconfigBuildFile: TemplateFile;
}
const ADMIN: TsConfigFiles = {
tsconfigFile: {
name: 'admin/tsconfig.json',
contents: outdent`
{
"extends": "@strapi/typescript-utils/tsconfigs/admin",
"include": ["./src", "./custom.d.ts"],
"compilerOptions": {
"rootDir": "../",
"baseUrl": ".",
},
}
`,
},
tsconfigBuildFile: {
name: 'admin/tsconfig.build.json',
contents: outdent`
{
"extends": "./tsconfig",
"include": ["./src", "./custom.d.ts"],
"exclude": ["**/*.test.ts", "**/*.test.tsx"],
"compilerOptions": {
"rootDir": "../",
"baseUrl": ".",
"outDir": "./dist",
}
}
`,
},
};
const SERVER: TsConfigFiles = {
tsconfigFile: {
name: 'server/tsconfig.json',
contents: outdent`
{
"extends": "@strapi/typescript-utils/tsconfigs/server",
"include": ["./src"],
"compilerOptions": {
"rootDir": "../",
"baseUrl": ".",
},
}
`,
},
tsconfigBuildFile: {
name: 'server/tsconfig.build.json',
contents: outdent`
{
"extends": "./tsconfig",
"include": ["./src"],
"exclude": ["**/*.test.ts"],
"compilerOptions": {
"rootDir": "../",
"baseUrl": ".",
"outDir": "./dist",
}
}
`,
},
};
export { ADMIN as adminTsconfigFiles, SERVER as serverTsconfigFiles };

View File

@ -2,23 +2,17 @@ import boxen from 'boxen';
import chalk from 'chalk';
import { ConfigBundle, WatchCLIOptions, watch } from '@strapi/pack-up';
import { notifyExperimentalCommand } from '../../../utils/helpers';
import { createLogger } from '../../../utils/logger';
import { Export, loadPkg, validatePkg } from '../../../utils/pkg';
import { CLIContext } from '../../../types';
interface ActionOptions extends WatchCLIOptions {
force?: boolean;
}
interface ActionOptions extends WatchCLIOptions {}
export default async ({ force, ...opts }: ActionOptions) => {
const logger = createLogger({ debug: opts.debug, silent: opts.silent, timestamp: false });
export default async (opts: ActionOptions, _cmd: unknown, { cwd, logger }: CLIContext) => {
try {
/**
* Notify users this is an experimental command and get them to approve first
* this can be opted out by setting the argument --yes
* Notify users this is an experimental command.
*/
await notifyExperimentalCommand('plugin:watch', { force });
const cwd = process.cwd();
await notifyExperimentalCommand('plugin:watch', { force: true });
const pkg = await loadPkg({ cwd, logger });
const pkgJson = await validatePkg({ pkg });

View File

@ -3,15 +3,15 @@ import { runAction } from '../../../utils/helpers';
import action from './action';
/**
* `$ strapi plugin:build`
* `$ strapi plugin:watch`
*/
const command: StrapiCommand = ({ command }) => {
const command: StrapiCommand = ({ command, ctx }) => {
command
.command('plugin:watch')
.description('Watch & compile your strapi plugin for local development.')
.option('-d, --debug', 'Enable debugging mode with verbose logs', false)
.option('--silent', "Don't log anything", false)
.action(runAction('plugin:watch', action));
.action((...args) => runAction('plugin:watch', action)(...args, ctx));
};
export default command;

View File

@ -27,6 +27,7 @@ import versionCommand from './actions/version/command';
import watchAdminCommand from './actions/watch-admin/command';
import buildPluginCommand from './actions/plugin/build-command/command';
import initPluginCommand from './actions/plugin/init/command';
import watchPluginCommand from './actions/plugin/watch/command';
import { createLogger } from './utils/logger';
@ -63,6 +64,7 @@ const strapiCommands = {
* Plugins
*/
buildPluginCommand,
initPluginCommand,
watchPluginCommand,
} as const;

View File

@ -1,7 +1,9 @@
{
"extends": "tsconfig/base.json",
"compilerOptions": {
"noEmit": true
"noEmit": true,
"moduleResolution": "Bundler",
"module": "ESNext"
},
"include": ["src"],
"exclude": ["node_modules"]

View File

@ -19,4 +19,5 @@ export type {
TemplateOrTemplateResolver,
TemplateFeature,
TemplateOption,
TemplateFile,
} from './node/templates/types';

View File

@ -58,3 +58,4 @@ const parseGlobalGitConfig = async (): Promise<GitConfig | null> => {
};
export { parseGlobalGitConfig };
export type { GitConfig };

View File

@ -5,6 +5,7 @@ import prettier, { Config as PrettierConfig } from 'prettier';
import prompts from 'prompts';
import { isError } from '../core/errors';
import { parseGlobalGitConfig } from '../core/git';
import { Logger } from '../core/logger';
import { Template, TemplateFeature, TemplateOption, TemplateOrTemplateResolver } from './types';
@ -27,9 +28,11 @@ const createPackageFromTemplate = async (
) => {
const { cwd, logger, template: templateOrResolver } = opts;
const gitConfig = await parseGlobalGitConfig();
const template =
typeof templateOrResolver === 'function'
? await templateOrResolver({ cwd, logger, packagePath })
? await templateOrResolver({ cwd, logger, packagePath, gitConfig })
: templateOrResolver;
logger.info('Creating a new package at: ', relative(cwd, packagePath));

View File

@ -3,7 +3,6 @@ import gitUrlParse from 'git-url-parse';
import { outdent } from 'outdent';
import { isError } from '../../core/errors';
import { parseGlobalGitConfig } from '../../core/git';
import { PackageJson } from '../../core/pkg';
import { definePackageFeature, definePackageOption, defineTemplate } from '../create';
import { TemplateFile } from '../types';
@ -14,9 +13,7 @@ import { prettierFile, prettierIgnoreFile } from './files/prettier';
const PACKAGE_NAME_REGEXP = /^(?:@(?:[a-z0-9-*~][a-z0-9-*._~]*)\/)?[a-z0-9-~][a-z0-9-._~]*$/i;
const defaultTemplate = defineTemplate(async ({ logger }) => {
const gitConfig = await parseGlobalGitConfig();
const defaultTemplate = defineTemplate(async ({ logger, gitConfig }) => {
let repo: {
source?: string;
owner?: string;

View File

@ -1,5 +1,6 @@
import { PromptObject } from 'prompts';
import { GitConfig } from '../core/git';
import { Logger } from '../core/logger';
interface TemplateFeature<T extends string = string> extends Pick<PromptObject<T>, 'initial'> {
@ -45,6 +46,7 @@ interface Template {
interface TemplateContext {
cwd: string;
gitConfig: GitConfig | null;
logger: Logger;
packagePath: string;
}

View File

@ -8476,6 +8476,8 @@ __metadata:
eslint-config-custom: "npm:4.15.5"
execa: "npm:5.1.1"
fs-extra: "npm:10.0.0"
get-latest-version: "npm:5.1.0"
git-url-parse: "npm:13.1.0"
glob: "npm:7.2.3"
http-errors: "npm:1.8.1"
https-proxy-agent: "npm:5.0.1"
@ -8497,6 +8499,7 @@ __metadata:
node-schedule: "npm:2.1.0"
open: "npm:8.4.0"
ora: "npm:5.4.1"
outdent: "npm:0.8.0"
package-json: "npm:7.0.0"
pkg-up: "npm:3.1.0"
qs: "npm:6.11.1"