envio 3.14.0 → 3.14.1-resolvers.2
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/evm.schema.json +8 -1
- package/fuel.schema.json +7 -0
- package/index.d.ts +258 -5
- package/index.js +10 -0
- package/package.json +6 -6
- package/src/Bin.res +48 -0
- package/src/Bin.res.mjs +34 -1
- package/src/Config.res +12 -2
- package/src/Config.res.mjs +7 -0
- package/src/Env.res +141 -1
- package/src/Env.res.mjs +133 -1
- package/src/HandlerLoader.res +7 -13
- package/src/HandlerLoader.res.mjs +7 -14
- package/src/HandlerTypeCheck.res +207 -0
- package/src/HandlerTypeCheck.res.mjs +262 -0
- package/src/bindings/NodeJs.res +31 -0
- package/src/bindings/NodeJs.res.mjs +3 -0
- package/src/resolvers/ResolverProcess.res +805 -0
- package/src/resolvers/ResolverProcess.res.mjs +662 -0
- package/src/resolvers/ResolverQuery.res +313 -0
- package/src/resolvers/ResolverQuery.res.mjs +220 -0
- package/src/resolvers/collisions.js +108 -0
- package/src/resolvers/db.js +363 -0
- package/src/resolvers/dispatch.js +259 -0
- package/src/resolvers/errors.js +30 -0
- package/src/resolvers/graphqlSelection.js +292 -0
- package/src/resolvers/hasuraAction.js +84 -0
- package/src/resolvers/hasuraApply.js +333 -0
- package/src/resolvers/hasuraMetadata.js +137 -0
- package/src/resolvers/index.js +123 -0
- package/src/resolvers/manifest.js +376 -0
- package/src/resolvers/server.js +304 -0
- package/src/sources/EvmRpcClient.res +2 -0
- package/src/sources/RpcSource.res +17 -4
- package/src/sources/RpcSource.res.mjs +23 -5
- package/src/sources/SourceManager.res +63 -40
- package/src/sources/SourceManager.res.mjs +32 -20
- package/src/tui/Tui.res +1 -1
- package/src/tui/Tui.res.mjs +1 -1
- package/svm.schema.json +7 -0
- package/src/HandlerTypeCheck.mjs +0 -14
- package/src/HandlerTypeCheckWorker.mjs +0 -68
package/src/Env.res
CHANGED
|
@@ -167,6 +167,141 @@ module ClickHouse = {
|
|
|
167
167
|
let databaseEngine = () => read("ENVIO_CLICKHOUSE_DATABASE_ENGINE")
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
// The resolver process runs on its own, so its database connection comes from
|
|
171
|
+
// the same ENVIO_PG_* vars the indexer's does -- point them wherever reads
|
|
172
|
+
// should go. Only what genuinely differs from the indexer's shape gets a var
|
|
173
|
+
// of its own.
|
|
174
|
+
//
|
|
175
|
+
// Read at call time rather than module load, for the same reason ClickHouse's
|
|
176
|
+
// are: `envio dev` injects them after this module has been evaluated.
|
|
177
|
+
module Resolvers = {
|
|
178
|
+
%%private(
|
|
179
|
+
let read: string => option<string> = %raw(`(k) => {
|
|
180
|
+
const v = process.env[k];
|
|
181
|
+
return v === undefined || v === "" ? undefined : v;
|
|
182
|
+
}`)
|
|
183
|
+
|
|
184
|
+
// `Int.fromString` is `parseInt`: it takes the leading digits and drops the
|
|
185
|
+
// rest, so "60s" reads as 60 and "1e3" as 1. For a duration that is a
|
|
186
|
+
// thousandfold error in the quiet direction -- "60s" meaning a minute
|
|
187
|
+
// becomes a 60ms loop -- so the parse only accepts what it can write back
|
|
188
|
+
// unchanged.
|
|
189
|
+
let parseWhole = raw => {
|
|
190
|
+
let trimmed = raw->String.trim
|
|
191
|
+
switch trimmed->Int.fromString {
|
|
192
|
+
| Some(value) if trimmed === value->Int.toString => Some(value)
|
|
193
|
+
| _ => None
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let readInt = (key, ~fallback) =>
|
|
198
|
+
switch read(key) {
|
|
199
|
+
| None => fallback
|
|
200
|
+
| Some(raw) =>
|
|
201
|
+
switch raw->parseWhole {
|
|
202
|
+
| Some(value) if value > 0 => value
|
|
203
|
+
| _ =>
|
|
204
|
+
JsError.throwWithMessage(
|
|
205
|
+
`Invalid ${key} value: "${raw}". Expected a positive whole number.`,
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
let port = () =>
|
|
212
|
+
switch readInt("ENVIO_RESOLVERS_PORT", ~fallback=9900) {
|
|
213
|
+
| value if value <= 65535 => value
|
|
214
|
+
| value =>
|
|
215
|
+
// `readInt` only rejects zero and below, so an out-of-range port reached
|
|
216
|
+
// `listen` and failed there instead, naming neither the variable nor why.
|
|
217
|
+
JsError.throwWithMessage(
|
|
218
|
+
`Invalid ENVIO_RESOLVERS_PORT value: "${value->Int.toString}". A TCP port is 1 to 65535.`,
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Sized as concurrent heavy requests x per-request fan-out, not as the
|
|
223
|
+
// indexer's two long-lived connections: one resolver request can hold four
|
|
224
|
+
// at once.
|
|
225
|
+
let poolSize = () => readInt("ENVIO_RESOLVERS_POOL_SIZE", ~fallback=25)
|
|
226
|
+
|
|
227
|
+
let poolWaitTimeoutMs = () => readInt("ENVIO_RESOLVERS_POOL_WAIT_TIMEOUT_MS", ~fallback=10_000)
|
|
228
|
+
|
|
229
|
+
// `envio dev` sets this on the resolver process it spawns: locally the
|
|
230
|
+
// caller is the person who wrote the resolver, so an unexpected error's own
|
|
231
|
+
// message is what they need. Deployed, the caller may be the public internet
|
|
232
|
+
// and the message can carry a connection string.
|
|
233
|
+
let exposeErrors = () =>
|
|
234
|
+
switch read("ENVIO_RESOLVERS_EXPOSE_ERRORS") {
|
|
235
|
+
| None
|
|
236
|
+
| Some("false") => false
|
|
237
|
+
| Some("true") => true
|
|
238
|
+
| Some(other) =>
|
|
239
|
+
// Refused rather than read as false: a typo here silently keeps a
|
|
240
|
+
// resolver's own error messages off the wire, which is the opposite of
|
|
241
|
+
// what the person who set it wanted, and nothing would say so.
|
|
242
|
+
JsError.throwWithMessage(
|
|
243
|
+
`Invalid ENVIO_RESOLVERS_EXPOSE_ERRORS value: "${other}". Expected "true" or "false".`,
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// The URL *Hasura* posts to, which is not the address this process binds:
|
|
248
|
+
// it is baked into every action, so Hasura has no other way to reach the
|
|
249
|
+
// resolvers.
|
|
250
|
+
let publicUrl = () => read("ENVIO_RESOLVERS_PUBLIC_URL")
|
|
251
|
+
|
|
252
|
+
// How often the metadata is re-asserted. 0 disables it, for a deployment
|
|
253
|
+
// where something else owns Hasura's metadata.
|
|
254
|
+
let metadataIntervalMs = () =>
|
|
255
|
+
switch read("ENVIO_RESOLVERS_METADATA_INTERVAL_MS") {
|
|
256
|
+
| None => 60_000
|
|
257
|
+
| Some(raw) =>
|
|
258
|
+
switch raw->parseWhole {
|
|
259
|
+
| Some(value) if value >= 0 => value
|
|
260
|
+
| _ =>
|
|
261
|
+
JsError.throwWithMessage(
|
|
262
|
+
`Invalid ENVIO_RESOLVERS_METADATA_INTERVAL_MS value: "${raw}". Expected a whole number of milliseconds, or 0 to disable.`,
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Required on every request to either route. Unset, the service cannot tell
|
|
268
|
+
// its callers apart from anything else that reaches the socket, and both
|
|
269
|
+
// routes take the role from the request body -- so anyone can claim `admin`.
|
|
270
|
+
let actionSecret = () => read("ENVIO_RESOLVERS_ACTION_SECRET")
|
|
271
|
+
|
|
272
|
+
// Unlocks resolvers declared `private`. Comma-separated so a rotation can
|
|
273
|
+
// present the old and new key at once; a private resolver with none of these
|
|
274
|
+
// configured is unreachable rather than open.
|
|
275
|
+
let privateKeys = () =>
|
|
276
|
+
switch read("ENVIO_RESOLVERS_PRIVATE_KEYS") {
|
|
277
|
+
| None => []
|
|
278
|
+
| Some(raw) =>
|
|
279
|
+
raw
|
|
280
|
+
->String.split(",")
|
|
281
|
+
->Array.map(String.trim)
|
|
282
|
+
->Array.filter(key => key !== "")
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Read raw rather than through `Env.Hasura`, whose dev fallbacks would have
|
|
286
|
+
// this process apply metadata to a localhost Hasura nobody asked for. Only
|
|
287
|
+
// an endpoint someone set means "there is a Hasura to register with".
|
|
288
|
+
let hasuraEndpoint = () => read("HASURA_GRAPHQL_ENDPOINT")
|
|
289
|
+
let hasuraAdminSecret = () => read("HASURA_GRAPHQL_ADMIN_SECRET")
|
|
290
|
+
|
|
291
|
+
// Set where a transaction-mode pooler is the only way in. Those reject
|
|
292
|
+
// named prepared statements, so this gives up plan reuse.
|
|
293
|
+
let poolerBacked = () =>
|
|
294
|
+
switch read("ENVIO_RESOLVERS_POOLER_BACKED") {
|
|
295
|
+
| None
|
|
296
|
+
| Some("false") => false
|
|
297
|
+
| Some("true") => true
|
|
298
|
+
| Some(other) =>
|
|
299
|
+
JsError.throwWithMessage(
|
|
300
|
+
`Invalid ENVIO_RESOLVERS_POOLER_BACKED value: "${other}". Expected "true" or "false".`,
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
170
305
|
module Hasura = {
|
|
171
306
|
// Disable it on HS indexer run, since we don't have Hasura credentials anyways
|
|
172
307
|
// Also, it might be useful for some users who don't care about Hasura
|
|
@@ -184,7 +319,12 @@ module Hasura = {
|
|
|
184
319
|
~devFallback="http://localhost:8080/v1/metadata",
|
|
185
320
|
)
|
|
186
321
|
|
|
187
|
-
|
|
322
|
+
// Lazy on purpose. `graphqlEndpoint` is required in production, and a missing
|
|
323
|
+
// one arrives here as undefined -- slicing it threw a bare TypeError at import
|
|
324
|
+
// and killed the process before `EnvSafe.close` could say which variable was
|
|
325
|
+
// missing. Only the dev TUI reads this, so there is nothing to compute up
|
|
326
|
+
// front.
|
|
327
|
+
let url = () => graphqlEndpoint->String.slice(~start=0, ~end=-("/v1/metadata"->String.length))
|
|
188
328
|
|
|
189
329
|
let role = envSafe->EnvSafe.get("HASURA_GRAPHQL_ROLE", S.string, ~devFallback="admin")
|
|
190
330
|
|
package/src/Env.res.mjs
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as EnvSafe from "./EnvSafe.res.mjs";
|
|
4
4
|
import * as Logging from "./Logging.res.mjs";
|
|
5
5
|
import * as Postgres from "./bindings/Postgres.res.mjs";
|
|
6
|
+
import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
|
|
6
7
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
8
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
8
9
|
import * as HyperSyncClient from "./sources/HyperSyncClient.res.mjs";
|
|
@@ -147,6 +148,134 @@ let ClickHouse = {
|
|
|
147
148
|
databaseEngine: databaseEngine
|
|
148
149
|
};
|
|
149
150
|
|
|
151
|
+
let read$1 = ((k) => {
|
|
152
|
+
const v = process.env[k];
|
|
153
|
+
return v === undefined || v === "" ? undefined : v;
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
function parseWhole(raw) {
|
|
157
|
+
let trimmed = raw.trim();
|
|
158
|
+
let value = Stdlib_Int.fromString(trimmed, undefined);
|
|
159
|
+
if (value !== undefined && trimmed === value.toString()) {
|
|
160
|
+
return value;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function readInt(key, fallback) {
|
|
165
|
+
let raw = read$1(key);
|
|
166
|
+
if (raw === undefined) {
|
|
167
|
+
return fallback;
|
|
168
|
+
}
|
|
169
|
+
let value = parseWhole(raw);
|
|
170
|
+
if (value !== undefined && value > 0) {
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
return Stdlib_JsError.throwWithMessage(`Invalid ` + key + ` value: "` + raw + `". Expected a positive whole number.`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function port$1() {
|
|
177
|
+
let value = readInt("ENVIO_RESOLVERS_PORT", 9900);
|
|
178
|
+
if (value <= 65535) {
|
|
179
|
+
return value;
|
|
180
|
+
} else {
|
|
181
|
+
return Stdlib_JsError.throwWithMessage(`Invalid ENVIO_RESOLVERS_PORT value: "` + value.toString() + `". A TCP port is 1 to 65535.`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function poolSize() {
|
|
186
|
+
return readInt("ENVIO_RESOLVERS_POOL_SIZE", 25);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function poolWaitTimeoutMs() {
|
|
190
|
+
return readInt("ENVIO_RESOLVERS_POOL_WAIT_TIMEOUT_MS", 10000);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function exposeErrors() {
|
|
194
|
+
let other = read$1("ENVIO_RESOLVERS_EXPOSE_ERRORS");
|
|
195
|
+
if (other === undefined) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
switch (other) {
|
|
199
|
+
case "false" :
|
|
200
|
+
return false;
|
|
201
|
+
case "true" :
|
|
202
|
+
return true;
|
|
203
|
+
default:
|
|
204
|
+
return Stdlib_JsError.throwWithMessage(`Invalid ENVIO_RESOLVERS_EXPOSE_ERRORS value: "` + other + `". Expected "true" or "false".`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function publicUrl() {
|
|
209
|
+
return read$1("ENVIO_RESOLVERS_PUBLIC_URL");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function metadataIntervalMs() {
|
|
213
|
+
let raw = read$1("ENVIO_RESOLVERS_METADATA_INTERVAL_MS");
|
|
214
|
+
if (raw === undefined) {
|
|
215
|
+
return 60000;
|
|
216
|
+
}
|
|
217
|
+
let value = parseWhole(raw);
|
|
218
|
+
if (value !== undefined) {
|
|
219
|
+
if (value >= 0) {
|
|
220
|
+
return value;
|
|
221
|
+
} else {
|
|
222
|
+
return Stdlib_JsError.throwWithMessage(`Invalid ENVIO_RESOLVERS_METADATA_INTERVAL_MS value: "` + raw + `". Expected a whole number of milliseconds, or 0 to disable.`);
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
return Stdlib_JsError.throwWithMessage(`Invalid ENVIO_RESOLVERS_METADATA_INTERVAL_MS value: "` + raw + `". Expected a whole number of milliseconds, or 0 to disable.`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function actionSecret() {
|
|
230
|
+
return read$1("ENVIO_RESOLVERS_ACTION_SECRET");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function privateKeys() {
|
|
234
|
+
let raw = read$1("ENVIO_RESOLVERS_PRIVATE_KEYS");
|
|
235
|
+
if (raw !== undefined) {
|
|
236
|
+
return raw.split(",").map(prim => prim.trim()).filter(key => key !== "");
|
|
237
|
+
} else {
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function hasuraEndpoint() {
|
|
243
|
+
return read$1("HASURA_GRAPHQL_ENDPOINT");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function hasuraAdminSecret() {
|
|
247
|
+
return read$1("HASURA_GRAPHQL_ADMIN_SECRET");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function poolerBacked() {
|
|
251
|
+
let other = read$1("ENVIO_RESOLVERS_POOLER_BACKED");
|
|
252
|
+
if (other === undefined) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
switch (other) {
|
|
256
|
+
case "false" :
|
|
257
|
+
return false;
|
|
258
|
+
case "true" :
|
|
259
|
+
return true;
|
|
260
|
+
default:
|
|
261
|
+
return Stdlib_JsError.throwWithMessage(`Invalid ENVIO_RESOLVERS_POOLER_BACKED value: "` + other + `". Expected "true" or "false".`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
let Resolvers = {
|
|
266
|
+
port: port$1,
|
|
267
|
+
poolSize: poolSize,
|
|
268
|
+
poolWaitTimeoutMs: poolWaitTimeoutMs,
|
|
269
|
+
exposeErrors: exposeErrors,
|
|
270
|
+
publicUrl: publicUrl,
|
|
271
|
+
metadataIntervalMs: metadataIntervalMs,
|
|
272
|
+
actionSecret: actionSecret,
|
|
273
|
+
privateKeys: privateKeys,
|
|
274
|
+
hasuraEndpoint: hasuraEndpoint,
|
|
275
|
+
hasuraAdminSecret: hasuraAdminSecret,
|
|
276
|
+
poolerBacked: poolerBacked
|
|
277
|
+
};
|
|
278
|
+
|
|
150
279
|
let enabled = EnvSafe.get(envSafe, "ENVIO_HASURA", S$RescriptSchema.bool, undefined, true, undefined, undefined);
|
|
151
280
|
|
|
152
281
|
let s = EnvSafe.get(envSafe, "ENVIO_HASURA_RESPONSE_LIMIT", S$RescriptSchema.option(S$RescriptSchema.int), undefined, undefined, undefined, undefined);
|
|
@@ -155,7 +284,9 @@ let responseLimit = s !== undefined ? s : EnvSafe.get(envSafe, "HASURA_RESPONSE_
|
|
|
155
284
|
|
|
156
285
|
let graphqlEndpoint = EnvSafe.get(envSafe, "HASURA_GRAPHQL_ENDPOINT", S$RescriptSchema.string, undefined, undefined, "http://localhost:8080/v1/metadata", undefined);
|
|
157
286
|
|
|
158
|
-
|
|
287
|
+
function url() {
|
|
288
|
+
return graphqlEndpoint.slice(0, -"/v1/metadata".length | 0);
|
|
289
|
+
}
|
|
159
290
|
|
|
160
291
|
let role = EnvSafe.get(envSafe, "HASURA_GRAPHQL_ROLE", S$RescriptSchema.string, undefined, undefined, "admin", undefined);
|
|
161
292
|
|
|
@@ -240,6 +371,7 @@ export {
|
|
|
240
371
|
logStrategy,
|
|
241
372
|
Db,
|
|
242
373
|
ClickHouse,
|
|
374
|
+
Resolvers,
|
|
243
375
|
Hasura,
|
|
244
376
|
Configurable,
|
|
245
377
|
ThrottleWrites,
|
package/src/HandlerLoader.res
CHANGED
|
@@ -77,15 +77,6 @@ let getAutoLoadFiles = async (~config: Config.t) => {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
type typeCheckResult = {
|
|
81
|
-
skipped?: string,
|
|
82
|
-
errors?: string,
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
@module("./HandlerTypeCheck.mjs")
|
|
86
|
-
external checkTypesInWorker: (~cwd: string, ~files: array<string>) => promise<typeCheckResult> =
|
|
87
|
-
"check"
|
|
88
|
-
|
|
89
80
|
let typeCheck = async (~config: Config.t, ~autoLoadFiles) => {
|
|
90
81
|
// A contract's `handler:` may also be an auto-loaded file.
|
|
91
82
|
let files =
|
|
@@ -96,10 +87,13 @@ let typeCheck = async (~config: Config.t, ~autoLoadFiles) => {
|
|
|
96
87
|
->Utils.Set.fromArray
|
|
97
88
|
->Utils.Set.toArray
|
|
98
89
|
if files->Array.length > 0 {
|
|
99
|
-
switch await
|
|
100
|
-
|
|
|
101
|
-
|
|
102
|
-
|
|
90
|
+
switch await HandlerTypeCheck.check(~cwd=NodeJs.Process.cwd(), ~files) {
|
|
91
|
+
| TypeErrors(errors) =>
|
|
92
|
+
JsError.throwWithMessage(
|
|
93
|
+
`The handler type check found errors. Fix them to start the indexer:\n\n${errors}`,
|
|
94
|
+
)
|
|
95
|
+
| Skipped(warning) => Logging.warn(warning)
|
|
96
|
+
| Passed => ()
|
|
103
97
|
}
|
|
104
98
|
}
|
|
105
99
|
}
|
|
@@ -10,10 +10,10 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
|
10
10
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
11
11
|
import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
|
|
12
12
|
import * as HandlerRegister from "./HandlerRegister.res.mjs";
|
|
13
|
+
import * as HandlerTypeCheck from "./HandlerTypeCheck.res.mjs";
|
|
13
14
|
import * as Promises from "node:fs/promises";
|
|
14
15
|
import * as TsModuleHooksMjs from "./TsModuleHooks.mjs";
|
|
15
16
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
16
|
-
import * as HandlerTypeCheckMjs from "./HandlerTypeCheck.mjs";
|
|
17
17
|
|
|
18
18
|
function registerTsHooksWith(prim) {
|
|
19
19
|
TsModuleHooksMjs.register(prim);
|
|
@@ -70,24 +70,18 @@ async function getAutoLoadFiles(config) {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
function checkTypesInWorker(prim0, prim1) {
|
|
74
|
-
return HandlerTypeCheckMjs.check(prim0, prim1);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
73
|
async function typeCheck(config, autoLoadFiles) {
|
|
78
74
|
let files = Array.from(new Set(autoLoadFiles.concat(Stdlib_Array.filterMap(config.contractHandlers, param => param.handler)).filter(isTypeScript).map(toAbsolutePath)));
|
|
79
75
|
if (files.length === 0) {
|
|
80
76
|
return;
|
|
81
77
|
}
|
|
82
|
-
let
|
|
83
|
-
|
|
84
|
-
let errors = match.errors;
|
|
85
|
-
if (errors !== undefined) {
|
|
86
|
-
return Stdlib_JsError.throwWithMessage(`Handler files have type errors:\n\n` + errors);
|
|
87
|
-
} else if (skipped !== undefined) {
|
|
88
|
-
return Logging.warn(skipped);
|
|
89
|
-
} else {
|
|
78
|
+
let errors = await HandlerTypeCheck.check(Process.cwd(), files);
|
|
79
|
+
if (typeof errors !== "object") {
|
|
90
80
|
return;
|
|
81
|
+
} else if (errors.TAG === "TypeErrors") {
|
|
82
|
+
return Stdlib_JsError.throwWithMessage(`The handler type check found errors. Fix them to start the indexer:\n\n` + errors._0);
|
|
83
|
+
} else {
|
|
84
|
+
return Logging.warn(errors._0);
|
|
91
85
|
}
|
|
92
86
|
}
|
|
93
87
|
|
|
@@ -111,7 +105,6 @@ export {
|
|
|
111
105
|
importHandler,
|
|
112
106
|
registerContractHandlers,
|
|
113
107
|
getAutoLoadFiles,
|
|
114
|
-
checkTypesInWorker,
|
|
115
108
|
typeCheck,
|
|
116
109
|
registerAllHandlers,
|
|
117
110
|
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
type result =
|
|
2
|
+
| Passed
|
|
3
|
+
| TypeErrors(string)
|
|
4
|
+
| Skipped(string)
|
|
5
|
+
|
|
6
|
+
let timeoutSeconds = 30
|
|
7
|
+
|
|
8
|
+
// tsc's pretty output reads the same from TypeScript 5 to 7: a diagnostic
|
|
9
|
+
// starts unindented with its location, its code frame and related locations
|
|
10
|
+
// follow indented, and `--listFiles` prints the program's files after the
|
|
11
|
+
// last one, before the closing summary.
|
|
12
|
+
let fileDiagnostic = /^(.+):\d+:\d+ - error TS(\d+): /
|
|
13
|
+
let globalDiagnostic = /^error TS(\d+): /
|
|
14
|
+
let summary = /^Found \d+ errors?\b/
|
|
15
|
+
let thrownError = /^\w*Error: /
|
|
16
|
+
|
|
17
|
+
let lines = output => output->String.replaceAll("\r\n", "\n")->String.split("\n")
|
|
18
|
+
|
|
19
|
+
let rec findUp = (directory, name) => {
|
|
20
|
+
let candidate = NodeJs.Path.resolve([directory, name])->NodeJs.Path.toString
|
|
21
|
+
if NodeJs.Fs.existsSync(candidate) {
|
|
22
|
+
Some(candidate)
|
|
23
|
+
} else {
|
|
24
|
+
let parent = NodeJs.Path.dirname(directory)->NodeJs.Path.toString
|
|
25
|
+
parent === directory ? None : findUp(parent, name)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// The `tsc` the project's own scripts run. TypeScript 7 has no JavaScript API
|
|
30
|
+
// to call instead, only this command.
|
|
31
|
+
let resolveTsc = (~cwd) =>
|
|
32
|
+
try {
|
|
33
|
+
let manifest =
|
|
34
|
+
NodeJs.Module.createRequire(
|
|
35
|
+
NodeJs.Path.resolve([cwd, "package.json"])->NodeJs.Path.toString,
|
|
36
|
+
)->NodeJs.Module.resolve("typescript/package.json")
|
|
37
|
+
let bin = switch NodeJs.Fs.readFileSync(manifest)->JSON.parseOrThrow {
|
|
38
|
+
| JSON.Object(fields) =>
|
|
39
|
+
switch fields->Dict.get("bin") {
|
|
40
|
+
| Some(JSON.String(bin)) => Some(bin)
|
|
41
|
+
| Some(JSON.Object(bins)) =>
|
|
42
|
+
switch bins->Dict.get("tsc") {
|
|
43
|
+
| Some(JSON.String(bin)) => Some(bin)
|
|
44
|
+
| _ => None
|
|
45
|
+
}
|
|
46
|
+
| _ => None
|
|
47
|
+
}
|
|
48
|
+
| _ => None
|
|
49
|
+
}
|
|
50
|
+
bin->Option.map(bin =>
|
|
51
|
+
NodeJs.Path.resolve([
|
|
52
|
+
NodeJs.Path.dirname(manifest)->NodeJs.Path.toString,
|
|
53
|
+
bin,
|
|
54
|
+
])->NodeJs.Path.toString
|
|
55
|
+
)
|
|
56
|
+
} catch {
|
|
57
|
+
| _ => None
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type run = {error: option<NodeJs.ChildProcess.execFileError>, output: string}
|
|
61
|
+
|
|
62
|
+
let runTsc = (~tsc, ~cwd, args) =>
|
|
63
|
+
Promise.make((resolve, _) =>
|
|
64
|
+
NodeJs.ChildProcess.execFile(
|
|
65
|
+
NodeJs.Process.process->NodeJs.Process.execPath,
|
|
66
|
+
[tsc]->Array.concat(args),
|
|
67
|
+
{cwd, timeout: timeoutSeconds * 1000, maxBuffer: Float.Constants.positiveInfinity},
|
|
68
|
+
(error, stdout, stderr) =>
|
|
69
|
+
resolve({
|
|
70
|
+
error: error->Null.toOption,
|
|
71
|
+
output: NodeJs.Util.stripVTControlCharacters(stdout ++ stderr),
|
|
72
|
+
}),
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
type diagnostic = {file: option<string>, code: int, lines: array<string>}
|
|
77
|
+
|
|
78
|
+
// tsc type-checks nothing after a syntax error, an options error or a global
|
|
79
|
+
// error, and an options error is reported at its line in tsconfig.json.
|
|
80
|
+
let canStopTypeChecking = ({file, code}) =>
|
|
81
|
+
file->Option.isNone || code < 2000 || (code >= 5000 && code < 7000)
|
|
82
|
+
|
|
83
|
+
// Paths compare relative to the project, which on Windows also ignores the
|
|
84
|
+
// case of a drive letter tsc may print differently.
|
|
85
|
+
let parse = (output, ~cwd) => {
|
|
86
|
+
let relative = file =>
|
|
87
|
+
NodeJs.Path.relative(cwd, NodeJs.Path.resolve([cwd, file])->NodeJs.Path.toString)
|
|
88
|
+
let diagnostics = []
|
|
89
|
+
let programFiles = Utils.Set.make()
|
|
90
|
+
let current = ref(None)
|
|
91
|
+
let start = (~file, ~code, line) => {
|
|
92
|
+
let diagnostic = {
|
|
93
|
+
file,
|
|
94
|
+
code: code->Int.fromString->Option.getOr(0),
|
|
95
|
+
lines: [line],
|
|
96
|
+
}
|
|
97
|
+
diagnostics->Array.push(diagnostic)
|
|
98
|
+
current := Some(diagnostic)
|
|
99
|
+
}
|
|
100
|
+
output
|
|
101
|
+
->lines
|
|
102
|
+
->Array.forEach(line =>
|
|
103
|
+
switch (
|
|
104
|
+
fileDiagnostic->RegExp.exec(line)->Option.map(RegExp.Result.matches),
|
|
105
|
+
globalDiagnostic->RegExp.exec(line)->Option.map(RegExp.Result.matches),
|
|
106
|
+
) {
|
|
107
|
+
| (Some([Some(file), Some(code)]), _) => start(~file=Some(relative(file)), ~code, line)
|
|
108
|
+
| (_, Some([Some(code)])) => start(~file=None, ~code, line)
|
|
109
|
+
| _ if NodeJs.Path.isAbsolute(line) =>
|
|
110
|
+
programFiles->Utils.Set.add(relative(line))->ignore
|
|
111
|
+
current := None
|
|
112
|
+
| _ if summary->RegExp.test(line) => current := None
|
|
113
|
+
| _ => current.contents->Option.forEach(diagnostic => diagnostic.lines->Array.push(line))
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
(diagnostics, programFiles)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Exit codes say nothing here: TypeScript 5 and 6 exit with 2 on type errors,
|
|
120
|
+
// 7 with 1, and 1 is also how its launcher fails when the native compiler
|
|
121
|
+
// isn't installed.
|
|
122
|
+
let failure = ({error, output}) =>
|
|
123
|
+
switch error {
|
|
124
|
+
| Some({killed: true}) =>
|
|
125
|
+
`Skipped the handler type check: tsc didn't finish within ${timeoutSeconds->Int.toString} seconds.`
|
|
126
|
+
| _ =>
|
|
127
|
+
let reason =
|
|
128
|
+
output
|
|
129
|
+
->lines
|
|
130
|
+
->Array.find(line => thrownError->RegExp.test(line))
|
|
131
|
+
->Option.getOr(output->String.trim)
|
|
132
|
+
`Skipped the handler type check: tsc failed to run:\n\n${reason}`
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let check = async (~cwd, ~files) => {
|
|
136
|
+
// The ReScript compiler checks a ReScript project's handlers, so it has no
|
|
137
|
+
// reason to set up TypeScript, and the warning would only be noise there.
|
|
138
|
+
// `rescript.json` at the root is what makes codegen treat it as ReScript.
|
|
139
|
+
let skip = warning =>
|
|
140
|
+
NodeJs.Fs.existsSync(NodeJs.Path.resolve([cwd, "rescript.json"])->NodeJs.Path.toString)
|
|
141
|
+
? Passed
|
|
142
|
+
: Skipped(warning)
|
|
143
|
+
|
|
144
|
+
switch (resolveTsc(~cwd), findUp(cwd, "tsconfig.json")) {
|
|
145
|
+
| (None, _) =>
|
|
146
|
+
skip(
|
|
147
|
+
"Skipped the handler type check: typescript isn't installed. Add it to devDependencies to type-check handlers on start.",
|
|
148
|
+
)
|
|
149
|
+
| (_, None) =>
|
|
150
|
+
skip(
|
|
151
|
+
"Skipped the handler type check: no tsconfig.json found. Add one, like the one envio init creates, to type-check handlers on start.",
|
|
152
|
+
)
|
|
153
|
+
| (Some(tsc), Some(tsconfig)) =>
|
|
154
|
+
// The project's own `tsc --noEmit`, minus the build info it would write.
|
|
155
|
+
// `explainFiles` would replace the listed files' absolute paths with
|
|
156
|
+
// relative ones and the reasons each is included.
|
|
157
|
+
let run = await runTsc(
|
|
158
|
+
~tsc,
|
|
159
|
+
~cwd,
|
|
160
|
+
[
|
|
161
|
+
"--project",
|
|
162
|
+
tsconfig,
|
|
163
|
+
"--noEmit",
|
|
164
|
+
"--incremental",
|
|
165
|
+
"false",
|
|
166
|
+
"--composite",
|
|
167
|
+
"false",
|
|
168
|
+
"--listFiles",
|
|
169
|
+
"--explainFiles",
|
|
170
|
+
"false",
|
|
171
|
+
"--pretty",
|
|
172
|
+
],
|
|
173
|
+
)
|
|
174
|
+
let (diagnostics, programFiles) = run.output->parse(~cwd)
|
|
175
|
+
let report = diagnostics =>
|
|
176
|
+
diagnostics
|
|
177
|
+
->Array.map(({lines}) => lines->Array.join("\n")->String.trimEnd)
|
|
178
|
+
->Array.join("\n\n")
|
|
179
|
+
let handlers = files->Array.map(file => NodeJs.Path.relative(cwd, file))
|
|
180
|
+
let isHandler = ({file}) => file->Option.mapOr(false, file => handlers->Array.includes(file))
|
|
181
|
+
let errors = diagnostics->Array.filter(isHandler)
|
|
182
|
+
let stopping =
|
|
183
|
+
diagnostics->Array.filter(diagnostic =>
|
|
184
|
+
!isHandler(diagnostic) && canStopTypeChecking(diagnostic)
|
|
185
|
+
)
|
|
186
|
+
let unchecked = handlers->Array.filter(file => !(programFiles->Utils.Set.has(file)))
|
|
187
|
+
if run.error->Option.isSome && diagnostics->Array.length === 0 {
|
|
188
|
+
Skipped(failure(run))
|
|
189
|
+
} else if errors->Array.length > 0 {
|
|
190
|
+
TypeErrors(report(errors))
|
|
191
|
+
} else if stopping->Array.length > 0 {
|
|
192
|
+
let reason = report(stopping)
|
|
193
|
+
Skipped(
|
|
194
|
+
`Skipped the handler type check: tsc can't check the handlers until these errors are fixed:\n\n${reason}`,
|
|
195
|
+
)
|
|
196
|
+
} else if unchecked->Array.length > 0 {
|
|
197
|
+
let names = unchecked->Array.join(", ")
|
|
198
|
+
let tsconfig = NodeJs.Path.relative(cwd, tsconfig)
|
|
199
|
+
let pronoun = unchecked->Array.length === 1 ? "it" : "them"
|
|
200
|
+
Skipped(
|
|
201
|
+
`Skipped the handler type check: ${tsconfig} doesn't include ${names}. Add ${pronoun} to "include" to type-check ${pronoun} on start.`,
|
|
202
|
+
)
|
|
203
|
+
} else {
|
|
204
|
+
Passed
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|