From 3dcb7f31bca57948d82205dfda6f08075518fccd Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Thu, 17 Mar 2022 05:35:46 -0500 Subject: [PATCH] fix: ensure doc.defaultView, url as string (#285) --- src/server/api.md | 2 +- src/server/document.ts | 29 ++----------- src/server/platform.ts | 4 +- src/server/types.ts | 2 +- src/server/utils.ts | 56 ++++++++++++++++++++++++ src/server/utils.unit.ts | 92 ++++++++++++++++++++++++++++++++++++++++ src/testing/api.md | 2 +- 7 files changed, 156 insertions(+), 31 deletions(-) create mode 100644 src/server/utils.unit.ts diff --git a/src/server/api.md b/src/server/api.md index fd2fadc2865..d8221fd7d84 100644 --- a/src/server/api.md +++ b/src/server/api.md @@ -28,7 +28,7 @@ export interface DocumentOptions { // (undocumented) debug?: boolean; // (undocumented) - url?: URL; + url?: URL | string; } // @alpha diff --git a/src/server/document.ts b/src/server/document.ts index 6fe902e6da6..964fa3290f1 100644 --- a/src/server/document.ts +++ b/src/server/document.ts @@ -1,3 +1,4 @@ +import { createTimer, ensureGlobals } from './utils'; import { dehydrate, FunctionComponent, JSXNode, render } from '@builder.io/qwik'; import qwikDom from '@builder.io/qwik-dom'; import { setServerPlatform } from './platform'; @@ -11,7 +12,6 @@ import type { RenderToStringOptions, RenderToStringResult, } from './types'; -import { createTimer } from './utils'; /** * Create emulated `Global` for server environment. Does not implement a browser @@ -22,27 +22,8 @@ export function createGlobal(opts?: GlobalOptions): QwikGlobal { opts = opts || {}; const doc: QwikDocument = qwikDom.createDocument() as any; - const baseURI = opts.url === undefined ? BASE_URI : opts.url.href; - const loc = new URL(baseURI, BASE_URI); - Object.defineProperty(doc, 'baseURI', { - get: () => loc.href, - set: (url: string) => (loc.href = url), - }); - - const glb: any = { - document: doc, - location: loc, - CustomEvent: class CustomEvent { - type: string; - constructor(type: string, details: any) { - Object.assign(this, details); - this.type = type; - } - }, - }; - - glb.document.defaultView = glb; + const glb = ensureGlobals(doc, opts); return glb; } @@ -69,9 +50,7 @@ export async function renderToDocument( rootNode: JSXNode | FunctionComponent, opts: RenderToDocumentOptions ) { - if (!doc || doc.nodeType !== 9) { - throw new Error(`Invalid document`); - } + ensureGlobals(doc, opts); await setServerPlatform(doc, opts); @@ -108,5 +87,3 @@ export async function renderToString(rootNode: any, opts: RenderToStringOptions) return result; } - -const BASE_URI = `http://document.qwik.dev/`; diff --git a/src/server/platform.ts b/src/server/platform.ts index fa149ce2ae2..5cdb2c4c1bf 100644 --- a/src/server/platform.ts +++ b/src/server/platform.ts @@ -1,9 +1,9 @@ import type { CorePlatform } from '@builder.io/qwik'; +import { normalizeUrl } from './utils'; import { setPlatform } from '@builder.io/qwik'; import type { SerializeDocumentOptions } from './types'; const _setImmediate = typeof setImmediate === 'function' ? setImmediate : setTimeout; -// const _nextTick = typeof queueMicrotask === 'function' ? queueMicrotask : process.nextTick; declare const require: (module: string) => Record; @@ -14,7 +14,7 @@ function createPlatform(document: any, opts: SerializeDocumentOptions) { const doc: Document = document; const symbols = opts.symbols; if (opts?.url) { - doc.location.href = opts.url.href; + doc.location.href = normalizeUrl(opts.url).href; } const serverPlatform: CorePlatform = { async importSymbol(_element, qrl, symbolName) { diff --git a/src/server/types.ts b/src/server/types.ts index d8eef9a3ce8..cdeb0c00f55 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -27,7 +27,7 @@ export interface QwikDocument extends Document {} * @public */ export interface DocumentOptions { - url?: URL; + url?: URL | string; debug?: boolean; } diff --git a/src/server/utils.ts b/src/server/utils.ts index c58bf286979..c2617565ff2 100644 --- a/src/server/utils.ts +++ b/src/server/utils.ts @@ -1,3 +1,5 @@ +import type { DocumentOptions } from './types'; + /** * Utility timer function for performance profiling. * Returns a duration of 0 in environments that do not support performance. @@ -14,3 +16,57 @@ export function createTimer() { return delta / 1000000; }; } + +export function ensureGlobals(doc: any, opts: DocumentOptions) { + if (!doc[QWIK_DOC]) { + if (!doc || doc.nodeType !== 9) { + throw new Error(`Invalid document`); + } + + doc[QWIK_DOC] = true; + + const loc = normalizeUrl(opts.url); + + Object.defineProperty(doc, 'baseURI', { + get: () => loc.href, + set: (url: string) => (loc.href = normalizeUrl(url).href), + }); + + doc.defaultView = { + get document() { + return doc; + }, + get location() { + return loc; + }, + get origin() { + return loc.origin; + }, + CustomEvent: class CustomEvent { + type: string; + constructor(type: string, details: any) { + Object.assign(this, details); + this.type = type; + } + }, + }; + } + + return doc.defaultView; +} + +const QWIK_DOC = Symbol(); + +export function normalizeUrl(url: string | URL | undefined | null) { + if (url != null) { + if (typeof url === 'string') { + return new URL(url || '/', BASE_URI); + } + if (typeof url.href === 'string') { + return new URL(url.href || '/', BASE_URI); + } + } + return new URL(BASE_URI); +} + +const BASE_URI = `http://document.qwik.dev/`; diff --git a/src/server/utils.unit.ts b/src/server/utils.unit.ts new file mode 100644 index 00000000000..1d77fb98d16 --- /dev/null +++ b/src/server/utils.unit.ts @@ -0,0 +1,92 @@ +import { createDocument, createGlobal } from './document'; +import { ensureGlobals, normalizeUrl } from './utils'; + +describe('normalizeUrl', () => { + it('no url', () => { + expect(normalizeUrl(null).href).toBe('http://document.qwik.dev/'); + expect(normalizeUrl(undefined).href).toBe('http://document.qwik.dev/'); + expect(normalizeUrl('').href).toBe('http://document.qwik.dev/'); + expect(normalizeUrl({} as any).href).toBe('http://document.qwik.dev/'); + }); + + it('string, full url', () => { + const url = normalizeUrl('https://my.qwik.dev/some-path?query=string#hash'); + expect(url.pathname).toBe('/some-path'); + expect(url.hash).toBe('#hash'); + expect(url.searchParams.get('query')).toBe('string'); + expect(url.origin).toBe('https://my.qwik.dev'); + expect(url.href).toBe('https://my.qwik.dev/some-path?query=string#hash'); + }); + + it('string, pathname', () => { + const url = normalizeUrl('/some-path?query=string#hash'); + expect(url.pathname).toBe('/some-path'); + expect(url.hash).toBe('#hash'); + expect(url.searchParams.get('query')).toBe('string'); + expect(url.origin).toBe('http://document.qwik.dev'); + expect(url.href).toBe('http://document.qwik.dev/some-path?query=string#hash'); + }); +}); + +describe('ensureGlobals', () => { + it('baseURI', () => { + const glb = ensureGlobals({ nodeType: 9 }, { url: 'http://my.qwik.dev/my-path' }); + expect(glb.document.baseURI).toBe('http://my.qwik.dev/my-path'); + + glb.document.baseURI = 'http://my.qwik.dev/new-path'; + expect(glb.document.baseURI).toBe('http://my.qwik.dev/new-path'); + }); + + it('location, no options', () => { + const glb = ensureGlobals({ nodeType: 9 }, {}); + expect(glb.location.pathname).toBe('/'); + + glb.location.pathname = '/new-path'; + expect(glb.location.pathname).toBe('/new-path'); + }); + + it('location', () => { + const glb = ensureGlobals({ nodeType: 9 }, { url: '/my-path' }); + expect(glb.location.pathname).toBe('/my-path'); + }); + + it('origin, no options', () => { + const glb = ensureGlobals({ nodeType: 9 }, {}); + expect(glb.origin).toBe('http://document.qwik.dev'); + }); + + it('origin', () => { + const glb = ensureGlobals({ nodeType: 9 }, { url: '/my-path' }); + expect(glb.origin).toBe('http://document.qwik.dev'); + }); + + it('invalid document', () => { + expect(() => { + ensureGlobals({}, {}); + }).toThrow(); + }); +}); + +describe('document ensureGlobals', () => { + it('qwik server createDocument()', () => { + const doc = createDocument(); + expect(doc.defaultView).not.toBeUndefined(); + expect(doc.defaultView!.document).toBe(doc); + }); + + it('qwik server createGlobal()', () => { + const gbl = createGlobal(); + expect(gbl.document.defaultView).not.toBeUndefined(); + expect(gbl.document.defaultView).toBe(gbl); + }); + + it('some other document', () => { + const doc: any = { + nodeType: 9, + }; + ensureGlobals(doc, {}); + ensureGlobals(doc, {}); // shouldn't reset + expect(doc.defaultView).not.toBeUndefined(); + expect(doc.defaultView.document).toBe(doc); + }); +}); diff --git a/src/testing/api.md b/src/testing/api.md index fd2fadc2865..d8221fd7d84 100644 --- a/src/testing/api.md +++ b/src/testing/api.md @@ -28,7 +28,7 @@ export interface DocumentOptions { // (undocumented) debug?: boolean; // (undocumented) - url?: URL; + url?: URL | string; } // @alpha