@grafana/create-plugin 4.8.0 → 4.9.1-canary.837.e9165e7.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,15 @@
1
+ # v4.9.0 (Thu Apr 25 2024)
2
+
3
+ #### 🚀 Enhancement
4
+
5
+ - Create plugin: update scaffolded tests when creating an app plugin [#892](https://github.com/grafana/plugin-tools/pull/892) ([@mckn](https://github.com/mckn))
6
+
7
+ #### Authors: 1
8
+
9
+ - Marcus Andersson ([@mckn](https://github.com/mckn))
10
+
11
+ ---
12
+
1
13
  # v4.8.0 (Wed Apr 24 2024)
2
14
 
3
15
  #### 🚀 Enhancement
@@ -2,23 +2,19 @@ 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 { DEFAULT_FEATURE_FLAGS, EXTRA_TEMPLATE_VARIABLES, IS_DEV, PLUGIN_TYPES, TEMPLATE_PATHS } from '../constants.js';
6
- import { getConfig } from '../utils/utils.config.js';
5
+ import { EXTRA_TEMPLATE_VARIABLES, IS_DEV, TEMPLATE_PATHS } from '../constants.js';
7
6
  import { printError } from '../utils/utils.console.js';
8
7
  import { directoryExists, getExportFileName, isFile } from '../utils/utils.files.js';
9
- import { normalizeId } from '../utils/utils.handlebars.js';
10
- import { getPackageManagerFromUserAgent, getPackageManagerInstallCmd } from '../utils/utils.packageManager.js';
11
8
  import { getExportPath } from '../utils/utils.path.js';
12
- import { renderTemplateFromFile } from '../utils/utils.templates.js';
13
- import { getVersion } from '../utils/utils.version.js';
9
+ import { renderTemplateFromFile, getTemplateData } from '../utils/utils.templates.js';
14
10
  import { prettifyFiles } from '../utils/utils.prettifyFiles.js';
15
11
  import { printGenerateSuccessMessage } from './generate/print-success-message.js';
16
12
  import { promptUser } from './generate/prompt-user.js';
17
13
  import { updateGoSdkAndModules } from './generate/update-go-sdk-and-packages.js';
18
14
  export const generate = async (argv) => {
19
15
  const answers = await promptUser(argv);
20
- const templateData = getTemplateData(answers);
21
- const exportPath = getExportPath(answers.pluginName, answers.orgName, answers.pluginType);
16
+ const templateData = getTemplateData({ ...answers });
17
+ const exportPath = getExportPath(templateData.pluginName, templateData.orgName, templateData.pluginType);
22
18
  const exportPathExists = await directoryExists(exportPath);
23
19
  const exportPathIsPopulated = exportPathExists ? (await readdir(exportPath)).length > 0 : false;
24
20
  if (exportPathIsPopulated && !IS_DEV) {
@@ -33,39 +29,12 @@ export const generate = async (argv) => {
33
29
  failures.forEach((failure) => {
34
30
  printError(`${failure.error}`);
35
31
  });
36
- if (answers.hasBackend) {
32
+ if (templateData.hasBackend) {
37
33
  await execPostScaffoldFunction(updateGoSdkAndModules, exportPath);
38
34
  }
39
35
  await execPostScaffoldFunction(prettifyFiles, { targetPath: exportPath });
40
- printGenerateSuccessMessage(answers);
36
+ printGenerateSuccessMessage(templateData);
41
37
  };
42
- function getTemplateData(answers) {
43
- const { pluginName, orgName, pluginType } = answers;
44
- const { features } = getConfig();
45
- const currentVersion = getVersion();
46
- const pluginId = normalizeId(pluginName, orgName, pluginType);
47
- const { packageManagerName, packageManagerVersion } = getPackageManagerFromUserAgent();
48
- const packageManagerInstallCmd = getPackageManagerInstallCmd(packageManagerName);
49
- const isAppType = pluginType === PLUGIN_TYPES.app || pluginType === PLUGIN_TYPES.scenes;
50
- const useReactRouterV6 = features.useReactRouterV6 === true && pluginType === PLUGIN_TYPES.app;
51
- const usePlaywright = features.usePlaywright === true;
52
- const templateData = {
53
- ...answers,
54
- pluginId,
55
- packageManagerName,
56
- packageManagerInstallCmd,
57
- packageManagerVersion,
58
- isAppType,
59
- isNPM: packageManagerName === 'npm',
60
- version: currentVersion,
61
- bundleGrafanaUI: features.bundleGrafanaUI ?? DEFAULT_FEATURE_FLAGS.bundleGrafanaUI,
62
- useReactRouterV6,
63
- reactRouterVersion: useReactRouterV6 ? '6.22.0' : '5.2.0',
64
- usePlaywright,
65
- e2eTestCmd: 'playwright test',
66
- };
67
- return templateData;
68
- }
69
38
  function getTemplateActions({ exportPath, templateData }) {
70
39
  const commonActions = getActionsForTemplateFolder({
71
40
  folderPath: TEMPLATE_PATHS.common,
@@ -8,7 +8,8 @@ export const migrate = async () => {
8
8
  try {
9
9
  printMessage(TEXT.migrationCommandWarning);
10
10
  if (await confirmPrompt(TEXT.overrideFilesPrompt + '\n' + displayArrayAsList(MIGRATION_CONFIG.filesToOverride))) {
11
- compileTemplateFiles(MIGRATION_CONFIG.filesToOverride.map(getExportTemplateName), getTemplateData());
11
+ const templateData = getTemplateData();
12
+ compileTemplateFiles(MIGRATION_CONFIG.filesToOverride.map(getExportTemplateName), templateData);
12
13
  printSuccessMessage(TEXT.overrideFilesSuccess);
13
14
  }
14
15
  else {
@@ -12,8 +12,9 @@ export const provisioning = async () => {
12
12
  if (await confirmPrompt(TEXT.addProvisioning)) {
13
13
  if (!fs.existsSync(provisioningFolder)) {
14
14
  const provisioningSpecificFiles = glob.sync(`${TEMPLATE_PATHS[type]}/provisioning/**`, { dot: true });
15
+ const templateData = getTemplateData();
15
16
  provisioningSpecificFiles.forEach((file) => {
16
- compileProvisioningTemplateFile(type, file, getTemplateData());
17
+ compileProvisioningTemplateFile(type, file, templateData);
17
18
  });
18
19
  printSuccessMessage(TEXT.addProvisioningSuccess);
19
20
  }
@@ -1,7 +1,7 @@
1
1
  import Handlebars from 'handlebars';
2
2
  import { camelCase, snakeCase, dotCase, pathCase, sentenceCase, constantCase, kebabCase, pascalCase, } from 'change-case';
3
3
  import { titleCase } from 'title-case';
4
- import { PARTIALS_DIR } from '../constants.js';
4
+ import { PARTIALS_DIR, PLUGIN_TYPES } from '../constants.js';
5
5
  import { readFileSync, readdirSync } from 'node:fs';
6
6
  import { basename, join } from 'node:path';
7
7
  export const ifEq = (a, b, options) => {
@@ -12,7 +12,8 @@ export const normalizeId = (pluginName, orgName, type) => {
12
12
  const nameRegex = new RegExp('[^0-9a-zA-Z]', 'g');
13
13
  const newPluginName = pluginName.replace(re, '').replace(nameRegex, '');
14
14
  const newOrgName = orgName.replace(nameRegex, '');
15
- return newOrgName.toLowerCase() + '-' + newPluginName.toLowerCase() + `-${type}`;
15
+ const newType = type === PLUGIN_TYPES.scenes ? PLUGIN_TYPES.app : type;
16
+ return newOrgName.toLowerCase() + '-' + newPluginName.toLowerCase() + `-${newType}`;
16
17
  };
17
18
  export const kebabToPascalKebab = (str) => {
18
19
  if (typeof str !== 'string') {
@@ -43,7 +44,6 @@ function registerHandlebarsHelpers() {
43
44
  properCase: pascalCase,
44
45
  pascalCase: pascalCase,
45
46
  if_eq: ifEq,
46
- normalize_id: normalizeId,
47
47
  };
48
48
  Object.keys(helpers).forEach((helperName) => Handlebars.registerHelper(helperName, helpers[helperName]));
49
49
  }
@@ -4,10 +4,10 @@ import fs from 'node:fs';
4
4
  import { mkdirp } from 'mkdirp';
5
5
  import createDebug from 'debug';
6
6
  import { filterOutCommonFiles, isFile, isFileStartingWith } from './utils.files.js';
7
- import { renderHandlebarsTemplate } from './utils.handlebars.js';
7
+ import { normalizeId, renderHandlebarsTemplate } from './utils.handlebars.js';
8
8
  import { getPluginJson } from './utils.plugin.js';
9
9
  import { TEMPLATE_PATHS, EXPORT_PATH_PREFIX, EXTRA_TEMPLATE_VARIABLES, PLUGIN_TYPES, DEFAULT_FEATURE_FLAGS, } from '../constants.js';
10
- import { getPackageManagerInstallCmd, getPackageManagerWithFallback } from './utils.packageManager.js';
10
+ import { getPackageManagerInstallCmd, getPackageManagerWithFallback, getPackageManagerFromUserAgent, } from './utils.packageManager.js';
11
11
  import { getExportFileName } from '../utils/utils.files.js';
12
12
  import { getVersion } from './utils.version.js';
13
13
  import { getConfig } from './utils.config.js';
@@ -54,37 +54,71 @@ export function compileProvisioningTemplateFile(pluginType, templateFile, data)
54
54
  export function renderTemplateFromFile(templateFile, data) {
55
55
  return renderHandlebarsTemplate(fs.readFileSync(templateFile).toString(), data);
56
56
  }
57
- export function getTemplateData() {
58
- const pluginJson = getPluginJson();
57
+ export function getTemplateData(cliArgs) {
59
58
  const { features } = getConfig();
60
59
  const currentVersion = getVersion();
61
- const useReactRouterV6 = features.useReactRouterV6 === true && pluginJson.type === PLUGIN_TYPES.app;
62
- const { packageManagerName, packageManagerVersion } = getPackageManagerWithFallback();
63
- const packageManagerInstallCmd = getPackageManagerInstallCmd(packageManagerName);
64
60
  const usePlaywright = features.usePlaywright === true || isFile(path.join(process.cwd(), 'playwright.config.ts'));
65
- const e2eTestCmd = usePlaywright
61
+ const bundleGrafanaUI = features.bundleGrafanaUI ?? DEFAULT_FEATURE_FLAGS.bundleGrafanaUI;
62
+ const shouldUseReactRouterV6 = (pluginType) => features.useReactRouterV6 === true && pluginType === PLUGIN_TYPES.app;
63
+ const getReactRouterVersion = (pluginType) => (shouldUseReactRouterV6(pluginType) ? '6.22.0' : '5.2.0');
64
+ const isAppType = (pluginType) => pluginType === PLUGIN_TYPES.app || pluginType === PLUGIN_TYPES.scenes;
65
+ const isNPM = (packageManagerName) => packageManagerName === 'npm';
66
+ const getE2eTestCmd = (packageManagerName) => usePlaywright
66
67
  ? 'playwright test'
67
68
  : `${packageManagerName} exec cypress install && ${packageManagerName} exec grafana-e2e run`;
68
- const templateData = {
69
- ...EXTRA_TEMPLATE_VARIABLES,
70
- pluginId: pluginJson.id,
71
- pluginName: pluginJson.name,
72
- pluginDescription: pluginJson.info?.description,
73
- hasBackend: Boolean(pluginJson.backend),
74
- orgName: pluginJson.info?.author?.name,
75
- pluginType: pluginJson.type,
76
- isAppType: pluginJson.type === PLUGIN_TYPES.app || pluginJson.type === PLUGIN_TYPES.scenes,
77
- isNPM: packageManagerName === 'npm',
78
- packageManagerName,
79
- packageManagerVersion,
80
- packageManagerInstallCmd,
81
- version: currentVersion,
82
- bundleGrafanaUI: features.bundleGrafanaUI ?? DEFAULT_FEATURE_FLAGS.bundleGrafanaUI,
83
- useReactRouterV6: useReactRouterV6,
84
- reactRouterVersion: useReactRouterV6 ? '6.22.0' : '5.2.0',
85
- usePlaywright,
86
- e2eTestCmd,
87
- };
69
+ let templateData;
70
+ if (cliArgs) {
71
+ const { packageManagerName, packageManagerVersion } = getPackageManagerFromUserAgent();
72
+ templateData = {
73
+ ...EXTRA_TEMPLATE_VARIABLES,
74
+ pluginId: normalizeId(cliArgs.pluginName, cliArgs.orgName, cliArgs.pluginType),
75
+ pluginName: cliArgs.pluginName,
76
+ pluginDescription: cliArgs.pluginDescription,
77
+ hasBackend: cliArgs.hasBackend,
78
+ orgName: cliArgs.orgName,
79
+ pluginType: cliArgs.pluginType,
80
+ packageManagerName,
81
+ packageManagerVersion,
82
+ packageManagerInstallCmd: getPackageManagerInstallCmd(packageManagerName),
83
+ isAppType: isAppType(cliArgs.pluginType),
84
+ isNPM: isNPM(packageManagerName),
85
+ version: currentVersion,
86
+ bundleGrafanaUI,
87
+ useReactRouterV6: shouldUseReactRouterV6(cliArgs.pluginType),
88
+ reactRouterVersion: getReactRouterVersion(cliArgs.pluginType),
89
+ usePlaywright,
90
+ e2eTestCmd: getE2eTestCmd(packageManagerName),
91
+ hasGithubWorkflows: cliArgs.hasGithubWorkflows,
92
+ hasGithubLevitateWorkflow: cliArgs.hasGithubLevitateWorkflow,
93
+ };
94
+ }
95
+ else {
96
+ const pluginJson = getPluginJson();
97
+ const { packageManagerName, packageManagerVersion } = getPackageManagerWithFallback();
98
+ const githubFolder = path.join(process.cwd(), '.github', 'workflows');
99
+ templateData = {
100
+ ...EXTRA_TEMPLATE_VARIABLES,
101
+ pluginId: pluginJson.id,
102
+ pluginName: pluginJson.name,
103
+ pluginDescription: pluginJson.info.description,
104
+ hasBackend: pluginJson.backend,
105
+ orgName: pluginJson.info.author.name,
106
+ pluginType: pluginJson.type,
107
+ packageManagerName: packageManagerName,
108
+ packageManagerVersion: packageManagerVersion,
109
+ packageManagerInstallCmd: getPackageManagerInstallCmd(packageManagerName),
110
+ isAppType: isAppType(pluginJson.type),
111
+ isNPM: isNPM(packageManagerName),
112
+ version: currentVersion,
113
+ bundleGrafanaUI,
114
+ useReactRouterV6: shouldUseReactRouterV6(pluginJson.type),
115
+ reactRouterVersion: getReactRouterVersion(pluginJson.type),
116
+ usePlaywright,
117
+ e2eTestCmd: getE2eTestCmd(packageManagerName),
118
+ hasGithubWorkflows: isFile(path.join(githubFolder, 'ci.yml')),
119
+ hasGithubLevitateWorkflow: isFile(path.join(githubFolder, 'is-compatible.yml')),
120
+ };
121
+ }
88
122
  debug('\nTemplate data:\n' + JSON.stringify(templateData, null, 2));
89
123
  return templateData;
90
124
  }
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json",
3
3
  "type": "app",
4
4
  "name": "{{ titleCase pluginName }}",
5
- "id": "{{ normalize_id pluginName orgName 'app' }}",
5
+ "id": "{{ pluginId }}",
6
6
  "backend": true,
7
7
  "executable": "gpx_{{ snakeCase pluginName }}",
8
8
  "info": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grafana/create-plugin",
3
- "version": "4.8.0",
3
+ "version": "4.9.1-canary.837.e9165e7.0",
4
4
  "repository": {
5
5
  "directory": "packages/create-plugin",
6
6
  "url": "https://github.com/grafana/plugin-tools"
@@ -87,5 +87,5 @@
87
87
  "engines": {
88
88
  "node": ">=20"
89
89
  },
90
- "gitHead": "96389ff99fec497ed6b2ab509709d578ecaf13f0"
90
+ "gitHead": "e9165e7f7cf278f7cc11fa6a1fbdc6f3343dc5dd"
91
91
  }
@@ -1,9 +1,9 @@
1
1
  import { displayAsMarkdown } from '../../utils/utils.console.js';
2
2
  import { normalizeId } from '../../utils/utils.handlebars.js';
3
3
  import { getPackageManagerFromUserAgent } from '../../utils/utils.packageManager.js';
4
- import { CliArgs } from '../types.js';
4
+ import { TemplateData } from '../../types.js';
5
5
 
6
- export function printGenerateSuccessMessage(answers: CliArgs) {
6
+ export function printGenerateSuccessMessage(answers: TemplateData) {
7
7
  const directory = normalizeId(answers.pluginName, answers.orgName, answers.pluginType);
8
8
  const { packageManagerName } = getPackageManagerFromUserAgent();
9
9
  const commands = [
@@ -1,10 +1,10 @@
1
1
  import minimist from 'minimist';
2
2
  import Enquirer from 'enquirer';
3
3
  import { PLUGIN_TYPES } from '../../constants.js';
4
- import { CliArgs } from '../types.js';
4
+ import { GenerateCliArgs } from '../../types.js';
5
5
 
6
6
  export async function promptUser(argv: minimist.ParsedArgs) {
7
- let answers: Partial<CliArgs> = {};
7
+ let answers: Partial<GenerateCliArgs> = {};
8
8
  const enquirer = new Enquirer();
9
9
 
10
10
  for (const prompt of prompts) {
@@ -22,17 +22,17 @@ export async function promptUser(argv: minimist.ParsedArgs) {
22
22
  }
23
23
  }
24
24
 
25
- return answers as CliArgs;
25
+ return answers as GenerateCliArgs;
26
26
  }
27
27
 
28
28
  type Prompt = {
29
- name: keyof CliArgs;
29
+ name: keyof GenerateCliArgs;
30
30
  type: string | (() => string);
31
31
  message: string | (() => string) | (() => Promise<string>);
32
32
  validate?: (value: string) => boolean | string | Promise<boolean | string>;
33
33
  initial?: any;
34
34
  choices?: Array<string | Choice>;
35
- shouldPrompt?: (answers: Partial<CliArgs>) => boolean;
35
+ shouldPrompt?: (answers: Partial<GenerateCliArgs>) => boolean;
36
36
  };
37
37
 
38
38
  type Choice = {
@@ -3,25 +3,21 @@ 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 { DEFAULT_FEATURE_FLAGS, EXTRA_TEMPLATE_VARIABLES, IS_DEV, PLUGIN_TYPES, TEMPLATE_PATHS } from '../constants.js';
7
- import { getConfig } from '../utils/utils.config.js';
6
+ import { EXTRA_TEMPLATE_VARIABLES, IS_DEV, TEMPLATE_PATHS } from '../constants.js';
8
7
  import { printError } from '../utils/utils.console.js';
9
8
  import { directoryExists, getExportFileName, isFile } from '../utils/utils.files.js';
10
- import { normalizeId } from '../utils/utils.handlebars.js';
11
- import { getPackageManagerFromUserAgent, getPackageManagerInstallCmd } from '../utils/utils.packageManager.js';
12
9
  import { getExportPath } from '../utils/utils.path.js';
13
- import { renderTemplateFromFile } from '../utils/utils.templates.js';
14
- import { getVersion } from '../utils/utils.version.js';
10
+ import { renderTemplateFromFile, getTemplateData } from '../utils/utils.templates.js';
15
11
  import { prettifyFiles } from '../utils/utils.prettifyFiles.js';
16
12
  import { printGenerateSuccessMessage } from './generate/print-success-message.js';
17
13
  import { promptUser } from './generate/prompt-user.js';
18
14
  import { updateGoSdkAndModules } from './generate/update-go-sdk-and-packages.js';
19
- import { CliArgs, TemplateData } from '../types.js';
15
+ import { TemplateData } from '../types.js';
20
16
 
21
17
  export const generate = async (argv: minimist.ParsedArgs) => {
22
18
  const answers = await promptUser(argv);
23
- const templateData = getTemplateData(answers);
24
- const exportPath = getExportPath(answers.pluginName, answers.orgName, answers.pluginType);
19
+ const templateData = getTemplateData({ ...answers });
20
+ const exportPath = getExportPath(templateData.pluginName, templateData.orgName, templateData.pluginType);
25
21
  const exportPathExists = await directoryExists(exportPath);
26
22
  const exportPathIsPopulated = exportPathExists ? (await readdir(exportPath)).length > 0 : false;
27
23
 
@@ -42,46 +38,14 @@ export const generate = async (argv: minimist.ParsedArgs) => {
42
38
  printError(`${failure.error}`);
43
39
  });
44
40
 
45
- if (answers.hasBackend) {
41
+ if (templateData.hasBackend) {
46
42
  await execPostScaffoldFunction(updateGoSdkAndModules, exportPath);
47
43
  }
48
44
  await execPostScaffoldFunction(prettifyFiles, { targetPath: exportPath });
49
45
 
50
- printGenerateSuccessMessage(answers);
46
+ printGenerateSuccessMessage(templateData);
51
47
  };
52
48
 
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
- // We don't enable this by default yet for new scenes plugins.
63
- const useReactRouterV6 = features.useReactRouterV6 === true && pluginType === PLUGIN_TYPES.app;
64
- const usePlaywright = features.usePlaywright === true;
65
-
66
- const templateData: TemplateData = {
67
- ...answers,
68
- pluginId,
69
- packageManagerName,
70
- packageManagerInstallCmd,
71
- packageManagerVersion,
72
- isAppType,
73
- isNPM: packageManagerName === 'npm',
74
- version: currentVersion,
75
- bundleGrafanaUI: features.bundleGrafanaUI ?? DEFAULT_FEATURE_FLAGS.bundleGrafanaUI,
76
- useReactRouterV6,
77
- reactRouterVersion: useReactRouterV6 ? '6.22.0' : '5.2.0',
78
- usePlaywright,
79
- e2eTestCmd: 'playwright test',
80
- };
81
-
82
- return templateData;
83
- }
84
-
85
49
  type TemplateAction = {
86
50
  templateFile: string;
87
51
  path: string;
@@ -28,7 +28,8 @@ export const migrate = async () => {
28
28
  // 1. Add / update configuration files
29
29
  // --------------------------
30
30
  if (await confirmPrompt(TEXT.overrideFilesPrompt + '\n' + displayArrayAsList(MIGRATION_CONFIG.filesToOverride))) {
31
- compileTemplateFiles(MIGRATION_CONFIG.filesToOverride.map(getExportTemplateName), getTemplateData());
31
+ const templateData = getTemplateData();
32
+ compileTemplateFiles(MIGRATION_CONFIG.filesToOverride.map(getExportTemplateName), templateData);
32
33
  printSuccessMessage(TEXT.overrideFilesSuccess);
33
34
  } else {
34
35
  printMessage(TEXT.overrideFilesAborted);
@@ -13,9 +13,9 @@ export const provisioning = async () => {
13
13
  if (await confirmPrompt(TEXT.addProvisioning)) {
14
14
  if (!fs.existsSync(provisioningFolder)) {
15
15
  const provisioningSpecificFiles = glob.sync(`${TEMPLATE_PATHS[type]}/provisioning/**`, { dot: true });
16
-
16
+ const templateData = getTemplateData();
17
17
  provisioningSpecificFiles.forEach((file) => {
18
- compileProvisioningTemplateFile(type, file, getTemplateData());
18
+ compileProvisioningTemplateFile(type, file, templateData);
19
19
  });
20
20
  printSuccessMessage(TEXT.addProvisioningSuccess);
21
21
  } else {
package/src/types.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { PLUGIN_TYPES } from './constants.js';
2
2
 
3
- export type CliArgs = {
3
+ // The arguments that are passed to the CLI when generating a new plugin.
4
+ // (Either via user prompts or CLI arguments)
5
+ export type GenerateCliArgs = {
4
6
  pluginName: string;
5
7
  pluginDescription: string;
6
8
  orgName: string;
@@ -12,6 +14,11 @@ export type CliArgs = {
12
14
 
13
15
  export type TemplateData = {
14
16
  pluginId: string;
17
+ pluginName: string;
18
+ pluginDescription: string;
19
+ hasBackend: boolean;
20
+ orgName: string;
21
+ pluginType: PLUGIN_TYPES;
15
22
  packageManagerName: string;
16
23
  packageManagerInstallCmd: string;
17
24
  packageManagerVersion: string;
@@ -23,4 +30,6 @@ export type TemplateData = {
23
30
  reactRouterVersion: string;
24
31
  usePlaywright: boolean;
25
32
  e2eTestCmd: string;
33
+ hasGithubWorkflows: boolean;
34
+ hasGithubLevitateWorkflow: boolean;
26
35
  };
@@ -25,7 +25,9 @@ export const normalizeId = (pluginName: string, orgName: string, type: PLUGIN_TY
25
25
 
26
26
  const newPluginName = pluginName.replace(re, '').replace(nameRegex, '');
27
27
  const newOrgName = orgName.replace(nameRegex, '');
28
- return newOrgName.toLowerCase() + '-' + newPluginName.toLowerCase() + `-${type}`;
28
+ const newType = type === PLUGIN_TYPES.scenes ? PLUGIN_TYPES.app : type;
29
+
30
+ return newOrgName.toLowerCase() + '-' + newPluginName.toLowerCase() + `-${newType}`;
29
31
  };
30
32
 
31
33
  export const kebabToPascalKebab = (str: string) => {
@@ -60,7 +62,6 @@ function registerHandlebarsHelpers() {
60
62
  properCase: pascalCase,
61
63
  pascalCase: pascalCase,
62
64
  if_eq: ifEq,
63
- normalize_id: normalizeId,
64
65
  };
65
66
 
66
67
  Object.keys(helpers).forEach((helperName) =>
@@ -4,7 +4,7 @@ import fs from 'node:fs';
4
4
  import { mkdirp } from 'mkdirp';
5
5
  import createDebug from 'debug';
6
6
  import { filterOutCommonFiles, isFile, isFileStartingWith } from './utils.files.js';
7
- import { renderHandlebarsTemplate } from './utils.handlebars.js';
7
+ import { normalizeId, renderHandlebarsTemplate } from './utils.handlebars.js';
8
8
  import { getPluginJson } from './utils.plugin.js';
9
9
  import {
10
10
  TEMPLATE_PATHS,
@@ -13,8 +13,12 @@ import {
13
13
  PLUGIN_TYPES,
14
14
  DEFAULT_FEATURE_FLAGS,
15
15
  } from '../constants.js';
16
- import { TemplateData } from '../types.js';
17
- import { getPackageManagerInstallCmd, getPackageManagerWithFallback } from './utils.packageManager.js';
16
+ import { GenerateCliArgs, TemplateData } from '../types.js';
17
+ import {
18
+ getPackageManagerInstallCmd,
19
+ getPackageManagerWithFallback,
20
+ getPackageManagerFromUserAgent,
21
+ } from './utils.packageManager.js';
18
22
  import { getExportFileName } from '../utils/utils.files.js';
19
23
  import { getVersion } from './utils.version.js';
20
24
  import { getConfig } from './utils.config.js';
@@ -88,38 +92,79 @@ export function renderTemplateFromFile(templateFile: string, data?: any) {
88
92
  return renderHandlebarsTemplate(fs.readFileSync(templateFile).toString(), data);
89
93
  }
90
94
 
91
- export function getTemplateData(): TemplateData {
92
- const pluginJson = getPluginJson();
95
+ export function getTemplateData(cliArgs?: GenerateCliArgs): TemplateData {
93
96
  const { features } = getConfig();
94
97
  const currentVersion = getVersion();
95
- const useReactRouterV6 = features.useReactRouterV6 === true && pluginJson.type === PLUGIN_TYPES.app;
96
- const { packageManagerName, packageManagerVersion } = getPackageManagerWithFallback();
97
- const packageManagerInstallCmd = getPackageManagerInstallCmd(packageManagerName);
98
98
  const usePlaywright = features.usePlaywright === true || isFile(path.join(process.cwd(), 'playwright.config.ts'));
99
- const e2eTestCmd = usePlaywright
100
- ? 'playwright test'
101
- : `${packageManagerName} exec cypress install && ${packageManagerName} exec grafana-e2e run`;
102
-
103
- const templateData = {
104
- ...EXTRA_TEMPLATE_VARIABLES,
105
- pluginId: pluginJson.id,
106
- pluginName: pluginJson.name,
107
- pluginDescription: pluginJson.info?.description,
108
- hasBackend: Boolean(pluginJson.backend),
109
- orgName: pluginJson.info?.author?.name,
110
- pluginType: pluginJson.type,
111
- isAppType: pluginJson.type === PLUGIN_TYPES.app || pluginJson.type === PLUGIN_TYPES.scenes,
112
- isNPM: packageManagerName === 'npm',
113
- packageManagerName,
114
- packageManagerVersion,
115
- packageManagerInstallCmd,
116
- version: currentVersion,
117
- bundleGrafanaUI: features.bundleGrafanaUI ?? DEFAULT_FEATURE_FLAGS.bundleGrafanaUI,
118
- useReactRouterV6: useReactRouterV6,
119
- reactRouterVersion: useReactRouterV6 ? '6.22.0' : '5.2.0',
120
- usePlaywright,
121
- e2eTestCmd,
122
- };
99
+ const bundleGrafanaUI = features.bundleGrafanaUI ?? DEFAULT_FEATURE_FLAGS.bundleGrafanaUI;
100
+ const shouldUseReactRouterV6 = (pluginType: string) =>
101
+ features.useReactRouterV6 === true && pluginType === PLUGIN_TYPES.app;
102
+ const getReactRouterVersion = (pluginType: string) => (shouldUseReactRouterV6(pluginType) ? '6.22.0' : '5.2.0');
103
+ const isAppType = (pluginType: string) => pluginType === PLUGIN_TYPES.app || pluginType === PLUGIN_TYPES.scenes;
104
+ const isNPM = (packageManagerName: string) => packageManagerName === 'npm';
105
+ const getE2eTestCmd = (packageManagerName: string) =>
106
+ usePlaywright
107
+ ? 'playwright test'
108
+ : `${packageManagerName} exec cypress install && ${packageManagerName} exec grafana-e2e run`;
109
+
110
+ let templateData: TemplateData;
111
+
112
+ // `cliArgs` is only passed in when scaffolding a new plugin via the CLI (generate command)
113
+ if (cliArgs) {
114
+ const { packageManagerName, packageManagerVersion } = getPackageManagerFromUserAgent();
115
+
116
+ templateData = {
117
+ ...EXTRA_TEMPLATE_VARIABLES,
118
+ pluginId: normalizeId(cliArgs.pluginName, cliArgs.orgName, cliArgs.pluginType),
119
+ pluginName: cliArgs.pluginName,
120
+ pluginDescription: cliArgs.pluginDescription,
121
+ hasBackend: cliArgs.hasBackend,
122
+ orgName: cliArgs.orgName,
123
+ pluginType: cliArgs.pluginType,
124
+ packageManagerName,
125
+ packageManagerVersion,
126
+ packageManagerInstallCmd: getPackageManagerInstallCmd(packageManagerName),
127
+ isAppType: isAppType(cliArgs.pluginType),
128
+ isNPM: isNPM(packageManagerName),
129
+ version: currentVersion,
130
+ bundleGrafanaUI,
131
+ useReactRouterV6: shouldUseReactRouterV6(cliArgs.pluginType),
132
+ reactRouterVersion: getReactRouterVersion(cliArgs.pluginType),
133
+ usePlaywright,
134
+ e2eTestCmd: getE2eTestCmd(packageManagerName),
135
+ hasGithubWorkflows: cliArgs.hasGithubWorkflows,
136
+ hasGithubLevitateWorkflow: cliArgs.hasGithubLevitateWorkflow,
137
+ };
138
+ // Updating or migrating a plugin
139
+ // (plugin.json and package.json files are only present if it's an existing plugin)
140
+ } else {
141
+ const pluginJson = getPluginJson();
142
+ const { packageManagerName, packageManagerVersion } = getPackageManagerWithFallback();
143
+ const githubFolder = path.join(process.cwd(), '.github', 'workflows');
144
+
145
+ templateData = {
146
+ ...EXTRA_TEMPLATE_VARIABLES,
147
+ pluginId: pluginJson.id,
148
+ pluginName: pluginJson.name,
149
+ pluginDescription: pluginJson.info.description,
150
+ hasBackend: pluginJson.backend,
151
+ orgName: pluginJson.info.author.name,
152
+ pluginType: pluginJson.type,
153
+ packageManagerName: packageManagerName,
154
+ packageManagerVersion: packageManagerVersion,
155
+ packageManagerInstallCmd: getPackageManagerInstallCmd(packageManagerName),
156
+ isAppType: isAppType(pluginJson.type),
157
+ isNPM: isNPM(packageManagerName),
158
+ version: currentVersion,
159
+ bundleGrafanaUI,
160
+ useReactRouterV6: shouldUseReactRouterV6(pluginJson.type),
161
+ reactRouterVersion: getReactRouterVersion(pluginJson.type),
162
+ usePlaywright,
163
+ e2eTestCmd: getE2eTestCmd(packageManagerName),
164
+ hasGithubWorkflows: isFile(path.join(githubFolder, 'ci.yml')),
165
+ hasGithubLevitateWorkflow: isFile(path.join(githubFolder, 'is-compatible.yml')),
166
+ };
167
+ }
123
168
 
124
169
  debug('\nTemplate data:\n' + JSON.stringify(templateData, null, 2));
125
170
 
@@ -1,7 +1,7 @@
1
1
  apiVersion: 1
2
2
 
3
3
  apps:
4
- - type: '{{ normalize_id pluginName orgName 'app' }}'
4
+ - type: '{{ pluginId }}'
5
5
  org_id: 1
6
6
  org_name: '{{ orgName }}'
7
7
  disabled: false
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json",
3
3
  "type": "app",
4
4
  "name": "{{ titleCase pluginName }}",
5
- "id": "{{ normalize_id pluginName orgName 'app' }}",{{#if hasBackend}}
5
+ "id": "{{ pluginId }}",{{#if hasBackend}}
6
6
  "backend": true,
7
7
  "executable": "gpx_{{ snakeCase pluginName }}",{{/if}}
8
8
  "info": {
@@ -1,7 +1,7 @@
1
1
  import { test, expect } from './fixtures';
2
2
 
3
3
  test('should be possible to save app configuration', async ({ appConfigPage, page }) => {
4
- const saveForm = page.getByRole('button', { name: /Save API settings/i });
4
+ const saveButton = page.getByRole('button', { name: /Save API settings/i });
5
5
 
6
6
  // reset the configured secret
7
7
  await page.getByRole('button', { name: /reset/i }).click();
@@ -14,6 +14,6 @@ test('should be possible to save app configuration', async ({ appConfigPage, pag
14
14
  // listen for the server response on the saved form
15
15
  const saveResponse = appConfigPage.waitForSettingsResponse();
16
16
 
17
- await saveForm.click();
17
+ await saveButton.click();
18
18
  await expect(saveResponse).toBeOK();
19
19
  });
@@ -1,26 +1,25 @@
1
- import pluginJson from '../src/plugin.json';
2
- import { test, expect } from '@grafana/plugin-e2e';
1
+ import { test, expect } from './fixtures';
3
2
  import { ROUTES } from '../src/constants';
4
3
 
5
4
  test.describe('navigating app', () => {
6
- test('page one should render successfully', async ({ page }) => {
7
- await page.goto(`/a/${pluginJson.id}/${ROUTES.One}`);
5
+ test('page one should render successfully', async ({ gotoPage, page }) => {
6
+ await gotoPage(`/${ROUTES.One}`);
8
7
  await expect(page.getByText('This is page one.')).toBeVisible();
9
8
  });
10
9
 
11
- test('page two should render successfully', async ({ page }) => {
12
- await page.goto(`/a/${pluginJson.id}/${ROUTES.Two}`);
10
+ test('page two should render successfully', async ({ gotoPage, page }) => {
11
+ await gotoPage(`/${ROUTES.Two}`);
13
12
  await expect(page.getByText('This is page two.')).toBeVisible();
14
13
  });
15
14
 
16
- test('page three should support an id parameter', async ({ page }) => {
17
- await page.goto(`/a/${pluginJson.id}/${ROUTES.Three}/123456`);
15
+ test('page three should support an id parameter', async ({ gotoPage, page }) => {
16
+ await gotoPage(`/${ROUTES.Three}/123456`);
18
17
  await expect(page.getByText('ID: 123456')).toBeVisible();
19
18
  });
20
19
 
21
- test('page three should render sucessfully', async ({ page }) => {
20
+ test('page three should render sucessfully', async ({ gotoPage, page }) => {
22
21
  // wait for page to successfully render
23
- await page.goto(`/a/${pluginJson.id}/${ROUTES.One}`);
22
+ await gotoPage(`/${ROUTES.One}`);
24
23
  await expect(page.getByText('This is page one.')).toBeVisible();
25
24
 
26
25
  // navigating to page four with full width layout without sidebar menu
@@ -1,19 +1,26 @@
1
- import { AppConfigPage, test as base } from '@grafana/plugin-e2e';
1
+ import { AppConfigPage, AppPage, test as base } from '@grafana/plugin-e2e';
2
2
  import pluginJson from '../src/plugin.json';
3
3
 
4
- type AppTestFixture = { appConfigPage: AppConfigPage };
4
+ type AppTestFixture = {
5
+ appConfigPage: AppConfigPage;
6
+ gotoPage: (path?: string) => Promise<AppPage>;
7
+ };
5
8
 
6
9
  export const test = base.extend<AppTestFixture>({
7
- appConfigPage: async ({ page, selectors, grafanaVersion, request }, use, testInfo) => {
8
- const configPage = new AppConfigPage(
9
- { page, selectors, grafanaVersion, request, testInfo },
10
- {
10
+ appConfigPage: async ({ gotoAppConfigPage }, use) => {
11
+ const configPage = await gotoAppConfigPage({
12
+ pluginId: pluginJson.id,
13
+ });
14
+ await use(configPage);
15
+ },
16
+ gotoPage: async ({ gotoAppPage }, use) => {
17
+ await use((path) =>
18
+ gotoAppPage({
19
+ path,
11
20
  pluginId: pluginJson.id,
12
- }
21
+ })
13
22
  );
14
- await configPage.goto();
15
- await use(configPage);
16
23
  },
17
24
  });
18
25
 
19
- export { expect } from '@grafana/plugin-e2e';
26
+ export { expect } from '@grafana/plugin-e2e';
@@ -17,8 +17,8 @@ func main() {
17
17
  // from Grafana to create different instances of SampleDatasource (per datasource
18
18
  // ID). When datasource configuration changed Dispose method will be called and
19
19
  // new datasource instance created using NewSampleDatasource factory.
20
- if err := datasource.Manage("{{ normalize_id pluginName orgName pluginType }}", plugin.NewDatasource, datasource.ManageOpts{}); err != nil {
20
+ if err := datasource.Manage("{{ pluginId }}", plugin.NewDatasource, datasource.ManageOpts{}); err != nil {
21
21
  log.DefaultLogger.Error(err.Error())
22
22
  os.Exit(1)
23
23
  }
24
- }
24
+ }
@@ -16,7 +16,7 @@ func main() {
16
16
  // argument. This factory will be automatically called on incoming request
17
17
  // from Grafana to create different instances of `App` (per plugin
18
18
  // ID).
19
- if err := app.Manage("{{ normalize_id pluginName orgName pluginType }}", plugin.NewApp, app.ManageOpts{}); err != nil {
19
+ if err := app.Manage("{{ pluginId }}", plugin.NewApp, app.ManageOpts{}); err != nil {
20
20
  log.DefaultLogger.Error(err.Error())
21
21
  os.Exit(1)
22
22
  }
@@ -2,7 +2,7 @@ apiVersion: 1
2
2
 
3
3
  datasources:
4
4
  - name: '{{ pluginName }}'
5
- type: '{{ normalize_id pluginName orgName 'datasource' }}'
5
+ type: '{{ pluginId }}'
6
6
  access: proxy
7
7
  isDefault: false
8
8
  orgId: 1
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json",
3
3
  "type": "datasource",
4
4
  "name": "{{ titleCase pluginName }}",
5
- "id": "{{ normalize_id pluginName orgName 'datasource' }}",
5
+ "id": "{{ pluginId }}",
6
6
  "metrics": true,{{#if hasBackend}}
7
7
  "backend": true,
8
8
  "executable": "gpx_{{ snakeCase pluginName }}",{{/if}}
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json",
3
3
  "type": "panel",
4
4
  "name": "{{ titleCase pluginName }}",
5
- "id": "{{ normalize_id pluginName orgName 'panel' }}",
5
+ "id": "{{ pluginId }}",
6
6
  "info": {
7
7
  "keywords": ["panel"],
8
8
  "description": "{{ sentenceCase pluginDescription }}",
@@ -1,7 +1,7 @@
1
1
  apiVersion: 1
2
2
 
3
3
  apps:
4
- - type: '{{ normalize_id pluginName orgName 'app' }}'
4
+ - type: '{{ pluginId }}'
5
5
  org_id: 1
6
6
  org_name: '{{ orgName }}'
7
7
  disabled: false
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://raw.githubusercontent.com/grafana/grafana/main/docs/sources/developers/plugins/plugin.schema.json",
3
3
  "type": "app",
4
4
  "name": "{{ titleCase pluginName }}",
5
- "id": "{{ normalize_id pluginName orgName 'app' }}",{{#if hasBackend}}
5
+ "id": "{{ pluginId }}",{{#if hasBackend}}
6
6
  "backend": true,
7
7
  "executable": "gpx_{{ snakeCase pluginName }}",{{/if}}
8
8
  "info": {
@@ -1 +0,0 @@
1
- export {};
@@ -1,24 +0,0 @@
1
- import { PLUGIN_TYPES } from '../constants.js';
2
-
3
- export type CliArgs = {
4
- pluginName: string;
5
- pluginDescription: string;
6
- orgName: string;
7
- pluginType: PLUGIN_TYPES;
8
- hasBackend: boolean;
9
- hasGithubWorkflows: boolean;
10
- hasGithubLevitateWorkflow: boolean;
11
- };
12
-
13
- export type TemplateData = {
14
- pluginId: string;
15
- packageManagerName: string;
16
- packageManagerInstallCmd: string;
17
- packageManagerVersion: string;
18
- isAppType: boolean;
19
- isNPM: boolean;
20
- version: string;
21
- bundleGrafanaUI: boolean;
22
- useReactRouterV6: boolean;
23
- reactRouterVersion: string;
24
- };