sounding 0.0.3 → 0.1.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 +336 -1
- package/bin/sounding.js +156 -0
- package/index.js +23 -0
- package/lib/create-app-manager.js +380 -26
- package/lib/create-auth-helpers.js +168 -21
- package/lib/create-browser-manager.js +578 -31
- package/lib/create-error.js +35 -0
- package/lib/create-expect.js +1070 -27
- package/lib/create-helper-runner.js +38 -2
- package/lib/create-mail-capture.js +174 -25
- package/lib/create-mailbox.js +20 -0
- package/lib/create-request-client.js +635 -57
- package/lib/create-runtime.js +222 -21
- package/lib/create-socket-manager.js +706 -0
- package/lib/create-test-api.js +491 -102
- package/lib/create-visit-client.js +40 -2
- package/lib/create-world-engine.js +106 -7
- package/lib/create-world-loader.js +150 -8
- package/lib/default-config.js +26 -0
- package/lib/define-world.js +27 -2
- package/lib/init-project.js +403 -0
- package/lib/merge-config.js +11 -0
- package/lib/normalize-config.js +16 -19
- package/lib/resolve-auth-config.js +36 -0
- package/lib/resolve-datastore.js +50 -7
- package/lib/resolve-dependency.js +145 -0
- package/lib/test-runner.js +427 -0
- package/lib/trial-context.js +29 -0
- package/lib/types.js +675 -0
- package/lib/validate-config.js +633 -0
- package/lib/validate-test-args.js +480 -0
- package/package.json +16 -2
|
@@ -1,13 +1,30 @@
|
|
|
1
|
+
const { createSoundingError } = require('./create-error')
|
|
2
|
+
|
|
1
3
|
const DEFAULT_HEADERS = {
|
|
2
4
|
'x-inertia': 'true',
|
|
3
5
|
'x-requested-with': 'XMLHttpRequest',
|
|
4
6
|
accept: 'text/html, application/xhtml+xml',
|
|
5
7
|
}
|
|
6
8
|
|
|
9
|
+
/** @typedef {import('./types').AnyRecord} AnyRecord */
|
|
10
|
+
/** @typedef {import('./types').SoundingRequestClient} SoundingRequestClient */
|
|
11
|
+
/** @typedef {import('./types').SoundingRequestOptions} SoundingRequestOptions */
|
|
12
|
+
/** @typedef {import('./types').SoundingTransport} SoundingTransport */
|
|
13
|
+
/** @typedef {import('./types').SoundingVisitClient} SoundingVisitClient */
|
|
14
|
+
/** @typedef {import('./types').SoundingVisitOptions} SoundingVisitOptions */
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string | string[]} value
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
7
20
|
function joinHeaderValue(value) {
|
|
8
21
|
return Array.isArray(value) ? value.join(',') : value
|
|
9
22
|
}
|
|
10
23
|
|
|
24
|
+
/**
|
|
25
|
+
* @param {SoundingVisitOptions} [options]
|
|
26
|
+
* @returns {AnyRecord}
|
|
27
|
+
*/
|
|
11
28
|
function buildVisitHeaders(options = {}) {
|
|
12
29
|
const headers = {
|
|
13
30
|
...(options.headers || {}),
|
|
@@ -23,7 +40,13 @@ function buildVisitHeaders(options = {}) {
|
|
|
23
40
|
|
|
24
41
|
if (options.only?.length) {
|
|
25
42
|
if (!options.component) {
|
|
26
|
-
throw
|
|
43
|
+
throw createSoundingError({
|
|
44
|
+
code: 'E_SOUNDING_VISIT_COMPONENT_REQUIRED',
|
|
45
|
+
message: 'Sounding visit() requires `component` when using `only`.',
|
|
46
|
+
details: {
|
|
47
|
+
partialReload: 'only',
|
|
48
|
+
},
|
|
49
|
+
})
|
|
27
50
|
}
|
|
28
51
|
|
|
29
52
|
headers['x-inertia-partial-component'] = options.component
|
|
@@ -32,7 +55,13 @@ function buildVisitHeaders(options = {}) {
|
|
|
32
55
|
|
|
33
56
|
if (options.except?.length) {
|
|
34
57
|
if (!options.component) {
|
|
35
|
-
throw
|
|
58
|
+
throw createSoundingError({
|
|
59
|
+
code: 'E_SOUNDING_VISIT_COMPONENT_REQUIRED',
|
|
60
|
+
message: 'Sounding visit() requires `component` when using `except`.',
|
|
61
|
+
details: {
|
|
62
|
+
partialReload: 'except',
|
|
63
|
+
},
|
|
64
|
+
})
|
|
36
65
|
}
|
|
37
66
|
|
|
38
67
|
headers['x-inertia-partial-component'] = options.component
|
|
@@ -46,6 +75,10 @@ function buildVisitHeaders(options = {}) {
|
|
|
46
75
|
return headers
|
|
47
76
|
}
|
|
48
77
|
|
|
78
|
+
/**
|
|
79
|
+
* @param {SoundingVisitOptions} [options]
|
|
80
|
+
* @returns {SoundingRequestOptions}
|
|
81
|
+
*/
|
|
49
82
|
function buildRequestOptions(options = {}) {
|
|
50
83
|
const {
|
|
51
84
|
component,
|
|
@@ -78,6 +111,10 @@ function buildRequestOptions(options = {}) {
|
|
|
78
111
|
return output
|
|
79
112
|
}
|
|
80
113
|
|
|
114
|
+
/**
|
|
115
|
+
* @param {{ request: SoundingRequestClient }} input
|
|
116
|
+
* @returns {SoundingVisitClient}
|
|
117
|
+
*/
|
|
81
118
|
function createVisitClient({ request }) {
|
|
82
119
|
const client = request.withHeaders(DEFAULT_HEADERS)
|
|
83
120
|
|
|
@@ -95,6 +132,7 @@ function createVisitClient({ request }) {
|
|
|
95
132
|
client.delete(target, payload, buildRequestOptions(options))
|
|
96
133
|
visit.del = visit.delete
|
|
97
134
|
visit.using = (transport) => createVisitClient({ request: request.using(transport) })
|
|
135
|
+
visit.as = (actor) => createVisitClient({ request: request.as(actor) })
|
|
98
136
|
|
|
99
137
|
Object.defineProperty(visit, 'transport', {
|
|
100
138
|
enumerable: true,
|
|
@@ -2,10 +2,27 @@ const {
|
|
|
2
2
|
isFactoryDefinition,
|
|
3
3
|
isScenarioDefinition,
|
|
4
4
|
} = require('./define-world')
|
|
5
|
-
|
|
5
|
+
const { createSoundingError } = require('./create-error')
|
|
6
|
+
|
|
7
|
+
/** @typedef {import('./types').AnyRecord} AnyRecord */
|
|
8
|
+
/** @typedef {import('./types').SoundingBuilder} SoundingBuilder */
|
|
9
|
+
/** @typedef {import('./types').SoundingFactoryDefinition} SoundingFactoryDefinition */
|
|
10
|
+
/** @typedef {import('./types').SoundingFactoryRegistration} SoundingFactoryRegistration */
|
|
11
|
+
/** @typedef {import('./types').SoundingSailsApp} SoundingSailsApp */
|
|
12
|
+
/** @typedef {import('./types').SoundingScenarioDefinition} SoundingScenarioDefinition */
|
|
13
|
+
/** @typedef {import('./types').SoundingWorldEngine} SoundingWorldEngine */
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {AnyRecord} base
|
|
17
|
+
* @param {AnyRecord | ((base: AnyRecord) => AnyRecord)} patch
|
|
18
|
+
* @returns {AnyRecord}
|
|
19
|
+
*/
|
|
6
20
|
function mergeValue(base, patch) {
|
|
7
21
|
if (typeof patch === 'function') {
|
|
8
|
-
return
|
|
22
|
+
return {
|
|
23
|
+
...base,
|
|
24
|
+
...(patch(base) || {}),
|
|
25
|
+
}
|
|
9
26
|
}
|
|
10
27
|
|
|
11
28
|
return {
|
|
@@ -14,6 +31,10 @@ function mergeValue(base, patch) {
|
|
|
14
31
|
}
|
|
15
32
|
}
|
|
16
33
|
|
|
34
|
+
/**
|
|
35
|
+
* @param {{ sequence: Function }} input
|
|
36
|
+
* @returns {import('./types').SoundingFactoryHelpers['fake']}
|
|
37
|
+
*/
|
|
17
38
|
function createFakeHelpers({ sequence }) {
|
|
18
39
|
return {
|
|
19
40
|
person: {
|
|
@@ -37,6 +58,11 @@ function createFakeHelpers({ sequence }) {
|
|
|
37
58
|
}
|
|
38
59
|
}
|
|
39
60
|
|
|
61
|
+
/**
|
|
62
|
+
* @param {(overrides: AnyRecord, options: { traits: string[] }) => any} executor
|
|
63
|
+
* @param {AnyRecord} [initialOverrides]
|
|
64
|
+
* @returns {SoundingBuilder}
|
|
65
|
+
*/
|
|
40
66
|
function createThenableBuilder(executor, initialOverrides = {}) {
|
|
41
67
|
const state = {
|
|
42
68
|
overrides: initialOverrides,
|
|
@@ -61,6 +87,14 @@ function createThenableBuilder(executor, initialOverrides = {}) {
|
|
|
61
87
|
},
|
|
62
88
|
|
|
63
89
|
with(overrides = {}) {
|
|
90
|
+
state.overrides = {
|
|
91
|
+
...state.overrides,
|
|
92
|
+
...overrides,
|
|
93
|
+
}
|
|
94
|
+
return builder
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
withOnly(overrides = {}) {
|
|
64
98
|
state.overrides = overrides
|
|
65
99
|
return builder
|
|
66
100
|
},
|
|
@@ -85,6 +119,18 @@ function createThenableBuilder(executor, initialOverrides = {}) {
|
|
|
85
119
|
return builder
|
|
86
120
|
}
|
|
87
121
|
|
|
122
|
+
/**
|
|
123
|
+
* @param {string[]} values
|
|
124
|
+
* @returns {string}
|
|
125
|
+
*/
|
|
126
|
+
function formatAvailable(values) {
|
|
127
|
+
return values.length ? values.join(', ') : 'none'
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @param {{ sails?: SoundingSailsApp }} input
|
|
132
|
+
* @returns {SoundingWorldEngine}
|
|
133
|
+
*/
|
|
88
134
|
function createWorldEngine({ sails }) {
|
|
89
135
|
const factories = new Map()
|
|
90
136
|
const scenarios = new Map()
|
|
@@ -104,7 +150,15 @@ function createWorldEngine({ sails }) {
|
|
|
104
150
|
const entry = factories.get(name)
|
|
105
151
|
|
|
106
152
|
if (!entry) {
|
|
107
|
-
|
|
153
|
+
const availableFactories = Array.from(factories.keys()).sort()
|
|
154
|
+
throw createSoundingError({
|
|
155
|
+
code: 'E_SOUNDING_WORLD_FACTORY_UNKNOWN',
|
|
156
|
+
message: `Unknown Sounding factory: ${name}. Available factories: ${formatAvailable(availableFactories)}.`,
|
|
157
|
+
details: {
|
|
158
|
+
factory: name,
|
|
159
|
+
availableFactories,
|
|
160
|
+
},
|
|
161
|
+
})
|
|
108
162
|
}
|
|
109
163
|
|
|
110
164
|
return entry
|
|
@@ -126,7 +180,16 @@ function createWorldEngine({ sails }) {
|
|
|
126
180
|
|
|
127
181
|
for (const traitName of options.traits || []) {
|
|
128
182
|
if (!entry.traits.has(traitName)) {
|
|
129
|
-
|
|
183
|
+
const availableTraits = Array.from(entry.traits.keys()).sort()
|
|
184
|
+
throw createSoundingError({
|
|
185
|
+
code: 'E_SOUNDING_WORLD_TRAIT_UNKNOWN',
|
|
186
|
+
message: `Unknown Sounding trait \`${traitName}\` for factory \`${name}\`. Available traits for \`${name}\`: ${formatAvailable(availableTraits)}.`,
|
|
187
|
+
details: {
|
|
188
|
+
factory: name,
|
|
189
|
+
trait: traitName,
|
|
190
|
+
availableTraits,
|
|
191
|
+
},
|
|
192
|
+
})
|
|
130
193
|
}
|
|
131
194
|
|
|
132
195
|
value = mergeValue(value, entry.traits.get(traitName))
|
|
@@ -150,6 +213,10 @@ function createWorldEngine({ sails }) {
|
|
|
150
213
|
return value
|
|
151
214
|
}
|
|
152
215
|
|
|
216
|
+
/**
|
|
217
|
+
* @param {any} entry
|
|
218
|
+
* @returns {SoundingFactoryRegistration}
|
|
219
|
+
*/
|
|
153
220
|
function registerFactoryDefinition(entry) {
|
|
154
221
|
const nextEntry = {
|
|
155
222
|
definition: entry.definition,
|
|
@@ -166,11 +233,20 @@ function createWorldEngine({ sails }) {
|
|
|
166
233
|
}
|
|
167
234
|
}
|
|
168
235
|
|
|
236
|
+
/**
|
|
237
|
+
* @param {any} entry
|
|
238
|
+
* @returns {SoundingScenarioDefinition}
|
|
239
|
+
*/
|
|
169
240
|
function registerScenarioDefinition(entry) {
|
|
170
241
|
scenarios.set(entry.name, entry.definition)
|
|
171
242
|
return entry
|
|
172
243
|
}
|
|
173
244
|
|
|
245
|
+
/**
|
|
246
|
+
* @param {string | any} nameOrEntry
|
|
247
|
+
* @param {any} [definition]
|
|
248
|
+
* @returns {SoundingFactoryRegistration}
|
|
249
|
+
*/
|
|
174
250
|
function defineFactory(nameOrEntry, definition) {
|
|
175
251
|
if (isFactoryDefinition(nameOrEntry)) {
|
|
176
252
|
return registerFactoryDefinition(nameOrEntry)
|
|
@@ -185,6 +261,11 @@ function createWorldEngine({ sails }) {
|
|
|
185
261
|
return registerFactoryDefinition(entry)
|
|
186
262
|
}
|
|
187
263
|
|
|
264
|
+
/**
|
|
265
|
+
* @param {string | any} nameOrEntry
|
|
266
|
+
* @param {any} [definition]
|
|
267
|
+
* @returns {SoundingScenarioDefinition}
|
|
268
|
+
*/
|
|
188
269
|
function defineScenario(nameOrEntry, definition) {
|
|
189
270
|
if (isScenarioDefinition(nameOrEntry)) {
|
|
190
271
|
return registerScenarioDefinition(nameOrEntry)
|
|
@@ -200,7 +281,15 @@ function createWorldEngine({ sails }) {
|
|
|
200
281
|
const definition = scenarios.get(name)
|
|
201
282
|
|
|
202
283
|
if (!definition) {
|
|
203
|
-
|
|
284
|
+
const availableScenarios = Array.from(scenarios.keys()).sort()
|
|
285
|
+
throw createSoundingError({
|
|
286
|
+
code: 'E_SOUNDING_WORLD_SCENARIO_UNKNOWN',
|
|
287
|
+
message: `Unknown Sounding scenario: ${name}. Available scenarios: ${formatAvailable(availableScenarios)}.`,
|
|
288
|
+
details: {
|
|
289
|
+
scenario: name,
|
|
290
|
+
availableScenarios,
|
|
291
|
+
},
|
|
292
|
+
})
|
|
204
293
|
}
|
|
205
294
|
|
|
206
295
|
currentWorld = await definition({
|
|
@@ -237,7 +326,14 @@ function createWorldEngine({ sails }) {
|
|
|
237
326
|
},
|
|
238
327
|
|
|
239
328
|
create(name, overrides = {}, options = {}) {
|
|
240
|
-
return
|
|
329
|
+
return createThenableBuilder(
|
|
330
|
+
(nextOverrides, nextOptions) =>
|
|
331
|
+
createOne(name, nextOverrides, {
|
|
332
|
+
...options,
|
|
333
|
+
traits: [...(options.traits || []), ...(nextOptions.traits || [])],
|
|
334
|
+
}),
|
|
335
|
+
overrides
|
|
336
|
+
)
|
|
241
337
|
},
|
|
242
338
|
|
|
243
339
|
async createMany(name, count, overrides = {}, options = {}) {
|
|
@@ -260,7 +356,10 @@ function createWorldEngine({ sails }) {
|
|
|
260
356
|
return defineScenario(definition)
|
|
261
357
|
}
|
|
262
358
|
|
|
263
|
-
throw
|
|
359
|
+
throw createSoundingError({
|
|
360
|
+
code: 'E_SOUNDING_WORLD_DEFINITION_UNKNOWN',
|
|
361
|
+
message: 'Sounding could not register an unknown world definition.',
|
|
362
|
+
})
|
|
264
363
|
},
|
|
265
364
|
|
|
266
365
|
get current() {
|
|
@@ -8,17 +8,29 @@ const {
|
|
|
8
8
|
isFactoryDefinition,
|
|
9
9
|
isScenarioDefinition,
|
|
10
10
|
} = require('./define-world')
|
|
11
|
+
const { createSoundingError } = require('./create-error')
|
|
12
|
+
|
|
13
|
+
/** @typedef {import('./types').AnyRecord} AnyRecord */
|
|
14
|
+
/** @typedef {import('./types').SoundingConfig} SoundingConfig */
|
|
15
|
+
/** @typedef {import('./types').SoundingSailsApp} SoundingSailsApp */
|
|
16
|
+
/** @typedef {import('./types').SoundingWorldEngine} SoundingWorldEngine */
|
|
11
17
|
|
|
12
18
|
const WORLD_EXTENSIONS = new Set(['.js', '.cjs', '.mjs'])
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} directory
|
|
22
|
+
* @param {{ directories?: string[] }} [options]
|
|
23
|
+
* @returns {string[]}
|
|
24
|
+
*/
|
|
25
|
+
function listDefinitionFiles(directory, options = {}) {
|
|
15
26
|
const output = []
|
|
27
|
+
options.directories?.push(directory)
|
|
16
28
|
|
|
17
29
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
18
30
|
const nextPath = path.join(directory, entry.name)
|
|
19
31
|
|
|
20
32
|
if (entry.isDirectory()) {
|
|
21
|
-
output.push(...listDefinitionFiles(nextPath))
|
|
33
|
+
output.push(...listDefinitionFiles(nextPath, options))
|
|
22
34
|
continue
|
|
23
35
|
}
|
|
24
36
|
|
|
@@ -30,6 +42,91 @@ function listDefinitionFiles(directory) {
|
|
|
30
42
|
return output.sort()
|
|
31
43
|
}
|
|
32
44
|
|
|
45
|
+
/**
|
|
46
|
+
* @param {string} entryPath
|
|
47
|
+
* @returns {{ path: string, mtimeMs: number, size: number }}
|
|
48
|
+
*/
|
|
49
|
+
function getFileSignature(entryPath) {
|
|
50
|
+
const stat = fs.statSync(entryPath)
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
path: entryPath,
|
|
54
|
+
mtimeMs: stat.mtimeMs,
|
|
55
|
+
size: stat.size,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @returns {{
|
|
61
|
+
* directories: Map<string, { signatures: Array<{ path: string, mtimeMs: number, size: number }>, files: string[] }>,
|
|
62
|
+
* modules: Map<string, { signature: { path: string, mtimeMs: number, size: number }, value: any }>,
|
|
63
|
+
* stats: { directoryScans: number, moduleLoads: number },
|
|
64
|
+
* clear(): void,
|
|
65
|
+
* }}
|
|
66
|
+
*/
|
|
67
|
+
function createWorldLoaderCache() {
|
|
68
|
+
const stats = {
|
|
69
|
+
directoryScans: 0,
|
|
70
|
+
moduleLoads: 0,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
directories: new Map(),
|
|
75
|
+
modules: new Map(),
|
|
76
|
+
stats,
|
|
77
|
+
clear() {
|
|
78
|
+
this.directories.clear()
|
|
79
|
+
this.modules.clear()
|
|
80
|
+
stats.directoryScans = 0
|
|
81
|
+
stats.moduleLoads = 0
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {{ path: string, mtimeMs: number, size: number }} expected
|
|
88
|
+
* @returns {boolean}
|
|
89
|
+
*/
|
|
90
|
+
function signatureMatches(expected) {
|
|
91
|
+
if (!fs.existsSync(expected.path)) {
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const actual = getFileSignature(expected.path)
|
|
96
|
+
return actual.mtimeMs === expected.mtimeMs && actual.size === expected.size
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {string} directory
|
|
101
|
+
* @param {ReturnType<typeof createWorldLoaderCache> | undefined} cache
|
|
102
|
+
* @returns {string[]}
|
|
103
|
+
*/
|
|
104
|
+
function getDefinitionFiles(directory, cache) {
|
|
105
|
+
if (!cache) {
|
|
106
|
+
return listDefinitionFiles(directory)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const cached = cache.directories.get(directory)
|
|
110
|
+
|
|
111
|
+
if (cached && cached.signatures.every(signatureMatches)) {
|
|
112
|
+
return cached.files
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const directories = []
|
|
116
|
+
const files = listDefinitionFiles(directory, { directories })
|
|
117
|
+
cache.stats.directoryScans += 1
|
|
118
|
+
cache.directories.set(directory, {
|
|
119
|
+
signatures: directories.map(getFileSignature),
|
|
120
|
+
files,
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
return files
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {string} filePath
|
|
128
|
+
* @returns {Promise<any>}
|
|
129
|
+
*/
|
|
33
130
|
async function loadModule(filePath) {
|
|
34
131
|
try {
|
|
35
132
|
delete require.cache[require.resolve(filePath)]
|
|
@@ -43,6 +140,42 @@ async function loadModule(filePath) {
|
|
|
43
140
|
}
|
|
44
141
|
}
|
|
45
142
|
|
|
143
|
+
/**
|
|
144
|
+
* @param {string} filePath
|
|
145
|
+
* @param {ReturnType<typeof createWorldLoaderCache> | undefined} cache
|
|
146
|
+
* @returns {Promise<any>}
|
|
147
|
+
*/
|
|
148
|
+
async function loadCachedModule(filePath, cache) {
|
|
149
|
+
if (!cache) {
|
|
150
|
+
return loadModule(filePath)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const signature = getFileSignature(filePath)
|
|
154
|
+
const cached = cache.modules.get(filePath)
|
|
155
|
+
|
|
156
|
+
if (
|
|
157
|
+
cached &&
|
|
158
|
+
cached.signature.mtimeMs === signature.mtimeMs &&
|
|
159
|
+
cached.signature.size === signature.size
|
|
160
|
+
) {
|
|
161
|
+
return cached.value
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const value = await loadModule(filePath)
|
|
165
|
+
cache.stats.moduleLoads += 1
|
|
166
|
+
cache.modules.set(filePath, {
|
|
167
|
+
signature,
|
|
168
|
+
value,
|
|
169
|
+
})
|
|
170
|
+
return value
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @param {any} value
|
|
175
|
+
* @param {AnyRecord} api
|
|
176
|
+
* @param {string} source
|
|
177
|
+
* @returns {Promise<void>}
|
|
178
|
+
*/
|
|
46
179
|
async function registerExport(value, api, source) {
|
|
47
180
|
const entry = value?.default ?? value
|
|
48
181
|
|
|
@@ -84,12 +217,20 @@ async function registerExport(value, api, source) {
|
|
|
84
217
|
return
|
|
85
218
|
}
|
|
86
219
|
|
|
87
|
-
throw
|
|
88
|
-
|
|
89
|
-
|
|
220
|
+
throw createSoundingError({
|
|
221
|
+
code: 'E_SOUNDING_WORLD_DEFINITION_UNKNOWN',
|
|
222
|
+
message: `Sounding could not understand the world definition exported from ${source}.`,
|
|
223
|
+
details: {
|
|
224
|
+
source,
|
|
225
|
+
},
|
|
226
|
+
})
|
|
90
227
|
}
|
|
91
228
|
|
|
92
|
-
|
|
229
|
+
/**
|
|
230
|
+
* @param {{ world: SoundingWorldEngine, appPath: string, config: SoundingConfig, sails?: SoundingSailsApp, cache?: ReturnType<typeof createWorldLoaderCache> }} input
|
|
231
|
+
* @returns {Promise<string[]>}
|
|
232
|
+
*/
|
|
233
|
+
async function loadWorldFiles({ world, appPath, config, sails, cache }) {
|
|
93
234
|
const directories = [config.world?.factories, config.world?.scenarios]
|
|
94
235
|
.filter(Boolean)
|
|
95
236
|
.map((relativePath) => path.resolve(appPath, relativePath))
|
|
@@ -111,8 +252,8 @@ async function loadWorldFiles({ world, appPath, config, sails }) {
|
|
|
111
252
|
continue
|
|
112
253
|
}
|
|
113
254
|
|
|
114
|
-
for (const filePath of
|
|
115
|
-
const loaded = await
|
|
255
|
+
for (const filePath of getDefinitionFiles(directory, cache)) {
|
|
256
|
+
const loaded = await loadCachedModule(filePath, cache)
|
|
116
257
|
await registerExport(loaded, api, filePath)
|
|
117
258
|
loadedFiles.push(filePath)
|
|
118
259
|
}
|
|
@@ -122,6 +263,7 @@ async function loadWorldFiles({ world, appPath, config, sails }) {
|
|
|
122
263
|
}
|
|
123
264
|
|
|
124
265
|
module.exports = {
|
|
266
|
+
createWorldLoaderCache,
|
|
125
267
|
loadWorldFiles,
|
|
126
268
|
listDefinitionFiles,
|
|
127
269
|
loadModule,
|
package/lib/default-config.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/** @typedef {import('./types').SoundingConfig} SoundingConfig */
|
|
2
|
+
|
|
3
|
+
/** @type {Readonly<SoundingConfig>} */
|
|
1
4
|
const DEFAULT_CONFIG = Object.freeze({
|
|
2
5
|
// Sails environments where the Sounding hook should boot.
|
|
3
6
|
// Keep this test-only by default so non-test processes stay dark.
|
|
@@ -27,13 +30,29 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
27
30
|
launchOptions: {
|
|
28
31
|
headless: true,
|
|
29
32
|
},
|
|
33
|
+
artifacts: {
|
|
34
|
+
outputDir: '.tmp/sounding/artifacts',
|
|
35
|
+
screenshot: true,
|
|
36
|
+
trace: false,
|
|
37
|
+
video: false,
|
|
38
|
+
currentUrl: true,
|
|
39
|
+
},
|
|
30
40
|
},
|
|
31
41
|
mail: {
|
|
32
42
|
capture: true,
|
|
43
|
+
layout: 'mail',
|
|
33
44
|
},
|
|
34
45
|
request: {
|
|
35
46
|
transport: 'virtual',
|
|
36
47
|
},
|
|
48
|
+
sockets: {
|
|
49
|
+
enabled: true,
|
|
50
|
+
timeout: 1000,
|
|
51
|
+
transports: ['websocket'],
|
|
52
|
+
path: '/socket.io',
|
|
53
|
+
headers: {},
|
|
54
|
+
initialConnectionHeaders: {},
|
|
55
|
+
},
|
|
37
56
|
auth: {
|
|
38
57
|
defaultActor: 'guest',
|
|
39
58
|
modelIdentity: null,
|
|
@@ -54,6 +73,10 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
54
73
|
},
|
|
55
74
|
})
|
|
56
75
|
|
|
76
|
+
/**
|
|
77
|
+
* @param {any} value
|
|
78
|
+
* @returns {any}
|
|
79
|
+
*/
|
|
57
80
|
function cloneValue(value) {
|
|
58
81
|
if (Array.isArray(value)) {
|
|
59
82
|
return value.map(cloneValue)
|
|
@@ -68,6 +91,9 @@ function cloneValue(value) {
|
|
|
68
91
|
return value
|
|
69
92
|
}
|
|
70
93
|
|
|
94
|
+
/**
|
|
95
|
+
* @returns {SoundingConfig}
|
|
96
|
+
*/
|
|
71
97
|
function getDefaultConfig() {
|
|
72
98
|
return cloneValue(DEFAULT_CONFIG)
|
|
73
99
|
}
|
package/lib/define-world.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
|
+
/** @typedef {import('./types').SoundingFactoryDefinition} SoundingFactoryDefinition */
|
|
2
|
+
/** @typedef {import('./types').SoundingScenarioDefinition} SoundingScenarioDefinition */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Define a reusable record factory for Sounding worlds.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} name
|
|
8
|
+
* @param {SoundingFactoryDefinition['definition']} definition
|
|
9
|
+
* @returns {SoundingFactoryDefinition}
|
|
10
|
+
*/
|
|
1
11
|
function defineFactory(name, definition) {
|
|
2
12
|
const entry = {
|
|
3
|
-
__soundingType: 'factory',
|
|
13
|
+
__soundingType: /** @type {'factory'} */ ('factory'),
|
|
4
14
|
name,
|
|
5
15
|
definition,
|
|
6
16
|
traits: [],
|
|
@@ -13,18 +23,33 @@ function defineFactory(name, definition) {
|
|
|
13
23
|
return entry
|
|
14
24
|
}
|
|
15
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Define a named scenario that can seed a trial with product-language data.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} name
|
|
30
|
+
* @param {SoundingScenarioDefinition['definition']} definition
|
|
31
|
+
* @returns {SoundingScenarioDefinition}
|
|
32
|
+
*/
|
|
16
33
|
function defineScenario(name, definition) {
|
|
17
34
|
return {
|
|
18
|
-
__soundingType: 'scenario',
|
|
35
|
+
__soundingType: /** @type {'scenario'} */ ('scenario'),
|
|
19
36
|
name,
|
|
20
37
|
definition,
|
|
21
38
|
}
|
|
22
39
|
}
|
|
23
40
|
|
|
41
|
+
/**
|
|
42
|
+
* @param {any} value
|
|
43
|
+
* @returns {value is SoundingFactoryDefinition}
|
|
44
|
+
*/
|
|
24
45
|
function isFactoryDefinition(value) {
|
|
25
46
|
return value?.__soundingType === 'factory'
|
|
26
47
|
}
|
|
27
48
|
|
|
49
|
+
/**
|
|
50
|
+
* @param {any} value
|
|
51
|
+
* @returns {value is SoundingScenarioDefinition}
|
|
52
|
+
*/
|
|
28
53
|
function isScenarioDefinition(value) {
|
|
29
54
|
return value?.__soundingType === 'scenario'
|
|
30
55
|
}
|