unexpectedly 0.0.7 → 1.0.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/runner.js CHANGED
@@ -1,81 +1,152 @@
1
- 'use strict'
1
+ import {ModType} from './modtype.js';
2
+ import {createRequire} from 'node:module';
3
+ import {mapObj} from './utils.js';
4
+ import path from 'node:path';
5
+ import vm from 'node:vm';
2
6
 
3
- const fs = require('fs')
4
- const vm = require('vm')
5
- const vm2 = require('vm2')
7
+ const vmGlobals = new vm.Script('Object.getOwnPropertyNames(globalThis)')
8
+ .runInNewContext()
9
+ .filter(f => !f.startsWith('global'));
10
+ vmGlobals.push('process', 'Buffer');
6
11
 
7
- class VMScriptLines extends vm2.VMScript {
8
- constructor (code, { filename, lineOffset = 0, columnOffset = 0 } = {}) {
9
- super(code, filename)
10
- this._lineOffset = lineOffset
11
- this._columnOffset = columnOffset
12
+ async function linker(specifier, referencingModule, cache) {
13
+ // Note: not convinced the cache actually does anything, due to timing.
14
+ if (cache?.has(specifier)) {
15
+ return cache.get(specifier);
12
16
  }
17
+ const m = await import(specifier);
18
+ const exportNames = Object.keys(m);
19
+ const imported = new vm.SyntheticModule(
20
+ exportNames,
21
+ // eslint-disable-next-line func-names
22
+ function() {
23
+ // eslint-disable-next-line no-invalid-this
24
+ exportNames.forEach(key => this.setExport(key, m[key]));
25
+ },
26
+ {identifier: specifier, context: referencingModule.context}
27
+ );
13
28
 
14
- compile () {
15
- if (this._compiled) {
16
- return this
17
- }
18
-
19
- this._compiled = new vm.Script(this.code, {
20
- filename: this.filename,
21
- displayErrors: false,
22
- lineOffset: this._lineOffset,
23
- columnOffset: this._columnOffset
24
- })
25
-
26
- return this
27
- }
29
+ cache?.set(specifier, imported);
30
+ return imported;
28
31
  }
29
32
 
30
33
  /**
31
- * Fancy "eval"
34
+ * Fancy "eval". This is NOT for adding security, even though we're using
35
+ * node:vm. This is about providing:
36
+ *
37
+ * - Good backtraces with file/line/column information correct, even when the
38
+ * code was inlined into a test.
39
+ * - A require() and import() that are relative to the source file.
32
40
  *
33
41
  * @class Runner
34
42
  */
35
- class Runner {
36
- constructor ({
37
- filename = null,
38
- text = null,
43
+ export class Runner {
44
+ #columnOffset;
45
+ #dirname;
46
+ #env;
47
+ #filename;
48
+ #lineOffset;
49
+ #sandbox;
50
+ #type;
51
+
52
+ constructor({
53
+ filename = null, // Resolved
39
54
  lineOffset = 0,
40
55
  columnOffset = 0,
41
- sandbox = null
56
+ sandbox = null,
57
+ env = {},
58
+ type = null,
42
59
  } = {}) {
43
- if (!text) {
44
- if (!filename) {
45
- throw new Error('Either file or text is required')
46
- }
47
- text = fs.readFileSync(filename, 'utf8')
48
- }
49
- this.filename = filename
50
- this.sandbox = sandbox
51
- this.script = new VMScriptLines(text, {
52
- filename,
53
- lineOffset,
54
- columnOffset
55
- })
60
+ this.#filename = filename;
61
+ this.#dirname = path.dirname(filename);
62
+ this.#env = env;
63
+ this.#sandbox = sandbox;
64
+ this.#columnOffset = columnOffset;
65
+ this.#lineOffset = lineOffset;
66
+ this.#type = type;
56
67
  }
57
68
 
58
- run (extra = {}, ...params) {
59
- const nvm = new vm2.NodeVM({
60
- console: 'inherit',
61
- require: { // TODO: Think about how much of this is for security?
62
- external: true,
63
- builtin: ['*']
69
+ async parse(text, extra = {}) {
70
+ if (!this.#type) {
71
+ this.#type = await ModType.find(this.#filename);
72
+ }
73
+ // Find package.json relative to test, decide whether we're commonjs or
74
+ // module.
75
+ const exports = {};
76
+ const context = {
77
+ ...mapObj(vmGlobals, g => (
78
+ (typeof globalThis[g] === 'function') ?
79
+ globalThis[g] :
80
+ {...globalThis[g]}
81
+ )),
82
+ require: createRequire(this.#filename),
83
+ exports,
84
+ module: {
85
+ exports,
86
+ filename: this.#filename,
87
+ id: this.#filename,
88
+ path: this.#dirname,
64
89
  },
65
- sandbox: Object.assign({}, this.sandbox, extra)
66
- })
67
- let f = nvm.run(this.script, this.filename)
68
- if (!f) {
69
- throw new Error('Nothing exported')
90
+ __filename: this.#filename,
91
+ __dirname: this.#dirname,
92
+ ...this.#sandbox,
93
+ ...extra,
94
+ };
95
+ context.process.env = this.#env;
96
+ context.global = context;
97
+ context.globalThis = context;
98
+ vm.createContext(context);
99
+
100
+ let f = null;
101
+ const imports = new Map();
102
+
103
+ if (this.#type === 'module') {
104
+ if (text.indexOf('export') === -1) {
105
+ text = `export default ${text}`; // Most common case
106
+ }
107
+
108
+ const mod = new vm.SourceTextModule(text, {
109
+ context,
110
+ id: this.#filename,
111
+ lineOffset: this.#lineOffset,
112
+ columnOffset: this.#columnOffset,
113
+ });
114
+
115
+ await mod.link((specifier, referencingModule) => {
116
+ if (path.isAbsolute(specifier) || !/^\.\.?\//.test(specifier)) {
117
+ return linker(specifier, referencingModule, imports);
118
+ }
119
+ return linker(
120
+ path.resolve(path.dirname(this.#filename), specifier),
121
+ referencingModule,
122
+ imports
123
+ );
124
+ });
125
+ await mod.evaluate();
126
+ f = mod.namespace;
127
+ } else {
128
+ if (text.indexOf('exports') === -1) {
129
+ text = `module.exports= ${text}`; // Most common case
130
+ }
131
+
132
+ const script = new vm.Script(text, {
133
+ filename: this.#filename,
134
+ lineOffset: this.#lineOffset,
135
+ columnOffset: this.#columnOffset,
136
+ });
137
+ f = script.runInContext(context);
70
138
  }
139
+ return f;
140
+ }
141
+
142
+ async run(text, extra = {}, ...params) {
143
+ let f = await this.parse(text, extra);
71
144
  if (typeof f !== 'function') {
72
- f = f.test
145
+ f = f?.test ?? f?.default;
73
146
  if (typeof f !== 'function') {
74
- throw new Error('Must export function or {test}')
147
+ throw new Error('Must export function or {test}');
75
148
  }
76
149
  }
77
- return f.apply(null, params)
150
+ return f(...params);
78
151
  }
79
152
  }
80
-
81
- module.exports = Runner