@valbuild/language-server 0.124.0 → 0.125.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.
@@ -170,22 +170,71 @@ function negotiateProtocolVersion(client, server = SUPPORTED_PROTOCOL_VERSIONS)
170
170
  };
171
171
  }
172
172
 
173
+ var packageJson = {
174
+ name: "@valbuild/language-server",
175
+ "private": false,
176
+ description: "Val - editor language server (LSP)",
177
+ repository: {
178
+ type: "git",
179
+ url: "git+https://github.com/valbuild/val.git"
180
+ },
181
+ keywords: [
182
+ "CMS",
183
+ "lsp",
184
+ "language-server"
185
+ ],
186
+ version: "0.125.0",
187
+ bin: {
188
+ "val-language-server": "./bin.js"
189
+ },
190
+ main: "dist/valbuild-language-server.cjs.js",
191
+ module: "dist/valbuild-language-server.esm.js",
192
+ exports: {
193
+ ".": {
194
+ module: "./dist/valbuild-language-server.esm.js",
195
+ "default": "./dist/valbuild-language-server.cjs.js"
196
+ },
197
+ "./package.json": "./package.json"
198
+ },
199
+ types: "dist/valbuild-language-server.cjs.d.ts",
200
+ scripts: {
201
+ typecheck: "tsc --noEmit",
202
+ test: "jest",
203
+ "test:watch": "jest --watch",
204
+ bench: "node scripts/evalLatency.bench.js"
205
+ },
206
+ dependencies: {
207
+ "@valbuild/core": "workspace:*",
208
+ "@valbuild/server": "workspace:*",
209
+ "@valbuild/shared": "workspace:*",
210
+ typescript: "^6.0.3",
211
+ "vscode-languageserver": "^10.1.0",
212
+ "vscode-languageserver-textdocument": "^1.0.14"
213
+ },
214
+ devDependencies: {
215
+ "@types/jest": "^30.0.0",
216
+ "vscode-jsonrpc": "^9.0.2"
217
+ },
218
+ engines: {
219
+ node: "^20.19.0 || >=22"
220
+ },
221
+ files: [
222
+ "CHANGELOG.md",
223
+ "dist",
224
+ "bin.js",
225
+ "README.md"
226
+ ]
227
+ };
228
+
173
229
  /**
174
- * Version of this package, read from its own package.json.
230
+ * This package's version, inlined at build time.
175
231
  *
176
- * Mirrors how `@valbuild/core` reports `Internal.VERSION.core`
177
- * (see `packages/core/src/index.ts`): the built output lives one directory
178
- * below the package root, so `../package.json` resolves correctly. Returns
179
- * `null` rather than throwing if the file cannot be read — the version is only
180
- * ever used for display, never for behaviour.
232
+ * See `packages/core/src/index.ts` for why this is a static JSON import rather
233
+ * than `require("../package.json")`. The version is only ever displayed here,
234
+ * but the two should not drift apart in how they read it.
181
235
  */
182
236
  function getLanguageServerVersion() {
183
- try {
184
- // eslint-disable-next-line @typescript-eslint/no-require-imports
185
- return require("../package.json").version;
186
- } catch {
187
- return null;
188
- }
237
+ return packageJson.version ;
189
238
  }
190
239
 
191
240
  /**
@@ -858,19 +907,28 @@ function traverseObjectLiteral(node, sourceFile) {
858
907
  }
859
908
 
860
909
  /**
861
- * Adjudicating the media metadata placeholders core emits unconditionally.
910
+ * Adjudicating the media placeholders core emits unconditionally.
911
+ *
912
+ * A media schema cannot answer two of its own questions, because both need
913
+ * something core has no access to -- the bytes on disk, or the bytes on the
914
+ * content host. So it defers, by reporting an error that only means "nobody
915
+ * has looked yet":
862
916
  *
863
- * `ImageSchema.validate` cannot read bytes, so it never answers "does the
864
- * stored width/height/mimeType match the file". It defers instead: any
865
- * `s.image()` carrying metadata gets an `image:check-metadata` error whether
866
- * or not anything is wrong (`packages/core/src/schema/image.ts`), and
867
- * `s.file()` does the same with `file:check-metadata`.
917
+ * - **Metadata.** Any `s.image()` carrying metadata gets an
918
+ * `image:check-metadata` error whether or not anything is wrong
919
+ * (`packages/core/src/schema/image.ts`), and `s.file()` does the same with
920
+ * `file:check-metadata`.
921
+ * - **Remote refs.** Every `s.image().remote()` / `s.file().remote()` value
922
+ * whose path is a remote ref gets `Remote image was not checked.` with
923
+ * `image:check-remote` -- unconditionally, for every remote image in the
924
+ * project.
868
925
  *
869
- * `val validate` resolves those by running the fix handler and then
870
- * `createFixPatch` in report mode, which compares each field against the file
871
- * and returns one error per field that disagrees. The Studio cannot do that at
872
- * all -- a browser has no filesystem -- so it drops them wholesale
873
- * (`partitionValidationErrors` in `@valbuild/shared`).
926
+ * `val validate` resolves both by running the fix handler and then
927
+ * `createFixPatch` in report mode, which returns one error per real problem
928
+ * and nothing at all when there is none. The Studio cannot do that -- a
929
+ * browser has neither the filesystem nor the project's remote credentials --
930
+ * so it drops them wholesale (`partitionValidationErrors` in
931
+ * `@valbuild/shared`).
874
932
  *
875
933
  * An editor is in the CLI's position, not the browser's, and publishing the
876
934
  * placeholders raw put a permanent warning on every image in the project. So
@@ -886,15 +944,42 @@ function traverseObjectLiteral(node, sourceFile) {
886
944
  */
