You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

541 lines
14 KiB

//@ts-check
/** @typedef {import('webpack').Configuration} WebpackConfig **/
/* eslint-disable @typescript-eslint/ban-ts-comment */
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-disable @typescript-eslint/strict-boolean-expressions */
/* eslint-disable @typescript-eslint/prefer-optional-chain */
'use strict';
const { execFileSync } = require('child_process');
const path = require('path');
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 esbuild = require('esbuild');
const { ESBuildMinifyPlugin } = require('esbuild-loader');
const ForkTsCheckerPlugin = require('fork-ts-checker-webpack-plugin');
const HtmlPlugin = require('html-webpack-plugin');
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
const JSON5 = require('json5');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const { WebpackError } = require('webpack');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports =
/**
* @param {{ analyzeBundle?: boolean; analyzeDeps?: boolean; esbuild?: 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,
...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; }} env
* @returns { WebpackConfig }
*/
function getExtensionConfig(target, mode, env) {
/**
* @type WebpackConfig['plugins'] | any
*/
const plugins = [
new CleanPlugin({ cleanOnceBeforeBuildPatterns: ['!webviews/**'] }),
new ForkTsCheckerPlugin({
async: false,
eslint: {
enabled: true,
files: 'src/**/*.ts',
options: {
// cache: true,
cacheLocation: path.join(
__dirname,
target === 'webworker' ? '.eslintcache.browser' : '.eslintcache',
),
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 (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) {
plugins.push(new BundleAnalyzerPlugin());
}
return {
name: `extension:${target}`,
entry: {
extension: './src/extension.ts',
},
mode: mode,
target: target,
devtool: 'source-map',
output: {
path: target === 'webworker' ? path.join(__dirname, 'dist', 'browser') : path.join(__dirname, 'dist'),
libraryTarget: 'commonjs2',
filename: 'gitlens.js',
chunkFilename: 'feature-[name].js',
},
optimization: {
minimizer: [
env.esbuild
? new ESBuildMinifyPlugin({
format: 'cjs',
// @ts-ignore
implementation: esbuild,
minify: true,
treeShaking: true,
// Keep the class names otherwise @log won't provide a useful name
keepNames: true,
target: 'es2019',
})
: new TerserPlugin({
extractComments: false,
parallel: true,
terserOptions: {
ecma: 2019,
// Keep the class names otherwise @log won't provide a useful name
keep_classnames: true,
module: true,
},
}),
],
splitChunks: {
cacheGroups: {
defaultVendors: false,
},
},
},
externals: {
vscode: 'commonjs vscode',
},
module: {
rules: [
{
exclude: /\.d\.ts$/,
include: path.join(__dirname, 'src'),
test: /\.tsx?$/,
use: env.esbuild
? {
loader: 'esbuild-loader',
options: {
implementation: esbuild,
loader: 'ts',
target: 'es2020',
tsconfigRaw: resolveTSConfig(
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:
target === 'webworker'
? {
'@env': path.resolve(__dirname, 'src', 'env', 'browser'),
}
: {
'@env': path.resolve(__dirname, 'src', 'env', target),
// 'universal-user-agent': path.join(
// __dirname,
// 'node_modules',
// 'universal-user-agent',
// 'dist-node',
// 'index.js',
// ),
},
fallback:
target === 'webworker'
? {
child_process: false,
crypto: require.resolve('crypto-browserify'),
fs: false,
os: false,
path: require.resolve('path-browserify'),
stream: false,
url: false,
}
: undefined,
mainFields: target === 'webworker' ? ['browser', 'module', 'main'] : ['module', 'main'],
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
},
plugins: plugins,
stats: {
preset: 'errors-warnings',
assets: true,
colors: true,
env: true,
errorsCount: true,
warningsCount: true,
timings: true,
},
};
}
/**
* @param { 'production' | 'development' | 'none' } mode
* @param {{ analyzeBundle?: boolean; analyzeDeps?: boolean; esbuild?: boolean; }} env
* @returns { WebpackConfig }
*/
function getWebviewsConfig(mode, env) {
const basePath = path.join(__dirname, 'src', 'webviews', 'apps');
const cspHtmlPlugin = new CspHtmlPlugin(
{
'default-src': "'none'",
'img-src': ['#{cspSource}', 'https:', 'data:'],
'script-src':
mode !== 'production'
? ['#{cspSource}', "'nonce-#{cspNonce}'", "'unsafe-eval'"]
: ['#{cspSource}', "'nonce-#{cspNonce}'"],
'style-src': ['#{cspSource}', "'nonce-#{cspNonce}'"],
'font-src': ['#{cspSource}'],
},
{
enabled: true,
hashingMethod: 'sha256',
hashEnabled: {
'script-src': true,
'style-src': true,
},
nonceEnabled: {
'script-src': true,
'style-src': true,
},
},
);
// Override the nonce creation so we can dynamically generate them at runtime
// @ts-ignore
cspHtmlPlugin.createNonce = () => '#{cspNonce}';
/**
* @type WebpackConfig['plugins'] | any
*/
const plugins = [
new CleanPlugin(
mode === 'production'
? {
cleanOnceBeforeBuildPatterns: [
path.posix.join(__dirname.replace(/\\/g, '/'), 'images', 'settings', '**'),
],
dangerouslyAllowCleanPatternsOutsideProject: true,
dry: false,
}
: undefined,
),
new ForkTsCheckerPlugin({
async: false,
eslint: {
enabled: true,
files: path.join(basePath, '**', '*.ts'),
options: { cache: true },
},
formatter: 'basic',
typescript: {
configFile: path.join(basePath, 'tsconfig.json'),
},
}),
new MiniCssExtractPlugin({
filename: '[name].css',
}),
new HtmlPlugin({
template: 'rebase/rebase.html',
chunks: ['rebase'],
filename: path.join(__dirname, 'dist', 'webviews', 'rebase.html'),
inject: true,
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,
}),
new HtmlPlugin({
template: 'settings/settings.html',
chunks: ['settings'],
filename: path.join(__dirname, 'dist', 'webviews', 'settings.html'),
inject: true,
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,
}),
new HtmlPlugin({
template: 'welcome/welcome.html',
chunks: ['welcome'],
filename: path.join(__dirname, 'dist', 'webviews', 'welcome.html'),
inject: true,
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,
}),
cspHtmlPlugin,
new InlineChunkHtmlPlugin(HtmlPlugin, mode === 'production' ? ['\\.css$'] : []),
new CopyPlugin({
patterns: [
{
from: path.posix.join(basePath.replace(/\\/g, '/'), 'images', 'settings', '*.png'),
to: __dirname.replace(/\\/g, '/'),
},
{
from: path.posix.join(
__dirname.replace(/\\/g, '/'),
'node_modules',
'@vscode',
'codicons',
'dist',
'codicon.ttf',
),
to: path.posix.join(__dirname.replace(/\\/g, '/'), 'dist', 'webviews'),
},
],
}),
new ImageMinimizerPlugin({
test: /\.(png)$/i,
filename: '[path][name].webp',
loader: false,
deleteOriginalAssets: true,
minimizerOptions: {
plugins: [
[
'imagemin-webp',
{
lossless: true,
nearLossless: 0,
quality: 100,
method: mode === 'production' ? 4 : 0,
},
],
],
},
}),
];
return {
name: 'webviews',
context: basePath,
entry: {
rebase: './rebase/rebase.ts',
settings: './settings/settings.ts',
welcome: './welcome/welcome.ts',
},
mode: mode,
target: 'web',
devtool: 'source-map',
output: {
filename: '[name].js',
path: path.join(__dirname, 'dist', 'webviews'),
publicPath: '#{root}/dist/webviews/',
},
module: {
rules: [
{
exclude: /\.d\.ts$/,
include: path.join(__dirname, 'src'),
test: /\.tsx?$/,
use: env.esbuild
? {
loader: 'esbuild-loader',
options: {
implementation: esbuild,
loader: 'ts',
target: 'es2020',
tsconfigRaw: resolveTSConfig(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: true,
url: false,
},
},
{
loader: 'sass-loader',
options: {
sourceMap: true,
},
},
],
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
modules: [basePath, 'node_modules'],
},
plugins: plugins,
stats: {
preset: 'errors-warnings',
assets: true,
colors: true,
env: true,
errorsCount: true,
warningsCount: true,
timings: true,
},
infrastructureLogging: {
level: 'log', // enables logging required for problem matchers
},
};
}
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);
this.htmlPlugin.getHooks(compilation).alterAssetTagGroups.tap('InlineChunkHtmlPlugin', assets => {
assets.headTags = assets.headTags.map(getInlinedTagFn);
assets.bodyTags = assets.bodyTags.map(getInlinedTagFn);
});
});
}
}
function resolveTSConfig(configFile) {
const data = execFileSync('yarn', ['tsc', `-p ${configFile}`, '--showConfig'], {
cwd: __dirname,
encoding: 'utf8',
shell: true,
});
const index = data.indexOf('\n');
const json = JSON5.parse(data.substr(index + 1));
return json;
}