@jskit-ai/jskit-catalog 0.2.42 → 0.2.43
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 +270 -263
- package/package.json +1 -1
- package/scripts/data-access-files.mjs +140 -0
- package/scripts/data-access.mjs +18 -112
- package/scripts/jskit.mjs +12 -12
- package/scripts/material3.mjs +41 -37
- package/scripts/ui-components.mjs +159 -0
- package/scripts/verify-packages.mjs +18 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { kebab } from "./material3-source.mjs";
|
|
4
|
+
|
|
5
|
+
// How app code uses components, for the Material 3 check (material3.mjs).
|
|
6
|
+
// - ui/removed-prop: a prop a JSKIT component no longer has, which Vue would otherwise pass on as an HTML attribute
|
|
7
|
+
// that does nothing. Each package lists the props its releases removed from its exported components in
|
|
8
|
+
// package.json, as `jskit.metadata.ui.removedProps: { "<Component>": { "<prop>": "<what to do instead>" } }`, read
|
|
9
|
+
// from the installed package, so an app is judged against the release it runs.
|
|
10
|
+
// - ui/vuetify-by-name: a Vuetify component named as a string for Vue to resolve at run time (resolveComponent("v-dialog"),
|
|
11
|
+
// resolveDynamicComponent, h("v-dialog"), <component is="v-dialog">). A JSKIT app registers no Vuetify component
|
|
12
|
+
// globally: vite-plugin-vuetify imports each one where a template writes its tag, so the name finds nothing and Vue
|
|
13
|
+
// renders an unknown element. Only the names the app's installed Vuetify exports from vuetify/components count:
|
|
14
|
+
// another library's v-chart is that library's to register.
|
|
15
|
+
|
|
16
|
+
const PACKAGE_DIRECTORY = path.join("node_modules", "@jskit-ai");
|
|
17
|
+
// `import CrudViewScreen from "@jskit-ai/http-web/client/components/CrudViewScreen"`.
|
|
18
|
+
const COMPONENT_IMPORT = /\bimport\s+([A-Za-z_$][\w$]*)\s+from\s*["']@jskit-ai\/([\w-]+)\/client\/components\/([\w-]+?)(?:\.vue)?["']/gu;
|
|
19
|
+
// `import { CrudViewScreen, CrudListScreen as List } from "@jskit-ai/http-web/client"`.
|
|
20
|
+
const CLIENT_IMPORT = /\bimport\s*\{([^}]*)\}\s*from\s*["']@jskit-ai\/([\w-]+)\/client["']/gu;
|
|
21
|
+
|
|
22
|
+
// What a Vuetify component's name looks like: kebab-case with its v- prefix, or PascalCase with its V. Whether it is one
|
|
23
|
+
// is the installed Vuetify's to say (readVuetifyComponents).
|
|
24
|
+
const VUETIFY_NAME = String.raw`v-[a-z][a-z0-9-]*|V[A-Z][A-Za-z0-9]*`;
|
|
25
|
+
const VUETIFY_BY_NAME_CALL = new RegExp(String.raw`\b(resolveComponent|resolveDynamicComponent|h)\s*\(\s*(["'\x60])(${VUETIFY_NAME})\2`, "gu");
|
|
26
|
+
const QUOTED_VUETIFY_NAME = new RegExp(String.raw`(["'\x60])(${VUETIFY_NAME})\1`, "gu");
|
|
27
|
+
const RESOLVING_CALL = /\b(?:resolveComponent|resolveDynamicComponent|h)\s*\(\s*$/u;
|
|
28
|
+
const EXPORT_ALL = /\bexport\s*\*\s*from\s*["']([^"']+)["']/gu;
|
|
29
|
+
const EXPORT_LIST = /\bexport\s*\{([^}]*)\}/gu;
|
|
30
|
+
const EXPORT_DECLARATION = /\bexport\s+(?:const|let|var|function|class)\s+([A-Za-z_$][\w$]*)/gu;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The props each installed @jskit-ai package lists as removed from its components, by package name:
|
|
34
|
+
* Map<"@jskit-ai/<name>", Record<component, Record<prop, hint>>>.
|
|
35
|
+
* @param {string} root
|
|
36
|
+
*/
|
|
37
|
+
async function readRemovedProps(root) {
|
|
38
|
+
const removed = new Map();
|
|
39
|
+
let entries;
|
|
40
|
+
try {
|
|
41
|
+
entries = await readdir(path.join(root, PACKAGE_DIRECTORY));
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error?.code === "ENOENT") return removed;
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
let manifest;
|
|
48
|
+
try {
|
|
49
|
+
manifest = JSON.parse(await readFile(path.join(root, PACKAGE_DIRECTORY, entry, "package.json"), "utf8"));
|
|
50
|
+
} catch {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const props = manifest?.jskit?.metadata?.ui?.removedProps;
|
|
54
|
+
if (props && typeof props === "object") removed.set(`@jskit-ai/${entry}`, props);
|
|
55
|
+
}
|
|
56
|
+
return removed;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The names a module exports, following its `export * from`: the components of vuetify/components.
|
|
60
|
+
async function readExportNames(file, names = new Set(), seen = new Set()) {
|
|
61
|
+
if (seen.has(file)) return names;
|
|
62
|
+
seen.add(file);
|
|
63
|
+
let code;
|
|
64
|
+
try {
|
|
65
|
+
code = await readFile(file, "utf8");
|
|
66
|
+
} catch {
|
|
67
|
+
return names;
|
|
68
|
+
}
|
|
69
|
+
for (const [, from] of code.matchAll(EXPORT_ALL)) await readExportNames(path.resolve(path.dirname(file), from), names, seen);
|
|
70
|
+
for (const [, list] of code.matchAll(EXPORT_LIST)) {
|
|
71
|
+
for (const specifier of list.split(",")) {
|
|
72
|
+
const name = specifier.trim().split(/\s+as\s+/u).pop();
|
|
73
|
+
if (name) names.add(name);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const [, name] of code.matchAll(EXPORT_DECLARATION)) names.add(name);
|
|
77
|
+
return names;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The components the app's installed Vuetify exports from vuetify/components, by PascalCase name (VDialog); none when
|
|
82
|
+
* the app has no Vuetify.
|
|
83
|
+
* @param {string} root
|
|
84
|
+
*/
|
|
85
|
+
async function readVuetifyComponents(root) {
|
|
86
|
+
const packageRoot = path.join(root, "node_modules", "vuetify");
|
|
87
|
+
let manifest;
|
|
88
|
+
try {
|
|
89
|
+
manifest = JSON.parse(await readFile(path.join(packageRoot, "package.json"), "utf8"));
|
|
90
|
+
} catch {
|
|
91
|
+
return new Set();
|
|
92
|
+
}
|
|
93
|
+
const entry = (target) => typeof target === "string" || !target ? target : entry(target.import ?? target.module ?? target.default);
|
|
94
|
+
return await readExportNames(path.join(packageRoot, entry(manifest?.exports?.["./components"]) || "lib/components/index.js"));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// The JSKIT components a script imports, by the tag a template writes them as (kebab-case): { name, packageName, local }.
|
|
98
|
+
function readComponentImports(script) {
|
|
99
|
+
const components = new Map();
|
|
100
|
+
const add = (local, packageName, name) => components.set(kebab(local), { name, packageName: `@jskit-ai/${packageName}`, local });
|
|
101
|
+
for (const [, local, packageName, name] of script.matchAll(COMPONENT_IMPORT)) add(local, packageName, name);
|
|
102
|
+
for (const [, names, packageName] of script.matchAll(CLIENT_IMPORT)) {
|
|
103
|
+
for (const specifier of names.split(",")) {
|
|
104
|
+
const [name, local = name] = specifier.trim().split(/\s+as\s+/u);
|
|
105
|
+
if (/^[A-Z]\w*$/u.test(name)) add(local, packageName, name);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return components;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Reports each prop a template passes to a JSKIT component that the installed package lists as removed, written in
|
|
112
|
+
// camelCase or kebab-case, static or bound.
|
|
113
|
+
function checkRemovedProps(tags, script, removedProps, report) {
|
|
114
|
+
const components = readComponentImports(script);
|
|
115
|
+
for (const tag of tags) {
|
|
116
|
+
const component = components.get(tag.name);
|
|
117
|
+
const removed = component && removedProps.get(component.packageName)?.[component.name];
|
|
118
|
+
if (!removed) continue;
|
|
119
|
+
for (const entry of tag.attributes) {
|
|
120
|
+
const prop = Object.keys(removed).find((name) => [kebab(name), name.toLowerCase()].includes(entry.name));
|
|
121
|
+
if (prop) {
|
|
122
|
+
report("ui/removed-prop", entry.offset, `${component.local} (${component.packageName}) no longer has the prop ${prop}, so Vue ` +
|
|
123
|
+
`passes it on as an HTML attribute that does nothing. ${removed[prop]}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Reports a name the installed Vuetify exports a component as (vuetifyComponents, from readVuetifyComponents).
|
|
130
|
+
function reportByName({ report, vuetifyComponents }, offset, name, how) {
|
|
131
|
+
const component = name.startsWith("v-") ? name.split("-").map((part) => part[0].toUpperCase() + part.slice(1)).join("") : name;
|
|
132
|
+
if (!vuetifyComponents.has(component)) return;
|
|
133
|
+
report("ui/vuetify-by-name", offset, `${how} names Vuetify's ${component} for Vue to resolve at run time, which finds nothing ` +
|
|
134
|
+
`in a JSKIT app: Vuetify components are imported where a template writes their tag, never registered by name, so Vue ` +
|
|
135
|
+
`renders an unknown element. Import it, import { ${component} } from "vuetify/components", and pass the component itself.`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Reports each Vuetify component that code (a script, or a template's bound expression) names for Vue to resolve.
|
|
139
|
+
// context: { report, vuetifyComponents }.
|
|
140
|
+
function checkVuetifyByNameCode(code, offset, context) {
|
|
141
|
+
for (const match of code.matchAll(VUETIFY_BY_NAME_CALL)) reportByName(context, offset + match.index, match[3], `${match[1]}("${match[3]}")`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Reports a <component> whose `is` names a Vuetify component: written as it is, or as a string in a bound expression.
|
|
145
|
+
// context: { report, vuetifyComponents }.
|
|
146
|
+
function checkVuetifyByNameAttribute(tag, entry, context) {
|
|
147
|
+
if (tag.name !== "component" || entry.name !== "is") return;
|
|
148
|
+
if (!entry.bound) {
|
|
149
|
+
const name = entry.value.trim();
|
|
150
|
+
if (new RegExp(`^(?:${VUETIFY_NAME})$`, "u").test(name)) reportByName(context, entry.offset, name, `is="${name}"`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
for (const match of entry.value.matchAll(QUOTED_VUETIFY_NAME)) {
|
|
154
|
+
// A call's argument is checkVuetifyByNameCode's.
|
|
155
|
+
if (!RESOLVING_CALL.test(entry.value.slice(0, match.index))) reportByName(context, entry.valueOffset + match.index, match[2], `:is="${match[0]}"`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export { checkRemovedProps, checkVuetifyByNameAttribute, checkVuetifyByNameCode, readRemovedProps, readVuetifyComponents };
|
|
@@ -260,6 +260,22 @@ function validateApiSummarySurfaces(packageJson) {
|
|
|
260
260
|
}
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
// Each component that jskit.metadata.ui.removedProps lists is one the package exports as ./client/components/<name>,
|
|
264
|
+
// with a hint for each removed prop, so jskit check's ui/removed-prop judges a component an app can import.
|
|
265
|
+
function validateRemovedProps(packageJson) {
|
|
266
|
+
const exported = packageJson.exports && typeof packageJson.exports === "object" ? packageJson.exports : {};
|
|
267
|
+
for (const [component, props] of Object.entries(packageJson.jskit?.metadata?.ui?.removedProps || {})) {
|
|
268
|
+
if (!exported[`./client/components/${component}`]) {
|
|
269
|
+
throw new Error(`${packageJson.name}#jskit.metadata.ui.removedProps lists ${component}, which the package does not export as ./client/components/${component}.`);
|
|
270
|
+
}
|
|
271
|
+
for (const [prop, hint] of Object.entries(props || {})) {
|
|
272
|
+
if (typeof hint !== "string" || !hint.trim()) {
|
|
273
|
+
throw new Error(`${packageJson.name}#jskit.metadata.ui.removedProps.${component}.${prop} says nothing; say what to do instead.`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
263
279
|
async function validatePackage(packageRecord, localVersions, migrationOwners) {
|
|
264
280
|
const { packageJson, packageRoot, manifestPath } = packageRecord;
|
|
265
281
|
const packageId = String(packageJson.name || "").trim();
|
|
@@ -294,6 +310,7 @@ async function validatePackage(packageRecord, localVersions, migrationOwners) {
|
|
|
294
310
|
}
|
|
295
311
|
|
|
296
312
|
validateApiSummarySurfaces(packageJson);
|
|
313
|
+
validateRemovedProps(packageJson);
|
|
297
314
|
const serverProviderCount = await validateProviderList(packageRecord, "server");
|
|
298
315
|
await validateProviderList(packageRecord, "client");
|
|
299
316
|
await validateMigrations(packageRecord, migrationOwners);
|
|
@@ -423,6 +440,7 @@ if (process.argv[1] && path.resolve(process.argv[1]) === SCRIPT_PATH) {
|
|
|
423
440
|
export {
|
|
424
441
|
main,
|
|
425
442
|
validateApiSummarySurfaces,
|
|
443
|
+
validateRemovedProps,
|
|
426
444
|
validateCapabilityClosure,
|
|
427
445
|
validateMigrations,
|
|
428
446
|
validateProviderExport,
|