mirror of
https://github.com/prometheus/prometheus.git
synced 2024-11-14 17:44:06 -08:00
Merge remote-tracking branch 'upstream/main' into codesome/syncprom
This commit is contained in:
commit
0af335cfe8
|
@ -55,7 +55,7 @@ Prometheus will now be reachable at http://localhost:9090/.
|
|||
|
||||
### Building from source
|
||||
|
||||
To build Prometheus from source code, first ensure that have a working
|
||||
To build Prometheus from source code, first ensure that you have a working
|
||||
Go environment with [version 1.14 or greater installed](https://golang.org/doc/install).
|
||||
You also need [Node.js](https://nodejs.org/) and [npm](https://www.npmjs.com/)
|
||||
installed in order to build the frontend assets.
|
||||
|
|
|
@ -25,7 +25,7 @@ Here is a table comparing our two generic Service Discovery implementations.
|
|||
|
||||
## Requirements of HTTP SD endpoints
|
||||
|
||||
If you implement an HTTP SD endpoint, here is a few requirements you should be
|
||||
If you implement an HTTP SD endpoint, here are a few requirements you should be
|
||||
aware of.
|
||||
|
||||
The response is consumed as is, unmodified. On each refresh interval (default: 1
|
||||
|
@ -47,7 +47,7 @@ for incremental updates. A Prometheus instance does not send its hostname and it
|
|||
is not possible for a SD endpoint to know if the SD requests is the first one
|
||||
after a restart or not.
|
||||
|
||||
The URL to the HTTP SD is not considered secret. The authentication, and any API
|
||||
The URL to the HTTP SD is not considered secret. The authentication and any API
|
||||
keys should be passed with the appropriate authentication mechanisms. Prometheus
|
||||
supports TLS authentication, basic authentication, OAuth2, and authorization
|
||||
headers.
|
||||
|
|
2
go.mod
2
go.mod
|
@ -8,7 +8,7 @@ require (
|
|||
github.com/Azure/go-autorest/autorest/adal v0.9.15
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
|
||||
github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
|
||||
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15
|
||||
github.com/alecthomas/units v0.0.0-20210912230133-d1bdfacee922
|
||||
github.com/aws/aws-sdk-go v1.40.37
|
||||
github.com/cespare/xxhash/v2 v2.1.2
|
||||
github.com/containerd/containerd v1.5.4 // indirect
|
||||
|
|
3
go.sum
3
go.sum
|
@ -151,8 +151,9 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
|
|||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 h1:AUNCr9CiJuwrRYS3XieqF+Z9B9gNxo/eANAJCF2eiN4=
|
||||
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
|
||||
github.com/alecthomas/units v0.0.0-20210912230133-d1bdfacee922 h1:8ypNbf5sd3Sm3cKJ9waOGoQv6dKAFiFty9L6NP1AqJ4=
|
||||
github.com/alecthomas/units v0.0.0-20210912230133-d1bdfacee922/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
|
||||
github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6/go.mod h1:V8iCPQYkqmusNa815XgQio277wI47sdRh1dUOLdyC6Q=
|
||||
|
|
|
@ -71,7 +71,7 @@ func BenchmarkRangeQuery(b *testing.B) {
|
|||
a := storage.Appender(context.Background())
|
||||
ts := int64(s * 10000) // 10s interval.
|
||||
for i, metric := range metrics {
|
||||
ref, _ := a.Append(refs[i], metric, ts, float64(s))
|
||||
ref, _ := a.Append(refs[i], metric, ts, float64(s)+float64(i)/float64(len(metrics)))
|
||||
refs[i] = ref
|
||||
}
|
||||
if err := a.Commit(); err != nil {
|
||||
|
@ -130,6 +130,9 @@ func BenchmarkRangeQuery(b *testing.B) {
|
|||
{
|
||||
expr: "a_X unless b_X{l=~'.*[0-4]$'}",
|
||||
},
|
||||
{
|
||||
expr: "a_X and b_X{l='notfound'}",
|
||||
},
|
||||
// Simple functions.
|
||||
{
|
||||
expr: "abs(a_X)",
|
||||
|
@ -159,6 +162,9 @@ func BenchmarkRangeQuery(b *testing.B) {
|
|||
{
|
||||
expr: "count_values('value', h_X)",
|
||||
},
|
||||
{
|
||||
expr: "topk(1, a_X)",
|
||||
},
|
||||
// Combinations.
|
||||
{
|
||||
expr: "rate(a_X[1m]) + rate(b_X[1m])",
|
||||
|
@ -172,6 +178,10 @@ func BenchmarkRangeQuery(b *testing.B) {
|
|||
{
|
||||
expr: "histogram_quantile(0.9, rate(h_X[5m]))",
|
||||
},
|
||||
// Many-to-one join.
|
||||
{
|
||||
expr: "a_X + on(l) group_right a_one",
|
||||
},
|
||||
}
|
||||
|
||||
// X in an expr will be replaced by different metric sizes.
|
||||
|
|
140
promql/engine.go
140
promql/engine.go
|
@ -913,6 +913,8 @@ func (ev *evaluator) Eval(expr parser.Expr) (v parser.Value, ws storage.Warnings
|
|||
type EvalSeriesHelper struct {
|
||||
// The grouping key used by aggregation.
|
||||
groupingKey uint64
|
||||
// Used to map left-hand to right-hand in binary operations.
|
||||
signature string
|
||||
}
|
||||
|
||||
// EvalNodeHelper stores extra information and caches for evaluating a single node across steps.
|
||||
|
@ -925,8 +927,6 @@ type EvalNodeHelper struct {
|
|||
// Caches.
|
||||
// DropMetricName and label_*.
|
||||
Dmn map[uint64]labels.Labels
|
||||
// signatureFunc.
|
||||
sigf map[string]string
|
||||
// funcHistogramQuantile.
|
||||
signatureToMetricWithBuckets map[string]*metricWithBuckets
|
||||
// label_replace.
|
||||
|
@ -957,23 +957,6 @@ func (enh *EvalNodeHelper) DropMetricName(l labels.Labels) labels.Labels {
|
|||
return ret
|
||||
}
|
||||
|
||||
func (enh *EvalNodeHelper) signatureFunc(on bool, names ...string) func(labels.Labels) string {
|
||||
if enh.sigf == nil {
|
||||
enh.sigf = make(map[string]string, len(enh.Out))
|
||||
}
|
||||
f := signatureFunc(on, enh.lblBuf, names...)
|
||||
return func(l labels.Labels) string {
|
||||
enh.lblBuf = l.Bytes(enh.lblBuf)
|
||||
ret, ok := enh.sigf[string(enh.lblBuf)]
|
||||
if ok {
|
||||
return ret
|
||||
}
|
||||
ret = f(l)
|
||||
enh.sigf[string(enh.lblBuf)] = ret
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
// rangeEval evaluates the given expressions, and then for each step calls
|
||||
// the given funcCall with the values computed for each expression at that
|
||||
// step. The return value is the combination into time series of all the
|
||||
|
@ -1432,22 +1415,28 @@ func (ev *evaluator) eval(expr parser.Expr) (parser.Value, storage.Warnings) {
|
|||
return append(enh.Out, Sample{Point: Point{V: val}}), nil
|
||||
}, e.LHS, e.RHS)
|
||||
case lt == parser.ValueTypeVector && rt == parser.ValueTypeVector:
|
||||
// Function to compute the join signature for each series.
|
||||
buf := make([]byte, 0, 1024)
|
||||
sigf := signatureFunc(e.VectorMatching.On, buf, e.VectorMatching.MatchingLabels...)
|
||||
initSignatures := func(series labels.Labels, h *EvalSeriesHelper) {
|
||||
h.signature = sigf(series)
|
||||
}
|
||||
switch e.Op {
|
||||
case parser.LAND:
|
||||
return ev.rangeEval(nil, func(v []parser.Value, _ [][]EvalSeriesHelper, enh *EvalNodeHelper) (Vector, storage.Warnings) {
|
||||
return ev.VectorAnd(v[0].(Vector), v[1].(Vector), e.VectorMatching, enh), nil
|
||||
return ev.rangeEval(initSignatures, func(v []parser.Value, sh [][]EvalSeriesHelper, enh *EvalNodeHelper) (Vector, storage.Warnings) {
|
||||
return ev.VectorAnd(v[0].(Vector), v[1].(Vector), e.VectorMatching, sh[0], sh[1], enh), nil
|
||||
}, e.LHS, e.RHS)
|
||||
case parser.LOR:
|
||||
return ev.rangeEval(nil, func(v []parser.Value, _ [][]EvalSeriesHelper, enh *EvalNodeHelper) (Vector, storage.Warnings) {
|
||||
return ev.VectorOr(v[0].(Vector), v[1].(Vector), e.VectorMatching, enh), nil
|
||||
return ev.rangeEval(initSignatures, func(v []parser.Value, sh [][]EvalSeriesHelper, enh *EvalNodeHelper) (Vector, storage.Warnings) {
|
||||
return ev.VectorOr(v[0].(Vector), v[1].(Vector), e.VectorMatching, sh[0], sh[1], enh), nil
|
||||
}, e.LHS, e.RHS)
|
||||
case parser.LUNLESS:
|
||||
return ev.rangeEval(nil, func(v []parser.Value, _ [][]EvalSeriesHelper, enh *EvalNodeHelper) (Vector, storage.Warnings) {
|
||||
return ev.VectorUnless(v[0].(Vector), v[1].(Vector), e.VectorMatching, enh), nil
|
||||
return ev.rangeEval(initSignatures, func(v []parser.Value, sh [][]EvalSeriesHelper, enh *EvalNodeHelper) (Vector, storage.Warnings) {
|
||||
return ev.VectorUnless(v[0].(Vector), v[1].(Vector), e.VectorMatching, sh[0], sh[1], enh), nil
|
||||
}, e.LHS, e.RHS)
|
||||
default:
|
||||
return ev.rangeEval(nil, func(v []parser.Value, _ [][]EvalSeriesHelper, enh *EvalNodeHelper) (Vector, storage.Warnings) {
|
||||
return ev.VectorBinop(e.Op, v[0].(Vector), v[1].(Vector), e.VectorMatching, e.ReturnBool, enh), nil
|
||||
return ev.rangeEval(initSignatures, func(v []parser.Value, sh [][]EvalSeriesHelper, enh *EvalNodeHelper) (Vector, storage.Warnings) {
|
||||
return ev.VectorBinop(e.Op, v[0].(Vector), v[1].(Vector), e.VectorMatching, e.ReturnBool, sh[0], sh[1], enh), nil
|
||||
}, e.LHS, e.RHS)
|
||||
}
|
||||
|
||||
|
@ -1774,62 +1763,72 @@ func (ev *evaluator) matrixIterSlice(it *storage.BufferedSeriesIterator, mint, m
|
|||
return out
|
||||
}
|
||||
|
||||
func (ev *evaluator) VectorAnd(lhs, rhs Vector, matching *parser.VectorMatching, enh *EvalNodeHelper) Vector {
|
||||
func (ev *evaluator) VectorAnd(lhs, rhs Vector, matching *parser.VectorMatching, lhsh, rhsh []EvalSeriesHelper, enh *EvalNodeHelper) Vector {
|
||||
if matching.Card != parser.CardManyToMany {
|
||||
panic("set operations must only use many-to-many matching")
|
||||
}
|
||||
sigf := enh.signatureFunc(matching.On, matching.MatchingLabels...)
|
||||
if len(lhs) == 0 || len(rhs) == 0 {
|
||||
return nil // Short-circuit: AND with nothing is nothing.
|
||||
}
|
||||
|
||||
// The set of signatures for the right-hand side Vector.
|
||||
rightSigs := map[string]struct{}{}
|
||||
// Add all rhs samples to a map so we can easily find matches later.
|
||||
for _, rs := range rhs {
|
||||
rightSigs[sigf(rs.Metric)] = struct{}{}
|
||||
for _, sh := range rhsh {
|
||||
rightSigs[sh.signature] = struct{}{}
|
||||
}
|
||||
|
||||
for _, ls := range lhs {
|
||||
for i, ls := range lhs {
|
||||
// If there's a matching entry in the right-hand side Vector, add the sample.
|
||||
if _, ok := rightSigs[sigf(ls.Metric)]; ok {
|
||||
if _, ok := rightSigs[lhsh[i].signature]; ok {
|
||||
enh.Out = append(enh.Out, ls)
|
||||
}
|
||||
}
|
||||
return enh.Out
|
||||
}
|
||||
|
||||
func (ev *evaluator) VectorOr(lhs, rhs Vector, matching *parser.VectorMatching, enh *EvalNodeHelper) Vector {
|
||||
func (ev *evaluator) VectorOr(lhs, rhs Vector, matching *parser.VectorMatching, lhsh, rhsh []EvalSeriesHelper, enh *EvalNodeHelper) Vector {
|
||||
if matching.Card != parser.CardManyToMany {
|
||||
panic("set operations must only use many-to-many matching")
|
||||
}
|
||||
sigf := enh.signatureFunc(matching.On, matching.MatchingLabels...)
|
||||
if len(lhs) == 0 { // Short-circuit.
|
||||
return rhs
|
||||
} else if len(rhs) == 0 {
|
||||
return lhs
|
||||
}
|
||||
|
||||
leftSigs := map[string]struct{}{}
|
||||
// Add everything from the left-hand-side Vector.
|
||||
for _, ls := range lhs {
|
||||
leftSigs[sigf(ls.Metric)] = struct{}{}
|
||||
for i, ls := range lhs {
|
||||
leftSigs[lhsh[i].signature] = struct{}{}
|
||||
enh.Out = append(enh.Out, ls)
|
||||
}
|
||||
// Add all right-hand side elements which have not been added from the left-hand side.
|
||||
for _, rs := range rhs {
|
||||
if _, ok := leftSigs[sigf(rs.Metric)]; !ok {
|
||||
for j, rs := range rhs {
|
||||
if _, ok := leftSigs[rhsh[j].signature]; !ok {
|
||||
enh.Out = append(enh.Out, rs)
|
||||
}
|
||||
}
|
||||
return enh.Out
|
||||
}
|
||||
|
||||
func (ev *evaluator) VectorUnless(lhs, rhs Vector, matching *parser.VectorMatching, enh *EvalNodeHelper) Vector {
|
||||
func (ev *evaluator) VectorUnless(lhs, rhs Vector, matching *parser.VectorMatching, lhsh, rhsh []EvalSeriesHelper, enh *EvalNodeHelper) Vector {
|
||||
if matching.Card != parser.CardManyToMany {
|
||||
panic("set operations must only use many-to-many matching")
|
||||
}
|
||||
sigf := enh.signatureFunc(matching.On, matching.MatchingLabels...)
|
||||
|
||||
rightSigs := map[string]struct{}{}
|
||||
for _, rs := range rhs {
|
||||
rightSigs[sigf(rs.Metric)] = struct{}{}
|
||||
// Short-circuit: empty rhs means we will return everything in lhs;
|
||||
// empty lhs means we will return empty - don't need to build a map.
|
||||
if len(lhs) == 0 || len(rhs) == 0 {
|
||||
return lhs
|
||||
}
|
||||
|
||||
for _, ls := range lhs {
|
||||
if _, ok := rightSigs[sigf(ls.Metric)]; !ok {
|
||||
rightSigs := map[string]struct{}{}
|
||||
for _, sh := range rhsh {
|
||||
rightSigs[sh.signature] = struct{}{}
|
||||
}
|
||||
|
||||
for i, ls := range lhs {
|
||||
if _, ok := rightSigs[lhsh[i].signature]; !ok {
|
||||
enh.Out = append(enh.Out, ls)
|
||||
}
|
||||
}
|
||||
|
@ -1837,17 +1836,20 @@ func (ev *evaluator) VectorUnless(lhs, rhs Vector, matching *parser.VectorMatchi
|
|||
}
|
||||
|
||||
// VectorBinop evaluates a binary operation between two Vectors, excluding set operators.
|
||||
func (ev *evaluator) VectorBinop(op parser.ItemType, lhs, rhs Vector, matching *parser.VectorMatching, returnBool bool, enh *EvalNodeHelper) Vector {
|
||||
func (ev *evaluator) VectorBinop(op parser.ItemType, lhs, rhs Vector, matching *parser.VectorMatching, returnBool bool, lhsh, rhsh []EvalSeriesHelper, enh *EvalNodeHelper) Vector {
|
||||
if matching.Card == parser.CardManyToMany {
|
||||
panic("many-to-many only allowed for set operators")
|
||||
}
|
||||
sigf := enh.signatureFunc(matching.On, matching.MatchingLabels...)
|
||||
if len(lhs) == 0 || len(rhs) == 0 {
|
||||
return nil // Short-circuit: nothing is going to match.
|
||||
}
|
||||
|
||||
// The control flow below handles one-to-one or many-to-one matching.
|
||||
// For one-to-many, swap sidedness and account for the swap when calculating
|
||||
// values.
|
||||
if matching.Card == parser.CardOneToMany {
|
||||
lhs, rhs = rhs, lhs
|
||||
lhsh, rhsh = rhsh, lhsh
|
||||
}
|
||||
|
||||
// All samples from the rhs hashed by the matching label/values.
|
||||
|
@ -1861,8 +1863,8 @@ func (ev *evaluator) VectorBinop(op parser.ItemType, lhs, rhs Vector, matching *
|
|||
rightSigs := enh.rightSigs
|
||||
|
||||
// Add all rhs samples to a map so we can easily find matches later.
|
||||
for _, rs := range rhs {
|
||||
sig := sigf(rs.Metric)
|
||||
for i, rs := range rhs {
|
||||
sig := rhsh[i].signature
|
||||
// The rhs is guaranteed to be the 'one' side. Having multiple samples
|
||||
// with the same signature means that the matching is many-to-many.
|
||||
if duplSample, found := rightSigs[sig]; found {
|
||||
|
@ -1892,8 +1894,8 @@ func (ev *evaluator) VectorBinop(op parser.ItemType, lhs, rhs Vector, matching *
|
|||
|
||||
// For all lhs samples find a respective rhs sample and perform
|
||||
// the binary operation.
|
||||
for _, ls := range lhs {
|
||||
sig := sigf(ls.Metric)
|
||||
for i, ls := range lhs {
|
||||
sig := lhsh[i].signature
|
||||
|
||||
rs, found := rightSigs[sig] // Look for a match in the rhs Vector.
|
||||
if !found {
|
||||
|
@ -2210,22 +2212,24 @@ func (ev *evaluator) aggregation(op parser.ItemType, grouping []string, without
|
|||
resultSize := k
|
||||
if k > inputVecLen {
|
||||
resultSize = inputVecLen
|
||||
} else if k == 0 {
|
||||
resultSize = 1
|
||||
}
|
||||
switch op {
|
||||
case parser.STDVAR, parser.STDDEV:
|
||||
result[groupingKey].value = 0
|
||||
case parser.TOPK, parser.QUANTILE:
|
||||
result[groupingKey].heap = make(vectorByValueHeap, 0, resultSize)
|
||||
heap.Push(&result[groupingKey].heap, &Sample{
|
||||
result[groupingKey].heap = make(vectorByValueHeap, 1, resultSize)
|
||||
result[groupingKey].heap[0] = Sample{
|
||||
Point: Point{V: s.V},
|
||||
Metric: s.Metric,
|
||||
})
|
||||
}
|
||||
case parser.BOTTOMK:
|
||||
result[groupingKey].reverseHeap = make(vectorByReverseValueHeap, 0, resultSize)
|
||||
heap.Push(&result[groupingKey].reverseHeap, &Sample{
|
||||
result[groupingKey].reverseHeap = make(vectorByReverseValueHeap, 1, resultSize)
|
||||
result[groupingKey].reverseHeap[0] = Sample{
|
||||
Point: Point{V: s.V},
|
||||
Metric: s.Metric,
|
||||
})
|
||||
}
|
||||
case parser.GROUP:
|
||||
result[groupingKey].value = 1
|
||||
}
|
||||
|
@ -2283,6 +2287,13 @@ func (ev *evaluator) aggregation(op parser.ItemType, grouping []string, without
|
|||
case parser.TOPK:
|
||||
if int64(len(group.heap)) < k || group.heap[0].V < s.V || math.IsNaN(group.heap[0].V) {
|
||||
if int64(len(group.heap)) == k {
|
||||
if k == 1 { // For k==1 we can replace in-situ.
|
||||
group.heap[0] = Sample{
|
||||
Point: Point{V: s.V},
|
||||
Metric: s.Metric,
|
||||
}
|
||||
break
|
||||
}
|
||||
heap.Pop(&group.heap)
|
||||
}
|
||||
heap.Push(&group.heap, &Sample{
|
||||
|
@ -2294,6 +2305,13 @@ func (ev *evaluator) aggregation(op parser.ItemType, grouping []string, without
|
|||
case parser.BOTTOMK:
|
||||
if int64(len(group.reverseHeap)) < k || group.reverseHeap[0].V > s.V || math.IsNaN(group.reverseHeap[0].V) {
|
||||
if int64(len(group.reverseHeap)) == k {
|
||||
if k == 1 { // For k==1 we can replace in-situ.
|
||||
group.reverseHeap[0] = Sample{
|
||||
Point: Point{V: s.V},
|
||||
Metric: s.Metric,
|
||||
}
|
||||
break
|
||||
}
|
||||
heap.Pop(&group.reverseHeap)
|
||||
}
|
||||
heap.Push(&group.reverseHeap, &Sample{
|
||||
|
@ -2327,7 +2345,9 @@ func (ev *evaluator) aggregation(op parser.ItemType, grouping []string, without
|
|||
|
||||
case parser.TOPK:
|
||||
// The heap keeps the lowest value on top, so reverse it.
|
||||
if len(aggr.heap) > 1 {
|
||||
sort.Sort(sort.Reverse(aggr.heap))
|
||||
}
|
||||
for _, v := range aggr.heap {
|
||||
enh.Out = append(enh.Out, Sample{
|
||||
Metric: v.Metric,
|
||||
|
@ -2338,7 +2358,9 @@ func (ev *evaluator) aggregation(op parser.ItemType, grouping []string, without
|
|||
|
||||
case parser.BOTTOMK:
|
||||
// The heap keeps the highest value on top, so reverse it.
|
||||
if len(aggr.reverseHeap) > 1 {
|
||||
sort.Sort(sort.Reverse(aggr.reverseHeap))
|
||||
}
|
||||
for _, v := range aggr.reverseHeap {
|
||||
enh.Out = append(enh.Out, Sample{
|
||||
Metric: v.Metric,
|
||||
|
|
5
web/ui/package-lock.json
generated
5
web/ui/package-lock.json
generated
|
@ -8,7 +8,10 @@
|
|||
"workspaces": [
|
||||
"react-app",
|
||||
"module/*"
|
||||
]
|
||||
],
|
||||
"engines": {
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"module/codemirror-promql": {
|
||||
"version": "0.18.0",
|
||||
|
|
|
@ -12,5 +12,8 @@
|
|||
"workspaces": [
|
||||
"react-app",
|
||||
"module/*"
|
||||
]
|
||||
],
|
||||
"engines": {
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue