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

731 行
19 KiB

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