"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.ModuleFederationPluginV2 = void 0;
var _estreeUtilIsIdentifierName = require("estree-util-is-identifier-name");
var _isRspackCompiler = require("./utils/isRspackCompiler.js");
/**
 * {@link ModuleFederationPlugin} configuration options.
 *
 * The fields and types are exactly the same as in the official `ModuleFederationPlugin`.
 *
 * You can check documentation for all supported options here: https://module-federation.io/configure/
 */

/**
 * Webpack plugin to configure Module Federation with platform differences
 * handled under the hood.
 *
 * Usually, you should use `Repack.plugin.ModuleFederationPlugin`
 * instead of `webpack.container.ModuleFederationPlugin`.
 *
 * `Repack.plugin.ModuleFederationPlugin` creates:
 * - default for `filename` option when `exposes` is defined
 * - default for `library` option when `exposes` is defined
 * - default for `shared` option with `react` and `react-native` dependencies
 * - converts `remotes` into `ScriptManager`-powered `promise new Promise` loaders
 *
 * You can overwrite all defaults by passing respective options.
 *
 * `remotes` will always be converted to ScriptManager`-powered `promise new Promise` loaders
 * using {@link Federated.createRemote}.
 *
 * @example Host example.
 * ```js
 * import * as Repack from '@callstack/repack';
 *
 * new Repack.plugins.ModuleFederationPlugin({
 *   name: 'host',
 * });
 * ```
 *
 * @example Host example with additional `shared` dependencies.
 * ```js
 * import * as Repack from '@callstack/repack';
 *
 * new Repack.plugins.ModuleFederationPlugin({
 *   name: 'host',
 *   shared: {
 *     react: Repack.Federated.SHARED_REACT,
 *     'react-native': Repack.Federated.SHARED_REACT,
 *     'react-native-reanimated': {
 *       singleton: true,
 *     },
 *   },
 * });
 * ```
 *
 * @example Container examples.
 * ```js
 * import * as Repack from '@callstack/repack';
 *
 * new Repack.plugins.ModuleFederationPlugin({
 *   name: 'app1',
 *   remotes: {
 *     module1: 'module1@https://example.com/module1.container.bundle',
 *   },
 * });
 *
 * new Repack.plugins.ModuleFederationPlugin({
 *   name: 'app2',
 *   remotes: {
 *     module1: 'module1@https://example.com/module1.container.bundle',
 *     module2: 'module1@dynamic',
 *   },
 * });
 * ```
 *
 * @category Webpack Plugin
 */
class ModuleFederationPluginV2 {
  constructor(pluginConfig) {
    const {
      defaultRuntimePlugins,
      reactNativeDeepImports,
      ...config
    } = pluginConfig;
    this.config = config;
    this.deepImports = reactNativeDeepImports ?? true;
    this.defaultRuntimePlugins = defaultRuntimePlugins ?? ['@callstack/repack/mf/core-plugin', '@callstack/repack/mf/resolver-plugin'];
  }
  validateModuleFederationContainerName(name) {
    if (!name) return;
    if (!(0, _estreeUtilIsIdentifierName.name)(name)) {
      const error = new Error(`[RepackModuleFederationPlugin] The container's name: '${name}' must be a valid JavaScript identifier. ` + 'Please correct it to proceed. For more information, see: https://developer.mozilla.org/en-US/docs/Glossary/Identifier');
      // remove the stack trace to make the error more readable
      error.stack = undefined;
      throw error;
    }
  }
  ensureModuleFederationPackageInstalled(context) {
    try {
      require.resolve('@module-federation/enhanced', {
        paths: [context]
      });
    } catch {
      throw new Error("[RepackModuleFederationPlugin] Dependency '@module-federation/enhanced' is required, but not found in your project. " + 'Did you forget to install it?');
    }
  }
  adaptRuntimePlugins(context, runtimePlugins = []) {
    const plugins = runtimePlugins.map(pluginPath => {
      try {
        // resolve the paths to compare against absolute paths
        return require.resolve(pluginPath, {
          paths: [context]
        });
      } catch {
        // ignore invalid paths
        return undefined;
      }
    }).filter(pluginPath => !!pluginPath);
    for (const plugin of this.defaultRuntimePlugins) {
      const pluginPath = require.resolve(plugin);
      if (!plugins.includes(pluginPath)) {
        plugins.unshift(pluginPath);
      }
    }
    return plugins;
  }
  getModuleFederationPlugin(compiler) {
    if ((0, _isRspackCompiler.isRspackCompiler)(compiler)) {
      return require('@module-federation/enhanced/rspack').ModuleFederationPlugin;
    }
    return require('@module-federation/enhanced/webpack').ModuleFederationPlugin;
  }
  getDefaultSharedDependencies() {
    return {
      react: {
        singleton: true,
        eager: true
      },
      'react-native': {
        singleton: true,
        eager: true
      }
    };
  }

