diff --git a/libs/native-federation-runtime/src/lib/utils/add-import-map.ts b/libs/native-federation-runtime/src/lib/utils/add-import-map.ts index 0b1a78d3..e6f0b946 100644 --- a/libs/native-federation-runtime/src/lib/utils/add-import-map.ts +++ b/libs/native-federation-runtime/src/lib/utils/add-import-map.ts @@ -1,10 +1,11 @@ import { ImportMap } from '../model/import-map'; +import { tryCreateTrustedScript } from './trusted-types'; export function appendImportMap(importMap: ImportMap) { document.head.appendChild( Object.assign(document.createElement('script'), { - type: 'importmap-shim', - innerHTML: JSON.stringify(importMap), + type: tryCreateTrustedScript('importmap-shim'), + textContent: tryCreateTrustedScript(JSON.stringify(importMap)), }), ); } diff --git a/libs/native-federation-runtime/src/lib/utils/trusted-types.spec.ts b/libs/native-federation-runtime/src/lib/utils/trusted-types.spec.ts new file mode 100644 index 00000000..954703ea --- /dev/null +++ b/libs/native-federation-runtime/src/lib/utils/trusted-types.spec.ts @@ -0,0 +1,95 @@ +import { + afterEach, + beforeEach, + describe, + expect, + expectTypeOf, + it, + vi, +} from 'vitest'; + +type TryCreateTrustedScript = (string: string) => string | TrustedScript; + +describe('tryCreateTrustedScript', () => { + let tryCreateTrustedScript: TryCreateTrustedScript | undefined; + + beforeEach(async () => { + // Setup + vi.resetModules(); + const mod = await import('./trusted-types'); + tryCreateTrustedScript = mod.tryCreateTrustedScript; + }); + + afterEach(() => { + // Clean up + delete (globalThis as any).trustedTypes; + vi.resetModules(); + }); + + it('returns input when trustedTypes is undefined', () => { + // Setup + (globalThis as any).trustedTypes = undefined; + const input = 'alert(1)'; + + // Execute + const result = tryCreateTrustedScript!(input); + + // Assert + expectTypeOf(result as string).toExtend(); + expect(result).toBe(input); + }); + + it('returns input when trustedTypes is defined', () => { + // Setup + const input = 'alert(1)'; + + // Execute + const result = tryCreateTrustedScript!(input); + + // Assert + expectTypeOf(result as TrustedScript).toExtend(); + expect(result).toBe(input); + }); + + it('caches policy and only calls createPolicy once', () => { + // Setup + let createPolicyMock = vi.fn(() => ({ + createScript: (s: string) => `CACHE:${s}`, + createHTML: (h: string) => h, + createScriptURL: (u: string) => u, + })); + + (globalThis as any).trustedTypes = { + createPolicy: createPolicyMock, + }; + + // Execute + const result1 = tryCreateTrustedScript!('a=1'); + const result2 = tryCreateTrustedScript!('b=2'); + + // Assert + expect(result1).toBe(`CACHE:a=1`); + expect(result2).toBe(`CACHE:b=2`); + expect(createPolicyMock).toHaveBeenCalledTimes(1); + }); + + it('falls back when createPolicy throws', () => { + // Setup + const createPolicyMock = vi.fn(() => { + throw new Error('policy exists'); + }); + + (globalThis as any).trustedTypes = { + createPolicy: createPolicyMock, + }; + + const input = 'console.log("fallback")'; + // Execute + const result = tryCreateTrustedScript!(input); + + // Assert + expect(typeof result).toBe('string'); + expect(result).toBe(input); + expect(createPolicyMock).toHaveBeenCalled(); + }); +}); diff --git a/libs/native-federation-runtime/src/lib/utils/trusted-types.ts b/libs/native-federation-runtime/src/lib/utils/trusted-types.ts new file mode 100644 index 00000000..f3a8e3c9 --- /dev/null +++ b/libs/native-federation-runtime/src/lib/utils/trusted-types.ts @@ -0,0 +1,30 @@ +import { TrustedTypePolicy, TrustedTypePolicyFactory } from 'trusted-types'; + +const global: any = globalThis; + +let policy: TrustedTypePolicy | null | undefined; + +function createPolicy(): TrustedTypePolicy | null { + if (policy === undefined) { + policy = null; + if (global.trustedTypes) { + try { + policy = (global.trustedTypes as TrustedTypePolicyFactory).createPolicy( + 'native-federation', + { + createHTML: (html: string) => html, + createScript: (script: string) => script, + createScriptURL: (url: string) => url, + }, + ); + } catch { + // trustedTypes.createPolicy may throw an exception if called with a name that is already registered, even in report-only mode. + } + } + } + return policy; +} + +export function tryCreateTrustedScript(script: string): string | TrustedScript { + return createPolicy()?.createScript(script) ?? script; +} diff --git a/package-lock.json b/package-lock.json index f78feec4..7b215447 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,6 +64,7 @@ "@types/browser-sync": "^2.29.0", "@types/jest": "^29.5.0", "@types/node": "^24.9.0", + "@types/trusted-types": "^2.0.7", "@typescript-eslint/eslint-plugin": "^8.46.0", "@typescript-eslint/parser": "^8.46.0", "@typescript-eslint/utils": "^8.46.0", @@ -16652,6 +16653,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/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", diff --git a/package.json b/package.json index 03f21e22..899edf25 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "@types/browser-sync": "^2.29.0", "@types/jest": "^29.5.0", "@types/node": "^24.9.0", + "@types/trusted-types": "^2.0.7", "@typescript-eslint/eslint-plugin": "^8.46.0", "@typescript-eslint/parser": "^8.46.0", "@typescript-eslint/utils": "^8.46.0",