diff --git a/src/transforms/mdx/esm-module-loader/module-fetcher/index.ts b/src/transforms/mdx/esm-module-loader/module-fetcher/index.ts index fcd03d5d36..d16eafe193 100644 --- a/src/transforms/mdx/esm-module-loader/module-fetcher/index.ts +++ b/src/transforms/mdx/esm-module-loader/module-fetcher/index.ts @@ -21,7 +21,7 @@ import { getModulePathCache } from "../cache/index.ts"; import { hashString } from "../utils/hash.ts"; import { resolveModuleFile } from "../resolution/file-finder.ts"; import { getTransformCacheKey, getVersionedPathCacheKey } from "./cache-keys.ts"; -import { resolveNestedModuleImports } from "./nested-imports.ts"; +import { resolveNestedImportBase, resolveNestedModuleImports } from "./nested-imports.ts"; import { readDistributedCache } from "./distributed-cache.ts"; import { resolveUnresolvedModuleViaHttpFallback } from "./http-fallback.ts"; import { normalizePath } from "./module-cache.ts"; @@ -285,6 +285,7 @@ async function doFetchAndCacheModule( moduleCode, esmCacheDir, normalizedPath, + parentBasePath: resolveNestedImportBase(normalizedPath, actualFilePath), strictMissingModules: context.strictMissingModules ?? true, projectSlug, fetchAndCacheModule: fetchAndCacheModuleFn, diff --git a/src/transforms/mdx/esm-module-loader/module-fetcher/nested-imports.test.ts b/src/transforms/mdx/esm-module-loader/module-fetcher/nested-imports.test.ts index c869e08b37..3d1e82d80e 100644 --- a/src/transforms/mdx/esm-module-loader/module-fetcher/nested-imports.test.ts +++ b/src/transforms/mdx/esm-module-loader/module-fetcher/nested-imports.test.ts @@ -4,6 +4,7 @@ import { describe, it } from "#veryfront/testing/bdd.ts"; import { findNestedImports, hasUnresolvedImports, + resolveNestedImportBase, resolveNestedModuleImports, } from "./nested-imports.ts"; @@ -159,3 +160,56 @@ import { bar } from "./local.js"; }); }); }); + +describe("resolveNestedImportBase", () => { + // A barrel lives at lib/index.ts but is addressed as _vf_modules/lib. + // Resolving its children against "_vf_modules/lib.js" drops the "lib" + // segment, so ./constants.js resolved to _vf_modules/constants.js — one + // directory too high. The file was then stubbed and the barrel silently + // stopped re-exporting: "does not provide an export named 'COLORS'". + it("keeps the directory segment for an index module", () => { + assertEquals( + resolveNestedImportBase("_vf_modules/lib.js", "/project/lib/index.ts"), + "_vf_modules/lib/index.js", + ); + assertEquals( + resolveNestedImportBase("_vf_modules/components.js", "/project/components/index.tsx"), + "_vf_modules/components/index.js", + ); + }); + + it("leaves a plain module untouched", () => { + assertEquals( + resolveNestedImportBase("_vf_modules/lib/constants.js", "/project/lib/constants.ts"), + "_vf_modules/lib/constants.js", + ); + }); + + it("does not double up when the path already names index", () => { + assertEquals( + resolveNestedImportBase("_vf_modules/lib/index.js", "/project/lib/index.ts"), + "_vf_modules/lib/index.js", + ); + }); + + it("is a no-op without a resolved file path", () => { + assertEquals(resolveNestedImportBase("_vf_modules/lib.js"), "_vf_modules/lib.js"); + }); + + it("does not treat a file merely named index-something as an index module", () => { + assertEquals( + resolveNestedImportBase("_vf_modules/lib.js", "/project/lib/indexer.ts"), + "_vf_modules/lib.js", + ); + }); + + it("recognises every index extension the resolver accepts", () => { + for (const ext of ["ts", "tsx", "js", "jsx", "mdx", "md"]) { + assertEquals( + resolveNestedImportBase("_vf_modules/lib.js", `/project/lib/index.${ext}`), + "_vf_modules/lib/index.js", + ext, + ); + } + }); +}); diff --git a/src/transforms/mdx/esm-module-loader/module-fetcher/nested-imports.ts b/src/transforms/mdx/esm-module-loader/module-fetcher/nested-imports.ts index 1cf5544b2b..e83fe5bd77 100644 --- a/src/transforms/mdx/esm-module-loader/module-fetcher/nested-imports.ts +++ b/src/transforms/mdx/esm-module-loader/module-fetcher/nested-imports.ts @@ -132,6 +132,38 @@ export interface ResolveNestedModuleImportsInput { strictMissingModules: boolean; fetchAndCacheModule: (path: string, parent?: string) => Promise; log?: Logger; + /** + * Path this module's relative imports resolve against. Defaults to + * `normalizedPath`; see {@link resolveNestedImportBase}. + */ + parentBasePath?: string; +} + +/** + * The path a module's own relative imports should resolve against. + * + * A directory barrel lives at `lib/index.ts` but is addressed as + * `_vf_modules/lib`. Resolving its children against `_vf_modules/lib.js` drops + * the trailing segment as if it were a filename, so `./constants.js` becomes + * `_vf_modules/constants.js` — one directory too high. The file is then not + * found and gets replaced by a stub, and the barrel silently stops re-exporting + * anything: `does not provide an export named 'COLORS'`. + * + * When the module actually resolved to an index file, keep the directory + * segment by addressing it as `/index.js`. + */ +export function resolveNestedImportBase( + normalizedPath: string, + actualFilePath?: string, +): string { + if (!actualFilePath || !/(?:^|\/)index\.(?:tsx?|jsx?|mdx|md)$/.test(actualFilePath)) { + return normalizedPath; + } + + const withoutExt = normalizedPath.replace(/\.(?:js|mjs)$/, ""); + if (withoutExt.endsWith("/index")) return normalizedPath; + + return `${withoutExt}/index.js`; } /** @@ -167,7 +199,10 @@ export async function resolveNestedModuleImports( original, start, end, - nestedFilePath: await input.fetchAndCacheModule(path, input.normalizedPath), + nestedFilePath: await input.fetchAndCacheModule( + path, + input.parentBasePath ?? input.normalizedPath, + ), [key]: path, })), );