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.

548 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 { 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. process: false,
  203. stream: false,
  204. url: false,
  205. }
  206. : undefined,
  207. mainFields: target === 'webworker' ? ['browser', 'module', 'main'] : ['module', 'main'],
  208. extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
  209. },
  210. plugins: plugins,
  211. stats: {
  212. preset: 'errors-warnings',
  213. assets: true,
  214. colors: true,
  215. env: true,
  216. errorsCount: true,
  217. warningsCount: true,
  218. timings: true,
  219. },
  220. };
  221. }
  222. /**
  223. * @param { 'production' | 'development' | 'none' } mode
  224. * @param {{ analyzeBundle?: boolean; analyzeDeps?: boolean; esbuild?: boolean; }} env
  225. * @returns { WebpackConfig }
  226. */
  227. function getWebviewsConfig(mode, env) {
  228. const basePath = path.join(__dirname, 'src', 'webviews', 'apps');
  229. const cspHtmlPlugin = new CspHtmlPlugin(
  230. {
  231. 'default-src': "'none'",
  232. 'img-src': ['#{cspSource}', 'https:', 'data:'],
  233. 'script-src':
  234. mode !== 'production'
  235. ? ['#{cspSource}', "'nonce-#{cspNonce}'", "'unsafe-eval'"]
  236. : ['#{cspSource}', "'nonce-#{cspNonce}'"],
  237. 'style-src': ['#{cspSource}', "'nonce-#{cspNonce}'"],
  238. 'font-src': ['#{cspSource}'],
  239. },
  240. {
  241. enabled: true,
  242. hashingMethod: 'sha256',
  243. hashEnabled: {
  244. 'script-src': true,
  245. 'style-src': true,
  246. },
  247. nonceEnabled: {
  248. 'script-src': true,
  249. 'style-src': true,
  250. },
  251. },
  252. );
  253. // Override the nonce creation so we can dynamically generate them at runtime
  254. // @ts-ignore
  255. cspHtmlPlugin.createNonce = () => '#{cspNonce}';
  256. /**
  257. * @type WebpackConfig['plugins'] | any
  258. */
  259. const plugins = [
  260. new CleanPlugin(
  261. mode === 'production'
  262. ? {
  263. cleanOnceBeforeBuildPatterns: [
  264. path.posix.join(__dirname.replace(/\\/g, '/'), 'images', 'settings', '**'),
  265. ],
  266. dangerouslyAllowCleanPatternsOutsideProject: true,
  267. dry: false,
  268. }
  269. : undefined,
  270. ),
  271. new ForkTsCheckerPlugin({
  272. async: false,
  273. eslint: {
  274. enabled: true,
  275. files: path.join(basePath, '**', '*.ts'),
  276. // options: { cache: true },
  277. },
  278. formatter: 'basic',
  279. typescript: {
  280. configFile: path.join(basePath, 'tsconfig.json'),
  281. },
  282. }),
  283. new MiniCssExtractPlugin({
  284. filename: '[name].css',
  285. }),
  286. new HtmlPlugin({
  287. template: 'rebase/rebase.html',
  288. chunks: ['rebase'],
  289. filename: path.join(__dirname, 'dist', 'webviews', 'rebase.html'),
  290. inject: true,
  291. inlineSource: mode === 'production' ? '.css$' : undefined,
  292. minify:
  293. mode === 'production'
  294. ? {
  295. removeComments: true,
  296. collapseWhitespace: true,
  297. removeRedundantAttributes: false,
  298. useShortDoctype: true,
  299. removeEmptyAttributes: true,
  300. removeStyleLinkTypeAttributes: true,
  301. keepClosingSlash: true,
  302. minifyCSS: true,
  303. }
  304. : false,
  305. }),
  306. new HtmlPlugin({
  307. template: 'settings/settings.html',
  308. chunks: ['settings'],
  309. filename: path.join(__dirname, 'dist', 'webviews', 'settings.html'),
  310. inject: true,
  311. inlineSource: mode === 'production' ? '.css$' : undefined,
  312. minify:
  313. mode === 'production'
  314. ? {
  315. removeComments: true,
  316. collapseWhitespace: true,
  317. removeRedundantAttributes: false,
  318. useShortDoctype: true,
  319. removeEmptyAttributes: true,
  320. removeStyleLinkTypeAttributes: true,
  321. keepClosingSlash: true,
  322. minifyCSS: true,
  323. }
  324. : false,
  325. }),
  326. new HtmlPlugin({
  327. template: 'welcome/welcome.html',
  328. chunks: ['welcome'],
  329. filename: path.join(__dirname, 'dist', 'webviews', 'welcome.html'),
  330. inject: true,
  331. inlineSource: mode === 'production' ? '.css$' : undefined,
  332. minify:
  333. mode === 'production'
  334. ? {
  335. removeComments: true,
  336. collapseWhitespace: true,
  337. removeRedundantAttributes: false,
  338. useShortDoctype: true,
  339. removeEmptyAttributes: true,
  340. removeStyleLinkTypeAttributes: true,
  341. keepClosingSlash: true,
  342. minifyCSS: true,
  343. }
  344. : false,
  345. }),
  346. cspHtmlPlugin,
  347. new InlineChunkHtmlPlugin(HtmlPlugin, mode === 'production' ? ['\\.css$'] : []),
  348. new CopyPlugin({
  349. patterns: [
  350. {
  351. from: path.posix.join(basePath.replace(/\\/g, '/'), 'images', 'settings', '*.png'),
  352. to: __dirname.replace(/\\/g, '/'),
  353. },
  354. {
  355. from: path.posix.join(
  356. __dirname.replace(/\\/g, '/'),
  357. 'node_modules',
  358. '@vscode',
  359. 'codicons',
  360. 'dist',
  361. 'codicon.ttf',
  362. ),
  363. to: path.posix.join(__dirname.replace(/\\/g, '/'), 'dist', 'webviews'),
  364. },
  365. ],
  366. }),
  367. new ImageMinimizerPlugin({
  368. test: /\.(png)$/i,
  369. filename: '[path][name].webp',
  370. loader: false,
  371. deleteOriginalAssets: true,
  372. minimizerOptions: {
  373. plugins: [
  374. [
  375. 'imagemin-webp',
  376. {
  377. lossless: true,
  378. nearLossless: 0,
  379. quality: 100,
  380. method: mode === 'production' ? 4 : 0,
  381. },
  382. ],
  383. ],
  384. },
  385. }),
  386. ];
  387. return {
  388. name: 'webviews',
  389. context: basePath,
  390. entry: {
  391. rebase: './rebase/rebase.ts',
  392. settings: './settings/settings.ts',
  393. welcome: './welcome/welcome.ts',
  394. },
  395. mode: mode,
  396. target: 'web',
  397. devtool: 'source-map',
  398. output: {
  399. filename: '[name].js',
  400. path: path.join(__dirname, 'dist', 'webviews'),
  401. publicPath: '#{root}/dist/webviews/',
  402. },
  403. module: {
  404. rules: [
  405. {
  406. exclude: /\.d\.ts$/,
  407. include: path.join(__dirname, 'src'),
  408. test: /\.tsx?$/,
  409. use: env.esbuild
  410. ? {
  411. loader: 'esbuild-loader',
  412. options: {
  413. implementation: esbuild,
  414. loader: 'ts',
  415. target: 'es2020',
  416. tsconfigRaw: resolveTSConfig(path.join(basePath, 'tsconfig.json')),
  417. },
  418. }
  419. : {
  420. loader: 'ts-loader',
  421. options: {
  422. configFile: path.join(basePath, 'tsconfig.json'),
  423. experimentalWatchApi: true,
  424. transpileOnly: true,
  425. },
  426. },
  427. },
  428. {
  429. test: /\.scss$/,
  430. use: [
  431. {
  432. loader: MiniCssExtractPlugin.loader,
  433. },
  434. {
  435. loader: 'css-loader',
  436. options: {
  437. sourceMap: true,
  438. url: false,
  439. },
  440. },
  441. {
  442. loader: 'sass-loader',
  443. options: {
  444. sourceMap: true,
  445. },
  446. },
  447. ],
  448. exclude: /node_modules/,
  449. },
  450. ],
  451. },
  452. resolve: {
  453. extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
  454. modules: [basePath, 'node_modules'],
  455. },
  456. plugins: plugins,
  457. stats: {
  458. preset: 'errors-warnings',
  459. assets: true,
  460. colors: true,
  461. env: true,
  462. errorsCount: true,
  463. warningsCount: true,
  464. timings: true,
  465. },
  466. infrastructureLogging: {
  467. level: 'log', // enables logging required for problem matchers
  468. },
  469. };
  470. }
  471. class InlineChunkHtmlPlugin {
  472. constructor(htmlPlugin, patterns) {
  473. this.htmlPlugin = htmlPlugin;
  474. this.patterns = patterns;
  475. }
  476. getInlinedTag(publicPath, assets, tag) {
  477. if (
  478. (tag.tagName !== 'script' || !(tag.attributes && tag.attributes.src)) &&
  479. (tag.tagName !== 'link' || !(tag.attributes && tag.attributes.href))
  480. ) {
  481. return tag;
  482. }
  483. let chunkName = tag.tagName === 'link' ? tag.attributes.href : tag.attributes.src;
  484. if (publicPath) {
  485. chunkName = chunkName.replace(publicPath, '');
  486. }
  487. if (!this.patterns.some(pattern => chunkName.match(pattern))) {
  488. return tag;
  489. }
  490. const asset = assets[chunkName];
  491. if (asset == null) {
  492. return tag;
  493. }
  494. return { tagName: tag.tagName === 'link' ? 'style' : tag.tagName, innerHTML: asset.source(), closeTag: true };
  495. }
  496. apply(compiler) {
  497. let publicPath = compiler.options.output.publicPath || '';
  498. if (publicPath && !publicPath.endsWith('/')) {
  499. publicPath += '/';
  500. }
  501. compiler.hooks.compilation.tap('InlineChunkHtmlPlugin', compilation => {
  502. const getInlinedTagFn = tag => this.getInlinedTag(publicPath, compilation.assets, tag);
  503. this.htmlPlugin.getHooks(compilation).alterAssetTagGroups.tap('InlineChunkHtmlPlugin', assets => {
  504. assets.headTags = assets.headTags.map(getInlinedTagFn);
  505. assets.bodyTags = assets.bodyTags.map(getInlinedTagFn);
  506. });
  507. });
  508. }
  509. }
  510. /**
  511. * @param { string } configFile
  512. * @returns { string }
  513. */
  514. function resolveTSConfig(configFile) {
  515. const result = spawnSync('yarn', ['tsc', `-p ${configFile}`, '--showConfig'], {
  516. cwd: __dirname,
  517. encoding: 'utf8',
  518. shell: true,
  519. });
  520. const data = result.stdout;
  521. const start = data.indexOf('{');
  522. const end = data.lastIndexOf('}') + 1;
  523. const json = JSON5.parse(data.substring(start, end));
  524. return json;
  525. }