@grafana/create-plugin 6.2.0-canary.2233.19133609453.0 → 6.2.0-canary.2233.19368311379.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 (54) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/CONTRIBUTING.md +3 -0
  3. package/dist/codemods/additions/additions.js +8 -8
  4. package/dist/codemods/additions/scripts/example-addition.js +19 -33
  5. package/dist/codemods/migrations/manager.js +13 -40
  6. package/dist/codemods/migrations/migrations.js +34 -25
  7. package/dist/codemods/migrations/scripts/004-eslint9-flat-config.js +1 -2
  8. package/dist/codemods/migrations/scripts/005-react-18-3.js +20 -0
  9. package/dist/codemods/migrations/scripts/example-migration.js +7 -3
  10. package/dist/codemods/runner.js +38 -0
  11. package/dist/codemods/schema-parser.js +20 -0
  12. package/dist/codemods/utils.js +15 -6
  13. package/dist/commands/add.command.js +24 -55
  14. package/dist/commands/update.command.js +7 -41
  15. package/dist/utils/utils.checks.js +40 -0
  16. package/dist/utils/utils.config.js +1 -16
  17. package/package.json +3 -2
  18. package/src/codemods/additions/additions.test.ts +12 -0
  19. package/src/codemods/additions/additions.ts +9 -22
  20. package/src/codemods/additions/scripts/example-addition.test.ts +14 -33
  21. package/src/codemods/additions/scripts/example-addition.ts +27 -44
  22. package/src/codemods/migrations/fixtures/migrations.ts +19 -18
  23. package/src/codemods/migrations/manager.test.ts +67 -73
  24. package/src/codemods/migrations/manager.ts +17 -50
  25. package/src/codemods/migrations/migrations.test.ts +8 -5
  26. package/src/codemods/migrations/migrations.ts +38 -34
  27. package/src/codemods/migrations/scripts/004-eslint9-flat-config.ts +2 -2
  28. package/src/codemods/migrations/scripts/005-react-18-3.test.ts +145 -0
  29. package/src/codemods/migrations/scripts/005-react-18-3.ts +19 -0
  30. package/src/codemods/migrations/scripts/example-migration.test.ts +1 -1
  31. package/src/codemods/migrations/scripts/example-migration.ts +20 -3
  32. package/src/codemods/runner.ts +57 -0
  33. package/src/codemods/schema-parser.ts +27 -0
  34. package/src/codemods/types.ts +9 -14
  35. package/src/codemods/{migrations/utils.test.ts → utils.test.ts} +8 -7
  36. package/src/codemods/utils.ts +28 -36
  37. package/src/commands/add.command.ts +26 -62
  38. package/src/commands/update.command.ts +8 -47
  39. package/src/migrations/migrations.ts +44 -0
  40. package/src/utils/utils.checks.ts +47 -0
  41. package/src/utils/utils.config.ts +1 -28
  42. package/templates/common/_package.json +7 -5
  43. package/templates/github/workflows/bundle-stats.yml +1 -1
  44. package/templates/github/workflows/ci.yml +11 -11
  45. package/templates/github/workflows/cp-update.yml +9 -14
  46. package/templates/github/workflows/is-compatible.yml +3 -3
  47. package/templates/github/workflows/release.yml +1 -1
  48. package/vitest.config.ts +12 -0
  49. package/dist/codemods/additions/manager.js +0 -115
  50. package/dist/codemods/additions/utils.js +0 -10
  51. package/dist/codemods/migrations/utils.js +0 -10
  52. package/src/codemods/additions/manager.ts +0 -145
  53. package/src/codemods/additions/utils.ts +0 -12
  54. package/src/codemods/migrations/utils.ts +0 -12
