@triflare/fx 1.0.3 → 2.1.0-beta.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 (68) hide show
  1. package/README.md +59 -0
  2. package/bin/fx.js +4 -1162
  3. package/package.json +8 -4
  4. package/product.yml +1 -1
  5. package/src/brand.js +9 -3
  6. package/src/catalog.js +103 -0
  7. package/src/cli/commands/build.js +98 -0
  8. package/src/cli/commands/catalog.js +42 -0
  9. package/src/cli/commands/check.js +44 -0
  10. package/src/cli/commands/completion.js +54 -0
  11. package/src/cli/commands/decompile.js +266 -0
  12. package/src/cli/commands/dev.js +179 -0
  13. package/src/cli/commands/extension.js +110 -0
  14. package/src/cli/commands/feature.js +114 -0
  15. package/src/cli/commands/fmt.js +173 -0
  16. package/src/cli/commands/index.js +45 -0
  17. package/src/cli/commands/init.js +169 -0
  18. package/src/cli/commands/lint.js +270 -0
  19. package/src/cli/commands/package.js +137 -0
  20. package/src/cli/commands/sprite/add.js +85 -0
  21. package/src/cli/commands/sprite/build.js +62 -0
  22. package/src/cli/commands/sprite/helpers.js +66 -0
  23. package/src/cli/commands/sprite/index.js +29 -0
  24. package/src/cli/commands/sprite/list.js +31 -0
  25. package/src/cli/commands/sprite/remove.js +51 -0
  26. package/src/cli/commands/vendor/add.js +98 -0
  27. package/src/cli/commands/vendor/helpers.js +50 -0
  28. package/src/cli/commands/vendor/index.js +39 -0
  29. package/src/cli/commands/vendor/install.js +64 -0
  30. package/src/cli/commands/vendor/list.js +45 -0
  31. package/src/cli/commands/vendor/migrate.js +47 -0
  32. package/src/cli/commands/vendor/outdated.js +51 -0
  33. package/src/cli/commands/vendor/remove.js +34 -0
  34. package/src/cli/commands/vendor/update.js +49 -0
  35. package/src/cli/commands/vendor/verify.js +44 -0
  36. package/src/cli/completion/bash.js +10 -0
  37. package/src/cli/completion/fish.js +9 -0
  38. package/src/cli/completion/index.js +97 -0
  39. package/src/cli/completion/zsh.js +11 -0
  40. package/src/cli/context.js +270 -0
  41. package/src/cli/define-command.js +147 -0
  42. package/src/cli/errors.js +111 -0
  43. package/src/cli/format.js +31 -0
  44. package/src/cli/prompts.js +183 -0
  45. package/src/cli/reporter.js +203 -0
  46. package/src/cli/run.js +248 -0
  47. package/src/cli/templates/index.js +112 -0
  48. package/src/cli/theme.js +84 -0
  49. package/src/compiler.js +133 -0
  50. package/src/config-edit.js +399 -1
  51. package/src/config.js +174 -4
  52. package/src/decompiler.js +45 -0
  53. package/src/extensions.js +72 -19
  54. package/src/features.js +339 -0
  55. package/src/formatter.js +499 -0
  56. package/src/index.js +31 -0
  57. package/src/lexer.js +45 -4
  58. package/src/lint/analyze.js +1414 -0
  59. package/src/lint/config-file.js +97 -0
  60. package/src/lint/config.js +174 -0
  61. package/src/lint/rules.js +461 -0
  62. package/src/linter.js +220 -0
  63. package/src/merge.js +109 -0
  64. package/src/parser.js +53 -13
  65. package/src/project.js +7 -87
  66. package/src/vendor.js +1323 -0
  67. package/vendor/pm-packager-dist/.fx-build.json +1 -1
  68. package/vendor/tempvars/fx-tempvars.js +321 -0
