@isentinel/rojo-utils 0.1.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/CLAUDE.md +40 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/clover.xml +388 -0
- package/coverage/coverage-final.json +7 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +191 -0
- package/coverage/loader.ts.html +220 -0
- package/coverage/mount-collector.ts.html +301 -0
- package/coverage/path-mapper.ts.html +220 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/rojo-resolver.ts.html +1900 -0
- package/coverage/rojo-tree.ts.html +559 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/tree-mapper.ts.html +247 -0
- package/dist/index.d.mts +175 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +551 -0
- package/dist/index.mjs.map +1 -0
- package/eslint.config.ts +23 -0
- package/out-tsc/index.d.ts +10 -0
- package/out-tsc/index.d.ts.map +1 -0
- package/out-tsc/index.js +6 -0
- package/out-tsc/loader.d.ts +3 -0
- package/out-tsc/loader.d.ts.map +1 -0
- package/out-tsc/loader.js +37 -0
- package/out-tsc/mount-collector.d.ts +10 -0
- package/out-tsc/mount-collector.d.ts.map +1 -0
- package/out-tsc/mount-collector.js +37 -0
- package/out-tsc/path-mapper.d.ts +4 -0
- package/out-tsc/path-mapper.d.ts.map +1 -0
- package/out-tsc/path-mapper.js +34 -0
- package/out-tsc/rojo-resolver.d.ts +110 -0
- package/out-tsc/rojo-resolver.d.ts.map +1 -0
- package/out-tsc/rojo-resolver.js +448 -0
- package/out-tsc/rojo-tree.d.ts +15 -0
- package/out-tsc/rojo-tree.d.ts.map +1 -0
- package/out-tsc/rojo-tree.js +106 -0
- package/out-tsc/tree-mapper.d.ts +4 -0
- package/out-tsc/tree-mapper.d.ts.map +1 -0
- package/out-tsc/tree-mapper.js +34 -0
- package/out-tsc/tsconfig.tsbuildinfo +1 -0
- package/out-tsc/types.d.ts +26 -0
- package/out-tsc/types.d.ts.map +1 -0
- package/out-tsc/types.js +1 -0
- package/out-tsc/vitest/eslint.config.d.ts +3 -0
- package/out-tsc/vitest/eslint.config.d.ts.map +1 -0
- package/out-tsc/vitest/eslint.shared.d.ts +13 -0
- package/out-tsc/vitest/eslint.shared.d.ts.map +1 -0
- package/out-tsc/vitest/libs/rojo-utils/eslint.config.d.ts +3 -0
- package/out-tsc/vitest/libs/rojo-utils/eslint.config.d.ts.map +1 -0
- package/out-tsc/vitest/libs/rojo-utils/src/loader.spec.d.ts +2 -0
- package/out-tsc/vitest/libs/rojo-utils/src/loader.spec.d.ts.map +1 -0
- package/out-tsc/vitest/libs/rojo-utils/src/mount-collector.spec.d.ts +2 -0
- package/out-tsc/vitest/libs/rojo-utils/src/mount-collector.spec.d.ts.map +1 -0
- package/out-tsc/vitest/libs/rojo-utils/src/path-mapper.spec.d.ts +2 -0
- package/out-tsc/vitest/libs/rojo-utils/src/path-mapper.spec.d.ts.map +1 -0
- package/out-tsc/vitest/libs/rojo-utils/src/rojo-resolver.spec.d.ts +2 -0
- package/out-tsc/vitest/libs/rojo-utils/src/rojo-resolver.spec.d.ts.map +1 -0
- package/out-tsc/vitest/libs/rojo-utils/src/rojo-tree.spec.d.ts +2 -0
- package/out-tsc/vitest/libs/rojo-utils/src/rojo-tree.spec.d.ts.map +1 -0
- package/out-tsc/vitest/libs/rojo-utils/src/tree-mapper.spec.d.ts +2 -0
- package/out-tsc/vitest/libs/rojo-utils/src/tree-mapper.spec.d.ts.map +1 -0
- package/out-tsc/vitest/libs/rojo-utils/test/setup/jest-extended.d.ts +2 -0
- package/out-tsc/vitest/libs/rojo-utils/test/setup/jest-extended.d.ts.map +1 -0
- package/out-tsc/vitest/libs/rojo-utils/tsconfig.spec.tsbuildinfo +1 -0
- package/out-tsc/vitest/libs/rojo-utils/tsdown.config.d.ts +3 -0
- package/out-tsc/vitest/libs/rojo-utils/tsdown.config.d.ts.map +1 -0
- package/out-tsc/vitest/libs/rojo-utils/vitest.config.d.ts +3 -0
- package/out-tsc/vitest/libs/rojo-utils/vitest.config.d.ts.map +1 -0
- package/out-tsc/vitest/src/loader.spec.d.ts +2 -0
- package/out-tsc/vitest/src/loader.spec.d.ts.map +1 -0
- package/out-tsc/vitest/src/mount-collector.spec.d.ts +2 -0
- package/out-tsc/vitest/src/mount-collector.spec.d.ts.map +1 -0
- package/out-tsc/vitest/src/path-mapper.spec.d.ts +2 -0
- package/out-tsc/vitest/src/path-mapper.spec.d.ts.map +1 -0
- package/out-tsc/vitest/src/rojo-resolver.spec.d.ts +2 -0
- package/out-tsc/vitest/src/rojo-resolver.spec.d.ts.map +1 -0
- package/out-tsc/vitest/src/rojo-tree.spec.d.ts +2 -0
- package/out-tsc/vitest/src/rojo-tree.spec.d.ts.map +1 -0
- package/out-tsc/vitest/src/tree-mapper.spec.d.ts +2 -0
- package/out-tsc/vitest/src/tree-mapper.spec.d.ts.map +1 -0
- package/out-tsc/vitest/test/setup/jest-extended.d.ts +2 -0
- package/out-tsc/vitest/test/setup/jest-extended.d.ts.map +1 -0
- package/out-tsc/vitest/tsconfig.spec.tsbuildinfo +1 -0
- package/out-tsc/vitest/tsdown.config.d.ts +3 -0
- package/out-tsc/vitest/tsdown.config.d.ts.map +1 -0
- package/out-tsc/vitest/vitest.config.d.ts +3 -0
- package/out-tsc/vitest/vitest.config.d.ts.map +1 -0
- package/package.json +48 -0
- package/project.json +25 -0
- package/src/index.ts +25 -0
- package/src/loader.spec.ts +240 -0
- package/src/loader.ts +45 -0
- package/src/mount-collector.spec.ts +272 -0
- package/src/mount-collector.ts +72 -0
- package/src/path-mapper.spec.ts +201 -0
- package/src/path-mapper.ts +45 -0
- package/src/rojo-resolver.spec.ts +956 -0
- package/src/rojo-resolver.ts +605 -0
- package/src/rojo-tree.spec.ts +622 -0
- package/src/rojo-tree.ts +162 -0
- package/src/tree-mapper.spec.ts +147 -0
- package/src/tree-mapper.ts +54 -0
- package/src/types.ts +29 -0
- package/test/setup/jest-extended.ts +4 -0
- package/test/setup/vitest-jest-extended.d.ts +1 -0
- package/tsconfig.json +13 -0
- package/tsconfig.lib.json +17 -0
- package/tsconfig.lib.tsbuildinfo +1 -0
- package/tsconfig.spec.json +26 -0
- package/tsdown.config.ts +15 -0
- package/vitest.config.ts +20 -0
|
@@ -0,0 +1,956 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { platform } from "node:process";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
FileRelation,
|
|
9
|
+
NetworkType,
|
|
10
|
+
RbxPathParent,
|
|
11
|
+
RbxType,
|
|
12
|
+
RojoResolver,
|
|
13
|
+
} from "./rojo-resolver.ts";
|
|
14
|
+
|
|
15
|
+
type FileMap = Record<string, string>;
|
|
16
|
+
|
|
17
|
+
function withProject(files: FileMap, run: (directory: string) => void): void {
|
|
18
|
+
const directory = realpathSync(mkdtempSync(path.join(tmpdir(), "rojo-resolver-")));
|
|
19
|
+
try {
|
|
20
|
+
for (const [relativePath, content] of Object.entries(files)) {
|
|
21
|
+
const full = path.join(directory, relativePath);
|
|
22
|
+
mkdirSync(path.dirname(full), { recursive: true });
|
|
23
|
+
writeFileSync(full, content);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
run(directory);
|
|
27
|
+
} finally {
|
|
28
|
+
rmSync(directory, { force: true, recursive: true });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function project(tree: object, name = "Game"): string {
|
|
33
|
+
return JSON.stringify({ name, tree });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function fromProject(directory: string): RojoResolver {
|
|
37
|
+
return RojoResolver.fromPath(path.join(directory, "default.project.json"));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("findRojoConfigFilePath", () => {
|
|
41
|
+
it("should return the default project file when present", () => {
|
|
42
|
+
expect.assertions(2);
|
|
43
|
+
|
|
44
|
+
withProject(
|
|
45
|
+
{ "default.project.json": project({ $className: "DataModel" }) },
|
|
46
|
+
(directory) => {
|
|
47
|
+
const result = RojoResolver.findRojoConfigFilePath(directory);
|
|
48
|
+
|
|
49
|
+
expect(result.path).toBe(path.join(directory, "default.project.json"));
|
|
50
|
+
expect(result.warnings).toBeEmpty();
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should return a single non-default project file without warning", () => {
|
|
56
|
+
expect.assertions(2);
|
|
57
|
+
|
|
58
|
+
withProject({ "game.project.json": project({ $className: "DataModel" }) }, (directory) => {
|
|
59
|
+
const result = RojoResolver.findRojoConfigFilePath(directory);
|
|
60
|
+
|
|
61
|
+
expect(result.path).toBe(path.join(directory, "game.project.json"));
|
|
62
|
+
expect(result.warnings).toBeEmpty();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should recognize the legacy roblox-project.json name", () => {
|
|
67
|
+
expect.assertions(1);
|
|
68
|
+
|
|
69
|
+
withProject(
|
|
70
|
+
{ "roblox-project.json": project({ $className: "DataModel" }) },
|
|
71
|
+
(directory) => {
|
|
72
|
+
expect(RojoResolver.findRojoConfigFilePath(directory).path).toBe(
|
|
73
|
+
path.join(directory, "roblox-project.json"),
|
|
74
|
+
);
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should warn when multiple project files are present", () => {
|
|
80
|
+
expect.assertions(1);
|
|
81
|
+
|
|
82
|
+
withProject(
|
|
83
|
+
{
|
|
84
|
+
"a.project.json": project({ $className: "DataModel" }),
|
|
85
|
+
"b.project.json": project({ $className: "DataModel" }),
|
|
86
|
+
},
|
|
87
|
+
(directory) => {
|
|
88
|
+
expect(RojoResolver.findRojoConfigFilePath(directory).warnings[0]).toInclude(
|
|
89
|
+
"Multiple *.project.json files found",
|
|
90
|
+
);
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should return undefined when no project file is present", () => {
|
|
96
|
+
expect.assertions(1);
|
|
97
|
+
|
|
98
|
+
withProject({ "readme.md": "hi" }, (directory) => {
|
|
99
|
+
expect(RojoResolver.findRojoConfigFilePath(directory).path).toBeUndefined();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("fromPath", () => {
|
|
105
|
+
it("should mark a DataModel project as a game", () => {
|
|
106
|
+
expect.assertions(1);
|
|
107
|
+
|
|
108
|
+
withProject(
|
|
109
|
+
{ "default.project.json": project({ $className: "DataModel" }) },
|
|
110
|
+
(directory) => {
|
|
111
|
+
expect(fromProject(directory).isGame).toBeTrue();
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("should warn when the config path does not exist", () => {
|
|
117
|
+
expect.assertions(1);
|
|
118
|
+
|
|
119
|
+
withProject({ "readme.md": "hi" }, (directory) => {
|
|
120
|
+
const resolver = RojoResolver.fromPath(path.join(directory, "default.project.json"));
|
|
121
|
+
|
|
122
|
+
expect(resolver.getWarnings()[0]).toInclude("Path does not exist");
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("should warn on an invalid configuration", () => {
|
|
127
|
+
expect.assertions(1);
|
|
128
|
+
|
|
129
|
+
withProject({ "default.project.json": JSON.stringify({ notName: 1 }) }, (directory) => {
|
|
130
|
+
expect(fromProject(directory).getWarnings()[0]).toInclude("Invalid configuration");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should warn on malformed JSON instead of throwing", () => {
|
|
135
|
+
expect.assertions(1);
|
|
136
|
+
|
|
137
|
+
withProject({ "default.project.json": "{ broken json" }, (directory) => {
|
|
138
|
+
expect(fromProject(directory).getWarnings()[0]).toInclude("Invalid configuration");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("should map a module file path directly", () => {
|
|
143
|
+
expect.assertions(1);
|
|
144
|
+
|
|
145
|
+
withProject(
|
|
146
|
+
{
|
|
147
|
+
"config.luau": "return {}",
|
|
148
|
+
"default.project.json": project({
|
|
149
|
+
$className: "DataModel",
|
|
150
|
+
Config: { $path: "config.luau" },
|
|
151
|
+
}),
|
|
152
|
+
},
|
|
153
|
+
(directory) => {
|
|
154
|
+
expect(
|
|
155
|
+
fromProject(directory).getRbxPathFromFilePath(
|
|
156
|
+
path.join(directory, "config.luau"),
|
|
157
|
+
),
|
|
158
|
+
).toStrictEqual(["Config"]);
|
|
159
|
+
},
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("should resolve a directory partition and strip init", () => {
|
|
164
|
+
expect.assertions(2);
|
|
165
|
+
|
|
166
|
+
withProject(
|
|
167
|
+
{
|
|
168
|
+
"default.project.json": project({
|
|
169
|
+
$className: "DataModel",
|
|
170
|
+
ReplicatedStorage: { $path: "src/shared" },
|
|
171
|
+
}),
|
|
172
|
+
"src/shared/init.luau": "return {}",
|
|
173
|
+
"src/shared/module.luau": "return {}",
|
|
174
|
+
},
|
|
175
|
+
(directory) => {
|
|
176
|
+
const resolver = fromProject(directory);
|
|
177
|
+
|
|
178
|
+
expect(
|
|
179
|
+
resolver.getRbxPathFromFilePath(path.join(directory, "src/shared/init.luau")),
|
|
180
|
+
).toStrictEqual(["ReplicatedStorage"]);
|
|
181
|
+
expect(
|
|
182
|
+
resolver.getRbxPathFromFilePath(path.join(directory, "src/shared/module.luau")),
|
|
183
|
+
).toStrictEqual(["ReplicatedStorage", "module"]);
|
|
184
|
+
},
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("should accept the optional $path object form", () => {
|
|
189
|
+
expect.assertions(1);
|
|
190
|
+
|
|
191
|
+
withProject(
|
|
192
|
+
{
|
|
193
|
+
"default.project.json": project({
|
|
194
|
+
$className: "DataModel",
|
|
195
|
+
Maybe: { $path: { optional: "src/maybe" } },
|
|
196
|
+
}),
|
|
197
|
+
"src/maybe/value.luau": "return {}",
|
|
198
|
+
},
|
|
199
|
+
(directory) => {
|
|
200
|
+
expect(
|
|
201
|
+
fromProject(directory).getRbxPathFromFilePath(
|
|
202
|
+
path.join(directory, "src/maybe/value.luau"),
|
|
203
|
+
),
|
|
204
|
+
).toStrictEqual(["Maybe", "value"]);
|
|
205
|
+
},
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("should create a partition for a $path that does not exist", () => {
|
|
210
|
+
expect.assertions(1);
|
|
211
|
+
|
|
212
|
+
withProject(
|
|
213
|
+
{
|
|
214
|
+
"default.project.json": project({
|
|
215
|
+
$className: "DataModel",
|
|
216
|
+
Ghost: { $path: "does/not/exist" },
|
|
217
|
+
}),
|
|
218
|
+
},
|
|
219
|
+
(directory) => {
|
|
220
|
+
expect(fromProject(directory).getPartitions()).not.toBeEmpty();
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("should parse a nested default project referenced by directory", () => {
|
|
226
|
+
expect.assertions(1);
|
|
227
|
+
|
|
228
|
+
withProject(
|
|
229
|
+
{
|
|
230
|
+
"default.project.json": project({
|
|
231
|
+
$className: "DataModel",
|
|
232
|
+
Nested: { $path: "nested" },
|
|
233
|
+
}),
|
|
234
|
+
"nested/default.project.json": project({ $path: "src" }, "Nested"),
|
|
235
|
+
"nested/src/inside.luau": "return {}",
|
|
236
|
+
},
|
|
237
|
+
(directory) => {
|
|
238
|
+
expect(
|
|
239
|
+
fromProject(directory).getRbxPathFromFilePath(
|
|
240
|
+
path.join(directory, "nested/src/inside.luau"),
|
|
241
|
+
),
|
|
242
|
+
).toStrictEqual(["Nested", "inside"]);
|
|
243
|
+
},
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("should embed a nested project referenced by a project file and ignore its name", () => {
|
|
248
|
+
expect.assertions(1);
|
|
249
|
+
|
|
250
|
+
withProject(
|
|
251
|
+
{
|
|
252
|
+
"default.project.json": project({
|
|
253
|
+
$className: "DataModel",
|
|
254
|
+
Pkg: { $path: "shared.project.json" },
|
|
255
|
+
}),
|
|
256
|
+
"shared.project.json": project({ $path: "src" }, "IgnoredName"),
|
|
257
|
+
"src/mod.luau": "return {}",
|
|
258
|
+
},
|
|
259
|
+
(directory) => {
|
|
260
|
+
// Mounts under the parent key "Pkg", not the nested project's own
|
|
261
|
+
// name "IgnoredName".
|
|
262
|
+
expect(
|
|
263
|
+
fromProject(directory).getRbxPathFromFilePath(
|
|
264
|
+
path.join(directory, "src/mod.luau"),
|
|
265
|
+
),
|
|
266
|
+
).toStrictEqual(["Pkg", "mod"]);
|
|
267
|
+
},
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("should register the nested partition and not map the project file as a leaf", () => {
|
|
272
|
+
expect.assertions(3);
|
|
273
|
+
|
|
274
|
+
withProject(
|
|
275
|
+
{
|
|
276
|
+
"default.project.json": project({
|
|
277
|
+
$className: "DataModel",
|
|
278
|
+
Pkg: { $path: "shared.project.json" },
|
|
279
|
+
}),
|
|
280
|
+
"shared.project.json": project({ $path: "src" }, "Pkg"),
|
|
281
|
+
"src/mod.luau": "return {}",
|
|
282
|
+
},
|
|
283
|
+
(directory) => {
|
|
284
|
+
const resolver = fromProject(directory);
|
|
285
|
+
const state = resolver.getState();
|
|
286
|
+
const projectFile = path.join(directory, "shared.project.json");
|
|
287
|
+
|
|
288
|
+
expect(
|
|
289
|
+
state.partitions.some((partition) => partition.fsPath.endsWith("src")),
|
|
290
|
+
).toBeTrue();
|
|
291
|
+
expect(
|
|
292
|
+
state.filePathToRbxPathMap.some(([filePath]) => filePath === projectFile),
|
|
293
|
+
).toBeFalse();
|
|
294
|
+
// The nested project file is walked, so cache invalidation tracks
|
|
295
|
+
// edits to it (RojoResolverCache keys on walkedConfigFiles).
|
|
296
|
+
expect([...resolver.walkedConfigFiles]).toContain(projectFile);
|
|
297
|
+
},
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("should return undefined for an unmapped file", () => {
|
|
302
|
+
expect.assertions(1);
|
|
303
|
+
|
|
304
|
+
withProject(
|
|
305
|
+
{ "default.project.json": project({ $className: "DataModel" }) },
|
|
306
|
+
(directory) => {
|
|
307
|
+
expect(
|
|
308
|
+
fromProject(directory).getRbxPathFromFilePath(
|
|
309
|
+
path.join(directory, "elsewhere.luau"),
|
|
310
|
+
),
|
|
311
|
+
).toBeUndefined();
|
|
312
|
+
},
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
describe("searchDirectory", () => {
|
|
318
|
+
it("should follow nested project files and subfolders", () => {
|
|
319
|
+
expect.assertions(2);
|
|
320
|
+
|
|
321
|
+
withProject(
|
|
322
|
+
{
|
|
323
|
+
"default.project.json": project({
|
|
324
|
+
$className: "DataModel",
|
|
325
|
+
ReplicatedStorage: { $path: "src/shared" },
|
|
326
|
+
}),
|
|
327
|
+
"extra/leaf.luau": "return {}",
|
|
328
|
+
"src/shared/extra.project.json": project({ $path: "../../extra" }, "Extra"),
|
|
329
|
+
"src/shared/sub/leaf.luau": "return {}",
|
|
330
|
+
},
|
|
331
|
+
(directory) => {
|
|
332
|
+
const resolver = fromProject(directory);
|
|
333
|
+
|
|
334
|
+
expect(
|
|
335
|
+
resolver.getRbxPathFromFilePath(
|
|
336
|
+
path.join(directory, "src/shared/sub/leaf.luau"),
|
|
337
|
+
),
|
|
338
|
+
).toStrictEqual(["ReplicatedStorage", "sub", "leaf"]);
|
|
339
|
+
expect(resolver.getPartitions().length).toBeGreaterThan(1);
|
|
340
|
+
},
|
|
341
|
+
);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("should stop at a subfolder that has its own default project", () => {
|
|
345
|
+
expect.assertions(1);
|
|
346
|
+
|
|
347
|
+
withProject(
|
|
348
|
+
{
|
|
349
|
+
"default.project.json": project({
|
|
350
|
+
$className: "DataModel",
|
|
351
|
+
ReplicatedStorage: { $path: "src/shared" },
|
|
352
|
+
}),
|
|
353
|
+
"src/shared/module.luau": "return {}",
|
|
354
|
+
"src/shared/withDefault/default.project.json": project(
|
|
355
|
+
{ $path: "../module.luau" },
|
|
356
|
+
"Inner",
|
|
357
|
+
),
|
|
358
|
+
},
|
|
359
|
+
(directory) => {
|
|
360
|
+
expect(fromProject(directory).isGame).toBeTrue();
|
|
361
|
+
},
|
|
362
|
+
);
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
describe("getRbxTypeFromFilePath", () => {
|
|
367
|
+
const resolver = RojoResolver.synthetic(".");
|
|
368
|
+
|
|
369
|
+
it("should classify a .server script", () => {
|
|
370
|
+
expect.assertions(1);
|
|
371
|
+
expect(resolver.getRbxTypeFromFilePath("a.server.luau")).toBe(RbxType.Script);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("should classify a .client script", () => {
|
|
375
|
+
expect.assertions(1);
|
|
376
|
+
expect(resolver.getRbxTypeFromFilePath("a.client.luau")).toBe(RbxType.LocalScript);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("should classify a plain module script", () => {
|
|
380
|
+
expect.assertions(1);
|
|
381
|
+
expect(resolver.getRbxTypeFromFilePath("a.luau")).toBe(RbxType.ModuleScript);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("should classify a non-script module by extension", () => {
|
|
385
|
+
expect.assertions(1);
|
|
386
|
+
expect(resolver.getRbxTypeFromFilePath("a.json")).toBe(RbxType.ModuleScript);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("should return Unknown for an unrecognized sub-extension", () => {
|
|
390
|
+
expect.assertions(1);
|
|
391
|
+
expect(resolver.getRbxTypeFromFilePath("a.weird.luau")).toBe(RbxType.Unknown);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it("should convert a .lua extension to .luau", () => {
|
|
395
|
+
expect.assertions(1);
|
|
396
|
+
expect(resolver.getRbxTypeFromFilePath("a.lua")).toBe(RbxType.ModuleScript);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
describe("container queries", () => {
|
|
401
|
+
function gameResolver(directory: string): RojoResolver {
|
|
402
|
+
writeFileSync(
|
|
403
|
+
path.join(directory, "default.project.json"),
|
|
404
|
+
project({
|
|
405
|
+
$className: "DataModel",
|
|
406
|
+
ServerScriptService: { $path: "src/server" },
|
|
407
|
+
StarterGui: { $path: "src/gui" },
|
|
408
|
+
StarterPack: { $path: "src/pack" },
|
|
409
|
+
}),
|
|
410
|
+
);
|
|
411
|
+
for (const relativePath of ["src/server/a.luau", "src/gui/b.luau", "src/pack/c.luau"]) {
|
|
412
|
+
const full = path.join(directory, relativePath);
|
|
413
|
+
mkdirSync(path.dirname(full), { recursive: true });
|
|
414
|
+
writeFileSync(full, "return {}");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return fromProject(directory);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
it("should report network type by container", () => {
|
|
421
|
+
expect.assertions(3);
|
|
422
|
+
|
|
423
|
+
withProject({}, (directory) => {
|
|
424
|
+
const resolver = gameResolver(directory);
|
|
425
|
+
|
|
426
|
+
expect(resolver.getNetworkType(["ServerScriptService"])).toBe(NetworkType.Server);
|
|
427
|
+
expect(resolver.getNetworkType(["StarterGui"])).toBe(NetworkType.Client);
|
|
428
|
+
expect(resolver.getNetworkType(["ReplicatedStorage"])).toBe(NetworkType.Unknown);
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("should detect isolated containers", () => {
|
|
433
|
+
expect.assertions(2);
|
|
434
|
+
|
|
435
|
+
withProject({}, (directory) => {
|
|
436
|
+
const resolver = gameResolver(directory);
|
|
437
|
+
|
|
438
|
+
expect(resolver.isIsolated(["StarterGui"])).toBeTrue();
|
|
439
|
+
expect(resolver.isIsolated(["ReplicatedStorage"])).toBeFalse();
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it("should compute file relations across containers", () => {
|
|
444
|
+
expect.assertions(5);
|
|
445
|
+
|
|
446
|
+
withProject({}, (directory) => {
|
|
447
|
+
const resolver = gameResolver(directory);
|
|
448
|
+
|
|
449
|
+
expect(resolver.getFileRelation(["StarterGui", "a"], ["StarterGui", "b"])).toBe(
|
|
450
|
+
FileRelation.InToIn,
|
|
451
|
+
);
|
|
452
|
+
expect(resolver.getFileRelation(["StarterGui"], ["StarterPack"])).toBe(
|
|
453
|
+
FileRelation.OutToIn,
|
|
454
|
+
);
|
|
455
|
+
expect(resolver.getFileRelation(["StarterGui"], ["ReplicatedStorage"])).toBe(
|
|
456
|
+
FileRelation.InToOut,
|
|
457
|
+
);
|
|
458
|
+
expect(resolver.getFileRelation(["ReplicatedStorage"], ["StarterGui"])).toBe(
|
|
459
|
+
FileRelation.OutToIn,
|
|
460
|
+
);
|
|
461
|
+
expect(resolver.getFileRelation(["ReplicatedStorage"], ["Workspace"])).toBe(
|
|
462
|
+
FileRelation.OutToOut,
|
|
463
|
+
);
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it("should treat a synthetic non-game resolver as all-out", () => {
|
|
468
|
+
expect.assertions(3);
|
|
469
|
+
|
|
470
|
+
const resolver = RojoResolver.synthetic(".");
|
|
471
|
+
|
|
472
|
+
expect(resolver.getNetworkType(["StarterGui"])).toBe(NetworkType.Unknown);
|
|
473
|
+
expect(resolver.isIsolated(["StarterGui"])).toBeFalse();
|
|
474
|
+
expect(resolver.getFileRelation(["StarterGui"], ["StarterGui"])).toBe(
|
|
475
|
+
FileRelation.OutToOut,
|
|
476
|
+
);
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
describe("relative", () => {
|
|
481
|
+
it("should return an empty path for identical locations", () => {
|
|
482
|
+
expect.assertions(1);
|
|
483
|
+
expect(RojoResolver.relative(["a", "b"], ["a", "b"])).toBeEmpty();
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it("should descend into a child path", () => {
|
|
487
|
+
expect.assertions(1);
|
|
488
|
+
expect(RojoResolver.relative(["a"], ["a", "b", "c"])).toStrictEqual(["b", "c"]);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it("should ascend with parent markers", () => {
|
|
492
|
+
expect.assertions(1);
|
|
493
|
+
expect(RojoResolver.relative(["a", "b", "c"], ["a"])).toStrictEqual([
|
|
494
|
+
RbxPathParent,
|
|
495
|
+
RbxPathParent,
|
|
496
|
+
]);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("should mix ascent and descent", () => {
|
|
500
|
+
expect.assertions(1);
|
|
501
|
+
expect(RojoResolver.relative(["a", "b"], ["a", "c"])).toStrictEqual([RbxPathParent, "c"]);
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
describe("state serialization", () => {
|
|
506
|
+
it("should round-trip through getState and fromState", () => {
|
|
507
|
+
expect.assertions(4);
|
|
508
|
+
|
|
509
|
+
withProject(
|
|
510
|
+
{
|
|
511
|
+
"default.project.json": project({
|
|
512
|
+
$className: "DataModel",
|
|
513
|
+
ReplicatedStorage: { $path: "src/shared" },
|
|
514
|
+
}),
|
|
515
|
+
"src/shared/module.luau": "return {}",
|
|
516
|
+
},
|
|
517
|
+
(directory) => {
|
|
518
|
+
const original = fromProject(directory);
|
|
519
|
+
const restored = RojoResolver.fromState(original.getState());
|
|
520
|
+
const modulePath = path.join(directory, "src/shared/module.luau");
|
|
521
|
+
|
|
522
|
+
expect(restored.isGame).toBe(original.isGame);
|
|
523
|
+
expect(restored.getRbxPathFromFilePath(modulePath)).toStrictEqual(
|
|
524
|
+
original.getRbxPathFromFilePath(modulePath),
|
|
525
|
+
);
|
|
526
|
+
expect(restored.walkedConfigFiles.size).toBe(original.walkedConfigFiles.size);
|
|
527
|
+
expect(restored.walkedDirectories.size).toBe(original.walkedDirectories.size);
|
|
528
|
+
},
|
|
529
|
+
);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it("should record walked directories and config files", () => {
|
|
533
|
+
expect.assertions(2);
|
|
534
|
+
|
|
535
|
+
withProject(
|
|
536
|
+
{
|
|
537
|
+
"default.project.json": project({
|
|
538
|
+
$className: "DataModel",
|
|
539
|
+
ReplicatedStorage: { $path: "src/shared" },
|
|
540
|
+
}),
|
|
541
|
+
"src/shared/module.luau": "return {}",
|
|
542
|
+
},
|
|
543
|
+
(directory) => {
|
|
544
|
+
const resolver = fromProject(directory);
|
|
545
|
+
|
|
546
|
+
expect(resolver.walkedConfigFiles.size).toBeGreaterThan(0);
|
|
547
|
+
expect(resolver.walkedDirectories.size).toBeGreaterThan(0);
|
|
548
|
+
},
|
|
549
|
+
);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it("should round-trip a direct module-file mapping", () => {
|
|
553
|
+
expect.assertions(1);
|
|
554
|
+
|
|
555
|
+
withProject(
|
|
556
|
+
{
|
|
557
|
+
"config.luau": "return {}",
|
|
558
|
+
"default.project.json": project({
|
|
559
|
+
$className: "DataModel",
|
|
560
|
+
Config: { $path: "config.luau" },
|
|
561
|
+
}),
|
|
562
|
+
},
|
|
563
|
+
(directory) => {
|
|
564
|
+
const original = fromProject(directory);
|
|
565
|
+
const restored = RojoResolver.fromState(original.getState());
|
|
566
|
+
const configPath = path.join(directory, "config.luau");
|
|
567
|
+
|
|
568
|
+
expect(restored.getRbxPathFromFilePath(configPath)).toStrictEqual(
|
|
569
|
+
original.getRbxPathFromFilePath(configPath),
|
|
570
|
+
);
|
|
571
|
+
},
|
|
572
|
+
);
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
describe("non-module file partitions", () => {
|
|
577
|
+
it("should resolve a partition that targets a non-module file", () => {
|
|
578
|
+
expect.assertions(1);
|
|
579
|
+
|
|
580
|
+
withProject(
|
|
581
|
+
{
|
|
582
|
+
"data.txt": "hello",
|
|
583
|
+
"default.project.json": project({
|
|
584
|
+
$className: "DataModel",
|
|
585
|
+
Data: { $path: "data.txt" },
|
|
586
|
+
}),
|
|
587
|
+
},
|
|
588
|
+
(directory) => {
|
|
589
|
+
expect(
|
|
590
|
+
fromProject(directory).getRbxPathFromFilePath(path.join(directory, "data.txt")),
|
|
591
|
+
).toStrictEqual(["Data"]);
|
|
592
|
+
},
|
|
593
|
+
);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it("should resolve a non-script module extension under a partition", () => {
|
|
597
|
+
expect.assertions(1);
|
|
598
|
+
|
|
599
|
+
withProject(
|
|
600
|
+
{
|
|
601
|
+
"default.project.json": project({
|
|
602
|
+
$className: "DataModel",
|
|
603
|
+
ReplicatedStorage: { $path: "src/shared" },
|
|
604
|
+
}),
|
|
605
|
+
"src/shared/data.json": "{}",
|
|
606
|
+
},
|
|
607
|
+
(directory) => {
|
|
608
|
+
expect(
|
|
609
|
+
fromProject(directory).getRbxPathFromFilePath(
|
|
610
|
+
path.join(directory, "src/shared/data.json"),
|
|
611
|
+
),
|
|
612
|
+
).toStrictEqual(["ReplicatedStorage", "data"]);
|
|
613
|
+
},
|
|
614
|
+
);
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
describe("script sub-extension resolution", () => {
|
|
619
|
+
it("should strip a .server sub-extension when resolving via a partition", () => {
|
|
620
|
+
expect.assertions(1);
|
|
621
|
+
|
|
622
|
+
withProject(
|
|
623
|
+
{
|
|
624
|
+
"default.project.json": project({
|
|
625
|
+
$className: "DataModel",
|
|
626
|
+
ServerScriptService: { $path: "src/server" },
|
|
627
|
+
}),
|
|
628
|
+
"src/server/main.server.luau": "return {}",
|
|
629
|
+
},
|
|
630
|
+
(directory) => {
|
|
631
|
+
expect(
|
|
632
|
+
fromProject(directory).getRbxPathFromFilePath(
|
|
633
|
+
path.join(directory, "src/server/main.server.luau"),
|
|
634
|
+
),
|
|
635
|
+
).toStrictEqual(["ServerScriptService", "main"]);
|
|
636
|
+
},
|
|
637
|
+
);
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
describe("walker syscall behavior", () => {
|
|
642
|
+
it("should parse sibling project files before recursing into sibling directories", () => {
|
|
643
|
+
expect.assertions(3);
|
|
644
|
+
|
|
645
|
+
withProject(
|
|
646
|
+
{
|
|
647
|
+
"default.project.json": project({
|
|
648
|
+
$className: "DataModel",
|
|
649
|
+
ReplicatedStorage: { $path: "src/shared" },
|
|
650
|
+
}),
|
|
651
|
+
"src/shared/child.project.json": project({ $path: "child" }, "ChildViaProject"),
|
|
652
|
+
"src/shared/child/leaf.luau": "return {}",
|
|
653
|
+
},
|
|
654
|
+
(directory) => {
|
|
655
|
+
const resolver = fromProject(directory);
|
|
656
|
+
const partitions = resolver.getPartitions();
|
|
657
|
+
const childRoot = path.join(directory, "src/shared/child");
|
|
658
|
+
const sharedRoot = path.join(directory, "src/shared");
|
|
659
|
+
const leafPath = path.join(directory, "src/shared/child/leaf.luau");
|
|
660
|
+
|
|
661
|
+
const childIndex = partitions.findIndex((partition) => {
|
|
662
|
+
return (
|
|
663
|
+
partition.fsPath === childRoot &&
|
|
664
|
+
partition.rbxPath.join(".") === "ReplicatedStorage.ChildViaProject"
|
|
665
|
+
);
|
|
666
|
+
});
|
|
667
|
+
const sharedIndex = partitions.findIndex((partition) => {
|
|
668
|
+
return (
|
|
669
|
+
partition.fsPath === sharedRoot &&
|
|
670
|
+
partition.rbxPath.join(".") === "ReplicatedStorage"
|
|
671
|
+
);
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
expect(childIndex).toBeGreaterThanOrEqual(0);
|
|
675
|
+
expect(childIndex).toBeLessThan(sharedIndex);
|
|
676
|
+
expect(resolver.getRbxPathFromFilePath(leafPath)).toStrictEqual([
|
|
677
|
+
"ReplicatedStorage",
|
|
678
|
+
"ChildViaProject",
|
|
679
|
+
"leaf",
|
|
680
|
+
]);
|
|
681
|
+
},
|
|
682
|
+
);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it("should follow a directory symlink and resolve files inside it", () => {
|
|
686
|
+
expect.assertions(1);
|
|
687
|
+
|
|
688
|
+
withProject(
|
|
689
|
+
{
|
|
690
|
+
"default.project.json": project({
|
|
691
|
+
$className: "DataModel",
|
|
692
|
+
ReplicatedStorage: { $path: "src/shared" },
|
|
693
|
+
}),
|
|
694
|
+
"real/leaf.luau": "return {}",
|
|
695
|
+
},
|
|
696
|
+
(directory) => {
|
|
697
|
+
mkdirSync(path.join(directory, "src/shared"), { recursive: true });
|
|
698
|
+
symlinkSync(
|
|
699
|
+
path.join(directory, "real"),
|
|
700
|
+
path.join(directory, "src/shared/linked"),
|
|
701
|
+
"dir",
|
|
702
|
+
);
|
|
703
|
+
|
|
704
|
+
const resolver = fromProject(directory);
|
|
705
|
+
|
|
706
|
+
expect(
|
|
707
|
+
resolver.getRbxPathFromFilePath(
|
|
708
|
+
path.join(directory, "src/shared/linked/leaf.luau"),
|
|
709
|
+
),
|
|
710
|
+
).toStrictEqual(["ReplicatedStorage", "linked", "leaf"]);
|
|
711
|
+
},
|
|
712
|
+
);
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
it("should follow a file symlink that targets a sibling project.json", () => {
|
|
716
|
+
expect.assertions(1);
|
|
717
|
+
|
|
718
|
+
withProject(
|
|
719
|
+
{
|
|
720
|
+
"default.project.json": project({
|
|
721
|
+
$className: "DataModel",
|
|
722
|
+
ReplicatedStorage: { $path: "src/shared" },
|
|
723
|
+
}),
|
|
724
|
+
"extra/leaf.luau": "return {}",
|
|
725
|
+
"real/extra.project.json": project({ $path: "../../extra" }, "Linked"),
|
|
726
|
+
},
|
|
727
|
+
(directory) => {
|
|
728
|
+
mkdirSync(path.join(directory, "src/shared"), { recursive: true });
|
|
729
|
+
symlinkSync(
|
|
730
|
+
path.join(directory, "real/extra.project.json"),
|
|
731
|
+
path.join(directory, "src/shared/extra.project.json"),
|
|
732
|
+
"file",
|
|
733
|
+
);
|
|
734
|
+
|
|
735
|
+
const resolver = fromProject(directory);
|
|
736
|
+
|
|
737
|
+
expect(
|
|
738
|
+
resolver.getRbxPathFromFilePath(path.join(directory, "extra/leaf.luau")),
|
|
739
|
+
).toStrictEqual(["ReplicatedStorage", "Linked", "leaf"]);
|
|
740
|
+
},
|
|
741
|
+
);
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
it("should warn and skip when a child symlink target is missing", () => {
|
|
745
|
+
expect.assertions(2);
|
|
746
|
+
|
|
747
|
+
withProject(
|
|
748
|
+
{
|
|
749
|
+
"default.project.json": project({
|
|
750
|
+
$className: "DataModel",
|
|
751
|
+
ReplicatedStorage: { $path: "src/shared" },
|
|
752
|
+
}),
|
|
753
|
+
"src/shared/leaf.luau": "return {}",
|
|
754
|
+
},
|
|
755
|
+
(directory) => {
|
|
756
|
+
symlinkSync(
|
|
757
|
+
path.join(directory, "does-not-exist"),
|
|
758
|
+
path.join(directory, "src/shared/broken"),
|
|
759
|
+
"dir",
|
|
760
|
+
);
|
|
761
|
+
|
|
762
|
+
const resolver = fromProject(directory);
|
|
763
|
+
|
|
764
|
+
expect(
|
|
765
|
+
resolver.getRbxPathFromFilePath(path.join(directory, "src/shared/leaf.luau")),
|
|
766
|
+
).toStrictEqual(["ReplicatedStorage", "leaf"]);
|
|
767
|
+
expect(
|
|
768
|
+
resolver.getWarnings().some((warning) => warning.includes("broken")),
|
|
769
|
+
).toBeTrue();
|
|
770
|
+
},
|
|
771
|
+
);
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
it.skipIf(platform !== "win32")(
|
|
775
|
+
"should follow an NTFS junction via the realpath+stat fallback",
|
|
776
|
+
() => {
|
|
777
|
+
// Junctions classify as isSymbolicLink=true, isDirectory=false from a
|
|
778
|
+
// Dirent on current Node, so they route through the same fallback
|
|
779
|
+
// path as cross-volume symlinks. Pinning this guards against a Node
|
|
780
|
+
// version where junctions are reported as directories (which would
|
|
781
|
+
// skip the fallback) or as unknown entries that fail stat.
|
|
782
|
+
expect.assertions(1);
|
|
783
|
+
|
|
784
|
+
withProject(
|
|
785
|
+
{
|
|
786
|
+
"default.project.json": project({
|
|
787
|
+
$className: "DataModel",
|
|
788
|
+
ReplicatedStorage: { $path: "src/shared" },
|
|
789
|
+
}),
|
|
790
|
+
"real/leaf.luau": "return {}",
|
|
791
|
+
},
|
|
792
|
+
(directory) => {
|
|
793
|
+
mkdirSync(path.join(directory, "src/shared"), { recursive: true });
|
|
794
|
+
symlinkSync(
|
|
795
|
+
path.join(directory, "real"),
|
|
796
|
+
path.join(directory, "src/shared/linked"),
|
|
797
|
+
"junction",
|
|
798
|
+
);
|
|
799
|
+
|
|
800
|
+
const resolver = fromProject(directory);
|
|
801
|
+
|
|
802
|
+
expect(
|
|
803
|
+
resolver.getRbxPathFromFilePath(
|
|
804
|
+
path.join(directory, "src/shared/linked/leaf.luau"),
|
|
805
|
+
),
|
|
806
|
+
).toStrictEqual(["ReplicatedStorage", "linked", "leaf"]);
|
|
807
|
+
},
|
|
808
|
+
);
|
|
809
|
+
},
|
|
810
|
+
);
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
describe("walker parity snapshot", () => {
|
|
814
|
+
function snapshot(
|
|
815
|
+
state: ReturnType<RojoResolver["getState"]>,
|
|
816
|
+
root: string,
|
|
817
|
+
): {
|
|
818
|
+
filePathToRbxPathMap: Array<[string, ReadonlyArray<string>]>;
|
|
819
|
+
isGame: boolean;
|
|
820
|
+
partitions: Array<{ fsPath: string; rbxPath: ReadonlyArray<string> }>;
|
|
821
|
+
walkedConfigFiles: Array<string>;
|
|
822
|
+
walkedDirs: Array<string>;
|
|
823
|
+
warnings: Array<string>;
|
|
824
|
+
} {
|
|
825
|
+
function strip(full: string): string {
|
|
826
|
+
return full.replaceAll(root, "<root>").replaceAll("\\", "/");
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
return {
|
|
830
|
+
// Partitions order matters: parseConfig/parsePath unshift, and
|
|
831
|
+
// getRbxPathFromFilePath returns the first descendant match. Don't
|
|
832
|
+
// sort.
|
|
833
|
+
filePathToRbxPathMap: state.filePathToRbxPathMap.map(([file, rbxPath]) => [
|
|
834
|
+
strip(file),
|
|
835
|
+
rbxPath,
|
|
836
|
+
]),
|
|
837
|
+
isGame: state.isGame,
|
|
838
|
+
partitions: state.partitions.map((partition) => {
|
|
839
|
+
return { fsPath: strip(partition.fsPath), rbxPath: partition.rbxPath };
|
|
840
|
+
}),
|
|
841
|
+
// walkedConfigFiles / walkedDirs come from Sets used only for cache
|
|
842
|
+
// invalidation; sort for stability across traversal-order changes.
|
|
843
|
+
walkedConfigFiles: state.walkedConfigFiles.map(strip).toSorted(),
|
|
844
|
+
walkedDirs: state.walkedDirs.map(strip).toSorted(),
|
|
845
|
+
warnings: state.warnings,
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
it("should capture combined partition / nested / overlap / init shapes", () => {
|
|
850
|
+
// One synth tree exercising the interactions the isolated tests miss:
|
|
851
|
+
// a sibling *.project.json next to a sibling directory both contributing
|
|
852
|
+
// partitions, a nested default.project.json reached via $path, init
|
|
853
|
+
// stripping, and a direct module-file mapping. The snapshot pins the
|
|
854
|
+
// resolver-state shape; the trailing expectations back the comment's
|
|
855
|
+
// claim by asserting init/module resolve via the partition fallback.
|
|
856
|
+
expect.assertions(3);
|
|
857
|
+
|
|
858
|
+
withProject(
|
|
859
|
+
{
|
|
860
|
+
"config.luau": "return {}",
|
|
861
|
+
"default.project.json": project({
|
|
862
|
+
$className: "DataModel",
|
|
863
|
+
Direct: { $path: "config.luau" },
|
|
864
|
+
Nested: { $path: "nested" },
|
|
865
|
+
ReplicatedStorage: { $path: "src/shared" },
|
|
866
|
+
}),
|
|
867
|
+
"nested/default.project.json": project({ $path: "src" }, "Nested"),
|
|
868
|
+
"nested/src/inside.luau": "return {}",
|
|
869
|
+
"src/shared/child.project.json": project({ $path: "child" }, "ChildViaProject"),
|
|
870
|
+
"src/shared/child/leaf.luau": "return {}",
|
|
871
|
+
"src/shared/init.luau": "return {}",
|
|
872
|
+
"src/shared/module.luau": "return {}",
|
|
873
|
+
},
|
|
874
|
+
(directory) => {
|
|
875
|
+
const resolver = fromProject(directory);
|
|
876
|
+
|
|
877
|
+
expect(
|
|
878
|
+
resolver.getRbxPathFromFilePath(path.join(directory, "src/shared/init.luau")),
|
|
879
|
+
).toStrictEqual(["ReplicatedStorage"]);
|
|
880
|
+
expect(
|
|
881
|
+
resolver.getRbxPathFromFilePath(path.join(directory, "src/shared/module.luau")),
|
|
882
|
+
).toStrictEqual(["ReplicatedStorage", "module"]);
|
|
883
|
+
expect(snapshot(resolver.getState(), directory)).toMatchInlineSnapshot(`
|
|
884
|
+
{
|
|
885
|
+
"filePathToRbxPathMap": [
|
|
886
|
+
[
|
|
887
|
+
"<root>/config.luau",
|
|
888
|
+
[
|
|
889
|
+
"Direct",
|
|
890
|
+
],
|
|
891
|
+
],
|
|
892
|
+
],
|
|
893
|
+
"isGame": true,
|
|
894
|
+
"partitions": [
|
|
895
|
+
{
|
|
896
|
+
"fsPath": "<root>/src/shared/child",
|
|
897
|
+
"rbxPath": [
|
|
898
|
+
"ReplicatedStorage",
|
|
899
|
+
"ChildViaProject",
|
|
900
|
+
],
|
|
901
|
+
},
|
|
902
|
+
{
|
|
903
|
+
"fsPath": "<root>/src/shared",
|
|
904
|
+
"rbxPath": [
|
|
905
|
+
"ReplicatedStorage",
|
|
906
|
+
],
|
|
907
|
+
},
|
|
908
|
+
{
|
|
909
|
+
"fsPath": "<root>/nested/src",
|
|
910
|
+
"rbxPath": [
|
|
911
|
+
"Nested",
|
|
912
|
+
],
|
|
913
|
+
},
|
|
914
|
+
],
|
|
915
|
+
"walkedConfigFiles": [
|
|
916
|
+
"<root>/default.project.json",
|
|
917
|
+
"<root>/nested/default.project.json",
|
|
918
|
+
"<root>/src/shared/child.project.json",
|
|
919
|
+
],
|
|
920
|
+
"walkedDirs": [
|
|
921
|
+
"<root>/nested",
|
|
922
|
+
"<root>/nested/src",
|
|
923
|
+
"<root>/src/shared",
|
|
924
|
+
"<root>/src/shared/child",
|
|
925
|
+
],
|
|
926
|
+
"warnings": [],
|
|
927
|
+
}
|
|
928
|
+
`);
|
|
929
|
+
},
|
|
930
|
+
);
|
|
931
|
+
});
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
describe("fromTree", () => {
|
|
935
|
+
it("should build a resolver from a tree object", () => {
|
|
936
|
+
expect.assertions(1);
|
|
937
|
+
|
|
938
|
+
withProject({ "src/value.luau": "return {}" }, (directory) => {
|
|
939
|
+
// RojoTree's index signature collides with its $-prefixed metadata
|
|
940
|
+
// keys (an upstream quirk), so a literal tree must be cast through
|
|
941
|
+
// unknown.
|
|
942
|
+
const tree: unknown = {
|
|
943
|
+
$className: "DataModel",
|
|
944
|
+
Shared: { $path: "src" },
|
|
945
|
+
};
|
|
946
|
+
const resolver = RojoResolver.fromTree(
|
|
947
|
+
directory,
|
|
948
|
+
tree as Parameters<typeof RojoResolver.fromTree>[1],
|
|
949
|
+
);
|
|
950
|
+
|
|
951
|
+
expect(
|
|
952
|
+
resolver.getRbxPathFromFilePath(path.join(directory, "src/value.luau")),
|
|
953
|
+
).toStrictEqual(["Shared", "value"]);
|
|
954
|
+
});
|
|
955
|
+
});
|
|
956
|
+
});
|