@panagos/jojoscript 0.7.1 → 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 +429 -418
- package/bin/jojo.js +430 -170
- package/package.json +50 -59
- package/src/compiler.js +197 -186
- package/src/pipeline-hash.js +27 -0
- package/src/pipeline-ir.js +100 -0
- package/src/pipeline-parser.js +583 -418
- package/src/runtime/checkpoint-store.js +122 -0
- package/src/runtime/checkpoint.js +69 -0
- package/src/runtime/collections.js +438 -327
- package/src/runtime/errors.js +29 -0
- package/src/runtime/execution-context.js +86 -0
- package/src/runtime/index.js +51 -29
- package/src/runtime/inspect.js +62 -0
- package/src/runtime/metrics.js +41 -0
- package/src/runtime/retry.js +67 -0
- package/src/runtime/trace.js +66 -0
package/package.json
CHANGED
|
@@ -1,59 +1,50 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@panagos/jojoscript",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "A tiny JavaScript-compatible language layer with Jojo syntax and pipelines.",
|
|
5
|
-
"license": "Unlicense",
|
|
6
|
-
"type": "module",
|
|
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
|
-
"javascript",
|
|
52
|
-
"language",
|
|
53
|
-
"compiler",
|
|
54
|
-
"pipeline",
|
|
55
|
-
"functional-programming",
|
|
56
|
-
"iterators",
|
|
57
|
-
"lazy-iteration"
|
|
58
|
-
]
|
|
59
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@panagos/jojoscript",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "A tiny JavaScript-compatible language layer with Jojo syntax and pipelines.",
|
|
5
|
+
"license": "Unlicense",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"jojo": "./bin/jojo.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./src/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./runtime": {
|
|
15
|
+
"import": "./src/runtime/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"bin/",
|
|
20
|
+
"src/",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "node --test test/*.test.js",
|
|
29
|
+
"build:example": "npm --prefix example install && npm --prefix example run build",
|
|
30
|
+
"run:example": "npm --prefix example run start"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/panagos/jojoscript.git"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/panagos/jojoscript",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/panagos/jojoscript/issues"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"jojoscript",
|
|
42
|
+
"javascript",
|
|
43
|
+
"language",
|
|
44
|
+
"compiler",
|
|
45
|
+
"pipeline",
|
|
46
|
+
"functional-programming",
|
|
47
|
+
"iterators",
|
|
48
|
+
"lazy-iteration"
|
|
49
|
+
]
|
|
50
|
+
}
|
package/src/compiler.js
CHANGED
|
@@ -1,186 +1,197 @@
|
|
|
1
|
-
import fs from 'node:fs'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import { fileURLToPath } from 'node:url'
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
transformPipelines,
|
|
7
|
-
findPipelineStdlibFunctions
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
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
|
-
result =
|
|
61
|
-
result =
|
|
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
|
-
const
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
transformPipelines,
|
|
7
|
+
findPipelineStdlibFunctions,
|
|
8
|
+
hasPipelines
|
|
9
|
+
} from './pipeline-parser.js'
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
transformMutableDeclarations,
|
|
13
|
+
transformConstDeclarations,
|
|
14
|
+
transformMultiBindingDeclarations,
|
|
15
|
+
transformDestructuringDeclarations,
|
|
16
|
+
transformFunctionDeclarations,
|
|
17
|
+
transformExpressionFunctions,
|
|
18
|
+
transformLocalImports
|
|
19
|
+
} from './transforms.js'
|
|
20
|
+
|
|
21
|
+
import { transformMatchExpressions } from './match-parser.js'
|
|
22
|
+
import { transformTypeAnnotations } from './type-annotations.js'
|
|
23
|
+
|
|
24
|
+
/*
|
|
25
|
+
* The auto-generated runtime import must use whatever name this package is
|
|
26
|
+
* actually installed under (e.g. a scoped fork like "@me/jojoscript"), not a
|
|
27
|
+
* hardcoded literal, so it's derived from this package's own package.json
|
|
28
|
+
* instead of being hardcoded.
|
|
29
|
+
*/
|
|
30
|
+
const PACKAGE_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
31
|
+
const OWN_PACKAGE_NAME = JSON.parse(
|
|
32
|
+
fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8')
|
|
33
|
+
).name
|
|
34
|
+
const JOJO_RUNTIME = `${OWN_PACKAGE_NAME}/runtime`
|
|
35
|
+
|
|
36
|
+
export function compile(source, options = {}) {
|
|
37
|
+
let result = source
|
|
38
|
+
const runtimeImport = options.runtimeImport ?? JOJO_RUNTIME
|
|
39
|
+
|
|
40
|
+
/*
|
|
41
|
+
* Optional type annotations, stripped and re-expressed as JSDoc before
|
|
42
|
+
* anything else touches `fn` declarations.
|
|
43
|
+
*/
|
|
44
|
+
result = transformTypeAnnotations(result)
|
|
45
|
+
|
|
46
|
+
/*
|
|
47
|
+
* Jojo functions.
|
|
48
|
+
*/
|
|
49
|
+
result = transformExpressionFunctions(result)
|
|
50
|
+
result = transformFunctionDeclarations(result)
|
|
51
|
+
|
|
52
|
+
/*
|
|
53
|
+
* `match` expressions.
|
|
54
|
+
*/
|
|
55
|
+
result = transformMatchExpressions(result)
|
|
56
|
+
|
|
57
|
+
/*
|
|
58
|
+
* Jojo declarations.
|
|
59
|
+
*/
|
|
60
|
+
result = transformMultiBindingDeclarations(result)
|
|
61
|
+
result = transformDestructuringDeclarations(result)
|
|
62
|
+
result = transformMutableDeclarations(result)
|
|
63
|
+
result = transformConstDeclarations(result)
|
|
64
|
+
|
|
65
|
+
/*
|
|
66
|
+
* Remember which pipeline stages need the Jojo runtime before
|
|
67
|
+
* the pipeline syntax itself is removed.
|
|
68
|
+
*/
|
|
69
|
+
const pipelineStdlibFunctions =
|
|
70
|
+
findPipelineStdlibFunctions(result)
|
|
71
|
+
|
|
72
|
+
/*
|
|
73
|
+
* `jojo profile` compiles with `{ profile: true }`, which wraps every
|
|
74
|
+
* pipeline stage (and the initial source expression) in a call to the
|
|
75
|
+
* `traceNode` runtime helper. That helper is never written by the user,
|
|
76
|
+
* so it must be added to the auto-import list explicitly.
|
|
77
|
+
*/
|
|
78
|
+
if (options.profile && hasPipelines(result) && !pipelineStdlibFunctions.includes('traceNode')) {
|
|
79
|
+
pipelineStdlibFunctions.push('traceNode')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/*
|
|
83
|
+
* Pipelines.
|
|
84
|
+
*/
|
|
85
|
+
result = transformPipelines(result, options)
|
|
86
|
+
|
|
87
|
+
/*
|
|
88
|
+
* ES module imports.
|
|
89
|
+
*/
|
|
90
|
+
result = transformLocalImports(result)
|
|
91
|
+
|
|
92
|
+
/*
|
|
93
|
+
* Generated JavaScript imports the runtime as a normal package.
|
|
94
|
+
* This makes compiled applications portable and avoids globals. If the
|
|
95
|
+
* source already has a manual `jojoscript/runtime` import (e.g. for a
|
|
96
|
+
* stdlib function that is only ever called directly, never through a
|
|
97
|
+
* pipeline stage), the auto-imported names are merged into it instead
|
|
98
|
+
* of creating a second, colliding import declaration.
|
|
99
|
+
*/
|
|
100
|
+
if (pipelineStdlibFunctions.length) {
|
|
101
|
+
result = mergeRuntimeImport(result, pipelineStdlibFunctions, runtimeImport)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return result.endsWith('\n') ? result : result + '\n'
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function escapeRegExp(value) {
|
|
108
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function mergeRuntimeImport(source, stdlibFunctions, runtimeImport) {
|
|
112
|
+
const pattern = new RegExp(
|
|
113
|
+
`import\\s*\\{([^}]*)\\}\\s*from\\s*(["'])([^"']+\\/runtime)\\2`
|
|
114
|
+
)
|
|
115
|
+
const existing = source.match(pattern)
|
|
116
|
+
|
|
117
|
+
if (!existing) {
|
|
118
|
+
return `import { ${stdlibFunctions.join(', ')} } from "${runtimeImport}"\n` + source
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const existingNames = existing[1].split(',').map(name => name.trim()).filter(Boolean)
|
|
122
|
+
const merged = [...new Set([...existingNames, ...stdlibFunctions])]
|
|
123
|
+
|
|
124
|
+
return source.replace(
|
|
125
|
+
pattern,
|
|
126
|
+
`import { ${merged.join(', ')} } from "${runtimeImport}"`
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function compileFile(input, outDir = null, options = {}) {
|
|
131
|
+
const src = path.resolve(input)
|
|
132
|
+
const dir = outDir
|
|
133
|
+
? path.resolve(outDir)
|
|
134
|
+
: path.join(path.dirname(src), 'dist')
|
|
135
|
+
|
|
136
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
137
|
+
|
|
138
|
+
const out = path.join(
|
|
139
|
+
dir,
|
|
140
|
+
path.basename(src, path.extname(src)) + '.js'
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
fs.writeFileSync(
|
|
144
|
+
out,
|
|
145
|
+
compile(fs.readFileSync(src, 'utf8'), options),
|
|
146
|
+
'utf8'
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return { input: src, output: out }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function compileDirectory(inputDir, outDir = null, options = {}) {
|
|
153
|
+
const root = path.resolve(inputDir)
|
|
154
|
+
const target = outDir
|
|
155
|
+
? path.resolve(outDir)
|
|
156
|
+
: path.join(root, 'dist')
|
|
157
|
+
|
|
158
|
+
const result = []
|
|
159
|
+
|
|
160
|
+
function walk(dir) {
|
|
161
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
162
|
+
if (
|
|
163
|
+
entry.name === 'node_modules' ||
|
|
164
|
+
entry.name === 'dist' ||
|
|
165
|
+
entry.name.startsWith('.')
|
|
166
|
+
) {
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const full = path.join(dir, entry.name)
|
|
171
|
+
|
|
172
|
+
if (entry.isDirectory()) {
|
|
173
|
+
walk(full)
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!entry.name.endsWith('.jojo')) continue
|
|
178
|
+
|
|
179
|
+
const rel =
|
|
180
|
+
path.relative(root, full).slice(0, -'.jojo'.length) + '.js'
|
|
181
|
+
|
|
182
|
+
const out = path.join(target, rel)
|
|
183
|
+
|
|
184
|
+
fs.mkdirSync(path.dirname(out), { recursive: true })
|
|
185
|
+
fs.writeFileSync(
|
|
186
|
+
out,
|
|
187
|
+
compile(fs.readFileSync(full, 'utf8'), options),
|
|
188
|
+
'utf8'
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
result.push({ input: full, output: out })
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
walk(root)
|
|
196
|
+
return result
|
|
197
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* A pipeline's identity must be a deterministic hash of its *normalized*
|
|
5
|
+
* structure (stage names + arguments, whitespace-insensitive) rather than a
|
|
6
|
+
* random id, so the same program hashes to the same pipeline id across
|
|
7
|
+
* separate process runs. This is what lets checkpoint data saved by one run
|
|
8
|
+
* be found again by a later run (`jojo resume`, `jojo replay`).
|
|
9
|
+
*/
|
|
10
|
+
export function computePipelineId(pipeline) {
|
|
11
|
+
const normalized = {
|
|
12
|
+
expression: normalizeText(pipeline.expression),
|
|
13
|
+
stages: pipeline.stages.map(stage => ({
|
|
14
|
+
name: stage.name,
|
|
15
|
+
args: stage.args.map(normalizeText)
|
|
16
|
+
}))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return createHash('sha256')
|
|
20
|
+
.update(JSON.stringify(normalized))
|
|
21
|
+
.digest('hex')
|
|
22
|
+
.slice(0, 16)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeText(text) {
|
|
26
|
+
return text.replace(/\s+/g, ' ').trim()
|
|
27
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { findAllPipelines } from './pipeline-parser.js'
|
|
2
|
+
import { computePipelineId } from './pipeline-hash.js'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Static, execution-free representation of a pipeline: SOURCE, one node per
|
|
6
|
+
* stage, RESULT, chained by `dependencies`. Used by `jojo graph` (pure
|
|
7
|
+
* text/DOT rendering, no execution) and correlated at runtime with
|
|
8
|
+
* `jojo profile`'s collected metrics via the same `node-<n>` id scheme the
|
|
9
|
+
* compiler uses when emitting code (see `pipeline-parser.js`'s
|
|
10
|
+
* `emitPipeline`).
|
|
11
|
+
*/
|
|
12
|
+
export function buildGraph(pipeline, pipelineId = computePipelineId(pipeline)) {
|
|
13
|
+
const nodes = []
|
|
14
|
+
|
|
15
|
+
nodes.push({
|
|
16
|
+
id: 'node-0',
|
|
17
|
+
type: 'source',
|
|
18
|
+
label: pipeline.expression.trim(),
|
|
19
|
+
args: [],
|
|
20
|
+
dependencies: [],
|
|
21
|
+
metadata: {}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
pipeline.stages.forEach((stage, index) => {
|
|
25
|
+
nodes.push({
|
|
26
|
+
id: `node-${index + 1}`,
|
|
27
|
+
type: stage.name,
|
|
28
|
+
label: stage.args.length ? `${stage.name}(${stage.args.join(', ')})` : stage.name,
|
|
29
|
+
args: stage.args,
|
|
30
|
+
dependencies: [`node-${index}`],
|
|
31
|
+
metadata: {}
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const resultId = `node-${pipeline.stages.length + 1}`
|
|
36
|
+
nodes.push({
|
|
37
|
+
id: resultId,
|
|
38
|
+
type: 'result',
|
|
39
|
+
label: 'RESULT',
|
|
40
|
+
args: [],
|
|
41
|
+
dependencies: [`node-${pipeline.stages.length}`],
|
|
42
|
+
metadata: {}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return { id: pipelineId, expression: pipeline.expression.trim(), nodes }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/*
|
|
49
|
+
* Parses every pipeline statement in `source` and returns their graphs,
|
|
50
|
+
* each with a deterministic id derived from its normalized structure.
|
|
51
|
+
*/
|
|
52
|
+
export function analyzePipelines(source) {
|
|
53
|
+
return findAllPipelines(source).map(pipeline => buildGraph(pipeline))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function renderGraphText(graph) {
|
|
57
|
+
const lines = []
|
|
58
|
+
|
|
59
|
+
graph.nodes.forEach((node, index) => {
|
|
60
|
+
let label
|
|
61
|
+
if (node.type === 'source') {
|
|
62
|
+
label = `SOURCE ${node.label}`
|
|
63
|
+
} else if (node.type === 'result') {
|
|
64
|
+
label = 'RESULT'
|
|
65
|
+
} else {
|
|
66
|
+
label = node.args.length
|
|
67
|
+
? `${node.type.toUpperCase()} ${node.args.join(', ')}`
|
|
68
|
+
: node.type.toUpperCase()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
lines.push(label)
|
|
72
|
+
|
|
73
|
+
if (index < graph.nodes.length - 1) {
|
|
74
|
+
lines.push(' │')
|
|
75
|
+
lines.push(' ▼')
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
return lines.join('\n')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function renderGraphDot(graph) {
|
|
83
|
+
const lines = ['digraph pipeline {']
|
|
84
|
+
|
|
85
|
+
for (const node of graph.nodes) {
|
|
86
|
+
const dotLabel = node.args.length
|
|
87
|
+
? `${node.type}\\n${node.args.join(', ').replace(/"/g, '\\"')}`
|
|
88
|
+
: node.type
|
|
89
|
+
lines.push(` "${node.id}" [label="${dotLabel}"];`)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const node of graph.nodes) {
|
|
93
|
+
for (const dependency of node.dependencies) {
|
|
94
|
+
lines.push(` "${dependency}" -> "${node.id}";`)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
lines.push('}')
|
|
99
|
+
return lines.join('\n')
|
|
100
|
+
}
|