What is the best way to handle libraries for the new language

I’m adding support for the Luau language, and started to “dig” into the modules.
There are several package managers for the language, and the most basic thing I want to support is to detect package manager configuration and exclude the Packages folder and also use the list of excluded folders to configure LSP. For the simplicity I handle only situations where project is a single module now.

I guess there is a lot of way to achieve that, but I wanted to create a LibraryEntity and add an excluded roots to it.

I create a library and then add it to the module as a dependency (assume all the checks to avoid adding library multiple times are done as well). The code now runs as a ProjectActivity

val libraryEntitySource =
    LegacyBridgeJpsEntitySourceFactory.getInstance(project)
        // TODO (AleksandrSl 31/08/2025): It's null in the example, should it be?
        .createEntitySourceForProjectLibrary(null)
val library = builder.addEntity(
    LibraryEntity(
        name = libraryName,
        tableId = LibraryTableId.ProjectLibraryTableId,
        roots = listOf(LibraryRoot(packagesDirUrl, LibraryRootTypeId.SOURCES)),
        entitySource = libraryEntitySource
    ) {
        this.excludedRoots = listOf(ExcludeUrlEntity(packagesDirUrl, libraryEntitySource))
    }
)
libraryId = library.symbolicId

builder.modifyModuleEntity(moduleEntity) {
        dependencies += LibraryDependency(libraryId, false, DependencyScope.COMPILE)
}

Doing this I encountered several things

  1. If my project had an .idea folder already this code has no effect. I see it run using a debugger, and the library is created and added to the dependencies, but no configs are created/changed.
  2. If I remove the .idea folder, do File > Open on the same folder, and then close the project I see the new module being persisted and libraries created (I guess until you close the project all this data is in the memory)

The files created look like this

Module

<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
  <component name="NewModuleRootManager">
    <content url="file://$MODULE_DIR$">
      <excludeFolder url="file://$MODULE_DIR$/.tmp" />
      <excludeFolder url="file://$MODULE_DIR$/temp" />
      <excludeFolder url="file://$MODULE_DIR$/tmp" />
    </content>
    <orderEntry type="inheritedJdk" />
    <orderEntry type="sourceFolder" forTests="false" />
    <orderEntry type="library" name="Luau Packages" level="project" />
  </component>
</module>

Library

<component name="libraryTable">
  <library name="Luau Packages">
    <CLASSES />
    <JAVADOC />
    <SOURCES>
      <root url="file://$PROJECT_DIR$/Packages" />
    </SOURCES>
    <excluded>
      <root url="file://$PROJECT_DIR$/Packages" />
    </excluded>
  </library>
</component>

However the folder is still not excluded :smile:

So the questions would be:

  1. Is this a meaningful approach?
  2. Is there a way to add a library to the already existing project? (e.g. if people install the new version of my plugin I want this functionality to work without destroying the old modules)
  3. Should I excluded the folder in the library, or just do that in the module itself? Since this exclusion is specific to a library I thought the first approach is the correct one.
  4. Should the whole folder be a single library? In WS the whole node_modules folder is considered a single library. However, in a gradle project every dependency is considered a separate library. Is there a correct way to do this, or it depends on the purpose?
  5. Since the Packages folder could be created later, does it make sense to check for it’s creation in AsyncFileListener and add to excluded roots or there is a better way?

I also experimented with creating my own modules for a monorepo project, but ended up with the same thing - modules not being persisted.

I tried excluding the Packages from the source root

builder.modifyContentRootEntity(contentRootEntity) {
  excludedUrls += ExcludeUrlEntity(packagesDirUrl, it.entitySource)
}

This seems to have an effect, since I see the directory being colored as excluded for a couple of seconds, but then it reverts to normal.

So it looks like even the way I’m adding exclusions works only for the first time opening a project, but doesn’t work for already existing ones.

Probably I need to use replaceBySource for these kind of updates, but haven’t figured out how to correctly apply it yet

Everything works if I delay my updates for 10 seconds. Now I wonder is there a way to not loose the updates or wait until they could be done safely?

I’m not sure what exactly overwrites them, maybe the cache stuff does that
2025-09-03 20:12:06,108 [ 10971] INFO - #c.i.w.i.i.j.s.DelayedProjectSynchronizer$Util - Workspace model loaded from cache

Unfortunately we have no such guides and all examples live in huge plugins such as Java, Python and JavaScript. But what I can do is to give you some basic ideas:

  1. You most likely need Workspace Model contributors. See inheritors of WorkspaceFileIndexContributor
  2. Library is an entity in this model that represents abstract library for a language
  3. There must be a source of truth (better be single) to find libraries defined in code, configs, etc of the project
  4. You must rely on PULL semantics, where you never ever create libraries as effects but the platform calls your contributors when needed. It is a MUST to have a consistent world state.

The code now runs as a ProjectActivity

So this is rather incorrect approach, it will lead to conflicts

As a simplified example you can take a look at GraphQL plugin libraries

1 Like

Good, I’ll study those, many thing I saw including Nuxt plugin also use the internal codegen to have custom entities so it’s sometimes hard to tell what is the way :sweat_smile:

As for the ProjectActivity, I took this from the Dart plugin which is inside intellij-plugins :joy_cat:

We at the moment trying to release code generator to public.

Dart is a rather old plugin, probably not migrated yet

1 Like

What do you mean exactly by “create libraries” here? The GraphQL Plugin you linked is creating GraphQLLibraryEntity objects during startup as a ProjectActivity.

class GraphQLStartupActivity : ProjectActivity {
  override suspend fun execute(project: Project) {
...
      libraryManager.syncLibraries() // calls new GraphQLLibraryEntity() eventually