  /**
   * As including 'react-native' as a shared dependency is not enough to support
   * deep imports from 'react-native' (e.g. 'react-native/Libraries/Utilities/PixelRatio'),
   * we need to add deep imports using an undocumented feature of ModuleFederationPlugin.
   *
   * When a dependency has a trailing slash, deep imports of that dependency will be correctly
   * resolved by reaching out to the shared scope. This also ensures single instances of things
   * like 'assetsRegistry'. Additionally, we mark every package from '@react-native' group as shared
   * as well, as these are used by React Native too.
   *
   * Reference: https://stackoverflow.com/questions/65636979/wp5-module-federation-sharing-deep-imports
   * Reference: https://github.com/webpack/webpack/blob/main/lib/sharing/resolveMatchedConfigs.js#L77-L79
   *
   * @param shared shared dependencies configuration from ModuleFederationPlugin
   * @returns adjusted shared dependencies configuration
   *
   * @internal
   */
  adaptSharedDependencies(shared) {
    const sharedDependencyConfig = eager => ({
      singleton: true,
      eager: eager ?? true,
      requiredVersion: '*'
    });
    const findSharedDependency = (name, dependencies) => {
      if (Array.isArray(dependencies)) {
        return dependencies.find(item => typeof item === 'string' ? item === name : Boolean(item[name]));
      }
      return dependencies[name];
    };
    const sharedReactNative = findSharedDependency('react-native', shared);
    const reactNativeEager = typeof sharedReactNative === 'object' ? sharedReactNative.eager : undefined;
    if (!this.deepImports || !sharedReactNative) {
      return shared;
    }
    if (Array.isArray(shared)) {
      const adjustedSharedDependencies = [...shared];
      if (!findSharedDependency('react-native/', shared)) {
        adjustedSharedDependencies.push({
          'react-native/': sharedDependencyConfig(reactNativeEager)
        });
      }
      if (!findSharedDependency('@react-native/', shared)) {
        adjustedSharedDependencies.push({
          '@react-native/': sharedDependencyConfig(reactNativeEager)
        });
      }
      return adjustedSharedDependencies;
    }
    const adjustedSharedDependencies = {
      ...shared
    };
    if (!findSharedDependency('react-native/', shared)) {
      Object.assign(adjustedSharedDependencies, {
        'react-native/': sharedDependencyConfig(reactNativeEager)
      });
    }
    if (!findSharedDependency('@react-native/', shared)) {
      Object.assign(adjustedSharedDependencies, {
        '@react-native/': sharedDependencyConfig(reactNativeEager)
      });
    }
    return adjustedSharedDependencies;
  }
  setupIgnoredWarnings(compiler) {
    // MF2 produces warning about not supporting async await
    // we can silence this warning since it works just fine
    compiler.options.ignoreWarnings = compiler.options.ignoreWarnings ?? [];
    compiler.options.ignoreWarnings.push(warning => warning.name === 'EnvironmentNotSupportAsyncWarning');
    // MF2 produces warning about dynamic import in loadEsmEntry but it's not relevant
    // in RN env since we override the loadEntry logic through a hook
    // https://github.com/module-federation/core/blob/fa7a0bd20eb64eccd6648fea340c6078a2268e39/packages/runtime/src/utils/load.ts#L28-L37
    compiler.options.ignoreWarnings.push(warning => {
      if ('moduleDescriptor' in warning) {
        const moduleDescriptor = warning.moduleDescriptor;

        // warning can come from either runtime or runtime-core (in newer versions of MF2)
        const isMF2Runtime = moduleDescriptor.name.endsWith('@module-federation/runtime/dist/index.cjs.js');
        const isMF2RuntimeCore = moduleDescriptor.name.endsWith('@module-federation/runtime-core/dist/index.cjs.js');
        if (isMF2Runtime || isMF2RuntimeCore) {
          const requestExpressionWarning = /Critical dependency: the request of a dependency is an expression/;
          return requestExpressionWarning.test(warning.message);
        }
      }
      return false;
    });
  }
  apply(compiler) {
    this.validateModuleFederationContainerName(this.config.name);
    this.ensureModuleFederationPackageInstalled(compiler.context);
    this.setupIgnoredWarnings(compiler);
    const ModuleFederationPlugin = this.getModuleFederationPlugin(compiler);
    const sharedConfig = this.adaptSharedDependencies(this.config.shared ?? this.getDefaultSharedDependencies());
    const shareStrategyConfig = this.config.shareStrategy ?? 'loaded-first';
    const runtimePluginsConfig = this.adaptRuntimePlugins(compiler.context, this.config.runtimePlugins);

    // NOTE: we keep the default library config since it's the most compatible
    // Default library config uses 'externalType': 'script' and 'type': 'var'
    // var works identical to 'self' since declaring var in a global scope is
    // equal to assigning to the globalObject (normalized by Re.Pack to 'self')
    const config = {
      ...this.config,
      shared: sharedConfig,
      shareStrategy: shareStrategyConfig,
      runtimePlugins: runtimePluginsConfig
    };
    new ModuleFederationPlugin(config).apply(compiler);
  }
}
exports.ModuleFederationPluginV2 = ModuleFederationPluginV2;
//# sourceMappingURL=ModuleFederationPluginV2.js.map