@grafana/create-plugin 6.2.0-canary.2233.19133609453.0 → 6.2.0-canary.2233.19424871609.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 +6 -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 +33 -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 +7 -4
  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 +7 -21
  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 +10 -5
  26. package/src/codemods/migrations/migrations.ts +37 -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 +13 -35
  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
@@ -1,11 +1,7 @@
1
- import defaultMigrations, { MigrationMeta } from './migrations.js';
2
- import { flushChanges, formatFiles, installNPMDependencies } from '../utils.js';
1
+ import defaultMigrations, { Migration } from './migrations.js';
2
+ import { runCodemod } from '../runner.js';
3
3
  import { gte, satisfies } from 'semver';
4
- import { migrationsDebug, printChanges } from './utils.js';
5
-
6
4
  import { CURRENT_APP_VERSION } from '../../utils/utils.version.js';
7
- import { Context } from '../context.js';
8
- import type { MigrationModule } from '../types.js';
9
5
  import { gitCommitNoVerify } from '../../utils/utils.git.js';
10
6
  import { output } from '../../utils/utils.console.js';
11
7
  import { setRootConfig } from '../../utils/utils.config.js';
@@ -13,59 +9,36 @@ import { setRootConfig } from '../../utils/utils.config.js';
13
9
  export function getMigrationsToRun(
14
10
  fromVersion: string,
15
11
  toVersion: string,
16
- migrations: Record<string, MigrationMeta> = defaultMigrations.migrations
17
- ): Record<string, MigrationMeta> {
12
+ migrations: Migration[] = defaultMigrations
13
+ ): Migration[] {
18
14
  const semverRange = `${fromVersion} - ${toVersion}`;
19
15
 
20
- const migrationsToRun = Object.entries(migrations)
16
+ return migrations
17
+ .filter((meta) => satisfies(meta.version, semverRange))
21
18
  .sort((a, b) => {
22
- return gte(a[1].version, b[1].version) ? 1 : -1;
23
- })
24
- .reduce<Record<string, MigrationMeta>>((acc, [key, meta]) => {
25
- if (satisfies(meta.version, semverRange)) {
26
- acc[key] = meta;
27
- }
28
- return acc;
29
- }, {});
30
-
31
- return migrationsToRun;
19
+ return gte(a.version, b.version) ? 1 : -1;
20
+ });
32
21
  }
33
22
 
34
23
  type RunMigrationsOptions = {
35
24
  commitEachMigration?: boolean;
25
+ codemodOptions?: Record<string, any>;
36
26
  };
37
27
 
