@usebruno/js 0.50.0 → 0.52.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/package.json +6 -6
- package/src/bru.js +29 -28
- package/src/bruno-request.js +4 -0
- package/src/bruno-response.js +4 -1
- package/src/grpc/bruno-grpc-request.js +52 -0
- package/src/grpc/bruno-grpc-response.js +43 -0
- package/src/grpc/grpc-message-list.js +74 -0
- package/src/grpc/grpc-message.js +20 -0
- package/src/grpc/grpc-metadata-list.js +214 -0
- package/src/grpc/grpc-metadata.js +21 -0
- package/src/grpc/grpc-script-runtime.js +302 -0
- package/src/index.js +2 -0
- package/src/interpolate-string.js +2 -1
- package/src/runtime/assert-runtime.js +1 -0
- package/src/runtime/script-runtime.js +20 -4
- package/src/runtime/test-runtime.js +15 -8
- package/src/sandbox/bundle-browser-rollup.js +32 -32
- package/src/sandbox/node-vm/cjs-loader.js +416 -51
- package/src/sandbox/node-vm/index.js +96 -65
- package/src/sandbox/node-vm/index.spec.js +896 -0
- package/src/sandbox/quickjs/index.js +101 -17
- package/src/sandbox/quickjs/shims/bru.js +54 -54
- package/src/sandbox/quickjs/shims/bruno-grpc.js +29 -0
- package/src/sandbox/quickjs/shims/grpc/bruno-grpc-request.js +47 -0
- package/src/sandbox/quickjs/shims/grpc/bruno-grpc-response.js +40 -0
- package/src/sandbox/quickjs/shims/grpc/grpc-message-list.js +29 -0
- package/src/sandbox/quickjs/shims/grpc/grpc-metadata-list.js +31 -0
- package/src/sandbox/quickjs/utils/index.js +42 -23
- package/src/utils/error-formatter.js +13 -9
- package/src/utils/error-formatter.spec.js +97 -2
- package/src/utils/results.js +30 -3
|
@@ -3,6 +3,37 @@ const fs = require('fs');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const { runScriptInNodeVm } = require('./index');
|
|
6
|
+
const { __resetNpmModuleStateForTests } = require('./cjs-loader');
|
|
7
|
+
|
|
8
|
+
// Windows denies symlink creation without developer mode / admin. Probe once at
|
|
9
|
+
// module load so the dependent tests can be marked skipped in the reporter
|
|
10
|
+
// instead of silently no-oping mid-test.
|
|
11
|
+
const symlinksSupported = (() => {
|
|
12
|
+
const target = fs.mkdtempSync(path.join(os.tmpdir(), 'bruno-symlink-probe-'));
|
|
13
|
+
const link = target + '-link';
|
|
14
|
+
try {
|
|
15
|
+
fs.symlinkSync(target, link, 'dir');
|
|
16
|
+
fs.unlinkSync(link);
|
|
17
|
+
return true;
|
|
18
|
+
} catch (e) {
|
|
19
|
+
if (e.code === 'EPERM' || e.code === 'ENOTSUP') return false;
|
|
20
|
+
throw e;
|
|
21
|
+
} finally {
|
|
22
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
})();
|
|
25
|
+
const itIfSymlinks = symlinksSupported ? it : it.skip;
|
|
26
|
+
|
|
27
|
+
const makePkg = (parentDir, pkgName, files) => {
|
|
28
|
+
const pkgDir = path.join(parentDir, pkgName);
|
|
29
|
+
fs.mkdirSync(pkgDir, { recursive: true });
|
|
30
|
+
for (const [relPath, content] of Object.entries(files)) {
|
|
31
|
+
const filePath = path.join(pkgDir, relPath);
|
|
32
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
33
|
+
fs.writeFileSync(filePath, content);
|
|
34
|
+
}
|
|
35
|
+
return pkgDir;
|
|
36
|
+
};
|
|
6
37
|
|
|
7
38
|
describe('node-vm sandbox', () => {
|
|
8
39
|
let testDir;
|
|
@@ -240,6 +271,256 @@ describe('node-vm sandbox', () => {
|
|
|
240
271
|
// Nested module should successfully access the additional root
|
|
241
272
|
expect(context.bru.setVar).toHaveBeenCalledWith('result', true);
|
|
242
273
|
});
|
|
274
|
+
|
|
275
|
+
it('should not cross-resolve npm package from a sibling additionalContextRoot when required by a collection script', async () => {
|
|
276
|
+
// Package lives only in additionalRoot/node_modules — the collection has
|
|
277
|
+
// no dependency declared for it.
|
|
278
|
+
const additionalRoot = path.join(testDir, 'shared');
|
|
279
|
+
makePkg(path.join(additionalRoot, 'node_modules'), 'shared-package', {
|
|
280
|
+
'index.js': 'module.exports = { fromShared: true };'
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// A COLLECTION script directly requiring `shared-package` must fail:
|
|
284
|
+
// native Node walk-up from the collection never reaches a sibling
|
|
285
|
+
// additional root's node_modules, and cross-root discovery for bare-name
|
|
286
|
+
// resolution is intentionally not implemented. The supported patterns
|
|
287
|
+
// are (a) require the package from a shared script that itself lives
|
|
288
|
+
// inside additionalRoot (see the next test), or (b) declare the dep in
|
|
289
|
+
// the collection's own package.json.
|
|
290
|
+
const script = `require('shared-package');`;
|
|
291
|
+
|
|
292
|
+
const context = {
|
|
293
|
+
bru: { setVar: jest.fn() },
|
|
294
|
+
console: console
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const scriptingConfig = {
|
|
298
|
+
additionalContextRoots: [additionalRoot]
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
await expect(
|
|
302
|
+
runScriptInNodeVm({ script, context, collectionPath, scriptingConfig })
|
|
303
|
+
).rejects.toThrow(/Could not resolve module "shared-package"/);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should resolve npm module required by a shared script in additionalContextRoots', async () => {
|
|
307
|
+
const additionalRoot = path.join(testDir, 'shared');
|
|
308
|
+
makePkg(path.join(additionalRoot, 'node_modules'), 'shared-util', {
|
|
309
|
+
'index.js': 'module.exports = { parse: function(s) { return JSON.parse(s); } };'
|
|
310
|
+
});
|
|
311
|
+
fs.writeFileSync(
|
|
312
|
+
path.join(additionalRoot, 'parser.js'),
|
|
313
|
+
'const sharedUtil = require("shared-util"); module.exports = { parse: sharedUtil.parse };'
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// Collection script requires the shared local script, which internally
|
|
317
|
+
// requires an npm package from the shared root's node_modules
|
|
318
|
+
const script = `
|
|
319
|
+
const parser = require('../shared/parser');
|
|
320
|
+
const result = parser.parse('{"ok":true}');
|
|
321
|
+
bru.setVar('result', result.ok);
|
|
322
|
+
`;
|
|
323
|
+
|
|
324
|
+
const context = {
|
|
325
|
+
bru: { setVar: jest.fn() },
|
|
326
|
+
console: console
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const scriptingConfig = {
|
|
330
|
+
additionalContextRoots: [additionalRoot]
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig });
|
|
334
|
+
|
|
335
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', true);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should walk up from a nested shared script to find its npm dependency', async () => {
|
|
339
|
+
// Structure:
|
|
340
|
+
// shared/
|
|
341
|
+
// node_modules/deep-dep/index.js ← package hoisted at shared root
|
|
342
|
+
// deep/nested/parser.js ← requires 'deep-dep'
|
|
343
|
+
const additionalRoot = path.join(testDir, 'shared');
|
|
344
|
+
const nestedDir = path.join(additionalRoot, 'deep', 'nested');
|
|
345
|
+
fs.mkdirSync(nestedDir, { recursive: true });
|
|
346
|
+
|
|
347
|
+
makePkg(path.join(additionalRoot, 'node_modules'), 'deep-dep', {
|
|
348
|
+
'index.js': 'module.exports = { walkedUp: true };'
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
fs.writeFileSync(
|
|
352
|
+
path.join(nestedDir, 'parser.js'),
|
|
353
|
+
'const dep = require("deep-dep"); module.exports = { ok: dep.walkedUp };'
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
const script = `
|
|
357
|
+
const parser = require('../shared/deep/nested/parser');
|
|
358
|
+
bru.setVar('result', parser.ok);
|
|
359
|
+
`;
|
|
360
|
+
|
|
361
|
+
const context = {
|
|
362
|
+
bru: { setVar: jest.fn() },
|
|
363
|
+
console: console
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const scriptingConfig = {
|
|
367
|
+
additionalContextRoots: [additionalRoot]
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig });
|
|
371
|
+
|
|
372
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', true);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
itIfSymlinks('should resolve npm modules when additionalContextRoots points at a symlink', async () => {
|
|
376
|
+
// Physical location of the shared root
|
|
377
|
+
const realShared = path.join(testDir, 'real-shared');
|
|
378
|
+
makePkg(path.join(realShared, 'node_modules'), 'symlinked-lib', {
|
|
379
|
+
'index.js': 'module.exports = { via: "symlink" };'
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// Shared script inside the real location that requires the npm package.
|
|
383
|
+
// Loaded through the symlink below.
|
|
384
|
+
fs.writeFileSync(
|
|
385
|
+
path.join(realShared, 'helper.js'),
|
|
386
|
+
'const pkg = require("symlinked-lib"); module.exports = { via: pkg.via };'
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
// User-facing symlink that Bruno is told to treat as the shared root.
|
|
390
|
+
const linkedShared = path.join(testDir, 'linked-shared');
|
|
391
|
+
fs.symlinkSync(realShared, linkedShared, 'dir');
|
|
392
|
+
|
|
393
|
+
const script = `
|
|
394
|
+
const helper = require('../linked-shared/helper');
|
|
395
|
+
bru.setVar('via', helper.via);
|
|
396
|
+
`;
|
|
397
|
+
|
|
398
|
+
const context = {
|
|
399
|
+
bru: { setVar: jest.fn() },
|
|
400
|
+
console: console
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const scriptingConfig = {
|
|
404
|
+
additionalContextRoots: [linkedShared]
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig });
|
|
408
|
+
|
|
409
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('via', 'symlink');
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
itIfSymlinks('should allow subpath imports into an npm-linked package', async () => {
|
|
413
|
+
// Physical location of a multi-file package outside every declared root.
|
|
414
|
+
// Subpath file utils.js — require('subpath-pkg/utils') maps to utils.js
|
|
415
|
+
// (a file, not a directory-with-index.js).
|
|
416
|
+
const externalPkg = makePkg(testDir, 'external-subpath-pkg', {
|
|
417
|
+
'package.json': JSON.stringify({ name: 'subpath-pkg', main: 'index.js' }),
|
|
418
|
+
'index.js': 'module.exports = { root: true };',
|
|
419
|
+
'utils.js': 'module.exports = { greet: () => "sub-hello" };'
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const nodeModulesDir = path.join(collectionPath, 'node_modules');
|
|
423
|
+
fs.mkdirSync(nodeModulesDir, { recursive: true });
|
|
424
|
+
fs.symlinkSync(externalPkg, path.join(nodeModulesDir, 'subpath-pkg'), 'dir');
|
|
425
|
+
|
|
426
|
+
const script = `
|
|
427
|
+
const utils = require('subpath-pkg/utils');
|
|
428
|
+
bru.setVar('result', utils.greet());
|
|
429
|
+
`;
|
|
430
|
+
|
|
431
|
+
const context = {
|
|
432
|
+
bru: { setVar: jest.fn() },
|
|
433
|
+
console: console
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
437
|
+
|
|
438
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'sub-hello');
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
itIfSymlinks('should allow internal relative requires inside an npm-linked package', async () => {
|
|
442
|
+
// Physical location of a multi-file package outside every declared root.
|
|
443
|
+
const externalPkg = makePkg(testDir, 'external-pkg', {
|
|
444
|
+
'package.json': JSON.stringify({ name: 'linked-pkg', main: 'index.js' }),
|
|
445
|
+
'index.js': 'const util = require("./util"); module.exports = { greet: util.greet };',
|
|
446
|
+
'util.js': 'module.exports = { greet: () => "hello" };'
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// npm-link style: collection has node_modules/<pkg> as a symlink to the
|
|
450
|
+
// physical location that lives outside the collection.
|
|
451
|
+
const nodeModulesDir = path.join(collectionPath, 'node_modules');
|
|
452
|
+
fs.mkdirSync(nodeModulesDir, { recursive: true });
|
|
453
|
+
fs.symlinkSync(externalPkg, path.join(nodeModulesDir, 'linked-pkg'), 'dir');
|
|
454
|
+
|
|
455
|
+
const script = `
|
|
456
|
+
const pkg = require('linked-pkg');
|
|
457
|
+
bru.setVar('result', pkg.greet());
|
|
458
|
+
`;
|
|
459
|
+
|
|
460
|
+
const context = {
|
|
461
|
+
bru: { setVar: jest.fn() },
|
|
462
|
+
console: console
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
466
|
+
|
|
467
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'hello');
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('should allow a package in collection node_modules to require a sibling package', async () => {
|
|
471
|
+
// Two packages installed side-by-side in the collection's node_modules —
|
|
472
|
+
// pkg-a transitively requires pkg-b.
|
|
473
|
+
const nodeModulesDir = path.join(collectionPath, 'node_modules');
|
|
474
|
+
makePkg(nodeModulesDir, 'pkg-a', {
|
|
475
|
+
'package.json': JSON.stringify({ name: 'pkg-a', main: 'index.js' }),
|
|
476
|
+
'index.js': 'const b = require("pkg-b"); module.exports = { value: b.value + 1 };'
|
|
477
|
+
});
|
|
478
|
+
makePkg(nodeModulesDir, 'pkg-b', {
|
|
479
|
+
'package.json': JSON.stringify({ name: 'pkg-b', main: 'index.js' }),
|
|
480
|
+
'index.js': 'module.exports = { value: 41 };'
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
const script = `
|
|
484
|
+
const a = require('pkg-a');
|
|
485
|
+
bru.setVar('result', a.value);
|
|
486
|
+
`;
|
|
487
|
+
|
|
488
|
+
const context = {
|
|
489
|
+
bru: { setVar: jest.fn() },
|
|
490
|
+
console: console
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
494
|
+
|
|
495
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 42);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
itIfSymlinks('should allow a scoped npm-linked package', async () => {
|
|
499
|
+
// Physical location of a scoped multi-file package outside every declared root.
|
|
500
|
+
const externalPkg = makePkg(testDir, 'external-scoped-pkg', {
|
|
501
|
+
'package.json': JSON.stringify({ name: '@bruno/scoped-pkg', main: 'index.js' }),
|
|
502
|
+
'index.js': 'const util = require("./util"); module.exports = { greet: util.greet };',
|
|
503
|
+
'util.js': 'module.exports = { greet: () => "scoped-hello" };'
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
const scopeDir = path.join(collectionPath, 'node_modules', '@bruno');
|
|
507
|
+
fs.mkdirSync(scopeDir, { recursive: true });
|
|
508
|
+
fs.symlinkSync(externalPkg, path.join(scopeDir, 'scoped-pkg'), 'dir');
|
|
509
|
+
|
|
510
|
+
const script = `
|
|
511
|
+
const pkg = require('@bruno/scoped-pkg');
|
|
512
|
+
bru.setVar('result', pkg.greet());
|
|
513
|
+
`;
|
|
514
|
+
|
|
515
|
+
const context = {
|
|
516
|
+
bru: { setVar: jest.fn() },
|
|
517
|
+
console: console
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig: {} });
|
|
521
|
+
|
|
522
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'scoped-hello');
|
|
523
|
+
});
|
|
243
524
|
});
|
|
244
525
|
|
|
245
526
|
describe('createCustomRequire - npm modules', () => {
|
|
@@ -333,6 +614,621 @@ describe('node-vm sandbox', () => {
|
|
|
333
614
|
});
|
|
334
615
|
});
|
|
335
616
|
|
|
617
|
+
describe('createCustomRequire - npm modules are shared across script executions', () => {
|
|
618
|
+
const scriptingConfig = { cacheModules: true };
|
|
619
|
+
|
|
620
|
+
beforeEach(() => {
|
|
621
|
+
__resetNpmModuleStateForTests();
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it('should evaluate an npm module once per process, not once per script context', async () => {
|
|
625
|
+
const marker = `_npmEvalCount_${Date.now()}`;
|
|
626
|
+
makePkg(path.join(collectionPath, 'node_modules'), 'counted-module', {
|
|
627
|
+
'index.js': `
|
|
628
|
+
process.${marker} = (process.${marker} || 0) + 1;
|
|
629
|
+
module.exports = { token: Symbol('counted') };
|
|
630
|
+
`
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
const script = `
|
|
634
|
+
const mod = require('counted-module');
|
|
635
|
+
bru.setVar('token', mod.token);
|
|
636
|
+
`;
|
|
637
|
+
const contextA = { bru: { setVar: jest.fn() }, console };
|
|
638
|
+
const contextB = { bru: { setVar: jest.fn() }, console };
|
|
639
|
+
|
|
640
|
+
await runScriptInNodeVm({ script, context: contextA, collectionPath, scriptingConfig });
|
|
641
|
+
await runScriptInNodeVm({ script, context: contextB, collectionPath, scriptingConfig });
|
|
642
|
+
|
|
643
|
+
expect(process[marker]).toBe(1);
|
|
644
|
+
expect(contextA.bru.setVar.mock.calls[0][1]).toBe(contextB.bru.setVar.mock.calls[0][1]);
|
|
645
|
+
delete process[marker];
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
it('should keep Date/Array/Object instanceof true for values from cached npm modules', async () => {
|
|
649
|
+
makePkg(path.join(collectionPath, 'node_modules'), 'realm-values', {
|
|
650
|
+
'index.js': `
|
|
651
|
+
module.exports = {
|
|
652
|
+
date: () => new Date(0),
|
|
653
|
+
array: () => [1],
|
|
654
|
+
object: () => ({ a: 1 }),
|
|
655
|
+
map: () => new Map([['k', 1]]),
|
|
656
|
+
set: () => new Set([1]),
|
|
657
|
+
regexp: () => /x/,
|
|
658
|
+
error: () => new Error('e')
|
|
659
|
+
};
|
|
660
|
+
`
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
const script = `
|
|
664
|
+
const v = require('realm-values');
|
|
665
|
+
bru.setVar('checks', [
|
|
666
|
+
v.date() instanceof Date,
|
|
667
|
+
v.array() instanceof Array,
|
|
668
|
+
v.object() instanceof Object,
|
|
669
|
+
v.map() instanceof Map,
|
|
670
|
+
v.set() instanceof Set,
|
|
671
|
+
v.regexp() instanceof RegExp,
|
|
672
|
+
v.error() instanceof Error
|
|
673
|
+
].every(Boolean));
|
|
674
|
+
`;
|
|
675
|
+
const context = { bru: { setVar: jest.fn() }, console };
|
|
676
|
+
|
|
677
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig });
|
|
678
|
+
|
|
679
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('checks', true);
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
it('should not let Object.freeze(bru) in one script poison later scripts', async () => {
|
|
683
|
+
makePkg(path.join(collectionPath, 'node_modules'), 'bru-freezer', {
|
|
684
|
+
'index.js': `
|
|
685
|
+
module.exports = {
|
|
686
|
+
freeze: () => { Object.freeze(bru); return true; },
|
|
687
|
+
read: (name) => bru.getVar(name)
|
|
688
|
+
};
|
|
689
|
+
`
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
const freezeScript = `
|
|
693
|
+
bru.setVar('froze', require('bru-freezer').freeze());
|
|
694
|
+
`;
|
|
695
|
+
const readScript = `
|
|
696
|
+
bru.setVar('seen', require('bru-freezer').read('who'));
|
|
697
|
+
`;
|
|
698
|
+
const contextA = { bru: { getVar: jest.fn(), setVar: jest.fn() }, console };
|
|
699
|
+
const contextB = {
|
|
700
|
+
bru: { getVar: jest.fn().mockReturnValue('B'), setVar: jest.fn() },
|
|
701
|
+
console
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
await runScriptInNodeVm({ script: freezeScript, context: contextA, collectionPath, scriptingConfig });
|
|
705
|
+
await runScriptInNodeVm({ script: readScript, context: contextB, collectionPath, scriptingConfig });
|
|
706
|
+
|
|
707
|
+
expect(contextA.bru.setVar).toHaveBeenCalledWith('froze', true);
|
|
708
|
+
expect(contextB.bru.setVar).toHaveBeenCalledWith('seen', 'B');
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
it('should let a cached npm module see the bru of the script currently running', async () => {
|
|
712
|
+
makePkg(path.join(collectionPath, 'node_modules'), 'bru-reader', {
|
|
713
|
+
'index.js': `module.exports = { read: (name) => bru.getVar(name) };`
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
const script = `
|
|
717
|
+
const reader = require('bru-reader');
|
|
718
|
+
bru.setVar('seen', reader.read('who'));
|
|
719
|
+
`;
|
|
720
|
+
const contextA = { bru: { getVar: jest.fn().mockReturnValue('A'), setVar: jest.fn() }, console };
|
|
721
|
+
const contextB = { bru: { getVar: jest.fn().mockReturnValue('B'), setVar: jest.fn() }, console };
|
|
722
|
+
|
|
723
|
+
await runScriptInNodeVm({ script, context: contextA, collectionPath, scriptingConfig });
|
|
724
|
+
await runScriptInNodeVm({ script, context: contextB, collectionPath, scriptingConfig });
|
|
725
|
+
|
|
726
|
+
expect(contextA.bru.setVar).toHaveBeenCalledWith('seen', 'A');
|
|
727
|
+
expect(contextB.bru.setVar).toHaveBeenCalledWith('seen', 'B');
|
|
728
|
+
expect(contextA.bru.getVar).toHaveBeenCalledTimes(1);
|
|
729
|
+
expect(contextB.bru.getVar).toHaveBeenCalledTimes(1);
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
it('should switch req dynamically but preserve the module-load URL snapshot', async () => {
|
|
733
|
+
makePkg(path.join(collectionPath, 'node_modules'), 'request-url-reader', {
|
|
734
|
+
'index.js': `
|
|
735
|
+
const urlAtLoad = req.getUrl();
|
|
736
|
+
module.exports = {
|
|
737
|
+
read: () => req.getUrl(),
|
|
738
|
+
readCaptured: () => urlAtLoad
|
|
739
|
+
};
|
|
740
|
+
`
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
const script = `
|
|
744
|
+
const reader = require('request-url-reader');
|
|
745
|
+
bru.setVar('dynamicUrl', reader.read());
|
|
746
|
+
bru.setVar('capturedUrl', reader.readCaptured());
|
|
747
|
+
`;
|
|
748
|
+
const makeContext = (url) => ({
|
|
749
|
+
bru: { setVar: jest.fn() },
|
|
750
|
+
req: { getUrl: jest.fn().mockReturnValue(url) },
|
|
751
|
+
console
|
|
752
|
+
});
|
|
753
|
+
const contextA = makeContext('https://example.com/a');
|
|
754
|
+
const contextB = makeContext('https://example.com/b');
|
|
755
|
+
|
|
756
|
+
await runScriptInNodeVm({ script, context: contextA, collectionPath, scriptingConfig });
|
|
757
|
+
await runScriptInNodeVm({ script, context: contextB, collectionPath, scriptingConfig });
|
|
758
|
+
|
|
759
|
+
expect(contextA.bru.setVar).toHaveBeenCalledWith('dynamicUrl', 'https://example.com/a');
|
|
760
|
+
expect(contextB.bru.setVar).toHaveBeenCalledWith('dynamicUrl', 'https://example.com/b');
|
|
761
|
+
expect(contextA.bru.setVar).toHaveBeenCalledWith('capturedUrl', 'https://example.com/a');
|
|
762
|
+
expect(contextB.bru.setVar).toHaveBeenCalledWith('capturedUrl', 'https://example.com/b');
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
it.each([
|
|
766
|
+
['the first-started script finishes first', 5, 40],
|
|
767
|
+
['the first-started script finishes last', 40, 5]
|
|
768
|
+
])('should keep interleaved executions bound to their own bru when %s', async (_, delayA, delayB) => {
|
|
769
|
+
makePkg(path.join(collectionPath, 'node_modules'), 'bru-reader-async', {
|
|
770
|
+
'index.js': `module.exports = { read: (name) => bru.getVar(name) };`
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
const scriptFor = (delayMs) => `
|
|
774
|
+
const reader = require('bru-reader-async');
|
|
775
|
+
await new Promise((resolve) => setTimeout(resolve, ${delayMs}));
|
|
776
|
+
bru.setVar('seen', reader.read('who'));
|
|
777
|
+
`;
|
|
778
|
+
const contextA = { bru: { getVar: jest.fn().mockReturnValue('A'), setVar: jest.fn() }, console };
|
|
779
|
+
const contextB = { bru: { getVar: jest.fn().mockReturnValue('B'), setVar: jest.fn() }, console };
|
|
780
|
+
|
|
781
|
+
await Promise.all([
|
|
782
|
+
runScriptInNodeVm({ script: scriptFor(delayA), context: contextA, collectionPath, scriptingConfig }),
|
|
783
|
+
runScriptInNodeVm({ script: scriptFor(delayB), context: contextB, collectionPath, scriptingConfig })
|
|
784
|
+
]);
|
|
785
|
+
|
|
786
|
+
expect(contextA.bru.setVar).toHaveBeenCalledWith('seen', 'A');
|
|
787
|
+
expect(contextB.bru.setVar).toHaveBeenCalledWith('seen', 'B');
|
|
788
|
+
expect(contextA.bru.getVar).toHaveBeenCalledTimes(1);
|
|
789
|
+
expect(contextB.bru.getVar).toHaveBeenCalledTimes(1);
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
it('should let a module that captured bru at load time talk to the current script', async () => {
|
|
793
|
+
makePkg(path.join(collectionPath, 'node_modules'), 'bru-capturer', {
|
|
794
|
+
'index.js': `
|
|
795
|
+
const captured = bru;
|
|
796
|
+
const { getVar } = bru;
|
|
797
|
+
module.exports = {
|
|
798
|
+
viaCaptured: (name) => captured.getVar(name),
|
|
799
|
+
viaDestructured: (name) => getVar(name),
|
|
800
|
+
hasSetVar: () => 'setVar' in captured,
|
|
801
|
+
setThroughCaptured: (name, value) => { captured.setVar(name, value); },
|
|
802
|
+
types: () => typeof captured + '/' + typeof console + '/' + typeof test
|
|
803
|
+
};
|
|
804
|
+
`
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
const script = `
|
|
808
|
+
const capturer = require('bru-capturer');
|
|
809
|
+
capturer.setThroughCaptured('seen', capturer.viaCaptured('who') + capturer.viaDestructured('who'));
|
|
810
|
+
bru.setVar('hasSetVar', capturer.hasSetVar());
|
|
811
|
+
bru.setVar('types', capturer.types());
|
|
812
|
+
`;
|
|
813
|
+
const makeContext = (who) => ({
|
|
814
|
+
bru: { getVar: jest.fn().mockReturnValue(who), setVar: jest.fn() },
|
|
815
|
+
test: () => {},
|
|
816
|
+
console
|
|
817
|
+
});
|
|
818
|
+
const contextA = makeContext('A');
|
|
819
|
+
const contextB = makeContext('B');
|
|
820
|
+
|
|
821
|
+
await runScriptInNodeVm({ script, context: contextA, collectionPath, scriptingConfig });
|
|
822
|
+
await runScriptInNodeVm({ script, context: contextB, collectionPath, scriptingConfig });
|
|
823
|
+
|
|
824
|
+
expect(contextA.bru.setVar).toHaveBeenCalledWith('seen', 'AA');
|
|
825
|
+
expect(contextB.bru.setVar).toHaveBeenCalledWith('seen', 'BB');
|
|
826
|
+
expect(contextB.bru.setVar).toHaveBeenCalledWith('hasSetVar', true);
|
|
827
|
+
expect(contextB.bru.setVar).toHaveBeenCalledWith('types', 'object/object/function');
|
|
828
|
+
expect(contextA.bru.getVar).toHaveBeenCalledTimes(2);
|
|
829
|
+
expect(contextB.bru.getVar).toHaveBeenCalledTimes(2);
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
it('should preserve mutable singleton state across script executions', async () => {
|
|
833
|
+
makePkg(path.join(collectionPath, 'node_modules'), 'stateful-module', {
|
|
834
|
+
'index.js': `
|
|
835
|
+
let count = 0;
|
|
836
|
+
module.exports = { next: () => ++count };
|
|
837
|
+
`
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
const script = `bru.setVar('count', require('stateful-module').next());`;
|
|
841
|
+
const contextA = { bru: { setVar: jest.fn() }, console };
|
|
842
|
+
const contextB = { bru: { setVar: jest.fn() }, console };
|
|
843
|
+
|
|
844
|
+
await runScriptInNodeVm({ script, context: contextA, collectionPath, scriptingConfig });
|
|
845
|
+
await runScriptInNodeVm({ script, context: contextB, collectionPath, scriptingConfig });
|
|
846
|
+
|
|
847
|
+
expect(contextA.bru.setVar).toHaveBeenCalledWith('count', 1);
|
|
848
|
+
expect(contextB.bru.setVar).toHaveBeenCalledWith('count', 2);
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
it('should preserve Bruno global identity and cross-realm behavior inside npm modules', async () => {
|
|
852
|
+
makePkg(path.join(collectionPath, 'node_modules'), 'identity-reader', {
|
|
853
|
+
'index.js': `
|
|
854
|
+
module.exports = {
|
|
855
|
+
sameBru: () => bru === globalThis.bru,
|
|
856
|
+
readArray: (value) => Array.isArray(value)
|
|
857
|
+
};
|
|
858
|
+
`
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
const array = [];
|
|
862
|
+
const context = { bru: { setVar: jest.fn() }, array, console };
|
|
863
|
+
const script = `
|
|
864
|
+
const reader = require('identity-reader');
|
|
865
|
+
bru.setVar('sameBru', reader.sameBru());
|
|
866
|
+
bru.setVar('arrayIsArray', reader.readArray(array));
|
|
867
|
+
`;
|
|
868
|
+
|
|
869
|
+
await runScriptInNodeVm({ script, context, collectionPath, scriptingConfig });
|
|
870
|
+
|
|
871
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('sameBru', true);
|
|
872
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('arrayIsArray', true);
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
it('should bind callbacks created by cached npm modules to their calling script', async () => {
|
|
876
|
+
makePkg(path.join(collectionPath, 'node_modules'), 'callback-runner', {
|
|
877
|
+
'index.js': `
|
|
878
|
+
module.exports = {
|
|
879
|
+
runLater: (callback) => new Promise((resolve) => {
|
|
880
|
+
setTimeout(() => { callback(); resolve(); }, 5);
|
|
881
|
+
})
|
|
882
|
+
};
|
|
883
|
+
`
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
const scriptFor = (value) => `
|
|
887
|
+
await require('callback-runner').runLater(() => bru.setVar('value', '${value}'));
|
|
888
|
+
`;
|
|
889
|
+
const contextA = { bru: { setVar: jest.fn() }, console };
|
|
890
|
+
const contextB = { bru: { setVar: jest.fn() }, console };
|
|
891
|
+
|
|
892
|
+
await Promise.all([
|
|
893
|
+
runScriptInNodeVm({ script: scriptFor('A'), context: contextA, collectionPath, scriptingConfig }),
|
|
894
|
+
runScriptInNodeVm({ script: scriptFor('B'), context: contextB, collectionPath, scriptingConfig })
|
|
895
|
+
]);
|
|
896
|
+
|
|
897
|
+
expect(contextA.bru.setVar).toHaveBeenCalledWith('value', 'A');
|
|
898
|
+
expect(contextB.bru.setVar).toHaveBeenCalledWith('value', 'B');
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
it('should keep bru available when req.onFail runs after the script ends', async () => {
|
|
902
|
+
let onFailHandler;
|
|
903
|
+
const req = {
|
|
904
|
+
onFail(callback) {
|
|
905
|
+
onFailHandler = callback;
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
const context = {
|
|
909
|
+
bru: { setVar: jest.fn() },
|
|
910
|
+
req,
|
|
911
|
+
console
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
await runScriptInNodeVm({
|
|
915
|
+
script: `
|
|
916
|
+
req.onFail(() => {
|
|
917
|
+
bru.setVar('token', 'after');
|
|
918
|
+
});
|
|
919
|
+
`,
|
|
920
|
+
context,
|
|
921
|
+
collectionPath,
|
|
922
|
+
scriptingConfig
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
expect(typeof onFailHandler).toBe('function');
|
|
926
|
+
onFailHandler(new Error('Connection failed'));
|
|
927
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('token', 'after');
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
it('should restore req.onFail after a syntax-error run so a later run uses a fresh wrapper', async () => {
|
|
931
|
+
let onFailHandler;
|
|
932
|
+
const req = {
|
|
933
|
+
onFail(callback) {
|
|
934
|
+
onFailHandler = callback;
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
const originalOnFail = req.onFail;
|
|
938
|
+
|
|
939
|
+
await expect(
|
|
940
|
+
runScriptInNodeVm({
|
|
941
|
+
script: 'this is not valid js {{{',
|
|
942
|
+
context: { bru: {}, req, console },
|
|
943
|
+
collectionPath,
|
|
944
|
+
scriptingConfig
|
|
945
|
+
})
|
|
946
|
+
).rejects.toThrow();
|
|
947
|
+
|
|
948
|
+
expect(req.onFail).toBe(originalOnFail);
|
|
949
|
+
|
|
950
|
+
const context = {
|
|
951
|
+
bru: { setVar: jest.fn() },
|
|
952
|
+
req,
|
|
953
|
+
console
|
|
954
|
+
};
|
|
955
|
+
await runScriptInNodeVm({
|
|
956
|
+
script: `
|
|
957
|
+
req.onFail(() => {
|
|
958
|
+
bru.setVar('token', 'after');
|
|
959
|
+
});
|
|
960
|
+
`,
|
|
961
|
+
context,
|
|
962
|
+
collectionPath,
|
|
963
|
+
scriptingConfig
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
expect(typeof onFailHandler).toBe('function');
|
|
967
|
+
onFailHandler(new Error('Connection failed'));
|
|
968
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('token', 'after');
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
it('should not expose loader internals as script globals', async () => {
|
|
972
|
+
const context = {
|
|
973
|
+
bru: { setVar: jest.fn() },
|
|
974
|
+
console
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
await runScriptInNodeVm({
|
|
978
|
+
script: `
|
|
979
|
+
bru.setVar('vm', typeof __brunoVmContext);
|
|
980
|
+
bru.setVar('cache', typeof __brunoLocalModuleCache);
|
|
981
|
+
`,
|
|
982
|
+
context,
|
|
983
|
+
collectionPath,
|
|
984
|
+
scriptingConfig
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('vm', 'undefined');
|
|
988
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('cache', 'undefined');
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
it('should read a missing key as undefined and still call it once a later execution provides a function', async () => {
|
|
992
|
+
makePkg(path.join(collectionPath, 'node_modules'), 'helper-caller', {
|
|
993
|
+
'index.js': `module.exports = { probe: () => typeof helper, run: () => helper('x') };`
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
const contextA = { bru: { setVar: jest.fn() }, helper: undefined, console };
|
|
997
|
+
await runScriptInNodeVm({
|
|
998
|
+
script: `bru.setVar('probe', require('helper-caller').probe());`,
|
|
999
|
+
context: contextA, collectionPath, scriptingConfig
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
const helper = jest.fn().mockReturnValue('called');
|
|
1003
|
+
const contextB = { bru: { setVar: jest.fn() }, helper, console };
|
|
1004
|
+
await runScriptInNodeVm({
|
|
1005
|
+
script: `bru.setVar('ran', require('helper-caller').run());`,
|
|
1006
|
+
context: contextB, collectionPath, scriptingConfig
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
expect(contextA.bru.setVar).toHaveBeenCalledWith('probe', 'undefined');
|
|
1010
|
+
expect(contextB.bru.setVar).toHaveBeenCalledWith('ran', 'called');
|
|
1011
|
+
expect(helper).toHaveBeenCalledWith('x');
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
it('should preserve primitive custom globals for npm modules', async () => {
|
|
1015
|
+
makePkg(path.join(collectionPath, 'node_modules'), 'primitive-reader', {
|
|
1016
|
+
'index.js': `
|
|
1017
|
+
module.exports = {
|
|
1018
|
+
read: () => [typeof scalar, scalar, typeof flag, flag].join(':')
|
|
1019
|
+
};
|
|
1020
|
+
`
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
const context = {
|
|
1024
|
+
bru: { setVar: jest.fn() },
|
|
1025
|
+
scalar: 'value',
|
|
1026
|
+
flag: false,
|
|
1027
|
+
console
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
await runScriptInNodeVm({
|
|
1031
|
+
script: `bru.setVar('result', require('primitive-reader').read());`,
|
|
1032
|
+
context,
|
|
1033
|
+
collectionPath,
|
|
1034
|
+
scriptingConfig
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
expect(context.bru.setVar).toHaveBeenCalledWith('result', 'string:value:boolean:false');
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
it('should re-evaluate a parent that snapshots a context-bound dependency at load time', async () => {
|
|
1041
|
+
makePkg(path.join(collectionPath, 'node_modules'), 'leaf-bru-snapshot', {
|
|
1042
|
+
'index.js': `module.exports = { who: bru.getVar('who') };`
|
|
1043
|
+
});
|
|
1044
|
+
makePkg(path.join(collectionPath, 'node_modules'), 'parent-bru-snapshot', {
|
|
1045
|
+
'index.js': `
|
|
1046
|
+
const leaf = require('leaf-bru-snapshot');
|
|
1047
|
+
module.exports = { who: leaf.who };
|
|
1048
|
+
`
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
const script = `bru.setVar('seen', require('parent-bru-snapshot').who);`;
|
|
1052
|
+
const contextA = { bru: { getVar: jest.fn().mockReturnValue('A'), setVar: jest.fn() }, console };
|
|
1053
|
+
const contextB = { bru: { getVar: jest.fn().mockReturnValue('B'), setVar: jest.fn() }, console };
|
|
1054
|
+
|
|
1055
|
+
await runScriptInNodeVm({ script, context: contextA, collectionPath, scriptingConfig });
|
|
1056
|
+
await runScriptInNodeVm({ script, context: contextB, collectionPath, scriptingConfig });
|
|
1057
|
+
|
|
1058
|
+
expect(contextA.bru.setVar).toHaveBeenCalledWith('seen', 'A');
|
|
1059
|
+
expect(contextB.bru.setVar).toHaveBeenCalledWith('seen', 'B');
|
|
1060
|
+
expect(contextA.bru.getVar).toHaveBeenCalledTimes(1);
|
|
1061
|
+
expect(contextB.bru.getVar).toHaveBeenCalledTimes(1);
|
|
1062
|
+
expect(contextA.bru.setVar).toHaveBeenCalledTimes(1);
|
|
1063
|
+
expect(contextB.bru.setVar).toHaveBeenCalledTimes(1);
|
|
1064
|
+
});
|
|
1065
|
+
|
|
1066
|
+
it('should re-evaluate a three-level parent chain that snapshots bru at the leaf', async () => {
|
|
1067
|
+
makePkg(path.join(collectionPath, 'node_modules'), 'deep-leaf-bru', {
|
|
1068
|
+
'index.js': `module.exports = { who: bru.getVar('who') };`
|
|
1069
|
+
});
|
|
1070
|
+
makePkg(path.join(collectionPath, 'node_modules'), 'deep-mid-bru', {
|
|
1071
|
+
'index.js': `
|
|
1072
|
+
const leaf = require('deep-leaf-bru');
|
|
1073
|
+
module.exports = { who: leaf.who };
|
|
1074
|
+
`
|
|
1075
|
+
});
|
|
1076
|
+
makePkg(path.join(collectionPath, 'node_modules'), 'deep-root-bru', {
|
|
1077
|
+
'index.js': `
|
|
1078
|
+
const mid = require('deep-mid-bru');
|
|
1079
|
+
module.exports = { who: mid.who };
|
|
1080
|
+
`
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
const script = `bru.setVar('seen', require('deep-root-bru').who);`;
|
|
1084
|
+
const contextA = { bru: { getVar: jest.fn().mockReturnValue('A'), setVar: jest.fn() }, console };
|
|
1085
|
+
const contextB = { bru: { getVar: jest.fn().mockReturnValue('B'), setVar: jest.fn() }, console };
|
|
1086
|
+
|
|
1087
|
+
await runScriptInNodeVm({ script, context: contextA, collectionPath, scriptingConfig });
|
|
1088
|
+
await runScriptInNodeVm({ script, context: contextB, collectionPath, scriptingConfig });
|
|
1089
|
+
|
|
1090
|
+
expect(contextA.bru.setVar).toHaveBeenCalledWith('seen', 'A');
|
|
1091
|
+
expect(contextB.bru.setVar).toHaveBeenCalledWith('seen', 'B');
|
|
1092
|
+
expect(contextA.bru.getVar).toHaveBeenCalledTimes(1);
|
|
1093
|
+
expect(contextB.bru.getVar).toHaveBeenCalledTimes(1);
|
|
1094
|
+
expect(contextA.bru.setVar).toHaveBeenCalledTimes(1);
|
|
1095
|
+
expect(contextB.bru.setVar).toHaveBeenCalledTimes(1);
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
it('should still share an inert transitive npm module tree across scripts', async () => {
|
|
1099
|
+
const marker = `_inertParentEval_${Date.now()}`;
|
|
1100
|
+
makePkg(path.join(collectionPath, 'node_modules'), 'inert-leaf', {
|
|
1101
|
+
'index.js': `module.exports = { token: Symbol('inert') };`
|
|
1102
|
+
});
|
|
1103
|
+
makePkg(path.join(collectionPath, 'node_modules'), 'inert-parent', {
|
|
1104
|
+
'index.js': `
|
|
1105
|
+
process.${marker} = (process.${marker} || 0) + 1;
|
|
1106
|
+
module.exports = { token: require('inert-leaf').token };
|
|
1107
|
+
`
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
const script = `bru.setVar('token', require('inert-parent').token);`;
|
|
1111
|
+
const contextA = { bru: { setVar: jest.fn() }, console };
|
|
1112
|
+
const contextB = { bru: { setVar: jest.fn() }, console };
|
|
1113
|
+
|
|
1114
|
+
await runScriptInNodeVm({ script, context: contextA, collectionPath, scriptingConfig });
|
|
1115
|
+
await runScriptInNodeVm({ script, context: contextB, collectionPath, scriptingConfig });
|
|
1116
|
+
|
|
1117
|
+
expect(process[marker]).toBe(1);
|
|
1118
|
+
expect(contextA.bru.setVar.mock.calls[0][1]).toBe(contextB.bru.setVar.mock.calls[0][1]);
|
|
1119
|
+
delete process[marker];
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
it('should re-evaluate a context-bound leaf required lazily from a shared parent', async () => {
|
|
1123
|
+
makePkg(path.join(collectionPath, 'node_modules'), 'lazy-leaf-bru', {
|
|
1124
|
+
'index.js': `
|
|
1125
|
+
let calls = 0;
|
|
1126
|
+
calls += 1;
|
|
1127
|
+
const who = bru.getVar('who');
|
|
1128
|
+
module.exports = {
|
|
1129
|
+
calls: () => calls,
|
|
1130
|
+
who: () => who,
|
|
1131
|
+
liveWho: () => bru.getVar('who')
|
|
1132
|
+
};
|
|
1133
|
+
`
|
|
1134
|
+
});
|
|
1135
|
+
makePkg(path.join(collectionPath, 'node_modules'), 'lazy-parent-inert', {
|
|
1136
|
+
'index.js': `
|
|
1137
|
+
module.exports = {
|
|
1138
|
+
loadLeaf: () => require('lazy-leaf-bru')
|
|
1139
|
+
};
|
|
1140
|
+
`
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
const script = `
|
|
1144
|
+
const leaf = require('lazy-parent-inert').loadLeaf();
|
|
1145
|
+
bru.setVar('calls', leaf.calls());
|
|
1146
|
+
bru.setVar('who', leaf.who());
|
|
1147
|
+
bru.setVar('liveWho', leaf.liveWho());
|
|
1148
|
+
`;
|
|
1149
|
+
const makeContext = (who) => ({
|
|
1150
|
+
bru: { getVar: jest.fn().mockReturnValue(who), setVar: jest.fn() },
|
|
1151
|
+
console
|
|
1152
|
+
});
|
|
1153
|
+
const contextA = makeContext('A');
|
|
1154
|
+
const contextB = makeContext('B');
|
|
1155
|
+
const contextC = makeContext('C');
|
|
1156
|
+
|
|
1157
|
+
await runScriptInNodeVm({ script, context: contextA, collectionPath, scriptingConfig });
|
|
1158
|
+
await runScriptInNodeVm({ script, context: contextB, collectionPath, scriptingConfig });
|
|
1159
|
+
await runScriptInNodeVm({ script, context: contextC, collectionPath, scriptingConfig });
|
|
1160
|
+
|
|
1161
|
+
expect(contextA.bru.setVar).toHaveBeenCalledWith('calls', 1);
|
|
1162
|
+
expect(contextB.bru.setVar).toHaveBeenCalledWith('calls', 1);
|
|
1163
|
+
expect(contextC.bru.setVar).toHaveBeenCalledWith('calls', 1);
|
|
1164
|
+
expect(contextA.bru.setVar).toHaveBeenCalledWith('who', 'A');
|
|
1165
|
+
expect(contextB.bru.setVar).toHaveBeenCalledWith('who', 'B');
|
|
1166
|
+
expect(contextC.bru.setVar).toHaveBeenCalledWith('who', 'C');
|
|
1167
|
+
expect(contextA.bru.setVar).toHaveBeenCalledWith('liveWho', 'A');
|
|
1168
|
+
expect(contextB.bru.setVar).toHaveBeenCalledWith('liveWho', 'B');
|
|
1169
|
+
expect(contextC.bru.setVar).toHaveBeenCalledWith('liveWho', 'C');
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
it('should re-evaluate both sides of a context-bound circular npm dependency', async () => {
|
|
1173
|
+
makePkg(path.join(collectionPath, 'node_modules'), 'cycle-a', {
|
|
1174
|
+
'index.js': `
|
|
1175
|
+
let calls = 0;
|
|
1176
|
+
calls += 1;
|
|
1177
|
+
exports.calls = () => calls;
|
|
1178
|
+
exports.who = bru.getVar('who');
|
|
1179
|
+
const b = require('cycle-b');
|
|
1180
|
+
exports.fromB = () => b.aWho();
|
|
1181
|
+
`
|
|
1182
|
+
});
|
|
1183
|
+
makePkg(path.join(collectionPath, 'node_modules'), 'cycle-b', {
|
|
1184
|
+
'index.js': `
|
|
1185
|
+
// Inert at load except for the cycle edge — must not stay shared with
|
|
1186
|
+
// a stale capture of cycle-a's exports across script runs.
|
|
1187
|
+
const a = require('cycle-a');
|
|
1188
|
+
exports.aWho = () => a.who;
|
|
1189
|
+
`
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
const script = `
|
|
1193
|
+
const a = require('cycle-a');
|
|
1194
|
+
bru.setVar('calls', a.calls());
|
|
1195
|
+
bru.setVar('who', a.who);
|
|
1196
|
+
bru.setVar('fromB', a.fromB());
|
|
1197
|
+
`;
|
|
1198
|
+
const makeContext = (who) => ({
|
|
1199
|
+
bru: { getVar: jest.fn().mockReturnValue(who), setVar: jest.fn() },
|
|
1200
|
+
console
|
|
1201
|
+
});
|
|
1202
|
+
const contextA = makeContext('A');
|
|
1203
|
+
const contextB = makeContext('B');
|
|
1204
|
+
|
|
1205
|
+
await runScriptInNodeVm({ script, context: contextA, collectionPath, scriptingConfig });
|
|
1206
|
+
await runScriptInNodeVm({ script, context: contextB, collectionPath, scriptingConfig });
|
|
1207
|
+
|
|
1208
|
+
expect(contextA.bru.setVar).toHaveBeenCalledWith('calls', 1);
|
|
1209
|
+
expect(contextB.bru.setVar).toHaveBeenCalledWith('calls', 1);
|
|
1210
|
+
expect(contextA.bru.setVar).toHaveBeenCalledWith('who', 'A');
|
|
1211
|
+
expect(contextB.bru.setVar).toHaveBeenCalledWith('who', 'B');
|
|
1212
|
+
expect(contextA.bru.setVar).toHaveBeenCalledWith('fromB', 'A');
|
|
1213
|
+
expect(contextB.bru.setVar).toHaveBeenCalledWith('fromB', 'B');
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
it('should keep collection-local modules per script context', async () => {
|
|
1217
|
+
const marker = `_localEvalCount_${Date.now()}`;
|
|
1218
|
+
fs.writeFileSync(
|
|
1219
|
+
path.join(collectionPath, 'local-counted.js'),
|
|
1220
|
+
`process.${marker} = (process.${marker} || 0) + 1; module.exports = {};`
|
|
1221
|
+
);
|
|
1222
|
+
const script = `require('./local-counted');`;
|
|
1223
|
+
|
|
1224
|
+
await runScriptInNodeVm({ script, context: { bru: {}, console }, collectionPath, scriptingConfig });
|
|
1225
|
+
await runScriptInNodeVm({ script, context: { bru: {}, console }, collectionPath, scriptingConfig });
|
|
1226
|
+
|
|
1227
|
+
expect(process[marker]).toBe(2);
|
|
1228
|
+
delete process[marker];
|
|
1229
|
+
});
|
|
1230
|
+
});
|
|
1231
|
+
|
|
336
1232
|
describe('createCustomRequire - Node.js builtin modules', () => {
|
|
337
1233
|
it('should load builtin modules (crypto)', async () => {
|
|
338
1234
|
const script = `
|