@triflare/fx 2.0.0 → 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 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.0",
3
+ "version": "2.1.0-beta.1",
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
  ],
package/product.yml CHANGED
@@ -1,7 +1,7 @@
1
1
  # Identity
2
2
  name: "Triflare Fx"
3
3
  command: "fx"
4
- version: "2.0.0"
4
+ version: "2.1.0-beta.1"
5
5
  tagline: "Code PenguinMod projects with text."
6
6
  author: "Triflare"
7
7
  homepage: "https://github.com/triflare/fx"
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
  /**
@@ -0,0 +1,114 @@
1
+ import path from "node:path";
2
+ import {
3
+ disableFeature,
4
+ enableFeature,
5
+ updateFeature,
6
+ } from "../../features.js";
7
+ import { loadVendorConfig, requireConfigPath } from "./vendor/helpers.js";
8
+ import { UsageError } from "../errors.js";
9
+
10
+ const ACTIONS = ["enable", "disable", "update"];
11
+
12
+ export const featureCommand = {
13
+ name: "feature",
14
+ group: "Core Commands:",
15
+ summary: "Install, update or remove a built-in language feature",
16
+ 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.`,
17
+ arguments: ["<name>", "<action>"],
18
+ options: [
19
+ {
20
+ flags: "--force",
21
+ description: "Replace an installed copy that was edited (enable)",
22
+ },
23
+ { flags: "--dry-run", description: "Preview update without writing files" },
24
+ ],
25
+ examples: [
26
+ "fx feature temporaryvariables enable",
27
+ "fx feature temporaryvariables update --dry-run",
28
+ "fx feature temporaryvariables disable",
29
+ ],
30
+ async run({ args, options, cwd, reporter, prompts, yes, product }) {
31
+ const [name, action] = args;
32
+ if (!ACTIONS.includes(action))
33
+ throw new UsageError(
34
+ `Unknown feature action ${JSON.stringify(action)} for ${JSON.stringify(name)}.`,
35
+ { hint: `use one of: ${ACTIONS.join(", ")}` },
36
+ );
37
+
38
+ const configPath = requireConfigPath(cwd, product);
39
+ const configDir = path.dirname(configPath);
40
+ if (action === "enable") {
41
+ const loaded = loadVendorConfig(configPath);
42
+ const result = enableFeature({
43
+ configPath,
44
+ configDir,
45
+ name,
46
+ vendorDir: loaded.vendorDir,
47
+ force: Boolean(options.force),
48
+ });
49
+ if (result.changed)
50
+ reporter.success(`${result.feature.title} enabled (${result.file})`);
51
+ else reporter.info(`${result.feature.title} is already installed`);
52
+ if (result.configChanged)
53
+ reporter.info(`Recorded the feature in ${path.basename(configPath)}`);
54
+ return {
55
+ feature: result.name,
56
+ action,
57
+ file: result.file,
58
+ changed: result.changed,
59
+ };
60
+ }
61
+
62
+ if (action === "disable") {
63
+ await prompts.confirmDestructive({
64
+ message: `Disable ${name} and delete its installed file?`,
65
+ yes,
66
+ });
67
+ const result = disableFeature({ configPath, configDir, name });
68
+ if (result.fileRemoved)
69
+ reporter.success(
70
+ `Disabled ${result.feature.title} (${result.file}) and deleted its copy`,
71
+ );
72
+ else
73
+ reporter.success(`Disabled ${result.feature.title} (${result.file})`);
74
+ if (result.filePreserved)
75
+ reporter.info(
76
+ `Kept ${result.file}: it is not the copy Fx installs, so it may be user-owned`,
77
+ );
78
+ return {
79
+ feature: result.name,
80
+ action,
81
+ file: result.file,
82
+ fileRemoved: result.fileRemoved,
83
+ filePreserved: result.filePreserved,
84
+ };
85
+ }
86
+
87
+ const result = updateFeature({
88
+ configPath,
89
+ configDir,
90
+ name,
91
+ dryRun: Boolean(options.dryRun),
92
+ });
93
+ if (!result.changed) {
94
+ reporter.info(`${result.feature.title} is already up to date`);
95
+ return {
96
+ feature: result.name,
97
+ action,
98
+ file: result.file,
99
+ changed: false,
100
+ };
101
+ }
102
+ reporter.text(result.diff);
103
+ if (result.dryRun)
104
+ reporter.info(`Dry run: ${result.file} would be replaced`);
105
+ else reporter.success(`Updated ${result.feature.title} (${result.file})`);
106
+ return {
107
+ feature: result.name,
108
+ action,
109
+ file: result.file,
110
+ changed: true,
111
+ dryRun: result.dryRun,
112
+ };
113
+ },
114
+ };
@@ -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,7 +5,10 @@ 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";
11
14
  import { registerVendor } from "./vendor/index.js";
@@ -17,7 +20,10 @@ const TOP_LEVEL = [
17
20
  packageCommand,
18
21
  decompileCommand,
19
22
  checkCommand,
23
+ lintCommand,
24
+ fmtCommand,
20
25
  catalogCommand,
26
+ featureCommand,
21
27
  initCommand,
22
28
  completionCommand,
23
29
  ];