prostgles-types 4.0.274 → 4.0.277

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.
Files changed (26) hide show
  1. package/dist/JSONBSchemaValidation/JSONBSchema.d.ts +82 -63
  2. package/dist/JSONBSchemaValidation/JSONBSchema.d.ts.map +1 -1
  3. package/dist/JSONBSchemaValidation/JSONBSchema.js +1 -0
  4. package/dist/JSONBSchemaValidation/JSONBSchema.js.map +1 -1
  5. package/dist/JSONBSchemaValidation/JSONBSchemaValidation.d.ts +14 -0
  6. package/dist/JSONBSchemaValidation/JSONBSchemaValidation.d.ts.map +1 -1
  7. package/dist/JSONBSchemaValidation/JSONBSchemaValidation.js +124 -14
  8. package/dist/JSONBSchemaValidation/JSONBSchemaValidation.js.map +1 -1
  9. package/dist/JSONBSchemaValidation/getJSONBSchemaAsJSONSchema.d.ts.map +1 -1
  10. package/dist/JSONBSchemaValidation/getJSONBSchemaAsJSONSchema.js +5 -4
  11. package/dist/JSONBSchemaValidation/getJSONBSchemaAsJSONSchema.js.map +1 -1
  12. package/dist/JSONBSchemaValidation/getJSONBSchemaTSTypes.d.ts.map +1 -1
  13. package/dist/JSONBSchemaValidation/getJSONBSchemaTSTypes.js +21 -40
  14. package/dist/JSONBSchemaValidation/getJSONBSchemaTSTypes.js.map +1 -1
  15. package/dist/index.d.ts +7 -7
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1 -2
  18. package/dist/index.js.map +1 -1
  19. package/dist/index_umd.js +1 -1
  20. package/lib/JSONBSchemaValidation/JSONBSchema.ts +102 -79
  21. package/lib/JSONBSchemaValidation/JSONBSchemaValidation.ts +163 -12
  22. package/lib/JSONBSchemaValidation/getJSONBSchemaAsJSONSchema.ts +4 -4
  23. package/lib/JSONBSchemaValidation/getJSONBSchemaTSTypes.ts +29 -44
  24. package/lib/index.ts +7 -14
  25. package/package.json +3 -2
  26. package/scripts/check-type-instantiations.mjs +40 -0
@@ -1,19 +1,26 @@
1
- import type { JSONB, PrimitiveTypeMap } from "./JSONBSchema";
1
+ import type { DBHandler } from "lib";
2
2
  import { getKeys, isDefined, isEmpty, isObject } from "../util";
3
- import { safeGetKeys, safeGetProperty, safeHasOwn } from "./utils";
4
3
  import { includes } from "../utilFuncs/includes";
4
+ import type { JSONB, PrimitiveTypeMap } from "./JSONBSchema";
5
+ import { safeGetKeys, safeGetProperty, safeHasOwn } from "./utils";
5
6
 
6
7
  type ValidationOptions = {
7
8
  allowExtraProperties?: boolean;
8
9
  };
9
10
 
11
+ type PendingLookupValidation = {
12
+ schema: JSONB.Lookup;
13
+ value: unknown;
14
+ path: string[];
15
+ };
16
+
10
17
  export const getFieldTypeObj = (rawFieldType: JSONB.FieldType): JSONB.FieldTypeObj => {
11
18
  if (typeof rawFieldType === "string") return { type: rawFieldType };
12
19
 
13
20
  return rawFieldType;
14
21
  };
15
22
 
