@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
package/src/rojo-tree.ts
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { RojoTreeNode } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Mutable state threaded through {@link resolveTree} during a single
|
|
8
|
+
* resolution.
|
|
9
|
+
*/
|
|
10
|
+
interface ResolveContext {
|
|
11
|
+
/** Absolute paths of every nested project file inlined so far. */
|
|
12
|
+
sources: Set<string>;
|
|
13
|
+
/** Project files on the current chain, for circular-reference detection. */
|
|
14
|
+
visited: Set<string>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Like {@link resolveNestedProjects}, but also reports the absolute path of
|
|
19
|
+
* every nested project file inlined during resolution. Change-detection
|
|
20
|
+
* callers hash these so an edit to a nested `*.project.json` invalidates the
|
|
21
|
+
* build.
|
|
22
|
+
*/
|
|
23
|
+
export function resolveNestedProjectSources(
|
|
24
|
+
tree: RojoTreeNode,
|
|
25
|
+
rootDirectory: string,
|
|
26
|
+
): { projectFiles: Array<string>; tree: RojoTreeNode } {
|
|
27
|
+
const context: ResolveContext = { sources: new Set<string>(), visited: new Set<string>() };
|
|
28
|
+
const resolved = resolveTree(tree, rootDirectory, rootDirectory, context);
|
|
29
|
+
return { projectFiles: [...context.sources], tree: resolved };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function resolveNestedProjects(tree: RojoTreeNode, rootDirectory: string): RojoTreeNode {
|
|
33
|
+
return resolveNestedProjectSources(tree, rootDirectory).tree;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function collectPaths(node: RojoTreeNode, result: Array<string>): void {
|
|
37
|
+
for (const [key, value] of Object.entries(node)) {
|
|
38
|
+
if (key === "$path" && typeof value === "string") {
|
|
39
|
+
result.push(value.replaceAll("\\", "/"));
|
|
40
|
+
} else if (typeof value === "object" && !Array.isArray(value) && !key.startsWith("$")) {
|
|
41
|
+
collectPaths(value as RojoTreeNode, result);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function rebaseTreePaths(
|
|
47
|
+
node: RojoTreeNode,
|
|
48
|
+
fromDirectory: string,
|
|
49
|
+
toDirectory: string,
|
|
50
|
+
): RojoTreeNode {
|
|
51
|
+
const result: RojoTreeNode = {};
|
|
52
|
+
|
|
53
|
+
for (const [key, value] of Object.entries(node)) {
|
|
54
|
+
if (key === "$path" && typeof value === "string") {
|
|
55
|
+
const absolutePath = resolve(fromDirectory, value);
|
|
56
|
+
result[key] = relative(toDirectory, absolutePath).replaceAll("\\", "/");
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (typeof value !== "object" || key.startsWith("$") || Array.isArray(value)) {
|
|
61
|
+
result[key] = value;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
result[key] = rebaseTreePaths(value as RojoTreeNode, fromDirectory, toDirectory);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function nestedProjectPath(currentDirectory: string, value: string): string | undefined {
|
|
72
|
+
// Resolve a `$path` string to the nested project file it should inline, or
|
|
73
|
+
// undefined when the path is a plain source mount. Rojo treats a `$path`
|
|
74
|
+
// pointing at a directory containing `default.project.json` as a nested
|
|
75
|
+
// project (e.g. `$path: ".."` into a package root), so honor that alongside
|
|
76
|
+
// explicit `*.project.json` references.
|
|
77
|
+
if (value.endsWith(".project.json")) {
|
|
78
|
+
return join(currentDirectory, value);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const directoryDefault = join(currentDirectory, value, "default.project.json");
|
|
82
|
+
return existsSync(directoryDefault) ? directoryDefault : undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function inlineNestedProject(
|
|
86
|
+
projectPath: string,
|
|
87
|
+
currentDirectory: string,
|
|
88
|
+
originalRoot: string,
|
|
89
|
+
context: ResolveContext,
|
|
90
|
+
): RojoTreeNode {
|
|
91
|
+
const chain = new Set(context.visited);
|
|
92
|
+
chain.add(projectPath);
|
|
93
|
+
context.sources.add(projectPath);
|
|
94
|
+
|
|
95
|
+
let content: string;
|
|
96
|
+
try {
|
|
97
|
+
content = readFileSync(projectPath, "utf-8");
|
|
98
|
+
} catch (err) {
|
|
99
|
+
const relativePath = relative(currentDirectory, projectPath);
|
|
100
|
+
throw new Error(`Could not read nested Rojo project: ${relativePath}`, { cause: err });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let project: { tree: RojoTreeNode };
|
|
104
|
+
try {
|
|
105
|
+
project = JSON.parse(content) as { tree: RojoTreeNode };
|
|
106
|
+
} catch (err) {
|
|
107
|
+
const relativePath = relative(currentDirectory, projectPath);
|
|
108
|
+
throw new Error(`Failed to parse nested Rojo project: ${relativePath}`, { cause: err });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return resolveTree(project.tree, dirname(projectPath), originalRoot, {
|
|
112
|
+
sources: context.sources,
|
|
113
|
+
visited: chain,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function resolveRootRelativePath(
|
|
118
|
+
currentDirectory: string,
|
|
119
|
+
value: string,
|
|
120
|
+
originalRoot: string,
|
|
121
|
+
): string {
|
|
122
|
+
const absolutePath = join(currentDirectory, value);
|
|
123
|
+
return relative(originalRoot, absolutePath).replaceAll("\\", "/");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function resolveTree(
|
|
127
|
+
node: RojoTreeNode,
|
|
128
|
+
currentDirectory: string,
|
|
129
|
+
originalRoot: string,
|
|
130
|
+
context: ResolveContext,
|
|
131
|
+
): RojoTreeNode {
|
|
132
|
+
const resolved: RojoTreeNode = {};
|
|
133
|
+
|
|
134
|
+
for (const [key, value] of Object.entries(node)) {
|
|
135
|
+
if (key === "$path" && typeof value === "string") {
|
|
136
|
+
const projectPath = nestedProjectPath(currentDirectory, value);
|
|
137
|
+
if (projectPath === undefined) {
|
|
138
|
+
resolved[key] = resolveRootRelativePath(currentDirectory, value, originalRoot);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (context.visited.has(projectPath)) {
|
|
143
|
+
throw new Error(`Circular project reference: ${value}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
Object.assign(
|
|
147
|
+
resolved,
|
|
148
|
+
inlineNestedProject(projectPath, currentDirectory, originalRoot, context),
|
|
149
|
+
);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (typeof value !== "object" || key.startsWith("$") || Array.isArray(value)) {
|
|
154
|
+
resolved[key] = value;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
resolved[key] = resolveTree(value as RojoTreeNode, currentDirectory, originalRoot, context);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return resolved;
|
|
162
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { findInTree, matchNodePath } from "./tree-mapper.ts";
|
|
4
|
+
import type { RojoTreeNode } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
describe(matchNodePath, () => {
|
|
7
|
+
it("should return undefined when $path is not a string", () => {
|
|
8
|
+
expect.assertions(1);
|
|
9
|
+
|
|
10
|
+
const node: RojoTreeNode = { $path: { optional: "maybe" } };
|
|
11
|
+
|
|
12
|
+
expect(matchNodePath(node, "out/client", "ReplicatedStorage/client")).toBeUndefined();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("should return DataModel path on exact match", () => {
|
|
16
|
+
expect.assertions(1);
|
|
17
|
+
|
|
18
|
+
const node: RojoTreeNode = { $path: "out/client" };
|
|
19
|
+
|
|
20
|
+
expect(matchNodePath(node, "out/client", "ReplicatedStorage/client")).toBe(
|
|
21
|
+
"ReplicatedStorage/client",
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should strip trailing slash before matching", () => {
|
|
26
|
+
expect.assertions(1);
|
|
27
|
+
|
|
28
|
+
const node: RojoTreeNode = { $path: "out/client/" };
|
|
29
|
+
|
|
30
|
+
expect(matchNodePath(node, "out/client", "ReplicatedStorage/client")).toBe(
|
|
31
|
+
"ReplicatedStorage/client",
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should return nested DataModel path when target is under $path", () => {
|
|
36
|
+
expect.assertions(1);
|
|
37
|
+
|
|
38
|
+
const node: RojoTreeNode = { $path: "out/client" };
|
|
39
|
+
|
|
40
|
+
expect(matchNodePath(node, "out/client/ui", "ReplicatedStorage/client")).toBe(
|
|
41
|
+
"ReplicatedStorage/client/ui",
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should return undefined when paths do not match", () => {
|
|
46
|
+
expect.assertions(1);
|
|
47
|
+
|
|
48
|
+
const node: RojoTreeNode = { $path: "out/server" };
|
|
49
|
+
|
|
50
|
+
expect(matchNodePath(node, "out/client", "ServerScriptService/server")).toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should not match partial directory names", () => {
|
|
54
|
+
expect.assertions(1);
|
|
55
|
+
|
|
56
|
+
const node: RojoTreeNode = { $path: "out/cli" };
|
|
57
|
+
|
|
58
|
+
expect(matchNodePath(node, "out/client", "ReplicatedStorage/cli")).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should return undefined when node has no $path", () => {
|
|
62
|
+
expect.assertions(1);
|
|
63
|
+
|
|
64
|
+
const node: RojoTreeNode = { $className: "Folder" };
|
|
65
|
+
|
|
66
|
+
expect(matchNodePath(node, "out/client", "ReplicatedStorage/folder")).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const simpleRojoTree: RojoTreeNode = {
|
|
71
|
+
$className: "DataModel",
|
|
72
|
+
ReplicatedStorage: {
|
|
73
|
+
client: { $path: "out/client" },
|
|
74
|
+
},
|
|
75
|
+
ServerScriptService: {
|
|
76
|
+
server: { $path: "out/server" },
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
describe(findInTree, () => {
|
|
81
|
+
it("should find exact $path match", () => {
|
|
82
|
+
expect.assertions(1);
|
|
83
|
+
|
|
84
|
+
expect(findInTree(simpleRojoTree, "out/client", "")).toBe("ReplicatedStorage/client");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should find nested path under $path entry", () => {
|
|
88
|
+
expect.assertions(1);
|
|
89
|
+
|
|
90
|
+
expect(findInTree(simpleRojoTree, "out/client/ui", "")).toBe("ReplicatedStorage/client/ui");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should find deeply nested tree structures", () => {
|
|
94
|
+
expect.assertions(1);
|
|
95
|
+
|
|
96
|
+
const nestedTree: RojoTreeNode = {
|
|
97
|
+
$className: "DataModel",
|
|
98
|
+
ReplicatedStorage: {
|
|
99
|
+
shared: {
|
|
100
|
+
lib: { $path: "out/shared/lib" },
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
expect(findInTree(nestedTree, "out/shared/lib", "")).toBe("ReplicatedStorage/shared/lib");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should return undefined when no match", () => {
|
|
109
|
+
expect.assertions(1);
|
|
110
|
+
|
|
111
|
+
expect(findInTree(simpleRojoTree, "out/unknown", "")).toBeUndefined();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should skip $ prefixed keys", () => {
|
|
115
|
+
expect.assertions(1);
|
|
116
|
+
|
|
117
|
+
const tree: RojoTreeNode = {
|
|
118
|
+
$className: "DataModel",
|
|
119
|
+
$properties: { Name: "Game" },
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
expect(findInTree(tree, "out/client", "")).toBeUndefined();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should skip non-object values", () => {
|
|
126
|
+
expect.assertions(1);
|
|
127
|
+
|
|
128
|
+
const tree: RojoTreeNode = {
|
|
129
|
+
$className: "DataModel",
|
|
130
|
+
$ignoreUnknownInstances: true,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
expect(findInTree(tree, "out/client", "")).toBeUndefined();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("should build DataModel path from current path", () => {
|
|
137
|
+
expect.assertions(1);
|
|
138
|
+
|
|
139
|
+
const subtree: RojoTreeNode = {
|
|
140
|
+
lib: { $path: "out/shared/lib" },
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
expect(findInTree(subtree, "out/shared/lib", "ReplicatedStorage/shared")).toBe(
|
|
144
|
+
"ReplicatedStorage/shared/lib",
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { RojoTreeNode } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
const TRAILING_SLASH = /\/$/;
|
|
4
|
+
|
|
5
|
+
export function matchNodePath(
|
|
6
|
+
childNode: RojoTreeNode,
|
|
7
|
+
targetPath: string,
|
|
8
|
+
childDataModelPath: string,
|
|
9
|
+
): string | undefined {
|
|
10
|
+
const nodePath = childNode.$path;
|
|
11
|
+
if (typeof nodePath !== "string") {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const normalizedNodePath = nodePath.replace(TRAILING_SLASH, "");
|
|
16
|
+
if (normalizedNodePath === targetPath) {
|
|
17
|
+
return childDataModelPath;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (targetPath.startsWith(`${normalizedNodePath}/`)) {
|
|
21
|
+
const remainder = targetPath.slice(normalizedNodePath.length + 1);
|
|
22
|
+
return `${childDataModelPath}/${remainder}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function findInTree(
|
|
29
|
+
node: RojoTreeNode,
|
|
30
|
+
targetPath: string,
|
|
31
|
+
currentDataModelPath: string,
|
|
32
|
+
): string | undefined {
|
|
33
|
+
for (const [key, value] of Object.entries(node)) {
|
|
34
|
+
if (typeof value !== "object" || key.startsWith("$")) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const childNode = value as RojoTreeNode;
|
|
39
|
+
const childDataModelPath =
|
|
40
|
+
currentDataModelPath === "" ? key : `${currentDataModelPath}/${key}`;
|
|
41
|
+
|
|
42
|
+
const pathMatch = matchNodePath(childNode, targetPath, childDataModelPath);
|
|
43
|
+
if (pathMatch !== undefined) {
|
|
44
|
+
return pathMatch;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const found = findInTree(childNode, targetPath, childDataModelPath);
|
|
48
|
+
if (found !== undefined) {
|
|
49
|
+
return found;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface RojoTreeNode {
|
|
2
|
+
$className?: string;
|
|
3
|
+
$ignoreUnknownInstances?: boolean;
|
|
4
|
+
$path?: string | { optional: string };
|
|
5
|
+
$properties?: Record<string, unknown>;
|
|
6
|
+
[key: string]:
|
|
7
|
+
| boolean
|
|
8
|
+
| Record<string, unknown>
|
|
9
|
+
| RojoTreeNode
|
|
10
|
+
| string
|
|
11
|
+
| undefined
|
|
12
|
+
| { optional: string };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface RojoProject {
|
|
16
|
+
name: string;
|
|
17
|
+
servePort?: number;
|
|
18
|
+
tree: RojoTreeNode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Result of `loadRojoProject` — the validated narrow shape plus the original
|
|
23
|
+
* parsed JSON. Callers that need top-level fields beyond name/servePort/tree
|
|
24
|
+
* (gameId, placeId, globIgnorePaths, etc.) read them from `raw` rather than
|
|
25
|
+
* re-parsing the project file.
|
|
26
|
+
*/
|
|
27
|
+
export interface LoadedRojoProject extends RojoProject {
|
|
28
|
+
raw: Record<string, unknown>;
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "jest-extended";
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.node.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "src",
|
|
5
|
+
"allowImportingTsExtensions": true,
|
|
6
|
+
"rewriteRelativeImportExtensions": true,
|
|
7
|
+
"allowJs": false,
|
|
8
|
+
"isolatedDeclarations": true
|
|
9
|
+
},
|
|
10
|
+
"references": [
|
|
11
|
+
{
|
|
12
|
+
"path": "../../tools/eslint-rules/tsconfig.lib.json"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"include": ["src/**/*.ts"],
|
|
16
|
+
"exclude": ["./node_modules", "src/**/*.spec.ts", "src/**/*.spec-d.ts", "**/*.config.ts"]
|
|
17
|
+
}
|