diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..c632575 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "WebSearch" + ], + "deny": [], + "ask": [] + } +} diff --git a/.config/.cprc.json b/.config/.cprc.json new file mode 100644 index 0000000..4d3e5d2 --- /dev/null +++ b/.config/.cprc.json @@ -0,0 +1,3 @@ +{ + "version": "6.1.1" +} diff --git a/.config/.prettierrc.js b/.config/.prettierrc.js index 66a76ec..bf506f5 100644 --- a/.config/.prettierrc.js +++ b/.config/.prettierrc.js @@ -5,12 +5,12 @@ */ module.exports = { - "endOfLine": "auto", - "printWidth": 120, - "trailingComma": "es5", - "semi": true, - "jsxSingleQuote": false, - "singleQuote": true, - "useTabs": false, - "tabWidth": 2 -}; \ No newline at end of file + endOfLine: 'auto', + printWidth: 120, + trailingComma: 'es5', + semi: true, + jsxSingleQuote: false, + singleQuote: true, + useTabs: false, + tabWidth: 2, +}; diff --git a/.config/Dockerfile b/.config/Dockerfile index 35d89bd..c55d9e1 100644 --- a/.config/Dockerfile +++ b/.config/Dockerfile @@ -3,14 +3,52 @@ ARG grafana_image=grafana-enterprise FROM grafana/${grafana_image}:${grafana_version} +ARG anonymous_auth_enabled=true +ARG development=false +ARG TARGETARCH + + +ENV DEV "${development}" + # Make it as simple as possible to access the grafana instance for development purposes # Do NOT enable these settings in a public facing / production grafana instance ENV GF_AUTH_ANONYMOUS_ORG_ROLE "Admin" -ENV GF_AUTH_ANONYMOUS_ENABLED "true" +ENV GF_AUTH_ANONYMOUS_ENABLED "${anonymous_auth_enabled}" ENV GF_AUTH_BASIC_ENABLED "false" # Set development mode so plugins can be loaded without the need to sign ENV GF_DEFAULT_APP_MODE "development" -# Inject livereload script into grafana index.html + +LABEL maintainer="Grafana Labs " + +ENV GF_PATHS_HOME="/usr/share/grafana" +WORKDIR $GF_PATHS_HOME + USER root -RUN sed -i 's/<\/body><\/html>/|g' /usr/share/grafana/public/views/index.html + + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] diff --git a/.config/README.md b/.config/README.md index f1ded34..93d9a3b 100644 --- a/.config/README.md +++ b/.config/README.md @@ -13,19 +13,29 @@ to issues around working with the project. ### Extending the ESLint config -Edit the `.eslintrc` file in the project root in order to extend the ESLint configuration. +Edit the `eslint.config.mjs` file in the project root to extend the ESLint configuration. The following example disables deprecation notices for source files. **Example:** -```json -{ - "extends": "./.config/.eslintrc", - "rules": { - "react/prop-types": "off" - } -} +```javascript +import { defineConfig } from 'eslint/config'; +import baseConfig from './.config/eslint.config.mjs'; + +export default defineConfig([ + { + ignores: [ + //... + ], + }, + ...baseConfig, + { + files: ['src/**/*.{ts,tsx}'], + rules: { + '@typescript-eslint/no-deprecated': 'off', + }, + }, +]); ``` - --- ### Extending the Prettier config @@ -56,7 +66,7 @@ set up the Jest DOM for the testing library and to apply some polyfills. ([link #### ESM errors with Jest -A common issue found with the current jest config involves importing an npm package which only offers an ESM build. These packages cause jest to error with `SyntaxError: Cannot use import statement outside a module`. To work around this we provide a list of known packages to pass to the `[transformIgnorePatterns](https://jestjs.io/docs/configuration#transformignorepatterns-arraystring)` jest configuration property. If need be this can be extended in the following way: +A common issue with the current jest config involves importing an npm package that only offers an ESM build. These packages cause jest to error with `SyntaxError: Cannot use import statement outside a module`. To work around this, we provide a list of known packages to pass to the `[transformIgnorePatterns](https://jestjs.io/docs/configuration#transformignorepatterns-arraystring)` jest configuration property. If need be, this can be extended in the following way: ```javascript process.env.TZ = 'UTC'; @@ -106,9 +116,9 @@ We are going to use [`webpack-merge`](https://github.com/survivejs/webpack-merge // webpack.config.ts import type { Configuration } from 'webpack'; import { merge } from 'webpack-merge'; -import grafanaConfig from './.config/webpack/webpack.config'; +import grafanaConfig, { type Env } from './.config/webpack/webpack.config'; -const config = async (env): Promise => { +const config = async (env: Env): Promise => { const baseConfig = await grafanaConfig(env); return merge(baseConfig, { @@ -142,7 +152,7 @@ We need to update the `scripts` in the `package.json` to use the extended Webpac ### Configure grafana image to use when running docker -By default `grafana-enterprise` will be used as the docker image for all docker related commands. If you want to override this behaviour simply alter the `docker-compose.yaml` by adding the following build arg `grafana_image`. +By default, `grafana-enterprise` will be used as the docker image for all docker related commands. If you want to override this behavior, simply alter the `docker-compose.yaml` by adding the following build arg `grafana_image`. **Example:** @@ -151,14 +161,15 @@ version: '3.7' services: grafana: - container_name: 'myorg-basic-app' + extends: + file: .config/docker-compose-base.yaml + service: grafana build: - context: ./.config args: grafana_version: ${GRAFANA_VERSION:-9.1.2} grafana_image: ${GRAFANA_IMAGE:-grafana} ``` -In this example we are assigning the environment variable `GRAFANA_IMAGE` to the build arg `grafana_image` with a default value of `grafana`. This will give you the possibility to set the value while running the docker-compose commands which might be convinent in some scenarios. +In this example, we assign the environment variable `GRAFANA_IMAGE` to the build arg `grafana_image` with a default value of `grafana`. This will allow you to set the value while running the docker compose commands, which might be convenient in some scenarios. --- diff --git a/.config/bundler/externals.ts b/.config/bundler/externals.ts new file mode 100644 index 0000000..82501b6 --- /dev/null +++ b/.config/bundler/externals.ts @@ -0,0 +1,43 @@ +import type { Configuration, ExternalItemFunctionData } from 'webpack'; + + type ExternalsType = Configuration['externals']; + +export const externals: ExternalsType = [ + // Required for dynamic publicPath resolution + { 'amd-module': 'module' }, + 'lodash', + 'jquery', + 'moment', + 'slate', + 'emotion', + '@emotion/react', + '@emotion/css', + 'prismjs', + 'slate-plain-serializer', + '@grafana/slate-react', + 'react', + 'react-dom', + 'react-redux', + 'redux', + 'rxjs', + 'i18next', + 'react-router', + 'd3', + 'angular', + /^@grafana\/ui/i, + /^@grafana\/runtime/i, + /^@grafana\/data/i, + + // Mark legacy SDK imports as external if their name starts with the "grafana/" prefix + ({ request }: ExternalItemFunctionData, callback: (error?: Error, result?: string) => void) => { + const prefix = 'grafana/'; + const hasPrefix = (request: string) => request.indexOf(prefix) === 0; + const stripPrefix = (request: string) => request.slice(prefix.length); + + if (request && hasPrefix(request)) { + return callback(undefined, stripPrefix(request)); + } + + callback(); + }, +]; \ No newline at end of file diff --git a/.config/docker-compose-base.yaml b/.config/docker-compose-base.yaml new file mode 100644 index 0000000..0bc3044 --- /dev/null +++ b/.config/docker-compose-base.yaml @@ -0,0 +1,25 @@ +services: + grafana: + user: root + container_name: 'swiftmade-sankey-panel' + + build: + context: . + args: + grafana_image: ${GRAFANA_IMAGE:-grafana-enterprise} + grafana_version: ${GRAFANA_VERSION:-12.2.0} + development: ${DEVELOPMENT:-false} + anonymous_auth_enabled: ${ANONYMOUS_AUTH_ENABLED:-true} + ports: + - 3000:3000/tcp + volumes: + - ../dist:/var/lib/grafana/plugins/swiftmade-sankey-panel + - ../provisioning:/etc/grafana/provisioning + - ..:/root/swiftmade-sankey-panel + + environment: + NODE_ENV: development + GF_LOG_FILTERS: plugin.swiftmade-sankey-panel:debug + GF_LOG_LEVEL: debug + GF_DATAPROXY_LOGGING: 1 + GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS: swiftmade-sankey-panel diff --git a/.config/entrypoint.sh b/.config/entrypoint.sh new file mode 100644 index 0000000..00c69f2 --- /dev/null +++ b/.config/entrypoint.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +if [ "${DEV}" = "false" ]; then + echo "Starting test mode" + exec /run.sh +fi + +echo "Starting development mode" + +if grep -i -q alpine /etc/issue; then + exec /usr/bin/supervisord -c /etc/supervisord.conf +elif grep -i -q ubuntu /etc/issue; then + exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf +else + echo 'ERROR: Unsupported base image' + exit 1 +fi + diff --git a/.config/eslint.config.mjs b/.config/eslint.config.mjs new file mode 100644 index 0000000..bafeaf5 --- /dev/null +++ b/.config/eslint.config.mjs @@ -0,0 +1,38 @@ +/* + * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ + * + * In order to extend the configuration follow the steps in + * https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations#extend-the-eslint-config + */ + +import { defineConfig } from 'eslint/config'; +import grafanaConfig from '@grafana/eslint-config/flat.js'; + +export default defineConfig([ + ...grafanaConfig, + { + rules: { + 'react/prop-types': 'off', + }, + }, + { + files: ['src/**/*.{ts,tsx}'], + + languageOptions: { + parserOptions: { + project: './tsconfig.json', + }, + }, + + rules: { + '@typescript-eslint/no-deprecated': 'warn', + }, + }, + { + files: ['./tests/**/*'], + + rules: { + 'react-hooks/rules-of-hooks': 'off', + }, + }, +]); diff --git a/.config/jest-setup.js b/.config/jest-setup.js index 575b354..7b1771e 100644 --- a/.config/jest-setup.js +++ b/.config/jest-setup.js @@ -2,15 +2,18 @@ * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ * * In order to extend the configuration follow the steps in - * https://grafana.github.io/plugin-tools/docs/advanced-configuration#extending-the-jest-config + * https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations#extend-the-jest-config */ import '@testing-library/jest-dom'; +import { TextEncoder, TextDecoder } from 'util'; + +Object.assign(global, { TextDecoder, TextEncoder }); // 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) => ({ + value: (query) => ({ matches: false, media: query, onchange: null, @@ -19,7 +22,7 @@ Object.defineProperty(global, 'matchMedia', { addEventListener: jest.fn(), removeEventListener: jest.fn(), dispatchEvent: jest.fn(), - })), + }), }); HTMLCanvasElement.prototype.getContext = () => {}; diff --git a/.config/jest.config.js b/.config/jest.config.js index 027c0ff..efe1938 100644 --- a/.config/jest.config.js +++ b/.config/jest.config.js @@ -2,7 +2,7 @@ * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ * * In order to extend the configuration follow the steps in - * https://grafana.github.io/plugin-tools/docs/advanced-configuration#extending-the-jest-config + * https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations#extend-the-jest-config */ const path = require('path'); @@ -25,7 +25,7 @@ module.exports = { '^.+\\.(t|j)sx?$': [ '@swc/jest', { - sourceMaps: true, + sourceMaps: 'inline', jsc: { parser: { syntax: 'typescript', @@ -40,4 +40,5 @@ module.exports = { // Jest will throw `Cannot use import statement outside module` if it tries to load an // ES module without it being transformed first. ./config/README.md#esm-errors-with-jest transformIgnorePatterns: [nodeModulesToTransform(grafanaESModules)], + watchPathIgnorePatterns: ['/node_modules', '/dist'], }; diff --git a/.config/jest/utils.js b/.config/jest/utils.js index f8ae417..55d9cb6 100644 --- a/.config/jest/utils.js +++ b/.config/jest/utils.js @@ -8,22 +8,30 @@ * This utility function is useful in combination with jest `transformIgnorePatterns` config * to transform specific packages (e.g.ES modules) in a projects node_modules folder. */ -const nodeModulesToTransform = (moduleNames) => `node_modules\/(?!(${moduleNames.join('|')})\/)`; +const nodeModulesToTransform = (moduleNames) => `node_modules\/(?!.*(${moduleNames.join('|')})\/.*)`; // Array of known nested grafana package dependencies that only bundle an ESM version const grafanaESModules = [ '.pnpm', // Support using pnpm symlinked packages + '@grafana/schema', + '@wojtekmaj/date-utils', 'd3', 'd3-color', 'd3-force', 'd3-interpolate', 'd3-scale-chromatic', + 'get-user-locale', + 'marked', + 'memoize', + 'mimic-function', 'ol', + 'react-calendar', 'react-colorful', + 'rxjs', 'uuid', ]; module.exports = { nodeModulesToTransform, - grafanaESModules -} \ No newline at end of file + grafanaESModules, +}; diff --git a/.config/rspack/BuildModeRspackPlugin.ts b/.config/rspack/BuildModeRspackPlugin.ts new file mode 100644 index 0000000..ef21506 --- /dev/null +++ b/.config/rspack/BuildModeRspackPlugin.ts @@ -0,0 +1,36 @@ +import * as webpack from 'webpack'; + +const PLUGIN_NAME = 'BuildModeRspackPlugin'; + +export class BuildModeRspackPlugin { + apply(compiler: webpack.Compiler) { + compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => { + compilation.hooks.processAssets.tap( + { + name: PLUGIN_NAME, + stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, + }, + (assets) => { + const assetName = 'plugin.json'; + const asset = assets[assetName]; + if (!asset) { + return; + } + + const { RawSource } = compiler.webpack.sources; + const pluginJsonContent = JSON.parse(asset.source().toString()); + const pluginJsonWithBuildMode = JSON.stringify( + { + ...pluginJsonContent, + buildMode: compilation.options.mode, + }, + null, + 4 + ); + const source = new RawSource(pluginJsonWithBuildMode); + compilation.updateAsset(assetName, source); + } + ); + }); + } +} diff --git a/.config/rspack/constants.ts b/.config/rspack/constants.ts new file mode 100644 index 0000000..071e4fd --- /dev/null +++ b/.config/rspack/constants.ts @@ -0,0 +1,2 @@ +export const SOURCE_DIR = 'src'; +export const DIST_DIR = 'dist'; diff --git a/.config/rspack/liveReloadPlugin.js b/.config/rspack/liveReloadPlugin.js new file mode 100644 index 0000000..c6cf523 --- /dev/null +++ b/.config/rspack/liveReloadPlugin.js @@ -0,0 +1,110 @@ +const path = require('path'); +const WebSocket = require('ws'); +const http = require('http'); + +class RspackLiveReloadPlugin { + constructor(options = {}) { + this.options = Object.assign( + { + port: 35729, + delay: 0, + appendScriptTag: true, + protocol: 'http', + }, + options + ); + } + + apply(compiler) { + const isRspack = compiler.rspack !== undefined; + if (!isRspack) { + throw new Error('This plugin is designed to work with Rspack 1'); + } + + compiler.hooks.afterEmit.tap('RspackLiveReloadPlugin', (compilation) => { + this._startServer(); + this._notifyClient(); + }); + + compiler.hooks.done.tap('RspackLiveReloadPlugin', (stats) => { + if (this.options.appendScriptTag) { + this._injectLiveReloadScript(stats.compilation); + } + }); + } + + _startServer() { + if (this.server) { + return; + } + + const port = this.options.port; + + this.httpServer = http.createServer((req, res) => { + if (req.url === '/livereload.js') { + res.writeHead(200, { 'Content-Type': 'application/javascript' }); + res.end(this._getLiveReloadScript()); + } else { + res.writeHead(404); + res.end('Not Found'); + } + }); + + this.server = new WebSocket.Server({ server: this.httpServer }); + this.httpServer.listen(port, () => { + console.log(`LiveReload server started on http://localhost:${port}`); + }); + } + + _notifyClient() { + if (!this.server) { + return; + } + + this.server.clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + client.send(JSON.stringify({ action: 'reload' })); + } + }); + } + + _injectLiveReloadScript(compilation) { + compilation.hooks.processAssets.tap( + { + name: 'RspackLiveReloadPlugin', + stage: compilation.PROCESS_ASSETS_STAGE_ADDITIONAL, + }, + (assets) => { + Object.keys(assets).forEach((filename) => { + if (path.extname(filename) === '.html') { + const assetSource = compilation.getAsset(filename).source; + const updatedSource = assetSource + .source() + .replace('', ``); + compilation.updateAsset(filename, { + source: () => updatedSource, + size: () => updatedSource.length, + }); + } + }); + } + ); + } + + _getLiveReloadScript() { + return ` + (function() { + if (typeof WebSocket === 'undefined') return; + const ws = new WebSocket('${this.options.protocol}://localhost:${this.options.port}'); + ws.onmessage = function(event) { + const data = JSON.parse(event.data); + if (data.action === 'reload') { + window.location.reload(); + } + }; + })(); + `; + } +} + +module.exports = RspackLiveReloadPlugin; diff --git a/.config/rspack/rspack.config.ts b/.config/rspack/rspack.config.ts new file mode 100644 index 0000000..7d311f2 --- /dev/null +++ b/.config/rspack/rspack.config.ts @@ -0,0 +1,233 @@ +/* + * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ + * + * In order to extend the configuration follow the steps in + * https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations#extend-the-webpack-config + */ + +import rspack, { type Configuration } from '@rspack/core'; +import ESLintPlugin from 'eslint-webpack-plugin'; +import { TsCheckerRspackPlugin } from 'ts-checker-rspack-plugin'; +import path from 'path'; +import ReplaceInFileWebpackPlugin from 'replace-in-file-webpack-plugin'; +import TerserPlugin from 'terser-webpack-plugin'; +import { RspackVirtualModulePlugin } from 'rspack-plugin-virtual-module'; +import RspackLiveReloadPlugin from './liveReloadPlugin'; +import { BuildModeRspackPlugin } from './BuildModeRspackPlugin'; +import { DIST_DIR, SOURCE_DIR } from './constants'; +import { getCPConfigVersion, getEntries, getPackageJson, getPluginJson, hasReadme, isWSL } from './utils'; +import { externals } from '../bundler/externals'; + +const { SubresourceIntegrityPlugin } = rspack.experiments; +const pluginJson = getPluginJson(); +const cpVersion = getCPConfigVersion(); + +const virtualPublicPath = new RspackVirtualModulePlugin({ + 'grafana-public-path': ` +import amdMetaModule from 'amd-module'; + +__webpack_public_path__ = + amdMetaModule && amdMetaModule.uri + ? amdMetaModule.uri.slice(0, amdMetaModule.uri.lastIndexOf('/') + 1) + : 'public/plugins/${pluginJson.id}/'; +`, +}); + +const config = async (env): Promise => { + const baseConfig: Configuration = { + context: path.join(process.cwd(), SOURCE_DIR), + + devtool: env.production ? 'source-map' : 'eval-source-map', + + entry: await getEntries(), + + externals, + + // Support WebAssembly according to latest spec - makes WebAssembly module async + experiments: { + asyncWebAssembly: true, + }, + + mode: env.production ? 'production' : 'development', + + module: { + rules: [ + // This must come first in the rules array otherwise it breaks sourcemaps. + { + test: /src\/(?:.*\/)?module\.tsx?$/, + use: [ + { + loader: 'imports-loader', + options: { + imports: `side-effects grafana-public-path`, + }, + }, + ], + }, + { + exclude: /(node_modules)/, + test: /\.[tj]sx?$/, + use: { + loader: 'builtin:swc-loader', + options: { + jsc: { + externalHelpers: true, + parser: { + syntax: 'typescript', + tsx: true, + }, + transform: { + react: { + development: !env.production, + refresh: false, + }, + }, + target: 'es2022', + }, + }, + }, + type: 'javascript/auto', + }, + { + test: /\.css$/, + use: ['style-loader', 'css-loader'], + }, + { + test: /\.s[ac]ss$/, + use: ['style-loader', 'css-loader', 'sass-loader'], + }, + { + test: /\.(png|jpe?g|gif|svg)$/, + type: 'asset/resource', + generator: { + filename: Boolean(env.production) ? '[hash][ext]' : '[file]', + }, + }, + { + test: /\.(woff|woff2|eot|ttf|otf)(\?v=\d+\.\d+\.\d+)?$/, + type: 'asset/resource', + generator: { + filename: Boolean(env.production) ? '[hash][ext]' : '[file]', + }, + }, + ], + }, + + optimization: { + minimize: Boolean(env.production), + minimizer: [ + new TerserPlugin({ + terserOptions: { + format: { + comments: (_, { type, value }) => type === 'comment2' && value.trim().startsWith('[create-plugin]'), + }, + compress: { + drop_console: ['log', 'info'], + }, + }, + }), + ], + }, + + output: { + clean: { + keep: new RegExp(`(.*?_(amd64|arm(64)?)(.exe)?|go_plugin_build_manifest)`), + }, + filename: '[name].js', + chunkFilename: env.production ? '[name].js?_cache=[contenthash]' : '[name].js', + library: { + type: 'amd', + }, + path: path.resolve(process.cwd(), DIST_DIR), + publicPath: `public/plugins/${pluginJson.id}/`, + uniqueName: pluginJson.id, + crossOriginLoading: 'anonymous', + }, + + plugins: [ + new BuildModeRspackPlugin(), + virtualPublicPath, + // Insert create plugin version information into the bundle + new rspack.BannerPlugin({ + banner: '/* [create-plugin] version: ' + cpVersion + ' */', + raw: true, + entryOnly: true, + }), + new rspack.CopyRspackPlugin({ + patterns: [ + // If src/README.md exists use it; otherwise the root README + // To `compiler.options.output` + { from: hasReadme() ? 'README.md' : '../README.md', to: '.', force: true }, + { from: 'plugin.json', to: '.' }, + { from: '../LICENSE', to: '.' }, + { from: '../CHANGELOG.md', to: '.', force: true }, + { from: '**/*.json', to: '.' }, // TODO + { from: '**/*.svg', to: '.', noErrorOnMissing: true }, // Optional + { from: '**/*.png', to: '.', noErrorOnMissing: true }, // Optional + { from: '**/*.html', to: '.', noErrorOnMissing: true }, // Optional + { from: 'img/**/*', to: '.', noErrorOnMissing: true }, // Optional + { from: 'libs/**/*', to: '.', noErrorOnMissing: true }, // Optional + { from: 'static/**/*', to: '.', noErrorOnMissing: true }, // Optional + { from: '**/query_help.md', to: '.', noErrorOnMissing: true }, // Optional + ], + }), + // Replace certain template-variables in the README and plugin.json + new ReplaceInFileWebpackPlugin([ + { + dir: DIST_DIR, + files: ['plugin.json', 'README.md'], + rules: [ + { + search: /\%VERSION\%/g, + replace: getPackageJson().version, + }, + { + search: /\%TODAY\%/g, + replace: new Date().toISOString().substring(0, 10), + }, + { + search: /\%PLUGIN_ID\%/g, + replace: pluginJson.id, + }, + ], + }, + ]), + new SubresourceIntegrityPlugin({ + hashFuncNames: ["sha256"], + }), + ...(env.development + ? [ + new RspackLiveReloadPlugin(), + new TsCheckerRspackPlugin({ + async: Boolean(env.development), + issue: { + include: [{ file: '**/*.{ts,tsx}' }], + }, + typescript: { configFile: path.join(process.cwd(), 'tsconfig.json') }, + }), + new ESLintPlugin({ + extensions: ['.ts', '.tsx'], + lintDirtyModulesOnly: Boolean(env.development), // don't lint on start, only lint changed files + }), + ] + : []), + ], + + resolve: { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + // handle resolving "rootDir" paths + modules: [path.resolve(process.cwd(), 'src'), 'node_modules'], + }, + }; + + if (isWSL()) { + baseConfig.watchOptions = { + poll: 3000, + ignored: /node_modules/, + }; + } + + return baseConfig; +}; + +export default config; diff --git a/.config/rspack/utils.ts b/.config/rspack/utils.ts new file mode 100644 index 0000000..015aa05 --- /dev/null +++ b/.config/rspack/utils.ts @@ -0,0 +1,63 @@ +import fs from 'fs'; +import process from 'process'; +import os from 'os'; +import path from 'path'; +import { glob } from 'glob'; +import { SOURCE_DIR } from './constants'; + +export function isWSL() { + if (process.platform !== 'linux') { + return false; + } + + if (os.release().toLowerCase().includes('microsoft')) { + return true; + } + + try { + return fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft'); + } catch { + return false; + } +} + +export function getPackageJson() { + return require(path.resolve(process.cwd(), 'package.json')); +} + +export function getPluginJson() { + return require(path.resolve(process.cwd(), `${SOURCE_DIR}/plugin.json`)); +} + +export function getCPConfigVersion() { + const cprcJson = path.resolve(__dirname, '../', '.cprc.json'); + return fs.existsSync(cprcJson) ? require(cprcJson).version : { version: 'unknown' }; +} + +export function hasReadme() { + return fs.existsSync(path.resolve(process.cwd(), SOURCE_DIR, 'README.md')); +} + +// Support bundling nested plugins by finding all plugin.json files in src directory +// then checking for a sibling module.[jt]sx? file. +export async function getEntries(): Promise> { + const pluginsJson = await glob('**/src/**/plugin.json', { absolute: true }); + + const plugins = await Promise.all( + pluginsJson.map((pluginJson) => { + const folder = path.dirname(pluginJson); + return glob(`${folder}/module.{ts,tsx,js,jsx}`, { absolute: true }); + }) + ); + + return plugins.reduce((result, modules) => { + return modules.reduce((result, module) => { + const pluginPath = path.dirname(module); + const pluginName = path.relative(process.cwd(), pluginPath).replace(/src\/?/i, ''); + const entryName = pluginName === '' ? 'module' : `${pluginName}/module`; + + result[entryName] = module; + return result; + }, result); + }, {}); +} diff --git a/.config/supervisord/supervisord.conf b/.config/supervisord/supervisord.conf new file mode 100644 index 0000000..47624f0 --- /dev/null +++ b/.config/supervisord/supervisord.conf @@ -0,0 +1,15 @@ +[supervisord] +nodaemon=true +user=root + +[program:grafana] +user=root +directory=/var/lib/grafana +command=/run.sh +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +redirect_stderr=true +killasgroup=true +stopasgroup=true +autostart=true + diff --git a/.config/tsconfig.json b/.config/tsconfig.json index 64b3769..c0fc6d4 100644 --- a/.config/tsconfig.json +++ b/.config/tsconfig.json @@ -2,9 +2,9 @@ * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ * * In order to extend the configuration follow the steps in - * https://grafana.github.io/plugin-tools/docs/advanced-configuration#extending-the-typescript-config + * https://grafana.com/developers/plugin-tools/how-to-guides/extend-configurations#extend-the-typescript-config */ - { +{ "compilerOptions": { "alwaysStrict": true, "declaration": false, diff --git a/.config/types/bundler-rules.d.ts b/.config/types/bundler-rules.d.ts new file mode 100644 index 0000000..e67197c --- /dev/null +++ b/.config/types/bundler-rules.d.ts @@ -0,0 +1,37 @@ +// Image declarations +declare module '*.gif' { + const src: string; + export default src; +} + +declare module '*.jpg' { + const src: string; + export default src; +} + +declare module '*.jpeg' { + const src: string; + export default src; +} + +declare module '*.png' { + const src: string; + export default src; +} + +declare module '*.webp' { + const src: string; + export default src; +} + +declare module '*.svg' { + const src: string; + export default src; +} + +// Font declarations +declare module '*.woff'; +declare module '*.woff2'; +declare module '*.eot'; +declare module '*.ttf'; +declare module '*.otf'; diff --git a/.config/types/webpack-plugins.d.ts b/.config/types/webpack-plugins.d.ts new file mode 100644 index 0000000..6dbab10 --- /dev/null +++ b/.config/types/webpack-plugins.d.ts @@ -0,0 +1,83 @@ +declare module 'replace-in-file-webpack-plugin' { + import { Compiler, Plugin } from 'webpack'; + + interface ReplaceRule { + search: string | RegExp; + replace: string | ((match: string) => string); + } + + interface ReplaceOption { + dir?: string; + files?: string[]; + test?: RegExp | RegExp[]; + rules: ReplaceRule[]; + } + + class ReplaceInFilePlugin extends Plugin { + constructor(options?: ReplaceOption[]); + options: ReplaceOption[]; + apply(compiler: Compiler): void; + } + + export = ReplaceInFilePlugin; +} + +declare module 'webpack-livereload-plugin' { + import { ServerOptions } from 'https'; + import { Compiler, Plugin, Stats, Compilation } from 'webpack'; + + interface Options extends Pick { + /** + * protocol for livereload `