38
- export async function runMigrations(migrations: Record<string, MigrationMeta>, options: RunMigrationsOptions = {}) {
39
- const basePath = process.cwd();
40
- const migrationList = Object.entries(migrations).map(
41
- ([key, migrationMeta]) => `${key} (${migrationMeta.description})`
42
- );
28
+ export async function runMigrations(migrations: Migration[], options: RunMigrationsOptions = {}) {
29
+ const migrationList = migrations.map((meta) => `${meta.name} (${meta.description})`);
43
30
 
44
31
  const migrationListBody = migrationList.length > 0 ? output.bulletList(migrationList) : ['No migrations to run.'];
45
32
 
46
33
  output.log({ title: 'Running the following migrations:', body: migrationListBody });
47
34
 
48
- for (const [key, migration] of Object.entries(migrations)) {
49
- try {
50
- const context = await runMigration(migration, new Context(basePath));
51
- const shouldCommit = options.commitEachMigration && context.hasChanges();
52
-
53
- migrationsDebug(`context for "${key} (${migration.migrationScript})":`);
54
- migrationsDebug('%O', context.listChanges());
55
-
56
- await formatFiles(context);
57
- flushChanges(context);
58
- printChanges(context, key, migration);
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();
59
39
 
60
- installNPMDependencies(context);
61
-
62
- if (shouldCommit) {
63
- await gitCommitNoVerify(`chore: run create-plugin migration - ${key} (${migration.migrationScript})`);
64
- }
65
- } catch (error) {
66
- if (error instanceof Error) {
67
- throw new Error(`Error running migration "${key} (${migration.migrationScript})": ${error.message}`);
68
- }
40
+ if (shouldCommit) {
41
+ await gitCommitNoVerify(`chore: run create-plugin migration - ${migration.name} (${migration.scriptPath})`);
69
42
  }
70
43
  }
71
44
 
@@ -75,9 +48,3 @@ export async function runMigrations(migrations: Record<string, MigrationMeta>, o
75
48
  await gitCommitNoVerify(`chore: update .config/.cprc.json to version ${CURRENT_APP_VERSION}.`);
76
49
  }
77
50
  }
78
-
79
- export async function runMigration(migration: MigrationMeta, context: Context): Promise<Context> {
80
- const module = (await import(migration.migrationScript)) as MigrationModule;
81
-
82
- return module.default(context);
83
- }
@@ -1,12 +1,17 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
1
3
  import defaultMigrations from './migrations.js';
2
4
 
3
5
  describe('migrations json', () => {
4
6
  // As migration scripts are imported dynamically when update is run we assert the path is valid
5
- Object.entries(defaultMigrations.migrations).forEach(([key, migration]) => {
6
- it(`should have a valid migration script path for ${key}`, () => {
7
- expect(async () => {
8
- await import(migration.migrationScript);
9
- }).not.toThrow();
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);
10
15
  });
11
16
  });
12
17
  });
@@ -1,39 +1,42 @@
1
1
  import { LEGACY_UPDATE_CUTOFF_VERSION } from '../../constants.js';
2
+ import { Codemod } from '../types.js';
2
3
 
3
- export type MigrationMeta = {
4
+ export interface Migration extends Codemod {
4
5
  version: string;
5
- description: string;
6
- migrationScript: string;
7
- };
6
+ }
8
7
 
9
- type Migrations = {
10
- migrations: Record<string, MigrationMeta>;
11
- };
12
-
13
- export default {
14
- migrations: {
15
- '001-update-grafana-compose-extend': {
16
- version: LEGACY_UPDATE_CUTOFF_VERSION,
17
- description: 'Update ./docker-compose.yaml to extend from ./.config/docker-compose-base.yaml.',
18
- migrationScript: './scripts/001-update-grafana-compose-extend.js',
19
- },
20
- '002-update-is-compatible-workflow': {
21
- version: LEGACY_UPDATE_CUTOFF_VERSION,
22
- description:
23
- 'Update ./.github/workflows/is-compatible.yml to use is-compatible github action instead of calling levitate directly',
24
- migrationScript: './scripts/002-update-is-compatible-workflow.js',
25
- },
26
- '003-update-eslint-deprecation-rule': {
27
- version: LEGACY_UPDATE_CUTOFF_VERSION,
28
- description: 'Replace deprecated eslint-plugin-deprecation with @typescript-eslint/no-deprecated rule.',
29
- migrationScript: './scripts/003-update-eslint-deprecation-rule.js',
30
- },
31
- '004-eslint9-flat-config': {
32
- version: LEGACY_UPDATE_CUTOFF_VERSION,
33
- description: 'Migrate eslint config to flat config format and update devDependencies to latest versions.',
34
- migrationScript: './scripts/004-eslint9-flat-config.js',
35
- },
36
- // Do not use LEGACY_UPDATE_CUTOFF_VERSION for new migrations. It is only used above to force migrations to run
37
- // for those written before the switch to updates as migrations.
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'),
38
39
  },
39
- } as Migrations;
40
+ // Do not use LEGACY_UPDATE_CUTOFF_VERSION for new migrations. It is only used above to force migrations to run
41
+ // for those written before the switch to updates as migrations.
42
+ ] satisfies Migration[];
@@ -7,7 +7,7 @@ import { dirname, relative, resolve } from 'node:path';
7
7
  import * as recast from 'recast';
8
8
  import type { Context } from '../../context.js';
9
9
  import { addDependenciesToPackageJson } from '../../utils.js';
10
- import { migrationsDebug } from '../utils.js';
10
+ // migrationsDebug removed - was from deleted migrations/utils.js
11
11
 
12
12
  type Imports = Map<string, { name?: string; bindings?: string[] }>;
13
13
 
@@ -407,7 +407,7 @@ function getIgnorePaths(context: Context): string[] {
407
407
  }
408
408
  }
409
409
  } catch (error) {
410
- migrationsDebug('Error parsing package.json: %s', error);
410
+ console.log('Error parsing package.json: %s', error);
411
411
  }
412
412
  }
413
413
 
