ts-proto 1.97.0 → 1.99.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.99.0](https://github.com/stephenh/ts-proto/compare/v1.98.0...v1.99.0) (2022-01-07)
2
+
3
+
4
+ ### Features
5
+
6
+ * yarn watch updates (specified) tests when source files change ([#465](https://github.com/stephenh/ts-proto/issues/465)) ([275d0e7](https://github.com/stephenh/ts-proto/commit/275d0e7c61f3acb2b1fd670b1974e64dd49d6ff4))
7
+
8
+ # [1.98.0](https://github.com/stephenh/ts-proto/compare/v1.97.2...v1.98.0) (2022-01-06)
9
+
10
+
11
+ ### Features
12
+
13
+ * watch for changed integration test files ([#464](https://github.com/stephenh/ts-proto/issues/464)) ([988cd7e](https://github.com/stephenh/ts-proto/commit/988cd7eb84bc3b8b72d6b4d59c38aa794c16c638))
14
+
15
+ ## [1.97.2](https://github.com/stephenh/ts-proto/compare/v1.97.1...v1.97.2) (2022-01-06)
16
+
17
+
18
+ ### Performance Improvements
19
+
20
+ * 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))
21
+
22
+ ## [1.97.1](https://github.com/stephenh/ts-proto/compare/v1.97.0...v1.97.1) (2022-01-05)
23
+
24
+
25
+ ### Bug Fixes
26
+
27
+ * 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))
28
+
1
29
  # [1.97.0](https://github.com/stephenh/ts-proto/compare/v1.96.1...v1.97.0) (2021-12-30)
2
30
 
3
31
 
package/README.markdown CHANGED
@@ -13,6 +13,7 @@
13
13
  - [Table of contents](#table-of-contents)
14
14
  - [Overview](#overview)
15
15
  - [QuickStart](#quickstart)
16
+ - [Buf](#buf)
16
17
  - [Goals](#goals)
17
18
  - [Example Types](#example-types)
18
19
  - [Highlights](#highlights)
@@ -84,6 +85,19 @@ This will generate `*.ts` source files for the given `*.proto` types.
84
85
 
85
86
  If you want to package these source files into an npm package to distribute to clients, just run `tsc` on them as usual to generate the `.js`/`.d.ts` files, and deploy the output as a regular npm package.
86
87
 
88
+ ## Buf
89
+
90
+ If you're using Buf, pass `strategy: all` in your `buf.gen.yaml` file ([docs](https://docs.buf.build/configuration/v1/buf-gen-yaml#strategy)).
91
+
92
+ ```yaml
93
+ version: v1
94
+ plugins:
95
+ - name: ts
96
+ out: ../gen/ts
97
+ strategy: all
98
+ path: ../node_modules/ts-proto/protoc-gen-ts_proto
99
+ ```
100
+
87
101
  # Goals
88
102
 
89
103
  - Idiomatic TypeScript/ES6 types
@@ -404,11 +418,13 @@ The commands below assume you have **Docker** installed. To use a **local** copy
404
418
  **Workflow**
405
419
 
406
420
  - Modifying the plugin implementation:
421
+ - The most important logic is found in [src/main.ts](src/main.ts).
407
422
  - Run `yarn bin2ts` or `yarn bin2ts:local`.
408
423
  _Since the proto files were not changed, you only need to regenerate the typescript files._
409
424
  - Run `yarn test` to verify the typescript files are compatible with the reference implementation, and pass other tests.
410
425
  - Updating or adding `.proto` files in the integration directory:
411
- - Run `yarn build:test` to regenerate the integration test files.
426
+ - Run `yarn watch` to automatically regenerate test files when proto files change.
427
+ - Or run `yarn build:test` to regenerate all integration test files.
412
428
  - Run `yarn test` to retest.
413
429
 
414
430
  **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
  }
@@ -1223,7 +1254,7 @@ function generateFromPartial(ctx, fullName, messageDesc) {
1223
1254
  chunks.push(ts_poet_1.code `}`);
1224
1255
  return ts_poet_1.joinCode(chunks, { on: '\n' });
1225
1256
  }
1226
- function generateWrap(fullProtoTypeName) {
1257
+ function generateWrap(ctx, fullProtoTypeName) {
1227
1258
  const chunks = [];
1228
1259
  if (types_1.isStructTypeName(fullProtoTypeName)) {
1229
1260
  chunks.push(ts_poet_1.code `wrap(object: {[key: string]: any} | undefined): Struct {
@@ -1237,7 +1268,29 @@ function generateWrap(fullProtoTypeName) {
1237
1268
  }`);
1238
1269
  }
1239
1270
  if (types_1.isAnyValueTypeName(fullProtoTypeName)) {
1240
- 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 {
1241
1294
  if (value === null) {
1242
1295
  return {nullValue: NullValue.NULL_VALUE} as Value;
1243
1296
  } else if (typeof value === 'boolean') {
@@ -1256,15 +1309,16 @@ function generateWrap(fullProtoTypeName) {
1256
1309
  throw new Error('Unsupported any value type: ' + typeof value);
1257
1310
  }
1258
1311
  }`);
1312
+ }
1259
1313
  }
1260
1314
  if (types_1.isListValueTypeName(fullProtoTypeName)) {
1261
- chunks.push(ts_poet_1.code `wrap(value: Array<any>): ListValue {
1262
- return {values: value};
1315
+ chunks.push(ts_poet_1.code `wrap(value: Array<any> | undefined): ListValue {
1316
+ return {values: value ?? []};
1263
1317
  }`);
1264
1318
  }
1265
1319
  return chunks;
1266
1320
  }
1267
- function generateUnwrap(fullProtoTypeName) {
1321
+ function generateUnwrap(ctx, fullProtoTypeName) {
1268
1322
  const chunks = [];
1269
1323
  if (types_1.isStructTypeName(fullProtoTypeName)) {
1270
1324
  chunks.push(ts_poet_1.code `unwrap(message: Struct): {[key: string]: any} {
@@ -1276,7 +1330,27 @@ function generateUnwrap(fullProtoTypeName) {
1276
1330
  }`);
1277
1331
  }
1278
1332
  if (types_1.isAnyValueTypeName(fullProtoTypeName)) {
1279
- 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 {
1280
1354
  if (message?.stringValue !== undefined) {
1281
1355
  return message.stringValue;
1282
1356
  } else if (message?.numberValue !== undefined) {
@@ -1292,6 +1366,7 @@ function generateUnwrap(fullProtoTypeName) {
1292
1366
  }
1293
1367
  return undefined;
1294
1368
  }`);
1369
+ }
1295
1370
  }
1296
1371
  if (types_1.isListValueTypeName(fullProtoTypeName)) {
1297
1372
  chunks.push(ts_poet_1.code `unwrap(message: ListValue): Array<any> {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-proto",
3
- "version": "1.97.0",
3
+ "version": "1.99.0",
4
4
  "description": "",
5
5
  "main": "build/plugin.js",
6
6
  "repository": "github:stephenh/ts-proto",
@@ -19,9 +19,10 @@
19
19
  "proto2pbjs:local": "integration/pbjs.sh",
20
20
  "bin2ts:local": "integration/codegen.sh",
21
21
  "test": "yarn jest -c jest.config.js --maxWorkers=2",
22
- "prettier": "prettier --write {src,tests}/**/*.ts",
22
+ "prettier": "prettier --write {src,tests}/**/*.ts integration/*.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",