@jsreport/jsreport-core 4.3.0 → 4.4.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
@@ -282,6 +282,16 @@ jsreport.documentStore.collection('templates')
282
282
 
283
283
  ## Changelog
284
284
 
285
+ ### 4.4.0
286
+
287
+ - trigger async report using header to avoid allocating worker
288
+ - fix require('..') and require('.') in sandbox
289
+ - fix component rendering in loop with async work on helpers
290
+
291
+ ### 4.3.1
292
+
293
+ - fix `waitForAsyncHelper`, `waitForAsyncHelpers` not working with trustUserCode: true
294
+
285
295
  ### 4.3.0
286
296
 
287
297
  - expose safe properties of `req.context.user` in sandbox
@@ -159,6 +159,10 @@ module.exports = (reporter) => {
159
159
  reporter.beforeRenderWorkerAllocatedListeners.add('profiler', async (req) => {
160
160
  req.context.profiling = req.context.profiling || {}
161
161
 
162
+ if (req.context.profiling.enabled === false) {
163
+ return
164
+ }
165
+
162
166
  if (req.context.profiling.mode == null) {
163
167
  const profilerSettings = await reporter.settings.findValue('profiler', req)
164
168
  const defaultMode = reporter.options.profiler.defaultMode || 'standard'
@@ -382,7 +382,12 @@ class MainReporter extends Reporter {
382
382
  const res = Response(this, req.context.id)
383
383
 
384
384
  try {
385
- await this.beforeRenderWorkerAllocatedListeners.fire(req)
385
+ await this.beforeRenderWorkerAllocatedListeners.fire(req, res)
386
+ if (req.context.clientNotification) {
387
+ const r = req.context.clientNotification
388
+ delete req.context.clientNotification
389
+ return r
390
+ }
386
391
 
387
392
  worker = await this._workersManager.allocate(req, {
388
393
  timeout: this.getReportTimeout(req)
@@ -14,7 +14,7 @@ module.exports = (reporter) => {
14
14
 
15
15
  reporter.templatingEngines = { cache: templatesCache }
16
16
 
17
- const contextExecutionChainMap = new WeakMap()
17
+ const contextExecutionChainMap = new Map()
18
18
  const executionFnParsedParamsMap = new Map()
19
19
  const executionAsyncResultsMap = new Map()
20
20
  const executionAsyncCallChainMap = new Map()
@@ -69,7 +69,7 @@ module.exports = (reporter) => {
69
69
  return templatingEnginesEvaluate(false, executionInfo, entityInfo, req)
70
70
  },
71
71
  waitForAsyncHelper: async (maybeAsyncContent) => {
72
- const executionChain = contextExecutionChainMap.get(context) || []
72
+ const executionChain = contextExecutionChainMap.get(context.__sandboxId) || []
73
73
  const executionId = executionChain[executionChain.length - 1]
74
74
 
75
75
  if (
@@ -99,7 +99,7 @@ module.exports = (reporter) => {
99
99
  return content
100
100
  },
101
101
  waitForAsyncHelpers: async () => {
102
- const executionChain = contextExecutionChainMap.get(context) || []
102
+ const executionChain = contextExecutionChainMap.get(context.__sandboxId) || []
103
103
  const executionId = executionChain[executionChain.length - 1]
104
104
 
105
105
  if (executionId != null && executionAsyncResultsMap.has(executionId)) {
@@ -115,7 +115,7 @@ module.exports = (reporter) => {
115
115
  }
116
116
  },
117
117
  addFinishListener: (fn) => {
118
- const executionChain = contextExecutionChainMap.get(context) || []
118
+ const executionChain = contextExecutionChainMap.get(context.__sandboxId) || []
119
119
  const executionId = executionChain[executionChain.length - 1]
120
120
 
121
121
  if (executionId && executionFinishListenersMap.has(executionId)) {
@@ -123,7 +123,7 @@ module.exports = (reporter) => {
123
123
  }
124
124
  },
125
125
  createAsyncHelperResult: (v) => {
126
- const executionChain = contextExecutionChainMap.get(context) || []
126
+ const executionChain = contextExecutionChainMap.get(context.__sandboxId) || []
127
127
  const executionId = executionChain[executionChain.length - 1]
128
128
 
129
129
  const asyncResultMap = executionAsyncResultsMap.get(executionId)
@@ -170,6 +170,7 @@ module.exports = (reporter) => {
170
170
 
171
171
  const normalizedHelpers = `${helpers || ''}`
172
172
  const executionFnParsedParamsKey = `entity:${entity.shortid || 'anonymous'}:helpers:${normalizedHelpers}`
173
+ let sandboxId
173
174
 
174
175
  const initFn = async (getTopLevelFunctions, compileScript) => {
175
176
  if (systemHelpersCache != null) {
@@ -213,112 +214,118 @@ module.exports = (reporter) => {
213
214
  }
214
215
 
215
216
  const executionFn = async ({ require, console, topLevelFunctions, context }) => {
217
+ sandboxId = context.__sandboxId
216
218
  const asyncResultMap = new Map()
217
219
  const asyncCallChainSet = new Set()
218
220
 
219
- if (!contextExecutionChainMap.has(context)) {
220
- contextExecutionChainMap.set(context, [])
221
+ if (!contextExecutionChainMap.has(sandboxId)) {
222
+ contextExecutionChainMap.set(sandboxId, [])
221
223
  }
222
224
 
223
- contextExecutionChainMap.get(context).push(executionId)
225
+ contextExecutionChainMap.get(sandboxId).push(executionId)
224
226
 
225
227
  executionAsyncResultsMap.set(executionId, asyncResultMap)
226
228
  executionAsyncCallChainMap.set(executionId, asyncCallChainSet)
227
229
  executionFinishListenersMap.set(executionId, reporter.createListenerCollection())
228
230
  executionFnParsedParamsMap.get(req.context.id).get(executionFnParsedParamsKey).resolve({ require, console, topLevelFunctions, context })
229
231
 
230
- const key = engine.buildTemplateCacheKey
231
- ? engine.buildTemplateCacheKey({ content }, req)
232
- : `template:${content}:${engine.name}`
233
-
234
- if (!templatesCache.has(key)) {
235
- try {
236
- templatesCache.set(key, engine.compile(content, { require }))
237
- } catch (e) {
238
- e.property = 'content'
239
- throw e
232
+ try {
233
+ const key = engine.buildTemplateCacheKey
234
+ ? engine.buildTemplateCacheKey({ content }, req)
235
+ : `template:${content}:${engine.name}`
236
+
237
+ if (!templatesCache.has(key)) {
238
+ try {
239
+ templatesCache.set(key, engine.compile(content, { require }))
240
+ } catch (e) {
241
+ e.property = 'content'
242
+ throw e
243
+ }
240
244
  }
241
- }
242
245
 
243
- const compiledTemplate = templatesCache.get(key)
244
- const wrappedTopLevelFunctions = {}
246
+ const compiledTemplate = templatesCache.get(key)
247
+ const wrappedTopLevelFunctions = {}
245
248
 
246
- for (const h of Object.keys(topLevelFunctions)) {
247
- // extra wrapping for enhance the error with the helper name
248
- wrappedTopLevelFunctions[h] = wrapHelperForHelperNameWhenError(topLevelFunctions[h], h, () => executionFnParsedParamsMap.has(req.context.id))
249
+ for (const h of Object.keys(topLevelFunctions)) {
250
+ // extra wrapping for enhance the error with the helper name
251
+ wrappedTopLevelFunctions[h] = wrapHelperForHelperNameWhenError(topLevelFunctions[h], h, () => executionFnParsedParamsMap.has(req.context.id))
249
252
 
250
- if (engine.getWrappingHelpersEnabled && engine.getWrappingHelpersEnabled(req) === false) {
251
- wrappedTopLevelFunctions[h] = engine.wrapHelper(wrappedTopLevelFunctions[h], { context })
252
- } else {
253
- wrappedTopLevelFunctions[h] = wrapHelperForAsyncSupport(wrappedTopLevelFunctions[h], asyncResultMap, asyncCallChainSet)
253
+ if (engine.getWrappingHelpersEnabled && engine.getWrappingHelpersEnabled(req) === false) {
254
+ wrappedTopLevelFunctions[h] = engine.wrapHelper(wrappedTopLevelFunctions[h], { context })
255
+ } else {
256
+ wrappedTopLevelFunctions[h] = wrapHelperForAsyncSupport(wrappedTopLevelFunctions[h], asyncResultMap, asyncCallChainSet)
257
+ }
254
258
  }
255
- }
256
259
 
257
- let contentResult = await engine.execute(compiledTemplate, wrappedTopLevelFunctions, data, { require })
260
+ let contentResult = await engine.execute(compiledTemplate, wrappedTopLevelFunctions, data, { require })
258
261
 
259
- const resolvedResultsMap = new Map()
262
+ const resolvedResultsMap = new Map()
260
263
 
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)
264
+ // we need to use the cloned map, because there can be a waitForAsyncHelper pending that needs the asyncResultMap values
265
+ let clonedMap = new Map(asyncResultMap)
263
266
 
264
- while (clonedMap.size > 0) {
265
- const keysEvaluated = [...clonedMap.keys()]
267
+ while (clonedMap.size > 0) {
268
+ const keysEvaluated = [...clonedMap.keys()]
266
269
 
267
- await Promise.all(keysEvaluated.map(async (k) => {
268
- const result = await clonedMap.get(k)
269
- asyncCallChainSet.delete(k)
270
- resolvedResultsMap.set(k, `${result}`)
271
- clonedMap.delete(k)
272
- }))
270
+ await Promise.all(keysEvaluated.map(async (k) => {
271
+ const result = await clonedMap.get(k)
272
+ asyncCallChainSet.delete(k)
273
+ resolvedResultsMap.set(k, `${result}`)
274
+ clonedMap.delete(k)
275
+ }))
273
276
 
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
- }
284
- }
277
+ // we need to remove the keys processed from the original map at this point
278
+ // (after the await) because during the async work the asyncResultMap will be read
279
+ for (const k of keysEvaluated) {
280
+ asyncResultMap.delete(k)
281
+ }
285
282
 
286
- while (contentResult.includes('{#asyncHelperResult')) {
287
- contentResult = contentResult.replace(/{#asyncHelperResult ([^{}]+)}/g, (str, p1) => {
288
- const asyncResultId = p1
289
- // this can happen if a child jsreport.templatingEngines.evaluate receives an async value from outer scope
290
- // because every evaluate uses a unique map of async results
291
- // example is the case when component receives as a value async thing
292
- // instead of returning "undefined" we let the outer eval to do the replace
293
- if (!resolvedResultsMap.has(asyncResultId)) {
294
- // returning asyncUnresolvedHelperResult just to avoid endless loop, after replace we put it back to asyncHelperResult
295
- return `{#asyncUnresolvedHelperResult ${asyncResultId}}`
283
+ // we want to process the new generated pending async results
284
+ if (asyncResultMap.size > 0) {
285
+ clonedMap = new Map(asyncResultMap)
296
286
  }
297
- return `${resolvedResultsMap.get(asyncResultId)}`
298
- })
299
- }
287
+ }
300
288
 
301
- contentResult = contentResult.replace(/asyncUnresolvedHelperResult/g, 'asyncHelperResult')
289
+ while (contentResult.includes('{#asyncHelperResult')) {
290
+ contentResult = contentResult.replace(/{#asyncHelperResult ([^{}]+)}/g, (str, p1) => {
291
+ const asyncResultId = p1
292
+ // this can happen if a child jsreport.templatingEngines.evaluate receives an async value from outer scope
293
+ // because every evaluate uses a unique map of async results
294
+ // example is the case when component receives as a value async thing
295
+ // instead of returning "undefined" we let the outer eval to do the replace
296
+ if (!resolvedResultsMap.has(asyncResultId)) {
297
+ // returning asyncUnresolvedHelperResult just to avoid endless loop, after replace we put it back to asyncHelperResult
298
+ return `{#asyncUnresolvedHelperResult ${asyncResultId}}`
299
+ }
300
+ return `${resolvedResultsMap.get(asyncResultId)}`
301
+ })
302
+ }
302
303
 
303
- await executionFinishListenersMap.get(executionId).fire()
304
+ contentResult = contentResult.replace(/asyncUnresolvedHelperResult/g, 'asyncHelperResult')
304
305
 
305
- contextExecutionChainMap.set(context, contextExecutionChainMap.get(context).filter((id) => id !== executionId))
306
+ await executionFinishListenersMap.get(executionId).fire()
306
307
 
307
- return {
308
- // handlebars escapes single brackets before execution to prevent errors on {#asset}
309
- // we need to unescape them later here, because at the moment the engine.execute finishes
310
- // the async helpers aren't executed yet
311
- content: engine.unescape ? engine.unescape(contentResult) : contentResult
308
+ return {
309
+ // handlebars escapes single brackets before execution to prevent errors on {#asset}
310
+ // we need to unescape them later here, because at the moment the engine.execute finishes
311
+ // the async helpers aren't executed yet
312
+ content: engine.unescape ? engine.unescape(contentResult) : contentResult
313
+ }
314
+ } finally {
315
+ // ensure we clean the execution from the chain always, even on errors
316
+ contextExecutionChainMap.set(sandboxId, contextExecutionChainMap.get(sandboxId).filter((id) => id !== executionId))
312
317
  }
313
318
  }
314
319
 
315
- // executionFnParsedParamsMap is there to cache parsed components helpers to speed up longer loops
316
- // we store there for the particular request and component a promise and only the first component gets compiled
317
- if (executionFnParsedParamsMap.get(req.context.id).has(executionFnParsedParamsKey)) {
318
- const { require, console, topLevelFunctions, context } = await (executionFnParsedParamsMap.get(req.context.id).get(executionFnParsedParamsKey).promise)
320
+ try {
321
+ // executionFnParsedParamsMap is there to cache parsed components helpers to speed up longer loops
322
+ // we store there for the particular request and component a promise and only the first component gets compiled
323
+ if (executionFnParsedParamsMap.get(req.context.id).has(executionFnParsedParamsKey)) {
324
+ const { require, console, topLevelFunctions, context } = await (executionFnParsedParamsMap.get(req.context.id).get(executionFnParsedParamsKey).promise)
325
+
326
+ return await executionFn({ require, console, topLevelFunctions, context })
327
+ }
319
328
 
320
- return executionFn({ require, console, topLevelFunctions, context })
321
- } else {
322
329
  const awaiter = {}
323
330
 
324
331
  awaiter.promise = new Promise((resolve) => {
@@ -326,67 +333,71 @@ module.exports = (reporter) => {
326
333
  })
327
334
 
328
335
  executionFnParsedParamsMap.get(req.context.id).set(executionFnParsedParamsKey, awaiter)
329
- }
330
336
 
331
- if (reporter.options.sandbox.cache && reporter.options.sandbox.cache.enabled === false) {
332
- templatesCache.reset()
333
- }
337
+ if (reporter.options.sandbox.cache && reporter.options.sandbox.cache.enabled === false) {
338
+ templatesCache.reset()
339
+ }
334
340
 
335
- try {
336
- return await reporter.runInSandbox({
337
- context: {
338
- ...(engine.createContext ? engine.createContext(req) : {})
339
- },
340
- userCode: normalizedHelpers,
341
- initFn,
342
- executionFn,
343
- currentPath: entityPath,
344
- onRequire: (moduleName, { context }) => {
345
- if (engine.onRequire) {
346
- return engine.onRequire(moduleName, { context })
341
+ try {
342
+ return await reporter.runInSandbox({
343
+ context: {
344
+ ...(engine.createContext ? engine.createContext(req) : {})
345
+ },
346
+ userCode: normalizedHelpers,
347
+ initFn,
348
+ executionFn,
349
+ currentPath: entityPath,
350
+ onRequire: (moduleName, { context }) => {
351
+ if (engine.onRequire) {
352
+ return engine.onRequire(moduleName, { context })
353
+ }
347
354
  }
355
+ }, req)
356
+ } catch (e) {
357
+ if (!handleErrors) {
358
+ throw e
348
359
  }
349
- }, req)
350
- } catch (e) {
351
- if (!handleErrors) {
352
- throw e
353
- }
354
360
 
355
- const nestedErrorWithEntity = e.entity != null
361
+ const nestedErrorWithEntity = e.entity != null
356
362
 
357
- const templatePath = req.template._id ? await reporter.folders.resolveEntityPath(req.template, 'templates', req) : 'anonymous'
363
+ const templatePath = req.template._id ? await reporter.folders.resolveEntityPath(req.template, 'templates', req) : 'anonymous'
358
364
 
359
- const newError = reporter.createError(`Error when evaluating engine ${engine.name} for template ${templatePath}`, { original: e })
365
+ const newError = reporter.createError(`Error when evaluating engine ${engine.name} for template ${templatePath}`, { original: e })
360
366
 
361
- if (templatePath !== 'anonymous' && !nestedErrorWithEntity) {
362
- const templateFound = await reporter.folders.resolveEntityFromPath(templatePath, 'templates', req)
367
+ if (templatePath !== 'anonymous' && !nestedErrorWithEntity) {
368
+ const templateFound = await reporter.folders.resolveEntityFromPath(templatePath, 'templates', req)
363
369
 
364
- if (templateFound != null) {
365
- newError.entity = {
366
- shortid: templateFound.entity.shortid,
367
- name: templateFound.entity.name,
368
- content
370
+ if (templateFound != null) {
371
+ newError.entity = {
372
+ shortid: templateFound.entity.shortid,
373
+ name: templateFound.entity.name,
374
+ content
375
+ }
369
376
  }
370
377
  }
371
- }
372
378
 
373
- if (!nestedErrorWithEntity && e.property !== 'content') {
374
- newError.property = 'helpers'
375
- }
379
+ if (!nestedErrorWithEntity && e.property !== 'content') {
380
+ newError.property = 'helpers'
381
+ }
376
382
 
377
- if (nestedErrorWithEntity) {
378
- // errors from nested assets evals needs an unwrap for some reason
379
- newError.entity = { ...e.entity }
380
- }
383
+ if (nestedErrorWithEntity) {
384
+ // errors from nested assets evals needs an unwrap for some reason
385
+ newError.entity = { ...e.entity }
386
+ }
381
387
 
382
- // we remove the decoratedSuffix (created from sandbox) from the stack trace (if it is there) because it
383
- // just creates noise and duplication when printing the error,
384
- // we just want the decoration on the message not in the stack trace
385
- if (e.decoratedSuffix != null && newError.stack.includes(e.decoratedSuffix)) {
386
- newError.stack = newError.stack.replace(e.decoratedSuffix, '')
387
- }
388
+ // we remove the decoratedSuffix (created from sandbox) from the stack trace (if it is there) because it
389
+ // just creates noise and duplication when printing the error,
390
+ // we just want the decoration on the message not in the stack trace
391
+ if (e.decoratedSuffix != null && newError.stack.includes(e.decoratedSuffix)) {
392
+ newError.stack = newError.stack.replace(e.decoratedSuffix, '')
393
+ }
388
394
 
389
- throw newError
395
+ throw newError
396
+ }
397
+ } finally {
398
+ if (sandboxId != null && contextExecutionChainMap.get(sandboxId)?.length === 0) {
399
+ contextExecutionChainMap.delete(sandboxId)
400
+ }
390
401
  }
391
402
  }
392
403
 
@@ -88,7 +88,7 @@ class WorkerReporter extends Reporter {
88
88
  require('@jsreport/ses')
89
89
 
90
90
  // eslint-disable-next-line
91
- lockdown({
91
+ repairIntrinsics({
92
92
  // don't change locale based methods which users may be using in their templates
93
93
  localeTaming: 'unsafe',
94
94
  errorTaming: 'unsafe',
@@ -108,6 +108,8 @@ class WorkerReporter extends Reporter {
108
108
  overrideTaming: 'severe'
109
109
  })
110
110
 
111
+ // NOTE: we need to add these overrides between repairIntrinsics and hardenIntrinsics
112
+ // for them to be valid.
111
113
  // in this mode we alias the unsafe methods to safe ones
112
114
  Buffer.allocUnsafe = function allocUnsafe (size) {
113
115
  return Buffer.alloc(size)
@@ -117,6 +119,9 @@ class WorkerReporter extends Reporter {
117
119
  return Buffer.alloc(size)
118
120
  }
119
121
 
122
+ // eslint-disable-next-line
123
+ hardenIntrinsics()
124
+
120
125
  // we also harden Buffer because we expose it to sandbox
121
126
  // eslint-disable-next-line
122
127
  harden(Buffer)
@@ -12,7 +12,7 @@ const ISOLATED_PACKAGE_JSON_CACHE = new Map()
12
12
  // to bring isolated modules across renders and without memory leaks.
13
13
  // most of the code is copied from node.js source code and adapted a bit
14
14
  // (you will see in some parts specific links to node.js source code counterpart for reference)
15
- function isolatedRequire (_moduleId, modulesMeta, requireFromRootDirectory) {
15
+ function isolatedRequire (_moduleId, modulesMeta, resolveModule) {
16
16
  const parentModule = typeof _moduleId !== 'string' ? _moduleId.parent : null
17
17
  const moduleId = parentModule ? _moduleId.moduleId : _moduleId
18
18
 
@@ -32,10 +32,14 @@ function isolatedRequire (_moduleId, modulesMeta, requireFromRootDirectory) {
32
32
  }
33
33
 
34
34
  const { rootModule, modulesCache, requireExtensions } = modulesMeta
35
-
36
- const fullModulePath = resolveFilename(ISOLATED_REQUIRE_RESOLVE_CACHE, requireFromRootDirectory.resolve, moduleId, { parentModulePath: parentModule?.path })
35
+ const fullModulePath = resolveFilename(ISOLATED_REQUIRE_RESOLVE_CACHE, resolveModule, moduleId, { parentModulePath: parentModule?.path })
37
36
 
38
37
  if (modulesCache[fullModulePath]) {
38
+ // if module was already tried to be loaded and ended with error we rethrow the error
39
+ if (modulesCache[fullModulePath].loadingError != null) {
40
+ throw modulesCache[fullModulePath].loadingError
41
+ }
42
+
39
43
  return modulesCache[fullModulePath].exports
40
44
  }
41
45
 
@@ -50,29 +54,33 @@ function isolatedRequire (_moduleId, modulesMeta, requireFromRootDirectory) {
50
54
  // https://github.com/nodejs/node/blob/v18.14.2/lib/internal/modules/cjs/loader.js#L1133
51
55
  // we can not add this to the IsolatedModule.prototype because we need access to other variables
52
56
  mod.require = function (id) {
53
- return isolatedRequire({ parent: this, moduleId: id }, modulesMeta, requireFromRootDirectory)
57
+ return isolatedRequire({ parent: this, moduleId: id }, modulesMeta, resolveModule)
54
58
  }
55
59
 
56
60
  modulesCache[fullModulePath] = mod
57
61
 
58
- mod.filename = fullModulePath
59
- mod.paths = Module._nodeModulePaths(path.dirname(fullModulePath))
62
+ try {
63
+ mod.filename = fullModulePath
64
+ mod.paths = Module._nodeModulePaths(path.dirname(fullModulePath))
60
65
 
61
- const extension = findLongestRegisteredExtension(fullModulePath, requireExtensions)
66
+ const extension = findLongestRegisteredExtension(fullModulePath, requireExtensions)
62
67
 
63
- // https://github.com/nodejs/node/blob/v18.14.2/lib/internal/modules/cjs/loader.js#L1113
64
- // allow .mjs to be overridden
65
- if (fullModulePath.endsWith('.mjs') && !requireExtensions['.mjs']) {
66
- throw createRequireESMError(fullModulePath)
67
- }
68
-
69
- const moduleResolver = requireExtensions[extension]
68
+ // https://github.com/nodejs/node/blob/v18.14.2/lib/internal/modules/cjs/loader.js#L1113
69
+ // allow .mjs to be overridden
70
+ if (fullModulePath.endsWith('.mjs') && !requireExtensions['.mjs']) {
71
+ throw createRequireESMError(fullModulePath)
72
+ }
70
73
 
71
- moduleResolver(mod, fullModulePath)
74
+ const moduleResolver = requireExtensions[extension]
72
75
 
73
- mod.loaded = true
76
+ moduleResolver(mod, fullModulePath)
74
77
 
75
- return mod.exports
78
+ mod.loaded = true
79
+ return mod.exports
80
+ } catch (error) {
81
+ mod.loadingError = error
82
+ throw error
83
+ }
76
84
  }
77
85
 
78
86
  function setDefaultRequireExtensions (currentExtensions, requireFromRootDirectory, compileScript) {
@@ -80,11 +80,24 @@ function doRequire (moduleId, requireFromRootDirectory, _requirePaths, modulesMe
80
80
  const _require = isolateModules ? isolatedRequire : requireFromRootDirectory
81
81
  const extraRequireParams = []
82
82
 
83
- if (isolateModules) {
84
- extraRequireParams.push(modulesMeta, requireFromRootDirectory)
83
+ const resolveModule = (mId, ...args) => {
84
+ let normalizedModuleId = mId
85
+
86
+ if (normalizedModuleId === '..' || normalizedModuleId === '.') {
87
+ // NOTE: we need to manually normalize because node has a bug
88
+ // https://github.com/nodejs/node/issues/47000
89
+ // when using require.resolve and using options.paths, it does not recognize for "..", "."
90
+ // to be relative, just other cases work like "../", "..\\",
91
+ // so when we detect this case we normalize it in order for node to resolve correctly
92
+ normalizedModuleId = normalizedModuleId + path.sep
93
+ }
94
+
95
+ return requireFromRootDirectory.resolve(normalizedModuleId, ...args)
85
96
  }
86
97
 
87
- const resolveModule = requireFromRootDirectory.resolve
98
+ if (isolateModules) {
99
+ extraRequireParams.push(modulesMeta, resolveModule)
100
+ }
88
101
 
89
102
  let result = executeRequire(_require, resolveModule, moduleId, searchedPaths, ...extraRequireParams)
90
103
 
@@ -25,6 +25,9 @@ module.exports = function createRunInSandbox (reporter) {
25
25
  // it may turn out it is a bad approach in assets so we gonna delete it here
26
26
  const executionFnName = `${nanoid()}_executionFn`
27
27
 
28
+ // creating new id different than execution to ensure user code can not get access to
29
+ // internal functions by using the __sandboxId
30
+ context.__sandboxId = nanoid()
28
31
  context[executionFnName] = executionFn
29
32
  context.__appDirectory = reporter.options.appDirectory
30
33
  context.__rootDirectory = reporter.options.rootDirectory
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsreport/jsreport-core",
3
- "version": "4.3.0",
3
+ "version": "4.4.0",
4
4
  "description": "javascript based business reporting",
5
5
  "keywords": [
6
6
  "report",
@@ -44,7 +44,7 @@
44
44
  "@jsreport/mingo": "2.4.1",
45
45
  "@jsreport/reap": "0.1.0",
46
46
  "@jsreport/serializator": "1.0.0",
47
- "@jsreport/ses": "1.0.1",
47
+ "@jsreport/ses": "1.1.0",
48
48
  "ajv": "6.12.6",
49
49
  "app-root-path": "3.0.0",
50
50
  "bytes": "3.1.2",