@0xsequence/catapult 1.4.0 → 1.5.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 +27 -0
- package/dist/lib/__tests__/network-loader.spec.js.map +1 -1
- package/dist/lib/core/__tests__/resolver.spec.js +22 -0
- package/dist/lib/core/__tests__/resolver.spec.js.map +1 -1
- package/dist/lib/core/__tests__/sign-actions.spec.d.ts +2 -0
- package/dist/lib/core/__tests__/sign-actions.spec.d.ts.map +1 -0
- package/dist/lib/core/__tests__/sign-actions.spec.js +128 -0
- package/dist/lib/core/__tests__/sign-actions.spec.js.map +1 -0
- package/dist/lib/core/__tests__/signer.spec.d.ts +2 -0
- package/dist/lib/core/__tests__/signer.spec.d.ts.map +1 -0
- package/dist/lib/core/__tests__/signer.spec.js +40 -0
- package/dist/lib/core/__tests__/signer.spec.js.map +1 -0
- package/dist/lib/core/context.d.ts +3 -2
- package/dist/lib/core/context.d.ts.map +1 -1
- package/dist/lib/core/context.js +3 -2
- package/dist/lib/core/context.js.map +1 -1
- package/dist/lib/core/engine.d.ts +4 -0
- package/dist/lib/core/engine.d.ts.map +1 -1
- package/dist/lib/core/engine.js +173 -0
- package/dist/lib/core/engine.js.map +1 -1
- package/dist/lib/core/signer.d.ts +7 -0
- package/dist/lib/core/signer.d.ts.map +1 -0
- package/dist/lib/core/signer.js +60 -0
- package/dist/lib/core/signer.js.map +1 -0
- package/dist/lib/parsers/__tests__/source.spec.js +37 -0
- package/dist/lib/parsers/__tests__/source.spec.js.map +1 -1
- package/dist/lib/parsers/source.js +1 -1
- package/dist/lib/parsers/source.js.map +1 -1
- package/dist/lib/provenance.js +51 -2
- package/dist/lib/provenance.js.map +1 -1
- package/dist/lib/types/actions.d.ts +26 -2
- package/dist/lib/types/actions.d.ts.map +1 -1
- package/dist/lib/types/actions.js +3 -0
- package/dist/lib/types/actions.js.map +1 -1
- package/dist/lib/types/source.d.ts +2 -0
- package/dist/lib/types/source.d.ts.map +1 -1
- package/package.json +4 -1
- package/.eslintrc.json +0 -29
- package/.github/workflows/ci.yml +0 -181
- package/CONCEPT.md +0 -24
- package/contracts/checked-call.huff +0 -65
- package/eslint.config.js +0 -48
- package/examples/jobs/guards-v1.yaml +0 -17
- package/examples/jobs/sequence-seq-0001-patch.yaml +0 -59
- package/examples/jobs/sequence-v1.yaml +0 -59
- package/examples/templates/sequence-factory-v1.yaml +0 -56
- package/jest.config.js +0 -25
- package/src/cli.ts +0 -18
- package/src/commands/common.ts +0 -61
- package/src/commands/dry.ts +0 -209
- package/src/commands/etherscan.ts +0 -360
- package/src/commands/index.ts +0 -6
- package/src/commands/list.ts +0 -262
- package/src/commands/provenance.ts +0 -120
- package/src/commands/run.ts +0 -146
- package/src/commands/utils.ts +0 -215
- package/src/index.ts +0 -67
- package/src/lib/__tests__/deployer-events.spec.ts +0 -338
- package/src/lib/__tests__/deployer.spec.ts +0 -2269
- package/src/lib/__tests__/network-loader.spec.ts +0 -150
- package/src/lib/__tests__/network-selection.spec.ts +0 -41
- package/src/lib/__tests__/network-utils.spec.ts +0 -230
- package/src/lib/__tests__/provenance.spec.ts +0 -208
- package/src/lib/artifacts/__tests__/fixtures/contract1.json +0 -19
- package/src/lib/artifacts/__tests__/fixtures/contract2.json +0 -19
- package/src/lib/artifacts/__tests__/fixtures/duplicate-name.json +0 -19
- package/src/lib/artifacts/__tests__/fixtures/nested/nested-contract.json +0 -18
- package/src/lib/artifacts/__tests__/fixtures/not-an-artifact.json +0 -8
- package/src/lib/artifacts/__tests__/fixtures/readme.txt +0 -2
- package/src/lib/contracts/__tests__/repository.spec.ts +0 -612
- package/src/lib/contracts/repository.ts +0 -411
- package/src/lib/core/__tests__/assert-action.spec.ts +0 -474
- package/src/lib/core/__tests__/context.spec.ts +0 -37
- package/src/lib/core/__tests__/engine.spec.ts +0 -2005
- package/src/lib/core/__tests__/graph.spec.ts +0 -125
- package/src/lib/core/__tests__/json-integration.spec.ts +0 -425
- package/src/lib/core/__tests__/loader.spec.ts +0 -367
- package/src/lib/core/__tests__/multi-platform-verification.spec.ts +0 -406
- package/src/lib/core/__tests__/resolver.spec.ts +0 -2496
- package/src/lib/core/__tests__/static-action.spec.ts +0 -172
- package/src/lib/core/context.ts +0 -127
- package/src/lib/core/engine.ts +0 -1834
- package/src/lib/core/graph.ts +0 -252
- package/src/lib/core/loader.ts +0 -253
- package/src/lib/core/resolver.ts +0 -873
- package/src/lib/deployer.ts +0 -1005
- package/src/lib/events/__tests__/event-system.spec.ts +0 -392
- package/src/lib/events/cli-adapter.ts +0 -369
- package/src/lib/events/emitter.ts +0 -62
- package/src/lib/events/index.ts +0 -3
- package/src/lib/events/types.ts +0 -520
- package/src/lib/index.ts +0 -17
- package/src/lib/network-loader.ts +0 -90
- package/src/lib/network-selection.ts +0 -73
- package/src/lib/network-utils.ts +0 -64
- package/src/lib/parsers/__tests__/buildinfo.spec.ts +0 -122
- package/src/lib/parsers/__tests__/fixtures/buildinfo/invalid-bytecode-buildinfo.json +0 -62
- package/src/lib/parsers/__tests__/fixtures/buildinfo/invalid-json.txt +0 -2
- package/src/lib/parsers/__tests__/fixtures/buildinfo/multi-contract-buildinfo.json +0 -89
- package/src/lib/parsers/__tests__/fixtures/buildinfo/no-contracts-buildinfo.json +0 -17
- package/src/lib/parsers/__tests__/fixtures/buildinfo/simple-buildinfo.json +0 -63
- package/src/lib/parsers/__tests__/fixtures/buildinfo/wrong-format.json +0 -4
- package/src/lib/parsers/__tests__/job.spec.ts +0 -439
- package/src/lib/parsers/__tests__/source.spec.ts +0 -134
- package/src/lib/parsers/__tests__/template.spec.ts +0 -111
- package/src/lib/parsers/artifact/__tests__/artifact.spec.ts +0 -117
- package/src/lib/parsers/artifact/__tests__/fixtures/empty-bytecode.json +0 -5
- package/src/lib/parsers/artifact/__tests__/fixtures/hardhat-artifact.json +0 -67
- package/src/lib/parsers/artifact/__tests__/fixtures/invalid-bytecode.json +0 -5
- package/src/lib/parsers/artifact/__tests__/fixtures/invalid-json.txt +0 -11
- package/src/lib/parsers/artifact/__tests__/fixtures/minimal-artifact.json +0 -5
- package/src/lib/parsers/artifact/__tests__/fixtures/missing-abi.json +0 -4
- package/src/lib/parsers/artifact/__tests__/fixtures/missing-bytecode.json +0 -11
- package/src/lib/parsers/artifact/__tests__/fixtures/missing-contract-name.json +0 -11
- package/src/lib/parsers/artifact/__tests__/fixtures/simple-artifact.json +0 -40
- package/src/lib/parsers/artifact/__tests__/fixtures/wrong-types.json +0 -7
- package/src/lib/parsers/artifact/foundry-1.2.ts +0 -72
- package/src/lib/parsers/artifact/index.ts +0 -27
- package/src/lib/parsers/artifact/types.ts +0 -9
- package/src/lib/parsers/buildinfo.ts +0 -127
- package/src/lib/parsers/constants.ts +0 -56
- package/src/lib/parsers/index.ts +0 -6
- package/src/lib/parsers/job.ts +0 -160
- package/src/lib/parsers/source.ts +0 -129
- package/src/lib/parsers/template.ts +0 -135
- package/src/lib/provenance.ts +0 -785
- package/src/lib/std/templates/arachnid-deterministic-deployment-proxy.yaml +0 -68
- package/src/lib/std/templates/assured-deployment.yaml +0 -46
- package/src/lib/std/templates/era-evm-predeploy.yaml +0 -35
- package/src/lib/std/templates/erc-2470.yaml +0 -70
- package/src/lib/std/templates/min-balance.yaml +0 -35
- package/src/lib/std/templates/nano-universal-deployer.yaml +0 -61
- package/src/lib/std/templates/raw-erc-2470.yaml +0 -62
- package/src/lib/std/templates/raw-nano-universal-deployer.yaml +0 -54
- package/src/lib/std/templates/raw-sequence-universal-deployer-2.yaml +0 -52
- package/src/lib/std/templates/sequence-universal-deployer-2.yaml +0 -61
- package/src/lib/types/__tests__/json-request-action.spec.ts +0 -243
- package/src/lib/types/__tests__/read-json-value.spec.ts +0 -278
- package/src/lib/types/__tests__/resolve-json-value.spec.ts +0 -769
- package/src/lib/types/actions.ts +0 -148
- package/src/lib/types/artifacts.ts +0 -21
- package/src/lib/types/buildinfo.ts +0 -116
- package/src/lib/types/conditions.ts +0 -50
- package/src/lib/types/contracts.ts +0 -26
- package/src/lib/types/definitions.ts +0 -77
- package/src/lib/types/index.ts +0 -9
- package/src/lib/types/network.ts +0 -33
- package/src/lib/types/project.ts +0 -9
- package/src/lib/types/source.ts +0 -26
- package/src/lib/types/task.ts +0 -9
- package/src/lib/types/values.ts +0 -221
- package/src/lib/utils/assertion.ts +0 -24
- package/src/lib/utils/validation.ts +0 -116
- package/src/lib/validation/contract-references.ts +0 -210
- package/src/lib/validation/index.ts +0 -1
- package/src/lib/verification/__tests__/etherscan.spec.ts +0 -710
- package/src/lib/verification/__tests__/sourcify.spec.ts +0 -288
- package/src/lib/verification/etherscan.ts +0 -547
- package/src/lib/verification/sourcify.ts +0 -248
- package/test_validation/artifacts/TestContract.json +0 -9
- package/test_validation/jobs/test-missing.yaml +0 -16
- package/test_validation/networks.yaml +0 -3
- package/tsconfig.json +0 -36
package/src/lib/core/graph.ts
DELETED
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
// src/lib/core/graph.ts
|
|
2
|
-
import { Job, Template, isJobCompletedCondition } from '../types'
|
|
3
|
-
import { isPrimitiveActionType } from '../types/actions'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Represents the complete dependency graph of all jobs in a project.
|
|
7
|
-
* It is responsible for parsing job and template dependencies, detecting
|
|
8
|
-
* cycles, and providing a valid execution order.
|
|
9
|
-
*/
|
|
10
|
-
export class DependencyGraph {
|
|
11
|
-
private graph: Map<string, Set<string>> = new Map()
|
|
12
|
-
private executionOrder: string[] = []
|
|
13
|
-
private allJobNames: Set<string>
|
|
14
|
-
|
|
15
|
-
constructor(
|
|
16
|
-
private readonly jobs: Map<string, Job>,
|
|
17
|
-
private readonly templates: Map<string, Template>,
|
|
18
|
-
) {
|
|
19
|
-
this.allJobNames = new Set(this.jobs.keys())
|
|
20
|
-
this.build()
|
|
21
|
-
this.checkForCycles()
|
|
22
|
-
this.executionOrder = this.topologicalSort()
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Returns the list of job names in a valid execution order.
|
|
27
|
-
* Jobs with no dependencies come first.
|
|
28
|
-
*/
|
|
29
|
-
public getExecutionOrder(): string[] {
|
|
30
|
-
return this.executionOrder
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Returns the direct and transitive dependencies for a given job.
|
|
35
|
-
*/
|
|
36
|
-
public getDependencies(jobName: string): Set<string> {
|
|
37
|
-
return this.graph.get(jobName) || new Set()
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Populates the dependency graph by analyzing each job.
|
|
42
|
-
*/
|
|
43
|
-
private build(): void {
|
|
44
|
-
for (const jobName of this.allJobNames) {
|
|
45
|
-
this.graph.set(jobName, this.findAllDependencies(jobName))
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Recursively finds all dependencies for a given job, including those
|
|
51
|
-
* from its `depends_on` list and those hidden within its templates' setup blocks.
|
|
52
|
-
*
|
|
53
|
-
* @param jobName The name of the job to analyze.
|
|
54
|
-
* @param visited A set to track visited jobs in the current path to detect cycles.
|
|
55
|
-
* @returns A set of all job names that the given job depends on.
|
|
56
|
-
*/
|
|
57
|
-
private findAllDependencies(jobName: string, visited: Set<string> = new Set()): Set<string> {
|
|
58
|
-
if (visited.has(jobName)) {
|
|
59
|
-
// This path is cyclic. The cycle will be properly reported by `checkForCycles`.
|
|
60
|
-
// Here, we just stop the recursion to prevent an infinite loop.
|
|
61
|
-
return new Set()
|
|
62
|
-
}
|
|
63
|
-
visited.add(jobName)
|
|
64
|
-
|
|
65
|
-
const job = this.jobs.get(jobName)
|
|
66
|
-
if (!job) {
|
|
67
|
-
throw new Error(`Integrity error: Job "${jobName}" not found during graph build.`)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const directDependencies = new Set<string>()
|
|
71
|
-
|
|
72
|
-
// 1. Add dependencies from the job's `depends_on` field.
|
|
73
|
-
job.depends_on?.forEach(dep => directDependencies.add(dep))
|
|
74
|
-
|
|
75
|
-
// 2. Find dependencies within the templates used by the job's actions.
|
|
76
|
-
for (const action of job.actions) {
|
|
77
|
-
// Get the template or type name
|
|
78
|
-
const templateName = action.template || action.type
|
|
79
|
-
if (!templateName) {
|
|
80
|
-
throw new Error(`Invalid configuration: Action in job "${jobName}" has no template or type field.`)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// If the action is a primitive, it cannot have job dependencies. Skip it.
|
|
84
|
-
if (isPrimitiveActionType(templateName)) {
|
|
85
|
-
continue
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const template = this.templates.get(templateName)
|
|
89
|
-
if (!template) {
|
|
90
|
-
throw new Error(`Invalid configuration: Template "${templateName}" used by job "${jobName}" not found.`)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Recursively find dependencies in the template's setup block.
|
|
94
|
-
this.findTemplateSetupDependencies(template).forEach(dep => directDependencies.add(dep))
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// 3. Now, for each direct dependency, find its transitive dependencies.
|
|
98
|
-
const allDependencies = new Set<string>(directDependencies)
|
|
99
|
-
for (const dep of directDependencies) {
|
|
100
|
-
if (!this.allJobNames.has(dep)) {
|
|
101
|
-
throw new Error(`Invalid dependency: Job "${jobName}" depends on "${dep}", which does not exist.`)
|
|
102
|
-
}
|
|
103
|
-
// The `visited` set is passed down to detect cycles across calls.
|
|
104
|
-
const transitiveDeps = this.findAllDependencies(dep, new Set(visited))
|
|
105
|
-
transitiveDeps.forEach(transDep => allDependencies.add(transDep))
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return allDependencies
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Helper to extract job dependencies from a template's setup block.
|
|
113
|
-
*/
|
|
114
|
-
private findTemplateSetupDependencies(template: Template): Set<string> {
|
|
115
|
-
const dependencies = new Set<string>()
|
|
116
|
-
const setup = template.setup
|
|
117
|
-
if (!setup) return dependencies
|
|
118
|
-
|
|
119
|
-
// Case 1: Dependencies from `skip_condition` (e.g., `job-completed`).
|
|
120
|
-
setup.skip_condition?.forEach(condition => {
|
|
121
|
-
if (isJobCompletedCondition(condition)) {
|
|
122
|
-
dependencies.add(condition.arguments.job)
|
|
123
|
-
}
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
// Case 2: Dependencies from setup actions that are themselves templates.
|
|
127
|
-
setup.actions?.forEach(action => {
|
|
128
|
-
// Ignore primitive actions within a setup block.
|
|
129
|
-
if (isPrimitiveActionType(action.type)) {
|
|
130
|
-
return
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// In your YAML, setup blocks sometimes call other templates directly.
|
|
134
|
-
// e.g. `sequence-universal-deployer-2`'s setup calls `nano-universal-deployer`
|
|
135
|
-
// Here, `action.type` IS the template name.
|
|
136
|
-
const actionTemplate = this.templates.get(action.type)
|
|
137
|
-
if (actionTemplate) {
|
|
138
|
-
// This is a nested template call, so we must find its dependencies too.
|
|
139
|
-
this.findTemplateSetupDependencies(actionTemplate).forEach(dep => dependencies.add(dep))
|
|
140
|
-
}
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
return dependencies
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Checks the entire graph for circular dependencies.
|
|
148
|
-
* @throws {Error} if a cycle is detected.
|
|
149
|
-
*/
|
|
150
|
-
private checkForCycles(): void {
|
|
151
|
-
for (const [jobName, dependencies] of this.graph.entries()) {
|
|
152
|
-
if (dependencies.has(jobName)) {
|
|
153
|
-
// To provide a more helpful error, we need to find the actual path.
|
|
154
|
-
const path = this.findPath(jobName, jobName)
|
|
155
|
-
throw new Error(`Circular dependency detected: ${path.join(' -> ')}`)
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Helper to find a dependency path from a start node to an end node.
|
|
162
|
-
* Used for creating helpful error messages for cycles.
|
|
163
|
-
*/
|
|
164
|
-
private findPath(start: string, end: string, visited: Set<string> = new Set()): string[] {
|
|
165
|
-
visited.add(start)
|
|
166
|
-
const job = this.jobs.get(start)
|
|
167
|
-
if (!job) return []
|
|
168
|
-
|
|
169
|
-
const directDependencies = new Set(job.depends_on || [])
|
|
170
|
-
for (const action of job.actions) {
|
|
171
|
-
const templateName = action.template || action.type
|
|
172
|
-
if (!templateName || isPrimitiveActionType(templateName)) {
|
|
173
|
-
continue
|
|
174
|
-
}
|
|
175
|
-
const template = this.templates.get(templateName)!
|
|
176
|
-
this.findTemplateSetupDependencies(template).forEach(dep => directDependencies.add(dep))
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (directDependencies.has(end)) {
|
|
180
|
-
return [start, end]
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
for (const dep of directDependencies) {
|
|
184
|
-
if (!visited.has(dep)) {
|
|
185
|
-
const path = this.findPath(dep, end, visited)
|
|
186
|
-
if (path.length > 0) {
|
|
187
|
-
return [start, ...path]
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return []
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Performs a topological sort on the graph to determine execution order.
|
|
197
|
-
* Uses Kahn's algorithm.
|
|
198
|
-
*/
|
|
199
|
-
private topologicalSort(): string[] {
|
|
200
|
-
const inDegree = new Map<string, number>()
|
|
201
|
-
const sorted: string[] = []
|
|
202
|
-
|
|
203
|
-
// This map stores which jobs depend on a given key.
|
|
204
|
-
// e.g., `adjacency.get('A')` -> `['B', 'C']` means B and C depend on A.
|
|
205
|
-
const adjacency = new Map<string, string[]>()
|
|
206
|
-
|
|
207
|
-
// Initialize in-degrees and adjacency list for all jobs.
|
|
208
|
-
for (const jobName of this.allJobNames) {
|
|
209
|
-
inDegree.set(jobName, 0)
|
|
210
|
-
adjacency.set(jobName, [])
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Build the inverted graph (adjacency list) and calculate initial in-degrees.
|
|
214
|
-
for (const [jobName, dependencies] of this.graph.entries()) {
|
|
215
|
-
inDegree.set(jobName, dependencies.size)
|
|
216
|
-
for (const dep of dependencies) {
|
|
217
|
-
// `jobName` depends on `dep`, so `dep` is a prerequisite for `jobName`.
|
|
218
|
-
adjacency.get(dep)?.push(jobName)
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Initialize queue with nodes having an in-degree of 0.
|
|
223
|
-
const queue: string[] = []
|
|
224
|
-
for (const [jobName, degree] of inDegree.entries()) {
|
|
225
|
-
if (degree === 0) {
|
|
226
|
-
queue.push(jobName)
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Process the queue.
|
|
231
|
-
while (queue.length > 0) {
|
|
232
|
-
const current = queue.shift()!
|
|
233
|
-
sorted.push(current)
|
|
234
|
-
|
|
235
|
-
const dependents = adjacency.get(current) || []
|
|
236
|
-
for (const dependent of dependents) {
|
|
237
|
-
const newDegree = (inDegree.get(dependent) || 1) - 1
|
|
238
|
-
inDegree.set(dependent, newDegree)
|
|
239
|
-
if (newDegree === 0) {
|
|
240
|
-
queue.push(dependent)
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (sorted.length !== this.allJobNames.size) {
|
|
246
|
-
// This should theoretically be caught by `checkForCycles`, but it's good defense.
|
|
247
|
-
throw new Error('Topological sort failed. The graph likely has a cycle.')
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return sorted
|
|
251
|
-
}
|
|
252
|
-
}
|
package/src/lib/core/loader.ts
DELETED
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs/promises'
|
|
2
|
-
import * as path from 'path'
|
|
3
|
-
import { parseJob, parseTemplate } from '../parsers'
|
|
4
|
-
import { Job, Template } from '../types'
|
|
5
|
-
import { ContractRepository } from '../contracts/repository'
|
|
6
|
-
import { parseConstants } from '../parsers/constants'
|
|
7
|
-
|
|
8
|
-
export interface ProjectLoaderOptions {
|
|
9
|
-
loadStdTemplates?: boolean
|
|
10
|
-
loadContracts?: boolean
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export class ProjectLoader {
|
|
14
|
-
public jobs: Map<string, Job> = new Map()
|
|
15
|
-
public templates: Map<string, Template> = new Map()
|
|
16
|
-
public readonly contractRepository: ContractRepository
|
|
17
|
-
|
|
18
|
-
// Top-level constants registry
|
|
19
|
-
public constants: Map<string, any> = new Map()
|
|
20
|
-
// Track source files for constants for duplicate reporting
|
|
21
|
-
private constantSources: Map<string, string> = new Map()
|
|
22
|
-
|
|
23
|
-
constructor(
|
|
24
|
-
private readonly projectRoot: string,
|
|
25
|
-
private readonly options: ProjectLoaderOptions = {}
|
|
26
|
-
) {
|
|
27
|
-
this.contractRepository = new ContractRepository()
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async load() {
|
|
31
|
-
// Load all contracts from the project root first
|
|
32
|
-
if (this.options.loadContracts !== false) {
|
|
33
|
-
await this.contractRepository.loadFrom(this.projectRoot)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Load standard library templates (unless disabled)
|
|
37
|
-
if (this.options.loadStdTemplates !== false) {
|
|
38
|
-
const stdTemplatePath = path.resolve(__dirname, '..', 'std', 'templates')
|
|
39
|
-
if (await this.pathExists(stdTemplatePath)) {
|
|
40
|
-
await this.loadTemplatesFromDir(stdTemplatePath)
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Load user-defined templates
|
|
45
|
-
const userTemplatePath = path.join(this.projectRoot, 'templates')
|
|
46
|
-
if (await this.pathExists(userTemplatePath)) {
|
|
47
|
-
await this.loadTemplatesFromDir(userTemplatePath)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Load jobs
|
|
51
|
-
const jobsPath = path.join(this.projectRoot, 'jobs')
|
|
52
|
-
if (await this.pathExists(jobsPath)) {
|
|
53
|
-
await this.loadJobsFromDir(jobsPath)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Load templates from within job directories
|
|
57
|
-
if (await this.pathExists(jobsPath)) {
|
|
58
|
-
await this.loadTemplatesFromJobDirs(jobsPath)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Load top-level constants from anywhere in project root
|
|
62
|
-
await this.loadConstantsFromDir(this.projectRoot)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
private async loadTemplatesFromDir(dir: string) {
|
|
66
|
-
const templateFiles = await this.findTemplateFiles(dir)
|
|
67
|
-
for (const filePath of templateFiles) {
|
|
68
|
-
try {
|
|
69
|
-
const content = await fs.readFile(filePath, 'utf-8')
|
|
70
|
-
const template = parseTemplate(content)
|
|
71
|
-
template._path = filePath
|
|
72
|
-
this.templates.set(template.name, template)
|
|
73
|
-
} catch (error) {
|
|
74
|
-
// Surface YAML/template parsing errors so malformed templates fail the load
|
|
75
|
-
if (error instanceof Error && (error.message.startsWith('Failed to parse template YAML:') || error.message.startsWith('Invalid template'))) {
|
|
76
|
-
throw new Error(`Template load error in ${filePath}: ${error.message}`)
|
|
77
|
-
}
|
|
78
|
-
// If it's a file system error (e.g., permission), rethrow to make it visible as well
|
|
79
|
-
if (error instanceof Error && (error as any).code) {
|
|
80
|
-
throw new Error(`Failed to read template file ${filePath}: ${(error as any).code} ${error.message}`)
|
|
81
|
-
}
|
|
82
|
-
// Otherwise rethrow to avoid silently ignoring real issues
|
|
83
|
-
throw error
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Recursively finds all template files (.yaml/.yml) in a directory.
|
|
90
|
-
*/
|
|
91
|
-
private async findTemplateFiles(dir: string, ignoreDirs: Set<string> = new Set(['node_modules', 'dist', '.git', '.idea', '.vscode'])): Promise<string[]> {
|
|
92
|
-
let results: string[] = []
|
|
93
|
-
try {
|
|
94
|
-
const list = await fs.readdir(dir, { withFileTypes: true })
|
|
95
|
-
|
|
96
|
-
for (const dirent of list) {
|
|
97
|
-
const fullPath = path.resolve(dir, dirent.name)
|
|
98
|
-
if (dirent.isDirectory()) {
|
|
99
|
-
if (!ignoreDirs.has(dirent.name)) {
|
|
100
|
-
results = results.concat(await this.findTemplateFiles(fullPath, ignoreDirs))
|
|
101
|
-
}
|
|
102
|
-
} else if (dirent.isFile() && (dirent.name.endsWith('.yaml') || dirent.name.endsWith('.yml'))) {
|
|
103
|
-
results.push(fullPath)
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
} catch (err) {
|
|
107
|
-
// Ignore errors from trying to read directories we don't have access to, etc.
|
|
108
|
-
}
|
|
109
|
-
return results
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
private async loadJobsFromDir(dir: string) {
|
|
113
|
-
const jobFiles = await this.findJobFiles(dir)
|
|
114
|
-
for (const filePath of jobFiles) {
|
|
115
|
-
try {
|
|
116
|
-
const content = await fs.readFile(filePath, 'utf-8')
|
|
117
|
-
const raw: any = (() => { try { return require('yaml').parse(content) } catch { return {} } })()
|
|
118
|
-
if (raw && typeof raw === 'object' && raw.type === 'template') {
|
|
119
|
-
// This is actually a template file; load through the template path to avoid misclassification
|
|
120
|
-
const template = parseTemplate(content)
|
|
121
|
-
template._path = filePath
|
|
122
|
-
this.templates.set(template.name, template)
|
|
123
|
-
continue
|
|
124
|
-
}
|
|
125
|
-
if (raw && typeof raw === 'object' && (raw.type === 'source' || raw.type === 'constants')) {
|
|
126
|
-
continue
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const job = parseJob(content)
|
|
130
|
-
job._path = filePath
|
|
131
|
-
this.jobs.set(job.name, job)
|
|
132
|
-
} catch (error) {
|
|
133
|
-
// If job YAML is malformed or invalid, skip this job but continue loading others
|
|
134
|
-
if (error instanceof Error && (error.message.startsWith('Failed to parse job YAML:') || error.message.startsWith('Invalid job'))) {
|
|
135
|
-
console.warn(`Skipping malformed job at ${filePath}: ${error.message}`)
|
|
136
|
-
continue
|
|
137
|
-
}
|
|
138
|
-
// If it's a file system error (e.g., permission), rethrow to make it visible as well
|
|
139
|
-
if (error instanceof Error && (error as any).code) {
|
|
140
|
-
throw new Error(`Failed to read job file ${filePath}: ${(error as any).code} ${error.message}`)
|
|
141
|
-
}
|
|
142
|
-
// For other unexpected errors, rethrow
|
|
143
|
-
throw error
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Recursively finds all job files (.yaml/.yml) in a directory.
|
|
150
|
-
*/
|
|
151
|
-
private async findJobFiles(dir: string, ignoreDirs: Set<string> = new Set(['node_modules', 'dist', '.git', '.idea', '.vscode'])): Promise<string[]> {
|
|
152
|
-
let results: string[] = []
|
|
153
|
-
try {
|
|
154
|
-
const list = await fs.readdir(dir, { withFileTypes: true })
|
|
155
|
-
|
|
156
|
-
for (const dirent of list) {
|
|
157
|
-
const fullPath = path.resolve(dir, dirent.name)
|
|
158
|
-
if (dirent.isDirectory()) {
|
|
159
|
-
if (!ignoreDirs.has(dirent.name)) {
|
|
160
|
-
results = results.concat(await this.findJobFiles(fullPath, ignoreDirs))
|
|
161
|
-
}
|
|
162
|
-
} else if (dirent.isFile() && (dirent.name.endsWith('.yaml') || dirent.name.endsWith('.yml'))) {
|
|
163
|
-
results.push(fullPath)
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
} catch (err) {
|
|
167
|
-
// Ignore errors from trying to read directories we don't have access to, etc.
|
|
168
|
-
}
|
|
169
|
-
return results
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Loads templates from within job directories by scanning for 'templates' subdirectories.
|
|
174
|
-
*/
|
|
175
|
-
private async loadTemplatesFromJobDirs(jobsRootDir: string) {
|
|
176
|
-
await this.findAndLoadTemplatesInJobDirs(jobsRootDir)
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Recursively searches for 'templates' directories within job directories and loads templates from them.
|
|
181
|
-
*/
|
|
182
|
-
private async findAndLoadTemplatesInJobDirs(dir: string, ignoreDirs: Set<string> = new Set(['node_modules', 'dist', '.git', '.idea', '.vscode'])): Promise<void> {
|
|
183
|
-
try {
|
|
184
|
-
const list = await fs.readdir(dir, { withFileTypes: true })
|
|
185
|
-
|
|
186
|
-
for (const dirent of list) {
|
|
187
|
-
const fullPath = path.resolve(dir, dirent.name)
|
|
188
|
-
if (dirent.isDirectory()) {
|
|
189
|
-
if (!ignoreDirs.has(dirent.name)) {
|
|
190
|
-
// If this directory is named 'templates', load templates from it
|
|
191
|
-
if (dirent.name === 'templates') {
|
|
192
|
-
await this.loadTemplatesFromDir(fullPath)
|
|
193
|
-
}
|
|
194
|
-
// Continue recursively searching for more template directories
|
|
195
|
-
await this.findAndLoadTemplatesInJobDirs(fullPath, ignoreDirs)
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
} catch (err) {
|
|
200
|
-
// Ignore errors from trying to read directories we don't have access to, etc.
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Load and merge all top-level constants from any YAML file with type: "constants"
|
|
206
|
-
* located anywhere under the given directory.
|
|
207
|
-
*/
|
|
208
|
-
private async loadConstantsFromDir(dir: string, ignoreDirs: Set<string> = new Set(['node_modules', 'dist', '.git', '.idea', '.vscode'])): Promise<void> {
|
|
209
|
-
try {
|
|
210
|
-
const list = await fs.readdir(dir, { withFileTypes: true })
|
|
211
|
-
for (const dirent of list) {
|
|
212
|
-
const fullPath = path.resolve(dir, dirent.name)
|
|
213
|
-
if (dirent.isDirectory()) {
|
|
214
|
-
if (!ignoreDirs.has(dirent.name)) {
|
|
215
|
-
await this.loadConstantsFromDir(fullPath, ignoreDirs)
|
|
216
|
-
}
|
|
217
|
-
} else if (dirent.isFile() && (dirent.name.endsWith('.yaml') || dirent.name.endsWith('.yml'))) {
|
|
218
|
-
try {
|
|
219
|
-
const content = await fs.readFile(fullPath, 'utf-8')
|
|
220
|
-
const constantsDoc = parseConstants(content)
|
|
221
|
-
if (constantsDoc) {
|
|
222
|
-
for (const [key, value] of Object.entries(constantsDoc.constants)) {
|
|
223
|
-
if (this.constants.has(key)) {
|
|
224
|
-
const prevSource = this.constantSources.get(key)
|
|
225
|
-
throw new Error(`Duplicate constant "${key}" found in ${fullPath}${prevSource ? ` (previously defined in ${prevSource})` : ''}`)
|
|
226
|
-
}
|
|
227
|
-
this.constants.set(key, value)
|
|
228
|
-
this.constantSources.set(key, fullPath)
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
} catch (err) {
|
|
232
|
-
// For constants files, surface parsing errors to fail fast
|
|
233
|
-
if (err instanceof Error && (err.message.startsWith('Failed to parse constants YAML:') || err.message.startsWith('Invalid constants'))) {
|
|
234
|
-
throw new Error(`Constants load error in ${fullPath}: ${err.message}`)
|
|
235
|
-
}
|
|
236
|
-
// Otherwise, ignore if not a constants file
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
} catch (err) {
|
|
241
|
-
// Ignore directory read errors (like permission or non-existent)
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
private async pathExists(p: string): Promise<boolean> {
|
|
246
|
-
try {
|
|
247
|
-
await fs.access(p)
|
|
248
|
-
return true
|
|
249
|
-
} catch {
|
|
250
|
-
return false
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|