@appium/base-driver 10.6.0 → 10.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/build/lib/basedriver/capabilities.d.ts +1 -1
- package/build/lib/basedriver/capabilities.d.ts.map +1 -1
- package/build/lib/basedriver/capabilities.js +15 -7
- package/build/lib/basedriver/capabilities.js.map +1 -1
- package/build/lib/basedriver/commands/bidi.js +1 -1
- package/build/lib/basedriver/commands/event.js.map +1 -1
- package/build/lib/basedriver/commands/execute.js.map +1 -1
- package/build/lib/basedriver/commands/find.d.ts.map +1 -1
- package/build/lib/basedriver/commands/find.js +2 -1
- package/build/lib/basedriver/commands/find.js.map +1 -1
- package/build/lib/basedriver/commands/timeout.js +4 -4
- package/build/lib/basedriver/commands/timeout.js.map +1 -1
- package/build/lib/basedriver/core.d.ts.map +1 -1
- package/build/lib/basedriver/core.js +5 -2
- package/build/lib/basedriver/core.js.map +1 -1
- package/build/lib/basedriver/device-settings.d.ts.map +1 -1
- package/build/lib/basedriver/device-settings.js.map +1 -1
- package/build/lib/basedriver/driver.d.ts.map +1 -1
- package/build/lib/basedriver/driver.js +23 -24
- package/build/lib/basedriver/driver.js.map +1 -1
- package/build/lib/basedriver/extension-core.d.ts.map +1 -1
- package/build/lib/basedriver/extension-core.js +11 -5
- package/build/lib/basedriver/extension-core.js.map +1 -1
- package/build/lib/basedriver/helpers.d.ts.map +1 -1
- package/build/lib/basedriver/helpers.js +20 -4
- package/build/lib/basedriver/helpers.js.map +1 -1
- package/build/lib/basedriver/ipc.d.ts.map +1 -1
- package/build/lib/basedriver/ipc.js +6 -4
- package/build/lib/basedriver/ipc.js.map +1 -1
- package/build/lib/basedriver/validation.d.ts.map +1 -1
- package/build/lib/basedriver/validation.js +3 -2
- package/build/lib/basedriver/validation.js.map +1 -1
- package/build/lib/express/express-logging.d.ts +0 -1
- package/build/lib/express/express-logging.d.ts.map +1 -1
- package/build/lib/express/express-logging.js +9 -8
- package/build/lib/express/express-logging.js.map +1 -1
- package/build/lib/express/idempotency.js.map +1 -1
- package/build/lib/express/middleware.d.ts.map +1 -1
- package/build/lib/express/middleware.js.map +1 -1
- package/build/lib/express/server.d.ts +1 -1
- package/build/lib/express/server.d.ts.map +1 -1
- package/build/lib/express/server.js +19 -20
- package/build/lib/express/server.js.map +1 -1
- package/build/lib/express/websocket.d.ts.map +1 -1
- package/build/lib/express/websocket.js.map +1 -1
- package/build/lib/helpers/capabilities.d.ts.map +1 -1
- package/build/lib/helpers/capabilities.js.map +1 -1
- package/build/lib/helpers/levenshtein-match.d.ts.map +1 -1
- package/build/lib/helpers/levenshtein-match.js +4 -1
- package/build/lib/helpers/levenshtein-match.js.map +1 -1
- package/build/lib/index.d.ts +1 -1
- package/build/lib/index.d.ts.map +1 -1
- package/build/lib/index.js +3 -2
- package/build/lib/index.js.map +1 -1
- package/build/lib/jsonwp-proxy/protocol-converter.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/protocol-converter.js +14 -7
- package/build/lib/jsonwp-proxy/protocol-converter.js.map +1 -1
- package/build/lib/jsonwp-proxy/proxy.d.ts.map +1 -1
- package/build/lib/jsonwp-proxy/proxy.js +17 -11
- package/build/lib/jsonwp-proxy/proxy.js.map +1 -1
- package/build/lib/protocol/errors.d.ts.map +1 -1
- package/build/lib/protocol/errors.js +13 -13
- package/build/lib/protocol/errors.js.map +1 -1
- package/build/lib/protocol/protocol.d.ts +1 -1
- package/build/lib/protocol/protocol.d.ts.map +1 -1
- package/build/lib/protocol/protocol.js +35 -18
- package/build/lib/protocol/protocol.js.map +1 -1
- package/build/lib/protocol/routes.d.ts.map +1 -1
- package/build/lib/protocol/routes.js +7 -5
- package/build/lib/protocol/routes.js.map +1 -1
- package/build/lib/test-pages/crash.d.ts.map +1 -0
- package/build/lib/test-pages/crash.js.map +1 -0
- package/build/lib/test-pages/env.d.ts +5 -0
- package/build/lib/test-pages/env.d.ts.map +1 -0
- package/build/lib/test-pages/env.js +12 -0
- package/build/lib/test-pages/env.js.map +1 -0
- package/build/lib/{express/static.d.ts → test-pages/handlers.d.ts} +1 -2
- package/build/lib/test-pages/handlers.d.ts.map +1 -0
- package/build/lib/{express/static.js → test-pages/handlers.js} +7 -17
- package/build/lib/test-pages/handlers.js.map +1 -0
- package/build/lib/test-pages/index.d.ts +6 -0
- package/build/lib/test-pages/index.d.ts.map +1 -0
- package/build/lib/test-pages/index.js +35 -0
- package/build/lib/test-pages/index.js.map +1 -0
- package/build/lib/test-pages/static-dir.d.ts +8 -0
- package/build/lib/test-pages/static-dir.d.ts.map +1 -0
- package/build/lib/test-pages/static-dir.js +24 -0
- package/build/lib/test-pages/static-dir.js.map +1 -0
- package/build/lib/test-pages/template.d.ts +3 -0
- package/build/lib/test-pages/template.d.ts.map +1 -0
- package/build/lib/test-pages/template.js +19 -0
- package/build/lib/test-pages/template.js.map +1 -0
- package/build/lib/utils.d.ts +0 -2
- package/build/lib/utils.d.ts.map +1 -1
- package/build/lib/utils.js +0 -16
- package/build/lib/utils.js.map +1 -1
- package/lib/basedriver/capabilities.ts +72 -66
- package/lib/basedriver/commands/bidi.ts +1 -1
- package/lib/basedriver/commands/event.ts +10 -5
- package/lib/basedriver/commands/execute.ts +12 -9
- package/lib/basedriver/commands/find.ts +20 -12
- package/lib/basedriver/commands/log.ts +2 -2
- package/lib/basedriver/commands/timeout.ts +17 -8
- package/lib/basedriver/core.ts +14 -14
- package/lib/basedriver/device-settings.ts +4 -8
- package/lib/basedriver/driver.ts +50 -40
- package/lib/basedriver/extension-core.ts +33 -17
- package/lib/basedriver/helpers.ts +57 -26
- package/lib/basedriver/ipc.ts +37 -18
- package/lib/basedriver/validation.ts +13 -6
- package/lib/express/express-logging.ts +14 -17
- package/lib/express/idempotency.ts +6 -6
- package/lib/express/middleware.ts +10 -12
- package/lib/express/server.ts +53 -61
- package/lib/express/websocket.ts +5 -7
- package/lib/helpers/capabilities.ts +5 -4
- package/lib/helpers/extension-command-name.ts +1 -1
- package/lib/helpers/levenshtein-match.ts +20 -11
- package/lib/index.js +2 -1
- package/lib/jsonwp-proxy/protocol-converter.ts +51 -27
- package/lib/jsonwp-proxy/proxy.ts +42 -42
- package/lib/protocol/errors.ts +47 -67
- package/lib/protocol/protocol.ts +116 -72
- package/lib/protocol/routes.ts +9 -9
- package/lib/test-pages/env.ts +9 -0
- package/lib/{express/static.ts → test-pages/handlers.ts} +7 -27
- package/lib/test-pages/index.ts +34 -0
- package/lib/test-pages/static-dir.ts +19 -0
- package/lib/test-pages/template.ts +17 -0
- package/lib/utils.ts +3 -23
- package/package.json +9 -10
- package/tsconfig.json +1 -0
- package/build/lib/express/crash.d.ts.map +0 -1
- package/build/lib/express/crash.js.map +0 -1
- package/build/lib/express/static.d.ts.map +0 -1
- package/build/lib/express/static.js.map +0 -1
- /package/build/lib/{express → test-pages}/crash.d.ts +0 -0
- /package/build/lib/{express → test-pages}/crash.js +0 -0
- /package/lib/{express → test-pages}/crash.ts +0 -0
- /package/{static → test-fixtures/static}/appium.png +0 -0
- /package/{static → test-fixtures/static}/favicon.ico +0 -0
- /package/{static → test-fixtures/static}/js/jquery.min.js +0 -0
- /package/{static → test-fixtures/static}/test/frameset.html +0 -0
- /package/{static → test-fixtures/static}/test/guinea-pig-app-banner.html +0 -0
- /package/{static → test-fixtures/static}/test/guinea-pig-scrollable.html +0 -0
- /package/{static → test-fixtures/static}/test/guinea-pig.html +0 -0
- /package/{static → test-fixtures/static}/test/guinea-pig2.html +0 -0
- /package/{static → test-fixtures/static}/test/guinea-pig3.html +0 -0
- /package/{static → test-fixtures/static}/test/guinea-pig4.html +0 -0
- /package/{static → test-fixtures/static}/test/guinea-pig5.html +0 -0
- /package/{static → test-fixtures/static}/test/iframes.html +0 -0
- /package/{static → test-fixtures/static}/test/shadow-dom.html +0 -0
- /package/{static → test-fixtures/static}/test/subframe1.html +0 -0
- /package/{static → test-fixtures/static}/test/subframe2.html +0 -0
- /package/{static → test-fixtures/static}/test/subframe3.html +0 -0
- /package/{static → test-fixtures/static}/test/touch.html +0 -0
- /package/{static → test-fixtures/static}/test/welcome.html +0 -0
package/lib/protocol/protocol.ts
CHANGED
|
@@ -15,7 +15,14 @@ import {isW3cCaps} from '../helpers/capabilities';
|
|
|
15
15
|
import {log} from '../basedriver/logger';
|
|
16
16
|
import {omitKeys} from '../utils';
|
|
17
17
|
import {generateDriverLogPrefix} from '../basedriver/helpers';
|
|
18
|
-
import type {
|
|
18
|
+
import type {
|
|
19
|
+
Core,
|
|
20
|
+
AppiumLogger,
|
|
21
|
+
PayloadParams,
|
|
22
|
+
MethodMap,
|
|
23
|
+
Driver,
|
|
24
|
+
DriverMethodDef,
|
|
25
|
+
} from '@appium/types';
|
|
19
26
|
import type {BaseDriver} from '../basedriver/driver';
|
|
20
27
|
import type {Request, Response, Application} from 'express';
|
|
21
28
|
import type {MultidimensionalReadonlyArray} from 'type-fest';
|
|
@@ -54,10 +61,10 @@ export function getSessionId(driver: Core<any>, req: Request): string | undefine
|
|
|
54
61
|
const sessionId = req.params.sessionId[0];
|
|
55
62
|
getLogger(driver, sessionId).warn(
|
|
56
63
|
`Received malformed sessionId as array from the route: ${req.originalUrl}. ` +
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
`This indicates the route definition issue. The route should start with '/session/:sessionId' (named parameter) ` +
|
|
65
|
+
`instead of '/session/*sessionId' (wildcard). ` +
|
|
66
|
+
`Using the first element as session id: ${sessionId}. ` +
|
|
67
|
+
`Please fix the route definition to prevent this error.`,
|
|
61
68
|
);
|
|
62
69
|
// This is to not log the message multiple times.
|
|
63
70
|
req.params.sessionId = sessionId;
|
|
@@ -83,7 +90,7 @@ export function isSessionCommand(command: string): boolean {
|
|
|
83
90
|
export function checkParams(
|
|
84
91
|
paramSpec: PayloadParams,
|
|
85
92
|
args: Record<string, any>,
|
|
86
|
-
protocol?: keyof typeof PROTOCOLS
|
|
93
|
+
protocol?: keyof typeof PROTOCOLS,
|
|
87
94
|
): Record<string, any> {
|
|
88
95
|
let requiredParams: string[][] = [];
|
|
89
96
|
let optionalParams: string[] = [];
|
|
@@ -95,8 +102,7 @@ export function checkParams(
|
|
|
95
102
|
requiredParams = structuredClone(
|
|
96
103
|
(hasMultipleRequiredParamSets(paramSpec.required)
|
|
97
104
|
? paramSpec.required
|
|
98
|
-
: [paramSpec.required]
|
|
99
|
-
) as string[][]
|
|
105
|
+
: [paramSpec.required]) as string[][],
|
|
100
106
|
);
|
|
101
107
|
}
|
|
102
108
|
// optional parameters are just an array
|
|
@@ -126,7 +132,10 @@ export function checkParams(
|
|
|
126
132
|
|
|
127
133
|
if (util.isEmpty(requiredParams)) {
|
|
128
134
|
// if we don't have any required parameters, then just filter out unknown ones
|
|
129
|
-
return pickKnownParams(
|
|
135
|
+
return pickKnownParams(
|
|
136
|
+
args,
|
|
137
|
+
actualParamNames.filter((name) => !optionalParams.includes(name)),
|
|
138
|
+
);
|
|
130
139
|
}
|
|
131
140
|
|
|
132
141
|
// go through the required parameters and check against our arguments
|
|
@@ -135,25 +144,30 @@ export function checkParams(
|
|
|
135
144
|
if (!Array.isArray(requiredParamsSet)) {
|
|
136
145
|
throw new Error(
|
|
137
146
|
`The required parameter set item ${JSON.stringify(requiredParamsSet)} ` +
|
|
138
|
-
|
|
139
|
-
|
|
147
|
+
`in ${JSON.stringify(paramSpec)} is not an array. ` +
|
|
148
|
+
`This is a bug in the method map definition.`,
|
|
140
149
|
);
|
|
141
150
|
}
|
|
142
151
|
if (requiredParamsSet.every((name) => actualParamNames.includes(name))) {
|
|
143
152
|
return pickKnownParams(
|
|
144
153
|
args,
|
|
145
|
-
actualParamNames.filter(
|
|
154
|
+
actualParamNames.filter(
|
|
155
|
+
(name) => !requiredParamsSet.includes(name) && !optionalParams.includes(name),
|
|
156
|
+
),
|
|
146
157
|
);
|
|
147
158
|
}
|
|
148
159
|
if (!util.isEmpty(requiredParamsSet) && util.isEmpty(matchedReqParamSet)) {
|
|
149
160
|
matchedReqParamSet = requiredParamsSet;
|
|
150
161
|
}
|
|
151
162
|
}
|
|
152
|
-
throw new BadParametersError(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
163
|
+
throw new BadParametersError(
|
|
164
|
+
{
|
|
165
|
+
...paramSpec,
|
|
166
|
+
required: matchedReqParamSet,
|
|
167
|
+
optional: optionalParams,
|
|
168
|
+
},
|
|
169
|
+
actualParamNames,
|
|
170
|
+
);
|
|
157
171
|
}
|
|
158
172
|
|
|
159
173
|
/**
|
|
@@ -162,7 +176,11 @@ export function checkParams(
|
|
|
162
176
|
* @param jsonObj - Parsed JSON request body
|
|
163
177
|
* @param payloadParams - Route payload definition (required/optional/makeArgs)
|
|
164
178
|
*/
|
|
165
|
-
export function makeArgs(
|
|
179
|
+
export function makeArgs(
|
|
180
|
+
requestParams: Record<string, string | string[] | undefined>,
|
|
181
|
+
jsonObj: any,
|
|
182
|
+
payloadParams: PayloadParams,
|
|
183
|
+
): any[] {
|
|
166
184
|
// We want to pass the "url" parameters to the commands in reverse order
|
|
167
185
|
// since the command will sometimes want to ignore, say, the sessionId.
|
|
168
186
|
// This has the effect of putting sessionId last, which means in JS we can
|
|
@@ -224,14 +242,14 @@ export function validateExecuteMethodParams(params: any[], paramSpec?: PayloadPa
|
|
|
224
242
|
if (!params || !Array.isArray(params) || params.length > 1) {
|
|
225
243
|
throw new errors.InvalidArgumentError(
|
|
226
244
|
`Did not get correct format of arguments for execute method. Expected zero or one ` +
|
|
227
|
-
`arguments to execute script and instead received: ${JSON.stringify(params)}
|
|
245
|
+
`arguments to execute script and instead received: ${JSON.stringify(params)}`,
|
|
228
246
|
);
|
|
229
247
|
}
|
|
230
248
|
const args: Record<string, any> = params[0] ?? {};
|
|
231
249
|
if (!util.isPlainObject(args)) {
|
|
232
250
|
throw new errors.InvalidArgumentError(
|
|
233
251
|
`Did not receive an appropriate execute method parameters object. It needs to be ` +
|
|
234
|
-
`deserializable as a plain JS object
|
|
252
|
+
`deserializable as a plain JS object`,
|
|
235
253
|
);
|
|
236
254
|
}
|
|
237
255
|
const specToUse = {
|
|
@@ -268,14 +286,7 @@ export function routeConfiguringFunction(driver: Core<any>): RouteConfiguringFun
|
|
|
268
286
|
for (const [method, spec] of Object.entries(methods)) {
|
|
269
287
|
const isSessCommand = spec.command ? isSessionCommand(spec.command) : false;
|
|
270
288
|
// set up the express route handler
|
|
271
|
-
buildHandler(
|
|
272
|
-
app,
|
|
273
|
-
method,
|
|
274
|
-
`${basePath}${path}`,
|
|
275
|
-
spec,
|
|
276
|
-
driver,
|
|
277
|
-
isSessCommand
|
|
278
|
-
);
|
|
289
|
+
buildHandler(app, method, `${basePath}${path}`, spec, driver, isSessCommand);
|
|
279
290
|
}
|
|
280
291
|
}
|
|
281
292
|
};
|
|
@@ -309,10 +320,14 @@ export function driverShouldDoJwpProxy(driver: Core<any>, req: Request, command:
|
|
|
309
320
|
return true;
|
|
310
321
|
}
|
|
311
322
|
|
|
312
|
-
function extractProtocol(
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
323
|
+
function extractProtocol(
|
|
324
|
+
driver: Core<any>,
|
|
325
|
+
sessionId: string | null = null,
|
|
326
|
+
): keyof typeof PROTOCOLS {
|
|
327
|
+
const dstDriver =
|
|
328
|
+
typeof driver.driverForSession === 'function' && sessionId
|
|
329
|
+
? driver.driverForSession(sessionId)
|
|
330
|
+
: driver;
|
|
316
331
|
if (dstDriver === driver) {
|
|
317
332
|
// Shortcircuit if the driver instance is not an umbrella driver
|
|
318
333
|
// or it is Fake driver instance, where `driver.driverForSession`
|
|
@@ -327,7 +342,7 @@ function extractProtocol(driver: Core<any>, sessionId: string | null = null): ke
|
|
|
327
342
|
function getLogger(driver: Core<any>, sessionId: string | null = null): AppiumLogger {
|
|
328
343
|
const dstDriver =
|
|
329
344
|
sessionId && typeof driver.driverForSession === 'function'
|
|
330
|
-
? driver.driverForSession(sessionId) ?? driver
|
|
345
|
+
? (driver.driverForSession(sessionId) ?? driver)
|
|
331
346
|
: driver;
|
|
332
347
|
if (typeof dstDriver.log?.info === 'function') {
|
|
333
348
|
return dstDriver.log;
|
|
@@ -337,14 +352,15 @@ function getLogger(driver: Core<any>, sessionId: string | null = null): AppiumLo
|
|
|
337
352
|
return logger.getLogger(logPrefix);
|
|
338
353
|
}
|
|
339
354
|
|
|
340
|
-
function wrapParams<T>(paramSets, jsonObj: T): T | Record<string, T> {
|
|
355
|
+
function wrapParams<T>(paramSets: PayloadParams, jsonObj: T): T | Record<string, T> {
|
|
341
356
|
/* There are commands like performTouch which take a single parameter (primitive type or array).
|
|
342
357
|
* Some drivers choose to pass this parameter as a value (eg. [action1, action2...]) while others to
|
|
343
358
|
* wrap it within an object(eg' {gesture: [action1, action2...]}), which makes it hard to validate.
|
|
344
359
|
* The wrap option in the spec enforce wrapping before validation, so that all params are wrapped at
|
|
345
360
|
* the time they are validated and later passed to the commands.
|
|
346
361
|
*/
|
|
347
|
-
return (Array.isArray(jsonObj) || typeof jsonObj !== 'object' || jsonObj === null) &&
|
|
362
|
+
return (Array.isArray(jsonObj) || typeof jsonObj !== 'object' || jsonObj === null) &&
|
|
363
|
+
paramSets.wrap
|
|
348
364
|
? {[paramSets.wrap]: jsonObj}
|
|
349
365
|
: jsonObj;
|
|
350
366
|
}
|
|
@@ -353,14 +369,15 @@ function unwrapParams<T>(paramSets: PayloadParams, jsonObj: T): T | Record<strin
|
|
|
353
369
|
/* There are commands like setNetworkConnection which send parameters wrapped inside a key such as
|
|
354
370
|
* "parameters". This function unwraps them (eg. {"parameters": {"type": 1}} becomes {"type": 1}).
|
|
355
371
|
*/
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
372
|
+
const unwrapped =
|
|
373
|
+
typeof jsonObj === 'object' && jsonObj !== null && paramSets.unwrap
|
|
374
|
+
? (jsonObj as Record<string, T>)[paramSets.unwrap]
|
|
375
|
+
: undefined;
|
|
376
|
+
return unwrapped !== undefined ? unwrapped : jsonObj;
|
|
359
377
|
}
|
|
360
378
|
|
|
361
|
-
|
|
362
379
|
function hasMultipleRequiredParamSets(
|
|
363
|
-
required: ReadonlyArray<string> | MultidimensionalReadonlyArray<string, 2> | undefined
|
|
380
|
+
required: ReadonlyArray<string> | MultidimensionalReadonlyArray<string, 2> | undefined,
|
|
364
381
|
): required is MultidimensionalReadonlyArray<string, 2> {
|
|
365
382
|
return Boolean(required && Array.isArray(required?.[0]));
|
|
366
383
|
}
|
|
@@ -379,7 +396,7 @@ function buildHandler(
|
|
|
379
396
|
path: string,
|
|
380
397
|
spec: DriverMethodDef<Driver>,
|
|
381
398
|
driver: Core<any>,
|
|
382
|
-
isSessCmd: boolean
|
|
399
|
+
isSessCmd: boolean,
|
|
383
400
|
): void {
|
|
384
401
|
const asyncHandler = async (req: Request, res: Response) => {
|
|
385
402
|
let jsonObj = req.body;
|
|
@@ -396,7 +413,7 @@ function buildHandler(
|
|
|
396
413
|
getLogger(driver, sessionId).warn(
|
|
397
414
|
`The ${method} ${path} endpoint has been deprecated and will be removed in a future ` +
|
|
398
415
|
`version of Appium or your driver/plugin. Please use a different endpoint or contact the ` +
|
|
399
|
-
`driver/plugin author to add explicit support for the endpoint before it is removed
|
|
416
|
+
`driver/plugin author to add explicit support for the endpoint before it is removed`,
|
|
400
417
|
);
|
|
401
418
|
}
|
|
402
419
|
|
|
@@ -415,9 +432,15 @@ function buildHandler(
|
|
|
415
432
|
// commands and generally would not want that command to be proxied instead of handled by the
|
|
416
433
|
// plugin)
|
|
417
434
|
let didPluginOverrideProxy = false;
|
|
418
|
-
if (
|
|
435
|
+
if (
|
|
436
|
+
isSessCmd &&
|
|
437
|
+
!spec.neverProxy &&
|
|
438
|
+
spec.command &&
|
|
439
|
+
driverShouldDoJwpProxy(driver, req, spec.command)
|
|
440
|
+
) {
|
|
419
441
|
if (
|
|
420
|
-
!('pluginsToHandleCmd' in driver) ||
|
|
442
|
+
!('pluginsToHandleCmd' in driver) ||
|
|
443
|
+
typeof driver.pluginsToHandleCmd !== 'function' ||
|
|
421
444
|
driver.pluginsToHandleCmd(spec.command, sessionId).length === 0
|
|
422
445
|
) {
|
|
423
446
|
await doJwpProxy(driver as BaseDriver<any>, req, res);
|
|
@@ -426,7 +449,7 @@ function buildHandler(
|
|
|
426
449
|
getLogger(driver, sessionId).debug(
|
|
427
450
|
`Would have proxied ` +
|
|
428
451
|
`command directly, but a plugin exists which might require its value, so will let ` +
|
|
429
|
-
`its value be collected internally and made part of plugin chain
|
|
452
|
+
`its value be collected internally and made part of plugin chain`,
|
|
430
453
|
);
|
|
431
454
|
didPluginOverrideProxy = true;
|
|
432
455
|
}
|
|
@@ -451,7 +474,7 @@ function buildHandler(
|
|
|
451
474
|
// try to determine protocol by session creation args, so we can throw a
|
|
452
475
|
// properly formatted error if arguments validation fails
|
|
453
476
|
currentProtocol = determineProtocol(
|
|
454
|
-
makeArgs(req.params, jsonObj, spec.payloadParams || {})
|
|
477
|
+
makeArgs(req.params, jsonObj, spec.payloadParams || {}),
|
|
455
478
|
);
|
|
456
479
|
}
|
|
457
480
|
|
|
@@ -465,15 +488,21 @@ function buildHandler(
|
|
|
465
488
|
const args = makeArgs(req.params, jsonObj, spec.payloadParams || {});
|
|
466
489
|
let driverRes: any;
|
|
467
490
|
// validate command args according to MJSONWP
|
|
468
|
-
|
|
469
|
-
validators[
|
|
491
|
+
const validator = (
|
|
492
|
+
validators as Record<string, ((...validatorArgs: any[]) => void) | undefined>
|
|
493
|
+
)[spec.command];
|
|
494
|
+
if (validator) {
|
|
495
|
+
validator(...args);
|
|
470
496
|
}
|
|
471
497
|
|
|
472
498
|
// run the driver command wrapped inside the argument validators
|
|
473
499
|
getLogger(driver, sessionId).debug(
|
|
474
500
|
`Calling %s.%s() with args: %s`,
|
|
475
|
-
driver.constructor.name,
|
|
476
|
-
|
|
501
|
+
driver.constructor.name,
|
|
502
|
+
spec.command,
|
|
503
|
+
logger.markSensitive(
|
|
504
|
+
util.truncateString(JSON.stringify(args), {length: MAX_LOG_BODY_LENGTH}),
|
|
505
|
+
),
|
|
477
506
|
);
|
|
478
507
|
|
|
479
508
|
if (didPluginOverrideProxy) {
|
|
@@ -491,7 +520,8 @@ function buildHandler(
|
|
|
491
520
|
// If `executeCommand` was overridden and the method returns an object
|
|
492
521
|
// with a protocol and value/error property, re-assign the protocol
|
|
493
522
|
if (util.isPlainObject(driverRes) && Object.hasOwn(driverRes, 'protocol')) {
|
|
494
|
-
currentProtocol =
|
|
523
|
+
currentProtocol =
|
|
524
|
+
(driverRes as {protocol?: keyof typeof PROTOCOLS}).protocol || currentProtocol;
|
|
495
525
|
if (driverRes.error) {
|
|
496
526
|
throw driverRes.error;
|
|
497
527
|
}
|
|
@@ -502,7 +532,7 @@ function buildHandler(
|
|
|
502
532
|
if (spec.command === CREATE_SESSION_COMMAND) {
|
|
503
533
|
newSessionId = driverRes[0];
|
|
504
534
|
getLogger(driver, newSessionId).debug(
|
|
505
|
-
`Cached the protocol value '${currentProtocol}' for the new session ${newSessionId}
|
|
535
|
+
`Cached the protocol value '${currentProtocol}' for the new session ${newSessionId}`,
|
|
506
536
|
);
|
|
507
537
|
if (currentProtocol === PROTOCOLS.MJSONWP) {
|
|
508
538
|
driverRes = driverRes[1];
|
|
@@ -520,7 +550,7 @@ function buildHandler(
|
|
|
520
550
|
getLogger(driver, sessionId).debug(
|
|
521
551
|
`Received response: ${util.truncateString(JSON.stringify(driverRes), {
|
|
522
552
|
length: MAX_LOG_BODY_LENGTH,
|
|
523
|
-
})}
|
|
553
|
+
})}`,
|
|
524
554
|
);
|
|
525
555
|
getLogger(driver, sessionId).debug('But deleting session, so not returning');
|
|
526
556
|
driverRes = null;
|
|
@@ -538,7 +568,7 @@ function buildHandler(
|
|
|
538
568
|
throw errorFromW3CJsonCode(
|
|
539
569
|
driverRes.value.error,
|
|
540
570
|
driverRes.value.message,
|
|
541
|
-
driverRes.value.stacktrace
|
|
571
|
+
driverRes.value.stacktrace,
|
|
542
572
|
);
|
|
543
573
|
}
|
|
544
574
|
}
|
|
@@ -546,38 +576,48 @@ function buildHandler(
|
|
|
546
576
|
httpResBody.value = driverRes;
|
|
547
577
|
getLogger(driver, sessionId || newSessionId).debug(
|
|
548
578
|
`Responding ` +
|
|
549
|
-
`to client with driver.${spec.command}() result: ${util.truncateString(
|
|
550
|
-
|
|
551
|
-
|
|
579
|
+
`to client with driver.${spec.command}() result: ${util.truncateString(
|
|
580
|
+
JSON.stringify(driverRes),
|
|
581
|
+
{
|
|
582
|
+
length: MAX_LOG_BODY_LENGTH,
|
|
583
|
+
},
|
|
584
|
+
)}`,
|
|
552
585
|
);
|
|
553
586
|
} catch (err) {
|
|
554
587
|
// if anything goes wrong, figure out what our response should be
|
|
555
588
|
// based on the type of error that we encountered
|
|
556
|
-
let actualErr;
|
|
557
|
-
if (err instanceof Error
|
|
589
|
+
let actualErr: Error;
|
|
590
|
+
if (err instanceof Error) {
|
|
558
591
|
actualErr = err;
|
|
592
|
+
} else if (
|
|
593
|
+
typeof err === 'object' &&
|
|
594
|
+
err !== null &&
|
|
595
|
+
Object.hasOwn(err, 'stack') &&
|
|
596
|
+
Object.hasOwn(err, 'message')
|
|
597
|
+
) {
|
|
598
|
+
actualErr = err as Error;
|
|
559
599
|
} else {
|
|
560
600
|
getLogger(driver, sessionId || newSessionId).warn(
|
|
561
601
|
'The thrown error object does not seem to be a valid instance of the Error class. This ' +
|
|
562
|
-
'might be a genuine bug of a driver or a plugin.'
|
|
602
|
+
'might be a genuine bug of a driver or a plugin.',
|
|
563
603
|
);
|
|
564
604
|
actualErr = new Error(`${err ?? 'unknown'}`);
|
|
565
605
|
}
|
|
566
606
|
|
|
567
|
-
currentProtocol =
|
|
568
|
-
currentProtocol || extractProtocol(driver, sessionId || newSessionId);
|
|
607
|
+
currentProtocol = currentProtocol || extractProtocol(driver, sessionId || newSessionId);
|
|
569
608
|
|
|
570
|
-
|
|
571
|
-
|
|
609
|
+
const stacktrace = (err as {stacktrace?: string}).stacktrace;
|
|
610
|
+
let errMsg = stacktrace || actualErr.stack || '';
|
|
611
|
+
if (!errMsg.includes(actualErr.message)) {
|
|
572
612
|
// if the message has more information, add it. but often the message
|
|
573
613
|
// is the first part of the stack trace
|
|
574
|
-
errMsg = `${
|
|
614
|
+
errMsg = `${actualErr.message}${errMsg ? '\n' + errMsg : ''}`;
|
|
575
615
|
}
|
|
576
616
|
if (isErrorType(err, errors.ProxyRequestError)) {
|
|
577
617
|
actualErr = err.getActualError();
|
|
578
618
|
} else {
|
|
579
619
|
getLogger(driver, sessionId || newSessionId).debug(
|
|
580
|
-
`Encountered internal error running command: ${errMsg}
|
|
620
|
+
`Encountered internal error running command: ${errMsg}`,
|
|
581
621
|
);
|
|
582
622
|
}
|
|
583
623
|
|
|
@@ -586,7 +626,8 @@ function buildHandler(
|
|
|
586
626
|
|
|
587
627
|
// decode the response, which is either a string or json
|
|
588
628
|
if (typeof httpResBody === 'string') {
|
|
589
|
-
res
|
|
629
|
+
res
|
|
630
|
+
.status(httpStatus)
|
|
590
631
|
.setHeader('content-type', 'application/json; charset=utf-8')
|
|
591
632
|
.send(httpResBody);
|
|
592
633
|
} else {
|
|
@@ -597,16 +638,17 @@ function buildHandler(
|
|
|
597
638
|
}
|
|
598
639
|
};
|
|
599
640
|
// add the method to the app
|
|
600
|
-
|
|
641
|
+
const registerRoute = (
|
|
642
|
+
app as Application & Record<string, (routePath: string, ...handlers: any[]) => void>
|
|
643
|
+
)[method.toLowerCase()].bind(app);
|
|
644
|
+
registerRoute(path, (req: Request, res: Response) => {
|
|
601
645
|
void asyncHandler(req, res);
|
|
602
646
|
});
|
|
603
647
|
}
|
|
604
648
|
|
|
605
649
|
async function doJwpProxy(driver: BaseDriver<any>, req: Request, res: Response): Promise<void> {
|
|
606
650
|
const sessionId = getSessionId(driver, req) as string;
|
|
607
|
-
getLogger(driver, sessionId).info(
|
|
608
|
-
'Driver proxy active, passing request on via HTTP proxy'
|
|
609
|
-
);
|
|
651
|
+
getLogger(driver, sessionId).info('Driver proxy active, passing request on via HTTP proxy');
|
|
610
652
|
|
|
611
653
|
// check that the inner driver has a proxy function
|
|
612
654
|
if (!driver.canProxy(sessionId)) {
|
|
@@ -617,8 +659,10 @@ async function doJwpProxy(driver: BaseDriver<any>, req: Request, res: Response):
|
|
|
617
659
|
} catch (err) {
|
|
618
660
|
if (isErrorType(err, errors.ProxyRequestError)) {
|
|
619
661
|
throw err;
|
|
620
|
-
}
|
|
662
|
+
}
|
|
663
|
+
if (err instanceof Error) {
|
|
621
664
|
throw new Error(`Could not proxy. Proxy error: ${err.message}`, {cause: err});
|
|
622
665
|
}
|
|
666
|
+
throw new Error(`Could not proxy. Proxy error: ${String(err)}`, {cause: err});
|
|
623
667
|
}
|
|
624
668
|
}
|
package/lib/protocol/routes.ts
CHANGED
|
@@ -14,7 +14,6 @@ const COMMAND_NAMES_CACHE = new LRUCache<string, string>({
|
|
|
14
14
|
* `optional`.
|
|
15
15
|
*/
|
|
16
16
|
export const METHOD_MAP = {
|
|
17
|
-
|
|
18
17
|
// #region W3C WebDriver
|
|
19
18
|
// https://www.w3.org/TR/webdriver2/
|
|
20
19
|
'/session': {
|
|
@@ -244,8 +243,8 @@ export const METHOD_MAP = {
|
|
|
244
243
|
'shrinkToFit',
|
|
245
244
|
'pageRanges',
|
|
246
245
|
],
|
|
247
|
-
}
|
|
248
|
-
}
|
|
246
|
+
},
|
|
247
|
+
},
|
|
249
248
|
},
|
|
250
249
|
// #endregion
|
|
251
250
|
|
|
@@ -274,7 +273,7 @@ export const METHOD_MAP = {
|
|
|
274
273
|
GET: {command: 'getOrientation'},
|
|
275
274
|
POST: {
|
|
276
275
|
command: 'setOrientation',
|
|
277
|
-
payloadParams: {required: ['orientation']}
|
|
276
|
+
payloadParams: {required: ['orientation']},
|
|
278
277
|
},
|
|
279
278
|
},
|
|
280
279
|
'/session/:sessionId/location': {
|
|
@@ -318,7 +317,7 @@ export const METHOD_MAP = {
|
|
|
318
317
|
GET: {command: 'getAppiumSessions'},
|
|
319
318
|
},
|
|
320
319
|
'/session/:sessionId/appium/capabilities': {
|
|
321
|
-
GET: {command: 'getAppiumSessionCapabilities'}
|
|
320
|
+
GET: {command: 'getAppiumSessionCapabilities'},
|
|
322
321
|
},
|
|
323
322
|
'/session/:sessionId/appium/settings': {
|
|
324
323
|
POST: {command: 'updateSettings', payloadParams: {required: ['settings']}},
|
|
@@ -592,7 +591,7 @@ export const ALL_COMMANDS = Object.values(METHOD_MAP)
|
|
|
592
591
|
export function routeToCommandName(
|
|
593
592
|
endpoint: string,
|
|
594
593
|
method?: HTTPMethod,
|
|
595
|
-
basePath?: string
|
|
594
|
+
basePath?: string,
|
|
596
595
|
): string | undefined {
|
|
597
596
|
const resolvedBasePath = basePath ?? DEFAULT_BASE_PATH;
|
|
598
597
|
let normalizedEndpoint = resolvedBasePath
|
|
@@ -624,9 +623,10 @@ export function routeToCommandName(
|
|
|
624
623
|
const routeMatcher = match(routePath);
|
|
625
624
|
if (possiblePathnames.some((pp) => routeMatcher(pp))) {
|
|
626
625
|
const spec = routeSpec as Record<string, DriverMethodDef<Driver>>;
|
|
627
|
-
const commandForAnyMethod = () =>
|
|
628
|
-
|
|
629
|
-
|
|
626
|
+
const commandForAnyMethod = () => Object.keys(spec).map((key) => spec[key]?.command)[0];
|
|
627
|
+
const commandName = normalizedMethod
|
|
628
|
+
? spec[normalizedMethod]?.command
|
|
629
|
+
: commandForAnyMethod();
|
|
630
630
|
if (commandName) {
|
|
631
631
|
COMMAND_NAMES_CACHE.set(cacheKey, commandName);
|
|
632
632
|
return commandName;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Environment variable that opts into deprecated built-in test pages on the Appium server. */
|
|
2
|
+
export const LEGACY_TEST_PAGES_ENV = 'APPIUM_ENABLE_LEGACY_TEST_PAGES';
|
|
3
|
+
|
|
4
|
+
const TRUTHY = new Set(['1', 'true', 'yes']);
|
|
5
|
+
|
|
6
|
+
/** @returns Whether built-in legacy test pages should be mounted on the Appium server. */
|
|
7
|
+
export function isLegacyTestPagesEnabled(): boolean {
|
|
8
|
+
return TRUTHY.has(String(process.env[LEGACY_TEST_PAGES_ENV] ?? '').toLowerCase());
|
|
9
|
+
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import {log} from '
|
|
2
|
+
import {log} from '../express/logger';
|
|
3
3
|
import {fs} from '@appium/support';
|
|
4
4
|
import {sleep} from 'asyncbox';
|
|
5
5
|
import type {Request, Response} from 'express';
|
|
6
|
-
import {compileLodashTemplate} from '
|
|
7
|
-
|
|
8
|
-
export const STATIC_DIR = resolveStaticDir();
|
|
6
|
+
import {compileLodashTemplate} from './template';
|
|
7
|
+
import {TEST_FIXTURES_DIR} from './static-dir';
|
|
9
8
|
|
|
10
9
|
type TemplateParams = Record<string, unknown>;
|
|
11
10
|
|
|
@@ -32,15 +31,8 @@ export async function welcome(req: Request, res: Response): Promise<void> {
|
|
|
32
31
|
res.send(template(params));
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
async function guineaPigTemplate(
|
|
36
|
-
req
|
|
37
|
-
res: Response,
|
|
38
|
-
page: string
|
|
39
|
-
): Promise<void> {
|
|
40
|
-
const delay = parseInt(
|
|
41
|
-
String(req.params.delay ?? (req.query?.delay ?? 0)),
|
|
42
|
-
10
|
|
43
|
-
);
|
|
34
|
+
async function guineaPigTemplate(req: Request, res: Response, page: string): Promise<void> {
|
|
35
|
+
const delay = parseInt(String(req.params.delay ?? req.query?.delay ?? 0), 10);
|
|
44
36
|
const throwError = String(req.params.throwError ?? req.query?.throwError ?? '');
|
|
45
37
|
const params: TemplateParams = {
|
|
46
38
|
throwError,
|
|
@@ -67,19 +59,7 @@ async function guineaPigTemplate(
|
|
|
67
59
|
res.send(template(params));
|
|
68
60
|
}
|
|
69
61
|
|
|
70
|
-
async function getTemplate(
|
|
71
|
-
templateName
|
|
72
|
-
): Promise<(params: TemplateParams) => string> {
|
|
73
|
-
const content = await fs.readFile(path.resolve(STATIC_DIR, 'test', templateName));
|
|
62
|
+
async function getTemplate(templateName: string): Promise<(params: TemplateParams) => string> {
|
|
63
|
+
const content = await fs.readFile(path.resolve(TEST_FIXTURES_DIR, 'test', templateName));
|
|
74
64
|
return compileLodashTemplate(content.toString());
|
|
75
65
|
}
|
|
76
|
-
|
|
77
|
-
function resolveStaticDir(): string {
|
|
78
|
-
const fromDir = __dirname;
|
|
79
|
-
const parts = path.resolve(fromDir).split(path.sep);
|
|
80
|
-
const baseDriverIndex = parts.indexOf('base-driver');
|
|
81
|
-
if (baseDriverIndex < 0) {
|
|
82
|
-
throw new Error(`Could not find the module root folder in the path: ${fromDir}`);
|
|
83
|
-
}
|
|
84
|
-
return path.join(parts.slice(0, baseDriverIndex + 1).join(path.sep), 'static');
|
|
85
|
-
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import type {Express} from 'express';
|
|
4
|
+
import favicon from 'serve-favicon';
|
|
5
|
+
import {guineaPig, guineaPigScrollable, guineaPigAppBanner, welcome} from './handlers';
|
|
6
|
+
import {produceError, produceCrash} from './crash';
|
|
7
|
+
import {TEST_FIXTURES_DIR} from './static-dir';
|
|
8
|
+
|
|
9
|
+
export interface RegisterTestPagesOpts {
|
|
10
|
+
basePath: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Mount deprecated built-in test pages and crash routes on an Express app.
|
|
15
|
+
*
|
|
16
|
+
* @deprecated Built-in test pages on the Appium server are deprecated and will be removed in
|
|
17
|
+
* Appium 4. Driver CI should hard-copy needed fixtures and run a local test HTTP server.
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
export function registerTestPages(app: Express, {basePath}: RegisterTestPagesOpts): void {
|
|
21
|
+
app.use(favicon(path.resolve(TEST_FIXTURES_DIR, 'favicon.ico')));
|
|
22
|
+
app.use(express.static(TEST_FIXTURES_DIR));
|
|
23
|
+
|
|
24
|
+
app.use(`${basePath}/produce_error`, produceError);
|
|
25
|
+
app.use(`${basePath}/crash`, produceCrash);
|
|
26
|
+
|
|
27
|
+
app.all('/welcome', welcome);
|
|
28
|
+
app.all('/test/guinea-pig', guineaPig);
|
|
29
|
+
app.all('/test/guinea-pig-scrollable', guineaPigScrollable);
|
|
30
|
+
app.all('/test/guinea-pig-app-banner', guineaPigAppBanner);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export {TEST_FIXTURES_DIR} from './static-dir';
|
|
34
|
+
export {isLegacyTestPagesEnabled} from './env';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Absolute path to bundled legacy test fixture static files.
|
|
5
|
+
*
|
|
6
|
+
* @deprecated Removed in Appium 4. Test fixture files will live in the appium/test-fixtures
|
|
7
|
+
* repository. Do not depend on this path in driver CI — hard-copy needed files locally.
|
|
8
|
+
*/
|
|
9
|
+
export const TEST_FIXTURES_DIR = resolveTestFixturesDir();
|
|
10
|
+
|
|
11
|
+
function resolveTestFixturesDir(): string {
|
|
12
|
+
const fromDir = __dirname;
|
|
13
|
+
const parts = path.resolve(fromDir).split(path.sep);
|
|
14
|
+
const baseDriverIndex = parts.indexOf('base-driver');
|
|
15
|
+
if (baseDriverIndex < 0) {
|
|
16
|
+
throw new Error(`Could not find the module root folder in the path: ${fromDir}`);
|
|
17
|
+
}
|
|
18
|
+
return path.join(parts.slice(0, baseDriverIndex + 1).join(path.sep), 'test-fixtures', 'static');
|
|
19
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Compile a lodash-style template string (`<%= expression %>`) into a render function. */
|
|
2
|
+
export function compileLodashTemplate(
|
|
3
|
+
template: string,
|
|
4
|
+
): (params: Record<string, unknown>) => string {
|
|
5
|
+
const parts: string[] = [];
|
|
6
|
+
let lastIndex = 0;
|
|
7
|
+
const re = /<%=\s*([\s\S]+?)\s*%>/g;
|
|
8
|
+
let match;
|
|
9
|
+
while ((match = re.exec(template)) !== null) {
|
|
10
|
+
parts.push(JSON.stringify(template.slice(lastIndex, match.index)));
|
|
11
|
+
parts.push(`String(${match[1]})`);
|
|
12
|
+
lastIndex = match.index + match[0].length;
|
|
13
|
+
}
|
|
14
|
+
parts.push(JSON.stringify(template.slice(lastIndex)));
|
|
15
|
+
const fn = new Function('obj', `with (obj) { return ${parts.join(' + ')}; }`);
|
|
16
|
+
return (params) => fn(params) as string;
|
|
17
|
+
}
|
package/lib/utils.ts
CHANGED
|
@@ -48,7 +48,7 @@ export function omitKeys<T extends Record<string, unknown>>(obj: T, keys: readon
|
|
|
48
48
|
/** Return a shallow copy of `obj` containing only listed keys. */
|
|
49
49
|
export function pick<T extends Record<string, unknown>>(
|
|
50
50
|
obj: T,
|
|
51
|
-
keys: readonly string[]
|
|
51
|
+
keys: readonly string[],
|
|
52
52
|
): Partial<T> {
|
|
53
53
|
const keysToPick = new Set(keys);
|
|
54
54
|
return Object.fromEntries(Object.entries(obj).filter(([k]) => keysToPick.has(k))) as Partial<T>;
|
|
@@ -57,29 +57,9 @@ export function pick<T extends Record<string, unknown>>(
|
|
|
57
57
|
/** Return a shallow copy of `obj` whose entries pass `predicate`. */
|
|
58
58
|
export function pickBy<T extends Record<string, unknown>>(
|
|
59
59
|
obj: T,
|
|
60
|
-
predicate: (value: T[keyof T], key: keyof T) => boolean
|
|
60
|
+
predicate: (value: T[keyof T], key: keyof T) => boolean,
|
|
61
61
|
): Partial<T> {
|
|
62
62
|
return Object.fromEntries(
|
|
63
|
-
Object.entries(obj).filter(([key, value]) =>
|
|
64
|
-
predicate(value as T[keyof T], key as keyof T)
|
|
65
|
-
)
|
|
63
|
+
Object.entries(obj).filter(([key, value]) => predicate(value as T[keyof T], key as keyof T)),
|
|
66
64
|
) as Partial<T>;
|
|
67
65
|
}
|
|
68
|
-
|
|
69
|
-
/** Compile a lodash-style template string (`<%= expression %>`) into a render function. */
|
|
70
|
-
export function compileLodashTemplate(
|
|
71
|
-
template: string
|
|
72
|
-
): (params: Record<string, unknown>) => string {
|
|
73
|
-
const parts: string[] = [];
|
|
74
|
-
let lastIndex = 0;
|
|
75
|
-
const re = /<%=\s*([\s\S]+?)\s*%>/g;
|
|
76
|
-
let match;
|
|
77
|
-
while ((match = re.exec(template)) !== null) {
|
|
78
|
-
parts.push(JSON.stringify(template.slice(lastIndex, match.index)));
|
|
79
|
-
parts.push(`String(${match[1]})`);
|
|
80
|
-
lastIndex = match.index + match[0].length;
|
|
81
|
-
}
|
|
82
|
-
parts.push(JSON.stringify(template.slice(lastIndex)));
|
|
83
|
-
const fn = new Function('obj', `with (obj) { return ${parts.join(' + ')}; }`);
|
|
84
|
-
return (params) => fn(params) as string;
|
|
85
|
-
}
|