diff --git a/package-lock.json b/package-lock.json index 501bc95..52d6228 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,7 @@ "license": "MIT", "dependencies": { "@xmldom/xmldom": "~0.8.4", - "jszip": "^3.5.0", - "xmldom-qsa": "^1.1.3" + "jszip": "^3.5.0" }, "devDependencies": { "@babel/core": "^7.14.6", @@ -15662,14 +15661,6 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, - "node_modules/xmldom-qsa": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/xmldom-qsa/-/xmldom-qsa-1.1.3.tgz", - "integrity": "sha512-IJBOczBpAYrIBJFFsmCBwfBhwe4zdMR3Xz0ZBX0OFtgO49rLy/BWbhkegOwsthdBWb1gUtFK6ZZnGdT8ZqPRBA==", - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 5ae0bc3..3409214 100644 --- a/package.json +++ b/package.json @@ -41,9 +41,10 @@ }, "homepage": "https://github.com/microsoft/connected-workbooks#readme", "dependencies": { - "@xmldom/xmldom": "~0.8.4", - "jszip": "^3.5.0", - "xmldom-qsa": "^1.1.3" + "jszip": "^3.5.0" + }, + "optionalDependencies": { + "@xmldom/xmldom": "~0.8.4" }, "devDependencies": { "@babel/core": "^7.14.6", diff --git a/src/utils/documentUtils.ts b/src/utils/documentUtils.ts index 6aedb46..bb7b6b5 100644 --- a/src/utils/documentUtils.ts +++ b/src/utils/documentUtils.ts @@ -15,7 +15,7 @@ import { Errors, } from "./constants"; import { DataTypes } from "../types"; -import { DOMParser } from "xmldom-qsa"; +import { DOMParser } from "./domUtils"; const createOrUpdateProperty = (doc: Document, parent: Element, property: string, value?: string | null): void => { if (value === undefined) { diff --git a/src/utils/domUtils.ts b/src/utils/domUtils.ts new file mode 100644 index 0000000..496baf6 --- /dev/null +++ b/src/utils/domUtils.ts @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/** + * Cross-platform DOM utilities that work in both Node.js and browser environments + */ + +// TypeScript types for DOMParser and XMLSerializer constructors +type DOMParserConstructor = new () => DOMParser; +type XMLSerializerConstructor = new () => XMLSerializer; + +let domParserCache: DOMParserConstructor | undefined; +let xmlSerializerCache: XMLSerializerConstructor | undefined; + +/** + * Gets a DOMParser implementation that works in both browser and Node.js environments + * In browsers, uses the native DOMParser + * In Node.js, requires the optional @xmldom/xmldom dependency + */ +export function getDOMParser(): DOMParserConstructor { + if (domParserCache) { + return domParserCache; + } + + // Browser environment - use native implementation + if (typeof window !== 'undefined' && window.DOMParser) { + domParserCache = window.DOMParser; + return window.DOMParser; + } + + // Node.js environment - try to load @xmldom/xmldom + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const xmldom = require('@xmldom/xmldom'); + domParserCache = xmldom.DOMParser; + return xmldom.DOMParser; + } catch (error) { + throw new Error( + 'DOM implementation not available in Node.js environment. ' + + 'Please install the optional dependency: npm install @xmldom/xmldom' + ); + } +} + +/** + * Gets an XMLSerializer implementation that works in both browser and Node.js environments + * In browsers, uses the native XMLSerializer + * In Node.js, requires the optional @xmldom/xmldom dependency + */ +export function getXMLSerializer(): XMLSerializerConstructor { + if (xmlSerializerCache) { + return xmlSerializerCache; + } + + // Browser environment - use native implementation + if (typeof window !== 'undefined' && window.XMLSerializer) { + xmlSerializerCache = window.XMLSerializer; + return window.XMLSerializer; + } + + // Node.js environment - try to load @xmldom/xmldom + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const xmldom = require('@xmldom/xmldom'); + xmlSerializerCache = xmldom.XMLSerializer; + return xmldom.XMLSerializer; + } catch (error) { + throw new Error( + 'DOM implementation not available in Node.js environment. ' + + 'Please install the optional dependency: npm install @xmldom/xmldom' + ); + } +} + +// For backward compatibility, export the classes directly +// These will throw helpful error messages if @xmldom/xmldom is not available in Node.js +export const DOMParser: DOMParserConstructor = getDOMParser(); +export const XMLSerializer: XMLSerializerConstructor = getXMLSerializer(); \ No newline at end of file diff --git a/src/utils/mashupDocumentParser.ts b/src/utils/mashupDocumentParser.ts index 92d6d72..ebf0458 100644 --- a/src/utils/mashupDocumentParser.ts +++ b/src/utils/mashupDocumentParser.ts @@ -17,7 +17,7 @@ import { } from "./constants"; import { arrayUtils } from "."; import { Metadata } from "../types"; -import { DOMParser, XMLSerializer } from "xmldom-qsa"; +import { DOMParser, XMLSerializer } from "./domUtils"; export const replaceSingleQuery = async (base64Str: string, queryName: string, queryMashupDoc: string): Promise => { const { version, packageOPC, permissionsSize, permissions, metadata, endBuffer } = getPackageComponents(base64Str); diff --git a/src/utils/pqUtils.ts b/src/utils/pqUtils.ts index 4610033..8e969b5 100644 --- a/src/utils/pqUtils.ts +++ b/src/utils/pqUtils.ts @@ -4,7 +4,7 @@ import JSZip from "jszip"; import { maxQueryLength, URLS, BOM, Errors } from "./constants"; import { generateMashupXMLTemplate, generateCustomXmlFilePath } from "../generators"; -import { DOMParser } from "xmldom-qsa"; +import { DOMParser } from "./domUtils"; import arrayUtils from "./arrayUtils"; type CustomXmlFile = { diff --git a/src/utils/tableUtils.ts b/src/utils/tableUtils.ts index a386575..3aec2ce 100644 --- a/src/utils/tableUtils.ts +++ b/src/utils/tableUtils.ts @@ -15,7 +15,7 @@ import { } from "./constants"; import documentUtils from "./documentUtils"; import { generateUUID } from "./uuid"; -import { DOMParser, XMLSerializer } from "xmldom-qsa"; +import { DOMParser, XMLSerializer } from "./domUtils"; /** * Update initial data for a table, its sheet, query table, and defined name if provided. diff --git a/src/utils/xmlInnerPartsUtils.ts b/src/utils/xmlInnerPartsUtils.ts index 676f6f9..70bc5b5 100644 --- a/src/utils/xmlInnerPartsUtils.ts +++ b/src/utils/xmlInnerPartsUtils.ts @@ -29,7 +29,7 @@ import { Errors, } from "./constants"; import documentUtils from "./documentUtils"; -import { DOMParser, XMLSerializer } from "xmldom-qsa"; +import { DOMParser, XMLSerializer } from "./domUtils"; /** * Helper function to check for XML parser errors without using querySelector diff --git a/tests/documentUtils.test.ts b/tests/documentUtils.test.ts index 593dd64..4801875 100644 --- a/tests/documentUtils.test.ts +++ b/tests/documentUtils.test.ts @@ -5,7 +5,7 @@ import { DataTypes } from "../src/types"; import { documentUtils } from "../src/utils"; import { element } from "../src/utils/constants"; import { describe, test, expect } from '@jest/globals'; -import { DOMParser } from 'xmldom-qsa'; +import { DOMParser } from '../src/utils/domUtils'; describe("Document Utils tests", () => { test("ResolveType date not supported success", () => { diff --git a/tests/xmlInnerPartsUtils.test.ts b/tests/xmlInnerPartsUtils.test.ts index b7b5c82..3d39fcf 100644 --- a/tests/xmlInnerPartsUtils.test.ts +++ b/tests/xmlInnerPartsUtils.test.ts @@ -7,7 +7,7 @@ import { describe, test, expect } from '@jest/globals'; import JSZip from "jszip"; import { SIMPLE_BLANK_TABLE_TEMPLATE, SIMPLE_QUERY_WORKBOOK_TEMPLATE, WORKBOOK_TEMPLATE_MOVED_TABLE } from "../src/workbookTemplate"; import { customXML, Errors } from "../src/utils/constants"; -import { DOMParser } from "xmldom-qsa"; +import { DOMParser } from "../src/utils/domUtils"; describe("Workbook Manager tests", () => { const mockConnectionString = `