@trioforge/crucible 0.1.0 → 0.3.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/maths/spline/package.json +4 -0
- package/out/client/index.d.ts +7 -0
- package/out/client/init.luau +36 -1
- package/out/configs.d.ts +37 -0
- package/out/configs.luau +46 -0
- package/out/data.d.ts +224 -0
- package/out/data.luau +97 -0
- package/out/emit/configs.d.ts +4 -0
- package/out/emit/configs.luau +22 -0
- package/out/emit/data.d.ts +6 -0
- package/out/emit/data.luau +19 -0
- package/out/emit/index.d.ts +5 -1
- package/out/emit/init.luau +23 -7
- package/out/emit/network.d.ts +6 -0
- package/out/emit/network.luau +36 -0
- package/out/emit/replicated.d.ts +4 -0
- package/out/emit/replicated.luau +13 -0
- package/out/index.d.ts +8 -0
- package/out/init.luau +23 -0
- package/out/internal/binding.luau +5 -0
- package/out/internal/boot.luau +112 -8
- package/out/internal/configs/limits.d.ts +10 -0
- package/out/internal/configs/limits.luau +25 -0
- package/out/internal/configs/port.d.ts +29 -0
- package/out/internal/configs/port.luau +99 -0
- package/out/internal/configs/runtime.d.ts +21 -0
- package/out/internal/configs/runtime.luau +981 -0
- package/out/internal/data/admission.d.ts +1 -0
- package/out/internal/data/admission.luau +673 -0
- package/out/internal/data/declarations.d.ts +1 -0
- package/out/internal/data/declarations.luau +384 -0
- package/out/internal/data/envelope.d.ts +1 -0
- package/out/internal/data/envelope.luau +318 -0
- package/out/internal/data/handles.d.ts +1 -0
- package/out/internal/data/handles.luau +255 -0
- package/out/internal/data/inbox-transforms.d.ts +1 -0
- package/out/internal/data/inbox-transforms.luau +458 -0
- package/out/internal/data/inbox.d.ts +1 -0
- package/out/internal/data/inbox.luau +437 -0
- package/out/internal/data/lease.d.ts +1 -0
- package/out/internal/data/lease.luau +304 -0
- package/out/internal/data/limits.d.ts +1 -0
- package/out/internal/data/limits.luau +234 -0
- package/out/internal/data/messages.d.ts +1 -0
- package/out/internal/data/messages.luau +69 -0
- package/out/internal/data/operation-id.d.ts +1 -0
- package/out/internal/data/operation-id.luau +61 -0
- package/out/internal/data/paths.d.ts +2 -0
- package/out/internal/data/paths.luau +323 -0
- package/out/internal/data/port.d.ts +1 -0
- package/out/internal/data/port.luau +290 -0
- package/out/internal/data/profiles.d.ts +1 -0
- package/out/internal/data/profiles.luau +862 -0
- package/out/internal/data/receipts.d.ts +1 -0
- package/out/internal/data/receipts.luau +190 -0
- package/out/internal/data/record-bindings.d.ts +1 -0
- package/out/internal/data/record-bindings.luau +92 -0
- package/out/internal/data/record-declarations.d.ts +1 -0
- package/out/internal/data/record-declarations.luau +140 -0
- package/out/internal/data/records.d.ts +1 -0
- package/out/internal/data/records.luau +400 -0
- package/out/internal/data/runtime.d.ts +1 -0
- package/out/internal/data/runtime.luau +918 -0
- package/out/internal/data/scheduler.d.ts +1 -0
- package/out/internal/data/scheduler.luau +507 -0
- package/out/internal/data/session.d.ts +1 -0
- package/out/internal/data/session.luau +234 -0
- package/out/internal/data/shutdown.d.ts +1 -0
- package/out/internal/data/shutdown.luau +91 -0
- package/out/internal/data/waiters.d.ts +1 -0
- package/out/internal/data/waiters.luau +65 -0
- package/out/internal/lifecycle.luau +45 -0
- package/out/internal/network/catalog.d.ts +26 -0
- package/out/internal/network/catalog.luau +182 -0
- package/out/internal/network/claims.d.ts +1 -0
- package/out/internal/network/claims.luau +146 -0
- package/out/internal/network/client-session.d.ts +1 -0
- package/out/internal/network/client-session.luau +405 -0
- package/out/internal/network/codec.d.ts +63 -0
- package/out/internal/network/codec.luau +1183 -0
- package/out/internal/network/envelope.d.ts +1 -0
- package/out/internal/network/envelope.luau +247 -0
- package/out/internal/network/limits.d.ts +26 -0
- package/out/internal/network/limits.luau +55 -0
- package/out/internal/network/port.d.ts +1 -0
- package/out/internal/network/port.luau +19 -0
- package/out/internal/network/roblox-network-port.d.ts +1 -0
- package/out/internal/network/roblox-network-port.luau +105 -0
- package/out/internal/network/schema.d.ts +72 -0
- package/out/internal/network/schema.luau +386 -0
- package/out/internal/network/server-session.d.ts +1 -0
- package/out/internal/network/server-session.luau +395 -0
- package/out/internal/network/session.d.ts +1 -0
- package/out/internal/network/session.luau +266 -0
- package/out/internal/reactive/context.d.ts +1 -0
- package/out/internal/reactive/context.luau +149 -0
- package/out/internal/reactive/flush.d.ts +1 -0
- package/out/internal/reactive/flush.luau +355 -0
- package/out/internal/reactive/graph.d.ts +1 -0
- package/out/internal/reactive/graph.luau +626 -0
- package/out/internal/reactive/limits.d.ts +1 -0
- package/out/internal/reactive/limits.luau +54 -0
- package/out/internal/reactive/ownership.d.ts +1 -0
- package/out/internal/reactive/ownership.luau +626 -0
- package/out/internal/registry.luau +10 -1
- package/out/internal/sync/client-sync.d.ts +1 -0
- package/out/internal/sync/client-sync.luau +157 -0
- package/out/internal/sync/declarations.d.ts +1 -0
- package/out/internal/sync/declarations.luau +276 -0
- package/out/internal/sync/frame.d.ts +1 -0
- package/out/internal/sync/frame.luau +91 -0
- package/out/internal/sync/server-sync.d.ts +1 -0
- package/out/internal/sync/server-sync.luau +502 -0
- package/out/internal/sync/values.d.ts +1 -0
- package/out/internal/sync/values.luau +930 -0
- package/out/maths/spline/curve.d.ts +35 -0
- package/out/maths/spline/curve.luau +1122 -0
- package/out/maths/spline/index.d.ts +2 -0
- package/out/maths/spline/init.luau +8 -0
- package/out/network.d.ts +66 -0
- package/out/network.luau +24 -0
- package/out/reactive.d.ts +76 -0
- package/out/reactive.luau +136 -0
- package/out/replicated.d.ts +38 -0
- package/out/replicated.luau +45 -0
- package/out/server/data.d.ts +54 -0
- package/out/server/data.luau +44 -0
- package/out/server/index.d.ts +17 -0
- package/out/server/init.luau +58 -4
- package/out/server/publish.d.ts +20 -0
- package/out/server/publish.luau +24 -0
- package/out/testing/index.d.ts +38 -0
- package/out/testing/init.luau +189 -0
- package/out/util/random/index.d.ts +24 -0
- package/out/util/random/init.luau +301 -0
- package/out/vide/index.d.ts +7 -0
- package/out/vide/init.luau +50 -0
- package/package.json +28 -2
- package/testing/package.json +4 -0
- package/transformer/data.cjs +72 -0
- package/transformer/declarations.cjs +153 -0
- package/transformer/index.js +87 -23
- package/transformer/network.cjs +961 -0
- package/util/random/package.json +4 -0
- package/vide/package.json +4 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
function createDataChecks({ ts, program, checker, api }) {
|
|
2
|
+
const unwrap = (expression) => {
|
|
3
|
+
while (ts.isParenthesizedExpression(expression) || ts.isNonNullExpression(expression))
|
|
4
|
+
expression = expression.expression;
|
|
5
|
+
return expression;
|
|
6
|
+
};
|
|
7
|
+
const member = (expression) => {
|
|
8
|
+
if (ts.isPropertyAccessExpression(expression)) return expression.name.text;
|
|
9
|
+
if (ts.isElementAccessExpression(expression) && ts.isStringLiteralLike(expression.argumentExpression))
|
|
10
|
+
return expression.argumentExpression.text;
|
|
11
|
+
};
|
|
12
|
+
const store = (expression) => api(checker.getTypeAtLocation(expression).getSymbol()) === "ProfileStore";
|
|
13
|
+
const marketplace = (expression) => {
|
|
14
|
+
const symbol = checker.getTypeAtLocation(expression).getSymbol();
|
|
15
|
+
return (
|
|
16
|
+
symbol?.name === "MarketplaceService" &&
|
|
17
|
+
symbol.declarations?.some((d) =>
|
|
18
|
+
d.getSourceFile().fileName.replaceAll("\\", "/").includes("/@rbxts/types/"),
|
|
19
|
+
)
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
const storeCall = (node) => {
|
|
23
|
+
const callee = unwrap(node.expression);
|
|
24
|
+
if (store(callee)) return { store: callee };
|
|
25
|
+
const name = member(callee);
|
|
26
|
+
if (name !== undefined && store(callee.expression)) return { store: callee.expression, member: name };
|
|
27
|
+
};
|
|
28
|
+
let granted;
|
|
29
|
+
const grants = () => {
|
|
30
|
+
if (granted === undefined) {
|
|
31
|
+
granted = false;
|
|
32
|
+
const walk = (node) => {
|
|
33
|
+
if (granted) return;
|
|
34
|
+
if (ts.isCallExpression(node) && storeCall(node)?.member === "grant") granted = true;
|
|
35
|
+
else ts.forEachChild(node, walk);
|
|
36
|
+
};
|
|
37
|
+
for (const source of program.getSourceFiles()) if (!source.isDeclarationFile) walk(source);
|
|
38
|
+
}
|
|
39
|
+
return granted;
|
|
40
|
+
};
|
|
41
|
+
return ({ report, side }) =>
|
|
42
|
+
(node) => {
|
|
43
|
+
if (ts.isCallExpression(node)) {
|
|
44
|
+
const call = storeCall(node);
|
|
45
|
+
if (call) {
|
|
46
|
+
const text = call.store.getText();
|
|
47
|
+
const client =
|
|
48
|
+
call.member === undefined || call.member === "loaded" ? !node.arguments.length : false;
|
|
49
|
+
const required = client ? "client" : "server";
|
|
50
|
+
if (side(node) !== required)
|
|
51
|
+
report(
|
|
52
|
+
node,
|
|
53
|
+
"CRU027",
|
|
54
|
+
client
|
|
55
|
+
? `${text}${call.member ? ".loaded" : ""}() reads the local player's profile; on the server call ${text}${call.member ? ".loaded" : ""}(player).`
|
|
56
|
+
: `${text}${call.member === undefined ? "(player)" : `.${call.member}`} is server-only; the client reads ${text}().`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
61
|
+
const target = unwrap(node.left);
|
|
62
|
+
if (member(target) === "ProcessReceipt" && marketplace(target.expression) && grants())
|
|
63
|
+
report(
|
|
64
|
+
node,
|
|
65
|
+
"CRU025",
|
|
66
|
+
"Crucible's data module owns ProcessReceipt; register grants with a profile store's grant(productId, ...).",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { createDataChecks };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
const limits = Object.freeze({
|
|
2
|
+
MESSAGE_NAME_MAX_BYTES: 64,
|
|
3
|
+
MAX_ASK_TIMEOUT_SECONDS: 120,
|
|
4
|
+
MAX_UNION_BRANCHES: 256,
|
|
5
|
+
MAX_SCHEMA_NODES: 4096,
|
|
6
|
+
MAX_DEPTH: 32,
|
|
7
|
+
});
|
|
8
|
+
const reactiveLimits = Object.freeze({
|
|
9
|
+
MESSAGE_NAME_MAX_BYTES: 64,
|
|
10
|
+
SYNC_VALUE_DEPTH: 28,
|
|
11
|
+
});
|
|
12
|
+
const dataLimits = Object.freeze({
|
|
13
|
+
STORE_NAME_MAX_BYTES: 50,
|
|
14
|
+
MESSAGE_NAME_MAX_BYTES: 64,
|
|
15
|
+
CONFIG_NAME_MAX_BYTES: 64,
|
|
16
|
+
PERSIST_DEPTH: 28,
|
|
17
|
+
VIEW_NAME_PREFIX: "Crucible.Data.",
|
|
18
|
+
LIVE_NAME_PREFIX: "Crucible.Live.",
|
|
19
|
+
VIEW_NAME_MAX_BYTES: 64,
|
|
20
|
+
RESERVED_HANDLE_NAMES: Object.freeze(["set", "update", "insert", "remove"]),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const message = {
|
|
24
|
+
label: "messages",
|
|
25
|
+
side: "shared",
|
|
26
|
+
names: "Message",
|
|
27
|
+
maxBytes: limits.MESSAGE_NAME_MAX_BYTES,
|
|
28
|
+
typeArguments: 1,
|
|
29
|
+
arguments: 1,
|
|
30
|
+
passed: [],
|
|
31
|
+
position: "payload",
|
|
32
|
+
};
|
|
33
|
+
const replicated = {
|
|
34
|
+
label: "replicated state",
|
|
35
|
+
side: "shared",
|
|
36
|
+
names: "Replicated",
|
|
37
|
+
maxBytes: reactiveLimits.MESSAGE_NAME_MAX_BYTES,
|
|
38
|
+
typeArguments: 1,
|
|
39
|
+
arguments: 2,
|
|
40
|
+
passed: [1],
|
|
41
|
+
position: "state",
|
|
42
|
+
};
|
|
43
|
+
const config = {
|
|
44
|
+
label: "configs",
|
|
45
|
+
side: "server",
|
|
46
|
+
names: "Config",
|
|
47
|
+
maxBytes: dataLimits.CONFIG_NAME_MAX_BYTES,
|
|
48
|
+
typeArguments: 1,
|
|
49
|
+
arguments: 2,
|
|
50
|
+
passed: [1],
|
|
51
|
+
position: "persist",
|
|
52
|
+
};
|
|
53
|
+
const kinds = Object.freeze({
|
|
54
|
+
toServer: message,
|
|
55
|
+
toClient: message,
|
|
56
|
+
askServer: { ...message, typeArguments: 2, arguments: 2 },
|
|
57
|
+
replicated,
|
|
58
|
+
replicatedPerPlayer: replicated,
|
|
59
|
+
profileStore: {
|
|
60
|
+
label: "profile stores",
|
|
61
|
+
side: "shared",
|
|
62
|
+
names: "Store",
|
|
63
|
+
maxBytes: dataLimits.STORE_NAME_MAX_BYTES,
|
|
64
|
+
typeArguments: "optional",
|
|
65
|
+
arguments: 2,
|
|
66
|
+
passed: [1],
|
|
67
|
+
position: "persist",
|
|
68
|
+
store: true,
|
|
69
|
+
handles: true,
|
|
70
|
+
},
|
|
71
|
+
dataStore: {
|
|
72
|
+
label: "data stores",
|
|
73
|
+
side: "server",
|
|
74
|
+
names: "Store",
|
|
75
|
+
maxBytes: dataLimits.STORE_NAME_MAX_BYTES,
|
|
76
|
+
typeArguments: 1,
|
|
77
|
+
arguments: 2,
|
|
78
|
+
passed: [1],
|
|
79
|
+
position: "persist",
|
|
80
|
+
store: true,
|
|
81
|
+
},
|
|
82
|
+
profileMessage: {
|
|
83
|
+
label: "profile messages",
|
|
84
|
+
side: "server",
|
|
85
|
+
names: "Profile message",
|
|
86
|
+
maxBytes: dataLimits.MESSAGE_NAME_MAX_BYTES,
|
|
87
|
+
typeArguments: 1,
|
|
88
|
+
arguments: 1,
|
|
89
|
+
passed: [],
|
|
90
|
+
position: "persist",
|
|
91
|
+
},
|
|
92
|
+
config,
|
|
93
|
+
playerConfig: config,
|
|
94
|
+
});
|
|
95
|
+
const depthLimits = Object.freeze({
|
|
96
|
+
payload: limits.MAX_DEPTH,
|
|
97
|
+
state: reactiveLimits.SYNC_VALUE_DEPTH,
|
|
98
|
+
persist: dataLimits.PERSIST_DEPTH,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Marks sit only on object fields from the root, so only object fields can lose every field.
|
|
102
|
+
function replica(node) {
|
|
103
|
+
if (node.kind !== "object") return node;
|
|
104
|
+
const fields = [];
|
|
105
|
+
for (const [key, child] of node.fields) {
|
|
106
|
+
if (child.serverOnly) continue;
|
|
107
|
+
const kept = replica(child);
|
|
108
|
+
if (kept.kind === "object" && !kept.fields.length) continue;
|
|
109
|
+
fields.push([key, kept]);
|
|
110
|
+
}
|
|
111
|
+
return { kind: "object", fields };
|
|
112
|
+
}
|
|
113
|
+
// Object fields are sorted by bytes, so a depth-first walk lists paths in rule 131 order, a prefix first.
|
|
114
|
+
function serverOnlyPaths(node, prefix = []) {
|
|
115
|
+
if (node.kind !== "object") return [];
|
|
116
|
+
return node.fields.flatMap(([key, child]) =>
|
|
117
|
+
child.serverOnly ? [[...prefix, key]] : serverOnlyPaths(child, [...prefix, key]),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
const SEGMENT = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
121
|
+
function viewNameProblems(store, node) {
|
|
122
|
+
if (node.kind !== "object") return [];
|
|
123
|
+
const prefix = `${dataLimits.VIEW_NAME_PREFIX}${store}.`;
|
|
124
|
+
return node.fields.flatMap(([field]) => {
|
|
125
|
+
const name = prefix + field;
|
|
126
|
+
if (!SEGMENT.test(field))
|
|
127
|
+
return [
|
|
128
|
+
[
|
|
129
|
+
`value[${JSON.stringify(field)}]`,
|
|
130
|
+
"a field the owner's client receives is named by one declaration segment, [A-Za-z][A-Za-z0-9_]*; rename it or mark it serverOnly",
|
|
131
|
+
],
|
|
132
|
+
];
|
|
133
|
+
if (Buffer.byteLength(name) > dataLimits.VIEW_NAME_MAX_BYTES)
|
|
134
|
+
return [
|
|
135
|
+
[
|
|
136
|
+
`value.${field}`,
|
|
137
|
+
`its view name ${name} is ${Buffer.byteLength(name)} bytes, over ${dataLimits.VIEW_NAME_MAX_BYTES}; this store allows field names of at most ${dataLimits.VIEW_NAME_MAX_BYTES - Buffer.byteLength(prefix)} bytes; rename the field, shorten the store name, or mark it serverOnly`,
|
|
138
|
+
],
|
|
139
|
+
];
|
|
140
|
+
return [];
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = {
|
|
145
|
+
limits,
|
|
146
|
+
reactiveLimits,
|
|
147
|
+
dataLimits,
|
|
148
|
+
kinds,
|
|
149
|
+
depthLimits,
|
|
150
|
+
replica,
|
|
151
|
+
serverOnlyPaths,
|
|
152
|
+
viewNameProblems,
|
|
153
|
+
};
|
package/transformer/index.js
CHANGED
|
@@ -2,6 +2,8 @@ const path = require("node:path");
|
|
|
2
2
|
const { createRequire } = require("node:module");
|
|
3
3
|
const { packageOf, typeId, sourceRoot } = require("./type-id.cjs");
|
|
4
4
|
const { createReporter } = require("./diagnostics.cjs");
|
|
5
|
+
const { createNetwork } = require("./network.cjs");
|
|
6
|
+
const { createDataChecks } = require("./data.cjs");
|
|
5
7
|
|
|
6
8
|
module.exports = function transformer(program, config = {}, extras = {}) {
|
|
7
9
|
const ts = extras.ts ?? require("typescript");
|
|
@@ -9,10 +11,20 @@ module.exports = function transformer(program, config = {}, extras = {}) {
|
|
|
9
11
|
const f = ts.factory;
|
|
10
12
|
const symbols = new Map();
|
|
11
13
|
const unalias = (s) => s && (s.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(s) : s);
|
|
12
|
-
|
|
14
|
+
const describe = (error) =>
|
|
15
|
+
error instanceof RangeError && error.message === "Maximum call stack size exceeded"
|
|
16
|
+
? "Cannot transform input: the type checker ran out of stack. A type in this file nests or inherits too deeply to analyse."
|
|
17
|
+
: `Cannot transform input: ${String(error instanceof Error ? error.message : error).split(/\r?\n/)[0]}`;
|
|
18
|
+
let startupFault;
|
|
13
19
|
try {
|
|
14
20
|
for (const source of program.getSourceFiles()) {
|
|
15
|
-
|
|
21
|
+
let owner;
|
|
22
|
+
try {
|
|
23
|
+
owner = packageOf(source.fileName);
|
|
24
|
+
} catch {
|
|
25
|
+
startupFault = "Cannot read package metadata. Check the project's package.json files.";
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
16
28
|
if (owner?.json.name !== "@trioforge/crucible") continue;
|
|
17
29
|
const module = checker.getSymbolAtLocation(source);
|
|
18
30
|
if (!module) continue;
|
|
@@ -21,8 +33,8 @@ module.exports = function transformer(program, config = {}, extras = {}) {
|
|
|
21
33
|
if (!symbols.has(target)) symbols.set(target, symbol.name);
|
|
22
34
|
}
|
|
23
35
|
}
|
|
24
|
-
} catch {
|
|
25
|
-
|
|
36
|
+
} catch (error) {
|
|
37
|
+
startupFault = describe(error);
|
|
26
38
|
}
|
|
27
39
|
const api = (symbol) => symbols.get(unalias(symbol));
|
|
28
40
|
const modifiers = (node, flags) => (ts.getCombinedModifierFlags(node) & flags) !== 0;
|
|
@@ -46,12 +58,14 @@ module.exports = function transformer(program, config = {}, extras = {}) {
|
|
|
46
58
|
};
|
|
47
59
|
const diagnosticSink = extras.addDiagnostic;
|
|
48
60
|
let report;
|
|
61
|
+
let networkProgram;
|
|
62
|
+
let dataProgram;
|
|
49
63
|
return (context) => (source) => {
|
|
50
64
|
if (source.isDeclarationFile) return source;
|
|
51
65
|
let used = false;
|
|
52
66
|
report ??= createReporter(ts, diagnosticSink ?? ((d) => context.addDiagnostic(d)));
|
|
53
|
-
if (
|
|
54
|
-
report(source, "Crucible",
|
|
67
|
+
if (startupFault) {
|
|
68
|
+
report(source, "Crucible", startupFault);
|
|
55
69
|
return source;
|
|
56
70
|
}
|
|
57
71
|
const isGlobal = (decl) => {
|
|
@@ -326,6 +340,26 @@ module.exports = function transformer(program, config = {}, extras = {}) {
|
|
|
326
340
|
call(runtime("defineClass"), [f.createIdentifier(node.name.text), object(data)]),
|
|
327
341
|
);
|
|
328
342
|
};
|
|
343
|
+
// A Rojo project may map a folder's files one by one rather than the folder, as Rogen does before their
|
|
344
|
+
// outputs exist. Each such file names the instance of the folder that holds it.
|
|
345
|
+
const mappedByFiles = (resolver, folder, out) => {
|
|
346
|
+
const found = new Set();
|
|
347
|
+
for (const source of program.getSourceFiles()) {
|
|
348
|
+
const parts = path
|
|
349
|
+
.relative(folder, source.fileName)
|
|
350
|
+
.replace(/\.tsx?$/, "")
|
|
351
|
+
.split(path.sep);
|
|
352
|
+
const name = parts.at(-1).replace(/\.(server|client)$/, "");
|
|
353
|
+
if (source.isDeclarationFile || parts[0] === ".." || path.isAbsolute(parts[0]) || name === "index")
|
|
354
|
+
continue;
|
|
355
|
+
const rbx = resolver.getRbxPathFromFilePath(path.join(out, ...parts) + ".luau");
|
|
356
|
+
const tail = [...parts.slice(0, -1), name];
|
|
357
|
+
if (rbx?.length > tail.length && tail.every((part, at) => rbx.at(at - tail.length) === part))
|
|
358
|
+
found.add(rbx.slice(0, -tail.length).join("\0"));
|
|
359
|
+
}
|
|
360
|
+
if (found.size > 1) return null;
|
|
361
|
+
return found.size ? [...found][0].split("\0") : undefined;
|
|
362
|
+
};
|
|
329
363
|
const boot = (node) => {
|
|
330
364
|
let options = node.arguments[0];
|
|
331
365
|
while (
|
|
@@ -360,10 +394,10 @@ module.exports = function transformer(program, config = {}, extras = {}) {
|
|
|
360
394
|
report(entry, "CRU006", "load entry must be a string literal.");
|
|
361
395
|
return entry;
|
|
362
396
|
}
|
|
363
|
-
const
|
|
397
|
+
const configFile =
|
|
364
398
|
program.getCompilerOptions().configFilePath ??
|
|
365
|
-
|
|
366
|
-
);
|
|
399
|
+
path.join(program.getCurrentDirectory(), "tsconfig.json");
|
|
400
|
+
const root = packageOf(configFile)?.dir ?? path.dirname(configFile);
|
|
367
401
|
let resolver;
|
|
368
402
|
try {
|
|
369
403
|
const compiler =
|
|
@@ -371,8 +405,10 @@ module.exports = function transformer(program, config = {}, extras = {}) {
|
|
|
371
405
|
/roblox-ts[\\/]out[\\/]Project[\\/]functions[\\/]compileFiles\.js$/.test(file),
|
|
372
406
|
) ?? require.resolve("roblox-ts/package.json", { paths: [root, __dirname] });
|
|
373
407
|
const { RojoResolver } = createRequire(compiler)("@roblox-ts/rojo-resolver");
|
|
374
|
-
const rojoPath = config.rojo
|
|
375
|
-
|
|
408
|
+
const rojoPath = config.rojo
|
|
409
|
+
? path.resolve(root, config.rojo)
|
|
410
|
+
: RojoResolver.findRojoConfigFilePath(path.dirname(configFile)).path;
|
|
411
|
+
resolver = rojoPath && RojoResolver.fromPath(rojoPath);
|
|
376
412
|
} catch {
|
|
377
413
|
report(entry, "CRU006", "Cannot load the running compiler's Rojo resolver or project mapping.");
|
|
378
414
|
return entry;
|
|
@@ -385,12 +421,18 @@ module.exports = function transformer(program, config = {}, extras = {}) {
|
|
|
385
421
|
const suffix = entry.text.slice(prefix.length).replace(/\.tsx?$/, "");
|
|
386
422
|
const opts = program.getCompilerOptions();
|
|
387
423
|
const inputRoot = sourceRoot(opts, [program.getCommonSourceDirectory()]);
|
|
388
|
-
const
|
|
424
|
+
const folder = path.resolve(root, prefix);
|
|
425
|
+
const relative = inputRoot && path.relative(inputRoot, folder);
|
|
426
|
+
const inside = relative !== undefined && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
427
|
+
const out = inside && path.resolve(opts.outDir, relative);
|
|
389
428
|
const mapped =
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
429
|
+
out &&
|
|
430
|
+
resolver &&
|
|
431
|
+
(resolver.getRbxPathFromFilePath(out) ?? mappedByFiles(resolver, folder, out));
|
|
432
|
+
if (mapped === null) {
|
|
433
|
+
report(entry, "CRU006", "load prefix maps to more than one Rojo folder.");
|
|
434
|
+
return entry;
|
|
435
|
+
}
|
|
394
436
|
if (!mapped) {
|
|
395
437
|
report(entry, "CRU006", "load prefix is outside the Rojo mapping.");
|
|
396
438
|
return entry;
|
|
@@ -499,7 +541,11 @@ module.exports = function transformer(program, config = {}, extras = {}) {
|
|
|
499
541
|
.slice(0, -1)
|
|
500
542
|
.reverse()
|
|
501
543
|
.find((part) => ["server", "client", "shared"].includes(part));
|
|
544
|
+
let network;
|
|
545
|
+
let data;
|
|
502
546
|
const inspect = (node) => {
|
|
547
|
+
network.inspect(node);
|
|
548
|
+
data(node);
|
|
503
549
|
if (
|
|
504
550
|
ts.isImportDeclaration(node) ||
|
|
505
551
|
ts.isExportDeclaration(node) ||
|
|
@@ -513,9 +559,19 @@ module.exports = function transformer(program, config = {}, extras = {}) {
|
|
|
513
559
|
specifier &&
|
|
514
560
|
ts.isStringLiteral(specifier) &&
|
|
515
561
|
specifier.text.startsWith("@trioforge/crucible/") &&
|
|
516
|
-
![
|
|
562
|
+
![
|
|
563
|
+
"@trioforge/crucible/server",
|
|
564
|
+
"@trioforge/crucible/client",
|
|
565
|
+
"@trioforge/crucible/vide",
|
|
566
|
+
"@trioforge/crucible/util/random",
|
|
567
|
+
"@trioforge/crucible/maths/spline",
|
|
568
|
+
].includes(specifier.text)
|
|
517
569
|
)
|
|
518
|
-
report(
|
|
570
|
+
report(
|
|
571
|
+
node,
|
|
572
|
+
"CRU004",
|
|
573
|
+
"Import only the root, server, client, vide, util/random or maths/spline Crucible entries.",
|
|
574
|
+
);
|
|
519
575
|
}
|
|
520
576
|
if (ts.isClassExpression(node) || ts.isClassDeclaration(node)) {
|
|
521
577
|
const type = instanceType(node);
|
|
@@ -622,6 +678,8 @@ module.exports = function transformer(program, config = {}, extras = {}) {
|
|
|
622
678
|
};
|
|
623
679
|
|
|
624
680
|
const visit = (node) => {
|
|
681
|
+
const networkNode = network.visit(node);
|
|
682
|
+
if (networkNode) return networkNode;
|
|
625
683
|
if (isMacro(node, "attribute")) {
|
|
626
684
|
const field = node.parent;
|
|
627
685
|
if (
|
|
@@ -694,6 +752,16 @@ module.exports = function transformer(program, config = {}, extras = {}) {
|
|
|
694
752
|
return next;
|
|
695
753
|
};
|
|
696
754
|
try {
|
|
755
|
+
networkProgram ??= createNetwork({ ts, program, checker, api, kindOf, instanceType, modifiers, isMacro });
|
|
756
|
+
network = networkProgram({
|
|
757
|
+
source,
|
|
758
|
+
report,
|
|
759
|
+
runtime,
|
|
760
|
+
side,
|
|
761
|
+
visitInput: (node) => ts.visitNode(node, visit),
|
|
762
|
+
});
|
|
763
|
+
dataProgram ??= createDataChecks({ ts, program, checker, api });
|
|
764
|
+
data = dataProgram({ report, side });
|
|
697
765
|
inspect(source);
|
|
698
766
|
const next = ts.visitEachChild(source, visit, context);
|
|
699
767
|
if (!used) return next;
|
|
@@ -704,11 +772,7 @@ module.exports = function transformer(program, config = {}, extras = {}) {
|
|
|
704
772
|
);
|
|
705
773
|
return f.updateSourceFile(next, [imports, ...next.statements]);
|
|
706
774
|
} catch (error) {
|
|
707
|
-
report(
|
|
708
|
-
source,
|
|
709
|
-
"Crucible",
|
|
710
|
-
`Cannot transform input: ${String(error instanceof Error ? error.message : error).split(/\r?\n/)[0]}`,
|
|
711
|
-
);
|
|
775
|
+
report(source, "Crucible", describe(error));
|
|
712
776
|
return source;
|
|
713
777
|
}
|
|
714
778
|
};
|