Skip to content

Commit 9931b7c

Browse files
committed
Add support for resolveFromAST in plugins (#1005)
# Conflicts: # packages/knip/src/graph/build.ts # packages/knip/src/plugins/ava/index.ts # packages/knip/src/plugins/jest/index.ts # packages/knip/src/plugins/storybook/index.ts # packages/knip/src/util/plugin.ts
1 parent 499b721 commit 9931b7c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+861
-166
lines changed

knip.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"project": ["src/**/*.ts!", "!src/util/empty.ts", "!**/_template/**"]
1010
},
1111
"packages/docs": {
12-
"entry": ["{remark,scripts}/*.ts", "src/components/{Head,Footer}.astro!"]
12+
"entry": ["{remark,scripts}/*.ts"]
1313
}
1414
}
1515
}

packages/docs/src/content/docs/guides/writing-a-plugin.md

+125-15
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ find otherwise. Plugins always do at least one of the following:
88
1. Define entry file patterns
99
2. Find dependencies in configuration files
1010

11-
Knip v5.1.0 introduces a new plugin API, which makes them a breeze to write and
11+
Knip v5.1.0 introduced a new plugin API, which makes them a breeze to write and
1212
maintain.
1313

1414
:::tip[The new plugin API]
@@ -18,7 +18,7 @@ Easy things should be easy, and complex things possible.
1818
:::
1919

2020
This tutorial walks through example plugins so you'll be ready to write your
21-
own!
21+
own! The following examples demonstrate the elements a plugin can implement.
2222

2323
## Example 1: entry
2424

@@ -193,12 +193,119 @@ contains one or more options that represent [entry points][2].
193193

194194
:::
195195

196+
## Example 4: Use the AST directly
197+
198+
For the `resolveEntryPaths` and `resolveFromConfig` functions, Knip loads the
199+
configuration file and passes the default-exported object to this plugin
200+
function. However, that object might then not contain the information we need.
201+
202+
Here's an example `astro.config.ts` configuration file with a Starlight
203+
integration:
204+
205+
```ts
206+
import starlight from '@astrojs/starlight';
207+
import { defineConfig } from 'astro/config';
208+
209+
export default defineConfig({
210+
integrations: [
211+
starlight({
212+
components: {
213+
Head: './src/components/Head.astro',
214+
Footer: './src/components/Footer.astro',
215+
},
216+
}),
217+
],
218+
});
219+
```
220+
221+
With Starlight, components can be defined to override the default internal ones.
222+
They're not otherwise referenced in your source code, so you'd have to manually
223+
add them as entry files ([Knip itself did this][3]).
224+
225+
In the Astro plugin, there's no way to access this object containing
226+
`components` to add the component files as entry files if we were to try:
227+
228+
```ts
229+
const resolveEntryPaths: ResolveEntryPaths<AstroConfig> = async config => {
230+
console.log(config); // ¯\_(ツ)_/¯
231+
};
232+
```
233+
234+
This is why plugins can implement the `resolveFromAST` function.
235+
236+
### 8. resolveFromAST
237+
238+
Let's take a look at the Astro plugin implementation. This example assumes some
239+
familiarity with Abstract Syntax Trees (AST) and the TypeScript compiler API.
240+
Knip will provide more and more AST helpers to make implementing plugins more
241+
fun and a little less tedious.
242+
243+
Anyway, let's dive in. Here's how we're adding the Starlight `components` paths
244+
to the default `production` file patterns:
245+
246+
```ts
247+
import ts from 'typescript';
248+
import {
249+
getDefaultImportName,
250+
getImportMap,
251+
getPropertyValues,
252+
} from '../../typescript/ast-helpers.js';
253+
254+
const title = 'Astro';
255+
256+
const production = [
257+
'src/pages/**/*.{astro,mdx,js,ts}',
258+
'src/content/**/*.mdx',
259+
'src/middleware.{js,ts}',
260+
'src/actions/index.{js,ts}',
261+
];
262+
263+
const getComponentPathsFromSourceFile = (sourceFile: ts.SourceFile) => {
264+
const componentPaths: Set<string> = new Set();
265+
const importMap = getImportMap(sourceFile);
266+
const importName = getDefaultImportName(importMap, '@astrojs/starlight');
267+
268+
function visit(node: ts.Node) {
269+
if (
270+
ts.isCallExpression(node) &&
271+
ts.isIdentifier(node.expression) &&
272+
node.expression.text === importName // match the starlight() function call
273+
) {
274+
const starlightConfig = node.arguments[0];
275+
if (ts.isObjectLiteralExpression(starlightConfig)) {
276+
const values = getPropertyValues(starlightConfig, 'components');
277+
for (const value of values) componentPaths.add(value);
278+
}
279+
}
280+
281+
ts.forEachChild(node, visit);
282+
}
283+
284+
visit(sourceFile);
285+
286+
return componentPaths;
287+
};
288+
289+
const resolveFromAST: ResolveFromAST = (sourceFile: ts.SourceFile) => {
290+
// Include './src/components/Head.astro' and './src/components/Footer.astro'
291+
// as production entry files so they're also part of the analysis
292+
const componentPaths = getComponentPathsFromSourceFile(sourceFile);
293+
return [...production, ...componentPaths].map(id => toProductionEntry(id));
294+
};
295+
296+
export default {
297+
title,
298+
production,
299+
resolveFromAST,
300+
} satisfies Plugin;
301+
```
302+
196303
## Inputs
197304

