ソース情報
- リポジトリ
- JetBrains/intellij-community
- ソースの最終更新活動
- 2026年8月5日 18:20
- 検出された SKILL.md の言語
- 英語
- スター
- 20,445
- フォーク
- 6,009
インストール方法
デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。
ソースファイルを確認
インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。
SKILL.md を表示中
SKILL.md
ソースの指示 · 読み取り専用プレビュー- name
- extract-module
- description
- Extract optional plugin dependencies into IntelliJ content modules.
# Extracting an Optional Dependency into a New Content Module
Use this guide when making a dependency of a plugin module optional by moving it into a separate content module.
**Goal:** the host module continues to work when the new module is absent, and behaves identically when it is present.
---
## When to Use Each Pattern
### Pattern A — Simple move
All code that touches dependency X is isolated in a few files with no callers inside the host module. Move those files wholesale into the new module.
### Pattern B — Extension Point (EP)
The host module's core files contain scattered references to X. Introduce an EP interface in the host module (no X imports), implement it in the new module, and replace direct X calls with null-safe static helpers.
---
## Steps
### 1. Create the module directory
```
plugins/<plugin-name>/<module-dir>/
resources/
intellij.<module-name>.xml ← module descriptor
src/
com/intellij/<...>/ ← sources (directory must match package)
intellij.<module-name>.iml ← module definition
```
### 2. Write the `.iml`
```xml
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="jetbrains-annotations" level="project" />
<orderEntry type="module" module-name="intellij.<host-module>" />
<!-- add other required modules -->
</component>
</module>
```
Rules:
- Never use `packagePrefix` on `<sourceFolder>`. Use a matching directory structure instead.
- `.iml` files are serialized in canonical form — no trailing newline, no reformatting, no reordering.
#### Kotlin stdlib — mandatory for any Kotlin module
Every new Kotlin module needs the `kotlin-stdlib` library on its Bazel compilation classpath. Add it as a direct IML library dependency:
```xml
<orderEntry type="library" name="kotlin-stdlib" level="project" />
```
```
Cannot access built-in declaration 'kotlin.Any'. Ensure that you have a dependency on the Kotlin standard library.
```
This error is deceptive: it looks like a single missing type, but it means the entire stdlib is absent, so virtually every Kotlin annotation (`@JvmStatic`, `@Throws`, `checkNotNull`, `::class.java`, …) and all built-in types fail simultaneously.
#### Common class-to-module mapping
Bazel strict-deps requires every class to be in a direct IML dependency. The following classes live in modules that differ from what their package names suggest:
| Class(es) | Package | IML module | Bazel target |
|-----------|---------|-----------|--------------|
| `ExecutionException`, `GeneralCommandLine`, `KillableColoredProcessHandler`, `OSProcessHandler`, `ScriptRunnerUtil`, `Url`, `NetUtils` | `com.intellij.execution.*`, `com.intellij.util.*` | `intellij.platform.ide.util.io` | `@community//platform/platform-util-io:ide-util-io` |
| `AsyncPromise`, `Promise` | `org.jetbrains.concurrency` | `intellij.platform.concurrency` | `@community//platform/util/concurrency` |
| `AppExecutorUtil`, `ProcessAdapter`, `ProcessEvent`, `ProcessOutputTypes`, `ParametersListUtil`, `FileUtil`, `StringUtil` | `com.intellij.util.*`, `com.intellij.openapi.util.*` | `intellij.platform.util` | `@community//platform/util` |
| `Editor` | `com.intellij.openapi.editor` | `intellij.platform.editor.ui` | `@community//platform/editor-ui-api` |
| `MultipleLangCommentProvider` | `com.intellij.psi.templateLanguages` | `intellij.platform.lang.impl` | `@community//platform/lang-impl` |
Note: the IML module name for `AsyncPromise`/`Promise` is `intellij.platform.concurrency` (not `intellij.platform.util.concurrency`), even though the source lives under `platform/util/concurrency/`.
If the new module calls a method whose **return type** comes from a third module (e.g. a method returning `ImmutableList<String>`), that third module (`intellij.libraries.guava`) must also be a direct IML dep — Kotlin's type checker needs to verify the return type at compile time.
### 3. Write the module descriptor XML
If a class in the new module is used directly by another plugin (not just another module within the same plugin), set the module's `visibility` on the `<idea-plugin>` root. Choose the level by who the consumer is:
- `visibility="internal"` — the consumer is another plugin **inside this monorepo** (a separate `<plugin id="...">`, e.g. a class subclassed or called from it). This is the common case.
- `visibility="public"` — the class is part of API meant for **external third-party plugins**.
```xml
<idea-plugin visibility="internal">
...
</idea-plugin>
```
Without this, the other plugin cannot load the class even if it declares the module as a dependency.
The generator manages a `<dependencies>` region. For dependencies the generator doesn't produce automatically (e.g. a plugin dependency that was removed from the host), declare them **before the `<!-- region -->` marker, inside the same `<dependencies>` tag**:
```xml
<idea-plugin>
<dependencies>
<plugin id="com.example.some-plugin"/> <!-- manual: not emitted by generator -->
<!-- region Generated dependencies - run `Generate Product Layouts` to regenerate -->
<module name="intellij.<host-module>"/>
<!-- ... -->
<!-- endregion -->
</dependencies>
<extensions defaultExtensionNs="com.intellij">
<!-- register extensions here -->
</extensions>
</idea-plugin>
```
Do **not** create a second standalone `<dependencies>` block before the region — this makes the file "out of sync" and breaks `AllProductsPackagingTest#suiteValidations`.
### 4. Package and source file conventions
- **Always create Kotlin files** for new code. But when you just move the file, please keep it the same type it was (Kotlin or Java).
- Source files must live in a directory matching their package: `src/com/intellij/foo/bar/MyClass.kt` for package `com.intellij.foo.bar`.
- Package names must follow the `com.<module-name>` convention:
- Module `intellij.foo.bar` → package `com.intellij.foo.bar`
- `IntelliJProjectPackageNamesTest` enforces this. Do not add exceptions to `non-standard-root-packages.txt` for new modules.
- **Preserve freemium availability:** both the host module and the new content module must have the same availability in IDEA Free mode as the original module had. If the original module was available in free mode, both new modules must remain available there. If the original was not available in free mode, neither should the new modules be. `PluginsAvailableInIdeaFreeModeTest` enforces this.
### 5. Declare the EP in the host module (Pattern B only)
In the host module's XML, declare the EP with `qualifiedName`:
```xml
<extensionPoints>
<extensionPoint qualifiedName="com.intellij.<language>.<epName>"
interface="com.intellij.<language>.<feature>.MyFeatureHelper"
dynamic="true"/>
</extensionPoints>
```
`qualifiedName` format: `com.intellij.<language/framework, lowercase>.<epNameLikeThisOne>`
Write the EP interface in the host module (no X imports). Provide `@JvmStatic` companion helpers that gracefully return no-op defaults when the EP is absent:
```kotlin
interface MyFeatureHelper {
fun doSomething(element: PsiElement): Boolean
companion object {
@JvmField
val EP = ExtensionPointName.create<MyFeatureHelper>("com.intellij.<language>.<epName>")
// An EP can have several registered extensions — process all of them, not just the first.
@JvmStatic fun checkSomething(element: PsiElement?): Boolean =
element != null && EP.extensionList.any { it.doSomething(element) }
}
}
```
Register the implementation in the new module's XML:
```xml
<extensions defaultExtensionNs="com.intellij">
<<language>.<epName> implementation="com.intellij.<language>.<feature>.MyFeatureHelperImpl"/>
</extensions>
```
#### Multi-method EP bundling and opaque state pattern
When separating a library whose operations form a **lifecycle** (capture some state, then use it later), bundle all related operations into one EP rather than creating one EP per method. This avoids proliferating EP registrations and keeps the implementation cohesive.
When lifecycle EP methods need to **pass typed state** between calls (e.g., capture a `List<CssSelectorSuffix>` in step 1, consume it in step 2), but the host module cannot import that type, use `Any?` as the state type. The EP interface uses `Any?`; the implementation casts internally with `@Suppress("UNCHECKED_CAST")`:
```kotlin
// In host module (no X imports)
interface MyLifecycleHelper {
fun captureState(file: PsiFile): Any? // returns X-typed data, opaque to host
fun applyState(file: PsiFile, state: Any?) // receives it back; casts inside impl
companion object {
val EP = ExtensionPointName.create<MyLifecycleHelper>("com.intellij.<language>.myLifecycleHelper")
fun captureState(file: PsiFile): Any? = EP.extensionList.firstOrNull()?.captureState(file)
fun applyState(file: PsiFile, state: Any?) = EP.extensionList.firstOrNull()?.applyState(file, state)
}
}
// In new CSS/X module
internal class MyLifecycleHelperImpl : MyLifecycleHelper {
override fun captureState(file: PsiFile): Any? = getThings(file) // returns List<XThing>
override fun applyState(file: PsiFile, state: Any?) {
@Suppress("UNCHECKED_CAST")
val things = state as? List<XThing> ?: emptyList()
// use things...
}
}
```
### 6. Register the new module in the plugin
**`plugin/resources/META-INF/plugin.xml`** — add a `<module>` content entry.
**`plugin/plugin-content.yaml`** — add a jar entry.
**Project module files** — register the new module with the helper; it updates `.idea/modules.xml`, updates `community/.idea/modules.xml` for community modules, preserves canonical entry order, and removes the `.iml` trailing newline:
```bash
bun build/jps-module.mjs register plugins/<plugin-name>/<module-dir>/intellij.<module-name>.iml --fix-iml-eof
```
### 7. Fix downstream consumers of the removed transitive dependency
Removing a dependency from the host module may break modules that relied on it transitively. `AllProductsPackagingTest#targetValidations` will report which ones.
Fix: add explicit deps to their plugin XML in the manual section before the `<!-- region -->` marker:
```xml
<dependencies>
<module name="intellij.some.formerly.transitive.module"/>
<!-- region Generated dependencies ... -->
...
<!-- endregion -->
</dependencies>
```
**Also check other plugins.** If the moved code was a superclass or utility called from a different plugin, that plugin will fail to compile. For each such plugin:
1. Add `<module name="intellij.new.module"/>` to its plugin XML.
2. Add `<orderEntry type="module" module-name="intellij.new.module" />` to its `.iml`.
3. Ensure `visibility="public"` is set on the new module's XML (see step 3).
#### Kotlin `open` — required when a class is subclassed from outside the module
Kotlin classes are `final` by default. If the moved class is:
- subclassed by another module or plugin, or
- has an inner class that is anonymously subclassed elsewhere,
both the outer class **and** the relevant inner class must be marked `open`:
```kotlin
open class MyStrategy : BaseStrategy() {
// ...
open class MyTokenizer : BaseTokenizer() {
protected open fun shouldSkip(element: MyElement): Boolean { ... }
}
}
```
Forgetting `open` produces `cannot inherit from final class` compile errors in the downstream plugin.
### 8. `git add` all new files
New files are untracked. Add them explicitly before running tests:
```
git add plugins/<plugin-name>/<new-module>/
```
---
## Required Commands After Changes
### After any `.iml` change
```bash
./build/jpsModelToBazel.cmd
```
### After any `.iml`, plugin XML, or module structure change
```bash
./bazel.cmd run //platform/buildScripts:plugin-model-tool
```
Expected: `✓ All files unchanged`. If files change, inspect them — the generator may be removing deps you set incorrectly, or adding ones you missed.
### Required tests
```bash
# Validates packaging: runtime deps available, generated XMLs in sync
./tests.cmd --module intellij.idea.ultimate.build.tests \
--test "com.intellij.idea.ultimate.build.smokeTests.AllProductsPackagingTest"
# Validates package naming: com.<module-name> convention
./tests.cmd --module intellij.projectStructureTests \
--test "com.intellij.ideaProjectStructure.fast.IntelliJProjectPackageNamesTest"
# Validates plugin availability in IDEA Free mode
./tests.cmd --module intellij.projectStructureTests \
--test "com.intellij.ideaProjectStructure.fast.PluginsAvailableInIdeaFreeModeTest"
```
All three must pass before the work is done.
Run them one at a time. `tests.cmd` kills leftover processes by name on startup (`Killing process containing subpath 'ide-tests'`), so a concurrent run shoots down the one already in flight — which surfaces as an unrelated-looking failure such as `AllProductsPackagingTest#build`.
---
## Common Pitfalls
| Pitfall | Symptom | Fix |
|---------|---------|-----|
| `packagePrefix` in `.iml` | `IntelliJProjectPackageNamesTest` finds wrong root package | Remove `packagePrefix`; use matching directory structure |
| Package doesn't match module name | `IntelliJProjectPackageNamesTest` fails | Rename to `com.<module-name>` and move files |
| EP registered with `name` instead of `qualifiedName` | EP not found | Use `qualifiedName="com.intellij.<language>.epName"` |
GitHubで見るこの SKILL.md は非常に大きいため、SkillsMP では最初のセクションだけを表示しています。 GitHubで見る