mirror of https://github.com/grafana/grafana
Toolkit: Remove deprecated `plugin:build` (#67485)
Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com>pull/67507/head
parent
ea7e5e2d82
commit
50fb1497e5
@ -1,3 +1,5 @@ |
||||
module.exports = { |
||||
...require('@grafana/toolkit/src/config/prettier.plugin.config.json'), |
||||
trailingComma: 'es5', |
||||
singleQuote: true, |
||||
printWidth: 120, |
||||
}; |
||||
|
@ -1,381 +1,15 @@ |
||||
> **WARNING: @grafana/toolkit is currently in BETA**. |
||||
> **WARNING: @grafana/toolkit is deprecated**. |
||||
|
||||
# grafana-toolkit |
||||
|
||||
grafana-toolkit is a CLI that enables efficient development of Grafana plugins. We want to help our community focus on the core value of their plugins rather than all the setup required to develop them. |
||||
|
||||
## Getting started |
||||
|
||||
Set up a new plugin with `grafana-toolkit plugin:create` command: |
||||
|
||||
```sh |
||||
npx @grafana/toolkit plugin:create my-grafana-plugin |
||||
cd my-grafana-plugin |
||||
yarn install |
||||
yarn dev |
||||
``` |
||||
|
||||
> **Note:** If running NPM 7+ the `npx` commands mentioned in this article may hang. The workaround is to use `npx --legacy-peer-deps <command to run>`. |
||||
|
||||
## Update your plugin to use grafana-toolkit |
||||
|
||||
Follow the steps below to start using grafana-toolkit in your existing plugin. |
||||
|
||||
1. Add `@grafana/toolkit` package to your project by running `yarn add @grafana/toolkit` or `npm install @grafana/toolkit`. |
||||
2. Create `tsconfig.json` file in the root dir of your plugin and paste the code below: |
||||
|
||||
```json |
||||
{ |
||||
"extends": "./node_modules/@grafana/toolkit/src/config/tsconfig.plugin.json", |
||||
"include": ["src", "types"], |
||||
"compilerOptions": { |
||||
"rootDir": "./src", |
||||
"baseUrl": "./src", |
||||
"typeRoots": ["./node_modules/@types"] |
||||
} |
||||
} |
||||
``` |
||||
|
||||
3. Create `.prettierrc.js` file in the root dir of your plugin and paste the code below: |
||||
|
||||
```js |
||||
module.exports = { |
||||
...require('./node_modules/@grafana/toolkit/src/config/prettier.plugin.config.json'), |
||||
}; |
||||
``` |
||||
|
||||
4. In your `package.json` file add following scripts: |
||||
|
||||
```json |
||||
"scripts": { |
||||
"build": "grafana-toolkit plugin:build", |
||||
"test": "grafana-toolkit plugin:test", |
||||
"dev": "grafana-toolkit plugin:dev", |
||||
"watch": "grafana-toolkit plugin:dev --watch" |
||||
}, |
||||
``` |
||||
|
||||
## Usage |
||||
|
||||
With grafana-toolkit, we give you a CLI that addresses common tasks performed when working on Grafana plugin: |
||||
|
||||
- `grafana-toolkit plugin:create` |
||||
- `grafana-toolkit plugin:dev` |
||||
- `grafana-toolkit plugin:test` |
||||
- `grafana-toolkit plugin:build` |
||||
- `grafana-toolkit plugin:sign` |
||||
|
||||
### Create your plugin |
||||
|
||||
`grafana-toolkit plugin:create plugin-name` |
||||
|
||||
This command creates a new Grafana plugin from template. |
||||
|
||||
If `plugin-name` is provided, then the template is downloaded to `./plugin-name` directory. Otherwise, it will be downloaded to the current directory. |
||||
|
||||
### Develop your plugin |
||||
|
||||
`grafana-toolkit plugin:dev` |
||||
|
||||
This command creates a development build that's easy to play with and debug using common browser tooling. |
||||
|
||||
Available options: |
||||
|
||||
- `-w`, `--watch` - run development task in a watch mode |
||||
|
||||
### Test your plugin |
||||
|
||||
`grafana-toolkit plugin:test` |
||||
|
||||
This command runs Jest against your codebase. |
||||
|
||||
Available options: |
||||
|
||||
- `--watch` - Runs tests in interactive watch mode. |
||||
- `--coverage` - Reports code coverage. |
||||
- `-u`, `--updateSnapshot` - Performs snapshots update. |
||||
- `--testNamePattern=<regex>` - Runs test with names that match provided regex (https://jestjs.io/docs/en/cli#testnamepattern-regex). |
||||
- `--testPathPattern=<regex>` - Runs test with paths that match provided regex (https://jestjs.io/docs/en/cli#testpathpattern-regex). |
||||
- `--maxWorkers=<num>|<string>` - Limit number of Jest workers spawned (https://jestjs.io/docs/en/cli#--maxworkersnumstring) |
||||
|
||||
### Build your plugin |
||||
|
||||
`grafana-toolkit plugin:build` |
||||
|
||||
This command creates a production-ready build of your plugin. |
||||
|
||||
Available options: |
||||
|
||||
- `--skipTest` - Skip running tests as part of build. Useful if you're running the build as part of a larger pipeline. |
||||
- `--skipLint` - Skip linting as part of build. Useful if you're running the build as part of a larger pipeline. |
||||
- `--coverage` - Reports code coverage after the test step of the build. |
||||
- `--preserveConsole` - Preserves console statements in the code. |
||||
|
||||
### Sign your plugin |
||||
|
||||
`grafana-toolkit plugin:sign` |
||||
|
||||
This command creates a signed MANIFEST.txt file which Grafana uses to validate the integrity of the plugin. |
||||
|
||||
Available options: |
||||
|
||||
- `--signatureType` - The [type of Signature](https://grafana.com/legal/plugins/) you are generating: `private`, `community` or `commercial` |
||||
- `--rootUrls` - For private signatures, a list of the Grafana instance URLs that the plugin will be used on |
||||
|
||||
To generate a signature, you will need to sign up for a free account on https://grafana.com, create an API key with the Plugin Publisher role, and pass that in the `GRAFANA_API_KEY` environment variable. |
||||
|
||||
## FAQ |
||||
|
||||
### Which version of grafana-toolkit should I use? |
||||
|
||||
See [Grafana packages versioning guide](https://github.com/grafana/grafana/blob/main/packages/README.md#versioning). |
||||
|
||||
### What tools does grafana-toolkit use? |
||||
|
||||
grafana-toolkit comes with TypeScript, ESLint, Prettier, Jest, CSS and SASS support. |
||||
|
||||
### How to start using grafana-toolkit in my plugin? |
||||
|
||||
See [Updating your plugin to use grafana-toolkit](#updating-your-plugin-to-use-grafana-toolkit). |
||||
|
||||
### Can I use TypeScript to develop Grafana plugins? |
||||
|
||||
Yes! grafana-toolkit supports TypeScript by default. |
||||
|
||||
### How can I test my plugin? |
||||
|
||||
grafana-toolkit comes with Jest as a test runner. |
||||
|
||||
Internally at Grafana we use React Testing Library. |
||||
|
||||
You can also set up Jest with shims of your needs by creating `jest-shim.ts` file in the same directory: `<YOUR_PLUGIN_DIR_>/config/jest-shim.ts` |
||||
|
||||
### Can I provide custom setup for Jest? |
||||
|
||||
You can provide custom Jest configuration with a `package.json` file. For more details, see [Jest docs](https://jest-bot.github.io/jest/docs/configuration.html). |
||||
|
||||
Currently we support following Jest configuration properties: |
||||
|
||||
- [`snapshotSerializers`](https://jest-bot.github.io/jest/docs/configuration.html#snapshotserializers-array-string) |
||||
- [`moduleNameMapper`](https://jestjs.io/docs/en/configuration#modulenamemapper-object-string-string) |
||||
|
||||
### I want to use monaco-editor, is there anything particular I should pay attention? |
||||
|
||||
Yes there is. |
||||
We already bundled monaco-editor in grafana. See [webpack config](https://github. |
||||
com/grafana/grafana/blob/main/scripts/webpack/webpack.common.js#L49-L58). |
||||
We use `@monaco-editor/react` package with loader-config https://github.com/suren-atoyan/monaco-react#loader-config |
||||
By default, monaco files are being downloaded from CDN. We use the bundled version. |
||||
Initialization: https://github.com/grafana/grafana/blob/main/scripts/webpack/webpack.common.js#L49-L58t |
||||
We suggest to use the bundled version to prevent unwanted issues related to version mismatch. |
||||
|
||||
### How can I customize Webpack rules or plugins? |
||||
|
||||
You can provide your own `webpack.config.js` file that exports a `getWebpackConfig` function. We recommend that you extend the standard configuration, but you are free to create your own: |
||||
|
||||
```js |
||||
const CustomPlugin = require('custom-plugin'); |
||||
|
||||
module.exports.getWebpackConfig = (config, options) => ({ |
||||
...config, |
||||
plugins: [...config.plugins, new CustomPlugin()], |
||||
}); |
||||
``` |
||||
|
||||
### How can I style my plugin? |
||||
|
||||
We support pure CSS, SASS, and CSS-in-JS approach (via [Emotion](https://emotion.sh/)). |
||||
|
||||
#### Single CSS or SASS file |
||||
|
||||
Create your CSS or SASS file and import it in your plugin entry point (typically `module.ts`): |
||||
|
||||
```ts |
||||
import 'path/to/your/css_or_sass'; |
||||
``` |
||||
|
||||
The styles will be injected via `style` tag during runtime. |
||||
|
||||
> **Note:** that imported static assets will be inlined as base64 URIs. _This can be subject of change in the future!_ |
||||
|
||||
#### Theme-specific stylesheets |
||||
|
||||
If you want to provide different stylesheets for dark/light theme, then create `dark.[css|scss]` and `light.[css|scss]` files in the `src/styles` directory of your plugin. grafana-toolkit generates theme-specific stylesheets that are stored in `dist/styles` directory. |
||||
|
||||
In order for Grafana to pick up your theme stylesheets, you need to use `loadPluginCss` from `@grafana/runtime` package. Typically you would do that in the entry point of your plugin: |
||||
|
||||
```ts |
||||
import { loadPluginCss } from '@grafana/runtime'; |
||||
|
||||
loadPluginCss({ |
||||
dark: 'plugins/<YOUR-PLUGIN-ID>/styles/dark.css', |
||||
light: 'plugins/<YOUR-PLUGIN-ID>/styles/light.css', |
||||
}); |
||||
``` |
||||
|
||||
You must add `@grafana/runtime` to your plugin dependencies by running `yarn add @grafana/runtime` or `npm install @grafana/runtime`. |
||||
|
||||
> **Note:** that in this case static files (png, svg, json, html) are all copied to dist directory when the plugin is bundled. Relative paths to those files does not change! |
||||
|
||||
#### Emotion |
||||
|
||||
Starting from Grafana 6.2 _our suggested way_ for styling plugins is by using [Emotion](https://emotion.sh). It's a CSS-in-JS library that we use internally at Grafana. The biggest advantage of using Emotion is that you can access Grafana Theme variables. |
||||
|
||||
To start using Emotion, you first must add it to your plugin dependencies: |
||||
|
||||
``` |
||||
yarn add "emotion"@10.0.27 |
||||
``` |
||||
|
||||
Then, import `css` function from Emotion: |
||||
|
||||
```ts |
||||
import { css } from 'emotion'; |
||||
``` |
||||
|
||||
Now you are ready to implement your styles: |
||||
|
||||
```tsx |
||||
const MyComponent = () => { |
||||
return ( |
||||
<div |
||||
className={css` |
||||
background: red; |
||||
`} |
||||
/> |
||||
); |
||||
}; |
||||
``` |
||||
|
||||
To learn more about using Grafana theme please refer to [Theme usage guide](https://github.com/grafana/grafana/blob/main/style_guides/themes.md#react) |
||||
|
||||
> We do not support Emotion's `css` prop. Use className instead! |
||||
|
||||
### Can I adjust TypeScript configuration to suit my needs? |
||||
|
||||
Yes! However, it's important that your `tsconfig.json` file contains the following lines: |
||||
|
||||
```json |
||||
{ |
||||
"extends": "./node_modules/@grafana/toolkit/src/config/tsconfig.plugin.json", |
||||
"include": ["src"], |
||||
"compilerOptions": { |
||||
"rootDir": "./src", |
||||
"typeRoots": ["./node_modules/@types"] |
||||
} |
||||
} |
||||
``` |
||||
|
||||
### Can I adjust ESLint configuration to suit my needs? |
||||
|
||||
grafana-toolkit comes with [default config for ESLint](https://github.com/grafana/grafana/blob/main/packages/grafana-toolkit/src/config/eslint.plugin.json). For now, there is no way to customise ESLint config. |
||||
|
||||
### How is Prettier integrated into grafana-toolkit workflow? |
||||
|
||||
When building plugin with [`grafana-toolkit plugin:build`](#building-plugin) task, grafana-toolkit performs Prettier check. If the check detects any Prettier issues, the build will not pass. To avoid such situation we suggest developing plugin with [`grafana-toolkit plugin:dev --watch`](#developing-plugin) task running. This task tries to fix Prettier issues automatically. |
||||
|
||||
### My editor does not respect Prettier config, what should I do? |
||||
|
||||
In order for your editor to pick up our Prettier config you need to create `.prettierrc.js` file in the root directory of your plugin with following content: |
||||
|
||||
```js |
||||
module.exports = { |
||||
...require('./node_modules/@grafana/toolkit/src/config/prettier.plugin.config.json'), |
||||
}; |
||||
``` |
||||
|
||||
### How do I add third-party dependencies that are not npm packages? |
||||
|
||||
Put them in the `static` directory in the root of your project. The `static` directory is copied when the plugin is built. |
||||
|
||||
### I am getting this message when I run yarn install: `Request failed \"404 Not Found\"` |
||||
|
||||
If you are using version `canary`, this error occurs because a `canary` release unpublishes previous versions leaving `yarn.lock` outdated. Remove `yarn.lock` and run `yarn install` again. |
||||
|
||||
### I am getting this message when I run my plugin: `Unable to dynamically transpile ES module A loader plugin needs to be configured via SystemJS.config({ transpiler: 'transpiler-module' }).` |
||||
|
||||
This error occurs when you bundle your plugin using the `grafana-toolkit plugin:dev` task and your code comments include ES2016 code. |
||||
|
||||
There are two issues at play: |
||||
|
||||
- The `grafana-toolkit plugin:dev` task does not remove comments from your bundled package. |
||||
- Grafana does not support [ES modules](https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/). |
||||
|
||||
If your comments include ES2016 code, then SystemJS v0.20.19, which Grafana uses internally to load plugins, interprets your code as an ESM and fails. |
||||
|
||||
To fix this error, remove the ES2016 code from your comments. |
||||
|
||||
### I would like to dynamically import modules in my plugin |
||||
|
||||
Create a webpack.config.js with this content (in the root of _your_ plugin) |
||||
|
||||
```ts |
||||
// webpack.config.js |
||||
const pluginJson = require('./src/plugin.json'); |
||||
module.exports.getWebpackConfig = (config, options) => ({ |
||||
...config, |
||||
output: { |
||||
...config.output, |
||||
publicPath: `public/plugins/${pluginJson.id}/`, |
||||
}, |
||||
}); |
||||
``` |
||||
|
||||
The plugin id is the id written in the plugin.json file. |
||||
You should not use grafana-toolkit anymore. [create-plugin](https://github.com/grafana/plugin-tools/tree/main/packages/create-plugin/) is the tool to create new plugins and existing plugins should migrate. |
||||
|
||||
## Contribute to grafana-toolkit |
||||
|
||||
You can contribute to grafana-toolkit by helping develop it or by debugging it. |
||||
|
||||
### Develop grafana-toolkit |
||||
|
||||
Typically plugins should be developed using the `@grafana/toolkit` installed from npm. However, when working on the toolkit, you might want to use the local version. There are two ways to run the local toolkit version: |
||||
|
||||
### Using `NODE_OPTIONS` to load the yarn pnp file: |
||||
|
||||
You can run grafana toolkit directly from the grafana repository with this command. |
||||
|
||||
`NODE_OPTIONS="--require $GRAFANA_REPO/.pnp.cjs" $GRAFANA_REPO/packages/grafana-toolkit/dist/bin/grafana-toolkit.js [command]` |
||||
|
||||
You can replace `$GRAFANA_REPO` with the path to your grafana clone or set it in your environment. e.g.: `export GRAFANA_REPO=/home/dev/your_grafana_clone` |
||||
|
||||
> Note: This will run grafana toolkit from your local clone but it won't change the dependencies of what you are trying to build. |
||||
|
||||
### Using yarn berry linking. |
||||
|
||||
#### Prerequisites |
||||
|
||||
For the following to work make sure to have a plugin that uses yarn berry and yarn PnP to test changes against. Feel free to clone the panel plugin [here](https://github.com/jackw/plugin-yarn-berry) if need be. |
||||
|
||||
1. Clone [Grafana repository](https://github.com/grafana/grafana). |
||||
2. Navigate to the directory you have cloned Grafana repo to and then run `yarn install --immutable`. |
||||
3. Open `packages/grafana-toolkit/package.json`, delete any mention of `@grafana/data` and `@grafana/ui`. Then add the following: |
||||
|
||||
``` |
||||
"devDependencies": { |
||||
"@grafana/data": "workspace:*", |
||||
"@grafana/ui": "workspace:*" |
||||
}, |
||||
"peerDependencies": { |
||||
"@grafana/data": "*", |
||||
"@grafana/ui": "*" |
||||
} |
||||
``` |
||||
|
||||
**DO NOT commit this `package.json` code change. It is required to resolve these `@grafana` packages from the plugin for development purposes only.** |
||||
|
||||
4. Run `yarn` in the grafana repo. |
||||
5. Navigate to the directory where your plugin code is and then run `yarn link <GRAFANA_DIR>/packages/grafana-toolkit`. This uses yarn berry's `portal` protocol to "link" the grafana-toolkit package and resolve it's dependencies into your plugin code allowing you to develop toolkit and test changes against plugin code. |
||||
6. Make the required changes to Grafana toolkit. |
||||
|
||||
### Debug grafana-toolkit |
||||
|
||||
To debug grafana-toolkit you can use standard [NodeJS debugging methods](https://nodejs.org/de/docs/guides/debugging-getting-started/#enable-inspector) (`node --inspect`, `node --inspect-brk`). |
||||
|
||||
To run grafana-toolkit in a debugging session use the following command in the toolkit's directory: |
||||
|
||||
`node --inspect-brk ./bin/grafana-toolkit.js [task]` |
||||
|
||||
To run [linked](#develop-grafana-toolkit) grafana-toolkit in a debugging session use the following command in the plugin's directory: |
||||
|
||||
`node --inspect-brk ./node_modules/@grafana/toolkit/bin/grafana-toolkit.js [task]` |
||||
Grafana Toolkit is now deprecated. |
||||
|
@ -1,38 +0,0 @@ |
||||
const fs = require('fs'); |
||||
const path = require('path'); |
||||
|
||||
function copyFiles(files, cwd, distDir) { |
||||
for (const file of files) { |
||||
const basedir = path.dirname(`${distDir}/${file}`); |
||||
const name = file.replace('.generated', ''); |
||||
if (!fs.existsSync(basedir)) { |
||||
fs.mkdirSync(basedir, { recursive: true }); |
||||
} |
||||
fs.copyFileSync(`${cwd}/${file}`, `${distDir}/${name}`); |
||||
} |
||||
} |
||||
const configFilesToCopy = [ |
||||
'src/config/prettier.plugin.config.json', |
||||
'src/config/prettier.plugin.rc.js', |
||||
'src/config/tsconfig.plugin.json', |
||||
'src/config/tsconfig.plugin.local.json', |
||||
'src/config/eslint.plugin.js', |
||||
'src/config/styles.mock.js', |
||||
'src/config/jest.babel.config.js', |
||||
'src/config/jest.plugin.config.local.js', |
||||
'src/config/matchMedia.js', |
||||
'src/config/react-inlinesvg.tsx', |
||||
]; |
||||
const sassFilesToCopy = [ |
||||
'_variables.generated.scss', |
||||
'_variables.dark.generated.scss', |
||||
'_variables.light.generated.scss', |
||||
]; |
||||
|
||||
const cwd = path.resolve(__dirname, '../'); |
||||
const distPath = path.resolve(cwd, 'dist'); |
||||
const sassPath = path.resolve(cwd, 'sass'); |
||||
const grafanaSassPath = path.resolve(cwd, '../../public/sass'); |
||||
|
||||
copyFiles(configFilesToCopy, cwd, distPath); |
||||
copyFiles(sassFilesToCopy, grafanaSassPath, sassPath); |
@ -1,161 +0,0 @@ |
||||
import { ESLint } from 'eslint'; |
||||
import execa from 'execa'; |
||||
import { constants as fsConstants, promises as fs } from 'fs'; |
||||
import globby from 'globby'; |
||||
import { resolve as resolvePath } from 'path'; |
||||
import rimrafCallback from 'rimraf'; |
||||
import { promisify } from 'util'; |
||||
|
||||
import { useSpinner } from '../utils/useSpinner'; |
||||
|
||||
import { bundlePlugin as bundleFn, PluginBundleOptions } from './plugin/bundle'; |
||||
import { testPlugin } from './plugin/tests'; |
||||
import { Task, TaskRunner } from './task'; |
||||
|
||||
const { access, copyFile } = fs; |
||||
const { COPYFILE_EXCL } = fsConstants; |
||||
const rimraf = promisify(rimrafCallback); |
||||
|
||||
interface PluginBuildOptions { |
||||
coverage: boolean; |
||||
maxJestWorkers?: string; |
||||
preserveConsole?: boolean; |
||||
skipTest?: boolean; |
||||
skipLint?: boolean; |
||||
} |
||||
|
||||
interface Fixable { |
||||
fix?: boolean; |
||||
} |
||||
|
||||
const bundlePlugin = (options: PluginBundleOptions) => useSpinner('Compiling...', () => bundleFn(options)); |
||||
|
||||
// @ts-ignore
|
||||
const clean = () => useSpinner('Cleaning', () => rimraf(`${process.cwd()}/dist`)); |
||||
|
||||
const copyIfNonExistent = (srcPath: string, destPath: string) => |
||||
copyFile(srcPath, destPath, COPYFILE_EXCL) |
||||
.then(() => console.log(`Created: ${destPath}`)) |
||||
.catch((error) => { |
||||
if (error.code !== 'EEXIST') { |
||||
throw error; |
||||
} |
||||
}); |
||||
|
||||
export const prepare = () => |
||||
useSpinner('Preparing', () => |
||||
Promise.all([ |
||||
// Remove local dependencies for @grafana/data/node_modules
|
||||
// See: https://github.com/grafana/grafana/issues/26748
|
||||
rimraf(resolvePath(__dirname, 'node_modules/@grafana/data/node_modules')), |
||||
// Copy only if local tsconfig does not exist. Otherwise this will work, but have odd behavior
|
||||
copyIfNonExistent( |
||||
resolvePath(__dirname, '../../config/tsconfig.plugin.local.json'), |
||||
resolvePath(process.cwd(), 'tsconfig.json') |
||||
), |
||||
// Copy only if local prettierrc does not exist. Otherwise this will work, but have odd behavior
|
||||
copyIfNonExistent( |
||||
resolvePath(__dirname, '../../config/prettier.plugin.rc.js'), |
||||
resolvePath(process.cwd(), '.prettierrc.js') |
||||
), |
||||
]) |
||||
); |
||||
|
||||
export const versions = async () => { |
||||
try { |
||||
const nodeVersion = await execa('node', ['--version']); |
||||
console.log(`Using Node.js ${nodeVersion.stdout}`); |
||||
|
||||
const toolkitVersion = await execa('grafana-toolkit', ['--version']); |
||||
console.log(`Using @grafana/toolkit ${toolkitVersion.stdout}`); |
||||
} catch (err) { |
||||
console.log(`Error reading versions`, err); |
||||
} |
||||
}; |
||||
|
||||
// @ts-ignore
|
||||
const typecheckPlugin = () => useSpinner('Typechecking', () => execa('tsc', ['--noEmit'])); |
||||
|
||||
// @ts-ignore
|
||||
const getStylesSources = () => globby(resolvePath(process.cwd(), 'src/**/*.+(scss|css)')); |
||||
|
||||
export const lintPlugin = ({ fix }: Fixable = {}) => |
||||
useSpinner('Linting', async () => { |
||||
try { |
||||
// Show a warning if the tslint file exists
|
||||
await access(resolvePath(process.cwd(), 'tslint.json')); |
||||
console.log('\n'); |
||||
console.log('--------------------------------------------------------------'); |
||||
console.log('NOTE: @grafana/toolkit has migrated to use eslint'); |
||||
console.log('Update your configs to use .eslintrc rather than tslint.json'); |
||||
console.log('--------------------------------------------------------------'); |
||||
} catch { |
||||
// OK: tslint does not exist
|
||||
} |
||||
|
||||
// @todo should remove this because the config file could be in a parent dir or within package.json
|
||||
const configFile = await globby(resolvePath(process.cwd(), '.eslintrc?(.cjs|.js|.json|.yaml|.yml)')).then( |
||||
(filePaths) => { |
||||
if (filePaths.length > 0) { |
||||
return filePaths[0]; |
||||
} else { |
||||
return resolvePath(__dirname, '../../config/eslint.plugin.js'); |
||||
} |
||||
} |
||||
); |
||||
|
||||
const eslint = new ESLint({ |
||||
extensions: ['.ts', '.tsx'], |
||||
overrideConfigFile: configFile, |
||||
fix, |
||||
useEslintrc: false, |
||||
}); |
||||
|
||||
const results = await eslint.lintFiles(resolvePath(process.cwd(), 'src')); |
||||
|
||||
if (fix) { |
||||
await ESLint.outputFixes(results); |
||||
} |
||||
|
||||
const { errorCount, warningCount } = results.reduce<Record<string, number>>( |
||||
(acc, value) => { |
||||
acc.errorCount += value.errorCount; |
||||
acc.warningCount += value.warningCount; |
||||
return acc; |
||||
}, |
||||
{ errorCount: 0, warningCount: 0 } |
||||
); |
||||
|
||||
const formatter = await eslint.loadFormatter('stylish'); |
||||
const resultText = formatter.format(results); |
||||
|
||||
if (errorCount > 0 || warningCount > 0) { |
||||
console.log('\n'); |
||||
console.log(resultText); |
||||
console.log('\n'); |
||||
} |
||||
|
||||
if (errorCount > 0) { |
||||
throw new Error(`${errorCount} linting errors found in ${results.length} files`); |
||||
} |
||||
}); |
||||
|
||||
export const pluginBuildRunner: TaskRunner<PluginBuildOptions> = async ({ |
||||
coverage, |
||||
maxJestWorkers, |
||||
preserveConsole, |
||||
skipTest, |
||||
skipLint, |
||||
}) => { |
||||
await versions(); |
||||
await prepare(); |
||||
if (!skipLint) { |
||||
await lintPlugin({ fix: false }); |
||||
} |
||||
if (!skipTest) { |
||||
await testPlugin({ updateSnapshot: false, coverage, maxWorkers: maxJestWorkers, watch: false }); |
||||
} |
||||
await bundlePlugin({ watch: false, production: true, preserveConsole }); |
||||
}; |
||||
|
||||
export const pluginBuildTask = new Task<PluginBuildOptions>('Build plugin', pluginBuildRunner); |
@ -1,79 +0,0 @@ |
||||
import clearConsole from 'react-dev-utils/clearConsole'; |
||||
import formatWebpackMessages from 'react-dev-utils/formatWebpackMessages'; |
||||
import webpack from 'webpack'; |
||||
|
||||
import { loadWebpackConfig } from '../../../config/webpack.plugin.config'; |
||||
|
||||
export interface PluginBundleOptions { |
||||
watch: boolean; |
||||
production?: boolean; |
||||
preserveConsole?: boolean; |
||||
} |
||||
|
||||
export const bundlePlugin = async ({ watch, production, preserveConsole }: PluginBundleOptions) => { |
||||
const compiler = webpack( |
||||
await loadWebpackConfig({ |
||||
watch, |
||||
production, |
||||
preserveConsole, |
||||
}) |
||||
); |
||||
|
||||
const webpackPromise = new Promise<void>((resolve, reject) => { |
||||
if (watch) { |
||||
console.log('Started watching plugin for changes...'); |
||||
compiler.watch({ ignored: ['**/node_modules', '**/dist'] }, (err, stats) => {}); |
||||
|
||||
compiler.hooks.invalid.tap('invalid', () => { |
||||
clearConsole(); |
||||
console.log('Compiling...'); |
||||
}); |
||||
|
||||
compiler.hooks.done.tap('done', (stats) => { |
||||
clearConsole(); |
||||
const json = stats.toJson(); |
||||
const output = formatWebpackMessages(json); |
||||
|
||||
if (!output.errors.length && !output.warnings.length) { |
||||
console.log('Compiled successfully!\n'); |
||||
console.log(stats.toString({ colors: true })); |
||||
} |
||||
|
||||
if (output.errors.length) { |
||||
console.log('Compilation failed!'); |
||||
output.errors.forEach((e) => console.log(e)); |
||||
|
||||
if (output.warnings.length) { |
||||
console.log('Warnings:'); |
||||
output.warnings.forEach((w) => console.log(w)); |
||||
} |
||||
} |
||||
if (output.errors.length === 0 && output.warnings.length) { |
||||
console.log('Compiled with warnings!'); |
||||
output.warnings.forEach((w) => console.log(w)); |
||||
} |
||||
}); |
||||
} else { |
||||
compiler.run((err, stats) => { |
||||
if (err) { |
||||
reject(err); |
||||
return; |
||||
} |
||||
|
||||
if (stats?.hasErrors()) { |
||||
stats.compilation.errors.forEach((e) => { |
||||
console.log(e.message); |
||||
}); |
||||
|
||||
reject('Build failed'); |
||||
return; |
||||
} |
||||
|
||||
console.log('\n', stats?.toString({ colors: true }), '\n'); |
||||
resolve(); |
||||
}); |
||||
} |
||||
}); |
||||
|
||||
return webpackPromise; |
||||
}; |
@ -1 +0,0 @@ |
||||
NOTE: not a real image, but should be excluded from hashed files |
@ -1,50 +0,0 @@ |
||||
import { runCLI } from '@jest/core'; |
||||
|
||||
import { loadJestPluginConfig } from '../../../config/jest.plugin.config'; |
||||
import { useSpinner } from '../../utils/useSpinner'; |
||||
|
||||
export interface PluginTestOptions { |
||||
updateSnapshot: boolean; |
||||
coverage: boolean; |
||||
watch: boolean; |
||||
testPathPattern?: string; |
||||
testNamePattern?: string; |
||||
maxWorkers?: string; |
||||
} |
||||
|
||||
export const testPlugin = ({ |
||||
updateSnapshot, |
||||
coverage, |
||||
watch, |
||||
testPathPattern, |
||||
testNamePattern, |
||||
maxWorkers, |
||||
}: PluginTestOptions) => |
||||
useSpinner('Running tests', async () => { |
||||
const testConfig = loadJestPluginConfig(); |
||||
|
||||
const cliConfig = { |
||||
config: JSON.stringify(testConfig), |
||||
updateSnapshot, |
||||
coverage, |
||||
watch, |
||||
testPathPattern: testPathPattern ? [testPathPattern] : [], |
||||
testNamePattern: testNamePattern ? [testNamePattern] : [], |
||||
passWithNoTests: true, |
||||
maxWorkers, |
||||
}; |
||||
|
||||
// @ts-ignore
|
||||
const runJest = () => runCLI(cliConfig, [process.cwd()]); |
||||
|
||||
if (watch) { |
||||
runJest(); |
||||
} else { |
||||
// @ts-ignore
|
||||
const results = await runJest(); |
||||
|
||||
if (results.results.numFailedTests > 0 || results.results.numFailedTestSuites > 0) { |
||||
throw new Error('Tests failed'); |
||||
} |
||||
} |
||||
}); |
@ -1,23 +0,0 @@ |
||||
import ora from 'ora'; |
||||
|
||||
export const useSpinner = async (label: string, fn: () => Promise<any>, killProcess = true) => { |
||||
const spinner = ora(label); |
||||
spinner.start(); |
||||
try { |
||||
await fn(); |
||||
spinner.succeed(); |
||||
} catch (err: any) { |
||||
spinner.fail(err.message || err); |
||||
|
||||
if (err.stdout) { |
||||
console.error(err.stdout); |
||||
} else if (err.message) { |
||||
// Return stack trace if error object
|
||||
console.trace(err); // eslint-disable-line no-console
|
||||
} |
||||
|
||||
if (killProcess) { |
||||
process.exit(1); |
||||
} |
||||
} |
||||
}; |
@ -1,6 +0,0 @@ |
||||
module.exports = { |
||||
extends: ['@grafana/eslint-config'], |
||||
rules: { |
||||
'react/prop-types': 'off', |
||||
}, |
||||
}; |
@ -1 +0,0 @@ |
||||
export { CustomWebpackConfigurationGetter, WebpackConfigurationOptions } from './webpack.plugin.config'; |
@ -1,2 +0,0 @@ |
||||
// Transform es modules to prevent `SyntaxError: Cannot use import statement outside a module`
|
||||
module.exports = { presets: [['@babel/preset-env', { targets: { esmodules: false, node: 'current' } }]] }; |
@ -1,8 +0,0 @@ |
||||
// This file is needed because it is used by vscode and other tools that
|
||||
// call `jest` directly. However, unless you are doing anything special
|
||||
// do not edit this file
|
||||
|
||||
const standard = require('@grafana/toolkit/src/config/jest.plugin.config'); |
||||
|
||||
// This process will use the same config that `yarn test` is using
|
||||
module.exports = standard.jestConfig(); |
@ -1,40 +0,0 @@ |
||||
import { jestConfig, allowedJestConfigOverrides } from './jest.plugin.config'; |
||||
|
||||
describe('Jest config', () => { |
||||
afterEach(() => { |
||||
jest.restoreAllMocks(); |
||||
}); |
||||
|
||||
it('should throw if not supported overrides provided', () => { |
||||
// Do not show console error,log when running test
|
||||
jest.spyOn(console, 'error').mockImplementation(); |
||||
jest.spyOn(console, 'log').mockImplementation(); |
||||
const getConfig = () => jestConfig(`${__dirname}/mocks/jestSetup/unsupportedOverrides`); |
||||
|
||||
expect(getConfig).toThrow('Provided Jest config is not supported'); |
||||
}); |
||||
|
||||
it(`should allow ${allowedJestConfigOverrides} settings overrides`, () => { |
||||
const config = jestConfig(`${__dirname}/mocks/jestSetup/overrides`); |
||||
const configKeys = Object.keys(config); |
||||
|
||||
for (const whitelistedOption of allowedJestConfigOverrides) { |
||||
expect(configKeys).toContain(whitelistedOption); |
||||
} |
||||
}); |
||||
|
||||
describe('stylesheets support', () => { |
||||
it('should provide module name mapper for stylesheets by default', () => { |
||||
const config = jestConfig(`${__dirname}/mocks/jestSetup/noOverrides`); |
||||
expect(config.moduleNameMapper).toBeDefined(); |
||||
expect(Object.keys(config.moduleNameMapper)).toContain('\\.(css|sass|scss)$'); |
||||
}); |
||||
|
||||
it('should preserve mapping for stylesheets when moduleNameMapper overrides provided', () => { |
||||
const config = jestConfig(`${__dirname}/mocks/jestSetup/overrides`); |
||||
expect(config.moduleNameMapper).toBeDefined(); |
||||
expect(Object.keys(config.moduleNameMapper)).toContain('\\.(css|sass|scss)$'); |
||||
expect(Object.keys(config.moduleNameMapper)).toContain('someOverride'); |
||||
}); |
||||
}); |
||||
}); |
@ -1,117 +0,0 @@ |
||||
import fs from 'fs'; |
||||
import path = require('path'); |
||||
|
||||
export const allowedJestConfigOverrides = [ |
||||
'snapshotSerializers', |
||||
'moduleNameMapper', |
||||
'globalSetup', |
||||
'globalTeardown', |
||||
'testEnvironment', |
||||
]; |
||||
|
||||
interface EnabledJestConfigOverrides { |
||||
snapshotSerializers: string[]; |
||||
moduleNameMapper: { [key: string]: string }; |
||||
} |
||||
|
||||
const getSetupFile = (filePath: string) => { |
||||
if (fs.existsSync(`${filePath}.js`)) { |
||||
return `${filePath}.js`; |
||||
} |
||||
if (fs.existsSync(`${filePath}.ts`)) { |
||||
return `${filePath}.ts`; |
||||
} |
||||
return undefined; |
||||
}; |
||||
|
||||
export const jestConfig = (baseDir: string = process.cwd()) => { |
||||
const jestConfigOverrides = (require(path.resolve(baseDir, 'package.json')).jest || {}) as EnabledJestConfigOverrides; |
||||
|
||||
const deniedOverrides = jestConfigOverrides |
||||
? Object.keys(jestConfigOverrides).filter((override) => allowedJestConfigOverrides.indexOf(override) === -1) |
||||
: []; |
||||
|
||||
if (deniedOverrides.length > 0) { |
||||
console.error("\ngrafana-toolkit doesn't support following Jest options: ", deniedOverrides); |
||||
console.log('Supported Jest options are: ', JSON.stringify(allowedJestConfigOverrides)); |
||||
throw new Error('Provided Jest config is not supported'); |
||||
} |
||||
|
||||
const shimsFilePath = path.resolve(baseDir, 'config/jest-shim'); |
||||
const setupFilePath = path.resolve(baseDir, 'config/jest-setup'); |
||||
|
||||
// Mock css imports for tests. Otherwise Jest will have troubles understanding SASS/CSS imports
|
||||
const { moduleNameMapper, ...otherOverrides } = jestConfigOverrides; |
||||
const moduleNameMapperConfig = { |
||||
'\\.(css|sass|scss)$': `${__dirname}/styles.mock.js`, |
||||
'react-inlinesvg': `${__dirname}/react-inlinesvg.tsx`, |
||||
...moduleNameMapper, |
||||
}; |
||||
|
||||
const setupFile = getSetupFile(setupFilePath); |
||||
const shimsFile = getSetupFile(shimsFilePath); |
||||
|
||||
const setupFiles = [setupFile, shimsFile, `${__dirname}/matchMedia.js`, require.resolve('jest-canvas-mock')].filter( |
||||
(f) => f |
||||
); |
||||
const defaultJestConfig = { |
||||
verbose: false, |
||||
moduleDirectories: ['node_modules', 'src'], |
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], |
||||
setupFiles, |
||||
globals: { |
||||
'ts-jest': { |
||||
isolatedModules: true, |
||||
tsconfig: path.resolve(baseDir, 'tsconfig.json'), |
||||
}, |
||||
}, |
||||
coverageReporters: ['json-summary', 'text', 'lcov'], |
||||
collectCoverageFrom: ['src/**/*.{ts,tsx}', '!**/node_modules/**', '!**/vendor/**'], |
||||
reporters: [ |
||||
'default', |
||||
[ |
||||
require.resolve('jest-junit'), |
||||
{ |
||||
outputDirectory: 'coverage', |
||||
}, |
||||
], |
||||
], |
||||
testEnvironment: 'jsdom', |
||||
testMatch: [ |
||||
'<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}', |
||||
'<rootDir>/src/**/*.{spec,test,jest}.{js,jsx,ts,tsx}', |
||||
'<rootDir>/spec/**/*.{spec,test,jest}.{js,jsx,ts,tsx}', |
||||
], |
||||
transform: { |
||||
'^.+\\.(js|jsx|mjs)$': [ |
||||
require.resolve('babel-jest'), |
||||
{ configFile: path.resolve(__dirname, './jest.babel.config.js') }, |
||||
], |
||||
'^.+\\.tsx?$': require.resolve('ts-jest'), |
||||
}, |
||||
transformIgnorePatterns: [ |
||||
'[/\\\\\\\\]node_modules[/\\\\\\\\].+\\\\.(js|jsx|ts|tsx)$', |
||||
'^.+\\\\.module\\\\.(css|sass|scss)$', |
||||
], |
||||
moduleNameMapper: moduleNameMapperConfig, |
||||
}; |
||||
|
||||
return { |
||||
...defaultJestConfig, |
||||
...otherOverrides, |
||||
}; |
||||
}; |
||||
|
||||
/** |
||||
* This will load the existing just setup, or use the default if it exists |
||||
*/ |
||||
export const loadJestPluginConfig = (baseDir: string = process.cwd()) => { |
||||
const cfgpath = path.resolve(baseDir, 'jest.config.js'); |
||||
if (!fs.existsSync(cfgpath)) { |
||||
const toolkitDir = path.dirname(require.resolve(`@grafana/toolkit/package.json`)); |
||||
const src = path.join(toolkitDir, 'src/config/jest.plugin.config.local.js'); |
||||
fs.copyFileSync(src, cfgpath); |
||||
console.log('Using standard jest plugin config', src); |
||||
} |
||||
return require(cfgpath); |
||||
}; |
@ -1,14 +0,0 @@ |
||||
// https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
|
||||
Object.defineProperty(global, 'matchMedia', { |
||||
writable: true, |
||||
value: jest.fn().mockImplementation((query) => ({ |
||||
matches: false, |
||||
media: query, |
||||
onchange: null, |
||||
addListener: jest.fn(), // deprecated
|
||||
removeListener: jest.fn(), // deprecated
|
||||
addEventListener: jest.fn(), |
||||
removeEventListener: jest.fn(), |
||||
dispatchEvent: jest.fn(), |
||||
})), |
||||
}); |
@ -1 +0,0 @@ |
||||
{} |
@ -1,11 +0,0 @@ |
||||
{ |
||||
"jest": { |
||||
"moduleNameMapper": { |
||||
"someOverride": "somePath" |
||||
}, |
||||
"snapshotSerializers": "serializers", |
||||
"globalSetup": "path", |
||||
"globalTeardown": "path", |
||||
"testEnvironment": "node" |
||||
} |
||||
} |
@ -1,5 +0,0 @@ |
||||
{ |
||||
"jest": { |
||||
"runner": "some-runner" |
||||
} |
||||
} |
@ -1,3 +0,0 @@ |
||||
{ |
||||
"version": "Testversion" |
||||
} |
@ -1,3 +0,0 @@ |
||||
{ |
||||
"version": "Testversion" |
||||
} |
@ -1,10 +0,0 @@ |
||||
'use strict'; |
||||
const { cloneDeep } = require('lodash'); |
||||
|
||||
const overrideWebpackConfig = (originalConfig, options) => { |
||||
const config = cloneDeep(originalConfig); |
||||
config.name = 'customConfig'; |
||||
return config; |
||||
}; |
||||
|
||||
module.exports = overrideWebpackConfig; |
@ -1,3 +0,0 @@ |
||||
{ |
||||
"version": "Testversion" |
||||
} |
@ -1,8 +0,0 @@ |
||||
'use strict'; |
||||
const { cloneDeep } = require('lodash'); |
||||
|
||||
module.exports.getWebpackConfig = (originalConfig, options) => { |
||||
const config = cloneDeep(originalConfig); |
||||
config.name = 'customConfig'; |
||||
return config; |
||||
}; |
@ -1,3 +0,0 @@ |
||||
{ |
||||
"version": "Testversion" |
||||
} |
@ -1,5 +0,0 @@ |
||||
/* WRONG CONFIG ON PURPOSE - DO NOT COPY THIS */ |
||||
|
||||
module.exports.config = { |
||||
name: 'test', |
||||
}; |
@ -1,5 +0,0 @@ |
||||
{ |
||||
"trailingComma": "es5", |
||||
"singleQuote": true, |
||||
"printWidth": 120 |
||||
} |
@ -1,3 +0,0 @@ |
||||
module.exports = { |
||||
...require('@grafana/toolkit/src/config/prettier.plugin.config.json'), |
||||
}; |
@ -1,25 +0,0 @@ |
||||
// Due to the grafana/ui Icon component making fetch requests to
|
||||
// `/public/img/icon/<icon_name>.svg` we need to mock react-inlinesvg to prevent
|
||||
// the failed fetch requests from displaying errors in console.
|
||||
|
||||
import React from 'react'; |
||||
|
||||
type Callback = (...args: any[]) => void; |
||||
|
||||
export interface StorageItem { |
||||
content: string; |
||||
queue: Callback[]; |
||||
status: string; |
||||
} |
||||
|
||||
export const cacheStore: { [key: string]: StorageItem } = Object.create(null); |
||||
|
||||
const SVG_FILE_NAME_REGEX = /(.+)\/(.+)\.svg$/; |
||||
|
||||
const InlineSVG = ({ src }: { src: string }) => { |
||||
// testId will be the file name without extension (e.g. `public/img/icons/angle-double-down.svg` -> `angle-double-down`)
|
||||
const testId = src.replace(SVG_FILE_NAME_REGEX, '$2'); |
||||
return <svg xmlns="http://www.w3.org/2000/svg" data-testid={testId} viewBox="0 0 24 24" />; |
||||
}; |
||||
|
||||
export default InlineSVG; |
@ -1,4 +0,0 @@ |
||||
// Mock for handling stylesheet file imports in tests
|
||||
// https://jestjs.io/docs/en/webpack.html#handling-static-assets
|
||||
|
||||
module.exports = {}; |
@ -1,7 +0,0 @@ |
||||
{ |
||||
"compilerOptions": { |
||||
"alwaysStrict": true, |
||||
"declaration": false |
||||
}, |
||||
"extends": "@grafana/tsconfig" |
||||
} |
@ -1,9 +0,0 @@ |
||||
{ |
||||
"compilerOptions": { |
||||
"rootDir": "./src", |
||||
"baseUrl": "./src", |
||||
"jsx": "react" |
||||
}, |
||||
"extends": "@grafana/toolkit/src/config/tsconfig.plugin.json", |
||||
"include": ["src", "types"] |
||||
} |
@ -1,11 +0,0 @@ |
||||
import path from 'path'; |
||||
|
||||
let PLUGIN_ID: string; |
||||
|
||||
export const getPluginId = () => { |
||||
if (!PLUGIN_ID) { |
||||
const pluginJson = require(path.resolve(process.cwd(), 'src/plugin.json')); |
||||
PLUGIN_ID = pluginJson.id; |
||||
} |
||||
return PLUGIN_ID; |
||||
}; |
@ -1,15 +0,0 @@ |
||||
import { getPluginJson, validatePluginJson } from './pluginValidation'; |
||||
|
||||
describe('pluginValidation', () => { |
||||
describe('plugin.json', () => { |
||||
test('missing plugin.json file', () => { |
||||
expect(() => getPluginJson(`${__dirname}/mocks/missing-plugin.json`)).toThrowError(); |
||||
}); |
||||
}); |
||||
|
||||
describe('validatePluginJson', () => { |
||||
test('missing plugin.json file', () => { |
||||
expect(() => validatePluginJson({})).toThrow('Plugin id is missing in plugin.json'); |
||||
}); |
||||
}); |
||||
}); |
@ -1,46 +0,0 @@ |
||||
import { PluginMeta } from '@grafana/data'; |
||||
|
||||
export const validatePluginJson = (pluginJson: any) => { |
||||
if (!pluginJson.id) { |
||||
throw new Error('Plugin id is missing in plugin.json'); |
||||
} |
||||
|
||||
if (!pluginJson.info) { |
||||
throw new Error('Plugin info node is missing in plugin.json'); |
||||
} |
||||
|
||||
if (!pluginJson.info.version) { |
||||
throw new Error('Plugin info.version is missing in plugin.json'); |
||||
} |
||||
|
||||
const types = ['panel', 'datasource', 'app']; |
||||
const type = pluginJson.type; |
||||
if (!types.includes(type)) { |
||||
throw new Error('Invalid plugin type in plugin.json: ' + type); |
||||
} |
||||
|
||||
if (!pluginJson.id.endsWith('-' + type)) { |
||||
throw new Error('[plugin.json] id should end with: -' + type); |
||||
} |
||||
}; |
||||
|
||||
export const getPluginJson = (path: string): PluginMeta => { |
||||
let pluginJson; |
||||
try { |
||||
pluginJson = require(path); |
||||
} catch (e) { |
||||
throw new Error('Unable to find: ' + path); |
||||
} |
||||
|
||||
validatePluginJson(pluginJson); |
||||
|
||||
return pluginJson as PluginMeta; |
||||
}; |
||||
|
||||
export const assertRootUrlIsValid = (rootUrl: string) => { |
||||
try { |
||||
new URL(rootUrl); |
||||
} catch (err) { |
||||
throw new Error(`${rootUrl} is not a valid URL`); |
||||
} |
||||
}; |
@ -1,77 +0,0 @@ |
||||
import fs, { BigIntStats } from 'fs'; |
||||
|
||||
import { findModuleFiles, loadWebpackConfig } from './webpack.plugin.config'; |
||||
// eslint-disable-next-line no-duplicate-imports
|
||||
import * as webpackConfig from './webpack.plugin.config'; |
||||
|
||||
jest.mock('./webpack/loaders', () => ({ |
||||
getFileLoaders: (): Array<{}> => [], |
||||
getStylesheetEntries: () => ({}), |
||||
getStyleLoaders: (): Array<{}> => [], |
||||
})); |
||||
|
||||
const modulePathsMock = [ |
||||
'some/path/module.ts', |
||||
'some/path/module.ts.whatever', |
||||
'some/path/module.tsx', |
||||
'some/path/module.tsx.whatever', |
||||
'some/path/anotherFile.ts', |
||||
'some/path/anotherFile.tsx', |
||||
]; |
||||
|
||||
describe('Plugin webpack config', () => { |
||||
describe('findModuleTs', () => { |
||||
beforeAll(() => { |
||||
jest.spyOn(fs, 'statSync').mockReturnValue({ |
||||
isDirectory: () => false, |
||||
} as BigIntStats); |
||||
}); |
||||
|
||||
afterAll(() => { |
||||
jest.restoreAllMocks(); |
||||
}); |
||||
|
||||
it('finds module.ts and module.tsx files', async () => { |
||||
const moduleFiles = await findModuleFiles('/', modulePathsMock); |
||||
expect(moduleFiles.length).toBe(2); |
||||
// normalize windows path - \\ -> /
|
||||
expect(moduleFiles.map((p) => p.replace(/\\/g, '/'))).toEqual(['/some/path/module.ts', '/some/path/module.tsx']); |
||||
}); |
||||
}); |
||||
|
||||
describe('loadWebpackConfig', () => { |
||||
beforeAll(() => { |
||||
jest.spyOn(webpackConfig, 'findModuleFiles').mockReturnValue(new Promise((res, _) => res([]))); |
||||
}); |
||||
|
||||
afterAll(() => { |
||||
jest.restoreAllMocks(); |
||||
}); |
||||
|
||||
it('uses default config if no override exists', async () => { |
||||
const spy = jest.spyOn(process, 'cwd'); |
||||
spy.mockReturnValue(`${__dirname}/mocks/webpack/noOverride/`); |
||||
await loadWebpackConfig({}); |
||||
}); |
||||
|
||||
it('calls customConfig if it exists', async () => { |
||||
const spy = jest.spyOn(process, 'cwd'); |
||||
spy.mockReturnValue(`${__dirname}/mocks/webpack/overrides/`); |
||||
const config = await loadWebpackConfig({}); |
||||
expect(config.name).toBe('customConfig'); |
||||
}); |
||||
|
||||
it('loads export named getWebpackConfiguration', async () => { |
||||
const spy = jest.spyOn(process, 'cwd'); |
||||
spy.mockReturnValue(`${__dirname}/mocks/webpack/overridesNamedExport/`); |
||||
const config = await loadWebpackConfig({}); |
||||
expect(config.name).toBe('customConfig'); |
||||
}); |
||||
|
||||
it('throws an error if module does not export function', async () => { |
||||
const spy = jest.spyOn(process, 'cwd'); |
||||
spy.mockReturnValue(`${__dirname}/mocks/webpack/unsupportedOverride/`); |
||||
await expect(loadWebpackConfig({})).rejects.toThrowError(); |
||||
}); |
||||
}); |
||||
}); |
@ -1,289 +0,0 @@ |
||||
import * as webpack from 'webpack'; |
||||
|
||||
import { getStyleLoaders, getStylesheetEntries, getFileLoaders } from './webpack/loaders'; |
||||
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin'); |
||||
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); |
||||
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); |
||||
const fs = require('fs'); |
||||
const HtmlWebpackPlugin = require('html-webpack-plugin'); |
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); |
||||
const path = require('path'); |
||||
const ReplaceInFileWebpackPlugin = require('replace-in-file-webpack-plugin'); |
||||
const TerserPlugin = require('terser-webpack-plugin'); |
||||
const util = require('util'); |
||||
|
||||
const readdirPromise = util.promisify(fs.readdir); |
||||
const accessPromise = util.promisify(fs.access); |
||||
|
||||
export interface WebpackConfigurationOptions { |
||||
watch?: boolean; |
||||
production?: boolean; |
||||
preserveConsole?: boolean; |
||||
} |
||||
|
||||
type WebpackConfigurationGetter = (options: WebpackConfigurationOptions) => Promise<webpack.Configuration>; |
||||
|
||||
export type CustomWebpackConfigurationGetter = ( |
||||
originalConfig: webpack.Configuration, |
||||
options: WebpackConfigurationOptions |
||||
) => webpack.Configuration; |
||||
|
||||
export const findModuleFiles = async (base: string, files?: string[], result?: string[]) => { |
||||
files = files || (await readdirPromise(base)); |
||||
result = result || []; |
||||
|
||||
if (files) { |
||||
await Promise.all( |
||||
files.map(async (file) => { |
||||
const newbase = path.join(base, file); |
||||
if (fs.statSync(newbase).isDirectory()) { |
||||
result = await findModuleFiles(newbase, await readdirPromise(newbase), result); |
||||
} else { |
||||
const filename = path.basename(file); |
||||
if (/^module.(t|j)sx?$/.exec(filename)) { |
||||
// @ts-ignore
|
||||
result.push(newbase); |
||||
} |
||||
} |
||||
}) |
||||
); |
||||
} |
||||
return result; |
||||
}; |
||||
|
||||
const getModuleFiles = () => { |
||||
return findModuleFiles(path.resolve(process.cwd(), 'src')); |
||||
}; |
||||
|
||||
const getManualChunk = (id: string) => { |
||||
if (id.endsWith('module.ts') || id.endsWith('module.js') || id.endsWith('module.tsx')) { |
||||
const idx = id.lastIndexOf(path.sep + 'src' + path.sep); |
||||
if (idx > 0) { |
||||
const name = id.substring(idx + 5, id.lastIndexOf('.')); |
||||
|
||||
return { |
||||
name, |
||||
module: id, |
||||
}; |
||||
} |
||||
} |
||||
return null; |
||||
}; |
||||
|
||||
const getEntries = async () => { |
||||
const entries: { [key: string]: string } = {}; |
||||
const modules = await getModuleFiles(); |
||||
|
||||
modules.forEach((modFile) => { |
||||
const mod = getManualChunk(modFile); |
||||
// @ts-ignore
|
||||
entries[mod.name] = mod.module; |
||||
}); |
||||
return { |
||||
...entries, |
||||
...getStylesheetEntries(), |
||||
}; |
||||
}; |
||||
|
||||
const getCommonPlugins = (options: WebpackConfigurationOptions) => { |
||||
const hasREADME = fs.existsSync(path.resolve(process.cwd(), 'src', 'README.md')); |
||||
const packageJson = require(path.resolve(process.cwd(), 'package.json')); |
||||
return [ |
||||
new MiniCssExtractPlugin({ |
||||
// both options are optional
|
||||
filename: 'styles/[name].css', |
||||
}), |
||||
new CopyWebpackPlugin({ |
||||
patterns: [ |
||||
// If src/README.md exists use it; otherwise the root README
|
||||
{ from: hasREADME ? 'README.md' : '../README.md', to: '.', force: true, priority: 1, noErrorOnMissing: true }, |
||||
{ from: 'plugin.json', to: '.', noErrorOnMissing: true }, |
||||
{ from: '**/README.md', to: '[path]README.md', priority: 0, noErrorOnMissing: true }, |
||||
{ from: '../LICENSE', to: '.', noErrorOnMissing: true }, |
||||
{ from: '../CHANGELOG.md', to: '.', force: true, noErrorOnMissing: true }, |
||||
{ from: '**/*.{json,svg,png,html}', to: '.', noErrorOnMissing: true }, |
||||
{ from: 'img/**/*', to: '.', noErrorOnMissing: true }, |
||||
{ from: 'libs/**/*', to: '.', noErrorOnMissing: true }, |
||||
{ from: 'static/**/*', to: '.', noErrorOnMissing: true }, |
||||
], |
||||
}), |
||||
|
||||
new ReplaceInFileWebpackPlugin([ |
||||
{ |
||||
dir: 'dist', |
||||
files: ['plugin.json', 'README.md'], |
||||
rules: [ |
||||
{ |
||||
search: '%VERSION%', |
||||
replace: packageJson.version, |
||||
}, |
||||
{ |
||||
search: '%TODAY%', |
||||
replace: new Date().toISOString().substring(0, 10), |
||||
}, |
||||
], |
||||
}, |
||||
]), |
||||
new ForkTsCheckerWebpackPlugin({ |
||||
typescript: { configFile: path.join(process.cwd(), 'tsconfig.json') }, |
||||
issue: { |
||||
include: [{ file: '**/*.{ts,tsx}' }], |
||||
}, |
||||
}), |
||||
]; |
||||
}; |
||||
|
||||
const getBaseWebpackConfig: WebpackConfigurationGetter = async (options) => { |
||||
const plugins = getCommonPlugins(options); |
||||
const optimization: { [key: string]: any } = {}; |
||||
|
||||
if (options.production) { |
||||
const compressOptions = { drop_console: !options.preserveConsole, drop_debugger: true }; |
||||
optimization.minimizer = [ |
||||
new TerserPlugin({ terserOptions: { compress: compressOptions } }), |
||||
new CssMinimizerPlugin(), |
||||
]; |
||||
optimization.chunkIds = 'total-size'; |
||||
optimization.moduleIds = 'size'; |
||||
} else if (options.watch) { |
||||
plugins.push(new HtmlWebpackPlugin()); |
||||
} |
||||
|
||||
return { |
||||
mode: options.production ? 'production' : 'development', |
||||
target: 'web', |
||||
context: path.join(process.cwd(), 'src'), |
||||
devtool: 'source-map', |
||||
entry: await getEntries(), |
||||
output: { |
||||
filename: '[name].js', |
||||
path: path.join(process.cwd(), 'dist'), |
||||
libraryTarget: 'amd', |
||||
publicPath: '/', |
||||
}, |
||||
performance: { hints: false }, |
||||
externals: [ |
||||
'lodash', |
||||
'jquery', |
||||
'moment', |
||||
'slate', |
||||
'emotion', |
||||
'@emotion/react', |
||||
'@emotion/css', |
||||
'prismjs', |
||||
'slate-plain-serializer', |
||||
'slate-react', |
||||
'react', |
||||
'react-dom', |
||||
'react-redux', |
||||
'redux', |
||||
'rxjs', |
||||
'react-router', |
||||
'react-router-dom', |
||||
'd3', |
||||
'angular', |
||||
'@grafana/ui', |
||||
'@grafana/runtime', |
||||
'@grafana/data', |
||||
({ request }, callback) => { |
||||
const prefix = 'grafana/'; |
||||
if (request?.indexOf(prefix) === 0) { |
||||
return callback(undefined, request.slice(prefix.length)); |
||||
} |
||||
|
||||
callback(); |
||||
}, |
||||
], |
||||
plugins, |
||||
resolve: { |
||||
extensions: ['.ts', '.tsx', '.js'], |
||||
modules: [path.resolve(process.cwd(), 'src'), 'node_modules'], |
||||
fallback: { |
||||
buffer: false, |
||||
fs: false, |
||||
stream: false, |
||||
http: false, |
||||
https: false, |
||||
string_decoder: false, |
||||
os: false, |
||||
timers: false, |
||||
}, |
||||
}, |
||||
module: { |
||||
rules: [ |
||||
{ |
||||
test: /\.[tj]sx?$/, |
||||
use: { |
||||
loader: require.resolve('babel-loader'), |
||||
options: { |
||||
cacheDirectory: true, |
||||
cacheCompression: false, |
||||
presets: [ |
||||
[require.resolve('@babel/preset-env'), { modules: false }], |
||||
[ |
||||
require.resolve('@babel/preset-typescript'), |
||||
{ |
||||
allowNamespaces: true, |
||||
allowDeclareFields: true, |
||||
}, |
||||
], |
||||
[require.resolve('@babel/preset-react')], |
||||
], |
||||
plugins: [ |
||||
[ |
||||
require.resolve('@babel/plugin-transform-typescript'), |
||||
{ |
||||
allowNamespaces: true, |
||||
allowDeclareFields: true, |
||||
}, |
||||
], |
||||
require.resolve('@babel/plugin-proposal-class-properties'), |
||||
[require.resolve('@babel/plugin-proposal-object-rest-spread'), { loose: true }], |
||||
require.resolve('@babel/plugin-transform-react-constant-elements'), |
||||
require.resolve('@babel/plugin-proposal-nullish-coalescing-operator'), |
||||
require.resolve('@babel/plugin-proposal-optional-chaining'), |
||||
require.resolve('@babel/plugin-syntax-dynamic-import'), |
||||
require.resolve('babel-plugin-angularjs-annotate'), |
||||
], |
||||
}, |
||||
}, |
||||
exclude: /node_modules/, |
||||
}, |
||||
...getStyleLoaders(), |
||||
{ |
||||
test: /\.html$/, |
||||
exclude: [/node_modules/], |
||||
use: { |
||||
loader: require.resolve('html-loader'), |
||||
}, |
||||
}, |
||||
...getFileLoaders(), |
||||
], |
||||
}, |
||||
optimization, |
||||
}; |
||||
}; |
||||
|
||||
export const loadWebpackConfig: WebpackConfigurationGetter = async (options) => { |
||||
const baseConfig = await getBaseWebpackConfig(options); |
||||
const customWebpackPath = path.resolve(process.cwd(), 'webpack.config.js'); |
||||
|
||||
try { |
||||
await accessPromise(customWebpackPath); |
||||
const customConfig = require(customWebpackPath); |
||||
const configGetter = customConfig.getWebpackConfig || customConfig; |
||||
if (typeof configGetter !== 'function') { |
||||
throw Error( |
||||
'Custom webpack config needs to export a function implementing CustomWebpackConfigurationGetter. Function needs to be ' + |
||||
'module export or named "getWebpackConfig"' |
||||
); |
||||
} |
||||
return (configGetter as CustomWebpackConfigurationGetter)(baseConfig, options); |
||||
} catch (err: any) { |
||||
if (err.code === 'ENOENT') { |
||||
return baseConfig; |
||||
} |
||||
throw err; |
||||
} |
||||
}; |
@ -1,26 +0,0 @@ |
||||
import { getStylesheetEntries } from './loaders'; |
||||
|
||||
describe('Loaders', () => { |
||||
describe('stylesheet helpers', () => { |
||||
beforeEach(() => { |
||||
jest.spyOn(console, 'log').mockImplementation(); |
||||
}); |
||||
|
||||
afterEach(() => { |
||||
jest.restoreAllMocks(); |
||||
}); |
||||
|
||||
describe('getStylesheetEntries', () => { |
||||
it('returns entries for dark and light theme', () => { |
||||
const result = getStylesheetEntries(`${__dirname}/../mocks/stylesheetsSupport/ok`); |
||||
expect(Object.keys(result)).toHaveLength(2); |
||||
}); |
||||
it('throws on theme files duplicates', () => { |
||||
const result = () => { |
||||
getStylesheetEntries(`${__dirname}/../mocks/stylesheetsSupport/duplicates`); |
||||
}; |
||||
expect(result).toThrow(); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
@ -1,129 +0,0 @@ |
||||
import fs from 'fs'; |
||||
import path from 'path'; |
||||
|
||||
import { getPluginId } from '../utils/getPluginId'; |
||||
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); |
||||
|
||||
const supportedExtensions = ['css', 'scss', 'less', 'sass']; |
||||
|
||||
const getStylesheetPaths = (root: string = process.cwd()) => { |
||||
return [`${root}/src/styles/light`, `${root}/src/styles/dark`]; |
||||
}; |
||||
|
||||
export const getStylesheetEntries = (root: string = process.cwd()) => { |
||||
const stylesheetsPaths = getStylesheetPaths(root); |
||||
const entries: { [key: string]: string } = {}; |
||||
supportedExtensions.forEach((e) => { |
||||
stylesheetsPaths.forEach((p) => { |
||||
const entryName = p.split('/').slice(-1)[0]; |
||||
if (fs.existsSync(`${p}.${e}`)) { |
||||
if (entries[entryName]) { |
||||
console.log(`\nSeems like you have multiple files for ${entryName} theme:`); |
||||
console.log(entries[entryName]); |
||||
console.log(`${p}.${e}`); |
||||
throw new Error('Duplicated stylesheet'); |
||||
} else { |
||||
entries[entryName] = `${p}.${e}`; |
||||
} |
||||
} |
||||
}); |
||||
}); |
||||
|
||||
return entries; |
||||
}; |
||||
|
||||
export const getStyleLoaders = () => { |
||||
const extractionLoader = { |
||||
loader: MiniCssExtractPlugin.loader, |
||||
options: { |
||||
publicPath: '../', |
||||
}, |
||||
}; |
||||
|
||||
const cssLoaders = [ |
||||
{ |
||||
loader: require.resolve('css-loader'), |
||||
options: { |
||||
importLoaders: 1, |
||||
sourceMap: true, |
||||
}, |
||||
}, |
||||
{ |
||||
loader: require.resolve('postcss-loader'), |
||||
options: { |
||||
postcssOptions: { |
||||
plugins: () => [ |
||||
require('postcss-flexbugs-fixes'), |
||||
require('postcss-preset-env')({ |
||||
autoprefixer: { flexbox: 'no-2009', grid: true }, |
||||
}), |
||||
], |
||||
}, |
||||
}, |
||||
}, |
||||
]; |
||||
|
||||
const styleDir = path.resolve(process.cwd(), 'src', 'styles') + path.sep; |
||||
const rules = [ |
||||
{ |
||||
test: /(dark|light)\.css$/, |
||||
use: [extractionLoader, ...cssLoaders], |
||||
}, |
||||
{ |
||||
test: /(dark|light)\.scss$/, |
||||
use: [extractionLoader, ...cssLoaders, require.resolve('sass-loader')], |
||||
}, |
||||
{ |
||||
test: /\.css$/, |
||||
use: ['style-loader', ...cssLoaders, require.resolve('sass-loader')], |
||||
exclude: [`${styleDir}light.css`, `${styleDir}dark.css`], |
||||
}, |
||||
{ |
||||
test: /\.s[ac]ss$/, |
||||
use: ['style-loader', ...cssLoaders, require.resolve('sass-loader')], |
||||
exclude: [`${styleDir}light.scss`, `${styleDir}dark.scss`], |
||||
}, |
||||
{ |
||||
test: /\.less$/, |
||||
use: [ |
||||
{ |
||||
loader: require.resolve('style-loader'), |
||||
}, |
||||
...cssLoaders, |
||||
{ |
||||
loader: require.resolve('less-loader'), |
||||
options: { |
||||
lessOptions: { |
||||
javascriptEnabled: true, |
||||
}, |
||||
}, |
||||
}, |
||||
], |
||||
exclude: [`${styleDir}light.less`, `${styleDir}dark.less`], |
||||
}, |
||||
]; |
||||
|
||||
return rules; |
||||
}; |
||||
|
||||
export const getFileLoaders = () => { |
||||
return [ |
||||
{ |
||||
test: /\.(png|jpe?g|gif|svg)$/, |
||||
type: 'asset/resource', |
||||
generator: { |
||||
publicPath: `public/plugins/${getPluginId()}/img/`, |
||||
outputPath: 'img/', |
||||
}, |
||||
}, |
||||
{ |
||||
test: /\.(woff|woff2|eot|ttf|otf)(\?v=\d+\.\d+\.\d+)?$/, |
||||
type: 'asset/resource', |
||||
generator: { |
||||
publicPath: `public/plugins/${getPluginId()}/fonts/`, |
||||
outputPath: 'fonts/', |
||||
}, |
||||
}, |
||||
]; |
||||
}; |
Loading…
Reference in new issue