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.

560 lines
14 KiB

3 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
3 years ago
3 years ago
3 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 { spawnSync } = require('child_process');
  9. const path = require('path');
  10. const CircularDependencyPlugin = require('circular-dependency-plugin');
  11. const { CleanWebpackPlugin: CleanPlugin } = require('clean-webpack-plugin');
  12. const CopyPlugin = require('copy-webpack-plugin');
  13. const CspHtmlPlugin = require('csp-html-webpack-plugin');
  14. const esbuild = require('esbuild');
  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 JSON5 = require('json5');
  19. const MiniCssExtractPlugin = require('mini-css-extract-plugin');
  20. const TerserPlugin = require('terser-webpack-plugin');
  21. const { WebpackError } = require('webpack');
  22. const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
  23. module.exports =
  24. /**
  25. * @param {{ analyzeBundle?: boolean; analyzeDeps?: boolean; esbuild?: boolean; } | undefined } env
  26. * @param {{ mode: 'production' | 'development' | 'none' | undefined; }} argv
  27. * @returns { WebpackConfig[] }
  28. */
  29. function (env, argv) {
  30. const mode = argv.mode || 'none';
  31. env = {
  32. analyzeBundle: false,
  33. analyzeDeps: false,
  34. esbuild: true,
  35. ...env,
  36. };
  37. return [
  38. getExtensionConfig('node', mode, env),
  39. getExtensionConfig('webworker', mode, env),
  40. getWebviewsConfig(mode, env),
  41. ];
  42. };
  43. /**
  44. * @param { 'node' | 'webworker' } target
  45. * @param { 'production' | 'development' | 'none' } mode
  46. * @param {{ analyzeBundle?: boolean; analyzeDeps?: boolean; esbuild?: boolean; }} env
  47. * @returns { WebpackConfig }
  48. */
  49. function getExtensionConfig(target, mode, env) {
  50. /**
  51. * @type WebpackConfig['plugins'] | any
  52. */
  53. const plugins = [
  54. new CleanPlugin({ cleanOnceBeforeBuildPatterns: ['!webviews/**'] }),
  55. new ForkTsCheckerPlugin({
  56. async: false,
  57. eslint: {
  58. enabled: true,
  59. files: 'src/**/*.ts',
  60. options: {
  61. // cache: true,
  62. cacheLocation: path.join(
  63. __dirname,
  64. target === 'webworker' ? '.eslintcache.browser' : '.eslintcache',
  65. ),
  66. overrideConfigFile: path.join(
  67. __dirname,
  68. target === 'webworker' ? '.eslintrc.browser.json' : '.eslintrc.json',
  69. ),
  70. },
  71. },
  72. formatter: 'basic',
  73. typescript: {
  74. configFile: path.join(__dirname, target === 'webworker' ? 'tsconfig.browser.json' : 'tsconfig.json'),
  75. },
  76. }),
  77. ];
  78. if (env.analyzeDeps) {
  79. plugins.push(
  80. new CircularDependencyPlugin({
  81. cwd: __dirname,
  82. exclude: /node_modules/,
  83. failOnError: false,
  84. onDetected: function ({ module: _webpackModuleRecord, paths, compilation }) {
  85. if (paths.some(p => p.includes('container.ts'))) return;
  86. // @ts-ignore
  87. compilation.warnings.push(new WebpackError(paths.join(' -> ')));
  88. },
  89. }),
  90. );
  91. }
  92. if (env.analyzeBundle) {
  93. plugins.push(new BundleAnalyzerPlugin());
  94. }
  95. return {
  96. name: `extension:${target}`,
  97. entry: {
  98. extension: './src/extension.ts',
  99. },
  100. mode: mode,
  101. target: target,
  102. devtool: 'source-map',
  103. output: {
  104. path: target === 'webworker' ? path.join(__dirname, 'dist', 'browser') : path.join(__dirname, 'dist'),
  105. libraryTarget: 'commonjs2',
  106. filename: 'gitlens.js',
  107. chunkFilename: 'feature-[name].js',
  108. },
  109. optimization: {
  110. minimizer: [
  111. env.esbuild
  112. ? new TerserPlugin({
  113. minify: TerserPlugin.esbuildMinify,
  114. terserOptions: {
  115. // @ts-ignore
  116. format: 'cjs',
  117. minify: true,
  118. treeShaking: true,
  119. // Keep the class names otherwise @log won't provide a useful name
  120. keepNames: true,
  121. target: 'es2020',
  122. },
  123. })
  124. : new TerserPlugin({
  125. extractComments: false,
  126. parallel: true,
  127. // @ts-ignore
  128. terserOptions: {
  129. ecma: 2020,
  130. // Keep the class names otherwise @log won't provide a useful name
  131. keep_classnames: true,
  132. module: true,
  133. },
  134. }),
  135. ],
  136. splitChunks: {
  137. cacheGroups: {
  138. defaultVendors: false,
  139. },
  140. },
  141. },
  142. externals: {
  143. vscode: 'commonjs vscode',
  144. },
  145. module: {
  146. rules: [
  147. {
  148. exclude: /\.d\.ts$/,
  149. include: path.join(__dirname, 'src'),
  150. test: /\.tsx?$/,
  151. use: env.esbuild
  152. ? {
  153. loader: 'esbuild-loader',
  154. options: {
  155. implementation: esbuild,
  156. loader: 'ts',
  157. target: ['es2020', 'chrome91', 'node14.16'],
  158. tsconfigRaw: resolveTSConfig(
  159. path.join(
  160. __dirname,
  161. target === 'webworker' ? 'tsconfig.browser.json' : 'tsconfig.json',
  162. ),
  163. ),
  164. },
  165. }
  166. : {
  167. loader: 'ts-loader',
  168. options: {
  169. configFile: path.join(
  170. __dirname,
  171. target === 'webworker' ? 'tsconfig.browser.json' : 'tsconfig.json',
  172. ),
  173. experimentalWatchApi: true,
  174. transpileOnly: true,
  175. },
  176. },
  177. },
  178. ],
  179. },
  180. resolve: {
  181. alias:
  182. target === 'webworker'
  183. ? {
  184. '@env': path.resolve(__dirname, 'src', 'env', 'browser'),
  185. }
  186. : {
  187. '@env': path.resolve(__dirname, 'src', 'env', target),
  188. // 'universal-user-agent': path.join(
  189. // __dirname,
  190. // 'node_modules',
  191. // 'universal-user-agent',
  192. // 'dist-node',
  193. // 'index.js',
  194. // ),
  195. },
  196. fallback:
  197. target === 'webworker'
  198. ? {
  199. child_process: false,
  200. crypto: require.resolve('crypto-browserify'),
  201. fs: false,
  202. os: false,
  203. path: require.resolve('path-browserify'),
  204. process: false,
  205. stream: false,
  206. url: false,
  207. }
  208. : undefined,
  209. mainFields: target === 'webworker' ? ['browser', 'module', 'main'] : ['module', 'main'],
  210. extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
  211. },
  212. plugins: plugins,
  213. stats: {
  214. preset: 'errors-warnings',
  215. assets: true,
  216. colors: true,
  217. env: true,
  218. errorsCount: true,
  219. warningsCount: true,
  220. timings: true,
  221. },
  222. };
  223. }
  224. /**
  225. * @param { 'production' | 'development' | 'none' } mode
  226. * @param {{ analyzeBundle?: boolean; analyzeDeps?: boolean; esbuild?: boolean; }} env
  227. * @returns { WebpackConfig }
  228. */
  229. function getWebviewsConfig(mode, env) {
  230. const basePath = path.join(__dirname, 'src', 'webviews', 'apps');
  231. const cspHtmlPlugin = new CspHtmlPlugin(
  232. {
  233. 'default-src': "'none'",
  234. 'img-src': ['#{cspSource}', 'https:', 'data:'],
  235. 'script-src':
  236. mode !== 'production'
  237. ? ['#{cspSource}', "'nonce-#{cspNonce}'", "'unsafe-eval'"]
  238. : ['#{cspSource}', "'nonce-#{cspNonce}'"],
  239. 'style-src': ['#{cspSource}', "'nonce-#{cspNonce}'"],
  240. 'font-src': ['#{cspSource}'],
  241. },
  242. {
  243. enabled: true,
  244. hashingMethod: 'sha256',
  245. hashEnabled: {
  246. 'script-src': true,
  247. 'style-src': true,
  248. },
  249. nonceEnabled: {
  250. 'script-src': true,
  251. 'style-src': true,
  252. },
  253. },
  254. );
  255. // Override the nonce creation so we can dynamically generate them at runtime
  256. // @ts-ignore
  257. cspHtmlPlugin.createNonce = () => '#{cspNonce}';
  258. /**
  259. * @type WebpackConfig['plugins'] | any
  260. */
  261. const plugins = [
  262. new CleanPlugin(
  263. mode === 'production'
  264. ? {
  265. cleanOnceBeforeBuildPatterns: [
  266. path.posix.join(__dirname.replace(/\\/g, '/'), 'images', 'settings', '**'),
  267. ],
  268. dangerouslyAllowCleanPatternsOutsideProject: true,
  269. dry: false,
  270. }
  271. : undefined,
  272. ),
  273. new ForkTsCheckerPlugin({
  274. async: false,
  275. eslint: {
  276. enabled: true,
  277. files: path.join(basePath, '**', '*.ts'),
  278. // options: { cache: true },
  279. },
  280. formatter: 'basic',
  281. typescript: {
  282. configFile: path.join(basePath, 'tsconfig.json'),
  283. },
  284. }),
  285. new MiniCssExtractPlugin({
  286. filename: '[name].css',
  287. }),
  288. new HtmlPlugin({
  289. template: 'rebase/rebase.html',
  290. chunks: ['rebase'],
  291. filename: path.join(__dirname, 'dist', 'webviews', 'rebase.html'),
  292. inject: true,
  293. inlineSource: mode === 'production' ? '.css$' : undefined,
  294. minify:
  295. mode === 'production'
  296. ? {
  297. removeComments: true,
  298. collapseWhitespace: true,
  299. removeRedundantAttributes: false,
  300. useShortDoctype: true,
  301. removeEmptyAttributes: true,
  302. removeStyleLinkTypeAttributes: true,
  303. keepClosingSlash: true,
  304. minifyCSS: true,
  305. }
  306. : false,
  307. }),
  308. new HtmlPlugin({
  309. template: 'settings/settings.html',
  310. chunks: ['settings'],
  311. filename: path.join(__dirname, 'dist', 'webviews', 'settings.html'),
  312. inject: true,
  313. inlineSource: mode === 'production' ? '.css$' : undefined,
  314. minify:
  315. mode === 'production'
  316. ? {
  317. removeComments: true,
  318. collapseWhitespace: true,
  319. removeRedundantAttributes: false,
  320. useShortDoctype: true,
  321. removeEmptyAttributes: true,
  322. removeStyleLinkTypeAttributes: true,
  323. keepClosingSlash: true,
  324. minifyCSS: true,
  325. }
  326. : false,
  327. }),
  328. new HtmlPlugin({
  329. template: 'welcome/welcome.html',
  330. chunks: ['welcome'],
  331. filename: path.join(__dirname, 'dist', 'webviews', 'welcome.html'),
  332. inject: true,
  333. inlineSource: mode === 'production' ? '.css$' : undefined,
  334. minify:
  335. mode === 'production'
  336. ? {
  337. removeComments: true,
  338. collapseWhitespace: true,
  339. removeRedundantAttributes: false,
  340. useShortDoctype: true,
  341. removeEmptyAttributes: true,
  342. removeStyleLinkTypeAttributes: true,
  343. keepClosingSlash: true,
  344. minifyCSS: true,
  345. }
  346. : false,
  347. }),
  348. cspHtmlPlugin,
  349. new InlineChunkHtmlPlugin(HtmlPlugin, mode === 'production' ? ['\\.css$'] : []),
  350. new CopyPlugin({
  351. patterns: [
  352. {
  353. from: path.posix.join(basePath.replace(/\\/g, '/'), 'images', 'settings', '*.png'),
  354. to: __dirname.replace(/\\/g, '/'),
  355. },
  356. {
  357. from: path.posix.join(
  358. __dirname.replace(/\\/g, '/'),
  359. 'node_modules',
  360. '@vscode',
  361. 'codicons',
  362. 'dist',
  363. 'codicon.ttf',
  364. ),
  365. to: path.posix.join(__dirname.replace(/\\/g, '/'), 'dist', 'webviews'),
  366. },
  367. ],
  368. }),
  369. new ImageMinimizerPlugin({
  370. deleteOriginalAssets: true,
  371. minimizer: {
  372. implementation: ImageMinimizerPlugin.imageminGenerate,
  373. options: {
  374. plugins: [
  375. [
  376. 'imagemin-webp',
  377. {
  378. lossless: true,
  379. nearLossless: 0,
  380. quality: 100,
  381. method: mode === 'production' ? 4 : 0,
  382. },
  383. ],
  384. ],
  385. },
  386. // implementation: ImageMinimizerPlugin.squooshGenerate,
  387. // options: {
  388. // // @ts-ignore
  389. // encodeOptions: {
  390. // webp: {
  391. // // quality: 90,
  392. // lossless: 1,
  393. // },
  394. // },
  395. // },
  396. },
  397. }),
  398. ];
  399. return {
  400. name: 'webviews',
  401. context: basePath,
  402. entry: {
  403. rebase: './rebase/rebase.ts',
  404. settings: './settings/settings.ts',
  405. welcome: './welcome/welcome.ts',
  406. },
  407. mode: mode,
  408. target: 'web',
  409. devtool: 'source-map',
  410. output: {
  411. filename: '[name].js',
  412. path: path.join(__dirname, 'dist', 'webviews'),
  413. publicPath: '#{root}/dist/webviews/',
  414. },
  415. module: {
  416. rules: [
  417. {
  418. exclude: /\.d\.ts$/,
  419. include: path.join(__dirname, 'src'),
  420. test: /\.tsx?$/,
  421. use: env.esbuild
  422. ? {
  423. loader: 'esbuild-loader',
  424. options: {
  425. implementation: esbuild,
  426. loader: 'ts',
  427. target: 'es2020',
  428. tsconfigRaw: resolveTSConfig(path.join(basePath, 'tsconfig.json')),
  429. },
  430. }
  431. : {
  432. loader: 'ts-loader',
  433. options: {
  434. configFile: path.join(basePath, 'tsconfig.json'),
  435. experimentalWatchApi: true,
  436. transpileOnly: true,
  437. },
  438. },
  439. },
  440. {
  441. test: /\.scss$/,
  442. use: [
  443. {
  444. loader: MiniCssExtractPlugin.loader,
  445. },
  446. {
  447. loader: 'css-loader',
  448. options: {
  449. sourceMap: true,
  450. url: false,
  451. },
  452. },
  453. {
  454. loader: 'sass-loader',
  455. options: {
  456. sourceMap: true,
  457. },
  458. },
  459. ],
  460. exclude: /node_modules/,
  461. },
  462. ],
  463. },
  464. resolve: {
  465. extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
  466. modules: [basePath, 'node_modules'],
  467. },
  468. plugins: plugins,
  469. stats: {
  470. preset: 'errors-warnings',
  471. assets: true,
  472. colors: true,
  473. env: true,
  474. errorsCount: true,
  475. warningsCount: true,
  476. timings: true,
  477. },
  478. infrastructureLogging: {
  479. level: 'log', // enables logging required for problem matchers
  480. },
  481. };
  482. }
  483. class InlineChunkHtmlPlugin {
  484. constructor(htmlPlugin, patterns) {
  485. this.htmlPlugin = htmlPlugin;
  486. this.patterns = patterns;
  487. }
  488. getInlinedTag(publicPath, assets, tag) {
  489. if (
  490. (tag.tagName !== 'script' || !(tag.attributes && tag.attributes.src)) &&
  491. (tag.tagName !== 'link' || !(tag.attributes && tag.attributes.href))
  492. ) {
  493. return tag;
  494. }
  495. let chunkName = tag.tagName === 'link' ? tag.attributes.href : tag.attributes.src;
  496. if (publicPath) {
  497. chunkName = chunkName.replace(publicPath, '');
  498. }
  499. if (!this.patterns.some(pattern => chunkName.match(pattern))) {
  500. return tag;
  501. }
  502. const asset = assets[chunkName];
  503. if (asset == null) {
  504. return tag;
  505. }
  506. return { tagName: tag.tagName === 'link' ? 'style' : tag.tagName, innerHTML: asset.source(), closeTag: true };
  507. }
  508. apply(compiler) {
  509. let publicPath = compiler.options.output.publicPath || '';
  510. if (publicPath && !publicPath.endsWith('/')) {
  511. publicPath += '/';
  512. }
  513. compiler.hooks.compilation.tap('InlineChunkHtmlPlugin', compilation => {
  514. const getInlinedTagFn = tag => this.getInlinedTag(publicPath, compilation.assets, tag);
  515. this.htmlPlugin.getHooks(compilation).alterAssetTagGroups.tap('InlineChunkHtmlPlugin', assets => {
  516. assets.headTags = assets.headTags.map(getInlinedTagFn);
  517. assets.bodyTags = assets.bodyTags.map(getInlinedTagFn);
  518. });
  519. });
  520. }
  521. }
  522. /**
  523. * @param { string } configFile
  524. * @returns { string }
  525. */
  526. function resolveTSConfig(configFile) {
  527. const result = spawnSync('yarn', ['tsc', `-p ${configFile}`, '--showConfig'], {
  528. cwd: __dirname,
  529. encoding: 'utf8',
  530. shell: true,
  531. });
  532. const data = result.stdout;
  533. const start = data.indexOf('{');
  534. const end = data.lastIndexOf('}') + 1;
  535. const json = JSON5.parse(data.substring(start, end));
  536. return json;
  537. }