@grafana/create-plugin 2.10.1 → 2.11.0-canary.602.81e7a0e.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/commands/{generate-actions → generate}/prettify-files.js +8 -11
  2. package/dist/commands/{generate-actions → generate}/print-success-message.js +2 -2
  3. package/dist/commands/generate/prompt-user.js +145 -0
  4. package/dist/commands/{generate-actions → generate}/update-go-sdk-and-packages.js +10 -13
  5. package/dist/commands/generate.command.js +225 -9
  6. package/dist/utils/utils.console.js +5 -2
  7. package/dist/utils/utils.files.js +58 -1
  8. package/dist/utils/utils.handlebars.js +14 -3
  9. package/package.json +22 -15
  10. package/src/commands/{generate-actions → generate}/prettify-files.ts +1 -5
  11. package/src/commands/{generate-actions → generate}/print-success-message.ts +2 -3
  12. package/src/commands/generate/prompt-user.ts +102 -0
  13. package/src/commands/{generate-actions → generate}/update-go-sdk-and-packages.ts +1 -6
  14. package/src/commands/generate.command.ts +194 -11
  15. package/src/utils/utils.console.ts +5 -2
  16. package/src/utils/utils.files.ts +10 -0
  17. package/src/utils/utils.handlebars.ts +17 -4
  18. package/templates/app/README.md +3 -3
  19. package/templates/datasource/README.md +3 -3
  20. package/templates/panel/README.md +2 -2
  21. package/templates/scenes-app/README.md +2 -2
  22. package/dist/commands/generate.plopfile.js +0 -214
  23. package/scripts/generate-app-backend.js +0 -29
  24. package/scripts/generate-app.js +0 -29
  25. package/scripts/generate-datasource-with-backend.js +0 -29
  26. package/scripts/generate-datasource-without-backend.js +0 -29
  27. package/scripts/generate-panel.js +0 -28
  28. package/scripts/generate-scenes-app.js +0 -29
  29. package/src/commands/generate.plopfile.ts +0 -272
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grafana/create-plugin",
3
- "version": "2.10.1",
3
+ "version": "2.11.0-canary.602.81e7a0e.0",
4
4
  "main": "index.js",
