@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
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
# v5.18.0 (Wed Feb 19 2025)
|
|
2
|
+
|
|
3
|
+
#### 🚀 Enhancement
|
|
4
|
+
|
|
5
|
+
- Feat: Create Plugin updates as migrations [#1479](https://github.com/grafana/plugin-tools/pull/1479) ([@jackw](https://github.com/jackw) [@leventebalogh](https://github.com/leventebalogh))
|
|
6
|
+
|
|
7
|
+
#### 🐛 Bug Fix
|
|
8
|
+
|
|
9
|
+
- chore(deps): update grafana patch dependencies to v11.5.2 [#1549](https://github.com/grafana/plugin-tools/pull/1549) ([@renovate[bot]](https://github.com/renovate[bot]))
|
|
10
|
+
|
|
11
|
+
#### Authors: 3
|
|
12
|
+
|
|
13
|
+
- [@renovate[bot]](https://github.com/renovate[bot])
|
|
14
|
+
- Jack Westbrook ([@jackw](https://github.com/jackw))
|
|
15
|
+
- Levente Balogh ([@leventebalogh](https://github.com/leventebalogh))
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
1
19
|
# v5.17.0 (Fri Feb 14 2025)
|
|
2
20
|
|
|
3
21
|
#### 🚀 Enhancement
|
package/CONTRIBUTING.md
CHANGED
|
@@ -33,7 +33,8 @@ npm install
|
|
|
33
33
|
├── src // Executable code
|
|
34
34
|
│ ├── bin // the entrypoint file
|
|
35
35
|
│ ├── commands // Code that runs commands
|
|
36
|
-
│
|
|
36
|
+
│ ├── utils // Utilities used by commands
|
|
37
|
+
│ └── migrations // Migrations for updating create-plugins
|
|
37
38
|
└── templates // Handlebars templates
|
|
38
39
|
├── _partials // Composable parts of a template
|
|
39
40
|
├── app // Templates specific to scaffolding an app plugin
|
|
@@ -81,3 +82,118 @@ _Work in progress._
|
|
|
81
82
|
The templates are used by Handlebars to scaffold Grafana plugins. Whilst they appear to be the intended filetype they are infact treated as markdown by Handlebars when it runs. As such we need to be mindful of syntax and to [escape particular characters](https://handlebarsjs.com/guide/expressions.html#whitespace-control) where necessary. The [github/ci.yml](./templates/github/ci/.github/workflows/ci.yml) file is a good example of this.
|
|
82
83
|
|
|
83
84
|
Note that certain files are intentionally named differently (e.g. npmrc, package.json). This is done due to other tooling preventing the files from being packaged for NPM or breaking other tools during local development.
|
|
85
|
+
|
|
86
|
+
### Migrations
|
|
87
|
+
|
|
88
|
+
> **Note:** Migrations are currently behind the `--experimental-updates` flag and are not enabled by default.
|
|
89
|
+
|
|
90
|
+
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.
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
└── src/
|
|
94
|
+
├── migrations/
|
|
95
|
+
│ └── scripts/ // The directory where migration scripts live
|
|
96
|
+
│ ├── add-webpack-profile.test.ts // migration script tests
|
|
97
|
+
│ └── add-webpack-profile.ts // migration script
|
|
98
|
+
├── context.ts // The context object that is passed to the migration scripts
|
|
99
|
+
├── manager.ts // The manager object that is used to run the migration scripts
|
|
100
|
+
├── migrations.ts // The configuration that registers the migration scripts
|
|
101
|
+
└── utils.ts // The utilities used by the migration scripts
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### How do migrations run?
|
|
105
|
+
|
|
106
|
+
The update command follows these steps:
|
|
107
|
+
|
|
108
|
+
1. Checks the current project version (`projectCpVersion`) and package version (`packageCpVersion`).
|
|
109
|
+
2. If the project version is greater than or equal to the package version, exits early.
|
|
110
|
+
3. Identifies all migrations needed between the project version and package version.
|
|
111
|
+
4. Executes migrations sequentially.
|
|
112
|
+
5. If the `--commit` flag is passed, it will commit changes after each migration.
|
|
113
|
+
|
|
114
|
+
#### How to add a migration?
|
|
115
|
+
|
|
116
|
+
1. Create a new migration script file with a descriptive name (e.g. `add-webpack-profile.ts`)
|
|
117
|
+
2. Register your migration in `migrations.ts`:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
migrations: {
|
|
121
|
+
'add-webpack-profile': {
|
|
122
|
+
version: '5.13.0',
|
|
123
|
+
description: 'Update build command to use webpack profile flag.',
|
|
124
|
+
migrationScript: './scripts/add-webpack-profile.js',
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
3. Write your migration script:
|
|
130
|
+
|
|
131
|
+
The migration script makes changes to files in a Grafana plugin to bring updates or improvements to the project. It should be isolated to a single task (e.g. add profile flag to webpack builds) rather than update the entire .config directory and all the projects dependencies.
|
|
132
|
+
|
|
133
|
+
> **Note:** The migration script must use the context to access the file system and return the updated context.
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
import { Context } from '../context.js';
|
|
137
|
+
|
|
138
|
+
export default async function (context: Context): Promise<Context> {
|
|
139
|
+
// Your migration logic here. for example:
|
|
140
|
+
// update files, delete files, add files, rename files, etc.
|
|
141
|
+
// Once done, return the updated context.
|
|
142
|
+
return context;
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
#### How to test a migration?
|
|
147
|
+
|
|
148
|
+
Migrations should be thoroughly tested using the provided testing utilities. Create a test file alongside your migration script (e.g., `add-webpack-profile.test.ts`).
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import migrate from './add-webpack-profile.js';
|
|
152
|
+
import { createDefaultContext } from '../test-utils.js';
|
|
153
|
+
|
|
154
|
+
describe('Migration - append profile to webpack', () => {
|
|
155
|
+
test('should update the package.json', async () => {
|
|
156
|
+
// 1. Set up a test context with some default files.
|
|
157
|
+
const context = await createDefaultContext();
|
|
158
|
+
|
|
159
|
+
// 2. Create some file state to test against.
|
|
160
|
+
await context.updateFile(
|
|
161
|
+
'./package.json',
|
|
162
|
+
JSON.stringify({
|
|
163
|
+
scripts: {
|
|
164
|
+
build: 'webpack -c ./.config/webpack/webpack.config.ts --env production',
|
|
165
|
+
},
|
|
166
|
+
})
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// 3. Run the migration function and get the updated context.
|
|
170
|
+
const updatedContext = await migrate(context);
|
|
171
|
+
|
|
172
|
+
// 4. Assert expected changes
|
|
173
|
+
expect(await updatedContext.getFile('./package.json')).toMatch(
|
|
174
|
+
'webpack -c ./.config/webpack/webpack.config.ts --profile --env production'
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
It is important that migration scripts can run multiple times without making additional changes to files. To make it easy to test this you can make use of the `.toBeIdempotent()` test matcher.
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
describe('Migration - append profile to webpack', () => {
|
|
184
|
+
it('should not make additional changes when run multiple times', async () => {
|
|
185
|
+
const context = await createDefaultContext();
|
|
186
|
+
|
|
187
|
+
await context.updateFile(
|
|
188
|
+
'./package.json',
|
|
189
|
+
JSON.stringify({
|
|
190
|
+
scripts: {
|
|
191
|
+
build: 'webpack -c ./.config/webpack/webpack.config.ts --env production',
|
|
192
|
+
},
|
|
193
|
+
})
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
await expect(migrate).toBeIdempotent(context);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
```
|
|
@@ -1,78 +1,38 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import { printBlueBox, printRedBox } from '../utils/utils.console.js';
|
|
4
|
-
import { getOnlyExistingInCwd, removeFilesInCwd } from '../utils/utils.files.js';
|
|
1
|
+
import { standardUpdate } from './update.standard.command.js';
|
|
2
|
+
import { migrationUpdate } from './update.migrate.command.js';
|
|
5
3
|
import { isGitDirectory, isGitDirectoryClean } from '../utils/utils.git.js';
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import { isPluginDirectory, updateDotConfigFolder } from '../utils/utils.plugin.js';
|
|
10
|
-
import { getGrafanaRuntimeVersion, getVersion } from '../utils/utils.version.js';
|
|
4
|
+
import { printRedBox } from '../utils/utils.console.js';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { isPluginDirectory } from '../utils/utils.plugin.js';
|
|
11
7
|
export const update = async (argv) => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
title: 'You are not inside a git directory',
|
|
17
|
-
content: `In order to proceed please run "git init" in the root of your project and commit your changes.\n
|
|
8
|
+
if (!(await isGitDirectory()) && !argv.force) {
|
|
9
|
+
printRedBox({
|
|
10
|
+
title: 'You are not inside a git directory',
|
|
11
|
+
content: `In order to proceed please run "git init" in the root of your project and commit your changes.\n
|
|
18
12
|
(This check is necessary to make sure that the updates are easy to revert and don't mess with any changes you currently have.
|
|
19
13
|
In case you want to proceed as is please use the ${chalk.bold('--force')} flag.)`,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
14
|
+
});
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
if (!(await isGitDirectoryClean()) && !argv.force) {
|
|
18
|
+
printRedBox({
|
|
19
|
+
title: 'Please clean your repository working tree before updating.',
|
|
20
|
+
subtitle: '(Commit your changes or stash them.)',
|
|
21
|
+
content: `(This check is necessary to make sure that the updates are easy to revert and don't mess with any changes you currently have.
|
|
28
22
|
In case you want to proceed as is please use the ${chalk.bold('--force')} flag.)`,
|
|
29
|
-
});
|
|
30
|
-
process.exit(1);
|
|
31
|
-
}
|
|
32
|
-
if (!isPluginDirectory() && !argv.force) {
|
|
33
|
-
printRedBox({
|
|
34
|
-
title: 'Are you inside a plugin directory?',
|
|
35
|
-
subtitle: 'We couldn\'t find a "src/plugin.json" file under your current directory.',
|
|
36
|
-
content: `(Please make sure to run this command from the root of your plugin folder. In case you want to proceed as is please use the ${chalk.bold('--force')} flag.)`,
|
|
37
|
-
});
|
|
38
|
-
process.exit(1);
|
|
39
|
-
}
|
|
40
|
-
await updateDotConfigFolder();
|
|
41
|
-
updateNpmScripts();
|
|
42
|
-
updatePackageJson({
|
|
43
|
-
onlyOutdated: true,
|
|
44
|
-
ignoreGrafanaDependencies: false,
|
|
45
23
|
});
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
content: `${chalk.bold('@grafana/* package version:')} ${getGrafanaRuntimeVersion()}
|
|
54
|
-
${chalk.bold('@grafana/create-plugin version:')} ${getVersion()}
|
|
55
|
-
|
|
56
|
-
${chalk.bold.underline('Next steps:')}
|
|
57
|
-
- 1. Run ${chalk.bold(`${packageManagerName} install`)} to install the package updates
|
|
58
|
-
- 2. Check if you encounter any breaking changes
|
|
59
|
-
(refer to our migration guide: https://grafana.com/developers/plugin-tools/migration-guides/update-from-grafana-versions/)
|
|
60
|
-
${chalk.bold('Do you have questions?')}
|
|
61
|
-
Please don't hesitate to reach out in one of the following ways:
|
|
62
|
-
- Open an issue in https://github.com/grafana/plugin-tools
|
|
63
|
-
- Ask a question in the community forum at https://community.grafana.com/c/plugin-development/30
|
|
64
|
-
- Join our community slack channel at https://slack.grafana.com/`,
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
if (!isPluginDirectory() && !argv.force) {
|
|
27
|
+
printRedBox({
|
|
28
|
+
title: 'Are you inside a plugin directory?',
|
|
29
|
+
subtitle: 'We couldn\'t find a "src/plugin.json" file under your current directory.',
|
|
30
|
+
content: `(Please make sure to run this command from the root of your plugin folder. In case you want to proceed as is please use the ${chalk.bold('--force')} flag.)`,
|
|
65
31
|
});
|
|
32
|
+
process.exit(1);
|
|
66
33
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
printRedBox({
|
|
70
|
-
title: 'Something went wrong while updating your plugin.',
|
|
71
|
-
content: error.message,
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
console.error(error);
|
|
76
|
-
}
|
|
34
|
+
if (argv.experimentalUpdates) {
|
|
35
|
+
return await migrationUpdate(argv);
|
|
77
36
|
}
|
|
37
|
+
return await standardUpdate();
|
|
78
38
|
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { gte } from 'semver';
|
|
2
|
+
import { getMigrationsToRun, runMigrations } from '../migrations/manager.js';
|
|
3
|
+
import { getConfig } from '../utils/utils.config.js';
|
|
4
|
+
import { getVersion } from '../utils/utils.version.js';
|
|
5
|
+
import { printHeader } from '../utils/utils.console.js';
|
|
6
|
+
export const migrationUpdate = async (argv) => {
|
|
7
|
+
try {
|
|
8
|
+
const projectCpVersion = getConfig().version;
|
|
9
|
+
const packageCpVersion = getVersion();
|
|
10
|
+
if (gte(projectCpVersion, packageCpVersion)) {
|
|
11
|
+
console.warn('Nothing to update, exiting.');
|
|
12
|
+
process.exit(0);
|
|
13
|
+
}
|
|
14
|
+
console.log(`Running migrations from ${projectCpVersion} to ${packageCpVersion}.`);
|
|
15
|
+
const commitEachMigration = argv.commit;
|
|
16
|
+
const migrations = getMigrationsToRun(projectCpVersion, packageCpVersion);
|
|
17
|
+
await runMigrations(migrations, { commitEachMigration });
|
|
18
|
+
printHeader('the update command completed successfully.');
|
|
19
|
+
console.log('');
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
printHeader('the update command encountered an error.');
|
|
23
|
+
console.log('');
|
|
24
|
+
console.error(error);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { UDPATE_CONFIG } from '../constants.js';
|
|
3
|
+
import { printBlueBox, printRedBox } from '../utils/utils.console.js';
|
|
4
|
+
import { getOnlyExistingInCwd, removeFilesInCwd } from '../utils/utils.files.js';
|
|
5
|
+
import { updateGoSdkAndModules } from '../utils/utils.goSdk.js';
|
|
6
|
+
import { updateNpmScripts, updatePackageJson } from '../utils/utils.npm.js';
|
|
7
|
+
import { getPackageManagerFromUserAgent } from '../utils/utils.packageManager.js';
|
|
8
|
+
import { updateDotConfigFolder } from '../utils/utils.plugin.js';
|
|
9
|
+
import { getGrafanaRuntimeVersion, getVersion } from '../utils/utils.version.js';
|
|
10
|
+
export const standardUpdate = async () => {
|
|
11
|
+
const { packageManagerName } = getPackageManagerFromUserAgent();
|
|
12
|
+
try {
|
|
13
|
+
await updateDotConfigFolder();
|
|
14
|
+
updateNpmScripts();
|
|
15
|
+
updatePackageJson({
|
|
16
|
+
onlyOutdated: true,
|
|
17
|
+
ignoreGrafanaDependencies: false,
|
|
18
|
+
});
|
|
19
|
+
await updateGoSdkAndModules(process.cwd());
|
|
20
|
+
const filesToRemove = getOnlyExistingInCwd(UDPATE_CONFIG.filesToRemove);
|
|
21
|
+
if (filesToRemove.length) {
|
|
22
|
+
removeFilesInCwd(filesToRemove);
|
|
23
|
+
}
|
|
24
|
+
printBlueBox({
|
|
25
|
+
title: 'Update successful ✔',
|
|
26
|
+
content: `${chalk.bold('@grafana/* package version:')} ${getGrafanaRuntimeVersion()}
|
|
27
|
+
${chalk.bold('@grafana/create-plugin version:')} ${getVersion()}
|
|
28
|
+
|
|
29
|
+
${chalk.bold.underline('Next steps:')}
|
|
30
|
+
- 1. Run ${chalk.bold(`${packageManagerName} install`)} to install the package updates
|
|
31
|
+
- 2. Check if you encounter any breaking changes
|
|
32
|
+
(refer to our migration guide: https://grafana.com/developers/plugin-tools/migration-guides/update-from-grafana-versions/)
|
|
33
|
+
${chalk.bold('Do you have questions?')}
|
|
34
|
+
Please don't hesitate to reach out in one of the following ways:
|
|
35
|
+
- Open an issue in https://github.com/grafana/plugin-tools
|
|
36
|
+
- Ask a question in the community forum at https://community.grafana.com/c/plugin-development/30
|
|
37
|
+
- Join our community slack channel at https://slack.grafana.com/`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error instanceof Error) {
|
|
42
|
+
printRedBox({
|
|
43
|
+
title: 'Something went wrong while updating your plugin.',
|
|
44
|
+
content: error.message,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
console.error(error);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { constants, accessSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { relative, normalize, join, dirname } from 'node:path';
|
|
3
|
+
import { migrationsDebug } from './utils.js';
|
|
4
|
+
export class Context {
|
|
5
|
+
files = {};
|
|
6
|
+
basePath;
|
|
7
|
+
constructor(basePath) {
|
|
8
|
+
this.basePath = basePath || process.cwd();
|
|
9
|
+
}
|
|
10
|
+
addFile(filePath, content) {
|
|
11
|
+
const path = this.normalisePath(filePath);
|
|
12
|
+
if (!this.doesFileExist(path)) {
|
|
13
|
+
this.files[path] = { content, changeType: 'add' };
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
throw new Error(`File ${path} already exists`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
deleteFile(filePath) {
|
|
20
|
+
const path = this.normalisePath(filePath);
|
|
21
|
+
if (this.files[path] && this.files[path].changeType === 'add') {
|
|
22
|
+
delete this.files[path];
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
else if (this.doesFileExistOnDisk(path)) {
|
|
26
|
+
this.files[path] = { ...this.files[path], changeType: 'delete' };
|
|
27
|
+
}
|
|
28
|
+
else if (this.files[path] && this.files[path].changeType === 'update') {
|
|
29
|
+
throw new Error(`File ${path} was marked as updated already`);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
throw new Error(`File ${path} does not exist`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
updateFile(filePath, content) {
|
|
36
|
+
const path = this.normalisePath(filePath);
|
|
37
|
+
const originalContent = this.getFile(path);
|
|
38
|
+
if (originalContent === undefined) {
|
|
39
|
+
throw new Error(`File ${path} does not exist`);
|
|
40
|
+
}
|
|
41
|
+
if (originalContent !== content) {
|
|
42
|
+
this.files[path] = { content, changeType: 'update' };
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
migrationsDebug(`Context.updateFile() - no updates for ${filePath}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
doesFileExist(filePath) {
|
|
49
|
+
const path = this.normalisePath(filePath);
|
|
50
|
+
if (this.files[path] && this.files[path].changeType !== 'delete') {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return this.doesFileExistOnDisk(path);
|
|
54
|
+
}
|
|
55
|
+
doesFileExistOnDisk(filePath) {
|
|
56
|
+
const path = join(this.basePath, this.normalisePath(filePath));
|
|
57
|
+
try {
|
|
58
|
+
accessSync(path, constants.R_OK | constants.W_OK);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
getFile(filePath) {
|
|
66
|
+
const path = this.normalisePath(filePath);
|
|
67
|
+
if (this.files[path] && this.files[path].changeType === 'delete') {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
if (this.files[path]) {
|
|
71
|
+
return this.files[path].content;
|
|
72
|
+
}
|
|
73
|
+
if (this.doesFileExistOnDisk(path)) {
|
|
74
|
+
return readFileSync(join(this.basePath, path), 'utf-8');
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
listChanges() {
|
|
79
|
+
return this.files;
|
|
80
|
+
}
|
|
81
|
+
hasChanges() {
|
|
82
|
+
return Object.keys(this.files).length > 0;
|
|
83
|
+
}
|
|
84
|
+
renameFile(from, to) {
|
|
85
|
+
const normalisedTo = this.normalisePath(to);
|
|
86
|
+
const contents = this.getFile(from);
|
|
87
|
+
if (contents === undefined) {
|
|
88
|
+
throw new Error(`File ${from} does not exist`);
|
|
89
|
+
}
|
|
90
|
+
else if (this.files[normalisedTo]) {
|
|
91
|
+
throw new Error(`File ${to} already exists`);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
this.deleteFile(from);
|
|
95
|
+
this.addFile(to, contents);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
readDir(folderPath) {
|
|
99
|
+
const path = this.normalisePath(folderPath);
|
|
100
|
+
const childrenOnDisk = this.readDirFromDisk(folderPath)
|
|
101
|
+
.map((child) => join(path, child))
|
|
102
|
+
.filter((child) => !this.files[child] || this.files[child].changeType !== 'delete');
|
|
103
|
+
const childrenAddedInContext = Object.keys(this.files).filter((p) => dirname(p) === path && this.files[p].changeType === 'add');
|
|
104
|
+
return [...childrenOnDisk, ...childrenAddedInContext];
|
|
105
|
+
}
|
|
106
|
+
readDirFromDisk(folderPath) {
|
|
107
|
+
const path = this.normalisePath(folderPath);
|
|
108
|
+
try {
|
|
109
|
+
return readdirSync(join(this.basePath, path));
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
normalisePath(filePath) {
|
|
116
|
+
return normalize(relative(this.basePath, join(this.basePath, filePath)));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Context } from './context.js';
|
|
2
|
+
describe('Context', () => {
|
|
3
|
+
describe('getFile', () => {
|
|
4
|
+
it('should read a file from the file system', () => {
|
|
5
|
+
const context = new Context(`${__dirname}/fixtures`);
|
|
6
|
+
const content = context.getFile('foo/bar.ts');
|
|
7
|
+
expect(content).toEqual("console.log('foo/bar.ts');\n");
|
|
8
|
+
});
|
|
9
|
+
it('should get a file that was just added to the context', () => {
|
|
10
|
+
const context = new Context();
|
|
11
|
+
context.addFile('file.txt', 'content');
|
|
12
|
+
const content = context.getFile('file.txt');
|
|
13
|
+
expect(content).toEqual('content');
|
|
14
|
+
});
|
|
15
|
+
it('should get a file that was updated in the current context', () => {
|
|
16
|
+
const context = new Context(`${__dirname}/fixtures`);
|
|
17
|
+
context.updateFile('foo/bar.ts', 'content');
|
|
18
|
+
const content = context.getFile('foo/bar.ts');
|
|
19
|
+
expect(content).toEqual('content');
|
|
20
|
+
});
|
|
21
|
+
it('should not return a file that was marked for deletion', () => {
|
|
22
|
+
const context = new Context(`${__dirname}/fixtures`);
|
|
23
|
+
context.deleteFile('foo/bar.ts');
|
|
24
|
+
const content = context.getFile('foo/bar.ts');
|
|
25
|
+
expect(content).toEqual(undefined);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
describe('addFile', () => {
|
|
29
|
+
it('should add a file to the context', () => {
|
|
30
|
+
const context = new Context();
|
|
31
|
+
context.addFile('file.txt', 'content');
|
|
32
|
+
expect(context.listChanges()).toEqual({ 'file.txt': { content: 'content', changeType: 'add' } });
|
|
33
|
+
});
|
|
34
|
+
it('should not add a file if it already exists', () => {
|
|
35
|
+
const context = new Context();
|
|
36
|
+
context.addFile('file.txt', 'content');
|
|
37
|
+
expect(() => context.addFile('file.txt', 'new content')).toThrowError('File file.txt already exists');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe('deleteFile', () => {
|
|
41
|
+
it('should delete a file from the context', () => {
|
|
42
|
+
const context = new Context();
|
|
43
|
+
context.addFile('file.txt', 'content');
|
|
44
|
+
context.deleteFile('file.txt');
|
|
45
|
+
expect(context.listChanges()).toEqual({});
|
|
46
|
+
});
|
|
47
|
+
it('should not delete a file if it does not exist', () => {
|
|
48
|
+
const context = new Context();
|
|
49
|
+
expect(() => context.deleteFile('file.txt')).toThrowError('File file.txt does not exist');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe('updateFile', () => {
|
|
53
|
+
it('should update a file in the context', () => {
|
|
54
|
+
const context = new Context();
|
|
55
|
+
context.addFile('file.txt', 'content');
|
|
56
|
+
context.updateFile('file.txt', 'new content');
|
|
57
|
+
expect(context.listChanges()).toEqual({ 'file.txt': { content: 'new content', changeType: 'update' } });
|
|
58
|
+
});
|
|
59
|
+
it('should not update a file if it does not exist', () => {
|
|
60
|
+
const context = new Context();
|
|
61
|
+
expect(() => context.updateFile('file.txt', 'new content')).toThrowError('File file.txt does not exist');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
describe('renameFile', () => {
|
|
65
|
+
it('should rename a file', () => {
|
|
66
|
+
const context = new Context(`${__dirname}/fixtures`);
|
|
67
|
+
context.renameFile('foo/bar.ts', 'new-file.txt');
|
|
68
|
+
expect(context.listChanges()).toEqual({
|
|
69
|
+
'new-file.txt': { content: "console.log('foo/bar.ts');\n", changeType: 'add' },
|
|
70
|
+
'foo/bar.ts': { changeType: 'delete' },
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
it('should not rename a file if it does not exist', () => {
|
|
74
|
+
const context = new Context();
|
|
75
|
+
expect(() => context.renameFile('file.txt', 'new-file.txt')).toThrowError('File file.txt does not exist');
|
|
76
|
+
});
|
|
77
|
+
it('should not rename a file if the new name already exists', () => {
|
|
78
|
+
const context = new Context();
|
|
79
|
+
context.addFile('file.txt', 'content');
|
|
80
|
+
context.addFile('new-file.txt', 'content');
|
|
81
|
+
expect(() => context.renameFile('file.txt', 'new-file.txt')).toThrowError('File new-file.txt already exists');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
describe('readDir', () => {
|
|
85
|
+
it('should read the directory', () => {
|
|
86
|
+
const context = new Context(`${__dirname}/fixtures`);
|
|
87
|
+
const files = context.readDir('foo');
|
|
88
|
+
expect(files).toEqual(['foo/bar.ts', 'foo/baz.ts']);
|
|
89
|
+
});
|
|
90
|
+
it('should filter out deleted files', () => {
|
|
91
|
+
const context = new Context(`${__dirname}/fixtures`);
|
|
92
|
+
context.deleteFile('foo/bar.ts');
|
|
93
|
+
const files = context.readDir('foo');
|
|
94
|
+
expect(files).toEqual(['foo/baz.ts']);
|
|
95
|
+
});
|
|
96
|
+
it('should include files that are only added to the context', () => {
|
|
97
|
+
const context = new Context(`${__dirname}/fixtures`);
|
|
98
|
+
context.addFile('foo/foo.txt', '');
|
|
99
|
+
const files = context.readDir('foo');
|
|
100
|
+
expect(files).toEqual(['foo/bar.ts', 'foo/baz.ts', 'foo/foo.txt']);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe('normalisePath', () => {
|
|
104
|
+
it('should normalise the path', () => {
|
|
105
|
+
const context = new Context(`${__dirname}/fixtures`);
|
|
106
|
+
expect(context.normalisePath('foo/bar.ts')).toEqual('foo/bar.ts');
|
|
107
|
+
expect(context.normalisePath('./foo/bar.ts')).toEqual('foo/bar.ts');
|
|
108
|
+
expect(context.normalisePath('/foo/bar.ts')).toEqual('foo/bar.ts');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe('hasChanges', () => {
|
|
112
|
+
it('should return FALSE if the context has no changes', () => {
|
|
113
|
+
const context = new Context(`${__dirname}/fixtures`);
|
|
114
|
+
expect(context.hasChanges()).toEqual(false);
|
|
115
|
+
});
|
|
116
|
+
it('should return TRUE if the context has changes', () => {
|
|
117
|
+
const context = new Context(`${__dirname}/fixtures`);
|
|
118
|
+
context.addFile('foo.ts', '');
|
|
119
|
+
expect(context.hasChanges()).toEqual(true);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
migrations: {
|
|
3
|
+
'migration-key1': {
|
|
4
|
+
version: '5.0.0',
|
|
5
|
+
description: 'Update project to use new cache directory',
|
|
6
|
+
migrationScript: './5-0-0-cache-directory.js',
|
|
7
|
+
},
|
|
8
|
+
'migration-key2': {
|
|
9
|
+
version: '5.4.0',
|
|
10
|
+
description: 'Update project to use new cache directory',
|
|
11
|
+
migrationScript: './5-4-0-cache-directory.js',
|
|
12
|
+
},
|
|
13
|
+
'migration-key3': {
|
|
14
|
+
version: '6.0.0',
|
|
15
|
+
description: 'Update project to use new cache directory',
|
|
16
|
+
migrationScript: './5-4-0-cache-directory.js',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { satisfies, gte } from 'semver';
|
|
2
|
+
import { Context } from './context.js';
|
|
3
|
+
import defaultMigrations 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
|
+
export function getMigrationsToRun(fromVersion, toVersion, migrations = defaultMigrations.migrations) {
|
|
8
|
+
const semverRange = `${fromVersion} - ${toVersion}`;
|
|
9
|
+
const migrationsToRun = Object.entries(migrations)
|
|
10
|
+
.sort((a, b) => {
|
|
11
|
+
return gte(a[1].version, b[1].version) ? 1 : -1;
|
|
12
|
+
})
|
|
13
|
+
.reduce((acc, [key, meta]) => {
|
|
14
|
+
if (satisfies(meta.version, semverRange)) {
|
|
15
|
+
acc[key] = meta;
|
|
16
|
+
}
|
|
17
|
+
return acc;
|
|
18
|
+
}, {});
|
|
19
|
+
return migrationsToRun;
|
|
20
|
+
}
|
|
21
|
+
export async function runMigrations(migrations, options = {}) {
|
|
22
|
+
const basePath = process.cwd();
|
|
23
|
+
console.log('');
|
|
24
|
+
console.log('Running the following migrations:');
|
|
25
|
+
Object.entries(migrations).map(([key, migrationMeta]) => console.log(`- ${key} (${migrationMeta.description})`));
|
|
26
|
+
console.log('');
|
|
27
|
+
for (const [key, migration] of Object.entries(migrations)) {
|
|
28
|
+
try {
|
|
29
|
+
const context = await runMigration(migration, new Context(basePath));
|
|
30
|
+
const shouldCommit = options.commitEachMigration && context.hasChanges();
|
|
31
|
+
migrationsDebug(`context for "${key} (${migration.migrationScript})":`);
|
|
32
|
+
migrationsDebug('%O', context.listChanges());
|
|
33
|
+
flushChanges(context);
|
|
34
|
+
printChanges(context, key, migration);
|
|
35
|
+
if (shouldCommit) {
|
|
36
|
+
await gitCommitNoVerify(`chore: run create-plugin migration - ${key} (${migration.migrationScript})`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof Error) {
|
|
41
|
+
throw new Error(`Error running migration "${key} (${migration.migrationScript})": ${error.message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
setRootConfig({ version: Object.values(migrations).at(-1).version });
|
|
46
|
+
}
|
|
47
|
+
export async function runMigration(migration, context) {
|
|
48
|
+
const module = await import(migration.migrationScript);
|
|
49
|
+
return module.default(context);
|
|
50
|
+
}
|