@@ -0,0 +1,145 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Context } from '../../context.js';
3
+ import migrate from './005-react-18-3.js';
4
+
5
+ describe('005-react-18-3', () => {
6
+ it('should not modify anything if package.json does not exist', async () => {
7
+ const context = new Context('/virtual');
8
+ await migrate(context);
9
+ expect(context.listChanges()).toEqual({});
10
+ });
11
+
12
+ it('should not modify anything if there are no React dependencies', async () => {
13
+ const context = new Context('/virtual');
14
+ context.addFile(
15
+ './package.json',
16
+ JSON.stringify({
17
+ dependencies: {
18
+ lodash: '^4.17.21',
19
+ },
20
+ })
21
+ );
22
+ const initialChanges = context.listChanges();
23
+ await migrate(context);
24
+ expect(context.listChanges()).toEqual(initialChanges);
25
+ });
26
+
27
+ it('should not update React if version is below 18.0.0', async () => {
28
+ const context = new Context('/virtual');
29
+ const packageJson = {
30
+ dependencies: {
31
+ react: '^17.0.2',
32
+ 'react-dom': '^17.0.2',
33
+ },
34
+ devDependencies: {
35
+ '@types/react': '^17.0.0',
36
+ '@types/react-dom': '^17.0.0',
37
+ },
38
+ };
39
+ context.addFile('./package.json', JSON.stringify(packageJson, null, 2));
40
+ const initialPackageJson = context.getFile('./package.json');
41
+
42
+ await migrate(context);
43
+
44
+ expect(context.getFile('./package.json')).toBe(initialPackageJson);
45
+ });
46
+
47
+ it('should update React 18.0.0 to ^18.3.0', async () => {
48
+ const context = new Context('/virtual');
49
+ context.addFile(
50
+ './package.json',
51
+ JSON.stringify({
52
+ dependencies: {
53
+ react: '^18.0.0',
54
+ },
55
+ })
56
+ );
57
+
58
+ await migrate(context);
59
+
60
+ const updatedPackageJson = JSON.parse(context.getFile('./package.json') || '{}');
61
+ expect(updatedPackageJson.dependencies.react).toBe('^18.3.0');
62
+ expect(updatedPackageJson.devDependencies?.['@types/react']).toBe('^18.3.0');
63
+ });
64
+
65
+ it('should not update React if version is already 18.3.0 or higher', async () => {
66
+ const context = new Context('/virtual');
67
+ const packageJson = {
68
+ dependencies: {
69
+ react: '^18.3.0',
70
+ 'react-dom': '^18.3.0',
71
+ },
72
+ devDependencies: {
73
+ '@types/react': '^18.3.0',
74
+ '@types/react-dom': '^18.3.0',
75
+ },
76
+ };
77
+ context.addFile('./package.json', JSON.stringify(packageJson, null, 2));
78
+
79
+ await migrate(context);
80
+
81
+ const updatedPackageJson = JSON.parse(context.getFile('./package.json') || '{}');
82
+ expect(updatedPackageJson.dependencies.react).toBe('^18.3.0');
83
+ expect(updatedPackageJson.dependencies['react-dom']).toBe('^18.3.0');
84
+ });
85
+
86
+ it('should not downgrade React 19.0.0 to 18.3.0', async () => {
87
+ const context = new Context('/virtual');
88
+ const packageJson = {
89
+ dependencies: {
90
+ react: '^19.0.0',
91
+ 'react-dom': '^19.0.0',
92
+ },
93
+ devDependencies: {
94
+ '@types/react': '^19.0.0',
95
+ '@types/react-dom': '^19.0.0',
96
+ },
97
+ };
98
+ context.addFile('./package.json', JSON.stringify(packageJson, null, 2));
99
+
100
+ await migrate(context);
101
+
102
+ const updatedPackageJson = JSON.parse(context.getFile('./package.json') || '{}');
103
+ expect(updatedPackageJson.dependencies.react).toBe('^19.0.0');
104
+ expect(updatedPackageJson.dependencies['react-dom']).toBe('^19.0.0');
105
+ expect(updatedPackageJson.devDependencies['@types/react']).toBe('^19.0.0');
106
+ expect(updatedPackageJson.devDependencies['@types/react-dom']).toBe('^19.0.0');
107
+ });
108
+
109
+ it('should handle version ranges correctly', async () => {
110
+ const context = new Context('/virtual');
111
+ context.addFile(
112
+ './package.json',
113
+ JSON.stringify({
114
+ dependencies: {
115
+ react: '~18.1.0',
116
+ 'react-dom': '18.2.0',
117
+ },
118
+ })
119
+ );
120
+
121
+ await migrate(context);
122
+
123
+ const updatedPackageJson = JSON.parse(context.getFile('./package.json') || '{}');
124
+ expect(updatedPackageJson.dependencies.react).toBe('^18.3.0');
125
+ expect(updatedPackageJson.dependencies['react-dom']).toBe('^18.3.0');
126
+ });
127
+
128
+ it('should be idempotent', async () => {
129
+ const context = new Context('/virtual');
130
+ context.addFile(
131
+ './package.json',
132
+ JSON.stringify({
133
+ dependencies: {
134
+ react: '^18.2.0',
135
+ 'react-dom': '^18.2.0',
136
+ },
137
+ devDependencies: {
138
+ '@types/react': '^18.2.0',
139
+ '@types/react-dom': '^18.2.0',
140
+ },
141
+ })
142
+ );
143
+ await expect(migrate).toBeIdempotent(context);
144
+ });
145
+ });
@@ -0,0 +1,19 @@
1
+ import type { Context } from '../../context.js';
2
+ import { addDependenciesToPackageJson, isVersionGreater } from '../../utils.js';
3
+
4
+ export default function migrate(context: Context) {
5
+ if (context.doesFileExist('package.json')) {
6
+ const packageJson = JSON.parse(context.getFile('package.json') || '{}');
7
+ if (packageJson.dependencies?.react) {
8
+ if (isVersionGreater(packageJson.dependencies.react, '18.0.0', true)) {
9
+ addDependenciesToPackageJson(context, { react: '^18.3.0' }, { '@types/react': '^18.3.0' });
10
+ }
11
+ }
12
+ if (packageJson.dependencies?.['react-dom']) {
13
+ if (isVersionGreater(packageJson.dependencies['react-dom'], '18.0.0', true)) {
14
+ addDependenciesToPackageJson(context, { 'react-dom': '^18.3.0' }, { '@types/react-dom': '^18.3.0' });
15
+ }
16
+ }
17
+ }
18
+ return context;
19
+ }
@@ -14,7 +14,7 @@ describe('Migration - append profile to webpack', () => {
14
14
  })
