diff --git a/.gitignore b/.gitignore
index fd60e545..e7cdd7db 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,6 +31,9 @@ jasmine-config/reporter.js
bundle-config-loader.d.ts
bundle-config-loader.js
+xml-namespace-loader.d.ts
+xml-namespace-loader.js
+
**/*.spec.js*
**/*.spec.d.ts*
diff --git a/package.json b/package.json
index b0b0b743..23cff215 100644
--- a/package.json
+++ b/package.json
@@ -63,6 +63,7 @@
"request": "2.88.0",
"resolve-url-loader": "~3.0.0",
"sass-loader": "~7.1.0",
+ "sax": "^1.2.4",
"schema-utils": "0.4.5",
"semver": "^6.0.0",
"shelljs": "0.6.0",
@@ -84,6 +85,7 @@
"@types/loader-utils": "^1.1.3",
"@types/node": "^10.12.12",
"@types/proxyquire": "1.3.28",
+ "@types/sax": "^1.2.0",
"@types/semver": "^6.0.0",
"@types/webpack": "^4.4.34",
"conventional-changelog-cli": "^1.3.22",
diff --git a/xml-namespace-loader.spec.ts b/xml-namespace-loader.spec.ts
new file mode 100644
index 00000000..641d83ed
--- /dev/null
+++ b/xml-namespace-loader.spec.ts
@@ -0,0 +1,224 @@
+import xmlNsLoader from "./xml-namespace-loader";
+import { convertSlashesInPath } from "./projectHelpers";
+
+const CODE_FILE = `
+
+
+
+
+
+
+
+
+
+
+`;
+
+interface TestSetup {
+ resolveMap: { [path: string]: string },
+ expectedDeps: string[],
+ expectedRegs: { name: string, path: string }[],
+ ignore?: RegExp,
+ assureNoDeps?: boolean,
+ expectError?: boolean
+}
+
+function getContext(
+ done: DoneFn,
+ { resolveMap, expectedDeps, expectedRegs, assureNoDeps, ignore, expectError }: TestSetup) {
+ const actualDeps: string[] = [];
+
+ const loaderContext = {
+ rootContext: "app",
+ context: "app/component",
+ async: () => (error, source: string) => {
+ expectedDeps.forEach(expectedDep => expect(actualDeps).toContain(expectedDep));
+
+ expectedRegs.forEach(({ name, path }) => {
+ const regCode = `global.registerModule("${name}", function() { return require("${path}"); });`;
+ expect(source).toContain(regCode);
+ })
+
+ if (assureNoDeps) {
+ expect(actualDeps.length).toBe(0);
+ expect(source).not.toContain("global.registerModule");
+ }
+
+ if (error && !expectError) {
+ done.fail(error)
+ } else if (!error && expectError) {
+ done.fail("Error expected here")
+ } else {
+ done();
+ }
+ },
+ resolve: (context: string, request: string, callback: (err: Error, result: string) => void) => {
+ request = convertSlashesInPath(request);
+ if (resolveMap[request]) {
+ callback(undefined, resolveMap[request]);
+ } else {
+ callback(new Error(`Module ${request} not found`), undefined);
+ }
+ },
+ addDependency: (dep: string) => {
+ actualDeps.push(dep);
+ },
+ query: { ignore }
+ }
+
+ return loaderContext;
+}
+
+describe("XmlNamespaceLoader", () => {
+ it("with namespace pointing to files", (done) => {
+ const resolveMap = {
+ "app/nativescript-ui-chart": "app/nativescript-ui-chart.js",
+ "app/nativescript-ui-chart.xml": "app/nativescript-ui-chart.xml",
+ "app/nativescript-ui-chart.css": "app/nativescript-ui-chart.css",
+ };
+
+ const expectedDeps = [
+ "app/nativescript-ui-chart.js",
+ "app/nativescript-ui-chart.xml",
+ "app/nativescript-ui-chart.css",
+ ];
+
+ const expectedRegs = [
+ { name: "nativescript-ui-chart", path: "app/nativescript-ui-chart.js" },
+ { name: "nativescript-ui-chart/RadCartesianChart", path: "app/nativescript-ui-chart.js" },
+ { name: "nativescript-ui-chart/RadCartesianChart.xml", path: "app/nativescript-ui-chart.xml" },
+ { name: "nativescript-ui-chart/RadCartesianChart.css", path: "app/nativescript-ui-chart.css" },
+ ];
+
+ const loaderContext = getContext(done, { resolveMap, expectedDeps, expectedRegs });
+
+ xmlNsLoader.call(loaderContext, CODE_FILE);
+ })
+
+ it("with namespace/elementName pointing to files (with package.json)", (done) => {
+ const resolveMap = {
+ "app/nativescript-ui-chart": "app/nativescript-ui-chart/RadCartesianChart.js", //simulate package.json
+ "app/nativescript-ui-chart/RadCartesianChart": "app/nativescript-ui-chart/RadCartesianChart.js",
+ "app/nativescript-ui-chart/RadCartesianChart.xml": "app/nativescript-ui-chart/RadCartesianChart.xml",
+ "app/nativescript-ui-chart/RadCartesianChart.css": "app/nativescript-ui-chart/RadCartesianChart.css",
+ }
+
+ const expectedDeps = [
+ "app/nativescript-ui-chart/RadCartesianChart.js",
+ "app/nativescript-ui-chart/RadCartesianChart.xml",
+ "app/nativescript-ui-chart/RadCartesianChart.css",
+ ];
+
+ const expectedRegs = [
+ { name: "nativescript-ui-chart", path: "app/nativescript-ui-chart/RadCartesianChart.js" },
+ { name: "nativescript-ui-chart/RadCartesianChart", path: "app/nativescript-ui-chart/RadCartesianChart.js" },
+ { name: "nativescript-ui-chart/RadCartesianChart.xml", path: "app/nativescript-ui-chart/RadCartesianChart.xml" },
+ { name: "nativescript-ui-chart/RadCartesianChart.css", path: "app/nativescript-ui-chart/RadCartesianChart.css" },
+ ];
+
+ const loaderContext = getContext(done, { resolveMap, expectedDeps, expectedRegs });
+ xmlNsLoader.call(loaderContext, CODE_FILE);
+ })
+
+ it("with namespace/elementName pointing to files", (done) => {
+ const resolveMap = {
+ "app/nativescript-ui-chart/RadCartesianChart": "app/nativescript-ui-chart/RadCartesianChart.js",
+ "app/nativescript-ui-chart/RadCartesianChart.xml": "app/nativescript-ui-chart/RadCartesianChart.xml",
+ "app/nativescript-ui-chart/RadCartesianChart.css": "app/nativescript-ui-chart/RadCartesianChart.css",
+ }
+
+ const expectedDeps = [
+ "app/nativescript-ui-chart/RadCartesianChart.js",
+ "app/nativescript-ui-chart/RadCartesianChart.xml",
+ "app/nativescript-ui-chart/RadCartesianChart.css",
+ ];
+
+ const expectedRegs = [
+ { name: "nativescript-ui-chart", path: "app/nativescript-ui-chart/RadCartesianChart.js" },
+ { name: "nativescript-ui-chart/RadCartesianChart", path: "app/nativescript-ui-chart/RadCartesianChart.js" },
+ { name: "nativescript-ui-chart/RadCartesianChart.xml", path: "app/nativescript-ui-chart/RadCartesianChart.xml" },
+ { name: "nativescript-ui-chart/RadCartesianChart.css", path: "app/nativescript-ui-chart/RadCartesianChart.css" },
+ ];
+
+ const loaderContext = getContext(done, { resolveMap, expectedDeps, expectedRegs });
+ xmlNsLoader.call(loaderContext, CODE_FILE);
+ })
+
+ it("with namespace/elementName pointing to files - only XML and CSS", (done) => {
+ const resolveMap = {
+ "app/nativescript-ui-chart/RadCartesianChart.xml": "app/nativescript-ui-chart/RadCartesianChart.xml",
+ "app/nativescript-ui-chart/RadCartesianChart.css": "app/nativescript-ui-chart/RadCartesianChart.css",
+ }
+
+ const expectedDeps = [
+ "app/nativescript-ui-chart/RadCartesianChart.xml",
+ "app/nativescript-ui-chart/RadCartesianChart.css",
+ ];
+
+ const expectedRegs = [
+ { name: "nativescript-ui-chart/RadCartesianChart.xml", path: "app/nativescript-ui-chart/RadCartesianChart.xml" },
+ { name: "nativescript-ui-chart/RadCartesianChart.css", path: "app/nativescript-ui-chart/RadCartesianChart.css" },
+ ];
+
+ const loaderContext = getContext(done, { resolveMap, expectedDeps, expectedRegs });
+ xmlNsLoader.call(loaderContext, CODE_FILE);
+ })
+
+ it("with plugin path", (done) => {
+ const resolveMap = {
+ "nativescript-ui-chart": "node_module/nativescript-ui-chart/ui-chart.js",
+ }
+
+ const expectedDeps = [
+ ];
+
+ const expectedRegs = [
+ { name: "nativescript-ui-chart", path: "nativescript-ui-chart" },
+ { name: "nativescript-ui-chart/RadCartesianChart", path: "nativescript-ui-chart" },
+ ];
+
+ const loaderContext = getContext(done, { resolveMap, expectedDeps, expectedRegs });
+ xmlNsLoader.call(loaderContext, CODE_FILE);
+ })
+
+ it("with ignored namespace should not add deps or register calls", (done) => {
+ const resolveMap = {
+ "app/nativescript-ui-chart": "app/nativescript-ui-chart.js",
+ "app/nativescript-ui-chart.xml": "app/nativescript-ui-chart.xml",
+ "app/nativescript-ui-chart.css": "app/nativescript-ui-chart.css",
+ };
+ const expectedDeps = [];
+ const expectedRegs = [];
+
+ const loaderContext = getContext(done, { resolveMap, expectedDeps, expectedRegs, ignore: /nativescript\-ui\-chart/, assureNoDeps: true });
+
+ xmlNsLoader.call(loaderContext, CODE_FILE);
+ })
+
+ it("with XML declaration and Doctype does not fail", (done) => {
+ const resolveMap = {};
+ const expectedDeps = [];
+ const expectedRegs = [];
+
+ const testXml = `
+
+
+
+ `;
+
+ const loaderContext = getContext(done, { resolveMap, expectedDeps, expectedRegs, assureNoDeps: true });
+
+ xmlNsLoader.call(loaderContext, testXml);
+ })
+ it("with invalid XML fails", (done) => {
+ const resolveMap = {};
+ const expectedDeps = [];
+ const expectedRegs = [];
+
+ const testXml = ``;
+
+ const loaderContext = getContext(done, { resolveMap, expectedDeps, expectedRegs, expectError: true });
+
+ xmlNsLoader.call(loaderContext, testXml);
+ })
+});
diff --git a/xml-namespace-loader.js b/xml-namespace-loader.ts
similarity index 69%
rename from xml-namespace-loader.js
rename to xml-namespace-loader.ts
index e1294953..26e592dc 100644
--- a/xml-namespace-loader.js
+++ b/xml-namespace-loader.ts
@@ -1,22 +1,26 @@
-const { parse, relative, join, basename, extname } = require("path");
-const { promisify } = require('util');
-const { convertSlashesInPath } = require("./projectHelpers");
+import { parse, join } from "path";
+import { promisify } from "util";
+import { loader } from "webpack";
+import { parser, QualifiedTag } from "sax";
-module.exports = function (source, map) {
- this.value = source;
+import { convertSlashesInPath } from "./projectHelpers";
+
+interface NamespaceEntry {
+ name: string;
+ path: string
+}
+
+const loader: loader.Loader = function (source: string, map) {
const { ignore } = this.query;
const callback = this.async();
- const { XmlParser } = require("tns-core-modules/xml");
-
const resolvePromise = promisify(this.resolve);
- const promises = [];
+ const promises: Promise[] = [];
+ const namespaces: NamespaceEntry[] = [];
+ let parsingError = false;
- const namespaces = [];
- const parser = new XmlParser((event) => {
- const { namespace, elementName } = event;
+ const handleOpenTag = (namespace: string, elementName: string) => {
const moduleName = `${namespace}/${elementName}`;
-
if (
namespace &&
!namespace.startsWith("http") &&
@@ -55,7 +59,7 @@ module.exports = function (source, map) {
promises.push(resolvePromise(this.context, localNamespacePath)
.then(path => pathResolved(path))
.catch(() => {
- return promise = resolvePromise(this.context, localModulePath)
+ return resolvePromise(this.context, localModulePath)
.then(path => pathResolved(path))
.catch(() => {
return Promise.all([
@@ -81,17 +85,25 @@ module.exports = function (source, map) {
})
);
}
- }, undefined, true);
+ }
- parser.parse(source);
+ const saxParser = parser(true, { xmlns: true });
+ saxParser.onopentag = (node: QualifiedTag) => { handleOpenTag(node.uri, node.local); };
+ saxParser.onerror = (err) => {
+ saxParser.error = null;
+ parsingError = true;
+ callback(err);
+ };
+ saxParser.write(source).close();
Promise.all(promises).then(() => {
- const moduleRegisters = namespaces
- .map(convertPath)
- .map(n =>
- `global.registerModule("${n.name}", function() { return require("${n.path}"); });`
- )
- .join("");
+ const distinctNamespaces = new Map();
+ namespaces.forEach(({ name, path }) => distinctNamespaces.set(name, convertSlashesInPath(path)));
+
+ const moduleRegisters: string[] = [];
+ distinctNamespaces.forEach((path, name) => {
+ moduleRegisters.push(`global.registerModule("${name}", function() { return require("${path}"); });\n`);
+ });
// escape special whitespace characters
// see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Issue_with_plain_JSON.stringify_for_use_as_JavaScript
@@ -99,16 +111,16 @@ module.exports = function (source, map) {
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029');
- const wrapped = `${moduleRegisters}\nmodule.exports = ${json}`;
+ const wrapped = `${moduleRegisters.join("")}\nmodule.exports = ${json}`;
- callback(null, wrapped, map);
+ if (!parsingError) {
+ callback(null, wrapped, map);
+ }
}).catch((err) => {
- callback(err);
+ if (!parsingError) {
+ callback(err);
+ }
})
-
}
-function convertPath(obj) {
- obj.path = convertSlashesInPath(obj.path);
- return obj;
-}
+export default loader;
\ No newline at end of file