ts2workflows 0.14.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # ts2workflows changelog
2
2
 
3
+ ## Version 0.15.0 - 2026-03-18
4
+
5
+ New features:
6
+
7
+ - Static method `Array.includes(arr, x)` for checking if the array `arr` contain the value `x`
8
+
9
+ Fixes:
10
+
11
+ - Print the correct source code line on error messages
12
+ - Type annotation fixes in workflowslib
13
+
3
14
  ## Version 0.14.0 - 2025-11-19
4
15
 
5
16
  Fixes:
package/README.md CHANGED
@@ -85,7 +85,7 @@ npm run test
85
85
  Run tests and print the test coverage:
86
86
 
87
87
  ```sh
88
- npm run test-coverage
88
+ npm run test:coverage
89
89
  ```
90
90
 
91
91
  ### Architecture
package/dist/cli.js CHANGED
@@ -6,6 +6,7 @@ import { program } from 'commander';
6
6
  import { transpile, transpileText } from './transpiler/index.js';
7
7
  import { WorkflowSyntaxError } from './errors.js';
8
8
  import { TSError } from '@typescript-eslint/typescript-estree';
9
+ import { prettifySyntaxError } from './diagnostics.js';
9
10
  function parseArgs() {
10
11
  program
11
12
  .name('ts2workflow')
@@ -37,19 +38,18 @@ function cliMain() {
37
38
  files = args.sourceFiles;
38
39
  }
39
40
  files.forEach((inputFile) => {
40
- const input = readSourceCode(inputFile);
41
41
  try {
42
- const transpiled = generateTranspiledText(input, args.generatedFileComment, args.link, args.project);
42
+ const transpiled = generateTranspiledText(inputFile, readSourceCode(inputFile), args.generatedFileComment, args.link, args.project);
43
43
  writeOutput(transpiled, inputFile, args.outdir);
44
44
  }
45
45
  catch (err) {
46
46
  if (isIoError(err)) {
47
47
  let message;
48
- if ('code' in err && err.code === 'EAGAIN' && inputFile === '-') {
48
+ if (err.code === 'EAGAIN' && inputFile === '-') {
49
49
  // Reading from stdin if there's no input causes error. This is a bug in node
50
50
  message = 'Error: Failed to read from stdin';
51
51
  }
52
- else if ('code' in err && err.code === 'EISDIR') {
52
+ else if (err.code === 'EISDIR') {
53
53
  message = `Error: "${inputFile}" is a directory`;
54
54
  }
55
55
  else {
@@ -59,11 +59,11 @@ function cliMain() {
59
59
  process.exit(1);
60
60
  }
61
61
  else if (err instanceof WorkflowSyntaxError) {
62
- prettyPrintSyntaxError(err, input);
62
+ console.error(prettifySyntaxError(err));
63
63
  process.exit(1);
64
64
  }
65
65
  else if (err instanceof TSError) {
66
- prettyPrintSyntaxError(err, input);
66
+ console.error(prettifySyntaxError(err));
67
67
  process.exit(1);
68
68
  }
69
69
  else {
@@ -72,31 +72,19 @@ function cliMain() {
72
72
  }
73
73
  });
74
74
  }
75
- function generateTranspiledText(input, addGeneratedFileComment, linkSubworkflows, project) {
76
- if (input.filename === undefined) {
77
- return transpileText(input.read());
75
+ function generateTranspiledText(filename, sourceCode, addGeneratedFileComment, linkSubworkflows, project) {
76
+ if (filename === undefined) {
77
+ return transpileText(sourceCode);
78
78
  }
79
79
  else {
80
- const header = addGeneratedFileComment
81
- ? generatedFileComment(input.filename)
82
- : '';
83
- const transpiled = transpile(input, project, linkSubworkflows);
80
+ const header = addGeneratedFileComment ? generatedFileComment(filename) : '';
81
+ const transpiled = transpile(filename, sourceCode, project, linkSubworkflows);
84
82
  return `${header}${transpiled}`;
85
83
  }
86
84
  }
87
85
  function readSourceCode(filename) {
88
- const readCode = crateMemorizedReader(filename === '-' ? process.stdin.fd : filename);
89
- return {
90
- filename: filename === '-' ? undefined : filename,
91
- read: () => readCode(),
92
- };
93
- }
94
- function crateMemorizedReader(filenameOrFd) {
95
- let cached;
96
- return () => {
97
- cached ??= fs.readFileSync(filenameOrFd, 'utf8');
98
- return cached;
99
- };
86
+ const filenameOrFd = filename === '-' ? process.stdin.fd : filename;
87
+ return fs.readFileSync(filenameOrFd, 'utf8');
100
88
  }
101
89
  function writeOutput(transpiled, inputFile, outdir) {
102
90
  if (outdir !== undefined) {
@@ -119,58 +107,12 @@ function createOutputFilename(inputFile, outdir) {
119
107
  });
120
108
  }
121
109
  function generatedFileComment(inputFile) {
122
- return (`# This file has been generated by "ts2workflows ${inputFile}"\n` +
110
+ return (`# This file has been generated by ts2workflows from source file ${inputFile}\n` +
123
111
  '# Do not edit!\n\n');
124
112
  }
125
113
  function isIoError(err) {
126
114
  return err instanceof Error && 'code' in err;
127
115
  }
128
- function prettyPrintSyntaxError(exception, inp) {
129
- console.error(errorDisplay(inp, exception.location));
130
- console.error(`${exception.message}`);
131
- }
132
- function errorDisplay(inp, location) {
133
- const lines = [];
134
- const prettyFilename = inp.filename ?? '<stdin>';
135
- if (typeof location?.start === 'undefined' ||
136
- typeof location?.end === 'undefined' ||
137
- isNaN(location?.start.line) ||
138
- isNaN(location?.end.line)) {
139
- lines.push(`File ${prettyFilename}:`);
140
- }
141
- else {
142
- lines.push(`File ${prettyFilename}, line ${location.start.line}, column ${location.start.column + 1}:`);
143
- }
144
- const highlightedLine = highlightedSourceCodeLine(inp.read(), location?.start?.line, location?.start?.column, location?.start?.line === location?.end?.line
145
- ? location?.end?.column
146
- : undefined);
147
- if (highlightedLine.length > 0) {
148
- lines.push(highlightedLine);
149
- lines.push('');
150
- }
151
- return lines.join('\n');
152
- }
153
- function highlightedSourceCodeLine(sourceCode, lineNumber, start, end) {
154
- if (typeof lineNumber === 'undefined' ||
155
- typeof start === 'undefined' ||
156
- isNaN(start)) {
157
- return '';
158
- }
159
- const lines = sourceCode.split('\n');
160
- const sourceLine = lines[lineNumber - 1];
161
- if (typeof sourceLine === 'undefined') {
162
- return '';
163
- }
164
- let markerLength;
165
- if (typeof end === 'undefined') {
166
- markerLength = sourceLine.length - start;
167
- }
168
- else {
169
- markerLength = Math.min(end - start + 1, sourceLine.length - start);
170
- }
171
- const markerLine = `${' '.repeat(start)}${'^'.repeat(markerLength)}`;
172
- return `${sourceLine}\n${markerLine}`;
173
- }
174
116
  function versionFromPackageJson() {
175
117
  const currentFile = fileURLToPath(import.meta.url);
176
118
  const currentDir = path.dirname(currentFile);
@@ -0,0 +1,3 @@
1
+ import { WorkflowSyntaxError } from './errors.js';
2
+ export declare function prettifySyntaxError(exception: WorkflowSyntaxError): string;
3
+ //# sourceMappingURL=diagnostics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnostics.d.ts","sourceRoot":"","sources":["../src/diagnostics.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,mBAAmB,EAEpB,MAAM,aAAa,CAAA;AAEpB,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,mBAAmB,GAAG,MAAM,CAc1E"}
@@ -0,0 +1,46 @@
1
+ import { WorkflowSyntaxErrorWithText, } from './errors.js';
2
+ export function prettifySyntaxError(exception) {
3
+ const filename = exception instanceof WorkflowSyntaxErrorWithText
4
+ ? exception.filename
5
+ : '???';
6
+ const errorLineText = exception instanceof WorkflowSyntaxErrorWithText
7
+ ? exception.errorLine
8
+ : undefined;
9
+ return (`${errorLocator(filename, exception.location, errorLineText)}\n` +
10
+ `${exception.message}`);
11
+ }
12
+ function errorLocator(filename, location, errorLineText) {
13
+ const lines = [];
14
+ if (isNaN(location.start.line) || isNaN(location.end.line)) {
15
+ lines.push(`File ${filename}:`);
16
+ }
17
+ else {
18
+ lines.push(`File ${filename}, line ${location.start.line}, column ${location.start.column + 1}:`);
19
+ }
20
+ if (errorLineText) {
21
+ const highlightedLine = highlightedSourceCodeLine(errorLineText, location.start.column, location.start.line === location.end.line
22
+ ? location.end.column
23
+ : undefined);
24
+ if (highlightedLine.length > 0) {
25
+ lines.push(highlightedLine);
26
+ lines.push('');
27
+ }
28
+ }
29
+ return lines.join('\n');
30
+ }
31
+ function highlightedSourceCodeLine(errorLineText, start, end) {
32
+ if (isNaN(start) || start < 0) {
33
+ return '';
34
+ }
35
+ const sourceLine = errorLineText.split('\n')[0] ?? '';
36
+ let markerLength;
37
+ if (end === undefined || isNaN(end) || end < 0) {
38
+ markerLength = sourceLine.length - start;
39
+ }
40
+ else {
41
+ markerLength = Math.min(end - start + 1, sourceLine.length - start);
42
+ }
43
+ markerLength = Math.max(markerLength, 0);
44
+ const markerLine = `${' '.repeat(start)}${'^'.repeat(markerLength)}`;
45
+ return `${sourceLine}\n${markerLine}`;
46
+ }
package/dist/errors.d.ts CHANGED
@@ -9,9 +9,16 @@ export interface SourceCodeLocation {
9
9
  };
10
10
  }
11
11
  export declare class WorkflowSyntaxError extends Error {
12
- location: SourceCodeLocation;
12
+ readonly location: SourceCodeLocation;
13
13
  constructor(message: string, location: SourceCodeLocation);
14
14
  }
15
+ export declare class WorkflowSyntaxErrorWithText extends WorkflowSyntaxError {
16
+ readonly location: SourceCodeLocation;
17
+ readonly filename: string;
18
+ readonly errorLine: string;
19
+ constructor(message: string, location: SourceCodeLocation, filename: string, errorLine: string);
20
+ }
21
+ export declare function syntaxErrorWithText(error: WorkflowSyntaxError, filename: string, sourceCode: string): WorkflowSyntaxErrorWithText;
15
22
  export declare class InternalTranspilingError extends Error {
16
23
  constructor(message: string);
17
24
  }
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;IACvC,GAAG,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;CACtC;AAID,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,QAAQ,EAAE,kBAAkB,CAAA;gBAEhB,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB;CAI1D;AAID,qBAAa,wBAAyB,SAAQ,KAAK;gBACrC,OAAO,EAAE,MAAM;CAG5B;AAED,qBAAa,OAAQ,SAAQ,KAAK;aAGd,IAAI,EAAE,MAAM;gBAD5B,OAAO,EAAE,MAAM,EACC,IAAI,EAAE,MAAM;CAI/B"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;IACvC,GAAG,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;CACtC;AAID,qBAAa,mBAAoB,SAAQ,KAAK;aAG1B,QAAQ,EAAE,kBAAkB;gBAD5C,OAAO,EAAE,MAAM,EACC,QAAQ,EAAE,kBAAkB;CAI/C;AAID,qBAAa,2BAA4B,SAAQ,mBAAmB;aAGhD,QAAQ,EAAE,kBAAkB;aAC5B,QAAQ,EAAE,MAAM;aAChB,SAAS,EAAE,MAAM;gBAHjC,OAAO,EAAE,MAAM,EACC,QAAQ,EAAE,kBAAkB,EAC5B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM;CAIpC;AAED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,mBAAmB,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,2BAA2B,CAW7B;AAID,qBAAa,wBAAyB,SAAQ,KAAK;gBACrC,OAAO,EAAE,MAAM;CAG5B;AAGD,qBAAa,OAAQ,SAAQ,KAAK;aAGd,IAAI,EAAE,MAAM;gBAD5B,OAAO,EAAE,MAAM,EACC,IAAI,EAAE,MAAM;CAI/B"}
package/dist/errors.js CHANGED
@@ -7,13 +7,33 @@ export class WorkflowSyntaxError extends Error {
7
7
  this.location = location;
8
8
  }
9
9
  }
10
+ // WorkflowSyntaxError enriched with the source code filename and the text of
11
+ // the line where the error occurred.
12
+ export class WorkflowSyntaxErrorWithText extends WorkflowSyntaxError {
13
+ location;
14
+ filename;
15
+ errorLine;
16
+ constructor(message, location, filename, errorLine) {
17
+ super(message, location);
18
+ this.location = location;
19
+ this.filename = filename;
20
+ this.errorLine = errorLine;
21
+ }
22
+ }
23
+ export function syntaxErrorWithText(error, filename, sourceCode) {
24
+ const lineNumber = Math.max(error.location.start.line, 0);
25
+ const lines = sourceCode.split('\n');
26
+ const errorLine = lines[lineNumber - 1];
27
+ return new WorkflowSyntaxErrorWithText(error.message, error.location, filename, errorLine);
28
+ }
10
29
  // InternalTranspilingError is thrown when ts2workflow ends up in an unexpected state.
11
- // The error is in ts2workflow.
30
+ // This is a bug in ts2workflow.
12
31
  export class InternalTranspilingError extends Error {
13
32
  constructor(message) {
14
33
  super(`Internal error: ${message}`);
15
34
  }
16
35
  }
36
+ // An IO error with an error code string, similar to Node's SystemError
17
37
  export class IOError extends Error {
18
38
  code;
19
39
  constructor(message, code) {
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { transpile } from './transpiler/index.js';
2
- export { WorkflowSyntaxError, InternalTranspilingError, SourceCodeLocation, } from './errors.js';
1
+ export { transpile, transpileText } from './transpiler/index.js';
2
+ export { WorkflowSyntaxError, WorkflowSyntaxErrorWithText, InternalTranspilingError, SourceCodeLocation, } from './errors.js';
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AACjD,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,kBAAkB,GACnB,MAAM,aAAa,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAChE,OAAO,EACL,mBAAmB,EACnB,2BAA2B,EAC3B,wBAAwB,EACxB,kBAAkB,GACnB,MAAM,aAAa,CAAA"}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { transpile } from './transpiler/index.js';
2
- export { WorkflowSyntaxError, InternalTranspilingError, } from './errors.js';
1
+ export { transpile, transpileText } from './transpiler/index.js';
2
+ export { WorkflowSyntaxError, WorkflowSyntaxErrorWithText, InternalTranspilingError, } from './errors.js';
@@ -1,9 +1,6 @@
1
1
  import ts from 'typescript';
2
2
  import { WorkflowApp } from '../ast/workflows.js';
3
- export declare function transpile(input: {
4
- filename?: string;
5
- read: () => string;
6
- }, tsconfigPath: string | undefined, linkSubworkflows: boolean): string;
3
+ export declare function transpile(filename: string, sourceCode: string, tsconfigPath: string | undefined, linkSubworkflows: boolean): string;
7
4
  export declare function transpileText(sourceCode: string): string;
8
5
  /**
9
6
  * Returns true if node is an ambient function declaration.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/transpiler/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,MAAM,YAAY,CAAA;AAO3B,OAAO,EAGL,WAAW,EAEZ,MAAM,qBAAqB,CAAA;AAU5B,wBAAgB,SAAS,CACvB,KAAK,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,MAAM,CAAA;CAAE,EAChD,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,gBAAgB,EAAE,OAAO,GACxB,MAAM,CAoBR;AAED,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,UAK/C;AAqID;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,EAAE,CAAC,mBAAmB,GAC3B,OAAO,CAkBT;AAgID;;GAEG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,WAAW,GAAG,MAAM,CAI1D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/transpiler/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,MAAM,YAAY,CAAA;AAQ3B,OAAO,EAGL,WAAW,EAEZ,MAAM,qBAAqB,CAAA;AAU5B,wBAAgB,SAAS,CACvB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,gBAAgB,EAAE,OAAO,GACxB,MAAM,CA2BR;AAED,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,UAS/C;AA2JD;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,EAAE,CAAC,mBAAmB,GAC3B,OAAO,CAkBT;AAgID;;GAEG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,WAAW,GAAG,MAAM,CAI1D"}
@@ -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;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"}
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
  }
@@ -319,7 +323,7 @@ function convertConditionalExpression(node) {
319
323
  function convertTemplateLiteralToExpression(node) {
320
324
  const stringTerms = node.quasis
321
325
  .map((x) => x.value.cooked)
322
- .map((x) => stringEx(x));
326
+ .map((x) => stringEx(x ?? ''));
323
327
  const templateTerms = node.expressions
324
328
  .map(convertExpression)
325
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
  /**
@@ -420,13 +420,13 @@ function extractNestedMapUnary(ex, generateName, nestingLevel) {
420
420
  tempVariables,
421
421
  };
422
422
  }
423
- /**
424
- * Replace `Array.isArray(x)` with `get_type(x) == "list"`
425
- */
426
423
  const intrinsicFunctionImplementation = expandExpressionToStatements((ex) => [
427
424
  [],
428
- transformExpression(replaceIsArray, ex),
425
+ transformExpression(R.pipe(replaceIsArray, replaceArrayIncludes), ex),
429
426
  ]);
427
+ /**
428
+ * Replace `Array.isArray(x)` with `get_type(x) == "list"`
429
+ */
430
430
  function replaceIsArray(ex) {
431
431
  if (ex.tag === 'functionInvocation' && ex.functionName === 'Array.isArray') {
432
432
  return binaryEx(functionInvocationEx('get_type', ex.arguments), '==', stringEx('list'));
@@ -435,3 +435,16 @@ function replaceIsArray(ex) {
435
435
  return ex;
436
436
  }
437
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.14.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
@@ -32,6 +32,16 @@ type HTTPQuery = Record<
32
32
  string | number | boolean | (string | number | boolean)[]
33
33
  >
34
34
 
35
+ type RetryPolicy = {
36
+ predicate?: (errormap: Record<string, any>) => boolean
37
+ max_retries?: number | string | null
38
+ backoff: {
39
+ initial_delay?: number | string | null
40
+ max_delay?: number | string | null
41
+ multiplier?: number | string | null
42
+ }
43
+ }
44
+
35
45
  // GCP Workflows expression helpers
36
46
 
37
47
  export declare function double(x: string | number): number
@@ -93,10 +103,8 @@ export declare namespace hash {
93
103
  }
94
104
 
95
105
  export declare namespace http {
96
- export function default_retry(errormap: Record<string, any>): void
97
- export function default_retry_non_idempotent(
98
- errormap: Record<string, any>,
99
- ): void
106
+ export const default_retry: RetryPolicy
107
+ export const default_retry_non_idempotent: RetryPolicy
100
108
  export function default_retry_predicate(
101
109
  errormap: Record<string, any>,
102
110
  ): boolean
@@ -1076,19 +1084,7 @@ export declare function parallel(
1076
1084
  },
1077
1085
  ): void
1078
1086
 
1079
- export declare function retry_policy(
1080
- params:
1081
- | ((errormap: Record<string, any>) => void)
1082
- | {
1083
- predicate?: (errormap: Record<string, any>) => boolean
1084
- max_retries?: number | string | null
1085
- backoff: {
1086
- initial_delay?: number | string | null
1087
- max_delay?: number | string | null
1088
- multiplier?: number | string | null
1089
- }
1090
- },
1091
- ): void
1087
+ export declare function retry_policy(params: RetryPolicy): void
1092
1088
 
1093
1089
  export declare function call_step<T, A extends any[]>(
1094
1090
  func: (...args: A) => T,