Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.

559 righe
14 KiB

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