@@ -0,0 +1,57 @@
1
+ import { Context } from './context.js';
2
+ import { formatFiles, flushChanges, installNPMDependencies, printChanges } from './utils.js';
3
+ import { parseAndValidateOptions } from './schema-parser.js';
4
+ import { output } from '../utils/utils.console.js';
5
+ import { Codemod } from './types.js';
6
+
7
+ /**
8
+ * Run a single codemod
9
+ *
10
+ * Steps:
11
+ * 1. Load codemod module from scriptPath
12
+ * 2. Parse and validate options from schema
13
+ * 3. Execute codemod transformation
14
+ * 4. Format files
15
+ * 5. Flush changes to disk
16
+ * 6. Print summary
17
+ * 7. Install dependencies if needed
18
+ */
19
+ export async function runCodemod(codemod: Codemod, options?: Record<string, any>): Promise<Context> {
20
+ const codemodModule = await import(codemod.scriptPath);
21
+ if (!codemodModule.default || typeof codemodModule.default !== 'function') {
22
+ throw new Error(`Codemod ${codemod.name} must export a default function`);
23
+ }
24
+
25
+ let codemodOptions = {};
26
+
27
+ if (options && codemodModule.schema) {
28
+ codemodOptions = parseAndValidateOptions(options, codemodModule.schema);
29
+ }
30
+
31
+ const basePath = process.cwd();
32
+ const context = new Context(basePath);
33
+
34
+ try {
35
+ output.log({
36
+ title: `Running ${codemod.name}`,
37
+ body: [codemod.description],
38
+ });
39
+
40
+ const updatedContext = await codemodModule.default(context, codemodOptions);
41
+
42
+ // standard post-processing pipeline
43
+ await formatFiles(updatedContext);
44
+ flushChanges(updatedContext);
45
+ printChanges(updatedContext, codemod.name, codemod.description);
46
+ installNPMDependencies(updatedContext);
47
+
48
+ return updatedContext;
49
+ } catch (error) {
50
+ if (error instanceof Error) {
51
+ const newError = new Error(`Error running ${codemod.name}: ${error.message}`);
52
+ newError.cause = error;
53
+ throw newError;
54
+ }
55
+ throw error;
56
+ }
57
+ }
@@ -0,0 +1,27 @@
1
+ import * as v from 'valibot';
2
+
3
+ /**
4
+ * Parse and validate options using Valibot schema
5
+ * Valibot handles parsing, validation, type coercion, and defaults automatically
6
+ */
7
+ export function parseAndValidateOptions<T extends v.BaseSchema<any, any, any>>(
8
+ options: Record<string, any>,
9
+ schema: T
10
+ ): v.InferOutput<T> {
11
+ try {
12
+ return v.parse(schema, options);
13
+ } catch (error) {
14
+ if (v.isValiError(error)) {
15
+ // format Valibot validation errors
16
+ const formattedErrors = error.issues
17
+ .map((issue) => {
18
+ const path = issue.path?.map((p) => p.key).join('.') || '';
19
+ return ` --${path}: ${issue.message}`;
20
+ })
21
+ .join('\n');
22
+
23
+ throw new Error(`Invalid flag(s) provided:\n\n${formattedErrors}`);
24
+ }
25
+ throw error;
26
+ }
27
+ }
@@ -1,21 +1,16 @@
1
1
  import type { Context } from './context.js';
2
+ import type * as v from 'valibot';
2
3
 
