605 行
16 KiB

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