importmap.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  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 = {
  11. debug?: boolean;
  12. defaultProvider?: 'esm.sh' | 'jsdelivr' | 'jspm.io';
  13. importmap?: Array<{ name: string; range?: string }>;
  14. } & GeneratorOptions;
  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. // eslint-disable-next-line no-console
  60. console.log(`${type}: ${message}`);
  61. }
  62. })();
  63. }
  64. const imports = options.inputMap?.imports ?? {};
  65. const scopes = options.inputMap?.scopes ?? {};
  66. const firstLayerKeys = Object.keys(scopes);
  67. const inputMapScopes: string[] = [];
  68. firstLayerKeys.forEach((key) => {
  69. inputMapScopes.push(...Object.keys(scopes[key]));
  70. });
  71. const inputMapImports = Object.keys(imports);
  72. const allDepNames: string[] = [
  73. ...(importmap?.map((item) => item.name) || []),
  74. ...inputMapImports,
  75. ...inputMapScopes,
  76. ];
  77. const depNames = new Set<string>(allDepNames);
  78. const installDeps = importmap?.map((item) => ({
  79. range: item.range,
  80. target: item.name,
  81. }));
  82. return [
  83. {
  84. async config(_, { command, isSsrBuild }) {
  85. isBuild = command === 'build';
  86. isSSR = !!isSsrBuild;
  87. },
  88. enforce: 'pre',
  89. name: 'importmap:external',
  90. resolveId(id) {
  91. if (isSSR || !isBuild) {
  92. return null;
  93. }
  94. if (!depNames.has(id)) {
  95. return null;
  96. }
  97. return { external: true, id };
  98. },
  99. },
  100. {
  101. enforce: 'post',
  102. name: 'importmap:install',
  103. async resolveId() {
  104. if (isSSR || !isBuild || installed) {
  105. return null;
  106. }
  107. try {
  108. installed = true;
  109. await Promise.allSettled(
  110. (installDeps || []).map((dep) => generator.install(dep)),
  111. );
  112. } catch (error: any) {
  113. installError = error;
  114. installed = false;
  115. }
  116. return null;
  117. },
  118. },
  119. {
  120. buildEnd() {
  121. // 未生成importmap时,抛出错误,防止被turbo缓存
  122. if (!installed && !isSSR) {
  123. installError && console.error(installError);
  124. throw new Error('Importmap installation failed.');
  125. }
  126. },
  127. enforce: 'post',
  128. name: 'importmap:html',
  129. transformIndexHtml: {
  130. async handler(html) {
  131. if (isSSR || !isBuild) {
  132. return html;
  133. }
  134. const importmapJson = generator.getMap();
  135. if (!importmapJson) {
  136. return html;
  137. }
  138. const esModuleShimsSrc = await getShimsUrl(
  139. options.defaultProvider || DEFAULT_PROVIDER,
  140. );
  141. const resultHtml = await injectShimsToHtml(html, esModuleShimsSrc);
  142. html = await minify(resultHtml || html, {
  143. collapseWhitespace: true,
  144. minifyCSS: true,
  145. minifyJS: true,
  146. removeComments: false,
  147. });
  148. return {
  149. html,
  150. tags: [
  151. {
  152. attrs: {
  153. type: 'importmap',
  154. },
  155. injectTo: 'head-prepend',
  156. tag: 'script',
  157. children: `${JSON.stringify(importmapJson)}`,
  158. },
  159. ],
  160. };
  161. },
  162. order: 'post',
  163. },
  164. },
  165. ];
  166. }
  167. async function injectShimsToHtml(html: string, esModuleShimUrl: string) {
  168. const $ = load(html);
  169. const $script = $(`script[type='module']`);
  170. if (!$script) {
  171. return;
  172. }
  173. const entry = $script.attr('src');
  174. $script.removeAttr('type');
  175. $script.removeAttr('crossorigin');
  176. $script.removeAttr('src');
  177. $script.html(`
  178. if (!HTMLScriptElement.supports || !HTMLScriptElement.supports('importmap')) {
  179. self.importShim = function () {
  180. const promise = new Promise((resolve, reject) => {
  181. document.head.appendChild(
  182. Object.assign(document.createElement('script'), {
  183. src: '${esModuleShimUrl}',
  184. crossorigin: 'anonymous',
  185. async: true,
  186. onload() {
  187. if (!importShim.$proxy) {
  188. resolve(importShim);
  189. } else {
  190. reject(new Error('No globalThis.importShim found:' + esModuleShimUrl));
  191. }
  192. },
  193. onerror(error) {
  194. reject(error);
  195. },
  196. }),
  197. );
  198. });
  199. importShim.$proxy = true;
  200. return promise.then((importShim) => importShim(...arguments));
  201. };
  202. }
  203. var modules = ['${entry}'];
  204. typeof importShim === 'function'
  205. ? modules.forEach((moduleName) => importShim(moduleName))
  206. : modules.forEach((moduleName) => import(moduleName));
  207. `);
  208. $('body').after($script);
  209. $('head').remove(`script[type='module']`);
  210. return $.html();
  211. }
  212. export { viteImportMapPlugin };