16
- const isBlob = (val: unknown): val is Blob => {
23
+ const isUint8ArrayOrBuffer = (val: unknown): val is Uint8Array => {
17
24
  return (
18
25
  val instanceof Blob ||
19
26
  //@ts-ignore
@@ -23,7 +30,7 @@ const isBlob = (val: unknown): val is Blob => {
23
30
  );
24
31
  };
25
32
 
26
- type DataType = JSONB.FieldTypeObj["type"];
33
+ type DataType = JSONB.BasicType["type"];
27
34
  type ElementType<T extends DataType> = T extends `${infer E}[]` ? E : never;
28
35
  type ArrayTypes = Extract<DataType, `${string}[]`>;
29
36
  type NonArrayTypes = Extract<Exclude<DataType, ArrayTypes>, string>;
@@ -47,10 +54,8 @@ const PRIMITIVE_VALIDATORS: {
47
54
  any: (val): val is any => typeof val !== "function" && typeof val !== "symbol",
48
55
  unknown: (val): val is unknown => typeof val !== "function" && typeof val !== "symbol",
49
56
  Date: (val) => typeof val === "string",
50
- Lookup: (val): val is unknown => {
51
- throw new Error("Lookup type is not supported for validation");
52
- },
53
- Blob: isBlob,
57
+ Uint8Array: isUint8ArrayOrBuffer,
58
+ Blob: isUint8ArrayOrBuffer,
54
59
  FileLike: (val, s): val is PrimitiveTypeMap["FileLike"] => {
55
60
  if (s.type !== "FileLike") {
56
61
  throw new Error("FileLike type must have type 'FileLike'");
@@ -59,7 +64,7 @@ const PRIMITIVE_VALIDATORS: {
59
64
  isObject(val) &&
60
65
  typeof val.name === "string" &&
61
66
  typeof val.type === "string" &&
62
- isBlob(val.data);
67
+ isUint8ArrayOrBuffer(val.data);
63
68
  if (validStructure && s.mimeTypes) {
64
69
  const validMime = Object.keys(s.mimeTypes).some((mime) => val.type === mime);
65
70
  if (!validMime) {
@@ -107,6 +112,7 @@ const getPropertyValidationError = (
107
112
  rawFieldType: JSONB.FieldType,
108
113
  path: string[] = [],
109
114
  opts: ValidationOptions | undefined,
115
+ pendingLookupValidations?: PendingLookupValidation[],
110
116
  ): string | undefined => {
111
117
  const err = `${path.join(".")} is of invalid type. Expecting ${getTypeDescription(rawFieldType).replaceAll("\n", "")}`;
112
118
  const fieldDefinition = getFieldTypeObj(rawFieldType);
@@ -147,6 +153,7 @@ const getPropertyValidationError = (
147
153
  subSchema,
148
154
  [...path, subKey],
149
155
  opts,
156
+ pendingLookupValidations,
150
157
  );
151
158
  if (error !== undefined) {
152
159
  return error;
@@ -166,7 +173,38 @@ const getPropertyValidationError = (
166
173
  return;
167
174
  }
168
175
 
169
- const { validator } = getValidator(type, fieldDefinition);
176
+ if (type.includes("Lookup")) {
177
+ if (!pendingLookupValidations) {
178
+ throw new Error("Lookup types are not supported for validation");
179
+ }
180
+ const isArray = type.endsWith("[]");
181
+ if (isArray !== Array.isArray(value)) return err;
182
+
183
+ const lookupType = type.replace("[]", "");
184
+ const values = isArray ? value : [value];
185
+ const isValid = values.every((item: unknown) => {
186
+ if (lookupType === "RowLookup") return isObject(item);
187
+ if (lookupType === "ValueLookup") {
188
+ return typeof item !== "function" && typeof item !== "symbol";
189
+ }
190
+ if (lookupType === "TableLookup") return typeof item === "string";
191
+ return (
192
+ isObject(item) &&
193
+ typeof safeGetProperty(item, "table") === "string" &&
194
+ typeof safeGetProperty(item, "column") === "string" &&
195
+ safeGetKeys(item).every((key) => key === "table" || key === "column")
196
+ );
197
+ });
198
+ if (!isValid) return err;
199
+
200
+ pendingLookupValidations.push({
201
+ schema: fieldDefinition as JSONB.Lookup,
202
+ value,
203
+ path,
204
+ });
205
+ return;
206
+ }
207
+ const { validator } = getValidator(type as DataType, fieldDefinition);
170
208
  const isValid = validator(value, fieldDefinition);
171
209
  if (!isValid) {
172
210
  return err;
@@ -192,6 +230,7 @@ const getPropertyValidationError = (
192
230
  tuple[index]!,
193
231
  [...path, `${index}`],
194
232
  opts,
233
+ pendingLookupValidations,
195
234
  );
196
235
 
197
236
  if (elementError !== undefined) {
@@ -221,7 +260,13 @@ const getPropertyValidationError = (
221
260
  }
222
261
  const error = value
223
262
  .map((element, i) => {
224
- return getPropertyValidationError(element, arrayOf, [...path, `${i}`], opts);
263
+ return getPropertyValidationError(
264
+ element,
265
+ arrayOf,
266
+ [...path, `${i}`],
267
+ opts,
268
+ pendingLookupValidations,
269
+ );
225
270
  })
226
271
  .filter(isDefined)[0];
227
272
  if (error !== undefined) {
@@ -237,7 +282,11 @@ const getPropertyValidationError = (
237
282
  }
238
283
  let firstError: string | undefined;
239
284
  const validMember = oneOf.find((member) => {
240
- const error = getPropertyValidationError(value, member, path, opts);
285
+ const pendingLookupCount = pendingLookupValidations?.length;
286
+ const error = getPropertyValidationError(value, member, path, opts, pendingLookupValidations);
287
+ if (error !== undefined && pendingLookupCount !== undefined) {
288
+ pendingLookupValidations!.length = pendingLookupCount;
289
+ }
241
290
  firstError ??= error;
242
291
  return error === undefined;
243
292
  });
@@ -271,6 +320,7 @@ const getPropertyValidationError = (
271
320
  valuesSchema,
272
321
  [...path, propKey],
273
322
  opts,
323
+ pendingLookupValidations,
274
324
  );
275
325
  if (valError !== undefined) {
276
326
  return `${valError}`;
@@ -358,6 +408,107 @@ export const getJSONBSchemaValidationError = <S extends JSONB.FieldType>(
358
408
  }
359
409
  return { data: obj as JSONB.GetType<S> };
360
410
  };
411
+
412
+ const getLookupValuePath = (path: string[], index?: number): string => {
413
+ const result = [...path, ...(index === undefined ? [] : [String(index)])].join(".");
414
+ return result || "value";
415
+ };
416
+
417
+ const getLookupTableHandler = (db: DBHandler, table: string): DBHandler[string] | undefined => {
418
+ if (!safeHasOwn(db, table)) return;
419
+ const handler = safeGetProperty(db, table);
420
+ return isObject(handler) ? (handler as DBHandler[string]) : undefined;
421
+ };
422
+
423
+ const getLookupValidationError = async (
424
+ lookup: PendingLookupValidation,
425
+ db: DBHandler,
426
+ ): Promise<string | undefined> => {
427
+ const { schema, path } = lookup;
428
+ const isArray = schema.type.endsWith("[]");
429
+ const values = isArray ? (lookup.value as unknown[]) : [lookup.value];
430
+
431
+ for (const [index, value] of values.entries()) {
432
+ const valuePath = getLookupValuePath(path, isArray ? index : undefined);
433
+
434
+ if (schema.type === "TableLookup" || schema.type === "TableLookup[]") {
435
+ if (!getLookupTableHandler(db, value as string)) {
436
+ return `${valuePath} references an unknown table ${JSON.stringify(value)}`;
437
+ }
438
+ continue;
439
+ }
440
+
441
+ if (schema.type === "ColumnLookup" || schema.type === "ColumnLookup[]") {
442
+ const reference = value as { table: string; column: string };
443
+ if (schema.filter?.table && reference.table !== schema.filter.table) {
444
+ return `${valuePath} references a column that does not match the lookup filter`;
445
+ }
446
+
447
+ const tableHandler = getLookupTableHandler(db, reference.table);
448
+ if (!tableHandler) {
449
+ return `${valuePath} references an unknown table ${JSON.stringify(reference.table)}`;
450
+ }
451
+ if (typeof tableHandler.getColumns !== "function") {
452
+ throw new Error(
453
+ `Cannot validate ColumnLookup: table ${JSON.stringify(reference.table)} does not expose getColumns`,
454
+ );
455
+ }
456
+ const columns = await tableHandler.getColumns();
457
+ const column = columns.find(({ name }) => name === reference.column);
458
+ if (
459
+ !column ||
460
+ (schema.filter?.tsDataType && column.tsDataType !== schema.filter.tsDataType) ||
461
+ (schema.filter?.udt_name && column.udt_name !== schema.filter.udt_name)
462
+ ) {
463
+ return `${valuePath} references an unknown or disallowed column ${JSON.stringify(`${reference.table}.${reference.column}`)}`;
464
+ }
465
+ continue;
466
+ }
467
+
468
+ const dataLookup = schema as JSONB.RowLookup | JSONB.ValueLookup;
469
+ const tableHandler = getLookupTableHandler(db, dataLookup.table);
470
+
471
+ if (!tableHandler) {
472
+ throw new Error(
473
+ `Cannot validate ${dataLookup.type}: table ${JSON.stringify(dataLookup.table)} does not expose findOne`,
474
+ );
475
+ }
476
+
477
+ const valueFilter =
478
+ dataLookup.type === "RowLookup" || dataLookup.type === "RowLookup[]" ?
479
+ (value as Record<string, unknown>)
480
+ : { [(dataLookup as JSONB.ValueLookup).column]: value };
481
+ const filter = dataLookup.filter ? { $and: [dataLookup.filter, valueFilter] } : valueFilter;
482
+ const row = await tableHandler?.findOne?.(filter);
483
+ if (row === undefined) {
484
+ return `${valuePath} does not reference an existing row in ${JSON.stringify(dataLookup.table)}`;
485
+ }
486
+ }
487
+ };
488
+
489
+ /**
490
+ * Validates lookup values as well as the synchronous JSONB schema rules.
491
+ * A prostgles DB handler validates every lookup kind. A fetch-row callback
492
+ * validates RowLookup and ValueLookup records, while table/column lookups are
493
+ * structurally validated.
494
+ */
495
+ export const getJSONBSchemaValidationErrorAsync = async <S extends JSONB.FieldType>(
496
+ schema: S,
497
+ obj: any,
498
+ source: DBHandler,
499
+ opts?: ValidationOptions,
500
+ ): Promise<{ error: string; data?: undefined } | { error?: undefined; data: JSONB.GetType<S> }> => {
501
+ const pendingLookupValidations: PendingLookupValidation[] = [];
502
+ const error = getPropertyValidationError(obj, schema, undefined, opts, pendingLookupValidations);
503
+ if (error) return { error };
504
+
505
+ for (const lookup of pendingLookupValidations) {
506
+ const lookupError = await getLookupValidationError(lookup, source);
507
+ if (lookupError) return { error: lookupError };
508
+ }
509
+ return { data: obj as JSONB.GetType<S> };
510
+ };
511
+
361
512
  export const validateJSONBObjectAgainstSchema = <S extends JSONB.ObjectType["type"]>(
362
513
  schema: S,
363
514
  obj: any,
@@ -1,5 +1,5 @@
1
1
  import type { JSONSchema7, JSONSchema7Definition, JSONSchema7TypeName } from "json-schema";
2
- import type { JSONB, PrimitiveTypes } from "./JSONBSchema";
2
+ import type { JSONB } from "./JSONBSchema";
3
3
  import { getObjectEntries, isObject } from "../util";
4
4
  import { safeGetKeys } from "./utils";
5
5
 
@@ -7,8 +7,7 @@ const getJSONSchemaType = (
7
7
  rawType: JSONB.BasicType["type"] | JSONB.Lookup["type"] | undefined,
8
8
  ): { type: JSONSchema7TypeName | undefined; isArray: boolean } | undefined => {
9
9
  if (!rawType) return;
10
- const type: (typeof PrimitiveTypes)[number] | "Lookup" =
11
- rawType.endsWith("[]") ? (rawType.slice(0, -2) as any) : rawType;
10
+ const type = rawType.endsWith("[]") ? rawType.slice(0, -2) : rawType;
12
11
 
13
12
  return {
14
13
  type:
@@ -17,7 +16,8 @@ const getJSONSchemaType = (
17
16
  : type === "number" ? "number"
18
17
  : type === "any" ? undefined
19
18
  : type === "unknown" ? undefined
20
- : type === "Lookup" ? undefined
19
+ : type.endsWith("Lookup") ? undefined
20
+ : type === "Uint8Array" ? undefined
21
21
  : type === "Blob" ? undefined
22
22
  : type === "FileLike" ? undefined
23
23
  : "string",
@@ -24,6 +24,33 @@ const valueToString = (v: any): string => {
24
24
  return String(v);
25
25
  };
26
26
 
27
+ const getLookupTSType = (tables: TableSchema[], fieldType: JSONB.Lookup): string => {
28
+ const nullType = fieldType.nullable ? "null | " : "";
29
+ const arrayType = fieldType.type.endsWith("[]") ? "[]" : "";
30
+
31
+ if (fieldType.type === "TableLookup" || fieldType.type === "TableLookup[]") {
32
+ return `${nullType}string${arrayType}`;
33
+ }
34
+
35
+ if (fieldType.type === "ColumnLookup" || fieldType.type === "ColumnLookup[]") {
36
+ return `${nullType}{ "table": string; "column": string; }${arrayType}`;
37
+ }
38
+
39
+ const dataLookup = fieldType as JSONB.RowLookup | JSONB.ValueLookup;
40
+ const cols = tables.find((table) => table.name === dataLookup.table)?.columns;
41
+ if (fieldType.type === "ValueLookup" || fieldType.type === "ValueLookup[]") {
42
+ const valueLookup = fieldType as JSONB.ValueLookup;
43
+ const udtName = cols?.find((column) => column.name === valueLookup.column)?.udt_name;
44
+ return `${nullType}${postgresToTsType(udtName ?? "text")}${arrayType}`;
45
+ }
46
+
47
+ const rowType =
48
+ !cols ? "any" : (
49
+ `{ ${cols.map((column) => `${JSON.stringify(column.name)}: ${column.is_nullable ? "null | " : ""} ${postgresToTsType(column.udt_name)}; `).join(" ")} }`
50
+ );
51
+ return `${nullType}${rowType}${arrayType}`;
52
+ };
53
+
27
54
  export const getJSONBTSTypes = (
28
55
  tables: TableSchema[],
29
56
  rawFieldType: JSONB.FieldType,
@@ -33,51 +60,9 @@ export const getJSONBTSTypes = (
33
60
  ): string => {
34
61
  const fieldType = getFieldTypeObj(rawFieldType);
35
62
  const nullType = fieldType.nullable ? `null | ` : "";
36
- if (fieldType.lookup) {
37
- const l = fieldType.lookup;
38
- if (l.type === "data-def") {
39
- return `${fieldType.nullable ? `null |` : ""} ${getJSONBTSTypes(tables, {
40
- type: {
41
- table: "string",
42
- column: "string",
43
- filter: { record: {}, optional: true },
44
- isArray: { type: "boolean", optional: true },
45
- searchColumns: { type: "string[]", optional: true },
46
- isFullRow: {
47
- optional: true,
48
- type: {
49
- displayColumns: { type: "string[]", optional: true },
50
- },
51
- },
52
- showInRowCard: { optional: true, record: {} },
53
- },
54
- })}`;
55
- }
56
-
57
- const isSChema = l.type === "schema";
58
- let type =
59
- isSChema ?
60
- l.object === "table" ?
61
- "string"
62
- : `{ "table": string; "column": string; }`
63
- : "";
64
- if (!isSChema) {
65
- const cols = tables.find((t) => t.name === l.table)?.columns;
66
- if (!l.isFullRow) {
67
- type = postgresToTsType(cols?.find((c) => c.name === l.column)?.udt_name ?? "text");
68
- } else {
69
- type =
70
- !cols ? "any" : (
71
- `{ ${cols.map((c) => `${JSON.stringify(c.name)}: ${c.is_nullable ? "null | " : ""} ${postgresToTsType(c.udt_name)}; `).join(" ")} }`
72
- );
73
- }
74
- }
75
- return `${fieldType.nullable ? `null | ` : ""}${type}${l.isArray ? "[]" : ""}`;
63
+ if (typeof fieldType.type === "string" && fieldType.type.includes("Lookup")) {
64
+ return getLookupTSType(tables, fieldType as JSONB.Lookup);
76
65
  } else if (typeof fieldType.type === "string") {
77
- if (fieldType.type.toLowerCase().includes("lookup")) {
78
- throw new Error(`getJSONBTSTypes: Lookup type not handled correctly`);
79
- }
80
-
81
66
  /** Primitives */
82
67
  const correctType = fieldType.type
83
68
  .replace("integer", "number")
package/lib/index.ts CHANGED
@@ -48,7 +48,7 @@ export type PG_COLUMN_UDT_DATA_TYPE =
48
48
  | (typeof _PG_bytes)[number];
49
49
 
50
50
  const TS_PG_PRIMITIVES = {
51
- "ArrayBuffer | Uint8Array": _PG_bytes,
51
+ Uint8Array: _PG_bytes,
52
52
  string: [
53
53
  ..._PG_strings,
54
54
  ..._PG_numbers_str,
@@ -74,7 +74,6 @@ export const TS_PG_Types = {
74
74
  "string[]": TS_PG_PRIMITIVES.string.map((s) => `_${s}` as const),
75
75
  "any[]": TS_PG_PRIMITIVES.any.map((s) => `_${s}` as const),
76
76
  // "Date[]": _PG_date.map(s => `_${s}` as const),
77
- // "any": [],
78
77
  } as const;
79
78
  export type TS_COLUMN_DATA_TYPES = keyof typeof TS_PG_Types;
80
79
 
@@ -113,7 +112,7 @@ export type DBTableSchema = {
113
112
  update?: boolean;
114
113
  delete?: boolean;
115
114
  /**
116
- * Used in update, insertm select and filters
115
+ * Used in update, insert, select and filters
117
116
  * fields that are nullable or with a default value are be optional
118
117
  */
119
118
  columns: AnyObject;
@@ -138,7 +137,7 @@ export type ColumnInfo = {
138
137
  name: string;
139
138
 
140
139
  /**
141
- * Column display name. Will be first non empty value from i18n data, comment, name
140
+ * Column display name. Will be first non empty value from i18n data, or prettified name
142
141
  */
143
142
  label: string;
144
143
 
@@ -230,7 +229,7 @@ export type ColumnInfo = {
230
229
 
231
230
  /**
232
231
  * JSONB schema (a simplified version of json schema) for the column (if defined in the tableConfig)
233
- * A check constraint will use this schema for runtime data validation and apropriate TS types will be generated
232
+ * A check constraint will use this schema for runtime data validation and appropriate TS types will be generated
234
233
  */
235
234
  jsonbSchema?: JSONB.JSONBSchema;
236
235
 
@@ -741,9 +740,7 @@ export type InsertParams<T extends AnyObject | void = void, S extends DBSchema |
741
740
  * - DoUpdate: will update all non primary key columns of the conflicting row
742
741
  */
743
742
  onConflict?:
744
- | "DoNothing"
745
- | "DoUpdate"
746
- | { action: "DoNothing" | "DoUpdate"; conflictColumns: string[] };
743
+ "DoNothing" | "DoUpdate" | { action: "DoNothing" | "DoUpdate"; conflictColumns: string[] };
747
744
 
748
745
  /**
749
746
  * Used for sync.
@@ -918,7 +915,7 @@ export type InsertDataWithNested<
918
915
  export type TableHandler<
919
916
  TD extends AnyObject = AnyObject,
920
917
  S extends DBSchema | void = void,
921
- TName extends S extends DBSchema ? keyof S : never = never,
918
+ TName extends (S extends DBSchema ? keyof S : never) = never,
922
919
  > = {
923
920
  /**
924
921
  * Retrieves the table/view info
@@ -1210,11 +1207,7 @@ type DeleteMethods<T extends DBTableSchema> =
1210
1207
  T["delete"] extends true ? keyof Pick<TableHandler, "delete"> : never;
1211
1208
 
1212
1209
  export type ValidatedMethods<T extends DBTableSchema> =
1213
- | SelectMethods<T>
1214
- | UpdateMethods<T>
1215
- | InsertMethods<T>
1216
- | UpsertMethods<T>
1217
- | DeleteMethods<T>;
1210
+ SelectMethods<T> | UpdateMethods<T> | InsertMethods<T> | UpsertMethods<T> | DeleteMethods<T>;
1218
1211
  // | SyncMethods<T>
1219
1212
 
1220
1213
  export type DBHandler<S = void> =
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "prostgles-types",
3
- "version": "4.0.274",
3
+ "version": "4.0.277",
4
4
  "description": "",
5
5
  "main": "dist/index_umd.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "directories": {},
8
8
  "scripts": {
9
9
  "start": "tsc --watch",
10
- "test": "npm run build && node --test dist/*.spec.js dist/**/*.spec.js",
10
+ "test": "npm run build && npm run test:type-instantiations && node --test dist/*.spec.js dist/**/*.spec.js",
11
+ "test:type-instantiations": "node scripts/check-type-instantiations.mjs",
11
12
  "build": "npm i && rm -rf ./dist/* && tsc && webpack --config webpack.prod.js",
12
13
  "prebuild": "tsc --noEmit -p tsconfig.browser.json",
13
14
  "perf": "tsc --extendedDiagnostics 2>&1 | grep -E \"Instantiations|Check time\"",
@@ -0,0 +1,40 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ // Baseline: 108,997. The small margin avoids hiding meaningful inference regressions.
5
+ const MAX_INSTANTIATIONS = 105_000;
6
+ const tscPath = fileURLToPath(new URL("../node_modules/typescript/bin/tsc", import.meta.url));
7
+ const result = spawnSync(
8
+ process.execPath,
9
+ [tscPath, "--noEmit", "--extendedDiagnostics", "--pretty", "false"],
10
+ { encoding: "utf8" },
11
+ );
12
+
13
+ if (result.error) {
14
+ throw result.error;
15
+ }
16
+
17
+ if (result.status !== 0) {
18
+ process.stdout.write(result.stdout);
19
+ process.stderr.write(result.stderr);
20
+ process.exit(result.status ?? 1);
21
+ }
22
+
23
+ const output = result.stdout + result.stderr;
24
+ const label = "Instantiations:";
25
+ const line = output.split("\n").find((outputLine) => outputLine.startsWith(label));
26
+ const instantiations = Number(line?.slice(label.length).trim());
27
+
28
+ if (!Number.isFinite(instantiations)) {
29
+ throw new Error(`Could not read type instantiations from tsc output:\n${output}`);
30
+ }
31
+
32
+ if (instantiations > MAX_INSTANTIATIONS) {
33
+ throw new Error(
34
+ `Type instantiations increased to ${instantiations.toLocaleString()} (maximum: ${MAX_INSTANTIATIONS.toLocaleString()}).`,
35
+ );
36
+ }
37
+
38
+ console.log(
39
+ `Type instantiations: ${instantiations.toLocaleString()} / ${MAX_INSTANTIATIONS.toLocaleString()}`,
40
+ );