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.

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