@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
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Scaffolding templates used by `fx init`.
3
+ */
4
+
5
+ import {
6
+ configFilename,
7
+ sourceDirectory,
8
+ sourceExtension,
9
+ } from "../../brand.js";
10
+
11
+ const CAT_ASSET = `<svg xmlns="http://www.w3.org/2000/svg" width="95" height="101" viewBox="0 0 95 101">
12
+ <ellipse cx="47" cy="60" rx="35" ry="35" fill="#ffab19" stroke="#cf8b17" stroke-width="4"/>
13
+ <circle cx="35" cy="50" r="6" fill="#000"/>
14
+ <circle cx="60" cy="50" r="6" fill="#000"/>
15
+ </svg>
16
+ `;
17
+
18
+ function configText({ name, width, height, target }) {
19
+ const lines = [
20
+ "project:",
21
+ ` name: ${JSON.stringify(name)}`,
22
+ " fps: 60",
23
+ ` width: ${width}`,
24
+ ` height: ${height}`,
25
+ " turbo: false",
26
+ " interpolation: true",
27
+ "fx:",
28
+ " partialSprites: false",
29
+ ];
30
+ if (target !== "none") {
31
+ lines.push("package:", ` target: ${target}`);
32
+ }
33
+ return `${lines.join("\n")}\n`;
34
+ }
35
+
36
+ const CAT_SOURCE = `sprite "Cat" {
37
+ costumes: {
38
+ cat: "../assets/cat.svg"
39
+ };
40
+
41
+ let score = 0;
42
+
43
+ when greenFlag {
44
+ motion.goToXY(0, 0);
45
+ control.forever {
46
+ motion.turnRight(1);
47
+ }
48
+ }
49
+
50
+ when keyPressed("space") {
51
+ score += 1;
52
+ looks.sayForSecs(operators.join("Score: ", score), 1);
53
+ }
54
+ }
55
+ `;
56
+
57
+ const EMPTY_SOURCE = `// Your project starts here.
58
+ when greenFlag {
59
+ looks.say("Hello, world!");
60
+ }
61
+ `;
62
+
63
+ const BLANK_SOURCE = `// Your project starts here.
64
+ `;
65
+
66
+ /**
67
+ * Build the file map for an init run.
68
+ *
69
+ * @param {{
70
+ * product: object,
71
+ * name: string,
72
+ * template: "cat"|"empty"|"blank",
73
+ * width: number,
74
+ * height: number,
75
+ * target: string,
76
+ * }} options
77
+ */
78
+ export function initFiles({ product, name, template, width, height, target }) {
79
+ const files = {
80
+ [configFilename(product)]: configText({ name, width, height, target }),
81
+ [`${sourceDirectory(product)}/main${sourceExtension(product)}`]:
82
+ template === "cat"
83
+ ? CAT_SOURCE
84
+ : template === "empty"
85
+ ? EMPTY_SOURCE
86
+ : BLANK_SOURCE,
87
+ };
88
+ if (template === "cat") files["assets/cat.svg"] = CAT_ASSET;
89
+ return files;
90
+ }
91
+
92
+ export const TEMPLATES = [
93
+ {
94
+ value: "cat",
95
+ label: "Cat demo",
96
+ hint: "sprite, costumes, events, variables",
97
+ },
98
+ { value: "empty", label: "Empty project", hint: "one green-flag script" },
99
+ { value: "blank", label: "Blank", hint: "just the config and a source file" },
100
+ ];
101
+
102
+ export const TARGETS = [
103
+ { value: "none", label: "None", hint: "source only" },
104
+ { value: "html", label: "HTML", hint: "single-file web build" },
105
+ { value: "zip", label: "ZIP", hint: "packaged archive" },
106
+ ];
107
+
108
+ export const STAGE_SIZES = [
109
+ { value: "480x360", label: "480 × 360", hint: "Scratch default" },
110
+ { value: "640x360", label: "640 × 360", hint: "wide" },
111
+ { value: "1920x1080", label: "1920 × 1080", hint: "full HD" },
112
+ ];
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Terminal theme shared by help output and the reporter.
3
+ *
4
+ * Colors are derived from the product palette so a rebrand reaches the CLI
5
+ * too. Everything falls back to plain text when the stream cannot show color.
6
+ */
7
+
8
+ import { styleText } from "node:util";
9
+ import { brandingColors } from "../brand.js";
10
+
11
+ const STATUS_COLORS = {
12
+ success: "#34D399",
13
+ warning: "#FBBF24",
14
+ danger: "#F87171",
15
+ };
16
+
17
+ function rgb(hex) {
18
+ const digits =
19
+ hex.length === 4 ? hex.slice(1).replace(/./g, "$&$&") : hex.slice(1);
20
+ return digits.match(/../g).map((part) => parseInt(part, 16));
21
+ }
22
+
23
+ function mix(from, to, amount) {
24
+ const [r1, g1, b1] = rgb(from);
25
+ const [r2, g2, b2] = rgb(to);
26
+ const channel = (a, b) => Math.round(a + (b - a) * amount);
27
+ return [channel(r1, r2), channel(g1, g2), channel(b1, b2)];
28
+ }
29
+
30
+ function supportsColor(stream) {
31
+ if (!stream?.isTTY) return false;
32
+ if (process.env.NO_COLOR) return false;
33
+ if (process.env.TERM === "dumb") return false;
34
+ return styleText("red", "x", { stream }).includes("\x1b");
35
+ }
36
+
37
+ /**
38
+ * @param {{ product?: object, color?: "auto"|"always"|"never", stream?: NodeJS.WriteStream }} [options]
39
+ */
40
+ export function createTheme({
41
+ product,
42
+ color = "auto",
43
+ stream = process.stdout,
44
+ } = {}) {
45
+ const palette = brandingColors(product);
46
+ const enabled =
47
+ color === "always" || (color !== "never" && supportsColor(stream));
48
+
49
+ const paint = (text, codes) => {
50
+ if (!enabled || codes.length === 0) return String(text);
51
+ return `\x1b[${codes.join(";")}m${text}\x1b[0m`;
52
+ };
53
+
54
+ const foreground = (colorValue) => {
55
+ const [r, g, b] = Array.isArray(colorValue) ? colorValue : rgb(colorValue);
56
+ return `38;2;${r};${g};${b}`;
57
+ };
58
+
59
+ const command = mix(palette.primary, "#FFFFFF", 0.35);
60
+ const heading = mix(palette.primary, "#FFFFFF", 0.2);
61
+ const option = mix(palette.primary, "#FFFFFF", 0.58);
62
+ const argument = mix(palette.primary, "#FFFFFF", 0.74);
63
+
64
+ return {
65
+ enabled,
66
+ stream,
67
+
68
+ accent: (text) => paint(text, [foreground(palette.primary)]),
69
+ command: (text) => paint(text, ["1", foreground(command)]),
70
+ heading: (text) => paint(text, ["1", foreground(heading)]),
71
+ option: (text) => paint(text, [foreground(option)]),
72
+ argument: (text) => paint(text, [foreground(argument)]),
73
+ muted: (text) => paint(text, ["2"]),
74
+ bold: (text) => paint(text, ["1"]),
75
+ success: (text) => paint(text, [foreground(STATUS_COLORS.success)]),
76
+ warning: (text) => paint(text, [foreground(STATUS_COLORS.warning)]),
77
+ danger: (text) => paint(text, [foreground(STATUS_COLORS.danger)]),
78
+
79
+ tone(name, text) {
80
+ const value = STATUS_COLORS[name] ?? palette.primary;
81
+ return paint(text, [foreground(value)]);
82
+ },
83
+ };
84
+ }
package/src/compiler.js CHANGED
@@ -137,6 +137,7 @@ export class Compiler {
137
137
  this.loadAsset = loadAsset || null;
138
138
  this.catalog = catalog ? { ...CATALOG, ...catalog } : CATALOG;
139
139
  this.extensionUrls = extensionUrls;
140
+ this.requiredExtensions = new Map();
140
141
  this.file = file;
141
142
  /** @type {string[]} Non-fatal problems worth surfacing to the user. */
142
143
  this.warnings = [];
@@ -252,6 +253,8 @@ export class Compiler {
252
253
  // can only be given their mutation once every stack is complete.
253
254
  for (const ctx of contexts) this.applyDynamicNextMutations(ctx);
254
255
 
256
+ this.validateRequiredExtensions();
257
+
255
258
  return { project: this.assembleProject(contexts), assets: this.assets };
256
259
  }
257
260
 
@@ -335,6 +338,11 @@ export class Compiler {
335
338
  }
336
339
 
337
340
  declareTargetVar(ctx, decl) {
341
+ if (decl.name.startsWith("$"))
342
+ this.fail(
343
+ `Names starting with "$" are reserved for temporary variables; rename ${JSON.stringify(decl.name)}.`,
344
+ decl.loc,
345
+ );
338
346
  const global = ctx.isStage;
339
347
  if (global && this.globals.has(decl.name)) {
340
348
  const first = this.globals.get(decl.name);
@@ -380,6 +388,11 @@ export class Compiler {
380
388
  }
381
389
 
382
390
  declareTargetList(ctx, decl) {
391
+ if (decl.name.startsWith("$"))
392
+ this.fail(
393
+ `Names starting with "$" are reserved for temporary variables; rename ${JSON.stringify(decl.name)}.`,
394
+ decl.loc,
395
+ );
383
396
  if (ctx.isStage && this.globals.has(decl.name)) {
384
397
  const first = this.globals.get(decl.name);
385
398
  const where = this.where(first.loc);
@@ -421,6 +434,13 @@ export class Compiler {
421
434
  decl.loc,
422
435
  );
423
436
  }
437
+ for (const param of decl.params) {
438
+ if (param.name.startsWith("$"))
439
+ this.fail(
440
+ `Names starting with "$" are reserved for temporary variables; rename parameter ${JSON.stringify(param.name)}.`,
441
+ decl.loc,
442
+ );
443
+ }
424
444
  const argIds = decl.params.map((_, i) => `arg${i}`);
425
445
  const proccode = decl.params.length
426
446
  ? `${decl.name} ${decl.params.map((param) => (param.boolean ? "%b" : "%s")).join(" ")}`
@@ -785,6 +805,8 @@ export class Compiler {
785
805
  return this.compileForever(ctx, node);
786
806
  case "Return":
787
807
  return this.compileReturn(ctx, node);
808
+ case "TempVarDecl":
809
+ return this.compileTempVarDecl(ctx, node);
788
810
  default:
789
811
  this.fail(`Unsupported statement ${node.type}`, node.loc);
790
812
  }
@@ -824,6 +846,13 @@ export class Compiler {
824
846
  }
825
847
 
826
848
  compileAssign(ctx, node) {
849
+ const sigil = /^(\$\$?)([A-Za-z_][A-Za-z0-9_]*)$/.exec(node.name);
850
+ if (sigil) return this.compileSigilAssign(ctx, node, sigil);
851
+ if (node.name.startsWith("$"))
852
+ this.fail(
853
+ `Invalid temporary variable reference ${JSON.stringify(node.name)}`,
854
+ node.loc,
855
+ );
827
856
  const resolved = ctx.resolveValue(node.name);
828
857
  if (!resolved || resolved.kind !== "var") {
829
858
  this.fail(`Unknown variable ${JSON.stringify(node.name)}`, node.loc);
@@ -880,6 +909,64 @@ export class Compiler {
880
909
  return id;
881
910
  }
882
911
 
912
+ compileTempVarDecl(ctx, node) {
913
+ if (node.name.startsWith("$"))
914
+ this.fail(
915
+ 'Temporary variable names cannot start with "$"; use a plain name.',
916
+ node.loc,
917
+ );
918
+ const name =
919
+ node.kind === "runtime" ? "tempvars.setRuntime" : "tempvars.setThread";
920
+ const entry = this.catalog[name];
921
+ if (!entry)
922
+ this.fail(
923
+ "Temporary variables are not available in this Fx build",
924
+ node.loc,
925
+ );
926
+ return this.emitCatalogCall(
927
+ ctx,
928
+ {
929
+ name,
930
+ segments: name.split("."),
931
+ args: [
932
+ { type: "String", value: node.name, loc: node.loc },
933
+ node.init ?? { type: "String", value: "0", loc: node.loc },
934
+ ],
935
+ loc: node.loc,
936
+ },
937
+ entry,
938
+ null,
939
+ ).id;
940
+ }
941
+
942
+ compileSigilAssign(ctx, node, sigil) {
943
+ if (node.op !== "=" && node.op !== "+=")
944
+ this.fail(
945
+ `Temporary variables only support "=" and "+="; ${JSON.stringify(node.op)} is not available.`,
946
+ node.loc,
947
+ );
948
+ const kind = sigil[1] === "$$" ? "Runtime" : "Thread";
949
+ const action = node.op === "=" ? "set" : "change";
950
+ const name = `tempvars.${action}${kind}`;
951
+ const entry = this.catalog[name];
952
+ if (!entry)
953
+ this.fail(
954
+ "Temporary variables are not available in this Fx build",
955
+ node.loc,
956
+ );
957
+ return this.emitCatalogCall(
958
+ ctx,
959
+ {
960
+ name,
961
+ segments: name.split("."),
962
+ args: [{ type: "String", value: sigil[2], loc: node.loc }, node.value],
963
+ loc: node.loc,
964
+ },
965
+ entry,
966
+ null,
967
+ ).id;
968
+ }
969
+
883
970
  compileIf(ctx, node) {
884
971
  const id = this.addBlock(ctx, {
885
972
  opcode: node.else ? "control_if_else" : "control_if",
@@ -976,6 +1063,11 @@ export class Compiler {
976
1063
  emitCatalogCall(ctx, node, entry, parentId) {
977
1064
  if (entry.kind === "hat")
978
1065
  this.fail(`${node.name} is an event and cannot be called`, node.loc);
1066
+ if (entry.requiresExtension) {
1067
+ const id = entry.extension || extensionForOpcode(entry.opcode);
1068
+ if (id && !this.requiredExtensions.has(id))
1069
+ this.requiredExtensions.set(id, node.loc);
1070
+ }
979
1071
  const id = this.addBlock(ctx, { opcode: entry.opcode, parent: parentId });
980
1072
  const fieldNames = Object.keys(entry.fields || {});
981
1073
  const inputNames = Object.keys(entry.inputs || {});
@@ -1282,6 +1374,33 @@ export class Compiler {
1282
1374
  }
1283
1375
 
1284
1376
  emitIdentifier(ctx, node) {
1377
+ const sigil = /^(\$\$?)([A-Za-z_][A-Za-z0-9_]*)$/.exec(node.name);
1378
+ if (sigil) {
1379
+ const name = sigil[1] === "$$" ? "tempvars.runtime" : "tempvars.thread";
1380
+ const entry = this.catalog[name];
1381
+ if (!entry)
1382
+ this.fail(
1383
+ "Temporary variables are not available in this Fx build",
1384
+ node.loc,
1385
+ );
1386
+ const result = this.emitCatalogCall(
1387
+ ctx,
1388
+ {
1389
+ name,
1390
+ segments: name.split("."),
1391
+ args: [{ type: "String", value: sigil[2], loc: node.loc }],
1392
+ loc: node.loc,
1393
+ },
1394
+ entry,
1395
+ null,
1396
+ );
1397
+ return { k: "block", id: result.id, kind: "reporter" };
1398
+ }
1399
+ if (node.name.startsWith("$"))
1400
+ this.fail(
1401
+ `Invalid temporary variable reference ${JSON.stringify(node.name)}`,
1402
+ node.loc,
1403
+ );
1285
1404
  if (node.segments.length > 1)
1286
1405
  this.fail(
1287
1406
  `Unexpected dotted name ${JSON.stringify(node.name)}`,
@@ -1537,6 +1656,20 @@ export class Compiler {
1537
1656
 
1538
1657
  // ------------------------------------------------------------ assembly
1539
1658
 
1659
+ validateRequiredExtensions() {
1660
+ for (const [id, loc] of this.requiredExtensions) {
1661
+ if (this.extensionUrls[id]) continue;
1662
+ const feature = id === "fxTempVars" ? "temporaryvariables" : null;
1663
+ const hint = feature
1664
+ ? ` Run "fx feature ${feature} enable" to install it.`
1665
+ : "";
1666
+ this.fail(
1667
+ `This project uses blocks from extension ${JSON.stringify(id)}, which is not loaded.${hint}`,
1668
+ loc,
1669
+ );
1670
+ }
1671
+ }
1672
+
1540
1673
  assembleProject(contexts) {
1541
1674
  const targets = [];
1542
1675
  const extensions = new Set();