@ldgerrits/rogen 1.4.1 → 1.4.2
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 +128 -51
- package/dist/cli.js +6 -2
- package/dist/constants.js +5 -1
- package/dist/execute.js +33 -16
- package/dist/index.js +7 -2
- package/dist/tree.js +64 -24
- 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,7 @@ 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.2";
|
|
8968
8970
|
var defaultProject = {
|
|
8969
8971
|
name: "roblox-game",
|
|
8970
8972
|
tree: { $className: "DataModel" }
|
|
@@ -9018,6 +9020,10 @@ var serviceParents = {
|
|
|
9018
9020
|
StarterPlayerScripts: "StarterPlayer",
|
|
9019
9021
|
StarterCharacterScripts: "StarterPlayer"
|
|
9020
9022
|
};
|
|
9023
|
+
var combinedServices = /* @__PURE__ */ new Set([
|
|
9024
|
+
...Object.values(services),
|
|
9025
|
+
...Object.values(serviceParents)
|
|
9026
|
+
]);
|
|
9021
9027
|
var serverContainers = /* @__PURE__ */ new Set([
|
|
9022
9028
|
"ServerScriptService",
|
|
9023
9029
|
"ServerStorage"
|
|
@@ -9418,9 +9424,27 @@ function applyCasing(value, casing) {
|
|
|
9418
9424
|
const firstCharacter = casing === "PascalCase" ? value[0].toUpperCase() : value[0].toLowerCase();
|
|
9419
9425
|
return firstCharacter + value.slice(1);
|
|
9420
9426
|
}
|
|
9427
|
+
function getPathString(node) {
|
|
9428
|
+
const p = node.$path;
|
|
9429
|
+
if (typeof p === "string") return p;
|
|
9430
|
+
if (p && typeof p === "object" && typeof p.optional === "string")
|
|
9431
|
+
return p.optional;
|
|
9432
|
+
return void 0;
|
|
9433
|
+
}
|
|
9421
9434
|
function getOrCreateNode(parent, key, className) {
|
|
9435
|
+
const isRootService = combinedServices.has(key) || className && combinedServices.has(className);
|
|
9422
9436
|
if (!parent[key]) {
|
|
9423
|
-
parent[key] = className == null ? {} : {
|
|
9437
|
+
parent[key] = className == null ? {} : {
|
|
9438
|
+
$className: className,
|
|
9439
|
+
...isRootService ? {} : { $ignoreUnknownInstances: false }
|
|
9440
|
+
};
|
|
9441
|
+
} else if (className != null) {
|
|
9442
|
+
const existing = parent[key];
|
|
9443
|
+
if (existing.$ignoreUnknownInstances === void 0 && existing.$path === void 0) {
|
|
9444
|
+
if (!isRootService) {
|
|
9445
|
+
existing.$ignoreUnknownInstances = false;
|
|
9446
|
+
}
|
|
9447
|
+
}
|
|
9424
9448
|
}
|
|
9425
9449
|
return parent[key];
|
|
9426
9450
|
}
|
|
@@ -9430,16 +9454,20 @@ function pruneObject(node, buildDir, outputDir, removed = [], treePath = "") {
|
|
|
9430
9454
|
if (typeof val !== "object" || val === null) continue;
|
|
9431
9455
|
const childTreePath = treePath ? `${treePath}.${key}` : key;
|
|
9432
9456
|
const childNode = val;
|
|
9433
|
-
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9457
|
+
const childPath = getPathString(childNode);
|
|
9458
|
+
if (childPath) {
|
|
9459
|
+
if (hasPathPrefix(childPath, buildDir)) continue;
|
|
9460
|
+
const isOptional = typeof childNode.$path === "object";
|
|
9461
|
+
if (!isOptional) {
|
|
9462
|
+
const absolutePath = import_path2.default.resolve(outputDir, childPath);
|
|
9463
|
+
if (!import_fs2.default.existsSync(absolutePath)) {
|
|
9464
|
+
delete node[key];
|
|
9465
|
+
removed.push({
|
|
9466
|
+
treePath: childTreePath,
|
|
9467
|
+
rojoPath: childPath
|
|
9468
|
+
});
|
|
9469
|
+
continue;
|
|
9470
|
+
}
|
|
9443
9471
|
}
|
|
9444
9472
|
}
|
|
9445
9473
|
pruneObject(childNode, buildDir, outputDir, removed, childTreePath);
|
|
@@ -9462,14 +9490,15 @@ function findMissingPaths(node, buildDir, outputDir, missing = [], treePath = ""
|
|
|
9462
9490
|
if (typeof val !== "object" || val === null) continue;
|
|
9463
9491
|
const childTreePath = treePath ? `${treePath}.${key}` : key;
|
|
9464
9492
|
const childNode = val;
|
|
9465
|
-
|
|
9466
|
-
|
|
9493
|
+
const childPath = getPathString(childNode);
|
|
9494
|
+
if (childPath && hasPathPrefix(childPath, buildDir)) {
|
|
9495
|
+
const absolutePath = import_path2.default.resolve(outputDir, childPath);
|
|
9467
9496
|
if (!import_fs2.default.existsSync(absolutePath)) {
|
|
9468
9497
|
missing.push({
|
|
9469
9498
|
parent: node,
|
|
9470
9499
|
key,
|
|
9471
9500
|
treePath: childTreePath,
|
|
9472
|
-
path:
|
|
9501
|
+
path: childPath,
|
|
9473
9502
|
absolutePath
|
|
9474
9503
|
});
|
|
9475
9504
|
}
|
|
@@ -9488,6 +9517,7 @@ var isScript = (filename) => /\.(tsx?|luau|lua)$/i.test(filename) && !filename.t
|
|
|
9488
9517
|
var isModel = (filename) => /\.(rbxm|rbxmx)$/i.test(filename);
|
|
9489
9518
|
function isData(filename) {
|
|
9490
9519
|
if (isScript(filename) || isModel(filename)) return false;
|
|
9520
|
+
if (filename.toLowerCase().endsWith(".d.ts")) return false;
|
|
9491
9521
|
if (filename.startsWith(".")) return false;
|
|
9492
9522
|
return /\.[a-z0-9]+$/i.test(filename);
|
|
9493
9523
|
}
|
|
@@ -9505,7 +9535,7 @@ function getRojoBaseName(filename) {
|
|
|
9505
9535
|
}
|
|
9506
9536
|
return filename;
|
|
9507
9537
|
}
|
|
9508
|
-
function collapseFolders(node, buildDir, outputDir) {
|
|
9538
|
+
function collapseFolders(node, buildDir, outputDir, isIgnored = () => false) {
|
|
9509
9539
|
let childCount = 0;
|
|
9510
9540
|
let canCollapse = node.$path === void 0;
|
|
9511
9541
|
let commonDir = null;
|
|
@@ -9514,12 +9544,13 @@ function collapseFolders(node, buildDir, outputDir) {
|
|
|
9514
9544
|
const val = node[key];
|
|
9515
9545
|
if (typeof val !== "object" || val === null) continue;
|
|
9516
9546
|
const childNode = val;
|
|
9517
|
-
collapseFolders(childNode, buildDir, outputDir);
|
|
9547
|
+
collapseFolders(childNode, buildDir, outputDir, isIgnored);
|
|
9518
9548
|
childCount++;
|
|
9519
|
-
|
|
9549
|
+
const childPath = getPathString(childNode);
|
|
9550
|
+
if (!childPath) {
|
|
9520
9551
|
canCollapse = false;
|
|
9521
9552
|
} else {
|
|
9522
|
-
const childAbsPath = import_path2.default.resolve(outputDir,
|
|
9553
|
+
const childAbsPath = import_path2.default.resolve(outputDir, childPath);
|
|
9523
9554
|
const parentDir = import_path2.default.dirname(childAbsPath);
|
|
9524
9555
|
if (commonDir === null) {
|
|
9525
9556
|
commonDir = parentDir;
|
|
@@ -9539,10 +9570,11 @@ function collapseFolders(node, buildDir, outputDir) {
|
|
|
9539
9570
|
if (!hasPathPrefix(toPosix(commonDir), toPosix(absoluteBuildDir))) {
|
|
9540
9571
|
return;
|
|
9541
9572
|
}
|
|
9573
|
+
const relativeCommonDir = toPosix(import_path2.default.relative(outputDir, commonDir));
|
|
9542
9574
|
try {
|
|
9543
9575
|
const diskItems = import_fs2.default.readdirSync(commonDir);
|
|
9544
9576
|
const visibleDiskItems = diskItems.filter(
|
|
9545
|
-
(item) => !item.startsWith(".")
|
|
9577
|
+
(item) => !item.startsWith(".") && !isIgnored(toPosix(import_path2.default.join(relativeCommonDir, item)))
|
|
9546
9578
|
);
|
|
9547
9579
|
if (visibleDiskItems.length !== childCount) {
|
|
9548
9580
|
return;
|
|
@@ -9550,13 +9582,12 @@ function collapseFolders(node, buildDir, outputDir) {
|
|
|
9550
9582
|
} catch {
|
|
9551
9583
|
return;
|
|
9552
9584
|
}
|
|
9553
|
-
const relativeCommonDir = toPosix(import_path2.default.relative(outputDir, commonDir));
|
|
9554
9585
|
for (const key in node) {
|
|
9555
9586
|
if (!key.startsWith("$")) {
|
|
9556
9587
|
delete node[key];
|
|
9557
9588
|
}
|
|
9558
9589
|
}
|
|
9559
|
-
node.$path = relativeCommonDir;
|
|
9590
|
+
node.$path = { optional: relativeCommonDir };
|
|
9560
9591
|
delete node.$className;
|
|
9561
9592
|
}
|
|
9562
9593
|
function findExposedDataFiles(node, exposed = []) {
|
|
@@ -9565,8 +9596,9 @@ function findExposedDataFiles(node, exposed = []) {
|
|
|
9565
9596
|
const val = node[key];
|
|
9566
9597
|
if (typeof val !== "object" || val === null) continue;
|
|
9567
9598
|
const childNode = val;
|
|
9568
|
-
|
|
9569
|
-
|
|
9599
|
+
const childPath = getPathString(childNode);
|
|
9600
|
+
if (childPath && isData(childPath)) {
|
|
9601
|
+
exposed.push({ parent: node, key, path: childPath });
|
|
9570
9602
|
}
|
|
9571
9603
|
findExposedDataFiles(childNode, exposed);
|
|
9572
9604
|
}
|
|
@@ -10003,6 +10035,24 @@ async function build(targetConfig, baseProjectTree, config, env, sourcePaths, cl
|
|
|
10003
10035
|
modeCopy.build = cliArgs.build;
|
|
10004
10036
|
}
|
|
10005
10037
|
const rojoTree = structuredClone(baseProjectTree);
|
|
10038
|
+
for (const targetService of Object.values(services)) {
|
|
10039
|
+
let current = rojoTree.tree;
|
|
10040
|
+
if (serviceParents[targetService]) {
|
|
10041
|
+
current = getOrCreateNode(current, serviceParents[targetService]);
|
|
10042
|
+
}
|
|
10043
|
+
getOrCreateNode(current, targetService);
|
|
10044
|
+
}
|
|
10045
|
+
if (config.aliases) {
|
|
10046
|
+
for (const aliasPath of Object.values(config.aliases)) {
|
|
10047
|
+
if (typeof aliasPath === "string") {
|
|
10048
|
+
const parts = aliasPath.split(".");
|
|
10049
|
+
let current = rojoTree.tree;
|
|
10050
|
+
for (const part of parts) {
|
|
10051
|
+
current = getOrCreateNode(current, part);
|
|
10052
|
+
}
|
|
10053
|
+
}
|
|
10054
|
+
}
|
|
10055
|
+
}
|
|
10006
10056
|
const mergedTags = { ...config.tags || {}, ...modeCopy.tags || {} };
|
|
10007
10057
|
const knownTags = new Set(
|
|
10008
10058
|
Object.keys(config.tags || {}).map((t) => t.toLowerCase())
|
|
@@ -10038,6 +10088,7 @@ async function build(targetConfig, baseProjectTree, config, env, sourcePaths, cl
|
|
|
10038
10088
|
};
|
|
10039
10089
|
const combinedGlobIgnorePaths = Array.from(
|
|
10040
10090
|
/* @__PURE__ */ new Set([
|
|
10091
|
+
...rojoTree.globIgnorePaths || [],
|
|
10041
10092
|
...config.globIgnorePaths || [],
|
|
10042
10093
|
...modeCopy.globIgnorePaths || []
|
|
10043
10094
|
])
|
|
@@ -10067,7 +10118,6 @@ async function build(targetConfig, baseProjectTree, config, env, sourcePaths, cl
|
|
|
10067
10118
|
listings,
|
|
10068
10119
|
(filepath, isInit, isSync) => {
|
|
10069
10120
|
const relativePath2 = import_path4.default.relative(sourcePath, filepath);
|
|
10070
|
-
if (relativePath2 && isIgnored(toPosix(relativePath2))) return;
|
|
10071
10121
|
const {
|
|
10072
10122
|
targetService,
|
|
10073
10123
|
wrapperFolder,
|
|
@@ -10077,7 +10127,8 @@ async function build(targetConfig, baseProjectTree, config, env, sourcePaths, cl
|
|
|
10077
10127
|
dropped,
|
|
10078
10128
|
unwrap
|
|
10079
10129
|
} = resolveRoute(relativePath2, isInit, newContext, isSync);
|
|
10080
|
-
if (dropped)
|
|
10130
|
+
if (dropped || projectPath && isIgnored(toPosix(projectPath)))
|
|
10131
|
+
return;
|
|
10081
10132
|
fileCount++;
|
|
10082
10133
|
let current = rojoTree.tree;
|
|
10083
10134
|
if (serviceParents[targetService]) {
|
|
@@ -10109,10 +10160,13 @@ async function build(targetConfig, baseProjectTree, config, env, sourcePaths, cl
|
|
|
10109
10160
|
const existingNode = existingNodeRaw || {};
|
|
10110
10161
|
const newNode = {
|
|
10111
10162
|
...existingNode,
|
|
10112
|
-
$path: projectPath
|
|
10163
|
+
$path: { optional: projectPath }
|
|
10113
10164
|
};
|
|
10114
10165
|
if (newNode.$className === "Folder") {
|
|
10115
10166
|
delete newNode.$className;
|
|
10167
|
+
if (newNode.$ignoreUnknownInstances === false) {
|
|
10168
|
+
delete newNode.$ignoreUnknownInstances;
|
|
10169
|
+
}
|
|
10116
10170
|
}
|
|
10117
10171
|
current[nodeName] = newNode;
|
|
10118
10172
|
nodeOrigins.set(newNode, {
|
|
@@ -10130,8 +10184,11 @@ async function build(targetConfig, baseProjectTree, config, env, sourcePaths, cl
|
|
|
10130
10184
|
outputDir,
|
|
10131
10185
|
removed
|
|
10132
10186
|
);
|
|
10133
|
-
collapseFolders(prunedTree, context.build, outputDir);
|
|
10187
|
+
collapseFolders(prunedTree, context.build, outputDir, isIgnored);
|
|
10134
10188
|
rojoTree.tree = prunedTree;
|
|
10189
|
+
if (combinedGlobIgnorePaths.length > 0) {
|
|
10190
|
+
rojoTree.globIgnorePaths = combinedGlobIgnorePaths;
|
|
10191
|
+
}
|
|
10135
10192
|
const sortedTree = sortObject(rojoTree);
|
|
10136
10193
|
const missingPaths = findMissingPaths(
|
|
10137
10194
|
sortedTree.tree,
|
|
@@ -10242,7 +10299,7 @@ var ConsoleLogger = class extends AbstractLogger {
|
|
|
10242
10299
|
};
|
|
10243
10300
|
|
|
10244
10301
|
// src/execute.ts
|
|
10245
|
-
async function execute(sourcePaths, env, activeModes, baseProjectTree, config, cliArgs, anchor, logger = new ConsoleLogger()) {
|
|
10302
|
+
async function execute(sourcePaths, env, activeModes, baseProjectTree, config, cliArgs, anchor, logger = new ConsoleLogger(), isInitialRun = false) {
|
|
10246
10303
|
try {
|
|
10247
10304
|
for (const activeMode of activeModes) {
|
|
10248
10305
|
const targetConfig = activeMode.config;
|
|
@@ -10281,15 +10338,16 @@ async function execute(sourcePaths, env, activeModes, baseProjectTree, config, c
|
|
|
10281
10338
|
}
|
|
10282
10339
|
}
|
|
10283
10340
|
if (buildResult.missingPaths.length > 0) {
|
|
10341
|
+
const isCompiledProject = env.isTsProject || env.isDarkluaProject;
|
|
10284
10342
|
for (const item of buildResult.missingPaths) {
|
|
10285
10343
|
const ext = import_path5.default.extname(item.absolutePath).toLowerCase();
|
|
10286
|
-
if (ext === ".luau" || ext === ".lua") {
|
|
10344
|
+
if (isCompiledProject && (ext === ".luau" || ext === ".lua")) {
|
|
10287
10345
|
const dir = import_path5.default.dirname(item.absolutePath);
|
|
10288
10346
|
if (!import_fs4.default.existsSync(dir)) {
|
|
10289
10347
|
import_fs4.default.mkdirSync(dir, { recursive: true });
|
|
10290
10348
|
}
|
|
10291
10349
|
import_fs4.default.writeFileSync(item.absolutePath, "");
|
|
10292
|
-
} else if (ext === "") {
|
|
10350
|
+
} else if (isCompiledProject && ext === "") {
|
|
10293
10351
|
if (!import_fs4.default.existsSync(item.absolutePath)) {
|
|
10294
10352
|
import_fs4.default.mkdirSync(item.absolutePath, {
|
|
10295
10353
|
recursive: true
|
|
@@ -10312,14 +10370,18 @@ async function execute(sourcePaths, env, activeModes, baseProjectTree, config, c
|
|
|
10312
10370
|
shouldWrite = false;
|
|
10313
10371
|
}
|
|
10314
10372
|
}
|
|
10315
|
-
if (!shouldWrite) {
|
|
10373
|
+
if (!shouldWrite && cliArgs.watch && !isInitialRun) {
|
|
10316
10374
|
continue;
|
|
10317
10375
|
}
|
|
10318
|
-
|
|
10319
|
-
|
|
10320
|
-
import_fs4.default.
|
|
10376
|
+
if (shouldWrite) {
|
|
10377
|
+
const outputDir = import_path5.default.dirname(buildResult.output);
|
|
10378
|
+
if (!import_fs4.default.existsSync(outputDir)) {
|
|
10379
|
+
import_fs4.default.mkdirSync(outputDir, { recursive: true });
|
|
10380
|
+
}
|
|
10381
|
+
const tempOutput = `${buildResult.output}.tmp`;
|
|
10382
|
+
import_fs4.default.writeFileSync(tempOutput, finalContent);
|
|
10383
|
+
import_fs4.default.renameSync(tempOutput, buildResult.output);
|
|
10321
10384
|
}
|
|
10322
|
-
import_fs4.default.writeFileSync(buildResult.output, finalContent);
|
|
10323
10385
|
const totalRemoved = buildResult.removed.length + dropped.length;
|
|
10324
10386
|
if (totalRemoved > 0) {
|
|
10325
10387
|
if (cliArgs.watch) {
|
|
@@ -10341,22 +10403,30 @@ async function execute(sourcePaths, env, activeModes, baseProjectTree, config, c
|
|
|
10341
10403
|
logger.warn(collision);
|
|
10342
10404
|
}
|
|
10343
10405
|
}
|
|
10344
|
-
if (
|
|
10406
|
+
if (!isInitialRun) {
|
|
10345
10407
|
const outputName = import_path5.default.basename(buildResult.output);
|
|
10346
10408
|
logger.success(
|
|
10347
10409
|
`[${modeName}] Rebuilt "${buildResult.name}" -> ${outputName}`
|
|
10348
10410
|
);
|
|
10349
10411
|
} else {
|
|
10350
|
-
|
|
10351
|
-
|
|
10352
|
-
|
|
10353
|
-
|
|
10354
|
-
(
|
|
10355
|
-
|
|
10356
|
-
|
|
10357
|
-
|
|
10412
|
+
if (shouldWrite) {
|
|
10413
|
+
logger.success(
|
|
10414
|
+
`Generated Rojo tree for "${buildResult.name}"`
|
|
10415
|
+
);
|
|
10416
|
+
logger.info(` Processed: ${buildResult.fileCount} files`);
|
|
10417
|
+
logger.info(` Build dir: ${buildResult.buildDir}`);
|
|
10418
|
+
const activeTags = Object.keys(
|
|
10419
|
+
targetConfig.tags || {}
|
|
10420
|
+
).filter((t) => targetConfig.tags[t]);
|
|
10421
|
+
if (activeTags.length > 0) {
|
|
10422
|
+
logger.info(` Tags: ${activeTags.join(", ")}`);
|
|
10423
|
+
}
|
|
10424
|
+
logger.info(` Output to: ${buildResult.output}`);
|
|
10425
|
+
} else {
|
|
10426
|
+
logger.success(
|
|
10427
|
+
`Rojo tree for "${buildResult.name}" is already up to date`
|
|
10428
|
+
);
|
|
10358
10429
|
}
|
|
10359
|
-
logger.info(` Output to: ${buildResult.output}`);
|
|
10360
10430
|
}
|
|
10361
10431
|
}
|
|
10362
10432
|
return true;
|
|
@@ -10440,7 +10510,8 @@ async function main() {
|
|
|
10440
10510
|
config,
|
|
10441
10511
|
cliArgs,
|
|
10442
10512
|
anchor,
|
|
10443
|
-
logger
|
|
10513
|
+
logger,
|
|
10514
|
+
true
|
|
10444
10515
|
);
|
|
10445
10516
|
if (!success) {
|
|
10446
10517
|
process.exit(1);
|
|
@@ -10449,9 +10520,14 @@ async function main() {
|
|
|
10449
10520
|
logger.info(
|
|
10450
10521
|
`Watching for file changes in: "${sourceDirs.join(", ")}" (Ctrl+C to stop)`
|
|
10451
10522
|
);
|
|
10523
|
+
const isWindows = process.platform === "win32";
|
|
10452
10524
|
const watcher = import_chokidar.default.watch(sourcePaths, {
|
|
10453
10525
|
persistent: true,
|
|
10454
|
-
ignoreInitial: true
|
|
10526
|
+
ignoreInitial: true,
|
|
10527
|
+
// Use polling on Windows to avoid "folder in use" locks
|
|
10528
|
+
usePolling: isWindows,
|
|
10529
|
+
interval: isWindows ? 400 : void 0,
|
|
10530
|
+
binaryInterval: isWindows ? 800 : void 0
|
|
10455
10531
|
});
|
|
10456
10532
|
let debounceTimeout;
|
|
10457
10533
|
let isBuilding = false;
|
|
@@ -10470,7 +10546,8 @@ async function main() {
|
|
|
10470
10546
|
config,
|
|
10471
10547
|
cliArgs,
|
|
10472
10548
|
anchor,
|
|
10473
|
-
logger
|
|
10549
|
+
logger,
|
|
10550
|
+
false
|
|
10474
10551
|
);
|
|
10475
10552
|
} catch (err) {
|
|
10476
10553
|
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,4 @@
|
|
|
1
|
-
export const version = "1.4.
|
|
1
|
+
export const version = "1.4.2";
|
|
2
2
|
export const defaultProject = {
|
|
3
3
|
name: "roblox-game",
|
|
4
4
|
tree: { $className: "DataModel" },
|
|
@@ -52,6 +52,10 @@ export const serviceParents = {
|
|
|
52
52
|
StarterPlayerScripts: "StarterPlayer",
|
|
53
53
|
StarterCharacterScripts: "StarterPlayer",
|
|
54
54
|
};
|
|
55
|
+
export const combinedServices = new Set([
|
|
56
|
+
...Object.values(services),
|
|
57
|
+
...Object.values(serviceParents),
|
|
58
|
+
]);
|
|
55
59
|
export const serverContainers = new Set([
|
|
56
60
|
"ServerScriptService",
|
|
57
61
|
"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 } 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,6 +118,8 @@ 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
|
return /\.[a-z0-9]+$/i.test(filename);
|
|
@@ -103,7 +140,7 @@ function getRojoBaseName(filename) {
|
|
|
103
140
|
}
|
|
104
141
|
return filename;
|
|
105
142
|
}
|
|
106
|
-
export function collapseFolders(node, buildDir, outputDir) {
|
|
143
|
+
export function collapseFolders(node, buildDir, outputDir, isIgnored = () => false) {
|
|
107
144
|
let childCount = 0;
|
|
108
145
|
let canCollapse = node.$path === undefined; // Prevent overwriting a folder with an explicit $path
|
|
109
146
|
let commonDir = null;
|
|
@@ -115,13 +152,14 @@ export function collapseFolders(node, buildDir, outputDir) {
|
|
|
115
152
|
continue;
|
|
116
153
|
const childNode = val;
|
|
117
154
|
// Process deepest nested children first
|
|
118
|
-
collapseFolders(childNode, buildDir, outputDir);
|
|
155
|
+
collapseFolders(childNode, buildDir, outputDir, isIgnored);
|
|
119
156
|
childCount++;
|
|
120
|
-
|
|
157
|
+
const childPath = getPathString(childNode);
|
|
158
|
+
if (!childPath) {
|
|
121
159
|
canCollapse = false;
|
|
122
160
|
}
|
|
123
161
|
else {
|
|
124
|
-
const childAbsPath = path.resolve(outputDir,
|
|
162
|
+
const childAbsPath = path.resolve(outputDir, childPath);
|
|
125
163
|
const parentDir = path.dirname(childAbsPath);
|
|
126
164
|
// All children should share the same directory
|
|
127
165
|
if (commonDir === null) {
|
|
@@ -143,10 +181,12 @@ export function collapseFolders(node, buildDir, outputDir) {
|
|
|
143
181
|
if (!hasPathPrefix(toPosix(commonDir), toPosix(absoluteBuildDir))) {
|
|
144
182
|
return;
|
|
145
183
|
}
|
|
184
|
+
const relativeCommonDir = toPosix(path.relative(outputDir, commonDir));
|
|
146
185
|
try {
|
|
147
186
|
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(".")
|
|
187
|
+
// filter out marker files and ignored files so they do not prevent folders from collapsing
|
|
188
|
+
const visibleDiskItems = diskItems.filter((item) => !item.startsWith(".") &&
|
|
189
|
+
!isIgnored(toPosix(path.join(relativeCommonDir, item))));
|
|
150
190
|
if (visibleDiskItems.length !== childCount) {
|
|
151
191
|
return;
|
|
152
192
|
}
|
|
@@ -155,13 +195,12 @@ export function collapseFolders(node, buildDir, outputDir) {
|
|
|
155
195
|
return;
|
|
156
196
|
}
|
|
157
197
|
// Replace all child files with a single folder $path
|
|
158
|
-
const relativeCommonDir = toPosix(path.relative(outputDir, commonDir));
|
|
159
198
|
for (const key in node) {
|
|
160
199
|
if (!key.startsWith("$")) {
|
|
161
200
|
delete node[key];
|
|
162
201
|
}
|
|
163
202
|
}
|
|
164
|
-
node.$path = relativeCommonDir;
|
|
203
|
+
node.$path = { optional: relativeCommonDir };
|
|
165
204
|
delete node.$className;
|
|
166
205
|
}
|
|
167
206
|
export function findExposedDataFiles(node, exposed = []) {
|
|
@@ -172,9 +211,10 @@ export function findExposedDataFiles(node, exposed = []) {
|
|
|
172
211
|
if (typeof val !== "object" || val === null)
|
|
173
212
|
continue;
|
|
174
213
|
const childNode = val;
|
|
214
|
+
const childPath = getPathString(childNode);
|
|
175
215
|
// Check if the current node exposes a raw data file path
|
|
176
|
-
if (
|
|
177
|
-
exposed.push({ parent: node, key, path:
|
|
216
|
+
if (childPath && isData(childPath)) {
|
|
217
|
+
exposed.push({ parent: node, key, path: childPath });
|
|
178
218
|
}
|
|
179
219
|
findExposedDataFiles(childNode, exposed);
|
|
180
220
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ldgerrits/rogen",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
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",
|