Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/alpinejs/src/alpine.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { mapAttributes, directive, setPrefix as prefix, prefix as prefixed } fro
import { start, addRootSelector, addInitSelector, closestRoot, findClosest, initTree, destroyTree, interceptInit } from './lifecycle'
import { onElRemoved, onAttributeRemoved, onAttributesAdded, mutateDom, deferMutations, flushAndStopDeferringMutations, startObservingMutations, stopObservingMutations } from './mutation'
import { mergeProxies, closestDataStack, addScopeToNode, scope as $data } from './scope'
import { setEvaluator, evaluate, evaluateLater, dontAutoEvaluateFunctions } from './evaluator'
import { setEvaluator, setRawEvaluator, evaluate, evaluateLater, dontAutoEvaluateFunctions, evaluateRaw } from './evaluator'
import { transition } from './directives/x-transition'
import { clone, cloneNode, skipDuringClone, onlyDuringClone, interceptClone } from './clone'
import { interceptor, initInterceptors } from './interceptor'
Expand Down Expand Up @@ -50,6 +50,7 @@ let Alpine = {
initInterceptors,
injectMagics,
setEvaluator,
setRawEvaluator,
mergeProxies,
extractProp,
findClosest,
Expand All @@ -65,6 +66,7 @@ let Alpine = {
throttle,
debounce,
evaluate,
evaluateRaw,
initTree,
nextTick,
prefixed,
Expand Down
80 changes: 79 additions & 1 deletion packages/alpinejs/src/evaluator.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ export function setEvaluator(newEvaluator) {
theEvaluatorFunction = newEvaluator
}

let theRawEvaluatorFunction

export function setRawEvaluator(newEvaluator) {
theRawEvaluatorFunction = newEvaluator
}

export function normalEvaluator(el, expression) {
let overriddenMagics = {}

Expand All @@ -50,6 +56,13 @@ export function normalEvaluator(el, expression) {

export function generateEvaluatorFromFunction(dataStack, func) {
return (receiver = () => {}, { scope = {}, params = [], context } = {}) => {
// If auto-evaluation is disabled, pass the function itself instead of calling it
if (! shouldAutoEvaluateFunctions) {
runIfTypeOfFunction(receiver, func, mergeProxies([scope, ...dataStack]), params)

return
}

let result = func.apply(mergeProxies([scope, ...dataStack]), params)

runIfTypeOfFunction(receiver, result)
Expand All @@ -67,7 +80,7 @@ function generateFunctionFromString(expression, el) {

// Some expressions that are useful in Alpine are not valid as the right side of an expression.
// Here we'll detect if the expression isn't valid for an assignment and wrap it in a self-
// calling function so that we don't throw an error AND a "return" statement can b e used.
// calling function so that we don't throw an error AND a "return" statement can be used.
let rightSideSafeExpression = 0
// Support expressions starting with "if" statements like: "if (...) doSomething()"
|| /^[\n\s]*if.*\(.*\)/.test(expression.trim())
Expand Down Expand Up @@ -149,3 +162,68 @@ export function runIfTypeOfFunction(receiver, value, scope, params, el) {
receiver(value)
}
}

export function evaluateRaw(...args) {
return theRawEvaluatorFunction(...args)
}

export function normalRawEvaluator(el, expression, extras = {}) {
let overriddenMagics = {}

injectMagics(overriddenMagics, el)

let dataStack = [overriddenMagics, ...closestDataStack(el)]

let scope = mergeProxies([extras.scope ?? {}, ...dataStack])

let params = extras.params ?? []

if (expression.includes('await')) {
let AsyncFunction = Object.getPrototypeOf(async function(){}).constructor

// Some expressions that are useful in Alpine are not valid as the right side of an expression.
// Here we'll detect if the expression isn't valid for an assignment and wrap it in a self-
// calling function so that we don't throw an error AND a "return" statement can be used.
let rightSideSafeExpression = 0
// Support expressions starting with "if" statements like: "if (...) doSomething()"
|| /^[\n\s]*if.*\(.*\)/.test(expression.trim())
// Support expressions starting with "let/const" like: "let foo = 'bar'"
|| /^(let|const)\s/.test(expression.trim())
? `(async()=>{ ${expression} })()`
: expression

let func = new AsyncFunction(
["scope"],
`with (scope) { let __result = ${rightSideSafeExpression}; return __result }`
)

let result = func.call(extras.context, scope)

return result
} else {
// Some expressions that are useful in Alpine are not valid as the right side of an expression.
// Here we'll detect if the expression isn't valid for an assignment and wrap it in a self-
// calling function so that we don't throw an error AND a "return" statement can be used.
let rightSideSafeExpression = 0
// Support expressions starting with "if" statements like: "if (...) doSomething()"
|| /^[\n\s]*if.*\(.*\)/.test(expression.trim())
// Support expressions starting with "let/const" like: "let foo = 'bar'"
|| /^(let|const)\s/.test(expression.trim())
? `(()=>{ ${expression} })()`
: expression

let func = new Function(
["scope"],
`with (scope) { let __result = ${rightSideSafeExpression}; return __result }`
)

let result = func.call(extras.context, scope)

// If the result is a function, call it
if (typeof result === 'function' && shouldAutoEvaluateFunctions) {
return result.apply(scope, params)
}

return result
}
}
3 changes: 2 additions & 1 deletion packages/alpinejs/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ import Alpine from './alpine'
* It's the function that converts raw JavaScript string
* expressions like @click="toggle()", into actual JS.
*/
import { normalEvaluator } from './evaluator'
import { normalEvaluator, normalRawEvaluator } from './evaluator'

Alpine.setEvaluator(normalEvaluator)
Alpine.setRawEvaluator(normalRawEvaluator)

/**
* _______________________________________________________
Expand Down
22 changes: 22 additions & 0 deletions packages/csp/src/evaluator.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,28 @@ import { tryCatch } from 'alpinejs/src/utils/error'
import { generateRuntimeFunction } from './parser'
import { injectMagics } from 'alpinejs/src/magics'

export function cspRawEvaluator(el, expression, extras = {}) {
let dataStack = generateDataStack(el)

let scope = mergeProxies([extras.scope ?? {}, ...dataStack])

let params = extras.params ?? []

let evaluate = generateRuntimeFunction(expression)

let result = evaluate({
scope,
forceBindingRootScopeToFunctions: true,
})

// If the result is a function, call it
if (typeof result === 'function' && shouldAutoEvaluateFunctions) {
return result.apply(scope, params)
}

return result
}

export function cspEvaluator(el, expression) {
let dataStack = generateDataStack(el)

Expand Down
3 changes: 2 additions & 1 deletion packages/csp/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ import Alpine from 'alpinejs/src/alpine'
* interpret strings as runtime JS. We're going to use
* a more CSP-friendly evaluator for this instead.
*/
import { cspEvaluator } from './evaluator'
import { cspEvaluator, cspRawEvaluator } from './evaluator'

Alpine.setEvaluator(cspEvaluator)
Alpine.setRawEvaluator(cspRawEvaluator)

/**
* The rest of this file bootstraps Alpine the way it is
Expand Down
97 changes: 97 additions & 0 deletions tests/vitest/csp-evaluator.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// @vitest-environment jsdom

import { describe, it, expect, beforeAll } from 'vitest';
import Alpine from '../../packages/csp/src/index.js';
import { cspRawEvaluator } from '../../packages/csp/src/evaluator.js';

beforeAll(() => Alpine.start())

describe('cspRawEvaluator', () => {
it('simple expression', () => {
let element = { parentNode: null, _x_dataStack: [] }

expect(cspRawEvaluator(element, '42')).toBe(42)
});

it('with scope', () => {
let element = { parentNode: null, _x_dataStack: [] }

expect(cspRawEvaluator(element, 'foo', { scope: { foo: 42 } })).toBe(42)
});

it('with params', () => {
let element = { parentNode: null, _x_dataStack: [] }

expect(cspRawEvaluator(element, 'fn', { scope: { fn: (i) => i }, params: [42] })).toBe(42)
});

it('auto-evaluating function expression', () => {
let element = { parentNode: null, _x_dataStack: [] }

let scope = { getAnswer: () => 42 }

expect(cspRawEvaluator(element, 'getAnswer()', { scope })).toBe(42)
});

it('non auto-evaluating function expression', () => {
let element = { parentNode: null, _x_dataStack: [] }

let scope = { getAnswer: () => 42 }

Alpine.dontAutoEvaluateFunctions(() => {
let fn = cspRawEvaluator(element, 'getAnswer', { scope })
expect(fn()).toBe(42)
})
});

it('property access', () => {
let element = { parentNode: null, _x_dataStack: [] }

let scope = { user: { name: 'John' } }

expect(cspRawEvaluator(element, 'user.name', { scope })).toBe('John')
});

it('method calls preserve context', () => {
let element = { parentNode: null, _x_dataStack: [] }

let scope = {
counter: {
count: 5,
getCount() { return this.count }
}
}

expect(cspRawEvaluator(element, 'counter.getCount()', { scope })).toBe(5)
});

it('ternary expressions', () => {
let element = { parentNode: null, _x_dataStack: [] }

expect(cspRawEvaluator(element, 'true ? 1 : 2')).toBe(1)
expect(cspRawEvaluator(element, 'false ? 1 : 2')).toBe(2)
});

it('arithmetic operations', () => {
let element = { parentNode: null, _x_dataStack: [] }

expect(cspRawEvaluator(element, '2 + 3 * 4')).toBe(14)
expect(cspRawEvaluator(element, '(2 + 3) * 4')).toBe(20)
});

it('comparison operations', () => {
let element = { parentNode: null, _x_dataStack: [] }

expect(cspRawEvaluator(element, '5 > 3')).toBe(true)
expect(cspRawEvaluator(element, '5 === 5')).toBe(true)
expect(cspRawEvaluator(element, '5 === "5"')).toBe(false)
});

it('logical operations', () => {
let element = { parentNode: null, _x_dataStack: [] }

expect(cspRawEvaluator(element, 'true && false')).toBe(false)
expect(cspRawEvaluator(element, 'true || false')).toBe(true)
expect(cspRawEvaluator(element, '!false')).toBe(true)
});
});
Loading