@chainlink/cre-sdk 1.5.0-alpha.2 → 1.5.0-alpha.4

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 (98) hide show
  1. package/README.md +18 -1
  2. package/bin/cre-compile.ts +39 -17
  3. package/dist/generated/capabilities/blockchain/evm/v1alpha/client_pb.js +1 -1
  4. package/dist/generated/chain-selectors/mainnet/evm/ab-mainnet.d.ts +3 -0
  5. package/dist/generated/chain-selectors/mainnet/evm/ab-mainnet.js +12 -0
  6. package/dist/generated/chain-selectors/mainnet/evm/adi-mainnet.d.ts +3 -0
  7. package/dist/generated/chain-selectors/mainnet/evm/adi-mainnet.js +12 -0
  8. package/dist/generated/chain-selectors/mainnet/evm/edge-mainnet.d.ts +3 -0
  9. package/dist/generated/chain-selectors/mainnet/evm/edge-mainnet.js +12 -0
  10. package/dist/generated/chain-selectors/mainnet/evm/everclear-mainnet.d.ts +3 -0
  11. package/dist/generated/chain-selectors/mainnet/evm/everclear-mainnet.js +12 -0
  12. package/dist/generated/chain-selectors/mainnet/evm/gate-chain-mainnet.d.ts +3 -0
  13. package/dist/generated/chain-selectors/mainnet/evm/gate-chain-mainnet.js +12 -0
  14. package/dist/generated/chain-selectors/mainnet/evm/gate-layer-mainnet.d.ts +3 -0
  15. package/dist/generated/chain-selectors/mainnet/evm/gate-layer-mainnet.js +12 -0
  16. package/dist/generated/chain-selectors/mainnet/evm/jovay-mainnet.d.ts +3 -0
  17. package/dist/generated/chain-selectors/mainnet/evm/jovay-mainnet.js +12 -0
  18. package/dist/generated/chain-selectors/mainnet/evm/megaeth-mainnet.d.ts +3 -0
  19. package/dist/generated/chain-selectors/mainnet/evm/megaeth-mainnet.js +12 -0
  20. package/dist/generated/chain-selectors/mainnet/evm/pharos-mainnet.d.ts +3 -0
  21. package/dist/generated/chain-selectors/mainnet/evm/pharos-mainnet.js +12 -0
  22. package/dist/generated/chain-selectors/mainnet/evm/stable-mainnet.d.ts +3 -0
  23. package/dist/generated/chain-selectors/mainnet/evm/stable-mainnet.js +12 -0
  24. package/dist/generated/chain-selectors/mainnet/evm/tempo-mainnet.d.ts +3 -0
  25. package/dist/generated/chain-selectors/mainnet/evm/tempo-mainnet.js +12 -0
  26. package/dist/generated/chain-selectors/testnet/evm/0g-testnet-galileo-1.d.ts +3 -0
  27. package/dist/generated/chain-selectors/testnet/evm/0g-testnet-galileo-1.js +12 -0
  28. package/dist/generated/chain-selectors/testnet/evm/ab-testnet.d.ts +3 -0
  29. package/dist/generated/chain-selectors/testnet/evm/ab-testnet.js +12 -0
  30. package/dist/generated/chain-selectors/testnet/evm/adi-testnet.d.ts +3 -0
  31. package/dist/generated/chain-selectors/testnet/evm/adi-testnet.js +12 -0
  32. package/dist/generated/chain-selectors/testnet/evm/arc-testnet.d.ts +3 -0
  33. package/dist/generated/chain-selectors/testnet/evm/arc-testnet.js +12 -0
  34. package/dist/generated/chain-selectors/testnet/evm/celo-sepolia.d.ts +3 -0
  35. package/dist/generated/chain-selectors/testnet/evm/celo-sepolia.js +12 -0
  36. package/dist/generated/chain-selectors/testnet/evm/dogeos-testnet-chikyu.d.ts +3 -0
  37. package/dist/generated/chain-selectors/testnet/evm/dogeos-testnet-chikyu.js +12 -0
  38. package/dist/generated/chain-selectors/testnet/evm/edge-testnet.d.ts +3 -0
  39. package/dist/generated/chain-selectors/testnet/evm/edge-testnet.js +12 -0
  40. package/dist/generated/chain-selectors/testnet/evm/ethereum-testnet-hoodi-morph.d.ts +3 -0
  41. package/dist/generated/chain-selectors/testnet/evm/ethereum-testnet-hoodi-morph.js +12 -0
  42. package/dist/generated/chain-selectors/testnet/evm/ethereum-testnet-hoodi-taiko-1.d.ts +3 -0
  43. package/dist/generated/chain-selectors/testnet/evm/ethereum-testnet-hoodi-taiko-1.js +12 -0
  44. package/dist/generated/chain-selectors/testnet/evm/ethereum-testnet-hoodi-taiko.d.ts +3 -0
  45. package/dist/generated/chain-selectors/testnet/evm/ethereum-testnet-hoodi-taiko.js +12 -0
  46. package/dist/generated/chain-selectors/testnet/evm/ethereum-testnet-hoodi.d.ts +3 -0
  47. package/dist/generated/chain-selectors/testnet/evm/ethereum-testnet-hoodi.js +12 -0
  48. package/dist/generated/chain-selectors/testnet/evm/ethereum-testnet-sepolia-ronin-1.d.ts +3 -0
  49. package/dist/generated/chain-selectors/testnet/evm/ethereum-testnet-sepolia-ronin-1.js +12 -0
  50. package/dist/generated/chain-selectors/testnet/evm/everclear-testnet-sepolia.d.ts +3 -0
  51. package/dist/generated/chain-selectors/testnet/evm/everclear-testnet-sepolia.js +12 -0
  52. package/dist/generated/chain-selectors/testnet/evm/gate-chain-testnet-meteora.d.ts +3 -0
  53. package/dist/generated/chain-selectors/testnet/evm/gate-chain-testnet-meteora.js +12 -0
  54. package/dist/generated/chain-selectors/testnet/evm/gate-layer-testnet.d.ts +3 -0
  55. package/dist/generated/chain-selectors/testnet/evm/gate-layer-testnet.js +12 -0
  56. package/dist/generated/chain-selectors/testnet/evm/megaeth-testnet-2.d.ts +3 -0
  57. package/dist/generated/chain-selectors/testnet/evm/megaeth-testnet-2.js +12 -0
  58. package/dist/generated/chain-selectors/testnet/evm/pharos-atlantic-testnet.d.ts +3 -0
  59. package/dist/generated/chain-selectors/testnet/evm/pharos-atlantic-testnet.js +12 -0
  60. package/dist/generated/chain-selectors/testnet/evm/robinhood-testnet.d.ts +3 -0
  61. package/dist/generated/chain-selectors/testnet/evm/robinhood-testnet.js +12 -0
  62. package/dist/generated/chain-selectors/testnet/evm/sonic-testnet.d.ts +3 -0
  63. package/dist/generated/chain-selectors/testnet/evm/sonic-testnet.js +12 -0
  64. package/dist/generated/chain-selectors/testnet/evm/stable-testnet.d.ts +3 -0
  65. package/dist/generated/chain-selectors/testnet/evm/stable-testnet.js +12 -0
  66. package/dist/generated/chain-selectors/testnet/evm/tempo-testnet-moderato.d.ts +3 -0
  67. package/dist/generated/chain-selectors/testnet/evm/tempo-testnet-moderato.js +12 -0
  68. package/dist/generated/chain-selectors/testnet/evm/tempo-testnet.d.ts +3 -0
  69. package/dist/generated/chain-selectors/testnet/evm/tempo-testnet.js +12 -0
  70. package/dist/generated/chain-selectors/testnet/evm/xlayer-testnet.d.ts +3 -0
  71. package/dist/generated/chain-selectors/testnet/evm/xlayer-testnet.js +12 -0
  72. package/dist/generated/networks.d.ts +2 -2
  73. package/dist/generated/networks.js +238 -0
  74. package/dist/generated-sdk/capabilities/blockchain/evm/v1alpha/client_sdk_gen.d.ts +24 -0
  75. package/dist/generated-sdk/capabilities/blockchain/evm/v1alpha/client_sdk_gen.js +24 -0
  76. package/dist/index.d.ts +3 -0
  77. package/dist/sdk/types/global.d.ts +146 -2
  78. package/dist/sdk/types/restricted-apis.d.ts +18 -23
  79. package/dist/sdk/types/restricted-node-modules.d.ts +462 -0
  80. package/dist/sdk/utils/capabilities/blockchain/blockchain-helpers.d.ts +40 -3
  81. package/dist/sdk/utils/capabilities/blockchain/blockchain-helpers.js +80 -6
  82. package/dist/sdk/utils/prepare-runtime.d.ts +1 -1
  83. package/dist/sdk/utils/prepare-runtime.js +9 -2
  84. package/dist/sdk/wasm/host-bindings.d.ts +4 -4
  85. package/package.json +5 -5
  86. package/scripts/run.ts +11 -1
  87. package/scripts/src/build-types.ts +31 -1
  88. package/scripts/src/compile-cli-args.test.ts +32 -0
  89. package/scripts/src/compile-cli-args.ts +35 -0
  90. package/scripts/src/compile-to-js.test.ts +90 -0
  91. package/scripts/src/compile-to-js.ts +51 -13
  92. package/scripts/src/compile-to-wasm.ts +35 -34
  93. package/scripts/src/compile-workflow.ts +55 -27
  94. package/scripts/src/generate-chain-selectors.ts +9 -27
  95. package/scripts/src/typecheck-workflow.test.ts +77 -0
  96. package/scripts/src/typecheck-workflow.ts +96 -0
  97. package/scripts/src/validate-workflow-runtime-compat.test.ts +433 -0
  98. package/scripts/src/validate-workflow-runtime-compat.ts +631 -0
