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.

478 lines
12 KiB

5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
  1. //@ts-check
  2. /** @typedef {import('webpack').Configuration} WebpackConfig **/
  3. /* eslint-disable @typescript-eslint/ban-ts-comment */
  4. /* eslint-disable @typescript-eslint/no-var-requires */
  5. /* eslint-disable @typescript-eslint/strict-boolean-expressions */
  6. /* eslint-disable @typescript-eslint/prefer-optional-chain */
  7. 'use strict';
  8. const path = require('path');
  9. const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
  10. const { CleanWebpackPlugin: CleanPlugin } = require('clean-webpack-plugin');
  11. const CircularDependencyPlugin = require('circular-dependency-plugin');
  12. const CopyPlugin = require('copy-webpack-plugin');
  13. const CspHtmlPlugin = require('csp-html-webpack-plugin');
  14. const { ESBuildMinifyPlugin } = require('esbuild-loader');
  15. const ForkTsCheckerPlugin = require('fork-ts-checker-webpack-plugin');
  16. const HtmlPlugin = require('html-webpack-plugin');
  17. const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
  18. const MiniCssExtractPlugin = require('mini-css-extract-plugin');
  19. const TerserPlugin = require('terser-webpack-plugin');
  20. class InlineChunkHtmlPlugin {
  21. constructor(htmlPlugin, patterns) {
  22. this.htmlPlugin = htmlPlugin;
  23. this.patterns = patterns;
  24. }
  25. getInlinedTag(publicPath, assets, tag) {
  26. if (
  27. (tag.tagName !== 'script' || !(tag.attributes && tag.attributes.src)) &&
  28. (tag.tagName !== 'link' || !(tag.attributes && tag.attributes.href))
  29. ) {
  30. return tag;
  31. }
  32. let chunkName = tag.tagName === 'link' ? tag.attributes.href : tag.attributes.src;
  33. if (publicPath) {
  34. chunkName = chunkName.replace(publicPath, '');
  35. }
  36. if (!this.patterns.some(pattern => chunkName.match(pattern))) {
  37. return tag;
  38. }
  39. const asset = assets[chunkName];
  40. if (asset == null) {
  41. return tag;
  42. }
  43. return { tagName: tag.tagName === 'link' ? 'style' : tag.tagName, innerHTML: asset.source(), closeTag: true };
  44. }
  45. apply(compiler) {
  46. let publicPath = compiler.options.output.publicPath || '';
  47. if (publicPath && !publicPath.endsWith('/')) {
  48. publicPath += '/';
  49. }
  50. compiler.hooks.compilation.tap('InlineChunkHtmlPlugin', compilation => {
  51. const getInlinedTagFn = tag => this.getInlinedTag(publicPath, compilation.assets, tag);
  52. this.htmlPlugin.getHooks(compilation).alterAssetTagGroups.tap('InlineChunkHtmlPlugin', assets => {
  53. assets.headTags = assets.headTags.map(getInlinedTagFn);
  54. assets.bodyTags = assets.bodyTags.map(getInlinedTagFn);
  55. });
  56. });
  57. }
  58. }
  59. module.exports =
  60. /**
  61. * @param {{ analyzeBundle?: boolean; analyzeDeps?: boolean; esbuild?: boolean; } | undefined } env
  62. * @param {{ mode: 'production' | 'development' | 'none' | undefined; }} argv
  63. * @returns { WebpackConfig[] }
  64. */
  65. function (env, argv) {
  66. const mode = argv.mode || 'none';
  67. env = {
  68. analyzeBundle: false,
  69. analyzeDeps: false,
  70. esbuild: false,
  71. ...env,
  72. };
  73. return [getExtensionConfig(mode, env), getWebviewsConfig(mode, env)];
  74. };
  75. /**
  76. * @param { 'production' | 'development' | 'none' } mode
  77. * @param {{ analyzeBundle?: boolean; analyzeDeps?: boolean; esbuild?: boolean; }} env
  78. * @returns { WebpackConfig }
  79. */
  80. function getExtensionConfig(mode, env) {
  81. /**
  82. * @type WebpackConfig['plugins'] | any
  83. */
  84. const plugins = [
  85. new CleanPlugin({ cleanOnceBeforeBuildPatterns: ['!webviews/**'] }),
  86. new ForkTsCheckerPlugin({
  87. async: false,
  88. eslint: { enabled: true, files: 'src/**/*.ts', options: { cache: true } },
  89. formatter: 'basic',
  90. }),
  91. ];
  92. if (env.analyzeDeps) {
  93. plugins.push(
  94. new CircularDependencyPlugin({
  95. cwd: __dirname,
  96. exclude: /node_modules/,
  97. failOnError: false,
  98. onDetected: function ({ module: _webpackModuleRecord, paths, compilation }) {
  99. if (paths.some(p => p.includes('container.ts'))) return;
  100. compilation.warnings.push(new Error(paths.join(' -> ')));
  101. },
  102. }),
  103. );
  104. }
  105. if (env.analyzeBundle) {
  106. plugins.push(new BundleAnalyzerPlugin());
  107. }
  108. return {
  109. name: 'extension',
  110. entry: './src/extension.ts',
  111. mode: mode,
  112. target: 'node',
  113. node: {
  114. __dirname: false,
  115. },
  116. devtool: 'source-map',
  117. output: {
  118. path: path.join(__dirname, 'dist'),
  119. libraryTarget: 'commonjs2',
  120. filename: 'gitlens.js',
  121. chunkFilename: 'feature-[name].js',
  122. },
  123. optimization: {
  124. minimizer: [
  125. // @ts-ignore
  126. env.esbuild
  127. ? new ESBuildMinifyPlugin({
  128. format: 'cjs',
  129. minify: true,
  130. treeShaking: true,
  131. // Keep the class names otherwise @log won't provide a useful name
  132. keepNames: true,
  133. target: 'es2019',
  134. })
  135. : new TerserPlugin({
  136. parallel: true,
  137. terserOptions: {
  138. ecma: 2019,
  139. // Keep the class names otherwise @log won't provide a useful name
  140. keep_classnames: true,
  141. module: true,
  142. },
  143. }),
  144. ],
  145. splitChunks: {
  146. cacheGroups: {
  147. defaultVendors: false,
  148. },
  149. },
  150. },
  151. externals: {
  152. vscode: 'commonjs vscode',
  153. },
  154. module: {
  155. rules: [
  156. {
  157. exclude: /\.d\.ts$/,
  158. include: path.join(__dirname, 'src'),
  159. test: /\.tsx?$/,
  160. use: env.esbuild
  161. ? {
  162. loader: 'esbuild-loader',
  163. options: {
  164. loader: 'ts',
  165. target: 'es2019',
  166. tsconfigRaw: require('./tsconfig.json'),
  167. },
  168. }
  169. : {
  170. loader: 'ts-loader',
  171. options: {
  172. experimentalWatchApi: true,
  173. transpileOnly: true,
  174. },
  175. },
  176. },
  177. ],
  178. },
  179. resolve: {
  180. alias: {
  181. 'universal-user-agent': path.join(
  182. __dirname,
  183. 'node_modules',
  184. 'universal-user-agent',
  185. 'dist-node',
  186. 'index.js',
  187. ),
  188. },
  189. extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
  190. symlinks: false,
  191. },
  192. plugins: plugins,
  193. stats: {
  194. preset: 'errors-warnings',
  195. assets: true,
  196. colors: true,
  197. env: true,
  198. errorsCount: true,
  199. warningsCount: true,
  200. timings: true,
  201. },
  202. };
  203. }
  204. /**
  205. * @param { 'production' | 'development' | 'none' } mode
  206. * @param {{ analyzeBundle?: boolean; analyzeDeps?: boolean; esbuild?: boolean; }} env
  207. * @returns { WebpackConfig }
  208. */
  209. function getWebviewsConfig(mode, env) {
  210. const basePath = path.join(__dirname, 'src', 'webviews', 'apps');
  211. const cspPolicy = {
  212. 'default-src': "'none'",
  213. 'img-src': ['#{cspSource}', 'https:', 'data:'],
  214. 'script-src': ['#{cspSource}', "'nonce-Z2l0bGVucy1ib290c3RyYXA='"],
  215. 'style-src': ['#{cspSource}', "'nonce-Z2l0bGVucy1ib290c3RyYXA='"],
  216. 'font-src': ['#{cspSource}'],
  217. };
  218. if (mode !== 'production') {
  219. cspPolicy['script-src'].push("'unsafe-eval'");
  220. }
  221. /**
  222. * @type WebpackConfig['plugins'] | any
  223. */
  224. const plugins = [
  225. new CleanPlugin(
  226. mode === 'production'
  227. ? {
  228. cleanOnceBeforeBuildPatterns: [
  229. path.posix.join(__dirname.replace(/\\/g, '/'), 'images', 'settings', '**'),
  230. ],
  231. dangerouslyAllowCleanPatternsOutsideProject: true,
  232. dry: false,
  233. }
  234. : undefined,
  235. ),
  236. new ForkTsCheckerPlugin({
  237. async: false,
  238. eslint: {
  239. enabled: true,
  240. files: path.join(basePath, '**', '*.ts'),
  241. options: { cache: true },
  242. },
  243. formatter: 'basic',
  244. typescript: {
  245. configFile: path.join(basePath, 'tsconfig.json'),
  246. },
  247. }),
  248. new MiniCssExtractPlugin({
  249. filename: '[name].css',
  250. }),
  251. new HtmlPlugin({
  252. template: 'rebase/rebase.html',
  253. chunks: ['rebase'],
  254. filename: path.join(__dirname, 'dist', 'webviews', 'rebase.html'),
  255. inject: true,
  256. inlineSource: mode === 'production' ? '.css$' : undefined,
  257. cspPlugin: {
  258. enabled: true,
  259. policy: cspPolicy,
  260. nonceEnabled: {
  261. 'script-src': true,
  262. 'style-src': true,
  263. },
  264. },
  265. minify:
  266. mode === 'production'
  267. ? {
  268. removeComments: true,
  269. collapseWhitespace: true,
  270. removeRedundantAttributes: false,
  271. useShortDoctype: true,
  272. removeEmptyAttributes: true,
  273. removeStyleLinkTypeAttributes: true,
  274. keepClosingSlash: true,
  275. minifyCSS: true,
  276. }
  277. : false,
  278. }),
  279. new HtmlPlugin({
  280. template: 'settings/settings.html',
  281. chunks: ['settings'],
  282. filename: path.join(__dirname, 'dist', 'webviews', 'settings.html'),
  283. inject: true,
  284. inlineSource: mode === 'production' ? '.css$' : undefined,
  285. cspPlugin: {
  286. enabled: true,
  287. policy: cspPolicy,
  288. nonceEnabled: {
  289. 'script-src': true,
  290. 'style-src': true,
  291. },
  292. },
  293. minify:
  294. mode === 'production'
  295. ? {
  296. removeComments: true,
  297. collapseWhitespace: true,
  298. removeRedundantAttributes: false,
  299. useShortDoctype: true,
  300. removeEmptyAttributes: true,
  301. removeStyleLinkTypeAttributes: true,
  302. keepClosingSlash: true,
  303. minifyCSS: true,
  304. }
  305. : false,
  306. }),
  307. new HtmlPlugin({
  308. template: 'welcome/welcome.html',
  309. chunks: ['welcome'],
  310. filename: path.join(__dirname, 'dist', 'webviews', 'welcome.html'),
  311. inject: true,
  312. inlineSource: mode === 'production' ? '.css$' : undefined,
  313. cspPlugin: {
  314. enabled: true,
  315. policy: cspPolicy,
  316. nonceEnabled: {
  317. 'script-src': true,
  318. 'style-src': true,
  319. },
  320. },
  321. minify:
  322. mode === 'production'
  323. ? {
  324. removeComments: true,
  325. collapseWhitespace: true,
  326. removeRedundantAttributes: false,
  327. useShortDoctype: true,
  328. removeEmptyAttributes: true,
  329. removeStyleLinkTypeAttributes: true,
  330. keepClosingSlash: true,
  331. minifyCSS: true,
  332. }
  333. : false,
  334. }),
  335. new CspHtmlPlugin(),
  336. new InlineChunkHtmlPlugin(HtmlPlugin, mode === 'production' ? ['\\.css$'] : []),
  337. new CopyPlugin({
  338. patterns: [
  339. {
  340. from: path.posix.join(basePath.replace(/\\/g, '/'), 'images', 'settings', '*.png'),
  341. to: __dirname.replace(/\\/g, '/'),
  342. },
  343. {
  344. from: path.posix.join(
  345. __dirname.replace(/\\/g, '/'),
  346. 'node_modules',
  347. 'vscode-codicons',
  348. 'dist',
  349. 'codicon.ttf',
  350. ),
  351. to: path.posix.join(__dirname.replace(/\\/g, '/'), 'dist', 'webviews'),
  352. },
  353. ],
  354. }),
  355. new ImageMinimizerPlugin({
  356. test: /\.(png)$/i,
  357. filename: '[path][name].webp',
  358. loader: false,
  359. deleteOriginalAssets: true,
  360. minimizerOptions: {
  361. plugins: [
  362. [
  363. 'imagemin-webp',
  364. {
  365. lossless: true,
  366. nearLossless: 0,
  367. quality: 100,
  368. method: mode === 'production' ? 4 : 0,
  369. },
  370. ],
  371. ],
  372. },
  373. }),
  374. ];
  375. return {
  376. name: 'webviews',
  377. context: basePath,
  378. entry: {
  379. rebase: './rebase/rebase.ts',
  380. settings: './settings/settings.ts',
  381. welcome: './welcome/welcome.ts',
  382. },
  383. mode: mode,
  384. target: 'web',
  385. devtool: 'source-map',
  386. output: {
  387. filename: '[name].js',
  388. path: path.join(__dirname, 'dist', 'webviews'),
  389. publicPath: '#{root}/dist/webviews/',
  390. },
  391. module: {
  392. rules: [
  393. {
  394. exclude: /\.d\.ts$/,
  395. include: path.join(__dirname, 'src'),
  396. test: /\.tsx?$/,
  397. use: env.esbuild
  398. ? {
  399. loader: 'esbuild-loader',
  400. options: {
  401. loader: 'ts',
  402. target: 'es2019',
  403. // eslint-disable-next-line import/no-dynamic-require
  404. tsconfigRaw: require(path.join(basePath, 'tsconfig.json')),
  405. },
  406. }
  407. : {
  408. loader: 'ts-loader',
  409. options: {
  410. configFile: path.join(basePath, 'tsconfig.json'),
  411. experimentalWatchApi: true,
  412. transpileOnly: true,
  413. },
  414. },
  415. },
  416. {
  417. test: /\.scss$/,
  418. use: [
  419. {
  420. loader: MiniCssExtractPlugin.loader,
  421. },
  422. {
  423. loader: 'css-loader',
  424. options: {
  425. sourceMap: true,
  426. url: false,
  427. },
  428. },
  429. {
  430. loader: 'sass-loader',
  431. options: {
  432. sourceMap: true,
  433. },
  434. },
  435. ],
  436. exclude: /node_modules/,
  437. },
  438. ],
  439. },
  440. resolve: {
  441. extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
  442. modules: [basePath, 'node_modules'],
  443. symlinks: false,
  444. },
  445. plugins: plugins,
  446. stats: {
  447. preset: 'errors-warnings',
  448. assets: true,
  449. colors: true,
  450. env: true,
  451. errorsCount: true,
  452. warningsCount: true,
  453. timings: true,
  454. },
  455. };
  456. }