@grafana/create-plugin 6.2.0-canary.2233.19424871609.0 → 6.2.0-canary.2233.19500011348.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 (37) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/codemods/additions/additions.js +1 -1
  3. package/dist/codemods/additions/scripts/example-addition.js +1 -1
  4. package/dist/codemods/migrations/migrations.js +12 -0
  5. package/dist/codemods/migrations/scripts/004-eslint9-flat-config.js +2 -2
  6. package/dist/codemods/migrations/scripts/006-webpack-nested-fix.js +80 -0
  7. package/dist/codemods/migrations/scripts/007-remove-testing-library-types.js +25 -0
  8. package/dist/codemods/runner.js +0 -5
  9. package/dist/codemods/utils.js +4 -1
  10. package/dist/commands/add.command.js +0 -4
  11. package/package.json +3 -2
  12. package/src/codemods/additions/additions.test.ts +9 -4
  13. package/src/codemods/additions/additions.ts +1 -1
  14. package/src/codemods/additions/scripts/example-addition.test.ts +1 -1
  15. package/src/codemods/additions/scripts/example-addition.ts +1 -1
  16. package/src/codemods/migrations/manager.test.ts +1 -1
  17. package/src/codemods/migrations/migrations.ts +13 -0
  18. package/src/codemods/migrations/scripts/004-eslint9-flat-config.ts +2 -3
  19. package/src/codemods/migrations/scripts/006-webpack-nested-fix.test.ts +169 -0
  20. package/src/codemods/migrations/scripts/006-webpack-nested-fix.ts +117 -0
  21. package/src/codemods/migrations/scripts/007-remove-testing-library-types.test.ts +137 -0
  22. package/src/codemods/migrations/scripts/007-remove-testing-library-types.ts +25 -0
  23. package/src/codemods/runner.ts +0 -6
  24. package/src/codemods/utils.ts +4 -0
  25. package/src/commands/add.command.ts +0 -5
  26. package/src/utils/utils.config.ts +2 -2
  27. package/templates/backend/go.mod +67 -47
  28. package/templates/backend/go.sum +197 -222
  29. package/templates/backend-app/go.mod +67 -48
  30. package/templates/backend-app/go.sum +197 -222
  31. package/templates/common/.config/types/setupTests.d.ts +1 -0
  32. package/templates/common/.config/webpack/webpack.config.ts +1 -1
  33. package/templates/common/_package.json +0 -1
  34. package/dist/codemods/migrations/scripts/example-migration.js +0 -29
  35. package/src/codemods/migrations/scripts/example-migration.test.ts +0 -40
  36. package/src/codemods/migrations/scripts/example-migration.ts +0 -51
  37. package/src/migrations/migrations.ts +0 -44
