diff --git a/README.md b/README.md index aec87eed..4a8adabd 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,28 @@ The following libraries are available: - `addressValidation`: [`google.maps.AddressValidationLibrary`](https://developers.google.com/maps/documentation/javascript/reference/library-interfaces#AddressValidationLibrary) - `drawing`: [`google.maps.DrawingLibrary`](https://developers.google.com/maps/documentation/javascript/reference/library-interfaces#DrawingLibrary) (deprecated) +### Content Security Policy and Trusted Types + +The loader supports pages that enforce +[`require-trusted-types-for 'script'`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/require-trusted-types-for). +When Trusted Types are available, the loader creates a policy named +`@googlemaps/js-api-loader` and uses it to assign the Google Maps JavaScript API +script URL. + +If your page uses a `trusted-types` CSP directive, allow this policy name: + +```http +Content-Security-Policy: require-trusted-types-for 'script'; trusted-types @googlemaps/js-api-loader google-maps-api-loader google-maps-api#html lit-html +``` + +`@googlemaps/js-api-loader` is used by this package. `google-maps-api-loader` +`google-maps-api#html`, and `lit-html` are used by the Maps JavaScript API +script internally during execution. + +If the policy name is not allowed, the loader logs a development warning and +falls back to assigning a string URL. On pages that enforce +`require-trusted-types-for 'script'`, the browser will block that fallback. + ## Migrating from v1 to v2 See the [migration guide](MIGRATION.md). diff --git a/eslint.config.js b/eslint.config.js index 68bfc6e9..0d985232 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -28,6 +28,14 @@ export default defineConfig( "@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/no-empty-function": "warn", "@typescript-eslint/member-ordering": "warn", + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], "@typescript-eslint/explicit-member-accessibility": [ "warn", { diff --git a/package-lock.json b/package-lock.json index f7b13cf9..38eaf3db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@rollup/plugin-terser": "^1.0.0", "@rollup/plugin-typescript": "^12.1.0", "@types/jest": "^30.0.0", + "@types/trusted-types": "^2.0.7", "@typescript-eslint/eslint-plugin": "^8.42.0", "@typescript-eslint/parser": "^8.42.0", "core-js": "^3.6.4", @@ -35,7 +36,7 @@ "prettier": "^3.0.3", "rollup": "^4.6.1", "rollup-plugin-dts": "^6.2.3", - "ts-jest": "^29.1.1", + "ts-jest": "^29.4.6", "tslib": "^2.8.1", "typescript": "^6.0.3", "typescript-eslint": "^8.42.0" @@ -3436,6 +3437,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", diff --git a/package.json b/package.json index 29d087fc..4d8b97e7 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "scripts": { "prepack": "npm run build", "lint": "eslint .", - "test": "npm run lint && npm run test:unit && npm run test:bundlers", + "typecheck": "tsc --noEmit", + "test": "npm run lint && npm run typecheck && npm run test:unit && npm run test:bundlers", "test:unit": "NODE_OPTIONS='--experimental-vm-modules --disable-warning=ExperimentalWarning' jest ./src", "test:bundlers": "cd test-bundlers && ./test-all.sh", "build": "rm -rf ./dist && rollup -c", @@ -53,6 +54,7 @@ "@rollup/plugin-terser": "^1.0.0", "@rollup/plugin-typescript": "^12.1.0", "@types/jest": "^30.0.0", + "@types/trusted-types": "^2.0.7", "@typescript-eslint/eslint-plugin": "^8.42.0", "@typescript-eslint/parser": "^8.42.0", "core-js": "^3.6.4", @@ -67,7 +69,7 @@ "prettier": "^3.0.3", "rollup": "^4.6.1", "rollup-plugin-dts": "^6.2.3", - "ts-jest": "^29.1.1", + "ts-jest": "^29.4.6", "tslib": "^2.8.1", "typescript": "^6.0.3", "typescript-eslint": "^8.42.0" diff --git a/src/index.test.ts b/src/index.test.ts index 348de249..49b8ea3b 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -49,7 +49,7 @@ beforeEach(() => { jest.resetModules(); jest.clearAllMocks(); - delete globalThis.google; + delete (globalThis as { google?: unknown }).google; }); describe("importLibrary(): basic operation", () => { diff --git a/src/messages.ts b/src/messages.ts index 341f7529..44fb2072 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -48,6 +48,13 @@ export const MSG_API_KEY_USED = "The 'apiKey' parameter was used in setOptions(), but 'key' is the correct " + "parameter name. Please update your configuration."; +export const MSG_TRUSTED_TYPES_POLICY_FAILED = (policyName: string, error: unknown) => + `Failed to create Trusted Types policy "${policyName}": ${error instanceof Error ? error.message : String(error)}.\n\n` + + `If your Content Security Policy uses "require-trusted-types-for 'script'", ` + + `allow this policy with "trusted-types ${policyName} google-maps-api-loader google-maps-api#html lit-html". ` + + `The "google-maps-api-loader", "lit-html", and "google-maps-api#html" policies are required for full Maps JavaScript API execution. ` + + `Falling back to a string script URL.`; + // Development mode check - bundlers will replace process.env.NODE_ENV at build time declare const process: { env: { NODE_ENV?: string } }; const __DEV__ = process.env.NODE_ENV !== 'production'; diff --git a/src/setScriptSrc.ts b/src/setScriptSrc.ts index f8956e6c..52000262 100644 --- a/src/setScriptSrc.ts +++ b/src/setScriptSrc.ts @@ -3,6 +3,40 @@ * SPDX-License-Identifier: Apache-2.0 */ -export function setScriptSrc(script: HTMLScriptElement, src: string) { - script.src = src; +import type { TrustedTypePolicyFactory } from "trusted-types"; + +import { logDevWarning, MSG_TRUSTED_TYPES_POLICY_FAILED } from "./messages.js"; + +const TRUSTED_TYPES_POLICY_NAME = "@googlemaps/js-api-loader"; +type TrustedTypesWindow = Window & { + trustedTypes?: TrustedTypePolicyFactory; +}; + +// Try to create a Trusted Types policy when supported. Falls back to a string +// passthrough when Trusted Types is unsupported, blocked by CSP, or already +// registered. + +let policy: { + createScriptURL: (url: string) => string | TrustedScriptURL; +}; + +const trustedTypes = (window as TrustedTypesWindow).trustedTypes; + +if (!trustedTypes) { + policy = { createScriptURL: (url: string) => url }; +} else { + try { + policy = trustedTypes.createPolicy(TRUSTED_TYPES_POLICY_NAME, { + createScriptURL: (url: string) => url, + }); + } catch (e) { + logDevWarning( + MSG_TRUSTED_TYPES_POLICY_FAILED(TRUSTED_TYPES_POLICY_NAME, e) + ); + policy = { createScriptURL: (url: string) => url }; + } +} + +export function setScriptSrc(script: HTMLScriptElement, src: string): void { + script.src = policy.createScriptURL(src) as string; } diff --git a/test-bundlers/test-all.sh b/test-bundlers/test-all.sh index e424d7b3..2a5ac68a 100755 --- a/test-bundlers/test-all.sh +++ b/test-bundlers/test-all.sh @@ -29,13 +29,12 @@ echo "" for bundler in vite webpack rollup ; do dir="$SCRIPT_DIR/${bundler}-test" + echo " Installing dependencies for $bundler..." ( - cd $dir - # Install dependencies - echo " Installing dependencies for $bundler..." - npm install --silent - npm install --silent --no-save "../../$TARBALL" - ) + cd "$dir" || exit 1 + npm install --silent || { echo "Failed to install dependencies for $bundler"; exit 1; } + npm install --silent --no-save "../../$TARBALL" || { echo "Failed to install tarball for $bundler"; exit 1; } + ) || exit 1 done echo "" @@ -131,8 +130,25 @@ echo "" echo "======================================" if [ $NUM_FAILED -eq 0 ]; then echo -e "${GREEN}โœ“ All bundler tests passed!${NC}" - exit 0 else echo -e "${RED}โœ— $NUM_FAILED bundler test(s) failed${NC}" +fi + +# Cleanup +echo "" +echo "๐Ÿงน Cleaning up..." +for bundler in vite webpack rollup ; do + dir="$SCRIPT_DIR/${bundler}-test" + echo " Cleaning $bundler-test..." + (cd "$dir" && git clean -qfdx) +done + +cd "$ROOT_DIR" +rm -f "$TARBALL" +echo " Removed $TARBALL" + +if [ $NUM_FAILED -eq 0 ]; then + exit 0 +else exit 1 fi diff --git a/tsconfig.json b/tsconfig.json index 52fef94f..a5c210e3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,12 +5,14 @@ "sourceMap": true, "esModuleInterop": true, "isolatedModules": true, + "skipLibCheck": true, "lib": ["DOM", "DOM.Iterable", "ESNext"], "target": "es2020", "module": "NodeNext", "moduleResolution": "NodeNext", - "types": ["google.maps", "jest"] + "typeRoots": ["./node_modules/@types"], + "types": ["google.maps", "jest", "trusted-types"] }, - "include": ["src/**/*", "e2e/**/*.ts"], + "include": ["src/**/*"], "exclude": ["node_modules", "./dist"] }