887
945
  const METADATA_CHECK_FIXES = ["image:check-metadata", "file:check-metadata"];
888
946
 
889
- /** One field of a media value that disagrees with the file behind it. */
947
+ /**
948
+ * The fixes that carry an unconditional remote-ref placeholder.
949
+ *
950
+ * The plural `images:check-remote` / `files:check-remote` are deliberately
951
+ * absent. `RecordSchema.validateMediaKey` emits those only for a key that is
952
+ * already wrong -- an unparseable remote URL, or one outside the gallery's
953
+ * directory -- and their messages say so. They are findings, not deferrals,
954
+ * and `createFixPatch` has no branch for them at all, so adjudicating one
955
+ * would come back empty and silently drop a real error.
956
+ */
957
+ const REMOTE_CHECK_FIXES = ["image:check-remote", "file:check-remote"];
958
+
959
+ /** One thing about a media value that a real check found to be wrong. */
890
960
 
891
961
  /**
892
- * The verdict on one placeholder. Empty means the metadata agrees with the
893
- * file and nothing should be published.
962
+ * The verdict on one placeholder. Empty means the check passed -- the metadata
963
+ * agrees with the file, or the remote ref is sound -- and nothing should be
964
+ * published.
965
+ */
966
+
967
+ /**
968
+ * Whether this error is the unconditional remote-ref deferral.
969
+ *
970
+ * Unlike the metadata deferral this needs no re-derivation: `image:check-remote`
971
+ * is attached to exactly one error in `ImageSchema.executeValidate`, the
972
+ * fall-through for a remote schema with a remote path, and `FileSchema` mirrors
973
+ * it. Everything else a remote media field can be wrong about carries a
974
+ * different fix (`image:upload-remote`, `image:download-remote`) or none.
894
975
  */
976
+ function isDeferredRemoteCheck(error) {
977
+ return (error.fixes ?? []).some(fix => REMOTE_CHECK_FIXES.includes(fix));
978
+ }
895
979
 
896
980
  /**
897
- * Whether this error is the unconditional deferral rather than a real finding.
981
+ * Whether this error is the unconditional METADATA deferral rather than a real
982
+ * finding.
898
983
  *
899
984
  * This is the whole difficulty of the change. `image:check-metadata` is
900
985
  * attached to FIVE different image errors, and only the last is a placeholder:
@@ -913,7 +998,7 @@ const METADATA_CHECK_FIXES = ["image:check-metadata", "file:check-metadata"];
913
998
  * There is no flag on `ValidationError` saying which is which, so the four
914
999
  * conditions are re-derived here from the same inputs core used. They are
915
1000
  * transcribed from `ImageSchema.executeValidate`, in its order, and
916
- * `mediaMetadataChecks.test.ts` drives real core through all five cases to
1001
+ * `mediaChecks.test.ts` drives real core through all five cases to
917
1002
  * check that this agrees with it. If core grows a sixth condition, that test is
918
1003
  * what catches it.
919
1004
  *
@@ -958,33 +1043,33 @@ function isDeferredMediaMetadataCheck({
958
1043
  }
959
1044
 
960
1045
  /**
961
- * {@link isDeferredMediaMetadataCheck} for an error in a module, resolving the
962
- * schema it needs.
1046
+ * Whether an error in a module is either deferral, resolving the schema the
1047
+ * metadata classification needs.
963
1048
  *
964
1049
  * Both the adjudicator and `createValDiagnostics` have to make the same call,
965
1050
  * on the same inputs -- one to decide what to adjudicate, the other to decide
966
1051
  * what to publish -- so it lives here rather than being written out twice.
967
1052
  */
