fix: 修复vite构建时public下的html文件无法正常访问问题

This commit is contained in:
harrywan 2023-11-10 14:55:11 +08:00
parent 585235697a
commit 72dddfb7cb
6 changed files with 418 additions and 30 deletions

View File

@ -14,6 +14,7 @@ module.exports = {
'no-restricted-syntax': 'off',
'no-undefined': 'off',
'vue/valid-template-root': 'off',
'no-use-before-define': 'off',
},
env: {
jest: true,

View File

@ -39,7 +39,18 @@
"rollup-plugin-visualizer": "^5.6.0",
"terser": "^5.16.8",
"vite": "^4.2.1",
"vite-plugin-html": "^3.2.0"
"@rollup/pluginutils": "^4.2.0",
"colorette": "^2.0.16",
"connect-history-api-fallback": "^2.0.0",
"consola": "^2.15.3",
"dotenv": "^16.0.0",
"dotenv-expand": "^8.0.2",
"ejs": "^3.1.6",
"fast-glob": "^3.2.11",
"fs-extra": "^10.0.1",
"html-minifier-terser": "^6.1.0",
"node-html-parser": "^5.3.3",
"pathe": "^0.2.0"
},
"peerDependencies": {
"@fesjs/fes": "^3.1.4",

View File

@ -0,0 +1,15 @@
import { join } from 'path';
import { pathExistsSync } from 'fs-extra';
import historyFallback from 'connect-history-api-fallback';
const proxyMiddleware = (viteConfig, params) => (req, res, next) => {
const fileName = join(viteConfig.publicDir, req.url);
if (req.url.length > 1 && req.url.startsWith('/') && pathExistsSync(fileName)) {
return next();
}
const history = historyFallback(params);
history(req, res, next);
};
export default proxyMiddleware;

View File

@ -2,7 +2,7 @@ import { join } from 'path';
import { existsSync } from 'fs';
import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import { createHtmlPlugin } from 'vite-plugin-html';
import { createHtmlPlugin } from './vite-plugin-html';
import SFCConfigBlockPlugin from './SFCConfigBlockPlugin';
import getDefine from './getDefine';

View File

@ -0,0 +1,348 @@
import { render } from 'ejs';
import { expand } from 'dotenv-expand';
import dotenv from 'dotenv';
import path, { join, dirname } from 'pathe';
import fse from 'fs-extra';
import { normalizePath } from 'vite';
import { parse } from 'node-html-parser';
import fg from 'fast-glob';
import consola from 'consola';
import { dim } from 'colorette';
import { minify } from 'html-minifier-terser';
import { createFilter } from '@rollup/pluginutils';
import history from './connectHistoryMiddleware';
function lookupFile(dir, formats, pathOnly = false) {
for (const format of formats) {
const fullPath = join(dir, format);
if (fse.pathExistsSync(fullPath) && fse.statSync(fullPath).isFile()) {
return pathOnly ? fullPath : fse.readFileSync(fullPath, 'utf-8');
}
}
const parentDir = dirname(dir);
if (parentDir !== dir) {
return lookupFile(parentDir, formats, pathOnly);
}
}
function loadEnv(mode, envDir, prefix = '') {
if (mode === 'local') {
throw new Error(`"local" cannot be used as a mode name because it conflicts with the .local postfix for .env files.`);
}
const env = {};
const envFiles = [`.env.${mode}.local`, `.env.${mode}`, `.env.local`, `.env`];
for (const file of envFiles) {
const _path = lookupFile(envDir, [file], true);
if (_path) {
const parsed = dotenv.parse(fse.readFileSync(_path));
expand({
parsed,
ignoreProcessEnv: true,
});
for (const [key, value] of Object.entries(parsed)) {
if (key.startsWith(prefix) && env[key] === undefined) {
env[key] = value;
} else if (key === 'NODE_ENV') {
process.env.VITE_USER_NODE_ENV = value;
}
}
}
}
return env;
}
async function isDirEmpty(dir) {
return fse.readdir(dir).then((files) => files.length === 0);
}
const DEFAULT_TEMPLATE = 'index.html';
const ignoreDirs = ['.', '', '/'];
const bodyInjectRE = /<\/body>/;
function createPlugin(userOptions = {}) {
const { entry, template = DEFAULT_TEMPLATE, pages = [], verbose = false } = userOptions;
let viteConfig;
let env = {};
return {
name: 'vite:html',
enforce: 'pre',
configResolved(resolvedConfig) {
viteConfig = resolvedConfig;
env = loadEnv(viteConfig.mode, viteConfig.root, '');
},
config(conf) {
const input = createInput(userOptions, conf);
if (input) {
return {
build: {
rollupOptions: {
input,
},
},
};
}
},
configureServer(server) {
let _pages = [];
const rewrites = [];
if (!isMpa(viteConfig)) {
const template2 = userOptions.template || DEFAULT_TEMPLATE;
const filename = DEFAULT_TEMPLATE;
_pages.push({
filename,
template: template2,
});
} else {
_pages = pages.map((page) => ({
filename: page.filename || DEFAULT_TEMPLATE,
template: page.template || DEFAULT_TEMPLATE,
}));
}
const proxy = viteConfig.server?.proxy ?? {};
const baseUrl = viteConfig.base ?? '/';
const keys = Object.keys(proxy);
let indexPage = null;
for (const page of _pages) {
if (page.filename !== 'index.html') {
rewrites.push(createRewire(page.template, page, baseUrl, keys));
} else {
indexPage = page;
}
}
if (indexPage) {
rewrites.push(createRewire('', indexPage, baseUrl, keys));
}
server.middlewares.use(
history(viteConfig, {
disableDotRule: undefined,
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
rewrites,
}),
);
},
transformIndexHtml: {
enforce: 'pre',
async transform(html, ctx) {
const url = ctx.filename;
const base = viteConfig.base;
const excludeBaseUrl = url.replace(base, '/');
const htmlName = path.relative(process.cwd(), excludeBaseUrl);
const page = getPage(userOptions, htmlName, viteConfig);
const { injectOptions = {} } = page;
const _html = await renderHtml(html, {
injectOptions,
viteConfig,
env,
entry: page.entry || entry,
verbose,
});
const { tags = [] } = injectOptions;
return {
html: _html,
tags,
};
},
},
async closeBundle() {
const outputDirs = [];
if (isMpa(viteConfig) || pages.length) {
for (const page of pages) {
const dir = path.dirname(page.template);
if (!ignoreDirs.includes(dir)) {
outputDirs.push(dir);
}
}
} else {
const dir = path.dirname(template);
if (!ignoreDirs.includes(dir)) {
outputDirs.push(dir);
}
}
const cwd = path.resolve(viteConfig.root, viteConfig.build.outDir);
const htmlFiles = await fg(
outputDirs.map((dir) => `${dir}/*.html`),
{ cwd: path.resolve(cwd), absolute: true },
);
await Promise.all(
htmlFiles.map((file) =>
fse.move(file, path.resolve(cwd, path.basename(file)), {
overwrite: true,
}),
),
);
const htmlDirs = await fg(
outputDirs.map((dir) => dir),
{ cwd: path.resolve(cwd), onlyDirectories: true, absolute: true },
);
await Promise.all(
htmlDirs.map(async (item) => {
const isEmpty = await isDirEmpty(item);
if (isEmpty) {
return fse.remove(item);
}
}),
);
},
};
}
function createInput({ pages = [], template = DEFAULT_TEMPLATE }, viteConfig) {
const input = {};
if (isMpa(viteConfig) || pages?.length) {
const templates = pages.map((page) => page.template);
templates.forEach((temp) => {
let dirName = path.dirname(temp);
const file = path.basename(temp);
dirName = dirName.replace(/\s+/g, '').replace(/\//g, '-');
const key = dirName === '.' || dirName === 'public' || !dirName ? file.replace(/\.html/, '') : dirName;
input[key] = path.resolve(viteConfig.root, temp);
});
return input;
}
const dir = path.dirname(template);
if (ignoreDirs.includes(dir)) {
return undefined;
}
const file = path.basename(template);
const key = file.replace(/\.html/, '');
return {
[key]: path.resolve(viteConfig.root, template),
};
}
async function renderHtml(html, config) {
const { injectOptions, viteConfig, env, entry, verbose } = config;
const { data, ejsOptions } = injectOptions;
const ejsData = {
...(viteConfig?.env ?? {}),
...(viteConfig?.define ?? {}),
...(env || {}),
...data,
};
let result = await render(html, ejsData, ejsOptions);
if (entry) {
result = removeEntryScript(result, verbose);
result = result.replace(bodyInjectRE, `<script type="module" src="${normalizePath(`${entry}`)}"></script></body>`);
}
return result;
}
function getPage({ pages = [], entry, template = DEFAULT_TEMPLATE, inject = {} }, name, viteConfig) {
let page;
if (isMpa(viteConfig) || pages?.length) {
page = getPageConfig(name, pages, DEFAULT_TEMPLATE);
} else {
page = createSpaPage(entry, template, inject);
}
return page;
}
function isMpa(viteConfig) {
const input = viteConfig?.build?.rollupOptions?.input ?? undefined;
return typeof input !== 'string' && Object.keys(input || {}).length > 1;
}
function removeEntryScript(html, verbose = false) {
if (!html) {
return html;
}
const root = parse(html);
const scriptNodes = root.querySelectorAll('script[type=module]') || [];
const removedNode = [];
scriptNodes.forEach((item) => {
removedNode.push(item.toString());
item.parentNode.removeChild(item);
});
verbose &&
removedNode.length &&
consola.warn(`vite-plugin-html: Since you have already configured entry, ${dim(
removedNode.toString(),
)} is deleted. You may also delete it from the index.html.
`);
return root.toString();
}
function createSpaPage(entry, template, inject = {}) {
return {
entry,
filename: 'index.html',
template,
injectOptions: inject,
};
}
function getPageConfig(htmlName, pages, defaultPage) {
const defaultPageOption = {
filename: defaultPage,
template: `./${defaultPage}`,
};
const page = pages.filter((page2) => path.resolve(`/${page2.template}`) === path.resolve(`/${htmlName}`))?.[0];
return page ?? defaultPageOption ?? undefined;
}
function createRewire(reg, page, baseUrl, proxyUrlKeys) {
return {
from: new RegExp(`^/${reg}*`),
to({ parsedUrl }) {
const pathname = parsedUrl.pathname;
const excludeBaseUrl = pathname.replace(baseUrl, '/');
const template = path.resolve(baseUrl, page.template);
if (excludeBaseUrl === '/') {
return template;
}
const isApiUrl = proxyUrlKeys.some((item) => pathname.startsWith(path.resolve(baseUrl, item)));
return isApiUrl ? parsedUrl.path : template;
},
};
}
const htmlFilter = createFilter(['**/*.html']);
function getOptions(_minify) {
return {
collapseWhitespace: _minify,
keepClosingSlash: _minify,
removeComments: _minify,
removeRedundantAttributes: _minify,
removeScriptTypeAttributes: _minify,
removeStyleLinkTypeAttributes: _minify,
useShortDoctype: _minify,
minifyCSS: _minify,
};
}
async function minifyHtml(html, minify$1) {
if (typeof minify$1 === 'boolean' && !minify$1) {
return html;
}
let minifyOptions = minify$1;
if (typeof minify$1 === 'boolean' && minify$1) {
minifyOptions = getOptions(minify$1);
}
const res = await minify(html, minifyOptions);
return res;
}
function createMinifyHtmlPlugin({ _minify = true } = {}) {
return {
name: 'vite:minify-html',
enforce: 'post',
async generateBundle(_, outBundle) {
if (_minify) {
for (const bundle of Object.values(outBundle)) {
if (bundle.type === 'asset' && htmlFilter(bundle.fileName) && typeof bundle.source === 'string') {
bundle.source = await minifyHtml(bundle.source, _minify);
}
}
}
},
};
}
consola.wrapConsole();
function createHtmlPlugin(userOptions = {}) {
return [createPlugin(userOptions), createMinifyHtmlPlugin(userOptions)];
}
export { createHtmlPlugin };

69
pnpm-lock.yaml generated
View File

@ -125,6 +125,9 @@ importers:
'@fesjs/utils':
specifier: ^3.0.1
version: link:../fes-utils
'@rollup/pluginutils':
specifier: ^4.2.0
version: 4.2.1
'@vitejs/plugin-basic-ssl':
specifier: ^1.0.1
version: 1.0.1(vite@4.2.1)
@ -143,12 +146,45 @@ importers:
babel-plugin-polyfill-corejs3:
specifier: ^0.7.1
version: 0.7.1(@babel/core@7.21.3)
colorette:
specifier: ^2.0.16
version: 2.0.20
connect-history-api-fallback:
specifier: ^2.0.0
version: 2.0.0
consola:
specifier: ^2.15.3
version: 2.15.3
core-js:
specifier: ^3.29.1
version: 3.29.1
dotenv:
specifier: ^16.0.0
version: 16.0.3
dotenv-expand:
specifier: ^8.0.2
version: 8.0.3
ejs:
specifier: ^3.1.6
version: 3.1.9
fast-glob:
specifier: ^3.2.11
version: 3.2.12
fs-extra:
specifier: ^10.0.1
version: 10.1.0
html-minifier-terser:
specifier: ^6.1.0
version: 6.1.0
less:
specifier: ^4.1.2
version: 4.1.3
node-html-parser:
specifier: ^5.3.3
version: 5.4.2
pathe:
specifier: ^0.2.0
version: 0.2.0
postcss-flexbugs-fixes:
specifier: ^5.0.2
version: 5.0.2(postcss@8.4.21)
@ -164,9 +200,6 @@ importers:
vite:
specifier: ^4.2.1
version: 4.2.1(@types/node@18.15.13)(less@4.1.3)(terser@5.16.8)
vite-plugin-html:
specifier: ^3.2.0
version: 3.2.0(vite@4.2.1)
packages/fes-builder-webpack:
dependencies:
@ -2829,7 +2862,7 @@ packages:
vue: ^3.2.47
dependencies:
'@fesjs/fes': link:packages/fes
'@fesjs/utils': 3.0.0
'@fesjs/utils': 3.0.1
axios: 1.3.6
vue: 3.2.47
transitivePeerDependencies:
@ -2837,8 +2870,8 @@ packages:
- supports-color
dev: false
/@fesjs/utils@3.0.0:
resolution: {integrity: sha512-mQoQKn7wm+itO0iR2ysaoEGiEATHgbjvY2gvEj/ev8K/zwTjxBpSID/XIGyAJMh7DxCOPARpWf8BO6Kyafh6hA==}
/@fesjs/utils@3.0.1:
resolution: {integrity: sha512-L8Ygr1/coKCoRRsxZdkV2b0R3xgup127uXZ2mbzEihGzMzUJgN9jlfarHfsAaBbxvOcswEbLqMrTQWg6CNqFdg==}
dependencies:
'@babel/generator': 7.21.3
'@babel/parser': 7.21.3
@ -3270,7 +3303,7 @@ packages:
dev: true
/@nodelib/fs.scandir@2.1.5:
resolution: {integrity: sha1-dhnC6yGyVIP20WdUi0z9WnSIw9U=, tarball: https://registry.npmmirror.com/@nodelib/fs.scandir/download/@nodelib/fs.scandir-2.1.5.tgz}
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
dependencies:
'@nodelib/fs.stat': 2.0.5
@ -5452,7 +5485,7 @@ packages:
dev: false
/concat-map@0.0.1:
resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=, tarball: https://registry.npmmirror.com/concat-map/download/concat-map-0.0.1.tgz}
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
/confusing-browser-globals@1.0.11:
resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==}
@ -12419,26 +12452,6 @@ packages:
engines: {node: '>= 0.8'}
dev: false
/vite-plugin-html@3.2.0(vite@4.2.1):
resolution: {integrity: sha512-2VLCeDiHmV/BqqNn5h2V+4280KRgQzCFN47cst3WiNK848klESPQnzuC3okH5XHtgwHH/6s1Ho/YV6yIO0pgoQ==}
peerDependencies:
vite: '>=2.0.0'
dependencies:
'@rollup/pluginutils': 4.2.1
colorette: 2.0.20
connect-history-api-fallback: 1.6.0
consola: 2.15.3
dotenv: 16.0.3
dotenv-expand: 8.0.3
ejs: 3.1.9
fast-glob: 3.2.12
fs-extra: 10.1.0
html-minifier-terser: 6.1.0
node-html-parser: 5.4.2
pathe: 0.2.0
vite: 4.2.1(@types/node@18.15.13)(less@4.1.3)(terser@5.16.8)
dev: false
/vite-plugin-monaco-editor@1.1.0(monaco-editor@0.36.1):
resolution: {integrity: sha512-IvtUqZotrRoVqwT0PBBDIZPNraya3BxN/bfcNfnxZ5rkJiGcNtO5eAOWWSgT7zullIAEqQwxMU83yL9J5k7gww==}
peerDependencies: