Skip to content

Commit e11c2b1

Browse files
committed
Add plugin-contributed source-map rules
1 parent fab5871 commit e11c2b1

5 files changed

Lines changed: 103 additions & 27 deletions

File tree

packages/knip/src/ConfigurationChief.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
PluginsConfiguration,
88
RawConfiguration,
99
RawPluginConfiguration,
10+
SourceMap,
1011
WorkspaceConfiguration,
1112
} from './types/config.ts';
1213
import type { ConfigurationHint } from './types/issues.ts';
@@ -72,8 +73,7 @@ export type Workspace = {
7273
manifestPath: string;
7374
manifestStr: string;
7475
ignoreMembers: IgnorePatterns;
75-
srcDir?: string;
76-
outDir?: string;
76+
sourceMaps?: SourceMap[];
7777
};
7878

7979
/**

packages/knip/src/WorkspaceWorker.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
Plugin,
1313
RegisterCompiler,
1414
RegisterVisitorsOptions,
15+
SourceMap,
1516
WorkspaceConfiguration,
1617
} from './types/config.ts';
1718
import type { ConfigurationHint } from './types/issues.ts';
@@ -258,6 +259,23 @@ export class WorkspaceWorker {
258259
}
259260
}
260261

262+
public async resolveSourceMaps() {
263+
const options = {
264+
cwd: this.dir,
265+
rootCwd: this.options.cwd,
266+
manifest: this.manifest,
267+
rootManifest: this.rootManifest,
268+
dependencies: this.dependencies,
269+
};
270+
const pairs: SourceMap[] = [];
271+
for (const pluginName of this.enabledPlugins) {
272+
const plugin = Plugins[pluginName];
273+
if (!plugin.resolveSourceMap) continue;
274+
for (const pair of await plugin.resolveSourceMap(options)) pairs.push(pair);
275+
}
276+
return pairs;
277+
}
278+
261279
public registerVisitors(options: RegisterVisitorsOptions) {
262280
for (const pluginName of this.enabledPlugins) {
263281
if (options.registeredPlugins.has(pluginName)) continue;

packages/knip/src/graph/build.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import { getPackageNameFromModuleSpecifier, isStartsLikePackageName, sanitizeSpe
3636
import { perfObserver } from '../util/Performance.ts';
3737
import { getEntrySpecifiersFromManifest, getManifestImportDependencies } from '../util/package-json.ts';
3838
import { dirname, extname, isAbsolute, isInNodeModules, join, relative } from '../util/path.ts';
39-
import { augmentWorkspace, getToSourcePathsHandler } from '../util/to-source-path.ts';
39+
import { augmentWorkspace, getToSourcePathsHandler, toSourceMappedSpecifiers } from '../util/to-source-path.ts';
4040
import { WorkspaceWorker } from '../WorkspaceWorker.ts';
4141

4242
interface BuildOptions {
@@ -117,8 +117,6 @@ export async function build({
117117
const { isFile, compilerOptions, fileNames } = await loadTSConfig(tsConfigFilePath);
118118
const [definitionPaths, tscSourcePaths] = partition(fileNames, filePath => IS_DTS.test(filePath));
119119

120-
if (isFile) augmentWorkspace(workspace, dir, compilerOptions);
121-
122120
const worker = new WorkspaceWorker({
123121
name,
124122
dir,
@@ -154,6 +152,9 @@ export async function build({
154152
const config = chief.getConfigForWorkspace(name, extensions);
155153
worker.config = config;
156154

155+
const pluginSourceMaps = await worker.resolveSourceMaps();
156+
augmentWorkspace(workspace, dir, isFile ? compilerOptions : undefined, pluginSourceMaps);
157+
157158
const inputs = new Set<Input>();
158159

159160
if (definitionPaths.length > 0) {
@@ -180,9 +181,12 @@ export async function build({
180181
const packageName = getPackageNameFromModuleSpecifier(identifier);
181182
if (packageName && dependencies.has(packageName)) continue;
182183
}
183-
const exists = identifier.includes('*')
184-
? _syncGlob({ patterns: [identifier], cwd: dir }).length > 0
185-
: existsSync(join(dir, identifier));
184+
const abs = prependDir(dir, identifier);
185+
const mapped = toSourceMappedSpecifiers(workspace, abs, extensionGlobStr);
186+
const hasWildcard = identifier.includes('*');
187+
const exists = hasWildcard
188+
? _syncGlob({ patterns: [identifier, ...mapped], cwd: dir }).length > 0
189+
: existsSync(abs) || mapped.some(pattern => _syncGlob({ patterns: [pattern], cwd: dir }).length > 0);
186190
if (!exists) {
187191
collector.addConfigurationHint({ type: 'package-entry', filePath, identifier, workspaceName: name });
188192
}

packages/knip/src/types/config.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,18 @@ export type ResolveConfig<T = any> = (config: T, options: PluginOptions) => Prom
134134

135135
export type Resolve = (options: PluginOptions) => Promise<Input[]> | Input[];
136136

137+
export type SourceMap = { srcDir: string; outDir: string };
138+
139+
export interface ResolveSourceMapOptions {
140+
cwd: string;
141+
manifest: PackageJson;
142+
dependencies: Set<string>;
143+
rootCwd: string;
144+
rootManifest: PackageJson | undefined;
145+
}
146+
147+
export type ResolveSourceMap = (options: ResolveSourceMapOptions) => Promise<SourceMap[]> | SourceMap[];
148+
137149
export type HandleInput = (input: Input) => string | undefined;
138150

139151
export type RegisterCompilerInput = {
@@ -191,6 +203,13 @@ export interface Plugin {
191203
isLoadConfig?: IsLoadConfig;
192204
resolveConfig?: ResolveConfig;
193205
resolve?: Resolve;
206+
/**
207+
* Contributes src↔out mappings so knip can rewire files in the emitted output tree back to
208+
* their source counterparts (e.g. `package.json#exports` pointing into `dist/`). Runs per
209+
* workspace, before any other plugin hook, so it cannot depend on state populated by
210+
* `resolveConfig`/`resolveFromAST`/`resolve`.
211+
*/
212+
resolveSourceMap?: ResolveSourceMap;
194213
resolveFromAST?: ResolveFromAST;
195214
isFilterTransitiveDependencies?: boolean;
196215
registerCompilers?: RegisterCompilers;
Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { SourceMap } from '../types/config.ts';
12
import type { CompilerOptions } from '../types/project.ts';
23
import type { ConfigurationChief, Workspace } from '../ConfigurationChief.ts';
34
import { DEFAULT_EXTENSIONS } from '../constants.ts';
@@ -12,11 +13,34 @@ const matchExt = /(\.d)?\.(m|c)?(j|t)s$/;
1213

