@thigasdevelopment/luam 0.4.0 → 0.4.1

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 (2) hide show
  1. package/luam.mjs +158 -35
  2. package/package.json +1 -1
package/luam.mjs CHANGED
@@ -190,7 +190,7 @@ function parseArguments(argv) {
190
190
  continue;
191
191
  }
192
192
  if (parsed.command !== null) {
193
- if (parsed.command === "trace" && parsed.operand === null) {
193
+ if ((parsed.command === "init" || parsed.command === "trace") && parsed.operand === null) {
194
194
  parsed.operand = token;
195
195
  continue;
196
196
  }
@@ -212,12 +212,13 @@ var EXIT_DIAGNOSTICS = 1;
212
212
  var EXIT_USAGE = 2;
213
213
 
214
214
  // src/cli/usage.ts
215
- var VERSION = true ? "0.4.0" : "0.0.0-dev";
215
+ var VERSION = true ? "0.4.1" : "0.0.0-dev";
216
216
  var USAGE = [
217
217
  "luam \u2014 the Luam compiler for Multi Theft Auto resources.",
218
218
  "",
219
219
  "Usage:",
220
220
  " luam <command> [options]",
221
+ " luam init [path] [options]",
221
222
  "",
222
223
  "Commands:",
223
224
  " build Compile the project and write the resource to the output directory.",
@@ -225,16 +226,16 @@ var USAGE = [
225
226
  " dev Build, sync, restart, watch, and stream resource development logs.",
226
227
  " doctor Check the CLI, supported editors, and the Luam extension.",
227
228
  " ensure Build, sync the resource into the MTA server, and restart it.",
228
- " init Scaffold a new resource project with the framework and example handlers.",
229
+ " init Interactively scaffold a new resource project.",
229
230
  " setup Detect supported editors and install the Luam extension with consent.",
230
231
  " trace Resolve generated Lua positions back to Luam source positions.",
231
232
  "",
232
233
  "Options:",
233
234
  " --cwd <path> Project directory that contains luam.json. Defaults to the current directory.",
234
235
  " --config <path> Configuration file to load instead of luam.json.",
235
- " --name <name> Resource name for init. Defaults to the project directory name.",
236
+ " --name <name> Resource name for init. Defaults to the destination directory name.",
236
237
  " --force Let init overwrite files that already exist.",
237
- " -y, --yes Install the extension in every detected editor without prompting.",
238
+ " -y, --yes Accept init defaults or install every detected editor without prompting.",
238
239
  " --watch Keep ensure or dev running and rebuild on source changes.",
239
240
  " --no-watch Run ensure or dev once and exit.",
240
241
  " --bundle Build or ensure with bundled output.",
@@ -1164,8 +1165,8 @@ var Binder = class {
1164
1165
  isDeclaredInCurrentScope(name) {
1165
1166
  return this.scopes[this.scopes.length - 1]?.has(name) ?? false;
1166
1167
  }
1167
- declareAlias(name, type) {
1168
- this.aliases.set(name, type);
1168
+ declareAlias(name, type, parameters = []) {
1169
+ this.aliases.set(name, { type, parameters });
1169
1170
  }
1170
1171
  lookupAlias(name) {
1171
1172
  return this.aliases.get(name) ?? null;
@@ -5005,6 +5006,35 @@ function mtaMember(className, member) {
5005
5006
  return mtaClassRegistry().lookupMember(className, member);
5006
5007
  }
5007
5008
 
5009
+ // ../compiler/src/checker/type-substitution.ts
5010
+ function substituteType(type, substitutions) {
5011
+ if (type.kind === "named") {
5012
+ return substitutions.get(type.name) ?? type;
5013
+ }
5014
+ if (type.kind === "array") {
5015
+ return createArray(substituteType(type.element, substitutions));
5016
+ }
5017
+ if (type.kind === "optional") {
5018
+ return createOptional(substituteType(type.element, substitutions));
5019
+ }
5020
+ if (type.kind === "union") {
5021
+ return createUnion(type.options.map((option) => substituteType(option, substitutions)));
5022
+ }
5023
+ if (type.kind === "function") {
5024
+ const parameters = type.parameters.map((parameter) => substituteType(parameter, substitutions));
5025
+ const returnType = substituteType(type.returnType, substitutions);
5026
+ return createFunction(parameters, returnType, type.minimumArguments, type.isVariadic);
5027
+ }
5028
+ if (type.kind === "tuple") {
5029
+ return createTuple(type.elements.map((element) => substituteType(element, substitutions)));
5030
+ }
5031
+ if (type.kind === "record") {
5032
+ const members = new Map([...type.members].map(([name, member]) => [name, substituteType(member, substitutions)]));
5033
+ return createRecord(type.name, members, type.origin);
5034
+ }
5035
+ return type;
5036
+ }
5037
+
5008
5038
  // ../compiler/src/checker/context.ts
5009
5039
  var BUILTIN_TYPES = {
5010
5040
  any: ANY_TYPE,
@@ -5086,13 +5116,32 @@ var CheckContext = class {
5086
5116
  return this.types.get(expression) ?? UNKNOWN_TYPE;
5087
5117
  }
5088
5118
  pushReturnType(type) {
5089
- this.returnStack.push(type);
5119
+ this.returnStack.push({ expected: type, inferred: [] });
5090
5120
  }
5091
5121
  popReturnType() {
5092
- this.returnStack.pop();
5122
+ const frame = this.returnStack.pop();
5123
+ if (frame === void 0 || frame.inferred.length === 0) {
5124
+ return VOID_TYPE;
5125
+ }
5126
+ const tuples = frame.inferred.filter((type) => type.kind === "tuple");
5127
+ const tupleSize = tuples[0]?.kind === "tuple" ? tuples[0].elements.length : null;
5128
+ const sameTupleSize = tuples.length === frame.inferred.length && tuples.every((type) => type.kind === "tuple" && type.elements.length === tupleSize);
5129
+ if (sameTupleSize) {
5130
+ const first = tuples[0];
5131
+ if (first?.kind === "tuple") {
5132
+ const elements = first.elements.map(
5133
+ (_, index) => createUnion(tuples.map((type) => type.kind === "tuple" ? type.elements[index] ?? ANY_TYPE : ANY_TYPE))
5134
+ );
5135
+ return createTuple(elements);
5136
+ }
5137
+ }
5138
+ return createUnion(frame.inferred);
5093
5139
  }
5094
5140
  currentReturnType() {
5095
- return this.returnStack[this.returnStack.length - 1] ?? null;
5141
+ return this.returnStack[this.returnStack.length - 1]?.expected ?? null;
5142
+ }
5143
+ inferReturnType(type) {
5144
+ this.returnStack[this.returnStack.length - 1]?.inferred.push(type);
5096
5145
  }
5097
5146
  pushClassMethod(frame) {
5098
5147
  this.methodStack.push(frame);
@@ -5132,7 +5181,7 @@ var CheckContext = class {
5132
5181
  const minimum = optional === -1 ? parameters.length : optional;
5133
5182
  return createFunction(parameters, this.resolveAnnotation(annotation.returnType), minimum, annotation.isVariadic);
5134
5183
  }
5135
- return this.resolveNamedAnnotation(annotation.name);
5184
+ return this.resolveNamedAnnotation(annotation.name, annotation.typeArguments, annotation.position);
5136
5185
  }
5137
5186
  expectAssignable(source, target, position2, subject) {
5138
5187
  if (isAssignable(source, target, { allowNil: this.allowNil })) {
@@ -5168,12 +5217,25 @@ var CheckContext = class {
5168
5217
  this.binder.declareGlobal({ name: info.name, type: info.type, isLocal: false, position: info.position });
5169
5218
  }
5170
5219
  }
5171
- resolveNamedAnnotation(name) {
5220
+ resolveNamedAnnotation(name, typeArguments, position2) {
5172
5221
  const builtin = BUILTIN_TYPES[name];
5173
5222
  if (builtin !== void 0) {
5223
+ if (typeArguments.length > 0) {
5224
+ this.report("check-generic-arity", `Type "${name}" does not accept type arguments.`, position2);
5225
+ }
5174
5226
  return builtin;
5175
5227
  }
5176
- return this.binder.lookupAlias(name) ?? createNamed(name);
5228
+ const alias = this.binder.lookupAlias(name);
5229
+ if (alias === null) {
5230
+ return createNamed(name);
5231
+ }
5232
+ if (typeArguments.length !== alias.parameters.length) {
5233
+ const expected = alias.parameters.length === 1 ? "1 type argument" : `${alias.parameters.length} type arguments`;
5234
+ this.report("check-generic-arity", `Type "${name}" expects ${expected} but received ${typeArguments.length}.`, position2);
5235
+ }
5236
+ const resolved = typeArguments.map((argument) => this.resolveAnnotation(argument));
5237
+ const substitutions = new Map(alias.parameters.map((parameter, index) => [parameter, resolved[index] ?? ANY_TYPE]));
5238
+ return substituteType(alias.type, substitutions);
5177
5239
  }
5178
5240
  };
5179
5241
 
@@ -6347,7 +6409,7 @@ function checkMultiValueExpression(context, expression) {
6347
6409
  return checkTable(context, expression);
6348
6410
  case "function-expression": {
6349
6411
  const type = buildFunctionType(context, expression.parameters, expression.returnAnnotation);
6350
- checkFunctionBody(context, expression.parameters, expression.returnAnnotation, expression.body, null);
6412
+ checkFunctionBody(context, expression.parameters, expression.returnAnnotation, expression.body, type, null);
6351
6413
  return context.record(expression, type);
6352
6414
  }
6353
6415
  case "binary-expression": {
@@ -6412,8 +6474,12 @@ function checkMethodBody(context, info, member) {
6412
6474
  if (explicitSelf !== void 0) {
6413
6475
  context.report("check-explicit-self-parameter", 'Class methods receive "self" automatically; remove it from the parameter list.', explicitSelf.position);
6414
6476
  }
6477
+ const signature = info.members.get(member.name)?.type;
6478
+ if (signature?.kind !== "function") {
6479
+ return;
6480
+ }
6415
6481
  context.pushClassMethod({ className: info.name, methodName: member.name });
6416
- checkFunctionBody(context, member.parameters, member.returnAnnotation, member.body, createNamed(info.name));
6482
+ checkFunctionBody(context, member.parameters, member.returnAnnotation, member.body, signature, createNamed(info.name));
6417
6483
  context.popClassMethod();
6418
6484
  }
6419
6485
  function checkContract(context, info, contract, member) {
@@ -6600,9 +6666,9 @@ function buildFunctionType(context, parameters, returnAnnotation) {
6600
6666
  const types = parameters.filter((parameter) => !parameter.isVararg).map((parameter) => context.resolveAnnotation(parameter.annotation));
6601
6667
  return createFunction(types, context.resolveAnnotation(returnAnnotation), minimumArguments(context, parameters), isVariadic);
6602
6668
  }
6603
- function checkFunctionBody(context, parameters, returnAnnotation, body, selfType) {
6669
+ function checkFunctionBody(context, parameters, returnAnnotation, body, signature, selfType) {
6604
6670
  context.binder.pushScope();
6605
- context.pushReturnType(context.resolveAnnotation(returnAnnotation));
6671
+ context.pushReturnType(returnAnnotation === null ? null : context.resolveAnnotation(returnAnnotation));
6606
6672
  if (selfType !== null) {
6607
6673
  context.binder.declare({ name: "self", type: selfType, isLocal: true, position: ORIGIN });
6608
6674
  }
@@ -6611,7 +6677,10 @@ function checkFunctionBody(context, parameters, returnAnnotation, body, selfType
6611
6677
  context.binder.declare({ name: parameter.name, type, isLocal: true, position: parameter.position });
6612
6678
  }
6613
6679
  checkStatements(context, body);
6614
- context.popReturnType();
6680
+ const inferred = context.popReturnType();
6681
+ if (returnAnnotation === null) {
6682
+ signature.returnType = inferred;
6683
+ }
6615
6684
  context.binder.popScope();
6616
6685
  }
6617
6686
  function valueAt(values, valueTypes, index) {
@@ -6681,12 +6750,16 @@ function checkFunctionDeclaration(context, statement) {
6681
6750
  }
6682
6751
  }
6683
6752
  context.record(statement.name, type);
6684
- checkFunctionBody(context, statement.parameters, statement.returnAnnotation, statement.body, statement.isMethod ? ANY_TYPE : null);
6753
+ checkFunctionBody(context, statement.parameters, statement.returnAnnotation, statement.body, type, statement.isMethod ? ANY_TYPE : null);
6685
6754
  }
6686
6755
  function checkReturn(context, statement) {
6687
6756
  const valueTypes = checkValueList(context, statement.values);
6688
6757
  const expected = context.currentReturnType();
6689
- if (expected === null || expected.kind === "any") {
6758
+ if (expected === null) {
6759
+ context.inferReturnType(createTuple(valueTypes));
6760
+ return;
6761
+ }
6762
+ if (expected.kind === "any") {
6690
6763
  return;
6691
6764
  }
6692
6765
  if (expected.kind === "void") {
@@ -6761,7 +6834,7 @@ function checkStatement(context, statement) {
6761
6834
  case "type-alias-statement": {
6762
6835
  const resolved = context.resolveAnnotation(statement.annotation);
6763
6836
  const named2 = statement.annotation.kind === "type-object" ? renameRecord(resolved, statement.name) : resolved;
6764
- context.binder.declareAlias(statement.name, named2);
6837
+ context.binder.declareAlias(statement.name, named2, statement.typeParameters);
6765
6838
  return;
6766
6839
  }
6767
6840
  case "declare-statement":
@@ -8483,6 +8556,14 @@ function parseLegacyClassMethod(stream, token, decorators) {
8483
8556
  function parseFieldAnnotation(stream) {
8484
8557
  return parseNamedAnnotation(stream, "field");
8485
8558
  }
8559
+ function expectClassFieldBoundary(stream, token) {
8560
+ const current = stream.current();
8561
+ const delimiter = current.kind === "punctuation" && (current.value === "}" || current.value === ";" || current.value === ",");
8562
+ if (current.kind === "eof" || delimiter || current.position.line > token.position.line) {
8563
+ return;
8564
+ }
8565
+ throw stream.error(`Expected a line break or separator after class member "${token.value}".`, "parse-unexpected-token");
8566
+ }
8486
8567
  function parseClassMember(stream) {
8487
8568
  const decorators = parseDecorators(stream);
8488
8569
  const token = stream.expectName();
@@ -8494,6 +8575,7 @@ function parseClassMember(stream) {
8494
8575
  }
8495
8576
  const annotation = parseFieldAnnotation(stream);
8496
8577
  const value = stream.match("operator", "=") ? parseExpression(stream) : null;
8578
+ expectClassFieldBoundary(stream, token);
8497
8579
  return { kind: "class-field", name: token.value, annotation, value, decorators, position: token.position };
8498
8580
  }
8499
8581
  function parseClassModifiers(stream, declaration) {
@@ -11002,6 +11084,28 @@ async function runDevCommand(context, options) {
11002
11084
  // src/commands/init-command.ts
11003
11085
  import { basename } from "node:path";
11004
11086
 
11087
+ // src/commands/init-prompt.ts
11088
+ import { createInterface } from "node:readline/promises";
11089
+ async function question(prompt, label2, fallback) {
11090
+ const answer = (await prompt.question(`${label2}${fallback === "" ? "" : ` (${fallback})`}: `)).trim();
11091
+ return answer === "" ? fallback : answer;
11092
+ }
11093
+ async function promptForProject(defaults, nameLocked) {
11094
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
11095
+ return defaults;
11096
+ }
11097
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
11098
+ try {
11099
+ const name = nameLocked ? defaults.name : await question(prompt, "Project name", defaults.name);
11100
+ const version = await question(prompt, "Version", defaults.version);
11101
+ const description = await question(prompt, "Description", defaults.description ?? "");
11102
+ const author = await question(prompt, "Author", defaults.author ?? "");
11103
+ return { name, version, description: description || null, author: author || null };
11104
+ } finally {
11105
+ prompt.close();
11106
+ }
11107
+ }
11108
+
11005
11109
  // src/config/config-schema.ts
11006
11110
  var CONFIG_FILE_NAME = "luam.json";
11007
11111
  var DEFAULT_SOURCE_DIRS = ["src"];
@@ -11049,21 +11153,29 @@ function readTemplateSource(source) {
11049
11153
  }
11050
11154
 
11051
11155
  // src/scaffold/scaffold-plan.ts
11052
- function renderConfig(source, name) {
11156
+ function renderConfig(source, details) {
11053
11157
  const parsed = JSON.parse(source);
11054
11158
  const config = typeof parsed === "object" && parsed !== null ? parsed : {};
11055
- return `${JSON.stringify({ ...config, name, description: `The ${name} resource, scaffolded by luam init.` }, null, 4)}
11159
+ const { name: _name, version: _version, description: _description, author: _author, ...template } = config;
11160
+ const metadata = {
11161
+ ...template,
11162
+ name: details.name,
11163
+ version: details.version,
11164
+ ...details.description === null ? {} : { description: details.description },
11165
+ ...details.author === null ? {} : { author: details.author }
11166
+ };
11167
+ return `${JSON.stringify(metadata, null, 4)}
11056
11168
  `;
11057
11169
  }
11058
- function renderFile(file, name) {
11170
+ function renderFile(file, details) {
11059
11171
  const source = readTemplateSource(file.source);
11060
11172
  if (file.path !== CONFIG_FILE_NAME) {
11061
11173
  return { path: file.path, content: source };
11062
11174
  }
11063
- return { path: file.path, content: renderConfig(source, name) };
11175
+ return { path: file.path, content: renderConfig(source, details) };
11064
11176
  }
11065
- function buildScaffoldPlan(name) {
11066
- return { name, files: TEMPLATE_FILES.map((file) => renderFile(file, name)) };
11177
+ function buildScaffoldPlan(details) {
11178
+ return { name: details.name, files: TEMPLATE_FILES.map((file) => renderFile(file, details)) };
11067
11179
  }
11068
11180
 
11069
11181
  // src/scaffold/scaffold-writer.ts
@@ -11094,21 +11206,31 @@ function resolveResourceName(root, requested) {
11094
11206
  const folder = basename(root);
11095
11207
  return isValidResourceName(folder) ? folder : FALLBACK_RESOURCE_NAME;
11096
11208
  }
11097
- function runInitCommand(root, logger, options) {
11209
+ async function runInitCommand(root, logger, options) {
11098
11210
  if (options.name !== null && !isValidResourceName(options.name)) {
11099
11211
  logger.error(`"--name" must be a valid MTA resource name but received "${options.name}".`);
11100
11212
  return EXIT_USAGE;
11101
11213
  }
11102
- const name = resolveResourceName(root, options.name);
11103
- const result = writeScaffold(root, buildScaffoldPlan(name), options.force);
11214
+ const defaults = {
11215
+ name: resolveResourceName(root, options.name),
11216
+ version: "1.0.0",
11217
+ description: null,
11218
+ author: null
11219
+ };
11220
+ const details = options.yes ? defaults : await options.prompt(defaults, options.name !== null);
11221
+ if (!isValidResourceName(details.name)) {
11222
+ logger.error(`"Project name" must be a valid MTA resource name but received "${details.name}".`);
11223
+ return EXIT_USAGE;
11224
+ }
11225
+ const result = writeScaffold(root, buildScaffoldPlan(details), options.force);
11104
11226
  for (const path of result.skipped) {
11105
11227
  logger.warn(`Kept the existing "${path}". Pass "--force" to overwrite it.`);
11106
11228
  }
11107
11229
  if (result.written.length === 0) {
11108
- logger.info(`The project "${name}" already exists in "${root}". Nothing was written.`);
11230
+ logger.info(`The project "${details.name}" already exists in "${root}". Nothing was written.`);
11109
11231
  return EXIT_OK;
11110
11232
  }
11111
- logger.info(`Scaffolded "${name}" into "${root}" (${result.written.length} files, ${result.skipped.length} kept).`);
11233
+ logger.info(`Scaffolded "${details.name}" into "${root}" (${result.written.length} files, ${result.skipped.length} kept).`);
11112
11234
  logger.info('Next: run "luam check", then "luam build".');
11113
11235
  return EXIT_OK;
11114
11236
  }
@@ -11720,12 +11842,12 @@ function createEditorService() {
11720
11842
  }
11721
11843
 
11722
11844
  // src/editor/installation-prompt.ts
11723
- import { createInterface } from "node:readline/promises";
11845
+ import { createInterface as createInterface2 } from "node:readline/promises";
11724
11846
  async function promptForInstallation(editorName) {
11725
11847
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
11726
11848
  return null;
11727
11849
  }
11728
- const prompt = createInterface({ input: process.stdin, output: process.stdout });
11850
+ const prompt = createInterface2({ input: process.stdin, output: process.stdout });
11729
11851
  try {
11730
11852
  const answer = (await prompt.question(`Install the Luam extension in ${editorName}? [Y/n] `)).trim().toLowerCase();
11731
11853
  return answer === "" || answer === "y" || answer === "yes";
@@ -11848,7 +11970,8 @@ async function runCli(argv, overrides = {}) {
11848
11970
  }
11849
11971
  const root = resolve12(overrides.cwd ?? process.cwd(), parsed.cwd ?? ".");
11850
11972
  if (parsed.command === "init") {
11851
- return runInitCommand(root, logger, { name: parsed.name, force: parsed.force });
11973
+ const target = parsed.operand === null ? root : resolve12(root, parsed.operand);
11974
+ return runInitCommand(target, logger, { name: parsed.name, force: parsed.force, yes: parsed.yes, prompt: overrides.initPrompt ?? promptForProject });
11852
11975
  }
11853
11976
  const editorService = overrides.editorService ?? createEditorService();
11854
11977
  if (parsed.command === "doctor") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thigasdevelopment/luam",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "The Luam compiler for Multi Theft Auto resources.",
5
5
  "keywords": [
6
6
  "mta",