@jskit-ai/jskit-catalog 0.2.25 → 0.2.27
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 +284 -256
- package/package.json +1 -1
- package/scripts/jskit.mjs +58 -8
- package/scripts/material3-source.mjs +222 -0
- package/scripts/material3.mjs +371 -0
- package/scripts/navigation.mjs +338 -0
package/package.json
CHANGED
package/scripts/jskit.mjs
CHANGED
|
@@ -11,24 +11,35 @@ import {
|
|
|
11
11
|
formatCheckReport,
|
|
12
12
|
formatExportSummary
|
|
13
13
|
} from "./i18n.mjs";
|
|
14
|
+
import { checkMaterial3, formatMaterial3Report, formatNavigationReport } from "./material3.mjs";
|
|
14
15
|
import { checkProject, updateProject } from "./project-packages.mjs";
|
|
15
16
|
|
|
16
17
|
const SCRIPT_PATH = fileURLToPath(import.meta.url);
|
|
17
18
|
const PACKAGE_ROOT = path.resolve(path.dirname(SCRIPT_PATH), "..");
|
|
18
19
|
const CATALOG_PATH = path.join(PACKAGE_ROOT, "catalog", "packages.json");
|
|
19
20
|
const I18N_SUBCOMMANDS = new Set(["export", "check"]);
|
|
21
|
+
// #241: plain `jskit check` fails on every nav/* rule. The JSKIT release that ships once every app has
|
|
22
|
+
// been converted sets this to true, so plain `jskit check` (and every app's verify) fails on every m3/* finding too.
|
|
23
|
+
const MATERIAL3_GATE = false;
|
|
20
24
|
|
|
21
25
|
function usage() {
|
|
22
26
|
return [
|
|
23
27
|
"Usage:",
|
|
24
28
|
" jskit update [--root <path>] [--no-install] [--manifest <path> ...]",
|
|
25
29
|
" jskit check [--root <path>] [--manifest <path> ...]",
|
|
30
|
+
" jskit check m3 [<path> ...] [--root <path>]",
|
|
26
31
|
" jskit i18n export <locale> [--root <path>]",
|
|
27
32
|
" jskit i18n check <locale> [--root <path>]",
|
|
28
33
|
"",
|
|
29
34
|
"Commands:",
|
|
30
35
|
" update Align root and workspace @jskit-ai/* declarations to this published catalog, then run npm install.",
|
|
31
|
-
" check Fail when manifests or package-lock.json contain stale, mixed, or private singleton JSKIT stacks
|
|
36
|
+
" check Fail when manifests or package-lock.json contain stale, mixed, or private singleton JSKIT stacks,",
|
|
37
|
+
" or when application source breaks a JSKIT navigation rule (nav/*): history or window.location",
|
|
38
|
+
" navigation, a hand-built in-app path, a back control other than the shell's Up arrow, an inner",
|
|
39
|
+
" page without a declared parent, or a main-navigation page that declares one.",
|
|
40
|
+
" check m3 List application UI source that departs from the JSKIT Material 3 guide, each finding with its",
|
|
41
|
+
" guide section: the whole app, or only the given files and directories (a section, or a task's",
|
|
42
|
+
" changed files). Its visual rules (m3/*) are a report; its nav/* rules are plain jskit check's.",
|
|
32
43
|
" i18n export Write the English messages of the installed JSKIT packages to src/locales/<locale>/jskit.json",
|
|
33
44
|
" for translators, keeping existing translations and extra keys.",
|
|
34
45
|
" i18n check Compare src/locales/<locale>/jskit.json with the installed packages' English; fail on missing keys,",
|
|
@@ -57,7 +68,7 @@ function parseArgs(argv = []) {
|
|
|
57
68
|
options.help = true;
|
|
58
69
|
continue;
|
|
59
70
|
}
|
|
60
|
-
if (command === "i18n" && argument && !argument.startsWith("-")) {
|
|
71
|
+
if ((command === "i18n" || command === "check") && argument && !argument.startsWith("-")) {
|
|
61
72
|
positionals.push(argument);
|
|
62
73
|
continue;
|
|
63
74
|
}
|
|
@@ -92,6 +103,9 @@ function parseArgs(argv = []) {
|
|
|
92
103
|
if (command === "i18n") {
|
|
93
104
|
return parseI18nArgs(options, positionals);
|
|
94
105
|
}
|
|
106
|
+
if (command === "check" && positionals.length > 0) {
|
|
107
|
+
return parseMaterial3Args(options, positionals);
|
|
108
|
+
}
|
|
95
109
|
if (command !== "update" && command !== "check") {
|
|
96
110
|
throw new Error(usage());
|
|
97
111
|
}
|
|
@@ -127,6 +141,33 @@ function parseI18nArgs(options, positionals) {
|
|
|
127
141
|
};
|
|
128
142
|
}
|
|
129
143
|
|
|
144
|
+
function parseMaterial3Args(options, positionals) {
|
|
145
|
+
const [check, ...paths] = positionals;
|
|
146
|
+
if (check !== "m3") {
|
|
147
|
+
throw new Error(`Unknown argument: ${check}`);
|
|
148
|
+
}
|
|
149
|
+
if (options.additionalManifests) {
|
|
150
|
+
throw new Error("--manifest is only valid with jskit update and jskit check.");
|
|
151
|
+
}
|
|
152
|
+
if (!options.install) {
|
|
153
|
+
throw new Error("--no-install is only valid with jskit update.");
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
command: "check-m3",
|
|
157
|
+
paths,
|
|
158
|
+
projectRoot: options.projectRoot
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function runMaterial3({ projectRoot, paths }) {
|
|
163
|
+
const result = await checkMaterial3({ projectRoot, paths });
|
|
164
|
+
const report = formatMaterial3Report(result);
|
|
165
|
+
if (!result.ok) {
|
|
166
|
+
throw new Error(report);
|
|
167
|
+
}
|
|
168
|
+
process.stdout.write(`${report}\n`);
|
|
169
|
+
}
|
|
170
|
+
|
|
130
171
|
async function runI18n({ subcommand, locale, projectRoot }) {
|
|
131
172
|
if (subcommand === "export") {
|
|
132
173
|
const result = await exportTranslations({ projectRoot, locale });
|
|
@@ -155,6 +196,10 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
155
196
|
await runI18n(options);
|
|
156
197
|
return;
|
|
157
198
|
}
|
|
199
|
+
if (options.command === "check-m3") {
|
|
200
|
+
await runMaterial3(options);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
158
203
|
|
|
159
204
|
const catalog = await readCatalog();
|
|
160
205
|
if (options.command === "update") {
|
|
@@ -184,13 +229,18 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
184
229
|
checkInstalled: true,
|
|
185
230
|
catalog
|
|
186
231
|
});
|
|
187
|
-
|
|
188
|
-
|
|
232
|
+
const ui = await checkMaterial3({ projectRoot: options.projectRoot, navigationOnly: !MATERIAL3_GATE });
|
|
233
|
+
const reports = [
|
|
234
|
+
result.ok
|
|
235
|
+
? `JSKIT project graph is coordinated across ${result.manifests.length} manifest(s) and ` +
|
|
236
|
+
`${result.installations.length} resolved package installation(s).`
|
|
237
|
+
: `JSKIT project graph is inconsistent:\n- ${result.issues.join("\n- ")}`,
|
|
238
|
+
MATERIAL3_GATE ? formatMaterial3Report(ui) : formatNavigationReport(ui)
|
|
239
|
+
];
|
|
240
|
+
if (!result.ok || !ui.ok) {
|
|
241
|
+
throw new Error(reports.join("\n"));
|
|
189
242
|
}
|
|
190
|
-
process.stdout.write(
|
|
191
|
-
`JSKIT project graph is coordinated across ${result.manifests.length} manifest(s) and ` +
|
|
192
|
-
`${result.installations.length} resolved package installation(s).\n`
|
|
193
|
-
);
|
|
243
|
+
process.stdout.write(`${reports.join("\n")}\n`);
|
|
194
244
|
}
|
|
195
245
|
|
|
196
246
|
function isCliEntrypoint(argvPath = process.argv[1]) {
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// Small source readers for `jskit check m3`. @jskit-ai/jskit-catalog has no dependencies (apps run it through npx),
|
|
2
|
+
// so these read Vue, JavaScript and CSS just far enough for the Material 3 rules: blocks, tags with attributes,
|
|
3
|
+
// declarations, and comments blanked to spaces so every offset still maps to its original line and column.
|
|
4
|
+
|
|
5
|
+
const HEX_DIGITS = "[0-9a-fA-F]";
|
|
6
|
+
// #rgb, #rgba, #rrggbb and #rrggbbaa, not followed by another word character.
|
|
7
|
+
const HEX_COLOUR = `#(?:${HEX_DIGITS}{8}|${HEX_DIGITS}{6}|${HEX_DIGITS}{3,4})(?![\\w-])`;
|
|
8
|
+
const EXCEPTION_RULE = /m3-check:\s*allow\s+(\S+?)(?:-->|\*\/)?(?:\s|$)/u;
|
|
9
|
+
const EXCEPTION_REASON = /m3-check:\s*allow\s+\S+\s+-\s+(.*?)\s*(?:-->|\*\/)?\s*$/u;
|
|
10
|
+
|
|
11
|
+
function lineIndex(text) {
|
|
12
|
+
const starts = [0];
|
|
13
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
14
|
+
if (text[index] === "\n") starts.push(index + 1);
|
|
15
|
+
}
|
|
16
|
+
return (offset) => {
|
|
17
|
+
let low = 0;
|
|
18
|
+
let high = starts.length - 1;
|
|
19
|
+
while (low < high) {
|
|
20
|
+
const middle = Math.ceil((low + high) / 2);
|
|
21
|
+
if (starts[middle] <= offset) low = middle;
|
|
22
|
+
else high = middle - 1;
|
|
23
|
+
}
|
|
24
|
+
return { line: low + 1, column: offset - starts[low] + 1 };
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function blank(text) {
|
|
29
|
+
return text.replace(/[^\n]/gu, " ");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// JavaScript with its comments replaced by spaces; strings and template literals are kept.
|
|
33
|
+
function blankJsComments(source) {
|
|
34
|
+
let output = "";
|
|
35
|
+
let index = 0;
|
|
36
|
+
while (index < source.length) {
|
|
37
|
+
const char = source[index];
|
|
38
|
+
const next = source[index + 1];
|
|
39
|
+
if (char === "/" && next === "/") {
|
|
40
|
+
const end = source.indexOf("\n", index);
|
|
41
|
+
const stop = end < 0 ? source.length : end;
|
|
42
|
+
output += blank(source.slice(index, stop));
|
|
43
|
+
index = stop;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (char === "/" && next === "*") {
|
|
47
|
+
const end = source.indexOf("*/", index + 2);
|
|
48
|
+
const stop = end < 0 ? source.length : end + 2;
|
|
49
|
+
output += blank(source.slice(index, stop));
|
|
50
|
+
index = stop;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (char === "\"" || char === "'" || char === "`") {
|
|
54
|
+
let stop = index + 1;
|
|
55
|
+
while (stop < source.length && source[stop] !== char) {
|
|
56
|
+
if (source[stop] === "\\") stop += 1;
|
|
57
|
+
if (char !== "`" && source[stop] === "\n") break;
|
|
58
|
+
stop += 1;
|
|
59
|
+
}
|
|
60
|
+
output += source.slice(index, stop + 1);
|
|
61
|
+
index = stop + 1;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
output += char;
|
|
65
|
+
index += 1;
|
|
66
|
+
}
|
|
67
|
+
return output;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function blankMatches(source, pattern) {
|
|
71
|
+
return source.replace(pattern, (match) => blank(match));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const blankCssComments = (source) => blankMatches(source, /\/\*[\s\S]*?(?:\*\/|$)/gu);
|
|
75
|
+
const blankHtmlComments = (source) => blankMatches(source, /<!--[\s\S]*?(?:-->|$)/gu);
|
|
76
|
+
|
|
77
|
+
// Every line that mentions m3-check, read as an exception: `m3-check: allow <rule> - <reason>`. A line in any other
|
|
78
|
+
// form gives no rule or no reason, and the check reports it.
|
|
79
|
+
function readExceptions(source) {
|
|
80
|
+
const exceptions = [];
|
|
81
|
+
source.split("\n").forEach((text, index) => {
|
|
82
|
+
if (!text.includes("m3-check:")) return;
|
|
83
|
+
exceptions.push({
|
|
84
|
+
line: index + 1,
|
|
85
|
+
rule: EXCEPTION_RULE.exec(text)?.[1] || "",
|
|
86
|
+
reason: EXCEPTION_REASON.exec(text)?.[1] || ""
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
return exceptions;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function findBlocks(source, tag) {
|
|
93
|
+
const blocks = [];
|
|
94
|
+
const pattern = new RegExp(`<${tag}(\\s[^>]*)?>([\\s\\S]*?)</${tag}\\s*>`, "giu");
|
|
95
|
+
for (const match of source.matchAll(pattern)) {
|
|
96
|
+
const openLength = match[0].indexOf(">") + 1;
|
|
97
|
+
blocks.push({
|
|
98
|
+
attributes: match[1] || "",
|
|
99
|
+
offset: match.index + openLength,
|
|
100
|
+
text: match[2],
|
|
101
|
+
start: match.index,
|
|
102
|
+
end: match.index + match[0].length
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return blocks;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// A single-file component's blocks. The template is the outermost <template>, so nested slot templates stay in it.
|
|
109
|
+
function splitVue(source) {
|
|
110
|
+
const scripts = findBlocks(source, "script");
|
|
111
|
+
const styles = findBlocks(source, "style");
|
|
112
|
+
const masked = [...scripts, ...styles]
|
|
113
|
+
.reduce((text, block) => text.slice(0, block.start) + blank(text.slice(block.start, block.end)) + text.slice(block.end), source);
|
|
114
|
+
const open = /<template(\s[^>]*)?>/iu.exec(masked);
|
|
115
|
+
const close = masked.lastIndexOf("</template");
|
|
116
|
+
const template = open && close > open.index
|
|
117
|
+
? { offset: open.index + open[0].length, text: masked.slice(open.index + open[0].length, close) }
|
|
118
|
+
: null;
|
|
119
|
+
return { template, scripts, styles };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const ATTRIBUTE = /([^\s"'<>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/gu;
|
|
123
|
+
const TAG = /<([A-Za-z][\w.-]*)((?:\s+[^\s"'<>/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+))?)*)\s*(\/?)>/gu;
|
|
124
|
+
|
|
125
|
+
function kebab(name) {
|
|
126
|
+
return name.replace(/([a-z0-9])([A-Z])/gu, "$1-$2").toLowerCase();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function readAttributes(text, offset) {
|
|
130
|
+
const attributes = [];
|
|
131
|
+
for (const match of text.matchAll(ATTRIBUTE)) {
|
|
132
|
+
const value = match[2] ?? match[3] ?? match[4];
|
|
133
|
+
const valueStart = value === undefined ? -1 : match.index + match[0].lastIndexOf(value);
|
|
134
|
+
const raw = match[1];
|
|
135
|
+
const bound = raw.startsWith(":") || raw.startsWith("v-bind:");
|
|
136
|
+
attributes.push({
|
|
137
|
+
raw,
|
|
138
|
+
name: raw.replace(/^(?::|v-bind:)/u, "").toLowerCase(),
|
|
139
|
+
bound,
|
|
140
|
+
event: raw.startsWith("@") || raw.startsWith("v-on:"),
|
|
141
|
+
value: value ?? "",
|
|
142
|
+
present: true,
|
|
143
|
+
offset: offset + match.index,
|
|
144
|
+
valueOffset: valueStart < 0 ? offset + match.index : offset + valueStart
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return attributes;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// The tags of a template (comments already blanked), each with its attributes and, for a closed element, its
|
|
151
|
+
// inner source up to the first matching close tag.
|
|
152
|
+
function readTags(template) {
|
|
153
|
+
const tags = [];
|
|
154
|
+
for (const match of template.text.matchAll(TAG)) {
|
|
155
|
+
const name = kebab(match[1]);
|
|
156
|
+
const attributesStart = match.index + 1 + match[1].length;
|
|
157
|
+
const selfClosing = match[3] === "/";
|
|
158
|
+
let inner = "";
|
|
159
|
+
if (!selfClosing) {
|
|
160
|
+
const closePattern = new RegExp(`</${match[1]}\\s*>`, "u");
|
|
161
|
+
const rest = template.text.slice(match.index + match[0].length);
|
|
162
|
+
const close = closePattern.exec(rest);
|
|
163
|
+
if (close) inner = rest.slice(0, close.index);
|
|
164
|
+
}
|
|
165
|
+
tags.push({
|
|
166
|
+
name,
|
|
167
|
+
offset: template.offset + match.index,
|
|
168
|
+
attributes: readAttributes(match[2], template.offset + attributesStart),
|
|
169
|
+
inner
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return tags;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// CSS declarations with the selectors that enclose them, for flat CSS, nested SCSS and at-rule blocks.
|
|
176
|
+
function readDeclarations(css, offset) {
|
|
177
|
+
const declarations = [];
|
|
178
|
+
const selectors = [];
|
|
179
|
+
let start = 0;
|
|
180
|
+
for (let index = 0; index <= css.length; index += 1) {
|
|
181
|
+
const char = css[index];
|
|
182
|
+
if (char !== "{" && char !== "}" && char !== ";" && index !== css.length) continue;
|
|
183
|
+
const text = css.slice(start, index);
|
|
184
|
+
if (char === "{") {
|
|
185
|
+
selectors.push(text.trim());
|
|
186
|
+
} else {
|
|
187
|
+
const colon = text.indexOf(":");
|
|
188
|
+
if (colon > 0 && selectors.length > 0 && text.trim()) {
|
|
189
|
+
const leading = text.length - text.trimStart().length;
|
|
190
|
+
declarations.push({
|
|
191
|
+
property: text.slice(0, colon).trim().toLowerCase(),
|
|
192
|
+
value: text.slice(colon + 1).trim(),
|
|
193
|
+
selectors: [...selectors],
|
|
194
|
+
offset: offset + start + leading,
|
|
195
|
+
valueOffset: offset + start + colon + 1
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (char === "}") selectors.pop();
|
|
199
|
+
}
|
|
200
|
+
start = index + 1;
|
|
201
|
+
}
|
|
202
|
+
return declarations;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Declarations written inline, as in a style attribute.
|
|
206
|
+
function readInlineDeclarations(text, offset) {
|
|
207
|
+
return readDeclarations(`x{${text}}`, offset - 2);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export {
|
|
211
|
+
HEX_COLOUR,
|
|
212
|
+
blankCssComments,
|
|
213
|
+
blankHtmlComments,
|
|
214
|
+
blankJsComments,
|
|
215
|
+
kebab,
|
|
216
|
+
lineIndex,
|
|
217
|
+
readDeclarations,
|
|
218
|
+
readExceptions,
|
|
219
|
+
readInlineDeclarations,
|
|
220
|
+
readTags,
|
|
221
|
+
splitVue
|
|
222
|
+
};
|