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.

744 lines
19 KiB

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