Repository navigation
Add textlint plugin - #2039
Add textlint plugin#2039
Conversation
Resolve rules, presets, filter rules and plugins referenced in textlint config files and `textlint --rule/--preset/--plugin/--config` arguments to their package names, following textlint's module resolution.
webpro
left a comment
There was a problem hiding this comment.
Thanks for adding textlint support! Two dependency-resolution issues are detailed inline.
| const toPackageDependency = (prefix: string, name: string, candidates: string[], manifest: Manifest) => | ||
| toDependency( | ||
| candidates.find(candidate => isDeclared(manifest, candidate)) ?? | ||
| (hasPrefix(prefix, name) ? name : createFullPackageName(prefix, name)) | ||
| ); |
There was a problem hiding this comment.
[P2] Local rule paths become bogus dependencies
A rule configured with require.resolve('./rules/check-document.cjs') works in textlint, but Knip prefixes the resolved path and reports an unlisted textlint-rule-. Preserve local paths as file inputs.
There was a problem hiding this comment.
Fixed in 1368973. Rules, filters and plugins given as a relative or absolute path are now added as entry files instead of being turned into a textlint-rule- dependency. The fixture now has a .textlintrc.cjs using require.resolve('./rules/check-document.cjs'): the rule file is picked up and nothing extra is reported as unlisted.
| string: ['rule', 'preset', 'plugin'], | ||
| resolveInputs: (parsed, { manifest }) => { | ||
| const inputs: Input[] = []; | ||
| for (const rule of [parsed.rule ?? []].flat()) inputs.push(toRuleDependency(String(rule), manifest)); |
There was a problem hiding this comment.
[P2] Comma-separated CLI values are not split
--rule no-todo,max-comma loads two rules in textlint. Knip reports both dependencies unused and invents an unlisted textlint-rule-no-todo,max-comma. Handle array values for rules, presets, and plugins.
There was a problem hiding this comment.
Fixed in 1368973. --rule, --preset and --plugin values are now split on commas (and repeated flags still work). The fixture script now uses --rule no-dead-link,no-empty-section --preset jtf-style,ja-spacing --plugin review,latex2e. Before this change it reported all six as unused along with three invented unlisted names. Now it reports neither.
…CLI values
Rules, filters and plugins given as a relative or absolute path (e.g.
`require.resolve('./rules/check-document.cjs')`) are now added as entry
files instead of being prefixed into a `textlint-rule-` dependency.
`--rule`, `--preset` and `--plugin` accept comma-separated lists, as
textlint does, so `--rule no-todo,max-comma` yields both rules.
commit: |
webpro
left a comment
There was a problem hiding this comment.
Thanks for addressing both earlier findings. One local-module resolution issue remains.
| const toModuleDependency = (prefix: string, key: string, manifest: Manifest) => | ||
| toPackageDependency(prefix, key, [createFullPackageName(prefix, key), key], manifest); | ||
|
|
||
| const toLocalEntry = (key: string, cwd: string) => toEntry(toAbsolute(key, cwd)); |
There was a problem hiding this comment.
[P2] Extensionless local modules are reported unused
A .textlintrc.cjs rule keyed by path.join(__dirname, 'rules/check') can load rules/check.js or rules/check/index.js in textlint. Here, toEntry treats the path as a glob, so Knip reports the loaded rule file as unused. Please use toDeferResolveEntry for these local module paths, and cover extensionless files and directory modules.
There was a problem hiding this comment.
Fixed in 6680de6. toLocalEntry now uses toDeferResolveEntry, so the path goes through module resolution instead of being treated as a glob:
const toLocalEntry = (key: string, cwd: string) => toDeferResolveEntry(toAbsolute(key, cwd));That covers rules, filters and plugins, since all three route through this helper.
The fixture's .textlintrc.cjs now keys rules by all three shapes textlint accepts:
rules: {
[require.resolve('./rules/check-document.cjs')]: true,
[path.join(__dirname, 'rules/check-heading')]: true, // rules/check-heading.js
[path.join(__dirname, 'rules/check-link')]: true, // rules/check-link/index.js
}The test asserts Object.keys(issues.files).length === 0 and the counters go to 4 processed. On the previous commit the two new rule files are reported unused (files: 2), so it fails there and passes now.
pnpm run ci is clean.
A rule, filter or plugin keyed by a local path is resolved by textlint's module resolver, so it can omit the extension or point at a directory with an index file. toEntry treated the path as a glob, which matched nothing and left the loaded rule file reported as unused. The fixture now also keys rules by an extensionless path and by a directory.
6680de6 to
c2ba06f
Compare
|
🚀 This pull request is included in v6.36.0. See Release 6.36.0 for release notes. Using Knip in a commercial project? Please consider becoming a sponsor. |
Adds a plugin for textlint. It turns on when
textlintis a dependency.Right now, projects using textlint get every
textlint-rule-*,textlint-rule-preset-*,textlint-filter-rule-*andtextlint-plugin-*package reported as an unused devDependency, because those packages are only referenced by short names in the config.What it does
.textlintrc,.textlintrc.{json,yaml,yml,js,cjs}and thetextlintkey inpackage.json. This is the same list textlint's@textlint/config-loaderloads viarc-config-loader.rules: each key maps totextlint-rule-*. Preset keys (preset-x,@scope/preset-x,textlint-rule-preset-x) map totextlint-rule-preset-*. Rules set tofalseare skipped, like textlint does.filtersmaps totextlint-filter-rule-*.plugins(array or object) maps totextlint-plugin-*.--rule,--presetand--pluginresolve the same way.--config/-csends that file throughresolveConfig.Name resolution follows textlint's
createFullPackageNameandTextLintModuleResolver. textlint tries the prefixed name first (for example@scope/x→@scope/textlint-rule-x) and then the name as written. The plugin picks whichever of those is declared inpackage.json, similar to the Remark plugin. If none is declared, it reports the prefixed name as unlisted. A name that already has the prefix (textlint-rule-foo,@scope/textlint-plugin-foo) is reported unchanged.Example:
{ "rules": { "no-todo": true, "@textlint-rule/no-unmatched-pair": true, "preset-ja-technical-writing": true }, "filters": { "comments": true }, "plugins": { "@textlint/markdown": true } }This resolves to
textlint-rule-no-todo,@textlint-rule/textlint-rule-no-unmatched-pair,textlint-rule-preset-ja-technical-writing,textlint-filter-rule-commentsand@textlint/textlint-plugin-markdown.Tests
test/plugins/textlint.test.tscovers.textlintrc.jsonplus a script with--config,--rule,--presetand--plugin. It includes unscoped, scoped, already-prefixed, preset and disabled rules, and expects two unlisted rules.test/plugins/textlint-package-json.test.tscovers thepackage.jsontextlintkey with array-styleplugins.Commands run:
bun test test/plugins/textlint.test.ts test/plugins/textlint-package-json.test.ts: passpnpm test --runtime node --smoke(packages/knip): 1055/1055 passpnpm build,pnpm knip,pnpm knip:production(packages/knip): cleanpnpm run ci(root: oxlint, oxfmt --check, installed-check, line endings): cleanClaude Code helped write this change. I reviewed it.
🤖 Generated with Claude Code
https://claude.ai/code/session_01QBWFWqjbUQGS1c9LsM2bxX