@gxp-dev/tools 2.0.71 → 2.0.73

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.
@@ -0,0 +1,358 @@
1
+ /**
2
+ * MCP tools for plugin testing:
3
+ *
4
+ * - test_scaffold_component_test : writes a Vitest + Vue Test Utils file
5
+ * for a given Vue component, with render/props/events placeholders.
6
+ *
7
+ * - test_api_route : resolves an OpenAPI operationId to method+path,
8
+ * substitutes path parameters, and hits the local mock API (default
9
+ * http://localhost:3069/api) with optional query and body, returning
10
+ * status, headers, and response body.
11
+ */
12
+
13
+ const fs = require("fs")
14
+ const path = require("path")
15
+
16
+ const { fetchSpec } = require("./specs")
17
+
18
+ function contentResult(obj) {
19
+ return {
20
+ content: [{ type: "text", text: JSON.stringify(obj, null, 2) }],
21
+ }
22
+ }
23
+
24
+ function resolveProjectPath(p) {
25
+ if (!p) throw new Error("`path` argument is required")
26
+ return path.isAbsolute(p) ? p : path.resolve(process.cwd(), p)
27
+ }
28
+
29
+ /* ------------------------- scaffold_component_test ----------------------- */
30
+
31
+ function componentNameFromFile(componentPath) {
32
+ const base = path.basename(componentPath).replace(/\.vue$/i, "")
33
+ return base.replace(/[^A-Za-z0-9_$]/g, "_") || "Component"
34
+ }
35
+
36
+ function defaultTestPathFor(componentAbsPath, projectRoot) {
37
+ const rel = path.relative(projectRoot, componentAbsPath)
38
+ const base = path.basename(rel, ".vue")
39
+ // Siblings to the project root: tests/<Component>.test.js
40
+ return path.join(projectRoot, "tests", `${base}.test.js`)
41
+ }
42
+
43
+ function renderScaffold({ componentName, importPath }) {
44
+ return `import { describe, it, expect } from "vitest"
45
+ import { mount } from "@vue/test-utils"
46
+ import ${componentName} from "${importPath}"
47
+
48
+ describe("${componentName}", () => {
49
+ it("renders the component", () => {
50
+ const wrapper = mount(${componentName})
51
+ expect(wrapper.exists()).toBe(true)
52
+ })
53
+
54
+ it.todo("renders expected headings/labels")
55
+
56
+ it.todo("accepts props")
57
+
58
+ it.todo("emits events on interaction")
59
+ })
60
+ `
61
+ }
62
+
63
+ function toImportPath(absComponent, absTestFile, projectRoot) {
64
+ // Prefer the "@/…" alias when the component lives under <projectRoot>/src.
65
+ const srcDir = path.join(projectRoot, "src")
66
+ if (absComponent.startsWith(srcDir + path.sep) || absComponent === srcDir) {
67
+ const rel = path.relative(srcDir, absComponent).replace(/\\/g, "/")
68
+ return `@/${rel}`
69
+ }
70
+ // Fall back to a relative path from the test file.
71
+ const rel = path.relative(path.dirname(absTestFile), absComponent)
72
+ const norm = rel.replace(/\\/g, "/")
73
+ return norm.startsWith(".") ? norm : `./${norm}`
74
+ }
75
+
76
+ function scaffoldComponentTest({
77
+ componentPath,
78
+ testPath,
79
+ componentName,
80
+ overwrite = false,
81
+ projectRoot,
82
+ }) {
83
+ const projRoot = projectRoot || process.cwd()
84
+ const absComponent = resolveProjectPath(componentPath)
85
+ if (!fs.existsSync(absComponent)) {
86
+ return { ok: false, error: `Component not found: ${absComponent}` }
87
+ }
88
+
89
+ const absTest = testPath
90
+ ? resolveProjectPath(testPath)
91
+ : defaultTestPathFor(absComponent, projRoot)
92
+ const name = componentName || componentNameFromFile(absComponent)
93
+ const importPath = toImportPath(absComponent, absTest, projRoot)
94
+
95
+ if (fs.existsSync(absTest) && !overwrite) {
96
+ return {
97
+ ok: false,
98
+ error: `Test already exists at ${absTest}. Pass overwrite: true to replace.`,
99
+ test_path: absTest,
100
+ }
101
+ }
102
+
103
+ fs.mkdirSync(path.dirname(absTest), { recursive: true })
104
+ fs.writeFileSync(
105
+ absTest,
106
+ renderScaffold({ componentName: name, importPath }),
107
+ "utf-8",
108
+ )
109
+ return {
110
+ ok: true,
111
+ test_path: absTest,
112
+ component_name: name,
113
+ import_path: importPath,
114
+ run_with: "npx vitest run " + path.relative(projRoot, absTest),
115
+ }
116
+ }
117
+
118
+ /* ------------------------------ test_api_route --------------------------- */
119
+
120
+ function substitutePathParams(rawPath, params = {}) {
121
+ // Replace /path/{name} and /path/:name with the given values.
122
+ const missing = []
123
+ let out = rawPath.replace(/\{([^}]+)\}/g, (_m, name) => {
124
+ if (params[name] === undefined || params[name] === null) {
125
+ missing.push(name)
126
+ return `{${name}}`
127
+ }
128
+ return encodeURIComponent(String(params[name]))
129
+ })
130
+ out = out.replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, (_m, name) => {
131
+ if (params[name] === undefined || params[name] === null) {
132
+ missing.push(name)
133
+ return `:${name}`
134
+ }
135
+ return encodeURIComponent(String(params[name]))
136
+ })
137
+ return { path: out, missing }
138
+ }
139
+
140
+ function buildQueryString(query) {
141
+ if (!query || typeof query !== "object") return ""
142
+ const params = new URLSearchParams()
143
+ for (const [k, v] of Object.entries(query)) {
144
+ if (v === undefined || v === null) continue
145
+ if (Array.isArray(v)) {
146
+ for (const item of v) params.append(k, String(item))
147
+ } else {
148
+ params.set(k, String(v))
149
+ }
150
+ }
151
+ const s = params.toString()
152
+ return s ? `?${s}` : ""
153
+ }
154
+
155
+ function findOperation(spec, operationId) {
156
+ if (!spec?.paths) return null
157
+ for (const [p, methods] of Object.entries(spec.paths)) {
158
+ for (const [method, op] of Object.entries(methods)) {
159
+ if (typeof op === "object" && op?.operationId === operationId) {
160
+ return { path: p, method: method.toUpperCase(), op }
161
+ }
162
+ }
163
+ }
164
+ return null
165
+ }
166
+
167
+ async function testApiRoute({
168
+ operationId,
169
+ pathParams,
170
+ query,
171
+ body,
172
+ headers,
173
+ baseUrl,
174
+ timeoutMs,
175
+ }) {
176
+ const spec = await fetchSpec("openapi")
177
+ const found = findOperation(spec, operationId)
178
+ if (!found) {
179
+ return { ok: false, error: `Operation not found: ${operationId}` }
180
+ }
181
+
182
+ const { path: rawPath, method } = found
183
+ const { path: resolvedPath, missing } = substitutePathParams(
184
+ rawPath,
185
+ pathParams,
186
+ )
187
+ if (missing.length > 0) {
188
+ return {
189
+ ok: false,
190
+ error: `Missing required path parameters: ${missing.join(", ")}`,
191
+ required_parameters: missing,
192
+ }
193
+ }
194
+
195
+ const base = (baseUrl || "http://localhost:3069/api").replace(/\/+$/, "")
196
+ const url = `${base}${resolvedPath}${buildQueryString(query)}`
197
+
198
+ const controller = new AbortController()
199
+ const timer = setTimeout(
200
+ () => controller.abort(),
201
+ Math.max(1000, timeoutMs ?? 10000),
202
+ )
203
+ const t0 = Date.now()
204
+ try {
205
+ const hasBody =
206
+ body !== undefined && body !== null && !["GET", "HEAD"].includes(method)
207
+ const res = await fetch(url, {
208
+ method,
209
+ signal: controller.signal,
210
+ headers: {
211
+ Accept: "application/json",
212
+ ...(hasBody ? { "Content-Type": "application/json" } : {}),
213
+ ...(headers || {}),
214
+ },
215
+ body: hasBody ? JSON.stringify(body) : undefined,
216
+ })
217
+
218
+ const duration_ms = Date.now() - t0
219
+ const respHeaders = {}
220
+ res.headers.forEach((v, k) => {
221
+ respHeaders[k] = v
222
+ })
223
+ const raw = await res.text()
224
+ let parsedBody = raw
225
+ try {
226
+ parsedBody = JSON.parse(raw)
227
+ } catch {
228
+ // keep as text
229
+ }
230
+
231
+ return {
232
+ ok: res.ok,
233
+ request: {
234
+ method,
235
+ url,
236
+ },
237
+ response: {
238
+ status: res.status,
239
+ statusText: res.statusText,
240
+ headers: respHeaders,
241
+ body: parsedBody,
242
+ },
243
+ duration_ms,
244
+ }
245
+ } catch (err) {
246
+ const duration_ms = Date.now() - t0
247
+ return {
248
+ ok: false,
249
+ request: { method, url },
250
+ error: err.name === "AbortError" ? "Request timed out" : err.message,
251
+ duration_ms,
252
+ }
253
+ } finally {
254
+ clearTimeout(timer)
255
+ }
256
+ }
257
+
258
+ /* ------------------------------- tool schemas ----------------------------- */
259
+
260
+ const TEST_TOOLS = [
261
+ {
262
+ name: "test_scaffold_component_test",
263
+ description:
264
+ "Create a Vitest + Vue Test Utils test file for a given Vue component. Picks an import path via the @/ alias when the component lives under src/, else falls back to a relative path. Refuses to overwrite unless overwrite=true.",
265
+ inputSchema: {
266
+ type: "object",
267
+ properties: {
268
+ componentPath: {
269
+ type: "string",
270
+ description:
271
+ "Absolute or project-relative path to the .vue file to test.",
272
+ },
273
+ testPath: {
274
+ type: "string",
275
+ description:
276
+ "Optional override for where to write the test file. Default: tests/<ComponentName>.test.js at the project root.",
277
+ },
278
+ componentName: {
279
+ type: "string",
280
+ description:
281
+ "Optional override for the identifier used in the test. Default: the component filename without extension.",
282
+ },
283
+ overwrite: { type: "boolean", default: false },
284
+ },
285
+ required: ["componentPath"],
286
+ },
287
+ },
288
+ {
289
+ name: "test_api_route",
290
+ description:
291
+ "Exercise a single API endpoint by operationId. Resolves the OpenAPI spec to method+path, substitutes path params, and issues a request. Defaults to the local mock API at http://localhost:3069/api — override via baseUrl to hit staging/develop. Returns status, headers, parsed body, and duration.",
292
+ inputSchema: {
293
+ type: "object",
294
+ properties: {
295
+ operationId: {
296
+ type: "string",
297
+ description: "OpenAPI operationId (e.g. 'attendees.index').",
298
+ },
299
+ pathParams: {
300
+ type: "object",
301
+ description:
302
+ "Values for {param} / :param segments in the path, keyed by parameter name.",
303
+ },
304
+ query: {
305
+ type: "object",
306
+ description:
307
+ "Query-string values keyed by parameter name. Arrays send repeated keys.",
308
+ },
309
+ body: {
310
+ description: "Request body. Sent as JSON for non-GET/HEAD methods.",
311
+ },
312
+ headers: {
313
+ type: "object",
314
+ description: "Additional headers to include.",
315
+ },
316
+ baseUrl: {
317
+ type: "string",
318
+ description:
319
+ "Override the base URL. Default: http://localhost:3069/api (the toolkit's mock API mount).",
320
+ },
321
+ timeoutMs: {
322
+ type: "integer",
323
+ description: "Abort after N ms. Default 10000.",
324
+ },
325
+ },
326
+ required: ["operationId"],
327
+ },
328
+ },
329
+ ]
330
+
331
+ async function handleTestToolCall(name, args = {}) {
332
+ switch (name) {
333
+ case "test_scaffold_component_test":
334
+ return contentResult(scaffoldComponentTest(args))
335
+
336
+ case "test_api_route":
337
+ return contentResult(await testApiRoute(args))
338
+
339
+ default:
340
+ throw new Error(`Unknown test tool: ${name}`)
341
+ }
342
+ }
343
+
344
+ function isTestTool(name) {
345
+ return TEST_TOOLS.some((t) => t.name === name)
346
+ }
347
+
348
+ module.exports = {
349
+ TEST_TOOLS,
350
+ handleTestToolCall,
351
+ isTestTool,
352
+ // Exported for testing
353
+ scaffoldComponentTest,
354
+ substitutePathParams,
355
+ buildQueryString,
356
+ findOperation,
357
+ testApiRoute,
358
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gxp-dev/tools",
3
- "version": "2.0.71",
3
+ "version": "2.0.73",
4
4
  "description": "Dev tools to create platform plugins",
5
5
  "type": "commonjs",
6
6
  "publishConfig": {
@@ -60,6 +60,8 @@
60
60
  "@gramercytech/gx-componentkit": "^1.0.23",
61
61
  "@vitejs/plugin-vue": "^6.0.6",
62
62
  "adm-zip": "^0.5.17",
63
+ "ajv": "^8.18.0",
64
+ "ajv-formats": "^3.0.1",
63
65
  "chrome-launcher": "^1.2.1",
64
66
  "concurrently": "^9.2.1",
65
67
  "cors": "^2.8.6",
@@ -244,8 +244,8 @@ export const useGxpStore = defineStore("gxp-portal-app", () => {
244
244
 
245
245
  if (manifest.dependencies && Array.isArray(manifest.dependencies)) {
246
246
  dependencies.value = manifest.dependencies // Store full dependency objects
247
- dependencyList.value = manifest.dependencies.reduce((acc, dependency) => {
248
- acc[dependency.identifier] = "1"
247
+ dependencyList.value = manifest.dependencies.reduce((acc, permission) => {
248
+ acc[permission.identifier] = "1"
249
249
  return acc
250
250
  }, {})
251
251
  console.log("[GxP Store] Dependency List:", dependencyList.value)
@@ -379,58 +379,16 @@ export const useGxpStore = defineStore("gxp-portal-app", () => {
379
379
 
380
380
  // Initialize dependency-based sockets based on the new structure
381
381
  if (Array.isArray(dependencies.value)) {
382
- dependencies.value.forEach((dependency) => {
383
- if (
384
- dependency.operations &&
385
- Object.keys(dependency.operations).length > 0
386
- ) {
387
- // Object.keys(dependency.operations).forEach((operation) => {
388
- // if (
389
- // Object.keys(apiOperations.value[dependency.identifier]).every(
390
- // (key) =>
391
- // [
392
- // "identifier",
393
- // "model",
394
- // "permissionKey",
395
- // "operations",
396
- // ].includes(key)
397
- // )
398
- // ) {
399
- // let method = "get";
400
- // let path = dependency.operations[operation];
401
- // if (path.includes(":")) {
402
- // let pathSplit = path.split(":");
403
- // method = pathSplit[0];
404
- // path = pathSplit[1];
405
- // }
406
- // path = path.replace(
407
- // "{teamSlug}/{projectSlug}",
408
- // pluginVars.value.projectId
409
- // );
410
- // path = path.replace(
411
- // `{${dependency.permissionKey}}`,
412
- // dependencyList.value[dependency.identifier]
413
- // );
414
- // if (!apiOperations.value[dependency.identifier]) {
415
- // apiOperations.value[dependency.identifier] = {};
416
- // }
417
- // apiOperations.value[dependency.identifier][operation] = {
418
- // method: method,
419
- // path: path,
420
- // model_key: dependency.permissionKey,
421
- // };
422
- // }
423
- // });
424
- }
425
- if (dependency.events && Object.keys(dependency.events).length > 0) {
382
+ dependencies.value.forEach((permission) => {
383
+ if (permission.events && Object.keys(permission.events).length > 0) {
426
384
  // Create socket listeners for each event type
427
- sockets[dependency.identifier] = {}
385
+ sockets[permission.identifier] = {}
428
386
 
429
- Object.keys(dependency.events).forEach((eventType) => {
430
- const eventName = dependency.events[eventType]
431
- const channel = `private.${dependency.model}.${dependency.identifier}`
387
+ Object.keys(permission.events).forEach((eventType) => {
388
+ const eventName = permission.events[eventType]
389
+ const channel = `private.${permission.model}.${permission.identifier}`
432
390
 
433
- sockets[dependency.identifier][eventType] = {
391
+ sockets[permission.identifier][eventType] = {
434
392
  listen: function (callback) {
435
393
  // Listen for the specific event on the primary socket
436
394
  return primarySocket.on(eventName, (data) => {
@@ -445,7 +403,7 @@ export const useGxpStore = defineStore("gxp-portal-app", () => {
445
403
  })
446
404
  } else {
447
405
  // For dependencies without events, create empty listeners
448
- sockets[dependency.identifier] = {
406
+ sockets[permission.identifier] = {
449
407
  created: { listen: () => () => {} },
450
408
  updated: { listen: () => () => {} },
451
409
  deleted: { listen: () => () => {} },
@@ -500,7 +458,7 @@ export const useGxpStore = defineStore("gxp-portal-app", () => {
500
458
  throw new Error(`DELETE ${endpoint}: ${error.message}`)
501
459
  }
502
460
  }
503
- async function callApi(operationId, identifier, data = {}) {
461
+ async function callApi(operationId, permissionIdentifier, data = {}) {
504
462
  // Initialize operations if not done
505
463
  if (Object.keys(apiOperations.value).length === 0) {
506
464
  await initializeApiOperations()
@@ -520,7 +478,7 @@ export const useGxpStore = defineStore("gxp-portal-app", () => {
520
478
 
521
479
  // Build context parameters from multiple sources:
522
480
  // 1. Auto-inject teamSlug and projectSlug from portal context
523
- // 2. Look up identifier value from dependencyList (if identifier provided)
481
+ // 2. Look up permissionIdentifier value from dependencyList (if permissionIdentifier provided)
524
482
  // 3. Merge in additional data parameters
525
483
 
526
484
  let projectTeamId = pluginVars.value?.projectId?.split("/")
@@ -537,14 +495,19 @@ export const useGxpStore = defineStore("gxp-portal-app", () => {
537
495
  if (parameters.includes("form") && pluginVars.value?.formId) {
538
496
  contextParams["form"] = pluginVars.value?.formId
539
497
  }
540
- // If identifier is provided, look up its value from dependencyList
541
- // dependencyList stores parent object IDs as { 'identifier': idValue }
542
- if (identifier !== null && identifier !== undefined) {
543
- const identifierValue = dependencyList.value?.[identifier]
544
- if (identifierValue !== undefined) {
545
- // Add the identifier value using the identifier key as the param name
546
- // e.g., identifier='form' with dependencyList.form='quiz-123' adds { form: 'quiz-123' }
547
- contextParams[identifier] = identifierValue
498
+ // If permissionIdentifier is provided, look up its value from dependencyList
499
+ // dependencyList stores parent object IDs as { 'permissionIdentifier': idValue }
500
+ if (
501
+ permissionIdentifier !== null &&
502
+ permissionIdentifier !== undefined &&
503
+ permissionIdentifier !== "project"
504
+ ) {
505
+ const permissionIdentifierValue =
506
+ dependencyList.value?.[permissionIdentifier]
507
+ if (permissionIdentifierValue !== undefined) {
508
+ // Add the permissionIdentifier value using the permissionIdentifier key as the param name
509
+ // e.g., permissionIdentifier='form' with dependencyList.form='quiz-123' adds { form: 'quiz-123' }
510
+ contextParams[permissionIdentifier] = permissionIdentifierValue
548
511
  }
549
512
  }
550
513
  const parsedData = {}
@@ -582,9 +545,9 @@ export const useGxpStore = defineStore("gxp-portal-app", () => {
582
545
  for (const param of parameters) {
583
546
  delete bodyData[param]
584
547
  }
585
- // Also remove identifier from body if it was in data
586
- if (identifier && bodyData[identifier] !== undefined) {
587
- delete bodyData[identifier]
548
+ // Also remove permissionIdentifier from body if it was in data
549
+ if (permissionIdentifier && bodyData[permissionIdentifier] !== undefined) {
550
+ delete bodyData[permissionIdentifier]
588
551
  }
589
552
 
590
553
  try {
@@ -611,20 +574,6 @@ export const useGxpStore = defineStore("gxp-portal-app", () => {
611
574
  )
612
575
  throw new Error(`${method.toUpperCase()} ${resolvedPath}: ${message}`)
613
576
  }
614
-
615
- // try {
616
- // const operationConfig = apiOperations.value[identifier][operation];
617
- // if (!operationConfig) {
618
- // throw new Error(`Operation not found: ${operation}`);
619
- // }
620
- // const response = await apiClient[operationConfig.method](
621
- // operationConfig.path,
622
- // data
623
- // );
624
- // return response.data;
625
- // } catch (error) {
626
- // throw new Error(`${method} ${endpoint}: ${error.message}`);
627
- // }
628
577
  }
629
578
 
630
579
  // Utility methods
@@ -643,8 +592,8 @@ export const useGxpStore = defineStore("gxp-portal-app", () => {
643
592
  function getState(key, fallback = null) {
644
593
  return triggerState.value[key] || fallback
645
594
  }
646
- function findDependency(identifier) {
647
- return dependencyList.value[identifier]
595
+ function findDependency(permissionIdentifier) {
596
+ return dependencyList.value[permissionIdentifier]
648
597
  }
649
598
  function hasPermission(flag) {
650
599
  return permissionFlags.value.includes(flag)
@@ -661,13 +610,65 @@ export const useGxpStore = defineStore("gxp-portal-app", () => {
661
610
  }
662
611
 
663
612
  // Standard Socket helper methods
664
- function listen(socketName, event, callback) {
665
- if (sockets[socketName] && sockets[socketName].listen) {
666
- return sockets[socketName].listen(event, callback)
667
- } else {
668
- console.warn(`Socket not found: ${socketName}`)
669
- return () => {}
613
+ //
614
+ // Polymorphic supports two forms:
615
+ //
616
+ // 1. listen(socketName, event, callback)
617
+ // Subscribes to `event` on the named socket (e.g. 'primary' or a
618
+ // dependency identifier whose socket was initialized via
619
+ // initializeDependencySockets). This matches the legacy shape.
620
+ //
621
+ // 2. listen(eventName, permissionIdentifier, callback)
622
+ // Subscribes to an AsyncAPI-defined platform event on the primary
623
+ // socket, scoped to a permission identifier from dependencyList
624
+ // (or the reserved "project" identifier). Use this for events whose
625
+ // `x-triggered-by` matches a callApi operationId.
626
+ //
627
+ // Disambiguation: if arg1 names a registered socket we take form 1,
628
+ // otherwise we fall through to form 2.
629
+ function listen(arg1, arg2, arg3) {
630
+ const hasRegisteredSocket =
631
+ sockets[arg1] && typeof sockets[arg1].listen === "function"
632
+
633
+ if (hasRegisteredSocket && typeof arg3 === "function") {
634
+ return sockets[arg1].listen(arg2, arg3)
635
+ }
636
+
637
+ if (typeof arg3 === "function") {
638
+ const eventName = arg1
639
+ const permissionIdentifier = arg2
640
+ const callback = arg3
641
+ const primary = socketConnections.primary
642
+ if (!primary) {
643
+ console.warn(
644
+ "[GxP Store] listen(): primary socket not initialized",
645
+ )
646
+ return () => {}
647
+ }
648
+ if (
649
+ permissionIdentifier !== "project" &&
650
+ dependencyList.value?.[permissionIdentifier] === undefined
651
+ ) {
652
+ console.warn(
653
+ `[GxP Store] listen("${eventName}", "${permissionIdentifier}"): permission identifier not bound in dependencyList`,
654
+ )
655
+ }
656
+ const handler = (data) => {
657
+ try {
658
+ callback(data)
659
+ } catch (err) {
660
+ console.error(
661
+ `[GxP Store] listen callback error for ${eventName}:`,
662
+ err,
663
+ )
664
+ }
665
+ }
666
+ primary.on(eventName, handler)
667
+ return () => primary.off(eventName, handler)
670
668
  }
669
+
670
+ console.warn(`Socket not found: ${arg1}`)
671
+ return () => {}
671
672
  }
672
673
 
673
674
  function broadcast(socketName, event, data) {
@@ -320,9 +320,11 @@ export default defineConfig(async (ctx) => {
320
320
  "@/stores/gxpPortalConfigStore":
321
321
  "(window.useGxpStore || (() => { console.warn('useGxpStore not found on window, using fallback'); return {}; }))",
322
322
  },
323
- {
324
- include: ["src/**"],
325
- },
323
+ // No `include` filter — rewrite `from "vue"` / `from "pinia"`
324
+ // in every module of the final bundle, including transitive deps
325
+ // from node_modules (component libraries, etc.). Without this,
326
+ // deps' internal `import { h } from "vue"` leak through as bare
327
+ // specifiers and crash at runtime on the platform.
326
328
  ),
327
329
  // Custom request logging and CORS plugin
328
330
  {