@usebruno/js 0.51.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 +15 -23
- package/src/bruno-request.js +4 -0
- 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 +18 -2
- package/src/runtime/test-runtime.js +12 -7
- package/src/sandbox/bundle-browser-rollup.js +32 -32
- package/src/sandbox/node-vm/cjs-loader.js +390 -39
- package/src/sandbox/node-vm/index.js +96 -65
- package/src/sandbox/node-vm/index.spec.js +616 -0
- package/src/sandbox/quickjs/index.js +101 -17
- 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,7 @@ 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');
|
|
6
7
|
|
|
7
8
|
// Windows denies symlink creation without developer mode / admin. Probe once at
|
|
8
9
|
// module load so the dependent tests can be marked skipped in the reporter
|
|
@@ -613,6 +614,621 @@ describe('node-vm sandbox', () => {
|
|
|
613
614
|
});
|
|
614
615
|
});
|
|
615
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
|
+
|
|
616
1232
|
describe('createCustomRequire - Node.js builtin modules', () => {
|
|
617
1233
|
it('should load builtin modules (crypto)', async () => {
|
|
618
1234
|
const script = `
|