@valbuild/language-server 0.124.0 → 0.126.0

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.
@@ -159,22 +159,71 @@ function negotiateProtocolVersion(client, server = SUPPORTED_PROTOCOL_VERSIONS)
159
159
  };
160
160
  }
161
161
 
162
+ var packageJson = {
163
+ name: "@valbuild/language-server",
164
+ "private": false,
165
+ description: "Val - editor language server (LSP)",
166
+ repository: {
167
+ type: "git",
168
+ url: "git+https://github.com/valbuild/val.git"
169
+ },
170
+ keywords: [
171
+ "CMS",
172
+ "lsp",
173
+ "language-server"
174
+ ],
175
+ version: "0.126.0",
176
+ bin: {
177
+ "val-language-server": "./bin.js"
178
+ },
179
+ main: "dist/valbuild-language-server.cjs.js",
180
+ module: "dist/valbuild-language-server.esm.js",
181
+ exports: {
182
+ ".": {
183
+ module: "./dist/valbuild-language-server.esm.js",
184
+ "default": "./dist/valbuild-language-server.cjs.js"
185
+ },
186
+ "./package.json": "./package.json"
187
+ },
188
+ types: "dist/valbuild-language-server.cjs.d.ts",
189
+ scripts: {
190
+ typecheck: "tsc --noEmit",
191
+ test: "jest",
192
+ "test:watch": "jest --watch",
193
+ bench: "node scripts/evalLatency.bench.js"
194
+ },
195
+ dependencies: {
196
+ "@valbuild/core": "workspace:*",
197
+ "@valbuild/server": "workspace:*",
198
+ "@valbuild/shared": "workspace:*",
199
+ typescript: "^6.0.3",
200
+ "vscode-languageserver": "^10.1.0",
201
+ "vscode-languageserver-textdocument": "^1.0.14"
202
+ },
203
+ devDependencies: {
204
+ "@types/jest": "^30.0.0",
205
+ "vscode-jsonrpc": "^9.0.2"
206
+ },
207
+ engines: {
208
+ node: "^20.19.0 || >=22"
209
+ },
210
+ files: [
211
+ "CHANGELOG.md",
212
+ "dist",
213
+ "bin.js",
214
+ "README.md"
215
+ ]
216
+ };
217
+
162
218
  /**
163
- * Version of this package, read from its own package.json.
219
+ * This package's version, inlined at build time.
164
220
  *
165
- * Mirrors how `@valbuild/core` reports `Internal.VERSION.core`
166
- * (see `packages/core/src/index.ts`): the built output lives one directory
167
- * below the package root, so `../package.json` resolves correctly. Returns
168
- * `null` rather than throwing if the file cannot be read — the version is only
169
- * ever used for display, never for behaviour.
221
+ * See `packages/core/src/index.ts` for why this is a static JSON import rather
222
+ * than `require("../package.json")`. The version is only ever displayed here,
223
+ * but the two should not drift apart in how they read it.
170
224
  */
171
225
  function getLanguageServerVersion() {
172
- try {
173
- // eslint-disable-next-line @typescript-eslint/no-require-imports
174
- return require("../package.json").version;
175
- } catch {
176
- return null;
177
- }
226
+ return packageJson.version ;
178
227
  }
179
228
 
