From 8a7f93c0ac36d704b43a3c4c031bcba57f73fad4 Mon Sep 17 00:00:00 2001 From: "Juan P. Prieto" Date: Thu, 11 Aug 2022 18:10:16 -0700 Subject: [PATCH 01/26] initial Script scaffold --- .../src/components/Script/Script.client.tsx | 175 ++++++++++++++++++ .../hydrogen/src/components/Script/index.ts | 2 + packages/hydrogen/src/components/index.ts | 1 + .../request-idle-callback-polyfill.ts | 23 +++ .../src/routes/script.server.jsx | 88 +++++++++ 5 files changed, 289 insertions(+) create mode 100644 packages/hydrogen/src/components/Script/Script.client.tsx create mode 100644 packages/hydrogen/src/components/Script/index.ts create mode 100644 packages/hydrogen/src/utilities/request-idle-callback-polyfill.ts create mode 100644 packages/playground/server-components/src/routes/script.server.jsx diff --git a/packages/hydrogen/src/components/Script/Script.client.tsx b/packages/hydrogen/src/components/Script/Script.client.tsx new file mode 100644 index 0000000000..a6f296a6fe --- /dev/null +++ b/packages/hydrogen/src/components/Script/Script.client.tsx @@ -0,0 +1,175 @@ +/** + The `Script` component renders a + + // inline external script via src + + + + + + + + + + + ); +} diff --git a/packages/hydrogen/src/components/Script/index.ts b/packages/hydrogen/src/foundation/Script/index.ts similarity index 54% rename from packages/hydrogen/src/components/Script/index.ts rename to packages/hydrogen/src/foundation/Script/index.ts index e7726469cc..178aa08de9 100644 --- a/packages/hydrogen/src/components/Script/index.ts +++ b/packages/hydrogen/src/foundation/Script/index.ts @@ -1,6 +1,6 @@ export type {ScriptProps} from './loadScript.js'; -export type {ScriptState} from './useLoadScript.js'; +export type {ScriptState} from './useLoadScript.client.js'; export {Script} from './Script.client.js'; export {loadScript} from './loadScript.js'; -export {useLoadScript} from './useLoadScript.js'; +export {useLoadScript} from './useLoadScript.client.js'; diff --git a/packages/hydrogen/src/components/Script/loadScriptOnIdle.ts b/packages/hydrogen/src/foundation/Script/loadScriptOnIdle.ts similarity index 90% rename from packages/hydrogen/src/components/Script/loadScriptOnIdle.ts rename to packages/hydrogen/src/foundation/Script/loadScriptOnIdle.ts index b245dc79ff..a4d6629aca 100644 --- a/packages/hydrogen/src/components/Script/loadScriptOnIdle.ts +++ b/packages/hydrogen/src/foundation/Script/loadScriptOnIdle.ts @@ -21,16 +21,16 @@ TODO: write to the dom with a requestAnimationFrame inside loadScript */ // Handles onIdle strategy -export function loadScriptOnIdle(props: ScriptProps) { +export async function loadScriptOnIdle(props: ScriptProps) { if (document.readyState === 'complete') { - requestIdleCallback((deadline) => { + return requestIdleCallback((deadline) => { return loadScript(props) .then((script) => script) .catch((error) => error); }); } else { window.addEventListener('load', () => { - requestIdleCallback((deadline) => { + return requestIdleCallback((deadline) => { console.log('requestIdleCallback:load'); return loadScript(props) .then((script) => script) diff --git a/packages/hydrogen/src/components/Script/logScriptPerformance.ts b/packages/hydrogen/src/foundation/Script/logScriptPerformance.ts similarity index 85% rename from packages/hydrogen/src/components/Script/logScriptPerformance.ts rename to packages/hydrogen/src/foundation/Script/logScriptPerformance.ts index 06d7b19f85..96df81fb1e 100644 --- a/packages/hydrogen/src/components/Script/logScriptPerformance.ts +++ b/packages/hydrogen/src/foundation/Script/logScriptPerformance.ts @@ -11,9 +11,9 @@ export function logScriptPerformance(key: string, src: string | undefined) { } } const durationNoThrottle = Math.ceil(performance.now() - loadTime); - const duration4G = durationNoThrottle * 3; // ~3x slower than no throttle + const duration4G = durationNoThrottle * 6; // ~6x slower than no throttle const duration = duration4G; - // console.log(`📡 ${src} ${duration}ms ${duration4G}ms`); + // console.log(`📡 ${src} ${durationNoThrottle}ms ${duration4G}ms`); const time = duration > 1000 ? `${duration / 1000}s` : `${duration}ms`; const score = duration > 500 ? 'bad' : duration > 375 ? 'ok' : 'good'; diff --git a/packages/hydrogen/src/components/Script/useLoadScript.ts b/packages/hydrogen/src/foundation/Script/useLoadScript.client.ts similarity index 55% rename from packages/hydrogen/src/components/Script/useLoadScript.ts rename to packages/hydrogen/src/foundation/Script/useLoadScript.client.ts index 7e613ef54f..5ae5528713 100644 --- a/packages/hydrogen/src/components/Script/useLoadScript.ts +++ b/packages/hydrogen/src/foundation/Script/useLoadScript.client.ts @@ -1,17 +1,24 @@ import {useState, useEffect} from 'react'; -import {useUrl} from '../../foundation/useUrl/useUrl.js'; -import {loadScript, type ScriptProps} from './loadScript.js'; +import {useUrl} from '../useUrl/useUrl.js'; +import {loadScript, type PostHydrationProps} from './loadScript.js'; import {loadScriptOnIdle} from './loadScriptOnIdle.js'; export type ScriptState = 'loading' | 'done' | 'error'; let prevPathname = ''; -// TODO: async won't work with `onIdle` strategy -export function useLoadScript(options: ScriptProps) { +type UseScriptProps = { + /* because the hook form is stateful we don't accept `beforeHydration` */ + strategy?: Exclude< + PostHydrationProps['strategy'], + 'beforeHydration' | 'worker' + >; +} & PostHydrationProps; + +export function useLoadScript(options: UseScriptProps) { const {pathname} = useUrl(); const [status, setStatus] = useState('loading'); - const stringifiedOptions = JSON.stringify(options); + const optionString = JSON.stringify(options); const pathChanged = prevPathname ? pathname !== prevPathname : false; const reloadOnNav = options?.reload && pathChanged; @@ -23,9 +30,13 @@ export function useLoadScript(options: ScriptProps) { } async function loadScriptWrapper() { + let loaded; try { - // TODO: - const loaded = await loadScript(options); + if (options?.strategy === 'afterHydration') { + loaded = await loadScript(options); + } else if (options?.strategy === 'onIdle') { + loaded = await loadScriptOnIdle(options); + } if (loaded) { setStatus('done'); } @@ -35,7 +46,7 @@ export function useLoadScript(options: ScriptProps) { } loadScriptWrapper(); - }, [stringifiedOptions, options, status, pathname]); + }, [optionString, options, status, pathname]); // if reload === true, reload on path change useEffect(() => { diff --git a/packages/playground/server-components/src/components/Hydration/HydrationCompleteListener.server.jsx b/packages/playground/server-components/src/components/Hydration/HydrationCompleteListener.server.jsx index feb4bf2dad..0a03900f84 100644 --- a/packages/playground/server-components/src/components/Hydration/HydrationCompleteListener.server.jsx +++ b/packages/playground/server-components/src/components/Hydration/HydrationCompleteListener.server.jsx @@ -9,13 +9,12 @@ export default function HydrationCompleteListener() { const start = performance.now(); let end; let interval; - let clicks = 0; // This event is fired by the button click handler which will // only happen when the button is hydrated. window.addEventListener('hydration-complete', function() { end = performance.now(); - const message = '💦 hydration completed in: '+ Math.round(end - start) + 'ms, ticks: ' + clicks + ', time/tick: ' + Math.round((end - start) / clicks) + 'ms'; + const message = '💦 Hydration completed in: '+ Math.round(end - start) + 'ms'; console.log('------------------------------------------------------------'); console.log(message); console.log('------------------------------------------------------------'); diff --git a/packages/playground/server-components/src/components/Hydration/HydrationCompleteTrigger.client.jsx b/packages/playground/server-components/src/components/Hydration/HydrationCompleteTrigger.client.jsx index e7f4b020c7..3e1cfc9d91 100644 --- a/packages/playground/server-components/src/components/Hydration/HydrationCompleteTrigger.client.jsx +++ b/packages/playground/server-components/src/components/Hydration/HydrationCompleteTrigger.client.jsx @@ -1,4 +1,4 @@ -import {useLayoutEffect} from 'react'; +import {useEffect} from 'react'; /* We use this button's onClick handler to measure when hydration @@ -10,7 +10,7 @@ import {useLayoutEffect} from 'react'; */ let hydrated = false; export default function HydrationComplete({children}) { - useLayoutEffect(() => { + useEffect(() => { if (hydrated) return; hydrated = true; const hydrationEvent = new CustomEvent('hydration-complete', { diff --git a/packages/playground/server-components/src/routes/scripts/[handle].server.jsx b/packages/playground/server-components/src/routes/scripts/[handle].server.jsx index 5e95c0339c..98b032770f 100644 --- a/packages/playground/server-components/src/routes/scripts/[handle].server.jsx +++ b/packages/playground/server-components/src/routes/scripts/[handle].server.jsx @@ -132,11 +132,7 @@ function ScriptHead() {

