@trioforge/crucible 0.0.0-stage → 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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/client/package.json +4 -0
  3. package/emit/package.json +4 -0
  4. package/out/all.d.ts +7 -0
  5. package/out/all.luau +2 -0
  6. package/out/attribute.d.ts +7 -0
  7. package/out/attribute.luau +7 -0
  8. package/out/client/index.d.ts +3 -0
  9. package/out/client/init.luau +20 -0
  10. package/out/crucible-behaviour.d.ts +16 -0
  11. package/out/crucible-behaviour.luau +78 -0
  12. package/out/crucible-object.d.ts +10 -0
  13. package/out/crucible-object.luau +69 -0
  14. package/out/emit/index.d.ts +41 -0
  15. package/out/emit/init.luau +85 -0
  16. package/out/index.d.ts +12 -0
  17. package/out/init.luau +11 -0
  18. package/out/inject.d.ts +1 -0
  19. package/out/inject.luau +7 -0
  20. package/out/internal/binding.d.ts +1 -0
  21. package/out/internal/binding.luau +610 -0
  22. package/out/internal/boot.d.ts +1 -0
  23. package/out/internal/boot.luau +463 -0
  24. package/out/internal/construction-claim.d.ts +1 -0
  25. package/out/internal/construction-claim.luau +26 -0
  26. package/out/internal/dense-list.d.ts +1 -0
  27. package/out/internal/dense-list.luau +155 -0
  28. package/out/internal/diagnostics.d.ts +1 -0
  29. package/out/internal/diagnostics.luau +29 -0
  30. package/out/internal/hooks.d.ts +1 -0
  31. package/out/internal/hooks.luau +53 -0
  32. package/out/internal/implementers.d.ts +1 -0
  33. package/out/internal/implementers.luau +119 -0
  34. package/out/internal/lifecycle.d.ts +1 -0
  35. package/out/internal/lifecycle.luau +867 -0
  36. package/out/internal/log-output.d.ts +1 -0
  37. package/out/internal/log-output.luau +106 -0
  38. package/out/internal/port.d.ts +1 -0
  39. package/out/internal/port.luau +12 -0
  40. package/out/internal/registry.d.ts +1 -0
  41. package/out/internal/registry.luau +446 -0
  42. package/out/internal/roblox-port.d.ts +1 -0
  43. package/out/internal/roblox-port.luau +117 -0
  44. package/out/internal/runner.d.ts +1 -0
  45. package/out/internal/runner.luau +86 -0
  46. package/out/internal/scheduler.d.ts +1 -0
  47. package/out/internal/scheduler.luau +207 -0
  48. package/out/internal/state.d.ts +1 -0
  49. package/out/internal/state.luau +41 -0
  50. package/out/log-level.d.ts +7 -0
  51. package/out/log-level.luau +21 -0
  52. package/out/logging.d.ts +34 -0
  53. package/out/logging.luau +233 -0
  54. package/out/server/index.d.ts +2 -0
  55. package/out/server/init.luau +16 -0
  56. package/out/subsystem.d.ts +4 -0
  57. package/out/subsystem.luau +30 -0
  58. package/package.json +41 -5
  59. package/server/package.json +4 -0
  60. package/transformer/diagnostics.cjs +19 -0
  61. package/transformer/index.js +715 -0
  62. package/transformer/type-id.cjs +80 -0
  63. package/README.md +0 -3
