@atproto/xrpc-server 0.4.2 → 0.4.4-next.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/CHANGELOG.md +18 -0
- package/LICENSE.txt +1 -1
- package/dist/auth.d.ts +3 -2
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +124 -0
- package/dist/auth.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -53377
- package/dist/index.js.map +1 -7
- package/dist/logger.d.ts +1 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +7 -0
- package/dist/logger.js.map +1 -0
- package/dist/rate-limiter.d.ts +2 -1
- package/dist/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter.js +166 -0
- package/dist/rate-limiter.js.map +1 -0
- package/dist/server.d.ts +6 -5
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +472 -0
- package/dist/server.js.map +1 -0
- package/dist/stream/frames.d.ts +2 -1
- package/dist/stream/frames.d.ts.map +1 -0
- package/dist/stream/frames.js +141 -0
- package/dist/stream/frames.js.map +1 -0
- package/dist/stream/index.d.ts +1 -0
- package/dist/stream/index.d.ts.map +1 -0
- package/dist/stream/index.js +22 -0
- package/dist/stream/index.js.map +1 -0
- package/dist/stream/logger.d.ts +1 -0
- package/dist/stream/logger.d.ts.map +1 -0
- package/dist/stream/logger.js +7 -0
- package/dist/stream/logger.js.map +1 -0
- package/dist/stream/server.d.ts +3 -1
- package/dist/stream/server.d.ts.map +1 -0
- package/dist/stream/server.js +70 -0
- package/dist/stream/server.js.map +1 -0
- package/dist/stream/stream.d.ts +1 -0
- package/dist/stream/stream.d.ts.map +1 -0
- package/dist/stream/stream.js +44 -0
- package/dist/stream/stream.js.map +1 -0
- package/dist/stream/subscription.d.ts +2 -0
- package/dist/stream/subscription.d.ts.map +1 -0
- package/dist/stream/subscription.js +80 -0
- package/dist/stream/subscription.js.map +1 -0
- package/dist/stream/types.d.ts +5 -4
- package/dist/stream/types.d.ts.map +1 -0
- package/dist/stream/types.js +47 -0
- package/dist/stream/types.js.map +1 -0
- package/dist/stream/websocket-keepalive.d.ts +2 -0
- package/dist/stream/websocket-keepalive.d.ts.map +1 -0
- package/dist/stream/websocket-keepalive.js +160 -0
- package/dist/stream/websocket-keepalive.js.map +1 -0
- package/dist/types.d.ts +54 -34
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +163 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +3 -2
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +263 -0
- package/dist/util.js.map +1 -0
- package/jest.config.js +4 -3
- package/package.json +10 -11
- package/src/rate-limiter.ts +3 -0
- package/src/server.ts +53 -14
- package/src/stream/frames.ts +1 -1
- package/src/stream/websocket-keepalive.ts +2 -1
- package/src/types.ts +22 -10
- package/src/util.ts +3 -3
- package/tests/bodies.test.ts +4 -4
- package/tests/errors.test.ts +1 -1
- package/tsconfig.build.json +6 -2
- package/tsconfig.json +3 -11
- package/tsconfig.tests.json +7 -0
- package/babel.config.js +0 -1
- package/build.js +0 -14
package/jest.config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/** @type {import('jest').Config} */
|
|
3
2
|
module.exports = {
|
|
4
|
-
...base,
|
|
5
3
|
displayName: 'XRPC Server',
|
|
4
|
+
transform: { '^.+\\.(t|j)s$': '@swc/jest' },
|
|
5
|
+
transformIgnorePatterns: [`<rootDir>/node_modules/(?!get-port)`],
|
|
6
|
+
setupFiles: ['<rootDir>/../../jest.setup.ts'],
|
|
6
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/xrpc-server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4-next.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "atproto HTTP API (XRPC) server library",
|
|
6
6
|
"keywords": [
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"directory": "packages/xrpc-server"
|
|
15
15
|
},
|
|
16
16
|
"main": "dist/index.js",
|
|
17
|
+
"types": "dist/index.d.ts",
|
|
17
18
|
"dependencies": {
|
|
18
19
|
"cbor-x": "^1.5.1",
|
|
19
20
|
"express": "^4.17.2",
|
|
@@ -23,9 +24,9 @@
|
|
|
23
24
|
"uint8arrays": "3.0.0",
|
|
24
25
|
"ws": "^8.12.0",
|
|
25
26
|
"zod": "^3.21.4",
|
|
26
|
-
"@atproto/common": "^0.3.
|
|
27
|
-
"@atproto/
|
|
28
|
-
"@atproto/
|
|
27
|
+
"@atproto/common": "^0.3.4-next.0",
|
|
28
|
+
"@atproto/lexicon": "^0.3.3-next.0",
|
|
29
|
+
"@atproto/crypto": "^0.3.1-next.0"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
|
31
32
|
"@types/express": "^4.17.13",
|
|
@@ -34,16 +35,14 @@
|
|
|
34
35
|
"@types/ws": "^8.5.4",
|
|
35
36
|
"get-port": "^6.1.2",
|
|
36
37
|
"jose": "^4.15.4",
|
|
38
|
+
"jest": "^28.1.2",
|
|
37
39
|
"key-encoder": "^2.0.3",
|
|
38
40
|
"multiformats": "^9.9.0",
|
|
39
|
-
"@atproto/crypto": "^0.3.0",
|
|
40
|
-
"@atproto/xrpc": "^0.4.
|
|
41
|
+
"@atproto/crypto": "^0.3.1-next.0",
|
|
42
|
+
"@atproto/xrpc": "^0.4.3-next.0"
|
|
41
43
|
},
|
|
42
44
|
"scripts": {
|
|
43
45
|
"test": "jest",
|
|
44
|
-
"build": "
|
|
45
|
-
|
|
46
|
-
"update-main-to-dist": "node ../../update-main-to-dist.js packages/xrpc-server"
|
|
47
|
-
},
|
|
48
|
-
"types": "dist/index.d.ts"
|
|
46
|
+
"build": "tsc --build tsconfig.build.json"
|
|
47
|
+
}
|
|
49
48
|
}
|
package/src/rate-limiter.ts
CHANGED
|
@@ -75,6 +75,9 @@ export class RateLimiter implements RateLimiterI {
|
|
|
75
75
|
return null
|
|
76
76
|
}
|
|
77
77
|
const key = opts?.calcKey ? opts.calcKey(ctx) : this.calcKey(ctx)
|
|
78
|
+
if (key === null) {
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
78
81
|
const points = opts?.calcPoints
|
|
79
82
|
? opts.calcPoints(ctx)
|
|
80
83
|
: this.calcPoints(ctx)
|
package/src/server.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { Readable } from 'stream'
|
|
2
2
|
import express, {
|
|
3
|
+
Application,
|
|
4
|
+
Express,
|
|
5
|
+
Router,
|
|
6
|
+
Request,
|
|
7
|
+
Response,
|
|
3
8
|
ErrorRequestHandler,
|
|
4
9
|
NextFunction,
|
|
5
10
|
RequestHandler,
|
|
@@ -36,6 +41,7 @@ import {
|
|
|
36
41
|
RateLimiterConsume,
|
|
37
42
|
isShared,
|
|
38
43
|
RateLimitExceededError,
|
|
44
|
+
HandlerPipeThrough,
|
|
39
45
|
} from './types'
|
|
40
46
|
import {
|
|
41
47
|
decodeQueryParams,
|
|
@@ -51,8 +57,8 @@ export function createServer(lexicons?: LexiconDoc[], options?: Options) {
|
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
export class Server {
|
|
54
|
-
router = express()
|
|
55
|
-
routes = express.Router()
|
|
60
|
+
router: Express = express()
|
|
61
|
+
routes: Router = express.Router()
|
|
56
62
|
subscriptions = new Map<string, XrpcStreamServer>()
|
|
57
63
|
lex = new Lexicons()
|
|
58
64
|
options: Options
|
|
@@ -68,7 +74,7 @@ export class Server {
|
|
|
68
74
|
this.router.use(this.routes)
|
|
69
75
|
this.router.use('/xrpc/:methodId', this.catchall.bind(this))
|
|
70
76
|
this.router.use(errorMiddleware)
|
|
71
|
-
this.router.once('mount', (app:
|
|
77
|
+
this.router.once('mount', (app: Application) => {
|
|
72
78
|
this.enableStreamingOnListen(app)
|
|
73
79
|
})
|
|
74
80
|
this.options = opts ?? {}
|
|
@@ -173,15 +179,11 @@ export class Server {
|
|
|
173
179
|
this.routes[verb](
|
|
174
180
|
`/xrpc/${nsid}`,
|
|
175
181
|
...middleware,
|
|
176
|
-
this.createHandler(nsid, def, config
|
|
182
|
+
this.createHandler(nsid, def, config),
|
|
177
183
|
)
|
|
178
184
|
}
|
|
179
185
|
|
|
180
|
-
async catchall(
|
|
181
|
-
req: express.Request,
|
|
182
|
-
_res: express.Response,
|
|
183
|
-
next: NextFunction,
|
|
184
|
-
) {
|
|
186
|
+
async catchall(req: Request, _res: Response, next: NextFunction) {
|
|
185
187
|
const def = this.lex.getDef(req.params.methodId)
|
|
186
188
|
if (!def) {
|
|
187
189
|
return next(new MethodNotImplementedError())
|
|
@@ -206,10 +208,13 @@ export class Server {
|
|
|
206
208
|
createHandler(
|
|
207
209
|
nsid: string,
|
|
208
210
|
def: LexXrpcQuery | LexXrpcProcedure,
|
|
209
|
-
|
|
211
|
+
routeCfg: XRPCHandlerConfig,
|
|
210
212
|
): RequestHandler {
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
+
const routeOpts = {
|
|
214
|
+
blobLimit: routeCfg.opts?.blobLimit ?? this.options.payload?.blobLimit,
|
|
215
|
+
}
|
|
216
|
+
const validateReqInput = (req: Request) =>
|
|
217
|
+
validateInput(nsid, def, req, routeOpts, this.lex)
|
|
213
218
|
const validateResOutput =
|
|
214
219
|
this.options.validateResponse === false
|
|
215
220
|
? (output?: HandlerSuccess) => output
|
|
@@ -254,12 +259,26 @@ export class Server {
|
|
|
254
259
|
}
|
|
255
260
|
|
|
256
261
|
// run the handler
|
|
257
|
-
const outputUnvalidated = await handler(reqCtx)
|
|
262
|
+
const outputUnvalidated = await routeCfg.handler(reqCtx)
|
|
258
263
|
|
|
259
264
|
if (isHandlerError(outputUnvalidated)) {
|
|
260
265
|
throw XRPCError.fromError(outputUnvalidated)
|
|
261
266
|
}
|
|
262
267
|
|
|
268
|
+
if (outputUnvalidated && isHandlerPipeThrough(outputUnvalidated)) {
|
|
269
|
+
// set headers
|
|
270
|
+
if (outputUnvalidated?.headers) {
|
|
271
|
+
Object.entries(outputUnvalidated.headers).forEach(([name, val]) => {
|
|
272
|
+
res.header(name, val)
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
res
|
|
276
|
+
.header('Content-Type', outputUnvalidated.encoding)
|
|
277
|
+
.status(200)
|
|
278
|
+
.send(Buffer.from(outputUnvalidated.buffer))
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
|
|
263
282
|
if (!outputUnvalidated || isHandlerSuccess(outputUnvalidated)) {
|
|
264
283
|
// validate response
|
|
265
284
|
const output = validateResOutput(outputUnvalidated)
|
|
@@ -371,7 +390,7 @@ export class Server {
|
|
|
371
390
|
)
|
|
372
391
|
}
|
|
373
392
|
|
|
374
|
-
private enableStreamingOnListen(app:
|
|
393
|
+
private enableStreamingOnListen(app: Application) {
|
|
375
394
|
const _listen = app.listen
|
|
376
395
|
app.listen = (...args) => {
|
|
377
396
|
// @ts-ignore the args spread
|
|
@@ -445,6 +464,26 @@ function isHandlerSuccess(v: HandlerOutput): v is HandlerSuccess {
|
|
|
445
464
|
return handlerSuccess.safeParse(v).success
|
|
446
465
|
}
|
|
447
466
|
|
|
467
|
+
function isHandlerPipeThrough(v: HandlerOutput): v is HandlerPipeThrough {
|
|
468
|
+
if (v === null || typeof v !== 'object') {
|
|
469
|
+
return false
|
|
470
|
+
}
|
|
471
|
+
if (!isString(v['encoding']) || !(v['buffer'] instanceof ArrayBuffer)) {
|
|
472
|
+
return false
|
|
473
|
+
}
|
|
474
|
+
if (v['headers'] !== undefined) {
|
|
475
|
+
if (v['headers'] === null || typeof v['headers'] !== 'object') {
|
|
476
|
+
return false
|
|
477
|
+
}
|
|
478
|
+
if (!Object.values(v['headers']).every(isString)) {
|
|
479
|
+
return false
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return true
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const isString = (val: unknown): val is string => typeof val === 'string'
|
|
486
|
+
|
|
448
487
|
const kRequestLocals = Symbol('requestLocals')
|
|
449
488
|
|
|
450
489
|
function createLocalsMiddleware(nsid: string): RequestHandler {
|
package/src/stream/frames.ts
CHANGED
|
@@ -80,7 +80,7 @@ export class WebSocketKeepAlive {
|
|
|
80
80
|
|
|
81
81
|
startHeartbeat(ws: WebSocket) {
|
|
82
82
|
let isAlive = true
|
|
83
|
-
let heartbeatInterval: NodeJS.
|
|
83
|
+
let heartbeatInterval: NodeJS.Timeout | null = null
|
|
84
84
|
|
|
85
85
|
const checkAlive = () => {
|
|
86
86
|
if (!isAlive) {
|
|
@@ -145,6 +145,7 @@ function forwardSignal(signal: AbortSignal, ac: AbortController) {
|
|
|
145
145
|
return ac.abort(signal.reason)
|
|
146
146
|
} else {
|
|
147
147
|
signal.addEventListener('abort', () => ac.abort(signal.reason), {
|
|
148
|
+
// @ts-ignore https://github.com/DefinitelyTyped/DefinitelyTyped/pull/68625
|
|
148
149
|
signal: ac.signal,
|
|
149
150
|
})
|
|
150
151
|
}
|
package/src/types.ts
CHANGED
|
@@ -46,6 +46,13 @@ export const handlerSuccess = zod.object({
|
|
|
46
46
|
})
|
|
47
47
|
export type HandlerSuccess = zod.infer<typeof handlerSuccess>
|
|
48
48
|
|
|
49
|
+
export const handlerPipeThrough = zod.object({
|
|
50
|
+
encoding: zod.string(),
|
|
51
|
+
buffer: zod.instanceof(ArrayBuffer),
|
|
52
|
+
headers: zod.record(zod.string()).optional(),
|
|
53
|
+
})
|
|
54
|
+
export type HandlerPipeThrough = zod.infer<typeof handlerPipeThrough>
|
|
55
|
+
|
|
49
56
|
export const handlerError = zod.object({
|
|
50
57
|
status: zod.number(),
|
|
51
58
|
error: zod.string().optional(),
|
|
@@ -53,7 +60,7 @@ export const handlerError = zod.object({
|
|
|
53
60
|
})
|
|
54
61
|
export type HandlerError = zod.infer<typeof handlerError>
|
|
55
62
|
|
|
56
|
-
export type HandlerOutput = HandlerSuccess | HandlerError
|
|
63
|
+
export type HandlerOutput = HandlerSuccess | HandlerPipeThrough | HandlerError
|
|
57
64
|
|
|
58
65
|
export type XRPCReqContext = {
|
|
59
66
|
auth: HandlerAuth | undefined
|
|
@@ -85,7 +92,7 @@ export type StreamAuthVerifier = (ctx: {
|
|
|
85
92
|
req: IncomingMessage
|
|
86
93
|
}) => Promise<AuthOutput> | AuthOutput
|
|
87
94
|
|
|
88
|
-
export type CalcKeyFn = (ctx: XRPCReqContext) => string
|
|
95
|
+
export type CalcKeyFn = (ctx: XRPCReqContext) => string | null
|
|
89
96
|
export type CalcPointsFn = (ctx: XRPCReqContext) => number
|
|
90
97
|
|
|
91
98
|
export interface RateLimiterI {
|
|
@@ -101,29 +108,29 @@ export type RateLimiterCreator = (opts: {
|
|
|
101
108
|
keyPrefix: string
|
|
102
109
|
durationMs: number
|
|
103
110
|
points: number
|
|
104
|
-
calcKey?:
|
|
105
|
-
calcPoints?:
|
|
111
|
+
calcKey?: CalcKeyFn
|
|
112
|
+
calcPoints?: CalcPointsFn
|
|
106
113
|
}) => RateLimiterI
|
|
107
114
|
|
|
108
115
|
export type ServerRateLimitDescription = {
|
|
109
116
|
name: string
|
|
110
117
|
durationMs: number
|
|
111
118
|
points: number
|
|
112
|
-
calcKey?:
|
|
113
|
-
calcPoints?:
|
|
119
|
+
calcKey?: CalcKeyFn
|
|
120
|
+
calcPoints?: CalcPointsFn
|
|
114
121
|
}
|
|
115
122
|
|
|
116
123
|
export type SharedRateLimitOpts = {
|
|
117
124
|
name: string
|
|
118
|
-
calcKey?:
|
|
119
|
-
calcPoints?:
|
|
125
|
+
calcKey?: CalcKeyFn
|
|
126
|
+
calcPoints?: CalcPointsFn
|
|
120
127
|
}
|
|
121
128
|
|
|
122
129
|
export type RouteRateLimitOpts = {
|
|
123
130
|
durationMs: number
|
|
124
131
|
points: number
|
|
125
|
-
calcKey?:
|
|
126
|
-
calcPoints?:
|
|
132
|
+
calcKey?: CalcKeyFn
|
|
133
|
+
calcPoints?: CalcPointsFn
|
|
127
134
|
}
|
|
128
135
|
|
|
129
136
|
export type HandlerRateLimitOpts = SharedRateLimitOpts | RouteRateLimitOpts
|
|
@@ -143,7 +150,12 @@ export type RateLimiterStatus = {
|
|
|
143
150
|
isFirstInDuration: boolean
|
|
144
151
|
}
|
|
145
152
|
|
|
153
|
+
export type RouteOpts = {
|
|
154
|
+
blobLimit?: number
|
|
155
|
+
}
|
|
156
|
+
|
|
146
157
|
export type XRPCHandlerConfig = {
|
|
158
|
+
opts?: RouteOpts
|
|
147
159
|
rateLimit?: HandlerRateLimitOpts | HandlerRateLimitOpts[]
|
|
148
160
|
auth?: AuthVerifier
|
|
149
161
|
handler: XRPCHandler
|
package/src/util.ts
CHANGED
|
@@ -19,8 +19,8 @@ import {
|
|
|
19
19
|
handlerSuccess,
|
|
20
20
|
InvalidRequestError,
|
|
21
21
|
InternalServerError,
|
|
22
|
-
Options,
|
|
23
22
|
XRPCError,
|
|
23
|
+
RouteOpts,
|
|
24
24
|
} from './types'
|
|
25
25
|
|
|
26
26
|
export function decodeQueryParams(
|
|
@@ -82,7 +82,7 @@ export function validateInput(
|
|
|
82
82
|
nsid: string,
|
|
83
83
|
def: LexXrpcProcedure | LexXrpcQuery,
|
|
84
84
|
req: express.Request,
|
|
85
|
-
opts:
|
|
85
|
+
opts: RouteOpts,
|
|
86
86
|
lexicons: Lexicons,
|
|
87
87
|
): HandlerInput | undefined {
|
|
88
88
|
// request expectation
|
|
@@ -139,7 +139,7 @@ export function validateInput(
|
|
|
139
139
|
if (req.readableEnded) {
|
|
140
140
|
body = req.body
|
|
141
141
|
} else {
|
|
142
|
-
body = decodeBodyStream(req, opts.
|
|
142
|
+
body = decodeBodyStream(req, opts.blobLimit)
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
return {
|
package/tests/bodies.test.ts
CHANGED
|
@@ -274,9 +274,9 @@ describe('Bodies', () => {
|
|
|
274
274
|
const resBody = await res.json()
|
|
275
275
|
const status = res.status
|
|
276
276
|
expect(status).toBe(400)
|
|
277
|
-
expect(resBody
|
|
278
|
-
|
|
279
|
-
'Request encoding (Content-Type) required but not provided',
|
|
280
|
-
)
|
|
277
|
+
expect(resBody).toMatchObject({
|
|
278
|
+
error: 'InvalidRequest',
|
|
279
|
+
message: 'Request encoding (Content-Type) required but not provided',
|
|
280
|
+
})
|
|
281
281
|
})
|
|
282
282
|
})
|
package/tests/errors.test.ts
CHANGED
package/tsconfig.build.json
CHANGED
package/tsconfig.json
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"rootDir": "./src",
|
|
5
|
-
"outDir": "./dist", // Your outDir,
|
|
6
|
-
"emitDeclarationOnly": true
|
|
7
|
-
},
|
|
8
|
-
"include": ["./src", "__tests__/**/**.ts"],
|
|
2
|
+
"include": [],
|
|
9
3
|
"references": [
|
|
10
|
-
{ "path": "
|
|
11
|
-
{ "path": "
|
|
12
|
-
{ "path": "../lexicon/tsconfig.build.json" },
|
|
13
|
-
{ "path": "../xrpc/tsconfig.build.json" }
|
|
4
|
+
{ "path": "./tsconfig.build.json" },
|
|
5
|
+
{ "path": "./tsconfig.tests.json" }
|
|
14
6
|
]
|
|
15
7
|
}
|
package/babel.config.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
module.exports = require('../../babel.config.js')
|
package/build.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
const { nodeExternalsPlugin } = require('esbuild-node-externals')
|
|
2
|
-
|
|
3
|
-
const buildShallow =
|
|
4
|
-
process.argv.includes('--shallow') || process.env.ATP_BUILD_SHALLOW === 'true'
|
|
5
|
-
|
|
6
|
-
require('esbuild').build({
|
|
7
|
-
logLevel: 'info',
|
|
8
|
-
entryPoints: ['src/index.ts'],
|
|
9
|
-
bundle: true,
|
|
10
|
-
sourcemap: true,
|
|
11
|
-
outdir: 'dist',
|
|
12
|
-
platform: 'node',
|
|
13
|
-
plugins: buildShallow ? [nodeExternalsPlugin()] : [],
|
|
14
|
-
})
|