From eb44488b709f027cf706188225dff7d4c8ea97d2 Mon Sep 17 00:00:00 2001
From: Jasper De Moor <jasperdemoor@gmail.com>
Date: Thu, 25 Jul 2024 13:24:43 +0200
Subject: [PATCH 1/2] feat: add support for pkg#imports

---
 .../sandpack-core/src/resolver/resolver.ts    | 50 ++++++++++++++++++-
 .../utils/__snapshots__/pkg-json.test.ts.snap |  3 ++
 .../src/resolver/utils/exports.ts             |  6 +--
 .../src/resolver/utils/glob.test.ts           | 24 +++++++++
 .../sandpack-core/src/resolver/utils/glob.ts  | 43 ++++++++++++++++
 .../src/resolver/utils/imports.ts             | 49 ++++++++++++++++++
 .../src/resolver/utils/pkg-json.ts            | 32 +++++++++++-
 7 files changed, 201 insertions(+), 6 deletions(-)
 create mode 100644 packages/sandpack-core/src/resolver/utils/glob.test.ts
 create mode 100644 packages/sandpack-core/src/resolver/utils/glob.ts
 create mode 100644 packages/sandpack-core/src/resolver/utils/imports.ts

diff --git a/packages/sandpack-core/src/resolver/resolver.ts b/packages/sandpack-core/src/resolver/resolver.ts
index a738b802304..1dca97d61f0 100644
--- a/packages/sandpack-core/src/resolver/resolver.ts
+++ b/packages/sandpack-core/src/resolver/resolver.ts
@@ -12,6 +12,7 @@ import {
   processTSConfig,
   getPotentialPathsFromTSConfig,
 } from './utils/tsconfig';
+import { replaceGlob } from './utils/glob';
 
 export type ResolverCache = Map<string, any>;
 
@@ -264,6 +265,7 @@ function* findPackageJSON(
         content: {
           aliases: {},
           hasExports: false,
+          imports: {},
         },
       };
     }
@@ -343,14 +345,60 @@ function* getTSConfig(
   return config;
 }
 
