- name
- module-set-pluginization
- description
- Convert Product DSL module sets into bundled wrapper plugins.
# Module Set Pluginization
Use this workflow when a group of platform modules that currently live inside an aggregate Product DSL `ModuleSet` (typically `essential`, `essentialMinimal`, or `ide.common`) should become a standalone bundled plugin wrapper.
**New canonical pattern:** the wrapper is a **hand-written** JPS module placed **next to the feature modules** — not auto-generated under `community/module-set-plugins/generated/`. Auto-generation is being phased out; legacy wrappers stay in place until migrated. Reference example: `community/platform/navbar/plugin/` (IJPL-245430, commit `9d61ae6803221`).
## Before Editing
- Look at `community/platform/navbar/plugin/` as the reference: a sibling `plugin/` directory of the feature modules with its own `.iml`, `resources/META-INF/plugin.xml`, and `plugin-content.yaml`.
- Decide whether the modules should still be inlined inside a parent module set in any product. Pluginized wrappers usually stop being emitted through the aggregate `intellij.moduleSets.<name>.xml` and become a bundled plugin instead.
- For library modules with only a small closed consumer set, do **not** create or keep a shared wrapper just to make the library available. Prefer `visibility="private"` on the library descriptor and register the module as unnamed `<content>` in each consuming plugin, so each copy lives in that plugin's implicit private namespace.
## Create the Wrapper Plugin Module
Create a new JPS module at `<feature-root>/plugin/`, e.g. `community/platform/<feature>/plugin/`:
- `intellij.platform.<feature>.plugin.iml` under `community/platform/` — resources-only `JAVA_MODULE` inheriting JDK; for example:
```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" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
```
After creating the `.iml`, register it with the project-file helper instead of editing `modules.xml` by hand:
```bash
bun build/jps-module.mjs register community/platform/<feature>/plugin/intellij.platform.<feature>.plugin.iml --fix-iml-eof
```
- `resources/META-INF/plugin.xml` — a minimal `<idea-plugin>` listing the content modules. Mirror the wrapper shape from navbar, but use the canonical platform plugin id for new wrappers (replace `FEATURE` with the actual short name; the description must be at least 40 characters):
```text
<idea-plugin>
<id>intellij.platform.FEATURE.plugin</id>
<name>FEATURE</name>
<description>Provides FEATURE platform modules for the IDE.</description>
<vendor>JetBrains</vendor>
<content namespace="jetbrains">
<module name="intellij.platform.FEATURE" loading="required"/>
<module name="intellij.platform.FEATURE.backend"/>
<module name="intellij.platform.FEATURE.frontend"/>
<module name="intellij.platform.FEATURE.monolith"/>
</content>
</idea-plugin>
```
**Naming:** for new hand-written wrappers, use the JPS wrapper module name as the plugin **id** whenever possible: `intellij.platform.FEATURE.plugin` under `community/platform/` (for example, `intellij.platform.tasks.plugin`) or `intellij.FEATURE.plugin` outside the `platform/` subtree. This keeps bundled plugin ids and bundled plugin module names aligned in Product DSL, logs, and dependency declarations. Some older hand-written wrappers still use short ids such as `intellij.navbar.plugin`; do not copy that form for new platform wrappers. Do **not** reuse the legacy `com.intellij.moduleSet.FEATURE` prefix — that is the auto-generated wrapper convention being phased out, kept only on existing wrappers under `community/module-set-plugins/generated/`. The plugin **name** is a short human-readable label (e.g. `Tasks Platform`).
No wrapper-specific descriptor marker is needed. Add the JPS wrapper module name to `HAND_WRITTEN_MODULE_SET_PLUGIN_MODULES` in `community/platform/build-scripts/product-dsl/src/ModuleSetPlugins.kt`. Generation intersects that build-time registry with each product's `ProductModulesLayout.bundledPluginModules`, avoiding changes to the runtime plugin descriptor model while keeping complete generated dependencies for wrapper content.
Mark **only the shared/anchor module** (the one that loads in every product mode) as `loading="required"` — `intellij.platform.FEATURE` in the example. Plugin-XML inspection requires at least one required/embedded/required-if-available content module per wrapper, so the anchor satisfies that. Do **not** add `loading="required"` to backend / frontend / monolith modules: in frontend product mode (JetBrains Client) the platform's backend module is unavailable, so a required `FEATURE.backend` excludes itself and takes the whole wrapper plugin down — producing a confusing cascade like "Plugin 'FEATURE' depends on plugin 'IDEA CORE' which failed to load".
- `plugin-content.yaml` — list the JAR layout. Mirror the navbar example:
```yaml
- name: lib/modules/intellij.platform.<feature>.backend.jar
contentModules:
- name: intellij.platform.<feature>.backend
- name: lib/modules/intellij.platform.<feature>.frontend.jar
contentModules:
- name: intellij.platform.<feature>.frontend
- name: lib/modules/intellij.platform.<feature>.jar
contentModules:
- name: intellij.platform.<feature>
- name: lib/modules/intellij.platform.<feature>.monolith.jar
contentModules:
- name: intellij.platform.<feature>.monolith
- name: lib/platform-<feature>-plugin.jar
modules:
- name: intellij.platform.<feature>.plugin
```
- Suppress `SplitModeMixedDependencies` and `PluginXmlPluginLogo` in `plugin.xml` only if the inspection actually flags the wrapper.
## Update the Product DSL
Remove the now-pluginized modules from the aggregate `ModuleSet`:
- In `community/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/CommunityModuleSets.kt` (and any product-specific layout file that listed them), delete the `module("intellij.platform.<feature>.…")` / `embeddedModule(...)` calls that the wrapper now owns.
- Add the wrapper JPS module name to `DEFAULT_BUNDLED_PLUGINS` in `community/platform/build-scripts/src/org/jetbrains/intellij/build/productLayout/ProductModulesLayout.kt`, e.g. `"intellij.platform.<feature>.plugin"`.
- Add the wrapper JPS module name to `HAND_WRITTEN_MODULE_SET_PLUGIN_MODULES` in `community/platform/build-scripts/product-dsl/src/ModuleSetPlugins.kt`.
- Check products that override or reset default bundled plugins (Rider's `ReSharperExternalProductProperties`, Gateway, JetBrains Client product-modules XML under `remote-dev/`, etc.) and add the wrapper module name where appropriate.
Do **not** create a new `plugin("<feature>")` DSL block under the old generator path. The wrapper exists as a standalone JPS module instead.
## Re-trim Content Module Descriptors
Pluginization is a good moment to verify that each former direct module declares only the dependencies it really needs:
- Trim `intellij.platform.<feature>.<backend|frontend|monolith>.xml` so it declares the actual runtime dependencies (e.g. `intellij.platform.backend`, `intellij.platform.frontend`, the shared `intellij.platform.<feature>` module).
- If any extension point was renamed/qualified, grep the repo for the old short name and update every `<extensionPoint …>` reference accordingly.
- If a downstream consumer used a module that the new wrapper now hides, switch the consumer's descriptor to depend on the wrapper plugin: `<plugin id="intellij.platform.<feature>.plugin"/>` instead of `<module name="intellij.platform.<feature>.frontend"/>`. Existing references to legacy `com.intellij.moduleSet.<feature>` ids stay valid for the auto-generated wrappers that have not yet been migrated.
## Re-run Code Generation
After the source edits, run the generators so the platform descriptors, Bazel files, and `.idea/modules.xml` are updated to match:
```bash
bazel run //platform/buildScripts:plugin-model-tool
./build/jpsModelToBazel.cmd
```
Expected diffs:
- `community/platform/platform-resources/generated/META-INF/intellij.moduleSets.essential.xml`, `…ide.common.xml`, and `licenseCommon/generated/META-INF/intellij.moduleSets.ide.ultimate.xml` no longer list the pluginized modules.
- `community/.idea/modules.xml` and `.idea/modules.xml` gain the new `intellij.platform.<feature>.plugin.iml` module in helper-generated canonical order.
- `build/bazel-generated-file-list.txt` and `community/build/bazel-generated-file-list.txt` gain the new plugin directory.
- Product content snapshots (`build/expected/ultimate-content-platform.yaml`, `dbe/build/datagrip-content.yaml`, etc.) gain the wrapper plugin entry.
- `tests/ideaProjectStructure/testResources/com/intellij/ideaProjectStructure/fast/available-in-idea-free-mode.txt` gains the new module if it is free-mode available.
- Existing wrappers under `community/module-set-plugins/generated/intellij.moduleSet.plugin.<name>/` are unrelated — do not touch them unless you are migrating one to the new pattern.
## Fix Ordering Bugs When Validation Runs Before Generation
Hand-written wrappers usually do not hit the pre-generation-on-disk ordering trap, since the descriptor exists in source from the first build. The cache path remains relevant only for the legacy generated wrappers:
- `PluginContentCache` injects `PluginContentInfo` objects for already-generated wrappers during the model-building stage. If you migrate one of those to the new pattern, drop the corresponding cache entry along with the generated directory.
## Downstream Test Runtime Dependencies
Pluginization can break narrow test modules even when product packaging tests pass. Test runtime classpaths may still include a plugin module whose extracted platform dependency is no longer embedded. Catch this from `idea.log`, not from test assertion text alone.
- Inspect `Plugin set resolution` and `Problems found loading plugins` in local or TeamCity test logs.
- Treat messages such as missing `intellij.platform.structureView.impl`, `intellij.platform.execution.serviceView`, or `intellij.platform.navbar.frontend` as a missing wrapper-plugin runtime dependency (`intellij.moduleSet.plugin.structureView`, `intellij.moduleSet.plugin.servicesView`, or the new `intellij.platform.<feature>.plugin`).
- Add the wrapper dep only to the affected test module with `scope="RUNTIME"`. Do not add it to broad shared modules such as `intellij.platform.testFramework` or aggregate/global test infrastructure.
- After changing `.iml`, run `./build/jpsModelToBazel.cmd` and verify the generated `BUILD.bazel` runtime deps are updated.
- For TeamCity, read the published test log artifact when needed: `/app/rest/builds/id:<buildId>/artifacts/content/testlog.zip!.../idea.log`.
## Pre-TeamCity Canary Suite
Run this suite before relying on TeamCity after pluginization or plugin dependency changes. Run the listed commands in parallel when resources allow; the order is diagnostic priority, not execution order. Fix failures in checks 1-6 before interpreting downstream canary failures.
1. Embedded dependency closure:
`./bazel.cmd run //platform/buildScripts:plugin-model-tool -- --json='{"filter":"embeddedDependencyClosure","pluginSourceOnly":true}'`
2. Product DSL module-set validation:
`./tests.cmd --module intellij.platform.buildScripts.productDsl.tests --test org.jetbrains.intellij.build.productLayout.validator.ProductModuleSetValidatorTest`
3. Fast project structure/root packages:
`./tests.cmd --module intellij.projectStructureTests --test com.intellij.ideaProjectStructure.fast.IntelliJProjectPackageNamesTest`
4. Product packaging baseline:
`./tests.cmd --module intellij.idea.ultimate.build.tests --test com.intellij.idea.ultimate.build.smokeTests.AllProductsPackagingTest`
5. CLion packaging baseline:
`./tests.cmd --module intellij.clion.build.tests --test org.jetbrains.intellij.build.clion.CLionPackagingTest`
6. Rider packaging baseline:
`./tests.cmd --module intellij.rider.build.tests --test com.jetbrains.rider.build.RiderPackagingTest`
7. Database and SQL plugin loading:
`./tests.cmd --module intellij.database.tests --test com.intellij.database.DataGripLiteSuite`
`./tests.cmd --module intellij.database.sql.tests --test com.intellij.sql.SqlFileStructureViewTest`
`./tests.cmd --module intellij.database.sql.tests --test com.intellij.sql.editor.SqlMultiLineTodoTest`
8. Code Server DB plugin loading:
`./tests.cmd --module intellij.codeServer.db.sql.tests --test com.intellij.database.SqlCompletionTest`
`./tests.cmd --module intellij.codeServer.db.csv.datagrid.tests --test com.intellij.codeServer.db.csv.datagrid.tests.CsvCoreGridTest`
9. Package Checker plugin loading:
`./tests.cmd --module intellij.packageChecker.tests --test com.intellij.packageChecker.model.VulnerabilitiesRepositoryServerTest`
10. Grazie and spellchecker plugin loading:
`./community/tests.cmd --module intellij.grazie.tests --test com.intellij.grazie.spellchecker.inspector.SuggestionTest`
`./community/tests.cmd --module intellij.grazie.tests --test com.intellij.grazie.spellchecker.inspection.CommentsWithMistakesInspectionTest`
11. Shell plus Markdown plugin loading:
`./community/tests.cmd --module intellij.sh.tests --test com.intellij.sh.highlighting.ShHighlightUsagesInMarkdownTest`
12. Ruby plugin loading:
`./tests.cmd --module intellij.ruby.tests --test org.jetbrains.plugins.ruby.gem.GemsProviderTest`
`./tests.cmd --module intellij.ruby.tests --test org.jetbrains.plugins.ruby.ruby.psi.RubyHeredocInjectorTest#testSqlHeredoc`
13. RustRover structure and SQL loading:
`./tests.cmd --module intellij.rustrover.core.test --test org.rust.ide.structure.RsStructureViewModelTest`
`./tests.cmd --module intellij.rustrover.sql.tests --test com.jetbrains.rust.sql.SqlInjectorTest`
14. ReSharper external services/DataGrip bridge:
`./tests.cmd --module intellij.resharper.external.services.test.cases.rd --test com.jetbrains.resharper.external.services.test.cases.ReSharperDataGripRdTest`
15. AI Assistant plugin loading:
`./tests.cmd --module intellij.ml.llm.chat.tests --test com.intellij.ml.llm.core.chat.ui.chat.navigation.FilePathMatcherTest`
`./tests.cmd --module intellij.ml.llm.completion.tests --test com.intellij.ml.llm.completion.cloud.context.mainEditor.RecentLocationsContextCollectorTest`
`./tests.cmd --module intellij.ml.llm.devkit.tests --test com.intellij.ml.llm.devkit.context.DevKitChatContextProviderTest`
`./tests.cmd --module intellij.ml.llm.java.embeddings.tests --test com.intellij.ml.llm.java.embeddings.JavaEmbeddingParsingTest`
`./tests.cmd --module intellij.ml.llm.java.inlinePromptDetector.tests --test com.intellij.ml.llm.java.inlinePromptDetector.JavaInlinePromptHighlightingTest`
`./tests.cmd --module intellij.ml.llm.nextEdits.tests --test 'com.intellij.ml.llm.nextEdits.tests.history.NextEditFileHistoryTest;com.intellij.ml.llm.nextEdits.tests.diff.CheckFileRangesSyntaxCorrectTest'`
`./tests.cmd --module intellij.ml.llm.ruby.test --test com.intellij.ml.llm.ruby.testGeneration.RubyLLMGenerateTestsSupportPromptTest`
`./tests.cmd --module intellij.ml.llm.ruby.test --test com.intellij.ml.llm.ruby.testGeneration.rails.RailsGenerateTestsPromptTest`
`./tests.cmd --module intellij.ml.llm.sql.tests --test com.intellij.ml.llm.sql.SqlObjectChatInputTest`
## Verification
- Run `lint_files()` on every changed Kotlin source and test file.
- Run the Pre-TeamCity Canary Suite after generated files and narrow test dependencies are updated.
- Keep CLion and Rider packaging tests in the standard suite: wrapper plugins and `DEFAULT_BUNDLED_PLUGINS` changes can affect those product baselines even when `AllProductsPackagingTest` is green.
- Run at least one representative test from each affected test module via `./tests.cmd --module <module> --test <FQN or FQN#method>`. Prefer a small smoke test that exercises plugin loading without external fixtures.
- If the original CI test needs unavailable data or services, run a nearby smoke test and document the local blocker. Confirm `idea.log` no longer reports the original missing wrapper plugin.
- Re-render the skill mirrors:
```bash
node community/.ai/render-guides.mjs
```
Auf GitHub ansehen