소스 검색

Convert to rollup

Caleb Porzio 5 년 전
부모
커밋
b29653204b
12개의 변경된 파일673개의 추가작업 그리고 754개의 파일을 삭제
  1. 583 0
      alpine.js
  2. 0 0
      dist/alpine.js
  3. 0 0
      dist/alpine.js.map
  4. 0 0
      dist/alpine.mjs
  5. 0 0
      dist/alpine.mjs.map
  6. 0 0
      dist/alpine.umd.js
  7. 0 0
      dist/alpine.umd.js.map
  8. 1 1
      index.html
  9. 57 747
      package-lock.json
  10. 8 5
      package.json
  11. 23 0
      rollup.config.js
  12. 1 1
      src/component.js

+ 583 - 0
alpine.js

@@ -0,0 +1,583 @@
+(function (global, factory) {
+    typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+    typeof define === 'function' && define.amd ? define(factory) :
+    (global = global || self, global.Alpine = factory());
+}(this, (function () { 'use strict';
+
+    // Thanks @stimulus:
+    // https://github.com/stimulusjs/stimulus/blob/master/packages/%40stimulus/core/src/application.ts
+    function domReady() {
+        return new Promise(resolve => {
+            if (document.readyState == "loading") {
+                document.addEventListener("DOMContentLoaded", resolve);
+            } else {
+                resolve();
+            }
+        })
+    }
+
+    function isTesting() {
+        return navigator.userAgent.includes("Node.js")
+            || navigator.userAgent.includes("jsdom")
+    }
+
+    function kebabCase(subject) {
+        return subject.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/[_\s]/, '-').toLowerCase()
+    }
+
+    function walkSkippingNestedComponents(el, callback, isRoot = true) {
+        if (el.hasAttribute('x-data') && ! isRoot) return
+
+        callback(el);
+
+        let node = el.firstElementChild;
+
+        while (node) {
+            walkSkippingNestedComponents(node, callback, false);
+
+            node = node.nextElementSibling;
+        }
+    }
+
+    function debounce(func, wait, immediate) {
+        var timeout;
+        return function () {
+            var context = this, args = arguments;
+            var later = function () {
+                timeout = null;
+                if (!immediate) func.apply(context, args);
+            };
+            var callNow = immediate && !timeout;
+            clearTimeout(timeout);
+            timeout = setTimeout(later, wait);
+            if (callNow) func.apply(context, args);
+        };
+    }
+    function saferEval(expression, dataContext, additionalHelperVariables = {}) {
+        return (new Function(['$data', ...Object.keys(additionalHelperVariables)], `var result; with($data) { result = ${expression} }; return result`))(
+            dataContext, ...Object.values(additionalHelperVariables)
+        )
+    }
+
+    function saferEvalNoReturn(expression, dataContext, additionalHelperVariables = {}) {
+        return (new Function(['$data', ...Object.keys(additionalHelperVariables)], `with($data) { ${expression} }`))(
+            dataContext, ...Object.values(additionalHelperVariables)
+        )
+    }
+
+    function isXAttr(attr) {
+        const xAttrRE = /x-(on|bind|data|text|model|if|show|cloak|ref)/;
+
+        return xAttrRE.test(attr.name)
+    }
+
+    function getXAttrs(el, type) {
+        return Array.from(el.attributes)
+            .filter(isXAttr)
+            .map(attr => {
+                const typeMatch = attr.name.match(/x-(on|bind|data|text|model|if|show|cloak|ref)/);
+                const valueMatch = attr.name.match(/:([a-zA-Z\-]+)/);
+                const modifiers = attr.name.match(/\.[^.\]]+(?=[^\]]*$)/g) || [];
+
+                return {
+                    type: typeMatch ? typeMatch[1] : null,
+                    value: valueMatch ? valueMatch[1] : null,
+                    modifiers: modifiers.map(i => i.replace('.', '')),
+                    expression: attr.value,
+                }
+            })
+            .filter(i => {
+                // If no type is passed in for filtering, bypassfilter
+                if (! type) return true
+
+                return i.type === name
+            })
+    }
+
+    class Component {
+        constructor(el) {
+            this.el = el;
+
+            const rawData = saferEval(this.el.getAttribute('x-data'), {});
+
+            rawData.$refs =  this.getRefsProxy();
+
+            this.data = this.wrapDataInObservable(rawData);
+
+            this.initialize();
+
+            this.listenForNewElementsToInitialize();
+        }
+
+        wrapDataInObservable(data) {
+            this.concernedData = [];
+
+            var self = this;
+
+            const proxyHandler = keyPrefix => ({
+                set(obj, property, value) {
+                    const propertyName = keyPrefix ? `${keyPrefix}.${property}` : property;
+
+                    const setWasSuccessful = Reflect.set(obj, property, value);
+
+                    if (self.concernedData.indexOf(propertyName) === -1) {
+                        self.concernedData.push(propertyName);
+                    }
+
+                    self.refresh();
+
+                    return setWasSuccessful
+                },
+                get(target, key) {
+                    if (typeof target[key] === 'object' && target[key] !== null) {
+                        const propertyName = keyPrefix ? `${keyPrefix}.${key}` : key;
+
+                        return new Proxy(target[key], proxyHandler(propertyName))
+                    }
+
+                    return target[key]
+                }
+            });
+
+            return new Proxy(data, proxyHandler())
+        }
+
+        initialize() {
+            walkSkippingNestedComponents(this.el, el => {
+                this.initializeElement(el);
+            });
+        }
+
+        initializeElement(el) {
+            getXAttrs(el).forEach(({ type, value, modifiers, expression }) => {
+                switch (type) {
+                    case 'on':
+                        var event = value;
+                        this.registerListener(el, event, modifiers, expression);
+                        break;
+
+                    case 'model':
+                        // If the element we are binding to is a select, a radio, or checkbox
+                        // we'll listen for the change event instead of the "input" event.
+                        var event = (el.tagName.toLowerCase() === 'select')
+                            || ['checkbox', 'radio'].includes(el.type)
+                            || modifiers.includes('lazy')
+                            ? 'change' : 'input';
+
+                        const listenerExpression = this.generateExpressionForXModelListener(el, modifiers, expression);
+
+                        this.registerListener(el, event, modifiers, listenerExpression);
+
+                        var attrName = 'value';
+                        var { output } = this.evaluateReturnExpression(expression);
+                        this.updateAttributeValue(el, attrName, output);
+                        break;
+
+                    case 'bind':
+                        var attrName = value;
+                        var { output } = this.evaluateReturnExpression(expression);
+                        this.updateAttributeValue(el, attrName, output);
+                        break;
+
+                    case 'text':
+                        var { output } = this.evaluateReturnExpression(expression);
+                        this.updateTextValue(el, output);
+                        break;
+
+                    case 'show':
+                        var { output } = this.evaluateReturnExpression(expression);
+                        this.updateVisibility(el, output);
+                        break;
+
+                    case 'if':
+                        var { output } = this.evaluateReturnExpression(expression);
+                        this.updatePresence(el, output);
+                        break;
+
+                    case 'cloak':
+                        el.removeAttribute('x-cloak');
+                        break;
+                }
+            });
+        }
+
+        listenForNewElementsToInitialize() {
+            const targetNode = this.el;
+
+            const observerOptions = {
+                childList: true,
+                attributes: true,
+                subtree: true,
+            };
+
+            const observer = new MutationObserver((mutations) => {
+                for (let i=0; i < mutations.length; i++){
+                    // Filter out mutations triggered from child components.
+                    if (! mutations[i].target.closest('[x-data]').isSameNode(this.el)) return
+
+                    if (mutations[i].type === 'attributes' && mutations[i].attributeName === 'x-data') {
+                        const rawData = saferEval(mutations[i].target.getAttribute('x-data'), {});
+
+                        Object.keys(rawData).forEach(key => {
+                            if (this.data[key] !== rawData[key]) {
+                                this.data[key] = rawData[key];
+                            }
+                        });
+                    }
+
+                    if (mutations[i].addedNodes.length > 0) {
+                        mutations[i].addedNodes.forEach(node => {
+                            if (node.nodeType !== 1) return
+
+                            if (node.matches('[x-data]')) return
+
+                            if (getXAttrs(node).length > 0) {
+                                this.initializeElement(node);
+                            }
+                        });
+                    }
+                  }
+            });
+
+            observer.observe(targetNode, observerOptions);
+        }
+
+        refresh() {
+            var self = this;
+
+            const actionByDirectiveType = {
+                'model': ({el, output}) => { self.updateAttributeValue(el, 'value', output); },
+                'bind': ({el, attrName, output}) => { self.updateAttributeValue(el, attrName, output); },
+                'text': ({el, output}) => { self.updateTextValue(el, output); },
+                'show': ({el, output}) => { self.updateVisibility(el, output); },
+                'if': ({el, output}) => { self.updatePresence(el, output); },
+            };
+
+            const walkThenClearDependancyTracker = (rootEl, callback) => {
+                walkSkippingNestedComponents(rootEl, callback);
+
+                self.concernedData = [];
+            };
+
+            debounce(walkThenClearDependancyTracker, 5)(this.el, function (el) {
+                getXAttrs(el).forEach(({ type, value, expression }) => {
+                    if (! actionByDirectiveType[type]) return
+
+                    var { output, deps } = self.evaluateReturnExpression(expression);
+
+                    if (self.concernedData.filter(i => deps.includes(i)).length > 0) {
+                        (actionByDirectiveType[type])({ el, attrName: value, output });
+                    }
+                });
+            });
+        }
+
+        generateExpressionForXModelListener(el, modifiers, dataKey) {
+            var rightSideOfExpression = '';
+            if (el.type === 'checkbox') {
+                // If the data we are binding to is an array, toggle it's value inside the array.
+                if (Array.isArray(this.data[dataKey])) {
+                    rightSideOfExpression = `$event.target.checked ? ${dataKey}.concat([$event.target.value]) : ${dataKey}.filter(i => i !== $event.target.value)`;
+                } else {
+                    rightSideOfExpression = `$event.target.checked`;
+                }
+            } else if (el.tagName.toLowerCase() === 'select' && el.multiple) {
+                rightSideOfExpression = modifiers.includes('number')
+                    ? 'Array.from($event.target.selectedOptions).map(option => { return parseFloat(option.value || option.text) })'
+                    : 'Array.from($event.target.selectedOptions).map(option => { return option.value || option.text })';
+            } else {
+                rightSideOfExpression = modifiers.includes('number')
+                    ? 'parseFloat($event.target.value)'
+                    : (modifiers.includes('trim') ? '$event.target.value.trim()' : '$event.target.value');
+            }
+
+            if (el.type === 'radio') {
+                // Radio buttons only work properly when they share a name attribute.
+                // People might assume we take care of that for them, because
+                // they already set a shared "x-model" attribute.
+                if (! el.hasAttribute('name')) el.setAttribute('name', dataKey);
+            }
+
+            return `${dataKey} = ${rightSideOfExpression}`
+        }
+
+        registerListener(el, event, modifiers, expression) {
+            if (modifiers.includes('away')) {
+                const handler = e => {
+                    // Don't do anything if the click came form the element or within it.
+                    if (el.contains(e.target)) return
+
+                    // Don't do anything if this element isn't currently visible.
+                    if (el.offsetWidth < 1 && el.offsetHeight < 1) return
+
+                    // Now that we are sure the element is visible, AND the click
+                    // is from outside it, let's run the expression.
+                    this.runListenerHandler(expression, e);
+
+                    if (modifiers.includes('once')) {
+                        document.removeEventListener(event, handler);
+                    }
+                };
+
+                // Listen for this event at the root level.
+                document.addEventListener(event, handler);
+            } else {
+                const listenerTarget = modifiers.includes('window')
+                    ? window : (modifiers.includes('document') ? document : el);
+
+                const handler = e => {
+                    const modifiersWithoutWindowOrDocument = modifiers
+                        .filter(i => i !== 'window').filter(i => i !== 'document');
+
+                    if (event === 'keydown' && modifiersWithoutWindow.length > 0 && ! modifiersWithoutWindow.includes(kebabCase(e.key))) return
+
+                    if (modifiers.includes('prevent')) e.preventDefault();
+                    if (modifiers.includes('stop')) e.stopPropagation();
+
+                    this.runListenerHandler(expression, e);
+
+                    if (modifiers.includes('once')) {
+                        listenerTarget.removeEventListener(event, handler);
+                    }
+                };
+
+                listenerTarget.addEventListener(event, handler);
+            }
+        }
+
+        runListenerHandler(expression, e) {
+            this.evaluateCommandExpression(expression, {
+                '$event': e,
+            });
+        }
+
+        evaluateReturnExpression(expression) {
+            var affectedDataKeys = [];
+
+            const proxyHandler = prefix => ({
+                get(object, prop) {
+                    // Sometimes non-proxyable values are accessed. These are of type "symbol".
+                    // We can ignore them.
+                    if (typeof prop === 'symbol') return
+
+                    const propertyName = prefix ? `${prefix}.${prop}` : prop;
+
+                    // If we are accessing an object prop, we'll make this proxy recursive to build
+                    // a nested dependancy key.
+                    if (typeof object[prop] === 'object' && object[prop] !== null && ! Array.isArray(object[prop])) {
+                        return new Proxy(object[prop], proxyHandler(propertyName))
+                    }
+
+                    affectedDataKeys.push(propertyName);
+
+                    return object[prop]
+                }
+            });
+
+            const proxiedData = new Proxy(this.data, proxyHandler());
+
+            const result = saferEval(expression, proxiedData);
+
+            return {
+                output: result,
+                deps: affectedDataKeys
+            }
+        }
+
+        evaluateCommandExpression(expression, extraData) {
+            saferEvalNoReturn(expression, this.data, extraData);
+        }
+
+        updateTextValue(el, value) {
+            el.innerText = value;
+        }
+
+        updateVisibility(el, value) {
+            if (! value) {
+                el.style.display = 'none';
+            } else {
+                if (el.style.length === 1 && el.style.display !== '') {
+                    el.removeAttribute('style');
+                } else {
+                    el.style.removeProperty('display');
+                }
+            }
+        }
+
+        updatePresence(el, expressionResult) {
+            if (el.nodeName.toLowerCase() !== 'template') console.warn(`Alpine: [x-if] directive should only be added to <template> tags.`);
+
+            const elementHasAlreadyBeenAdded = el.nextElementSibling && el.nextElementSibling.__x_inserted_me === true;
+
+            if (expressionResult && ! elementHasAlreadyBeenAdded) {
+                const clone = document.importNode(el.content, true);
+
+                el.parentElement.insertBefore(clone, el.nextElementSibling);
+
+                el.nextElementSibling.__x_inserted_me = true;
+            } else if (! expressionResult && elementHasAlreadyBeenAdded) {
+                el.nextElementSibling.remove();
+            }
+        }
+
+        updateAttributeValue(el, attrName, value) {
+            if (attrName === 'value') {
+                if (el.type === 'radio') {
+                    el.checked = el.value == value;
+                } else if (el.type === 'checkbox') {
+                    if (Array.isArray(value)) {
+                        // I'm purposely not using Array.includes here because it's
+                        // strict, and because of Numeric/String mis-casting, I
+                        // want the "includes" to be "fuzzy".
+                        let valueFound = false;
+                        value.forEach(val => {
+                            if (val == el.value) {
+                                valueFound = true;
+                            }
+                        });
+
+                        el.checked = valueFound;
+                    } else {
+                        el.checked = !! value;
+                    }
+                } else if (el.tagName === 'SELECT') {
+                    this.updateSelect(el, value);
+                } else {
+                    el.value = value;
+                }
+            } else if (attrName === 'class') {
+                if (Array.isArray(value)) {
+                    el.setAttribute('class', value.join(' '));
+                } else {
+                    // Use the class object syntax that vue uses to toggle them.
+                    Object.keys(value).forEach(classNames => {
+                        if (value[classNames]) {
+                            classNames.split(' ').forEach(className => el.classList.add(className));
+                        } else {
+                            classNames.split(' ').forEach(className => el.classList.remove(className));
+                        }
+                    });
+                }
+            } else if (['disabled', 'readonly', 'required', 'checked', 'hidden'].includes(attrName)) {
+                // Boolean attributes have to be explicitly added and removed, not just set.
+                if (!! value) {
+                    el.setAttribute(attrName, '');
+                } else {
+                    el.removeAttribute(attrName);
+                }
+            } else {
+                el.setAttribute(attrName, value);
+            }
+        }
+
+        updateSelect(el, value) {
+            const arrayWrappedValue = [].concat(value).map(value => { return value + '' });
+
+            Array.from(el.options).forEach(option => {
+                option.selected = arrayWrappedValue.includes(option.value || option.text);
+            });
+        }
+
+        getRefsProxy() {
+            var self = this;
+
+            // One of the goals of this is to not hold elements in memory, but rather re-evaluate
+            // the DOM when the system needs something from it. This way, the framework is flexible and
+            // friendly to outside DOM changes from libraries like Vue/Livewire.
+            // For this reason, I'm using an "on-demand" proxy to fake a "$refs" object.
+            return new Proxy({}, {
+                get(object, property) {
+                    var ref;
+
+                    // We can't just query the DOM because it's hard to filter out refs in
+                    // nested components.
+                    walkSkippingNestedComponents(self.el, el => {
+                        if (el.hasAttribute('x-ref') && el.getAttribute('x-ref') === property) {
+                            ref = el;
+                        }
+                    });
+
+                    return ref
+                }
+            })
+        }
+    }
+
+    const Alpine = {
+        start: async function () {
+            if (! isTesting()) {
+                await domReady();
+            }
+
+            this.discoverComponents(el => {
+                this.initializeComponent(el);
+            });
+
+            // It's easier and more performant to just support Turbolinks than listen
+            // to MutationOberserver mutations at the document level.
+            document.addEventListener("turbolinks:load", () => {
+                this.discoverUninitializedComponents(el => {
+                    this.initializeComponent(el);
+                });
+            });
+
+            this.listenForNewUninitializedComponentsAtRunTime(el => {
+                this.initializeComponent(el);
+            });
+        },
+
+        discoverComponents: function (callback) {
+            const rootEls = document.querySelectorAll('[x-data]');
+
+            rootEls.forEach(rootEl => {
+                callback(rootEl);
+            });
+        },
+
+        discoverUninitializedComponents: function (callback) {
+            const rootEls = document.querySelectorAll('[x-data]');
+
+            Array.from(rootEls)
+                .filter(el => el.__x === undefined)
+                .forEach(rootEl => {
+                    callback(rootEl);
+                });
+        },
+
+        listenForNewUninitializedComponentsAtRunTime: function (callback) {
+            const targetNode = document.querySelector('body');
+
+            const observerOptions = {
+                childList: true,
+                attributes: true,
+                subtree: true,
+            };
+
+            const observer = new MutationObserver((mutations) => {
+                for (let i=0; i < mutations.length; i++){
+                    if (mutations[i].addedNodes.length > 0) {
+                        mutations[i].addedNodes.forEach(node => {
+                            if (node.nodeType !== 1) return
+
+                            if (node.matches('[x-data]')) callback(node);
+                        });
+                    }
+                  }
+            });
+
+            observer.observe(targetNode, observerOptions);
+        },
+
+        initializeComponent: function (el) {
+            el.__x = new Component(el);
+        }
+    };
+
+    if (! isTesting()) {
+        window.Alpine = Alpine;
+        window.Alpine.start();
+    }
+
+    return Alpine;
+
+})));

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
dist/alpine.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
dist/alpine.js.map


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
dist/alpine.mjs


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
dist/alpine.mjs.map


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
dist/alpine.umd.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
dist/alpine.umd.js.map


