@bomb.sh/router 0.0.1 → 0.7.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 +293 -0
- package/esm/_dnt.polyfills.d.ts +11 -0
- package/esm/_dnt.polyfills.js +15 -0
- package/esm/lib/argument.d.ts +7 -0
- package/esm/lib/argument.js +66 -0
- package/esm/lib/bind.d.ts +41 -0
- package/esm/lib/bind.js +285 -0
- package/esm/lib/checkpoint.d.ts +3 -0
- package/esm/lib/checkpoint.js +5 -0
- package/esm/lib/command.d.ts +6 -0
- package/esm/lib/command.js +16 -0
- package/esm/lib/dasherize.d.ts +1 -0
- package/esm/lib/dasherize.js +7 -0
- package/esm/lib/decode.d.ts +6 -0
- package/esm/lib/decode.js +46 -0
- package/esm/lib/definition.d.ts +4 -0
- package/esm/lib/definition.js +10 -0
- package/esm/lib/dynamic.d.ts +6 -0
- package/esm/lib/dynamic.js +21 -0
- package/esm/lib/env.d.ts +34 -0
- package/esm/lib/env.js +83 -0
- package/esm/lib/extend.d.ts +2 -0
- package/esm/lib/extend.js +4 -0
- package/esm/lib/maybe.d.ts +9 -0
- package/esm/lib/maybe.js +12 -0
- package/esm/lib/multiple.d.ts +8 -0
- package/esm/lib/multiple.js +9 -0
- package/esm/lib/option.d.ts +7 -0
- package/esm/lib/option.js +31 -0
- package/esm/lib/param.d.ts +26 -0
- package/esm/lib/param.js +37 -0
- package/esm/lib/parse.d.ts +2 -0
- package/esm/lib/parse.js +338 -0
- package/esm/lib/pipeline.d.ts +182 -0
- package/esm/lib/pipeline.js +6 -0
- package/esm/lib/print.d.ts +7 -0
- package/esm/lib/print.js +156 -0
- package/esm/lib/read.d.ts +27 -0
- package/esm/lib/read.js +95 -0
- package/esm/lib/rest.d.ts +9 -0
- package/esm/lib/rest.js +1 -0
- package/esm/lib/result.d.ts +9 -0
- package/esm/lib/result.js +1 -0
- package/esm/lib/route.d.ts +9 -0
- package/esm/lib/route.js +45 -0
- package/esm/lib/toggle.d.ts +7 -0
- package/esm/lib/toggle.js +91 -0
- package/esm/lib/tokenize.d.ts +20 -0
- package/esm/lib/tokenize.js +41 -0
- package/esm/lib/tokenizer.d.ts +34 -0
- package/esm/lib/tokenizer.js +130 -0
- package/esm/lib/transform.d.ts +8 -0
- package/esm/lib/transform.js +34 -0
- package/esm/lib/types.d.ts +196 -0
- package/esm/lib/types.js +1 -0
- package/esm/lib/values.d.ts +29 -0
- package/esm/lib/values.js +83 -0
- package/esm/mod.d.ts +29 -0
- package/esm/mod.js +18 -0
- package/esm/package.json +3 -0
- package/package.json +27 -9
- package/src/_dnt.polyfills.ts +27 -0
- package/src/lib/argument.ts +100 -0
- package/src/lib/bind.ts +421 -0
- package/src/lib/checkpoint.ts +10 -0
- package/src/lib/command.ts +36 -0
- package/src/lib/dasherize.ts +7 -0
- package/src/lib/decode.ts +55 -0
- package/src/lib/definition.ts +17 -0
- package/src/lib/dynamic.ts +39 -0
- package/src/lib/env.ts +142 -0
- package/src/lib/extend.ts +12 -0
- package/src/lib/maybe.ts +22 -0
- package/src/lib/multiple.ts +23 -0
- package/src/lib/option.ts +57 -0
- package/src/lib/param.ts +96 -0
- package/src/lib/parse.ts +478 -0
- package/src/lib/pipeline.ts +549 -0
- package/src/lib/print.ts +216 -0
- package/src/lib/read.ts +133 -0
- package/src/lib/rest.ts +10 -0
- package/src/lib/result.ts +10 -0
- package/src/lib/route.ts +77 -0
- package/src/lib/toggle.ts +132 -0
- package/src/lib/tokenize.ts +74 -0
- package/src/lib/tokenizer.ts +191 -0
- package/src/lib/transform.ts +49 -0
- package/src/lib/types.ts +460 -0
- package/src/lib/values.ts +131 -0
- package/src/mod.ts +74 -0
package/src/lib/parse.ts
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import { bindPhase } from "./bind.js";
|
|
2
|
+
import { Envs } from "./env.js";
|
|
3
|
+
import type { Symbol } from "./read.js";
|
|
4
|
+
import type { Rest } from "./rest.js";
|
|
5
|
+
import type { Result } from "./result.js";
|
|
6
|
+
import { type AnyToken, type Literal, tokenize } from "./tokenize.js";
|
|
7
|
+
import { Tokenizer } from "./tokenizer.js";
|
|
8
|
+
import { Values } from "./values.js";
|
|
9
|
+
import type {
|
|
10
|
+
AnyIntent,
|
|
11
|
+
AnyPhase,
|
|
12
|
+
AnyPhases,
|
|
13
|
+
AnyRoute,
|
|
14
|
+
Input,
|
|
15
|
+
Issue,
|
|
16
|
+
Method,
|
|
17
|
+
ModelsByRoute,
|
|
18
|
+
Outcome,
|
|
19
|
+
Parse,
|
|
20
|
+
Path,
|
|
21
|
+
RoutePath,
|
|
22
|
+
UnprocessableContent,
|
|
23
|
+
} from "./types.js";
|
|
24
|
+
|
|
25
|
+
export function parse<const R extends AnyRoute>(
|
|
26
|
+
route: R,
|
|
27
|
+
input: Input,
|
|
28
|
+
): Parse<R>;
|
|
29
|
+
|
|
30
|
+
export function parse(
|
|
31
|
+
route: AnyRoute,
|
|
32
|
+
input: Input,
|
|
33
|
+
): unknown {
|
|
34
|
+
let tokenizer = new Tokenizer(tokenize(input.argv));
|
|
35
|
+
let help = tokenizer.claimAll(flags("-h", "--help"));
|
|
36
|
+
let version = help.rest.claimAll(flags("-v", "--version"));
|
|
37
|
+
let escape = version.rest.claimAll((t) => t.type === "separator");
|
|
38
|
+
let literals = escape.rest.claimAll((t) => t.type === "literal");
|
|
39
|
+
|
|
40
|
+
let method: Method = "execute";
|
|
41
|
+
|
|
42
|
+
if (help.tokens.length > 0) {
|
|
43
|
+
method = "help";
|
|
44
|
+
} else if (version.tokens.length > 0) {
|
|
45
|
+
method = "version";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let rest: Rest = {
|
|
49
|
+
tokens: literals.rest as Tokenizer<Symbol>,
|
|
50
|
+
values: new Values().mount([], input.values ?? []),
|
|
51
|
+
envs: new Envs().mount([], input.envs ?? []),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return advance({
|
|
55
|
+
segments: [{
|
|
56
|
+
id: "/",
|
|
57
|
+
route,
|
|
58
|
+
phases: route.phases,
|
|
59
|
+
tokens: Array.from(rest.tokens),
|
|
60
|
+
path: [],
|
|
61
|
+
start: -1,
|
|
62
|
+
model: {},
|
|
63
|
+
issues: [],
|
|
64
|
+
history: [],
|
|
65
|
+
}],
|
|
66
|
+
active: 0,
|
|
67
|
+
rest,
|
|
68
|
+
models: {},
|
|
69
|
+
method,
|
|
70
|
+
literals: literals.tokens,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function advance(
|
|
75
|
+
state: ParserState,
|
|
76
|
+
): Outcome<AnyIntent | AnyIncrement> {
|
|
77
|
+
while (true) {
|
|
78
|
+
// Starting from the deepest open segment, synchronously descend through
|
|
79
|
+
// routes visible on each segment's head phase.
|
|
80
|
+
state = search(state);
|
|
81
|
+
|
|
82
|
+
let index = state.active;
|
|
83
|
+
let segment = state.segments[index];
|
|
84
|
+
let [phase] = segment.phases;
|
|
85
|
+
|
|
86
|
+
state = {
|
|
87
|
+
...state,
|
|
88
|
+
rest: {
|
|
89
|
+
...state.rest,
|
|
90
|
+
values: state.rest.values.mount(segment.path, phase.values),
|
|
91
|
+
envs: state.rest.envs.mount(segment.path, phase.envs),
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Controls are free of ordinary configuration validation. A dynamic phase
|
|
96
|
+
// is the exception: its model is needed by the caller before the remaining
|
|
97
|
+
// route graph (and therefore the final control target) can be known.
|
|
98
|
+
if (state.method !== "execute" && !phase.resolver) {
|
|
99
|
+
let models = {
|
|
100
|
+
...state.models,
|
|
101
|
+
[segment.id]: segment.model,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
if (index + 1 < state.segments.length) {
|
|
105
|
+
state = {
|
|
106
|
+
...state,
|
|
107
|
+
active: index + 1,
|
|
108
|
+
models,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return resolve({ ...state, models }, segment);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Bind only this phase, within this segment's token view.
|
|
118
|
+
//
|
|
119
|
+
// For an open dynamic segment, binding may consume the horizon but may not
|
|
120
|
+
// look beyond it. For a committed segment, `end` is the hard boundary.
|
|
121
|
+
let binding = bindPhase({
|
|
122
|
+
phase,
|
|
123
|
+
segment: {
|
|
124
|
+
range: {
|
|
125
|
+
start: segment.start,
|
|
126
|
+
end: segment.end,
|
|
127
|
+
},
|
|
128
|
+
path: segment.path,
|
|
129
|
+
},
|
|
130
|
+
rest: state.rest,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
segment = {
|
|
134
|
+
...segment,
|
|
135
|
+
model: {
|
|
136
|
+
...segment.model,
|
|
137
|
+
...binding.model,
|
|
138
|
+
},
|
|
139
|
+
issues: [
|
|
140
|
+
...segment.issues,
|
|
141
|
+
...binding.issues,
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
state = {
|
|
146
|
+
...state,
|
|
147
|
+
rest: binding.rest,
|
|
148
|
+
segments: replace(state.segments, index, segment),
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
if (!binding.valid) {
|
|
152
|
+
let issues = phase.resolver
|
|
153
|
+
? binding.issues
|
|
154
|
+
: [...unexpected(state, segment), ...binding.issues];
|
|
155
|
+
|
|
156
|
+
return unprocessableContent(
|
|
157
|
+
state.segments[state.segments.length - 1],
|
|
158
|
+
issues,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (phase.resolver) {
|
|
163
|
+
// Binding the phase succeeded, but the requirement is still needed.
|
|
164
|
+
let suspended = state;
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
ok: true,
|
|
168
|
+
route: segment.id,
|
|
169
|
+
model: binding.model,
|
|
170
|
+
|
|
171
|
+
resume(result) {
|
|
172
|
+
if (!result.ok) {
|
|
173
|
+
return unprocessableContent(segment, result.issues);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// AnyPhase erases these exact types with `never`; dynamic() already
|
|
177
|
+
// proved them at its public boundary.
|
|
178
|
+
let resolver = phase.resolver as unknown as (
|
|
179
|
+
requirement: unknown,
|
|
180
|
+
) => (route: AnyRoute) => AnyRoute;
|
|
181
|
+
|
|
182
|
+
// The extension operates against the same aggregate route metadata,
|
|
183
|
+
// but begins with one fresh, empty phase.
|
|
184
|
+
let continuation = resolver(result.value)(
|
|
185
|
+
seed(segment.route),
|
|
186
|
+
);
|
|
187
|
+
let phases = stitch(
|
|
188
|
+
continuation.phases,
|
|
189
|
+
segment.phases.slice(1),
|
|
190
|
+
);
|
|
191
|
+
let history = [...segment.history, phase];
|
|
192
|
+
|
|
193
|
+
let next: Segment = {
|
|
194
|
+
...segment,
|
|
195
|
+
route: {
|
|
196
|
+
...continuation,
|
|
197
|
+
phases: [...history, ...phases] as unknown as AnyPhases,
|
|
198
|
+
},
|
|
199
|
+
phases,
|
|
200
|
+
history,
|
|
201
|
+
issues: [
|
|
202
|
+
...segment.issues,
|
|
203
|
+
...(result.issues ?? []),
|
|
204
|
+
],
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
return advance({
|
|
208
|
+
...suspended,
|
|
209
|
+
segments: replace(suspended.segments, index, next),
|
|
210
|
+
});
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// A phase without a resolver is the final phase of this segment.
|
|
216
|
+
let issues = unexpected(state, segment);
|
|
217
|
+
|
|
218
|
+
if (issues.length > 0) {
|
|
219
|
+
return unprocessableContent(
|
|
220
|
+
state.segments[state.segments.length - 1],
|
|
221
|
+
issues,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let id = `/${segment.path.join("/")}`;
|
|
226
|
+
let models = {
|
|
227
|
+
...state.models,
|
|
228
|
+
[id]: segment.model,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// There is already a synchronously matched child segment.
|
|
232
|
+
if (index + 1 < state.segments.length) {
|
|
233
|
+
state = {
|
|
234
|
+
...state,
|
|
235
|
+
active: index + 1,
|
|
236
|
+
models,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// The deepest selected segment is fully resolved.
|
|
243
|
+
return resolve({
|
|
244
|
+
...state,
|
|
245
|
+
models,
|
|
246
|
+
}, segment);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
interface ParserState {
|
|
251
|
+
segments: [Segment, ...Segment[]];
|
|
252
|
+
active: number;
|
|
253
|
+
rest: Rest;
|
|
254
|
+
models: ModelsByRoute;
|
|
255
|
+
method: Method;
|
|
256
|
+
literals: Literal[];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
interface AnyIncrement {
|
|
260
|
+
readonly ok: true;
|
|
261
|
+
readonly route: RoutePath;
|
|
262
|
+
readonly model: object;
|
|
263
|
+
resume(result: Result<unknown>): Outcome<AnyIntent | AnyIncrement>;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
interface Segment {
|
|
267
|
+
id: RoutePath;
|
|
268
|
+
route: AnyRoute;
|
|
269
|
+
path: Path;
|
|
270
|
+
phases: AnyPhases;
|
|
271
|
+
tokens: Symbol[];
|
|
272
|
+
start: number;
|
|
273
|
+
end?: number;
|
|
274
|
+
model: Record<string, unknown>;
|
|
275
|
+
issues: Issue[];
|
|
276
|
+
history: AnyPhase[];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function search(state: ParserState): ParserState {
|
|
280
|
+
let segments = [...state.segments] as [Segment, ...Segment[]];
|
|
281
|
+
let rest = state.rest;
|
|
282
|
+
|
|
283
|
+
while (true) {
|
|
284
|
+
let index = segments.length - 1;
|
|
285
|
+
let segment = segments[index];
|
|
286
|
+
let [phase] = segment.phases;
|
|
287
|
+
let remaining = new Set(Array.from(rest.tokens, (token) => token.index));
|
|
288
|
+
let selector = segment.tokens.find((token) => {
|
|
289
|
+
return remaining.has(token.index) && token.type === "word" &&
|
|
290
|
+
phase.routes.some((route) => route.name === token.text);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
if (!selector) {
|
|
294
|
+
return { ...state, segments, rest };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
let child = phase.routes.find((route) => route.name === selector.text)!;
|
|
298
|
+
let path = [...segment.path, child.name];
|
|
299
|
+
let before = segment.tokens.filter((token) => token.index < selector.index);
|
|
300
|
+
let after = segment.tokens.filter((token) => token.index > selector.index);
|
|
301
|
+
|
|
302
|
+
segments[index] = {
|
|
303
|
+
...segment,
|
|
304
|
+
tokens: before,
|
|
305
|
+
end: selector.index,
|
|
306
|
+
};
|
|
307
|
+
segments.push({
|
|
308
|
+
id: `/${path.join("/")}`,
|
|
309
|
+
route: child,
|
|
310
|
+
path,
|
|
311
|
+
phases: child.phases,
|
|
312
|
+
tokens: after,
|
|
313
|
+
start: selector.index,
|
|
314
|
+
model: {},
|
|
315
|
+
issues: [],
|
|
316
|
+
history: [],
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
rest = {
|
|
320
|
+
...rest,
|
|
321
|
+
tokens: rest.tokens.claimOne((token) => token.index === selector.index)
|
|
322
|
+
.rest,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function flags(...texts: string[]): (token: AnyToken) => boolean {
|
|
328
|
+
return (token) => token.type === "flag" && texts.includes(token.text);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function stitch(
|
|
332
|
+
before: AnyPhases,
|
|
333
|
+
after: readonly AnyPhase[],
|
|
334
|
+
): AnyPhases {
|
|
335
|
+
if (after.length === 0) {
|
|
336
|
+
return before;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
let phases = [...before];
|
|
340
|
+
let phase = phases.pop()!;
|
|
341
|
+
let [next, ...rest] = after;
|
|
342
|
+
|
|
343
|
+
phases.push({
|
|
344
|
+
...phase,
|
|
345
|
+
...next,
|
|
346
|
+
model: {
|
|
347
|
+
params: {
|
|
348
|
+
...phase.model.params,
|
|
349
|
+
...next.model.params,
|
|
350
|
+
},
|
|
351
|
+
steps: [
|
|
352
|
+
...phase.model.steps,
|
|
353
|
+
...next.model.steps,
|
|
354
|
+
],
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
routes: [
|
|
358
|
+
...phase.routes,
|
|
359
|
+
...next.routes,
|
|
360
|
+
],
|
|
361
|
+
values: [
|
|
362
|
+
...phase.values,
|
|
363
|
+
...next.values,
|
|
364
|
+
],
|
|
365
|
+
envs: [
|
|
366
|
+
...phase.envs,
|
|
367
|
+
...next.envs,
|
|
368
|
+
],
|
|
369
|
+
});
|
|
370
|
+
phases.push(...rest);
|
|
371
|
+
|
|
372
|
+
return phases as unknown as AnyPhases;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function resolve(
|
|
376
|
+
state: ParserState,
|
|
377
|
+
segment: Segment,
|
|
378
|
+
): Outcome<AnyIntent> {
|
|
379
|
+
let route = `/${segment.path.join("/")}` as RoutePath;
|
|
380
|
+
let definition = segment.route;
|
|
381
|
+
|
|
382
|
+
if (!definition.methods.includes(state.method)) {
|
|
383
|
+
return {
|
|
384
|
+
ok: false,
|
|
385
|
+
code: "method-not-allowed",
|
|
386
|
+
route,
|
|
387
|
+
definition,
|
|
388
|
+
path: segment.path,
|
|
389
|
+
method: state.method,
|
|
390
|
+
allowed: definition.methods,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
let intent = {
|
|
395
|
+
ok: true as const,
|
|
396
|
+
route,
|
|
397
|
+
definition,
|
|
398
|
+
path: segment.path,
|
|
399
|
+
literals: state.literals,
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
switch (state.method) {
|
|
403
|
+
case "help":
|
|
404
|
+
return {
|
|
405
|
+
...intent,
|
|
406
|
+
method: "help",
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
case "version":
|
|
410
|
+
return {
|
|
411
|
+
...intent,
|
|
412
|
+
method: "version",
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
case "execute":
|
|
416
|
+
return {
|
|
417
|
+
...intent,
|
|
418
|
+
method: "execute",
|
|
419
|
+
model: state.models[route],
|
|
420
|
+
models: state.models,
|
|
421
|
+
issues: state.segments.flatMap((segment) => segment.issues),
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function seed(route: AnyRoute): AnyRoute {
|
|
427
|
+
return {
|
|
428
|
+
...route,
|
|
429
|
+
phases: [{
|
|
430
|
+
model: {
|
|
431
|
+
params: {},
|
|
432
|
+
steps: [],
|
|
433
|
+
},
|
|
434
|
+
routes: [],
|
|
435
|
+
values: [],
|
|
436
|
+
envs: [],
|
|
437
|
+
}],
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function replace<T>(
|
|
442
|
+
items: readonly [T, ...T[]],
|
|
443
|
+
index: number,
|
|
444
|
+
value: T,
|
|
445
|
+
): [T, ...T[]] {
|
|
446
|
+
let result = [...items] as [T, ...T[]];
|
|
447
|
+
result[index] = value;
|
|
448
|
+
return result;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function unprocessableContent(
|
|
452
|
+
segment: Segment,
|
|
453
|
+
issues: readonly Issue[],
|
|
454
|
+
): UnprocessableContent {
|
|
455
|
+
return {
|
|
456
|
+
ok: false,
|
|
457
|
+
code: "unprocessable-content",
|
|
458
|
+
route: segment.id,
|
|
459
|
+
definition: segment.route,
|
|
460
|
+
path: segment.path,
|
|
461
|
+
issues: [...issues],
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function unexpected(
|
|
466
|
+
state: ParserState,
|
|
467
|
+
segment: Segment,
|
|
468
|
+
): Issue[] {
|
|
469
|
+
let members = new Set(
|
|
470
|
+
segment.tokens.map((token) => token.index),
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
return Array.from(state.rest.tokens)
|
|
474
|
+
.filter((token) => members.has(token.index))
|
|
475
|
+
.map((token) => ({
|
|
476
|
+
message: `unexpected ${JSON.stringify(token.text)}`,
|
|
477
|
+
}));
|
|
478
|
+
}
|