15
15
  );
16
16
 
17
- const updatedContext = await migrate(context);
17
+ const updatedContext = await migrate(context, { profile: true, skipBackup: false, verbose: false });
18
18
 
19
19
  expect(updatedContext.getFile('./package.json')).toMatch(
20
20
  'webpack -c ./.config/webpack/webpack.config.ts --profile --env production'
@@ -1,6 +1,23 @@
1
1
  import type { Context } from '../../context.js';
2
2
 
3
- export default function migrate(context: Context): Context {
3
+ /**
4
+ * Example migration that demonstrates basic context operations
5
+ * This example shows how to modify package.json, add/delete files, and rename files
6
+ */
7
+
8
+ type MigrateOptions = {
9
+ profile?: boolean;
10
+ skipBackup?: boolean;
11
+ verbose?: boolean;
12
+ };
13
+
14
+ export default function migrate(context: Context, options: MigrateOptions = {}): Context {
15
+ const { profile = false, skipBackup = false, verbose = false } = options;
16
+
17
+ if (verbose) {
18
+ console.log('Running migration with options:', options);
19
+ }
20
+
4
21
  const rawPkgJson = context.getFile('./package.json') ?? '{}';
5
22
  const packageJson = JSON.parse(rawPkgJson);
6
23
 
@@ -9,14 +26,14 @@ export default function migrate(context: Context): Context {
9
26
 
10
27
  const pattern = /(webpack.+-c\s.+\.ts)\s(.+)/;
11
28
 
12
- if (pattern.test(buildScript) && !buildScript.includes('--profile')) {
29
+ if (profile && pattern.test(buildScript) && !buildScript.includes('--profile')) {
13
30
  packageJson.scripts.build = buildScript.replace(pattern, `$1 --profile $2`);
14
31
  }
15
32
 
16
33
  context.updateFile('./package.json', JSON.stringify(packageJson, null, 2));
17
34
  }
18
35
 
19
- if (context.doesFileExist('./src/README.md')) {
36
+ if (!skipBackup && context.doesFileExist('./src/README.md')) {
20
37
  context.deleteFile('./src/README.md');
21
38
  }
22
39
 
@@ -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);