package/src/merge.js ADDED
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Multi-file project merge.
3
+ *
4
+ * Kept separate from `project.js` because merging is pure: it needs no
5
+ * filesystem access, so editor tooling (including the browser VS Code
6
+ * extension host) can reuse it without pulling in `node:fs`.
7
+ */
8
+ import { ParseError } from "./errors.js";
9
+
10
+ /** @param {object} loc */
11
+ function errorInfo(loc) {
12
+ return {
13
+ file: loc && loc.file,
14
+ line: loc && loc.line,
15
+ column: loc && loc.column,
16
+ };
17
+ }
18
+
19
+ /**
20
+ * Merge parsed programs into a single project AST.
21
+ *
22
+ * - At most one `stage { }` declaration is allowed across all files.
23
+ * - Top-level statements from every file become stage content.
24
+ * - At most one plain `sprite "Name" { }` declaration is allowed per name.
25
+ * Additional `partial sprite "Name" { }` fragments are appended to it.
26
+ * - Partial sprites only merge when `partialSprites` is enabled.
27
+ *
28
+ * Bodies are composed base-first, then fragments in build order, so costume
29
+ * order and local variable initializers stay predictable regardless of which
30
+ * file happens to sort first.
31
+ *
32
+ * @param {Array<{ast: object, file: string}>} entries
33
+ * @param {{partialSprites?: boolean, configName?: string}} [options]
34
+ * @returns {object} a `Program` node
35
+ */
36
+ export function mergePrograms(
37
+ entries,
38
+ { partialSprites = false, configName = "fx.yml" } = {},
39
+ ) {
40
+ let stage = null;
41
+ const topItems = [];
42
+ /** @type {Map<string, {name: string, loc: object, base: object|null, fragments: object[], files: string[]}>} */
43
+ const groups = new Map();
44
+
45
+ for (const { ast, file } of entries) {
46
+ if (ast.stage) {
47
+ if (stage) {
48
+ throw new ParseError(
49
+ "A project may only declare one stage",
50
+ errorInfo(ast.stage.loc),
51
+ );
52
+ }
53
+ stage = ast.stage;
54
+ }
55
+ topItems.push(...ast.topItems);
56
+ for (const sprite of ast.sprites) {
57
+ if (sprite.partial && !partialSprites) {
58
+ throw new ParseError(
59
+ `partial sprite ${JSON.stringify(sprite.name)} is disabled; ` +
60
+ `set "fx.partialSprites: true" in ${configName} to enable it`,
61
+ errorInfo(sprite.loc),
62
+ );
63
+ }
64
+
65
+ let group = groups.get(sprite.name);
66
+ if (!group) {
67
+ group = {
68
+ name: sprite.name,
69
+ loc: sprite.loc,
70
+ base: null,
71
+ fragments: [],
72
+ files: [],
73
+ };
74
+ groups.set(sprite.name, group);
75
+ }
76
+ group.files.push(file);
77
+
78
+ if (sprite.partial) {
79
+ group.fragments.push({ decl: sprite, file });
80
+ } else if (group.base) {
81
+ throw new ParseError(
82
+ `Duplicate sprite ${JSON.stringify(sprite.name)} ` +
83
+ `(declared in ${group.base.file} and ${file}); ` +
84
+ `use \`partial sprite ${JSON.stringify(sprite.name)}\` to split it across files`,
85
+ errorInfo(sprite.loc),
86
+ );
87
+ } else {
88
+ group.base = { decl: sprite, file };
89
+ }
90
+ }
91
+ }
92
+
93
+ const sprites = [...groups.values()].map((group) => {
94
+ const base = group.base ? [group.base.decl] : [];
95
+ const fragments = group.fragments.map(({ decl }) => decl);
96
+ const [first] = base.length ? base : fragments;
97
+ return {
98
+ type: "SpriteDecl",
99
+ name: group.name,
100
+ body: [...base, ...fragments].flatMap((decl) => decl.body),
101
+ loc: first.loc,
102
+ open: first.open ?? null,
103
+ partial: false,
104
+ files: group.files,
105
+ };
106
+ });
107
+
108
+ return { type: "Program", stage, sprites, topItems };
109
+ }
package/src/parser.js CHANGED
@@ -127,18 +127,31 @@ class Parser {
127
127
  parseSprite(partial = false, startTok = null) {
128
128
  const tok = this.expect("sprite");
129
129
  const name = this.expectType("string", "a sprite name string").value;
130
- const body = this.parseBlockBody();
131
- return { type: "SpriteDecl", name, body, loc: startTok ?? tok, partial };
130
+ const { body, open } = this.parseBlockBody(true);
131
+ return {
132
+ type: "SpriteDecl",
133
+ name,
134
+ body,
135
+ loc: startTok ?? tok,
136
+ // Tokens are the brace's position, needed to insert declarations into
137
+ // the body when the brace is not on the declaration line.
138
+ open,
139
+ partial,
140
+ };
132
141
  }
133
142
 
134
143
  parseStage() {
135
144
  const tok = this.expect("stage");
136
- const body = this.parseBlockBody();
137
- return { type: "StageDecl", name: "Stage", body, loc: tok };
145
+ const { body, open } = this.parseBlockBody(true);
146
+ return { type: "StageDecl", name: "Stage", body, loc: tok, open };
138
147
  }
139
148
 
140
- parseBlockBody() {
141
- this.expect("{");
149
+ /**
150
+ * Parse `{ ... }`.
151
+ * @param {boolean} [withOpen] return `{body, open}` instead of the items only
152
+ */
153
+ parseBlockBody(withOpen = false) {
154
+ const open = this.expect("{");
142
155
  const items = [];
143
156
  while (!this.atPunct("}")) {
144
157
  if (this.peek().type === "eof")
@@ -146,7 +159,7 @@ class Parser {
146
159
  items.push(this.parseBodyItem());
147
160
  }
148
161
  this.expect("}");
149
- return items;
162
+ return withOpen ? { body: items, open } : items;
150
163
  }
151
164
 
152
165
  parseBodyItem() {
@@ -161,6 +174,12 @@ class Parser {
161
174
  return this.parseVarDecl(true);
162
175
  }
163
176
  break;
177
+ case "thread":
178
+ case "runtime":
179
+ if (this.peek(1).type === "ident" && this.peek(1).value === "let") {
180
+ return this.parseTempVarDecl(tok.value);
181
+ }
182
+ break;
164
183
  case "list":
165
184
  if (this.peek(1).type === "ident") {
166
185
  return this.parseListDecl();
@@ -224,6 +243,16 @@ class Parser {
224
243
  return { type: "VarDecl", name, init, cloud, loc: tok };
225
244
  }
226
245
 
246
+ parseTempVarDecl(kind) {
247
+ const tok = this.next();
248
+ this.expect("let");
249
+ const name = this.expectIdent();
250
+ let init = null;
251
+ if (this.eat("=")) init = this.parseExpression();
252
+ this.expect(";");
253
+ return { type: "TempVarDecl", kind, name, init, loc: tok };
254
+ }
255
+
227
256
  parseListDecl() {
228
257
  const tok = this.expect("list");
229
258
  const name = this.expectIdent();
@@ -248,15 +277,16 @@ class Parser {
248
277
  const ptok = this.peek();
249
278
  const paramName = this.expectIdent();
250
279
  let boolean = false;
280
+ let type = null;
251
281
  // Optional type annotation: `flag: boolean`
252
282
  if (this.eat(":")) {
253
- const typeName = this.expectIdent();
254
- if (typeName === "boolean") boolean = true;
255
- else if (typeName !== "number" && typeName !== "string") {
256
- this.fail(`Unknown parameter type ${JSON.stringify(typeName)}`, ptok);
283
+ type = this.expectIdent();
284
+ if (type === "boolean") boolean = true;
285
+ else if (type !== "number" && type !== "string") {
286
+ this.fail(`Unknown parameter type ${JSON.stringify(type)}`, ptok);
257
287
  }
258
288
  }
259
- params.push({ name: paramName, boolean });
289
+ params.push({ name: paramName, boolean, type });
260
290
  if (!this.eat(",")) break;
261
291
  }
262
292
  this.expect(")");
@@ -603,6 +633,16 @@ class Parser {
603
633
  * @param {string} [file]
604
634
  */
605
635
  export function parse(source, file = "<input>") {
606
- const tokens = tokenize(source, file);
636
+ return parseTokens(tokenize(source, file), source, file);
637
+ }
638
+
639
+ /**
640
+ * Parse an existing token stream. Keeping the caller's tokens lets tools such
641
+ * as the formatter match AST locations back to tokens without re-lexing.
642
+ * @param {import('./lexer.js').Token[]} tokens
643
+ * @param {string} source
644
+ * @param {string} [file]
645
+ */
646
+ export function parseTokens(tokens, source, file = "<input>") {
607
647
  return new Parser(tokens, source, file).parse();
608
648
  }
package/src/project.js CHANGED
@@ -7,8 +7,8 @@
7
7
  */
8
8
  import fs from "node:fs";
9
9
  import path from "node:path";
10
- import { ParseError } from "./errors.js";
11
10
  import { configFilename, sourceExtension } from "./brand.js";
11
+ import { mergePrograms as mergeParsed } from "./merge.js";
12
12
 
13
13
  /** Directories never scanned for `.tfx` sources. */
14
14
  const IGNORED_DIRS = new Set([
@@ -58,99 +58,19 @@ export function discoverSources(dir) {
58
58
  return found.sort();
59
59
  }
60
60
 
61
- /** @param {object} loc */
62
- function errorInfo(loc) {
63
- return {
64
- file: loc && loc.file,
65
- line: loc && loc.line,
66
- column: loc && loc.column,
67
- };
68
- }
69
-
70
61
  /**
71
62
  * Merge parsed programs into a single project AST.
72
63
  *
73
- * - At most one `stage { }` declaration is allowed across all files.
74
- * - Top-level statements from every file become stage content.
75
- * - At most one plain `sprite "Name" { }` declaration is allowed per name.
76
- * Additional `partial sprite "Name" { }` fragments are appended to it.
77
- * - Partial sprites only merge when `partialSprites` is enabled.
78
- *
79
- * Bodies are composed base-first, then fragments in build order, so costume
80
- * order and local variable initializers stay predictable regardless of which
81
- * file happens to sort first.
64
+ * The merge itself is pure (see `merge.js`); this wrapper only injects the
65
+ * configured config filename so project-facing errors keep the right name.
82
66
  *
83
67
  * @param {Array<{ast: object, file: string}>} entries
84
68
  * @param {{partialSprites?: boolean}} [options]
85
69
  * @returns {object} a `Program` node
86
70
  */
87
- export function mergePrograms(entries, { partialSprites = false } = {}) {
88
- let stage = null;
89
- const topItems = [];
90
- /** @type {Map<string, {name: string, loc: object, base: object|null, fragments: object[], files: string[]}>} */
91
- const groups = new Map();
92
-
93
- for (const { ast, file } of entries) {
94
- if (ast.stage) {
95
- if (stage) {
96
- throw new ParseError(
97
- "A project may only declare one stage",
98
- errorInfo(ast.stage.loc),
99
- );
100
- }
101
- stage = ast.stage;
102
- }
103
- topItems.push(...ast.topItems);
104
- for (const sprite of ast.sprites) {
105
- if (sprite.partial && !partialSprites) {
106
- throw new ParseError(
107
- `partial sprite ${JSON.stringify(sprite.name)} is disabled; ` +
108
- `set "fx.partialSprites: true" in ${configFilename()} to enable it`,
109
- errorInfo(sprite.loc),
110
- );
111
- }
112
-
113
- let group = groups.get(sprite.name);
114
- if (!group) {
115
- group = {
116
- name: sprite.name,
117
- loc: sprite.loc,
118
- base: null,
119
- fragments: [],
120
- files: [],
121
- };
122
- groups.set(sprite.name, group);
123
- }
124
- group.files.push(file);
125
-
126
- if (sprite.partial) {
127
- group.fragments.push({ decl: sprite, file });
128
- } else if (group.base) {
129
- throw new ParseError(
130
- `Duplicate sprite ${JSON.stringify(sprite.name)} ` +
131
- `(declared in ${group.base.file} and ${file}); ` +
132
- `use \`partial sprite ${JSON.stringify(sprite.name)}\` to split it across files`,
133
- errorInfo(sprite.loc),
134
- );
135
- } else {
136
- group.base = { decl: sprite, file };
137
- }
138
- }
139
- }
140
-
141
- const sprites = [...groups.values()].map((group) => {
142
- const base = group.base ? [group.base.decl] : [];
143
- const fragments = group.fragments.map(({ decl }) => decl);
144
- const [first] = base.length ? base : fragments;
145
- return {
146
- type: "SpriteDecl",
147
- name: group.name,
148
- body: [...base, ...fragments].flatMap((decl) => decl.body),
149
- loc: first.loc,
150
- partial: false,
151
- files: group.files,
152
- };
71
+ export function mergePrograms(entries, options = {}) {
72
+ return mergeParsed(entries, {
73
+ ...options,
74
+ configName: configFilename(),
153
75
  });
154
-
155
- return { type: "Program", stage, sprites, topItems };
156
76
  }