968
- function isDeferredMediaMetadataCheckAt({
1053
+ function isDeferredMediaCheckAt({
969
1054
  sourcePath,
970
1055
  error,
971
1056
  content
972
1057
  }) {
973
- return isDeferredMediaMetadataCheck({
1058
+ return isDeferredRemoteCheck(error) || isDeferredMediaMetadataCheck({
974
1059
  error,
975
1060
  schema: resolveSchemaAt$1(sourcePath, content)
976
1061
  });
977
1062
  }
978
1063
 
979
1064
  /**
980
- * Adjudicate every deferred metadata placeholder in `validation`.
1065
+ * Adjudicate every deferred media placeholder in `validation`.
981
1066
  *
982
1067
  * Async, and therefore separate from `createValDiagnostics`, which stays
983
1068
  * synchronous so it can be tested without a project -- the same split
984
1069
  * `resolveGalleryChecks` uses. The caller runs this first and passes the
985
1070
  * result in.
986
1071
  */
987
- async function resolveMediaMetadataChecks({
1072
+ async function resolveMediaChecks({
988
1073
  validation,
989
1074
  content,
990
1075
  valRoot,
@@ -993,14 +1078,14 @@ async function resolveMediaMetadataChecks({
993
1078
  const verdicts = new Map();
994
1079
  for (const [sourcePath, errors] of Object.entries(validation)) {
995
1080
  for (const error of errors) {
996
- if (!isDeferredMediaMetadataCheckAt({
1081
+ if (!isDeferredMediaCheckAt({
997
1082
  sourcePath,
998
1083
  error,
999
1084
  content
1000
1085
  })) {
1001
1086
  continue;
1002
1087
  }
1003
- const key = mediaMetadataCheckKey(sourcePath, error);
1088
+ const key = mediaCheckKey(sourcePath, error);
1004
1089
  try {
1005
1090
  verdicts.set(key, await adjudicate({
1006
1091
  sourcePath,
@@ -1012,7 +1097,7 @@ async function resolveMediaMetadataChecks({
1012
1097
  } catch {
1013
1098
  // This runs inside `validate`, which publishes NOTHING if it throws --
1014
1099
  // one bad image would silently clear every Val diagnostic in the file.
1015
- // Keep the placeholder rather than claiming the metadata is fine.
1100
+ // Keep the placeholder rather than claiming the check passed.
1016
1101
  verdicts.set(key, keep(error));
1017
1102
  }
1018
1103
  }
@@ -1024,7 +1109,7 @@ async function resolveMediaMetadataChecks({
1024
1109
  * Key for one placeholder. A value can in principle carry more than one, so
1025
1110
  * the fix names are part of the key -- as they are for the gallery checks.
1026
1111
  */
1027
- function mediaMetadataCheckKey(sourcePath, error) {
1112
+ function mediaCheckKey(sourcePath, error) {
1028
1113
  return `${sourcePath}|${(error.fixes ?? []).join(",")}`;
1029
1114
  }
1030
1115
 
@@ -1044,6 +1129,17 @@ function keep(error) {
1044
1129
  /**
1045
1130
  * What `createFixPatch` says about one placeholder, in report mode.
1046
1131
  *
1132
+ * Which placeholder decides only what has to be true before asking; the asking
1133
+ * is the same call either way, and `reportFixPatch` is it.
1134
+ */
1135
+ async function adjudicate(args) {
1136
+ return isDeferredRemoteCheck(args.error) ? adjudicateRemote(args) : adjudicateMetadata(args);
1137
+ }
1138
+
1139
+ /**
1140
+ * The metadata placeholder: does the stored width/height/mimeType match the
1141
+ * bytes?
1142
+ *
1047
1143
  * `val validate` runs the fix handler first and then `createFixPatch`, but for
1048
1144
  * these four fixes the handler is `handleFileMetadata`, whose whole job is the
1049
1145
  * precondition "the ref resolves and the file is on disk". That precondition is
@@ -1065,7 +1161,7 @@ function keep(error) {
1065
1161
  * `createFixPatch` reads and decodes the image itself, so this is one file read
1066
1162
  * per media field per validation pass, and nothing here should add another.
1067
1163
  */
1068
- async function adjudicate({
1164
+ async function adjudicateMetadata({
1069
1165
  sourcePath,
1070
1166
  error,
1071
1167
  content,
@@ -1086,19 +1182,17 @@ async function adjudicate({
1086
1182
  // means this verdict never has the last word on a file that is not there.
1087
1183
  return keep(error);
1088
1184
  }
1089
- let fixed;
1090
- try {
1091
- fixed = await server.createFixPatch({
1092
- projectRoot: valRoot,
1093
- remoteHost
1094
- },
1095
- // `false`: this is a question, not a fix. Asking for the patch would have
1096
- // createFixPatch read and rewrite files behind the editor's back.
1097
- false, sourcePath, error, {}, content.source, content.schema);
1098
- } catch {
1185
+ const reported = await reportFixPatch({
1186
+ sourcePath,
1187
+ error,
1188
+ content,
1189
+ valRoot,
1190
+ remoteHost
1191
+ });
1192
+ if (reported === undefined) {
1099
1193
  return keep(error);
1100
1194
  }
1101
- return (fixed?.remainingErrors ?? []).map(remaining => ({
1195
+ return reported.map(remaining => ({
1102
1196
  message: remaining.message,
1103
1197
  // `createFixPatch` clears `fixes` on the per-field errors it reports, but
1104
1198
  // the fix is still available and still the remedy -- it is what the
@@ -1114,6 +1208,77 @@ async function adjudicate({
1114
1208
  }));
1115
1209
  }
1116
1210
 
1211
+ /**
1212
+ * The remote-ref placeholder: is the ref this value carries still the ref the
1213
+ * bytes behind it produce?
1214
+ *
1215
+ * `handleRemoteFileCheck` is a no-op that asks for the patch, so unlike the
1216
+ * metadata case there is no precondition of its own to stand in for -- the
1217
+ * whole check is `checkRemoteRef` inside `createFixPatch`. It recomputes the
1218
+ * validation hash from the schema, the file extension, the metadata on the
1219
+ * value and the file hash in the ref; when that agrees with the ref, nothing is
1220
+ * downloaded and nothing is reported. Only a ref that no longer adds up costs a
1221
+ * download, and that is cached under `.val/remote-file-cache`.
1222
+ *
1223
+ * The findings deliberately carry no `fixes`. `createFixPatch` clears them
1224
+ * (`Remote ref: ... is not valid. Use the --fix flag to fix this issue.`
1225
+ * arrives with `fixes: undefined`), no quick fix is registered for
1226
+ * `image:check-remote` -- rewriting the ref needs the bytes off the content
1227
+ * host, so it is `val validate --fix`'s job, not a lightbulb's -- and dropping
1228
+ * them is what makes this an Error rather than a Warning, matching the `✘` the
1229
+ * CLI prints for the same finding.
1230
+ */
1231
+ async function adjudicateRemote({
1232
+ sourcePath,
1233
+ error,
1234
+ content,
1235
+ valRoot,
1236
+ remoteHost
1237
+ }) {
1238
+ const reported = await reportFixPatch({
1239
+ sourcePath,
1240
+ error,
1241
+ content,
1242
+ valRoot,
1243
+ remoteHost
1244
+ });
1245
+ if (reported === undefined) {
1246
+ return keep(error);
1247
+ }
1248
+ return reported.map(remaining => ({
1249
+ message: remaining.message,
1250
+ ...(error.value !== undefined ? {
1251
+ value: error.value
1252
+ } : {})
1253
+ }));
1254
+ }
1255
+
1256
+ /**
1257
+ * `createFixPatch` in report mode, or `undefined` if it could not answer.
1258
+ *
1259
+ * `false` is the load-bearing argument: this is a question, not a fix. Asking
1260
+ * for the patch would have `createFixPatch` read and rewrite files behind the
1261
+ * editor's back -- and, for a remote ref, re-derive one from bytes it had to
1262
+ * download first.
1263
+ */
1264
+ async function reportFixPatch({
1265
+ sourcePath,
1266
+ error,
1267
+ content,
1268
+ valRoot,
1269
+ remoteHost
1270
+ }) {
1271
+ try {
1272
+ const fixed = await server.createFixPatch({
1273
+ projectRoot: valRoot,
1274
+ remoteHost
1275
+ }, false, sourcePath, error, {}, content.source, content.schema);
1276
+ return fixed?.remainingErrors ?? [];
1277
+ } catch {
1278
+ return undefined;
1279
+ }
1280
+ }
1281
+
1117
1282
  /**
1118
1283
  * The `accept` a media schema declares, if any.
1119
1284
  *
@@ -1238,8 +1403,18 @@ const FALLBACK_RANGE = {
1238
1403
  }
1239
1404
  };
1240
1405
 
1241
- /** Fixes that operate on a file or image reference. */
1242
- 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"];
1406
+ /**
1407
+ * Fixes that operate on a LOCAL file or image reference.
1408
+ *
1409
+ * `*:check-remote` is deliberately absent. A remote value's bytes are on the
1410
+ * content host by design, so "is it on disk" is never its question -- and for
1411
+ * a path that is neither under `/public` nor a parseable remote ref, this
1412
+ * check answered "File <root>/... does not exist" and pre-empted the remote
1413
+ * adjudication, which has the real answer ("Invalid remote ref: ..."). The
1414
+ * upload/download fixes stay: `*:upload-remote` is reported on a local path
1415
+ * and a missing file really is its problem.
1416
+ */
1417
+ 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"];
1243
1418
  function build(range, message, data) {
1244
1419
  return {
1245
1420
  range,
@@ -1257,7 +1432,7 @@ function createValDiagnostics({
1257
1432
  valRoot,
1258
1433
  snapshot,
1259
1434
  galleryChecks,
1260
- mediaMetadataChecks
1435
+ mediaChecks
1261
1436
  }) {
1262
1437
  if (content.errors === false) {
1263
1438
  return [];
@@ -1329,18 +1504,20 @@ function createValDiagnostics({
1329
1504
  continue;
1330
1505
  }
1331
1506
 
1332
- // An unconditional media metadata placeholder: core reports
1333
- // `image:check-metadata` on every `s.image()` carrying metadata, whether
1334
- // or not it disagrees with the file. Show only what the adjudication
1335
- // actually found, and nothing when it found nothing. Deliberately after
1336
- // the missing-file branch above, so a deleted file is still reported as
1337
- // `val/file-not-found` rather than as a metadata mismatch.
1338
- if (isDeferredMediaMetadataCheckAt({
1507
+ // An unconditional media placeholder: core reports
1508
+ // `image:check-metadata` on every `s.image()` carrying metadata whether
1509
+ // or not it disagrees with the file, and `Remote image was not checked.`
1510
+ // on every remote image whether or not its ref is stale. Show only what
1511
+ // the adjudication actually found, and nothing when it found nothing.
1512
+ // Deliberately after the missing-file branch above, so a deleted file is
1513
+ // still reported as `val/file-not-found` rather than as a metadata
1514
+ // mismatch.
1515
+ if (isDeferredMediaCheckAt({
1339
1516
  sourcePath,
1340
1517
  error,
1341
1518
  content
1342
1519
  })) {
1343
- const verdict = mediaMetadataChecks?.get(mediaMetadataCheckKey(sourcePath, error));
1520
+ const verdict = mediaChecks?.get(mediaCheckKey(sourcePath, error));
1344
1521
  for (const finding of verdict ?? []) {
1345
1522
  diagnostics.push(build(rangeOf(sourcePath, modulePathMap), finding.message, {
1346
1523
  code: "val/validation",
@@ -4069,9 +4246,11 @@ function createValLanguageServer(connection) {
4069
4246
  })
4070
4247
  }) : undefined;
4071
4248
  // Core also defers "does this image's stored metadata match its file",
4072
- // reporting it on every `s.image()` that carries any metadata. Same
4073
- // treatment: adjudicate with the fix machinery before showing anything.
4074
- const mediaMetadataChecks = result.content.errors !== false && result.content.errors.validation ? await resolveMediaMetadataChecks({
4249
+ // reporting it on every `s.image()` that carries any metadata, and
4250
+ // "is this remote ref still valid", reporting it on every remote image.
4251
+ // Same treatment: adjudicate with the fix machinery before showing
4252
+ // anything.
4253
+ const mediaChecks = result.content.errors !== false && result.content.errors.validation ? await resolveMediaChecks({
4075
4254
  validation: result.content.errors.validation,
4076
4255
  content: result.content,
4077
4256
  valRoot: activeProject.valRoot
@@ -4087,8 +4266,8 @@ function createValLanguageServer(connection) {
4087
4266
  ...(galleryChecks ? {
4088
4267
  galleryChecks
4089
4268
  } : {}),
4090
- ...(mediaMetadataChecks ? {
4091
- mediaMetadataChecks
4269
+ ...(mediaChecks ? {
4270
+ mediaChecks
4092
4271
  } : {})
4093
4272
  });
4094
4273
  const unregistered = findMissingModuleDiagnostic(project.valRoot, moduleFilePath, readOpenDocument);