diff --git a/examples/index.html b/examples/index.html index dfa8abb5..7fa9d7dd 100644 --- a/examples/index.html +++ b/examples/index.html @@ -36,7 +36,7 @@ diff --git a/package-lock.json b/package-lock.json index aff95591..c64ceda2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3804,6 +3804,32 @@ "fastq": "^1.6.0" } }, + "@rollup/plugin-node-resolve": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-10.0.0.tgz", + "integrity": "sha512-sNijGta8fqzwA1VwUEtTvWCx2E7qC70NMsDh4ZG13byAXYigBNZMxALhKUSycBks5gupJdq0lFrKumFrRZ8H3A==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "builtin-modules": "^3.1.0", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.17.0" + }, + "dependencies": { + "resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "dev": true, + "requires": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + } + } + } + }, "@rollup/pluginutils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", @@ -3963,6 +3989,12 @@ "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==", "dev": true }, + "@types/lodash": { + "version": "4.14.165", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.165.tgz", + "integrity": "sha512-tjSSOTHhI5mCHTy/OOXYIhi2Wt1qcbHmuXD1Ha7q70CgI/I71afO4XtLb/cVexki1oVYchpul/TOuu3Arcdxrg==", + "dev": true + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -3987,6 +4019,15 @@ "integrity": "sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ==", "dev": true }, + "@types/resolve": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", + "integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/selenium-webdriver": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-4.0.10.tgz", @@ -4736,6 +4777,12 @@ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true }, + "builtin-modules": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz", + "integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==", + "dev": true + }, "cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", @@ -6500,6 +6547,15 @@ "ci-info": "^2.0.0" } }, + "is-core-module": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.1.0.tgz", + "integrity": "sha512-YcV7BgVMRFRua2FqQzKtTDMz8iCuLEyGKjr70q8Zm1yy2qKcurbFEd79PAdHV77oL3NrAaOVQIbMmiHQCHB7ZA==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, "is-data-descriptor": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", @@ -6585,6 +6641,12 @@ "is-extglob": "^2.1.1" } }, + "is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", + "dev": true + }, "is-negative-zero": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz", @@ -7012,17 +7074,45 @@ } }, "jest-diff": { - "version": "26.6.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.1.tgz", - "integrity": "sha512-BBNy/zin2m4kG5In126O8chOBxLLS/XMTuuM2+YhgyHk87ewPzKTuTJcqj3lOWOi03NNgrl+DkMeV/exdvG9gg==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", + "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", "dev": true, "requires": { "chalk": "^4.0.0", - "diff-sequences": "^26.5.0", + "diff-sequences": "^26.6.2", "jest-get-type": "^26.3.0", - "pretty-format": "^26.6.1" + "pretty-format": "^26.6.2" }, "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, "chalk": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", @@ -7034,9 +7124,9 @@ } }, "diff-sequences": { - "version": "26.5.0", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.5.0.tgz", - "integrity": "sha512-ZXx86srb/iYy6jG71k++wBN9P9J05UNQ5hQHQd9MtMPvcqXPx/vKU69jfHV637D00Q2gSgPk2D+jSx3l1lDW/Q==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", + "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", "dev": true }, "jest-get-type": { @@ -7044,6 +7134,24 @@ "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", "dev": true + }, + "pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + } + }, + "react-is": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz", + "integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==", + "dev": true } } }, @@ -7996,10 +8104,9 @@ } }, "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", - "dev": true + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" }, "lodash.memoize": { "version": "4.1.2", diff --git a/package.json b/package.json index 2789571d..d0da1e56 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,10 @@ "devDependencies": { "@babel/preset-env": "^7.9.5", "@babel/runtime-corejs3": "^7.9.2", + "@rollup/plugin-node-resolve": "^10.0.0", "@types/googlemaps": "^3.39.3", "@types/jest": "^26.0.10", + "@types/lodash": "^4.14.165", "@types/selenium-webdriver": "^4.0.9", "@typescript-eslint/eslint-plugin": ">=2.25.0", "@typescript-eslint/parser": ">=2.25.0", @@ -56,5 +58,8 @@ "publishConfig": { "access": "public", "registry": "https://wombat-dressing-room.appspot.com" + }, + "dependencies": { + "lodash": "^4.17.20" } } diff --git a/rollup.config.js b/rollup.config.js index 5756fbd7..a4fb9cb1 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -16,6 +16,7 @@ import babel from "rollup-plugin-babel"; import commonjs from "rollup-plugin-commonjs"; +import { nodeResolve } from "@rollup/plugin-node-resolve"; import { terser } from "rollup-plugin-terser"; import typescript from "rollup-plugin-typescript2"; @@ -25,11 +26,16 @@ const babelOptions = { const terserOptions = { output: { comments: "" } }; +const resolveOptions = { + mainFields: ["browser", "jsnext:main", "module", "main"], +}; + export default [ { input: "src/index.ts", plugins: [ typescript(), + nodeResolve(resolveOptions), commonjs(), babel(babelOptions), terser(terserOptions), @@ -45,6 +51,7 @@ export default [ input: "src/index.ts", plugins: [ typescript(), + nodeResolve(resolveOptions), commonjs(), babel(babelOptions), terser(terserOptions), @@ -57,7 +64,12 @@ export default [ }, { input: "src/index.ts", - plugins: [typescript(), commonjs(), babel(babelOptions)], + plugins: [ + typescript(), + nodeResolve(resolveOptions), + commonjs(), + babel(babelOptions), + ], output: { file: "dist/index.dev.js", format: "iife", @@ -66,7 +78,7 @@ export default [ }, { input: "src/index.ts", - plugins: [typescript()], + plugins: [typescript(), nodeResolve(resolveOptions), commonjs()], output: { file: "dist/index.esm.js", format: "esm", diff --git a/src/index.test.ts b/src/index.test.ts index a63faff3..bb9c7b98 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -18,6 +18,7 @@ import { Loader, LoaderOptions } from "."; afterEach(() => { document.getElementsByTagName("html")[0].innerHTML = ""; + delete Loader["instance"]; }); test.each([ @@ -108,20 +109,50 @@ test("loadCallback callback should fire", () => { window.__googleMapsCallback(null); }); -test("script onerror should reject promise", () => { +test("script onerror should reject promise", async () => { const loader = new Loader({ apiKey: "foo" }); - expect.assertions(3); + const rejection = expect(loader.load()).rejects.toBeInstanceOf(ErrorEvent); - const promise = loader.load().catch((e) => { - expect(e).toBeTruthy(); - expect(loader["done"]).toBeTruthy(); - expect(loader["loading"]).toBeFalsy(); - }); + loader["loadErrorCallback"](document.createEvent("ErrorEvent")); + + await rejection; + expect(loader["done"]).toBeTruthy(); + expect(loader["loading"]).toBeFalsy(); +}); + +test("script onerror should reject promise with multiple loaders", async () => { + const loader = new Loader({ apiKey: "foo" }); + const extraLoader = new Loader({ apiKey: "foo" }); + let rejection = expect(loader.load()).rejects.toBeInstanceOf(ErrorEvent); loader["loadErrorCallback"](document.createEvent("ErrorEvent")); - return promise; + await rejection; + expect(loader["done"]).toBeTruthy(); + expect(loader["loading"]).toBeFalsy(); + expect(loader["onerrorEvent"]).toBeInstanceOf(ErrorEvent); + rejection = expect(extraLoader.load()).rejects.toBeInstanceOf(ErrorEvent); + + await rejection; + expect(extraLoader["done"]).toBeTruthy(); + expect(extraLoader["loading"]).toBeFalsy(); +}); + +test("singleton should be used", () => { + const loader = new Loader({ apiKey: "foo" }); + const extraLoader = new Loader({ apiKey: "foo" }); + expect(extraLoader).toBe(loader); + + loader["done"] = true; + expect(extraLoader["done"]).toBe(loader["done"]); +}); + +test("singleton should throw with different options", () => { + new Loader({ apiKey: "foo" }); + expect(() => { + new Loader({ apiKey: "bar" }); + }).toThrowError(); }); test("loader should resolve immediately when successfully loaded", async () => { diff --git a/src/index.ts b/src/index.ts index 7407b5bf..c01fe598 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import isEqual from "lodash/fp/isEqual"; + /** * @ignore */ @@ -42,7 +44,7 @@ export interface LoaderOptions { */ apiKey: string; /** - * @deprecated See https://developers.google.com/maps/premium/overview. + * @deprecated See https://developers.google.com/maps/premium/overview. */ channel?: string; /** @@ -231,6 +233,7 @@ export class Loader { private done = false; private loading = false; private onerrorEvent: Event; + private static instance: Loader; /** * Creates an instance of Loader using [[LoaderOptions]]. No defaults are set @@ -265,6 +268,36 @@ export class Loader { this.mapIds = mapIds; this.nonce = nonce; this.url = url; + + if (Loader.instance) { + if (!isEqual(this.options, Loader.instance.options)) { + throw new Error( + `Loader must not be called again with different options. ${JSON.stringify( + this.options + )} !== ${JSON.stringify(Loader.instance.options)}` + ); + } + + return Loader.instance; + } + + Loader.instance = this; + } + + get options(): LoaderOptions { + return { + version: this.version, + apiKey: this.apiKey, + channel: this.channel, + client: this.client, + id: this.id, + libraries: this.libraries, + language: this.language, + region: this.region, + mapIds: this.mapIds, + nonce: this.nonce, + url: this.url, + }; } /** * CreateUrl returns the Google Maps JavaScript API script url given the [[LoaderOptions]]. @@ -348,6 +381,7 @@ export class Loader { */ private setScript(): void { if (this.id && document.getElementById(this.id)) { + // TODO wrap onerror callback for cases where the script was loaded elsewhere this.callback(); return; } diff --git a/tsconfig.json b/tsconfig.json index 5b210ceb..627d3dcb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,9 @@ "outDir": "./dist", "sourceMap": true, "esModuleInterop": true, - "lib": ["DOM", "ESNext"] + "lib": ["DOM", "ESNext"], + "target": "ES6", + "moduleResolution": "node" }, "include": ["src/**/*"] }