ts2workflows 0.6.0 → 0.8.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/README.md +8 -2
- package/dist/cli.js +85 -28
- package/dist/transpiler/expressions.d.ts.map +1 -1
- package/dist/transpiler/expressions.js +35 -8
- package/dist/transpiler/index.d.ts +1 -1
- package/dist/transpiler/index.d.ts.map +1 -1
- package/dist/transpiler/index.js +43 -19
- package/dist/transpiler/statements.js +10 -7
- package/language_reference.md +38 -10
- package/package.json +4 -4
- package/types/workflowslib.d.ts +44 -20
package/README.md
CHANGED
|
@@ -16,6 +16,12 @@ Converting Typescript code in a file samples/sample1.ts into GCP Workflows YAML
|
|
|
16
16
|
npx ts2workflows samples/sample1.ts
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
Compile multiple files and write result to an output directory `workflowsfiles`. This will write one output file corresponding to each input file in a directory given by the `--outdir` argument. The output files are named similarly to the input files but using `.yaml` as the file extension. The output directory will be created if it doesn't exist. Supplying the TSConfig with the `--project` argument makes compiling multiple files faster.
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npx ts2workflows --project samples/tsconfig.json --outdir workflowsfiles samples/*.ts
|
|
23
|
+
```
|
|
24
|
+
|
|
19
25
|
When developing ts2workflows, you can run the transpiler directly from the source directory:
|
|
20
26
|
|
|
21
27
|
```sh
|
|
@@ -29,10 +35,10 @@ One benefit of writing the workflow programs in Typescript is that the sources c
|
|
|
29
35
|
This example command shows how to type check source files in the [samples](samples) directory using the standard Typescript compiler `tsc`. The command prints typing errors or finishes silently, if there are no errors.
|
|
30
36
|
|
|
31
37
|
```sh
|
|
32
|
-
npx tsc --project tsconfig.
|
|
38
|
+
npx tsc --project samples/tsconfig.json
|
|
33
39
|
```
|
|
34
40
|
|
|
35
|
-
The file [tsconfig.
|
|
41
|
+
The file [samples/tsconfig.json](samples/tsconfig.json) contains a sample configuration for type checking workflow sources.
|
|
36
42
|
|
|
37
43
|
Type annotations for [Workflows standard library functions and expression helpers](https://cloud.google.com/workflows/docs/reference/stdlib/overview) and for some [connectors](https://cloud.google.com/workflows/docs/reference/googleapis) are provided in [types/workflowslib.d.ts](types/workflowslib.d.ts). They are also included in the published npm module. To import type annotations in a project that has ts2workflows module as a dependency, use the following import command:
|
|
38
44
|
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
3
5
|
import { program } from 'commander';
|
|
4
6
|
import { transpile } from './transpiler/index.js';
|
|
5
7
|
import { WorkflowSyntaxError } from './errors.js';
|
|
@@ -7,11 +9,16 @@ import { TSError } from '@typescript-eslint/typescript-estree';
|
|
|
7
9
|
function parseArgs() {
|
|
8
10
|
program
|
|
9
11
|
.name('ts2workflow')
|
|
10
|
-
.version(
|
|
12
|
+
.version(versionFromPackageJson())
|
|
11
13
|
.description('Transpile a Typescript program into GCP Workflows YAML syntax.')
|
|
12
|
-
.
|
|
13
|
-
|
|
14
|
+
.option('--project <path>', 'Path to TSConfig for the Typescript sources files.')
|
|
15
|
+
.option('--outdir <path>', 'Specify an output directory for where transpilation result are written.')
|
|
16
|
+
.option('--generated-file-comment', 'Include a comment stating that the result is a generated file', true)
|
|
17
|
+
.option('--no-generated-file-comment', "Don't include a comment about file being generated")
|
|
18
|
+
.argument('[FILES...]', 'Path to source file(s) to compile. If not given, reads from stdin.')
|
|
19
|
+
.parse();
|
|
14
20
|
return {
|
|
21
|
+
...program.opts(),
|
|
15
22
|
sourceFiles: program.args,
|
|
16
23
|
};
|
|
17
24
|
}
|
|
@@ -25,32 +32,27 @@ function cliMain() {
|
|
|
25
32
|
files = args.sourceFiles;
|
|
26
33
|
}
|
|
27
34
|
files.forEach((inputFile) => {
|
|
28
|
-
const inp = inputFile === '-' ? process.stdin.fd : inputFile;
|
|
29
|
-
let sourceCode = '';
|
|
30
35
|
try {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
catch (err) {
|
|
35
|
-
if (isIoError(err, 'ENOENT')) {
|
|
36
|
-
console.error(`Error: "${inp}" not found`);
|
|
37
|
-
process.exit(1);
|
|
38
|
-
}
|
|
39
|
-
else if (isIoError(err, 'EISDIR')) {
|
|
40
|
-
console.error(`Error: "${inp}" is a directory`);
|
|
36
|
+
const transpiled = generateTranspiledText(inputFile, args.generatedFileComment, args.project);
|
|
37
|
+
if (transpiled === undefined) {
|
|
41
38
|
process.exit(1);
|
|
42
39
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
40
|
+
writeOutput(transpiled, inputFile, args.outdir);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
if (isIoError(err)) {
|
|
44
|
+
let message;
|
|
45
|
+
if ('code' in err && err.code === 'EAGAIN' && inputFile === '-') {
|
|
46
|
+
// Reading from stdin if there's no input causes error. This is a bug in node
|
|
47
|
+
message = 'Error: Failed to read from stdin';
|
|
48
|
+
}
|
|
49
|
+
else if ('code' in err && err.code === 'EISDIR') {
|
|
50
|
+
message = `Error: "${inputFile}" is a directory`;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
message = err.message;
|
|
54
|
+
}
|
|
55
|
+
console.error(message);
|
|
54
56
|
process.exit(1);
|
|
55
57
|
}
|
|
56
58
|
else {
|
|
@@ -59,8 +61,56 @@ function cliMain() {
|
|
|
59
61
|
}
|
|
60
62
|
});
|
|
61
63
|
}
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
+
function generateTranspiledText(inputFile, addGeneratedFileComment, project) {
|
|
65
|
+
const inputIsStdIn = inputFile === '-';
|
|
66
|
+
const inp = inputIsStdIn ? process.stdin.fd : inputFile;
|
|
67
|
+
const sourceCode = fs.readFileSync(inp, 'utf8');
|
|
68
|
+
try {
|
|
69
|
+
const needsHeader = addGeneratedFileComment && !inputIsStdIn;
|
|
70
|
+
const header = needsHeader ? generatedFileComment(inputFile) : '';
|
|
71
|
+
const transpiled = transpile(sourceCode, inputFile, project);
|
|
72
|
+
return `${header}${transpiled}`;
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
if (err instanceof WorkflowSyntaxError) {
|
|
76
|
+
prettyPrintSyntaxError(err, inputFile, sourceCode);
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
else if (err instanceof TSError) {
|
|
80
|
+
prettyPrintSyntaxError(err, inputFile, sourceCode);
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function writeOutput(transpiled, inputFile, outdir) {
|
|
89
|
+
if (outdir !== undefined) {
|
|
90
|
+
if (!fs.existsSync(outdir)) {
|
|
91
|
+
fs.mkdirSync(outdir, { recursive: true });
|
|
92
|
+
}
|
|
93
|
+
const outputFile = createOutputFilename(inputFile, outdir);
|
|
94
|
+
fs.writeFileSync(outputFile, transpiled);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
process.stdout.write(transpiled);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function createOutputFilename(inputFile, outdir) {
|
|
101
|
+
const parsedInput = path.parse(inputFile);
|
|
102
|
+
return path.format({
|
|
103
|
+
dir: outdir,
|
|
104
|
+
name: parsedInput.name,
|
|
105
|
+
ext: '.yaml',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
function generatedFileComment(inputFile) {
|
|
109
|
+
return (`# This file has been generated by "ts2workflows ${inputFile}"\n` +
|
|
110
|
+
'# Do not edit!\n\n');
|
|
111
|
+
}
|
|
112
|
+
function isIoError(err) {
|
|
113
|
+
return err instanceof Error && 'code' in err;
|
|
64
114
|
}
|
|
65
115
|
function prettyPrintSyntaxError(exception, inputFile, sourceCode) {
|
|
66
116
|
console.error(errorDisplay(inputFile, sourceCode, exception.location));
|
|
@@ -108,6 +158,13 @@ function highlightedSourceCodeLine(sourceCode, lineNumber, start, end) {
|
|
|
108
158
|
const markerLine = `${' '.repeat(start)}${'^'.repeat(markerLength)}`;
|
|
109
159
|
return `${sourceLine}\n${markerLine}`;
|
|
110
160
|
}
|
|
161
|
+
function versionFromPackageJson() {
|
|
162
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
163
|
+
const currentDir = path.dirname(currentFile);
|
|
164
|
+
const packagePath = path.join(currentDir, '..', 'package.json');
|
|
165
|
+
const pjson = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
|
|
166
|
+
return pjson.version ?? '???';
|
|
167
|
+
}
|
|
111
168
|
if (import.meta.url.endsWith(process.argv[1]) ||
|
|
112
169
|
process.argv[1].endsWith('/ts2workflows')) {
|
|
113
170
|
cliMain();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"expressions.d.ts","sourceRoot":"","sources":["../../src/transpiler/expressions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAkB,MAAM,sCAAsC,CAAA;AAC/E,OAAO,EAGL,UAAU,EAGV,SAAS,EAMV,MAAM,uBAAuB,CAAA;AAI9B,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,UAAU,CAO3E;AAED,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,QAAQ,CAAC,gBAAgB,GAC9B,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,UAAU,CAAC,CAgCxC;AAED,wBAAgB,+BAA+B,CAC7C,IAAI,EAAE,QAAQ,CAAC,gBAAgB,GAC9B,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAG5B;
|
|
1
|
+
{"version":3,"file":"expressions.d.ts","sourceRoot":"","sources":["../../src/transpiler/expressions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAkB,MAAM,sCAAsC,CAAA;AAC/E,OAAO,EAGL,UAAU,EAGV,SAAS,EAMV,MAAM,uBAAuB,CAAA;AAI9B,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,GAAG,UAAU,CAO3E;AAED,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,QAAQ,CAAC,gBAAgB,GAC9B,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,UAAU,CAAC,CAgCxC;AAED,wBAAgB,+BAA+B,CAC7C,IAAI,EAAE,QAAQ,CAAC,gBAAgB,GAC9B,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAG5B;AAmOD,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,QAAQ,CAAC,gBAAgB,GAC9B,UAAU,CAcZ;AA0JD,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAG3D;AAED,wBAAgB,2BAA2B,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAGvE;AAoDD,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,YAAY,CAAC,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,UAAU,CAElE"}
|
|
@@ -87,6 +87,8 @@ function convertExpressionOrPrimitive(instance) {
|
|
|
87
87
|
return convertExpressionOrPrimitive(instance.expression);
|
|
88
88
|
case AST_NODE_TYPES.AwaitExpression:
|
|
89
89
|
return convertExpressionOrPrimitive(instance.argument);
|
|
90
|
+
case AST_NODE_TYPES.TSInstantiationExpression:
|
|
91
|
+
return convertExpressionOrPrimitive(instance.expression);
|
|
90
92
|
default:
|
|
91
93
|
throw new WorkflowSyntaxError(`Not implemented expression type: ${instance.type}`, instance.loc);
|
|
92
94
|
}
|
|
@@ -149,6 +151,7 @@ function convertUnaryExpression(instance) {
|
|
|
149
151
|
throw new WorkflowSyntaxError('only prefix unary operators are supported', instance.loc);
|
|
150
152
|
}
|
|
151
153
|
let op;
|
|
154
|
+
let istypeof = false;
|
|
152
155
|
switch (instance.operator) {
|
|
153
156
|
case '+':
|
|
154
157
|
op = '+';
|
|
@@ -164,6 +167,10 @@ function convertUnaryExpression(instance) {
|
|
|
164
167
|
// This is wrong: the return value should be ignored.
|
|
165
168
|
op = undefined;
|
|
166
169
|
break;
|
|
170
|
+
case 'typeof':
|
|
171
|
+
op = undefined;
|
|
172
|
+
istypeof = true;
|
|
173
|
+
break;
|
|
167
174
|
case undefined:
|
|
168
175
|
op = undefined;
|
|
169
176
|
break;
|
|
@@ -171,7 +178,27 @@ function convertUnaryExpression(instance) {
|
|
|
171
178
|
throw new WorkflowSyntaxError(`Unsupported unary operator: ${instance.operator}`, instance.loc);
|
|
172
179
|
}
|
|
173
180
|
const ex = convertExpression(instance.argument);
|
|
174
|
-
|
|
181
|
+
if (istypeof) {
|
|
182
|
+
return convertTypeOfExpression(ex);
|
|
183
|
+
}
|
|
184
|
+
else if (op) {
|
|
185
|
+
return new UnaryExpression(op, ex);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
return ex;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function convertTypeOfExpression(value) {
|
|
192
|
+
// Note for future rectoring: evalute value only once (in case it has side effects)
|
|
193
|
+
return new FunctionInvocationExpression('text.replace_all_regex', [
|
|
194
|
+
new FunctionInvocationExpression('text.replace_all_regex', [
|
|
195
|
+
new FunctionInvocationExpression('get_type', [value]),
|
|
196
|
+
new PrimitiveExpression('^(bytes|list|map|null)$'),
|
|
197
|
+
new PrimitiveExpression('object'),
|
|
198
|
+
]),
|
|
199
|
+
new PrimitiveExpression('^(double|integer)$'),
|
|
200
|
+
new PrimitiveExpression('number'),
|
|
201
|
+
]);
|
|
175
202
|
}
|
|
176
203
|
export function convertMemberExpression(node) {
|
|
177
204
|
if (node.property.type === AST_NODE_TYPES.PrivateIdentifier) {
|
|
@@ -307,7 +334,12 @@ function convertTemplateLiteralToExpression(node) {
|
|
|
307
334
|
const stringTerms = node.quasis
|
|
308
335
|
.map((x) => x.value.cooked)
|
|
309
336
|
.map((x) => new PrimitiveExpression(x));
|
|
310
|
-
const templateTerms = node.expressions
|
|
337
|
+
const templateTerms = node.expressions
|
|
338
|
+
.map(convertExpression)
|
|
339
|
+
.map((ex) => new FunctionInvocationExpression('default', [
|
|
340
|
+
ex,
|
|
341
|
+
new PrimitiveExpression('null'),
|
|
342
|
+
]));
|
|
311
343
|
// interleave string parts and the expression parts starting with strings
|
|
312
344
|
const interleavedTerms = stringTerms
|
|
313
345
|
.slice(0, stringTerms.length - 1)
|
|
@@ -324,13 +356,8 @@ function convertTemplateLiteralToExpression(node) {
|
|
|
324
356
|
if (interleavedTerms.length === 0) {
|
|
325
357
|
return new PrimitiveExpression('');
|
|
326
358
|
}
|
|
327
|
-
else if (interleavedTerms.length === 1) {
|
|
328
|
-
return interleavedTerms[0];
|
|
329
|
-
}
|
|
330
359
|
else {
|
|
331
|
-
return interleavedTerms.reduce((previous, current) =>
|
|
332
|
-
return new BinaryExpression(previous, '+', current);
|
|
333
|
-
});
|
|
360
|
+
return interleavedTerms.reduce((previous, current) => new BinaryExpression(previous, '+', current));
|
|
334
361
|
}
|
|
335
362
|
}
|
|
336
363
|
export function throwIfSpread(nodes) {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare function transpile(code: string): string;
|
|
1
|
+
export declare function transpile(code: string, inputFile?: string, tsconfigPath?: string): string;
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/transpiler/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/transpiler/index.ts"],"names":[],"mappings":"AAaA,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,EACZ,SAAS,CAAC,EAAE,MAAM,EAClB,YAAY,CAAC,EAAE,MAAM,GACpB,MAAM,CAkBR"}
|
package/dist/transpiler/index.js
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AST_NODE_TYPES, parseAndGenerateServices, } from '@typescript-eslint/typescript-estree';
|
|
2
2
|
import * as YAML from 'yaml';
|
|
3
3
|
import { SubworkflowAST } from '../ast/steps.js';
|
|
4
4
|
import { WorkflowSyntaxError } from '../errors.js';
|
|
5
5
|
import { generateStepNames } from '../ast/stepnames.js';
|
|
6
6
|
import { parseStatement } from './statements.js';
|
|
7
|
-
export function transpile(code) {
|
|
7
|
+
export function transpile(code, inputFile, tsconfigPath) {
|
|
8
8
|
const parserOptions = {
|
|
9
9
|
jsDocParsingMode: 'none',
|
|
10
10
|
loc: true,
|
|
11
11
|
range: false,
|
|
12
12
|
};
|
|
13
|
-
|
|
13
|
+
if (tsconfigPath && inputFile && inputFile != '-') {
|
|
14
|
+
parserOptions.filePath = inputFile;
|
|
15
|
+
parserOptions.projectService = {
|
|
16
|
+
defaultProject: tsconfigPath,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const { ast } = parseAndGenerateServices(code, parserOptions);
|
|
14
20
|
const workflowAst = { subworkflows: ast.body.flatMap(parseTopLevelStatement) };
|
|
15
21
|
const workflow = generateStepNames(workflowAst);
|
|
16
22
|
return YAML.stringify(workflow.render(), { lineWidth: 100 });
|
|
@@ -49,24 +55,14 @@ function parseSubworkflows(node) {
|
|
|
49
55
|
const workflowParams = nodeParams.map((param) => {
|
|
50
56
|
switch (param.type) {
|
|
51
57
|
case AST_NODE_TYPES.Identifier:
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (param.left.type !== AST_NODE_TYPES.Identifier) {
|
|
55
|
-
throw new WorkflowSyntaxError('The default value must be an identifier', param.left.loc);
|
|
56
|
-
}
|
|
57
|
-
if (param.right.type !== AST_NODE_TYPES.Literal) {
|
|
58
|
-
throw new WorkflowSyntaxError('The default value must be a literal', param.right.loc);
|
|
58
|
+
if (param.optional) {
|
|
59
|
+
return { name: param.name, default: null };
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
typeof param.right.value === 'boolean' ||
|
|
63
|
-
param.right.value === null)) {
|
|
64
|
-
throw new WorkflowSyntaxError('The default value must be a string, number, boolean or null', param.left.loc);
|
|
61
|
+
else {
|
|
62
|
+
return { name: param.name };
|
|
65
63
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
default: param.right.value,
|
|
69
|
-
};
|
|
64
|
+
case AST_NODE_TYPES.AssignmentPattern:
|
|
65
|
+
return parseSubworkflowDefaultArgument(param);
|
|
70
66
|
default:
|
|
71
67
|
throw new WorkflowSyntaxError('Function parameter must be an identifier or an assignment', param.loc);
|
|
72
68
|
}
|
|
@@ -74,3 +70,31 @@ function parseSubworkflows(node) {
|
|
|
74
70
|
const steps = parseStatement(node.body, {});
|
|
75
71
|
return new SubworkflowAST(node.id.name, steps, workflowParams);
|
|
76
72
|
}
|
|
73
|
+
function parseSubworkflowDefaultArgument(param) {
|
|
74
|
+
if (param.left.type !== AST_NODE_TYPES.Identifier) {
|
|
75
|
+
throw new WorkflowSyntaxError('The default value must be an identifier', param.left.loc);
|
|
76
|
+
}
|
|
77
|
+
if (param.left.optional) {
|
|
78
|
+
throw new WorkflowSyntaxError("Parameter can't have default value and initializer", param.left.loc);
|
|
79
|
+
}
|
|
80
|
+
const name = param.left.name;
|
|
81
|
+
let defaultValue;
|
|
82
|
+
if (param.right.type === AST_NODE_TYPES.Identifier &&
|
|
83
|
+
param.right.name === 'undefined') {
|
|
84
|
+
defaultValue = null;
|
|
85
|
+
}
|
|
86
|
+
else if (param.right.type === AST_NODE_TYPES.Literal &&
|
|
87
|
+
(typeof param.right.value === 'string' ||
|
|
88
|
+
typeof param.right.value === 'number' ||
|
|
89
|
+
typeof param.right.value === 'boolean' ||
|
|
90
|
+
param.right.value === null)) {
|
|
91
|
+
defaultValue = param.right.value;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
throw new WorkflowSyntaxError('The default value must be a literal number, string, boolean, null, or undefined', param.right.loc);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
name,
|
|
98
|
+
default: defaultValue,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -199,18 +199,21 @@ function createCallStep(node, argumentsNode, resultVariable) {
|
|
|
199
199
|
throw new WorkflowSyntaxError('The first argument must be a Function', node.loc);
|
|
200
200
|
}
|
|
201
201
|
let functionName;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
202
|
+
const argNode = argumentsNode[0].type === AST_NODE_TYPES.TSInstantiationExpression
|
|
203
|
+
? argumentsNode[0].expression
|
|
204
|
+
: argumentsNode[0];
|
|
205
|
+
if (argNode.type === AST_NODE_TYPES.Identifier) {
|
|
206
|
+
functionName = argNode.name;
|
|
207
|
+
}
|
|
208
|
+
else if (argNode.type === AST_NODE_TYPES.MemberExpression) {
|
|
209
|
+
const memberExp = convertMemberExpression(argNode);
|
|
207
210
|
if (!isFullyQualifiedName(memberExp)) {
|
|
208
|
-
throw new WorkflowSyntaxError('Function name must be a fully-qualified name',
|
|
211
|
+
throw new WorkflowSyntaxError('Function name must be a fully-qualified name', argNode.loc);
|
|
209
212
|
}
|
|
210
213
|
functionName = memberExp.toString();
|
|
211
214
|
}
|
|
212
215
|
else {
|
|
213
|
-
throw new WorkflowSyntaxError('Expected an identifier or a member expression',
|
|
216
|
+
throw new WorkflowSyntaxError('Expected an identifier or a member expression', argNode.loc);
|
|
214
217
|
}
|
|
215
218
|
let args = {};
|
|
216
219
|
if (argumentsNode.length >= 2) {
|
package/language_reference.md
CHANGED
|
@@ -34,11 +34,13 @@ Map keys can be identifiers or strings: `{temperature: -12}` or `{"temperature":
|
|
|
34
34
|
|
|
35
35
|
### Bytes type
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
A `bytes` object can only be constructed by calling a function that returns bytes (e.g. `base64.decode`). The only things that can be done with a `bytes` object is to assign it to variable and to pass the `bytes` object to one of the functions that take `bytes` type as input variable (e.g. `base64.encode`).
|
|
38
38
|
|
|
39
39
|
### null type
|
|
40
40
|
|
|
41
|
-
In addition to the literal `null`, the Typescript `undefined` value is also
|
|
41
|
+
In addition to the literal `null`, the Typescript `undefined` value is also translated to `null` in Workflows YAML.
|
|
42
|
+
|
|
43
|
+
Note that on Typescript-level typechecking `null` and `undefined` are considered distinct types.
|
|
42
44
|
|
|
43
45
|
### Implicit type conversions
|
|
44
46
|
|
|
@@ -79,6 +81,7 @@ sys.get_env('GOOGLE_CLOUD_PROJECT_ID')
|
|
|
79
81
|
| ?? | nullish coalescing |
|
|
80
82
|
| ?. | optional chaining |
|
|
81
83
|
| ? : | conditional operator |
|
|
84
|
+
| typeof | return the type of the operand as a string |
|
|
82
85
|
|
|
83
86
|
The [precendence order of operators](https://cloud.google.com/workflows/docs/reference/syntax/datatypes#order-operations) is the same as in GCP Workflows.
|
|
84
87
|
|
|
@@ -98,7 +101,7 @@ is converted to an [if() expression](https://cloud.google.com/workflows/docs/ref
|
|
|
98
101
|
${if(x > 0, "positive", "not positive")}
|
|
99
102
|
```
|
|
100
103
|
|
|
101
|
-
⚠️
|
|
104
|
+
⚠️ Workflows always evaluates both expression branches unlike Typescript which evaluates only the branch that gets executed.
|
|
102
105
|
|
|
103
106
|
### Nullish coalescing operator
|
|
104
107
|
|
|
@@ -114,7 +117,7 @@ is converted to a [default() expression](https://cloud.google.com/workflows/docs
|
|
|
114
117
|
${default(x, "default value")}
|
|
115
118
|
```
|
|
116
119
|
|
|
117
|
-
⚠️
|
|
120
|
+
⚠️ Workflows always evaluates the right-hand side expression unlike Typescript which evaluates the right-hand side only if the left-hand side is `null` or `undefined`.
|
|
118
121
|
|
|
119
122
|
### Optional chaining
|
|
120
123
|
|
|
@@ -130,11 +133,28 @@ is converted to a [map.get() expression](https://cloud.google.com/workflows/docs
|
|
|
130
133
|
${map.get(data, ["user", "name"])}
|
|
131
134
|
```
|
|
132
135
|
|
|
136
|
+
### typeof operator
|
|
137
|
+
|
|
138
|
+
Returns the type of the operand as a string. The return values are the same ones that Javascript typeof operation returns. The following table shows the possible values for different operand types.
|
|
139
|
+
|
|
140
|
+
| Operand | Result |
|
|
141
|
+
| ------- | --------- |
|
|
142
|
+
| boolean | "boolean" |
|
|
143
|
+
| bytes | "object" |
|
|
144
|
+
| double | "number" |
|
|
145
|
+
| integer | "number" |
|
|
146
|
+
| list | "object" |
|
|
147
|
+
| map | "object" |
|
|
148
|
+
| string | "string" |
|
|
149
|
+
| null | "object" |
|
|
150
|
+
|
|
151
|
+
The typeof operator is useful as a type guard in Typescript (e.g. `typeof x === "string"`). For other use cases, consider the [get_type function](https://cloud.google.com/workflows/docs/reference/stdlib/expression-helpers#type_functions) from the Workflows standard library. It makes finer distinctions between types. It, for example, returns distinct values for lists and maps.
|
|
152
|
+
|
|
133
153
|
## Template literals
|
|
134
154
|
|
|
135
155
|
Template literals are strings that support string interpolation. For example, `Hello ${name}`.
|
|
136
156
|
|
|
137
|
-
⚠️ Interpolated values can (only) be numbers, strings or
|
|
157
|
+
⚠️ Interpolated values can (only) be numbers, strings, booleans or nulls. Other types will throw a TypeError at runtime.
|
|
138
158
|
|
|
139
159
|
## Subworkflow definitions
|
|
140
160
|
|
|
@@ -155,7 +175,7 @@ function anotherWorkflow(): number {
|
|
|
155
175
|
}
|
|
156
176
|
```
|
|
157
177
|
|
|
158
|
-
|
|
178
|
+
Subworkflows can have parameters:
|
|
159
179
|
|
|
160
180
|
```typescript
|
|
161
181
|
function multiply(firstFactor: number, secondFactor: number): number {
|
|
@@ -163,10 +183,18 @@ function multiply(firstFactor: number, secondFactor: number): number {
|
|
|
163
183
|
}
|
|
164
184
|
```
|
|
165
185
|
|
|
166
|
-
|
|
186
|
+
Optional parameters can be specified with a question mark. If a value is not provided on the call site, the value is set to `null` during the subworkflow execution. The following subworkflow can be called as `greet()` or `greet("Tiabeanie")`.
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
function greet(name?: string): string {
|
|
190
|
+
return 'Hello, ${name ?? "world"}'
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Parameters can have default values. The default value is used if a value is not provided in a subworkflow call. The following subworkflow can be called with one parameter (`log(3)`) or with two parameters (`log(3, 2)`).
|
|
167
195
|
|
|
168
196
|
```typescript
|
|
169
|
-
function log(x, base = 10) {
|
|
197
|
+
function log(x: number, base: number = 10) {
|
|
170
198
|
return 'Should compute the logarithm of x'
|
|
171
199
|
}
|
|
172
200
|
```
|
|
@@ -201,7 +229,7 @@ is converted to an [assign step](https://cloud.google.com/workflows/docs/referen
|
|
|
201
229
|
|
|
202
230
|
This syntax can be used to call [standard library functions](https://cloud.google.com/workflows/docs/reference/stdlib/overview), subworkflows or connectors. Note that Javascript runtime functions (such as `fetch()`, `console.error()` or `new XMLHttpRequest()`) are not available on Workflows.
|
|
203
231
|
|
|
204
|
-
GCP Workflows language has two ways of calling functions and subworkflows: as expression in an [assign step](https://cloud.google.com/workflows/docs/reference/syntax/variables#assign-step) or as [call step](https://cloud.google.com/workflows/docs/reference/syntax/calls). They can mostly be used interchangeably. However, [blocking calls](https://cloud.google.com/workflows/docs/reference/syntax/expressions#blocking-calls) must be made as call steps. The transpiler tries to automatically output a call step when necessary.
|
|
232
|
+
GCP Workflows language has two ways of calling functions and subworkflows: as expression in an [assign step](https://cloud.google.com/workflows/docs/reference/syntax/variables#assign-step) or as [call step](https://cloud.google.com/workflows/docs/reference/syntax/calls). They can mostly be used interchangeably. However, [blocking calls](https://cloud.google.com/workflows/docs/reference/syntax/expressions#blocking-calls) must be made as call steps. The ts2workflows transpiler tries to automatically output a call step when necessary.
|
|
205
233
|
|
|
206
234
|
It is also possible to force a function to be called as call step. This might be useful, if the transpiler fails to output call step when it should, or if you want to use named parameters. For example, the following Typescript program
|
|
207
235
|
|
|
@@ -738,7 +766,7 @@ is converted to a step with the label `setName`:
|
|
|
738
766
|
|
|
739
767
|
## Type annotations for standard library functions
|
|
740
768
|
|
|
741
|
-
Type annotations for GCP Workflows standard library functions and expression helpers are provided by importing "ts2workflows/types/workflowslib".
|
|
769
|
+
Type annotations for [GCP Workflows standard library functions and expression helpers](https://cloud.google.com/workflows/docs/reference/stdlib/overview) are provided by importing "ts2workflows/types/workflowslib".
|
|
742
770
|
|
|
743
771
|
```typescript
|
|
744
772
|
import { sys } from 'ts2workflows/types/workflowslib'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts2workflows",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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
|
-
"prepare": "husky"
|
|
23
|
+
"prepare": "husky && npm run build"
|
|
24
24
|
},
|
|
25
25
|
"lint-staged": {
|
|
26
26
|
"src/**/*.ts": [
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"eslint": "^9.10.0",
|
|
67
67
|
"husky": "^9.1.6",
|
|
68
68
|
"lint-staged": "^15.2.10",
|
|
69
|
-
"mocha": "^
|
|
69
|
+
"mocha": "^11.1.0",
|
|
70
70
|
"prettier": "^3.2.5",
|
|
71
71
|
"rimraf": "^5.0.10",
|
|
72
72
|
"tsx": "^4.10.2",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
},
|
|
75
75
|
"dependencies": {
|
|
76
76
|
"@typescript-eslint/typescript-estree": "^8.0.0",
|
|
77
|
-
"commander": "^
|
|
77
|
+
"commander": "^13.1.0",
|
|
78
78
|
"typescript": "^5.0.0",
|
|
79
79
|
"yaml": "^2.4.2"
|
|
80
80
|
}
|
package/types/workflowslib.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
export {}
|
|
2
|
+
|
|
1
3
|
// Type annotations for GCP Workflows expression helpers, standard library
|
|
2
4
|
// functions and (some) connectors
|
|
3
5
|
|
|
4
6
|
// An opaque bytes type.
|
|
5
7
|
// This should really be defined in the transpiler, not here.
|
|
6
|
-
declare const
|
|
8
|
+
declare const __bytes_tag: unique symbol
|
|
7
9
|
export interface bytes {
|
|
8
|
-
readonly [
|
|
10
|
+
readonly [__bytes_tag]: 'bytes'
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
// GCP Workflows expression helpers
|
|
@@ -37,17 +39,36 @@ export declare namespace base64 {
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export declare namespace events {
|
|
40
|
-
function await_callback(
|
|
42
|
+
function await_callback<ResponseType = unknown>(
|
|
41
43
|
callback: {
|
|
42
44
|
url: string
|
|
43
45
|
},
|
|
44
46
|
timeout?: number,
|
|
45
|
-
):
|
|
47
|
+
): {
|
|
48
|
+
http_request: {
|
|
49
|
+
body: ResponseType | null
|
|
50
|
+
headers: Record<string, string>
|
|
51
|
+
method: string
|
|
52
|
+
query: Record<string, string>
|
|
53
|
+
url: string
|
|
54
|
+
}
|
|
55
|
+
received_time: string
|
|
56
|
+
type: string
|
|
57
|
+
}
|
|
46
58
|
function create_callback_endpoint(http_callback_method: string): {
|
|
47
59
|
url: string
|
|
48
60
|
}
|
|
49
61
|
}
|
|
50
62
|
|
|
63
|
+
export declare namespace hash {
|
|
64
|
+
export function compute_checksum(data: bytes, algorithm: string): bytes
|
|
65
|
+
export function compute_hmac(
|
|
66
|
+
key: bytes,
|
|
67
|
+
data: bytes,
|
|
68
|
+
algorithm: string,
|
|
69
|
+
): bytes
|
|
70
|
+
}
|
|
71
|
+
|
|
51
72
|
export declare namespace http {
|
|
52
73
|
export function default_retry(errormap: Record<string, any>): void
|
|
53
74
|
export function default_retry_non_idempotent(
|
|
@@ -59,7 +80,7 @@ export declare namespace http {
|
|
|
59
80
|
export function default_retry_predicate_non_idempotent(
|
|
60
81
|
errormap: Record<string, any>,
|
|
61
82
|
): boolean
|
|
62
|
-
function _delete(
|
|
83
|
+
function _delete<ResponseType = unknown>(
|
|
63
84
|
url: string,
|
|
64
85
|
timeout?: number,
|
|
65
86
|
body?: unknown,
|
|
@@ -72,11 +93,11 @@ export declare namespace http {
|
|
|
72
93
|
private_service_name?: string,
|
|
73
94
|
ca_certificate?: string,
|
|
74
95
|
): {
|
|
75
|
-
body:
|
|
96
|
+
body: ResponseType
|
|
76
97
|
code: number
|
|
77
98
|
headers: Record<string, string>
|
|
78
99
|
}
|
|
79
|
-
export function get(
|
|
100
|
+
export function get<ResponseType = unknown>(
|
|
80
101
|
url: string,
|
|
81
102
|
timeout?: number,
|
|
82
103
|
headers?: Record<string, string>,
|
|
@@ -88,11 +109,11 @@ export declare namespace http {
|
|
|
88
109
|
private_service_name?: string,
|
|
89
110
|
ca_certificate?: string,
|
|
90
111
|
): {
|
|
91
|
-
body:
|
|
112
|
+
body: ResponseType
|
|
92
113
|
code: number
|
|
93
114
|
headers: Record<string, string>
|
|
94
115
|
}
|
|
95
|
-
export function patch(
|
|
116
|
+
export function patch<ResponseType = unknown>(
|
|
96
117
|
url: string,
|
|
97
118
|
timeout?: number,
|
|
98
119
|
body?: unknown,
|
|
@@ -105,11 +126,11 @@ export declare namespace http {
|
|
|
105
126
|
private_service_name?: string,
|
|
106
127
|
ca_certificate?: string,
|
|
107
128
|
): {
|
|
108
|
-
body:
|
|
129
|
+
body: ResponseType
|
|
109
130
|
code: number
|
|
110
131
|
headers: Record<string, string>
|
|
111
132
|
}
|
|
112
|
-
export function post(
|
|
133
|
+
export function post<ResponseType = unknown>(
|
|
113
134
|
url: string,
|
|
114
135
|
timeout?: number,
|
|
115
136
|
body?: unknown,
|
|
@@ -122,11 +143,11 @@ export declare namespace http {
|
|
|
122
143
|
private_service_name?: string,
|
|
123
144
|
ca_certificate?: string,
|
|
124
145
|
): {
|
|
125
|
-
body:
|
|
146
|
+
body: ResponseType
|
|
126
147
|
code: number
|
|
127
148
|
headers: Record<string, string>
|
|
128
149
|
}
|
|
129
|
-
export function put(
|
|
150
|
+
export function put<ResponseType = unknown>(
|
|
130
151
|
url: string,
|
|
131
152
|
timeout?: number,
|
|
132
153
|
body?: unknown,
|
|
@@ -139,11 +160,11 @@ export declare namespace http {
|
|
|
139
160
|
private_service_name?: string,
|
|
140
161
|
ca_certificate?: string,
|
|
141
162
|
): {
|
|
142
|
-
body:
|
|
163
|
+
body: ResponseType
|
|
143
164
|
code: number
|
|
144
165
|
headers: Record<string, string>
|
|
145
166
|
}
|
|
146
|
-
export function request(
|
|
167
|
+
export function request<ResponseType = unknown>(
|
|
147
168
|
method: string,
|
|
148
169
|
url: string,
|
|
149
170
|
timeout?: number,
|
|
@@ -157,7 +178,7 @@ export declare namespace http {
|
|
|
157
178
|
private_service_name?: string,
|
|
158
179
|
ca_certificate?: string,
|
|
159
180
|
): {
|
|
160
|
-
body:
|
|
181
|
+
body: ResponseType
|
|
161
182
|
code: number
|
|
162
183
|
headers: Record<string, string>
|
|
163
184
|
}
|
|
@@ -173,7 +194,8 @@ export declare namespace json {
|
|
|
173
194
|
| boolean
|
|
174
195
|
| unknown[]
|
|
175
196
|
| Record<string, unknown>
|
|
176
|
-
| null
|
|
197
|
+
| null
|
|
198
|
+
| undefined,
|
|
177
199
|
indent?:
|
|
178
200
|
| boolean
|
|
179
201
|
| {
|
|
@@ -188,7 +210,8 @@ export declare namespace json {
|
|
|
188
210
|
| boolean
|
|
189
211
|
| unknown[]
|
|
190
212
|
| Record<string, unknown>
|
|
191
|
-
| null
|
|
213
|
+
| null
|
|
214
|
+
| undefined,
|
|
192
215
|
indent?:
|
|
193
216
|
| boolean
|
|
194
217
|
| {
|
|
@@ -237,7 +260,8 @@ export declare namespace retry {
|
|
|
237
260
|
}
|
|
238
261
|
|
|
239
262
|
export declare namespace sys {
|
|
240
|
-
function get_env(name: string
|
|
263
|
+
function get_env(name: string): string | null
|
|
264
|
+
function get_env(name: string, default_value: string): string
|
|
241
265
|
function log(
|
|
242
266
|
data?: number | boolean | string | unknown[] | object,
|
|
243
267
|
severity?: string,
|
|
@@ -716,7 +740,7 @@ export declare function retry_policy(
|
|
|
716
740
|
| ((errormap: Record<string, any>) => void)
|
|
717
741
|
| {
|
|
718
742
|
predicate?: (errormap: Record<string, any>) => boolean
|
|
719
|
-
max_retries
|
|
743
|
+
max_retries?: number | string | null
|
|
720
744
|
backoff: {
|
|
721
745
|
initial_delay?: number | string | null
|
|
722
746
|
max_delay?: number | string | null
|