@jskit-ai/jskit-catalog 0.2.45 → 0.2.46

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/jskit-catalog",
3
- "version": "0.2.45",
3
+ "version": "0.2.46",
4
4
  "description": "Published metadata catalog for JSKIT package metadata.",
5
5
  "type": "module",
6
6
  "files": [
@@ -18,6 +18,7 @@ const ROOT = String.raw`(?:(?:packages|tooling)/[^/]+/)?`;
18
18
  const SERVER_FILE = new RegExp(`^${ROOT}(?:[^/]+$|(?:src|server|scripts|bin|config)/)`, "u");
19
19
  const FRAMEWORK_DIRECTORY = path.join("node_modules", "@jskit-ai");
20
20
  const SOURCE_FILE = /\.[cm]?js$/u;
21
+ const VUE_FILE = /\.vue$/u;
21
22
  const SKIPPED_FILE = /\.(?:test|spec|vitest)\.[cm]?js$/u;
22
23
  // Code that defines no table resource or system scope an app runs: dependencies, anywhere; and, at a root (ROOT), build
23
24
  // output, tests and fixtures, migrations and seeds, and the examples, templates and documentation JSKIT packages carry.
@@ -29,7 +30,7 @@ const PACKAGE_DIRECTORY = /^(?:packages|tooling)\/[^/]+\/(.+)$/u;
29
30
  const skipsDirectory = (name) => path.posix.basename(name) === "node_modules" ||
30
31
  SKIPPED_ROOT_DIRECTORIES.has(PACKAGE_DIRECTORY.exec(name)?.[1] ?? name);
31
32
 
32
- async function walk(root, directory, files) {
33
+ async function walk(root, directory, files, pattern = SOURCE_FILE) {
33
34
  let entries;
34
35
  try {
35
36
  entries = await readdir(path.join(root, directory), { withFileTypes: true });
@@ -40,8 +41,8 @@ async function walk(root, directory, files) {
40
41
  for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
41
42
  const relative = directory ? path.join(directory, entry.name) : entry.name;
42
43
  if (entry.isDirectory()) {
43
- if (!entry.name.startsWith(".") && !skipsDirectory(toPosixPath(relative))) await walk(root, relative, files);
44
- } else if (entry.isFile() && SOURCE_FILE.test(entry.name) && !SKIPPED_FILE.test(entry.name)) {
44
+ if (!entry.name.startsWith(".") && !skipsDirectory(toPosixPath(relative))) await walk(root, relative, files, pattern);
45
+ } else if (entry.isFile() && pattern.test(entry.name) && !SKIPPED_FILE.test(entry.name)) {
45
46
  files.push(relative);
46
47
  }
47
48
  }
@@ -68,8 +69,12 @@ async function frameworkPackages(root) {
68
69
  return packages;
69
70
  }
70
71
 
71
- // The app's ESLint, from the app or else beside this check, set to find each file's config as `eslint` does; null when
72
- // neither has one, or the app has no config: then the lint itself cannot run, which fails on its own.
72
+ // A file directly in a directory, for asking ESLint about the directory: its API judges files, not directories.
73
+ const DIRECTORY_PROBE = "jskit-directory-probe.js";
74
+
75
+ // The app's ESLint, from the app or else beside this check, set to find each file's config as `eslint` does, with
76
+ // its ESLint class; null when neither has one, or the app has no config: then the lint itself cannot run, which fails
77
+ // on its own.
73
78
  async function appEslint(root) {
74
79
  for (const base of [path.join(root, "package.json"), fileURLToPath(import.meta.url)]) {
75
80
  let resolved;
@@ -79,25 +84,58 @@ async function appEslint(root) {
79
84
  continue;
80
85
  }
81
86
  const api = await import(pathToFileURL(resolved).href);
82
- const eslint = new (api.ESLint || api.default.ESLint)({ cwd: root });
83
- return await eslint.findConfigFile(path.join(root, "package.json")) ? eslint : null;
87
+ const { ESLint } = api.ESLint ? api : api.default;
88
+ const eslint = new ESLint({ cwd: root });
89
+ return await eslint.findConfigFile(path.join(root, "package.json")) ? { eslint, ESLint } : null;
84
90
  }
85
91
  return null;
86
92
  }
87
93
 
88
- // The ESLint config of each app file that one governs, read as `eslint` reads it: each { file, governed, config },
89
- // `governed` the file's path from that config's directory and `config` undefined when the config ignores the file; and the
90
- // first error reading one, after which no more are read. None when the app has no ESLint config (appEslint).
94
+ // For a config file, the config whose ignores keep `eslint .`, run at the app's root, out of its directory; undefined
95
+ // when it enters it. ESLint enters a directory unless the config of the directory above ignores it, so a parent config
96
+ // ignores a nested config's directory whole, and past it the nested config alone judges what it holds. The directory's
97
+ // verdict is that of a file directly in it, asked of an ESLint bound to the config above alone.
98
+ function configIgnoredBy(root, { eslint, ESLint }) {
99
+ const verdicts = new Map();
100
+ const bound = new Map();
101
+ const boundTo = (configFile) => {
102
+ if (!bound.has(configFile)) bound.set(configFile, new ESLint({ cwd: path.dirname(configFile), overrideConfigFile: configFile }));
103
+ return bound.get(configFile);
104
+ };
105
+ async function verdict(configFile) {
106
+ const directory = path.dirname(configFile);
107
+ const inside = path.relative(root, directory);
108
+ if (!inside || inside.startsWith("..") || path.isAbsolute(inside)) return undefined;
109
+ // The config of the directory above, which ESLint asks before entering this one.
110
+ const above = await eslint.findConfigFile(directory);
111
+ if (!above) return undefined;
112
+ return await ignoredBy(above) ??
113
+ (await boundTo(above).isPathIgnored(path.join(directory, DIRECTORY_PROBE)) ? above : undefined);
114
+ }
115
+ function ignoredBy(configFile) {
116
+ if (!verdicts.has(configFile)) verdicts.set(configFile, verdict(configFile));
117
+ return verdicts.get(configFile);
118
+ }
119
+ return ignoredBy;
120
+ }
121
+
122
+ // The ESLint config of each app file that one governs, read as `eslint .` reads it: each { file, governed, config },
123
+ // `governed` the file's path from that config's directory and `config` undefined when the config ignores the file, or
124
+ // when a config above ignores the directory of the file's nested config (configIgnoredBy), as `eslint .` never reaches
125
+ // it: `governed` is then told from the directory of the config that ignores it. And the first error reading one, after
126
+ // which no more are read. None when the app has no ESLint config (appEslint).
91
127
  async function lintConfigs(root, files) {
92
128
  const configs = [];
93
- const eslint = await appEslint(root);
94
- for (const file of eslint ? files : []) {
129
+ const app = await appEslint(root);
130
+ const ignoredBy = app && configIgnoredBy(root, app);
131
+ for (const file of app ? files : []) {
95
132
  const absolute = path.join(root, file);
96
133
  try {
97
- const configFile = await eslint.findConfigFile(absolute);
134
+ const configFile = await app.eslint.findConfigFile(absolute);
98
135
  if (!configFile) continue;
99
- const governed = toPosixPath(path.relative(path.dirname(configFile), absolute));
100
- configs.push({ file, governed, config: await eslint.calculateConfigForFile(absolute) });
136
+ const outer = await ignoredBy(configFile);
137
+ const governed = toPosixPath(path.relative(path.dirname(outer ?? configFile), absolute));
138
+ configs.push({ file, governed, config: outer ? undefined : await app.eslint.calculateConfigForFile(absolute) });
101
139
  } catch (error) {
102
140
  return { configs, error: { file, message: String(error?.message || error).split("\n")[0] } };
103
141
  }
@@ -137,4 +175,11 @@ async function sourceFiles(root) {
137
175
  return { sources, lint, skipped: skippedPaths(names, skipped) };
138
176
  }
139
177
 
140
- export { ROOT, lintIgnores, sourceFiles };
178
+ // The app's own Vue components, which data/search-q reads for the list screens they show.
179
+ async function vueFiles(root) {
180
+ const files = [];
181
+ await walk(root, "", files, VUE_FILE);
182
+ return files.map((file) => ({ file: toPosixPath(file), absolute: path.join(root, file) }));
183
+ }
184
+
185
+ export { ROOT, lintIgnores, sourceFiles, vueFiles };
@@ -0,0 +1,169 @@
1
+ import { blankJsComments, lineIndex } from "./material3-source.mjs";
2
+
3
+ // JavaScript source read as `jskit check`'s data part reads it, never run: the code with its comments blanked, a masked
4
+ // copy with the contents of its strings blanked too, every offset kept, and the object literals, keys and literal values
5
+ // found in them.
6
+
7
+ const blank = (text) => text.replace(/[^\n]/gu, " ");
8
+
9
+ // Code with its comments and the contents of its strings and templates blanked to spaces, every offset kept: calls
10
+ // and keys are found in it, values are read from the code at the same offsets.
11
+ function maskStrings(code) {
12
+ let output = "";
13
+ let index = 0;
14
+ while (index < code.length) {
15
+ const quote = code[index];
16
+ if (quote !== "\"" && quote !== "'" && quote !== "`") {
17
+ output += quote;
18
+ index += 1;
19
+ continue;
20
+ }
21
+ let end = index + 1;
22
+ while (end < code.length && code[end] !== quote && (quote === "`" || code[end] !== "\n")) {
23
+ end += code[end] === "\\" ? 2 : 1;
24
+ }
25
+ output += quote + blank(code.slice(index + 1, end)) + code.slice(end, end + 1);
26
+ index = end + 1;
27
+ }
28
+ return output;
29
+ }
30
+
31
+ // The offset just past the bracket that closes the one at `start`, in masked code; -1 when it never closes.
32
+ function closingOffset(masked, start) {
33
+ let depth = 0;
34
+ for (let index = start; index < masked.length; index += 1) {
35
+ if ("([{".includes(masked[index])) depth += 1;
36
+ else if (")]}".includes(masked[index]) && --depth === 0) return index + 1;
37
+ }
38
+ return -1;
39
+ }
40
+
41
+ const nextCodeOffset = (text, offset) => offset + (/^\s*/u.exec(text.slice(offset))?.[0].length || 0);
42
+
43
+ // The string a top-level `const NAME = "..."` gives, or null.
44
+ function constantValue(code, name) {
45
+ const match = new RegExp(`(?:^|[\\s;])const\\s+${name}\\s*=\\s*(["'\`])((?:\\\\.|(?!\\1)[^\\\\\\n])*)\\1\\s*[;,\\n]`, "u").exec(code);
46
+ return match && !(match[1] === "`" && match[2].includes("${")) ? match[2] : null;
47
+ }
48
+
49
+ /**
50
+ * The value written at an offset, read up to the comma or bracket that ends it: { text } for a string literal, a
51
+ * template without expressions, or a const holding one; { text: null } for anything else.
52
+ */
53
+ function readValue(code, masked, offset) {
54
+ const start = nextCodeOffset(code, offset);
55
+ const quote = code[start];
56
+ if (quote === "\"" || quote === "'" || quote === "`") {
57
+ let end = start + 1;
58
+ while (end < code.length && code[end] !== quote) end += code[end] === "\\" ? 2 : 1;
59
+ const after = masked[nextCodeOffset(masked, end + 1)];
60
+ const text = code.slice(start + 1, end);
61
+ const whole = after === "," || after === "}" || after === ")";
62
+ return { text: whole && !(quote === "`" && text.includes("${")) ? text.replace(/\\(.)/gu, "$1") : null };
63
+ }
64
+ const identifier = /^[A-Za-z_$][\w$]*(?=\s*[,})])/u.exec(code.slice(start));
65
+ return { text: identifier ? constantValue(code, identifier[0]) : null };
66
+ }
67
+
68
+ // The top-level keys of the object literal between `start` and `end`, in masked code: for each wanted key, the offset
69
+ // just past its colon, or "shorthand" for `{ name }`.
70
+ function topLevelKeys(masked, start, end, names) {
71
+ let depth = 0;
72
+ let top = "";
73
+ for (let index = start; index < end; index += 1) {
74
+ const char = masked[index];
75
+ if ("([{".includes(char)) depth += 1;
76
+ top += depth === 1 || char === "\n" ? char : " ";
77
+ if (")]}".includes(char)) depth -= 1;
78
+ }
79
+ const keys = new Map();
80
+ for (const match of top.matchAll(/(?:^|[{,])\s*([A-Za-z_$][\w$]*)\s*(:|(?=[,}]))/gu)) {
81
+ const [whole, key, colon] = match;
82
+ if (names.includes(key) && !keys.has(key)) keys.set(key, colon ? start + match.index + whole.length : "shorthand");
83
+ }
84
+ return keys;
85
+ }
86
+
87
+ // The object literal a call at `callOffset` takes as its first argument: { start, end } in masked code, or null when the
88
+ // call is a declaration (`function defineFeature(`) or its first argument is anything else.
89
+ function objectArgument(masked, callOffset, matchLength) {
90
+ if (/function\s*\*?\s*$/u.test(masked.slice(Math.max(0, callOffset - 20), callOffset))) return null;
91
+ const start = nextCodeOffset(masked, callOffset + matchLength);
92
+ if (masked[start] !== "{") return null;
93
+ const end = closingOffset(masked, start);
94
+ return end < 0 ? null : { start, end };
95
+ }
96
+
97
+ // The value of one key of an object literal, as readValue reads it; undefined when the object has no such key.
98
+ function keyValue(code, masked, keys, key) {
99
+ const at = keys.get(key);
100
+ if (at === undefined) return undefined;
101
+ return at === "shorthand" ? { text: constantValue(code, key) } : readValue(code, masked, at);
102
+ }
103
+
104
+ function readSourceFile(source) {
105
+ const code = blankJsComments(source);
106
+ return { source, code, masked: maskStrings(code), position: lineIndex(source) };
107
+ }
108
+
109
+ // Every key of the object literal between `start` and `end` (masked code), in order, and whether the literal also
110
+ // spreads or computes keys, so that its keys are not all known.
111
+ function objectKeys(masked, start, end) {
112
+ let depth = 0;
113
+ let top = "";
114
+ for (let index = start; index < end; index += 1) {
115
+ const char = masked[index];
116
+ if ("([{".includes(char)) depth += 1;
117
+ top += depth === 1 || char === "\n" ? char : " ";
118
+ if (")]}".includes(char)) depth -= 1;
119
+ }
120
+ const keys = [...top.matchAll(/(?:^|[{,])\s*([A-Za-z_$][\w$]*)\s*(?::|(?=[,}]))/gu)].map((match) => ({
121
+ key: match[1],
122
+ at: start + match.index + match[0].length
123
+ }));
124
+ return { keys, partial: /(?:^|[{,])\s*(?:\.\.\.|["'`[])/u.test(top) };
125
+ }
126
+
127
+ // The object literal written at an offset: { start, end } in masked code, or null for anything else.
128
+ function objectAt(masked, offset) {
129
+ const start = nextCodeOffset(masked, offset);
130
+ if (masked[start] !== "{") return null;
131
+ const end = closingOffset(masked, start);
132
+ return end < 0 ? null : { start, end };
133
+ }
134
+
135
+ // The variable a declaration just before an offset assigns: `const name = ` ends the text before it.
136
+ const assignedVariable = (masked, offset) =>
137
+ /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*$/u.exec(masked.slice(Math.max(0, offset - 120), offset))?.[1] ?? null;
138
+
139
+ // The identifier written at an offset, or null.
140
+ const identifierAt = (code, offset) => /^\s*([A-Za-z_$][\w$]*)\s*[,})]/u.exec(code.slice(offset))?.[1] ?? null;
141
+
142
+ // The object literal around an offset, in masked code: { start, end }, or null when the offset is in none.
143
+ function enclosingObject(masked, offset) {
144
+ let depth = 0;
145
+ for (let index = offset - 1; index >= 0; index -= 1) {
146
+ if (")]}".includes(masked[index])) depth += 1;
147
+ else if ("([{".includes(masked[index]) && depth-- === 0) {
148
+ const end = masked[index] === "{" ? closingOffset(masked, index) : -1;
149
+ return end < 0 ? null : { start: index, end };
150
+ }
151
+ }
152
+ return null;
153
+ }
154
+
155
+ export {
156
+ assignedVariable,
157
+ blank,
158
+ closingOffset,
159
+ enclosingObject,
160
+ identifierAt,
161
+ keyValue,
162
+ nextCodeOffset,
163
+ objectArgument,
164
+ objectAt,
165
+ objectKeys,
166
+ readSourceFile,
167
+ readValue,
168
+ topLevelKeys
169
+ };