@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.
Files changed (115) hide show
  1. package/CLAUDE.md +40 -0
  2. package/coverage/base.css +224 -0
  3. package/coverage/block-navigation.js +87 -0
  4. package/coverage/clover.xml +388 -0
  5. package/coverage/coverage-final.json +7 -0
  6. package/coverage/favicon.png +0 -0
  7. package/coverage/index.html +191 -0
  8. package/coverage/loader.ts.html +220 -0
  9. package/coverage/mount-collector.ts.html +301 -0
  10. package/coverage/path-mapper.ts.html +220 -0
  11. package/coverage/prettify.css +1 -0
  12. package/coverage/prettify.js +2 -0
  13. package/coverage/rojo-resolver.ts.html +1900 -0
  14. package/coverage/rojo-tree.ts.html +559 -0
  15. package/coverage/sort-arrow-sprite.png +0 -0
  16. package/coverage/sorter.js +210 -0
  17. package/coverage/tree-mapper.ts.html +247 -0
  18. package/dist/index.d.mts +175 -0
  19. package/dist/index.d.mts.map +1 -0
  20. package/dist/index.mjs +551 -0
  21. package/dist/index.mjs.map +1 -0
  22. package/eslint.config.ts +23 -0
  23. package/out-tsc/index.d.ts +10 -0
  24. package/out-tsc/index.d.ts.map +1 -0
  25. package/out-tsc/index.js +6 -0
  26. package/out-tsc/loader.d.ts +3 -0
  27. package/out-tsc/loader.d.ts.map +1 -0
  28. package/out-tsc/loader.js +37 -0
  29. package/out-tsc/mount-collector.d.ts +10 -0
  30. package/out-tsc/mount-collector.d.ts.map +1 -0
  31. package/out-tsc/mount-collector.js +37 -0
  32. package/out-tsc/path-mapper.d.ts +4 -0
  33. package/out-tsc/path-mapper.d.ts.map +1 -0
  34. package/out-tsc/path-mapper.js +34 -0
  35. package/out-tsc/rojo-resolver.d.ts +110 -0
  36. package/out-tsc/rojo-resolver.d.ts.map +1 -0
  37. package/out-tsc/rojo-resolver.js +448 -0
  38. package/out-tsc/rojo-tree.d.ts +15 -0
  39. package/out-tsc/rojo-tree.d.ts.map +1 -0
  40. package/out-tsc/rojo-tree.js +106 -0
  41. package/out-tsc/tree-mapper.d.ts +4 -0
  42. package/out-tsc/tree-mapper.d.ts.map +1 -0
  43. package/out-tsc/tree-mapper.js +34 -0
  44. package/out-tsc/tsconfig.tsbuildinfo +1 -0
  45. package/out-tsc/types.d.ts +26 -0
  46. package/out-tsc/types.d.ts.map +1 -0
  47. package/out-tsc/types.js +1 -0
  48. package/out-tsc/vitest/eslint.config.d.ts +3 -0
  49. package/out-tsc/vitest/eslint.config.d.ts.map +1 -0
  50. package/out-tsc/vitest/eslint.shared.d.ts +13 -0
  51. package/out-tsc/vitest/eslint.shared.d.ts.map +1 -0
  52. package/out-tsc/vitest/libs/rojo-utils/eslint.config.d.ts +3 -0
  53. package/out-tsc/vitest/libs/rojo-utils/eslint.config.d.ts.map +1 -0
  54. package/out-tsc/vitest/libs/rojo-utils/src/loader.spec.d.ts +2 -0
  55. package/out-tsc/vitest/libs/rojo-utils/src/loader.spec.d.ts.map +1 -0
  56. package/out-tsc/vitest/libs/rojo-utils/src/mount-collector.spec.d.ts +2 -0
  57. package/out-tsc/vitest/libs/rojo-utils/src/mount-collector.spec.d.ts.map +1 -0
  58. package/out-tsc/vitest/libs/rojo-utils/src/path-mapper.spec.d.ts +2 -0
  59. package/out-tsc/vitest/libs/rojo-utils/src/path-mapper.spec.d.ts.map +1 -0
  60. package/out-tsc/vitest/libs/rojo-utils/src/rojo-resolver.spec.d.ts +2 -0
  61. package/out-tsc/vitest/libs/rojo-utils/src/rojo-resolver.spec.d.ts.map +1 -0
  62. package/out-tsc/vitest/libs/rojo-utils/src/rojo-tree.spec.d.ts +2 -0
  63. package/out-tsc/vitest/libs/rojo-utils/src/rojo-tree.spec.d.ts.map +1 -0
  64. package/out-tsc/vitest/libs/rojo-utils/src/tree-mapper.spec.d.ts +2 -0
  65. package/out-tsc/vitest/libs/rojo-utils/src/tree-mapper.spec.d.ts.map +1 -0
  66. package/out-tsc/vitest/libs/rojo-utils/test/setup/jest-extended.d.ts +2 -0
  67. package/out-tsc/vitest/libs/rojo-utils/test/setup/jest-extended.d.ts.map +1 -0
  68. package/out-tsc/vitest/libs/rojo-utils/tsconfig.spec.tsbuildinfo +1 -0
  69. package/out-tsc/vitest/libs/rojo-utils/tsdown.config.d.ts +3 -0
  70. package/out-tsc/vitest/libs/rojo-utils/tsdown.config.d.ts.map +1 -0
  71. package/out-tsc/vitest/libs/rojo-utils/vitest.config.d.ts +3 -0
  72. package/out-tsc/vitest/libs/rojo-utils/vitest.config.d.ts.map +1 -0
  73. package/out-tsc/vitest/src/loader.spec.d.ts +2 -0
  74. package/out-tsc/vitest/src/loader.spec.d.ts.map +1 -0
  75. package/out-tsc/vitest/src/mount-collector.spec.d.ts +2 -0
  76. package/out-tsc/vitest/src/mount-collector.spec.d.ts.map +1 -0
  77. package/out-tsc/vitest/src/path-mapper.spec.d.ts +2 -0
  78. package/out-tsc/vitest/src/path-mapper.spec.d.ts.map +1 -0
  79. package/out-tsc/vitest/src/rojo-resolver.spec.d.ts +2 -0
  80. package/out-tsc/vitest/src/rojo-resolver.spec.d.ts.map +1 -0
  81. package/out-tsc/vitest/src/rojo-tree.spec.d.ts +2 -0
  82. package/out-tsc/vitest/src/rojo-tree.spec.d.ts.map +1 -0
  83. package/out-tsc/vitest/src/tree-mapper.spec.d.ts +2 -0
  84. package/out-tsc/vitest/src/tree-mapper.spec.d.ts.map +1 -0
  85. package/out-tsc/vitest/test/setup/jest-extended.d.ts +2 -0
  86. package/out-tsc/vitest/test/setup/jest-extended.d.ts.map +1 -0
  87. package/out-tsc/vitest/tsconfig.spec.tsbuildinfo +1 -0
  88. package/out-tsc/vitest/tsdown.config.d.ts +3 -0
  89. package/out-tsc/vitest/tsdown.config.d.ts.map +1 -0
  90. package/out-tsc/vitest/vitest.config.d.ts +3 -0
  91. package/out-tsc/vitest/vitest.config.d.ts.map +1 -0
  92. package/package.json +48 -0
  93. package/project.json +25 -0
  94. package/src/index.ts +25 -0
  95. package/src/loader.spec.ts +240 -0
  96. package/src/loader.ts +45 -0
  97. package/src/mount-collector.spec.ts +272 -0
  98. package/src/mount-collector.ts +72 -0
  99. package/src/path-mapper.spec.ts +201 -0
  100. package/src/path-mapper.ts +45 -0
  101. package/src/rojo-resolver.spec.ts +956 -0
  102. package/src/rojo-resolver.ts +605 -0
  103. package/src/rojo-tree.spec.ts +622 -0
  104. package/src/rojo-tree.ts +162 -0
  105. package/src/tree-mapper.spec.ts +147 -0
  106. package/src/tree-mapper.ts +54 -0
  107. package/src/types.ts +29 -0
  108. package/test/setup/jest-extended.ts +4 -0
  109. package/test/setup/vitest-jest-extended.d.ts +1 -0
  110. package/tsconfig.json +13 -0
  111. package/tsconfig.lib.json +17 -0
  112. package/tsconfig.lib.tsbuildinfo +1 -0
  113. package/tsconfig.spec.json +26 -0
  114. package/tsdown.config.ts +15 -0
  115. package/vitest.config.ts +20 -0
