ts2workflows 0.13.0 → 0.15.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.
@@ -3,7 +3,7 @@ import * as path from 'node:path';
3
3
  import { AST_NODE_TYPES, parseAndGenerateServices, } from '@typescript-eslint/typescript-estree';
4
4
  import ts from 'typescript';
5
5
  import * as YAML from 'yaml';
6
- import { InternalTranspilingError, IOError, WorkflowSyntaxError, } from '../errors.js';
6
+ import { InternalTranspilingError, IOError, syntaxErrorWithText, WorkflowSyntaxError, } from '../errors.js';
7
7
  import { SubworkflowStatements, WorkflowApp, } from '../ast/workflows.js';
8
8
  import { parseStatement } from './parsestatement.js';
9
9
  import { transformAST } from './transformations.js';
@@ -12,11 +12,14 @@ import { isPrimitive, nullEx } from '../ast/expressions.js';
12
12
  import { convertExpression } from './parseexpressions.js';
13
13
  import { generateStepNames } from './stepnames.js';
14
14
  const workflowCache = new Map();
15
- export function transpile(input, tsconfigPath, linkSubworkflows) {
16
- const { ast, services } = parseMainFile(input, tsconfigPath);
17
- const inputWorkflow = generateStepNames(ast.body.flatMap(parseTopLevelStatement));
18
- if (linkSubworkflows && tsconfigPath && services.program && input.filename) {
19
- const canonicalInput = path.join(process.cwd(), input.filename);
15
+ export function transpile(filename, sourceCode, tsconfigPath, linkSubworkflows) {
16
+ const { ast, services } = parseMainFile(filename, sourceCode, tsconfigPath);
17
+ const inputWorkflow = esProgramToWorkflowAppEnrichErrors(ast, filename, sourceCode);
18
+ if (linkSubworkflows &&
19
+ tsconfigPath &&
20
+ services.program !== null &&
21
+ filename) {
22
+ const canonicalInput = path.join(process.cwd(), filename);
20
23
  workflowCache.set(canonicalInput, inputWorkflow);
21
24
  const subworkflows = generateLinkedOutput(canonicalInput, tsconfigPath, services.program);
22
25
  const combinedWorkflow = new WorkflowApp(subworkflows);
@@ -29,24 +32,40 @@ export function transpile(input, tsconfigPath, linkSubworkflows) {
29
32
  export function transpileText(sourceCode) {
30
33
  const parserOptions = eslintParserOptions();
31
34
  const { ast } = parseAndGenerateServices(sourceCode, parserOptions);
32
- const workflow = generateStepNames(ast.body.flatMap(parseTopLevelStatement));
35
+ const workflow = esProgramToWorkflowAppEnrichErrors(ast, '<stdin>', sourceCode);
33
36
  return toYAMLString(workflow);
34
37
  }
35
- function parseMainFile(input, tsconfigPath) {
36
- const parserOptions = eslintParserOptions(input.filename, tsconfigPath);
37
- if (tsconfigPath && input.filename) {
38
+ function esProgramToWorkflowAppEnrichErrors(program, filename, sourceCode) {
39
+ try {
40
+ return esProgramToWorkflowApp(program);
41
+ }
42
+ catch (error) {
43
+ if (error instanceof WorkflowSyntaxError) {
44
+ throw syntaxErrorWithText(error, filename, sourceCode);
45
+ }
46
+ else {
47
+ throw error;
48
+ }
49
+ }
50
+ }
51
+ function esProgramToWorkflowApp(program) {
52
+ return generateStepNames(program.body.flatMap(parseTopLevelStatement));
53
+ }
54
+ function parseMainFile(filename, sourceCode, tsconfigPath) {
55
+ const parserOptions = eslintParserOptions(filename, tsconfigPath);
56
+ if (tsconfigPath) {
38
57
  const cwd = process.cwd();
39
58
  const configJSON = JSON.parse(fs.readFileSync(path.join(cwd, tsconfigPath), 'utf-8'));
40
59
  const { options } = ts.parseJsonConfigFileContent(configJSON, ts.sys, cwd);
41
- const program = ts.createProgram([input.filename], options);
42
- const mainSourceFile = program.getSourceFile(input.filename);
60
+ const program = ts.createProgram([filename], options);
61
+ const mainSourceFile = program.getSourceFile(filename);
43
62
  if (mainSourceFile === undefined) {
44
- throw new IOError(`Source file ${input.filename} not found`, 'ENOENT');
63
+ throw new IOError(`Source file ${filename} not found`, 'ENOENT');
45
64
  }
46
65
  return parseAndGenerateServices(mainSourceFile, parserOptions);
47
66
  }
48
67
  else {
49
- return parseAndGenerateServices(input.read(), parserOptions);
68
+ return parseAndGenerateServices(sourceCode, parserOptions);
50
69
  }
51
70
  }
52
71
  function eslintParserOptions(inputFile, tsconfigPath) {
@@ -110,7 +129,7 @@ function getCachedWorkflow(filename, tsconfigPath) {
110
129
  const parserOptions = eslintParserOptions(filename, tsconfigPath);
111
130
  const code = fs.readFileSync(filename, 'utf8');
112
131
  const { ast } = parseAndGenerateServices(code, parserOptions);
113
- const workflow = generateStepNames(ast.body.flatMap(parseTopLevelStatement));
132
+ const workflow = esProgramToWorkflowAppEnrichErrors(ast, filename, code);
114
133
  workflowCache.set(filename, workflow);
115
134
  return workflow;
116
135
  }
@@ -1 +1 @@
1
- {"version":3,"file":"parseexpressions.d.ts","sourceRoot":"","sources":["../../src/transpiler/parseexpressions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAkB,MAAM,sCAAsC,CAAA;AAC/E,OAAO,EAGL,UAAU,EAGV,aAAa,EACb,gBAAgB,EAChB,2BAA2B,EAc5B,MAAM,uBAAuB,CAAA;AAG9B,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,UAAU,CAmE3E;AAED,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,QAAQ,CAAC,gBAAgB,GAC9B,aAAa,CA8Bf;AAmJD,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,QAAQ,CAAC,gBAAgB,GAC9B,gBAAgB,CAOlB;AA8JD,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAGvD;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAGhE;AA8CD,wBAAgB,aAAa,CAC3B,CAAC,SACG,QAAQ,CAAC,UAAU,GACnB,QAAQ,CAAC,QAAQ,GACjB,QAAQ,CAAC,aAAa,GACtB,IAAI,EACR,KAAK,EAAE,CAAC,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,QAAQ,CAAC,aAAa,CAAC,EAAE,CAgBlD;AAED,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,QAAQ,CAAC,UAAU,GAAG,QAAQ,CAAC,iBAAiB,GACrD,QAAQ,CAAC,UAAU,CAMrB;AAED,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,QAAQ,CAAC,UAAU,GACxB,2BAA2B,GAAG,gBAAgB,CAWhD"}
1
+ {"version":3,"file":"parseexpressions.d.ts","sourceRoot":"","sources":["../../src/transpiler/parseexpressions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAkB,MAAM,sCAAsC,CAAA;AAC/E,OAAO,EAGL,UAAU,EAGV,aAAa,EACb,gBAAgB,EAChB,2BAA2B,EAc5B,MAAM,uBAAuB,CAAA;AAG9B,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,UAAU,CAmE3E;AAED,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,QAAQ,CAAC,gBAAgB,GAC9B,aAAa,CAmCf;AAmJD,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,QAAQ,CAAC,gBAAgB,GAC9B,gBAAgB,CAOlB;AA2KD,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAGvD;AAED,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAGhE;AA8CD,wBAAgB,aAAa,CAC3B,CAAC,SACG,QAAQ,CAAC,UAAU,GACnB,QAAQ,CAAC,QAAQ,GACjB,QAAQ,CAAC,aAAa,GACtB,IAAI,EACR,KAAK,EAAE,CAAC,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,QAAQ,CAAC,aAAa,CAAC,EAAE,CAgBlD;AAED,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,QAAQ,CAAC,UAAU,GAAG,QAAQ,CAAC,iBAAiB,GACrD,QAAQ,CAAC,UAAU,CAMrB;AAED,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,QAAQ,CAAC,UAAU,GACxB,2BAA2B,GAAG,gBAAgB,CAWhD"}
@@ -69,6 +69,10 @@ export function convertObjectExpression(node) {
69
69
  typeof key.value === 'string') {
70
70
  keyPrimitive = key.value;
71
71
  }
72
+ else if (key.type === AST_NODE_TYPES.Literal &&
73
+ typeof key.value === 'number') {
74
+ keyPrimitive = key.value.toString();
75
+ }
72
76
  else {
73
77
  throw new WorkflowSyntaxError(`Map keys must be identifiers or strings, encountered: ${key.type}`, key.loc);
74
78
  }
@@ -286,13 +290,22 @@ function convertCallExpression(node) {
286
290
  }
287
291
  throw new WorkflowSyntaxError(msg, node.callee.loc);
288
292
  }
289
- const argumentExpressions = throwIfSpread(node.arguments).map(convertExpression);
293
+ const argumentExpressions = throwIfSpread(node.arguments).map(convertExpressionOrUndefined);
290
294
  return functionInvocationEx(calleeName, argumentExpressions);
291
295
  }
292
296
  else {
293
297
  throw new WorkflowSyntaxError('Callee should be a qualified name', node.loc);
294
298
  }
295
299
  }
300
+ function convertExpressionOrUndefined(instance) {
301
+ if (instance.type === TSESTree.AST_NODE_TYPES.Identifier &&
302
+ instance.name === 'undefined') {
303
+ return undefined;
304
+ }
305
+ else {
306
+ return convertExpression(instance);
307
+ }
308
+ }
296
309
  export function isIntrinsic(calleeName) {
297
310
  const intrinsics = ['parallel', 'retry_policy', 'call_step'];
298
311
  return intrinsics.includes(calleeName);
@@ -310,7 +323,7 @@ function convertConditionalExpression(node) {
310
323
  function convertTemplateLiteralToExpression(node) {
311
324
  const stringTerms = node.quasis
312
325
  .map((x) => x.value.cooked)
313
- .map((x) => stringEx(x));
326
+ .map((x) => stringEx(x ?? ''));
314
327
  const templateTerms = node.expressions
315
328
  .map(convertExpression)
316
329
  .map((ex) => functionInvocationEx('default', [ex, stringEx('null')]));
@@ -1 +1 @@
1
- {"version":3,"file":"transformations.d.ts","sourceRoot":"","sources":["../../src/transpiler/transformations.ts"],"names":[],"mappings":"AAmBA,OAAO,EAcL,iBAAiB,EAClB,MAAM,sBAAsB,CAAA;AAG7B;;GAEG;AACH,wBAAgB,YAAY,CAC1B,UAAU,EAAE,iBAAiB,EAAE,GAC9B,iBAAiB,EAAE,CAUrB"}
1
+ {"version":3,"file":"transformations.d.ts","sourceRoot":"","sources":["../../src/transpiler/transformations.ts"],"names":[],"mappings":"AAoBA,OAAO,EAcL,iBAAiB,EAClB,MAAM,sBAAsB,CAAA;AAG7B;;GAEG;AACH,wBAAgB,YAAY,CAC1B,UAAU,EAAE,iBAAiB,EAAE,GAC9B,iBAAiB,EAAE,CAUrB"}
@@ -1,6 +1,6 @@
1
1
  import * as R from 'ramda';
2
2
  import { InternalTranspilingError } from '../errors.js';
3
- import { binaryEx, functionInvocationEx, listEx, mapEx, memberEx, stringEx, unaryEx, variableReferenceEx, } from '../ast/expressions.js';
3
+ import { binaryEx, functionInvocationEx, listEx, mapEx, memberEx, nullEx, stringEx, unaryEx, variableReferenceEx, } from '../ast/expressions.js';
4
4
  import { applyNested, AssignStatement, FunctionInvocationStatement, IfStatement, LabelledStatement, } from '../ast/statements.js';
5
5
  import { blockingFunctions } from './generated/functionMetadata.js';
6
6
  /**
@@ -8,7 +8,7 @@ import { blockingFunctions } from './generated/functionMetadata.js';
8
8
  */
9
9
  export function transformAST(statements) {
10
10
  const tempGen = createTempVariableGenerator();
11
- const transform = R.pipe(R.chain(mapLiteralsAsAssigns(tempGen)), mergeAssigns, R.chain(intrinsicFunctionImplementation), R.chain(blockingCallsAsFunctionCalls(tempGen)));
11
+ const transform = R.pipe(R.chain(mapLiteralsAsAssigns(tempGen)), R.chain(intrinsicFunctionImplementation), R.chain(blockingCallsAsFunctionCalls(tempGen)), mergeAssigns);
12
12
  return transform(statements.map((s) => applyNested(transformAST, s)));
13
13
  }
14
14
  /**
@@ -206,7 +206,7 @@ function replaceBlockingCalls(generateName, expression) {
206
206
  }
207
207
  const blockingCallArgumentNames = blockingFunctions.get(ex.functionName);
208
208
  if (blockingCallArgumentNames) {
209
- const nameAndValue = R.zip(blockingCallArgumentNames, ex.arguments);
209
+ const nameAndValue = R.zip(blockingCallArgumentNames, ex.arguments).filter(isDefinedArgument);
210
210
  const callArgs = R.fromPairs(nameAndValue);
211
211
  const tempCallResultVariable = generateName();
212
212
  callStatements.push(new FunctionInvocationStatement(ex.functionName, callArgs, tempCallResultVariable));
@@ -223,6 +223,9 @@ function replaceBlockingCalls(generateName, expression) {
223
223
  transformExpression(replaceBlockingFunctionInvocations, expression),
224
224
  ];
225
225
  }
226
+ function isDefinedArgument(x) {
227
+ return x[1] !== undefined;
228
+ }
226
229
  /**
227
230
  * Search for blocking calls in expressions and replace them with a call + variable.
228
231
  *
@@ -258,6 +261,7 @@ function blockingCallsAsFunctionCalls(generateTempName) {
258
261
  */
259
262
  function transformExpression(transform, ex) {
260
263
  const nestedTr = (y) => transformExpression(transform, y);
264
+ const nestedTr2 = R.ifElse((R.isNil), () => undefined, nestedTr);
261
265
  switch (ex.tag) {
262
266
  case 'string':
263
267
  case 'number':
@@ -272,7 +276,7 @@ function transformExpression(transform, ex) {
272
276
  case 'binary':
273
277
  return transform(binaryEx(nestedTr(ex.left), ex.binaryOperator, nestedTr(ex.right)));
274
278
  case 'functionInvocation':
275
- return transform(functionInvocationEx(ex.functionName, ex.arguments.map(nestedTr)));
279
+ return transform(functionInvocationEx(ex.functionName, ex.arguments.map(nestedTr2)));
276
280
  case 'member':
277
281
  return transform(memberEx(nestedTr(ex.object), nestedTr(ex.property), ex.computed));
278
282
  case 'unary':
@@ -375,11 +379,19 @@ function extractMapsInMap(map, generateName, nestingLevel) {
375
379
  }
376
380
  function extractNestedMapFunctionInvocation(ex, generateName, nestingLevel) {
377
381
  const { expressions, temps } = ex.arguments.reduce((acc, arg) => {
378
- const { transformedExpression, tempVariables } = extractNestedMaps(arg, generateName, nestingLevel + 1);
379
- acc.expressions.push(transformedExpression);
380
- acc.temps.push(...tempVariables);
382
+ if (arg === undefined) {
383
+ acc.expressions.push(undefined);
384
+ }
385
+ else {
386
+ const { transformedExpression, tempVariables } = extractNestedMaps(arg, generateName, nestingLevel + 1);
387
+ acc.expressions.push(transformedExpression);
388
+ acc.temps.push(...tempVariables);
389
+ }
381
390
  return acc;
382
- }, { expressions: [], temps: [] });
391
+ }, {
392
+ expressions: [],
393
+ temps: [],
394
+ });
383
395
  return {
384
396
  transformedExpression: functionInvocationEx(ex.functionName, expressions),
385
397
  tempVariables: temps,
@@ -408,13 +420,13 @@ function extractNestedMapUnary(ex, generateName, nestingLevel) {
408
420
  tempVariables,
409
421
  };
410
422
  }
411
- /**
412
- * Replace `Array.isArray(x)` with `get_type(x) == "list"`
413
- */
414
423
  const intrinsicFunctionImplementation = expandExpressionToStatements((ex) => [
415
424
  [],
416
- transformExpression(replaceIsArray, ex),
425
+ transformExpression(R.pipe(replaceIsArray, replaceArrayIncludes), ex),
417
426
  ]);
427
+ /**
428
+ * Replace `Array.isArray(x)` with `get_type(x) == "list"`
429
+ */
418
430
  function replaceIsArray(ex) {
419
431
  if (ex.tag === 'functionInvocation' && ex.functionName === 'Array.isArray') {
420
432
  return binaryEx(functionInvocationEx('get_type', ex.arguments), '==', stringEx('list'));
@@ -423,3 +435,16 @@ function replaceIsArray(ex) {
423
435
  return ex;
424
436
  }
425
437
  }
438
+ /**
439
+ * Replace `Array.includes(arr, x)` with `x in arr`
440
+ */
441
+ function replaceArrayIncludes(ex) {
442
+ if (ex.tag === 'functionInvocation' && ex.functionName === 'Array.includes') {
443
+ const arrayEx = ex.arguments[0] ?? nullEx;
444
+ const valueEx = ex.arguments[1] ?? nullEx;
445
+ return binaryEx(valueEx, 'in', arrayEx);
446
+ }
447
+ else {
448
+ return ex;
449
+ }
450
+ }
@@ -72,6 +72,23 @@ name === 'Bean'
72
72
  sys.get_env('GOOGLE_CLOUD_PROJECT_ID')
73
73
  ```
74
74
 
75
+ ### Member expression
76
+
77
+ To get a value in an array, use the following code:
78
+
79
+ ```javasript
80
+ const val = arr[2];
81
+ ```
82
+
83
+ To get a value in a map, use either of following:
84
+
85
+ ```javasript
86
+ const val = obj.name;
87
+ const val2 = obj["name"];
88
+ ```
89
+
90
+ ⚠️ Numeric keys in maps are not cast to strings like in Typescript. It's best not to use numeric keys because GCP Workflows throws errors on numeric keys.
91
+
75
92
  ## Operators
76
93
 
77
94
  | Operator | Description |
@@ -880,7 +897,15 @@ This section describes the few standard Javascript runtime functions that are av
880
897
  Array.isArray(arg: any): arg is any[]
881
898
  ```
882
899
 
883
- Gets converted to the comparison `get_type(arg) == "list"`. Unlike a direct call to `get_type()`, `Array.isArray()` acts a type guard and allows narrowing the `arg` type to an array.
900
+ Returns true, if arg is an array, and false otherwise. `Array.isArray` gets converted to the following comparison on Workflows code: `get_type(arg) == "list"`. Unlike a direct call to `get_type()`, `Array.isArray()` acts a type guard in Typescript and allows narrowing the type of `arg` to an array.
901
+
902
+ ### Array.includes()
903
+
904
+ ```typescript
905
+ Array.includes<T>(arr: Array<T>, x: T): boolean
906
+ ```
907
+
908
+ Returns true, if the array `arr` contains the value `x`, and false otherwise. This static Array method should be used instead of `arr.includes()`, which won't work in Workflows, because arrays in Workflows don't have methods. `Array.includes()` is converted to `x in arr` in the outputted Workflows code.
884
909
 
885
910
  ## Compiler intrinsics
886
911
 
@@ -925,19 +950,7 @@ The `parallel` function executes code blocks in parallel using a [parallel step]
925
950
  ### retry_policy()
926
951
 
927
952
  ```typescript
928
- function retry_policy(
929
- params:
930
- | ((errormap: Record<string, any>) => void)
931
- | {
932
- predicate: (errormap: Record<string, any>) => boolean
933
- max_retries: number
934
- backoff: {
935
- initial_delay: number
936
- max_delay: number
937
- multiplier: number
938
- }
939
- },
940
- ): void
953
+ function retry_policy(params: RetryPolicy): void
941
954
  ```
942
955
 
943
956
  A retry policy can be attached to a `try`-`catch` block by calling `retry_policy` inside the `try` block. ts2workflows ignores `retry_policy` everywhere else.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts2workflows",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Transpile Typescript code to GCP Workflows programs",
5
5
  "homepage": "https://github.com/aajanki/ts2workflows",
6
6
  "repository": {
@@ -20,7 +20,7 @@
20
20
  "lint": "eslint src test scripts",
21
21
  "format": "prettier . --write",
22
22
  "test": "mocha",
23
- "test-coverage": "nyc --reporter=text --reporter=html mocha",
23
+ "test:coverage": "nyc --reporter=text --reporter=html mocha",
24
24
  "prepare": "husky && npm run build"
25
25
  },
26
26
  "lint-staged": {
@@ -73,7 +73,7 @@
73
73
  "husky": "^9.1.6",
74
74
  "lint-staged": "^16.1.2",
75
75
  "mocha": "^11.1.0",
76
- "nyc": "^17.1.0",
76
+ "nyc": "^18.0.0",
77
77
  "prettier": "^3.2.5",
78
78
  "rimraf": "^6.0.1",
79
79
  "source-map-support": "^0.5.21",
package/types/global.d.ts CHANGED
@@ -38,6 +38,7 @@ declare global {
38
38
 
39
39
  interface ArrayConstructor {
40
40
  isArray(arg: any): arg is any[]
41
+ includes<T>(arr: Array<T>, x: T): boolean
41
42
  }
42
43
 
43
44
  var Array: ArrayConstructor