180
229
  /**
@@ -847,19 +896,28 @@ function traverseObjectLiteral(node, sourceFile) {
847
896
  }
848
897
 
849
898
  /**
850
- * Adjudicating the media metadata placeholders core emits unconditionally.
899
+ * Adjudicating the media placeholders core emits unconditionally.
900
+ *
901
+ * A media schema cannot answer two of its own questions, because both need
902
+ * something core has no access to -- the bytes on disk, or the bytes on the
903
+ * content host. So it defers, by reporting an error that only means "nobody
904
+ * has looked yet":
851
905
  *
852
- * `ImageSchema.validate` cannot read bytes, so it never answers "does the
853
- * stored width/height/mimeType match the file". It defers instead: any
854
- * `s.image()` carrying metadata gets an `image:check-metadata` error whether
855
- * or not anything is wrong (`packages/core/src/schema/image.ts`), and
856
- * `s.file()` does the same with `file:check-metadata`.
906
+ * - **Metadata.** Any `s.image()` carrying metadata gets an
907
+ * `image:check-metadata` error whether or not anything is wrong
908
+ * (`packages/core/src/schema/image.ts`), and `s.file()` does the same with
909
+ * `file:check-metadata`.
910
+ * - **Remote refs.** Every `s.image().remote()` / `s.file().remote()` value
911
+ * whose path is a remote ref gets `Remote image was not checked.` with
912
+ * `image:check-remote` -- unconditionally, for every remote image in the
913
+ * project.
857
914
  *
858
- * `val validate` resolves those by running the fix handler and then
859
- * `createFixPatch` in report mode, which compares each field against the file
860
- * and returns one error per field that disagrees. The Studio cannot do that at
861
- * all -- a browser has no filesystem -- so it drops them wholesale
862
- * (`partitionValidationErrors` in `@valbuild/shared`).
915
+ * `val validate` resolves both by running the fix handler and then
916
+ * `createFixPatch` in report mode, which returns one error per real problem
917
+ * and nothing at all when there is none. The Studio cannot do that -- a
918
+ * browser has neither the filesystem nor the project's remote credentials --
919
+ * so it drops them wholesale (`partitionValidationErrors` in
920
+ * `@valbuild/shared`).
863
921
  *
864
922
  * An editor is in the CLI's position, not the browser's, and publishing the
865
923
  * placeholders raw put a permanent warning on every image in the project. So
@@ -875,15 +933,42 @@ function traverseObjectLiteral(node, sourceFile) {
875
933
  */
876
934
  const METADATA_CHECK_FIXES = ["image:check-metadata", "file:check-metadata"];
877
935
 
878
- /** One field of a media value that disagrees with the file behind it. */
936
+ /**
937
+ * The fixes that carry an unconditional remote-ref placeholder.
938
+ *
939
+ * The plural `images:check-remote` / `files:check-remote` are deliberately
940
+ * absent. `RecordSchema.validateMediaKey` emits those only for a key that is
941
+ * already wrong -- an unparseable remote URL, or one outside the gallery's
942
+ * directory -- and their messages say so. They are findings, not deferrals,
943
+ * and `createFixPatch` has no branch for them at all, so adjudicating one
944
+ * would come back empty and silently drop a real error.
945
+ */
946
+ const REMOTE_CHECK_FIXES = ["image:check-remote", "file:check-remote"];
947
+
948
+ /** One thing about a media value that a real check found to be wrong. */
879
949
 
880
950
  /**
881
- * The verdict on one placeholder. Empty means the metadata agrees with the
882
- * file and nothing should be published.
951
+ * The verdict on one placeholder. Empty means the check passed -- the metadata
952
+ * agrees with the file, or the remote ref is sound -- and nothing should be
953
+ * published.
954
+ */
955
+
956
+ /**
957
+ * Whether this error is the unconditional remote-ref deferral.
958
+ *
959
+ * Unlike the metadata deferral this needs no re-derivation: `image:check-remote`
960
+ * is attached to exactly one error in `ImageSchema.executeValidate`, the
961
+ * fall-through for a remote schema with a remote path, and `FileSchema` mirrors
962
+ * it. Everything else a remote media field can be wrong about carries a
963
+ * different fix (`image:upload-remote`, `image:download-remote`) or none.
883
964
  */
965
+ function isDeferredRemoteCheck(error) {
966
+ return (error.fixes ?? []).some(fix => REMOTE_CHECK_FIXES.includes(fix));
967
+ }
884
968
 
