@jsreport/jsreport-core 4.2.1 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -256,7 +256,7 @@ jsreport currently support these main listeners
|
|
|
256
256
|
- `closeListeners()` called when jsreport is about to be closed, you will usually put here some code that clean up some resource
|
|
257
257
|
|
|
258
258
|
## Studio
|
|
259
|
-
jsreport includes also visual html studio and rest API. This is provided through two extensions, [@jsreport/jsreport-express](https://github.com/jsreport/jsreport/tree/master/packages/jsreport-express) extension to have a web server available and [@jsreport/jsreport-studio](https://github.com/jsreport/jsreport/tree/master/packages/jsreport-studio) for the web UI, both extensions should be installed in order to have the studio ready. See the documentation of each extension for the details
|
|
259
|
+
jsreport includes also visual html studio and rest API. This is provided through two extensions, [@jsreport/jsreport-express](https://github.com/jsreport/jsreport/tree/master/packages/jsreport-express) extension to have a web server available and [@jsreport/jsreport-studio](https://github.com/jsreport/jsreport/tree/master/packages/jsreport-studio) for the web UI, both extensions should be installed in order to have the studio ready. See the documentation of each extension for the details
|
|
260
260
|
|
|
261
261
|
## Template store
|
|
262
262
|
`jsreport-core` includes API for persisting and accessing report templates. This API is then used by extensions mainly in combination with jsreport [studio](#studio). `jsreport-core` implements just in-memory persistence, but you can add other persistence methods through extensions, see the [template stores](https://jsreport.net/learn/template-stores) docummentation
|
|
@@ -282,6 +282,20 @@ jsreport.documentStore.collection('templates')
|
|
|
282
282
|
|
|
283
283
|
## Changelog
|
|
284
284
|
|
|
285
|
+
### 4.3.0
|
|
286
|
+
|
|
287
|
+
- expose safe properties of `req.context.user` in sandbox
|
|
288
|
+
- fix component execution when wrapped with async helper
|
|
289
|
+
- fix jsdom require in sandbox
|
|
290
|
+
|
|
291
|
+
### 4.2.2
|
|
292
|
+
|
|
293
|
+
- fix recursive component rendering
|
|
294
|
+
|
|
295
|
+
### 4.2.1
|
|
296
|
+
|
|
297
|
+
- response.output.update now accepts Web ReadableStream (which is what new version of puppeteer returns)
|
|
298
|
+
|
|
285
299
|
### 4.2.0
|
|
286
300
|
|
|
287
301
|
- the response object has new structure and api, the `response.output` is new addition and contains different methods to work with the response output. the `response.content`, `response.stream` are now soft deprecated (will be removed in future versions)
|
|
@@ -14,8 +14,10 @@ module.exports = (reporter) => {
|
|
|
14
14
|
|
|
15
15
|
reporter.templatingEngines = { cache: templatesCache }
|
|
16
16
|
|
|
17
|
+
const contextExecutionChainMap = new WeakMap()
|
|
17
18
|
const executionFnParsedParamsMap = new Map()
|
|
18
19
|
const executionAsyncResultsMap = new Map()
|
|
20
|
+
const executionAsyncCallChainMap = new Map()
|
|
19
21
|
const executionFinishListenersMap = new Map()
|
|
20
22
|
|
|
21
23
|
const templatingEnginesEvaluate = async (mainCall, { engine, content, helpers, data }, { entity, entitySet }, req) => {
|
|
@@ -48,11 +50,14 @@ module.exports = (reporter) => {
|
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
executionAsyncResultsMap.delete(executionId)
|
|
53
|
+
executionAsyncCallChainMap.delete(executionId)
|
|
51
54
|
executionFinishListenersMap.delete(executionId)
|
|
52
55
|
}
|
|
53
56
|
}
|
|
54
57
|
|
|
55
|
-
reporter.templatingEngines.evaluate = (executionInfo, entityInfo, req) =>
|
|
58
|
+
reporter.templatingEngines.evaluate = (executionInfo, entityInfo, req) => {
|
|
59
|
+
return templatingEnginesEvaluate(true, executionInfo, entityInfo, req)
|
|
60
|
+
}
|
|
56
61
|
|
|
57
62
|
reporter.extendProxy((proxy, req, {
|
|
58
63
|
runInSandbox,
|
|
@@ -64,15 +69,18 @@ module.exports = (reporter) => {
|
|
|
64
69
|
return templatingEnginesEvaluate(false, executionInfo, entityInfo, req)
|
|
65
70
|
},
|
|
66
71
|
waitForAsyncHelper: async (maybeAsyncContent) => {
|
|
72
|
+
const executionChain = contextExecutionChainMap.get(context) || []
|
|
73
|
+
const executionId = executionChain[executionChain.length - 1]
|
|
74
|
+
|
|
67
75
|
if (
|
|
68
|
-
|
|
69
|
-
!executionAsyncResultsMap.has(
|
|
76
|
+
executionId == null ||
|
|
77
|
+
!executionAsyncResultsMap.has(executionId) ||
|
|
70
78
|
typeof maybeAsyncContent !== 'string'
|
|
71
79
|
) {
|
|
72
80
|
return maybeAsyncContent
|
|
73
81
|
}
|
|
74
82
|
|
|
75
|
-
const asyncResultMap = executionAsyncResultsMap.get(
|
|
83
|
+
const asyncResultMap = executionAsyncResultsMap.get(executionId)
|
|
76
84
|
const asyncHelperResultRegExp = /{#asyncHelperResult ([^{}]+)}/
|
|
77
85
|
let content = maybeAsyncContent
|
|
78
86
|
let matchResult
|
|
@@ -91,18 +99,34 @@ module.exports = (reporter) => {
|
|
|
91
99
|
return content
|
|
92
100
|
},
|
|
93
101
|
waitForAsyncHelpers: async () => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
102
|
+
const executionChain = contextExecutionChainMap.get(context) || []
|
|
103
|
+
const executionId = executionChain[executionChain.length - 1]
|
|
104
|
+
|
|
105
|
+
if (executionId != null && executionAsyncResultsMap.has(executionId)) {
|
|
106
|
+
const asyncCallChainSet = executionAsyncCallChainMap.get(executionId)
|
|
107
|
+
const lastAsyncCall = [...asyncCallChainSet].pop()
|
|
108
|
+
|
|
109
|
+
const asyncResultMap = executionAsyncResultsMap.get(executionId)
|
|
110
|
+
// we should exclude the last async call because if it exists it represents the parent
|
|
111
|
+
// async call that called .waitForAsyncHelpers, it is not going to be resolved at this point
|
|
112
|
+
const targetAsyncResultKeys = [...asyncResultMap.keys()].filter((key) => key !== lastAsyncCall)
|
|
113
|
+
|
|
114
|
+
return Promise.all(targetAsyncResultKeys.map((k) => asyncResultMap.get(k)))
|
|
97
115
|
}
|
|
98
116
|
},
|
|
99
117
|
addFinishListener: (fn) => {
|
|
100
|
-
|
|
101
|
-
|
|
118
|
+
const executionChain = contextExecutionChainMap.get(context) || []
|
|
119
|
+
const executionId = executionChain[executionChain.length - 1]
|
|
120
|
+
|
|
121
|
+
if (executionId && executionFinishListenersMap.has(executionId)) {
|
|
122
|
+
executionFinishListenersMap.get(executionId).add('finish', fn)
|
|
102
123
|
}
|
|
103
124
|
},
|
|
104
125
|
createAsyncHelperResult: (v) => {
|
|
105
|
-
const
|
|
126
|
+
const executionChain = contextExecutionChainMap.get(context) || []
|
|
127
|
+
const executionId = executionChain[executionChain.length - 1]
|
|
128
|
+
|
|
129
|
+
const asyncResultMap = executionAsyncResultsMap.get(executionId)
|
|
106
130
|
const asyncResultId = nanoid(7)
|
|
107
131
|
asyncResultMap.set(asyncResultId, v)
|
|
108
132
|
return `{#asyncHelperResult ${asyncResultId}}`
|
|
@@ -133,6 +157,7 @@ module.exports = (reporter) => {
|
|
|
133
157
|
} finally {
|
|
134
158
|
executionFnParsedParamsMap.delete(req.context.id)
|
|
135
159
|
executionAsyncResultsMap.delete(executionId)
|
|
160
|
+
executionAsyncCallChainMap.delete(executionId)
|
|
136
161
|
}
|
|
137
162
|
}
|
|
138
163
|
|
|
@@ -147,10 +172,6 @@ module.exports = (reporter) => {
|
|
|
147
172
|
const executionFnParsedParamsKey = `entity:${entity.shortid || 'anonymous'}:helpers:${normalizedHelpers}`
|
|
148
173
|
|
|
149
174
|
const initFn = async (getTopLevelFunctions, compileScript) => {
|
|
150
|
-
if (reporter.options.trustUserCode === false) {
|
|
151
|
-
return null
|
|
152
|
-
}
|
|
153
|
-
|
|
154
175
|
if (systemHelpersCache != null) {
|
|
155
176
|
return systemHelpersCache
|
|
156
177
|
}
|
|
@@ -193,10 +214,16 @@ module.exports = (reporter) => {
|
|
|
193
214
|
|
|
194
215
|
const executionFn = async ({ require, console, topLevelFunctions, context }) => {
|
|
195
216
|
const asyncResultMap = new Map()
|
|
217
|
+
const asyncCallChainSet = new Set()
|
|
196
218
|
|
|
197
|
-
context
|
|
219
|
+
if (!contextExecutionChainMap.has(context)) {
|
|
220
|
+
contextExecutionChainMap.set(context, [])
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
contextExecutionChainMap.get(context).push(executionId)
|
|
198
224
|
|
|
199
225
|
executionAsyncResultsMap.set(executionId, asyncResultMap)
|
|
226
|
+
executionAsyncCallChainMap.set(executionId, asyncCallChainSet)
|
|
200
227
|
executionFinishListenersMap.set(executionId, reporter.createListenerCollection())
|
|
201
228
|
executionFnParsedParamsMap.get(req.context.id).get(executionFnParsedParamsKey).resolve({ require, console, topLevelFunctions, context })
|
|
202
229
|
|
|
@@ -223,7 +250,7 @@ module.exports = (reporter) => {
|
|
|
223
250
|
if (engine.getWrappingHelpersEnabled && engine.getWrappingHelpersEnabled(req) === false) {
|
|
224
251
|
wrappedTopLevelFunctions[h] = engine.wrapHelper(wrappedTopLevelFunctions[h], { context })
|
|
225
252
|
} else {
|
|
226
|
-
wrappedTopLevelFunctions[h] = wrapHelperForAsyncSupport(wrappedTopLevelFunctions[h], asyncResultMap)
|
|
253
|
+
wrappedTopLevelFunctions[h] = wrapHelperForAsyncSupport(wrappedTopLevelFunctions[h], asyncResultMap, asyncCallChainSet)
|
|
227
254
|
}
|
|
228
255
|
}
|
|
229
256
|
|
|
@@ -231,21 +258,36 @@ module.exports = (reporter) => {
|
|
|
231
258
|
|
|
232
259
|
const resolvedResultsMap = new Map()
|
|
233
260
|
|
|
234
|
-
// we need to use the cloned map,
|
|
235
|
-
|
|
261
|
+
// we need to use the cloned map, because there can be a waitForAsyncHelper pending that needs the asyncResultMap values
|
|
262
|
+
let clonedMap = new Map(asyncResultMap)
|
|
263
|
+
|
|
236
264
|
while (clonedMap.size > 0) {
|
|
237
|
-
|
|
238
|
-
|
|
265
|
+
const keysEvaluated = [...clonedMap.keys()]
|
|
266
|
+
|
|
267
|
+
await Promise.all(keysEvaluated.map(async (k) => {
|
|
268
|
+
const result = await clonedMap.get(k)
|
|
269
|
+
asyncCallChainSet.delete(k)
|
|
270
|
+
resolvedResultsMap.set(k, `${result}`)
|
|
239
271
|
clonedMap.delete(k)
|
|
240
272
|
}))
|
|
273
|
+
|
|
274
|
+
// we need to remove the keys processed from the original map at this point
|
|
275
|
+
// (after the await) because during the async work the asyncResultMap will be read
|
|
276
|
+
for (const k of keysEvaluated) {
|
|
277
|
+
asyncResultMap.delete(k)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// we want to process the new generated pending async results
|
|
281
|
+
if (asyncResultMap.size > 0) {
|
|
282
|
+
clonedMap = new Map(asyncResultMap)
|
|
283
|
+
}
|
|
241
284
|
}
|
|
242
|
-
asyncResultMap.clear()
|
|
243
285
|
|
|
244
286
|
while (contentResult.includes('{#asyncHelperResult')) {
|
|
245
287
|
contentResult = contentResult.replace(/{#asyncHelperResult ([^{}]+)}/g, (str, p1) => {
|
|
246
288
|
const asyncResultId = p1
|
|
247
289
|
// this can happen if a child jsreport.templatingEngines.evaluate receives an async value from outer scope
|
|
248
|
-
// because every evaluate uses a unique map of async
|
|
290
|
+
// because every evaluate uses a unique map of async results
|
|
249
291
|
// example is the case when component receives as a value async thing
|
|
250
292
|
// instead of returning "undefined" we let the outer eval to do the replace
|
|
251
293
|
if (!resolvedResultsMap.has(asyncResultId)) {
|
|
@@ -255,9 +297,12 @@ module.exports = (reporter) => {
|
|
|
255
297
|
return `${resolvedResultsMap.get(asyncResultId)}`
|
|
256
298
|
})
|
|
257
299
|
}
|
|
300
|
+
|
|
258
301
|
contentResult = contentResult.replace(/asyncUnresolvedHelperResult/g, 'asyncHelperResult')
|
|
259
302
|
|
|
260
|
-
await executionFinishListenersMap.get(
|
|
303
|
+
await executionFinishListenersMap.get(executionId).fire()
|
|
304
|
+
|
|
305
|
+
contextExecutionChainMap.set(context, contextExecutionChainMap.get(context).filter((id) => id !== executionId))
|
|
261
306
|
|
|
262
307
|
return {
|
|
263
308
|
// handlebars escapes single brackets before execution to prevent errors on {#asset}
|
|
@@ -287,30 +332,12 @@ module.exports = (reporter) => {
|
|
|
287
332
|
templatesCache.reset()
|
|
288
333
|
}
|
|
289
334
|
|
|
290
|
-
let helpersStr = normalizedHelpers
|
|
291
|
-
if (reporter.options.trustUserCode === false) {
|
|
292
|
-
const registerResults = await reporter.registerHelpersListeners.fire()
|
|
293
|
-
const systemHelpers = []
|
|
294
|
-
|
|
295
|
-
for (const result of registerResults) {
|
|
296
|
-
if (result == null) {
|
|
297
|
-
continue
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (typeof result === 'string') {
|
|
301
|
-
systemHelpers.push(result)
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
const systemHelpersStr = systemHelpers.join('\n')
|
|
305
|
-
helpersStr = normalizedHelpers + '\n' + systemHelpersStr
|
|
306
|
-
}
|
|
307
|
-
|
|
308
335
|
try {
|
|
309
336
|
return await reporter.runInSandbox({
|
|
310
337
|
context: {
|
|
311
338
|
...(engine.createContext ? engine.createContext(req) : {})
|
|
312
339
|
},
|
|
313
|
-
userCode:
|
|
340
|
+
userCode: normalizedHelpers,
|
|
314
341
|
initFn,
|
|
315
342
|
executionFn,
|
|
316
343
|
currentPath: entityPath,
|
|
@@ -363,7 +390,7 @@ module.exports = (reporter) => {
|
|
|
363
390
|
}
|
|
364
391
|
}
|
|
365
392
|
|
|
366
|
-
function wrapHelperForAsyncSupport (fn, asyncResultMap) {
|
|
393
|
+
function wrapHelperForAsyncSupport (fn, asyncResultMap, asyncCallChainSet) {
|
|
367
394
|
return function (...args) {
|
|
368
395
|
// important to call the helper with the current this to preserve the same behavior
|
|
369
396
|
const fnResult = fn.call(this, ...args)
|
|
@@ -374,6 +401,7 @@ module.exports = (reporter) => {
|
|
|
374
401
|
|
|
375
402
|
const asyncResultId = nanoid(7)
|
|
376
403
|
asyncResultMap.set(asyncResultId, fnResult)
|
|
404
|
+
asyncCallChainSet.add(asyncResultId)
|
|
377
405
|
|
|
378
406
|
return `{#asyncHelperResult ${asyncResultId}}`
|
|
379
407
|
}
|
|
@@ -253,6 +253,23 @@ function applyPropertiesConfig (context, hierarchyPropertiesConfig, {
|
|
|
253
253
|
shouldStoreOriginal = false
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
// allow configuring parent as hidden, but child props as readonly so we expose
|
|
257
|
+
// just part of the parent
|
|
258
|
+
// const ignoreParentHidden = false
|
|
259
|
+
const ignoreParentHidden = (
|
|
260
|
+
isHidden &&
|
|
261
|
+
isGrouped &&
|
|
262
|
+
(hierarchyPropertiesConfig.inner != null || hierarchyPropertiesConfig.standalone != null)
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
if (parentOpts?.originalSandboxHidden !== true && ignoreParentHidden) {
|
|
266
|
+
// we store the original value for the top parent with hidden value
|
|
267
|
+
shouldStoreOriginal = true
|
|
268
|
+
} else if (parentOpts && parentOpts.originalSandboxHidden === true) {
|
|
269
|
+
// we don't store original value when parent was configured as hidden
|
|
270
|
+
shouldStoreOriginal = false
|
|
271
|
+
}
|
|
272
|
+
|
|
256
273
|
// saving original value
|
|
257
274
|
if (shouldStoreOriginal) {
|
|
258
275
|
let exists = true
|
|
@@ -284,6 +301,19 @@ function applyPropertiesConfig (context, hierarchyPropertiesConfig, {
|
|
|
284
301
|
Object.keys(c.standalone).forEach((standaloneKey) => {
|
|
285
302
|
const standaloneConfig = c.standalone[standaloneKey]
|
|
286
303
|
|
|
304
|
+
const newParentOpts = {
|
|
305
|
+
sandboxHidden: ignoreParentHidden ? false : isHidden,
|
|
306
|
+
sandboxReadOnly: isReadOnly
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// set and inherit originalSandboxHidden if needed
|
|
310
|
+
if (
|
|
311
|
+
parentOpts?.originalSandboxHidden === true ||
|
|
312
|
+
(ignoreParentHidden && isHidden)
|
|
313
|
+
) {
|
|
314
|
+
newParentOpts.originalSandboxHidden = true
|
|
315
|
+
}
|
|
316
|
+
|
|
287
317
|
applyPropertiesConfig(context, standaloneConfig, {
|
|
288
318
|
original,
|
|
289
319
|
customProxies,
|
|
@@ -291,7 +321,7 @@ function applyPropertiesConfig (context, hierarchyPropertiesConfig, {
|
|
|
291
321
|
isRoot: false,
|
|
292
322
|
isGrouped: false,
|
|
293
323
|
onlyReadOnlyTopLevel,
|
|
294
|
-
parentOpts:
|
|
324
|
+
parentOpts: newParentOpts
|
|
295
325
|
}, readOnlyConfigured)
|
|
296
326
|
})
|
|
297
327
|
}
|
|
@@ -300,19 +330,65 @@ function applyPropertiesConfig (context, hierarchyPropertiesConfig, {
|
|
|
300
330
|
Object.keys(c.inner).forEach((innerKey) => {
|
|
301
331
|
const innerConfig = c.inner[innerKey]
|
|
302
332
|
|
|
333
|
+
const newParentOpts = {
|
|
334
|
+
sandboxHidden: ignoreParentHidden ? false : isHidden,
|
|
335
|
+
sandboxReadOnly: isReadOnly
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// set and inherit originalSandboxHidden if needed
|
|
339
|
+
if (
|
|
340
|
+
parentOpts?.originalSandboxHidden === true ||
|
|
341
|
+
(ignoreParentHidden && isHidden)
|
|
342
|
+
) {
|
|
343
|
+
newParentOpts.originalSandboxHidden = true
|
|
344
|
+
}
|
|
345
|
+
|
|
303
346
|
applyPropertiesConfig(context, innerConfig, {
|
|
304
347
|
original,
|
|
305
348
|
customProxies,
|
|
306
349
|
prop: innerKey,
|
|
307
350
|
isRoot: false,
|
|
308
351
|
isGrouped: true,
|
|
309
|
-
parentOpts:
|
|
352
|
+
parentOpts: newParentOpts
|
|
310
353
|
}, readOnlyConfigured)
|
|
311
354
|
})
|
|
312
355
|
}
|
|
313
356
|
|
|
314
357
|
if (isHidden) {
|
|
315
|
-
|
|
358
|
+
if (ignoreParentHidden) {
|
|
359
|
+
// when parent is hidden but there are configuration for child properties we just copy
|
|
360
|
+
// the properties listed there, we do this because we want to work with just the configured
|
|
361
|
+
// properties, the other properties should not be exposed
|
|
362
|
+
const currentValue = get(context, prop)
|
|
363
|
+
|
|
364
|
+
if (currentValue != null && typeof currentValue === 'object') {
|
|
365
|
+
let newValue
|
|
366
|
+
|
|
367
|
+
if (Array.isArray(currentValue)) {
|
|
368
|
+
newValue = []
|
|
369
|
+
} else {
|
|
370
|
+
newValue = {}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
for (const config of [hierarchyPropertiesConfig.standalone, hierarchyPropertiesConfig.inner]) {
|
|
374
|
+
if (config == null) {
|
|
375
|
+
continue
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
for (const childProp of Object.keys(config)) {
|
|
379
|
+
if (hasOwn(context, childProp)) {
|
|
380
|
+
const childValue = get(context, childProp)
|
|
381
|
+
const targetProp = childProp.replace(`${prop}.`, '')
|
|
382
|
+
set(newValue, targetProp, childValue)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
set(context, prop, newValue)
|
|
388
|
+
}
|
|
389
|
+
} else {
|
|
390
|
+
omitProp(context, prop)
|
|
391
|
+
}
|
|
316
392
|
} else if (isReadOnly) {
|
|
317
393
|
readOnlyProp(context, prop, readOnlyConfigured, customProxies, {
|
|
318
394
|
onlyTopLevel: false,
|
|
@@ -38,7 +38,7 @@ module.exports = function createSandboxRequire (safeExecution, isolateModules, m
|
|
|
38
38
|
|
|
39
39
|
if (isolateModules) {
|
|
40
40
|
const requireExtensions = Object.create(null)
|
|
41
|
-
isolatedRequire.setDefaultRequireExtensions(requireExtensions,
|
|
41
|
+
isolatedRequire.setDefaultRequireExtensions(requireExtensions, requireFromRootDirectory, compileScript)
|
|
42
42
|
modulesMeta.requireExtensions = requireExtensions
|
|
43
43
|
}
|
|
44
44
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsreport/jsreport-core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"description": "javascript based business reporting",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"report",
|
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
80
|
"@node-rs/jsonwebtoken": "0.2.0",
|
|
81
|
+
"jsdom": "17.0.0",
|
|
81
82
|
"mocha": "10.1.0",
|
|
82
83
|
"should": "13.2.3",
|
|
83
84
|
"standard": "16.0.4",
|