25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

601 lines
16 KiB

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