1314
const sourceExtensions = [...DEFAULT_EXTENSIONS];
1415

15-
export const augmentWorkspace = (workspace: Workspace, dir: string, compilerOptions: CompilerOptions) => {
16+
const tsconfigSourceMap = (dir: string, compilerOptions: CompilerOptions): SourceMap => {
1617
const srcDir = join(dir, 'src');
1718
const outDirHasSrc = compilerOptions.outDir && isDirectory(compilerOptions.outDir, 'src');
18-
workspace.srcDir = compilerOptions.rootDir ?? (outDirHasSrc ? dir : isDirectory(srcDir) ? srcDir : dir);
19-
workspace.outDir = compilerOptions.outDir || workspace.srcDir;
19+
const resolvedSrc = compilerOptions.rootDir ?? (outDirHasSrc ? dir : isDirectory(srcDir) ? srcDir : dir);
20+
return { srcDir: resolvedSrc, outDir: compilerOptions.outDir || resolvedSrc };
21+
};
22+
23+
export const augmentWorkspace = (
24+
workspace: Workspace,
25+
dir: string,
26+
compilerOptions: CompilerOptions | undefined,
27+
pluginSourceMaps: SourceMap[] = []
28+
) => {
29+
const all = compilerOptions ? [...pluginSourceMaps, tsconfigSourceMap(dir, compilerOptions)] : pluginSourceMaps;
30+
if (all.length === 0) return;
31+
workspace.sourceMaps = all.sort((a, b) => b.outDir.length - a.outDir.length);
32+
};
33+
34+
const isUnderOutDir = (absPath: string, outDir: string) => absPath === outDir || absPath.startsWith(`${outDir}/`);
35+
36+
const isUnderSrcDir = (absPath: string, srcDir: string) => absPath === srcDir || absPath.startsWith(`${srcDir}/`);
37+
38+
const rewritePattern = (sourceMaps: SourceMap[], absSpecifier: string, extensions: string) => {
39+
for (const { srcDir, outDir } of sourceMaps) {
40+
if (!isUnderSrcDir(absSpecifier, srcDir) && isUnderOutDir(absSpecifier, outDir)) {
41+
return srcDir + absSpecifier.slice(outDir.length).replace(matchExt, extensions);
42+
}
43+
}
2044
};
2145

2246
export const getModuleSourcePathHandler = (chief: ConfigurationChief) => {
@@ -26,16 +50,16 @@ export const getModuleSourcePathHandler = (chief: ConfigurationChief) => {
2650
if (!isInternal(filePath) || hasTSExt.test(filePath)) return;
2751
if (toSourceMapCache.has(filePath)) return toSourceMapCache.get(filePath);
2852
const workspace = chief.findWorkspaceByFilePath(filePath);
29-
if (workspace?.srcDir && workspace.outDir) {
30-
if (filePath.startsWith(workspace.outDir) || workspace.srcDir === workspace.outDir) {
31-
const basePath = filePath.replace(workspace.outDir, workspace.srcDir).replace(matchExt, '');
32-
const srcFilePath = findFileWithExtensions(basePath, sourceExtensions);
33-
if (srcFilePath) {
34-
toSourceMapCache.set(filePath, srcFilePath);
35-
if (srcFilePath !== filePath) {
36-
debugLog('*', `Source mapping ${toRelative(filePath, chief.cwd)}${toRelative(srcFilePath, chief.cwd)}`);
37-
return srcFilePath;
38-
}
53+
if (!workspace?.sourceMaps) return;
54+
for (const { srcDir, outDir } of workspace.sourceMaps) {
55+
if (!(isUnderOutDir(filePath, outDir) || srcDir === outDir)) continue;
56+
const basePath = (srcDir + filePath.slice(outDir.length)).replace(matchExt, '');
57+
const srcFilePath = findFileWithExtensions(basePath, sourceExtensions);
58+
if (srcFilePath) {
59+
toSourceMapCache.set(filePath, srcFilePath);
60+
if (srcFilePath !== filePath) {
61+
debugLog('*', `Source mapping ${toRelative(filePath, chief.cwd)}${toRelative(srcFilePath, chief.cwd)}`);
62+
return srcFilePath;
3963
}
4064
}
4165
}
@@ -49,12 +73,8 @@ export const getToSourcePathsHandler = (chief: ConfigurationChief) => {
4973
for (const specifier of specifiers) {
5074
const absSpecifier = isAbsolute(specifier) ? specifier : prependDirToPattern(dir, specifier);
5175
const ws = chief.findWorkspaceByFilePath(absSpecifier);
52-
if (ws?.srcDir && ws.outDir && !absSpecifier.startsWith(ws.srcDir) && absSpecifier.startsWith(ws.outDir)) {
53-
const pattern = absSpecifier.replace(ws.outDir, ws.srcDir).replace(matchExt, extensions);
54-
patterns.add(pattern);
55-
} else {
56-
patterns.add(absSpecifier);
57-
}
76+
const mapped = ws?.sourceMaps && rewritePattern(ws.sourceMaps, absSpecifier, extensions);
77+
patterns.add(mapped ?? absSpecifier);
5878
}
5979

6080
const filePaths = await _glob({ patterns: Array.from(patterns), cwd: dir, label });
@@ -65,4 +85,19 @@ export const getToSourcePathsHandler = (chief: ConfigurationChief) => {
6585
};
6686
};
6787

88+
export const toSourceMappedSpecifiers = (
89+
ws: Workspace | undefined,
90+
absSpecifier: string,
91+
extensions = defaultExtensions
92+
) => {
93+
const out: string[] = [];
94+
if (!ws?.sourceMaps) return out;
95+
for (const { srcDir, outDir } of ws.sourceMaps) {
96+
if (!isUnderSrcDir(absSpecifier, srcDir) && isUnderOutDir(absSpecifier, outDir)) {
97+
out.push(srcDir + absSpecifier.slice(outDir.length).replace(matchExt, extensions));
98+
}
99+
}
100+
return out;
101+
};
102+
68103
export type ToSourceFilePath = ReturnType<typeof getModuleSourcePathHandler>;

0 commit comments

Comments
 (0)