@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.
- package/README.md +59 -0
- package/bin/fx.js +4 -1162
- package/package.json +8 -4
- package/product.yml +1 -1
- package/src/brand.js +9 -3
- package/src/catalog.js +103 -0
- package/src/cli/commands/build.js +98 -0
- package/src/cli/commands/catalog.js +42 -0
- package/src/cli/commands/check.js +44 -0
- package/src/cli/commands/completion.js +54 -0
- package/src/cli/commands/decompile.js +266 -0
- package/src/cli/commands/dev.js +179 -0
- package/src/cli/commands/extension.js +110 -0
- package/src/cli/commands/feature.js +114 -0
- package/src/cli/commands/fmt.js +173 -0
- package/src/cli/commands/index.js +45 -0
- package/src/cli/commands/init.js +169 -0
- package/src/cli/commands/lint.js +270 -0
- package/src/cli/commands/package.js +137 -0
- package/src/cli/commands/sprite/add.js +85 -0
- package/src/cli/commands/sprite/build.js +62 -0
- package/src/cli/commands/sprite/helpers.js +66 -0
- package/src/cli/commands/sprite/index.js +29 -0
- package/src/cli/commands/sprite/list.js +31 -0
- package/src/cli/commands/sprite/remove.js +51 -0
- package/src/cli/commands/vendor/add.js +98 -0
- package/src/cli/commands/vendor/helpers.js +50 -0
- package/src/cli/commands/vendor/index.js +39 -0
- package/src/cli/commands/vendor/install.js +64 -0
- package/src/cli/commands/vendor/list.js +45 -0
- package/src/cli/commands/vendor/migrate.js +47 -0
- package/src/cli/commands/vendor/outdated.js +51 -0
- package/src/cli/commands/vendor/remove.js +34 -0
- package/src/cli/commands/vendor/update.js +49 -0
- package/src/cli/commands/vendor/verify.js +44 -0
- package/src/cli/completion/bash.js +10 -0
- package/src/cli/completion/fish.js +9 -0
- package/src/cli/completion/index.js +97 -0
- package/src/cli/completion/zsh.js +11 -0
- package/src/cli/context.js +270 -0
- package/src/cli/define-command.js +147 -0
- package/src/cli/errors.js +111 -0
- package/src/cli/format.js +31 -0
- package/src/cli/prompts.js +183 -0
- package/src/cli/reporter.js +203 -0
- package/src/cli/run.js +248 -0
- package/src/cli/templates/index.js +112 -0
- package/src/cli/theme.js +84 -0
- package/src/compiler.js +133 -0
- package/src/config-edit.js +399 -1
- package/src/config.js +174 -4
- package/src/decompiler.js +45 -0
- package/src/extensions.js +72 -19
- package/src/features.js +339 -0
- package/src/formatter.js +499 -0
- package/src/index.js +31 -0
- package/src/lexer.js +45 -4
- package/src/lint/analyze.js +1414 -0
- package/src/lint/config-file.js +97 -0
- package/src/lint/config.js +174 -0
- package/src/lint/rules.js +461 -0
- package/src/linter.js +220 -0
- package/src/merge.js +109 -0
- package/src/parser.js +53 -13
- package/src/project.js +7 -87
- package/src/vendor.js +1323 -0
- package/vendor/pm-packager-dist/.fx-build.json +1 -1
- 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 {
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
254
|
-
if (
|
|
255
|
-
else if (
|
|
256
|
-
this.fail(`Unknown parameter type ${JSON.stringify(
|
|
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
|
-
|
|
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
|
-
*
|
|
74
|
-
*
|
|
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,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
}
|