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

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