@chainlink/cre-sdk 1.6.0-alpha.2 → 1.6.0-alpha.3

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.
Files changed (38) hide show
  1. package/README.md +7 -1
  2. package/bin/cre-compile.ts +34 -17
  3. package/dist/generated/capabilities/blockchain/aptos/v1alpha/client_pb.d.ts +1023 -0
  4. package/dist/generated/capabilities/blockchain/aptos/v1alpha/client_pb.js +290 -0
  5. package/dist/generated/capabilities/blockchain/solana/v1alpha/client_pb.d.ts +2904 -0
  6. package/dist/generated/capabilities/blockchain/solana/v1alpha/client_pb.js +506 -0
  7. package/dist/generated-sdk/capabilities/blockchain/aptos/v1alpha/client_sdk_gen.d.ts +52 -0
  8. package/dist/generated-sdk/capabilities/blockchain/aptos/v1alpha/client_sdk_gen.js +186 -0
  9. package/dist/generated-sdk/capabilities/blockchain/solana/v1alpha/client_sdk_gen.d.ts +92 -0
  10. package/dist/generated-sdk/capabilities/blockchain/solana/v1alpha/client_sdk_gen.js +343 -0
  11. package/dist/sdk/cre/index.d.ts +6 -0
  12. package/dist/sdk/cre/index.js +8 -0
  13. package/dist/sdk/report.js +0 -15
  14. package/dist/sdk/test/generated/capabilities/blockchain/aptos/v1alpha/aptos_mock_gen.d.ts +25 -0
  15. package/dist/sdk/test/generated/capabilities/blockchain/aptos/v1alpha/aptos_mock_gen.js +111 -0
  16. package/dist/sdk/test/generated/capabilities/blockchain/solana/v1alpha/solana_mock_gen.d.ts +33 -0
  17. package/dist/sdk/test/generated/capabilities/blockchain/solana/v1alpha/solana_mock_gen.js +178 -0
  18. package/dist/sdk/test/generated/index.d.ts +2 -0
  19. package/dist/sdk/test/generated/index.js +2 -0
  20. package/package.json +3 -3
  21. package/scripts/run-standard-tests.sh +3 -3
  22. package/scripts/run.ts +6 -1
  23. package/scripts/src/check-determinism.test.ts +64 -0
  24. package/scripts/src/check-determinism.ts +32 -0
  25. package/scripts/src/compile-cli-args.test.ts +32 -0
  26. package/scripts/src/compile-cli-args.ts +35 -0
  27. package/scripts/src/compile-to-js.test.ts +90 -0
  28. package/scripts/src/compile-to-js.ts +53 -7
  29. package/scripts/src/compile-to-wasm.ts +18 -32
  30. package/scripts/src/compile-workflow.ts +55 -27
  31. package/scripts/src/generate-chain-selectors.ts +9 -27
  32. package/scripts/src/generate-sdks.ts +12 -0
  33. package/scripts/src/typecheck-workflow.test.ts +77 -0
  34. package/scripts/src/typecheck-workflow.ts +96 -0
  35. package/scripts/src/validate-shared.ts +400 -0
  36. package/scripts/src/validate-workflow-determinism.test.ts +409 -0
  37. package/scripts/src/validate-workflow-determinism.ts +545 -0
  38. package/scripts/src/validate-workflow-runtime-compat.ts +25 -377
@@ -67,24 +67,16 @@
67
67
  * @see https://docs.chain.link/cre/concepts/typescript-wasm-runtime
68
68
  */
69
69
 
70
- import { existsSync, readFileSync, statSync } from 'node:fs'
71
- import path from 'node:path'
72
70
  import * as ts from 'typescript'
73
-
74
- /**
75
- * A single detected violation: a location in the source code where a
76
- * restricted API is referenced.
77
- */
78
- type Violation = {
79
- /** Absolute path to the file containing the violation. */
80
- filePath: string
81
- /** 1-based line number. */
82
- line: number
83
- /** 1-based column number. */
84
- column: number
85
- /** Human-readable description of the violation. */
86
- message: string
87
- }
71
+ import {
72
+ collectLocalSourceFiles,
73
+ createValidationProgram,
74
+ createViolation,
75
+ formatViolations,
76
+ isDeclarationName,
77
+ toAbsolutePath,
78
+ type Violation,
79
+ } from './validate-shared'
88
80
 
