@jskit-ai/jskit-catalog 0.2.31 → 0.2.32
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/catalog/packages.json +258 -254
- package/package.json +3 -2
- package/scripts/jskit.mjs +22 -3
- package/scripts/material3-source.mjs +83 -0
- package/scripts/material3.mjs +42 -7
- package/scripts/navigation.mjs +195 -53
- package/scripts/package-routes.mjs +93 -0
- package/scripts/test-budget.mjs +19 -0
- package/scripts/test-command.mjs +116 -0
- package/scripts/test-runs.mjs +148 -0
- package/scripts/verify-packages.mjs +23 -1
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileExists, toPosixPath } from "./files.mjs";
|
|
4
|
+
|
|
5
|
+
// The test-run part of plain `jskit check`: every test run uses the machine's test budget, so a suite neither runs one
|
|
6
|
+
// file at a time nor overwhelms the machine. A package script runs node's test runner through `jskit test`, passes no
|
|
7
|
+
// fixed worker count, and the Playwright and Vitest configs its scripts use set their workers from testConcurrency().
|
|
8
|
+
// The one exception is a test file's own first line, `// jskit-test: alone - <reason>`, which `jskit test` honours.
|
|
9
|
+
|
|
10
|
+
const GUIDE = "https://mobily-enterprises.github.io/jskit-ai/guide/framework/typed-features#test-files-run-in-parallel";
|
|
11
|
+
const BUDGET_MODULE = "@jskit-ai/jskit-catalog/test-budget";
|
|
12
|
+
const CONFIG_EXTENSIONS = Object.freeze(["ts", "mts", "cts", "js", "mjs", "cjs"]);
|
|
13
|
+
const SCRIPT_FILE = /\.(?:[cm]?[jt]s|jsx|tsx)$/u;
|
|
14
|
+
const FIXED_PLAYWRIGHT_WORKERS = /^(?:--workers(?:=.*)?|-j.*)$/u;
|
|
15
|
+
const FIXED_VITEST_WORKERS = /^--(?:max-?[wW]orkers|min-?[wW]orkers|no-file-?[pP]arallelism|file-?[pP]arallelism)(?:=.*)?$/u;
|
|
16
|
+
|
|
17
|
+
function commandsOf(script) {
|
|
18
|
+
return String(script || "")
|
|
19
|
+
.split(/&&|\|\||[;|\n]/u)
|
|
20
|
+
.map((command) => command.trim().split(/\s+/u).map((token) => token.replace(/^['"(]+|['")]+$/gu, "")).filter(Boolean))
|
|
21
|
+
.filter((tokens) => tokens.length > 0);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const isProgram = (token, name) => token === name || token.endsWith(`/${name}`);
|
|
25
|
+
|
|
26
|
+
// node's own options come before its script; --test among them runs the test runner.
|
|
27
|
+
function runsNodeTest(tokens) {
|
|
28
|
+
return tokens.some((token, index) => {
|
|
29
|
+
if (!isProgram(token, "node")) return false;
|
|
30
|
+
for (const option of tokens.slice(index + 1)) {
|
|
31
|
+
if (option === "--test") return true;
|
|
32
|
+
if (SCRIPT_FILE.test(option)) return false;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function runnerArguments(tokens, runner) {
|
|
39
|
+
const index = tokens.findIndex((token, position) => isProgram(token, runner) &&
|
|
40
|
+
(runner !== "playwright" || tokens[position + 1] === "test"));
|
|
41
|
+
return index < 0 ? null : tokens.slice(index + 1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function configOption(args) {
|
|
45
|
+
const index = args.findIndex((token) => token === "--config" || token === "-c" || token.startsWith("--config="));
|
|
46
|
+
if (index < 0) return "";
|
|
47
|
+
return args[index].startsWith("--config=") ? args[index].slice("--config=".length) : args[index + 1] || "";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function lineOf(contents, needle, after = "") {
|
|
51
|
+
const text = String(contents);
|
|
52
|
+
const offset = text.indexOf(needle, after ? Math.max(0, text.indexOf(after)) : 0);
|
|
53
|
+
return offset < 0 ? 1 : text.slice(0, offset).split("\n").length;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function findConfig(directory, explicit, names) {
|
|
57
|
+
if (explicit) return path.resolve(directory, explicit);
|
|
58
|
+
for (const name of names) {
|
|
59
|
+
for (const extension of CONFIG_EXTENSIONS) {
|
|
60
|
+
const candidate = path.join(directory, `${name}.config.${extension}`);
|
|
61
|
+
if (await fileExists(candidate)) return candidate;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const RUNNERS = Object.freeze({
|
|
68
|
+
playwright: {
|
|
69
|
+
rule: "test/playwright-workers",
|
|
70
|
+
configNames: ["playwright"],
|
|
71
|
+
setting: "workers",
|
|
72
|
+
fixedArgument: FIXED_PLAYWRIGHT_WORKERS
|
|
73
|
+
},
|
|
74
|
+
vitest: {
|
|
75
|
+
rule: "test/vitest-workers",
|
|
76
|
+
configNames: ["vitest", "vite"],
|
|
77
|
+
setting: "maxWorkers",
|
|
78
|
+
fixedArgument: FIXED_VITEST_WORKERS
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// A config sets the runner's workers from the budget, and Vitest's never turns file parallelism off.
|
|
83
|
+
async function checkRunnerConfig({ root, configPath, directory, runner, report }) {
|
|
84
|
+
const { rule, setting, configNames } = RUNNERS[runner];
|
|
85
|
+
const wanted = `set \`${setting}: testConcurrency()\` from ${BUDGET_MODULE}`;
|
|
86
|
+
if (!configPath || !(await fileExists(configPath))) {
|
|
87
|
+
const missing = configPath || path.join(directory, `${configNames[0]}.config.*`);
|
|
88
|
+
report({ file: toPosixPath(path.relative(root, missing)), line: 1, rule,
|
|
89
|
+
message: `A script runs ${runner} without this config: add it and ${wanted}.` });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const file = toPosixPath(path.relative(root, configPath));
|
|
93
|
+
const source = await readFile(configPath, "utf8");
|
|
94
|
+
if (!source.includes(BUDGET_MODULE) || !new RegExp(`\\b${setting}\\s*:\\s*testConcurrency\\(\\s*\\)`, "u").test(source)) {
|
|
95
|
+
report({ file, line: lineOf(source, `${setting}:`), rule, message: `${runner} runs at a fixed or default worker count; ${wanted}.` });
|
|
96
|
+
}
|
|
97
|
+
if (runner === "vitest" && /\bfileParallelism\s*:\s*false\b/u.test(source)) {
|
|
98
|
+
report({ file, line: lineOf(source, "fileParallelism"), rule,
|
|
99
|
+
message: "Vitest runs one file at a time; remove fileParallelism: false and isolate the files instead." });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* @param {{ projectRoot: string, manifests: ReadonlyArray<{ absolutePath: string, contents: string, value: object }> }} input
|
|
105
|
+
*/
|
|
106
|
+
async function checkTestRuns({ projectRoot, manifests }) {
|
|
107
|
+
const root = path.resolve(projectRoot);
|
|
108
|
+
const findings = [];
|
|
109
|
+
const report = (finding) => findings.push(finding);
|
|
110
|
+
const configs = new Map();
|
|
111
|
+
for (const manifest of manifests) {
|
|
112
|
+
const file = toPosixPath(path.relative(root, manifest.absolutePath)) || "package.json";
|
|
113
|
+
const directory = path.dirname(manifest.absolutePath);
|
|
114
|
+
for (const [name, script] of Object.entries(manifest.value?.scripts || {})) {
|
|
115
|
+
const line = lineOf(manifest.contents, `"${name}"`, '"scripts"');
|
|
116
|
+
const at = (rule, message) => report({ file, line, rule, message: `scripts.${name} ${message}` });
|
|
117
|
+
for (const tokens of commandsOf(script)) {
|
|
118
|
+
if (runsNodeTest(tokens)) at("test/node-test", "runs node --test directly; use `jskit test`, which runs test files in parallel within the machine's test budget.");
|
|
119
|
+
if (tokens.some((token) => /^--test-concurrency(?:=|$)/u.test(token))) {
|
|
120
|
+
at("test/fixed-concurrency", "sets --test-concurrency; drop it, so `jskit test` takes the concurrency from the machine's test budget.");
|
|
121
|
+
}
|
|
122
|
+
for (const runner of Object.keys(RUNNERS)) {
|
|
123
|
+
const args = runnerArguments(tokens, runner);
|
|
124
|
+
if (!args) continue;
|
|
125
|
+
if (args.some((token) => RUNNERS[runner].fixedArgument.test(token))) {
|
|
126
|
+
at(RUNNERS[runner].rule, `passes ${runner} a fixed worker count; drop it, so the config's testConcurrency() sets it.`);
|
|
127
|
+
}
|
|
128
|
+
const configPath = await findConfig(directory, configOption(args), RUNNERS[runner].configNames);
|
|
129
|
+
configs.set(`${runner}\0${configPath || directory}`, { runner, configPath, directory });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
for (const config of configs.values()) await checkRunnerConfig({ root, report, ...config });
|
|
135
|
+
findings.sort((left, right) => left.file.localeCompare(right.file) || left.line - right.line);
|
|
136
|
+
return { ok: findings.length < 1, findings, manifests: manifests.length };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function formatTestRunsReport(result) {
|
|
140
|
+
const lines = result.findings.flatMap(({ file, line, rule, message }) => [`${file}:${line} ${rule} ${message}`, ` ${GUIDE}`]);
|
|
141
|
+
lines.push(result.ok
|
|
142
|
+
? `JSKIT test runs use the machine's test budget in ${result.manifests} manifest(s).`
|
|
143
|
+
: `JSKIT test runs: ${result.findings.length} error(s). Each test run takes its concurrency from the machine's test budget; ` +
|
|
144
|
+
"a file that must run alone says why on its first line (`// jskit-test: alone - <reason>`).");
|
|
145
|
+
return lines.join("\n");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export { checkTestRuns, formatTestRunsReport };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { globSync } from "node:fs";
|
|
2
3
|
import { readdir } from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -8,6 +9,8 @@ import {
|
|
|
8
9
|
discoverPackagePatterns
|
|
9
10
|
} from "./pattern-assets.mjs";
|
|
10
11
|
import { collectSingletonPackageIds, discoverFrameworkPackages } from "./framework-packages.mjs";
|
|
12
|
+
import { discoverProjectManifests, readJsonRecord } from "./project-manifests.mjs";
|
|
13
|
+
import { checkTestRuns, formatTestRunsReport } from "./test-runs.mjs";
|
|
11
14
|
|
|
12
15
|
const SCRIPT_PATH = fileURLToPath(import.meta.url);
|
|
13
16
|
const REPO_ROOT = path.resolve(path.dirname(SCRIPT_PATH), "../../..");
|
|
@@ -352,6 +355,23 @@ function validateSingletonPeerDependencies(packages) {
|
|
|
352
355
|
}
|
|
353
356
|
}
|
|
354
357
|
|
|
358
|
+
// JSKIT's own package scripts and every pattern example an application copies run tests within the test budget.
|
|
359
|
+
async function verifyTestRuns() {
|
|
360
|
+
const examples = globSync("packages/*/patterns/*/example/**/package.json", {
|
|
361
|
+
cwd: REPO_ROOT,
|
|
362
|
+
exclude: (name) => path.basename(name) === "node_modules"
|
|
363
|
+
});
|
|
364
|
+
const manifests = [
|
|
365
|
+
...await discoverProjectManifests(REPO_ROOT),
|
|
366
|
+
...await Promise.all(examples.sort().map((file) => readJsonRecord(path.join(REPO_ROOT, file))))
|
|
367
|
+
];
|
|
368
|
+
const result = await checkTestRuns({ projectRoot: REPO_ROOT, manifests });
|
|
369
|
+
if (!result.ok) {
|
|
370
|
+
throw new Error(formatTestRunsReport(result));
|
|
371
|
+
}
|
|
372
|
+
return result.manifests;
|
|
373
|
+
}
|
|
374
|
+
|
|
355
375
|
async function main() {
|
|
356
376
|
const packages = await discoverFrameworkPackages(PACKAGES_ROOT);
|
|
357
377
|
const localVersions = new Map(packages.map(({ packageJson }) => [packageJson.name, packageJson.version]));
|
|
@@ -366,9 +386,11 @@ async function main() {
|
|
|
366
386
|
serverProviderCount += packageResult.serverProviderCount;
|
|
367
387
|
}
|
|
368
388
|
assertUniquePatternIds(patterns);
|
|
389
|
+
const testManifests = await verifyTestRuns();
|
|
369
390
|
process.stdout.write(
|
|
370
391
|
`Verified ${packages.length} JSKIT runtime packages, ${serverProviderCount} loadable server providers, ` +
|
|
371
|
-
`${patterns.length} patterns,
|
|
392
|
+
`${patterns.length} patterns, ${migrationOwners.size} loadable package migrations, and the test runs of ` +
|
|
393
|
+
`${testManifests} manifests.\n`
|
|
372
394
|
);
|
|
373
395
|
}
|
|
374
396
|
|