@volter/twin-standard 1.0.84 → 1.0.86
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/dist/src/walk/behavior-journey.js +32 -5
- package/dist/src/walk/pack-directory.d.ts +1 -0
- package/dist/src/walk/pack-directory.js +15 -0
- package/dist/src/walk/vendor-fetch.js +12 -3
- package/package.json +2 -2
- package/src/walk/behavior-journey.ts +31 -5
- package/src/walk/pack-directory.ts +15 -0
- package/src/walk/vendor-fetch.ts +9 -3
|
@@ -18,6 +18,7 @@ import { createHostTable } from '@volter/world-core/host-routing';
|
|
|
18
18
|
import packFactsDocument from '@volter/world-core/generated/pack-facts.json' with { type: 'json' };
|
|
19
19
|
import { isMysql, isResp, at, captureAt, echoFailures, expandFills, expectFailures, fill, fillJson, formBody, isGit, isLine, isSql, isSocket, isWait, jwtAuthorization, oauth1Authorization, presignedPath, sigv4Headers, waitMs } from "./journey-kit.js";
|
|
20
20
|
import { lanesOf, lineShape, shapeChecker } from "./shape.js";
|
|
21
|
+
import { walkPackDirectory } from "./pack-directory.js";
|
|
21
22
|
import { xmlMembers } from "./xml-view.js";
|
|
22
23
|
/** The pack repository the walk finds a vendor's pack in, set by the caller before its first walk. */
|
|
23
24
|
let packsRoot;
|
|
@@ -31,6 +32,8 @@ function socketAt(open, host) {
|
|
|
31
32
|
const baselineFacts = packFactsDocument.packs;
|
|
32
33
|
let walkFacts = baselineFacts;
|
|
33
34
|
let walkHosts = createHostTable(walkFacts).hosts;
|
|
35
|
+
const directories = new Map();
|
|
36
|
+
const installedFacts = new Set();
|
|
34
37
|
export function usePacks(dir) {
|
|
35
38
|
packsRoot = dir;
|
|
36
39
|
const installed = {};
|
|
@@ -40,12 +43,34 @@ export function usePacks(dir) {
|
|
|
40
43
|
Object.assign(installed, JSON.parse(readFileSync(file, 'utf8')).packs);
|
|
41
44
|
}
|
|
42
45
|
walkFacts = { ...baselineFacts, ...installed };
|
|
46
|
+
installedFacts.clear();
|
|
47
|
+
for (const vendor of Object.keys(installed))
|
|
48
|
+
installedFacts.add(vendor);
|
|
43
49
|
walkHosts = createHostTable(walkFacts, Object.keys(installed)).hosts;
|
|
44
50
|
shapes.clear();
|
|
45
51
|
fieldsByVendor.clear();
|
|
52
|
+
directories.clear();
|
|
46
53
|
}
|
|
47
54
|
const packsDir = () => { if (packsRoot === undefined)
|
|
48
55
|
throw new Error('usePacks(<packs dir>) first'); return packsRoot; };
|
|
56
|
+
function directoryFor(vendor) {
|
|
57
|
+
if (!directories.has(vendor)) {
|
|
58
|
+
const directory = walkPackDirectory(packsDir(), vendor);
|
|
59
|
+
directories.set(vendor, directory);
|
|
60
|
+
if (directory) {
|
|
61
|
+
const name = vendor.split('/')[0];
|
|
62
|
+
const root = vendor.includes('/') ? join(directory, '..') : directory;
|
|
63
|
+
const facts = join(root, 'generated', 'pack-facts.json');
|
|
64
|
+
if (existsSync(facts)) {
|
|
65
|
+
const document = JSON.parse(readFileSync(facts, 'utf8'));
|
|
66
|
+
walkFacts = { ...walkFacts, ...document.packs };
|
|
67
|
+
installedFacts.add(name);
|
|
68
|
+
walkHosts = createHostTable(walkFacts, [...installedFacts]).hosts;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return directories.get(vendor) ?? join(packsDir(), vendor);
|
|
73
|
+
}
|
|
49
74
|
/** The walk's World: the twin's own code on its in-memory World and blob stores (the backends a serverless
|
|
50
75
|
* entry runs on), so a walk of any length is state changes in memory and never touches the disk; the World
|
|
51
76
|
* clock, pinned to 2026-01-01, is world state in the same store, and the World is sealed. Each root is a fresh key
|
|
@@ -97,10 +122,11 @@ function defaultType(body) {
|
|
|
97
122
|
const vendorFetch = (vendor) => vendorFetchFrom(packsDir(), vendor);
|
|
98
123
|
const shapes = new Map();
|
|
99
124
|
const shapeOf = (vendor) => { if (!shapes.has(vendor))
|
|
100
|
-
shapes.set(vendor, shapeChecker(
|
|
125
|
+
shapes.set(vendor, shapeChecker(directoryFor(vendor))); return shapes.get(vendor); };
|
|
101
126
|
/** The routing keys a pack answers under: its descriptor's host rules (pack-facts), each rule's `key` or the vendor's own
|
|
102
127
|
* name, as vendor-hosts.cjs compiles them. */
|
|
103
128
|
function routingKeys(vendor) {
|
|
129
|
+
directoryFor(vendor);
|
|
104
130
|
const name = vendor.split('/')[0];
|
|
105
131
|
const hosts = walkFacts[name]?.hosts ?? [];
|
|
106
132
|
return [...new Set(hosts.map((rule) => rule.key ?? name))].filter((key) => walkHosts[key] !== undefined);
|
|
@@ -151,7 +177,8 @@ const fieldsByVendor = new Map();
|
|
|
151
177
|
function specFields(vendor) {
|
|
152
178
|
if (!fieldsByVendor.has(vendor)) {
|
|
153
179
|
// the pack's surface and each of its lanes' (a walk reaches a lane through the pack's router)
|
|
154
|
-
const
|
|
180
|
+
const directory = directoryFor(vendor);
|
|
181
|
+
const files = [directory, ...lanesOf(directory)].map((dir) => join(dir, 'src', 'generated', 'surface.gen.json')).filter((f) => existsSync(f));
|
|
155
182
|
const surfaces = files.map((f) => parseExactJson(readFileSync(f, 'utf8')));
|
|
156
183
|
fieldsByVendor.set(vendor, new Set(surfaces.flatMap((surface) => (surface.resources ?? []).flatMap((r) => r.fields.map((f) => f.name)))));
|
|
157
184
|
}
|
|
@@ -213,7 +240,7 @@ export async function walk(journey, root, collect, opts) {
|
|
|
213
240
|
const companionOf = (url) => (claims(ownKeys, url) ? undefined : companions.find((c) => claims(c.keys, url)));
|
|
214
241
|
// the life's scenario is the World's: written into the walk's World store, which the pack reads it from as a World's
|
|
215
242
|
// handlers/<vendor>.json is read
|
|
216
|
-
const scenarioPath = journey.scenario ? join(
|
|
243
|
+
const scenarioPath = journey.scenario ? join(directoryFor(journey.vendor), 'journeys', journey.scenario) : undefined;
|
|
217
244
|
if (scenarioPath)
|
|
218
245
|
getActiveWorldStore().write(scenarioPath, readFileSync(scenarioPath, 'utf8'));
|
|
219
246
|
// the application the vendor asks (ctx.ask) is the life's stand-in, this walk's fetch's alone: its answers, in order,
|
|
@@ -311,7 +338,7 @@ export async function walk(journey, root, collect, opts) {
|
|
|
311
338
|
// replies each line drew; its answer is checked as a request's is (status, SHAPE, captures, expectations).
|
|
312
339
|
const connections = new Map();
|
|
313
340
|
let streams;
|
|
314
|
-
const lines = lineShape(
|
|
341
|
+
const lines = lineShape(directoryFor(journey.vendor));
|
|
315
342
|
const encoder = new TextEncoder();
|
|
316
343
|
const decoder = new TextDecoder();
|
|
317
344
|
const speak = async (step) => {
|
|
@@ -953,7 +980,7 @@ export async function walk(journey, root, collect, opts) {
|
|
|
953
980
|
}
|
|
954
981
|
}
|
|
955
982
|
const serving = companionOf(path)?.vendor ?? journey.vendor;
|
|
956
|
-
const graphqlBody = existsSync(join(
|
|
983
|
+
const graphqlBody = existsSync(join(directoryFor(serving), 'src', 'generated', 'graphql-sdl.gen.json'));
|
|
957
984
|
let body;
|
|
958
985
|
let headers;
|
|
959
986
|
// a form's bytes, built before anything signs the request, so a signature covers what is sent
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function walkPackDirectory(from: string, vendor: string): string | undefined;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Source siblings and installed companions use the kernel's existing package identity and selection rules.
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { installedVendorPacks, isDerivedPack, packVendor } from '@volter/world-core/runtime';
|
|
5
|
+
export function walkPackDirectory(from, vendor) {
|
|
6
|
+
if (!/^[a-z][a-z0-9-]*(?:\/[a-z][a-z0-9-]*)?$/.test(vendor))
|
|
7
|
+
return undefined;
|
|
8
|
+
const [name, lane] = vendor.split('/');
|
|
9
|
+
const sibling = join(from, name);
|
|
10
|
+
const root = isDerivedPack(sibling) && packVendor(sibling) === name
|
|
11
|
+
? sibling
|
|
12
|
+
: installedVendorPacks(from, { vendors: [name] }).get(name);
|
|
13
|
+
const unit = root && (lane ? join(root, lane) : root);
|
|
14
|
+
return unit && existsSync(join(unit, 'src', 'index.ts')) ? unit : undefined;
|
|
15
|
+
}
|
|
@@ -12,10 +12,14 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
12
12
|
import { join } from 'node:path';
|
|
13
13
|
import { startPgliteHost } from "./walk-database.js";
|
|
14
14
|
import { worldNow } from '@volter/world-core/runtime';
|
|
15
|
+
import { walkPackDirectory } from "./pack-directory.js";
|
|
15
16
|
export async function vendorFetch(twinDir, vendor) {
|
|
17
|
+
const directory = walkPackDirectory(twinDir, vendor);
|
|
18
|
+
if (!directory)
|
|
19
|
+
return undefined;
|
|
16
20
|
let mod;
|
|
17
21
|
try {
|
|
18
|
-
mod = (await import(__rewriteRelativeImportExtension(join(
|
|
22
|
+
mod = (await import(__rewriteRelativeImportExtension(join(directory, 'src', 'index.ts'))));
|
|
19
23
|
}
|
|
20
24
|
catch {
|
|
21
25
|
return undefined;
|
|
@@ -57,16 +61,21 @@ const jwtView = (field) => (body) => {
|
|
|
57
61
|
};
|
|
58
62
|
export async function vendorAnswerViews(twinDir, vendor) {
|
|
59
63
|
let own = {};
|
|
64
|
+
const directory = walkPackDirectory(twinDir, vendor);
|
|
60
65
|
try {
|
|
61
|
-
|
|
66
|
+
if (directory)
|
|
67
|
+
own = (await import(__rewriteRelativeImportExtension(join(directory, 'src', 'index.ts')))).answerViews ?? {};
|
|
62
68
|
}
|
|
63
69
|
catch { /* none of its own */ }
|
|
64
70
|
return new Proxy(own, { get: (views, name) => (typeof name === 'string' && name.startsWith('jwt:') ? jwtView(name.slice(4)) : views[name]) });
|
|
65
71
|
}
|
|
66
72
|
export async function vendorStream(twinDir, vendor) {
|
|
73
|
+
const directory = walkPackDirectory(twinDir, vendor);
|
|
74
|
+
if (!directory)
|
|
75
|
+
return undefined;
|
|
67
76
|
let mod;
|
|
68
77
|
try {
|
|
69
|
-
mod = (await import(__rewriteRelativeImportExtension(join(
|
|
78
|
+
mod = (await import(__rewriteRelativeImportExtension(join(directory, 'src', 'index.ts'))));
|
|
70
79
|
}
|
|
71
80
|
catch {
|
|
72
81
|
return undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/twin-standard",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.86",
|
|
4
4
|
"description": "The Protocol 3 standard for Volter twin packs: the criteria a pack is graded by, the grader, the gate a pack repository and the twin catalog run on every change and submission, and the spec derivation the grade checks against. Pin a version: every place a pack is judged judges by that standard.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twin",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"yaml": "2.9.0"
|
|
76
76
|
},
|
|
77
77
|
"peerDependencies": {
|
|
78
|
-
"@volter/world-core": "3.0.
|
|
78
|
+
"@volter/world-core": "3.0.94",
|
|
79
79
|
"@volter/world-runtime": "*",
|
|
80
80
|
"playwright": "^1.63.0"
|
|
81
81
|
},
|
|
@@ -19,6 +19,7 @@ import { createHostTable, type HostFacts } from '@volter/world-core/host-routing
|
|
|
19
19
|
import packFactsDocument from '@volter/world-core/generated/pack-facts.json' with { type: 'json' };
|
|
20
20
|
import { type Captured, type GitStep, type Journey, type JourneyStep, type LineStep, type SocketStep, type SqlStep, type RespStep, type MysqlStep, isMysql, isResp, at, captureAt, echoFailures, expandFills, expectFailures, fill, fillJson, formBody, isGit, isLine, isSql, isSocket, isWait, jwtAuthorization, oauth1Authorization, presignedPath, sigv4Headers, waitMs } from './journey-kit.ts';
|
|
21
21
|
import { lanesOf, lineShape, shapeChecker, type LineConnection, type ShapeChecker } from './shape.ts';
|
|
22
|
+
import { walkPackDirectory } from './pack-directory.ts';
|
|
22
23
|
import { xmlMembers } from './xml-view.ts';
|
|
23
24
|
|
|
24
25
|
/** The pack repository the walk finds a vendor's pack in, set by the caller before its first walk. */
|
|
@@ -33,6 +34,8 @@ function socketAt(open: string, host: string): { host: string; path: string } {
|
|
|
33
34
|
const baselineFacts = packFactsDocument.packs as unknown as HostFacts;
|
|
34
35
|
let walkFacts: HostFacts = baselineFacts;
|
|
35
36
|
let walkHosts = createHostTable(walkFacts).hosts;
|
|
37
|
+
const directories = new Map<string, string | undefined>();
|
|
38
|
+
const installedFacts = new Set<string>();
|
|
36
39
|
export function usePacks(dir: string): void {
|
|
37
40
|
packsRoot = dir;
|
|
38
41
|
const installed: Record<string, HostFacts[string]> = {};
|
|
@@ -41,10 +44,31 @@ export function usePacks(dir: string): void {
|
|
|
41
44
|
if (existsSync(file)) Object.assign(installed, JSON.parse(readFileSync(file, 'utf8')).packs);
|
|
42
45
|
}
|
|
43
46
|
walkFacts = { ...baselineFacts, ...installed };
|
|
47
|
+
installedFacts.clear();
|
|
48
|
+
for (const vendor of Object.keys(installed)) installedFacts.add(vendor);
|
|
44
49
|
walkHosts = createHostTable(walkFacts, Object.keys(installed)).hosts;
|
|
45
50
|
shapes.clear(); fieldsByVendor.clear();
|
|
51
|
+
directories.clear();
|
|
46
52
|
}
|
|
47
53
|
const packsDir = (): string => { if (packsRoot === undefined) throw new Error('usePacks(<packs dir>) first'); return packsRoot; };
|
|
54
|
+
function directoryFor(vendor: string): string {
|
|
55
|
+
if (!directories.has(vendor)) {
|
|
56
|
+
const directory = walkPackDirectory(packsDir(), vendor);
|
|
57
|
+
directories.set(vendor, directory);
|
|
58
|
+
if (directory) {
|
|
59
|
+
const name = vendor.split('/')[0]!;
|
|
60
|
+
const root = vendor.includes('/') ? join(directory, '..') : directory;
|
|
61
|
+
const facts = join(root, 'generated', 'pack-facts.json');
|
|
62
|
+
if (existsSync(facts)) {
|
|
63
|
+
const document = JSON.parse(readFileSync(facts, 'utf8')) as { packs: HostFacts };
|
|
64
|
+
walkFacts = { ...walkFacts, ...document.packs };
|
|
65
|
+
installedFacts.add(name);
|
|
66
|
+
walkHosts = createHostTable(walkFacts, [...installedFacts]).hosts;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return directories.get(vendor) ?? join(packsDir(), vendor);
|
|
71
|
+
}
|
|
48
72
|
|
|
49
73
|
/** The walk's World: the twin's own code on its in-memory World and blob stores (the backends a serverless
|
|
50
74
|
* entry runs on), so a walk of any length is state changes in memory and never touches the disk; the World
|
|
@@ -85,11 +109,12 @@ function defaultType(body: unknown): string {
|
|
|
85
109
|
}
|
|
86
110
|
const vendorFetch = (vendor: string) => vendorFetchFrom(packsDir(), vendor);
|
|
87
111
|
const shapes = new Map<string, Promise<ShapeChecker | undefined>>();
|
|
88
|
-
const shapeOf = (vendor: string): Promise<ShapeChecker | undefined> => { if (!shapes.has(vendor)) shapes.set(vendor, shapeChecker(
|
|
112
|
+
const shapeOf = (vendor: string): Promise<ShapeChecker | undefined> => { if (!shapes.has(vendor)) shapes.set(vendor, shapeChecker(directoryFor(vendor))); return shapes.get(vendor)!; };
|
|
89
113
|
|
|
90
114
|
/** The routing keys a pack answers under: its descriptor's host rules (pack-facts), each rule's `key` or the vendor's own
|
|
91
115
|
* name, as vendor-hosts.cjs compiles them. */
|
|
92
116
|
function routingKeys(vendor: string): string[] {
|
|
117
|
+
directoryFor(vendor);
|
|
93
118
|
const name = vendor.split('/')[0]!;
|
|
94
119
|
const hosts = walkFacts[name]?.hosts ?? [];
|
|
95
120
|
return [...new Set(hosts.map((rule) => rule.key ?? name))].filter((key) => walkHosts[key] !== undefined);
|
|
@@ -132,7 +157,8 @@ const fieldsByVendor = new Map<string, Set<string>>();
|
|
|
132
157
|
function specFields(vendor: string): Set<string> {
|
|
133
158
|
if (!fieldsByVendor.has(vendor)) {
|
|
134
159
|
// the pack's surface and each of its lanes' (a walk reaches a lane through the pack's router)
|
|
135
|
-
const
|
|
160
|
+
const directory = directoryFor(vendor);
|
|
161
|
+
const files = [directory, ...lanesOf(directory)].map((dir) => join(dir, 'src', 'generated', 'surface.gen.json')).filter((f) => existsSync(f));
|
|
136
162
|
const surfaces = files.map((f) => parseExactJson(readFileSync(f, 'utf8')) as { resources?: Array<{ fields: Array<{ name: string }> }> });
|
|
137
163
|
fieldsByVendor.set(vendor, new Set(surfaces.flatMap((surface) => (surface.resources ?? []).flatMap((r) => r.fields.map((f) => f.name)))));
|
|
138
164
|
}
|
|
@@ -200,7 +226,7 @@ export async function walk(journey: Journey, root: string, collect: Failure[], o
|
|
|
200
226
|
const companionOf = (url: string) => (claims(ownKeys, url) ? undefined : companions.find((c) => claims(c.keys, url)));
|
|
201
227
|
// the life's scenario is the World's: written into the walk's World store, which the pack reads it from as a World's
|
|
202
228
|
// handlers/<vendor>.json is read
|
|
203
|
-
const scenarioPath = journey.scenario ? join(
|
|
229
|
+
const scenarioPath = journey.scenario ? join(directoryFor(journey.vendor), 'journeys', journey.scenario) : undefined;
|
|
204
230
|
if (scenarioPath) getActiveWorldStore().write(scenarioPath, readFileSync(scenarioPath, 'utf8'));
|
|
205
231
|
// the application the vendor asks (ctx.ask) is the life's stand-in, this walk's fetch's alone: its answers, in order,
|
|
206
232
|
// by method and URL prefix, each request header it declares checked as sent; a question none takes is unreachable, as
|
|
@@ -276,7 +302,7 @@ export async function walk(journey: Journey, root: string, collect: Failure[], o
|
|
|
276
302
|
// replies each line drew; its answer is checked as a request's is (status, SHAPE, captures, expectations).
|
|
277
303
|
const connections = new Map<string, Connection>();
|
|
278
304
|
let streams: VendorStreamFactory | undefined;
|
|
279
|
-
const lines = lineShape(
|
|
305
|
+
const lines = lineShape(directoryFor(journey.vendor));
|
|
280
306
|
const encoder = new TextEncoder();
|
|
281
307
|
const decoder = new TextDecoder();
|
|
282
308
|
const speak = async (step: LineStep): Promise<void> => {
|
|
@@ -662,7 +688,7 @@ export async function walk(journey: Journey, root: string, collect: Failure[], o
|
|
|
662
688
|
// a presigned URL is signed again with the key that presigned it, as its client's SDK signed it
|
|
663
689
|
if (step.presign) { try { path = presignedPath(step.presign, captured, step.method, path, host, fillJson({ ...(step.bodyBase64 === undefined ? { 'content-type': defaultType(step.body) } : {}), ...(journey.headers ?? {}), ...(step.headers ?? {}) }, captured) as Record<string, string>); } catch (e) { collect.push({ step: step.name, beat: 'CAPTURE', detail: (e as Error).message }); continue; } }
|
|
664
690
|
const serving = companionOf(path)?.vendor ?? journey.vendor;
|
|
665
|
-
const graphqlBody = existsSync(join(
|
|
691
|
+
const graphqlBody = existsSync(join(directoryFor(serving), 'src', 'generated', 'graphql-sdl.gen.json'));
|
|
666
692
|
let body: unknown;
|
|
667
693
|
let headers: Record<string, string> | undefined;
|
|
668
694
|
// a form's bytes, built before anything signs the request, so a signature covers what is sent
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Source siblings and installed companions use the kernel's existing package identity and selection rules.
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { installedVendorPacks, isDerivedPack, packVendor } from '@volter/world-core/runtime';
|
|
5
|
+
|
|
6
|
+
export function walkPackDirectory(from: string, vendor: string): string | undefined {
|
|
7
|
+
if (!/^[a-z][a-z0-9-]*(?:\/[a-z][a-z0-9-]*)?$/.test(vendor)) return undefined;
|
|
8
|
+
const [name, lane] = vendor.split('/');
|
|
9
|
+
const sibling = join(from, name!);
|
|
10
|
+
const root = isDerivedPack(sibling) && packVendor(sibling) === name
|
|
11
|
+
? sibling
|
|
12
|
+
: installedVendorPacks(from, { vendors: [name!] }).get(name!);
|
|
13
|
+
const unit = root && (lane ? join(root, lane) : root);
|
|
14
|
+
return unit && existsSync(join(unit, 'src', 'index.ts')) ? unit : undefined;
|
|
15
|
+
}
|
package/src/walk/vendor-fetch.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { join } from 'node:path';
|
|
|
5
5
|
import { type PgliteHost, startPgliteHost } from './walk-database.ts';
|
|
6
6
|
import { type TwinStream, worldNow } from '@volter/world-core/runtime';
|
|
7
7
|
import type { PackFetchOptions } from '@volter/world-core';
|
|
8
|
+
import { walkPackDirectory } from './pack-directory.ts';
|
|
8
9
|
|
|
9
10
|
type ApplicationStandIn = NonNullable<PackFetchOptions['application']>;
|
|
10
11
|
|
|
@@ -13,8 +14,10 @@ type ApplicationStandIn = NonNullable<PackFetchOptions['application']>;
|
|
|
13
14
|
export type VendorFetchFactory = (root: string, options?: { scenarioPath?: string; application?: ApplicationStandIn }) => (request: Request) => Response | Promise<Response>;
|
|
14
15
|
|
|
15
16
|
export async function vendorFetch(twinDir: string, vendor: string): Promise<VendorFetchFactory | undefined> {
|
|
17
|
+
const directory = walkPackDirectory(twinDir, vendor);
|
|
18
|
+
if (!directory) return undefined;
|
|
16
19
|
let mod: Record<string, unknown>;
|
|
17
|
-
try { mod = (await import(join(
|
|
20
|
+
try { mod = (await import(join(directory, 'src', 'index.ts'))) as Record<string, unknown>; } catch { return undefined; }
|
|
18
21
|
// a pack exports create<Name>Fetch (its generated fetch.ts)
|
|
19
22
|
const name = Object.keys(mod).find((k) => /^create\w+?Fetch$/.test(k) && !/TwinFetch$/.test(k) && typeof mod[k] === 'function');
|
|
20
23
|
if (!name) return undefined;
|
|
@@ -51,8 +54,9 @@ const jwtView = (field: string) => (body: unknown): unknown => {
|
|
|
51
54
|
|
|
52
55
|
export async function vendorAnswerViews(twinDir: string, vendor: string): Promise<AnswerViews> {
|
|
53
56
|
let own: AnswerViews = {};
|
|
57
|
+
const directory = walkPackDirectory(twinDir, vendor);
|
|
54
58
|
try {
|
|
55
|
-
own = ((await import(join(
|
|
59
|
+
if (directory) own = ((await import(join(directory, 'src', 'index.ts'))) as { answerViews?: AnswerViews }).answerViews ?? {};
|
|
56
60
|
} catch { /* none of its own */ }
|
|
57
61
|
return new Proxy(own, { get: (views, name) => (typeof name === 'string' && name.startsWith('jwt:') ? jwtView(name.slice(4)) : views[name as string]) });
|
|
58
62
|
}
|
|
@@ -62,8 +66,10 @@ export async function vendorAnswerViews(twinDir: string, vendor: string): Promis
|
|
|
62
66
|
export type VendorStreamFactory = (root: string, options?: Record<string, unknown>) => TwinStream;
|
|
63
67
|
|
|
64
68
|
export async function vendorStream(twinDir: string, vendor: string): Promise<VendorStreamFactory | undefined> {
|
|
69
|
+
const directory = walkPackDirectory(twinDir, vendor);
|
|
70
|
+
if (!directory) return undefined;
|
|
65
71
|
let mod: Record<string, unknown>;
|
|
66
|
-
try { mod = (await import(join(
|
|
72
|
+
try { mod = (await import(join(directory, 'src', 'index.ts'))) as Record<string, unknown>; } catch { return undefined; }
|
|
67
73
|
const name = Object.keys(mod).find((k) => /^create\w+TwinStream$/.test(k) && typeof mod[k] === 'function');
|
|
68
74
|
if (!name) return undefined;
|
|
69
75
|
const factory = mod[name] as (options: Record<string, unknown>) => TwinStream;
|