@grafana/create-plugin 5.17.0 → 5.18.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/CHANGELOG.md +18 -0
- package/CONTRIBUTING.md +117 -1
- package/dist/commands/update.command.js +28 -68
- package/dist/commands/update.migrate.command.js +27 -0
- package/dist/commands/update.standard.command.js +51 -0
- package/dist/migrations/context.js +118 -0
- package/dist/migrations/context.test.js +122 -0
- package/dist/migrations/fixtures/foo/bar.js +2 -0
- package/dist/migrations/fixtures/foo/baz.js +1 -0
- package/dist/migrations/fixtures/migrations.js +19 -0
- package/dist/migrations/manager.js +50 -0
- package/dist/migrations/manager.test.js +177 -0
- package/dist/migrations/migrations.js +3 -0
- package/dist/migrations/migrations.test.js +10 -0
- package/dist/migrations/scripts/example-migration.js +23 -0
- package/dist/migrations/scripts/example-migration.test.js +24 -0
- package/dist/migrations/test-utils.js +9 -0
- package/dist/migrations/utils.js +46 -0
- package/dist/migrations/utils.test.js +65 -0
- package/dist/utils/utils.cli.js +3 -0
- package/dist/utils/utils.config.js +9 -1
- package/dist/utils/utils.console.js +6 -0
- package/dist/utils/utils.git.js +13 -0
- package/dist/utils/utils.goSdk.js +6 -6
- package/dist/utils/utils.templates.js +3 -3
- package/package.json +6 -4
- package/src/commands/update.command.ts +33 -75
- package/src/commands/update.migrate.command.ts +31 -0
- package/src/commands/update.standard.command.ts +55 -0
- package/src/migrations/context.test.ts +148 -0
- package/src/migrations/context.ts +155 -0
- package/src/migrations/fixtures/foo/bar.ts +1 -0
- package/src/migrations/fixtures/foo/baz.ts +0 -0
- package/src/migrations/fixtures/migrations.ts +19 -0
- package/src/migrations/manager.test.ts +217 -0
- package/src/migrations/manager.ts +70 -0
- package/src/migrations/migrations.test.ts +12 -0
- package/src/migrations/migrations.ts +20 -0
- package/src/migrations/scripts/example-migration.test.ts +40 -0
- package/src/migrations/scripts/example-migration.ts +34 -0
- package/src/migrations/test-utils.ts +12 -0
- package/src/migrations/utils.test.ts +81 -0
- package/src/migrations/utils.ts +50 -0
- package/src/utils/utils.cli.ts +5 -0
- package/src/utils/utils.config.ts +12 -1
- package/src/utils/utils.console.ts +7 -0
- package/src/utils/utils.git.ts +14 -0
- package/src/utils/utils.goSdk.ts +6 -6
- package/src/utils/utils.templates.ts +3 -3
- package/templates/common/.config/docker-compose-base.yaml +1 -1
- package/templates/common/_package.json +4 -4
- package/tsconfig.json +1 -1
- package/vitest.config.ts +1 -0
- package/vitest.d.ts +11 -0
- package/vitest.setup.ts +53 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
import { getMigrationsToRun, runMigration, runMigrations } from './manager.js';
|
|
3
|
+
import migrationFixtures from './fixtures/migrations.js';
|
|
4
|
+
import { Context } from './context.js';
|
|
5
|
+
import { gitCommitNoVerify } from '../utils/utils.git.js';
|
|
6
|
+
import { flushChanges, printChanges } 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
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock('../utils/utils.config.js', () => ({
|
|
17
|
+
setRootConfig: vi.fn(),
|
|
18
|
+
}));
|
|
19
|
+
vi.mock('../utils/utils.git.js', () => ({
|
|
20
|
+
gitCommitNoVerify: vi.fn(),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
describe('Migrations', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
vi.clearAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('getMigrationsToRun', () => {
|
|
29
|
+
it('should return the migrations that need to be run', () => {
|
|
30
|
+
const fromVersion = '3.0.0';
|
|
31
|
+
const toVersion = '5.0.0';
|
|
32
|
+
const migrations = getMigrationsToRun(fromVersion, toVersion, migrationFixtures.migrations);
|
|
33
|
+
expect(migrations).toEqual({
|
|
34
|
+
'migration-key1': {
|
|
35
|
+
version: '5.0.0',
|
|
36
|
+
description: 'Update project to use new cache directory',
|
|
37
|
+
migrationScript: './5-0-0-cache-directory.js',
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const fromVersion2 = '5.0.0';
|
|
42
|
+
const toVersion2 = '5.5.0';
|
|
43
|
+
const migrations2 = getMigrationsToRun(fromVersion2, toVersion2, migrationFixtures.migrations);
|
|
44
|
+
expect(migrations2).toEqual({
|
|
45
|
+
'migration-key1': {
|
|
46
|
+
version: '5.0.0',
|
|
47
|
+
description: 'Update project to use new cache directory',
|
|
48
|
+
migrationScript: './5-0-0-cache-directory.js',
|
|
49
|
+
},
|
|
50
|
+
'migration-key2': {
|
|
51
|
+
version: '5.4.0',
|
|
52
|
+
description: 'Update project to use new cache directory',
|
|
53
|
+
migrationScript: './5-4-0-cache-directory.js',
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const fromVersion3 = '5.5.0';
|
|
58
|
+
const toVersion3 = '6.0.0';
|
|
59
|
+
const migrations3 = getMigrationsToRun(fromVersion3, toVersion3, migrationFixtures.migrations);
|
|
60
|
+
expect(migrations3).toEqual({
|
|
61
|
+
'migration-key3': {
|
|
62
|
+
version: '6.0.0',
|
|
63
|
+
description: 'Update project to use new cache directory',
|
|
64
|
+
migrationScript: './5-4-0-cache-directory.js',
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should sort migrations by version', () => {
|
|
70
|
+
const fromVersion = '2.0.0';
|
|
71
|
+
const toVersion = '6.0.0';
|
|
72
|
+
const migrations = getMigrationsToRun(fromVersion, toVersion, {
|
|
73
|
+
'migration-key1': {
|
|
74
|
+
version: '5.3.0',
|
|
75
|
+
description: 'Update project to use new cache directory',
|
|
76
|
+
migrationScript: './5.3.0-migration.js',
|
|
77
|
+
},
|
|
78
|
+
'migration-key2': {
|
|
79
|
+
version: '2.3.0',
|
|
80
|
+
description: 'Update project to use new cache directory',
|
|
81
|
+
migrationScript: './2.3.0-migration.js',
|
|
82
|
+
},
|
|
83
|
+
'migration-key3': {
|
|
84
|
+
version: '2.0.0',
|
|
85
|
+
description: 'Update project to use new cache directory',
|
|
86
|
+
migrationScript: './2.0.0-migration.js',
|
|
87
|
+
},
|
|
88
|
+
'migration-key4': {
|
|
89
|
+
version: '2.0.0',
|
|
90
|
+
description: 'Update project to use new cache directory',
|
|
91
|
+
migrationScript: './2.0.0-migration.js',
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(Object.keys(migrations)).toEqual(['migration-key3', 'migration-key4', 'migration-key2', 'migration-key1']);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('runMigration', () => {
|
|
100
|
+
it('should pass a context to the migration script', async () => {
|
|
101
|
+
const mockContext = new Context('/virtual');
|
|
102
|
+
const migrationFn = vi.fn().mockResolvedValue(mockContext);
|
|
103
|
+
|
|
104
|
+
vi.doMock('./test-migration.js', () => ({
|
|
105
|
+
default: migrationFn,
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
const migration: MigrationMeta = {
|
|
109
|
+
version: '1.0.0',
|
|
110
|
+
description: 'test migration',
|
|
111
|
+
migrationScript: './test-migration.js',
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const result = await runMigration(migration, mockContext);
|
|
115
|
+
|
|
116
|
+
expect(migrationFn).toHaveBeenCalledWith(mockContext);
|
|
117
|
+
expect(result).toBe(mockContext);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('runMigrations', () => {
|
|
122
|
+
const migrationOneFn = vi.fn();
|
|
123
|
+
const migrationTwoFn = vi.fn();
|
|
124
|
+
const consoleMock = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
125
|
+
|
|
126
|
+
vi.doMock('./migration-one.js', () => ({
|
|
127
|
+
default: migrationOneFn,
|
|
128
|
+
}));
|
|
129
|
+
vi.doMock('./migration-two.js', () => ({
|
|
130
|
+
default: migrationTwoFn,
|
|
131
|
+
}));
|
|
132
|
+
|
|
133
|
+
const migrations: Record<string, MigrationMeta> = {
|
|
134
|
+
'migration-one': {
|
|
135
|
+
version: '1.0.0',
|
|
136
|
+
description: '...',
|
|
137
|
+
migrationScript: './migration-one.js',
|
|
138
|
+
},
|
|
139
|
+
'migration-two': {
|
|
140
|
+
version: '1.2.0',
|
|
141
|
+
description: '...',
|
|
142
|
+
migrationScript: './migration-two.js',
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
beforeEach(() => {
|
|
147
|
+
migrationOneFn.mockImplementation(async (context: Context) => {
|
|
148
|
+
await context.addFile('one.ts', '');
|
|
149
|
+
|
|
150
|
+
return context;
|
|
151
|
+
});
|
|
152
|
+
migrationTwoFn.mockImplementation(async (context: Context) => {
|
|
153
|
+
await context.addFile('two.ts', '');
|
|
154
|
+
|
|
155
|
+
return context;
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
afterAll(() => {
|
|
160
|
+
consoleMock.mockReset();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should flush the changes for each migration', async () => {
|
|
164
|
+
await runMigrations(migrations);
|
|
165
|
+
|
|
166
|
+
expect(flushChanges).toHaveBeenCalledTimes(2);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should print the changes for each migration', async () => {
|
|
170
|
+
await runMigrations(migrations);
|
|
171
|
+
|
|
172
|
+
expect(printChanges).toHaveBeenCalledTimes(2);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should not commit the changes for each migration by default', async () => {
|
|
176
|
+
await runMigrations(migrations);
|
|
177
|
+
|
|
178
|
+
expect(gitCommitNoVerify).toHaveBeenCalledTimes(0);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should commit the changes for each migration if the CLI arg is present', async () => {
|
|
182
|
+
await runMigrations(migrations, { commitEachMigration: true });
|
|
183
|
+
|
|
184
|
+
expect(gitCommitNoVerify).toHaveBeenCalledTimes(2);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should not create a commit for a migration that has no changes', async () => {
|
|
188
|
+
migrationTwoFn.mockImplementation(async (context: Context) => context);
|
|
189
|
+
|
|
190
|
+
await runMigrations(migrations, { commitEachMigration: true });
|
|
191
|
+
|
|
192
|
+
expect(gitCommitNoVerify).toHaveBeenCalledTimes(1);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should update version in ".config/.cprc.json" on a successful update', async () => {
|
|
196
|
+
await runMigrations(migrations);
|
|
197
|
+
|
|
198
|
+
expect(setRootConfig).toHaveBeenCalledTimes(1);
|
|
199
|
+
|
|
200
|
+
// The latest version in the migrations
|
|
201
|
+
// (For `runMigrations()` this means the last key in the object according to `getMigrationsToRun()`)
|
|
202
|
+
expect(setRootConfig).toHaveBeenCalledWith({ version: '1.2.0' });
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should NOT update version in ".config/.cprc.json" if any of the migrations fail', async () => {
|
|
206
|
+
migrationTwoFn.mockImplementation(async () => {
|
|
207
|
+
throw new Error('Unknown error.');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
await expect(async () => {
|
|
211
|
+
await runMigrations(migrations);
|
|
212
|
+
}).rejects.toThrow();
|
|
213
|
+
|
|
214
|
+
expect(setRootConfig).not.toHaveBeenCalled();
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { satisfies, gte } from 'semver';
|
|
2
|
+
import { Context } from './context.js';
|
|
3
|
+
import defaultMigrations, { MigrationMeta } from './migrations.js';
|
|
4
|
+
import { flushChanges, printChanges, migrationsDebug } from './utils.js';
|
|
5
|
+
import { gitCommitNoVerify } from '../utils/utils.git.js';
|
|
6
|
+
import { setRootConfig } from '../utils/utils.config.js';
|
|
7
|
+
|
|
8
|
+
export type MigrationFn = (context: Context) => Context | Promise<Context>;
|
|
9
|
+
|
|
10
|
+
export function getMigrationsToRun(
|
|
11
|
+
fromVersion: string,
|
|
12
|
+
toVersion: string,
|
|
13
|
+
migrations: Record<string, MigrationMeta> = defaultMigrations.migrations
|
|
14
|
+
): Record<string, MigrationMeta> {
|
|
15
|
+
const semverRange = `${fromVersion} - ${toVersion}`;
|
|
16
|
+
|
|
17
|
+
const migrationsToRun = Object.entries(migrations)
|
|
18
|
+
.sort((a, b) => {
|
|
19
|
+
return gte(a[1].version, b[1].version) ? 1 : -1;
|
|
20
|
+
})
|
|
21
|
+
.reduce<Record<string, MigrationMeta>>((acc, [key, meta]) => {
|
|
22
|
+
if (satisfies(meta.version, semverRange)) {
|
|
23
|
+
acc[key] = meta;
|
|
24
|
+
}
|
|
25
|
+
return acc;
|
|
26
|
+
}, {});
|
|
27
|
+
|
|
28
|
+
return migrationsToRun;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type RunMigrationsOptions = {
|
|
32
|
+
commitEachMigration?: boolean;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export async function runMigrations(migrations: Record<string, MigrationMeta>, options: RunMigrationsOptions = {}) {
|
|
36
|
+
const basePath = process.cwd();
|
|
37
|
+
|
|
38
|
+
console.log('');
|
|
39
|
+
console.log('Running the following migrations:');
|
|
40
|
+
Object.entries(migrations).map(([key, migrationMeta]) => console.log(`- ${key} (${migrationMeta.description})`));
|
|
41
|
+
console.log('');
|
|
42
|
+
|
|
43
|
+
for (const [key, migration] of Object.entries(migrations)) {
|
|
44
|
+
try {
|
|
45
|
+
const context = await runMigration(migration, new Context(basePath));
|
|
46
|
+
const shouldCommit = options.commitEachMigration && context.hasChanges();
|
|
47
|
+
|
|
48
|
+
migrationsDebug(`context for "${key} (${migration.migrationScript})":`);
|
|
49
|
+
migrationsDebug('%O', context.listChanges());
|
|
50
|
+
|
|
51
|
+
flushChanges(context);
|
|
52
|
+
printChanges(context, key, migration);
|
|
53
|
+
|
|
54
|
+
if (shouldCommit) {
|
|
55
|
+
await gitCommitNoVerify(`chore: run create-plugin migration - ${key} (${migration.migrationScript})`);
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error instanceof Error) {
|
|
59
|
+
throw new Error(`Error running migration "${key} (${migration.migrationScript})": ${error.message}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
setRootConfig({ version: Object.values(migrations).at(-1)!.version });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function runMigration(migration: MigrationMeta, context: Context): Promise<Context> {
|
|
67
|
+
const module: { default: MigrationFn } = await import(migration.migrationScript);
|
|
68
|
+
|
|
69
|
+
return module.default(context);
|
|
70
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import defaultMigrations from './migrations.js';
|
|
2
|
+
|
|
3
|
+
describe('migrations json', () => {
|
|
4
|
+
// 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();
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type MigrationMeta = {
|
|
2
|
+
version: string;
|
|
3
|
+
description: string;
|
|
4
|
+
migrationScript: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
type Migrations = {
|
|
8
|
+
migrations: Record<string, MigrationMeta>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
migrations: {
|
|
13
|
+
// Example migration entry (DO NOT UNCOMMENT!)
|
|
14
|
+
// 'example-migration': {
|
|
15
|
+
// version: '5.13.0',
|
|
16
|
+
// description: 'Update build command to use webpack profile flag.',
|
|
17
|
+
// migrationScript: './scripts/example-migration.js',
|
|
18
|
+
// },
|
|
19
|
+
},
|
|
20
|
+
} as Migrations;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import migrate from './example-migration.js';
|
|
2
|
+
import { createDefaultContext } from '../test-utils.js';
|
|
3
|
+
|
|
4
|
+
describe('Migration - append profile to webpack', () => {
|
|
5
|
+
test('should update the package.json', async () => {
|
|
6
|
+
const context = createDefaultContext();
|
|
7
|
+
|
|
8
|
+
context.updateFile(
|
|
9
|
+
'./package.json',
|
|
10
|
+
JSON.stringify({
|
|
11
|
+
scripts: {
|
|
12
|
+
build: 'webpack -c ./.config/webpack/webpack.config.ts --env production',
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const updatedContext = await migrate(context);
|
|
18
|
+
|
|
19
|
+
expect(updatedContext.getFile('./package.json')).toMatch(
|
|
20
|
+
'webpack -c ./.config/webpack/webpack.config.ts --profile --env production'
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
expect(updatedContext.readDir('./src')).toEqual(['src/FOO.md', 'src/foo.json']);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should not make additional changes when run multiple times', async () => {
|
|
27
|
+
const context = await createDefaultContext();
|
|
28
|
+
|
|
29
|
+
await context.updateFile(
|
|
30
|
+
'./package.json',
|
|
31
|
+
JSON.stringify({
|
|
32
|
+
scripts: {
|
|
33
|
+
build: 'webpack -c ./.config/webpack/webpack.config.ts --env production',
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
await expect(migrate).toBeIdempotent(context);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Context } from '../context.js';
|
|
2
|
+
|
|
3
|
+
export default function migrate(context: Context): Context {
|
|
4
|
+
const rawPkgJson = context.getFile('./package.json') ?? '{}';
|
|
5
|
+
const packageJson = JSON.parse(rawPkgJson);
|
|
6
|
+
|
|
7
|
+
if (packageJson.scripts && packageJson.scripts.build) {
|
|
8
|
+
const buildScript = packageJson.scripts.build;
|
|
9
|
+
|
|
10
|
+
const pattern = /(webpack.+-c\s.+\.ts)\s(.+)/;
|
|
11
|
+
|
|
12
|
+
if (pattern.test(buildScript) && !buildScript.includes('--profile')) {
|
|
13
|
+
packageJson.scripts.build = buildScript.replace(pattern, `$1 --profile $2`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
context.updateFile('./package.json', JSON.stringify(packageJson, null, 2));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (context.doesFileExist('./src/README.md')) {
|
|
20
|
+
context.deleteFile('./src/README.md');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!context.doesFileExist('./src/foo.json')) {
|
|
24
|
+
context.addFile('./src/foo.json', JSON.stringify({ foo: 'bar' }));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (context.doesFileExist('.eslintrc')) {
|
|
28
|
+
context.renameFile('.eslintrc', '.eslint.config.json');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
context.readDir('./src');
|
|
32
|
+
|
|
33
|
+
return context;
|
|
34
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Context } from './context.js';
|
|
2
|
+
|
|
3
|
+
export function createDefaultContext() {
|
|
4
|
+
const context = new Context('/virtual');
|
|
5
|
+
|
|
6
|
+
context.addFile('.eslintrc', '{}');
|
|
7
|
+
context.addFile('./package.json', '{}');
|
|
8
|
+
context.addFile('./src/README.md', '');
|
|
9
|
+
context.addFile('./src/FOO.md', '');
|
|
10
|
+
|
|
11
|
+
return context;
|
|
12
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { dirSync } from 'tmp';
|
|
2
|
+
import { Context } from './context.js';
|
|
3
|
+
import { flushChanges, printChanges } from './utils.js';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
|
|
8
|
+
describe('utils', () => {
|
|
9
|
+
const tmpObj = dirSync({ unsafeCleanup: true });
|
|
10
|
+
const tmpDir = join(tmpObj.name, 'cp-test-migration');
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
14
|
+
await mkdir(tmpDir, { recursive: true });
|
|
15
|
+
|
|
16
|
+
await writeFile(join(tmpDir, 'bar.ts'), 'content');
|
|
17
|
+
await writeFile(join(tmpDir, 'baz.ts'), 'content');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterAll(() => {
|
|
21
|
+
tmpObj.removeCallback();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('flushChanges', () => {
|
|
25
|
+
it('should write files to disk', async () => {
|
|
26
|
+
const context = new Context(tmpDir);
|
|
27
|
+
await context.addFile('file.txt', 'content');
|
|
28
|
+
await context.addFile('deeper/path/to/file.txt', 'content');
|
|
29
|
+
flushChanges(context);
|
|
30
|
+
expect(readFileSync(join(tmpDir, 'file.txt'), 'utf-8')).toBe('content');
|
|
31
|
+
expect(readFileSync(join(tmpDir, 'deeper/path/to/file.txt'), 'utf-8')).toBe('content');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should update files on disk', async () => {
|
|
35
|
+
const context = new Context(tmpDir);
|
|
36
|
+
await context.updateFile('bar.ts', 'new content');
|
|
37
|
+
flushChanges(context);
|
|
38
|
+
expect(readFileSync(join(tmpDir, 'bar.ts'), 'utf-8')).toBe('new content');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should delete files from disk', async () => {
|
|
42
|
+
const context = new Context(tmpDir);
|
|
43
|
+
await context.deleteFile('bar.ts');
|
|
44
|
+
flushChanges(context);
|
|
45
|
+
expect(() => readFileSync(join(tmpDir, 'bar.ts'), 'utf-8')).toThrowError();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('printChanges', () => {
|
|
50
|
+
const originalConsoleLog = console.log;
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
vitest.spyOn(console, 'log').mockImplementation(() => {});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
console.log = originalConsoleLog;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should print changes', async () => {
|
|
61
|
+
const context = new Context(tmpDir);
|
|
62
|
+
await context.addFile('file.txt', 'content');
|
|
63
|
+
await context.updateFile('baz.ts', 'new content');
|
|
64
|
+
await context.deleteFile('bar.ts');
|
|
65
|
+
|
|
66
|
+
printChanges(context, 'key', { migrationScript: 'test', description: 'test', version: '1.0.0' });
|
|
67
|
+
|
|
68
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringMatching(/ADD.+file\.txt/));
|
|
69
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringMatching(/UPDATE.+baz\.ts/));
|
|
70
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringMatching(/DELETE.+bar\.ts/));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should print no changes', async () => {
|
|
74
|
+
const context = new Context(tmpDir);
|
|
75
|
+
|
|
76
|
+
printChanges(context, 'key', { migrationScript: 'test', description: 'test', version: '1.0.0' });
|
|
77
|
+
|
|
78
|
+
expect(console.log).toHaveBeenCalledWith(expect.stringMatching(/No changes were made/));
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { dirname, join } from 'node:path';
|
|
2
|
+
import { Context } from './context.js';
|
|
3
|
+
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { debug } from '../utils/utils.cli.js';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { MigrationMeta } from './migrations.js';
|
|
7
|
+
|
|
8
|
+
export function printChanges(context: Context, key: string, migration: MigrationMeta) {
|
|
9
|
+
const changes = context.listChanges();
|
|
10
|
+
const lines = [];
|
|
11
|
+
|
|
12
|
+
for (const [filePath, { changeType }] of Object.entries(changes)) {
|
|
13
|
+
if (changeType === 'add') {
|
|
14
|
+
lines.push(`${chalk.green('ADD')} ${filePath}`);
|
|
15
|
+
} else if (changeType === 'update') {
|
|
16
|
+
lines.push(`${chalk.yellow('UPDATE')} ${filePath}`);
|
|
17
|
+
} else if (changeType === 'delete') {
|
|
18
|
+
lines.push(`${chalk.red('DELETE')} ${filePath}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log('--------------------------------');
|
|
23
|
+
console.log('Running migration:', key, chalk.bold(migration.migrationScript));
|
|
24
|
+
|
|
25
|
+
if (lines.length === 0) {
|
|
26
|
+
console.log('No changes were made');
|
|
27
|
+
} else {
|
|
28
|
+
console.log(`${chalk.bold('Changes:')}\n ${lines.join('\n ')}`);
|
|
29
|
+
}
|
|
30
|
+
console.log('');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function flushChanges(context: Context) {
|
|
34
|
+
const basePath = context.basePath;
|
|
35
|
+
const changes = context.listChanges();
|
|
36
|
+
|
|
37
|
+
for (const [filePath, { changeType, content }] of Object.entries(changes)) {
|
|
38
|
+
const resolvedPath = join(basePath, filePath);
|
|
39
|
+
if (changeType === 'add') {
|
|
40
|
+
mkdirSync(dirname(resolvedPath), { recursive: true });
|
|
41
|
+
writeFileSync(resolvedPath, content!);
|
|
42
|
+
} else if (changeType === 'update') {
|
|
43
|
+
writeFileSync(resolvedPath, content!);
|
|
44
|
+
} else if (changeType === 'delete') {
|
|
45
|
+
rmSync(resolvedPath);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const migrationsDebug = debug.extend('migrations');
|
package/src/utils/utils.cli.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import minimist from 'minimist';
|
|
2
|
+
import createDebug from 'debug';
|
|
3
|
+
|
|
4
|
+
export const debug = createDebug('create-plugin');
|
|
2
5
|
|
|
3
6
|
export const args = process.argv.slice(2);
|
|
4
7
|
|
|
@@ -10,6 +13,8 @@ export const argv = minimist(args, {
|
|
|
10
13
|
hasBackend: 'backend',
|
|
11
14
|
pluginName: 'plugin-name',
|
|
12
15
|
orgName: 'org-name',
|
|
16
|
+
// temporary flag whilst we work on the migration updates
|
|
17
|
+
experimentalUpdates: 'experimental-updates',
|
|
13
18
|
},
|
|
14
19
|
});
|
|
15
20
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import { writeFile } from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { getVersion } from './utils.version.js';
|
|
4
5
|
import { argv, commandName } from './utils.cli.js';
|
|
@@ -91,7 +92,7 @@ function readRCFileSync(path: string): CreatePluginConfig | undefined {
|
|
|
91
92
|
// This function creates feature flags based on the defaults for generate command else flags read from config.
|
|
92
93
|
// In all cases it will override the flags with the featureFlag cli arg values.
|
|
93
94
|
function createFeatureFlags(flags?: FeatureFlags): FeatureFlags {
|
|
94
|
-
const featureFlags = commandName === 'generate' ? DEFAULT_FEATURE_FLAGS : flags ?? {};
|
|
95
|
+
const featureFlags = commandName === 'generate' ? DEFAULT_FEATURE_FLAGS : (flags ?? {});
|
|
95
96
|
const cliArgFlags = parseFeatureFlagsFromCliArgs();
|
|
96
97
|
return { ...featureFlags, ...cliArgFlags };
|
|
97
98
|
}
|
|
@@ -117,3 +118,13 @@ function parseFeatureFlagsFromCliArgs() {
|
|
|
117
118
|
return { ...acc, [flag]: true };
|
|
118
119
|
}, {} as FeatureFlags);
|
|
119
120
|
}
|
|
121
|
+
|
|
122
|
+
export async function setRootConfig(configOverride: Partial<CreatePluginConfig> = {}): Promise<CreatePluginConfig> {
|
|
123
|
+
const rootConfig = getRootConfig();
|
|
124
|
+
const rootConfigPath = path.resolve(process.cwd(), '.config/.cprc.json');
|
|
125
|
+
const updatedConfig = { ...rootConfig, ...configOverride };
|
|
126
|
+
|
|
127
|
+
await writeFile(rootConfigPath, JSON.stringify(updatedConfig, null, 2));
|
|
128
|
+
|
|
129
|
+
return updatedConfig;
|
|
130
|
+
}
|
|
@@ -13,6 +13,13 @@ marked.use(
|
|
|
13
13
|
}) as MarkedExtension
|
|
14
14
|
);
|
|
15
15
|
|
|
16
|
+
export function printHeader(message: string, status: 'success' | 'info' | 'error' = 'success') {
|
|
17
|
+
const color = status === 'success' ? 'green' : status === 'info' ? 'blue' : 'red';
|
|
18
|
+
let prefix = chalk.reset.inverse.bold[color](` CREATE PLUGIN `);
|
|
19
|
+
let txt = chalk[color](message);
|
|
20
|
+
console.log(`${prefix} ${txt}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
16
23
|
export function displayAsMarkdown(msg: string) {
|
|
17
24
|
return marked(msg);
|
|
18
25
|
}
|
package/src/utils/utils.git.ts
CHANGED
|
@@ -27,3 +27,17 @@ export async function isGitDirectoryClean() {
|
|
|
27
27
|
return false;
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
+
|
|
31
|
+
export async function gitCommitNoVerify(commitMsg: string) {
|
|
32
|
+
try {
|
|
33
|
+
let addAllCommand = 'git add -A';
|
|
34
|
+
let commitCommand = `git commit --no-verify -m ${commitMsg}`;
|
|
35
|
+
|
|
36
|
+
await exec(addAllCommand);
|
|
37
|
+
await exec(commitCommand);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error instanceof Error) {
|
|
40
|
+
throw new Error(`Error committing changes:\n${error.message}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/utils/utils.goSdk.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import which from 'which';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
-
import createDebug from 'debug';
|
|
4
3
|
import { exec } from 'node:child_process';
|
|
4
|
+
import { debug } from './utils.cli.js';
|
|
5
5
|
|
|
6
6
|
const SDK_GO_MODULE = 'github.com/grafana/grafana-plugin-sdk-go';
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const updateGoDebugger = debug.extend('update-go');
|
|
9
9
|
|
|
10
10
|
export async function updateGoSdkAndModules(exportPath: string) {
|
|
11
11
|
// check if there is a go.mod file in exportPath
|
|
@@ -40,7 +40,7 @@ function updateSdk(exportPath: string): Promise<void> {
|
|
|
40
40
|
const command = `go get ${SDK_GO_MODULE}`;
|
|
41
41
|
exec(command, { cwd: exportPath }, (error) => {
|
|
42
42
|
if (error) {
|
|
43
|
-
|
|
43
|
+
updateGoDebugger(error);
|
|
44
44
|
reject();
|
|
45
45
|
}
|
|
46
46
|
resolve();
|
|
@@ -54,7 +54,7 @@ function updateGoMod(exportPath: string): Promise<void> {
|
|
|
54
54
|
const command = `go mod tidy`;
|
|
55
55
|
exec(command, { cwd: exportPath }, (error) => {
|
|
56
56
|
if (error) {
|
|
57
|
-
|
|
57
|
+
updateGoDebugger(error);
|
|
58
58
|
reject();
|
|
59
59
|
}
|
|
60
60
|
resolve();
|
|
@@ -68,7 +68,7 @@ function getLatestSdkVersion(exportPath: string): Promise<string> {
|
|
|
68
68
|
const command = `go list -m -json ${SDK_GO_MODULE}@latest`;
|
|
69
69
|
exec(command, { cwd: exportPath }, (error, stdout) => {
|
|
70
70
|
if (error) {
|
|
71
|
-
|
|
71
|
+
updateGoDebugger(error);
|
|
72
72
|
reject();
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -76,7 +76,7 @@ function getLatestSdkVersion(exportPath: string): Promise<string> {
|
|
|
76
76
|
const version = JSON.parse(stdout).Version;
|
|
77
77
|
resolve(version);
|
|
78
78
|
} catch (e) {
|
|
79
|
-
|
|
79
|
+
updateGoDebugger(e);
|
|
80
80
|
reject();
|
|
81
81
|
}
|
|
82
82
|
});
|