跳到正文
cccskills
免费在 GitHub 公开中文介绍

extract-module

将 IntelliJ 插件的可选依赖拆到独立内容模块,同时确保宿主模块在其缺席时仍可运行。指导选择移动独立文件或引入扩展点,并更新模块配置、下游依赖及打包检查。

原文查看原文说明

Extract optional plugin dependencies into IntelliJ content modules.

JetBrains/intellij-community星标 20,685(GitHub 20,685 + cccskills 0)

实测评分 未实测

查看安装方法

适用场景

  • 将插件依赖改为可选
  • 把功能拆到独立模块
  • 通过扩展点分离依赖
  • 整理模块拆分后的检查

中文介绍

功能

本技能介绍如何把插件依赖移入单独的 IntelliJ 内容模块。目标是新模块不存在时宿主模块仍能运行,存在时则保持原有行为。如果使用该依赖的代码集中在少数几个文件中,可以整体移动这些文件;如果宿主代码中散布着许多相关调用,则在宿主模块定义扩展点(EP),再由新模块提供实现。指南还涵盖 .iml、插件 XML、源文件目录、可见性以及下游模块的依赖声明。

适用场景

当需要从大型插件中拆出功能,或希望让特定语言、框架所需的依赖变为可选时,可以使用本技能。内容还涉及会影响构建和功能可用性的事项,包括 Kotlin 模块依赖、包名、Free 模式下的可用状态,以及其他插件如何使用新模块。

使用示例

  • “规划如何将这个依赖移到独立内容模块。”
  • “说明如何通过扩展点替换分散的依赖调用。”
  • “整理模块拆分后需要的依赖声明和检查。”

注意事项

本指南以 IntelliJ 平台仓库的目录结构和命令为前提。修改 .iml、插件 XML 或模块结构后,需要运行生成工具和指定测试。

本介绍由 AI(OpenAI Codex)根据公开的 SKILL.md 撰写。准确的规格请查看下方原文。

文档检查

AI 根据 SKILL.md 的内容检查了需要注意的操作和使用前提。评分只依据实际运行的结果(实测对比)。

安全性发送数据、删除文件、执行命令等是否有说明
B部分不足会修改源文件和打包配置并运行测试;修改前的确认步骤较少。
安装难易度所需工具和 API 密钥是否少且有说明
B部分不足需要 IntelliJ 仓库、构建工具和测试,但安装说明不完整。

不计入评分。 由 AI(OpenAI Codex)于 2026年10月28日 评估。仅供参考,安装前请同时查看原文。

如果您认为评估有误,请申请复核

请写明哪一项有误以及原因。无需登录。我们会对照 SKILL.md 重新评估,并在此处更新结果。

此表单受 reCAPTCHA 保护,适用 Google 的隐私权政策和服务条款。

包含的文件(1)

  • SKILL.md20.6 KB

SKILL.md(原文)

安装前可以查看交给 AI 代理的指令内容。

<!-- Generated by community/.ai/render-guides.mjs; edit community/.agents/skills/extract-module/SKILL.md -->

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 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:

<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)PackageIML moduleBazel target
ExecutionException, GeneralCommandLine, KillableColoredProcessHandler, OSProcessHandler, ScriptRunnerUtil, Url, NetUtilscom.intellij.execution.*, com.intellij.util.*intellij.platform.ide.util.io@community//platform/platform-util-io:ide-util-io
AsyncPromise, Promiseorg.jetbrains.concurrencyintellij.platform.concurrency@community//platform/util/concurrency
AppExecutorUtil, ProcessAdapter, ProcessEvent, ProcessOutputTypes, ParametersListUtil, FileUtil, StringUtilcom.intellij.util.*, com.intellij.openapi.util.*intellij.platform.util@community//platform/util
Editorcom.intellij.openapi.editorintellij.platform.editor.ui@community//platform/editor-ui-api
MultipleLangCommentProvidercom.intellij.psi.templateLanguagesintellij.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.
<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:

<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:

<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:

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:

<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"):

// 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. That entry is the whole packaging statement: the dev distribution derives the member's jar from its loading rule and its own descriptor, so no packaging file needs a new line. Run ./build/jpsModelToBazel.cmd and never hand-edit a dev-dist.yaml.

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:

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:

<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:

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

./build/jpsModelToBazel.cmd

After any .iml, plugin XML, or module structure change

./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

# Validates packaging: runtime deps available, generated XMLs in sync
./bazel.cmd test //build:all-products-packaging_test

# 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