89
81
  /**
90
82
  * Node.js built-in module specifiers that are not available in the QuickJS
@@ -134,9 +126,6 @@ const restrictedGlobalApis = new Set([
134
126
  'setInterval',
135
127
  ])
136
128
 
137
- /** File extensions treated as scannable source code. */
138
- const sourceExtensions = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs']
139
-
140
129
  /**
141
130
  * Error thrown when one or more runtime-incompatible API usages are detected.
142
131
  * The message includes a docs link and a formatted list of every violation
@@ -144,336 +133,18 @@ const sourceExtensions = ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs',
144
133
  */
145
134
  class WorkflowRuntimeCompatibilityError extends Error {
146
135
  constructor(violations: Violation[]) {
147
- const sortedViolations = [...violations].sort((a, b) => {
148
- if (a.filePath !== b.filePath) return a.filePath.localeCompare(b.filePath)
149
- if (a.line !== b.line) return a.line - b.line
150
- return a.column - b.column
151
- })
152
-
153
- const formattedViolations = sortedViolations
154
- .map((violation) => {
155
- const relativePath = path.relative(process.cwd(), violation.filePath)
156
- return `- ${relativePath}:${violation.line}:${violation.column} ${violation.message}`
157
- })
158
- .join('\n')
159
-
160
136
  super(
161
137
  `Unsupported API usage found in workflow source.
162
138
  CRE workflows run on Javy (QuickJS), not full Node.js.
163
139
  Use CRE capabilities instead (for example, HTTPClient instead of fetch/node:http).
164
140
  See https://docs.chain.link/cre/concepts/typescript-wasm-runtime
165
141
 
166
- ${formattedViolations}`,
142
+ ${formatViolations(violations)}`,
167
143
  )
168
144
  this.name = 'WorkflowRuntimeCompatibilityError'
169
145
  }
170
146
  }
171
147
 
172
- /** Resolves a file path to an absolute path using the current working directory. */
173
- const toAbsolutePath = (filePath: string) => path.resolve(filePath)
174
-
175
- const defaultValidationCompilerOptions: ts.CompilerOptions = {
176
- allowJs: true,
177
- checkJs: true,
178
- noEmit: true,
179
- skipLibCheck: true,
180
- target: ts.ScriptTarget.ESNext,
181
- module: ts.ModuleKind.ESNext,
182
- moduleResolution: ts.ModuleResolutionKind.Bundler,
183
- }
184
-
185
- const formatDiagnostic = (diagnostic: ts.Diagnostic) => {
186
- const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')
187
- if (!diagnostic.file || diagnostic.start == null) {
188
- return message
189
- }
190
-
191
- const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start)
192
- return `${toAbsolutePath(diagnostic.file.fileName)}:${line + 1}:${character + 1} ${message}`
193
- }
194
-
195
- /**
196
- * Loads compiler options from the nearest tsconfig.json so validation runs
197
- * against the same ambient/type environment as the workflow project.
198
- */
199
- const loadClosestTsconfigCompilerOptions = (entryFilePath: string): ts.CompilerOptions | null => {
200
- const configPath = ts.findConfigFile(
201
- path.dirname(entryFilePath),
202
- ts.sys.fileExists,
203
- 'tsconfig.json',
204
- )
205
- if (!configPath) {
206
- return null
207
- }
208
-
209
- let unrecoverableDiagnostic: ts.Diagnostic | null = null
210
- const parsed = ts.getParsedCommandLineOfConfigFile(
211
- configPath,
212
- {},
213
- {
214
- ...ts.sys,
215
- onUnRecoverableConfigFileDiagnostic: (diagnostic) => {
216
- unrecoverableDiagnostic = diagnostic
217
- },
218
- },
219
- )
220
-
221
- if (!parsed) {
222
- if (unrecoverableDiagnostic) {
223
- throw new Error(
224
- `Failed to parse TypeScript config for workflow validation.\n${formatDiagnostic(unrecoverableDiagnostic)}`,
225
- )
226
- }
227
- return null
228
- }
229
-
230
- return parsed.options
231
- }
232
-
233
- /**
234
- * Maps a file extension to the appropriate TypeScript {@link ts.ScriptKind}
235
- * so the parser handles JSX, CommonJS, and ESM files correctly.
236
- */
237
- const getScriptKind = (filePath: string): ts.ScriptKind => {
238
- switch (path.extname(filePath).toLowerCase()) {
239
- case '.js':
240
- return ts.ScriptKind.JS
241
- case '.jsx':
242
- return ts.ScriptKind.JSX
243
- case '.mjs':
244
- return ts.ScriptKind.JS
245
- case '.cjs':
246
- return ts.ScriptKind.JS
247
- case '.tsx':
248
- return ts.ScriptKind.TSX
249
- case '.mts':
250
- return ts.ScriptKind.TS
251
- case '.cts':
252
- return ts.ScriptKind.TS
253
- default:
254
- return ts.ScriptKind.TS
255
- }
256
- }
257
-
258
- /**
259
- * Creates a {@link Violation} with 1-based line and column numbers derived
260
- * from a character position in the source file.
261
- */
262
- const createViolation = (
263
- filePath: string,
264
- pos: number,
265
- sourceFile: ts.SourceFile,
266
- message: string,
267
- ): Violation => {
268
- const { line, character } = sourceFile.getLineAndCharacterOfPosition(pos)
269
- return {
270
- filePath: toAbsolutePath(filePath),
271
- line: line + 1,
272
- column: character + 1,
273
- message,
274
- }
275
- }
276
-
277
- /** Returns `true` if the specifier looks like a relative or absolute file path. */
278
- const isRelativeImport = (specifier: string) => {
279
- return specifier.startsWith('./') || specifier.startsWith('../') || specifier.startsWith('/')
280
- }
281
-
282
- /**
283
- * Attempts to resolve a relative import specifier to an absolute file path.
284
- * Tries the path as-is first, then appends each known source extension, then
285
- * looks for an index file inside the directory. Returns `null` if nothing is
286
- * found on disk.
287
- */
288
- const resolveRelativeImport = (fromFilePath: string, specifier: string): string | null => {
289
- const basePath = specifier.startsWith('/')
290
- ? path.resolve(specifier)
291
- : path.resolve(path.dirname(fromFilePath), specifier)
292
-
293
- if (existsSync(basePath) && statSync(basePath).isFile()) {
294
- return toAbsolutePath(basePath)
295
- }
296
-
297
- for (const extension of sourceExtensions) {
298
- const withExtension = `${basePath}${extension}`
299
- if (existsSync(withExtension)) {
300
- return toAbsolutePath(withExtension)
301
- }
302
- }
303
-
304
- for (const extension of sourceExtensions) {
305
- const asIndex = path.join(basePath, `index${extension}`)
306
- if (existsSync(asIndex)) {
307
- return toAbsolutePath(asIndex)
308
- }
309
- }
310
-
311
- return null
312
- }
313
-
314
- /**
315
- * Extracts a string literal from the first argument of a call expression.
316
- * Used for `require('node:fs')` and `import('node:fs')` patterns.
317
- * Returns `null` if the first argument is not a static string literal.
318
- */
319
- const getStringLiteralFromCall = (node: ts.CallExpression): string | null => {
320
- const [firstArg] = node.arguments
321
- if (!firstArg || !ts.isStringLiteral(firstArg)) return null
322
- return firstArg.text
323
- }
324
-
325
- /**
326
- * **Pass 1 — Module import analysis.**
327
- *
328
- * Walks the AST of a single source file and:
329
- * - Flags any import/export/require/dynamic-import of a restricted module.
330
- * - Enqueues relative imports for recursive scanning so the validator
331
- * transitively covers the entire local dependency graph.
332
- *
333
- * Handles all module import syntaxes:
334
- * - `import ... from 'node:fs'`
335
- * - `export ... from 'node:fs'`
336
- * - `import fs = require('node:fs')`
337
- * - `require('node:fs')`
338
- * - `import('node:fs')`
339
- */
340
- const collectModuleUsage = (
341
- sourceFile: ts.SourceFile,
342
- filePath: string,
343
- violations: Violation[],
344
- enqueueFile: (nextFile: string) => void,
345
- ) => {
346
- const checkModuleSpecifier = (specifier: string, pos: number) => {
347
- if (restrictedModuleSpecifiers.has(specifier)) {
348
- violations.push(
349
- createViolation(
350
- filePath,
351
- pos,
352
- sourceFile,
353
- `'${specifier}' is not available in CRE workflow runtime.`,
354
- ),
355
- )
356
- }
357
-
358
- if (!isRelativeImport(specifier)) return
359
- const resolved = resolveRelativeImport(filePath, specifier)
360
- if (resolved) {
361
- enqueueFile(resolved)
362
- }
363
- }
364
-
365
- const visit = (node: ts.Node) => {
366
- // import ... from 'specifier'
367
- if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
368
- checkModuleSpecifier(node.moduleSpecifier.text, node.moduleSpecifier.getStart(sourceFile))
369
- }
370
-
371
- // export ... from 'specifier'
372
- if (
373
- ts.isExportDeclaration(node) &&
374
- node.moduleSpecifier &&
375
- ts.isStringLiteral(node.moduleSpecifier)
376
- ) {
377
- checkModuleSpecifier(node.moduleSpecifier.text, node.moduleSpecifier.getStart(sourceFile))
378
- }
379
-
380
- // import fs = require('specifier')
381
- if (
382
- ts.isImportEqualsDeclaration(node) &&
383
- ts.isExternalModuleReference(node.moduleReference) &&
384
- node.moduleReference.expression &&
385
- ts.isStringLiteral(node.moduleReference.expression)
386
- ) {
387
- checkModuleSpecifier(
388
- node.moduleReference.expression.text,
389
- node.moduleReference.expression.getStart(sourceFile),
390
- )
391
- }
392
-
393
- if (ts.isCallExpression(node)) {
394
- // require('specifier')
395
- if (ts.isIdentifier(node.expression) && node.expression.text === 'require') {
396
- const requiredModule = getStringLiteralFromCall(node)
397
- if (requiredModule) {
398
- checkModuleSpecifier(requiredModule, node.arguments[0].getStart(sourceFile))
399
- }
400
- }
401
-
402
- // import('specifier')
403
- if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
404
- const importedModule = getStringLiteralFromCall(node)
405
- if (importedModule) {
406
- checkModuleSpecifier(importedModule, node.arguments[0].getStart(sourceFile))
407
- }
408
- }
409
- }
410
-
411
- ts.forEachChild(node, visit)
412
- }
413
-
414
- visit(sourceFile)
415
- }
416
-
417
- /**
418
- * Checks whether an identifier AST node is the **name being declared** (as
419
- * opposed to a reference/usage). For example, in `const fetch = ...` the
420
- * `fetch` token is a declaration name, while in `fetch(url)` it is a usage.
421
- *
422
- * This distinction is critical so that user-defined variables that shadow
423
- * restricted global names are not flagged as violations.
424
- */
425
- const isDeclarationName = (identifier: ts.Identifier): boolean => {
426
- const parent = identifier.parent
427
-
428
- // Variable, function, class, interface, type alias, enum, module,
429
- // type parameter, parameter, binding element, import names, enum member,
430
- // property/method declarations, property assignments, and labels.
431
- if (
432
- (ts.isFunctionDeclaration(parent) && parent.name === identifier) ||
433
- (ts.isFunctionExpression(parent) && parent.name === identifier) ||
434
- (ts.isClassDeclaration(parent) && parent.name === identifier) ||
435
- (ts.isClassExpression(parent) && parent.name === identifier) ||
436
- (ts.isInterfaceDeclaration(parent) && parent.name === identifier) ||
437
- (ts.isTypeAliasDeclaration(parent) && parent.name === identifier) ||
438
- (ts.isEnumDeclaration(parent) && parent.name === identifier) ||
439
- (ts.isModuleDeclaration(parent) && parent.name === identifier) ||
440
- (ts.isTypeParameterDeclaration(parent) && parent.name === identifier) ||
441
- (ts.isVariableDeclaration(parent) && parent.name === identifier) ||
442
- (ts.isParameter(parent) && parent.name === identifier) ||
443
- (ts.isBindingElement(parent) && parent.name === identifier) ||
444
- (ts.isImportClause(parent) && parent.name === identifier) ||
445
- (ts.isImportSpecifier(parent) && parent.name === identifier) ||
446
- (ts.isNamespaceImport(parent) && parent.name === identifier) ||
447
- (ts.isImportEqualsDeclaration(parent) && parent.name === identifier) ||
448
- (ts.isNamespaceExport(parent) && parent.name === identifier) ||
449
- (ts.isEnumMember(parent) && parent.name === identifier) ||
450
- (ts.isPropertyDeclaration(parent) && parent.name === identifier) ||
451
- (ts.isPropertySignature(parent) && parent.name === identifier) ||
452
- (ts.isMethodDeclaration(parent) && parent.name === identifier) ||
453
- (ts.isMethodSignature(parent) && parent.name === identifier) ||
454
- (ts.isGetAccessorDeclaration(parent) && parent.name === identifier) ||
455
- (ts.isSetAccessorDeclaration(parent) && parent.name === identifier) ||
456
- (ts.isPropertyAssignment(parent) && parent.name === identifier) ||
457
- (ts.isShorthandPropertyAssignment(parent) && parent.name === identifier) ||
458
- (ts.isLabeledStatement(parent) && parent.label === identifier)
459
- ) {
460
- return true
461
- }
462
-
463
- // Property access (obj.fetch), qualified names (Ns.fetch), and type
464
- // references (SomeType) — the right-hand identifier is not a standalone
465
- // usage of the global name.
466
- if (
467
- (ts.isPropertyAccessExpression(parent) && parent.name === identifier) ||
468
- (ts.isQualifiedName(parent) && parent.right === identifier) ||
469
- (ts.isTypeReferenceNode(parent) && parent.typeName === identifier)
470
- ) {
471
- return true
472
- }
473
-
474
- return false
475
- }
476
-
477
148
  /**
478
149
  * **Pass 2 — Global API analysis.**
479
150
  *
@@ -577,50 +248,27 @@ const collectGlobalApiUsage = (
577
248
  */
578
249
  export const assertWorkflowRuntimeCompatibility = (entryFilePath: string) => {
579
250
  const rootFile = toAbsolutePath(entryFilePath)
580
- const projectCompilerOptions = loadClosestTsconfigCompilerOptions(rootFile) ?? {}
581
- const filesToScan = [rootFile]
582
- const scannedFiles = new Set<string>()
583
- const localSourceFiles = new Set<string>()
584
251
  const violations: Violation[] = []
585
252
 
586
253
  // Pass 1: Walk the local import graph and collect module-level violations.
587
- while (filesToScan.length > 0) {
588
- const currentFile = filesToScan.pop()
589
- if (!currentFile || scannedFiles.has(currentFile)) continue
590
- scannedFiles.add(currentFile)
591
-
592
- if (!existsSync(currentFile)) continue
593
- localSourceFiles.add(currentFile)
594
-
595
- const fileContents = readFileSync(currentFile, 'utf-8')
596
- const sourceFile = ts.createSourceFile(
597
- currentFile,
598
- fileContents,
599
- ts.ScriptTarget.Latest,
600
- true,
601
- getScriptKind(currentFile),
602
- )
603
-
604
- collectModuleUsage(sourceFile, currentFile, violations, (nextFile) => {
605
- if (!scannedFiles.has(nextFile)) {
606
- filesToScan.push(nextFile)
254
+ const localSourceFiles = collectLocalSourceFiles(
255
+ rootFile,
256
+ (specifier, pos, sourceFile, filePath) => {
257
+ if (restrictedModuleSpecifiers.has(specifier)) {
258
+ violations.push(
259
+ createViolation(
260
+ filePath,
261
+ pos,
262
+ sourceFile,
263
+ `'${specifier}' is not available in CRE workflow runtime.`,
264
+ ),
265
+ )
607
266
  }
608
- })
609
- }
610
-
611
- // Pass 2: Use the type-checker to detect restricted global API usage.
612
- const program = ts.createProgram({
613
- rootNames: [...localSourceFiles],
614
- options: {
615
- ...defaultValidationCompilerOptions,
616
- ...projectCompilerOptions,
617
- allowJs: true,
618
- checkJs: true,
619
- noEmit: true,
620
- skipLibCheck: true,
621
267
  },
622
- })
268
+ )
623
269
 
270
+ // Pass 2: Use the type-checker to detect restricted global API usage.
271
+ const program = createValidationProgram(rootFile, localSourceFiles)
624
272
  collectGlobalApiUsage(program, localSourceFiles, violations)
625
273
 
626
274
  if (violations.length > 0) {