@qwik.dev/core
Version:
An open source framework for building instant loading web apps at any scale, without the extra effort.
1,369 lines (1,342 loc) • 482 kB
JavaScript
/**
* @license
* @qwik.dev/core 2.0.0-beta.13-dev+cb19ff7
* Copyright QwikDev. All Rights Reserved.
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://github.com/QwikDev/qwik/blob/main/LICENSE
*/
import { isDev, isServer, isBrowser } from '@qwik.dev/core/build';
export { isBrowser, isDev, isServer } from '@qwik.dev/core/build';
import { p } from '@qwik.dev/core/preloader';
/**
* QWIK_VERSION
*
* @public
*/
const version = "2.0.0-beta.13-dev+cb19ff7";
// same as isDev but separate so we can test
const qDev = globalThis.qDev !== false;
const qInspector = globalThis.qInspector === true;
const qSerialize = globalThis.qSerialize !== false;
const qDynamicPlatform = globalThis.qDynamicPlatform !== false;
const qTest = globalThis.qTest === true;
const qRuntimeQrl = globalThis.qRuntimeQrl === true;
const seal = (obj) => {
if (qDev) {
Object.seal(obj);
}
};
const STYLE = qDev
? `background: #564CE0; color: white; padding: 2px 3px; border-radius: 2px; font-size: 0.8em;`
: '';
const logError = (message, ...optionalParams) => {
return createAndLogError(false, message, ...optionalParams);
};
const throwErrorAndStop = (message, ...optionalParams) => {
const error = createAndLogError(false, message, ...optionalParams);
// eslint-disable-next-line no-debugger
debugger;
throw error;
};
const logErrorAndStop = (message, ...optionalParams) => {
const err = createAndLogError(qDev, message, ...optionalParams);
// eslint-disable-next-line no-debugger
debugger;
return err;
};
const _printed = /*#__PURE__*/ new Set();
const logOnceWarn = (message, ...optionalParams) => {
if (qDev) {
const key = 'warn' + String(message);
if (!_printed.has(key)) {
_printed.add(key);
logWarn(message, ...optionalParams);
}
}
};
const logWarn = (message, ...optionalParams) => {
if (qDev) {
console.warn('%cQWIK WARN', STYLE, message, ...optionalParams);
}
};
const createAndLogError = (asyncThrow, message, ...optionalParams) => {
const err = message instanceof Error ? message : new Error(message);
// display the error message first, then the optional params, and finally the stack trace
// the stack needs to be displayed last because the given params will be lost among large stack traces so it will
// provide a bad developer experience
!qTest && console.error('%cQWIK ERROR', STYLE, err.message, ...optionalParams, err.stack);
asyncThrow &&
!qTest &&
setTimeout(() => {
// throwing error asynchronously to avoid breaking the current call stack.
// We throw so that the error is delivered to the global error handler for
// reporting it to a third-party tools such as Qwik Insights, Sentry or New Relic.
throw err;
}, 0);
return err;
};
/** @private */
const isSerializableObject = (v) => {
const proto = Object.getPrototypeOf(v);
return proto === Object.prototype || proto === Array.prototype || proto === null;
};
const isObject = (v) => {
return typeof v === 'object' && v !== null;
};
const isArray = (v) => {
return Array.isArray(v);
};
const isString = (v) => {
return typeof v === 'string';
};
const isNumber$1 = (v) => {
return typeof v === 'number';
};
const isFunction = (v) => {
return typeof v === 'function';
};
const isPrimitive = (v) => {
return typeof v !== 'object' && typeof v !== 'function' && v !== null && v !== undefined;
};
const codeToText = (code, ...parts) => {
if (qDev) {
// Keep one error, one line to make it easier to search for the error message.
const MAP = [
'Error while serializing class or style attributes', // 0
'Scheduler not found', // 1
'track() received object, without prop to track', // 2
'Only primitive and object literals can be serialized. {{0}}', // 3
'You can render over a existing q:container. Skipping render().', // 4
'QRL is not a function', // 5
'Dynamic import not found', // 6
'Unknown type argument', // 7
`Actual value for useContext({{0}}) can not be found, make sure some ancestor component has set a value using useContextProvider(). In the browser make sure that the context was used during SSR so its state was serialized.`, // 8
"Invoking 'use*()' method outside of invocation context.", // 9
`Calling a 'use*()' method outside 'component$(() => { HERE })' is not allowed. 'use*()' methods provide hooks to the 'component$' state and lifecycle, ie 'use' hooks can only be called synchronously within the 'component$' function or another 'use' method.\nSee https://qwik.dev/docs/core/tasks/#use-method-rules`, // 10
'The provided Context reference "{{0}}" is not a valid context created by createContextId()', // 11
'SsrError(tag): {{0}}', // 12
'QRLs can not be resolved because it does not have an attached container. This means that the QRL does not know where it belongs inside the DOM, so it cant dynamically import() from a relative path.', // 13
'QRLs can not be dynamically resolved, because it does not have a chunk path', // 14
'{{0}}\nThe JSX ref attribute must be a Signal', // 15
'Serialization Error: Deserialization of data type {{0}} is not implemented', // 16
'Serialization Error: Expected vnode for ref prop, but got {{0}}', // 17
'Serialization Error: Cannot allocate data type {{0}}', // 18
'Serialization Error: Missing root id for {{0}}', // 19
'Serialization Error: Serialization of data type {{0}} is not implemented', // 20
'Serialization Error: Unvisited {{0}}', // 21
'Serialization Error: Missing QRL chunk for {{0}}', // 22
'{{0}}\nThe value of the textarea must be a string found {{1}}', // 23
'Unable to find q:container', // 24
"Element must have 'q:container' attribute.", // 25
'Unknown vnode type {{0}}.', // 26
'Materialize error: missing element: {{0}} {{1}} {{2}}', // 27
'Cannot coerce a Signal, use `.value` instead', // 28
'useComputed$ QRL {{0}} {{1}} cannot return a Promise', // 29
'ComputedSignal is read-only', // 30
'WrappedSignal is read-only', // 31
'Attribute value is unsafe for SSR', // 32
'SerializerSymbol function returned rejected promise', // 33
'Serialization Error: Cannot serialize function: {{0}}', // 34
];
let text = MAP[code] ?? '';
if (parts.length) {
text = text.replaceAll(/{{(\d+)}}/g, (_, index) => {
let v = parts[index];
if (v && isObject(v) && v.constructor === Object) {
v = JSON.stringify(v).slice(0, 50);
}
return v;
});
}
return `Code(Q${code}): ${text}`;
}
else {
// cute little hack to give roughly the correct line number. Update the line number if it shifts.
return `Code(Q${code}) https://github.com/QwikDev/qwik/blob/main/packages/qwik/src/core/error/error.ts#L${8 + code}`;
}
};
const qError = (code, errorMessageArgs = []) => {
const text = codeToText(code, ...errorMessageArgs);
return logErrorAndStop(text, ...errorMessageArgs);
};
/** QRL related utilities that you can import without importing all of Qwik. */
const SYNC_QRL = '<sync>';
/** Sync QRL is a function which is serialized into `<script q:func="qwik/json">` tag. */
const isSyncQrl = (value) => {
return isQrl(value) && value.$symbol$ == SYNC_QRL;
};
const isQrl = (value) => {
return typeof value === 'function' && typeof value.getSymbol === 'function';
};
function assertQrl(qrl) {
if (isDev) {
if (!isQrl(qrl)) {
throw new Error('Not a QRL');
}
}
}
const getSymbolHash = (symbolName) => {
const index = symbolName.lastIndexOf('_');
if (index > -1) {
return symbolName.slice(index + 1);
}
return symbolName;
};
/** State factory of the component. */
const OnRenderProp = 'q:renderFn';
/** Component style content prefix */
const ComponentStylesPrefixContent = '⚡️';
/** `<some-element q:slot="...">` */
const QSlot = 'q:slot';
const QSlotParent = 'q:sparent';
const QSlotS = 'q:s';
const QStyle = 'q:style';
const QStyleSelector = 'style[q\\:style]';
const QStyleSSelector = 'style[q\\:sstyle]';
const QStylesAllSelector = QStyleSelector + ',' + QStyleSSelector;
const QScopedStyle = 'q:sstyle';
const QCtxAttr = 'q:ctx';
const QBackRefs = 'q:brefs';
const QFuncsPrefix = 'qFuncs_';
const getQFuncs = (document, hash) => {
return document[QFuncsPrefix + hash] || [];
};
const QBaseAttr = 'q:base';
const QLocaleAttr = 'q:locale';
const QManifestHashAttr = 'q:manifest-hash';
const QInstanceAttr = 'q:instance';
const QContainerIsland = 'q:container-island';
const QContainerIslandEnd = '/' + QContainerIsland;
const QIgnore = 'q:ignore';
const QIgnoreEnd = '/' + QIgnore;
const QContainerAttr = 'q:container';
const QContainerAttrEnd = '/' + QContainerAttr;
const QTemplate = 'q:template';
// the same selector should be inside the qwik loader
// and the same selector should be inside the qwik router spa-shim and spa-init
const QContainerSelector = '[q\\:container]:not([q\\:container=' +
"html" /* QContainerValue.HTML */ +
']):not([q\\:container=' +
"text" /* QContainerValue.TEXT */ +
'])';
// Node namespaces
const HTML_NS = 'http://www.w3.org/1999/xhtml';
const SVG_NS = 'http://www.w3.org/2000/svg';
const MATH_NS = 'http://www.w3.org/1998/Math/MathML';
// Attributes namespaces
const XLINK_NS = 'http://www.w3.org/1999/xlink';
const XML_NS = 'http://www.w3.org/XML/1998/namespace';
const ResourceEvent = 'qResource';
const RenderEvent = 'qRender';
const TaskEvent = 'qTask';
/** `<q:slot name="...">` */
const QDefaultSlot = '';
/**
* Attribute to mark that this VNode has a pointer to itself from the `qwik/json` state.
*
* As the VNode get materialized the vnode now becomes eligible for mutation. Once the vnode mutates
* the `VNode` references from the `qwik/json` may become invalid. For this reason, these references
* need to be eagerly resolved. `VNODE_REF` stores a pointer to "this" vnode. This allows the system
* to eagerly resolve these pointes as the vnodes are materialized.
*/
const ELEMENT_ID = 'q:id';
const ELEMENT_KEY = 'q:key';
const ELEMENT_PROPS = 'q:props';
const ELEMENT_SEQ = 'q:seq';
const ELEMENT_SEQ_IDX = 'q:seqIdx';
const Q_PREFIX = 'q:';
/** Non serializable markers - always begins with `:` character */
const NON_SERIALIZABLE_MARKER_PREFIX = ':';
const USE_ON_LOCAL = NON_SERIALIZABLE_MARKER_PREFIX + 'on';
const USE_ON_LOCAL_SEQ_IDX = NON_SERIALIZABLE_MARKER_PREFIX + 'onIdx';
const USE_ON_LOCAL_FLAGS = NON_SERIALIZABLE_MARKER_PREFIX + 'onFlags';
// comment nodes
const FLUSH_COMMENT = 'qkssr-f';
const STREAM_BLOCK_START_COMMENT = 'qkssr-pu';
const STREAM_BLOCK_END_COMMENT = 'qkssr-po';
const Q_PROPS_SEPARATOR = ':';
const dangerouslySetInnerHTML = 'dangerouslySetInnerHTML';
const qwikInspectorAttr = 'data-qwik-inspector';
// keep this import from core/build so the cjs build works
const createPlatform = () => {
return {
isServer,
importSymbol(containerEl, url, symbolName) {
if (isServer) {
const hash = getSymbolHash(symbolName);
const regSym = globalThis.__qwik_reg_symbols?.get(hash);
if (regSym) {
return regSym;
}
}
if (!url) {
throw qError(14 /* QError.qrlMissingChunk */, [symbolName]);
}
if (!containerEl) {
throw qError(13 /* QError.qrlMissingContainer */, [url, symbolName]);
}
const urlDoc = toUrl(containerEl.ownerDocument, containerEl, url).toString();
const urlCopy = new URL(urlDoc);
urlCopy.hash = '';
const importURL = urlCopy.href;
return import(/* @vite-ignore */ importURL).then((mod) => {
return mod[symbolName];
});
},
raf: (fn) => {
return new Promise((resolve) => {
requestAnimationFrame(() => {
resolve(fn());
});
});
},
chunkForSymbol(symbolName, chunk) {
return [symbolName, chunk ?? '_'];
},
};
};
/**
* Convert relative base URI and relative URL into a fully qualified URL.
*
* @param base -`QRL`s are relative, and therefore they need a base for resolution.
*
* - `Element` use `base.ownerDocument.baseURI`
* - `Document` use `base.baseURI`
* - `string` use `base` as is
* - `QConfig` use `base.baseURI`
*
* @param url - Relative URL
* @returns Fully qualified URL.
*/
const toUrl = (doc, containerEl, url) => {
const baseURI = doc.baseURI;
const base = new URL(containerEl.getAttribute(QBaseAttr) ?? baseURI, baseURI);
return new URL(url, base);
};
let _platform = /*#__PURE__ */ createPlatform();
// <docs markdown="./readme.md#setPlatform">
// !!DO NOT EDIT THIS COMMENT DIRECTLY!!!
// (edit ./readme.md#setPlatform instead and run `pnpm docs.sync`)
/**
* Sets the `CorePlatform`.
*
* This is useful to override the platform in tests to change the behavior of,
* `requestAnimationFrame`, and import resolution.
*
* @param doc - The document of the application for which the platform is needed.
* @param platform - The platform to use.
* @public
*/
// </docs>
const setPlatform = (plt) => (_platform = plt);
// <docs markdown="./readme.md#getPlatform">
// !!DO NOT EDIT THIS COMMENT DIRECTLY!!!
// (edit ./readme.md#getPlatform instead and run `pnpm docs.sync`)
/**
* Retrieve the `CorePlatform`.
*
* The `CorePlatform` is also responsible for retrieving the Manifest, that contains mappings from
* symbols to javascript import chunks. For this reason, `CorePlatform` can't be global, but is
* specific to the application currently running. On server it is possible that many different
* applications are running in a single server instance, and for this reason the `CorePlatform` is
* associated with the application document.
*
* @param docOrNode - The document (or node) of the application for which the platform is needed.
* @public
*/
// </docs>
const getPlatform = () => {
return _platform;
};
const isServerPlatform = () => {
if (qDynamicPlatform) {
return _platform.isServer;
}
return false;
};
const isNode = (value) => {
return value && typeof value.nodeType === 'number';
};
const isDocument = (value) => {
return value.nodeType === 9;
};
const isElement$1 = (value) => {
return value.nodeType === 1;
};
const MAX_RETRY_ON_PROMISE_COUNT = 100;
const isPromise = (value) => {
// not using "value instanceof Promise" to have zone.js support
return !!value && typeof value == 'object' && typeof value.then === 'function';
};
const safeCall = (call, thenFn, rejectFn) => {
try {
const result = call();
if (isPromise(result)) {
return result.then(thenFn, rejectFn);
}
else {
return thenFn(result);
}
}
catch (e) {
return rejectFn(e);
}
};
const maybeThen = (valueOrPromise, thenFn) => {
return isPromise(valueOrPromise)
? valueOrPromise.then(thenFn, shouldNotError)
: thenFn(valueOrPromise);
};
const shouldNotError = (reason) => {
throwErrorAndStop(reason);
};
const delay = (timeout) => {
return new Promise((resolve) => {
setTimeout(resolve, timeout);
});
};
/** Retries a function that throws a promise. */
function retryOnPromise(fn, retryCount = 0) {
const retryOrThrow = (e) => {
if (isPromise(e) && retryCount < MAX_RETRY_ON_PROMISE_COUNT) {
return e.then(retryOnPromise.bind(null, fn, retryCount++));
}
throw e;
};
try {
const result = fn();
if (isPromise(result)) {
// not awaited promise is not caught by try/catch block
return result.catch((e) => retryOrThrow(e));
}
return result;
}
catch (e) {
if (isDev && isServer && e instanceof ReferenceError && e.message.includes('window')) {
e.message = 'It seems like you forgot to add "if (isBrowser) {...}" here:' + e.message;
throw e;
}
return retryOrThrow(e);
}
}
const ASSERT_DISCLAIMER = 'Internal assert, this is likely caused by a bug in Qwik: ';
function assertDefined(value, text, ...parts) {
if (qDev) {
if (value != null) {
return;
}
throwErrorAndStop(ASSERT_DISCLAIMER + text, ...parts);
}
}
function assertEqual(value1, value2, text, ...parts) {
if (qDev) {
if (value1 === value2) {
return;
}
throwErrorAndStop(ASSERT_DISCLAIMER + text, ...parts);
}
}
function assertTrue(value1, text, ...parts) {
if (qDev) {
if (value1 === true) {
return;
}
throwErrorAndStop(ASSERT_DISCLAIMER + text, ...parts);
}
}
function assertFalse(value1, text, ...parts) {
if (qDev) {
if (value1 === false) {
return;
}
throwErrorAndStop(ASSERT_DISCLAIMER + text, ...parts);
}
}
function assertNumber(value1, text, ...parts) {
if (qDev) {
if (typeof value1 === 'number') {
return;
}
throwErrorAndStop(ASSERT_DISCLAIMER + text, ...parts);
}
}
let _locale = undefined;
let localAsyncStore;
if (isServer) {
import('node:async_hooks')
.then((module) => {
localAsyncStore = new module.AsyncLocalStorage();
})
.catch(() => {
// ignore if AsyncLocalStorage is not available
});
}
/**
* Retrieve the current locale.
*
* If no current locale and there is no `defaultLocale` the function throws an error.
*
* @returns The locale.
* @public
*/
function getLocale(defaultLocale) {
// Prefer per-request locale from local AsyncLocalStorage if available (server-side)
if (localAsyncStore) {
const locale = localAsyncStore.getStore();
if (locale) {
return locale;
}
}
if (_locale === undefined) {
const ctx = tryGetInvokeContext();
if (ctx && ctx.$locale$) {
return ctx.$locale$;
}
if (defaultLocale !== undefined) {
return defaultLocale;
}
throw new Error('Reading `locale` outside of context.');
}
return _locale;
}
/**
* Override the `getLocale` with `lang` within the `fn` execution.
*
* @public
*/
function withLocale(locale, fn) {
if (localAsyncStore) {
return localAsyncStore.run(locale, fn);
}
const previousLang = _locale;
try {
_locale = locale;
return fn();
}
finally {
_locale = previousLang;
}
}
/**
* Globally set a lang.
*
* This can be used only in browser. Server execution requires that each request could potentially
* be a different lang, therefore setting a global lang would produce incorrect responses.
*
* @public
*/
function setLocale(locale) {
if (localAsyncStore) {
localAsyncStore.enterWith(locale);
return;
}
_locale = locale;
}
/**
* A friendly name tag for a VirtualVNode.
*
* Theses are used to give a name to a VirtualVNode. This is useful for debugging and testing.
*
* The name is only added in development mode and is not included in production builds.
*/
const DEBUG_TYPE = 'q:type';
const VirtualTypeName = {
["V" /* VirtualType.Virtual */]: /* ********* */ 'Virtual', //
["F" /* VirtualType.Fragment */]: /* ******** */ 'Fragment', //
["S" /* VirtualType.WrappedSignal */]: /* *** */ 'Signal', //
["A" /* VirtualType.Awaited */]: /* ********* */ 'Awaited', //
["C" /* VirtualType.Component */]: /* ******* */ 'Component', //
["I" /* VirtualType.InlineComponent */]: /* * */ 'InlineComponent', //
["P" /* VirtualType.Projection */]: /* ****** */ 'Projection', //
};
/**
* @file
*
* VNodeData is additional information which allows the `vnode` to recover virtual VNode information
* from the HTML.
*/
/**
* VNodeDataSeparator contains information about splitting up the VNodeData and attaching it to the
* HTML.
*/
const VNodeDataSeparator = {
REFERENCE: /* ******** */ 126, // `~` is a reference to the node. Save it.
ADVANCE_1: /* ********* */ 33, // `!` is vNodeData separator skipping 0. (ie next vNode)
ADVANCE_8192: /* ****** */ 46, // `.` is vNodeData separator skipping 4096.
};
/**
* VNodeDataChar contains information about the VNodeData used for encoding props.
*
* Available character ranges: 59 - 64, 91 - 94, 96, 123 - 126
*/
const VNodeDataChar = {
OPEN: /* ************** */ 123, // `{` is the start of the VNodeData for a virtual element.
CLOSE: /* ************* */ 125, // `}` is the end of the VNodeData for a virtual element.
SCOPED_STYLE: /* ******* */ 59, // `;` - `q:sstyle` - Style attribute.
RENDER_FN: /* ********** */ 60, // `<` - `q:renderFn' - Component QRL render function (body)
ID: /* ***************** */ 61, // `=` - `q:id` - ID of the element.
PROPS: /* ************** */ 62, // `>` - `q:props' - Component Props
SLOT_PARENT: /* ******** */ 63, // `?` - `q:sparent` - Slot parent.
KEY: /* **************** */ 64, // `@` - `q:key` - Element key.
SEQ: /* **************** */ 91, // `[` - `q:seq' - Seq value from `useSequentialScope()`
CONTEXT: /* ************ */ 93, // `]` - `q:ctx' - Component context/props
SEQ_IDX: /* ************ */ 94, // `^` - `q:seqIdx' - Sequential scope id
BACK_REFS: /* ********** */ 96, // '`' - `q:brefs' - Effect dependencies/subscriptions
SEPARATOR: /* ********* */ 124, // `|` - Separator char to encode any key/value pairs.
SLOT: /* ************** */ 126};
/** @internal */
const mapApp_findIndx = (array, key, start) => {
assertTrue(start % 2 === 0, 'Expecting even number.');
let bottom = start >> 1;
let top = (array.length - 2) >> 1;
while (bottom <= top) {
const mid = bottom + ((top - bottom) >> 1);
const midKey = array[mid << 1];
if (midKey === key) {
return mid << 1;
}
if (midKey < key) {
bottom = mid + 1;
}
else {
top = mid - 1;
}
}
return (bottom << 1) ^ -1;
};
/** @internal */
const mapArray_set = (array, key, value, start, allowNullValue = false) => {
const indx = mapApp_findIndx(array, key, start);
if (indx >= 0) {
if (value == null && !allowNullValue) {
array.splice(indx, 2);
}
else {
array[indx + 1] = value;
}
}
else if (value != null || allowNullValue) {
array.splice(indx ^ -1, 0, key, value);
}
};
/** @internal */
const mapArray_get = (array, key, start) => {
const indx = mapApp_findIndx(array, key, start);
if (indx >= 0) {
return array[indx + 1];
}
else {
return null;
}
};
const mapArray_has = (array, key, start) => {
return mapApp_findIndx(array, key, start) >= 0;
};
const isForeignObjectElement = (elementName) => {
return isDev ? elementName.toLowerCase() === 'foreignobject' : elementName === 'foreignObject';
};
const isSvgElement = (elementName) => elementName === 'svg' || isForeignObjectElement(elementName);
const isMathElement = (elementName) => elementName === 'math';
const vnode_isDefaultNamespace = (vnode) => {
const flags = vnode.flags;
return (flags & 192 /* VNodeFlags.NAMESPACE_MASK */) === 0;
};
const vnode_getElementNamespaceFlags = (element) => {
const namespace = fastNamespaceURI(element);
switch (namespace) {
case SVG_NS:
return 64 /* VNodeFlags.NS_svg */;
case MATH_NS:
return 128 /* VNodeFlags.NS_math */;
default:
return 0 /* VNodeFlags.NS_html */;
}
};
function vnode_getDomChildrenWithCorrectNamespacesToInsert(journal, domParentVNode, newChild) {
const { elementNamespace, elementNamespaceFlag } = getNewElementNamespaceData(domParentVNode, newChild);
let domChildren = [];
if (elementNamespace === HTML_NS) {
// parent is in the default namespace, so just get the dom children. This is the fast path.
domChildren = vnode_getDOMChildNodes(journal, newChild);
}
else {
// parent is in a different namespace, so we need to clone the children with the correct namespace.
// The namespace cannot be changed on nodes, so we need to clone these nodes
const children = vnode_getDOMChildNodes(journal, newChild, true);
for (let i = 0; i < children.length; i++) {
const childVNode = children[i];
if (vnode_isTextVNode(childVNode)) {
// text nodes are always in the default namespace
domChildren.push(childVNode.textNode);
continue;
}
if ((childVNode.flags & 192 /* VNodeFlags.NAMESPACE_MASK */) ===
(domParentVNode.flags & 192 /* VNodeFlags.NAMESPACE_MASK */)) {
// if the child and parent have the same namespace, we don't need to clone the element
domChildren.push(childVNode.element);
continue;
}
// clone the element with the correct namespace
const newChildElement = vnode_cloneElementWithNamespace(childVNode, domParentVNode, elementNamespace, elementNamespaceFlag);
if (newChildElement) {
domChildren.push(newChildElement);
}
}
}
return domChildren;
}
/** This function clones an element with a different namespace, including the children */
function cloneDomTreeWithNamespace(element, elementName, namespace, deep = false) {
const newElement = element.ownerDocument.createElementNS(namespace, elementName);
// Copy all attributes
for (const attr of element.attributes) {
if (attr.name !== Q_PROPS_SEPARATOR) {
newElement.setAttribute(attr.name, attr.value);
}
}
if (deep) {
// Recursively clone all child nodes
for (const child of element.childNodes) {
const nodeType = child.nodeType;
if (nodeType === 3 /* Node.TEXT_NODE */) {
newElement.appendChild(child.cloneNode());
}
else if (nodeType === 1 /* Node.ELEMENT_NODE */) {
newElement.appendChild(cloneDomTreeWithNamespace(child, child.localName, namespace, deep));
}
}
}
return newElement;
}
/**
* This function clones an ElementVNode with a different namespace, including the children. This
* traverse the tree using depth-first search and clones the elements using
* `cloneElementWithNamespace`.
*/
function vnode_cloneElementWithNamespace(elementVNode, parentVNode, namespace, namespaceFlag) {
ensureElementVNode(elementVNode);
let vCursor = elementVNode;
let vParent = null;
let rootElement = null;
let parentElement = null;
while (vCursor) {
let childElement = null;
let newChildElement = null;
if (vnode_isElementVNode(vCursor)) {
// Clone the element
childElement = vCursor.element;
const childElementTag = vnode_getElementName(vCursor);
// We need to check if the parent is a foreignObject element
// and get a new namespace data.
const vCursorParent = vCursor.parent;
// For the first vNode parentNode is not parent from vNode tree, but parent from DOM tree
// this is because vNode is not moved yet.
// rootElement is null only for the first vNode
const vCursorDomParent = rootElement == null ? parentVNode : vCursorParent && vnode_getDomParentVNode(vCursorParent);
if (vCursorDomParent) {
const namespaceData = getNewElementNamespaceData(vCursorDomParent, vnode_getElementName(vCursor));
namespace = namespaceData.elementNamespace;
namespaceFlag = namespaceData.elementNamespaceFlag;
}
const vFirstChild = vnode_getFirstChild(vCursor);
newChildElement = cloneDomTreeWithNamespace(childElement, childElementTag, namespace,
// deep if there is no vnode children, children are probably inserted via innerHTML
!vFirstChild);
childElement.remove();
if (rootElement == null) {
rootElement = newChildElement;
}
if (parentElement) {
parentElement.appendChild(newChildElement);
}
// Descend into children
// We need first get the first child, if any
// Then we can overwrite the cursor with newly created element.
// This is because we need to materialize the children before we assign new element
vCursor.element = newChildElement;
// Set correct namespace flag
vCursor.flags &= -193 /* VNodeFlags.NEGATED_NAMESPACE_MASK */;
vCursor.flags |= namespaceFlag;
if (vFirstChild) {
vCursor = vFirstChild;
parentElement = newChildElement;
continue;
}
else if (shouldIgnoreChildren(childElement)) {
// If we should ignore children of the element this means that the element is a container
// We need to get the first child of the container
const container = getDomContainerFromQContainerElement(childElement);
if (container) {
const innerContainerFirstVNode = vnode_getFirstChild(container.rootVNode);
if (innerContainerFirstVNode) {
vCursor = innerContainerFirstVNode;
parentElement = newChildElement;
continue;
}
}
}
}
if (vCursor === elementVNode) {
// we are where we started, this means that vNode has no children, so we are done.
return rootElement;
}
// Out of children, go to next sibling
const vNextSibling = vCursor.nextSibling;
if (vNextSibling) {
vCursor = vNextSibling;
continue;
}
// Out of siblings, go to parent
vParent = vCursor.parent;
while (vParent) {
if (vParent === elementVNode) {
// We are back where we started, we are done.
return rootElement;
}
const vNextParentSibling = vParent.nextSibling;
if (vNextParentSibling) {
vCursor = vNextParentSibling;
return rootElement;
}
vParent = vParent.parent;
}
if (vParent == null) {
// We are done.
return rootElement;
}
}
return rootElement;
}
function isSvg(tagOrVNode) {
return typeof tagOrVNode === 'string'
? isSvgElement(tagOrVNode)
: (tagOrVNode.flags & 64 /* VNodeFlags.NS_svg */) !== 0;
}
function isMath(tagOrVNode) {
return typeof tagOrVNode === 'string'
? isMathElement(tagOrVNode)
: (tagOrVNode.flags & 128 /* VNodeFlags.NS_math */) !== 0;
}
function getNewElementNamespaceData(domParentVNode, tagOrVNode) {
const parentIsDefaultNamespace = domParentVNode
? !!vnode_getElementName(domParentVNode) && vnode_isDefaultNamespace(domParentVNode)
: true;
const parentIsForeignObject = !parentIsDefaultNamespace
? isForeignObjectElement(vnode_getElementName(domParentVNode))
: false;
let elementNamespace = HTML_NS;
let elementNamespaceFlag = 0 /* VNodeFlags.NS_html */;
const isElementVNodeOrString = typeof tagOrVNode === 'string' || vnode_isElementVNode(tagOrVNode);
if (isElementVNodeOrString && isSvg(tagOrVNode)) {
elementNamespace = SVG_NS;
elementNamespaceFlag = 64 /* VNodeFlags.NS_svg */;
}
else if (isElementVNodeOrString && isMath(tagOrVNode)) {
elementNamespace = MATH_NS;
elementNamespaceFlag = 128 /* VNodeFlags.NS_math */;
}
else if (domParentVNode && !parentIsForeignObject && !parentIsDefaultNamespace) {
const isParentSvg = (domParentVNode.flags & 64 /* VNodeFlags.NS_svg */) !== 0;
const isParentMath = (domParentVNode.flags & 128 /* VNodeFlags.NS_math */) !== 0;
elementNamespace = isParentSvg ? SVG_NS : isParentMath ? MATH_NS : HTML_NS;
elementNamespaceFlag = domParentVNode.flags & 192 /* VNodeFlags.NAMESPACE_MASK */;
}
return {
elementNamespace,
elementNamespaceFlag,
};
}
function getAttributeNamespace(attributeName) {
switch (attributeName) {
case 'xlink:href':
case 'xlink:actuate':
case 'xlink:arcrole':
case 'xlink:role':
case 'xlink:show':
case 'xlink:title':
case 'xlink:type':
return XLINK_NS;
case 'xml:base':
case 'xml:lang':
case 'xml:space':
return XML_NS;
default:
return null;
}
}
const mergeMaps = (map1, map2) => {
for (const [k, v] of map2) {
map1.set(k, v);
}
return map1;
};
/**
* # ================================
*
* Signal Types
*
* # ================================
*/
/**
* Special value used to mark that a given signal needs to be computed. This is essentially a
* "marked as dirty" flag.
*/
const NEEDS_COMPUTATION = Symbol('invalid');
/** @internal */
const _EFFECT_BACK_REF = Symbol('backRef');
/**
* # ================================
*
* Store Types
*
* # ================================
*/
const STORE_TARGET = Symbol('store.target');
const STORE_HANDLER = Symbol('store.handler');
const STORE_ALL_PROPS = Symbol('store.all');
class SignalImpl {
$untrackedValue$;
/** Store a list of effects which are dependent on this signal. */
$effects$ = null;
$container$ = null;
$wrappedSignal$ = null;
constructor(container, value) {
this.$container$ = container;
this.$untrackedValue$ = value;
}
/**
* Use this to force running subscribers, for example when the calculated value has mutated but
* remained the same object
*/
force() {
this.$container$?.$scheduler$(7 /* ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS */, null, this, this.$effects$);
}
get untrackedValue() {
return this.$untrackedValue$;
}
// TODO: should we disallow setting the value directly?
set untrackedValue(value) {
this.$untrackedValue$ = value;
}
get value() {
return setupSignalValueAccess(this, () => (this.$effects$ ||= new Set()), () => this.untrackedValue);
}
set value(value) {
if (value !== this.$untrackedValue$) {
this.$untrackedValue$ = value;
this.$container$?.$scheduler$(7 /* ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS */, null, this, this.$effects$);
}
}
// prevent accidental use as value
valueOf() {
if (qDev) {
throw qError(28 /* QError.cannotCoerceSignal */);
}
}
toString() {
if (isDev) {
return (`[${this.constructor.name}${this.$flags$ & 1 /* SignalFlags.INVALID */ ? ' INVALID' : ''} ${String(this.$untrackedValue$)}]` +
(Array.from(this.$effects$ || [])
.map((e) => '\n -> ' + pad(qwikDebugToString(e[0]), ' '))
.join('\n') || ''));
}
else {
return this.constructor.name;
}
}
toJSON() {
return { value: this.$untrackedValue$ };
}
}
const addEffect = (signal, effectSubscriber, effects) => {
// Let's make sure that we have a reference to this effect.
// Adding reference is essentially adding a subscription, so if the signal
// changes we know who to notify.
ensureContainsSubscription(effects, effectSubscriber);
// But when effect is scheduled in needs to be able to know which signals
// to unsubscribe from. So we need to store the reference from the effect back
// to this signal.
ensureContainsBackRef(effectSubscriber, signal);
addQrlToSerializationCtx(effectSubscriber, signal.$container$);
};
const setupSignalValueAccess = (target, effectsFn, returnValueFn) => {
const ctx = tryGetInvokeContext();
if (ctx) {
if (target.$container$ === null) {
if (!ctx.$container$) {
return returnValueFn();
}
// Grab the container now we have access to it
target.$container$ = ctx.$container$;
}
else {
assertTrue(!ctx.$container$ || ctx.$container$ === target.$container$, 'Do not use signals across containers');
}
const effectSubscriber = ctx.$effectSubscriber$;
if (effectSubscriber) {
addEffect(target, effectSubscriber, effectsFn());
}
}
return returnValueFn();
};
/** @internal */
const _CONST_PROPS = Symbol('CONST');
/** @internal */
const _VAR_PROPS = Symbol('VAR');
/** @internal */
const _OWNER = Symbol('OWNER');
/** @internal @deprecated v1 compat */
const _IMMUTABLE = Symbol('IMMUTABLE');
/** @internal */
const _UNINITIALIZED = Symbol('UNINITIALIZED');
/**
* Think of `-` as an escape character which makes the next character uppercase. `--` is just `-`.
*
* Rules for JSX property event names starting with `on`:
*
* - Are case insensitive: `onClick$` is same `onclick$`
* - A `--` is `-`: `dbl--click` => `dbl-click`
* - Become case sensitive if prefixed by `-`: `-Click` is `Click`
* - A `-` (not at the beginning) makes next character uppercase: `dbl-click` => `dblClick`
*/
const EVENT_SUFFIX = '$';
const isHtmlAttributeAnEventName = (name) => {
return (name.startsWith("on:" /* EventNameHtmlScope.on */) ||
name.startsWith("on-window:" /* EventNameHtmlScope.window */) ||
name.startsWith("on-document:" /* EventNameHtmlScope.document */));
};
function jsxEventToHtmlAttribute(jsxEvent) {
if (jsxEvent.endsWith(EVENT_SUFFIX)) {
const [prefix, idx] = getEventScopeDataFromJsxEvent(jsxEvent);
if (idx !== -1) {
const name = jsxEvent.slice(idx, -1);
return name === 'DOMContentLoaded'
? // The only DOM event that is not all lowercase
prefix + '-d-o-m-content-loaded'
: createEventName(name.charAt(0) === '-'
? // marker for case sensitive event name
name.slice(1)
: name.toLowerCase(), prefix);
}
}
return null; // Return null if not matching expected format
}
function createEventName(event, prefix) {
const eventName = fromCamelToKebabCase(event);
return prefix + eventName;
}
function getEventScopeDataFromJsxEvent(eventName) {
let prefix = "on:" /* EventNameHtmlScope.on */;
let idx = -1;
// set prefix and idx based on the scope
if (eventName.startsWith("on" /* EventNameJSXScope.on */)) {
prefix = "on:" /* EventNameHtmlScope.on */;
idx = 2;
}
else if (eventName.startsWith("window:on" /* EventNameJSXScope.window */)) {
prefix = "on-window:" /* EventNameHtmlScope.window */;
idx = 9;
}
else if (eventName.startsWith("document:on" /* EventNameJSXScope.document */)) {
prefix = "on-document:" /* EventNameHtmlScope.document */;
idx = 11;
}
return [prefix, idx];
}
function isPreventDefault(key) {
return key.startsWith('preventdefault:');
}
/** Converts a camelCase string to kebab-case. This is used for event names. */
const fromCamelToKebabCase = (text) => {
return text.replace(/([A-Z-])/g, (a) => '-' + a.toLowerCase());
};
const getEventDataFromHtmlAttribute = (htmlKey) => {
if (htmlKey.startsWith("on:" /* EventNameHtmlScope.on */)) {
return ['', htmlKey.substring(3)];
}
if (htmlKey.startsWith("on-window:" /* EventNameHtmlScope.window */)) {
return ['window', htmlKey.substring(10)];
}
return ['document', htmlKey.substring(12)];
};
/** @internal */
const EMPTY_ARRAY = [];
const EMPTY_OBJ = {};
Object.freeze(EMPTY_ARRAY);
Object.freeze(EMPTY_OBJ);
function createPropsProxy(owner) {
// TODO don't make a proxy but populate getters? benchmark
return new Proxy({}, new PropsProxyHandler(owner));
}
class PropsProxyHandler {
owner;
constructor(owner) {
this.owner = owner;
}
get(_, prop) {
// escape hatch to get the separated props from a component
if (prop === _CONST_PROPS) {
return this.owner.constProps;
}
if (prop === _VAR_PROPS) {
return this.owner.varProps;
}
if (prop === _OWNER) {
return this.owner;
}
let value;
if (prop === 'children') {
value = this.owner.children;
}
else {
if (typeof prop === 'string' && typeof this.owner.type === 'string') {
const attr = jsxEventToHtmlAttribute(prop);
if (attr) {
prop = attr;
}
}
value = directGetPropsProxyProp(this.owner, prop);
}
// a proxied value that the optimizer made
return value instanceof WrappedSignalImpl && value.$flags$ & 4 /* WrappedSignalFlags.UNWRAP */
? value.value
: value;
}
set(_, prop, value) {
if (prop === _OWNER) {
// used for deserialization
this.owner = value;
}
else if (prop === 'children') {
this.owner.children = value;
}
else {
if (typeof prop === 'string' && typeof this.owner.type === 'string') {
const attr = jsxEventToHtmlAttribute(prop);
if (attr) {
prop = attr;
}
}
if (this.owner.constProps && prop in this.owner.constProps) {
this.owner.constProps[prop] = undefined;
if (!(prop in this.owner.varProps)) {
this.owner.toSort = true;
}
this.owner.varProps[prop] = value;
}
else {
if (this.owner.varProps === EMPTY_OBJ) {
this.owner.varProps = {};
}
else {
if (!(prop in this.owner.varProps)) {
this.owner.toSort = true;
}
}
this.owner.varProps[prop] = value;
}
}
return true;
}
deleteProperty(_, prop) {
let didDelete = delete this.owner.varProps[prop];
if (this.owner.constProps) {
didDelete = delete this.owner.constProps[prop] || didDelete;
}
if (this.owner.children != null && prop === 'children') {
this.owner.children = null;
didDelete = true;
}
return didDelete;
}
has(_, prop) {
if (prop === 'children') {
return this.owner.children != null;
}
else if (prop === _CONST_PROPS || prop === _VAR_PROPS) {
return true;
}
if (typeof prop === 'string' && typeof this.owner.type === 'string') {
const attr = jsxEventToHtmlAttribute(prop);
if (attr) {
prop = attr;
}
}
return (prop in this.owner.varProps || (this.owner.constProps ? prop in this.owner.constProps : false));
}
getOwnPropertyDescriptor(_, p) {
const value = p === 'children'
? this.owner.children
: this.owner.constProps && p in this.owner.constProps
? this.owner.constProps[p]
: this.owner.varProps[p];
return {
configurable: true,
enumerable: true,
value: value,
};
}
ownKeys() {
const out = Object.keys(this.owner.varProps);
if (this.owner.children != null) {
out.push('children');
}
if (this.owner.constProps) {
for (const key in this.owner.constProps) {
if (out.indexOf(key) === -1) {
out.push(key);
}
}
}
return out;
}
}
/**
* Instead of using PropsProxyHandler getter (which could create a component-level subscription).
* Use this function to get the props directly from a const or var props.
*
* This does not convert jsx event names.
*/
const directGetPropsProxyProp = (jsx, prop) => {
return (jsx.constProps && prop in jsx.constProps ? jsx.constProps[prop] : jsx.varProps[prop]);
};
/** Used by the optimizer for spread props operations @internal */
const _getVarProps = (props) => {
if (!props) {
return null;
}
return _VAR_PROPS in props
? 'children' in props
? { ...props[_VAR_PROPS], children: props.children }
: props[_VAR_PROPS]
: props;
};
/** Used by the optimizer for spread props operations @internal */
const _getConstProps = (props) => {
if (!props) {
return null;
}
return _CONST_PROPS in props ? props[_CONST_PROPS] : null;
};
const isPropsProxy = (obj) => {
return obj && _VAR_PROPS in obj;
};
function getSubscriber(effect, prop, data) {
if (!effect[_EFFECT_BACK_REF]) {
if (isServer && isSsrNode(effect)) {
effect.setProp(QBackRefs, new Map());
}
else {
effect[_EFFECT_BACK_REF] = new Map();
}
}
const subMap = effect[_EFFECT_BACK_REF];
let sub = subMap.get(prop);
if (!sub) {
sub = [effect, prop];
subMap.set(prop, sub);
}
if (data) {
sub[3 /* EffectSubscriptionProp.DATA */] = data;
}
return sub;
}
function isSsrNode(value) {
return '__brand__' in value && value.__brand__ === 'SsrNode';
}
const trackFn = (target, container) => (obj, prop) => {
const ctx = newInvokeContext();
ctx.$effectSubscriber$ = getSubscriber(target, ":" /* EffectProperty.COMPONENT */);
ctx.$container$ = container || undefined;
return invoke(ctx, () => {
if (isFunction(obj)) {
return obj();
}
if (prop) {
return obj[prop];
}
else if (isSignal(obj)) {
return obj.value;
}
else if (isObject(obj) && isStore(obj)) {
// track whole store
addStoreEffect(getStoreTarget(obj), STORE_ALL_PROPS, getStoreHandler(obj), ctx.$effectSubscriber$);
return obj;
}
else {
throw qError(2 /* QError.trackObjectWithoutProp */);
}
});
};
const cleanupFn = (target, handleError) => {
let cleanupFns = null;
const cleanup = (fn) => {
if (typeof fn == 'function') {
if (!cleanupFns) {
cleanupFns = [];
target.$destroy$ = noSerialize(() => {
target.$destroy$ = null;
cleanupFns.forEach((fn) => {
try {
fn();
}
catch (err) {
handleError(err);
}
});
});
}
cleanupFns.push(fn);
}
};
return [cleanup, cleanupFns ?? []];
};
const DEBUG$1 = false;
// eslint-disable-next-line no-console
const log = (...args) => console.log('COMPUTED SIGNAL', ...args.map(qwikDebugToString));
/**
* A signal which is computed from other signals.
*
* The value is available synchronously, but the computation is done lazily.
*/
class ComputedSignalImpl extends SignalImpl {
/**
* The compute function is stored here.
*
* The computed functions must be executed synchronously (because of this we need to eagerly
* resolve the QRL during the mark dirty phase so that any call to it will be synchronous). )
*/
$computeQrl$;
$flags$;
[_EFFECT_BACK_REF] = null;
constructor(container, fn,
// We need a separate flag to know when the computation needs running because
// we need the old value to know if effects need running after computation
flags = 1 /* SignalFlags.INVALID */ |
32 /* SerializationSignalFlags.SERIALIZATION_STRATEGY_ALWAYS */) {
// The value is used for comparison when signals trigger, which can only happen
// when it was calculated before. Therefore we can pass whatever we like.
super(container, NEEDS_COMPUTATION);
this.$computeQrl$ = fn;
this.$flags$ = flags;
}
invalidate() {
this.$flags$ |= 1 /* SignalFlags.INVALID */;
this.$container$?.$scheduler$(7 /* ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS */, null, this, this.$effects$);
}
/**
* Use this to force running subscribers, for example when the calculated value has mutated but
* remained the same object
*/
force() {
this.$flags$ |= 2 /* SignalFlags.RUN_EFFECTS */;
super.force();
}
get untrackedValue() {
this.$computeIfNeeded$();
assertFalse(this.$untrackedValue$ === NEEDS_COMPUTATION, 'Invalid state');
return this.$untrackedValue$;
}
$computeIfNeeded$() {
if (!(this.$flags$ & 1 /* SignalFlags.INVALID */)) {
return;