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.

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