@ldgerrits/rogen 1.4.1 → 1.4.3
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/README.md +8 -6
- package/dist/build.js +30 -6
- package/dist/bundle.cjs +140 -52
- package/dist/cli.js +6 -2
- package/dist/constants.js +15 -1
- package/dist/execute.js +33 -16
- package/dist/index.js +7 -2
- package/dist/tree.js +66 -25
- package/package.json +12 -2
package/README.md
CHANGED
|
@@ -5,20 +5,22 @@
|
|
|
5
5
|
</div>
|
|
6
6
|
|
|
7
7
|
<div align="center">
|
|
8
|
-
<a href="https://rogen-playfully.vercel.app"><strong>
|
|
8
|
+
<a href="https://rogen-playfully.vercel.app"><strong>documentation</strong></a>
|
|
9
9
|
</div>
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
<div align="center">
|
|
14
|
+
A CLI tool that enables feature-based architecture for Roblox
|
|
15
|
+
</div>
|
|
14
16
|
|
|
15
17
|
## Features
|
|
16
18
|
|
|
17
|
-
- **Feature Folders:**
|
|
18
|
-
- **Auto-Routing:**
|
|
19
|
+
- **Feature Folders:** Keep related client, server, and shared code inside a single folder.
|
|
20
|
+
- **Auto-Routing:** Map files to Roblox services automatically using Rojo or Argon.
|
|
19
21
|
- **Multi-Place Support:** Merge multiple source directories to share core systems across different places.
|
|
20
|
-
- **Environment Tags:** Filter test files and inject mock
|
|
21
|
-
- **
|
|
22
|
+
- **Environment Tags:** Filter test files and inject mock dependencies at build time using tags.
|
|
23
|
+
- **Granular Control:** Shape the project tree using invisible folders, hoisting, and marker files.
|
|
22
24
|
- **Modern Tooling:** Support Luau, roblox-ts, and Darklua pipelines out of the box.
|
|
23
25
|
|
|
24
26
|
## Contributing
|
package/dist/build.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from "path";
|
|
|
3
3
|
import picomatch from "picomatch";
|
|
4
4
|
import { applyCasing, getOrCreateNode, pruneObject, sortObject, findMissingPaths, toPosix, collapseFolders, isInitFile, isValidSource, findExposedDataFiles, } from "./tree.js";
|
|
5
5
|
import { resolveRoute, } from "./route.js";
|
|
6
|
-
import { serviceParents, generateRoutingMaps, defaultConfig, } from "./constants.js";
|
|
6
|
+
import { serviceParents, generateRoutingMaps, defaultConfig, services, } from "./constants.js";
|
|
7
7
|
const SYSTEM_MARKERS = {
|
|
8
8
|
structure: true,
|
|
9
9
|
verbatim: true,
|
|
@@ -116,6 +116,25 @@ export async function build(targetConfig, baseProjectTree, config, env, sourcePa
|
|
|
116
116
|
modeCopy.build = cliArgs.build;
|
|
117
117
|
}
|
|
118
118
|
const rojoTree = structuredClone(baseProjectTree);
|
|
119
|
+
// Pre-populate all routing containers to prevent desync issues
|
|
120
|
+
for (const targetService of Object.values(services)) {
|
|
121
|
+
let current = rojoTree.tree;
|
|
122
|
+
if (serviceParents[targetService]) {
|
|
123
|
+
current = getOrCreateNode(current, serviceParents[targetService]);
|
|
124
|
+
}
|
|
125
|
+
getOrCreateNode(current, targetService);
|
|
126
|
+
}
|
|
127
|
+
if (config.aliases) {
|
|
128
|
+
for (const aliasPath of Object.values(config.aliases)) {
|
|
129
|
+
if (typeof aliasPath === "string") {
|
|
130
|
+
const parts = aliasPath.split(".");
|
|
131
|
+
let current = rojoTree.tree;
|
|
132
|
+
for (const part of parts) {
|
|
133
|
+
current = getOrCreateNode(current, part);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
119
138
|
const mergedTags = { ...(config.tags || {}), ...(modeCopy.tags || {}) };
|
|
120
139
|
const knownTags = new Set(Object.keys(config.tags || {}).map((t) => t.toLowerCase()));
|
|
121
140
|
const activeTags = new Set(Object.keys(mergedTags)
|
|
@@ -146,6 +165,7 @@ export async function build(targetConfig, baseProjectTree, config, env, sourcePa
|
|
|
146
165
|
tagRegexes,
|
|
147
166
|
};
|
|
148
167
|
const combinedGlobIgnorePaths = Array.from(new Set([
|
|
168
|
+
...(rojoTree.globIgnorePaths || []),
|
|
149
169
|
...(config.globIgnorePaths || []),
|
|
150
170
|
...(modeCopy.globIgnorePaths || []),
|
|
151
171
|
]));
|
|
@@ -167,10 +187,8 @@ export async function build(targetConfig, baseProjectTree, config, env, sourcePa
|
|
|
167
187
|
};
|
|
168
188
|
walkSource(sourcePath, sourcePath, listings, (filepath, isInit, isSync) => {
|
|
169
189
|
const relativePath = path.relative(sourcePath, filepath);
|
|
170
|
-
if (relativePath && isIgnored(toPosix(relativePath)))
|
|
171
|
-
return;
|
|
172
190
|
const { targetService, wrapperFolder, virtualParts, nodeName, projectPath, dropped, unwrap, } = resolveRoute(relativePath, isInit, newContext, isSync);
|
|
173
|
-
if (dropped)
|
|
191
|
+
if (dropped || (projectPath && isIgnored(toPosix(projectPath))))
|
|
174
192
|
return;
|
|
175
193
|
fileCount++;
|
|
176
194
|
let current = rojoTree.tree;
|
|
@@ -194,10 +212,13 @@ export async function build(targetConfig, baseProjectTree, config, env, sourcePa
|
|
|
194
212
|
const existingNode = existingNodeRaw || {};
|
|
195
213
|
const newNode = {
|
|
196
214
|
...existingNode,
|
|
197
|
-
$path: projectPath,
|
|
215
|
+
$path: { optional: projectPath },
|
|
198
216
|
};
|
|
199
217
|
if (newNode.$className === "Folder") {
|
|
200
218
|
delete newNode.$className;
|
|
219
|
+
if (newNode.$ignoreUnknownInstances === false) {
|
|
220
|
+
delete newNode.$ignoreUnknownInstances;
|
|
221
|
+
}
|
|
201
222
|
}
|
|
202
223
|
current[nodeName] = newNode;
|
|
203
224
|
nodeOrigins.set(newNode, {
|
|
@@ -209,8 +230,11 @@ export async function build(targetConfig, baseProjectTree, config, env, sourcePa
|
|
|
209
230
|
const outputDir = path.dirname(modeCopy.output);
|
|
210
231
|
const removed = [];
|
|
211
232
|
const prunedTree = pruneObject(rojoTree.tree, context.build, outputDir, removed);
|
|
212
|
-
collapseFolders(prunedTree, context.build, outputDir);
|
|
233
|
+
collapseFolders(prunedTree, context.build, outputDir, isIgnored);
|
|
213
234
|
rojoTree.tree = prunedTree;
|
|
235
|
+
if (combinedGlobIgnorePaths.length > 0) {
|
|
236
|
+
rojoTree.globIgnorePaths = combinedGlobIgnorePaths;
|
|
237
|
+
}
|
|
214
238
|
const sortedTree = sortObject(rojoTree);
|
|
215
239
|
const missingPaths = findMissingPaths(sortedTree.tree, context.build, outputDir);
|
|
216
240
|
const exposedDataFiles = findExposedDataFiles(sortedTree.tree);
|
package/dist/bundle.cjs
CHANGED
|
@@ -8858,14 +8858,15 @@ var import_chokidar = __toESM(require_chokidar(), 1);
|
|
|
8858
8858
|
var import_util = require("util");
|
|
8859
8859
|
function printHelp(logger) {
|
|
8860
8860
|
const helpText = `
|
|
8861
|
-
Rogen - Feature-based
|
|
8861
|
+
Rogen - Feature-based architecture for Roblox
|
|
8862
8862
|
|
|
8863
8863
|
Usage:
|
|
8864
8864
|
rogen [command] [options]
|
|
8865
8865
|
|
|
8866
8866
|
Commands:
|
|
8867
8867
|
init Generate a .rogen.json config file
|
|
8868
|
-
|
|
8868
|
+
build Build the project
|
|
8869
|
+
watch Watch the source and build automatically
|
|
8869
8870
|
|
|
8870
8871
|
Options:
|
|
8871
8872
|
-c, --config <path> Specify custom config file
|
|
@@ -8916,6 +8917,7 @@ function parseCliArgs(args = process.argv.slice(2), logger) {
|
|
|
8916
8917
|
parsedArgs.help = true;
|
|
8917
8918
|
} else if (subcommand === "version") {
|
|
8918
8919
|
parsedArgs.version = true;
|
|
8920
|
+
} else if (subcommand === "build") {
|
|
8919
8921
|
} else {
|
|
8920
8922
|
const errorMsg = `unknown subcommand "${positionals[0]}".
|
|
8921
8923
|
Run 'rogen --help' to see a list of available commands.`;
|
|
@@ -8964,7 +8966,17 @@ var import_fs = __toESM(require("fs"), 1);
|
|
|
8964
8966
|
var import_path = __toESM(require("path"), 1);
|
|
8965
8967
|
|
|
8966
8968
|
// src/constants.ts
|
|
8967
|
-
var version = "1.4.
|
|
8969
|
+
var version = "1.4.3";
|
|
8970
|
+
var dataExtensions = /* @__PURE__ */ new Set([
|
|
8971
|
+
".csv",
|
|
8972
|
+
".txt",
|
|
8973
|
+
".json",
|
|
8974
|
+
".toml",
|
|
8975
|
+
".yaml",
|
|
8976
|
+
".yml",
|
|
8977
|
+
".msgpack",
|
|
8978
|
+
".md"
|
|
8979
|
+
]);
|
|
8968
8980
|
var defaultProject = {
|
|
8969
8981
|
name: "roblox-game",
|
|
8970
8982
|
tree: { $className: "DataModel" }
|
|
@@ -9018,6 +9030,10 @@ var serviceParents = {
|
|
|
9018
9030
|
StarterPlayerScripts: "StarterPlayer",
|
|
9019
9031
|
StarterCharacterScripts: "StarterPlayer"
|
|
9020
9032
|
};
|
|
9033
|
+
var combinedServices = /* @__PURE__ */ new Set([
|
|
9034
|
+
...Object.values(services),
|
|
9035
|
+
...Object.values(serviceParents)
|
|
9036
|
+
]);
|
|
9021
9037
|
var serverContainers = /* @__PURE__ */ new Set([
|
|
9022
9038
|
"ServerScriptService",
|
|
9023
9039
|
"ServerStorage"
|
|
@@ -9418,9 +9434,27 @@ function applyCasing(value, casing) {
|
|
|
9418
9434
|
const firstCharacter = casing === "PascalCase" ? value[0].toUpperCase() : value[0].toLowerCase();
|
|
9419
9435
|
return firstCharacter + value.slice(1);
|
|
9420
9436
|
}
|
|
9437
|
+
function getPathString(node) {
|
|
9438
|
+
const p = node.$path;
|
|
9439
|
+
if (typeof p === "string") return p;
|
|
9440
|
+
if (p && typeof p === "object" && typeof p.optional === "string")
|
|
9441
|
+
return p.optional;
|
|
9442
|
+
return void 0;
|
|
9443
|
+
}
|
|
9421
9444
|
function getOrCreateNode(parent, key, className) {
|
|
9445
|
+
const isRootService = combinedServices.has(key) || className && combinedServices.has(className);
|
|
9422
9446
|
if (!parent[key]) {
|
|
9423
|
-
parent[key] = className == null ? {} : {
|
|
9447
|
+
parent[key] = className == null ? {} : {
|
|
9448
|
+
$className: className,
|
|
9449
|
+
...isRootService ? {} : { $ignoreUnknownInstances: false }
|
|
9450
|
+
};
|
|
9451
|
+
} else if (className != null) {
|
|
9452
|
+
const existing = parent[key];
|
|
9453
|
+
if (existing.$ignoreUnknownInstances === void 0 && existing.$path === void 0) {
|
|
9454
|
+
if (!isRootService) {
|
|
9455
|
+
existing.$ignoreUnknownInstances = false;
|
|
9456
|
+
}
|
|
9457
|
+
}
|
|
9424
9458
|
}
|
|
9425
9459
|
return parent[key];
|
|
9426
9460
|
}
|
|
@@ -9430,16 +9464,20 @@ function pruneObject(node, buildDir, outputDir, removed = [], treePath = "") {
|
|
|
9430
9464
|
if (typeof val !== "object" || val === null) continue;
|
|
9431
9465
|
const childTreePath = treePath ? `${treePath}.${key}` : key;
|
|
9432
9466
|
const childNode = val;
|
|
9433
|
-
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9467
|
+
const childPath = getPathString(childNode);
|
|
9468
|
+
if (childPath) {
|
|
9469
|
+
if (hasPathPrefix(childPath, buildDir)) continue;
|
|
9470
|
+
const isOptional = typeof childNode.$path === "object";
|
|
9471
|
+
if (!isOptional) {
|
|
9472
|
+
const absolutePath = import_path2.default.resolve(outputDir, childPath);
|
|
9473
|
+
if (!import_fs2.default.existsSync(absolutePath)) {
|
|
9474
|
+
delete node[key];
|
|
9475
|
+
removed.push({
|
|
9476
|
+
treePath: childTreePath,
|
|
9477
|
+
rojoPath: childPath
|
|
9478
|
+
});
|
|
9479
|
+
continue;
|
|
9480
|
+
}
|
|
9443
9481
|
}
|
|
9444
9482
|
}
|
|
9445
9483
|
pruneObject(childNode, buildDir, outputDir, removed, childTreePath);
|
|
@@ -9462,14 +9500,15 @@ function findMissingPaths(node, buildDir, outputDir, missing = [], treePath = ""
|
|
|
9462
9500
|
if (typeof val !== "object" || val === null) continue;
|
|
9463
9501
|
const childTreePath = treePath ? `${treePath}.${key}` : key;
|
|
9464
9502
|
const childNode = val;
|
|
9465
|
-
|
|
9466
|
-
|
|
9503
|
+
const childPath = getPathString(childNode);
|
|
9504
|
+
if (childPath && hasPathPrefix(childPath, buildDir)) {
|
|
9505
|
+
const absolutePath = import_path2.default.resolve(outputDir, childPath);
|
|
9467
9506
|
if (!import_fs2.default.existsSync(absolutePath)) {
|
|
9468
9507
|
missing.push({
|
|
9469
9508
|
parent: node,
|
|
9470
9509
|
key,
|
|
9471
9510
|
treePath: childTreePath,
|
|
9472
|
-
path:
|
|
9511
|
+
path: childPath,
|
|
9473
9512
|
absolutePath
|
|
9474
9513
|
});
|
|
9475
9514
|
}
|
|
@@ -9488,8 +9527,10 @@ var isScript = (filename) => /\.(tsx?|luau|lua)$/i.test(filename) && !filename.t
|
|
|
9488
9527
|
var isModel = (filename) => /\.(rbxm|rbxmx)$/i.test(filename);
|
|
9489
9528
|
function isData(filename) {
|
|
9490
9529
|
if (isScript(filename) || isModel(filename)) return false;
|
|
9530
|
+
if (filename.toLowerCase().endsWith(".d.ts")) return false;
|
|
9491
9531
|
if (filename.startsWith(".")) return false;
|
|
9492
|
-
|
|
9532
|
+
const ext = import_path2.default.extname(filename).toLowerCase();
|
|
9533
|
+
return dataExtensions.has(ext);
|
|
9493
9534
|
}
|
|
9494
9535
|
var isValidSource = (filename) => isScript(filename) || isModel(filename) || isData(filename);
|
|
9495
9536
|
var isInitFile = (filename) => isScript(filename) && /^(index|init)([.-][a-z0-9_]+)?\./i.test(filename);
|
|
@@ -9505,7 +9546,7 @@ function getRojoBaseName(filename) {
|
|
|
9505
9546
|
}
|
|
9506
9547
|
return filename;
|
|
9507
9548
|
}
|
|
9508
|
-
function collapseFolders(node, buildDir, outputDir) {
|
|
9549
|
+
function collapseFolders(node, buildDir, outputDir, isIgnored = () => false) {
|
|
9509
9550
|
let childCount = 0;
|
|
9510
9551
|
let canCollapse = node.$path === void 0;
|
|
9511
9552
|
let commonDir = null;
|
|
@@ -9514,12 +9555,13 @@ function collapseFolders(node, buildDir, outputDir) {
|
|
|
9514
9555
|
const val = node[key];
|
|
9515
9556
|
if (typeof val !== "object" || val === null) continue;
|
|
9516
9557
|
const childNode = val;
|
|
9517
|
-
collapseFolders(childNode, buildDir, outputDir);
|
|
9558
|
+
collapseFolders(childNode, buildDir, outputDir, isIgnored);
|
|
9518
9559
|
childCount++;
|
|
9519
|
-
|
|
9560
|
+
const childPath = getPathString(childNode);
|
|
9561
|
+
if (!childPath) {
|
|
9520
9562
|
canCollapse = false;
|
|
9521
9563
|
} else {
|
|
9522
|
-
const childAbsPath = import_path2.default.resolve(outputDir,
|
|
9564
|
+
const childAbsPath = import_path2.default.resolve(outputDir, childPath);
|
|
9523
9565
|
const parentDir = import_path2.default.dirname(childAbsPath);
|
|
9524
9566
|
if (commonDir === null) {
|
|
9525
9567
|
commonDir = parentDir;
|
|
@@ -9539,10 +9581,11 @@ function collapseFolders(node, buildDir, outputDir) {
|
|
|
9539
9581
|
if (!hasPathPrefix(toPosix(commonDir), toPosix(absoluteBuildDir))) {
|
|
9540
9582
|
return;
|
|
9541
9583
|
}
|
|
9584
|
+
const relativeCommonDir = toPosix(import_path2.default.relative(outputDir, commonDir));
|
|
9542
9585
|
try {
|
|
9543
9586
|
const diskItems = import_fs2.default.readdirSync(commonDir);
|
|
9544
9587
|
const visibleDiskItems = diskItems.filter(
|
|
9545
|
-
(item) => !item.startsWith(".")
|
|
9588
|
+
(item) => !item.startsWith(".") && !isIgnored(toPosix(import_path2.default.join(relativeCommonDir, item)))
|
|
9546
9589
|
);
|
|
9547
9590
|
if (visibleDiskItems.length !== childCount) {
|
|
9548
9591
|
return;
|
|
@@ -9550,13 +9593,12 @@ function collapseFolders(node, buildDir, outputDir) {
|
|
|
9550
9593
|
} catch {
|
|
9551
9594
|
return;
|
|
9552
9595
|
}
|
|
9553
|
-
const relativeCommonDir = toPosix(import_path2.default.relative(outputDir, commonDir));
|
|
9554
9596
|
for (const key in node) {
|
|
9555
9597
|
if (!key.startsWith("$")) {
|
|
9556
9598
|
delete node[key];
|
|
9557
9599
|
}
|
|
9558
9600
|
}
|
|
9559
|
-
node.$path = relativeCommonDir;
|
|
9601
|
+
node.$path = { optional: relativeCommonDir };
|
|
9560
9602
|
delete node.$className;
|
|
9561
9603
|
}
|
|
9562
9604
|
function findExposedDataFiles(node, exposed = []) {
|
|
@@ -9565,8 +9607,9 @@ function findExposedDataFiles(node, exposed = []) {
|
|
|
9565
9607
|
const val = node[key];
|
|
9566
9608
|
if (typeof val !== "object" || val === null) continue;
|
|
9567
9609
|
const childNode = val;
|
|
9568
|
-
|
|
9569
|
-
|
|
9610
|
+
const childPath = getPathString(childNode);
|
|
9611
|
+
if (childPath && isData(childPath)) {
|
|
9612
|
+
exposed.push({ parent: node, key, path: childPath });
|
|
9570
9613
|
}
|
|
9571
9614
|
findExposedDataFiles(childNode, exposed);
|
|
9572
9615
|
}
|
|
@@ -10003,6 +10046,24 @@ async function build(targetConfig, baseProjectTree, config, env, sourcePaths, cl
|
|
|
10003
10046
|
modeCopy.build = cliArgs.build;
|
|
10004
10047
|
}
|
|
10005
10048
|
const rojoTree = structuredClone(baseProjectTree);
|
|
10049
|
+
for (const targetService of Object.values(services)) {
|
|
10050
|
+
let current = rojoTree.tree;
|
|
10051
|
+
if (serviceParents[targetService]) {
|
|
10052
|
+
current = getOrCreateNode(current, serviceParents[targetService]);
|
|
10053
|
+
}
|
|
10054
|
+
getOrCreateNode(current, targetService);
|
|
10055
|
+
}
|
|
10056
|
+
if (config.aliases) {
|
|
10057
|
+
for (const aliasPath of Object.values(config.aliases)) {
|
|
10058
|
+
if (typeof aliasPath === "string") {
|
|
10059
|
+
const parts = aliasPath.split(".");
|
|
10060
|
+
let current = rojoTree.tree;
|
|
10061
|
+
for (const part of parts) {
|
|
10062
|
+
current = getOrCreateNode(current, part);
|
|
10063
|
+
}
|
|
10064
|
+
}
|
|
10065
|
+
}
|
|
10066
|
+
}
|
|
10006
10067
|
const mergedTags = { ...config.tags || {}, ...modeCopy.tags || {} };
|
|
10007
10068
|
const knownTags = new Set(
|
|
10008
10069
|
Object.keys(config.tags || {}).map((t) => t.toLowerCase())
|
|
@@ -10038,6 +10099,7 @@ async function build(targetConfig, baseProjectTree, config, env, sourcePaths, cl
|
|
|
10038
10099
|
};
|
|
10039
10100
|
const combinedGlobIgnorePaths = Array.from(
|
|
10040
10101
|
/* @__PURE__ */ new Set([
|
|
10102
|
+
...rojoTree.globIgnorePaths || [],
|
|
10041
10103
|
...config.globIgnorePaths || [],
|
|
10042
10104
|
...modeCopy.globIgnorePaths || []
|
|
10043
10105
|
])
|
|
@@ -10067,7 +10129,6 @@ async function build(targetConfig, baseProjectTree, config, env, sourcePaths, cl
|
|
|
10067
10129
|
listings,
|
|
10068
10130
|
(filepath, isInit, isSync) => {
|
|
10069
10131
|
const relativePath2 = import_path4.default.relative(sourcePath, filepath);
|
|
10070
|
-
if (relativePath2 && isIgnored(toPosix(relativePath2))) return;
|
|
10071
10132
|
const {
|
|
10072
10133
|
targetService,
|
|
10073
10134
|
wrapperFolder,
|
|
@@ -10077,7 +10138,8 @@ async function build(targetConfig, baseProjectTree, config, env, sourcePaths, cl
|
|
|
10077
10138
|
dropped,
|
|
10078
10139
|
unwrap
|
|
10079
10140
|
} = resolveRoute(relativePath2, isInit, newContext, isSync);
|
|
10080
|
-
if (dropped)
|
|
10141
|
+
if (dropped || projectPath && isIgnored(toPosix(projectPath)))
|
|
10142
|
+
return;
|
|
10081
10143
|
fileCount++;
|
|
10082
10144
|
let current = rojoTree.tree;
|
|
10083
10145
|
if (serviceParents[targetService]) {
|
|
@@ -10109,10 +10171,13 @@ async function build(targetConfig, baseProjectTree, config, env, sourcePaths, cl
|
|
|
10109
10171
|
const existingNode = existingNodeRaw || {};
|
|
10110
10172
|
const newNode = {
|
|
10111
10173
|
...existingNode,
|
|
10112
|
-
$path: projectPath
|
|
10174
|
+
$path: { optional: projectPath }
|
|
10113
10175
|
};
|
|
10114
10176
|
if (newNode.$className === "Folder") {
|
|
10115
10177
|
delete newNode.$className;
|
|
10178
|
+
if (newNode.$ignoreUnknownInstances === false) {
|
|
10179
|
+
delete newNode.$ignoreUnknownInstances;
|
|
10180
|
+
}
|
|
10116
10181
|
}
|
|
10117
10182
|
current[nodeName] = newNode;
|
|
10118
10183
|
nodeOrigins.set(newNode, {
|
|
@@ -10130,8 +10195,11 @@ async function build(targetConfig, baseProjectTree, config, env, sourcePaths, cl
|
|
|
10130
10195
|
outputDir,
|
|
10131
10196
|
removed
|
|
10132
10197
|
);
|
|
10133
|
-
collapseFolders(prunedTree, context.build, outputDir);
|
|
10198
|
+
collapseFolders(prunedTree, context.build, outputDir, isIgnored);
|
|
10134
10199
|
rojoTree.tree = prunedTree;
|
|
10200
|
+
if (combinedGlobIgnorePaths.length > 0) {
|
|
10201
|
+
rojoTree.globIgnorePaths = combinedGlobIgnorePaths;
|
|
10202
|
+
}
|
|
10135
10203
|
const sortedTree = sortObject(rojoTree);
|
|
10136
10204
|
const missingPaths = findMissingPaths(
|
|
10137
10205
|
sortedTree.tree,
|
|
@@ -10242,7 +10310,7 @@ var ConsoleLogger = class extends AbstractLogger {
|
|
|
10242
10310
|
};
|
|
10243
10311
|
|
|
10244
10312
|
// src/execute.ts
|
|
10245
|
-
async function execute(sourcePaths, env, activeModes, baseProjectTree, config, cliArgs, anchor, logger = new ConsoleLogger()) {
|
|
10313
|
+
async function execute(sourcePaths, env, activeModes, baseProjectTree, config, cliArgs, anchor, logger = new ConsoleLogger(), isInitialRun = false) {
|
|
10246
10314
|
try {
|
|
10247
10315
|
for (const activeMode of activeModes) {
|
|
10248
10316
|
const targetConfig = activeMode.config;
|
|
@@ -10281,15 +10349,16 @@ async function execute(sourcePaths, env, activeModes, baseProjectTree, config, c
|
|
|
10281
10349
|
}
|
|
10282
10350
|
}
|
|
10283
10351
|
if (buildResult.missingPaths.length > 0) {
|
|
10352
|
+
const isCompiledProject = env.isTsProject || env.isDarkluaProject;
|
|
10284
10353
|
for (const item of buildResult.missingPaths) {
|
|
10285
10354
|
const ext = import_path5.default.extname(item.absolutePath).toLowerCase();
|
|
10286
|
-
if (ext === ".luau" || ext === ".lua") {
|
|
10355
|
+
if (isCompiledProject && (ext === ".luau" || ext === ".lua")) {
|
|
10287
10356
|
const dir = import_path5.default.dirname(item.absolutePath);
|
|
10288
10357
|
if (!import_fs4.default.existsSync(dir)) {
|
|
10289
10358
|
import_fs4.default.mkdirSync(dir, { recursive: true });
|
|
10290
10359
|
}
|
|
10291
10360
|
import_fs4.default.writeFileSync(item.absolutePath, "");
|
|
10292
|
-
} else if (ext === "") {
|
|
10361
|
+
} else if (isCompiledProject && ext === "") {
|
|
10293
10362
|
if (!import_fs4.default.existsSync(item.absolutePath)) {
|
|
10294
10363
|
import_fs4.default.mkdirSync(item.absolutePath, {
|
|
10295
10364
|
recursive: true
|
|
@@ -10312,14 +10381,18 @@ async function execute(sourcePaths, env, activeModes, baseProjectTree, config, c
|
|
|
10312
10381
|
shouldWrite = false;
|
|
10313
10382
|
}
|
|
10314
10383
|
}
|
|
10315
|
-
if (!shouldWrite) {
|
|
10384
|
+
if (!shouldWrite && cliArgs.watch && !isInitialRun) {
|
|
10316
10385
|
continue;
|
|
10317
10386
|
}
|
|
10318
|
-
|
|
10319
|
-
|
|
10320
|
-
import_fs4.default.
|
|
10387
|
+
if (shouldWrite) {
|
|
10388
|
+
const outputDir = import_path5.default.dirname(buildResult.output);
|
|
10389
|
+
if (!import_fs4.default.existsSync(outputDir)) {
|
|
10390
|
+
import_fs4.default.mkdirSync(outputDir, { recursive: true });
|
|
10391
|
+
}
|
|
10392
|
+
const tempOutput = `${buildResult.output}.tmp`;
|
|
10393
|
+
import_fs4.default.writeFileSync(tempOutput, finalContent);
|
|
10394
|
+
import_fs4.default.renameSync(tempOutput, buildResult.output);
|
|
10321
10395
|
}
|
|
10322
|
-
import_fs4.default.writeFileSync(buildResult.output, finalContent);
|
|
10323
10396
|
const totalRemoved = buildResult.removed.length + dropped.length;
|
|
10324
10397
|
if (totalRemoved > 0) {
|
|
10325
10398
|
if (cliArgs.watch) {
|
|
@@ -10341,22 +10414,30 @@ async function execute(sourcePaths, env, activeModes, baseProjectTree, config, c
|
|
|
10341
10414
|
logger.warn(collision);
|
|
10342
10415
|
}
|
|
10343
10416
|
}
|
|
10344
|
-
if (
|
|
10417
|
+
if (!isInitialRun) {
|
|
10345
10418
|
const outputName = import_path5.default.basename(buildResult.output);
|
|
10346
10419
|
logger.success(
|
|
10347
10420
|
`[${modeName}] Rebuilt "${buildResult.name}" -> ${outputName}`
|
|
10348
10421
|
);
|
|
10349
10422
|
} else {
|
|
10350
|
-
|
|
10351
|
-
|
|
10352
|
-
|
|
10353
|
-
|
|
10354
|
-
(
|
|
10355
|
-
|
|
10356
|
-
|
|
10357
|
-
|
|
10423
|
+
if (shouldWrite) {
|
|
10424
|
+
logger.success(
|
|
10425
|
+
`Generated Rojo tree for "${buildResult.name}"`
|
|
10426
|
+
);
|
|
10427
|
+
logger.info(` Processed: ${buildResult.fileCount} files`);
|
|
10428
|
+
logger.info(` Build dir: ${buildResult.buildDir}`);
|
|
10429
|
+
const activeTags = Object.keys(
|
|
10430
|
+
targetConfig.tags || {}
|
|
10431
|
+
).filter((t) => targetConfig.tags[t]);
|
|
10432
|
+
if (activeTags.length > 0) {
|
|
10433
|
+
logger.info(` Tags: ${activeTags.join(", ")}`);
|
|
10434
|
+
}
|
|
10435
|
+
logger.info(` Output to: ${buildResult.output}`);
|
|
10436
|
+
} else {
|
|
10437
|
+
logger.success(
|
|
10438
|
+
`Rojo tree for "${buildResult.name}" is already up to date`
|
|
10439
|
+
);
|
|
10358
10440
|
}
|
|
10359
|
-
logger.info(` Output to: ${buildResult.output}`);
|
|
10360
10441
|
}
|
|
10361
10442
|
}
|
|
10362
10443
|
return true;
|
|
@@ -10440,7 +10521,8 @@ async function main() {
|
|
|
10440
10521
|
config,
|
|
10441
10522
|
cliArgs,
|
|
10442
10523
|
anchor,
|
|
10443
|
-
logger
|
|
10524
|
+
logger,
|
|
10525
|
+
true
|
|
10444
10526
|
);
|
|
10445
10527
|
if (!success) {
|
|
10446
10528
|
process.exit(1);
|
|
@@ -10449,9 +10531,14 @@ async function main() {
|
|
|
10449
10531
|
logger.info(
|
|
10450
10532
|
`Watching for file changes in: "${sourceDirs.join(", ")}" (Ctrl+C to stop)`
|
|
10451
10533
|
);
|
|
10534
|
+
const isWindows = process.platform === "win32";
|
|
10452
10535
|
const watcher = import_chokidar.default.watch(sourcePaths, {
|
|
10453
10536
|
persistent: true,
|
|
10454
|
-
ignoreInitial: true
|
|
10537
|
+
ignoreInitial: true,
|
|
10538
|
+
// Use polling on Windows to avoid "folder in use" locks
|
|
10539
|
+
usePolling: isWindows,
|
|
10540
|
+
interval: isWindows ? 400 : void 0,
|
|
10541
|
+
binaryInterval: isWindows ? 800 : void 0
|
|
10455
10542
|
});
|
|
10456
10543
|
let debounceTimeout;
|
|
10457
10544
|
let isBuilding = false;
|
|
@@ -10470,7 +10557,8 @@ async function main() {
|
|
|
10470
10557
|
config,
|
|
10471
10558
|
cliArgs,
|
|
10472
10559
|
anchor,
|
|
10473
|
-
logger
|
|
10560
|
+
logger,
|
|
10561
|
+
false
|
|
10474
10562
|
);
|
|
10475
10563
|
} catch (err) {
|
|
10476
10564
|
logger.error(err instanceof Error ? err : String(err));
|
package/dist/cli.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { parseArgs } from "util";
|
|
2
2
|
export function printHelp(logger) {
|
|
3
3
|
const helpText = `
|
|
4
|
-
Rogen - Feature-based
|
|
4
|
+
Rogen - Feature-based architecture for Roblox
|
|
5
5
|
|
|
6
6
|
Usage:
|
|
7
7
|
rogen [command] [options]
|
|
8
8
|
|
|
9
9
|
Commands:
|
|
10
10
|
init Generate a .rogen.json config file
|
|
11
|
-
|
|
11
|
+
build Build the project
|
|
12
|
+
watch Watch the source and build automatically
|
|
12
13
|
|
|
13
14
|
Options:
|
|
14
15
|
-c, --config <path> Specify custom config file
|
|
@@ -64,6 +65,9 @@ export function parseCliArgs(args = process.argv.slice(2), logger) {
|
|
|
64
65
|
else if (subcommand === "version") {
|
|
65
66
|
parsedArgs.version = true;
|
|
66
67
|
}
|
|
68
|
+
else if (subcommand === "build") {
|
|
69
|
+
// build is the default command, so it falls through cleanly
|
|
70
|
+
}
|
|
67
71
|
else {
|
|
68
72
|
const errorMsg = `unknown subcommand "${positionals[0]}".\nRun 'rogen --help' to see a list of available commands.`;
|
|
69
73
|
if (logger) {
|
package/dist/constants.js
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
export const version = "1.4.
|
|
1
|
+
export const version = "1.4.3";
|
|
2
|
+
export const dataExtensions = new Set([
|
|
3
|
+
".csv",
|
|
4
|
+
".txt",
|
|
5
|
+
".json",
|
|
6
|
+
".toml",
|
|
7
|
+
".yaml",
|
|
8
|
+
".yml",
|
|
9
|
+
".msgpack",
|
|
10
|
+
".md",
|
|
11
|
+
]);
|
|
2
12
|
export const defaultProject = {
|
|
3
13
|
name: "roblox-game",
|
|
4
14
|
tree: { $className: "DataModel" },
|
|
@@ -52,6 +62,10 @@ export const serviceParents = {
|
|
|
52
62
|
StarterPlayerScripts: "StarterPlayer",
|
|
53
63
|
StarterCharacterScripts: "StarterPlayer",
|
|
54
64
|
};
|
|
65
|
+
export const combinedServices = new Set([
|
|
66
|
+
...Object.values(services),
|
|
67
|
+
...Object.values(serviceParents),
|
|
68
|
+
]);
|
|
55
69
|
export const serverContainers = new Set([
|
|
56
70
|
"ServerScriptService",
|
|
57
71
|
"ServerStorage",
|
package/dist/execute.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from "fs";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { build } from "./build.js";
|
|
4
4
|
import { ConsoleLogger, getTimeStamp } from "./logger.js";
|
|
5
|
-
export async function execute(sourcePaths, env, activeModes, baseProjectTree, config, cliArgs, anchor, logger = new ConsoleLogger()) {
|
|
5
|
+
export async function execute(sourcePaths, env, activeModes, baseProjectTree, config, cliArgs, anchor, logger = new ConsoleLogger(), isInitialRun = false) {
|
|
6
6
|
try {
|
|
7
7
|
for (const activeMode of activeModes) {
|
|
8
8
|
const targetConfig = activeMode.config;
|
|
@@ -24,16 +24,22 @@ export async function execute(sourcePaths, env, activeModes, baseProjectTree, co
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
if (buildResult.missingPaths.length > 0) {
|
|
27
|
+
// Only synthesize placeholder stubs/dirs for compiled projects (ts/darklua),
|
|
28
|
+
// where the build output legitimately does not exist yet at generation time. For a luau
|
|
29
|
+
// project the build dir IS the source, so a missing path means the file was removed —
|
|
30
|
+
// recreating it would fight an in-progress delete and desync Rojo. Drop the stale node instead.
|
|
31
|
+
const isCompiledProject = env.isTsProject || env.isDarkluaProject;
|
|
27
32
|
for (const item of buildResult.missingPaths) {
|
|
28
33
|
const ext = path.extname(item.absolutePath).toLowerCase();
|
|
29
|
-
if (
|
|
34
|
+
if (isCompiledProject &&
|
|
35
|
+
(ext === ".luau" || ext === ".lua")) {
|
|
30
36
|
const dir = path.dirname(item.absolutePath);
|
|
31
37
|
if (!fs.existsSync(dir)) {
|
|
32
38
|
fs.mkdirSync(dir, { recursive: true });
|
|
33
39
|
}
|
|
34
40
|
fs.writeFileSync(item.absolutePath, "");
|
|
35
41
|
}
|
|
36
|
-
else if (ext === "") {
|
|
42
|
+
else if (isCompiledProject && ext === "") {
|
|
37
43
|
if (!fs.existsSync(item.absolutePath)) {
|
|
38
44
|
fs.mkdirSync(item.absolutePath, {
|
|
39
45
|
recursive: true,
|
|
@@ -54,14 +60,20 @@ export async function execute(sourcePaths, env, activeModes, baseProjectTree, co
|
|
|
54
60
|
shouldWrite = false;
|
|
55
61
|
}
|
|
56
62
|
}
|
|
57
|
-
if (!shouldWrite) {
|
|
63
|
+
if (!shouldWrite && cliArgs.watch && !isInitialRun) {
|
|
58
64
|
continue;
|
|
59
65
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
fs.
|
|
66
|
+
if (shouldWrite) {
|
|
67
|
+
const outputDir = path.dirname(buildResult.output);
|
|
68
|
+
if (!fs.existsSync(outputDir)) {
|
|
69
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
// Write atomically (temp + rename) so a watcher (e.g. Rojo) never reads a
|
|
72
|
+
// half-written project file during rapid regenerations.
|
|
73
|
+
const tempOutput = `${buildResult.output}.tmp`;
|
|
74
|
+
fs.writeFileSync(tempOutput, finalContent);
|
|
75
|
+
fs.renameSync(tempOutput, buildResult.output);
|
|
63
76
|
}
|
|
64
|
-
fs.writeFileSync(buildResult.output, finalContent);
|
|
65
77
|
const totalRemoved = buildResult.removed.length + dropped.length;
|
|
66
78
|
if (totalRemoved > 0) {
|
|
67
79
|
if (cliArgs.watch) {
|
|
@@ -82,19 +94,24 @@ export async function execute(sourcePaths, env, activeModes, baseProjectTree, co
|
|
|
82
94
|
logger.warn(collision);
|
|
83
95
|
}
|
|
84
96
|
}
|
|
85
|
-
if (
|
|
97
|
+
if (!isInitialRun) {
|
|
86
98
|
const outputName = path.basename(buildResult.output);
|
|
87
99
|
logger.success(`[${modeName}] Rebuilt "${buildResult.name}" -> ${outputName}`);
|
|
88
100
|
}
|
|
89
101
|
else {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
102
|
+
if (shouldWrite) {
|
|
103
|
+
logger.success(`Generated Rojo tree for "${buildResult.name}"`);
|
|
104
|
+
logger.info(` Processed: ${buildResult.fileCount} files`);
|
|
105
|
+
logger.info(` Build dir: ${buildResult.buildDir}`);
|
|
106
|
+
const activeTags = Object.keys(targetConfig.tags || {}).filter((t) => targetConfig.tags[t]);
|
|
107
|
+
if (activeTags.length > 0) {
|
|
108
|
+
logger.info(` Tags: ${activeTags.join(", ")}`);
|
|
109
|
+
}
|
|
110
|
+
logger.info(` Output to: ${buildResult.output}`);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
logger.success(`Rojo tree for "${buildResult.name}" is already up to date`);
|
|
96
114
|
}
|
|
97
|
-
logger.info(` Output to: ${buildResult.output}`);
|
|
98
115
|
}
|
|
99
116
|
}
|
|
100
117
|
return true;
|
package/dist/index.js
CHANGED
|
@@ -67,15 +67,20 @@ async function main() {
|
|
|
67
67
|
});
|
|
68
68
|
const env = getEnvironment(anchor, cliArgs.mode);
|
|
69
69
|
const activeModes = resolveActiveModes(config, cliArgs.mode, env);
|
|
70
|
-
const success = await execute(sourcePaths, env, activeModes, config.project, config, cliArgs, anchor, logger);
|
|
70
|
+
const success = await execute(sourcePaths, env, activeModes, config.project, config, cliArgs, anchor, logger, true);
|
|
71
71
|
if (!success) {
|
|
72
72
|
process.exit(1);
|
|
73
73
|
}
|
|
74
74
|
if (cliArgs.watch) {
|
|
75
75
|
logger.info(`Watching for file changes in: "${sourceDirs.join(", ")}" (Ctrl+C to stop)`);
|
|
76
|
+
const isWindows = process.platform === "win32";
|
|
76
77
|
const watcher = chokidar.watch(sourcePaths, {
|
|
77
78
|
persistent: true,
|
|
78
79
|
ignoreInitial: true,
|
|
80
|
+
// Use polling on Windows to avoid "folder in use" locks
|
|
81
|
+
usePolling: isWindows,
|
|
82
|
+
interval: isWindows ? 400 : undefined,
|
|
83
|
+
binaryInterval: isWindows ? 800 : undefined,
|
|
79
84
|
});
|
|
80
85
|
let debounceTimeout;
|
|
81
86
|
let isBuilding = false;
|
|
@@ -87,7 +92,7 @@ async function main() {
|
|
|
87
92
|
return;
|
|
88
93
|
isBuilding = true;
|
|
89
94
|
try {
|
|
90
|
-
execute(sourcePaths, env, activeModes, config.project, config, cliArgs, anchor, logger);
|
|
95
|
+
execute(sourcePaths, env, activeModes, config.project, config, cliArgs, anchor, logger, false);
|
|
91
96
|
}
|
|
92
97
|
catch (err) {
|
|
93
98
|
logger.error(err instanceof Error ? err : String(err));
|
package/dist/tree.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import { combinedServices, dataExtensions } from "./constants.js";
|
|
3
4
|
function hasPathPrefix(p, dir) {
|
|
4
5
|
return p === dir || p.startsWith(dir + "/");
|
|
5
6
|
}
|
|
@@ -12,9 +13,36 @@ export function applyCasing(value, casing) {
|
|
|
12
13
|
: value[0].toLowerCase();
|
|
13
14
|
return firstCharacter + value.slice(1);
|
|
14
15
|
}
|
|
16
|
+
export function getPathString(node) {
|
|
17
|
+
const p = node.$path;
|
|
18
|
+
if (typeof p === "string")
|
|
19
|
+
return p;
|
|
20
|
+
if (p && typeof p === "object" && typeof p.optional === "string")
|
|
21
|
+
return p.optional;
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
15
24
|
export function getOrCreateNode(parent, key, className) {
|
|
25
|
+
const isRootService = combinedServices.has(key) ||
|
|
26
|
+
(className && combinedServices.has(className));
|
|
16
27
|
if (!parent[key]) {
|
|
17
|
-
parent[key] =
|
|
28
|
+
parent[key] =
|
|
29
|
+
className == null
|
|
30
|
+
? {}
|
|
31
|
+
: {
|
|
32
|
+
$className: className,
|
|
33
|
+
...(isRootService
|
|
34
|
+
? {}
|
|
35
|
+
: { $ignoreUnknownInstances: false }),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
else if (className != null) {
|
|
39
|
+
const existing = parent[key];
|
|
40
|
+
if (existing.$ignoreUnknownInstances === undefined &&
|
|
41
|
+
existing.$path === undefined) {
|
|
42
|
+
if (!isRootService) {
|
|
43
|
+
existing.$ignoreUnknownInstances = false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
18
46
|
}
|
|
19
47
|
return parent[key];
|
|
20
48
|
}
|
|
@@ -25,17 +53,23 @@ export function pruneObject(node, buildDir, outputDir, removed = [], treePath =
|
|
|
25
53
|
continue;
|
|
26
54
|
const childTreePath = treePath ? `${treePath}.${key}` : key;
|
|
27
55
|
const childNode = val;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const absolutePath = path.resolve(outputDir, childNode.$path);
|
|
32
|
-
if (!fs.existsSync(absolutePath)) {
|
|
33
|
-
delete node[key];
|
|
34
|
-
removed.push({
|
|
35
|
-
treePath: childTreePath,
|
|
36
|
-
rojoPath: childNode.$path,
|
|
37
|
-
});
|
|
56
|
+
const childPath = getPathString(childNode);
|
|
57
|
+
if (childPath) {
|
|
58
|
+
if (hasPathPrefix(childPath, buildDir))
|
|
38
59
|
continue;
|
|
60
|
+
// Optional paths are allowed to be absent so only required (string) template
|
|
61
|
+
// paths are pruned when missing.
|
|
62
|
+
const isOptional = typeof childNode.$path === "object";
|
|
63
|
+
if (!isOptional) {
|
|
64
|
+
const absolutePath = path.resolve(outputDir, childPath);
|
|
65
|
+
if (!fs.existsSync(absolutePath)) {
|
|
66
|
+
delete node[key];
|
|
67
|
+
removed.push({
|
|
68
|
+
treePath: childTreePath,
|
|
69
|
+
rojoPath: childPath,
|
|
70
|
+
});
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
39
73
|
}
|
|
40
74
|
}
|
|
41
75
|
pruneObject(childNode, buildDir, outputDir, removed, childTreePath);
|
|
@@ -61,14 +95,15 @@ export function findMissingPaths(node, buildDir, outputDir, missing = [], treePa
|
|
|
61
95
|
continue;
|
|
62
96
|
const childTreePath = treePath ? `${treePath}.${key}` : key;
|
|
63
97
|
const childNode = val;
|
|
64
|
-
|
|
65
|
-
|
|
98
|
+
const childPath = getPathString(childNode);
|
|
99
|
+
if (childPath && hasPathPrefix(childPath, buildDir)) {
|
|
100
|
+
const absolutePath = path.resolve(outputDir, childPath);
|
|
66
101
|
if (!fs.existsSync(absolutePath)) {
|
|
67
102
|
missing.push({
|
|
68
103
|
parent: node,
|
|
69
104
|
key,
|
|
70
105
|
treePath: childTreePath,
|
|
71
|
-
path:
|
|
106
|
+
path: childPath,
|
|
72
107
|
absolutePath,
|
|
73
108
|
});
|
|
74
109
|
}
|
|
@@ -83,9 +118,12 @@ const isModel = (filename) => /\.(rbxm|rbxmx)$/i.test(filename);
|
|
|
83
118
|
export function isData(filename) {
|
|
84
119
|
if (isScript(filename) || isModel(filename))
|
|
85
120
|
return false;
|
|
121
|
+
if (filename.toLowerCase().endsWith(".d.ts"))
|
|
122
|
+
return false;
|
|
86
123
|
if (filename.startsWith("."))
|
|
87
124
|
return false;
|
|
88
|
-
|
|
125
|
+
const ext = path.extname(filename).toLowerCase();
|
|
126
|
+
return dataExtensions.has(ext);
|
|
89
127
|
}
|
|
90
128
|
export const isValidSource = (filename) => isScript(filename) || isModel(filename) || isData(filename);
|
|
91
129
|
export const isInitFile = (filename) => isScript(filename) && /^(index|init)([.-][a-z0-9_]+)?\./i.test(filename);
|
|
@@ -103,7 +141,7 @@ function getRojoBaseName(filename) {
|
|
|
103
141
|
}
|
|
104
142
|
return filename;
|
|
105
143
|
}
|
|
106
|
-
export function collapseFolders(node, buildDir, outputDir) {
|
|
144
|
+
export function collapseFolders(node, buildDir, outputDir, isIgnored = () => false) {
|
|
107
145
|
let childCount = 0;
|
|
108
146
|
let canCollapse = node.$path === undefined; // Prevent overwriting a folder with an explicit $path
|
|
109
147
|
let commonDir = null;
|
|
@@ -115,13 +153,14 @@ export function collapseFolders(node, buildDir, outputDir) {
|
|
|
115
153
|
continue;
|
|
116
154
|
const childNode = val;
|
|
117
155
|
// Process deepest nested children first
|
|
118
|
-
collapseFolders(childNode, buildDir, outputDir);
|
|
156
|
+
collapseFolders(childNode, buildDir, outputDir, isIgnored);
|
|
119
157
|
childCount++;
|
|
120
|
-
|
|
158
|
+
const childPath = getPathString(childNode);
|
|
159
|
+
if (!childPath) {
|
|
121
160
|
canCollapse = false;
|
|
122
161
|
}
|
|
123
162
|
else {
|
|
124
|
-
const childAbsPath = path.resolve(outputDir,
|
|
163
|
+
const childAbsPath = path.resolve(outputDir, childPath);
|
|
125
164
|
const parentDir = path.dirname(childAbsPath);
|
|
126
165
|
// All children should share the same directory
|
|
127
166
|
if (commonDir === null) {
|
|
@@ -143,10 +182,12 @@ export function collapseFolders(node, buildDir, outputDir) {
|
|
|
143
182
|
if (!hasPathPrefix(toPosix(commonDir), toPosix(absoluteBuildDir))) {
|
|
144
183
|
return;
|
|
145
184
|
}
|
|
185
|
+
const relativeCommonDir = toPosix(path.relative(outputDir, commonDir));
|
|
146
186
|
try {
|
|
147
187
|
const diskItems = fs.readdirSync(commonDir);
|
|
148
|
-
// filter out marker files so they do not prevent folders from collapsing
|
|
149
|
-
const visibleDiskItems = diskItems.filter((item) => !item.startsWith(".")
|
|
188
|
+
// filter out marker files and ignored files so they do not prevent folders from collapsing
|
|
189
|
+
const visibleDiskItems = diskItems.filter((item) => !item.startsWith(".") &&
|
|
190
|
+
!isIgnored(toPosix(path.join(relativeCommonDir, item))));
|
|
150
191
|
if (visibleDiskItems.length !== childCount) {
|
|
151
192
|
return;
|
|
152
193
|
}
|
|
@@ -155,13 +196,12 @@ export function collapseFolders(node, buildDir, outputDir) {
|
|
|
155
196
|
return;
|
|
156
197
|
}
|
|
157
198
|
// Replace all child files with a single folder $path
|
|
158
|
-
const relativeCommonDir = toPosix(path.relative(outputDir, commonDir));
|
|
159
199
|
for (const key in node) {
|
|
160
200
|
if (!key.startsWith("$")) {
|
|
161
201
|
delete node[key];
|
|
162
202
|
}
|
|
163
203
|
}
|
|
164
|
-
node.$path = relativeCommonDir;
|
|
204
|
+
node.$path = { optional: relativeCommonDir };
|
|
165
205
|
delete node.$className;
|
|
166
206
|
}
|
|
167
207
|
export function findExposedDataFiles(node, exposed = []) {
|
|
@@ -172,9 +212,10 @@ export function findExposedDataFiles(node, exposed = []) {
|
|
|
172
212
|
if (typeof val !== "object" || val === null)
|
|
173
213
|
continue;
|
|
174
214
|
const childNode = val;
|
|
215
|
+
const childPath = getPathString(childNode);
|
|
175
216
|
// Check if the current node exposes a raw data file path
|
|
176
|
-
if (
|
|
177
|
-
exposed.push({ parent: node, key, path:
|
|
217
|
+
if (childPath && isData(childPath)) {
|
|
218
|
+
exposed.push({ parent: node, key, path: childPath });
|
|
178
219
|
}
|
|
179
220
|
findExposedDataFiles(childNode, exposed);
|
|
180
221
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ldgerrits/rogen",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.3",
|
|
4
|
+
"description": "A CLI tool that enables feature-based architecture for Roblox",
|
|
4
5
|
"publishConfig": {
|
|
5
6
|
"access": "public"
|
|
6
7
|
},
|
|
7
|
-
"
|
|
8
|
+
"homepage": "https://rogen-playfully.vercel.app/",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/LDGerrits/rogen.git"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"roblox",
|
|
15
|
+
"organization",
|
|
16
|
+
"feature-based architecture"
|
|
17
|
+
],
|
|
8
18
|
"author": "ldgerrits",
|
|
9
19
|
"license": "MIT",
|
|
10
20
|
"type": "module",
|