diff --git a/packages/csp/src/evaluator.js b/packages/csp/src/evaluator.js new file mode 100644 index 000000000..7a2d2ed30 --- /dev/null +++ b/packages/csp/src/evaluator.js @@ -0,0 +1,50 @@ +import { generateEvaluatorFromFunction, runIfTypeOfFunction } from 'alpinejs/src/evaluator' +import { closestDataStack, mergeProxies } from 'alpinejs/src/scope' +import { tryCatch } from 'alpinejs/src/utils/error' +import { injectMagics } from 'alpinejs/src/magics' + +export function cspEvaluator(el, expression) { + let dataStack = generateDataStack(el) + + // Return if the provided expression is already a function... + if (typeof expression === 'function') { + return generateEvaluatorFromFunction(dataStack, expression) + } + + let evaluator = generateEvaluator(el, expression, dataStack) + + return tryCatch.bind(null, el, expression, evaluator) +} + +function generateDataStack(el) { + let overriddenMagics = {} + + injectMagics(overriddenMagics, el) + + return [overriddenMagics, ...closestDataStack(el)] +} + +function generateEvaluator(el, expression, dataStack) { + return (receiver = () => {}, { scope = {}, params = [] } = {}) => { + let completeScope = mergeProxies([scope, ...dataStack]) + + if (completeScope[expression] === undefined) { + throwExpressionError(el, expression) + } + + runIfTypeOfFunction(receiver, completeScope[expression], completeScope, params) + } +} + +function throwExpressionError(el, expression) { + console.warn( +`Alpine Error: Alpine is unable to interpret the following expression using the CSP-friendly build: + +"${expression}" + +Read more about the Alpine's CSP-friendly build restrictions here: https://alpinejs.dev/advanced/csp + +`, +el + ) +} diff --git a/packages/csp/src/index.js b/packages/csp/src/index.js index 9b5b26c63..5638985c5 100644 --- a/packages/csp/src/index.js +++ b/packages/csp/src/index.js @@ -1,38 +1,37 @@ +/** + * Alpine CSP Build. + * + * Alpine allows you to use JavaScript directly inside your HTML. This is an + * incredibly powerful features. However, it violates the "unsafe-eval" + * Content Security Policy. This alternate Alpine build provides a + * more constrained API for Alpine that is also CSP-friendly... + */ import Alpine from 'alpinejs/src/alpine' -Alpine.setEvaluator(cspCompliantEvaluator) - +/** + * _______________________________________________________ + * The Evaluator + * ------------------------------------------------------- + * + * By default, Alpine's evaluator "eval"-like utilties to + * interpret strings as runtime JS. We're going to use + * a more CSP-friendly evaluator for this instead. + */ +import { cspEvaluator } from './evaluator' + +Alpine.setEvaluator(cspEvaluator) + +/** + * The rest of this file bootstraps Alpine the way it is + * normally bootstrapped in the default build. We will + * set and define it's directives, magics, etc... + */ import { reactive, effect, stop, toRaw } from '@vue/reactivity' + Alpine.setReactivityEngine({ reactive, effect, release: stop, raw: toRaw }) import 'alpinejs/src/magics/index' -import 'alpinejs/src/directives/index' - -import { closestDataStack, mergeProxies } from 'alpinejs/src/scope' -import { injectMagics } from 'alpinejs/src/magics' -import { generateEvaluatorFromFunction, runIfTypeOfFunction } from 'alpinejs/src/evaluator' -import { tryCatch } from 'alpinejs/src/utils/error' - -function cspCompliantEvaluator(el, expression) { - let overriddenMagics = {} - - injectMagics(overriddenMagics, el) - let dataStack = [overriddenMagics, ...closestDataStack(el)] - - if (typeof expression === 'function') { - return generateEvaluatorFromFunction(dataStack, expression) - } - - let evaluator = (receiver = () => {}, { scope = {}, params = [] } = {}) => { - let completeScope = mergeProxies([scope, ...dataStack]) - - if (completeScope[expression] !== undefined) { - runIfTypeOfFunction(receiver, completeScope[expression], completeScope, params) - } - } - - return tryCatch.bind(null, el, expression, evaluator) -} +import 'alpinejs/src/directives/index' export default Alpine diff --git a/packages/docs/src/en/advanced/csp.md b/packages/docs/src/en/advanced/csp.md index f825645ba..c20c0e9ac 100644 --- a/packages/docs/src/en/advanced/csp.md +++ b/packages/docs/src/en/advanced/csp.md @@ -1,49 +1,87 @@ --- -order: 5 +order: 1 title: CSP --- -# CSP (Content-Security Policy) +# CSP (Content-Security Policy) Build -In order for Alpine to be able to execute plain strings from HTML attributes as JavaScript expressions, for example `x-on:click="console.log()"`, it needs to rely on utilities that violate the "unsafe-eval" content security policy. +In order for Alpine to be able to execute plain strings from HTML attributes as JavaScript expressions, for example `x-on:click="console.log()"`, it needs to rely on utilities that violate the "unsafe-eval" [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) that some applications may enforce for security purposes. > Under the hood, Alpine doesn't actually use eval() itself because it's slow and problematic. Instead it uses Function declarations, which are much better, but still violate "unsafe-eval". -In order to accommodate environments where this CSP is necessary, Alpine will offer an alternate build that doesn't violate "unsafe-eval", but has a more restrictive syntax. +In order to accommodate environments where this CSP is necessary, Alpine offer's an alternate build that doesn't violate "unsafe-eval", but has a more restrictive syntax. ## Installation -The CSP build hasn’t been officially released yet. In the meantime, you may build it from source. To do this, clone the [`alpinejs/alpine`](https://github.com/alpinejs/alpine) repository and run: +You can use this build by either including it from a ` ``` -This will generate a `/packages/csp/dist/` directory with the built files. After copying the appropriate file into your project, you can include it either via ` - +```shell +npm install @alpinejs/csp ``` - -### Module import +Then initialize it from your bundle: ```js -import Alpine from './path/to/module.esm.js' +import Alpine from '@alpinejs/csp' window.Alpine = Alpine -window.Alpine.start() + +Alpine.start() ``` - -## Restrictions + +## Basic Example + +To provide a glimpse of how using the CSP build might feel, here is a copy-pastable HTML file with a working counter componennt using a common CSP setup: + +```alpine + + + + + + + + +
+ + + +
+ + + + +``` + + +## API Restrictions Since Alpine can no longer interpret strings as plain JavaScript, it has to parse and construct JavaScript functions from them manually. @@ -70,10 +108,13 @@ However, breaking out the expressions into external APIs, the following is valid ``` + ```js Alpine.data('counter', () => ({ count: 1, - increment() { this.count++ } + increment() { + this.count++ + }, })) ``` diff --git a/packages/docs/src/en/advanced/extending.md b/packages/docs/src/en/advanced/extending.md index b356818d7..d90896204 100644 --- a/packages/docs/src/en/advanced/extending.md +++ b/packages/docs/src/en/advanced/extending.md @@ -1,5 +1,5 @@ --- -order: 2 +order: 3 title: Extending --- @@ -229,7 +229,7 @@ Now if the directive is removed from this element or the element is removed itse By default, any new directive will run after the majority of the standard ones (with the exception of `x-teleport`). This is usually acceptable but some times you might need to run your custom directive before another specific one. This can be achieved by chaining the `.before() function to `Alpine.directive()` and specifying which directive needs to run after your custom one. - + ```js Alpine.directive('foo', (el, { value, modifiers, expression }) => { Alpine.addScopeToNode(el, {foo: 'bar'}) diff --git a/packages/docs/src/en/advanced/reactivity.md b/packages/docs/src/en/advanced/reactivity.md index ddbb601fd..1650953cd 100644 --- a/packages/docs/src/en/advanced/reactivity.md +++ b/packages/docs/src/en/advanced/reactivity.md @@ -1,5 +1,5 @@ --- -order: 1 +order: 2 title: Reactivity ---