Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 97 additions & 56 deletions src/core/entrypoints/detectEntryPoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import fs from "fs";
import path from "path";
import { normalizeFilePath } from "../fileSystem/normalizePath";

interface EntryPointInfo {
export interface EntryPointInfo {
all: Set<string>;
fromPackageJson: Set<string>;
fromConventions: Set<string>;
Expand All @@ -22,97 +22,137 @@ export function detectEntryPoints(projectRoot: string, projectFiles: string[]):

const fromPackageJson = resolveFromPackageJson(projectRoot, normalizedFiles);
const fromConventions = resolveFromConventions(projectRoot, normalizedFiles);

const all = new Set<string>([...fromPackageJson, ...fromConventions]);

return { all, fromPackageJson, fromConventions };
}

/**
* Extract entry points from package.json fields:
* main, module, types/typings, bin
*/
function resolveFromPackageJson(projectRoot: string, projectFiles: Set<string>): Set<string> {
const result = new Set<string>();
const pkgPath = path.join(projectRoot, "package.json");
const entryPoints = new Set<string>();

if (!fs.existsSync(pkgPath)) return result;
const pkgPath = path.join(projectRoot, "package.json");
if (!fs.existsSync(pkgPath)) return entryPoints;

let pkg: any;

try {
pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
} catch {
return result;
return entryPoints;
}

const candidates: string[] = [];
const candidatePaths: string[] = [];

if (typeof pkg.main === "string") candidates.push(pkg.main);
if (typeof pkg.module === "string") candidates.push(pkg.module);
if (typeof pkg.types === "string") candidates.push(pkg.types);
if (typeof pkg.typings === "string") candidates.push(pkg.typings);
// package.json fields
if (typeof pkg.main === "string") candidatePaths.push(pkg.main);
if (typeof pkg.module === "string") candidatePaths.push(pkg.module);
if (typeof pkg.types === "string") candidatePaths.push(pkg.types);
if (typeof pkg.typings === "string") candidatePaths.push(pkg.typings);

// package.json bin - string or object
if (typeof pkg.bin === "string") {
candidates.push(pkg.bin);
candidatePaths.push(pkg.bin);
} else if (pkg.bin && typeof pkg.bin === "object") {
for (const value of Object.values<string>(pkg.bin)) {
if (typeof value === "string") candidates.push(value);
for (const value of Object.values(pkg.bin)) {
Comment thread
ElijahKotyluk marked this conversation as resolved.
if (typeof value === "string") candidatePaths.push(value);
}
}

for (const rel of candidates) {
const resolved = resolveCandidateToFile(projectRoot, rel, projectFiles);
if (resolved) result.add(resolved);
// exports: string | object | nested conditions | arrays
candidatePaths.push(...collectExportsTargets(pkg.exports));

for (const rawCandidate of candidatePaths) {
const resolved = resolveCandidateToProjectFile(projectRoot, rawCandidate, projectFiles);

if (resolved) entryPoints.add(resolved);
}

return result;
return entryPoints;
}

/**
* Maps a package.json path to a source file when possible.
* Attempts:
* • exact file match
* • dist/*.js → src/*.ts mapping
*/
function resolveCandidateToFile(
function collectExportsTargets(exportsField: unknown): string[] {
const targets: string[] = [];

function walk(value: unknown) {
if (value == null) return;

if (typeof value === "string") {
targets.push(value);

return;
}

if (Array.isArray(value)) {
for (const item of value) walk(item);

return;
}

if (typeof value === "object") {
for (const propVal of Object.values(value as Record<string, unknown>)) {
Comment thread
ElijahKotyluk marked this conversation as resolved.
walk(propVal);
}
}
}

walk(exportsField);

return targets;
}

function resolveCandidateToProjectFile(
projectRoot: string,
relativePath: string,
candidatePath: string,
projectFiles: Set<string>,
): string | null {
const abs = normalizeFilePath(path.resolve(projectRoot, relativePath));
const absoluteCandidate = normalizeFilePath(path.resolve(projectRoot, candidatePath));

if (projectFiles.has(abs)) return abs;
if (projectFiles.has(absoluteCandidate)) return absoluteCandidate;

// Basic .js/.jsx → .ts/.tsx mapping
const jsExts = [".js", ".jsx"];
const tsExts = [".ts", ".tsx"];
const resolvedByExt = trySwapExtensions(absoluteCandidate, projectFiles);
if (resolvedByExt) return resolvedByExt;

for (const jsExt of jsExts) {
if (abs.endsWith(jsExt)) {
const base = abs.slice(0, -jsExt.length);
for (const tsExt of tsExts) {
const tsCandidate = `${base}${tsExt}`;
if (projectFiles.has(tsCandidate)) return tsCandidate;
}
const resolvedByDistSrc = tryDistToSrcMapping(absoluteCandidate, projectFiles);
if (resolvedByDistSrc) return resolvedByDistSrc;

return null;
}

function trySwapExtensions(absoluteCandidate: string, projectFiles: Set<string>): string | null {
const fromExtensions = [".js", ".jsx", ".mjs", ".cjs"];
const toExtensions = [".ts", ".tsx"];

for (const fromExt of fromExtensions) {
if (!absoluteCandidate.endsWith(fromExt)) continue;

const base = absoluteCandidate.slice(0, -fromExt.length);

for (const toExt of toExtensions) {
const tsCandidate = `${base}${toExt}`;

if (projectFiles.has(tsCandidate)) return tsCandidate;
}
}

// dist → src heuristic
if (abs.includes("/dist/")) {
const srcCandidate = abs.replace("/dist/", "/src/");
if (projectFiles.has(srcCandidate)) return srcCandidate;
}
return null;
}

function tryDistToSrcMapping(absoluteCandidate: string, projectFiles: Set<string>): string | null {
if (!absoluteCandidate.includes("/dist/")) return null;

const srcCandidate = absoluteCandidate.replace("/dist/", "/src/");
if (projectFiles.has(srcCandidate)) return srcCandidate;

const resolvedByExt = trySwapExtensions(srcCandidate, projectFiles);
if (resolvedByExt) return resolvedByExt;

return null;
}

/**
* Detect common entry point filenames when package.json does not specify them.
*/
function resolveFromConventions(projectRoot: string, projectFiles: Set<string>): Set<string> {
const result = new Set<string>();
const entryPoints = new Set<string>();

const candidates = [
const conventionalCandidates = [
path.join(projectRoot, "index.ts"),
path.join(projectRoot, "index.tsx"),
path.join(projectRoot, "src", "index.ts"),
Expand All @@ -121,10 +161,11 @@ function resolveFromConventions(projectRoot: string, projectFiles: Set<string>):
path.join(projectRoot, "src", "main.tsx"),
];

for (const abs of candidates) {
const normalized = normalizeFilePath(abs);
if (projectFiles.has(normalized)) result.add(normalized);
for (const candidate of conventionalCandidates) {
const normalized = normalizeFilePath(candidate);

if (projectFiles.has(normalized)) entryPoints.add(normalized);
}

return result;
return entryPoints;
}
11 changes: 6 additions & 5 deletions src/core/fileSystem/normalizePath.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { resolve } from "path";

// Memoize normalized paths, since this is called frequently
const normalizeCache = new Map<string, string>();
const normalizedFileCache = new Map<string, string>();

/**
* Normalize a file path to a canonical absolute form.
Expand All @@ -11,12 +11,13 @@ const normalizeCache = new Map<string, string>();
* - Fully memoized
*/
export function normalizeFilePath(path: string): string {
const cached = normalizeCache.get(path);
const cached = normalizedFileCache.get(path);
if (cached !== undefined) return cached;

const abs = resolve(path).replace(/\\/g, "/");
const normalized = process.platform === "win32" ? abs.toLowerCase() : abs;
const absolutePath = resolve(path).replace(/\\/g, "/");
const normalized = process.platform === "win32" ? absolutePath.toLowerCase() : absolutePath;

normalizedFileCache.set(path, normalized);

normalizeCache.set(path, normalized);
return normalized;
}
Loading