Loading script in the head...

- @@ -44,11 +44,11 @@ import type {BeforeHydrationProps} from './loadScript'; - diff --git a/packages/hydrogen/src/foundation/Script/loadScript.ts b/packages/hydrogen/src/foundation/Script/loadScript.ts index de86f15572..0dcb7e11ec 100644 --- a/packages/hydrogen/src/foundation/Script/loadScript.ts +++ b/packages/hydrogen/src/foundation/Script/loadScript.ts @@ -5,80 +5,16 @@ To test: - strips out `async` or `defer` attributes if `src` is not present */ - // #see: https://stackoverflow.com/questions/2920129/can-i-run-javascript-before-the-whole-page-is-loaded // @see: https://i.stack.imgur.com/FcAKu.png -import React, {type ScriptHTMLAttributes} from 'react'; import {logScriptPerformance} from './logScriptPerformance.js'; - -interface ErrorEventHandler { - ( - event: Event | string, - source?: string, - fileno?: number, - columnNumber?: number, - error?: Error - ): void; -} - -export type ScriptState = 'loading' | 'done' | 'error'; -export type ScriptTarget = 'head' | 'body'; -export type ScriptStrategy = - | 'beforeHydration' - | 'afterHydration' - | 'onIdle' - | 'worker'; - -/* Ensure that either `src`, `children` or `dangerouslySetInnerHTML` is set, but not any combination of them */ -type InlineProps = - | { - dangerouslySetInnerHTML: {__html: string}; - children?: never; - src?: never; - } - | { - children: React.ReactNode; - dangerouslySetInnerHTML?: never; - src?: never; - } - | { - children?: never; - dangerouslySetInnerHTML?: never; - src: string; - }; - -/* All scripts share these props */ -type BaseProps = { - children?: React.ReactNode; - id?: string; - src?: string; -} & InlineProps; - -export type BeforeHydrationProps = { - strategy: 'beforeHydration'; - [key: string]: any; -} & BaseProps & - ScriptHTMLAttributes; - -export type PostHydrationProps = { - onError?: (e: any) => void; - /* Event emitted when the script is loaded */ - onLoad?: (e: any) => void; - /* Event emitted when the script is ready */ - onReady?: (e?: any) => void; - /* Simulates MPA architecture force reloading the script on every navigation */ - reload?: boolean; - /* defines the loading mechanism in relation to the main loop */ - strategy?: Exclude; - /* where to insert the script tag */ - target?: ScriptTarget; -} & BaseProps & - ScriptHTMLAttributes; - -type StrategyProps = BeforeHydrationProps | PostHydrationProps; - -export type ScriptProps = StrategyProps; +import { + ScriptProps, + ScriptTarget, + ScriptResponse, + ScriptCacheProps, +} from './types.js'; // Don't spread these props in the @@ -164,7 +164,7 @@ function ScriptsBeforeHydration() { ; + } + `, + errors: [error()], + }, + { + code: dedent` + import React from 'react'; + + function Component() { + return - - // inline external script via src - - - - - ); -} diff --git a/packages/hydrogen/src/foundation/Script/index.ts b/packages/hydrogen/src/foundation/Script/index.ts deleted file mode 100644 index 178aa08de9..0000000000 --- a/packages/hydrogen/src/foundation/Script/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type {ScriptProps} from './loadScript.js'; -export type {ScriptState} from './useLoadScript.client.js'; - -export {Script} from './Script.client.js'; -export {loadScript} from './loadScript.js'; -export {useLoadScript} from './useLoadScript.client.js'; diff --git a/packages/hydrogen/src/foundation/Script/loadScript.ts b/packages/hydrogen/src/foundation/Script/loadScript.ts index 0dcb7e11ec..b99d52d8b1 100644 --- a/packages/hydrogen/src/foundation/Script/loadScript.ts +++ b/packages/hydrogen/src/foundation/Script/loadScript.ts @@ -1,14 +1,3 @@ -/** - The `Script` component renders a ' + ) + ); + } else { + pathname.endsWith('proxytown') && + ev.respondWith(httpRequestFromWebWorker(req)); + } +}; diff --git a/packages/playground/server-components/public/~partytown/debug/partytown-ww-atomics.js b/packages/playground/server-components/public/~partytown/debug/partytown-ww-atomics.js new file mode 100644 index 0000000000..59878634ed --- /dev/null +++ b/packages/playground/server-components/public/~partytown/debug/partytown-ww-atomics.js @@ -0,0 +1,2569 @@ +/* Partytown 0.6.4 - MIT builder.io */ +((self) => { + const WinIdKey = Symbol(); + const InstanceIdKey = Symbol(); + const InstanceDataKey = Symbol(); + const NamespaceKey = Symbol(); + const ApplyPathKey = Symbol(); + const InstanceStateKey = Symbol(); + const HookContinue = Symbol(); + const HookPrevent = Symbol(); + const webWorkerInstances = new Map(); + const webWorkerRefsByRefId = {}; + const webWorkerRefIdsByRef = new WeakMap(); + const postMessages = []; + const webWorkerCtx = {}; + const webWorkerlocalStorage = new Map(); + const webWorkerSessionStorage = new Map(); + const environments = {}; + const cachedDimensions = new Map(); + const cachedStructure = new Map(); + const commaSplit = (str) => str.split(','); + const partytownLibUrl = (url) => { + url = webWorkerCtx.$libPath$ + url; + if (new URL(url).origin != location.origin) { + throw 'Invalid ' + url; + } + return url; + }; + const getterDimensionPropNames = commaSplit( + 'clientWidth,clientHeight,clientTop,clientLeft,innerWidth,innerHeight,offsetWidth,offsetHeight,offsetTop,offsetLeft,outerWidth,outerHeight,pageXOffset,pageYOffset,scrollWidth,scrollHeight,scrollTop,scrollLeft' + ); + const elementStructurePropNames = commaSplit( + 'childElementCount,children,firstElementChild,lastElementChild,nextElementSibling,previousElementSibling' + ); + const structureChangingMethodNames = commaSplit( + 'insertBefore,remove,removeChild,replaceChild' + ); + const dimensionChangingSetterNames = commaSplit( + 'className,width,height,hidden,innerHTML,innerText,textContent' + ); + const dimensionChangingMethodNames = commaSplit( + 'setAttribute,setAttributeNS,setProperty' + ); + const eventTargetMethods = commaSplit( + 'addEventListener,dispatchEvent,removeEventListener' + ); + const nonBlockingMethods = eventTargetMethods.concat( + dimensionChangingMethodNames, + commaSplit('add,observe,remove,unobserve') + ); + const IS_TAG_REG = /^[A-Z_]([A-Z0-9-]*[A-Z0-9])?$/; + const noop = () => {}; + const len = (obj) => obj.length; + const getConstructorName = (obj) => { + var _a, _b, _c; + try { + const constructorName = + null === (_a = null == obj ? void 0 : obj.constructor) || void 0 === _a + ? void 0 + : _a.name; + if (constructorName) { + return constructorName; + } + } catch (e) {} + try { + const zoneJsConstructorName = + null === + (_c = + null === + (_b = + null == obj ? void 0 : obj.__zone_symbol__originalInstance) || + void 0 === _b + ? void 0 + : _b.constructor) || void 0 === _c + ? void 0 + : _c.name; + if (zoneJsConstructorName) { + return zoneJsConstructorName; + } + } catch (e) {} + return ''; + }; + const EMPTY_ARRAY = []; + const randomId = () => + Math.round(Math.random() * Number.MAX_SAFE_INTEGER).toString(36); + const defineProperty = (obj, memberName, descriptor) => + Object.defineProperty(obj, memberName, { + ...descriptor, + configurable: true, + }); + const defineConstructorName = (Cstr, value) => + defineProperty(Cstr, 'name', { + value: value, + }); + const definePrototypeProperty = (Cstr, memberName, descriptor) => + defineProperty(Cstr.prototype, memberName, descriptor); + const definePrototypePropertyDescriptor = (Cstr, propertyDescriptorMap) => + Object.defineProperties(Cstr.prototype, propertyDescriptorMap); + const definePrototypeValue = (Cstr, memberName, value) => + definePrototypeProperty(Cstr, memberName, { + value: value, + writable: true, + }); + const hasInstanceStateValue = (instance, stateKey) => + stateKey in instance[InstanceStateKey]; + const getInstanceStateValue = (instance, stateKey) => + instance[InstanceStateKey][stateKey]; + const setInstanceStateValue = (instance, stateKey, stateValue) => + (instance[InstanceStateKey][stateKey] = stateValue); + const setWorkerRef = (ref, refId) => { + if (!(refId = webWorkerRefIdsByRef.get(ref))) { + webWorkerRefIdsByRef.set(ref, (refId = randomId())); + webWorkerRefsByRefId[refId] = ref; + } + return refId; + }; + const getOrCreateNodeInstance = ( + winId, + instanceId, + nodeName, + namespace, + instance + ) => { + instance = webWorkerInstances.get(instanceId); + if (!instance && nodeName && environments[winId]) { + instance = environments[winId].$createNode$( + nodeName, + instanceId, + namespace + ); + webWorkerInstances.set(instanceId, instance); + } + return instance; + }; + const definePrototypeNodeType = (Cstr, nodeType) => + definePrototypeValue(Cstr, 'nodeType', nodeType); + const cachedTreeProps = (Cstr, treeProps) => + treeProps.map((propName) => + definePrototypeProperty(Cstr, propName, { + get() { + let cacheKey = getInstanceCacheKey(this, propName); + let result = cachedStructure.get(cacheKey); + if (!result) { + result = getter(this, [propName]); + cachedStructure.set(cacheKey, result); + } + return result; + }, + }) + ); + const getInstanceCacheKey = (instance, memberName, args) => + [ + instance[WinIdKey], + instance[InstanceIdKey], + memberName, + ...(args || EMPTY_ARRAY).map((arg) => + String(arg && arg[WinIdKey] ? arg[InstanceIdKey] : arg) + ), + ].join('.'); + const cachedProps = (Cstr, propNames) => + commaSplit(propNames).map((propName) => + definePrototypeProperty(Cstr, propName, { + get() { + hasInstanceStateValue(this, propName) || + setInstanceStateValue(this, propName, getter(this, [propName])); + return getInstanceStateValue(this, propName); + }, + set(val) { + getInstanceStateValue(this, propName) !== val && + setter(this, [propName], val); + setInstanceStateValue(this, propName, val); + }, + }) + ); + const cachedDimensionProps = (Cstr) => + getterDimensionPropNames.map((propName) => + definePrototypeProperty(Cstr, propName, { + get() { + const dimension = cachedDimensions.get( + getInstanceCacheKey(this, propName) + ); + if ('number' == typeof dimension) { + return dimension; + } + const groupedDimensions = getter( + this, + [propName], + getterDimensionPropNames + ); + if (groupedDimensions && 'object' == typeof groupedDimensions) { + Object.entries(groupedDimensions).map( + ([dimensionPropName, value]) => + cachedDimensions.set( + getInstanceCacheKey(this, dimensionPropName), + value + ) + ); + return groupedDimensions[propName]; + } + return groupedDimensions; + }, + }) + ); + const cachedDimensionMethods = (Cstr, dimensionMethodNames) => + dimensionMethodNames.map((methodName) => { + Cstr.prototype[methodName] = function (...args) { + let cacheKey = getInstanceCacheKey(this, methodName, args); + let dimensions = cachedDimensions.get(cacheKey); + if (!dimensions) { + dimensions = callMethod(this, [methodName], args); + cachedDimensions.set(cacheKey, dimensions); + } + return dimensions; + }; + }); + const serializeForMain = ($winId$, $instanceId$, value, added, type) => + void 0 !== value && (type = typeof value) + ? 'string' === type || + 'boolean' === type || + 'number' === type || + null == value + ? [0, value] + : 'function' === type + ? [ + 4, + { + $winId$: $winId$, + $instanceId$: $instanceId$, + $refId$: setWorkerRef(value), + }, + ] + : (added = added || new Set()) && Array.isArray(value) + ? added.has(value) + ? [1, []] + : added.add(value) && [ + 1, + value.map((v) => + serializeForMain($winId$, $instanceId$, v, added) + ), + ] + : 'object' === type + ? value[InstanceIdKey] + ? [3, [value[WinIdKey], value[InstanceIdKey]]] + : value instanceof Event + ? [ + 5, + serializeObjectForMain( + $winId$, + $instanceId$, + value, + false, + added + ), + ] + : supportsTrustedHTML && value instanceof TrustedHTML + ? [0, value.toString()] + : value instanceof ArrayBuffer + ? [8, value] + : ArrayBuffer.isView(value) + ? [9, value.buffer, getConstructorName(value)] + : [ + 2, + serializeObjectForMain($winId$, $instanceId$, value, true, added), + ] + : void 0 + : value; + const supportsTrustedHTML = 'undefined' != typeof TrustedHTML; + const serializeObjectForMain = ( + winId, + instanceId, + obj, + includeFunctions, + added, + serializedObj, + propName, + propValue + ) => { + serializedObj = {}; + if (!added.has(obj)) { + added.add(obj); + for (propName in obj) { + propValue = obj[propName]; + (includeFunctions || 'function' != typeof propValue) && + (serializedObj[propName] = serializeForMain( + winId, + instanceId, + propValue, + added + )); + } + } + return serializedObj; + }; + const serializeInstanceForMain = (instance, value) => + instance + ? serializeForMain(instance[WinIdKey], instance[InstanceIdKey], value) + : [0, value]; + const deserializeFromMain = ( + winId, + instanceId, + applyPath, + serializedValueTransfer, + serializedType, + serializedValue, + obj, + key + ) => { + if (serializedValueTransfer) { + serializedType = serializedValueTransfer[0]; + serializedValue = serializedValueTransfer[1]; + if ( + 0 === serializedType || + 11 === serializedType || + 12 === serializedType + ) { + return serializedValue; + } + if (4 === serializedType) { + return deserializeRefFromMain(applyPath, serializedValue); + } + if (6 === serializedType) { + return winId && applyPath.length > 0 + ? (...args) => + callMethod(environments[winId].$window$, applyPath, args, 1) + : noop; + } + if (3 === serializedType) { + return getOrCreateSerializedInstance(serializedValue); + } + if (7 === serializedType) { + return new NodeList(serializedValue.map(getOrCreateSerializedInstance)); + } + if (10 === serializedType) { + return new Attr(serializedValue); + } + if (1 === serializedType) { + return serializedValue.map((v) => + deserializeFromMain(winId, instanceId, applyPath, v) + ); + } + if (14 === serializedType) { + return new CustomError(serializedValue); + } + obj = {}; + for (key in serializedValue) { + obj[key] = deserializeFromMain( + winId, + instanceId, + [...applyPath, key], + serializedValue[key] + ); + } + if (13 === serializedType) { + return new environments[winId].$window$.CSSStyleDeclaration( + winId, + instanceId, + applyPath, + obj + ); + } + if (5 === serializedType) { + if ('message' === obj.type && obj.origin) { + let postMessageKey = JSON.stringify(obj.data); + let postMessageData = postMessages.find( + (pm) => pm.$data$ === postMessageKey + ); + let env; + if (postMessageData) { + env = environments[postMessageData.$winId$]; + if (env) { + obj.source = env.$window$; + obj.origin = env.$location$.origin; + } + } + } + return new Proxy(new Event(obj.type, obj), { + get: (target, propName) => + propName in obj + ? obj[propName] + : 'function' == typeof target[String(propName)] + ? noop + : target[String(propName)], + }); + } + if (2 === serializedType) { + return obj; + } + } + }; + const getOrCreateSerializedInstance = ([winId, instanceId, nodeName]) => + instanceId === winId && environments[winId] + ? environments[winId].$window$ + : getOrCreateNodeInstance(winId, instanceId, nodeName); + const deserializeRefFromMain = ( + applyPath, + { + $winId$: $winId$, + $instanceId$: $instanceId$, + $nodeName$: $nodeName$, + $refId$: $refId$, + } + ) => { + webWorkerRefsByRefId[$refId$] || + webWorkerRefIdsByRef.set( + (webWorkerRefsByRefId[$refId$] = function (...args) { + const instance = getOrCreateNodeInstance( + $winId$, + $instanceId$, + $nodeName$ + ); + return callMethod(instance, applyPath, args); + }), + $refId$ + ); + return webWorkerRefsByRefId[$refId$]; + }; + class CustomError extends Error { + constructor(errorObject) { + super(errorObject.message); + this.name = errorObject.name; + this.message = errorObject.message; + this.stack = errorObject.stack; + } + } + const NodeList = class { + constructor(nodes) { + (this._ = nodes).map((node, index) => (this[index] = node)); + } + entries() { + return this._.entries(); + } + forEach(cb, thisArg) { + this._.map(cb, thisArg); + } + item(index) { + return this[index]; + } + keys() { + return this._.keys(); + } + get length() { + return len(this._); + } + values() { + return this._.values(); + } + [Symbol.iterator]() { + return this._[Symbol.iterator](); + } + }; + const Attr = class { + constructor(serializedAttr) { + this.name = serializedAttr[0]; + this.value = serializedAttr[1]; + } + get nodeName() { + return this.name; + } + get nodeType() { + return 2; + } + }; + const warnCrossOrgin = (apiType, apiName, env) => + console.warn( + `Partytown unable to ${apiType} cross-origin ${apiName}: ` + + env.$location$ + ); + const logWorker = (msg, winId) => { + try { + const config = webWorkerCtx.$config$; + if (config.logStackTraces) { + const frames = new Error().stack.split('\n'); + const i = frames.findIndex((f) => f.includes('logWorker')); + msg += '\n' + frames.slice(i + 1).join('\n'); + } + let prefix; + let color; + if (winId) { + prefix = `Worker (${normalizedWinId(winId)}) 🎉`; + color = winColor(winId); + } else { + prefix = self.name; + color = '#9844bf'; + } + if (webWorkerCtx.lastLog !== msg) { + webWorkerCtx.lastLog = msg; + console.debug.apply(console, [ + `%c${prefix}`, + `background: ${color}; color: white; padding: 2px 3px; border-radius: 2px; font-size: 0.8em;`, + msg, + ]); + } + } catch (e) {} + }; + const winIds = []; + const normalizedWinId = (winId) => { + winIds.includes(winId) || winIds.push(winId); + return winIds.indexOf(winId) + 1; + }; + const winColor = (winId) => { + const colors = ['#00309e', '#ea3655', '#eea727']; + const index = normalizedWinId(winId) - 1; + return colors[index] || colors[colors.length - 1]; + }; + const getTargetProp = (target, applyPath) => { + let n = ''; + if (target) { + target[InstanceIdKey]; + const cstrName = getConstructorName(target); + if ('Window' === cstrName) { + n = ''; + } else if ('string' == typeof target[InstanceDataKey]) { + let nodeName = target[InstanceDataKey]; + n = + '#text' === nodeName + ? 'textNode.' + : '#comment' === nodeName + ? 'commentNode.' + : '#document' === nodeName + ? 'document.' + : 'html' === nodeName + ? 'doctype.' + : nodeName.toLowerCase() + '.'; + } else { + n = + 'nodeType' in target && 2 === target.nodeType + ? 'attributes.' + : 'CanvasRenderingContext2D' === cstrName + ? 'context2D.' + : 'CanvasRenderingContextWebGL' === cstrName + ? 'contextWebGL.' + : 'CSSStyleDeclaration' === cstrName + ? 'style.' + : 'MutationObserver' === cstrName + ? 'mutationObserver.' + : 'NamedNodeMap' === cstrName + ? 'namedNodeMap.' + : 'ResizeObserver' === cstrName + ? 'resizeObserver.' + : cstrName.substring(0, 1).toLowerCase() + + cstrName.substring(1) + + '.'; + } + target[ApplyPathKey] && + target[ApplyPathKey].length && + (n += [...target[ApplyPathKey]].join('.') + '.'); + } + if (applyPath.length > 1) { + const first = applyPath.slice(0, applyPath.length - 1); + const last = applyPath[applyPath.length - 1]; + if (!isNaN(last)) { + return n + `${first.join('.')}[${last}]`; + } + } + return n + applyPath.join('.'); + }; + const getLogValue = (applyPath, v) => { + const type = typeof v; + if (void 0 === v) { + return 'undefined'; + } + if ('boolean' === type || 'number' === type || null == v) { + return JSON.stringify(v); + } + if ('string' === type) { + return applyPath.includes('cookie') + ? JSON.stringify(v.slice(0, 10) + '...') + : JSON.stringify(v.length > 50 ? v.slice(0, 40) + '...' : v); + } + if (Array.isArray(v)) { + return `[${v.map(getLogValue).join(', ')}]`; + } + if ('object' === type) { + const instanceId = v[InstanceIdKey]; + const cstrName = getConstructorName(v); + if ('string' == typeof instanceId) { + if ('Window' === cstrName) { + return 'window'; + } + if ('string' == typeof v[InstanceDataKey]) { + if (1 === v.nodeType) { + return `<${v[InstanceDataKey].toLowerCase()}>`; + } + if (10 === v.nodeType) { + return ``; + } + if (v.nodeType <= 11) { + return v[InstanceDataKey]; + } + } + return '¯\\_(ツ)_/¯ instance obj'; + } + return v[Symbol.iterator] + ? `[${Array.from(v) + .map((i) => getLogValue(applyPath, i)) + .join(', ')}]` + : 'value' in v + ? 'string' == typeof v.value + ? `"${v.value}"` + : objToString(v.value) + : objToString(v); + } + return ((v) => 'object' == typeof v && v && v.then)(v) + ? 'Promise' + : 'function' === type + ? `ƒ() ${v.name || ''}`.trim() + : `¯\\_(ツ)_/¯ ${String(v)}`.trim(); + }; + const objToString = (obj) => { + const s = []; + for (let key in obj) { + const value = obj[key]; + const type = typeof value; + 'string' === type + ? s.push(`${key}: "${value}"`) + : 'function' === type + ? s.push(`${key}: ƒ`) + : Array.isArray(type) + ? s.push(`${key}: [..]`) + : 'object' === type && value + ? s.push(`${key}: {..}`) + : s.push(`${key}: ${String(value)}`); + } + let str = s.join(', '); + str.length > 200 && (str = str.substring(0, 200) + '..'); + return `{ ${str} }`; + }; + const logDimensionCacheClearStyle = (target, propName) => { + (webWorkerCtx.$config$.logGetters || webWorkerCtx.$config$.logSetters) && + logWorker( + `Dimension cache cleared from style.${propName} setter`, + target[WinIdKey] + ); + }; + const logDimensionCacheClearMethod = (target, methodName) => { + (webWorkerCtx.$config$.logGetters || webWorkerCtx.$config$.logCalls) && + logWorker( + `Dimension cache cleared from method call ${methodName}()`, + target[WinIdKey] + ); + }; + const taskQueue = []; + const queue = ( + instance, + $applyPath$, + callType, + $assignInstanceId$, + $groupedGetters$, + buffer + ) => { + if (instance[ApplyPathKey]) { + taskQueue.push({ + $winId$: instance[WinIdKey], + $instanceId$: instance[InstanceIdKey], + $applyPath$: [...instance[ApplyPathKey], ...$applyPath$], + $assignInstanceId$: $assignInstanceId$, + $groupedGetters$: $groupedGetters$, + }); + taskQueue[len(taskQueue) - 1].$debug$ = (( + target, + applyPath, + callType + ) => { + let m = getTargetProp(target, applyPath); + 1 === callType + ? (m += ' (blocking)') + : 2 === callType + ? (m += ' (non-blocking)') + : 3 === callType && (m += ' (non-blocking, no-side-effect)'); + return m.trim(); + })(instance, $applyPath$, callType); + buffer && + 3 !== callType && + console.error('buffer must be sent NonBlockingNoSideEffect'); + if (3 === callType) { + webWorkerCtx.$postMessage$( + [ + 12, + { + $msgId$: randomId(), + $tasks$: [...taskQueue], + }, + ], + buffer + ? [buffer instanceof ArrayBuffer ? buffer : buffer.buffer] + : void 0 + ); + taskQueue.length = 0; + } else if (1 === callType) { + return sendToMain(true); + } + webWorkerCtx.$asyncMsgTimer$ = setTimeout(sendToMain, 20); + } + }; + const sendToMain = (isBlocking) => { + clearTimeout(webWorkerCtx.$asyncMsgTimer$); + if (len(taskQueue)) { + webWorkerCtx.$config$.logMainAccess && + logWorker(`Main access, tasks sent: ${taskQueue.length}`); + const endTask = taskQueue[len(taskQueue) - 1]; + const accessReq = { + $msgId$: randomId(), + $tasks$: [...taskQueue], + }; + taskQueue.length = 0; + if (isBlocking) { + const accessRsp = ((webWorkerCtx, accessReq) => { + const sharedDataBuffer = webWorkerCtx.$sharedDataBuffer$; + const sharedData = new Int32Array(sharedDataBuffer); + Atomics.store(sharedData, 0, 0); + webWorkerCtx.$postMessage$([11, accessReq]); + Atomics.wait(sharedData, 0, 0); + let dataLength = Atomics.load(sharedData, 0); + let accessRespStr = ''; + let i = 0; + for (; i < dataLength; i++) { + accessRespStr += String.fromCharCode(sharedData[i + 1]); + } + return JSON.parse(accessRespStr); + })(webWorkerCtx, accessReq); + const isPromise = accessRsp.$isPromise$; + const rtnValue = deserializeFromMain( + endTask.$winId$, + endTask.$instanceId$, + endTask.$applyPath$, + accessRsp.$rtnValue$ + ); + if (accessRsp.$error$) { + if (isPromise) { + return Promise.reject(accessRsp.$error$); + } + throw new Error(accessRsp.$error$); + } + return isPromise ? Promise.resolve(rtnValue) : rtnValue; + } + webWorkerCtx.$postMessage$([12, accessReq]); + } + }; + const getter = (instance, applyPath, groupedGetters, rtnValue) => { + if (webWorkerCtx.$config$.get) { + rtnValue = webWorkerCtx.$config$.get( + createHookOptions(instance, applyPath) + ); + if (rtnValue !== HookContinue) { + return rtnValue; + } + } + rtnValue = queue(instance, applyPath, 1, void 0, groupedGetters); + (( + target, + applyPath, + rtnValue, + restrictedToWorker = false, + groupedGetters = false + ) => { + if (webWorkerCtx.$config$.logGetters) { + try { + const msg = `Get ${getTargetProp( + target, + applyPath + )}, returned: ${getLogValue(applyPath, rtnValue)}${ + restrictedToWorker ? ' (restricted to worker)' : '' + }${groupedGetters ? ' (grouped getter)' : ''}`; + msg.includes('Symbol(') || logWorker(msg, target[WinIdKey]); + } catch (e) {} + } + })(instance, applyPath, rtnValue, false, !!groupedGetters); + return rtnValue; + }; + const setter = (instance, applyPath, value, hookSetterValue) => { + if (webWorkerCtx.$config$.set) { + hookSetterValue = webWorkerCtx.$config$.set({ + value: value, + prevent: HookPrevent, + ...createHookOptions(instance, applyPath), + }); + if (hookSetterValue === HookPrevent) { + return; + } + hookSetterValue !== HookContinue && (value = hookSetterValue); + } + if (dimensionChangingSetterNames.some((s) => applyPath.includes(s))) { + cachedDimensions.clear(); + ((target, propName) => { + (webWorkerCtx.$config$.logGetters || + webWorkerCtx.$config$.logSetters) && + logWorker( + `Dimension cache cleared from setter "${propName}"`, + target[WinIdKey] + ); + })(instance, applyPath[applyPath.length - 1]); + } + applyPath = [...applyPath, serializeInstanceForMain(instance, value), 0]; + ((target, applyPath, value, restrictedToWorker = false) => { + if (webWorkerCtx.$config$.logSetters) { + try { + applyPath = applyPath.slice(0, applyPath.length - 2); + logWorker( + `Set ${getTargetProp(target, applyPath)}, value: ${getLogValue( + applyPath, + value + )}${restrictedToWorker ? ' (restricted to worker)' : ''}`, + target[WinIdKey] + ); + } catch (e) {} + } + })(instance, applyPath, value); + queue(instance, applyPath, 2); + }; + const callMethod = ( + instance, + applyPath, + args, + callType, + assignInstanceId, + buffer, + rtnValue, + methodName + ) => { + if (webWorkerCtx.$config$.apply) { + rtnValue = webWorkerCtx.$config$.apply({ + args: args, + ...createHookOptions(instance, applyPath), + }); + if (rtnValue !== HookContinue) { + return rtnValue; + } + } + methodName = applyPath[len(applyPath) - 1]; + applyPath = [...applyPath, serializeInstanceForMain(instance, args)]; + callType = callType || (nonBlockingMethods.includes(methodName) ? 2 : 1); + if ( + 'setAttribute' === methodName && + hasInstanceStateValue(instance, args[0]) + ) { + setInstanceStateValue(instance, args[0], args[1]); + } else if (structureChangingMethodNames.includes(methodName)) { + cachedDimensions.clear(); + cachedStructure.clear(); + ((target, methodName) => { + (webWorkerCtx.$config$.logGetters || webWorkerCtx.$config$.logCalls) && + logWorker( + `Dimension and DOM structure cache cleared from method call ${methodName}()`, + target[WinIdKey] + ); + })(instance, methodName); + } else if (dimensionChangingMethodNames.includes(methodName)) { + callType = 2; + cachedDimensions.clear(); + logDimensionCacheClearMethod(instance, methodName); + } + rtnValue = queue( + instance, + applyPath, + callType, + assignInstanceId, + void 0, + buffer + ); + ((target, applyPath, args, rtnValue) => { + if (webWorkerCtx.$config$.logCalls) { + try { + applyPath = applyPath.slice(0, applyPath.length - 1); + logWorker( + `Call ${getTargetProp(target, applyPath)}(${args + .map((v) => getLogValue(applyPath, v)) + .join(', ')}), returned: ${getLogValue(applyPath, rtnValue)}`, + target[WinIdKey] + ); + } catch (e) {} + } + })(instance, applyPath, args, rtnValue); + return rtnValue; + }; + const constructGlobal = (instance, cstrName, args) => { + ((target, cstrName, args) => { + if (webWorkerCtx.$config$.logCalls) { + try { + logWorker( + `Construct new ${cstrName}(${args + .map((v) => getLogValue([], v)) + .join(', ')})`, + target[WinIdKey] + ); + } catch (e) {} + } + })(instance, cstrName, args); + queue(instance, [1, cstrName, serializeInstanceForMain(instance, args)], 1); + }; + const createHookOptions = (instance, applyPath) => ({ + name: applyPath.join('.'), + continue: HookContinue, + nodeName: instance[InstanceDataKey], + constructor: getConstructorName(instance), + }); + const addStorageApi = (win, storageName, storages, isSameOrigin, env) => { + let getItems = (items) => { + items = storages.get(win.origin); + items || storages.set(win.origin, (items = [])); + return items; + }; + let getIndexByKey = (key) => + getItems().findIndex((i) => i[STORAGE_KEY] === key); + let index; + let item; + let storage = { + getItem(key) { + index = getIndexByKey(key); + return index > -1 ? getItems()[index][STORAGE_VALUE] : null; + }, + setItem(key, value) { + index = getIndexByKey(key); + index > -1 + ? (getItems()[index][STORAGE_VALUE] = value) + : getItems().push([key, value]); + isSameOrigin + ? callMethod(win, [storageName, 'setItem'], [key, value], 2) + : warnCrossOrgin('set', storageName, env); + }, + removeItem(key) { + index = getIndexByKey(key); + index > -1 && getItems().splice(index, 1); + isSameOrigin + ? callMethod(win, [storageName, 'removeItem'], [key], 2) + : warnCrossOrgin('remove', storageName, env); + }, + key(index) { + item = getItems()[index]; + return item ? item[STORAGE_KEY] : null; + }, + clear() { + getItems().length = 0; + isSameOrigin + ? callMethod(win, [storageName, 'clear'], EMPTY_ARRAY, 2) + : warnCrossOrgin('clear', storageName, env); + }, + get length() { + return getItems().length; + }, + }; + win[storageName] = storage; + }; + const STORAGE_KEY = 0; + const STORAGE_VALUE = 1; + const createCSSStyleDeclarationCstr = (win, WorkerBase, cstrName) => { + win[cstrName] = defineConstructorName( + class extends WorkerBase { + constructor(winId, instanceId, applyPath, styles) { + super(winId, instanceId, applyPath, styles || {}); + return new Proxy(this, { + get(target, propName) { + if (target[propName]) { + return target[propName]; + } + target[propName] || + 'string' != typeof propName || + target[InstanceDataKey][propName] || + (target[InstanceDataKey][propName] = getter(target, [ + propName, + ])); + return target[InstanceDataKey][propName]; + }, + set(target, propName, propValue) { + target[InstanceDataKey][propName] = propValue; + setter(target, [propName], propValue); + logDimensionCacheClearStyle(target, propName); + cachedDimensions.clear(); + return true; + }, + }); + } + setProperty(...args) { + this[InstanceDataKey][args[0]] = args[1]; + callMethod(this, ['setProperty'], args, 2); + logDimensionCacheClearStyle(this, args[0]); + cachedDimensions.clear(); + } + getPropertyValue(propName) { + return this[propName]; + } + removeProperty(propName) { + let value = this[InstanceDataKey][propName]; + callMethod(this, ['removeProperty'], [propName], 2); + logDimensionCacheClearStyle(this, propName); + cachedDimensions.clear(); + this[InstanceDataKey][propName] = void 0; + return value; + } + }, + cstrName + ); + }; + const createCSSStyleSheetConstructor = (win, cssStyleSheetCstrName) => { + win[cssStyleSheetCstrName] = defineConstructorName( + class { + constructor(ownerNode) { + this.ownerNode = ownerNode; + } + get cssRules() { + const ownerNode = this.ownerNode; + return new Proxy( + {}, + { + get(target, propKey) { + const propName = String(propKey); + return 'item' === propName + ? (index) => getCssRule(ownerNode, index) + : 'length' === propName + ? getCssRules(ownerNode).length + : isNaN(propName) + ? target[propKey] + : getCssRule(ownerNode, propName); + }, + } + ); + } + insertRule(ruleText, index) { + const cssRules = getCssRules(this.ownerNode); + index = void 0 === index ? 0 : index; + if (index >= 0 && index <= cssRules.length) { + callMethod( + this.ownerNode, + ['sheet', 'insertRule'], + [ruleText, index], + 2 + ); + cssRules.splice(index, 0, 0); + } + logDimensionCacheClearMethod(this.ownerNode, 'insertRule'); + cachedDimensions.clear(); + return index; + } + deleteRule(index) { + callMethod(this.ownerNode, ['sheet', 'deleteRule'], [index], 2); + getCssRules(this.ownerNode).splice(index, 1); + logDimensionCacheClearMethod(this.ownerNode, 'deleteRule'); + cachedDimensions.clear(); + } + get type() { + return 'text/css'; + } + }, + cssStyleSheetCstrName + ); + const HTMLStyleDescriptorMap = { + sheet: { + get() { + return new win[cssStyleSheetCstrName](this); + }, + }, + }; + definePrototypePropertyDescriptor( + win.HTMLStyleElement, + HTMLStyleDescriptorMap + ); + }; + const getCssRules = (ownerNode, cssRules) => { + cssRules = getInstanceStateValue(ownerNode, 2); + if (!cssRules) { + cssRules = getter(ownerNode, ['sheet', 'cssRules']); + setInstanceStateValue(ownerNode, 2, cssRules); + } + return cssRules; + }; + const getCssRule = (ownerNode, index, cssRules) => { + cssRules = getCssRules(ownerNode); + 0 === cssRules[index] && + (cssRules[index] = getter(ownerNode, [ + 'sheet', + 'cssRules', + parseInt(index, 10), + ])); + return cssRules[index]; + }; + const runScriptContent = ( + env, + instanceId, + scriptContent, + winId, + errorMsg + ) => { + try { + webWorkerCtx.$config$.logScriptExecution && + logWorker( + `Execute script: ${scriptContent + .substring(0, 100) + .split('\n') + .map((l) => l.trim()) + .join(' ') + .trim() + .substring(0, 60)}...`, + winId + ); + env.$currentScriptId$ = instanceId; + run(env, scriptContent); + } catch (contentError) { + console.error(scriptContent, contentError); + errorMsg = String(contentError.stack || contentError); + } + env.$currentScriptId$ = ''; + return errorMsg; + }; + const run = (env, scriptContent, scriptUrl) => { + env.$runWindowLoadEvent$ = 1; + scriptContent = + `with(this){${ + (webWorkerCtx.$config$.globalFns || []) + .filter((globalFnName) => + /[a-zA-Z_$][0-9a-zA-Z_$]*/.test(globalFnName) + ) + .map((g) => `(typeof ${g}=='function'&&(window.${g}=${g}))`) + .join(';') + + scriptContent + .replace(/\bthis\b/g, '(thi$(this)?window:this)') + .replace(/\/\/# so/g, '//Xso') + }\n;function thi$(t){return t===this}}` + + (scriptUrl ? '\n//# sourceURL=' + scriptUrl : ''); + env.$isSameOrigin$ || + (scriptContent = scriptContent.replace( + /.postMessage\(/g, + `.postMessage('${env.$winId$}',` + )); + new Function(scriptContent).call(env.$window$); + env.$runWindowLoadEvent$ = 0; + }; + const runStateLoadHandlers = (instance, type, handlers) => { + handlers = getInstanceStateValue(instance, type); + handlers && + setTimeout(() => + handlers.map((cb) => + cb({ + type: type, + }) + ) + ); + }; + const resolveToUrl = ( + env, + url, + type, + baseLocation, + resolvedUrl, + configResolvedUrl + ) => { + baseLocation = env.$location$; + while (!baseLocation.host) { + env = environments[env.$parentWinId$]; + baseLocation = env.$location$; + if (env.$winId$ === env.$parentWinId$) { + break; + } + } + resolvedUrl = new URL(url || '', baseLocation); + if (type && webWorkerCtx.$config$.resolveUrl) { + configResolvedUrl = webWorkerCtx.$config$.resolveUrl( + resolvedUrl, + baseLocation, + type + ); + if (configResolvedUrl) { + return configResolvedUrl; + } + } + return resolvedUrl; + }; + const resolveUrl = (env, url, type) => resolveToUrl(env, url, type) + ''; + const getPartytownScript = () => + `' + ) + ) + : i.endsWith('proxytown') && + n.respondWith( + ((n) => + new Promise(async (s) => { + const i = await n.clone().json(), + o = await ((r) => + new Promise(async (n) => { + const s = [...(await self.clients.matchAll())].sort( + (e, t) => (e.url > t.url ? -1 : e.url < t.url ? 1 : 0) + )[0]; + if (s) { + const i = [ + n, + setTimeout(() => { + e.delete(r.F), n(t(r, 'Timeout')); + }, 1e4), + ]; + e.set(r.F, i), s.postMessage(r); + } else n(t(r, 'NoParty')); + }))(i); + s(r(JSON.stringify(o), 'application/json')); + }))(s) + ); + }); diff --git a/packages/playground/server-components/public/~partytown/partytown.js b/packages/playground/server-components/public/~partytown/partytown.js new file mode 100644 index 0000000000..3fa1d810c7 --- /dev/null +++ b/packages/playground/server-components/public/~partytown/partytown.js @@ -0,0 +1,72 @@ +/* Partytown 0.6.4 - MIT builder.io */ +!(function (t, e, n, i, r, o, a, d, s, c, p, l) { + function u() { + l || + ((l = 1), + '/' == (a = (o.lib || '/~partytown/') + (o.debug ? 'debug/' : ''))[0] && + ((s = e.querySelectorAll('script[type="text/partytown"]')), + i != t + ? i.dispatchEvent(new CustomEvent('pt1', {detail: t})) + : ((d = setTimeout(w, 1e4)), + e.addEventListener('pt0', f), + r + ? h(1) + : n.serviceWorker + ? n.serviceWorker + .register(a + (o.swPath || 'partytown-sw.js'), {scope: a}) + .then(function (t) { + t.active + ? h() + : t.installing && + t.installing.addEventListener( + 'statechange', + function (t) { + 'activated' == t.target.state && h(); + } + ); + }, console.error) + : w()))); + } + function h(t) { + (c = e.createElement(t ? 'script' : 'iframe')), + t || + (c.setAttribute( + 'style', + 'display:block;width:0;height:0;border:0;visibility:hidden' + ), + c.setAttribute('aria-hidden', !0)), + (c.src = + a + + 'partytown-' + + (t ? 'atomics.js?v=0.6.4' : 'sandbox-sw.html?' + Date.now())), + e.body.appendChild(c); + } + function w(t, n) { + for (f(), t = 0; t < s.length; t++) + ((n = e.createElement('script')).innerHTML = s[t].innerHTML), + e.head.appendChild(n); + c && c.parentNode.removeChild(c); + } + function f() { + clearTimeout(d); + } + (o = t.partytown || {}), + i == t && + (o.forward || []).map(function (e) { + (p = t), + e.split('.').map(function (e, n, i) { + p = p[i[n]] = + n + 1 < i.length + ? 'push' == i[n + 1] + ? [] + : p[i[n]] || {} + : function () { + (t._ptf = t._ptf || []).push(i, arguments); + }; + }); + }), + 'complete' == e.readyState + ? u() + : (t.addEventListener('DOMContentLoaded', u), + t.addEventListener('load', u)); +})(window, document, navigator, top, window.crossOriginIsolated); diff --git a/packages/playground/server-components/src/App.server.jsx b/packages/playground/server-components/src/App.server.jsx index a168f10746..b46b251a47 100644 --- a/packages/playground/server-components/src/App.server.jsx +++ b/packages/playground/server-components/src/App.server.jsx @@ -1,4 +1,7 @@ import renderHydrogen from '@shopify/hydrogen/entry-server'; +import {partytownSnippet} from '@builder.io/partytown/integration'; +// import {Partytown} from '@builder.io/partytown/react'; + import { Route, Router, @@ -6,22 +9,92 @@ import { ShopifyProvider, useRequestContext, } from '@shopify/hydrogen'; +import {Script} from '@shopify/hydrogen/experimental'; import {Suspense} from 'react'; import Custom1 from './customRoutes/custom1.server'; import Custom2 from './customRoutes/custom2.server'; import LazyRoute from './customRoutes/lazyRoute.server'; import ServerParams from './customRoutes/params.server'; +/* + Set the required response headers to enable partytown atomics + @see: https://partytown.builder.io/atomics +*/ +function enablePartytownAtomic(response) { + response.headers.set('Cross-Origin-Embedder-Policy', 'credentialless'); + response.headers.set('Cross-Origin-Opener-Policy', 'same-origin'); +} + export default renderHydrogen(({request, response}) => { if (request.headers.get('user-agent') === 'custom bot') { response.doNotStream(); } + enablePartytownAtomic(response); + useRequestContext().test1 = true; useRequestContext('scope').test2 = true; return ( + {/* Add to App.server.jsx */} + + {/* partytown config */} + ); }); diff --git a/packages/playground/server-components/src/components/Hydration/HydrationComplete.server.jsx b/packages/playground/server-components/src/components/Hydration/HydrationComplete.server.jsx deleted file mode 100644 index 87798b7011..0000000000 --- a/packages/playground/server-components/src/components/Hydration/HydrationComplete.server.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import HydrationCompleteTrigger from './HydrationCompleteTrigger.client'; -import HydrationCompleteListener from './HydrationCompleteListener.server'; - -export default function HydrationComplete() { - return ( - - - - ); -} diff --git a/packages/playground/server-components/src/components/Hydration/HydrationCompleteListener.server.jsx b/packages/playground/server-components/src/components/Hydration/HydrationCompleteListener.server.jsx deleted file mode 100644 index 6084544f43..0000000000 --- a/packages/playground/server-components/src/components/Hydration/HydrationCompleteListener.server.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import {Script} from '@shopify/hydrogen/experimental'; - -export default function HydrationCompleteListener() { - return ( - - - - - - - + + + + {/* ready callback */} + + + {/* error callback */} + + + ); +} diff --git a/packages/playground/server-components/src/routes/scripts/script/after-hydration/[handle].server.jsx b/packages/playground/server-components/src/routes/scripts/script/after-hydration/[handle].server.jsx new file mode 100644 index 0000000000..49ddc9e683 --- /dev/null +++ b/packages/playground/server-components/src/routes/scripts/script/after-hydration/[handle].server.jsx @@ -0,0 +1,17 @@ +import {Link} from '@shopify/hydrogen'; +import {ScriptsAfterHydration} from './ScriptsAfterHydration'; + +export default function ScriptsAfterHydrationNestedRoute() { + return ( +
+

