@jskit-ai/jskit-cli 0.2.65 → 0.2.67

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.65",
3
+ "version": "0.2.67",
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.64",
24
- "@jskit-ai/kernel": "0.1.56",
25
- "@jskit-ai/shell-web": "0.1.55"
23
+ "@jskit-ai/jskit-catalog": "0.1.66",
24
+ "@jskit-ai/kernel": "0.1.58",
25
+ "@jskit-ai/shell-web": "0.1.57"
26
26
  },
27
27
  "engines": {
28
28
  "node": "20.x"
@@ -196,7 +196,10 @@ async function applyFileMutations(
196
196
  managedMigrations,
197
197
  touchedFiles,
198
198
  warnings = [],
199
- existingManagedFiles = []
199
+ existingManagedFiles = [],
200
+ {
201
+ reapplyManagedAppFiles = false
202
+ } = {}
200
203
  ) {
201
204
  const existingManagedFilesByPath = new Map();
202
205
  for (const managedFileValue of ensureArray(existingManagedFiles)) {
@@ -242,8 +245,16 @@ async function applyFileMutations(
242
245
  const relativeTargetPath = normalizeRelativePath(appRoot, targetPath);
243
246
  const previous = await readFileBufferIfExists(targetPath);
244
247
  const existingManaged = existingManagedFilesByPath.get(relativeTargetPath);
248
+ const existingManagedHash = String(existingManaged?.hash || "").trim();
249
+ const currentContentMatchesManagedVersion =
250
+ previous.exists &&
251
+ existingManagedHash &&
252
+ hashBuffer(previous.buffer) === existingManagedHash;
253
+ const canSafelyReapplyManagedAppFile =
254
+ reapplyManagedAppFiles === true &&
255
+ (!previous.exists || currentContentMatchesManagedVersion);
245
256
 
246
- if (mutation.ownership === "app" && existingManaged) {
257
+ if (mutation.ownership === "app" && existingManaged && !canSafelyReapplyManagedAppFile) {
247
258
  managedFiles.push({
248
259
  ...existingManaged,
249
260
  path: relativeTargetPath,
@@ -503,7 +503,10 @@ async function applyPackageInstall({
503
503
  managedRecord.managed.migrations,
504
504
  touchedFiles,
505
505
  mutationWarnings,
506
- ensureArray(existingManaged.files)
506
+ ensureArray(existingManaged.files),
507
+ {
508
+ reapplyManagedAppFiles: Object.keys(existingInstall).length > 0
509
+ }
507
510
  );
508
511
 
509
512
  await applyTextMutations(
@@ -1,5 +1,5 @@
1
1
  import path from "node:path";
2
- import { mkdir, rm, symlink } from "node:fs/promises";
2
+ import { lstat, mkdir, readFile, readlink, rm, symlink } from "node:fs/promises";
3
3
  import {
4
4
  discoverLocalPackageMap,
5
5
  fileExists,
@@ -8,6 +8,117 @@ import {
8
8
  resolveSymlinkType
9
9
  } from "./shared.js";
10
10
 
11
+ const COMPANION_PACKAGES = Object.freeze([
12
+ Object.freeze({
13
+ packageName: "json-rest-schema",
14
+ repoDirName: "json-rest-schema"
15
+ }),
16
+ Object.freeze({
17
+ packageName: "json-rest-stores",
18
+ repoDirName: "json-rest-stores"
19
+ })
20
+ ]);
21
+
22
+ function collectDeclaredPackageNames(packageJson = {}) {
23
+ const names = new Set();
24
+ const sections = [
25
+ packageJson?.dependencies,
26
+ packageJson?.devDependencies,
27
+ packageJson?.optionalDependencies,
28
+ packageJson?.peerDependencies
29
+ ];
30
+
31
+ for (const section of sections) {
32
+ if (!section || typeof section !== "object" || Array.isArray(section)) {
33
+ continue;
34
+ }
35
+
36
+ for (const packageName of Object.keys(section)) {
37
+ const normalizedPackageName = String(packageName || "").trim();
38
+ if (normalizedPackageName) {
39
+ names.add(normalizedPackageName);
40
+ }
41
+ }
42
+ }
43
+
44
+ return names;
45
+ }
46
+
47
+ async function verifySymlinkTarget(targetPath = "", sourceDir = "", {
48
+ packageName = ""
49
+ } = {}) {
50
+ const stats = await lstat(targetPath);
51
+ if (!stats.isSymbolicLink()) {
52
+ throw new Error(`[link-local] expected ${packageName || targetPath} to be a symlink after linking.`);
53
+ }
54
+
55
+ const linkedTarget = await readlink(targetPath);
56
+ if (linkedTarget !== sourceDir) {
57
+ throw new Error(
58
+ `[link-local] expected ${packageName || targetPath} to link to ${sourceDir}, got ${linkedTarget}.`
59
+ );
60
+ }
61
+ }
62
+
63
+ async function maybeLinkCompanionPackages({
64
+ appRoot = "",
65
+ repoRoot = "",
66
+ stdout,
67
+ createCliError
68
+ }) {
69
+ const companionRoot = path.dirname(repoRoot);
70
+ const appPackageJsonPath = path.join(appRoot, "package.json");
71
+ let appPackageJson = {};
72
+ try {
73
+ appPackageJson = JSON.parse(await readFile(appPackageJsonPath, "utf8"));
74
+ } catch {
75
+ appPackageJson = {};
76
+ }
77
+ const declaredPackageNames = collectDeclaredPackageNames(appPackageJson);
78
+ let linkedCount = 0;
79
+
80
+ for (const companion of COMPANION_PACKAGES) {
81
+ if (!declaredPackageNames.has(companion.packageName)) {
82
+ continue;
83
+ }
84
+
85
+ const sourceDir = path.join(companionRoot, companion.repoDirName);
86
+ const packageJsonPath = path.join(sourceDir, "package.json");
87
+ if (!(await fileExists(packageJsonPath))) {
88
+ throw createCliError(
89
+ `[link-local] companion package ${companion.packageName} is declared by the app but local source was not found at ${sourceDir}.`,
90
+ { exitCode: 1 }
91
+ );
92
+ }
93
+
94
+ let packageJson = {};
95
+ try {
96
+ packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
97
+ } catch {
98
+ continue;
99
+ }
100
+
101
+ if (String(packageJson?.name || "").trim() !== companion.packageName) {
102
+ throw createCliError(
103
+ `[link-local] companion source at ${sourceDir} does not match expected package ${companion.packageName}.`,
104
+ { exitCode: 1 }
105
+ );
106
+ }
107
+
108
+ const targetPath = path.join(appRoot, "node_modules", companion.packageName);
109
+ await mkdir(path.dirname(targetPath), { recursive: true });
110
+ await rm(targetPath, { recursive: true, force: true });
111
+ await symlink(sourceDir, targetPath, resolveSymlinkType());
112
+ await verifySymlinkTarget(targetPath, sourceDir, {
113
+ packageName: companion.packageName
114
+ });
115
+ stdout.write(`[link-local] linked ${companion.packageName} -> ${sourceDir}\n`);
116
+ linkedCount += 1;
117
+ }
118
+
119
+ return linkedCount;
120
+ }
121
+
11
122
  async function runAppLinkLocalPackagesCommand(ctx = {}, { appRoot = "", options = {}, stdout }) {
12
123
  const { createCliError } = ctx;
13
124
  const explicitRepoRoot = String(options?.inlineOptions?.["repo-root"] || "").trim();
@@ -51,6 +162,13 @@ async function runAppLinkLocalPackagesCommand(ctx = {}, { appRoot = "", options
51
162
  linkedCount += 1;
52
163
  }
53
164
 
165
+ linkedCount += await maybeLinkCompanionPackages({
166
+ appRoot,
167
+ repoRoot,
168
+ stdout,
169
+ createCliError
170
+ });
171
+
54
172
  if (await fileExists(viteCacheDirectory)) {
55
173
  await rm(viteCacheDirectory, { recursive: true, force: true });
56
174
  stdout.write(`[link-local] cleared Vite cache at ${viteCacheDirectory}\n`);
@@ -75,6 +75,11 @@ function createHealthCommands(ctx = {}) {
75
75
  "createCrudListFilters",
76
76
  "useCrudListFilters"
77
77
  ]);
78
+ const CRUD_TRANSPORT_RUNTIME_CALLEES = Object.freeze([
79
+ "useCrudList",
80
+ "useCrudView",
81
+ "useCrudAddEdit"
82
+ ]);
78
83
 
79
84
  function collectDescriptorContainerTokens({ packageId, side, values, issues }) {
80
85
  const declaredTokens = new Set();
@@ -485,6 +490,146 @@ function createHealthCommands(ctx = {}) {
485
490
  return bindings;
486
491
  }
487
492
 
493
+ function hasTopLevelObjectProperty(sourceText = "", propertyName = "") {
494
+ const normalizedPropertyName = String(propertyName || "").trim();
495
+ const normalizedSourceText = String(sourceText || "").trim();
496
+ if (!normalizedPropertyName || !normalizedSourceText.startsWith("{")) {
497
+ return false;
498
+ }
499
+
500
+ let parenDepth = 0;
501
+ let braceDepth = 0;
502
+ let bracketDepth = 0;
503
+ let inLineComment = false;
504
+ let inBlockComment = false;
505
+
506
+ for (let index = 0; index < normalizedSourceText.length; index += 1) {
507
+ const character = normalizedSourceText[index];
508
+ const nextCharacter = normalizedSourceText[index + 1] || "";
509
+
510
+ if (inLineComment) {
511
+ if (character === "\n") {
512
+ inLineComment = false;
513
+ }
514
+ continue;
515
+ }
516
+
517
+ if (inBlockComment) {
518
+ if (character === "*" && nextCharacter === "/") {
519
+ inBlockComment = false;
520
+ index += 1;
521
+ }
522
+ continue;
523
+ }
524
+
525
+ if (character === "/" && nextCharacter === "/") {
526
+ inLineComment = true;
527
+ index += 1;
528
+ continue;
529
+ }
530
+
531
+ if (character === "/" && nextCharacter === "*") {
532
+ inBlockComment = true;
533
+ index += 1;
534
+ continue;
535
+ }
536
+
537
+ if (character === "'" || character === "\"") {
538
+ const quote = character;
539
+ const stringStart = index + 1;
540
+ let stringEnd = stringStart;
541
+ for (; stringEnd < normalizedSourceText.length; stringEnd += 1) {
542
+ if (
543
+ normalizedSourceText[stringEnd] === quote &&
544
+ !isEscapedCharacter(normalizedSourceText, stringEnd)
545
+ ) {
546
+ break;
547
+ }
548
+ }
549
+
550
+ const stringValue = normalizedSourceText.slice(stringStart, stringEnd);
551
+ index = stringEnd;
552
+ if (braceDepth === 1 && parenDepth === 0 && bracketDepth === 0 && stringValue === normalizedPropertyName) {
553
+ let cursor = index + 1;
554
+ while (/\s/u.test(normalizedSourceText[cursor] || "")) {
555
+ cursor += 1;
556
+ }
557
+ if (normalizedSourceText[cursor] === ":") {
558
+ return true;
559
+ }
560
+ }
561
+ continue;
562
+ }
563
+
564
+ if (character === "`") {
565
+ for (index += 1; index < normalizedSourceText.length; index += 1) {
566
+ if (
567
+ normalizedSourceText[index] === "`" &&
568
+ !isEscapedCharacter(normalizedSourceText, index)
569
+ ) {
570
+ break;
571
+ }
572
+ }
573
+ continue;
574
+ }
575
+
576
+ if (character === "(") {
577
+ parenDepth += 1;
578
+ continue;
579
+ }
580
+ if (character === ")") {
581
+ parenDepth -= 1;
582
+ continue;
583
+ }
584
+ if (character === "{") {
585
+ braceDepth += 1;
586
+ continue;
587
+ }
588
+ if (character === "}") {
589
+ braceDepth -= 1;
590
+ continue;
591
+ }
592
+ if (character === "[") {
593
+ bracketDepth += 1;
594
+ continue;
595
+ }
596
+ if (character === "]") {
597
+ bracketDepth -= 1;
598
+ continue;
599
+ }
600
+
601
+ if (
602
+ braceDepth === 1 &&
603
+ parenDepth === 0 &&
604
+ bracketDepth === 0 &&
605
+ /[A-Za-z_$]/u.test(character)
606
+ ) {
607
+ const identifierStart = index;
608
+ for (index += 1; index < normalizedSourceText.length; index += 1) {
609
+ if (!/[\w$]/u.test(normalizedSourceText[index] || "")) {
610
+ break;
611
+ }
612
+ }
613
+
614
+ const identifier = normalizedSourceText.slice(identifierStart, index);
615
+ index -= 1;
616
+ if (identifier !== normalizedPropertyName) {
617
+ continue;
618
+ }
619
+
620
+ let cursor = index + 1;
621
+ while (/\s/u.test(normalizedSourceText[cursor] || "")) {
622
+ cursor += 1;
623
+ }
624
+ if (normalizedSourceText[cursor] === ":") {
625
+ return true;
626
+ }
627
+ }
628
+ }
629
+
630
+ return false;
631
+ }
632
+
488
633
  function isSharedListFiltersImportSource(sourcePath = "") {
489
634
  return /(^|\/)shared\/[^/'"]*ListFilters(?:\.[A-Za-z0-9]+)?$/u.test(String(sourcePath || "").trim());
490
635
  }
@@ -570,6 +715,26 @@ function createHealthCommands(ctx = {}) {
570
715
  }
571
716
  }
572
717
 
718
+ function collectCrudTransportOwnershipIssues({
719
+ sourceText = "",
720
+ relativePath = "",
721
+ issues = []
722
+ }) {
723
+ for (const calleeName of CRUD_TRANSPORT_RUNTIME_CALLEES) {
724
+ for (const callSite of findCallSites(sourceText, calleeName)) {
725
+ const firstArgument = extractFirstArgumentText(callSite.argsText).trim();
726
+ if (!hasTopLevelObjectProperty(firstArgument, "transport")) {
727
+ continue;
728
+ }
729
+
730
+ const lineNumber = resolveLineNumberFromIndex(sourceText, callSite.index);
731
+ issues.push(
732
+ `${relativePath}:${lineNumber}: [crud:transport-derived] do not pass explicit transport to ${calleeName}(...). Let the shared CRUD resource derive JSON:API transport automatically, or drop to useList/useView/useAddEdit/usersWebHttpClient.request(...) for custom transport behavior.`
733
+ );
734
+ }
735
+ }
736
+ }
737
+
573
738
  async function collectMdiSvgDoctorIssues({ appRoot, issues }) {
574
739
  if (!(await appUsesVuetifyMdiSvg(appRoot))) {
575
740
  return;
@@ -609,7 +774,10 @@ function createHealthCommands(ctx = {}) {
609
774
  if (
610
775
  !sourceText.includes("useCrudListFilters") &&
611
776
  !sourceText.includes("createCrudListFilters") &&
612
- !sourceText.includes("createQueryValidator")
777
+ !sourceText.includes("createQueryValidator") &&
778
+ !sourceText.includes("useCrudList") &&
779
+ !sourceText.includes("useCrudView") &&
780
+ !sourceText.includes("useCrudAddEdit")
613
781
  ) {
614
782
  continue;
615
783
  }
@@ -625,6 +793,11 @@ function createHealthCommands(ctx = {}) {
625
793
  relativePath,
626
794
  issues
627
795
  });
796
+ collectCrudTransportOwnershipIssues({
797
+ sourceText,
798
+ relativePath,
799
+ issues
800
+ });
628
801
  }
629
802
  }
630
803