@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/LICENSE +674 -188
- package/README.md +150 -11
- package/bin.js +14 -4
- package/classes/bundler.js +172 -36
- package/classes/loader.js +1 -1
- package/classes/runtime.js +25 -15
- package/classes/test-runner.js +210 -0
- package/commands/bundle.js +4 -3
- package/commands/default.js +3 -1
- package/commands/project.js +110 -74
- package/commands/quality.js +52 -0
- package/commands/task.js +23 -0
- package/commands/test.js +39 -171
- package/config.js +128 -0
- package/config.schema.json +150 -0
- package/global.d.ts +68 -0
- package/index.js +4 -0
- package/loaders/bundler.js +3 -1
- package/loaders/index.js +2 -2
- package/main.js +1 -1
- package/package.json +17 -14
- package/register.js +15 -7
- package/tests/bundler.test.js +118 -0
- package/tests/config.test.js +217 -0
- package/tests/index.test.js +140 -0
- package/tests/loader.test.js +23 -0
- package/tests/project.test.js +116 -0
- package/tests/runtime.test.js +76 -0
- package/tests/test-runner.test.js +83 -0
- package/tools.js +210 -0
- package/tsconfig.json +4 -5
- package/utils.js +122 -108
- package/.gitea/workflows/prerelease.yml +0 -62
- package/.gitea/workflows/release.yml +0 -124
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
function project(report = {}) {
|
|
9
|
+
const cwd = mkdtempSync(join(tmpdir(), "vistta-project-"));
|
|
10
|
+
const files = {
|
|
11
|
+
"package.json": { name: "fixture", version: "1.0.0", dependencies: { latest: "^1.0.0" } },
|
|
12
|
+
"package-lock.json": { packages: {} },
|
|
13
|
+
"packages/first/package.json": { name: "first", dependencies: { shared: "^1.0.0", untouched: "^1.0.0" } },
|
|
14
|
+
"packages/second/package.json": { name: "second", devDependencies: { shared: "^2.0.0" } },
|
|
15
|
+
};
|
|
16
|
+
for (const [path, content] of Object.entries(files)) {
|
|
17
|
+
mkdirSync(dirname(join(cwd, path)), { recursive: true });
|
|
18
|
+
writeFileSync(join(cwd, path), JSON.stringify(content));
|
|
19
|
+
}
|
|
20
|
+
const npm = join(cwd, "npm.cjs");
|
|
21
|
+
writeFileSync(
|
|
22
|
+
npm,
|
|
23
|
+
`process.stdout.write(${JSON.stringify(JSON.stringify(report))}); process.exit(${Object.keys(report).length ? 1 : 0});`,
|
|
24
|
+
);
|
|
25
|
+
return {
|
|
26
|
+
cwd,
|
|
27
|
+
read: (path) => JSON.parse(readFileSync(join(cwd, path), "utf8")),
|
|
28
|
+
invoke: (...args) =>
|
|
29
|
+
spawnSync(process.execPath, [fileURLToPath(new URL("../bin.js", import.meta.url)), "project", ...args], {
|
|
30
|
+
cwd,
|
|
31
|
+
encoding: "utf8",
|
|
32
|
+
env: { ...process.env, npm_execpath: npm },
|
|
33
|
+
}),
|
|
34
|
+
clean: () => rmSync(cwd, { recursive: true, force: true }),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const report = {
|
|
39
|
+
shared: [
|
|
40
|
+
{ current: "1.0.0", wanted: "1.5.0", latest: "3.0.0", type: "dependencies", dependedByLocation: "packages/first" },
|
|
41
|
+
{ current: "2.0.0", wanted: "2.8.0", latest: "3.0.0", type: "devDependencies", dependedByLocation: "packages/second" },
|
|
42
|
+
],
|
|
43
|
+
latest: { current: "1.0.0", wanted: "1.0.0", latest: "2.0.0", type: "dependencies" },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
suite("Project command", () => {
|
|
47
|
+
test("shows consistent help and metadata and rejects unknown commands and arguments", () => {
|
|
48
|
+
const fixture = project();
|
|
49
|
+
try {
|
|
50
|
+
assert.match(fixture.invoke().stdout, /Project metadata and dependency maintenance/);
|
|
51
|
+
assert.match(fixture.invoke("update", "--help").stdout, /may include major upgrades/);
|
|
52
|
+
assert.match(fixture.invoke("help", "patch").stdout, /not patch-only/);
|
|
53
|
+
assert.match(fixture.invoke("name").stdout, /fixture/);
|
|
54
|
+
assert.match(fixture.invoke("version").stdout, /1\.0\.0/);
|
|
55
|
+
assert.equal(fixture.invoke("version", "patch").status, 0);
|
|
56
|
+
assert.equal(fixture.read("package.json").version, "1.0.1");
|
|
57
|
+
assert.notEqual(fixture.invoke("typo").status, 0);
|
|
58
|
+
assert.notEqual(fixture.invoke("update", "--typo").status, 0);
|
|
59
|
+
assert.notEqual(fixture.invoke("name", "extra").status, 0);
|
|
60
|
+
} finally {
|
|
61
|
+
fixture.clean();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("reports versions and manifests and preserves informational-only exit status", () => {
|
|
66
|
+
const fixture = project(report);
|
|
67
|
+
try {
|
|
68
|
+
const result = fixture.invoke("outdated");
|
|
69
|
+
assert.equal(result.status, 1);
|
|
70
|
+
assert.match(result.stdout, /Current\s+Wanted\s+Latest/);
|
|
71
|
+
assert.match(result.stdout, /1\.5\.0/);
|
|
72
|
+
assert.match(result.stdout, /packages[\\/]second[\\/]package\.json/);
|
|
73
|
+
assert.match(result.stdout, /2 compatible update/);
|
|
74
|
+
const informational = fixture.invoke("outdated", "latest");
|
|
75
|
+
assert.equal(informational.status, 0);
|
|
76
|
+
assert.doesNotMatch(informational.stdout, /shared/);
|
|
77
|
+
assert.match(fixture.invoke("outdated", "absent").stdout, /No outdated dependency/);
|
|
78
|
+
} finally {
|
|
79
|
+
fixture.clean();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("previews compatible changes and writes each workspace's own target version", () => {
|
|
84
|
+
const fixture = project(report);
|
|
85
|
+
try {
|
|
86
|
+
const preview = fixture.invoke("update", "shared", "--compatible", "--dry-run");
|
|
87
|
+
assert.equal(preview.status, 0, preview.stdout + preview.stderr);
|
|
88
|
+
assert.match(preview.stdout, /Dry run: no files changed/);
|
|
89
|
+
assert.equal(fixture.read("packages/first/package.json").dependencies.shared, "^1.0.0");
|
|
90
|
+
assert.equal(fixture.read("packages/second/package.json").devDependencies.shared, "^2.0.0");
|
|
91
|
+
const updated = fixture.invoke("patch", "shared");
|
|
92
|
+
assert.equal(updated.status, 0, updated.stdout + updated.stderr);
|
|
93
|
+
assert.equal(fixture.read("packages/first/package.json").dependencies.shared, "^1.5.0");
|
|
94
|
+
assert.equal(fixture.read("packages/first/package.json").dependencies.untouched, "^1.0.0");
|
|
95
|
+
assert.equal(fixture.read("packages/second/package.json").devDependencies.shared, "^2.8.0");
|
|
96
|
+
assert.equal(fixture.read("package.json").dependencies.latest, "^1.0.0");
|
|
97
|
+
assert.match(updated.stdout, /Run npm install/);
|
|
98
|
+
assert.match(fixture.invoke("patch", "shared").stdout, /No compatible dependency range changes needed/);
|
|
99
|
+
} finally {
|
|
100
|
+
fixture.clean();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("keeps latest update behavior explicit and reports missing installed dependencies", () => {
|
|
105
|
+
const fixture = project({ ...report, missing: { wanted: "1.0.0", latest: "1.0.0", type: "dependencies" } });
|
|
106
|
+
try {
|
|
107
|
+
assert.equal(fixture.invoke("outdated", "missing").status, 1);
|
|
108
|
+
assert.match(fixture.invoke("outdated", "missing").stdout, /MISSING/);
|
|
109
|
+
assert.equal(fixture.invoke("update", "shared").status, 0);
|
|
110
|
+
assert.equal(fixture.read("packages/first/package.json").dependencies.shared, "^3.0.0");
|
|
111
|
+
assert.equal(fixture.read("packages/second/package.json").devDependencies.shared, "^3.0.0");
|
|
112
|
+
} finally {
|
|
113
|
+
fixture.clean();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { Runtime } from "../classes/runtime.js";
|
|
5
|
+
|
|
6
|
+
suite("Runtime", () => {
|
|
7
|
+
test("resolve yields distinct URLs for plain vs bundler imports of the same file", async () => {
|
|
8
|
+
const runtime = new Runtime({ loaders: [], resolvers: [], options: {} });
|
|
9
|
+
const file = import.meta.filename || new URL(import.meta.url).pathname.replace(/^\//, "");
|
|
10
|
+
const captured = [];
|
|
11
|
+
const next = (url) => {
|
|
12
|
+
captured.push(url);
|
|
13
|
+
return url;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
await runtime.resolve(file, {}, next);
|
|
17
|
+
await runtime.resolve(file, { importAttributes: { type: "bundler" } }, next);
|
|
18
|
+
await runtime.resolve(file, { conditions: ["bundler"] }, next);
|
|
19
|
+
|
|
20
|
+
expect(captured[0]).not.toEqual(captured[1]);
|
|
21
|
+
expect(captured[0]).not.toEqual(captured[2]);
|
|
22
|
+
expect(captured[1]).toEqual(captured[2]);
|
|
23
|
+
expect(/\?__bundler__$/.test(captured[1])).toEqual(true);
|
|
24
|
+
expect(/\?__bundler__$/.test(captured[0])).toEqual(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("resolve strips bundler suffix from parentURL when resolving relatives", async () => {
|
|
28
|
+
const runtime = new Runtime({ loaders: [], resolvers: [], options: {} });
|
|
29
|
+
const here = new URL(".", import.meta.url).href;
|
|
30
|
+
const parentURL = here + "runtime.test.js?__bundler__";
|
|
31
|
+
let captured;
|
|
32
|
+
await runtime.resolve("./runtime.test.js", { parentURL, importAttributes: { type: "bundler" } }, (url) => {
|
|
33
|
+
captured = url;
|
|
34
|
+
return url;
|
|
35
|
+
});
|
|
36
|
+
expect(captured.includes("?__bundler__?")).toEqual(false);
|
|
37
|
+
expect(/\?__bundler__$/.test(captured)).toEqual(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("resolve detects bundler mode from importAttributes.type even without conditions", async () => {
|
|
41
|
+
const runtime = new Runtime({ loaders: [], resolvers: [], options: {} });
|
|
42
|
+
const file = import.meta.filename || new URL(import.meta.url).pathname.replace(/^\//, "");
|
|
43
|
+
const captured = [];
|
|
44
|
+
await runtime.resolve(file, { importAttributes: { type: "bundler" } }, (url) => {
|
|
45
|
+
captured.push(url);
|
|
46
|
+
return url;
|
|
47
|
+
});
|
|
48
|
+
expect(/\?__bundler__$/.test(captured[0])).toEqual(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("resolve marks context.file for absolute bundler imports", async () => {
|
|
52
|
+
const runtime = new Runtime({ loaders: [], resolvers: [], options: {} });
|
|
53
|
+
const dir = mkdtempSync(join(tmpdir(), "vistta-runtime-"));
|
|
54
|
+
const file = join(dir, "page.jsx");
|
|
55
|
+
writeFileSync(file, "export default 1;");
|
|
56
|
+
const ctx = { importAttributes: { type: "bundler" } };
|
|
57
|
+
await runtime.resolve(file, ctx, (url) => url);
|
|
58
|
+
expect(ctx.file).toEqual(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("resolve preserves non-bundler query string params", async () => {
|
|
62
|
+
const runtime = new Runtime({ loaders: [], resolvers: ["js"], options: {} });
|
|
63
|
+
const dir = mkdtempSync(join(tmpdir(), "vistta-runtime-"));
|
|
64
|
+
const file = join(dir, "data.js");
|
|
65
|
+
writeFileSync(file, "export default 1;");
|
|
66
|
+
const here = new URL(import.meta.url);
|
|
67
|
+
const hereURL = new URL(".", here).href;
|
|
68
|
+
const captured = [];
|
|
69
|
+
await runtime.resolve("./data.js?type=json", { parentURL: hereURL }, (url) => {
|
|
70
|
+
captured.push(url);
|
|
71
|
+
return url;
|
|
72
|
+
});
|
|
73
|
+
expect(captured[0].includes("type=json")).toEqual(true);
|
|
74
|
+
expect(/\?__bundler__$/.test(captured[0])).toEqual(false);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { TestRunner } from "../classes/test-runner.js";
|
|
2
|
+
|
|
3
|
+
suite("TestRunner", () => {
|
|
4
|
+
test("suite().test() registers a passing test", async () => {
|
|
5
|
+
const runner = new TestRunner();
|
|
6
|
+
runner.suite("s", () => {
|
|
7
|
+
runner.test("t", () => 1);
|
|
8
|
+
});
|
|
9
|
+
const results = await runner.run();
|
|
10
|
+
expect(results[0].name).toEqual("s");
|
|
11
|
+
expect(results[0].tests[0].name).toEqual("t");
|
|
12
|
+
expect(results[0].tests[0].status).toEqual("pass");
|
|
13
|
+
expect(results[0].tests[0].result).toEqual(1);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("failing test reports status=fail with the error", async () => {
|
|
17
|
+
const runner = new TestRunner();
|
|
18
|
+
runner.suite("s", () => {
|
|
19
|
+
runner.test("boom", () => {
|
|
20
|
+
throw new Error("kaboom");
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
const results = await runner.run();
|
|
24
|
+
expect(results[0].tests[0].status).toEqual("fail");
|
|
25
|
+
expect(results[0].tests[0].error.message).toEqual("kaboom");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("async tests are awaited before status is set", async () => {
|
|
29
|
+
const runner = new TestRunner();
|
|
30
|
+
runner.suite("s", () => {
|
|
31
|
+
runner.test("async", async () => {
|
|
32
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
33
|
+
return "done";
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
const results = await runner.run();
|
|
37
|
+
expect(results[0].tests[0].status).toEqual("pass");
|
|
38
|
+
expect(results[0].tests[0].result).toEqual("done");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("test.skip is recorded as 'skip'", async () => {
|
|
42
|
+
const runner = new TestRunner();
|
|
43
|
+
runner.suite("s", () => {
|
|
44
|
+
runner.test.skip("skipped", () => {});
|
|
45
|
+
});
|
|
46
|
+
const results = await runner.run();
|
|
47
|
+
expect(results[0].tests[0].status).toEqual("skip");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("only: option restricts execution to marked tests", async () => {
|
|
51
|
+
const runner = new TestRunner({ only: true });
|
|
52
|
+
runner.suite("s", () => {
|
|
53
|
+
runner.test("not run", () => "x");
|
|
54
|
+
runner.test.only("run", () => "y");
|
|
55
|
+
});
|
|
56
|
+
const results = await runner.run();
|
|
57
|
+
const names = results[0].tests.map((t) => t.name);
|
|
58
|
+
expect(names.length).toEqual(1);
|
|
59
|
+
expect(names[0]).toEqual("run");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("filter option restricts by test name (case-insensitive)", async () => {
|
|
63
|
+
const runner = new TestRunner({ filter: "MATCH" });
|
|
64
|
+
runner.suite("s", () => {
|
|
65
|
+
runner.test("will match", () => {});
|
|
66
|
+
runner.test("will skip", () => {});
|
|
67
|
+
});
|
|
68
|
+
const results = await runner.run();
|
|
69
|
+
const names = results[0].tests.map((t) => t.name);
|
|
70
|
+
expect(names.length).toEqual(1);
|
|
71
|
+
expect(names[0]).toEqual("will match");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("suite errors are swallowed and surfaced via empty tests", async () => {
|
|
75
|
+
const runner = new TestRunner();
|
|
76
|
+
runner.suite("s", () => {
|
|
77
|
+
throw new Error("setup failed");
|
|
78
|
+
});
|
|
79
|
+
const results = await runner.run();
|
|
80
|
+
expect(results.length).toEqual(1);
|
|
81
|
+
expect(results[0].tests.length).toEqual(0);
|
|
82
|
+
});
|
|
83
|
+
});
|
package/tools.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, globSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { dirname, matchesGlob, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
export async function projectTool(name, cwd) {
|
|
8
|
+
const require = createRequire(resolve(cwd, "package.json"));
|
|
9
|
+
let entry;
|
|
10
|
+
try {
|
|
11
|
+
entry = require.resolve(name);
|
|
12
|
+
} catch {
|
|
13
|
+
throw new Error(`Missing project tool "${name}". Install it locally: npm install --save-dev ${name}`);
|
|
14
|
+
}
|
|
15
|
+
const module = await import(pathToFileURL(entry).href);
|
|
16
|
+
return module.default || module;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function toolFiles(project, name, patterns = [], cwd = process.cwd()) {
|
|
20
|
+
const section = project.config.cli?.[name] || {};
|
|
21
|
+
const explicit = patterns.length > 0;
|
|
22
|
+
const source = project.sources[`cli.${name}.include`] || project.sources.include || project.file;
|
|
23
|
+
const base = explicit ? cwd : source ? dirname(source) : cwd;
|
|
24
|
+
const defaults = name === "lint" ? ["**/*.{js,jsx,mjs,cjs,ts,tsx}"] : ["**/*"];
|
|
25
|
+
const include = explicit ? patterns : section.include || project.config.include || (project.config.files ? [] : defaults);
|
|
26
|
+
const excluded = ["**/node_modules", "**/node_modules/**", "**/.git", "**/.git/**"];
|
|
27
|
+
for (const pattern of section.exclude || project.config.exclude || []) {
|
|
28
|
+
const source = project.sources[`cli.${name}.exclude`] || project.sources.exclude || project.file;
|
|
29
|
+
const path = resolve(source ? dirname(source) : cwd, pattern).replaceAll("\\", "/");
|
|
30
|
+
excluded.push(path, path + "/**");
|
|
31
|
+
}
|
|
32
|
+
const output = project.config.compilerOptions?.outDir;
|
|
33
|
+
if (output) {
|
|
34
|
+
const source = project.sources["compilerOptions.outDir"] || project.file;
|
|
35
|
+
excluded.push(resolve(source ? dirname(source) : cwd, output).replaceAll("\\", "/") + "/**");
|
|
36
|
+
}
|
|
37
|
+
const isExcluded = (entry) => {
|
|
38
|
+
const path = resolve(base, entry).replaceAll("\\", "/");
|
|
39
|
+
return (
|
|
40
|
+
path.split("/").some((part) => part === "node_modules" || part === ".git") ||
|
|
41
|
+
excluded.some((pattern) => matchesGlob(path, pattern))
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
const files = globSync(
|
|
45
|
+
include.map((pattern) => resolve(base, pattern).replaceAll("\\", "/")),
|
|
46
|
+
{
|
|
47
|
+
cwd: base,
|
|
48
|
+
exclude: isExcluded,
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
if (!explicit && !section.include) {
|
|
52
|
+
for (const file of project.config.files || []) files.push(resolve(dirname(project.sources.files || project.file), file));
|
|
53
|
+
}
|
|
54
|
+
return [...new Set(files)].filter((file) => !isExcluded(file) && statSync(file).isFile()).sort();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function lint(project, patterns, fix, cwd) {
|
|
58
|
+
const section = project.config.cli?.lint || {};
|
|
59
|
+
const { ESLint } = await projectTool("eslint", cwd);
|
|
60
|
+
const external = section.config ? (await import(section.config)).default : undefined;
|
|
61
|
+
const inline = section.rules || section.languageOptions || section.options;
|
|
62
|
+
const override = {
|
|
63
|
+
...section.options,
|
|
64
|
+
...(section.rules ? { rules: section.rules } : {}),
|
|
65
|
+
...(section.languageOptions ? { languageOptions: section.languageOptions } : {}),
|
|
66
|
+
};
|
|
67
|
+
const probe = new ESLint({ cwd });
|
|
68
|
+
const hasConfig = await probe.findConfigFile();
|
|
69
|
+
const eslint = new ESLint({
|
|
70
|
+
cwd,
|
|
71
|
+
fix,
|
|
72
|
+
...(external || inline
|
|
73
|
+
? { overrideConfig: [...(external ? (Array.isArray(external) ? external : [external]) : []), override] }
|
|
74
|
+
: {}),
|
|
75
|
+
...(external || (!hasConfig && inline) ? { overrideConfigFile: true } : {}),
|
|
76
|
+
});
|
|
77
|
+
const files = [];
|
|
78
|
+
for (const file of toolFiles(project, "lint", patterns, cwd)) if (!(await eslint.isPathIgnored(file))) files.push(file);
|
|
79
|
+
if (!files.length) return 0;
|
|
80
|
+
const results = await eslint.lintFiles(files);
|
|
81
|
+
if (fix) await ESLint.outputFixes(results);
|
|
82
|
+
const formatted = await (await eslint.loadFormatter("stylish")).format(results);
|
|
83
|
+
if (formatted) console.log(formatted);
|
|
84
|
+
return results.some((result) => result.errorCount || result.warningCount) ? 1 : 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function format(project, patterns, write, cwd) {
|
|
88
|
+
const prettier = await projectTool("prettier", cwd);
|
|
89
|
+
const section = project.config.cli?.format || {};
|
|
90
|
+
const external = section.config ? (await import(section.config)).default : undefined;
|
|
91
|
+
let failed = false;
|
|
92
|
+
const ignorePath = resolve(cwd, ".prettierignore");
|
|
93
|
+
for (const file of toolFiles(project, "format", patterns, cwd)) {
|
|
94
|
+
const info = await prettier.getFileInfo(file, {
|
|
95
|
+
...(existsSync(ignorePath) ? { ignorePath } : {}),
|
|
96
|
+
withNodeModules: false,
|
|
97
|
+
});
|
|
98
|
+
if (info.ignored || !info.inferredParser) continue;
|
|
99
|
+
const options = { ...(external || (await prettier.resolveConfig(file))), ...section.options, filepath: file };
|
|
100
|
+
const original = readFileSync(file, "utf8");
|
|
101
|
+
if (await prettier.check(original, options)) continue;
|
|
102
|
+
if (write) writeFileSync(file, await prettier.format(original, options));
|
|
103
|
+
else failed = true;
|
|
104
|
+
console.log(`${write ? "Formatted" : "Needs formatting"}: ${file}`);
|
|
105
|
+
}
|
|
106
|
+
return failed ? 1 : 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function types(project, cwd) {
|
|
110
|
+
if (!project.file) throw new Error("Type checking requires tsconfig.json or jsconfig.json");
|
|
111
|
+
const ts = await projectTool("typescript", cwd);
|
|
112
|
+
const diagnostics = [];
|
|
113
|
+
const host = { ...ts.sys, onUnRecoverableConfigFileDiagnostic: (error) => diagnostics.push(error) };
|
|
114
|
+
const parsed = ts.getParsedCommandLineOfConfigFile(project.file, { noEmit: true }, host);
|
|
115
|
+
if (parsed) {
|
|
116
|
+
diagnostics.push(...parsed.errors);
|
|
117
|
+
if (!parsed.errors.length) {
|
|
118
|
+
const program = ts.createProgram({
|
|
119
|
+
rootNames: parsed.fileNames,
|
|
120
|
+
options: parsed.options,
|
|
121
|
+
projectReferences: parsed.projectReferences,
|
|
122
|
+
});
|
|
123
|
+
diagnostics.push(...ts.getPreEmitDiagnostics(program));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (diagnostics.length)
|
|
127
|
+
console.log(
|
|
128
|
+
ts.formatDiagnosticsWithColorAndContext(diagnostics, {
|
|
129
|
+
getCanonicalFileName: (file) => file,
|
|
130
|
+
getCurrentDirectory: () => cwd,
|
|
131
|
+
getNewLine: () => "\n",
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
return diagnostics.some((error) => error.category === ts.DiagnosticCategory.Error) ? 1 : 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function runChecks(names, project, { patterns = [], fix = false, cwd = process.cwd() } = {}) {
|
|
138
|
+
const results = [];
|
|
139
|
+
const runners = { format, lint, types };
|
|
140
|
+
for (const name of names) {
|
|
141
|
+
try {
|
|
142
|
+
const code = name === "types" ? await types(project, cwd) : await runners[name](project, patterns, fix, cwd);
|
|
143
|
+
results.push({ name, code });
|
|
144
|
+
console.log(`${name}: ${code ? "failed" : "passed"}`);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
results.push({ name, code: 1 });
|
|
147
|
+
console.error(`${name}: ${error.message}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
console.log(`Checks: ${results.filter((result) => !result.code).length}/${results.length} passed`);
|
|
151
|
+
return results.some((result) => result.code) ? 1 : 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function planTasks(name, tasks = {}, scripts = {}, commands = ["check", "lint", "fmt", "types", "test", "bundle"]) {
|
|
155
|
+
const result = [];
|
|
156
|
+
const visited = new Set();
|
|
157
|
+
const active = new Set();
|
|
158
|
+
function visit(name) {
|
|
159
|
+
if (active.has(name)) throw new Error(`Circular task dependency: ${[...active, name].join(" -> ")}`);
|
|
160
|
+
if (visited.has(name)) return;
|
|
161
|
+
const task = Object.hasOwn(tasks, name) ? tasks[name] : undefined;
|
|
162
|
+
if (!task && !Object.hasOwn(scripts, name) && !commands.includes(name))
|
|
163
|
+
throw new Error(`Unknown task or package script "${name}"`);
|
|
164
|
+
active.add(name);
|
|
165
|
+
for (const dependency of task?.dependsOn || []) visit(dependency);
|
|
166
|
+
active.delete(name);
|
|
167
|
+
visited.add(name);
|
|
168
|
+
if (task?.script && !Object.hasOwn(scripts, task.script))
|
|
169
|
+
throw new Error(`Unknown package script "${task.script}" for task "${name}"`);
|
|
170
|
+
result.push({
|
|
171
|
+
name,
|
|
172
|
+
...(task ? { script: task.script } : Object.hasOwn(scripts, name) ? { script: name } : { command: name }),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
visit(name);
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function executeNode(args, cwd) {
|
|
180
|
+
return new Promise((resolve, reject) => {
|
|
181
|
+
const child = spawn(process.execPath, args, { cwd, stdio: "inherit", windowsHide: true });
|
|
182
|
+
const stop = () => child.kill("SIGINT");
|
|
183
|
+
process.on("SIGINT", stop);
|
|
184
|
+
child.on("error", (error) => {
|
|
185
|
+
process.removeListener("SIGINT", stop);
|
|
186
|
+
reject(error);
|
|
187
|
+
});
|
|
188
|
+
child.on("close", (code) => {
|
|
189
|
+
process.removeListener("SIGINT", stop);
|
|
190
|
+
resolve(code ?? 1);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function runTasks(plan, cwd, args = [], execute = executeNode) {
|
|
196
|
+
const bin = fileURLToPath(new URL("./bin.js", import.meta.url));
|
|
197
|
+
for (const task of plan) {
|
|
198
|
+
if (!task.script && !task.command) continue;
|
|
199
|
+
console.log(`Task: ${task.name}`);
|
|
200
|
+
const forwarded = task === plan.at(-1) ? args : [];
|
|
201
|
+
const code = await execute(
|
|
202
|
+
task.script
|
|
203
|
+
? ["--run", task.script, ...(forwarded.length ? ["--", ...forwarded] : [])]
|
|
204
|
+
: [bin, task.command, ...forwarded],
|
|
205
|
+
cwd,
|
|
206
|
+
);
|
|
207
|
+
if (code) return code;
|
|
208
|
+
}
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
"strict": false,
|
|
4
4
|
"noEmit": true,
|
|
5
5
|
"allowJs": true,
|
|
6
|
+
"checkJs": true,
|
|
6
7
|
"resolveJsonModule": true,
|
|
7
8
|
"module": "esnext",
|
|
8
9
|
"target": "ESNext",
|
|
@@ -10,9 +11,7 @@
|
|
|
10
11
|
"moduleResolution": "bundler",
|
|
11
12
|
"skipLibCheck": true,
|
|
12
13
|
"jsx": "react",
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
}
|
|
14
|
+
"paths": {}
|
|
15
|
+
},
|
|
16
|
+
"include": ["./**/*.js", "./global.d.ts"]
|
|
18
17
|
}
|