@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,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
+ };
@@ -0,0 +1,45 @@
1
+ import { completeCommand, completionCommand } from "./completion.js";
2
+ import { buildCommand } from "./build.js";
3
+ import { catalogCommand } from "./catalog.js";
4
+ import { checkCommand } from "./check.js";
5
+ import { decompileCommand } from "./decompile.js";
6
+ import { devCommand } from "./dev.js";
7
+ import { registerExtension } from "./extension.js";
8
+ import { featureCommand } from "./feature.js";
9
+ import { fmtCommand } from "./fmt.js";
10
+ import { initCommand } from "./init.js";
11
+ import { lintCommand } from "./lint.js";
12
+ import { packageCommand } from "./package.js";
13
+ import { registerSprite } from "./sprite/index.js";
14
+ import { registerVendor } from "./vendor/index.js";
15
+ import { defineCommand } from "../define-command.js";
16
+
17
+ const TOP_LEVEL = [
18
+ buildCommand,
19
+ devCommand,
20
+ packageCommand,
21
+ decompileCommand,
22
+ checkCommand,
23
+ lintCommand,
24
+ fmtCommand,
25
+ catalogCommand,
26
+ featureCommand,
27
+ initCommand,
28
+ completionCommand,
29
+ ];
30
+
31
+ /**
32
+ * Register every command on the root program.
33
+ *
34
+ * @param {import("commander").Command} program
35
+ * @param {{state: object, product: object}} options
36
+ */
37
+ export function registerCommands(program, options) {
38
+ program.commandsGroup("Core Commands:");
39
+ for (const spec of TOP_LEVEL) defineCommand(program, spec, options);
40
+ registerVendor(program, options);
41
+ registerSprite(program, options);
42
+ defineCommand(program, completeCommand, options);
43
+ registerExtension(program, options);
44
+ return program;
45
+ }
@@ -0,0 +1,169 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { normalizePackageTarget } from "../../config.js";
4
+ import { UsageError } from "../errors.js";
5
+ import {
6
+ initFiles,
7
+ STAGE_SIZES,
8
+ TARGETS,
9
+ TEMPLATES,
10
+ } from "../templates/index.js";
11
+
12
+ const TEMPLATE_VALUES = new Set(TEMPLATES.map((entry) => entry.value));
13
+ const TARGET_VALUES = new Set(TARGETS.map((entry) => entry.value));
14
+
15
+ function parseSize(value) {
16
+ const match = /^(\d+)x(\d+)$/i.exec(String(value).trim());
17
+ if (!match)
18
+ throw new UsageError(
19
+ `--size must look like 480x360, received ${JSON.stringify(value)}`,
20
+ );
21
+ return { width: Number(match[1]), height: Number(match[2]) };
22
+ }
23
+
24
+ function sizeValue(width, height) {
25
+ return `${width}x${height}`;
26
+ }
27
+
28
+ export const initCommand = {
29
+ name: "init",
30
+ aliases: ["i"],
31
+ group: "Core Commands:",
32
+ summary: "Create a new project",
33
+ description: `Scaffold a project config, a source file, and optional starter
34
+ assets. On a terminal this opens a short wizard; use --yes for defaults.`,
35
+ arguments: ["[dir]"],
36
+ options: [
37
+ { flags: "--name <name>", description: "Project name" },
38
+ {
39
+ flags: "--template <template>",
40
+ description: "Template: cat, empty, or blank",
41
+ },
42
+ {
43
+ flags: "--target <target>",
44
+ description: "Package target: none, html, or zip",
45
+ },
46
+ {
47
+ flags: "--size <WxH>",
48
+ description: "Stage size (default 480x360)",
49
+ },
50
+ { flags: "--force", description: "Overwrite existing files" },
51
+ ],
52
+ examples: [
53
+ "fx init",
54
+ "fx init examples/demo --yes",
55
+ "fx init --template blank --target zip",
56
+ ],
57
+ async run({ args, options, cwd, reporter, prompts, product, yes }) {
58
+ const directory = path.resolve(cwd, args[0] || ".");
59
+ const defaultName = `My ${product.name} Project`;
60
+ const interactive = prompts.enabled && !yes;
61
+
62
+ if (interactive) prompts.intro("Create a new project");
63
+
64
+ const name =
65
+ options.name ??
66
+ (interactive
67
+ ? await prompts.text({
68
+ message: "Project name",
69
+ defaultValue: defaultName,
70
+ })
71
+ : defaultName);
72
+
73
+ let template = options.template ?? "cat";
74
+ if (!TEMPLATE_VALUES.has(template))
75
+ throw new UsageError(
76
+ `--template must be one of: ${[...TEMPLATE_VALUES].join(", ")}`,
77
+ );
78
+ if (interactive && options.template === undefined)
79
+ template = await prompts.select({
80
+ message: "Template",
81
+ options: TEMPLATES,
82
+ initialValue: template,
83
+ });
84
+
85
+ let size = options.size ?? "480x360";
86
+ if (interactive && options.size === undefined)
87
+ size = await prompts.select({
88
+ message: "Stage size",
89
+ options: STAGE_SIZES,
90
+ initialValue: "480x360",
91
+ });
92
+ const { width, height } = parseSize(size);
93
+
94
+ let target = options.target ?? "html";
95
+ if (!TARGET_VALUES.has(target)) {
96
+ try {
97
+ target = normalizePackageTarget(target);
98
+ } catch {
99
+ throw new UsageError(
100
+ `--target must be one of: ${[...TARGET_VALUES].join(", ")}`,
101
+ );
102
+ }
103
+ }
104
+ if (interactive && options.target === undefined)
105
+ target = await prompts.select({
106
+ message: "Package target",
107
+ options: TARGETS,
108
+ initialValue: target,
109
+ });
110
+
111
+ const files = initFiles({
112
+ product,
113
+ name,
114
+ template,
115
+ width,
116
+ height,
117
+ target,
118
+ });
119
+ const conflicts = Object.keys(files).filter((relative) =>
120
+ fs.existsSync(path.join(directory, relative)),
121
+ );
122
+
123
+ let force = Boolean(options.force);
124
+ if (conflicts.length > 0 && !force) {
125
+ if (interactive) {
126
+ const overwrite = await prompts.confirm({
127
+ message: `Overwrite ${conflicts.length} existing file(s)?`,
128
+ initialValue: false,
129
+ });
130
+ force = Boolean(overwrite);
131
+ }
132
+ if (!force)
133
+ reporter.warn(
134
+ `Skipping ${conflicts.length} existing file(s); pass --force to overwrite`,
135
+ );
136
+ }
137
+
138
+ const written = [];
139
+ for (const relative of Object.keys(files).sort()) {
140
+ const destination = path.join(directory, relative);
141
+ if (fs.existsSync(destination) && !force) {
142
+ written.push({ path: relative, status: "skipped" });
143
+ continue;
144
+ }
145
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
146
+ fs.writeFileSync(destination, files[relative]);
147
+ written.push({ path: relative, status: "written" });
148
+ reporter.bullet(relative);
149
+ }
150
+
151
+ const count = written.filter((entry) => entry.status === "written").length;
152
+ reporter.success(
153
+ `Created ${count} file(s) in ${path.relative(cwd, directory) || "."}`,
154
+ );
155
+ if (interactive)
156
+ prompts.outro(
157
+ `Run "${product.command} build" or "${product.command} dev" next.`,
158
+ );
159
+
160
+ return {
161
+ directory,
162
+ name,
163
+ template,
164
+ target,
165
+ size: sizeValue(width, height),
166
+ files: written,
167
+ };
168
+ },
169
+ };
@@ -0,0 +1,270 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { sourceExtension } from "../../brand.js";
4
+ import { discoverSources } from "../../project.js";
5
+ import { applyFixes, lintSources } from "../../linter.js";
6
+ import {
7
+ effectivePreset,
8
+ normalizeLintConfig,
9
+ parseRuleOption,
10
+ } from "../../lint/config.js";
11
+ import { PRESET_NAMES, ruleById } from "../../lint/rules.js";
12
+ import { loadProjectState, resolveInput } from "../context.js";
13
+ import { outcome } from "../define-command.js";
14
+ import { EXIT, UsageError } from "../errors.js";
15
+ import { plural } from "../format.js";
16
+
17
+ const EXTENSION = sourceExtension();
18
+
19
+ /** Repeatable `--rule` values are collected into an array. */
20
+ function collect(value, previous = []) {
21
+ return [...previous, value];
22
+ }
23
+
24
+ function displayPath(cwd, file) {
25
+ const relativePath = path.relative(cwd, file);
26
+ return relativePath.startsWith("..") || path.isAbsolute(relativePath)
27
+ ? file
28
+ : relativePath;
29
+ }
30
+
31
+ /**
32
+ * Resolve `--preset` and `--rule` on top of the project's `fx.lint` block.
33
+ * A bare `--rule FX201` enables the rule at its default severity; `off`
34
+ * disables it, and `error`/`warn` override the severity.
35
+ */
36
+ function resolveOptions(options, loaded, product) {
37
+ const lintConfig = normalizeLintConfig(loaded.fx?.lint ?? {}, (message) => {
38
+ throw new UsageError(message, {
39
+ hint: "check the lint block in fx.yml",
40
+ });
41
+ });
42
+
43
+ if (options.preset != null) {
44
+ const preset = String(options.preset).trim().toLowerCase();
45
+ if (!PRESET_NAMES.includes(preset))
46
+ throw new UsageError(`unknown lint preset: ${options.preset}`, {
47
+ hint: `use one of: ${PRESET_NAMES.join(", ")}`,
48
+ });
49
+ lintConfig.extends = preset;
50
+ }
51
+
52
+ const overrides = {};
53
+ for (const raw of options.rule ?? []) {
54
+ const parsed = parseRuleOption(raw);
55
+ const preset = parsed.id.toLowerCase();
56
+ if (!parsed.severity && PRESET_NAMES.includes(preset)) {
57
+ lintConfig.extends = preset;
58
+ continue;
59
+ }
60
+ const rule = ruleById(parsed.id);
61
+ if (!rule)
62
+ throw new UsageError(`unknown lint rule: ${parsed.id}`, {
63
+ hint: `run "${product.command} lint --help" for rule selection`,
64
+ });
65
+ if (!parsed.severity) {
66
+ overrides[rule.id] = rule.severity;
67
+ continue;
68
+ }
69
+ if (!["error", "warning", "off"].includes(parsed.severity))
70
+ throw new UsageError(
71
+ `invalid severity for ${rule.id}: ${String(parsed.severity)}`,
72
+ { hint: "use error, warn or off" },
73
+ );
74
+ overrides[rule.id] = parsed.severity;
75
+ }
76
+
77
+ return { lintConfig, overrides };
78
+ }
79
+
80
+ /** Strip fix internals and display paths relative to the working directory. */
81
+ function toJsonDiagnostic(diagnostic, cwd) {
82
+ return {
83
+ ruleId: diagnostic.ruleId,
84
+ severity: diagnostic.severity,
85
+ category: diagnostic.category,
86
+ message: diagnostic.message,
87
+ file: diagnostic.file ? displayPath(cwd, diagnostic.file) : null,
88
+ line: diagnostic.line,
89
+ column: diagnostic.column,
90
+ endLine: diagnostic.endLine,
91
+ endColumn: diagnostic.endColumn,
92
+ explanation: diagnostic.explanation,
93
+ fixable: diagnostic.fixable,
94
+ };
95
+ }
96
+
97
+ function reportDiagnostics(reporter, diagnostics, cwd) {
98
+ let current = null;
99
+ for (const diagnostic of diagnostics) {
100
+ const file = diagnostic.file
101
+ ? displayPath(cwd, diagnostic.file)
102
+ : "<input>";
103
+ if (file !== current) {
104
+ current = file;
105
+ reporter.text(reporter.bold(file));
106
+ }
107
+ const mark =
108
+ diagnostic.severity === "error"
109
+ ? "error"
110
+ : diagnostic.severity === "warning"
111
+ ? "warn "
112
+ : "info ";
113
+ reporter.text(
114
+ ` ${mark} ${diagnostic.line}:${diagnostic.column} ${diagnostic.ruleId} ${diagnostic.message}`,
115
+ );
116
+ }
117
+ }
118
+
119
+ export const lintCommand = {
120
+ name: "lint",
121
+ group: "Core Commands:",
122
+ summary: "Report source and project problems",
123
+ description: `Statically analyse a file, or every ${EXTENSION} file in a directory
124
+ (defaults to the working directory), plus the merged project. Rules come from
125
+ the "lint" block under "fx" in fx.yml; --preset and --rule override them.
126
+
127
+ Exit status is 1 when errors are reported, or when warnings are reported with
128
+ --strict. Linting never changes a file unless --fix is passed.`,
129
+ arguments: [`[input${EXTENSION}|dir]`],
130
+ options: [
131
+ { flags: "--fix", description: "Apply safe, explicit fixes" },
132
+ {
133
+ flags: "--strict",
134
+ description: "Use the strict preset and fail on warnings",
135
+ },
136
+ {
137
+ flags: "--preset <name>",
138
+ description: `Use a preset: ${PRESET_NAMES.join(", ")}`,
139
+ },
140
+ {
141
+ flags: "--rule <id[=severity]>",
142
+ description: "Enable, disable or set one rule (repeatable)",
143
+ parser: collect,
144
+ defaultValue: [],
145
+ },
146
+ ],
147
+ examples: [
148
+ "fx lint",
149
+ "fx lint src/main.tfx",
150
+ "fx lint --strict",
151
+ "fx lint --json",
152
+ "fx lint --rule FX201=off",
153
+ "fx lint --fix",
154
+ ],
155
+ async run({ args, cwd, options, reporter, product }) {
156
+ const target = resolveInput(args[0], cwd);
157
+ const files = target.file ? [target.file] : discoverSources(target.dir);
158
+ if (files.length === 0)
159
+ throw new UsageError(`No ${EXTENSION} files found under ${target.dir}`);
160
+
161
+ const state = await loadProjectState({
162
+ input: args[0],
163
+ cwd,
164
+ options,
165
+ reporter,
166
+ includeExtensions: true,
167
+ migrateLegacy: false,
168
+ });
169
+ const { lintConfig, overrides } = resolveOptions(
170
+ options,
171
+ state.loaded,
172
+ product,
173
+ );
174
+ const preset = effectivePreset(lintConfig, Boolean(options.strict));
175
+
176
+ const lintOptions = {
177
+ config: state.loaded,
178
+ configPath: state.loaded.path,
179
+ configDir: state.configDir,
180
+ rootDir: state.configDir ?? cwd,
181
+ extensionCatalog: state.catalog,
182
+ lintConfig,
183
+ strict: Boolean(options.strict),
184
+ overrides,
185
+ };
186
+
187
+ // Analyse the whole project so cross-file declarations resolve, but keep
188
+ // the selected files separate for reporting, counts and --fix writes.
189
+ // Diagnostics whose file is not a source (assets, fx.yml) stay visible.
190
+ const projectDir = state.configDir ?? target.dir;
191
+ const analysisFiles = discoverSources(projectDir);
192
+ for (const file of files)
193
+ if (!analysisFiles.includes(file)) analysisFiles.push(file);
194
+ const analysisSet = new Set(analysisFiles);
195
+ const selected = new Set(files);
196
+ const inScope = (diagnostic) =>
197
+ diagnostic.file == null ||
198
+ selected.has(diagnostic.file) ||
199
+ !analysisSet.has(diagnostic.file);
200
+
201
+ let sources = analysisFiles.map((file) => ({
202
+ file,
203
+ source: fs.readFileSync(file, "utf8"),
204
+ }));
205
+ let result = lintSources({ sources, ...lintOptions });
206
+ let diagnostics = result.diagnostics.filter(inScope);
207
+ let fixed = 0;
208
+
209
+ if (options.fix) {
210
+ const applied = applyFixes(sources, diagnostics);
211
+ fixed = applied.fixed;
212
+ if (fixed > 0) {
213
+ for (const file of applied.files)
214
+ fs.writeFileSync(file, applied.sources.get(file));
215
+ sources = sources.map(({ file, source }) => ({
216
+ file,
217
+ source: applied.sources.get(file) ?? source,
218
+ }));
219
+ result = lintSources({ sources, ...lintOptions });
220
+ diagnostics = result.diagnostics.filter(inScope);
221
+ }
222
+ }
223
+
224
+ const summary = {
225
+ errors: diagnostics.filter((entry) => entry.severity === "error").length,
226
+ warnings: diagnostics.filter((entry) => entry.severity === "warning")
227
+ .length,
228
+ suppressed: result.summary.suppressed,
229
+ files: files.length,
230
+ };
231
+
232
+ if (!reporter.json) {
233
+ reportDiagnostics(reporter, diagnostics, cwd);
234
+ if (fixed > 0) reporter.info(`Fixed ${plural(fixed, "problem")}.`);
235
+ const { errors, warnings } = summary;
236
+ if (errors > 0 || warnings > 0) {
237
+ const parts = [];
238
+ if (errors > 0) parts.push(plural(errors, "error"));
239
+ if (warnings > 0) parts.push(plural(warnings, "warning"));
240
+ reporter.error(
241
+ `${parts.join(", ")} in ${plural(files.length, "file")} (${preset} preset)`,
242
+ );
243
+ } else {
244
+ reporter.success(
245
+ `Linted ${plural(files.length, "file")}: no problems (${preset} preset)`,
246
+ );
247
+ }
248
+ }
249
+
250
+ const failsOnWarnings = Boolean(options.strict);
251
+ const exitCode =
252
+ summary.errors > 0 || (failsOnWarnings && summary.warnings > 0)
253
+ ? EXIT.FAILURE
254
+ : EXIT.OK;
255
+
256
+ return outcome({
257
+ exitCode,
258
+ data: {
259
+ preset,
260
+ strict: failsOnWarnings,
261
+ fixed,
262
+ files: files.map((file) => displayPath(cwd, file)),
263
+ summary,
264
+ diagnostics: diagnostics.map((diagnostic) =>
265
+ toJsonDiagnostic(diagnostic, cwd),
266
+ ),
267
+ },
268
+ });
269
+ },
270
+ };