@@ -13,6 +13,7 @@ import stripIndent from 'strip-indent~3';
1313import { ErrorMessage } from 'universe:errors.ts' ;
1414import { $type } from 'universe:symbols.ts' ;
1515
16+ import type { PluginItem } from '@babel/core' ;
1617import type { Class } from 'type-fest' ;
1718
1819import type {
@@ -830,6 +831,7 @@ function pluginTester(options: PluginTesterOptions = {}) {
830831
831832 verbose3 ( 'partially constructed fixture-based test object: %O' , testConfig ) ;
832833
834+ // ! Invariant: test plugin/preset is always first/last respectively
833835 if ( plugin ) {
834836 testConfig . babelOptions . plugins . push ( [
835837 plugin ,
@@ -843,6 +845,8 @@ function pluginTester(options: PluginTesterOptions = {}) {
843845 }
844846
845847 finalizePluginAndPresetRunOrder ( testConfig . babelOptions ) ;
848+ reduceDuplicatePluginsAndPresets ( testConfig . babelOptions ) ;
849+
846850 verbose3 ( 'finalized fixture-based test object: %O' , testConfig ) ;
847851
848852 validateTestConfig ( testConfig ) ;
@@ -998,6 +1002,8 @@ function pluginTester(options: PluginTesterOptions = {}) {
9981002 }
9991003
10001004 finalizePluginAndPresetRunOrder ( testConfig . babelOptions ) ;
1005+ reduceDuplicatePluginsAndPresets ( testConfig . babelOptions ) ;
1006+
10011007 verbose3 ( 'finalized test object: %O' , testConfig ) ;
10021008
10031009 validateTestConfig ( testConfig , {
@@ -1630,7 +1636,7 @@ function trimAndFixLineEndings(
16301636function finalizePluginAndPresetRunOrder (
16311637 babelOptions : PluginTesterOptions [ 'babelOptions' ]
16321638) {
1633- const { verbose : verbose2 } = getDebuggers ( 'finalize' , debug1 ) ;
1639+ const { verbose : verbose2 } = getDebuggers ( 'finalize:order ' , debug1 ) ;
16341640
16351641 if ( babelOptions ?. plugins ) {
16361642 babelOptions . plugins = babelOptions . plugins . filter ( ( p ) => {
@@ -1686,6 +1692,116 @@ function finalizePluginAndPresetRunOrder(
16861692 verbose2 ( 'finalized test object plugin and preset run order' ) ;
16871693}
16881694
1695+ /**
1696+ * Collapses duplicate plugin/preset entries down to a single plugin/preset
1697+ * entry. The last duplicate plugin/preset (i.e. highest index) wins, the rest
1698+ * are essentially deleted.
1699+ *
1700+ * This function accounts for the three generic babel plugin/preset
1701+ * configurations: string, 2-tuple, and 3-tuple.
1702+ *
1703+ * @see {@link PluginItem }
1704+ */
1705+ function reduceDuplicatePluginsAndPresets (
1706+ babelOptions : PluginTesterOptions [ 'babelOptions' ]
1707+ ) {
1708+ const { verbose : verbose2 } = getDebuggers ( 'finalize:duplicates' , debug1 ) ;
1709+
1710+ if ( babelOptions ?. plugins ) {
1711+ const plugins : typeof babelOptions . plugins = [ ] ;
1712+
1713+ babelOptions . plugins . forEach ( ( incomingPlugin ) => {
1714+ if ( incomingPlugin && typeof incomingPlugin !== 'symbol' ) {
1715+ const incomingPluginName = pluginOrPresetToName ( incomingPlugin ) ;
1716+
1717+ if ( incomingPluginName ) {
1718+ const duplicatedPluginIndex = plugins . findIndex ( ( outgoingPlugin ) => {
1719+ if ( outgoingPlugin && typeof outgoingPlugin !== 'symbol' ) {
1720+ const outgoingPluginName = pluginOrPresetToName ( outgoingPlugin ) ;
1721+ return outgoingPluginName === incomingPluginName ;
1722+ }
1723+ } ) ;
1724+
1725+ if ( duplicatedPluginIndex !== - 1 ) {
1726+ verbose2 (
1727+ 'collapsed duplicate plugin configuration for %O (at index %O)' ,
1728+ incomingPluginName ,
1729+ duplicatedPluginIndex
1730+ ) ;
1731+
1732+ plugins [ duplicatedPluginIndex ] = incomingPlugin ;
1733+ return ;
1734+ }
1735+ }
1736+ }
1737+
1738+ plugins . push ( incomingPlugin ) ;
1739+ } ) ;
1740+
1741+ babelOptions . plugins = plugins ;
1742+ }
1743+
1744+ if ( babelOptions ?. presets ) {
1745+ const presets : typeof babelOptions . presets = [ ] ;
1746+
1747+ babelOptions . presets . forEach ( ( incomingPreset ) => {
1748+ if ( incomingPreset && typeof incomingPreset !== 'symbol' ) {
1749+ const incomingPresetName = pluginOrPresetToName ( incomingPreset ) ;
1750+
1751+ if ( incomingPresetName ) {
1752+ const duplicatedPresetIndex = presets . findIndex ( ( outgoingPreset ) => {
1753+ if ( outgoingPreset && typeof outgoingPreset !== 'symbol' ) {
1754+ const outgoingPresetName = pluginOrPresetToName ( outgoingPreset ) ;
1755+ return outgoingPresetName === incomingPresetName ;
1756+ }
1757+ } ) ;
1758+
1759+ if ( duplicatedPresetIndex !== - 1 ) {
1760+ verbose2 (
1761+ 'collapsed duplicate preset configuration for %O (at index %O)' ,
1762+ incomingPresetName ,
1763+ duplicatedPresetIndex
1764+ ) ;
1765+
1766+ presets [ duplicatedPresetIndex ] = incomingPreset ;
1767+ return ;
1768+ }
1769+ }
1770+ }
1771+
1772+ presets . push ( incomingPreset ) ;
1773+ } ) ;
1774+
1775+ babelOptions . presets = presets ;
1776+ }
1777+
1778+ verbose2 ( 'collapsed duplicate test object plugins and presets' ) ;
1779+
1780+ /**
1781+ * Note that, due to how flexible Babel configuration is, not all plugins or
1782+ * presets will have a name.
1783+ *
1784+ * This function is a _much_ more generic version of `tryInferPluginName`.
1785+ * TODO: perhaps they should be merged?
1786+ */
1787+ function pluginOrPresetToName ( pluginOrPreset : PluginItem ) {
1788+ if ( typeof pluginOrPreset === 'string' ) {
1789+ return pluginOrPreset ;
1790+ }
1791+
1792+ if ( Array . isArray ( pluginOrPreset ) ) {
1793+ const candidate = pluginOrPreset . at ( 2 ) ?? pluginOrPreset . at ( 0 ) ;
1794+ return typeof candidate === 'string' ? candidate : undefined ;
1795+ }
1796+
1797+ if ( typeof pluginOrPreset === 'object' && 'name' in pluginOrPreset ) {
1798+ return typeof pluginOrPreset . name === 'string' ? pluginOrPreset . name : undefined ;
1799+ }
1800+
1801+ return undefined ;
1802+ }
1803+ }
1804+
16891805/**
16901806 * Determines if `numericPrefix` equals at least one number or is covered by at
16911807 * least one range Range in the `ranges` array.
0 commit comments