-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.ts
146 lines (130 loc) · 4.11 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import {
parse,
TSESTree,
AST_NODE_TYPES,
} from '@typescript-eslint/typescript-estree'
import * as YAML from 'yaml'
import { SubworkflowAST } from '../ast/steps.js'
import { WorkflowSyntaxError } from '../errors.js'
import { WorkflowParameter } from '../ast/workflows.js'
import { generateStepNames } from '../ast/stepnames.js'
import { parseStatement } from './statements.js'
export function transpile(code: string): string {
const parserOptions = {
jsDocParsingMode: 'none' as const,
loc: true,
range: false,
}
const ast = parse(code, parserOptions)
const workflowAst = { subworkflows: ast.body.flatMap(parseTopLevelStatement) }
const workflow = generateStepNames(workflowAst)
return YAML.stringify(workflow.render(), { lineWidth: 100 })
}
function parseTopLevelStatement(
node: TSESTree.ProgramStatement,
): SubworkflowAST[] {
switch (node.type) {
case AST_NODE_TYPES.FunctionDeclaration:
return [parseSubworkflows(node)]
case AST_NODE_TYPES.ImportDeclaration:
if (
node.specifiers.some(
(spec) =>
spec.type === AST_NODE_TYPES.ImportNamespaceSpecifier ||
spec.type === AST_NODE_TYPES.ImportDefaultSpecifier,
)
) {
throw new WorkflowSyntaxError(
'Only named imports are allowed',
node.loc,
)
}
return []
case AST_NODE_TYPES.ExportNamedDeclaration:
// "export" keyword is ignored, but a possible function declaration is transpiled.
if (
node.declaration?.type === AST_NODE_TYPES.FunctionDeclaration &&
node.declaration.id?.type === AST_NODE_TYPES.Identifier
) {
// Why is "as" needed here?
return parseTopLevelStatement(
node.declaration as TSESTree.FunctionDeclarationWithName,
)
} else {
return []
}
case AST_NODE_TYPES.TSInterfaceDeclaration:
case AST_NODE_TYPES.TSTypeAliasDeclaration:
case AST_NODE_TYPES.TSDeclareFunction:
// Ignore "type", "interface" and "declare function" at the top-level
return []
default:
throw new WorkflowSyntaxError(
`Only function definitions, imports and type aliases allowed at the top level, encountered ${node.type}`,
node.loc,
)
}
}
function parseSubworkflows(
node: TSESTree.FunctionDeclarationWithName,
): SubworkflowAST {
const nodeParams = node.params
const workflowParams: WorkflowParameter[] = nodeParams.map((param) => {
switch (param.type) {
case AST_NODE_TYPES.Identifier:
if (param.optional) {
return { name: param.name, default: null }
} else {
return { name: param.name }
}
case AST_NODE_TYPES.AssignmentPattern:
return parseSubworkflowDefaultArgument(param)
default:
throw new WorkflowSyntaxError(
'Function parameter must be an identifier or an assignment',
param.loc,
)
}
})
const steps = parseStatement(node.body, {})
return new SubworkflowAST(node.id.name, steps, workflowParams)
}
function parseSubworkflowDefaultArgument(param: TSESTree.AssignmentPattern) {
if (param.left.type !== AST_NODE_TYPES.Identifier) {
throw new WorkflowSyntaxError(
'The default value must be an identifier',
param.left.loc,
)
}
if (param.left.optional) {
throw new WorkflowSyntaxError(
"Parameter can't have default value and initializer",
param.left.loc,
)
}
const name = param.left.name
let defaultValue: string | number | boolean | null
if (
param.right.type === AST_NODE_TYPES.Identifier &&
param.right.name === 'undefined'
) {
defaultValue = null
} else if (
param.right.type === AST_NODE_TYPES.Literal &&
(typeof param.right.value === 'string' ||
typeof param.right.value === 'number' ||
typeof param.right.value === 'boolean' ||
param.right.value === null)
) {
defaultValue = param.right.value
} else {
throw new WorkflowSyntaxError(
'The default value must be a literal number, string, boolean, null, or undefined',
param.right.loc,
)
}
return {
name,
default: defaultValue,
}
}