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.

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