You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

751 lines
20 KiB

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