@ldgerrits/rogen 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +7 -0
- package/README.md +33 -0
- package/dist/build.js +228 -0
- package/dist/bundle.cjs +10544 -0
- package/dist/cli.js +111 -0
- package/dist/config.js +349 -0
- package/dist/constants.js +86 -0
- package/dist/execute.js +109 -0
- package/dist/index.js +110 -0
- package/dist/logger.js +104 -0
- package/dist/route.js +345 -0
- package/dist/tree.js +182 -0
- package/dist/types.js +1 -0
- package/package.json +56 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright © 2026 Loek Gerrits
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<div> </div>
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
<img src="docs/public/icon.png" alt="Rogen" width="100" />
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<div align="center">
|
|
8
|
+
<a href="https://rogen-playfully.vercel.app"><strong>Documentation</strong></a>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
Bring feature-based architecture to Roblox.
|
|
14
|
+
Build self-contained systems with client, server, and shared code in one place.
|
|
15
|
+
Rogen routes every file to the correct Roblox service automatically.
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- **Feature Folders:** Store related client, server and shared scripts inside a single folder.
|
|
20
|
+
- **Auto-Routing:** Route files to Roblox services automatically using Rojo or Argon.
|
|
21
|
+
- **Multi-Place Support:** Merge multiple source directories to share core systems across different places.
|
|
22
|
+
- **Environment Tags:** Filter test files and inject mock files at build time using tags.
|
|
23
|
+
- **Custom Pathing:** Control the final output structure using invisible folders and marker files.
|
|
24
|
+
- **Modern Tooling:** Support Luau, roblox-ts, and Darklua pipelines out of the box.
|
|
25
|
+
|
|
26
|
+
## Contributing
|
|
27
|
+
|
|
28
|
+
Pull requests are welcome!
|
|
29
|
+
Read the [contribution guidelines](CONTRIBUTING.md) to set up your local environment, run tests, and submit code changes.
|
|
30
|
+
|
|
31
|
+
## License
|
|
32
|
+
|
|
33
|
+
See the [licence](LICENSE.md) for details.
|
package/dist/build.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { promises as fsp } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import picomatch from "picomatch";
|
|
4
|
+
import { applyCasing, getOrCreateNode, pruneObject, sortObject, findMissingPaths, toPosix, collapseFolders, isInitFile, isValidSource, findExposedDataFiles, } from "./tree.js";
|
|
5
|
+
import { resolveRoute, } from "./route.js";
|
|
6
|
+
import { serviceParents, generateRoutingMaps, defaultConfig, } from "./constants.js";
|
|
7
|
+
const SYSTEM_MARKERS = {
|
|
8
|
+
structure: true,
|
|
9
|
+
verbatim: true,
|
|
10
|
+
unwrap: true,
|
|
11
|
+
};
|
|
12
|
+
function buildSubPath(sourceRel) {
|
|
13
|
+
const segments = sourceRel.split(/[\\/]/).filter(Boolean);
|
|
14
|
+
let rootIndex = 0;
|
|
15
|
+
while (rootIndex < segments.length &&
|
|
16
|
+
(segments[rootIndex] === ".." || segments[rootIndex] === ".")) {
|
|
17
|
+
rootIndex++;
|
|
18
|
+
}
|
|
19
|
+
if (rootIndex + 1 >= segments.length) {
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
return segments.slice(rootIndex + 1).join("/");
|
|
23
|
+
}
|
|
24
|
+
async function listTree(dir) {
|
|
25
|
+
const listings = new Map();
|
|
26
|
+
async function scan(currentDir) {
|
|
27
|
+
try {
|
|
28
|
+
const entries = await fsp.readdir(currentDir, {
|
|
29
|
+
withFileTypes: true,
|
|
30
|
+
});
|
|
31
|
+
listings.set(currentDir, entries);
|
|
32
|
+
const hasInit = entries.some((e) => e.isFile() && isInitFile(e.name));
|
|
33
|
+
const hasSync = entries.some((e) => e.isFile() && e.name === ".sync");
|
|
34
|
+
if (hasInit || hasSync)
|
|
35
|
+
return;
|
|
36
|
+
const subdirs = entries
|
|
37
|
+
.filter((e) => e.isDirectory())
|
|
38
|
+
.map((e) => path.join(currentDir, e.name));
|
|
39
|
+
await Promise.all(subdirs.map(scan));
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error instanceof Error &&
|
|
43
|
+
"code" in error &&
|
|
44
|
+
error.code === "ENOENT")
|
|
45
|
+
return;
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
await scan(dir);
|
|
50
|
+
return listings;
|
|
51
|
+
}
|
|
52
|
+
function compileTagRegexes(activeEnvs) {
|
|
53
|
+
return Array.from(activeEnvs).map((env) => ({
|
|
54
|
+
suffix: new RegExp(`[\\.\\-_\\+]${env}$`, "i"),
|
|
55
|
+
prefix: new RegExp(`^${env}[\\.\\-_\\+]`, "i"),
|
|
56
|
+
middle: new RegExp(`[\\.\\-_\\+]${env}(?=[\\.\\-_\\+])`, "i"),
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
function buildDirectoryMarkers(sourcePath, listings, routingMaps, knownFlags) {
|
|
60
|
+
const directoryMarkers = {};
|
|
61
|
+
for (const [dir, entries] of listings.entries()) {
|
|
62
|
+
const markers = [];
|
|
63
|
+
for (const entry of entries) {
|
|
64
|
+
if (entry.isFile() && entry.name.startsWith(".")) {
|
|
65
|
+
const possibleMarker = entry.name.slice(1).toLowerCase();
|
|
66
|
+
if (possibleMarker in SYSTEM_MARKERS ||
|
|
67
|
+
routingMaps.lowerCaseMap[possibleMarker] ||
|
|
68
|
+
knownFlags.has(possibleMarker)) {
|
|
69
|
+
markers.push(possibleMarker);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (markers.length > 0) {
|
|
74
|
+
let relDir = path.relative(sourcePath, dir);
|
|
75
|
+
relDir = relDir.split(path.sep).join("/");
|
|
76
|
+
directoryMarkers[relDir] = markers;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return directoryMarkers;
|
|
80
|
+
}
|
|
81
|
+
function walkSource(dir, sourcePath, listings, callback) {
|
|
82
|
+
const entries = listings.get(dir);
|
|
83
|
+
if (!entries)
|
|
84
|
+
return;
|
|
85
|
+
// Rojo expects a specific structure for folders with an init.luau file that we cannot deviate from.
|
|
86
|
+
// Because of this, we must return early if an initialization file has been found.
|
|
87
|
+
const syncFile = entries.find((e) => e.isFile() && e.name === ".sync");
|
|
88
|
+
if (syncFile) {
|
|
89
|
+
callback(dir, false, true);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const initFile = entries.find((e) => e.isFile() && isInitFile(e.name));
|
|
93
|
+
if (initFile) {
|
|
94
|
+
callback(path.join(dir, initFile.name), true, false);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
const fullPath = path.join(dir, entry.name);
|
|
99
|
+
if (entry.isDirectory()) {
|
|
100
|
+
walkSource(fullPath, sourcePath, listings, callback);
|
|
101
|
+
}
|
|
102
|
+
else if (isValidSource(entry.name)) {
|
|
103
|
+
callback(fullPath, false, false);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export async function build(targetConfig, baseProjectTree, config, env, sourcePaths, cliArgs, anchor) {
|
|
108
|
+
const modeCopy = { ...targetConfig };
|
|
109
|
+
if (cliArgs.output) {
|
|
110
|
+
modeCopy.output = path.resolve(process.cwd(), cliArgs.output);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
modeCopy.output = path.resolve(anchor, targetConfig.output);
|
|
114
|
+
}
|
|
115
|
+
if (cliArgs.build) {
|
|
116
|
+
modeCopy.build = cliArgs.build;
|
|
117
|
+
}
|
|
118
|
+
const rojoTree = structuredClone(baseProjectTree);
|
|
119
|
+
const mergedTags = { ...(config.tags || {}), ...(modeCopy.tags || {}) };
|
|
120
|
+
const knownTags = new Set(Object.keys(config.tags || {}).map((t) => t.toLowerCase()));
|
|
121
|
+
const activeTags = new Set(Object.keys(mergedTags)
|
|
122
|
+
.filter((t) => mergedTags[t])
|
|
123
|
+
.map((t) => t.toLowerCase()));
|
|
124
|
+
const cliTags = (cliArgs.tag || []).map((t) => t.toLowerCase());
|
|
125
|
+
for (const tag of cliTags) {
|
|
126
|
+
activeTags.add(tag);
|
|
127
|
+
}
|
|
128
|
+
for (const tag of activeTags) {
|
|
129
|
+
if (!knownTags.has(tag)) {
|
|
130
|
+
throw new Error(`active tag "${tag}" is not declared in the root "tags" object.`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const tagRegexes = compileTagRegexes(activeTags);
|
|
134
|
+
const context = {
|
|
135
|
+
source: config.source || structuredClone(defaultConfig.source),
|
|
136
|
+
...modeCopy,
|
|
137
|
+
isTsProject: env.isTsProject || (cliArgs.mode?.includes("ts") ?? false),
|
|
138
|
+
emitLegacyScripts: rojoTree.emitLegacyScripts ?? true,
|
|
139
|
+
name: rojoTree.name,
|
|
140
|
+
routingMaps: generateRoutingMaps(config.aliases || {}),
|
|
141
|
+
verbatim: config.verbatim ?? defaultConfig.verbatim,
|
|
142
|
+
unwrap: config.unwrap ?? defaultConfig.unwrap,
|
|
143
|
+
directoryMarkers: {},
|
|
144
|
+
knownTags,
|
|
145
|
+
activeTags,
|
|
146
|
+
tagRegexes,
|
|
147
|
+
};
|
|
148
|
+
const combinedGlobIgnorePaths = Array.from(new Set([
|
|
149
|
+
...(config.globIgnorePaths || []),
|
|
150
|
+
...(modeCopy.globIgnorePaths || []),
|
|
151
|
+
]));
|
|
152
|
+
const isIgnored = combinedGlobIgnorePaths.length > 0
|
|
153
|
+
? picomatch(combinedGlobIgnorePaths)
|
|
154
|
+
: () => false;
|
|
155
|
+
const nodeOrigins = new WeakMap();
|
|
156
|
+
const collisions = [];
|
|
157
|
+
let fileCount = 0;
|
|
158
|
+
for (const sourcePath of sourcePaths) {
|
|
159
|
+
const relativePath = path.relative(anchor, sourcePath);
|
|
160
|
+
const subPath = buildSubPath(relativePath);
|
|
161
|
+
const listings = await listTree(sourcePath);
|
|
162
|
+
const directoryMarkers = buildDirectoryMarkers(sourcePath, listings, context.routingMaps, knownTags);
|
|
163
|
+
const newContext = {
|
|
164
|
+
...context,
|
|
165
|
+
build: path.join(context.build, subPath),
|
|
166
|
+
directoryMarkers,
|
|
167
|
+
};
|
|
168
|
+
walkSource(sourcePath, sourcePath, listings, (filepath, isInit, isSync) => {
|
|
169
|
+
const relativePath = path.relative(sourcePath, filepath);
|
|
170
|
+
if (relativePath && isIgnored(toPosix(relativePath)))
|
|
171
|
+
return;
|
|
172
|
+
const { targetService, wrapperFolder, virtualParts, nodeName, projectPath, dropped, unwrap, } = resolveRoute(relativePath, isInit, newContext, isSync);
|
|
173
|
+
if (dropped)
|
|
174
|
+
return;
|
|
175
|
+
fileCount++;
|
|
176
|
+
let current = rojoTree.tree;
|
|
177
|
+
if (serviceParents[targetService]) {
|
|
178
|
+
current = getOrCreateNode(current, serviceParents[targetService]);
|
|
179
|
+
}
|
|
180
|
+
current = getOrCreateNode(current, targetService);
|
|
181
|
+
if (!unwrap) {
|
|
182
|
+
current = getOrCreateNode(current, applyCasing(wrapperFolder, config.casing), "Folder");
|
|
183
|
+
}
|
|
184
|
+
for (const part of virtualParts) {
|
|
185
|
+
current = getOrCreateNode(current, part, "Folder");
|
|
186
|
+
}
|
|
187
|
+
const existingNodeRaw = current[nodeName];
|
|
188
|
+
if (existingNodeRaw && existingNodeRaw.$path) {
|
|
189
|
+
const origin = nodeOrigins.get(existingNodeRaw);
|
|
190
|
+
if (origin && origin.sourcePath === sourcePath) {
|
|
191
|
+
collisions.push(`Name collision: "${origin.filepath}" and "${relativePath}" both map to the node "${nodeName}".`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const existingNode = existingNodeRaw || {};
|
|
195
|
+
const newNode = {
|
|
196
|
+
...existingNode,
|
|
197
|
+
$path: projectPath,
|
|
198
|
+
};
|
|
199
|
+
if (newNode.$className === "Folder") {
|
|
200
|
+
delete newNode.$className;
|
|
201
|
+
}
|
|
202
|
+
current[nodeName] = newNode;
|
|
203
|
+
nodeOrigins.set(newNode, {
|
|
204
|
+
sourcePath,
|
|
205
|
+
filepath: relativePath,
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
const outputDir = path.dirname(modeCopy.output);
|
|
210
|
+
const removed = [];
|
|
211
|
+
const prunedTree = pruneObject(rojoTree.tree, context.build, outputDir, removed);
|
|
212
|
+
collapseFolders(prunedTree, context.build, outputDir);
|
|
213
|
+
rojoTree.tree = prunedTree;
|
|
214
|
+
const sortedTree = sortObject(rojoTree);
|
|
215
|
+
const missingPaths = findMissingPaths(sortedTree.tree, context.build, outputDir);
|
|
216
|
+
const exposedDataFiles = findExposedDataFiles(sortedTree.tree);
|
|
217
|
+
return {
|
|
218
|
+
output: modeCopy.output,
|
|
219
|
+
tree: sortedTree,
|
|
220
|
+
missingPaths,
|
|
221
|
+
removed,
|
|
222
|
+
collisions,
|
|
223
|
+
exposedDataFiles,
|
|
224
|
+
name: context.name,
|
|
225
|
+
buildDir: context.build,
|
|
226
|
+
fileCount,
|
|
227
|
+
};
|
|
228
|
+
}
|