feat: add plugin check command (Experimental)#1228
Conversation
b70b642 to
bf016db
Compare
8fab3df to
6382bf7
Compare
src/plugin/check/index.ts
Outdated
| import noCybozuData from "./rules/no-cybozu-data"; | ||
| import noKintoneInternalSelector from "./rules/no-kintone-internal-selector"; | ||
|
|
||
| const eslintConfig: Linter.Config[] = [ | ||
| { | ||
| // https://eslint.org/docs/latest/use/configure/plugins#configure-a-virtual-plugin | ||
| plugins: { | ||
| local: { | ||
| rules: { | ||
| "no-cybozu-data": noCybozuData, | ||
| "no-kintone-internal-selector": noKintoneInternalSelector, | ||
| }, | ||
| }, | ||
| }, |
There was a problem hiding this comment.
cli-kintoneのローカルルールとして作っていたルールをjs-sdkのeslint-pluginに移動した。
kintone/js-sdk#3616
そのため、このローカルルールを削除して、@kintone/eslint-pluginを使うようにする。
There was a problem hiding this comment.
eslint-pluginのrecommendedを設定すればOK。
(eslint-plugin側がリリースされるまでCIが落ちるかも)
|
|
||
| ## Checked Rules | ||
|
|
||
| The `plugin check` command runs the following checks on JavaScript files in the plugin: |
There was a problem hiding this comment.
ここのルール説明はeslint-pluginへのリンクにする
There was a problem hiding this comment.
Pull request overview
Adds an experimental plugin check subcommand to help plugin developers detect usage of kintone internal/unsupported APIs and internal CSS selectors before release.
Changes:
- Introduces
plugin checkcommand wiring in the CLI and a new implementation that runs custom ESLint rules against plugin JS sources. - Updates packaging/dependencies so ESLint and
@kintone/eslint-pluginare available for the npm-distributed CLI while excluded from the SEA binary. - Adds (unlisted) documentation and E2E scaffolding/assets for the new command.
Reviewed changes
Copilot reviewed 14 out of 16 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| website/docs/guide/commands/plugin-check.md | New English docs page for plugin check usage/options/output. |
| website/i18n/ja/docusaurus-plugin-content-docs/current/guide/commands/plugin-check.md | New Japanese docs page for plugin check. |
| src/plugin/core/plugin/index.ts | Exposes contentsZip() so other features can read plugin contents from a zip. |
| src/plugin/check/index.ts | New implementation that dynamically imports ESLint + runs checks, supports plain/json output. |
| src/cli/plugin/index.ts | Registers the new plugin check subcommand under plugin. |
| src/cli/plugin/check.ts | New yargs command module for plugin check (experimental). |
| package.json | Excludes ESLint from ncc bundle; adds runtime deps for plugin check. |
| pnpm-workspace.yaml | Excludes @kintone/eslint-plugin from minimum release age. |
| pnpm-lock.yaml | Lockfile updates for added dependencies. |
| license-manager.config.js | Adds license overrides for newly introduced transitive deps. |
| cucumber.js | Skips @skip scenarios by default. |
| features/plugin/check.feature | Adds E2E scenarios for plugin check (real linting scenarios are @skip). |
| features/step_definitions/file.ts | Adds fixture mapping for plugin_with_issues. |
| features/assets/plugin_with_issues/** | Adds fixture plugin project used by E2E tests. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ``` | ||
| js/script.js | ||
| 42:7 error Accessing `cybozu.data` is not allowed. (local/no-cybozu-data) | ||
|
|
||
| Files checked: 3 | ||
| Problems: 1 (Errors: 1, Warnings: 0) | ||
| ``` | ||
|
|
||
| ### JSON Format | ||
|
|
||
| ```json | ||
| { | ||
| "errorCount": 1, | ||
| "warningCount": 0, | ||
| "filesChecked": 3, | ||
| "filesSkipped": 1, | ||
| "files": [ | ||
| { | ||
| "filePath": "js/script.js", | ||
| "messages": [ | ||
| { | ||
| "ruleId": "local/no-cybozu-data", | ||
| "severity": 2, | ||
| "message": "Accessing `cybozu.data` is not allowed.", | ||
| "line": 42, | ||
| "column": 7 | ||
| } |
There was a problem hiding this comment.
The docs’ sample output uses the rule id local/no-cybozu-data, but the implementation enables @kintone/eslint-plugin/no-cybozu-data / @kintone/eslint-plugin/no-kintone-internal-selector, so the reported ruleId will include the plugin name (e.g. @kintone/eslint-plugin/no-cybozu-data). Please update the sample outputs to match the actual ruleId values (both in the Plain and JSON examples).
| ### JSON Format | ||
|
|
||
| ```json | ||
| { | ||
| "errorCount": 1, | ||
| "warningCount": 0, | ||
| "filesChecked": 3, | ||
| "filesSkipped": 1, | ||
| "files": [ | ||
| { | ||
| "filePath": "js/script.js", | ||
| "messages": [ | ||
| { | ||
| "ruleId": "local/no-cybozu-data", | ||
| "severity": 2, | ||
| "message": "Accessing `cybozu.data` is not allowed.", | ||
| "line": 42, | ||
| "column": 7 | ||
| } | ||
| ], | ||
| "errorCount": 1, | ||
| "warningCount": 0 | ||
| } | ||
| ] | ||
| } |
There was a problem hiding this comment.
The JSON output example documents messages entries as a reduced shape (ruleId/severity/message/line/column), but the code currently serializes ESLint’s full message objects. Either normalize the JSON output to the documented shape, or adjust the docs to clarify that the output is raw ESLint messages (and may include additional fields).
| | Option | Required | Description | | ||
| | --------------- | -------- | --------------------------------------------------------------------- | | ||
| | `--input`, `-i` | Yes | The input plugin zip file or manifest.json file path. | | ||
| | `--format` | | Output format.<br/>Format: `plain` or `json`.<br/>Default to `plain`. | |
There was a problem hiding this comment.
Minor grammar: “Default to plain.” should be “Defaults to plain.” (or “Default: plain.”) to match the style used elsewhere in the docs.
| | `--format` | | Output format.<br/>Format: `plain` or `json`.<br/>Default to `plain`. | | |
| | `--format` | | Output format.<br/>Format: `plain` or `json`.<br/>Defaults to `plain`.| |
| ### Plainフォーマット(デフォルト) | ||
|
|
||
| ``` | ||
| js/script.js | ||
| 42:7 error Accessing `cybozu.data` is not allowed. (local/no-cybozu-data) | ||
|
|
||
| Files checked: 3 | ||
| Problems: 1 (Errors: 1, Warnings: 0) | ||
| ``` | ||
|
|
||
| ### JSONフォーマット | ||
|
|
||
| ```json | ||
| { | ||
| "errorCount": 1, | ||
| "warningCount": 0, | ||
| "filesChecked": 3, | ||
| "filesSkipped": 1, | ||
| "files": [ | ||
| { | ||
| "filePath": "js/script.js", | ||
| "messages": [ | ||
| { | ||
| "ruleId": "local/no-cybozu-data", | ||
| "severity": 2, | ||
| "message": "Accessing `cybozu.data` is not allowed.", | ||
| "line": 42, | ||
| "column": 7 | ||
| } |
There was a problem hiding this comment.
サンプル出力のルールIDが local/no-cybozu-data になっていますが、実装では @kintone/eslint-plugin/no-cybozu-data / @kintone/eslint-plugin/no-kintone-internal-selector を有効化しているため、出力される ruleId もそれに合わせた値になります。Plain/JSON両方の例を実際の ruleId に合わせて更新してください。
| import { check } from "../../plugin/check"; | ||
| import type { OutputFormat } from "../../plugin/info"; | ||
|
|
||
| const command = "check"; | ||
|
|
||
| const describe = "Check plugin"; | ||
|
|
||
| const outputFormats: OutputFormat[] = ["plain", "json"]; |
There was a problem hiding this comment.
OutputFormat is imported from ../../plugin/info, but this command calls ../../plugin/check and that module already defines its own OutputFormat. Importing the type from plugin/info introduces unnecessary coupling and could become incorrect if the formats diverge; please import OutputFormat from ../../plugin/check (or define a local union here).
| ### JSONフォーマット | ||
|
|
||
| ```json | ||
| { | ||
| "errorCount": 1, | ||
| "warningCount": 0, | ||
| "filesChecked": 3, | ||
| "filesSkipped": 1, | ||
| "files": [ | ||
| { | ||
| "filePath": "js/script.js", | ||
| "messages": [ | ||
| { | ||
| "ruleId": "local/no-cybozu-data", | ||
| "severity": 2, | ||
| "message": "Accessing `cybozu.data` is not allowed.", | ||
| "line": 42, | ||
| "column": 7 | ||
| } | ||
| ], | ||
| "errorCount": 1, | ||
| "warningCount": 0 | ||
| } | ||
| ] | ||
| } |
There was a problem hiding this comment.
JSONフォーマット例では messages を最小限のフィールドだけに絞った形で記載していますが、現状の実装は ESLint の message オブジェクトをそのまま JSON 化するため、追加フィールド(endLine 等)が含まれます。出力を例の形に正規化するか、ドキュメント側で「ESLintの生メッセージを出力する」旨を明記してください。
| "dependencies": { | ||
| "@inquirer/prompts": "^7.10.1", | ||
| "@kintone/eslint-plugin": "^0.1.1", | ||
| "@kintone/plugin-manifest-validator": "^11.1.0", | ||
| "@kintone/rest-api-client": "^6.1.2", | ||
| "chalk": "4.1.2", | ||
| "chokidar": "^4.0.3", | ||
| "csv-parse": "^5.6.0", | ||
| "csv-stringify": "6.5.2", | ||
| "eslint": "^9.38.0", | ||
| "https-proxy-agent": "^7.0.6", |
There was a problem hiding this comment.
eslint is now listed in both devDependencies and dependencies (with different ranges). This can lead to duplicate installs and version skew between development and runtime. Since plugin check needs ESLint at runtime, consider keeping a single entry (typically in dependencies) and aligning/removing the devDependency version.
| if (m.message.match(/Definition for rule '.+' was not found./)) { | ||
| continue; | ||
| } |
There was a problem hiding this comment.
normalizeResults silently drops ESLint messages that indicate a rule is missing (“Definition for rule ... was not found.”). This can hide misconfiguration (e.g., the recommended config enabling rules whose plugins aren’t installed) and give users a false sense that checks are complete. Consider surfacing these as errors/warnings in the output (or fail fast) so users know the check setup is incomplete.
| if (m.message.match(/Definition for rule '.+' was not found./)) { | |
| continue; | |
| } |
| export const check = async (inputFilePath: string, format: OutputFormat) => { | ||
| const { ESLint, eslintConfig } = await loadESLintDependencies(); | ||
|
|
||
| let manifest: ManifestInterface; | ||
| let driver: DriverInterface; | ||
|
|
||
| // TODO: Better file type detection | ||
| switch (path.extname(inputFilePath)) { | ||
| case ".json": { | ||
| manifest = await ManifestFactory.loadJsonFile(inputFilePath); | ||
| driver = new LocalFSDriver(path.dirname(inputFilePath)); | ||
| break; | ||
| } | ||
| case ".zip": { | ||
| const buffer = await fs.readFile(inputFilePath); | ||
| const pluginZip = await PluginZip.fromBuffer(buffer); | ||
| manifest = await pluginZip.manifest(); | ||
| driver = await pluginZip.contentsZip(); | ||
| break; | ||
| } | ||
| default: { | ||
| throw new Error(`Unsupported file format: ${inputFilePath}`); | ||
| } | ||
| } | ||
|
|
||
| const eslint = new ESLint({ | ||
| overrideConfigFile: true, | ||
| overrideConfig: eslintConfig, | ||
| }); | ||
|
|
||
| const allResults: AllResult = { | ||
| errorCount: 0, | ||
| warningCount: 0, | ||
| filesChecked: 0, | ||
| filesSkipped: 0, | ||
| files: [], | ||
| }; | ||
|
|
||
| for (const source of manifest.sourceList()) { | ||
| if (path.extname(source) !== ".js") { | ||
| allResults.filesSkipped++; | ||
| logger.debug(`Skip ${source}`); | ||
| continue; | ||
| } | ||
|
|
||
| logger.debug(`Checking ${source}`); | ||
| allResults.filesChecked++; | ||
| const sourceFile = await driver.readFile(source); | ||
| const results = normalizeResults( | ||
| await eslint.lintText(sourceFile.toString(), { | ||
| filePath: source, | ||
| }), | ||
| ); | ||
|
|
||
| const fileResult: FileResult = { | ||
| filePath: source, | ||
| messages: [], | ||
| errorCount: 0, | ||
| warningCount: 0, | ||
| }; | ||
| for (const result of results) { | ||
| fileResult.errorCount += result.errorCount; | ||
| fileResult.warningCount += result.warningCount; | ||
|
|
||
| fileResult.messages.push(...result.messages); | ||
| } | ||
| allResults.errorCount += fileResult.errorCount; | ||
| allResults.warningCount += fileResult.warningCount; | ||
| allResults.files.push(fileResult); | ||
| } | ||
|
|
||
| console.log(formatResult(allResults, format)); | ||
| }; |
There was a problem hiding this comment.
There’s no non-skipped automated coverage for the new plugin check behavior (the E2E scenarios that validate real linting are tagged @skip). Please add a Vitest test that runs the command/module in “npm mode” against a small fixture plugin and asserts the plain/json outputs (and counts) so regressions in rule wiring/output formatting are caught in CI.
Why
When developing kintone plugins, using internal APIs or internal CSS class names creates a risk that the plugin will break in future kintone updates. However, there was no mechanism to detect these usages beforehand.
We need a tool that can automatically detect potential issues so plugin developers can write safer code.
What
Added a
plugin checkcommand to check kintone plugin code for potential issues.This command inspects JavaScript files in plugins using the following custom ESLint rules:
Check Rules
no-cybozu-data (Doc) :
cybozu.datacybozu.data,cybozu["data"],cybozu?.data, etc.no-kintone-internal-selector (Doc) :
gaia-argoui-*(e.g.,gaia-argoui-button)*-gaia(e.g.,button-gaia)ocean-*(e.g.,ocean-ui-button)kintone-*(e.g.,kintone-dialog)querySelector,querySelectorAll,closest,getElementsByClassName,matches) but also class names in string literals (e.g., jQuery)Features
plain(default) andjsonoutput formatsHow to test
Basic functionality
Run test cases
Sample code for detection verification
You can test with a plugin containing code like:
Checklist
pnpm lintandpnpm teston the root directory.