3
- export interface CodemodModule {
4
- default: (context: Context) => Context | Promise<Context>;
4
+ // used as a generic constraint for codemod schemas. accepts any input, output and error types
5
+ type AnySchema = v.BaseSchema<any, any, any>;
6
+
7
+ export interface CodemodModule<TSchema extends AnySchema = AnySchema> {
8
+ default: (context: Context, options: v.InferOutput<TSchema>) => Context | Promise<Context>;
9
+ schema?: TSchema;
5
10
  }
6
11
 
7
- export interface FlagDefinition {
12
+ export interface Codemod {
8
13
  name: string;
9
14
  description: string;
10
- required: boolean;
11
- }
12
-
13
- export interface AdditionModule<TOptions = any> extends CodemodModule {
14
- default: (context: Context, options?: TOptions) => Context | Promise<Context>;
15
- flags?: FlagDefinition[];
16
- parseFlags?: (argv: any) => TOptions;
17
- }
18
-
19
- export interface MigrationModule extends CodemodModule {
20
- default: (context: Context) => Context | Promise<Context>;
15
+ scriptPath: string;
21
16
  }
@@ -1,5 +1,5 @@
1
1
  import { dirSync } from 'tmp';
2
- import { Context } from '../context.js';
2
+ import { Context } from './context.js';
3
3
  import {
4
4
  addDependenciesToPackageJson,
5
5
  removeDependenciesFromPackageJson,
@@ -7,12 +7,13 @@ import {
7
7
  formatFiles,
8
8
  readJsonFile,
9
9
  isVersionGreater,
10
- } from '../utils.js';
11
- import { printChanges } from './utils.js';
10
+ printChanges,
11
+ } from './utils.js';
12
12
  import { join } from 'node:path';
13
13
  import { mkdir, rm, writeFile } from 'node:fs/promises';
14
14
  import { readFileSync } from 'node:fs';
15
- import { output } from '../../utils/utils.console.js';
15
+ import { output } from '../utils/utils.console.js';
16
+ import { vi } from 'vitest';
16
17
 
17
18
  describe('utils', () => {
18
19
  const tmpObj = dirSync({ unsafeCleanup: true });
@@ -80,7 +81,7 @@ describe('utils', () => {
80
81
  context.updateFile('baz.ts', 'new content');
81
82
  context.deleteFile('bar.ts');
82
83
 
83
- printChanges(context, 'key', { migrationScript: 'test', description: 'test', version: '1.0.0' });
84
+ printChanges(context, 'key', 'test');
84
85
 
85
86
  expect(outputMock.addHorizontalLine).toHaveBeenCalledWith('gray');
86
87
  expect(outputMock.logSingleLine).toHaveBeenCalledWith('key (test)');
@@ -102,7 +103,7 @@ describe('utils', () => {
102
103
  it('should print no changes', async () => {
103
104
  const context = new Context(tmpDir);
104
105
 
105
- printChanges(context, 'key', { migrationScript: 'test', description: 'test', version: '1.0.0' });
106
+ printChanges(context, 'key', 'test');
106
107
 
107
108
  expect(outputMock.logSingleLine).toHaveBeenCalledWith('No changes were made');
108
109
  });
@@ -271,7 +272,7 @@ describe('utils', () => {
271
272
  });
272
273
  });
273
274
 
274
- describe('isIncomingVersionGreater', () => {
275
+ describe('isVersionGreater', () => {
275
276
  describe('dist tag comparison', () => {
276
277
  it('should return false when incoming is "latest" and existing is "next"', () => {
277
278
  expect(isVersionGreater('latest', 'next')).toBe(false);
@@ -1,9 +1,4 @@
1
- /**
2
- * Shared utilities for codemods (migrations and additions).
3
- * These functions work with the Context class to modify plugin codebases.
4
- */
5
-
6
- import { dirname, join } from 'node:path';
1
+ import { dirname, join, resolve } from 'node:path';
7
2
  import { createRequire } from 'node:module';
8
3
  import { Context } from './context.js';
9
4
  import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
@@ -11,20 +6,10 @@ import chalk from 'chalk';
11
6
  import { output } from '../utils/utils.console.js';
12
7
  import { getPackageManagerSilentInstallCmd, getPackageManagerWithFallback } from '../utils/utils.packageManager.js';
13
8
  import { execSync } from 'node:child_process';
14
- import { clean, coerce, gt } from 'semver';
15
-
16
- /**
17
- * Generic metadata type for printChanges
18
- */
19
- type ChangeMetadata = {
20
- description: string;
21
- };
9
+ import { clean, coerce, gt, gte } from 'semver';
10
+ import { fileURLToPath } from 'node:url';
22
11
 
23
- /**
24
- * Prints changes made to the context in a formatted way.
25
- * Works with both migrations and additions.
26
- */
27
- export function printChanges(context: Context, key: string, meta: ChangeMetadata) {
12
+ export function printChanges(context: Context, key: string, description: string) {
28
13
  const changes = context.listChanges();
29
14
  const lines = [];
30
15
 
@@ -39,7 +24,7 @@ export function printChanges(context: Context, key: string, meta: ChangeMetadata
39
24
  }
40
25
 
41
26
  output.addHorizontalLine('gray');
42
- output.logSingleLine(`${key} (${meta.description})`);
27
+ output.logSingleLine(`${key} (${description})`);
43
28
 
44
29
  if (lines.length === 0) {
45
30
  output.logSingleLine('No changes were made');
@@ -48,9 +33,6 @@ export function printChanges(context: Context, key: string, meta: ChangeMetadata
48
33
  }
49
34
  }
50
35
 
51
- /**
52
- * Writes all changes from the context to the filesystem.
53
- */
54
36
  export function flushChanges(context: Context) {
55
37
  const basePath = context.basePath;
56
38
  const changes = context.listChanges();
@@ -122,9 +104,6 @@ export async function formatFiles(context: Context) {
122
104
  // (This runs for each codemod used in an update)
123
105
  let packageJsonInstallCache: string;
124
106
 
125
- /**
126
- * Installs NPM dependencies if package.json has changed.
127
- */
128
107
  export function installNPMDependencies(context: Context) {
129
108
  const hasPackageJsonChanges = Object.entries(context.listChanges()).some(
130
109
  ([filePath, { changeType }]) => filePath === 'package.json' && changeType === 'update'
@@ -152,9 +131,6 @@ export function installNPMDependencies(context: Context) {
152
131
  }
153
132
  }
154
133
 
155
- /**
156
- * Reads and parses a JSON file from the context.
157
- */
158
134
  export function readJsonFile<T extends object = any>(context: Context, path: string): T {
159
135
  if (!context.doesFileExist(path)) {
160
136
  throw new Error(`Cannot find ${path}`);
@@ -166,9 +142,6 @@ export function readJsonFile<T extends object = any>(context: Context, path: str
166
142
  }
167
143
  }
168
144
 
169
- /**
170
- * Adds or updates dependencies in package.json, preventing downgrades.
171
- */
172
145
  export function addDependenciesToPackageJson(
173
146
  context: Context,
174
147
  dependencies: Record<string, string>,
@@ -233,9 +206,6 @@ export function addDependenciesToPackageJson(
233
206
  context.updateFile(packageJsonPath, JSON.stringify(updatedPackageJson, null, 2));
234
207
  }
235
208
 
236
- /**
237
- * Removes dependencies from package.json.
238
- */
239
209
  export function removeDependenciesFromPackageJson(
240
210
  context: Context,
241
211
  dependencies: string[],
@@ -276,8 +246,13 @@ const DIST_TAGS = {
276
246
 
277
247
  /**
278
248
  * Compares two version strings to determine if the incoming version is greater
249
+ *
250
+ * @param incomingVersion - The incoming version to compare.
251
+ * @param existingVersion - The existing version to compare.
252
+ * @param orEqualTo - Whether to check for greater than or equal to (>=) instead of just greater than (>).
253
+ *
279
254
  */
280
- export function isVersionGreater(incomingVersion: string, existingVersion: string): boolean {
255
+ export function isVersionGreater(incomingVersion: string, existingVersion: string, orEqualTo = false) {
281
256
  const incomingIsDistTag = incomingVersion in DIST_TAGS;
282
257
  const existingIsDistTag = existingVersion in DIST_TAGS;
283
258
 
@@ -301,6 +276,10 @@ export function isVersionGreater(incomingVersion: string, existingVersion: strin
301
276
  return true;
302
277
  }
303
278
 
279
+ if (orEqualTo) {
280
+ return gte(incomingSemver, existingSemver);
281
+ }
282
+
304
283
  return gt(incomingSemver, existingSemver);
305
284
  }
306
285
 
@@ -319,3 +298,16 @@ function sortObjectByKeys<T extends Record<string, any>>(obj: T): T {
319
298
  .sort()
320
299
  .reduce((acc, key) => ({ ...acc, [key]: obj[key] }), {} as T);
321
300
  }
301
+
302
+ /**
303
+ * Resolves a script path relative to the caller's file location.
304
+ *
305
+ * @param callerUrl - The import.meta.url from the calling file
306
+ * @param relativePath - The relative path to resolve (e.g., './scripts/example.js')
307
+ * @returns The absolute resolved path
308
+ */
309
+ export function resolveScriptPath(callerUrl: string, relativePath: string): string {
310
+ const __filename = fileURLToPath(callerUrl);
311
+ const __dirname = dirname(__filename);
312
+ return resolve(__dirname, relativePath);
313
+ }
@@ -1,7 +1,7 @@
1
- import { getAdditionFlags, getAvailableAdditions, runAdditionByName } from '../codemods/additions/manager.js';
2
- import { isGitDirectory, isGitDirectoryClean } from '../utils/utils.git.js';
3
-
4
- import { isPluginDirectory } from '../utils/utils.plugin.js';
1
+ import defaultAdditions from '../codemods/additions/additions.js';
2
+ import { runCodemod } from '../codemods/runner.js';
3
+ import { getPackageManagerExecCmd, getPackageManagerFromUserAgent } from '../utils/utils.packageManager.js';
4
+ import { performPreCodemodChecks } from '../utils/utils.checks.js';
5
5
  import minimist from 'minimist';
6
6
  import { output } from '../utils/utils.console.js';
7
7
 
@@ -13,10 +13,27 @@ export const add = async (argv: minimist.ParsedArgs) => {
13
13
  process.exit(1);
14
14
  }
15
15
 
16
- await performPreAddChecks(argv);
16
+ await performPreCodemodChecks(argv);
17
17
 
18
18
  try {
19
- await runAdditionByName(subCommand, argv);
19
+ const addition = defaultAdditions.find((addition) => addition.name === subCommand);
20
+ if (!addition) {
21
+ const additionsList = defaultAdditions.map((addition) => addition.name);
22
+ throw new Error(`Unknown addition: ${subCommand}\n\nAvailable additions: ${additionsList.join(', ')}`);
23
+ }
24
+
25
+ output.log({
26
+ title: `Running addition: ${addition.name}`,
27
+ body: [addition.description],
28
+ });
29
+
30
+ // filter out minimist internal properties (_ and $0) before passing to codemod
31
+ const { _, $0, ...codemodOptions } = argv;
32
+ await runCodemod(addition, codemodOptions);
33
+
34
+ output.success({
35
+ title: `Successfully added ${addition.name} to your plugin.`,
36
+ });
20
37
  } catch (error) {
21
38
  if (error instanceof Error) {
22
39
  output.error({
@@ -29,69 +46,16 @@ export const add = async (argv: minimist.ParsedArgs) => {
29
46
  };
30
47
 
31
48
  async function showAdditionsHelp() {
32
- const availableAdditions = getAvailableAdditions();
33
- const additionsList = await Promise.all(
34
- Object.values(availableAdditions).map(async (addition) => {
35
- let info = `${addition.name} - ${addition.description}`;
36
- const flags = await getAdditionFlags(addition);
37
- if (flags.length > 0) {
38
- const flagDocs = flags.map((flag) => {
39
- const req = flag.required ? ' (required)' : ' (optional)';
40
- return ` --${flag.name}: ${flag.description}${req}`;
41
- });
42
- info += '\n' + flagDocs.join('\n');
43
- }
44
- return info;
45
- })
46
- );
49
+ const additionsList = defaultAdditions.map((addition) => addition.name);
50
+ const { packageManagerName, packageManagerVersion } = getPackageManagerFromUserAgent();
47
51
 
48
52
  output.error({
49
53
  title: 'No addition specified',
50
54
  body: [
51
- 'Usage: npx @grafana/create-plugin add <addition-name> [options]',
55
+ `Usage: ${getPackageManagerExecCmd(packageManagerName, packageManagerVersion)} add <addition-name> [options]`,
52
56
  '',
53
57
  'Available additions:',
54
58
  ...output.bulletList(additionsList),
55
59
  ],
56
60
  });
57
61
  }
58
-
59
- async function performPreAddChecks(argv: minimist.ParsedArgs) {
60
- if (!(await isGitDirectory()) && !argv.force) {
61
- output.error({
62
- title: 'You are not inside a git directory',
63
- body: [
64
- `In order to proceed please run ${output.formatCode('git init')} in the root of your project and commit your changes.`,
65
- `(This check is necessary to make sure that the changes are easy to revert and don't interfere with any changes you currently have.`,
66
- `In case you want to proceed as is please use the ${output.formatCode('--force')} flag.)`,
67
- ],
68
- });
69
-
70
- process.exit(1);
71
- }
72
-
73
- if (!(await isGitDirectoryClean()) && !argv.force) {
74
- output.error({
75
- title: 'Please clean your repository working tree before adding features.',
76
- body: [
77
- 'Commit your changes or stash them.',
78
- `(This check is necessary to make sure that the changes are easy to revert and don't mess with any changes you currently have.`,
79
- `In case you want to proceed as is please use the ${output.formatCode('--force')} flag.)`,
80
- ],
81
- });
82
-
83
- process.exit(1);
84
- }
85
-
86
- if (!isPluginDirectory() && !argv.force) {
87
- output.error({
88
- title: 'Are you inside a plugin directory?',
89
- body: [
90
- `We couldn't find a "src/plugin.json" file under your current directory.`,
91
- `(Please make sure to run this command from the root of your plugin folder. In case you want to proceed as is please use the ${output.formatCode('--force')} flag.)`,
92
- ],
93
- });
94
-
95
- process.exit(1);
96
- }
97
- }
@@ -5,18 +5,17 @@ import {
5
5
  getPackageManagerWithFallback,
6
6
  } from '../utils/utils.packageManager.js';
7
7
  import { gte, lt } from 'semver';
8
- import { isGitDirectory, isGitDirectoryClean } from '../utils/utils.git.js';
8
+ import { performPreCodemodChecks } from '../utils/utils.checks.js';
9
9
 
10
10
  import { CURRENT_APP_VERSION } from '../utils/utils.version.js';
11
11
  import { LEGACY_UPDATE_CUTOFF_VERSION } from '../constants.js';
12
12
  import { getConfig } from '../utils/utils.config.js';
13
- import { isPluginDirectory } from '../utils/utils.plugin.js';
14
13
  import minimist from 'minimist';
15
14
  import { output } from '../utils/utils.console.js';
16
15
  import { spawnSync } from 'node:child_process';
17
16
 
18
17
  export const update = async (argv: minimist.ParsedArgs) => {
19
- await performPreUpdateChecks(argv);
18
+ await performPreCodemodChecks(argv);
20
19
  const { version } = getConfig();
21
20
 
22
21
  if (lt(version, LEGACY_UPDATE_CUTOFF_VERSION)) {
@@ -32,9 +31,13 @@ export const update = async (argv: minimist.ParsedArgs) => {
32
31
  process.exit(0);
33
32
  }
34
33
 
35
- const commitEachMigration = argv.commit;
36
34
  const migrations = getMigrationsToRun(version, CURRENT_APP_VERSION);
37
- await runMigrations(migrations, { commitEachMigration });
35
+ // filter out minimist internal properties (_ and $0) before passing to codemod
36
+ const { _, $0, ...codemodOptions } = argv;
37
+ await runMigrations(migrations, {
38
+ commitEachMigration: !!argv.commit,
39
+ codemodOptions,
40
+ });
38
41
  output.success({
39
42
  title: `Successfully updated create-plugin from ${version} to ${CURRENT_APP_VERSION}.`,
40
43
  });
@@ -49,48 +52,6 @@ export const update = async (argv: minimist.ParsedArgs) => {
49
52
  }
50
53
  };
51
54
 
52
- async function performPreUpdateChecks(argv: minimist.ParsedArgs) {
53
- if (!(await isGitDirectory()) && !argv.force) {
54
- output.error({
55
- title: 'You are not inside a git directory',
56
- body: [
57
- `In order to proceed please run ${output.formatCode('git init')} in the root of your project and commit your changes.`,
58
- `(This check is necessary to make sure that the updates are easy to revert and don't interfere with any changes you currently have.`,
59
- `In case you want to proceed as is please use the ${output.formatCode('--force')} flag.)`,
60
- ],
61
- });
62
-
63
- process.exit(1);
64
- }
65
-
66
- if (!(await isGitDirectoryClean()) && !argv.force) {
67
- output.error({
68
- title: 'Please clean your repository working tree before updating.',
69
- body: [
70
- 'Commit your changes or stash them.',
71
- `(This check is necessary to make sure that the updates are easy to revert and don't mess with any changes you currently have.`,
72
- `In case you want to proceed as is please use the ${output.formatCode('--force')} flag.)`,
73
- ],
74
- });
75
-
76
- process.exit(1);
77
- }
78
-
79
- if (!isPluginDirectory() && !argv.force) {
80
- output.error({
81
- title: 'Are you inside a plugin directory?',
82
- body: [
83
- `We couldn't find a "src/plugin.json" file under your current directory.`,
84
- `(Please make sure to run this command from the root of your plugin folder. In case you want to proceed as is please use the ${output.formatCode(
85
- '--force'
86
- )} flag.)`,
87
- ],
88
- });
89
-
90
- process.exit(1);
91
- }
92
- }
93
-
94
55
  /**
95
56
  * Prepares a plugin for migrations by running the legacy update command and installing dependencies.
96
57
  * This is a one time operation that ensures the plugin configs are "as expected" by the new migration system.
@@ -0,0 +1,44 @@
1
+ import { LEGACY_UPDATE_CUTOFF_VERSION } from '../constants.js';
2
+
3
+ export type MigrationMeta = {
4
+ version: string;
5
+ description: string;
6
+ migrationScript: string;
7
+ };
8
+
9
+ type Migrations = {
10
+ migrations: Record<string, MigrationMeta>;
11
+ };
12
+
13
+ // Do not use LEGACY_UPDATE_CUTOFF_VERSION for new migrations. It was used to force migrations to run
14
+ // for those written before the switch to updates as migrations.
15
+ export default {
16
+ migrations: {
17
+ '001-update-grafana-compose-extend': {
18
+ version: LEGACY_UPDATE_CUTOFF_VERSION,
19
+ description: 'Update ./docker-compose.yaml to extend from ./.config/docker-compose-base.yaml.',
20
+ migrationScript: './scripts/001-update-grafana-compose-extend.js',
21
+ },
22
+ '002-update-is-compatible-workflow': {
23
+ version: LEGACY_UPDATE_CUTOFF_VERSION,
24
+ description:
25
+ 'Update ./.github/workflows/is-compatible.yml to use is-compatible github action instead of calling levitate directly',
26
+ migrationScript: './scripts/002-update-is-compatible-workflow.js',
27
+ },
28
+ '003-update-eslint-deprecation-rule': {
29
+ version: LEGACY_UPDATE_CUTOFF_VERSION,
30
+ description: 'Replace deprecated eslint-plugin-deprecation with @typescript-eslint/no-deprecated rule.',
31
+ migrationScript: './scripts/003-update-eslint-deprecation-rule.js',
32
+ },
33
+ '004-eslint9-flat-config': {
34
+ version: LEGACY_UPDATE_CUTOFF_VERSION,
35
+ description: 'Migrate eslint config to flat config format and update devDependencies to latest versions.',
36
+ migrationScript: './scripts/004-eslint9-flat-config.js',
37
+ },
38
+ '005-react-18-3': {
39
+ version: '6.1.9',
40
+ description: 'Update React and ReactDOM 18.x versions to ^18.3.0 to surface React 19 compatibility issues.',
41
+ migrationScript: './scripts/005-react-18-3.js',
42
+ },
43
+ },
44
+ } as Migrations;
@@ -0,0 +1,47 @@
1
+ import minimist from 'minimist';
2
+ import { isGitDirectory, isGitDirectoryClean } from './utils.git.js';
3
+ import { isPluginDirectory } from './utils.plugin.js';
4
+ import { output } from './utils.console.js';
5
+
6
+ /**
7
+ * Ensures git directory exists, is clean, and we're in a plugin directory
8
+ */
9
+ export async function performPreCodemodChecks(argv: minimist.ParsedArgs): Promise<void> {
10
+ if (!(await isGitDirectory()) && !argv.force) {
11
+ output.error({
12
+ title: 'You are not inside a git directory',
13
+ body: [
14
+ `In order to proceed please run ${output.formatCode('git init')} in the root of your project and commit your changes.`,
15
+ `(This check is necessary to make sure that changes are easy to revert and don't interfere with any changes you currently have.`,
16
+ `In case you want to proceed as is please use the ${output.formatCode('--force')} flag.)`,
17
+ ],
18
+ });
19
+
20
+ process.exit(1);
21
+ }
22
+
23
+ if (!(await isGitDirectoryClean()) && !argv.force) {
24
+ output.error({
25
+ title: 'Please clean your repository working tree before making changes.',
26
+ body: [
27
+ 'Commit your changes or stash them.',
28
+ `(This check is necessary to make sure that changes are easy to revert and don't mess with any changes you currently have.`,
29
+ `In case you want to proceed as is please use the ${output.formatCode('--force')} flag.)`,
30
+ ],
31
+ });
32
+
33
+ process.exit(1);
34
+ }
35
+
36
+ if (!isPluginDirectory() && !argv.force) {
37
+ output.error({
38
+ title: 'Are you inside a plugin directory?',
39
+ body: [
40
+ `We couldn't find a "src/plugin.json" file under your current directory.`,
41
+ `(Please make sure to run this command from the root of your plugin folder. In case you want to proceed as is please use the ${output.formatCode('--force')} flag.)`,
42
+ ],
43
+ });
44
+
45
+ process.exit(1);
46
+ }
47
+ }
@@ -1,6 +1,4 @@
1
1
  import { argv, commandName } from './utils.cli.js';
2
-
3
- import type { AdditionFeatureName } from '../codemods/additions/additions.js';
4
2
  import { CURRENT_APP_VERSION } from './utils.version.js';
5
3
  import { DEFAULT_FEATURE_FLAGS } from '../constants.js';
6
4
  import fs from 'node:fs';
@@ -10,7 +8,7 @@ import path from 'node:path';
10
8
  import { writeFile } from 'node:fs/promises';
11
9
  import { EOL } from 'node:os';
12
10
 
13
- type CoreFeatureFlags = {
11
+ type FeatureFlags = {
14
12
  bundleGrafanaUI?: boolean;
15
13
 
16
14
  // If set to true, the plugin will be scaffolded with React Router v6. Defaults to true.
@@ -21,16 +19,6 @@ type CoreFeatureFlags = {
21
19
  useExperimentalUpdates?: boolean;
22
20
  };
23
21
 
24
- type AdditionFeatureFlags = {
25
- [K in AdditionFeatureName]?: boolean;
26
- };
27
-
28
- export type FeatureFlags = CoreFeatureFlags & AdditionFeatureFlags;
29
-
30
- export function isFeatureEnabled(features: FeatureFlags, featureName: AdditionFeatureName): boolean {
31
- return features[featureName] === true;
32
- }
33
-
34
22
  export type CreatePluginConfig = UserConfig & {
35
23
  version: string;
36
24
  };
@@ -144,18 +132,3 @@ export async function setRootConfig(configOverride: Partial<CreatePluginConfig>
144
132
 
145
133
  return updatedConfig;
146
134
  }
147
-
148
- export async function setFeatureFlag(featureName: string, enabled = true): Promise<void> {
149
- const userConfig = getUserConfig() || { features: {} };
150
- const userConfigPath = path.resolve(process.cwd(), '.cprc.json');
151
-
152
- const updatedConfig = {
153
- ...userConfig,
154
- features: {
155
- ...userConfig.features,
156
- [featureName]: enabled,
157
- },
158
- };
159
-
160
- await writeFile(userConfigPath, JSON.stringify(updatedConfig, null, 2) + EOL);
161
- }