885
969
  /**
886
- * Whether this error is the unconditional deferral rather than a real finding.
970
+ * Whether this error is the unconditional METADATA deferral rather than a real
971
+ * finding.
887
972
  *
888
973
  * This is the whole difficulty of the change. `image:check-metadata` is
889
974
  * attached to FIVE different image errors, and only the last is a placeholder:
@@ -902,7 +987,7 @@ const METADATA_CHECK_FIXES = ["image:check-metadata", "file:check-metadata"];
902
987
  * There is no flag on `ValidationError` saying which is which, so the four
903
988
  * conditions are re-derived here from the same inputs core used. They are
904
989
  * transcribed from `ImageSchema.executeValidate`, in its order, and
905
- * `mediaMetadataChecks.test.ts` drives real core through all five cases to
990
+ * `mediaChecks.test.ts` drives real core through all five cases to
906
991
  * check that this agrees with it. If core grows a sixth condition, that test is
907
992
  * what catches it.
908
993
  *
@@ -947,33 +1032,33 @@ function isDeferredMediaMetadataCheck({
947
1032
  }
948
1033
 
949
1034
  /**
950
- * {@link isDeferredMediaMetadataCheck} for an error in a module, resolving the
951
- * schema it needs.
1035
+ * Whether an error in a module is either deferral, resolving the schema the
1036
+ * metadata classification needs.
952
1037
  *
953
1038
  * Both the adjudicator and `createValDiagnostics` have to make the same call,
954
1039
  * on the same inputs -- one to decide what to adjudicate, the other to decide
955
1040
  * what to publish -- so it lives here rather than being written out twice.
956
1041
  */