@@ -0,0 +1,77 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
3
+ import { tmpdir } from 'node:os'
4
+ import path from 'node:path'
5
+ import { assertWorkflowTypecheck, WorkflowTypecheckError } from './typecheck-workflow'
6
+
7
+ let tempDir: string
8
+
9
+ beforeEach(() => {
10
+ tempDir = mkdtempSync(path.join(tmpdir(), 'cre-typecheck-test-'))
11
+ })
12
+
13
+ afterEach(() => {
14
+ rmSync(tempDir, { recursive: true, force: true })
15
+ })
16
+
17
+ const writeTemp = (filename: string, content: string): string => {
18
+ const filePath = path.join(tempDir, filename)
19
+ mkdirSync(path.dirname(filePath), { recursive: true })
20
+ writeFileSync(filePath, content, 'utf-8')
21
+ return filePath
22
+ }
23
+
24
+ describe('assertWorkflowTypecheck', () => {
25
+ test('passes for valid project using nearby tsconfig', () => {
26
+ writeTemp(
27
+ 'tsconfig.json',
28
+ JSON.stringify(
29
+ {
30
+ compilerOptions: {
31
+ target: 'ES2022',
32
+ module: 'ESNext',
33
+ moduleResolution: 'Bundler',
34
+ skipLibCheck: true,
35
+ },
36
+ include: ['src/**/*.ts'],
37
+ },
38
+ null,
39
+ 2,
40
+ ),
41
+ )
42
+ const entry = writeTemp('src/workflow.ts', 'export const value: number = 42\n')
43
+ expect(() => assertWorkflowTypecheck(entry)).not.toThrow()
44
+ })
45
+
46
+ test('fails when tsconfig cannot be found', () => {
47
+ const entry = writeTemp('src/workflow.ts', 'export const value = 1\n')
48
+ expect(() => assertWorkflowTypecheck(entry)).toThrow(WorkflowTypecheckError)
49
+ expect(() => assertWorkflowTypecheck(entry)).toThrow('Could not find tsconfig.json')
50
+ })
51
+
52
+ test('fails on whole-project type errors outside entry file', () => {
53
+ writeTemp(
54
+ 'tsconfig.json',
55
+ JSON.stringify(
56
+ {
57
+ compilerOptions: {
58
+ target: 'ES2022',
59
+ module: 'ESNext',
60
+ moduleResolution: 'Bundler',
61
+ skipLibCheck: true,
62
+ strict: true,
63
+ },
64
+ include: ['src/**/*.ts'],
65
+ },
66
+ null,
67
+ 2,
68
+ ),
69
+ )
70
+
71
+ const entry = writeTemp('src/workflow.ts', 'export const value: number = 1\n')
72
+ writeTemp('src/unrelated.ts', "export const shouldBeNumber: number = 'not-a-number'\n")
73
+
74
+ expect(() => assertWorkflowTypecheck(entry)).toThrow(WorkflowTypecheckError)
75
+ expect(() => assertWorkflowTypecheck(entry)).toThrow('unrelated.ts')
76
+ })
77
+ })
@@ -0,0 +1,96 @@
1
+ import path from 'node:path'
2
+ import * as ts from 'typescript'
3
+ import { skipTypeChecksFlag } from './compile-cli-args'
4
+
5
+ const toAbsolutePath = (filePath: string) => path.resolve(filePath)
6
+
7
+ const formatDiagnostic = (diagnostic: ts.Diagnostic): string => {
8
+ const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')
9
+ if (!diagnostic.file || diagnostic.start == null) {
10
+ return message
11
+ }
12
+
13
+ const absoluteFilePath = toAbsolutePath(diagnostic.file.fileName)
14
+ const relativeFilePath = path.relative(process.cwd(), absoluteFilePath)
15
+ const displayPath = relativeFilePath || absoluteFilePath
16
+ const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start)
17
+ return `${displayPath}:${line + 1}:${character + 1} ${message}`
18
+ }
19
+
20
+ class WorkflowTypecheckError extends Error {
21
+ constructor(message: string) {
22
+ super(message)
23
+ this.name = 'WorkflowTypecheckError'
24
+ }
25
+ }
26
+
27
+ const findNearestTsconfigPath = (entryFilePath: string): string | null => {
28
+ const configPath = ts.findConfigFile(
29
+ path.dirname(entryFilePath),
30
+ ts.sys.fileExists,
31
+ 'tsconfig.json',
32
+ )
33
+ return configPath ?? null
34
+ }
35
+
36
+ export const assertWorkflowTypecheck = (entryFilePath: string) => {
37
+ const rootFile = toAbsolutePath(entryFilePath)
38
+ const configPath = findNearestTsconfigPath(rootFile)
39
+ if (!configPath) {
40
+ throw new WorkflowTypecheckError(
41
+ `TypeScript typecheck failed before workflow compilation.
42
+ Could not find tsconfig.json near: ${rootFile}
43
+ Create a tsconfig.json in your workflow project, or re-run compile with ${skipTypeChecksFlag}.`,
44
+ )
45
+ }
46
+
47
+ let unrecoverableDiagnostic: ts.Diagnostic | null = null
48
+ const parsedConfig = ts.getParsedCommandLineOfConfigFile(
49
+ configPath,
50
+ {},
51
+ {
52
+ ...ts.sys,
53
+ onUnRecoverableConfigFileDiagnostic: (diagnostic) => {
54
+ unrecoverableDiagnostic = diagnostic
55
+ },
56
+ },
57
+ )
58
+
59
+ if (!parsedConfig) {
60
+ const details = unrecoverableDiagnostic ? formatDiagnostic(unrecoverableDiagnostic) : ''
61
+ throw new WorkflowTypecheckError(
62
+ `TypeScript typecheck failed before workflow compilation.
63
+ Failed to parse tsconfig.json: ${configPath}
64
+ ${details}
65
+ Fix your tsconfig.json, or re-run compile with ${skipTypeChecksFlag}.`,
66
+ )
67
+ }
68
+
69
+ const program = ts.createProgram({
70
+ rootNames: parsedConfig.fileNames,
71
+ options: {
72
+ ...parsedConfig.options,
73
+ noEmit: true,
74
+ },
75
+ projectReferences: parsedConfig.projectReferences,
76
+ })
77
+
78
+ const diagnostics = [...parsedConfig.errors, ...ts.getPreEmitDiagnostics(program)].filter(
79
+ (diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error,
80
+ )
81
+
82
+ if (diagnostics.length > 0) {
83
+ const formatted = diagnostics.map(formatDiagnostic).join('\n')
84
+ const relativeConfigPath = path.relative(process.cwd(), toAbsolutePath(configPath))
85
+ const displayConfigPath = relativeConfigPath || toAbsolutePath(configPath)
86
+ throw new WorkflowTypecheckError(
87
+ `TypeScript typecheck failed before workflow compilation.
88
+ Using tsconfig: ${displayConfigPath}
89
+ Fix TypeScript errors, or re-run compile with ${skipTypeChecksFlag}.
90
+
91
+ ${formatted}`,
92
+ )
93
+ }
94
+ }
95
+
96
+ export { WorkflowTypecheckError }
@@ -0,0 +1,433 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
3
+ import { tmpdir } from 'node:os'
4
+ import path from 'node:path'
5
+ import {
6
+ assertWorkflowRuntimeCompatibility,
7
+ WorkflowRuntimeCompatibilityError,
8
+ } from './validate-workflow-runtime-compat'
9
+
10
+ let tempDir: string
11
+
12
+ beforeEach(() => {
13
+ tempDir = mkdtempSync(path.join(tmpdir(), 'cre-validate-test-'))
14
+ })
15
+
16
+ afterEach(() => {
17
+ rmSync(tempDir, { recursive: true, force: true })
18
+ })
19
+
20
+ /** Write a file in the temp directory and return its absolute path. */
21
+ const writeTemp = (filename: string, content: string): string => {
22
+ const filePath = path.join(tempDir, filename)
23
+ const dir = path.dirname(filePath)
24
+ mkdirSync(dir, { recursive: true })
25
+ writeFileSync(filePath, content, 'utf-8')
26
+ return filePath
27
+ }
28
+
29
+ /** Assert that the validator throws with violations matching the given patterns. */
30
+ const expectViolations = (entryPath: string, expectedPatterns: (string | RegExp)[]) => {
31
+ try {
32
+ assertWorkflowRuntimeCompatibility(entryPath)
33
+ throw new Error('Expected WorkflowRuntimeCompatibilityError but none was thrown')
34
+ } catch (error) {
35
+ expect(error).toBeInstanceOf(WorkflowRuntimeCompatibilityError)
36
+ const message = (error as Error).message
37
+ for (const pattern of expectedPatterns) {
38
+ if (typeof pattern === 'string') {
39
+ expect(message).toContain(pattern)
40
+ } else {
41
+ expect(message).toMatch(pattern)
42
+ }
43
+ }
44
+ }
45
+ }
46
+
47
+ /** Assert that the validator does NOT throw. */
48
+ const expectNoViolations = (entryPath: string) => {
49
+ expect(() => assertWorkflowRuntimeCompatibility(entryPath)).not.toThrow()
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Pass 1: Module import analysis
54
+ // ---------------------------------------------------------------------------
55
+
56
+ describe('module import analysis', () => {
57
+ test("detects import ... from 'node:fs'", () => {
58
+ const entry = writeTemp('workflow.ts', `import { readFileSync } from 'node:fs';\n`)
59
+ expectViolations(entry, ["'node:fs' is not available"])
60
+ })
61
+
62
+ test("detects bare module specifier 'fs' (without node: prefix)", () => {
63
+ const entry = writeTemp('workflow.ts', `import { readFileSync } from 'fs';\n`)
64
+ expectViolations(entry, ["'fs' is not available"])
65
+ })
66
+
67
+ test('detects export ... from restricted module', () => {
68
+ const entry = writeTemp('workflow.ts', `export { createHash } from 'node:crypto';\n`)
69
+ expectViolations(entry, ["'node:crypto' is not available"])
70
+ })
71
+
72
+ test('detects import = require() syntax', () => {
73
+ const entry = writeTemp('workflow.ts', `import fs = require('node:fs');\n`)
74
+ expectViolations(entry, ["'node:fs' is not available"])
75
+ })
76
+
77
+ test('detects require() call', () => {
78
+ const entry = writeTemp('workflow.js', `const fs = require('node:fs');\n`)
79
+ expectViolations(entry, ["'node:fs' is not available"])
80
+ })
81
+
82
+ test('detects dynamic import()', () => {
83
+ const entry = writeTemp('workflow.ts', `const fs = await import('node:fs');\n`)
84
+ expectViolations(entry, ["'node:fs' is not available"])
85
+ })
86
+
87
+ test('detects all restricted modules in a single file', () => {
88
+ const modules = [
89
+ 'crypto',
90
+ 'node:crypto',
91
+ 'fs',
92
+ 'node:fs',
93
+ 'fs/promises',
94
+ 'node:fs/promises',
95
+ 'net',
96
+ 'node:net',
97
+ 'http',
98
+ 'node:http',
99
+ 'https',
100
+ 'node:https',
101
+ 'child_process',
102
+ 'node:child_process',
103
+ 'os',
104
+ 'node:os',
105
+ 'stream',
106
+ 'node:stream',
107
+ 'worker_threads',
108
+ 'node:worker_threads',
109
+ 'dns',
110
+ 'node:dns',
111
+ 'zlib',
112
+ 'node:zlib',
113
+ ]
114
+
115
+ const imports = modules.map((mod, i) => `import m${i} from '${mod}';`).join('\n')
116
+ const entry = writeTemp('workflow.ts', `${imports}\n`)
117
+ expectViolations(
118
+ entry,
119
+ modules.map((mod) => `'${mod}' is not available`),
120
+ )
121
+ })
122
+
123
+ test('does NOT flag allowed third-party modules', () => {
124
+ const entry = writeTemp(
125
+ 'workflow.ts',
126
+ `import { something } from '@chainlink/cre-sdk';\nimport lodash from 'lodash';\n`,
127
+ )
128
+ expectNoViolations(entry)
129
+ })
130
+
131
+ test('does NOT flag relative imports themselves', () => {
132
+ const helper = writeTemp('helper.ts', `export const add = (a: number, b: number) => a + b;\n`)
133
+ const entry = writeTemp(
134
+ 'workflow.ts',
135
+ `import { add } from './helper';\nconsole.log(add(1, 2));\n`,
136
+ )
137
+ expectNoViolations(entry)
138
+ })
139
+
140
+ test('follows relative imports transitively and detects violations in them', () => {
141
+ writeTemp('deep.ts', `import { readFileSync } from 'node:fs';\nexport const x = 1;\n`)
142
+ writeTemp('middle.ts', `import { x } from './deep';\nexport const y = x;\n`)
143
+ const entry = writeTemp('workflow.ts', `import { y } from './middle';\nconsole.log(y);\n`)
144
+ expectViolations(entry, ["'node:fs' is not available"])
145
+ })
146
+
147
+ test('handles circular relative imports without infinite loop', () => {
148
+ writeTemp('a.ts', `import { b } from './b';\nexport const a = 'a';\n`)
149
+ writeTemp('b.ts', `import { a } from './a';\nexport const b = 'b';\n`)
150
+ const entry = writeTemp('workflow.ts', `import { a } from './a';\n`)
151
+ expectNoViolations(entry)
152
+ })
153
+
154
+ test('resolves imports without file extension', () => {
155
+ writeTemp('utils.ts', `import { cpus } from 'node:os';\nexport const x = 1;\n`)
156
+ const entry = writeTemp('workflow.ts', `import { x } from './utils';\nconsole.log(x);\n`)
157
+ expectViolations(entry, ["'node:os' is not available"])
158
+ })
159
+
160
+ test('resolves index file imports', () => {
161
+ writeTemp('lib/index.ts', `import { hostname } from 'node:os';\nexport const name = 'test';\n`)
162
+ const entry = writeTemp('workflow.ts', `import { name } from './lib';\nconsole.log(name);\n`)
163
+ expectViolations(entry, ["'node:os' is not available"])
164
+ })
165
+
166
+ test('reports multiple violations from multiple files', () => {
167
+ writeTemp('helper.ts', `import { exec } from 'node:child_process';\nexport const run = exec;\n`)
168
+ const entry = writeTemp(
169
+ 'workflow.ts',
170
+ `import { run } from './helper';\nimport { readFileSync } from 'node:fs';\n`,
171
+ )
172
+ expectViolations(entry, ["'node:child_process' is not available", "'node:fs' is not available"])
173
+ })
174
+ })
175
+
176
+ // ---------------------------------------------------------------------------
177
+ // Pass 2: Global API analysis
178
+ // ---------------------------------------------------------------------------
179
+
180
+ describe('global API analysis', () => {
181
+ test('detects bare fetch() usage', () => {
182
+ const entry = writeTemp('workflow.ts', `const res = fetch('https://example.com');\n`)
183
+ expectViolations(entry, ["'fetch' is not available"])
184
+ })
185
+
186
+ test('detects setTimeout usage', () => {
187
+ const entry = writeTemp('workflow.ts', `setTimeout(() => {}, 1000);\n`)
188
+ expectViolations(entry, ["'setTimeout' is not available"])
189
+ })
190
+
191
+ test('detects setInterval usage', () => {
192
+ const entry = writeTemp('workflow.ts', `setInterval(() => {}, 1000);\n`)
193
+ expectViolations(entry, ["'setInterval' is not available"])
194
+ })
195
+
196
+ test('detects window reference', () => {
197
+ const entry = writeTemp('workflow.ts', `const w = window;\n`)
198
+ expectViolations(entry, ["'window' is not available"])
199
+ })
200
+
201
+ test('detects document reference', () => {
202
+ const entry = writeTemp('workflow.ts', `const el = document.getElementById('app');\n`)
203
+ expectViolations(entry, ["'document' is not available"])
204
+ })
205
+
206
+ test('detects XMLHttpRequest usage', () => {
207
+ const entry = writeTemp('workflow.ts', `const xhr = new XMLHttpRequest();\n`)
208
+ expectViolations(entry, ["'XMLHttpRequest' is not available"])
209
+ })
210
+
211
+ test('detects localStorage usage', () => {
212
+ const entry = writeTemp('workflow.ts', `localStorage.setItem('key', 'value');\n`)
213
+ expectViolations(entry, ["'localStorage' is not available"])
214
+ })
215
+
216
+ test('detects sessionStorage usage', () => {
217
+ const entry = writeTemp('workflow.ts', `sessionStorage.getItem('key');\n`)
218
+ expectViolations(entry, ["'sessionStorage' is not available"])
219
+ })
220
+
221
+ test('detects globalThis.fetch access', () => {
222
+ const entry = writeTemp('workflow.ts', `const res = globalThis.fetch('https://example.com');\n`)
223
+ expectViolations(entry, ["'globalThis.fetch' is not available"])
224
+ })
225
+
226
+ test('detects globalThis.setTimeout access', () => {
227
+ const entry = writeTemp('workflow.ts', `globalThis.setTimeout(() => {}, 100);\n`)
228
+ expectViolations(entry, ["'globalThis.setTimeout' is not available"])
229
+ })
230
+
231
+ test('does NOT flag user-defined variable named fetch', () => {
232
+ const entry = writeTemp(
233
+ 'workflow.ts',
234
+ `export const fetch = (url: string) => url;\nconst result = fetch('test');\n`,
235
+ )
236
+ expectNoViolations(entry)
237
+ })
238
+
239
+ test('does NOT flag user-defined function named fetch', () => {
240
+ const entry = writeTemp(
241
+ 'workflow.ts',
242
+ `export function fetch(url: string) { return url; }\nconst result = fetch('test');\n`,
243
+ )
244
+ expectNoViolations(entry)
245
+ })
246
+
247
+ test('does NOT flag function parameter named fetch', () => {
248
+ const entry = writeTemp(
249
+ 'workflow.ts',
250
+ `export function doRequest(fetch: (url: string) => void) { fetch('test'); }\n`,
251
+ )
252
+ expectNoViolations(entry)
253
+ })
254
+
255
+ test('does NOT flag property access obj.fetch', () => {
256
+ const entry = writeTemp('workflow.ts', `const obj = { fetch: () => {} };\nobj.fetch();\n`)
257
+ expectNoViolations(entry)
258
+ })
259
+
260
+ test('does NOT flag interface property named fetch', () => {
261
+ const entry = writeTemp('workflow.ts', `interface Client { fetch: (url: string) => void; }\n`)
262
+ expectNoViolations(entry)
263
+ })
264
+
265
+ test('does NOT flag destructured property named fetch from local object', () => {
266
+ const entry = writeTemp(
267
+ 'workflow.ts',
268
+ `const capabilities = { fetch: (url: string) => url };\nconst { fetch } = capabilities;\nexport const result = fetch('test');\n`,
269
+ )
270
+ expectNoViolations(entry)
271
+ })
272
+
273
+ test('does NOT flag class method named fetch', () => {
274
+ const entry = writeTemp(
275
+ 'workflow.ts',
276
+ `class HttpClient {\n fetch(url: string) { return url; }\n}\nnew HttpClient().fetch('test');\n`,
277
+ )
278
+ expectNoViolations(entry)
279
+ })
280
+
281
+ test('detects global APIs in transitively imported files', () => {
282
+ writeTemp('helper.ts', `export const doFetch = () => fetch('https://example.com');\n`)
283
+ const entry = writeTemp('workflow.ts', `import { doFetch } from './helper';\ndoFetch();\n`)
284
+ expectViolations(entry, ["'fetch' is not available"])
285
+ })
286
+
287
+ test('handles a nearby tsconfig with sparse libs and no ambient types', () => {
288
+ writeTemp(
289
+ 'tsconfig.json',
290
+ JSON.stringify(
291
+ {
292
+ compilerOptions: {
293
+ lib: ['ESNext'],
294
+ module: 'ESNext',
295
+ moduleResolution: 'Bundler',
296
+ skipLibCheck: true,
297
+ types: [],
298
+ },
299
+ },
300
+ null,
301
+ 2,
302
+ ),
303
+ )
304
+ const entry = writeTemp('workflow.ts', `fetch('https://example.com');\n`)
305
+ expectViolations(entry, ["'fetch' is not available"])
306
+ })
307
+ })
308
+
309
+ // ---------------------------------------------------------------------------
310
+ // Combined / integration tests
311
+ // ---------------------------------------------------------------------------
312
+
313
+ describe('integration', () => {
314
+ test('clean workflow passes validation', () => {
315
+ const entry = writeTemp(
316
+ 'workflow.ts',
317
+ `
318
+ import { Runner, cre } from '@chainlink/cre-sdk';
319
+
320
+ export async function main() {
321
+ const runner = await Runner.newRunner();
322
+ console.log('Hello from CRE');
323
+ }
324
+ `,
325
+ )
326
+ expectNoViolations(entry)
327
+ })
328
+
329
+ test('detects both module and global API violations in same file', () => {
330
+ const entry = writeTemp(
331
+ 'workflow.ts',
332
+ `
333
+ import { readFileSync } from 'node:fs';
334
+ const data = readFileSync('/tmp/data.json', 'utf-8');
335
+ const res = fetch('https://api.example.com');
336
+ `,
337
+ )
338
+ expectViolations(entry, ["'node:fs' is not available", "'fetch' is not available"])
339
+ })
340
+
341
+ test('error message includes file path and line/column info', () => {
342
+ const entry = writeTemp('workflow.ts', `import { readFileSync } from 'node:fs';\n`)
343
+ try {
344
+ assertWorkflowRuntimeCompatibility(entry)
345
+ throw new Error('Expected error')
346
+ } catch (error) {
347
+ expect(error).toBeInstanceOf(WorkflowRuntimeCompatibilityError)
348
+ const msg = (error as Error).message
349
+ // Should contain relative or absolute path to the file
350
+ expect(msg).toContain('workflow.ts')
351
+ // Should contain line:column format
352
+ expect(msg).toMatch(/:\d+:\d+/)
353
+ }
354
+ })
355
+
356
+ test('error message includes docs link', () => {
357
+ const entry = writeTemp('workflow.ts', `import { readFileSync } from 'node:fs';\n`)
358
+ try {
359
+ assertWorkflowRuntimeCompatibility(entry)
360
+ throw new Error('Expected error')
361
+ } catch (error) {
362
+ const msg = (error as Error).message
363
+ expect(msg).toContain('https://docs.chain.link/cre/concepts/typescript-wasm-runtime')
364
+ }
365
+ })
366
+
367
+ test('handles .js files', () => {
368
+ const entry = writeTemp('workflow.js', `const fs = require('node:fs');\n`)
369
+ expectViolations(entry, ["'node:fs' is not available"])
370
+ })
371
+
372
+ test('handles .mjs files', () => {
373
+ const entry = writeTemp('workflow.mjs', `import { readFileSync } from 'node:fs';\n`)
374
+ expectViolations(entry, ["'node:fs' is not available"])
375
+ })
376
+
377
+ test('handles .cjs files', () => {
378
+ const entry = writeTemp('workflow.cjs', `const fs = require('node:fs');\n`)
379
+ expectViolations(entry, ["'node:fs' is not available"])
380
+ })
381
+
382
+ test('violations are sorted by file path, then line, then column', () => {
383
+ writeTemp(
384
+ 'b-helper.ts',
385
+ `import { exec } from 'node:child_process';\nexport const run = exec;\n`,
386
+ )
387
+ const entry = writeTemp(
388
+ 'a-workflow.ts',
389
+ `import { run } from './b-helper';\nimport { readFileSync } from 'node:fs';\nimport { cpus } from 'node:os';\n`,
390
+ )
391
+ try {
392
+ assertWorkflowRuntimeCompatibility(entry)
393
+ throw new Error('Expected error')
394
+ } catch (error) {
395
+ const msg = (error as Error).message
396
+ const violationLines = msg.split('\n').filter((line) => line.startsWith('- '))
397
+
398
+ // Should have 3 violations minimum
399
+ expect(violationLines.length).toBeGreaterThanOrEqual(3)
400
+
401
+ // Extract file paths from violation lines
402
+ const filePaths = violationLines.map((line) => line.split(':')[0].replace('- ', ''))
403
+
404
+ // Verify sorted order: a-workflow.ts violations before b-helper.ts
405
+ const aIndexes = filePaths
406
+ .map((f, i) => (f.includes('a-workflow') ? i : -1))
407
+ .filter((i) => i >= 0)
408
+ const bIndexes = filePaths
409
+ .map((f, i) => (f.includes('b-helper') ? i : -1))
410
+ .filter((i) => i >= 0)
411
+
412
+ if (aIndexes.length > 0 && bIndexes.length > 0) {
413
+ expect(Math.max(...aIndexes)).toBeLessThan(Math.min(...bIndexes))
414
+ }
415
+ }
416
+ })
417
+
418
+ test('empty file passes validation', () => {
419
+ const entry = writeTemp('workflow.ts', '')
420
+ expectNoViolations(entry)
421
+ })
422
+
423
+ test('file with only comments passes validation', () => {
424
+ const entry = writeTemp('workflow.ts', `// This is a comment\n/* Block comment */\n`)
425
+ expectNoViolations(entry)
426
+ })
427
+
428
+ test('non-existent entry file does not throw', () => {
429
+ const nonExistent = path.join(tempDir, 'does-not-exist.ts')
430
+ // Should not throw since the file doesn't exist - it just won't find violations
431
+ expectNoViolations(nonExistent)
432
+ })
433
+ })