@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,64 @@
1
+ import path from "node:path";
2
+ import { LOCK_FILENAME, installVendor } from "../../../vendor.js";
3
+ import { UsageError } from "../../errors.js";
4
+ import { addVendor } from "./add.js";
5
+ import { loadVendorConfig, requireConfigPath } from "./helpers.js";
6
+
7
+ export const vendorInstallCommand = {
8
+ name: "install",
9
+ summary: "Restore vendored extensions from fx.lock",
10
+ description: `Fetch only the vendored files that are missing, verifying them
11
+ against fx.lock. Without a source, restores every configured entry.`,
12
+ arguments: ["[source]"],
13
+ options: [
14
+ { flags: "--as <name>", description: "Name for the vendored extension" },
15
+ {
16
+ flags: "--unsandboxed",
17
+ description: "Allow the extension to run unsandboxed",
18
+ },
19
+ { flags: "--force", description: "Replace an existing entry with the name" },
20
+ ],
21
+ examples: ["fx vendor install", "fx vendor add https://example.com/ext.js"],
22
+ async run(context) {
23
+ const { args, options, cwd, reporter, product } = context;
24
+ const source = args[0];
25
+ if (source) {
26
+ reporter.warn(
27
+ '"vendor install <source>" is deprecated; use "vendor add <source>".',
28
+ );
29
+ return addVendor({ source, options, cwd, reporter, product });
30
+ }
31
+
32
+ const configPath = requireConfigPath(cwd, product);
33
+ const configDir = path.dirname(configPath);
34
+ const loaded = loadVendorConfig(configPath);
35
+ if (loaded.vendor.length === 0)
36
+ throw new UsageError(
37
+ `No vendor entries in ${path.basename(configPath)}.`,
38
+ {
39
+ hint: `add one with "${product.command} vendor add <url|path>"`,
40
+ },
41
+ );
42
+
43
+ const result = await installVendor({
44
+ configPath,
45
+ configDir,
46
+ config: loaded,
47
+ });
48
+ for (const warning of result.warnings) reporter.warn(warning);
49
+ for (const entry of result.entries)
50
+ reporter.success(`${entry.name}: ${entry.status} (${entry.file})`);
51
+ reporter.success(
52
+ `Installed ${result.entries.length} extension(s) from ${LOCK_FILENAME}`,
53
+ );
54
+
55
+ return {
56
+ lock: LOCK_FILENAME,
57
+ entries: result.entries.map(({ name, status, file }) => ({
58
+ name,
59
+ status,
60
+ file,
61
+ })),
62
+ };
63
+ },
64
+ };
@@ -0,0 +1,45 @@
1
+ import path from "node:path";
2
+ import { readLock } from "../../../vendor.js";
3
+ import {
4
+ loadVendorConfig,
5
+ requireConfigPath,
6
+ vendorEntriesWithLock,
7
+ } from "./helpers.js";
8
+
9
+ export const vendorListCommand = {
10
+ name: "list",
11
+ summary: "List vendored extensions",
12
+ description: "List every extension declared in the vendor config.",
13
+ examples: ["fx vendor list", "fx vendor list --json"],
14
+ run({ cwd, reporter, product }) {
15
+ const configPath = requireConfigPath(cwd, product);
16
+ const configDir = path.dirname(configPath);
17
+ const loaded = loadVendorConfig(configPath);
18
+ const lock = readLock(configDir);
19
+ const entries = vendorEntriesWithLock(loaded, lock, configDir);
20
+
21
+ reporter.heading(
22
+ `${path.basename(configPath)}: ${entries.length} vendored extension(s) in ${
23
+ loaded.vendorDir || "extensions"
24
+ }/`,
25
+ );
26
+ for (const entry of entries) {
27
+ const flags = [
28
+ entry.remote ? "remote" : entry.local ? "local" : "embedded",
29
+ entry.volatile ? "volatile" : null,
30
+ entry.unsandboxed ? "unsandboxed" : null,
31
+ ]
32
+ .filter(Boolean)
33
+ .join(", ");
34
+ reporter.item(entry.name, entry.file, {
35
+ width: 24,
36
+ suffix: flags ? `[${flags}]` : "",
37
+ });
38
+ }
39
+
40
+ return {
41
+ vendorDir: loaded.vendorDir || "extensions",
42
+ extensions: entries,
43
+ };
44
+ },
45
+ };
@@ -0,0 +1,47 @@
1
+ import path from "node:path";
2
+ import { migrateProjectExtensions } from "../../../vendor.js";
3
+ import { UsageError } from "../../errors.js";
4
+ import { loadVendorConfig, requireConfigPath } from "./helpers.js";
5
+
6
+ export const vendorMigrateCommand = {
7
+ name: "migrate",
8
+ summary: "Move project.extensions under vendor:",
9
+ description: `Move legacy project.extensions entries into the vendor system,
10
+ writing a .bak copy of the config first.`,
11
+ examples: ["fx vendor migrate", "fx vendor migrate --yes"],
12
+ async run({ cwd, reporter, prompts, yes, product }) {
13
+ const configPath = requireConfigPath(cwd, product);
14
+ const configDir = path.dirname(configPath);
15
+ const loaded = loadVendorConfig(configPath);
16
+ if (loaded.project.extensions.length === 0)
17
+ throw new UsageError(
18
+ `No project.extensions to migrate in ${path.basename(configPath)}`,
19
+ );
20
+
21
+ const interactive = prompts.enabled && !yes;
22
+ const result = await migrateProjectExtensions({
23
+ configPath,
24
+ configDir,
25
+ config: loaded,
26
+ nameFor: interactive
27
+ ? (name, source) =>
28
+ prompts.text({
29
+ message: `Name for ${source}`,
30
+ defaultValue: name,
31
+ })
32
+ : undefined,
33
+ });
34
+ for (const warning of result.warnings) reporter.warn(warning);
35
+ if (result.diff) reporter.text(result.diff);
36
+ reporter.success(`Backed up ${path.basename(configPath)} -> ${result.backup}`);
37
+ reporter.success(`Migrated ${result.migrated.length} extension(s) to vendor:`);
38
+ for (const entry of result.migrated)
39
+ reporter.item(entry.name, entry.file, { width: 24 });
40
+
41
+ return {
42
+ backup: result.backup,
43
+ migrated: result.migrated.map(({ name, file }) => ({ name, file })),
44
+ warnings: result.warnings,
45
+ };
46
+ },
47
+ };
@@ -0,0 +1,51 @@
1
+ import path from "node:path";
2
+ import { outdatedVendor } from "../../../vendor.js";
3
+ import { UsageError } from "../../errors.js";
4
+ import { outcome } from "../../define-command.js";
5
+ import { loadVendorConfig, requireConfigPath } from "./helpers.js";
6
+
7
+ export const vendorOutdatedCommand = {
8
+ name: "outdated",
9
+ summary: "Compare upstream sources with fx.lock",
10
+ description: "Check whether any vendored extension changed upstream.",
11
+ examples: ["fx vendor outdated"],
12
+ async run({ cwd, reporter, product }) {
13
+ const configPath = requireConfigPath(cwd, product);
14
+ const configDir = path.dirname(configPath);
15
+ const loaded = loadVendorConfig(configPath);
16
+ if (loaded.vendor.length === 0)
17
+ throw new UsageError(`No vendor entries in ${path.basename(configPath)}`);
18
+
19
+ const results = await outdatedVendor({ configDir, config: loaded });
20
+ let outdated = 0;
21
+ let errors = 0;
22
+ for (const result of results) {
23
+ if (result.status === "outdated") outdated += 1;
24
+ if (result.status === "error") errors += 1;
25
+ const label =
26
+ result.status === "outdated"
27
+ ? "outdated"
28
+ : result.status === "error"
29
+ ? "error"
30
+ : "up to date";
31
+ const tone =
32
+ result.status === "outdated"
33
+ ? "warning"
34
+ : result.status === "error"
35
+ ? "danger"
36
+ : "success";
37
+ reporter.item(result.name, reporter.tone(tone, label), { width: 24 });
38
+ if (result.error) reporter.warn(result.error);
39
+ }
40
+ if (outdated > 0)
41
+ reporter.warn(
42
+ `${outdated} extension(s) changed upstream; run "${product.command} vendor update"`,
43
+ );
44
+ else if (errors === 0) reporter.success("Everything is up to date");
45
+
46
+ return outcome({
47
+ data: results,
48
+ exitCode: errors > 0 ? 1 : 0,
49
+ });
50
+ },
51
+ };
@@ -0,0 +1,34 @@
1
+ import path from "node:path";
2
+ import { removeVendoredExtension } from "../../../vendor.js";
3
+ import { loadVendorConfig, requireConfigPath } from "./helpers.js";
4
+
5
+ export const vendorRemoveCommand = {
6
+ name: "remove",
7
+ summary: "Remove a vendored extension",
8
+ description:
9
+ "Remove an extension from the project config, fx.lock, and the vendor directory.",
10
+ arguments: ["<name>"],
11
+ examples: ["fx vendor remove signals"],
12
+ async run({ args, cwd, reporter, prompts, yes, product }) {
13
+ const name = args[0];
14
+ const configPath = requireConfigPath(cwd, product);
15
+ const configDir = path.dirname(configPath);
16
+ const loaded = loadVendorConfig(configPath);
17
+
18
+ await prompts.confirmDestructive({
19
+ message: `Remove vendored extension ${JSON.stringify(name)}?`,
20
+ yes,
21
+ });
22
+
23
+ const result = removeVendoredExtension({
24
+ configPath,
25
+ configDir,
26
+ config: loaded,
27
+ name,
28
+ });
29
+ reporter.success(
30
+ `Removed ${name} from ${path.basename(configPath)}, fx.lock and ${result.file}`,
31
+ );
32
+ return { name, file: result.file };
33
+ },
34
+ };
@@ -0,0 +1,49 @@
1
+ import path from "node:path";
2
+ import { updateVendor } from "../../../vendor.js";
3
+ import { UsageError } from "../../errors.js";
4
+ import { loadVendorConfig, requireConfigPath } from "./helpers.js";
5
+
6
+ export const vendorUpdateCommand = {
7
+ name: "update",
8
+ summary: "Update vendored extensions and show diffs",
9
+ description: `Re-fetch upstream sources, update the vendored copies, and show a
10
+ diff for each change. Use --dry-run to preview without writing.`,
11
+ arguments: ["[name]"],
12
+ options: [
13
+ { flags: "--dry-run", description: "Show diffs without writing files" },
14
+ ],
15
+ examples: ["fx vendor update", "fx vendor update signals --dry-run"],
16
+ async run({ args, options, cwd, reporter, product }) {
17
+ const configPath = requireConfigPath(cwd, product);
18
+ const configDir = path.dirname(configPath);
19
+ const loaded = loadVendorConfig(configPath);
20
+ if (loaded.vendor.length === 0)
21
+ throw new UsageError(`No vendor entries in ${path.basename(configPath)}`);
22
+
23
+ const result = await updateVendor({
24
+ configPath,
25
+ configDir,
26
+ config: loaded,
27
+ name: args[0],
28
+ write: !options.dryRun,
29
+ });
30
+ for (const entry of result.entries) {
31
+ if (!entry.changed) {
32
+ reporter.info(`${entry.name} is up to date`);
33
+ continue;
34
+ }
35
+ reporter.text(entry.diff);
36
+ }
37
+ if (options.dryRun && result.changed > 0)
38
+ reporter.info(`Dry run: ${result.changed} extension(s) would change`);
39
+ else if (result.changed > 0)
40
+ reporter.success(`Updated ${result.changed} extension(s)`);
41
+ else reporter.success("Everything is up to date");
42
+
43
+ return {
44
+ dryRun: Boolean(options.dryRun),
45
+ changed: result.changed,
46
+ entries: result.entries.map(({ name, changed }) => ({ name, changed })),
47
+ };
48
+ },
49
+ };
@@ -0,0 +1,44 @@
1
+ import path from "node:path";
2
+ import { LOCK_FILENAME, verifyVendor } from "../../../vendor.js";
3
+ import { UsageError } from "../../errors.js";
4
+ import { outcome } from "../../define-command.js";
5
+ import { loadVendorConfig, requireConfigPath } from "./helpers.js";
6
+
7
+ export const vendorVerifyCommand = {
8
+ name: "verify",
9
+ summary: "Check vendored files against fx.lock offline",
10
+ description:
11
+ "Verify every vendored extension without making network requests.",
12
+ examples: ["fx vendor verify"],
13
+ run({ cwd, reporter, product }) {
14
+ const configPath = requireConfigPath(cwd, product);
15
+ const configDir = path.dirname(configPath);
16
+ const loaded = loadVendorConfig(configPath);
17
+ if (loaded.vendor.length === 0)
18
+ throw new UsageError(`No vendor entries in ${path.basename(configPath)}`);
19
+
20
+ const result = verifyVendor({ configDir, config: loaded });
21
+ for (const warning of result.warnings) reporter.warn(warning);
22
+ for (const entry of result.entries)
23
+ if (entry.status === "ok")
24
+ reporter.item(entry.name, entry.file, { width: 24 });
25
+
26
+ if (!result.ok) {
27
+ for (const problem of result.problems) reporter.error(problem);
28
+ return outcome({
29
+ exitCode: 1,
30
+ data: { entries: result.entries, problems: result.problems },
31
+ error: {
32
+ code: "VENDOR_VERIFY_FAILED",
33
+ message: `Vendor verification failed for ${result.problems.length} extension(s)`,
34
+ details: { problems: result.problems },
35
+ },
36
+ });
37
+ }
38
+
39
+ reporter.success(
40
+ `Verified ${result.entries.length} extension(s) against ${LOCK_FILENAME}`,
41
+ );
42
+ return { entries: result.entries };
43
+ },
44
+ };
@@ -0,0 +1,10 @@
1
+ /** Generate a bash completion script. */
2
+ export function bashCompletion(command) {
3
+ return `# bash completion for ${command}
4
+ _${command}_complete() {
5
+ local IFS=$'\\n'
6
+ COMPREPLY=( $("${command}" __complete bash -- "\${COMP_WORDS[@]:1}") )
7
+ }
8
+ complete -o bashdefault -o default -F _${command}_complete ${command}
9
+ `;
10
+ }
@@ -0,0 +1,9 @@
1
+ /** Generate a fish completion script. */
2
+ export function fishCompletion(command) {
3
+ return `# fish completion for ${command}
4
+ function __${command}_complete
5
+ ${command} __complete fish -- (commandline -opc)[2..-1]
6
+ end
7
+ complete -c ${command} -f -a '(__${command}_complete)'
8
+ `;
9
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Shell completion support.
3
+ *
4
+ * Completion is intentionally data-driven: the hidden `__complete` command
5
+ * walks the live commander tree, so new commands and options are completed
6
+ * without maintaining a second list.
7
+ */
8
+
9
+ const VALUE_OPTIONS = {
10
+ "--color": ["auto", "always", "never"],
11
+ "--cwd": [],
12
+ "--config": [],
13
+ "--template": ["cat", "empty", "blank"],
14
+ "--target": ["html", "zip", "windows", "macos", "linux"],
15
+ "--size": ["480x360", "640x360", "1920x1080"],
16
+ };
17
+
18
+ function commandFor(program, words) {
19
+ let command = program;
20
+ for (const word of words) {
21
+ if (word.startsWith("-")) continue;
22
+ const next = command.commands.find(
23
+ (entry) => entry.name() === word || entry.aliases().includes(word),
24
+ );
25
+ if (!next) break;
26
+ command = next;
27
+ }
28
+ return command;
29
+ }
30
+
31
+ function commandWords(command) {
32
+ const words = new Set();
33
+ for (const entry of command.commands) {
34
+ if (entry._hidden) continue;
35
+ words.add(entry.name());
36
+ for (const alias of entry.aliases()) words.add(alias);
37
+ }
38
+ return [...words];
39
+ }
40
+
41
+ function optionWords(command) {
42
+ const words = new Set();
43
+ for (let current = command; current; current = current.parent) {
44
+ for (const option of current.options) {
45
+ if (option.hidden) continue;
46
+ if (option.long) words.add(option.long);
47
+ if (option.short) words.add(option.short);
48
+ }
49
+ }
50
+ return [...words];
51
+ }
52
+
53
+ /** Candidate completions for the words typed after the command name. */
54
+ export function completionCandidates(program, words) {
55
+ const current = words.at(-1) ?? "";
56
+ const before = words.slice(0, -1).filter((word) => word !== "");
57
+ const command = commandFor(program, before);
58
+
59
+ // `--color=` style value completion.
60
+ if (current.includes("=")) {
61
+ const [flag, prefix] = current.split("=", 2);
62
+ const values = VALUE_OPTIONS[flag] ?? [];
63
+ return values
64
+ .filter((value) => value.startsWith(prefix))
65
+ .map((value) => `${flag}=${value}`);
66
+ }
67
+
68
+ // A value is expected for the previous token.
69
+ const previous = before.at(-1);
70
+ if (previous && Object.hasOwn(VALUE_OPTIONS, previous)) {
71
+ const values = VALUE_OPTIONS[previous];
72
+ return values.filter((value) => value.startsWith(current));
73
+ }
74
+
75
+ const candidates = new Set();
76
+ if (current.startsWith("-")) {
77
+ for (const option of optionWords(command)) {
78
+ if (option.startsWith(current)) candidates.add(option);
79
+ }
80
+ return [...candidates].sort();
81
+ }
82
+
83
+ // Groups with subcommands (the root, vendor, sprite, ...) offer their
84
+ // command words; leaf commands have none.
85
+ if (command.commands.length > 0) {
86
+ for (const word of commandWords(command))
87
+ if (word.startsWith(current)) candidates.add(word);
88
+ }
89
+
90
+ // Positional values that are useful and finite.
91
+ if (command.name() === "completion") {
92
+ for (const shell of ["bash", "zsh", "fish"])
93
+ if (shell.startsWith(current)) candidates.add(shell);
94
+ }
95
+
96
+ return [...candidates].sort();
97
+ }
@@ -0,0 +1,11 @@
1
+ /** Generate a zsh completion script. */
2
+ export function zshCompletion(command) {
3
+ return `#compdef ${command}
4
+ _${command}_complete() {
5
+ local -a completions
6
+ completions=("\${(@f)$("${command}" __complete zsh -- "\${words[@]:1}")}")
7
+ _describe '${command}' completions
8
+ }
9
+ compdef _${command}_complete ${command}
10
+ `;
11
+ }