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.

486 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 { ESBuildPlugin, 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.esbuild) {
  93. plugins.push(new ESBuildPlugin());
  94. }
  95. if (env.analyzeDeps) {
  96. plugins.push(
  97. new CircularDependencyPlugin({
  98. cwd: __dirname,
  99. exclude: /node_modules/,
  100. failOnError: false,
  101. onDetected: function ({ module: _webpackModuleRecord, paths, compilation }) {
  102. if (paths.some(p => p.includes('container.ts'))) return;
  103. compilation.warnings.push(new Error(paths.join(' -> ')));
  104. },
  105. }),
  106. );
  107. }
  108. if (env.analyzeBundle) {
  109. plugins.push(new BundleAnalyzerPlugin());
  110. }
  111. return {
  112. name: 'extension',
  113. entry: './src/extension.ts',
  114. mode: mode,
  115. target: 'node',
  116. node: {
  117. __dirname: false,
  118. },
  119. devtool: 'source-map',
  120. output: {
  121. path: path.join(__dirname, 'dist'),
  122. libraryTarget: 'commonjs2',
  123. filename: 'gitlens.js',
  124. chunkFilename: 'feature-[name].js',
  125. },
  126. optimization: {
  127. minimizer: [
  128. // @ts-ignore
  129. env.esbuild
  130. ? new ESBuildMinifyPlugin({
  131. format: 'cjs',
  132. minify: true,
  133. treeShaking: true,
  134. // Keep the class names otherwise @log won't provide a useful name
  135. keepNames: true,
  136. target: 'es2019',
  137. })
  138. : new TerserPlugin({
  139. parallel: true,
  140. terserOptions: {
  141. ecma: 2019,
  142. // Keep the class names otherwise @log won't provide a useful name
  143. keep_classnames: true,
  144. module: true,
  145. },
  146. }),
  147. ],
  148. splitChunks: {
  149. cacheGroups: {
  150. defaultVendors: false,
  151. },
  152. },
  153. },
  154. externals: {
  155. vscode: 'commonjs vscode',
  156. },
  157. module: {
  158. rules: [
  159. {
  160. exclude: /\.d\.ts$/,
  161. include: path.join(__dirname, 'src'),
  162. test: /\.tsx?$/,
  163. use: env.esbuild
  164. ? {
  165. loader: 'esbuild-loader',
  166. options: {
  167. loader: 'ts',
  168. target: 'es2019',
  169. tsconfigRaw: require('./tsconfig.json'),
  170. },
  171. }
  172. : {
  173. loader: 'ts-loader',
  174. options: {
  175. experimentalWatchApi: true,
  176. transpileOnly: true,
  177. },
  178. },
  179. },
  180. ],
  181. },
  182. resolve: {
  183. alias: {
  184. 'universal-user-agent': path.join(
  185. __dirname,
  186. 'node_modules',
  187. 'universal-user-agent',
  188. 'dist-node',
  189. 'index.js',
  190. ),
  191. },
  192. extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
  193. symlinks: false,
  194. },
  195. plugins: plugins,
  196. stats: {
  197. preset: 'errors-warnings',
  198. assets: true,
  199. colors: true,
  200. env: true,
  201. errorsCount: true,
  202. warningsCount: true,
  203. timings: true,
  204. },
  205. };
  206. }
  207. /**
  208. * @param { 'production' | 'development' | 'none' } mode
  209. * @param {{ analyzeBundle?: boolean; analyzeDeps?: boolean; esbuild?: boolean; }} env
  210. * @returns { WebpackConfig }
  211. */
  212. function getWebviewsConfig(mode, env) {
  213. const basePath = path.join(__dirname, 'src', 'webviews', 'apps');
  214. const cspPolicy = {
  215. 'default-src': "'none'",
  216. 'img-src': ['#{cspSource}', 'https:', 'data:'],
  217. 'script-src': ['#{cspSource}', "'nonce-Z2l0bGVucy1ib290c3RyYXA='"],
  218. 'style-src': ['#{cspSource}', "'nonce-Z2l0bGVucy1ib290c3RyYXA='"],
  219. 'font-src': ['#{cspSource}'],
  220. };
  221. if (mode !== 'production') {
  222. cspPolicy['script-src'].push("'unsafe-eval'");
  223. }
  224. /**
  225. * @type WebpackConfig['plugins'] | any
  226. */
  227. const plugins = [
  228. new CleanPlugin(
  229. mode === 'production'
  230. ? {
  231. cleanOnceBeforeBuildPatterns: [
  232. path.posix.join(__dirname.replace(/\\/g, '/'), 'images', 'settings', '**'),
  233. ],
  234. dangerouslyAllowCleanPatternsOutsideProject: true,
  235. dry: false,
  236. }
  237. : undefined,
  238. ),
  239. new ForkTsCheckerPlugin({
  240. async: false,
  241. eslint: {
  242. enabled: true,
  243. files: path.join(basePath, '**', '*.ts'),
  244. options: { cache: true },
  245. },
  246. formatter: 'basic',
  247. typescript: {
  248. configFile: path.join(basePath, 'tsconfig.json'),
  249. },
  250. }),
  251. new MiniCssExtractPlugin({
  252. filename: '[name].css',
  253. }),
  254. new HtmlPlugin({
  255. template: 'rebase/rebase.html',
  256. chunks: ['rebase'],
  257. filename: path.join(__dirname, 'dist', 'webviews', 'rebase.html'),
  258. inject: true,
  259. inlineSource: mode === 'production' ? '.css$' : undefined,
  260. cspPlugin: {
  261. enabled: true,
  262. policy: cspPolicy,
  263. nonceEnabled: {
  264. 'script-src': true,
  265. 'style-src': true,
  266. },
  267. },
  268. minify:
  269. mode === 'production'
  270. ? {
  271. removeComments: true,
  272. collapseWhitespace: true,
  273. removeRedundantAttributes: false,
  274. useShortDoctype: true,
  275. removeEmptyAttributes: true,
  276. removeStyleLinkTypeAttributes: true,
  277. keepClosingSlash: true,
  278. minifyCSS: true,
  279. }
  280. : false,
  281. }),
  282. new HtmlPlugin({
  283. template: 'settings/settings.html',
  284. chunks: ['settings'],
  285. filename: path.join(__dirname, 'dist', 'webviews', 'settings.html'),
  286. inject: true,
  287. inlineSource: mode === 'production' ? '.css$' : undefined,
  288. cspPlugin: {
  289. enabled: true,
  290. policy: cspPolicy,
  291. nonceEnabled: {
  292. 'script-src': true,
  293. 'style-src': true,
  294. },
  295. },
  296. minify:
  297. mode === 'production'
  298. ? {
  299. removeComments: true,
  300. collapseWhitespace: true,
  301. removeRedundantAttributes: false,
  302. useShortDoctype: true,
  303. removeEmptyAttributes: true,
  304. removeStyleLinkTypeAttributes: true,
  305. keepClosingSlash: true,
  306. minifyCSS: true,
  307. }
  308. : false,
  309. }),
  310. new HtmlPlugin({
  311. template: 'welcome/welcome.html',
  312. chunks: ['welcome'],
  313. filename: path.join(__dirname, 'dist', 'webviews', 'welcome.html'),
  314. inject: true,
  315. inlineSource: mode === 'production' ? '.css$' : undefined,
  316. cspPlugin: {
  317. enabled: true,
  318. policy: cspPolicy,
  319. nonceEnabled: {
  320. 'script-src': true,
  321. 'style-src': true,
  322. },
  323. },
  324. minify:
  325. mode === 'production'
  326. ? {
  327. removeComments: true,
  328. collapseWhitespace: true,
  329. removeRedundantAttributes: false,
  330. useShortDoctype: true,
  331. removeEmptyAttributes: true,
  332. removeStyleLinkTypeAttributes: true,
  333. keepClosingSlash: true,
  334. minifyCSS: true,
  335. }
  336. : false,
  337. }),
  338. new CspHtmlPlugin(),
  339. new InlineChunkHtmlPlugin(HtmlPlugin, mode === 'production' ? ['\\.css$'] : []),
  340. new CopyPlugin({
  341. patterns: [
  342. {
  343. from: path.posix.join(basePath.replace(/\\/g, '/'), 'images', 'settings', '*.png'),
  344. to: __dirname.replace(/\\/g, '/'),
  345. },
  346. {
  347. from: path.posix.join(
  348. __dirname.replace(/\\/g, '/'),
  349. 'node_modules',
  350. 'vscode-codicons',
  351. 'dist',
  352. 'codicon.ttf',
  353. ),
  354. to: path.posix.join(__dirname.replace(/\\/g, '/'), 'dist', 'webviews'),
  355. },
  356. ],
  357. }),
  358. new ImageMinimizerPlugin({
  359. test: /\.(png)$/i,
  360. filename: '[path][name].webp',
  361. loader: false,
  362. deleteOriginalAssets: true,
  363. minimizerOptions: {
  364. plugins: [
  365. [
  366. 'imagemin-webp',
  367. {
  368. lossless: true,
  369. nearLossless: 0,
  370. quality: 100,
  371. method: mode === 'production' ? 4 : 0,
  372. },
  373. ],
  374. ],
  375. },
  376. }),
  377. ];
  378. if (env.esbuild) {
  379. plugins.push(new ESBuildPlugin());
  380. }
  381. return {
  382. name: 'webviews',
  383. context: basePath,
  384. entry: {
  385. rebase: './rebase/rebase.ts',
  386. settings: './settings/settings.ts',
  387. welcome: './welcome/welcome.ts',
  388. },
  389. mode: mode,
  390. target: 'web',
  391. devtool: 'source-map',
  392. output: {
  393. filename: '[name].js',
  394. path: path.join(__dirname, 'dist', 'webviews'),
  395. publicPath: '#{root}/dist/webviews/',
  396. },
  397. module: {
  398. rules: [
  399. {
  400. exclude: /\.d\.ts$/,
  401. include: path.join(__dirname, 'src'),
  402. test: /\.tsx?$/,
  403. use: env.esbuild
  404. ? {
  405. loader: 'esbuild-loader',
  406. options: {
  407. loader: 'ts',
  408. target: 'es2019',
  409. // eslint-disable-next-line import/no-dynamic-require
  410. tsconfigRaw: require(path.join(basePath, 'tsconfig.json')),
  411. },
  412. }
  413. : {
  414. loader: 'ts-loader',
  415. options: {
  416. configFile: path.join(basePath, 'tsconfig.json'),
  417. experimentalWatchApi: true,
  418. transpileOnly: true,
  419. },
  420. },
  421. },
  422. {
  423. test: /\.scss$/,
  424. use: [
  425. {
  426. loader: MiniCssExtractPlugin.loader,
  427. },
  428. {
  429. loader: 'css-loader',
  430. options: {
  431. sourceMap: true,
  432. url: false,
  433. },
  434. },
  435. {
  436. loader: 'sass-loader',
  437. options: {
  438. sourceMap: true,
  439. },
  440. },
  441. ],
  442. exclude: /node_modules/,
  443. },
  444. ],
  445. },
  446. resolve: {
  447. extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
  448. modules: [basePath, 'node_modules'],
  449. symlinks: false,
  450. },
  451. plugins: plugins,
  452. stats: {
  453. preset: 'errors-warnings',
  454. assets: true,
  455. colors: true,
  456. env: true,
  457. errorsCount: true,
  458. warningsCount: true,
  459. timings: true,
  460. },
  461. };
  462. }