@grafana/create-plugin 5.0.0-canary.1018.f88ab25.0 → 5.0.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,27 @@
1
+ # v5.0.0 (Wed Jul 31 2024)
2
+
3
+ #### 💥 Breaking Change
4
+
5
+ - Create-Plugin: Simplify Prompts [#1018](https://github.com/grafana/plugin-tools/pull/1018) ([@jackw](https://github.com/jackw))
6
+
7
+ #### Authors: 1
8
+
9
+ - Jack Westbrook ([@jackw](https://github.com/jackw))
10
+
11
+ ---
12
+
13
+ # v4.16.3 (Mon Jul 29 2024)
14
+
15
+ #### 🐛 Bug Fix
16
+
17
+ - Create Plugin: Template dynamic public path [#1023](https://github.com/grafana/plugin-tools/pull/1023) ([@jackw](https://github.com/jackw))
18
+
19
+ #### Authors: 1
20
+
21
+ - Jack Westbrook ([@jackw](https://github.com/jackw))
22
+
23
+ ---
24
+
1
25
  # v4.16.2 (Fri Jul 12 2024)
2
26
 
3
27
  #### 🐛 Bug Fix
@@ -1,3 +1,4 @@
1
+ import { machine } from 'node:os';
1
2
  import { displayAsMarkdown } from '../../utils/utils.console.js';
2
3
  import { normalizeId } from '../../utils/utils.handlebars.js';
3
4
  import { getPackageManagerFromUserAgent } from '../../utils/utils.packageManager.js';
@@ -11,7 +12,7 @@ export function printGenerateSuccessMessage(answers) {
11
12
  `- \`${packageManagerName} run dev\` to build (and watch) the plugin frontend code.`,
12
13
  ...(answers.hasBackend
13
14
  ? [
14
- '- `mage -v build:backend` to build the plugin backend code. Rerun this command every time you edit your backend files.',
15
+ `- ${getBackendCmd()} to build the plugin backend code. Rerun this command every time you edit your backend files.`,
15
16
  ]
16
17
  : []),
17
18
  '- `docker-compose up` to start a grafana development server.',
@@ -30,3 +31,10 @@ _Note: We strongly recommend creating a new Git repository by running \`git init
30
31
  `;
31
32
  console.log(displayAsMarkdown(msg));
32
33
  }
34
+ function getBackendCmd() {
35
+ const platform = machine();
36
+ if (platform === 'arm64') {
37
+ return '`mage -v build:linuxARM64`';
38
+ }
39
+ return '`mage -v build:linux`';
40
+ }
@@ -3,17 +3,17 @@ import { PLUGIN_TYPES } from '../../constants.js';
3
3
  export async function promptUser(argv) {
4
4
  let answers = {};
5
5
  const enquirer = new Enquirer();
6
- for (const prompt of prompts) {
7
- const { name, shouldPrompt } = prompt(answers);
8
- if (argv.hasOwnProperty(name)) {
9
- answers = { ...answers, [name]: argv[name] };
6
+ for (const p of prompts) {
7
+ const prompt = p(answers);
8
+ if (argv.hasOwnProperty(prompt.name)) {
9
+ answers = { ...answers, [prompt.name]: argv[prompt.name] };
10
10
  }
11
11
  else {
12
- if (typeof shouldPrompt === 'function' && !shouldPrompt(answers)) {
12
+ if (typeof prompt.shouldPrompt === 'function' && !prompt.shouldPrompt(answers)) {
13
13
  continue;
14
14
  }
15
15
  else {
16
- const result = await enquirer.prompt(prompt(answers));
16
+ const result = await enquirer.prompt(prompt);
17
17
  answers = { ...answers, ...result };
18
18
  }
19
19
  }
@@ -26,33 +26,37 @@ const prompts = [
26
26
  type: 'select',
27
27
  choices: [
28
28
  {
29
- name: 'App (Custom pages, UI Extensions and bundling other plugins)',
29
+ message: 'App (add custom pages, UI extensions and bundle other plugins)',
30
30
  value: PLUGIN_TYPES.app,
31
31
  },
32
32
  {
33
- name: 'Data source (Query data from a custom source)',
33
+ message: 'Data source (query data from a custom source)',
34
34
  value: PLUGIN_TYPES.datasource,
35
35
  },
36
36
  {
37
- name: 'Panel (New visualization for data or a widget)',
37
+ message: 'Panel (add a visualization for data or a widget)',
38
38
  value: PLUGIN_TYPES.panel,
39
39
  },
40
40
  {
41
- name: 'App with Scenes (Create dynamic dashboards in app pages)',
41
+ message: 'App with Scenes (create dynamic dashboards in app pages)',
42
42
  value: PLUGIN_TYPES.scenes,
43
43
  },
44
44
  ],
45
- message: 'Select plugin type',
46
- }),
47
- (answers) => ({
48
- name: 'hasBackend',
49
- type: 'confirm',
50
- message: answers.pluginType === PLUGIN_TYPES.app
51
- ? 'Does your plugin require a backend to support server-side functionality (e.g. calling external APIs, custom backend logic, advanced authentication, etc)?'
52
- : 'Does your plugin require a backend to support server-side functionality (e.g. alerting, advanced authentication, public dashboards, etc)?',
53
- initial: false,
54
- shouldPrompt: (answers) => answers.pluginType !== PLUGIN_TYPES.panel,
45
+ message: 'Select a plugin type',
55
46
  }),
47
+ (answers) => {
48
+ const isAppType = answers.pluginType === PLUGIN_TYPES.app || answers.pluginType === PLUGIN_TYPES.scenes;
49
+ const message = isAppType
50
+ ? 'Add a backend to support server-side functionality? (calling external APIs, custom backend logic, advanced authentication, etc)'
51
+ : 'Add a backend to support server-side functionality? (alerting, advanced authentication, public dashboards, etc)';
52
+ return {
53
+ name: 'hasBackend',
54
+ type: 'confirm',
55
+ message: message,
56
+ initial: false,
57
+ shouldPrompt: (answers) => answers.pluginType !== PLUGIN_TYPES.panel,
58
+ };
59
+ },
56
60
  () => ({
57
61
  name: 'pluginName',
58
62
  type: 'input',
@@ -75,10 +79,4 @@ const prompts = [
75
79
  return 'Organization name is required';
76
80
  },
77
81
  }),
78
- () => ({
79
- name: 'pluginDescription',
80
- type: 'input',
81
- message: 'How would you describe your plugin?',
82
- initial: '',
83
- }),
84
82
  ];
@@ -1,7 +1,9 @@
1
1
  import which from 'which';
2
2
  import fs from 'node:fs';
3
+ import createDebug from 'debug';
3
4
  import { exec } from 'node:child_process';
4
5
  const SDK_GO_MODULE = 'github.com/grafana/grafana-plugin-sdk-go';
6
+ const debug = createDebug('create-plugin:update-go');
5
7
  export async function updateGoSdkAndModules(exportPath) {
6
8
  const goModPath = `${exportPath}/go.mod`;
7
9
  if (!fs.existsSync(goModPath)) {
@@ -12,19 +14,21 @@ export async function updateGoSdkAndModules(exportPath) {
12
14
  return '';
13
15
  }
14
16
  try {
17
+ const version = await getLatestSdkVersion(exportPath);
15
18
  await updateSdk(exportPath);
16
19
  await updateGoMod(exportPath);
20
+ return `Updated Grafana go sdk to ${version} (latest)`;
17
21
  }
18
22
  catch {
19
23
  throw new Error('There was an error trying to update the grafana go sdk. Please run `go get github.com/grafana/grafana-plugin-sdk-go` manually in your plugin directory.');
20
24
  }
21
- return 'Grafana go sdk updated successfully.';
22
25
  }
23
26
  function updateSdk(exportPath) {
24
27
  return new Promise(async (resolve, reject) => {
25
28
  const command = `go get ${SDK_GO_MODULE}`;
26
29
  exec(command, { cwd: exportPath }, (error) => {
27
30
  if (error) {
31
+ debug(error);
28
32
  reject();
29
33
  }
30
34
  resolve();
@@ -36,9 +40,23 @@ function updateGoMod(exportPath) {
36
40
  const command = `go mod tidy`;
37
41
  exec(command, { cwd: exportPath }, (error) => {
38
42
  if (error) {
43
+ debug(error);
39
44
  reject();
40
45
  }
41
46
  resolve();
42
47
  });
43
48
  });
44
49
  }
50
+ function getLatestSdkVersion(exportPath) {
51
+ return new Promise(async (resolve, reject) => {
52
+ const command = `go list -m -json ${SDK_GO_MODULE}@latest`;
53
+ exec(command, { cwd: exportPath }, (error, stdout) => {
54
+ if (error) {
55
+ debug(error);
56
+ reject();
57
+ }
58
+ const version = JSON.parse(stdout).Version;
59
+ resolve(version);
60
+ });
61
+ });
62
+ }
@@ -2,7 +2,7 @@ import { glob } from 'glob';
2
2
  import chalk from 'chalk';
3
3
  import { mkdir, readdir, writeFile } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
- import { EXTRA_TEMPLATE_VARIABLES, IS_DEV, TEMPLATE_PATHS } from '../constants.js';
5
+ import { EXTRA_TEMPLATE_VARIABLES, IS_DEV, PLUGIN_TYPES, TEMPLATE_PATHS } from '../constants.js';
6
6
  import { printError } from '../utils/utils.console.js';
7
7
  import { directoryExists, getExportFileName, isFile } from '../utils/utils.files.js';
8
8
  import { getExportPath } from '../utils/utils.path.js';
@@ -22,9 +22,17 @@ export const generate = async (argv) => {
22
22
  process.exit(1);
23
23
  }
24
24
  const actions = getTemplateActions({ templateData, exportPath });
25
- const { changes, failures } = await generateFiles({ actions });
25
+ const failures = await generateFiles({ actions });
26
+ const changes = [
27
+ `Scaffolded ${templateData.pluginId} ${templateData.pluginType} plugin ${templateData.hasBackend ? '(with Go backend)' : ''}`,
28
+ 'Added basic E2E test (Playwright)',
29
+ `${provisioningMsg[templateData.pluginType]}`,
30
+ 'Configured development environment (Docker)',
31
+ 'Added default GitHub actions for CI, releases and Grafana compatibility',
32
+ ];
33
+ console.log('');
26
34
  changes.forEach((change) => {
27
- console.log(`${chalk.green('✔︎ ++')} ${change.path}`);
35
+ console.log(`${chalk.green('✔︎')} ${change}`);
28
36
  });
29
37
  failures.forEach((failure) => {
30
38
  printError(`${failure.error}`);
@@ -33,6 +41,7 @@ export const generate = async (argv) => {
33
41
  await execPostScaffoldFunction(updateGoSdkAndModules, exportPath);
34
42
  }
35
43
  await execPostScaffoldFunction(prettifyFiles, { targetPath: exportPath });
44
+ console.log('\n');
36
45
  printGenerateSuccessMessage(templateData);
37
46
  };
38
47
  function getTemplateActions({ exportPath, templateData }) {
@@ -60,7 +69,7 @@ function getTemplateActions({ exportPath, templateData }) {
60
69
  }, []);
61
70
  const ciWorkflowActions = getActionsForTemplateFolder({
62
71
  folderPath: TEMPLATE_PATHS.ciWorkflows,
63
- exportPath,
72
+ exportPath: path.join(exportPath, '.github'),
64
73
  templateData,
65
74
  });
66
75
  return [...pluginActions, ...ciWorkflowActions];
@@ -84,7 +93,6 @@ function getActionsForTemplateFolder({ folderPath, exportPath, templateData, })
84
93
  }
85
94
  async function generateFiles({ actions }) {
86
95
  const failures = [];
87
- const changes = [];
88
96
  for (const action of actions) {
89
97
  try {
90
98
  const rootDir = path.dirname(action.path);
@@ -94,9 +102,6 @@ async function generateFiles({ actions }) {
94
102
  }
95
103
  const rendered = renderTemplateFromFile(action.templateFile, action.data);
96
104
  await writeFile(action.path, rendered);
97
- changes.push({
98
- path: action.path,
99
- });
100
105
  }
101
106
  catch (error) {
102
107
  let message;
@@ -112,7 +117,7 @@ async function generateFiles({ actions }) {
112
117
  });
113
118
  }
114
119
  }
115
- return { failures, changes };
120
+ return failures;
116
121
  }
117
122
  async function execPostScaffoldFunction(fn, ...args) {
118
123
  try {
@@ -125,3 +130,9 @@ async function execPostScaffoldFunction(fn, ...args) {
125
130
  printError(`${error}`);
126
131
  }
127
132
  }
133
+ const provisioningMsg = {
134
+ [PLUGIN_TYPES.app]: 'Set up provisioning for app',
135
+ [PLUGIN_TYPES.datasource]: 'Set up provisioning for data source instance',
136
+ [PLUGIN_TYPES.panel]: 'Set up provisioning for basic dashboard and TestData data source instance',
137
+ [PLUGIN_TYPES.scenes]: 'Set up provisioning for app and TestData data source instance',
138
+ };
package/dist/constants.js CHANGED
@@ -16,15 +16,13 @@ export const TEMPLATE_PATHS = {
16
16
  common: path.join(TEMPLATES_DIR, 'common'),
17
17
  datasource: path.join(TEMPLATES_DIR, 'datasource'),
18
18
  panel: path.join(TEMPLATES_DIR, 'panel'),
19
- ciWorkflows: path.join(TEMPLATES_DIR, '.github'),
20
- isCompatibleWorkflow: path.join(TEMPLATES_DIR, 'github', 'is-compatible'),
19
+ ciWorkflows: path.join(TEMPLATES_DIR, 'github'),
21
20
  };
22
21
  export var PLUGIN_TYPES;
23
22
  (function (PLUGIN_TYPES) {
24
23
  PLUGIN_TYPES["app"] = "app";
25
24
  PLUGIN_TYPES["panel"] = "panel";
26
25
  PLUGIN_TYPES["datasource"] = "datasource";
27
- PLUGIN_TYPES["secretsmanager"] = "secretsmanager";
28
26
  PLUGIN_TYPES["scenes"] = "scenesapp";
29
27
  })(PLUGIN_TYPES || (PLUGIN_TYPES = {}));
30
28
  export const EXTRA_TEMPLATE_VARIABLES = {
@@ -20,7 +20,7 @@ export async function prettifyFiles(options) {
20
20
  catch (error) {
21
21
  throw new Error('There was a problem running prettier on the plugin files. Please run `npx -y prettier@2 . --write` manually in your plugin directory.');
22
22
  }
23
- return 'Successfully ran prettier against new plugin.';
23
+ return '';
24
24
  }
25
25
  function isPrettierUsed(projectRoot) {
26
26
  return Boolean(getPrettierVersion(projectRoot));
@@ -12,7 +12,7 @@ import { getPackageManagerInstallCmd, getPackageManagerWithFallback, getPackageM
12
12
  import { getExportFileName } from '../utils/utils.files.js';
13
13
  import { getGrafanaRuntimeVersion, getVersion } from './utils.version.js';
14
14
  import { getConfig } from './utils.config.js';
15
- const debug = createDebug('templates');
15
+ const debug = createDebug('create-plugin:templates');
16
16
  export function getTemplateFiles(pluginType, filter) {
17
17
  const commonFiles = glob.sync(`${TEMPLATE_PATHS.common}/**`, { dot: true });
18
18
  const pluginTypeSpecificFiles = glob.sync(`${TEMPLATE_PATHS[pluginType]}/**`, { dot: true });
@@ -73,7 +73,6 @@ export function getTemplateData(cliArgs) {
73
73
  ...EXTRA_TEMPLATE_VARIABLES,
74
74
  pluginId: normalizeId(cliArgs.pluginName, cliArgs.orgName, cliArgs.pluginType),
75
75
  pluginName: cliArgs.pluginName,
76
- pluginDescription: cliArgs.pluginDescription,
77
76
  hasBackend: cliArgs.hasBackend,
78
77
  orgName: cliArgs.orgName,
79
78
  pluginType: cliArgs.pluginType,
@@ -97,7 +96,6 @@ export function getTemplateData(cliArgs) {
97
96
  ...EXTRA_TEMPLATE_VARIABLES,
98
97
  pluginId: pluginJson.id,
99
98
  pluginName: pluginJson.name,
100
- pluginDescription: pluginJson.info.description,
101
99
  hasBackend: pluginJson.backend,
102
100
  orgName: pluginJson.info.author.name,
103
101
  pluginType: pluginJson.type,
@@ -6,7 +6,7 @@
6
6
  "backend": true,
7
7
  "executable": "gpx_{{ snakeCase pluginName }}",
8
8
  "info": {
9
- "description": "{{ sentenceCase pluginDescription }}",
9
+ "description": "",
10
10
  "author": {
11
11
  "name": "{{ sentenceCase orgName }}"
12
12
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grafana/create-plugin",
3
- "version": "5.0.0-canary.1018.f88ab25.0",
3
+ "version": "5.0.0",
4
4
  "repository": {
5
5
  "directory": "packages/create-plugin",
6
6
  "url": "https://github.com/grafana/plugin-tools"
@@ -28,12 +28,12 @@
28
28
  "dev-scenes-app": "nodemon --exec 'npm run generate-scenes-app'",
29
29
  "dev-panel": "nodemon --exec 'npm run generate-panel'",
30
30
  "dev-datasource": "nodemon --exec 'npm run generate-datasource'",
31
- "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",
32
- "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",
33
- "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",
34
- "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'",
35
- "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",
36
- "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",
31
+ "generate-app": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./dist/bin/run.js --pluginName='Sample app' --orgName='sample-org' --pluginType='app' --no-hasBackend",
32
+ "generate-scenes-app": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./dist/bin/run.js --pluginName='Sample scenesapp' --orgName='sample-org' --pluginType='scenesapp' --no-hasBackend",
33
+ "generate-app-backend": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./dist/bin/run.js --pluginName='Sample app' --orgName='sample-org' --pluginType='app' --hasBackend",
34
+ "generate-panel": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./dist/bin/run.js --pluginName='Sample panel' --orgName='sample-org' --pluginType='panel'",
35
+ "generate-datasource": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./dist/bin/run.js --pluginName='Sample datasource' --orgName='sample-org' --pluginType='datasource' --no-hasBackend",
36
+ "generate-datasource-backend": "tsc && npm run clean-generated && CREATE_PLUGIN_DEV=true node ./dist/bin/run.js --pluginName='Sample datasource' --orgName='sample-org' --pluginType='datasource' --hasBackend",
37
37
  "lint": "eslint --cache --ext .js,.jsx,.ts,.tsx ./src",
38
38
  "lint:fix": "npm run lint -- --fix",
39
39
  "test": "vitest",
@@ -87,5 +87,5 @@
87
87
  "engines": {
88
88
  "node": ">=20"
89
89
  },
90
- "gitHead": "f88ab256aaf0f053f7536d3033f564d8f34566f5"
90
+ "gitHead": "56c4ac2b94ab9d83be84be4379a90751359fe66e"
91
91
  }
@@ -1,11 +1,13 @@
1
+ import { machine } from 'node:os';
2
+ import { TemplateData } from '../../types.js';
1
3
  import { displayAsMarkdown } from '../../utils/utils.console.js';
2
4
  import { normalizeId } from '../../utils/utils.handlebars.js';
3
5
  import { getPackageManagerFromUserAgent } from '../../utils/utils.packageManager.js';
4
- import { TemplateData } from '../../types.js';
5
6
 
6
7
  export function printGenerateSuccessMessage(answers: TemplateData) {
7
8
  const directory = normalizeId(answers.pluginName, answers.orgName, answers.pluginType);
8
9
  const { packageManagerName } = getPackageManagerFromUserAgent();
10
+
9
11
  const commands = [
10
12
  `- \`cd ./${directory}\``,
11
13
  `- \`${packageManagerName} install\` to install frontend dependencies.`,
@@ -13,7 +15,7 @@ export function printGenerateSuccessMessage(answers: TemplateData) {
13
15
  `- \`${packageManagerName} run dev\` to build (and watch) the plugin frontend code.`,
14
16
  ...(answers.hasBackend
15
17
  ? [
16
- '- `mage -v build:backend` to build the plugin backend code. Rerun this command every time you edit your backend files.',
18
+ `- ${getBackendCmd()} to build the plugin backend code. Rerun this command every time you edit your backend files.`,
17
19
  ]
18
20
  : []),
19
21
  '- `docker-compose up` to start a grafana development server.',
@@ -34,3 +36,12 @@ _Note: We strongly recommend creating a new Git repository by running \`git init
34
36
 
35
37
  console.log(displayAsMarkdown(msg));
36
38
  }
39
+
40
+ function getBackendCmd() {
41
+ const platform = machine();
42
+ if (platform === 'arm64') {
43
+ return '`mage -v build:linuxARM64`';
44
+ }
45
+
46
+ return '`mage -v build:linux`';
47
+ }
@@ -7,16 +7,16 @@ export async function promptUser(argv: minimist.ParsedArgs) {
7
7
  let answers: Partial<GenerateCliArgs> = {};
8
8
  const enquirer = new Enquirer();
9
9
 
10
- for (const prompt of prompts) {
11
- const { name, shouldPrompt } = prompt(answers);
10
+ for (const p of prompts) {
11
+ const prompt = p(answers);
12
12
 
13
- if (argv.hasOwnProperty(name)) {
14
- answers = { ...answers, [name]: argv[name] };
13
+ if (argv.hasOwnProperty(prompt.name)) {
14
+ answers = { ...answers, [prompt.name]: argv[prompt.name] };
15
15
  } else {
16
- if (typeof shouldPrompt === 'function' && !shouldPrompt(answers)) {
16
+ if (typeof prompt.shouldPrompt === 'function' && !prompt.shouldPrompt(answers)) {
17
17
  continue;
18
18
  } else {
19
- const result = await enquirer.prompt(prompt(answers));
19
+ const result = await enquirer.prompt(prompt);
20
20
  answers = { ...answers, ...result };
21
21
  }
22
22
  }
@@ -36,7 +36,7 @@ type Prompt = {
36
36
  };
37
37
 
38
38
  type Choice = {
39
- name: string;
39
+ name?: string;
40
40
  message?: string;
41
41
  value?: unknown;
42
42
  hint?: string;
@@ -51,34 +51,38 @@ const prompts: Array<(answers: Partial<GenerateCliArgs>) => Prompt> = [
51
51
  type: 'select',
52
52
  choices: [
53
53
  {
54
- name: 'App (Custom pages, UI Extensions and bundling other plugins)',
54
+ message: 'App (add custom pages, UI extensions and bundle other plugins)',
55
55
  value: PLUGIN_TYPES.app,
56
56
  },
57
57
  {
58
- name: 'Data source (Query data from a custom source)',
58
+ message: 'Data source (query data from a custom source)',
59
59
  value: PLUGIN_TYPES.datasource,
60
60
  },
61
61
  {
62
- name: 'Panel (New visualization for data or a widget)',
62
+ message: 'Panel (add a visualization for data or a widget)',
63
63
  value: PLUGIN_TYPES.panel,
64
64
  },
65
65
  {
66
- name: 'App with Scenes (Create dynamic dashboards in app pages)',
66
+ message: 'App with Scenes (create dynamic dashboards in app pages)',
67
67
  value: PLUGIN_TYPES.scenes,
68
68
  },
69
69
  ],
70
- message: 'Select plugin type',
71
- }),
72
- (answers) => ({
73
- name: 'hasBackend',
74
- type: 'confirm',
75
- message:
76
- answers.pluginType === PLUGIN_TYPES.app
77
- ? 'Does your plugin require a backend to support server-side functionality (e.g. calling external APIs, custom backend logic, advanced authentication, etc)?'
78
- : 'Does your plugin require a backend to support server-side functionality (e.g. alerting, advanced authentication, public dashboards, etc)?',
79
- initial: false,
80
- shouldPrompt: (answers) => answers.pluginType !== PLUGIN_TYPES.panel,
70
+ message: 'Select a plugin type',
81
71
  }),
72
+ (answers) => {
73
+ const isAppType = answers.pluginType === PLUGIN_TYPES.app || answers.pluginType === PLUGIN_TYPES.scenes;
74
+ const message = isAppType
75
+ ? 'Add a backend to support server-side functionality? (calling external APIs, custom backend logic, advanced authentication, etc)'
76
+ : 'Add a backend to support server-side functionality? (alerting, advanced authentication, public dashboards, etc)';
77
+
78
+ return {
79
+ name: 'hasBackend',
80
+ type: 'confirm',
81
+ message: message,
82
+ initial: false,
83
+ shouldPrompt: (answers) => answers.pluginType !== PLUGIN_TYPES.panel,
84
+ };
85
+ },
82
86
  () => ({
83
87
  name: 'pluginName',
84
88
  type: 'input',
@@ -101,10 +105,4 @@ const prompts: Array<(answers: Partial<GenerateCliArgs>) => Prompt> = [
101
105
  return 'Organization name is required';
102
106
  },
103
107
  }),
104
- () => ({
105
- name: 'pluginDescription',
106
- type: 'input',
107
- message: 'How would you describe your plugin?',
108
- initial: '',
109
- }),
110
108
  ];
@@ -1,9 +1,12 @@
1
1
  import which from 'which';
2
2
  import fs from 'node:fs';
3
+ import createDebug from 'debug';
3
4
  import { exec } from 'node:child_process';
4
5
 
5
6
  const SDK_GO_MODULE = 'github.com/grafana/grafana-plugin-sdk-go';
6
7
 
8
+ const debug = createDebug('create-plugin:update-go');
9
+
7
10
  export async function updateGoSdkAndModules(exportPath: string) {
8
11
  // check if there is a go.mod file in exportPath
9
12
  const goModPath = `${exportPath}/go.mod`;
@@ -20,14 +23,15 @@ export async function updateGoSdkAndModules(exportPath: string) {
20
23
  }
21
24
 
22
25
  try {
26
+ const version = await getLatestSdkVersion(exportPath);
23
27
  await updateSdk(exportPath);
24
28
  await updateGoMod(exportPath);
29
+ return `Updated Grafana go sdk to ${version} (latest)`;
25
30
  } catch {
26
31
  throw new Error(
27
32
  'There was an error trying to update the grafana go sdk. Please run `go get github.com/grafana/grafana-plugin-sdk-go` manually in your plugin directory.'
28
33
  );
29
34
  }
30
- return 'Grafana go sdk updated successfully.';
31
35
  }
32
36
 
33
37
  function updateSdk(exportPath: string): Promise<void> {
@@ -36,6 +40,7 @@ function updateSdk(exportPath: string): Promise<void> {
36
40
  const command = `go get ${SDK_GO_MODULE}`;
37
41
  exec(command, { cwd: exportPath }, (error) => {
38
42
  if (error) {
43
+ debug(error);
39
44
  reject();
40
45
  }
41
46
  resolve();
@@ -49,9 +54,25 @@ function updateGoMod(exportPath: string): Promise<void> {
49
54
  const command = `go mod tidy`;
50
55
  exec(command, { cwd: exportPath }, (error) => {
51
56
  if (error) {
57
+ debug(error);
52
58
  reject();
53
59
  }
54
60
  resolve();
55
61
  });
56
62
  });
57
63
  }
64
+
65
+ function getLatestSdkVersion(exportPath: string): Promise<string> {
66
+ return new Promise(async (resolve, reject) => {
67
+ // run go list SDK_GO_MODULE@latest to get the latest version number
68
+ const command = `go list -m -json ${SDK_GO_MODULE}@latest`;
69
+ exec(command, { cwd: exportPath }, (error, stdout) => {
70
+ if (error) {
71
+ debug(error);
72
+ reject();
73
+ }
74
+ const version = JSON.parse(stdout).Version;
75
+ resolve(version);
76
+ });
77
+ });
78
+ }
@@ -3,7 +3,7 @@ import minimist from 'minimist';
3
3
  import chalk from 'chalk';
4
4
  import { mkdir, readdir, writeFile } from 'node:fs/promises';
5
5
  import path from 'node:path';
6
- import { EXTRA_TEMPLATE_VARIABLES, IS_DEV, TEMPLATE_PATHS } from '../constants.js';
6
+ import { EXTRA_TEMPLATE_VARIABLES, IS_DEV, PLUGIN_TYPES, TEMPLATE_PATHS } from '../constants.js';
7
7
  import { printError } from '../utils/utils.console.js';
8
8
  import { directoryExists, getExportFileName, isFile } from '../utils/utils.files.js';
9
9
  import { getExportPath } from '../utils/utils.path.js';
@@ -28,10 +28,19 @@ export const generate = async (argv: minimist.ParsedArgs) => {
28
28
  }
29
29
 
30
30
  const actions = getTemplateActions({ templateData, exportPath });
31
- const { changes, failures } = await generateFiles({ actions });
32
-
31
+ const failures = await generateFiles({ actions });
32
+ const changes = [
33
+ `Scaffolded ${templateData.pluginId} ${templateData.pluginType} plugin ${
34
+ templateData.hasBackend ? '(with Go backend)' : ''
35
+ }`,
36
+ 'Added basic E2E test (Playwright)',
37
+ `${provisioningMsg[templateData.pluginType]}`,
38
+ 'Configured development environment (Docker)',
39
+ 'Added default GitHub actions for CI, releases and Grafana compatibility',
40
+ ];
41
+ console.log('');
33
42
  changes.forEach((change) => {
34
- console.log(`${chalk.green('✔︎ ++')} ${change.path}`);
43
+ console.log(`${chalk.green('✔︎')} ${change}`);
35
44
  });
36
45
 
37
46
  failures.forEach((failure) => {
@@ -42,7 +51,7 @@ export const generate = async (argv: minimist.ParsedArgs) => {
42
51
  await execPostScaffoldFunction(updateGoSdkAndModules, exportPath);
43
52
  }
44
53
  await execPostScaffoldFunction(prettifyFiles, { targetPath: exportPath });
45
-
54
+ console.log('\n');
46
55
  printGenerateSuccessMessage(templateData);
47
56
  };
48
57
 
@@ -91,7 +100,7 @@ function getTemplateActions({ exportPath, templateData }: { exportPath: string;
91
100
  // Copy over Github workflow files
92
101
  const ciWorkflowActions = getActionsForTemplateFolder({
93
102
  folderPath: TEMPLATE_PATHS.ciWorkflows,
94
- exportPath,
103
+ exportPath: path.join(exportPath, '.github'),
95
104
  templateData,
96
105
  });
97
106
 
@@ -131,7 +140,6 @@ function getActionsForTemplateFolder({
131
140
 
132
141
  async function generateFiles({ actions }: { actions: any[] }) {
133
142
  const failures = [];
134
- const changes = [];
135
143
  for (const action of actions) {
136
144
  try {
137
145
  const rootDir = path.dirname(action.path);
@@ -142,9 +150,6 @@ async function generateFiles({ actions }: { actions: any[] }) {
142
150
 
143
151
  const rendered = renderTemplateFromFile(action.templateFile, action.data);
144
152
  await writeFile(action.path, rendered);
145
- changes.push({
146
- path: action.path,
147
- });
148
153
  } catch (error) {
149
154
  let message;
150
155
  if (error instanceof Error) {
@@ -158,7 +163,7 @@ async function generateFiles({ actions }: { actions: any[] }) {
158
163
  });
159
164
  }
160
165
  }
161
- return { failures, changes };
166
+ return failures;
162
167
  }
163
168
 
164
169
  type AsyncFunction<T> = (...args: any[]) => Promise<T>;
@@ -173,3 +178,10 @@ async function execPostScaffoldFunction<T>(fn: AsyncFunction<T>, ...args: Parame
173
178
  printError(`${error}`);
174
179
  }
175
180
  }
181
+
182
+ const provisioningMsg = {
183
+ [PLUGIN_TYPES.app]: 'Set up provisioning for app',
184
+ [PLUGIN_TYPES.datasource]: 'Set up provisioning for data source instance',
185
+ [PLUGIN_TYPES.panel]: 'Set up provisioning for basic dashboard and TestData data source instance',
186
+ [PLUGIN_TYPES.scenes]: 'Set up provisioning for app and TestData data source instance',
187
+ };
package/src/constants.ts CHANGED
@@ -27,15 +27,15 @@ export const TEMPLATE_PATHS: Record<string, string> = {
27
27
  common: path.join(TEMPLATES_DIR, 'common'),
28
28
  datasource: path.join(TEMPLATES_DIR, 'datasource'),
29
29
  panel: path.join(TEMPLATES_DIR, 'panel'),
30
- ciWorkflows: path.join(TEMPLATES_DIR, '.github'),
31
- isCompatibleWorkflow: path.join(TEMPLATES_DIR, 'github', 'is-compatible'),
30
+ ciWorkflows: path.join(TEMPLATES_DIR, 'github'),
32
31
  };
33
32
 
34
33
  export enum PLUGIN_TYPES {
35
34
  app = 'app',
36
35
  panel = 'panel',
37
36
  datasource = 'datasource',
38
- secretsmanager = 'secretsmanager',
37
+ // TODO: Don't understand why this is here. Cannot create a secretsmanager or a renderer.
38
+ // secretsmanager = 'secretsmanager',
39
39
  scenes = 'scenesapp',
40
40
  }
41
41
 
package/src/types.ts CHANGED
@@ -4,7 +4,6 @@ import { PLUGIN_TYPES } from './constants.js';
4
4
  // (Either via user prompts or CLI arguments)
5
5
  export type GenerateCliArgs = {
6
6
  pluginName: string;
7
- pluginDescription: string;
8
7
  orgName: string;
9
8
  pluginType: PLUGIN_TYPES;
10
9
  hasBackend: boolean;
@@ -13,7 +12,6 @@ export type GenerateCliArgs = {
13
12
  export type TemplateData = {
14
13
  pluginId: string;
15
14
  pluginName: string;
16
- pluginDescription: string;
17
15
  hasBackend: boolean;
18
16
  orgName: string;
19
17
  pluginType: PLUGIN_TYPES;
@@ -35,7 +35,7 @@ export async function prettifyFiles(options: PrettifyFilesArgs) {
35
35
  'There was a problem running prettier on the plugin files. Please run `npx -y prettier@2 . --write` manually in your plugin directory.'
36
36
  );
37
37
  }
38
- return 'Successfully ran prettier against new plugin.';
38
+ return '';
39
39
  }
40
40
 
41
41
  function isPrettierUsed(projectRoot?: string) {
@@ -24,7 +24,7 @@ import { getExportFileName } from '../utils/utils.files.js';
24
24
  import { getGrafanaRuntimeVersion, getVersion } from './utils.version.js';
25
25
  import { getConfig } from './utils.config.js';
26
26
 
27
- const debug = createDebug('templates');
27
+ const debug = createDebug('create-plugin:templates');
28
28
 
29
29
  /**
30
30
  *
@@ -118,7 +118,6 @@ export function getTemplateData(cliArgs?: GenerateCliArgs): TemplateData {
118
118
  ...EXTRA_TEMPLATE_VARIABLES,
119
119
  pluginId: normalizeId(cliArgs.pluginName, cliArgs.orgName, cliArgs.pluginType),
120
120
  pluginName: cliArgs.pluginName,
121
- pluginDescription: cliArgs.pluginDescription,
122
121
  hasBackend: cliArgs.hasBackend,
123
122
  orgName: cliArgs.orgName,
124
123
  pluginType: cliArgs.pluginType,
@@ -144,7 +143,6 @@ export function getTemplateData(cliArgs?: GenerateCliArgs): TemplateData {
144
143
  ...EXTRA_TEMPLATE_VARIABLES,
145
144
  pluginId: pluginJson.id,
146
145
  pluginName: pluginJson.name,
147
- pluginDescription: pluginJson.info.description,
148
146
  hasBackend: pluginJson.backend,
149
147
  orgName: pluginJson.info.author.name,
150
148
  pluginType: pluginJson.type,
@@ -7,7 +7,7 @@
7
7
  "executable": "gpx_{{ snakeCase pluginName }}",{{/if}}
8
8
  "info": {
9
9
  "keywords": ["app"],
10
- "description": "{{ sentenceCase pluginDescription }}",
10
+ "description": "",
11
11
  "author": {
12
12
  "name": "{{ sentenceCase orgName }}"
13
13
  },
@@ -8,18 +8,30 @@
8
8
  import CopyWebpackPlugin from 'copy-webpack-plugin';
9
9
  import ESLintPlugin from 'eslint-webpack-plugin';
10
10
  import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
11
- import LiveReloadPlugin from 'webpack-livereload-plugin';
12
11
  import path from 'path';
13
12
  import ReplaceInFileWebpackPlugin from 'replace-in-file-webpack-plugin';
14
13
  import TerserPlugin from 'terser-webpack-plugin';
15
14
  import { type Configuration, BannerPlugin } from 'webpack';
15
+ import LiveReloadPlugin from 'webpack-livereload-plugin';
16
+ import VirtualModulesPlugin from 'webpack-virtual-modules';
16
17
 
17
- import { getPackageJson, getPluginJson, hasReadme, getEntries, isWSL, getCPConfigVersion } from './utils';
18
- import { SOURCE_DIR, DIST_DIR } from './constants';
18
+ import { DIST_DIR, SOURCE_DIR } from './constants';
19
+ import { getCPConfigVersion, getEntries, getPackageJson, getPluginJson, hasReadme, isWSL } from './utils';
19
20
 
20
21
  const pluginJson = getPluginJson();
21
22
  const cpVersion = getCPConfigVersion();
22
23
 
24
+ const virtualPublicPath = new VirtualModulesPlugin({
25
+ 'node_modules/grafana-public-path.js': `
26
+ import amdMetaModule from 'amd-module';
27
+
28
+ __webpack_public_path__ =
29
+ amdMetaModule && amdMetaModule.uri
30
+ ? amdMetaModule.uri.slice(0, amdMetaModule.uri.lastIndexOf('/') + 1)
31
+ : 'public/plugins/${pluginJson.id}/';
32
+ `,
33
+ });
34
+
23
35
  const config = async (env): Promise<Configuration> => {
24
36
  const baseConfig: Configuration = {
25
37
  cache: {
@@ -112,7 +124,7 @@ const config = async (env): Promise<Configuration> => {
112
124
  {
113
125
  loader: 'imports-loader',
114
126
  options: {
115
- imports: `side-effects ${path.join(__dirname, 'publicPath.ts')}`,
127
+ imports: `side-effects grafana-public-path`,
116
128
  },
117
129
  },
118
130
  ],
@@ -169,6 +181,7 @@ const config = async (env): Promise<Configuration> => {
169
181
  },
170
182
 
171
183
  plugins: [
184
+ virtualPublicPath,
172
185
  // Insert create plugin version information into the bundle
173
186
  new BannerPlugin({
174
187
  banner: "/* [create-plugin] version: " + cpVersion + " */",
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "name": "{{ kebabCase pluginName }}",
3
3
  "version": "1.0.0",
4
- "description": "{{ sentenceCase pluginDescription }}",
5
4
  "scripts": {
6
5
  "build": "webpack -c ./.config/webpack/webpack.config.ts --env production",
7
6
  "dev": "webpack -w -c ./.config/webpack/webpack.config.ts --env development",
@@ -57,7 +56,8 @@
57
56
  "typescript": "4.8.4",
58
57
  "webpack": "^5.86.0",
59
58
  "webpack-cli": "^5.1.4",
60
- "webpack-livereload-plugin": "^3.0.2"
59
+ "webpack-livereload-plugin": "^3.0.2",
60
+ "webpack-virtual-modules": "^0.6.2"
61
61
  },
62
62
  "engines": {
63
63
  "node": ">=20"
@@ -3,19 +3,17 @@
3
3
  Remove any remaining comments before publishing as these may be displayed on Grafana.com -->
4
4
  # {{ titleCase pluginName }}
5
5
 
6
- {{ sentenceCase pluginDescription }}
7
-
8
6
  <!-- To help maximize the impact of your README and improve usability for users, we propose the following loose structure:
9
7
 
10
8
  **BEFORE YOU BEGIN**
11
9
  - Ensure all links are absolute URLs so that they will work when the README is displayed within Grafana and Grafana.com
12
- - Be inspired ✨
10
+ - Be inspired ✨
13
11
  - [grafana-polystat-panel](https://github.com/grafana/grafana-polystat-panel)
14
12
  - [volkovlabs-variable-panel](https://github.com/volkovlabs/volkovlabs-variable-panel)
15
13
 
16
14
  **ADD SOME BADGES**
17
15
 
18
- Badges convey useful information at a glance for users whether in the Catalog or viewing the source code. You can use the generator on [Shields.io](https://shields.io/badges/dynamic-json-badge) together with the Grafana.com API
16
+ Badges convey useful information at a glance for users whether in the Catalog or viewing the source code. You can use the generator on [Shields.io](https://shields.io/badges/dynamic-json-badge) together with the Grafana.com API
19
17
  to create dynamic badges that update automatically when you publish a new version to the marketplace.
20
18
 
21
19
  - For the logo field use 'grafana'.
@@ -30,7 +28,7 @@ Full example: ![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?lo
30
28
  Consider other [badges](https://shields.io/badges) as you feel appropriate for your project.
31
29
 
32
30
  ## Overview / Introduction
33
- Provide one or more paragraphs as an introduction to your plugin to help users understand why they should use it.
31
+ Provide one or more paragraphs as an introduction to your plugin to help users understand why they should use it.
34
32
 
35
33
  Consider including screenshots:
36
34
  - in [plugin.json](https://grafana.com/developers/plugin-tools/reference/plugin-json#info) include them as relative links.
@@ -47,4 +45,4 @@ If your project has dedicated documentation available for users, provide links h
47
45
 
48
46
  ## Contributing
49
47
  Do you want folks to contribute to the plugin or provide feedback through specific means? If so, tell them how!
50
- -->
48
+ -->
@@ -7,7 +7,7 @@
7
7
  "backend": true,
8
8
  "executable": "gpx_{{ snakeCase pluginName }}",{{/if}}
9
9
  "info": {
10
- "description": "{{ sentenceCase pluginDescription }}",
10
+ "description": "",
11
11
  "author": {
12
12
  "name": "{{ sentenceCase orgName }}"
13
13
  },
@@ -5,7 +5,7 @@
5
5
  "id": "{{ pluginId }}",
6
6
  "info": {
7
7
  "keywords": ["panel"],
8
- "description": "{{ sentenceCase pluginDescription }}",
8
+ "description": "",
9
9
  "author": {
10
10
  "name": "{{ sentenceCase orgName }}"
11
11
  },
@@ -9,7 +9,7 @@
9
9
  "keywords": [
10
10
  "app"
11
11
  ],
12
- "description": "{{ sentenceCase pluginDescription }}",
12
+ "description": "",
13
13
  "author": {
14
14
  "name": "{{ sentenceCase orgName }}"
15
15
  },
@@ -1,17 +0,0 @@
1
- /*
2
- * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️
3
- *
4
- * This file dynamically sets the public path at runtime based on the location of the plugin's AMD module.
5
- * It relies on the magic `module` which is defined by the AMD loader.
6
- * https://github.com/requirejs/requirejs/wiki/Differences-between-the-simplified-CommonJS-wrapper-and-standard-AMD-define#module
7
- *
8
- * We fallback to the plugin root so that older versions of Grafana will continue to load the plugin correctly.
9
- */
10
-
11
- // @ts-nocheck
12
- import amdMetaModule from 'amd-module';
13
-
14
- __webpack_public_path__ =
15
- amdMetaModule && amdMetaModule.uri
16
- ? amdMetaModule.uri.slice(0, amdMetaModule.uri.lastIndexOf('/') + 1)
17
- : 'public/plugins/{{ pluginId }}/';
File without changes