From 72dddfb7cb04e6a83b4702abd634a1fb72911fde Mon Sep 17 00:00:00 2001 From: harrywan Date: Fri, 10 Nov 2023 14:55:11 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dvite=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E6=97=B6public=E4=B8=8B=E7=9A=84html=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E6=AD=A3=E5=B8=B8=E8=AE=BF=E9=97=AE=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.js | 1 + packages/fes-builder-vite/package.json | 13 +- .../src/common/connectHistoryMiddleware.js | 15 + .../fes-builder-vite/src/common/getConfig.js | 2 +- .../src/common/vite-plugin-html.js | 348 ++++++++++++++++++ pnpm-lock.yaml | 69 ++-- 6 files changed, 418 insertions(+), 30 deletions(-) create mode 100644 packages/fes-builder-vite/src/common/connectHistoryMiddleware.js create mode 100644 packages/fes-builder-vite/src/common/vite-plugin-html.js diff --git a/.eslintrc.js b/.eslintrc.js index d09347a2..29ba03dd 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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, diff --git a/packages/fes-builder-vite/package.json b/packages/fes-builder-vite/package.json index 84d3de06..5ac17daa 100644 --- a/packages/fes-builder-vite/package.json +++ b/packages/fes-builder-vite/package.json @@ -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", diff --git a/packages/fes-builder-vite/src/common/connectHistoryMiddleware.js b/packages/fes-builder-vite/src/common/connectHistoryMiddleware.js new file mode 100644 index 00000000..3a6a864a --- /dev/null +++ b/packages/fes-builder-vite/src/common/connectHistoryMiddleware.js @@ -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; diff --git a/packages/fes-builder-vite/src/common/getConfig.js b/packages/fes-builder-vite/src/common/getConfig.js index a434bc86..7954483a 100644 --- a/packages/fes-builder-vite/src/common/getConfig.js +++ b/packages/fes-builder-vite/src/common/getConfig.js @@ -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'; diff --git a/packages/fes-builder-vite/src/common/vite-plugin-html.js b/packages/fes-builder-vite/src/common/vite-plugin-html.js new file mode 100644 index 00000000..619c2494 --- /dev/null +++ b/packages/fes-builder-vite/src/common/vite-plugin-html.js @@ -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, ``); + } + 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 }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 76653ea1..d712823b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: