@fedify/vocab-tools 2.4.0-dev.1666 → 2.4.0-dev.1727

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/README.md CHANGED
@@ -25,3 +25,24 @@ pnpm add @fedify/vocab-tools
25
25
  ~~~~ bash
26
26
  yarn add @fedify/vocab-tools
27
27
  ~~~~
28
+
29
+
30
+ Development
31
+ -----------
32
+
33
+ Run development tasks from the repository root with [mise].
34
+
35
+ [mise]: https://mise.jdx.dev/
36
+
37
+ ### Updating snapshots
38
+
39
+ The code generator has separate output snapshots for Deno, Node.js, and Bun.
40
+ When a change affects generated output, update all three from the repository
41
+ root:
42
+
43
+ ~~~~ bash
44
+ mise run test:update_snapshots
45
+ ~~~~
46
+
47
+ Review and commit every changed snapshot file. Updating only one runtime
48
+ leaves the other test suites with stale expectations.
package/deno.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/vocab-tools",
3
- "version": "2.4.0-dev.1666+690d9b28",
3
+ "version": "2.4.0-dev.1727+1eadcb04",
4
4
  "license": "MIT",
5
5
  "exports": {
6
6
  ".": "./src/mod.ts"
package/dist/mod.cjs CHANGED
@@ -8,7 +8,7 @@ let yaml = require("yaml");
8
8
  let es_toolkit = require("es-toolkit");
9
9
  //#region deno.json
10
10
  var name = "@fedify/vocab-tools";
11
- var version = "2.4.0-dev.1666+690d9b28";
11
+ var version = "2.4.0-dev.1727+1eadcb04";
12
12
  //#endregion
13
13
  //#region src/type.ts
14
14
  const HEURISTICS_CONTEXTS = [
@@ -186,7 +186,8 @@ const scalarTypes = {
186
186
  dataCheck(v) {
187
187
  return `typeof ${v} === "object" && "@language" in ${v} && "@value" in ${v}
188
188
  && typeof ${v}["@language"] === "string"
189
- && typeof ${v}["@value"] === "string"`;
189
+ && typeof ${v}["@value"] === "string"
190
+ && isValidLanguageTag(${v}["@language"])`;
190
191
  },
191
192
  decoder(v) {
192
193
  return `new LanguageString(${v}["@value"], ${v}["@language"])`;
@@ -871,12 +872,17 @@ async function* generateEncoder(typeUri, types) {
871
872
  yield `
872
873
  array = [];
873
874
  for (const v of this.${await getFieldName(property.uri)}) {
874
- const element = (
875
+ let element = (
875
876
  `;
876
877
  if (!areAllScalarTypes(property.range, types)) yield "v instanceof URL ? { \"@id\": formatIri(v) } : ";
877
878
  for (const code of getEncoders(property.range, types, "v", "options")) yield code;
878
879
  yield `
879
880
  );
881
+ if (Array.isArray(element)) {
882
+ if (element.length < 1) continue;
883
+ if (element.length == 1) element = element[0];
884
+ }
885
+ if (typeof element === "undefined") continue;
880
886
  `;
881
887
  if (isNonFunctionalProperty(property) && property.container === "graph") yield `array.push({ "@graph": element });`;
882
888
  else yield `array.push(element);`;
@@ -904,7 +910,10 @@ async function* generateEncoder(typeUri, types) {
904
910
  if (options.format === "expand") {
905
911
  return await jsonld.expand(
906
912
  values,
907
- { documentLoader: options.contextLoader },
913
+ {
914
+ documentLoader: options.contextLoader,
915
+ keepFreeFloatingNodes: true,
916
+ },
908
917
  );
909
918
  }
910
919
  const docContext = options.context ??
@@ -1027,7 +1036,17 @@ async function* generateDecoder(typeUri, types, moduleVarNames) {
1027
1036
  // deno-lint-ignore no-explicit-any
1028
1037
  let values: Record<string, any[]> & { "@id"?: string };
1029
1038
  let expanded: unknown[];
1030
- if (globalThis.Object.keys(json).length == 0) {
1039
+ if (
1040
+ "_fromSubclass" in options &&
1041
+ options._fromSubclass &&
1042
+ typeof json === "object" &&
1043
+ !Array.isArray(json)
1044
+ ) {
1045
+ values =
1046
+ // deno-lint-ignore no-explicit-any
1047
+ json as (Record<string, any[]> & { "@id"?: string });
1048
+ expanded = [values];
1049
+ } else if (globalThis.Object.keys(json).length == 0) {
1031
1050
  values = {};
1032
1051
  expanded = [values];
1033
1052
  } else {
@@ -1084,8 +1103,9 @@ async function* generateDecoder(typeUri, types, moduleVarNames) {
1084
1103
  );
1085
1104
  `;
1086
1105
  else yield `
1087
- delete values["@type"];
1088
- const instance = await super.fromJsonLd(values, {
1106
+ const superValues = structuredClone(values);
1107
+ delete superValues["@type"];
1108
+ const instance = await super.fromJsonLd(superValues, {
1089
1109
  ...options,
1090
1110
  // @ts-ignore: an internal option
1091
1111
  _fromSubclass: true,
@@ -1094,6 +1114,9 @@ async function* generateDecoder(typeUri, types, moduleVarNames) {
1094
1114
  throw new TypeError("Unexpected type: " + instance.constructor.name);
1095
1115
  }
1096
1116
  `;
1117
+ yield `
1118
+ let shouldCacheJsonLd = instance._shouldCacheJsonLd;
1119
+ `;
1097
1120
  for (const property of type.properties) {
1098
1121
  const variable = await getFieldName(property.uri, "");
1099
1122
  yield await generateField(property, types, "const ");
@@ -1106,13 +1129,20 @@ async function* generateDecoder(typeUri, types, moduleVarNames) {
1106
1129
  ${arrayVariable} = values[${JSON.stringify(prop.uri)}];
1107
1130
  }
1108
1131
  `;
1132
+ let itemsExpression = `${arrayVariable}.length === 1 && "@list" in ${arrayVariable}[0]
1133
+ ? ${arrayVariable}[0]["@list"]
1134
+ : ${arrayVariable}`;
1135
+ if (isNonFunctionalProperty(property) && property.container === "graph") itemsExpression = `(${itemsExpression}).flatMap((item: unknown) =>
1136
+ item != null && typeof item === "object" && "@graph" in item &&
1137
+ Array.isArray((item as { "@graph": unknown })["@graph"])
1138
+ ? (item as { "@graph": unknown[] })["@graph"]
1139
+ : [item]
1140
+ )`;
1109
1141
  yield `
1110
1142
  for (
1111
1143
  const v of ${arrayVariable} == null
1112
1144
  ? []
1113
- : ${arrayVariable}.length === 1 && "@list" in ${arrayVariable}[0]
1114
- ? ${arrayVariable}[0]["@list"]
1115
- : ${arrayVariable}
1145
+ : ${itemsExpression}
1116
1146
  ) {
1117
1147
  if (v == null) continue;
1118
1148
  `;
@@ -1126,29 +1156,40 @@ async function* generateDecoder(typeUri, types, moduleVarNames) {
1126
1156
  continue;
1127
1157
  }
1128
1158
  `;
1129
- if (property.range.length == 1) yield `
1130
- const decoded = ${getDecoder(property.range[0], types, "v", "options", propertyBaseUrl)};
1131
- if (typeof decoded === "undefined") continue;
1132
- ${variable}.push(decoded);`;
1133
- else {
1134
- yield `
1159
+ yield `
1135
1160
  const decoded =
1136
- `;
1161
+ `;
1162
+ if (property.range.length == 1) yield getDecoder(property.range[0], types, "v", "options", propertyBaseUrl);
1163
+ else {
1137
1164
  const decoders = getDecoders(property.range, types, "v", "options", propertyBaseUrl);
1138
1165
  for (const code of decoders) yield code;
1139
- yield `
1166
+ }
1167
+ yield `
1140
1168
  ;
1141
- if (typeof decoded === "undefined") continue;
1169
+ `;
1170
+ yield `
1171
+ if (typeof decoded === "undefined") {
1172
+ shouldCacheJsonLd = false;
1173
+ continue;
1174
+ }
1175
+ `;
1176
+ yield `
1177
+ if (!this._shouldCacheDecodedJsonLd(decoded)) {
1178
+ shouldCacheJsonLd = false;
1179
+ }
1142
1180
  ${variable}.push(decoded);
1143
- `;
1144
- }
1181
+ `;
1145
1182
  yield `
1146
1183
  }
1147
1184
  instance.${await getFieldName(property.uri)} = ${variable};
1148
1185
  `;
1149
1186
  }
1150
1187
  yield `
1151
- if (!("_fromSubclass" in options) || !options._fromSubclass) {
1188
+ instance._shouldCacheJsonLd = shouldCacheJsonLd;
1189
+ if (
1190
+ shouldCacheJsonLd &&
1191
+ (!("_fromSubclass" in options) || !options._fromSubclass)
1192
+ ) {
1152
1193
  try {
1153
1194
  if (cacheJsonLd != null && cacheJsonLd !== expanded) {
1154
1195
  const compactArray = Array.isArray(json) && json.length === 1;
@@ -2016,6 +2057,7 @@ async function* generateClass(typeUri, types, moduleVarNames) {
2016
2057
  };
2017
2058
  #cachedJsonLd?: unknown;
2018
2059
  readonly #_baseUrl?: URL;
2060
+ #shouldCacheJsonLd = true;
2019
2061
  readonly id: URL | null;
2020
2062
 
2021
2063
  protected get _documentLoader(): DocumentLoader | undefined {
@@ -2049,6 +2091,20 @@ async function* generateClass(typeUri, types, moduleVarNames) {
2049
2091
  protected get _baseUrl(): URL | undefined {
2050
2092
  return this.#_baseUrl;
2051
2093
  }
2094
+
2095
+ protected get _shouldCacheJsonLd(): boolean {
2096
+ return this.#shouldCacheJsonLd;
2097
+ }
2098
+
2099
+ protected set _shouldCacheJsonLd(value: boolean) {
2100
+ this.#shouldCacheJsonLd = value;
2101
+ }
2102
+
2103
+ protected static _shouldCacheDecodedJsonLd(value: unknown): boolean {
2104
+ if (value == null || typeof value !== "object") return true;
2105
+ if (!("_shouldCacheJsonLd" in value)) return true;
2106
+ return (value as { _shouldCacheJsonLd: boolean })._shouldCacheJsonLd;
2107
+ }
2052
2108
  `;
2053
2109
  yield `
2054
2110
  /**
@@ -2139,7 +2195,19 @@ async function* generateClasses(types) {
2139
2195
  isTemporalDuration,
2140
2196
  isTemporalInstant,
2141
2197
  } from "@fedify/vocab-runtime/temporal";\n`;
2142
- const portableIriKeys = new Set(["@id", "id"]);
2198
+ yield `
2199
+ function isValidLanguageTag(language: string): boolean {
2200
+ try {
2201
+ new Intl.Locale(language);
2202
+ return true;
2203
+ } catch (error) {
2204
+ if (error instanceof RangeError) return false;
2205
+ throw error;
2206
+ }
2207
+ }
2208
+ `;
2209
+ yield "\n\n";
2210
+ const portableIriKeys = /* @__PURE__ */ new Set(["@id", "id"]);
2143
2211
  for (const type of Object.values(types)) for (const property of type.properties) addPortableIriKeys(portableIriKeys, property, types);
2144
2212
  yield "const PORTABLE_IRI_PATTERN = /^ap(?:\\+ef61)?:\\/\\//i;\n";
2145
2213
  yield `const PORTABLE_IRI_KEYS: ReadonlySet<string> = new Set(${JSON.stringify([...portableIriKeys].sort())});\n\n`;
package/dist/mod.js CHANGED
@@ -7,7 +7,7 @@ import { parse } from "yaml";
7
7
  import { pascalCase } from "es-toolkit";
8
8
  //#region deno.json
9
9
  var name = "@fedify/vocab-tools";
10
- var version = "2.4.0-dev.1666+690d9b28";
10
+ var version = "2.4.0-dev.1727+1eadcb04";
11
11
  //#endregion
12
12
  //#region src/type.ts
13
13
  const HEURISTICS_CONTEXTS = [
@@ -185,7 +185,8 @@ const scalarTypes = {
185
185
  dataCheck(v) {
186
186
  return `typeof ${v} === "object" && "@language" in ${v} && "@value" in ${v}
187
187
  && typeof ${v}["@language"] === "string"
188
- && typeof ${v}["@value"] === "string"`;
188
+ && typeof ${v}["@value"] === "string"
189
+ && isValidLanguageTag(${v}["@language"])`;
189
190
  },
190
191
  decoder(v) {
191
192
  return `new LanguageString(${v}["@value"], ${v}["@language"])`;
@@ -870,12 +871,17 @@ async function* generateEncoder(typeUri, types) {
870
871
  yield `
871
872
  array = [];
872
873
  for (const v of this.${await getFieldName(property.uri)}) {
873
- const element = (
874
+ let element = (
874
875
  `;
875
876
  if (!areAllScalarTypes(property.range, types)) yield "v instanceof URL ? { \"@id\": formatIri(v) } : ";
876
877
  for (const code of getEncoders(property.range, types, "v", "options")) yield code;
877
878
  yield `
878
879
  );
880
+ if (Array.isArray(element)) {
881
+ if (element.length < 1) continue;
882
+ if (element.length == 1) element = element[0];
883
+ }
884
+ if (typeof element === "undefined") continue;
879
885
  `;
880
886
  if (isNonFunctionalProperty(property) && property.container === "graph") yield `array.push({ "@graph": element });`;
881
887
  else yield `array.push(element);`;
@@ -903,7 +909,10 @@ async function* generateEncoder(typeUri, types) {
903
909
  if (options.format === "expand") {
904
910
  return await jsonld.expand(
905
911
  values,
906
- { documentLoader: options.contextLoader },
912
+ {
913
+ documentLoader: options.contextLoader,
914
+ keepFreeFloatingNodes: true,
915
+ },
907
916
  );
908
917
  }
909
918
  const docContext = options.context ??
@@ -1026,7 +1035,17 @@ async function* generateDecoder(typeUri, types, moduleVarNames) {
1026
1035
  // deno-lint-ignore no-explicit-any
1027
1036
  let values: Record<string, any[]> & { "@id"?: string };
1028
1037
  let expanded: unknown[];
1029
- if (globalThis.Object.keys(json).length == 0) {
1038
+ if (
1039
+ "_fromSubclass" in options &&
1040
+ options._fromSubclass &&
1041
+ typeof json === "object" &&
1042
+ !Array.isArray(json)
1043
+ ) {
1044
+ values =
1045
+ // deno-lint-ignore no-explicit-any
1046
+ json as (Record<string, any[]> & { "@id"?: string });
1047
+ expanded = [values];
1048
+ } else if (globalThis.Object.keys(json).length == 0) {
1030
1049
  values = {};
1031
1050
  expanded = [values];
1032
1051
  } else {
@@ -1083,8 +1102,9 @@ async function* generateDecoder(typeUri, types, moduleVarNames) {
1083
1102
  );
1084
1103
  `;
1085
1104
  else yield `
1086
- delete values["@type"];
1087
- const instance = await super.fromJsonLd(values, {
1105
+ const superValues = structuredClone(values);
1106
+ delete superValues["@type"];
1107
+ const instance = await super.fromJsonLd(superValues, {
1088
1108
  ...options,
1089
1109
  // @ts-ignore: an internal option
1090
1110
  _fromSubclass: true,
@@ -1093,6 +1113,9 @@ async function* generateDecoder(typeUri, types, moduleVarNames) {
1093
1113
  throw new TypeError("Unexpected type: " + instance.constructor.name);
1094
1114
  }
1095
1115
  `;
1116
+ yield `
1117
+ let shouldCacheJsonLd = instance._shouldCacheJsonLd;
1118
+ `;
1096
1119
  for (const property of type.properties) {
1097
1120
  const variable = await getFieldName(property.uri, "");
1098
1121
  yield await generateField(property, types, "const ");
@@ -1105,13 +1128,20 @@ async function* generateDecoder(typeUri, types, moduleVarNames) {
1105
1128
  ${arrayVariable} = values[${JSON.stringify(prop.uri)}];
1106
1129
  }
1107
1130
  `;
1131
+ let itemsExpression = `${arrayVariable}.length === 1 && "@list" in ${arrayVariable}[0]
1132
+ ? ${arrayVariable}[0]["@list"]
1133
+ : ${arrayVariable}`;
1134
+ if (isNonFunctionalProperty(property) && property.container === "graph") itemsExpression = `(${itemsExpression}).flatMap((item: unknown) =>
1135
+ item != null && typeof item === "object" && "@graph" in item &&
1136
+ Array.isArray((item as { "@graph": unknown })["@graph"])
1137
+ ? (item as { "@graph": unknown[] })["@graph"]
1138
+ : [item]
1139
+ )`;
1108
1140
  yield `
1109
1141
  for (
1110
1142
  const v of ${arrayVariable} == null
1111
1143
  ? []
1112
- : ${arrayVariable}.length === 1 && "@list" in ${arrayVariable}[0]
1113
- ? ${arrayVariable}[0]["@list"]
1114
- : ${arrayVariable}
1144
+ : ${itemsExpression}
1115
1145
  ) {
1116
1146
  if (v == null) continue;
1117
1147
  `;
@@ -1125,29 +1155,40 @@ async function* generateDecoder(typeUri, types, moduleVarNames) {
1125
1155
  continue;
1126
1156
  }
1127
1157
  `;
1128
- if (property.range.length == 1) yield `
1129
- const decoded = ${getDecoder(property.range[0], types, "v", "options", propertyBaseUrl)};
1130
- if (typeof decoded === "undefined") continue;
1131
- ${variable}.push(decoded);`;
1132
- else {
1133
- yield `
1158
+ yield `
1134
1159
  const decoded =
1135
- `;
1160
+ `;
1161
+ if (property.range.length == 1) yield getDecoder(property.range[0], types, "v", "options", propertyBaseUrl);
1162
+ else {
1136
1163
  const decoders = getDecoders(property.range, types, "v", "options", propertyBaseUrl);
1137
1164
  for (const code of decoders) yield code;
1138
- yield `
1165
+ }
1166
+ yield `
1139
1167
  ;
1140
- if (typeof decoded === "undefined") continue;
1168
+ `;
1169
+ yield `
1170
+ if (typeof decoded === "undefined") {
1171
+ shouldCacheJsonLd = false;
1172
+ continue;
1173
+ }
1174
+ `;
1175
+ yield `
1176
+ if (!this._shouldCacheDecodedJsonLd(decoded)) {
1177
+ shouldCacheJsonLd = false;
1178
+ }
1141
1179
  ${variable}.push(decoded);
1142
- `;
1143
- }
1180
+ `;
1144
1181
  yield `
1145
1182
  }
1146
1183
  instance.${await getFieldName(property.uri)} = ${variable};
1147
1184
  `;
1148
1185
  }
1149
1186
  yield `
1150
- if (!("_fromSubclass" in options) || !options._fromSubclass) {
1187
+ instance._shouldCacheJsonLd = shouldCacheJsonLd;
1188
+ if (
1189
+ shouldCacheJsonLd &&
1190
+ (!("_fromSubclass" in options) || !options._fromSubclass)
1191
+ ) {
1151
1192
  try {
1152
1193
  if (cacheJsonLd != null && cacheJsonLd !== expanded) {
1153
1194
  const compactArray = Array.isArray(json) && json.length === 1;
@@ -2015,6 +2056,7 @@ async function* generateClass(typeUri, types, moduleVarNames) {
2015
2056
  };
2016
2057
  #cachedJsonLd?: unknown;
2017
2058
  readonly #_baseUrl?: URL;
2059
+ #shouldCacheJsonLd = true;
2018
2060
  readonly id: URL | null;
2019
2061
 
2020
2062
  protected get _documentLoader(): DocumentLoader | undefined {
@@ -2048,6 +2090,20 @@ async function* generateClass(typeUri, types, moduleVarNames) {
2048
2090
  protected get _baseUrl(): URL | undefined {
2049
2091
  return this.#_baseUrl;
2050
2092
  }
2093
+
2094
+ protected get _shouldCacheJsonLd(): boolean {
2095
+ return this.#shouldCacheJsonLd;
2096
+ }
2097
+
2098
+ protected set _shouldCacheJsonLd(value: boolean) {
2099
+ this.#shouldCacheJsonLd = value;
2100
+ }
2101
+
2102
+ protected static _shouldCacheDecodedJsonLd(value: unknown): boolean {
2103
+ if (value == null || typeof value !== "object") return true;
2104
+ if (!("_shouldCacheJsonLd" in value)) return true;
2105
+ return (value as { _shouldCacheJsonLd: boolean })._shouldCacheJsonLd;
2106
+ }
2051
2107
  `;
2052
2108
  yield `
2053
2109
  /**
@@ -2138,7 +2194,19 @@ async function* generateClasses(types) {
2138
2194
  isTemporalDuration,
2139
2195
  isTemporalInstant,
2140
2196
  } from "@fedify/vocab-runtime/temporal";\n`;
2141
- const portableIriKeys = new Set(["@id", "id"]);
2197
+ yield `
2198
+ function isValidLanguageTag(language: string): boolean {
2199
+ try {
2200
+ new Intl.Locale(language);
2201
+ return true;
2202
+ } catch (error) {
2203
+ if (error instanceof RangeError) return false;
2204
+ throw error;
2205
+ }
2206
+ }
2207
+ `;
2208
+ yield "\n\n";
2209
+ const portableIriKeys = /* @__PURE__ */ new Set(["@id", "id"]);
2142
2210
  for (const type of Object.values(types)) for (const property of type.properties) addPortableIriKeys(portableIriKeys, property, types);
2143
2211
  yield "const PORTABLE_IRI_PATTERN = /^ap(?:\\+ef61)?:\\/\\//i;\n";
2144
2212
  yield `const PORTABLE_IRI_KEYS: ReadonlySet<string> = new Set(${JSON.stringify([...portableIriKeys].sort())});\n\n`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fedify/vocab-tools",
3
- "version": "2.4.0-dev.1666+690d9b28",
3
+ "version": "2.4.0-dev.1727+1eadcb04",
4
4
  "description": "Code generator for Activity Vocabulary APIs",
5
5
  "homepage": "https://fedify.dev/",
6
6
  "repository": {