@@ -0,0 +1,72 @@
1
+ import type { RojoTreeNode } from "./types.ts";
2
+
3
+ export type PathKind = "directory" | "file" | "missing";
4
+
5
+ export type PathClassifier = (fsPath: string) => PathKind;
6
+
7
+ export interface Mount {
8
+ dataModelPath: string;
9
+ fsPath: string;
10
+ }
11
+
12
+ const TrailingSlashPattern = /\/$/;
13
+
14
+ export function collectMounts(
15
+ node: RojoTreeNode,
16
+ currentDataModelPath: string,
17
+ classify: PathClassifier,
18
+ ): Array<Mount> {
19
+ const result: Array<Mount> = [];
20
+ walk(node, currentDataModelPath, classify, result);
21
+ return result;
22
+ }
23
+
24
+ export function pruneAncestors(paths: Array<string>): Array<string> {
25
+ return paths.filter((candidate) => {
26
+ return paths.every((other) => other === candidate || !candidate.startsWith(`${other}/`));
27
+ });
28
+ }
29
+
30
+ function addDirectoryMount(
31
+ node: RojoTreeNode,
32
+ dataModelPath: string,
33
+ classify: PathClassifier,
34
+ result: Array<Mount>,
35
+ ): void {
36
+ const rawPath = node.$path;
37
+ if (typeof rawPath !== "string") {
38
+ return;
39
+ }
40
+
41
+ if (rawPath.endsWith(".project.json")) {
42
+ return;
43
+ }
44
+
45
+ const fsPath = rawPath.replace(TrailingSlashPattern, "");
46
+ if (classify(fsPath) === "directory") {
47
+ result.push({ dataModelPath, fsPath });
48
+ }
49
+ }
50
+
51
+ function isTreeChild(value: unknown): value is RojoTreeNode {
52
+ return typeof value === "object" && value !== null && !Array.isArray(value);
53
+ }
54
+
55
+ function walk(
56
+ node: RojoTreeNode,
57
+ currentDataModelPath: string,
58
+ classify: PathClassifier,
59
+ result: Array<Mount>,
60
+ ): void {
61
+ for (const [key, value] of Object.entries(node)) {
62
+ if (key.startsWith("$") || !isTreeChild(value)) {
63
+ continue;
64
+ }
65
+
66
+ const childDataModelPath =
67
+ currentDataModelPath === "" ? key : `${currentDataModelPath}/${key}`;
68
+
69
+ addDirectoryMount(value, childDataModelPath, classify, result);
70
+ walk(value, childDataModelPath, classify, result);
71
+ }
72
+ }
@@ -0,0 +1,201 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { mapFsPathToDataModel, mapFsRootToDataModel } from "./path-mapper.ts";
4
+ import type { RojoTreeNode } from "./types.ts";
5
+
6
+ const simpleRojoTree: RojoTreeNode = {
7
+ $className: "DataModel",
8
+ ReplicatedStorage: {
9
+ client: { $path: "out/client" },
10
+ },
11
+ ServerScriptService: {
12
+ server: { $path: "out/server" },
13
+ },
14
+ };
15
+
16
+ describe(mapFsRootToDataModel, () => {
17
+ it("should map outDir to DataModel path via Rojo tree", () => {
18
+ expect.assertions(1);
19
+
20
+ expect(mapFsRootToDataModel("out/client", simpleRojoTree)).toBe("ReplicatedStorage/client");
21
+ });
22
+
23
+ it("should map server outDir to DataModel path", () => {
24
+ expect.assertions(1);
25
+
26
+ expect(mapFsRootToDataModel("out/server", simpleRojoTree)).toBe(
27
+ "ServerScriptService/server",
28
+ );
29
+ });
30
+
31
+ it("should handle nested tree structures", () => {
32
+ expect.assertions(1);
33
+
34
+ const nestedTree: RojoTreeNode = {
35
+ $className: "DataModel",
36
+ ReplicatedStorage: {
37
+ shared: {
38
+ lib: { $path: "out/shared/lib" },
39
+ },
40
+ },
41
+ };
42
+
43
+ expect(mapFsRootToDataModel("out/shared/lib", nestedTree)).toBe(
44
+ "ReplicatedStorage/shared/lib",
45
+ );
46
+ });
47
+
48
+ it("should handle path nested under a $path entry", () => {
49
+ expect.assertions(1);
50
+
51
+ expect(mapFsRootToDataModel("out/client/ui", simpleRojoTree)).toBe(
52
+ "ReplicatedStorage/client/ui",
53
+ );
54
+ });
55
+
56
+ it("should throw when no mapping found", () => {
57
+ expect.assertions(1);
58
+
59
+ expect(() => mapFsRootToDataModel("out/unknown", simpleRojoTree)).toThrow(
60
+ /No Rojo tree mapping found for path: out\/unknown\n\nAvailable \$path entries: out\/client, out\/server/,
61
+ );
62
+ });
63
+
64
+ it("should omit available paths line when tree has no $path entries", () => {
65
+ expect.assertions(1);
66
+
67
+ const emptyTree: RojoTreeNode = { $className: "DataModel" };
68
+
69
+ expect(() => mapFsRootToDataModel("out/foo", emptyTree)).toThrow(
70
+ "No Rojo tree mapping found for path: out/foo",
71
+ );
72
+ });
73
+
74
+ it("should strip trailing slash before lookup", () => {
75
+ expect.assertions(1);
76
+
77
+ expect(mapFsRootToDataModel("out/client/", simpleRojoTree)).toBe(
78
+ "ReplicatedStorage/client",
79
+ );
80
+ });
81
+
82
+ it("should look up source path directly for pure Luau", () => {
83
+ expect.assertions(1);
84
+
85
+ const tree: RojoTreeNode = {
86
+ $className: "DataModel",
87
+ ReplicatedStorage: {
88
+ shared: { $path: "shared" },
89
+ },
90
+ };
91
+
92
+ expect(mapFsRootToDataModel("shared", tree)).toBe("ReplicatedStorage/shared");
93
+ });
94
+ });
95
+
96
+ describe(mapFsPathToDataModel, () => {
97
+ it("should map a .luau file path to DataModel path", () => {
98
+ expect.assertions(1);
99
+
100
+ expect(mapFsPathToDataModel("out/client/Button.luau", simpleRojoTree)).toBe(
101
+ "ReplicatedStorage/client/Button",
102
+ );
103
+ });
104
+
105
+ it("should map a .lua file path to DataModel path", () => {
106
+ expect.assertions(1);
107
+
108
+ expect(mapFsPathToDataModel("out/client/Button.lua", simpleRojoTree)).toBe(
109
+ "ReplicatedStorage/client/Button",
110
+ );
111
+ });
112
+
113
+ it("should handle init.luau by mapping to parent", () => {
114
+ expect.assertions(1);
115
+
116
+ expect(mapFsPathToDataModel("out/client/Button/init.luau", simpleRojoTree)).toBe(
117
+ "ReplicatedStorage/client/Button",
118
+ );
119
+ });
120
+
121
+ it("should handle init.lua by mapping to parent", () => {
122
+ expect.assertions(1);
123
+
124
+ expect(mapFsPathToDataModel("out/client/Button/init.lua", simpleRojoTree)).toBe(
125
+ "ReplicatedStorage/client/Button",
126
+ );
127
+ });
128
+
129
+ it("should normalize backslashes", () => {
130
+ expect.assertions(1);
131
+
132
+ expect(mapFsPathToDataModel("out\\client\\Button.luau", simpleRojoTree)).toBe(
133
+ "ReplicatedStorage/client/Button",
134
+ );
135
+ });
136
+
137
+ it("should return undefined when no mapping found", () => {
138
+ expect.assertions(1);
139
+
140
+ expect(mapFsPathToDataModel("unknown/file.luau", simpleRojoTree)).toBeUndefined();
141
+ });
142
+
143
+ it("should handle paths without Luau extension", () => {
144
+ expect.assertions(1);
145
+
146
+ expect(mapFsPathToDataModel("out/client/Button", simpleRojoTree)).toBe(
147
+ "ReplicatedStorage/client/Button",
148
+ );
149
+ });
150
+
151
+ it("should handle deeply nested paths", () => {
152
+ expect.assertions(1);
153
+
154
+ expect(mapFsPathToDataModel("out/client/components/ui/Button.luau", simpleRojoTree)).toBe(
155
+ "ReplicatedStorage/client/components/ui/Button",
156
+ );
157
+ });
158
+
159
+ it("should map root init.luau when tree root has $path", () => {
160
+ expect.assertions(1);
161
+
162
+ const rootOnlyTree: RojoTreeNode = { $path: "out" };
163
+
164
+ expect(mapFsPathToDataModel("out/init.luau", rootOnlyTree)).toBe("");
165
+ });
166
+
167
+ it("should map nested file when tree root has $path", () => {
168
+ expect.assertions(1);
169
+
170
+ const rootOnlyTree: RojoTreeNode = { $path: "out" };
171
+
172
+ expect(mapFsPathToDataModel("out/core/module.luau", rootOnlyTree)).toBe("core/module");
173
+ });
174
+
175
+ it("should map nested init.luau when tree root has $path", () => {
176
+ expect.assertions(1);
177
+
178
+ const rootOnlyTree: RojoTreeNode = { $path: "out" };
179
+
180
+ expect(mapFsPathToDataModel("out/core/init.luau", rootOnlyTree)).toBe("core");
181
+ });
182
+
183
+ it("should return undefined when tree root $path does not match", () => {
184
+ expect.assertions(1);
185
+
186
+ const rootOnlyTree: RojoTreeNode = { $path: "out" };
187
+
188
+ expect(mapFsPathToDataModel("other/file.luau", rootOnlyTree)).toBeUndefined();
189
+ });
190
+
191
+ it("should fall through to findInTree when root $path is an optional object", () => {
192
+ expect.assertions(1);
193
+
194
+ const tree: RojoTreeNode = {
195
+ $path: { optional: "out" },
196
+ ReplicatedStorage: { lib: { $path: "lib" } },
197
+ };
198
+
199
+ expect(mapFsPathToDataModel("lib/foo.luau", tree)).toBe("ReplicatedStorage/lib/foo");
200
+ });
201
+ });
@@ -0,0 +1,45 @@
1
+ import { collectPaths } from "./rojo-tree.ts";
2
+ import { findInTree } from "./tree-mapper.ts";
3
+ import type { RojoTreeNode } from "./types.ts";
4
+
5
+ const TRAILING_SLASH = /\/$/;
6
+
7
+ export function mapFsRootToDataModel(outDirectory: string, rojoTree: RojoTreeNode): string {
8
+ const normalized = outDirectory.replace(TRAILING_SLASH, "");
9
+ const result = findInTree(rojoTree, normalized, "");
10
+ if (result === undefined) {
11
+ const available: Array<string> = [];
12
+ collectPaths(rojoTree, available);
13
+
14
+ let message = `No Rojo tree mapping found for path: ${normalized}`;
15
+ if (available.length > 0) {
16
+ message += `\n\nAvailable $path entries: ${available.join(", ")}`;
17
+ }
18
+
19
+ throw new Error(message);
20
+ }
21
+
22
+ return result;
23
+ }
24
+
25
+ const LUAU_EXTENSIONS = /\.luau?$/;
26
+ const INIT_SUFFIX = /\/init$/;
27
+
28
+ export function mapFsPathToDataModel(fsPath: string, rojoTree: RojoTreeNode): string | undefined {
29
+ const normalized = fsPath.replaceAll("\\", "/").replace(LUAU_EXTENSIONS, "");
30
+ const withoutInit = normalized.replace(INIT_SUFFIX, "");
31
+
32
+ const rootPath = rojoTree.$path;
33
+ if (typeof rootPath === "string") {
34
+ const normalizedRootPath = rootPath.replace(TRAILING_SLASH, "");
35
+ if (withoutInit === normalizedRootPath) {
36
+ return "";
37
+ }
38
+
39
+ if (withoutInit.startsWith(`${normalizedRootPath}/`)) {
40
+ return withoutInit.slice(normalizedRootPath.length + 1);
41
+ }
42
+ }
43
+
44
+ return findInTree(rojoTree, withoutInit, "");
45
+ }