@jskit-ai/jskit-cli 0.2.53 → 0.2.54

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-cli",
3
- "version": "0.2.53",
3
+ "version": "0.2.54",
4
4
  "description": "Bundle and package orchestration CLI for JSKIT apps.",
5
5
  "type": "module",
6
6
  "files": [
@@ -20,9 +20,9 @@
20
20
  "test": "node --test"
21
21
  },
22
22
  "dependencies": {
23
- "@jskit-ai/jskit-catalog": "0.1.52",
24
- "@jskit-ai/kernel": "0.1.44",
25
- "@jskit-ai/shell-web": "0.1.43"
23
+ "@jskit-ai/jskit-catalog": "0.1.53",
24
+ "@jskit-ai/kernel": "0.1.45",
25
+ "@jskit-ai/shell-web": "0.1.44"
26
26
  },
27
27
  "engines": {
28
28
  "node": "20.x"
@@ -23,16 +23,11 @@ function createHealthCommands(ctx = {}) {
23
23
  path
24
24
  } = ctx;
25
25
 
26
- const MDI_SVG_MAIN_ENTRY_CANDIDATES = Object.freeze([
27
- "src/main.js",
28
- "src/main.mjs",
29
- "src/main.ts"
30
- ]);
31
- const MDI_SVG_SCAN_ROOTS = Object.freeze([
26
+ const APP_SOURCE_SCAN_ROOTS = Object.freeze([
32
27
  "src",
33
28
  "packages"
34
29
  ]);
35
- const MDI_SVG_IGNORED_DIRECTORY_NAMES = new Set([
30
+ const APP_SOURCE_IGNORED_DIRECTORY_NAMES = new Set([
36
31
  ".git",
37
32
  ".jskit",
38
33
  ".build",
@@ -45,15 +40,34 @@ function createHealthCommands(ctx = {}) {
45
40
  "tests",
46
41
  "__tests__"
47
42
  ]);
48
- const MDI_SVG_IGNORED_FILE_PATTERNS = Object.freeze([
43
+ const APP_SOURCE_IGNORED_FILE_PATTERNS = Object.freeze([
49
44
  /\.spec\./i,
50
45
  /\.test\./i,
51
46
  /\.vitest\./i
52
47
  ]);
48
+ const APP_SOURCE_CODE_EXTENSIONS = new Set([
49
+ ".cjs",
50
+ ".js",
51
+ ".jsx",
52
+ ".mjs",
53
+ ".ts",
54
+ ".tsx",
55
+ ".vue"
56
+ ]);
57
+ const VUE_SOURCE_EXTENSIONS = new Set([".vue"]);
58
+ const MDI_SVG_MAIN_ENTRY_CANDIDATES = Object.freeze([
59
+ "src/main.js",
60
+ "src/main.mjs",
61
+ "src/main.ts"
62
+ ]);
53
63
  const DIRECT_MDI_LITERAL_ICON_PATTERN =
54
64
  /<(v-[a-z0-9-]+)[^>]*?\b(icon|prepend-icon|append-icon)\s*=\s*(['"])(mdi-[^'"]+)\3/gi;
55
65
  const DIRECT_MDI_BOUND_LITERAL_ICON_PATTERN =
56
66
  /<(v-[a-z0-9-]+)[^>]*?(?::|v-bind:)(icon|prepend-icon|append-icon)\s*=\s*(['"])(['"])(mdi-[^'"]+)\4\3/gi;
67
+ const FILTER_RUNTIME_CALLEES = Object.freeze([
68
+ "createCrudListFilters",
69
+ "useCrudListFilters"
70
+ ]);
57
71
 
58
72
  function collectDescriptorContainerTokens({ packageId, side, values, issues }) {
59
73
  const declaredTokens = new Set();
@@ -152,34 +166,33 @@ function createHealthCommands(ctx = {}) {
152
166
  }
153
167
  }
154
168
 
155
- async function appUsesVuetifyMdiSvg(appRoot) {
156
- for (const relativePath of MDI_SVG_MAIN_ENTRY_CANDIDATES) {
157
- const absolutePath = path.join(appRoot, relativePath);
158
- if (!(await fileExists(absolutePath))) {
159
- continue;
160
- }
161
- const fileContent = await readFile(absolutePath, "utf8");
162
- if (fileContent.includes("vuetify/iconsets/mdi-svg")) {
163
- return true;
164
- }
165
- }
166
-
167
- return false;
168
- }
169
-
170
- function shouldSkipMdiSvgDoctorDirectory(directoryName = "") {
171
- return MDI_SVG_IGNORED_DIRECTORY_NAMES.has(String(directoryName || "").trim());
169
+ function shouldSkipAppSourceDirectory(directoryName = "") {
170
+ return APP_SOURCE_IGNORED_DIRECTORY_NAMES.has(String(directoryName || "").trim());
172
171
  }
173
172
 
174
- function shouldSkipMdiSvgDoctorFile(fileName = "") {
173
+ function shouldSkipAppSourceFile(
174
+ fileName = "",
175
+ {
176
+ extensions = APP_SOURCE_CODE_EXTENSIONS,
177
+ ignoredFilePatterns = APP_SOURCE_IGNORED_FILE_PATTERNS
178
+ } = {}
179
+ ) {
175
180
  const normalizedFileName = String(fileName || "").trim();
176
- if (!normalizedFileName.endsWith(".vue")) {
181
+ const extension = path.extname(normalizedFileName).toLowerCase();
182
+ if (!extensions.has(extension)) {
177
183
  return true;
178
184
  }
179
- return MDI_SVG_IGNORED_FILE_PATTERNS.some((pattern) => pattern.test(normalizedFileName));
185
+ return ignoredFilePatterns.some((pattern) => pattern.test(normalizedFileName));
180
186
  }
181
187
 
182
- async function collectVueSourceFiles(rootDirectory, collected = []) {
188
+ async function collectAppSourceFiles(
189
+ rootDirectory,
190
+ {
191
+ extensions = APP_SOURCE_CODE_EXTENSIONS,
192
+ ignoredFilePatterns = APP_SOURCE_IGNORED_FILE_PATTERNS
193
+ } = {},
194
+ collected = []
195
+ ) {
183
196
  if (!(await fileExists(rootDirectory))) {
184
197
  return collected;
185
198
  }
@@ -190,13 +203,26 @@ function createHealthCommands(ctx = {}) {
190
203
  for (const entry of entries) {
191
204
  const entryPath = path.join(rootDirectory, entry.name);
192
205
  if (entry.isDirectory()) {
193
- if (shouldSkipMdiSvgDoctorDirectory(entry.name)) {
206
+ if (shouldSkipAppSourceDirectory(entry.name)) {
194
207
  continue;
195
208
  }
196
- await collectVueSourceFiles(entryPath, collected);
209
+ await collectAppSourceFiles(
210
+ entryPath,
211
+ {
212
+ extensions,
213
+ ignoredFilePatterns
214
+ },
215
+ collected
216
+ );
197
217
  continue;
198
218
  }
199
- if (entry.isFile() && !shouldSkipMdiSvgDoctorFile(entry.name)) {
219
+ if (
220
+ entry.isFile() &&
221
+ !shouldSkipAppSourceFile(entry.name, {
222
+ extensions,
223
+ ignoredFilePatterns
224
+ })
225
+ ) {
200
226
  collected.push(entryPath);
201
227
  }
202
228
  }
@@ -204,6 +230,21 @@ function createHealthCommands(ctx = {}) {
204
230
  return collected;
205
231
  }
206
232
 
233
+ async function appUsesVuetifyMdiSvg(appRoot) {
234
+ for (const relativePath of MDI_SVG_MAIN_ENTRY_CANDIDATES) {
235
+ const absolutePath = path.join(appRoot, relativePath);
236
+ if (!(await fileExists(absolutePath))) {
237
+ continue;
238
+ }
239
+ const fileContent = await readFile(absolutePath, "utf8");
240
+ if (fileContent.includes("vuetify/iconsets/mdi-svg")) {
241
+ return true;
242
+ }
243
+ }
244
+
245
+ return false;
246
+ }
247
+
207
248
  function resolveLineNumberFromIndex(sourceText = "", index = 0) {
208
249
  return String(sourceText || "").slice(0, Math.max(0, index)).split("\n").length;
209
250
  }
@@ -228,14 +269,312 @@ function createHealthCommands(ctx = {}) {
228
269
  }
229
270
  }
230
271
 
272
+ function isEscapedCharacter(sourceText = "", index = 0) {
273
+ let backslashCount = 0;
274
+ for (let position = index - 1; position >= 0 && sourceText[position] === "\\"; position -= 1) {
275
+ backslashCount += 1;
276
+ }
277
+ return backslashCount % 2 === 1;
278
+ }
279
+
280
+ function findClosingParenIndex(sourceText = "", openParenIndex = -1) {
281
+ if (openParenIndex < 0 || sourceText[openParenIndex] !== "(") {
282
+ return -1;
283
+ }
284
+
285
+ let parenDepth = 0;
286
+ let quote = "";
287
+ let inLineComment = false;
288
+ let inBlockComment = false;
289
+
290
+ for (let index = openParenIndex; index < sourceText.length; index += 1) {
291
+ const character = sourceText[index];
292
+ const nextCharacter = sourceText[index + 1] || "";
293
+
294
+ if (inLineComment) {
295
+ if (character === "\n") {
296
+ inLineComment = false;
297
+ }
298
+ continue;
299
+ }
300
+
301
+ if (inBlockComment) {
302
+ if (character === "*" && nextCharacter === "/") {
303
+ inBlockComment = false;
304
+ index += 1;
305
+ }
306
+ continue;
307
+ }
308
+
309
+ if (quote) {
310
+ if (character === quote && !isEscapedCharacter(sourceText, index)) {
311
+ quote = "";
312
+ }
313
+ continue;
314
+ }
315
+
316
+ if (character === "/" && nextCharacter === "/") {
317
+ inLineComment = true;
318
+ index += 1;
319
+ continue;
320
+ }
321
+
322
+ if (character === "/" && nextCharacter === "*") {
323
+ inBlockComment = true;
324
+ index += 1;
325
+ continue;
326
+ }
327
+
328
+ if (character === "'" || character === "\"" || character === "`") {
329
+ quote = character;
330
+ continue;
331
+ }
332
+
333
+ if (character === "(") {
334
+ parenDepth += 1;
335
+ continue;
336
+ }
337
+
338
+ if (character === ")") {
339
+ parenDepth -= 1;
340
+ if (parenDepth === 0) {
341
+ return index;
342
+ }
343
+ }
344
+ }
345
+
346
+ return -1;
347
+ }
348
+
349
+ function extractFirstArgumentText(argsText = "") {
350
+ let parenDepth = 0;
351
+ let braceDepth = 0;
352
+ let bracketDepth = 0;
353
+ let quote = "";
354
+ let inLineComment = false;
355
+ let inBlockComment = false;
356
+
357
+ for (let index = 0; index < argsText.length; index += 1) {
358
+ const character = argsText[index];
359
+ const nextCharacter = argsText[index + 1] || "";
360
+
361
+ if (inLineComment) {
362
+ if (character === "\n") {
363
+ inLineComment = false;
364
+ }
365
+ continue;
366
+ }
367
+
368
+ if (inBlockComment) {
369
+ if (character === "*" && nextCharacter === "/") {
370
+ inBlockComment = false;
371
+ index += 1;
372
+ }
373
+ continue;
374
+ }
375
+
376
+ if (quote) {
377
+ if (character === quote && !isEscapedCharacter(argsText, index)) {
378
+ quote = "";
379
+ }
380
+ continue;
381
+ }
382
+
383
+ if (character === "/" && nextCharacter === "/") {
384
+ inLineComment = true;
385
+ index += 1;
386
+ continue;
387
+ }
388
+
389
+ if (character === "/" && nextCharacter === "*") {
390
+ inBlockComment = true;
391
+ index += 1;
392
+ continue;
393
+ }
394
+
395
+ if (character === "'" || character === "\"" || character === "`") {
396
+ quote = character;
397
+ continue;
398
+ }
399
+
400
+ if (character === "(") {
401
+ parenDepth += 1;
402
+ continue;
403
+ }
404
+ if (character === ")") {
405
+ parenDepth -= 1;
406
+ continue;
407
+ }
408
+ if (character === "{") {
409
+ braceDepth += 1;
410
+ continue;
411
+ }
412
+ if (character === "}") {
413
+ braceDepth -= 1;
414
+ continue;
415
+ }
416
+ if (character === "[") {
417
+ bracketDepth += 1;
418
+ continue;
419
+ }
420
+ if (character === "]") {
421
+ bracketDepth -= 1;
422
+ continue;
423
+ }
424
+
425
+ if (character === "," && parenDepth === 0 && braceDepth === 0 && bracketDepth === 0) {
426
+ return argsText.slice(0, index);
427
+ }
428
+ }
429
+
430
+ return argsText;
431
+ }
432
+
433
+ function collectStaticImportBindings(sourceText = "") {
434
+ const bindings = new Map();
435
+ const importPattern = /^\s*import\s+([\s\S]*?)\s+from\s+["']([^"']+)["'];?/gmu;
436
+
437
+ for (const match of sourceText.matchAll(importPattern)) {
438
+ const specifierText = String(match[1] || "").trim();
439
+ const sourcePath = String(match[2] || "").trim();
440
+ if (!specifierText || !sourcePath) {
441
+ continue;
442
+ }
443
+
444
+ const namedMatch = specifierText.match(/\{([\s\S]*)\}/u);
445
+ if (namedMatch) {
446
+ const namedContent = String(namedMatch[1] || "");
447
+ for (const rawSpecifier of namedContent.split(",")) {
448
+ const specifier = String(rawSpecifier || "").trim();
449
+ if (!specifier) {
450
+ continue;
451
+ }
452
+ const aliasMatch = specifier.match(/^([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)$/u);
453
+ const localName = aliasMatch ? aliasMatch[2] : specifier;
454
+ if (/^[A-Za-z_$][\w$]*$/u.test(localName)) {
455
+ bindings.set(localName, sourcePath);
456
+ }
457
+ }
458
+ }
459
+
460
+ const leadingSpecifier = namedMatch
461
+ ? specifierText.slice(0, namedMatch.index).replace(/,$/u, "").trim()
462
+ : specifierText;
463
+ if (!leadingSpecifier) {
464
+ continue;
465
+ }
466
+
467
+ const namespaceMatch = leadingSpecifier.match(/^\*\s+as\s+([A-Za-z_$][\w$]*)$/u);
468
+ if (namespaceMatch) {
469
+ bindings.set(namespaceMatch[1], sourcePath);
470
+ continue;
471
+ }
472
+
473
+ if (/^[A-Za-z_$][\w$]*$/u.test(leadingSpecifier)) {
474
+ bindings.set(leadingSpecifier, sourcePath);
475
+ }
476
+ }
477
+
478
+ return bindings;
479
+ }
480
+
481
+ function isSharedListFiltersImportSource(sourcePath = "") {
482
+ return /(^|\/)shared\/[^/'"]*ListFilters(?:\.[A-Za-z0-9]+)?$/u.test(String(sourcePath || "").trim());
483
+ }
484
+
485
+ function findCallSites(sourceText = "", calleeName = "") {
486
+ const normalizedCalleeName = String(calleeName || "").trim();
487
+ if (!normalizedCalleeName) {
488
+ return [];
489
+ }
490
+
491
+ const callPattern = new RegExp(`\\b${normalizedCalleeName}\\s*\\(`, "gu");
492
+ const calls = [];
493
+
494
+ for (const match of sourceText.matchAll(callPattern)) {
495
+ const matchedText = String(match[0] || "");
496
+ const openParenIndex = (match.index || 0) + matchedText.lastIndexOf("(");
497
+ const closeParenIndex = findClosingParenIndex(sourceText, openParenIndex);
498
+ if (closeParenIndex < 0) {
499
+ continue;
500
+ }
501
+
502
+ calls.push({
503
+ calleeName: normalizedCalleeName,
504
+ index: match.index || 0,
505
+ openParenIndex,
506
+ closeParenIndex,
507
+ argsText: sourceText.slice(openParenIndex + 1, closeParenIndex)
508
+ });
509
+ }
510
+
511
+ return calls;
512
+ }
513
+
514
+ function collectFilterDefinitionOwnershipIssues({
515
+ sourceText = "",
516
+ relativePath = "",
517
+ issues = []
518
+ }) {
519
+ const importBindings = collectStaticImportBindings(sourceText);
520
+
521
+ for (const calleeName of FILTER_RUNTIME_CALLEES) {
522
+ for (const callSite of findCallSites(sourceText, calleeName)) {
523
+ const lineNumber = resolveLineNumberFromIndex(sourceText, callSite.index);
524
+ const firstArgument = extractFirstArgumentText(callSite.argsText).trim();
525
+
526
+ if (!firstArgument || firstArgument.startsWith("{")) {
527
+ issues.push(
528
+ `${relativePath}:${lineNumber}: [filters:shared-definition] do not inline structured filter definitions in ${calleeName}(...). Put them in packages/<crud>/src/shared/<crud>ListFilters.js and import that shared module.`
529
+ );
530
+ continue;
531
+ }
532
+
533
+ if (!/^[A-Za-z_$][\w$]*$/u.test(firstArgument)) {
534
+ issues.push(
535
+ `${relativePath}:${lineNumber}: [filters:shared-definition] ${calleeName}(...) must receive a definitions symbol imported from a CRUD shared *ListFilters module, not an ad-hoc expression.`
536
+ );
537
+ continue;
538
+ }
539
+
540
+ const importSource = importBindings.get(firstArgument) || "";
541
+ if (!isSharedListFiltersImportSource(importSource)) {
542
+ issues.push(
543
+ `${relativePath}:${lineNumber}: [filters:shared-definition] ${calleeName}(${firstArgument}, ...) must use definitions imported from a CRUD shared *ListFilters module. Found ${importSource ? `import source "${importSource}"` : "a local symbol"} instead.`
544
+ );
545
+ }
546
+ }
547
+ }
548
+ }
549
+
550
+ function collectFilterValidatorModeIssues({
551
+ sourceText = "",
552
+ relativePath = "",
553
+ issues = []
554
+ }) {
555
+ for (const callSite of findCallSites(sourceText, "createQueryValidator")) {
556
+ const lineNumber = resolveLineNumberFromIndex(sourceText, callSite.index);
557
+ const argsText = String(callSite.argsText || "").trim();
558
+ if (!argsText.startsWith("{") || !/\binvalidValues\s*:/u.test(argsText)) {
559
+ issues.push(
560
+ `${relativePath}:${lineNumber}: [filters:validator-mode] createQueryValidator(...) must be written explicitly as createQueryValidator({ invalidValues: "reject" | "discard" }). Do not rely on hidden defaults, aliases, or indirect option objects.`
561
+ );
562
+ }
563
+ }
564
+ }
565
+
231
566
  async function collectMdiSvgDoctorIssues({ appRoot, issues }) {
232
567
  if (!(await appUsesVuetifyMdiSvg(appRoot))) {
233
568
  return;
234
569
  }
235
570
 
236
571
  const vueFilePaths = [];
237
- for (const relativeRoot of MDI_SVG_SCAN_ROOTS) {
238
- await collectVueSourceFiles(path.join(appRoot, relativeRoot), vueFilePaths);
572
+ for (const relativeRoot of APP_SOURCE_SCAN_ROOTS) {
573
+ await collectAppSourceFiles(
574
+ path.join(appRoot, relativeRoot),
575
+ { extensions: VUE_SOURCE_EXTENSIONS },
576
+ vueFilePaths
577
+ );
239
578
  }
240
579
 
241
580
  vueFilePaths.sort((left, right) => left.localeCompare(right));
@@ -250,6 +589,38 @@ function createHealthCommands(ctx = {}) {
250
589
  }
251
590
  }
252
591
 
592
+ async function collectCrudFilterDoctorIssues({ appRoot, issues }) {
593
+ const sourceFilePaths = [];
594
+ for (const relativeRoot of APP_SOURCE_SCAN_ROOTS) {
595
+ await collectAppSourceFiles(path.join(appRoot, relativeRoot), undefined, sourceFilePaths);
596
+ }
597
+
598
+ sourceFilePaths.sort((left, right) => left.localeCompare(right));
599
+
600
+ for (const absolutePath of sourceFilePaths) {
601
+ const sourceText = await readFile(absolutePath, "utf8");
602
+ if (
603
+ !sourceText.includes("useCrudListFilters") &&
604
+ !sourceText.includes("createCrudListFilters") &&
605
+ !sourceText.includes("createQueryValidator")
606
+ ) {
607
+ continue;
608
+ }
609
+
610
+ const relativePath = normalizeRelativePath(appRoot, absolutePath);
611
+ collectFilterDefinitionOwnershipIssues({
612
+ sourceText,
613
+ relativePath,
614
+ issues
615
+ });
616
+ collectFilterValidatorModeIssues({
617
+ sourceText,
618
+ relativePath,
619
+ issues
620
+ });
621
+ }
622
+ }
623
+
253
624
  function collectDiLabelParityIssuesForPackage({ packageEntry, packageInsights }) {
254
625
  const packageId = String(packageEntry?.packageId || "").trim();
255
626
  const descriptor = ensureObject(packageEntry?.descriptor);
@@ -338,6 +709,10 @@ function createHealthCommands(ctx = {}) {
338
709
  appRoot,
339
710
  issues
340
711
  });
712
+ await collectCrudFilterDoctorIssues({
713
+ appRoot,
714
+ issues
715
+ });
341
716
 
342
717
  const payload = {
343
718
  appRoot,