@grafana/create-plugin 6.2.0-canary.2314.19505018775.0 → 6.2.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 (71) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/bin/run.js +3 -1
  3. package/dist/codemods/additions/additions.js +9 -0
  4. package/dist/codemods/additions/scripts/example-addition.js +47 -0
  5. package/dist/{migrations → codemods}/context.js +3 -2
  6. package/dist/codemods/migrations/manager.js +32 -0
  7. package/dist/codemods/migrations/migrations.js +50 -0
  8. package/dist/{migrations → codemods/migrations}/scripts/003-update-eslint-deprecation-rule.js +1 -1
  9. package/dist/{migrations → codemods/migrations}/scripts/004-eslint9-flat-config.js +1 -1
  10. package/dist/{migrations → codemods/migrations}/scripts/005-react-18-3.js +1 -1
  11. package/dist/{migrations → codemods/migrations}/scripts/007-remove-testing-library-types.js +1 -1
  12. package/dist/codemods/runner.js +33 -0
  13. package/dist/codemods/schema-parser.js +20 -0
  14. package/dist/{migrations → codemods}/utils.js +5 -16
  15. package/dist/commands/add.command.js +51 -0
  16. package/dist/commands/update.command.js +8 -42
  17. package/dist/utils/utils.checks.js +40 -0
  18. package/package.json +3 -2
  19. package/src/bin/run.ts +2 -1
  20. package/src/codemods/additions/additions.test.ts +17 -0
  21. package/src/codemods/additions/additions.ts +9 -0
  22. package/src/codemods/additions/scripts/example-addition.test.ts +92 -0
  23. package/src/codemods/additions/scripts/example-addition.ts +62 -0
  24. package/src/{migrations → codemods}/context.test.ts +10 -10
  25. package/src/{migrations → codemods}/context.ts +4 -2
  26. package/src/codemods/migrations/fixtures/migrations.ts +20 -0
  27. package/src/{migrations → codemods/migrations}/manager.test.ts +76 -77
  28. package/src/codemods/migrations/manager.ts +50 -0
  29. package/src/codemods/migrations/migrations.test.ts +17 -0
  30. package/src/codemods/migrations/migrations.ts +55 -0
  31. package/src/{migrations → codemods/migrations}/scripts/001-update-grafana-compose-extend.test.ts +1 -1
  32. package/src/{migrations → codemods/migrations}/scripts/001-update-grafana-compose-extend.ts +1 -1
  33. package/src/{migrations → codemods/migrations}/scripts/002-update-is-compatible-workflow.test.ts +1 -1
  34. package/src/{migrations → codemods/migrations}/scripts/002-update-is-compatible-workflow.ts +1 -1
  35. package/src/{migrations → codemods/migrations}/scripts/003-update-eslint-deprecation-rule.test.ts +1 -1
  36. package/src/{migrations → codemods/migrations}/scripts/003-update-eslint-deprecation-rule.ts +2 -2
  37. package/src/{migrations → codemods/migrations}/scripts/004-eslint9-flat-config.test.ts +1 -1
  38. package/src/{migrations → codemods/migrations}/scripts/004-eslint9-flat-config.ts +2 -2
  39. package/src/{migrations → codemods/migrations}/scripts/005-react-18-3.test.ts +1 -1
  40. package/src/{migrations → codemods/migrations}/scripts/005-react-18-3.ts +2 -2
  41. package/src/{migrations → codemods/migrations}/scripts/006-webpack-nested-fix.test.ts +1 -1
  42. package/src/{migrations → codemods/migrations}/scripts/006-webpack-nested-fix.ts +1 -1
  43. package/src/{migrations → codemods/migrations}/scripts/007-remove-testing-library-types.test.ts +1 -1
  44. package/src/{migrations → codemods/migrations}/scripts/007-remove-testing-library-types.ts +2 -2
  45. package/src/codemods/runner.ts +51 -0
  46. package/src/codemods/schema-parser.ts +27 -0
  47. package/src/codemods/types.ts +16 -0
  48. package/src/{migrations → codemods}/utils.test.ts +5 -4
  49. package/src/{migrations → codemods}/utils.ts +8 -22
  50. package/src/commands/add.command.ts +56 -0
  51. package/src/commands/index.ts +1 -0
  52. package/src/commands/update.command.ts +9 -48
  53. package/src/utils/utils.checks.ts +47 -0
  54. package/dist/migrations/manager.js +0 -58
  55. package/dist/migrations/migrations.js +0 -43
  56. package/dist/migrations/scripts/example-migration.js +0 -25
  57. package/src/migrations/fixtures/migrations.ts +0 -19
  58. package/src/migrations/manager.ts +0 -82
  59. package/src/migrations/migrations.test.ts +0 -16
  60. package/src/migrations/migrations.ts +0 -55
  61. package/src/migrations/scripts/example-migration.test.ts +0 -40
  62. package/src/migrations/scripts/example-migration.ts +0 -34
  63. package/templates/panel/.config/AGENTS/fundamentals.md +0 -81
  64. package/templates/panel/.config/AGENTS/howto/add-panel-options.md +0 -130
  65. package/templates/panel/AGENTS.md +0 -3
  66. /package/dist/{migrations → codemods/migrations}/scripts/001-update-grafana-compose-extend.js +0 -0
  67. /package/dist/{migrations → codemods/migrations}/scripts/002-update-is-compatible-workflow.js +0 -0
  68. /package/dist/{migrations → codemods/migrations}/scripts/006-webpack-nested-fix.js +0 -0
  69. /package/src/{migrations → codemods/migrations}/fixtures/foo/bar.ts +0 -0
  70. /package/src/{migrations → codemods/migrations}/fixtures/foo/baz.ts +0 -0
  71. /package/src/{migrations → codemods}/test-utils.ts +0 -0