+ 1 - 1
index.html

@@ -5,7 +5,7 @@
             [x-cloak] { display: none; }
         </style>
 
-        <script src="/dist/alpine.umd.js" defer></script>
+        <script src="/dist/alpine.js" defer></script>
     </head>
     <body>
         <table>

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 57 - 747
package-lock.json


+ 8 - 5
package.json

@@ -1,28 +1,31 @@
 {
   "main": "dist/alpine.js",
   "name": "alpinejs",
-  "version": "1.1.2",
+  "version": "1.1.3",
   "repository": {
     "type": "git",
     "url": "git://github.com/alpinejs/alpine.git"
   },
   "scripts": {
-    "watch": "npx microbundle watch src/index.js",
-    "build": "npx microbundle build src/index.js",
+    "watch": "npx rollup -c -w",
+    "build": "npx rollup -c",
     "test": "npx jest",
     "test:debug": "node --inspect node_modules/.bin/jest --runInBand"
   },
   "author": "Caleb Porzio",
   "license": "MIT",
   "devDependencies": {
+    "@babel/core": "^7.7.7",
     "@babel/preset-env": "^7.7.7",
     "@testing-library/dom": "^6.11.0",
     "@testing-library/jest-dom": "^4.2.4",
     "babel-jest": "^24.8.0",
     "jest": "^24.8.0",
     "jsdom-simulant": "^1.1.2",
-    "microbundle": "^0.11.0",
-    "mock-echo": "^1.1.0"
+    "rollup": "^1.27.14",
+    "rollup-plugin-babel": "^4.3.3",
+    "rollup-plugin-node-resolve": "^5.2.0",
+    "rollup-plugin-terser": "^5.1.3"
   },
   "dependencies": {}
 }

