- //@ts-check
- /** @typedef {import('webpack').Configuration} WebpackConfig **/
-
- const { spawnSync } = require('child_process');
- const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
- const CircularDependencyPlugin = require('circular-dependency-plugin');
- const { CleanWebpackPlugin: CleanPlugin } = require('clean-webpack-plugin');
- const CopyPlugin = require('copy-webpack-plugin');
- const CspHtmlPlugin = require('csp-html-webpack-plugin');
- const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
- const esbuild = require('esbuild');
- const { EsbuildPlugin } = require('esbuild-loader');
- const { generateFonts } = require('fantasticon');
- const ForkTsCheckerPlugin = require('fork-ts-checker-webpack-plugin');
- const fs = require('fs');
- const HtmlPlugin = require('html-webpack-plugin');
- const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
- const MiniCssExtractPlugin = require('mini-css-extract-plugin');
- const path = require('path');
- const { validate } = require('schema-utils');
- const TerserPlugin = require('terser-webpack-plugin');
- const { DefinePlugin, optimize, WebpackError } = require('webpack');
- const WebpackRequireFromPlugin = require('webpack-require-from');
-
- module.exports =
- /**
- * @param {{ analyzeBundle?: boolean; analyzeDeps?: boolean; esbuild?: boolean; esbuildMinify?: boolean; useSharpForImageOptimization?: boolean } | undefined } env
- * @param {{ mode: 'production' | 'development' | 'none' | undefined }} argv
- * @returns { WebpackConfig[] }
- */
- function (env, argv) {
- const mode = argv.mode || 'none';
-
- env = {
- analyzeBundle: false,
- analyzeDeps: false,
- esbuild: true,
- esbuildMinify: false,
- useSharpForImageOptimization: true,
- ...env,
- };
-
- return [
- getExtensionConfig('node', mode, env),
- getExtensionConfig('webworker', mode, env),
- getWebviewsConfig(mode, env),
- ];
- };
-
- /**
- * @param { 'node' | 'webworker' } target
- * @param { 'production' | 'development' | 'none' } mode
- * @param {{ analyzeBundle?: boolean; analyzeDeps?: boolean; esbuild?: boolean; esbuildMinify?: boolean; useSharpForImageOptimization?: boolean }} env
- * @returns { WebpackConfig }
- */
- function getExtensionConfig(target, mode, env) {
- /**
- * @type WebpackConfig['plugins'] | any
- */
- const plugins = [
- new CleanPlugin({ cleanOnceBeforeBuildPatterns: ['!dist/webviews/**'] }),
- new ForkTsCheckerPlugin({
- async: false,
- eslint: {
- enabled: true,
- files: 'src/**/*.ts?(x)',
- options: {
- cache: true,
- cacheLocation: path.join(__dirname, '.eslintcache/', target === 'webworker' ? 'browser/' : ''),
- cacheStrategy: 'content',
- fix: mode !== 'production',
- overrideConfigFile: path.join(
- __dirname,
- target === 'webworker' ? '.eslintrc.browser.json' : '.eslintrc.json',
- ),
- },
- },
- formatter: 'basic',
- typescript: {
- configFile: path.join(__dirname, target === 'webworker' ? 'tsconfig.browser.json' : 'tsconfig.json'),
- },
- }),
- ];
-
- if (target === 'webworker') {
- plugins.push(new optimize.LimitChunkCountPlugin({ maxChunks: 1 }));
- } else {
- // Ensure that the dist folder exists otherwise the FantasticonPlugin will fail
- const dist = path.join(__dirname, 'dist');
- if (!fs.existsSync(dist)) {
- fs.mkdirSync(dist);
- }
-
- plugins.push(
- new FantasticonPlugin({
- configPath: '.fantasticonrc.js',
- onBefore: () =>
- spawnSync('yarn', ['run', 'icons:svgo'], {
- cwd: __dirname,
- encoding: 'utf8',
- shell: true,
- }),
- onComplete: () =>
- spawnSync('yarn', ['run', 'icons:apply'], {
- cwd: __dirname,
- encoding: 'utf8',
- shell: true,
- }),
- }),
- );
- }
-
- if (env.analyzeDeps) {
- plugins.push(
- new CircularDependencyPlugin({
- cwd: __dirname,
- exclude: /node_modules/,
- failOnError: false,
- onDetected: function ({ module: _webpackModuleRecord, paths, compilation }) {
- if (paths.some(p => p.includes('container.ts'))) return;
-
- // @ts-ignore
- compilation.warnings.push(new WebpackError(paths.join(' -> ')));
- },
- }),
- );
- }
-
- if (env.analyzeBundle) {
- const out = path.join(__dirname, 'out');
- if (!fs.existsSync(out)) {
- fs.mkdirSync(out);
- }
-
- plugins.push(
- new BundleAnalyzerPlugin({
- analyzerMode: 'static',
- generateStatsFile: true,
- openAnalyzer: false,
- reportFilename: path.join(out, `extension-${target}-bundle-report.html`),
- statsFilename: path.join(out, 'stats.json'),
- }),
- );
- }
-
- return {
- name: `extension:${target}`,
- entry: {
- extension: './src/extension.ts',
- },
- mode: mode,
- target: target,
- devtool: mode === 'production' ? false : 'source-map',
- output: {
- chunkFilename: 'feature-[name].js',
- filename: 'gitlens.js',
- libraryTarget: 'commonjs2',
- path: target === 'webworker' ? path.join(__dirname, 'dist', 'browser') : path.join(__dirname, 'dist'),
- },
- optimization: {
- minimizer: [
- env.esbuildMinify
- ? new EsbuildPlugin({
- drop: ['debugger'],
- format: 'cjs',
- // Keep the class names otherwise @log won't provide a useful name
- keepNames: true,
- legalComments: 'none',
- minify: true,
- target: 'es2022',
- treeShaking: true,
- })
- : new TerserPlugin({
- extractComments: false,
- parallel: true,
- terserOptions: {
- compress: {
- drop_debugger: true,
- ecma: 2020,
- module: true,
- },
- ecma: 2020,
- format: {
- comments: false,
- ecma: 2020,
- },
- // Keep the class names otherwise @log won't provide a useful name
- keep_classnames: true,
- module: true,
- },
- }),
- ],
- splitChunks:
- target === 'webworker'
- ? false
- : {
- // Disable all non-async code splitting
- chunks: () => false,
- cacheGroups: {
- default: false,
- vendors: false,
- },
- },
- },
- externals: {
- vscode: 'commonjs vscode',
- },
- module: {
- rules: [
- {
- exclude: /\.d\.ts$/,
- include: path.join(__dirname, 'src'),
- test: /\.tsx?$/,
- use: env.esbuild
- ? {
- loader: 'esbuild-loader',
- options: {
- format: 'esm',
- implementation: esbuild,
- target: ['es2022', 'chrome102', 'node16.14.2'],
- tsconfig: path.join(
- __dirname,
- target === 'webworker' ? 'tsconfig.browser.json' : 'tsconfig.json',
- ),
- },
- }
- : {
- loader: 'ts-loader',
- options: {
- configFile: path.join(
- __dirname,
- target === 'webworker' ? 'tsconfig.browser.json' : 'tsconfig.json',
- ),
- experimentalWatchApi: true,
- transpileOnly: true,
- },
- },
- },
- ],
- },
- resolve: {
- alias: {
- '@env': path.resolve(__dirname, 'src', 'env', target === 'webworker' ? 'browser' : target),
- // Stupid dependency that is used by `http[s]-proxy-agent`
- debug: path.resolve(__dirname, 'patches', 'debug.js'),
- // This dependency is very large, and isn't needed for our use-case
- tr46: path.resolve(__dirname, 'patches', 'tr46.js'),
- // This dependency is unnecessary for our use-case
- 'whatwg-url': path.resolve(__dirname, 'patches', 'whatwg-url.js'),
- },
- fallback:
- target === 'webworker'
- ? { path: require.resolve('path-browserify'), os: require.resolve('os-browserify/browser') }
- : undefined,
- mainFields: target === 'webworker' ? ['browser', 'module', 'main'] : ['module', 'main'],
- extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
- },
- plugins: plugins,
- infrastructureLogging:
- mode === 'production'
- ? undefined
- : {
- level: 'log', // enables logging required for problem matchers
- },
- stats: {
- preset: 'errors-warnings',
- assets: true,
- assetsSort: 'name',
- assetsSpace: 100,
- colors: true,
- env: true,
- errorsCount: true,
- warningsCount: true,
- timings: true,
- },
- };
- }
-
- /**
- * @param { 'production' | 'development' | 'none' } mode
- * @param {{ analyzeBundle?: boolean; analyzeDeps?: boolean; esbuild?: boolean; esbuildMinify?: boolean; useSharpForImageOptimization?: boolean }} env
- * @returns { WebpackConfig }
- */
- function getWebviewsConfig(mode, env) {
- const basePath = path.join(__dirname, 'src', 'webviews', 'apps');
-
- /** @type WebpackConfig['plugins'] | any */
- const plugins = [
- new CleanPlugin(
- mode === 'production'
- ? {
- cleanOnceBeforeBuildPatterns: [
- path.posix.join(__dirname.replace(/\\/g, '/'), 'dist', 'webviews', 'media', '**'),
- ],
- dangerouslyAllowCleanPatternsOutsideProject: true,
- dry: false,
- }
- : undefined,
- ),
- new DefinePlugin({
- DEBUG: mode === 'development',
- }),
- new ForkTsCheckerPlugin({
- async: false,
- eslint: {
- enabled: true,
- files: path.join(basePath, '**', '*.ts?(x)'),
- options: {
- cache: true,
- cacheLocation: path.join(__dirname, '.eslintcache', 'webviews/'),
- cacheStrategy: 'content',
- fix: mode !== 'production',
- },
- },
- formatter: 'basic',
- typescript: {
- configFile: path.join(basePath, 'tsconfig.json'),
- },
- }),
- new WebpackRequireFromPlugin({
- variableName: 'webpackResourceBasePath',
- }),
- new MiniCssExtractPlugin({ filename: '[name].css' }),
- getHtmlPlugin('commitDetails', false, mode, env),
- getHtmlPlugin('graph', true, mode, env),
- getHtmlPlugin('home', false, mode, env),
- getHtmlPlugin('rebase', false, mode, env),
- getHtmlPlugin('settings', false, mode, env),
- getHtmlPlugin('timeline', true, mode, env),
- getHtmlPlugin('welcome', false, mode, env),
- getHtmlPlugin('focus', true, mode, env),
- getHtmlPlugin('account', true, mode, env),
- getCspHtmlPlugin(mode, env),
- new InlineChunkHtmlPlugin(HtmlPlugin, mode === 'production' ? ['\\.css$'] : []),
- new CopyPlugin({
- patterns: [
- {
- from: path.posix.join(basePath.replace(/\\/g, '/'), 'media', '*.*'),
- to: path.posix.join(__dirname.replace(/\\/g, '/'), 'dist', 'webviews'),
- },
- {
- from: path.posix.join(
- __dirname.replace(/\\/g, '/'),
- 'node_modules',
- '@vscode',
- 'codicons',
- 'dist',
- 'codicon.ttf',
- ),
- to: path.posix.join(__dirname.replace(/\\/g, '/'), 'dist', 'webviews'),
- },
- ],
- }),
- ];
-
- const imageGeneratorConfig = getImageMinimizerConfig(mode, env);
-
- if (mode !== 'production') {
- plugins.push(
- new ImageMinimizerPlugin({
- deleteOriginalAssets: true,
- generator: [imageGeneratorConfig],
- }),
- );
- }
-
- if (env.analyzeBundle) {
- const out = path.join(__dirname, 'out');
- if (!fs.existsSync(out)) {
- fs.mkdirSync(out);
- }
-
- plugins.push(
- new BundleAnalyzerPlugin({
- analyzerMode: 'static',
- generateStatsFile: true,
- openAnalyzer: false,
- reportFilename: path.join(out, 'webview-bundle-report.html'),
- statsFilename: path.join(out, 'stats.json'),
- }),
- );
- }
-
- return {
- name: 'webviews',
- context: basePath,
- entry: {
- commitDetails: './commitDetails/commitDetails.ts',
- graph: './plus/graph/graph.tsx',
- home: './home/home.ts',
- rebase: './rebase/rebase.ts',
- settings: './settings/settings.ts',
- timeline: './plus/timeline/timeline.ts',
- welcome: './welcome/welcome.ts',
- focus: './plus/focus/focus.ts',
- account: './plus/account/account.ts',
- },
- mode: mode,
- target: 'web',
- devtool: mode === 'production' ? false : 'source-map',
- output: {
- chunkFilename: 'feature-[name].js',
- filename: '[name].js',
- libraryTarget: 'module',
- path: path.join(__dirname, 'dist', 'webviews'),
- publicPath: '#{root}/dist/webviews/',
- },
- experiments: {
- outputModule: true,
- },
- optimization: {
- minimizer:
- mode === 'production'
- ? [
- env.esbuildMinify
- ? new EsbuildPlugin({
- css: true,
- drop: ['debugger', 'console'],
- format: 'esm',
- // Keep the class names otherwise @log won't provide a useful name
- // keepNames: true,
- legalComments: 'none',
- minify: true,
- target: 'es2022',
- treeShaking: true,
- })
- : new TerserPlugin({
- extractComments: false,
- parallel: true,
- terserOptions: {
- compress: {
- drop_debugger: true,
- drop_console: true,
- ecma: 2020,
- module: true,
- },
- ecma: 2020,
- format: {
- comments: false,
- ecma: 2020,
- },
- // // Keep the class names otherwise @log won't provide a useful name
- // keep_classnames: true,
- module: true,
- },
- }),
- new ImageMinimizerPlugin({
- deleteOriginalAssets: true,
- generator: [imageGeneratorConfig],
- }),
- new CssMinimizerPlugin({
- minimizerOptions: {
- preset: [
- 'cssnano-preset-advanced',
- { discardUnused: false, mergeIdents: false, reduceIdents: false },
- ],
- },
- }),
- ]
- : [],
- splitChunks: {
- // Disable all non-async code splitting
- // chunks: () => false,
- cacheGroups: {
- default: false,
- vendors: false,
- },
- },
- },
- module: {
- rules: [
- {
- test: /\.m?js/,
- resolve: { fullySpecified: false },
- },
- {
- exclude: /\.d\.ts$/,
- include: path.join(__dirname, 'src'),
- test: /\.tsx?$/,
- use: env.esbuild
- ? {
- loader: 'esbuild-loader',
- options: {
- format: 'esm',
- implementation: esbuild,
- target: 'es2021',
- tsconfig: path.join(basePath, 'tsconfig.json'),
- },
- }
- : {
- loader: 'ts-loader',
- options: {
- configFile: path.join(basePath, 'tsconfig.json'),
- experimentalWatchApi: true,
- transpileOnly: true,
- },
- },
- },
- {
- test: /\.scss$/,
- use: [
- {
- loader: MiniCssExtractPlugin.loader,
- },
- {
- loader: 'css-loader',
- options: {
- sourceMap: mode !== 'production',
- url: false,
- },
- },
- {
- loader: 'sass-loader',
- options: {
- sourceMap: mode !== 'production',
- },
- },
- ],
- exclude: /node_modules/,
- },
- ],
- },
- resolve: {
- alias: {
- '@env': path.resolve(__dirname, 'src', 'env', 'browser'),
- '@microsoft/fast-foundation': path.resolve(
- __dirname,
- 'node_modules/@microsoft/fast-foundation/dist/esm/index.js',
- ),
- '@microsoft/fast-react-wrapper': path.resolve(
- __dirname,
- 'node_modules/@microsoft/fast-react-wrapper/dist/esm/index.js',
- ),
- react: path.resolve(__dirname, 'node_modules', 'react'),
- 'react-dom': path.resolve(__dirname, 'node_modules', 'react-dom'),
- tslib: path.resolve(__dirname, 'node_modules/tslib/tslib.es6.js'),
- },
- extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
- modules: [basePath, 'node_modules'],
- },
- plugins: plugins,
- infrastructureLogging:
- mode === 'production'
- ? undefined
- : {
- level: 'log', // enables logging required for problem matchers
- },
- stats: {
- preset: 'errors-warnings',
- assets: true,
- assetsSort: 'name',
- assetsSpace: 100,
- colors: true,
- env: true,
- errorsCount: true,
- excludeAssets: [/\.(ttf|webp)/],
- warningsCount: true,
- timings: true,
- },
- };
- }
-
- /**
- * @param { 'production' | 'development' | 'none' } mode
- * @param {{ analyzeBundle?: boolean; analyzeDeps?: boolean; esbuild?: boolean; useSharpForImageOptimization?: boolean } | undefined } env
- * @returns { CspHtmlPlugin }
- */
- function getCspHtmlPlugin(mode, env) {
- const cspPlugin = new CspHtmlPlugin(
- {
- 'default-src': "'none'",
- 'img-src': ['#{cspSource}', 'https:', 'data:'],
- 'script-src':
- mode !== 'production'
- ? ['#{cspSource}', "'nonce-#{cspNonce}'", "'unsafe-eval'"]
- : ['#{cspSource}', "'nonce-#{cspNonce}'"],
- 'style-src':
- mode === 'production'
- ? ['#{cspSource}', "'nonce-#{cspNonce}'", "'unsafe-hashes'"]
- : ['#{cspSource}', "'unsafe-hashes'", "'unsafe-inline'"],
- 'font-src': ['#{cspSource}'],
- },
- {
- enabled: true,
- hashingMethod: 'sha256',
- hashEnabled: {
- 'script-src': true,
- 'style-src': mode === 'production',
- },
- nonceEnabled: {
- 'script-src': true,
- 'style-src': mode === 'production',
- },
- },
- );
- // Override the nonce creation so we can dynamically generate them at runtime
- // @ts-ignore
- cspPlugin.createNonce = () => '#{cspNonce}';
-
- return cspPlugin;
- }
-
- /**
- * @param { 'production' | 'development' | 'none' } mode
- * @param {{ analyzeBundle?: boolean; analyzeDeps?: boolean; esbuild?: boolean; useSharpForImageOptimization?: boolean } | undefined } env
- * @returns { ImageMinimizerPlugin.Generator<any> }
- */
- function getImageMinimizerConfig(mode, env) {
- /** @type ImageMinimizerPlugin.Generator<any> */
- // @ts-ignore
- return env.useSharpForImageOptimization
- ? {
- type: 'asset',
- implementation: ImageMinimizerPlugin.sharpGenerate,
- options: {
- encodeOptions: {
- webp: {
- lossless: true,
- },
- },
- },
- }
- : {
- type: 'asset',
- implementation: ImageMinimizerPlugin.imageminGenerate,
- options: {
- plugins: [
- [
- 'imagemin-webp',
- {
- lossless: true,
- nearLossless: 0,
- quality: 100,
- method: mode === 'production' ? 4 : 0,
- },
- ],
- ],
- },
- };
- }
-
- /**
- * @param { string } name
- * @param { boolean } plus
- * @param { 'production' | 'development' | 'none' } mode
- * @param {{ analyzeBundle?: boolean; analyzeDeps?: boolean; esbuild?: boolean; useSharpForImageOptimization?: boolean } | undefined } env
- * @returns { HtmlPlugin }
- */
- function getHtmlPlugin(name, plus, mode, env) {
- return new HtmlPlugin({
- template: plus ? path.join('plus', name, `${name}.html`) : path.join(name, `${name}.html`),
- chunks: [name],
- filename: path.join(__dirname, 'dist', 'webviews', `${name}.html`),
- inject: true,
- scriptLoading: 'module',
- inlineSource: mode === 'production' ? '.css$' : undefined,
- minify:
- mode === 'production'
- ? {
- removeComments: true,
- collapseWhitespace: true,
- removeRedundantAttributes: false,
- useShortDoctype: true,
- removeEmptyAttributes: true,
- removeStyleLinkTypeAttributes: true,
- keepClosingSlash: true,
- minifyCSS: true,
- }
- : false,
- });
- }
-
- class InlineChunkHtmlPlugin {
- constructor(htmlPlugin, patterns) {
- this.htmlPlugin = htmlPlugin;
- this.patterns = patterns;
- }
-
- getInlinedTag(publicPath, assets, tag) {
- if (
- (tag.tagName !== 'script' || !(tag.attributes && tag.attributes.src)) &&
- (tag.tagName !== 'link' || !(tag.attributes && tag.attributes.href))
- ) {
- return tag;
- }
-
- let chunkName = tag.tagName === 'link' ? tag.attributes.href : tag.attributes.src;
- if (publicPath) {
- chunkName = chunkName.replace(publicPath, '');
- }
- if (!this.patterns.some(pattern => chunkName.match(pattern))) {
- return tag;
- }
-
- const asset = assets[chunkName];
- if (asset == null) {
- return tag;
- }
-
- return { tagName: tag.tagName === 'link' ? 'style' : tag.tagName, innerHTML: asset.source(), closeTag: true };
- }
-
- apply(compiler) {
- let publicPath = compiler.options.output.publicPath || '';
- if (publicPath && !publicPath.endsWith('/')) {
- publicPath += '/';
- }
-
- compiler.hooks.compilation.tap('InlineChunkHtmlPlugin', compilation => {
- const getInlinedTagFn = tag => this.getInlinedTag(publicPath, compilation.assets, tag);
- const sortFn = (a, b) => (a.tagName === 'script' ? 1 : -1) - (b.tagName === 'script' ? 1 : -1);
- this.htmlPlugin.getHooks(compilation).alterAssetTagGroups.tap('InlineChunkHtmlPlugin', assets => {
- assets.headTags = assets.headTags.map(getInlinedTagFn).sort(sortFn);
- assets.bodyTags = assets.bodyTags.map(getInlinedTagFn).sort(sortFn);
- });
- });
- }
- }
-
- const schema = {
- type: 'object',
- properties: {
- config: {
- type: 'object',
- },
- configPath: {
- type: 'string',
- },
- onBefore: {
- instanceof: 'Function',
- },
- onComplete: {
- instanceof: 'Function',
- },
- },
- };
-
- class FantasticonPlugin {
- alreadyRun = false;
-
- constructor(options = {}) {
- this.pluginName = 'fantasticon';
- this.options = options;
-
- validate(
- // @ts-ignore
- schema,
- options,
- {
- name: this.pluginName,
- baseDataPath: 'options',
- },
- );
- }
-
- /**
- * @param {import("webpack").Compiler} compiler
- */
- apply(compiler) {
- const {
- config = undefined,
- configPath = undefined,
- onBefore = undefined,
- onComplete = undefined,
- } = this.options;
-
- let loadedConfig;
- if (configPath) {
- try {
- loadedConfig = require(path.join(__dirname, configPath));
- } catch (ex) {
- console.error(`[${this.pluginName}] Error loading configuration: ${ex}`);
- }
- }
-
- if (!loadedConfig && !config) {
- console.error(`[${this.pluginName}] Error loading configuration: no configuration found`);
- return;
- }
-
- const fontConfig = { ...(loadedConfig ?? {}), ...(config ?? {}) };
-
- // TODO@eamodio: Figure out how to add watching for the fontConfig.inputDir
- // Maybe something like: https://github.com/Fridus/webpack-watch-files-plugin
-
- /**
- * @this {FantasticonPlugin}
- * @param {import("webpack").Compiler} compiler
- */
- async function generate(compiler) {
- if (compiler.watchMode) {
- if (this.alreadyRun) return;
- this.alreadyRun = true;
- }
-
- const logger = compiler.getInfrastructureLogger(this.pluginName);
- logger.log(`Generating icon font...`);
- await onBefore?.(fontConfig);
- await generateFonts(fontConfig);
- await onComplete?.(fontConfig);
- logger.log(`Generated icon font`);
- }
-
- compiler.hooks.beforeRun.tapPromise(this.pluginName, generate.bind(this));
- compiler.hooks.watchRun.tapPromise(this.pluginName, generate.bind(this));
- }
- }
|