@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.
Files changed (145) hide show
  1. package/maths/spline/package.json +4 -0
  2. package/out/client/index.d.ts +7 -0
  3. package/out/client/init.luau +36 -1
  4. package/out/configs.d.ts +37 -0
  5. package/out/configs.luau +46 -0
  6. package/out/data.d.ts +224 -0
  7. package/out/data.luau +97 -0
  8. package/out/emit/configs.d.ts +4 -0
  9. package/out/emit/configs.luau +22 -0
  10. package/out/emit/data.d.ts +6 -0
  11. package/out/emit/data.luau +19 -0
  12. package/out/emit/index.d.ts +5 -1
  13. package/out/emit/init.luau +23 -7
  14. package/out/emit/network.d.ts +6 -0
  15. package/out/emit/network.luau +36 -0
  16. package/out/emit/replicated.d.ts +4 -0
  17. package/out/emit/replicated.luau +13 -0
  18. package/out/index.d.ts +8 -0
  19. package/out/init.luau +23 -0
  20. package/out/internal/binding.luau +5 -0
  21. package/out/internal/boot.luau +112 -8
  22. package/out/internal/configs/limits.d.ts +10 -0
  23. package/out/internal/configs/limits.luau +25 -0
  24. package/out/internal/configs/port.d.ts +29 -0
  25. package/out/internal/configs/port.luau +99 -0
  26. package/out/internal/configs/runtime.d.ts +21 -0
  27. package/out/internal/configs/runtime.luau +981 -0
  28. package/out/internal/data/admission.d.ts +1 -0
  29. package/out/internal/data/admission.luau +673 -0
  30. package/out/internal/data/declarations.d.ts +1 -0
  31. package/out/internal/data/declarations.luau +384 -0
  32. package/out/internal/data/envelope.d.ts +1 -0
  33. package/out/internal/data/envelope.luau +318 -0
  34. package/out/internal/data/handles.d.ts +1 -0
  35. package/out/internal/data/handles.luau +255 -0
  36. package/out/internal/data/inbox-transforms.d.ts +1 -0
  37. package/out/internal/data/inbox-transforms.luau +458 -0
  38. package/out/internal/data/inbox.d.ts +1 -0
  39. package/out/internal/data/inbox.luau +437 -0
  40. package/out/internal/data/lease.d.ts +1 -0
  41. package/out/internal/data/lease.luau +304 -0
  42. package/out/internal/data/limits.d.ts +1 -0
  43. package/out/internal/data/limits.luau +234 -0
  44. package/out/internal/data/messages.d.ts +1 -0
  45. package/out/internal/data/messages.luau +69 -0
  46. package/out/internal/data/operation-id.d.ts +1 -0
  47. package/out/internal/data/operation-id.luau +61 -0
  48. package/out/internal/data/paths.d.ts +2 -0
  49. package/out/internal/data/paths.luau +323 -0
  50. package/out/internal/data/port.d.ts +1 -0
  51. package/out/internal/data/port.luau +290 -0
  52. package/out/internal/data/profiles.d.ts +1 -0
  53. package/out/internal/data/profiles.luau +862 -0
  54. package/out/internal/data/receipts.d.ts +1 -0
  55. package/out/internal/data/receipts.luau +190 -0
  56. package/out/internal/data/record-bindings.d.ts +1 -0
  57. package/out/internal/data/record-bindings.luau +92 -0
  58. package/out/internal/data/record-declarations.d.ts +1 -0
  59. package/out/internal/data/record-declarations.luau +140 -0
  60. package/out/internal/data/records.d.ts +1 -0
  61. package/out/internal/data/records.luau +400 -0
  62. package/out/internal/data/runtime.d.ts +1 -0
  63. package/out/internal/data/runtime.luau +918 -0
  64. package/out/internal/data/scheduler.d.ts +1 -0
  65. package/out/internal/data/scheduler.luau +507 -0
  66. package/out/internal/data/session.d.ts +1 -0
  67. package/out/internal/data/session.luau +234 -0
  68. package/out/internal/data/shutdown.d.ts +1 -0
  69. package/out/internal/data/shutdown.luau +91 -0
  70. package/out/internal/data/waiters.d.ts +1 -0
  71. package/out/internal/data/waiters.luau +65 -0
  72. package/out/internal/lifecycle.luau +45 -0
  73. package/out/internal/network/catalog.d.ts +26 -0
  74. package/out/internal/network/catalog.luau +182 -0
  75. package/out/internal/network/claims.d.ts +1 -0
  76. package/out/internal/network/claims.luau +146 -0
  77. package/out/internal/network/client-session.d.ts +1 -0
  78. package/out/internal/network/client-session.luau +405 -0
  79. package/out/internal/network/codec.d.ts +63 -0
  80. package/out/internal/network/codec.luau +1183 -0
  81. package/out/internal/network/envelope.d.ts +1 -0
  82. package/out/internal/network/envelope.luau +247 -0
  83. package/out/internal/network/limits.d.ts +26 -0
  84. package/out/internal/network/limits.luau +55 -0
  85. package/out/internal/network/port.d.ts +1 -0
  86. package/out/internal/network/port.luau +19 -0
  87. package/out/internal/network/roblox-network-port.d.ts +1 -0
  88. package/out/internal/network/roblox-network-port.luau +105 -0
  89. package/out/internal/network/schema.d.ts +72 -0
  90. package/out/internal/network/schema.luau +386 -0
  91. package/out/internal/network/server-session.d.ts +1 -0
  92. package/out/internal/network/server-session.luau +395 -0
  93. package/out/internal/network/session.d.ts +1 -0
  94. package/out/internal/network/session.luau +266 -0
  95. package/out/internal/reactive/context.d.ts +1 -0
  96. package/out/internal/reactive/context.luau +149 -0
  97. package/out/internal/reactive/flush.d.ts +1 -0
  98. package/out/internal/reactive/flush.luau +355 -0
  99. package/out/internal/reactive/graph.d.ts +1 -0
  100. package/out/internal/reactive/graph.luau +626 -0
  101. package/out/internal/reactive/limits.d.ts +1 -0
  102. package/out/internal/reactive/limits.luau +54 -0
  103. package/out/internal/reactive/ownership.d.ts +1 -0
  104. package/out/internal/reactive/ownership.luau +626 -0
  105. package/out/internal/registry.luau +10 -1
  106. package/out/internal/sync/client-sync.d.ts +1 -0
  107. package/out/internal/sync/client-sync.luau +157 -0
  108. package/out/internal/sync/declarations.d.ts +1 -0
  109. package/out/internal/sync/declarations.luau +276 -0
  110. package/out/internal/sync/frame.d.ts +1 -0
  111. package/out/internal/sync/frame.luau +91 -0
  112. package/out/internal/sync/server-sync.d.ts +1 -0
  113. package/out/internal/sync/server-sync.luau +502 -0
  114. package/out/internal/sync/values.d.ts +1 -0
  115. package/out/internal/sync/values.luau +930 -0
  116. package/out/maths/spline/curve.d.ts +35 -0
  117. package/out/maths/spline/curve.luau +1122 -0
  118. package/out/maths/spline/index.d.ts +2 -0
  119. package/out/maths/spline/init.luau +8 -0
  120. package/out/network.d.ts +66 -0
  121. package/out/network.luau +24 -0
  122. package/out/reactive.d.ts +76 -0
  123. package/out/reactive.luau +136 -0
  124. package/out/replicated.d.ts +38 -0
  125. package/out/replicated.luau +45 -0
  126. package/out/server/data.d.ts +54 -0
  127. package/out/server/data.luau +44 -0
  128. package/out/server/index.d.ts +17 -0
  129. package/out/server/init.luau +58 -4
  130. package/out/server/publish.d.ts +20 -0
  131. package/out/server/publish.luau +24 -0
  132. package/out/testing/index.d.ts +38 -0
  133. package/out/testing/init.luau +189 -0
  134. package/out/util/random/index.d.ts +24 -0
  135. package/out/util/random/init.luau +301 -0
  136. package/out/vide/index.d.ts +7 -0
  137. package/out/vide/init.luau +50 -0
  138. package/package.json +28 -2
  139. package/testing/package.json +4 -0
  140. package/transformer/data.cjs +72 -0
  141. package/transformer/declarations.cjs +153 -0
  142. package/transformer/index.js +87 -23
  143. package/transformer/network.cjs +961 -0
  144. package/util/random/package.json +4 -0
  145. 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
+ };
@@ -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
- let invalidMetadata = false;
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
- const owner = packageOf(source.fileName);
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
- invalidMetadata = true;
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 (invalidMetadata) {
54
- report(source, "Crucible", "Cannot read package metadata. Check the project's package.json files.");
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 root = path.dirname(
397
+ const configFile =
364
398
  program.getCompilerOptions().configFilePath ??
365
- path.join(program.getCurrentDirectory(), "tsconfig.json"),
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 ?? RojoResolver.findRojoConfigFilePath(root).path;
375
- resolver = rojoPath && RojoResolver.fromPath(path.resolve(root, rojoPath));
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 relative = inputRoot && path.relative(inputRoot, path.resolve(root, prefix));
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
- relative !== undefined &&
391
- !relative.startsWith("..") &&
392
- !path.isAbsolute(relative) &&
393
- resolver?.getRbxPathFromFilePath(path.resolve(opts.outDir, relative));
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
- !["@trioforge/crucible/server", "@trioforge/crucible/client"].includes(specifier.text)
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(node, "CRU004", "Import only the root, server or client Crucible entries.");
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
  };