Free Rojo .project.json File Generator & File Watcher — Filtering, SSA, and Automatic Updates
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.ps1SyncMappings.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 |
-Filteris an alias for-Include.- The
switchtype can be viewed as a less verboseboolinstead of writing-Recursive:$trueor-Recursive:$falsefor passing booleans you can instead omit the value set and use-Recursiveto 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.ps1should generally be treated as the generator dependency.- SSA detection is specifically based on the presence of
init.luaufile 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
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
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.