@vistta/cli 1.1.0 → 1.2.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
@@ -27,25 +27,198 @@ npm install @vistta/cli
27
27
  - `vistta bundle <filename> [outdir]`
28
28
  Bundle an entry file and its dependencies.
29
29
 
30
+ - `vistta check [patterns] [--fix]`
31
+ Runs the enabled formatting, lint, and type checks. Read-only by default;
32
+ `--fix` applies formatting and lint fixes. Any failed stage, including lint
33
+ warnings, produces a nonzero exit status. Patterns limit formatting and lint;
34
+ type checking still covers the whole configured project.
35
+
36
+ - `vistta lint [patterns] [--fix]`
37
+ Runs project-local ESLint against the configured files or supplied patterns.
38
+ `--fix` applies available fixes. Errors and warnings produce a nonzero exit
39
+ status.
40
+
41
+ - `vistta fmt [patterns] [--check|--write]`
42
+ Checks formatting with project-local Prettier without changing files by
43
+ default. `--check` makes that mode explicit; `--write` formats files in place.
44
+ `--fix` is also accepted as a write alias. Check and write modes cannot be
45
+ combined.
46
+
47
+ - `vistta types`
48
+ Checks the complete TypeScript or JavaScript project using its configuration.
49
+ Never emits files and does not accept file patterns.
50
+
51
+ - `vistta config [--sources]`
52
+ Prints the merged project configuration, including built-in tool defaults.
53
+ `--sources` also shows the selected config file and the origin of each setting.
54
+
55
+ - `vistta task <name> [-- args]`
56
+ Runs a configured task, package script, or registered command with its task
57
+ dependencies. Stops on the first failure and forwards arguments after `--`
58
+ only to the requested task's executable command or script.
59
+
60
+ - `vistta run <name> [-- args]`
61
+ Alias for `vistta task` in the base CLI. Frameworks can override it:
62
+ `@vistta/js` uses `run` to launch applications, so use `vistta task` there.
63
+
30
64
  - `vistta project name`
31
65
  Outputs the current project name from `package.json`.
32
66
 
33
- - `vistta project version [major|minor|patch]`
34
- Outputs the current project version. If a level (`major`, `minor`, `patch`) is provided, it increments the version accordingly.
67
+ - `vistta project version [increment|version] [prerelease-id]`
68
+ Prints the current project version, increments it (for example, `patch` or
69
+ `prerelease`), or sets an explicit version such as `1.2.3`.
70
+
71
+ - `vistta project outdated [dependency]`
72
+ Shows installed (`Current`), range-compatible (`Wanted`), and `Latest`
73
+ versions, plus the owning manifest and dependency type, using `npm outdated`,
74
+ respecting npm registry settings and policies such as `min-release-age`.
75
+ Inside a workspace package, only that package's dependencies are checked;
76
+ running from the workspace root checks all workspace consumers.
77
+ Linked workspace packages are excluded. Available compatible updates cause a
78
+ nonzero exit status; newer versions outside the declared range are informational.
79
+
80
+ - `vistta project update [dependency] [--compatible] [--dry-run]`
81
+ Updates declared dependency ranges to latest versions by default, which may
82
+ include major upgrades. `--compatible` selects npm's wanted version within
83
+ each dependency's existing range instead. `--dry-run` previews the exact
84
+ manifest changes without writing files.
85
+
86
+ - `vistta project patch [dependency] [--dry-run]`
87
+ Backward-compatible alias for `project update --compatible`. Despite the
88
+ historical name, this can select a compatible minor update, not just patches.
89
+
90
+ Dependency updates only edit `package.json` ranges; run `npm install` afterward
91
+ to update installed versions and the lockfile. Workspace-root updates preserve
92
+ each consumer's own target version. If no ranges need changing, the command
93
+ says so explicitly. A dependency filter selects matching outdated entries; no
94
+ match means no outdated entry was found, not that the dependency exists.
95
+
96
+ ```sh
97
+ vistta project --help
98
+ vistta project update --help
99
+ vistta project outdated react
100
+ vistta project update --compatible --dry-run
101
+ vistta project update react --compatible
102
+ vistta project update react --dry-run
103
+ ```
35
104
 
