@grafana/create-plugin 7.4.0 → 7.5.0-canary.2614.25786572918.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CONTRIBUTING.md CHANGED
@@ -109,8 +109,6 @@ Note that certain files are intentionally named differently (e.g. npmrc, package
109
109
 
110
110
  ### Migrations
111
111
 
112
- > **Note:** Migrations are currently behind the `--experimental-updates` flag and are not enabled by default.
113
-
114
112
  Migrations are scripts that update a particular aspect of a project created with create-plugin. When users run `@grafana/create-plugin@latest update`, the command compares their project's version against the running package version and executes any necessary migrations to bring their project up to date.
115
113
 
116
114
  ```js
@@ -48,6 +48,18 @@ var defaultMigrations = [
48
48
  version: "7.3.2",
49
49
  description: "Harden bundle-stats/bundle-size workflow permissions: contents permission was set to write but only read access is required; restricted to read for least-privilege.",
50
50
  scriptPath: import.meta.resolve("./scripts/008-bundle-stats-permissions.js")
51
+ },
52
+ {
53
+ name: "009-remove-tsconfig-baseurl",
54
+ version: "7.4.1",
55
+ description: "Fix TypeScript 6 compatibility: baseUrl is deprecated in TS6 (error TS5101) and must be replaced with an equivalent paths entry to preserve non-relative import resolution.",
56
+ scriptPath: import.meta.resolve("./scripts/009-remove-tsconfig-baseurl.js")
57
+ },
58
+ {
59
+ name: "010-ts-node-nodenext",
60
+ version: "7.4.1",
61
+ description: "Fix ts-node compatibility with the latest @grafana/tsconfig: outdated module/moduleResolution/target overrides break TypeScript 5/6 builds, replaced with nodenext/nodenext/es2022.",
62
+ scriptPath: import.meta.resolve("./scripts/010-ts-node-nodenext.js")
51
63
  }
52
64
  // Do not use LEGACY_UPDATE_CUTOFF_VERSION for new migrations. It is only used above to force migrations to run
53
65
  // for those written before the switch to updates as migrations.
@@ -0,0 +1,24 @@
1
+ import { applyEdits, modify } from 'jsonc-parser';
2
+
3
+ function migrate(context) {
4
+ if (!context.doesFileExist(".config/tsconfig.json")) {
5
+ return context;
6
+ }
7
+ const content = context.getFile(".config/tsconfig.json") || "";
8
+ if (!content.includes('"baseUrl"')) {
9
+ return context;
10
+ }
11
+ const formattingOptions = { formattingOptions: { insertSpaces: true, tabSize: 2 } };
12
+ const contentWithoutBaseUrl = applyEdits(
13
+ content,
14
+ modify(content, ["compilerOptions", "baseUrl"], void 0, formattingOptions)
15
+ );
16
+ const contentWithPaths = applyEdits(
17
+ contentWithoutBaseUrl,
18
+ modify(contentWithoutBaseUrl, ["compilerOptions", "paths", "*"], ["../src/*"], formattingOptions)
19
+ );
20
+ context.updateFile(".config/tsconfig.json", contentWithPaths);
21
+ return context;
22
+ }
23
+
24
+ export { migrate as default };
@@ -0,0 +1,33 @@
1
+ import { applyEdits, modify } from 'jsonc-parser';
2
+
3
+ const TSCONFIG_PATH = ".config/tsconfig.json";
4
+ const FORMATTING_OPTIONS = {
5
+ formattingOptions: { insertSpaces: true, tabSize: 2 }
6
+ };
7
+ const UPDATES = [
8
+ { key: "module", oldValue: "commonjs", newValue: "nodenext" },
9
+ { key: "moduleResolution", newValue: "nodenext" },
10
+ { key: "target", oldValue: "es5", newValue: "es2022" }
11
+ ];
12
+ function migrate(context) {
13
+ if (!context.doesFileExist(TSCONFIG_PATH)) {
14
+ return context;
15
+ }
16
+ const original = context.getFile(TSCONFIG_PATH) || "";
17
+ let next = original;
18
+ for (const { key, oldValue, newValue } of UPDATES) {
19
+ if (oldValue !== void 0) {
20
+ const oldPattern = `"${key}": "${oldValue}"`;
21
+ if (!next.includes(oldPattern)) {
22
+ continue;
23
+ }
24
+ }
25
+ next = applyEdits(next, modify(next, ["ts-node", "compilerOptions", key], newValue, FORMATTING_OPTIONS));
26
+ }
27
+ if (next !== original) {
28
+ context.updateFile(TSCONFIG_PATH, next);
29
+ }
30
+ return context;
31
+ }
32
+
33
+ export { migrate as default };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grafana/create-plugin",
3
- "version": "7.4.0",
3
+ "version": "7.5.0-canary.2614.25786572918.0",
4
4
  "repository": {
5
5
  "directory": "packages/create-plugin",
6
6
  "url": "https://github.com/grafana/plugin-tools"
@@ -55,5 +55,5 @@
55
55
  "engines": {
56
56
  "node": ">=20"
57
57
  },
58
- "gitHead": "d9190314df0893842f12e33bbdfb7d39c3a704cc"
58
+ "gitHead": "01b24eb05e3f0b58fcff5d895015a9783b53a92f"
59
59
  }
@@ -57,6 +57,20 @@ export default [
57
57
  'Harden bundle-stats/bundle-size workflow permissions: contents permission was set to write but only read access is required; restricted to read for least-privilege.',
58
58
  scriptPath: import.meta.resolve('./scripts/008-bundle-stats-permissions.js'),
59
59
  },
60
+ {
61
+ name: '009-remove-tsconfig-baseurl',
62
+ version: '7.4.1',
63
+ description:
64
+ 'Fix TypeScript 6 compatibility: baseUrl is deprecated in TS6 (error TS5101) and must be replaced with an equivalent paths entry to preserve non-relative import resolution.',
65
+ scriptPath: import.meta.resolve('./scripts/009-remove-tsconfig-baseurl.js'),
66
+ },
67
+ {
68
+ name: '010-ts-node-nodenext',
69
+ version: '7.4.1',
70
+ description:
71
+ 'Fix ts-node compatibility with the latest @grafana/tsconfig: outdated module/moduleResolution/target overrides break TypeScript 5/6 builds, replaced with nodenext/nodenext/es2022.',
72
+ scriptPath: import.meta.resolve('./scripts/010-ts-node-nodenext.js'),
73
+ },
60
74
  // Do not use LEGACY_UPDATE_CUTOFF_VERSION for new migrations. It is only used above to force migrations to run
61
75
  // for those written before the switch to updates as migrations.
62
76
  ] satisfies Migration[];
@@ -0,0 +1,91 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { parse } from 'jsonc-parser';
3
+ import migrate from './009-remove-tsconfig-baseurl.js';
4
+ import { Context } from '../../context.js';
5
+
6
+ const SCAFFOLD_TSCONFIG = `/*
7
+ * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY \`@grafana/create-plugin\`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️
8
+ */
9
+ {
10
+ "compilerOptions": {
11
+ "alwaysStrict": true,
12
+ "rootDir": "../src",
13
+ "baseUrl": "../src",
14
+ "typeRoots": ["../node_modules/@types"],
15
+ "resolveJsonModule": true
16
+ },
17
+ "extends": "@grafana/tsconfig"
18
+ }
19
+ `;
20
+
21
+ describe('008-remove-tsconfig-baseurl', () => {
22
+ it('should remove baseUrl and add paths catch-all, preserving comment header and rootDir', () => {
23
+ const context = new Context('/virtual');
24
+ context.addFile('.config/tsconfig.json', SCAFFOLD_TSCONFIG);
25
+
26
+ const result = migrate(context);
27
+ const content = result.getFile('.config/tsconfig.json') || '';
28
+ const config = parse(content) as { compilerOptions: Record<string, unknown> };
29
+
30
+ expect(config.compilerOptions['baseUrl']).toBeUndefined();
31
+ expect(config.compilerOptions['paths']).toEqual({ '*': ['../src/*'] });
32
+ expect(config.compilerOptions['rootDir']).toBe('../src');
33
+ expect(content).toContain('DO NOT EDIT THIS FILE DIRECTLY');
34
+ });
35
+
36
+ it('should be idempotent', async () => {
37
+ const context = new Context('/virtual');
38
+ context.addFile('.config/tsconfig.json', SCAFFOLD_TSCONFIG);
39
+
40
+ await expect(migrate).toBeIdempotent(context);
41
+ });
42
+
43
+ it('should not modify file when baseUrl is already absent', () => {
44
+ const context = new Context('/virtual');
45
+ const content = `{
46
+ "compilerOptions": {
47
+ "rootDir": "../src",
48
+ "paths": { "*": ["../src/*"] }
49
+ }
50
+ }
51
+ `;
52
+ context.addFile('.config/tsconfig.json', content);
53
+
54
+ const result = migrate(context);
55
+
56
+ expect(result.getFile('.config/tsconfig.json')).toBe(content);
57
+ });
58
+
59
+ it('should not modify anything when .config/tsconfig.json does not exist', () => {
60
+ const context = new Context('/virtual');
61
+
62
+ const result = migrate(context);
63
+
64
+ expect(result.hasChanges()).toBe(false);
65
+ });
66
+
67
+ it('should merge paths["*"] into an existing paths object without overwriting other entries', () => {
68
+ const context = new Context('/virtual');
69
+ context.addFile(
70
+ '.config/tsconfig.json',
71
+ `{
72
+ "compilerOptions": {
73
+ "rootDir": "../src",
74
+ "baseUrl": "../src",
75
+ "paths": {
76
+ "@components/*": ["../src/components/*"]
77
+ }
78
+ }
79
+ }
80
+ `
81
+ );
82
+
83
+ const result = migrate(context);
84
+ const content = result.getFile('.config/tsconfig.json') || '';
85
+ const config = parse(content) as { compilerOptions: { paths: Record<string, string[]> } };
86
+
87
+ expect(config.compilerOptions.paths['@components/*']).toEqual(['../src/components/*']);
88
+ expect(config.compilerOptions.paths['*']).toEqual(['../src/*']);
89
+ expect(config.compilerOptions).not.toHaveProperty('baseUrl');
90
+ });
91
+ });
@@ -0,0 +1,32 @@
1
+ import { modify, applyEdits } from 'jsonc-parser';
2
+ import type { Context } from '../../context.js';
3
+
4
+ export default function migrate(context: Context) {
5
+ if (!context.doesFileExist('.config/tsconfig.json')) {
6
+ return context;
7
+ }
8
+
9
+ const content = context.getFile('.config/tsconfig.json') || '';
10
+
11
+ if (!content.includes('"baseUrl"')) {
12
+ return context;
13
+ }
14
+
15
+ const formattingOptions = { formattingOptions: { insertSpaces: true, tabSize: 2 } };
16
+
17
+ // applyEdits accepts Edit[] and modify returns Edit[], but both modify calls must operate on
18
+ // the same base string to produce correct offsets. Chaining them sequentially is safer since
19
+ // removing baseUrl shifts positions in compilerOptions before we add paths.
20
+ const contentWithoutBaseUrl = applyEdits(
21
+ content,
22
+ modify(content, ['compilerOptions', 'baseUrl'], undefined, formattingOptions)
23
+ );
24
+ const contentWithPaths = applyEdits(
25
+ contentWithoutBaseUrl,
26
+ modify(contentWithoutBaseUrl, ['compilerOptions', 'paths', '*'], ['../src/*'], formattingOptions)
27
+ );
28
+
29
+ context.updateFile('.config/tsconfig.json', contentWithPaths);
30
+
31
+ return context;
32
+ }
@@ -0,0 +1,109 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import migrate from './010-ts-node-nodenext.js';
3
+ import { Context } from '../../context.js';
4
+
5
+ const SCAFFOLDED_TSCONFIG = `/*
6
+ * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY \`@grafana/create-plugin\`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️
7
+ *
8
+ * In order to extend the configuration follow the steps in
9
+ * https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations#extend-the-typescript-config
10
+ */
11
+ {
12
+ "compilerOptions": {
13
+ "alwaysStrict": true,
14
+ "declaration": false,
15
+ "rootDir": "../src",
16
+ "paths": {
17
+ "*": ["../src/*"]
18
+ },
19
+ "typeRoots": ["../node_modules/@types"],
20
+ "resolveJsonModule": true
21
+ },
22
+ "ts-node": {
23
+ "compilerOptions": {
24
+ "module": "commonjs",
25
+ "moduleResolution": "node",
26
+ "target": "es5",
27
+ "esModuleInterop": true
28
+ },
29
+ "transpileOnly": true
30
+ },
31
+ "include": ["../src", "./types"],
32
+ "extends": "@grafana/tsconfig"
33
+ }
34
+ `;
35
+
36
+ describe('009-ts-node-nodenext', () => {
37
+ it('should update module, moduleResolution, and target inside ts-node compilerOptions', () => {
38
+ const context = new Context('/virtual');
39
+ context.addFile('.config/tsconfig.json', SCAFFOLDED_TSCONFIG);
40
+
41
+ const result = migrate(context);
42
+ const updated = result.getFile('.config/tsconfig.json') || '';
43
+
44
+ expect(updated).toContain('"module": "nodenext"');
45
+ expect(updated).toContain('"moduleResolution": "nodenext"');
46
+ expect(updated).toContain('"target": "es2022"');
47
+ expect(updated).not.toContain('"module": "commonjs"');
48
+ expect(updated).not.toContain('"moduleResolution": "node"');
49
+ expect(updated).not.toContain('"target": "es5"');
50
+ });
51
+
52
+ it('should preserve the JSONC comment header', () => {
53
+ const context = new Context('/virtual');
54
+ context.addFile('.config/tsconfig.json', SCAFFOLDED_TSCONFIG);
55
+
56
+ const result = migrate(context);
57
+ const updated = result.getFile('.config/tsconfig.json') || '';
58
+
59
+ expect(updated).toContain('THIS FILE WAS SCAFFOLDED BY');
60
+ expect(updated.startsWith('/*')).toBe(true);
61
+ });
62
+
63
+ it('should be idempotent', async () => {
64
+ const context = new Context('/virtual');
65
+ context.addFile('.config/tsconfig.json', SCAFFOLDED_TSCONFIG);
66
+
67
+ await expect(migrate).toBeIdempotent(context);
68
+ });
69
+
70
+ it('should be a no-op when the file is already migrated', () => {
71
+ const alreadyMigrated = SCAFFOLDED_TSCONFIG.replace('"module": "commonjs"', '"module": "nodenext"')
72
+ .replace('"moduleResolution": "node"', '"moduleResolution": "nodenext"')
73
+ .replace('"target": "es5"', '"target": "es2022"');
74
+
75
+ const context = new Context('/virtual');
76
+ context.addFile('.config/tsconfig.json', alreadyMigrated);
77
+
78
+ const result = migrate(context);
79
+
80
+ expect(result.getFile('.config/tsconfig.json')).toBe(alreadyMigrated);
81
+ });
82
+
83
+ it('should be a no-op when .config/tsconfig.json does not exist', () => {
84
+ const context = new Context('/virtual');
85
+
86
+ const result = migrate(context);
87
+
88
+ expect(result.hasChanges()).toBe(false);
89
+ });
90
+
91
+ it('should leave user-customised module/target untouched but always force moduleResolution to nodenext', () => {
92
+ const partiallyCustomised = SCAFFOLDED_TSCONFIG.replace('"target": "es5"', '"target": "es2020"').replace(
93
+ '"moduleResolution": "node"',
94
+ '"moduleResolution": "bundler"'
95
+ );
96
+
97
+ const context = new Context('/virtual');
98
+ context.addFile('.config/tsconfig.json', partiallyCustomised);
99
+
100
+ const result = migrate(context);
101
+ const updated = result.getFile('.config/tsconfig.json') || '';
102
+
103
+ expect(updated).toContain('"module": "nodenext"');
104
+ expect(updated).toContain('"moduleResolution": "nodenext"');
105
+ expect(updated).toContain('"target": "es2020"');
106
+ expect(updated).not.toContain('"moduleResolution": "bundler"');
107
+ expect(updated).not.toContain('"target": "es2022"');
108
+ });
109
+ });
@@ -0,0 +1,39 @@
1
+ import { applyEdits, modify } from 'jsonc-parser';
2
+ import type { Context } from '../../context.js';
3
+
4
+ const TSCONFIG_PATH = '.config/tsconfig.json';
5
+
6
+ const FORMATTING_OPTIONS = {
7
+ formattingOptions: { insertSpaces: true, tabSize: 2 },
8
+ };
9
+
10
+ const UPDATES: Array<{ key: string; oldValue?: string; newValue: string }> = [
11
+ { key: 'module', oldValue: 'commonjs', newValue: 'nodenext' },
12
+ { key: 'moduleResolution', newValue: 'nodenext' },
13
+ { key: 'target', oldValue: 'es5', newValue: 'es2022' },
14
+ ];
15
+
16
+ export default function migrate(context: Context) {
17
+ if (!context.doesFileExist(TSCONFIG_PATH)) {
18
+ return context;
19
+ }
20
+
21
+ const original = context.getFile(TSCONFIG_PATH) || '';
22
+ let next = original;
23
+
24
+ for (const { key, oldValue, newValue } of UPDATES) {
25
+ if (oldValue !== undefined) {
26
+ const oldPattern = `"${key}": "${oldValue}"`;
27
+ if (!next.includes(oldPattern)) {
28
+ continue;
29
+ }
30
+ }
31
+ next = applyEdits(next, modify(next, ['ts-node', 'compilerOptions', key], newValue, FORMATTING_OPTIONS));
32
+ }
33
+
34
+ if (next !== original) {
35
+ context.updateFile(TSCONFIG_PATH, next);
36
+ }
37
+
38
+ return context;
39
+ }
@@ -9,15 +9,17 @@
9
9
  "alwaysStrict": true,
10
10
  "declaration": false,
11
11
  "rootDir": "../src",
12
- "baseUrl": "../src",
12
+ "paths": {
13
+ "*": ["../src/*"]
14
+ },
13
15
  "typeRoots": ["../node_modules/@types"],
14
16
  "resolveJsonModule": true
15
17
  },
16
18
  "ts-node": {
17
19
  "compilerOptions": {
18
- "module": "commonjs",
19
- "moduleResolution": "node",
20
- "target": "es5",
20
+ "module": "nodenext",
21
+ "moduleResolution": "nodenext",
22
+ "target": "es2022",
21
23
  "esModuleInterop": true
22
24
  },
23
25
  "transpileOnly": true