import type { MixinModule } from '@/scripts/mf-modules' import { getPluginConfigContent } from '@/scripts/shared' import { WEB_DIR_NAME } from '@ones-open/cli-utils' import { babelConfig, postcssConfig } from '@ones-open/config' import CopyPlugin from 'copy-webpack-plugin' import HtmlWebpackPlugin from 'html-webpack-plugin' import MiniCssExtractPlugin from 'mini-css-extract-plugin' import { dirname, join, relative, sep } from 'path' import webpack from 'webpack' import type { Configuration } from 'webpack' import { cwd } from 'process' // eslint-disable-next-line @typescript-eslint/no-explicit-any const htmltemplateCretor = ({ htmlWebpackPlugin }: Record) => ` ${htmlWebpackPlugin?.options?.title}
` const onesMFSourceFilePrefix = 'ones-mf-plugin-file' interface WebpackContextInfo { /** * ├── dist * | ├── logo.svg * │ ├── modules * │ │ ├── ones-performance-vm21 * │ │ │ └── ones-performance-yxEb * │ │ │ └── index.html * │ ├── ones-performance-yxEb.28bf16.js * │ ├── ones-performance-yxEb.8c5b22.css * * This is the file structure of the plugin project after the build. The base path of the plugin is the * `modules/ones-performance-vm21/ones-performance-yxEb`. If the plugin wants to load the static script, it needs * to back to the `dist` directory. So the `publicPath` of the plugin is `../../../`. * * But in the dev hot update server, the server fetches the JSON file with relative URL, the browser completes the URL * with the ONES Project IP, the dev server is different from the ONES Project IP, so the `publicPath` should be the * absolute URL of the server(MF doesn't patch `fetch`). * * In summary, the `publicPath` of the plugin is the absolute URL in the dev server(for example, `127.0.0.1:3000`), * and is the relative URL in the ONES Project(for example: `../../../`). * * #312088 插件web工程构建,webpack热更新存在问题 * https://our.ones.pro/project/#/team/RDjYMhKq/task/Xa7wVTYoyt658OK3 * * Because the plugin project tries to load `modules/ones-performance-vm21/ones-performance-yxEb/ones-performance-yxEb.886d40.hot-update.json`, * but the right URL is `./ones-performance-yxEb.886d40.hot-update.json`. */ getPublicPath?: () => string appID: string } function getExtendEntriesAndPlugin(modules: MixinModule[], contextInfo: WebpackContextInfo) { const entries: Configuration['entry'] = {} const entriesBlockSet = new Set() const plugins: Configuration['plugins'] = [] const updateEntriesAndPlugins = (module: MixinModule) => { if (module?.modules?.length) { const { modules: subModules } = module subModules.forEach(updateEntriesAndPlugins) return } if (module?.entry && !entriesBlockSet.has(module.entry)) { const { id: moduleId, title: moduleTitle, entry: moduleEntry, icon, moduleType } = module const moduleEntryPath = join('src', moduleEntry.replace(/\.html$/, '.tsx')) // static public path const publicPath = join('public').split(sep).join('/') // Strip up the path and convert to the POSIX style const modulePublicPath = relative(dirname(moduleEntry), '').split(sep).join('/') entries[moduleId] = { import: `./${moduleEntryPath}`, filename: '[name].[contenthash].js', publicPath: contextInfo.getPublicPath?.() ?? modulePublicPath + '/', } const baseConfig: HtmlWebpackPlugin.Options = { title: moduleTitle ?? '', filename: moduleEntry, chunks: [moduleId], publicPath: modulePublicPath, scriptLoading: 'blocking', templateContent: htmltemplateCretor, } const enhancedConfig: HtmlWebpackPlugin.Options = {} /** * when meet the special module `about:blank` * supplement favicon by enhancedConfig */ if (moduleType === 'about:blank') { enhancedConfig.favicon = icon ? `${publicPath}/${icon}` : false } plugins.push( new HtmlWebpackPlugin({ ...baseConfig, ...enhancedConfig, }), new MiniCssExtractPlugin({ filename: '[name].[contenthash].css', }), ) entriesBlockSet.add(moduleEntry) } } modules.forEach(updateEntriesAndPlugins) return { entries, plugins, } } function getDefaultWebpackConfig( modules: MixinModule[], currentWorkingDirectory: string, contextInfo: WebpackContextInfo, ) { const { entries, plugins } = getExtendEntriesAndPlugin(modules, contextInfo) const CONTEXT_PATH = join(currentWorkingDirectory, WEB_DIR_NAME) const BASIC_WEBPACK_CONFIG = { entry: entries, context: CONTEXT_PATH, output: { path: join(CONTEXT_PATH, 'dist'), clean: true, assetModuleFilename: '[name].[contenthash][ext]', chunkFilename: `${onesMFSourceFilePrefix}-${contextInfo.appID}` + '.[name].[contenthash].js', hashDigestLength: 6, publicPath: '', // fix: Automatic publicPath is not supported in this browser }, module: { rules: [ { test: /\.tsx?$/, use: { loader: 'babel-loader', options: { ...babelConfig, cwd: CONTEXT_PATH }, }, }, { test: /\.css$/i, use: [ { loader: MiniCssExtractPlugin.loader, }, { loader: 'css-loader', }, { loader: 'postcss-loader', options: { postcssOptions: postcssConfig, }, }, ], }, { test: /\.svg$/i, use: [ { loader: '@svgr/webpack', options: { memo: true, }, }, ], }, { test: /\.(webp|png|jpg|jpeg|gif)$/i, type: 'asset', parser: { dataUrlCondition: { maxSize: 1024, // 小于 1kb 视为 `inline` 模块类型,否则为 `resource` 模块类型 }, }, }, ], }, resolve: { extensions: ['.ts', '.js', '.tsx', '.jsx', '.json'], }, plugins: [ new CopyPlugin({ patterns: [ { from: 'public', }, ], }), ...plugins, ], } return BASIC_WEBPACK_CONFIG } async function buildPluginProjectFrontEnd(currentWorkingDirectory = cwd()) { const pluginConfigContent = await getPluginConfigContent() const pluginModules = pluginConfigContent?.modules ?? [] const defaultConfig = getDefaultWebpackConfig(pluginModules, currentWorkingDirectory, { appID: pluginConfigContent.service.app_id, }) const webpackConfig: Configuration = { ...defaultConfig, mode: 'production', } type WebpackCallbackError = | undefined | (Error & { details?: string }) const build = new Promise((resolve, reject) => { webpack(webpackConfig, (err: WebpackCallbackError, stats) => { if (err) { let errorMessage = `` errorMessage += `Error stack: ${err.stack || err}` if (err?.details) { errorMessage += `, Error details: ${err.details}` } reject(errorMessage) } if (stats?.hasErrors()) { const [statsErrors] = stats?.toJson().errors ?? [] reject(statsErrors.message) } resolve(stats) }) }) await build } export { getExtendEntriesAndPlugin, getDefaultWebpackConfig, buildPluginProjectFrontEnd }