@@ -0,0 +1,92 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { Context } from '../../context.js';
4
+ import migrate from './example-addition.js';
5
+
6
+ describe('example-addition', () => {
7
+ it('should add example script to package.json', () => {
8
+ const context = new Context('/virtual');
9
+
10
+ context.addFile('package.json', JSON.stringify({ scripts: {}, dependencies: {}, devDependencies: {} }));
11
+
12
+ const result = migrate(context, { featureName: 'testFeature', enabled: true, frameworks: ['react'] });
13
+
14
+ const packageJson = JSON.parse(result.getFile('package.json') || '{}');
15
+ expect(packageJson.scripts['example-script']).toBe('echo "Running testFeature"');
16
+ });
17
+
18
+ it('should add dev dependency', () => {
19
+ const context = new Context('/virtual');
20
+
21
+ context.addFile('package.json', JSON.stringify({ scripts: {}, dependencies: {}, devDependencies: {} }));
22
+
23
+ const result = migrate(context, { featureName: 'myFeature', enabled: false, frameworks: ['react'] });
24
+
25
+ const packageJson = JSON.parse(result.getFile('package.json') || '{}');
26
+ expect(packageJson.devDependencies['@types/node']).toBe('^20.0.0');
27
+ });
28
+
29
+ it('should create feature TypeScript file with options', () => {
30
+ const context = new Context('/virtual');
31
+
32
+ context.addFile('package.json', JSON.stringify({ scripts: {}, dependencies: {}, devDependencies: {} }));
33
+
34
+ const result = migrate(context, {
35
+ featureName: 'myFeature',
36
+ enabled: false,
37
+ port: 4000,
38
+ frameworks: ['react', 'vue'],
39
+ });
40
+
41
+ expect(result.doesFileExist('src/features/myFeature.ts')).toBe(true);
42
+ const featureCode = result.getFile('src/features/myFeature.ts');
43
+ expect(featureCode).toContain('export const myFeature');
44
+ expect(featureCode).toContain('enabled: false');
45
+ expect(featureCode).toContain('port: 4000');
46
+ expect(featureCode).toContain('frameworks: ["react","vue"]');
47
+ expect(featureCode).toContain('myFeature initialized on port 4000');
48
+ });
49
+
50
+ it('should delete deprecated file if it exists', () => {
51
+ const context = new Context('/virtual');
52
+
53
+ context.addFile('package.json', JSON.stringify({ scripts: {}, dependencies: {}, devDependencies: {} }));
54
+ context.addFile('src/deprecated.ts', 'export const old = true;');
55
+
56
+ const result = migrate(context, { featureName: 'testFeature', enabled: true, frameworks: ['react'] });
57
+
58
+ expect(result.doesFileExist('src/deprecated.ts')).toBe(false);
59
+ });
60
+
61
+ it('should rename old-config.json if it exists', () => {
62
+ const context = new Context('/virtual');
63
+
64
+ context.addFile('package.json', JSON.stringify({ scripts: {}, dependencies: {}, devDependencies: {} }));
65
+ context.addFile('src/old-config.json', JSON.stringify({ old: true }));
66
+
67
+ const result = migrate(context, { featureName: 'testFeature', enabled: true, frameworks: ['react'] });
68
+
69
+ expect(result.doesFileExist('src/old-config.json')).toBe(false);
70
+ expect(result.doesFileExist('src/new-config.json')).toBe(true);
71
+ const newConfig = JSON.parse(result.getFile('src/new-config.json') || '{}');
72
+ expect(newConfig.old).toBe(true);
73
+ });
74
+
75
+ it('should not add script if it already exists', () => {
76
+ const context = new Context('/virtual');
77
+
78
+ context.addFile(
79
+ 'package.json',
80
+ JSON.stringify({
81
+ scripts: { 'example-script': 'existing command' },
82
+ dependencies: {},
83
+ devDependencies: {},
84
+ })
85
+ );
86
+
87
+ const result = migrate(context, { featureName: 'testFeature', enabled: true, frameworks: ['react'] });
88
+
89
+ const packageJson = JSON.parse(result.getFile('package.json') || '{}');
90
+ expect(packageJson.scripts['example-script']).toBe('existing command');
91
+ });
92
+ });
@@ -0,0 +1,62 @@
1
+ import * as v from 'valibot';
2
+ import type { Context } from '../../context.js';
3
+ import { addDependenciesToPackageJson } from '../../utils.js';
4
+
5
+ /**
6
+ * Example addition demonstrating Valibot schema with type inference
7
+ * Schema defines validation rules, defaults and types are automatically inferred
8
+ */
9
+ export const schema = v.object({
10
+ featureName: v.pipe(
11
+ v.string(),
12
+ v.minLength(3, 'Feature name must be at least 3 characters'),
13
+ v.maxLength(50, 'Feature name must be at most 50 characters')
14
+ ),
15
+ enabled: v.optional(v.boolean(), true),
16
+ port: v.optional(
17
+ v.pipe(v.number(), v.minValue(1000, 'Port must be at least 1000'), v.maxValue(65535, 'Port must be at most 65535'))
18
+ ),
19
+ frameworks: v.optional(v.array(v.string()), ['react']),
20
+ });
21
+
22
+ // Type is automatically inferred from the schema
23
+ type ExampleOptions = v.InferOutput<typeof schema>;
24
+
25
+ export default function exampleAddition(context: Context, options: ExampleOptions): Context {
26
+ // These options have been validated by the framework
27
+ const { featureName, enabled, port, frameworks } = options;
28
+
29
+ const rawPkgJson = context.getFile('./package.json') ?? '{}';
30
+ const packageJson = JSON.parse(rawPkgJson);
31
+
32
+ if (packageJson.scripts && !packageJson.scripts['example-script']) {
33
+ packageJson.scripts['example-script'] = `echo "Running ${featureName}"`;
34
+ context.updateFile('./package.json', JSON.stringify(packageJson, null, 2));
35
+ }
36
+
37
+ addDependenciesToPackageJson(context, {}, { '@types/node': '^20.0.0' });
38
+
39
+ if (!context.doesFileExist(`./src/features/${featureName}.ts`)) {
40
+ const featureCode = `export const ${featureName} = {
41
+ name: '${featureName}',
42
+ enabled: ${enabled},
43
+ port: ${port ?? 3000},
44
+ frameworks: ${JSON.stringify(frameworks)},
45
+ init() {
46
+ console.log('${featureName} initialized on port ${port ?? 3000}');
47
+ },
48
+ };
49
+ `;
50
+ context.addFile(`./src/features/${featureName}.ts`, featureCode);
51
+ }
52
+
53
+ if (context.doesFileExist('./src/deprecated.ts')) {
54
+ context.deleteFile('./src/deprecated.ts');
55
+ }
56
+
57
+ if (context.doesFileExist('./src/old-config.json')) {
58
+ context.renameFile('./src/old-config.json', './src/new-config.json');
59
+ }
60
+
61
+ return context;
62
+ }
@@ -3,7 +3,7 @@ import { Context } from './context.js';
3
3
  describe('Context', () => {
4
4
  describe('getFile', () => {
5
5
  it('should read a file from the file system', () => {
6
- const context = new Context(`${__dirname}/fixtures`);
6
+ const context = new Context(`${__dirname}/migrations/fixtures`);
7
7
  const content = context.getFile('foo/bar.ts');
8
8
  expect(content).toEqual("console.log('foo/bar.ts');\n");
9
9
  });
@@ -16,14 +16,14 @@ describe('Context', () => {
16
16
  });
17
17
 
18
18
  it('should get a file that was updated in the current context', () => {
19
- const context = new Context(`${__dirname}/fixtures`);
19
+ const context = new Context(`${__dirname}/migrations/fixtures`);
20
20
  context.updateFile('foo/bar.ts', 'content');
21
21
  const content = context.getFile('foo/bar.ts');
22
22
  expect(content).toEqual('content');
23
23
  });
24
24
 
25
25
  it('should not return a file that was marked for deletion', () => {
26
- const context = new Context(`${__dirname}/fixtures`);
26
+ const context = new Context(`${__dirname}/migrations/fixtures`);
27
27
  context.deleteFile('foo/bar.ts');
28
28
  const content = context.getFile('foo/bar.ts');
29
29
  expect(content).toEqual(undefined);
@@ -77,7 +77,7 @@ describe('Context', () => {
77
77
 
78
78
  describe('renameFile', () => {
79
79
  it('should rename a file', () => {
80
- const context = new Context(`${__dirname}/fixtures`);
80
+ const context = new Context(`${__dirname}/migrations/fixtures`);
81
81
  context.renameFile('foo/bar.ts', 'new-file.txt');
82
82
  expect(context.listChanges()).toEqual({
83
83
  'new-file.txt': { content: "console.log('foo/bar.ts');\n", changeType: 'add' },
@@ -102,20 +102,20 @@ describe('Context', () => {
102
102
 
103
103
  describe('readDir', () => {
104
104
  it('should read the directory', () => {
105
- const context = new Context(`${__dirname}/fixtures`);
105
+ const context = new Context(`${__dirname}/migrations/fixtures`);
106
106
  const files = context.readDir('foo');
107
107
  expect(files).toEqual(['foo/bar.ts', 'foo/baz.ts']);
108
108
  });
109
109
 
110
110
  it('should filter out deleted files', () => {
111
- const context = new Context(`${__dirname}/fixtures`);
111
+ const context = new Context(`${__dirname}/migrations/fixtures`);
112
112
  context.deleteFile('foo/bar.ts');
113
113
  const files = context.readDir('foo');
114
114
  expect(files).toEqual(['foo/baz.ts']);
115
115
  });
116
116
 
117
117
  it('should include files that are only added to the context', () => {
118
- const context = new Context(`${__dirname}/fixtures`);
118
+ const context = new Context(`${__dirname}/migrations/fixtures`);
119
119
  context.addFile('foo/foo.txt', '');
120
120
  const files = context.readDir('foo');
121
121
  expect(files).toEqual(['foo/bar.ts', 'foo/baz.ts', 'foo/foo.txt']);
@@ -124,7 +124,7 @@ describe('Context', () => {
124
124
 
125
125
  describe('normalisePath', () => {
126
126
  it('should normalise the path', () => {
127
- const context = new Context(`${__dirname}/fixtures`);
127
+ const context = new Context(`${__dirname}/migrations/fixtures`);
128
128
  expect(context.normalisePath('foo/bar.ts')).toEqual('foo/bar.ts');
129
129
  expect(context.normalisePath('./foo/bar.ts')).toEqual('foo/bar.ts');
130
130
  expect(context.normalisePath('/foo/bar.ts')).toEqual('foo/bar.ts');
@@ -133,12 +133,12 @@ describe('Context', () => {
133
133
 
134
134
  describe('hasChanges', () => {
135
135
  it('should return FALSE if the context has no changes', () => {
136
- const context = new Context(`${__dirname}/fixtures`);
136
+ const context = new Context(`${__dirname}/migrations/fixtures`);
137
137
  expect(context.hasChanges()).toEqual(false);
138
138
  });
139
139
 
140
140
  it('should return TRUE if the context has changes', () => {
141
- const context = new Context(`${__dirname}/fixtures`);
141
+ const context = new Context(`${__dirname}/migrations/fixtures`);
142
142
 
143
143
  context.addFile('foo.ts', '');
144
144
 
@@ -1,6 +1,8 @@
1
1
  import { constants, accessSync, readFileSync, readdirSync } from 'node:fs';
2
2
  import { relative, normalize, join, dirname } from 'node:path';
3
- import { migrationsDebug } from './utils.js';
3
+ import { debug } from '../utils/utils.cli.js';
4
+
5
+ const codemodsDebug = debug.extend('codemods');
4
6
 
5
7
  export type ContextFile = Record<
6
8
  string,
@@ -58,7 +60,7 @@ export class Context {
58
60
  if (originalContent !== content) {
59
61
  this.files[path] = { content, changeType: 'update' };
60
62
  } else {
61
- migrationsDebug(`Context.updateFile() - no updates for ${filePath}`);
63
+ codemodsDebug(`Context.updateFile() - no updates for ${filePath}`);
62
64
  }
63
65
  }
64
66
 
@@ -0,0 +1,20 @@
1
+ export default [
2
+ {
3
+ name: 'migration-key1',
4
+ version: '5.0.0',
5
+ description: 'Update project to use new cache directory',
6
+ scriptPath: './5-0-0-cache-directory.js',
7
+ },
8
+ {
9
+ name: 'migration-key2',
10
+ version: '5.4.0',
11
+ description: 'Update project to use new cache directory',
12
+ scriptPath: './5-4-0-cache-directory.js',
13
+ },
14
+ {
15
+ name: 'migration-key3',
16
+ version: '6.0.0',
17
+ description: 'Update project to use new cache directory',
18
+ scriptPath: './5-4-0-cache-directory.js',
19
+ },
20
+ ];
@@ -1,22 +1,26 @@
1
- import { vi } from 'vitest';
2
- import { getMigrationsToRun, runMigration, runMigrations } from './manager.js';
1
+ import { flushChanges, formatFiles, printChanges } from '../utils.js';
2
+ import { getMigrationsToRun, runMigrations } from './manager.js';
3
+
4
+ import { Context } from '../context.js';
5
+ import { Migration } from './migrations.js';
6
+ import { gitCommitNoVerify } from '../../utils/utils.git.js';
3
7
  import migrationFixtures from './fixtures/migrations.js';
4
- import { Context } from './context.js';
5
- import { gitCommitNoVerify } from '../utils/utils.git.js';
6
- import { flushChanges, printChanges, formatFiles } from './utils.js';
7
- import { setRootConfig } from '../utils/utils.config.js';
8
- import { MigrationMeta } from './migrations.js';
9
-
10
- vi.mock('./utils.js', () => ({
11
- flushChanges: vi.fn(),
12
- printChanges: vi.fn(),
13
- migrationsDebug: vi.fn(),
14
- formatFiles: vi.fn(),
15
- installNPMDependencies: vi.fn(),
16
- }));
8
+ import { setRootConfig } from '../../utils/utils.config.js';
9
+ import { vi } from 'vitest';
10
+
11
+ vi.mock('../utils.js', async (importOriginal) => {
12
+ const actual: typeof import('../utils.js') = await importOriginal();
13
+ return {
14
+ ...actual,
15
+ flushChanges: vi.fn(),
16
+ formatFiles: vi.fn(),
17
+ installNPMDependencies: vi.fn(),
18
+ printChanges: vi.fn(),
19
+ };
20
+ });
17
21
 
18
22
  // Silence terminal output during tests.
19
- vi.mock('../utils/utils.console.js', () => ({
23
+ vi.mock('../../utils/utils.console.js', () => ({
20
24
  output: {
21
25
  log: vi.fn(),
22
26
  addHorizontalLine: vi.fn(),
@@ -25,10 +29,10 @@ vi.mock('../utils/utils.console.js', () => ({
25
29
  },
26
30
  }));
27
31
 
28
- vi.mock('../utils/utils.config.js', () => ({
32
+ vi.mock('../../utils/utils.config.js', () => ({
29
33
  setRootConfig: vi.fn(),
30
34
  }));
31
- vi.mock('../utils/utils.git.js', () => ({
35
+ vi.mock('../../utils/utils.git.js', () => ({
32
36
  gitCommitNoVerify: vi.fn(),
33
37
  }));
34
38
 
@@ -45,92 +49,83 @@ describe('Migrations', () => {
45
49
  it('should return the migrations that need to be run', () => {
46
50
  const fromVersion = '3.0.0';
47
51
  const toVersion = '5.0.0';
48
- const migrations = getMigrationsToRun(fromVersion, toVersion, migrationFixtures.migrations);
49
- expect(migrations).toEqual({
50
- 'migration-key1': {
52
+ const migrations = getMigrationsToRun(fromVersion, toVersion, migrationFixtures);
53
+ expect(migrations).toEqual([
54
+ {
55
+ name: 'migration-key1',
51
56
  version: '5.0.0',
52
57
  description: 'Update project to use new cache directory',
53
- migrationScript: './5-0-0-cache-directory.js',
58
+ scriptPath: './5-0-0-cache-directory.js',
54
59
  },
55
- });
60
+ ]);
56
61
 
57
62
  const fromVersion2 = '5.0.0';
58
63
  const toVersion2 = '5.5.0';
59
- const migrations2 = getMigrationsToRun(fromVersion2, toVersion2, migrationFixtures.migrations);
60
- expect(migrations2).toEqual({
61
- 'migration-key1': {
64
+ const migrations2 = getMigrationsToRun(fromVersion2, toVersion2, migrationFixtures);
65
+ expect(migrations2).toEqual([
66
+ {
67
+ name: 'migration-key1',
62
68
  version: '5.0.0',
63
69
  description: 'Update project to use new cache directory',
64
- migrationScript: './5-0-0-cache-directory.js',
70
+ scriptPath: './5-0-0-cache-directory.js',
65
71
  },
66
- 'migration-key2': {
72
+ {
73
+ name: 'migration-key2',
67
74
  version: '5.4.0',
68
75
  description: 'Update project to use new cache directory',
69
- migrationScript: './5-4-0-cache-directory.js',
76
+ scriptPath: './5-4-0-cache-directory.js',
70
77
  },
71
- });
78
+ ]);
72
79
 
73
80
  const fromVersion3 = '5.5.0';
74
81
  const toVersion3 = '6.0.0';
75
- const migrations3 = getMigrationsToRun(fromVersion3, toVersion3, migrationFixtures.migrations);
76
- expect(migrations3).toEqual({
77
- 'migration-key3': {
82
+ const migrations3 = getMigrationsToRun(fromVersion3, toVersion3, migrationFixtures);
83
+ expect(migrations3).toEqual([
84
+ {
85
+ name: 'migration-key3',
78
86
  version: '6.0.0',
79
87
  description: 'Update project to use new cache directory',
80
- migrationScript: './5-4-0-cache-directory.js',
88
+ scriptPath: './5-4-0-cache-directory.js',
81
89
  },
82
- });
90
+ ]);
83
91
  });
84
92
 
85
93
  it('should sort migrations by version', () => {
86
94
  const fromVersion = '2.0.0';
87
95
  const toVersion = '6.0.0';
88
- const migrations = getMigrationsToRun(fromVersion, toVersion, {
89
- 'migration-key1': {
96
+ const migrations = getMigrationsToRun(fromVersion, toVersion, [
97
+ {
98
+ name: 'migration-key1',
90
99
  version: '5.3.0',
91
100
  description: 'Update project to use new cache directory',
92
- migrationScript: './5.3.0-migration.js',
101
+ scriptPath: './5.3.0-migration.js',
93
102
  },
94
- 'migration-key2': {
103
+ {
104
+ name: 'migration-key2',
95
105
  version: '2.3.0',
96
106
  description: 'Update project to use new cache directory',
97
- migrationScript: './2.3.0-migration.js',
107
+ scriptPath: './2.3.0-migration.js',
98
108
  },
99
- 'migration-key3': {
109
+ {
110
+ name: 'migration-key3',
100
111
  version: '2.0.0',
101
112
  description: 'Update project to use new cache directory',
102
- migrationScript: './2.0.0-migration.js',
113
+ scriptPath: './2.0.0-migration.js',
103
114
  },
104
- 'migration-key4': {
115
+ {
116
+ name: 'migration-key4',
105
117
  version: '2.0.0',
106
118
  description: 'Update project to use new cache directory',
107
- migrationScript: './2.0.0-migration.js',
119
+ scriptPath: './2.0.0-migration.js',
108
120
  },
109
- });
110
-
111
- expect(Object.keys(migrations)).toEqual(['migration-key3', 'migration-key4', 'migration-key2', 'migration-key1']);
112
- });
113
- });
114
-
115
- describe('runMigration', () => {
116
- it('should pass a context to the migration script', async () => {
117
- const mockContext = new Context('/virtual');
118
- const migrationFn = vi.fn().mockResolvedValue(mockContext);
119
-
120
- vi.doMock('virtual-test-migration.js', () => ({
121
- default: migrationFn,
122
- }));
123
-
124
- const migration: MigrationMeta = {
125
- version: '1.0.0',
126
- description: 'test migration',
127
- migrationScript: 'virtual-test-migration.js',
128
- };
129
-
130
- const result = await runMigration(migration, mockContext);
131
-
132
- expect(migrationFn).toHaveBeenCalledWith(mockContext);
133
- expect(result).toBe(mockContext);
121
+ ]);
122
+
123
+ expect(migrations.map((m) => m.name)).toEqual([
124
+ 'migration-key3',
125
+ 'migration-key4',
126
+ 'migration-key2',
127
+ 'migration-key1',
128
+ ]);
134
129
  });
135
130
  });
136
131
 
@@ -146,18 +141,20 @@ describe('Migrations', () => {
146
141
  default: migrationTwoFn,
147
142
  }));
148
143
 
149
- const migrations: Record<string, MigrationMeta> = {
150
- 'migration-one': {
144
+ const migrations: Migration[] = [
145
+ {
146
+ name: 'migration-one',
151
147
  version: '1.0.0',
152
148
  description: '...',
153
- migrationScript: 'virtual-test-migration.js',
149
+ scriptPath: 'virtual-test-migration.js',
154
150
  },
155
- 'migration-two': {
151
+ {
152
+ name: 'migration-two',
156
153
  version: '1.2.0',
157
154
  description: '...',
158
- migrationScript: 'virtual-test-migration2.js',
155
+ scriptPath: 'virtual-test-migration2.js',
159
156
  },
160
- };
157
+ ];
161
158
 
162
159
  beforeEach(() => {
163
160
  migrationOneFn.mockImplementation(async (context: Context) => {
@@ -203,7 +200,8 @@ describe('Migrations', () => {
203
200
  it('should commit the changes for each migration if the CLI arg is present', async () => {
204
201
  await runMigrations(migrations, { commitEachMigration: true });
205
202
 
206
- expect(gitCommitNoVerify).toHaveBeenCalledTimes(2);
203
+ // 2 migration commits + 1 version update commit = 3 total
204
+ expect(gitCommitNoVerify).toHaveBeenCalledTimes(3);
207
205
  });
208
206
 
209
207
  it('should not create a commit for a migration that has no changes', async () => {
@@ -211,7 +209,8 @@ describe('Migrations', () => {
211
209
 
212
210
  await runMigrations(migrations, { commitEachMigration: true });
213
211
 
214
- expect(gitCommitNoVerify).toHaveBeenCalledTimes(1);
212
+ // 1 migration commit (only migration-one has changes) + 1 version update commit = 2 total
213
+ expect(gitCommitNoVerify).toHaveBeenCalledTimes(2);
215
214
  });
216
215
 
217
216
  it('should update version in ".config/.cprc.json" on a successful update', async () => {
@@ -0,0 +1,50 @@
1
+ import defaultMigrations, { Migration } from './migrations.js';
2
+ import { runCodemod } from '../runner.js';
3
+ import { gte, satisfies } from 'semver';
4
+ import { CURRENT_APP_VERSION } from '../../utils/utils.version.js';
5
+ import { gitCommitNoVerify } from '../../utils/utils.git.js';
6
+ import { output } from '../../utils/utils.console.js';
7
+ import { setRootConfig } from '../../utils/utils.config.js';
8
+
9
+ export function getMigrationsToRun(
10
+ fromVersion: string,
11
+ toVersion: string,
12
+ migrations: Migration[] = defaultMigrations
13
+ ): Migration[] {
14
+ const semverRange = `${fromVersion} - ${toVersion}`;
15
+
16
+ return migrations
17
+ .filter((meta) => satisfies(meta.version, semverRange))
18
+ .sort((a, b) => {
19
+ return gte(a.version, b.version) ? 1 : -1;
20
+ });
21
+ }
22
+
23
+ type RunMigrationsOptions = {
24
+ commitEachMigration?: boolean;
25
+ codemodOptions?: Record<string, any>;
26
+ };
27
+
28
+ export async function runMigrations(migrations: Migration[], options: RunMigrationsOptions = {}) {
29
+ const migrationList = migrations.map((meta) => `${meta.name} (${meta.description})`);
30
+
31
+ const migrationListBody = migrationList.length > 0 ? output.bulletList(migrationList) : ['No migrations to run.'];
32
+
33
+ output.log({ title: 'Running the following migrations:', body: migrationListBody });
34
+
35
+ // run migrations sequentially in version order where lowest version runs first
36
+ for (const migration of migrations) {
37
+ const context = await runCodemod(migration, options.codemodOptions);
38
+ const shouldCommit = options.commitEachMigration && context.hasChanges();
39
+
40
+ if (shouldCommit) {
41
+ await gitCommitNoVerify(`chore: run create-plugin migration - ${migration.name} (${migration.scriptPath})`);
42
+ }
43
+ }
44
+
45
+ setRootConfig({ version: CURRENT_APP_VERSION });
46
+
47
+ if (options.commitEachMigration) {
48
+ await gitCommitNoVerify(`chore: update .config/.cprc.json to version ${CURRENT_APP_VERSION}.`);
49
+ }
50
+ }
@@ -0,0 +1,17 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import defaultMigrations from './migrations.js';
4
+
5
+ describe('migrations json', () => {
6
+ // As migration scripts are imported dynamically when update is run we assert the path is valid
7
+ // Vitest 4 reimplemented its workers, which caused the previous dynamic import tests to fail.
8
+ // This test now only asserts that the migration script source file exists.
9
+ defaultMigrations.forEach((migration) => {
10
+ it(`should have a valid migration script path for ${migration.name}`, () => {
11
+ // import.meta.resolve() returns a file:// URL, convert to path
12
+ const filePath = fileURLToPath(migration.scriptPath);
13
+ const sourceFilePath = filePath.replace('.js', '.ts');
14
+ expect(existsSync(sourceFilePath)).toBe(true);
15
+ });
16
+ });
17
+ });
@@ -0,0 +1,55 @@
1
+ import { LEGACY_UPDATE_CUTOFF_VERSION } from '../../constants.js';
2
+ import { Codemod } from '../types.js';
3
+
4
+ export interface Migration extends Codemod {
5
+ version: string;
6
+ }
7
+
8
+ export default [
9
+ {
10
+ name: '001-update-grafana-compose-extend',
11
+ version: LEGACY_UPDATE_CUTOFF_VERSION,
12
+ description: 'Update ./docker-compose.yaml to extend from ./.config/docker-compose-base.yaml.',
13
+ scriptPath: import.meta.resolve('./scripts/001-update-grafana-compose-extend.js'),
14
+ },
15
+ {
16
+ name: '002-update-is-compatible-workflow',
17
+ version: LEGACY_UPDATE_CUTOFF_VERSION,
18
+ description:
19
+ 'Update ./.github/workflows/is-compatible.yml to use is-compatible github action instead of calling levitate directly',
20
+ scriptPath: import.meta.resolve('./scripts/002-update-is-compatible-workflow.js'),
21
+ },
22
+ {
23
+ name: '003-update-eslint-deprecation-rule',
24
+ version: LEGACY_UPDATE_CUTOFF_VERSION,
25
+ description: 'Replace deprecated eslint-plugin-deprecation with @typescript-eslint/no-deprecated rule.',
26
+ scriptPath: import.meta.resolve('./scripts/003-update-eslint-deprecation-rule.js'),
27
+ },
28
+ {
29
+ name: '004-eslint9-flat-config',
30
+ version: LEGACY_UPDATE_CUTOFF_VERSION,
31
+ description: 'Migrate eslint config to flat config format and update devDependencies to latest versions.',
32
+ scriptPath: import.meta.resolve('./scripts/004-eslint9-flat-config.js'),
33
+ },
34
+ {
35
+ name: '005-react-18-3',
36
+ version: '6.1.9',
37
+ description: 'Update React and ReactDOM 18.x versions to ^18.3.0 to surface React 19 compatibility issues.',
38
+ scriptPath: import.meta.resolve('./scripts/005-react-18-3.js'),
39
+ },
40
+ {
41
+ name: '006-webpack-nested-fix',
42
+ version: '6.1.11',
43
+ description: 'Fix webpack variable replacement in nested plugins files.',
44
+ scriptPath: import.meta.resolve('./scripts/006-webpack-nested-fix.js'),
45
+ },
46
+ {
47
+ name: '007-remove-testing-library-types',
48
+ version: '6.1.13',
49
+ description:
50
+ 'Add setupTests.d.ts for @testing-library/jest-dom types and remove @types/testing-library__jest-dom npm package.',
51
+ scriptPath: import.meta.resolve('./scripts/007-remove-testing-library-types.js'),
52
+ },
53
+ // Do not use LEGACY_UPDATE_CUTOFF_VERSION for new migrations. It is only used above to force migrations to run
54
+ // for those written before the switch to updates as migrations.
55
+ ] satisfies Migration[];
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { Context } from '../context.js';
2
+ import { Context } from '../../context.js';
3
3
  import migrate from './001-update-grafana-compose-extend.js';
4
4
  import { parse, stringify } from 'yaml';
5
5
 
@@ -1,5 +1,5 @@
1
1
  import { resolve } from 'node:path';
2
- import { type Context } from '../context.js';
2
+ import { type Context } from '../../context.js';
3
3
  import { Node, Pair, parseDocument, Scalar, stringify, visit, YAMLMap, Document, YAMLSeq, visitorFn } from 'yaml';
4
4
 
5
5
  export default async function migrate(context: Context) {
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { Context } from '../context.js';
2
+ import { Context } from '../../context.js';
3
3
  import migrate from './002-update-is-compatible-workflow.js';
4
4
  import { parse } from 'yaml';
5
5