@valbuild/language-server 0.116.0 → 0.117.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.
@@ -857,6 +857,314 @@ function traverseObjectLiteral(node, sourceFile) {
857
857
  return map;
858
858
  }
859
859
 
860
+ /**
861
+ * Adjudicating the media metadata placeholders core emits unconditionally.
862
+ *
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`.
868
+ *
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`).
874
+ *
875
+ * An editor is in the CLI's position, not the browser's, and publishing the
876
+ * placeholders raw put a permanent warning on every image in the project. So
877
+ * they are adjudicated here, by the same comparison `val validate` makes, and
878
+ * only a real disagreement is shown.
879
+ */
880
+ /**
881
+ * The fixes that carry an unconditional metadata placeholder.
882
+ *
883
+ * `*:add-metadata` is deliberately absent: it is emitted only when every
884
+ * metadata field is missing, which needs no adjudication -- there is nothing
885
+ * stored to compare, and the error stands on its own.
886
+ */
887
+ const METADATA_CHECK_FIXES = ["image:check-metadata", "file:check-metadata"];
888
+
889
+ /** One field of a media value that disagrees with the file behind it. */
890
+
891
+ /**
892
+ * The verdict on one placeholder. Empty means the metadata agrees with the
893
+ * file and nothing should be published.
894
+ */
895
+
896
+ /**
897
+ * Whether this error is the unconditional deferral rather than a real finding.
898
+ *
899
+ * This is the whole difficulty of the change. `image:check-metadata` is
900
+ * attached to FIVE different image errors, and only the last is a placeholder:
901
+ *
902
+ * 1. `Invalid mime type format` -- `accept` set, mimeType has no `/`
903
+ * 2. `Mime type mismatch` -- `accept` set and not satisfied
904
+ * 3. `Could not determine mime type from file extension`
905
+ * 4. `Mime type and file extension not matching`
906
+ * 5. the fall-through deferral
907
+ *
908
+ * The first four are real: they are about the mime type disagreeing with the
909
+ * schema or with the filename, neither of which reading the file can settle.
910
+ * Adjudicating one of them would find the stored metadata matches the bytes and
911
+ * drop a genuine error -- so they have to be told apart before any file is read.
912
+ *
913
+ * There is no flag on `ValidationError` saying which is which, so the four
914
+ * conditions are re-derived here from the same inputs core used. They are
915
+ * transcribed from `ImageSchema.executeValidate`, in its order, and
916
+ * `mediaMetadataChecks.test.ts` drives real core through all five cases to
917
+ * check that this agrees with it. If core grows a sixth condition, that test is
918
+ * what catches it.
919
+ *
920
+ * `FileSchema` needs none of this: its four equivalent errors carry no `fixes`
921
+ * at all, so `file:check-metadata` is unambiguous. The test pins that too.
922
+ */
923
+ function isDeferredMediaMetadataCheck({
924
+ error,
925
+ schema
926
+ }) {
927
+ const fixes = error.fixes ?? [];
928
+ if (!fixes.some(fix => METADATA_CHECK_FIXES.includes(fix))) {
929
+ return false;
930
+ }
931
+ if (fixes.includes("file:check-metadata")) {
932
+ return true;
933
+ }
934
+ const value = mediaValueOf(error.value);
935
+ if (typeof value?.path !== "string") {
936
+ // Nothing to re-derive the conditions from. Keep the error: claiming an
937
+ // image is fine on no evidence is the worse failure.
938
+ return false;
939
+ }
940
+ const accept = acceptOf(schema);
941
+ // Core reads `src.mimeType ?? ""`, and every condition below is guarded on
942
+ // it being non-empty, so an absent mimeType passes all four.
943
+ const mimeType = typeof value.mimeType === "string" ? value.mimeType : "";
944
+ if (accept && mimeType && !mimeType.includes("/")) {
945
+ return false;
946
+ }
947
+ if (accept && mimeType && mimeType.includes("/") && !core.Internal.mimeTypeMatchesAccept(mimeType, accept)) {
948
+ return false;
949
+ }
950
+ const fileMimeType = core.Internal.filenameToMimeType(value.path);
951
+ if (!fileMimeType) {
952
+ return false;
953
+ }
954
+ if (mimeType && fileMimeType !== mimeType) {
955
+ return false;
956
+ }
957
+ return true;
958
+ }
959
+
960
+ /**
961
+ * {@link isDeferredMediaMetadataCheck} for an error in a module, resolving the
962
+ * schema it needs.
963
+ *
964
+ * Both the adjudicator and `createValDiagnostics` have to make the same call,
965
+ * on the same inputs -- one to decide what to adjudicate, the other to decide
966
+ * what to publish -- so it lives here rather than being written out twice.
967
+ */
968
+ function isDeferredMediaMetadataCheckAt({
969
+ sourcePath,
970
+ error,
971
+ content
972
+ }) {
973
+ return isDeferredMediaMetadataCheck({
974
+ error,
975
+ schema: resolveSchemaAt$1(sourcePath, content)
976
+ });
977
+ }
978
+
979
+ /**
980
+ * Adjudicate every deferred metadata placeholder in `validation`.
981
+ *
982
+ * Async, and therefore separate from `createValDiagnostics`, which stays
983
+ * synchronous so it can be tested without a project -- the same split
984
+ * `resolveGalleryChecks` uses. The caller runs this first and passes the
985
+ * result in.
986
+ */
987
+ async function resolveMediaMetadataChecks({
988
+ validation,
989
+ content,
990
+ valRoot,
991
+ remoteHost = process.env.VAL_REMOTE_HOST || core.DEFAULT_VAL_REMOTE_HOST
992
+ }) {
993
+ const verdicts = new Map();
994
+ for (const [sourcePath, errors] of Object.entries(validation)) {
995
+ for (const error of errors) {
996
+ if (!isDeferredMediaMetadataCheckAt({
997
+ sourcePath,
998
+ error,
999
+ content
1000
+ })) {
1001
+ continue;
1002
+ }
1003
+ const key = mediaMetadataCheckKey(sourcePath, error);
1004
+ try {
1005
+ verdicts.set(key, await adjudicate({
1006
+ sourcePath,
1007
+ error,
1008
+ content,
1009
+ valRoot,
1010
+ remoteHost
1011
+ }));
1012
+ } catch {
1013
+ // This runs inside `validate`, which publishes NOTHING if it throws --
1014
+ // one bad image would silently clear every Val diagnostic in the file.
1015
+ // Keep the placeholder rather than claiming the metadata is fine.
1016
+ verdicts.set(key, keep(error));
1017
+ }
1018
+ }
1019
+ }
1020
+ return verdicts;
1021
+ }
1022
+
1023
+ /**
1024
+ * Key for one placeholder. A value can in principle carry more than one, so
1025
+ * the fix names are part of the key -- as they are for the gallery checks.
1026
+ */
1027
+ function mediaMetadataCheckKey(sourcePath, error) {
1028
+ return `${sourcePath}|${(error.fixes ?? []).join(",")}`;
1029
+ }
1030
+
1031
+ /** Keeping the placeholder: what to publish when we learned nothing. */
1032
+ function keep(error) {
1033
+ return [{
1034
+ message: error.message,
1035
+ ...(error.fixes ? {
1036
+ fixes: error.fixes
1037
+ } : {}),
1038
+ ...(error.value !== undefined ? {
1039
+ value: error.value
1040
+ } : {})
1041
+ }];
1042
+ }
1043
+
1044
+ /**
1045
+ * What `createFixPatch` says about one placeholder, in report mode.
1046
+ *
1047
+ * `val validate` runs the fix handler first and then `createFixPatch`, but for
1048
+ * these four fixes the handler is `handleFileMetadata`, whose whole job is the
1049
+ * precondition "the ref resolves and the file is on disk". That precondition is
1050
+ * checked directly here instead, for two reasons:
1051
+ *
1052
+ * - `createValDiagnostics` already reports a missing file as
1053
+ * `val/file-not-found`, which is a better diagnostic than a metadata
1054
+ * mismatch and is produced before this verdict is consulted.
1055
+ * - `handleFileMetadata` resolves the source path through the module, and that
1056
+ * throws outright for a `.jsonValues()` entry ("Cannot resolve path into a
1057
+ * jsonValues entry until its content is loaded") -- so routing through it
1058
+ * would leave every entry-backed image stuck on the placeholder.
1059
+ *
1060
+ * The comparison itself is still `createFixPatch`, unchanged, which is the
1061
+ * parity that matters: the editor's wording is the CLI's wording because it is
1062
+ * the CLI's code -- including for bytes it cannot measure, where it reports
1063
+ * what `val validate` reports rather than a second opinion of our own.
1064
+ *
1065
+ * `createFixPatch` reads and decodes the image itself, so this is one file read
1066
+ * per media field per validation pass, and nothing here should add another.
1067
+ */
1068
+ async function adjudicate({
1069
+ sourcePath,
1070
+ error,
1071
+ content,
1072
+ valRoot,
1073
+ remoteHost
1074
+ }) {
1075
+ const ref = mediaValueOf(error.value)?.path;
1076
+ if (typeof ref !== "string") {
1077
+ return keep(error);
1078
+ }
1079
+ // A remote ref is a URL and is not expected on disk. Core does not emit these
1080
+ // fixes for one, but a stale ref should not be reported as a local mismatch.
1081
+ if (core.Internal.remote.splitRemoteRef(ref).status === "success") {
1082
+ return keep(error);
1083
+ }
1084
+ if (!fs__default["default"].existsSync(path__default["default"].join(valRoot, ref))) {
1085
+ // Reported as `val/file-not-found` instead; keeping the placeholder here
1086
+ // means this verdict never has the last word on a file that is not there.
1087
+ return keep(error);
1088
+ }
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 {
1099
+ return keep(error);
1100
+ }
1101
+ return (fixed?.remainingErrors ?? []).map(remaining => ({
1102
+ message: remaining.message,
1103
+ // `createFixPatch` clears `fixes` on the per-field errors it reports, but
1104
+ // the fix is still available and still the remedy -- it is what the
1105
+ // placeholder was asking for. Carrying the placeholder's own fixes is what
1106
+ // keeps the "update image metadata" quick fix offered, and the diagnostic
1107
+ // a Warning rather than an Error.
1108
+ ...(error.fixes ? {
1109
+ fixes: error.fixes
1110
+ } : {}),
1111
+ ...(error.value !== undefined ? {
1112
+ value: error.value
1113
+ } : {})
1114
+ }));
1115
+ }
1116
+
1117
+ /**
1118
+ * The `accept` a media schema declares, if any.
1119
+ *
1120
+ * Read from the serialized schema (`SerializedImageSchema.options`) rather
1121
+ * than from a schema instance: this runs against what `Service.get` returns.
1122
+ */
1123
+ function acceptOf(schema) {
1124
+ if (typeof schema !== "object" || schema === null || !("options" in schema)) {
1125
+ return undefined;
1126
+ }
1127
+ const {
1128
+ options
1129
+ } = schema;
1130
+ if (typeof options !== "object" || options === null || !("accept" in options)) {
1131
+ return undefined;
1132
+ }
1133
+ const {
1134
+ accept
1135
+ } = options;
1136
+ return typeof accept === "string" ? accept : undefined;
1137
+ }
1138
+
1139
+ /** A media value, read as a plain record. */
1140
+ function mediaValueOf(value) {
1141
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1142
+ return undefined;
1143
+ }
1144
+ return value;
1145
+ }
1146
+
1147
+ /**
1148
+ * The serialized schema at a source path.
1149
+ *
1150
+ * Only the schema is taken from resolution -- `accept` is not on the value, and
1151
+ * the value itself comes from the error. Same call `missingFileRef` in
1152
+ * `diagnostics.ts` makes, and it can fail the same ways (a schema that failed
1153
+ * to serialize, a path that no longer resolves), so failure is not an error
1154
+ * here, just an absence.
1155
+ */
1156
+ function resolveSchemaAt$1(sourcePath, content) {
1157
+ if (!content.source || !content.schema) {
1158
+ return undefined;
1159
+ }
1160
+ try {
1161
+ const [, modulePath] = core.Internal.splitModuleFilePathAndModulePath(sourcePath);
1162
+ return core.Internal.resolvePath(modulePath, content.source, content.schema).schema;
1163
+ } catch {
1164
+ return undefined;
1165
+ }
1166
+ }
1167
+
860
1168
  /** Marks diagnostics as ours, so a client can filter on it. */
