Free Rojo .project.json File Generator & File Watcher — Filtering, SSA, and Automatic Updates

Free Rojo .project.json File Generator & File Watcher — Filtering, SSA, and Automatic Updates

:double_exclamation_mark:Notice

These files have been updated to v1.0.0 and now have their version displayed at the top of each file. If you do not see the version at the top of the file then you are using a pre-v1.0.0 version which may have unintended bugs.

Also the aim for this project is to nested .project.json files within others so rojo v6 or above is needed for the nested files to be used.

Changelog

Update 1.0.0

Update 1.0.0 (Massive Overhall!)

Deprecated

-IncludeSSA has been deprecated so remove the command or you will see errors with the new version

Best Addition

SSA folder matching

Folders/Directories when using the -IncludeSSA tag will inherit the name of the init.lua or init.luau file when filtering using -Filter / -Include and -Exclude. E.g. a folder you named “abc” with a child of init.luau will be filtered as if its called abc.luau and if the child is instead init.lua the folder will be filtered as abc.lua.

Unimportant discrepency you will be unlikely to come across, for this addition init.luau will be used for the filter before init.lua. However to properly use rojo you would only use one of them as a child of the directory.


Other Additions

Seeing Your Version

Added a version comment at the top of the file to keep track. Any previous version without this comment is pre 1.0.0

Ignoring Init Silently

Files and directories named init.luau or init.lua will be ignored and will never be manually built into the final .project.json file generated to avoid rojo errors

Fixed SSA Folders Not Being Fully Included

Fixed an issue where a single SSA Folder would be generated for a particular parent directory. So now it will add all the folder/file regardless of how many matches that is found for the children of a parent directory

File Matching Change for SSA’s (When Recursive is off)

The previous matching algorithm (When -Recursive is off / not included) would traverse the root directory and for each pathway it took it would only terminate until every path had a match. This was not the best way to do it as that is more like an imperfect recursive approach.

The new implementation will still traverse every path but will now treat SSA as files therefore it won’t traverse further if its a file

Example

This is an example of what the project would build if filtering using “*.luau” (src is the root) which matchrd any.luau file

Before:

src
├───abc
│   ├───z
│   │   ├───a
│   │   │    └───init.luau
│   │   └───init.luau
│   ├───f
│   │   └───j
│   │   	  └───init.luau
...

# This will build all `*.luau` matches

After:

src
├───abc
│   ├───z
│   │   └───init.luau
│   ├───f
│   │   └───j
│   │   	  └───init.luau
...

# This new version treats init files as the stopping point and will not go any deeper

Files

Files.zip (5.5 KB)

Closing Statement

If there are any further issues or suggestion or if the dev forum post is not written to an acceptable let me know through the post or by dm for now

Summary

You can also download the files immediately (at the files section near the bottom of the file) as its is somewhat easy to figure out and look at the top of the SyncMapping.ps1 file as it has comments on where to put your code and is faster than reading this comprehensive dev forum post.

Place both files to the same immediate parent e.g. :

YourProject/
├── SyncMappings.ps1
├── GenerateSeparateMappings.ps1
└── src/

Then configure the Invoke-Mappings function for your project.

Run:

<Path To SyncMappings.ps1>

to generate the mappings once.

Or:

<Path To SyncMappings.ps1> -Watch

to keep them synchronized while developing.

You can also add the:

-PathToSync <Path>

parameter to both of the commands to change the path as it defaults to the src under the working directory.

Also -Exclude and -Include/-Filter take arrays of strings or just a single string. Each string in the resulting array (converted to array when passed) follows windows pattern matching which can be found here “about_Wildcards - PowerShell | Microsoft Learn”


Introduction

