您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

630 行
16 KiB

3 年前
2 年前
2 年前
3 年前
3 年前
3 年前
  1. //@ts-check
  2. /** @typedef {import('webpack').Configuration} WebpackConfig **/
  3. const fs = require('fs');
  4. const { spawnSync } = require('child_process');
  5. const path = require('path');
  6. const CircularDependencyPlugin = require('circular-dependency-plugin');
  7. const { CleanWebpackPlugin: CleanPlugin } = require('clean-webpack-plugin');
  8. const CopyPlugin = require('copy-webpack-plugin');
  9. const CspHtmlPlugin = require('csp-html-webpack-plugin');
  10. const esbuild = require('esbuild');
  11. const FantasticonPlugin = require('fantasticon-webpack-plugin');
  12. const ForkTsCheckerPlugin = require('fork-ts-checker-webpack-plugin');
  13. const HtmlPlugin = require('html-webpack-plugin');
  14. const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
  15. const JSON5 = require('json5');
  16. const MiniCssExtractPlugin = require('mini-css-extract-plugin');
  17. const TerserPlugin = require('terser-webpack-plugin');
  18. const { WebpackError, webpack, optimize } = require('webpack');
  19. const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
  20. module.exports =
  21. /**
  22. * @param {{ analyzeBundle?: boolean; analyzeDeps?: boolean; esbuild?: boolean; squoosh?: boolean } | undefined } env
  23. * @param {{ mode: 'production' | 'development' | 'none' | undefined }} argv
  24. * @returns { WebpackConfig[] }
  25. */
  26. function (env, argv) {
  27. const mode = argv.mode || 'none';
  28. env = {
  29. analyzeBundle: false,
  30. analyzeDeps: false,
  31. esbuild: true,
  32. squoosh: true,
  33. ...env,
  34. };
  35. return [
  36. getExtensionConfig('node', mode, env),
  37. getExtensionConfig('webworker', mode, env),
  38. getWebviewsConfig(mode, env),
  39. ];
  40. };
  41. /**
  42. * @param { 'node' | 'webworker' } target
  43. * @param { 'production' | 'development' | 'none' } mode
  44. * @param {{ analyzeBundle?: boolean; analyzeDeps?: boolean; esbuild?: boolean; squoosh?: boolean } } env
  45. * @returns { WebpackConfig }
  46. */
  47. function getExtensionConfig(target, mode, env) {
  48. /**
  49. * @type WebpackConfig['plugins'] | any
  50. */
  51. const plugins = [
  52. new CleanPlugin({ cleanOnceBeforeBuildPatterns: ['!dist/webviews/**'] }),
  53. new ForkTsCheckerPlugin({
  54. async: false,
  55. eslint: {
  56. enabled: true,
  57. files: 'src/**/*.ts?(x)',
  58. options: {
  59. // cache: true,
  60. // cacheLocation: path.join(
  61. // __dirname,
  62. // target === 'webworker' ? '.eslintcache.browser' : '.eslintcache',
  63. // ),
  64. fix: mode !== 'production',
  65. overrideConfigFile: path.join(
  66. __dirname,
  67. target === 'webworker' ? '.eslintrc.browser.json' : '.eslintrc.json',
  68. ),
  69. },
  70. },
  71. formatter: 'basic',
  72. typescript: {
  73. configFile: path.join(__dirname, target === 'webworker' ? 'tsconfig.browser.json' : 'tsconfig.json'),
  74. },
  75. }),
  76. ];
  77. if (target === 'webworker') {
  78. plugins.push(new optimize.LimitChunkCountPlugin({ maxChunks: 1 }));
  79. } else {
  80. // Ensure that the dist folder exists otherwise the FantasticonPlugin will fail
  81. const dist = path.join(__dirname, 'dist');
  82. if (!fs.existsSync(dist)) {
  83. fs.mkdirSync(dist);
  84. }
  85. plugins.push(
  86. new FantasticonPlugin({
  87. runOnComplete: true,
  88. configPath: '.fantasticonrc.js',
  89. }),
  90. );
  91. }
  92. if (env.analyzeDeps) {
  93. plugins.push(
  94. new CircularDependencyPlugin({
  95. cwd: __dirname,
  96. exclude: /node_modules/,
  97. failOnError: false,
  98. onDetected: function ({ module: _webpackModuleRecord, paths, compilation }) {
  99. if (paths.some(p => p.includes('container.ts'))) return;
  100. // @ts-ignore
  101. compilation.warnings.push(new WebpackError(paths.join(' -> ')));
  102. },
  103. }),
  104. );
  105. }
  106. if (env.analyzeBundle) {
  107. plugins.push(new BundleAnalyzerPlugin({ analyzerPort: 'auto' }));
  108. }
  109. return {
  110. name: `extension:${target}`,
  111. entry: {
  112. extension: './src/extension.ts',
  113. },
  114. mode: mode,
  115. target: target,
  116. devtool: 'source-map',
  117. output: {
  118. path: target === 'webworker' ? path.join(__dirname, 'dist', 'browser') : path.join(__dirname, 'dist'),
  119. libraryTarget: 'commonjs2',
  120. filename: 'gitlens.js',
  121. chunkFilename: 'feature-[name].js',
  122. },
  123. optimization: {
  124. minimizer: [
  125. new TerserPlugin(
  126. env.esbuild
  127. ? {
  128. minify: TerserPlugin.esbuildMinify,
  129. terserOptions: {
  130. // @ts-ignore
  131. drop: ['debugger'],
  132. format: 'cjs',
  133. minify: true,
  134. treeShaking: true,
  135. // Keep the class names otherwise @log won't provide a useful name
  136. keepNames: true,
  137. target: 'es2020',
  138. },
  139. }
  140. : {
  141. extractComments: false,
  142. parallel: true,
  143. terserOptions: {
  144. compress: {
  145. drop_debugger: true,
  146. },
  147. ecma: 2020,
  148. // Keep the class names otherwise @log won't provide a useful name
  149. keep_classnames: true,
  150. module: true,
  151. },
  152. },
  153. ),
  154. ],
  155. splitChunks:
  156. target === 'webworker'
  157. ? false
  158. : {
  159. // Disable all non-async code splitting
  160. chunks: () => false,
  161. cacheGroups: {
  162. default: false,
  163. vendors: false,
  164. },
  165. },
  166. },
  167. externals: {
  168. vscode: 'commonjs vscode',
  169. },
  170. module: {
  171. rules: [
  172. {
  173. exclude: /\.d\.ts$/,
  174. include: path.join(__dirname, 'src'),
  175. test: /\.tsx?$/,
  176. use: env.esbuild
  177. ? {
  178. loader: 'esbuild-loader',
  179. options: {
  180. implementation: esbuild,
  181. loader: 'tsx',
  182. target: ['es2020', 'chrome91', 'node14.16'],
  183. tsconfigRaw: resolveTSConfig(
  184. path.join(
  185. __dirname,
  186. target === 'webworker' ? 'tsconfig.browser.json' : 'tsconfig.json',
  187. ),
  188. ),
  189. },
  190. }
  191. : {
  192. loader: 'ts-loader',
  193. options: {
  194. configFile: path.join(
  195. __dirname,
  196. target === 'webworker' ? 'tsconfig.browser.json' : 'tsconfig.json',
  197. ),
  198. experimentalWatchApi: true,
  199. transpileOnly: true,
  200. },
  201. },
  202. },
  203. ],
  204. },
  205. resolve: {
  206. alias: {
  207. '@env': path.resolve(__dirname, 'src', 'env', target === 'webworker' ? 'browser' : target),
  208. // This dependency is very large, and isn't needed for our use-case
  209. tr46: path.resolve(__dirname, 'patches', 'tr46.js'),
  210. },
  211. fallback: target === 'webworker' ? { path: require.resolve('path-browserify') } : undefined,
  212. mainFields: target === 'webworker' ? ['browser', 'module', 'main'] : ['module', 'main'],
  213. extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
  214. },
  215. plugins: plugins,
  216. infrastructureLogging: {
  217. level: 'log', // enables logging required for problem matchers
  218. },
  219. stats: {
  220. preset: 'errors-warnings',
  221. assets: true,
  222. colors: true,
  223. env: true,
  224. errorsCount: true,
  225. warningsCount: true,
  226. timings: true,
  227. },
  228. };
  229. }
  230. /**
  231. * @param { 'production' | 'development' | 'none' } mode
  232. * @param {{ analyzeBundle?: boolean; analyzeDeps?: boolean; esbuild?: boolean; squoosh?: boolean } } env
  233. * @returns { WebpackConfig }
  234. */
  235. function getWebviewsConfig(mode, env) {
  236. const basePath = path.join(__dirname, 'src', 'webviews', 'apps');
  237. /** @type WebpackConfig['plugins'] | any */
  238. const plugins = [
  239. new CleanPlugin(
  240. mode === 'production'
  241. ? {
  242. cleanOnceBeforeBuildPatterns: [
  243. path.posix.join(__dirname.replace(/\\/g, '/'), 'dist', 'webviews', 'media', '**'),
  244. ],
  245. dangerouslyAllowCleanPatternsOutsideProject: true,
  246. dry: false,
  247. }
  248. : undefined,
  249. ),
  250. new ForkTsCheckerPlugin({
  251. async: false,
  252. eslint: {
  253. enabled: true,
  254. files: path.join(basePath, '**', '*.ts?(x)'),
  255. options: {
  256. // cache: true,
  257. fix: mode !== 'production',
  258. },
  259. },
  260. formatter: 'basic',
  261. typescript: {
  262. configFile: path.join(basePath, 'tsconfig.json'),
  263. },
  264. }),
  265. new MiniCssExtractPlugin({ filename: '[name].css' }),
  266. getHtmlPlugin('commitDetails', false, mode, env),
  267. getHtmlPlugin('graph', true, mode, env),
  268. getHtmlPlugin('home', false, mode, env),
  269. getHtmlPlugin('rebase', false, mode, env),
  270. getHtmlPlugin('settings', false, mode, env),
  271. getHtmlPlugin('timeline', true, mode, env),
  272. getHtmlPlugin('welcome', false, mode, env),
  273. getCspHtmlPlugin(mode, env),
  274. new InlineChunkHtmlPlugin(HtmlPlugin, mode === 'production' ? ['\\.css$'] : []),
  275. new CopyPlugin({
  276. patterns: [
  277. {
  278. from: path.posix.join(basePath.replace(/\\/g, '/'), 'media', '*.*'),
  279. to: path.posix.join(__dirname.replace(/\\/g, '/'), 'dist', 'webviews'),
  280. },
  281. {
  282. from: path.posix.join(
  283. __dirname.replace(/\\/g, '/'),
  284. 'node_modules',
  285. '@vscode',
  286. 'codicons',
  287. 'dist',
  288. 'codicon.ttf',
  289. ),
  290. to: path.posix.join(__dirname.replace(/\\/g, '/'), 'dist', 'webviews'),
  291. },
  292. ],
  293. }),
  294. ];
  295. const imageGeneratorConfig = getImageMinimizerConfig(mode, env);
  296. if (mode !== 'production') {
  297. plugins.push(
  298. new ImageMinimizerPlugin({
  299. deleteOriginalAssets: true,
  300. generator: [imageGeneratorConfig],
  301. }),
  302. );
  303. }
  304. return {
  305. name: 'webviews',
  306. context: basePath,
  307. entry: {
  308. commitDetails: './commitDetails/commitDetails.ts',
  309. graph: './plus/graph/graph.tsx',
  310. home: './home/home.ts',
  311. rebase: './rebase/rebase.ts',
  312. settings: './settings/settings.ts',
  313. timeline: './plus/timeline/timeline.ts',
  314. welcome: './welcome/welcome.ts',
  315. },
  316. mode: mode,
  317. target: 'web',
  318. devtool: 'source-map',
  319. output: {
  320. filename: '[name].js',
  321. path: path.join(__dirname, 'dist', 'webviews'),
  322. publicPath: '#{root}/dist/webviews/',
  323. },
  324. optimization: {
  325. minimizer: [
  326. new TerserPlugin(
  327. env.esbuild
  328. ? {
  329. minify: TerserPlugin.esbuildMinify,
  330. terserOptions: {
  331. // @ts-ignore
  332. drop: ['debugger', 'console'],
  333. // @ts-ignore
  334. format: 'esm',
  335. minify: true,
  336. treeShaking: true,
  337. // // Keep the class names otherwise @log won't provide a useful name
  338. // keepNames: true,
  339. target: 'es2020',
  340. },
  341. }
  342. : {
  343. extractComments: false,
  344. parallel: true,
  345. // @ts-ignore
  346. terserOptions: {
  347. compress: {
  348. drop_debugger: true,
  349. drop_console: true,
  350. },
  351. ecma: 2020,
  352. // // Keep the class names otherwise @log won't provide a useful name
  353. // keep_classnames: true,
  354. module: true,
  355. },
  356. },
  357. ),
  358. new ImageMinimizerPlugin({
  359. deleteOriginalAssets: true,
  360. generator: [imageGeneratorConfig],
  361. }),
  362. ],
  363. },
  364. module: {
  365. rules: [
  366. {
  367. exclude: /\.d\.ts$/,
  368. include: path.join(__dirname, 'src'),
  369. test: /\.tsx?$/,
  370. use: env.esbuild
  371. ? {
  372. loader: 'esbuild-loader',
  373. options: {
  374. implementation: esbuild,
  375. loader: 'tsx',
  376. target: 'es2020',
  377. tsconfigRaw: resolveTSConfig(path.join(basePath, 'tsconfig.json')),
  378. },
  379. }
  380. : {
  381. loader: 'ts-loader',
  382. options: {
  383. configFile: path.join(basePath, 'tsconfig.json'),
  384. experimentalWatchApi: true,
  385. transpileOnly: true,
  386. },
  387. },
  388. },
  389. {
  390. test: /\.scss$/,
  391. use: [
  392. {
  393. loader: MiniCssExtractPlugin.loader,
  394. },
  395. {
  396. loader: 'css-loader',
  397. options: {
  398. sourceMap: true,
  399. url: false,
  400. },
  401. },
  402. {
  403. loader: 'sass-loader',
  404. options: {
  405. sourceMap: true,
  406. },
  407. },
  408. ],
  409. exclude: /node_modules/,
  410. },
  411. ],
  412. },
  413. resolve: {
  414. alias: {
  415. '@env': path.resolve(__dirname, 'src', 'env', 'browser'),
  416. },
  417. extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
  418. modules: [basePath, 'node_modules'],
  419. fallback: {
  420. crypto: require.resolve('crypto-browserify'),
  421. stream: require.resolve('stream-browserify'),
  422. },
  423. },
  424. plugins: plugins,
  425. infrastructureLogging: {
  426. level: 'log', // enables logging required for problem matchers
  427. },
  428. stats: {
  429. preset: 'errors-warnings',
  430. assets: true,
  431. colors: true,
  432. env: true,
  433. errorsCount: true,
  434. warningsCount: true,
  435. timings: true,
  436. },
  437. };
  438. }
  439. /**
  440. * @param { 'production' | 'development' | 'none' } mode
  441. * @param {{ analyzeBundle?: boolean; analyzeDeps?: boolean; esbuild?: boolean; squoosh?: boolean } | undefined } env
  442. * @returns { CspHtmlPlugin }
  443. */
  444. function getCspHtmlPlugin(mode, env) {
  445. const cspPlugin = new CspHtmlPlugin(
  446. {
  447. 'default-src': "'none'",
  448. 'img-src': ['#{cspSource}', 'https:', 'data:'],
  449. 'script-src':
  450. mode !== 'production'
  451. ? ['#{cspSource}', "'nonce-#{cspNonce}'", "'unsafe-eval'"]
  452. : ['#{cspSource}', "'nonce-#{cspNonce}'"],
  453. 'style-src':
  454. mode === 'production'
  455. ? ['#{cspSource}', "'nonce-#{cspNonce}'", "'unsafe-hashes'"]
  456. : ['#{cspSource}', "'unsafe-hashes'", "'unsafe-inline'"],
  457. 'font-src': ['#{cspSource}'],
  458. },
  459. {
  460. enabled: true,
  461. hashingMethod: 'sha256',
  462. hashEnabled: {
  463. 'script-src': true,
  464. 'style-src': mode === 'production',
  465. },
  466. nonceEnabled: {
  467. 'script-src': true,
  468. 'style-src': mode === 'production',
  469. },
  470. },
  471. );
  472. // Override the nonce creation so we can dynamically generate them at runtime
  473. // @ts-ignore
  474. cspPlugin.createNonce = () => '#{cspNonce}';
  475. return cspPlugin;
  476. }
  477. /**
  478. * @param { 'production' | 'development' | 'none' } mode
  479. * @param {{ analyzeBundle?: boolean; analyzeDeps?: boolean; esbuild?: boolean; squoosh?: boolean } | undefined } env
  480. * @returns { ImageMinimizerPlugin.Generator<any> }
  481. */
  482. function getImageMinimizerConfig(mode, env) {
  483. /** @type ImageMinimizerPlugin.Generator<any> */
  484. // @ts-ignore
  485. return env.squoosh
  486. ? {
  487. type: 'asset',
  488. implementation: ImageMinimizerPlugin.squooshGenerate,
  489. options: {
  490. encodeOptions: {
  491. webp: {
  492. // quality: 90,
  493. lossless: 1,
  494. },
  495. },
  496. },
  497. }
  498. : {
  499. type: 'asset',
  500. implementation: ImageMinimizerPlugin.imageminGenerate,
  501. options: {
  502. plugins: [
  503. [
  504. 'imagemin-webp',
  505. {
  506. lossless: true,
  507. nearLossless: 0,
  508. quality: 100,
  509. method: mode === 'production' ? 4 : 0,
  510. },
  511. ],
  512. ],
  513. },
  514. };
  515. }
  516. /**
  517. * @param { string } name
  518. * @param { boolean } plus
  519. * @param { 'production' | 'development' | 'none' } mode
  520. * @param {{ analyzeBundle?: boolean; analyzeDeps?: boolean; esbuild?: boolean; squoosh?: boolean } | undefined } env
  521. * @returns { HtmlPlugin }
  522. */
  523. function getHtmlPlugin(name, plus, mode, env) {
  524. return new HtmlPlugin({
  525. template: plus ? path.join('plus', name, `${name}.html`) : path.join(name, `${name}.html`),
  526. chunks: [name],
  527. filename: path.join(__dirname, 'dist', 'webviews', `${name}.html`),
  528. inject: true,
  529. scriptLoading: 'module',
  530. inlineSource: mode === 'production' ? '.css$' : undefined,
  531. minify:
  532. mode === 'production'
  533. ? {
  534. removeComments: true,
  535. collapseWhitespace: true,
  536. removeRedundantAttributes: false,
  537. useShortDoctype: true,
  538. removeEmptyAttributes: true,
  539. removeStyleLinkTypeAttributes: true,
  540. keepClosingSlash: true,
  541. minifyCSS: true,
  542. }
  543. : false,
  544. });
  545. }
  546. class InlineChunkHtmlPlugin {
  547. constructor(htmlPlugin, patterns) {
  548. this.htmlPlugin = htmlPlugin;
  549. this.patterns = patterns;
  550. }
  551. getInlinedTag(publicPath, assets, tag) {
  552. if (
  553. (tag.tagName !== 'script' || !(tag.attributes && tag.attributes.src)) &&
  554. (tag.tagName !== 'link' || !(tag.attributes && tag.attributes.href))
  555. ) {
  556. return tag;
  557. }
  558. let chunkName = tag.tagName === 'link' ? tag.attributes.href : tag.attributes.src;
  559. if (publicPath) {
  560. chunkName = chunkName.replace(publicPath, '');
  561. }
  562. if (!this.patterns.some(pattern => chunkName.match(pattern))) {
  563. return tag;
  564. }
  565. const asset = assets[chunkName];
  566. if (asset == null) {
  567. return tag;
  568. }
  569. return { tagName: tag.tagName === 'link' ? 'style' : tag.tagName, innerHTML: asset.source(), closeTag: true };
  570. }
  571. apply(compiler) {
  572. let publicPath = compiler.options.output.publicPath || '';
  573. if (publicPath && !publicPath.endsWith('/')) {
  574. publicPath += '/';
  575. }
  576. compiler.hooks.compilation.tap('InlineChunkHtmlPlugin', compilation => {
  577. const getInlinedTagFn = tag => this.getInlinedTag(publicPath, compilation.assets, tag);
  578. const sortFn = (a, b) => (a.tagName === 'script' ? 1 : -1) - (b.tagName === 'script' ? 1 : -1);
  579. this.htmlPlugin.getHooks(compilation).alterAssetTagGroups.tap('InlineChunkHtmlPlugin', assets => {
  580. assets.headTags = assets.headTags.map(getInlinedTagFn).sort(sortFn);
  581. assets.bodyTags = assets.bodyTags.map(getInlinedTagFn).sort(sortFn);
  582. });
  583. });
  584. }
  585. }
  586. /**
  587. * @param { string } configFile
  588. * @returns { string }
  589. */
  590. function resolveTSConfig(configFile) {
  591. const result = spawnSync('yarn', ['tsc', `-p ${configFile}`, '--showConfig'], {
  592. cwd: __dirname,
  593. encoding: 'utf8',
  594. shell: true,
  595. });
  596. const data = result.stdout;
  597. const start = data.indexOf('{');
  598. const end = data.lastIndexOf('}') + 1;
  599. const json = JSON5.parse(data.substring(start, end));
  600. return json;
  601. }