5
5
  "repository": {
6
6
  "directory": "packages/create-plugin",
@@ -17,17 +17,17 @@
17
17
  "clean": "rm -rf ./dist ./generated",
18
18
  "clean-generated": "find ./generated -not -path \"./generated/node_modules*\" -not -path \"./generated\" -not -path \"./generated/package-lock.json\" -maxdepth 1 -print0 | xargs -0 -I {} rm -rf {}",
19
19
  "build": "npm run clean && tsc && chmod +x ./dist/bin/run.js",
20
- "dev": "CREATE_PLUGIN_DEV=true nodemon --exec 'tsc'",
21
- "dev-app": "CREATE_PLUGIN_DEV=true nodemon --exec 'npm run generate-app'",
22
- "dev-scenes-app": "CREATE_PLUGIN_DEV=true nodemon --exec 'npm run generate-scenes-app'",
23
- "dev-panel": "CREATE_PLUGIN_DEV=true nodemon --exec 'npm run generate-panel'",
24
- "dev-datasource": "CREATE_PLUGIN_DEV=true nodemon --exec 'npm run generate-datasource'",
25
- "generate-app": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./scripts/generate-app.js",
26
- "generate-scenes-app": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./scripts/generate-scenes-app.js",
27
- "generate-app-backend": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./scripts/generate-app-backend.js",
28
- "generate-panel": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./scripts/generate-panel.js",
29
- "generate-datasource": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./scripts/generate-datasource-without-backend.js",
30
- "generate-datasource-backend": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./scripts/generate-datasource-with-backend.js",
20
+ "dev": "nodemon --exec 'tsc'",
21
+ "dev-app": "nodemon --exec 'npm run generate-app'",
22
+ "dev-scenes-app": "nodemon --exec 'npm run generate-scenes-app'",
23
+ "dev-panel": "nodemon --exec 'npm run generate-panel'",
24
+ "dev-datasource": "nodemon --exec 'npm run generate-datasource'",
25
+ "generate-app": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./dist/bin/run.js --pluginName='Sample app' --orgName='sample-org' --pluginDescription='This is a sample app.' --pluginType='app' --no-hasBackend --hasGithubWorkflows --hasGithubLevitateWorkflow",
26
+ "generate-scenes-app": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./dist/bin/run.js --pluginName='Sample scenesapp' --orgName='sample-org' --pluginDescription='This is a sample scenes app.' --pluginType='scenesapp' --no-hasBackend --hasGithubWorkflows --hasGithubLevitateWorkflow",
27
+ "generate-app-backend": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./dist/bin/run.js --pluginName='Sample app' --orgName='sample-org' --pluginDescription='This is a sample backend app.' --pluginType='app' --hasBackend --hasGithubWorkflows --hasGithubLevitateWorkflow",
28
+ "generate-panel": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./dist/bin/run.js --pluginName='Sample panel' --orgName='sample-org' --pluginDescription='This is a sample panel.' --pluginType='panel' --hasGithubWorkflows --hasGithubLevitateWorkflow",
29
+ "generate-datasource": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./dist/bin/run.js --pluginName='Sample datasource' --orgName='sample-org' --pluginDescription='This is a sample datasource.' --pluginType='datasource' --no-hasBackend --hasGithubWorkflows --hasGithubLevitateWorkflow",
30
+ "generate-datasource-backend": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./dist/bin/run.js --pluginName='Sample datasource' --orgName='sample-org' --pluginDescription='This is a sample datasource.' --pluginType='datasource' --hasBackend --hasGithubWorkflows --hasGithubLevitateWorkflow",
31
31
  "lint": "eslint --cache --ext .js,.jsx,.ts,.tsx ./src",
32
32
  "lint:fix": "npm run lint -- --fix",
33
33
  "test": "jest",
@@ -38,14 +38,18 @@
38
38
  "@types/mkdirp": "^1.0.2",
39
39
  "@types/semver": "^7.3.9",
40
40
  "@types/which": "^2.0.2",
41
- "enquirer": "^2.3.6",
41
+ "chalk": "^4.1.2",
42
+ "change-case": "^3.1.0",
43
+ "enquirer": "^2.4.1",
42
44
  "find-up": "^5.0.0",
43
45
  "glob": "^7.1.7",
46
+ "handlebars": "^4.7.8",
44
47
  "marked": "^10.0.0",
45
48
  "marked-terminal": "^5.1.1",
49
+ "minimist": "^1.2.8",
46
50
  "mkdirp": "^1.0.4",
47
- "plop": "^2.7.4",
48
51
  "semver": "^7.3.5",
52
+ "title-case": "^2.1.1",
49
53
  "which": "^3.0.0"
50
54
  },
51
55
  "devDependencies": {
@@ -61,10 +65,13 @@
61
65
  "ext": "*",
62
66
  "events": {
63
67
  "start": "cls || clear"
68
+ },
69
+ "env": {
70
+ "CREATE_PLUGIN_DEV": true
64
71
  }
65
72
  },
66
73
  "engines": {
67
74
  "node": ">=20"
68
75
  },
69
- "gitHead": "d6027085fe429ab9100466c475350850ca7da220"
76
+ "gitHead": "81e7a0e9e06ec8f596722bd14f25f9ae6a640803"
70
77
  }
@@ -1,14 +1,10 @@
1
1
  import { exec as nodeExec } from 'node:child_process';
2
2
  import { promisify } from 'node:util';
3
3
  import fs from 'fs';
4
- import { getExportPath } from '../../utils/utils.path';
5
- import { CliArgs } from '../types';
6
4
 
7
5
  const exec = promisify(nodeExec);
8
6
 
