/**
* Source file discovery — exports-first with glob fallback.
*
* Used by `analyzeFromFiles` to locate source files from a project root.
* Build tool integrations using `analyze` should construct `SourceFileInfo[]`
* from their own module graph instead.
*
* @see `exports.ts` for `discoverFromExports` (package.json exports → source files)
* @see `files.ts` for `globFiles` (glob-based discovery)
*
* @module
*/
import type {SourceFileInfo} from './source.js';
import {type ModuleSourceOptions, getSourceRoot} from './source-config.js';
import type {Diagnostic} from './diagnostics.js';
import type {AnalysisLog} from './log.js';
import {globFiles, deriveIncludePatterns} from './files.js';
import {discoverFromExports} from './exports.js';
/**
* Discovery strategy for source files.
*
* - `'auto'` (default) — try package.json `exports` first, fall back to glob
* patterns when `exports` is missing or resolves to nothing
* - `'exports'` — package.json `exports` only, **throw** if `exports` is missing
* or resolves to no source files (strict mode for libraries that should
* always declare their public surface via `exports`)
* - `'glob'` — skip `exports` entirely, use glob patterns
*
* Providing `include` patterns implies `'glob'` semantics regardless of mode
* — when `discovery: 'auto'` and `include` is set, the auto fallback chain
* collapses to glob immediately. Combining `discovery: 'exports'` with
* `include` is a configuration error (the modes are contradictory) and
* throws at discovery time.
*/
export type Discovery = 'auto' | 'exports' | 'glob';
/** Options for `discoverSourceFiles`. */
export interface DiscoverSourceFilesOptions {
/**
* Source options used to resolve the source directory for exports-based discovery.
*
* `sourceOptions.projectRoot` is the resolution base for `include` globs and the
* `sourceOptions.exclude` glob patterns. Build via `createSourceOptions` (which
* normalizes) or pass a normalized return from `normalizeSourceOptions`.
*
* `sourceOptions.exclude` is the single source of truth for exclusion globs —
* applied at both this discovery stage and analysis time (via `isSource()`).
*/
sourceOptions: ModuleSourceOptions;
/**
* Glob patterns to include (relative to `projectRoot`).
*
* Filter for glob-based discovery. When `discovery` is `'auto'` (default),
* providing `include` collapses the chain to glob immediately. Combining
* `include` with `discovery: 'exports'` throws.
*
* When omitted, the glob fallback derives an include pattern from
* `sourceOptions.sourcePaths` via `deriveIncludePatterns`, so custom
* `sourcePaths` (e.g., `['packages/foo']`) discover files instead of
* silently defaulting to `src/lib`.
*/
include?: Array<string>;
/**
* Discovery strategy.
*
* @default 'auto'
* @see {@link Discovery} for semantics of each variant
*/
discovery?: Discovery;
/**
* Dist directory name relative to `projectRoot`, used for exports-based discovery.
*
* Maps dist paths from package.json exports back to source paths.
*
* @default 'dist'
*/
distDir?: string;
/** Optional logger for status messages. */
log?: AnalysisLog;
}
/** Result of `discoverSourceFiles`. */
export interface DiscoverSourceFilesResult {
/** Discovered source files with content already loaded. */
files: Array<SourceFileInfo>;
/** Diagnostics collected during discovery (e.g., malformed package.json exports). */
diagnostics: Array<Diagnostic>;
}
/**
* Discover source files from a project root.
*
* Used internally by `analyzeFromFiles` for the discovery step. Standalone
* consumers can call it directly when they want the discovered file list
* without running full analysis.
*
* Strategy is selected by `discovery`:
* - `'auto'` (default) — try `exports` first, fall back to glob.
* - `'exports'` — `exports` only; **throws** if `exports` is missing or
* resolves to no source files. Combining with `include` is a configuration
* error and also throws.
* - `'glob'` — glob only; `include` parameterizes the search.
*
* Exclusion globs come from `sourceOptions.exclude` (the single source of
* truth, also applied at analysis time by `isSource()`).
*
* @param options - discovery configuration
* @returns discovered files (content loaded) and any diagnostics from the exports step
* @throws Error in strict `'exports'` mode when `exports` is missing or
* resolves to no source files, or when `include` is combined with
* `discovery: 'exports'`.
*
* @example
* ```ts
* const sourceOptions = createSourceOptions(process.cwd());
* const {files, diagnostics} = await discoverSourceFiles({sourceOptions});
* ```
*/
export const discoverSourceFiles = async (
options: DiscoverSourceFilesOptions,
): Promise<DiscoverSourceFilesResult> => {
const {sourceOptions, include, discovery = 'auto', distDir, log} = options;
const {projectRoot, exclude} = sourceOptions;
// Reject contradictory configurations early. `include` parameterizes glob,
// so combining it with strict `'exports'` is a user error rather than a
// silent override.
if (discovery === 'exports' && include) {
throw new Error(
"discovery: 'exports' is incompatible with `include` — `include` is a glob filter. " +
"Use discovery: 'glob' (with include) or remove include for strict exports mode.",
);
}
// `include` collapses 'auto' to glob immediately — exports-discovery has
// no concept of include patterns, so honoring `include` under 'auto' would
// silently drop the user's filter on packages that have an `exports` field.
const effectiveStrategy: Discovery = discovery === 'auto' && include ? 'glob' : discovery;
let files: Array<SourceFileInfo> | null = null;
let diagnostics: Array<Diagnostic> = [];
if (effectiveStrategy === 'exports' || effectiveStrategy === 'auto') {
const sourceDir = getSourceRoot(sourceOptions);
// Exports discovery is single-`sourceDir`-only: it maps every dist path
// through one source-dir prefix. When `sourcePaths.length > 1` and the
// auto-derived (or explicit) `sourceRoot` is `''`, every dist entry
// would resolve to project-root-relative paths instead of the user's
// actual subdirs — exports discovery cannot represent the layout.
// Short-circuit instead of letting it silently produce zero files
// (under 'auto' that falls through harmlessly to glob, but under
// 'exports' it would throw the misleading generic "resolved to no
// source files" error).
if (sourceOptions.sourcePaths.length > 1 && sourceDir === '') {
if (effectiveStrategy === 'exports') {
throw new Error(
"discovery: 'exports' failed — source paths share no common prefix " +
`(sourcePaths=${JSON.stringify(sourceOptions.sourcePaths)}, sourceRoot=''), ` +
'so exports discovery cannot map dist paths to source files. ' +
"Use discovery: 'auto' (default) or 'glob' for this layout, or " +
'restructure to a single source root.',
);
}
log?.info(
'Source paths share no common prefix — exports discovery cannot represent this layout; falling back to glob patterns',
);
} else {
const exportsResult = await discoverFromExports({
projectRoot,
exclude,
sourceDir,
distDir,
});
const {files: exportsFiles, diagnostics: exportsDiagnostics} = exportsResult;
diagnostics = exportsDiagnostics;
if (exportsFiles && exportsFiles.length > 0) {
files = exportsFiles;
log?.info(`Discovered ${files.length} source files from package.json exports`);
} else if (effectiveStrategy === 'exports') {
// Strict mode: no fallback. Throw with a message that names the
// failure mode so users can fix the package.json or relax the strictness.
const reason =
exportsFiles === null
? 'no `exports` field found in package.json'
: '`exports` field present but resolved to no source files';
throw new Error(
`discovery: 'exports' failed — ${reason}. ` +
`Use discovery: 'auto' (default) to fall back to glob, or fix the package.json exports mapping.`,
);
} else if (exportsFiles === null) {
log?.info('No package.json exports found, falling back to glob patterns');
} else {
log?.warn(
'Package.json exports found but resolved no source files — falling back to glob patterns',
);
}
}
}
// Fall back to glob-based discovery (auto fallback or explicit glob mode).
// Derive include from `sourceOptions.sourcePaths` when none is supplied so
// custom `sourcePaths` (e.g. `['packages/foo']`) discover files instead of
// silently defaulting to `src/lib`. Mirrors the CLI's prior local
// derivation — single source of truth lives here now.
if (!files) {
files = await globFiles({
projectRoot,
include: include ?? deriveIncludePatterns(sourceOptions.sourcePaths),
exclude,
});
log?.info(`Discovered ${files.length} source files via glob`);
}
return {files, diagnostics};
};
{
"path": "discovery.ts",
"declarations": [
{
"name": "Discovery",
"kind": "type",
"docComment": "Discovery strategy for source files.\n\n- `'auto'` (default) — try package.json `exports` first, fall back to glob\n patterns when `exports` is missing or resolves to nothing\n- `'exports'` — package.json `exports` only, **throw** if `exports` is missing\n or resolves to no source files (strict mode for libraries that should\n always declare their public surface via `exports`)\n- `'glob'` — skip `exports` entirely, use glob patterns\n\nProviding `include` patterns implies `'glob'` semantics regardless of mode\n— when `discovery: 'auto'` and `include` is set, the auto fallback chain\ncollapses to glob immediately. Combining `discovery: 'exports'` with\n`include` is a configuration error (the modes are contradictory) and\nthrows at discovery time.",
"typeSignature": "Discovery",
"sourceLine": 37,
"alsoExportedFrom": [
"index.ts"
]
},
{
"name": "DiscoverSourceFilesOptions",
"kind": "interface",
"docComment": "Options for `discoverSourceFiles`.",
"typeSignature": "DiscoverSourceFilesOptions",
"sourceLine": 40,
"alsoExportedFrom": [
"index.ts"
],
"members": [
{
"name": "sourceOptions",
"kind": "variable",
"docComment": "Source options used to resolve the source directory for exports-based discovery.\n\n`sourceOptions.projectRoot` is the resolution base for `include` globs and the\n`sourceOptions.exclude` glob patterns. Build via `createSourceOptions` (which\nnormalizes) or pass a normalized return from `normalizeSourceOptions`.\n\n`sourceOptions.exclude` is the single source of truth for exclusion globs —\napplied at both this discovery stage and analysis time (via `isSource()`).",
"typeSignature": "ModuleSourceOptions"
},
{
"name": "include",
"kind": "variable",
"docComment": "Glob patterns to include (relative to `projectRoot`).\n\nFilter for glob-based discovery. When `discovery` is `'auto'` (default),\nproviding `include` collapses the chain to glob immediately. Combining\n`include` with `discovery: 'exports'` throws.\n\nWhen omitted, the glob fallback derives an include pattern from\n`sourceOptions.sourcePaths` via `deriveIncludePatterns`, so custom\n`sourcePaths` (e.g., `['packages/foo']`) discover files instead of\nsilently defaulting to `src/lib`.",
"typeSignature": "Array<string>",
"optional": true
},
{
"name": "discovery",
"kind": "variable",
"docComment": "Discovery strategy.",
"typeSignature": "Discovery",
"seeAlso": [
"{@link Discovery} for semantics of each variant"
],
"optional": true,
"defaultValue": "'auto'"
},
{
"name": "distDir",
"kind": "variable",
"docComment": "Dist directory name relative to `projectRoot`, used for exports-based discovery.\n\nMaps dist paths from package.json exports back to source paths.",
"typeSignature": "string",
"optional": true,
"defaultValue": "'dist'"
},
{
"name": "log",
"kind": "variable",
"docComment": "Optional logger for status messages.",
"typeSignature": "AnalysisLog",
"optional": true
}
]
},
{
"name": "DiscoverSourceFilesResult",
"kind": "interface",
"docComment": "Result of `discoverSourceFiles`.",
"typeSignature": "DiscoverSourceFilesResult",
"sourceLine": 89,
"alsoExportedFrom": [
"index.ts"
],
"members": [
{
"name": "files",
"kind": "variable",
"docComment": "Discovered source files with content already loaded.",
"typeSignature": "Array<SourceFileInfo>"
},
{
"name": "diagnostics",
"kind": "variable",
"docComment": "Diagnostics collected during discovery (e.g., malformed package.json exports).",
"typeSignature": "Array<Diagnostic>"
}
]
},
{
"name": "discoverSourceFiles",
"kind": "function",
"docComment": "Discover source files from a project root.\n\nUsed internally by `analyzeFromFiles` for the discovery step. Standalone\nconsumers can call it directly when they want the discovered file list\nwithout running full analysis.\n\nStrategy is selected by `discovery`:\n- `'auto'` (default) — try `exports` first, fall back to glob.\n- `'exports'` — `exports` only; **throws** if `exports` is missing or\n resolves to no source files. Combining with `include` is a configuration\n error and also throws.\n- `'glob'` — glob only; `include` parameterizes the search.\n\nExclusion globs come from `sourceOptions.exclude` (the single source of\ntruth, also applied at analysis time by `isSource()`).",
"typeSignature": "(options: DiscoverSourceFilesOptions): Promise<DiscoverSourceFilesResult>",
"sourceLine": 125,
"examples": [
"```ts\nconst sourceOptions = createSourceOptions(process.cwd());\nconst {files, diagnostics} = await discoverSourceFiles({sourceOptions});\n```"
],
"throws": [
{
"type": "Error",
"description": "in strict `'exports'` mode when `exports` is missing or"
}
],
"alsoExportedFrom": [
"index.ts"
],
"parameters": [
{
"name": "options",
"type": "DiscoverSourceFilesOptions",
"description": "discovery configuration"
}
],
"returnType": "Promise<DiscoverSourceFilesResult>",
"returnDescription": "discovered files (content loaded) and any diagnostics from the exports step"
}
],
"moduleComment": "Source file discovery — exports-first with glob fallback.\n\nUsed by `analyzeFromFiles` to locate source files from a project root.\nBuild tool integrations using `analyze` should construct `SourceFileInfo[]`\nfrom their own module graph instead.\n\n@see `exports.ts` for `discoverFromExports` (package.json exports → source files)\n@see `files.ts` for `globFiles` (glob-based discovery)",
"dependencies": [
"diagnostics.ts",
"exports.ts",
"files.ts",
"log.ts",
"source-config.ts",
"source.ts"
],
"dependents": [
"analyze.ts",
"cli.ts",
"index.ts",
"vite.ts"
]
}