36
- - `vistta project outdated`
37
- Lists all outdated dependencies in the project.
105
+ - `vistta test [...patterns] [--filter=pattern] [--only]`
106
+ Runs matching test files with the base CLI's Node runner. `--filter` selects
107
+ test names using a case-insensitive regular expression; `--only` selects tests
108
+ or suites marked as exclusive. Frameworks can provide a different test runner.
38
109
 
39
- - `vistta patch [module-name]` or `vistta project patch [module-name]`
40
- Updates the version of a specific module (or all outdated modules if none is specified) to the latest patch version in `package.json`.
110
+ For browser-based test harnesses, import `TestRunner` from `@vistta/cli/test-runner` to avoid bundling Node-only CLI modules.
41
111
 
42
- - `vistta update [module-name]` or `vistta project update [module-name]`
43
- Updates the version of a specific module (or all outdated modules if none is specified) to its latest available version in `package.json`.
112
+ ### Unified Project Configuration
44
113
 
45
- - `vistta test [pattern]`
46
- Run tests matching the specified pattern.
114
+ Keep VISTTA settings in the `cli` block of `tsconfig.json` or `jsconfig.json`.
115
+ Comments and trailing commas are supported. If both files exist, `tsconfig.json`
116
+ takes precedence. The existing `cliOptions` block remains supported; `cli` wins
117
+ when both specify the same setting.
47
118
 
48
- For browser-based test harnesses, import `TestRunner` from `@vistta/cli/test-runner` to avoid bundling Node-only CLI modules.
119
+ Install the tools you want to use locally:
120
+
121
+ ```sh
122
+ npm install --save-dev eslint prettier typescript
123
+ ```
124
+
125
+ ```jsonc
126
+ {
127
+ "$schema": "./node_modules/@vistta/cli/config.schema.json",
128
+ "compilerOptions": {
129
+ "allowJs": true,
130
+ "checkJs": true,
131
+ "noEmit": true,
132
+ },
133
+ "include": ["src/**/*.js", "src/**/*.ts"],
134
+ "exclude": ["dist"],
135
+ "cli": {
136
+ "check": { "format": true, "lint": true, "types": true },
137
+ "format": {
138
+ "engine": "prettier",
139
+ "options": { "printWidth": 120, "semi": true },
140
+ },
141
+ "lint": {
142
+ "engine": "eslint",
143
+ "rules": { "no-unused-vars": "warn" },
144
+ },
145
+ "tasks": {
146
+ "validate": { "dependsOn": ["check", "test"] },
147
+ "release": { "dependsOn": ["validate"], "script": "publish:package" },
148
+ },
149
+ },
150
+ }
151
+ ```
152
+
153
+ The optional `$schema` enables completion and validation for VISTTA settings.
154
+ The schema leaves other TypeScript and framework settings open; TypeScript
155
+ validates compiler options during `vistta types` or `vistta check`.
156
+
157
+ ```sh
158
+ vistta check
159
+ vistta check --fix
160
+ vistta lint "src/**/*.js" --fix
161
+ vistta fmt --check
162
+ vistta fmt --write
163
+ vistta types
164
+ vistta config
165
+ vistta config --sources
166
+ vistta task validate
167
+ vistta run validate
168
+ ```
169
+
170
+ - `check` runs formatting, linting, and type checking by default. Disable a stage
171
+ with `cli.check`. All enabled stages run, and any failure produces a nonzero
172
+ exit code. Lint warnings also fail the check.
173
+ - Checks are read-only unless `--fix` or `fmt --write` is supplied. Fix mode
174
+ applies Prettier formatting and ESLint fixes; type checking never emits files.
175
+ - Tools are resolved from the current project, not downloaded automatically.
176
+ Only enabled tools need to be installed. ESLint 9+ and Prettier 3 are supported.
177
+ - `format.include` and `lint.include` override top-level `include`. Explicit
178
+ command-line patterns override both and are relative to the working directory.
179
+ The corresponding `exclude` settings, `compilerOptions.outDir`, dependencies,
180
+ and Git metadata are excluded. Inherited patterns are relative to their owning
181
+ configuration file. Type checking always covers the whole configured project.
182
+ - Existing ESLint and Prettier configs are used when present. Inline settings
183
+ override them. Without an ESLint config, inline rules/options provide the flat
184
+ config. `lint.options` contains flat-config properties, not ESLint constructor
185
+ options. Without explicit includes, lint scans JS/TS and formatting scans files
186
+ supported by Prettier. An empty match set passes without changing files.
187
+ - For plugins, parsers, or other executable settings, set `lint.config` or
188
+ `format.config` to a module path such as `"./tools/eslint.js"`. It must default
189
+ export the relevant tool configuration. Relative paths resolve from the owning
190
+ config file; package references resolve from that same directory. Referenced
191
+ lint configs replace ESLint config discovery; inline settings still win.
192
+
193
+ ### Presets And Tasks
194
+
195
+ `extends` accepts a relative JSON/JSONC config, a package config export, or an
196
+ array of either. Parents apply left to right, then local settings override them.
197
+ Objects merge recursively; arrays and scalar values replace inherited values.
198
+ Circular presets, malformed JSONC, and invalid VISTTA tool/task settings fail
199
+ explicitly. `vistta config --sources` shows the merged configuration and the
200
+ file responsible for each leaf setting, including built-in check defaults.
201
+
202
+ Workspace packages can extend a shared root/preset config and override `cli`
203
+ locally. Commands run in the current package directory; they do not automatically
204
+ fan out across workspaces. Existing `compilerOptions`, `bundlerOptions`, and
205
+ custom commands continue to work.
206
+
207
+ Tasks run their `dependsOn` dependencies once, in order, before their optional
208
+ package `script`. Dependencies can name other tasks, current-package scripts, or
209
+ registered commands such as `check`, `lint`, `fmt`, `types`, `test`, and `bundle`
210
+ (or `build` when provided by a framework).
211
+ Missing tasks/scripts and dependency cycles fail before execution. The first
212
+ failed command stops the plan and propagates its exit status. Arguments after
213
+ `--` are forwarded only to the requested task's command or script:
214
+
215
+ ```sh
216
+ vistta task release -- --dry-run
217
+ ```
218
+
219
+ `vistta run` is a task alias in the base CLI. Framework command overrides are
220
+ preserved: `@vistta/js` keeps its application-oriented `run`, so use `vistta task`
221
+ there. Task caching and automatic cross-workspace scheduling are not implemented.
49
222
 
