diff --git a/src/lib/exports.ts b/src/lib/exports.ts
index 2ce05a6..d06fde2 100644
--- a/src/lib/exports.ts
+++ b/src/lib/exports.ts
@@ -337,6 +337,38 @@ const resolveConcreteExport = async (
}
};
+/**
+ * Widen an exports-derived source pattern into a directory-crossing glob.
+ *
+ * A package.json `exports` wildcard matches subpaths *including* `/` — the
+ * `./*.js` key resolves `./auth/session.js` with `*` capturing `auth/session`.
+ * A plain glob `*`, by contrast, stops at a directory separator, so globbing
+ * the mapped `src/lib/*.ts` would match only top-level modules and silently
+ * drop every nested one. When the wildcard sits after a directory separator
+ * (the ubiquitous `
/*.ext` shape), splice in a globstar segment so the
+ * glob crosses directories the way Node's resolver does; the globstar matches
+ * zero-or-more segments, so top-level files still match.
+ *
+ * Two shapes are deliberately left un-widened:
+ * - A mid-segment wildcard (`prefix-*.ts`) has no faithful globstar translation.
+ * - A bare-root wildcard (`*.ts`, from an empty `sourceDir`) would widen to a
+ * project-root `**` that rakes in `node_modules`/`dist`. Empty `sourceDir`
+ * only arises from the multi-`sourcePaths` no-common-prefix layout (which
+ * `discoverSourceFiles` short-circuits before reaching here) or an explicit
+ * `sourceRoot: ''`; leaving it non-recursive matches the prior behavior
+ * without the explosion risk.
+ *
+ * Both fall through to matching same-directory files only.
+ */
+const toRecursiveExportGlob = (pattern: string): string => {
+ const star = pattern.indexOf('*');
+ if (star === -1) return pattern;
+ const before = pattern.slice(0, star);
+ const after = pattern.slice(star + 1);
+ if (before.endsWith('/')) return `${before}**/*${after}`;
+ return pattern;
+};
+
/**
* Expand a wildcard export pattern to matching source files.
*/
@@ -348,10 +380,12 @@ const expandWildcardExport = async (
exclude: Array | undefined,
discovered: Map,
): Promise => {
- const sourcePattern = mapDistToSource(distPath, condition, mappingOptions);
- if (!sourcePattern) return;
+ const mappedPattern = mapDistToSource(distPath, condition, mappingOptions);
+ if (!mappedPattern) return;
- // Convert mapped pattern to glob: "src/lib/*.ts" is already a valid glob
+ // The exports `*` crosses directory separators; a glob `*` does not — widen
+ // it so nested modules aren't silently dropped.
+ const sourcePattern = toRecursiveExportGlob(mappedPattern);
const patterns = [sourcePattern];
// For .ts patterns, also try .svelte and .js
diff --git a/src/test/exports.test.ts b/src/test/exports.test.ts
index 27e9b42..eac304c 100644
--- a/src/test/exports.test.ts
+++ b/src/test/exports.test.ts
@@ -358,6 +358,87 @@ describe('discoverFromExports', () => {
});
});
+ test('discovers wildcard exports in nested directories', async () => {
+ // A package.json `exports` wildcard (`./*.js`) matches subpaths including
+ // `/` — `@scope/pkg/auth/session.js` resolves through it — so discovery
+ // must recurse into subdirectories, not just the top-level source dir.
+ await withTestDir(async (dir) => {
+ await mkdir(join(dir, 'src/lib/auth'), {recursive: true});
+ await mkdir(join(dir, 'src/lib/db/deep'), {recursive: true});
+ await writeFile(join(dir, 'src/lib/root.ts'), 'export const r = 0;');
+ await writeFile(join(dir, 'src/lib/auth/session.ts'), 'export const s = 1;');
+ await writeFile(join(dir, 'src/lib/auth/Login.svelte'), 'login
');
+ await writeFile(join(dir, 'src/lib/db/deep/nested.ts'), 'export const n = 2;');
+ await writeFile(
+ join(dir, 'package.json'),
+ JSON.stringify({
+ exports: {
+ './*.js': {types: './dist/*.d.ts', default: './dist/*.js'},
+ './*.svelte': {svelte: './dist/*.svelte', default: './dist/*.svelte'},
+ },
+ }),
+ );
+
+ const {files} = await discoverFromExports({projectRoot: dir});
+ assert.ok(files);
+ const paths = files.map((f) => f.id).sort();
+ assert.strictEqual(files.length, 4);
+ assert.ok(paths.some((p) => p.endsWith('src/lib/root.ts')));
+ assert.ok(paths.some((p) => p.endsWith('src/lib/auth/session.ts')));
+ assert.ok(paths.some((p) => p.endsWith('src/lib/auth/Login.svelte')));
+ assert.ok(paths.some((p) => p.endsWith('src/lib/db/deep/nested.ts')));
+ });
+ });
+
+ test('honors exclude for nested wildcard matches', async () => {
+ // Recursive wildcard expansion means `exclude` now governs nested files
+ // that a non-recursive `src/lib/*.ts` glob never reached. A co-located
+ // test file (or any excluded subtree) must still be dropped.
+ await withTestDir(async (dir) => {
+ await mkdir(join(dir, 'src/lib/auth'), {recursive: true});
+ await writeFile(join(dir, 'src/lib/auth/session.ts'), 'export const s = 1;');
+ await writeFile(join(dir, 'src/lib/auth/session.test.ts'), 'export const t = 1;');
+ await writeFile(
+ join(dir, 'package.json'),
+ JSON.stringify({
+ exports: {'./*.js': {types: './dist/*.d.ts', default: './dist/*.js'}},
+ }),
+ );
+
+ const {files} = await discoverFromExports({
+ projectRoot: dir,
+ exclude: ['**/*.test.ts'],
+ });
+ assert.ok(files);
+ assert.strictEqual(files.length, 1);
+ assert.ok(files[0]!.id.endsWith('src/lib/auth/session.ts'));
+ });
+ });
+
+ test('bare-root wildcard (empty sourceDir) does not recurse into subdirectories', async () => {
+ // Guard the deliberate non-widening: an empty `sourceDir` maps `./dist/*.js`
+ // to a bare `*.ts`. Widening that to a project-root `**` would rake in
+ // `node_modules`/`dist`, so it must stay a single-segment match. Only the
+ // root-level file is found; the nested one is not.
+ await withTestDir(async (dir) => {
+ await mkdir(join(dir, 'nested'), {recursive: true});
+ await writeFile(join(dir, 'root.ts'), 'export const r = 0;');
+ await writeFile(join(dir, 'nested/deep.ts'), 'export const d = 1;');
+ await writeFile(
+ join(dir, 'package.json'),
+ JSON.stringify({
+ exports: {'./*.js': {types: './dist/*.d.ts', default: './dist/*.js'}},
+ }),
+ );
+
+ const {files} = await discoverFromExports({projectRoot: dir, sourceDir: ''});
+ assert.ok(files);
+ assert.strictEqual(files.length, 1);
+ assert.ok(files[0]!.id.endsWith('root.ts'));
+ assert.ok(!files.some((f) => f.id.endsWith('deep.ts')));
+ });
+ });
+
test('deduplicates across .js and .ts patterns', async () => {
await withTestDir(async (dir) => {
await mkdir(join(dir, 'src/lib'), {recursive: true});