afterHydration (nested) scripts

+ + + +
+ Back +
+ +
+ ); +} diff --git a/packages/playground/server-components/src/routes/scripts/script/after-hydration/index.server.jsx b/packages/playground/server-components/src/routes/scripts/script/after-hydration/index.server.jsx new file mode 100644 index 0000000000..1f3d490759 --- /dev/null +++ b/packages/playground/server-components/src/routes/scripts/script/after-hydration/index.server.jsx @@ -0,0 +1,22 @@ +import {Link} from '@shopify/hydrogen'; +import {ScriptsAfterHydration} from './ScriptsAfterHydration'; + +export default function ScriptsAfterHydrationRoute() { + return ( +
+

afterHydration scripts

+ + +
+ + Simulate navigation + +
+ +
+ Back +
+ +
+ ); +} diff --git a/packages/playground/server-components/src/routes/scripts/script/before-hydration/index.server.jsx b/packages/playground/server-components/src/routes/scripts/script/before-hydration/index.server.jsx new file mode 100644 index 0000000000..5682a39b10 --- /dev/null +++ b/packages/playground/server-components/src/routes/scripts/script/before-hydration/index.server.jsx @@ -0,0 +1,54 @@ +import {Link} from '@shopify/hydrogen'; +import {Script} from '@shopify/hydrogen/experimental'; + +export default function ScriptsBeforeHydration() { + return ( +
+

beforeHydration scripts

+

+ {``} are executed if rendered on the + initial page load (like regular {` + + {/* + + {/* Test forwarded function from worker */} + + + + + + + + + {/* */} + + + {/* ready callback */} + + + {/* error callback */} + + + ); +} diff --git a/packages/playground/server-components/src/routes/scripts/script/on-idle/[handle].server.jsx b/packages/playground/server-components/src/routes/scripts/script/on-idle/[handle].server.jsx new file mode 100644 index 0000000000..d3f55b2bf4 --- /dev/null +++ b/packages/playground/server-components/src/routes/scripts/script/on-idle/[handle].server.jsx @@ -0,0 +1,17 @@ +import {Link} from '@shopify/hydrogen'; +import {ScriptsOnIdle} from './ScriptsOnIdle'; + +export default function ScriptsOnIdleTests() { + return ( +

+

onIdle scripts tests (nested)

+ + + +
+ Back +
+ +
+ ); +} diff --git a/packages/playground/server-components/src/routes/scripts/script/on-idle/index.server.jsx b/packages/playground/server-components/src/routes/scripts/script/on-idle/index.server.jsx new file mode 100644 index 0000000000..fac70a42b6 --- /dev/null +++ b/packages/playground/server-components/src/routes/scripts/script/on-idle/index.server.jsx @@ -0,0 +1,22 @@ +import {Link} from '@shopify/hydrogen'; +import {ScriptsOnIdle} from './ScriptsOnIdle'; + +export default function ScriptsOnIdleTests() { + return ( +
+

onIdle scripts tests

+ + +
+ + Simulate navigation + +
+ +
+ Back +
+ +
+ ); +} diff --git a/packages/playground/server-components/src/routes/scripts/use-script/UseLoadScripts.jsx b/packages/playground/server-components/src/routes/scripts/use-script/UseLoadScripts.jsx new file mode 100644 index 0000000000..5b67cff060 --- /dev/null +++ b/packages/playground/server-components/src/routes/scripts/use-script/UseLoadScripts.jsx @@ -0,0 +1,29 @@ +import ScriptUseLoadScript from '../../../components/ScriptUseLoadScript.client'; + +export default function () { + return ( + <> + {/* afterHydration */} + + + + + {/* onIdle */} + + + ); +} diff --git a/packages/playground/server-components/src/routes/scripts/use-script/[handle].server.jsx b/packages/playground/server-components/src/routes/scripts/use-script/[handle].server.jsx new file mode 100644 index 0000000000..1a5e35882a --- /dev/null +++ b/packages/playground/server-components/src/routes/scripts/use-script/[handle].server.jsx @@ -0,0 +1,16 @@ +import {Link} from '@shopify/hydrogen'; +import UseLoadScripts from './UseLoadScripts'; + +export default function () { + return ( + <> + + + +
+ Simulate navigation (back) +
+ + + ); +} diff --git a/packages/playground/server-components/src/routes/scripts/use-script/index.server.jsx b/packages/playground/server-components/src/routes/scripts/use-script/index.server.jsx new file mode 100644 index 0000000000..5e4024fe17 --- /dev/null +++ b/packages/playground/server-components/src/routes/scripts/use-script/index.server.jsx @@ -0,0 +1,21 @@ +import {Link} from '@shopify/hydrogen'; +import UseLoadScripts from './UseLoadScripts'; + +export default function () { + return ( + <> + + + +
+ Simulate navigation +
+ + +
+ Back to /scripts +
+ + + ); +} From 645ba78f4033cbfdaf188055d0586565b43616cd Mon Sep 17 00:00:00 2001 From: "Juan P. Prieto" Date: Fri, 16 Sep 2022 16:23:36 -0700 Subject: [PATCH 11/26] add Script and useScript e2e tests --- .../server-components/tests/e2e-test-cases.ts | 453 +++++++++++++++++- 1 file changed, 452 insertions(+), 1 deletion(-) diff --git a/packages/playground/server-components/tests/e2e-test-cases.ts b/packages/playground/server-components/tests/e2e-test-cases.ts index f65ce3d4cc..7f281bf910 100644 --- a/packages/playground/server-components/tests/e2e-test-cases.ts +++ b/packages/playground/server-components/tests/e2e-test-cases.ts @@ -829,7 +829,7 @@ export default async function testCases({ }); }); - describe('Load 3rd-party scripts', () => { + describe('Load 3rd-party scripts (legacy loadScript)', () => { it('should load script in the body', async () => { await page.goto(getServerUrl() + '/loadscript/body', { waitUntil: 'networkidle', @@ -912,4 +912,455 @@ export default async function testCases({ expect(await page.textContent('h2')).toContain('itBroke is not defined'); }); }); + + describe.only(' + + // or via src — not recommended as this would block the main thread. + // use afterHydration or onIdle instead to improve performance + + + // with a src + + + // with a src + .... From 538ad4d8fb2718380b982c83879d0e7342a9b073 Mon Sep 17 00:00:00 2001 From: "Juan P. Prieto" Date: Tue, 27 Sep 2022 16:03:50 -0700 Subject: [PATCH 16/26] ignore Script lint rules in test routes --- .../scripts/script/before-hydration/index.server.jsx | 8 ++++++-- .../src/routes/scripts/script/in-worker/index.server.jsx | 5 +++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/playground/server-components/src/routes/scripts/script/before-hydration/index.server.jsx b/packages/playground/server-components/src/routes/scripts/script/before-hydration/index.server.jsx index 5682a39b10..8e09c80a34 100644 --- a/packages/playground/server-components/src/routes/scripts/script/before-hydration/index.server.jsx +++ b/packages/playground/server-components/src/routes/scripts/script/before-hydration/index.server.jsx @@ -20,6 +20,7 @@ export default function ScriptsBeforeHydration() {
+ {/* eslint-disable-next-line hydrogen/prefer-script-component */} - {/* + {/* eslint-disable-next-line hydrogen/scripts-in-layout-components */} + {/* load partytown lib/runtime last so that it can pick type="text/partytown" scripts */} + +
); }); From 282b3f71f9295ce96d06ac96fd938803089a3c0e Mon Sep 17 00:00:00 2001 From: "Juan P. Prieto" Date: Tue, 27 Sep 2022 16:05:31 -0700 Subject: [PATCH 18/26] temporarily export Script from index --- packages/hydrogen/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/hydrogen/src/index.ts b/packages/hydrogen/src/index.ts index 894c7b38dc..17584fe711 100644 --- a/packages/hydrogen/src/index.ts +++ b/packages/hydrogen/src/index.ts @@ -45,6 +45,7 @@ export {useServerAnalytics} from './foundation/Analytics/hook.js'; export {ShopifyAnalytics} from './foundation/Analytics/connectors/Shopify/ShopifyAnalytics.server.js'; export {ShopifyAnalyticsConstants} from './foundation/Analytics/connectors/Shopify/const.js'; export {useSession} from './foundation/useSession/useSession.js'; +export {Script} from './components/Script/Script.client.js'; export {Cookie} from './foundation/Cookie/Cookie.js'; /** From f742badaf3241cb7314e34b4e75ec3f1d7c57a39 Mon Sep 17 00:00:00 2001 From: "Juan P. Prieto" Date: Tue, 27 Sep 2022 16:09:53 -0700 Subject: [PATCH 19/26] re-enable all e2e tests --- packages/playground/server-components/tests/e2e-test-cases.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/playground/server-components/tests/e2e-test-cases.ts b/packages/playground/server-components/tests/e2e-test-cases.ts index b5d3f5cdef..b58e2ed179 100644 --- a/packages/playground/server-components/tests/e2e-test-cases.ts +++ b/packages/playground/server-components/tests/e2e-test-cases.ts @@ -950,7 +950,7 @@ export default async function testCases({ }); }); - describe.only(' ); diff --git a/packages/playground/server-components/src/components/ScriptCallbacks.client.jsx b/packages/playground/async-config/src/components/ScriptCallbacks.client.jsx similarity index 100% rename from packages/playground/server-components/src/components/ScriptCallbacks.client.jsx rename to packages/playground/async-config/src/components/ScriptCallbacks.client.jsx diff --git a/packages/playground/server-components/src/components/ScriptLoadScript.client.jsx b/packages/playground/async-config/src/components/ScriptLoadScript.client.jsx similarity index 100% rename from packages/playground/server-components/src/components/ScriptLoadScript.client.jsx rename to packages/playground/async-config/src/components/ScriptLoadScript.client.jsx diff --git a/packages/playground/server-components/src/components/ScriptUseLoadScript.client.jsx b/packages/playground/async-config/src/components/ScriptUseLoadScript.client.jsx similarity index 100% rename from packages/playground/server-components/src/components/ScriptUseLoadScript.client.jsx rename to packages/playground/async-config/src/components/ScriptUseLoadScript.client.jsx diff --git a/packages/playground/server-components/src/routes/scripts/cdn.server.jsx b/packages/playground/async-config/src/routes/scripts/cdn.server.jsx similarity index 100% rename from packages/playground/server-components/src/routes/scripts/cdn.server.jsx rename to packages/playground/async-config/src/routes/scripts/cdn.server.jsx diff --git a/packages/playground/server-components/src/routes/scripts/index.server.jsx b/packages/playground/async-config/src/routes/scripts/index.server.jsx similarity index 100% rename from packages/playground/server-components/src/routes/scripts/index.server.jsx rename to packages/playground/async-config/src/routes/scripts/index.server.jsx diff --git a/packages/playground/server-components/src/routes/scripts/script/after-hydration/ScriptsAfterHydration.tsx b/packages/playground/async-config/src/routes/scripts/script/after-hydration/ScriptsAfterHydration.tsx similarity index 100% rename from packages/playground/server-components/src/routes/scripts/script/after-hydration/ScriptsAfterHydration.tsx rename to packages/playground/async-config/src/routes/scripts/script/after-hydration/ScriptsAfterHydration.tsx diff --git a/packages/playground/server-components/src/routes/scripts/script/after-hydration/[handle].server.jsx b/packages/playground/async-config/src/routes/scripts/script/after-hydration/[handle].server.jsx similarity index 100% rename from packages/playground/server-components/src/routes/scripts/script/after-hydration/[handle].server.jsx rename to packages/playground/async-config/src/routes/scripts/script/after-hydration/[handle].server.jsx diff --git a/packages/playground/server-components/src/routes/scripts/script/after-hydration/index.server.jsx b/packages/playground/async-config/src/routes/scripts/script/after-hydration/index.server.jsx similarity index 100% rename from packages/playground/server-components/src/routes/scripts/script/after-hydration/index.server.jsx rename to packages/playground/async-config/src/routes/scripts/script/after-hydration/index.server.jsx diff --git a/packages/playground/server-components/src/routes/scripts/script/before-hydration/index.server.jsx b/packages/playground/async-config/src/routes/scripts/script/before-hydration/index.server.jsx similarity index 89% rename from packages/playground/server-components/src/routes/scripts/script/before-hydration/index.server.jsx rename to packages/playground/async-config/src/routes/scripts/script/before-hydration/index.server.jsx index 8e09c80a34..949bc3be66 100644 --- a/packages/playground/server-components/src/routes/scripts/script/before-hydration/index.server.jsx +++ b/packages/playground/async-config/src/routes/scripts/script/before-hydration/index.server.jsx @@ -6,12 +6,12 @@ export default function ScriptsBeforeHydration() {

beforeHydration scripts

- {``} are executed if rendered on the - initial page load (like regular {` ); diff --git a/packages/playground/server-components/tests/e2e-test-cases.ts b/packages/playground/server-components/tests/e2e-test-cases.ts index b58e2ed179..d6bad271d3 100644 --- a/packages/playground/server-components/tests/e2e-test-cases.ts +++ b/packages/playground/server-components/tests/e2e-test-cases.ts @@ -916,7 +916,7 @@ export default async function testCases({ }); }); - describe('Custom error apge', () => { + describe('Custom error page', () => { beforeEach(() => { jest.spyOn(console, 'error').mockImplementation(() => {}); }); @@ -949,455 +949,4 @@ export default async function testCases({ expect(await page.textContent('h2')).toContain('itBroke is not defined'); }); }); - - describe(' + {/* eslint-disable-next-line hydrogen/prefer-script-component */} diff --git a/packages/hydrogen/src/components/Seo/ProductSeo.client.tsx b/packages/hydrogen/src/components/Seo/ProductSeo.client.tsx index 495b75c452..447abe0b96 100644 --- a/packages/hydrogen/src/components/Seo/ProductSeo.client.tsx +++ b/packages/hydrogen/src/components/Seo/ProductSeo.client.tsx @@ -95,6 +95,7 @@ export function ProductSeo({ /> )} + {/* eslint-disable-next-line hydrogen/prefer-script-component */} From 56341a1ed9e413387de75f4a0bc69bf62a8d1767 Mon Sep 17 00:00:00 2001 From: "Juan P. Prieto" Date: Wed, 28 Sep 2022 09:00:10 -0700 Subject: [PATCH 26/26] cleanup unnecessary async/await --- .../components/Script/ScriptPostHydration.client.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/hydrogen/src/components/Script/ScriptPostHydration.client.tsx b/packages/hydrogen/src/components/Script/ScriptPostHydration.client.tsx index 1cffed603f..da198f30b3 100644 --- a/packages/hydrogen/src/components/Script/ScriptPostHydration.client.tsx +++ b/packages/hydrogen/src/components/Script/ScriptPostHydration.client.tsx @@ -15,13 +15,11 @@ export function ScriptPostHydration(props: PostHydrationProps): null { // Load script based on delayed loading load useEffect(() => { - (async () => { - if (load === 'afterHydration' || load === 'inWorker') { - await loadScript(props); - } else if (load === 'onIdle') { - loadScriptOnIdle(props); - } - })(); + if (load === 'afterHydration' || load === 'inWorker') { + loadScript(props); + } else if (load === 'onIdle') { + loadScriptOnIdle(props); + } }, [props, load]); // keep track or url changes to know when to reload scripts