mirror of https://github.com/grafana/grafana
Chore: Delete Input Datasource (#83163)
* chore(input-datasource): delete bundled plugin for grafana 11 * chore(betterer): refresh results file * chore(yarn): run dedupe to clean up deps * chore(yarn): pin playwright to 1.41.2 to see if CI passes * chore(yarn): pin playwright to 1.42.1pull/76039/head^2
parent
6204f1e847
commit
1de4187a6e
@ -1 +0,0 @@ |
||||
.yarn |
@ -1,3 +0,0 @@ |
||||
# Direct Input Data Source - Bundled Plugin |
||||
|
||||
This data source lets you define results directly in CSV. The values are stored either in a shared data source, or directly in panels. |
@ -1 +0,0 @@ |
||||
module.exports = {}; |
@ -1,18 +0,0 @@ |
||||
module.exports = { |
||||
testEnvironment: 'jest-environment-jsdom', |
||||
preset: 'ts-jest', |
||||
extensionsToTreatAsEsm: ['.ts'], |
||||
transform: { |
||||
'^.+\\.(t|j)sx?$': [ |
||||
'ts-jest', |
||||
{ |
||||
useESM: true, |
||||
isolatedModules: true, |
||||
allowJs: true, |
||||
}, |
||||
], |
||||
}, |
||||
moduleNameMapper: { |
||||
'^d3-interpolate$': '<rootDir>/__mocks__/d3-interpolate.ts', |
||||
}, |
||||
}; |
@ -1,36 +0,0 @@ |
||||
{ |
||||
"name": "@grafana-plugins/input-datasource", |
||||
"version": "11.0.0-pre", |
||||
"description": "Input Datasource", |
||||
"private": true, |
||||
"repository": { |
||||
"type": "git", |
||||
"url": "http://github.com/grafana/grafana.git" |
||||
}, |
||||
"scripts": { |
||||
"build": "yarn test && webpack -c webpack.config.ts --env production", |
||||
"dev": "webpack -w -c webpack.config.ts --env development", |
||||
"test": "jest -c jest.config.js" |
||||
}, |
||||
"author": "Grafana Labs", |
||||
"devDependencies": { |
||||
"@grafana/tsconfig": "^1.3.0-rc1", |
||||
"@types/jest": "26.0.15", |
||||
"@types/react": "18.0.28", |
||||
"copy-webpack-plugin": "11.0.0", |
||||
"eslint-webpack-plugin": "4.0.0", |
||||
"fork-ts-checker-webpack-plugin": "8.0.0", |
||||
"jest": "29.3.1", |
||||
"jest-environment-jsdom": "29.3.1", |
||||
"swc-loader": "0.2.3", |
||||
"ts-jest": "29.0.5", |
||||
"ts-node": "10.9.2", |
||||
"webpack": "5.76.0" |
||||
}, |
||||
"dependencies": { |
||||
"@grafana/data": "11.0.0-pre", |
||||
"@grafana/ui": "11.0.0-pre", |
||||
"react": "18.2.0", |
||||
"tslib": "2.5.0" |
||||
} |
||||
} |
@ -1,69 +0,0 @@ |
||||
// Libraries
|
||||
import React, { PureComponent } from 'react'; |
||||
|
||||
// Types
|
||||
import { DataSourcePluginOptionsEditorProps, DataFrame, MutableDataFrame } from '@grafana/data'; |
||||
import { TableInputCSV } from '@grafana/ui'; |
||||
|
||||
import { InputOptions } from './types'; |
||||
import { dataFrameToCSV } from './utils'; |
||||
|
||||
interface Props extends DataSourcePluginOptionsEditorProps<InputOptions> {} |
||||
|
||||
interface State { |
||||
text: string; |
||||
} |
||||
|
||||
export class InputConfigEditor extends PureComponent<Props, State> { |
||||
state = { |
||||
text: '', |
||||
}; |
||||
|
||||
componentDidMount() { |
||||
const { options } = this.props; |
||||
if (options.jsonData.data) { |
||||
const text = dataFrameToCSV(options.jsonData.data); |
||||
this.setState({ text }); |
||||
} |
||||
} |
||||
|
||||
onSeriesParsed = (data: DataFrame[], text: string) => { |
||||
const { options, onOptionsChange } = this.props; |
||||
if (!data) { |
||||
data = [new MutableDataFrame()]; |
||||
} |
||||
// data is a property on 'jsonData'
|
||||
const jsonData = { |
||||
...options.jsonData, |
||||
data, |
||||
}; |
||||
|
||||
onOptionsChange({ |
||||
...options, |
||||
jsonData, |
||||
}); |
||||
this.setState({ text }); |
||||
}; |
||||
|
||||
render() { |
||||
const { text } = this.state; |
||||
return ( |
||||
<div> |
||||
<div className="gf-form-group"> |
||||
<h4>Shared Data:</h4> |
||||
<span>Enter CSV</span> |
||||
<TableInputCSV text={text} onSeriesParsed={this.onSeriesParsed} width={'100%'} height={200} /> |
||||
</div> |
||||
|
||||
<div className="grafana-info-box"> |
||||
This data is stored in the datasource json and is returned to every user in the initial request for any |
||||
datasource. This is an appropriate place to enter a few values. Large datasets will perform better in other |
||||
datasources. |
||||
<br /> |
||||
<br /> |
||||
<b>NOTE:</b> Changes to this data will only be reflected after a browser refresh. |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
} |
@ -1,58 +0,0 @@ |
||||
import { |
||||
DataFrame, |
||||
DataFrameDTO, |
||||
DataSourceInstanceSettings, |
||||
MutableDataFrame, |
||||
PluginMeta, |
||||
readCSV, |
||||
} from '@grafana/data'; |
||||
|
||||
import InputDatasource, { describeDataFrame } from './InputDatasource'; |
||||
import { getQueryOptions } from './testHelpers'; |
||||
import { InputOptions, InputQuery } from './types'; |
||||
|
||||
describe('InputDatasource', () => { |
||||
const data = readCSV('a,b,c\n1,2,3\n4,5,6'); |
||||
const instanceSettings: DataSourceInstanceSettings<InputOptions> = { |
||||
id: 1, |
||||
uid: 'xxx', |
||||
type: 'x', |
||||
name: 'xxx', |
||||
meta: {} as PluginMeta, |
||||
access: 'proxy', |
||||
readOnly: false, |
||||
jsonData: { |
||||
data, |
||||
}, |
||||
}; |
||||
|
||||
describe('when querying', () => { |
||||
test('should return the saved data with a query', () => { |
||||
const ds = new InputDatasource(instanceSettings); |
||||
const options = getQueryOptions<InputQuery>({ |
||||
targets: [{ refId: 'Z' }], |
||||
}); |
||||
|
||||
return ds.query(options).then((rsp) => { |
||||
expect(rsp.data.length).toBe(1); |
||||
|
||||
const series: DataFrame = rsp.data[0]; |
||||
expect(series.refId).toBe('Z'); |
||||
expect(series.fields[0].values).toEqual(data[0].fields[0].values); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
test('DataFrame descriptions', () => { |
||||
expect(describeDataFrame([])).toEqual(''); |
||||
expect(describeDataFrame(null as unknown as Array<DataFrameDTO | DataFrame>)).toEqual(''); |
||||
expect( |
||||
describeDataFrame([ |
||||
new MutableDataFrame({ |
||||
name: 'x', |
||||
fields: [{ name: 'a' }], |
||||
}), |
||||
]) |
||||
).toEqual('1 Fields, 0 Rows'); |
||||
}); |
||||
}); |
@ -1,124 +0,0 @@ |
||||
// Types
|
||||
import { |
||||
DataQueryRequest, |
||||
DataQueryResponse, |
||||
TestDataSourceResponse, |
||||
DataSourceApi, |
||||
DataSourceInstanceSettings, |
||||
MetricFindValue, |
||||
DataFrame, |
||||
DataFrameDTO, |
||||
toDataFrame, |
||||
} from '@grafana/data'; |
||||
|
||||
import { InputQuery, InputOptions } from './types'; |
||||
|
||||
export class InputDatasource extends DataSourceApi<InputQuery, InputOptions> { |
||||
data: DataFrame[] = []; |
||||
|
||||
constructor(instanceSettings: DataSourceInstanceSettings<InputOptions>) { |
||||
super(instanceSettings); |
||||
|
||||
if (instanceSettings.jsonData.data) { |
||||
this.data = instanceSettings.jsonData.data.map((v) => toDataFrame(v)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Convert a query to a simple text string |
||||
*/ |
||||
getQueryDisplayText(query: InputQuery): string { |
||||
if (query.data) { |
||||
return 'Panel Data: ' + describeDataFrame(query.data); |
||||
} |
||||
return `Shared Data From: ${this.name} (${describeDataFrame(this.data)})`; |
||||
} |
||||
|
||||
metricFindQuery(query: string, options?: any): Promise<MetricFindValue[]> { |
||||
return new Promise((resolve, reject) => { |
||||
const names = []; |
||||
for (const series of this.data) { |
||||
for (const field of series.fields) { |
||||
// TODO, match query/options?
|
||||
names.push({ |
||||
text: field.name, |
||||
}); |
||||
} |
||||
} |
||||
resolve(names); |
||||
}); |
||||
} |
||||
|
||||
query(options: DataQueryRequest<InputQuery>): Promise<DataQueryResponse> { |
||||
const results: DataFrame[] = []; |
||||
for (const query of options.targets) { |
||||
if (query.hide) { |
||||
continue; |
||||
} |
||||
let data = this.data; |
||||
if (query.data) { |
||||
data = query.data.map((v) => toDataFrame(v)); |
||||
} |
||||
for (let i = 0; i < data.length; i++) { |
||||
results.push({ |
||||
...data[i], |
||||
refId: query.refId, |
||||
}); |
||||
} |
||||
} |
||||
return Promise.resolve({ data: results }); |
||||
} |
||||
|
||||
testDatasource(): Promise<TestDataSourceResponse> { |
||||
return new Promise((resolve, reject) => { |
||||
let rowCount = 0; |
||||
let info = `${this.data.length} Series:`; |
||||
for (const series of this.data) { |
||||
const length = series.length; |
||||
info += ` [${series.fields.length} Fields, ${length} Rows]`; |
||||
rowCount += length; |
||||
} |
||||
|
||||
if (rowCount > 0) { |
||||
resolve({ |
||||
status: 'success', |
||||
message: info, |
||||
}); |
||||
} |
||||
reject({ |
||||
status: 'error', |
||||
message: 'No Data Entered', |
||||
}); |
||||
}); |
||||
} |
||||
} |
||||
|
||||
function getLength(data?: DataFrameDTO | DataFrame) { |
||||
if (!data || !data.fields || !data.fields.length) { |
||||
return 0; |
||||
} |
||||
if ('length' in data) { |
||||
return data.length; |
||||
} |
||||
return data.fields[0].values!.length; |
||||
} |
||||
|
||||
export function describeDataFrame(data: Array<DataFrameDTO | DataFrame>): string { |
||||
if (!data || !data.length) { |
||||
return ''; |
||||
} |
||||
if (data.length > 1) { |
||||
const count = data.reduce((acc, series) => { |
||||
return acc + getLength(series); |
||||
}, 0); |
||||
return `${data.length} Series, ${count} Rows`; |
||||
} |
||||
const series = data[0]; |
||||
if (!series.fields) { |
||||
return 'Missing Fields'; |
||||
} |
||||
const length = getLength(series); |
||||
return `${series.fields.length} Fields, ${length} Rows`; |
||||
} |
||||
|
||||
export default InputDatasource; |
@ -1,86 +0,0 @@ |
||||
// Libraries
|
||||
import React, { PureComponent } from 'react'; |
||||
|
||||
// Types
|
||||
import { DataFrame, toCSV, SelectableValue, MutableDataFrame, QueryEditorProps } from '@grafana/data'; |
||||
import { Select, TableInputCSV, LinkButton, Icon, InlineField } from '@grafana/ui'; |
||||
|
||||
import { InputDatasource, describeDataFrame } from './InputDatasource'; |
||||
import { InputQuery, InputOptions } from './types'; |
||||
import { dataFrameToCSV } from './utils'; |
||||
|
||||
type Props = QueryEditorProps<InputDatasource, InputQuery, InputOptions>; |
||||
|
||||
const options = [ |
||||
{ value: 'panel', label: 'Panel', description: 'Save data in the panel configuration.' }, |
||||
{ value: 'shared', label: 'Shared', description: 'Save data in the shared datasource object.' }, |
||||
]; |
||||
|
||||
interface State { |
||||
text: string; |
||||
} |
||||
|
||||
export class InputQueryEditor extends PureComponent<Props, State> { |
||||
state = { |
||||
text: '', |
||||
}; |
||||
|
||||
onComponentDidMount() { |
||||
const { query } = this.props; |
||||
const text = dataFrameToCSV(query.data); |
||||
this.setState({ text }); |
||||
} |
||||
|
||||
onSourceChange = (item: SelectableValue<string>) => { |
||||
const { datasource, query, onChange, onRunQuery } = this.props; |
||||
let data: DataFrame[] | undefined = undefined; |
||||
if (item.value === 'panel') { |
||||
if (query.data) { |
||||
return; |
||||
} |
||||
data = [...datasource.data]; |
||||
if (!data) { |
||||
data = [new MutableDataFrame()]; |
||||
} |
||||
this.setState({ text: toCSV(data) }); |
||||
} |
||||
onChange({ ...query, data }); |
||||
onRunQuery(); |
||||
}; |
||||
|
||||
onSeriesParsed = (data: DataFrame[], text: string) => { |
||||
const { query, onChange, onRunQuery } = this.props; |
||||
this.setState({ text }); |
||||
if (!data) { |
||||
data = [new MutableDataFrame()]; |
||||
} |
||||
onChange({ ...query, data }); |
||||
onRunQuery(); |
||||
}; |
||||
|
||||
render() { |
||||
const { datasource, query } = this.props; |
||||
const { uid, name } = datasource; |
||||
const { text } = this.state; |
||||
|
||||
const selected = query.data ? options[0] : options[1]; |
||||
return ( |
||||
<div> |
||||
<InlineField label="Data" labelWidth={8}> |
||||
<> |
||||
<Select width={20} options={options} value={selected} onChange={this.onSourceChange} /> |
||||
{query.data ? ( |
||||
<div style={{ alignSelf: 'center' }}>{describeDataFrame(query.data)}</div> |
||||
) : ( |
||||
<LinkButton fill="text" href={`datasources/edit/${uid}/`}> |
||||
{name}: {describeDataFrame(datasource.data)} |
||||
<Icon name="pen" /> |
||||
</LinkButton> |
||||
)} |
||||
</> |
||||
</InlineField> |
||||
{query.data && <TableInputCSV text={text} onSeriesParsed={this.onSeriesParsed} width={'100%'} height={200} />} |
||||
</div> |
||||
); |
||||
} |
||||
} |
Before Width: | Height: | Size: 1.3 KiB |
@ -1,10 +0,0 @@ |
||||
import { DataSourcePlugin } from '@grafana/data'; |
||||
|
||||
import { InputConfigEditor } from './InputConfigEditor'; |
||||
import { InputDatasource } from './InputDatasource'; |
||||
import { InputQueryEditor } from './InputQueryEditor'; |
||||
import { InputOptions, InputQuery } from './types'; |
||||
|
||||
export const plugin = new DataSourcePlugin<InputDatasource, InputQuery, InputOptions>(InputDatasource) |
||||
.setConfigEditor(InputConfigEditor) |
||||
.setQueryEditor(InputQueryEditor); |
@ -1,21 +0,0 @@ |
||||
{ |
||||
"type": "datasource", |
||||
"name": "Direct Input", |
||||
"id": "input", |
||||
"state": "alpha", |
||||
|
||||
"metrics": true, |
||||
|
||||
"info": { |
||||
"version": "1.0.0", |
||||
"description": "Data source that supports manual table & CSV input", |
||||
"author": { |
||||
"name": "Grafana Labs", |
||||
"url": "https://grafana.com" |
||||
}, |
||||
"logos": { |
||||
"small": "img/input.svg", |
||||
"large": "img/input.svg" |
||||
} |
||||
} |
||||
} |
@ -1,27 +0,0 @@ |
||||
import { DataQueryRequest, DataQuery, CoreApp, dateTime } from '@grafana/data'; |
||||
|
||||
export function getQueryOptions<TQuery extends DataQuery>( |
||||
options: Partial<DataQueryRequest<TQuery>> |
||||
): DataQueryRequest<TQuery> { |
||||
const raw = { from: 'now', to: 'now-1h' }; |
||||
const range = { from: dateTime(), to: dateTime(), raw: raw }; |
||||
|
||||
const defaults: DataQueryRequest<TQuery> = { |
||||
requestId: 'TEST', |
||||
app: CoreApp.Dashboard, |
||||
range: range, |
||||
targets: [], |
||||
scopedVars: {}, |
||||
timezone: 'browser', |
||||
panelId: 1, |
||||
dashboardUID: 'test-uid-1', |
||||
interval: '60s', |
||||
intervalMs: 60000, |
||||
maxDataPoints: 500, |
||||
startTime: 0, |
||||
}; |
||||
|
||||
Object.assign(defaults, options); |
||||
|
||||
return defaults; |
||||
} |
@ -1,11 +0,0 @@ |
||||
import { DataQuery, DataSourceJsonData, DataFrameDTO } from '@grafana/data'; |
||||
|
||||
export interface InputQuery extends DataQuery { |
||||
// Data saved in the panel
|
||||
data?: DataFrameDTO[]; |
||||
} |
||||
|
||||
export interface InputOptions extends DataSourceJsonData { |
||||
// Saved in the datasource and download with bootData
|
||||
data?: DataFrameDTO[]; |
||||
} |
@ -1,8 +0,0 @@ |
||||
import { toDataFrame, DataFrameDTO, toCSV } from '@grafana/data'; |
||||
|
||||
export function dataFrameToCSV(dto?: DataFrameDTO[]) { |
||||
if (!dto || !dto.length) { |
||||
return ''; |
||||
} |
||||
return toCSV(dto.map((v) => toDataFrame(v))); |
||||
} |
@ -1,17 +0,0 @@ |
||||
{ |
||||
"extends": "@grafana/tsconfig", |
||||
"include": ["src", "types"], |
||||
"compilerOptions": { |
||||
"declaration": false, |
||||
"rootDir": "./src", |
||||
"baseUrl": "./src" |
||||
}, |
||||
"ts-node": { |
||||
"compilerOptions": { |
||||
"module": "commonjs", |
||||
"target": "es5", |
||||
"esModuleInterop": true |
||||
}, |
||||
"transpileOnly": true |
||||
} |
||||
} |
@ -1,159 +0,0 @@ |
||||
import CopyWebpackPlugin from 'copy-webpack-plugin'; |
||||
import ESLintPlugin from 'eslint-webpack-plugin'; |
||||
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; |
||||
import path from 'path'; |
||||
import { Configuration } from 'webpack'; |
||||
|
||||
const SOURCE_DIR = path.resolve(__dirname, 'src'); |
||||
const DIST_DIR = path.resolve(__dirname, 'dist'); |
||||
const PLUGIN_ID = require(path.join(SOURCE_DIR, 'plugin.json')).id; |
||||
|
||||
const config = async (env: Record<string, string>): Promise<Configuration> => ({ |
||||
cache: { |
||||
type: 'filesystem', |
||||
buildDependencies: { |
||||
config: [__filename], |
||||
}, |
||||
}, |
||||
|
||||
context: path.join(process.cwd(), SOURCE_DIR), |
||||
|
||||
devtool: env.production ? 'source-map' : 'eval-source-map', |
||||
|
||||
entry: { |
||||
module: path.join(SOURCE_DIR, 'module.ts'), |
||||
}, |
||||
|
||||
externals: [ |
||||
'lodash', |
||||
'jquery', |
||||
'moment', |
||||
'slate', |
||||
'emotion', |
||||
'@emotion/react', |
||||
'@emotion/css', |
||||
'prismjs', |
||||
'slate-plain-serializer', |
||||
'@grafana/slate-react', |
||||
'react', |
||||
'react-dom', |
||||
'react-redux', |
||||
'redux', |
||||
'rxjs', |
||||
'react-router', |
||||
'react-router-dom', |
||||
'd3', |
||||
'angular', |
||||
'@grafana/ui', |
||||
'@grafana/runtime', |
||||
'@grafana/data', |
||||
|
||||
// Mark legacy SDK imports as external if their name starts with the "grafana/" prefix
|
||||
({ request }, callback) => { |
||||
const prefix = 'grafana/'; |
||||
const hasPrefix = (request: string) => request.indexOf(prefix) === 0; |
||||
const stripPrefix = (request: string) => request.substring(prefix.length); |
||||
|
||||
if (request && hasPrefix(request)) { |
||||
return callback(undefined, stripPrefix(request)); |
||||
} |
||||
|
||||
callback(); |
||||
}, |
||||
], |
||||
|
||||
mode: env.production ? 'production' : 'development', |
||||
|
||||
module: { |
||||
rules: [ |
||||
{ |
||||
exclude: /(node_modules)/, |
||||
test: /\.[tj]sx?$/, |
||||
use: { |
||||
loader: 'swc-loader', |
||||
options: { |
||||
jsc: { |
||||
baseUrl: path.resolve(__dirname), |
||||
target: 'es2015', |
||||
loose: false, |
||||
parser: { |
||||
syntax: 'typescript', |
||||
tsx: true, |
||||
decorators: false, |
||||
dynamicImport: true, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
{ |
||||
test: /\.(png|jpe?g|gif|svg)$/, |
||||
type: 'asset/resource', |
||||
generator: { |
||||
// Keep publicPath relative for host.com/grafana/ deployments
|
||||
publicPath: `public/plugins/${PLUGIN_ID}/img/`, |
||||
outputPath: 'img/', |
||||
filename: Boolean(env.production) ? '[hash][ext]' : '[name][ext]', |
||||
}, |
||||
}, |
||||
{ |
||||
test: /\.(woff|woff2|eot|ttf|otf)(\?v=\d+\.\d+\.\d+)?$/, |
||||
type: 'asset/resource', |
||||
generator: { |
||||
// Keep publicPath relative for host.com/grafana/ deployments
|
||||
publicPath: `public/plugins/${PLUGIN_ID}/fonts`, |
||||
outputPath: 'fonts/', |
||||
filename: Boolean(env.production) ? '[hash][ext]' : '[name][ext]', |
||||
}, |
||||
}, |
||||
], |
||||
}, |
||||
|
||||
output: { |
||||
clean: { |
||||
keep: new RegExp(`.*?_(amd64|arm(64)?)(.exe)?`), |
||||
}, |
||||
filename: '[name].js', |
||||
library: { |
||||
type: 'amd', |
||||
}, |
||||
path: DIST_DIR, |
||||
publicPath: '/', |
||||
}, |
||||
|
||||
plugins: [ |
||||
new CopyWebpackPlugin({ |
||||
patterns: [ |
||||
{ from: '../README.md', to: '.', force: true, context: SOURCE_DIR }, |
||||
{ from: 'plugin.json', to: '.', context: SOURCE_DIR }, |
||||
{ from: '**/*.json', to: '.', context: SOURCE_DIR }, |
||||
{ from: '**/*.svg', to: '.', noErrorOnMissing: true, context: SOURCE_DIR }, // Optional
|
||||
{ from: '**/*.png', to: '.', noErrorOnMissing: true, context: SOURCE_DIR }, // Optional
|
||||
{ from: '**/*.html', to: '.', noErrorOnMissing: true, context: SOURCE_DIR }, // Optional
|
||||
{ from: 'img/**/*', to: '.', noErrorOnMissing: true, context: SOURCE_DIR }, // Optional
|
||||
{ from: 'libs/**/*', to: '.', noErrorOnMissing: true, context: SOURCE_DIR }, // Optional
|
||||
{ from: 'static/**/*', to: '.', noErrorOnMissing: true, context: SOURCE_DIR }, // Optional
|
||||
], |
||||
}), |
||||
new ForkTsCheckerWebpackPlugin({ |
||||
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'], |
||||
unsafeCache: true, |
||||
}, |
||||
}); |
||||
|
||||
export default config; |
Loading…
Reference in new issue