50
223
  ## Extending the CLI
51
224
 
package/bin.js CHANGED
@@ -22,6 +22,10 @@ const argv = [];
22
22
  const execArgv = ["--import", pathToFileURL(fs.resolve(dirname, "register.js")), "--experimental-sqlite", "--title=VISTTA"];
23
23
  for (let i = 2, len = process.argv.length; i < len; i++) {
24
24
  const [arg, value] = process.argv[i].toLowerCase().split("=");
25
+ if (process.argv[i] === "--") {
26
+ argv.push(...process.argv.slice(i));
27
+ break;
28
+ }
25
29
  const nodeOption = ENABLED_NODE_OPTIONS[arg];
26
30
  if (nodeOption) {
27
31
  if (typeof nodeOption === "string") env[nodeOption] = value || true;
@@ -1,97 +1,137 @@
1
- import { COLORS } from "@vistta/console";
2
1
  import fs from "@vistta/fs";
3
- import { capitalize } from "@vistta/utils";
2
+ import { relative } from "node:path";
4
3
  import { Command } from "../classes/command.js";
5
- import { getOutdatedPackages, importJSON, incrementPackageVersion } from "../utils.js";
4
+ import { getOutdatedPackages, importJSON, incrementPackageVersion, parseArgs } from "../utils.js";
6
5
 
7
6
  const HELP = {
8
- name: "outputs current project name",
9
- version: "outputs the project version",
10
- "version [increment]": "updates the project version",
11
- outdated: "lists all outdated modules in the project",
12
- patch: "updates the version of outdated modules to the latest patch version in the project",
13
- "patch [module]": "updates the version of outdated module to the latest patch version in the project",
14
- update: "updates the version of outdated modules to the latest version in the project",
15
- "update [module]": "updates the version of outdated module to the latest version in the project",
7
+ name: ["name", "Print the project name."],
8
+ version: [
9
+ "version [increment|version] [prerelease-id]",
10
+ "Print or change the project version. Increments: major, minor, patch, premajor, preminor, prepatch, prerelease.",
11
+ ],
12
+ outdated: [
13
+ "outdated [dependency]",
14
+ "Compare installed, wanted (within the declared range), and latest dependency versions.",
15
+ ],
16
+ update: [
17
+ "update [dependency] [--compatible] [--dry-run]",
18
+ "Update dependency ranges to latest; this may include major upgrades. --compatible selects wanted versions instead.",
19
+ ],
20
+ patch: ["patch [dependency] [--dry-run]", "Alias for update --compatible. This is range-compatible, not patch-only."],
16
21
  };
17
22
 
18
23
  export default class extends Command {
19
24
  async help(_, command) {
20
- switch (command) {
21
- case "name":
22
- console.print(capitalize(HELP["name"]));
23
- console.print("\nUsage:");
24
- console.print("vistta project name");
25
- break;
26
- case "version":
27
- console.print(capitalize(HELP["version"]) + " or " + HELP["version [increment]"]);
28
- console.print("\nUsage:");
29
- console.print("vistta project version");
30
- console.print("vistta project version [increment]");
31
- break;
32
- case "outdated":
33
- console.print(capitalize(HELP["outdated"]));
34
- console.print("\nUsage:");
35
- console.print("vistta project outdated");
36
- break;
37
- case "patch":
38
- console.print(capitalize(HELP["patch"]) + " or " + HELP["patch [module]"]);
39
- console.print("\nUsage:");
40
- console.print("vistta project patch");
41
- console.print("vistta project patch [module]");
42
- break;
43
- case "update":
44
- console.print(capitalize(HELP["update"]) + " or " + HELP["update [module]"]);
45
- console.print("\nUsage:");
46
- console.print("vistta project update");
47
- console.print("vistta project update [module]");
48
- break;
49
- default:
50
- console.print("Outputs the current project name or version, or updates the version");
51
- console.print("\nUsage:");
52
- Object.keys(HELP).forEach((key) => console.print(`vistta project ${key}\t\t\t${HELP[key]}`));
53
- break;
25
+ if (command && !Object.hasOwn(HELP, command))
26
+ throw new Error(`Unknown project command "${command}". Run vistta project --help.`);
27
+ console.print("Project metadata and dependency maintenance\n");
28
+ for (const [usage, description] of command ? [HELP[command]] : Object.values(HELP)) {
29
+ console.print(` vistta project ${usage}\n ${description}\n`);
54
30
  }
31
+ if (!command || ["outdated", "update", "patch"].includes(command))
32
+ console.print(
33
+ "Wanted respects npm policies and dependency ranges. Updates edit package.json only; run npm install afterward.",
34
+ );
55
35
  }
56
36
 
57
37
  async main(_, command, ...args) {
58
- console.clear();
38
+ if (!command || command === "help") return this.help(_, args[0]);
39
+ if (!Object.hasOwn(HELP, command)) return this.help(_, command);
40
+ const [values, options] = parseArgs(args);
41
+ const allowed = command === "update" ? ["compatible", "dry-run"] : command === "patch" ? ["dry-run"] : [];
42
+ for (const [name, value] of Object.entries(options)) {
43
+ if (!allowed.includes(name)) throw new Error(`Unknown option --${name} for project ${command}`);
44
+ if (typeof value !== "boolean") throw new Error(`--${name} must be a boolean flag`);
45
+ }
46
+ const limit = command === "name" ? 0 : command === "version" ? 2 : 1;
47
+ if (values.length > limit) throw new Error(`Too many arguments. Run vistta project ${command} --help.`);
59
48
  switch (command) {
60
49
  case "name":
61
50
  return console.print(process.env.PROJECT_NAME);
62
51
  case "version":
63
- if (!args[0]) return console.print(process.env.PROJECT_VERSION);
52
+ if (!values[0]) return console.print(process.env.PROJECT_VERSION);
64
53
  return console.print(
65
- await incrementPackageVersion(fs.resolve(process.env.PROJECT_PATH, "package.json"), args[0], args[1]),
54
+ await incrementPackageVersion(fs.resolve(process.env.PROJECT_PATH, "package.json"), values[0], values[1]),
66
55
  );
67
56
  case "outdated":
68
57
  case "patch":
69
58
  case "update":
70
- return modules(command, args[0]);
59
+ process.exit(await dependencies(command, values[0], options));
71
60
  }
72
- return this.help(_, command);
73
61
  }
74
62
  }
75
63
 
76
- async function modules(command, arg1) {
64
+ async function dependencies(command, dependency, options) {
77
65
  const modules = await getOutdatedPackages(process.cwd());
78
- let valid = true;
79
- let update = 0;
80
- for (let i = 0, len = modules.length; i < len; i++) {
81
- const { name, package: packagePath, current, wanted, latest, dev } = modules[i];
82
- if (command === "patch" || command === "update") {
83
- if (command === "patch" && current === wanted) continue;
84
- if (arg1 && name !== arg1) continue;
85
- const packageObj = await importJSON(packagePath);
86
- packageObj[dev ? "devDependencies" : "dependencies"][name] = "^" + (command === "patch" ? wanted : latest);
87
- await fs.writeFile(packagePath, JSON.stringify(packageObj, null, 2));
88
- update++;
89
- } else if (current === wanted)
90
- console.print(`${COLORS.CYAN}Module "${name}" has a new version (${latest})${COLORS.RESET}`);
91
- else (console.print(`${COLORS.YELLOW}Module "${name}" is outdated (${latest})${COLORS.RESET}`), (valid = false));
66
+ const selected = modules.filter((entry) => !dependency || entry.name === dependency);
67
+ if (!selected.length) {
68
+ console.print(
69
+ dependency
70
+ ? `No outdated dependency "${dependency}" found in this scope.`
71
+ : "All checked dependencies are up to date.",
72
+ );
73
+ return 0;
92
74
  }
93
-
94
- if (modules.length == 0) console.print("Everything is up to date");
95
- else if (update > 0) console.print(`Updated ${update} module${update > 1 ? "s" : ""} versions in project package.`);
96
- process.exit(valid ? 0 : -1);
75
+ if (command === "outdated") {
76
+ const rows = [
77
+ ["Dependency", "Current", "Wanted", "Latest", "Manifest", "Type"],
78
+ ...selected.map((entry) => [
79
+ entry.name,
80
+ entry.current || "MISSING",
81
+ entry.wanted || "-",
82
+ entry.latest || "-",
83
+ relative(process.cwd(), entry.package) || "package.json",
84
+ entry.dev ? "dev" : "prod",
85
+ ]),
86
+ ];
87
+ const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => row[column].length)));
88
+ for (const row of rows)
89
+ console.print(
90
+ row
91
+ .map((value, column) => value.padEnd(widths[column]))
92
+ .join(" ")
93
+ .trimEnd(),
94
+ );
95
+ const compatible = selected.filter((entry) => !entry.current || entry.current !== entry.wanted).length;
96
+ console.print(
97
+ `\n${compatible} compatible update(s) or missing dependency(s); ${selected.length - compatible} latest-only update(s).`,
98
+ );
99
+ console.print("Compatible updates cause a nonzero exit status; latest-only updates are informational.");
100
+ return compatible ? 1 : 0;
101
+ }
102
+ const compatible = command === "patch" || options.compatible === true;
103
+ const changes = new Map();
104
+ let count = 0;
105
+ for (const entry of selected) {
106
+ const target = compatible ? entry.wanted : entry.latest;
107
+ if (!target || entry.current === target) continue;
108
+ const manifest = changes.get(entry.package) || (await importJSON(entry.package));
109
+ const section = entry.dev ? "devDependencies" : "dependencies";
110
+ const previous = manifest[section]?.[entry.name];
111
+ if (typeof previous !== "string") throw new Error(`Dependency "${entry.name}" is missing from ${entry.package}`);
112
+ const next = "^" + target;
113
+ if (previous === next) continue;
114
+ manifest[section][entry.name] = next;
115
+ changes.set(entry.package, manifest);
116
+ count++;
117
+ console.print(
118
+ `${options["dry-run"] ? "Would update" : "Update"}: ${entry.name} ${previous} -> ${next} (${relative(process.cwd(), entry.package)}, ${section})`,
119
+ );
120
+ }
121
+ if (!count) {
122
+ console.print(
123
+ `No ${compatible ? "compatible " : ""}dependency range changes needed. Run npm install if installed versions are behind.`,
124
+ );
125
+ return 0;
126
+ }
127
+ if (!options["dry-run"]) {
128
+ for (const [path, manifest] of changes) await fs.writeFile(path, JSON.stringify(manifest, null, 2) + "\n");
129
+ }
130
+ console.print(
131
+ `\n${options["dry-run"] ? "Would change" : "Changed"} ${count} dependency range(s) in ${changes.size} manifest(s).`,
132
+ );
133
+ console.print(
134
+ options["dry-run"] ? "Dry run: no files changed." : "Run npm install to update installed packages and the lockfile.",
135
+ );
136
+ return 0;
97
137
  }
@@ -0,0 +1,52 @@
1
+ import { Command } from "../classes/command.js";
2
+ import { loadProjectConfig, mergeConfig } from "../config.js";
3
+ import { runChecks } from "../tools.js";
4
+ import { parseArgs } from "../utils.js";
5
+
6
+ export default class extends Command {
7
+ help() {
8
+ console.log(
9
+ "vistta check [patterns] [--fix]\nvistta lint [patterns] [--fix]\nvistta fmt [patterns] [--write]\nvistta types\nvistta config [--sources]",
10
+ );
11
+ }
12
+
13
+ async main(command, ...argv) {
14
+ const [patterns, options] = parseArgs(argv);
15
+ const allowed = command === "config" ? ["sources"] : command === "fmt" ? ["write", "check", "fix"] : ["fix"];
16
+ for (const [option, value] of Object.entries(options)) {
17
+ if (!allowed.includes(option)) throw new Error(`Unknown option --${option}`);
18
+ if (typeof value !== "boolean") throw new Error(`--${option} must be a boolean flag`);
19
+ }
20
+ if (["types", "config"].includes(command) && patterns.length)
21
+ throw new Error(`${command} does not accept file patterns`);
22
+ if (command === "fmt" && options.check && (options.write || options.fix))
23
+ throw new Error("Use either --check or --write, not both");
24
+ const project = loadProjectConfig();
25
+ if (command === "config") {
26
+ const defaults = {
27
+ cli: {
28
+ check: { format: true, lint: true, types: true },
29
+ lint: { engine: "eslint" },
30
+ format: { engine: "prettier" },
31
+ },
32
+ };
33
+ const config = mergeConfig(defaults, project.config);
34
+ const sources = {
35
+ "cli.check.format": "<default>",
36
+ "cli.check.lint": "<default>",
37
+ "cli.check.types": "<default>",
38
+ "cli.lint.engine": "<default>",
39
+ "cli.format.engine": "<default>",
40
+ ...project.sources,
41
+ };
42
+ console.print(JSON.stringify(options.sources ? { file: project.file, config, sources } : config, null, 2));
43
+ return;
44
+ }
45
+ const names =
46
+ command === "check"
47
+ ? ["format", "lint", "types"].filter((name) => project.config.cli?.check?.[name] !== false)
48
+ : [command === "fmt" ? "format" : command];
49
+ const code = await runChecks(names, project, { patterns, fix: options.fix === true || options.write === true });
50
+ process.exit(code);
51
+ }
52
+ }
@@ -0,0 +1,23 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { Command } from "../classes/command.js";
4
+ import { loadProjectConfig } from "../config.js";
5
+ import { planTasks, runTasks } from "../tools.js";
6
+
7
+ export default class extends Command {
8
+ help() {
9
+ console.log("vistta task <name> [-- args]\nvistta run <name> [-- args] (unless overridden by a framework)");
10
+ }
11
+
12
+ async main(_, name, ...args) {
13
+ if (!name) return this.help();
14
+ const cwd = process.cwd();
15
+ const { config } = loadProjectConfig(cwd);
16
+ const { scripts = {} } = JSON.parse(readFileSync(resolve(cwd, "package.json"), "utf8"));
17
+ const commands = Object.keys(process.vistta.options.cli.commands).filter(
18
+ (name) => !["default", "run", "task"].includes(name),
19
+ );
20
+ const plan = planTasks(name, config.cli?.tasks, scripts, commands);
21
+ process.exit(await runTasks(plan, cwd, args[0] === "--" ? args.slice(1) : args));
22
+ }
23
+ }
package/commands/test.js CHANGED
@@ -45,9 +45,9 @@ export default class extends DefaultCommand {
45
45
  }
46
46
  entry = (await entries.next())?.value;
47
47
  }
48
- if (!process.env.NODE_DEBUG) console.enable();
49
48
  let output = "";
50
49
  const results = await this.#testRunner.run();
50
+ if (!process.env.NODE_DEBUG) console.enable();
51
51
  for (let i = 0, len = results.length; i < len; i++) {
52
52
  const { name, tests } = results[i];
53
53
  let passing = 0;
package/config.js ADDED
@@ -0,0 +1,128 @@
1
+ import { parse, printParseErrorCode } from "jsonc-parser";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import { dirname, isAbsolute, resolve } from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+
7
+ export function mergeConfig(target, source) {
8
+ const result = { ...target };
9
+ for (const [key, value] of Object.entries(source)) {
10
+ if (["__proto__", "constructor", "prototype"].includes(key)) continue;
11
+ result[key] = value && typeof value === "object" && !Array.isArray(value) ? mergeConfig(result[key], value) : value;
12
+ }
13
+ return result;
14
+ }
15
+
16
+ export function loadProjectConfig(directory = process.cwd()) {
17
+ const file = ["tsconfig.json", "jsconfig.json"].map((name) => resolve(directory, name)).find(existsSync);
18
+ if (!file) return { file: undefined, config: {}, sources: {} };
19
+ const active = new Set();
20
+ const sources = {};
21
+ function load(filepath) {
22
+ if (active.has(filepath)) throw new Error(`Circular configuration inheritance: ${filepath}`);
23
+ active.add(filepath);
24
+ const errors = [];
25
+ const own = parse(readFileSync(filepath, "utf8"), errors, { allowTrailingComma: true });
26
+ if (errors.length) throw new Error(`Invalid configuration ${filepath}: ${printParseErrorCode(errors[0].error)}`);
27
+ if (!own || typeof own !== "object" || Array.isArray(own))
28
+ throw new Error(`Expected a configuration object in ${filepath}`);
29
+ for (const name of ["cli", "cliOptions"]) {
30
+ if (own[name] !== undefined && (!own[name] || typeof own[name] !== "object" || Array.isArray(own[name])))
31
+ throw new Error(`${name} must be an object in ${filepath}`);
32
+ }
33
+ let inherited = {};
34
+ for (const parent of own.extends ? (Array.isArray(own.extends) ? own.extends : [own.extends]) : []) {
35
+ if (typeof parent !== "string") throw new Error(`Expected a string in extends in ${filepath}`);
36
+ const base = dirname(filepath);
37
+ let parentFile;
38
+ if (parent.startsWith(".") || isAbsolute(parent)) {
39
+ parentFile = resolve(base, parent);
40
+ if (!existsSync(parentFile) && existsSync(parentFile + ".json")) parentFile += ".json";
41
+ } else parentFile = createRequire(filepath).resolve(parent);
42
+ inherited = mergeConfig(inherited, load(parentFile));
43
+ }
44
+ const cli = mergeConfig(own.cliOptions, own.cli || {});
45
+ if (cli.commands !== undefined && (!cli.commands || typeof cli.commands !== "object" || Array.isArray(cli.commands)))
46
+ throw new Error(`cli.commands must be an object in ${filepath}`);
47
+ for (const [name, value] of Object.entries(cli.commands || {})) {
48
+ if (typeof value !== "string") throw new Error(`Expected a module path for cli.commands.${name} in ${filepath}`);
49
+ cli.commands[name] =
50
+ value.startsWith(".") || isAbsolute(value)
51
+ ? pathToFileURL(resolve(dirname(filepath), value)).href
52
+ : pathToFileURL(createRequire(filepath).resolve(value)).href;
53
+ }
54
+ for (const section of ["lint", "format"]) {
55
+ if (cli[section]?.config !== undefined && typeof cli[section].config !== "string")
56
+ throw new Error(`cli.${section}.config must be a module path in ${filepath}`);
57
+ if (typeof cli[section]?.config === "string") {
58
+ const value = cli[section].config;
59
+ cli[section].config =
60
+ value.startsWith(".") || isAbsolute(value)
61
+ ? pathToFileURL(resolve(dirname(filepath), value)).href
62
+ : pathToFileURL(createRequire(filepath).resolve(value)).href;
63
+ }
64
+ }
65
+ const normalized = { ...own, cli };
66
+ delete normalized.extends;
67
+ delete normalized.cliOptions;
68
+ function record(value, prefix = "") {
69
+ for (const [key, entry] of Object.entries(value)) {
70
+ const path = prefix ? `${prefix}.${key}` : key;
71
+ if (entry && typeof entry === "object" && !Array.isArray(entry)) record(entry, path);
72
+ else sources[path] = filepath;
73
+ }
74
+ }
75
+ record(normalized);
76
+ active.delete(filepath);
77
+ return mergeConfig(inherited, normalized);
78
+ }
79
+ const config = load(file);
80
+ function stringList(value, path) {
81
+ if (value !== undefined && (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")))
82
+ throw new Error(`${path} must be an array of strings in ${file}`);
83
+ }
84
+ for (const name of ["include", "exclude", "files"]) stringList(config[name], name);
85
+ for (const section of ["check", "lint", "format", "tasks"]) {
86
+ const value = config.cli?.[section];
87
+ if (value !== undefined && (!value || typeof value !== "object" || Array.isArray(value)))
88
+ throw new Error(`cli.${section} must be an object in ${file}`);
89
+ }
90
+ for (const [name, value] of Object.entries(config.cli?.check || {})) {
91
+ if (!["format", "lint", "types"].includes(name) || typeof value !== "boolean")
92
+ throw new Error(`cli.check.${name} must be a boolean check named format, lint, or types`);
93
+ }
94
+ for (const section of ["lint", "format"]) {
95
+ const options = config.cli?.[section] || {};
96
+ const engine = options.engine;
97
+ const expected = section === "lint" ? "eslint" : "prettier";
98
+ if (engine !== undefined && engine !== expected) throw new Error(`cli.${section}.engine must be "${expected}"`);
99
+ for (const name of ["include", "exclude"]) stringList(options[name], `cli.${section}.${name}`);
100
+ for (const name of section === "lint" ? ["rules", "languageOptions", "options"] : ["options"]) {
101
+ const value = options[name];
102
+ if (value !== undefined && (!value || typeof value !== "object" || Array.isArray(value)))
103
+ throw new Error(`cli.${section}.${name} must be an object in ${file}`);
104
+ }
105
+ for (const name of Object.keys(options)) {
106
+ if (
107
+ ![
108
+ "engine",
109
+ "include",
110
+ "exclude",
111
+ "options",
112
+ "config",
113
+ ...(section === "lint" ? ["rules", "languageOptions"] : []),
114
+ ].includes(name)
115
+ )
116
+ throw new Error(`Unknown setting cli.${section}.${name} in ${file}`);
117
+ }
118
+ }
119
+ for (const [name, task] of Object.entries(config.cli?.tasks || {})) {
120
+ if (!task || typeof task !== "object" || Array.isArray(task)) throw new Error(`cli.tasks.${name} must be an object`);
121
+ stringList(task.dependsOn, `cli.tasks.${name}.dependsOn`);
122
+ if (task.script !== undefined && typeof task.script !== "string")
123
+ throw new Error(`cli.tasks.${name}.script must be a package script name`);
124
+ for (const key of Object.keys(task))
125
+ if (!["dependsOn", "script"].includes(key)) throw new Error(`Unknown setting cli.tasks.${name}.${key}`);
126
+ }
127
+ return { file, config, sources };
128
+ }