@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triflare/fx",
3
- "version": "1.0.3",
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
  ],
@@ -40,12 +42,14 @@
40
42
  "prepack": "node scripts/build-packager.js --if-missing"
41
43
  },
42
44
  "dependencies": {
43
- "js-yaml": "^5.4.3",
44
- "quickjs-emscripten": "^0.32.0",
45
+ "@clack/prompts": "^1.8.1",
45
46
  "@fiahfy/icns": "0.0.7",
46
47
  "@turbowarp/sbdl": "^7.0.0",
48
+ "commander": "^15.0.0",
47
49
  "cross-fetch": "^4.1.0",
50
+ "js-yaml": "^5.4.3",
48
51
  "jszip": "^3.10.1",
52
+ "quickjs-emscripten": "^0.32.0",
49
53
  "sha.js": "^2.4.12"
50
54
  },
51
55
  "devDependencies": {
package/product.yml CHANGED
@@ -1,7 +1,7 @@
1
1
  # Identity
2
2
  name: "Triflare Fx"
3
3
  command: "fx"
4
- version: "1.0.3"
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/brand.js CHANGED
@@ -62,9 +62,10 @@ const HEX_COLOR = /^#(?:[\da-f]{3}|[\da-f]{6})$/i;
62
62
  // styleText only learned hex colors in Node 24.16, and engines allow any 22, so
63
63
  // the truecolor escape is built here. The gate is styleText's own: ask it to
64
64
  // color a marker, and stay plain when it comes back untouched.
65
- function colorize(text, color, stream) {
65
+ function colorize(text, color, stream, force = false) {
66
66
  if (!HEX_COLOR.test(color)) return styleText(color, text, { stream });
67
- if (!styleText("red", "\u0000", { stream }).includes("\u001b")) return text;
67
+ if (!force && !styleText("red", "\u0000", { stream }).includes("\u001b"))
68
+ return text;
68
69
  const digits =
69
70
  color.length === 4 ? color.slice(1).replace(/./g, "$&$&") : color.slice(1);
70
71
  const [red, green, blue] = digits
@@ -76,7 +77,11 @@ function colorize(text, color, stream) {
76
77
  // Paints a product name or command in three equal bands taken straight from the
77
78
  // logo palette. The colors disappear when the stream can't render them, so
78
79
  // piped output and CI logs stay plain text.
79
- export function brandText(text, stream = process.stdout) {
80
+ export function brandText(
81
+ text,
82
+ stream = process.stdout,
83
+ { force = false } = {},
84
+ ) {
80
85
  const { primary, secondary, tertiary } = brandingColors();
81
86
  const palette = [primary, secondary, tertiary];
82
87
  const chars = Array.from(text);
@@ -96,6 +101,7 @@ export function brandText(text, stream = process.stdout) {
96
101
  chars.slice(start, index).join(""),
97
102
  palette[bandFor(start)],
98
103
  stream,
104
+ force,
99
105
  ),
100
106
  );
101
107
  start = index;
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,98 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { build } from "../../build.js";
4
+ import { outputDirectory, sourceExtension } from "../../brand.js";
5
+ import { slugify } from "../../package.js";
6
+ import { loadProjectState } from "../context.js";
7
+ import { formatDuration } from "../format.js";
8
+
9
+ const EXTENSION = sourceExtension();
10
+
11
+ export const buildCommand = {
12
+ name: "build",
13
+ aliases: ["b"],
14
+ group: "Core Commands:",
15
+ summary: "Compile sources into a .pmp",
16
+ description: `Compile a source file, or every ${EXTENSION} file in a directory
17
+ (defaults to the working directory), into one .pmp archive.`,
18
+ arguments: [`[input${EXTENSION}|dir]`],
19
+ options: [{ flags: "-o, --output <file>", description: "Output .pmp path" }],
20
+ examples: [
21
+ "fx build",
22
+ "fx build src/main.tfx -o build/game.pmp",
23
+ "fx build examples/player --no-config",
24
+ ],
25
+ async run({ args, options, cwd, reporter, prompts }) {
26
+ const started = performance.now();
27
+ const state = await loadProjectState({
28
+ input: args[0],
29
+ cwd,
30
+ options,
31
+ reporter,
32
+ prompts,
33
+ });
34
+ const { target, loaded, catalog, extensionUrls } = state;
35
+
36
+ const result = build({
37
+ ...target,
38
+ projectConfig: loaded.project,
39
+ fxConfig: loaded.fx,
40
+ catalog,
41
+ extensionUrls,
42
+ useConfigFile: false,
43
+ });
44
+ for (const warning of result.warnings) reporter.warn(warning);
45
+
46
+ const explicit = options.output;
47
+ let output;
48
+ if (explicit) {
49
+ output = path.resolve(cwd, explicit);
50
+ } else if (target.file) {
51
+ output = target.file.toLowerCase().endsWith(EXTENSION.toLowerCase())
52
+ ? `${target.file.slice(0, -EXTENSION.length)}.pmp`
53
+ : `${target.file}.pmp`;
54
+ } else {
55
+ output = path.join(
56
+ target.dir,
57
+ outputDirectory(),
58
+ `${slugify(result.config.name || path.basename(target.dir))}.pmp`,
59
+ );
60
+ }
61
+ fs.mkdirSync(path.dirname(path.resolve(output)), { recursive: true });
62
+ fs.writeFileSync(output, result.sb3);
63
+
64
+ const blockCount = result.project.targets.reduce(
65
+ (sum, item) => sum + Object.keys(item.blocks).length,
66
+ 0,
67
+ );
68
+ const durationMs = Math.round(performance.now() - started);
69
+
70
+ reporter.success(`Built ${output}`);
71
+ reporter.text("");
72
+ reporter.detail("Sources", `${result.files.length} file(s)`);
73
+ reporter.detail("Targets", result.project.targets.length);
74
+ reporter.detail("Blocks", blockCount);
75
+ reporter.detail("Assets", result.assets.size);
76
+ reporter.detail(
77
+ "Extensions",
78
+ result.project.extensions.join(", ") || "(none)",
79
+ );
80
+ reporter.detail("Time", formatDuration(durationMs));
81
+ if (state.unsandboxed.length)
82
+ reporter.detail(
83
+ "Unsandboxed",
84
+ `${state.unsandboxed.join(", ")} (must be allowed in the editor)`,
85
+ );
86
+
87
+ return {
88
+ output,
89
+ sources: result.files,
90
+ targets: result.project.targets.length,
91
+ blocks: blockCount,
92
+ assets: result.assets.size,
93
+ extensions: result.project.extensions,
94
+ unsandboxed: state.unsandboxed,
95
+ durationMs,
96
+ };
97
+ },
98
+ };
@@ -0,0 +1,42 @@
1
+ import { CATALOG, blockNames } from "../../catalog.js";
2
+ import { loadProjectState } from "../context.js";
3
+
4
+ export const catalogCommand = {
5
+ name: "catalog",
6
+ aliases: ["l", "blocks"],
7
+ group: "Core Commands:",
8
+ summary: "List available blocks",
9
+ description: `List block names, including blocks from extensions configured in the
10
+ project.`,
11
+ arguments: ["[dir]"],
12
+ examples: ["fx catalog", "fx catalog --json"],
13
+ async run({ args, cwd, options, reporter, product, prompts }) {
14
+ const state = await loadProjectState({
15
+ input: args[0],
16
+ cwd,
17
+ options,
18
+ reporter,
19
+ prompts,
20
+ });
21
+ const merged = { ...CATALOG, ...state.catalog };
22
+ const names = Object.keys(merged).sort();
23
+ const builtIn = blockNames().length;
24
+
25
+ reporter.heading(
26
+ `${product.name} catalog (${names.length} blocks` +
27
+ (names.length > builtIn
28
+ ? `, ${names.length - builtIn} from extensions`
29
+ : "") +
30
+ "):",
31
+ );
32
+ for (const name of names) {
33
+ const entry = merged[name];
34
+ reporter.item(name, `-> ${entry.opcode} [${entry.kind}]`, {
35
+ width: 32,
36
+ suffix: entry.extension ? `(ext ${entry.extension})` : "",
37
+ });
38
+ }
39
+
40
+ return merged;
41
+ },
42
+ };
@@ -0,0 +1,44 @@
1
+ import fs from "node:fs";
2
+ import { parse } from "../../parser.js";
3
+ import { discoverSources, mergePrograms } from "../../project.js";
4
+ import { sourceExtension } from "../../brand.js";
5
+ import { loadProjectState } from "../context.js";
6
+ import { UsageError } from "../errors.js";
7
+
8
+ const EXTENSION = sourceExtension();
9
+
10
+ export const checkCommand = {
11
+ name: "check",
12
+ aliases: ["c"],
13
+ group: "Core Commands:",
14
+ summary: "Parse sources and report errors",
15
+ description: `Parse a file, or every ${EXTENSION} file in a directory, without
16
+ building an archive.`,
17
+ arguments: [`[input${EXTENSION}|dir]`],
18
+ examples: ["fx check", "fx check src/main.tfx"],
19
+ async run({ args, cwd, options, reporter }) {
20
+ const state = await loadProjectState({
21
+ input: args[0],
22
+ cwd,
23
+ options,
24
+ reporter,
25
+ includeExtensions: false,
26
+ migrateLegacy: false,
27
+ });
28
+ const { target, loaded } = state;
29
+ const files = target.file ? [target.file] : discoverSources(target.dir);
30
+ if (files.length === 0)
31
+ throw new UsageError(`No ${EXTENSION} files found under ${target.dir}`);
32
+
33
+ const entries = files.map((file) => ({
34
+ ast: parse(fs.readFileSync(file, "utf8"), file),
35
+ file,
36
+ }));
37
+ mergePrograms(entries, {
38
+ partialSprites: loaded.fx?.partialSprites ?? false,
39
+ });
40
+
41
+ reporter.success(`Checked ${files.length} file(s): OK`);
42
+ return { files, ok: true };
43
+ },
44
+ };
@@ -0,0 +1,54 @@
1
+ import { completionCandidates } from "../completion/index.js";
2
+ import { bashCompletion } from "../completion/bash.js";
3
+ import { fishCompletion } from "../completion/fish.js";
4
+ import { zshCompletion } from "../completion/zsh.js";
5
+ import { UsageError } from "../errors.js";
6
+
7
+ const SHELLS = new Set(["bash", "zsh", "fish"]);
8
+
9
+ export const completionCommand = {
10
+ name: "completion",
11
+ group: "Utility Commands:",
12
+ summary: "Print shell completion setup",
13
+ description: `Print a completion script for bash, zsh, or fish. Add the script
14
+ to your shell profile, or source it directly.`,
15
+ arguments: ["<shell>"],
16
+ examples: [
17
+ "fx completion bash >> ~/.bashrc",
18
+ "fx completion zsh > ~/.zfunc/_fx",
19
+ "fx completion fish > ~/.config/fish/completions/fx.fish",
20
+ ],
21
+ run({ args, reporter, product }) {
22
+ const shell = args[0];
23
+ if (!SHELLS.has(shell))
24
+ throw new UsageError(
25
+ `completion requires one of: ${[...SHELLS].join(", ")}`,
26
+ );
27
+ const command = product.command;
28
+ const script =
29
+ shell === "bash"
30
+ ? bashCompletion(command)
31
+ : shell === "zsh"
32
+ ? zshCompletion(command)
33
+ : fishCompletion(command);
34
+ reporter.text(script.endsWith("\n") ? script.slice(0, -1) : script);
35
+ return { shell, script };
36
+ },
37
+ };
38
+
39
+ export const completeCommand = {
40
+ name: "__complete",
41
+ hidden: true,
42
+ summary: "Internal shell completion protocol",
43
+ arguments: ["[words...]"],
44
+ run({ args, command, reporter }) {
45
+ const words = args[0] == null ? [] : args[0];
46
+ const list = Array.isArray(words) ? words : [words];
47
+ const program = command.parent ?? command;
48
+ // The generated shell scripts pass their own name first as a marker.
49
+ const input = SHELLS.has(list[0]) ? list.slice(1) : list;
50
+ const candidates = completionCandidates(program, input);
51
+ if (candidates.length > 0) reporter.text(candidates.join("\n"));
52
+ return { candidates };
53
+ },
54
+ };