@triflare/fx 2.0.0 → 2.1.0-beta.2
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 +27 -0
- package/package.json +6 -4
- package/product.yml +1 -1
- package/src/build.js +23 -6
- package/src/catalog.js +103 -0
- package/src/cli/commands/check.js +3 -1
- package/src/cli/commands/feature.js +163 -0
- package/src/cli/commands/fmt.js +173 -0
- package/src/cli/commands/index.js +8 -0
- package/src/cli/commands/lint.js +274 -0
- package/src/cli/commands/test.js +186 -0
- package/src/cli/context.js +16 -1
- package/src/cli/define-command.js +8 -1
- package/src/cli/reporter.js +9 -0
- package/src/compiler.js +133 -0
- package/src/config-edit.js +197 -0
- package/src/config.js +114 -1
- package/src/decompiler.js +45 -0
- package/src/features.js +425 -0
- package/src/formatter.js +519 -0
- package/src/index.js +31 -0
- package/src/lexer.js +45 -4
- package/src/lint/analyze.js +1434 -0
- package/src/lint/config-file.js +111 -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 +116 -0
- package/src/parser.js +131 -16
- package/src/project.js +21 -90
- package/src/test/discover.js +82 -0
- package/src/test/parse.js +72 -0
- package/src/test/pattern.js +84 -0
- package/src/test/reporters.js +332 -0
- package/src/test/runner.js +1457 -0
- package/src/vm.js +325 -0
- package/vendor/pm-packager-dist/.fx-build.json +1 -1
- package/vendor/tempvars/fx-tempvars.js +321 -0
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ Code PenguinMod projects with text.
|
|
|
16
16
|
- [Highlights](#highlights)
|
|
17
17
|
- [Overview](#overview)
|
|
18
18
|
- [Usage](#usage)
|
|
19
|
+
- [Formatting](#formatting)
|
|
19
20
|
- [Vendoring Extensions](#vendoring-extensions)
|
|
20
21
|
- [Installation](#installation)
|
|
21
22
|
- [Feedback and Contributing](#feedback-and-contributing)
|
|
@@ -54,8 +55,34 @@ $ fx build
|
|
|
54
55
|
extensions: (none)
|
|
55
56
|
```
|
|
56
57
|
|
|
58
|
+
<!-- coderabbit-ai-generated:v1 -->
|
|
59
|
+
|
|
60
|
+
<!-- coderabbit-ai-generated-section:v1:1f825081f9f722158035083df5b16f42120cc5fbd7d56eb25c770f3c64e339b2 -->
|
|
61
|
+
|
|
62
|
+
## Formatting
|
|
63
|
+
|
|
64
|
+
Run `fx fmt` to format every `.tfx` source in the current directory, or pass a file or directory:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
fx fmt
|
|
68
|
+
fx fmt src/main.tfx
|
|
69
|
+
fx fmt src/ --check
|
|
70
|
+
cat src/main.tfx | fx fmt --stdin
|
|
71
|
+
fx fmt src/main.tfx --stdout
|
|
72
|
+
fx fmt --check --json
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- `--check` leaves files untouched and exits with status 1 if formatting is needed (0 if already formatted).
|
|
76
|
+
- `--stdin` reads source from standard input and writes formatted source to standard output; it takes no file argument. With `--check`, it checks the input instead.
|
|
77
|
+
- `--stdout` writes formatted source for a single file to standard output without changing the file. It cannot be combined with `--check`.
|
|
78
|
+
- `--json` returns a structured result, including formatted code for `--stdin` and `--stdout` output.
|
|
79
|
+
|
|
80
|
+
Formatting is idempotent: running it twice produces no further changes. It preserves comments, declaration order, and project data, normalizes whitespace and line endings, and does not compile the project. Files with parse errors are left untouched.
|
|
81
|
+
|
|
57
82
|
## Vendoring Extensions
|
|
58
83
|
|
|
84
|
+
<!-- /coderabbit-ai-generated:v1 -->
|
|
85
|
+
|
|
59
86
|
Extension URLs are mutable and can disappear, so Fx can commit a verified copy of every extension instead. Declare them under `vendor:` in `fx.yml`; order is load order:
|
|
60
87
|
|
|
61
88
|
```yaml
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@triflare/fx",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.1.0-beta.2",
|
|
4
4
|
"description": "Code PenguinMod projects with text.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -15,12 +15,14 @@
|
|
|
15
15
|
"exports": {
|
|
16
16
|
".": "./src/index.js",
|
|
17
17
|
"./catalog": "./src/catalog.js",
|
|
18
|
-
"./language": "./src/language.js"
|
|
18
|
+
"./language": "./src/language.js",
|
|
19
|
+
"./lint": "./src/lint/analyze.js"
|
|
19
20
|
},
|
|
20
21
|
"files": [
|
|
21
22
|
"bin",
|
|
22
23
|
"src",
|
|
23
24
|
"vendor/pm-packager-dist",
|
|
25
|
+
"vendor/tempvars",
|
|
24
26
|
"product.yml",
|
|
25
27
|
"README.md"
|
|
26
28
|
],
|
|
@@ -48,6 +50,7 @@
|
|
|
48
50
|
"js-yaml": "^5.4.3",
|
|
49
51
|
"jszip": "^3.10.1",
|
|
50
52
|
"quickjs-emscripten": "^0.32.0",
|
|
53
|
+
"scratch-vm": "github:PenguinMod/PenguinMod-Vm#75ef6994e55270b9c2a2f3cb2edf6a5bf31cdf2a",
|
|
51
54
|
"sha.js": "^2.4.12"
|
|
52
55
|
},
|
|
53
56
|
"devDependencies": {
|
|
@@ -55,8 +58,7 @@
|
|
|
55
58
|
"eslint": "^10.14.0",
|
|
56
59
|
"eslint-config-prettier": "^10.1.8",
|
|
57
60
|
"globals": "^17.13.0",
|
|
58
|
-
"prettier": "^3.9.10"
|
|
59
|
-
"scratch-vm": "github:PenguinMod/PenguinMod-Vm#75ef6994e55270b9c2a2f3cb2edf6a5bf31cdf2a"
|
|
61
|
+
"prettier": "^3.9.10"
|
|
60
62
|
},
|
|
61
63
|
"overrides": {
|
|
62
64
|
"minimatch": "^11.0.4",
|
package/product.yml
CHANGED
package/src/build.js
CHANGED
|
@@ -9,6 +9,7 @@ import { Compiler } from "./compiler.js";
|
|
|
9
9
|
import { zipSync } from "./sb3.js";
|
|
10
10
|
import { loadConfigFrom, DEFAULT_PROJECT, DEFAULT_FX } from "./config.js";
|
|
11
11
|
import { sourceExtension } from "./brand.js";
|
|
12
|
+
import { DEFAULT_TEST_PATTERN } from "./test/discover.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* @typedef {object} BuildOptions
|
|
@@ -30,7 +31,7 @@ import { sourceExtension } from "./brand.js";
|
|
|
30
31
|
* @param {BuildOptions} options
|
|
31
32
|
* @returns {{dir: string, entries: Array<{file: string, source: string}>}}
|
|
32
33
|
*/
|
|
33
|
-
function resolveSources(options) {
|
|
34
|
+
function resolveSources(options, { testPattern = DEFAULT_TEST_PATTERN } = {}) {
|
|
34
35
|
const cwd = process.cwd();
|
|
35
36
|
|
|
36
37
|
if (Array.isArray(options.sources)) {
|
|
@@ -84,7 +85,7 @@ function resolveSources(options) {
|
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
const dir = path.resolve(options.dir || cwd);
|
|
87
|
-
const files = discoverSources(dir);
|
|
88
|
+
const files = discoverSources(dir, { testPattern });
|
|
88
89
|
if (files.length === 0)
|
|
89
90
|
throw new Error(`No ${sourceExtension()} files found under ${dir}`);
|
|
90
91
|
return {
|
|
@@ -101,15 +102,23 @@ function resolveSources(options) {
|
|
|
101
102
|
* @returns {{project: object, assets: Map<string, Buffer>, sb3: Buffer, config: object, fx: object, ast: object, files: string[], warnings: string[]}}
|
|
102
103
|
*/
|
|
103
104
|
export function build(options = {}) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
// Load the config before scanning, so a project's test glob decides which
|
|
106
|
+
// files are test files (and therefore excluded from the build).
|
|
107
|
+
const searchDir =
|
|
108
|
+
options.configDir ||
|
|
109
|
+
(options.dir
|
|
110
|
+
? path.resolve(options.dir)
|
|
111
|
+
: options.file
|
|
112
|
+
? path.dirname(path.resolve(options.file))
|
|
113
|
+
: Array.isArray(options.files) && options.files.length > 0
|
|
114
|
+
? path.dirname(path.resolve(options.files[0]))
|
|
115
|
+
: process.cwd());
|
|
107
116
|
|
|
108
117
|
let projectConfig = options.projectConfig || null;
|
|
109
118
|
let fxConfig = options.fxConfig || null;
|
|
110
119
|
let configPath = null;
|
|
111
120
|
if (!projectConfig && options.useConfigFile !== false) {
|
|
112
|
-
const loaded = loadConfigFrom(
|
|
121
|
+
const loaded = loadConfigFrom(searchDir);
|
|
113
122
|
projectConfig = loaded.project;
|
|
114
123
|
fxConfig = loaded.fx;
|
|
115
124
|
configPath = loaded.path;
|
|
@@ -117,6 +126,14 @@ export function build(options = {}) {
|
|
|
117
126
|
if (!projectConfig) projectConfig = { ...DEFAULT_PROJECT };
|
|
118
127
|
if (!fxConfig) fxConfig = { ...DEFAULT_FX };
|
|
119
128
|
|
|
129
|
+
const { dir, entries } = resolveSources(options, {
|
|
130
|
+
testPattern:
|
|
131
|
+
options.testPattern ??
|
|
132
|
+
(fxConfig.tests ? fxConfig.tests.pattern : DEFAULT_TEST_PATTERN),
|
|
133
|
+
});
|
|
134
|
+
if (entries.length === 0)
|
|
135
|
+
throw new Error("No Triflare Fx sources to compile");
|
|
136
|
+
|
|
120
137
|
const asts = entries.map(({ file, source }) => ({
|
|
121
138
|
ast: parse(source, file),
|
|
122
139
|
file,
|
package/src/catalog.js
CHANGED
|
@@ -610,6 +610,109 @@ const CORE_CATALOG = {
|
|
|
610
610
|
kind: "boolean",
|
|
611
611
|
fields: { VALUE: ["is penguinmod?", null] },
|
|
612
612
|
},
|
|
613
|
+
|
|
614
|
+
// ------------------------------------------------- temporary variables
|
|
615
|
+
// The Fx fork of Temporary Variables (`fxTempVars`). Entries are marked
|
|
616
|
+
// `requiresExtension` so the compiler can refuse to emit blocks whose
|
|
617
|
+
// extension is not loaded, and so the linter can point at the feature.
|
|
618
|
+
"tempvars.setThread": {
|
|
619
|
+
opcode: "fxTempVars_setThreadVariable",
|
|
620
|
+
extension: "fxTempVars",
|
|
621
|
+
requiresExtension: true,
|
|
622
|
+
inputs: {
|
|
623
|
+
VAR: { type: "string", default: "variable" },
|
|
624
|
+
STRING: { type: "string", default: "0" },
|
|
625
|
+
},
|
|
626
|
+
},
|
|
627
|
+
"tempvars.changeThread": {
|
|
628
|
+
opcode: "fxTempVars_changeThreadVariable",
|
|
629
|
+
extension: "fxTempVars",
|
|
630
|
+
requiresExtension: true,
|
|
631
|
+
inputs: {
|
|
632
|
+
VAR: { type: "string", default: "variable" },
|
|
633
|
+
NUM: { type: "number", default: "1" },
|
|
634
|
+
},
|
|
635
|
+
},
|
|
636
|
+
"tempvars.thread": {
|
|
637
|
+
opcode: "fxTempVars_getThreadVariable",
|
|
638
|
+
kind: "reporter",
|
|
639
|
+
extension: "fxTempVars",
|
|
640
|
+
requiresExtension: true,
|
|
641
|
+
inputs: { VAR: { type: "string", default: "variable" } },
|
|
642
|
+
},
|
|
643
|
+
"tempvars.threadExists": {
|
|
644
|
+
opcode: "fxTempVars_threadVariableExists",
|
|
645
|
+
kind: "boolean",
|
|
646
|
+
extension: "fxTempVars",
|
|
647
|
+
requiresExtension: true,
|
|
648
|
+
inputs: { VAR: { type: "string", default: "variable" } },
|
|
649
|
+
},
|
|
650
|
+
"tempvars.forEachThread": {
|
|
651
|
+
opcode: "fxTempVars_forEachThreadVariable",
|
|
652
|
+
extension: "fxTempVars",
|
|
653
|
+
requiresExtension: true,
|
|
654
|
+
inputs: {
|
|
655
|
+
VAR: { type: "string", default: "thread variable" },
|
|
656
|
+
NUM: { type: "number", default: "10" },
|
|
657
|
+
SUBSTACK: SUBSTACK,
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
"tempvars.threads": {
|
|
661
|
+
opcode: "fxTempVars_listThreadVariables",
|
|
662
|
+
kind: "reporter",
|
|
663
|
+
extension: "fxTempVars",
|
|
664
|
+
requiresExtension: true,
|
|
665
|
+
},
|
|
666
|
+
"tempvars.setRuntime": {
|
|
667
|
+
opcode: "fxTempVars_setRuntimeVariable",
|
|
668
|
+
extension: "fxTempVars",
|
|
669
|
+
requiresExtension: true,
|
|
670
|
+
inputs: {
|
|
671
|
+
VAR: { type: "string", default: "variable" },
|
|
672
|
+
STRING: { type: "string", default: "0" },
|
|
673
|
+
},
|
|
674
|
+
},
|
|
675
|
+
"tempvars.changeRuntime": {
|
|
676
|
+
opcode: "fxTempVars_changeRuntimeVariable",
|
|
677
|
+
extension: "fxTempVars",
|
|
678
|
+
requiresExtension: true,
|
|
679
|
+
inputs: {
|
|
680
|
+
VAR: { type: "string", default: "variable" },
|
|
681
|
+
// Upstream declares NUM as a string input; keep it so block JSON matches.
|
|
682
|
+
NUM: { type: "string", default: "1" },
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
"tempvars.runtime": {
|
|
686
|
+
opcode: "fxTempVars_getRuntimeVariable",
|
|
687
|
+
kind: "reporter",
|
|
688
|
+
extension: "fxTempVars",
|
|
689
|
+
requiresExtension: true,
|
|
690
|
+
inputs: { VAR: { type: "string", default: "variable" } },
|
|
691
|
+
},
|
|
692
|
+
"tempvars.runtimeExists": {
|
|
693
|
+
opcode: "fxTempVars_runtimeVariableExists",
|
|
694
|
+
kind: "boolean",
|
|
695
|
+
extension: "fxTempVars",
|
|
696
|
+
requiresExtension: true,
|
|
697
|
+
inputs: { VAR: { type: "string", default: "variable" } },
|
|
698
|
+
},
|
|
699
|
+
"tempvars.deleteRuntime": {
|
|
700
|
+
opcode: "fxTempVars_deleteRuntimeVariable",
|
|
701
|
+
extension: "fxTempVars",
|
|
702
|
+
requiresExtension: true,
|
|
703
|
+
inputs: { VAR: { type: "string", default: "variable" } },
|
|
704
|
+
},
|
|
705
|
+
"tempvars.deleteAllRuntime": {
|
|
706
|
+
opcode: "fxTempVars_deleteAllRuntimeVariables",
|
|
707
|
+
extension: "fxTempVars",
|
|
708
|
+
requiresExtension: true,
|
|
709
|
+
},
|
|
710
|
+
"tempvars.runtimes": {
|
|
711
|
+
opcode: "fxTempVars_listRuntimeVariables",
|
|
712
|
+
kind: "reporter",
|
|
713
|
+
extension: "fxTempVars",
|
|
714
|
+
requiresExtension: true,
|
|
715
|
+
},
|
|
613
716
|
};
|
|
614
717
|
|
|
615
718
|
/**
|
|
@@ -26,7 +26,9 @@ building an archive.`,
|
|
|
26
26
|
migrateLegacy: false,
|
|
27
27
|
});
|
|
28
28
|
const { target, loaded } = state;
|
|
29
|
-
const files = target.file
|
|
29
|
+
const files = target.file
|
|
30
|
+
? [target.file]
|
|
31
|
+
: discoverSources(target.dir, { testPattern: loaded.fx?.tests?.pattern });
|
|
30
32
|
if (files.length === 0)
|
|
31
33
|
throw new UsageError(`No ${EXTENSION} files found under ${target.dir}`);
|
|
32
34
|
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
disableFeature,
|
|
4
|
+
enableFeature,
|
|
5
|
+
featureEnabled,
|
|
6
|
+
getFeature,
|
|
7
|
+
updateFeature,
|
|
8
|
+
} from "../../features.js";
|
|
9
|
+
import { loadVendorConfig, requireConfigPath } from "./vendor/helpers.js";
|
|
10
|
+
import { UsageError } from "../errors.js";
|
|
11
|
+
|
|
12
|
+
const ACTIONS = ["enable", "disable", "update"];
|
|
13
|
+
|
|
14
|
+
export const featureCommand = {
|
|
15
|
+
name: "feature",
|
|
16
|
+
group: "Core Commands:",
|
|
17
|
+
summary: "Install, update or remove a built-in language feature",
|
|
18
|
+
description: `Features are extension-backed language additions. "enable" copies the\nfeature's file into the project and records it under "features:" in fx.yml;\n"update" replaces that copy with the one bundled in this Fx version; and\n"disable" removes both. The file is committed, so Fx upgrades never change it.`,
|
|
19
|
+
arguments: ["<name>", "[action]"],
|
|
20
|
+
options: [
|
|
21
|
+
{
|
|
22
|
+
flags: "--force",
|
|
23
|
+
description: "Replace an installed copy that was edited (enable)",
|
|
24
|
+
},
|
|
25
|
+
{ flags: "--dry-run", description: "Preview update without writing files" },
|
|
26
|
+
],
|
|
27
|
+
examples: [
|
|
28
|
+
"fx feature temporaryvariables enable",
|
|
29
|
+
"fx feature temporaryvariables update --dry-run",
|
|
30
|
+
"fx feature temporaryvariables disable",
|
|
31
|
+
],
|
|
32
|
+
async run({ args, options, cwd, reporter, prompts, yes, product }) {
|
|
33
|
+
const [name, action] = args;
|
|
34
|
+
const configPath = requireConfigPath(cwd, product);
|
|
35
|
+
const configDir = path.dirname(configPath);
|
|
36
|
+
const feature = getFeature(name);
|
|
37
|
+
|
|
38
|
+
if (action == null) {
|
|
39
|
+
if (!feature.core)
|
|
40
|
+
throw new UsageError(`Missing an action for ${JSON.stringify(name)}.`, {
|
|
41
|
+
hint: `use one of: ${ACTIONS.join(", ")}`,
|
|
42
|
+
});
|
|
43
|
+
const enabled = featureEnabled({ configPath, name: feature.name });
|
|
44
|
+
reporter.detail("Feature", feature.title);
|
|
45
|
+
reporter.detail("Status", enabled ? "enabled" : "disabled");
|
|
46
|
+
reporter.detail("Setting", `fx.${feature.name}`);
|
|
47
|
+
reporter.info(
|
|
48
|
+
enabled
|
|
49
|
+
? `Run "fx test" to run the project's tests.`
|
|
50
|
+
: `Run "fx feature ${feature.name} enable" to enable it.`,
|
|
51
|
+
);
|
|
52
|
+
return {
|
|
53
|
+
feature: feature.name,
|
|
54
|
+
action: "status",
|
|
55
|
+
enabled,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!ACTIONS.includes(action))
|
|
60
|
+
throw new UsageError(
|
|
61
|
+
`Unknown feature action ${JSON.stringify(action)} for ${JSON.stringify(name)}.`,
|
|
62
|
+
{ hint: `use one of: ${ACTIONS.join(", ")}` },
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (action === "enable") {
|
|
66
|
+
const loaded = loadVendorConfig(configPath);
|
|
67
|
+
const result = enableFeature({
|
|
68
|
+
configPath,
|
|
69
|
+
configDir,
|
|
70
|
+
name,
|
|
71
|
+
vendorDir: loaded.vendorDir,
|
|
72
|
+
force: Boolean(options.force),
|
|
73
|
+
});
|
|
74
|
+
if (result.changed)
|
|
75
|
+
reporter.success(
|
|
76
|
+
result.file
|
|
77
|
+
? `${result.feature.title} enabled (${result.file})`
|
|
78
|
+
: `${result.feature.title} enabled`,
|
|
79
|
+
);
|
|
80
|
+
else reporter.info(`${result.feature.title} is already enabled`);
|
|
81
|
+
if (result.configChanged)
|
|
82
|
+
reporter.info(`Recorded the feature in ${path.basename(configPath)}`);
|
|
83
|
+
return {
|
|
84
|
+
feature: result.name,
|
|
85
|
+
action,
|
|
86
|
+
file: result.file,
|
|
87
|
+
changed: result.changed,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (action === "disable") {
|
|
92
|
+
if (feature.core) {
|
|
93
|
+
const result = disableFeature({ configPath, configDir, name });
|
|
94
|
+
if (!result.changed) {
|
|
95
|
+
reporter.info(`${result.feature.title} is already disabled`);
|
|
96
|
+
return {
|
|
97
|
+
feature: result.name,
|
|
98
|
+
action,
|
|
99
|
+
changed: false,
|
|
100
|
+
warning: null,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
reporter.success(`Disabled ${result.feature.title}`);
|
|
104
|
+
if (result.warning) reporter.warn(result.warning);
|
|
105
|
+
return {
|
|
106
|
+
feature: result.name,
|
|
107
|
+
action,
|
|
108
|
+
changed: true,
|
|
109
|
+
warning: result.warning,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
await prompts.confirmDestructive({
|
|
113
|
+
message: `Disable ${name} and delete its installed file?`,
|
|
114
|
+
yes,
|
|
115
|
+
});
|
|
116
|
+
const result = disableFeature({ configPath, configDir, name });
|
|
117
|
+
if (result.fileRemoved)
|
|
118
|
+
reporter.success(
|
|
119
|
+
`Disabled ${result.feature.title} (${result.file}) and deleted its copy`,
|
|
120
|
+
);
|
|
121
|
+
else
|
|
122
|
+
reporter.success(`Disabled ${result.feature.title} (${result.file})`);
|
|
123
|
+
if (result.filePreserved)
|
|
124
|
+
reporter.info(
|
|
125
|
+
`Kept ${result.file}: it is not the copy Fx installs, so it may be user-owned`,
|
|
126
|
+
);
|
|
127
|
+
return {
|
|
128
|
+
feature: result.name,
|
|
129
|
+
action,
|
|
130
|
+
file: result.file,
|
|
131
|
+
fileRemoved: result.fileRemoved,
|
|
132
|
+
filePreserved: result.filePreserved,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const result = updateFeature({
|
|
137
|
+
configPath,
|
|
138
|
+
configDir,
|
|
139
|
+
name,
|
|
140
|
+
dryRun: Boolean(options.dryRun),
|
|
141
|
+
});
|
|
142
|
+
if (!result.changed) {
|
|
143
|
+
reporter.info(`${result.feature.title} is already up to date`);
|
|
144
|
+
return {
|
|
145
|
+
feature: result.name,
|
|
146
|
+
action,
|
|
147
|
+
file: result.file,
|
|
148
|
+
changed: false,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
reporter.text(result.diff);
|
|
152
|
+
if (result.dryRun)
|
|
153
|
+
reporter.info(`Dry run: ${result.file} would be replaced`);
|
|
154
|
+
else reporter.success(`Updated ${result.feature.title} (${result.file})`);
|
|
155
|
+
return {
|
|
156
|
+
feature: result.name,
|
|
157
|
+
action,
|
|
158
|
+
file: result.file,
|
|
159
|
+
changed: true,
|
|
160
|
+
dryRun: result.dryRun,
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { sourceExtension } from "../../brand.js";
|
|
5
|
+
import { formatSource } from "../../formatter.js";
|
|
6
|
+
import { discoverSources } from "../../project.js";
|
|
7
|
+
import { resolveInput } from "../context.js";
|
|
8
|
+
import { outcome } from "../define-command.js";
|
|
9
|
+
import { EXIT, UsageError, errorToJson, formatError } from "../errors.js";
|
|
10
|
+
import { plural } from "../format.js";
|
|
11
|
+
|
|
12
|
+
const EXTENSION = sourceExtension();
|
|
13
|
+
|
|
14
|
+
function displayPath(cwd, file) {
|
|
15
|
+
const relativePath = path.relative(cwd, file);
|
|
16
|
+
return relativePath.startsWith("..") || path.isAbsolute(relativePath)
|
|
17
|
+
? file
|
|
18
|
+
: relativePath;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readStdin() {
|
|
22
|
+
if (process.stdin.isTTY)
|
|
23
|
+
throw new UsageError("--stdin expects source on standard input", {
|
|
24
|
+
hint: "pipe a file into standard input, such as: cat src/main.tfx",
|
|
25
|
+
});
|
|
26
|
+
return fs.readFileSync(0, "utf8");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const fmtCommand = {
|
|
30
|
+
name: "fmt",
|
|
31
|
+
aliases: ["format"],
|
|
32
|
+
group: "Core Commands:",
|
|
33
|
+
summary: "Format sources into canonical style",
|
|
34
|
+
description: `Rewrite a file, or every ${EXTENSION} file in a directory
|
|
35
|
+
(defaults to the working directory), in the canonical source format.
|
|
36
|
+
Formatting is idempotent, preserves comments and project data, and never
|
|
37
|
+
compiles a project, so it cannot change generated behavior.`,
|
|
38
|
+
arguments: [`[input${EXTENSION}|dir]`],
|
|
39
|
+
options: [
|
|
40
|
+
{
|
|
41
|
+
flags: "--check",
|
|
42
|
+
description: "Do not write files; exit 1 if formatting is needed",
|
|
43
|
+
},
|
|
44
|
+
{ flags: "--stdin", description: "Format source read from stdin" },
|
|
45
|
+
{
|
|
46
|
+
flags: "--stdout",
|
|
47
|
+
description: "Write formatted source to stdout instead of files",
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
examples: [
|
|
51
|
+
"fx fmt",
|
|
52
|
+
"fx fmt src/main.tfx",
|
|
53
|
+
"fx fmt --check",
|
|
54
|
+
"fx fmt --stdin",
|
|
55
|
+
"fx fmt src/main.tfx --stdout",
|
|
56
|
+
],
|
|
57
|
+
async run({ args, cwd, options, reporter, product }) {
|
|
58
|
+
if (options.stdin) {
|
|
59
|
+
// Commander fills a missing optional argument with null.
|
|
60
|
+
if (args[0] != null)
|
|
61
|
+
throw new UsageError("--stdin does not take a file argument", {
|
|
62
|
+
hint: `run "${product.command} fmt --stdin" with piped input`,
|
|
63
|
+
});
|
|
64
|
+
const source = readStdin();
|
|
65
|
+
const code = formatSource(source, "<stdin>");
|
|
66
|
+
const changed = code !== source;
|
|
67
|
+
|
|
68
|
+
if (options.check) {
|
|
69
|
+
if (changed) reporter.error("stdin is not formatted");
|
|
70
|
+
else reporter.success("stdin is formatted");
|
|
71
|
+
return outcome({
|
|
72
|
+
exitCode: changed ? EXIT.FAILURE : EXIT.OK,
|
|
73
|
+
data: { file: "<stdin>", changed, checked: true },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const data = { file: "<stdin>", changed, checked: false };
|
|
78
|
+
if (reporter.json) data.code = code;
|
|
79
|
+
else reporter.raw(code);
|
|
80
|
+
return outcome({ data });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const target = resolveInput(args[0], cwd);
|
|
84
|
+
const files = target.file ? [target.file] : discoverSources(target.dir);
|
|
85
|
+
if (files.length === 0)
|
|
86
|
+
throw new UsageError(`No ${EXTENSION} files found under ${target.dir}`);
|
|
87
|
+
if (options.stdout && options.check)
|
|
88
|
+
throw new UsageError("--stdout cannot be combined with --check");
|
|
89
|
+
if (options.stdout && !target.file)
|
|
90
|
+
throw new UsageError(
|
|
91
|
+
"--stdout requires a single file; pass one or use --stdin",
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const results = [];
|
|
95
|
+
let changed = 0;
|
|
96
|
+
let parseErrors = 0;
|
|
97
|
+
let stdoutCode = null;
|
|
98
|
+
|
|
99
|
+
for (const file of files) {
|
|
100
|
+
const source = fs.readFileSync(file, "utf8");
|
|
101
|
+
let code;
|
|
102
|
+
try {
|
|
103
|
+
code = formatSource(source, file);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
parseErrors += 1;
|
|
106
|
+
results.push({
|
|
107
|
+
file: displayPath(cwd, file),
|
|
108
|
+
status: "error",
|
|
109
|
+
changed: false,
|
|
110
|
+
error: errorToJson(error),
|
|
111
|
+
});
|
|
112
|
+
reporter.error(formatError(error));
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const differs = code !== source;
|
|
117
|
+
if (differs) changed += 1;
|
|
118
|
+
results.push({
|
|
119
|
+
file: displayPath(cwd, file),
|
|
120
|
+
status: differs
|
|
121
|
+
? options.check
|
|
122
|
+
? "unformatted"
|
|
123
|
+
: "formatted"
|
|
124
|
+
: "unchanged",
|
|
125
|
+
changed: differs,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (options.stdout) {
|
|
129
|
+
if (!options.check) {
|
|
130
|
+
stdoutCode = code;
|
|
131
|
+
reporter.raw(code);
|
|
132
|
+
}
|
|
133
|
+
} else if (differs && !options.check) {
|
|
134
|
+
fs.writeFileSync(file, code);
|
|
135
|
+
reporter.bullet(displayPath(cwd, file));
|
|
136
|
+
} else if (differs) {
|
|
137
|
+
reporter.bullet(displayPath(cwd, file));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const data = {
|
|
142
|
+
checked: Boolean(options.check),
|
|
143
|
+
files: results,
|
|
144
|
+
changed,
|
|
145
|
+
unchanged: results.length - changed - parseErrors,
|
|
146
|
+
parseErrors,
|
|
147
|
+
};
|
|
148
|
+
if (options.stdout && reporter.json) data.code = stdoutCode;
|
|
149
|
+
|
|
150
|
+
if (!options.stdout) {
|
|
151
|
+
if (options.check) {
|
|
152
|
+
if (changed > 0)
|
|
153
|
+
reporter.error(`${plural(changed, "file")} would be reformatted`);
|
|
154
|
+
else if (parseErrors === 0)
|
|
155
|
+
reporter.success(
|
|
156
|
+
`Checked ${plural(files.length, "file")}: all formatted`,
|
|
157
|
+
);
|
|
158
|
+
} else if (changed > 0) {
|
|
159
|
+
reporter.success(`Formatted ${plural(changed, "file")}`);
|
|
160
|
+
} else if (parseErrors === 0) {
|
|
161
|
+
reporter.success(
|
|
162
|
+
`All ${plural(files.length, "file")} already formatted`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const exitCode =
|
|
168
|
+
parseErrors > 0 || (options.check && changed > 0)
|
|
169
|
+
? EXIT.FAILURE
|
|
170
|
+
: EXIT.OK;
|
|
171
|
+
return outcome({ exitCode, data });
|
|
172
|
+
},
|
|
173
|
+
};
|
|
@@ -5,9 +5,13 @@ import { checkCommand } from "./check.js";
|
|
|
5
5
|
import { decompileCommand } from "./decompile.js";
|
|
6
6
|
import { devCommand } from "./dev.js";
|
|
7
7
|
import { registerExtension } from "./extension.js";
|
|
8
|
+
import { featureCommand } from "./feature.js";
|
|
9
|
+
import { fmtCommand } from "./fmt.js";
|
|
8
10
|
import { initCommand } from "./init.js";
|
|
11
|
+
import { lintCommand } from "./lint.js";
|
|
9
12
|
import { packageCommand } from "./package.js";
|
|
10
13
|
import { registerSprite } from "./sprite/index.js";
|
|
14
|
+
import { testCommand } from "./test.js";
|
|
11
15
|
import { registerVendor } from "./vendor/index.js";
|
|
12
16
|
import { defineCommand } from "../define-command.js";
|
|
13
17
|
|
|
@@ -17,7 +21,11 @@ const TOP_LEVEL = [
|
|
|
17
21
|
packageCommand,
|
|
18
22
|
decompileCommand,
|
|
19
23
|
checkCommand,
|
|
24
|
+
lintCommand,
|
|
25
|
+
fmtCommand,
|
|
20
26
|
catalogCommand,
|
|
27
|
+
featureCommand,
|
|
28
|
+
testCommand,
|
|
21
29
|
initCommand,
|
|
22
30
|
completionCommand,
|
|
23
31
|
];
|