@vistta/cli 1.0.1 → 1.2.0

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/package.json CHANGED
@@ -1,15 +1,20 @@
1
1
  {
2
2
  "name": "@vistta/cli",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "main": "index.js",
6
6
  "exports": {
7
7
  ".": "./index.js",
8
+ "./global": "./global.d.ts",
8
9
  "./config": "./tsconfig.json",
9
- "./commands/default": "./commands/default.js",
10
- "./commands/bundle": "./commands/bundle.js",
11
- "./commands/project": "./commands/project.js",
12
- "./commands/test": "./commands/test.js",
10
+ "./schema": "./config.schema.json",
11
+ "./default": "./commands/default.js",
12
+ "./bundle": "./commands/bundle.js",
13
+ "./project": "./commands/project.js",
14
+ "./test": "./commands/test.js",
15
+ "./quality": "./commands/quality.js",
16
+ "./task": "./commands/task.js",
17
+ "./test-runner": "./classes/test-runner.js",
13
18
  "./loaders/bundler": "./loaders/bundler.js",
14
19
  "./loaders/file": "./loaders/file.js",
15
20
  "./loaders/json": "./loaders/json.js",
@@ -18,9 +23,6 @@
18
23
  "bin": {
19
24
  "vistta": "bin.js"
20
25
  },
21
- "scripts": {
22
- "postinstall": ""
23
- },
24
26
  "repository": {
25
27
  "type": "git",
26
28
  "url": "https://github.com/vistta-org/cli.git"
@@ -29,13 +31,14 @@
29
31
  "node": ">=22.0.0"
30
32
  },
31
33
  "dependencies": {
32
- "@vistta/console": "latest",
33
- "@vistta/fs": "latest",
34
- "@vistta/utils": "latest",
35
- "esbuild": "^0.25.5",
36
- "semver": "^7.7.2"
34
+ "@vistta/console": "^1.1.0",
35
+ "@vistta/fs": "^1.1.0",
36
+ "@vistta/utils": "^1.1.0",
37
+ "esbuild": "^0.28.2",
38
+ "jsonc-parser": "^3.3.1",
39
+ "semver": "^7.8.5"
37
40
  },
38
41
  "author": "Tiago Terenas Almeida",
39
- "license": "Apache 2.0 with Commons Clause",
42
+ "license": "GPL-3.0-only",
40
43
  "description": "Vistta CLI is a framework for extending the Node.js loader API and building reusable, customizable commands tools"
41
44
  }
package/register.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import "@vistta/console/global";
2
2
  import { register } from "node:module";
3
3
  import { MessageChannel } from "node:worker_threads";
4
- import { fs, importConfig, saveCrashReport } from "./utils.js";
4
+ import { importConfig, resolve, saveCrashReport } from "./utils.js";
5
5
 
6
6
  console.clear();
7
7
  console.print(`Vistta CLI v${process.env.CLI_VERSION}\n`);
@@ -15,7 +15,7 @@ global.loaders = {
15
15
  shared: new Proxy(shared, {
16
16
  set(_, prop, value) {
17
17
  port1.postMessage(JSON.stringify({ sync: true, prop, value }));
18
- return Reflect.set(...arguments);
18
+ return Reflect.set(shared, prop, value);
19
19
  },
20
20
  }),
21
21
  };
@@ -24,19 +24,27 @@ port1.on("message", (msg) => {
24
24
  if (event) events[event]?.forEach((callback) => callback(...args));
25
25
  else if (sync) shared[prop] = value;
26
26
  });
27
- port1.unref();
27
+ /** @type {any} */ (port1).unref?.();
28
28
 
29
29
  const config = await importConfig({
30
30
  cli: {
31
31
  commands: {
32
- default: fs.resolve(import.meta.dirname, "./commands/default.js"),
33
- bundle: fs.resolve(import.meta.dirname, "./commands/bundle.js"),
34
- project: fs.resolve(import.meta.dirname, "./commands/project.js"),
35
- test: fs.resolve(import.meta.dirname, "./commands/test.js"),
32
+ default: resolve(import.meta.dirname, "./commands/default.js"),
33
+ bundle: resolve(import.meta.dirname, "./commands/bundle.js"),
34
+ project: resolve(import.meta.dirname, "./commands/project.js"),
35
+ test: resolve(import.meta.dirname, "./commands/test.js"),
36
+ check: resolve(import.meta.dirname, "./commands/quality.js"),
37
+ lint: resolve(import.meta.dirname, "./commands/quality.js"),
38
+ fmt: resolve(import.meta.dirname, "./commands/quality.js"),
39
+ types: resolve(import.meta.dirname, "./commands/quality.js"),
40
+ config: resolve(import.meta.dirname, "./commands/quality.js"),
41
+ run: resolve(import.meta.dirname, "./commands/task.js"),
42
+ task: resolve(import.meta.dirname, "./commands/task.js"),
36
43
  },
37
44
  },
38
45
  });
39
46
  const command = new (await import(config.cli.commands[process.argv[2]] || config.cli.commands["default"])).default();
47
+ process.restart = () => process.send({ restart: true });
40
48
  process.vistta = {
41
49
  loaders: command.loaders,
42
50
  resolvers: command.resolvers,
@@ -0,0 +1,118 @@
1
+ import { mkdtempSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { Bundler } from "../classes/bundler.js";
5
+ import { Command } from "../classes/command.js";
6
+ import { Runtime } from "../classes/runtime.js";
7
+
8
+ suite("Bundler", () => {
9
+ test("constructor with Runtime stores it as the loader", () => {
10
+ const runtime = new Runtime({ loaders: [], resolvers: [], options: {} });
11
+ const bundler = new Bundler(runtime);
12
+ expect(typeof bundler.run).toEqual("function");
13
+ expect(typeof bundler.import).toEqual("function");
14
+ });
15
+
16
+ test("constructor with Command builds a Runtime from command options", () => {
17
+ const cmd = new Command();
18
+ const bundler = new Bundler(cmd);
19
+ expect(typeof bundler.run).toEqual("function");
20
+ });
21
+
22
+ test("constructor with no argument falls back to process.vistta", () => {
23
+ process.vistta = { loaders: [], resolvers: [], options: {} };
24
+ const bundler = new Bundler();
25
+ expect(typeof bundler.run).toEqual("function");
26
+ });
27
+
28
+ test("constructor throws on invalid argument", () => {
29
+ let caught;
30
+ try {
31
+ new Bundler(123);
32
+ } catch (e) {
33
+ caught = e;
34
+ }
35
+ expect(caught.message).toMatch("Invalid first argument");
36
+ });
37
+
38
+ test("run() applies defaults from runtime.options.bundler", async () => {
39
+ const dir = mkdtempSync(join(tmpdir(), "vistta-bundler-"));
40
+ const entry = join(dir, "entry.js");
41
+ writeFileSync(entry, "export const greeting = 'hi';\n");
42
+
43
+ const runtime = new Runtime({
44
+ loaders: [],
45
+ resolvers: ["js"],
46
+ options: { bundler: { target: "es2020" } },
47
+ });
48
+ const bundler = new Bundler(runtime);
49
+ const { code, errors } = await bundler.run(entry, { write: false, logLevel: "silent" });
50
+ expect((errors || []).length).toEqual(0);
51
+ expect(code).toInclude("greeting");
52
+ expect(code).toInclude("hi");
53
+ });
54
+
55
+ test("run() with exports option rewrites entry to a re-export via @exports namespace", async () => {
56
+ const dir = mkdtempSync(join(tmpdir(), "vistta-bundler-"));
57
+ const entry = join(dir, "page.jsx");
58
+ writeFileSync(entry, "export default function Page() { return null; }\n");
59
+
60
+ const runtime = new Runtime({ loaders: [], resolvers: ["jsx"], options: {} });
61
+ const bundler = new Bundler(runtime);
62
+ const { code, errors } = await bundler.run(entry, {
63
+ write: false,
64
+ logLevel: "silent",
65
+ exports: "default",
66
+ });
67
+ expect((errors || []).length).toEqual(0);
68
+ expect(code).toInclude("Page");
69
+ });
70
+
71
+ test("run() exposes window namespace defaults and named exports", async () => {
72
+ const dir = mkdtempSync(join(tmpdir(), "vistta-bundler-"));
73
+ const entry = join(dir, "entry.js");
74
+ writeFileSync(
75
+ entry,
76
+ 'import Files, { Files as NamedFiles } from "@vistta/connect/files";\nimport { equals } from "@vistta/connect/utils";\nexport const getFile = () => Files.get();\nexport const getNamedFile = () => NamedFiles.get();\nexport const compare = (left, right) => equals(left, right);\n',
77
+ );
78
+
79
+ const runtime = new Runtime({ loaders: [], resolvers: ["js"], options: {} });
80
+ const bundler = new Bundler(runtime);
81
+ const { code, errors } = await bundler.run(entry, {
82
+ write: false,
83
+ logLevel: "silent",
84
+ format: "cjs",
85
+ paths: {
86
+ "@vistta/connect/files": { path: "files", namespace: "window" },
87
+ "@vistta/connect/utils": { path: "utils", namespace: "window" },
88
+ },
89
+ });
90
+ expect((errors || []).length).toEqual(0);
91
+
92
+ const browser = /** @type {any} */ (globalThis);
93
+ browser.window = {};
94
+ const module = { exports: {} };
95
+ new Function("window", "module", "exports", code)(browser.window, module, module.exports);
96
+ browser.window.files = { default: { get: () => "file" }, Files: { get: () => "named" } };
97
+ browser.window.utils = { equals: (left, right) => left === right };
98
+ expect(module.exports.getFile()).toEqual("file");
99
+ expect(module.exports.getNamedFile()).toEqual("named");
100
+ expect(module.exports.compare("utils", "utils")).toEqual(true);
101
+ });
102
+
103
+ test("run() throws when source has syntax errors", async () => {
104
+ const dir = mkdtempSync(join(tmpdir(), "vistta-bundler-"));
105
+ const entry = join(dir, "entry.js");
106
+ writeFileSync(entry, "this is not valid javascript !@#$\n");
107
+
108
+ const runtime = new Runtime({ loaders: [], resolvers: ["js"], options: {} });
109
+ const bundler = new Bundler(runtime);
110
+ let caught;
111
+ try {
112
+ await bundler.run(entry, { write: false, logLevel: "silent" });
113
+ } catch (e) {
114
+ caught = e;
115
+ }
116
+ expect(caught.message).toMatch("Build failed");
117
+ });
118
+ });
@@ -0,0 +1,217 @@
1
+ import assert from "node:assert/strict";
2
+ import { spawnSync } from "node:child_process";
3
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { loadProjectConfig } from "../config.js";
8
+ import { planTasks, runChecks, runTasks, toolFiles } from "../tools.js";
9
+
10
+ function fixture(files) {
11
+ const cwd = mkdtempSync(join(tmpdir(), "vistta-config-"));
12
+ for (const [path, content] of Object.entries(files)) {
13
+ mkdirSync(dirname(join(cwd, path)), { recursive: true });
14
+ writeFileSync(join(cwd, path), typeof content === "string" ? content : JSON.stringify(content));
15
+ }
16
+ return cwd;
17
+ }
18
+
19
+ suite("CLI configuration", async () => {
20
+ await test("loads JSONC, relative presets, legacy settings, arrays, and setting sources", () => {
21
+ const cwd = fixture({
22
+ "presets/base.json": {
23
+ compilerOptions: { checkJs: true },
24
+ include: ["src/**/*.js"],
25
+ cliOptions: { check: { format: false, lint: true }, commands: { custom: "./custom.js" } },
26
+ cli: { format: { options: { semi: false } } },
27
+ },
28
+ "presets/custom.js": "export default class {}",
29
+ "tsconfig.json":
30
+ '{ // comment\n "extends": "./presets/base", "include": ["app/*.js"], "cli": { "check": { "types": false }, "format": { "options": { "singleQuote": true } } }, }',
31
+ });
32
+ try {
33
+ const result = loadProjectConfig(cwd);
34
+ assert.deepEqual(result.config.cli.check, { format: false, lint: true, types: false });
35
+ assert.deepEqual(result.config.include, ["app/*.js"]);
36
+ assert.deepEqual(result.config.cli.format.options, { semi: false, singleQuote: true });
37
+ assert.equal(fileURLToPath(result.config.cli.commands.custom), join(cwd, "presets/custom.js"));
38
+ assert.equal(result.sources["cli.format.options.semi"], join(cwd, "presets/base.json"));
39
+ assert.equal(result.sources["cli.format.options.singleQuote"], join(cwd, "tsconfig.json"));
40
+ } finally {
41
+ rmSync(cwd, { recursive: true, force: true });
42
+ }
43
+ });
44
+
45
+ await test("supports package presets, jsconfig, and multiple ordered parents", () => {
46
+ const cwd = fixture({
47
+ "node_modules/preset/package.json": { name: "preset", exports: { "./config": "./config.json" } },
48
+ "node_modules/preset/config.json": { cli: { format: { options: { semi: true, singleQuote: false } } } },
49
+ "second.json": { cli: { format: { options: { semi: false } } } },
50
+ "jsconfig.json": { extends: ["preset/config", "./second.json"] },
51
+ });
52
+ try {
53
+ assert.deepEqual(loadProjectConfig(cwd).config.cli.format.options, { semi: false, singleQuote: false });
54
+ } finally {
55
+ rmSync(cwd, { recursive: true, force: true });
56
+ }
57
+ });
58
+
59
+ await test("rejects malformed, circular, and invalid configuration", () => {
60
+ const cwd = fixture({ "tsconfig.json": "{broken}" });
61
+ try {
62
+ assert.throws(() => loadProjectConfig(cwd), /Invalid configuration/);
63
+ writeFileSync(join(cwd, "tsconfig.json"), JSON.stringify({ extends: "./tsconfig.json" }));
64
+ assert.throws(() => loadProjectConfig(cwd), /Circular/);
65
+ for (const cli of [
66
+ { check: { types: "yes" } },
67
+ { lint: { engine: "unknown" } },
68
+ { format: { include: "*.js" } },
69
+ { tasks: { build: { dependsOn: "test" } } },
70
+ ]) {
71
+ writeFileSync(join(cwd, "tsconfig.json"), JSON.stringify({ cli }));
72
+ assert.throws(() => loadProjectConfig(cwd));
73
+ }
74
+ } finally {
75
+ rmSync(cwd, { recursive: true, force: true });
76
+ }
77
+ });
78
+
79
+ await test("resolves inherited file patterns relative to their config and excludes dependencies", () => {
80
+ const cwd = fixture({
81
+ "preset/base.json": { include: ["src/**/*.js"], exclude: ["src/ignored/**"] },
82
+ "tsconfig.json": { extends: "./preset/base.json" },
83
+ "preset/src/main.js": "export {};",
84
+ "preset/src/ignored/skip.js": "export {};",
85
+ "preset/src/node_modules/skip/index.js": "export {};",
86
+ });
87
+ try {
88
+ assert.deepEqual(toolFiles(loadProjectConfig(cwd), "lint", [], cwd), [join(cwd, "preset/src/main.js")]);
89
+ assert.deepEqual(toolFiles(loadProjectConfig(cwd), "lint", ["preset/src/ignored/*.js"], cwd), []);
90
+ } finally {
91
+ rmSync(cwd, { recursive: true, force: true });
92
+ }
93
+ });
94
+
95
+ await test("orders dependencies once, rejects cycles and missing tasks, and stops on failure", async () => {
96
+ const tasks = {
97
+ validate: { dependsOn: ["lint", "test"], script: "finish" },
98
+ test: { dependsOn: ["lint"], script: "unit" },
99
+ };
100
+ const plan = planTasks("validate", tasks, { unit: "node unit.js", finish: "node finish.js" });
101
+ assert.deepEqual(
102
+ plan.map((task) => task.name),
103
+ ["lint", "test", "validate"],
104
+ );
105
+ assert.throws(() => planTasks("bad", { bad: { dependsOn: ["bad"] } }), /Circular/);
106
+ assert.throws(() => planTasks("unknown"), /Unknown/);
107
+ assert.throws(() => planTasks("build"), /Unknown/);
108
+ assert.deepEqual(planTasks("build", {}, {}, ["build"]), [{ name: "build", command: "build" }]);
109
+ assert.throws(() => planTasks("bad", { bad: { script: "missing" } }), /Unknown package script/);
110
+ const calls = [];
111
+ assert.equal(
112
+ await runTasks(plan, process.cwd(), [], async (args) => {
113
+ calls.push(args);
114
+ return 2;
115
+ }),
116
+ 2,
117
+ );
118
+ assert.equal(calls.length, 1);
119
+ calls.length = 0;
120
+ assert.equal(
121
+ await runTasks(plan, process.cwd(), ["--flag"], async (args) => {
122
+ calls.push(args);
123
+ return 0;
124
+ }),
125
+ 0,
126
+ );
127
+ assert.deepEqual(calls.at(-1), ["--run", "finish", "--", "--flag"]);
128
+ });
129
+ });
130
+
131
+ suite("CLI tool adapters", async () => {
132
+ await test("formats and lints temporary files with project-local tools and checks types without emitting", async () => {
133
+ const root = resolve(import.meta.dirname, "../../..");
134
+ const cwd = fixture({
135
+ "package.json": { name: "fixture", type: "module" },
136
+ "tsconfig.json": {
137
+ include: ["src/*.js"],
138
+ compilerOptions: { allowJs: true, checkJs: true, noEmit: true, types: [], target: "ES2022" },
139
+ cli: {
140
+ lint: {
141
+ engine: "eslint",
142
+ config: "./eslint.config.mjs",
143
+ rules: { "no-unused-vars": "error", semi: ["error", "always"] },
144
+ },
145
+ format: { engine: "prettier", config: "./prettier.config.mjs", options: { semi: true, singleQuote: false } },
146
+ },
147
+ },
148
+ "src/main.js": "export const value=1",
149
+ "eslint.config.mjs": 'export default [{ rules: { semi: ["error", "never"] } }];',
150
+ "prettier.config.mjs": "export default { semi: false };",
151
+ "node_modules/eslint/package.json": { main: join(root, "node_modules/eslint/lib/api.js") },
152
+ "node_modules/prettier/package.json": { main: join(root, "node_modules/prettier/index.cjs") },
153
+ "node_modules/typescript/package.json": { main: join(root, "node_modules/typescript/lib/typescript.js") },
154
+ });
155
+ try {
156
+ const project = loadProjectConfig(cwd);
157
+ assert.equal(await runChecks(["format"], project, { cwd }), 1);
158
+ assert.equal(readFileSync(join(cwd, "src/main.js"), "utf8"), "export const value=1");
159
+ assert.equal(await runChecks(["format"], project, { cwd, fix: true }), 0);
160
+ assert.equal(readFileSync(join(cwd, "src/main.js"), "utf8"), "export const value = 1;\n");
161
+ assert.equal(await runChecks(["lint", "types"], project, { cwd }), 0);
162
+ writeFileSync(join(cwd, "src/main.js"), "const unused = 1;\nexport const value = 1;\nvalue();\n");
163
+ assert.equal(await runChecks(["lint", "types"], project, { cwd }), 1);
164
+ assert.equal(existsSync(join(cwd, "src/main.d.ts")), false);
165
+ } finally {
166
+ rmSync(cwd, { recursive: true, force: true });
167
+ }
168
+ });
169
+ });
170
+
171
+ suite("CLI public commands", async () => {
172
+ await test("propagates check, fix, config, and task results through the executable", () => {
173
+ const root = resolve(import.meta.dirname, "../../..");
174
+ const bin = fileURLToPath(new URL("../bin.js", import.meta.url));
175
+ const cwd = fixture({
176
+ "package.json": {
177
+ name: "fixture",
178
+ scripts: { smoke: "node success.js", fail: "node failure.js", args: "node arguments.js" },
179
+ },
180
+ "tsconfig.json": {
181
+ files: ["main.js"],
182
+ cli: {
183
+ check: { lint: false, types: false },
184
+ format: { options: { semi: true } },
185
+ tasks: { validate: { dependsOn: ["check"], script: "smoke" } },
186
+ },
187
+ },
188
+ "main.js": "const value=1",
189
+ "success.js": "process.exit(0);",
190
+ "failure.js": "process.exit(7);",
191
+ "arguments.js": 'if (process.argv[2] !== "--help") process.exit(3); console.log("FORWARDED");',
192
+ "node_modules/prettier/package.json": { main: join(root, "node_modules/prettier/index.cjs") },
193
+ });
194
+ const invoke = (...args) => spawnSync(process.execPath, [bin, ...args], { cwd, encoding: "utf8" });
195
+ try {
196
+ assert.equal(invoke("check").status, 1);
197
+ const fixed = invoke("check", "--fix");
198
+ assert.equal(fixed.status, 0, fixed.stdout + fixed.stderr);
199
+ assert.equal(readFileSync(join(cwd, "main.js"), "utf8"), "const value = 1;\n");
200
+ const config = invoke("config", "--sources");
201
+ assert.equal(config.status, 0, config.stdout + config.stderr);
202
+ const effective = JSON.parse(config.stdout.slice(config.stdout.indexOf("{")));
203
+ assert.equal(effective.config.cli.check.lint, false);
204
+ assert.equal(effective.sources["cli.check.lint"], join(cwd, "tsconfig.json"));
205
+ const task = invoke("task", "validate");
206
+ assert.equal(task.status, 0, task.stdout + task.stderr);
207
+ assert.equal(invoke("run", "smoke").status, 0);
208
+ const forwarded = invoke("task", "args", "--", "--help");
209
+ assert.equal(forwarded.status, 0, forwarded.stdout + forwarded.stderr);
210
+ assert.match(forwarded.stdout, /FORWARDED/);
211
+ assert.notEqual(invoke("task", "fail").status, 0);
212
+ assert.notEqual(invoke("task", "missing").status, 0);
213
+ } finally {
214
+ rmSync(cwd, { recursive: true, force: true });
215
+ }
216
+ });
217
+ });
@@ -1,4 +1,9 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
1
5
  import { Command } from "../classes/command.js";
6
+ import { getOutdatedPackages } from "../utils.js";
2
7
 
3
8
  suite("CLI", () => {
4
9
  let cmd;
@@ -36,3 +41,138 @@ suite("CLI", () => {
36
41
  expect(process.env["BAR"]).toEqual("baz");
37
42
  });
38
43
  });
44
+
45
+ async function checkNpmReport(report, code, packages = {}, workspace = "") {
46
+ const dirname = mkdtempSync(join(tmpdir(), "vistta-outdated-"));
47
+ const npmPath = process.env.npm_execpath;
48
+ try {
49
+ writeFileSync(join(dirname, "package.json"), JSON.stringify({ name: "fixture", private: true }));
50
+ writeFileSync(join(dirname, "package-lock.json"), JSON.stringify({ packages }));
51
+ const script = join(dirname, "npm.cjs");
52
+ writeFileSync(
53
+ script,
54
+ [
55
+ "const assert = require('node:assert/strict');",
56
+ "assert.deepEqual(process.argv.slice(2), ['outdated', '--json', '--long', '--include=dev']);",
57
+ `process.stdout.write(${JSON.stringify(JSON.stringify(report))});`,
58
+ `process.exit(${code});`,
59
+ ].join("\n"),
60
+ );
61
+ process.env.npm_execpath = script;
62
+ const cwd = join(dirname, workspace);
63
+ if (workspace) {
64
+ mkdirSync(cwd, { recursive: true });
65
+ writeFileSync(join(cwd, "package.json"), JSON.stringify({ name: "workspace" }));
66
+ }
67
+ return { dirname, modules: await getOutdatedPackages(cwd) };
68
+ } finally {
69
+ if (npmPath === undefined) delete process.env.npm_execpath;
70
+ else process.env.npm_execpath = npmPath;
71
+ rmSync(dirname, { recursive: true, force: true });
72
+ }
73
+ }
74
+
75
+ suite("CLI outdated", async () => {
76
+ await test("uses npm wanted versions and preserves separate workspace consumers", async () => {
77
+ const { dirname, modules } = await checkNpmReport(
78
+ {
79
+ shared: [
80
+ { current: "1.0.0", wanted: "1.5.0", latest: "2.0.0", type: "dependencies", dependedByLocation: "packages/first" },
81
+ {
82
+ current: "1.0.0",
83
+ wanted: "1.0.0",
84
+ latest: "2.0.0",
85
+ type: "devDependencies",
86
+ dependedByLocation: "packages/second",
87
+ },
88
+ ],
89
+ linked: { current: "2.0.0", wanted: "1.0.0", latest: "1.0.0", type: "dependencies" },
90
+ peer: { current: "1.0.0", wanted: "2.0.0", latest: "2.0.0", type: "peerDependencies" },
91
+ },
92
+ 1,
93
+ { "node_modules/linked": { link: true } },
94
+ );
95
+ assert.deepEqual(modules, [
96
+ {
97
+ name: "shared",
98
+ package: join(dirname, "packages/first/package.json"),
99
+ current: "1.0.0",
100
+ wanted: "1.5.0",
101
+ latest: "2.0.0",
102
+ dev: false,
103
+ },
104
+ {
105
+ name: "shared",
106
+ package: join(dirname, "packages/second/package.json"),
107
+ current: "1.0.0",
108
+ wanted: "1.0.0",
109
+ latest: "2.0.0",
110
+ dev: true,
111
+ },
112
+ ]);
113
+ });
114
+
115
+ await test("accepts an empty npm report", async () => {
116
+ const { modules } = await checkNpmReport({}, 0);
117
+ assert.deepEqual(modules, []);
118
+ });
119
+
120
+ await test("resolves workspace owners relative to the lockfile when invoked inside a package", async () => {
121
+ const { dirname, modules } = await checkNpmReport(
122
+ {
123
+ dependency: {
124
+ current: "1.0.0",
125
+ wanted: "1.1.0",
126
+ latest: "1.1.0",
127
+ type: "dependencies",
128
+ dependedByLocation: "packages/child",
129
+ },
130
+ },
131
+ 1,
132
+ {},
133
+ "packages/child",
134
+ );
135
+ assert.equal(modules[0].package, join(dirname, "packages/child/package.json"));
136
+ });
137
+
138
+ await test("surfaces npm errors instead of reporting everything up to date", async () => {
139
+ await assert.rejects(checkNpmReport({ error: { summary: "Registry unavailable" } }, 1), /Registry unavailable/);
140
+ });
141
+
142
+ await test("limits package checks to their own dependency consumers", async () => {
143
+ const { dirname, modules } = await checkNpmReport(
144
+ {
145
+ root: { current: "1.0.0", wanted: "1.1.0", latest: "1.1.0", type: "dependencies" },
146
+ shared: [
147
+ {
148
+ current: "1.0.0",
149
+ wanted: "1.1.0",
150
+ latest: "1.1.0",
151
+ type: "devDependencies",
152
+ dependedByLocation: "packages/child",
153
+ },
154
+ {
155
+ current: "1.0.0",
156
+ wanted: "1.2.0",
157
+ latest: "1.2.0",
158
+ type: "dependencies",
159
+ dependedByLocation: "packages/sibling",
160
+ },
161
+ ],
162
+ },
163
+ 1,
164
+ {},
165
+ "packages/child",
166
+ );
167
+ assert.deepEqual(modules, [
168
+ {
169
+ name: "shared",
170
+ package: join(dirname, "packages/child/package.json"),
171
+ current: "1.0.0",
172
+ wanted: "1.1.0",
173
+ latest: "1.1.0",
174
+ dev: true,
175
+ },
176
+ ]);
177
+ });
178
+ });
@@ -0,0 +1,23 @@
1
+ import { Loader } from "../classes/loader.js";
2
+
3
+ suite("Loader", () => {
4
+ test("base load() throws 'must be implemented'", () => {
5
+ const loader = new Loader({});
6
+ expect(() => loader.load("source", {})).toThrow("Method 'load' must be implemented.");
7
+ });
8
+
9
+ test("subclass can override load()", () => {
10
+ let captured;
11
+ class Custom extends Loader {
12
+ load(source, properties) {
13
+ captured = { source, properties };
14
+ return { code: "ok" };
15
+ }
16
+ }
17
+ const loader = new Custom({ x: 1 });
18
+ const result = loader.load("input", { type: "json" });
19
+ expect(result.code).toEqual("ok");
20
+ expect(captured.source).toEqual("input");
21
+ expect(captured.properties.type).toEqual("json");
22
+ });
23
+ });