+function resolvePkgImport(
+  specifier: string,
+  pkgJson: IFoundPackageJSON
+): string {
+  const pkgImports = pkgJson.content.imports;
+  if (!pkgImports) return specifier;
+
+  if (pkgImports[specifier]) {
+    return pkgImports[specifier] as string;
+  }
+
+  for (const [importKey, importValue] of Object.entries(pkgImports)) {
+    if (!importKey.includes('*')) {
+      continue;
+    }
+
+    const match = replaceGlob(importKey, importValue, specifier);
+    if (match) {
+      return match;
+    }
+  }
+
+  return specifier;
+}
+
+function* resolvePkgImports(
+  specifier: string,
+  opts: IResolveOptions
+): Generator<any, string, any> {
+  // Imports always have the `#` prefix
+  if (specifier[0] !== '#') {
+    return specifier;
+  }
+
+  const pkgJson = yield* findPackageJSON(opts.filename, opts);
+  const resolved = resolvePkgImport(specifier, pkgJson);
+  if (resolved !== specifier) {
+    opts.filename = pkgJson.filepath;
+  }
+  return resolved;
+}
+
 function* resolve(
   moduleSpecifier: string,
   inputOpts: IResolveOptionsInput,
   skipIndexExpansion: boolean = false
 ): Generator<any, string, any> {
-  const normalizedSpecifier = normalizeModuleSpecifier(moduleSpecifier);
+  const _normalizedSpecifier = normalizeModuleSpecifier(moduleSpecifier);
   const opts = normalizeResolverOptions(inputOpts);
 
+  const normalizedSpecifier = yield* resolvePkgImports(
+    _normalizedSpecifier,
+    opts
+  );
   const modulePath = yield* resolveModule(normalizedSpecifier, opts);
 
   if (modulePath[0] !== '/') {
diff --git a/packages/sandpack-core/src/resolver/utils/__snapshots__/pkg-json.test.ts.snap b/packages/sandpack-core/src/resolver/utils/__snapshots__/pkg-json.test.ts.snap
index 7b4367c2343..69d9b165830 100644
--- a/packages/sandpack-core/src/resolver/utils/__snapshots__/pkg-json.test.ts.snap
+++ b/packages/sandpack-core/src/resolver/utils/__snapshots__/pkg-json.test.ts.snap
@@ -19,6 +19,7 @@ exports[`process package.json Should correctly handle nested pkg#exports fields
     "/node_modules/solid-js/web/dist/*": "/node_modules/solid-js/web/dist/$1",
   },
   "hasExports": true,
+  "imports": {},
 }
 `;
 
@@ -41,6 +42,7 @@ exports[`process package.json Should correctly handle root pkg.json 1`] = `
     "something/*": "/nested/test.js/$1",
   },
   "hasExports": false,
+  "imports": {},
 }
 `;
 
@@ -230,5 +232,6 @@ exports[`process package.json Should correctly process pkg.exports from @babel/r
     "/node_modules/@babel/runtime/regenerator/*.js": "/node_modules/@babel/runtime/regenerator/$1.js",
   },
   "hasExports": true,
+  "imports": {},
 }
 `;
diff --git a/packages/sandpack-core/src/resolver/utils/exports.ts b/packages/sandpack-core/src/resolver/utils/exports.ts
index 999994d0e11..7948f4b4c7d 100644
--- a/packages/sandpack-core/src/resolver/utils/exports.ts
+++ b/packages/sandpack-core/src/resolver/utils/exports.ts
@@ -3,16 +3,16 @@ import { normalizeAliasFilePath } from './alias';
 // exports keys, sorted from high to low priority
 const EXPORTS_KEYS = ['browser', 'development', 'default', 'require', 'import'];
 
-type PackageExportType =
+export type PackageExportType =
   | string
   | null
   | false
   | PackageExportObj
   | PackageExportArr;
 
-type PackageExportArr = Array<PackageExportObj | string>;
+export type PackageExportArr = Array<PackageExportObj | string>;
 
-type PackageExportObj = {
+export type PackageExportObj = {
   [key: string]: string | null | false | PackageExportType;
 };
 
diff --git a/packages/sandpack-core/src/resolver/utils/glob.test.ts b/packages/sandpack-core/src/resolver/utils/glob.test.ts
new file mode 100644
index 00000000000..6b16d7550f3
--- /dev/null
+++ b/packages/sandpack-core/src/resolver/utils/glob.test.ts
@@ -0,0 +1,24 @@
+import { replaceGlob } from './glob';
+
+describe('glob utils', () => {
+  it('replace glob at the end', () => {
+    expect(
+      replaceGlob('#test/*', './something/*/index.js', '#test/hello')
+    ).toBe('./something/hello/index.js');
+  });
+
+  it('replaces glob and target at the end', () => {
+    const input = replaceGlob(
+      '/@test/foo/*',
+      '/@test/foo/dist/*',
+      '/@test/foo/dist/index'
+    );
+    expect(input).toBe('/@test/foo/dist/index');
+  });
+
+  it('replace glob in the middle', () => {
+    expect(replaceGlob('#test/*.js', './test/*.js', '#test/hello.js')).toBe(
+      './test/hello.js'
+    );
+  });
+});
diff --git a/packages/sandpack-core/src/resolver/utils/glob.ts b/packages/sandpack-core/src/resolver/utils/glob.ts
new file mode 100644
index 00000000000..21c89e6f7c6
--- /dev/null
+++ b/packages/sandpack-core/src/resolver/utils/glob.ts
@@ -0,0 +1,43 @@
+export function replaceGlob(
+  source: string,
+  target: string,
+  specifier: string
+): false | string {
+  const starIndex = source.indexOf('*');
+  if (starIndex < 0) {
+    return false;
+  }
+
+  const prefix = source.substring(0, starIndex);
+  const suffix = source.substring(starIndex + 1);
+  if (
+    !specifier.startsWith(prefix) ||
+    (suffix && !specifier.endsWith(suffix))
+  ) {
+    return false;
+  }
+
+  const targetStarLocation = target.indexOf('*');
+  const targetBeforeStar = target.substring(0, targetStarLocation);
+
+  if (specifier.indexOf(targetBeforeStar) > -1) {
+    return (
+      targetBeforeStar +
+      specifier.substring(targetBeforeStar.length, specifier.length)
+    );
+  }
+
+  if (targetStarLocation < 0) {
+    return target;
+  }
+
+  const globPart = specifier.substring(
+    prefix.length,
+    specifier.length - suffix.length
+  );
+  return (
+    target.substring(0, targetStarLocation) +
+    globPart +
+    target.substring(targetStarLocation + 1)
+  );
+}
diff --git a/packages/sandpack-core/src/resolver/utils/imports.ts b/packages/sandpack-core/src/resolver/utils/imports.ts
new file mode 100644
index 00000000000..ab1b7a04a60
--- /dev/null
+++ b/packages/sandpack-core/src/resolver/utils/imports.ts
@@ -0,0 +1,49 @@
+type PackageImportArr = Array<PackageImportObj | string>;
+type PackageImportType =
+  | string
+  | null
+  | false
+  | PackageImportObj
+  | PackageImportArr;
+type PackageImportObj = {
+  [key: string]: string | null | false | PackageImportType;
+};
+
+export function extractSpecifierFromImport(
+  importValue: PackageImportType,
+  pkgRoot: string,
+  importKeys: string[]
+): string | false {
+  if (!importValue) {
+    return false;
+  }
+
+  if (typeof importValue === 'string') {
+    return importValue;
+  }
+
+  if (Array.isArray(importValue)) {
+    const foundPaths = importValue
+      .map(v => extractSpecifierFromImport(v, pkgRoot, importKeys))
+      .filter(Boolean);
+    if (!foundPaths.length) {
+      return false;
+    }
+    return foundPaths[0];
+  }
+
+  if (typeof importValue === 'object') {
+    for (const key of importKeys) {
+      const importFilename = importValue[key];
+      if (importFilename !== undefined) {
+        if (typeof importFilename === 'string') {
+          return importFilename;
+        }
+        return extractSpecifierFromImport(importFilename, pkgRoot, importKeys);
+      }
+    }
+    return false;
+  }
+
+  throw new Error(`Unsupported imports type ${typeof importValue}`);
+}
diff --git a/packages/sandpack-core/src/resolver/utils/pkg-json.ts b/packages/sandpack-core/src/resolver/utils/pkg-json.ts
index 5fd3490031f..4c0f1ffa539 100644
--- a/packages/sandpack-core/src/resolver/utils/pkg-json.ts
+++ b/packages/sandpack-core/src/resolver/utils/pkg-json.ts
@@ -1,16 +1,23 @@
 import { normalizeAliasFilePath } from './alias';
 import { extractPathFromExport } from './exports';
 import { EMPTY_SHIM } from './constants';
+import { extractSpecifierFromImport } from './imports';
 
 // alias/exports/main keys, sorted from high to low priority
 const MAIN_PKG_FIELDS = ['module', 'browser', 'main', 'jsnext:main'];
 const PKG_ALIAS_FIELDS = ['browser', 'alias'];
+const IMPORTS_KEYS = ['browser', 'development', 'default', 'require', 'import'];
+
+function sortDescending(a: string, b: string) {
+  return b.length - a.length;
+}
 
 type AliasesDict = { [key: string]: string };
 
 export interface ProcessedPackageJSON {
   aliases: AliasesDict;
   hasExports: boolean;
+  imports: AliasesDict;
 }
 
 // See https://webpack.js.org/guides/package-exports/ for a good reference on how this should work
@@ -20,7 +27,7 @@ export function processPackageJSON(
   pkgRoot: string
 ): ProcessedPackageJSON {
   if (!content || typeof content !== 'object') {
-    return { aliases: {}, hasExports: false };
+    return { aliases: {}, hasExports: false, imports: {} };
   }
 
   const aliases: AliasesDict = {};
@@ -83,5 +90,26 @@ export function processPackageJSON(
     }
   }
 
-  return { aliases, hasExports };
+  // load imports
+  const imports: AliasesDict = {};
+  if (content.imports) {
+    if (typeof content.imports === 'object') {
+      for (const importKey of Object.keys(content.imports).sort(
+        sortDescending
+      )) {
+        const value = extractSpecifierFromImport(
+          content.imports[importKey],
+          pkgRoot,
+          IMPORTS_KEYS
+        );
+        imports[importKey] = value || EMPTY_SHIM;
+      }
+    }
+  }
+
+  return {
+    aliases,
+    hasExports,
+    imports,
+  };
 }

From 56ead31e15e42d88b518605341e763fe6769a5a1 Mon Sep 17 00:00:00 2001
From: Jasper De Moor <jasperdemoor@gmail.com>
Date: Mon, 29 Jul 2024 16:49:58 +0000
Subject: [PATCH 2/2] resolver tweaks

---
 .../sandpack-core/src/resolver/resolver.ts    | 43 ++++++++++++-------
 1 file changed, 27 insertions(+), 16 deletions(-)

diff --git a/packages/sandpack-core/src/resolver/resolver.ts b/packages/sandpack-core/src/resolver/resolver.ts
index 1dca97d61f0..6a3e99969f8 100644
--- a/packages/sandpack-core/src/resolver/resolver.ts
+++ b/packages/sandpack-core/src/resolver/resolver.ts
@@ -220,17 +220,20 @@ function* resolveNodeModule(
             : yield* loadNearestPackageJSON(pkgFilePath, opts, rootDir);
         if (pkgJson) {
           try {
-            return yield* resolve(pkgFilePath, {
+            return yield* internalResolve(pkgFilePath, {
               ...opts,
               filename: pkgJson.filepath,
               pkgJson,
             });
           } catch (err) {
             if (!pkgSpecifierParts.filepath) {
-              return yield* resolve(pathUtils.join(pkgFilePath, 'index'), {
-                ...opts,
-                filename: pkgJson.filepath,
-              });
+              return yield* internalResolve(
+                pathUtils.join(pkgFilePath, 'index'),
+                {
+                  ...opts,
+                  filename: pkgJson.filepath,
+                }
+              );
             }
 
             throw err;
@@ -387,18 +390,12 @@ function* resolvePkgImports(
   return resolved;
 }
 
-function* resolve(
+function* internalResolve(
   moduleSpecifier: string,
-  inputOpts: IResolveOptionsInput,
+  opts: IResolveOptions,
   skipIndexExpansion: boolean = false
 ): Generator<any, string, any> {
-  const _normalizedSpecifier = normalizeModuleSpecifier(moduleSpecifier);
-  const opts = normalizeResolverOptions(inputOpts);
-
-  const normalizedSpecifier = yield* resolvePkgImports(
-    _normalizedSpecifier,
-    opts
-  );
+  const normalizedSpecifier = normalizeModuleSpecifier(moduleSpecifier);
   const modulePath = yield* resolveModule(normalizedSpecifier, opts);
 
   if (modulePath[0] !== '/') {
@@ -412,7 +409,7 @@ function* resolve(
         );
         for (const potentialPath of potentialPaths) {
           try {
-            return yield* resolve(potentialPath, opts);
+            return yield* internalResolve(potentialPath, opts);
           } catch {
             // do nothing, it's probably a node_module in this case
           }
@@ -438,7 +435,11 @@ function* resolve(
       try {
         const parts = moduleSpecifier.split('/');
         if (!parts.length || !parts[parts.length - 1].startsWith('index')) {
-          foundFile = yield* resolve(moduleSpecifier + '/index', opts, true);
+          foundFile = yield* internalResolve(
+            moduleSpecifier + '/index',
+            opts,
+            true
+          );
         }
       } catch (err) {
         // should throw ModuleNotFound for original specifier, not new one
@@ -453,6 +454,16 @@ function* resolve(
   return foundFile;
 }
 
+function* resolve(
+  moduleSpecifier: string,
+  inputOpts: IResolveOptionsInput,
+  skipIndexExpansion: boolean = false
+): Generator<any, string, any> {
+  const opts = normalizeResolverOptions(inputOpts);
+  const specifier = yield* resolvePkgImports(moduleSpecifier, opts);
+  return yield* internalResolve(specifier, opts, skipIndexExpansion);
+}
+
 export const resolver = gensync<
   (
     moduleSpecifier: string,