diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dd5082c40..28bb58e15 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,7 +30,7 @@ jobs: - name: Install Python Dependencies run: pip install -r requirements/test-run.txt - name: Run Tests - run: nox -s test -- --headless + run: nox -s test --verbose -- --headless test-python-versions: runs-on: ${{ matrix.os }} strategy: @@ -54,7 +54,7 @@ jobs: - name: Install Python Dependencies run: pip install -r requirements/test-run.txt - name: Run Tests - run: nox -s test -- --headless --no-cov + run: nox -s test --verbose -- --headless --no-cov test-javascript: runs-on: ubuntu-latest steps: @@ -67,7 +67,7 @@ jobs: npm install -g npm@latest npm --version - name: Test Javascript - working-directory: ./src/idom/client/app + working-directory: ./src/idom/client run: | npm install npm test diff --git a/.gitignore b/.gitignore index 07cc6eff7..2e8eb32af 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,7 @@ pip-wheel-metadata # --- IDE --- .idea .vscode + +# --- JS --- + +node_modules diff --git a/MANIFEST.in b/MANIFEST.in index 853a30e12..d780ab7a3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,4 @@ recursive-include src/idom * -recursive-exclude src/idom/client/app/node_modules * -recursive-exclude src/idom/client/app/web_modules * -recursive-exclude src/idom/client/build * +recursive-exclude src/idom/client/node_modules * include requirements/prod.txt include requirements/extras.txt diff --git a/docs/Dockerfile b/docs/Dockerfile index 252732b18..5c5781ab5 100644 --- a/docs/Dockerfile +++ b/docs/Dockerfile @@ -28,11 +28,6 @@ ADD README.md ./ RUN pip install -e .[all] -ENV IDOM_DEBUG_MODE=1 -ENV IDOM_CLIENT_BUILD_DIR=./build - -RUN python -m idom install htm victory semantic-ui-react @material-ui/core - # Build the Docs # -------------- ADD docs/main.py ./docs/ @@ -44,4 +39,5 @@ RUN sphinx-build -b html docs/source docs/build # Define Entrypoint # ----------------- ENV PORT 5000 +ENV IDOM_DEBUG_MODE=1 CMD python docs/main.py diff --git a/docs/main.py b/docs/main.py index 062ec5e7e..20dfd7014 100644 --- a/docs/main.py +++ b/docs/main.py @@ -6,9 +6,9 @@ from sanic import Sanic, response import idom -from idom.client.manage import web_modules_dir +from idom.config import IDOM_WED_MODULES_DIR from idom.server.sanic import PerClientStateServer -from idom.widgets.utils import multiview +from idom.widgets import multiview HERE = Path(__file__).parent @@ -18,7 +18,7 @@ def make_app(): app = Sanic(__name__) app.static("/docs", str(HERE / "build")) - app.static("/_modules", str(web_modules_dir())) + app.static("/_modules", str(IDOM_WED_MODULES_DIR.current)) @app.route("/") async def forward_to_index(request): diff --git a/docs/source/_exts/build_custom_js.py b/docs/source/_exts/build_custom_js.py new file mode 100644 index 000000000..c37e9847f --- /dev/null +++ b/docs/source/_exts/build_custom_js.py @@ -0,0 +1,13 @@ +import subprocess +from pathlib import Path + +from sphinx.application import Sphinx + + +SOURCE_DIR = Path(__file__).parent.parent +CUSTOM_JS_DIR = SOURCE_DIR / "custom_js" + + +def setup(app: Sphinx) -> None: + subprocess.run("npm install", cwd=CUSTOM_JS_DIR, shell=True) + subprocess.run("npm run build", cwd=CUSTOM_JS_DIR, shell=True) diff --git a/docs/source/_exts/interactive_widget.py b/docs/source/_exts/interactive_widget.py index 02a4696c4..5c37d1d48 100644 --- a/docs/source/_exts/interactive_widget.py +++ b/docs/source/_exts/interactive_widget.py @@ -26,8 +26,8 @@ def run(self):
""", diff --git a/docs/source/_static/custom.js b/docs/source/_static/custom.js new file mode 100644 index 000000000..ac042872d --- /dev/null +++ b/docs/source/_static/custom.js @@ -0,0 +1,1946 @@ +function createCommonjsModule(fn, module) { + return module = { exports: {} }, fn(module, module.exports), module.exports; +} + +/* +object-assign +(c) Sindre Sorhus +@license MIT +*/ +/* eslint-disable no-unused-vars */ +var getOwnPropertySymbols = Object.getOwnPropertySymbols; +var hasOwnProperty$1 = Object.prototype.hasOwnProperty; +var propIsEnumerable = Object.prototype.propertyIsEnumerable; + +function toObject(val) { + if (val === null || val === undefined) { + throw new TypeError('Object.assign cannot be called with null or undefined'); + } + + return Object(val); +} + +function shouldUseNative() { + try { + if (!Object.assign) { + return false; + } + + // Detect buggy property enumeration order in older V8 versions. + + // https://bugs.chromium.org/p/v8/issues/detail?id=4118 + var test1 = new String('abc'); // eslint-disable-line no-new-wrappers + test1[5] = 'de'; + if (Object.getOwnPropertyNames(test1)[0] === '5') { + return false; + } + + // https://bugs.chromium.org/p/v8/issues/detail?id=3056 + var test2 = {}; + for (var i = 0; i < 10; i++) { + test2['_' + String.fromCharCode(i)] = i; + } + var order2 = Object.getOwnPropertyNames(test2).map(function (n) { + return test2[n]; + }); + if (order2.join('') !== '0123456789') { + return false; + } + + // https://bugs.chromium.org/p/v8/issues/detail?id=3056 + var test3 = {}; + 'abcdefghijklmnopqrst'.split('').forEach(function (letter) { + test3[letter] = letter; + }); + if (Object.keys(Object.assign({}, test3)).join('') !== + 'abcdefghijklmnopqrst') { + return false; + } + + return true; + } catch (err) { + // We don't expect any of the above to throw, but better to be safe. + return false; + } +} + +var objectAssign = shouldUseNative() ? Object.assign : function (target, source) { + var from; + var to = toObject(target); + var symbols; + + for (var s = 1; s < arguments.length; s++) { + from = Object(arguments[s]); + + for (var key in from) { + if (hasOwnProperty$1.call(from, key)) { + to[key] = from[key]; + } + } + + if (getOwnPropertySymbols) { + symbols = getOwnPropertySymbols(from); + for (var i = 0; i < symbols.length; i++) { + if (propIsEnumerable.call(from, symbols[i])) { + to[symbols[i]] = from[symbols[i]]; + } + } + } + } + + return to; +}; + +var n$1="function"===typeof Symbol&&Symbol.for,p=n$1?Symbol.for("react.element"):60103,q=n$1?Symbol.for("react.portal"):60106,r=n$1?Symbol.for("react.fragment"):60107,t$1=n$1?Symbol.for("react.strict_mode"):60108,u$1=n$1?Symbol.for("react.profiler"):60114,v$1=n$1?Symbol.for("react.provider"):60109,w=n$1?Symbol.for("react.context"):60110,x=n$1?Symbol.for("react.forward_ref"):60112,y=n$1?Symbol.for("react.suspense"):60113,z=n$1?Symbol.for("react.memo"):60115,A=n$1?Symbol.for("react.lazy"): +60116,B="function"===typeof Symbol&&Symbol.iterator;function C$1(a){for(var b="https://reactjs.org/docs/error-decoder.html?invariant="+a,c=1;cQ$1.length&&Q$1.push(a);} +function T$1(a,b,c,e){var d=typeof a;if("undefined"===d||"boolean"===d)a=null;var g=!1;if(null===a)g=!0;else switch(d){case "string":case "number":g=!0;break;case "object":switch(a.$$typeof){case p:case q:g=!0;}}if(g)return c(e,a,""===b?"."+U$1(a,0):b),1;g=0;b=""===b?".":b+":";if(Array.isArray(a))for(var k=0;k=G};l=function(){};exports.unstable_forceFrameRate=function(a){0>a||125>>1,e=a[d];if(void 0!==e&&0K(n,c))void 0!==r&&0>K(r,n)?(a[d]=r,a[v]=c,d=v):(a[d]=n,a[m]=c,d=m);else if(void 0!==r&&0>K(r,c))a[d]=r,a[v]=c,d=v;else break a}}return b}return null}function K(a,b){var c=a.sortIndex-b.sortIndex;return 0!==c?c:a.id-b.id}var N=[],O=[],P=1,Q=null,R=3,S=!1,T=!1,U=!1; +function V(a){for(var b=L(O);null!==b;){if(null===b.callback)M(O);else if(b.startTime<=a)M(O),b.sortIndex=b.expirationTime,J(N,b);else break;b=L(O);}}function W(a){U=!1;V(a);if(!T)if(null!==L(N))T=!0,f(X);else {var b=L(O);null!==b&&g(W,b.startTime-a);}} +function X(a,b){T=!1;U&&(U=!1,h());S=!0;var c=R;try{V(b);for(Q=L(N);null!==Q&&(!(Q.expirationTime>b)||a&&!k());){var d=Q.callback;if(null!==d){Q.callback=null;R=Q.priorityLevel;var e=d(Q.expirationTime<=b);b=exports.unstable_now();"function"===typeof e?Q.callback=e:Q===L(N)&&M(N);V(b);}else M(N);Q=L(N);}if(null!==Q)var m=!0;else {var n=L(O);null!==n&&g(W,n.startTime-b);m=!1;}return m}finally{Q=null,R=c,S=!1;}} +function Y(a){switch(a){case 1:return -1;case 2:return 250;case 5:return 1073741823;case 4:return 1E4;default:return 5E3}}var Z=l;exports.unstable_IdlePriority=5;exports.unstable_ImmediatePriority=1;exports.unstable_LowPriority=4;exports.unstable_NormalPriority=3;exports.unstable_Profiling=null;exports.unstable_UserBlockingPriority=2;exports.unstable_cancelCallback=function(a){a.callback=null;};exports.unstable_continueExecution=function(){T||S||(T=!0,f(X));}; +exports.unstable_getCurrentPriorityLevel=function(){return R};exports.unstable_getFirstCallbackNode=function(){return L(N)};exports.unstable_next=function(a){switch(R){case 1:case 2:case 3:var b=3;break;default:b=R;}var c=R;R=b;try{return a()}finally{R=c;}};exports.unstable_pauseExecution=function(){};exports.unstable_requestPaint=Z;exports.unstable_runWithPriority=function(a,b){switch(a){case 1:case 2:case 3:case 4:case 5:break;default:a=3;}var c=R;R=a;try{return b()}finally{R=c;}}; +exports.unstable_scheduleCallback=function(a,b,c){var d=exports.unstable_now();if("object"===typeof c&&null!==c){var e=c.delay;e="number"===typeof e&&0d?(a.sortIndex=e,J(O,a),null===L(N)&&a===L(O)&&(U?h():U=!0,g(W,e-d))):(a.sortIndex=c,J(N,a),T||S||(T=!0,f(X)));return a}; +exports.unstable_shouldYield=function(){var a=exports.unstable_now();V(a);var b=L(N);return b!==Q&&null!==Q&&null!==b&&null!==b.callback&&b.startTime<=a&&b.expirationTimeb}return !1}function v(a,b,c,d,e,f){this.acceptsBooleans=2===b||3===b||4===b;this.attributeName=d;this.attributeNamespace=e;this.mustUseProperty=c;this.propertyName=a;this.type=b;this.sanitizeURL=f;}var C={}; +"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(a){C[a]=new v(a,0,!1,a,null,!1);});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(a){var b=a[0];C[b]=new v(b,1,!1,a[1],null,!1);});["contentEditable","draggable","spellCheck","value"].forEach(function(a){C[a]=new v(a,2,!1,a.toLowerCase(),null,!1);}); +["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(a){C[a]=new v(a,2,!1,a,null,!1);});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(a){C[a]=new v(a,3,!1,a.toLowerCase(),null,!1);}); +["checked","multiple","muted","selected"].forEach(function(a){C[a]=new v(a,3,!0,a,null,!1);});["capture","download"].forEach(function(a){C[a]=new v(a,4,!1,a,null,!1);});["cols","rows","size","span"].forEach(function(a){C[a]=new v(a,6,!1,a,null,!1);});["rowSpan","start"].forEach(function(a){C[a]=new v(a,5,!1,a.toLowerCase(),null,!1);});var Ua=/[\-:]([a-z])/g;function Va(a){return a[1].toUpperCase()} +"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(a){var b=a.replace(Ua, +Va);C[b]=new v(b,1,!1,a,null,!1);});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(a){var b=a.replace(Ua,Va);C[b]=new v(b,1,!1,a,"http://www.w3.org/1999/xlink",!1);});["xml:base","xml:lang","xml:space"].forEach(function(a){var b=a.replace(Ua,Va);C[b]=new v(b,1,!1,a,"http://www.w3.org/XML/1998/namespace",!1);});["tabIndex","crossOrigin"].forEach(function(a){C[a]=new v(a,1,!1,a.toLowerCase(),null,!1);}); +C.xlinkHref=new v("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0);["src","href","action","formAction"].forEach(function(a){C[a]=new v(a,1,!1,a.toLowerCase(),null,!0);});var Wa=react.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;Wa.hasOwnProperty("ReactCurrentDispatcher")||(Wa.ReactCurrentDispatcher={current:null});Wa.hasOwnProperty("ReactCurrentBatchConfig")||(Wa.ReactCurrentBatchConfig={suspense:null}); +function Xa(a,b,c,d){var e=C.hasOwnProperty(b)?C[b]:null;var f=null!==e?0===e.type:d?!1:!(2=c.length))throw Error(u(93));c=c[0];}b=c;}null==b&&(b="");c=b;}a._wrapperState={initialValue:rb(c)};} +function Kb(a,b){var c=rb(b.value),d=rb(b.defaultValue);null!=c&&(c=""+c,c!==a.value&&(a.value=c),null==b.defaultValue&&a.defaultValue!==c&&(a.defaultValue=c));null!=d&&(a.defaultValue=""+d);}function Lb(a){var b=a.textContent;b===a._wrapperState.initialValue&&""!==b&&null!==b&&(a.value=b);}var Mb={html:"http://www.w3.org/1999/xhtml",mathml:"http://www.w3.org/1998/Math/MathML",svg:"http://www.w3.org/2000/svg"}; +function Nb(a){switch(a){case "svg":return "http://www.w3.org/2000/svg";case "math":return "http://www.w3.org/1998/Math/MathML";default:return "http://www.w3.org/1999/xhtml"}}function Ob(a,b){return null==a||"http://www.w3.org/1999/xhtml"===a?Nb(b):"http://www.w3.org/2000/svg"===a&&"foreignObject"===b?"http://www.w3.org/1999/xhtml":a} +var Pb,Qb=function(a){return "undefined"!==typeof MSApp&&MSApp.execUnsafeLocalFunction?function(b,c,d,e){MSApp.execUnsafeLocalFunction(function(){return a(b,c,d,e)});}:a}(function(a,b){if(a.namespaceURI!==Mb.svg||"innerHTML"in a)a.innerHTML=b;else {Pb=Pb||document.createElement("div");Pb.innerHTML=""+b.valueOf().toString()+"";for(b=Pb.firstChild;a.firstChild;)a.removeChild(a.firstChild);for(;b.firstChild;)a.appendChild(b.firstChild);}}); +function Rb(a,b){if(b){var c=a.firstChild;if(c&&c===a.lastChild&&3===c.nodeType){c.nodeValue=b;return}}a.textContent=b;}function Sb(a,b){var c={};c[a.toLowerCase()]=b.toLowerCase();c["Webkit"+a]="webkit"+b;c["Moz"+a]="moz"+b;return c}var Tb={animationend:Sb("Animation","AnimationEnd"),animationiteration:Sb("Animation","AnimationIteration"),animationstart:Sb("Animation","AnimationStart"),transitionend:Sb("Transition","TransitionEnd")},Ub={},Vb={}; +ya&&(Vb=document.createElement("div").style,"AnimationEvent"in window||(delete Tb.animationend.animation,delete Tb.animationiteration.animation,delete Tb.animationstart.animation),"TransitionEvent"in window||delete Tb.transitionend.transition);function Wb(a){if(Ub[a])return Ub[a];if(!Tb[a])return a;var b=Tb[a],c;for(c in b)if(b.hasOwnProperty(c)&&c in Vb)return Ub[a]=b[c];return a} +var Xb=Wb("animationend"),Yb=Wb("animationiteration"),Zb=Wb("animationstart"),$b=Wb("transitionend"),ac="abort canplay canplaythrough durationchange emptied encrypted ended error loadeddata loadedmetadata loadstart pause play playing progress ratechange seeked seeking stalled suspend timeupdate volumechange waiting".split(" "),bc=new ("function"===typeof WeakMap?WeakMap:Map);function cc(a){var b=bc.get(a);void 0===b&&(b=new Map,bc.set(a,b));return b} +function dc(a){var b=a,c=a;if(a.alternate)for(;b.return;)b=b.return;else {a=b;do b=a,0!==(b.effectTag&1026)&&(c=b.return),a=b.return;while(a)}return 3===b.tag?c:null}function ec(a){if(13===a.tag){var b=a.memoizedState;null===b&&(a=a.alternate,null!==a&&(b=a.memoizedState));if(null!==b)return b.dehydrated}return null}function fc(a){if(dc(a)!==a)throw Error(u(188));} +function gc(a){var b=a.alternate;if(!b){b=dc(a);if(null===b)throw Error(u(188));return b!==a?null:a}for(var c=a,d=b;;){var e=c.return;if(null===e)break;var f=e.alternate;if(null===f){d=e.return;if(null!==d){c=d;continue}break}if(e.child===f.child){for(f=e.child;f;){if(f===c)return fc(e),a;if(f===d)return fc(e),b;f=f.sibling;}throw Error(u(188));}if(c.return!==d.return)c=e,d=f;else {for(var g=!1,h=e.child;h;){if(h===c){g=!0;c=e;d=f;break}if(h===d){g=!0;d=e;c=f;break}h=h.sibling;}if(!g){for(h=f.child;h;){if(h=== +c){g=!0;c=f;d=e;break}if(h===d){g=!0;d=f;c=e;break}h=h.sibling;}if(!g)throw Error(u(189));}}if(c.alternate!==d)throw Error(u(190));}if(3!==c.tag)throw Error(u(188));return c.stateNode.current===c?a:b}function hc(a){a=gc(a);if(!a)return null;for(var b=a;;){if(5===b.tag||6===b.tag)return b;if(b.child)b.child.return=b,b=b.child;else {if(b===a)break;for(;!b.sibling;){if(!b.return||b.return===a)return null;b=b.return;}b.sibling.return=b.return;b=b.sibling;}}return null} +function ic(a,b){if(null==b)throw Error(u(30));if(null==a)return b;if(Array.isArray(a)){if(Array.isArray(b))return a.push.apply(a,b),a;a.push(b);return a}return Array.isArray(b)?[a].concat(b):[a,b]}function jc(a,b,c){Array.isArray(a)?a.forEach(b,c):a&&b.call(c,a);}var kc=null; +function lc(a){if(a){var b=a._dispatchListeners,c=a._dispatchInstances;if(Array.isArray(b))for(var d=0;dpc.length&&pc.push(a);} +function rc(a,b,c,d){if(pc.length){var e=pc.pop();e.topLevelType=a;e.eventSystemFlags=d;e.nativeEvent=b;e.targetInst=c;return e}return {topLevelType:a,eventSystemFlags:d,nativeEvent:b,targetInst:c,ancestors:[]}} +function sc(a){var b=a.targetInst,c=b;do{if(!c){a.ancestors.push(c);break}var d=c;if(3===d.tag)d=d.stateNode.containerInfo;else {for(;d.return;)d=d.return;d=3!==d.tag?null:d.stateNode.containerInfo;}if(!d)break;b=c.tag;5!==b&&6!==b||a.ancestors.push(c);c=tc(d);}while(c);for(c=0;c=b)return {node:c,offset:b-a};a=d;}a:{for(;c;){if(c.nextSibling){c=c.nextSibling;break a}c=c.parentNode;}c=void 0;}c=ud(c);}} +function wd(a,b){return a&&b?a===b?!0:a&&3===a.nodeType?!1:b&&3===b.nodeType?wd(a,b.parentNode):"contains"in a?a.contains(b):a.compareDocumentPosition?!!(a.compareDocumentPosition(b)&16):!1:!1}function xd(){for(var a=window,b=td();b instanceof a.HTMLIFrameElement;){try{var c="string"===typeof b.contentWindow.location.href;}catch(d){c=!1;}if(c)a=b.contentWindow;else break;b=td(a.document);}return b} +function yd(a){var b=a&&a.nodeName&&a.nodeName.toLowerCase();return b&&("input"===b&&("text"===a.type||"search"===a.type||"tel"===a.type||"url"===a.type||"password"===a.type)||"textarea"===b||"true"===a.contentEditable)}var zd="$",Ad="/$",Bd="$?",Cd="$!",Dd=null,Ed=null;function Fd(a,b){switch(a){case "button":case "input":case "select":case "textarea":return !!b.autoFocus}return !1} +function Gd(a,b){return "textarea"===a||"option"===a||"noscript"===a||"string"===typeof b.children||"number"===typeof b.children||"object"===typeof b.dangerouslySetInnerHTML&&null!==b.dangerouslySetInnerHTML&&null!=b.dangerouslySetInnerHTML.__html}var Hd="function"===typeof setTimeout?setTimeout:void 0,Id="function"===typeof clearTimeout?clearTimeout:void 0;function Jd(a){for(;null!=a;a=a.nextSibling){var b=a.nodeType;if(1===b||3===b)break}return a} +function Kd(a){a=a.previousSibling;for(var b=0;a;){if(8===a.nodeType){var c=a.data;if(c===zd||c===Cd||c===Bd){if(0===b)return a;b--;}else c===Ad&&b++;}a=a.previousSibling;}return null}var Ld=Math.random().toString(36).slice(2),Md="__reactInternalInstance$"+Ld,Nd="__reactEventHandlers$"+Ld,Od="__reactContainere$"+Ld; +function tc(a){var b=a[Md];if(b)return b;for(var c=a.parentNode;c;){if(b=c[Od]||c[Md]){c=b.alternate;if(null!==b.child||null!==c&&null!==c.child)for(a=Kd(a);null!==a;){if(c=a[Md])return c;a=Kd(a);}return b}a=c;c=a.parentNode;}return null}function Nc(a){a=a[Md]||a[Od];return !a||5!==a.tag&&6!==a.tag&&13!==a.tag&&3!==a.tag?null:a}function Pd(a){if(5===a.tag||6===a.tag)return a.stateNode;throw Error(u(33));}function Qd(a){return a[Nd]||null} +function Rd(a){do a=a.return;while(a&&5!==a.tag);return a?a:null} +function Sd(a,b){var c=a.stateNode;if(!c)return null;var d=la(c);if(!d)return null;c=d[b];a:switch(b){case "onClick":case "onClickCapture":case "onDoubleClick":case "onDoubleClickCapture":case "onMouseDown":case "onMouseDownCapture":case "onMouseMove":case "onMouseMoveCapture":case "onMouseUp":case "onMouseUpCapture":case "onMouseEnter":(d=!d.disabled)||(a=a.type,d=!("button"===a||"input"===a||"select"===a||"textarea"===a));a=!d;break a;default:a=!1;}if(a)return null;if(c&&"function"!==typeof c)throw Error(u(231, +b,typeof c));return c}function Td(a,b,c){if(b=Sd(a,c.dispatchConfig.phasedRegistrationNames[b]))c._dispatchListeners=ic(c._dispatchListeners,b),c._dispatchInstances=ic(c._dispatchInstances,a);}function Ud(a){if(a&&a.dispatchConfig.phasedRegistrationNames){for(var b=a._targetInst,c=[];b;)c.push(b),b=Rd(b);for(b=c.length;0this.eventPool.length&&this.eventPool.push(a);}function de(a){a.eventPool=[];a.getPooled=ee;a.release=fe;}var ge=G.extend({data:null}),he=G.extend({data:null}),ie=[9,13,27,32],je=ya&&"CompositionEvent"in window,ke=null;ya&&"documentMode"in document&&(ke=document.documentMode); +var le=ya&&"TextEvent"in window&&!ke,me=ya&&(!je||ke&&8=ke),ne=String.fromCharCode(32),oe={beforeInput:{phasedRegistrationNames:{bubbled:"onBeforeInput",captured:"onBeforeInputCapture"},dependencies:["compositionend","keypress","textInput","paste"]},compositionEnd:{phasedRegistrationNames:{bubbled:"onCompositionEnd",captured:"onCompositionEndCapture"},dependencies:"blur compositionend keydown keypress keyup mousedown".split(" ")},compositionStart:{phasedRegistrationNames:{bubbled:"onCompositionStart", +captured:"onCompositionStartCapture"},dependencies:"blur compositionstart keydown keypress keyup mousedown".split(" ")},compositionUpdate:{phasedRegistrationNames:{bubbled:"onCompositionUpdate",captured:"onCompositionUpdateCapture"},dependencies:"blur compositionupdate keydown keypress keyup mousedown".split(" ")}},pe=!1; +function qe(a,b){switch(a){case "keyup":return -1!==ie.indexOf(b.keyCode);case "keydown":return 229!==b.keyCode;case "keypress":case "mousedown":case "blur":return !0;default:return !1}}function re(a){a=a.detail;return "object"===typeof a&&"data"in a?a.data:null}var se=!1;function te(a,b){switch(a){case "compositionend":return re(b);case "keypress":if(32!==b.which)return null;pe=!0;return ne;case "textInput":return a=b.data,a===ne&&pe?null:a;default:return null}} +function ue(a,b){if(se)return "compositionend"===a||!je&&qe(a,b)?(a=ae(),$d=Zd=Yd=null,se=!1,a):null;switch(a){case "paste":return null;case "keypress":if(!(b.ctrlKey||b.altKey||b.metaKey)||b.ctrlKey&&b.altKey){if(b.char&&1=document.documentMode,df={select:{phasedRegistrationNames:{bubbled:"onSelect",captured:"onSelectCapture"},dependencies:"blur contextmenu dragend focus keydown keyup mousedown mouseup selectionchange".split(" ")}},ef=null,ff=null,gf=null,hf=!1; +function jf(a,b){var c=b.window===b?b.document:9===b.nodeType?b:b.ownerDocument;if(hf||null==ef||ef!==td(c))return null;c=ef;"selectionStart"in c&&yd(c)?c={start:c.selectionStart,end:c.selectionEnd}:(c=(c.ownerDocument&&c.ownerDocument.defaultView||window).getSelection(),c={anchorNode:c.anchorNode,anchorOffset:c.anchorOffset,focusNode:c.focusNode,focusOffset:c.focusOffset});return gf&&bf(gf,c)?null:(gf=c,a=G.getPooled(df.select,ff,a,b),a.type="select",a.target=ef,Xd(a),a)} +var kf={eventTypes:df,extractEvents:function(a,b,c,d,e,f){e=f||(d.window===d?d.document:9===d.nodeType?d:d.ownerDocument);if(!(f=!e)){a:{e=cc(e);f=wa.onSelect;for(var g=0;gzf||(a.current=yf[zf],yf[zf]=null,zf--);} +function I(a,b){zf++;yf[zf]=a.current;a.current=b;}var Af={},J={current:Af},K={current:!1},Bf=Af;function Cf(a,b){var c=a.type.contextTypes;if(!c)return Af;var d=a.stateNode;if(d&&d.__reactInternalMemoizedUnmaskedChildContext===b)return d.__reactInternalMemoizedMaskedChildContext;var e={},f;for(f in c)e[f]=b[f];d&&(a=a.stateNode,a.__reactInternalMemoizedUnmaskedChildContext=b,a.__reactInternalMemoizedMaskedChildContext=e);return e}function L(a){a=a.childContextTypes;return null!==a&&void 0!==a} +function Df(){H(K);H(J);}function Ef(a,b,c){if(J.current!==Af)throw Error(u(168));I(J,b);I(K,c);}function Ff(a,b,c){var d=a.stateNode;a=b.childContextTypes;if("function"!==typeof d.getChildContext)return c;d=d.getChildContext();for(var e in d)if(!(e in a))throw Error(u(108,pb(b)||"Unknown",e));return objectAssign({},c,{},d)}function Gf(a){a=(a=a.stateNode)&&a.__reactInternalMemoizedMergedChildContext||Af;Bf=J.current;I(J,a);I(K,K.current);return !0} +function Hf(a,b,c){var d=a.stateNode;if(!d)throw Error(u(169));c?(a=Ff(a,b,Bf),d.__reactInternalMemoizedMergedChildContext=a,H(K),H(J),I(J,a)):H(K);I(K,c);} +var If=scheduler.unstable_runWithPriority,Jf=scheduler.unstable_scheduleCallback,Kf=scheduler.unstable_cancelCallback,Lf=scheduler.unstable_requestPaint,Mf=scheduler.unstable_now,Nf=scheduler.unstable_getCurrentPriorityLevel,Of=scheduler.unstable_ImmediatePriority,Pf=scheduler.unstable_UserBlockingPriority,Qf=scheduler.unstable_NormalPriority,Rf=scheduler.unstable_LowPriority,Sf=scheduler.unstable_IdlePriority,Tf={},Uf=scheduler.unstable_shouldYield,Vf=void 0!==Lf?Lf:function(){},Wf=null,Xf=null,Yf=!1,Zf=Mf(),$f=1E4>Zf?Mf:function(){return Mf()-Zf}; +function ag(){switch(Nf()){case Of:return 99;case Pf:return 98;case Qf:return 97;case Rf:return 96;case Sf:return 95;default:throw Error(u(332));}}function bg(a){switch(a){case 99:return Of;case 98:return Pf;case 97:return Qf;case 96:return Rf;case 95:return Sf;default:throw Error(u(332));}}function cg(a,b){a=bg(a);return If(a,b)}function dg(a,b,c){a=bg(a);return Jf(a,b,c)}function eg(a){null===Wf?(Wf=[a],Xf=Jf(Of,fg)):Wf.push(a);return Tf}function gg(){if(null!==Xf){var a=Xf;Xf=null;Kf(a);}fg();} +function fg(){if(!Yf&&null!==Wf){Yf=!0;var a=0;try{var b=Wf;cg(99,function(){for(;a=b&&(rg=!0),a.firstContext=null);} +function sg(a,b){if(mg!==a&&!1!==b&&0!==b){if("number"!==typeof b||1073741823===b)mg=a,b=1073741823;b={context:a,observedBits:b,next:null};if(null===lg){if(null===kg)throw Error(u(308));lg=b;kg.dependencies={expirationTime:0,firstContext:b,responders:null};}else lg=lg.next=b;}return a._currentValue}var tg=!1;function ug(a){a.updateQueue={baseState:a.memoizedState,baseQueue:null,shared:{pending:null},effects:null};} +function vg(a,b){a=a.updateQueue;b.updateQueue===a&&(b.updateQueue={baseState:a.baseState,baseQueue:a.baseQueue,shared:a.shared,effects:a.effects});}function wg(a,b){a={expirationTime:a,suspenseConfig:b,tag:0,payload:null,callback:null,next:null};return a.next=a}function xg(a,b){a=a.updateQueue;if(null!==a){a=a.shared;var c=a.pending;null===c?b.next=b:(b.next=c.next,c.next=b);a.pending=b;}} +function yg(a,b){var c=a.alternate;null!==c&&vg(c,a);a=a.updateQueue;c=a.baseQueue;null===c?(a.baseQueue=b.next=b,b.next=b):(b.next=c.next,c.next=b);} +function zg(a,b,c,d){var e=a.updateQueue;tg=!1;var f=e.baseQueue,g=e.shared.pending;if(null!==g){if(null!==f){var h=f.next;f.next=g.next;g.next=h;}f=g;e.shared.pending=null;h=a.alternate;null!==h&&(h=h.updateQueue,null!==h&&(h.baseQueue=g));}if(null!==f){h=f.next;var k=e.baseState,l=0,m=null,p=null,x=null;if(null!==h){var z=h;do{g=z.expirationTime;if(gl&&(l=g);}else {null!==x&&(x=x.next={expirationTime:1073741823,suspenseConfig:z.suspenseConfig,tag:z.tag,payload:z.payload,callback:z.callback,next:null});Ag(g,z.suspenseConfig);a:{var D=a,t=z;g=b;ca=c;switch(t.tag){case 1:D=t.payload;if("function"===typeof D){k=D.call(ca,k,g);break a}k=D;break a;case 3:D.effectTag=D.effectTag&-4097|64;case 0:D=t.payload;g="function"===typeof D?D.call(ca,k,g):D;if(null===g||void 0===g)break a;k=objectAssign({},k,g);break a;case 2:tg=!0;}}null!==z.callback&& +(a.effectTag|=32,g=e.effects,null===g?e.effects=[z]:g.push(z));}z=z.next;if(null===z||z===h)if(g=e.shared.pending,null===g)break;else z=f.next=g.next,g.next=h,e.baseQueue=f=g,e.shared.pending=null;}while(1)}null===x?m=k:x.next=p;e.baseState=m;e.baseQueue=x;Bg(l);a.expirationTime=l;a.memoizedState=k;}} +function Cg(a,b,c){a=b.effects;b.effects=null;if(null!==a)for(b=0;by?(A=m,m=null):A=m.sibling;var q=x(e,m,h[y],k);if(null===q){null===m&&(m=A);break}a&& +m&&null===q.alternate&&b(e,m);g=f(q,g,y);null===t?l=q:t.sibling=q;t=q;m=A;}if(y===h.length)return c(e,m),l;if(null===m){for(;yy?(A=t,t=null):A=t.sibling;var D=x(e,t,q.value,l);if(null===D){null===t&&(t=A);break}a&&t&&null===D.alternate&&b(e,t);g=f(D,g,y);null===m?k=D:m.sibling=D;m=D;t=A;}if(q.done)return c(e,t),k;if(null===t){for(;!q.done;y++,q=h.next())q=p(e,q.value,l),null!==q&&(g=f(q,g,y),null===m?k=q:m.sibling=q,m=q);return k}for(t=d(e,t);!q.done;y++,q=h.next())q=z(t,e,y,q.value,l),null!==q&&(a&&null!== +q.alternate&&t.delete(null===q.key?y:q.key),g=f(q,g,y),null===m?k=q:m.sibling=q,m=q);a&&t.forEach(function(a){return b(e,a)});return k}return function(a,d,f,h){var k="object"===typeof f&&null!==f&&f.type===ab&&null===f.key;k&&(f=f.props.children);var l="object"===typeof f&&null!==f;if(l)switch(f.$$typeof){case Za:a:{l=f.key;for(k=d;null!==k;){if(k.key===l){switch(k.tag){case 7:if(f.type===ab){c(a,k.sibling);d=e(k,f.props.children);d.return=a;a=d;break a}break;default:if(k.elementType===f.type){c(a, +k.sibling);d=e(k,f.props);d.ref=Pg(a,k,f);d.return=a;a=d;break a}}c(a,k);break}else b(a,k);k=k.sibling;}f.type===ab?(d=Wg(f.props.children,a.mode,h,f.key),d.return=a,a=d):(h=Ug(f.type,f.key,f.props,null,a.mode,h),h.ref=Pg(a,d,f),h.return=a,a=h);}return g(a);case $a:a:{for(k=f.key;null!==d;){if(d.key===k)if(4===d.tag&&d.stateNode.containerInfo===f.containerInfo&&d.stateNode.implementation===f.implementation){c(a,d.sibling);d=e(d,f.children||[]);d.return=a;a=d;break a}else {c(a,d);break}else b(a,d);d= +d.sibling;}d=Vg(f,a.mode,h);d.return=a;a=d;}return g(a)}if("string"===typeof f||"number"===typeof f)return f=""+f,null!==d&&6===d.tag?(c(a,d.sibling),d=e(d,f),d.return=a,a=d):(c(a,d),d=Tg(f,a.mode,h),d.return=a,a=d),g(a);if(Og(f))return ca(a,d,f,h);if(nb(f))return D(a,d,f,h);l&&Qg(a,f);if("undefined"===typeof f&&!k)switch(a.tag){case 1:case 0:throw a=a.type,Error(u(152,a.displayName||a.name||"Component"));}return c(a,d)}}var Xg=Rg(!0),Yg=Rg(!1),Zg={},$g={current:Zg},ah={current:Zg},bh={current:Zg}; +function ch(a){if(a===Zg)throw Error(u(174));return a}function dh(a,b){I(bh,b);I(ah,a);I($g,Zg);a=b.nodeType;switch(a){case 9:case 11:b=(b=b.documentElement)?b.namespaceURI:Ob(null,"");break;default:a=8===a?b.parentNode:b,b=a.namespaceURI||null,a=a.tagName,b=Ob(b,a);}H($g);I($g,b);}function eh(){H($g);H(ah);H(bh);}function fh(a){ch(bh.current);var b=ch($g.current);var c=Ob(b,a.type);b!==c&&(I(ah,a),I($g,c));}function gh(a){ah.current===a&&(H($g),H(ah));}var M={current:0}; +function hh(a){for(var b=a;null!==b;){if(13===b.tag){var c=b.memoizedState;if(null!==c&&(c=c.dehydrated,null===c||c.data===Bd||c.data===Cd))return b}else if(19===b.tag&&void 0!==b.memoizedProps.revealOrder){if(0!==(b.effectTag&64))return b}else if(null!==b.child){b.child.return=b;b=b.child;continue}if(b===a)break;for(;null===b.sibling;){if(null===b.return||b.return===a)return null;b=b.return;}b.sibling.return=b.return;b=b.sibling;}return null}function ih(a,b){return {responder:a,props:b}} +var jh=Wa.ReactCurrentDispatcher,kh=Wa.ReactCurrentBatchConfig,lh=0,N=null,O=null,P=null,mh=!1;function Q(){throw Error(u(321));}function nh(a,b){if(null===b)return !1;for(var c=0;cf))throw Error(u(301));f+=1;P=O=null;b.updateQueue=null;jh.current=rh;a=c(d,e);}while(b.expirationTime===lh)}jh.current=sh;b=null!==O&&null!==O.next;lh=0;P=O=N=null;mh=!1;if(b)throw Error(u(300));return a} +function th(){var a={memoizedState:null,baseState:null,baseQueue:null,queue:null,next:null};null===P?N.memoizedState=P=a:P=P.next=a;return P}function uh(){if(null===O){var a=N.alternate;a=null!==a?a.memoizedState:null;}else a=O.next;var b=null===P?N.memoizedState:P.next;if(null!==b)P=b,O=a;else {if(null===a)throw Error(u(310));O=a;a={memoizedState:O.memoizedState,baseState:O.baseState,baseQueue:O.baseQueue,queue:O.queue,next:null};null===P?N.memoizedState=P=a:P=P.next=a;}return P} +function vh(a,b){return "function"===typeof b?b(a):b} +function wh(a){var b=uh(),c=b.queue;if(null===c)throw Error(u(311));c.lastRenderedReducer=a;var d=O,e=d.baseQueue,f=c.pending;if(null!==f){if(null!==e){var g=e.next;e.next=f.next;f.next=g;}d.baseQueue=e=f;c.pending=null;}if(null!==e){e=e.next;d=d.baseState;var h=g=f=null,k=e;do{var l=k.expirationTime;if(lN.expirationTime&& +(N.expirationTime=l,Bg(l));}else null!==h&&(h=h.next={expirationTime:1073741823,suspenseConfig:k.suspenseConfig,action:k.action,eagerReducer:k.eagerReducer,eagerState:k.eagerState,next:null}),Ag(l,k.suspenseConfig),d=k.eagerReducer===a?k.eagerState:a(d,k.action);k=k.next;}while(null!==k&&k!==e);null===h?f=d:h.next=g;$e(d,b.memoizedState)||(rg=!0);b.memoizedState=d;b.baseState=f;b.baseQueue=h;c.lastRenderedState=d;}return [b.memoizedState,c.dispatch]} +function xh(a){var b=uh(),c=b.queue;if(null===c)throw Error(u(311));c.lastRenderedReducer=a;var d=c.dispatch,e=c.pending,f=b.memoizedState;if(null!==e){c.pending=null;var g=e=e.next;do f=a(f,g.action),g=g.next;while(g!==e);$e(f,b.memoizedState)||(rg=!0);b.memoizedState=f;null===b.baseQueue&&(b.baseState=f);c.lastRenderedState=f;}return [f,d]} +function yh(a){var b=th();"function"===typeof a&&(a=a());b.memoizedState=b.baseState=a;a=b.queue={pending:null,dispatch:null,lastRenderedReducer:vh,lastRenderedState:a};a=a.dispatch=zh.bind(null,N,a);return [b.memoizedState,a]}function Ah(a,b,c,d){a={tag:a,create:b,destroy:c,deps:d,next:null};b=N.updateQueue;null===b?(b={lastEffect:null},N.updateQueue=b,b.lastEffect=a.next=a):(c=b.lastEffect,null===c?b.lastEffect=a.next=a:(d=c.next,c.next=a,a.next=d,b.lastEffect=a));return a} +function Bh(){return uh().memoizedState}function Ch(a,b,c,d){var e=th();N.effectTag|=a;e.memoizedState=Ah(1|b,c,void 0,void 0===d?null:d);}function Dh(a,b,c,d){var e=uh();d=void 0===d?null:d;var f=void 0;if(null!==O){var g=O.memoizedState;f=g.destroy;if(null!==d&&nh(d,g.deps)){Ah(b,c,f,d);return}}N.effectTag|=a;e.memoizedState=Ah(1|b,c,f,d);}function Eh(a,b){return Ch(516,4,a,b)}function Fh(a,b){return Dh(516,4,a,b)}function Gh(a,b){return Dh(4,2,a,b)} +function Hh(a,b){if("function"===typeof b)return a=a(),b(a),function(){b(null);};if(null!==b&&void 0!==b)return a=a(),b.current=a,function(){b.current=null;}}function Ih(a,b,c){c=null!==c&&void 0!==c?c.concat([a]):null;return Dh(4,2,Hh.bind(null,b,a),c)}function Jh(){}function Kh(a,b){th().memoizedState=[a,void 0===b?null:b];return a}function Lh(a,b){var c=uh();b=void 0===b?null:b;var d=c.memoizedState;if(null!==d&&null!==b&&nh(b,d[1]))return d[0];c.memoizedState=[a,b];return a} +function Mh(a,b){var c=uh();b=void 0===b?null:b;var d=c.memoizedState;if(null!==d&&null!==b&&nh(b,d[1]))return d[0];a=a();c.memoizedState=[a,b];return a}function Nh(a,b,c){var d=ag();cg(98>d?98:d,function(){a(!0);});cg(97\x3c/script>",a=a.removeChild(a.firstChild)):"string"===typeof d.is?a=g.createElement(e,{is:d.is}):(a=g.createElement(e),"select"===e&&(g=a,d.multiple?g.multiple=!0:d.size&&(g.size=d.size))):a=g.createElementNS(a,e);a[Md]=b;a[Nd]=d;ni(a,b,!1,!1);b.stateNode=a;g=pd(e,d);switch(e){case "iframe":case "object":case "embed":F("load", +a);h=d;break;case "video":case "audio":for(h=0;hd.tailExpiration&&1b)&&tj.set(a,b)));}} +function xj(a,b){a.expirationTimea?c:a;return 2>=a&&b!==a?0:a} +function Z(a){if(0!==a.lastExpiredTime)a.callbackExpirationTime=1073741823,a.callbackPriority=99,a.callbackNode=eg(yj.bind(null,a));else {var b=zj(a),c=a.callbackNode;if(0===b)null!==c&&(a.callbackNode=null,a.callbackExpirationTime=0,a.callbackPriority=90);else {var d=Gg();1073741823===b?d=99:1===b||2===b?d=95:(d=10*(1073741821-b)-10*(1073741821-d),d=0>=d?99:250>=d?98:5250>=d?97:95);if(null!==c){var e=a.callbackPriority;if(a.callbackExpirationTime===b&&e>=d)return;c!==Tf&&Kf(c);}a.callbackExpirationTime= +b;a.callbackPriority=d;b=1073741823===b?eg(yj.bind(null,a)):dg(d,Bj.bind(null,a),{timeout:10*(1073741821-b)-$f()});a.callbackNode=b;}}} +function Bj(a,b){wj=0;if(b)return b=Gg(),Cj(a,b),Z(a),null;var c=zj(a);if(0!==c){b=a.callbackNode;if((W&(fj|gj))!==V)throw Error(u(327));Dj();a===T&&c===U||Ej(a,c);if(null!==X){var d=W;W|=fj;var e=Fj();do try{Gj();break}catch(h){Hj(a,h);}while(1);ng();W=d;cj.current=e;if(S===hj)throw b=kj,Ej(a,c),xi(a,c),Z(a),b;if(null===X)switch(e=a.finishedWork=a.current.alternate,a.finishedExpirationTime=c,d=S,T=null,d){case ti:case hj:throw Error(u(345));case ij:Cj(a,2=c){a.lastPingedTime=c;Ej(a,c);break}}f=zj(a);if(0!==f&&f!==c)break;if(0!==d&&d!==c){a.lastPingedTime=d;break}a.timeoutHandle=Hd(Jj.bind(null,a),e);break}Jj(a);break;case vi:xi(a,c);d=a.lastSuspendedTime;c===d&&(a.nextKnownPendingLevel=Ij(e));if(oj&&(e=a.lastPingedTime,0===e||e>=c)){a.lastPingedTime=c;Ej(a,c);break}e=zj(a);if(0!==e&&e!==c)break;if(0!==d&&d!==c){a.lastPingedTime= +d;break}1073741823!==mj?d=10*(1073741821-mj)-$f():1073741823===lj?d=0:(d=10*(1073741821-lj)-5E3,e=$f(),c=10*(1073741821-c)-e,d=e-d,0>d&&(d=0),d=(120>d?120:480>d?480:1080>d?1080:1920>d?1920:3E3>d?3E3:4320>d?4320:1960*bj(d/1960))-d,c=d?d=0:(e=g.busyDelayMs|0,f=$f()-(10*(1073741821-f)-(g.timeoutMs|0||5E3)),d=f<=e?0:e+d-f);if(10 component higher in the tree to provide a loading indicator or placeholder to display."+qb(g));}S!== +jj&&(S=ij);h=Ai(h,g);p=f;do{switch(p.tag){case 3:k=h;p.effectTag|=4096;p.expirationTime=b;var B=Xi(p,k,b);yg(p,B);break a;case 1:k=h;var w=p.type,ub=p.stateNode;if(0===(p.effectTag&64)&&("function"===typeof w.getDerivedStateFromError||null!==ub&&"function"===typeof ub.componentDidCatch&&(null===aj||!aj.has(ub)))){p.effectTag|=4096;p.expirationTime=b;var vb=$i(p,k,b);yg(p,vb);break a}}p=p.return;}while(null!==p)}X=Pj(X);}catch(Xc){b=Xc;continue}break}while(1)} +function Fj(){var a=cj.current;cj.current=sh;return null===a?sh:a}function Ag(a,b){awi&&(wi=a);}function Kj(){for(;null!==X;)X=Qj(X);}function Gj(){for(;null!==X&&!Uf();)X=Qj(X);}function Qj(a){var b=Rj(a.alternate,a,U);a.memoizedProps=a.pendingProps;null===b&&(b=Pj(a));dj.current=null;return b} +function Pj(a){X=a;do{var b=X.alternate;a=X.return;if(0===(X.effectTag&2048)){b=si(b,X,U);if(1===U||1!==X.childExpirationTime){for(var c=0,d=X.child;null!==d;){var e=d.expirationTime,f=d.childExpirationTime;e>c&&(c=e);f>c&&(c=f);d=d.sibling;}X.childExpirationTime=c;}if(null!==b)return b;null!==a&&0===(a.effectTag&2048)&&(null===a.firstEffect&&(a.firstEffect=X.firstEffect),null!==X.lastEffect&&(null!==a.lastEffect&&(a.lastEffect.nextEffect=X.firstEffect),a.lastEffect=X.lastEffect),1a?b:a}function Jj(a){var b=ag();cg(99,Sj.bind(null,a,b));return null} +function Sj(a,b){do Dj();while(null!==rj);if((W&(fj|gj))!==V)throw Error(u(327));var c=a.finishedWork,d=a.finishedExpirationTime;if(null===c)return null;a.finishedWork=null;a.finishedExpirationTime=0;if(c===a.current)throw Error(u(177));a.callbackNode=null;a.callbackExpirationTime=0;a.callbackPriority=90;a.nextKnownPendingLevel=0;var e=Ij(c);a.firstPendingTime=e;d<=a.lastSuspendedTime?a.firstSuspendedTime=a.lastSuspendedTime=a.nextKnownPendingLevel=0:d<=a.firstSuspendedTime&&(a.firstSuspendedTime= +d-1);d<=a.lastPingedTime&&(a.lastPingedTime=0);d<=a.lastExpiredTime&&(a.lastExpiredTime=0);a===T&&(X=T=null,U=0);1h&&(l=h,h=g,g=l),l=vd(q,g),m=vd(q,h),l&&m&&(1!==w.rangeCount||w.anchorNode!==l.node||w.anchorOffset!==l.offset||w.focusNode!==m.node||w.focusOffset!==m.offset)&&(B=B.createRange(),B.setStart(l.node,l.offset),w.removeAllRanges(),g>h?(w.addRange(B),w.extend(m.node,m.offset)):(B.setEnd(m.node,m.offset),w.addRange(B))))));B=[];for(w=q;w=w.parentNode;)1===w.nodeType&&B.push({element:w,left:w.scrollLeft, +top:w.scrollTop});"function"===typeof q.focus&&q.focus();for(q=0;q=c)return ji(a,b,c);I(M,M.current&1);b=$h(a,b,c);return null!==b?b.sibling:null}I(M,M.current&1);break;case 19:d=b.childExpirationTime>=c;if(0!==(a.effectTag&64)){if(d)return mi(a,b,c);b.effectTag|=64;}e=b.memoizedState;null!==e&&(e.rendering=null,e.tail=null);I(M,M.current);if(!d)return null}return $h(a,b,c)}rg=!1;}}else rg=!1;b.expirationTime=0;switch(b.tag){case 2:d=b.type;null!==a&&(a.alternate=null,b.alternate=null,b.effectTag|=2);a=b.pendingProps;e=Cf(b,J.current);qg(b,c);e=oh(null, +b,d,a,e,c);b.effectTag|=1;if("object"===typeof e&&null!==e&&"function"===typeof e.render&&void 0===e.$$typeof){b.tag=1;b.memoizedState=null;b.updateQueue=null;if(L(d)){var f=!0;Gf(b);}else f=!1;b.memoizedState=null!==e.state&&void 0!==e.state?e.state:null;ug(b);var g=d.getDerivedStateFromProps;"function"===typeof g&&Fg(b,d,g,a);e.updater=Jg;b.stateNode=e;e._reactInternalFiber=b;Ng(b,d,a,c);b=gi(null,b,d,!0,f,c);}else b.tag=0,R(null,b,e,c),b=b.child;return b;case 16:a:{e=b.elementType;null!==a&&(a.alternate= +null,b.alternate=null,b.effectTag|=2);a=b.pendingProps;ob(e);if(1!==e._status)throw e._result;e=e._result;b.type=e;f=b.tag=Xj(e);a=ig(e,a);switch(f){case 0:b=di(null,b,e,a,c);break a;case 1:b=fi(null,b,e,a,c);break a;case 11:b=Zh(null,b,e,a,c);break a;case 14:b=ai(null,b,e,ig(e.type,a),d,c);break a}throw Error(u(306,e,""));}return b;case 0:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:ig(d,e),di(a,b,d,e,c);case 1:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:ig(d,e),fi(a,b,d,e,c); +case 3:hi(b);d=b.updateQueue;if(null===a||null===d)throw Error(u(282));d=b.pendingProps;e=b.memoizedState;e=null!==e?e.element:null;vg(a,b);zg(b,d,null,c);d=b.memoizedState.element;if(d===e)Xh(),b=$h(a,b,c);else {if(e=b.stateNode.hydrate)Ph=Jd(b.stateNode.containerInfo.firstChild),Oh=b,e=Qh=!0;if(e)for(c=Yg(b,null,d,c),b.child=c;c;)c.effectTag=c.effectTag&-3|1024,c=c.sibling;else R(a,b,d,c),Xh();b=b.child;}return b;case 5:return fh(b),null===a&&Uh(b),d=b.type,e=b.pendingProps,f=null!==a?a.memoizedProps: +null,g=e.children,Gd(d,e)?g=null:null!==f&&Gd(d,f)&&(b.effectTag|=16),ei(a,b),b.mode&4&&1!==c&&e.hidden?(b.expirationTime=b.childExpirationTime=1,b=null):(R(a,b,g,c),b=b.child),b;case 6:return null===a&&Uh(b),null;case 13:return ji(a,b,c);case 4:return dh(b,b.stateNode.containerInfo),d=b.pendingProps,null===a?b.child=Xg(b,null,d,c):R(a,b,d,c),b.child;case 11:return d=b.type,e=b.pendingProps,e=b.elementType===d?e:ig(d,e),Zh(a,b,d,e,c);case 7:return R(a,b,b.pendingProps,c),b.child;case 8:return R(a, +b,b.pendingProps.children,c),b.child;case 12:return R(a,b,b.pendingProps.children,c),b.child;case 10:a:{d=b.type._context;e=b.pendingProps;g=b.memoizedProps;f=e.value;var h=b.type._context;I(jg,h._currentValue);h._currentValue=f;if(null!==g)if(h=g.value,f=$e(h,f)?0:("function"===typeof d._calculateChangedBits?d._calculateChangedBits(h,f):1073741823)|0,0===f){if(g.children===e.children&&!K.current){b=$h(a,b,c);break a}}else for(h=b.child,null!==h&&(h.return=b);null!==h;){var k=h.dependencies;if(null!== +k){g=h.child;for(var l=k.firstContext;null!==l;){if(l.context===d&&0!==(l.observedBits&f)){1===h.tag&&(l=wg(c,null),l.tag=2,xg(h,l));h.expirationTime=b&&a<=b}function xi(a,b){var c=a.firstSuspendedTime,d=a.lastSuspendedTime;cb||0===c)a.lastSuspendedTime=b;b<=a.lastPingedTime&&(a.lastPingedTime=0);b<=a.lastExpiredTime&&(a.lastExpiredTime=0);} +function yi(a,b){b>a.firstPendingTime&&(a.firstPendingTime=b);var c=a.firstSuspendedTime;0!==c&&(b>=c?a.firstSuspendedTime=a.lastSuspendedTime=a.nextKnownPendingLevel=0:b>=a.lastSuspendedTime&&(a.lastSuspendedTime=b+1),b>a.nextKnownPendingLevel&&(a.nextKnownPendingLevel=b));}function Cj(a,b){var c=a.lastExpiredTime;if(0===c||c>b)a.lastExpiredTime=b;} +function bk(a,b,c,d){var e=b.current,f=Gg(),g=Dg.suspense;f=Hg(f,e,g);a:if(c){c=c._reactInternalFiber;b:{if(dc(c)!==c||1!==c.tag)throw Error(u(170));var h=c;do{switch(h.tag){case 3:h=h.stateNode.context;break b;case 1:if(L(h.type)){h=h.stateNode.__reactInternalMemoizedMergedChildContext;break b}}h=h.return;}while(null!==h);throw Error(u(171));}if(1===c.tag){var k=c.type;if(L(k)){c=Ff(c,k,h);break a}}c=h;}else c=Af;null===b.context?b.context=c:b.pendingContext=c;b=wg(f,g);b.payload={element:a};d=void 0=== +d?null:d;null!==d&&(b.callback=d);xg(e,b);Ig(e,f);return f}function ck(a){a=a.current;if(!a.child)return null;switch(a.child.tag){case 5:return a.child.stateNode;default:return a.child.stateNode}}function dk(a,b){a=a.memoizedState;null!==a&&null!==a.dehydrated&&a.retryTime=5&&((e||!n&&5===r)&&(h.push(r,0,e,s),r=6),n&&(h.push(r,n,0,s),r=6)),e="";},a=0;a"===t?(r=1,e=""):e=t+e[0]:u?t===u?u="":e+=t:'"'===t||"'"===t?u=t:">"===t?(p(),r=1):r&&("="===t?(r=5,s=e,e=""):"/"===t&&(r<5||">"===n[a][l+1])?(p(),3===r&&(h=h[0]),r=h,(h=h[0]).push(2,0,r),r=0):" "===t||"\t"===t||"\n"===t||"\r"===t?(p(),r=2):e+=t),3===r&&"!--"===e&&(r=4,h=h[0]);}return p(),h}(s)),r),arguments,[])).length>1?r:r[0]} + +function serializeEvent(event) { + const data = {}; + + if (event.type in eventTransforms) { + Object.assign(data, eventTransforms[event.type](event)); + } + + const target = event.target; + if (target.tagName in targetTransforms) { + targetTransforms[target.tagName].forEach((trans) => + Object.assign(data, trans(target)) + ); + } + + return data; +} + +const targetTransformCategories = { + hasValue: (target) => ({ + value: target.value, + }), + hasCurrentTime: (target) => ({ + currentTime: target.currentTime, + }), + hasFiles: (target) => { + if (target?.type == "file") { + return { + files: Array.from(target.files).map((file) => ({ + lastModified: file.lastModified, + name: file.name, + size: file.size, + type: file.type, + })), + }; + } else { + return {}; + } + }, +}; + +const targetTagCategories = { + hasValue: ["BUTTON", "INPUT", "OPTION", "LI", "METER", "PROGRESS", "PARAM"], + hasCurrentTime: ["AUDIO", "VIDEO"], + hasFiles: ["INPUT"], +}; + +const targetTransforms = {}; + +Object.keys(targetTagCategories).forEach((category) => { + targetTagCategories[category].forEach((type) => { + const transforms = targetTransforms[type] || (targetTransforms[type] = []); + transforms.push(targetTransformCategories[category]); + }); +}); + +const eventTransformCategories = { + clipboard: (event) => ({ + clipboardData: event.clipboardData, + }), + composition: (event) => ({ + data: event.data, + }), + keyboard: (event) => ({ + altKey: event.altKey, + charCode: event.charCode, + ctrlKey: event.ctrlKey, + key: event.key, + keyCode: event.keyCode, + locale: event.locale, + location: event.location, + metaKey: event.metaKey, + repeat: event.repeat, + shiftKey: event.shiftKey, + which: event.which, + }), + mouse: (event) => ({ + altKey: event.altKey, + button: event.button, + buttons: event.buttons, + clientX: event.clientX, + clientY: event.clientY, + ctrlKey: event.ctrlKey, + metaKey: event.metaKey, + pageX: event.pageX, + pageY: event.pageY, + screenX: event.screenX, + screenY: event.screenY, + shiftKey: event.shiftKey, + }), + pointer: (event) => ({ + pointerId: event.pointerId, + width: event.width, + height: event.height, + pressure: event.pressure, + tiltX: event.tiltX, + tiltY: event.tiltY, + pointerType: event.pointerType, + isPrimary: event.isPrimary, + }), + selection: () => { + return { selectedText: window.getSelection().toString() }; + }, + touch: (event) => ({ + altKey: event.altKey, + ctrlKey: event.ctrlKey, + metaKey: event.metaKey, + shiftKey: event.shiftKey, + }), + ui: (event) => ({ + detail: event.detail, + }), + wheel: (event) => ({ + deltaMode: event.deltaMode, + deltaX: event.deltaX, + deltaY: event.deltaY, + deltaZ: event.deltaZ, + }), + animation: (event) => ({ + animationName: event.animationName, + pseudoElement: event.pseudoElement, + elapsedTime: event.elapsedTime, + }), + transition: (event) => ({ + propertyName: event.propertyName, + pseudoElement: event.pseudoElement, + elapsedTime: event.elapsedTime, + }), +}; + +const eventTypeCategories = { + clipboard: ["copy", "cut", "paste"], + composition: ["compositionend", "compositionstart", "compositionupdate"], + keyboard: ["keydown", "keypress", "keyup"], + mouse: [ + "click", + "contextmenu", + "doubleclick", + "drag", + "dragend", + "dragenter", + "dragexit", + "dragleave", + "dragover", + "dragstart", + "drop", + "mousedown", + "mouseenter", + "mouseleave", + "mousemove", + "mouseout", + "mouseover", + "mouseup", + ], + pointer: [ + "pointerdown", + "pointermove", + "pointerup", + "pointercancel", + "gotpointercapture", + "lostpointercapture", + "pointerenter", + "pointerleave", + "pointerover", + "pointerout", + ], + selection: ["select"], + touch: ["touchcancel", "touchend", "touchmove", "touchstart"], + ui: ["scroll"], + wheel: ["wheel"], + animation: ["animationstart", "animationend", "animationiteration"], + transition: ["transitionend"], +}; + +const eventTransforms = {}; + +Object.keys(eventTypeCategories).forEach((category) => { + eventTypeCategories[category].forEach((type) => { + eventTransforms[type] = eventTransformCategories[category]; + }); +}); + +/*! + * https://github.com/Starcounter-Jack/JSON-Patch + * (c) 2017 Joachim Wester + * MIT license + */ +var __extends = (undefined && undefined.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var _hasOwnProperty = Object.prototype.hasOwnProperty; +function hasOwnProperty(obj, key) { + return _hasOwnProperty.call(obj, key); +} +function _objectKeys(obj) { + if (Array.isArray(obj)) { + var keys = new Array(obj.length); + for (var k = 0; k < keys.length; k++) { + keys[k] = "" + k; + } + return keys; + } + if (Object.keys) { + return Object.keys(obj); + } + var keys = []; + for (var i in obj) { + if (hasOwnProperty(obj, i)) { + keys.push(i); + } + } + return keys; +} +/** +* Deeply clone the object. +* https://jsperf.com/deep-copy-vs-json-stringify-json-parse/25 (recursiveDeepCopy) +* @param {any} obj value to clone +* @return {any} cloned obj +*/ +function _deepClone(obj) { + switch (typeof obj) { + case "object": + return JSON.parse(JSON.stringify(obj)); //Faster than ES5 clone - http://jsperf.com/deep-cloning-of-objects/5 + case "undefined": + return null; //this is how JSON.stringify behaves for array items + default: + return obj; //no need to clone primitives + } +} +//3x faster than cached /^\d+$/.test(str) +function isInteger(str) { + var i = 0; + var len = str.length; + var charCode; + while (i < len) { + charCode = str.charCodeAt(i); + if (charCode >= 48 && charCode <= 57) { + i++; + continue; + } + return false; + } + return true; +} +/** +* Escapes a json pointer path +* @param path The raw pointer +* @return the Escaped path +*/ +function escapePathComponent(path) { + if (path.indexOf('/') === -1 && path.indexOf('~') === -1) + return path; + return path.replace(/~/g, '~0').replace(/\//g, '~1'); +} +/** + * Unescapes a json pointer path + * @param path The escaped pointer + * @return The unescaped path + */ +function unescapePathComponent(path) { + return path.replace(/~1/g, '/').replace(/~0/g, '~'); +} +/** +* Recursively checks whether an object has any undefined values inside. +*/ +function hasUndefined(obj) { + if (obj === undefined) { + return true; + } + if (obj) { + if (Array.isArray(obj)) { + for (var i = 0, len = obj.length; i < len; i++) { + if (hasUndefined(obj[i])) { + return true; + } + } + } + else if (typeof obj === "object") { + var objKeys = _objectKeys(obj); + var objKeysLength = objKeys.length; + for (var i = 0; i < objKeysLength; i++) { + if (hasUndefined(obj[objKeys[i]])) { + return true; + } + } + } + } + return false; +} +function patchErrorMessageFormatter(message, args) { + var messageParts = [message]; + for (var key in args) { + var value = typeof args[key] === 'object' ? JSON.stringify(args[key], null, 2) : args[key]; // pretty print + if (typeof value !== 'undefined') { + messageParts.push(key + ": " + value); + } + } + return messageParts.join('\n'); +} +var PatchError = /** @class */ (function (_super) { + __extends(PatchError, _super); + function PatchError(message, name, index, operation, tree) { + var _newTarget = this.constructor; + var _this = _super.call(this, patchErrorMessageFormatter(message, { name: name, index: index, operation: operation, tree: tree })) || this; + _this.name = name; + _this.index = index; + _this.operation = operation; + _this.tree = tree; + Object.setPrototypeOf(_this, _newTarget.prototype); // restore prototype chain, see https://stackoverflow.com/a/48342359 + _this.message = patchErrorMessageFormatter(message, { name: name, index: index, operation: operation, tree: tree }); + return _this; + } + return PatchError; +}(Error)); + +var JsonPatchError = PatchError; +var deepClone = _deepClone; +/* We use a Javascript hash to store each + function. Each hash entry (property) uses + the operation identifiers specified in rfc6902. + In this way, we can map each patch operation + to its dedicated function in efficient way. + */ +/* The operations applicable to an object */ +var objOps = { + add: function (obj, key, document) { + obj[key] = this.value; + return { newDocument: document }; + }, + remove: function (obj, key, document) { + var removed = obj[key]; + delete obj[key]; + return { newDocument: document, removed: removed }; + }, + replace: function (obj, key, document) { + var removed = obj[key]; + obj[key] = this.value; + return { newDocument: document, removed: removed }; + }, + move: function (obj, key, document) { + /* in case move target overwrites an existing value, + return the removed value, this can be taxing performance-wise, + and is potentially unneeded */ + var removed = getValueByPointer(document, this.path); + if (removed) { + removed = _deepClone(removed); + } + var originalValue = applyOperation(document, { op: "remove", path: this.from }).removed; + applyOperation(document, { op: "add", path: this.path, value: originalValue }); + return { newDocument: document, removed: removed }; + }, + copy: function (obj, key, document) { + var valueToCopy = getValueByPointer(document, this.from); + // enforce copy by value so further operations don't affect source (see issue #177) + applyOperation(document, { op: "add", path: this.path, value: _deepClone(valueToCopy) }); + return { newDocument: document }; + }, + test: function (obj, key, document) { + return { newDocument: document, test: _areEquals(obj[key], this.value) }; + }, + _get: function (obj, key, document) { + this.value = obj[key]; + return { newDocument: document }; + } +}; +/* The operations applicable to an array. Many are the same as for the object */ +var arrOps = { + add: function (arr, i, document) { + if (isInteger(i)) { + arr.splice(i, 0, this.value); + } + else { // array props + arr[i] = this.value; + } + // this may be needed when using '-' in an array + return { newDocument: document, index: i }; + }, + remove: function (arr, i, document) { + var removedList = arr.splice(i, 1); + return { newDocument: document, removed: removedList[0] }; + }, + replace: function (arr, i, document) { + var removed = arr[i]; + arr[i] = this.value; + return { newDocument: document, removed: removed }; + }, + move: objOps.move, + copy: objOps.copy, + test: objOps.test, + _get: objOps._get +}; +/** + * Retrieves a value from a JSON document by a JSON pointer. + * Returns the value. + * + * @param document The document to get the value from + * @param pointer an escaped JSON pointer + * @return The retrieved value + */ +function getValueByPointer(document, pointer) { + if (pointer == '') { + return document; + } + var getOriginalDestination = { op: "_get", path: pointer }; + applyOperation(document, getOriginalDestination); + return getOriginalDestination.value; +} +/** + * Apply a single JSON Patch Operation on a JSON document. + * Returns the {newDocument, result} of the operation. + * It modifies the `document` and `operation` objects - it gets the values by reference. + * If you would like to avoid touching your values, clone them: + * `jsonpatch.applyOperation(document, jsonpatch._deepClone(operation))`. + * + * @param document The document to patch + * @param operation The operation to apply + * @param validateOperation `false` is without validation, `true` to use default jsonpatch's validation, or you can pass a `validateOperation` callback to be used for validation. + * @param mutateDocument Whether to mutate the original document or clone it before applying + * @param banPrototypeModifications Whether to ban modifications to `__proto__`, defaults to `true`. + * @return `{newDocument, result}` after the operation + */ +function applyOperation(document, operation, validateOperation, mutateDocument, banPrototypeModifications, index) { + if (validateOperation === void 0) { validateOperation = false; } + if (mutateDocument === void 0) { mutateDocument = true; } + if (banPrototypeModifications === void 0) { banPrototypeModifications = true; } + if (index === void 0) { index = 0; } + if (validateOperation) { + if (typeof validateOperation == 'function') { + validateOperation(operation, 0, document, operation.path); + } + else { + validator(operation, 0); + } + } + /* ROOT OPERATIONS */ + if (operation.path === "") { + var returnValue = { newDocument: document }; + if (operation.op === 'add') { + returnValue.newDocument = operation.value; + return returnValue; + } + else if (operation.op === 'replace') { + returnValue.newDocument = operation.value; + returnValue.removed = document; //document we removed + return returnValue; + } + else if (operation.op === 'move' || operation.op === 'copy') { // it's a move or copy to root + returnValue.newDocument = getValueByPointer(document, operation.from); // get the value by json-pointer in `from` field + if (operation.op === 'move') { // report removed item + returnValue.removed = document; + } + return returnValue; + } + else if (operation.op === 'test') { + returnValue.test = _areEquals(document, operation.value); + if (returnValue.test === false) { + throw new JsonPatchError("Test operation failed", 'TEST_OPERATION_FAILED', index, operation, document); + } + returnValue.newDocument = document; + return returnValue; + } + else if (operation.op === 'remove') { // a remove on root + returnValue.removed = document; + returnValue.newDocument = null; + return returnValue; + } + else if (operation.op === '_get') { + operation.value = document; + return returnValue; + } + else { /* bad operation */ + if (validateOperation) { + throw new JsonPatchError('Operation `op` property is not one of operations defined in RFC-6902', 'OPERATION_OP_INVALID', index, operation, document); + } + else { + return returnValue; + } + } + } /* END ROOT OPERATIONS */ + else { + if (!mutateDocument) { + document = _deepClone(document); + } + var path = operation.path || ""; + var keys = path.split('/'); + var obj = document; + var t = 1; //skip empty element - http://jsperf.com/to-shift-or-not-to-shift + var len = keys.length; + var existingPathFragment = undefined; + var key = void 0; + var validateFunction = void 0; + if (typeof validateOperation == 'function') { + validateFunction = validateOperation; + } + else { + validateFunction = validator; + } + while (true) { + key = keys[t]; + if (banPrototypeModifications && key == '__proto__') { + throw new TypeError('JSON-Patch: modifying `__proto__` prop is banned for security reasons, if this was on purpose, please set `banPrototypeModifications` flag false and pass it to this function. More info in fast-json-patch README'); + } + if (validateOperation) { + if (existingPathFragment === undefined) { + if (obj[key] === undefined) { + existingPathFragment = keys.slice(0, t).join('/'); + } + else if (t == len - 1) { + existingPathFragment = operation.path; + } + if (existingPathFragment !== undefined) { + validateFunction(operation, 0, document, existingPathFragment); + } + } + } + t++; + if (Array.isArray(obj)) { + if (key === '-') { + key = obj.length; + } + else { + if (validateOperation && !isInteger(key)) { + throw new JsonPatchError("Expected an unsigned base-10 integer value, making the new referenced value the array element with the zero-based index", "OPERATION_PATH_ILLEGAL_ARRAY_INDEX", index, operation, document); + } // only parse key when it's an integer for `arr.prop` to work + else if (isInteger(key)) { + key = ~~key; + } + } + if (t >= len) { + if (validateOperation && operation.op === "add" && key > obj.length) { + throw new JsonPatchError("The specified index MUST NOT be greater than the number of elements in the array", "OPERATION_VALUE_OUT_OF_BOUNDS", index, operation, document); + } + var returnValue = arrOps[operation.op].call(operation, obj, key, document); // Apply patch + if (returnValue.test === false) { + throw new JsonPatchError("Test operation failed", 'TEST_OPERATION_FAILED', index, operation, document); + } + return returnValue; + } + } + else { + if (key && key.indexOf('~') != -1) { + key = unescapePathComponent(key); + } + if (t >= len) { + var returnValue = objOps[operation.op].call(operation, obj, key, document); // Apply patch + if (returnValue.test === false) { + throw new JsonPatchError("Test operation failed", 'TEST_OPERATION_FAILED', index, operation, document); + } + return returnValue; + } + } + obj = obj[key]; + } + } +} +/** + * Apply a full JSON Patch array on a JSON document. + * Returns the {newDocument, result} of the patch. + * It modifies the `document` object and `patch` - it gets the values by reference. + * If you would like to avoid touching your values, clone them: + * `jsonpatch.applyPatch(document, jsonpatch._deepClone(patch))`. + * + * @param document The document to patch + * @param patch The patch to apply + * @param validateOperation `false` is without validation, `true` to use default jsonpatch's validation, or you can pass a `validateOperation` callback to be used for validation. + * @param mutateDocument Whether to mutate the original document or clone it before applying + * @param banPrototypeModifications Whether to ban modifications to `__proto__`, defaults to `true`. + * @return An array of `{newDocument, result}` after the patch + */ +function applyPatch(document, patch, validateOperation, mutateDocument, banPrototypeModifications) { + if (mutateDocument === void 0) { mutateDocument = true; } + if (banPrototypeModifications === void 0) { banPrototypeModifications = true; } + if (validateOperation) { + if (!Array.isArray(patch)) { + throw new JsonPatchError('Patch sequence must be an array', 'SEQUENCE_NOT_AN_ARRAY'); + } + } + if (!mutateDocument) { + document = _deepClone(document); + } + var results = new Array(patch.length); + for (var i = 0, length_1 = patch.length; i < length_1; i++) { + // we don't need to pass mutateDocument argument because if it was true, we already deep cloned the object, we'll just pass `true` + results[i] = applyOperation(document, patch[i], validateOperation, true, banPrototypeModifications, i); + document = results[i].newDocument; // in case root was replaced + } + results.newDocument = document; + return results; +} +/** + * Apply a single JSON Patch Operation on a JSON document. + * Returns the updated document. + * Suitable as a reducer. + * + * @param document The document to patch + * @param operation The operation to apply + * @return The updated document + */ +function applyReducer(document, operation, index) { + var operationResult = applyOperation(document, operation); + if (operationResult.test === false) { // failed test + throw new JsonPatchError("Test operation failed", 'TEST_OPERATION_FAILED', index, operation, document); + } + return operationResult.newDocument; +} +/** + * Validates a single operation. Called from `jsonpatch.validate`. Throws `JsonPatchError` in case of an error. + * @param {object} operation - operation object (patch) + * @param {number} index - index of operation in the sequence + * @param {object} [document] - object where the operation is supposed to be applied + * @param {string} [existingPathFragment] - comes along with `document` + */ +function validator(operation, index, document, existingPathFragment) { + if (typeof operation !== 'object' || operation === null || Array.isArray(operation)) { + throw new JsonPatchError('Operation is not an object', 'OPERATION_NOT_AN_OBJECT', index, operation, document); + } + else if (!objOps[operation.op]) { + throw new JsonPatchError('Operation `op` property is not one of operations defined in RFC-6902', 'OPERATION_OP_INVALID', index, operation, document); + } + else if (typeof operation.path !== 'string') { + throw new JsonPatchError('Operation `path` property is not a string', 'OPERATION_PATH_INVALID', index, operation, document); + } + else if (operation.path.indexOf('/') !== 0 && operation.path.length > 0) { + // paths that aren't empty string should start with "/" + throw new JsonPatchError('Operation `path` property must start with "/"', 'OPERATION_PATH_INVALID', index, operation, document); + } + else if ((operation.op === 'move' || operation.op === 'copy') && typeof operation.from !== 'string') { + throw new JsonPatchError('Operation `from` property is not present (applicable in `move` and `copy` operations)', 'OPERATION_FROM_REQUIRED', index, operation, document); + } + else if ((operation.op === 'add' || operation.op === 'replace' || operation.op === 'test') && operation.value === undefined) { + throw new JsonPatchError('Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)', 'OPERATION_VALUE_REQUIRED', index, operation, document); + } + else if ((operation.op === 'add' || operation.op === 'replace' || operation.op === 'test') && hasUndefined(operation.value)) { + throw new JsonPatchError('Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)', 'OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED', index, operation, document); + } + else if (document) { + if (operation.op == "add") { + var pathLen = operation.path.split("/").length; + var existingPathLen = existingPathFragment.split("/").length; + if (pathLen !== existingPathLen + 1 && pathLen !== existingPathLen) { + throw new JsonPatchError('Cannot perform an `add` operation at the desired path', 'OPERATION_PATH_CANNOT_ADD', index, operation, document); + } + } + else if (operation.op === 'replace' || operation.op === 'remove' || operation.op === '_get') { + if (operation.path !== existingPathFragment) { + throw new JsonPatchError('Cannot perform the operation at a path that does not exist', 'OPERATION_PATH_UNRESOLVABLE', index, operation, document); + } + } + else if (operation.op === 'move' || operation.op === 'copy') { + var existingValue = { op: "_get", path: operation.from, value: undefined }; + var error = validate([existingValue], document); + if (error && error.name === 'OPERATION_PATH_UNRESOLVABLE') { + throw new JsonPatchError('Cannot perform the operation from a path that does not exist', 'OPERATION_FROM_UNRESOLVABLE', index, operation, document); + } + } + } +} +/** + * Validates a sequence of operations. If `document` parameter is provided, the sequence is additionally validated against the object document. + * If error is encountered, returns a JsonPatchError object + * @param sequence + * @param document + * @returns {JsonPatchError|undefined} + */ +function validate(sequence, document, externalValidator) { + try { + if (!Array.isArray(sequence)) { + throw new JsonPatchError('Patch sequence must be an array', 'SEQUENCE_NOT_AN_ARRAY'); + } + if (document) { + //clone document and sequence so that we can safely try applying operations + applyPatch(_deepClone(document), _deepClone(sequence), externalValidator || true); + } + else { + externalValidator = externalValidator || validator; + for (var i = 0; i < sequence.length; i++) { + externalValidator(sequence[i], i, document, undefined); + } + } + } + catch (e) { + if (e instanceof JsonPatchError) { + return e; + } + else { + throw e; + } + } +} +// based on https://github.com/epoberezkin/fast-deep-equal +// MIT License +// Copyright (c) 2017 Evgeny Poberezkin +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +function _areEquals(a, b) { + if (a === b) + return true; + if (a && b && typeof a == 'object' && typeof b == 'object') { + var arrA = Array.isArray(a), arrB = Array.isArray(b), i, length, key; + if (arrA && arrB) { + length = a.length; + if (length != b.length) + return false; + for (i = length; i-- !== 0;) + if (!_areEquals(a[i], b[i])) + return false; + return true; + } + if (arrA != arrB) + return false; + var keys = Object.keys(a); + length = keys.length; + if (length !== Object.keys(b).length) + return false; + for (i = length; i-- !== 0;) + if (!b.hasOwnProperty(keys[i])) + return false; + for (i = length; i-- !== 0;) { + key = keys[i]; + if (!_areEquals(a[key], b[key])) + return false; + } + return true; + } + return a !== a && b !== b; +} + +var core = /*#__PURE__*/Object.freeze({ + __proto__: null, + JsonPatchError: JsonPatchError, + deepClone: deepClone, + getValueByPointer: getValueByPointer, + applyOperation: applyOperation, + applyPatch: applyPatch, + applyReducer: applyReducer, + validator: validator, + validate: validate, + _areEquals: _areEquals +}); + +/*! + * https://github.com/Starcounter-Jack/JSON-Patch + * (c) 2017 Joachim Wester + * MIT license + */ +var beforeDict = new WeakMap(); +var Mirror = /** @class */ (function () { + function Mirror(obj) { + this.observers = new Map(); + this.obj = obj; + } + return Mirror; +}()); +var ObserverInfo = /** @class */ (function () { + function ObserverInfo(callback, observer) { + this.callback = callback; + this.observer = observer; + } + return ObserverInfo; +}()); +function getMirror(obj) { + return beforeDict.get(obj); +} +function getObserverFromMirror(mirror, callback) { + return mirror.observers.get(callback); +} +function removeObserverFromMirror(mirror, observer) { + mirror.observers.delete(observer.callback); +} +/** + * Detach an observer from an object + */ +function unobserve(root, observer) { + observer.unobserve(); +} +/** + * Observes changes made to an object, which can then be retrieved using generate + */ +function observe(obj, callback) { + var patches = []; + var observer; + var mirror = getMirror(obj); + if (!mirror) { + mirror = new Mirror(obj); + beforeDict.set(obj, mirror); + } + else { + var observerInfo = getObserverFromMirror(mirror, callback); + observer = observerInfo && observerInfo.observer; + } + if (observer) { + return observer; + } + observer = {}; + mirror.value = _deepClone(obj); + if (callback) { + observer.callback = callback; + observer.next = null; + var dirtyCheck = function () { + generate(observer); + }; + var fastCheck = function () { + clearTimeout(observer.next); + observer.next = setTimeout(dirtyCheck); + }; + if (typeof window !== 'undefined') { //not Node + window.addEventListener('mouseup', fastCheck); + window.addEventListener('keyup', fastCheck); + window.addEventListener('mousedown', fastCheck); + window.addEventListener('keydown', fastCheck); + window.addEventListener('change', fastCheck); + } + } + observer.patches = patches; + observer.object = obj; + observer.unobserve = function () { + generate(observer); + clearTimeout(observer.next); + removeObserverFromMirror(mirror, observer); + if (typeof window !== 'undefined') { + window.removeEventListener('mouseup', fastCheck); + window.removeEventListener('keyup', fastCheck); + window.removeEventListener('mousedown', fastCheck); + window.removeEventListener('keydown', fastCheck); + window.removeEventListener('change', fastCheck); + } + }; + mirror.observers.set(callback, new ObserverInfo(callback, observer)); + return observer; +} +/** + * Generate an array of patches from an observer + */ +function generate(observer, invertible) { + if (invertible === void 0) { invertible = false; } + var mirror = beforeDict.get(observer.object); + _generate(mirror.value, observer.object, observer.patches, "", invertible); + if (observer.patches.length) { + applyPatch(mirror.value, observer.patches); + } + var temp = observer.patches; + if (temp.length > 0) { + observer.patches = []; + if (observer.callback) { + observer.callback(temp); + } + } + return temp; +} +// Dirty check if obj is different from mirror, generate patches and update mirror +function _generate(mirror, obj, patches, path, invertible) { + if (obj === mirror) { + return; + } + if (typeof obj.toJSON === "function") { + obj = obj.toJSON(); + } + var newKeys = _objectKeys(obj); + var oldKeys = _objectKeys(mirror); + var deleted = false; + //if ever "move" operation is implemented here, make sure this test runs OK: "should not generate the same patch twice (move)" + for (var t = oldKeys.length - 1; t >= 0; t--) { + var key = oldKeys[t]; + var oldVal = mirror[key]; + if (hasOwnProperty(obj, key) && !(obj[key] === undefined && oldVal !== undefined && Array.isArray(obj) === false)) { + var newVal = obj[key]; + if (typeof oldVal == "object" && oldVal != null && typeof newVal == "object" && newVal != null) { + _generate(oldVal, newVal, patches, path + "/" + escapePathComponent(key), invertible); + } + else { + if (oldVal !== newVal) { + if (invertible) { + patches.push({ op: "test", path: path + "/" + escapePathComponent(key), value: _deepClone(oldVal) }); + } + patches.push({ op: "replace", path: path + "/" + escapePathComponent(key), value: _deepClone(newVal) }); + } + } + } + else if (Array.isArray(mirror) === Array.isArray(obj)) { + if (invertible) { + patches.push({ op: "test", path: path + "/" + escapePathComponent(key), value: _deepClone(oldVal) }); + } + patches.push({ op: "remove", path: path + "/" + escapePathComponent(key) }); + deleted = true; // property has been deleted + } + else { + if (invertible) { + patches.push({ op: "test", path: path, value: mirror }); + } + patches.push({ op: "replace", path: path, value: obj }); + } + } + if (!deleted && newKeys.length == oldKeys.length) { + return; + } + for (var t = 0; t < newKeys.length; t++) { + var key = newKeys[t]; + if (!hasOwnProperty(mirror, key) && obj[key] !== undefined) { + patches.push({ op: "add", path: path + "/" + escapePathComponent(key), value: _deepClone(obj[key]) }); + } + } +} +/** + * Create an array of patches from the differences in two objects + */ +function compare(tree1, tree2, invertible) { + if (invertible === void 0) { invertible = false; } + var patches = []; + _generate(tree1, tree2, patches, '', invertible); + return patches; +} + +var duplex = /*#__PURE__*/Object.freeze({ + __proto__: null, + unobserve: unobserve, + observe: observe, + generate: generate, + compare: compare +}); + +var jsonpatch = Object.assign({}, core, duplex, { + JsonPatchError: PatchError, + deepClone: _deepClone, + escapePathComponent, + unescapePathComponent +}); + +function applyPatchInplace(doc, pathPrefix, patch) { + if (pathPrefix) { + patch = patch.map((op) => + Object.assign({}, op, { path: pathPrefix + op.path }) + ); + } + jsonpatch.applyPatch(doc, patch, false, true); +} + +const html = htm.bind(react.createElement); +const LayoutConfigContext = react.createContext({ + sendEvent: undefined, + loadImportSource: undefined, +}); + +function Layout({ saveUpdateHook, sendEvent, loadImportSource }) { + const [model, patchModel] = useInplaceJsonPatch({}); + + react.useEffect(() => saveUpdateHook(patchModel), [patchModel]); + + if (model.tagName) { + return html` + <${LayoutConfigContext.Provider} value=${{ sendEvent, loadImportSource }}> + <${Element} model=${model} /> + + `; + } else { + return html`
`; + } +} + +function Element({ model, key }) { + if (model.importSource) { + return html`<${ImportedElement} model=${model} />`; + } else { + return html`<${StandardElement} model=${model} />`; + } +} + +function elementChildren(modelChildren) { + if (!modelChildren) { + return []; + } else { + return modelChildren.map((child) => { + switch (typeof child) { + case "object": + return html`<${Element} key=${child.key} model=${child} />`; + case "string": + return child; + } + }); + } +} + +function StandardElement({ model }) { + const config = react.useContext(LayoutConfigContext); + const children = elementChildren(model.children); + const attributes = elementAttributes(model, config.sendEvent); + if (model.children && model.children.length) { + return html`<${model.tagName} ...${attributes}>${children}`; + } else { + return html`<${model.tagName} ...${attributes} />`; + } +} + +function ImportedElement({ model }) { + const config = react.useContext(LayoutConfigContext); + config.sendEvent; + const mountPoint = react.useRef(null); + const fallback = model.importSource.fallback; + const importSource = useConst(() => + loadFromImportSource(config, model.importSource) + ); + + react.useEffect(() => { + if (fallback) { + importSource.then(() => { + reactDom.unmountComponentAtNode(mountPoint.current); + if (mountPoint.current.children) { + mountPoint.current.removeChild(mountPoint.current.children[0]); + } + }); + } + }, []); + + // this effect must run every time in case the model has changed + react.useEffect(() => { + importSource.then(({ createElement, renderElement }) => { + renderElement( + createElement( + model.tagName, + elementAttributes(model, config.sendEvent), + model.children + ), + mountPoint.current + ); + }); + }); + + react.useEffect( + () => () => + importSource.then(({ unmountElement }) => + unmountElement(mountPoint.current) + ), + [] + ); + + if (!fallback) { + return html`
`; + } else if (typeof fallback == "string") { + // need the second div there so we can removeChild above + return html`
${fallback}
`; + } else { + return html`
+ <${StandardElement} model=${fallback} /> +
`; + } +} + +function elementAttributes(model, sendEvent) { + const attributes = Object.assign({}, model.attributes); + + if (model.eventHandlers) { + for (const [eventName, eventSpec] of Object.entries(model.eventHandlers)) { + attributes[eventName] = eventHandler(sendEvent, eventSpec); + } + } + + return attributes; +} + +function eventHandler(sendEvent, eventSpec) { + return function () { + const data = Array.from(arguments).map((value) => { + if (typeof value === "object" && value.nativeEvent) { + if (eventSpec["preventDefault"]) { + value.preventDefault(); + } + if (eventSpec["stopPropagation"]) { + value.stopPropagation(); + } + return serializeEvent(value); + } else { + return value; + } + }); + new Promise((resolve, reject) => { + const msg = { + data: data, + target: eventSpec["target"], + }; + sendEvent(msg); + resolve(msg); + }); + }; +} + +function loadFromImportSource(config, importSource) { + return config + .loadImportSource(importSource.source, importSource.sourceType) + .then((module) => { + if ( + typeof module.createElement == "function" && + typeof module.renderElement == "function" && + typeof module.unmountElement == "function" + ) { + return { + createElement: (type, props, children) => + module.createElement(module[type], props, children, config), + renderElement: module.renderElement, + unmountElement: module.unmountElement, + }; + } else { + console.error(`${module} does not expose the required interfaces`); + } + }); +} + +function useInplaceJsonPatch(doc) { + const ref = react.useRef(doc); + const forceUpdate = useForceUpdate(); + + const applyPatch = react.useCallback( + (path, patch) => { + applyPatchInplace(ref.current, path, patch); + forceUpdate(); + }, + [ref, forceUpdate] + ); + + return [ref.current, applyPatch]; +} + +function useForceUpdate() { + const [, updateState] = react.useState(); + return react.useCallback(() => updateState({}), []); +} + +function useConst(func) { + const ref = react.useRef(); + + if (!ref.current) { + ref.current = func(); + } + + return ref.current; +} + +function mountLayout(mountElement, layoutProps) { + reactDom.render(react.createElement(Layout, layoutProps), mountElement); +} + +function mountLayoutWithWebSocket( + element, + endpoint, + loadImportSource, + maxReconnectTimeout +) { + mountLayoutWithReconnectingWebSocket( + element, + endpoint, + loadImportSource, + maxReconnectTimeout + ); +} + +function mountLayoutWithReconnectingWebSocket( + element, + endpoint, + loadImportSource, + maxReconnectTimeout, + mountState = { + everMounted: false, + reconnectAttempts: 0, + reconnectTimeoutRange: 0, + } +) { + const socket = new WebSocket(endpoint); + + const updateHookPromise = new LazyPromise(); + + socket.onopen = (event) => { + console.log(`Connected.`); + + if (mountState.everMounted) { + reactDom.unmountComponentAtNode(element); + } + _resetOpenMountState(mountState); + + mountLayout(element, { + loadImportSource, + saveUpdateHook: updateHookPromise.resolve, + sendEvent: (event) => socket.send(JSON.stringify(event)), + }); + }; + + socket.onmessage = (event) => { + const [pathPrefix, patch] = JSON.parse(event.data); + updateHookPromise.promise.then((update) => update(pathPrefix, patch)); + }; + + socket.onclose = (event) => { + if (!maxReconnectTimeout) { + console.log(`Connection lost.`); + return; + } + + const reconnectTimeout = _nextReconnectTimeout( + maxReconnectTimeout, + mountState + ); + + console.log(`Connection lost, reconnecting in ${reconnectTimeout} seconds`); + + setTimeout(function () { + mountState.reconnectAttempts++; + mountLayoutWithReconnectingWebSocket( + element, + endpoint, + loadImportSource, + maxReconnectTimeout, + mountState + ); + }, reconnectTimeout * 1000); + }; +} + +function _resetOpenMountState(mountState) { + mountState.everMounted = true; + mountState.reconnectAttempts = 0; + mountState.reconnectTimeoutRange = 0; +} + +function _nextReconnectTimeout(maxReconnectTimeout, mountState) { + const timeout = + Math.floor(Math.random() * mountState.reconnectTimeoutRange) || 1; + mountState.reconnectTimeoutRange = + (mountState.reconnectTimeoutRange + 5) % maxReconnectTimeout; + if (mountState.reconnectAttempts == 4) { + window.alert( + "Server connection was lost. Attempts to reconnect are being made in the background." + ); + } + return timeout; +} + +function LazyPromise() { + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); +} + +const LOC = window.location; +const HTTP_PROTO = LOC.protocol; +const WS_PROTO = HTTP_PROTO === "https:" ? "wss:" : "ws:"; +const IDOM_MODULES_PATH = "/_modules"; + +function mountWidgetExample(mountID, viewID, idomServerHost) { + const idom_url = "//" + (idomServerHost || LOC.host); + const http_idom_url = HTTP_PROTO + idom_url; + const ws_idom_url = WS_PROTO + idom_url; + + const mountEl = document.getElementById(mountID); + const enableWidgetButton = document.createElement("button"); + enableWidgetButton.appendChild(document.createTextNode("Enable Widget")); + enableWidgetButton.setAttribute("class", "enable-widget-button"); + + enableWidgetButton.addEventListener("click", () => + fadeOutElementThenCallback(enableWidgetButton, () => { + { + mountEl.removeChild(enableWidgetButton); + mountEl.setAttribute("class", "interactive widget-container"); + mountLayoutWithWebSocket( + mountEl, + ws_idom_url + `/_idom/stream?view_id=${viewID}`, + (source, sourceType) => + loadImportSource(http_idom_url, source, sourceType) + ); + } + }) + ); + + function fadeOutElementThenCallback(element, callback) { + { + var op = 1; // initial opacity + var timer = setInterval(function () { + { + if (op < 0.001) { + { + clearInterval(timer); + element.style.display = "none"; + callback(); + } + } + element.style.opacity = op; + element.style.filter = "alpha(opacity=" + op * 100 + ")"; + op -= op * 0.5; + } + }, 50); + } + } + + mountEl.appendChild(enableWidgetButton); +} + +function loadImportSource(baseUrl, source, sourceType) { + if (sourceType == "NAME") { + return import(baseUrl + IDOM_MODULES_PATH + "/" + source); + } else { + return import(source); + } +} + +export { mountWidgetExample }; diff --git a/docs/source/auto/api-reference.rst b/docs/source/auto/api-reference.rst index 7b71e8846..e488298ac 100644 --- a/docs/source/auto/api-reference.rst +++ b/docs/source/auto/api-reference.rst @@ -1,12 +1,6 @@ API Reference ============= -.. automodule:: idom.client.manage - :members: - -.. automodule:: idom.client.module - :members: - .. automodule:: idom.config :members: @@ -28,7 +22,7 @@ API Reference .. automodule:: idom.core.vdom :members: -.. automodule:: idom.dialect +.. automodule:: idom.html :members: .. automodule:: idom.log @@ -55,18 +49,15 @@ API Reference .. automodule:: idom.utils :members: -.. automodule:: idom.widgets.html +.. automodule:: idom.web.module :members: -.. automodule:: idom.widgets.utils +.. automodule:: idom.widgets :members: Misc Modules ------------ -.. automodule:: idom.cli - :members: - .. automodule:: idom.core.utils :members: @@ -75,3 +66,6 @@ Misc Modules .. automodule:: idom.server.utils :members: + +.. automodule:: idom.web.utils + :members: diff --git a/docs/source/auto/developer-apis.rst b/docs/source/auto/developer-apis.rst index a1fca80f5..dfe0e27a9 100644 --- a/docs/source/auto/developer-apis.rst +++ b/docs/source/auto/developer-apis.rst @@ -3,9 +3,3 @@ Developer APIs .. automodule:: idom._option :members: - -Misc Dev Modules ----------------- - -.. automodule:: idom.client._private - :members: diff --git a/docs/source/command-line.rst b/docs/source/command-line.rst deleted file mode 100644 index 75e766cfc..000000000 --- a/docs/source/command-line.rst +++ /dev/null @@ -1,51 +0,0 @@ -Command-line -============ - -IDOM supplies a CLI for: - -- Displaying version information -- Installing Javascript packages -- Restoring IDOM's client - - -Show Version Info ------------------ - -To see the version of ``idom`` being run: - -.. code-block:: bash - - idom version - -You can also show all available versioning information: - -.. code-block:: bash - - idom version --verbose - -This is useful for bug reports. - - -Install Javascript Packages ---------------------------- - -You can install Javascript packages at the command line rather than doing it -:ref:`programmatically `: - -.. code-block:: bash - - idom install some-js-package versioned-js-package@^1.0.0 - -If the package is already installed then the build will be skipped. - - -Restore The Client ------------------- - -Replace IDOM's client with a backup from its original installation. - -.. code-block:: bash - - idom restore - -This is useful if a build of the client fails and leaves it in an unusable state. diff --git a/docs/source/conf.py b/docs/source/conf.py index bdf9857b1..ce3d27afa 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -64,6 +64,7 @@ "interactive_widget", "patched_html_translator", "widget_example", + "build_custom_js", ] # Add any paths that contain templates here, relative to this directory. diff --git a/docs/source/custom_js/README.md b/docs/source/custom_js/README.md new file mode 100644 index 000000000..8c77d450b --- /dev/null +++ b/docs/source/custom_js/README.md @@ -0,0 +1,9 @@ +# Custom Javascript for IDOM's Docs + +Build the javascript with + +``` +npm run build +``` + +This will drop a javascript bundle into `../_static/custom.js` diff --git a/docs/source/custom_js/package-lock.json b/docs/source/custom_js/package-lock.json new file mode 100644 index 000000000..8f9ce2b91 --- /dev/null +++ b/docs/source/custom_js/package-lock.json @@ -0,0 +1,447 @@ +{ + "name": "idom-docs-example-loader", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "idom-docs-example-loader", + "version": "1.0.0", + "dependencies": { + "idom-client-react": "file:../../../src/idom/client/packages/idom-client-react" + }, + "devDependencies": { + "prettier": "^2.2.1", + "rollup": "^2.35.1", + "rollup-plugin-commonjs": "^10.1.0", + "rollup-plugin-node-resolve": "^5.2.0", + "rollup-plugin-replace": "^2.2.0" + } + }, + "../../../src/idom/client/packages/idom-client-react": { + "version": "0.8.3", + "license": "MIT", + "dependencies": { + "fast-json-patch": "^3.0.0-1", + "htm": "^3.0.3" + }, + "devDependencies": { + "jsdom": "16.3.0", + "prettier": "^2.2.1", + "uvu": "^0.5.1" + }, + "peerDependencies": { + "react": "^16.13.1", + "react-dom": "^16.13.1" + } + }, + "../../src/idom/client/packages/idom-client-react": { + "extraneous": true + }, + "../src/idom/client/package/idom-client-react": { + "extraneous": true + }, + "../src/idom/client/packages/idom-client-react": { + "extraneous": true + }, + "node_modules/@types/estree": { + "version": "0.0.48", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz", + "integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==", + "dev": true + }, + "node_modules/@types/node": { + "version": "15.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz", + "integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==", + "dev": true + }, + "node_modules/@types/resolve": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", + "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/builtin-modules": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", + "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/idom-client-react": { + "resolved": "../../../src/idom/client/packages/idom-client-react", + "link": true + }, + "node_modules/is-core-module": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", + "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", + "dev": true + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "dependencies": { + "sourcemap-codec": "^1.4.4" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/prettier": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.1.tgz", + "integrity": "sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "dependencies": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "2.52.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.52.1.tgz", + "integrity": "sha512-/SPqz8UGnp4P1hq6wc9gdTqA2bXQXGx13TtoL03GBm6qGRI6Hm3p4Io7GeiHNLl0BsQAne1JNYY+q/apcY933w==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-commonjs": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz", + "integrity": "sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-commonjs.", + "dev": true, + "dependencies": { + "estree-walker": "^0.6.1", + "is-reference": "^1.1.2", + "magic-string": "^0.25.2", + "resolve": "^1.11.0", + "rollup-pluginutils": "^2.8.1" + }, + "peerDependencies": { + "rollup": ">=1.12.0" + } + }, + "node_modules/rollup-plugin-node-resolve": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz", + "integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==", + "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-node-resolve.", + "dev": true, + "dependencies": { + "@types/resolve": "0.0.8", + "builtin-modules": "^3.1.0", + "is-module": "^1.0.0", + "resolve": "^1.11.1", + "rollup-pluginutils": "^2.8.1" + }, + "peerDependencies": { + "rollup": ">=1.11.0" + } + }, + "node_modules/rollup-plugin-replace": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-replace/-/rollup-plugin-replace-2.2.0.tgz", + "integrity": "sha512-/5bxtUPkDHyBJAKketb4NfaeZjL5yLZdeUihSfbF2PQMz+rSTEb8ARKoOl3UBT4m7/X+QOXJo3sLTcq+yMMYTA==", + "deprecated": "This module has moved and is now available at @rollup/plugin-replace. Please update your dependencies. This version is no longer maintained.", + "dev": true, + "dependencies": { + "magic-string": "^0.25.2", + "rollup-pluginutils": "^2.6.0" + } + }, + "node_modules/rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "dependencies": { + "estree-walker": "^0.6.1" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + } + }, + "dependencies": { + "@types/estree": { + "version": "0.0.48", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.48.tgz", + "integrity": "sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==", + "dev": true + }, + "@types/node": { + "version": "15.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz", + "integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==", + "dev": true + }, + "@types/resolve": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", + "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "builtin-modules": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz", + "integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==", + "dev": true + }, + "estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "idom-client-react": { + "version": "file:../../../src/idom/client/packages/idom-client-react", + "requires": { + "fast-json-patch": "^3.0.0-1", + "htm": "^3.0.3", + "jsdom": "16.3.0", + "prettier": "^2.2.1", + "uvu": "^0.5.1" + } + }, + "is-core-module": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", + "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", + "dev": true + }, + "is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "requires": { + "@types/estree": "*" + } + }, + "magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.4" + } + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "prettier": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.1.tgz", + "integrity": "sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==", + "dev": true + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "rollup": { + "version": "2.52.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.52.1.tgz", + "integrity": "sha512-/SPqz8UGnp4P1hq6wc9gdTqA2bXQXGx13TtoL03GBm6qGRI6Hm3p4Io7GeiHNLl0BsQAne1JNYY+q/apcY933w==", + "dev": true, + "requires": { + "fsevents": "~2.3.2" + } + }, + "rollup-plugin-commonjs": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.1.0.tgz", + "integrity": "sha512-jlXbjZSQg8EIeAAvepNwhJj++qJWNJw1Cl0YnOqKtP5Djx+fFGkp3WRh+W0ASCaFG5w1jhmzDxgu3SJuVxPF4Q==", + "dev": true, + "requires": { + "estree-walker": "^0.6.1", + "is-reference": "^1.1.2", + "magic-string": "^0.25.2", + "resolve": "^1.11.0", + "rollup-pluginutils": "^2.8.1" + } + }, + "rollup-plugin-node-resolve": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz", + "integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==", + "dev": true, + "requires": { + "@types/resolve": "0.0.8", + "builtin-modules": "^3.1.0", + "is-module": "^1.0.0", + "resolve": "^1.11.1", + "rollup-pluginutils": "^2.8.1" + } + }, + "rollup-plugin-replace": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-replace/-/rollup-plugin-replace-2.2.0.tgz", + "integrity": "sha512-/5bxtUPkDHyBJAKketb4NfaeZjL5yLZdeUihSfbF2PQMz+rSTEb8ARKoOl3UBT4m7/X+QOXJo3sLTcq+yMMYTA==", + "dev": true, + "requires": { + "magic-string": "^0.25.2", + "rollup-pluginutils": "^2.6.0" + } + }, + "rollup-pluginutils": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", + "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "dev": true, + "requires": { + "estree-walker": "^0.6.1" + } + }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + } + } +} diff --git a/docs/source/custom_js/package.json b/docs/source/custom_js/package.json new file mode 100644 index 000000000..c6686813a --- /dev/null +++ b/docs/source/custom_js/package.json @@ -0,0 +1,20 @@ +{ + "name": "idom-docs-example-loader", + "version": "1.0.0", + "description": "simple javascript client for IDOM's documentation", + "main": "index.js", + "scripts": { + "build": "rollup --config", + "format": "prettier --ignore-path .gitignore --write ." + }, + "devDependencies": { + "prettier": "^2.2.1", + "rollup": "^2.35.1", + "rollup-plugin-commonjs": "^10.1.0", + "rollup-plugin-node-resolve": "^5.2.0", + "rollup-plugin-replace": "^2.2.0" + }, + "dependencies": { + "idom-client-react": "file:../../../src/idom/client/packages/idom-client-react" + } +} diff --git a/docs/source/custom_js/rollup.config.js b/docs/source/custom_js/rollup.config.js new file mode 100644 index 000000000..b8ca529bc --- /dev/null +++ b/docs/source/custom_js/rollup.config.js @@ -0,0 +1,25 @@ +import resolve from "rollup-plugin-node-resolve"; +import commonjs from "rollup-plugin-commonjs"; +import replace from "rollup-plugin-replace"; + +export default { + input: "src/index.js", + output: { + file: "../_static/custom.js", + format: "esm", + }, + plugins: [ + resolve(), + commonjs(), + replace({ + "process.env.NODE_ENV": JSON.stringify("production"), + }), + ], + onwarn: function (warning) { + if (warning.code === "THIS_IS_UNDEFINED") { + // skip warning where `this` is undefined at the top level of a module + return; + } + console.warn(warning.message); + }, +}; diff --git a/docs/source/_static/js/load-widget-example.js b/docs/source/custom_js/src/index.js similarity index 55% rename from docs/source/_static/js/load-widget-example.js rename to docs/source/custom_js/src/index.js index bcb43c99a..70d981a5a 100644 --- a/docs/source/_static/js/load-widget-example.js +++ b/docs/source/custom_js/src/index.js @@ -1,10 +1,11 @@ +import { mountLayoutWithWebSocket } from "idom-client-react"; + const LOC = window.location; const HTTP_PROTO = LOC.protocol; const WS_PROTO = HTTP_PROTO === "https:" ? "wss:" : "ws:"; const IDOM_MODULES_PATH = "/_modules"; -const IDOM_CLIENT_REACT_PATH = IDOM_MODULES_PATH + "/idom-client-react.js"; -export default function loadWidgetExample(idomServerHost, mountID, viewID) { +export function mountWidgetExample(mountID, viewID, idomServerHost) { const idom_url = "//" + (idomServerHost || LOC.host); const http_idom_url = HTTP_PROTO + idom_url; const ws_idom_url = WS_PROTO + idom_url; @@ -14,28 +15,22 @@ export default function loadWidgetExample(idomServerHost, mountID, viewID) { enableWidgetButton.appendChild(document.createTextNode("Enable Widget")); enableWidgetButton.setAttribute("class", "enable-widget-button"); - enableWidgetButton.addEventListener("click", () => { - { - import(http_idom_url + IDOM_CLIENT_REACT_PATH).then((module) => { - { - fadeOutAndThen(enableWidgetButton, () => { - { - mountEl.removeChild(enableWidgetButton); - mountEl.setAttribute("class", "interactive widget-container"); - module.mountLayoutWithWebSocket( - mountEl, - ws_idom_url + `/_idom/stream?view_id=${viewID}`, - (source, sourceType) => - loadImportSource(http_idom_url, source, sourceType) - ); - } - }); - } - }); - } - }); + enableWidgetButton.addEventListener("click", () => + fadeOutElementThenCallback(enableWidgetButton, () => { + { + mountEl.removeChild(enableWidgetButton); + mountEl.setAttribute("class", "interactive widget-container"); + mountLayoutWithWebSocket( + mountEl, + ws_idom_url + `/_idom/stream?view_id=${viewID}`, + (source, sourceType) => + loadImportSource(http_idom_url, source, sourceType) + ); + } + }) + ); - function fadeOutAndThen(element, callback) { + function fadeOutElementThenCallback(element, callback) { { var op = 1; // initial opacity var timer = setInterval(function () { @@ -60,7 +55,7 @@ export default function loadWidgetExample(idomServerHost, mountID, viewID) { function loadImportSource(baseUrl, source, sourceType) { if (sourceType == "NAME") { - return import(baseUrl + IDOM_MODULES_PATH + "/" + source + ".js"); + return import(baseUrl + IDOM_MODULES_PATH + "/" + source); } else { return import(source); } diff --git a/docs/source/examples.rst b/docs/source/examples.rst index c9ab6365b..9afabb913 100644 --- a/docs/source/examples.rst +++ b/docs/source/examples.rst @@ -59,7 +59,7 @@ Simply install your javascript library of choice using the ``idom`` CLI: idom install victory -Then import the module with :class:`~idom.widgets.utils.Module`: +Then import the module with :mod:`~idom.web.module`: .. example:: victory_chart diff --git a/docs/source/examples/material_ui_button_no_action.py b/docs/source/examples/material_ui_button_no_action.py index 932efd28f..19300792b 100644 --- a/docs/source/examples/material_ui_button_no_action.py +++ b/docs/source/examples/material_ui_button_no_action.py @@ -1,12 +1,11 @@ import idom -material_ui = idom.install("@material-ui/core", fallback="loading...") +mui = idom.web.module_from_template("react", "@material-ui/core", fallback="⌛") +Button = idom.web.export(mui, "Button") idom.run( idom.component( - lambda: material_ui.Button( - {"color": "primary", "variant": "contained"}, "Hello World!" - ) + lambda: Button({"color": "primary", "variant": "contained"}, "Hello World!") ) ) diff --git a/docs/source/examples/material_ui_button_on_click.py b/docs/source/examples/material_ui_button_on_click.py index 3b0ec5a88..bde5b5d90 100644 --- a/docs/source/examples/material_ui_button_on_click.py +++ b/docs/source/examples/material_ui_button_on_click.py @@ -3,7 +3,8 @@ import idom -material_ui = idom.install("@material-ui/core", fallback="loading...") +mui = idom.web.module_from_template("react", "@material-ui/core", fallback="⌛") +Button = idom.web.export(mui, "Button") @idom.component @@ -11,7 +12,7 @@ def ViewButtonEvents(): event, set_event = idom.hooks.use_state(None) return idom.html.div( - material_ui.Button( + Button( { "color": "primary", "variant": "contained", diff --git a/docs/source/examples/material_ui_slider.py b/docs/source/examples/material_ui_slider.py index ddffcccb7..12f0f080e 100644 --- a/docs/source/examples/material_ui_slider.py +++ b/docs/source/examples/material_ui_slider.py @@ -3,7 +3,8 @@ import idom -material_ui = idom.install("@material-ui/core", fallback="loading...") +mui = idom.web.module_from_template("react", "@material-ui/core", fallback="⌛") +Slider = idom.web.export(mui, "Slider") @idom.component @@ -11,7 +12,7 @@ def ViewSliderEvents(): (event, value), set_data = idom.hooks.use_state((None, 50)) return idom.html.div( - material_ui.Slider( + Slider( { "color": "primary" if value < 50 else "secondary", "step": 10, diff --git a/docs/source/examples/matplotlib_plot.py b/docs/source/examples/matplotlib_plot.py index 108353d85..ee255a19a 100644 --- a/docs/source/examples/matplotlib_plot.py +++ b/docs/source/examples/matplotlib_plot.py @@ -3,7 +3,7 @@ import matplotlib.pyplot as plt import idom -from idom.widgets.html import image +from idom.widgets import image @idom.component diff --git a/docs/source/examples/simple_dashboard.py b/docs/source/examples/simple_dashboard.py index d4bedcd38..ed302153e 100644 --- a/docs/source/examples/simple_dashboard.py +++ b/docs/source/examples/simple_dashboard.py @@ -3,10 +3,11 @@ import time import idom -from idom.widgets.html import Input +from idom.widgets import Input -victory = idom.install("victory", fallback="loading...") +victory = idom.web.module_from_template("react", "victory", fallback="loading...") +VictoryLine = idom.web.export(victory, "VictoryLine") @idom.component @@ -53,7 +54,7 @@ async def animate(): } set_data(data[1:] + [next_data_point]) - return victory.VictoryLine( + return VictoryLine( { "data": data, "style": { diff --git a/docs/source/examples/super_simple_chart.js b/docs/source/examples/super_simple_chart.js index 0e220d2d9..490cc819b 100644 --- a/docs/source/examples/super_simple_chart.js +++ b/docs/source/examples/super_simple_chart.js @@ -1,4 +1,4 @@ -import { h, Component, render } from "https://unpkg.com/preact?module"; +import { h, render } from "https://unpkg.com/preact?module"; import htm from "https://unpkg.com/htm?module"; const html = htm.bind(h); @@ -6,7 +6,7 @@ const html = htm.bind(h); export { h as createElement, render as renderElement }; export function unmountElement(container) { - preactRender(null, container); + render(null, container); } export function SuperSimpleChart(props) { diff --git a/docs/source/examples/super_simple_chart.py b/docs/source/examples/super_simple_chart.py index 6f210b98d..8f5d77ae3 100644 --- a/docs/source/examples/super_simple_chart.py +++ b/docs/source/examples/super_simple_chart.py @@ -3,13 +3,13 @@ import idom -path_to_source_file = Path(__file__).parent / "super_simple_chart.js" -ssc = idom.Module("super-simple-chart", source_file=path_to_source_file) - +file = Path(__file__).parent / "super_simple_chart.js" +ssc = idom.web.module_from_file("super-simple-chart", file, fallback="⌛") +SuperSimpleChart = idom.web.export(ssc, "SuperSimpleChart") idom.run( idom.component( - lambda: ssc.SuperSimpleChart( + lambda: SuperSimpleChart( { "data": [ {"x": 1, "y": 2}, diff --git a/docs/source/examples/victory_chart.py b/docs/source/examples/victory_chart.py index 45c3dbb71..cdfad5e22 100644 --- a/docs/source/examples/victory_chart.py +++ b/docs/source/examples/victory_chart.py @@ -1,6 +1,8 @@ import idom -victory = idom.install("victory", fallback="loading...") +victory = idom.web.module_from_template("react", "victory", fallback="⌛") +VictoryBar = idom.web.export(victory, "VictoryBar") + bar_style = {"parent": {"width": "500px"}, "data": {"fill": "royalblue"}} -idom.run(idom.component(lambda: victory.VictoryBar({"style": bar_style}))) +idom.run(idom.component(lambda: VictoryBar({"style": bar_style}))) diff --git a/docs/source/faq.rst b/docs/source/faq.rst index 135ebd53e..10c6abb7c 100644 --- a/docs/source/faq.rst +++ b/docs/source/faq.rst @@ -24,11 +24,11 @@ simple and one which allows you to do everything you normally would in Python. Does IDOM support any React component? -------------------------------------- -If you :ref:`Dynamically Install Javascript` components, then the answer is no. Only -components whose props are JSON serializable, or which expect basic callback functions -similar to those of standard event handlers (e.g. ``onClick``) will operate as expected. +If you use :ref:`Dynamically Loaded Components`, then the answer is no. Only components +whose props are JSON serializable, or which expect basic callback functions similar to +those of standard event handlers (e.g. ``onClick``) will operate as expected. -However, if you import a pre-built :ref:`Custom Javascript Component ` +However, if you import a :ref:`Custom Javascript Component ` then, so long as the bundle has be defined appropriately, any component can be made to work, even those that don't rely on React. @@ -54,6 +54,8 @@ These restrictions apply because the Javascript from the CDN must be able to run natively in the browser and the module must be able to run in isolation from the main application. +See :ref:`Distributing Javascript via CDN_` for more info. + What props can I pass to Javascript components? ----------------------------------------------- diff --git a/docs/source/index.rst b/docs/source/index.rst index a7d8475c5..8686a7f49 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -17,7 +17,6 @@ IDOM core-concepts javascript-components - command-line specifications .. toctree:: @@ -41,6 +40,7 @@ IDOM A package for building highly interactive user interfaces in pure Python inspred by `ReactJS `__. + At a Glance ----------- diff --git a/docs/source/installation.rst b/docs/source/installation.rst index a4c46318f..2b1ab6f2a 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -55,97 +55,5 @@ table below: * - ``flask`` - `Flask `__ as a :ref:`Layout Server` - * - ``dialect`` - - :ref:`Python Language Extension` for writing JSX-like syntax - * - ``all`` - All the features listed above (not usually needed) - - -Python Language Extension -------------------------- - -IDOM includes an import-time transpiler for writing JSX-like syntax in a ``.py`` file! - -.. code-block:: python - - # dialect=html - from idom import html - - message = "hello world!" - attrs = {"height": "10px", "width": "10px"} - model = html(f"

{message}

") - - assert model == { - "tagName": "div", - "attributes": {"height": "10px", "width": "10px"}, - "children": [{"tagName": "p", "children": ["hello world!"]}], - } - -With Jupyter and IPython support: - -.. code-block:: python - - %%dialect html - from idom import html - assert html(f"
") == {"tagName": "div"} - -That you can install with ``pip``: - -.. code-block:: - - pip install idom[dialect] - - -Usage -..... - -1. Import ``idom`` in your application's ``entrypoint.py`` - -2. Import ``your_module.py`` with a ``# dialect=html`` header comment. - -3. Inside ``your_module.py`` import ``html`` from ``idom`` - -4. Run ``python entrypoint.py`` from your console. - -So here's the files you should have set up: - -.. code-block:: text - - project - |- entrypoint.py - |- your_module.py - -The contents of ``entrypoint.py`` should contain: - -.. code-block:: - - import idom # this needs to be first! - import your_module - -While ``your_module.py`` should contain the following: - -.. code-block:: - - # dialect=html - from idom import html - assert html(f"
") == {"tagName": "div"} - -And that's it! - - -How It Works -............ - -Once ``idom`` has been imported at your application's entrypoint, any following modules -imported with a ``# dialect=html`` header comment get transpiled just before they're -executed. This is accomplished by using Pyalect_ to hook a transpiler into Pythons -import system. The :class:`~idom.dialect.HtmlDialectTranspiler` implements Pyalect_'s -:class:`~pyalect.dialect.Transpiler` interface using some tooling from htm.py_. - - -.. Links -.. ===== - -.. _Pyalect: https://pyalect.readthedocs.io/en/latest/ -.. _htm.py: https://github.com/jviide/htm.py diff --git a/docs/source/javascript-components.rst b/docs/source/javascript-components.rst index ba6154379..e9debc6e6 100644 --- a/docs/source/javascript-components.rst +++ b/docs/source/javascript-components.rst @@ -4,12 +4,10 @@ Javascript Components While IDOM is a great tool for displaying HTML and responding to browser events with pure Python, there are other projects which already allow you to do this inside `Jupyter Notebooks `__ -or in -`webpages `__. +or in standard +`web apps `__. The real power of IDOM comes from its ability to seamlessly leverage the existing -ecosystem of -`React components `__. -This can be accomplished in different ways for different reasons: +Javascript ecosystem. This can be accomplished in different ways for different reasons: .. list-table:: :header-rows: 1 @@ -17,30 +15,49 @@ This can be accomplished in different ways for different reasons: * - Integration Method - Use Case + * - :ref:`Dynamically Loaded Components` + - You want to **quickly experiment** with IDOM and the Javascript ecosystem. + * - :ref:`Custom Javascript Components` - You want to create polished software that can be **easily shared** with others. - * - :ref:`Dynamically Install Javascript` (requires NPM_) - - You want to **quickly experiment** with IDOM and the Javascript ecosystem. + +Dynamically Loaded Components +----------------------------- + +.. note:: + + This method is not recommended in production systems - see + :ref:`Distributing Javascript Components` for more info. + +IDOM makes it easy to draft your code when you're in the early stages of development by +using a CDN_ to dynamically load Javascript packages on the fly. In this example we'll +be using the ubiquitous React-based UI framework `Material UI`_. + +.. example:: material_ui_button_no_action + +So now that we can display a Material UI Button we probably want to make it do +something. Thankfully there's nothing new to learn here, you can pass event handlers to +the button just as you did when :ref:`getting started`. Thus, all we need to do is add +an ``onClick`` handler to the component: + +.. example:: material_ui_button_on_click Custom Javascript Components ---------------------------- -For projects that will be shared with others we recommend bundling your Javascript with -`rollup `__ or `webpack `__ -into a -`web module `__. -IDOM also provides a -`template repository `__ +For projects that will be shared with others, we recommend bundling your Javascript with +Rollup_ or Webpack_ into a `web module`_. IDOM also provides a `template repository`_ that can be used as a blueprint to build a library of React components. -The core benefit of loading Javascript in this way is that users of your code won't need -to have NPM_ installed. Rather, they can use ``pip`` to install your Python package -without any other build steps because the bundled Javascript you distributed with it -will be symlinked into the IDOM client at runtime. +To work as intended, the Javascript bundle must provide named exports for the following +functions as well as any components that will be rendered. -To work as intended, the Javascript bundle must export the following named functions: +.. note:: + + The exported components do not have to be React-based since you'll have full control + over the rendering mechanism. .. code-block:: typescript @@ -63,25 +80,17 @@ These functions can be thought of as being analogous to those from React. .. |reactDOM.unmountComponentAtNode| replace:: ``reactDOM.unmountComponentAtNode`` .. _reactDOM.unmountComponentAtNode: https://reactjs.org/docs/react-api.html#createelement -And will be called in the following manner: +And will be called in the following manner, where ``component`` is a named export of +your module: .. code-block:: // on every render - renderElement(createElement(type, props), container); + renderElement(createElement(component, props), container); // on unmount unmountElement(container); -Once you've done this, you can distribute the bundled javascript in your Python package -and integrate it into IDOM by defining :class:`~idom.client.module.Module` objects that -load them from source: - -.. code-block:: - - import idom - my_js_package = idom.Module("my-js-package", source_file="/path/to/my/bundle.js") - -The simplest way to try this out yourself though, is to hook in simple a hand-crafted +The simplest way to try this out yourself though, is to hook in a simple hand-crafted Javascript module that has the requisite interface. In the example to follow we'll create a very basic SVG line chart. The catch though is that we are limited to using Javascript that can run directly in the browser. This means we can't use fancy syntax @@ -91,58 +100,307 @@ like `JSX `__ and instead will us .. example:: super_simple_chart -.. Links -.. ===== +Distributing Javascript Components +---------------------------------- -.. _Material UI: https://material-ui.com/ -.. _NPM: https://www.npmjs.com -.. _install NPM: https://www.npmjs.com/get-npm +There are two ways that you can distribute your :ref:`Custom Javascript Components`: +- Using a CDN_ +- In a Python package via PyPI_ +These options are not mutually exclusive though, and it may be beneficial to support +both options. For example, if you upload your Javascript components to NPM_ and also +bundle your Javascript inside a Python package, in principle your users can determine +which work best for them. Regardless though, either you or, if you give then the choice, +your users, will have to consider the tradeoffs of either approach. -Dynamically Install Javascript ------------------------------- +- :ref:`Distributing Javascript via CDN_` - Most useful in production-grade applications + where its assumed the user has a network connection. In this scenario a CDN's `edge + network `__ can be used to bring the + Javascript source closer to the user in order to reduce page load times. -.. warning:: +- :ref:`Distributing Javascript via PyPI_` - This method is ideal for local usage since + the user can server all the Javascript components they depend on from their computer + without requiring a network connection. - - Before continuing `install NPM`_. - - Not guaranteed to work in all client implementations - (see :attr:`~idom.config.IDOM_CLIENT_MODULES_MUST_HAVE_MOUNT`) -IDOM makes it easy to draft your code when you're in the early stages of development by -using NPM_ to directly install Javascript packages on the fly. In this example we'll be -using the ubiquitous React-based UI framework `Material UI`_ which can be installed -using the ``idom`` CLI: +Distributing Javascript via CDN_ +................................ + +Under this approach, to simplify these instructions, we're going to ignore the problem +of distributing the Javascript since that must be handled by your CDN. For open source +or personal projects, a CDN like https://unpkg.com/ makes things easy by automatically +preparing any package that's been uploaded to NPM_. If you need to roll with your own +private CDN, this will likely be more complicated. -.. code-block:: bash +In either case though, on the Python side, things are quite simple. You need only pass +the URL where your package can be found to :func:`~idom.web.module.module_from_file` +where you can then load any of its exports: - idom install @material-ui/core +.. code-block:: + + import idom -Or at runtime with :func:`idom.client.module.install` (this is useful if you're working -in a REPL or Jupyter Notebook): + your_module = ido.web.module_from_file("https://some.cdn/your-module") + YourComponent = idom.web.export(your_module, "YourComponent") + + +Distributing Javascript via PyPI_ +................................. + +This can be most easily accomplished by using the `template repository`_ that's been +purpose-built for this. However, to get a better sense for its inner workings, we'll +briefly look at what's required. At a high level, we must consider how to... + +1. bundle your Javascript into an `ECMAScript Module`) +2. include that Javascript bundle in a Python package +3. use it as a component in your applciation using IDOM + +In the descriptions to follow we'll be assuming that: + +- NPM_ is the Javascript package manager +- The components are implemented with React_ +- Rollup_ bundles the Javascript module +- Setuptools_ builds the Python package + +To start, let's take a look at the file structure we'll be building: + +.. code-block:: text + + your-project + |-- js + | |-- src + | | \-- index.js + | |-- package.json + | \-- rollup.config.js + |-- your_python_package + | |-- __init__.py + | \-- widget.py + |-- Manifest.in + |-- pyproject.toml + \-- setup.py + +``index.js`` should contain the relevant exports (see +:ref:`Custom JavaScript Components` for more info): + +.. code-block:: javascript + + import * as react from "react"; + import * as reactDOM from "react-dom"; + + // exports required to interface with IDOM + export const createElement = (component, props) => + react.createElement(component, props); + export const renderElement = reactDOM.render; + export const unmountElement = reactDOM.unmountComponentAtNode; + + // exports for your components + export YourFirstComponent(props) {...}; + export YourSecondComponent(props) {...}; + export YourThirdComponent(props) {...}; + + +Your ``package.json`` should include the following: + +.. code-block:: python + + { + "name": "YOUR-PACKAGE-NAME", + "scripts": { + "build": "rollup --config", + ... + }, + "devDependencies": { + "rollup": "^2.35.1", + "rollup-plugin-commonjs": "^10.1.0", + "rollup-plugin-node-resolve": "^5.2.0", + "rollup-plugin-replace": "^2.2.0", + ... + }, + "dependencies": { + "react": "^17.0.1", + "react-dom": "^17.0.1", + ... + }, + ... + } + +Getting a bit more in the weeds now, your ``rollup.config.js`` file should be designed +such that it drops an ES Module at ``your-project/your_python_package/bundle.js`` since +we'll be writing ``widget.py`` under that assumption. + +.. note:: + + Don't forget to ignore this ``bundle.js`` file when committing code (with a + ``.gitignore`` if you're using Git) since it can always rebuild from the raw + Javascript source in ``your-project/js``. + +.. code-block:: javascript + + import resolve from "rollup-plugin-node-resolve"; + import commonjs from "rollup-plugin-commonjs"; + import replace from "rollup-plugin-replace"; + + export default { + input: "src/index.js", + output: { + file: "../your_python_package/bundle.js", + format: "esm", + }, + plugins: [ + resolve(), + commonjs(), + replace({ + "process.env.NODE_ENV": JSON.stringify("production"), + }), + ] + }; + +Your ``widget.py`` file should then load the neighboring bundle file using +:func:`~idom.web.module.module_from_file`. Then components from that bundle can be +loaded with :func:`~idom.web.module.export`. .. code-block:: + from pathlib import Path + import idom - material_ui = idom.install("@material-ui/core") - # or install multiple modules at once - material_ui, *other_modules = idom.install(["@material-ui/core", ...]) + + _BUNDLE_PATH = Path(__file__).parent / "bundle.js" + _WEB_MODULE = idom.web.module_from_file( + # Note that this is the same name from package.json - this must be globally + # unique since it must share a namespace with all other javascript packages. + name="YOUR-PACKAGE-NAME", + file=_BUNDLE_PATH, + # What to temporarilly display while the module is being loaded + fallback="Loading...", + ) + + # Your module must provide a named export for YourFirstComponent + YourFirstComponent = idom.web.export(_WEB_MODULE, "YourFirstComponent") + + # It's possible to export multiple components at once + YourSecondComponent, YourThirdComponent = idom.web.export( + _WEB_MODULE, ["YourSecondComponent", "YourThirdComponent"] + ) .. note:: - Any standard javascript dependency specifier is allowed here. + When :data:`idom.config.IDOM_DEBUG_MODE` is active, named exports will be validated. -Once the package has been successfully installed, you can import and display the component: +The remaining files that we need to create are concerned with creating a Python package. +We won't cover all the details here, so refer to the Setuptools_ documentation for +more information. With that said, the first file to fill out is `pyproject.toml` since +we need to declare what our build tool is (in this case Setuptools): -.. example:: material_ui_button_no_action +.. code-block:: toml + [build-system] + requires = ["setuptools>=40.8.0", "wheel"] + build-backend = "setuptools.build_meta" -Passing Props To Javascript Components --------------------------------------- +Then, we can creat the ``setup.py`` file which uses Setuptools. This will differ +substantially from a normal ``setup.py`` file since, as part of the build process we'll +need to use NPM to bundle our Javascript. This requires customizing some of the build +commands in Setuptools like ``build``, ``sdist``, and ``develop``: -So now that we can install and display a Material UI Button we probably want to make it -do something. Thankfully there's nothing new to learn here, you can pass event handlers -to the button just as you did when :ref:`getting started`. Thus, all we need to do is -add an ``onClick`` handler to the component: +.. code-block:: python -.. example:: material_ui_button_on_click + import subprocess + from pathlib import Path + + from setuptools import setup, find_packages + from distutils.command.build import build + from distutils.command.sdist import sdist + from setuptools.command.develop import develop + + PACKAGE_SPEC = {} # gets passed to setup() at the end + + + # ----------------------------------------------------------------------------- + # General Package Info + # ----------------------------------------------------------------------------- + + + PACKAGE_NAME = "your_python_package" + + PACKAGE_SPEC.update( + name=PACKAGE_NAME, + version="0.0.1", + packages=find_packages(exclude=["tests*"]), + classifiers=["Framework :: IDOM", ...], + keywords=["IDOM", "components", ...], + # install IDOM with this package + install_requires=["idom"], + # required in order to include static files like bundle.js using MANIFEST.in + include_package_data=True, + # we need access to the file system, so cannot be run from a zip file + zip_safe=False, + ) + + + # ---------------------------------------------------------------------------- + # Build Javascript + # ---------------------------------------------------------------------------- + + + # basic paths used to gather files + PROJECT_ROOT = Path(__file__).parent + PACKAGE_DIR = PROJECT_ROOT / PACKAGE_NAME + JS_DIR = PROJECT_ROOT / "js" + + + def build_javascript_first(cls): + class Command(cls): + def run(self): + for cmd_str in ["npm install", "npm run build"]: + subprocess.run(cmd_str.split(), cwd=str(JS_DIR), check=True) + super().run() + + return Command + + + package["cmdclass"] = { + "sdist": build_javascript_first(sdist), + "build": build_javascript_first(build), + "develop": build_javascript_first(develop), + } + + + # ----------------------------------------------------------------------------- + # Run It + # ----------------------------------------------------------------------------- + + + if __name__ == "__main__": + setup(**package) + + +Finally, since we're using ``include_package_data`` you'll need a MANIFEST.in_ file that +includes ``bundle.js``: + +.. code-block:: text + + include your_python_package/bundle.js + +And that's it! While this might seem like a lot of work, you're always free to start +creating your custom components using the provided `template repository`_ so you can get +up and running as quickly as possible. + + +.. Links +.. ===== + +.. _Material UI: https://material-ui.com/ +.. _NPM: https://www.npmjs.com +.. _install NPM: https://www.npmjs.com/get-npm +.. _CDN: https://en.wikipedia.org/wiki/Content_delivery_network +.. _PyPI: https://pypi.org/ +.. _template repository: https://github.com/idom-team/idom-react-component-cookiecutter +.. _web module: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules +.. _Rollup: https://rollupjs.org/guide/en/ +.. _Webpack: https://webpack.js.org/ +.. _Setuptools: https://setuptools.readthedocs.io/en/latest/userguide/index.html +.. _ECMAScript Module: https://tc39.es/ecma262/#sec-modules +.. _React: https://reactjs.org +.. _MANIFEST.in: https://packaging.python.org/guides/using-manifest-in/ diff --git a/noxfile.py b/noxfile.py index 8742367fc..fb880cf24 100644 --- a/noxfile.py +++ b/noxfile.py @@ -60,7 +60,10 @@ def docs(session: Session) -> None: "scripts/live_docs.py", "--open-browser", # for some reason this matches absolute paths - "--ignore=**/docs/source/auto/*", + "--ignore=**/auto/*", + "--ignore=**/_static/custom.js", + "--ignore=**/node_modules/*", + "--ignore=**/package-lock.json", "-a", "-E", "-b", @@ -122,7 +125,7 @@ def test_suite(session: Session) -> None: posargs += ["--cov=src/idom", "--cov-report", "term"] install_idom_dev(session, extras="all") - session.run("pytest", "tests", *posargs) + session.run("pytest", *posargs) @nox.session @@ -205,7 +208,3 @@ def install_idom_dev(session: Session, extras: str = "stable") -> None: session.install("-e", f".[{extras}]") else: session.posargs.remove("--no-install") - if "--no-restore" not in session.posargs: - session.run("idom", "restore") - else: - session.posargs.remove("--no-restore") diff --git a/requirements/check-types.txt b/requirements/check-types.txt index b036bcd49..2ae26d60a 100644 --- a/requirements/check-types.txt +++ b/requirements/check-types.txt @@ -3,3 +3,4 @@ types-click types-tornado types-pkg-resources types-flask +types-requests diff --git a/requirements/pkg-deps.txt b/requirements/pkg-deps.txt index 75db38e0e..0ef2d1c96 100644 --- a/requirements/pkg-deps.txt +++ b/requirements/pkg-deps.txt @@ -1,10 +1,6 @@ typing-extensions >=3.7.4 mypy-extensions >=0.4.3 anyio >=3.0 -async_exit_stack >=1.0.1; python_version<"3.7" jsonpatch >=1.26 -typer >=0.3.2 -click-spinner >=0.1.10 fastjsonschema >=2.14.5 -rich >=9.13.0 -appdirs >=1.4.4 +requests >=2.0 diff --git a/requirements/pkg-extras.txt b/requirements/pkg-extras.txt index c1fbd225c..6205adf00 100644 --- a/requirements/pkg-extras.txt +++ b/requirements/pkg-extras.txt @@ -19,8 +19,3 @@ selenium # extra=matplotlib matplotlib - -# extra=dialect -htm -pyalect -tagged diff --git a/requirements/test-env.txt b/requirements/test-env.txt index 586c60948..6fd1686da 100644 --- a/requirements/test-env.txt +++ b/requirements/test-env.txt @@ -5,3 +5,4 @@ pytest-mock pytest-timeout selenium ipython +responses diff --git a/scripts/live_docs.py b/scripts/live_docs.py index 65cac6974..0f42a1ca6 100644 --- a/scripts/live_docs.py +++ b/scripts/live_docs.py @@ -11,6 +11,7 @@ from docs.main import IDOM_MODEL_SERVER_URL_PREFIX, make_app, make_component from idom.server.sanic import PerClientStateServer +from idom.testing import clear_idom_web_modules_dir # these environment variable are used in custom Sphinx extensions @@ -24,6 +25,7 @@ def wrap_builder(old_builder): # This is the bit that we're injecting to get the example components to reload too def new_builder(): [s.stop() for s in _running_idom_servers] + clear_idom_web_modules_dir() server = PerClientStateServer( make_component(), diff --git a/scripts/one_example.py b/scripts/one_example.py index ac51693c6..f5ae42f28 100644 --- a/scripts/one_example.py +++ b/scripts/one_example.py @@ -5,7 +5,7 @@ from threading import Thread import idom -from idom.widgets.utils import hotswap +from idom.widgets import hotswap here = Path(__file__).parent diff --git a/setup.cfg b/setup.cfg index 5ac552c3a..5d1bb1235 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,12 +8,12 @@ warn_redundant_casts = True warn_unused_ignores = True [flake8] -ignore = E203, E266, E501, W503, F811, N802 +ignore = E203, E266, E501, W503, F811, N802, N806 max-line-length = 88 max-complexity = 18 select = B,C,E,F,W,T4,B9,N,ROH exclude = - src/idom/client/app/node_modules/* + **/node_modules/* .eggs/* .tox/* diff --git a/setup.py b/setup.py index 469b1acc7..c8cddf9e8 100644 --- a/setup.py +++ b/setup.py @@ -63,16 +63,6 @@ def list2cmdline(cmd_list): } -# ----------------------------------------------------------------------------- -# CLI Entrypoints -# ----------------------------------------------------------------------------- - - -package["entry_points"] = { - "console_scripts": ["idom = idom.__main__:main"], -} - - # ----------------------------------------------------------------------------- # Requirements # ----------------------------------------------------------------------------- @@ -129,7 +119,7 @@ class Command(cls): def run(self): log.info("Installing Javascript...") try: - js_dir = str(package_dir / "client" / "app") + js_dir = str(package_dir / "client") npm = shutil.which("npm") # this is required on windows if npm is None: raise RuntimeError("NPM is not installed.") diff --git a/src/idom/__init__.py b/src/idom/__init__.py index 21fed6f1a..71efeaf1e 100644 --- a/src/idom/__init__.py +++ b/src/idom/__init__.py @@ -10,8 +10,7 @@ __author__ = "idom-team" -from . import config, log -from .client.module import Import, Module, install +from . import config, html, log, web from .core import hooks from .core.component import Component, component from .core.events import Events, event @@ -19,23 +18,7 @@ from .core.vdom import VdomDict, vdom from .server.prefab import run from .utils import Ref, html_to_vdom -from .widgets.html import html -from .widgets.utils import hotswap, multiview - - -# try to automatically setup the dialect's import hook -try: - import htm - import pyalect - import tagged -except ImportError: # pragma: no cover - pass -else: - from . import dialect - - del pyalect - del tagged - del htm +from .widgets import hotswap, multiview __all__ = [ @@ -51,8 +34,6 @@ "server", "Ref", "vdom", - "Module", - "Import", "hotswap", "multiview", "html_to_vdom", @@ -62,4 +43,5 @@ "install", "log", "config", + "web", ] diff --git a/src/idom/__main__.py b/src/idom/__main__.py deleted file mode 100644 index 2f05ddc22..000000000 --- a/src/idom/__main__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .cli import main - - -if __name__ == "__main__": - main() diff --git a/src/idom/_option.py b/src/idom/_option.py index 2ee46c941..ef18e02c0 100644 --- a/src/idom/_option.py +++ b/src/idom/_option.py @@ -8,16 +8,12 @@ import os from logging import getLogger from typing import Any, Callable, Generic, TypeVar, cast -from weakref import WeakSet _O = TypeVar("_O") logger = getLogger(__name__) -ALL_OPTIONS: WeakSet[Option[Any]] = WeakSet() - - class Option(Generic[_O]): """An option that can be set using an environment variable of the same name""" @@ -35,7 +31,6 @@ def __init__( if name in os.environ: self._current = validator(os.environ[name]) logger.debug(f"{self._name}={self.current}") - ALL_OPTIONS.add(self) @property def name(self) -> str: diff --git a/src/idom/cli.py b/src/idom/cli.py deleted file mode 100644 index 00701c788..000000000 --- a/src/idom/cli.py +++ /dev/null @@ -1,56 +0,0 @@ -from logging.config import dictConfig -from typing import List - -import typer - -import idom -from idom.client import manage as manage_client - -from .config import all_options -from .log import logging_config_defaults - - -main = typer.Typer() - - -@main.callback(invoke_without_command=True, no_args_is_help=True) -def root( - version: bool = typer.Option( - False, - "--version", - help="show the current version", - show_default=False, - is_eager=True, - ) -) -> None: - """Command line interface for IDOM""" - - # reset logging config after Typer() has wrapped stdout - dictConfig(logging_config_defaults()) - - if version: - typer.echo(idom.__version__) - - return None - - -@main.command() -def install(packages: List[str]) -> None: - """Install a Javascript package from NPM into the client""" - manage_client.build(packages, clean_build=False) - return None - - -@main.command() -def restore() -> None: - """Return to a fresh install of the client""" - manage_client.restore() - return None - - -@main.command() -def options() -> None: - """Show available global options and their current values""" - for opt in list(sorted(all_options(), key=lambda opt: opt.name)): - name = typer.style(opt.name, bold=True) - typer.echo(f"{name} = {opt.current}") diff --git a/src/idom/client/app/README.md b/src/idom/client/README.md similarity index 100% rename from src/idom/client/app/README.md rename to src/idom/client/README.md diff --git a/src/idom/client/_private.py b/src/idom/client/_private.py deleted file mode 100644 index 8ea00f890..000000000 --- a/src/idom/client/_private.py +++ /dev/null @@ -1,104 +0,0 @@ -import json -import logging -import re -import shutil -from os.path import getmtime -from pathlib import Path -from typing import Dict, Set, Tuple, cast - -from idom.config import IDOM_CLIENT_BUILD_DIR - - -logger = logging.getLogger(__name__) - -HERE = Path(__file__).parent -APP_DIR = HERE / "app" -BACKUP_BUILD_DIR = APP_DIR / "build" - -# the path relative to the build that contains import sources -IDOM_CLIENT_IMPORT_SOURCE_INFIX = "_snowpack/pkg" - - -def _run_build_dir_init_only_once() -> None: # pragma: no cover - """Initialize the runtime build directory - this should only be called once""" - if not IDOM_CLIENT_BUILD_DIR.current.exists(): - logger.debug("creating new runtime build directory") - IDOM_CLIENT_BUILD_DIR.current.parent.mkdir(parents=True, exist_ok=True) - # populate the runtime build directory if it doesn't exist - shutil.copytree(BACKUP_BUILD_DIR, IDOM_CLIENT_BUILD_DIR.current, symlinks=True) - elif getmtime(BACKUP_BUILD_DIR) > getmtime(IDOM_CLIENT_BUILD_DIR.current): - logger.debug("updating runtime build directory because it is out of date") - # delete the existing runtime build because it's out of date - shutil.rmtree(IDOM_CLIENT_BUILD_DIR.current) - # replace it with the newer backup build (presumable from a fresh install) - shutil.copytree(BACKUP_BUILD_DIR, IDOM_CLIENT_BUILD_DIR.current, symlinks=True) - else: - logger.debug("runtime build directory is up to date") - - -_run_build_dir_init_only_once() # this is only ever called once at runtime! - - -def get_user_packages_file(app_dir: Path) -> Path: - return app_dir / "packages" / "idom-app-react" / "src" / "user-packages.js" - - -def restore_build_dir_from_backup() -> None: - target = IDOM_CLIENT_BUILD_DIR.current - if target.exists(): - shutil.rmtree(target) - shutil.copytree(BACKUP_BUILD_DIR, target, symlinks=True) - - -def replace_build_dir(source: Path) -> None: - target = IDOM_CLIENT_BUILD_DIR.current - if target.exists(): - shutil.rmtree(target) - shutil.copytree(source, target, symlinks=True) - - -def get_package_name(pkg: str) -> str: - return split_package_name_and_version(pkg)[0] - - -def split_package_name_and_version(pkg: str) -> Tuple[str, str]: - at_count = pkg.count("@") - if pkg.startswith("@"): - if at_count == 1: - return pkg, "" - else: - name, version = pkg[1:].split("@", 1) - return ("@" + name), version - elif at_count: - name, version = pkg.split("@", 1) - return name, version - else: - return pkg, "" - - -def build_dependencies() -> Dict[str, str]: - package_json = IDOM_CLIENT_BUILD_DIR.current / "package.json" - return cast(Dict[str, str], json.loads(package_json.read_text())["dependencies"]) - - -_JS_MODULE_EXPORT_PATTERN = re.compile( - r";?\s*export\s*{([0-9a-zA-Z_$\s,]*)}\s*;", re.MULTILINE -) -_JS_VAR = r"[a-zA-Z_$][0-9a-zA-Z_$]*" -_JS_MODULE_EXPORT_NAME_PATTERN = re.compile( - fr";?\s*export\s+({_JS_VAR})\s+{_JS_VAR}\s*;", re.MULTILINE -) -_JS_MODULE_EXPORT_FUNC_PATTERN = re.compile( - fr";?\s*export\s+function\s+({_JS_VAR})\s*\(.*?", re.MULTILINE -) - - -def find_js_module_exports_in_source(content: str) -> Set[str]: - names: Set[str] = set() - for match in _JS_MODULE_EXPORT_PATTERN.findall(content): - for export in match.split(","): - export_parts = export.split(" as ", 1) - names.add(export_parts[-1].strip()) - names.update(_JS_MODULE_EXPORT_FUNC_PATTERN.findall(content)) - names.update(_JS_MODULE_EXPORT_NAME_PATTERN.findall(content)) - return names diff --git a/src/idom/client/app/.gitignore b/src/idom/client/app/.gitignore deleted file mode 100644 index f2e88ad45..000000000 --- a/src/idom/client/app/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -web_modules -build diff --git a/src/idom/client/app/packages/idom-app-react/src/user-packages.js b/src/idom/client/app/packages/idom-app-react/src/user-packages.js deleted file mode 100644 index ff8b4c563..000000000 --- a/src/idom/client/app/packages/idom-app-react/src/user-packages.js +++ /dev/null @@ -1 +0,0 @@ -export default {}; diff --git a/src/idom/client/app/idom-logo-square-small.svg b/src/idom/client/idom-logo-square-small.svg similarity index 100% rename from src/idom/client/app/idom-logo-square-small.svg rename to src/idom/client/idom-logo-square-small.svg diff --git a/src/idom/client/app/index.html b/src/idom/client/index.html similarity index 100% rename from src/idom/client/app/index.html rename to src/idom/client/index.html diff --git a/src/idom/client/manage.py b/src/idom/client/manage.py deleted file mode 100644 index 573568e98..000000000 --- a/src/idom/client/manage.py +++ /dev/null @@ -1,219 +0,0 @@ -""" -Client Manager -============== -""" - -import shutil -import subprocess -import sys -from logging import getLogger -from pathlib import Path -from tempfile import TemporaryDirectory -from typing import Dict, Iterable, List, Sequence, Set, Union - -from idom.config import IDOM_CLIENT_BUILD_DIR - -from . import _private - - -logger = getLogger(__name__) - - -def web_modules_dir() -> Path: - """The directory containing all web modules - - .. warning:: - - No assumptions should be made about the exact structure of this directory! - """ - return IDOM_CLIENT_BUILD_DIR.current / "_snowpack" / "pkg" - - -def web_module_path(package_name: str, must_exist: bool = False) -> Path: - """Get the :class:`Path` to a web module's source""" - path = web_modules_dir().joinpath(*(package_name + ".js").split("/")) - if must_exist and not path.exists(): - raise ValueError( - f"Web module {package_name!r} does not exist at path {str(path)!r}" - ) - return path - - -def web_module_exports(package_name: str) -> Set[str]: - """Get a list of names this module exports""" - web_module_path(package_name, must_exist=True) - return _private.find_js_module_exports_in_source( - web_module_path(package_name).read_text(encoding="utf-8") - ) - - -def web_module_exists(package_name: str) -> bool: - """Whether a web module with a given name exists""" - return web_module_path(package_name).exists() - - -def web_module_names() -> Set[str]: - """Get the names of all installed web modules""" - names = [] - web_mod_dir = web_modules_dir() - for pth in web_mod_dir.glob("**/*.js"): - rel_pth = pth.relative_to(web_mod_dir) - if Path("common") in rel_pth.parents: - continue - module_path = str(rel_pth.as_posix()) - if module_path.endswith(".js"): - module_path = module_path[:-3] - names.append(module_path) - return set(names) - - -def add_web_module( - package_name: str, - source: Union[Path, str], -) -> None: - """Add a web module from source""" - resolved_source = Path(source).resolve() - if not resolved_source.exists(): - raise FileNotFoundError(f"Package source file does not exist: {str(source)!r}") - target = web_module_path(package_name) - if target.resolve() == resolved_source: - return None # already added - target.parent.mkdir(parents=True, exist_ok=True) - # this will raise an error if already exists - target.symlink_to(resolved_source) - - -def remove_web_module(package_name: str, must_exist: bool = False) -> None: - """Remove a web module""" - web_module_path(package_name, must_exist).unlink() - - -def restore() -> None: - _private.restore_build_dir_from_backup() - - -def build( - packages: Sequence[str], - clean_build: bool = True, - skip_if_already_installed: bool = True, -) -> None: - """Build the client""" - package_specifiers_to_install = list(packages) - del packages # delete since we just renamed it - - packages_to_install = _parse_package_specs(package_specifiers_to_install) - installed_packages = _private.build_dependencies() - - if clean_build: - all_packages = packages_to_install - else: - if skip_if_already_installed: - for pkg_name, pkg_ver in packages_to_install.items(): - if pkg_name not in installed_packages or ( - pkg_ver and installed_packages[pkg_name] != pkg_ver - ): - break - else: - logger.info(f"Already installed {package_specifiers_to_install}") - logger.info("Build skipped ✅") - return None - all_packages = {**installed_packages, **packages_to_install} - - all_package_specifiers = [f"{p}@{v}" if v else p for p, v in all_packages.items()] - - with TemporaryDirectory() as tempdir: - tempdir_path = Path(tempdir) - temp_app_dir = tempdir_path / "app" - temp_build_dir = temp_app_dir / "build" - package_json_path = temp_app_dir / "package.json" - - # copy over the whole APP_DIR directory into the temp one - shutil.copytree(_private.APP_DIR, temp_app_dir, symlinks=True) - - _write_user_packages_file(temp_app_dir, list(all_packages)) - - logger.info("Installing dependencies...") - _npm_install(all_package_specifiers, temp_app_dir) - logger.info("Installed successfully ✅") - - logger.debug(f"package.json: {package_json_path.read_text()}") - - logger.info("Building client ...") - _npm_run_build(temp_app_dir) - logger.info("Client built successfully ✅") - - _private.replace_build_dir(temp_build_dir) - - not_discovered = set(all_packages).difference(web_module_names()) - if not_discovered: - raise RuntimeError( # pragma: no cover - f"Successfuly installed {list(all_packages)} but " - f"failed to discover {list(not_discovered)} post-install." - ) - - -if sys.platform == "win32" and sys.version_info[:2] == (3, 7): # pragma: no cover - - def build( - packages: Sequence[str], - clean_build: bool = True, - skip_if_already_installed: bool = True, - ) -> None: - msg = ( - "This feature is not available due to a bug in Python<3.8 on Windows - for " - "more information see: https://bugs.python.org/issue31226" - ) - try: - import pytest - except ImportError: - raise NotImplementedError(msg) - else: - pytest.xfail(msg) - - -def _parse_package_specs(package_strings: Sequence[str]) -> Dict[str, str]: - return { - dep: ver - for dep, ver in map(_private.split_package_name_and_version, package_strings) - } - - -def _npm_install(packages: List[str], cwd: Path) -> None: - _run_subprocess(["npm", "install"] + packages, cwd) - - -def _npm_run_build(cwd: Path) -> None: - _run_subprocess(["npm", "run", "build"], cwd) - - -def _run_subprocess(args: List[str], cwd: Path) -> None: - cmd, *args = args - which_cmd = shutil.which(cmd) - if which_cmd is None: - raise RuntimeError( # pragma: no cover - f"Failed to run command - {cmd!r} is not installed." - ) - try: - subprocess.run( - [which_cmd] + args, - cwd=cwd, - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - except subprocess.CalledProcessError as error: # pragma: no cover - raise subprocess.SubprocessError(error.stderr.decode()) from error - return None - - -def _write_user_packages_file(app_dir: Path, packages: Iterable[str]) -> None: - _private.get_user_packages_file(app_dir).write_text( - _USER_PACKAGES_FILE_TEMPLATE.format( - imports=",".join(f'"{pkg}":import({pkg!r})' for pkg in packages) - ) - ) - - -_USER_PACKAGES_FILE_TEMPLATE = """// THIS FILE WAS GENERATED BY IDOM - DO NOT MODIFY -export default {{{imports}}}; -""" diff --git a/src/idom/client/module.py b/src/idom/client/module.py deleted file mode 100644 index 4a0e4acef..000000000 --- a/src/idom/client/module.py +++ /dev/null @@ -1,230 +0,0 @@ -""" -Client Modules -============== -""" - -from __future__ import annotations - -from pathlib import Path -from typing import Any, Dict, List, Optional, Set, Tuple, Union, overload -from urllib.parse import urlparse - -from idom.core.vdom import ImportSourceDict, VdomDict, make_vdom_constructor - -from . import _private, manage - - -@overload -def install( - packages: str, - ignore_installed: bool, - fallback: Optional[str], -) -> Module: - ... - - -@overload -def install( - packages: Union[List[str], Tuple[str]], - ignore_installed: bool, - fallback: Optional[str], -) -> List[Module]: - ... - - -def install( - packages: Union[str, List[str], Tuple[str]], - ignore_installed: bool = False, - fallback: Optional[str] = None, -) -> Union[Module, List[Module]]: - return_one = False - if isinstance(packages, str): - packages = [packages] - return_one = True - - pkg_names = [_private.get_package_name(pkg) for pkg in packages] - - if ignore_installed or set(pkg_names).difference(manage.web_module_names()): - manage.build(packages, clean_build=False) - - if return_one: - return Module(pkg_names[0], fallback=fallback) - else: - return [Module(pkg, fallback=fallback) for pkg in pkg_names] - - -NAME_SOURCE = "NAME" -"""A named souce - usually a Javascript package name""" - -URL_SOURCE = "URL" -"""A source loaded from a URL, usually from a CDN""" - -SOURCE_TYPES = {NAME_SOURCE, URL_SOURCE} -"""The possible source types for a :class:`Module`""" - - -class Module: - """A Javascript module - - Parameters: - source: - The URL to an ECMAScript module which exports React components - (*with* a ``.js`` file extension) or name of a module installed in the - built-in client application (*without* a ``.js`` file extension). - source_type: - The type of the given ``source``. See :const:`SOURCE_TYPES` for the set of - possible values. - file: - Only applicable if running on a client app which supports this feature. - Dynamically install the code in the give file as a single-file module. The - built-in client will inject this module adjacent to other installed modules - which means they can be imported via a relative path like - ``./some-other-installed-module.js``. - fallack: - What to display while the modules is being loaded. - - Attributes: - installed: - Whether or not this module has been installed into the built-in client app. - url: - The URL this module will be imported from. - """ - - __slots__ = "source", "source_type", "fallback", "exports" - - def __init__( - self, - source: str, - source_type: Optional[str] = None, - source_file: Optional[Union[str, Path]] = None, - fallback: Optional[str] = None, - check_exports: Optional[bool] = None, - ) -> None: - self.source = source - self.fallback = fallback - self.exports: Optional[Set[str]] = None - - if source_type is None: - self.source_type = URL_SOURCE if _is_url(source) else NAME_SOURCE - elif source_type in SOURCE_TYPES: - self.source_type = source_type - else: - raise ValueError(f"Invalid source type {source_type!r}") - - if self.source_type == URL_SOURCE: - if check_exports is True: - raise ValueError(f"Can't check exports for source type {source_type!r}") - elif source_file is not None: - raise ValueError(f"File given, but source type is {source_type!r}") - else: - return None - elif check_exports is None: - check_exports = True - - if source_file is not None: - manage.add_web_module(source, source_file) - elif not manage.web_module_exists(source): - raise ValueError(f"Module {source!r} does not exist") - - if check_exports: - self.exports = manage.web_module_exports(source) - - def declare( - self, - name: str, - has_children: bool = True, - fallback: Optional[str] = None, - ) -> Import: - """Return an :class:`Import` for the given :class:`Module` and ``name`` - - This roughly translates to the javascript statement - - .. code-block:: javascript - - import { name } from "module" - - Where ``name`` is the given name, and ``module`` is the :attr:`Module.url` of - this :class:`Module` instance. - """ - if self.exports is not None and name not in self.exports: - raise ValueError( - f"{self} does not export {name!r}, available options are {list(self.exports)}" - ) - - return Import( - name, - self.source, - self.source_type, - has_children, - fallback or self.fallback, - ) - - def __getattr__(self, name: str) -> Import: - if name[0].lower() == name[0]: - # component names should be capitalized - raise AttributeError(f"{self} has no attribute {name!r}") - return self.declare(name) - - def __eq__(self, other: Any) -> bool: - return ( - isinstance(other, Module) - and self.source == other.source - and self.source_type == other.source_type - ) - - def __repr__(self) -> str: - return f"{type(self).__name__}({self.source})" - - -class Import: - """Import a react module - - Once imported, you can instantiate the library's components by calling them - via attribute-access. - - Examples: - - .. code-block:: python - - victory = idom.Import("victory", "VictoryBar" install=True) - style = {"parent": {"width": "500px"}} - victory.VictoryBar({"style": style}, fallback="loading...") - """ - - __slots__ = "_constructor", "_import_source", "_name" - - def __init__( - self, - name: str, - source: str, - source_type: str, - has_children: bool = True, - fallback: Optional[str] = None, - ) -> None: - self._name = name - self._constructor = make_vdom_constructor(name, has_children) - self._import_source = ImportSourceDict( - source=source, - sourceType=source_type, - fallback=fallback, - ) - - def __call__( - self, - *args: Any, - **kwargs: Any, - ) -> VdomDict: - return self._constructor(import_source=self._import_source, *args, **kwargs) - - def __repr__(self) -> str: - info: Dict[str, Any] = {"name": self._name, **self._import_source} - strings = ", ".join(f"{k}={v!r}" for k, v in info.items()) - return f"{type(self).__name__}({strings})" - - -def _is_url(string: str) -> bool: - if string.startswith("/") or string.startswith("./") or string.startswith("../"): - return True - else: - parsed = urlparse(string) - return bool(parsed.scheme and parsed.netloc) diff --git a/src/idom/client/app/package-lock.json b/src/idom/client/package-lock.json similarity index 99% rename from src/idom/client/app/package-lock.json rename to src/idom/client/package-lock.json index 045bbd4f7..a04120515 100644 --- a/src/idom/client/app/package-lock.json +++ b/src/idom/client/package-lock.json @@ -2663,7 +2663,7 @@ }, "packages/idom-app-react/packages/idom-client-react": {}, "packages/idom-client-react": { - "version": "0.8.2", + "version": "0.8.3", "license": "MIT", "dependencies": { "fast-json-patch": "^3.0.0-1", @@ -2672,8 +2672,6 @@ "devDependencies": { "jsdom": "16.3.0", "prettier": "^2.2.1", - "react": "^16.13.1", - "react-dom": "^16.13.1", "uvu": "^0.5.1" }, "peerDependencies": { @@ -3469,8 +3467,6 @@ "htm": "^3.0.3", "jsdom": "16.3.0", "prettier": "^2.2.1", - "react": "^16.13.1", - "react-dom": "^16.13.1", "uvu": "^0.5.1" } }, diff --git a/src/idom/client/app/package.json b/src/idom/client/package.json similarity index 100% rename from src/idom/client/app/package.json rename to src/idom/client/package.json diff --git a/src/idom/client/app/packages/idom-app-react/package.json b/src/idom/client/packages/idom-app-react/package.json similarity index 100% rename from src/idom/client/app/packages/idom-app-react/package.json rename to src/idom/client/packages/idom-app-react/package.json diff --git a/src/idom/client/app/packages/idom-app-react/src/index.js b/src/idom/client/packages/idom-app-react/src/index.js similarity index 78% rename from src/idom/client/app/packages/idom-app-react/src/index.js rename to src/idom/client/packages/idom-app-react/src/index.js index 50a639f9f..0e24439ac 100644 --- a/src/idom/client/app/packages/idom-app-react/src/index.js +++ b/src/idom/client/packages/idom-app-react/src/index.js @@ -1,14 +1,5 @@ import { mountLayoutWithWebSocket } from "idom-client-react"; -// imported so static analysis knows to pick up files linked by user-packages.js -import("./user-packages.js").then((module) => { - for (const pkgName in module.default) { - module.default[pkgName].then((pkg) => { - console.log(`Loaded module '${pkgName}'`); - }); - } -}); - export function mount(mountPoint) { mountLayoutWithWebSocket( mountPoint, @@ -35,7 +26,7 @@ function getWebSocketEndpoint() { } function loadImportSource(source, sourceType) { - return import(sourceType == "NAME" ? `./${source}.js` : source); + return import(sourceType == "NAME" ? `/modules/${source}` : source); } function shouldReconnect() { diff --git a/src/idom/client/app/packages/idom-client-react/.gitignore b/src/idom/client/packages/idom-client-react/.gitignore similarity index 100% rename from src/idom/client/app/packages/idom-client-react/.gitignore rename to src/idom/client/packages/idom-client-react/.gitignore diff --git a/src/idom/client/app/packages/idom-client-react/README.md b/src/idom/client/packages/idom-client-react/README.md similarity index 100% rename from src/idom/client/app/packages/idom-client-react/README.md rename to src/idom/client/packages/idom-client-react/README.md diff --git a/src/idom/client/app/packages/idom-client-react/package-lock.json b/src/idom/client/packages/idom-client-react/package-lock.json similarity index 98% rename from src/idom/client/app/packages/idom-client-react/package-lock.json rename to src/idom/client/packages/idom-client-react/package-lock.json index e590bff4f..72b4b3999 100644 --- a/src/idom/client/app/packages/idom-client-react/package-lock.json +++ b/src/idom/client/packages/idom-client-react/package-lock.json @@ -1,22 +1,20 @@ { "name": "idom-client-react", - "version": "0.7.4", + "version": "0.8.3", "lockfileVersion": 2, "requires": true, "packages": { "": { - "version": "0.7.4", + "version": "0.8.3", "license": "MIT", "dependencies": { "fast-json-patch": "^3.0.0-1", - "htm": "^3.0.3" + "htm": "^3.0.3", + "preact": "^10.5.13" }, "devDependencies": { - "esm": "^3.2.25", "jsdom": "16.3.0", "prettier": "^2.2.1", - "react": "^16.13.1", - "react-dom": "^16.13.1", "uvu": "^0.5.1" }, "peerDependencies": { @@ -297,15 +295,6 @@ "source-map": "~0.6.1" } }, - "node_modules/esm": { - "version": "3.2.25", - "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", - "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -505,7 +494,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "peer": true }, "node_modules/jsbn": { "version": "0.1.1", @@ -623,7 +612,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, + "peer": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -680,7 +669,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -714,6 +703,15 @@ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", "dev": true }, + "node_modules/preact": { + "version": "10.5.13", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.5.13.tgz", + "integrity": "sha512-q/vlKIGNwzTLu+jCcvywgGrt+H/1P/oIRSD6mV4ln3hmlC+Aa34C7yfPI4+5bzW8pONyVXYS7SvXosy2dKKtWQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -739,7 +737,7 @@ "version": "15.7.2", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "dev": true, + "peer": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -774,7 +772,7 @@ "version": "16.14.0", "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", - "dev": true, + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -788,7 +786,7 @@ "version": "16.14.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", - "dev": true, + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -803,7 +801,7 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "peer": true }, "node_modules/request": { "version": "2.88.2", @@ -950,7 +948,7 @@ "version": "0.19.1", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", - "dev": true, + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -1447,12 +1445,6 @@ "source-map": "~0.6.1" } }, - "esm": { - "version": "3.2.25", - "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", - "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", - "dev": true - }, "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -1610,7 +1602,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "peer": true }, "jsbn": { "version": "0.1.1", @@ -1708,7 +1700,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, + "peer": true, "requires": { "js-tokens": "^3.0.0 || ^4.0.0" } @@ -1750,7 +1742,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true + "peer": true }, "optionator": { "version": "0.8.3", @@ -1778,6 +1770,11 @@ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", "dev": true }, + "preact": { + "version": "10.5.13", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.5.13.tgz", + "integrity": "sha512-q/vlKIGNwzTLu+jCcvywgGrt+H/1P/oIRSD6mV4ln3hmlC+Aa34C7yfPI4+5bzW8pONyVXYS7SvXosy2dKKtWQ==" + }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -1794,7 +1791,7 @@ "version": "15.7.2", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "dev": true, + "peer": true, "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -1823,7 +1820,7 @@ "version": "16.14.0", "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", - "dev": true, + "peer": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -1834,7 +1831,7 @@ "version": "16.14.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", - "dev": true, + "peer": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -1846,7 +1843,7 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "peer": true }, "request": { "version": "2.88.2", @@ -1954,7 +1951,7 @@ "version": "0.19.1", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", - "dev": true, + "peer": true, "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" diff --git a/src/idom/client/app/packages/idom-client-react/package.json b/src/idom/client/packages/idom-client-react/package.json similarity index 89% rename from src/idom/client/app/packages/idom-client-react/package.json rename to src/idom/client/packages/idom-client-react/package.json index 845889d36..42ffc6d46 100644 --- a/src/idom/client/app/packages/idom-client-react/package.json +++ b/src/idom/client/packages/idom-client-react/package.json @@ -1,7 +1,7 @@ { "name": "idom-client-react", "description": "A client for IDOM implemented in React", - "version": "0.8.2", + "version": "0.8.3", "author": "Ryan Morshead", "license": "MIT", "type": "module", @@ -20,8 +20,6 @@ "devDependencies": { "jsdom": "16.3.0", "prettier": "^2.2.1", - "react": "^16.13.1", - "react-dom": "^16.13.1", "uvu": "^0.5.1" }, "dependencies": { diff --git a/src/idom/client/app/packages/idom-client-react/src/component.js b/src/idom/client/packages/idom-client-react/src/component.js similarity index 87% rename from src/idom/client/app/packages/idom-client-react/src/component.js rename to src/idom/client/packages/idom-client-react/src/component.js index b7fec67b3..9217985d6 100644 --- a/src/idom/client/app/packages/idom-client-react/src/component.js +++ b/src/idom/client/packages/idom-client-react/src/component.js @@ -7,7 +7,7 @@ import serializeEvent from "./event-to-object"; import { applyPatchInplace, joinUrl } from "./utils"; const html = htm.bind(react.createElement); -const LayoutConfigContext = react.createContext({ +export const LayoutConfigContext = react.createContext({ sendEvent: undefined, loadImportSource: undefined, }); @@ -28,7 +28,7 @@ export function Layout({ saveUpdateHook, sendEvent, loadImportSource }) { } } -function Element({ model, key }) { +export function Element({ model, key }) { if (model.importSource) { return html`<${ImportedElement} model=${model} />`; } else { @@ -36,6 +36,21 @@ function Element({ model, key }) { } } +export function elementChildren(modelChildren) { + if (!modelChildren) { + return []; + } else { + return modelChildren.map((child) => { + switch (typeof child) { + case "object": + return html`<${Element} key=${child.key} model=${child} />`; + case "string": + return child; + } + }); + } +} + function StandardElement({ model }) { const config = react.useContext(LayoutConfigContext); const children = elementChildren(model.children); @@ -58,7 +73,12 @@ function ImportedElement({ model }) { react.useEffect(() => { if (fallback) { - reactDOM.unmountComponentAtNode(mountPoint.current); + importSource.then(() => { + reactDOM.unmountComponentAtNode(mountPoint.current); + if (mountPoint.current.children) { + mountPoint.current.removeChild(mountPoint.current.children[0]); + } + }); } }, []); @@ -87,7 +107,8 @@ function ImportedElement({ model }) { if (!fallback) { return html`
`; } else if (typeof fallback == "string") { - return html`
${fallback}
`; + // need the second div there so we can removeChild above + return html`
${fallback}
`; } else { return html`
<${StandardElement} model=${fallback} /> @@ -95,21 +116,6 @@ function ImportedElement({ model }) { } } -function elementChildren(modelChildren) { - if (!modelChildren) { - return []; - } else { - return modelChildren.map((child) => { - switch (typeof child) { - case "object": - return html`<${Element} key=${child.key} model=${child} />`; - case "string": - return child; - } - }); - } -} - function elementAttributes(model, sendEvent) { const attributes = Object.assign({}, model.attributes); @@ -158,22 +164,13 @@ function loadFromImportSource(config, importSource) { typeof module.unmountElement == "function" ) { return { - createElement: (type, props) => - module.createElement(module[type], props), + createElement: (type, props, children) => + module.createElement(module[type], props, children, config), renderElement: module.renderElement, unmountElement: module.unmountElement, }; } else { - return { - createElement: (type, props, children) => - react.createElement( - module[type], - props, - ...elementChildren(children) - ), - renderElement: reactDOM.render, - unmountElement: reactDOM.unmountComponentAtNode, - }; + console.error(`${module} does not expose the required interfaces`); } }); } diff --git a/src/idom/client/app/packages/idom-client-react/src/event-to-object.js b/src/idom/client/packages/idom-client-react/src/event-to-object.js similarity index 100% rename from src/idom/client/app/packages/idom-client-react/src/event-to-object.js rename to src/idom/client/packages/idom-client-react/src/event-to-object.js diff --git a/src/idom/client/app/packages/idom-client-react/src/index.js b/src/idom/client/packages/idom-client-react/src/index.js similarity index 100% rename from src/idom/client/app/packages/idom-client-react/src/index.js rename to src/idom/client/packages/idom-client-react/src/index.js diff --git a/src/idom/client/app/packages/idom-client-react/src/mount.js b/src/idom/client/packages/idom-client-react/src/mount.js similarity index 100% rename from src/idom/client/app/packages/idom-client-react/src/mount.js rename to src/idom/client/packages/idom-client-react/src/mount.js diff --git a/src/idom/client/app/packages/idom-client-react/src/utils.js b/src/idom/client/packages/idom-client-react/src/utils.js similarity index 100% rename from src/idom/client/app/packages/idom-client-react/src/utils.js rename to src/idom/client/packages/idom-client-react/src/utils.js diff --git a/src/idom/client/app/packages/idom-client-react/tests/event-to-object.test.js b/src/idom/client/packages/idom-client-react/tests/event-to-object.test.js similarity index 100% rename from src/idom/client/app/packages/idom-client-react/tests/event-to-object.test.js rename to src/idom/client/packages/idom-client-react/tests/event-to-object.test.js diff --git a/src/idom/client/app/packages/idom-client-react/tests/tooling/dom.js b/src/idom/client/packages/idom-client-react/tests/tooling/dom.js similarity index 100% rename from src/idom/client/app/packages/idom-client-react/tests/tooling/dom.js rename to src/idom/client/packages/idom-client-react/tests/tooling/dom.js diff --git a/src/idom/client/app/packages/idom-client-react/tests/tooling/setup.js b/src/idom/client/packages/idom-client-react/tests/tooling/setup.js similarity index 100% rename from src/idom/client/app/packages/idom-client-react/tests/tooling/setup.js rename to src/idom/client/packages/idom-client-react/tests/tooling/setup.js diff --git a/src/idom/client/app/snowpack.config.js b/src/idom/client/snowpack.config.js similarity index 100% rename from src/idom/client/app/snowpack.config.js rename to src/idom/client/snowpack.config.js diff --git a/src/idom/config.py b/src/idom/config.py index fa3e3cd85..c3ae8292e 100644 --- a/src/idom/config.py +++ b/src/idom/config.py @@ -6,23 +6,12 @@ variables or, for those which allow it, a programatic interface. """ -import shutil from pathlib import Path -from typing import Any, List +from tempfile import TemporaryDirectory -from appdirs import user_data_dir - -import idom - -from ._option import ALL_OPTIONS as _ALL_OPTIONS from ._option import Option as _Option -def all_options() -> List[_Option[Any]]: - """Get a list of all options""" - return list(_ALL_OPTIONS) - - IDOM_DEBUG_MODE = _Option( "IDOM_DEBUG_MODE", default=False, @@ -38,9 +27,12 @@ def all_options() -> List[_Option[Any]]: is set to ``DEBUG``. """ -IDOM_CLIENT_BUILD_DIR = _Option( - "IDOM_CLIENT_BUILD_DIR", - default=Path(user_data_dir(idom.__name__, idom.__author__)) / "client", +# Because these web modules will be linked dynamically at runtime this can be temporary +_DEFAULT_WEB_MODULES_DIR = TemporaryDirectory() + +IDOM_WED_MODULES_DIR = _Option( + "IDOM_WED_MODULES_DIR", + default=Path(_DEFAULT_WEB_MODULES_DIR.name), validator=Path, ) """The location IDOM will use to store its client application @@ -50,11 +42,6 @@ def all_options() -> List[_Option[Any]]: a set of publically available APIs for working with the client. """ -# TODO: remove this in 0.30.0 -_DEPRECATED_BUILD_DIR = Path(__file__).parent / "client" / "build" -if _DEPRECATED_BUILD_DIR.exists(): # pragma: no cover - shutil.rmtree(_DEPRECATED_BUILD_DIR) - IDOM_FEATURE_INDEX_AS_DEFAULT_KEY = _Option( "IDOM_FEATURE_INDEX_AS_DEFAULT_KEY", default=False, diff --git a/src/idom/core/vdom.py b/src/idom/core/vdom.py index d542954e5..166c4ddd9 100644 --- a/src/idom/core/vdom.py +++ b/src/idom/core/vdom.py @@ -106,7 +106,7 @@ class _VdomDictRequired(TypedDict, total=True): class VdomDict(_VdomDictRequired, _VdomDictOptional): - """A VDOM dictionary""" + """A VDOM dictionary - see :ref:`VDOM Mimetype` for more info""" _AttributesAndChildrenArg = Union[Mapping[str, Any], str, Iterable[Any], Any] @@ -201,7 +201,7 @@ def constructor( qualname_prefix = constructor.__qualname__.rsplit(".", 1)[0] constructor.__qualname__ = qualname_prefix + f".{tag}" constructor.__doc__ = ( - f"""Create a new ``<{tag}/>`` - returns :ref:`VDOM `.""" + f"""Create a new ``<{tag}/>`` - returns a :class:`VdomDict`.""" ) return constructor diff --git a/src/idom/dialect.py b/src/idom/dialect.py deleted file mode 100644 index b2b74e5a1..000000000 --- a/src/idom/dialect.py +++ /dev/null @@ -1,174 +0,0 @@ -""" -HTML Language Extension -======================= -""" - -import ast -from typing import Any, List, Optional, Tuple, Union - -import htm -from pyalect import Dialect, DialectError - - -class HtmlDialectTranspiler(Dialect, name="html"): - """An HTML dialect transpiler for Python.""" - - def __init__(self, filename: Optional[str] = None): - self.filename: str = filename or "" - - def transform_src(self, source: str) -> str: - return source - - def transform_ast(self, node: ast.AST) -> ast.AST: - new_node: ast.AST = HtmlDialectNodeTransformer(self.filename).visit(node) - return new_node - - -class HtmlDialectNodeTransformer(ast.NodeTransformer): - def __init__(self, filename: str): - super().__init__() - self.filename = filename - - def visit_Call(self, node: ast.Call) -> Optional[ast.AST]: - if isinstance(node.func, ast.Name): - if node.func.id == "html": - if ( - not node.keywords - and len(node.args) == 1 - and isinstance(node.args[0], ast.JoinedStr) - ): - try: - new_node = self._transform_string(node.args[0]) - except htm.ParseError as error: - raise DialectError(str(error), self.filename, node.lineno) - return self.generic_visit( - ast.fix_missing_locations(ast.copy_location(new_node, node)) - ) - return node - - def _transform_string(self, node: ast.JoinedStr) -> ast.Call: - htm_strings: List[str] = [] - exp_nodes: List[ast.AST] = [] - for inner_node in node.values: - if isinstance(inner_node, ast.Str): - htm_strings.append(inner_node.s) - elif isinstance(inner_node, ast.FormattedValue): - if len(htm_strings) == len(exp_nodes): - htm_strings.append("") - if inner_node.conversion != -1 or inner_node.format_spec: - exp_nodes.append(ast.JoinedStr([inner_node])) - else: - exp_nodes.append(inner_node.value) - - call_stack = _HtmlCallStack() - for op_type, *data in htm.htm_parse(htm_strings): - getattr(self, f"_transform_htm_{op_type.lower()}")( - exp_nodes, call_stack, *data - ) - return call_stack.finish() - - def _transform_htm_open( - self, - exp_nodes: List[ast.AST], - call_stack: "_HtmlCallStack", - is_index: bool, - tag_or_index: Union[str, int], - ) -> None: - if isinstance(tag_or_index, int): - call_stack.begin_child(exp_nodes[tag_or_index]) - else: - call_stack.begin_child(ast.Str(tag_or_index)) - - def _transform_htm_close( - self, exp_nodes: List[ast.AST], call_stack: "_HtmlCallStack" - ) -> None: - call_stack.end_child() - - def _transform_htm_spread( - self, exp_nodes: List[ast.AST], call_stack: "_HtmlCallStack", _: Any, index: int - ) -> None: - call_stack.add_attributes(None, exp_nodes[index]) - - def _transform_htm_prop_single( - self, - exp_nodes: List[ast.AST], - call_stack: "_HtmlCallStack", - attr: str, - is_index: bool, - value_or_index: Union[str, int], - ) -> None: - if isinstance(value_or_index, bool): - const = ast.NameConstant(value_or_index) - call_stack.add_attributes(ast.Str(attr), const) - elif isinstance(value_or_index, int): - call_stack.add_attributes(ast.Str(attr), exp_nodes[value_or_index]) - else: - call_stack.add_attributes(ast.Str(attr), ast.Str(value_or_index)) - - def _transform_htm_prop_multi( - self, - exp_nodes: List[ast.AST], - call_stack: "_HtmlCallStack", - attr: str, - items: Tuple[Tuple[bool, Union[str, int]]], - ) -> None: - op_root = current_op = ast.BinOp(None, None, None) - for _, value_or_index in items: - if isinstance(value_or_index, str): - current_op.right = ast.BinOp(ast.Str(value_or_index), ast.Add(), None) - else: - current_op.right = ast.BinOp(exp_nodes[value_or_index], ast.Add(), None) - last_op = current_op - current_op = current_op.right - last_op.right = current_op.left - call_stack.add_attributes(ast.Str(attr), op_root.right) - - def _transform_htm_child( - self, - exp_nodes: List[ast.AST], - call_stack: "_HtmlCallStack", - is_index: bool, - child_or_index: Union[str, int], - ) -> None: - if isinstance(child_or_index, int): - call_stack.add_child(exp_nodes[child_or_index]) - else: - call_stack.add_child(ast.Str(child_or_index)) - - -class _HtmlCallStack: - def __init__(self) -> None: - self._root = self._new(ast.Str()) - self._stack: List[ast.Call] = [self._root] - - def begin_child(self, tag: ast.AST) -> None: - new = self._new(tag) - last = self._stack[-1] - children = last.args[2].elts # type: ignore - children.append(new) - self._stack.append(new) - - def add_child(self, child: ast.AST) -> None: - current = self._stack[-1] - children = current.args[2].elts # type: ignore - children.append(child) - - def add_attributes(self, key: Optional[ast.Str], value: ast.AST) -> None: - current = self._stack[-1] - attributes: ast.Dict = current.args[1] # type: ignore - attributes.keys.append(key) - attributes.values.append(value) # type: ignore - - def end_child(self) -> None: - self._stack.pop(-1) - - def finish(self) -> ast.Call: - root = self._root - self._root = self._new(ast.Str()) - self._stack.clear() - return root.args[2].elts[0] # type: ignore - - @staticmethod - def _new(tag: ast.AST) -> ast.Call: - args = [tag, ast.Dict([], []), ast.List([], ast.Load())] - return ast.Call(ast.Name("html", ast.Load()), args, []) diff --git a/src/idom/html.py b/src/idom/html.py new file mode 100644 index 000000000..b357fafec --- /dev/null +++ b/src/idom/html.py @@ -0,0 +1,218 @@ +""" +Standard HTML Elements +====================== + + +External sources +---------------- + +link = make_vdom_constructor("link", allow_children=False) + + +Content Sectioning +------------------ + +- :func:`style` +- :func:`address` +- :func:`article` +- :func:`aside` +- :func:`footer` +- :func:`h1` +- :func:`h2` +- :func:`h3` +- :func:`h4` +- :func:`h5` +- :func:`h6` +- :func:`header` +- :func:`hgroup` +- :func:`nav` +- :func:`section` + + +Text Content +------------ +- :func:`blockquote` +- :func:`dd` +- :func:`div` +- :func:`dl` +- :func:`dt` +- :func:`figcaption` +- :func:`figure` +- :func:`hr` +- :func:`li` +- :func:`ol` +- :func:`p` +- :func:`pre` +- :func:`ul` + + +Inline Text Semantics +--------------------- + +- :func:`a` +- :func:`abbr` +- :func:`b` +- :func:`br` +- :func:`cite` +- :func:`code` +- :func:`data` +- :func:`em` +- :func:`i` +- :func:`kbd` +- :func:`mark` +- :func:`q` +- :func:`s` +- :func:`samp` +- :func:`small` +- :func:`span` +- :func:`strong` +- :func:`sub` +- :func:`sup` +- :func:`time` +- :func:`u` +- :func:`var` + + +Image and video +--------------- + +- :func:`img` +- :func:`audio` +- :func:`video` +- :func:`source` + + +Table Content +------------- + +- :func:`caption` +- :func:`col` +- :func:`colgroup` +- :func:`table` +- :func:`tbody` +- :func:`td` +- :func:`tfoot` +- :func:`th` +- :func:`thead` +- :func:`tr` + + +Forms +----- + +- :func:`meter` +- :func:`output` +- :func:`progress` +- :func:`input` +- :func:`button` +- :func:`label` +- :func:`fieldset` +- :func:`legend` + + +Interactive Elements +-------------------- + +- :func:`details` +- :func:`dialog` +- :func:`menu` +- :func:`menuitem` +- :func:`summary` +""" + +from .core.vdom import make_vdom_constructor + + +# External sources +link = make_vdom_constructor("link", allow_children=False) + +# Content sectioning +style = make_vdom_constructor("style") +address = make_vdom_constructor("address") +article = make_vdom_constructor("article") +aside = make_vdom_constructor("aside") +footer = make_vdom_constructor("footer") +h1 = make_vdom_constructor("h1") +h2 = make_vdom_constructor("h2") +h3 = make_vdom_constructor("h3") +h4 = make_vdom_constructor("h4") +h5 = make_vdom_constructor("h5") +h6 = make_vdom_constructor("h6") +header = make_vdom_constructor("header") +hgroup = make_vdom_constructor("hgroup") +nav = make_vdom_constructor("nav") +section = make_vdom_constructor("section") + +# Text content +blockquote = make_vdom_constructor("blockquote") +dd = make_vdom_constructor("dd") +div = make_vdom_constructor("div") +dl = make_vdom_constructor("dl") +dt = make_vdom_constructor("dt") +figcaption = make_vdom_constructor("figcaption") +figure = make_vdom_constructor("figure") +hr = make_vdom_constructor("hr", allow_children=False) +li = make_vdom_constructor("li") +ol = make_vdom_constructor("ol") +p = make_vdom_constructor("p") +pre = make_vdom_constructor("pre") +ul = make_vdom_constructor("ul") + +# Inline text semantics +a = make_vdom_constructor("a") +abbr = make_vdom_constructor("abbr") +b = make_vdom_constructor("b") +br = make_vdom_constructor("br", allow_children=False) +cite = make_vdom_constructor("cite") +code = make_vdom_constructor("code") +data = make_vdom_constructor("data") +em = make_vdom_constructor("em") +i = make_vdom_constructor("i") +kbd = make_vdom_constructor("kbd") +mark = make_vdom_constructor("mark") +q = make_vdom_constructor("q") +s = make_vdom_constructor("s") +samp = make_vdom_constructor("samp") +small = make_vdom_constructor("small") +span = make_vdom_constructor("span") +strong = make_vdom_constructor("strong") +sub = make_vdom_constructor("sub") +sup = make_vdom_constructor("sup") +time = make_vdom_constructor("time") +u = make_vdom_constructor("u") +var = make_vdom_constructor("var") + +# Image and video +img = make_vdom_constructor("img", allow_children=False) +audio = make_vdom_constructor("audio") +video = make_vdom_constructor("video") +source = make_vdom_constructor("source", allow_children=False) + +# Table content +caption = make_vdom_constructor("caption") +col = make_vdom_constructor("col") +colgroup = make_vdom_constructor("colgroup") +table = make_vdom_constructor("table") +tbody = make_vdom_constructor("tbody") +td = make_vdom_constructor("td") +tfoot = make_vdom_constructor("tfoot") +th = make_vdom_constructor("th") +thead = make_vdom_constructor("thead") +tr = make_vdom_constructor("tr") + +# Forms +meter = make_vdom_constructor("meter") +output = make_vdom_constructor("output") +progress = make_vdom_constructor("progress") +input = make_vdom_constructor("input", allow_children=False) +button = make_vdom_constructor("button") +label = make_vdom_constructor("label") +fieldset = make_vdom_constructor("fieldset") +legend = make_vdom_constructor("legend") + +# Interactive elements +details = make_vdom_constructor("details") +dialog = make_vdom_constructor("dialog") +menu = make_vdom_constructor("menu") +menuitem = make_vdom_constructor("menuitem") +summary = make_vdom_constructor("summary") diff --git a/src/idom/server/fastapi.py b/src/idom/server/fastapi.py index 844567a91..fcd27172f 100644 --- a/src/idom/server/fastapi.py +++ b/src/idom/server/fastapi.py @@ -23,7 +23,7 @@ from uvicorn.supervisors.multiprocess import Multiprocess from uvicorn.supervisors.statreload import StatReload as ChangeReload -from idom.config import IDOM_CLIENT_BUILD_DIR +from idom.config import IDOM_WED_MODULES_DIR from idom.core.component import ComponentConstructor from idom.core.dispatcher import ( RecvCoroutine, @@ -34,7 +34,7 @@ ) from idom.core.layout import Layout, LayoutEvent, LayoutUpdate -from .utils import poll, threaded +from .utils import CLIENT_BUILD_DIR, poll, threaded logger = logging.getLogger(__name__) @@ -195,7 +195,16 @@ def _setup_common_routes(app: FastAPI, router: APIRouter, config: Config) -> Non app.mount( f"{url_prefix}/client", StaticFiles( - directory=str(IDOM_CLIENT_BUILD_DIR.current), + directory=str(CLIENT_BUILD_DIR), + html=True, + check_dir=True, + ), + name="idom_static_files", + ) + app.mount( + f"{url_prefix}/modules", + StaticFiles( + directory=str(IDOM_WED_MODULES_DIR.current), html=True, check_dir=True, ), diff --git a/src/idom/server/flask.py b/src/idom/server/flask.py index b5e1696c6..3f58a0d20 100644 --- a/src/idom/server/flask.py +++ b/src/idom/server/flask.py @@ -23,12 +23,12 @@ from typing_extensions import TypedDict import idom -from idom.config import IDOM_CLIENT_BUILD_DIR, IDOM_DEBUG_MODE +from idom.config import IDOM_DEBUG_MODE, IDOM_WED_MODULES_DIR from idom.core.component import AbstractComponent, ComponentConstructor from idom.core.dispatcher import dispatch_single_view from idom.core.layout import LayoutEvent, LayoutUpdate -from .utils import threaded, wait_on_event +from .utils import CLIENT_BUILD_DIR, threaded, wait_on_event logger = logging.getLogger(__name__) @@ -136,8 +136,12 @@ def _setup_common_routes(blueprint: Blueprint, config: Config) -> None: if config["serve_static_files"]: @blueprint.route("/client/") - def send_build_dir(path: str) -> Any: - return send_from_directory(str(IDOM_CLIENT_BUILD_DIR.current), path) + def send_client_dir(path: str) -> Any: + return send_from_directory(str(CLIENT_BUILD_DIR), path) + + @blueprint.route("/modules/") + def send_modules_dir(path: str) -> Any: + return send_from_directory(str(IDOM_WED_MODULES_DIR.current), path) if config["redirect_root_to_index"]: @@ -145,7 +149,7 @@ def send_build_dir(path: str) -> Any: def redirect_to_index() -> Any: return redirect( url_for( - "idom.send_build_dir", + "idom.send_client_dir", path="index.html", **request.args, ) diff --git a/src/idom/server/prefab.py b/src/idom/server/prefab.py index cc9adfca0..f11616611 100644 --- a/src/idom/server/prefab.py +++ b/src/idom/server/prefab.py @@ -7,7 +7,7 @@ from typing import Any, Dict, Optional, Tuple, TypeVar from idom.core.component import ComponentConstructor -from idom.widgets.utils import MountFunc, MultiViewMount, hotswap, multiview +from idom.widgets import MountFunc, MultiViewMount, hotswap, multiview from .proto import Server, ServerFactory from .utils import find_available_port, find_builtin_server_type diff --git a/src/idom/server/sanic.py b/src/idom/server/sanic.py index 71d5d931e..3a379c33a 100644 --- a/src/idom/server/sanic.py +++ b/src/idom/server/sanic.py @@ -17,7 +17,7 @@ from sanic_cors import CORS from websockets import WebSocketCommonProtocol -from idom.config import IDOM_CLIENT_BUILD_DIR +from idom.config import IDOM_WED_MODULES_DIR from idom.core.component import ComponentConstructor from idom.core.dispatcher import ( RecvCoroutine, @@ -28,11 +28,13 @@ ) from idom.core.layout import Layout, LayoutEvent, LayoutUpdate -from .utils import threaded, wait_on_event +from .utils import CLIENT_BUILD_DIR, threaded, wait_on_event logger = logging.getLogger(__name__) +_SERVER_COUNT = 0 + class Config(TypedDict, total=False): """Config for :class:`SanicRenderServer`""" @@ -153,6 +155,10 @@ def _setup_config_and_app( config: Optional[Config], app: Optional[Sanic], ) -> Tuple[Config, Sanic]: + if app is None: + global _SERVER_COUNT + _SERVER_COUNT += 1 + app = Sanic(f"{__name__}[{_SERVER_COUNT}]") return ( { "cors": False, @@ -161,7 +167,7 @@ def _setup_config_and_app( "redirect_root_to_index": True, **(config or {}), # type: ignore }, - app or Sanic(), + app, ) @@ -172,7 +178,8 @@ def _setup_common_routes(blueprint: Blueprint, config: Config) -> None: CORS(blueprint, **cors_params) if config["serve_static_files"]: - blueprint.static("/client", str(IDOM_CLIENT_BUILD_DIR.current)) + blueprint.static("/client", str(CLIENT_BUILD_DIR)) + blueprint.static("/modules", str(IDOM_WED_MODULES_DIR.current)) if config["redirect_root_to_index"]: diff --git a/src/idom/server/tornado.py b/src/idom/server/tornado.py index 46d29dd78..6eb419ad1 100644 --- a/src/idom/server/tornado.py +++ b/src/idom/server/tornado.py @@ -18,12 +18,12 @@ from tornado.websocket import WebSocketHandler from typing_extensions import TypedDict -from idom.config import IDOM_CLIENT_BUILD_DIR +from idom.config import IDOM_WED_MODULES_DIR from idom.core.component import ComponentConstructor from idom.core.dispatcher import dispatch_single_view from idom.core.layout import Layout, LayoutEvent, LayoutUpdate -from .utils import threaded, wait_on_event +from .utils import CLIENT_BUILD_DIR, threaded, wait_on_event _RouteHandlerSpecs = List[Tuple[str, Type[RequestHandler], Any]] @@ -126,7 +126,14 @@ def _setup_common_routes(config: Config) -> _RouteHandlerSpecs: ( r"/client/(.*)", StaticFileHandler, - {"path": str(IDOM_CLIENT_BUILD_DIR.current)}, + {"path": str(CLIENT_BUILD_DIR)}, + ) + ) + handlers.append( + ( + r"/modules/(.*)", + StaticFileHandler, + {"path": str(IDOM_WED_MODULES_DIR.current)}, ) ) if config["redirect_root_to_index"]: diff --git a/src/idom/server/utils.py b/src/idom/server/utils.py index 781e523f9..410bd7cc4 100644 --- a/src/idom/server/utils.py +++ b/src/idom/server/utils.py @@ -3,13 +3,18 @@ from contextlib import closing from functools import wraps from importlib import import_module +from pathlib import Path from socket import socket from threading import Event, Thread from typing import Any, Callable, List, Optional, TypeVar, cast +import idom + from .proto import ServerFactory +CLIENT_BUILD_DIR = Path(idom.__file__).parent / "client" / "build" + _SUPPORTED_PACKAGES = [ "sanic", "fastapi", @@ -17,7 +22,6 @@ "tornado", ] - _Func = TypeVar("_Func", bound=Callable[..., None]) diff --git a/src/idom/testing.py b/src/idom/testing.py index 02a8b6edc..4d703867b 100644 --- a/src/idom/testing.py +++ b/src/idom/testing.py @@ -5,6 +5,7 @@ import logging import re +import shutil from functools import wraps from types import TracebackType from typing import ( @@ -25,6 +26,7 @@ from selenium.webdriver import Chrome from selenium.webdriver.remote.webdriver import WebDriver +from idom.config import IDOM_WED_MODULES_DIR from idom.core.events import EventHandler from idom.core.hooks import LifeCycleHook, current_hook from idom.core.utils import hex_id @@ -43,8 +45,8 @@ def create_simple_selenium_web_driver( driver_type: Type[WebDriver] = Chrome, driver_options: Optional[Any] = None, - implicit_wait_timeout: float = 3.0, - page_load_timeout: float = 3.0, + implicit_wait_timeout: float = 5.0, + page_load_timeout: float = 5.0, window_size: Tuple[int, int] = (1080, 800), ) -> WebDriver: driver = driver_type(options=driver_options) @@ -285,3 +287,8 @@ def use(self, function: Callable[..., Any]) -> EventHandler: self._handler.clear() self._handler.add(function) return self._handler + + +def clear_idom_web_modules_dir() -> None: + for path in IDOM_WED_MODULES_DIR.current.iterdir(): + shutil.rmtree(path) if path.is_dir() else path.unlink() diff --git a/src/idom/web/__init__.py b/src/idom/web/__init__.py new file mode 100644 index 000000000..d3187366c --- /dev/null +++ b/src/idom/web/__init__.py @@ -0,0 +1,9 @@ +from .module import export, module_from_file, module_from_template, module_from_url + + +__all__ = [ + "module_from_file", + "module_from_template", + "module_from_url", + "export", +] diff --git a/src/idom/web/module.py b/src/idom/web/module.py new file mode 100644 index 000000000..d6ed49117 --- /dev/null +++ b/src/idom/web/module.py @@ -0,0 +1,253 @@ +""" +Web Modules +=========== +""" + +from __future__ import annotations + +import shutil +from dataclasses import dataclass +from functools import partial +from pathlib import Path +from typing import Any, List, NewType, Optional, Set, Tuple, Union, overload + +from idom.config import IDOM_DEBUG_MODE, IDOM_WED_MODULES_DIR +from idom.core.vdom import ImportSourceDict, VdomDictConstructor, make_vdom_constructor + +from .utils import ( + module_name_suffix, + resolve_module_exports_from_file, + resolve_module_exports_from_url, +) + + +SourceType = NewType("SourceType", str) + +NAME_SOURCE = SourceType("NAME") +"""A named souce - usually a Javascript package name""" + +URL_SOURCE = SourceType("URL") +"""A source loaded from a URL, usually a CDN""" + + +def module_from_url( + url: str, + fallback: Optional[Any] = None, + resolve_exports: bool = IDOM_DEBUG_MODE.current, + resolve_exports_depth: int = 5, +) -> WebModule: + """Load a :class:`WebModule` from a :data:`URL_SOURCE` + + Parameters: + url: + Where the javascript module will be loaded from which conforms to the + interface for :ref:`Custom Javascript Components` + fallback: + What to temporarilly display while the module is being loaded. + resolve_imports: + Whether to try and find all the named exports of this module. + resolve_exports_depth: + How deeply to search for those exports. + """ + return WebModule( + source=url, + source_type=URL_SOURCE, + default_fallback=fallback, + file=None, + export_names=( + resolve_module_exports_from_url(url, resolve_exports_depth) + if resolve_exports + else None + ), + ) + + +def module_from_template( + template: str, + package: str, + cdn: str = "https://esm.sh", + fallback: Optional[Any] = None, + resolve_exports: bool = IDOM_DEBUG_MODE.current, + resolve_exports_depth: int = 5, +) -> WebModule: + """Load a :class:`WebModule` from a :data:`URL_SOURCE` using a known framework + + Parameters: + template: + The name of the template to use with the given ``package`` (``react`` | ``preact``) + package: + The name of a package to load. May include a file extension (defaults to + ``.js`` if not given) + cdn: + Where the package should be loaded from. The CDN must distribute ESM modules + fallback: + What to temporarilly display while the module is being loaded. + resolve_imports: + Whether to try and find all the named exports of this module. + resolve_exports_depth: + How deeply to search for those exports. + """ + cdn = cdn.rstrip("/") + + template_file_name = f"{template}{module_name_suffix(package)}" + template_file = Path(__file__).parent / "templates" / template_file_name + if not template_file.exists(): + raise ValueError(f"No template for {template_file_name!r} exists") + + target_file = _web_module_path(package) + if not target_file.exists(): + target_file.parent.mkdir(parents=True, exist_ok=True) + target_file.write_text( + template_file.read_text().replace("$PACKAGE", package).replace("$CDN", cdn) + ) + + return WebModule( + source=package + module_name_suffix(package), + source_type=NAME_SOURCE, + default_fallback=fallback, + file=target_file, + export_names=( + resolve_module_exports_from_url(f"{cdn}/{package}", resolve_exports_depth) + if resolve_exports + else None + ), + ) + + +def module_from_file( + name: str, + file: Union[str, Path], + fallback: Optional[Any] = None, + resolve_exports: bool = IDOM_DEBUG_MODE.current, + resolve_exports_depth: int = 5, + symlink: bool = False, +) -> WebModule: + """Load a :class:`WebModule` from a :data:`URL_SOURCE` using a known framework + + Parameters: + template: + The name of the template to use with the given ``package`` + package: + The name of a package to load. May include a file extension (defaults to + ``.js`` if not given) + cdn: + Where the package should be loaded from. The CDN must distribute ESM modules + fallback: + What to temporarilly display while the module is being loaded. + resolve_imports: + Whether to try and find all the named exports of this module. + resolve_exports_depth: + How deeply to search for those exports. + """ + source_file = Path(file) + target_file = _web_module_path(name) + if not source_file.exists(): + raise FileNotFoundError(f"Source file does not exist: {source_file}") + elif target_file.exists() or target_file.is_symlink(): + raise FileExistsError(f"{name!r} already exists as {target_file.resolve()}") + else: + target_file.parent.mkdir(parents=True, exist_ok=True) + if symlink: + target_file.symlink_to(source_file) + else: + shutil.copy(source_file, target_file) + return WebModule( + source=name + module_name_suffix(name), + source_type=NAME_SOURCE, + default_fallback=fallback, + file=target_file, + export_names=( + resolve_module_exports_from_file(source_file, resolve_exports_depth) + if resolve_exports + else None + ), + ) + + +@dataclass(frozen=True) +class WebModule: + source: str + source_type: SourceType + default_fallback: Optional[Any] + export_names: Optional[Set[str]] + file: Optional[Path] + + +@overload +def export( + web_module: WebModule, + export_names: str, + fallback: Optional[Any], + allow_children: bool, +) -> VdomDictConstructor: + ... + + +@overload +def export( + web_module: WebModule, + export_names: Union[List[str], Tuple[str]], + fallback: Optional[Any], + allow_children: bool, +) -> List[VdomDictConstructor]: + ... + + +def export( + web_module: WebModule, + export_names: Union[str, List[str], Tuple[str]], + fallback: Optional[Any] = None, + allow_children: bool = True, +) -> Union[VdomDictConstructor, List[VdomDictConstructor]]: + """Return one or more VDOM constructors from a :class:`WebModule` + + Parameters: + export_names: + One or more names to export. If given as a string, a single component + will be returned. If a list is given, then a list of components will be + returned. + fallback: + What to temporarilly display while the module is being loaded. + allow_children: + Whether or not these components can have children. + """ + if isinstance(export_names, str): + if ( + web_module.export_names is not None + and export_names not in web_module.export_names + ): + raise ValueError(f"{web_module.source!r} does not export {export_names!r}") + return _make_export(web_module, export_names, fallback, allow_children) + else: + if web_module.export_names is not None: + missing = list( + sorted(set(export_names).difference(web_module.export_names)) + ) + if missing: + raise ValueError(f"{web_module.source!r} does not export {missing!r}") + return [ + _make_export(web_module, name, fallback, allow_children) + for name in export_names + ] + + +def _make_export( + web_module: WebModule, name: str, fallback: Optional[Any], allow_children: bool +) -> VdomDictConstructor: + return partial( + make_vdom_constructor( + name, + allow_children=allow_children, + ), + import_source=ImportSourceDict( + source=web_module.source, + sourceType=web_module.source_type, + fallback=(fallback or web_module.default_fallback), + ), + ) + + +def _web_module_path(name: str) -> Path: + name += module_name_suffix(name) + path = IDOM_WED_MODULES_DIR.current.joinpath(*name.split("/")) + return path.with_suffix(path.suffix) diff --git a/src/idom/web/templates/preact.js b/src/idom/web/templates/preact.js new file mode 100644 index 000000000..773463137 --- /dev/null +++ b/src/idom/web/templates/preact.js @@ -0,0 +1,9 @@ +import { render } from "$CDN/preact"; + +export { h as createElement, render as renderElement } from "$CDN/preact"; + +export function unmountElement(container) { + render(null, container); +} + +export * from "$CDN/$PACKAGE"; diff --git a/src/idom/web/templates/react.js b/src/idom/web/templates/react.js new file mode 100644 index 000000000..71c3eff1a --- /dev/null +++ b/src/idom/web/templates/react.js @@ -0,0 +1,9 @@ +export * from "$CDN/$PACKAGE"; + +import * as react from "$CDN/react"; +import * as reactDOM from "$CDN/react-dom"; + +export const createElement = (component, props) => + react.createElement(component, props); +export const renderElement = reactDOM.render; +export const unmountElement = reactDOM.unmountComponentAtNode; diff --git a/src/idom/web/utils.py b/src/idom/web/utils.py new file mode 100644 index 000000000..7e7c62895 --- /dev/null +++ b/src/idom/web/utils.py @@ -0,0 +1,128 @@ +import logging +import re +from pathlib import Path, PurePosixPath +from typing import Set, Tuple +from urllib.parse import urlparse + +import requests + + +logger = logging.getLogger(__name__) + + +def module_name_suffix(name: str) -> str: + head, _, tail = name.partition("@") # handle version identifier + version, _, tail = tail.partition("/") # get section after version + return PurePosixPath(tail or head).suffix or ".js" + + +def resolve_module_exports_from_file(file: Path, max_depth: int) -> Set[str]: + if max_depth == 0: + logger.warning(f"Did not resolve all exports for {file} - max depth reached") + return set() + elif not file.exists(): + logger.warning(f"Did not resolve exports for unknown file {file}") + return set() + + export_names, references = resolve_module_exports_from_source(file.read_text()) + + for ref in references: + if urlparse(ref).scheme: # is an absolute URL + export_names.update(resolve_module_exports_from_url(ref, max_depth - 1)) + else: + path = file.parent.joinpath(*ref.split("/")) + export_names.update(resolve_module_exports_from_file(path, max_depth - 1)) + + return export_names + + +def resolve_module_exports_from_url(url: str, max_depth: int) -> Set[str]: + if max_depth == 0: + logger.warning(f"Did not resolve all exports for {url} - max depth reached") + return set() + + try: + text = requests.get(url).text + except requests.exceptions.ConnectionError as error: + reason = "" if error is None else " - {error.errno}" + logger.warning("Did not resolve exports for url " + url + reason) + return set() + + export_names, references = resolve_module_exports_from_source(text) + + for ref in references: + url = _resolve_relative_url(url, ref) + export_names.update(resolve_module_exports_from_url(url, max_depth - 1)) + + return export_names + + +def resolve_module_exports_from_source(content: str) -> Tuple[Set[str], Set[str]]: + names: Set[str] = set() + references: Set[str] = set() + + if _JS_DEFAULT_EXPORT_PATTERN.search(content): + names.add("default") + + # Exporting functions and classes + names.update(_JS_FUNC_OR_CLS_EXPORT_PATTERN.findall(content)) + + for export in _JS_GENERAL_EXPORT_PATTERN.findall(content): + export = export.rstrip(";").strip() + # Exporting individual features + if export.startswith("let "): + names.update(let.split("=", 1)[0] for let in export[4:].split(",")) + # Renaming exports and export list + elif export.startswith("{") and export.endswith("}"): + names.update( + item.split(" as ", 1)[-1] for item in export.strip("{}").split(",") + ) + # Exporting destructured assignments with renaming + elif export.startswith("const "): + names.update( + item.split(":", 1)[0] + for item in export[6:].split("=", 1)[0].strip("{}").split(",") + ) + # Default exports + elif export.startswith("default "): + names.add("default") + # Aggregating modules + elif export.startswith("* as "): + names.add(export[5:].split(" from ", 1)[0]) + elif export.startswith("* "): + references.add(export[2:].split("from ", 1)[-1].strip("'\"")) + elif export.startswith("{") and " from " in export: + names.update( + item.split(" as ", 1)[-1] + for item in export.split(" from ")[0].strip("{}").split(",") + ) + elif not (export.startswith("function ") or export.startswith("class ")): + logger.warning(f"Unknown export type {export!r}") + return {n.strip() for n in names}, {r.strip() for r in references} + + +def _resolve_relative_url(base_url: str, rel_url: str) -> str: + if not rel_url.startswith("."): + return rel_url + + base_url = base_url.rsplit("/", 1)[0] + + if rel_url.startswith("./"): + return base_url + rel_url[1:] + + while rel_url.startswith("../"): + base_url = base_url.rsplit("/", 1)[0] + rel_url = rel_url[3:] + + return f"{base_url}/{rel_url}" + + +_JS_DEFAULT_EXPORT_PATTERN = re.compile( + r";?\s*export\s+default\s", +) +_JS_FUNC_OR_CLS_EXPORT_PATTERN = re.compile( + r";?\s*export\s+(?:function|class)\s+([a-zA-Z_$][0-9a-zA-Z_$]*)" +) +_JS_GENERAL_EXPORT_PATTERN = re.compile( + r";?\s*export(?=\s+|{)(.*?)(?:;|$)", re.MULTILINE +) diff --git a/src/idom/widgets/utils.py b/src/idom/widgets.py similarity index 78% rename from src/idom/widgets/utils.py rename to src/idom/widgets.py index d61d73fcc..fe3922477 100644 --- a/src/idom/widgets/utils.py +++ b/src/idom/widgets.py @@ -1,14 +1,68 @@ """ -Widget Tools -============ +Widgets +======= """ from __future__ import annotations -from typing import Any, Callable, Dict, Optional, Set, Tuple, TypeVar +from base64 import b64encode +from typing import Any, Callable, Dict, Optional, Set, Tuple, TypeVar, Union -from idom.core import hooks -from idom.core.component import ComponentConstructor, component -from idom.utils import Ref +import idom + +from . import html +from .core import hooks +from .core.component import ComponentConstructor, component +from .core.vdom import VdomDict +from .utils import Ref + + +def image( + format: str, + value: Union[str, bytes] = "", + attributes: Optional[Dict[str, Any]] = None, +) -> VdomDict: + """Utility for constructing an image from a string or bytes + + The source value will automatically be encoded to base64 + """ + if format == "svg": + format = "svg+xml" + + if isinstance(value, str): + bytes_value = value.encode() + else: + bytes_value = value + + base64_value = b64encode(bytes_value).decode() + src = f"data:image/{format};base64,{base64_value}" + + return {"tagName": "img", "attributes": {"src": src, **(attributes or {})}} + + +@component +def Input( + callback: Callable[[str], None], + type: str, + value: str = "", + attributes: Optional[Dict[str, Any]] = None, + cast: Optional[Callable[[str], Any]] = None, + ignore_empty: bool = True, +) -> VdomDict: + """Utility for making an ```` with a callback""" + attrs = attributes or {} + value, set_value = idom.hooks.use_state(value) + + events = idom.Events() + + @events.on("change") + def on_change(event: Dict[str, Any]) -> None: + value = event["value"] + set_value(value) + if not value and ignore_empty: + return + callback(value if cast is None else cast(value)) + + return html.input({"type": type, "value": value, **attrs}, event_handlers=events) MountFunc = Callable[[ComponentConstructor], None] diff --git a/src/idom/widgets/__init__.py b/src/idom/widgets/__init__.py deleted file mode 100644 index dbb88d0d9..000000000 --- a/src/idom/widgets/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from .html import Input, html, image -from .utils import hotswap, multiview - - -__all__ = [ - "node", - "hotswap", - "multiview", - "html", - "image", - "Input", -] diff --git a/src/idom/widgets/html.py b/src/idom/widgets/html.py deleted file mode 100644 index 3f72c62ef..000000000 --- a/src/idom/widgets/html.py +++ /dev/null @@ -1,209 +0,0 @@ -""" -HTML Widgets -============ -""" - -from base64 import b64encode -from typing import Any, Callable, Dict, Optional, Union, overload - -import idom -from idom.core.component import AbstractComponent, ComponentConstructor, component -from idom.core.vdom import ( - VdomDict, - VdomDictConstructor, - coalesce_attributes_and_children, - make_vdom_constructor, - vdom, -) - - -def image( - format: str, - value: Union[str, bytes] = "", - attributes: Optional[Dict[str, Any]] = None, -) -> VdomDict: - """Utility for constructing an image from a string or bytes - - The source value will automatically be encoded to base64 - """ - if format == "svg": - format = "svg+xml" - - if isinstance(value, str): - bytes_value = value.encode() - else: - bytes_value = value - - base64_value = b64encode(bytes_value).decode() - src = f"data:image/{format};base64,{base64_value}" - - return {"tagName": "img", "attributes": {"src": src, **(attributes or {})}} - - -@component -def Input( - callback: Callable[[str], None], - type: str, - value: str = "", - attributes: Optional[Dict[str, Any]] = None, - cast: Optional[Callable[[str], Any]] = None, - ignore_empty: bool = True, -) -> VdomDict: - """Utility for making an ```` with a callback""" - attrs = attributes or {} - value, set_value = idom.hooks.use_state(value) - - events = idom.Events() - - @events.on("change") - def on_change(event: Dict[str, Any]) -> None: - value = event["value"] - set_value(value) - if not value and ignore_empty: - return - callback(value if cast is None else cast(value)) - - return html.input({"type": type, "value": value, **attrs}, event_handlers=events) - - -class Html: - """Utility for making basic HTML elements - - Many basic elements already have constructors, however accessing an attribute - of any name on this object will return a constructor for an element with the - same ``tagName``. - - All constructors return :class:`~idom.core.vdom.VdomDict`. - """ - - def __init__(self) -> None: - # External sources - self.link = make_vdom_constructor("link", allow_children=False) - - # Content sectioning - self.style = make_vdom_constructor("style") - self.address = make_vdom_constructor("address") - self.article = make_vdom_constructor("article") - self.aside = make_vdom_constructor("aside") - self.footer = make_vdom_constructor("footer") - self.h1 = make_vdom_constructor("h1") - self.h2 = make_vdom_constructor("h2") - self.h3 = make_vdom_constructor("h3") - self.h4 = make_vdom_constructor("h4") - self.h5 = make_vdom_constructor("h5") - self.h6 = make_vdom_constructor("h6") - self.header = make_vdom_constructor("header") - self.hgroup = make_vdom_constructor("hgroup") - self.nav = make_vdom_constructor("nav") - self.section = make_vdom_constructor("section") - - # Text content - self.blockquote = make_vdom_constructor("blockquote") - self.dd = make_vdom_constructor("dd") - self.div = make_vdom_constructor("div") - self.dl = make_vdom_constructor("dl") - self.dt = make_vdom_constructor("dt") - self.figcaption = make_vdom_constructor("figcaption") - self.figure = make_vdom_constructor("figure") - self.hr = make_vdom_constructor("hr", allow_children=False) - self.li = make_vdom_constructor("li") - self.ol = make_vdom_constructor("ol") - self.p = make_vdom_constructor("p") - self.pre = make_vdom_constructor("pre") - self.ul = make_vdom_constructor("ul") - - # Inline text semantics - self.a = make_vdom_constructor("a") - self.abbr = make_vdom_constructor("abbr") - self.b = make_vdom_constructor("b") - self.br = make_vdom_constructor("br", allow_children=False) - self.cite = make_vdom_constructor("cite") - self.code = make_vdom_constructor("code") - self.data = make_vdom_constructor("data") - self.em = make_vdom_constructor("em") - self.i = make_vdom_constructor("i") - self.kbd = make_vdom_constructor("kbd") - self.mark = make_vdom_constructor("mark") - self.q = make_vdom_constructor("q") - self.s = make_vdom_constructor("s") - self.samp = make_vdom_constructor("samp") - self.small = make_vdom_constructor("small") - self.span = make_vdom_constructor("span") - self.strong = make_vdom_constructor("strong") - self.sub = make_vdom_constructor("sub") - self.sup = make_vdom_constructor("sup") - self.time = make_vdom_constructor("time") - self.u = make_vdom_constructor("u") - self.var = make_vdom_constructor("var") - - # Image and video - self.img = make_vdom_constructor("img", allow_children=False) - self.audio = make_vdom_constructor("audio") - self.video = make_vdom_constructor("video") - self.source = make_vdom_constructor("source", allow_children=False) - - # Table content - self.caption = make_vdom_constructor("caption") - self.col = make_vdom_constructor("col") - self.colgroup = make_vdom_constructor("colgroup") - self.table = make_vdom_constructor("table") - self.tbody = make_vdom_constructor("tbody") - self.td = make_vdom_constructor("td") - self.tfoot = make_vdom_constructor("tfoot") - self.th = make_vdom_constructor("th") - self.thead = make_vdom_constructor("thead") - self.tr = make_vdom_constructor("tr") - - # Forms - self.meter = make_vdom_constructor("meter") - self.output = make_vdom_constructor("output") - self.progress = make_vdom_constructor("progress") - self.input = make_vdom_constructor("input", allow_children=False) - self.button = make_vdom_constructor("button") - self.label = make_vdom_constructor("label") - self.fieldset = make_vdom_constructor("fieldset") - self.legend = make_vdom_constructor("legend") - - # Interactive elements - self.details = make_vdom_constructor("details") - self.dialog = make_vdom_constructor("dialog") - self.menu = make_vdom_constructor("menu") - self.menuitem = make_vdom_constructor("menuitem") - self.summary = make_vdom_constructor("summary") - - @overload - @staticmethod - def __call__( - tag: ComponentConstructor, *attributes_and_children: Any - ) -> AbstractComponent: - ... - - @overload - @staticmethod - def __call__(tag: str, *attributes_and_children: Any) -> VdomDict: - ... - - @staticmethod - def __call__( - tag: Union[str, ComponentConstructor], - *attributes_and_children: Any, - ) -> Union[VdomDict, AbstractComponent]: - if isinstance(tag, str): - return vdom(tag, *attributes_and_children) - - attributes, children = coalesce_attributes_and_children(attributes_and_children) - - if children: - return tag(children=children, **attributes) - else: - return tag(**attributes) - - def __getattr__(self, tag: str) -> VdomDictConstructor: - return make_vdom_constructor(tag) - - -html = Html() -"""Holds pre-made constructors for basic HTML elements - -See :class:`Html` for more info. -""" diff --git a/tests/conftest.py b/tests/conftest.py index 9bf8a6e42..228f2f4e7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,9 +2,8 @@ import inspect import os -from typing import Any, Iterator, List +from typing import Any, List -import pyalect.builtins.pytest # noqa import pytest from _pytest.config import Config from _pytest.config.argparsing import Parser @@ -12,8 +11,11 @@ from selenium.webdriver.support.ui import WebDriverWait import idom -from idom.client import manage as manage_client -from idom.testing import ServerMountPoint, create_simple_selenium_web_driver +from idom.testing import ( + ServerMountPoint, + clear_idom_web_modules_dir, + create_simple_selenium_web_driver, +) def pytest_collection_modifyitems( @@ -104,20 +106,9 @@ def driver_is_headless(pytestconfig: Config): return bool(pytestconfig.option.headless) -@pytest.fixture(scope="session", autouse=True) -def _restore_client(pytestconfig: Config) -> Iterator[None]: - """Restore the client's state before and after testing - - For faster test runs set ``--no-restore-client`` at the CLI. Test coverage and - breakages may occur if this is set. Further the client is not cleaned up - after testing and may effect usage of IDOM beyond the scope of the tests. - """ - if pytestconfig.option.restore_client: - manage_client.restore() - yield - manage_client.restore() - else: - yield +@pytest.fixture(autouse=True) +def _clear_web_modules_dir_after_test(): + clear_idom_web_modules_dir() def _mark_coros_as_async_tests(items: List[pytest.Item]) -> None: diff --git a/tests/test_cli.py b/tests/test_cli.py deleted file mode 100644 index aa59fd922..000000000 --- a/tests/test_cli.py +++ /dev/null @@ -1,37 +0,0 @@ -from typer.testing import CliRunner - -import idom -from idom.cli import main -from idom.client.manage import web_module_exists -from idom.config import all_options - - -cli_runner = CliRunner() - - -def test_root(): - assert idom.__version__ in cli_runner.invoke(main, ["--version"]).stdout - - -def test_install(): - cli_runner.invoke(main, ["restore"]) - assert cli_runner.invoke(main, ["install", "jquery"]).exit_code == 0 - assert web_module_exists("jquery") - - result = cli_runner.invoke(main, ["install", "jquery"]) - print(result.stdout) - assert result.exit_code == 0 - assert "Already installed ['jquery']" in result.stdout - assert "Build skipped" in result.stdout - assert web_module_exists("jquery") - - -def test_restore(): - assert cli_runner.invoke(main, ["restore"]).exit_code == 0 - - -def test_options(): - assert cli_runner.invoke(main, ["options"]).stdout.strip().split("\n") == [ - f"{opt.name} = {opt.current}" - for opt in sorted(all_options(), key=lambda o: o.name) - ] diff --git a/tests/test_client/test_app.py b/tests/test_client.py similarity index 59% rename from tests/test_client/test_app.py rename to tests/test_client.py index 793c03c20..9fbf50b74 100644 --- a/tests/test_client/test_app.py +++ b/tests/test_client.py @@ -43,35 +43,3 @@ def NewComponent(): # check that we can resume normal operation set_state.current(1) driver.find_element_by_id("new-component-1") - - -def test_that_js_module_unmount_is_called(driver, driver_wait, display): - module = idom.Module( - "set-flag-when-unmount-is-called", - source_file=JS_DIR / "set-flag-when-unmount-is-called.js", - ) - - set_current_component = idom.Ref(None) - - @idom.component - def ShowCurrentComponent(): - current_component, set_current_component.current = idom.hooks.use_state( - lambda: module.SomeComponent( - {"id": "some-component", "text": "initial component"} - ) - ) - return current_component - - display(ShowCurrentComponent) - - driver.find_element_by_id("some-component") - - set_current_component.current( - idom.html.h1({"id": "some-other-component"}, "some other component") - ) - - # the new component has been displayed - driver.find_element_by_id("some-other-component") - - # the unmount callback for the old component was called - driver.find_element_by_id("unmount-flag") diff --git a/tests/test_client/__init__.py b/tests/test_client/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/test_client/js/simple-button.js b/tests/test_client/js/simple-button.js deleted file mode 100644 index 91d6ad121..000000000 --- a/tests/test_client/js/simple-button.js +++ /dev/null @@ -1,14 +0,0 @@ -import react from "./react.js"; - -export function SimpleButton(props) { - return react.createElement( - "button", - { - id: props.id, - onClick(event) { - props.onClick({ data: props.eventResponseData }); - }, - }, - "simple button" - ); -} diff --git a/tests/test_client/test__private.py b/tests/test_client/test__private.py deleted file mode 100644 index 9044d802e..000000000 --- a/tests/test_client/test__private.py +++ /dev/null @@ -1,43 +0,0 @@ -import pytest - -from idom.client._private import ( - find_js_module_exports_in_source, - split_package_name_and_version, -) -from tests.general_utils import assert_same_items - - -@pytest.mark.parametrize( - "module_source, expected_names", - [ - ("asdfasdfasdf;export{one as One, two as Two};asdfasdf;", ["One", "Two"]), - ("asd;export{one as One};asdfasdf;export{two as Two};", ["One", "Two"]), - ("asdasd;export default something;", ["default"]), - ("asdasd;export One something;asdfa;export Two somethingElse;", ["One", "Two"]), - ( - "asdasd;export One something;asdfa;export{two as Two};asdfasdf;", - ["One", "Two"], - ), - ], -) -def test_find_js_module_exports_in_source(module_source, expected_names): - assert_same_items(find_js_module_exports_in_source(module_source), expected_names) - - -@pytest.mark.parametrize( - "package_specifier,expected_name_and_version", - [ - ("package", ("package", "")), - ("package@1.2.3", ("package", "1.2.3")), - ("@scope/pkg", ("@scope/pkg", "")), - ("@scope/pkg@1.2.3", ("@scope/pkg", "1.2.3")), - ("alias@npm:package", ("alias", "npm:package")), - ("alias@npm:package@1.2.3", ("alias", "npm:package@1.2.3")), - ("alias@npm:@scope/pkg@1.2.3", ("alias", "npm:@scope/pkg@1.2.3")), - ("@alias/pkg@npm:@scope/pkg@1.2.3", ("@alias/pkg", "npm:@scope/pkg@1.2.3")), - ], -) -def test_split_package_name_and_version(package_specifier, expected_name_and_version): - assert ( - split_package_name_and_version(package_specifier) == expected_name_and_version - ) diff --git a/tests/test_client/test_manage.py b/tests/test_client/test_manage.py deleted file mode 100644 index a494d7b5c..000000000 --- a/tests/test_client/test_manage.py +++ /dev/null @@ -1,151 +0,0 @@ -import pytest - -import idom -from idom.client.manage import ( - add_web_module, - build, - remove_web_module, - restore, - web_module_exists, - web_module_exports, - web_module_path, -) -from tests.general_utils import assert_same_items - - -@pytest.fixture(scope="module") -def victory(): - return idom.install("victory@35.4.0") - - -def test_clean_build(): - restore() - build(["jquery"]) - assert web_module_exists("jquery") - build([], clean_build=True) - assert not web_module_exists("jquery") - - -def test_add_web_module_source_must_exist(tmp_path): - with pytest.raises(FileNotFoundError, match="Package source file does not exist"): - add_web_module("test", tmp_path / "file-does-not-exist.js") - - -def test_cannot_add_web_module_if_already_exists(tmp_path): - first_temp_file = tmp_path / "temp-1.js" - second_temp_file = tmp_path / "temp-2.js" - - first_temp_file.write_text("console.log('hello!')") # this won't get run - second_temp_file.write_text("console.log('hello!')") # this won't get run - - add_web_module("test", first_temp_file) - with pytest.raises(FileExistsError): - add_web_module("test", second_temp_file) - - remove_web_module("test") - - -def test_can_add_web_module_if_already_exists_and_source_is_same(tmp_path): - temp_file = tmp_path / "temp.js" - temp_file.write_text("console.log('hello!')") - add_web_module("test", temp_file) - add_web_module("test", temp_file) - remove_web_module("test") - - -def test_web_module_path_must_exist(): - with pytest.raises(ValueError, match="does not exist at path"): - web_module_path("this-does-not-exist", must_exist=True) - assert not web_module_path("this-does-not-exist", must_exist=False).exists() - - -def test_web_module_exports(victory): - assert_same_items( - web_module_exports("victory"), - [ - "Area", - "Axis", - "Background", - "Bar", - "Border", - "Box", - "BrushHelpers", - "Candle", - "Circle", - "ClipPath", - "Collection", - "CursorHelpers", - "Curve", - "Data", - "DefaultTransitions", - "Domain", - "ErrorBar", - "Events", - "Flyout", - "Helpers", - "LabelHelpers", - "Line", - "LineSegment", - "Log", - "Path", - "Point", - "Portal", - "PropTypes", - "RawZoomHelpers", - "Rect", - "Scale", - "Selection", - "SelectionHelpers", - "Slice", - "Style", - "TSpan", - "Text", - "TextSize", - "Transitions", - "VictoryAnimation", - "VictoryArea", - "VictoryAxis", - "VictoryBar", - "VictoryBoxPlot", - "VictoryBrushContainer", - "VictoryBrushLine", - "VictoryCandlestick", - "VictoryChart", - "VictoryClipContainer", - "VictoryContainer", - "VictoryCursorContainer", - "VictoryErrorBar", - "VictoryGroup", - "VictoryHistogram", - "VictoryLabel", - "VictoryLegend", - "VictoryLine", - "VictoryPie", - "VictoryPolarAxis", - "VictoryPortal", - "VictoryScatter", - "VictorySelectionContainer", - "VictorySharedEvents", - "VictoryStack", - "VictoryTheme", - "VictoryTooltip", - "VictoryTransition", - "VictoryVoronoi", - "VictoryVoronoiContainer", - "VictoryZoomContainer", - "Voronoi", - "VoronoiHelpers", - "Whisker", - "Wrapper", - "ZoomHelpers", - "addEvents", - "brushContainerMixin", - "combineContainerMixins", - "createContainer", - "cursorContainerMixin", - "makeCreateContainerFunction", - "selectionContainerMixin", - "voronoiContainerMixin", - "zoomContainerMixin", - ], - ) diff --git a/tests/test_client/test_module.py b/tests/test_client/test_module.py deleted file mode 100644 index 1b26cde4d..000000000 --- a/tests/test_client/test_module.py +++ /dev/null @@ -1,116 +0,0 @@ -from pathlib import Path - -import pytest - -import idom -from idom import Module -from idom.client.module import URL_SOURCE - - -HERE = Path(__file__).parent -JS_FIXTURES = HERE / "js" - - -@pytest.fixture -def victory(): - return idom.install("victory@35.4.0") - - -@pytest.fixture(scope="module") -def simple_button(): - return Module("simple-button", source_file=JS_FIXTURES / "simple-button.js") - - -def test_any_relative_or_abolute_url_allowed(): - Module("/absolute/url/module") - Module("./relative/url/module") - Module("../relative/url/module") - Module("http://someurl.com/module") - - -def test_module_import_repr(): - assert ( - repr(Module("/absolute/url/module").declare("SomeComponent")) - == "Import(name='SomeComponent', source='/absolute/url/module', sourceType='URL', fallback=None)" - ) - - -def test_install_multiple(): - # install several random JS packages - pad_left, decamelize, is_sorted = idom.install( - ["pad-left", "decamelize", "is-sorted"] - ) - # ensure the output order is the same as the input order - assert pad_left.source.endswith("pad-left") - assert decamelize.source.endswith("decamelize") - assert is_sorted.source.endswith("is-sorted") - - -def test_module_does_not_exist(): - with pytest.raises(ValueError, match="does not exist"): - Module("this-module-does-not-exist") - - -def test_installed_module(driver, display, victory): - display(victory.VictoryBar) - driver.find_element_by_class_name("VictoryContainer") - - -def test_reference_pre_installed_module(victory): - assert victory == idom.Module("victory") - - -def test_module_from_url(): - url = "https://code.jquery.com/jquery-3.5.0.js" - jquery = idom.Module(url) - assert jquery.source == url - assert jquery.source_type == URL_SOURCE - assert jquery.exports is None - - -def test_module_from_source(driver, driver_wait, display, simple_button): - response_data = idom.Ref(None) - - @idom.component - def ShowButton(): - return simple_button.SimpleButton( - { - "id": "test-button", - "onClick": lambda event: response_data.set_current(event["data"]), - "eventResponseData": 10, - } - ) - - display(ShowButton) - - client_button = driver.find_element_by_id("test-button") - client_button.click() - driver_wait.until(lambda dvr: response_data.current == 10) - - -def test_module_checks_export_names(simple_button): - with pytest.raises(ValueError, match="does not export 'ComponentDoesNotExist'"): - simple_button.declare("ComponentDoesNotExist") - - -def test_cannot_have_source_file_for_url_source_type(): - with pytest.raises(ValueError, match="File given, but source type is 'URL'"): - idom.Module("test", source_file="something.js", source_type=URL_SOURCE) - - -def test_cannot_check_exports_for_url_source_type(): - with pytest.raises(ValueError, match="Can't check exports for source type 'URL'"): - idom.Module("test", check_exports=True, source_type=URL_SOURCE) - - -def test_invalid_source_type(): - with pytest.raises(ValueError, match="Invalid source type"): - idom.Module("test", source_type="TYPE_DOES_NOT_EXIST") - - -def test_attribute_error_if_lowercase_name_doesn_not_exist(): - mod = idom.Module("test", source_type=URL_SOURCE) - with pytest.raises(AttributeError, match="this_attribute_does_not_exist"): - # This attribute would otherwise be considered to - # be the name of a component the module exports. - mod.this_attribute_does_not_exist diff --git a/tests/test_client/utils.py b/tests/test_client/utils.py deleted file mode 100644 index 404998db7..000000000 --- a/tests/test_client/utils.py +++ /dev/null @@ -1,10 +0,0 @@ -from contextlib import contextmanager -from pathlib import Path - - -@contextmanager -def assert_file_is_touched(path): - path = Path(path) - last_modified = path.stat().st_mtime - yield - assert last_modified != path.stat().st_mtime diff --git a/tests/test_dialect.py b/tests/test_dialect.py deleted file mode 100644 index 7cb806a38..000000000 --- a/tests/test_dialect.py +++ /dev/null @@ -1,269 +0,0 @@ -import ast -from typing import Any, Dict, Tuple - -import pytest -from pyalect import DialectError, apply_dialects - -from idom import html - - -def eval_html(src, variables=None): - tree = apply_dialects(src, "html") - if len(tree.body) > 1 or not isinstance(tree.body[0], ast.Expr): - raise ValueError(f"Expected a single expression, not {src!r}") - code = compile(ast.Expression(tree.body[0].value), "", "eval") - return eval(code, {"html": html}, variables) - - -def make_html_dialect_test(*expectations: Tuple[str, Dict[str, Any], Any]): - def make_ids(exp): - source, variables = exp[:2] - source_repr = repr(source) - if len(source_repr) > 30: - source_repr = source_repr[:30] + "'..." - variables_repr = repr(variables) - if len(variables_repr) > 30: - variables_repr = variables_repr[:30] + "...}" - return source_repr + "-" + variables_repr - - @pytest.mark.parametrize("expect", expectations, ids=make_ids) - def test_html_dialect(expect): - source, variables, result = expect - assert eval_html(source, variables) == result - - return test_html_dialect - - -test_simple_htm_template = make_html_dialect_test( - ('html(f"
")', {}, {"tagName": "div"}) -) - -test_value_children = make_html_dialect_test( - ('html(f"
foo
")', {}, {"tagName": "div", "children": ["foo"]}), - ( - 'html(f"
")', - {}, - {"tagName": "div", "children": [{"tagName": "span"}]}, - ), -) - -test_expression_children = make_html_dialect_test( - ( - 'html(f"
{value}
")', - {"value": "foo"}, - {"tagName": "div", "children": ["foo"]}, - ), - ( - """html(f"
{html(f'')}
")""", - {}, - {"tagName": "div", "children": [{"tagName": "span"}]}, - ), -) - -test_preserve_whitespace_between_text_values = make_html_dialect_test( - ( - """html(f"
a {'b'} c
")""", - {}, - {"tagName": "div", "children": [" a ", "b", " c "]}, - ) -) - -test_collapse_whitespace_lines_in_text = make_html_dialect_test( - ( - r'html(f"
\n a b c \n
")', - {}, - {"tagName": "div", "children": ["a b c"]}, - ), - ( - r"""html(f"
a \n {'b'} \n c \n
")""", - {}, - {"tagName": "div", "children": ["a", "b", "c"]}, - ), -) - -test_value_tag = make_html_dialect_test( - ('html(f"
")', {}, {"tagName": "div"}), - ('html(f"
")', {}, {"tagName": "div"}), - ("""html(f"<'div' />")""", {}, {"tagName": "div"}), - ("""html(f'<"div" />')""", {}, {"tagName": "div"}), -) - -test_expression_tag = make_html_dialect_test( - ('html(f"<{tag} />")', {"tag": "div"}, {"tagName": "div"}) -) - -test_boolean_prop = make_html_dialect_test( - ('html(f"
")', {}, {"tagName": "div", "attributes": {"foo": True}}), - ("""html(f"
")""", {}, {"tagName": "div", "attributes": {"foo": True}}), - ("""html(f'
')""", {}, {"tagName": "div", "attributes": {"foo": True}}), -) - -test_value_prop_name = make_html_dialect_test( - ('html(f"
")', {}, {"tagName": "div", "attributes": {"foo": "1"}}), - ( - """html(f'
')""", - {}, - {"tagName": "div", "attributes": {"foo": "1"}}, - ), - ( - """html(f"
")""", - {}, - {"tagName": "div", "attributes": {"foo": "1"}}, - ), - ( - """html(f"
")""", - {}, - {"tagName": "div", "attributes": {"foo": "1"}}, - ), - ( - """html(f'
')""", - {}, - {"tagName": "div", "attributes": {"foo": "1"}}, - ), -) - -test_expression_prop_value = make_html_dialect_test( - ( - """html(f"
")""", - {"a": 1.23}, - {"tagName": "div", "attributes": {"foo": 1.23}}, - ), - ( - """html(f'
')""", - {"a": 1.23}, - {"tagName": "div", "attributes": {"foo": 1.23}}, - ), - ( - """html(f"
")""", - {"a": 1.23}, - {"tagName": "div", "attributes": {"foo": 1.23}}, - ), - ( - """html(f"
")""", - {"a": 1.23}, - {"tagName": "div", "attributes": {"foo": "1.2"}}, - ), -) - -test_concatenated_prop_value = make_html_dialect_test( - ( - """html(f"
")""", - {"a": "1"}, - {"tagName": "div", "attributes": {"foo": "12"}}, - ), - ( - """html(f"
")""", - {"a": "1"}, - {"tagName": "div", "attributes": {"foo": "0/1/2"}}, - ), -) - - -test_slash_in_prop_value = make_html_dialect_test( - ( - """html(f"
")""", - {}, - {"tagName": "div", "attributes": {"foo": "/bar/quux"}}, - ) -) - - -test_spread = make_html_dialect_test( - ( - """html(f"
")""", - {"foo": {"foo": 1}}, - {"tagName": "div", "attributes": {"foo": 1, "bar": 2}}, - ) -) - - -test_comments = make_html_dialect_test( - ( - '''html( - f""" -
- before - - after -
- """ - )''', - {}, - {"tagName": "div", "children": ["before", "after"]}, - ), - ( - """html(f"
slight deviation from HTML comments<-->
")""", - {}, - {"tagName": "div"}, - ), -) - - -test_component = make_html_dialect_test( - ( - 'html(f"<{MyComponentWithChildren}>hello")', - {"MyComponentWithChildren": lambda children: html.div(children + ["world"])}, - {"tagName": "div", "children": ["hello", "world"]}, - ), - ( - 'html(f"<{MyComponentWithAttributes} x=2 y=3 />")', - { - "MyComponentWithAttributes": lambda x, y: html.div( - {"x": int(x) * 2, "y": int(y) * 2} - ) - }, - {"tagName": "div", "attributes": {"x": 4, "y": 6}}, - ), - ( - 'html(f"<{MyComponentWithAttributesAndChildren} x=2 y=3>hello")', - { - "MyComponentWithAttributesAndChildren": lambda x, y, children: html.div( - {"x": int(x) * 2, "y": int(y) * 2}, children + ["world"] - ) - }, - { - "tagName": "div", - "attributes": {"x": 4, "y": 6}, - "children": ["hello", "world"], - }, - ), -) - - -def test_tag_errors(): - with pytest.raises(DialectError, match="no token found"): - apply_dialects('html(f"< >")', "html") - with pytest.raises(DialectError, match="no token found"): - apply_dialects('html(f"<>")', "html") - with pytest.raises(DialectError, match="no token found"): - apply_dialects("""html(f"<'")""", "html") - with pytest.raises(DialectError, match="unexpected end of data"): - apply_dialects('html(f"<")', "html") - - -def test_attribute_name_errors(): - with pytest.raises(DialectError, match="expression not allowed"): - apply_dialects('html(f"
")', "html") - with pytest.raises(DialectError, match="unexpected end of data"): - apply_dialects('html(f"
")', "html") diff --git a/tests/test_server/test_common/test_per_client_state.py b/tests/test_server/test_common/test_per_client_state.py index 197768de5..65639df01 100644 --- a/tests/test_server/test_common/test_per_client_state.py +++ b/tests/test_server/test_common/test_per_client_state.py @@ -65,7 +65,8 @@ def Counter(): client_counter.click() -def test_installed_module(driver, display): - victory = idom.install("victory@35.4.0") - display(victory.VictoryBar) +def test_module_from_template(driver, display): + victory = idom.web.module_from_template("react", "victory@35.4.0") + VictoryBar = idom.web.export(victory, "VictoryBar") + display(VictoryBar) driver.find_element_by_class_name("VictoryContainer") diff --git a/src/idom/client/__init__.py b/tests/test_web/__init__.py similarity index 100% rename from src/idom/client/__init__.py rename to tests/test_web/__init__.py diff --git a/tests/test_web/js_fixtures/export-resolution/index.js b/tests/test_web/js_fixtures/export-resolution/index.js new file mode 100644 index 000000000..2f1f46a51 --- /dev/null +++ b/tests/test_web/js_fixtures/export-resolution/index.js @@ -0,0 +1,2 @@ +export {index as Index}; +export * from "./one.js"; diff --git a/tests/test_web/js_fixtures/export-resolution/one.js b/tests/test_web/js_fixtures/export-resolution/one.js new file mode 100644 index 000000000..a0355241f --- /dev/null +++ b/tests/test_web/js_fixtures/export-resolution/one.js @@ -0,0 +1,3 @@ +export {one as One}; +// use ../ just to check that it works +export * from "../export-resolution/two.js"; diff --git a/tests/test_web/js_fixtures/export-resolution/two.js b/tests/test_web/js_fixtures/export-resolution/two.js new file mode 100644 index 000000000..4e1d807c2 --- /dev/null +++ b/tests/test_web/js_fixtures/export-resolution/two.js @@ -0,0 +1,2 @@ +export {two as Two}; +export * from "https://some.external.url"; diff --git a/tests/test_web/js_fixtures/exports-syntax.js b/tests/test_web/js_fixtures/exports-syntax.js new file mode 100644 index 000000000..8f9b0e612 --- /dev/null +++ b/tests/test_web/js_fixtures/exports-syntax.js @@ -0,0 +1,23 @@ +// Copied from: https://developer.mozilla.org/en-US/docs/web/javascript/reference/statements/export + +// Exporting individual features +export let name1, name2, name3; // also var, const +export let name4 = 4, name5 = 5, name6; // also var, const +export function functionName(){...} +export class ClassName {...} + +// Export list +export { name7, name8, name9 }; + +// Renaming exports +export { variable1 as name10, variable2 as name11, name12 }; + +// Exporting destructured assignments with renaming +export const { name13, name14: bar } = o; + +// Aggregating modules +export * from "https://source1.com"; // does not set the default export +export * from "https://source2.com"; // does not set the default export +export * as name15 from "https://source3.com"; // Draft ECMAScript® 2O21 +export { name16, name17 } from "https://source4.com"; +export { import1 as name18, import2 as name19, name20 } from "https://source5.com"; diff --git a/tests/test_web/js_fixtures/exports-two-components.js b/tests/test_web/js_fixtures/exports-two-components.js new file mode 100644 index 000000000..4266a24ca --- /dev/null +++ b/tests/test_web/js_fixtures/exports-two-components.js @@ -0,0 +1,18 @@ +import { h, render } from "https://unpkg.com/preact?module"; +import htm from "https://unpkg.com/htm?module"; + +const html = htm.bind(h); + +export { h as createElement, render as renderElement }; + +export function unmountElement(container) { + render(null, container); +} + +export function Header1(props) { + return h("h1", {id: props.id}, props.text); +} + +export function Header2(props) { + return h("h2", {id: props.id}, props.text); +} diff --git a/tests/test_client/js/set-flag-when-unmount-is-called.js b/tests/test_web/js_fixtures/set-flag-when-unmount-is-called.js similarity index 100% rename from tests/test_client/js/set-flag-when-unmount-is-called.js rename to tests/test_web/js_fixtures/set-flag-when-unmount-is-called.js diff --git a/tests/test_web/js_fixtures/simple-button.js b/tests/test_web/js_fixtures/simple-button.js new file mode 100644 index 000000000..ab2b13788 --- /dev/null +++ b/tests/test_web/js_fixtures/simple-button.js @@ -0,0 +1,23 @@ +import { h, render } from "https://unpkg.com/preact?module"; +import htm from "https://unpkg.com/htm?module"; + +const html = htm.bind(h); + +export { h as createElement, render as renderElement }; + +export function unmountElement(container) { + render(null, container); +} + +export function SimpleButton(props) { + return h( + "button", + { + id: props.id, + onClick(event) { + props.onClick({ data: props.eventResponseData }); + }, + }, + "simple button" + ); +} diff --git a/tests/test_web/test_module.py b/tests/test_web/test_module.py new file mode 100644 index 000000000..7e9b08200 --- /dev/null +++ b/tests/test_web/test_module.py @@ -0,0 +1,170 @@ +from pathlib import Path + +import pytest +from sanic import Sanic +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions +from selenium.webdriver.support.ui import WebDriverWait + +import idom +from idom.server.sanic import PerClientStateServer +from idom.testing import ServerMountPoint +from idom.web.module import NAME_SOURCE, WebModule + + +JS_FIXTURES_DIR = Path(__file__).parent / "js_fixtures" + + +def test_that_js_module_unmount_is_called(driver, display): + SomeComponent = idom.web.export( + idom.web.module_from_file( + "set-flag-when-unmount-is-called", + JS_FIXTURES_DIR / "set-flag-when-unmount-is-called.js", + ), + "SomeComponent", + ) + + set_current_component = idom.Ref(None) + + @idom.component + def ShowCurrentComponent(): + current_component, set_current_component.current = idom.hooks.use_state( + lambda: SomeComponent({"id": "some-component", "text": "initial component"}) + ) + return current_component + + display(ShowCurrentComponent) + + driver.find_element_by_id("some-component") + + set_current_component.current( + idom.html.h1({"id": "some-other-component"}, "some other component") + ) + + # the new component has been displayed + driver.find_element_by_id("some-other-component") + + # the unmount callback for the old component was called + driver.find_element_by_id("unmount-flag") + + +def test_module_from_url(driver): + app = Sanic(__name__) + + # instead of directing the URL to a CDN, we just point it to this static file + app.static( + "/simple-button.js", + str(JS_FIXTURES_DIR / "simple-button.js"), + content_type="text/javascript", + ) + + SimpleButton = idom.web.export( + idom.web.module_from_url("/simple-button.js", resolve_exports=False), + "SimpleButton", + ) + + @idom.component + def ShowSimpleButton(): + return SimpleButton({"id": "my-button"}) + + with ServerMountPoint(PerClientStateServer, app=app) as mount_point: + mount_point.mount(ShowSimpleButton) + driver.get(mount_point.url()) + driver.find_element_by_id("my-button") + + +def test_module_from_template_where_template_does_not_exist(): + with pytest.raises(ValueError, match="No template for 'does-not-exist.js'"): + idom.web.module_from_template("does-not-exist", "something.js") + + +def test_module_from_template(driver, display): + victory = idom.web.module_from_template("react", "victory@35.4.0") + VictoryBar = idom.web.export(victory, "VictoryBar") + display(VictoryBar) + wait = WebDriverWait(driver, 10) + wait.until( + expected_conditions.visibility_of_element_located( + (By.CLASS_NAME, "VictoryContainer") + ) + ) + + +def test_module_from_file(driver, driver_wait, display): + SimpleButton = idom.web.export( + idom.web.module_from_file( + "simple-button", JS_FIXTURES_DIR / "simple-button.js" + ), + "SimpleButton", + ) + + is_clicked = idom.Ref(False) + + @idom.component + def ShowSimpleButton(): + return SimpleButton( + {"id": "my-button", "onClick": lambda event: is_clicked.set_current(True)} + ) + + display(ShowSimpleButton) + + button = driver.find_element_by_id("my-button") + button.click() + driver_wait.until(lambda d: is_clicked.current) + + +def test_module_from_file_source_conflict(tmp_path): + first_file = tmp_path / "first.js" + + with pytest.raises(FileNotFoundError, match="does not exist"): + idom.web.module_from_file("temp", first_file) + + first_file.touch() + + idom.web.module_from_file("temp", first_file) + + second_file = tmp_path / "second.js" + second_file.touch() + + with pytest.raises(FileExistsError, match="already exists"): + idom.web.module_from_file("temp", second_file) + + +def test_web_module_from_file_symlink(tmp_path): + file = tmp_path / "temp.js" + file.touch() + + module = idom.web.module_from_file("temp", file, symlink=True) + + assert module.file.resolve().read_text() == "" + + file.write_text("hello world!") + + assert module.file.resolve().read_text() == "hello world!" + + +def test_module_missing_exports(): + module = WebModule("test", NAME_SOURCE, None, {"a", "b", "c"}, None) + + with pytest.raises(ValueError, match="does not export 'x'"): + idom.web.export(module, "x") + + with pytest.raises(ValueError, match=r"does not export \['x', 'y'\]"): + idom.web.export(module, ["x", "y"]) + + +def test_module_exports_multiple_components(driver, display): + Header1, Header2 = idom.web.export( + idom.web.module_from_file( + "exports-two-components", JS_FIXTURES_DIR / "exports-two-components.js" + ), + ["Header1", "Header2"], + ) + + display(lambda: Header1({"id": "my-h1"}, "My Header 1")) + + driver.find_element_by_id("my-h1") + + display(lambda: Header2({"id": "my-h2"}, "My Header 2")) + + driver.find_element_by_id("my-h2") diff --git a/tests/test_web/test_utils.py b/tests/test_web/test_utils.py new file mode 100644 index 000000000..dd657694a --- /dev/null +++ b/tests/test_web/test_utils.py @@ -0,0 +1,129 @@ +from pathlib import Path + +import pytest +import responses + +from idom.web.utils import ( + resolve_module_exports_from_file, + resolve_module_exports_from_source, + resolve_module_exports_from_url, +) + + +JS_FIXTURES_DIR = Path(__file__).parent / "js_fixtures" + + +@responses.activate +def test_resolve_module_exports_from_file(caplog): + responses.add( + responses.GET, + "https://some.external.url", + body="export {something as ExternalUrl}", + ) + path = JS_FIXTURES_DIR / "export-resolution" / "index.js" + assert resolve_module_exports_from_file(path, 4) == { + "Index", + "One", + "Two", + "ExternalUrl", + } + + +def test_resolve_module_exports_from_file_log_on_max_depth(caplog): + path = JS_FIXTURES_DIR / "export-resolution" / "index.js" + assert resolve_module_exports_from_file(path, 0) == set() + assert len(caplog.records) == 1 + assert caplog.records[0].message.endswith("max depth reached") + + caplog.records.clear() + + assert resolve_module_exports_from_file(path, 2) == {"Index", "One"} + assert len(caplog.records) == 1 + assert caplog.records[0].message.endswith("max depth reached") + + +def test_resolve_module_exports_from_file_log_on_unknown_file_location( + caplog, tmp_path +): + file = tmp_path / "some.js" + file.write_text("export * from './does-not-exist.js';") + resolve_module_exports_from_file(file, 2) + assert len(caplog.records) == 1 + assert caplog.records[0].message.startswith( + "Did not resolve exports for unknown file" + ) + + +@responses.activate +def test_resolve_module_exports_from_url(): + responses.add( + responses.GET, + "https://some.url/first.js", + body="export const First = 1; export * from 'https://another.url/path/second.js';", + ) + responses.add( + responses.GET, + "https://another.url/path/second.js", + body="export const Second = 2; export * from '../third.js';", + ) + responses.add( + responses.GET, + "https://another.url/third.js", + body="export const Third = 3; export * from './fourth.js';", + ) + responses.add( + responses.GET, + "https://another.url/fourth.js", + body="export const Fourth = 4;", + ) + + assert resolve_module_exports_from_url("https://some.url/first.js", 4) == { + "First", + "Second", + "Third", + "Fourth", + } + + +def test_resolve_module_exports_from_url_log_on_max_depth(caplog): + assert resolve_module_exports_from_url("https://some.url", 0) == set() + assert len(caplog.records) == 1 + assert caplog.records[0].message.endswith("max depth reached") + + +def test_resolve_module_exports_from_url_log_on_bad_response(caplog): + assert resolve_module_exports_from_url("https://some.url", 1) == set() + assert len(caplog.records) == 1 + assert caplog.records[0].message.startswith("Did not resolve exports for url") + + +@pytest.mark.parametrize( + "text", + [ + "export default expression;", + "export default function (…) { … } // also class, function*", + "export default function name1(…) { … } // also class, function*", + "export { something as default };", + "export { default } from 'some-source';", + "export { something as default } from 'some-source';", + ], +) +def test_resolve_module_default_exports_from_source(text): + names, references = resolve_module_exports_from_source(text) + assert names == {"default"} and not references + + +def test_resolve_module_exports_from_source(): + fixture_file = JS_FIXTURES_DIR / "exports-syntax.js" + names, references = resolve_module_exports_from_source(fixture_file.read_text()) + assert ( + names + == ( + {f"name{i}" for i in range(1, 21)} + | { + "functionName", + "ClassName", + } + ) + and references == {"https://source1.com", "https://source2.com"} + ) diff --git a/tests/test_widgets/test_html.py b/tests/test_widgets.py similarity index 61% rename from tests/test_widgets/test_html.py rename to tests/test_widgets.py index d93567943..ca0b97090 100644 --- a/tests/test_widgets/test_html.py +++ b/tests/test_widgets.py @@ -1,5 +1,6 @@ import time from base64 import b64encode +from pathlib import Path from selenium.webdriver.common.keys import Keys @@ -7,26 +8,79 @@ from tests.driver_utils import send_keys -_image_src_bytes = b""" +HERE = Path(__file__).parent + + +def test_multiview_repr(): + assert str(idom.widgets.MultiViewMount({})) == "MultiViewMount({})" + + +def test_hostwap_update_on_change(driver, display): + """Ensure shared hotswapping works + + This basically means that previously rendered views of a hotswap component get updated + when a new view is mounted, not just the next time it is re-displayed + + In this test we construct a scenario where clicking a button will cause a pre-existing + hotswap component to be updated + """ + + def make_next_count_constructor(count): + """We need to construct a new function so they're different when we set_state""" + + def constructor(): + count.current += 1 + return idom.html.div({"id": f"hotswap-{count.current}"}, count.current) + + return constructor + + @idom.component + def ButtonSwapsDivs(): + count = idom.Ref(0) + + @idom.event + async def on_click(event): + mount(make_next_count_constructor(count)) + + incr = idom.html.button({"onClick": on_click, "id": "incr-button"}, "incr") + + mount, make_hostswap = idom.widgets.hotswap(update_on_change=True) + mount(make_next_count_constructor(count)) + hotswap_view = make_hostswap() + + return idom.html.div(incr, hotswap_view) + + display(ButtonSwapsDivs) + + client_incr_button = driver.find_element_by_id("incr-button") + + driver.find_element_by_id("hotswap-1") + client_incr_button.click() + driver.find_element_by_id("hotswap-2") + client_incr_button.click() + driver.find_element_by_id("hotswap-3") + + +IMAGE_SRC_BYTES = b""" """ -_base64_image_src = b64encode(_image_src_bytes).decode() +BASE64_IMAGE_SRC = b64encode(IMAGE_SRC_BYTES).decode() def test_image_from_string(driver, display): - src = _image_src_bytes.decode() + src = IMAGE_SRC_BYTES.decode() display(lambda: idom.widgets.image("svg", src, {"id": "a-circle-1"})) client_img = driver.find_element_by_id("a-circle-1") - assert _base64_image_src in client_img.get_attribute("src") + assert BASE64_IMAGE_SRC in client_img.get_attribute("src") def test_image_from_bytes(driver, display): - src = _image_src_bytes + src = IMAGE_SRC_BYTES display(lambda: idom.widgets.image("svg", src, {"id": "a-circle-1"})) client_img = driver.find_element_by_id("a-circle-1") - assert _base64_image_src in client_img.get_attribute("src") + assert BASE64_IMAGE_SRC in client_img.get_attribute("src") def test_input_callback(driver, driver_wait, display): diff --git a/tests/test_widgets/__init__.py b/tests/test_widgets/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/test_widgets/test_utils.py b/tests/test_widgets/test_utils.py deleted file mode 100644 index c602fbc07..000000000 --- a/tests/test_widgets/test_utils.py +++ /dev/null @@ -1,56 +0,0 @@ -from pathlib import Path - -import idom - - -HERE = Path(__file__).parent - - -def test_multiview_repr(): - assert str(idom.widgets.utils.MultiViewMount({})) == "MultiViewMount({})" - - -def test_hostwap_update_on_change(driver, display): - """Ensure shared hotswapping works - - This basically means that previously rendered views of a hotswap component get updated - when a new view is mounted, not just the next time it is re-displayed - - In this test we construct a scenario where clicking a button will cause a pre-existing - hotswap component to be updated - """ - - def make_next_count_constructor(count): - """We need to construct a new function so they're different when we set_state""" - - def constructor(): - count.current += 1 - return idom.html.div({"id": f"hotswap-{count.current}"}, count.current) - - return constructor - - @idom.component - def ButtonSwapsDivs(): - count = idom.Ref(0) - - @idom.event - async def on_click(event): - mount(make_next_count_constructor(count)) - - incr = idom.html.button({"onClick": on_click, "id": "incr-button"}, "incr") - - mount, make_hostswap = idom.widgets.hotswap(update_on_change=True) - mount(make_next_count_constructor(count)) - hotswap_view = make_hostswap() - - return idom.html.div(incr, hotswap_view) - - display(ButtonSwapsDivs) - - client_incr_button = driver.find_element_by_id("incr-button") - - driver.find_element_by_id("hotswap-1") - client_incr_button.click() - driver.find_element_by_id("hotswap-2") - client_incr_button.click() - driver.find_element_by_id("hotswap-3")