Skip to content

Commit 07c0539

Browse files
committed
Find mdx plugins in next config (resolve #1367)
1 parent b107af4 commit 07c0539

File tree

5 files changed

+87
-7
lines changed

5 files changed

+87
-7
lines changed
Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
1+
const createMDX = require('@next/mdx');
12
const { PHASE_DEVELOPMENT_SERVER } = require('next/constants');
23

34
const withTM = require('next-transpile-modules')([]);
45

6+
const withMDX = createMDX({
7+
options: {
8+
remarkPlugins: ['remark-frontmatter', ['remark-mdx-frontmatter', { name: 'metadata' }]],
9+
rehypePlugins: [['rehype-starry-night']],
10+
recmaPlugins: ['recma-export-filepath'],
11+
},
12+
});
13+
514
module.exports = phase => {
615
const config = withTM({});
7-
return {
16+
return withMDX({
817
pageExtensions: ['ts', 'tsx'],
918
...config,
10-
};
19+
});
1120
};

packages/knip/src/plugins/next/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { IsPluginEnabled, Plugin, ResolveFromAST } from '../../types/config.js';
2-
import { toProductionEntry } from '../../util/input.js';
2+
import { toDependency, toProductionEntry } from '../../util/input.js';
33
import { hasDependency } from '../../util/plugin.js';
4-
import { getPageExtensions } from './resolveFromAST.js';
4+
import { getMdxPlugins, getPageExtensions } from './resolveFromAST.js';
55

66
// https://nextjs.org/docs/getting-started/project-structure
77

@@ -43,7 +43,8 @@ const resolveFromAST: ResolveFromAST = sourceFile => {
4343
const pageExtensions = getPageExtensions(sourceFile);
4444
const extensions = pageExtensions.length > 0 ? pageExtensions : defaultPageExtensions;
4545
const patterns = getEntryFilePatterns(extensions);
46-
return patterns.map(id => toProductionEntry(id));
46+
const mdxPlugins = getMdxPlugins(sourceFile);
47+
return [...patterns.map(id => toProductionEntry(id)), ...Array.from(mdxPlugins).map(id => toDependency(id))];
4748
};
4849

4950
const plugin: Plugin = {

packages/knip/src/plugins/next/resolveFromAST.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import ts from 'typescript';
2-
import { getPropertyValues } from '../../typescript/ast-helpers.js';
2+
import { getDefaultImportName, getImportMap, getPropertyValues, stripQuotes } from '../../typescript/ast-helpers.js';
33

44
export const getPageExtensions = (sourceFile: ts.SourceFile) => {
55
const pageExtensions: Set<string> = new Set();
@@ -17,3 +17,50 @@ export const getPageExtensions = (sourceFile: ts.SourceFile) => {
1717

1818
return Array.from(pageExtensions);
1919
};
20+
21+
const isNamedProp = (prop: ts.ObjectLiteralElementLike, name: string) =>
22+
ts.isPropertyAssignment(prop) && prop.name.getText() === name;
23+
24+
export const getMdxPlugins = (sourceFile: ts.SourceFile) => {
25+
const plugins = new Set<string>();
26+
const importMap = getImportMap(sourceFile);
27+
const mdxImportName = getDefaultImportName(importMap, '@next/mdx');
28+
29+
if (!mdxImportName) return plugins;
30+
31+
function visit(node: ts.Node): boolean {
32+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === mdxImportName) {
33+
if (node.arguments.length > 0 && ts.isObjectLiteralExpression(node.arguments[0])) {
34+
const options = node.arguments[0]?.properties.find(prop => isNamedProp(prop, 'options'));
35+
if (options && ts.isPropertyAssignment(options)) {
36+
if (ts.isObjectLiteralExpression(options.initializer)) {
37+
for (const pluginType of ['remarkPlugins', 'rehypePlugins', 'recmaPlugins']) {
38+
const props = options.initializer.properties.find(prop => isNamedProp(prop, pluginType));
39+
if (props && ts.isPropertyAssignment(props)) {
40+
if (ts.isArrayLiteralExpression(props.initializer)) {
41+
for (const element of props.initializer.elements) {
42+
if (ts.isStringLiteral(element)) {
43+
plugins.add(stripQuotes(element.text));
44+
} else if (ts.isArrayLiteralExpression(element) && element.elements.length > 0) {
45+
const firstElement = element.elements[0];
46+
if (ts.isStringLiteral(firstElement)) {
47+
plugins.add(stripQuotes(firstElement.text));
48+
}
49+
}
50+
}
51+
}
52+
}
53+
}
54+
}
55+
}
56+
}
57+
return true;
58+
}
59+
60+
return ts.forEachChild(node, visit) ?? false;
61+
}
62+
63+
visit(sourceFile);
64+
65+
return plugins;
66+
};

packages/knip/src/typescript/ast-helpers.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@ export const isModuleExportsAccess = (node: ts.PropertyAccessExpression) =>
313313
export const getImportMap = (sourceFile: ts.SourceFile) => {
314314
const importMap = new Map<string, string>();
315315
for (const statement of sourceFile.statements) {
316+
// ESM
316317
if (ts.isImportDeclaration(statement)) {
317318
const importClause = statement.importClause;
318319
const importPath = stripQuotes(statement.moduleSpecifier.getText());
@@ -321,7 +322,24 @@ export const getImportMap = (sourceFile: ts.SourceFile) => {
321322
for (const element of importClause.namedBindings.elements) importMap.set(element.name.text, importPath);
322323
}
323324
}
325+
326+
// CommonJS
327+
if (ts.isVariableStatement(statement)) {
328+
for (const declaration of statement.declarationList.declarations) {
329+
if (
330+
declaration.initializer &&
331+
isRequireCall(declaration.initializer) &&
332+
ts.isIdentifier(declaration.name) &&
333+
ts.isStringLiteral(declaration.initializer.arguments[0])
334+
) {
335+
const importName = declaration.name.text;
336+
const importPath = stripQuotes(declaration.initializer.arguments[0].text);
337+
importMap.set(importName, importPath);
338+
}
339+
}
340+
}
324341
}
342+
325343
return importMap;
326344
};
327345

packages/knip/test/plugins/next.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ test('Find dependencies with the Next.js plugin', async () => {
1616
assert(issues.files.has(join(cwd, 'pages/unused.jsx')));
1717

1818
assert(issues.unlisted['next.config.js']['next-transpile-modules']);
19+
assert(issues.unlisted['next.config.js']['@next/mdx']);
20+
assert(issues.unlisted['next.config.js']['remark-frontmatter']);
21+
assert(issues.unlisted['next.config.js']['remark-mdx-frontmatter']);
22+
assert(issues.unlisted['next.config.js']['rehype-starry-night']);
23+
assert(issues.unlisted['next.config.js']['recma-export-filepath']);
1924
assert(issues.unlisted['pages/[[...route]].tsx']['react']);
2025
assert(issues.unlisted['pages/[[...route]].tsx']['react-helmet']);
2126
assert(issues.unlisted['pages/home.tsx']['react']);
@@ -26,7 +31,7 @@ test('Find dependencies with the Next.js plugin', async () => {
2631
...baseCounters,
2732
files: 2,
2833
devDependencies: 0,
29-
unlisted: 6,
34+
unlisted: 11,
3035
processed: 13,
3136
total: 13,
3237
});

0 commit comments

Comments
 (0)