importmap.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. /**
  2. * 参考 https://github.com/jspm/vite-plugin-jspm,调整为需要的功能
  3. */
  4. import type { GeneratorOptions } from '@jspm/generator';
  5. import type { Plugin } from 'vite';
  6. import { Generator } from '@jspm/generator';
  7. import { load } from 'cheerio';
  8. import { minify } from 'html-minifier-terser';
  9. const DEFAULT_PROVIDER = 'jspm.io';
  10. type pluginOptions = GeneratorOptions & {
  11. debug?: boolean;
  12. defaultProvider?: 'esm.sh' | 'jsdelivr' | 'jspm.io';
  13. importmap?: Array<{ name: string; range?: string }>;
  14. };
  15. // async function getLatestVersionOfShims() {
  16. // const result = await fetch('https://ga.jspm.io/npm:es-module-shims');
  17. // const version = result.text();
  18. // return version;
  19. // }
  20. async function getShimsUrl(provide: string) {
  21. // const version = await getLatestVersionOfShims();
  22. const version = '1.10.0';
  23. const shimsSubpath = `dist/es-module-shims.js`;
  24. const providerShimsMap: Record<string, string> = {
  25. 'esm.sh': `https://esm.sh/es-module-shims@${version}/${shimsSubpath}`,
  26. // unpkg: `https://unpkg.com/es-module-shims@${version}/${shimsSubpath}`,
  27. jsdelivr: `https://cdn.jsdelivr.net/npm/es-module-shims@${version}/${shimsSubpath}`,
  28. // 下面两个CDN不稳定,暂时不用
  29. 'jspm.io': `https://ga.jspm.io/npm:es-module-shims@${version}/${shimsSubpath}`,
  30. };
  31. return providerShimsMap[provide] || providerShimsMap[DEFAULT_PROVIDER];
  32. }
  33. let generator: Generator;
  34. async function viteImportMapPlugin(
  35. pluginOptions?: pluginOptions,
  36. ): Promise<Plugin[]> {
  37. const { importmap } = pluginOptions || {};
  38. let isSSR = false;
  39. let isBuild = false;
  40. let installed = false;
  41. let installError: Error | null = null;
  42. const options: pluginOptions = Object.assign(
  43. {},
  44. {
  45. debug: false,
  46. defaultProvider: 'jspm.io',
  47. env: ['production', 'browser', 'module'],
  48. importmap: [],
  49. },
  50. pluginOptions,
  51. );
  52. generator = new Generator({
  53. ...options,
  54. baseUrl: process.cwd(),
  55. });
  56. if (options?.debug) {
  57. (async () => {
  58. for await (const { message, type } of generator.logStream()) {
  59. console.log(`${type}: ${message}`);
  60. }
  61. })();
  62. }
  63. const imports = options.inputMap?.imports ?? {};
  64. const scopes = options.inputMap?.scopes ?? {};
  65. const firstLayerKeys = Object.keys(scopes);
  66. const inputMapScopes: string[] = [];
  67. firstLayerKeys.forEach((key) => {
  68. inputMapScopes.push(...Object.keys(scopes[key] || {}));
  69. });
  70. const inputMapImports = Object.keys(imports);
  71. const allDepNames: string[] = [
  72. ...(importmap?.map((item) => item.name) || []),
  73. ...inputMapImports,
  74. ...inputMapScopes,
  75. ];
  76. const depNames = new Set<string>(allDepNames);
  77. const installDeps = importmap?.map((item) => ({
  78. range: item.range,
  79. target: item.name,
  80. }));
  81. return [
  82. {
  83. async config(_, { command, isSsrBuild }) {
  84. isBuild = command === 'build';
  85. isSSR = !!isSsrBuild;
  86. },
  87. enforce: 'pre',
  88. name: 'importmap:external',
  89. resolveId(id) {
  90. if (isSSR || !isBuild) {
  91. return null;
  92. }
  93. if (!depNames.has(id)) {
  94. return null;
  95. }
  96. return { external: true, id };
  97. },
  98. },
  99. {
  100. enforce: 'post',
  101. name: 'importmap:install',
  102. async resolveId() {
  103. if (isSSR || !isBuild || installed) {
  104. return null;
  105. }
  106. try {
  107. installed = true;
  108. await Promise.allSettled(
  109. (installDeps || []).map((dep) => generator.install(dep)),
  110. );
  111. } catch (error: any) {
  112. installError = error;
  113. installed = false;
  114. }
  115. return null;
  116. },
  117. },
  118. {
  119. buildEnd() {
  120. // 未生成importmap时,抛出错误,防止被turbo缓存
  121. if (!installed && !isSSR) {
  122. installError && console.error(installError);
  123. throw new Error('Importmap installation failed.');
  124. }
  125. },
  126. enforce: 'post',
  127. name: 'importmap:html',
  128. transformIndexHtml: {
  129. async handler(html) {
  130. if (isSSR || !isBuild) {
  131. return html;
  132. }
  133. const importmapJson = generator.getMap();
  134. if (!importmapJson) {
  135. return html;
  136. }
  137. const esModuleShimsSrc = await getShimsUrl(
  138. options.defaultProvider || DEFAULT_PROVIDER,
  139. );
  140. const resultHtml = await injectShimsToHtml(
  141. html,
  142. esModuleShimsSrc || '',
  143. );
  144. html = await minify(resultHtml || html, {
  145. collapseWhitespace: true,
  146. minifyCSS: true,
  147. minifyJS: true,
  148. removeComments: false,
  149. });
  150. return {
  151. html,
  152. tags: [
  153. {
  154. attrs: {
  155. type: 'importmap',
  156. },
  157. injectTo: 'head-prepend',
  158. tag: 'script',
  159. children: `${JSON.stringify(importmapJson)}`,
  160. },
  161. ],
  162. };
  163. },
  164. order: 'post',
  165. },
  166. },
  167. ];
  168. }
  169. async function injectShimsToHtml(html: string, esModuleShimUrl: string) {
  170. const $ = load(html);
  171. const $script = $(`script[type='module']`);
  172. if (!$script) {
  173. return;
  174. }
  175. const entry = $script.attr('src');
  176. $script.removeAttr('type');
  177. $script.removeAttr('crossorigin');
  178. $script.removeAttr('src');
  179. $script.html(`
  180. if (!HTMLScriptElement.supports || !HTMLScriptElement.supports('importmap')) {
  181. self.importShim = function () {
  182. const promise = new Promise((resolve, reject) => {
  183. document.head.appendChild(
  184. Object.assign(document.createElement('script'), {
  185. src: '${esModuleShimUrl}',
  186. crossorigin: 'anonymous',
  187. async: true,
  188. onload() {
  189. if (!importShim.$proxy) {
  190. resolve(importShim);
  191. } else {
  192. reject(new Error('No globalThis.importShim found:' + esModuleShimUrl));
  193. }
  194. },
  195. onerror(error) {
  196. reject(error);
  197. },
  198. }),
  199. );
  200. });
  201. importShim.$proxy = true;
  202. return promise.then((importShim) => importShim(...arguments));
  203. };
  204. }
  205. var modules = ['${entry}'];
  206. typeof importShim === 'function'
  207. ? modules.forEach((moduleName) => importShim(moduleName))
  208. : modules.forEach((moduleName) => import(moduleName));
  209. `);
  210. $('body').after($script);
  211. $('head').remove(`script[type='module']`);
  212. return $.html();
  213. }
  214. export { viteImportMapPlugin };