diff --git a/src/client/client-patch-browser.ts b/src/client/client-patch-browser.ts index fc5198607d1..e5e813faace 100644 --- a/src/client/client-patch-browser.ts +++ b/src/client/client-patch-browser.ts @@ -1,5 +1,5 @@ import { BUILD, NAMESPACE } from '@app-data'; -import { consoleDevInfo, doc, H, promiseResolve } from '@platform'; +import { consoleDevInfo, H, promiseResolve, win } from '@platform'; import type * as d from '../declarations'; @@ -15,7 +15,8 @@ export const patchBrowser = (): Promise => { } const scriptElm = BUILD.scriptDataOpts - ? Array.from(doc.querySelectorAll('script')).find( + ? win.document && + Array.from(win.document.querySelectorAll('script')).find( (s) => new RegExp(`\/${NAMESPACE}(\\.esm)?\\.js($|\\?|#)`).test(s.src) || s.getAttribute('data-stencil-namespace') === NAMESPACE, diff --git a/src/client/client-window.ts b/src/client/client-window.ts index 4337a10419f..85fee590f47 100644 --- a/src/client/client-window.ts +++ b/src/client/client-window.ts @@ -2,9 +2,11 @@ import { BUILD } from '@app-data'; import type * as d from '../declarations'; -export const win = typeof window !== 'undefined' ? window : ({} as Window); +interface StencilWindow extends Omit { + document?: Document; +} -export const doc = win.document || ({ head: {} } as Document); +export const win = (typeof window !== 'undefined' ? window : ({} as StencilWindow)) as StencilWindow; export const H = ((win as any).HTMLElement || (class {} as any)) as HTMLElement; @@ -33,7 +35,7 @@ export const supportsShadow = BUILD.shadowDom; export const supportsListenerOptions = /*@__PURE__*/ (() => { let supportsListenerOptions = false; try { - doc.addEventListener( + win.document?.addEventListener( 'e', null, Object.defineProperty({}, 'passive', { diff --git a/src/compiler/docs/style-docs.ts b/src/compiler/docs/style-docs.ts index 152b15b34cb..c037fd64ea5 100644 --- a/src/compiler/docs/style-docs.ts +++ b/src/compiler/docs/style-docs.ts @@ -68,22 +68,22 @@ function parseCssComment(styleDocs: d.StyleDoc[], comment: string, mode: string const docs = comment.split(CSS_PROP_ANNOTATION); docs.forEach((d) => { - const doc = d.trim(); + const cssDocument = d.trim(); - if (!doc.startsWith(`--`)) { + if (!cssDocument.startsWith(`--`)) { return; } - const splt = doc.split(`:`); - const cssDoc: d.StyleDoc = { + const splt = cssDocument.split(`:`); + const styleDoc: d.StyleDoc = { name: splt[0].trim(), docs: (splt.shift() && splt.join(`:`)).trim(), annotation: 'prop', mode, }; - if (!styleDocs.some((c) => c.name === cssDoc.name && c.annotation === 'prop')) { - styleDocs.push(cssDoc); + if (!styleDocs.some((c) => c.name === styleDoc.name && c.annotation === 'prop')) { + styleDocs.push(styleDoc); } }); } diff --git a/src/hydrate/platform/hydrate-app.ts b/src/hydrate/platform/hydrate-app.ts index 6f58eb5a156..73177c5c096 100644 --- a/src/hydrate/platform/hydrate-app.ts +++ b/src/hydrate/platform/hydrate-app.ts @@ -1,5 +1,5 @@ import { globalScripts } from '@app-globals'; -import { addHostEventListeners, doc, getHostRef, loadModule, plt, registerHost } from '@platform'; +import { addHostEventListeners, getHostRef, loadModule, plt, registerHost } from '@platform'; import { connectedCallback, insertVdomAnnotations } from '@runtime'; import { CMP_FLAGS } from '@utils'; @@ -169,7 +169,7 @@ export function hydrateApp( // ensure we use NodeJS's native setTimeout, not the mocked hydrate app scoped one tmrId = globalThis.setTimeout(timeoutExceeded, opts.timeout); - plt.$resourcesUrl$ = new URL(opts.resourcesUrl || './', doc.baseURI).href; + plt.$resourcesUrl$ = new URL(opts.resourcesUrl || './', win.document.baseURI).href; globalScripts(); diff --git a/src/hydrate/platform/index.ts b/src/hydrate/platform/index.ts index fb78f977016..3f9156812ae 100644 --- a/src/hydrate/platform/index.ts +++ b/src/hydrate/platform/index.ts @@ -54,8 +54,6 @@ export const registerComponents = (Cstrs: d.ComponentNativeConstructor[]) => { export const win = window; -export const doc = win.document; - export const readTask = (cb: Function) => { nextTick(() => { try { diff --git a/src/runtime/bootstrap-lazy.ts b/src/runtime/bootstrap-lazy.ts index 6e46f96f1bf..21b93d87cec 100644 --- a/src/runtime/bootstrap-lazy.ts +++ b/src/runtime/bootstrap-lazy.ts @@ -1,5 +1,5 @@ import { BUILD } from '@app-data'; -import { doc, getHostRef, plt, registerHost, supportsShadow, win } from '@platform'; +import { getHostRef, plt, registerHost, supportsShadow, win } from '@platform'; import { addHostEventListeners } from '@runtime'; import { CMP_FLAGS, queryNonceMetaTagContent } from '@utils'; @@ -27,19 +27,24 @@ export const bootstrapLazy = (lazyBundles: d.LazyBundlesRuntimeData, options: d. } installDevTools(); + if (!win.document) { + console.warn('Stencil: No document found. Skipping bootstrapping lazy components.'); + return; + } + const endBootstrap = createTime('bootstrapLazy'); const cmpTags: string[] = []; const exclude = options.exclude || []; const customElements = win.customElements; - const head = doc.head; + const head = win.document.head; const metaCharset = /*@__PURE__*/ head.querySelector('meta[charset]'); - const dataStyles = /*@__PURE__*/ doc.createElement('style'); + const dataStyles = /*@__PURE__*/ win.document.createElement('style'); const deferredConnectedCallbacks: { connectedCallback: () => void }[] = []; let appLoadFallback: any; let isBootstrapping = true; Object.assign(plt, options); - plt.$resourcesUrl$ = new URL(options.resourcesUrl || './', doc.baseURI).href; + plt.$resourcesUrl$ = new URL(options.resourcesUrl || './', win.document.baseURI).href; if (BUILD.asyncQueue) { if (options.syncQueue) { plt.$flags$ |= PLATFORM_FLAGS.queueSync; @@ -259,7 +264,7 @@ export const bootstrapLazy = (lazyBundles: d.LazyBundlesRuntimeData, options: d. dataStyles.setAttribute('data-styles', ''); // Apply CSP nonce to the style tag if it exists - const nonce = plt.$nonce$ ?? queryNonceMetaTagContent(doc); + const nonce = plt.$nonce$ ?? queryNonceMetaTagContent(win.document); if (nonce != null) { dataStyles.setAttribute('nonce', nonce); } diff --git a/src/runtime/client-hydrate.ts b/src/runtime/client-hydrate.ts index 603a84770d5..d3bcc2d954f 100644 --- a/src/runtime/client-hydrate.ts +++ b/src/runtime/client-hydrate.ts @@ -1,5 +1,5 @@ import { BUILD } from '@app-data'; -import { doc, plt } from '@platform'; +import { plt, win } from '@platform'; import { CMP_FLAGS } from '@utils'; import type * as d from '../declarations'; @@ -64,10 +64,10 @@ export const initializeClientHydrate = ( } } - if (!plt.$orgLocNodes$ || !plt.$orgLocNodes$.size) { + if (win.document && (!plt.$orgLocNodes$ || !plt.$orgLocNodes$.size)) { // This is the first pass over of this whole document; // does a scrape to construct a 'bare-bones' tree of what elements we have and where content has been moved from - initializeDocumentHydrate(doc.body, (plt.$orgLocNodes$ = new Map())); + initializeDocumentHydrate(win.document.body, (plt.$orgLocNodes$ = new Map())); } hostElm[HYDRATE_ID] = hostId; @@ -578,11 +578,11 @@ function addSlot( // Important because where it is now in the constructed SSR markup might be different to where to *should* be const parentNodeId = parentVNode?.$elm$ ? parentVNode.$elm$['s-id'] || parentVNode.$elm$.getAttribute('s-id') : ''; - if (BUILD.shadowDom && shadowRootNodes) { + if (BUILD.shadowDom && shadowRootNodes && win.document) { /* SHADOW */ // Browser supports shadowRoot and this is a shadow dom component; create an actual slot element - const slot = (childVNode.$elm$ = doc.createElement(childVNode.$tag$ as string) as d.RenderNode); + const slot = (childVNode.$elm$ = win.document.createElement(childVNode.$tag$ as string) as d.RenderNode); if (childVNode.$name$) { // Add the slot name attribute diff --git a/src/runtime/connected-callback.ts b/src/runtime/connected-callback.ts index 227512be800..209aa1fb3cb 100644 --- a/src/runtime/connected-callback.ts +++ b/src/runtime/connected-callback.ts @@ -1,5 +1,5 @@ import { BUILD } from '@app-data'; -import { addHostEventListeners, doc, getHostRef, nextTick, plt, supportsShadow } from '@platform'; +import { addHostEventListeners, getHostRef, nextTick, plt, supportsShadow, win } from '@platform'; import { CMP_FLAGS, HOST_FLAGS, MEMBER_FLAGS } from '@utils'; import type * as d from '../declarations'; @@ -124,13 +124,17 @@ export const connectedCallback = (elm: d.HostElement) => { }; const setContentReference = (elm: d.HostElement) => { + if (!win.document) { + return; + } + // only required when we're NOT using native shadow dom (slot) // or this browser doesn't support native shadow dom // and this host element was NOT created with SSR // let's pick out the inner content for slot projection // create a node to represent where the original // content was first placed, which is useful later on - const contentRefElm = (elm['s-cr'] = doc.createComment( + const contentRefElm = (elm['s-cr'] = win.document.createComment( BUILD.isDebug ? `content-ref (host=${elm.localName})` : '', ) as any); contentRefElm['s-cn'] = true; diff --git a/src/runtime/host-listener.ts b/src/runtime/host-listener.ts index 17ce2bad9f5..bf55bf0a4a8 100644 --- a/src/runtime/host-listener.ts +++ b/src/runtime/host-listener.ts @@ -1,5 +1,5 @@ import { BUILD } from '@app-data'; -import { consoleError, doc, plt, supportsListenerOptions, win } from '@platform'; +import { consoleError, plt, supportsListenerOptions, win } from '@platform'; import { HOST_FLAGS, LISTENER_FLAGS } from '@utils'; import type * as d from '../declarations'; @@ -10,7 +10,7 @@ export const addHostEventListeners = ( listeners?: d.ComponentRuntimeHostListener[], attachParentListeners?: boolean, ) => { - if (BUILD.hostListener && listeners) { + if (BUILD.hostListener && listeners && win.document) { // this is called immediately within the element's constructor // initialize our event listeners on the host element // we do this now so that we can listen to events that may @@ -32,7 +32,7 @@ export const addHostEventListeners = ( } listeners.map(([flags, name, method]) => { - const target = BUILD.hostListenerTarget ? getHostListenerTarget(elm, flags) : elm; + const target = BUILD.hostListenerTarget ? getHostListenerTarget(win.document, elm, flags) : elm; const handler = hostListenerProxy(hostRef, method); const opts = hostListenerOpts(flags); plt.ael(target, name, handler, opts); @@ -58,12 +58,20 @@ const hostListenerProxy = (hostRef: d.HostRef, methodName: string) => (ev: Event } }; -const getHostListenerTarget = (elm: Element, flags: number): EventTarget => { - if (BUILD.hostListenerTargetDocument && flags & LISTENER_FLAGS.TargetDocument) return doc; - if (BUILD.hostListenerTargetWindow && flags & LISTENER_FLAGS.TargetWindow) return win; - if (BUILD.hostListenerTargetBody && flags & LISTENER_FLAGS.TargetBody) return doc.body; - if (BUILD.hostListenerTargetParent && flags & LISTENER_FLAGS.TargetParent && elm.parentElement) +const getHostListenerTarget = (doc: Document, elm: Element, flags: number): EventTarget => { + if (BUILD.hostListenerTargetDocument && flags & LISTENER_FLAGS.TargetDocument) { + return doc; + } + if (BUILD.hostListenerTargetWindow && flags & LISTENER_FLAGS.TargetWindow) { + return win; + } + if (BUILD.hostListenerTargetBody && flags & LISTENER_FLAGS.TargetBody) { + return doc.body; + } + if (BUILD.hostListenerTargetParent && flags & LISTENER_FLAGS.TargetParent && elm.parentElement) { return elm.parentElement; + } + return elm; }; diff --git a/src/runtime/styles.ts b/src/runtime/styles.ts index ae5b6ef834d..3d02ba3256f 100644 --- a/src/runtime/styles.ts +++ b/src/runtime/styles.ts @@ -1,5 +1,5 @@ import { BUILD } from '@app-data'; -import { doc, plt, styles, supportsConstructableStylesheets, supportsShadow } from '@platform'; +import { plt, styles, supportsConstructableStylesheets, supportsShadow, win } from '@platform'; import { CMP_FLAGS, queryNonceMetaTagContent } from '@utils'; import type * as d from '../declarations'; @@ -51,12 +51,12 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet const scopeId = getScopeId(cmpMeta, mode); const style = styles.get(scopeId); - if (!BUILD.attachStyles) { + if (!BUILD.attachStyles || !win.document) { return scopeId; } // if an element is NOT connected then getRootNode() will return the wrong root node // so the fallback is to always use the document for the root node in those cases - styleContainerNode = styleContainerNode.nodeType === NODE_TYPE.DocumentFragment ? styleContainerNode : doc; + styleContainerNode = styleContainerNode.nodeType === NODE_TYPE.DocumentFragment ? styleContainerNode : win.document; if (style) { if (typeof style === 'string') { @@ -75,11 +75,12 @@ export const addStyle = (styleContainerNode: any, cmpMeta: d.ComponentRuntimeMet // This is only happening on native shadow-dom, do not needs CSS var shim styleElm.innerHTML = style; } else { - styleElm = document.querySelector(`[${HYDRATED_STYLE_ID}="${scopeId}"]`) || doc.createElement('style'); + styleElm = + document.querySelector(`[${HYDRATED_STYLE_ID}="${scopeId}"]`) || win.document.createElement('style'); styleElm.innerHTML = style; // Apply CSP nonce to the style tag if it exists - const nonce = plt.$nonce$ ?? queryNonceMetaTagContent(doc); + const nonce = plt.$nonce$ ?? queryNonceMetaTagContent(win.document); if (nonce != null) { styleElm.setAttribute('nonce', nonce); } @@ -247,7 +248,11 @@ export const convertScopedToShadow = (css: string) => css.replace(/\/\*!@([^\/]+ * and add them to a constructable stylesheet. */ export const hydrateScopedToShadow = () => { - const styles = doc.querySelectorAll(`[${HYDRATED_STYLE_ID}]`); + if (!win.document) { + return; + } + + const styles = win.document.querySelectorAll(`[${HYDRATED_STYLE_ID}]`); let i = 0; for (; i < styles.length; i++) { registerStyle(styles[i].getAttribute(HYDRATED_STYLE_ID), convertScopedToShadow(styles[i].innerHTML), true); diff --git a/src/runtime/test/bootstrap-lazy.spec.tsx b/src/runtime/test/bootstrap-lazy.spec.tsx index 0a34f39ac33..bbeb09a6890 100644 --- a/src/runtime/test/bootstrap-lazy.spec.tsx +++ b/src/runtime/test/bootstrap-lazy.spec.tsx @@ -1,11 +1,11 @@ -import { doc } from '@platform'; +import { win } from '@platform'; import { LazyBundlesRuntimeData } from '../../internal'; import { bootstrapLazy } from '../bootstrap-lazy'; describe('bootstrap lazy', () => { it('should not inject invalid CSS when no lazy bundles are provided', () => { - const spy = jest.spyOn(doc.head, 'insertBefore'); + const spy = jest.spyOn(win.document.head, 'insertBefore'); bootstrapLazy([]); @@ -25,7 +25,7 @@ describe('bootstrap lazy', () => { }); it('should not inject invalid CSS when components are already in custom element registry', () => { - const spy = jest.spyOn(doc.head, 'insertBefore'); + const spy = jest.spyOn(win.document.head, 'insertBefore'); const lazyBundles: LazyBundlesRuntimeData = [ ['my-component', [[0, 'my-component', { first: [1], middle: [1], last: [1] }]]], diff --git a/src/runtime/vdom/vdom-render.ts b/src/runtime/vdom/vdom-render.ts index 82c926fe761..e9875b1eb3e 100644 --- a/src/runtime/vdom/vdom-render.ts +++ b/src/runtime/vdom/vdom-render.ts @@ -7,7 +7,7 @@ * Modified for Stencil's renderer and slot projection */ import { BUILD } from '@app-data'; -import { consoleDevError, doc, plt, supportsShadow } from '@platform'; +import { consoleDevError, plt, supportsShadow, win } from '@platform'; import { CMP_FLAGS, HTML_NS, isDef, NODE_TYPES, SVG_NS } from '@utils'; import type * as d from '../../declarations'; @@ -74,11 +74,13 @@ const createElm = (oldParentVNode: d.VNode, newParentVNode: d.VNode, childIndex: if (BUILD.vdomText && newVNode.$text$ !== null) { // create text node - elm = newVNode.$elm$ = doc.createTextNode(newVNode.$text$) as any; + elm = newVNode.$elm$ = win.document.createTextNode(newVNode.$text$) as any; } else if (BUILD.slotRelocation && newVNode.$flags$ & VNODE_FLAGS.isSlotReference) { // create a slot reference node elm = newVNode.$elm$ = - BUILD.isDebug || BUILD.hydrateServerSide ? slotReferenceDebugNode(newVNode) : (doc.createTextNode('') as any); + BUILD.isDebug || BUILD.hydrateServerSide + ? slotReferenceDebugNode(newVNode) + : (win.document.createTextNode('') as any); // add css classes, attrs, props, listeners, etc. if (BUILD.vdomAttribute) { updateElement(null, newVNode, isSvgMode); @@ -87,16 +89,24 @@ const createElm = (oldParentVNode: d.VNode, newParentVNode: d.VNode, childIndex: if (BUILD.svg && !isSvgMode) { isSvgMode = newVNode.$tag$ === 'svg'; } + + if (!win.document) { + throw new Error( + "You are trying to render a Stencil component in an environment that doesn't support the DOM. " + + 'Make sure to populate the [`window`](https://developer.mozilla.org/en-US/docs/Web/API/Window/window) object before rendering a component.', + ); + } + // create element elm = newVNode.$elm$ = ( BUILD.svg - ? doc.createElementNS( + ? win.document.createElementNS( isSvgMode ? SVG_NS : HTML_NS, !useNativeShadowDom && BUILD.slotRelocation && newVNode.$flags$ & VNODE_FLAGS.isSlotFallback ? 'slot-fb' : (newVNode.$tag$ as string), ) - : doc.createElement( + : win.document.createElement( !useNativeShadowDom && BUILD.slotRelocation && newVNode.$flags$ & VNODE_FLAGS.isSlotFallback ? 'slot-fb' : (newVNode.$tag$ as string), @@ -1064,13 +1074,13 @@ render() { for (const relocateData of relocateNodes) { const nodeToRelocate = relocateData.$nodeToRelocate$; - if (!nodeToRelocate['s-ol']) { + if (!nodeToRelocate['s-ol'] && win.document) { // add a reference node marking this node's original location // keep a reference to this node for later lookups const orgLocationNode = BUILD.isDebug || BUILD.hydrateServerSide ? originalLocationDebugNode(nodeToRelocate) - : (doc.createTextNode('') as any); + : (win.document.createTextNode('') as any); orgLocationNode['s-nr'] = nodeToRelocate; insertBefore(nodeToRelocate.parentNode, (nodeToRelocate['s-ol'] = orgLocationNode), nodeToRelocate); @@ -1210,12 +1220,12 @@ render() { // slot comment debug nodes only created with the `--debug` flag // otherwise these nodes are text nodes w/out content const slotReferenceDebugNode = (slotVNode: d.VNode) => - doc.createComment( + win.document?.createComment( ` (host=${hostTagName.toLowerCase()})`, ); const originalLocationDebugNode = (nodeToRelocate: d.RenderNode): any => - doc.createComment( + win.document?.createComment( `org-location for ` + (nodeToRelocate.localName ? `<${nodeToRelocate.localName}> (host=${nodeToRelocate['s-hn']})` diff --git a/src/testing/platform/index.ts b/src/testing/platform/index.ts index 5d8e4074c41..1f59faeaa61 100644 --- a/src/testing/platform/index.ts +++ b/src/testing/platform/index.ts @@ -17,6 +17,6 @@ export { supportsShadow, } from './testing-platform'; export { flushAll, flushLoadModule, flushQueue, loadModule, nextTick, readTask, writeTask } from './testing-task-queue'; -export { doc, win } from './testing-window'; +export { win } from './testing-window'; export { Env } from '@app-data'; export * from '@runtime'; diff --git a/src/testing/platform/testing-window.ts b/src/testing/platform/testing-window.ts index ce931bc17d3..210e49975bd 100644 --- a/src/testing/platform/testing-window.ts +++ b/src/testing/platform/testing-window.ts @@ -1,5 +1,3 @@ import { setupGlobal } from '@stencil/core/mock-doc'; export const win = setupGlobal(global) as Window; - -export const doc = win.document; diff --git a/test/wdio/package.json b/test/wdio/package.json index 9cbbcec9869..4f55ff8a066 100644 --- a/test/wdio/package.json +++ b/test/wdio/package.json @@ -11,8 +11,9 @@ "build.prerender": "node ../../bin/stencil build --config prerender.stencil.config.ts --prerender --debug && node ./test-prerender/prerender.js && node ./test-prerender/no-script-build.js", "build.invisible-prehydration": "node ../../bin/stencil build --debug --es5 --config invisible-prehydration.stencil.config.ts", "build.no-external-runtime": "node ../../bin/stencil build --debug --es5 --config no-external-runtime.stencil.config.ts", - "test": "run-s build wdio", - "wdio": "wdio run ./wdio.conf.ts" + "test": "run-s build wdio end-to-end", + "wdio": "wdio run ./wdio.conf.ts", + "end-to-end": "node ./test-end-to-end-import.mjs" }, "devDependencies": { "@stencil/core": "file:../..", diff --git a/test/wdio/test-end-to-end-import.mjs b/test/wdio/test-end-to-end-import.mjs new file mode 100644 index 00000000000..18fdefbd7d0 --- /dev/null +++ b/test/wdio/test-end-to-end-import.mjs @@ -0,0 +1,9 @@ +import assert from 'node:assert'; + +import { AttributeBasicRoot, defineCustomElement } from './test-components/attribute-basic-root.js'; + +console.log(`🧪 Validate capability to import Stencil dist-custom-elements output target`); +assert.equal(typeof AttributeBasicRoot, 'function'); +assert.equal(typeof defineCustomElement, 'function'); +console.log(`✅ Success!`); +