driftjs-shared 0.0.1
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/dist/index-es.js +3536 -0
- package/package.json +25 -0
- package/src/constants.ts +3 -0
- package/src/evaluator.ts +101 -0
- package/src/index.ts +5 -0
- package/src/interpreter.ts +27 -0
- package/src/scope.ts +75 -0
- package/tests/utils.test.ts +78 -0
- package/types/index.ts +3 -0
- package/vite.config.ts +15 -0
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "driftjs-shared",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index-es.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": "./dist/index-es.js",
|
|
9
|
+
"require": "./dist/index-cjs.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"driftjs-compiler": "0.0.1"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"typescript": "^7.0.2",
|
|
18
|
+
"vite": "^8.1.5",
|
|
19
|
+
"vitest": "^4.1.10"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "vite build",
|
|
23
|
+
"test": "vitest run"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/constants.ts
ADDED
package/src/evaluator.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { setScopeValue, inScopeChain } from './scope.js';
|
|
2
|
+
import { astToJS } from 'driftjs-compiler';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Safely resolves an iterable object or array.
|
|
6
|
+
*/
|
|
7
|
+
export function resolveIterable(rawIter: any): any[] {
|
|
8
|
+
if (Array.isArray(rawIter)) return rawIter;
|
|
9
|
+
if (rawIter && typeof rawIter[Symbol.iterator] === 'function') {
|
|
10
|
+
return Array.from(rawIter);
|
|
11
|
+
}
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Executes a pre-compiled function string stored on AST constant nodes.
|
|
17
|
+
*/
|
|
18
|
+
export function executePrecompiledFn(node: any, scope: Record<string, any>, declaredVars?: Set<string>): any {
|
|
19
|
+
if (typeof node.__drift_fn__ === 'function') {
|
|
20
|
+
return node.__drift_fn__(scope, declaredVars, setScopeValue, inScopeChain, resolveIterable);
|
|
21
|
+
}
|
|
22
|
+
if (!node._executableFn) {
|
|
23
|
+
node._executableFn = typeof node.__drift_fn__ === 'string'
|
|
24
|
+
? new Function('return (' + node.__drift_fn__ + ')')()
|
|
25
|
+
: node.__drift_fn__;
|
|
26
|
+
}
|
|
27
|
+
return node._executableFn(scope, declaredVars, setScopeValue, inScopeChain, resolveIterable);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Evaluates any JS expression (AST node, pre-compiled wrapper, function, or primitive).
|
|
32
|
+
*/
|
|
33
|
+
export function evaluateExpression(node: any, scope: Record<string, any>, declaredVars?: Set<string>): any {
|
|
34
|
+
if (node === null || node === undefined) return node;
|
|
35
|
+
|
|
36
|
+
if (typeof node === 'function') {
|
|
37
|
+
return node(scope, declaredVars, setScopeValue, inScopeChain, resolveIterable);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (typeof node === 'object' && node !== null && '__drift_fn__' in node) {
|
|
41
|
+
return executePrecompiledFn(node, scope, declaredVars);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (typeof node !== 'object' || node === null) {
|
|
45
|
+
return node;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const codeStr = astToJS(node);
|
|
49
|
+
if (!codeStr || codeStr.trim().length === 0) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let executableFn: any;
|
|
54
|
+
try {
|
|
55
|
+
executableFn = new Function('scope', 'declaredVars', 'setScopeValue', 'inScopeChain', 'resolveIterable', 'return (' + codeStr + ')');
|
|
56
|
+
} catch {
|
|
57
|
+
executableFn = new Function('scope', 'declaredVars', 'setScopeValue', 'inScopeChain', 'resolveIterable', codeStr);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return executableFn(scope, declaredVars, setScopeValue, inScopeChain, resolveIterable);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolves constant or variable values against scope.
|
|
65
|
+
*/
|
|
66
|
+
export function resolveValue(val: any, scope: Record<string, any>, declaredVars?: Set<string>): any {
|
|
67
|
+
if (val === null || val === undefined) return val;
|
|
68
|
+
if (typeof val === 'string') return inScopeChain(scope, val) ? scope[val] : val;
|
|
69
|
+
if (typeof val === 'object' && (val.type || '__drift_fn__' in val || typeof val._executableFn === 'function')) {
|
|
70
|
+
return evaluateExpression(val, scope, declaredVars);
|
|
71
|
+
}
|
|
72
|
+
return val;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Safely unwraps an imported component module (handling ESM default exports,
|
|
77
|
+
* CompiledModule wrappers, and program objects).
|
|
78
|
+
*/
|
|
79
|
+
export function resolveComponentModule(raw: any): any | null {
|
|
80
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
81
|
+
if (Array.isArray(raw.bytecode) || ArrayBuffer.isView(raw.bytecode)) return raw;
|
|
82
|
+
if (raw.program && (Array.isArray(raw.program.bytecode) || ArrayBuffer.isView(raw.program.bytecode))) return raw.program;
|
|
83
|
+
if (raw.default) return resolveComponentModule(raw.default);
|
|
84
|
+
if (raw.compiledModule) return resolveComponentModule(raw.compiledModule);
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Evaluates a props specification object (mapping prop keys to static values or expression ASTs)
|
|
90
|
+
* against a VM scope, returning a plain JavaScript props object.
|
|
91
|
+
*/
|
|
92
|
+
export function evaluatePropsSpec(propsSpec: Record<string, any> | null | undefined, scope: Record<string, any>, declaredVars?: Set<string>): Record<string, any> {
|
|
93
|
+
if (!propsSpec || typeof propsSpec !== 'object') return {};
|
|
94
|
+
const res: Record<string, any> = {};
|
|
95
|
+
for (const key of Object.keys(propsSpec)) {
|
|
96
|
+
if (key === '__drift_props__') continue;
|
|
97
|
+
const rawVal = propsSpec[key];
|
|
98
|
+
res[key] = evaluateExpression(rawVal, scope, declaredVars);
|
|
99
|
+
}
|
|
100
|
+
return res;
|
|
101
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { evaluateExpression, executePrecompiledFn } from './evaluator.js';
|
|
2
|
+
import { setScopeValue } from './scope.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Interprets a block of Acorn AST statements (used by <script> AST and functions).
|
|
6
|
+
*/
|
|
7
|
+
export function executeBlockStatement(statements: any, scope: Record<string, any>, declaredVars?: Set<string>): any {
|
|
8
|
+
let result: any;
|
|
9
|
+
if (!statements) return result;
|
|
10
|
+
|
|
11
|
+
if (typeof statements === 'function') {
|
|
12
|
+
return statements(scope, declaredVars, setScopeValue);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (typeof statements === 'object' && statements !== null && '__drift_fn__' in statements) {
|
|
16
|
+
return executePrecompiledFn(statements, scope, declaredVars);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (Array.isArray(statements)) {
|
|
20
|
+
for (const stmt of statements) {
|
|
21
|
+
result = evaluateExpression(stmt, scope, declaredVars);
|
|
22
|
+
}
|
|
23
|
+
} else if (statements && typeof statements === 'object') {
|
|
24
|
+
result = evaluateExpression(statements, scope, declaredVars);
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
package/src/scope.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sets a variable in scope, updating parent scope if it exists higher in prototype chain.
|
|
3
|
+
*/
|
|
4
|
+
export function setScopeValue(targetScope: Record<string, any>, name: string, val: any): void {
|
|
5
|
+
if (!targetScope || typeof targetScope !== 'object') return;
|
|
6
|
+
|
|
7
|
+
let curr: any = targetScope;
|
|
8
|
+
let setOn: any = null;
|
|
9
|
+
const dirtyFns: Set<(name: string) => void> = new Set();
|
|
10
|
+
|
|
11
|
+
// 1. Traverse targetScope's prototype chain to find which scope object owns `name`
|
|
12
|
+
while (curr && curr !== Object.prototype) {
|
|
13
|
+
if (Object.prototype.hasOwnProperty.call(curr, name)) {
|
|
14
|
+
curr[name] = val;
|
|
15
|
+
setOn = curr;
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
curr = Object.getPrototypeOf(curr);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 2. If `name` was not found on any prototype in the chain, declare it as an own property on targetScope
|
|
22
|
+
if (!setOn) {
|
|
23
|
+
targetScope[name] = val;
|
|
24
|
+
setOn = targetScope;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 3. Collect dirty notification functions from targetScope up to setOn
|
|
28
|
+
let scan: any = targetScope;
|
|
29
|
+
while (scan && scan !== Object.prototype) {
|
|
30
|
+
if (typeof scan.__drift_mark_dirty__ === 'function') {
|
|
31
|
+
dirtyFns.add(scan.__drift_mark_dirty__);
|
|
32
|
+
}
|
|
33
|
+
if (scan === setOn) {
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
scan = Object.getPrototypeOf(scan);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 4. Trigger dirty marking on all affected scope VMs
|
|
40
|
+
for (const fn of dirtyFns) {
|
|
41
|
+
try {
|
|
42
|
+
fn(name);
|
|
43
|
+
} catch {
|
|
44
|
+
// ignore errors
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Writes back declared variables from function scope to enclosing target scope.
|
|
51
|
+
*/
|
|
52
|
+
export function syncDeclaredVars(fromScope: Record<string, any>, toScope: Record<string, any>, declaredVars?: Set<string>): void {
|
|
53
|
+
if (!declaredVars) return;
|
|
54
|
+
for (const name of declaredVars) {
|
|
55
|
+
if (Object.prototype.hasOwnProperty.call(fromScope, name)) {
|
|
56
|
+
setScopeValue(toScope, name, fromScope[name]);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Safely checks if a property exists on scope or any of its parent scopes,
|
|
63
|
+
* stopping before Object.prototype to prevent prototype pollution / scope hijacking.
|
|
64
|
+
*/
|
|
65
|
+
export function inScopeChain(scope: any, name: string): boolean {
|
|
66
|
+
if (!scope || typeof scope !== 'object') return false;
|
|
67
|
+
let curr: any = scope;
|
|
68
|
+
while (curr && curr !== Object.prototype) {
|
|
69
|
+
if (Object.prototype.hasOwnProperty.call(curr, name)) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
curr = Object.getPrototypeOf(curr);
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
evaluateExpression,
|
|
4
|
+
executeBlockStatement,
|
|
5
|
+
setScopeValue,
|
|
6
|
+
syncDeclaredVars,
|
|
7
|
+
resolveIterable,
|
|
8
|
+
MAX_REGISTERS,
|
|
9
|
+
} from '../src/index.js';
|
|
10
|
+
|
|
11
|
+
describe('driftjs-shared Module', () => {
|
|
12
|
+
it('exports MAX_REGISTERS constant equal to 256', () => {
|
|
13
|
+
expect(MAX_REGISTERS).toBe(256);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('evaluates binary, logical, and member expressions', () => {
|
|
17
|
+
const scope = { user: { age: 25 }, threshold: 20 };
|
|
18
|
+
const expr = {
|
|
19
|
+
type: 'BinaryExpression',
|
|
20
|
+
operator: '>',
|
|
21
|
+
left: {
|
|
22
|
+
type: 'MemberExpression',
|
|
23
|
+
object: { type: 'Identifier', name: 'user' },
|
|
24
|
+
property: { type: 'Identifier', name: 'age' },
|
|
25
|
+
},
|
|
26
|
+
right: { type: 'Identifier', name: 'threshold' },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
expect(evaluateExpression(expr, scope)).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('sets scope value up the prototype chain', () => {
|
|
33
|
+
const parentScope = { count: 0 };
|
|
34
|
+
const childScope = Object.create(parentScope);
|
|
35
|
+
|
|
36
|
+
setScopeValue(childScope, 'count', 5);
|
|
37
|
+
expect(parentScope.count).toBe(5);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('resolves iterables cleanly', () => {
|
|
41
|
+
expect(resolveIterable([1, 2, 3])).toEqual([1, 2, 3]);
|
|
42
|
+
expect(resolveIterable(new Set(['a', 'b']))).toEqual(['a', 'b']);
|
|
43
|
+
expect(resolveIterable(null)).toEqual([]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('executes block statements and updates scope', () => {
|
|
47
|
+
const scope = { val: 10 };
|
|
48
|
+
const statements = [
|
|
49
|
+
{
|
|
50
|
+
type: 'ExpressionStatement',
|
|
51
|
+
expression: {
|
|
52
|
+
type: 'AssignmentExpression',
|
|
53
|
+
operator: '=',
|
|
54
|
+
left: { type: 'Identifier', name: 'val' },
|
|
55
|
+
right: { type: 'Literal', value: 42 },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
executeBlockStatement(statements, scope);
|
|
61
|
+
expect(scope.val).toBe(42);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('prevents prototype pollution & scope lookup hijacking for built-in Object properties', () => {
|
|
65
|
+
const scope = { name: 'Alice' };
|
|
66
|
+
const exprToString = { type: 'Identifier', name: 'toString' };
|
|
67
|
+
const exprValueOf = { type: 'Identifier', name: 'valueOf' };
|
|
68
|
+
const exprConstructor = { type: 'Identifier', name: 'constructor' };
|
|
69
|
+
|
|
70
|
+
expect(evaluateExpression(exprToString, scope)).toBeUndefined();
|
|
71
|
+
expect(evaluateExpression(exprValueOf, scope)).toBeUndefined();
|
|
72
|
+
expect(evaluateExpression(exprConstructor, scope)).toBeUndefined();
|
|
73
|
+
|
|
74
|
+
// User-declared toString override should resolve correctly
|
|
75
|
+
const customScope = { toString: 'Custom String' };
|
|
76
|
+
expect(evaluateExpression(exprToString, customScope)).toBe('Custom String');
|
|
77
|
+
});
|
|
78
|
+
});
|
package/types/index.ts
ADDED
package/vite.config.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
build: {
|
|
6
|
+
lib: {
|
|
7
|
+
entry: path.resolve(__dirname, 'src/index.ts'),
|
|
8
|
+
name: 'DriftUtils',
|
|
9
|
+
fileName: (format) => `index-${format}.js`,
|
|
10
|
+
},
|
|
11
|
+
rollupOptions: {
|
|
12
|
+
external: [],
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
});
|