198-
You may have noticed the `toDeferResolve` and `toEntry` functions. They're a way
199-
for plugins to tell what they've found and how to handle it. The more precise a
200-
plugin can be, the better it is for results and performance. Here's a list of
201-
all input type functions:
305+
You may have noticed functions like `toDeferResolve` and `toEntry`. They're a
306+
way for plugins to tell what they've found and how Knip should handle those. The
307+
more precise a plugin can be, the better it is for results and performance.
308+
Here's an overview of all input type functions:
202309

203310
### toEntry
204311

@@ -262,7 +369,8 @@ worfklow YAML files or husky scripts. Using this input type, a binary is
262369

263370
### Options
264371

265-
When creating inputs from specifiers, extra `options` can be provided.
372+
When creating inputs from specifiers, an extra `options` object as the second
373+
argument can be provided.
266374

267375
#### dir
268376

@@ -291,14 +399,14 @@ Knip now understands `esbuild` is a dependency of the workspace in the
291399

292400
## Argument parsing
293401

294-
As part of the [script parser][3], Knip parses command-line arguments. Plugins
402+
As part of the [script parser][4], Knip parses command-line arguments. Plugins
295403
can implement the `arg` object to add custom argument parsing tailored to the
296404
executables of the tool.
297405

298406
For now, there are two resources available to learn more:
299407

300-
- [The documented `Args` type in source code][4]
301-
- [Implemented `args` in existing plugins][5]
408+
- [The documented `Args` type in source code][5]
409+
- [Implemented `args` in existing plugins][6]
302410

303411
## Create a new plugin
304412

@@ -330,12 +438,14 @@ individual plugin pages][1] from the exported plugin values.
330438

331439
Thanks for reading. If you have been following this guide to create a new
332440
plugin, this might be the right time to open a pull request! Feel free to join
333-
[the Knip Discord channel][6] if you have any questions.
441+
[the Knip Discord channel][7] if you have any questions.
334442

335443
[1]: ../reference/plugins.md
336444
[2]: ../explanations/plugins.md#entry-files-from-config-files
337-
[3]: ../features/script-parser.md
338-
[4]: https://github.com/webpro-nl/knip/blob/main/packages/knip/src/types/args.ts
339-
[5]:
445+
[3]:
446+
https://github.com/webpro-nl/knip/blob/6a6954386b33ee8a2919005230a4bc094e11bc03/knip.json#L12
447+
[4]: ../features/script-parser.md
448+
[5]: https://github.com/webpro-nl/knip/blob/main/packages/knip/src/types/args.ts
449+
[6]:
340450
https://github.com/search?q=repo%3Awebpro-nl%2Fknip++path%3Apackages%2Fknip%2Fsrc%2Fplugins+%22const+args+%3D%22&type=code
341-
[6]: https://discord.gg/r5uXTtbTpc
451+
[7]: https://discord.gg/r5uXTtbTpc

packages/knip/fixtures/plugins/next/next.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const withTM = require('next-transpile-modules')([]);
55
module.exports = phase => {
66
const config = withTM({});
77
return {
8+
pageExtensions: ['page.tsx'],
89
...config,
910
};
1011
};

