content: // Copyright 2024 Google Inc. Use of this source code is governed by an
  // MIT-style license that can be found in the LICENSE file or at
  // https://opensource.org/licenses/MIT.
  
  import 'package:cli_pkg/js.dart';
  import 'package:sass/src/util/map.dart';
  import 'package:sass/src/util/nullable.dart';
  
  import '../importer.dart';
  import './utils.dart';
  import 'dart:convert';
  import '../io.dart';
  import 'package:path/path.dart' as p;
  
  /// An [Importer] that resolves `pkg:` URLs using the Node resolution algorithm.
  class NodePackageImporter extends Importer {
    /// The starting path for canonicalizations without a containing URL.
    late final String _entryPointDirectory;
  
    /// Creates a Node package importer with the associated entry point.
    NodePackageImporter(String entryPointDirectory) {
      if (isBrowser) {
        throw "The Node package importer cannot be used without a filesystem.";
      }
      _entryPointDirectory = p.absolute(entryPointDirectory);
    }
  
    @override
    bool isNonCanonicalScheme(String scheme) => scheme == 'pkg';
  
    @override
    Uri? canonicalize(Uri url) {
      if (url.scheme == 'file') return FilesystemImporter.cwd.canonicalize(url);
      if (url.scheme != 'pkg') return null;
  
      if (url.hasAuthority) {
        throw "A pkg: URL must not have a host, port, username or password.";
      } else if (p.url.isAbsolute(url.path)) {
        throw "A pkg: URL's path must not begin with /.";
      } else if (url.path.isEmpty) {
        throw "A pkg: URL must not have an empty path.";
      } else if (url.hasQuery || url.hasFragment) {
        throw "A pkg: URL must not have a query or fragment.";
      }
  
      var baseDirectory = containingUrl?.scheme == 'file'
          ? p.dirname(p.fromUri(containingUrl!))
          : _entryPointDirectory;
  
      var (packageName, subpath) = _packageNameAndSubpath(url.path);
  
      // If the package name is not a valid Node package name, return null in case
      // another importer can handle.
      if (packageName.startsWith('.') ||
          packageName.contains('\\') ||
          packageName.contains('%') ||
          (packageName.startsWith('@') &&
              !packageName.contains(p.url.separator))) {
        return null;
      }
  
      var packageRoot = _resolvePackageRoot(packageName, baseDirectory);
  
      if (packageRoot == null) return null;
      var jsonPath = p.join(packageRoot, 'package.json');
  
      var jsonString = readFile(jsonPath);
      Map<String, dynamic> packageManifest;
      try {
        packageManifest = json.decode(jsonString) as Map<String, dynamic>;
      } catch (e) {
        throw "Failed to parse $jsonPath for \"pkg:$packageName\": $e";
      }
  
      if (_resolvePackageExports(
        packageRoot,
        subpath,
        packageManifest,
        packageName,
      )
          case var resolved?) {
        if (_validExtensions.contains(p.extension(resolved))) {
          return p.toUri(p.canonicalize(resolved));
        } else {
          throw "The export for '${subpath ?? "root"}' in "
              "'$packageName' resolved to '${resolved.toString()}', "
              "which is not a '.scss', '.sass', or '.css' file.";
        }
      }
      // If no subpath, attempt to resolve `sass` or `style` key in package.json,
      // then `index` file at package root, resolved for file extensions and
      // partials.
      if (subpath == null) {
        var rootPath = _resolvePackageRootValues(packageRoot, packageManifest);
        return rootPath != null ? p.toUri(p.canonicalize(rootPath)) : null;
      }
  
      // If there is a subpath, attempt to resolve the path relative to the
      // package root, and resolve for file extensions and partials.
      var subpathInRoot = p.join(packageRoot, subpath);
      return FilesystemImporter.cwd.canonicalize(p.toUri(subpathInRoot));
    }
  
    @override
    ImporterResult? load(Uri url) => FilesystemImporter.cwd.load(url);
  
    /// Splits a [bare import
    /// specifier](https://nodejs.org/api/esm.html#import-specifiers) `specifier`
    /// into its package name and subpath, if one exists.
    ///
    /// Because this is a bare import specifier and not a path, we always use `/`
    /// to avoid invalid values on non-Posix machines.
    (String, String?) _packageNameAndSubpath(String specifier) {
      var parts = p.url.split(specifier);
      var name = p.fromUri(parts.removeAt(0));
  
      if (name.startsWith('@')) {
        if (parts.isNotEmpty) name = p.url.join(name, parts.removeAt(0));
      }
      var subpath = parts.isNotEmpty ? p.fromUri(p.url.joinAll(parts)) : null;
      return (name, subpath);
    }
  
    /// Returns an absolute path to the root directory for the most proximate
    /// installed `packageName`.
    ///
    /// Implementation of `PACKAGE_RESOLVE` from the [Resolution Algorithm
    /// Specification](https://nodejs.org/api/esm.html#resolution-algorithm-specification).
    String? _resolvePackageRoot(String packageName, String baseDirectory) {
      while (true) {
        var potentialPackage = p.join(baseDirectory, 'node_modules', packageName);
        if (dirExists(potentialPackage)) return potentialPackage;
        // baseDirectory has now reached root without finding a match.
        if (p.split(baseDirectory).length == 1) return null;
        baseDirectory = p.dirname(baseDirectory);
      }
    }
  
    /// Returns a file path specified by the `sass` or `style` values in a package
    /// manifest, or an `index` file relative to the package root.
    String? _resolvePackageRootValues(
      String packageRoot,
      Map<String, dynamic> packageManifest,
    ) {
      if (packageManifest['sass'] case String sassValue
          when _validExtensions.contains(p.url.extension(sassValue))) {
        return p.join(packageRoot, sassValue);
      } else if (packageManifest['style'] case String styleValue
          when _validExtensions.contains(p.url.extension(styleValue))) {
        return p.join(packageRoot, styleValue);
      }
  
      var result = resolveImportPath(p.join(packageRoot, 'index'));
      return result;
    }
  
    /// Returns a file path specified by a `subpath` in the `exports` section of
    /// package.json.
    ///
    /// `packageName` is used for error reporting.
    String? _resolvePackageExports(
      String packageRoot,
      String? subpath,
      Map<String, dynamic> packageManifest,
      String packageName,
    ) {
      var exports = packageManifest['exports'] as Object?;
      if (exports == null) return null;
      var subpathVariants = _exportsToCheck(subpath);
      if (_nodePackageExportsResolve(
        packageRoot,
        subpathVariants,
        exports,
        subpath,
        packageName,
      )
          case var path?) {
        return path;
      }
  
      if (subpath != null && p.url.extension(subpath).isNotEmpty) return null;
  
      var subpathIndexVariants = _exportsToCheck(subpath, addIndex: true);
      if (_nodePackageExportsResolve(
        packageRoot,
        subpathIndexVariants,
        exports,
        subpath,
        packageName,
      )
          case var path?) {
        return path;
      }
  
      return null;
    }
  
    /// Returns the path to one subpath variant, resolved in the `exports` of a
    /// package manifest.
    ///
    /// Throws an error if multiple `subpathVariants` match, and null if none
    /// match.
    ///
    /// Implementation of `PACKAGE_EXPORTS_RESOLVE` from the [Resolution Algorithm
    /// Specification](https://nodejs.org/api/esm.html#resolution-algorithm-specification).
    String? _nodePackageExportsResolve(
      String packageRoot,
      List<String?> subpathVariants,
      Object exports,
      String? subpath,
      String packageName,
    ) {
      if (exports is Map<String, dynamic> &&
          exports.keys.any((key) => key.startsWith('.')) &&
          exports.keys.any((key) => !key.startsWith('.'))) {
        throw '`exports` in $packageName can not have both conditions and paths '
            'at the same level.\n'
            'Found ${exports.keys.map((key) => '"$key"').join(',')} in '
            '${p.join(packageRoot, 'package.json')}.';
      }
  
      var matches = subpathVariants
          .map((String? variant) {
            if (variant == null) {
              return _getMainExport(exports).andThen(
                (mainExport) =>
                    _packageTargetResolve(variant, mainExport, packageRoot),
              );
            } else if (exports is! Map<String, dynamic> ||
                exports.keys.every((key) => !key.startsWith('.'))) {
              return null;
            }
            var matchKey = "./${p.toUri(variant)}";
            if (exports.containsKey(matchKey) &&
                exports[matchKey] != null &&
                !matchKey.contains('*')) {
              return _packageTargetResolve(
                matchKey,
                exports[matchKey] as Object,
                packageRoot,
              );
            }
  
            var expansionKeys = [
              for (var key in exports.keys)
                if ('*'.allMatches(key).length == 1) key,
            ]..sort(_compareExpansionKeys);
  
            for (var expansionKey in expansionKeys) {
              var [patternBase, patternTrailer] = expansionKey.split('*');
              if (!matchKey.startsWith(patternBase)) continue;
              if (matchKey == patternBase) continue;
              if (patternTrailer.isEmpty ||
                  (matchKey.endsWith(patternTrailer) &&
                      matchKey.length >= expansionKey.length)) {
                var target = exports[expansionKey] as Object?;
                if (target == null) continue;
                var patternMatch = matchKey.substring(
                  patternBase.length,
                  matchKey.length - patternTrailer.length,
                );
                return _packageTargetResolve(
                  variant,
                  target,
                  packageRoot,
                  patternMatch,
                );
              }
            }
  
            return null;
          })
          .nonNulls
          .toSet()
          .toList();
  
      return switch (matches) {
        [var path] => path,
        [] => null,
        var paths =>
          throw "Unable to determine which of multiple potential resolutions "
              "found for ${subpath ?? 'root'} in $packageName should be used. "
              "\n\nFound:\n"
              "${paths.join('\n')}",
      };
    }
  
    /// Implementation of the `PATTERN_KEY_COMPARE` comparator from
    /// https://nodejs.org/api/esm.html#resolution-algorithm-specification.
    int _compareExpansionKeys(String keyA, String keyB) {
      var baseLengthA = keyA.contains('*') ? keyA.indexOf('*') + 1 : keyA.length;
      var baseLengthB = keyB.contains('*') ? keyB.indexOf('*') + 1 : keyB.length;
      if (baseLengthA > baseLengthB) return -1;
      if (baseLengthB > baseLengthA) return 1;
      if (!keyA.contains("*")) return 1;
      if (!keyB.contains("*")) return -1;
      if (keyA.length > keyB.length) return -1;
      if (keyB.length > keyA.length) return 1;
      return 0;
    }
  
    /// Returns a file path for `subpath`, as resolved in the `exports` object.
    ///
    /// Verifies the file exists relative to `packageRoot`. Instances of `*` will
    /// be replaced with `patternMatch`.
    ///
    /// `subpath` and `packageRoot` are native paths, and `patternMatch` is a URL
    /// path.
    ///
    /// Implementation of `PACKAGE_TARGET_RESOLVE` from the [Resolution Algorithm
    /// Specification](https://nodejs.org/api/esm.html#resolution-algorithm-specification).
    String? _packageTargetResolve(
      String? subpath,
      Object exports,
      String packageRoot, [
      String? patternMatch,
    ]) {
      switch (exports) {
        case String string when !string.startsWith('./'):
          throw "Export '$string' must be a path relative to the package root at '$packageRoot'.";
        case String string when patternMatch != null:
          var replaced = p.fromUri(string.replaceFirst('*', patternMatch));
          var path = p.normalize(p.join(packageRoot, replaced));
          return fileExists(path) ? path : null;
        case String string:
          return p.join(packageRoot, p.fromUri(string));
        case Map<String, dynamic> map:
          for (var (key, value) in map.pairs) {
            if (!const {'sass', 'style', 'default'}.contains(key)) continue;
            if (value == null) continue;
            if (_packageTargetResolve(
              subpath,
              value as Object,
              packageRoot,
              patternMatch,
            )
                case var result?) {
              return result;
            }
          }
          return null;
  
        case []:
          return null;
  
        case List<dynamic> array:
          for (var value in array) {
            if (value == null) continue;
            if (_packageTargetResolve(
              subpath,
              value as Object,
              packageRoot,
              patternMatch,
            )
                case var result?) {
              return result;
            }
          }
  
          return null;
  
        default:
          throw "Invalid 'exports' value $exports in "
              "${p.join(packageRoot, 'package.json')}.";
      }
    }
  
    /// Returns a path to a package's export without a subpath.
    Object? _getMainExport(Object exports) {
      return switch (exports) {
        String string => string,
        List<String> list => list,
        Map<String, dynamic> map
            when !map.keys.any((key) => key.startsWith('.')) =>
          map,
        <String, dynamic>{'.': var export?} => export,
        _ => null,
      };
    }
  
    /// Returns a list of all possible variations of `subpath` with extensions and
    /// partials.
    ///
    /// If there is no subpath, returns a single `null` value, which is used in
    /// `_nodePackageExportsResolve` to denote the main package export.
    List<String?> _exportsToCheck(String? subpath, {bool addIndex = false}) {
      var paths = <String>[];
  
      if (subpath == null && addIndex) {
        subpath = 'index';
      } else if (subpath != null && addIndex) {
        subpath = p.join(subpath, 'index');
      }
      if (subpath == null) return [null];
  
      if (_validExtensions.contains(p.url.extension(subpath))) {
        paths.add(subpath);
      } else {
        paths.addAll([subpath, '$subpath.scss', '$subpath.sass', '$subpath.css']);
      }
      var basename = p.basename(subpath);
      var dirname = p.dirname(subpath);
  
      if (basename.startsWith('_')) return paths;
  
      return [
        ...paths,
        for (var path in paths)
          if (dirname == '.')
            '_${p.basename(path)}'
          else
            p.join(dirname, '_${p.basename(path)}'),
      ];
    }
  }
  
  /// The set of file extensions that Sass can parse.
  ///
  /// `NodePackageImporter` will only resolve files with these extensions, and
  /// uses these extensions to check for matches if no extension is provided in
  /// the Url to canonicalize.
  const _validExtensions = {'.scss', '.sass', '.css'};
  