861
1169
  const VAL_DIAGNOSTIC_SOURCE = "val";
862
1170
 
@@ -948,7 +1256,8 @@ function createValDiagnostics({
948
1256
  text,
949
1257
  valRoot,
950
1258
  snapshot,
951
- galleryChecks
1259
+ galleryChecks,
1260
+ mediaMetadataChecks
952
1261
  }) {
953
1262
  if (content.errors === false) {
954
1263
  return [];
@@ -1020,6 +1329,33 @@ function createValDiagnostics({
1020
1329
  continue;
1021
1330
  }
1022
1331
 
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({
1339
+ sourcePath,
1340
+ error,
1341
+ content
1342
+ })) {
1343
+ const verdict = mediaMetadataChecks?.get(mediaMetadataCheckKey(sourcePath, error));
1344
+ for (const finding of verdict ?? []) {
1345
+ diagnostics.push(build(rangeOf(sourcePath, modulePathMap), finding.message, {
1346
+ code: "val/validation",
1347
+ sourcePath,
1348
+ ...(finding.fixes ? {
1349
+ fixes: finding.fixes
1350
+ } : {}),
1351
+ ...(finding.value !== undefined ? {
1352
+ value: finding.value
1353
+ } : {})
1354
+ }));
1355
+ }
1356
+ continue;
1357
+ }
1358
+
1023
1359
  // A gallery-backed field whose path the gallery does not track. Core
1024
1360
  // reports it with no fix because the remedy is elsewhere; giving it its
1025
1361
  // own code is what lets the editor offer the two remedies.
@@ -2110,6 +2446,17 @@ async function createValCodeActions({
2110
2446
  remoteHost = process.env.VAL_REMOTE_HOST || core.DEFAULT_VAL_REMOTE_HOST
2111
2447
  }) {
2112
2448
  const actions = [];
2449
+ /**
2450
+ * Fixes already offered, as `<fix target>|<fix>`.
2451
+ *
2452
+ * One problem can be reported as several diagnostics that share a fix: a
2453
+ * stale image reports its width and its height separately, and a gallery
2454
+ * check reports one finding per entry. The fix is the same patch each time --
2455
+ * `createFixPatch` corrects every field, and the gallery branch walks every
2456
+ * entry -- so without this the editor offers the identical "Val: update image
2457
+ * metadata" twice, and recomputes it (re-reading the image) to do so.
2458
+ */
2459
+ const offered = new Set();
2113
2460
  for (const diagnostic of diagnostics) {
2114
2461
  const data = diagnostic.data;
2115
2462
  if (!data?.fixes?.length) {
@@ -2148,6 +2495,10 @@ async function createValCodeActions({
2148
2495
  if (!isLocalFix(fix)) {
2149
2496
  continue;
2150
2497
  }
2498
+ const fixTarget = data.fixSourcePath ?? data.sourcePath;
2499
+ if (offered.has(`${fixTarget}|${fix}`)) {
2500
+ continue;
2501
+ }
2151
2502
  const fixEdit = await computeFixEdit({
2152
2503
  document,
2153
2504
  // A gallery check is reported on the entry but fixed against the record
@@ -2169,6 +2520,7 @@ async function createValCodeActions({
2169
2520
  if (!fixEdit) {
2170
2521
  continue;
2171
2522
  }
2523
+ offered.add(`${fixTarget}|${fix}`);
2172
2524
  actions.push(vscodeLanguageserver.CodeAction.create(FIX_TITLES[fix] ?? `Val: ${fix}`,
2173
2525
  // Not always `document.uri`: a fix inside a `.jsonValues()` entry
2174
2526
  // edits the entry's own `*.val.json`, which is a different file from
@@ -3530,6 +3882,14 @@ function createValLanguageServer(connection) {
3530
3882
  runFixHandler: args => activeProject.runFixHandler(args)
3531
3883
  })
3532
3884
  }) : undefined;
3885
+ // Core also defers "does this image's stored metadata match its file",
3886
+ // reporting it on every `s.image()` that carries any metadata. Same
3887
+ // treatment: adjudicate with the fix machinery before showing anything.
3888
+ const mediaMetadataChecks = result.content.errors !== false && result.content.errors.validation ? await resolveMediaMetadataChecks({
3889
+ validation: result.content.errors.validation,
3890
+ content: result.content,
3891
+ valRoot: activeProject.valRoot
3892
+ }) : undefined;
3533
3893
  const diagnostics = createValDiagnostics({
3534
3894
  moduleFilePath,
3535
3895
  content: result.content,
@@ -3540,6 +3900,9 @@ function createValLanguageServer(connection) {
3540
3900
  } : {}),
3541
3901
  ...(galleryChecks ? {
3542
3902
  galleryChecks
3903
+ } : {}),
3904
+ ...(mediaMetadataChecks ? {
3905
+ mediaMetadataChecks
3543
3906
  } : {})
3544
3907
  });
3545
3908
  const unregistered = findMissingModuleDiagnostic(project.valRoot, moduleFilePath, readOpenDocument);