/** * 参考 https://github.com/jspm/vite-plugin-jspm,调整为需要的功能 */ import type { GeneratorOptions } from '@jspm/generator'; import type { Plugin } from 'vite'; import { Generator } from '@jspm/generator'; import { load } from 'cheerio'; import { minify } from 'html-minifier-terser'; const DEFAULT_PROVIDER = 'jspm.io'; type pluginOptions = GeneratorOptions & { debug?: boolean; defaultProvider?: 'esm.sh' | 'jsdelivr' | 'jspm.io'; importmap?: Array<{ name: string; range?: string }>; }; // async function getLatestVersionOfShims() { // const result = await fetch('https://ga.jspm.io/npm:es-module-shims'); // const version = result.text(); // return version; // } async function getShimsUrl(provide: string) { // const version = await getLatestVersionOfShims(); const version = '1.10.0'; const shimsSubpath = `dist/es-module-shims.js`; const providerShimsMap: Record = { 'esm.sh': `https://esm.sh/es-module-shims@${version}/${shimsSubpath}`, // unpkg: `https://unpkg.com/es-module-shims@${version}/${shimsSubpath}`, jsdelivr: `https://cdn.jsdelivr.net/npm/es-module-shims@${version}/${shimsSubpath}`, // 下面两个CDN不稳定,暂时不用 'jspm.io': `https://ga.jspm.io/npm:es-module-shims@${version}/${shimsSubpath}`, }; return providerShimsMap[provide] || providerShimsMap[DEFAULT_PROVIDER]; } let generator: Generator; async function viteImportMapPlugin( pluginOptions?: pluginOptions, ): Promise { const { importmap } = pluginOptions || {}; let isSSR = false; let isBuild = false; let installed = false; let installError: Error | null = null; const options: pluginOptions = Object.assign( {}, { debug: false, defaultProvider: 'jspm.io', env: ['production', 'browser', 'module'], importmap: [], }, pluginOptions, ); generator = new Generator({ ...options, baseUrl: process.cwd(), }); if (options?.debug) { (async () => { for await (const { message, type } of generator.logStream()) { console.log(`${type}: ${message}`); } })(); } const imports = options.inputMap?.imports ?? {}; const scopes = options.inputMap?.scopes ?? {}; const firstLayerKeys = Object.keys(scopes); const inputMapScopes: string[] = []; firstLayerKeys.forEach((key) => { inputMapScopes.push(...Object.keys(scopes[key] || {})); }); const inputMapImports = Object.keys(imports); const allDepNames: string[] = [ ...(importmap?.map((item) => item.name) || []), ...inputMapImports, ...inputMapScopes, ]; const depNames = new Set(allDepNames); const installDeps = importmap?.map((item) => ({ range: item.range, target: item.name, })); return [ { async config(_, { command, isSsrBuild }) { isBuild = command === 'build'; isSSR = !!isSsrBuild; }, enforce: 'pre', name: 'importmap:external', resolveId(id) { if (isSSR || !isBuild) { return null; } if (!depNames.has(id)) { return null; } return { external: true, id }; }, }, { enforce: 'post', name: 'importmap:install', async resolveId() { if (isSSR || !isBuild || installed) { return null; } try { installed = true; await Promise.allSettled( (installDeps || []).map((dep) => generator.install(dep)), ); } catch (error: any) { installError = error; installed = false; } return null; }, }, { buildEnd() { // 未生成importmap时,抛出错误,防止被turbo缓存 if (!installed && !isSSR) { installError && console.error(installError); throw new Error('Importmap installation failed.'); } }, enforce: 'post', name: 'importmap:html', transformIndexHtml: { async handler(html) { if (isSSR || !isBuild) { return html; } const importmapJson = generator.getMap(); if (!importmapJson) { return html; } const esModuleShimsSrc = await getShimsUrl( options.defaultProvider || DEFAULT_PROVIDER, ); const resultHtml = await injectShimsToHtml( html, esModuleShimsSrc || '', ); html = await minify(resultHtml || html, { collapseWhitespace: true, minifyCSS: true, minifyJS: true, removeComments: false, }); return { html, tags: [ { attrs: { type: 'importmap', }, injectTo: 'head-prepend', tag: 'script', children: `${JSON.stringify(importmapJson)}`, }, ], }; }, order: 'post', }, }, ]; } async function injectShimsToHtml(html: string, esModuleShimUrl: string) { const $ = load(html); const $script = $(`script[type='module']`); if (!$script) { return; } const entry = $script.attr('src'); $script.removeAttr('type'); $script.removeAttr('crossorigin'); $script.removeAttr('src'); $script.html(` if (!HTMLScriptElement.supports || !HTMLScriptElement.supports('importmap')) { self.importShim = function () { const promise = new Promise((resolve, reject) => { document.head.appendChild( Object.assign(document.createElement('script'), { src: '${esModuleShimUrl}', crossorigin: 'anonymous', async: true, onload() { if (!importShim.$proxy) { resolve(importShim); } else { reject(new Error('No globalThis.importShim found:' + esModuleShimUrl)); } }, onerror(error) { reject(error); }, }), ); }); importShim.$proxy = true; return promise.then((importShim) => importShim(...arguments)); }; } var modules = ['${entry}']; typeof importShim === 'function' ? modules.forEach((moduleName) => importShim(moduleName)) : modules.forEach((moduleName) => import(moduleName)); `); $('body').after($script); $('head').remove(`script[type='module']`); return $.html(); } export { viteImportMapPlugin };