@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/dist/cli.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { parseArgs } from "util";
|
|
2
|
+
export function printHelp(logger) {
|
|
3
|
+
const helpText = `
|
|
4
|
+
Rogen - Feature-based folder routing for Roblox
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
rogen [command] [options]
|
|
8
|
+
|
|
9
|
+
Commands:
|
|
10
|
+
init Generate a .rogen.json config file
|
|
11
|
+
watch Watch the source and generate automatically
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
-c, --config <path> Specify custom config file
|
|
15
|
+
-p, --project <path> Specify path to Rojo project
|
|
16
|
+
-t, --tag <tag> Activate environment tags
|
|
17
|
+
-m, --mode <mode> Override modes (luau, ts, darklua, or custom)
|
|
18
|
+
-s, --source <path> Override the directory containing uncompiled code
|
|
19
|
+
-b, --build <path> Override output directory for transpiled code
|
|
20
|
+
-o, --output <path> Override path of Rojo project file
|
|
21
|
+
-h, --help Print help
|
|
22
|
+
-v, --version Print version
|
|
23
|
+
`;
|
|
24
|
+
if (logger) {
|
|
25
|
+
logger.info(helpText.trim());
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
console.log(helpText.trim());
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function parseCliArgs(args = process.argv.slice(2), logger) {
|
|
32
|
+
const options = {
|
|
33
|
+
help: { type: "boolean", short: "h" },
|
|
34
|
+
version: { type: "boolean", short: "v" },
|
|
35
|
+
init: { type: "boolean", short: "i" },
|
|
36
|
+
watch: { type: "boolean", short: "w" },
|
|
37
|
+
config: { type: "string", short: "c" },
|
|
38
|
+
mode: { type: "string", short: "m", multiple: true },
|
|
39
|
+
source: { type: "string", short: "s", multiple: true },
|
|
40
|
+
tag: { type: "string", short: "t", multiple: true },
|
|
41
|
+
project: { type: "string", short: "p" },
|
|
42
|
+
build: { type: "string", short: "b" },
|
|
43
|
+
output: { type: "string", short: "o" },
|
|
44
|
+
};
|
|
45
|
+
try {
|
|
46
|
+
const { values, positionals } = parseArgs({
|
|
47
|
+
args,
|
|
48
|
+
options,
|
|
49
|
+
allowPositionals: true,
|
|
50
|
+
strict: true,
|
|
51
|
+
});
|
|
52
|
+
const parsedArgs = values;
|
|
53
|
+
if (positionals.length > 0) {
|
|
54
|
+
const subcommand = positionals[0].toLowerCase();
|
|
55
|
+
if (subcommand === "init") {
|
|
56
|
+
parsedArgs.init = true;
|
|
57
|
+
}
|
|
58
|
+
else if (subcommand === "watch") {
|
|
59
|
+
parsedArgs.watch = true;
|
|
60
|
+
}
|
|
61
|
+
else if (subcommand === "help") {
|
|
62
|
+
parsedArgs.help = true;
|
|
63
|
+
}
|
|
64
|
+
else if (subcommand === "version") {
|
|
65
|
+
parsedArgs.version = true;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const errorMsg = `unknown subcommand "${positionals[0]}".\nRun 'rogen --help' to see a list of available commands.`;
|
|
69
|
+
if (logger) {
|
|
70
|
+
logger.error(errorMsg);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
console.error(errorMsg);
|
|
74
|
+
}
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return parsedArgs;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
let errCode;
|
|
82
|
+
let errMsg = String(error);
|
|
83
|
+
if (typeof error === "object" && error !== null) {
|
|
84
|
+
const errObj = error;
|
|
85
|
+
errCode = typeof errObj.code === "string" ? errObj.code : undefined;
|
|
86
|
+
errMsg =
|
|
87
|
+
typeof errObj.message === "string"
|
|
88
|
+
? errObj.message
|
|
89
|
+
: String(error);
|
|
90
|
+
}
|
|
91
|
+
if (errCode === "ERR_PARSE_ARGS_UNKNOWN_OPTION" ||
|
|
92
|
+
errMsg.includes("Unknown option")) {
|
|
93
|
+
const cleanMsg = errMsg.replace(/^TypeError \[ERR_PARSE_ARGS_UNKNOWN_OPTION\]:\s*/, "");
|
|
94
|
+
const errorMsg = `${cleanMsg}\nRun 'rogen --help' to see a list of available commands and options.`;
|
|
95
|
+
if (logger) {
|
|
96
|
+
logger.error(errorMsg);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
console.error(errorMsg);
|
|
100
|
+
}
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
if (logger) {
|
|
104
|
+
logger.error(errMsg);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
console.error(errMsg);
|
|
108
|
+
}
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { defaultConfig, defaultProject } from "./constants.js";
|
|
4
|
+
const CONFIG_KEYS_MAP = {
|
|
5
|
+
source: true,
|
|
6
|
+
tags: true,
|
|
7
|
+
verbatim: true,
|
|
8
|
+
casing: true,
|
|
9
|
+
unwrap: true,
|
|
10
|
+
aliases: true,
|
|
11
|
+
globIgnorePaths: true,
|
|
12
|
+
luau: true,
|
|
13
|
+
ts: true,
|
|
14
|
+
darklua: true,
|
|
15
|
+
project: true,
|
|
16
|
+
};
|
|
17
|
+
const KEYS = Object.keys(CONFIG_KEYS_MAP);
|
|
18
|
+
const LEGACY_KEYS = {
|
|
19
|
+
keepRouteNames: "verbatim",
|
|
20
|
+
keepSuffixes: "verbatim",
|
|
21
|
+
template: "project",
|
|
22
|
+
};
|
|
23
|
+
function getClosestMatch(input, targets, distanceThreshold) {
|
|
24
|
+
let closest = null;
|
|
25
|
+
let minDistance = Infinity;
|
|
26
|
+
for (const target of targets) {
|
|
27
|
+
const matrix = Array.from({ length: input.length + 1 }, () => Array(target.length + 1).fill(0));
|
|
28
|
+
for (let i = 0; i <= input.length; i++)
|
|
29
|
+
matrix[i][0] = i;
|
|
30
|
+
for (let j = 0; j <= target.length; j++)
|
|
31
|
+
matrix[0][j] = j;
|
|
32
|
+
for (let i = 1; i <= input.length; i++) {
|
|
33
|
+
for (let j = 1; j <= target.length; j++) {
|
|
34
|
+
const cost = input[i - 1] === target[j - 1] ? 0 : 1;
|
|
35
|
+
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, // deletion
|
|
36
|
+
matrix[i][j - 1] + 1, // insertion
|
|
37
|
+
matrix[i - 1][j - 1] + cost // substitution
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const distance = matrix[input.length][target.length];
|
|
42
|
+
if (distance < minDistance) {
|
|
43
|
+
minDistance = distance;
|
|
44
|
+
closest = target;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return minDistance <= distanceThreshold ? closest : null;
|
|
48
|
+
}
|
|
49
|
+
function isValidRojoTree(value) {
|
|
50
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
const record = value;
|
|
54
|
+
return (typeof record.tree === "object" &&
|
|
55
|
+
record.tree !== null &&
|
|
56
|
+
!Array.isArray(record.tree));
|
|
57
|
+
}
|
|
58
|
+
export function isMode(value) {
|
|
59
|
+
return (typeof value === "object" &&
|
|
60
|
+
value !== null &&
|
|
61
|
+
!Array.isArray(value) &&
|
|
62
|
+
typeof value.output === "string" &&
|
|
63
|
+
typeof value.build === "string");
|
|
64
|
+
}
|
|
65
|
+
export function resolveConfigPath(customPathArg, logger) {
|
|
66
|
+
const cwd = process.cwd();
|
|
67
|
+
if (customPathArg) {
|
|
68
|
+
const resolvedPath = path.resolve(cwd, customPathArg);
|
|
69
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
70
|
+
throw new Error(`specified config file not found: ${customPathArg}`);
|
|
71
|
+
}
|
|
72
|
+
return resolvedPath;
|
|
73
|
+
}
|
|
74
|
+
const defaultPath = path.resolve(cwd, ".rogen.json");
|
|
75
|
+
if (fs.existsSync(defaultPath)) {
|
|
76
|
+
return defaultPath;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const directoryFiles = fs.readdirSync(cwd);
|
|
80
|
+
const matchedConfig = directoryFiles.find((file) => file.endsWith(".rogen.json"));
|
|
81
|
+
if (matchedConfig) {
|
|
82
|
+
return path.resolve(cwd, matchedConfig);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
const msg = error instanceof Error ? error.message : "Unknown Error";
|
|
87
|
+
if (logger) {
|
|
88
|
+
logger.error(`failed to scan directory for .rogen.json - ${msg}`);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
console.error(`failed to scan directory for .rogen.json - ${msg}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
export function createFallbackConfig(cwd) {
|
|
97
|
+
const isTs = fs.existsSync(path.join(cwd, "tsconfig.json"));
|
|
98
|
+
const isWally = fs.existsSync(path.join(cwd, "wally.toml"));
|
|
99
|
+
const isPesde = fs.existsSync(path.join(cwd, "pesde.toml"));
|
|
100
|
+
const tree = structuredClone(defaultProject.tree);
|
|
101
|
+
const project = {
|
|
102
|
+
name: path.basename(cwd) || defaultProject.name,
|
|
103
|
+
tree: tree,
|
|
104
|
+
};
|
|
105
|
+
if (isTs) {
|
|
106
|
+
project.globIgnorePaths = ["**/package.json", "**/tsconfig.json"];
|
|
107
|
+
const hasRbxts = fs.existsSync(path.join(cwd, "node_modules", "@rbxts"));
|
|
108
|
+
const hasFlamework = fs.existsSync(path.join(cwd, "node_modules", "@flamework"));
|
|
109
|
+
const hasRbxtsJs = fs.existsSync(path.join(cwd, "node_modules", "@rbxts-js"));
|
|
110
|
+
const rbxtsIncludeNode = { $path: "include" };
|
|
111
|
+
if (hasRbxts || hasFlamework || hasRbxtsJs) {
|
|
112
|
+
const nodeModulesNode = { $className: "Folder" };
|
|
113
|
+
if (hasRbxts)
|
|
114
|
+
nodeModulesNode["@rbxts"] = { $path: "node_modules/@rbxts" };
|
|
115
|
+
if (hasFlamework)
|
|
116
|
+
nodeModulesNode["@flamework"] = {
|
|
117
|
+
$path: "node_modules/@flamework",
|
|
118
|
+
};
|
|
119
|
+
if (hasRbxtsJs)
|
|
120
|
+
nodeModulesNode["@rbxts-js"] = {
|
|
121
|
+
$path: "node_modules/@rbxts-js",
|
|
122
|
+
};
|
|
123
|
+
rbxtsIncludeNode.node_modules = nodeModulesNode;
|
|
124
|
+
}
|
|
125
|
+
tree.ReplicatedStorage = {
|
|
126
|
+
...(tree.ReplicatedStorage || {}),
|
|
127
|
+
rbxts_include: rbxtsIncludeNode,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (isWally) {
|
|
131
|
+
if (fs.existsSync(path.join(cwd, "Packages"))) {
|
|
132
|
+
tree.ReplicatedStorage = {
|
|
133
|
+
...(tree.ReplicatedStorage || {}),
|
|
134
|
+
Packages: { $path: "Packages" },
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if (fs.existsSync(path.join(cwd, "ServerPackages"))) {
|
|
138
|
+
tree.ServerScriptService = {
|
|
139
|
+
...(tree.ServerScriptService || {}),
|
|
140
|
+
ServerPackages: { $path: "ServerPackages" },
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (isPesde) {
|
|
145
|
+
if (fs.existsSync(path.join(cwd, "roblox_packages"))) {
|
|
146
|
+
tree.ReplicatedStorage = {
|
|
147
|
+
...(tree.ReplicatedStorage || {}),
|
|
148
|
+
Packages: { $path: "roblox_packages" },
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (fs.existsSync(path.join(cwd, "roblox_server_packages"))) {
|
|
152
|
+
tree.ServerScriptService = {
|
|
153
|
+
...(tree.ServerScriptService || {}),
|
|
154
|
+
ServerPackages: { $path: "roblox_server_packages" },
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const config = {
|
|
159
|
+
...structuredClone(defaultConfig),
|
|
160
|
+
project: project,
|
|
161
|
+
};
|
|
162
|
+
return config;
|
|
163
|
+
}
|
|
164
|
+
export function loadConfig(configPath, projectPathArg) {
|
|
165
|
+
const anchor = configPath ? path.dirname(configPath) : process.cwd();
|
|
166
|
+
const config = createFallbackConfig(anchor);
|
|
167
|
+
if (configPath) {
|
|
168
|
+
const rawConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
169
|
+
for (const key in rawConfig) {
|
|
170
|
+
if (!KEYS.includes(key)) {
|
|
171
|
+
if (key in LEGACY_KEYS) {
|
|
172
|
+
throw new Error(`key "${key}" has been renamed to "${LEGACY_KEYS[key]}". Please, update your configuration.`);
|
|
173
|
+
}
|
|
174
|
+
const value = rawConfig[key];
|
|
175
|
+
const isObject = typeof value === "object" &&
|
|
176
|
+
value !== null &&
|
|
177
|
+
!Array.isArray(value);
|
|
178
|
+
const modeData = isObject
|
|
179
|
+
? value
|
|
180
|
+
: null;
|
|
181
|
+
// Default modes merging
|
|
182
|
+
if (isObject &&
|
|
183
|
+
(key === "luau" || key === "ts" || key === "darklua")) {
|
|
184
|
+
config[key] = {
|
|
185
|
+
...config[key],
|
|
186
|
+
...modeData,
|
|
187
|
+
};
|
|
188
|
+
config[key].tags =
|
|
189
|
+
modeData &&
|
|
190
|
+
typeof modeData.tags === "object" &&
|
|
191
|
+
modeData.tags !== null &&
|
|
192
|
+
!Array.isArray(modeData.tags)
|
|
193
|
+
? modeData.tags
|
|
194
|
+
: {};
|
|
195
|
+
config[key].globIgnorePaths = Array.isArray(config[key].globIgnorePaths)
|
|
196
|
+
? config[key].globIgnorePaths
|
|
197
|
+
: [];
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
// Custom modes merging
|
|
201
|
+
const intendedAsMode = modeData &&
|
|
202
|
+
("output" in modeData ||
|
|
203
|
+
"build" in modeData ||
|
|
204
|
+
"tags" in modeData ||
|
|
205
|
+
"globIgnorePaths" in modeData);
|
|
206
|
+
if (intendedAsMode) {
|
|
207
|
+
if (typeof modeData.output !== "string") {
|
|
208
|
+
throw new Error(`custom mode "${key}" is missing a valid "output" string.`);
|
|
209
|
+
}
|
|
210
|
+
if (typeof modeData.build !== "string") {
|
|
211
|
+
throw new Error(`custom mode "${key}" is missing a valid "build" string.`);
|
|
212
|
+
}
|
|
213
|
+
config[key] = {
|
|
214
|
+
output: modeData.output,
|
|
215
|
+
build: modeData.build,
|
|
216
|
+
tags: typeof modeData.tags === "object" &&
|
|
217
|
+
modeData.tags !== null &&
|
|
218
|
+
!Array.isArray(modeData.tags)
|
|
219
|
+
? modeData.tags
|
|
220
|
+
: {},
|
|
221
|
+
globIgnorePaths: Array.isArray(modeData.globIgnorePaths)
|
|
222
|
+
? modeData.globIgnorePaths
|
|
223
|
+
: [],
|
|
224
|
+
};
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const closestMatch = getClosestMatch(key, KEYS, 2);
|
|
228
|
+
if (closestMatch) {
|
|
229
|
+
throw new Error(`unknown key "${key}". Did you mean "${closestMatch}"?`);
|
|
230
|
+
}
|
|
231
|
+
throw new Error(`unknown configuration key "${key}".`);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
// Validate keys
|
|
235
|
+
if (key === "source" &&
|
|
236
|
+
typeof rawConfig[key] !== "string" &&
|
|
237
|
+
!Array.isArray(rawConfig[key])) {
|
|
238
|
+
throw new Error(`'source' must be a string or an array of strings.`);
|
|
239
|
+
}
|
|
240
|
+
else if (key === "project" &&
|
|
241
|
+
typeof rawConfig[key] !== "object" &&
|
|
242
|
+
typeof rawConfig[key] !== "string") {
|
|
243
|
+
throw new Error(`'project' must be an inline object or a string path to a JSON file.`);
|
|
244
|
+
}
|
|
245
|
+
else if ((key === "verbatim" || key === "unwrap") &&
|
|
246
|
+
typeof rawConfig[key] !== "boolean") {
|
|
247
|
+
throw new Error(`'${key}' must be a boolean.`);
|
|
248
|
+
}
|
|
249
|
+
else if (key === "casing") {
|
|
250
|
+
if (typeof rawConfig[key] !== "string") {
|
|
251
|
+
throw new Error(`'casing' must be a string.`);
|
|
252
|
+
}
|
|
253
|
+
const casingVal = rawConfig[key]
|
|
254
|
+
.toLowerCase()
|
|
255
|
+
.replace(/[-_\s]/g, "");
|
|
256
|
+
if (casingVal === "pascal" || casingVal === "pascalcase") {
|
|
257
|
+
config.casing = "PascalCase";
|
|
258
|
+
}
|
|
259
|
+
else if (casingVal === "camel" ||
|
|
260
|
+
casingVal === "camelcase") {
|
|
261
|
+
config.casing = "camelCase";
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
throw new Error(`'casing' must be either "PascalCase" or "camelCase".`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else if (key === "globIgnorePaths" &&
|
|
268
|
+
!Array.isArray(rawConfig[key])) {
|
|
269
|
+
throw new Error(`'${key}' must be an array of strings.`);
|
|
270
|
+
}
|
|
271
|
+
else if (key === "tags") {
|
|
272
|
+
if (typeof rawConfig[key] !== "object" ||
|
|
273
|
+
rawConfig[key] === null ||
|
|
274
|
+
Array.isArray(rawConfig[key])) {
|
|
275
|
+
throw new Error(`'tags' must be a key-value object of booleans.`);
|
|
276
|
+
}
|
|
277
|
+
for (const tagKey in rawConfig[key]) {
|
|
278
|
+
if (typeof rawConfig[key][tagKey] !== "boolean") {
|
|
279
|
+
throw new Error(`value for tag "${tagKey}" must be a boolean.`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
config[key] = rawConfig[key];
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// Project handling
|
|
288
|
+
if (projectPathArg) {
|
|
289
|
+
const projectPath = path.resolve(process.cwd(), projectPathArg);
|
|
290
|
+
if (!fs.existsSync(projectPath)) {
|
|
291
|
+
throw new Error(`specified project file not found: ${projectPath}`);
|
|
292
|
+
}
|
|
293
|
+
config.project = JSON.parse(fs.readFileSync(projectPath, "utf-8"));
|
|
294
|
+
}
|
|
295
|
+
else if (typeof config.project === "string") {
|
|
296
|
+
const projectPath = path.resolve(anchor, config.project);
|
|
297
|
+
if (!fs.existsSync(projectPath)) {
|
|
298
|
+
throw new Error(`specified project file not found: ${projectPath}`);
|
|
299
|
+
}
|
|
300
|
+
config.project = JSON.parse(fs.readFileSync(projectPath, "utf-8"));
|
|
301
|
+
}
|
|
302
|
+
if (!isValidRojoTree(config.project)) {
|
|
303
|
+
throw new Error("provided project is not a valid Rojo project. " +
|
|
304
|
+
"It must be a JSON object containing a 'name' and 'tree' property.");
|
|
305
|
+
}
|
|
306
|
+
return { config, anchor };
|
|
307
|
+
}
|
|
308
|
+
export function getEnvironment(anchor, cliModes) {
|
|
309
|
+
if (cliModes && cliModes.length > 0) {
|
|
310
|
+
return {
|
|
311
|
+
isTsProject: cliModes.includes("ts"),
|
|
312
|
+
isDarkluaProject: cliModes.includes("darklua"),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
const isTsProject = fs.existsSync(path.join(anchor, "tsconfig.json"));
|
|
316
|
+
const isDarkluaProject = fs.existsSync(path.join(anchor, ".darklua.json")) ||
|
|
317
|
+
fs.existsSync(path.join(anchor, ".darklua.json5"));
|
|
318
|
+
return { isTsProject, isDarkluaProject };
|
|
319
|
+
}
|
|
320
|
+
export function resolveActiveModes(config, cliModes, env) {
|
|
321
|
+
const activeModes = [];
|
|
322
|
+
if (cliModes && cliModes.length > 0) {
|
|
323
|
+
for (const cliMode of cliModes) {
|
|
324
|
+
const requestedMode = config[cliMode];
|
|
325
|
+
if (!isMode(requestedMode)) {
|
|
326
|
+
throw new Error(`mode "${cliMode}" is not defined or is invalid in your config file.`);
|
|
327
|
+
}
|
|
328
|
+
activeModes.push({ name: cliMode, config: requestedMode });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
for (const key in config) {
|
|
333
|
+
const potentialMode = config[key];
|
|
334
|
+
if (isMode(potentialMode)) {
|
|
335
|
+
if (key === "luau" && env.isTsProject)
|
|
336
|
+
continue;
|
|
337
|
+
if (key === "ts" && !env.isTsProject)
|
|
338
|
+
continue;
|
|
339
|
+
if (key === "darklua" && !env.isDarkluaProject)
|
|
340
|
+
continue;
|
|
341
|
+
activeModes.push({ name: key, config: potentialMode });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (activeModes.length === 0) {
|
|
345
|
+
throw new Error("no output modes defined in configuration file. Add 'luau', 'ts', or custom modes.");
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return activeModes;
|
|
349
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export const version = "1.4.0";
|
|
2
|
+
export const defaultProject = {
|
|
3
|
+
name: "roblox-game",
|
|
4
|
+
tree: { $className: "DataModel" },
|
|
5
|
+
};
|
|
6
|
+
export const defaultConfig = {
|
|
7
|
+
source: ["src"],
|
|
8
|
+
tags: {},
|
|
9
|
+
verbatim: false,
|
|
10
|
+
casing: "camelCase",
|
|
11
|
+
globIgnorePaths: [],
|
|
12
|
+
aliases: {},
|
|
13
|
+
unwrap: false,
|
|
14
|
+
luau: {
|
|
15
|
+
output: "default.project.json",
|
|
16
|
+
build: "src",
|
|
17
|
+
tags: {},
|
|
18
|
+
globIgnorePaths: [],
|
|
19
|
+
},
|
|
20
|
+
ts: {
|
|
21
|
+
output: "default.project.json",
|
|
22
|
+
build: "out",
|
|
23
|
+
tags: {},
|
|
24
|
+
globIgnorePaths: [],
|
|
25
|
+
},
|
|
26
|
+
darklua: {
|
|
27
|
+
output: "build.project.json",
|
|
28
|
+
build: "dist",
|
|
29
|
+
tags: {},
|
|
30
|
+
globIgnorePaths: [],
|
|
31
|
+
},
|
|
32
|
+
project: defaultProject,
|
|
33
|
+
};
|
|
34
|
+
export const services = {
|
|
35
|
+
Server: "ServerScriptService",
|
|
36
|
+
Client: "StarterPlayerScripts",
|
|
37
|
+
Shared: "ReplicatedStorage",
|
|
38
|
+
ServerScriptService: "ServerScriptService",
|
|
39
|
+
ReplicatedStorage: "ReplicatedStorage",
|
|
40
|
+
ReplicatedFirst: "ReplicatedFirst",
|
|
41
|
+
ServerStorage: "ServerStorage",
|
|
42
|
+
StarterGui: "StarterGui",
|
|
43
|
+
StarterPack: "StarterPack",
|
|
44
|
+
StarterPlayerScripts: "StarterPlayerScripts",
|
|
45
|
+
StarterCharacterScripts: "StarterCharacterScripts",
|
|
46
|
+
Workspace: "Workspace",
|
|
47
|
+
Lighting: "Lighting",
|
|
48
|
+
SoundService: "SoundService",
|
|
49
|
+
RobloxPluginGuiService: "RobloxPluginGuiService",
|
|
50
|
+
};
|
|
51
|
+
export const serviceParents = {
|
|
52
|
+
StarterPlayerScripts: "StarterPlayer",
|
|
53
|
+
StarterCharacterScripts: "StarterPlayer",
|
|
54
|
+
};
|
|
55
|
+
export const serverContainers = new Set([
|
|
56
|
+
"ServerScriptService",
|
|
57
|
+
"ServerStorage",
|
|
58
|
+
]);
|
|
59
|
+
export const clientContainers = new Set([
|
|
60
|
+
"StarterPlayer",
|
|
61
|
+
"StarterPlayerScripts",
|
|
62
|
+
"StarterCharacterScripts",
|
|
63
|
+
"StarterGui",
|
|
64
|
+
"StarterPack",
|
|
65
|
+
"ReplicatedFirst",
|
|
66
|
+
]);
|
|
67
|
+
export const serviceAliases = new Set(["server", "client", "shared"]);
|
|
68
|
+
export function generateRoutingMaps(customAliases = {}) {
|
|
69
|
+
const mergedServices = { ...services, ...customAliases };
|
|
70
|
+
const lowerCaseMap = Object.fromEntries(Object.entries(mergedServices).map(([k, v]) => [k.toLowerCase(), v]));
|
|
71
|
+
const mergedKeys = Object.keys(mergedServices).sort((a, b) => b.length - a.length);
|
|
72
|
+
const lowerKeys = Object.keys(lowerCaseMap).sort((a, b) => b.length - a.length);
|
|
73
|
+
const allPrefixKeys = Array.from(new Set([...lowerKeys, ...mergedKeys])).sort((a, b) => b.length - a.length);
|
|
74
|
+
const separatorSuffixRegex = new RegExp(`[\\.\\-_\\+](${lowerKeys.join("|")})$`, "i");
|
|
75
|
+
const pascalCaseSuffixRegex = new RegExp(`(${mergedKeys.join("|")})$`);
|
|
76
|
+
const separatorPrefixRegex = new RegExp(`^(${lowerKeys.join("|")})([\\.\\-_\\+])`, "i");
|
|
77
|
+
const camelCasePrefixRegex = new RegExp(`^(${allPrefixKeys.join("|")})(?=[A-Z])`);
|
|
78
|
+
return {
|
|
79
|
+
mergedServices,
|
|
80
|
+
lowerCaseMap,
|
|
81
|
+
separatorSuffixRegex,
|
|
82
|
+
pascalCaseSuffixRegex,
|
|
83
|
+
separatorPrefixRegex,
|
|
84
|
+
camelCasePrefixRegex,
|
|
85
|
+
};
|
|
86
|
+
}
|
package/dist/execute.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { build } from "./build.js";
|
|
4
|
+
import { ConsoleLogger, getTimeStamp } from "./logger.js";
|
|
5
|
+
export async function execute(sourcePaths, env, activeModes, baseProjectTree, config, cliArgs, anchor, logger = new ConsoleLogger()) {
|
|
6
|
+
try {
|
|
7
|
+
for (const activeMode of activeModes) {
|
|
8
|
+
const targetConfig = activeMode.config;
|
|
9
|
+
const modeName = activeMode.name;
|
|
10
|
+
const buildResult = await build(targetConfig, baseProjectTree, config, env, sourcePaths, cliArgs, anchor);
|
|
11
|
+
const dropped = [];
|
|
12
|
+
if (buildResult.exposedDataFiles.length > 0) {
|
|
13
|
+
logger.warn(`${getTimeStamp()} [${modeName}] Skipping project file generation. Rogen cannot map data files directly.`);
|
|
14
|
+
for (const exposedPath of buildResult.exposedDataFiles) {
|
|
15
|
+
const ext = path.extname(exposedPath);
|
|
16
|
+
const fileName = path.basename(exposedPath);
|
|
17
|
+
const baseName = fileName.substring(0, fileName.length - ext.length);
|
|
18
|
+
const dirName = path.dirname(exposedPath);
|
|
19
|
+
logger.info(` - Cannot resolve data type "${ext}" for: "${exposedPath}"`);
|
|
20
|
+
logger.info(` Fix: Wrap it in a folder (e.g., move it to "${dirName}/${baseName}/data${ext}")`);
|
|
21
|
+
}
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (buildResult.missingPaths.length > 0) {
|
|
25
|
+
for (const item of buildResult.missingPaths) {
|
|
26
|
+
const ext = path.extname(item.absolutePath).toLowerCase();
|
|
27
|
+
if (ext === ".luau" || ext === ".lua") {
|
|
28
|
+
const dir = path.dirname(item.absolutePath);
|
|
29
|
+
if (!fs.existsSync(dir)) {
|
|
30
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
fs.writeFileSync(item.absolutePath, "");
|
|
33
|
+
}
|
|
34
|
+
else if (ext === "") {
|
|
35
|
+
if (!fs.existsSync(item.absolutePath)) {
|
|
36
|
+
fs.mkdirSync(item.absolutePath, {
|
|
37
|
+
recursive: true,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
delete item.parent[item.key];
|
|
43
|
+
dropped.push(`${item.treePath} ($path "${item.path}")`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const finalContent = JSON.stringify(buildResult.tree, null, "\t");
|
|
48
|
+
let shouldWrite = true;
|
|
49
|
+
if (fs.existsSync(buildResult.output)) {
|
|
50
|
+
const existingContent = fs.readFileSync(buildResult.output, "utf-8");
|
|
51
|
+
if (existingContent === finalContent) {
|
|
52
|
+
shouldWrite = false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (!shouldWrite) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const outputDir = path.dirname(buildResult.output);
|
|
59
|
+
if (!fs.existsSync(outputDir)) {
|
|
60
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
fs.writeFileSync(buildResult.output, finalContent);
|
|
63
|
+
const totalRemoved = buildResult.removed.length + dropped.length;
|
|
64
|
+
if (totalRemoved > 0) {
|
|
65
|
+
if (cliArgs.watch) {
|
|
66
|
+
logger.warn(`Pruned ${totalRemoved} unresolvable paths.`);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
logger.warn(`Removed entries whose paths do not exist:`);
|
|
70
|
+
for (const item of buildResult.removed) {
|
|
71
|
+
logger.info(` - ${item.treePath} ($path "${item.rojoPath}")`);
|
|
72
|
+
}
|
|
73
|
+
for (const item of dropped) {
|
|
74
|
+
logger.info(` - ${item}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (buildResult.collisions.length > 0) {
|
|
79
|
+
for (const collision of buildResult.collisions) {
|
|
80
|
+
logger.warn(collision);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (cliArgs.watch) {
|
|
84
|
+
const outputName = path.basename(buildResult.output);
|
|
85
|
+
logger.success(`[${modeName}] Rebuilt "${buildResult.name}" -> ${outputName}`);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
logger.success(`Generated Rojo tree for "${buildResult.name}"`);
|
|
89
|
+
logger.info(` Processed: ${buildResult.fileCount} files`);
|
|
90
|
+
logger.info(` Build dir: ${buildResult.buildDir}`);
|
|
91
|
+
const activeTags = Object.keys(targetConfig.tags || {}).filter((t) => targetConfig.tags[t]);
|
|
92
|
+
if (activeTags.length > 0) {
|
|
93
|
+
logger.info(` Tags: ${activeTags.join(", ")}`);
|
|
94
|
+
}
|
|
95
|
+
logger.info(` Output to: ${buildResult.output}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (error instanceof Error) {
|
|
102
|
+
logger.error(`failed to execute build - ${error.message}`);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
logger.error(`failed to execute build due to an unknown error`);
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|