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.

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