ts-proto 1.96.1 → 1.98.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,31 @@
1
+ # [1.98.0](https://github.com/stephenh/ts-proto/compare/v1.97.2...v1.98.0) (2022-01-06)
2
+
3
+
4
+ ### Features
5
+
6
+ * watch for changed integration test files ([#464](https://github.com/stephenh/ts-proto/issues/464)) ([988cd7e](https://github.com/stephenh/ts-proto/commit/988cd7eb84bc3b8b72d6b4d59c38aa794c16c638))
7
+
8
+ ## [1.97.2](https://github.com/stephenh/ts-proto/compare/v1.97.1...v1.97.2) (2022-01-06)
9
+
10
+
11
+ ### Performance Improvements
12
+
13
+ * fromJSON returns object literal to allow v8 optimizations ([#463](https://github.com/stephenh/ts-proto/issues/463)) ([5fcd05b](https://github.com/stephenh/ts-proto/commit/5fcd05b79e7c02547c4b6db46fae7a7202f97629))
14
+
15
+ ## [1.97.1](https://github.com/stephenh/ts-proto/compare/v1.97.0...v1.97.1) (2022-01-05)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * oneof=union breaks wrapper types [#458](https://github.com/stephenh/ts-proto/issues/458) ([#462](https://github.com/stephenh/ts-proto/issues/462)) ([dd16992](https://github.com/stephenh/ts-proto/commit/dd16992a409f24e88e3a142830cd0745f50dbd10))
21
+
22
+ # [1.97.0](https://github.com/stephenh/ts-proto/compare/v1.96.1...v1.97.0) (2021-12-30)
23
+
24
+
25
+ ### Features
26
+
27
+ * add an option to disable Exact types ([#456](https://github.com/stephenh/ts-proto/issues/456)) ([9c53d7e](https://github.com/stephenh/ts-proto/commit/9c53d7efb0252c7ea0af85a5d161ff94bcd69760))
28
+
1
29
  ## [1.96.1](https://github.com/stephenh/ts-proto/compare/v1.96.0...v1.96.1) (2021-12-28)
2
30
 
3
31
 
package/README.markdown CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  > `ts-proto` transforms your `.proto` files into strongly-typed, idiomatic TypeScript files!
7
7
 
8
- (Note, if you're a new user of ts-proto and using a modern TS setup with `esModuleInterop`, you need to also pass that as a `ts_proto_opt`.)
8
+ (Note, if you're a new user of ts-proto and using a modern TS setup with `esModuleInterop`, or want to use ts-proto in ESM / snowpack / vite, you need to also pass that as a `ts_proto_opt`.)
9
9
 
10
10
  ## Table of contents
11
11
 
@@ -350,6 +350,10 @@ Generated code will be placed in the Gradle build directory.
350
350
 
351
351
  - With `--ts_proto_opt=enumsAsLiterals=true`, the generated enum types will be enum-ish object with `as const`.
352
352
 
353
+ - With `--ts_proto_opt=useExactTypes=false`, the generated `fromPartial` method will not use Exact types.
354
+
355
+ The default behavior is `useExactTypes=true`, which makes `fromPartial` use Exact type for its argument to make TypeScript reject any unknown properties.
356
+
353
357
  ### Only Types
354
358
 
355
359
  If you're looking for `ts-proto` to generate only types for your Protobuf types then passing all three of `outputEncodeMethods`, `outputJsonMethods`, and `outputClientImpl` as `false` is probably what you want, i.e.:
@@ -400,11 +404,13 @@ The commands below assume you have **Docker** installed. To use a **local** copy
400
404
  **Workflow**
401
405
 
402
406
  - Modifying the plugin implementation:
407
+ - The most important logic is found in [src/main.ts](src/main.ts).
403
408
  - Run `yarn bin2ts` or `yarn bin2ts:local`.
404
409
  _Since the proto files were not changed, you only need to regenerate the typescript files._
405
410
  - Run `yarn test` to verify the typescript files are compatible with the reference implementation, and pass other tests.
406
411
  - Updating or adding `.proto` files in the integration directory:
407
- - Run `yarn build:test` to regenerate the integration test files.
412
+ - Run `yarn watch` to automatically regenerate test files when proto files change.
413
+ - Or run `yarn build:test` to regenerate all integration test files.
408
414
  - Run `yarn test` to retest.
409
415
 
410
416
  **Contributing**
package/build/main.js CHANGED
@@ -86,8 +86,8 @@ function generateFile(ctx, fileDesc) {
86
86
  if (options.outputPartialMethods) {
87
87
  staticMembers.push(generateFromPartial(ctx, fullName, message));
88
88
  }
89
- staticMembers.push(...generateWrap(fullTypeName));
90
- staticMembers.push(...generateUnwrap(fullTypeName));
89
+ staticMembers.push(...generateWrap(ctx, fullTypeName));
90
+ staticMembers.push(...generateUnwrap(ctx, fullTypeName));
91
91
  chunks.push(ts_poet_1.code `
92
92
  export const ${ts_poet_1.def(fullName)} = {
93
93
  ${ts_poet_1.joinCode(staticMembers, { on: ',\n\n' })}
@@ -195,6 +195,7 @@ function makeUtils(options) {
195
195
  ...makeDeepPartial(options, longs),
196
196
  ...makeTimestampMethods(options, longs),
197
197
  ...longs,
198
+ ...makeComparisonUtils(),
198
199
  };
199
200
  }
200
201
  exports.makeUtils = makeUtils;
@@ -396,6 +397,17 @@ function makeTimestampMethods(options, longs) {
396
397
  `);
397
398
  return { toTimestamp, fromTimestamp, fromJsonTimestamp };
398
399
  }
400
+ function makeComparisonUtils() {
401
+ const isObject = ts_poet_1.conditionalOutput('isObject', ts_poet_1.code `
402
+ function isObject(value: any): boolean {
403
+ return typeof value === 'object' && value !== null;
404
+ }`);
405
+ const isSet = ts_poet_1.conditionalOutput('isSet', ts_poet_1.code `
406
+ function isSet(value: any): boolean {
407
+ return value !== null && value !== undefined;
408
+ }`);
409
+ return { isObject, isSet };
410
+ }
399
411
  // Create the interface with properties
400
412
  function generateInterfaceDeclaration(ctx, fullName, messageDesc, sourceInfo, fullTypeName) {
401
413
  var _a;
@@ -805,13 +817,17 @@ function generateFromJson(ctx, fullName, messageDesc) {
805
817
  // create the basic function declaration
806
818
  chunks.push(ts_poet_1.code `
807
819
  fromJSON(${messageDesc.field.length > 0 ? 'object' : '_'}: any): ${fullName} {
808
- const message = createBase${fullName}();
820
+ return {
809
821
  `);
822
+ if (ctx.options.outputTypeRegistry) {
823
+ chunks.push(ts_poet_1.code `$type: ${fullName}.$type,`);
824
+ }
825
+ const oneofFieldsCases = messageDesc.oneofDecl.map((oneof, oneofIndex) => messageDesc.field.filter(types_1.isWithinOneOf).filter((field) => field.oneofIndex === oneofIndex));
810
826
  // add a check for each incoming field
811
827
  messageDesc.field.forEach((field) => {
812
828
  const fieldName = case_1.maybeSnakeToCamel(field.name, options);
813
829
  const jsonName = utils_1.determineFieldJsonName(field, options);
814
- // get a generic 'reader.doSomething' bit that is specific to the basic type
830
+ // get code that extracts value from incoming object
815
831
  const readSnippet = (from) => {
816
832
  if (types_1.isEnum(field)) {
817
833
  const fromJson = types_1.getEnumMethod(ctx, field.typeName, 'FromJSON');
@@ -846,6 +862,9 @@ function generateFromJson(ctx, fullName, messageDesc) {
846
862
  else if (types_1.isAnyValueType(field) || types_1.isStructType(field)) {
847
863
  return ts_poet_1.code `${from}`;
848
864
  }
865
+ else if (types_1.isListValueType(field)) {
866
+ return ts_poet_1.code `[...${from}]`;
867
+ }
849
868
  else if (types_1.isValueType(ctx, field)) {
850
869
  const valueType = types_1.valueTypeName(ctx, field.typeName);
851
870
  if (types_1.isLongValueType(field) && options.forceLong === options_1.LongOption.LONG) {
@@ -854,9 +873,6 @@ function generateFromJson(ctx, fullName, messageDesc) {
854
873
  else if (types_1.isBytesValueType(field)) {
855
874
  return ts_poet_1.code `new ${case_1.capitalize(valueType.toCodeString())}(${from})`;
856
875
  }
857
- else if (types_1.isListValueType(field)) {
858
- return ts_poet_1.code `[...${from}]`;
859
- }
860
876
  else {
861
877
  return ts_poet_1.code `${case_1.capitalize(valueType.toCodeString())}(${from})`;
862
878
  }
@@ -919,56 +935,71 @@ function generateFromJson(ctx, fullName, messageDesc) {
919
935
  const fieldType = types_1.toTypeName(ctx, messageDesc, field);
920
936
  const i = maybeCastToNumber(ctx, messageDesc, field, 'key');
921
937
  chunks.push(ts_poet_1.code `
922
- message.${fieldName} = Object.entries(object.${jsonName} ?? {}).reduce<${fieldType}>((acc, [key, value]) => {
923
- acc[${i}] = ${readSnippet('value')};
924
- return acc;
925
- }, {});
926
- `);
927
- }
928
- else if (types_1.isAnyValueType(field)) {
929
- chunks.push(ts_poet_1.code `
930
- message.${fieldName} = Array.isArray(object?.${jsonName}) ? [...object.${jsonName}] : [];
938
+ ${fieldName}: ${ctx.utils.isObject}(object.${jsonName})
939
+ ? Object.entries(object.${jsonName}).reduce<${fieldType}>((acc, [key, value]) => {
940
+ acc[${i}] = ${readSnippet('value')};
941
+ return acc;
942
+ }, {})
943
+ : {},
931
944
  `);
932
945
  }
933
946
  else {
934
- // Explicit `any` type required to make TS with noImplicitAny happy. `object` is also `any` here.
935
- chunks.push(ts_poet_1.code `
936
- message.${fieldName} = (object.${jsonName} ?? []).map((e: any) => ${readSnippet('e')});
937
- `);
947
+ const readValueSnippet = readSnippet('e');
948
+ if (readValueSnippet.toString() === ts_poet_1.code `e`.toString()) {
949
+ chunks.push(ts_poet_1.code `${fieldName}: Array.isArray(object?.${jsonName}) ? [...object.${jsonName}] : [],`);
950
+ }
951
+ else {
952
+ // Explicit `any` type required to make TS with noImplicitAny happy. `object` is also `any` here.
953
+ chunks.push(ts_poet_1.code `
954
+ ${fieldName}: Array.isArray(object?.${jsonName}) ? object.${jsonName}.map((e: any) => ${readValueSnippet}): [],
955
+ `);
956
+ }
938
957
  }
939
958
  }
940
959
  else if (types_1.isWithinOneOfThatShouldBeUnion(options, field)) {
941
- chunks.push(ts_poet_1.code `if (object.${jsonName} !== undefined && object.${jsonName} !== null) {`);
942
- const oneofName = case_1.maybeSnakeToCamel(messageDesc.oneofDecl[field.oneofIndex].name, options);
943
- chunks.push(ts_poet_1.code `
944
- message.${oneofName} = { $case: '${fieldName}', ${fieldName}: ${readSnippet(`object.${jsonName}`)} }
945
- `);
946
- chunks.push(ts_poet_1.code `}`);
960
+ const cases = oneofFieldsCases[field.oneofIndex];
961
+ const firstCase = cases[0];
962
+ const lastCase = cases[cases.length - 1];
963
+ if (field === firstCase) {
964
+ const fieldName = case_1.maybeSnakeToCamel(messageDesc.oneofDecl[field.oneofIndex].name, options);
965
+ chunks.push(ts_poet_1.code `${fieldName}: `);
966
+ }
967
+ const ternaryIf = ts_poet_1.code `${ctx.utils.isSet}(object.${jsonName})`;
968
+ const ternaryThen = ts_poet_1.code `{ $case: '${fieldName}', ${fieldName}: ${readSnippet(`object.${jsonName}`)}`;
969
+ chunks.push(ts_poet_1.code `${ternaryIf} ? ${ternaryThen}} : `);
970
+ if (field === lastCase) {
971
+ chunks.push(ts_poet_1.code `undefined,`);
972
+ }
947
973
  }
948
974
  else if (types_1.isAnyValueType(field)) {
949
- chunks.push(ts_poet_1.code `message.${fieldName} = object.${fieldName};`);
975
+ chunks.push(ts_poet_1.code `${fieldName}: ${ctx.utils.isSet}(object?.${jsonName})
976
+ ? ${readSnippet(`object.${jsonName}`)}
977
+ : undefined,
978
+ `);
950
979
  }
951
980
  else if (types_1.isStructType(field)) {
952
- chunks.push(ts_poet_1.code `message.${fieldName} = typeof(object.${fieldName}) === 'object' ? object.${fieldName} : undefined;`);
981
+ chunks.push(ts_poet_1.code `${fieldName}: ${ctx.utils.isObject}(object.${jsonName})
982
+ ? ${readSnippet(`object.${jsonName}`)}
983
+ : undefined,`);
953
984
  }
954
985
  else if (types_1.isListValueType(field)) {
955
986
  chunks.push(ts_poet_1.code `
956
- message.${fieldName} = Array.isArray(object?.${fieldName})
957
- ? ${readSnippet(`object.${fieldName}`)}
958
- : ${'undefined'};
987
+ ${fieldName}: Array.isArray(object.${jsonName})
988
+ ? ${readSnippet(`object.${jsonName}`)}
989
+ : undefined,
959
990
  `);
960
991
  }
961
992
  else {
962
993
  const fallback = types_1.isWithinOneOf(field) ? 'undefined' : types_1.defaultValue(ctx, field);
963
994
  chunks.push(ts_poet_1.code `
964
- message.${fieldName} = (object.${jsonName} !== undefined && object.${jsonName} !== null)
995
+ ${fieldName}: ${ctx.utils.isSet}(object.${jsonName})
965
996
  ? ${readSnippet(`object.${jsonName}`)}
966
- : ${fallback};
997
+ : ${fallback},
967
998
  `);
968
999
  }
969
1000
  });
970
1001
  // and then wrap up the switch/while/return
971
- chunks.push(ts_poet_1.code `return message`);
1002
+ chunks.push(ts_poet_1.code `};`);
972
1003
  chunks.push(ts_poet_1.code `}`);
973
1004
  return ts_poet_1.joinCode(chunks, { on: '\n' });
974
1005
  }
@@ -1101,13 +1132,19 @@ function generateToJson(ctx, fullName, messageDesc) {
1101
1132
  function generateFromPartial(ctx, fullName, messageDesc) {
1102
1133
  const { options, utils, typeMap } = ctx;
1103
1134
  const chunks = [];
1104
- const Timestamp = utils_1.impProto(options, 'google/protobuf/timestamp', 'Timestamp');
1105
1135
  // create the basic function declaration
1106
1136
  const paramName = messageDesc.field.length > 0 ? 'object' : '_';
1107
- chunks.push(ts_poet_1.code `
1108
- fromPartial<I extends ${utils.Exact}<${utils.DeepPartial}<${fullName}>, I>>(${paramName}: I): ${fullName} {
1109
- const message = createBase${fullName}();
1110
- `);
1137
+ if (ctx.options.useExactTypes) {
1138
+ chunks.push(ts_poet_1.code `
1139
+ fromPartial<I extends ${utils.Exact}<${utils.DeepPartial}<${fullName}>, I>>(${paramName}: I): ${fullName} {
1140
+ `);
1141
+ }
1142
+ else {
1143
+ chunks.push(ts_poet_1.code `
1144
+ fromPartial(${paramName}: ${utils.DeepPartial}<${fullName}>): ${fullName} {
1145
+ `);
1146
+ }
1147
+ chunks.push(ts_poet_1.code `const message = createBase${fullName}();`);
1111
1148
  // add a check for each incoming field
1112
1149
  messageDesc.field.forEach((field) => {
1113
1150
  const fieldName = case_1.maybeSnakeToCamel(field.name, options);
@@ -1217,7 +1254,7 @@ function generateFromPartial(ctx, fullName, messageDesc) {
1217
1254
  chunks.push(ts_poet_1.code `}`);
1218
1255
  return ts_poet_1.joinCode(chunks, { on: '\n' });
1219
1256
  }
1220
- function generateWrap(fullProtoTypeName) {
1257
+ function generateWrap(ctx, fullProtoTypeName) {
1221
1258
  const chunks = [];
1222
1259
  if (types_1.isStructTypeName(fullProtoTypeName)) {
1223
1260
  chunks.push(ts_poet_1.code `wrap(object: {[key: string]: any} | undefined): Struct {
@@ -1231,7 +1268,29 @@ function generateWrap(fullProtoTypeName) {
1231
1268
  }`);
1232
1269
  }
1233
1270
  if (types_1.isAnyValueTypeName(fullProtoTypeName)) {
1234
- chunks.push(ts_poet_1.code `wrap(value: any): Value {
1271
+ if (ctx.options.oneof === options_1.OneofOption.UNIONS) {
1272
+ chunks.push(ts_poet_1.code `wrap(value: any): Value {
1273
+ if (value === null) {
1274
+ return {kind: {$case: 'nullValue', nullValue: NullValue.NULL_VALUE}};
1275
+ } else if (typeof value === 'boolean') {
1276
+ return {kind: {$case: 'boolValue', boolValue: value}};
1277
+ } else if (typeof value === 'number') {
1278
+ return {kind: {$case: 'numberValue', numberValue: value}};
1279
+ } else if (typeof value === 'string') {
1280
+ return {kind: {$case: 'stringValue', stringValue: value}};
1281
+ } else if (Array.isArray(value)) {
1282
+ return {kind: {$case: 'listValue', listValue: value}};
1283
+ } else if (typeof value === 'object') {
1284
+ return {kind: {$case: 'structValue', structValue: value}};
1285
+ } else if (typeof value === 'undefined') {
1286
+ return {} as Value;
1287
+ } else {
1288
+ throw new Error('Unsupported any value type: ' + typeof value);
1289
+ }
1290
+ }`);
1291
+ }
1292
+ else {
1293
+ chunks.push(ts_poet_1.code `wrap(value: any): Value {
1235
1294
  if (value === null) {
1236
1295
  return {nullValue: NullValue.NULL_VALUE} as Value;
1237
1296
  } else if (typeof value === 'boolean') {
@@ -1250,15 +1309,16 @@ function generateWrap(fullProtoTypeName) {
1250
1309
  throw new Error('Unsupported any value type: ' + typeof value);
1251
1310
  }
1252
1311
  }`);
1312
+ }
1253
1313
  }
1254
1314
  if (types_1.isListValueTypeName(fullProtoTypeName)) {
1255
- chunks.push(ts_poet_1.code `wrap(value: Array<any>): ListValue {
1256
- return {values: value};
1315
+ chunks.push(ts_poet_1.code `wrap(value: Array<any> | undefined): ListValue {
1316
+ return {values: value ?? []};
1257
1317
  }`);
1258
1318
  }
1259
1319
  return chunks;
1260
1320
  }
1261
- function generateUnwrap(fullProtoTypeName) {
1321
+ function generateUnwrap(ctx, fullProtoTypeName) {
1262
1322
  const chunks = [];
1263
1323
  if (types_1.isStructTypeName(fullProtoTypeName)) {
1264
1324
  chunks.push(ts_poet_1.code `unwrap(message: Struct): {[key: string]: any} {
@@ -1270,7 +1330,27 @@ function generateUnwrap(fullProtoTypeName) {
1270
1330
  }`);
1271
1331
  }
1272
1332
  if (types_1.isAnyValueTypeName(fullProtoTypeName)) {
1273
- chunks.push(ts_poet_1.code `unwrap(message: Value): string | number | boolean | Object | null | Array<any> | undefined {
1333
+ if (ctx.options.oneof === options_1.OneofOption.UNIONS) {
1334
+ chunks.push(ts_poet_1.code `unwrap(message: Value): string | number | boolean | Object | null | Array<any> | undefined {
1335
+ if (message.kind?.$case === 'nullValue') {
1336
+ return null;
1337
+ } else if (message.kind?.$case === 'numberValue') {
1338
+ return message.kind?.numberValue;
1339
+ } else if (message.kind?.$case === 'stringValue') {
1340
+ return message.kind?.stringValue;
1341
+ } else if (message.kind?.$case === 'boolValue') {
1342
+ return message.kind?.boolValue;
1343
+ } else if (message.kind?.$case === 'structValue') {
1344
+ return message.kind?.structValue;
1345
+ } else if (message.kind?.$case === 'listValue') {
1346
+ return message.kind?.listValue;
1347
+ } else {
1348
+ return undefined;
1349
+ }
1350
+ }`);
1351
+ }
1352
+ else {
1353
+ chunks.push(ts_poet_1.code `unwrap(message: Value): string | number | boolean | Object | null | Array<any> | undefined {
1274
1354
  if (message?.stringValue !== undefined) {
1275
1355
  return message.stringValue;
1276
1356
  } else if (message?.numberValue !== undefined) {
@@ -1286,6 +1366,7 @@ function generateUnwrap(fullProtoTypeName) {
1286
1366
  }
1287
1367
  return undefined;
1288
1368
  }`);
1369
+ }
1289
1370
  }
1290
1371
  if (types_1.isListValueTypeName(fullProtoTypeName)) {
1291
1372
  chunks.push(ts_poet_1.code `unwrap(message: ListValue): Array<any> {
package/build/options.js CHANGED
@@ -61,6 +61,7 @@ function defaultOptions() {
61
61
  outputSchema: false,
62
62
  onlyTypes: false,
63
63
  emitImportedFiles: true,
64
+ useExactTypes: true,
64
65
  };
65
66
  }
66
67
  exports.defaultOptions = defaultOptions;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-proto",
3
- "version": "1.96.1",
3
+ "version": "1.98.0",
4
4
  "description": "",
5
5
  "main": "build/plugin.js",
6
6
  "repository": "github:stephenh/ts-proto",
@@ -21,7 +21,8 @@
21
21
  "test": "yarn jest -c jest.config.js --maxWorkers=2",
22
22
  "prettier": "prettier --write {src,tests}/**/*.ts",
23
23
  "prettier:check": "prettier --list-different {src,tests}/**/*.ts",
24
- "setup:docker": "docker-compose build"
24
+ "setup:docker": "docker-compose build",
25
+ "watch": "ts-node integration/watch.ts"
25
26
  },
26
27
  "files": [
27
28
  "build"
@@ -45,6 +46,7 @@
45
46
  "@semantic-release/release-notes-generator": "^9.0.2",
46
47
  "@types/jest": "^26.0.22",
47
48
  "@types/node": "^14.14.37",
49
+ "chokidar": "^3.5.2",
48
50
  "jest": "^26.6.3",
49
51
  "prettier": "^2.2.1",
50
52
  "reflect-metadata": "^0.1.13",