@@ -0,0 +1,715 @@
1
+ const path = require("node:path");
2
+ const { createRequire } = require("node:module");
3
+ const { packageOf, typeId, sourceRoot } = require("./type-id.cjs");
4
+ const { createReporter } = require("./diagnostics.cjs");
5
+
6
+ module.exports = function transformer(program, config = {}, extras = {}) {
7
+ const ts = extras.ts ?? require("typescript");
8
+ const checker = program.getTypeChecker();
9
+ const f = ts.factory;
10
+ const symbols = new Map();
11
+ const unalias = (s) => s && (s.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(s) : s);
12
+ let invalidMetadata = false;
13
+ try {
14
+ for (const source of program.getSourceFiles()) {
15
+ const owner = packageOf(source.fileName);
16
+ if (owner?.json.name !== "@trioforge/crucible") continue;
17
+ const module = checker.getSymbolAtLocation(source);
18
+ if (!module) continue;
19
+ for (const symbol of checker.getExportsOfModule(module)) {
20
+ const target = unalias(symbol);
21
+ if (!symbols.has(target)) symbols.set(target, symbol.name);
22
+ }
23
+ }
24
+ } catch {
25
+ invalidMetadata = true;
26
+ }
27
+ const api = (symbol) => symbols.get(unalias(symbol));
28
+ const modifiers = (node, flags) => (ts.getCombinedModifierFlags(node) & flags) !== 0;
29
+ const bases = (type) => (type.target ?? type).getBaseTypes?.() ?? [];
30
+ const instanceType = (node) => {
31
+ const located = checker.getTypeAtLocation(node);
32
+ const signature =
33
+ ts.isClassExpression(node) && checker.getSignaturesOfType(located, ts.SignatureKind.Construct)[0];
34
+ return signature ? checker.getReturnTypeOfSignature(signature) : located;
35
+ };
36
+ const kindOf = (type, seen = new Set()) => {
37
+ if (seen.has(type)) return undefined;
38
+ seen.add(type);
39
+ const name = api(type.getSymbol());
40
+ if (name === "Subsystem") return "subsystem";
41
+ if (name === "CrucibleBehaviour") return "behaviour";
42
+ for (const base of bases(type)) {
43
+ const kind = kindOf(base, seen);
44
+ if (kind) return kind;
45
+ }
46
+ };
47
+ const diagnosticSink = extras.addDiagnostic;
48
+ let report;
49
+ return (context) => (source) => {
50
+ if (source.isDeclarationFile) return source;
51
+ let used = false;
52
+ 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.");
55
+ return source;
56
+ }
57
+ const isGlobal = (decl) => {
58
+ if (!ts.isExternalModule(decl.getSourceFile())) return true;
59
+ for (let p = decl.parent; p; p = p.parent)
60
+ if (ts.isModuleDeclaration(p) && p.flags & ts.NodeFlags.GlobalAugmentation) return true;
61
+ return false;
62
+ };
63
+ const runtime = (name) => {
64
+ used = true;
65
+ return f.createPropertyAccessExpression(f.createIdentifier("__crucible"), name);
66
+ };
67
+ const call = (expression, args, types) => f.createCallExpression(expression, types, args);
68
+ const str = (s) => f.createStringLiteral(s);
69
+ const array = (values) => f.createArrayLiteralExpression(values);
70
+ const object = (values) =>
71
+ f.createObjectLiteralExpression(
72
+ Object.entries(values).map(([key, value]) => f.createPropertyAssignment(str(key), value)),
73
+ true,
74
+ );
75
+ const globalType = (type, at, seen = new Set()) => {
76
+ if (seen.has(type)) return;
77
+ seen.add(type);
78
+ for (const symbol of [type.aliasSymbol, type.getSymbol()]) {
79
+ for (const decl of symbol?.declarations ?? []) {
80
+ if (
81
+ program.isSourceFileDefaultLibrary(decl.getSourceFile()) ||
82
+ /node_modules[\\/]@rbxts[\\/]/.test(decl.getSourceFile().fileName)
83
+ )
84
+ continue;
85
+ if (isGlobal(decl))
86
+ report(
87
+ at,
88
+ "CRU010",
89
+ "Type comes from global declarations; import a module type for incremental correctness.",
90
+ );
91
+ }
92
+ }
93
+ for (const part of type.isUnion() ? type.types : []) globalType(part, at, seen);
94
+ };
95
+ const globalReferences = (node, at, seen = new Set()) => {
96
+ if (!node || seen.has(node)) return;
97
+ seen.add(node);
98
+ if (ts.isTypeReferenceNode(node)) {
99
+ const symbol = unalias(checker.getSymbolAtLocation(node.typeName));
100
+ for (const decl of symbol?.declarations ?? []) {
101
+ if (ts.isTypeAliasDeclaration(decl)) {
102
+ if (isGlobal(decl))
103
+ report(
104
+ at,
105
+ "CRU010",
106
+ "Attribute and dependency aliases must come from module declarations.",
107
+ );
108
+ globalReferences(decl.type, at, seen);
109
+ }
110
+ }
111
+ }
112
+ ts.forEachChild(node, (child) => globalReferences(child, at, seen));
113
+ };
114
+ const id = (type, at) => {
115
+ globalType(type, at);
116
+ const symbol = type.getSymbol();
117
+ const decl = symbol?.declarations?.find((d) => ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d));
118
+ if (!decl) return undefined;
119
+ const value = typeId(decl.getSourceFile().fileName, symbol.name, program, ts);
120
+ if (!value)
121
+ report(
122
+ at,
123
+ "Crucible",
124
+ "Cannot compute a type id. The declaring package needs a name and a mirrored source/declaration layout.",
125
+ );
126
+ return value;
127
+ };
128
+ const allTarget = (type) => (api(type.getSymbol()) === "All" ? checker.getTypeArguments(type)[0] : undefined);
129
+ const injectable = (type, at, all = false) => {
130
+ const target = allTarget(type);
131
+ if (target && !all) return object({ all: injectable(target, at, true) });
132
+ globalType(type, at);
133
+ const symbol = type.getSymbol();
134
+ const isNominal =
135
+ symbol?.declarations?.some((d) => ts.isClassDeclaration(d) || ts.isInterfaceDeclaration(d)) &&
136
+ checker.getDeclaredTypeOfSymbol(symbol) === (type.target ?? type);
137
+ const engineInstance = checker.resolveName("Instance", at, ts.SymbolFlags.Type, false);
138
+ const instance =
139
+ engineInstance && checker.isTypeAssignableTo(type, checker.getDeclaredTypeOfSymbol(engineInstance));
140
+ const generic =
141
+ type.objectFlags & ts.ObjectFlags.Reference &&
142
+ checker.getTypeArguments(type).length > 0 &&
143
+ !(all && !genericTarget(type, "target"));
144
+ const concreteBehaviour =
145
+ kindOf(type) === "behaviour" &&
146
+ symbol?.declarations?.some((d) => ts.isClassDeclaration(d) && !modifiers(d, ts.ModifierFlags.Abstract));
147
+ if (!isNominal || instance || type.isUnion() || generic || (!all && concreteBehaviour)) {
148
+ report(
149
+ at,
150
+ "CRU001",
151
+ concreteBehaviour
152
+ ? "Inject All<X> or call X.get(instance) for a concrete behaviour."
153
+ : "Constructor parameter must be a plain class, interface or All<T>.",
154
+ );
155
+ return str("invalid");
156
+ }
157
+ const value = id(type, at);
158
+ return str(value ?? "invalid");
159
+ };
160
+ const check = (type, at) => {
161
+ globalType(type, at);
162
+ const t = (name) => f.createPropertyAccessExpression(runtime("t"), name);
163
+ if (type.flags & ts.TypeFlags.Boolean) return t("boolean");
164
+ if (type.isUnion()) {
165
+ const parts = type.types.filter((p) => !(p.flags & ts.TypeFlags.Undefined));
166
+ const booleanPair =
167
+ parts.some((p) => p.flags & ts.TypeFlags.BooleanLiteral && p.intrinsicName === "true") &&
168
+ parts.some((p) => p.flags & ts.TypeFlags.BooleanLiteral && p.intrinsicName === "false");
169
+ const checks = parts
170
+ .filter((p) => !(booleanPair && p.flags & ts.TypeFlags.BooleanLiteral))
171
+ .map((p) => check(p, at));
172
+ if (booleanPair) checks.push(t("boolean"));
173
+ return checks.length === 1 ? checks[0] : call(t("union"), checks);
174
+ }
175
+ if (type.flags & ts.TypeFlags.String) return t("string");
176
+ if (type.flags & ts.TypeFlags.Number) return t("number");
177
+ if (type.isStringLiteral()) return call(t("literal"), [str(type.value)]);
178
+ if (type.isNumberLiteral())
179
+ return call(t("literal"), [
180
+ type.value < 0
181
+ ? f.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, f.createNumericLiteral(-type.value))
182
+ : f.createNumericLiteral(type.value),
183
+ ]);
184
+ if (type.flags & ts.TypeFlags.BooleanLiteral)
185
+ return call(t("literal"), [type.intrinsicName === "true" ? f.createTrue() : f.createFalse()]);
186
+ const name = type.getSymbol()?.name;
187
+ const engineType = name && checker.resolveName(name, at, ts.SymbolFlags.Type, false);
188
+ if ((name === "Vector3" || name === "Color3") && engineType === type.getSymbol())
189
+ return t(name.toLowerCase());
190
+ report(
191
+ at,
192
+ "CRU007",
193
+ "Attribute fields support string, number, boolean, Vector3, Color3 and literal unions.",
194
+ );
195
+ return t("string");
196
+ };
197
+ const isMacro = (node, name) => {
198
+ if (!ts.isCallExpression(node)) return false;
199
+ const declaration = checker.getResolvedSignature(node)?.declaration;
200
+ return (
201
+ api(checker.getSymbolAtLocation(node.expression)) === name ||
202
+ (declaration?.name && api(checker.getSymbolAtLocation(declaration.name)) === name)
203
+ );
204
+ };
205
+ const attributes = (type) => {
206
+ const result = {};
207
+ for (const prop of checker.getPropertiesOfType(type)) {
208
+ const decl = prop.valueDeclaration;
209
+ if (
210
+ !decl ||
211
+ !ts.isPropertyDeclaration(decl) ||
212
+ !decl.initializer ||
213
+ !isMacro(decl.initializer, "attribute")
214
+ )
215
+ continue;
216
+ const required = decl.initializer.arguments.length === 0;
217
+ let valueCheck = check(checker.getTypeOfSymbolAtLocation(prop, decl), decl);
218
+ if (!required)
219
+ valueCheck = call(f.createPropertyAccessExpression(runtime("t"), "optional"), [valueCheck]);
220
+ result[prop.name] = object({
221
+ name: str(prop.name[0].toUpperCase() + prop.name.slice(1)),
222
+ check: valueCheck,
223
+ required: required ? f.createTrue() : f.createFalse(),
224
+ });
225
+ }
226
+ return result;
227
+ };
228
+ const registration = (node) => {
229
+ const type = instanceType(node);
230
+ const kind = kindOf(type);
231
+ if (
232
+ !kind ||
233
+ modifiers(node, ts.ModifierFlags.Abstract | ts.ModifierFlags.Ambient) ||
234
+ node.flags & ts.NodeFlags.Ambient
235
+ )
236
+ return;
237
+ let ctor;
238
+ let current = type;
239
+ const constructors = new Set();
240
+ while (current && !constructors.has(current)) {
241
+ constructors.add(current);
242
+ const declaration = current.getSymbol()?.declarations?.find(ts.isClassDeclaration);
243
+ ctor = declaration?.members.find(ts.isConstructorDeclaration);
244
+ if (ctor) break;
245
+ current = bases(current)[0];
246
+ }
247
+ if (
248
+ !ts.isClassDeclaration(node) ||
249
+ !node.name ||
250
+ !ts.isSourceFile(node.parent) ||
251
+ node.typeParameters?.length ||
252
+ (ctor && modifiers(ctor, ts.ModifierFlags.Private | ts.ModifierFlags.Protected))
253
+ ) {
254
+ report(
255
+ node,
256
+ "CRU003",
257
+ "Registered classes must be named, non-generic, module-level classes with public constructors.",
258
+ );
259
+ return;
260
+ }
261
+ const staticType = checker.getTypeOfSymbolAtLocation(checker.getSymbolAtLocation(node.name), node);
262
+ const construct = checker.getSignaturesOfType(staticType, ts.SignatureKind.Construct)[0];
263
+ const deps = (construct?.parameters ?? []).map((param) => {
264
+ const decl = param.valueDeclaration ?? node;
265
+ globalReferences(decl.type, decl);
266
+ if (decl.questionToken || decl.dotDotDotToken || decl.initializer)
267
+ report(decl, "CRU001", "Optional, defaulted and rest parameters are not injectable.");
268
+ return injectable(checker.getTypeOfSymbolAtLocation(param, node), decl);
269
+ });
270
+ const provides = new Set();
271
+ const seen = new Set();
272
+ const provide = (current) => {
273
+ if (
274
+ seen.has(current) ||
275
+ ["Subsystem", "CrucibleBehaviour", "CrucibleObject"].includes(api(current.getSymbol()))
276
+ )
277
+ return;
278
+ seen.add(current);
279
+ if (current !== type) {
280
+ const value = id(current, node);
281
+ if (value) provides.add(value);
282
+ }
283
+ for (const base of bases(current)) provide(base);
284
+ for (const decl of current.getSymbol()?.declarations ?? []) {
285
+ for (const clause of decl.heritageClauses ?? [])
286
+ if (clause.token === ts.SyntaxKind.ImplementsKeyword)
287
+ for (const target of clause.types) provide(checker.getTypeAtLocation(target));
288
+ }
289
+ };
290
+ provide(type);
291
+ const data = {
292
+ kind: str(kind),
293
+ id: str(id(type, node) ?? "invalid"),
294
+ deps: array(deps),
295
+ provides: array([...provides].map(str)),
296
+ };
297
+ if (kind === "behaviour") {
298
+ const memberType = (name) => {
299
+ const prop = checker.getPropertyOfType(type, name);
300
+ return prop && checker.getTypeOfSymbolAtLocation(prop, node);
301
+ };
302
+ const tag = memberType("tag");
303
+ if (!tag?.isStringLiteral())
304
+ report(node, "CRU002", "A concrete behaviour tag must be exactly one string literal.");
305
+ data.tag = str(tag?.isStringLiteral() ? tag.value : "invalid");
306
+ const instance = memberType("instance");
307
+ const parts = instance?.isUnion() ? instance.types : [instance];
308
+ const instances = checker.resolveName("Instances", node, ts.SymbolFlags.Type, false);
309
+ const allowed = instances && checker.getDeclaredTypeOfSymbol(instances);
310
+ data.instance = array(
311
+ parts.map((part) => {
312
+ const name = part?.getSymbol()?.name;
313
+ const expected = name && allowed && checker.getPropertyOfType(allowed, name);
314
+ if (
315
+ !expected ||
316
+ checker.getTypeOfSymbolAtLocation(expected, node).getSymbol() !== part.getSymbol()
317
+ )
318
+ report(node, "CRU008", "Instance type must be an Instances class or union of them.");
319
+ return str(name ?? "Instance");
320
+ }),
321
+ );
322
+ const fields = attributes(type);
323
+ if (Object.keys(fields).length) data.attributes = object(fields);
324
+ }
325
+ return f.createExpressionStatement(
326
+ call(runtime("defineClass"), [f.createIdentifier(node.name.text), object(data)]),
327
+ );
328
+ };
329
+ const boot = (node) => {
330
+ let options = node.arguments[0];
331
+ while (
332
+ options &&
333
+ (ts.isParenthesizedExpression(options) ||
334
+ ts.isAsExpression(options) ||
335
+ ts.isTypeAssertionExpression(options) ||
336
+ ts.isSatisfiesExpression(options))
337
+ )
338
+ options = options.expression;
339
+ if (!options || !ts.isObjectLiteralExpression(options)) {
340
+ report(node, "CRU006", "boot requires literal load globs.");
341
+ return node;
342
+ }
343
+ const loadIndex = options.properties.findLastIndex(
344
+ (prop) => prop.name && (prop.name.text ?? prop.name.getText()) === "load",
345
+ );
346
+ if (loadIndex < 0) report(options, "CRU006", "boot requires a literal load property.");
347
+ const properties = options.properties.map((prop, index) => {
348
+ if (ts.isSpreadAssignment(prop) || (prop.name && ts.isComputedPropertyName(prop.name))) {
349
+ if (index > loadIndex)
350
+ report(prop, "CRU006", "A spread or computed key after load can replace its literal globs.");
351
+ return prop;
352
+ }
353
+ if (!prop.name || (prop.name.text ?? prop.name.getText()) !== "load") return prop;
354
+ if (!ts.isPropertyAssignment(prop) || !ts.isArrayLiteralExpression(prop.initializer)) {
355
+ report(prop, "CRU006", "load must be an array of string literals.");
356
+ return prop;
357
+ }
358
+ const entries = prop.initializer.elements.map((entry) => {
359
+ if (!ts.isStringLiteral(entry)) {
360
+ report(entry, "CRU006", "load entry must be a string literal.");
361
+ return entry;
362
+ }
363
+ const root = path.dirname(
364
+ program.getCompilerOptions().configFilePath ??
365
+ path.join(program.getCurrentDirectory(), "tsconfig.json"),
366
+ );
367
+ let resolver;
368
+ try {
369
+ const compiler =
370
+ Object.keys(require.cache).find((file) =>
371
+ /roblox-ts[\\/]out[\\/]Project[\\/]functions[\\/]compileFiles\.js$/.test(file),
372
+ ) ?? require.resolve("roblox-ts/package.json", { paths: [root, __dirname] });
373
+ 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));
376
+ } catch {
377
+ report(entry, "CRU006", "Cannot load the running compiler's Rojo resolver or project mapping.");
378
+ return entry;
379
+ }
380
+ const globAt = entry.text.search(/[?*{[]/);
381
+ const prefix =
382
+ globAt < 0
383
+ ? path.dirname(entry.text) + "/"
384
+ : entry.text.slice(0, entry.text.lastIndexOf("/", globAt) + 1);
385
+ const suffix = entry.text.slice(prefix.length).replace(/\.tsx?$/, "");
386
+ const opts = program.getCompilerOptions();
387
+ const inputRoot = sourceRoot(opts, [program.getCommonSourceDirectory()]);
388
+ const relative = inputRoot && path.relative(inputRoot, path.resolve(root, prefix));
389
+ const mapped =
390
+ relative !== undefined &&
391
+ !relative.startsWith("..") &&
392
+ !path.isAbsolute(relative) &&
393
+ resolver?.getRbxPathFromFilePath(path.resolve(opts.outDir, relative));
394
+ if (!mapped) {
395
+ report(entry, "CRU006", "load prefix is outside the Rojo mapping.");
396
+ return entry;
397
+ }
398
+ return str(`crucible-path:${[...mapped, suffix].join("/")}`);
399
+ });
400
+ return f.updatePropertyAssignment(prop, prop.name, array(entries));
401
+ });
402
+ return f.updateCallExpression(node, node.expression, node.typeArguments, [
403
+ f.updateObjectLiteralExpression(options, properties),
404
+ ]);
405
+ };
406
+
407
+ const resolvedBases = (type) => type.getBaseTypes?.() ?? bases(type);
408
+
409
+ const concreteBase = (base, seen = new Set()) => {
410
+ if (seen.has(base)) return false;
411
+ seen.add(base);
412
+ if (base.isIntersection()) return base.types.some((part) => concreteBase(part, seen));
413
+ if (
414
+ kindOf(base) &&
415
+ !api(base.getSymbol()) &&
416
+ base
417
+ .getSymbol()
418
+ ?.declarations?.some(
419
+ (decl) => ts.isClassDeclaration(decl) && !modifiers(decl, ts.ModifierFlags.Abstract),
420
+ )
421
+ )
422
+ return true;
423
+ return resolvedBases(base).some((parent) => concreteBase(parent, seen));
424
+ };
425
+ const genericTarget = (type, mode, seen = new Set()) => {
426
+ if (!type || seen.has(type)) return false;
427
+ seen.add(type);
428
+ if (type.isUnionOrIntersection()) return type.types.some((part) => genericTarget(part, mode, seen));
429
+ if (["Subsystem", "CrucibleBehaviour", "CrucibleObject"].includes(api(type.getSymbol()))) return false;
430
+ const args = type.objectFlags & ts.ObjectFlags.Reference ? checker.getTypeArguments(type) : [];
431
+ if (args.length) {
432
+ const target = type.target ?? type;
433
+ const tag = checker.getPropertyOfType(target, "tag");
434
+ const tagType = tag && checker.getTypeOfSymbolAtLocation(tag, tag.valueDeclaration ?? source);
435
+ const parameters = target.typeParameters ?? [];
436
+ if (
437
+ args.some(
438
+ (arg, i) =>
439
+ kindOf(target) !== "behaviour" ||
440
+ parameters[i] !== tagType ||
441
+ (mode !== "provides" && arg.flags !== ts.TypeFlags.String),
442
+ )
443
+ )
444
+ return true;
445
+ }
446
+ const baseMode = type.getSymbol()?.declarations?.some(ts.isClassDeclaration) ? "provides" : mode;
447
+ if (resolvedBases(type).some((base) => genericTarget(base, baseMode, seen))) return true;
448
+ for (const decl of type.getSymbol()?.declarations ?? [])
449
+ for (const clause of decl.heritageClauses ?? [])
450
+ if (clause.token === ts.SyntaxKind.ImplementsKeyword)
451
+ for (const target of clause.types)
452
+ if (genericTarget(checker.getTypeAtLocation(target), mode, seen)) return true;
453
+ return false;
454
+ };
455
+ const hooks = new Set();
456
+ for (const [symbol, name] of symbols) {
457
+ if (!["Subsystem", "CrucibleBehaviour", "CrucibleObject"].includes(name)) continue;
458
+ for (const decl of symbol.declarations ?? [])
459
+ for (const member of decl.members ?? [])
460
+ if (
461
+ ts.isMethodDeclaration(member) &&
462
+ modifiers(member, ts.ModifierFlags.Protected) &&
463
+ !modifiers(member, ts.ModifierFlags.Static)
464
+ )
465
+ hooks.add(member.name.getText());
466
+ }
467
+ const near = (a, b) => {
468
+ if (Math.abs(a.length - b.length) > 1) return false;
469
+ let i = 0,
470
+ j = 0,
471
+ edits = 0;
472
+ while (i < a.length && j < b.length) {
473
+ if (a[i] === b[j]) {
474
+ i++;
475
+ j++;
476
+ continue;
477
+ }
478
+ if (++edits > 1) return false;
479
+ if (a.length >= b.length) i++;
480
+ if (b.length >= a.length) j++;
481
+ }
482
+ return edits + a.length - i + b.length - j === 1;
483
+ };
484
+ const promise = (type, seen = new Set()) => {
485
+ if (!type || seen.has(type)) return false;
486
+ seen.add(type);
487
+ return (
488
+ type.getSymbol()?.name === "Promise" ||
489
+ (type.isUnionOrIntersection() && type.types.some((part) => promise(part, seen))) ||
490
+ resolvedBases(type).some((base) => promise(base, seen))
491
+ );
492
+ };
493
+ const sideRoot = packageOf(source.fileName)?.dir ?? path.parse(path.resolve(source.fileName)).root;
494
+ const side = (node) =>
495
+ path
496
+ .relative(sideRoot, node.getSourceFile().fileName)
497
+ .replaceAll("\\", "/")
498
+ .split("/")
499
+ .slice(0, -1)
500
+ .reverse()
501
+ .find((part) => ["server", "client", "shared"].includes(part));
502
+ const inspect = (node) => {
503
+ if (
504
+ ts.isImportDeclaration(node) ||
505
+ ts.isExportDeclaration(node) ||
506
+ ts.isImportTypeNode(node) ||
507
+ ts.isExternalModuleReference(node) ||
508
+ (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword)
509
+ ) {
510
+ const specifier =
511
+ node.moduleSpecifier ?? node.argument?.literal ?? node.arguments?.[0] ?? node.expression;
512
+ if (
513
+ specifier &&
514
+ ts.isStringLiteral(specifier) &&
515
+ specifier.text.startsWith("@trioforge/crucible/") &&
516
+ !["@trioforge/crucible/server", "@trioforge/crucible/client"].includes(specifier.text)
517
+ )
518
+ report(node, "CRU004", "Import only the root, server or client Crucible entries.");
519
+ }
520
+ if (ts.isClassExpression(node) || ts.isClassDeclaration(node)) {
521
+ const type = instanceType(node);
522
+ if (resolvedBases(type).some((base) => concreteBase(base)))
523
+ report(node, "CRU013", "Extend an abstract Crucible base, not a concrete registered class.");
524
+ if (kindOf(type)) {
525
+ if (!modifiers(node, ts.ModifierFlags.Abstract) && genericTarget(type, "provides"))
526
+ report(node, "CRU014", "Provided types must not instantiate non-tag generic parameters.");
527
+ for (const clause of node.heritageClauses ?? [])
528
+ if (clause.token === ts.SyntaxKind.ImplementsKeyword)
529
+ for (const target of clause.types)
530
+ if (genericTarget(checker.getTypeAtLocation(target), "provides"))
531
+ report(
532
+ node,
533
+ "CRU014",
534
+ "Provided interfaces must not instantiate generic parameters.",
535
+ );
536
+ const seen = new Set();
537
+ const members = (current) => {
538
+ if (seen.has(current) || api(current.getSymbol())) return;
539
+ seen.add(current);
540
+ for (const decl of current.getSymbol()?.declarations ?? [])
541
+ for (const member of decl.members ?? []) {
542
+ const name = member.name && checker.getSymbolAtLocation(member.name)?.name;
543
+ if (!name) continue;
544
+ const at = decl === node ? member : node;
545
+ if (side(node) !== "client" && (name === "preRender" || name === "renderPriority"))
546
+ report(at, "CRU011", "preRender and renderPriority require a client class.");
547
+ if (side(node) !== "server" && name === "onShutdown")
548
+ report(at, "CRU016", "onShutdown requires a server class.");
549
+ }
550
+ for (const base of resolvedBases(current)) members(base);
551
+ };
552
+ members(type);
553
+ for (const member of node.members) {
554
+ if (!ts.isMethodDeclaration(member)) continue;
555
+ const name = checker.getSymbolAtLocation(member.name)?.name;
556
+ if (!name) continue;
557
+ if (
558
+ !hooks.has(name) &&
559
+ (["onInit", "onStart", "onTick", "onPhysics", "onRender"].includes(name) ||
560
+ [...hooks].some(
561
+ (hook) => hook.toLowerCase() === name.toLowerCase() || near(name, hook),
562
+ ))
563
+ )
564
+ report(
565
+ member,
566
+ "CRU009",
567
+ "Method resembles a lifecycle hook. Use its exact Crucible declaration name.",
568
+ );
569
+ const signature = checker.getSignatureFromDeclaration(member);
570
+ if (
571
+ hooks.has(name) &&
572
+ (modifiers(member, ts.ModifierFlags.Async) ||
573
+ (signature && promise(checker.getReturnTypeOfSignature(signature))))
574
+ )
575
+ report(
576
+ member,
577
+ "CRU012",
578
+ "Crucible hooks must be synchronous and must not return a Promise.",
579
+ );
580
+ }
581
+ }
582
+ }
583
+ if (ts.isTypeReferenceNode(node)) {
584
+ const type = checker.getTypeFromTypeNode(node);
585
+ const target = allTarget(type);
586
+ if (target && genericTarget(target, "target"))
587
+ report(node, "CRU014", "All targets must not instantiate generic parameters except string tags.");
588
+ }
589
+ if (isMacro(node, "inject")) {
590
+ const own = node.typeArguments?.[0];
591
+ const type = own && checker.getTypeFromTypeNode(own);
592
+ const target = type && (allTarget(type) ?? type);
593
+ let inside = false;
594
+ for (let parent = node.parent; parent; parent = parent.parent)
595
+ if (
596
+ (ts.isClassDeclaration(parent) || ts.isClassExpression(parent)) &&
597
+ kindOf(instanceType(parent)) &&
598
+ !modifiers(parent, ts.ModifierFlags.Abstract)
599
+ )
600
+ inside = true;
601
+ const symbol = target?.getSymbol();
602
+ if (
603
+ inside ||
604
+ !symbol?.declarations?.some(
605
+ (decl) => ts.isClassDeclaration(decl) || ts.isInterfaceDeclaration(decl),
606
+ ) ||
607
+ checker.getDeclaredTypeOfSymbol(symbol) !== (target.target ?? target)
608
+ )
609
+ report(
610
+ node,
611
+ "CRU005",
612
+ "inject requires a class, interface or All target outside registered classes.",
613
+ );
614
+ if (target && genericTarget(target, "target"))
615
+ report(
616
+ node,
617
+ "CRU014",
618
+ "inject targets must not instantiate generic parameters except string tags.",
619
+ );
620
+ }
621
+ ts.forEachChild(node, inspect);
622
+ };
623
+
624
+ const visit = (node) => {
625
+ if (isMacro(node, "attribute")) {
626
+ const field = node.parent;
627
+ if (
628
+ !ts.isPropertyDeclaration(field) ||
629
+ field.initializer !== node ||
630
+ modifiers(field, ts.ModifierFlags.Static) ||
631
+ kindOf(instanceType(field.parent)) !== "behaviour"
632
+ ) {
633
+ report(node, "CRU015", "attribute() must initialize a behaviour instance field.");
634
+ return node;
635
+ }
636
+ globalReferences(field.type, field);
637
+ for (const type of node.typeArguments ?? []) globalReferences(type, field);
638
+ check(checker.getTypeAtLocation(field), field);
639
+ const typeNode =
640
+ node.typeArguments?.[0] ??
641
+ checker.typeToTypeNode(checker.getTypeAtLocation(field), field, ts.NodeBuilderFlags.NoTruncation);
642
+ return call(
643
+ runtime("attribute"),
644
+ [
645
+ f.createThis(),
646
+ str(checker.getSymbolAtLocation(field.name)?.name ?? field.name.getText()),
647
+ ...node.arguments.map((arg) => ts.visitNode(arg, visit)),
648
+ ],
649
+ [typeNode],
650
+ );
651
+ }
652
+ if (isMacro(node, "inject")) {
653
+ const own = node.typeArguments?.[0];
654
+ if (!own) return node;
655
+ const type = checker.getTypeFromTypeNode(own);
656
+ const target = allTarget(type);
657
+ const directAll =
658
+ ts.isTypeReferenceNode(own) && api(checker.getSymbolAtLocation(own.typeName)) === "All";
659
+ const typeNode = target
660
+ ? directAll
661
+ ? own.typeArguments?.[0]
662
+ : f.createConditionalTypeNode(
663
+ own,
664
+ f.createImportTypeNode(
665
+ f.createLiteralTypeNode(str("@trioforge/crucible")),
666
+ undefined,
667
+ f.createIdentifier("All"),
668
+ [
669
+ f.createInferTypeNode(
670
+ f.createTypeParameterDeclaration(
671
+ undefined,
672
+ "__CrucibleItem",
673
+ f.createKeywordTypeNode(ts.SyntaxKind.ObjectKeyword),
674
+ ),
675
+ ),
676
+ ],
677
+ ),
678
+ f.createTypeReferenceNode("__CrucibleItem"),
679
+ f.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword),
680
+ )
681
+ : own;
682
+ return call(
683
+ runtime(target ? "resolveAll" : "resolve"),
684
+ [str(id(target ?? type, node) ?? "invalid")],
685
+ [typeNode ?? own],
686
+ );
687
+ }
688
+ if (isMacro(node, "boot")) return boot(node);
689
+ const next = ts.visitEachChild(node, visit, context);
690
+ if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
691
+ const emitted = registration(node);
692
+ if (emitted) return [next, emitted];
693
+ }
694
+ return next;
695
+ };
696
+ try {
697
+ inspect(source);
698
+ const next = ts.visitEachChild(source, visit, context);
699
+ if (!used) return next;
700
+ const imports = f.createImportDeclaration(
701
+ undefined,
702
+ f.createImportClause(false, undefined, f.createNamespaceImport(f.createIdentifier("__crucible"))),
703
+ str("@trioforge/crucible/emit"),
704
+ );
705
+ return f.updateSourceFile(next, [imports, ...next.statements]);
706
+ } catch (error) {
707
+ report(
708
+ source,
709
+ "Crucible",
710
+ `Cannot transform input: ${String(error instanceof Error ? error.message : error).split(/\r?\n/)[0]}`,
711
+ );
712
+ return source;
713
+ }
714
+ };
715
+ };