957
- function isDeferredMediaMetadataCheckAt({
1042
+ function isDeferredMediaCheckAt({
958
1043
  sourcePath,
959
1044
  error,
960
1045
  content
961
1046
  }) {
962
- return isDeferredMediaMetadataCheck({
1047
+ return isDeferredRemoteCheck(error) || isDeferredMediaMetadataCheck({
963
1048
  error,
964
1049
  schema: resolveSchemaAt$1(sourcePath, content)
965
1050
  });
966
1051
  }
967
1052
 
968
1053
  /**
969
- * Adjudicate every deferred metadata placeholder in `validation`.
1054
+ * Adjudicate every deferred media placeholder in `validation`.
970
1055
  *
971
1056
  * Async, and therefore separate from `createValDiagnostics`, which stays
972
1057
  * synchronous so it can be tested without a project -- the same split
973
1058
  * `resolveGalleryChecks` uses. The caller runs this first and passes the
974
1059
  * result in.
975
1060
  */
976
- async function resolveMediaMetadataChecks({
1061
+ async function resolveMediaChecks({
977
1062
  validation,
978
1063
  content,
979
1064
  valRoot,
@@ -982,14 +1067,14 @@ async function resolveMediaMetadataChecks({
982
1067
  const verdicts = new Map();
983
1068
  for (const [sourcePath, errors] of Object.entries(validation)) {
984
1069
  for (const error of errors) {
985
- if (!isDeferredMediaMetadataCheckAt({
1070
+ if (!isDeferredMediaCheckAt({
986
1071
  sourcePath,
987
1072
  error,
988
1073
  content
989
1074
  })) {
990
1075
  continue;
991
1076
  }
992
- const key = mediaMetadataCheckKey(sourcePath, error);
1077
+ const key = mediaCheckKey(sourcePath, error);
993
1078
  try {
994
1079
  verdicts.set(key, await adjudicate({
995
1080
  sourcePath,
@@ -1001,7 +1086,7 @@ async function resolveMediaMetadataChecks({
1001
1086
  } catch {
1002
1087
  // This runs inside `validate`, which publishes NOTHING if it throws --
1003
1088
  // one bad image would silently clear every Val diagnostic in the file.
1004
- // Keep the placeholder rather than claiming the metadata is fine.
1089
+ // Keep the placeholder rather than claiming the check passed.
1005
1090
  verdicts.set(key, keep(error));
1006
1091
  }
1007
1092
  }
@@ -1013,7 +1098,7 @@ async function resolveMediaMetadataChecks({
1013
1098
  * Key for one placeholder. A value can in principle carry more than one, so
1014
1099
  * the fix names are part of the key -- as they are for the gallery checks.
1015
1100
  */
1016
- function mediaMetadataCheckKey(sourcePath, error) {
1101
+ function mediaCheckKey(sourcePath, error) {
1017
1102
  return `${sourcePath}|${(error.fixes ?? []).join(",")}`;
1018
1103
  }
1019
1104
 
@@ -1033,6 +1118,17 @@ function keep(error) {
1033
1118
  /**
1034
1119
  * What `createFixPatch` says about one placeholder, in report mode.
1035
1120
  *
1121
+ * Which placeholder decides only what has to be true before asking; the asking
1122
+ * is the same call either way, and `reportFixPatch` is it.
1123
+ */
1124
+ async function adjudicate(args) {
1125
+ return isDeferredRemoteCheck(args.error) ? adjudicateRemote(args) : adjudicateMetadata(args);
1126
+ }
1127
+
1128
+ /**
1129
+ * The metadata placeholder: does the stored width/height/mimeType match the
1130
+ * bytes?
1131
+ *
1036
1132
  * `val validate` runs the fix handler first and then `createFixPatch`, but for
1037
1133
  * these four fixes the handler is `handleFileMetadata`, whose whole job is the
1038
1134
  * precondition "the ref resolves and the file is on disk". That precondition is
@@ -1054,7 +1150,7 @@ function keep(error) {
1054
1150
  * `createFixPatch` reads and decodes the image itself, so this is one file read
1055
1151
  * per media field per validation pass, and nothing here should add another.
1056
1152
  */
1057
- async function adjudicate({
1153
+ async function adjudicateMetadata({
1058
1154
  sourcePath,
1059
1155
  error,
1060
1156
  content,
@@ -1075,19 +1171,17 @@ async function adjudicate({
1075
1171
  // means this verdict never has the last word on a file that is not there.
1076
1172
  return keep(error);
1077
1173
  }
1078
- let fixed;
1079
- try {
1080
- fixed = await createFixPatch({
1081
- projectRoot: valRoot,
1082
- remoteHost
1083
- },
1084
- // `false`: this is a question, not a fix. Asking for the patch would have
1085
- // createFixPatch read and rewrite files behind the editor's back.
1086
- false, sourcePath, error, {}, content.source, content.schema);
1087
- } catch {
1174
+ const reported = await reportFixPatch({
1175
+ sourcePath,
1176
+ error,
1177
+ content,
1178
+ valRoot,
1179
+ remoteHost
1180
+ });
1181
+ if (reported === undefined) {
1088
1182
  return keep(error);
1089
1183
  }
1090
- return (fixed?.remainingErrors ?? []).map(remaining => ({
1184
+ return reported.map(remaining => ({
1091
1185
  message: remaining.message,
1092
1186
  // `createFixPatch` clears `fixes` on the per-field errors it reports, but
1093
1187
  // the fix is still available and still the remedy -- it is what the
@@ -1103,6 +1197,77 @@ async function adjudicate({
1103
1197
  }));
1104
1198
  }
1105
1199
 
1200
+ /**
1201
+ * The remote-ref placeholder: is the ref this value carries still the ref the
1202
+ * bytes behind it produce?
1203
+ *
1204
+ * `handleRemoteFileCheck` is a no-op that asks for the patch, so unlike the
1205
+ * metadata case there is no precondition of its own to stand in for -- the
1206
+ * whole check is `checkRemoteRef` inside `createFixPatch`. It recomputes the
1207
+ * validation hash from the schema, the file extension, the metadata on the
1208
+ * value and the file hash in the ref; when that agrees with the ref, nothing is
1209
+ * downloaded and nothing is reported. Only a ref that no longer adds up costs a
1210
+ * download, and that is cached under `.val/remote-file-cache`.
1211
+ *
1212
+ * The findings deliberately carry no `fixes`. `createFixPatch` clears them
1213
+ * (`Remote ref: ... is not valid. Use the --fix flag to fix this issue.`
1214
+ * arrives with `fixes: undefined`), no quick fix is registered for
1215
+ * `image:check-remote` -- rewriting the ref needs the bytes off the content
1216
+ * host, so it is `val validate --fix`'s job, not a lightbulb's -- and dropping
1217
+ * them is what makes this an Error rather than a Warning, matching the `✘` the
1218
+ * CLI prints for the same finding.
1219
+ */
1220
+ async function adjudicateRemote({
1221
+ sourcePath,
1222
+ error,
1223
+ content,
1224
+ valRoot,
1225
+ remoteHost
1226
+ }) {
1227
+ const reported = await reportFixPatch({
1228
+ sourcePath,
1229
+ error,
1230
+ content,
1231
+ valRoot,
1232
+ remoteHost
1233
+ });
1234
+ if (reported === undefined) {
1235
+ return keep(error);
1236
+ }
1237
+ return reported.map(remaining => ({
1238
+ message: remaining.message,
1239
+ ...(error.value !== undefined ? {
1240
+ value: error.value
1241
+ } : {})
1242
+ }));
1243
+ }
1244
+
1245
+ /**
1246
+ * `createFixPatch` in report mode, or `undefined` if it could not answer.
1247
+ *
1248
+ * `false` is the load-bearing argument: this is a question, not a fix. Asking
1249
+ * for the patch would have `createFixPatch` read and rewrite files behind the
1250
+ * editor's back -- and, for a remote ref, re-derive one from bytes it had to
1251
+ * download first.
1252
+ */
1253
+ async function reportFixPatch({
1254
+ sourcePath,
1255
+ error,
1256
+ content,
1257
+ valRoot,
1258
+ remoteHost
1259
+ }) {
1260
+ try {
1261
+ const fixed = await createFixPatch({
1262
+ projectRoot: valRoot,
1263
+ remoteHost
1264
+ }, false, sourcePath, error, {}, content.source, content.schema);
1265
+ return fixed?.remainingErrors ?? [];
1266
+ } catch {
1267
+ return undefined;
1268
+ }
1269
+ }
1270
+
1106
1271
  /**
1107
1272
  * The `accept` a media schema declares, if any.
1108
1273
  *
@@ -1227,8 +1392,18 @@ const FALLBACK_RANGE = {
1227
1392
  }
1228
1393
  };
1229
1394
 
1230
- /** Fixes that operate on a file or image reference. */
1231
- const FILE_FIXES = ["image:add-metadata", "image:check-metadata", "image:upload-remote", "image:download-remote", "image:check-remote", "file:add-metadata", "file:check-metadata", "file:upload-remote", "file:download-remote", "file:check-remote"];
1395
+ /**
1396
+ * Fixes that operate on a LOCAL file or image reference.
1397
+ *
1398
+ * `*:check-remote` is deliberately absent. A remote value's bytes are on the
1399
+ * content host by design, so "is it on disk" is never its question -- and for
1400
+ * a path that is neither under `/public` nor a parseable remote ref, this
1401
+ * check answered "File <root>/... does not exist" and pre-empted the remote
1402
+ * adjudication, which has the real answer ("Invalid remote ref: ..."). The
1403
+ * upload/download fixes stay: `*:upload-remote` is reported on a local path
1404
+ * and a missing file really is its problem.
1405
+ */
1406
+ const FILE_FIXES = ["image:add-metadata", "image:check-metadata", "image:upload-remote", "image:download-remote", "file:add-metadata", "file:check-metadata", "file:upload-remote", "file:download-remote"];
1232
1407
  function build(range, message, data) {
1233
1408
  return {
1234
1409
  range,
@@ -1246,7 +1421,7 @@ function createValDiagnostics({
1246
1421
  valRoot,
1247
1422
  snapshot,
1248
1423
  galleryChecks,
1249
- mediaMetadataChecks
1424
+ mediaChecks
1250
1425
  }) {
1251
1426
  if (content.errors === false) {
1252
1427
  return [];
@@ -1318,18 +1493,20 @@ function createValDiagnostics({
1318
1493
  continue;
1319
1494
  }
1320
1495
 
1321
- // An unconditional media metadata placeholder: core reports
1322
- // `image:check-metadata` on every `s.image()` carrying metadata, whether
1323
- // or not it disagrees with the file. Show only what the adjudication
1324
- // actually found, and nothing when it found nothing. Deliberately after
1325
- // the missing-file branch above, so a deleted file is still reported as
1326
- // `val/file-not-found` rather than as a metadata mismatch.
1327
- if (isDeferredMediaMetadataCheckAt({
1496
+ // An unconditional media placeholder: core reports
1497
+ // `image:check-metadata` on every `s.image()` carrying metadata whether
1498
+ // or not it disagrees with the file, and `Remote image was not checked.`
1499
+ // on every remote image whether or not its ref is stale. Show only what
1500
+ // the adjudication actually found, and nothing when it found nothing.
1501
+ // Deliberately after the missing-file branch above, so a deleted file is
1502
+ // still reported as `val/file-not-found` rather than as a metadata
1503
+ // mismatch.
1504
+ if (isDeferredMediaCheckAt({
1328
1505
  sourcePath,
1329
1506
  error,
1330
1507
  content
1331
1508
  })) {
1332
- const verdict = mediaMetadataChecks?.get(mediaMetadataCheckKey(sourcePath, error));
1509
+ const verdict = mediaChecks?.get(mediaCheckKey(sourcePath, error));
1333
1510
  for (const finding of verdict ?? []) {
1334
1511
  diagnostics.push(build(rangeOf(sourcePath, modulePathMap), finding.message, {
1335
1512
  code: "val/validation",
@@ -4058,9 +4235,11 @@ function createValLanguageServer(connection) {
4058
4235
  })
4059
4236
  }) : undefined;
4060
4237
  // Core also defers "does this image's stored metadata match its file",
4061
- // reporting it on every `s.image()` that carries any metadata. Same
4062
- // treatment: adjudicate with the fix machinery before showing anything.
4063
- const mediaMetadataChecks = result.content.errors !== false && result.content.errors.validation ? await resolveMediaMetadataChecks({
4238
+ // reporting it on every `s.image()` that carries any metadata, and
4239
+ // "is this remote ref still valid", reporting it on every remote image.
4240
+ // Same treatment: adjudicate with the fix machinery before showing
4241
+ // anything.
4242
+ const mediaChecks = result.content.errors !== false && result.content.errors.validation ? await resolveMediaChecks({
4064
4243
  validation: result.content.errors.validation,
4065
4244
  content: result.content,
4066
4245
  valRoot: activeProject.valRoot
@@ -4076,8 +4255,8 @@ function createValLanguageServer(connection) {
4076
4255
  ...(galleryChecks ? {
4077
4256
  galleryChecks
4078
4257
  } : {}),
4079
- ...(mediaMetadataChecks ? {
4080
- mediaMetadataChecks
4258
+ ...(mediaChecks ? {
4259
+ mediaChecks
4081
4260
  } : {})
4082
4261
  });
4083
4262
  const unregistered = findMissingModuleDiagnostic(project.valRoot, moduleFilePath, readOpenDocument);
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "lsp",
12
12
  "language-server"
13
13
  ],
14
- "version": "0.124.0",
14
+ "version": "0.126.0",
15
15
  "bin": {
16
16
  "val-language-server": "./bin.js"
17
17
  },
@@ -29,9 +29,9 @@
29
29
  "typescript": "^6.0.3",
30
30
  "vscode-languageserver": "^10.1.0",
31
31
  "vscode-languageserver-textdocument": "^1.0.14",
32
- "@valbuild/core": "0.124.0",
33
- "@valbuild/server": "0.124.0",
34
- "@valbuild/shared": "0.124.0"
32
+ "@valbuild/core": "0.126.0",
33
+ "@valbuild/server": "0.126.0",
34
+ "@valbuild/shared": "0.126.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/jest": "^30.0.0",