PitfallSymptomFix
packagePrefix in .imlIntelliJProjectPackageNamesTest finds wrong root packageRemove packagePrefix; use matching directory structure
Package doesn't match module nameIntelliJProjectPackageNamesTest failsRename to com.<module-name> and move files
EP registered with name instead of qualifiedNameEP not foundUse qualifiedName="com.intellij.<language>.epName"
Manual <dependencies> block as a separate tag (not inside the region's block)AllProductsPackagingTest#suiteValidations: "Generated file is out of sync"Merge into one <dependencies> block; manual entries go before <!-- region -->
Module registered by appending to modules.xmlnoisy project-file diff or project-structure driftUse bun build/jps-module.mjs register <path-to-iml> --fix-iml-eof; order is by .iml basename
New files not git addedBuild/tests miss new sourcesgit add new module directory
Skipped plugin-model-toolGenerated XML has stale/missing depsRun ./bazel.cmd run //platform/buildScripts:plugin-model-tool
New source file created as JavaStyle violationAlways use .kt for new code
Removed transitive dep breaks callersAllProductsPackagingTest#targetValidations failsAdd explicit <module name="..."/> to affected modules' plugin XMLs
Cross-plugin subclass of moved classcannot inherit from final class in another pluginMark the class (and subclassable inner classes) open; add visibility="public" to the new module XML; add module dep to the other plugin's IML and plugin.xml
File moved but package declaration not updatedIntelliJProjectPackageNamesTest: "packages [com.old.pkg] are found in the module"Change the package declaration and physically move the file to the matching directory
File moved but physical directory not movedIDE confused, search finds file in two placesAlways move both the file AND update its package declaration
Nullable override mismatch after Java→Kotlin conversion'override' overrides nothingMatch the Kotlin override param types exactly — if the base class uses platform types (unannotated Java), both nullable and non-null work; but if a subclass uses ? the base must too
return@label in val lambdaUnresolved label 'myLabel'Labels only work at the call site. Use .let { ... } chaining instead
Kotlin interface constant access from Javacannot find symbol CONSTANT_NAMEUse explicit class qualification: MyInterface.CONSTANT_NAME
Kotlin-defined fun getXxx() not auto-exposed as propertyUnresolved reference 'xxx'Call with explicit (): element.getXxx()
object : JavaInterface() with parenthesesThis type does not have a constructorInterfaces have no constructor; use object : JavaInterface without ()
Top-level Kotlin function imported from Javacannot find symbol myFunctionFrom Java the class is MyFileKt; use import static com.pkg.MyFileKt.myFunction
Supertype cascade after removing a depCannot access 'com.X.BaseClass' which is a supertype of 'SubClass' — even though BaseClass is never directly importedKotlin needs the full supertype chain of every used type. If you use SubClass (from dep Y) whose supertype BaseClass lives in dep X, removing X breaks compilation even without direct X imports. Fix: move the SubClass usage entirely into the EP implementation in the new module, and expose only a non-X return type (e.g. Language instead of PostCssLanguage) through the EP interface
Class hierarchy access fails after removing a depcannot access BaseClass: class file not foundSubclasses of platform types may transitively require the platform dep; keep it even without direct imports
Broad downstream breakage after large file moveMany unrelated modules fail to compileAfter moving 10+ files, build //plugins/... //contrib/... immediately to find all broken consumers
Two tests.cmd runs at onceA test that passes on its own fails, typically AllProductsPackagingTest#build; log shows Killing process containing subpath 'ide-tests'Run the required tests one at a time — each tests.cmd kills leftover ide-tests processes on startup, including a run still in flight

Examples in the JavaScript Plugin

  • intellij.javascript.regexp (Pattern A) — moved JSRegexpInjector, JSRegexpHost, JSRegExpModifierProvider out of javascript-backend to make the regexp dependency optional.

  • intellij.javascript.backend.css (Pattern B) — introduced JsCssIntegrationHelper EP in javascript-backend, implemented in the new module. Also moved JavaScriptCssUsagesProvider, JQueryCssElementDescriptorProvider, JQueryCssInspectionSuppressor. Downstream fixes required in javascript-ultimate, jsf-core, webpack.

  • intellij.javascript.backend.spellchecker (Pattern A) — moved all spellchecker-related files out of javascript-backend. Required visibility="public" because CoffeeScript plugin (a separate plugin) subclasses JSSpellcheckingStrategy — both the class and its inner tokenizer had to be marked open after Java→Kotlin conversion. javascript-grazie required a manual dep added outside the generated region.

  • intellij.javascript.backend.xml (Pattern A + B) — largest extraction to date (~40 files). Pattern A: moved all JSX/HTML/injection files wholesale. Pattern B: introduced JsXmlContextHelper EP (interface + static dispatch companion) for scattered instanceof XmlTag/XmlElement checks across ~60 core files. Required visibility="public". Downstream fixes required in javascript-ultimate, jsf-core, webpack, flex, vuejs, svelte, react and others. Note: not all XML IML deps could be removed from javascript-backend — some remained due to indirect class hierarchy usage.

Examples in the Vue Plugin

  • intellij.vuejs.backend.css (Pattern B, 3 EPs) — removed all CSS plugin dependencies from intellij.vuejs.backend. Three EPs introduced:
    1. VueCssLanguageProvider — exposes getCssLanguage(), getDefaultStyleLanguage(), getStyleCommenter(). Implemented by VueCssLanguageProviderImpl using CSSLanguage.INSTANCE, PostCssLanguage.INSTANCE, and PostCssCommentProvider. The getDefaultStyleLanguage()/getStyleCommenter() methods were needed because PostCssLanguage extends CssLanguageProperties (supertype cascade): even removing a direct PostCssLanguage reference left the compiler needing intellij.css.common. The fix was to move all PostCssLanguage usage into the EP implementation and return Language (not PostCssLanguage) across the boundary.
    2. VueCssExtractHelper — multi-method lifecycle EP using opaque Any? state. captureUnusedStyles(file): Any? returns a List<CssSelectorSuffix> opaquely; optimizeStyles(file, state: Any?) casts it back with @Suppress("UNCHECKED_CAST") inside the impl. This kept CssSelectorSuffix (from intellij.css.analysis) entirely within the CSS module.
    3. VueCssBindingHelper — single-method EP wrapping CssClassInJSLiteralOrIdentifierReferenceProvider.getClassesFromEmbeddedContent() to remove the intellij.javascript.web.css dep from the host.

评价

还没有评价。欢迎分享你的使用感受。

评论

无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。

还没有评论。来写第一条吧。

提交后会立即公开。请勿填写邮箱、电话等个人信息。URL 最多 2 个。

0 / 1000 字

此表单受 reCAPTCHA 保护,适用 Google 的隐私权政策和服务条款。

同一仓库的技能

actions

免费有简介

实现或修改 IntelliJ `AnAction`,并在 `plugin.xml` 中注册。将显示名称和说明写入消息资源文件,按需设置图标,并通过事件协程作用域启动挂起任务,避免同步处理阻塞 IDE。

  • 为 IDE 菜单添加操作
  • 修改操作注册或图标
  • 整理操作名称和说明
JetBrains/intellij-community2.1万2026年10月29日 更新

bazel-test-migration

免费有简介

将 IntelliJ JPS 测试模块迁移到 Bazel,并排查仅在 Bazel 中出现的失败。根据症状检查依赖、插件、测试数据和 runfiles,重新生成并验证目标。

  • 将 JPS 测试模块迁移到 Bazel
  • 排查仅在 Bazel 中失败的测试
  • 检查测试依赖和数据配置
JetBrains/intellij-community2.1万2026年10月29日 更新

code-style

免费有简介

按 IntelliJ 项目规范编写或审查 Kotlin 和 Java 代码,涵盖语言选择、格式、密封类型、线程注解及控制流异常处理。

  • 按 IntelliJ 规范编写 Kotlin
  • 审查 Java 代码
  • 检查异常处理和密封类型分支
JetBrains/intellij-community2.1万2026年10月29日 更新

commits

免费有简介

本指南帮助您按 IntelliJ 项目规则拆分更改并撰写提交信息:有工单时使用编号和变更说明,无工单时使用标签与子系统名,并在需要时说明原因和决定。

  • 按项目格式编写带工单号的提交信息
  • 整理测试或文档提交并选择合适标签
  • 按规则修改已有提交信息
JetBrains/intellij-community2.1万2026年10月29日 更新

使用多个 Python 类型检查器比较代码片段或文件的类型推断和报错判断,并汇总各工具的结论、耗时与诊断信息,可保存为 Markdown 报告。

  • 比较不同检查器的类型推断
  • 确认代码是否会被判为类型错误
  • 查看多个工具的诊断差异
JetBrains/intellij-community2.1万2026年10月29日 更新

整理 Compose Desktop 界面中的状态和副作用,让可组合函数负责绘制,由展示器或协调器处理应用决策。了解状态归属、Effect、可复用组件接口、延迟列表键、动画和性能,适用于单项界面修改。

  • 分离界面绘制与展示器决策
  • 整理界面状态的归属
  • 检查 Effect 键和协程用法
JetBrains/intellij-community2.1万2026年10月29日 更新

查看 JetBrains 的全部技能

报告此技能的问题