Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

597 rader
16 KiB

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