I wanted to share a small PowerShell-based workflow tool I built for projects that are using Rojo, (Documentation: “https://rojo.space/”) particularly projects that want mostly automated .project.json files generation based on pattern matches.

The goal is to make maintaining separate Rojo project mappings less repetitive and ensuring these nested .project.json files can easily be added to the main one.

What does it do?

The tool consists of two PowerShell scripts:

  • GenerateSeparateMappings.ps1
  • SyncMappings.ps1

GenerateSeparateMappings.ps1 contains the mapping generator itself, while SyncMappings.ps1 provides the configuration and optional file-system watcher.

Together, they allow you to define mappings such as:

src/
├── Services/
│   ├── DataService/
│   │   ├── init.luau
│   │   ├── DataServiceClient.luau
│   │   └── DataServiceServer.luau
│   └── Logger.luau
│
└── Features/
    ├── Inventory/
    │   ├── init.luau
    │   └── ...
    └── ...

and automatically generate separate Rojo project files through pattern matching.

For example:

NestedProjects/
├── services.entry.project.json
├── services.client.project.json
├── services.server.project.json
├── services.shared.project.json
├── features.entry.project.json
├── features.client.project.json
├── features.server.project.json
└── features.shared.project.json

Main features

File filtering

Mappings can use PowerShell wildcard patterns to select files.

For example:

-Filter "*.luau"

or:

-Filter “[Cc][Ll][Ii][Ee][Nn][Tt]”

Multiple patterns can also be supplied:

-Filter @("*.luau", "*Client*")

This makes it possible to create different mappings from the same source directory.


Exclusion filters

You can exclude files using the same wildcard syntax:

-Exclude @(
    "*[Ss][Ee][Rr][Vv][Ee][Rr]*",
    "*[Cc][Ll][Ii][Ee][Nn][Tt]*",
    "*[Ss][Hh][Aa][Rr][Ee][Dd]*"
)

This is useful for creating an “entry” mapping containing everything except specifically classified client/server/shared files.


Folder-aware mappings

The generator can optionally preserve the source folder structure when generating the Rojo tree.

This is controlled by:

-IncludeFolders:$true

Without folder inclusion, matching files are placed directly into the generated tree.

With folder inclusion enabled, the generated project follows the source directory hierarchy.


Single Script Architecture (SSA)

One of the main reasons I built the SSA functionality was to support folders containing an init.luau.

For example:

DataService/
├── DataService.luau
│   └── init.luau
├── DataServiceClient.luau
└── DataServiceServer.luau

The important distinction is that the generator does not simply add every directory containing arbitrary descendants.

When SSA support is enabled, a directory qualifies specifically when it contains:

init.luau

The init.luau file itself is deliberately not emitted as a separate mapped script.

Instead, the directory is represented by its own path in the generated Rojo project.

This allows the folder to act as the SSA entry point while still allowing sibling scripts to be mapped normally.

For example:

Export-RojoMapping `
    -Name "Services" `
    -Root "src\Services" `
    -Filter "*.luau" `
    -Output "NestedProjects/services.entry.project.json" 

Backticks tell powershell thagt the command continue on the next line


Why SSA handling is useful

Without SSA handling, a structure such as:

DataService/
├── init.luau
├── DataServiceClient.luau
└── DataServiceServer.luau

can become awkward to represent when the folder itself is intended to be the script/module entry point.

The generator instead produces the folder mapping while intentionally ignoring the init.luau file as a standalone mapping which would otherwise error when attempting to server with Rojo


Automatic watching

SyncMappings.ps1 can also run as a watcher.

Normally:

.\SyncMappings.ps1

generates the mappings once and exits.

To continuously watch the source directory:

.\SyncMappings.ps1 -Watch

The watcher monitors:

  • File creation
  • File changes
  • File deletion
  • File renaming
  • Directory creation/deletion
  • Directory changes

It also includes a small debounce mechanism so multiple rapid file-system events do not cause a generator invocation for every individual event.

For example, saving several files at once should result in a single regeneration rather than a large number of sequential generations.


Configuration

The main configuration point is the Invoke-Mappings function inside SyncMappings.ps1.

The intention is that you generally only need to modify the mapping definitions in this section.

For example:

Export-RojoMapping `
    -Name "ClientFeatures" `
    -Root "src/Features" `
    -Filter "*[Cc][Ll][Ii][Ee][Nn][Tt]*" `
    -Output "NestedProjects/features.client.project.json" 

You can add as many mappings as your project requires.


Multiple mappings from one source tree

The intended workflow is to define several mappings against the same source directory.

For example:

Entry mapping

Export-RojoMapping `
    -Name "Services" `
    -Root "src\Services" `
    -Filter "*.luau" `
    -Exclude @(
        "*[Ss][Ee][Rr][Vv][Ee][Rr]*",
        "*[Cc][Ll][Ii][Ee][Nn][Tt]*",
        "*[Ss][Hh][Aa][Rr][Ee][Dd]*"
    ) `
    -Output "NestedProjects/services.entry.project.json" `
    -IncludeSSA:$true

Client mapping

Export-RojoMapping `
    -Name "ClientServices" `
    -Root "src\Services" `
    -Filter "*[Cc][Ll][Ii][Ee][Nn][Tt]*" `
    -Output "NestedProjects/services.client.project.json"

Server mapping

Export-RojoMapping `
    -Name "ServerServices" `
    -Root "src\Services" `
    -Filter "*[Ss][Ee][Rr][Vv][Ee][Rr]*" `
    -Output "NestedProjects/services.server.project.json" 

This allows the same source tree to be represented differently depending on where the resulting Rojo project is intended to be used.


Configurable source directory

The watcher by default tracks src relative to the working directory in the terminal which you can find out by executing into your terminal:

pwd

and updated by running (replace the string with its relative or absolute path):

cd "SomeDirectoryPath"

but this can be changed by running:

<Path To SyncMappings.ps1> -PathToSync <SomeDirectoryPath>

or

<Path To SyncMappings.ps1> -PathToSync <SomeDirectoryPath> -Watch

This is useful if your source tree isn’t called src.


Available parameters for Export-RojoMapping

The Export-RojoMapping function currently supports:

Parameter Mandatory Type Description
-Name string Name written into the generated Rojo project
-Root string Source directory/path to search
-Output string Generated path (Note: you must add .project.json at the end of the file name)
-Filter / -Include array of strings or a string File/directory matching patterns
-Exclude array of strings or a string Patterns to exclude
-Recursive switch Search the entire directory tree
-IncludeFolders switch Preserve folder structure in the generated tree
-IgnoreUnknownInstances switch Whether the IgnoreUnknownInstances parameter is true or false
  • -Filter is an alias for -Include.
  • The switch type can be viewed as a less verbose bool instead of writing -Recursive:$true or -Recursive:$false for passing booleans you can instead omit the value set and use -Recursive to pass true and omit it to pass false.

Wildcards

The filters use PowerShell’s -like wildcard matching. Which mostly applies to -Filter -Include or -Exclude.

Documentation for the wildcard syntax can be found in Microsoft’s PowerShell documentation here “about_Wildcards - PowerShell | Microsoft Learn”

Common examples include:

*.luau        #Matches anything that end with .luau
*Client*      #Matches anything that contains the word Client
*Server*      #Matches anything that contains the word Server
*Shared*      #Matches anything that contains the word Shared

Case-insensitive patterns can also be written explicitly, for example:

*[Cc][Ll][Ii][Ee][Nn][Tt]* #Matches anything that contains "client" regardless of case.

Using the astericks matches zero or more characters so allows the client to exist anywhere in the file or with .


Intended workflow

A typical development workflow becomes:

Edit Luau file
      ↓
File system event
      ↓
SyncMappings.ps1 detects change
      ↓
Mappings regenerated
      ↓
Rojo project files stay synchronized

This means the generated .project.json files don’t need to be manually maintained whenever the source structure changes.


Generated files

The generated files are normal Rojo .project.json files.

The generator also uses:

"$ignoreUnknownInstances": true

on the generated root folder so that instances which aren’t explicitly represented by the mapping don’t cause unnecessary Rojo errors.

The generator avoids rewriting a generated file when its resulting JSON has not changed.


Full Examples

Example: Basic mapping

Suppose the source tree is:


The command:

Export-RojoMapping `
    -Name "Services" `
    -Root "src\Services" `
    -Filter "*.luau" `
    -Output "NestedProjects/services.project.json"

generates:

MyProject/
├── NestedProjects/
│   └── services.project.json

with a structure equivalent to:

{
    "name": "Services",
    "tree": {
        "$className": "Folder",
        "$ignoreUnknownInstances": true,
        "Logger": {
            "$path": "../src/Services/Logger.luau"
        },
        "DataService": {
            "$path": "../src/Services/DataService.luau"
        },
        "DebugService": {
            "$path": "../src/Services/DebugService.luau"
        }
    }
}

The exact relative paths are calculated automatically from the location of the generated project file.


Example: Filtering client files

Given:

src/
└── Services/
    ├── DataServiceClient.luau
    ├── DataServiceServer.luau
    ├── InventoryClient.luau
    └── InventoryServer.luau

Run:

Export-RojoMapping `
    -Name "ClientServices" `
    -Root "src\Services" `
    -Filter "*[Cc][Ll][Ii][Ee][Nn][Tt]*" `
    -Output "NestedProjects/services.client.project.json"

The resulting project contains only the client-matching files:

{
    "name": "ClientServices",
    "tree": {
        "$className": "Folder",
        "$ignoreUnknownInstances": true,
        "DataServiceClient": {
            "$path": "../src/Services/DataServiceClient.luau"
        },
        "InventoryClient": {
            "$path": "../src/Services/InventoryClient.luau"
        }
    }
}

Example: Excluding files

Given:

src/
└── Services/
    ├── DataService.luau
    ├── DataServiceClient.luau
    ├── DataServiceServer.luau
    └── DataServiceShared.luau

You can create an entry mapping that excludes the explicitly separated implementations:

Export-RojoMapping `
    -Name "Services" `
    -Root "src\Services" `
    -Filter "*.luau" `
    -Exclude @(
        "*[Cc][Ll][Ii][Ee][Nn][Tt]*",
        "*[Ss][Ee][Rr][Vv][Ee][Rr]*",
        "*[Ss][Hh][Aa][Rr][Ee][Dd]*"
    ) `
    -Output "NestedProjects/services.entry.project.json"

The generated project will contain the remaining matching files.


Example: SSA

Consider this source structure:

src/
└── Services/
    └── DataService/
        ├── init.luau
        ├── DataServiceClient.luau
        └── DataServiceServer.luau

The command:

Export-RojoMapping `
    -Name "Services" `
    -Root "src\Services" `
    -Filter "*.luau" `
    -Output "NestedProjects/services.project.json" 

detects that:

DataService/
└── init.luau

exists.

The important part is that init.luau is not emitted as a separate mapping .

Instead, the directory itself is represented:

{
    "name": "Services",
    "tree": {
        "$className": "Folder",
        "$ignoreUnknownInstances": true,
        "DataService": {
            "$path": "../src/Services/DataService"
        }
    }
}

This allows the SSA directory to be represented by the folder path rather than producing an unnecessary direct init.luau mapping.

If a directory does not contain init.luau , it is not treated as an SSA directory.

For example:

src/
└── Services/
    ├── DataService/
    │   └── init.luau
    │
    └── Utilities/
        └── Helper.luau

Only DataService qualifies as an SSA directory.


Example: SSA with multiple mappings

A more realistic project might look like:

src/
├── Services/
│   ├── DataService/
│   │   ├── init.luau
│   │   ├── DataServiceClient.luau
│   │   └── DataServiceServer.luau
│   │
│   └── Logger.luau
│
└── Features/
    └── Inventory/
        ├── init.luau
        ├── InventoryClient.luau
        └── InventoryServer.luau

You could generate separate projects:

Export-RojoMapping `
    -Name "Services" `
    -Root "src\Services" `
    -Filter "*.luau" `
    -Output "NestedProjects/services.entry.project.json" 

Export-RojoMapping `
    -Name "ClientServices" `
    -Root "src\Services" `
    -Filter "*[Cc][Ll][Ii][Ee][Nn][Tt]*" `
    -Output "NestedProjects/services.client.project.json" 

Export-RojoMapping `
    -Name "ServerServices" `
    -Root "src\Services" `
    -Filter "*[Ss][Ee][Rr][Vv][Ee][Rr]*" `
    -Output "NestedProjects/services.server.project.json" 

which produces:

NestedProjects/
├── services.entry.project.json
├── services.client.project.json
└── services.server.project.json

The source tree remains the single source of truth, while the generated Rojo projects provide the different views required by the project.


Current limitations / things to be aware of

This is primarily a workflow utility rather than a complete project-management system.

A few things to keep in mind:

  • The generated project files are intended to be generated rather than manually edited.
  • Your mapping definitions should live at the top of in SyncMappings.ps1.
  • GenerateSeparateMappings.ps1 should generally be treated as the generator dependency.
  • SSA detection is specifically based on the presence of init.luau file under a folder.
  • The watcher is intended to run in a PowerShell session during development.
  • The generated mappings depend on the source paths being available when the generator runs.

Requirements & Important Notes

PowerShell version

This tool is intended for PowerShell 7 or newer .

Older PowerShell versions, particularly Windows PowerShell 5.1, are not supported/tested and may encounter compatibility issues.

You can upgrade your version to 7 by going to “Install PowerShell 7 on Windows - PowerShell | Microsoft Learn”

You can check your installed version by running these commands in your terminal with:

$PSVersionTable

Or

$PSVersionTable.PSVersion

:warning: Execution Policy

Your PowerShell execution policy may prevent the scripts from running.

The tool requires an execution policy that permits script execution. In particular, RemoteSigned or Unrestricted should be used if your environment is preventing these scripts from running .

Typically your execution policy by default is RemoteSigned which allows files created locally to run.

You can check your current policy by running in terminal:

Get-ExecutionPolicy

If the scripts were downloaded from the internet, Windows may also mark them as coming from an untrusted source. This can cause PowerShell to block them even when the script itself is otherwise valid. So copy the contents of the file and paste them to a new file.


Running the files

There isn’t really an installation process.

Place both files together:

YourProject/
├── SyncMappings.ps1
├── GenerateSeparateMappings.ps1
└── src/

SyncMappings.ps1 automatically loads:

GenerateSeparateMappings.ps1

from the same directory.

Then configure the Invoke-Mappings functions for your project.

Run:

<Path To SyncMappings.ps1>

to generate the mappings once.

Or:

<Path To SyncMappings.ps1> -Watch

to keep them synchronized while developing.

You can also add the:

-PathToSync <Path>

parameter to both of the commands to change the path from the src under the working directory

Files

:double_exclamation_mark: Your main focus is to edit the Invoke-Mappings function at the top of the SyncMappings.ps1 file unless you undertand the code

I’ve attached the two PowerShell files in the form of a .zip. Extract the files and use them as needed:

Files.zip (5.5 KB)

with the structure:

Files.zip/
    ├──GenerateSeparateMappings.ps1
    └──SyncMappings.ps1

The first is the mapping-generation dependency, and the second is the configurable entry point/watchers.


Finally

If there are useful patterns that the current filtering or mapping system doesn’t handle well, I’d be interested in hearing about them.

I am unlikely however to update it unless its an unavoidable error or memory leaks but still mention it if you find anything by replying to this post

Hopefully this tool is useful to anyone who has found themselves repeatedly maintaining large numbers of Rojo project mappings by hand.

2 Likes

Hopefully no one downloaded the previous version (its just less peformant) watcher would update on file content changes so if you did before you saw this post download the new version if I see more issues in these files I will likely make a github repo

I will try and see if it still error-prone before I try to to make the github repo as I found another issue. The download has been updated again. Let me know if there are any further issues.

Have not found any further bugs so I am hoping those were the last

Also let me know if this dev forum post could be more concise or be made more clearer etc.

You can also download the files immediately (unzip them) and look at the top of the SyncMapping.ps1 file as it has comments on where to put your code and faster than this comprehensive dev forum post.

Just Identified an issue where ssa folders would only build one matched file will update the download soon

Update 1.0.0 (Massive Overhall!)

Deprecated

-IncludeSSA has been deprecated so remove the command or you will see errors with the new version

Best Addition

SSA folder matching

Folders/Directories when using the -IncludeSSA tag will inherit the name of the init.lua or init.luau file when filtering using -Filter / -Include and -Exclude. E.g. a folder you named “abc” with a child of init.luau will be filtered as if its called abc.luau and if the child is instead init.lua the folder will be filtered as abc.lua.

Unimportant discrepency you will be unlikely to come across, for this addition init.luau will be used for the filter before init.lua. However to properly use rojo you would only use one of them as a child of the directory.


Other Additions

Seeing Your Version

Added a version comment at the top of the file to keep track. Any previous version without this comment is pre 1.0.0

Ignoring Init Silently

Files and directories named init.luau or init.lua will be ignored and will never be manually built into the final .project.json file generated to avoid rojo errors

Fixed SSA Folders Not Being Fully Included

Fixed an issue where a single SSA Folder would be generated for a particular parent directory. So now it will add all the folder/file regardless of how many matches that is found for the children of a parent directory

File Matching Change for SSA’s (When Recursive is off)

The previous matching algorithm (When -Recursive is off / not included) would traverse the root directory and for each pathway it took it would only terminate until every path had a match. This was not the best way to do it as that is more like an imperfect recursive approach.

The new implementation will still traverse every path but will now treat SSA as files therefore it won’t traverse further if its a file

Example

This is an example of what the project would build if filtering using “*.luau” (src is the root) which matchrd any.luau file

Before:

src
├───abc
│   ├───z
│   │   ├───a
│   │   │    └───init.luau
│   │   └───init.luau
│   ├───f
│   │   └───j
│   │   	  └───init.luau
...

# This will build all `*.luau` matches

After:

src
├───abc
│   ├───z
│   │   └───init.luau
│   ├───f
│   │   └───j
│   │   	  └───init.luau
...

# This new version treats init files as the stopping point and will not go any deeper

Files

Files.zip (5.5 KB)

Closing Statement

If there are any further issues or suggestion or if the dev forum post is not written to an acceptable let me know through the post or by dm for now

1 Like

Hopefully this v1.0.0 update has no further issues and the scripts are now stable.

As I have not seen anymore issues I will no longer monitor this tool