9
- export async function prettifyFiles({ pluginName, orgName, pluginType }: CliArgs) {
10
- const exportPath = getExportPath(pluginName, orgName, pluginType);
11
-
7
+ export async function prettifyFiles(exportPath: string) {
12
8
  if (!fs.existsSync(exportPath)) {
13
9
  return '';
14
10
  }
@@ -19,8 +19,7 @@ export function printGenerateSuccessMessage(answers: CliArgs) {
19
19
  '- Open http://localhost:3000 in your browser to create a dashboard to begin developing your plugin.',
20
20
  ];
21
21
 
22
- const msg = `
23
- Congratulations on scaffolding a Grafana ${answers.pluginType} plugin! 🚀
22
+ const msg = `\n# Congratulations on scaffolding a Grafana ${answers.pluginType} plugin! 🚀
24
23
 
25
24
  ## What's next?
26
25
 
@@ -32,5 +31,5 @@ _Note: We strongly recommend creating a new Git repository by running \`git init
32
31
  - Learn more about Grafana Plugin Development at https://grafana.com/developers/plugin-tools
33
32
  `;
34
33
 
35
- return displayAsMarkdown(msg);
34
+ console.log(displayAsMarkdown(msg));
36
35
  }
@@ -0,0 +1,102 @@
1
+ import minimist from 'minimist';
2
+ import Enquirer from 'enquirer';
3
+ import { PLUGIN_TYPES } from '../../constants';
4
+ import { CliArgs } from '../types';
5
+
6
+ export async function promptUser(argv: minimist.ParsedArgs) {
7
+ let answers: Partial<CliArgs> = {};
8
+ const enquirer = new Enquirer();
9
+
10
+ for (const prompt of prompts) {
11
+ const { name, shouldPrompt } = prompt;
12
+
13
+ if (argv.hasOwnProperty(name)) {
14
+ answers = { ...answers, [name]: argv[name] };
15
+ } else {
16
+ if (typeof shouldPrompt === 'function' && !shouldPrompt(answers)) {
17
+ continue;
18
+ } else {
19
+ const result = await enquirer.prompt(prompt);
20
+ answers = { ...answers, ...result };
21
+ }
22
+ }
23
+ }
24
+
25
+ return answers as CliArgs;
26
+ }
27
+
28
+ type Prompt = {
29
+ name: keyof CliArgs;
30
+ type: string | (() => string);
31
+ message: string | (() => string) | (() => Promise<string>);
32
+ validate?: (value: string) => boolean | string | Promise<boolean | string>;
33
+ initial?: any;
34
+ choices?: Array<string | Choice>;
35
+ shouldPrompt?: ((state: object) => boolean | Promise<boolean>) | boolean;
36
+ };
37
+
38
+ type Choice = {
39
+ name: string;
40
+ message?: string;
41
+ value?: unknown;
42
+ hint?: string;
43
+ role?: string;
44
+ enabled?: boolean;
45
+ disabled?: boolean | string;
46
+ };
47
+
48
+ const prompts: Prompt[] = [
49
+ {
50
+ name: 'pluginName',
51
+ type: 'input',
52
+ message: 'What is going to be the name of your plugin?',
53
+ validate: (value: string) => {
54
+ if (/.+/.test(value)) {
55
+ return true;
56
+ }
57
+ return 'Plugin name is required';
58
+ },
59
+ },
60
+ {
61
+ name: 'orgName',
62
+ type: 'input',
63
+ message: 'What is the organization name of your plugin?',
64
+ validate: (value: string) => {
65
+ if (/.+/.test(value)) {
66
+ return true;
67
+ }
68
+ return 'Organization name is required';
69
+ },
70
+ },
71
+ {
72
+ name: 'pluginDescription',
73
+ type: 'input',
74
+ message: 'How would you describe your plugin?',
75
+ initial: '',
76
+ },
77
+ {
78
+ name: 'pluginType',
79
+ type: 'select',
80
+ choices: [PLUGIN_TYPES.app, PLUGIN_TYPES.datasource, PLUGIN_TYPES.panel, PLUGIN_TYPES.scenes],
81
+ message: 'What type of plugin would you like?',
82
+ },
83
+ {
84
+ name: 'hasBackend',
85
+ type: 'confirm',
86
+ message: 'Do you want a backend part of your plugin?',
87
+ initial: false,
88
+ shouldPrompt: (answers: CliArgs) => answers.pluginType !== PLUGIN_TYPES.panel,
89
+ },
90
+ {
91
+ name: 'hasGithubWorkflows',
92
+ type: 'confirm',
93
+ message: 'Do you want to add Github CI and Release workflows?',
94
+ initial: false,
95
+ },
96
+ {
97
+ name: 'hasGithubLevitateWorkflow',
98
+ type: 'confirm',
99
+ message: 'Do you want to add a Github workflow for automatically checking "Grafana API compatibility" on PRs?',
100
+ initial: false,
101
+ },
102
+ ];
@@ -1,15 +1,10 @@
1
- import { getExportPath } from '../../utils/utils.path';
2
- import { CliArgs } from '../types';
3
1
  import which from 'which';
4
2
  import fs from 'fs';
5
3
  import { exec } from 'child_process';
6
4
 
7
5
  const SDK_GO_MODULE = 'github.com/grafana/grafana-plugin-sdk-go';
8
6
 
9
- export async function updateGoSdkAndModules({ pluginName, orgName, pluginType }: CliArgs): Promise<string> {
10
- // plugin directory
11
- const exportPath = getExportPath(pluginName, orgName, pluginType);
12
-
7
+ export async function updateGoSdkAndModules(exportPath: string) {
13
8
  // check if there is a go.mod file in exportPath
14
9
  const goModPath = `${exportPath}/go.mod`;
15
10
  if (!fs.existsSync(goModPath)) {
@@ -1,15 +1,198 @@
1
- import path from 'path';
1
+ import glob from 'glob';
2
2
  import minimist from 'minimist';
3
- import { Plop, run } from 'plop';
3
+ import chalk from 'chalk';
4
+ import { mkdir, readdir, writeFile } from 'node:fs/promises';
5
+ import path from 'path';
6
+ import { EXTRA_TEMPLATE_VARIABLES, IS_DEV, PLUGIN_TYPES, TEMPLATE_PATHS } from '../constants';
7
+ import { getConfig } from '../utils/utils.config';
8
+ import { printError } from '../utils/utils.console';
9
+ import { directoryExists, getExportFileName, isFile } from '../utils/utils.files';
10
+ import { normalizeId } from '../utils/utils.handlebars';
11
+ import { getPackageManagerFromUserAgent, getPackageManagerInstallCmd } from '../utils/utils.packageManager';
12
+ import { getExportPath } from '../utils/utils.path';
13
+ import { renderTemplateFromFile } from '../utils/utils.templates';
14
+ import { getVersion } from '../utils/utils.version';
15
+ import { prettifyFiles } from './generate/prettify-files';
16
+ import { printGenerateSuccessMessage } from './generate/print-success-message';
17
+ import { promptUser } from './generate/prompt-user';
18
+ import { updateGoSdkAndModules } from './generate/update-go-sdk-and-packages';
19
+ import { CliArgs, TemplateData } from './types';
4
20
 
5
21
  export const generate = async (argv: minimist.ParsedArgs) => {
6
- Plop.launch(
7
- {
8
- cwd: argv.cwd,
9
- configPath: path.join(__dirname, 'generate.plopfile.js'),
10
- require: argv.require,
11
- completion: argv.completion,
12
- },
13
- (env) => run(env, undefined, true)
14
- );
22
+ const answers = await promptUser(argv);
23
+ const templateData = getTemplateData(answers);
24
+ const exportPath = getExportPath(answers.pluginName, answers.orgName, answers.pluginType);
25
+ const exportPathExists = await directoryExists(exportPath);
26
+ const exportPathIsPopulated = exportPathExists ? (await readdir(exportPath)).length > 0 : false;
27
+
28
+ // Prevent generation from writing to an existing, populated directory unless in DEV mode.
29
+ if (exportPathIsPopulated && !IS_DEV) {
30
+ printError(`**Aborting plugin scaffold. '${exportPath}' exists and contains files.**`);
31
+ process.exit(1);
32
+ }
33
+
34
+ const actions = getTemplateActions({ templateData, exportPath });
35
+ const { changes, failures } = await generateFiles({ actions });
36
+
37
+ changes.forEach((change) => {
38
+ console.log(`${chalk.green('✔︎ ++')} ${change.path}`);
39
+ });
40
+
41
+ failures.forEach((failure) => {
42
+ printError(`${failure.error}`);
43
+ });
44
+
45
+ if (answers.hasBackend) {
46
+ await execPostScaffoldFunction(updateGoSdkAndModules, exportPath);
47
+ }
48
+ await execPostScaffoldFunction(prettifyFiles, exportPath);
49
+
50
+ printGenerateSuccessMessage(answers);
15
51
  };
52
+
53
+ function getTemplateData(answers: CliArgs) {
54
+ const { pluginName, orgName, pluginType } = answers;
55
+ const { features } = getConfig();
56
+ const currentVersion = getVersion();
57
+ const pluginId = normalizeId(pluginName, orgName, pluginType);
58
+ // Support the users package manager of choice.
59
+ const { packageManagerName, packageManagerVersion } = getPackageManagerFromUserAgent();
60
+ const packageManagerInstallCmd = getPackageManagerInstallCmd(packageManagerName);
61
+ const isAppType = pluginType === PLUGIN_TYPES.app || pluginType === PLUGIN_TYPES.scenes;
62
+ const templateData: TemplateData = {
63
+ ...answers,
64
+ pluginId,
65
+ packageManagerName,
66
+ packageManagerInstallCmd,
67
+ packageManagerVersion,
68
+ isAppType,
69
+ isNPM: packageManagerName === 'npm',
70
+ version: currentVersion,
71
+ bundleGrafanaUI: features.bundleGrafanaUI,
72
+ };
73
+
74
+ return templateData;
75
+ }
76
+
77
+ function getTemplateActions({ exportPath, templateData }: { exportPath: string; templateData: any }) {
78
+ const commonActions = getActionsForTemplateFolder({
79
+ folderPath: TEMPLATE_PATHS.common,
80
+ exportPath,
81
+ templateData,
82
+ });
83
+
84
+ // Copy over files from the plugin type specific folder, e.g. "templates/app" for "app" plugins ("app" | "panel" | "datasource").
85
+ const pluginTypeSpecificActions = getActionsForTemplateFolder({
86
+ folderPath: TEMPLATE_PATHS[templateData.pluginType],
87
+ exportPath,
88
+ templateData,
89
+ });
90
+
91
+ // Copy over backend-specific files (if selected)
92
+ const backendFolderPath = templateData.isAppType ? TEMPLATE_PATHS.backendApp : TEMPLATE_PATHS.backend;
93
+ const backendActions = templateData.hasBackend
94
+ ? getActionsForTemplateFolder({ folderPath: backendFolderPath, exportPath, templateData })
95
+ : [];
96
+
97
+ // Common, pluginType and backend actions can contain different templates for the same destination.
98
+ // This filtering removes the duplicate file additions to make sure the correct template is scaffolded.
99
+ // Note that the order is reversed so backend > pluginType > common
100
+ const pluginActions = [...backendActions, ...pluginTypeSpecificActions, ...commonActions].reduce((acc, file) => {
101
+ const actionExists = acc.find((f) => f.path === file.path);
102
+ // return early to prevent duplicate file additions
103
+ if (actionExists) {
104
+ return acc;
105
+ }
106
+ acc.push(file);
107
+ return acc;
108
+ }, []);
109
+
110
+ // Copy over Github workflow files (if selected)
111
+ const ciWorkflowActions = templateData.hasGithubWorkflows
112
+ ? getActionsForTemplateFolder({
113
+ folderPath: TEMPLATE_PATHS.ciWorkflows,
114
+ exportPath,
115
+ templateData,
116
+ })
117
+ : [];
118
+
119
+ const isCompatibleWorkflowActions = templateData.hasGithubLevitateWorkflow
120
+ ? getActionsForTemplateFolder({
121
+ folderPath: TEMPLATE_PATHS.isCompatibleWorkflow,
122
+ exportPath,
123
+ templateData,
124
+ })
125
+ : [];
126
+
127
+ return [...pluginActions, ...ciWorkflowActions, ...isCompatibleWorkflowActions];
128
+ }
129
+
130
+ function getActionsForTemplateFolder({
131
+ folderPath,
132
+ exportPath,
133
+ templateData,
134
+ }: {
135
+ folderPath: string;
136
+ exportPath: string;
137
+ templateData: TemplateData;
138
+ }) {
139
+ let files = glob.sync(`${folderPath}/**`, { dot: true });
140
+
141
+ // The npmrc file is only useful for `pnpm` settings. We can remove it for other package managers.
142
+ if (templateData.packageManagerName !== 'pnpm') {
143
+ files = files.filter((file) => path.basename(file) !== 'npmrc');
144
+ }
145
+
146
+ function getFileExportPath(f: string) {
147
+ return path.relative(folderPath, path.dirname(f));
148
+ }
149
+
150
+ return files.filter(isFile).map((f) => ({
151
+ templateFile: f,
152
+ // The target path where the compiled template is saved to
153
+ path: path.join(exportPath, getFileExportPath(f), getExportFileName(f)),
154
+ data: {
155
+ ...EXTRA_TEMPLATE_VARIABLES,
156
+ ...templateData,
157
+ },
158
+ }));
159
+ }
160
+
161
+ async function generateFiles({ actions }: { actions: any[] }) {
162
+ const failures = [];
163
+ const changes = [];
164
+ for (const action of actions) {
165
+ try {
166
+ const rootDir = path.dirname(action.path);
167
+ const pathExists = await directoryExists(rootDir);
168
+ if (!pathExists) {
169
+ await mkdir(rootDir, { recursive: true });
170
+ }
171
+
172
+ const rendered = renderTemplateFromFile(action.templateFile, action.data);
173
+ await writeFile(action.path, rendered);
174
+ changes.push({
175
+ path: action.path,
176
+ });
177
+ } catch (error) {
178
+ failures.push({
179
+ path: action.path,
180
+ error: error.message || error.toString(),
181
+ });
182
+ }
183
+ }
184
+ return { failures, changes };
185
+ }
186
+
187
+ type AsyncFunction<T> = (...args: any[]) => Promise<T>;
188
+
189
+ async function execPostScaffoldFunction<T>(fn: AsyncFunction<T>, ...args: Parameters<AsyncFunction<T>>) {
190
+ try {
191
+ const resultMsg = await fn.apply(undefined, args);
192
+ if (resultMsg) {
193
+ console.log(`${chalk.green('✔︎')} ${resultMsg}`);
194
+ }
195
+ } catch (error) {
196
+ printError(`${error}`);
197
+ }
198
+ }
@@ -1,9 +1,12 @@
1
1
  const { marked } = require('marked');
2
+ const chalk = require('chalk');
2
3
  const TerminalRenderer = require('marked-terminal');
3
4
  const { Confirm, Select } = require('enquirer');
4
5
 
5
6
  marked.setOptions({
6
- renderer: new TerminalRenderer(),
7
+ renderer: new TerminalRenderer({
8
+ firstHeading: chalk.hex('#ff9900').underline.bold,
9
+ }),
7
10
  });
8
11
 
9
12
  export function displayAsMarkdown(msg: string) {
@@ -19,7 +22,7 @@ export function printMessage(msg: string) {
19
22
  }
20
23
 
21
24
  export function printSuccessMessage(msg: string) {
22
- console.log(displayAsMarkdown(`\n✔ ${msg}`));
25
+ console.log(displayAsMarkdown(`\n✔ ${msg}`).trim());
23
26
  }
24
27
 
25
28
  export function printError(error: string) {
@@ -1,6 +1,7 @@
1
1
  import path from 'path';
2
2
  import fs from 'fs';
3
3
  import { TEMPLATE_PATHS } from '../constants';
4
+ import { access, constants } from 'fs/promises';
4
5
 
5
6
  // Removes common template files from the list in case they have a plugin-specific override
6
7
  export function filterOutCommonFiles(files: string[], pluginType: string) {
@@ -36,6 +37,15 @@ export function readJsonFile(filename: string) {
36
37
  }
37
38
  }
38
39
 
40
+ export async function directoryExists(path: string) {
41
+ try {
42
+ await access(path, constants.F_OK);
43
+ return true;
44
+ } catch (e) {
45
+ return false;
46
+ }
47
+ }
48
+
39
49
  export function getOnlyExistingInCwd(files: string[]) {
40
50
  return files.filter((file) => fs.existsSync(path.join(process.cwd(), file)));
41
51
  }
@@ -3,7 +3,9 @@ import * as changeCase from 'change-case';
3
3
  import titleCase from 'title-case';
4
4
  import upperCase from 'upper-case';
5
5
  import lowerCase from 'lower-case';
6
- import { PLUGIN_TYPES } from '../constants';
6
+ import { PARTIALS_DIR, PLUGIN_TYPES } from '../constants';
7
+ import { readFileSync, readdirSync } from 'fs';
8
+ import { basename, join } from 'path';
7
9
 
8
10
  // Why? The `{#if}` expression in Handlebars unfortunately only accepts a boolean, which makes it hard to compare values in templates.
9
11
  export const ifEq = (a: any, b: any, options: HelperOptions) => {
@@ -19,8 +21,11 @@ export const normalizeId = (pluginName: string, orgName: string, type: PLUGIN_TY
19
21
  return changeCase.lowerCase(newOrgName) + '-' + changeCase.lowerCase(newPluginName) + `-${type}`;
20
22
  };
21
23
 
22
- // Needed when we are rendering the templates outside of the context of Plop but still would like to support the same helpers
23
- export function registerHandlebarsHelpers() {
24
+ // Register our helpers and partials with handlebars.
25
+ registerHandlebarsHelpers();
26
+ registerHandlebarsPartials();
27
+
28
+ function registerHandlebarsHelpers() {
24
29
  const helpers = {
25
30
  camelCase: changeCase.camelCase,
26
31
  snakeCase: changeCase.snakeCase,
@@ -45,7 +50,15 @@ export function registerHandlebarsHelpers() {
45
50
  );
46
51
  }
47
52
 
53
+ function registerHandlebarsPartials() {
54
+ const partialFiles = readdirSync(PARTIALS_DIR);
55
+ partialFiles.forEach((fileName) => {
56
+ const name = basename(fileName, '.md');
57
+ const template = readFileSync(join(PARTIALS_DIR, fileName), 'utf-8');
58
+ Handlebars.registerPartial(name, template);
59
+ });
60
+ }
61
+
48
62
  export function renderHandlebarsTemplate(template: string, data?: any) {
49
- registerHandlebarsHelpers();
50
63
  return Handlebars.compile(template)(data);
51
64
  }
@@ -8,10 +8,10 @@ App plugins can let you create a custom out-of-the-box monitoring experience by
8
8
 
9
9
  ## Get started
10
10
 
11
- {{#if hasBackend}}-- INSERT BACKEND GETTING STARTED --{{/if}}
12
- -- INSERT FRONTEND GETTING STARTED --
11
+ {{#if hasBackend}}{{> backend-getting-started }}{{/if}}
12
+ {{> frontend-getting-started packageManagerName=packageManagerName }}
13
13
 
14
- -- INSERT DISTRIBUTING YOUR PLUGIN --
14
+ {{> distributing-your-plugin }}
15
15
 
16
16
  ## Learn more
17
17
 
@@ -8,10 +8,10 @@ Grafana supports a wide range of data sources, including Prometheus, MySQL, and
8
8
 
9
9
  ## Getting started
10
10
 
11
- {{#if hasBackend}}-- INSERT BACKEND GETTING STARTED --{{/if}}
12
- -- INSERT FRONTEND GETTING STARTED --
11
+ {{#if hasBackend}}{{> backend-getting-started }}{{/if}}
12
+ {{> frontend-getting-started packageManagerName=packageManagerName }}
13
13
 
14
- -- INSERT DISTRIBUTING YOUR PLUGIN --
14
+ {{> distributing-your-plugin }}
15
15
 
16
16
  ## Learn more
17
17
 
@@ -10,9 +10,9 @@ Use panel plugins when you want to do things like visualize data returned by dat
10
10
 
11
11
  ## Getting started
12
12
 
13
- -- INSERT FRONTEND GETTING STARTED --
13
+ {{> frontend-getting-started packageManagerName=packageManagerName }}
14
14
 
15
- -- INSERT DISTRIBUTING YOUR PLUGIN --
15
+ {{> distributing-your-plugin }}
16
16
 
17
17
  ## Learn more
18
18
 
@@ -18,6 +18,6 @@ To learn more about @grafana/scenes usage please refer to the [documentation](ht
18
18
  1. An example of a scene with tabs. See [Scene with tabs](./src/pages/WithTabs/WithTabs.tsx)
19
19
  1. An example of a scene with drill down. See [Scene with drill down](./src/pages/WithDrilldown/WithDrilldown.tsx)
20
20
 
21
- -- INSERT FRONTEND GETTING STARTED --
21
+ {{> frontend-getting-started packageManagerName=packageManagerName }}
22
22
 
23
- -- INSERT DISTRIBUTING YOUR PLUGIN --
23
+ {{> distributing-your-plugin }}