diff --git a/libs/native-federation-runtime/import-shim.d.ts b/libs/native-federation-runtime/import-shim.d.ts new file mode 100644 index 00000000..4b0557cf --- /dev/null +++ b/libs/native-federation-runtime/import-shim.d.ts @@ -0,0 +1,212 @@ +interface ESMSInitOptions { + /** + * Enable Shim Mode + */ + shimMode?: boolean; + + /** + * Enable polyfill features. + * Currently supports ['css-modules', 'json-modules'] + */ + polyfillEnable?: Array<'css-modules' | 'json-modules'> + + /** + * #### Enforce Integrity + * + * Set to *true* to enable secure mode to not support loading modules without integrity (integrity is always verified though). + * + */ + enforceIntegrity?: boolean; + + /** + * Nonce for CSP build + */ + nonce?: boolean; + + /** + * Disable retriggering of document readystate + */ + noLoadEventRetriggers?: true; + + /** + * #### Skip Processing Stability + * + * > Non-spec feature + * + * When loading modules that you know will only use baseline modules + * features, it is possible to set a rule to explicitly opt-out modules + * from rewriting. This improves performance because those modules then do + * not need to be processed or transformed at all, so that only local + * application code is handled and not library code. + * + * This can be configured by setting the importShim.skip URL regular + * expression: + * + * ```js + * importShim.skip = /^https:\/\/cdn\.com/; + * ``` + * + * By default, this expression supports jspm.dev, dev.jspm.io and + * cdn.pika.dev. + */ + skip?: RegExp | string[] | string + + /** + * #### Error hook + * + * Register a callback for any ES Module Shims module errors. + * + */ + onerror?: (e: any) => any; + + /** + * #### Polyfill hook + * + * Register a callback invoked when polyfill mode first engages. + * + */ + onpolyfill?: () => void; + + /** + * #### Resolve Hook + * + * Only supported in Shim Mode. + * + * Provide a custom resolver function. + */ + resolve?: ( + id: string, + parentUrl: string, + resolve: (id: string, parentUrl: string) => string + ) => string | Promise; + + /** + * #### Fetch Hook + * + * Only supported in Shim Mode. + * + * > Stability: Non-spec feature + * + * This is provided as a convenience feature since the pipeline handles + * the same data URL rewriting and circular handling of the module graph + * that applies when trying to implement any module transform system. + * + * The ES Module Shims fetch hook can be used to implement transform + * plugins. + * + * For example: + * + * ```js + * importShim.fetch = async function (url) { + * const response = await fetch(url); + * if (response.url.endsWith('.ts')) { + * const source = await response.body(); + * const transformed = tsCompile(source); + * return new Response(new Blob([transformed], { type: 'application/javascript' })); + * } + * return response; + * }; + * ``` + * + * Because the dependency analysis applies by ES Module Shims takes care + * of ensuring all dependencies run through the same fetch hook, the above + * is all that is needed to implement custom plugins. + * + * Streaming support is also provided, for example here is a hook with + * streaming support for JSON: + * + * ```js + * importShim.fetch = async function (url) { + * const response = await fetch(url); + * if (!response.ok) + * throw new Error(`${response.status} ${response.statusText} ${response.url}`); + * const contentType = response.headers.get('content-type'); + * if (!/^application\/json($|;)/.test(contentType)) + * return response; + * const reader = response.body.getReader(); + * return new Response(new ReadableStream({ + * async start (controller) { + * let done, value; + * controller.enqueue(new Uint8Array([...'export default '].map(c => c.charCodeAt(0)))); + * while (({ done, value } = await reader.read()) && !done) { + * controller.enqueue(value); + * } + * controller.close(); + * } + * }), { + * status: 200, + * headers: { + * "Content-Type": "application/javascript" + * } + * }); + * } + * ``` + */ + fetch?: (input: RequestInfo, init?: RequestInit) => Promise; + + /** + * #### Revoke Blob URLs + * + * Set to *true* to cleanup blob URLs from memory after execution. + * Can cost some compute time for large loads. + * + */ + revokeBlobURLs?: boolean; + + /** + * #### Map Overrides + * + * Set to *true* to permit overrides to import maps. + * + */ + mapOverrides?: boolean; + + /** + * #### Meta hook + * + * Register a callback for import.meta construction. + * + */ + meta?: (meta: any, url: string) => void; + + /** + * #### On import hook + * + * Register a callback for top-level imports. + * + */ + onimport?: (url: string, options: any, parentUrl: string) => void; +} + +interface ImportMap { + imports: Record; + scopes: Record>; +} + +/** + * Dynamic import(...) within any modules loaded will be rewritten as + * importShim(...) automatically providing full support for all es-module-shims + * features through dynamic import. + * + * To load code dynamically (say from the browser console), importShim can be + * called similarly: + * + * ```js + * importShim('/path/to/module.js').then(x => console.log(x)); + * ``` + */ +declare function importShim( + specifier: string, + parentUrl?: string +): Promise<{ default: Default } & Exports>; + +declare namespace importShim { + const resolve: (id: string, parentURL?: string) => string; + const addImportMap: (importMap: Partial) => void; + const getImportMap: () => ImportMap; +} + +interface Window { + esmsInitOptions?: ESMSInitOptions; + importShim: typeof importShim; +} diff --git a/libs/native-federation-runtime/src/lib/init-federation.ts b/libs/native-federation-runtime/src/lib/init-federation.ts index ecd3ba0c..713f259a 100644 --- a/libs/native-federation-runtime/src/lib/init-federation.ts +++ b/libs/native-federation-runtime/src/lib/init-federation.ts @@ -4,11 +4,10 @@ import { ImportMap, mergeImportMaps, } from './model/import-map'; -import { getExternalUrl, setExternalUrl } from './model/externals'; import { joinPaths, getDirectory } from './utils/path-utils'; import { addRemote } from './model/remotes'; -import { appendImportMap } from './utils/add-import-map'; import { FederationInfo } from './model/federation-info'; +import * as semver from 'semver'; export async function initFederation( remotesOrManifestUrl: Record | string = {} @@ -19,12 +18,10 @@ export async function initFederation( : remotesOrManifestUrl; const hostImportMap = await processHostInfo(); + importShim.addImportMap(hostImportMap); const remotesImportMap = await processRemoteInfos(remotes); - - const importMap = mergeImportMaps(hostImportMap, remotesImportMap); - appendImportMap(importMap); - - return importMap; + importShim.addImportMap(remotesImportMap); + return importShim.getImportMap(); } async function loadManifest(remotes: string): Promise> { @@ -92,12 +89,24 @@ function processRemoteImports( ): Scopes { const scopes: Scopes = {}; const scopedImports: Imports = {}; + const importMap = importShim.getImportMap(); for (const shared of remoteInfo.shared) { - const outFileName = - getExternalUrl(shared) ?? joinPaths(baseUrl, shared.outFileName); - setExternalUrl(shared, outFileName); - scopedImports[shared.packageName] = outFileName; + let isImported = false; + if (shared.singleton) { + try { + const importedURL = new URL(importMap.imports?.[shared.packageName]); + const version = importedURL.searchParams.get('version'); + if (version) { + isImported = semver.satisfies(version, shared.requiredVersion); + } + } + catch {} + } + if (!isImported) { + const outFileName = joinPaths(baseUrl, shared.outFileName); + scopedImports[shared.packageName] = outFileName; + } } scopes[baseUrl + '/'] = scopedImports; @@ -124,12 +133,9 @@ async function processHostInfo(): Promise { const hostInfo = await loadFederationInfo('./remoteEntry.json'); const imports = hostInfo.shared.reduce( - (acc, cur) => ({ ...acc, [cur.packageName]: './' + cur.outFileName }), + (acc, cur) => ({ ...acc, [cur.packageName]: cur.version ? `./${cur.outFileName}?version=${cur.version}` : `./${cur.outFileName}` }), {} ) as Imports; - - for (const shared of hostInfo.shared) { - setExternalUrl(shared, './' + shared.outFileName); - } + return { imports, scopes: {} }; } diff --git a/libs/native-federation-runtime/src/lib/load-remote-module.ts b/libs/native-federation-runtime/src/lib/load-remote-module.ts index 333eecaf..73d7231e 100644 --- a/libs/native-federation-runtime/src/lib/load-remote-module.ts +++ b/libs/native-federation-runtime/src/lib/load-remote-module.ts @@ -1,5 +1,4 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { appendImportMap } from './utils/add-import-map'; import { processRemoteInfo } from './init-federation'; import { getRemote, @@ -8,22 +7,20 @@ import { } from './model/remotes'; import { getDirectory, joinPaths } from './utils/path-utils'; -declare function importShim(url: string): T; - export type LoadRemoteModuleOptions = { remoteEntry?: string; remoteName?: string; exposedModule: string; }; -export async function loadRemoteModule( +export async function loadRemoteModule( options: LoadRemoteModuleOptions ): Promise; -export async function loadRemoteModule( +export async function loadRemoteModule( remoteName: string, exposedModule: string ): Promise; -export async function loadRemoteModule( +export async function loadRemoteModule( optionsOrRemoteName: LoadRemoteModuleOptions | string, exposedModule?: string ): Promise { @@ -47,7 +44,7 @@ export async function loadRemoteModule( } const url = joinPaths(remote.baseUrl, exposed.outFileName); - const module = await importShim(url); + const module = await importShim(url); return module; } @@ -80,7 +77,7 @@ async function ensureRemoteInitialized( !isRemoteInitialized(getDirectory(options.remoteEntry)) ) { const importMap = await processRemoteInfo(options.remoteEntry); - appendImportMap(importMap); + importShim.addImportMap(importMap); } } diff --git a/libs/native-federation-runtime/src/lib/model/externals.ts b/libs/native-federation-runtime/src/lib/model/externals.ts deleted file mode 100644 index 53754322..00000000 --- a/libs/native-federation-runtime/src/lib/model/externals.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { SharedInfo } from './federation-info'; - -const externals = new Map(); - -function getExternalKey(shared: SharedInfo) { - return `${shared.packageName}@${shared.version}`; -} - -export function getExternalUrl(shared: SharedInfo): string | undefined { - const packageKey = getExternalKey(shared); - return externals.get(packageKey); -} - -export function setExternalUrl(shared: SharedInfo, url: string): void { - const packageKey = getExternalKey(shared); - externals.set(packageKey, url); -} 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 deleted file mode 100644 index 2f44f133..00000000 --- a/libs/native-federation-runtime/src/lib/utils/add-import-map.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ImportMap } from '../model/import-map'; - -export function appendImportMap(importMap: ImportMap) { - document.body.appendChild( - Object.assign(document.createElement('script'), { - type: 'importmap-shim', - innerHTML: JSON.stringify(importMap), - }) - ); -} diff --git a/package-lock.json b/package-lock.json index ac60a8d3..459a72dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "npmlog": "^6.0.2", "rollup-plugin-esbuild": "^5.0.0", "rxjs": "^7.0.0", - "semver": "^7.3.5", + "semver": "^7.5.4", "tslib": "^2.0.0", "word-wrap": "^1.2.5", "zone.js": "0.14.2" @@ -66,6 +66,7 @@ "@types/jest": "29.4.4", "@types/node": "18.7.1", "@types/npmlog": "^4.1.4", + "@types/semver": "^7.5.6", "@typescript-eslint/eslint-plugin": "6.9.1", "@typescript-eslint/parser": "6.9.1", "browser-sync": "^2.29.3", @@ -7367,9 +7368,9 @@ "dev": true }, "node_modules/@types/semver": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.5.tgz", - "integrity": "sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, "node_modules/@types/send": { diff --git a/package.json b/package.json index 62558176..13c43b07 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "npmlog": "^6.0.2", "rollup-plugin-esbuild": "^5.0.0", "rxjs": "^7.0.0", - "semver": "^7.3.5", + "semver": "^7.5.4", "tslib": "^2.0.0", "word-wrap": "^1.2.5", "zone.js": "0.14.2" @@ -68,6 +68,7 @@ "@nx/angular": "17.1.1", "@nx/cypress": "17.1.1", "@nx/devkit": "17.1.1", + "@nx/eslint": "17.1.1", "@nx/eslint-plugin": "17.1.1", "@nx/jest": "17.1.1", "@nx/js": "17.1.1", @@ -86,6 +87,7 @@ "@types/jest": "29.4.4", "@types/node": "18.7.1", "@types/npmlog": "^4.1.4", + "@types/semver": "^7.5.6", "@typescript-eslint/eslint-plugin": "6.9.1", "@typescript-eslint/parser": "6.9.1", "browser-sync": "^2.29.3", @@ -119,8 +121,7 @@ "ts-node": "10.9.1", "tslib": "^2.3.0", "typescript": "~5.2.0", - "verdaccio": "^5.0.4", - "@nx/eslint": "17.1.1" + "verdaccio": "^5.0.4" }, "nx": { "includedScripts": []