package/CHANGELOG.md CHANGED
@@ -1,3 +1,27 @@
1
+ # v6.1.13 (Wed Nov 19 2025)
2
+
3
+ #### 🐛 Bug Fix
4
+
5
+ - Create Plugin: remove types/testing-library__jest-dom [#2250](https://github.com/grafana/plugin-tools/pull/2250) ([@jackw](https://github.com/jackw))
6
+
7
+ #### Authors: 1
8
+
9
+ - Jack Westbrook ([@jackw](https://github.com/jackw))
10
+
11
+ ---
12
+
13
+ # v6.1.12 (Mon Nov 17 2025)
14
+
15
+ #### 🐛 Bug Fix
16
+
17
+ - Create Plugin: fix replacing version and plugin id in nested plugins [#2287](https://github.com/grafana/plugin-tools/pull/2287) ([@jackw](https://github.com/jackw))
18
+
19
+ #### Authors: 1
20
+
21
+ - Jack Westbrook ([@jackw](https://github.com/jackw))
22
+
23
+ ---
24
+
1
25
  # v6.1.11 (Fri Nov 14 2025)
2
26
 
3
27
  #### 🐛 Bug Fix
@@ -1,7 +1,7 @@
1
1
  var defaultAdditions = [
2
2
  {
3
3
  name: "example-addition",
4
- description: "Example addition demonstrating Valibot schema with type inference",
4
+ description: "Adds an example addition to the plugin",
5
5
  scriptPath: import.meta.resolve("./scripts/example-addition.js")
6
6
  }
7
7
  ];
@@ -21,7 +21,7 @@ function exampleAddition(context, options) {
21
21
  packageJson.scripts["example-script"] = `echo "Running ${featureName}"`;
22
22
  context.updateFile("./package.json", JSON.stringify(packageJson, null, 2));
23
23
  }
24
- addDependenciesToPackageJson(context, {}, { "example-dev-dep": "^1.0.0" });
24
+ addDependenciesToPackageJson(context, {}, { "@types/node": "^20.0.0" });
25
25
  if (!context.doesFileExist(`./src/features/${featureName}.ts`)) {
26
26
  const featureCode = `export const ${featureName} = {
27
27
  name: '${featureName}',
@@ -30,6 +30,18 @@ var defaultMigrations = [
30
30
  version: "6.1.9",
31
31
  description: "Update React and ReactDOM 18.x versions to ^18.3.0 to surface React 19 compatibility issues.",
32
32
  scriptPath: import.meta.resolve("./scripts/005-react-18-3.js")
33
+ },
34
+ {
35
+ name: "006-webpack-nested-fix",
36
+ version: "6.1.11",
37
+ description: "Fix webpack variable replacement in nested plugins files.",
38
+ scriptPath: import.meta.resolve("./scripts/006-webpack-nested-fix.js")
39
+ },
40
+ {
41
+ name: "007-remove-testing-library-types",
42
+ version: "6.1.13",
43
+ description: "Add setupTests.d.ts for @testing-library/jest-dom types and remove @types/testing-library__jest-dom npm package.",
44
+ scriptPath: import.meta.resolve("./scripts/007-remove-testing-library-types.js")
33
45
  }
34
46
  // Do not use LEGACY_UPDATE_CUTOFF_VERSION for new migrations. It is only used above to force migrations to run
35
47
  // for those written before the switch to updates as migrations.
@@ -4,7 +4,7 @@ import { parse } from 'jsonc-parser';
4
4
  import minimist from 'minimist';
5
5
  import { resolve, dirname, relative } from 'node:path';
6
6
  import * as recast from 'recast';
7
- import { addDependenciesToPackageJson } from '../../utils.js';
7
+ import { addDependenciesToPackageJson, migrationsDebug } from '../../utils.js';
8
8
 
9
9
  const { builders } = recast.types;
10
10
  const legacyKeysToCopy = ["rules", "settings"];
@@ -317,7 +317,7 @@ function getIgnorePaths(context) {
317
317
  }
318
318
  }
319
319
  } catch (error) {
320
- console.log("Error parsing package.json: %s", error);
320
+ migrationsDebug("Error parsing package.json: %s", error);
321
321
  }
322
322
  }
323
323
  return Array.from(result);
@@ -0,0 +1,80 @@
1
+ import { join } from 'node:path';
2
+ import * as recast from 'recast';
3
+ import * as typeScriptParser from 'recast/parsers/typescript.js';
4
+
5
+ const { builders } = recast.types;
6
+ function migrate(context) {
7
+ const webpackConfigPath = join(".config", "webpack", "webpack.config.ts");
8
+ if (!context.doesFileExist(webpackConfigPath)) {
9
+ return context;
10
+ }
11
+ const webpackConfigContent = context.getFile(webpackConfigPath);
12
+ if (!webpackConfigContent) {
13
+ return context;
14
+ }
15
+ let hasChanges = false;
16
+ const ast = recast.parse(webpackConfigContent, {
17
+ parser: typeScriptParser
18
+ });
19
+ recast.visit(ast, {
20
+ visitNewExpression(path) {
21
+ const { node } = path;
22
+ if (node.callee.type === "Identifier" && node.callee.name === "ReplaceInFileWebpackPlugin" && node.arguments.length > 0) {
23
+ const firstArg = node.arguments[0];
24
+ if (firstArg.type === "ArrayExpression" && firstArg.elements) {
25
+ firstArg.elements.forEach((element) => {
26
+ if (element && element.type === "ObjectExpression") {
27
+ const changed = transformFilesProperty(element);
28
+ if (changed) {
29
+ hasChanges = true;
30
+ }
31
+ }
32
+ });
33
+ }
34
+ }
35
+ return this.traverse(path);
36
+ }
37
+ });
38
+ if (hasChanges) {
39
+ const output = recast.print(ast, {
40
+ tabWidth: 2,
41
+ trailingComma: true,
42
+ lineTerminator: "\n"
43
+ });
44
+ context.updateFile(webpackConfigPath, output.code);
45
+ }
46
+ return context;
47
+ }
48
+ function transformFilesProperty(objectExpression) {
49
+ const properties = objectExpression.properties;
50
+ if (!properties) {
51
+ return false;
52
+ }
53
+ const filesPropertyIndex = properties.findIndex(
54
+ (prop) => (prop.type === "Property" || prop.type === "ObjectProperty") && prop.key.type === "Identifier" && prop.key.name === "files"
55
+ );
56
+ if (filesPropertyIndex === -1) {
57
+ return false;
58
+ }
59
+ const filesProperty = properties[filesPropertyIndex];
60
+ if (filesProperty.type !== "Property" && filesProperty.type !== "ObjectProperty" || !("value" in filesProperty)) {
61
+ return false;
62
+ }
63
+ if (filesProperty.value.type === "ArrayExpression" && filesProperty.value.elements.length === 2) {
64
+ const elements = filesProperty.value.elements;
65
+ const values = elements.map((el) => el?.type === "Literal" || el?.type === "StringLiteral" ? el?.value : null).filter((v) => v !== null);
66
+ if (values.length === 2 && values.includes("plugin.json") && values.includes("README.md")) {
67
+ properties.splice(filesPropertyIndex, 1);
68
+ const testProperty = builders.property(
69
+ "init",
70
+ builders.identifier("test"),
71
+ builders.arrayExpression([builders.literal(/(^|\/)plugin\.json$/), builders.literal(/(^|\/)README\.md$/)])
72
+ );
73
+ properties.splice(filesPropertyIndex, 0, testProperty);
74
+ return true;
75
+ }
76
+ }
77
+ return false;
78
+ }
79
+
80
+ export { migrate as default };
@@ -0,0 +1,25 @@
1
+ import { isVersionGreater, removeDependenciesFromPackageJson } from '../../utils.js';
2
+
3
+ function migrate(context) {
4
+ if (context.doesFileExist("package.json")) {
5
+ const packageJson = JSON.parse(context.getFile("package.json") || "{}");
6
+ if (isVersionGreater(packageJson.devDependencies["@testing-library/jest-dom"], "6.0.0", true)) {
7
+ if (context.doesFileExist("./.config/types/setupTests.d.ts")) {
8
+ const setupTestsContent = context.getFile("./.config/types/setupTests.d.ts");
9
+ if (!setupTestsContent?.includes("@testing-library/jest-dom")) {
10
+ context.updateFile(
11
+ "./.config/types/setupTests.d.ts",
12
+ `import '@testing-library/jest-dom';
13
+ ${setupTestsContent}`
14
+ );
15
+ }
16
+ } else {
17
+ context.addFile("./.config/types/setupTests.d.ts", "import '@testing-library/jest-dom';\n");
18
+ }
19
+ removeDependenciesFromPackageJson(context, [], ["@types/testing-library__jest-dom"]);
20
+ }
21
+ }
22
+ return context;
23
+ }
24
+
25
+ export { migrate as default };
@@ -1,7 +1,6 @@
1
1
  import { Context } from './context.js';
2
2
  import { formatFiles, flushChanges, printChanges, installNPMDependencies } from './utils.js';
3
3
  import { parseAndValidateOptions } from './schema-parser.js';
4
- import { output } from '../utils/utils.console.js';
5
4
 
6
5
  async function runCodemod(codemod, options) {
7
6
  const codemodModule = await import(codemod.scriptPath);
@@ -15,10 +14,6 @@ async function runCodemod(codemod, options) {
15
14
  const basePath = process.cwd();
16
15
  const context = new Context(basePath);
17
16
  try {
18
- output.log({
19
- title: `Running ${codemod.name}`,
20
- body: [codemod.description]
21
- });
22
17
  const updatedContext = await codemodModule.default(context, codemodOptions);
23
18
  await formatFiles(updatedContext);
24
19
  flushChanges(updatedContext);
@@ -6,6 +6,7 @@ import { output } from '../utils/utils.console.js';
6
6
  import { getPackageManagerWithFallback, getPackageManagerSilentInstallCmd } from '../utils/utils.packageManager.js';
7
7
  import { execSync } from 'node:child_process';
8
8
  import { gte, gt, clean, coerce } from 'semver';
9
+ import { debug } from '../utils/utils.cli.js';
9
10
 
10
11
  function printChanges(context, key, description) {
11
12
  const changes = context.listChanges();
@@ -203,5 +204,7 @@ function cleanSemver(version) {
203
204
  function sortObjectByKeys(obj) {
204
205
  return Object.keys(obj).sort().reduce((acc, key) => ({ ...acc, [key]: obj[key] }), {});
205
206
  }
207
+ const migrationsDebug = debug.extend("migrations");
208
+ debug.extend("additions");
206
209
 
207
- export { addDependenciesToPackageJson, flushChanges, formatFiles, installNPMDependencies, isVersionGreater, printChanges, readJsonFile, removeDependenciesFromPackageJson };
210
+ export { addDependenciesToPackageJson, flushChanges, formatFiles, installNPMDependencies, isVersionGreater, migrationsDebug, printChanges, readJsonFile, removeDependenciesFromPackageJson };
@@ -19,10 +19,6 @@ const add = async (argv) => {
19
19
 
20
20
  Available additions: ${additionsList.join(", ")}`);
21
21
  }
22
- output.log({
23
- title: `Running addition: ${addition.name}`,
24
- body: [addition.description]
25
- });
26
22
  const { _, $0, ...codemodOptions } = argv;
27
23
  await runCodemod(addition, codemodOptions);
28
24
  output.success({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grafana/create-plugin",
3
- "version": "6.2.0-canary.2233.19424871609.0",
3
+ "version": "6.2.0-canary.2233.19500011348.0",
4
4
  "repository": {
5
5
  "directory": "packages/create-plugin",
6
6
  "url": "https://github.com/grafana/plugin-tools"
@@ -25,6 +25,7 @@
25
25
  "typecheck": "tsc --noEmit"
26
26
  },
27
27
  "dependencies": {
28
+ "@babel/parser": "^7.28.5",
28
29
  "@ivanmaxlogiudice/gitignore": "^0.0.2",
29
30
  "chalk": "^5.3.0",
30
31
  "change-case": "^5.4.0",
@@ -60,5 +61,5 @@
60
61
  "engines": {
61
62
  "node": ">=20"
62
63
  },
63
- "gitHead": "0b0b05e102a885fb4fb16588c6cfbd0f0b69392f"
64
+ "gitHead": "da5c19b22694d6591958bc7f4404eac423253c3c"
64
65
  }
@@ -1,12 +1,17 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
1
3
  import defaultAdditions from './additions.js';
2
4
 
3
5
  describe('additions json', () => {
4
- // as addition scripts are imported dynamically when add is run we assert the path is valid
6
+ // As addition scripts are imported dynamically when add 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 addition script source file exists.
5
9
  defaultAdditions.forEach((addition) => {
6
10
  it(`should have a valid addition script path for ${addition.name}`, () => {
7
- expect(async () => {
8
- await import(addition.scriptPath);
9
- }).not.toThrow();
11
+ // import.meta.resolve() returns a file:// URL, convert to path
12
+ const filePath = fileURLToPath(addition.scriptPath);
13
+ const sourceFilePath = filePath.replace('.js', '.ts');
14
+ expect(existsSync(sourceFilePath)).toBe(true);
10
15
  });
11
16
  });
12
17
  });
@@ -3,7 +3,7 @@ import { Codemod } from '../types.js';
3
3
  export default [
4
4
  {
5
5
  name: 'example-addition',
6
- description: 'Example addition demonstrating Valibot schema with type inference',
6
+ description: 'Adds an example addition to the plugin',
7
7
  scriptPath: import.meta.resolve('./scripts/example-addition.js'),
8
8
  },
9
9
  ] satisfies Codemod[];
@@ -23,7 +23,7 @@ describe('example-addition', () => {
23
23
  const result = migrate(context, { featureName: 'myFeature', enabled: false, frameworks: ['react'] });
24
24
 
25
25
  const packageJson = JSON.parse(result.getFile('package.json') || '{}');
26
- expect(packageJson.devDependencies['example-dev-dep']).toBe('^1.0.0');
26
+ expect(packageJson.devDependencies['@types/node']).toBe('^20.0.0');
27
27
  });
28
28
 
29
29
  it('should create feature TypeScript file with options', () => {
@@ -34,7 +34,7 @@ export default function exampleAddition(context: Context, options: ExampleOption
34
34
  context.updateFile('./package.json', JSON.stringify(packageJson, null, 2));
35
35
  }
36
36
 
37
- addDependenciesToPackageJson(context, {}, { 'example-dev-dep': '^1.0.0' });
37
+ addDependenciesToPackageJson(context, {}, { '@types/node': '^20.0.0' });
38
38
 
39
39
  if (!context.doesFileExist(`./src/features/${featureName}.ts`)) {
40
40
  const featureCode = `export const ${featureName} = {
@@ -9,7 +9,7 @@ import { setRootConfig } from '../../utils/utils.config.js';
9
9
  import { vi } from 'vitest';
10
10
 
11
11
  vi.mock('../utils.js', async (importOriginal) => {
12
- const actual = (await importOriginal()) as any;
12
+ const actual: typeof import('../utils.js') = await importOriginal();
13
13
  return {
14
14
  ...actual,
15
15
  flushChanges: vi.fn(),
@@ -37,6 +37,19 @@ export default [
37
37
  description: 'Update React and ReactDOM 18.x versions to ^18.3.0 to surface React 19 compatibility issues.',
38
38
  scriptPath: import.meta.resolve('./scripts/005-react-18-3.js'),
39
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
+ },
40
53
  // Do not use LEGACY_UPDATE_CUTOFF_VERSION for new migrations. It is only used above to force migrations to run
41
54
  // for those written before the switch to updates as migrations.
42
55
  ] satisfies Migration[];
@@ -6,8 +6,7 @@ import minimist from 'minimist';
6
6
  import { dirname, relative, resolve } from 'node:path';
7
7
  import * as recast from 'recast';
8
8
  import type { Context } from '../../context.js';
9
- import { addDependenciesToPackageJson } from '../../utils.js';
10
- // migrationsDebug removed - was from deleted migrations/utils.js
9
+ import { addDependenciesToPackageJson, migrationsDebug } from '../../utils.js';
11
10
 
12
11
  type Imports = Map<string, { name?: string; bindings?: string[] }>;
13
12
 
@@ -407,7 +406,7 @@ function getIgnorePaths(context: Context): string[] {
407
406
  }
408
407
  }
409
408
  } catch (error) {
410
- console.log('Error parsing package.json: %s', error);
409
+ migrationsDebug('Error parsing package.json: %s', error);
411
410
  }
412
411
  }
413
412
 
@@ -0,0 +1,169 @@
1
+ import migrate from './006-webpack-nested-fix.js';
2
+ import { createDefaultContext } from '../../test-utils.js';
3
+
4
+ describe('Migration - webpack nested fix', () => {
5
+ test('should transform files property to test property', () => {
6
+ const context = createDefaultContext();
7
+
8
+ const webpackConfigContent = `
9
+ import ReplaceInFileWebpackPlugin from 'replace-in-file-webpack-plugin';
10
+
11
+ const config = {
12
+ plugins: [
13
+ new ReplaceInFileWebpackPlugin([
14
+ {
15
+ dir: 'dist',
16
+ files: ['plugin.json', 'README.md'],
17
+ rules: [
18
+ { search: /VERSION/g, replace: '1.0.0' }
19
+ ]
20
+ }
21
+ ])
22
+ ]
23
+ };
24
+ `;
25
+
26
+ context.addFile('.config/webpack/webpack.config.ts', webpackConfigContent);
27
+
28
+ const updatedContext = migrate(context);
29
+ const webpackConfig = updatedContext.getFile('.config/webpack/webpack.config.ts');
30
+
31
+ expect(webpackConfig).toContain('test: [/(^|\\/)plugin\\.json$/, /(^|\\/)README\\.md$/]');
32
+ expect(webpackConfig).not.toContain('files:');
33
+ expect(webpackConfig).toContain('dir:');
34
+ expect(webpackConfig).toContain('rules:');
35
+ });
36
+
37
+ test('should not transform if files array has different values', () => {
38
+ const context = createDefaultContext();
39
+
40
+ const webpackConfigContent = `
41
+ import ReplaceInFileWebpackPlugin from 'replace-in-file-webpack-plugin';
42
+
43
+ const config = {
44
+ plugins: [
45
+ new ReplaceInFileWebpackPlugin([
46
+ {
47
+ dir: 'dist',
48
+ files: ['custom.json', 'other.md'],
49
+ rules: []
50
+ }
51
+ ])
52
+ ]
53
+ };
54
+ `;
55
+
56
+ context.addFile('.config/webpack/webpack.config.ts', webpackConfigContent);
57
+
58
+ const updatedContext = migrate(context);
59
+ const webpackConfig = updatedContext.getFile('.config/webpack/webpack.config.ts');
60
+
61
+ expect(webpackConfig).toBe(webpackConfigContent);
62
+ });
63
+
64
+ test('should not transform if already using test property', () => {
65
+ const context = createDefaultContext();
66
+
67
+ const webpackConfigContent = `
68
+ import ReplaceInFileWebpackPlugin from 'replace-in-file-webpack-plugin';
69
+
70
+ const config = {
71
+ plugins: [
72
+ new ReplaceInFileWebpackPlugin([
73
+ {
74
+ dir: 'dist',
75
+ test: [/(^|\\/)plugin\\.json$/, /(^|\\/)README\\.md$/],
76
+ rules: []
77
+ }
78
+ ])
79
+ ]
80
+ };
81
+ `;
82
+
83
+ context.addFile('.config/webpack/webpack.config.ts', webpackConfigContent);
84
+
85
+ const updatedContext = migrate(context);
86
+ const webpackConfig = updatedContext.getFile('.config/webpack/webpack.config.ts');
87
+
88
+ expect(webpackConfig).toBe(webpackConfigContent);
89
+ });
90
+
91
+ test('should handle multiple ReplaceInFileWebpackPlugin instances', () => {
92
+ const context = createDefaultContext();
93
+
94
+ const webpackConfigContent = `
95
+ import ReplaceInFileWebpackPlugin from 'replace-in-file-webpack-plugin';
96
+
97
+ const config = {
98
+ plugins: [
99
+ new ReplaceInFileWebpackPlugin([
100
+ {
101
+ dir: 'dist',
102
+ files: ['plugin.json', 'README.md'],
103
+ rules: [],
104
+ }
105
+ ]),
106
+ new ReplaceInFileWebpackPlugin([
107
+ {
108
+ dir: 'other',
109
+ files: ['custom.json'],
110
+ rules: [],
111
+ }
112
+ ])
113
+ ]
114
+ };
115
+ `;
116
+
117
+ const expectedConfig = `
118
+ import ReplaceInFileWebpackPlugin from 'replace-in-file-webpack-plugin';
119
+
120
+ const config = {
121
+ plugins: [
122
+ new ReplaceInFileWebpackPlugin([
123
+ {
124
+ dir: 'dist',
125
+ test: [/(^|\\/)plugin\\.json$/, /(^|\\/)README\\.md$/],
126
+ rules: [],
127
+ }
128
+ ]),
129
+ new ReplaceInFileWebpackPlugin([
130
+ {
131
+ dir: 'other',
132
+ files: ['custom.json'],
133
+ rules: [],
134
+ }
135
+ ])
136
+ ]
137
+ };
138
+ `;
139
+
140
+ context.addFile('.config/webpack/webpack.config.ts', webpackConfigContent);
141
+ const updatedContext = migrate(context);
142
+ const webpackConfig = updatedContext.getFile('.config/webpack/webpack.config.ts');
143
+ expect(webpackConfig).toBe(expectedConfig);
144
+ });
145
+
146
+ test('should be idempotent', () => {
147
+ const context = createDefaultContext();
148
+
149
+ const webpackConfigContent = `
150
+ import ReplaceInFileWebpackPlugin from 'replace-in-file-webpack-plugin';
151
+
152
+ const config = {
153
+ plugins: [
154
+ new ReplaceInFileWebpackPlugin([
155
+ {
156
+ dir: 'dist',
157
+ files: ['plugin.json', 'README.md'],
158
+ rules: []
159
+ }
160
+ ])
161
+ ]
162
+ };
163
+ `;
164
+
165
+ context.addFile('.config/webpack/webpack.config.ts', webpackConfigContent);
166
+
167
+ expect(migrate).toBeIdempotent(context);
168
+ });
169
+ });
@@ -0,0 +1,117 @@
1
+ import { join } from 'node:path';
2
+ import * as recast from 'recast';
3
+ import * as typeScriptParser from 'recast/parsers/typescript.js';
4
+ import type { Context } from '../../context.js';
5
+
6
+ const { builders } = recast.types;
7
+
8
+ export default function migrate(context: Context): Context {
9
+ const webpackConfigPath = join('.config', 'webpack', 'webpack.config.ts');
10
+ if (!context.doesFileExist(webpackConfigPath)) {
11
+ return context;
12
+ }
13
+
14
+ const webpackConfigContent = context.getFile(webpackConfigPath);
15
+ if (!webpackConfigContent) {
16
+ return context;
17
+ }
18
+
19
+ let hasChanges = false;
20
+ const ast = recast.parse(webpackConfigContent, {
21
+ parser: typeScriptParser,
22
+ });
23
+
24
+ recast.visit(ast, {
25
+ visitNewExpression(path) {
26
+ const { node } = path;
27
+
28
+ // Check if this is a ReplaceInFileWebpackPlugin constructor
29
+ if (
30
+ node.callee.type === 'Identifier' &&
31
+ node.callee.name === 'ReplaceInFileWebpackPlugin' &&
32
+ node.arguments.length > 0
33
+ ) {
34
+ const firstArg = node.arguments[0];
35
+
36
+ // The first argument should be an array of config objects
37
+ if (firstArg.type === 'ArrayExpression' && firstArg.elements) {
38
+ firstArg.elements.forEach((element) => {
39
+ if (element && element.type === 'ObjectExpression') {
40
+ const changed = transformFilesProperty(element);
41
+ if (changed) {
42
+ hasChanges = true;
43
+ }
44
+ }
45
+ });
46
+ }
47
+ }
48
+
49
+ return this.traverse(path);
50
+ },
51
+ });
52
+
53
+ // Only update the file if we made changes
54
+ if (hasChanges) {
55
+ const output = recast.print(ast, {
56
+ tabWidth: 2,
57
+ trailingComma: true,
58
+ lineTerminator: '\n',
59
+ });
60
+ context.updateFile(webpackConfigPath, output.code);
61
+ }
62
+
63
+ return context;
64
+ }
65
+
66
+ function transformFilesProperty(objectExpression: recast.types.namedTypes.ObjectExpression): boolean {
67
+ const properties = objectExpression.properties;
68
+ if (!properties) {
69
+ return false;
70
+ }
71
+
72
+ // Find the 'files' property
73
+ const filesPropertyIndex = properties.findIndex(
74
+ (prop) =>
75
+ (prop.type === 'Property' || prop.type === 'ObjectProperty') &&
76
+ prop.key.type === 'Identifier' &&
77
+ prop.key.name === 'files'
78
+ );
79
+
80
+ if (filesPropertyIndex === -1) {
81
+ return false;
82
+ }
83
+
84
+ const filesProperty = properties[filesPropertyIndex];
85
+
86
+ // Type guard: ensure it's a Property or ObjectProperty (which have a value)
87
+ if ((filesProperty.type !== 'Property' && filesProperty.type !== 'ObjectProperty') || !('value' in filesProperty)) {
88
+ return false;
89
+ }
90
+
91
+ // Check if it's an array with the expected values
92
+ if (filesProperty.value.type === 'ArrayExpression' && filesProperty.value.elements.length === 2) {
93
+ const elements = filesProperty.value.elements;
94
+ const values = elements
95
+ .map((el) => (el?.type === 'Literal' || el?.type === 'StringLiteral' ? el?.value : null))
96
+ .filter((v) => v !== null);
97
+
98
+ // Only transform if it matches the exact pattern we're looking for
99
+ if (values.length === 2 && values.includes('plugin.json') && values.includes('README.md')) {
100
+ // Remove the 'files' property
101
+ properties.splice(filesPropertyIndex, 1);
102
+
103
+ // Add the 'test' property with regex array
104
+ const testProperty = builders.property(
105
+ 'init',
106
+ builders.identifier('test'),
107
+ builders.arrayExpression([builders.literal(/(^|\/)plugin\.json$/), builders.literal(/(^|\/)README\.md$/)])
108
+ );
109
+
110
+ // Insert at the same position
111
+ properties.splice(filesPropertyIndex, 0, testProperty);
112
+ return true;
113
+ }
114
+ }
115
+
116
+ return false;
117
+ }