feat: initial swcx

This commit is contained in:
cha0s 2024-06-16 08:01:01 -05:00
parent ebb34eef95
commit 2f93e42106
35 changed files with 2178 additions and 0 deletions

View File

@ -28,6 +28,14 @@ module.exports = {
extends: ['eslint:recommended'],
overrides: [
// Tests
{
files: ['**/*.test.{js,jsx,ts,tsx}'],
rules: {
'no-empty-pattern': 'off',
},
},
// React
{
files: ['**/*.{js,jsx,ts,tsx}'],

1
.gitignore vendored
View File

@ -3,4 +3,5 @@ node_modules
/app/data
/.cache
/build
/coverage
.env

5
app/swcx/builders.js Normal file
View File

@ -0,0 +1,5 @@
export async function first(code) {
const {parse} = await import('@swc/core');
const ast = await parse(code);
return ast.body[0].expression;
}

47
app/swcx/evaluate.js Normal file
View File

@ -0,0 +1,47 @@
import {unwrap} from '@/swcx/types.js';
const evaluators = Object.fromEntries(
Object.entries(
import.meta.glob(
['./evaluators/*.js', '!./evaluators/*.test.js'],
{eager: true, import: 'default'},
)
)
.map(([path, evaluator]) => ([
path.replace(/\.\/evaluators\/(.*)\.js/, '$1'),
evaluator,
])),
);
export default function evaluate(node, {scope} = {}) {
const unwrapped = unwrap(node);
switch (unwrapped.type) {
case 'ArrayExpression':
return evaluators.array(unwrapped, {evaluate, scope});
case 'AssignmentExpression':
return evaluators.assignment(unwrapped, {evaluate, scope});
case 'AwaitExpression':
return evaluators.await(unwrapped, {evaluate, scope});
case 'BinaryExpression':
return evaluators.binary(unwrapped, {evaluate, scope});
case 'BooleanLiteral':
case 'NullLiteral':
case 'NumericLiteral':
case 'StringLiteral':
return evaluators.literal(unwrapped, {evaluate, scope});
case 'CallExpression':
return evaluators.call(unwrapped, {evaluate, scope});
case 'ConditionalExpression':
return evaluators.conditional(unwrapped, {evaluate, scope});
case 'Identifier':
return evaluators.identifier(unwrapped, {evaluate, scope});
case 'MemberExpression':
return evaluators.member(unwrapped, {evaluate, scope});
case 'ObjectExpression':
return evaluators.object(unwrapped, {evaluate, scope});
case 'UnaryExpression':
return evaluators.unary(unwrapped, {evaluate, scope});
case 'UpdateExpression':
return evaluators.update(unwrapped, {evaluate, scope});
}
}

View File

@ -0,0 +1,13 @@
export default function(node, {evaluate, scope}) {
const elements = [];
let isAsync = false;
for (const {expression} of node.elements) {
const {async, value} = evaluate(expression, {scope});
isAsync = isAsync || async;
elements.push(value);
}
return {
async: !!isAsync,
value: isAsync ? Promise.all(elements) : elements,
};
}

View File

@ -0,0 +1,17 @@
import {expect, test} from 'vitest';
import {first} from '@/swcx/builders.js';
import evaluate from '@/swcx/evaluate.js';
test('evaluates array of literals', async () => {
expect(evaluate(await first('[1.5, 2, "three"]')))
.to.deep.include({value: [1.5, 2, 'three']});
});
test('evaluates array containing promises', async () => {
const evaluated = evaluate(await first('[1.5, 2, await "three"]'));
expect(evaluated.async)
.to.equal(true);
expect(await evaluated.value)
.to.deep.equal([1.5, 2, 'three']);
});

View File

@ -0,0 +1,84 @@
import {
isComputed,
isMemberExpression,
} from '@/swcx/types.js';
export default function(node, {evaluate, scope}) {
const {operator, left} = node;
const right = evaluate(node.right, {scope});
if (!isMemberExpression(left)) {
const assign = (value) => {
switch (operator) {
case '=' : return scope.set(left.value, value);
case '+=' : return scope.set(left.value, scope.get(left.value) + value);
case '-=' : return scope.set(left.value, scope.get(left.value) - value);
case '*=' : return scope.set(left.value, scope.get(left.value) * value);
case '/=' : return scope.set(left.value, scope.get(left.value) / value);
case '%=' : return scope.set(left.value, scope.get(left.value) % value);
case '**=' : return scope.set(left.value, scope.get(left.value) ** value);
case '<<=' : return scope.set(left.value, scope.get(left.value) << value);
case '>>=' : return scope.set(left.value, scope.get(left.value) >> value);
case '>>>=': return scope.set(left.value, scope.get(left.value) >>> value);
case '|=' : return scope.set(left.value, scope.get(left.value) | value);
case '^=' : return scope.set(left.value, scope.get(left.value) ^ value);
case '&=' : return scope.set(left.value, scope.get(left.value) & value);
case '||=' : return scope.set(left.value, scope.get(left.value) || value);
case '&&=' : return scope.set(left.value, scope.get(left.value) && value);
case '??=' : {
const l = scope.get(left.value);
return scope.set(left.value, (l === null || l === undefined) ? value : l);
}
/* v8 ignore next 2 */
default:
throw new Error(`operator not implemented: ${node.operator}`);
}
};
if (right.async) {
return {
async: true,
value: Promise.resolve(right.value).then(assign),
};
}
return {value: assign(right.value)};
}
const {
object,
property,
} = left;
const memberAssign = (O, P, value) => {
switch (operator) {
case '=' : return O[P] = value;
case '+=' : return O[P] += value;
case '-=' : return O[P] -= value;
case '*=' : return O[P] *= value;
case '/=' : return O[P] /= value;
case '%=' : return O[P] %= value;
case '**=' : return O[P] **= value;
case '<<=' : return O[P] <<= value;
case '>>=' : return O[P] >>= value;
case '>>>=': return O[P] >>>= value;
case '|=' : return O[P] |= value;
case '^=' : return O[P] ^= value;
case '&=' : return O[P] &= value;
case '||=' : return O[P] ||= value;
case '&&=' : return O[P] &&= value;
case '??=' : return O[P] = (O[P] === null || O[P] === undefined) ? value : O[P];
/* v8 ignore next 2 */
default:
throw new Error(`operator not implemented: ${node.operator}`);
}
};
const makeAsync = (O, P, value) => (
Promise.all([O, P, value]).then(([O, P, value]) => memberAssign(O, P, value))
);
const O = evaluate(object, {scope});
const P = isComputed(property) ? evaluate(property, {scope}) : {value: property.value};
if (right.async || O.async || P.async) {
return {
async: true,
value: makeAsync(O.value, P.value, right.value),
};
}
return {value: memberAssign(O.value, P.value, right.value)};
}

View File

@ -0,0 +1,267 @@
import {expect, test} from 'vitest';
import {first} from '@/swcx/builders.js';
import evaluate from '@/swcx/evaluate.js';
const scopeTest = test.extend({
scope: async ({}, use) => {
await use({
S: {O: {}},
get(k) { return this.S[k]; },
set(k, v) { return this.S[k] = v; }
});
},
});
scopeTest('evaluates =', async ({scope}) => {
expect(evaluate(await first('x = 4'), {scope}))
.to.deep.include({value: 4});
expect(scope.get('x'))
.to.equal(4);
expect(evaluate(await first('O.x = 4'), {scope}))
.to.deep.include({value: 4});
expect(scope.get('O').x)
.to.equal(4);
});
scopeTest('evaluates +=', async ({scope}) => {
scope.set('x', 1);
expect(evaluate(await first('x += 4'), {scope}))
.to.deep.include({value: 5});
expect(scope.get('x'))
.to.equal(5);
scope.set('O', {x: 1});
expect(evaluate(await first('O.x += 4'), {scope}))
.to.deep.include({value: 5});
expect(scope.get('O').x)
.to.equal(5);
});
scopeTest('evaluates -=', async ({scope}) => {
scope.set('x', 5);
expect(evaluate(await first('x -= 4'), {scope}))
.to.deep.include({value: 1});
expect(scope.get('x'))
.to.equal(1);
scope.set('O', {x: 5});
expect(evaluate(await first('O.x -= 4'), {scope}))
.to.deep.include({value: 1});
expect(scope.get('O').x)
.to.equal(1);
});
scopeTest('evaluates *=', async ({scope}) => {
scope.set('x', 5);
expect(evaluate(await first('x *= 4'), {scope}))
.to.deep.include({value: 20});
expect(scope.get('x'))
.to.equal(20);
scope.set('O', {x: 5});
expect(evaluate(await first('O.x *= 4'), {scope}))
.to.deep.include({value: 20});
expect(scope.get('O').x)
.to.equal(20);
});
scopeTest('evaluates /=', async ({scope}) => {
scope.set('x', 25);
expect(evaluate(await first('x /= 5'), {scope}))
.to.deep.include({value: 5});
expect(scope.get('x'))
.to.equal(5);
scope.set('O', {x: 25});
expect(evaluate(await first('O.x /= 5'), {scope}))
.to.deep.include({value: 5});
expect(scope.get('O').x)
.to.equal(5);
});
scopeTest('evaluates %=', async ({scope}) => {
scope.set('x', 5);
expect(evaluate(await first('x %= 2'), {scope}))
.to.deep.include({value: 1});
expect(scope.get('x'))
.to.equal(1);
scope.set('O', {x: 5});
expect(evaluate(await first('O.x %= 2'), {scope}))
.to.deep.include({value: 1});
expect(scope.get('O').x)
.to.equal(1);
});
scopeTest('evaluates **=', async ({scope}) => {
scope.set('x', 5);
expect(evaluate(await first('x **= 3'), {scope}))
.to.deep.include({value: 125});
expect(scope.get('x'))
.to.equal(125);
scope.set('O', {x: 5});
expect(evaluate(await first('O.x **= 3'), {scope}))
.to.deep.include({value: 125});
expect(scope.get('O').x)
.to.equal(125);
});
scopeTest('evaluates <<=', async ({scope}) => {
scope.set('x', 2);
expect(evaluate(await first('x <<= 1'), {scope}))
.to.deep.include({value: 4});
expect(scope.get('x'))
.to.equal(4);
scope.set('O', {x: 2});
expect(evaluate(await first('O.x <<= 1'), {scope}))
.to.deep.include({value: 4});
expect(scope.get('O').x)
.to.equal(4);
});
scopeTest('evaluates >>=', async ({scope}) => {
scope.set('x', 8);
expect(evaluate(await first('x >>= 2'), {scope}))
.to.deep.include({value: 2});
expect(scope.get('x'))
.to.equal(2);
scope.set('O', {x: 8});
expect(evaluate(await first('O.x >>= 2'), {scope}))
.to.deep.include({value: 2});
expect(scope.get('O').x)
.to.equal(2);
});
scopeTest('evaluates >>>=', async ({scope}) => {
scope.set('x', -1);
expect(evaluate(await first('x >>>= 1'), {scope}))
.to.deep.include({value: 2147483647});
expect(scope.get('x'))
.to.equal(2147483647);
scope.set('O', {x: -1});
expect(evaluate(await first('O.x >>>= 1'), {scope}))
.to.deep.include({value: 2147483647});
expect(scope.get('O').x)
.to.equal(2147483647);
});
scopeTest('evaluates |=', async ({scope}) => {
scope.set('x', 3);
expect(evaluate(await first('x |= 5'), {scope}))
.to.deep.include({value: 7});
expect(scope.get('x'))
.to.equal(7);
scope.set('O', {x: 3});
expect(evaluate(await first('O.x |= 5'), {scope}))
.to.deep.include({value: 7});
expect(scope.get('O').x)
.to.equal(7);
});
scopeTest('evaluates ^=', async ({scope}) => {
scope.set('x', 7);
expect(evaluate(await first('x ^= 2'), {scope}))
.to.deep.include({value: 5});
expect(scope.get('x'))
.to.equal(5);
scope.set('O', {x: 7});
expect(evaluate(await first('O.x ^= 2'), {scope}))
.to.deep.include({value: 5});
expect(scope.get('O').x)
.to.equal(5);
});
scopeTest('evaluates &=', async ({scope}) => {
scope.set('x', 5);
expect(evaluate(await first('x &= 3'), {scope}))
.to.deep.include({value: 1});
expect(scope.get('x'))
.to.equal(1);
scope.set('O', {x: 5});
expect(evaluate(await first('O.x &= 3'), {scope}))
.to.deep.include({value: 1});
expect(scope.get('O').x)
.to.equal(1);
});
scopeTest('evaluates ||=', async ({scope}) => {
scope.set('x', false);
expect(evaluate(await first('x ||= true'), {scope}))
.to.deep.include({value: true});
expect(scope.get('x'))
.to.equal(true);
scope.set('O', {x: false});
expect(evaluate(await first('O.x ||= true'), {scope}))
.to.deep.include({value: true});
expect(scope.get('O').x)
.to.equal(true);
});
scopeTest('evaluates &&=', async ({scope}) => {
scope.set('x', true);
expect(evaluate(await first('x &&= true'), {scope}))
.to.deep.include({value: true});
expect(scope.get('x'))
.to.equal(true);
expect(evaluate(await first('x &&= false'), {scope}))
.to.deep.include({value: false});
expect(scope.get('x'))
.to.equal(false);
scope.set('O', {x: true});
expect(evaluate(await first('O.x &&= true'), {scope}))
.to.deep.include({value: true});
expect(scope.get('O').x)
.to.equal(true);
expect(evaluate(await first('O.x &&= false'), {scope}))
.to.deep.include({value: false});
expect(scope.get('O').x)
.to.equal(false);
});
scopeTest('evaluates ??=', async ({scope}) => {
scope.set('x', null);
expect(evaluate(await first('x ??= 2'), {scope}))
.to.deep.include({value: 2});
expect(scope.get('x'))
.to.equal(2);
expect(evaluate(await first('x ??= 4'), {scope}))
.to.deep.include({value: 2});
expect(scope.get('x'))
.to.equal(2);
scope.set('O', {x: null});
expect(evaluate(await first('O.x ??= 2'), {scope}))
.to.deep.include({value: 2});
expect(scope.get('O').x)
.to.equal(2);
expect(evaluate(await first('O.x ??= 4'), {scope}))
.to.deep.include({value: 2});
expect(scope.get('O').x)
.to.equal(2);
});
scopeTest('evaluates promised assignment', async ({scope}) => {
const evaluated = evaluate(await first('x = await 4'), {scope});
expect(evaluated.async)
.to.equal(true);
expect(await evaluated.value)
.to.equal(4);
expect(await scope.get('x'))
.to.equal(4);
const evaluatedComputedObject = evaluate(await first('O["x"] = await 4'), {scope});
expect(evaluatedComputedObject.async)
.to.equal(true);
expect(await evaluatedComputedObject.value)
.to.equal(4);
expect(await scope.get('O').x)
.to.equal(4);
const evaluatedObject = evaluate(await first('O.x = await 4'), {scope});
expect(evaluatedObject.async)
.to.equal(true);
expect(await evaluatedObject.value)
.to.equal(4);
expect(await scope.get('O').x)
.to.equal(4);
const evaluatedPromisedObject = evaluate(await first('(await O).x = await 4'), {scope});
expect(evaluatedPromisedObject.async)
.to.equal(true);
expect(await evaluatedPromisedObject.value)
.to.equal(4);
expect(await scope.get('O').x)
.to.equal(4);
});

View File

@ -0,0 +1,7 @@
export default function(node, {evaluate, scope}) {
const {value} = evaluate(node.argument, {scope});
return {
async: true,
value: value instanceof Promise ? value : Promise.resolve(value),
};
}

View File

@ -0,0 +1,20 @@
import {expect, test} from 'vitest';
import {first} from '@/swcx/builders.js';
import evaluate from '@/swcx/evaluate.js';
test('evaluates await expressions', async () => {
const evaluated = evaluate(await first('await 1'));
expect(evaluated.async)
.to.equal(true);
expect(await evaluated.value)
.to.equal(1);
});
test('coalesces promises', async () => {
const evaluated = evaluate(await first('await await await 1'));
expect(evaluated.async)
.to.equal(true);
expect(await evaluated.value)
.to.equal(1);
});

View File

@ -0,0 +1,45 @@
export default function(node, {evaluate, scope}) {
const binary = (left, right) => {
switch (node.operator) {
case '+' : return left + right;
case '-' : return left - right;
case '/' : return left / right;
case '%' : return left % right;
case '*' : return left * right;
case '>' : return left > right;
case '<' : return left < right;
case 'in' : return left in right;
case '>=' : return left >= right;
case '<=' : return left <= right;
case '**' : return left ** right;
case '===': return left === right;
case '!==': return left !== right;
case '^' : return left ^ right;
case '&' : return left & right;
case '|' : return left | right;
case '>>' : return left >> right;
case '<<' : return left << right;
case '>>>': return left >>> right;
case '==' : return left == right;
case '!=' : return left != right;
case '||' : return left || right;
case '&&' : return left && right;
case '??' : return (left === null || left === undefined) ? right : left;
case 'instanceof': return left instanceof right;
/* v8 ignore next 2 */
default:
throw new Error(`operator not implemented: ${node.operator}`);
}
};
const left = evaluate(node.left, {scope});
const right = evaluate(node.right, {scope});
if (left.async || right.async) {
return {
async: true,
value: Promise
.all([left.value, right.value])
.then(([left, right]) => binary(left, right)),
};
}
return {value: binary(left.value, right.value)};
}

View File

@ -0,0 +1,177 @@
import {expect, test} from 'vitest';
import {first} from '@/swcx/builders.js';
import evaluate from '@/swcx/evaluate.js';
test('evaluates +', async () => {
expect(evaluate(await first('10 + 2')))
.to.deep.include({value: 12});
});
test('evaluates -', async () => {
expect(evaluate(await first('10 - 2')))
.to.deep.include({value: 8});
});
test('evaluates /', async () => {
expect(evaluate(await first('10 / 2')))
.to.deep.include({value: 5});
});
test('evaluates %', async () => {
expect(evaluate(await first('10 % 3')))
.to.deep.include({value: 1});
});
test('evaluates *', async () => {
expect(evaluate(await first('10 * 2')))
.to.deep.include({value: 20});
});
test('evaluates >', async () => {
expect(evaluate(await first('10 > 2')))
.to.deep.include({value: true});
});
test('evaluates <', async () => {
expect(evaluate(await first('10 < 2')))
.to.deep.include({value: false});
});
test('evaluates in', async () => {
expect(evaluate(await first('"i" in {i: 69}')))
.to.deep.include({value: true});
expect(evaluate(await first('"j" in {i: 69}')))
.to.deep.include({value: false});
});
test('evaluates >=', async () => {
expect(evaluate(await first('10 >= 2')))
.to.deep.include({value: true});
expect(evaluate(await first('2 >= 2')))
.to.deep.include({value: true});
expect(evaluate(await first('1 >= 2')))
.to.deep.include({value: false});
});
test('evaluates <=', async () => {
expect(evaluate(await first('10 <= 2')))
.to.deep.include({value: false});
expect(evaluate(await first('2 <= 2')))
.to.deep.include({value: true});
expect(evaluate(await first('1 <= 2')))
.to.deep.include({value: true});
});
test('evaluates **', async () => {
expect(evaluate(await first('2 ** 16')))
.to.deep.include({value: 65536});
});
test('evaluates ===', async () => {
expect(evaluate(await first('10 === "10"')))
.to.deep.include({value: false});
expect(evaluate(await first('10 === 10')))
.to.deep.include({value: true});
});
test('evaluates !==', async () => {
expect(evaluate(await first('10 !== "10"')))
.to.deep.include({value: true});
expect(evaluate(await first('10 !== 10')))
.to.deep.include({value: false});
});
test('evaluates ^', async () => {
expect(evaluate(await first('7 ^ 2')))
.to.deep.include({value: 5});
});
test('evaluates &', async () => {
expect(evaluate(await first('5 & 3')))
.to.deep.include({value: 1});
});
test('evaluates |', async () => {
expect(evaluate(await first('1 | 2')))
.to.deep.include({value: 3});
});
test('evaluates >>', async () => {
expect(evaluate(await first('8 >> 1')))
.to.deep.include({value: 4});
});
test('evaluates <<', async () => {
expect(evaluate(await first('2 << 1')))
.to.deep.include({value: 4});
});
test('evaluates >>>', async () => {
expect(evaluate(await first('-1 >>> 1')))
.to.deep.include({value: 2147483647});
});
test('evaluates ==', async () => {
expect(evaluate(await first('10 == "10"')))
.to.deep.include({value: true});
expect(evaluate(await first('10 == 10')))
.to.deep.include({value: true});
expect(evaluate(await first('10 == "ten"')))
.to.deep.include({value: false});
});
test('evaluates !=', async () => {
expect(evaluate(await first('10 != "10"')))
.to.deep.include({value: false});
expect(evaluate(await first('10 != 10')))
.to.deep.include({value: false});
expect(evaluate(await first('10 != "ten"')))
.to.deep.include({value: true});
});
test('evaluates ||', async () => {
expect(evaluate(await first('true || true')))
.to.deep.include({value: true});
expect(evaluate(await first('true || false')))
.to.deep.include({value: true});
expect(evaluate(await first('false || false')))
.to.deep.include({value: false});
});
test('evaluates &&', async () => {
expect(evaluate(await first('true && true')))
.to.deep.include({value: true});
expect(evaluate(await first('true && false')))
.to.deep.include({value: false});
expect(evaluate(await first('false && false')))
.to.deep.include({value: false});
});
test('evaluates ??', async () => {
const scope = {
get() { return undefined; },
};
expect(evaluate(await first('null ?? 1')))
.to.deep.include({value: 1});
expect(evaluate(await first('undefined ?? 1'), {scope}))
.to.deep.include({value: 1});
expect(evaluate(await first('2 ?? 1')))
.to.deep.include({value: 2});
});
test('evaluates instanceof', async () => {
const scope = {
get() { return Object; },
};
expect(evaluate(await first('({}) instanceof Object'), {scope}))
.to.deep.include({value: true});
});
test('evaluates promised expressions', async () => {
const evaluated = evaluate(await first('(await 1) + (await 2)'));
expect(evaluated.async)
.to.equal(true);
expect(await evaluated.value)
.to.equal(3);
});

View File

@ -0,0 +1,53 @@
import fastCall from '@/util/fast-call.js';
import {
isComputed,
isMemberExpression,
unwrap,
} from '@/swcx/types.js';
export default function(node, {evaluate, scope}) {
let asyncArgs = false;
const args = [];
for (let i = 0; i < node.arguments.length; i++) {
const {expression: arg} = node.arguments[i];
const {async, value} = evaluate(arg, {scope});
asyncArgs ||= async;
args.push(value);
}
const {callee: wrappedCallee} = node;
const callee = unwrap(wrappedCallee);
const callOptional = callee.wrapper?.optional || node.wrapper?.optional;
const invoke = (fn, holder, args) => {
if (callOptional && !fn) {
return undefined;
}
return fastCall(fn, holder, args);
};
if (!isMemberExpression(callee)) {
const {async, value} = evaluate(callee, {scope});
if (asyncArgs || async) {
return {
async: true,
value: Promise
.all([value, Promise.all(args)])
.then(([callee, args]) => invoke(callee, undefined, args)),
};
}
return {value: invoke(value, undefined, args)};
}
const {
object,
property,
} = callee;
const O = evaluate(object, {scope});
const P = isComputed(property) ? evaluate(property, {scope}) : {value: property.value};
if (asyncArgs || O.async || P.async) {
return {
async: true,
value: Promise
.all([O.value, P.value, Promise.all(args)])
.then(([O, P, args]) => invoke(callOptional ? O?.[P] : O[P], O, args)),
};
}
return {value: invoke(callOptional ? O.value?.[P.value] : O.value[P.value], O.value, args)};
}

View File

@ -0,0 +1,64 @@
import {expect, test} from 'vitest';
import {first} from '@/swcx/builders.js';
import evaluate from '@/swcx/evaluate.js';
const scopeTest = test.extend({
scope: async ({}, use) => {
await use({
S: {O: {}},
get(k) { return this.S[k]; },
set(k, v) { return this.S[k] = v; }
});
},
});
scopeTest('evaluates calls', async ({scope}) => {
scope.set('f', (...args) => args.reduce((l, r) => l + r, 0));
const evaluated = evaluate(await first('f(1, 2, 3)'), {scope});
expect(evaluated.value)
.to.equal(6);
});
scopeTest('evaluates async calls', async ({scope}) => {
const f = (...args) => args.reduce((l, r) => l + r, 0);
scope.set('f', f);
scope.set('O', {f});
const evaluated = evaluate(await first('f(await 1, 2, 3)'), {scope});
expect(evaluated.async)
.to.equal(true);
expect(await evaluated.value)
.to.equal(6);
const evaluatedOptional = evaluate(await first('O?.f(await 1, 2, 3)'), {scope});
expect(evaluatedOptional.async)
.to.equal(true);
expect(await evaluatedOptional.value)
.to.equal(6);
});
scopeTest('evaluates member calls', async ({scope}) => {
scope.set('O', {f: (...args) => args.reduce((l, r) => l + r, 0)});
expect(evaluate(await first('O.f(1, 2, 3)'), {scope}).value)
.to.equal(6);
expect(evaluate(await first('O["f"](1, 2, 3)'), {scope}).value)
.to.equal(6);
});
scopeTest('evaluates optional calls', async ({scope}) => {
scope.set('O', {});
expect(evaluate(await first('g?.(1, 2, 3)'), {scope}).value)
.to.equal(undefined);
expect(evaluate(await first('O?.g(1, 2, 3)'), {scope}).value)
.to.equal(undefined);
expect(evaluate(await first('O?.g?.(1, 2, 3)'), {scope}).value)
.to.equal(undefined);
});
scopeTest('evaluates async calls', async ({scope}) => {
scope.set('O', {f: (...args) => args.reduce((l, r) => l + r, 0)});
const evaluated = evaluate(await first('O.f(await 1, 2, 3)'), {scope});
expect(evaluated.async)
.to.equal(true);
expect(await evaluated.value)
.to.equal(6);
});

View File

@ -0,0 +1,13 @@
export default function(node, {evaluate, scope}) {
const test = evaluate(node.test, {scope});
if (test.async) {
return {
async: true,
value: (
Promise.resolve(test.value)
.then((test) => evaluate(test ? node.consequent : node.alternate).value, {scope})
),
};
}
return evaluate(test.value ? node.consequent : node.alternate, {scope});
}

View File

@ -0,0 +1,43 @@
import {expect, test} from 'vitest';
import {first} from '@/swcx/builders.js';
import evaluate from '@/swcx/evaluate.js';
const scopeTest = test.extend({
scope: async ({}, use) => {
await use({
S: {O: {}},
get(k) { return this.S[k]; },
set(k, v) { return this.S[k] = v; }
});
},
});
scopeTest('evaluates conditional expression', async ({scope}) => {
scope.set('x', true);
let evaluated;
evaluated = evaluate(await first('x ? 2 : 3'), {scope});
expect(evaluated.value)
.to.equal(2);
scope.set('x', false);
evaluated = evaluate(await first('x ? 2 : 3'), {scope});
expect(evaluated.value)
.to.equal(3);
});
scopeTest('evaluates async conditional expression', async ({scope}) => {
scope.set('x', true);
let evaluated;
evaluated = evaluate(await first('(await x) ? 2 : 3'), {scope});
expect(await evaluated.async)
.to.equal(true);
expect(await evaluated.value)
.to.equal(2);
scope.set('x', false);
evaluated = evaluate(await first('(await x) ? 2 : 3'), {scope});
expect(await evaluated.async)
.to.equal(true);
expect(await evaluated.value)
.to.equal(3);
});

View File

@ -0,0 +1,3 @@
export default function(node, {scope}) {
return {value: scope.get(node.value)};
}

View File

@ -0,0 +1,3 @@
export default function({value}) {
return {value};
}

View File

@ -0,0 +1,14 @@
import {expect, test} from 'vitest';
import {first} from '@/swcx/builders.js';
import evaluate from '@/swcx/evaluate.js';
test('evaluates numeric literals', async () => {
expect(evaluate(await first('1')))
.to.deep.include({value: 1});
});
test('evaluates string literals', async () => {
expect(evaluate(await first('"1"')))
.to.deep.include({value: '1'});
});

View File

@ -0,0 +1,16 @@
import {
isComputed,
} from '@/swcx/types.js';
export default function({object, property, wrapper}, {evaluate, scope}) {
const member = (O, P) => (wrapper?.optional ? O?.[P] : O[P]);
const O = evaluate(object, {scope});
const P = isComputed(property) ? evaluate(property, {scope}) : {value: property.value};
if (O.async || P.async) {
return {
async: true,
value: Promise.all([O.value, P.value]).then(([O, P]) => member(O, P)),
};
}
return {value: member(O.value, P.value)};
}

View File

@ -0,0 +1,44 @@
import {expect, test} from 'vitest';
import {first} from '@/swcx/builders.js';
import evaluate from '@/swcx/evaluate.js';
const scopeTest = test.extend({
scope: async ({}, use) => {
await use({
S: {O: {x: 32}},
get(k) { return this.S[k]; },
set(k, v) { return this.S[k] = v; }
});
},
});
scopeTest('evaluates member expression', async ({scope}) => {
let evaluated;
evaluated = evaluate(await first('O.x'), {scope});
expect(evaluated.value)
.to.equal(32);
});
scopeTest('evaluates optional member expression', async ({scope}) => {
let evaluated;
evaluated = evaluate(await first('O?.y'), {scope});
expect(evaluated.value)
.to.equal(undefined);
});
scopeTest('evaluates computed member expression', async ({scope}) => {
let evaluated;
evaluated = evaluate(await first('O["x"]'), {scope});
expect(evaluated.value)
.to.equal(32);
});
scopeTest('evaluates async member expression', async ({scope}) => {
let evaluated;
evaluated = evaluate(await first('O[await "x"]'), {scope});
expect(evaluated.async)
.to.equal(true);
expect(await evaluated.value)
.to.equal(32);
});

View File

@ -0,0 +1,88 @@
import {
isComputed,
isIdentifier,
isKeyValueProperty,
isNumericLiteral,
isSpreadElement,
isStringLiteral,
} from '@/swcx/types.js';
export default function(node, {evaluate, scope}) {
const {properties} = node;
let isAsync = false;
const entries = [];
for (let i = 0; i < properties.length; i++) {
if (isKeyValueProperty(properties[i])) {
const {key, value} = properties[i];
let k;
if (isComputed(key)) {
k = evaluate(key, {scope});
}
else if (isIdentifier(key)) {
k = {value: key.value};
}
else if (isNumericLiteral(key)) {
k = {value: key.value};
}
else if (isStringLiteral(key)) {
k = {value: key.value};
}
/* v8 ignore next 3 */
else {
throw new Error(`property key type ${key.type} not implemented`);
}
const v = evaluate(value, {scope});
isAsync ||= k.async || v.async;
if (k.async || v.async) {
entries.push(Promise.all([k.value, v.value]));
}
else {
entries.push([k.value, v.value]);
}
}
if (isSpreadElement(properties[i])) {
const {arguments: argument} = properties[i];
const spreading = evaluate(argument, {scope});
isAsync ||= spreading.async;
if (spreading.async) {
entries.push(Promise.resolve(spreading.value).then((spreading) => {
const entries = [];
const keys = Object.keys(spreading);
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
entries.push([key, spreading[key]]);
}
return entries;
}));
}
else {
const keys = Object.keys(spreading.value);
for (let i = 0; i < keys.length; ++i) {
const key = keys[i];
entries.push([key, spreading.value[key]]);
}
}
}
}
return {
async: !!isAsync,
value: isAsync
? Promise.all(entries)
.then((entries) => {
const flat = [];
for (let i = 0; i < entries.length; ++i) {
const entry = entries[i];
if (Array.isArray(entry[0])) {
for (let j = 0; j < entry.length; j++) {
flat.push(entry[j]);
}
}
else {
flat.push(entry);
}
}
return Object.fromEntries(flat);
})
: Object.fromEntries(entries),
};
}

View File

@ -0,0 +1,60 @@
import {expect, test} from 'vitest';
import {first} from '@/swcx/builders.js';
import evaluate from '@/swcx/evaluate.js';
test('evaluates object expression', async () => {
let evaluated;
evaluated = evaluate(await first(`({
["foo"]: 16,
bar: 32,
'baz': 64,
})`));
expect(evaluated.value)
.to.deep.equal({
foo: 16,
bar: 32,
baz: 64,
});
});
test('evaluates async object expression', async () => {
let evaluated;
evaluated = evaluate(await first(`({
foo: await 32,
})`));
expect(evaluated.async)
.to.equal(true);
expect(await evaluated.value)
.to.deep.equal({
foo: 32,
});
});
test('evaluates object spread expression', async () => {
let evaluated;
evaluated = evaluate(await first(`({
foo: 16,
...({bar: 32}),
})`));
expect(evaluated.value)
.to.deep.equal({
foo: 16,
bar: 32,
});
});
test('evaluates async spread expression', async () => {
let evaluated;
evaluated = evaluate(await first(`({
foo: 16,
...(await {bar: 32}),
})`));
expect(evaluated.async)
.to.equal(true);
expect(await evaluated.value)
.to.deep.equal({
foo: 16,
bar: 32,
});
});

View File

@ -0,0 +1,24 @@
export default function(node, {evaluate, scope}) {
const unary = (arg) => {
switch (node.operator) {
case '+' : return +arg;
case '-' : return -arg;
case '!' : return !arg;
case '~' : return ~arg;
case 'typeof': return typeof arg;
case 'void' : return undefined;
// case 'delete': ...
/* v8 ignore next 2 */
default:
throw new Error(`operator not implemented: ${node.operator}`);
}
};
const arg = evaluate(node.argument, {scope});
if (arg.async) {
return {
async: true,
value: Promise.resolve(arg.value).then(unary),
};
}
return {value: unary(arg.value)};
}

View File

@ -0,0 +1,42 @@
import {expect, test} from 'vitest';
import {first} from '@/swcx/builders.js';
import evaluate from '@/swcx/evaluate.js';
test('evaluates +', async () => {
expect(evaluate(await first('+1')))
.to.deep.include({value: 1});
});
test('evaluates -', async () => {
expect(evaluate(await first('-1')))
.to.deep.include({value: -1});
});
test('evaluates !', async () => {
expect(evaluate(await first('!true')))
.to.deep.include({value: false});
});
test('evaluates ~', async () => {
expect(evaluate(await first('~1')))
.to.deep.include({value: -2});
});
test('evaluates typeof', async () => {
expect(evaluate(await first('typeof "a"')))
.to.deep.include({value: 'string'});
});
test('evaluates void', async () => {
expect(evaluate(await first('void 0')))
.to.deep.include({value: undefined});
});
test('evaluates promised unary expression', async () => {
const evaluated = evaluate(await first('-(await 4)'));
expect(evaluated.async)
.to.equal(true);
expect(await evaluated.value)
.to.equal(-4);
});

View File

@ -0,0 +1,23 @@
export default function(node, {evaluate, scope}) {
const {argument, operator, prefix} = node;
const {value} = evaluate(argument, {scope});
const update = (value) => {
if (prefix) {
switch (operator) {
case '++': return scope.set(argument.value, value + 1);
case '--': return scope.set(argument.value, value - 1);
}
}
switch (operator) {
case '++':
scope.set(argument.value, value + 1);
return value;
case '--':
scope.set(argument.value, value - 1);
return value;
}
/* v8 ignore next */
throw new Error(`operator not implemented: ${operator}`);
};
return {value: update(value)};
}

View File

@ -0,0 +1,50 @@
import {expect, test} from 'vitest';
import {first} from '@/swcx/builders.js';
import evaluate from '@/swcx/evaluate.js';
const scopeTest = test.extend({
scope: async ({}, use) => {
await use({
S: {O: {}},
get(k) { return this.S[k]; },
set(k, v) { return this.S[k] = v; }
});
},
});
scopeTest('evaluates postfix updates', async ({scope}) => {
scope.set('x', 4);
let evaluated = evaluate(await first('y = x++'), {scope});
expect(evaluated.value)
.to.equal(4);
expect(scope.get('x'))
.to.equal(5);
expect(scope.get('y'))
.to.equal(4);
evaluated = evaluate(await first('y = x--'), {scope});
expect(evaluated.value)
.to.equal(5);
expect(scope.get('x'))
.to.equal(4);
expect(scope.get('y'))
.to.equal(5);
});
scopeTest('evaluates prefix updates', async ({scope}) => {
scope.set('x', 4);
let evaluated = evaluate(await first('y = ++x'), {scope});
expect(evaluated.value)
.to.equal(5);
expect(scope.get('x'))
.to.equal(5);
expect(scope.get('y'))
.to.equal(5);
evaluated = evaluate(await first('y = --x'), {scope});
expect(evaluated.value)
.to.equal(4);
expect(scope.get('x'))
.to.equal(4);
expect(scope.get('y'))
.to.equal(4);
});

266
app/swcx/sandbox.js Normal file
View File

@ -0,0 +1,266 @@
import evaluate from '@/swcx/evaluate.js';
import Scope from '@/swcx/scope.js';
import traverse, {TRAVERSAL_PATH} from '@/swcx/traverse.js';
import {
isArrayPattern,
isBlockStatement,
isDoWhileStatement,
isExpressionStatement,
isForStatement,
isIdentifier,
isIfStatement,
isObjectPattern,
isVariableDeclarator,
isWhileStatement,
} from '@/swcx/types.js';
export default class Sandbox {
ast;
generator;
scopes;
constructor(ast, context = {}) {
this.ast = ast;
this.$$context = context;
this.compile();
}
compile() {
let scope = new Scope();
scope.context = this.$$context;
this.scopes = new WeakMap([[this.ast, scope]]);
traverse(
this.ast,
(node, verb) => {
if (
isBlockStatement(node)
|| isForStatement(node)
) {
switch (verb) {
case 'enter': {
scope = new Scope(scope);
break;
}
case 'exit': {
scope = scope.parent;
break;
}
}
}
if ('enter' === verb) {
this.scopes.set(node, scope);
}
},
);
}
get context() {
return this.scopes.get(this.ast).context;
}
destructureArray(id, init) {
const scope = this.scopes.get(id);
const {elements} = id;
for (let i = 0; i < elements.length; ++i) {
const element = elements[i];
if (null === element) {
continue;
}
if (isIdentifier(element)) {
scope.allocate(element.value, init[i]);
}
/* v8 ignore next 3 */
else {
throw new Error(`destructureArray(): Can't array destructure type ${element.type}`);
}
}
return undefined;
}
destructureObject(id, init) {
const scope = this.scopes.get(id);
const {properties} = id;
for (let i = 0; i < properties.length; ++i) {
const property = properties[i];
if (isObjectPattern(property.value)) {
this.destructureObject(property.value, init[property.key.value], scope);
}
else {
scope.allocate(
property.value ? property.value.value : property.key.value,
init[property.key.value],
);
}
}
}
evaluate(node) {
return evaluate(
node,
{scope: this.scopes.get(node)},
);
}
*execute(node, parent) {
let keys = TRAVERSAL_PATH[node.type];
if (isVariableDeclarator(node)) {
const {id} = node;
const scope = this.scopes.get(node);
if (null === node.init) {
scope.allocate(id.value, undefined);
}
else {
const init = this.evaluate(node.init);
if (isIdentifier(id)) {
if (init.async) {
yield {
async: true,
value: Promise.resolve(init.value).then((value) => {
scope.allocate(id.value, value);
}),
};
}
else {
scope.allocate(id.value, init.value);
}
}
else if (isArrayPattern(id)) {
const promiseOrVoid = init.async
? Promise.resolve(init.value).then((init) => this.destructureArray(id, init))
: this.destructureArray(id, init.value);
if (promiseOrVoid) {
yield {
async: true,
value: promiseOrVoid,
};
}
}
else if (isObjectPattern(id)) {
const promiseOrVoid = init.async
? Promise.resolve(init.value).then((init) => this.destructureObject(id, init))
: this.destructureObject(id, init.value);
if (promiseOrVoid) {
yield {
async: true,
value: promiseOrVoid,
};
}
}
}
}
// Blocks...
if (isIfStatement(node)) {
const {async, value} = this.evaluate(node.test);
const branch = (value) => {
keys = [value ? 'consequent' : 'alternate'];
};
if (async) {
yield {
async: true,
value: Promise.resolve(value).then(branch),
};
}
else {
branch(value);
}
}
// Loops...
let loop = false;
if (isForStatement(node)) {
const {value} = this.execute(node.init, node).next();
if (value?.async) {
yield value;
}
}
do {
if (
isForStatement(node)
|| isWhileStatement(node)
) {
const {async, value} = this.evaluate(node.test);
if (async) {
yield {
async: true,
value: Promise.resolve(value).then((value) => {
keys = value ? ['body'] : [];
}),
};
}
else {
keys = value ? ['body'] : [];
}
loop = keys.length > 0;
}
// Recur...
let children = [];
if (keys instanceof Function) {
children = keys(node);
}
else {
for (const key of keys) {
children.push(...(Array.isArray(node[key]) ? node[key] : [node[key]]));
}
}
for (const child of children) {
if (!child) {
continue;
}
yield* this.execute(child, node);
}
// Loops...
if (isForStatement(node)) {
const result = this.execute(node.update, node).next();
if (result.value?.async) {
yield result.value;
}
}
if (isDoWhileStatement(node)) {
const test = this.evaluate(node.test);
if (test.async) {
yield {
async: true,
value: Promise.resolve(test.value).then((value) => {
loop = value;
}),
};
}
else {
loop = test.value;
}
}
} while (loop);
if (isExpressionStatement(node)) {
yield this.evaluate(node.expression);
}
// yield ForStatement afterthought.
if (isForStatement(parent) && !isBlockStatement(node)) {
yield this.evaluate(node);
/* v8 ignore next */
}
}
run(ops = 1000) {
let result;
for (let i = 0; i < ops; ++i) {
result = this.step();
if (result.done || result.value?.async) {
break;
}
}
return result;
}
step() {
if (!this.generator) {
this.generator = this.execute(this.ast);
}
const result = this.generator.next();
if (result.done) {
this.generator = undefined;
this.compile();
}
return result;
}
}

197
app/swcx/sandbox.test.js Normal file
View File

@ -0,0 +1,197 @@
import {expect, test} from 'vitest';
import {parse} from '@swc/core';
import Sandbox from '@/swcx/sandbox.js';
test('declares variables', async () => {
const sandbox = new Sandbox(
await parse(`
const scalar = 1;
const asyncScalar = await 2;
const array = [3, 4, 5];
const asyncArray = await [6, 7, 8];
const object = {9: '10'};
const asyncObject = await {11: '12'};
`),
);
let result;
do {
result = sandbox.step();
if (result.value?.async) {
await result.value.async;
}
} while (!result.done);
expect(sandbox.context)
.to.deep.equal({
scalar: 1,
asyncScalar: 2,
array: [3, 4, 5],
asyncArray: [6, 7, 8],
object: {9: '10'},
asyncObject: {11: '12'},
});
});
test('destructures variables', async () => {
const sandbox = new Sandbox(
await parse(`
const [a, , c] = [1, 2, 3];
const {x: x1, y, z: {zz}} = {x: 4, y: 5, z: {zz: 6}};
const [d, e] = await [7, 8];
const {t, u: {uu}} = {t: 9, u: {uu: await 10}};
`),
);
let result;
do {
result = sandbox.step();
if (result.value?.async) {
await result.value.value;
}
} while (!result.done);
expect(sandbox.context)
.to.deep.equal({
a: 1,
c: 3,
x1: 4,
y: 5,
zz: 6,
d: 7,
e: 8,
t: 9,
uu: 10,
});
});
test('runs arbitrary number of ops', async () => {
const sandbox = new Sandbox(
await parse(`
const foo = [];
for (let i = 0; i < 1500; ++i) {
foo.push(i);
}
`),
);
sandbox.run(1000);
expect(sandbox.context.foo.length)
.to.equal(1000);
sandbox.run(1000);
expect(sandbox.context.foo.length)
.to.equal(1500);
expect(true)
.to.be.true;
});
test('evaluates conditional branches', async () => {
const sandbox = new Sandbox(
await parse(`
let foo, bar;
if (true) {
foo = 1;
}
else {
foo = 2;
}
if (await false) {
bar = 1;
}
else {
bar = 2;
}
`),
);
let result;
do {
result = sandbox.step();
if (result.value?.async) {
await result.value.value;
}
} while (!result.done);
expect(sandbox.context)
.to.deep.equal({
foo: 1,
bar: 2,
});
});
test('evaluates loops', async () => {
const sandbox = new Sandbox(
await parse(`
let x = 0, y = 0, a = 0, b = 0, c = 0;
for (let i = 0; i < 3; ++i) {
x += 1;
}
for (let i = await 0; i < await 3; i = 1 + await i) {
y += 1;
}
do {
a += 1;
} while (a < 3);
do {
b += 1;
} while (await b < 3);
while (c < 3) {
c += 1;
}
`),
);
let result;
do {
result = sandbox.step();
if (result.value?.async) {
await result.value.value;
}
} while (!result.done);
expect(sandbox.context)
.to.deep.equal({
a: 3,
b: 3,
c: 3,
x: 3,
y: 3,
});
});
test('retuns undefined for nonexistent variables in scope', async () => {
const sandbox = new Sandbox(
await parse(`
const x = y
`),
);
sandbox.run();
expect(sandbox.context)
.to.deep.equal({
x: undefined,
});
});
test('sets variables in global scope', async () => {
const sandbox = new Sandbox(
await parse(`
x = y
`),
);
sandbox.run();
expect(sandbox.context)
.to.deep.equal({
x: undefined,
});
});
test('runs arbitrary number of ops', async () => {
const sandbox = new Sandbox(
await parse(`
const foo = [];
for (let i = 0; i < 1500; ++i) {
foo.push(i);
}
`),
);
sandbox.run(1000);
expect(sandbox.context.foo.length)
.to.equal(1000);
sandbox.run(1000);
expect(sandbox.context.foo.length)
.to.equal(1500);
expect(true)
.to.be.true;
});

40
app/swcx/scope.js Normal file
View File

@ -0,0 +1,40 @@
export default class Scope {
context = {};
parent = null;
constructor(parent) {
this.parent = parent;
}
allocate(key, value) {
this.context[key] = value;
}
get(key) {
let walk = this;
while (walk) {
if (key in walk.context) {
return walk.context[key];
}
walk = walk.parent;
}
return undefined;
}
set(key, value) {
let walk = this;
while (walk) {
if (key in walk.context) {
walk.context[key] = value;
return value;
}
if (!walk.parent) {
walk.context[key] = value;
return value;
}
walk = walk.parent;
}
}
}

63
app/swcx/traverse.js Normal file
View File

@ -0,0 +1,63 @@
export const TRAVERSAL_PATH = {
ArrayExpression: (node) => node.elements.map(({expression}) => expression),
ArrayPattern: ['elements'],
AssignmentExpression: ['left', 'right'],
AssignmentPatternProperty: ['key'],
AwaitExpression: ['argument'],
BinaryExpression: ['left', 'right'],
BlockStatement: ['stmts'],
BooleanLiteral: [],
CallExpression: (node) => ([
node.callee,
...node.arguments.map(({expression}) => expression),
]),
Computed: ['expression'],
ConditionalExpression: ['alternate', 'consequent', 'test'],
DoWhileStatement: ['body', 'test'],
ExpressionStatement: ['expression'],
ForStatement: ['body', 'init', 'test', 'update'],
Identifier: [],
IfStatement: ['alternate', 'consequent', 'test'],
KeyValuePatternProperty: ['key', 'value'],
KeyValueProperty: ['key', 'value'],
MemberExpression: ['object', 'property'],
Module: ['body'],
NullLiteral: [],
NumericLiteral: [],
ObjectExpression: ['properties'],
ObjectPattern: ['properties'],
OptionalChainingExpression: ['base'],
ParenthesisExpression: ['expression'],
RegExpLiteral: [],
StringLiteral: [],
UnaryExpression: ['argument'],
UpdateExpression: ['argument'],
VariableDeclaration: ['declarations'],
VariableDeclarator: ['id', 'init'],
WhileStatement: ['body', 'test'],
};
export default function traverse(node, visitor) {
/* v8 ignore next 3 */
if (!(node.type in TRAVERSAL_PATH)) {
throw new Error(`node type ${node.type} not traversable`);
}
visitor(node, 'enter');
const path = TRAVERSAL_PATH[node.type];
let children;
if (path instanceof Function) {
children = path(node);
}
else if (Array.isArray(path)) {
children = [];
for (const key of path) {
children.push(...(Array.isArray(node[key]) ? node[key] : [node[key]]));
}
}
for (const child of children) {
if (child) {
traverse(child, visitor);
}
}
visitor(node, 'exit');
}

143
app/swcx/types.js Normal file
View File

@ -0,0 +1,143 @@
export function isArrayPattern(node) {
/* v8 ignore next 3 */
if (!node || node.type !== 'ArrayPattern') {
return false;
}
return true;
}
export function isBlockStatement(node) {
/* v8 ignore next 3 */
if (!node || node.type !== 'BlockStatement') {
return false;
}
return true;
}
export function isComputed(node) {
/* v8 ignore next 3 */
if (!node || node.type !== 'Computed') {
return false;
}
return true;
}
export function isDoWhileStatement(node) {
/* v8 ignore next 3 */
if (!node || node.type !== 'DoWhileStatement') {
return false;
}
return true;
}
export function isExpressionStatement(node) {
/* v8 ignore next 3 */
if (!node || node.type !== 'ExpressionStatement') {
return false;
}
return true;
}
export function isForStatement(node) {
/* v8 ignore next 3 */
if (!node || node.type !== 'ForStatement') {
return false;
}
return true;
}
export function isIdentifier(node) {
if (!node || node.type !== 'Identifier') {
return false;
}
return true;
}
export function isIfStatement(node) {
if (!node || node.type !== 'IfStatement') {
return false;
}
return true;
}
export function isKeyValueProperty(node) {
/* v8 ignore next 3 */
if (!node || node.type !== 'KeyValueProperty') {
return false;
}
return true;
}
export function isMemberExpression(node) {
/* v8 ignore next 3 */
if (!node || node.type !== 'MemberExpression') {
return false;
}
return true;
}
export function isNumericLiteral(node) {
/* v8 ignore next 3 */
if (!node || node.type !== 'NumericLiteral') {
return false;
}
return true;
}
export function isObjectPattern(node) {
/* v8 ignore next 3 */
if (!node || node.type !== 'ObjectPattern') {
return false;
}
return true;
}
export function isSpreadElement(node) {
/* v8 ignore next 3 */
if (!node || node.type !== 'SpreadElement') {
return false;
}
return true;
}
export function isStringLiteral(node) {
/* v8 ignore next 3 */
if (!node || node.type !== 'StringLiteral') {
return false;
}
return true;
}
export function isVariableDeclarator(node) {
/* v8 ignore next 3 */
if (!node || node.type !== 'VariableDeclarator') {
return false;
}
return true;
}
export function isWhileStatement(node) {
if (!node || node.type !== 'WhileStatement') {
return false;
}
return true;
}
export function unwrap(node) {
let wrapped = node;
switch (node.type) {
case 'Computed':
wrapped = unwrap(node.expression);
break;
case 'OptionalChainingExpression':
wrapped = unwrap(node.base);
break;
case 'ParenthesisExpression':
wrapped = unwrap(node.expression);
break;
}
if (node !== wrapped) {
wrapped.wrapper = node;
}
return wrapped;
}

36
app/util/fast-call.js Normal file
View File

@ -0,0 +1,36 @@
export default function(fn, holder, args) {
if (holder) {
const {name} = fn;
if (name in holder && holder[name] === fn) {
switch (args.length) {
case 0 : return holder[name]();
case 1 : return holder[name](args[0]);
case 2 : return holder[name](args[0], args[1]);
case 3 : return holder[name](args[0], args[1], args[2]);
case 4 : return holder[name](args[0], args[1], args[2], args[3]);
case 5 : return holder[name](args[0], args[1], args[2], args[3], args[4]);
default: return holder[name](...args);
}
}
const bound = fn.bind(holder);
switch (args.length) {
case 0 : return bound();
case 1 : return bound(args[0]);
case 2 : return bound(args[0], args[1]);
case 3 : return bound(args[0], args[1], args[2]);
case 4 : return bound(args[0], args[1], args[2], args[3]);
case 5 : return bound(args[0], args[1], args[2], args[3], args[4]);
default: return bound(...args);
}
}
switch (args.length) {
case 0 : return fn();
case 1 : return fn(args[0]);
case 2 : return fn(args[0], args[1]);
case 3 : return fn(args[0], args[1], args[2]);
case 4 : return fn(args[0], args[1], args[2], args[3]);
case 5 : return fn(args[0], args[1], args[2], args[3], args[4]);
default: return fn(...args);
}
}

201
package-lock.json generated
View File

@ -13,6 +13,7 @@
"@remix-run/express": "^2.9.2",
"@remix-run/node": "^2.9.2",
"@remix-run/react": "^2.9.2",
"@swc/core": "^1.6.0",
"compression": "^1.7.4",
"express": "^4.18.2",
"idb-keyval": "^6.2.1",
@ -6316,6 +6317,206 @@
"url": "https://opencollective.com/storybook"
}
},
"node_modules/@swc/core": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.6.0.tgz",
"integrity": "sha512-Wynbo79uIVBgmq3TPcTMdtXUkqk69IPSVuzo7/Jl1OhR4msC7cUaoRB1216ZanWttrAZ4/g6u17w9XZG4fzp1A==",
"hasInstallScript": true,
"dependencies": {
"@swc/counter": "^0.1.3",
"@swc/types": "^0.1.8"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.6.0",
"@swc/core-darwin-x64": "1.6.0",
"@swc/core-linux-arm-gnueabihf": "1.6.0",
"@swc/core-linux-arm64-gnu": "1.6.0",
"@swc/core-linux-arm64-musl": "1.6.0",
"@swc/core-linux-x64-gnu": "1.6.0",
"@swc/core-linux-x64-musl": "1.6.0",
"@swc/core-win32-arm64-msvc": "1.6.0",
"@swc/core-win32-ia32-msvc": "1.6.0",
"@swc/core-win32-x64-msvc": "1.6.0"
},
"peerDependencies": {
"@swc/helpers": "*"
},
"peerDependenciesMeta": {
"@swc/helpers": {
"optional": true
}
}
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.6.0.tgz",
"integrity": "sha512-W1Mwk0WRrJ5lAVkYRPxpxOmwu8p9ASXeOmiORhXvE7DYREyI30005xlqSOITU1pfSNKj7G9u3+9DjsOzPPPbBw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-darwin-x64": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.6.0.tgz",
"integrity": "sha512-EzxLnpPC1zgLb2Y0iVUG6b+/GUv43k6uJUIs52UzxOnBElYP/WeItI3RJ+LUMFzCpZMk/IxB10wofEoeQ1H/Xg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm-gnueabihf": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.6.0.tgz",
"integrity": "sha512-uP/STDjWZ5N6lc8mxJFsex4NXDaqhfzd8UOrI3LfdV97+4faE4/BC6bVqDNHFFzZi0PHuVBxD6md7IfPjugk6A==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.6.0.tgz",
"integrity": "sha512-UgNz6anowcnYzJtZohzpii31FOgouBHJqluiq+p2geX/agbC+KfOKwVXdljn95+Qc4ygBuw/hjKjgF2msOLeVg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-arm64-musl": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.6.0.tgz",
"integrity": "sha512-xPV6qrnj4nFwXQbIv70C1Kn5z5Th53sirIY76aEonr78qeC6+ywaBZR4uLFNHsljVjyuvVQfTTcl2qraGhu6oQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.6.0.tgz",
"integrity": "sha512-xTeWn4OT5uQ+DxT2cy94ngK8tF1U/5fMC49/V6FhCS2Wh+Xa/O+OWcOyKvYtk3b0eGYS4iNIRKgzog7fLSFtvQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-linux-x64-musl": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.6.0.tgz",
"integrity": "sha512-3P01mYD5XbyaVLT0MGZmZE+ZdgmGSvuvIhSejRDBlEXqkFnH79nWds+KsE+91hzVU8XsgzX57Yzv4eO5dlIuPw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-arm64-msvc": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.6.0.tgz",
"integrity": "sha512-xFuook1efU0ctzMAEeol4eI7J6+k/c/pMJpp/NP/4JJDnhlHwAi2iyiZcID8YZS+ePHgXMLndGdIMHVv/wIPkQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-ia32-msvc": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.6.0.tgz",
"integrity": "sha512-VCJa5vTywxzASqvf9OEUM5SZBcNrWbuIkSGM5T9guuBzyrh/tSqVHjzOWL9qpP69uPVj5G/I5bJObLiUKErhvQ==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/core-win32-x64-msvc": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.6.0.tgz",
"integrity": "sha512-L7i8WBSIJTQiMONJGHnznDydZmlJIqHjZ3VhBHeTTms8cEAuwkAVgzPwgr5cD9GhmcwdeBI9iYdOuKr1pUx19Q==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=10"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="
},
"node_modules/@swc/types": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.8.tgz",
"integrity": "sha512-RNFA3+7OJFNYY78x0FYwi1Ow+iF1eF5WvmfY1nXPOEH4R2p/D4Cr1vzje7dNAI2aLFqpv8Wyz4oKSWqIZArpQA==",
"dependencies": {
"@swc/counter": "^0.1.3"
}
},
"node_modules/@testing-library/dom": {
"version": "9.3.4",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz",

View File

@ -20,6 +20,7 @@
"@remix-run/express": "^2.9.2",
"@remix-run/node": "^2.9.2",
"@remix-run/react": "^2.9.2",
"@swc/core": "^1.6.0",
"compression": "^1.7.4",
"express": "^4.18.2",
"idb-keyval": "^6.2.1",