From 2e52a2ad3e90f89a2a96e84ff6d2837e2299c239 Mon Sep 17 00:00:00 2001 From: liximomo Date: Mon, 13 Apr 2020 17:49:13 +0800 Subject: [PATCH] feat(expermental): RN-style resolver plugin --- packages/toolpack/src/webpack/config/base.ts | 136 +++++++++--------- .../toolpack/src/webpack/config/browser.ts | 90 ++++++------ .../webpack/plugins/prefer-resolver-plugin.ts | 65 +++++++++ 3 files changed, 176 insertions(+), 115 deletions(-) create mode 100644 packages/toolpack/src/webpack/plugins/prefer-resolver-plugin.ts diff --git a/packages/toolpack/src/webpack/config/base.ts b/packages/toolpack/src/webpack/config/base.ts index d84bb35f5..e402c8d0b 100644 --- a/packages/toolpack/src/webpack/config/base.ts +++ b/packages/toolpack/src/webpack/config/base.ts @@ -1,15 +1,15 @@ -import WebpackChain from "webpack-chain"; -import TerserPlugin from "terser-webpack-plugin"; -import webpack from "webpack"; -import path from "path"; -import { getTypeScriptInfo } from "@shuvi/utils/lib/detectTypescript"; -import ChunkNamesPlugin from "../plugins/chunk-names-plugin"; -import BuildManifestPlugin from "../plugins/build-manifest-plugin"; -import ModuleReplacePlugin from "../plugins/module-replace-plugin"; -import RequireCacheHotReloaderPlugin from "../plugins/require-cache-hot-reloader-plugin"; -import { AppSourceRegexs } from "../../constants"; - -const dumpRouteComponent = require.resolve("../../utils/emptyComponent"); +import WebpackChain from 'webpack-chain'; +import TerserPlugin from 'terser-webpack-plugin'; +import webpack from 'webpack'; +import path from 'path'; +import { getTypeScriptInfo } from '@shuvi/utils/lib/detectTypescript'; +import ChunkNamesPlugin from '../plugins/chunk-names-plugin'; +import BuildManifestPlugin from '../plugins/build-manifest-plugin'; +import ModuleReplacePlugin from '../plugins/module-replace-plugin'; +import RequireCacheHotReloaderPlugin from '../plugins/require-cache-hot-reloader-plugin'; +import { AppSourceRegexs } from '../../constants'; + +const dumpRouteComponent = require.resolve('../../utils/emptyComponent'); const resolveLocalLoader = (name: string) => path.join(__dirname, `../loaders/${name}`); @@ -31,14 +31,14 @@ export interface BaseOptions { const terserOptions = { parse: { - ecma: 8 + ecma: 8, }, compress: { ecma: 5, warnings: false, // The following two options are known to break valid JavaScript code comparisons: false, - inline: 2 // https://github.com/zeit/next.js/issues/7178#issuecomment-493048965 + inline: 2, // https://github.com/zeit/next.js/issues/7178#issuecomment-493048965 }, mangle: { safari10: true }, output: { @@ -46,8 +46,8 @@ const terserOptions = { safari10: true, comments: false, // Fixes usage of Emoji and certain Regex - ascii_only: true - } + ascii_only: true, + }, }; export { WebpackChain }; @@ -58,15 +58,15 @@ export function baseWebpackChain({ srcDirs, mediaFilename, buildManifestFilename, - publicPath = "/", - env = {} + publicPath = '/', + env = {}, }: BaseOptions): WebpackChain { const { typeScriptPath, tsConfigPath, useTypeScript } = getTypeScriptInfo( projectRoot ); const config = new WebpackChain(); - config.mode(dev ? "development" : "production"); + config.mode(dev ? 'development' : 'production'); config.bail(!dev); config.performance.hints(false); config.context(projectRoot); @@ -77,92 +77,92 @@ export function baseWebpackChain({ nodeEnv: false, splitChunks: false, runtimeChunk: undefined, - minimize: !dev + minimize: !dev, }); if (!dev) { - config.optimization.minimizer("terser").use(TerserPlugin, [ + config.optimization.minimizer('terser').use(TerserPlugin, [ { extractComments: false, parallel: true, cache: true, sourceMap: false, - terserOptions - } + terserOptions, + }, ]); } config.output.merge({ publicPath, - hotUpdateChunkFilename: "static/webpack/[id].[hash].hot-update.js", - hotUpdateMainFilename: "static/webpack/[hash].hot-update.json", + hotUpdateChunkFilename: 'static/webpack/[id].[hash].hot-update.js', + hotUpdateMainFilename: 'static/webpack/[hash].hot-update.json', // This saves chunks with the name given via `import()` chunkFilename: `static/chunks/${ - dev ? "[name]" : "[name].[contenthash:8]" + dev ? '[name]' : '[name].[contenthash:8]' }.js`, strictModuleExceptionHandling: true, // crossOriginLoading: crossOrigin, futureEmitAssets: !dev, - webassemblyModuleFilename: "static/wasm/[modulehash:8].wasm" + webassemblyModuleFilename: 'static/wasm/[modulehash:8].wasm', }); // Support for NODE_PATH - const nodePathList = (process.env.NODE_PATH || "") - .split(process.platform === "win32" ? ";" : ":") - .filter(p => !!p); + const nodePathList = (process.env.NODE_PATH || '') + .split(process.platform === 'win32' ? ';' : ':') + .filter((p) => !!p); config.resolve.merge({ modules: [ - "node_modules", - ...nodePathList // Support for NODE_PATH environment variable + 'node_modules', + ...nodePathList, // Support for NODE_PATH environment variable ], - alias: {} + alias: {}, }); config.resolveLoader.merge({ - alias: ["babel-loader", "route-component-loader"].reduce( + alias: ['babel-loader', 'route-component-loader'].reduce( (alias, loader) => { alias[`@shuvi/${loader}`] = resolveLocalLoader(loader); return alias; }, {} as Record - ) + ), }); - config.module.set("strictExportPresence", true); - const mainRule = config.module.rule("main"); + config.module.set('strictExportPresence', true); + const mainRule = config.module.rule('main'); mainRule - .oneOf("js") + .oneOf('js') .test(/\.(tsx|ts|js|mjs|jsx)$/) .include.merge([...srcDirs, ...AppSourceRegexs]) .end() .exclude.add((path: string) => { - if (AppSourceRegexs.some(r => r.test(path))) { + if (AppSourceRegexs.some((r) => r.test(path))) { return false; } return /node_modules/.test(path); }) .end() - .use("babel-loader") - .loader("@shuvi/babel-loader") + .use('babel-loader') + .loader('@shuvi/babel-loader') .options({ isNode: false, - cacheDirectory: true + cacheDirectory: true, }); mainRule - .oneOf("media") + .oneOf('media') .exclude.merge([/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/]) .end() - .use("file-loader") - .loader(require.resolve("file-loader")) + .use('file-loader') + .loader(require.resolve('file-loader')) .options({ - name: mediaFilename + name: mediaFilename, }); - config.plugin("private/chunk-names-plugin").use(ChunkNamesPlugin); + config.plugin('private/chunk-names-plugin').use(ChunkNamesPlugin); config - .plugin("private/ignore-plugin") + .plugin('private/ignore-plugin') .use(webpack.IgnorePlugin, [/^\.\/locale$/, /moment$/]); - config.plugin("define").use(webpack.DefinePlugin, [ + config.plugin('define').use(webpack.DefinePlugin, [ { ...Object.keys(env).reduce((acc, key) => { if (/^(?:NODE_.+)|^(?:__.+)$/i.test(key)) { @@ -171,59 +171,57 @@ export function baseWebpackChain({ return { ...acc, - [`process.env.${key}`]: JSON.stringify(env[key]) + [`process.env.${key}`]: JSON.stringify(env[key]), }; }, {}), - "process.env.NODE_ENV": JSON.stringify(dev ? "development" : "production") - } + 'process.env.NODE_ENV': JSON.stringify( + dev ? 'development' : 'production' + ), + }, ]); config - .plugin("private/build-manifest") + .plugin('private/build-manifest') .use(BuildManifestPlugin, [{ filename: buildManifestFilename }]); if (useTypeScript) { config - .plugin("private/fork-ts-checker-webpack-plugin") - .use(require.resolve("fork-ts-checker-webpack-plugin"), [ + .plugin('private/fork-ts-checker-webpack-plugin') + .use(require.resolve('fork-ts-checker-webpack-plugin'), [ { typescript: typeScriptPath, async: dev, useTypescriptIncrementalApi: true, checkSyntacticErrors: true, tsconfig: tsConfigPath, - reportFiles: ["**", "!**/__tests__/**", "!**/?(*.)(spec|test).*"], + reportFiles: ['**', '!**/__tests__/**', '!**/?(*.)(spec|test).*'], compilerOptions: { isolatedModules: true, noEmit: true }, silent: true, - formatter: "codeframe" - } + formatter: 'codeframe', + }, ]); } if (dev) { - config.plugin("private/module-replace-plugin").use(ModuleReplacePlugin, [ + config.plugin('private/module-replace-plugin').use(ModuleReplacePlugin, [ { modules: [ { test: /\?__shuvi-route/, - module: dumpRouteComponent - } - ] - } + module: dumpRouteComponent, + }, + ], + }, ]); // Even though require.cache is server only we have to clear assets from both compilations // This is because the client compilation generates the build manifest that's used on the server side config - .plugin("private/require-cache-hot-reloader") + .plugin('private/require-cache-hot-reloader') .use(RequireCacheHotReloaderPlugin); } else { config - .plugin("private/hashed-moduleids-plugin") + .plugin('private/hashed-moduleids-plugin') .use(webpack.HashedModuleIdsPlugin); } return config; } - -// export function createWebpackConfig(option: Option) { -// return createWebpackChain(option); -// } diff --git a/packages/toolpack/src/webpack/config/browser.ts b/packages/toolpack/src/webpack/config/browser.ts index f0038260f..173d10fbc 100644 --- a/packages/toolpack/src/webpack/config/browser.ts +++ b/packages/toolpack/src/webpack/config/browser.ts @@ -1,11 +1,10 @@ -import crypto from "crypto"; -import webpack from "webpack"; -import WebpackChain from "webpack-chain"; -import { getTypeScriptInfo } from "@shuvi/utils/lib/detectTypescript"; -// import BuildManifestPlugin from "../plugins/build-manifest-plugin"; -import { baseWebpackChain, BaseOptions } from "./base"; -import { withStyle } from "./parts/style"; -import { resolvePreferTarget } from "./parts/resolve"; +import crypto from 'crypto'; +import webpack from 'webpack'; +import WebpackChain from 'webpack-chain'; +import { getTypeScriptInfo } from '@shuvi/utils/lib/detectTypescript'; +import PreferResolverPlugin from '../plugins/prefer-resolver-plugin'; +import { baseWebpackChain, BaseOptions } from './base'; +import { withStyle } from './parts/style'; const BIG_LIBRARY_THRESHOLD = 160000; // byte @@ -18,34 +17,33 @@ export function createBrowserWebpackChain({ const chain = baseWebpackChain(baseOptions); const { useTypeScript } = getTypeScriptInfo(baseOptions.projectRoot); - chain.target("web"); - chain.devtool(dev ? "cheap-module-source-map" : false); - const extensions = [ - ...(useTypeScript ? [".tsx", ".ts"] : []), - ".mjs", - ".js", - ".jsx", - ".json", - ".wasm" - ]; + chain.target('web'); + chain.devtool(dev ? 'cheap-module-source-map' : false); + chain.resolve.extensions.merge([ + ...(useTypeScript ? ['.tsx', '.ts'] : []), + '.mjs', + '.js', + '.jsx', + '.json', + '.wasm', + ]); + if (baseOptions.target) { + chain.resolve + .plugin('private/prefer-resolver-plugin') + .use(PreferResolverPlugin, [{ suffix: baseOptions.target }]); + } - // TODO: use a resolver plugin to replace this - chain.resolve.extensions.merge( - baseOptions.target - ? resolvePreferTarget(baseOptions.target, extensions) - : extensions - ); if (dev) { - chain.plugin("private/hmr-plugin").use(webpack.HotModuleReplacementPlugin); + chain.plugin('private/hmr-plugin').use(webpack.HotModuleReplacementPlugin); } else { chain.optimization.splitChunks({ - chunks: "all", + chunks: 'all', cacheGroups: { default: false, vendors: false, framework: { - chunks: "all", - name: "framework", + chunks: 'all', + name: 'framework', // This regex ignores nested copies of framework libraries so they're // bundled with their issuer. // https://github.com/zeit/next.js/pull/9012 @@ -53,7 +51,7 @@ export function createBrowserWebpackChain({ priority: 40, // Don't let webpack eliminate this chunk (prevents this chunk from // becoming a part of the commons chunk) - enforce: true + enforce: true, }, lib: { test(module: { size: Function; identifier: Function }): boolean { @@ -67,7 +65,7 @@ export function createBrowserWebpackChain({ libIdent?: Function; updateHash: (hash: crypto.Hash) => void; }): string { - const hash = crypto.createHash("sha1"); + const hash = crypto.createHash('sha1'); if (module.type === `css/mini-extract`) { module.updateHash(hash); } else { @@ -82,53 +80,53 @@ export function createBrowserWebpackChain({ ); } - return hash.digest("hex").substring(0, 8); + return hash.digest('hex').substring(0, 8); }, priority: 30, minChunks: 1, - reuseExistingChunk: true + reuseExistingChunk: true, }, commons: { - name: "commons", + name: 'commons', minChunks: 2, - priority: 20 + priority: 20, }, shared: { name(module: any, chunks: any) { return crypto - .createHash("sha1") + .createHash('sha1') .update( chunks.reduce( (acc: string, chunk: webpack.compilation.Chunk) => { return acc + chunk.name; }, - "" + '' ) ) - .digest("hex"); + .digest('hex'); }, priority: 10, minChunks: 2, - reuseExistingChunk: true - } + reuseExistingChunk: true, + }, }, maxInitialRequests: 25, - minSize: 20000 + minSize: 20000, }); } - chain.plugin("define").tap(([options]) => [ + chain.plugin('define').tap(([options]) => [ { ...options, // prevent errof of destructing process.env - "process.env": JSON.stringify("{}") - } + 'process.env': JSON.stringify('{}'), + }, ]); - chain.plugin("private/build-manifest").tap(([options]) => [ + chain.plugin('private/build-manifest').tap(([options]) => [ { ...options, - modules: true - } + modules: true, + }, ]); return withStyle(chain, { extractCss: !dev, publicPath }); diff --git a/packages/toolpack/src/webpack/plugins/prefer-resolver-plugin.ts b/packages/toolpack/src/webpack/plugins/prefer-resolver-plugin.ts new file mode 100644 index 000000000..4d4474672 --- /dev/null +++ b/packages/toolpack/src/webpack/plugins/prefer-resolver-plugin.ts @@ -0,0 +1,65 @@ +interface Options { + suffix: string; +} + +export default class PreferResolverPlugin { + private _options: Options; + + constructor(options: Options) { + this._options = options; + } + + apply(resolver: any) { + const target = resolver.ensureHook('resolve'); + const { suffix } = this._options; + resolver + .getHook('described-resolve') + .tapAsync( + 'FileExistsPlugin', + (request: any, resolveContext: any, callback: any) => { + const innerRequest = request.request || request.path; + if (!innerRequest) return callback(); + if (innerRequest.endsWith('.' + suffix)) { + return callback(); + } + + const requests = [innerRequest, `${innerRequest}.${suffix}`]; + const resolveWithPrefer = (newRequest: string, cb: any) => { + const obj = { + ...request, + request: newRequest, + }; + return resolver.doResolve( + target, + obj, + 'resolve with prefer request', + resolveContext, + (err: any, result: any) => { + if (err) return cb(err); + if (result) return cb(null, result); + return cb(); + } + ); + }; + + const next = (err: any, result?: any) => { + if (err) { + return callback(err); + } + + if (result) { + return callback(err, result); + } + + const nextReuqest = requests.pop(); + if (!nextReuqest) { + return callback(); + } + resolveWithPrefer(nextReuqest, next); + }; + + next(null); + } + ); + } +}