Skip to content
This repository was archived by the owner on Aug 7, 2021. It is now read-only.

refactor: remove tns-core-modules/xml dependency #1047

Merged
merged 6 commits into from
Sep 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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*

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
224 changes: 224 additions & 0 deletions xml-namespace-loader.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import xmlNsLoader from "./xml-namespace-loader";
import { convertSlashesInPath } from "./projectHelpers";

const CODE_FILE = `
<Page xmlns="http://www.nativescript.org/tns.xsd">
<StackLayout>
<GridLayout xmlns:chart="nativescript-ui-chart">
<chart:RadCartesianChart></chart:RadCartesianChart>
</GridLayout>
<GridLayout xmlns:chart="nativescript-ui-chart">
<chart:RadCartesianChart></chart:RadCartesianChart>
</GridLayout>
</StackLayout>
</Page>
`;

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 = `
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<!-- comment.xml -->
<Page xmlns="http://www.nativescript.org/tns.xsd"></Page>`;

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 = `<Page xmlns="http://www.nativescript.org/tns.xsd"></PageOpsWrongTagHere>`;

const loaderContext = getContext(done, { resolveMap, expectedDeps, expectedRegs, expectError: true });

xmlNsLoader.call(loaderContext, testXml);
})
});
70 changes: 41 additions & 29 deletions xml-namespace-loader.js → xml-namespace-loader.ts
Original file line number Diff line number Diff line change
@@ -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<any>[] = [];
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") &&
Expand Down Expand Up @@ -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([
Expand All @@ -81,34 +85,42 @@ 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<string, string>();
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
const json = JSON.stringify(source)
.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;