/**
|
* 参考 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 = {
|
debug?: boolean;
|
defaultProvider?: 'esm.sh' | 'jsdelivr' | 'jspm.io';
|
importmap?: Array<{ name: string; range?: string }>;
|
} & GeneratorOptions;
|
|
// 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<string, string> = {
|
'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<Plugin[]> {
|
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<string>(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 };
|