diff --git a/.gitignore b/.gitignore index 8f8de90d..e7cdd7db 100644 --- a/.gitignore +++ b/.gitignore @@ -31,10 +31,14 @@ 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* hooks .DS_Store - +.nyc_output +coverage !projectHelpers.spec.js diff --git a/.npmignore b/.npmignore index 1c2c46d5..58343b36 100644 --- a/.npmignore +++ b/.npmignore @@ -7,6 +7,8 @@ demo *.spec.* .vscode/ .github/ +.nyc_output +coverage/ jasmine-config/ CONTRIBUTING.md CODE_OF_CONDUCT.md diff --git a/.nycrc b/.nycrc new file mode 100644 index 00000000..3294892a --- /dev/null +++ b/.nycrc @@ -0,0 +1,5 @@ +{ + "extends": "@istanbuljs/nyc-config-typescript", + "exclude": ["/demo/**"], + "reporter": ["text", "lcov"] +} \ No newline at end of file diff --git a/jasmine-config/jasmine.json b/jasmine-config/jasmine.json index 8d3ecdc5..3d06fa01 100644 --- a/jasmine-config/jasmine.json +++ b/jasmine-config/jasmine.json @@ -3,7 +3,7 @@ "spec_files": [ "!node_modules/**/*.spec.js", "!demo/**/*.spec.js", - "./*.spec.js" + "./**/*.spec.js" ], "helpers": [ "jasmine-config/**/*.js" diff --git a/package.json b/package.json index 5b7ff3e6..2b04ea93 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "prepare": "npm run tsc && npm run jasmine", "test": "npm run prepare", "jasmine": "jasmine --config=jasmine-config/jasmine.json", + "coverage": "nyc npm run test", "version": "rm package-lock.json && conventional-changelog -p angular -i CHANGELOG.md -s && git add CHANGELOG.md" }, "bin": { @@ -62,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", @@ -77,17 +79,21 @@ "devDependencies": { "@angular/compiler": "8.2.0", "@angular/compiler-cli": "8.2.0", + "@istanbuljs/nyc-config-typescript": "^0.1.3", "@ngtools/webpack": "8.2.0", "@types/jasmine": "^3.3.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", "jasmine": "^3.2.0", "jasmine-spec-reporter": "^4.2.1", + "nyc": "^14.1.1", "proxyquire": "2.1.0", + "source-map-support": "^0.5.13", "tns-core-modules": "next", "typescript": "~3.5.3" } diff --git a/templates/webpack.config.spec.ts b/templates/webpack.config.spec.ts index 024461bd..e8ae3335 100644 --- a/templates/webpack.config.spec.ts +++ b/templates/webpack.config.spec.ts @@ -32,7 +32,9 @@ const nativeScriptDevWebpack = { getEntryModule: () => 'EntryModule', getResolver: () => null, getConvertedExternals: nsWebpackIndex.getConvertedExternals, - getSourceMapFilename: nsWebpackIndex.getSourceMapFilename + getSourceMapFilename: nsWebpackIndex.getSourceMapFilename, + processAppComponents: nsWebpackIndex.processAppComponents, + getUserDefinedEntries: nsWebpackIndex.getUserDefinedEntries, }; const emptyObject = {}; diff --git a/xml-namespace-loader.spec.ts b/xml-namespace-loader.spec.ts new file mode 100644 index 00000000..28d12583 --- /dev/null +++ b/xml-namespace-loader.spec.ts @@ -0,0 +1,223 @@ +import xmlNsLoader from "./xml-namespace-loader"; + +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) => { + // console.log(`Resolve request: ${request}, result: ${resolveMap[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..de07e734 100644 --- a/xml-namespace-loader.js +++ b/xml-namespace-loader.ts @@ -1,22 +1,27 @@ -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) { +import { convertSlashesInPath } from "./projectHelpers"; + +interface NamespaceEntry { + name: string; + path: string +} + +const loader: loader.Loader = function (source: string, map) { this.value = source; 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 +60,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 +86,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 +112,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