+ 23 - 0
rollup.config.js

@@ -0,0 +1,23 @@
+import resolve from "rollup-plugin-node-resolve"
+import babel from 'rollup-plugin-babel';
+import { terser } from "rollup-plugin-terser";
+
+export default {
+    input: 'src/index.js',
+    output: {
+        name: 'Alpine',
+        file: 'dist/alpine.js',
+        format: 'umd',
+    },
+    plugins: [
+        resolve(),
+        terser({
+            compress: {
+                drop_debugger: false,
+            },
+        }),
+        babel({
+            exclude: 'node_modules/**'
+        })
+    ]
+}

+ 1 - 1
src/component.js

@@ -238,7 +238,7 @@ export default class Component {
                 const modifiersWithoutWindowOrDocument = modifiers
                     .filter(i => i !== 'window').filter(i => i !== 'document')
 
-                if (event === 'keydown' && modifiersWithoutWindow.length > 0 && ! modifiersWithoutWindow.includes(kebabCase(e.key))) return
+                if (event === 'keydown' && modifiersWithoutWindowOrDocument.length > 0 && ! modifiersWithoutWindowOrDocument.includes(kebabCase(e.key))) return
 
                 if (modifiers.includes('prevent')) e.preventDefault()
                 if (modifiers.includes('stop')) e.stopPropagation()

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.