packages/knip/fixtures/plugins/next/pages/unused.tsx

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import 'sst-auth-handler-dep';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import 'sst-auth-dep';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import 'sst-some-dep';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "@fixtures/sst",
3+
"version": "*",
4+
"dependencies": {
5+
"sst": "*"
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { sst, SSTConfig } from 'sst';
2+
import { d } from 'sst-config-dep';
3+
import { AuthStack } from './stacks/AuthStack';
4+
import { AuthHandlerStack } from './stacks/AuthHandlerStack';
5+
6+
new sst.aws.Function('MyFunction', {
7+
handler: 'handlers/some-route-handler', // v3
8+
environment: {
9+
ACCOUNT: aws.getCallerIdentityOutput({}).accountId,
10+
REGION: aws.getRegionOutput().name,
11+
},
12+
});
13+
14+
export default {
15+
config(_input) {
16+
return {
17+
name: 'MyService',
18+
region: 'eu-west-2',
19+
stage: 'production',
20+
};
21+
},
22+
stacks(app) {
23+
app.stack(AuthStack).stack(AuthHandlerStack); // v2
24+
},
25+
} satisfies SSTConfig;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import sst from 'sst';
2+
import { d } from 'sst-auth-handler-stack-dep';
3+
import { use, StackContext, Function, FunctionProps } from 'sst/constructs';
4+
5+
export function AuthHandlerStack({ stack, app }: StackContext) {
6+
new sst.aws.Function('MyFunction', {
7+
handler: 'handlers/auth.handler',
8+
timeout: '3 minutes',
9+
memory: '1024 MB',
10+
});
11+
12+
return {
13+
handler,
14+
};
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import sst from 'sst';
2+
import { d } from 'sst-auth-stack-dep';
3+
import { use, StackContext, Function, FunctionProps } from 'sst/constructs';
4+
5+
export function AuthStack({ stack, app }: StackContext) {
6+
// Create single Lambda handler
7+
const handlerProps: FunctionProps = {
8+
handler: 'handlers/auth',
9+
permissions: ['perm1', 'perm2'],
10+
};
11+
12+
const handler = new Function(stack, 'MyHandler', handlerProps);
13+
14+
return {
15+
handler,
16+
};
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import starlight from '@astrojs/starlight';
2+
import { defineConfig } from 'astro/config';
3+
4+
export default defineConfig({
5+
integrations: [
6+
starlight({
7+
components: {
8+
Head: './components/Head.astro',
9+
Footer: './components/Footer.astro',
10+
},
11+
}),
12+
],
13+
});

packages/knip/fixtures/plugins/starlight/components/Footer.astro

Whitespace-only changes.

packages/knip/fixtures/plugins/starlight/components/Head.astro

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "@fixtures/starlight",
3+
"version": "*",
4+
"dependencies": {
5+
"@astrojs/starlight": "*",
6+
"astro": "*"
7+
}
8+
}

packages/knip/fixtures/plugins/tanstack-router/gen/routeTree.gen.ts

Whitespace-only changes.

packages/knip/fixtures/plugins/tanstack-router/node_modules/@tanstack/router-plugin/package.json

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/knip/fixtures/plugins/tanstack-router/node_modules/@tanstack/router-plugin/vite.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/knip/fixtures/plugins/tanstack-router/node_modules/@vitejs/plugin-react/index.js

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/knip/fixtures/plugins/tanstack-router/node_modules/@vitejs/plugin-react/package.json

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "@fixtures/tanstack-router",
3+
"version": "*",
4+
"dependencies": {
5+
"@tanstack/react-router": "*",
6+
"@tanstack/router-plugin": "*",
7+
"@vitejs/plugin-react": "*",
8+
"vite": "*"
9+
}
10+
}

packages/knip/fixtures/plugins/tanstack-router/routes.generated.ts

Whitespace-only changes.

packages/knip/fixtures/plugins/tanstack-router/routes/-unused.ts

Whitespace-only changes.

packages/knip/fixtures/plugins/tanstack-router/routes/-used.ts

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import '@tanstack/react-router';
2+
import './-used.ts';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"routesDirectory": "./routes",
3+
"generatedRouteTree": "./routes.generated.ts",
4+
"routeFileIgnorePrefix": "-",
5+
"quoteStyle": "single"
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { defineConfig } from 'vite';
2+
import react from '@vitejs/plugin-react';
3+
import { TanStackRouterVite } from '@tanstack/router-plugin/vite';
4+
5+
export default defineConfig({
6+
plugins: [
7+
TanStackRouterVite({
8+
target: 'react',
9+
autoCodeSplitting: true,
10+
routesDirectory: './routes',
11+
generatedRouteTree: './gen/routeTree.gen.ts',
12+
}),
13+
react(),
14+
],
15+
});

packages/knip/schema.json

+12
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,14 @@
555555
"title": "size-limit plugin configuration (https://knip.dev/reference/plugins/size-limit)",
556556
"$ref": "#/definitions/plugin"
557557
},
558+
"sst": {
559+
"title": "sst plugin configuration (https://knip.dev/reference/plugins/sst)",
560+
"$ref": "#/definitions/plugin"
561+
},
562+
"starlight": {
563+
"title": "starlight plugin configuration (https://knip.dev/reference/plugins/starlight)",
564+
"$ref": "#/definitions/plugin"
565+
},
558566
"storybook": {
559567
"title": "Storybook plugin configuration (https://knip.dev/reference/plugins/storybook)",
560568
"$ref": "#/definitions/plugin"
@@ -579,6 +587,10 @@
579587
"title": "tailwind plugin configuration (https://knip.dev/reference/plugins/tailwind)",
580588
"$ref": "#/definitions/plugin"
581589
},
590+
"tanstack-router": {
591+
"title": "tanstack-router plugin configuration (https://knip.dev/reference/plugins/tanstack-router)",
592+
"$ref": "#/definitions/plugin"
593+
},
582594
"travis": {
583595
"title": "travis plugin configuration (https://knip.dev/reference/plugins/travis)",
584596
"$ref": "#/definitions/plugin"

0 commit comments

Comments
 (0)