Skip to content

Commit a9fa449

Browse files
authored
fix(polyfill): importing it beside another DOM (solid-js / DOMiNATIVE) (#168)
query-selector probes the global document for old IE as it is evaluated. With DOMiNATIVE's undom-ng document already installed, `document.createElement('div')` has no getElementsByTagName and importing the polyfill threw. The document is now hidden while query-selector loads, then put back; the polyfill only runs it on xmldom trees, which do not need the probe. querySelectorAll now searches the same XML tree as querySelector instead of the wrapper element. __test__/query-selector.test.mjs: query-selector alone throws beside an undom-like document; the polyfill's import does not, puts the document back, and still selects in xmldom trees. Fixes #134
1 parent e8ca8e8 commit a9fa449

5 files changed

Lines changed: 95 additions & 5 deletions

File tree

‎packages/canvas-polyfill/DOM/Element.ts‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Node } from './Node';
22
import { Frame, StackLayout, View, ViewBase } from '@nativescript/core';
33
import setValue from 'set-value';
4-
import querySelector from 'query-selector';
4+
import querySelector from './querySelector';
55
import { HTMLCollection } from './HTMLCollection';
66
import { Canvas } from '@nativescript/canvas';
77
import { domGetElementsByClassName, domGetElementsByTagName } from './domUtils';
@@ -303,9 +303,12 @@ export class Element extends Node {
303303
return [];
304304
}
305305

306+
private get __selectorContext() {
307+
return (<any>this)._xmlDom?.documentElement ?? (<any>this)._xmlDom ?? this.__instance ?? this.nativeElement?.__domElement;
308+
}
309+
306310
querySelector(selector: string) {
307-
const context = (<any>this)._xmlDom?.documentElement ?? (<any>this)._xmlDom ?? this.__instance ?? this.nativeElement?.__domElement;
308-
const selection = querySelector(selector, context);
311+
const selection = querySelector(selector, this.__selectorContext);
309312
if (Array.isArray(selection)) {
310313
const item = selection[0];
311314
if (item) {
@@ -327,7 +330,7 @@ export class Element extends Node {
327330
}
328331

329332
querySelectorAll(selector: string) {
330-
return querySelector(selector, this);
333+
return querySelector(selector, this.__selectorContext);
331334
}
332335

333336
getBoundingClientRect() {

‎packages/canvas-polyfill/DOM/XMLDocument.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Document as Doc } from './Document';
22

3-
import querySelector from 'query-selector';
3+
import querySelector from './querySelector';
44

55
import { SVGSVGElement } from './svg/SVGSVGElement';
66

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// The guard is evaluated first: see ./querySelectorGuard.
2+
import { restoreDocument } from './querySelectorGuard';
3+
import querySelector from 'query-selector';
4+
5+
restoreDocument();
6+
7+
export default querySelector;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// query-selector probes the global `document` for old IE as it is evaluated. Another DOM's document
2+
// (undom-ng under DOMiNATIVE) fails that probe and the import throws, so ./querySelector hides it
3+
// until query-selector has loaded. Nothing it does later relies on the probe.
4+
const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'document');
5+
let hidden = false;
6+
7+
if (descriptor?.configurable) {
8+
delete (globalThis as any).document;
9+
hidden = true;
10+
} else if (descriptor?.writable) {
11+
(globalThis as any).document = undefined;
12+
hidden = true;
13+
}
14+
15+
export function restoreDocument() {
16+
if (!hidden) {
17+
return;
18+
}
19+
hidden = false;
20+
if (descriptor.configurable) {
21+
Object.defineProperty(globalThis, 'document', descriptor);
22+
} else {
23+
(globalThis as any).document = descriptor.value;
24+
}
25+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { test } from 'node:test';
2+
import assert from 'node:assert/strict';
3+
import { fileURLToPath } from 'node:url';
4+
import { build } from 'esbuild';
5+
import { DOMParser } from '@xmldom/xmldom';
6+
7+
// As undom-ng installs under DOMiNATIVE (solid-js): a document whose elements have no
8+
// getElementsByTagName, which query-selector's import-time probe calls.
9+
function installForeignDocument() {
10+
const foreign = {
11+
createElement: (tag) => ({ nodeName: tag.toUpperCase(), childNodes: [], appendChild(node) { this.childNodes.push(node); return node; } }),
12+
createComment: (data) => ({ nodeType: 8, data }),
13+
};
14+
globalThis.document = foreign;
15+
return foreign;
16+
}
17+
18+
const packageDir = fileURLToPath(new URL('..', import.meta.url));
19+
20+
/** `source` bundled the way an app bundler would, from this package. */
21+
async function load(source) {
22+
const { outputFiles } = await build({
23+
stdin: { contents: source, resolveDir: packageDir, loader: 'ts' },
24+
bundle: true,
25+
format: 'esm',
26+
write: false,
27+
platform: 'neutral',
28+
mainFields: ['module', 'main'],
29+
});
30+
return import('data:text/javascript;base64,' + Buffer.from(outputFiles[0].text).toString('base64') + '#' + Math.random());
31+
}
32+
33+
const svg = new DOMParser().parseFromString('<svg xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="g"/></defs><rect class="a"/><rect/></svg>', 'image/svg+xml');
34+
35+
test('query-selector on its own fails beside a foreign document', async () => {
36+
installForeignDocument();
37+
await assert.rejects(load("export { default } from 'query-selector';"), /getElementsByTagName/);
38+
delete globalThis.document;
39+
});
40+
41+
test('the polyfill loads query-selector beside a foreign document and puts it back', async () => {
42+
const foreign = installForeignDocument();
43+
const { default: querySelector } = await load("export { default } from './DOM/querySelector';");
44+
assert.equal(globalThis.document, foreign);
45+
assert.deepEqual(querySelector('#g', svg.documentElement).map((node) => node.nodeName), ['linearGradient']);
46+
assert.equal(querySelector('rect', svg.documentElement).length, 2);
47+
assert.equal(querySelector('rect.a', svg.documentElement).length, 1);
48+
delete globalThis.document;
49+
});
50+
51+
test('without a document nothing is installed', async () => {
52+
delete globalThis.document;
53+
await load("export { default } from './DOM/querySelector';");
54+
assert.equal('document' in globalThis, false);
55+
});

0 commit comments

Comments
 (0)