Pārlūkot izejas kodu

Merge branch 'alpinejs:main' into main

Alexandre Lepretre 1 gadu atpakaļ
vecāks
revīzija
f9d47f2f66

+ 3 - 3
packages/csp/package.json

@@ -1,12 +1,12 @@
 {
     "name": "@alpinejs/csp",
-    "version": "3.0.0-alpha.0",
-    "description": "A CSP compatible build of Alpine",
+    "version": "3.13.3",
+    "description": "A CSP-friendly build of AlpineJS",
     "author": "Caleb Porzio",
     "license": "MIT",
     "main": "dist/module.cjs.js",
     "module": "dist/module.esm.js",
     "dependencies": {
-        "@vue/reactivity": "^3.0.2"
+        "@vue/reactivity": "~3.1.1"
     }
 }

+ 50 - 0
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
+    )
+}

+ 28 - 29
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

+ 1 - 1
packages/docs/package.json

@@ -1,6 +1,6 @@
 {
     "name": "@alpinejs/docs",
-    "version": "3.13.3-revision.1",
+    "version": "3.13.3-revision.2",
     "description": "The documentation for Alpine",
     "author": "Caleb Porzio",
     "license": "MIT"

+ 63 - 22
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.
 
 <a name="installation"></a>
 ## 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 `<script>` tag or installing it via NPM:
 
-```shell
-npm install
-npm run build
+### Via CDN
+
+You can include this build's CDN as a `<script>` tag just like you would normally with standard Alpine build:
+
+```alpine
+<!-- Alpine's CSP-friendly Core -->
+<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/csp@3.x.x/dist/cdn.min.js"></script>
 ```
 
-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 `<script>` tag or module import:
+### Via NPM
 
-<a name="script-tag"></a>
-### Script tag
+You can alternatively install this build from NPM for use inside your bundle like so:
 
-```alpine
-<html>
-    <script src="/path/to/cdn.js" defer></script>
-</html>
+```shell
+npm install @alpinejs/csp
 ```
 
-<a name="module-import"></a>
-### 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()
 ```
 
-<a name="restrictions"></a>
-## Restrictions
+<a name="basic-example"></a>
+## 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 component using a common CSP setup:
+
+```alpine
+<html>
+    <head>
+        <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'nonce-a23gbfz9e'">
+
+        <script defer nonce="a23gbfz9e" src="https://cdn.jsdelivr.net/npm/@alpinejs/csp@3.x.x/dist/cdn.min.js"></script>
+    </head>
+
+    <body>
+        <div x-data="counter">
+            <button x-on:click="increment"></button>
+
+            <span x-text="count"></span>
+        </div>
+
+        <script nonce="a23gbfz9e">
+            document.addEventListener('alpine:init', () => {
+                Alpine.data('counter', () => {
+                    return {
+                        count: 1,
+
+                        increment() {
+                            this.count++;
+                        },
+                    }
+                })
+            })
+        </script>
+    </body>
+</html>
+```
+
+<a name="api-restrictions"></a>
+## 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
     <span x-text="count"></span>
 </div>
 ```
+
 ```js
 Alpine.data('counter', () => ({
     count: 1,
 
-    increment() { this.count++ }
+    increment() {
+        this.count++
+    },
 }))
 ```

+ 2 - 2
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'})

+ 1 - 1
packages/docs/src/en/advanced/reactivity.md

@@ -1,5 +1,5 @@
 ---
-order: 1
+order: 2
 title: Reactivity
 ---
 

+ 1 - 1
packages/docs/src/en/plugins/anchor.md

@@ -192,7 +192,7 @@ Because `x-anchor` accepts a reference to any DOM element, you can use utilities
 <div x-data="{ open: false }">
     <button id="trigger" @click="open = ! open">Toggle</button>
 
-    <div x-show="open" x-anchor="document.getElementById('#trigger')">
+    <div x-show="open" x-anchor="document.getElementById('trigger')">
         Dropdown content
     </div>
 </div>

+ 6 - 0
scripts/release.js

@@ -39,6 +39,9 @@ function writeNewAlpineVersion() {
     writeToPackageDotJson('alpinejs', 'version', version)
     console.log('Bumping alpinejs package.json: '+version)
 
+    writeToPackageDotJson('csp', 'version', version)
+    console.log('Bumping @alpinejs/csp package.json: '+version)
+
     writeToPackageDotJson('intersect', 'version', version)
     console.log('Bumping @alpinejs/intersect package.json: '+version)
 
@@ -77,6 +80,9 @@ function publish() {
     console.log('Publishing alpinejs on NPM...');
     runFromPackage('alpinejs', 'npm publish')
 
+    console.log('Publishing @alpinejs/csp on NPM...');
+    runFromPackage('csp', 'npm publish --access public')
+
     console.log('Publishing @alpinejs/docs on NPM...');
     runFromPackage('docs', 'npm publish --access public')